Main repository of MikuMikuStudio
Revisão | bacce56db44fee70023b48e77f925303997355a7 (tree) |
---|---|
Hora | 2013-06-03 01:44:47 |
Autor | remy.bouquet@gmail.com <remy.bouquet@gmai...> |
Commiter | remy.bouquet@gmail.com |
Implemented a Lod Generator based on Ogre progressive mesh
git-svn-id: http://jmonkeyengine.googlecode.com/svn/trunk@10638 75d07b2b-3a1a-0410-a2c5-0572b91ccdca
@@ -0,0 +1,243 @@ | ||
1 | +/* | |
2 | + * Copyright (c) 2009-2012 jMonkeyEngine | |
3 | + * All rights reserved. | |
4 | + * | |
5 | + * Redistribution and use in source and binary forms, with or without | |
6 | + * modification, are permitted provided that the following conditions are | |
7 | + * met: | |
8 | + * | |
9 | + * * Redistributions of source code must retain the above copyright | |
10 | + * notice, this list of conditions and the following disclaimer. | |
11 | + * | |
12 | + * * Redistributions in binary form must reproduce the above copyright | |
13 | + * notice, this list of conditions and the following disclaimer in the | |
14 | + * documentation and/or other materials provided with the distribution. | |
15 | + * | |
16 | + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors | |
17 | + * may be used to endorse or promote products derived from this software | |
18 | + * without specific prior written permission. | |
19 | + * | |
20 | + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
21 | + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED | |
22 | + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | |
23 | + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR | |
24 | + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, | |
25 | + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, | |
26 | + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR | |
27 | + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF | |
28 | + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING | |
29 | + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | |
30 | + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
31 | + */ | |
32 | +package jme3test.stress; | |
33 | + | |
34 | +import com.jme3.animation.AnimChannel; | |
35 | +import com.jme3.animation.SkeletonControl; | |
36 | +import com.jme3.app.SimpleApplication; | |
37 | +import com.jme3.bounding.BoundingBox; | |
38 | +import com.jme3.font.BitmapText; | |
39 | +import com.jme3.input.ChaseCamera; | |
40 | +import com.jme3.input.KeyInput; | |
41 | +import com.jme3.input.controls.ActionListener; | |
42 | +import com.jme3.input.controls.KeyTrigger; | |
43 | +import com.jme3.light.AmbientLight; | |
44 | +import com.jme3.light.DirectionalLight; | |
45 | +import com.jme3.math.ColorRGBA; | |
46 | +import com.jme3.math.FastMath; | |
47 | +import com.jme3.math.Vector3f; | |
48 | +import com.jme3.scene.Geometry; | |
49 | +import com.jme3.scene.Node; | |
50 | +import com.jme3.scene.Spatial; | |
51 | +import com.jme3.scene.VertexBuffer; | |
52 | +import java.util.ArrayList; | |
53 | +import java.util.List; | |
54 | +import java.util.concurrent.Callable; | |
55 | +import java.util.concurrent.ScheduledThreadPoolExecutor; | |
56 | +import jme3tools.optimize.LodGenerator; | |
57 | + | |
58 | +public class TestLodGeneration extends SimpleApplication { | |
59 | + | |
60 | + public static void main(String[] args) { | |
61 | + TestLodGeneration app = new TestLodGeneration(); | |
62 | + app.start(); | |
63 | + } | |
64 | + boolean wireFrame = false; | |
65 | + float reductionvalue = 0.0f; | |
66 | + private int lodLevel = 0; | |
67 | + private Node model; | |
68 | + private BitmapText hudText; | |
69 | + private List<Geometry> listGeoms = new ArrayList<Geometry>(); | |
70 | + private ScheduledThreadPoolExecutor exec = new ScheduledThreadPoolExecutor(5); | |
71 | + private AnimChannel ch; | |
72 | + | |
73 | + public void simpleInitApp() { | |
74 | + | |
75 | + DirectionalLight dl = new DirectionalLight(); | |
76 | + dl.setDirection(new Vector3f(-1, -1, -1).normalizeLocal()); | |
77 | + rootNode.addLight(dl); | |
78 | + AmbientLight al = new AmbientLight(); | |
79 | + al.setColor(ColorRGBA.White.mult(0.6f)); | |
80 | + rootNode.addLight(al); | |
81 | + | |
82 | + model = (Node) assetManager.loadModel("Models/Sinbad/Sinbad.mesh.xml"); | |
83 | + //model = (Node) assetManager.loadModel("Models/Jaime/Jaime.j3o"); | |
84 | + BoundingBox b = ((BoundingBox) model.getWorldBound()); | |
85 | + model.setLocalScale(1.2f / (b.getYExtent() * 2)); | |
86 | + // model.setLocalTranslation(0,-(b.getCenter().y - b.getYExtent())* model.getLocalScale().y, 0); | |
87 | + for (Spatial spatial : model.getChildren()) { | |
88 | + if (spatial instanceof Geometry) { | |
89 | + listGeoms.add((Geometry) spatial); | |
90 | + } | |
91 | + } | |
92 | + ChaseCamera chaseCam = new ChaseCamera(cam, inputManager); | |
93 | + model.addControl(chaseCam); | |
94 | + chaseCam.setLookAtOffset(b.getCenter()); | |
95 | + chaseCam.setDefaultDistance(5); | |
96 | + chaseCam.setMinVerticalRotation(-FastMath.HALF_PI + 0.01f); | |
97 | + chaseCam.setZoomSensitivity(0.5f); | |
98 | + | |
99 | + | |
100 | + | |
101 | + // ch = model.getControl(AnimControl.class).createChannel(); | |
102 | + // ch.setAnim("Wave"); | |
103 | + SkeletonControl c = model.getControl(SkeletonControl.class); | |
104 | + if (c != null) { | |
105 | + c.setEnabled(false); | |
106 | + } | |
107 | + | |
108 | + | |
109 | + reductionvalue = 0.001f; | |
110 | + // makeLod(LodGenerator.VertexReductionMethod.PROPORTIONAL, reductionvalue, 1); | |
111 | + | |
112 | + lodLevel = 1; | |
113 | + for (final Geometry geometry : listGeoms) { | |
114 | + LodGenerator lODGenerator = new LodGenerator(geometry); | |
115 | + lODGenerator.bakeLods(LodGenerator.TriangleReductionMethod.PROPORTIONAL, reductionvalue); | |
116 | + geometry.setLodLevel(lodLevel); | |
117 | + | |
118 | + | |
119 | + } | |
120 | + | |
121 | + rootNode.attachChild(model); | |
122 | + flyCam.setEnabled(false); | |
123 | + | |
124 | + | |
125 | + | |
126 | + guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt"); | |
127 | + hudText = new BitmapText(guiFont, false); | |
128 | + hudText.setSize(guiFont.getCharSet().getRenderedSize()); | |
129 | + hudText.setText(computeNbTri() + " tris"); | |
130 | + hudText.setLocalTranslation(cam.getWidth() / 2, hudText.getLineHeight(), 0); | |
131 | + guiNode.attachChild(hudText); | |
132 | + | |
133 | + inputManager.addListener(new ActionListener() { | |
134 | + public void onAction(String name, boolean isPressed, float tpf) { | |
135 | + if (isPressed) { | |
136 | + if (name.equals("plus")) { | |
137 | +// lodLevel++; | |
138 | +// for (Geometry geometry : listGeoms) { | |
139 | +// if (geometry.getMesh().getNumLodLevels() <= lodLevel) { | |
140 | +// lodLevel = 0; | |
141 | +// } | |
142 | +// geometry.setLodLevel(lodLevel); | |
143 | +// } | |
144 | +// jaimeText.setText(computeNbTri() + " tris"); | |
145 | + | |
146 | + | |
147 | + | |
148 | + reductionvalue += 0.05f; | |
149 | + updateLod(); | |
150 | + | |
151 | + | |
152 | + | |
153 | + } | |
154 | + if (name.equals("minus")) { | |
155 | +// lodLevel--; | |
156 | +// for (Geometry geometry : listGeoms) { | |
157 | +// if (lodLevel < 0) { | |
158 | +// lodLevel = geometry.getMesh().getNumLodLevels() - 1; | |
159 | +// } | |
160 | +// geometry.setLodLevel(lodLevel); | |
161 | +// } | |
162 | +// jaimeText.setText(computeNbTri() + " tris"); | |
163 | + | |
164 | + | |
165 | + | |
166 | + reductionvalue -= 0.05f; | |
167 | + updateLod(); | |
168 | + | |
169 | + | |
170 | + } | |
171 | + if (name.equals("wireFrame")) { | |
172 | + wireFrame = !wireFrame; | |
173 | + for (Geometry geometry : listGeoms) { | |
174 | + geometry.getMaterial().getAdditionalRenderState().setWireframe(wireFrame); | |
175 | + } | |
176 | + } | |
177 | + | |
178 | + } | |
179 | + | |
180 | + } | |
181 | + | |
182 | + private void updateLod() { | |
183 | + reductionvalue = FastMath.clamp(reductionvalue, 0.0f, 1.0f); | |
184 | + makeLod(LodGenerator.TriangleReductionMethod.PROPORTIONAL, reductionvalue, 1); | |
185 | + } | |
186 | + }, "plus", "minus", "wireFrame"); | |
187 | + | |
188 | + inputManager.addMapping("plus", new KeyTrigger(KeyInput.KEY_ADD)); | |
189 | + inputManager.addMapping("minus", new KeyTrigger(KeyInput.KEY_SUBTRACT)); | |
190 | + inputManager.addMapping("wireFrame", new KeyTrigger(KeyInput.KEY_SPACE)); | |
191 | + | |
192 | + | |
193 | + | |
194 | + } | |
195 | + | |
196 | + @Override | |
197 | + public void simpleUpdate(float tpf) { | |
198 | + // model.rotate(0, tpf, 0); | |
199 | + } | |
200 | + | |
201 | + private int computeNbTri() { | |
202 | + int nbTri = 0; | |
203 | + for (Geometry geometry : listGeoms) { | |
204 | + if (geometry.getMesh().getNumLodLevels() > 0) { | |
205 | + nbTri += geometry.getMesh().getLodLevel(lodLevel).getNumElements(); | |
206 | + } else { | |
207 | + nbTri += geometry.getMesh().getTriangleCount(); | |
208 | + } | |
209 | + } | |
210 | + return nbTri; | |
211 | + } | |
212 | + | |
213 | + @Override | |
214 | + public void destroy() { | |
215 | + super.destroy(); | |
216 | + exec.shutdown(); | |
217 | + } | |
218 | + | |
219 | + private void makeLod(final LodGenerator.TriangleReductionMethod method, final float value, final int ll) { | |
220 | + exec.execute(new Runnable() { | |
221 | + public void run() { | |
222 | + for (final Geometry geometry : listGeoms) { | |
223 | + LodGenerator lODGenerator = new LodGenerator(geometry); | |
224 | + final VertexBuffer[] lods = lODGenerator.computeLods(method, value); | |
225 | + | |
226 | + enqueue(new Callable<Void>() { | |
227 | + public Void call() throws Exception { | |
228 | + geometry.getMesh().setLodLevels(lods); | |
229 | + lodLevel = 0; | |
230 | + if (geometry.getMesh().getNumLodLevels() > ll) { | |
231 | + lodLevel = ll; | |
232 | + } | |
233 | + geometry.setLodLevel(lodLevel); | |
234 | + hudText.setText(computeNbTri() + " tris"); | |
235 | + return null; | |
236 | + } | |
237 | + }); | |
238 | + } | |
239 | + } | |
240 | + }); | |
241 | + | |
242 | + } | |
243 | +} |
@@ -0,0 +1,1016 @@ | ||
1 | +/* | |
2 | + * Copyright (c) 2009-2013 jMonkeyEngine | |
3 | + * All rights reserved. | |
4 | + * | |
5 | + * Redistribution and use in source and binary forms, with or without | |
6 | + * modification, are permitted provided that the following conditions are | |
7 | + * met: | |
8 | + * | |
9 | + * * Redistributions of source code must retain the above copyright | |
10 | + * notice, this list of conditions and the following disclaimer. | |
11 | + * | |
12 | + * * Redistributions in binary form must reproduce the above copyright | |
13 | + * notice, this list of conditions and the following disclaimer in the | |
14 | + * documentation and/or other materials provided with the distribution. | |
15 | + * | |
16 | + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors | |
17 | + * may be used to endorse or promote products derived from this software | |
18 | + * without specific prior written permission. | |
19 | + * | |
20 | + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
21 | + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED | |
22 | + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | |
23 | + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR | |
24 | + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, | |
25 | + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, | |
26 | + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR | |
27 | + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF | |
28 | + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING | |
29 | + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | |
30 | + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
31 | + */ | |
32 | +package jme3tools.optimize; | |
33 | + | |
34 | +import com.jme3.bounding.BoundingSphere; | |
35 | +import com.jme3.math.Vector3f; | |
36 | +import com.jme3.scene.Geometry; | |
37 | +import com.jme3.scene.Mesh; | |
38 | +import com.jme3.scene.VertexBuffer; | |
39 | +import com.jme3.util.BufferUtils; | |
40 | +import java.nio.Buffer; | |
41 | +import java.nio.FloatBuffer; | |
42 | +import java.nio.IntBuffer; | |
43 | +import java.nio.ShortBuffer; | |
44 | +import java.util.ArrayList; | |
45 | +import java.util.Collections; | |
46 | +import java.util.Comparator; | |
47 | +import java.util.HashSet; | |
48 | +import java.util.Iterator; | |
49 | +import java.util.List; | |
50 | +import java.util.Set; | |
51 | +import java.util.SortedSet; | |
52 | +import java.util.TreeSet; | |
53 | +import java.util.logging.Level; | |
54 | +import java.util.logging.Logger; | |
55 | + | |
56 | +/** | |
57 | + * This is an utility class that allows to generated the lod levels for an | |
58 | + * arbitrary mesh. It computes a collapse cost for each vertex and each edges. | |
59 | + * The higher the cost the most likely collapsing the edge or the vertex will | |
60 | + * produce artifacts on the mesh. <p>This class is the java implementation of | |
61 | + * the enhanced version og Ogre engine Lod generator, by Péter Szücs, originally | |
62 | + * based on Stan Melax "easy mesh simplification". The MIT licences C++ source | |
63 | + * code can be found here | |
64 | + * https://github.com/worldforge/ember/tree/master/src/components/ogre/lod more | |
65 | + * informations can be found here http://www.melax.com/polychop | |
66 | + * http://sajty.elementfx.com/progressivemesh/GSoC2012.pdf </p> | |
67 | + * | |
68 | + * <p>The algorithm sort the vertice according to their collapsse cost | |
69 | + * ascending. It collapse from the "cheapest" vertex to the more expensive.<br> | |
70 | + * <strong>Usage : </strong><br> | |
71 | + * <pre> | |
72 | + * LodGenerator lODGenerator = new LodGenerator(geometry); | |
73 | + * lODGenerator.bakeLods(reductionMethod,reductionvalue); | |
74 | + * </pre> redutionMethod type is VertexReductionMethod described here | |
75 | + * {@link TriangleReductionMethod} reductionvalue depends on the | |
76 | + * reductionMethod<p> | |
77 | + * | |
78 | + * | |
79 | + * @author Nehon | |
80 | + */ | |
81 | +public class LodGenerator { | |
82 | + | |
83 | + private static final Logger logger = Logger.getLogger(LodGenerator.class.getName()); | |
84 | + private static final float NEVER_COLLAPSE_COST = Float.MAX_VALUE; | |
85 | + private static final float UNINITIALIZED_COLLAPSE_COST = Float.POSITIVE_INFINITY; | |
86 | + private Vector3f tmpV1 = new Vector3f(); | |
87 | + private Vector3f tmpV2 = new Vector3f(); | |
88 | + private boolean bestQuality = true; | |
89 | + private int indexCount = 0; | |
90 | + private List<Vertex> collapseCostSet = new ArrayList<Vertex>(); | |
91 | + private float collapseCostLimit; | |
92 | + private List<Triangle> triangleList; | |
93 | + private List<Vertex> vertexList = new ArrayList<Vertex>(); | |
94 | + private float meshBoundingSphereRadius; | |
95 | + private Mesh mesh; | |
96 | + | |
97 | + /** | |
98 | + * Describe the way trinagles will be removed. <br> PROPORTIONAL : | |
99 | + * Percentage of triangles to be removed from the mesh. Valid range is a | |
100 | + * number between 0.0 and 1.0 <br> CONSTANT : Triangle count to be removed | |
101 | + * from the mesh. Pass only integers or it will be rounded. <br> | |
102 | + * COLLAPSE_COST : Reduces the vertices, until the cost is bigger then the | |
103 | + * given value. Collapse cost is equal to the amount of artifact the | |
104 | + * reduction causes. This generates the best Lod output, but the collapse | |
105 | + * cost depends on implementation. | |
106 | + */ | |
107 | + public enum TriangleReductionMethod { | |
108 | + | |
109 | + /** | |
110 | + * Percentage of triangles to be removed from the mesh. | |
111 | + * | |
112 | + * Valid range is a number between 0.0 and 1.0 | |
113 | + */ | |
114 | + PROPORTIONAL, | |
115 | + /** | |
116 | + * Triangle count to be removed from the mesh. | |
117 | + * | |
118 | + * Pass only integers or it will be rounded. | |
119 | + */ | |
120 | + CONSTANT, | |
121 | + /** | |
122 | + * Reduces the vertices, until the cost is bigger then the given value. | |
123 | + * | |
124 | + * Collapse cost is equal to the amount of artifact the reduction | |
125 | + * causes. This generates the best Lod output, but the collapse cost | |
126 | + * depends on implementation. | |
127 | + */ | |
128 | + COLLAPSE_COST | |
129 | + }; | |
130 | + | |
131 | + private class Edge { | |
132 | + | |
133 | + Vertex destination; | |
134 | + float collapseCost = UNINITIALIZED_COLLAPSE_COST; | |
135 | + int refCount; | |
136 | + | |
137 | + public Edge(Vertex destination) { | |
138 | + this.destination = destination; | |
139 | + } | |
140 | + | |
141 | + public void set(Edge other) { | |
142 | + destination = other.destination; | |
143 | + collapseCost = other.collapseCost; | |
144 | + refCount = other.refCount; | |
145 | + } | |
146 | + | |
147 | + @Override | |
148 | + public boolean equals(Object obj) { | |
149 | + if (!(obj instanceof Edge)) { | |
150 | + return false; | |
151 | + } | |
152 | + return destination == ((Edge) obj).destination; | |
153 | + } | |
154 | + | |
155 | + @Override | |
156 | + public int hashCode() { | |
157 | + return destination.hashCode(); | |
158 | + } | |
159 | + | |
160 | + @Override | |
161 | + public String toString() { | |
162 | + return "Edge{" + "collapsTo " + destination.index + '}'; | |
163 | + } | |
164 | + } | |
165 | + | |
166 | + private class Vertex { | |
167 | + | |
168 | + Vector3f position = new Vector3f(); | |
169 | + float collapseCost = UNINITIALIZED_COLLAPSE_COST; | |
170 | + List<Edge> edges = new ArrayList<Edge>(); | |
171 | + Set<Triangle> triangles = new HashSet<Triangle>(); | |
172 | + Vertex collapseTo; | |
173 | + boolean isSeam; | |
174 | + int index;//index in the buffer for debugging | |
175 | + | |
176 | + @Override | |
177 | + public String toString() { | |
178 | + return index + " : " + position.toString(); | |
179 | + } | |
180 | + } | |
181 | + | |
182 | + private class Triangle { | |
183 | + | |
184 | + Vertex[] vertex = new Vertex[3]; | |
185 | + Vector3f normal; | |
186 | + boolean isRemoved; | |
187 | + //indices of the vertices in the vertex buffer | |
188 | + int[] vertexId = new int[3]; | |
189 | + | |
190 | + void computeNormal() { | |
191 | + // Cross-product 2 edges | |
192 | + tmpV1.set(vertex[1].position).subtractLocal(vertex[0].position); | |
193 | + tmpV2.set(vertex[2].position).subtractLocal(vertex[1].position); | |
194 | + | |
195 | + normal = tmpV1.cross(tmpV2); | |
196 | + normal.normalizeLocal(); | |
197 | + } | |
198 | + | |
199 | + boolean hasVertex(Vertex v) { | |
200 | + return (v == vertex[0] || v == vertex[1] || v == vertex[2]); | |
201 | + } | |
202 | + | |
203 | + int getVertexIndex(Vertex v) { | |
204 | + for (int i = 0; i < 3; i++) { | |
205 | + if (vertex[i] == v) { | |
206 | + return vertexId[i]; | |
207 | + } | |
208 | + } | |
209 | + throw new IllegalArgumentException("Vertex " + v + "is not part of triangle" + this); | |
210 | + } | |
211 | + | |
212 | + boolean isMalformed() { | |
213 | + return vertex[0] == vertex[1] || vertex[0] == vertex[2] || vertex[1] == vertex[2]; | |
214 | + } | |
215 | + | |
216 | + @Override | |
217 | + public String toString() { | |
218 | + String out = "Triangle{\n"; | |
219 | + for (int i = 0; i < 3; i++) { | |
220 | + out += vertexId[i] + " : " + vertex[i].toString() + "\n"; | |
221 | + } | |
222 | + out += '}'; | |
223 | + return out; | |
224 | + } | |
225 | + } | |
226 | + /** | |
227 | + * Compartator used to sort vertices according to their collapse cost | |
228 | + */ | |
229 | + private Comparator collapseComparator = new Comparator<Vertex>() { | |
230 | + public int compare(Vertex o1, Vertex o2) { | |
231 | + if (Float.compare(o1.collapseCost, o2.collapseCost) == 0) { | |
232 | + return 0; | |
233 | + } | |
234 | + if (o1.collapseCost < o2.collapseCost) { | |
235 | + return -1; | |
236 | + } | |
237 | + return 1; | |
238 | + } | |
239 | + }; | |
240 | + | |
241 | + /** | |
242 | + * Construct a LodGenerator for the given geometry | |
243 | + * | |
244 | + * @param geom the geometry to consider to generate de Lods. | |
245 | + */ | |
246 | + public LodGenerator(Geometry geom) { | |
247 | + mesh = geom.getMesh(); | |
248 | + build(); | |
249 | + } | |
250 | + | |
251 | + private void build() { | |
252 | + BoundingSphere bs = new BoundingSphere(); | |
253 | + bs.computeFromPoints(mesh.getFloatBuffer(VertexBuffer.Type.Position)); | |
254 | + meshBoundingSphereRadius = bs.getRadius(); | |
255 | + List<Vertex> vertexLookup = new ArrayList<Vertex>(); | |
256 | + initialize(); | |
257 | + | |
258 | + gatherVertexData(mesh, vertexLookup); | |
259 | + gatherIndexData(mesh, vertexLookup); | |
260 | + computeCosts(); | |
261 | + assert (assertValidMesh()); | |
262 | + | |
263 | + } | |
264 | + | |
265 | + private void gatherVertexData(Mesh mesh, List<Vertex> vertexLookup) { | |
266 | + | |
267 | + //in case the model is currently animating with software animation | |
268 | + //attempting to retrieve the bind position instead of the position. | |
269 | + VertexBuffer position = mesh.getBuffer(VertexBuffer.Type.BindPosePosition); | |
270 | + if (position == null) { | |
271 | + position = mesh.getBuffer(VertexBuffer.Type.Position); | |
272 | + } | |
273 | + FloatBuffer pos = (FloatBuffer) position.getDataReadOnly(); | |
274 | + pos.rewind(); | |
275 | + | |
276 | + while (pos.remaining() != 0) { | |
277 | + Vertex v = new Vertex(); | |
278 | + v.position.setX(pos.get()); | |
279 | + v.position.setY(pos.get()); | |
280 | + v.position.setZ(pos.get()); | |
281 | + v.isSeam = false; | |
282 | + Vertex existingV = findSimilar(v); | |
283 | + if (existingV != null) { | |
284 | + //vertex position already exists | |
285 | + existingV.isSeam = true; | |
286 | + v.isSeam = true; | |
287 | + } else { | |
288 | + vertexList.add(v); | |
289 | + } | |
290 | + vertexLookup.add(v); | |
291 | + } | |
292 | + pos.rewind(); | |
293 | + } | |
294 | + | |
295 | + private Vertex findSimilar(Vertex v) { | |
296 | + for (Vertex vertex : vertexList) { | |
297 | + if (vertex.position.equals(v.position)) { | |
298 | + return vertex; | |
299 | + } | |
300 | + } | |
301 | + return null; | |
302 | + } | |
303 | + | |
304 | + private void gatherIndexData(Mesh mesh, List<Vertex> vertexLookup) { | |
305 | + VertexBuffer indexBuffer = mesh.getBuffer(VertexBuffer.Type.Index); | |
306 | + indexCount = indexBuffer.getNumElements() * 3; | |
307 | + Buffer b = indexBuffer.getDataReadOnly(); | |
308 | + b.rewind(); | |
309 | + | |
310 | + while (b.remaining() != 0) { | |
311 | + Triangle tri = new Triangle(); | |
312 | + tri.isRemoved = false; | |
313 | + triangleList.add(tri); | |
314 | + for (int i = 0; i < 3; i++) { | |
315 | + if (b instanceof IntBuffer) { | |
316 | + tri.vertexId[i] = ((IntBuffer) b).get(); | |
317 | + } else { | |
318 | + tri.vertexId[i] = ((ShortBuffer) b).get(); | |
319 | + } | |
320 | + assert (tri.vertexId[i] < vertexLookup.size()); | |
321 | + tri.vertex[i] = vertexLookup.get(tri.vertexId[i]); | |
322 | + //debug only; | |
323 | + tri.vertex[i].index = tri.vertexId[i]; | |
324 | + } | |
325 | + if (tri.isMalformed()) { | |
326 | + if (!tri.isRemoved) { | |
327 | + 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()}); | |
328 | + tri.isRemoved = true; | |
329 | + indexCount -= 3; | |
330 | + } | |
331 | + | |
332 | + } else { | |
333 | + tri.computeNormal(); | |
334 | + addTriangleToEdges(tri); | |
335 | + } | |
336 | + } | |
337 | + b.rewind(); | |
338 | + } | |
339 | + | |
340 | + private void computeCosts() { | |
341 | + collapseCostSet.clear(); | |
342 | + | |
343 | + for (Vertex vertex : vertexList) { | |
344 | + | |
345 | + if (!vertex.edges.isEmpty()) { | |
346 | + computeVertexCollapseCost(vertex); | |
347 | + } else { | |
348 | + logger.log(Level.FINE, "Found isolated vertex {0} It will be excluded from Lod level calculations.", vertex); | |
349 | + } | |
350 | + } | |
351 | + assert (vertexList.size() == collapseCostSet.size()); | |
352 | + assert (checkCosts()); | |
353 | + } | |
354 | + | |
355 | + //Debug only | |
356 | + private boolean checkCosts() { | |
357 | + for (Vertex vertex : vertexList) { | |
358 | + boolean test = find(collapseCostSet, vertex); | |
359 | + if (!test) { | |
360 | + System.out.println("vertex " + vertex.index + " not present in collapse costs"); | |
361 | + return false; | |
362 | + } | |
363 | + } | |
364 | + return true; | |
365 | + } | |
366 | + | |
367 | + private void computeVertexCollapseCost(Vertex vertex) { | |
368 | + | |
369 | + vertex.collapseCost = UNINITIALIZED_COLLAPSE_COST; | |
370 | + assert (!vertex.edges.isEmpty()); | |
371 | + for (Edge edge : vertex.edges) { | |
372 | + edge.collapseCost = computeEdgeCollapseCost(vertex, edge); | |
373 | + assert (edge.collapseCost != UNINITIALIZED_COLLAPSE_COST); | |
374 | + if (vertex.collapseCost > edge.collapseCost) { | |
375 | + vertex.collapseCost = edge.collapseCost; | |
376 | + vertex.collapseTo = edge.destination; | |
377 | + } | |
378 | + } | |
379 | + assert (vertex.collapseCost != UNINITIALIZED_COLLAPSE_COST); | |
380 | + collapseCostSet.add(vertex); | |
381 | + } | |
382 | + | |
383 | + float computeEdgeCollapseCost(Vertex src, Edge dstEdge) { | |
384 | + // This is based on Ogre's collapse cost calculation algorithm. | |
385 | + | |
386 | + Vertex dest = dstEdge.destination; | |
387 | + | |
388 | + // Check for singular triangle destruction | |
389 | + // If src and dest both only have 1 triangle (and it must be a shared one) | |
390 | + // then this would destroy the shape, so don't do this | |
391 | + if (src.triangles.size() == 1 && dest.triangles.size() == 1) { | |
392 | + return NEVER_COLLAPSE_COST; | |
393 | + } | |
394 | + | |
395 | + // Degenerate case check | |
396 | + // Are we going to invert a face normal of one of the neighbouring faces? | |
397 | + // Can occur when we have a very small remaining edge and collapse crosses it | |
398 | + // Look for a face normal changing by > 90 degrees | |
399 | + for (Triangle triangle : src.triangles) { | |
400 | + // Ignore the deleted faces (those including src & dest) | |
401 | + if (!triangle.hasVertex(dest)) { | |
402 | + // Test the new face normal | |
403 | + Vertex pv0, pv1, pv2; | |
404 | + | |
405 | + // Replace src with dest wherever it is | |
406 | + pv0 = (triangle.vertex[0] == src) ? dest : triangle.vertex[0]; | |
407 | + pv1 = (triangle.vertex[1] == src) ? dest : triangle.vertex[1]; | |
408 | + pv2 = (triangle.vertex[2] == src) ? dest : triangle.vertex[2]; | |
409 | + | |
410 | + // Cross-product 2 edges | |
411 | + tmpV1.set(pv1.position).subtractLocal(pv0.position); | |
412 | + tmpV2.set(pv2.position).subtractLocal(pv1.position); | |
413 | + | |
414 | + //computing the normal | |
415 | + Vector3f newNormal = tmpV1.crossLocal(tmpV2); | |
416 | + newNormal.normalizeLocal(); | |
417 | + | |
418 | + // Dot old and new face normal | |
419 | + // If < 0 then more than 90 degree difference | |
420 | + if (newNormal.dot(triangle.normal) < 0.0f) { | |
421 | + // Don't do it! | |
422 | + return NEVER_COLLAPSE_COST; | |
423 | + } | |
424 | + } | |
425 | + } | |
426 | + | |
427 | + float cost; | |
428 | + | |
429 | + // Special cases | |
430 | + // If we're looking at a border vertex | |
431 | + if (isBorderVertex(src)) { | |
432 | + if (dstEdge.refCount > 1) { | |
433 | + // src is on a border, but the src-dest edge has more than one tri on it | |
434 | + // So it must be collapsing inwards | |
435 | + // Mark as very high-value cost | |
436 | + // curvature = 1.0f; | |
437 | + cost = 1.0f; | |
438 | + } else { | |
439 | + // Collapsing ALONG a border | |
440 | + // We can't use curvature to measure the effect on the model | |
441 | + // Instead, see what effect it has on 'pulling' the other border edges | |
442 | + // The more colinear, the less effect it will have | |
443 | + // So measure the 'kinkiness' (for want of a better term) | |
444 | + | |
445 | + // Find the only triangle using this edge. | |
446 | + // PMTriangle* triangle = findSideTriangle(src, dst); | |
447 | + | |
448 | + cost = 0.0f; | |
449 | + Vector3f collapseEdge = tmpV1.set(src.position).subtractLocal(dest.position); | |
450 | + collapseEdge.normalizeLocal(); | |
451 | + | |
452 | + for (Edge edge : src.edges) { | |
453 | + | |
454 | + Vertex neighbor = edge.destination; | |
455 | + //reference check intended | |
456 | + if (neighbor != dest && edge.refCount == 1) { | |
457 | + Vector3f otherBorderEdge = tmpV2.set(src.position).subtractLocal(neighbor.position); | |
458 | + otherBorderEdge.normalizeLocal(); | |
459 | + // This time, the nearer the dot is to -1, the better, because that means | |
460 | + // the edges are opposite each other, therefore less kinkiness | |
461 | + // Scale into [0..1] | |
462 | + float kinkiness = (otherBorderEdge.dot(collapseEdge) + 1.002f) * 0.5f; | |
463 | + cost = Math.max(cost, kinkiness); | |
464 | + } | |
465 | + } | |
466 | + } | |
467 | + } else { // not a border | |
468 | + | |
469 | + // Standard inner vertex | |
470 | + // Calculate curvature | |
471 | + // use the triangle facing most away from the sides | |
472 | + // to determine our curvature term | |
473 | + // Iterate over src's faces again | |
474 | + cost = 0.001f; | |
475 | + | |
476 | + for (Triangle triangle : src.triangles) { | |
477 | + float mincurv = 1.0f; // curve for face i and closer side to it | |
478 | + | |
479 | + for (Triangle triangle2 : src.triangles) { | |
480 | + if (triangle2.hasVertex(dest)) { | |
481 | + | |
482 | + // Dot product of face normal gives a good delta angle | |
483 | + float dotprod = triangle.normal.dot(triangle2.normal); | |
484 | + // NB we do (1-..) to invert curvature where 1 is high curvature [0..1] | |
485 | + // Whilst dot product is high when angle difference is low | |
486 | + mincurv = Math.min(mincurv, (1.002f - dotprod) * 0.5f); | |
487 | + } | |
488 | + } | |
489 | + cost = Math.max(cost, mincurv); | |
490 | + } | |
491 | + } | |
492 | + | |
493 | + // check for texture seam ripping | |
494 | + if (src.isSeam) { | |
495 | + if (!dest.isSeam) { | |
496 | + cost += meshBoundingSphereRadius; | |
497 | + } else { | |
498 | + cost += meshBoundingSphereRadius * 0.5; | |
499 | + } | |
500 | + } | |
501 | + | |
502 | + assert (cost >= 0); | |
503 | + // TODO: use squared distance. | |
504 | + return cost * src.position.distance(dest.position); | |
505 | + } | |
506 | + int nbCollapsedTri = 0; | |
507 | + | |
508 | + /** | |
509 | + * Computes the lod and return a list of VertexBuffers that can then be used | |
510 | + * for lod (use Mesg.setLodLevels(VertexBuffer[]))<br> | |
511 | + * | |
512 | + * This method must be fed with the reduction method | |
513 | + * {@link TriangleReductionMethod} and a list of reduction values.<br> for | |
514 | + * each value a lod will be generated. <br> The resulting array will always | |
515 | + * contain at index 0 the original index buffer of the mesh. <p> | |
516 | + * <strong>Important note :</strong> some meshes cannot be decimated, so the | |
517 | + * result of this method can varry depending of the given mesh. Also the | |
518 | + * reduction values are indicative and the produces mesh will not always | |
519 | + * meet the required reduction. | |
520 | + * | |
521 | + * @param reductionMethod the reduction method to use | |
522 | + * @param reductionValues the reduction value to use for each lod level. | |
523 | + * @return an array of VertexBuffers containing the different index buffers | |
524 | + * representing the lod levels. | |
525 | + */ | |
526 | + public VertexBuffer[] computeLods(TriangleReductionMethod reductionMethod, float... reductionValues) { | |
527 | + int tricount = triangleList.size(); | |
528 | + int lastBakeVertexCount = tricount; | |
529 | + int lodCount = reductionValues.length; | |
530 | + VertexBuffer[] lods = new VertexBuffer[lodCount + 1]; | |
531 | + int numBakedLods = 1; | |
532 | + lods[0] = mesh.getBuffer(VertexBuffer.Type.Index); | |
533 | + for (int curLod = 0; curLod < lodCount; curLod++) { | |
534 | + int neededTriCount = calcLodTriCount(reductionMethod, reductionValues[curLod]); | |
535 | + while (neededTriCount < tricount) { | |
536 | + Collections.sort(collapseCostSet, collapseComparator); | |
537 | + Iterator<Vertex> it = collapseCostSet.iterator(); | |
538 | + | |
539 | + if (it.hasNext()) { | |
540 | + Vertex v = it.next(); | |
541 | + if (v.collapseCost < collapseCostLimit) { | |
542 | + if (!collapse(v)) { | |
543 | + logger.log(Level.FINE, "Couldn''t collapse vertex{0}", v.index); | |
544 | + } | |
545 | + Iterator<Vertex> it2 = collapseCostSet.iterator(); | |
546 | + if (it2.hasNext()) { | |
547 | + it2.next(); | |
548 | + it2.remove();// Remove src from collapse costs. | |
549 | + } | |
550 | + | |
551 | + } else { | |
552 | + break; | |
553 | + } | |
554 | + } else { | |
555 | + break; | |
556 | + } | |
557 | + tricount = triangleList.size() - nbCollapsedTri; | |
558 | + } | |
559 | + logger.log(Level.FINE, "collapsed {0} tris", nbCollapsedTri); | |
560 | + boolean outSkipped = (lastBakeVertexCount == tricount); | |
561 | + if (!outSkipped) { | |
562 | + lastBakeVertexCount = tricount; | |
563 | + lods[curLod + 1] = makeLod(mesh); | |
564 | + numBakedLods++; | |
565 | + } | |
566 | + } | |
567 | + if (numBakedLods <= lodCount) { | |
568 | + VertexBuffer[] bakedLods = new VertexBuffer[numBakedLods]; | |
569 | + System.arraycopy(lods, 0, bakedLods, 0, numBakedLods); | |
570 | + return bakedLods; | |
571 | + } else { | |
572 | + return lods; | |
573 | + } | |
574 | + } | |
575 | + | |
576 | + /** | |
577 | + * Computes the lods and bake them into the mesh<br> | |
578 | + * | |
579 | + * This method must be fed with the reduction method | |
580 | + * {@link TriangleReductionMethod} and a list of reduction values.<br> for | |
581 | + * each value a lod will be generated. <p> <strong>Important note :</strong> | |
582 | + * some meshes cannot be decimated, so the result of this method can varry | |
583 | + * depending of the given mesh. Also the reduction values are indicative and | |
584 | + * the produces mesh will not always meet the required reduction. | |
585 | + * | |
586 | + * @param reductionMethod the reduction method to use | |
587 | + * @param reductionValues the reduction value to use for each lod level. | |
588 | + */ | |
589 | + public void bakeLods(TriangleReductionMethod reductionMethod, float... reductionValues) { | |
590 | + mesh.setLodLevels(computeLods(reductionMethod, reductionValues)); | |
591 | + } | |
592 | + | |
593 | + private VertexBuffer makeLod(Mesh mesh) { | |
594 | + VertexBuffer indexBuffer = mesh.getBuffer(VertexBuffer.Type.Index); | |
595 | + | |
596 | + boolean isShortBuffer = indexBuffer.getFormat() == VertexBuffer.Format.UnsignedShort; | |
597 | + // Create buffers. | |
598 | + VertexBuffer lodBuffer = new VertexBuffer(VertexBuffer.Type.Index); | |
599 | + int bufsize = indexCount == 0 ? 3 : indexCount; | |
600 | + | |
601 | + if (isShortBuffer) { | |
602 | + lodBuffer.setupData(VertexBuffer.Usage.Static, 3, VertexBuffer.Format.UnsignedShort, BufferUtils.createShortBuffer(bufsize)); | |
603 | + } else { | |
604 | + lodBuffer.setupData(VertexBuffer.Usage.Static, 3, VertexBuffer.Format.UnsignedInt, BufferUtils.createIntBuffer(bufsize)); | |
605 | + } | |
606 | + | |
607 | + | |
608 | + | |
609 | + lodBuffer.getData().rewind(); | |
610 | + //Check if we should fill it with a "dummy" triangle. | |
611 | + if (indexCount == 0) { | |
612 | + if (isShortBuffer) { | |
613 | + for (int m = 0; m < 3; m++) { | |
614 | + ((ShortBuffer) lodBuffer.getData()).put((short) 0); | |
615 | + } | |
616 | + } else { | |
617 | + for (int m = 0; m < 3; m++) { | |
618 | + ((IntBuffer) lodBuffer.getData()).put(0); | |
619 | + } | |
620 | + } | |
621 | + } | |
622 | + | |
623 | + // Fill buffers. | |
624 | + Buffer buf = lodBuffer.getData(); | |
625 | + buf.rewind(); | |
626 | + for (Triangle triangle : triangleList) { | |
627 | + if (!triangle.isRemoved) { | |
628 | + assert (indexCount != 0); | |
629 | + if (isShortBuffer) { | |
630 | + for (int m = 0; m < 3; m++) { | |
631 | + ((ShortBuffer) buf).put((short) triangle.vertexId[m]); | |
632 | + | |
633 | + } | |
634 | + } else { | |
635 | + for (int m = 0; m < 3; m++) { | |
636 | + ((IntBuffer) buf).put(triangle.vertexId[m]); | |
637 | + } | |
638 | + | |
639 | + } | |
640 | + } | |
641 | + } | |
642 | + buf.clear(); | |
643 | + lodBuffer.updateData(buf); | |
644 | + return lodBuffer; | |
645 | + } | |
646 | + | |
647 | + private int calcLodTriCount(TriangleReductionMethod reductionMethod, float reductionValue) { | |
648 | + int nbTris = mesh.getTriangleCount(); | |
649 | + switch (reductionMethod) { | |
650 | + case PROPORTIONAL: | |
651 | + collapseCostLimit = NEVER_COLLAPSE_COST; | |
652 | + return (int) (nbTris - (nbTris * (reductionValue))); | |
653 | + | |
654 | + case CONSTANT: | |
655 | + collapseCostLimit = NEVER_COLLAPSE_COST; | |
656 | + if (reductionValue < nbTris) { | |
657 | + return nbTris - (int) reductionValue; | |
658 | + } | |
659 | + return 0; | |
660 | + | |
661 | + case COLLAPSE_COST: | |
662 | + collapseCostLimit = reductionValue; | |
663 | + return 0; | |
664 | + | |
665 | + default: | |
666 | + return nbTris; | |
667 | + } | |
668 | + } | |
669 | + | |
670 | + private int findDstID(int srcId, List<CollapsedEdge> tmpCollapsedEdges) { | |
671 | + int i = 0; | |
672 | + for (CollapsedEdge collapsedEdge : tmpCollapsedEdges) { | |
673 | + if (collapsedEdge.srcID == srcId) { | |
674 | + return i; | |
675 | + } | |
676 | + i++; | |
677 | + } | |
678 | + return Integer.MAX_VALUE; | |
679 | + } | |
680 | + | |
681 | + private class CollapsedEdge { | |
682 | + | |
683 | + int srcID; | |
684 | + int dstID; | |
685 | + }; | |
686 | + | |
687 | + private void removeTriangleFromEdges(Triangle triangle, Vertex skip) { | |
688 | + // skip is needed if we are iterating on the vertex's edges or triangles. | |
689 | + for (int i = 0; i < 3; i++) { | |
690 | + if (triangle.vertex[i] != skip) { | |
691 | + triangle.vertex[i].triangles.remove(triangle); | |
692 | + } | |
693 | + } | |
694 | + for (int i = 0; i < 3; i++) { | |
695 | + for (int n = 0; n < 3; n++) { | |
696 | + if (i != n) { | |
697 | + removeEdge(triangle.vertex[i], new Edge(triangle.vertex[n])); | |
698 | + } | |
699 | + } | |
700 | + } | |
701 | + } | |
702 | + | |
703 | + private void removeEdge(Vertex v, Edge edge) { | |
704 | + Edge ed = null; | |
705 | + for (Edge edge1 : v.edges) { | |
706 | + if (edge1.equals(edge)) { | |
707 | + ed = edge1; | |
708 | + break; | |
709 | + } | |
710 | + } | |
711 | + | |
712 | + if (ed.refCount == 1) { | |
713 | + v.edges.remove(ed); | |
714 | + } else { | |
715 | + ed.refCount--; | |
716 | + } | |
717 | + | |
718 | + } | |
719 | + | |
720 | + boolean isBorderVertex(Vertex vertex) { | |
721 | + for (Edge edge : vertex.edges) { | |
722 | + if (edge.refCount == 1) { | |
723 | + return true; | |
724 | + } | |
725 | + } | |
726 | + return false; | |
727 | + } | |
728 | + | |
729 | + private void addTriangleToEdges(Triangle tri) { | |
730 | + if (bestQuality) { | |
731 | + Triangle duplicate = getDuplicate(tri); | |
732 | + if (duplicate != null) { | |
733 | + if (!tri.isRemoved) { | |
734 | + tri.isRemoved = true; | |
735 | + indexCount -= 3; | |
736 | + logger.log(Level.FINE, "duplicate triangle found{0}{1} It will be excluded from Lod level calculations.", new Object[]{tri, duplicate}); | |
737 | + } | |
738 | + } | |
739 | + } | |
740 | + for (int i = 0; i < 3; i++) { | |
741 | + tri.vertex[i].triangles.add(tri); | |
742 | + } | |
743 | + for (int i = 0; i < 3; i++) { | |
744 | + for (int n = 0; n < 3; n++) { | |
745 | + if (i != n) { | |
746 | + addEdge(tri.vertex[i], new Edge(tri.vertex[n])); | |
747 | + } | |
748 | + } | |
749 | + } | |
750 | + } | |
751 | + | |
752 | + private void addEdge(Vertex v, Edge edge) { | |
753 | + assert (edge.destination != v); | |
754 | + | |
755 | + for (Edge ed : v.edges) { | |
756 | + if (ed.equals(edge)) { | |
757 | + ed.refCount++; | |
758 | + return; | |
759 | + } | |
760 | + } | |
761 | + | |
762 | + v.edges.add(edge); | |
763 | + edge.refCount = 1; | |
764 | + | |
765 | + } | |
766 | + | |
767 | + private void initialize() { | |
768 | + triangleList = new ArrayList<LodGenerator.Triangle>(); | |
769 | + } | |
770 | + | |
771 | + private Triangle getDuplicate(Triangle triangle) { | |
772 | + // duplicate triangle detection (where all vertices has the same position) | |
773 | + for (Triangle tri : triangle.vertex[0].triangles) { | |
774 | + if (isDuplicateTriangle(triangle, tri)) { | |
775 | + return tri; | |
776 | + } | |
777 | + } | |
778 | + return null; | |
779 | + } | |
780 | + | |
781 | + private boolean isDuplicateTriangle(Triangle triangle, Triangle triangle2) { | |
782 | + for (int i = 0; i < 3; i++) { | |
783 | + if (triangle.vertex[i] != triangle2.vertex[0] | |
784 | + || triangle.vertex[i] != triangle2.vertex[1] | |
785 | + || triangle.vertex[i] != triangle2.vertex[2]) { | |
786 | + return false; | |
787 | + } | |
788 | + } | |
789 | + return true; | |
790 | + } | |
791 | + | |
792 | + private void replaceVertexID(Triangle triangle, int oldID, int newID, Vertex dst) { | |
793 | + dst.triangles.add(triangle); | |
794 | + // NOTE: triangle is not removed from src. This is implementation specific optimization. | |
795 | + | |
796 | + // Its up to the compiler to unroll everything. | |
797 | + for (int i = 0; i < 3; i++) { | |
798 | + if (triangle.vertexId[i] == oldID) { | |
799 | + for (int n = 0; n < 3; n++) { | |
800 | + if (i != n) { | |
801 | + // This is implementation specific optimization to remove following line. | |
802 | + //removeEdge(triangle.vertex[i], new Edge(triangle.vertex[n])); | |
803 | + | |
804 | + removeEdge(triangle.vertex[n], new Edge(triangle.vertex[i])); | |
805 | + addEdge(triangle.vertex[n], new Edge(dst)); | |
806 | + addEdge(dst, new Edge(triangle.vertex[n])); | |
807 | + } | |
808 | + } | |
809 | + triangle.vertex[i] = dst; | |
810 | + triangle.vertexId[i] = newID; | |
811 | + return; | |
812 | + } | |
813 | + } | |
814 | + assert (false); | |
815 | + } | |
816 | + | |
817 | + private void updateVertexCollapseCost(Vertex vertex) { | |
818 | + float collapseCost = UNINITIALIZED_COLLAPSE_COST; | |
819 | + Vertex collapseTo = null; | |
820 | + | |
821 | + for (Edge edge : vertex.edges) { | |
822 | + edge.collapseCost = computeEdgeCollapseCost(vertex, edge); | |
823 | + assert (edge.collapseCost != UNINITIALIZED_COLLAPSE_COST); | |
824 | + if (collapseCost > edge.collapseCost) { | |
825 | + collapseCost = edge.collapseCost; | |
826 | + collapseTo = edge.destination; | |
827 | + } | |
828 | + } | |
829 | + if (collapseCost != vertex.collapseCost || vertex.collapseTo != collapseTo) { | |
830 | + assert (vertex.collapseTo != null); | |
831 | + assert (find(collapseCostSet, vertex)); | |
832 | + collapseCostSet.remove(vertex); | |
833 | + if (collapseCost != UNINITIALIZED_COLLAPSE_COST) { | |
834 | + vertex.collapseCost = collapseCost; | |
835 | + vertex.collapseTo = collapseTo; | |
836 | + collapseCostSet.add(vertex); | |
837 | + } | |
838 | + } | |
839 | + assert (vertex.collapseCost != UNINITIALIZED_COLLAPSE_COST); | |
840 | + } | |
841 | + | |
842 | + private boolean hasSrcID(int srcID, List<CollapsedEdge> cEdges) { | |
843 | + // This will only return exact matches. | |
844 | + for (CollapsedEdge collapsedEdge : cEdges) { | |
845 | + if (collapsedEdge.srcID == srcID) { | |
846 | + return true; | |
847 | + } | |
848 | + } | |
849 | + | |
850 | + return false; // Not found | |
851 | + } | |
852 | + | |
853 | + private boolean collapse(Vertex src) { | |
854 | + Vertex dest = src.collapseTo; | |
855 | + if (src.edges.isEmpty()) { | |
856 | + return false; | |
857 | + } | |
858 | + assert (assertValidVertex(dest)); | |
859 | + assert (assertValidVertex(src)); | |
860 | + | |
861 | + assert (src.collapseCost != NEVER_COLLAPSE_COST); | |
862 | + assert (src.collapseCost != UNINITIALIZED_COLLAPSE_COST); | |
863 | + assert (!src.edges.isEmpty()); | |
864 | + assert (!src.triangles.isEmpty()); | |
865 | + assert (src.edges.contains(new Edge(dest))); | |
866 | + | |
867 | + // It may have vertexIDs and triangles from different submeshes(different vertex buffers), | |
868 | + // so we need to connect them correctly based on deleted triangle's edge. | |
869 | + // mCollapsedEdgeIDs will be used, when looking up the connections for replacement. | |
870 | + List<CollapsedEdge> tmpCollapsedEdges = new ArrayList<CollapsedEdge>(); | |
871 | + for (Iterator<Triangle> it = src.triangles.iterator(); it.hasNext();) { | |
872 | + Triangle triangle = it.next(); | |
873 | + if (triangle.hasVertex(dest)) { | |
874 | + // Remove a triangle | |
875 | + // Tasks: | |
876 | + // 1. Add it to the collapsed edges list. | |
877 | + // 2. Reduce index count for the Lods, which will not have this triangle. | |
878 | + // 3. Mark as removed, so it will not be added in upcoming Lod levels. | |
879 | + // 4. Remove references/pointers to this triangle. | |
880 | + | |
881 | + // 1. task | |
882 | + int srcID = triangle.getVertexIndex(src); | |
883 | + if (!hasSrcID(srcID, tmpCollapsedEdges)) { | |
884 | + CollapsedEdge cEdge = new CollapsedEdge(); | |
885 | + cEdge.srcID = srcID; | |
886 | + cEdge.dstID = triangle.getVertexIndex(dest); | |
887 | + tmpCollapsedEdges.add(cEdge); | |
888 | + } | |
889 | + | |
890 | + // 2. task | |
891 | + indexCount -= 3; | |
892 | + | |
893 | + // 3. task | |
894 | + triangle.isRemoved = true; | |
895 | + nbCollapsedTri++; | |
896 | + | |
897 | + // 4. task | |
898 | + removeTriangleFromEdges(triangle, src); | |
899 | + it.remove(); | |
900 | + | |
901 | + } | |
902 | + } | |
903 | + assert (!tmpCollapsedEdges.isEmpty()); | |
904 | + assert (!dest.edges.contains(new Edge(src))); | |
905 | + | |
906 | + | |
907 | + for (Iterator<Triangle> it = src.triangles.iterator(); it.hasNext();) { | |
908 | + Triangle triangle = it.next(); | |
909 | + if (!triangle.hasVertex(dest)) { | |
910 | + // Replace a triangle | |
911 | + // Tasks: | |
912 | + // 1. Determine the edge which we will move along. (we need to modify single vertex only) | |
913 | + // 2. Move along the selected edge. | |
914 | + | |
915 | + // 1. task | |
916 | + int srcID = triangle.getVertexIndex(src); | |
917 | + int id = findDstID(srcID, tmpCollapsedEdges); | |
918 | + if (id == Integer.MAX_VALUE) { | |
919 | + // Not found any edge to move along. | |
920 | + // Destroy the triangle. | |
921 | + // if (!triangle.isRemoved) { | |
922 | + triangle.isRemoved = true; | |
923 | + indexCount -= 3; | |
924 | + removeTriangleFromEdges(triangle, src); | |
925 | + it.remove(); | |
926 | + nbCollapsedTri++; | |
927 | + continue; | |
928 | + } | |
929 | + int dstID = tmpCollapsedEdges.get(id).dstID; | |
930 | + | |
931 | + // 2. task | |
932 | + replaceVertexID(triangle, srcID, dstID, dest); | |
933 | + | |
934 | + | |
935 | + if (bestQuality) { | |
936 | + triangle.computeNormal(); | |
937 | + } | |
938 | + | |
939 | + } | |
940 | + } | |
941 | + | |
942 | + if (bestQuality) { | |
943 | + for (Edge edge : src.edges) { | |
944 | + updateVertexCollapseCost(edge.destination); | |
945 | + } | |
946 | + updateVertexCollapseCost(dest); | |
947 | + for (Edge edge : dest.edges) { | |
948 | + updateVertexCollapseCost(edge.destination); | |
949 | + } | |
950 | + | |
951 | + } else { | |
952 | + // TODO: Find out why is this needed. assertOutdatedCollapseCost() fails on some | |
953 | + // rare situations without this. For example goblin.mesh fails. | |
954 | + //Treeset to have an ordered list with unique values | |
955 | + SortedSet<Vertex> updatable = new TreeSet<Vertex>(collapseComparator); | |
956 | + | |
957 | + for (Edge edge : src.edges) { | |
958 | + updatable.add(edge.destination); | |
959 | + for (Edge edge1 : edge.destination.edges) { | |
960 | + updatable.add(edge1.destination); | |
961 | + } | |
962 | + } | |
963 | + | |
964 | + | |
965 | + for (Vertex vertex : updatable) { | |
966 | + updateVertexCollapseCost(vertex); | |
967 | + } | |
968 | + | |
969 | + } | |
970 | + return true; | |
971 | + } | |
972 | + | |
973 | + private boolean assertValidMesh() { | |
974 | + // Allows to find bugs in collapsing. | |
975 | + for (Vertex vertex : collapseCostSet) { | |
976 | + assertValidVertex(vertex); | |
977 | + } | |
978 | + return true; | |
979 | + | |
980 | + } | |
981 | + | |
982 | + private boolean assertValidVertex(Vertex v) { | |
983 | + // Allows to find bugs in collapsing. | |
984 | + // System.out.println("Asserting " + v.index); | |
985 | + for (Triangle t : v.triangles) { | |
986 | + for (int i = 0; i < 3; i++) { | |
987 | + // System.out.println("check " + t.vertex[i].index); | |
988 | + | |
989 | + //assert (collapseCostSet.contains(t.vertex[i])); | |
990 | + assert (find(collapseCostSet, t.vertex[i])); | |
991 | + | |
992 | + assert (t.vertex[i].edges.contains(new Edge(t.vertex[i].collapseTo))); | |
993 | + for (int n = 0; n < 3; n++) { | |
994 | + if (i != n) { | |
995 | + | |
996 | + int id = t.vertex[i].edges.indexOf(new Edge(t.vertex[n])); | |
997 | + Edge ed = t.vertex[i].edges.get(id); | |
998 | + //assert (ed.collapseCost != UNINITIALIZED_COLLAPSE_COST); | |
999 | + } else { | |
1000 | + assert (!t.vertex[i].edges.contains(new Edge(t.vertex[n]))); | |
1001 | + } | |
1002 | + } | |
1003 | + } | |
1004 | + } | |
1005 | + return true; | |
1006 | + } | |
1007 | + | |
1008 | + private boolean find(List<Vertex> set, Vertex v) { | |
1009 | + for (Vertex vertex : set) { | |
1010 | + if (v == vertex) { | |
1011 | + return true; | |
1012 | + } | |
1013 | + } | |
1014 | + return false; | |
1015 | + } | |
1016 | +} |