Переглянути джерело

FBX: add ear clipping triangulator

Kirill Vainer 9 роки тому
батько
коміт
f9ce9e246c

+ 25 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/file/FbxDump.java

@@ -31,6 +31,9 @@
  */
 package com.jme3.scene.plugins.fbx.file;
 
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.PrintStream;
 import java.lang.reflect.Array;
@@ -88,6 +91,28 @@ public final class FbxDump {
         dumpFile(file, System.out);
     }
     
+    /**
+     * Dump FBX to standard output.
+     * 
+     * @param file the file to dump.
+     */
+    public static void dumpFile(String file) {
+        InputStream in = null;
+        try {
+            in = new FileInputStream(file);
+            FbxFile scene = FbxReader.readFBX(in);
+            FbxDump.dumpFile(scene);
+        } catch (IOException ex) {
+            throw new RuntimeException(ex);
+        } finally {
+            if (in != null) {
+                try {
+                    in.close();
+                } catch (IOException ex) { }
+            }
+        }
+    }
+    
     /**
      * Dump FBX to the given output stream.
      * 

+ 5 - 3
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/mesh/FbxMesh.java

@@ -147,7 +147,8 @@ public final class FbxMesh extends FbxNodeAttribute<IntMap<Mesh>> {
     public void connectObject(FbxObject object) {
         if (object instanceof FbxSkinDeformer) {
             if (skinDeformer != null) {
-                logger.log(Level.WARNING, "This mesh already has a skin deformer attached. Ignoring.");
+                logger.log(Level.WARNING, "This mesh already has a skin "
+                                        + "deformer attached: {0}. Ignoring.", this);
                 return;
             }
             skinDeformer = (FbxSkinDeformer) object;
@@ -237,7 +238,7 @@ public final class FbxMesh extends FbxNodeAttribute<IntMap<Mesh>> {
        
         if (jmeMeshes.size() == 0) {
             // When will this actually happen? Not sure.
-            logger.log(Level.WARNING, "Empty FBX mesh found (unusual).");
+            logger.log(Level.WARNING, "Empty FBX mesh found: {0} (unusual).", this);
         }
         
         // IMPORTANT: If we have a -1 entry, those are triangles
@@ -245,7 +246,8 @@ public final class FbxMesh extends FbxNodeAttribute<IntMap<Mesh>> {
         // It makes sense only if the mesh uses a single material!
         if (jmeMeshes.containsKey(-1) && jmeMeshes.size() > 1) {
             logger.log(Level.WARNING, "Mesh has polygons with no material "
-                                    + "indices (unusual) - they will use material index 0.");
+                                    + "indices: {0} (unusual) - "
+                                    + "they will use material index 0.", this);
         }
         
         return jmeMeshes;

+ 8 - 3
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/node/FbxNode.java

@@ -293,7 +293,8 @@ public class FbxNode extends FbxObject<Spatial> {
                     float z = ((Double) e2.properties.get(6)).floatValue();
                     userDataValue = new Vector3f(x, y, z);
                 } else {
-                    logger.log(Level.WARNING, "Unsupported user data type: {0}. Ignoring.", userDataType);
+                    logger.log(Level.WARNING, "Unsupported user data type: {0}. "
+                                            + "Ignoring.", userDataType);
                     continue;
                 }
                 
@@ -329,6 +330,9 @@ public class FbxNode extends FbxObject<Spatial> {
             // Material index does not exist. Create default material.
             jmeMat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
             jmeMat.setReceivesShadows(true);
+            logger.log(Level.WARNING, "Material index {0} is undefined in: {1}. "
+                                    + "Will use default material.",
+                                    new Object[]{materialIndex, this});
         } else {
             FbxMaterial fbxMat = materials.get(materialIndex);
             jmeMat = fbxMat.getJmeObject();
@@ -400,7 +404,8 @@ public class FbxNode extends FbxObject<Spatial> {
             
             if (jmeMeshes == null || jmeMeshes.size() == 0) {
                 // No meshes found on FBXMesh (??)
-                logger.log(Level.WARNING, "No meshes could be loaded. Creating empty node.");
+                logger.log(Level.WARNING, "No meshes could be loaded: {0}. "
+                                        + "Creating empty node.", this);
                 spatial = new Node(getName() + "-node");
             } else {
                 // Multiple jME3 geometries required for a single FBXMesh.
@@ -437,7 +442,7 @@ public class FbxNode extends FbxObject<Spatial> {
             if (!FastMath.approximateEquals(localScale.x, localScale.y) || 
                 !FastMath.approximateEquals(localScale.x, localScale.z)) {
                 logger.log(Level.WARNING, "Non-uniform scale detected on parent node. " +
-                                          "The model may appear distorted.");
+                                          "The model {1} may appear distorted.", this);
             }
         }
         

+ 26 - 9
jme3-plugins/src/main/java/com/jme3/scene/plugins/IrUtils.java

@@ -37,6 +37,7 @@ import com.jme3.scene.VertexBuffer;
 import com.jme3.scene.mesh.IndexBuffer;
 import com.jme3.scene.mesh.IndexIntBuffer;
 import com.jme3.scene.mesh.IndexShortBuffer;
+import com.jme3.scene.plugins.triangulator.EarClippingTriangulator;
 import com.jme3.util.BufferUtils;
 import com.jme3.util.IntMap;
 import java.nio.ByteBuffer;
@@ -172,23 +173,40 @@ public final class IrUtils {
         }
     }
     
+    private static void dumpPoly(IrPolygon polygon) {
+        System.out.println("Polygon with " + polygon.vertices.length + " vertices");
+        for (IrVertex vertex : polygon.vertices) {
+            System.out.println("\t" + vertex.pos);
+        }
+    }
+    
     /**
      * Convert mesh from quads / triangles to triangles only.
      */
     public static void triangulate(IrMesh mesh) {
         List<IrPolygon> newPolygons = new ArrayList<IrPolygon>(mesh.polygons.length);
+        EarClippingTriangulator triangulator = new EarClippingTriangulator();
         for (IrPolygon inputPoly : mesh.polygons) {
-            if (inputPoly.vertices.length == 4) {
+            int numVertices = inputPoly.vertices.length;
+
+            if (numVertices < 3) {
+                // point / edge
+                logger.log(Level.WARNING, "Point or edge encountered. Ignoring.");
+            } else if (numVertices == 3) {
+                // triangle
+                newPolygons.add(inputPoly);
+            } else if (numVertices == 4) {
+                // quad
                 IrPolygon[] tris = quadToTri(inputPoly);
                 newPolygons.add(tris[0]);
                 newPolygons.add(tris[1]);
-            } else if (inputPoly.vertices.length == 3) {
-                newPolygons.add(inputPoly);
             } else {
-                // N-gon. We have to ignore it..
-                logger.log(Level.WARNING, "N-gon encountered, ignoring. "
-                                        + "The mesh may not appear correctly. "
-                                        + "Triangulate your model prior to export.");
+                // N-gon
+                dumpPoly(inputPoly);
+                IrPolygon[] tris = triangulator.triangulate(inputPoly);
+                for (IrPolygon tri : tris) {
+                    newPolygons.add(tri);
+                }
             }
         }
         mesh.polygons = new IrPolygon[newPolygons.size()];
@@ -373,12 +391,11 @@ public final class IrUtils {
                         boneIndices.put((byte)0);
                         boneWeights.put(0f);
                     }
+                    maxBonesPerVertex = Math.max(maxBonesPerVertex, vertex.boneWeightsIndices.length);
                 } else {
                     boneIndices.putInt(0);
                     boneWeights.put(0f).put(0f).put(0f).put(0f);
                 }
-                
-                maxBonesPerVertex = Math.max(maxBonesPerVertex, vertex.boneWeightsIndices.length);
             }
         }
         

