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

jme3tools.optimize.LodGenerator Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2009-2021 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 class is the java implementation of
 * the enhanced version of Ogre Engine LOD generator, by Péter Szücs, originally
 * based on Stan Melax "easy mesh simplification". The MIT licenced C++ source
 * code can be found here
 * https://github.com/worldforge/ember/tree/master/src/components/ogre/lod
 * The licencing for the original code is : 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * 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 a utility class that adds the ability to generate LOD levels
 * for an arbitrary mesh. It computes a collapse cost for each vertex and each edge.
 * The higher the cost the more likely collapsing the edge or the vertex will
 * produce artifacts on the mesh. 

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

* *

The algorithm sorts vertices according to their collapse cost in * ascending order. It collapses from the "cheapest" vertex to the more expensive.
* Usage:
*

 *      LodGenerator lODGenerator = new LodGenerator(geometry);
 *      lODGenerator.bakeLods(reductionMethod,reductionValue);
 * 
reductionMethod type is VertexReductionMethod described here * {@link TriangleReductionMethod} reduction value 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; final private Mesh mesh; /** * Enumerate criteria for removing triangles. */ public enum TriangleReductionMethod { /** * Percentage of triangles to be removed from the mesh. * * Valid range is a number between 0.0 and 1.0 */ PROPORTIONAL, /** * Number of triangles to be removed from the mesh. * * Pass an integer or it will be rounded. */ CONSTANT, /** * Collapses vertices until the cost exceeds the given value. * * Collapse cost indicates how much inaccuracy the * reduction causes. This generates the best LOD output, but the collapse * cost is implementation-dependant. */ 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{" + "collapseTo " + 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; } } /** * Comparator used to sort vertices according to their collapse cost */ final private Comparator collapseComparator = new Comparator() { @Override 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; } }; /** * Constructs an LodGenerator for the given Mesh. * * @param mesh the mesh for which to generate LODs. */ public LodGenerator(Mesh mesh) { this.mesh = mesh; build(); } /** * Constructs an LodGenerator for the given Geometry. * * @param geom the geometry for which to generate 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 { //bit shift to avoid negative values due to conversion form short to int. //we need an unsigned int here. tri.vertexId[i] = ((ShortBuffer) b).get()& 0xffff; } // 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) { if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "malformed triangle found with ID:{0}\n{1} It will be excluded from LOD 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 calculations.", vertex); } } // assert (vertexList.size() == collapseCostSet.size()); // assert (checkCosts()); } 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 collinear, 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); return cost * src.position.distanceSquared(dest.position); } int nbCollapsedTri = 0; /** * Computes the LODs and returns an array of VertexBuffers that can * be passed to Mesh.setLodLevels().
* * 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 vary 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++; } } return cleanBuffer(lods, numBakedLods); } private VertexBuffer[] cleanBuffer(VertexBuffer[] lods, int numBakedLods) { int index = 0; VertexBuffer[] result = new VertexBuffer[numBakedLods]; for (VertexBuffer lod : lods) { if (lod != null) { result[index] = lod; index++; } } return result; } /** * Computes the LODs and bakes 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 vary * depending on the given mesh. Also, the reduction values are approximate, and * the algorithm won't always achieve the specified 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; if (logger.isLoggable(Level.FINE)) { 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; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy