com.jme3.scene.plugins.blender.meshes.Face Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jme3-blender Show documentation
Show all versions of jme3-blender Show documentation
jMonkeyEngine is a 3D game engine for adventurous Java developers
The newest version!
package com.jme3.scene.plugins.blender.meshes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.math.FastMath;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
/**
* A class that represents a single face in the mesh. The face is a polygon. Its minimum count of
* vertices is = 3.
*
* @author Marcin Roguski (Kaelthas)
*/
public class Face implements Comparator {
private static final Logger LOGGER = Logger.getLogger(Face.class.getName());
/** The indexes loop of the face. */
private IndexesLoop indexes;
private List triangulatedFaces;
/** Indicates if the face is smooth or solid. */
private boolean smooth;
/** The material index of the face. */
private int materialNumber;
/** UV coordinate sets attached to the face. The key is the set name and value are the UV coords. */
private Map> faceUVCoords;
/** The vertex colors of the face. */
private List vertexColors;
/** The temporal mesh the face belongs to. */
private TemporalMesh temporalMesh;
/**
* Creates a complete face with all available data.
* @param indexes
* the indexes of the face (required)
* @param smooth
* indicates if the face is smooth or solid
* @param materialNumber
* the material index of the face
* @param faceUVCoords
* UV coordinate sets of the face (optional)
* @param vertexColors
* the vertex colors of the face (optional)
* @param temporalMesh
* the temporal mesh the face belongs to (required)
*/
public Face(Integer[] indexes, boolean smooth, int materialNumber, Map> faceUVCoords, List vertexColors, TemporalMesh temporalMesh) {
this.setTemporalMesh(temporalMesh);
this.indexes = new IndexesLoop(indexes);
this.smooth = smooth;
this.materialNumber = materialNumber;
this.faceUVCoords = faceUVCoords;
this.temporalMesh = temporalMesh;
this.vertexColors = vertexColors;
}
/**
* Default constructor. Used by the clone method.
*/
private Face() {
}
@Override
public Face clone() {
Face result = new Face();
result.indexes = indexes.clone();
result.smooth = smooth;
result.materialNumber = materialNumber;
if (faceUVCoords != null) {
result.faceUVCoords = new HashMap>(faceUVCoords.size());
for (Entry> entry : faceUVCoords.entrySet()) {
List uvs = new ArrayList(entry.getValue().size());
for (Vector2f v : entry.getValue()) {
uvs.add(v.clone());
}
result.faceUVCoords.put(entry.getKey(), uvs);
}
}
if (vertexColors != null) {
result.vertexColors = new ArrayList(vertexColors.size());
for (byte[] colors : vertexColors) {
result.vertexColors.add(colors.clone());
}
}
result.temporalMesh = temporalMesh;
return result;
}
/**
* Returns the index at the given position in the index loop. If the given position is negative or exceeds
* the amount of vertices - it is being looped properly so that it always hits an index.
* For example getIndex(-1) will return the index before the 0 - in this case it will be the last one.
* @param indexPosition
* the index position
* @return index value at the given position
*/
private Integer getIndex(int indexPosition) {
if (indexPosition >= indexes.size()) {
indexPosition = indexPosition % indexes.size();
} else if (indexPosition < 0) {
indexPosition = indexes.size() - -indexPosition % indexes.size();
}
return indexes.get(indexPosition);
}
/**
* @return the mesh this face belongs to
*/
public TemporalMesh getTemporalMesh() {
return temporalMesh;
}
/**
* @return the original indexes of the face
*/
public IndexesLoop getIndexes() {
return indexes;
}
/**
* @return the centroid of the face
*/
public Vector3f computeCentroid() {
Vector3f result = new Vector3f();
List vertices = temporalMesh.getVertices();
for (Integer index : indexes) {
result.addLocal(vertices.get(index));
}
return result.divideLocal(indexes.size());
}
/**
* @return current indexes of the face (if it is already triangulated then more than one index group will be in the result list)
*/
public List> getCurrentIndexes() {
if (triangulatedFaces == null) {
return Arrays.asList(indexes.getAll());
}
List> result = new ArrayList>(triangulatedFaces.size());
for (IndexesLoop loop : triangulatedFaces) {
result.add(loop.getAll());
}
return result;
}
/**
* The method detaches the triangle from the face. This method keeps the indexes loop normalized - every index
* has only two neighbours. So if detaching the triangle causes a vertex to have more than two neighbours - it is
* also detached and returned as a result.
* The result is an empty list if no such situation happens.
* @param triangleIndexes
* the indexes of a triangle to be detached
* @return a list of faces that need to be detached as well in order to keep them normalized
* @throws BlenderFileException
* an exception is thrown when vertices of a face create more than one loop; this is found during path finding
*/
private List detachTriangle(Integer[] triangleIndexes) throws BlenderFileException {
LOGGER.fine("Detaching triangle.");
if (triangleIndexes.length != 3) {
throw new IllegalArgumentException("Cannot detach triangle with that does not have 3 indexes!");
}
MeshHelper meshHelper = temporalMesh.getBlenderContext().getHelper(MeshHelper.class);
List detachedFaces = new ArrayList();
List path = new ArrayList(indexes.size());
boolean[] edgeRemoved = new boolean[] { indexes.removeEdge(triangleIndexes[0], triangleIndexes[1]), indexes.removeEdge(triangleIndexes[0], triangleIndexes[2]), indexes.removeEdge(triangleIndexes[1], triangleIndexes[2]) };
Integer[][] indexesPairs = new Integer[][] { new Integer[] { triangleIndexes[0], triangleIndexes[1] }, new Integer[] { triangleIndexes[0], triangleIndexes[2] }, new Integer[] { triangleIndexes[1], triangleIndexes[2] } };
for (int i = 0; i < 3; ++i) {
if (!edgeRemoved[i]) {
indexes.findPath(indexesPairs[i][0], indexesPairs[i][1], path);
if (path.size() == 0) {
indexes.findPath(indexesPairs[i][1], indexesPairs[i][0], path);
}
if (path.size() == 0) {
throw new IllegalStateException("Triangulation failed. Cannot find path between two indexes. Please apply triangulation in Blender as a workaround.");
}
if (detachedFaces.size() == 0 && path.size() < indexes.size()) {
Integer[] indexesSublist = path.toArray(new Integer[path.size()]);
detachedFaces.add(new Face(indexesSublist, smooth, materialNumber, meshHelper.selectUVSubset(this, indexesSublist), meshHelper.selectVertexColorSubset(this, indexesSublist), temporalMesh));
for (int j = 0; j < path.size() - 1; ++j) {
indexes.removeEdge(path.get(j), path.get(j + 1));
}
indexes.removeEdge(path.get(path.size() - 1), path.get(0));
} else {
indexes.addEdge(path.get(path.size() - 1), path.get(0));
}
}
}
return detachedFaces;
}
/**
* Sets the temporal mesh for the face. The given mesh cannot be null.
* @param temporalMesh
* the temporal mesh of the face
* @throws IllegalArgumentException
* thrown if given temporal mesh is null
*/
public void setTemporalMesh(TemporalMesh temporalMesh) {
if (temporalMesh == null) {
throw new IllegalArgumentException("No temporal mesh for the face given!");
}
this.temporalMesh = temporalMesh;
}
/**
* Flips the order of the indexes.
*/
public void flipIndexes() {
indexes.reverse();
if (faceUVCoords != null) {
for (Entry> entry : faceUVCoords.entrySet()) {
Collections.reverse(entry.getValue());
}
}
}
/**
* Flips UV coordinates.
* @param u
* indicates if U coords should be flipped
* @param v
* indicates if V coords should be flipped
*/
public void flipUV(boolean u, boolean v) {
if (faceUVCoords != null) {
for (Entry> entry : faceUVCoords.entrySet()) {
for (Vector2f uv : entry.getValue()) {
uv.set(u ? 1 - uv.x : uv.x, v ? 1 - uv.y : uv.y);
}
}
}
}
/**
* @return the UV sets of the face
*/
public Map> getUvSets() {
return faceUVCoords;
}
/**
* @return current vertex count of the face
*/
public int vertexCount() {
return indexes.size();
}
/**
* The method triangulates the face.
*/
public TriangulationWarning triangulate() {
LOGGER.fine("Triangulating face.");
assert indexes.size() >= 3 : "Invalid indexes amount for face. 3 is the required minimum!";
triangulatedFaces = new ArrayList(indexes.size() - 2);
Integer[] indexes = new Integer[3];
TriangulationWarning warning = TriangulationWarning.NONE;
try {
List facesToTriangulate = new ArrayList(Arrays.asList(this.clone()));
while (facesToTriangulate.size() > 0 && warning == TriangulationWarning.NONE) {
Face face = facesToTriangulate.remove(0);
// two special cases will improve the computations speed
if(face.getIndexes().size() == 3) {
triangulatedFaces.add(face.getIndexes().clone());
} else {
int previousIndex1 = -1, previousIndex2 = -1, previousIndex3 = -1;
while (face.vertexCount() > 0) {
indexes[0] = face.getIndex(0);
indexes[1] = face.findClosestVertex(indexes[0], -1);
indexes[2] = face.findClosestVertex(indexes[0], indexes[1]);
LOGGER.finer("Veryfying improper triangulation of the temporal mesh.");
if (indexes[0] < 0 || indexes[1] < 0 || indexes[2] < 0) {
warning = TriangulationWarning.CLOSEST_VERTS;
break;
}
if (previousIndex1 == indexes[0] && previousIndex2 == indexes[1] && previousIndex3 == indexes[2]) {
warning = TriangulationWarning.INFINITE_LOOP;
break;
}
previousIndex1 = indexes[0];
previousIndex2 = indexes[1];
previousIndex3 = indexes[2];
Arrays.sort(indexes, this);
facesToTriangulate.addAll(face.detachTriangle(indexes));
triangulatedFaces.add(new IndexesLoop(indexes));
}
}
}
} catch (BlenderFileException e) {
LOGGER.log(Level.WARNING, "Errors occurred during face triangulation: {0}. The face will be triangulated with the most direct algorithm, but the results might not be identical to blender.", e.getLocalizedMessage());
warning = TriangulationWarning.UNKNOWN;
}
if(warning != TriangulationWarning.NONE) {
LOGGER.finest("Triangulation the face using the most direct algorithm.");
indexes[0] = this.getIndex(0);
for (int i = 1; i < this.vertexCount() - 1; ++i) {
indexes[1] = this.getIndex(i);
indexes[2] = this.getIndex(i + 1);
triangulatedFaces.add(new IndexesLoop(indexes));
}
}
return warning;
}
/**
* A warning that indicates a problem with face triangulation. The warnings are collected and displayed once for each type for a mesh to
* avoid multiple warning loggings during triangulation. The amount of iterations can be really huge and logging every single failure would
* really slow down the importing process and make logs unreadable.
*
* @author Marcin Roguski (Kaelthas)
*/
public static enum TriangulationWarning {
NONE(null),
CLOSEST_VERTS("Unable to find two closest vertices while triangulating face."),
INFINITE_LOOP("Infinite loop detected during triangulation."),
UNKNOWN("There was an unknown problem with face triangulation. Please see log for details.");
private String description;
private TriangulationWarning(String description) {
this.description = description;
}
@Override
public String toString() {
return description;
}
}
/**
* @return true if the face is smooth and false otherwise
*/
public boolean isSmooth() {
return smooth;
}
/**
* @return the material index of the face
*/
public int getMaterialNumber() {
return materialNumber;
}
/**
* @return the vertices colord of the face
*/
public List getVertexColors() {
return vertexColors;
}
@Override
public String toString() {
return "Face " + indexes;
}
/**
* The method finds the closest vertex to the one specified by index.
* If the vertexToIgnore is positive than it will be ignored in the result.
* The closest vertex must be able to create an edge that is fully contained
* within the face and does not cross any other edges. Also if the
* vertexToIgnore is not negative then the condition that the edge between
* the found index and the one to ignore is inside the face must also be
* met.
*
* @param index
* the index of the vertex that needs to have found the nearest
* neighbour
* @param indexToIgnore
* the index to ignore in the result (pass -1 if none is to be
* ignored)
* @return the index of the closest vertex to the given one
*/
private int findClosestVertex(int index, int indexToIgnore) {
int result = -1;
List vertices = temporalMesh.getVertices();
Vector3f v1 = vertices.get(index);
float distance = Float.MAX_VALUE;
for (int i : indexes) {
if (i != index && i != indexToIgnore) {
Vector3f v2 = vertices.get(i);
float d = v2.distance(v1);
if (d < distance && this.contains(new Edge(index, i, 0, true, temporalMesh)) && (indexToIgnore < 0 || this.contains(new Edge(indexToIgnore, i, 0, true, temporalMesh)))) {
result = i;
distance = d;
}
}
}
return result;
}
/**
* The method verifies if the edge is contained within the face.
* It means it cannot cross any other edge and it must be inside the face and not outside of it.
* @param edge
* the edge to be checked
* @return true if the given edge is contained within the face and false otherwise
*/
private boolean contains(Edge edge) {
int index1 = edge.getFirstIndex();
int index2 = edge.getSecondIndex();
// check if the line between the vertices is not a border edge of the face
if (!indexes.areNeighbours(index1, index2)) {
for (int i = 0; i < indexes.size(); ++i) {
int i1 = this.getIndex(i - 1);
int i2 = this.getIndex(i);
// check if the edges have no common verts (because if they do, they cannot cross)
if (i1 != index1 && i1 != index2 && i2 != index1 && i2 != index2) {
if (edge.cross(new Edge(i1, i2, 0, false, temporalMesh))) {
return false;
}
}
}
// computing the edge's middle point
Vector3f edgeMiddlePoint = edge.computeCentroid();
// computing the edge that is perpendicular to the given edge and has a length of 1 (length actually does not matter)
Vector3f edgeVector = edge.getSecondVertex().subtract(edge.getFirstVertex());
Vector3f edgeNormal = temporalMesh.getNormals().get(index1).cross(edgeVector).normalizeLocal();
Edge e = new Edge(edgeMiddlePoint, edgeNormal.add(edgeMiddlePoint));
// compute the vectors from the middle point to the crossing between the extended edge 'e' and other edges of the face
List crossingVectors = new ArrayList();
for (int i = 0; i < indexes.size(); ++i) {
int i1 = this.getIndex(i);
int i2 = this.getIndex(i + 1);
Vector3f crossPoint = e.getCrossPoint(new Edge(i1, i2, 0, false, temporalMesh), true, false);
if(crossPoint != null) {
crossingVectors.add(crossPoint.subtractLocal(edgeMiddlePoint));
}
}
if(crossingVectors.size() == 0) {
return false;// edges do not cross
}
// use only distinct vertices (doubles may appear if the crossing point is a vertex)
List distinctCrossingVectors = new ArrayList();
for(Vector3f cv : crossingVectors) {
double minDistance = Double.MAX_VALUE;
for(Vector3f dcv : distinctCrossingVectors) {
minDistance = Math.min(minDistance, dcv.distance(cv));
}
if(minDistance > FastMath.FLT_EPSILON) {
distinctCrossingVectors.add(cv);
}
}
if(distinctCrossingVectors.size() == 0) {
throw new IllegalStateException("There MUST be at least 2 crossing vertices!");
}
// checking if all crossing vectors point to the same direction (if yes then the edge is outside the face)
float direction = Math.signum(distinctCrossingVectors.get(0).dot(edgeNormal));// if at least one vector has different direction that this - it means that the edge is inside the face
for(int i=1;i loadAll(Structure meshStructure, Map> userUVGroups, List verticesColors, TemporalMesh temporalMesh, BlenderContext blenderContext) throws BlenderFileException {
LOGGER.log(Level.FINE, "Loading all faces from mesh: {0}", meshStructure.getName());
List result = new ArrayList();
MeshHelper meshHelper = blenderContext.getHelper(MeshHelper.class);
if (meshHelper.isBMeshCompatible(meshStructure)) {
LOGGER.fine("Reading BMesh.");
Pointer pMLoop = (Pointer) meshStructure.getFieldValue("mloop");
Pointer pMPoly = (Pointer) meshStructure.getFieldValue("mpoly");
if (pMPoly.isNotNull() && pMLoop.isNotNull()) {
List polys = pMPoly.fetchData();
List loops = pMLoop.fetchData();
for (Structure poly : polys) {
int materialNumber = ((Number) poly.getFieldValue("mat_nr")).intValue();
int loopStart = ((Number) poly.getFieldValue("loopstart")).intValue();
int totLoop = ((Number) poly.getFieldValue("totloop")).intValue();
boolean smooth = (((Number) poly.getFieldValue("flag")).byteValue() & 0x01) != 0x00;
Integer[] vertexIndexes = new Integer[totLoop];
for (int i = loopStart; i < loopStart + totLoop; ++i) {
vertexIndexes[i - loopStart] = ((Number) loops.get(i).getFieldValue("v")).intValue();
}
// uvs always must be added wheater we have texture or not
Map> uvCoords = new HashMap>();
for (Entry> entry : userUVGroups.entrySet()) {
List uvs = entry.getValue().subList(loopStart, loopStart + totLoop);
uvCoords.put(entry.getKey(), new ArrayList(uvs));
}
List vertexColors = null;
if (verticesColors != null && verticesColors.size() > 0) {
vertexColors = new ArrayList(totLoop);
for (int i = loopStart; i < loopStart + totLoop; ++i) {
vertexColors.add(verticesColors.get(i));
}
}
result.add(new Face(vertexIndexes, smooth, materialNumber, uvCoords, vertexColors, temporalMesh));
}
}
} else {
LOGGER.fine("Reading traditional faces.");
Pointer pMFace = (Pointer) meshStructure.getFieldValue("mface");
List mFaces = pMFace.isNotNull() ? pMFace.fetchData() : null;
if (mFaces != null && mFaces.size() > 0) {
// indicates if the material with the specified number should have a texture attached
for (int i = 0; i < mFaces.size(); ++i) {
Structure mFace = mFaces.get(i);
int materialNumber = ((Number) mFace.getFieldValue("mat_nr")).intValue();
boolean smooth = (((Number) mFace.getFieldValue("flag")).byteValue() & 0x01) != 0x00;
int v1 = ((Number) mFace.getFieldValue("v1")).intValue();
int v2 = ((Number) mFace.getFieldValue("v2")).intValue();
int v3 = ((Number) mFace.getFieldValue("v3")).intValue();
int v4 = ((Number) mFace.getFieldValue("v4")).intValue();
int vertCount = v4 == 0 ? 3 : 4;
// uvs always must be added wheater we have texture or not
Map> faceUVCoords = new HashMap>();
for (Entry> entry : userUVGroups.entrySet()) {
List uvCoordsForASingleFace = new ArrayList(vertCount);
for (int j = 0; j < vertCount; ++j) {
uvCoordsForASingleFace.add(entry.getValue().get(i * 4 + j));
}
faceUVCoords.put(entry.getKey(), uvCoordsForASingleFace);
}
List vertexColors = null;
if (verticesColors != null && verticesColors.size() > 0) {
vertexColors = new ArrayList(vertCount);
vertexColors.add(verticesColors.get(v1));
vertexColors.add(verticesColors.get(v2));
vertexColors.add(verticesColors.get(v3));
if (vertCount == 4) {
vertexColors.add(verticesColors.get(v4));
}
}
result.add(new Face(vertCount == 4 ? new Integer[] { v1, v2, v3, v4 } : new Integer[] { v1, v2, v3 }, smooth, materialNumber, faceUVCoords, vertexColors, temporalMesh));
}
}
}
LOGGER.log(Level.FINE, "Loaded {0} faces.", result.size());
return result;
}
@Override
public int compare(Integer index1, Integer index2) {
return indexes.indexOf(index1) - indexes.indexOf(index2);
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy