diff --git a/engine/src/test/jme3test/stress/TestLodGeneration.java b/engine/src/test/jme3test/stress/TestLodGeneration.java new file mode 100644 index 000000000..07de66058 --- /dev/null +++ b/engine/src/test/jme3test/stress/TestLodGeneration.java @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2009-2012 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package jme3test.stress; + +import com.jme3.animation.AnimChannel; +import com.jme3.animation.SkeletonControl; +import com.jme3.app.SimpleApplication; +import com.jme3.bounding.BoundingBox; +import com.jme3.font.BitmapText; +import com.jme3.input.ChaseCamera; +import com.jme3.input.KeyInput; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.KeyTrigger; +import com.jme3.light.AmbientLight; +import com.jme3.light.DirectionalLight; +import com.jme3.math.ColorRGBA; +import com.jme3.math.FastMath; +import com.jme3.math.Vector3f; +import com.jme3.scene.Geometry; +import com.jme3.scene.Node; +import com.jme3.scene.Spatial; +import com.jme3.scene.VertexBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import jme3tools.optimize.LodGenerator; + +public class TestLodGeneration extends SimpleApplication { + + public static void main(String[] args) { + TestLodGeneration app = new TestLodGeneration(); + app.start(); + } + boolean wireFrame = false; + float reductionvalue = 0.0f; + private int lodLevel = 0; + private Node model; + private BitmapText hudText; + private List listGeoms = new ArrayList(); + private ScheduledThreadPoolExecutor exec = new ScheduledThreadPoolExecutor(5); + private AnimChannel ch; + + public void simpleInitApp() { + + DirectionalLight dl = new DirectionalLight(); + dl.setDirection(new Vector3f(-1, -1, -1).normalizeLocal()); + rootNode.addLight(dl); + AmbientLight al = new AmbientLight(); + al.setColor(ColorRGBA.White.mult(0.6f)); + rootNode.addLight(al); + + model = (Node) assetManager.loadModel("Models/Sinbad/Sinbad.mesh.xml"); + //model = (Node) assetManager.loadModel("Models/Jaime/Jaime.j3o"); + BoundingBox b = ((BoundingBox) model.getWorldBound()); + model.setLocalScale(1.2f / (b.getYExtent() * 2)); + // model.setLocalTranslation(0,-(b.getCenter().y - b.getYExtent())* model.getLocalScale().y, 0); + for (Spatial spatial : model.getChildren()) { + if (spatial instanceof Geometry) { + listGeoms.add((Geometry) spatial); + } + } + ChaseCamera chaseCam = new ChaseCamera(cam, inputManager); + model.addControl(chaseCam); + chaseCam.setLookAtOffset(b.getCenter()); + chaseCam.setDefaultDistance(5); + chaseCam.setMinVerticalRotation(-FastMath.HALF_PI + 0.01f); + chaseCam.setZoomSensitivity(0.5f); + + + + // ch = model.getControl(AnimControl.class).createChannel(); + // ch.setAnim("Wave"); + SkeletonControl c = model.getControl(SkeletonControl.class); + if (c != null) { + c.setEnabled(false); + } + + + reductionvalue = 0.001f; + // makeLod(LodGenerator.VertexReductionMethod.PROPORTIONAL, reductionvalue, 1); + + lodLevel = 1; + for (final Geometry geometry : listGeoms) { + LodGenerator lODGenerator = new LodGenerator(geometry); + lODGenerator.bakeLods(LodGenerator.TriangleReductionMethod.PROPORTIONAL, reductionvalue); + geometry.setLodLevel(lodLevel); + + + } + + rootNode.attachChild(model); + flyCam.setEnabled(false); + + + + guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt"); + hudText = new BitmapText(guiFont, false); + hudText.setSize(guiFont.getCharSet().getRenderedSize()); + hudText.setText(computeNbTri() + " tris"); + hudText.setLocalTranslation(cam.getWidth() / 2, hudText.getLineHeight(), 0); + guiNode.attachChild(hudText); + + inputManager.addListener(new ActionListener() { + public void onAction(String name, boolean isPressed, float tpf) { + if (isPressed) { + if (name.equals("plus")) { +// lodLevel++; +// for (Geometry geometry : listGeoms) { +// if (geometry.getMesh().getNumLodLevels() <= lodLevel) { +// lodLevel = 0; +// } +// geometry.setLodLevel(lodLevel); +// } +// jaimeText.setText(computeNbTri() + " tris"); + + + + reductionvalue += 0.05f; + updateLod(); + + + + } + if (name.equals("minus")) { +// lodLevel--; +// for (Geometry geometry : listGeoms) { +// if (lodLevel < 0) { +// lodLevel = geometry.getMesh().getNumLodLevels() - 1; +// } +// geometry.setLodLevel(lodLevel); +// } +// jaimeText.setText(computeNbTri() + " tris"); + + + + reductionvalue -= 0.05f; + updateLod(); + + + } + if (name.equals("wireFrame")) { + wireFrame = !wireFrame; + for (Geometry geometry : listGeoms) { + geometry.getMaterial().getAdditionalRenderState().setWireframe(wireFrame); + } + } + + } + + } + + private void updateLod() { + reductionvalue = FastMath.clamp(reductionvalue, 0.0f, 1.0f); + makeLod(LodGenerator.TriangleReductionMethod.PROPORTIONAL, reductionvalue, 1); + } + }, "plus", "minus", "wireFrame"); + + inputManager.addMapping("plus", new KeyTrigger(KeyInput.KEY_ADD)); + inputManager.addMapping("minus", new KeyTrigger(KeyInput.KEY_SUBTRACT)); + inputManager.addMapping("wireFrame", new KeyTrigger(KeyInput.KEY_SPACE)); + + + + } + + @Override + public void simpleUpdate(float tpf) { + // model.rotate(0, tpf, 0); + } + + private int computeNbTri() { + int nbTri = 0; + for (Geometry geometry : listGeoms) { + if (geometry.getMesh().getNumLodLevels() > 0) { + nbTri += geometry.getMesh().getLodLevel(lodLevel).getNumElements(); + } else { + nbTri += geometry.getMesh().getTriangleCount(); + } + } + return nbTri; + } + + @Override + public void destroy() { + super.destroy(); + exec.shutdown(); + } + + private void makeLod(final LodGenerator.TriangleReductionMethod method, final float value, final int ll) { + exec.execute(new Runnable() { + public void run() { + for (final Geometry geometry : listGeoms) { + LodGenerator lODGenerator = new LodGenerator(geometry); + final VertexBuffer[] lods = lODGenerator.computeLods(method, value); + + enqueue(new Callable() { + public Void call() throws Exception { + geometry.getMesh().setLodLevels(lods); + lodLevel = 0; + if (geometry.getMesh().getNumLodLevels() > ll) { + lodLevel = ll; + } + geometry.setLodLevel(lodLevel); + hudText.setText(computeNbTri() + " tris"); + return null; + } + }); + } + } + }); + + } +} diff --git a/engine/src/tools/jme3tools/optimize/LodGenerator.java b/engine/src/tools/jme3tools/optimize/LodGenerator.java new file mode 100644 index 000000000..d24b9e64c --- /dev/null +++ b/engine/src/tools/jme3tools/optimize/LodGenerator.java @@ -0,0 +1,1016 @@ +/* + * Copyright (c) 2009-2013 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package jme3tools.optimize; + +import com.jme3.bounding.BoundingSphere; +import com.jme3.math.Vector3f; +import com.jme3.scene.Geometry; +import com.jme3.scene.Mesh; +import com.jme3.scene.VertexBuffer; +import com.jme3.util.BufferUtils; +import java.nio.Buffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.ShortBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * This is an utility class that allows to generated the lod levels for an + * arbitrary mesh. It computes a collapse cost for each vertex and each edges. + * The higher the cost the most likely collapsing the edge or the vertex will + * produce artifacts on the mesh.

This class is the java implementation of + * the enhanced version og Ogre engine Lod generator, by Péter Szücs, originally + * based on Stan Melax "easy mesh simplification". The MIT licences C++ source + * code can be found here + * https://github.com/worldforge/ember/tree/master/src/components/ogre/lod more + * informations can be found here http://www.melax.com/polychop + * http://sajty.elementfx.com/progressivemesh/GSoC2012.pdf

+ * + *

The algorithm sort the vertice according to their collapsse cost + * ascending. It collapse from the "cheapest" vertex to the more expensive.
+ * Usage :
+ *

+ *      LodGenerator lODGenerator = new LodGenerator(geometry);
+ *      lODGenerator.bakeLods(reductionMethod,reductionvalue);
+ * 
redutionMethod type is VertexReductionMethod described here + * {@link TriangleReductionMethod} reductionvalue depends on the + * reductionMethod

+ * + * + * @author Nehon + */ +public class LodGenerator { + + private static final Logger logger = Logger.getLogger(LodGenerator.class.getName()); + private static final float NEVER_COLLAPSE_COST = Float.MAX_VALUE; + private static final float UNINITIALIZED_COLLAPSE_COST = Float.POSITIVE_INFINITY; + private Vector3f tmpV1 = new Vector3f(); + private Vector3f tmpV2 = new Vector3f(); + private boolean bestQuality = true; + private int indexCount = 0; + private List collapseCostSet = new ArrayList(); + private float collapseCostLimit; + private List triangleList; + private List vertexList = new ArrayList(); + private float meshBoundingSphereRadius; + private Mesh mesh; + + /** + * Describe the way trinagles will be removed.
PROPORTIONAL : + * Percentage of triangles to be removed from the mesh. Valid range is a + * number between 0.0 and 1.0
CONSTANT : Triangle count to be removed + * from the mesh. Pass only integers or it will be rounded.
+ * COLLAPSE_COST : Reduces the vertices, until the cost is bigger then the + * given value. Collapse cost is equal to the amount of artifact the + * reduction causes. This generates the best Lod output, but the collapse + * cost depends on implementation. + */ + public enum TriangleReductionMethod { + + /** + * Percentage of triangles to be removed from the mesh. + * + * Valid range is a number between 0.0 and 1.0 + */ + PROPORTIONAL, + /** + * Triangle count to be removed from the mesh. + * + * Pass only integers or it will be rounded. + */ + CONSTANT, + /** + * Reduces the vertices, until the cost is bigger then the given value. + * + * Collapse cost is equal to the amount of artifact the reduction + * causes. This generates the best Lod output, but the collapse cost + * depends on implementation. + */ + COLLAPSE_COST + }; + + private class Edge { + + Vertex destination; + float collapseCost = UNINITIALIZED_COLLAPSE_COST; + int refCount; + + public Edge(Vertex destination) { + this.destination = destination; + } + + public void set(Edge other) { + destination = other.destination; + collapseCost = other.collapseCost; + refCount = other.refCount; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Edge)) { + return false; + } + return destination == ((Edge) obj).destination; + } + + @Override + public int hashCode() { + return destination.hashCode(); + } + + @Override + public String toString() { + return "Edge{" + "collapsTo " + destination.index + '}'; + } + } + + private class Vertex { + + Vector3f position = new Vector3f(); + float collapseCost = UNINITIALIZED_COLLAPSE_COST; + List edges = new ArrayList(); + Set triangles = new HashSet(); + Vertex collapseTo; + boolean isSeam; + int index;//index in the buffer for debugging + + @Override + public String toString() { + return index + " : " + position.toString(); + } + } + + private class Triangle { + + Vertex[] vertex = new Vertex[3]; + Vector3f normal; + boolean isRemoved; + //indices of the vertices in the vertex buffer + int[] vertexId = new int[3]; + + void computeNormal() { + // Cross-product 2 edges + tmpV1.set(vertex[1].position).subtractLocal(vertex[0].position); + tmpV2.set(vertex[2].position).subtractLocal(vertex[1].position); + + normal = tmpV1.cross(tmpV2); + normal.normalizeLocal(); + } + + boolean hasVertex(Vertex v) { + return (v == vertex[0] || v == vertex[1] || v == vertex[2]); + } + + int getVertexIndex(Vertex v) { + for (int i = 0; i < 3; i++) { + if (vertex[i] == v) { + return vertexId[i]; + } + } + throw new IllegalArgumentException("Vertex " + v + "is not part of triangle" + this); + } + + boolean isMalformed() { + return vertex[0] == vertex[1] || vertex[0] == vertex[2] || vertex[1] == vertex[2]; + } + + @Override + public String toString() { + String out = "Triangle{\n"; + for (int i = 0; i < 3; i++) { + out += vertexId[i] + " : " + vertex[i].toString() + "\n"; + } + out += '}'; + return out; + } + } + /** + * Compartator used to sort vertices according to their collapse cost + */ + private Comparator collapseComparator = new Comparator() { + public int compare(Vertex o1, Vertex o2) { + if (Float.compare(o1.collapseCost, o2.collapseCost) == 0) { + return 0; + } + if (o1.collapseCost < o2.collapseCost) { + return -1; + } + return 1; + } + }; + + /** + * Construct a LodGenerator for the given geometry + * + * @param geom the geometry to consider to generate de Lods. + */ + public LodGenerator(Geometry geom) { + mesh = geom.getMesh(); + build(); + } + + private void build() { + BoundingSphere bs = new BoundingSphere(); + bs.computeFromPoints(mesh.getFloatBuffer(VertexBuffer.Type.Position)); + meshBoundingSphereRadius = bs.getRadius(); + List vertexLookup = new ArrayList(); + initialize(); + + gatherVertexData(mesh, vertexLookup); + gatherIndexData(mesh, vertexLookup); + computeCosts(); + assert (assertValidMesh()); + + } + + private void gatherVertexData(Mesh mesh, List vertexLookup) { + + //in case the model is currently animating with software animation + //attempting to retrieve the bind position instead of the position. + VertexBuffer position = mesh.getBuffer(VertexBuffer.Type.BindPosePosition); + if (position == null) { + position = mesh.getBuffer(VertexBuffer.Type.Position); + } + FloatBuffer pos = (FloatBuffer) position.getDataReadOnly(); + pos.rewind(); + + while (pos.remaining() != 0) { + Vertex v = new Vertex(); + v.position.setX(pos.get()); + v.position.setY(pos.get()); + v.position.setZ(pos.get()); + v.isSeam = false; + Vertex existingV = findSimilar(v); + if (existingV != null) { + //vertex position already exists + existingV.isSeam = true; + v.isSeam = true; + } else { + vertexList.add(v); + } + vertexLookup.add(v); + } + pos.rewind(); + } + + private Vertex findSimilar(Vertex v) { + for (Vertex vertex : vertexList) { + if (vertex.position.equals(v.position)) { + return vertex; + } + } + return null; + } + + private void gatherIndexData(Mesh mesh, List vertexLookup) { + VertexBuffer indexBuffer = mesh.getBuffer(VertexBuffer.Type.Index); + indexCount = indexBuffer.getNumElements() * 3; + Buffer b = indexBuffer.getDataReadOnly(); + b.rewind(); + + while (b.remaining() != 0) { + Triangle tri = new Triangle(); + tri.isRemoved = false; + triangleList.add(tri); + for (int i = 0; i < 3; i++) { + if (b instanceof IntBuffer) { + tri.vertexId[i] = ((IntBuffer) b).get(); + } else { + tri.vertexId[i] = ((ShortBuffer) b).get(); + } + assert (tri.vertexId[i] < vertexLookup.size()); + tri.vertex[i] = vertexLookup.get(tri.vertexId[i]); + //debug only; + tri.vertex[i].index = tri.vertexId[i]; + } + if (tri.isMalformed()) { + if (!tri.isRemoved) { + logger.log(Level.FINE, "malformed triangle found with ID:{0}\n{1} It will be excluded from Lod level calculations.", new Object[]{triangleList.indexOf(tri), tri.toString()}); + tri.isRemoved = true; + indexCount -= 3; + } + + } else { + tri.computeNormal(); + addTriangleToEdges(tri); + } + } + b.rewind(); + } + + private void computeCosts() { + collapseCostSet.clear(); + + for (Vertex vertex : vertexList) { + + if (!vertex.edges.isEmpty()) { + computeVertexCollapseCost(vertex); + } else { + logger.log(Level.FINE, "Found isolated vertex {0} It will be excluded from Lod level calculations.", vertex); + } + } + assert (vertexList.size() == collapseCostSet.size()); + assert (checkCosts()); + } + + //Debug only + private boolean checkCosts() { + for (Vertex vertex : vertexList) { + boolean test = find(collapseCostSet, vertex); + if (!test) { + System.out.println("vertex " + vertex.index + " not present in collapse costs"); + return false; + } + } + return true; + } + + private void computeVertexCollapseCost(Vertex vertex) { + + vertex.collapseCost = UNINITIALIZED_COLLAPSE_COST; + assert (!vertex.edges.isEmpty()); + for (Edge edge : vertex.edges) { + edge.collapseCost = computeEdgeCollapseCost(vertex, edge); + assert (edge.collapseCost != UNINITIALIZED_COLLAPSE_COST); + if (vertex.collapseCost > edge.collapseCost) { + vertex.collapseCost = edge.collapseCost; + vertex.collapseTo = edge.destination; + } + } + assert (vertex.collapseCost != UNINITIALIZED_COLLAPSE_COST); + collapseCostSet.add(vertex); + } + + float computeEdgeCollapseCost(Vertex src, Edge dstEdge) { + // This is based on Ogre's collapse cost calculation algorithm. + + Vertex dest = dstEdge.destination; + + // Check for singular triangle destruction + // If src and dest both only have 1 triangle (and it must be a shared one) + // then this would destroy the shape, so don't do this + if (src.triangles.size() == 1 && dest.triangles.size() == 1) { + return NEVER_COLLAPSE_COST; + } + + // Degenerate case check + // Are we going to invert a face normal of one of the neighbouring faces? + // Can occur when we have a very small remaining edge and collapse crosses it + // Look for a face normal changing by > 90 degrees + for (Triangle triangle : src.triangles) { + // Ignore the deleted faces (those including src & dest) + if (!triangle.hasVertex(dest)) { + // Test the new face normal + Vertex pv0, pv1, pv2; + + // Replace src with dest wherever it is + pv0 = (triangle.vertex[0] == src) ? dest : triangle.vertex[0]; + pv1 = (triangle.vertex[1] == src) ? dest : triangle.vertex[1]; + pv2 = (triangle.vertex[2] == src) ? dest : triangle.vertex[2]; + + // Cross-product 2 edges + tmpV1.set(pv1.position).subtractLocal(pv0.position); + tmpV2.set(pv2.position).subtractLocal(pv1.position); + + //computing the normal + Vector3f newNormal = tmpV1.crossLocal(tmpV2); + newNormal.normalizeLocal(); + + // Dot old and new face normal + // If < 0 then more than 90 degree difference + if (newNormal.dot(triangle.normal) < 0.0f) { + // Don't do it! + return NEVER_COLLAPSE_COST; + } + } + } + + float cost; + + // Special cases + // If we're looking at a border vertex + if (isBorderVertex(src)) { + if (dstEdge.refCount > 1) { + // src is on a border, but the src-dest edge has more than one tri on it + // So it must be collapsing inwards + // Mark as very high-value cost + // curvature = 1.0f; + cost = 1.0f; + } else { + // Collapsing ALONG a border + // We can't use curvature to measure the effect on the model + // Instead, see what effect it has on 'pulling' the other border edges + // The more colinear, the less effect it will have + // So measure the 'kinkiness' (for want of a better term) + + // Find the only triangle using this edge. + // PMTriangle* triangle = findSideTriangle(src, dst); + + cost = 0.0f; + Vector3f collapseEdge = tmpV1.set(src.position).subtractLocal(dest.position); + collapseEdge.normalizeLocal(); + + for (Edge edge : src.edges) { + + Vertex neighbor = edge.destination; + //reference check intended + if (neighbor != dest && edge.refCount == 1) { + Vector3f otherBorderEdge = tmpV2.set(src.position).subtractLocal(neighbor.position); + otherBorderEdge.normalizeLocal(); + // This time, the nearer the dot is to -1, the better, because that means + // the edges are opposite each other, therefore less kinkiness + // Scale into [0..1] + float kinkiness = (otherBorderEdge.dot(collapseEdge) + 1.002f) * 0.5f; + cost = Math.max(cost, kinkiness); + } + } + } + } else { // not a border + + // Standard inner vertex + // Calculate curvature + // use the triangle facing most away from the sides + // to determine our curvature term + // Iterate over src's faces again + cost = 0.001f; + + for (Triangle triangle : src.triangles) { + float mincurv = 1.0f; // curve for face i and closer side to it + + for (Triangle triangle2 : src.triangles) { + if (triangle2.hasVertex(dest)) { + + // Dot product of face normal gives a good delta angle + float dotprod = triangle.normal.dot(triangle2.normal); + // NB we do (1-..) to invert curvature where 1 is high curvature [0..1] + // Whilst dot product is high when angle difference is low + mincurv = Math.min(mincurv, (1.002f - dotprod) * 0.5f); + } + } + cost = Math.max(cost, mincurv); + } + } + + // check for texture seam ripping + if (src.isSeam) { + if (!dest.isSeam) { + cost += meshBoundingSphereRadius; + } else { + cost += meshBoundingSphereRadius * 0.5; + } + } + + assert (cost >= 0); + // TODO: use squared distance. + return cost * src.position.distance(dest.position); + } + int nbCollapsedTri = 0; + + /** + * Computes the lod and return a list of VertexBuffers that can then be used + * for lod (use Mesg.setLodLevels(VertexBuffer[]))
+ * + * This method must be fed with the reduction method + * {@link TriangleReductionMethod} and a list of reduction values.
for + * each value a lod will be generated.
The resulting array will always + * contain at index 0 the original index buffer of the mesh.

+ * Important note : some meshes cannot be decimated, so the + * result of this method can varry depending of the given mesh. Also the + * reduction values are indicative and the produces mesh will not always + * meet the required reduction. + * + * @param reductionMethod the reduction method to use + * @param reductionValues the reduction value to use for each lod level. + * @return an array of VertexBuffers containing the different index buffers + * representing the lod levels. + */ + public VertexBuffer[] computeLods(TriangleReductionMethod reductionMethod, float... reductionValues) { + int tricount = triangleList.size(); + int lastBakeVertexCount = tricount; + int lodCount = reductionValues.length; + VertexBuffer[] lods = new VertexBuffer[lodCount + 1]; + int numBakedLods = 1; + lods[0] = mesh.getBuffer(VertexBuffer.Type.Index); + for (int curLod = 0; curLod < lodCount; curLod++) { + int neededTriCount = calcLodTriCount(reductionMethod, reductionValues[curLod]); + while (neededTriCount < tricount) { + Collections.sort(collapseCostSet, collapseComparator); + Iterator it = collapseCostSet.iterator(); + + if (it.hasNext()) { + Vertex v = it.next(); + if (v.collapseCost < collapseCostLimit) { + if (!collapse(v)) { + logger.log(Level.FINE, "Couldn''t collapse vertex{0}", v.index); + } + Iterator it2 = collapseCostSet.iterator(); + if (it2.hasNext()) { + it2.next(); + it2.remove();// Remove src from collapse costs. + } + + } else { + break; + } + } else { + break; + } + tricount = triangleList.size() - nbCollapsedTri; + } + logger.log(Level.FINE, "collapsed {0} tris", nbCollapsedTri); + boolean outSkipped = (lastBakeVertexCount == tricount); + if (!outSkipped) { + lastBakeVertexCount = tricount; + lods[curLod + 1] = makeLod(mesh); + numBakedLods++; + } + } + if (numBakedLods <= lodCount) { + VertexBuffer[] bakedLods = new VertexBuffer[numBakedLods]; + System.arraycopy(lods, 0, bakedLods, 0, numBakedLods); + return bakedLods; + } else { + return lods; + } + } + + /** + * Computes the lods and bake them into the mesh
+ * + * This method must be fed with the reduction method + * {@link TriangleReductionMethod} and a list of reduction values.
for + * each value a lod will be generated.

Important note : + * some meshes cannot be decimated, so the result of this method can varry + * depending of the given mesh. Also the reduction values are indicative and + * the produces mesh will not always meet the required reduction. + * + * @param reductionMethod the reduction method to use + * @param reductionValues the reduction value to use for each lod level. + */ + public void bakeLods(TriangleReductionMethod reductionMethod, float... reductionValues) { + mesh.setLodLevels(computeLods(reductionMethod, reductionValues)); + } + + private VertexBuffer makeLod(Mesh mesh) { + VertexBuffer indexBuffer = mesh.getBuffer(VertexBuffer.Type.Index); + + boolean isShortBuffer = indexBuffer.getFormat() == VertexBuffer.Format.UnsignedShort; + // Create buffers. + VertexBuffer lodBuffer = new VertexBuffer(VertexBuffer.Type.Index); + int bufsize = indexCount == 0 ? 3 : indexCount; + + if (isShortBuffer) { + lodBuffer.setupData(VertexBuffer.Usage.Static, 3, VertexBuffer.Format.UnsignedShort, BufferUtils.createShortBuffer(bufsize)); + } else { + lodBuffer.setupData(VertexBuffer.Usage.Static, 3, VertexBuffer.Format.UnsignedInt, BufferUtils.createIntBuffer(bufsize)); + } + + + + lodBuffer.getData().rewind(); + //Check if we should fill it with a "dummy" triangle. + if (indexCount == 0) { + if (isShortBuffer) { + for (int m = 0; m < 3; m++) { + ((ShortBuffer) lodBuffer.getData()).put((short) 0); + } + } else { + for (int m = 0; m < 3; m++) { + ((IntBuffer) lodBuffer.getData()).put(0); + } + } + } + + // Fill buffers. + Buffer buf = lodBuffer.getData(); + buf.rewind(); + for (Triangle triangle : triangleList) { + if (!triangle.isRemoved) { + assert (indexCount != 0); + if (isShortBuffer) { + for (int m = 0; m < 3; m++) { + ((ShortBuffer) buf).put((short) triangle.vertexId[m]); + + } + } else { + for (int m = 0; m < 3; m++) { + ((IntBuffer) buf).put(triangle.vertexId[m]); + } + + } + } + } + buf.clear(); + lodBuffer.updateData(buf); + return lodBuffer; + } + + private int calcLodTriCount(TriangleReductionMethod reductionMethod, float reductionValue) { + int nbTris = mesh.getTriangleCount(); + switch (reductionMethod) { + case PROPORTIONAL: + collapseCostLimit = NEVER_COLLAPSE_COST; + return (int) (nbTris - (nbTris * (reductionValue))); + + case CONSTANT: + collapseCostLimit = NEVER_COLLAPSE_COST; + if (reductionValue < nbTris) { + return nbTris - (int) reductionValue; + } + return 0; + + case COLLAPSE_COST: + collapseCostLimit = reductionValue; + return 0; + + default: + return nbTris; + } + } + + private int findDstID(int srcId, List tmpCollapsedEdges) { + int i = 0; + for (CollapsedEdge collapsedEdge : tmpCollapsedEdges) { + if (collapsedEdge.srcID == srcId) { + return i; + } + i++; + } + return Integer.MAX_VALUE; + } + + private class CollapsedEdge { + + int srcID; + int dstID; + }; + + private void removeTriangleFromEdges(Triangle triangle, Vertex skip) { + // skip is needed if we are iterating on the vertex's edges or triangles. + for (int i = 0; i < 3; i++) { + if (triangle.vertex[i] != skip) { + triangle.vertex[i].triangles.remove(triangle); + } + } + for (int i = 0; i < 3; i++) { + for (int n = 0; n < 3; n++) { + if (i != n) { + removeEdge(triangle.vertex[i], new Edge(triangle.vertex[n])); + } + } + } + } + + private void removeEdge(Vertex v, Edge edge) { + Edge ed = null; + for (Edge edge1 : v.edges) { + if (edge1.equals(edge)) { + ed = edge1; + break; + } + } + + if (ed.refCount == 1) { + v.edges.remove(ed); + } else { + ed.refCount--; + } + + } + + boolean isBorderVertex(Vertex vertex) { + for (Edge edge : vertex.edges) { + if (edge.refCount == 1) { + return true; + } + } + return false; + } + + private void addTriangleToEdges(Triangle tri) { + if (bestQuality) { + Triangle duplicate = getDuplicate(tri); + if (duplicate != null) { + if (!tri.isRemoved) { + tri.isRemoved = true; + indexCount -= 3; + logger.log(Level.FINE, "duplicate triangle found{0}{1} It will be excluded from Lod level calculations.", new Object[]{tri, duplicate}); + } + } + } + for (int i = 0; i < 3; i++) { + tri.vertex[i].triangles.add(tri); + } + for (int i = 0; i < 3; i++) { + for (int n = 0; n < 3; n++) { + if (i != n) { + addEdge(tri.vertex[i], new Edge(tri.vertex[n])); + } + } + } + } + + private void addEdge(Vertex v, Edge edge) { + assert (edge.destination != v); + + for (Edge ed : v.edges) { + if (ed.equals(edge)) { + ed.refCount++; + return; + } + } + + v.edges.add(edge); + edge.refCount = 1; + + } + + private void initialize() { + triangleList = new ArrayList(); + } + + private Triangle getDuplicate(Triangle triangle) { + // duplicate triangle detection (where all vertices has the same position) + for (Triangle tri : triangle.vertex[0].triangles) { + if (isDuplicateTriangle(triangle, tri)) { + return tri; + } + } + return null; + } + + private boolean isDuplicateTriangle(Triangle triangle, Triangle triangle2) { + for (int i = 0; i < 3; i++) { + if (triangle.vertex[i] != triangle2.vertex[0] + || triangle.vertex[i] != triangle2.vertex[1] + || triangle.vertex[i] != triangle2.vertex[2]) { + return false; + } + } + return true; + } + + private void replaceVertexID(Triangle triangle, int oldID, int newID, Vertex dst) { + dst.triangles.add(triangle); + // NOTE: triangle is not removed from src. This is implementation specific optimization. + + // Its up to the compiler to unroll everything. + for (int i = 0; i < 3; i++) { + if (triangle.vertexId[i] == oldID) { + for (int n = 0; n < 3; n++) { + if (i != n) { + // This is implementation specific optimization to remove following line. + //removeEdge(triangle.vertex[i], new Edge(triangle.vertex[n])); + + removeEdge(triangle.vertex[n], new Edge(triangle.vertex[i])); + addEdge(triangle.vertex[n], new Edge(dst)); + addEdge(dst, new Edge(triangle.vertex[n])); + } + } + triangle.vertex[i] = dst; + triangle.vertexId[i] = newID; + return; + } + } + assert (false); + } + + private void updateVertexCollapseCost(Vertex vertex) { + float collapseCost = UNINITIALIZED_COLLAPSE_COST; + Vertex collapseTo = null; + + for (Edge edge : vertex.edges) { + edge.collapseCost = computeEdgeCollapseCost(vertex, edge); + assert (edge.collapseCost != UNINITIALIZED_COLLAPSE_COST); + if (collapseCost > edge.collapseCost) { + collapseCost = edge.collapseCost; + collapseTo = edge.destination; + } + } + if (collapseCost != vertex.collapseCost || vertex.collapseTo != collapseTo) { + assert (vertex.collapseTo != null); + assert (find(collapseCostSet, vertex)); + collapseCostSet.remove(vertex); + if (collapseCost != UNINITIALIZED_COLLAPSE_COST) { + vertex.collapseCost = collapseCost; + vertex.collapseTo = collapseTo; + collapseCostSet.add(vertex); + } + } + assert (vertex.collapseCost != UNINITIALIZED_COLLAPSE_COST); + } + + private boolean hasSrcID(int srcID, List cEdges) { + // This will only return exact matches. + for (CollapsedEdge collapsedEdge : cEdges) { + if (collapsedEdge.srcID == srcID) { + return true; + } + } + + return false; // Not found + } + + private boolean collapse(Vertex src) { + Vertex dest = src.collapseTo; + if (src.edges.isEmpty()) { + return false; + } + assert (assertValidVertex(dest)); + assert (assertValidVertex(src)); + + assert (src.collapseCost != NEVER_COLLAPSE_COST); + assert (src.collapseCost != UNINITIALIZED_COLLAPSE_COST); + assert (!src.edges.isEmpty()); + assert (!src.triangles.isEmpty()); + assert (src.edges.contains(new Edge(dest))); + + // It may have vertexIDs and triangles from different submeshes(different vertex buffers), + // so we need to connect them correctly based on deleted triangle's edge. + // mCollapsedEdgeIDs will be used, when looking up the connections for replacement. + List tmpCollapsedEdges = new ArrayList(); + for (Iterator it = src.triangles.iterator(); it.hasNext();) { + Triangle triangle = it.next(); + if (triangle.hasVertex(dest)) { + // Remove a triangle + // Tasks: + // 1. Add it to the collapsed edges list. + // 2. Reduce index count for the Lods, which will not have this triangle. + // 3. Mark as removed, so it will not be added in upcoming Lod levels. + // 4. Remove references/pointers to this triangle. + + // 1. task + int srcID = triangle.getVertexIndex(src); + if (!hasSrcID(srcID, tmpCollapsedEdges)) { + CollapsedEdge cEdge = new CollapsedEdge(); + cEdge.srcID = srcID; + cEdge.dstID = triangle.getVertexIndex(dest); + tmpCollapsedEdges.add(cEdge); + } + + // 2. task + indexCount -= 3; + + // 3. task + triangle.isRemoved = true; + nbCollapsedTri++; + + // 4. task + removeTriangleFromEdges(triangle, src); + it.remove(); + + } + } + assert (!tmpCollapsedEdges.isEmpty()); + assert (!dest.edges.contains(new Edge(src))); + + + for (Iterator it = src.triangles.iterator(); it.hasNext();) { + Triangle triangle = it.next(); + if (!triangle.hasVertex(dest)) { + // Replace a triangle + // Tasks: + // 1. Determine the edge which we will move along. (we need to modify single vertex only) + // 2. Move along the selected edge. + + // 1. task + int srcID = triangle.getVertexIndex(src); + int id = findDstID(srcID, tmpCollapsedEdges); + if (id == Integer.MAX_VALUE) { + // Not found any edge to move along. + // Destroy the triangle. + // if (!triangle.isRemoved) { + triangle.isRemoved = true; + indexCount -= 3; + removeTriangleFromEdges(triangle, src); + it.remove(); + nbCollapsedTri++; + continue; + } + int dstID = tmpCollapsedEdges.get(id).dstID; + + // 2. task + replaceVertexID(triangle, srcID, dstID, dest); + + + if (bestQuality) { + triangle.computeNormal(); + } + + } + } + + if (bestQuality) { + for (Edge edge : src.edges) { + updateVertexCollapseCost(edge.destination); + } + updateVertexCollapseCost(dest); + for (Edge edge : dest.edges) { + updateVertexCollapseCost(edge.destination); + } + + } else { + // TODO: Find out why is this needed. assertOutdatedCollapseCost() fails on some + // rare situations without this. For example goblin.mesh fails. + //Treeset to have an ordered list with unique values + SortedSet updatable = new TreeSet(collapseComparator); + + for (Edge edge : src.edges) { + updatable.add(edge.destination); + for (Edge edge1 : edge.destination.edges) { + updatable.add(edge1.destination); + } + } + + + for (Vertex vertex : updatable) { + updateVertexCollapseCost(vertex); + } + + } + return true; + } + + private boolean assertValidMesh() { + // Allows to find bugs in collapsing. + for (Vertex vertex : collapseCostSet) { + assertValidVertex(vertex); + } + return true; + + } + + private boolean assertValidVertex(Vertex v) { + // Allows to find bugs in collapsing. + // System.out.println("Asserting " + v.index); + for (Triangle t : v.triangles) { + for (int i = 0; i < 3; i++) { + // System.out.println("check " + t.vertex[i].index); + + //assert (collapseCostSet.contains(t.vertex[i])); + assert (find(collapseCostSet, t.vertex[i])); + + assert (t.vertex[i].edges.contains(new Edge(t.vertex[i].collapseTo))); + for (int n = 0; n < 3; n++) { + if (i != n) { + + int id = t.vertex[i].edges.indexOf(new Edge(t.vertex[n])); + Edge ed = t.vertex[i].edges.get(id); + //assert (ed.collapseCost != UNINITIALIZED_COLLAPSE_COST); + } else { + assert (!t.vertex[i].edges.contains(new Edge(t.vertex[n]))); + } + } + } + } + return true; + } + + private boolean find(List set, Vertex v) { + for (Vertex vertex : set) { + if (v == vertex) { + return true; + } + } + return false; + } +}