jme3tools.optimize.LodGenerator Maven / Gradle / Ivy
Show all versions of jme3-core Show documentation
/*
* 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;
}
}