package com.jme3.scene.plugins.blender.meshes; import java.nio.IntBuffer; import java.nio.ShortBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import com.jme3.bounding.BoundingBox; import com.jme3.bounding.BoundingVolume; import com.jme3.material.Material; import com.jme3.material.RenderState.FaceCullMode; import com.jme3.math.FastMath; import com.jme3.math.Vector2f; import com.jme3.math.Vector3f; import com.jme3.scene.Geometry; import com.jme3.scene.Mesh; import com.jme3.scene.Mesh.Mode; import com.jme3.scene.Node; import com.jme3.scene.VertexBuffer; import com.jme3.scene.VertexBuffer.Type; import com.jme3.scene.VertexBuffer.Usage; import com.jme3.scene.plugins.blender.BlenderContext; import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType; import com.jme3.scene.plugins.blender.file.BlenderFileException; import com.jme3.scene.plugins.blender.file.Structure; import com.jme3.scene.plugins.blender.materials.MaterialContext; import com.jme3.scene.plugins.blender.meshes.Face.TriangulationWarning; import com.jme3.scene.plugins.blender.meshes.MeshBuffers.BoneBuffersData; import com.jme3.scene.plugins.blender.modifiers.Modifier; import com.jme3.scene.plugins.blender.objects.Properties; /** * The class extends Geometry so that it can be temporalily added to the object's node. * Later each such node's child will be transformed into a list of geometries. * * @author Marcin Roguski (Kaelthas) */ public class TemporalMesh extends Geometry { private static final Logger LOGGER = Logger.getLogger(TemporalMesh.class.getName()); /** A minimum weight value. */ private static final double MINIMUM_BONE_WEIGHT = FastMath.DBL_EPSILON; /** The blender context. */ protected final BlenderContext blenderContext; /** The mesh's structure. */ protected final Structure meshStructure; /** Loaded vertices. */ protected List<Vector3f> vertices = new ArrayList<Vector3f>(); /** Loaded normals. */ protected List<Vector3f> normals = new ArrayList<Vector3f>(); /** Loaded vertex groups. */ protected List<Map<String, Float>> vertexGroups = new ArrayList<Map<String, Float>>(); /** Loaded vertex colors. */ protected List<byte[]> verticesColors = new ArrayList<byte[]>(); /** Materials used by the mesh. */ protected MaterialContext[] materials; /** The properties of the mesh. */ protected Properties properties; /** The bone indexes. */ protected Map<String, Integer> boneIndexes = new HashMap<String, Integer>(); /** The modifiers that should be applied after the mesh has been created. */ protected List<Modifier> postMeshCreationModifiers = new ArrayList<Modifier>(); /** The faces of the mesh. */ protected List<Face> faces = new ArrayList<Face>(); /** The edges of the mesh. */ protected List<Edge> edges = new ArrayList<Edge>(); /** The points of the mesh. */ protected List<Point> points = new ArrayList<Point>(); /** A map between index and faces that contain it (for faster index - face queries). */ protected Map<Integer, Set<Face>> indexToFaceMapping = new HashMap<Integer, Set<Face>>(); /** A map between index and edges that contain it (for faster index - edge queries). */ protected Map<Integer, Set<Edge>> indexToEdgeMapping = new HashMap<Integer, Set<Edge>>(); /** The bounding box of the temporal mesh. */ protected BoundingBox boundingBox; /** * Creates a temporal mesh based on the given mesh structure. * @param meshStructure * the mesh structure * @param blenderContext * the blender context * @throws BlenderFileException * an exception is thrown when problems with file reading occur */ public TemporalMesh(Structure meshStructure, BlenderContext blenderContext) throws BlenderFileException { this(meshStructure, blenderContext, true); } /** * Creates a temporal mesh based on the given mesh structure. * @param meshStructure * the mesh structure * @param blenderContext * the blender context * @param loadData * tells if the data should be loaded from the mesh structure * @throws BlenderFileException * an exception is thrown when problems with file reading occur */ protected TemporalMesh(Structure meshStructure, BlenderContext blenderContext, boolean loadData) throws BlenderFileException { this.blenderContext = blenderContext; this.meshStructure = meshStructure; if (loadData) { name = meshStructure.getName(); MeshHelper meshHelper = blenderContext.getHelper(MeshHelper.class); meshHelper.loadVerticesAndNormals(meshStructure, vertices, normals); verticesColors = meshHelper.loadVerticesColors(meshStructure, blenderContext); LinkedHashMap<String, List<Vector2f>> userUVGroups = meshHelper.loadUVCoordinates(meshStructure); vertexGroups = meshHelper.loadVerticesGroups(meshStructure); faces = Face.loadAll(meshStructure, userUVGroups, verticesColors, this, blenderContext); edges = Edge.loadAll(meshStructure, this); points = Point.loadAll(meshStructure); this.rebuildIndexesMappings(); } } /** * @return the blender context */ public BlenderContext getBlenderContext() { return blenderContext; } /** * @return the vertices of the mesh */ public List<Vector3f> getVertices() { return vertices; } /** * @return the normals of the mesh */ public List<Vector3f> getNormals() { return normals; } /** * @return all faces */ public List<Face> getFaces() { return faces; } /** * @return all edges */ public List<Edge> getEdges() { return edges; } /** * @return all points (do not mistake it with vertices) */ public List<Point> getPoints() { return points; } /** * @return all vertices colors */ public List<byte[]> getVerticesColors() { return verticesColors; } /** * @return all vertex groups for the vertices (each map has groups for the proper vertex) */ public List<Map<String, Float>> getVertexGroups() { return vertexGroups; } /** * @return the faces that contain the given index or null if none contain it */ public Collection<Face> getAdjacentFaces(Integer index) { return indexToFaceMapping.get(index); } /** * @param the * edge of the mesh * @return a list of faces that contain the given edge or an empty list */ public Collection<Face> getAdjacentFaces(Edge edge) { List<Face> result = new ArrayList<Face>(indexToFaceMapping.get(edge.getFirstIndex())); Set<Face> secondIndexAdjacentFaces = indexToFaceMapping.get(edge.getSecondIndex()); if (secondIndexAdjacentFaces != null) { result.retainAll(indexToFaceMapping.get(edge.getSecondIndex())); } return result; } /** * @param the * index of the mesh * @return a list of edges that contain the index */ public Collection<Edge> getAdjacentEdges(Integer index) { return indexToEdgeMapping.get(index); } /** * Tells if the given edge is a boundary edge. The boundary edge means that it belongs to a single * face or to none. * @param the * edge of the mesh * @return <b>true</b> if the edge is a boundary one and <b>false</b> otherwise */ public boolean isBoundary(Edge edge) { return this.getAdjacentFaces(edge).size() <= 1; } /** * The method tells if the given index is a boundary index. A boundary index belongs to at least * one boundary edge. * @param index * the index of the mesh * @return <b>true</b> if the index is a boundary one and <b>false</b> otherwise */ public boolean isBoundary(Integer index) { Collection<Edge> adjacentEdges = this.getAdjacentEdges(index); for (Edge edge : adjacentEdges) { if (this.isBoundary(edge)) { return true; } } return false; } @Override public TemporalMesh clone() { try { TemporalMesh result = new TemporalMesh(meshStructure, blenderContext, false); result.name = name; for (Vector3f v : vertices) { result.vertices.add(v.clone()); } for (Vector3f n : normals) { result.normals.add(n.clone()); } for (Map<String, Float> group : vertexGroups) { result.vertexGroups.add(new HashMap<String, Float>(group)); } for (byte[] vertColor : verticesColors) { result.verticesColors.add(vertColor.clone()); } result.materials = materials; result.properties = properties; result.boneIndexes.putAll(boneIndexes); result.postMeshCreationModifiers.addAll(postMeshCreationModifiers); for (Face face : faces) { result.faces.add(face.clone()); } for (Edge edge : edges) { result.edges.add(edge.clone()); } for (Point point : points) { result.points.add(point.clone()); } result.rebuildIndexesMappings(); return result; } catch (BlenderFileException e) { LOGGER.log(Level.SEVERE, "Error while cloning the temporal mesh: {0}. Returning null.", e.getLocalizedMessage()); } return null; } /** * The method rebuilds the mappings between faces and edges. Should be called after * every major change of the temporal mesh done outside it. * @note I will remove this method soon and make the mappings to be done automatically * when the mesh is modified. */ public void rebuildIndexesMappings() { indexToEdgeMapping.clear(); indexToFaceMapping.clear(); for (Face face : faces) { for (Integer index : face.getIndexes()) { Set<Face> faces = indexToFaceMapping.get(index); if (faces == null) { faces = new HashSet<Face>(); indexToFaceMapping.put(index, faces); } faces.add(face); } } for (Edge edge : edges) { Set<Edge> edges = indexToEdgeMapping.get(edge.getFirstIndex()); if (edges == null) { edges = new HashSet<Edge>(); indexToEdgeMapping.put(edge.getFirstIndex(), edges); } edges.add(edge); edges = indexToEdgeMapping.get(edge.getSecondIndex()); if (edges == null) { edges = new HashSet<Edge>(); indexToEdgeMapping.put(edge.getSecondIndex(), edges); } edges.add(edge); } } @Override public void updateModelBound() { if (boundingBox == null) { boundingBox = new BoundingBox(); } Vector3f min = new Vector3f(Float.MAX_VALUE, Float.MAX_VALUE, Float.MAX_VALUE); Vector3f max = new Vector3f(Float.MIN_VALUE, Float.MIN_VALUE, Float.MIN_VALUE); for (Vector3f v : vertices) { min.set(Math.min(min.x, v.x), Math.min(min.y, v.y), Math.min(min.z, v.z)); max.set(Math.max(max.x, v.x), Math.max(max.y, v.y), Math.max(max.z, v.z)); } boundingBox.setMinMax(min, max); } @Override public BoundingVolume getModelBound() { this.updateModelBound(); return boundingBox; } @Override public BoundingVolume getWorldBound() { this.updateModelBound(); Node parent = this.getParent(); if (parent != null) { BoundingVolume bv = boundingBox.clone(); bv.setCenter(parent.getWorldTranslation()); return bv; } else { return boundingBox; } } /** * Triangulates the mesh. */ public void triangulate() { Set<TriangulationWarning> warnings = new HashSet<>(TriangulationWarning.values().length - 1); LOGGER.fine("Triangulating temporal mesh."); for (Face face : faces) { TriangulationWarning warning = face.triangulate(); if(warning != TriangulationWarning.NONE) { warnings.add(warning); } } if(warnings.size() > 0 && LOGGER.isLoggable(Level.WARNING)) { StringBuilder sb = new StringBuilder(512); sb.append("There were problems with triangulating the faces of a mesh: ").append(name); for(TriangulationWarning w : warnings) { sb.append("\n\t").append(w); } LOGGER.warning(sb.toString()); } } /** * The method appends the given mesh to the current one. New faces and vertices and indexes are added. * @param mesh * the mesh to be appended */ public void append(TemporalMesh mesh) { if (mesh != null) { // we need to shift the indexes in faces, lines and points int shift = vertices.size(); if (shift > 0) { for (Face face : mesh.faces) { face.getIndexes().shiftIndexes(shift, null); face.setTemporalMesh(this); } for (Edge edge : mesh.edges) { edge.shiftIndexes(shift, null); } for (Point point : mesh.points) { point.shiftIndexes(shift, null); } } faces.addAll(mesh.faces); edges.addAll(mesh.edges); points.addAll(mesh.points); vertices.addAll(mesh.vertices); normals.addAll(mesh.normals); vertexGroups.addAll(mesh.vertexGroups); verticesColors.addAll(mesh.verticesColors); boneIndexes.putAll(mesh.boneIndexes); this.rebuildIndexesMappings(); } } /** * Sets the properties of the mesh. * @param properties * the properties of the mesh */ public void setProperties(Properties properties) { this.properties = properties; } /** * Sets the materials of the mesh. * @param materials * the materials of the mesh */ public void setMaterials(MaterialContext[] materials) { this.materials = materials; } /** * Adds bone index to the mesh. * @param boneName * the name of the bone * @param boneIndex * the index of the bone */ public void addBoneIndex(String boneName, Integer boneIndex) { boneIndexes.put(boneName, boneIndex); } /** * The modifier to be applied after the geometries are created. * @param modifier * the modifier to be applied */ public void applyAfterMeshCreate(Modifier modifier) { postMeshCreationModifiers.add(modifier); } @Override public int getVertexCount() { return vertices.size(); } /** * Removes all vertices from the mesh. */ public void clear() { vertices.clear(); normals.clear(); vertexGroups.clear(); verticesColors.clear(); faces.clear(); edges.clear(); points.clear(); indexToEdgeMapping.clear(); indexToFaceMapping.clear(); } /** * The mesh builds geometries from the mesh. The result is stored in the blender context * under the mesh's OMA. */ public void toGeometries() { LOGGER.log(Level.FINE, "Converting temporal mesh {0} to jme geometries.", name); List<Geometry> result = new ArrayList<Geometry>(); MeshHelper meshHelper = blenderContext.getHelper(MeshHelper.class); Node parent = this.getParent(); parent.detachChild(this); this.prepareFacesGeometry(result, meshHelper); this.prepareLinesGeometry(result, meshHelper); this.preparePointsGeometry(result, meshHelper); blenderContext.addLoadedFeatures(meshStructure.getOldMemoryAddress(), LoadedDataType.FEATURE, result); for (Geometry geometry : result) { parent.attachChild(geometry); } for (Modifier modifier : postMeshCreationModifiers) { modifier.postMeshCreationApply(parent, blenderContext); } } /** * The method creates geometries from faces. * @param result * the list where new geometries will be appended * @param meshHelper * the mesh helper */ protected void prepareFacesGeometry(List<Geometry> result, MeshHelper meshHelper) { LOGGER.fine("Preparing faces geometries."); this.triangulate(); Vector3f[] tempVerts = new Vector3f[3]; Vector3f[] tempNormals = new Vector3f[3]; byte[][] tempVertColors = new byte[3][]; List<Map<Float, Integer>> boneBuffers = new ArrayList<Map<Float, Integer>>(3); LOGGER.log(Level.FINE, "Appending {0} faces to mesh buffers.", faces.size()); Map<Integer, MeshBuffers> faceMeshes = new HashMap<Integer, MeshBuffers>(); for (Face face : faces) { MeshBuffers meshBuffers = faceMeshes.get(face.getMaterialNumber()); if (meshBuffers == null) { meshBuffers = new MeshBuffers(face.getMaterialNumber()); faceMeshes.put(face.getMaterialNumber(), meshBuffers); } List<List<Integer>> triangulatedIndexes = face.getCurrentIndexes(); List<byte[]> vertexColors = face.getVertexColors(); for (List<Integer> indexes : triangulatedIndexes) { assert indexes.size() == 3 : "The mesh has not been properly triangulated!"; Vector3f normal = null; if(!face.isSmooth()) { normal = FastMath.computeNormal(vertices.get(indexes.get(0)), vertices.get(indexes.get(1)), vertices.get(indexes.get(2))); } boneBuffers.clear(); for (int i = 0; i < 3; ++i) { int vertIndex = indexes.get(i); tempVerts[i] = vertices.get(vertIndex); tempNormals[i] = normal != null ? normal : normals.get(vertIndex); tempVertColors[i] = vertexColors != null ? vertexColors.get(face.getIndexes().indexOf(vertIndex)) : null; if (boneIndexes.size() > 0 && vertexGroups.size() > 0) { Map<Float, Integer> boneBuffersForVertex = new HashMap<Float, Integer>(); Map<String, Float> vertexGroupsForVertex = vertexGroups.get(vertIndex); for (Entry<String, Integer> entry : boneIndexes.entrySet()) { if (vertexGroupsForVertex.containsKey(entry.getKey())) { float weight = vertexGroupsForVertex.get(entry.getKey()); if (weight > MINIMUM_BONE_WEIGHT) { // only values of weight greater than MINIMUM_BONE_WEIGHT are used // if all non zero weights were used, and they were samm enough, problems with normalisation would occur // because adding a very small value to 1.0 will give 1.0 // so in order to avoid such errors, which can cause severe animation artifacts we need to use some minimum weight value boneBuffersForVertex.put(weight, entry.getValue()); } } } if (boneBuffersForVertex.size() == 0) {// attach the vertex to zero-indexed bone so that it does not collapse to (0, 0, 0) boneBuffersForVertex.put(1.0f, 0); } boneBuffers.add(boneBuffersForVertex); } } Map<String, List<Vector2f>> uvs = meshHelper.selectUVSubset(face, indexes.toArray(new Integer[indexes.size()])); meshBuffers.append(face.isSmooth(), tempVerts, tempNormals, uvs, tempVertColors, boneBuffers); } } LOGGER.fine("Converting mesh buffers to geometries."); Map<Geometry, MeshBuffers> geometryToBuffersMap = new HashMap<Geometry, MeshBuffers>(); for (Entry<Integer, MeshBuffers> entry : faceMeshes.entrySet()) { MeshBuffers meshBuffers = entry.getValue(); Mesh mesh = new Mesh(); if (meshBuffers.isShortIndexBuffer()) { mesh.setBuffer(Type.Index, 1, (ShortBuffer) meshBuffers.getIndexBuffer()); } else { mesh.setBuffer(Type.Index, 1, (IntBuffer) meshBuffers.getIndexBuffer()); } mesh.setBuffer(meshBuffers.getPositionsBuffer()); mesh.setBuffer(meshBuffers.getNormalsBuffer()); if (meshBuffers.areVertexColorsUsed()) { mesh.setBuffer(Type.Color, 4, meshBuffers.getVertexColorsBuffer()); mesh.getBuffer(Type.Color).setNormalized(true); } BoneBuffersData boneBuffersData = meshBuffers.getBoneBuffers(); if (boneBuffersData != null) { mesh.setMaxNumWeights(boneBuffersData.maximumWeightsPerVertex); mesh.setBuffer(boneBuffersData.verticesWeights); mesh.setBuffer(boneBuffersData.verticesWeightsIndices); LOGGER.fine("Generating bind pose and normal buffers."); mesh.generateBindPose(true); // change the usage type of vertex and normal buffers from Static to Stream mesh.getBuffer(Type.Position).setUsage(Usage.Stream); mesh.getBuffer(Type.Normal).setUsage(Usage.Stream); // creating empty buffers for HW skinning; the buffers will be setup if ever used VertexBuffer verticesWeightsHW = new VertexBuffer(Type.HWBoneWeight); VertexBuffer verticesWeightsIndicesHW = new VertexBuffer(Type.HWBoneIndex); mesh.setBuffer(verticesWeightsHW); mesh.setBuffer(verticesWeightsIndicesHW); } Geometry geometry = new Geometry(name + (result.size() + 1), mesh); if (properties != null && properties.getValue() != null) { meshHelper.applyProperties(geometry, properties); } result.add(geometry); geometryToBuffersMap.put(geometry, meshBuffers); } LOGGER.fine("Applying materials to geometries."); for (Entry<Geometry, MeshBuffers> entry : geometryToBuffersMap.entrySet()) { int materialIndex = entry.getValue().getMaterialIndex(); Geometry geometry = entry.getKey(); if (materialIndex >= 0 && materials != null && materials.length > materialIndex && materials[materialIndex] != null) { materials[materialIndex].applyMaterial(geometry, meshStructure.getOldMemoryAddress(), entry.getValue().getUvCoords(), blenderContext); } else { Material defaultMaterial = blenderContext.getDefaultMaterial().clone(); defaultMaterial.getAdditionalRenderState().setFaceCullMode(FaceCullMode.Off); geometry.setMaterial(defaultMaterial); } } } /** * The method creates geometries from lines. * @param result * the list where new geometries will be appended * @param meshHelper * the mesh helper */ protected void prepareLinesGeometry(List<Geometry> result, MeshHelper meshHelper) { if (edges.size() > 0) { LOGGER.fine("Preparing lines geometries."); List<List<Integer>> separateEdges = new ArrayList<List<Integer>>(); List<Edge> edges = new ArrayList<Edge>(this.edges.size()); for (Edge edge : this.edges) { if (!edge.isInFace()) { edges.add(edge); } } while (edges.size() > 0) { boolean edgeAppended = false; int edgeIndex = 0; for (List<Integer> list : separateEdges) { for (edgeIndex = 0; edgeIndex < edges.size() && !edgeAppended; ++edgeIndex) { Edge edge = edges.get(edgeIndex); if (list.get(0).equals(edge.getFirstIndex())) { list.add(0, edge.getSecondIndex()); --edgeIndex; edgeAppended = true; } else if (list.get(0).equals(edge.getSecondIndex())) { list.add(0, edge.getFirstIndex()); --edgeIndex; edgeAppended = true; } else if (list.get(list.size() - 1).equals(edge.getFirstIndex())) { list.add(edge.getSecondIndex()); --edgeIndex; edgeAppended = true; } else if (list.get(list.size() - 1).equals(edge.getSecondIndex())) { list.add(edge.getFirstIndex()); --edgeIndex; edgeAppended = true; } } if (edgeAppended) { break; } } Edge edge = edges.remove(edgeAppended ? edgeIndex : 0); if (!edgeAppended) { separateEdges.add(new ArrayList<Integer>(Arrays.asList(edge.getFirstIndex(), edge.getSecondIndex()))); } } for (List<Integer> list : separateEdges) { MeshBuffers meshBuffers = new MeshBuffers(0); for (int index : list) { meshBuffers.append(vertices.get(index), normals.get(index)); } Mesh mesh = new Mesh(); mesh.setLineWidth(blenderContext.getBlenderKey().getLinesWidth()); mesh.setMode(Mode.LineStrip); if (meshBuffers.isShortIndexBuffer()) { mesh.setBuffer(Type.Index, 1, (ShortBuffer) meshBuffers.getIndexBuffer()); } else { mesh.setBuffer(Type.Index, 1, (IntBuffer) meshBuffers.getIndexBuffer()); } mesh.setBuffer(meshBuffers.getPositionsBuffer()); mesh.setBuffer(meshBuffers.getNormalsBuffer()); Geometry geometry = new Geometry(meshStructure.getName() + (result.size() + 1), mesh); geometry.setMaterial(meshHelper.getBlackUnshadedMaterial(blenderContext)); if (properties != null && properties.getValue() != null) { meshHelper.applyProperties(geometry, properties); } result.add(geometry); } } } /** * The method creates geometries from points. * @param result * the list where new geometries will be appended * @param meshHelper * the mesh helper */ protected void preparePointsGeometry(List<Geometry> result, MeshHelper meshHelper) { if (points.size() > 0) { LOGGER.fine("Preparing point geometries."); MeshBuffers pointBuffers = new MeshBuffers(0); for (Point point : points) { pointBuffers.append(vertices.get(point.getIndex()), normals.get(point.getIndex())); } Mesh pointsMesh = new Mesh(); pointsMesh.setMode(Mode.Points); pointsMesh.setPointSize(blenderContext.getBlenderKey().getPointsSize()); if (pointBuffers.isShortIndexBuffer()) { pointsMesh.setBuffer(Type.Index, 1, (ShortBuffer) pointBuffers.getIndexBuffer()); } else { pointsMesh.setBuffer(Type.Index, 1, (IntBuffer) pointBuffers.getIndexBuffer()); } pointsMesh.setBuffer(pointBuffers.getPositionsBuffer()); pointsMesh.setBuffer(pointBuffers.getNormalsBuffer()); Geometry pointsGeometry = new Geometry(meshStructure.getName() + (result.size() + 1), pointsMesh); pointsGeometry.setMaterial(meshHelper.getBlackUnshadedMaterial(blenderContext)); if (properties != null && properties.getValue() != null) { meshHelper.applyProperties(pointsGeometry, properties); } result.add(pointsGeometry); } } @Override public String toString() { return "TemporalMesh [name=" + name + ", vertices.size()=" + vertices.size() + "]"; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + (meshStructure == null ? 0 : meshStructure.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof TemporalMesh)) { return false; } TemporalMesh other = (TemporalMesh) obj; return meshStructure.getOldMemoryAddress().equals(other.meshStructure.getOldMemoryAddress()); } }