All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.jme3.scene.plugins.blender.meshes.Face Maven / Gradle / Ivy

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