From f364d66640412308694e82d69f93270ec55ebc81 Mon Sep 17 00:00:00 2001 From: jmekaelthas Date: Thu, 30 Oct 2014 21:49:38 +0100 Subject: [PATCH] Feature: added support for subdivision surface modifier. --- .../scene/plugins/blender/meshes/Edge.java | 25 + .../scene/plugins/blender/meshes/Face.java | 36 ++ .../plugins/blender/meshes/IndexesLoop.java | 26 +- .../plugins/blender/meshes/TemporalMesh.java | 120 ++++ .../blender/modifiers/ModifierHelper.java | 2 + .../modifiers/SubdivisionSurfaceModifier.java | 530 ++++++++++++++++++ 6 files changed, 737 insertions(+), 2 deletions(-) create mode 100644 jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/SubdivisionSurfaceModifier.java diff --git a/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/Edge.java b/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/Edge.java index 32a0b88c2..b12826001 100644 --- a/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/Edge.java +++ b/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/Edge.java @@ -94,6 +94,22 @@ public class Edge extends Line { return index2; } + /** + * Returns the index other than the given. + * @param index + * index of the edge + * @return the remaining index number + */ + public int getOtherIndex(int index) { + if (index == index1) { + return index2; + } + if (index == index2) { + return index1; + } + throw new IllegalArgumentException("Cannot give the other index for [" + index + "] because this index does not exist in edge: " + this); + } + /** * @return the crease value of the edge (its weight) */ @@ -108,6 +124,15 @@ public class Edge extends Line { return inFace; } + /** + * @return the centroid of the edge + */ + public Vector3f computeCentroid() { + Vector3f v1 = this.getOrigin(); + Vector3f v2 = v1.add(this.getDirection()); + return v2.addLocal(v1).divideLocal(2); + } + /** * Shifts indexes by a given amount. * @param shift diff --git a/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/Face.java b/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/Face.java index da37a88eb..be9d65e1b 100644 --- a/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/Face.java +++ b/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/Face.java @@ -131,6 +131,18 @@ public class Face implements Comparator { 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) */ @@ -408,6 +420,30 @@ public class Face implements Comparator { return true; } + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + indexes.hashCode(); + result = prime * result + temporalMesh.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Face)) { + return false; + } + Face other = (Face) obj; + if (!indexes.equals(other.indexes)) { + return false; + } + return temporalMesh.equals(other.temporalMesh); + } + /** * Loads all faces of a given mesh. * @param meshStructure diff --git a/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/IndexesLoop.java b/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/IndexesLoop.java index 16629a4ea..edec9b489 100644 --- a/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/IndexesLoop.java +++ b/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/IndexesLoop.java @@ -121,12 +121,34 @@ public class IndexesLoop implements Comparator, Iterable { * @return true if the given indexes are neighbours and false otherwise */ public boolean areNeighbours(Integer index1, Integer index2) { - if (index1.equals(index2)) { + if (index1.equals(index2) || !edges.containsKey(index1) || !edges.containsKey(index2)) { return false; } return edges.get(index1).contains(index2) || edges.get(index2).contains(index1); } + /** + * Returns the value of the index located after the given one. Pointint the last index will return the first one. + * @param index + * the index value + * @return the value of 'next' index + */ + public Integer getNextIndex(Integer index) { + int i = nodes.indexOf(index); + return i == nodes.size() - 1 ? nodes.get(0) : nodes.get(i + 1); + } + + /** + * Returns the value of the index located before the given one. Pointint the first index will return the last one. + * @param index + * the index value + * @return the value of 'previous' index + */ + public Integer getPreviousIndex(Integer index) { + int i = nodes.indexOf(index); + return i == 0 ? nodes.get(nodes.size() - 1) : nodes.get(i - 1); + } + /** * The method shifts all indexes by a given value. * @param shift @@ -171,7 +193,7 @@ public class IndexesLoop implements Comparator, Iterable { * the index whose neighbour count will be checked * @return the count of neighbours of the given index */ - public int getNeighbourCount(Integer index) { + private int getNeighbourCount(Integer index) { int result = 0; if (edges.containsKey(index)) { result = edges.get(index).size(); diff --git a/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/TemporalMesh.java b/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/TemporalMesh.java index 9745953da..f65d38ed5 100644 --- a/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/TemporalMesh.java +++ b/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/TemporalMesh.java @@ -72,6 +72,9 @@ public class TemporalMesh extends Geometry { /** The points of the mesh. */ protected List points = new ArrayList(); + protected Map> indexToFaceMapping = new HashMap>(); + protected Map> indexToEdgeMapping = new HashMap>(); + /** The bounding box of the temporal mesh. */ protected BoundingBox boundingBox; @@ -116,6 +119,8 @@ public class TemporalMesh extends Geometry { faces = Face.loadAll(meshStructure, userUVGroups, verticesColors, this, blenderContext); edges = Edge.loadAll(meshStructure); points = Point.loadAll(meshStructure); + + this.rebuildIndexesMappings(); } } @@ -175,6 +180,61 @@ public class TemporalMesh extends Geometry { return vertexGroups; } + /** + * @return the faces that contain the given index or null if none contain it + */ + public List 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 List getAdjacentFaces(Edge edge) { + List result = new ArrayList(indexToFaceMapping.get(edge.getFirstIndex())); + result.retainAll(indexToFaceMapping.get(edge.getSecondIndex())); + return result; + } + + /** + * @param the + * index of the mesh + * @return a list of edges that contain the index + */ + public List 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 true if the edge is a boundary one and false 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 true if the index is a boundary one and false otherwise + */ + public boolean isBoundary(Integer index) { + List adjacentEdges = indexToEdgeMapping.get(index); + for (Edge edge : adjacentEdges) { + if (this.isBoundary(edge)) { + return true; + } + } + return false; + } + @Override public TemporalMesh clone() { try { @@ -205,6 +265,7 @@ public class TemporalMesh extends Geometry { 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()); @@ -212,6 +273,41 @@ public class TemporalMesh extends Geometry { 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()) { + List faces = indexToFaceMapping.get(index); + if (faces == null) { + faces = new ArrayList(); + indexToFaceMapping.put(index, faces); + } + faces.add(face); + } + } + for (Edge edge : edges) { + List edges = indexToEdgeMapping.get(edge.getFirstIndex()); + if (edges == null) { + edges = new ArrayList(); + indexToEdgeMapping.put(edge.getFirstIndex(), edges); + } + edges.add(edge); + edges = indexToEdgeMapping.get(edge.getSecondIndex()); + if (edges == null) { + edges = new ArrayList(); + indexToEdgeMapping.put(edge.getSecondIndex(), edges); + } + edges.add(edge); + } + } + @Override public void updateModelBound() { if (boundingBox == null) { @@ -285,6 +381,8 @@ public class TemporalMesh extends Geometry { vertexGroups.addAll(mesh.vertexGroups); verticesColors.addAll(mesh.verticesColors); boneIndexes.putAll(mesh.boneIndexes); + + this.rebuildIndexesMappings(); } /** @@ -341,6 +439,8 @@ public class TemporalMesh extends Geometry { faces.clear(); edges.clear(); points.clear(); + indexToEdgeMapping.clear(); + indexToFaceMapping.clear(); } /** @@ -600,4 +700,24 @@ public class TemporalMesh extends Geometry { 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()); + } } diff --git a/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/ModifierHelper.java b/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/ModifierHelper.java index 6a28e433d..4f1fae3bd 100644 --- a/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/ModifierHelper.java +++ b/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/ModifierHelper.java @@ -96,6 +96,8 @@ public class ModifierHelper extends AbstractBlenderHelper { modifier = new ArmatureModifier(objectStructure, modifierStructure, blenderContext); } else if (Modifier.PARTICLE_MODIFIER_DATA.equals(modifierStructure.getType())) { modifier = new ParticlesModifier(modifierStructure, blenderContext); + } else if(Modifier.SUBSURF_MODIFIER_DATA.equals(modifierStructure.getType())) { + modifier = new SubdivisionSurfaceModifier(modifierStructure, blenderContext); } if (modifier != null) { diff --git a/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/SubdivisionSurfaceModifier.java b/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/SubdivisionSurfaceModifier.java new file mode 100644 index 000000000..c34665e6a --- /dev/null +++ b/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/SubdivisionSurfaceModifier.java @@ -0,0 +1,530 @@ +package com.jme3.scene.plugins.blender.modifiers; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +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.math.Vector2f; +import com.jme3.math.Vector3f; +import com.jme3.scene.Node; +import com.jme3.scene.plugins.blender.BlenderContext; +import com.jme3.scene.plugins.blender.file.BlenderFileException; +import com.jme3.scene.plugins.blender.file.Structure; +import com.jme3.scene.plugins.blender.meshes.Edge; +import com.jme3.scene.plugins.blender.meshes.Face; +import com.jme3.scene.plugins.blender.meshes.TemporalMesh; +import com.jme3.scene.plugins.blender.textures.TexturePixel; + +/** + * A modifier that subdivides the mesh using either simple or catmull-clark subdivision. + * + * @author Marcin Roguski (Kaelthas) + */ +public class SubdivisionSurfaceModifier extends Modifier { + private static final Logger LOGGER = Logger.getLogger(SubdivisionSurfaceModifier.class.getName()); + + private static final int TYPE_CATMULLCLARK = 0; + private static final int TYPE_SIMPLE = 1; + + private static final int FLAG_SUBDIVIDE_UVS = 0x8; + + /** The subdivision type. */ + private int subdivType; + /** The amount of subdivision levels. */ + private int levels; + /** Indicates if the UV's should also be subdivided. */ + private boolean subdivideUVS; + + /** + * Constructor loads all neccessary modifier data. + * @param modifierStructure + * the modifier structure + * @param blenderContext + * the blender context + */ + public SubdivisionSurfaceModifier(Structure modifierStructure, BlenderContext blenderContext) { + if (this.validate(modifierStructure, blenderContext)) { + subdivType = ((Number) modifierStructure.getFieldValue("subdivType")).intValue(); + levels = ((Number) modifierStructure.getFieldValue("levels")).intValue(); + int flag = ((Number) modifierStructure.getFieldValue("flags")).intValue(); + subdivideUVS = (flag & FLAG_SUBDIVIDE_UVS) != 0 && subdivType == TYPE_CATMULLCLARK; + + if (subdivType != TYPE_CATMULLCLARK && subdivType != TYPE_SIMPLE) { + LOGGER.log(Level.SEVERE, "Unknown subdivision type: {0}.", subdivType); + invalid = true; + } + if (levels < 0) { + LOGGER.severe("The amount of subdivision levels cannot be negative."); + invalid = true; + } + } + } + + @Override + public void apply(Node node, BlenderContext blenderContext) { + if (invalid) { + LOGGER.log(Level.WARNING, "Subdivision surface modifier is invalid! Cannot be applied to: {0}", node.getName()); + } else if (levels > 0) {// no need to do anything if the levels is set to zero + TemporalMesh temporalMesh = this.getTemporalMesh(node); + if (temporalMesh != null) { + LOGGER.log(Level.FINE, "Applying subdivision surface modifier to: {0}", temporalMesh); + if (subdivType == TYPE_CATMULLCLARK) { + for (int i = 0; i < levels; ++i) { + this.subdivideSimple(temporalMesh);// first do simple subdivision ... + this.subdivideCatmullClark(temporalMesh);// ... and then apply Catmull-Clark algorithm + if (subdivideUVS) {// UV's can be subdivided only for Catmull-Clark subdivision algorithm + this.subdivideUVs(temporalMesh); + } + } + } else { + for (int i = 0; i < levels; ++i) { + this.subdivideSimple(temporalMesh); + } + } + } else { + LOGGER.log(Level.WARNING, "Cannot find temporal mesh for node: {0}. The modifier will NOT be applied!", node); + } + } + } + + /** + * Catmull-Clark subdivision. It assumes that the mesh was already simple-subdivided. + * @param temporalMesh + * the mesh whose vertices will be transformed to form Catmull-Clark subdivision + */ + private void subdivideCatmullClark(TemporalMesh temporalMesh) { + Set boundaryVertices = new HashSet(); + for (Face face : temporalMesh.getFaces()) { + for (Integer index : face.getIndexes()) { + if (temporalMesh.isBoundary(index)) { + boundaryVertices.add(index); + } + } + } + + Vector3f[] averageVert = new Vector3f[temporalMesh.getVertices().size()]; + int[] averageCount = new int[temporalMesh.getVertices().size()]; + + for (Face face : temporalMesh.getFaces()) { + Vector3f centroid = face.computeCentroid(); + + for (Integer index : face.getIndexes()) { + if (boundaryVertices.contains(index)) { + Edge edge = this.findEdge(temporalMesh, index, face.getIndexes().getNextIndex(index)); + if (temporalMesh.isBoundary(edge)) { + averageVert[index] = averageVert[index] == null ? edge.computeCentroid() : averageVert[index].addLocal(edge.computeCentroid()); + averageCount[index] += 1; + } + edge = this.findEdge(temporalMesh, face.getIndexes().getPreviousIndex(index), index); + if (temporalMesh.isBoundary(edge)) { + averageVert[index] = averageVert[index] == null ? edge.computeCentroid() : averageVert[index].addLocal(edge.computeCentroid()); + averageCount[index] += 1; + } + } else { + averageVert[index] = averageVert[index] == null ? centroid.clone() : averageVert[index].addLocal(centroid); + averageCount[index] += 1; + } + } + } + for (Edge edge : temporalMesh.getEdges()) { + if (!edge.isInFace()) { + Vector3f centroid = temporalMesh.getVertices().get(edge.getFirstIndex()).add(temporalMesh.getVertices().get(edge.getSecondIndex())).divideLocal(2); + + averageVert[edge.getFirstIndex()] = averageVert[edge.getFirstIndex()] == null ? centroid.clone() : averageVert[edge.getFirstIndex()].addLocal(centroid); + averageVert[edge.getSecondIndex()] = averageVert[edge.getSecondIndex()] == null ? centroid.clone() : averageVert[edge.getSecondIndex()].addLocal(centroid); + averageCount[edge.getSecondIndex()] += 2; + } + } + + for (int i = 0; i < averageVert.length; ++i) { + averageVert[i].divideLocal(averageCount[i]); + if (!boundaryVertices.contains(i)) { + temporalMesh.getVertices().get(i).addLocal(averageVert[i].subtract(temporalMesh.getVertices().get(i)).multLocal(4 / (float) averageCount[i])); + } else { + temporalMesh.getVertices().get(i).set(averageVert[i]); + } + } + } + + /** + * The method performs a simple subdivision of the mesh. + * + * @param temporalMesh + * the mesh to be subdivided + */ + @SuppressWarnings("unchecked") + private void subdivideSimple(TemporalMesh temporalMesh) { + Map edgePoints = new HashMap(); + Map facePoints = new HashMap(); + List newFaces = new ArrayList(); + + int originalFacesCount = temporalMesh.getFaces().size(); + + List> vertexGroups = temporalMesh.getVertexGroups(); + // the result vertex array will have verts in the following order [[original_verts], [face_verts], [edge_verts]] + List vertices = temporalMesh.getVertices(); + List edgeVertices = new ArrayList(); + List faceVertices = new ArrayList(); + // the same goes for normals + List normals = temporalMesh.getNormals(); + List edgeNormals = new ArrayList(); + List faceNormals = new ArrayList(); + + List faces = temporalMesh.getFaces(); + for (Face face : faces) { + Map> uvSets = face.getUvSets(); + + Vector3f facePoint = face.computeCentroid(); + Integer facePointIndex = vertices.size() + faceVertices.size(); + facePoints.put(face, facePointIndex); + faceVertices.add(facePoint); + faceNormals.add(this.computeFaceNormal(face)); + Map faceUV = this.computeFaceUVs(face); + byte[] faceVertexColor = this.computeFaceVertexColor(face); + Map faceVertexGroups = this.computeFaceVertexGroups(face); + if (vertexGroups.size() > 0) { + vertexGroups.add(faceVertexGroups); + } + + for (int i = 0; i < face.getIndexes().size(); ++i) { + int vIndex = face.getIndexes().get(i); + int vPrevIndex = i == 0 ? face.getIndexes().get(face.getIndexes().size() - 1) : face.getIndexes().get(i - 1); + int vNextIndex = i == face.getIndexes().size() - 1 ? face.getIndexes().get(0) : face.getIndexes().get(i + 1); + + Edge prevEdge = this.findEdge(temporalMesh, vPrevIndex, vIndex);// new Edge(vPrevIndex, vIndex, 0, true, temporalMesh.getVertices()); + Edge nextEdge = this.findEdge(temporalMesh, vIndex, vNextIndex);// new Edge(vIndex, vNextIndex, 0, true, temporalMesh.getVertices()); + int vPrevEdgeVertIndex = edgePoints.containsKey(prevEdge) ? edgePoints.get(prevEdge) : -1; + int vNextEdgeVertIndex = edgePoints.containsKey(nextEdge) ? edgePoints.get(nextEdge) : -1; + + Vector3f v = temporalMesh.getVertices().get(vIndex); + if (vPrevEdgeVertIndex < 0) { + vPrevEdgeVertIndex = vertices.size() + originalFacesCount + edgeVertices.size(); + edgeVertices.add(vertices.get(vPrevIndex).add(v).divideLocal(2)); + edgeNormals.add(normals.get(vPrevIndex).add(normals.get(vIndex)).normalizeLocal()); + edgePoints.put(prevEdge, vPrevEdgeVertIndex); + if (vertexGroups.size() > 0) { + vertexGroups.add(this.interpolateVertexGroups(Arrays.asList(vertexGroups.get(vPrevIndex), vertexGroups.get(vIndex)))); + } + } + if (vNextEdgeVertIndex < 0) { + vNextEdgeVertIndex = vertices.size() + originalFacesCount + edgeVertices.size(); + edgeVertices.add(vertices.get(vNextIndex).add(v).divideLocal(2)); + edgeNormals.add(normals.get(vNextIndex).add(normals.get(vIndex)).normalizeLocal()); + edgePoints.put(nextEdge, vNextEdgeVertIndex); + if (vertexGroups.size() > 0) { + vertexGroups.add(this.interpolateVertexGroups(Arrays.asList(vertexGroups.get(vNextIndex), vertexGroups.get(vIndex)))); + } + } + + Integer[] indexes = new Integer[] { vIndex, vNextEdgeVertIndex, facePointIndex, vPrevEdgeVertIndex }; + + Map> newUVSets = null; + if (uvSets != null) { + newUVSets = new HashMap>(uvSets.size()); + for (Entry> uvset : uvSets.entrySet()) { + int indexOfvIndex = i; + int indexOfvPrevIndex = face.getIndexes().indexOf(vPrevIndex); + int indexOfvNextIndex = face.getIndexes().indexOf(vNextIndex); + + Vector2f uv1 = uvset.getValue().get(indexOfvIndex); + Vector2f uv2 = uvset.getValue().get(indexOfvNextIndex).add(uv1).divideLocal(2); + Vector2f uv3 = faceUV.get(uvset.getKey()); + Vector2f uv4 = uvset.getValue().get(indexOfvPrevIndex).add(uv1).divideLocal(2); + List uvList = Arrays.asList(uv1, uv2, uv3, uv4); + newUVSets.put(uvset.getKey(), new ArrayList(uvList)); + } + } + + List vertexColors = null; + if (face.getVertexColors() != null) { + + int indexOfvIndex = i; + int indexOfvPrevIndex = face.getIndexes().indexOf(vPrevIndex); + int indexOfvNextIndex = face.getIndexes().indexOf(vNextIndex); + + byte[] vCol1 = face.getVertexColors().get(indexOfvIndex); + byte[] vCol2 = this.interpolateVertexColors(face.getVertexColors().get(indexOfvNextIndex), vCol1); + byte[] vCol3 = faceVertexColor; + byte[] vCol4 = this.interpolateVertexColors(face.getVertexColors().get(indexOfvPrevIndex), vCol1); + vertexColors = new ArrayList(Arrays.asList(vCol1, vCol2, vCol3, vCol4)); + } + + newFaces.add(new Face(indexes, face.isSmooth(), face.getMaterialNumber(), newUVSets, vertexColors, temporalMesh)); + } + } + + vertices.addAll(faceVertices); + vertices.addAll(edgeVertices); + normals.addAll(faceNormals); + normals.addAll(edgeNormals); + + List newEdges = new ArrayList(temporalMesh.getEdges().size() * 2); + for (Edge edge : temporalMesh.getEdges()) { + if (!edge.isInFace()) { + int newVertexIndex = vertices.size(); + vertices.add(vertices.get(edge.getFirstIndex()).add(vertices.get(edge.getSecondIndex())).divideLocal(2)); + normals.add(normals.get(edge.getFirstIndex()).add(normals.get(edge.getSecondIndex())).normalizeLocal()); + + newEdges.add(new Edge(edge.getFirstIndex(), newVertexIndex, 0, false, vertices)); + newEdges.add(new Edge(newVertexIndex, edge.getSecondIndex(), 0, false, vertices)); + } else { + Integer edgePoint = edgePoints.get(edge); + newEdges.add(new Edge(edge.getFirstIndex(), edgePoint, edge.getCrease(), true, vertices)); + newEdges.add(new Edge(edgePoint, edge.getSecondIndex(), edge.getCrease(), true, vertices)); + // adding edges between face points and edge points + List facesContainingTheEdge = temporalMesh.getAdjacentFaces(edge); + for (Face f : facesContainingTheEdge) { + newEdges.add(new Edge(facePoints.get(f), edgePoint, 0, true, vertices)); + } + } + } + + temporalMesh.getFaces().clear(); + temporalMesh.getFaces().addAll(newFaces); + temporalMesh.getEdges().clear(); + temporalMesh.getEdges().addAll(newEdges); + + temporalMesh.rebuildIndexesMappings(); + } + + /** + * The method subdivides mesh's UV coordinates. It actually performs only Catmull-Clark modifications because if any UV's are present then they are + * automatically subdivided by the simple algorithm. + * @param temporalMesh + * the mesh whose UV coordinates will be applied Catmull-Clark algorithm + */ + private void subdivideUVs(TemporalMesh temporalMesh) { + List faces = temporalMesh.getFaces(); + Map subdividedUVS = new HashMap(); + for (Face face : faces) { + if (face.getUvSets() != null) { + for (Entry> uvset : face.getUvSets().entrySet()) { + UvCoordsSubdivideTemporalMesh uvCoordsSubdivideTemporalMesh = subdividedUVS.get(uvset.getKey()); + if (uvCoordsSubdivideTemporalMesh == null) { + try { + uvCoordsSubdivideTemporalMesh = new UvCoordsSubdivideTemporalMesh(temporalMesh.getBlenderContext()); + } catch (BlenderFileException e) { + assert false : "Something went really wrong! The UvCoordsSubdivideTemporalMesh class should NOT throw exceptions here!"; + } + subdividedUVS.put(uvset.getKey(), uvCoordsSubdivideTemporalMesh); + } + uvCoordsSubdivideTemporalMesh.addFace(uvset.getValue()); + } + } + } + + for (Entry entry : subdividedUVS.entrySet()) { + entry.getValue().rebuildIndexesMappings(); + this.subdivideCatmullClark(entry.getValue()); + + for (int i = 0; i < faces.size(); ++i) { + List uvs = faces.get(i).getUvSets().get(entry.getKey()); + if (uvs != null) { + uvs.clear(); + uvs.addAll(entry.getValue().faceToUVs(i)); + } + } + } + } + + /** + * The method computes the face's normal vector. + * @param face + * the face of the mesh + * @return face's normal vector + */ + private Vector3f computeFaceNormal(Face face) { + Vector3f result = new Vector3f(); + for (Integer index : face.getIndexes()) { + result.addLocal(face.getTemporalMesh().getNormals().get(index)); + } + result.divideLocal(face.getIndexes().size()); + return result; + } + + /** + * The method computes the UV coordinates of the face middle point. + * @param face + * the face of the mesh + * @return a map whose key is the name of the UV set and value is the UV coordinate of the face's middle point + */ + private Map computeFaceUVs(Face face) { + Map result = null; + + Map> uvSets = face.getUvSets(); + if (uvSets != null && uvSets.size() > 0) { + result = new HashMap(uvSets.size()); + + for (Entry> entry : uvSets.entrySet()) { + Vector2f faceUV = new Vector2f(); + for (Vector2f uv : entry.getValue()) { + faceUV.addLocal(uv); + } + faceUV.divideLocal(entry.getValue().size()); + result.put(entry.getKey(), faceUV); + } + } + + return result; + } + + /** + * The mesh interpolates the values of vertex groups weights for new vertices. + * @param vertexGroups + * the vertex groups + * @return interpolated weights of given vertex groups' weights + */ + private Map interpolateVertexGroups(List> vertexGroups) { + Map weightSums = new HashMap(); + if (vertexGroups.size() > 0) { + for (Map vGroup : vertexGroups) { + for (Entry entry : vGroup.entrySet()) { + if (weightSums.containsKey(entry.getKey())) { + weightSums.put(entry.getKey(), weightSums.get(entry.getKey()) + entry.getValue()); + } else { + weightSums.put(entry.getKey(), entry.getValue()); + } + } + } + } + + Map result = new HashMap(weightSums.size()); + for (Entry entry : weightSums.entrySet()) { + result.put(entry.getKey(), entry.getValue() / vertexGroups.size()); + } + + return result; + } + + /** + * The method computes the vertex groups values for face's middle point. + * @param face + * the face of the mesh + * @return face's middle point interpolated vertex groups' weights + */ + private Map computeFaceVertexGroups(Face face) { + if (face.getTemporalMesh().getVertexGroups().size() > 0) { + List> vertexGroups = new ArrayList>(face.getIndexes().size()); + for (Integer index : face.getIndexes()) { + vertexGroups.add(face.getTemporalMesh().getVertexGroups().get(index)); + } + return this.interpolateVertexGroups(vertexGroups); + } + return new HashMap(); + } + + /** + * The method computes face's middle point vertex color. + * @param face + * the face of the mesh + * @return face's middle point vertex color + */ + private byte[] computeFaceVertexColor(Face face) { + if (face.getVertexColors() != null) { + return this.interpolateVertexColors(face.getVertexColors().toArray(new byte[face.getVertexColors().size()][])); + } + return null; + } + + /** + * The method computes the average value for the given vertex colors. + * @param colors + * the vertex colors + * @return vertex colors' average value + */ + private byte[] interpolateVertexColors(byte[]... colors) { + TexturePixel pixel = new TexturePixel(); + TexturePixel temp = new TexturePixel(); + for (int i = 0; i < colors.length; ++i) { + temp.fromARGB8(colors[i][3], colors[i][0], colors[i][1], colors[i][2]); + pixel.add(temp); + } + pixel.divide(colors.length); + byte[] result = new byte[4]; + pixel.toRGBA8(result); + return result; + } + + /** + * The method finds an edge between the given vertices in the mesh. + * @param temporalMesh + * the mesh + * @param index1 + * first index of the edge + * @param index2 + * second index of the edge + * @return found edge or null + */ + private Edge findEdge(TemporalMesh temporalMesh, int index1, int index2) { + for (Edge edge : temporalMesh.getEdges()) { + if (edge.getFirstIndex() == index1 && edge.getSecondIndex() == index2 || edge.getFirstIndex() == index2 && edge.getSecondIndex() == index1) { + return edge; + } + } + return null; + } + + /** + * This is a helper class for UV coordinates subdivision. UV's form a mesh that is being applied the same algorithms as a regular mesh. + * This way one code handles two issues. After applying Catmull-Clark algorithm the UV-mesh is transformed back into UV coordinates. + * + * @author Marcin Roguski (Kaelthas) + */ + private static class UvCoordsSubdivideTemporalMesh extends TemporalMesh { + private static final Vector3f NORMAL = new Vector3f(0, 0, 1); + + public UvCoordsSubdivideTemporalMesh(BlenderContext blenderContext) throws BlenderFileException { + super(null, blenderContext, false); + } + + /** + * Adds a UV-face to the mesh. + * @param uvs + * the UV coordinates + */ + public void addFace(List uvs) { + Integer[] indexes = new Integer[uvs.size()]; + int i = 0; + + for (Vector2f uv : uvs) { + Vector3f v = new Vector3f(uv.x, uv.y, 0); + int index = vertices.indexOf(v); + if (index >= 0) { + indexes[i++] = index; + } else { + indexes[i++] = vertices.size(); + vertices.add(v); + normals.add(NORMAL); + } + } + faces.add(new Face(indexes, false, 0, null, null, this)); + for (i = 1; i < indexes.length; ++i) { + edges.add(new Edge(indexes[i - 1], indexes[i], 0, true, vertices)); + } + edges.add(new Edge(indexes[indexes.length - 1], indexes[0], 0, true, vertices)); + } + + /** + * Converts the mesh back into UV coordinates for the given face. + * @param faceIndex + * the index of the face + * @return UV coordinates + */ + public List faceToUVs(int faceIndex) { + Face face = faces.get(faceIndex); + List result = new ArrayList(face.getIndexes().size()); + for (Integer index : face.getIndexes()) { + Vector3f v = vertices.get(index); + result.add(new Vector2f(v.x, v.y)); + } + return result; + } + } +}