+ 241 - 0
jme3-plugins/src/main/java/com/jme3/scene/plugins/triangulator/EarClippingTriangulator.java

@@ -0,0 +1,241 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins.triangulator;
+
+import com.jme3.math.FastMath;
+import com.jme3.math.Matrix3f;
+import com.jme3.math.Vector2f;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.plugins.IrPolygon;
+import com.jme3.scene.plugins.IrVertex;
+import java.util.ArrayList;
+
+/**
+ * Implemented according to
+ * <ul>
+ * <li>http://www.geometrictools.com/Documentation/TriangulationByEarClipping.pdf</li>
+ * <li>http://cgm.cs.mcgill.ca/~godfried/teaching/cg-projects/97/Ian/algorithm2.html</li>
+ * </ul>
+ */
+public final class EarClippingTriangulator {
+
+    private static enum VertexType {
+        Convex,
+        Reflex,
+        Ear;
+    }
+    
+    private final ArrayList<Integer> indices = new ArrayList<Integer>();
+    private final ArrayList<VertexType> types = new ArrayList<VertexType>();
+    private final ArrayList<Vector2f> positions = new ArrayList<Vector2f>();
+    
+    public EarClippingTriangulator() {
+    }
+    
+    private static int ccw(Vector2f p0, Vector2f p1, Vector2f p2) {
+        float result = (p1.x - p0.x) * (p2.y - p1.y) - (p1.y - p0.y) * (p2.x - p1.x);
+        if (result > 0) {
+            return 1;
+        } else if (result < 0) {
+            return -1;
+        } else {
+            return 0;
+        }
+    }
+    
+    private static boolean pointInTriangle(Vector2f t0, Vector2f t1, Vector2f t2, Vector2f p) {
+        float d = ((t1.y - t2.y) * (t0.x - t2.x) + (t2.x - t1.x) * (t0.y - t2.y));
+        float a = ((t1.y - t2.y) * (p.x - t2.x) + (t2.x - t1.x) * (p.y - t2.y)) / d;
+        float b = ((t2.y - t0.y) * (p.x - t2.x) + (t0.x - t2.x) * (p.y - t2.y)) / d;
+        float c = 1 - a - b;
+        return 0 <= a && a <= 1 && 0 <= b && b <= 1 && 0 <= c && c <= 1;
+    }
+    
+    private static Matrix3f normalToMatrix(Vector3f norm) {
+        Vector3f tang1 = norm.cross(Vector3f.UNIT_X);
+        if (tang1.lengthSquared() < FastMath.ZERO_TOLERANCE) {
+            tang1 = norm.cross(Vector3f.UNIT_Y);
+        }
+        tang1.normalizeLocal();
+        Vector3f tang2 = norm.cross(tang1).normalizeLocal();
+
+        return new Matrix3f(
+                tang1.x, tang1.y, tang1.z,
+                tang2.x, tang2.y, tang2.z,
+                norm.x, norm.y, norm.z);
+    }
+    
+    private int prev(int index) {
+        if (index == 0) {
+            return indices.size() - 1;
+        } else {
+            return index - 1;
+        }
+    }
+    
+    private int next(int index) {
+        if (index == indices.size() - 1) {
+            return 0;
+        } else {
+            return index + 1;
+        }
+    }
+    
+    private VertexType calcType(int index) {
+        int prev = prev(index);
+        int next = next(index);
+        
+        Vector2f p0 = positions.get(prev);
+        Vector2f p1 = positions.get(index);
+        Vector2f p2 = positions.get(next);
+
+        if (ccw(p0, p1, p2) <= 0) {
+            return VertexType.Reflex;
+        } else {
+            for (int i = 0; i < positions.size() - 3; i++) {
+                int testIndex = (index + 2 + i) % positions.size();
+                if (types.get(testIndex) != VertexType.Reflex) {
+                    continue;
+                }
+                Vector2f p = positions.get(testIndex);
+                if (pointInTriangle(p0, p1, p2, p)) {
+                    return VertexType.Convex;
+                }
+            }
+            return VertexType.Ear;
+        }
+    }
+    
+    private void updateType(int index) {
+        if (types.get(index) == VertexType.Convex) {
+            return;
+        }
+        types.set(index, calcType(index));
+    }
+    
+    private void loadVertices(IrVertex[] vertices) {
+        indices.ensureCapacity(vertices.length);
+        types.ensureCapacity(vertices.length);
+        positions.ensureCapacity(vertices.length);
+        
+        Vector3f normal = FastMath.computeNormal(
+                                        vertices[0].pos,
+                                        vertices[1].pos,
+                                        vertices[2].pos);
+
+        Matrix3f transform = normalToMatrix(normal);
+        
+        for (int i = 0; i < vertices.length; i++) {
+            Vector3f projected = transform.mult(vertices[i].pos);
+            indices.add(i);
+            positions.add(new Vector2f(projected.x, projected.y));
+            types.add(VertexType.Reflex);
+        }
+        
+        for (int i = 0; i < vertices.length; i++) {
+            types.set(i, calcType(i));
+        }
+    }
+    
+    private IrPolygon createTriangle(IrPolygon polygon, int prev, int index, int next) { 
+        int p0 = indices.get(prev);
+        int p1 = indices.get(index);
+        int p2 = indices.get(next);
+        IrPolygon triangle = new IrPolygon();
+        triangle.vertices = new IrVertex[] {
+            polygon.vertices[p0],
+            polygon.vertices[p1],
+            polygon.vertices[p2],
+        };
+        return triangle;
+    }
+    
+    /**
+     * Triangulates the given polygon.
+     * 
+     * Five or more vertices are required, if less are given, an exception
+     * is thrown.
+     * 
+     * @param polygon The polygon to triangulate.
+     * @return N - 2 triangles, where N is the number of vertices in the polygon.
+     * 
+     * @throws IllegalArgumentException If the polygon has less than 5 vertices.
+     */
+    public IrPolygon[] triangulate(IrPolygon polygon) {
+        if (polygon.vertices.length < 5) {
+            throw new IllegalArgumentException("Only polygons with 5 or more vertices are supported");
+        }
+        
+        try {
+            int numTris = 0;
+            IrPolygon[] triangles = new IrPolygon[polygon.vertices.length - 2];
+            
+            loadVertices(polygon.vertices);
+            
+            int index = 0;
+            while (types.size() > 3) {
+                if (types.get(index) == VertexType.Ear) {
+                    int prev = prev(index);
+                    int next = next(index);
+                    
+                    triangles[numTris++] = createTriangle(polygon, prev, index, next);
+                    
+                    indices.remove(index);
+                    types.remove(index);
+                    positions.remove(index);
+                    
+                    next = next(prev);
+                    updateType(prev);
+                    updateType(next);
+                    
+                    index = next(next);
+                } else {
+                    index = next(index);
+                }
+            }
+            
+            if (types.size() == 3) {
+                triangles[numTris++] = createTriangle(polygon, 0, 1, 2);
+            }
+            
+            if (numTris != triangles.length) {
+                throw new AssertionError("Triangulation failed to generate enough triangles");
+            }
+            
+            return triangles;
+        } finally {
+            indices.clear();
+            positions.clear();
+            types.clear();
+        }
+    }
+}

