Răsfoiți Sursa

Merge branch 'master' into experimental

Kirill Vainer 9 ani în urmă
părinte
comite
0aaa28e66b

+ 4 - 1
.travis.yml

@@ -25,7 +25,6 @@ install:
 script:
   - ./gradlew check
   - ./gradlew createZipDistribution
-  - "[ $TRAVIS_BRANCH == 'master' ] && [ $TRAVIS_PULL_REQUEST == 'false' ] && ./gradlew uploadArchives || :"
 
 before_deploy:
   - export RELEASE_DIST=$(ls build/distributions/*.zip)
@@ -54,3 +53,7 @@ before_install:
   # wget http://dl.google.com/android/ndk/android-ndk-r10c-linux-x86_64.bin -O ndk.bin
   # 7z x ndk.bin -y > /dev/null
   # export ANDROID_NDK=`pwd`/android-ndk-r10c
+
+after_success:
+  - '[ "$TRAVIS_BRANCH" == "master" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ] && ./gradlew uploadArchives || :'
+  - '[ -n "$TRAVIS_TAG" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ] && ./gradlew uploadArchives bintrayUpload || :'

+ 29 - 0
bintray.gradle

@@ -0,0 +1,29 @@
+//
+// This file is to be applied to some subproject.
+//
+
+apply plugin: 'com.jfrog.bintray'
+
+bintray {
+    user = bintray_user
+    key = bintray_api_key
+    configurations = ['archives']
+    dryRun = false 
+    pkg {
+        repo = 'org.jmonkeyengine'
+        userOrg = 'jmonkeyengine'
+        name = project.name
+        desc = POM_DESCRIPTION
+        websiteUrl = POM_URL
+        licenses = ['BSD New']
+        vcsUrl = POM_SCM_URL
+        labels = ['jmonkeyengine']
+    }
+}
+
+bintrayUpload.dependsOn(writeFullPom)
+
+bintrayUpload.onlyIf {
+    (bintray_api_key.length() > 0) &&
+    !(version ==~ /.*SNAPSHOT/)
+}

+ 8 - 2
build.gradle

@@ -2,10 +2,11 @@ import org.gradle.api.artifacts.*
 
 buildscript {
     repositories {
-        mavenCentral()
+        jcenter()
     }
     dependencies {
         classpath 'com.android.tools.build:gradle:1.1.0'
+        classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.5'
     }
 }
 
@@ -17,6 +18,9 @@ apply from: file('upload.gradle')
 subprojects {
     if(!project.name.equals('jme3-android-examples')) {
         apply from: rootProject.file('common.gradle')
+        if (!['jme3-testdata', 'sdk'].contains(project.name)) {
+            apply from: rootProject.file('bintray.gradle')
+        }
     } else {
         apply from: rootProject.file('common-android-app.gradle')
     }
@@ -86,6 +90,8 @@ task dist(dependsOn: [':jme3-examples:dist', 'mergedJavadoc']){
 task mergedJavadoc(type: Javadoc, description: 'Creates Javadoc from all the projects.') {
     title = 'jMonkeyEngine3'
     destinationDir = mkdir("dist/javadoc")
+    
+    options.encoding = 'UTF-8'
 
     // Allows Javadoc to be generated on Java 8 despite doclint errors.
     if (JavaVersion.current().isJava8Compatible()) {
@@ -174,4 +180,4 @@ task configureAndroidNDK {
 //    tasks.withType(Test) {
 //        enableAssertions = true // true by default
 //    }
-//}
+//}

+ 44 - 19
common.gradle

@@ -5,8 +5,8 @@
 apply plugin: 'java'
 apply plugin: 'maven'
 
-group = 'com.jme3'
-version = releaseInfo.pomVersion
+group = 'org.jmonkeyengine'
+version = jmePomVersion
 
 sourceCompatibility = '1.6'
 [compileJava, compileTestJava]*.options*.encoding = 'UTF-8'
@@ -61,12 +61,53 @@ task javadocJar(type: Jar, dependsOn: javadoc, description: 'Creates a jar from
     from javadoc.destinationDir
 }
 
+def pomConfig = {
+    name POM_NAME
+    description POM_DESCRIPTION
+    url POM_URL
+    inceptionYear '2016'
+    scm {
+        url POM_SCM_URL
+        connection POM_SCM_CONNECTION
+        developerConnection POM_SCM_DEVELOPER_CONNECTION
+    }
+    licenses {
+        license {
+            name POM_LICENSE_NAME
+            url POM_LICENSE_URL
+            distribution POM_LICENSE_DISTRIBUTION
+        }
+    }
+    // from http://hub.jmonkeyengine.org/introduction/team/
+    developers {
+        developer {
+            name 'jMonkeyEngine Team'
+            id 'jMonkeyEngine'
+        }
+    }
+}
+
+// workaround to be able to use same custom pom with 'maven' and 'bintray' plugin
+task writeFullPom {
+    ext.pomFile = "$mavenPomDir/${project.name}-${project.version}.pom"
+    outputs.file pomFile
+    doLast {
+        pom {
+            project pomConfig
+        }.writeTo(pomFile)
+    }
+}
+assemble.dependsOn(writeFullPom)
+install.dependsOn(writeFullPom)
+uploadArchives.dependsOn(writeFullPom)
+
 artifacts {
     archives jar
     archives sourcesJar
     if(buildJavaDoc == "true"){
         archives javadocJar
     }
+    archives writeFullPom.outputs.files[0]
 }
 
 uploadArchives {
@@ -80,23 +121,7 @@ uploadArchives {
              authentication(userName: "www-updater", privateKey: "private/www-updater.key")
         }
         
-        pom.project {
-            name POM_NAME
-            description POM_DESCRIPTION
-            url POM_URL
-            scm {
-                url POM_SCM_URL
-                connection POM_SCM_CONNECTION
-                developerConnection POM_SCM_DEVELOPER_CONNECTION
-            }
-            licenses {
-                license {
-                    name POM_LICENSE_NAME
-                    url POM_LICENSE_URL
-                    distribution POM_LICENSE_DISTRIBUTION
-                }
-            }
-        }
+        pom.project pomConfig
     }
 }
 

+ 4 - 0
gradle.properties

@@ -35,3 +35,7 @@ POM_SCM_DEVELOPER_CONNECTION=scm:git:[email protected]:jMonkeyEngine/jmonkeyengine.
 POM_LICENSE_NAME=New BSD (3-clause) License
 POM_LICENSE_URL=http://opensource.org/licenses/BSD-3-Clause
 POM_LICENSE_DISTRIBUTION=repo
+
+# Bintray settings to override in $HOME/.gradle/gradle.properties or ENV or commandline
+bintray_user=
+bintray_api_key=

+ 1 - 1
jme3-core/src/main/java/com/jme3/asset/AssetKey.java

@@ -156,7 +156,7 @@ public class AssetKey<T> implements Savable, Cloneable {
                     list.removeLast();
                 } else {
                     list.add("..");
-                    Logger.getLogger(AssetKey.class.getName()).log(Level.SEVERE, "Asset path is outside assetmanager root");
+                    Logger.getLogger(AssetKey.class.getName()).log(Level.SEVERE, "Asset path \"{0}\" is outside assetmanager root", path);
                 }
             } else {
                 list.add(string);

+ 34 - 4
jme3-core/src/main/java/com/jme3/audio/AudioNode.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2012, 2016 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -33,6 +33,7 @@ package com.jme3.audio;
 
 import com.jme3.asset.AssetManager;
 import com.jme3.asset.AssetNotFoundException;
+import com.jme3.audio.AudioData.DataType;
 import com.jme3.export.InputCapsule;
 import com.jme3.export.JmeExporter;
 import com.jme3.export.JmeImporter;
@@ -127,6 +128,17 @@ public class AudioNode extends Node implements AudioSource {
     public AudioNode(AudioData audioData, AudioKey audioKey) {
         setAudioData(audioData, audioKey);
     }
+    
+    /**
+     * Creates a new <code>AudioNode</code> with the given audio file.
+     * @param assetManager The asset manager to use to load the audio file
+     * @param name The filename of the audio file
+     * @param type The type. If <code>{@link com.jme3.audio.AudioData.DataType}.Stream</code>, the audio will be streamed gradually from disk,
+     *             otherwise it will be buffered (<code>{@link com.jme3.audio.AudioData.DataType}.Buffer</code>)
+     */
+    public AudioNode(AssetManager assetManager, String name, DataType type) {
+        this(assetManager, name, type == DataType.Stream, true);
+    }
 
     /**
      * Creates a new <code>AudioNode</code> with the given audio file.
@@ -139,6 +151,8 @@ public class AudioNode extends Node implements AudioSource {
      * the stream cache is used. When enabled, the audio stream will
      * be read entirely but not decoded, allowing features such as 
      * seeking, looping and determining duration.
+     * 
+     * @deprecated Use {@link AudioNode#AudioNode(com.jme3.asset.AssetManager, java.lang.String, com.jme3.audio.AudioData.DataType)} instead
      */
     public AudioNode(AssetManager assetManager, String name, boolean stream, boolean streamCache) {
         this.audioKey = new AudioKey(name, stream, streamCache);
@@ -152,9 +166,11 @@ public class AudioNode extends Node implements AudioSource {
      * @param name The filename of the audio file
      * @param stream If true, the audio will be streamed gradually from disk, 
      *               otherwise, it will be buffered.
+     * 
+     * @deprecated Use {@link AudioNode#AudioNode(com.jme3.asset.AssetManager, java.lang.String, com.jme3.audio.AudioData.DataType)} instead
      */
     public AudioNode(AssetManager assetManager, String name, boolean stream) {
-        this(assetManager, name, stream, false);
+        this(assetManager, name, stream, true); // Always streamCached
     }
 
     /**
@@ -167,7 +183,7 @@ public class AudioNode extends Node implements AudioSource {
      * @deprecated AudioRenderer parameter is ignored.
      */
     public AudioNode(AudioRenderer audioRenderer, AssetManager assetManager, String name) {
-        this(assetManager, name, false);
+        this(assetManager, name, DataType.Buffer);
     }
     
     /**
@@ -175,9 +191,10 @@ public class AudioNode extends Node implements AudioSource {
      * 
      * @param assetManager The asset manager to use to load the audio file
      * @param name The filename of the audio file
+     * @deprecated Use {@link AudioNode#AudioNode(com.jme3.asset.AssetManager, java.lang.String, com.jme3.audio.AudioData.DataType) } instead
      */
     public AudioNode(AssetManager assetManager, String name) {
-        this(assetManager, name, false);
+        this(assetManager, name, DataType.Buffer);
     }
     
     protected AudioRenderer getRenderer() {
@@ -310,6 +327,19 @@ public class AudioNode extends Node implements AudioSource {
         this.status = status;
     }
 
+    /**
+     * Get the Type of the underlying AudioData to see if it's streamed or buffered.
+     * This is a shortcut to getAudioData().getType()
+     * <b>Warning</b>: Can return null!
+     * @return The {@link com.jme3.audio.AudioData.DataType} of the audio node.
+     */
+    public DataType getType() {
+        if (data == null)
+            return null;
+        else
+            return data.getDataType();
+    }
+    
     /**
      * @return True if the audio will keep looping after it is done playing,
      * otherwise, false.

+ 11 - 0
jme3-core/src/main/java/com/jme3/effect/ParticleEmitter.java

@@ -106,6 +106,7 @@ public class ParticleEmitter extends Geometry {
     private boolean worldSpace = true;
     //variable that helps with computations
     private transient Vector3f temp = new Vector3f();
+    private transient Vector3f lastPos;
 
     public static class ParticleEmitterControl implements Control {
 
@@ -1013,12 +1014,16 @@ public class ParticleEmitter extends Geometry {
         
         // Spawns particles within the tpf timeslot with proper age
         float interval = 1f / particlesPerSec;
+        float originalTpf = tpf;
         tpf += timeDifference;
         while (tpf > interval){
             tpf -= interval;
             Particle p = emitParticle(min, max);
             if (p != null){
                 p.life -= tpf;
+                if (lastPos != null && isInWorldSpace()) {
+                    p.position.interpolateLocal(lastPos, 1 - tpf / originalTpf);
+                }
                 if (p.life <= 0){
                     freeParticle(lastUsed);
                 }else{
@@ -1028,6 +1033,12 @@ public class ParticleEmitter extends Geometry {
         }
         timeDifference = tpf;
 
+        if (lastPos == null) {
+            lastPos = new Vector3f();
+        }
+
+        lastPos.set(getWorldTranslation());
+
         BoundingBox bbox = (BoundingBox) this.getMesh().getBound();
         bbox.setMinMax(min, max);
         this.setBoundRefresh();

+ 33 - 94
jme3-core/src/main/java/com/jme3/scene/BatchNode.java

@@ -31,14 +31,6 @@
  */
 package com.jme3.scene;
 
-import com.jme3.export.*;
-import com.jme3.material.Material;
-import com.jme3.math.Matrix4f;
-import com.jme3.math.Vector3f;
-import com.jme3.scene.mesh.IndexBuffer;
-import com.jme3.util.SafeArrayList;
-import com.jme3.util.TempVars;
-import java.io.IOException;
 import java.nio.Buffer;
 import java.nio.FloatBuffer;
 import java.util.ArrayList;
@@ -48,13 +40,22 @@ import java.util.Map;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
+import com.jme3.collision.Collidable;
+import com.jme3.collision.CollisionResults;
+import com.jme3.material.Material;
+import com.jme3.math.Matrix4f;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.mesh.IndexBuffer;
+import com.jme3.util.SafeArrayList;
+import com.jme3.util.TempVars;
+
 /**
  * BatchNode holds geometries that are a batched version of all the geometries that are in its sub scenegraph.
  * There is one geometry per different material in the sub tree.
  * The geometries are directly attached to the node in the scene graph.
  * Usage is like any other node except you have to call the {@link #batch()} method once all the geometries have been attached to the sub scene graph and their material set
  * (see todo more automagic for further enhancements)
- * All the geometries that have been batched are set to {@link CullHint#Always} to not render them.
+ * All the geometries that have been batched are set to not be rendered - {@link CullHint} is left intact.
  * The sub geometries can be transformed as usual, their transforms are used to update the mesh of the geometryBatch.
  * Sub geoms can be removed but it may be slower than the normal spatial removing
  * Sub geoms can be added after the batch() method has been called but won't be batched and will just be rendered as normal geometries.
@@ -72,7 +73,7 @@ public class BatchNode extends GeometryGroupNode {
      */
     protected SafeArrayList<Batch> batches = new SafeArrayList<Batch>(Batch.class);
     /**
-     * a map storing he batches by geometry to quickly acces the batch when updating
+     * a map for storing the batches by geometry to quickly access the batch when updating
      */
     protected Map<Geometry, Batch> batchesByGeom = new HashMap<Geometry, Batch>();
     /**
@@ -118,7 +119,6 @@ public class BatchNode extends GeometryGroupNode {
     public void onGeoemtryUnassociated(Geometry geom) {
         setNeedsFullRebatch(true);
     }
-    
 
     protected Matrix4f getTransformMatrix(Geometry g){
         return g.cachedWorldMat;
@@ -166,7 +166,7 @@ public class BatchNode extends GeometryGroupNode {
      */
     public void batch() {
         doBatch();
-        //we set the batch geometries to ignore transforms to avoid transforms of parent nodes to be applied twice        
+        //we set the batch geometries to ignore transforms to avoid transforms of parent nodes to be applied twice
         for (Batch batch : batches.getArray()) {
             batch.geometry.setIgnoreTransform(true);
             batch.geometry.setUserData(UserData.JME_PHYSICSIGNORE, true);
@@ -174,10 +174,10 @@ public class BatchNode extends GeometryGroupNode {
     }
 
     protected void doBatch() {
-        Map<Material, List<Geometry>> matMap = new HashMap<Material, List<Geometry>>();    
+        Map<Material, List<Geometry>> matMap = new HashMap<Material, List<Geometry>>();
         int nbGeoms = 0;
 
-        gatherGeomerties(matMap, this, needsFullRebatch);
+        gatherGeometries(matMap, this, needsFullRebatch);
         if (needsFullRebatch) {
             for (Batch batch : batches.getArray()) {
                 batch.geometry.removeFromParent();
@@ -221,7 +221,7 @@ public class BatchNode extends GeometryGroupNode {
 
             batch.geometry.setMesh(m);
             batch.geometry.getMesh().updateCounts();
-            batch.geometry.updateModelBound();            
+            batch.geometry.updateModelBound();
             batches.add(batch);
         }
         if (batches.size() > 0) {
@@ -271,7 +271,7 @@ public class BatchNode extends GeometryGroupNode {
     }
     
     
-    private void gatherGeomerties(Map<Material, List<Geometry>> map, Spatial n, boolean rebatch) {
+    private void gatherGeometries(Map<Material, List<Geometry>> map, Spatial n, boolean rebatch) {
 
         if (n instanceof Geometry) {
 
@@ -304,7 +304,7 @@ public class BatchNode extends GeometryGroupNode {
                 if (child instanceof BatchNode) {
                     continue;
                 }
-                gatherGeomerties(map, child, rebatch);
+                gatherGeometries(map, child, rebatch);
             }
         }
 
@@ -319,7 +319,7 @@ public class BatchNode extends GeometryGroupNode {
         return null;
     }
 
-    private boolean isBatch(Spatial s) {
+    public final boolean isBatch(Spatial s) {
         for (Batch batch : batches.getArray()) {
             if (batch.geometry == s) {
                 return true;
@@ -336,9 +336,6 @@ public class BatchNode extends GeometryGroupNode {
      */
     @Override
     public void setMaterial(Material material) {
-//        for (Batch batch : batches.values()) {
-//            batch.geometry.setMaterial(material);
-//        }
         throw new UnsupportedOperationException("Unsupported for now, please set the material on the geoms before batching");
     }
 
@@ -356,74 +353,7 @@ public class BatchNode extends GeometryGroupNode {
             Batch b = batches.iterator().next();
             return b.geometry.getMaterial();
         }
-        return null;//material;
-    }
-
-//    /**
-//     * Sets the material to the a specific batch of this BatchNode
-//     * 
-//     * 
-//     * @param material the material to use for this geometry
-//     */   
-//    public void setMaterial(Material material,int batchIndex) {
-//        if (!batches.isEmpty()) {
-//            
-//        }
-//        
-//    }
-//
-//    /**
-//     * Returns the material that is used for the first batch of this BatchNode
-//     * 
-//     * use getMaterial(Material material,int batchIndex) to get a material from a specific batch
-//     * 
-//     * @return the material that is used for the first batch of this BatchNode
-//     * 
-//     * @see #setMaterial(com.jme3.material.Material) 
-//     */
-//    public Material getMaterial(int batchIndex) {
-//        if (!batches.isEmpty()) {
-//            Batch b = batches.get(batches.keySet().iterator().next());
-//            return b.geometry.getMaterial();
-//        }
-//        return null;//material;
-//    }
-    @Override
-    public void write(JmeExporter ex) throws IOException {
-        super.write(ex);
-        OutputCapsule oc = ex.getCapsule(this);
-//
-//        if (material != null) {
-//            oc.write(material.getAssetName(), "materialName", null);
-//        }
-//        oc.write(material, "material", null);
-
-    }
-
-    @Override
-    public void read(JmeImporter im) throws IOException {
-        super.read(im);
-        InputCapsule ic = im.getCapsule(this);
-
-
-//        material = null;
-//        String matName = ic.readString("materialName", null);
-//        if (matName != null) {
-//            // Material name is set,
-//            // Attempt to load material via J3M
-//            try {
-//                material = im.getAssetManager().loadMaterial(matName);
-//            } catch (AssetNotFoundException ex) {
-//                // Cannot find J3M file.
-//                logger.log(Level.FINE, "Could not load J3M file {0} for Geometry.",
-//                        matName);
-//            }
-//        }
-//        // If material is NULL, try to load it from the geometry
-//        if (material == null) {
-//            material = (Material) ic.readSavable("material", null);
-//        }
-
+        return null;
     }
 
     /**
@@ -494,7 +424,7 @@ public class BatchNode extends GeometryGroupNode {
             if (mode != null && mode != listMode) {
                 throw new UnsupportedOperationException("Cannot combine different"
                         + " primitive types: " + mode + " != " + listMode);
-            }            
+            }
             mode = listMode;
             if (mode == Mesh.Mode.Lines) {
                 if (lineWidth != 1f && listLineWidth != lineWidth) {
@@ -510,8 +440,7 @@ public class BatchNode extends GeometryGroupNode {
         outMesh.setMode(mode);
         outMesh.setLineWidth(lineWidth);
         if (totalVerts >= 65536) {
-            // make sure we create an UnsignedInt buffer so
-            // we can fit all of the meshes
+            // make sure we create an UnsignedInt buffer so we can fit all of the meshes
             formatForBuf[VertexBuffer.Type.Index.ordinal()] = VertexBuffer.Format.UnsignedInt;
         } else {
             formatForBuf[VertexBuffer.Type.Index.ordinal()] = VertexBuffer.Format.UnsignedShort;
@@ -733,7 +662,6 @@ public class BatchNode extends GeometryGroupNode {
     }
 
     protected class Batch {
-
         /**
          * update the batchesByGeom map for this batch with the given List of geometries
          * @param list 
@@ -745,7 +673,7 @@ public class BatchNode extends GeometryGroupNode {
                 }
             }
         }
-        Geometry geometry;        
+        Geometry geometry;
     }
 
     protected void setNeedsFullRebatch(boolean needsFullRebatch) {
@@ -771,4 +699,15 @@ public class BatchNode extends GeometryGroupNode {
         }
         return clone;
     }
+    
+    @Override
+    public int collideWith(Collidable other, CollisionResults results) {
+        int total = 0;
+        for (Spatial child : children.getArray()){
+            if (!isBatch(child)) {
+                total += child.collideWith(other, results);
+            }
+        }
+        return total;
+    }
 }

+ 97 - 0
jme3-core/src/main/java/com/jme3/util/mikktspace/MikkTSpaceContext.java

@@ -0,0 +1,97 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package com.jme3.util.mikktspace;
+
+/**
+ *
+ * @author Nehon
+ */
+public interface MikkTSpaceContext {
+
+    /**
+     * Returns the number of faces (triangles/quads) on the mesh to be
+     * processed.
+     *
+     * @return
+     */
+    public int getNumFaces();
+
+    /**
+     * Returns the number of vertices on face number iFace iFace is a number in
+     * the range {0, 1, ..., getNumFaces()-1}
+     *
+     * @param face
+     * @return
+     */
+    public int getNumVerticesOfFace(int face);
+
+    /**
+     * returns the position/normal/texcoord of the referenced face of vertex
+     * number iVert. iVert is in the range {0,1,2} for triangles and {0,1,2,3}
+     * for quads.
+     *
+     * @param posOut
+     * @param face
+     * @param vert
+     */
+    public void getPosition(float posOut[], int face, int vert);
+
+    public void getNormal(float normOut[], int face, int vert);
+
+    public void getTexCoord(float texOut[], int face, int vert);
+
+    /**
+     * The call-backsetTSpaceBasic() is sufficient for basic normal mapping.
+     * This function is used to return the tangent and sign to the application.
+     * tangent is a unit length vector. For normal maps it is sufficient to use
+     * the following simplified version of the bitangent which is generated at
+     * pixel/vertex level.
+     *
+     * bitangent = fSign * cross(vN, tangent);
+     *
+     * Note that the results are returned unindexed. It is possible to generate
+     * a new index list But averaging/overwriting tangent spaces by using an
+     * already existing index list WILL produce INCRORRECT results. DO NOT! use
+     * an already existing index list.
+     *
+     * @param tangent
+     * @param sign
+     * @param face
+     * @param vert
+     */
+    public void setTSpaceBasic(float tangent[], float sign, int face, int vert);
+
+    /**
+     * This function is used to return tangent space results to the application.
+     * tangent and biTangent are unit length vectors and fMagS and fMagT are
+     * their true magnitudes which can be used for relief mapping effects.
+     *
+     * biTangent is the "real" bitangent and thus may not be perpendicular to
+     * tangent. However, both are perpendicular to the vertex normal. For normal
+     * maps it is sufficient to use the following simplified version of the
+     * bitangent which is generated at pixel/vertex level.
+     *
+     * <pre>
+     * fSign = bIsOrientationPreserving ? 1.0f : (-1.0f);
+     * bitangent = fSign * cross(vN, tangent);
+     * </pre>
+     *
+     * Note that the results are returned unindexed. It is possible to generate
+     * a new index list. But averaging/overwriting tangent spaces by using an
+     * already existing index list WILL produce INCRORRECT results. DO NOT! use
+     * an already existing index list.
+     *
+     * @param tangent
+     * @param biTangent
+     * @param magS
+     * @param magT
+     * @param isOrientationPreserving
+     * @param face
+     * @param vert
+     */
+    void setTSpace(float tangent[], float biTangent[], float magS, float magT,
+            boolean isOrientationPreserving, int face, int vert);
+}

+ 100 - 0
jme3-core/src/main/java/com/jme3/util/mikktspace/MikkTSpaceImpl.java

@@ -0,0 +1,100 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package com.jme3.util.mikktspace;
+
+import com.jme3.scene.Mesh;
+import com.jme3.scene.VertexBuffer;
+import com.jme3.scene.mesh.IndexBuffer;
+import com.jme3.util.BufferUtils;
+import java.nio.FloatBuffer;
+
+/**
+ *
+ * @author Nehon
+ */
+public class MikkTSpaceImpl implements MikkTSpaceContext {
+
+    Mesh mesh;
+
+    public MikkTSpaceImpl(Mesh mesh) {
+        this.mesh = mesh;
+        VertexBuffer tangentBuffer = mesh.getBuffer(VertexBuffer.Type.Tangent);
+        if(tangentBuffer == null){
+            FloatBuffer fb = BufferUtils.createFloatBuffer(mesh.getVertexCount() * 4);
+            mesh.setBuffer(VertexBuffer.Type.Tangent, 4, fb);            
+        }
+        
+        //TODO ensure the Tangent buffer exists, else create one.
+    }
+
+    @Override
+    public int getNumFaces() {
+        return mesh.getTriangleCount();        
+    }
+
+    @Override
+    public int getNumVerticesOfFace(int face) {
+        return 3;
+    }
+
+    @Override
+    public void getPosition(float[] posOut, int face, int vert) {
+        int vertIndex = getIndex(face, vert);
+        VertexBuffer position = mesh.getBuffer(VertexBuffer.Type.Position);
+        FloatBuffer pos = (FloatBuffer) position.getData();
+        pos.position(vertIndex * 3);
+        posOut[0] = pos.get();
+        posOut[1] = pos.get();
+        posOut[2] = pos.get();
+    }
+
+    @Override
+    public void getNormal(float[] normOut, int face, int vert) {
+        int vertIndex = getIndex(face, vert);
+        VertexBuffer normal = mesh.getBuffer(VertexBuffer.Type.Normal);
+        FloatBuffer norm = (FloatBuffer) normal.getData();
+        norm.position(vertIndex * 3);
+        normOut[0] = norm.get();
+        normOut[1] = norm.get();
+        normOut[2] = norm.get();
+    }
+
+    @Override
+    public void getTexCoord(float[] texOut, int face, int vert) {
+        int vertIndex = getIndex(face, vert);
+        VertexBuffer texCoord = mesh.getBuffer(VertexBuffer.Type.TexCoord);
+        FloatBuffer tex = (FloatBuffer) texCoord.getData();
+        tex.position(vertIndex * 2);
+        texOut[0] = tex.get();
+        texOut[1] = tex.get();        
+    }
+
+    @Override
+    public void setTSpaceBasic(float[] tangent, float sign, int face, int vert) {
+        int vertIndex = getIndex(face, vert);
+        VertexBuffer tangentBuffer = mesh.getBuffer(VertexBuffer.Type.Tangent);
+        FloatBuffer tan = (FloatBuffer) tangentBuffer.getData();
+        
+        tan.position(vertIndex * 4);
+        tan.put(tangent);
+        tan.put(sign);
+        
+        tan.rewind();
+        tangentBuffer.setUpdateNeeded();
+    }
+
+    @Override
+    public void setTSpace(float[] tangent, float[] biTangent, float magS, float magT, boolean isOrientationPreserving, int face, int vert) {
+        //Do nothing
+    }
+
+    private int getIndex(int face, int vert) {
+        IndexBuffer index = mesh.getIndexBuffer();
+        int vertIndex = index.get(face * 3 + vert);
+        return vertIndex;
+    }
+
+}

+ 1717 - 0
jme3-core/src/main/java/com/jme3/util/mikktspace/MikktspaceTangentGenerator.java

@@ -0,0 +1,1717 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package com.jme3.util.mikktspace;
+
+import com.jme3.math.FastMath;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * This tangent generator is Highly experimental.
+ * This is the Java translation of The mikktspace generator made by Morten S. Mikkelsen
+ * C Source code can be found here
+ * https://developer.blender.org/diffusion/B/browse/master/intern/mikktspace/mikktspace.c
+ * https://developer.blender.org/diffusion/B/browse/master/intern/mikktspace/mikktspace.h
+ * 
+ * MikkTspace looks like the new standard of tengent generation in 3D softwares.
+ * Xnormal, Blender, Substance painter, and many more use it.
+ * 
+ * Usage is :
+ * MikkTSpaceTangentGenerator.generate(spatial);
+ * 
+ * 
+ * 
+ * @author Nehon
+ */
+public class MikktspaceTangentGenerator {
+
+    private final static int MARK_DEGENERATE = 1;
+    private final static int QUAD_ONE_DEGEN_TRI = 2;
+    private final static int GROUP_WITH_ANY = 4;
+    private final static int ORIENT_PRESERVING = 8;
+    private final static long INTERNAL_RND_SORT_SEED = 39871946 & 0xffffffffL;
+    static final int CELLS = 2048;
+
+    static int makeIndex(final int face, final int vert) {
+        assert (vert >= 0 && vert < 4 && face >= 0);
+        return (face << 2) | (vert & 0x3);
+    }
+
+    private static void indexToData(int[] face, int[] vert, final int indexIn) {
+        vert[0] = indexIn & 0x3;
+        face[0] = indexIn >> 2;
+    }
+
+    static TSpace avgTSpace(final TSpace tS0, final TSpace tS1) {
+        TSpace tsRes = new TSpace();
+
+        // this if is important. Due to floating point precision
+        // averaging when s0 == s1 will cause a slight difference
+        // which results in tangent space splits later on
+        if (tS0.magS == tS1.magS && tS0.magT == tS1.magT && tS0.os.equals(tS1.os) && tS0.ot.equals(tS1.ot)) {
+            tsRes.magS = tS0.magS;
+            tsRes.magT = tS0.magT;
+            tsRes.os.set(tS0.os);
+            tsRes.ot.set(tS0.ot);
+        } else {
+            tsRes.magS = 0.5f * (tS0.magS + tS1.magS);
+            tsRes.magT = 0.5f * (tS0.magT + tS1.magT);
+            tsRes.os.set(tS0.os).addLocal(tS1.os).normalizeLocal();
+            tsRes.ot.set(tS0.ot).addLocal(tS1.ot).normalizeLocal();
+        }
+        return tsRes;
+    }
+
+    public static void generate(Spatial s){
+        if(s instanceof Node){
+            Node n = (Node)s;
+            for (Spatial child : n.getChildren()) {
+                generate(child);
+            }
+        } else if (s instanceof Geometry){
+            Geometry g = (Geometry)s;
+            MikkTSpaceImpl context = new MikkTSpaceImpl(g.getMesh());
+            if(!genTangSpaceDefault(context)){
+                Logger.getLogger(MikktspaceTangentGenerator.class.getName()).log(Level.SEVERE, "Failed to generate tangents for geometry " + g.getName());
+            }
+        }
+    }
+    
+    public static boolean genTangSpaceDefault(MikkTSpaceContext mikkTSpace) {
+        return genTangSpace(mikkTSpace, 180.0f);
+    }
+
+    public static boolean genTangSpace(MikkTSpaceContext mikkTSpace, final float angularThreshold) {
+
+        // count nr_triangles
+        int[] piTriListIn;
+        int[] piGroupTrianglesBuffer;
+        TriInfo[] pTriInfos;
+        Group[] pGroups;
+        TSpace[] psTspace;
+        int iNrTrianglesIn = 0;
+        int iNrTSPaces, iTotTris, iDegenTriangles, iNrMaxGroups;
+        int iNrActiveGroups, index;
+        final int iNrFaces = mikkTSpace.getNumFaces();
+        //boolean bRes = false;
+        final float fThresCos = (float) FastMath.cos((angularThreshold * (float) FastMath.PI) / 180.0f);
+
+        // count triangles on supported faces
+        for (int f = 0; f < iNrFaces; f++) {
+            final int verts = mikkTSpace.getNumVerticesOfFace(f);
+            if (verts == 3) {
+                ++iNrTrianglesIn;
+            } else if (verts == 4) {
+                iNrTrianglesIn += 2;
+            }
+        }
+        if (iNrTrianglesIn <= 0) {
+            return false;
+        }
+
+        piTriListIn = new int[3 * iNrTrianglesIn];
+        pTriInfos = new TriInfo[iNrTrianglesIn];
+
+        // make an initial triangle -. face index list
+        iNrTSPaces = generateInitialVerticesIndexList(pTriInfos, piTriListIn, mikkTSpace, iNrTrianglesIn);
+
+        // make a welded index list of identical positions and attributes (pos, norm, texc)        
+        generateSharedVerticesIndexList(piTriListIn, mikkTSpace, iNrTrianglesIn);
+
+        // Mark all degenerate triangles
+        iTotTris = iNrTrianglesIn;
+        iDegenTriangles = 0;
+        for (int t = 0; t < iTotTris; t++) {
+            final int i0 = piTriListIn[t * 3 + 0];
+            final int i1 = piTriListIn[t * 3 + 1];
+            final int i2 = piTriListIn[t * 3 + 2];
+            final Vector3f p0 = getPosition(mikkTSpace, i0);
+            final Vector3f p1 = getPosition(mikkTSpace, i1);
+            final Vector3f p2 = getPosition(mikkTSpace, i2);
+            if (p0.equals(p1) || p0.equals(p2) || p1.equals(p2)) {// degenerate
+                pTriInfos[t].flag |= MARK_DEGENERATE;
+                ++iDegenTriangles;
+            }
+        }
+        iNrTrianglesIn = iTotTris - iDegenTriangles;
+
+        // mark all triangle pairs that belong to a quad with only one
+        // good triangle. These need special treatment in DegenEpilogue().
+        // Additionally, move all good triangles to the start of
+        // pTriInfos[] and piTriListIn[] without changing order and
+        // put the degenerate triangles last.
+        degenPrologue(pTriInfos, piTriListIn, iNrTrianglesIn, iTotTris);
+
+        // evaluate triangle level attributes and neighbor list        
+        initTriInfo(pTriInfos, piTriListIn, mikkTSpace, iNrTrianglesIn);
+
+        // based on the 4 rules, identify groups based on connectivity
+        iNrMaxGroups = iNrTrianglesIn * 3;
+        pGroups = new Group[iNrMaxGroups];
+        piGroupTrianglesBuffer = new int[iNrTrianglesIn * 3];
+
+        iNrActiveGroups
+                = build4RuleGroups(pTriInfos, pGroups, piGroupTrianglesBuffer, piTriListIn, iNrTrianglesIn);
+
+        psTspace = new TSpace[iNrTSPaces];
+
+        for (int t = 0; t < iNrTSPaces; t++) {
+            TSpace tSpace = new TSpace();
+            tSpace.os.set(1.0f, 0.0f, 0.0f);
+            tSpace.magS = 1.0f;
+            tSpace.ot.set(0.0f, 1.0f, 0.0f);
+            tSpace.magT = 1.0f;
+            psTspace[t] = tSpace;
+        }
+
+        // make tspaces, each group is split up into subgroups if necessary
+        // based on fAngularThreshold. Finally a tangent space is made for
+        // every resulting subgroup
+        generateTSpaces(psTspace, pTriInfos, pGroups, iNrActiveGroups, piTriListIn, fThresCos, mikkTSpace);
+
+        // degenerate quads with one good triangle will be fixed by copying a space from
+        // the good triangle to the coinciding vertex.
+        // all other degenerate triangles will just copy a space from any good triangle
+        // with the same welded index in piTriListIn[].
+        DegenEpilogue(psTspace, pTriInfos, piTriListIn, mikkTSpace, iNrTrianglesIn, iTotTris);
+
+        index = 0;
+        for (int f = 0; f < iNrFaces; f++) {
+            final int verts = mikkTSpace.getNumVerticesOfFace(f);
+            if (verts != 3 && verts != 4) {
+                continue;
+            }
+
+            // I've decided to let degenerate triangles and group-with-anythings
+            // vary between left/right hand coordinate systems at the vertices.
+            // All healthy triangles on the other hand are built to always be either or.
+
+            /*// force the coordinate system orientation to be uniform for every face.
+             // (this is already the case for good triangles but not for
+             // degenerate ones and those with bGroupWithAnything==true)
+             bool bOrient = psTspace[index].bOrient;
+             if (psTspace[index].iCounter == 0)  // tspace was not derived from a group
+             {
+             // look for a space created in GenerateTSpaces() by iCounter>0
+             bool bNotFound = true;
+             int i=1;
+             while (i<verts && bNotFound)
+             {
+             if (psTspace[index+i].iCounter > 0) bNotFound=false;
+             else ++i;
+             }
+             if (!bNotFound) bOrient = psTspace[index+i].bOrient;
+             }*/
+            // set data
+            for (int i = 0; i < verts; i++) {
+                final TSpace pTSpace = psTspace[index];
+                float tang[] = {pTSpace.os.x, pTSpace.os.y, pTSpace.os.z};
+                float bitang[] = {pTSpace.ot.x, pTSpace.ot.y, pTSpace.ot.z};
+                mikkTSpace.setTSpace(tang, bitang, pTSpace.magS, pTSpace.magT, pTSpace.orient, f, i);
+                mikkTSpace.setTSpaceBasic(tang, pTSpace.orient == true ? 1.0f : (-1.0f), f, i);
+                ++index;
+            }
+        }
+
+        return true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // it is IMPORTANT that this function is called to evaluate the hash since
+    // inlining could potentially reorder instructions and generate different
+    // results for the same effective input value fVal.
+    //TODO nehon: Wuuttt? something is fishy about this. How the fuck inlining can reorder instructions? Is that a C thing?
+    static int findGridCell(final float min, final float max, final float val) {
+        final float fIndex = CELLS * ((val - min) / (max - min));
+        final int iIndex = (int) fIndex;
+        return iIndex < CELLS ? (iIndex >= 0 ? iIndex : 0) : (CELLS - 1);
+    }
+
+    static void generateSharedVerticesIndexList(int piTriList_in_and_out[], final MikkTSpaceContext mikkTSpace, final int iNrTrianglesIn) {
+
+        // Generate bounding box
+        TmpVert[] pTmpVert;
+        Vector3f vMin = getPosition(mikkTSpace, 0);
+        Vector3f vMax = vMin.clone();
+        Vector3f vDim;
+        float fMin, fMax;
+        for (int i = 1; i < (iNrTrianglesIn * 3); i++) {
+            final int index = piTriList_in_and_out[i];
+
+            final Vector3f vP = getPosition(mikkTSpace, index);
+            if (vMin.x > vP.x) {
+                vMin.x = vP.x;
+            } else if (vMax.x < vP.x) {
+                vMax.x = vP.x;
+            }
+            if (vMin.y > vP.y) {
+                vMin.y = vP.y;
+            } else if (vMax.y < vP.y) {
+                vMax.y = vP.y;
+            }
+            if (vMin.z > vP.z) {
+                vMin.z = vP.z;
+            } else if (vMax.z < vP.z) {
+                vMax.z = vP.z;
+            }
+        }
+
+        vDim = vMax.subtract(vMin);
+        int iChannel = 0;
+        fMin = vMin.x;
+        fMax = vMax.x;
+        if (vDim.y > vDim.x && vDim.y > vDim.z) {
+            iChannel = 1;
+            fMin = vMin.y;
+            fMax = vMax.y;
+        } else if (vDim.z > vDim.x) {
+            iChannel = 2;
+            fMin = vMin.z;
+            fMax = vMax.z;
+        }
+
+        //TODO Nehon: this is really fishy... seems like a hashtable implementation with nasty array manipulation...
+        int[] piHashTable = new int[iNrTrianglesIn * 3];
+        int[] piHashCount = new int[CELLS];
+        int[] piHashOffsets = new int[CELLS];
+        int[] piHashCount2 = new int[CELLS];
+
+        // count amount of elements in each cell unit
+        for (int i = 0; i < (iNrTrianglesIn * 3); i++) {
+            final int index = piTriList_in_and_out[i];
+            final Vector3f vP = getPosition(mikkTSpace, index);
+            final float fVal = iChannel == 0 ? vP.x : (iChannel == 1 ? vP.y : vP.z);
+            final int iCell = findGridCell(fMin, fMax, fVal);
+            ++piHashCount[iCell];
+        }
+
+        // evaluate start index of each cell.
+        piHashOffsets[0] = 0;
+        for (int k = 1; k < CELLS; k++) {
+            piHashOffsets[k] = piHashOffsets[k - 1] + piHashCount[k - 1];
+        }
+
+        // insert vertices
+        for (int i = 0; i < (iNrTrianglesIn * 3); i++) {
+            final int index = piTriList_in_and_out[i];
+            final Vector3f vP = getPosition(mikkTSpace, index);
+            final float fVal = iChannel == 0 ? vP.x : (iChannel == 1 ? vP.y : vP.z);
+            final int iCell = findGridCell(fMin, fMax, fVal);
+
+            assert (piHashCount2[iCell] < piHashCount[iCell]);
+
+            //    int * pTable = &piHashTable[piHashOffsets[iCell]];
+            //    pTable[piHashCount2[iCell]] = i;  // vertex i has been inserted.
+            piHashTable[piHashOffsets[iCell] + piHashCount2[iCell]] = i;// vertex i has been inserted.     
+            ++piHashCount2[iCell];
+        }
+        for (int k = 0; k < CELLS; k++) {
+            assert (piHashCount2[k] == piHashCount[k]);  // verify the count
+        }
+
+        // find maximum amount of entries in any hash entry
+        int iMaxCount = piHashCount[0];
+        for (int k = 1; k < CELLS; k++) {
+            if (iMaxCount < piHashCount[k]) {
+                iMaxCount = piHashCount[k];
+            }
+        }
+
+        pTmpVert = new TmpVert[iMaxCount];
+
+        // complete the merge
+        for (int k = 0; k < CELLS; k++) {
+            // extract table of cell k and amount of entries in it
+            // int * pTable = &piHashTable[piHashOffsets[k]];
+            final int iEntries = piHashCount[k];
+            if (iEntries < 2) {
+                continue;
+            }
+
+            if (pTmpVert != null) {
+                for (int e = 0; e < iEntries; e++) {
+                    int j = piHashTable[piHashOffsets[k] + e];
+                    final Vector3f vP = getPosition(mikkTSpace, piTriList_in_and_out[j]);
+                    pTmpVert[e] = new TmpVert();
+                    pTmpVert[e].vert[0] = vP.x;
+                    pTmpVert[e].vert[1] = vP.y;
+                    pTmpVert[e].vert[2] = vP.z;
+                    pTmpVert[e].index = j;
+                }
+                MergeVertsFast(piTriList_in_and_out, pTmpVert, mikkTSpace, 0, iEntries - 1);
+            } else {
+                //TODO Nehon: pTempVert is very unlikely to be null...maybe remove this...
+                int[] pTable = Arrays.copyOfRange(piHashTable, piHashOffsets[k], piHashOffsets[k] + iEntries);
+                MergeVertsSlow(piTriList_in_and_out, mikkTSpace, pTable, iEntries);
+            }
+        }
+    }
+
+    static void MergeVertsFast(int piTriList_in_and_out[], TmpVert pTmpVert[], final MikkTSpaceContext mikkTSpace, final int iL_in, final int iR_in) {
+        // make bbox        
+        float[] fvMin = new float[3];
+        float[] fvMax = new float[3];
+        for (int c = 0; c < 3; c++) {
+            fvMin[c] = pTmpVert[iL_in].vert[c];
+            fvMax[c] = fvMin[c];
+        }
+        for (int l = (iL_in + 1); l <= iR_in; l++) {
+            for (int c = 0; c < 3; c++) {
+                if (fvMin[c] > pTmpVert[l].vert[c]) {
+                    fvMin[c] = pTmpVert[l].vert[c];
+                } else if (fvMax[c] < pTmpVert[l].vert[c]) {
+                    fvMax[c] = pTmpVert[l].vert[c];
+                }
+            }
+        }
+
+        float dx = fvMax[0] - fvMin[0];
+        float dy = fvMax[1] - fvMin[1];
+        float dz = fvMax[2] - fvMin[2];
+
+        int channel = 0;
+        if (dy > dx && dy > dz) {
+            channel = 1;
+        } else if (dz > dx) {
+            channel = 2;
+        }
+
+        float fSep = 0.5f * (fvMax[channel] + fvMin[channel]);
+
+        // terminate recursion when the separation/average value
+        // is no longer strictly between fMin and fMax values.
+        if (fSep >= fvMax[channel] || fSep <= fvMin[channel]) {
+            // complete the weld
+            for (int l = iL_in; l <= iR_in; l++) {
+                int i = pTmpVert[l].index;
+                final int index = piTriList_in_and_out[i];
+                final Vector3f vP = getPosition(mikkTSpace, index);
+                final Vector3f vN = getNormal(mikkTSpace, index);
+                final Vector3f vT = getTexCoord(mikkTSpace, index);
+
+                boolean bNotFound = true;
+                int l2 = iL_in, i2rec = -1;
+                while (l2 < l && bNotFound) {
+                    final int i2 = pTmpVert[l2].index;
+                    final int index2 = piTriList_in_and_out[i2];
+                    final Vector3f vP2 = getPosition(mikkTSpace, index2);
+                    final Vector3f vN2 = getNormal(mikkTSpace, index2);
+                    final Vector3f vT2 = getTexCoord(mikkTSpace, index2);
+                    i2rec = i2;
+
+                    //if (vP==vP2 && vN==vN2 && vT==vT2)
+                    if (vP.x == vP2.x && vP.y == vP2.y && vP.z == vP2.z
+                            && vN.x == vN2.x && vN.y == vN2.y && vN.z == vN2.z
+                            && vT.x == vT2.x && vT.y == vT2.y && vT.z == vT2.z) {
+                        bNotFound = false;
+                    } else {
+                        ++l2;
+                    }
+                }
+
+                // merge if previously found
+                if (!bNotFound) {
+                    piTriList_in_and_out[i] = piTriList_in_and_out[i2rec];
+                }
+            }
+        } else {
+            int iL = iL_in, iR = iR_in;
+            assert ((iR_in - iL_in) > 0);  // at least 2 entries
+
+            // separate (by fSep) all points between iL_in and iR_in in pTmpVert[]
+            while (iL < iR) {
+                boolean bReadyLeftSwap = false, bReadyRightSwap = false;
+                while ((!bReadyLeftSwap) && iL < iR) {
+                    assert (iL >= iL_in && iL <= iR_in);
+                    bReadyLeftSwap = !(pTmpVert[iL].vert[channel] < fSep);
+                    if (!bReadyLeftSwap) {
+                        ++iL;
+                    }
+                }
+                while ((!bReadyRightSwap) && iL < iR) {
+                    assert (iR >= iL_in && iR <= iR_in);
+                    bReadyRightSwap = pTmpVert[iR].vert[channel] < fSep;
+                    if (!bReadyRightSwap) {
+                        --iR;
+                    }
+                }
+                assert ((iL < iR) || !(bReadyLeftSwap && bReadyRightSwap));
+
+                if (bReadyLeftSwap && bReadyRightSwap) {
+                    final TmpVert sTmp = pTmpVert[iL];
+                    assert (iL < iR);
+                    pTmpVert[iL] = pTmpVert[iR];
+                    pTmpVert[iR] = sTmp;
+                    ++iL;
+                    --iR;
+                }
+            }
+
+            assert (iL == (iR + 1) || (iL == iR));
+            if (iL == iR) {
+                final boolean bReadyRightSwap = pTmpVert[iR].vert[channel] < fSep;
+                if (bReadyRightSwap) {
+                    ++iL;
+                } else {
+                    --iR;
+                }
+            }
+
+            // only need to weld when there is more than 1 instance of the (x,y,z)
+            if (iL_in < iR) {
+                MergeVertsFast(piTriList_in_and_out, pTmpVert, mikkTSpace, iL_in, iR);  // weld all left of fSep
+            }
+            if (iL < iR_in) {
+                MergeVertsFast(piTriList_in_and_out, pTmpVert, mikkTSpace, iL, iR_in);  // weld all right of (or equal to) fSep
+            }
+        }
+    }
+
+    //TODO Nehon: Used only if an array failed to be allocated... Can't happen in Java...
+    static void MergeVertsSlow(int piTriList_in_and_out[], final MikkTSpaceContext mikkTSpace, final int pTable[], final int iEntries) {
+        // this can be optimized further using a tree structure or more hashing.
+        for (int e = 0; e < iEntries; e++) {
+            int i = pTable[e];
+            final int index = piTriList_in_and_out[i];
+            final Vector3f vP = getPosition(mikkTSpace, index);
+            final Vector3f vN = getNormal(mikkTSpace, index);
+            final Vector3f vT = getTexCoord(mikkTSpace, index);
+
+            boolean bNotFound = true;
+            int e2 = 0, i2rec = -1;
+            while (e2 < e && bNotFound) {
+                final int i2 = pTable[e2];
+                final int index2 = piTriList_in_and_out[i2];
+                final Vector3f vP2 = getPosition(mikkTSpace, index2);
+                final Vector3f vN2 = getNormal(mikkTSpace, index2);
+                final Vector3f vT2 = getTexCoord(mikkTSpace, index2);
+                i2rec = i2;
+
+                if (vP.equals(vP2) && vN.equals(vN2) && vT.equals(vT2)) {
+                    bNotFound = false;
+                } else {
+                    ++e2;
+                }
+            }
+
+            // merge if previously found
+            if (!bNotFound) {
+                piTriList_in_and_out[i] = piTriList_in_and_out[i2rec];
+            }
+        }
+    }
+
+    //TODO Nehon : Not used...seemsit's used in the original version if the structure to store the data in the regular method failed...
+    static void generateSharedVerticesIndexListSlow(int piTriList_in_and_out[], final MikkTSpaceContext mikkTSpace, final int iNrTrianglesIn) {
+        int iNumUniqueVerts = 0;
+        for (int t = 0; t < iNrTrianglesIn; t++) {
+            for (int i = 0; i < 3; i++) {
+                final int offs = t * 3 + i;
+                final int index = piTriList_in_and_out[offs];
+
+                final Vector3f vP = getPosition(mikkTSpace, index);
+                final Vector3f vN = getNormal(mikkTSpace, index);
+                final Vector3f vT = getTexCoord(mikkTSpace, index);
+
+                boolean bFound = false;
+                int t2 = 0, index2rec = -1;
+                while (!bFound && t2 <= t) {
+                    int j = 0;
+                    while (!bFound && j < 3) {
+                        final int index2 = piTriList_in_and_out[t2 * 3 + j];
+                        final Vector3f vP2 = getPosition(mikkTSpace, index2);
+                        final Vector3f vN2 = getNormal(mikkTSpace, index2);
+                        final Vector3f vT2 = getTexCoord(mikkTSpace, index2);
+
+                        if (vP.equals(vP2) && vN.equals(vN2) && vT.equals(vT2)) {
+                            bFound = true;
+                        } else {
+                            ++j;
+                        }
+                    }
+                    if (!bFound) {
+                        ++t2;
+                    }
+                }
+
+                assert (bFound);
+                // if we found our own
+                if (index2rec == index) {
+                    ++iNumUniqueVerts;
+                }
+
+                piTriList_in_and_out[offs] = index2rec;
+            }
+        }
+    }
+
+    static int generateInitialVerticesIndexList(TriInfo pTriInfos[], int piTriList_out[], final MikkTSpaceContext mikkTSpace, final int iNrTrianglesIn) {
+        int iTSpacesOffs = 0;
+        int iDstTriIndex = 0;
+        for (int f = 0; f < mikkTSpace.getNumFaces(); f++) {
+            final int verts = mikkTSpace.getNumVerticesOfFace(f);
+            if (verts != 3 && verts != 4) {
+                continue;
+            }
+
+            //TODO nehon : clean this, have a local TrinInfo and assign it to pTriInfo[iDstTriIndex] at the end... and change those variables names...
+            pTriInfos[iDstTriIndex] = new TriInfo();
+            pTriInfos[iDstTriIndex].orgFaceNumber = f;
+            pTriInfos[iDstTriIndex].tSpacesOffs = iTSpacesOffs;
+
+            if (verts == 3) {
+                //TODO same here it should be easy once the local TriInfo is created.
+                byte[] pVerts = pTriInfos[iDstTriIndex].vertNum;
+                pVerts[0] = 0;
+                pVerts[1] = 1;
+                pVerts[2] = 2;
+                piTriList_out[iDstTriIndex * 3 + 0] = makeIndex(f, 0);
+                piTriList_out[iDstTriIndex * 3 + 1] = makeIndex(f, 1);
+                piTriList_out[iDstTriIndex * 3 + 2] = makeIndex(f, 2);
+                ++iDstTriIndex;  // next
+            } else {
+                //Note, Nehon: we should never get there with JME, because we don't support quads... 
+                //but I'm going to let it there incase someone needs it... Just know this code is not tested.
+                {//TODO remove those useless brackets...
+                    pTriInfos[iDstTriIndex + 1].orgFaceNumber = f;
+                    pTriInfos[iDstTriIndex + 1].tSpacesOffs = iTSpacesOffs;
+                }
+
+                {
+                    // need an order independent way to evaluate
+                    // tspace on quads. This is done by splitting
+                    // along the shortest diagonal.
+                    final int i0 = makeIndex(f, 0);
+                    final int i1 = makeIndex(f, 1);
+                    final int i2 = makeIndex(f, 2);
+                    final int i3 = makeIndex(f, 3);
+                    final Vector3f T0 = getTexCoord(mikkTSpace, i0);
+                    final Vector3f T1 = getTexCoord(mikkTSpace, i1);
+                    final Vector3f T2 = getTexCoord(mikkTSpace, i2);
+                    final Vector3f T3 = getTexCoord(mikkTSpace, i3);
+                    final float distSQ_02 = T2.subtract(T0).lengthSquared();
+                    final float distSQ_13 = T3.subtract(T1).lengthSquared();
+                    boolean bQuadDiagIs_02;
+                    if (distSQ_02 < distSQ_13) {
+                        bQuadDiagIs_02 = true;
+                    } else if (distSQ_13 < distSQ_02) {
+                        bQuadDiagIs_02 = false;
+                    } else {
+                        final Vector3f P0 = getPosition(mikkTSpace, i0);
+                        final Vector3f P1 = getPosition(mikkTSpace, i1);
+                        final Vector3f P2 = getPosition(mikkTSpace, i2);
+                        final Vector3f P3 = getPosition(mikkTSpace, i3);
+                        final float distSQ_022 = P2.subtract(P0).lengthSquared();
+                        final float distSQ_132 = P3.subtract(P1).lengthSquared();
+
+                        bQuadDiagIs_02 = distSQ_132 >= distSQ_022;
+                    }
+
+                    if (bQuadDiagIs_02) {
+                        {
+                            byte[] pVerts_A = pTriInfos[iDstTriIndex].vertNum;
+                            pVerts_A[0] = 0;
+                            pVerts_A[1] = 1;
+                            pVerts_A[2] = 2;
+                        }
+                        piTriList_out[iDstTriIndex * 3 + 0] = i0;
+                        piTriList_out[iDstTriIndex * 3 + 1] = i1;
+                        piTriList_out[iDstTriIndex * 3 + 2] = i2;
+                        ++iDstTriIndex;  // next
+                        {
+                            byte[] pVerts_B = pTriInfos[iDstTriIndex].vertNum;
+                            pVerts_B[0] = 0;
+                            pVerts_B[1] = 2;
+                            pVerts_B[2] = 3;
+                        }
+                        piTriList_out[iDstTriIndex * 3 + 0] = i0;
+                        piTriList_out[iDstTriIndex * 3 + 1] = i2;
+                        piTriList_out[iDstTriIndex * 3 + 2] = i3;
+                        ++iDstTriIndex;  // next
+                    } else {
+                        {
+                            byte[] pVerts_A = pTriInfos[iDstTriIndex].vertNum;
+                            pVerts_A[0] = 0;
+                            pVerts_A[1] = 1;
+                            pVerts_A[2] = 3;
+                        }
+                        piTriList_out[iDstTriIndex * 3 + 0] = i0;
+                        piTriList_out[iDstTriIndex * 3 + 1] = i1;
+                        piTriList_out[iDstTriIndex * 3 + 2] = i3;
+                        ++iDstTriIndex;  // next
+                        {
+                            byte[] pVerts_B = pTriInfos[iDstTriIndex].vertNum;
+                            pVerts_B[0] = 1;
+                            pVerts_B[1] = 2;
+                            pVerts_B[2] = 3;
+                        }
+                        piTriList_out[iDstTriIndex * 3 + 0] = i1;
+                        piTriList_out[iDstTriIndex * 3 + 1] = i2;
+                        piTriList_out[iDstTriIndex * 3 + 2] = i3;
+                        ++iDstTriIndex;  // next
+                    }
+                }
+            }
+
+            iTSpacesOffs += verts;
+            assert (iDstTriIndex <= iNrTrianglesIn);
+        }
+
+        for (int t = 0; t < iNrTrianglesIn; t++) {
+            pTriInfos[t].flag = 0;
+        }
+
+        // return total amount of tspaces
+        return iTSpacesOffs;
+    }
+
+    static Vector3f getPosition(final MikkTSpaceContext mikkTSpace, final int index) {
+        //TODO nehon: very ugly but works... using arrays to pass integers as references in the indexToData
+        int[] iF = new int[1];
+        int[] iI = new int[1];
+        float[] pos = new float[3];
+        indexToData(iF, iI, index);
+        mikkTSpace.getPosition(pos, iF[0], iI[0]);
+        return new Vector3f(pos[0], pos[1], pos[2]);
+    }
+
+    static Vector3f getNormal(final MikkTSpaceContext mikkTSpace, final int index) {
+        //TODO nehon: very ugly but works... using arrays to pass integers as references in the indexToData
+        int[] iF = new int[1];
+        int[] iI = new int[1];
+        float[] norm = new float[3];
+        indexToData(iF, iI, index);
+        mikkTSpace.getNormal(norm, iF[0], iI[0]);
+        return new Vector3f(norm[0], norm[1], norm[2]);
+    }
+
+    static Vector3f getTexCoord(final MikkTSpaceContext mikkTSpace, final int index) {
+        //TODO nehon: very ugly but works... using arrays to pass integers as references in the indexToData
+        int[] iF = new int[1];
+        int[] iI = new int[1];
+        float[] texc = new float[2];
+        indexToData(iF, iI, index);
+        mikkTSpace.getTexCoord(texc, iF[0], iI[0]);
+        return new Vector3f(texc[0], texc[1], 1.0f);
+    }
+
+    // returns the texture area times 2
+    static float calcTexArea(final MikkTSpaceContext mikkTSpace, final int indices[]) {
+        final Vector3f t1 = getTexCoord(mikkTSpace, indices[0]);
+        final Vector3f t2 = getTexCoord(mikkTSpace, indices[1]);
+        final Vector3f t3 = getTexCoord(mikkTSpace, indices[2]);
+
+        final float t21x = t2.x - t1.x;
+        final float t21y = t2.y - t1.y;
+        final float t31x = t3.x - t1.x;
+        final float t31y = t3.y - t1.y;
+
+        final float fSignedAreaSTx2 = t21x * t31y - t21y * t31x;
+
+        return fSignedAreaSTx2 < 0 ? (-fSignedAreaSTx2) : fSignedAreaSTx2;
+    }
+
+    private static boolean isNotZero(float v) {
+        return Math.abs(v) > 0;
+    }
+
+    static void initTriInfo(TriInfo pTriInfos[], final int piTriListIn[], final MikkTSpaceContext mikkTSpace, final int iNrTrianglesIn) {
+
+        // pTriInfos[f].flag is cleared in GenerateInitialVerticesIndexList() which is called before this function.
+        // generate neighbor info list
+        for (int f = 0; f < iNrTrianglesIn; f++) {
+            for (int i = 0; i < 3; i++) {
+                pTriInfos[f].faceNeighbors[i] = -1;
+                pTriInfos[f].assignedGroup[i] = null;
+
+                pTriInfos[f].os.x = 0.0f;
+                pTriInfos[f].os.y = 0.0f;
+                pTriInfos[f].os.z = 0.0f;
+                pTriInfos[f].ot.x = 0.0f;
+                pTriInfos[f].ot.y = 0.0f;
+                pTriInfos[f].ot.z = 0.0f;
+                pTriInfos[f].magS = 0;
+                pTriInfos[f].magT = 0;
+
+                // assumed bad
+                pTriInfos[f].flag |= GROUP_WITH_ANY;
+            }
+        }
+
+        // evaluate first order derivatives
+        for (int f = 0; f < iNrTrianglesIn; f++) {
+            // initial values
+            final Vector3f v1 = getPosition(mikkTSpace, piTriListIn[f * 3 + 0]);
+            final Vector3f v2 = getPosition(mikkTSpace, piTriListIn[f * 3 + 1]);
+            final Vector3f v3 = getPosition(mikkTSpace, piTriListIn[f * 3 + 2]);
+            final Vector3f t1 = getTexCoord(mikkTSpace, piTriListIn[f * 3 + 0]);
+            final Vector3f t2 = getTexCoord(mikkTSpace, piTriListIn[f * 3 + 1]);
+            final Vector3f t3 = getTexCoord(mikkTSpace, piTriListIn[f * 3 + 2]);
+
+            final float t21x = t2.x - t1.x;
+            final float t21y = t2.y - t1.y;
+            final float t31x = t3.x - t1.x;
+            final float t31y = t3.y - t1.y;
+            final Vector3f d1 = v2.subtract(v1);
+            final Vector3f d2 = v3.subtract(v1);
+
+            final float fSignedAreaSTx2 = t21x * t31y - t21y * t31x;
+            //assert(fSignedAreaSTx2!=0);
+            Vector3f vOs = d1.mult(t31y).subtract(d2.mult(t21y));  // eq 18
+            Vector3f vOt = d1.mult(-t31x).add(d2.mult(t21x));  // eq 19
+
+            pTriInfos[f].flag |= (fSignedAreaSTx2 > 0 ? ORIENT_PRESERVING : 0);
+
+            if (isNotZero(fSignedAreaSTx2)) {
+                final float fAbsArea = Math.abs(fSignedAreaSTx2);
+                final float fLenOs = vOs.length();
+                final float fLenOt = vOt.length();
+                final float fS = (pTriInfos[f].flag & ORIENT_PRESERVING) == 0 ? (-1.0f) : 1.0f;
+                if (isNotZero(fLenOs)) {
+                    pTriInfos[f].os = vOs.multLocal(fS / fLenOs);
+                }
+                if (isNotZero(fLenOt)) {
+                    pTriInfos[f].ot = vOt.multLocal(fS / fLenOt);
+                }
+
+                // evaluate magnitudes prior to normalization of vOs and vOt
+                pTriInfos[f].magS = fLenOs / fAbsArea;
+                pTriInfos[f].magT = fLenOt / fAbsArea;
+
+                // if this is a good triangle
+                if (isNotZero(pTriInfos[f].magS) && isNotZero(pTriInfos[f].magT)) {
+                    pTriInfos[f].flag &= (~GROUP_WITH_ANY);
+                }
+            }
+        }
+
+        // force otherwise healthy quads to a fixed orientation
+        int t = 0;
+        while (t < (iNrTrianglesIn - 1)) {
+            final int iFO_a = pTriInfos[t].orgFaceNumber;
+            final int iFO_b = pTriInfos[t + 1].orgFaceNumber;
+            if (iFO_a == iFO_b) {
+                // this is a quad
+                final boolean bIsDeg_a = (pTriInfos[t].flag & MARK_DEGENERATE) != 0;
+                final boolean bIsDeg_b = (pTriInfos[t + 1].flag & MARK_DEGENERATE) != 0;
+
+                // bad triangles should already have been removed by
+                // DegenPrologue(), but just in case check bIsDeg_a and bIsDeg_a are false
+                if ((bIsDeg_a || bIsDeg_b) == false) {
+                    final boolean bOrientA = (pTriInfos[t].flag & ORIENT_PRESERVING) != 0;
+                    final boolean bOrientB = (pTriInfos[t + 1].flag & ORIENT_PRESERVING) != 0;
+                    // if this happens the quad has extremely bad mapping!!
+                    if (bOrientA != bOrientB) {
+                        //printf("found quad with bad mapping\n");
+                        boolean bChooseOrientFirstTri = false;
+                        if ((pTriInfos[t + 1].flag & GROUP_WITH_ANY) != 0) {
+                            bChooseOrientFirstTri = true;
+                        } else if (calcTexArea(mikkTSpace, Arrays.copyOfRange(piTriListIn, t * 3 + 0, t * 3 + 3)) >= calcTexArea(mikkTSpace, Arrays.copyOfRange(piTriListIn, (t + 1) * 3 + 0, (t + 1) * 3 + 3))) {
+                            bChooseOrientFirstTri = true;
+                        }
+
+                        // force match
+                        {
+                            final int t0 = bChooseOrientFirstTri ? t : (t + 1);
+                            final int t1 = bChooseOrientFirstTri ? (t + 1) : t;
+                            pTriInfos[t1].flag &= (~ORIENT_PRESERVING);  // clear first
+                            pTriInfos[t1].flag |= (pTriInfos[t0].flag & ORIENT_PRESERVING);  // copy bit
+                        }
+                    }
+                }
+                t += 2;
+            } else {
+                ++t;
+            }
+        }
+
+        // match up edge pairs
+        {
+            //Edge * pEdges = (Edge *) malloc(sizeof(Edge)*iNrTrianglesIn*3);
+            Edge[] pEdges = new Edge[iNrTrianglesIn * 3];
+
+            //TODO nehon weird... original algorithm check if pEdges is null but it's just been allocated... weirder, it does soemthing different if the edges are null...
+            //    if (pEdges==null)
+            //      BuildNeighborsSlow(pTriInfos, piTriListIn, iNrTrianglesIn);
+            //    else
+            //    {
+            buildNeighborsFast(pTriInfos, pEdges, piTriListIn, iNrTrianglesIn);
+
+            //    }
+        }
+    }
+
+    static int build4RuleGroups(TriInfo pTriInfos[], Group pGroups[], int piGroupTrianglesBuffer[], final int piTriListIn[], final int iNrTrianglesIn) {
+        final int iNrMaxGroups = iNrTrianglesIn * 3;
+        int iNrActiveGroups = 0;
+        int iOffset = 0;
+        // (void)iNrMaxGroups;  /* quiet warnings in non debug mode */
+        for (int f = 0; f < iNrTrianglesIn; f++) {
+            for (int i = 0; i < 3; i++) {
+                // if not assigned to a group
+                if ((pTriInfos[f].flag & GROUP_WITH_ANY) == 0 && pTriInfos[f].assignedGroup[i] == null) {
+                    boolean bOrPre;                    
+                    final int vert_index = piTriListIn[f * 3 + i];
+                    assert (iNrActiveGroups < iNrMaxGroups);
+                    pTriInfos[f].assignedGroup[i] = new Group(); 
+                    pGroups[iNrActiveGroups] = pTriInfos[f].assignedGroup[i];
+                    pTriInfos[f].assignedGroup[i].vertexRepresentitive = vert_index;
+                    pTriInfos[f].assignedGroup[i].orientPreservering = (pTriInfos[f].flag & ORIENT_PRESERVING) != 0;
+                    pTriInfos[f].assignedGroup[i].nrFaces = 0;
+                    
+                    ++iNrActiveGroups;
+
+                    addTriToGroup(pTriInfos[f].assignedGroup[i], f);
+                    bOrPre = (pTriInfos[f].flag & ORIENT_PRESERVING) != 0;
+                    int neigh_indexL = pTriInfos[f].faceNeighbors[i];
+                    int neigh_indexR = pTriInfos[f].faceNeighbors[i > 0 ? (i - 1) : 2];
+                    if (neigh_indexL >= 0) {
+                        // neighbor
+                        final boolean bAnswer
+                                = assignRecur(piTriListIn, pTriInfos, neigh_indexL,
+                                        pTriInfos[f].assignedGroup[i]);
+
+                        final boolean bOrPre2 = (pTriInfos[neigh_indexL].flag & ORIENT_PRESERVING) != 0;
+                        final boolean bDiff = bOrPre != bOrPre2;
+                        assert (bAnswer || bDiff);
+                        //(void)bAnswer, (void)bDiff;  /* quiet warnings in non debug mode */
+                    }
+                    if (neigh_indexR >= 0) {
+                        // neighbor
+                        final boolean bAnswer
+                                = assignRecur(piTriListIn, pTriInfos, neigh_indexR,
+                                        pTriInfos[f].assignedGroup[i]);
+
+                        final boolean bOrPre2 = (pTriInfos[neigh_indexR].flag & ORIENT_PRESERVING) != 0;
+                        final boolean bDiff = bOrPre != bOrPre2;
+                        assert (bAnswer || bDiff);
+                        //(void)bAnswer, (void)bDiff;  /* quiet warnings in non debug mode */
+                    }
+
+                    int[] faceIndices = new int[pTriInfos[f].assignedGroup[i].nrFaces];
+                    //pTriInfos[f].assignedGroup[i].faceIndices.toArray(faceIndices);
+                    for (int j = 0; j < faceIndices.length; j++) {
+                        faceIndices[j] = pTriInfos[f].assignedGroup[i].faceIndices.get(j);
+                    }
+                    
+                    //Nehon: copy back the faceIndices data into the groupTriangleBuffer.
+                    System.arraycopy( faceIndices, 0, piGroupTrianglesBuffer, iOffset, pTriInfos[f].assignedGroup[i].nrFaces);
+                    // update offset
+                    iOffset += pTriInfos[f].assignedGroup[i].nrFaces;
+                    // since the groups are disjoint a triangle can never
+                    // belong to more than 3 groups. Subsequently something
+                    // is completely screwed if this assertion ever hits.
+                    assert (iOffset <= iNrMaxGroups);
+                }
+            }
+        }
+
+        return iNrActiveGroups;
+    }
+
+    static void addTriToGroup(Group group, final int triIndex) {
+        //group.faceIndices[group.nrFaces] = triIndex;
+        group.faceIndices.add(triIndex);
+        ++group.nrFaces;
+    }
+
+    static boolean assignRecur(final int piTriListIn[], TriInfo psTriInfos[], final int iMyTriIndex, Group pGroup) {
+        TriInfo pMyTriInfo = psTriInfos[iMyTriIndex];
+
+        // track down vertex
+        final int iVertRep = pGroup.vertexRepresentitive;
+        int index = 3 * iMyTriIndex;
+        int i = -1;
+        if (piTriListIn[index] == iVertRep) {
+            i = 0;
+        } else if (piTriListIn[index + 1] == iVertRep) {
+            i = 1;
+        } else if (piTriListIn[index + 2] == iVertRep) {
+            i = 2;
+        }
+        assert (i >= 0 && i < 3);
+
+        // early out
+        if (pMyTriInfo.assignedGroup[i] == pGroup) {
+            return true;
+        } else if (pMyTriInfo.assignedGroup[i] != null) {
+            return false;
+        }
+        if ((pMyTriInfo.flag & GROUP_WITH_ANY) != 0) {
+            // first to group with a group-with-anything triangle
+            // determines it's orientation.
+            // This is the only existing order dependency in the code!!
+            if (pMyTriInfo.assignedGroup[0] == null
+                    && pMyTriInfo.assignedGroup[1] == null
+                    && pMyTriInfo.assignedGroup[2] == null) {
+                pMyTriInfo.flag &= (~ORIENT_PRESERVING);
+                pMyTriInfo.flag |= (pGroup.orientPreservering ? ORIENT_PRESERVING : 0);
+            }
+        }
+        {
+            final boolean bOrient = (pMyTriInfo.flag & ORIENT_PRESERVING) != 0;
+            if (bOrient != pGroup.orientPreservering) {
+                return false;
+            }
+        }
+
+        addTriToGroup(pGroup, iMyTriIndex);
+        pMyTriInfo.assignedGroup[i] = pGroup;
+
+        {
+            final int neigh_indexL = pMyTriInfo.faceNeighbors[i];
+            final int neigh_indexR = pMyTriInfo.faceNeighbors[i > 0 ? (i - 1) : 2];
+            if (neigh_indexL >= 0) {
+                assignRecur(piTriListIn, psTriInfos, neigh_indexL, pGroup);
+            }
+            if (neigh_indexR >= 0) {
+                assignRecur(piTriListIn, psTriInfos, neigh_indexR, pGroup);
+            }
+        }
+
+        return true;
+    }
+
+    static boolean generateTSpaces(TSpace psTspace[], final TriInfo pTriInfos[], final Group pGroups[],
+            final int iNrActiveGroups, final int piTriListIn[], final float fThresCos,
+            final MikkTSpaceContext mikkTSpace) {
+        TSpace[] pSubGroupTspace;
+        SubGroup[] pUniSubGroups;
+        int[] pTmpMembers;
+        int iMaxNrFaces = 0, iUniqueTspaces = 0, g = 0, i = 0;
+        for (g = 0; g < iNrActiveGroups; g++) {
+            if (iMaxNrFaces < pGroups[g].nrFaces) {
+                iMaxNrFaces = pGroups[g].nrFaces;
+            }
+        }
+
+        if (iMaxNrFaces == 0) {
+            return true;
+        }
+
+        // make initial allocations
+        pSubGroupTspace = new TSpace[iMaxNrFaces];
+        pUniSubGroups = new SubGroup[iMaxNrFaces];
+        pTmpMembers = new int[iMaxNrFaces];
+
+
+        iUniqueTspaces = 0;
+        for (g = 0; g < iNrActiveGroups; g++) {
+            final Group pGroup = pGroups[g];
+            int iUniqueSubGroups = 0, s = 0;
+
+            for (i = 0; i < pGroup.nrFaces; i++) // triangles
+            {
+                final int f = pGroup.faceIndices.get(i);  // triangle number
+                int index = -1, iVertIndex = -1, iOF_1 = -1, iMembers = 0, j = 0, l = 0;
+                SubGroup tmp_group = new SubGroup();
+                boolean bFound;
+                Vector3f n, vOs, vOt;
+                if (pTriInfos[f].assignedGroup[0] == pGroup) {
+                    index = 0;
+                } else if (pTriInfos[f].assignedGroup[1] == pGroup) {
+                    index = 1;
+                } else if (pTriInfos[f].assignedGroup[2] == pGroup) {
+                    index = 2;
+                }
+                assert (index >= 0 && index < 3);
+
+                iVertIndex = piTriListIn[f * 3 + index];
+                assert (iVertIndex == pGroup.vertexRepresentitive);
+
+                // is normalized already
+                n = getNormal(mikkTSpace, iVertIndex);
+
+                // project
+                vOs = pTriInfos[f].os.subtract(n.mult(n.dot(pTriInfos[f].os)));
+                vOt = pTriInfos[f].ot.subtract(n.mult(n.dot(pTriInfos[f].ot)));
+                vOs.normalizeLocal();
+                vOt.normalizeLocal();
+
+                // original face number
+                iOF_1 = pTriInfos[f].orgFaceNumber;
+
+                iMembers = 0;
+                for (j = 0; j < pGroup.nrFaces; j++) {
+                    final int t = pGroup.faceIndices.get(j);  // triangle number
+                    final int iOF_2 = pTriInfos[t].orgFaceNumber;
+
+                    // project
+                    Vector3f vOs2 = pTriInfos[t].os.subtract(n.mult(n.dot(pTriInfos[t].os)));
+                    Vector3f vOt2 = pTriInfos[t].ot.subtract(n.mult(n.dot(pTriInfos[t].ot)));
+                    vOs2.normalizeLocal();
+                    vOt2.normalizeLocal();
+
+                    {
+                        final boolean bAny = ((pTriInfos[f].flag | pTriInfos[t].flag) & GROUP_WITH_ANY) != 0;
+                        // make sure triangles which belong to the same quad are joined.
+                        final boolean bSameOrgFace = iOF_1 == iOF_2;
+
+                        final float fCosS = vOs.dot(vOs2);
+                        final float fCosT = vOt.dot(vOt2);
+
+                        assert (f != t || bSameOrgFace);  // sanity check
+                        if (bAny || bSameOrgFace || (fCosS > fThresCos && fCosT > fThresCos)) {
+                            pTmpMembers[iMembers++] = t;
+                        }
+                    }
+                }
+
+                // sort pTmpMembers
+                tmp_group.nrFaces = iMembers;
+                tmp_group.triMembers = pTmpMembers;
+                if (iMembers > 1) {
+                    quickSort(pTmpMembers, 0, iMembers - 1, INTERNAL_RND_SORT_SEED);
+                }
+
+                // look for an existing match
+                bFound = false;
+                l = 0;
+                while (l < iUniqueSubGroups && !bFound) {
+                    bFound = compareSubGroups(tmp_group, pUniSubGroups[l]);
+                    if (!bFound) {
+                        ++l;
+                    }
+                }
+
+                // assign tangent space index
+                assert (bFound || l == iUniqueSubGroups);
+                //piTempTangIndices[f*3+index] = iUniqueTspaces+l;
+
+                // if no match was found we allocate a new subgroup
+                if (!bFound) {
+                    // insert new subgroup
+                    int[] pIndices = new int[iMembers];
+                    pUniSubGroups[iUniqueSubGroups] = new SubGroup();
+                    pUniSubGroups[iUniqueSubGroups].nrFaces = iMembers;
+                    pUniSubGroups[iUniqueSubGroups].triMembers = pIndices;
+                    System.arraycopy(tmp_group.triMembers, 0, pIndices, 0, iMembers);
+                    //memcpy(pIndices, tmp_group.pTriMembers, iMembers*sizeof(int));
+                    pSubGroupTspace[iUniqueSubGroups]
+                            = evalTspace(tmp_group.triMembers, iMembers, piTriListIn, pTriInfos, mikkTSpace, pGroup.vertexRepresentitive);
+                    ++iUniqueSubGroups;
+                }
+
+                // output tspace
+                {
+                    final int iOffs = pTriInfos[f].tSpacesOffs;
+                    final int iVert = pTriInfos[f].vertNum[index];
+                    TSpace pTS_out = psTspace[iOffs + iVert];
+                    assert (pTS_out.counter < 2);
+                    assert (((pTriInfos[f].flag & ORIENT_PRESERVING) != 0) == pGroup.orientPreservering);
+                    if (pTS_out.counter == 1) {
+                        pTS_out.set(avgTSpace(pTS_out, pSubGroupTspace[l]));
+                        pTS_out.counter = 2;  // update counter
+                        pTS_out.orient = pGroup.orientPreservering;
+                    } else {
+                        assert (pTS_out.counter == 0);
+                        pTS_out.set(pSubGroupTspace[l]);
+                        pTS_out.counter = 1;  // update counter
+                        pTS_out.orient = pGroup.orientPreservering;
+                    }
+                }
+            }
+
+            iUniqueTspaces += iUniqueSubGroups;
+        }
+
+        return true;
+    }
+
+    static TSpace evalTspace(int face_indices[], final int iFaces, final int piTriListIn[], final TriInfo pTriInfos[],
+            final MikkTSpaceContext mikkTSpace, final int iVertexRepresentitive) {
+        TSpace res = new TSpace();
+        float fAngleSum = 0;        
+
+        for (int face = 0; face < iFaces; face++) {
+            final int f = face_indices[face];
+
+            // only valid triangles get to add their contribution
+            if ((pTriInfos[f].flag & GROUP_WITH_ANY) == 0) {
+                
+                int i = -1;
+                if (piTriListIn[3 * f + 0] == iVertexRepresentitive) {
+                    i = 0;
+                } else if (piTriListIn[3 * f + 1] == iVertexRepresentitive) {
+                    i = 1;
+                } else if (piTriListIn[3 * f + 2] == iVertexRepresentitive) {
+                    i = 2;
+                }
+                assert (i >= 0 && i < 3);
+
+                // project
+                int index = piTriListIn[3 * f + i];
+                Vector3f n = getNormal(mikkTSpace, index);
+                Vector3f vOs = pTriInfos[f].os.subtract(n.mult(n.dot(pTriInfos[f].os)));
+                Vector3f vOt = pTriInfos[f].ot.subtract(n.mult(n.dot(pTriInfos[f].ot)));
+                vOs.normalizeLocal();
+                vOt.normalizeLocal();
+
+                int i2 = piTriListIn[3 * f + (i < 2 ? (i + 1) : 0)];
+                int i1 = piTriListIn[3 * f + i];
+                int i0 = piTriListIn[3 * f + (i > 0 ? (i - 1) : 2)];
+
+                Vector3f p0 = getPosition(mikkTSpace, i0);
+                Vector3f p1 = getPosition(mikkTSpace, i1);
+                Vector3f  p2 = getPosition(mikkTSpace, i2);
+                Vector3f v1 = p0.subtract(p1);
+                Vector3f v2 = p2.subtract(p1);
+
+                // project
+                v1.subtractLocal(n.mult(n.dot(v1))).normalizeLocal();
+                v2.subtractLocal(n.mult(n.dot(v2))).normalizeLocal();
+
+                // weight contribution by the angle
+                // between the two edge vectors
+                float fCos = v1.dot(v2);
+                fCos = fCos > 1 ? 1 : (fCos < (-1) ? (-1) : fCos);
+                float fAngle = (float) Math.acos(fCos);
+                float fMagS = pTriInfos[f].magS;
+                float fMagT = pTriInfos[f].magT;
+
+                res.os.addLocal(vOs.multLocal(fAngle));
+                res.ot.addLocal(vOt.multLocal(fAngle));
+                res.magS += (fAngle * fMagS);
+                res.magT += (fAngle * fMagT);
+                fAngleSum += fAngle;
+            }
+        }
+
+        // normalize
+        res.os.normalizeLocal();
+        res.ot.normalizeLocal();
+
+        if (fAngleSum > 0) {
+            res.magS /= fAngleSum;
+            res.magT /= fAngleSum;
+        }
+
+        return res;
+    }
+
+    static boolean compareSubGroups(final SubGroup pg1, final SubGroup pg2) {
+        if(pg2 == null || (pg1.nrFaces != pg2.nrFaces)){
+            return false;
+        }
+        boolean stillSame = true;
+        int i = 0;        
+        while (i < pg1.nrFaces && stillSame) {
+            stillSame = pg1.triMembers[i] == pg2.triMembers[i];
+            if (stillSame) {
+                ++i;
+            }
+        }
+        return stillSame;
+    }
+
+    static void quickSort(int[] pSortBuffer, int iLeft, int iRight, long uSeed) {
+        int iL, iR, n, index, iMid, iTmp;
+
+        // Random
+        long t = uSeed & 31;
+        t = (uSeed << t) | (uSeed >> (32 - t));
+        uSeed = uSeed + t + 3;
+        // Random end
+        uSeed = uSeed & 0xffffffffL;
+
+        iL = iLeft;
+        iR = iRight;
+        n = (iR - iL) + 1;
+        assert (n >= 0);
+        index = (int) ((uSeed & 0xffffffffL) % n);
+
+        iMid = pSortBuffer[index + iL];
+
+        do {
+            while (pSortBuffer[iL] < iMid) {
+                ++iL;
+            }
+            while (pSortBuffer[iR] > iMid) {
+                --iR;
+            }
+
+            if (iL <= iR) {
+                iTmp = pSortBuffer[iL];
+                pSortBuffer[iL] = pSortBuffer[iR];
+                pSortBuffer[iR] = iTmp;
+                ++iL;
+                --iR;
+            }
+        } while (iL <= iR);
+
+        if (iLeft < iR) {
+            quickSort(pSortBuffer, iLeft, iR, uSeed);
+        }
+        if (iL < iRight) {
+            quickSort(pSortBuffer, iL, iRight, uSeed);
+        }
+    }
+
+    static void buildNeighborsFast(TriInfo pTriInfos[], Edge[] pEdges, final int piTriListIn[], final int iNrTrianglesIn) {
+        // build array of edges
+        long uSeed = INTERNAL_RND_SORT_SEED;        // could replace with a random seed?
+        
+        for (int f = 0; f < iNrTrianglesIn; f++) {
+            for (int i = 0; i < 3; i++) {
+                final int i0 = piTriListIn[f * 3 + i];
+                final int i1 = piTriListIn[f * 3 + (i < 2 ? (i + 1) : 0)];
+                pEdges[f * 3 + i] = new Edge();
+                pEdges[f * 3 + i].setI0(i0 < i1 ? i0 : i1);      // put minimum index in i0
+                pEdges[f * 3 + i].setI1(!(i0 < i1) ? i0 : i1);    // put maximum index in i1
+                pEdges[f * 3 + i].setF(f);              // record face number
+            }
+        }
+
+        // sort over all edges by i0, this is the pricy one.
+        quickSortEdges(pEdges, 0, iNrTrianglesIn * 3 - 1, 0, uSeed);  // sort channel 0 which is i0
+
+        // sub sort over i1, should be fast.
+        // could replace this with a 64 bit int sort over (i0,i1)
+        // with i0 as msb in the quicksort call above.
+        int iEntries = iNrTrianglesIn * 3;
+        int iCurStartIndex = 0;
+        for (int i = 1; i < iEntries; i++) {
+            if (pEdges[iCurStartIndex].getI0() != pEdges[i].getI0()) {
+                final int iL = iCurStartIndex;
+                final int iR = i - 1;
+                //final int iElems = i-iL;
+                iCurStartIndex = i;
+                quickSortEdges(pEdges, iL, iR, 1, uSeed);  // sort channel 1 which is i1
+            }
+        }
+
+        // sub sort over f, which should be fast.
+        // this step is to remain compliant with BuildNeighborsSlow() when
+        // more than 2 triangles use the same edge (such as a butterfly topology).
+        iCurStartIndex = 0;
+        for (int i = 1; i < iEntries; i++) {
+            if (pEdges[iCurStartIndex].getI0() != pEdges[i].getI0() || pEdges[iCurStartIndex].getI1() != pEdges[i].getI1()) {
+                final int iL = iCurStartIndex;
+                final int iR = i - 1;
+                //final int iElems = i-iL;
+                iCurStartIndex = i;
+                quickSortEdges(pEdges, iL, iR, 2, uSeed);  // sort channel 2 which is f
+            }
+        }
+
+        // pair up, adjacent triangles
+        for (int i = 0; i < iEntries; i++) {
+            final int i0 = pEdges[i].getI0();
+            final int i1 = pEdges[i].getI1();
+            final int g = pEdges[i].getF();
+            boolean bUnassigned_A;
+
+            int[] i0_A = new int[1];
+            int[] i1_A = new int[1];
+            int[] edgenum_A = new int[1];
+            int[] edgenum_B = new int[1];
+            //int edgenum_B=0;  // 0,1 or 2
+            int[] triList = new int[3];
+            System.arraycopy(piTriListIn, g * 3, triList, 0, 3);
+            getEdge(i0_A, i1_A, edgenum_A, triList, i0, i1);  // resolve index ordering and edge_num
+            bUnassigned_A = pTriInfos[g].faceNeighbors[edgenum_A[0]] == -1;
+
+            if (bUnassigned_A) {
+                // get true index ordering
+                int j = i + 1, t;
+                boolean bNotFound = true;
+                while (j < iEntries && i0 == pEdges[j].getI0() && i1 == pEdges[j].getI1() && bNotFound) {
+                    boolean bUnassigned_B;
+                    int[] i0_B = new int[1];
+                    int[] i1_B = new int[1];
+                    t = pEdges[j].getF();
+                    // flip i0_B and i1_B
+                    System.arraycopy(piTriListIn, t * 3, triList, 0, 3);
+                    getEdge(i1_B, i0_B, edgenum_B, triList, pEdges[j].getI0(), pEdges[j].getI1());  // resolve index ordering and edge_num
+                    //assert(!(i0_A==i1_B && i1_A==i0_B));
+                    bUnassigned_B = pTriInfos[t].faceNeighbors[edgenum_B[0]] == -1;
+                    if (i0_A[0] == i0_B[0] && i1_A[0] == i1_B[0] && bUnassigned_B) {
+                        bNotFound = false;
+                    } else {
+                        ++j;
+                    }
+                }
+
+                if (!bNotFound) {
+                    int t2 = pEdges[j].getF();
+                    pTriInfos[g].faceNeighbors[edgenum_A[0]] = t2;
+                    //assert(pTriInfos[t].FaceNeighbors[edgenum_B]==-1);
+                    pTriInfos[t2].faceNeighbors[edgenum_B[0]] = g;
+                }
+            }
+        }
+    }
+
+    static void buildNeighborsSlow(TriInfo pTriInfos[], final int piTriListIn[], final int iNrTrianglesIn) {
+        
+        for (int f = 0; f < iNrTrianglesIn; f++) {
+            for (int i = 0; i < 3; i++) {
+                // if unassigned
+                if (pTriInfos[f].faceNeighbors[i] == -1) {
+                    final int i0_A = piTriListIn[f * 3 + i];
+                    final int i1_A = piTriListIn[f * 3 + (i < 2 ? (i + 1) : 0)];
+
+                    // search for a neighbor
+                    boolean bFound = false;
+                    int t = 0, j = 0;
+                    while (!bFound && t < iNrTrianglesIn) {
+                        if (t != f) {
+                            j = 0;
+                            while (!bFound && j < 3) {
+                                // in rev order
+                                final int i1_B = piTriListIn[t * 3 + j];
+                                final int i0_B = piTriListIn[t * 3 + (j < 2 ? (j + 1) : 0)];
+                                //assert(!(i0_A==i1_B && i1_A==i0_B));
+                                if (i0_A == i0_B && i1_A == i1_B) {
+                                    bFound = true;
+                                } else {
+                                    ++j;
+                                }
+                            }
+                        }
+
+                        if (!bFound) {
+                            ++t;
+                        }
+                    }
+
+                    // assign neighbors
+                    if (bFound) {
+                        pTriInfos[f].faceNeighbors[i] = t;
+                        //assert(pTriInfos[t].FaceNeighbors[j]==-1);
+                        pTriInfos[t].faceNeighbors[j] = f;
+                    }
+                }
+            }
+        }
+    }
+
+    static void quickSortEdges(Edge[] pSortBuffer, int iLeft, int iRight, final int channel, long uSeed) {
+        // early out
+        Edge sTmp;
+        final int iElems = iRight - iLeft + 1;
+        if (iElems < 2) {
+            return;
+        } else if (iElems == 2) {
+            if (pSortBuffer[iLeft].array[channel] > pSortBuffer[iRight].array[channel]) {
+                sTmp = pSortBuffer[iLeft];
+                pSortBuffer[iLeft] = pSortBuffer[iRight];
+                pSortBuffer[iRight] = sTmp;
+            }
+            return;
+        }
+
+        // Random
+        long t = uSeed & 31;
+        t = (uSeed << t) | (uSeed >> (32 - t));
+        uSeed = uSeed + t + 3;
+        // Random end
+        
+        uSeed = uSeed & 0xffffffffL;
+
+        int iL = iLeft;
+        int iR = iRight;
+        int n = (iR - iL) + 1;
+        assert (n >= 0);
+        int index = (int) (uSeed % n);
+
+        int iMid = pSortBuffer[index + iL].array[channel];
+
+        do {
+            while (pSortBuffer[iL].array[channel] < iMid) {
+                ++iL;
+            }
+            while (pSortBuffer[iR].array[channel] > iMid) {
+                --iR;
+            }
+
+            if (iL <= iR) {
+                sTmp = pSortBuffer[iL];
+                pSortBuffer[iL] = pSortBuffer[iR];
+                pSortBuffer[iR] = sTmp;
+                ++iL;
+                --iR;
+            }
+        } while (iL <= iR);
+
+        if (iLeft < iR) {
+            quickSortEdges(pSortBuffer, iLeft, iR, channel, uSeed);
+        }
+        if (iL < iRight) {
+            quickSortEdges(pSortBuffer, iL, iRight, channel, uSeed);
+        }
+    }
+
+// resolve ordering and edge number
+    static void getEdge(int[] i0_out, int[] i1_out, int[] edgenum_out, final int[] indices, final int i0_in, final int i1_in) {
+        edgenum_out[0] = -1;
+
+        // test if first index is on the edge
+        if (indices[0] == i0_in || indices[0] == i1_in) {
+            // test if second index is on the edge
+            if (indices[1] == i0_in || indices[1] == i1_in) {
+                edgenum_out[0] = 0;  // first edge
+                i0_out[0] = indices[0];
+                i1_out[0] = indices[1];
+            } else {
+                edgenum_out[0] = 2;  // third edge
+                i0_out[0] = indices[2];
+                i1_out[0] = indices[0];
+            }
+        } else {
+            // only second and third index is on the edge
+            edgenum_out[0] = 1;  // second edge
+            i0_out[0] = indices[1];
+            i1_out[0] = indices[2];
+        }
+    }
+
+    static void degenPrologue(TriInfo pTriInfos[], int piTriList_out[], final int iNrTrianglesIn, final int iTotTris) {
+        
+        // locate quads with only one good triangle
+        int t = 0;
+        while (t < (iTotTris - 1)) {
+            final int iFO_a = pTriInfos[t].orgFaceNumber;
+            final int iFO_b = pTriInfos[t + 1].orgFaceNumber;
+            if (iFO_a == iFO_b) {
+                // this is a quad
+                final boolean bIsDeg_a = (pTriInfos[t].flag & MARK_DEGENERATE) != 0;
+                final boolean bIsDeg_b = (pTriInfos[t + 1].flag & MARK_DEGENERATE) != 0;
+                //TODO nehon : Check this in detail as this operation is utterly strange
+                if ((bIsDeg_a ^ bIsDeg_b) != false) {
+                    pTriInfos[t].flag |= QUAD_ONE_DEGEN_TRI;
+                    pTriInfos[t + 1].flag |= QUAD_ONE_DEGEN_TRI;
+                }
+                t += 2;
+            } else {
+                ++t;
+            }
+        }
+
+        // reorder list so all degen triangles are moved to the back
+        // without reordering the good triangles
+        int iNextGoodTriangleSearchIndex = 1;
+        t = 0;
+        boolean bStillFindingGoodOnes = true;
+        while (t < iNrTrianglesIn && bStillFindingGoodOnes) {
+            final boolean bIsGood = (pTriInfos[t].flag & MARK_DEGENERATE) == 0;
+            if (bIsGood) {
+                if (iNextGoodTriangleSearchIndex < (t + 2)) {
+                    iNextGoodTriangleSearchIndex = t + 2;
+                }
+            } else {                
+                // search for the first good triangle.
+                boolean bJustADegenerate = true;
+                while (bJustADegenerate && iNextGoodTriangleSearchIndex < iTotTris) {
+                    final boolean bIsGood2 = (pTriInfos[iNextGoodTriangleSearchIndex].flag & MARK_DEGENERATE) == 0;
+                    if (bIsGood2) {
+                        bJustADegenerate = false;
+                    } else {
+                        ++iNextGoodTriangleSearchIndex;
+                    }
+                }
+
+                int t0 = t;
+                int t1 = iNextGoodTriangleSearchIndex;
+                ++iNextGoodTriangleSearchIndex;
+                assert (iNextGoodTriangleSearchIndex > (t + 1));
+
+                // swap triangle t0 and t1
+                if (!bJustADegenerate) {                    
+                    for (int i = 0; i < 3; i++) {
+                        final int index = piTriList_out[t0 * 3 + i];
+                        piTriList_out[t0 * 3 + i] = piTriList_out[t1 * 3 + i];
+                        piTriList_out[t1 * 3 + i] = index;
+                    }
+                    {
+                        final TriInfo tri_info = pTriInfos[t0];
+                        pTriInfos[t0] = pTriInfos[t1];
+                        pTriInfos[t1] = tri_info;
+                    }
+                } else {
+                    bStillFindingGoodOnes = false;  // this is not supposed to happen
+                }
+            }
+
+            if (bStillFindingGoodOnes) {
+                ++t;
+            }
+        }
+
+        assert (bStillFindingGoodOnes);  // code will still work.
+        assert (iNrTrianglesIn == t);
+    }
+
+    static void DegenEpilogue(TSpace psTspace[], TriInfo pTriInfos[], int piTriListIn[], final MikkTSpaceContext mikkTSpace, final int iNrTrianglesIn, final int iTotTris) {
+        
+        // deal with degenerate triangles
+        // punishment for degenerate triangles is O(N^2)
+        for (int t = iNrTrianglesIn; t < iTotTris; t++) {
+            // degenerate triangles on a quad with one good triangle are skipped
+            // here but processed in the next loop
+            final boolean bSkip = (pTriInfos[t].flag & QUAD_ONE_DEGEN_TRI) != 0;
+
+            if (!bSkip) {
+                for (int i = 0; i < 3; i++) {
+                    final int index1 = piTriListIn[t * 3 + i];
+                    // search through the good triangles
+                    boolean bNotFound = true;
+                    int j = 0;
+                    while (bNotFound && j < (3 * iNrTrianglesIn)) {
+                        final int index2 = piTriListIn[j];
+                        if (index1 == index2) {
+                            bNotFound = false;
+                        } else {
+                            ++j;
+                        }
+                    }
+
+                    if (!bNotFound) {
+                        final int iTri = j / 3;
+                        final int iVert = j % 3;
+                        final int iSrcVert = pTriInfos[iTri].vertNum[iVert];
+                        final int iSrcOffs = pTriInfos[iTri].tSpacesOffs;
+                        final int iDstVert = pTriInfos[t].vertNum[i];
+                        final int iDstOffs = pTriInfos[t].tSpacesOffs;
+
+                        // copy tspace
+                        psTspace[iDstOffs + iDstVert] = psTspace[iSrcOffs + iSrcVert];
+                    }
+                }
+            }
+        }
+
+        // deal with degenerate quads with one good triangle
+        for (int t = 0; t < iNrTrianglesIn; t++) {
+            // this triangle belongs to a quad where the
+            // other triangle is degenerate
+            if ((pTriInfos[t].flag & QUAD_ONE_DEGEN_TRI) != 0) {
+               
+                byte[] pV = pTriInfos[t].vertNum;
+                int iFlag = (1 << pV[0]) | (1 << pV[1]) | (1 << pV[2]);
+                int iMissingIndex = 0;
+                if ((iFlag & 2) == 0) {
+                    iMissingIndex = 1;
+                } else if ((iFlag & 4) == 0) {
+                    iMissingIndex = 2;
+                } else if ((iFlag & 8) == 0) {
+                    iMissingIndex = 3;
+                }
+
+                int iOrgF = pTriInfos[t].orgFaceNumber;
+                Vector3f vDstP = getPosition(mikkTSpace, makeIndex(iOrgF, iMissingIndex));
+                boolean bNotFound = true;
+                int i = 0;
+                while (bNotFound && i < 3) {
+                    final int iVert = pV[i];
+                    final Vector3f vSrcP = getPosition(mikkTSpace, makeIndex(iOrgF, iVert));
+                    if (vSrcP.equals(vDstP)) {
+                        final int iOffs = pTriInfos[t].tSpacesOffs;
+                        psTspace[iOffs + iMissingIndex] = psTspace[iOffs + iVert];
+                        bNotFound = false;
+                    } else {
+                        ++i;
+                    }
+                }
+                assert (!bNotFound);
+            }
+        }
+
+    }    
+
+    /**
+     * SubGroup inner class
+     */
+    private static class SubGroup {
+        int nrFaces;
+        int[] triMembers;
+    }
+
+    private static class Group {
+        int nrFaces;
+        List<Integer> faceIndices = new ArrayList<Integer>();
+        int vertexRepresentitive;
+        boolean orientPreservering;
+    }
+
+    private static class TriInfo {
+
+        int[] faceNeighbors = new int[3];
+        Group[] assignedGroup = new Group[3];
+
+        // normalized first order face derivatives
+        Vector3f os = new Vector3f();
+        Vector3f ot = new Vector3f();
+        float magS, magT;  // original magnitudes
+
+        // determines if the current and the next triangle are a quad.
+        int orgFaceNumber;
+        int flag, tSpacesOffs;
+        byte[] vertNum = new byte[4];
+    }
+
+    private static class TSpace {
+
+        Vector3f os = new Vector3f();
+        float magS;
+        Vector3f ot = new Vector3f();
+        float magT;
+        int counter;  // this is to average back into quads.
+        boolean orient;
+        
+        void set(TSpace ts){
+            os.set(ts.os);
+            magS = ts.magS;
+            ot.set(ts.ot);
+            magT = ts.magT;
+            counter = ts.counter;
+            orient = ts.orient;
+        }
+    }
+
+    private static class TmpVert {
+
+        float vert[] = new float[3];
+        int index;
+    }
+
+    private static class Edge {
+
+        void setI0(int i){            
+            array[0] = i;
+        }
+        
+        void setI1(int i){            
+            array[1] = i;
+        }
+        
+        void setF(int i){            
+            array[2] = i;
+        }
+        
+        int getI0(){            
+            return array[0];
+        }
+        
+        int getI1(){            
+            return array[1];
+        }
+        
+        int getF(){            
+            return array[2];
+        }
+        
+        int[] array = new int[3];
+    }
+
+}

+ 1 - 0
sdk/build.gradle

@@ -109,6 +109,7 @@ task createBaseXml(dependsOn: configurations.corelibs) <<{
         dep.dependencyProject.configurations.archives.allArtifacts.each{ artifact->
             if(artifact.classifier == "sources"){
             } else if(artifact.classifier == "javadoc"){
+            } else if(artifact.file.name.endsWith('.pom')) {
             } else{
                 if(!jmeJarFiles.contains(artifact.file)){
                     jmeJarFiles.add(artifact.file)

+ 1 - 1
sdk/jme3-materialeditor/src/com/jme3/gde/materialdefinition/EditableMatDefFile.java

@@ -186,7 +186,7 @@ public class EditableMatDefFile {
             return "";
         } catch (Exception e) {
             Exceptions.printStackTrace(e);
-            return "error generating shader " + e.getMessage();
+            return "Error generating shader: " + e.getMessage();
         }
     }
 

+ 1 - 0
sdk/jme3-materialeditor/src/com/jme3/gde/materialdefinition/MatDefDataObject.java

@@ -142,6 +142,7 @@ public class MatDefDataObject extends MultiDataObject {
         findAssetManager();
         final MatDefMetaData metaData = new MatDefMetaData(this);
         lookupContents.add(metaData);
+        lookupContents.add(new MatDefNavigatorPanel());
         pf.addFileChangeListener(new FileChangeAdapter() {
             @Override
             public void fileChanged(FileEvent fe) {

+ 15 - 1
sdk/jme3-materialeditor/src/com/jme3/gde/materialdefinition/editor/MatDefEditorToolBar.java

@@ -8,6 +8,8 @@ package com.jme3.gde.materialdefinition.editor;
 import com.jme3.gde.materialdefinition.fileStructure.TechniqueBlock;
 import java.awt.Component;
 import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 import javax.swing.DefaultComboBoxModel;
 import javax.swing.DefaultListCellRenderer;
 import javax.swing.JLabel;
@@ -23,7 +25,8 @@ public class MatDefEditorToolBar extends javax.swing.JPanel {
 
     private MatDefEditorlElement parent;
     private final DefaultComboBoxModel<TechniqueBlock> comboModel = new DefaultComboBoxModel<TechniqueBlock>();
-
+    private final static Logger logger = Logger.getLogger(MatDefEditorToolBar.class.getName());
+    
     /**
      * Creates new form MatDefEditorToolBar
      */
@@ -130,6 +133,17 @@ public class MatDefEditorToolBar extends javax.swing.JPanel {
     }// </editor-fold>//GEN-END:initComponents
 
     private void techniqueComboBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_techniqueComboBoxActionPerformed
+        if (techniqueComboBox.getSelectedItem() == null) {
+            if (techniqueComboBox.getItemCount() > 0) {
+                if (techniqueComboBox.getItemCount() > 1) {
+                    logger.log(Level.WARNING, "No Technique selected, taking the first one!"); /* Don't be over verbose: When there's only one Element, you can't select itself again, thus null */
+                }
+                techniqueComboBox.setSelectedIndex(0); /* Take the first one available */
+            } else {
+                logger.log(Level.WARNING, "No Techniques known for this MaterialDef. Please add one using the button to the right!");
+                return;
+            }
+        }
         parent.switchTechnique((TechniqueBlock) techniqueComboBox.getSelectedItem());
     }//GEN-LAST:event_techniqueComboBoxActionPerformed
 

+ 24 - 3
sdk/jme3-materialeditor/src/com/jme3/gde/materialdefinition/fileStructure/TechniqueBlock.java

@@ -15,6 +15,8 @@ import com.jme3.util.blockparser.Statement;
 import java.beans.PropertyChangeListener;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 import org.openide.util.WeakListeners;
 
 /**
@@ -28,6 +30,8 @@ public class TechniqueBlock extends UberStatement {
     public static final String ADD_WORLD_PARAM = "addWorldParam";
     public static final String REMOVE_WORLD_PARAM = "removeWorldParam";
     protected String name;
+    
+    private static final Logger logger = Logger.getLogger(TechniqueBlock.class.getName());
 
     protected TechniqueBlock(int lineNumber, String line) {
         super(lineNumber, line);
@@ -102,7 +106,13 @@ public class TechniqueBlock extends UberStatement {
     }
 
     public List<WorldParamBlock> getWorldParams() {
-        return getWorldParameters().getWorldParams();
+        WorldParametersBlock block = getWorldParameters();
+        if (block != null)
+            return getWorldParameters().getWorldParams();
+        else {
+            logger.log(Level.WARNING, "Unable to build ShaderNodes: Could not find any WorldParameters. Most likely the technique {0} is broken.", line);
+            return new ArrayList<WorldParamBlock>();
+        }
     }
 
     public void addWorldParam(WorldParamBlock block) {
@@ -180,8 +190,19 @@ public class TechniqueBlock extends UberStatement {
 
     public List<ShaderNodeBlock> getShaderNodes() {
         List<ShaderNodeBlock> list = new ArrayList<ShaderNodeBlock>();
-        list.addAll(getBlock(VertexShaderNodesBlock.class).getShaderNodes());
-        list.addAll(getBlock(FragmentShaderNodesBlock.class).getShaderNodes());
+        
+        VertexShaderNodesBlock vert_block = getBlock(VertexShaderNodesBlock.class);
+        if (vert_block == null)
+            logger.log(Level.WARNING, "Unable to build ShaderNodes: Could not find any VertexShaderNode. Most likely the technique {0} is broken.", line);
+        else
+            list.addAll(vert_block.getShaderNodes());
+        
+        FragmentShaderNodesBlock frag_block = getBlock(FragmentShaderNodesBlock.class);
+        if (frag_block == null)
+            logger.log(Level.WARNING, "Unable to build ShaderNodes: Could not find any FragmentShaderNode. Most likely the technique {0} is broken.", line);
+        else
+            list.addAll(frag_block.getShaderNodes());
+        
         return list;
     }
 

+ 3 - 2
sdk/jme3-materialeditor/src/com/jme3/gde/materials/MaterialPreviewRenderer.java

@@ -148,7 +148,7 @@ public class MaterialPreviewRenderer implements SceneListener {
         });
     }
 
-    private int lastErrorHash = 0;
+    private static int lastErrorHash = 0;
 
     private void smartLog(String expText, String message) {
         int hash = message.hashCode();
@@ -183,7 +183,8 @@ public class MaterialPreviewRenderer implements SceneListener {
             //compilation error, the shader code will be output to the console
             //the following code will output the error
             //System.err.println(e.getMessage());
-            Logger.getLogger(MaterialDebugAppState.class.getName()).log(Level.SEVERE, e.getMessage());
+            //Logger.getLogger(MaterialDebugAppState.class.getName()).log(Level.SEVERE, e.getMessage());
+            smartLog("{0}", e.getMessage());
 
             java.awt.EventQueue.invokeLater(new Runnable() {
                 public void run() {

+ 1 - 1
sdk/jme3-welcome-screen/src/com/jme3/gde/welcome/Bundle.properties

@@ -3,6 +3,6 @@ OpenIDE-Module-Long-Description=\
     The jMonkeyEngine GDE Welcome Screen
 OpenIDE-Module-Name=Welcome Screen
 OpenIDE-Module-Short-Description=The jMonkeyEngine GDE Welcome Screen
-WelcomeScreenTopComponent.http.link=http://hub.jmonkeyengine.org/wiki/doku.php/sdk:welcome:3_0?do=export_xhtmlbody
+WelcomeScreenTopComponent.http.link=http://hub.jmonkeyengine.org/wiki/doku.php/sdk:welcome:3_1?do=export_xhtmlbody
 WelcomeScreenTopComponent.rss.link=http://hub.jmonkeyengine.org/feed/rdf/
 WelcomeScreenTopComponent.local.link=nbres:/com/jme3/gde/docs/sdk/welcome/local.html

+ 118 - 136
version.gradle

@@ -2,21 +2,33 @@
  Version Info Examples
  =====================
  
- Nightly Build Snapshot (no git tag)
-  * Full Version: 3.1.0-5124
-  * POM Version: 3.1.0-SNAPSHOT
-
- Nightly Build Snapshot (PBRIsComing branch) (no git tag)
-  * Full Version: 3.1.0-PBRIsComing-5124
-  * POM Version: 3.1.0-PBRIsComing-SNAPSHOT
-
- Alpha1 Release (git tag: v3.1-alpha1)
-  * Full Version: 3.1.0-alpha1
-  * POM Version: 3.1.0-alpha1
+ Nightly Build Snapshot
+ * git tag:
+ * Full Version: 3.1-5124
+ * POM Version: 3.1.0-SNAPSHOT
+ * NBM Revision: 5124
+ * NBM UC Suffix: nightly/3.1/plugins
+
+ Nightly Build Snapshot (PBRIsComing branch)
+ * git tag:
+ * Full Version: 3.1-PBRIsComing-5124
+ * POM Version: 3.1.0-PBRIsComing-SNAPSHOT
+ * NBM Revision: 5124
+ * NBM UC Suffix: PBRIsComing-nightly/3.1/plugins
+
+ Alpha1 Release
+ * git tag: v3.1.0-alpha1
+ * Full Version: 3.1-alpha1
+ * POM Version: 3.1.0-alpha1
+ * NBM Revision: 0
+ * NBM UC Suffix: stable/3.1/plugins
  
- Final Release (git tag: v3.1)
-  * Full Version: 3.1.0
-  * POM Version: 3.1.0
+ Final Release
+ * git tag: v3.1.0
+ * Full Version: 3.1
+ * POM Version: 3.1.0
+ * NBM Revision: 0
+ * NBM UC Suffix: stable/3.1/plugins
  */ 
 
 import java.text.SimpleDateFormat
@@ -35,143 +47,113 @@ ext {
     releaseInfo = null;
 }
 
-enum ReleaseType {
-    Unknown,
-    Snapshot,
-    PreRelease,
-    Release;
-}
-
-class ReleaseInfo {
-
-    String tag;
-    String version;
-    String releaseName;
-    ReleaseType releaseType;
-    String buildDate;
-    
-    String branch;
-    String hash;
-    String shortHash;
-    int revision;
-
-    String fullVersion;
-    String pomVersion;
-    
-    ReleaseInfo(String version, Grgit repo) {
-        loadBuildDate();
-        loadRepoInfo(version, repo);
+def getReleaseInfo(String tag) {
+    if (tag == null) {
+        // not a tagged commit
+        return null;
     }
-
-    ReleaseInfo(String version) {
-        loadBuildDate();
-        loadUnknownInfo(version);
+    if (!tag.startsWith("v")) {
+        // syntax error
+        return null;
     }
-
-    final void loadBuildDate() {
-        this.buildDate = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
+    tag = tag.substring(1)
+
+    String[] parts = tag.split("-", 2);
+    String mainVersion;
+    boolean prerelease;
+    String releaseName = null;
+
+    if (parts.length == 2) {
+        // prerelease
+        prerelease = true;
+        mainVersion = parts[0];
+        releaseName = parts[1];
+        if (releaseName.size() == 0) {
+            // syntax error
+            return null;
+        }
+    } else if (parts.length == 1) {
+        // final release
+        prerelease = false;
+        mainVersion = parts[0];
+    } else {
+        // error
+        return null;
     }
 
-    final void loadUnknownInfo(String version) {
-        this.releaseType = ReleaseType.Unknown;
-        this.version     = version;
-        this.releaseName = "unknown";
-        this.tag         = "";
-        this.revision    = 0;
-        this.branch      = "unknown";
-        this.hash        = "";
-        this.shortHash   = "";
-        this.fullVersion = "${version}-UNKNOWN";
-        this.pomVersion  = "${version}-UNKNOWN";
+    if (mainVersion.size() == 0) {
+        // syntax error
+        return null;
     }
 
-    final void loadRepoInfo(String version, Grgit repo) {
-        this.releaseType = ReleaseType.Snapshot;
-        this.version     = version;
+    parts = mainVersion.split("\\.");
+    if (parts.size() != 3) {
+        // syntax error
+        return null;
+    }
 
-        Commit head = repo.head();
-        this.revision = repo.log(includes:[head]).size();
-        this.hash = head.id;
-        this.shortHash = head.abbreviatedId;
-        this.branch = repo.branch.current.name;
+    String baseVersion = parts[0] + "." + parts[1];
 
-        Tag gitTag = repo.tag.list().find { it.commit == head }
-        if (gitTag != null){
-            this.tag = gitTag.name;
-        } else {
-            this.tag = "";
-        }
+    return [
+        "tag" : tag,
+        "baseVersion" : baseVersion,
+        "mainVersion" : mainVersion,
+        "prerelease" :  prerelease,
+        "releaseName" : releaseName,
+        "releaseSuffix": (prerelease ? "-${releaseName}": "")
+    ]
+}
 
-        if (System.env.TRAVIS_BRANCH != null) {
-            this.branch = System.env.TRAVIS_BRANCH
-        }
-        if (System.env.TRAVIS_TAG != null) {
-            this.tag = System.env.TRAVIS_TAG
-        }
-        if (System.env.TRAVIS_PULL_REQUEST != null && System.env.TRAVIS_PULL_REQUEST != "false") {
-            this.branch += "-pr-" + System.env.TRAVIS_PULL_REQUEST
-        }
+task configureVersionInfo {
+    try {
+        def grgit = Grgit.open(project.file('.'))
+        def head = grgit.head()
+        jmeRevision = grgit.log(includes: [head]).size()
+        jmeGitHash = head.id
+        jmeShortGitHash = head.abbreviatedId
+        jmeBranchName = grgit.branch.current.name
+        jmeGitTag = grgit.tag.list().find { it.commit == head }
         
-        loadTagInfo(this.tag);
-
-        this.fullVersion = version;
-        if (this.branch != "master") {
-            this.fullVersion += "-${branch}";
+        if (jmeGitTag != null) {
+            jmeGitTag = jmeGitTag.name
+        } else {
+            jmeGitTag = System.env.TRAVIS_TAG
         }
 
-        switch (this.releaseType) {
-        case ReleaseType.Snapshot:
-            this.pomVersion  = "${fullVersion}-SNAPSHOT";
-            this.fullVersion += "-${revision}";
-            break;
-        case ReleaseType.PreRelease:
-            this.pomVersion  = "${fullVersion}-${releaseName}";
-            this.fullVersion += "-${releaseName}";
-            break;
-        case ReleaseType.Release:
-            this.pomVersion  = "${fullVersion}";
-            break;
-        }
-    }
-
-    final void loadTagInfo(String tag) {
-        this.tag = tag;
-        
-        if (tag == null || !tag.startsWith("v")) {
-            return;
-        }
-        
-        String[] parts = tag.split("-");
-        if (parts.length == 2) {
-            if (parts[0].size() < 1 || parts[1].size() < 1) {
-                return;
+        def releaseInfo = getReleaseInfo(jmeGitTag)
+        if (releaseInfo != null) {
+            jmeFullVersion = "${releaseInfo.baseVersion}${releaseInfo.releaseSuffix}"
+            jmePomVersion = "${releaseInfo.mainVersion}${releaseInfo.releaseSuffix}"
+            jmeNbmRevision = "0"
+            jmeNbmUcSuffix = "stable/${releaseInfo.baseVersion}/plugins"
+        } else {
+            // SNAPSHOT
+            jmeFullVersion = jmeMainVersion
+            jmePomVersion  = jmeVersion
+            if (System.env.TRAVIS_BRANCH != null) {
+                jmeBranchName = System.env.TRAVIS_BRANCH
             }
-
-            releaseType = ReleaseType.PreRelease;
-            version = parts[0].substring(1);
-            releaseName = parts[1];
-        } else if (parts.length == 1) {
-            if (parts[0].size() < 1) {
-                return;
+            if (System.env.TRAVIS_PULL_REQUEST != null && 
+                System.env.TRAVIS_PULL_REQUEST != "false") {
+                jmeBranchName += "-pr-" + System.env.TRAVIS_PULL_REQUEST
             }
-
-            releaseType = ReleaseType.Release;
-            version = parts[0];
+            if (jmeBranchName != "master") {
+                jmeFullVersion += "-${jmeBranchName}"
+                jmePomVersion  += "-${jmeBranchName}"
+                jmeNbmUcSuffix = "${jmeBranchName}-"
+            } else {
+                jmeNbmUcSuffix = ""
+            }
+            jmeNbmUcSuffix += "nightly/" + jmeMainVersion + "/plugins"
+            jmeFullVersion += "-${jmeRevision}"
+            jmePomVersion  += "-SNAPSHOT"
+            jmeNbmRevision = jmeRevision
         }
-    }
-
-    public String toString() {
-        return "tag = ${tag}, base_ver = ${baseVersion}, main_ver = ${mainVersion}, " +
-               "prerelease = ${prerelease}, release_name = ${releaseName}"
-    }
-}
-
-task configureVersionInfo {
-    try {
-        def repo = Grgit.open(project.file('.'))
-        releaseInfo = new ReleaseInfo(jmeVersion, repo);
-        logger.warn("Full Version: ${releaseInfo.fullVersion}")
-        logger.warn("POM Version: ${releaseInfo.pomVersion}")
+            
+        logger.warn("Full Version: ${jmeFullVersion}")
+        logger.warn("POM Version: ${jmePomVersion}")
+        logger.warn("NBM Revision: ${jmeNbmRevision}")
+        logger.warn("NBM UC Suffix: ${jmeNbmUcSuffix}")
     } catch (ex) {
         // Failed to get repo info
         logger.warn("Failed to get repository info: " + ex.message + ". " + \