FBX: add ear clipping triangulator

experimental
Kirill Vainer 9 years ago
parent 18db26292f
commit f9ce9e246c
  1. 25
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/file/FbxDump.java
  2. 8
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/mesh/FbxMesh.java
  3. 11
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/node/FbxNode.java
  4. 35
      jme3-plugins/src/main/java/com/jme3/scene/plugins/IrUtils.java
  5. 241
      jme3-plugins/src/main/java/com/jme3/scene/plugins/triangulator/EarClippingTriangulator.java
  6. 64
      jme3-plugins/src/test/java/com/jme3/scene/plugins/triangulator/TriangulatorTest.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.
*

@ -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;

@ -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);
}
}

@ -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);
}
}

@ -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();
}
}
}

@ -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);
}
}
Loading…
Cancel
Save