+ 64 - 0
jme3-plugins/src/test/java/com/jme3/scene/plugins/triangulator/TriangulatorTest.java

@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins.triangulator;
+
+import com.jme3.math.Vector3f;
+import com.jme3.scene.plugins.IrPolygon;
+import com.jme3.scene.plugins.IrVertex;
+import junit.framework.TestCase;
+
+public class TriangulatorTest extends TestCase {
+    
+    public void testTriangulator() {
+        Vector3f[] dataSet = new Vector3f[]{
+            new Vector3f(0.75f, 0.3f, 1.2f),
+            new Vector3f(0.75f, 0.3f, 0.0f),
+            new Vector3f(0.75f, 0.17f, 0.0f),
+            new Vector3f(0.75000095f, 0.17f, 1.02f),
+            new Vector3f(0.75f, -0.17f, 1.02f),
+            new Vector3f(0.75f, -0.17f, 0.0f),
+            new Vector3f(0.75f, -0.3f, 0.0f),
+            new Vector3f(0.75f, -0.3f, 1.2f)
+        };
+        
+        IrPolygon poly = new IrPolygon();
+        poly.vertices = new IrVertex[dataSet.length];
+        for (int i = 0; i < dataSet.length; i++) {
+            poly.vertices[i] = new IrVertex();
+            poly.vertices[i].pos = dataSet[i];
+        }
+        
+        EarClippingTriangulator triangulator = new EarClippingTriangulator();
+        triangulator.triangulate(poly);
+    }
+    
+}