diff --git a/jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/file/FbxDump.java b/jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/file/FbxDump.java index 00619dce7..d0e1d05c6 100644 --- a/jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/file/FbxDump.java +++ b/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. * diff --git a/jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/mesh/FbxMesh.java b/jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/mesh/FbxMesh.java index 5dd911bed..2278b1695 100644 --- a/jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/mesh/FbxMesh.java +++ b/jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/mesh/FbxMesh.java @@ -147,7 +147,8 @@ public final class FbxMesh extends FbxNodeAttribute> { 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> { 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> { // 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; diff --git a/jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/node/FbxNode.java b/jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/node/FbxNode.java index c0ce06d4b..05e031328 100644 --- a/jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/node/FbxNode.java +++ b/jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/node/FbxNode.java @@ -293,7 +293,8 @@ public class FbxNode extends FbxObject { 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 { // 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 { 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 { 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); } } diff --git a/jme3-plugins/src/main/java/com/jme3/scene/plugins/IrUtils.java b/jme3-plugins/src/main/java/com/jme3/scene/plugins/IrUtils.java index 7b20e1670..18b6a25e2 100644 --- a/jme3-plugins/src/main/java/com/jme3/scene/plugins/IrUtils.java +++ b/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 newPolygons = new ArrayList(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); } } diff --git a/jme3-plugins/src/main/java/com/jme3/scene/plugins/triangulator/EarClippingTriangulator.java b/jme3-plugins/src/main/java/com/jme3/scene/plugins/triangulator/EarClippingTriangulator.java new file mode 100644 index 000000000..df6b98d02 --- /dev/null +++ b/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 + *
    + *
  • http://www.geometrictools.com/Documentation/TriangulationByEarClipping.pdf
  • + *
  • http://cgm.cs.mcgill.ca/~godfried/teaching/cg-projects/97/Ian/algorithm2.html
  • + *
+ */ +public final class EarClippingTriangulator { + + private static enum VertexType { + Convex, + Reflex, + Ear; + } + + private final ArrayList indices = new ArrayList(); + private final ArrayList types = new ArrayList(); + private final ArrayList positions = new ArrayList(); + + 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(); + } + } +} diff --git a/jme3-plugins/src/test/java/com/jme3/scene/plugins/triangulator/TriangulatorTest.java b/jme3-plugins/src/test/java/com/jme3/scene/plugins/triangulator/TriangulatorTest.java new file mode 100644 index 000000000..c1a0b2ffa --- /dev/null +++ b/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); + } + +}