Quellcode durchsuchen

Merge branch 'renderer-rgtc' into experimental

Kirill Vainer vor 9 Jahren
Ursprung
Commit
f9500f955f

+ 6 - 1
jme3-core/src/main/java/com/jme3/renderer/Caps.java

@@ -349,7 +349,12 @@ public enum Caps {
     /**
      * GPU can provide and accept binary shaders.
      */
-    BinaryShader;
+    BinaryShader, 
+    
+    /**
+     * Supports {@link Format#RGTC} and {@link Format#RTC} texture compression.
+     */
+    TextureCompressionRGTC;
 
     /**
      * Returns true if given the renderer capabilities, the texture

+ 2 - 0
jme3-core/src/main/java/com/jme3/renderer/opengl/GLExt.java

@@ -44,6 +44,8 @@ import java.nio.IntBuffer;
 public interface GLExt {
 
         public static final int GL_ALREADY_SIGNALED = 0x911A;
+        public static final int GL_COMPRESSED_RED_RGTC1 = 0x8DBB;
+        public static final int GL_COMPRESSED_RG_RGTC2 = 0x8DBD;
 	public static final int GL_COMPRESSED_RGB8_ETC2 = 0x9274;
 	public static final int GL_COMPRESSED_RGBA_S3TC_DXT1_EXT = 0x83F1;
 	public static final int GL_COMPRESSED_RGBA_S3TC_DXT3_EXT = 0x83F2;

+ 5 - 0
jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormats.java

@@ -233,6 +233,11 @@ public final class GLImageFormats {
             formatComp(formatToGL, Format.ETC1, GLExt.GL_ETC1_RGB8_OES,        GL.GL_RGB, GL.GL_UNSIGNED_BYTE);
         }
         
+        if (caps.contains(Caps.TextureCompressionRGTC)) {
+            formatComp(formatToGL, Format.RGTC, GLExt.GL_COMPRESSED_RG_RGTC2,  GL.GL_RGB, GL.GL_UNSIGNED_BYTE);
+            formatComp(formatToGL, Format.RTC, GLExt.GL_COMPRESSED_RED_RGTC1,  GL.GL_RED, GL.GL_UNSIGNED_BYTE);
+        }
+        
         return formatToGL;
     }
 }

+ 4 - 0
jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java

@@ -351,6 +351,10 @@ public final class GLRenderer implements Renderer {
         } else if (hasExtension("GL_OES_compressed_ETC1_RGB8_texture")) {
             caps.add(Caps.TextureCompressionETC1);
         }
+        
+        if (hasExtension("GL_ARB_texture_compression_rgtc")) {
+            caps.add(Caps.TextureCompressionRGTC);
+        }
 
         // == end texture format extensions ==
 

+ 15 - 1
jme3-core/src/main/java/com/jme3/texture/Image.java

@@ -299,7 +299,21 @@ public class Image extends NativeObject implements Savable /*, Cloneable*/ {
          * 
          * Requires {@link Caps#TextureCompressionETC1}.
          */
-        ETC1(4, false, true, false);
+        ETC1(4, false, true, false),
+        
+        /**
+         * RGTC with red channel only.
+         * 
+         * Requires {@link Caps#TextureCompressionRGTC}.
+         */
+        RTC(4, false, true, false),
+        
+        /**
+         * RGTC with red and green channels.
+         * 
+         * Requires {@link Caps#TextureCompressionRGTC}.
+         */
+        RGTC(8, false, true, false);
 
         private int bpp;
         private boolean isDepth;

+ 67 - 13
jme3-core/src/plugins/java/com/jme3/texture/plugins/DDSLoader.java

@@ -85,6 +85,8 @@ public class DDSLoader implements AssetLoader {
     private static final int PF_DXT1 = 0x31545844;
     private static final int PF_DXT3 = 0x33545844;
     private static final int PF_DXT5 = 0x35545844;
+    private static final int PF_ETC1 = 0x31435445;
+    private static final int PF_ETC_ = 0x20435445; // the underscore represents a space
     private static final int PF_ATI1 = 0x31495441;
     private static final int PF_ATI2 = 0x32495441; // 0x41544932;
     private static final int PF_DX10 = 0x30315844; // a DX10 format
@@ -94,6 +96,9 @@ public class DDSLoader implements AssetLoader {
             DX10DIM_TEXTURE3D = 0x4;
     private static final int DX10MISC_GENERATE_MIPS = 0x1,
             DX10MISC_TEXTURECUBE = 0x4;
+    private static final int DXGI_FORMAT_BC4_TYPELESS = 79;
+    private static final int DXGI_FORMAT_BC4_UNORM = 80;
+    private static final int DXGI_FORMAT_BC4_SNORM = 81;
     private static final double LOG2 = Math.log(2);
     private int width;
     private int height;
@@ -105,9 +110,11 @@ public class DDSLoader implements AssetLoader {
     private int caps2;
     private boolean directx10;
     private boolean compressed;
+    private boolean dxtOrRgtc;
     private boolean texture3D;
     private boolean grayscaleOrAlpha;
     private boolean normal;
+    private ColorSpace colorSpace;
     private Format pixelFormat;
     private int bpp;
     private int[] sizes;
@@ -133,7 +140,8 @@ public class DDSLoader implements AssetLoader {
                 ((TextureKey) info.getKey()).setTextureTypeHint(Texture.Type.CubeMap);
             }
             ArrayList<ByteBuffer> data = readData(((TextureKey) info.getKey()).isFlipY());
-            return new Image(pixelFormat, width, height, depth, data, sizes, ColorSpace.sRGB);
+            
+            return new Image(pixelFormat, width, height, depth, data, sizes, colorSpace);
         } finally {
             if (stream != null){
                 stream.close();
@@ -145,18 +153,24 @@ public class DDSLoader implements AssetLoader {
         in = new LittleEndien(stream);
         loadHeader();
         ArrayList<ByteBuffer> data = readData(false);
-        return new Image(pixelFormat, width, height, depth, data, sizes, ColorSpace.sRGB);
+        return new Image(pixelFormat, width, height, depth, data, sizes, colorSpace);
     }
 
     private void loadDX10Header() throws IOException {
         int dxgiFormat = in.readInt();
+        
         if (dxgiFormat == 0) {
-                pixelFormat = Format.ETC1;
-                bpp = 4;
+            pixelFormat = Format.ETC1;
+            compressed = true;
+            bpp = 4;
         } else {
+            pixelFormat = DXGIFormat.getJmeFormat(dxgiFormat);
+            if (pixelFormat == null) {
                 throw new IOException("Unsupported DX10 format: " + dxgiFormat);
+            }
+            bpp = pixelFormat.getBitsPerPixel();
+            compressed = pixelFormat.isCompressed();
         }
-        compressed = true;
         
         int resDim = in.readInt();
         if (resDim == DX10DIM_TEXTURE3D) {
@@ -201,6 +215,7 @@ public class DDSLoader implements AssetLoader {
         caps2 = in.readInt();
         in.skipBytes(12);
         texture3D = false;
+        colorSpace = ColorSpace.sRGB;
 
         if (!directx10) {
             if (!is(caps1, DDSCAPS_TEXTURE)) {
@@ -268,10 +283,12 @@ public class DDSLoader implements AssetLoader {
                     } else {
                         pixelFormat = Image.Format.DXT1;
                     }
+                    dxtOrRgtc = true;
                     break;
                 case PF_DXT3:
                     bpp = 8;
                     pixelFormat = Image.Format.DXT3;
+                    dxtOrRgtc = true;
                     break;
                 case PF_DXT5:
                     bpp = 8;
@@ -279,17 +296,24 @@ public class DDSLoader implements AssetLoader {
                     if (swizzle == SWIZZLE_xGxR) {
                         normal = true;
                     }
+                    dxtOrRgtc = true;
                     break;
-                /*
                 case PF_ATI1:
                     bpp = 4;
-                    pixelFormat = Image.Format.LTC;
+                    pixelFormat = Image.Format.RTC;
+                    dxtOrRgtc = true;
                     break;
                 case PF_ATI2:
                     bpp = 8;
-                    pixelFormat = Image.Format.LATC;
+                    pixelFormat = Image.Format.RGTC;
+                    dxtOrRgtc = true;
+                    break;
+                case PF_ETC1:
+                case PF_ETC_:
+                    bpp = 4;
+                    pixelFormat = Image.Format.ETC1;
+                    dxtOrRgtc = false;
                     break;
-                */
                 case PF_DX10:
                     compressed = false;
                     directx10 = true;
@@ -530,6 +554,30 @@ public class DDSLoader implements AssetLoader {
         return dataBuffer;
     }
 
+    public ByteBuffer readCompressed2Dor3D(boolean flip, int totalSize) throws IOException {
+        logger.log(Level.FINEST, "Source image format: {0}", pixelFormat);
+        
+        ByteBuffer buffer = BufferUtils.createByteBuffer(totalSize * depth);
+
+        // TODO: add support for flipping ETC1
+        
+        for (int i = 0; i < depth; i++) {
+            int mipWidth = width;
+            int mipHeight = height;
+            for (int mip = 0; mip < mipMapCount; mip++) {
+                byte[] data = new byte[sizes[mip]];
+                in.readFully(data);
+                buffer.put(data);
+
+                mipWidth = Math.max(mipWidth / 2, 1);
+                mipHeight = Math.max(mipHeight / 2, 1);
+            }
+        }
+        buffer.rewind();
+        
+        return buffer;
+    }
+    
     /**
      * Reads a DXT compressed image from the InputStream
      *
@@ -738,8 +786,10 @@ public class DDSLoader implements AssetLoader {
         ArrayList<ByteBuffer> allMaps = new ArrayList<ByteBuffer>();
         if (depth > 1 && !texture3D) {
             for (int i = 0; i < depth; i++) {
-                if (compressed) {
+                if (compressed && dxtOrRgtc) {
                     allMaps.add(readDXT2D(flip, totalSize));
+                } else if (compressed) {
+                    allMaps.add(readCompressed2Dor3D(flip, totalSize));
                 } else if (grayscaleOrAlpha) {
                     allMaps.add(readGrayscale2D(flip, totalSize));
                 } else {
@@ -747,8 +797,10 @@ public class DDSLoader implements AssetLoader {
                 }
             }
         } else if (texture3D) {
-            if (compressed) {
+            if (compressed && dxtOrRgtc) {
                 allMaps.add(readDXT3D(flip, totalSize));
+            } else if (compressed) {
+                allMaps.add(readCompressed2Dor3D(flip, totalSize));
             } else if (grayscaleOrAlpha) {
                 allMaps.add(readGrayscale3D(flip, totalSize));
             } else {
@@ -756,8 +808,10 @@ public class DDSLoader implements AssetLoader {
             }
 
         } else {
-            if (compressed) {
+            if (compressed && dxtOrRgtc) {
                 allMaps.add(readDXT2D(flip, totalSize));
+            } else if (compressed) {
+                allMaps.add(readCompressed2Dor3D(flip, totalSize));
             } else if (grayscaleOrAlpha) {
                 allMaps.add(readGrayscale2D(flip, totalSize));
             } else {
@@ -822,7 +876,7 @@ public class DDSLoader implements AssetLoader {
         buf.append((char) (value & 0xFF));
         buf.append((char) ((value & 0xFF00) >> 8));
         buf.append((char) ((value & 0xFF0000) >> 16));
-        buf.append((char) ((value & 0xFF00000) >> 24));
+        buf.append((char) ((value & 0xFF000000) >> 24));
 
         return buf.toString();
     }

+ 3 - 5
jme3-core/src/plugins/java/com/jme3/texture/plugins/DXTFlipper.java

@@ -213,20 +213,18 @@ public class DXTFlipper {
             case DXT5:
                 type = 3;
                 break;
-            /*
-            case LATC:
+            case RGTC:
                 type = 4;
                 break;
-            case LTC:
+            case RTC:
                 type = 5;
                 break;
-            */
             default:
                 throw new IllegalArgumentException();
         }
 
         // DXT1 uses 8 bytes per block,
-        // DXT3, DXT5, LATC use 16 bytes per block
+        // DXT3, DXT5, RGTC use 16 bytes per block
         int bpb = type == 1 || type == 5 ? 8 : 16;
 
         ByteBuffer retImg = BufferUtils.createByteBuffer(blocksX * blocksY * bpb);

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