فهرست منبع

Merge branch 'master' of https://github.com/jMonkeyEngine/jmonkeyengine.git

Alrik 9 سال پیش
والد
کامیت
0a45432cea
78فایلهای تغییر یافته به همراه3402 افزوده شده و 648 حذف شده
  1. 4 1
      .travis.yml
  2. 29 0
      bintray.gradle
  3. 8 2
      build.gradle
  4. 43 18
      common.gradle
  5. 4 0
      gradle.properties
  6. 6 5
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/BlenderContext.java
  7. 83 19
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/Edge.java
  8. 100 63
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/Face.java
  9. 3 1
      jme3-bullet/src/common/java/com/jme3/bullet/control/BetterCharacterControl.java
  10. 4 2
      jme3-bullet/src/main/java/com/jme3/bullet/collision/shapes/SphereCollisionShape.java
  11. 31 0
      jme3-core/src/main/java/com/jme3/app/Application.java
  12. 1 1
      jme3-core/src/main/java/com/jme3/asset/AssetKey.java
  13. 34 4
      jme3-core/src/main/java/com/jme3/audio/AudioNode.java
  14. 7 4
      jme3-core/src/main/java/com/jme3/collision/bih/BIHTree.java
  15. 11 0
      jme3-core/src/main/java/com/jme3/effect/ParticleEmitter.java
  16. 12 3
      jme3-core/src/main/java/com/jme3/input/KeyInput.java
  17. 24 22
      jme3-core/src/main/java/com/jme3/input/KeyNames.java
  18. 5 0
      jme3-core/src/main/java/com/jme3/light/DefaultLightFilter.java
  19. 7 1
      jme3-core/src/main/java/com/jme3/light/DirectionalLight.java
  20. 18 15
      jme3-core/src/main/java/com/jme3/light/Light.java
  21. 8 1
      jme3-core/src/main/java/com/jme3/light/PointLight.java
  22. 9 1
      jme3-core/src/main/java/com/jme3/light/SpotLight.java
  23. 2 0
      jme3-core/src/main/java/com/jme3/material/Material.java
  24. 11 17
      jme3-core/src/main/java/com/jme3/renderer/RenderManager.java
  25. 0 1
      jme3-core/src/main/java/com/jme3/renderer/Statistics.java
  26. 33 94
      jme3-core/src/main/java/com/jme3/scene/BatchNode.java
  27. 5 0
      jme3-core/src/main/java/com/jme3/scene/Node.java
  28. 1 1
      jme3-core/src/main/java/com/jme3/system/AppSettings.java
  29. 19 0
      jme3-core/src/main/java/com/jme3/util/BufferUtils.java
  30. 97 0
      jme3-core/src/main/java/com/jme3/util/mikktspace/MikkTSpaceContext.java
  31. 100 0
      jme3-core/src/main/java/com/jme3/util/mikktspace/MikkTSpaceImpl.java
  32. 1717 0
      jme3-core/src/main/java/com/jme3/util/mikktspace/MikktspaceTangentGenerator.java
  33. 4 3
      jme3-core/src/main/resources/Common/MatDefs/Misc/Particle.vert
  34. 19 0
      jme3-core/src/main/resources/joystick-mapping.properties
  35. 1 1
      jme3-core/src/tools/java/jme3tools/optimize/TextureAtlas.java
  36. 1 1
      jme3-desktop/src/main/java/com/jme3/app/SettingsDialog.java
  37. 72 0
      jme3-examples/src/main/java/jme3test/app/TestEnqueueRunnable.java
  38. 22 21
      jme3-examples/src/main/java/jme3test/light/TestManyLightsSingle.java
  39. 1 1
      jme3-jbullet/src/main/java/com/jme3/bullet/objects/PhysicsVehicle.java
  40. 19 18
      jme3-jogl/src/main/java/com/jme3/system/jogl/JoglContext.java
  41. 53 52
      jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglContext.java
  42. 7 7
      jme3-lwjgl3/build.gradle
  43. BIN
      jme3-lwjgl3/lib/lwjgl-3.0.0b-35-natives.jar
  44. BIN
      jme3-lwjgl3/lib/lwjgl-3.0.0b-35.jar
  45. 22 18
      jme3-lwjgl3/src/main/java/com/jme3/audio/lwjgl/LwjglALC.java
  46. 2 1
      jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwKeyInput.java
  47. 171 0
      jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwKeyMap.java
  48. 91 47
      jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwMouseInput.java
  49. 16 12
      jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java
  50. 84 45
      jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java
  51. 31 22
      jme3-networking/src/main/java/com/jme3/network/base/DefaultClient.java
  52. 1 1
      jme3-networking/src/main/java/com/jme3/network/base/DefaultServer.java
  53. 7 3
      jme3-networking/src/main/java/com/jme3/network/base/KernelAdapter.java
  54. 1 1
      jme3-networking/src/main/java/com/jme3/network/base/MessageProtocol.java
  55. 12 0
      jme3-networking/src/main/java/com/jme3/network/kernel/AbstractKernel.java
  56. 4 9
      jme3-networking/src/main/java/com/jme3/network/kernel/tcp/SelectorKernel.java
  57. 5 10
      jme3-networking/src/main/java/com/jme3/network/kernel/udp/UdpKernel.java
  58. 5 3
      jme3-networking/src/main/java/com/jme3/network/message/SerializerRegistrationsMessage.java
  59. 18 2
      jme3-networking/src/main/java/com/jme3/network/serializing/Serializer.java
  60. 3 2
      jme3-networking/src/main/java/com/jme3/network/serializing/serializers/EnumSerializer.java
  61. 34 5
      jme3-networking/src/main/java/com/jme3/network/serializing/serializers/FieldSerializer.java
  62. 5 0
      jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiRegistry.java
  63. 6 1
      jme3-networking/src/main/java/com/jme3/network/service/rpc/RpcConnection.java
  64. 32 6
      jme3-networking/src/main/java/com/jme3/network/service/rpc/msg/RpcResponseMessage.java
  65. 1 1
      jme3-networking/src/main/java/com/jme3/network/service/serializer/ServerSerializerRegistrationsService.java
  66. 1 0
      sdk/build.gradle
  67. 2 2
      sdk/jme3-assetpack-support/src/com/jme3/gde/assetpack/browser/Bundle.properties
  68. 2 2
      sdk/jme3-core/src/com/jme3/gde/core/appstates/AppStateExplorerTopComponent.java
  69. 2 2
      sdk/jme3-core/src/com/jme3/gde/core/filters/Bundle.properties
  70. 2 2
      sdk/jme3-core/src/com/jme3/gde/core/sceneexplorer/Bundle.properties
  71. 1 1
      sdk/jme3-materialeditor/src/com/jme3/gde/materialdefinition/EditableMatDefFile.java
  72. 1 0
      sdk/jme3-materialeditor/src/com/jme3/gde/materialdefinition/MatDefDataObject.java
  73. 15 1
      sdk/jme3-materialeditor/src/com/jme3/gde/materialdefinition/editor/MatDefEditorToolBar.java
  74. 24 3
      sdk/jme3-materialeditor/src/com/jme3/gde/materialdefinition/fileStructure/TechniqueBlock.java
  75. 3 2
      sdk/jme3-materialeditor/src/com/jme3/gde/materials/MaterialPreviewRenderer.java
  76. 65 20
      sdk/jme3-terrain-editor/src/com/jme3/gde/terraineditor/sky/SkyboxWizardPanel2.java
  77. 1 1
      sdk/jme3-welcome-screen/src/com/jme3/gde/welcome/Bundle.properties
  78. 97 43
      version.gradle

+ 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
 //    }
-//}
+//}

+ 43 - 18
common.gradle

@@ -5,7 +5,7 @@
 apply plugin: 'java'
 apply plugin: 'maven'
 
-group = 'com.jme3'
+group = 'org.jmonkeyengine'
 version = jmePomVersion
 
 sourceCompatibility = '1.6'
@@ -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

@@ -37,3 +37,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=

+ 6 - 5
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/BlenderContext.java

@@ -59,6 +59,7 @@ import com.jme3.scene.plugins.blender.file.FileBlockHeader;
 import com.jme3.scene.plugins.blender.file.FileBlockHeader.BlockCode;
 import com.jme3.scene.plugins.blender.file.Structure;
 import com.jme3.scene.plugins.blender.materials.MaterialContext;
+import com.jme3.scene.plugins.blender.meshes.TemporalMesh;
 import com.jme3.texture.Texture;
 
 /**
@@ -389,11 +390,11 @@ public class BlenderContext {
                     }
                 }
             } else if("ME".equals(namePrefix)) {
-                List<Node> features = (List<Node>) linkedFeatures.get("meshes");
-                if(features != null) {
-                    for(Node feature : features) {
-                        if(featureName.equals(feature.getName())) {
-                            return feature;
+                List<TemporalMesh> temporalMeshes = (List<TemporalMesh>) linkedFeatures.get("meshes");
+                if(temporalMeshes != null) {
+                    for(TemporalMesh temporalMesh : temporalMeshes) {
+                        if(featureName.equals(temporalMesh.getName())) {
+                            return temporalMesh;
                         }
                     }
                 }

+ 83 - 19
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/Edge.java

@@ -10,6 +10,7 @@ import com.jme3.math.Vector3f;
 import com.jme3.scene.plugins.blender.file.BlenderFileException;
 import com.jme3.scene.plugins.blender.file.Pointer;
 import com.jme3.scene.plugins.blender.file.Structure;
+import com.jme3.scene.plugins.blender.math.Vector3d;
 import com.jme3.scene.plugins.blender.meshes.IndexesLoop.IndexPredicate;
 
 /**
@@ -24,6 +25,8 @@ public class Edge {
 
     /** The vertices indexes. */
     private int                 index1, index2;
+    /** The vertices that can be set if we need and abstract edge outside the mesh (for computations). */
+    private Vector3f 			v1, v2;
     /** The weight of the edge. */
     private float               crease;
     /** A variable that indicates if this edge belongs to any face or not. */
@@ -31,6 +34,13 @@ public class Edge {
     /** The mesh that owns the edge. */
     private TemporalMesh        temporalMesh;
 
+    public Edge(Vector3f v1, Vector3f v2) {
+		this.v1 = v1 == null ? new Vector3f() : v1;
+		this.v2 = v2 == null ? new Vector3f() : v2;
+		index1 = 0;
+		index2 = 1;
+	}
+    
     /**
      * This constructor only stores the indexes of the vertices. The position vertices should be stored
      * outside this class.
@@ -74,14 +84,14 @@ public class Edge {
      * @return the first vertex of the edge
      */
     public Vector3f getFirstVertex() {
-        return temporalMesh.getVertices().get(index1);
+        return temporalMesh == null ? v1 : temporalMesh.getVertices().get(index1);
     }
 
     /**
      * @return the second vertex of the edge
      */
     public Vector3f getSecondVertex() {
-        return temporalMesh.getVertices().get(index2);
+        return temporalMesh == null ? v2 : temporalMesh.getVertices().get(index2);
     }
 
     /**
@@ -188,28 +198,82 @@ public class Edge {
      * @return <b>true</b> if the edges cross and false otherwise
      */
     public boolean cross(Edge edge) {
-        Vector3f P1 = this.getFirstVertex();
-        Vector3f P2 = edge.getFirstVertex();
-        Vector3f u = this.getSecondVertex().subtract(P1);
-        Vector3f v = edge.getSecondVertex().subtract(P2);
-        float t2 = (u.x * (P2.y - P1.y) - u.y * (P2.x - P1.x)) / (u.y * v.x - u.x * v.y);
-        float t1 = (P2.x - P1.x + v.x * t2) / u.x;
-        Vector3f p1 = P1.add(u.mult(t1));
-        Vector3f p2 = P2.add(v.mult(t2));
+        return this.getCrossPoint(edge) != null;
+    }
+    
+    /**
+	 * The method computes the crossing pint of this edge and another edge. If
+	 * there is no crossing then null is returned.
+	 * 
+	 * @param edge
+	 *            the edge to compute corss point with
+	 * @return cross point on null if none exist
+	 */
+	public Vector3f getCrossPoint(Edge edge) {
+		return this.getCrossPoint(edge, false, false);
+	}
+    
+	/**
+	 * The method computes the crossing pint of this edge and another edge. If
+	 * there is no crossing then null is returned. This method also allows to
+	 * get the crossing point of the straight lines that contain these edges if
+	 * you set the 'extend' parameter to true.
+	 * 
+	 * @param edge
+	 *            the edge to compute corss point with
+	 * @param extendThisEdge
+	 *            set to <b>true</b> to find a crossing point along the whole
+	 *            straight that contains the current edge
+	 * @param extendSecondEdge
+	 *            set to <b>true</b> to find a crossing point along the whole
+	 *            straight that contains the given edge
+	 * @return cross point on null if none exist
+	 */
+	public Vector3f getCrossPoint(Edge edge, boolean extendThisEdge, boolean extendSecondEdge) {
+		Vector3d P1 = new Vector3d(this.getFirstVertex());
+		Vector3d P2 = new Vector3d(edge.getFirstVertex());
+		Vector3d u = new Vector3d(this.getSecondVertex()).subtract(P1).normalizeLocal();
+		Vector3d v = new Vector3d(edge.getSecondVertex()).subtract(P2).normalizeLocal();
+		
+		double t1 = 0, t2 = 0;
+		if(u.x == 0 && v.x == 0) {
+			t2 = (u.z * (P2.y - P1.y) - u.y * (P2.z - P1.z)) / (u.y * v.z - u.z * v.y);
+	        t1 = (P2.z - P1.z + v.z * t2) / u.z;
+		} else if(u.y == 0 && v.y == 0) {
+			t2 = (u.x * (P2.z - P1.z) - u.z * (P2.x - P1.x)) / (u.z * v.x - u.x * v.z);
+	        t1 = (P2.x - P1.x + v.x * t2) / u.x;
+		} else if(u.z == 0 && v.z == 0) {
+			t2 = (u.x * (P2.y - P1.y) - u.y * (P2.x - P1.x)) / (u.y * v.x - u.x * v.y);
+	        t1 = (P2.x - P1.x + v.x * t2) / u.x;
+		} else {
+			t2 = (P1.y * u.x - P1.x * u.y + P2.x * u.y - P2.y * u.x) / (v.y * u.x - u.y * v.x);
+			t1 = (P2.x - P1.x + v.x * t2) / u.x;
+			if(Math.abs(P1.z - P2.z + u.z * t1 - v.z * t2) > FastMath.FLT_EPSILON) {
+				return null;
+			}
+		}
+		Vector3d p1 = P1.add(u.mult(t1));
+        Vector3d p2 = P2.add(v.mult(t2));
 
-        if (p1.distance(p2) <= FastMath.FLT_EPSILON) {
-            // the lines cross, check if p1 and p2 are within the edges
-            Vector3f p = p1.subtract(P1);
-            float cos = p.dot(u) / (p.length() * u.length());
-            if (cos > 0 && p.length() <= u.length()) {
+		if (p1.distance(p2) <= FastMath.FLT_EPSILON) {
+			if(extendThisEdge && extendSecondEdge) {
+				return p1.toVector3f();
+			}
+			// the lines cross, check if p1 and p2 are within the edges
+            Vector3d p = p1.subtract(P1);
+            double cos = p.dot(u) / p.length();
+            if (extendThisEdge || p.length()<= FastMath.FLT_EPSILON || cos >= 1 - FastMath.FLT_EPSILON && p.length() <= this.getLength()) {
                 // p1 is inside the first edge, lets check the other edge now
                 p = p2.subtract(P2);
-                cos = p.dot(v) / (p.length() * v.length());
-                return cos > 0 && p.length() <= u.length();
+                cos = p.dot(v) / p.length();
+                if(extendSecondEdge || p.length()<= FastMath.FLT_EPSILON || cos >= 1 - FastMath.FLT_EPSILON && p.length() <= edge.getLength()) {
+                	return p1.toVector3f();
+                }
             }
         }
-        return false;
-    }
+		
+		return null;
+	}
 
     @Override
     public String toString() {

+ 100 - 63
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/Face.java

@@ -276,30 +276,45 @@ public class Face implements Comparator<Integer> {
             List<Face> facesToTriangulate = new ArrayList<Face>(Arrays.asList(this.clone()));
             while (facesToTriangulate.size() > 0) {
                 Face face = facesToTriangulate.remove(0);
-                int previousIndex1 = -1, previousIndex2 = -1, previousIndex3 = -1;
-                while (face.vertexCount() > 0) {
-                    indexes[0] = face.getIndex(0);
-                    indexes[1] = face.findClosestVertex(indexes[0], -1);
-                    indexes[2] = face.findClosestVertex(indexes[0], indexes[1]);
-
-                    LOGGER.finer("Veryfying improper triangulation of the temporal mesh.");
-                    if (indexes[0] < 0 || indexes[1] < 0 || indexes[2] < 0) {
-                        throw new BlenderFileException("Unable to find two closest vertices while triangulating face in mesh: " + temporalMesh + "Please apply triangulation modifier in blender as a workaround and load again!");
-                    }
-                    if (previousIndex1 == indexes[0] && previousIndex2 == indexes[1] && previousIndex3 == indexes[2]) {
-                        throw new BlenderFileException("Infinite loop detected during triangulation of mesh: " + temporalMesh + "Please apply triangulation modifier in blender as a workaround and load again!");
-                    }
-                    previousIndex1 = indexes[0];
-                    previousIndex2 = indexes[1];
-                    previousIndex3 = indexes[2];
+                // two special cases will improve the computations speed
+                if(face.getIndexes().size() == 3) {
+                	triangulatedFaces.add(face.getIndexes().clone());
+                } else if(face.getIndexes().size() == 4) {
+                	// in case face has 4 verts we use the plain triangulation
+                	indexes[0] = face.getIndex(0);
+                    indexes[1] = face.getIndex(1);
+                    indexes[2] = face.getIndex(2);
+                	triangulatedFaces.add(new IndexesLoop(indexes));
+                	
+                    indexes[1] = face.getIndex(2);
+                    indexes[2] = face.getIndex(3);
+                	triangulatedFaces.add(new IndexesLoop(indexes));
+                } else {
+                	int previousIndex1 = -1, previousIndex2 = -1, previousIndex3 = -1;
+                    while (face.vertexCount() > 0) {
+                        indexes[0] = face.getIndex(0);
+                        indexes[1] = face.findClosestVertex(indexes[0], -1);
+                        indexes[2] = face.findClosestVertex(indexes[0], indexes[1]);
+
+                        LOGGER.finer("Veryfying improper triangulation of the temporal mesh.");
+                        if (indexes[0] < 0 || indexes[1] < 0 || indexes[2] < 0) {
+                            throw new BlenderFileException("Unable to find two closest vertices while triangulating face in mesh: " + temporalMesh + "Please apply triangulation modifier in blender as a workaround and load again!");
+                        }
+                        if (previousIndex1 == indexes[0] && previousIndex2 == indexes[1] && previousIndex3 == indexes[2]) {
+                            throw new BlenderFileException("Infinite loop detected during triangulation of mesh: " + temporalMesh + "Please apply triangulation modifier in blender as a workaround and load again!");
+                        }
+                        previousIndex1 = indexes[0];
+                        previousIndex2 = indexes[1];
+                        previousIndex3 = indexes[2];
 
-                    Arrays.sort(indexes, this);
-                    facesToTriangulate.addAll(face.detachTriangle(indexes));
-                    triangulatedFaces.add(new IndexesLoop(indexes));
+                        Arrays.sort(indexes, this);
+                        facesToTriangulate.addAll(face.detachTriangle(indexes));
+                        triangulatedFaces.add(new IndexesLoop(indexes));
+                    }
                 }
             }
         } catch (BlenderFileException e) {
-            LOGGER.log(Level.WARNING, "Errors occured during face triangulation: {0}. The face will be triangulated with the most direct algorithm, " + "but the results might not be identical to blender.", e.getLocalizedMessage());
+            LOGGER.log(Level.WARNING, "Errors occured during face triangulation: {0}. The face will be triangulated with the most direct algorithm, but the results might not be identical to blender.", e.getLocalizedMessage());
             indexes[0] = this.getIndex(0);
             for (int i = 1; i < this.vertexCount() - 1; ++i) {
                 indexes[1] = this.getIndex(i);
@@ -308,7 +323,7 @@ public class Face implements Comparator<Integer> {
             }
         }
     }
-
+    
     /**
      * @return <b>true</b> if the face is smooth and <b>false</b> otherwise
      */
@@ -335,17 +350,23 @@ public class Face implements Comparator<Integer> {
         return "Face " + indexes;
     }
 
-    /**
-     * The method finds the closest vertex to the one specified by <b>index</b>.
-     * If the vertexToIgnore is positive than it will be ignored in the result.
-     * The closes vertex must be able to create an edge that is fully contained within the face and does not cross
-     * any other edges.
-     * @param index
-     *            the index of the vertex that needs to have found the nearest neighbour
-     * @param indexToIgnore
-     *            the index to ignore in the result (pass -1 if none is to be ignored)
-     * @return the index of the closest vertex to the given one
-     */
+	/**
+	 * The method finds the closest vertex to the one specified by <b>index</b>.
+	 * If the vertexToIgnore is positive than it will be ignored in the result.
+	 * The closest vertex must be able to create an edge that is fully contained
+	 * within the face and does not cross any other edges. Also if the
+	 * vertexToIgnore is not negative then the condition that the edge between
+	 * the found index and the one to ignore is inside the face must also be
+	 * met.
+	 * 
+	 * @param index
+	 *            the index of the vertex that needs to have found the nearest
+	 *            neighbour
+	 * @param indexToIgnore
+	 *            the index to ignore in the result (pass -1 if none is to be
+	 *            ignored)
+	 * @return the index of the closest vertex to the given one
+	 */
     private int findClosestVertex(int index, int indexToIgnore) {
         int result = -1;
         List<Vector3f> vertices = temporalMesh.getVertices();
@@ -355,7 +376,7 @@ public class Face implements Comparator<Integer> {
             if (i != index && i != indexToIgnore) {
                 Vector3f v2 = vertices.get(i);
                 float d = v2.distance(v1);
-                if (d < distance && this.contains(new Edge(index, i, 0, true, temporalMesh))) {
+                if (d < distance && this.contains(new Edge(index, i, 0, true, temporalMesh)) && (indexToIgnore < 0 || this.contains(new Edge(indexToIgnore, i, 0, true, temporalMesh)))) {
                     result = i;
                     distance = d;
                 }
@@ -376,11 +397,9 @@ public class Face implements Comparator<Integer> {
         int index2 = edge.getSecondIndex();
         // check if the line between the vertices is not a border edge of the face
         if (!indexes.areNeighbours(index1, index2)) {
-            List<Vector3f> vertices = temporalMesh.getVertices();
-
             for (int i = 0; i < indexes.size(); ++i) {
-                int i1 = this.getIndex(i);
-                int i2 = this.getIndex(i + 1);
+                int i1 = this.getIndex(i - 1);
+                int i2 = this.getIndex(i);
                 // check if the edges have no common verts (because if they do, they cannot cross)
                 if (i1 != index1 && i1 != index2 && i2 != index1 && i2 != index2) {
                     if (edge.cross(new Edge(i1, i2, 0, false, temporalMesh))) {
@@ -389,35 +408,53 @@ public class Face implements Comparator<Integer> {
                 }
             }
 
-            // the edge does NOT cross any of other edges, so now we need to verify if it is inside the face or outside
-            // we check it by comparing the angle that is created by vertices: [index1 - 1, index1, index1 + 1]
-            // with the one creaded by vertices: [index1 - 1, index1, index2]
-            // if the latter is greater than it means that the edge is outside the face
-            // IMPORTANT: we assume that all vertices are in one plane (this should be ensured before creating the Face)
-            int indexOfIndex1 = indexes.indexOf(index1);
-            int indexMinus1 = this.getIndex(indexOfIndex1 - 1);// indexOfIndex1 == 0 ? indexes.get(indexes.size() - 1) : indexes.get(indexOfIndex1 - 1);
-            int indexPlus1 = this.getIndex(indexOfIndex1 + 1);// indexOfIndex1 == indexes.size() - 1 ? 0 : indexes.get(indexOfIndex1 + 1);
-
-            Vector3f edge1 = vertices.get(indexMinus1).subtract(vertices.get(index1)).normalizeLocal();
-            Vector3f edge2 = vertices.get(indexPlus1).subtract(vertices.get(index1)).normalizeLocal();
-            Vector3f newEdge = vertices.get(index2).subtract(vertices.get(index1)).normalizeLocal();
-
-            // verify f the later computed angle is inside or outside the face
-            Vector3f direction1 = edge1.cross(edge2).normalizeLocal();
-            Vector3f direction2 = edge1.cross(newEdge).normalizeLocal();
-            Vector3f normal = temporalMesh.getNormals().get(index1);
-
-            boolean isAngle1Interior = normal.dot(direction1) < 0;
-            boolean isAngle2Interior = normal.dot(direction2) < 0;
-
-            float angle1 = isAngle1Interior ? edge1.angleBetween(edge2) : FastMath.TWO_PI - edge1.angleBetween(edge2);
-            float angle2 = isAngle2Interior ? edge1.angleBetween(newEdge) : FastMath.TWO_PI - edge1.angleBetween(newEdge);
-
-            return angle1 >= angle2;
+            // computing the edge's middle point
+            Vector3f edgeMiddlePoint = edge.computeCentroid();
+            // computing the edge that is perpendicular to the given edge and has a length of 1 (length actually does not matter)
+            Vector3f edgeVector = edge.getSecondVertex().subtract(edge.getFirstVertex());
+            Vector3f edgeNormal = temporalMesh.getNormals().get(index1).cross(edgeVector).normalizeLocal();
+            Edge e = new Edge(edgeMiddlePoint, edgeNormal.add(edgeMiddlePoint));
+            // compute the vectors from the middle point to the crossing between the extended edge 'e' and other edges of the face
+            List<Vector3f> crossingVectors = new ArrayList<Vector3f>();
+            for (int i = 0; i < indexes.size(); ++i) {
+                int i1 = this.getIndex(i);
+                int i2 = this.getIndex(i + 1);
+            	Vector3f crossPoint = e.getCrossPoint(new Edge(i1, i2, 0, false, temporalMesh), true, false);
+                if(crossPoint != null) {
+                	crossingVectors.add(crossPoint.subtractLocal(edgeMiddlePoint));
+                }
+            }
+            if(crossingVectors.size() == 0) {
+            	return false;// edges do not cross
+            }
+            
+            // use only distinct vertices (doubles may appear if the crossing point is a vertex)
+            List<Vector3f> distinctCrossingVectors = new ArrayList<Vector3f>();
+            for(Vector3f cv : crossingVectors) {
+        		double minDistance = Double.MAX_VALUE;
+        		for(Vector3f dcv : distinctCrossingVectors) {
+        			minDistance = Math.min(minDistance, dcv.distance(cv));
+        		}
+        		if(minDistance > FastMath.FLT_EPSILON) {
+        			distinctCrossingVectors.add(cv);
+        		}
+            }
+            
+            if(distinctCrossingVectors.size() == 0) {
+            	throw new IllegalStateException("There MUST be at least 2 crossing vertices!");
+            }
+            // checking if all crossing vectors point to the same direction (if yes then the edge is outside the face)
+            float direction = Math.signum(distinctCrossingVectors.get(0).dot(edgeNormal));// if at least one vector has different direction that this - it means that the edge is inside the face
+            for(int i=1;i<distinctCrossingVectors.size();++i) {
+            	if(direction != Math.signum(distinctCrossingVectors.get(i).dot(edgeNormal))) {
+            		return true;
+            	}
+            }
+            return false;
         }
         return true;
     }
-
+    
     @Override
     public int hashCode() {
         final int prime = 31;

+ 3 - 1
jme3-bullet/src/common/java/com/jme3/bullet/control/BetterCharacterControl.java

@@ -171,6 +171,8 @@ public class BetterCharacterControl extends AbstractPhysicsControl implements Ph
         }
         TempVars vars = TempVars.get();
 
+        Vector3f currentVelocity = vars.vect2.set(velocity);
+        
         // dampen existing x/z forces
         float existingLeftVelocity = velocity.dot(localLeft);
         float existingForwardVelocity = velocity.dot(localForward);
@@ -194,7 +196,7 @@ public class BetterCharacterControl extends AbstractPhysicsControl implements Ph
             //add resulting vector to existing velocity
             velocity.addLocal(localWalkDirection);
         }
-        rigidBody.setLinearVelocity(velocity);
+        if(currentVelocity.distance(velocity) > FastMath.ZERO_TOLERANCE) rigidBody.setLinearVelocity(velocity);
         if (jump) {
             //TODO: precalculate jump force
             Vector3f rotatedJumpForce = vars.vect1;

+ 4 - 2
jme3-bullet/src/main/java/com/jme3/bullet/collision/shapes/SphereCollisionShape.java

@@ -82,7 +82,9 @@ public class SphereCollisionShape extends CollisionShape {
      */
     @Override
     public void setScale(Vector3f scale) {
-        Logger.getLogger(this.getClass().getName()).log(Level.WARNING, "SphereCollisionShape cannot be scaled");
+        if (!scale.equals(Vector3f.UNIT_XYZ)) {
+            Logger.getLogger(this.getClass().getName()).log(Level.WARNING, "SphereCollisionShape cannot be scaled");
+        }
     }
 
     protected void createShape() {
@@ -91,7 +93,7 @@ public class SphereCollisionShape extends CollisionShape {
 //        new SphereShape(radius);
 //        objectId.setLocalScaling(Converter.convert(getScale()));
 //        objectId.setMargin(margin);
-        setScale(scale);
+        setScale(scale); // Set the scale to 1
         setMargin(margin);
     }
     

+ 31 - 0
jme3-core/src/main/java/com/jme3/app/Application.java

@@ -650,12 +650,28 @@ public class Application implements SystemListener {
      * Callables are executed right at the beginning of the main loop.
      * They are executed even if the application is currently paused
      * or out of focus.
+     * 
+     * @param callable The callable to run in the main jME3 thread
      */
     public <V> Future<V> enqueue(Callable<V> callable) {
         AppTask<V> task = new AppTask<V>(callable);
         taskQueue.add(task);
         return task;
     }
+    
+    /**
+     * Enqueues a runnable object to execute in the jME3
+     * rendering thread.
+     * <p>
+     * Runnables are executed right at the beginning of the main loop.
+     * They are executed even if the application is currently paused
+     * or out of focus.
+     * 
+     * @param runnable The runnable to run in the main jME3 thread
+     */    
+    public void enqueue(Runnable runnable){
+        enqueue(new RunnableWrapper(runnable));
+    }
 
     /**
      * Runs tasks enqueued via {@link #enqueue(Callable)}
@@ -740,4 +756,19 @@ public class Application implements SystemListener {
         return viewPort;
     }
 
+    private class RunnableWrapper implements Callable{
+        private final Runnable runnable;
+
+        public RunnableWrapper(Runnable runnable){
+            this.runnable = runnable;
+        }
+
+        @Override
+        public Object call(){
+            runnable.run();
+            return null;
+        }
+        
+    }
+    
 }

+ 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.

+ 7 - 4
jme3-core/src/main/java/com/jme3/collision/bih/BIHTree.java

@@ -109,8 +109,11 @@ public class BIHTree implements CollisionData {
         this.mesh = mesh;
         this.maxTrisPerNode = maxTrisPerNode;
 
-        if (maxTrisPerNode < 1 || mesh == null) {
-            throw new IllegalArgumentException();
+        if (maxTrisPerNode < 1) {
+            throw new IllegalArgumentException("maxTrisPerNode cannot be less than 1");
+        }
+        if (mesh == null) {
+            throw new IllegalArgumentException("Mesh cannot be null");
         }
 
         bihSwapTmp = new float[9];
@@ -451,7 +454,7 @@ public class BIHTree implements CollisionData {
         } else if (bv instanceof BoundingBox) {
             bbox = new BoundingBox((BoundingBox) bv);
         } else {
-            throw new UnsupportedCollisionException();
+            throw new UnsupportedCollisionException("BoundingVolume:" + bv);
         }
 
         bbox.transform(worldMatrix.invert(), bbox);
@@ -470,7 +473,7 @@ public class BIHTree implements CollisionData {
             BoundingVolume bv = (BoundingVolume) other;
             return collideWithBoundingVolume(bv, worldMatrix, results);
         } else {
-            throw new UnsupportedCollisionException();
+            throw new UnsupportedCollisionException("Collidable:" + other);
         }
     }
 

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

+ 12 - 3
jme3-core/src/main/java/com/jme3/input/KeyInput.java

@@ -36,6 +36,11 @@ package com.jme3.input;
  */
 public interface KeyInput extends Input {
 
+    /**
+     * unmapped key.
+     */
+    public static final int KEY_UNKNOWN = 0x00;
+
     /**
      * escape key.
      */
@@ -518,17 +523,17 @@ public interface KeyInput extends Input {
      * delete key.
      */
     public static final int KEY_DELETE = 0xD3;
-    
+
     /**
      * Left "Windows" key on PC keyboards, left "Option" key on Mac keyboards.
      */
     public static final int KEY_LMETA  = 0xDB;
-    
+
     /**
      * Right "Windows" key on PC keyboards, right "Option" key on Mac keyboards.
      */
     public static final int KEY_RMETA = 0xDC;
-    
+
     public static final int KEY_APPS = 0xDD;
     /**
      * power key.
@@ -539,4 +544,8 @@ public interface KeyInput extends Input {
      */
     public static final int KEY_SLEEP = 0xDF;
 
+    /**
+     * the last key.
+     */
+    public static final int KEY_LAST = 0xE0;
 }

+ 24 - 22
jme3-core/src/main/java/com/jme3/input/KeyNames.java

@@ -34,10 +34,11 @@ package com.jme3.input;
 import static com.jme3.input.KeyInput.*;
 
 public class KeyNames {
-    
+
     private static final String[] KEY_NAMES = new String[0xFF];
-    
+
     static {
+        KEY_NAMES[KEY_UNKNOWN] = "Unknown";
         KEY_NAMES[KEY_0] = "0";
         KEY_NAMES[KEY_1] = "1";
         KEY_NAMES[KEY_2] = "2";
@@ -48,7 +49,7 @@ public class KeyNames {
         KEY_NAMES[KEY_7] = "7";
         KEY_NAMES[KEY_8] = "8";
         KEY_NAMES[KEY_9] = "9";
-        
+
         KEY_NAMES[KEY_Q] = "Q";
         KEY_NAMES[KEY_W] = "W";
         KEY_NAMES[KEY_E] = "E";
@@ -75,7 +76,7 @@ public class KeyNames {
         KEY_NAMES[KEY_B] = "B";
         KEY_NAMES[KEY_N] = "N";
         KEY_NAMES[KEY_M] = "M";
-        
+
         KEY_NAMES[KEY_F1] = "F1";
         KEY_NAMES[KEY_F2] = "F2";
         KEY_NAMES[KEY_F3] = "F3";
@@ -91,7 +92,7 @@ public class KeyNames {
         KEY_NAMES[KEY_F13] = "F13";
         KEY_NAMES[KEY_F14] = "F14";
         KEY_NAMES[KEY_F15] = "F15";
-        
+
         KEY_NAMES[KEY_NUMPAD0] = "Numpad 0";
         KEY_NAMES[KEY_NUMPAD1] = "Numpad 1";
         KEY_NAMES[KEY_NUMPAD2] = "Numpad 2";
@@ -102,25 +103,26 @@ public class KeyNames {
         KEY_NAMES[KEY_NUMPAD7] = "Numpad 7";
         KEY_NAMES[KEY_NUMPAD8] = "Numpad 8";
         KEY_NAMES[KEY_NUMPAD9] = "Numpad 9";
-        
+
         KEY_NAMES[KEY_NUMPADEQUALS] = "Numpad =";
         KEY_NAMES[KEY_NUMPADENTER] = "Numpad Enter";
-        KEY_NAMES[KEY_NUMPADCOMMA] = "Numpad .";
+        KEY_NAMES[KEY_NUMPADCOMMA] = "Numpad ,";
         KEY_NAMES[KEY_DIVIDE] = "Numpad /";
-        
-        
+        KEY_NAMES[KEY_SUBTRACT] = "Numpad -";
+        KEY_NAMES[KEY_DECIMAL] = "Numpad .";
+
         KEY_NAMES[KEY_LMENU] = "Left Alt";
         KEY_NAMES[KEY_RMENU] = "Right Alt";
-        
+
         KEY_NAMES[KEY_LCONTROL] = "Left Ctrl";
         KEY_NAMES[KEY_RCONTROL] = "Right Ctrl";
-        
+
         KEY_NAMES[KEY_LSHIFT] = "Left Shift";
         KEY_NAMES[KEY_RSHIFT] = "Right Shift";
-        
+
         KEY_NAMES[KEY_LMETA] = "Left Option";
         KEY_NAMES[KEY_RMETA] = "Right Option";
-        
+
         KEY_NAMES[KEY_MINUS] = "-";
         KEY_NAMES[KEY_EQUALS] = "=";
         KEY_NAMES[KEY_LBRACKET] = "[";
@@ -137,37 +139,37 @@ public class KeyNames {
         KEY_NAMES[KEY_COLON] = ":";
         KEY_NAMES[KEY_UNDERLINE] = "_";
         KEY_NAMES[KEY_AT] = "@";
-        
+
         KEY_NAMES[KEY_APPS] = "Apps";
         KEY_NAMES[KEY_POWER] = "Power";
         KEY_NAMES[KEY_SLEEP] = "Sleep";
-        
+
         KEY_NAMES[KEY_STOP] = "Stop";
         KEY_NAMES[KEY_ESCAPE] = "Esc";
         KEY_NAMES[KEY_RETURN] = "Enter";
         KEY_NAMES[KEY_SPACE] = "Space";
         KEY_NAMES[KEY_BACK] = "Backspace";
         KEY_NAMES[KEY_TAB] = "Tab";
-        
+
         KEY_NAMES[KEY_SYSRQ] = "SysRq";
         KEY_NAMES[KEY_PAUSE] = "Pause";
-        
+
         KEY_NAMES[KEY_HOME] = "Home";
         KEY_NAMES[KEY_PGUP] = "Page Up";
         KEY_NAMES[KEY_PGDN] = "Page Down";
         KEY_NAMES[KEY_END] = "End";
         KEY_NAMES[KEY_INSERT] = "Insert";
         KEY_NAMES[KEY_DELETE] = "Delete";
-        
+
         KEY_NAMES[KEY_UP] = "Up";
         KEY_NAMES[KEY_LEFT] = "Left";
         KEY_NAMES[KEY_RIGHT] = "Right";
         KEY_NAMES[KEY_DOWN] = "Down";
-        
+
         KEY_NAMES[KEY_NUMLOCK] = "Num Lock";
         KEY_NAMES[KEY_CAPITAL] = "Caps Lock";
         KEY_NAMES[KEY_SCROLL] = "Scroll Lock";
-        
+
         KEY_NAMES[KEY_KANA] = "Kana";
         KEY_NAMES[KEY_CONVERT] = "Convert";
         KEY_NAMES[KEY_NOCONVERT] = "No Convert";
@@ -177,8 +179,8 @@ public class KeyNames {
         KEY_NAMES[KEY_AX] = "Ax";
         KEY_NAMES[KEY_UNLABELED] = "Unlabeled";
     }
-    
-    public String getName(int keyId){
+
+    public static String getName(int keyId) {
         return KEY_NAMES[keyId];
     }
 }

+ 5 - 0
jme3-core/src/main/java/com/jme3/light/DefaultLightFilter.java

@@ -60,6 +60,11 @@ public final class DefaultLightFilter implements LightFilter {
             for (int i = 0; i < worldLights.size(); i++) {
                 Light light = worldLights.get(i);
 
+                // If this light is not enabled it will be ignored.
+                if (!light.isEnabled()) {
+                    continue;
+                }
+
                 if (light.frustumCheckNeeded) {
                     processedLights.add(light);
                     light.frustumCheckNeeded = false;

+ 7 - 1
jme3-core/src/main/java/com/jme3/light/DirectionalLight.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012, 2015 jMonkeyEngine
+ * Copyright (c) 2009-2012, 2015-2016 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -146,4 +146,10 @@ public class DirectionalLight extends Light {
         direction = (Vector3f) ic.readSavable("direction", null);
     }
 
+    @Override
+    public DirectionalLight clone() {
+        DirectionalLight l = (DirectionalLight)super.clone();
+        l.direction = direction.clone();
+        return l;
+    }
 }

+ 18 - 15
jme3-core/src/main/java/com/jme3/light/Light.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012, 2015 jMonkeyEngine
+ * Copyright (c) 2009-2012, 2015-2016 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -103,9 +103,6 @@ public abstract class Light implements Savable, Cloneable {
      */
     protected transient float lastDistance = -1;
 
-    /**
-     * If light is disabled, it will not have any 
-     */
     protected boolean enabled = true;
 
     /** 
@@ -169,20 +166,24 @@ public abstract class Light implements Savable, Cloneable {
         this.color.set(color);
     }
 
-    
-    /*
-     * Returns true if the light is enabled
-     * 
-     * @return true if the light is enabled
-     * 
-     * @see Light#setEnabled(boolean)
+
+    /**
+     * Returns true if this light is enabled.
+     * @return true if enabled, otherwise false.
      */
-    /*
     public boolean isEnabled() {
         return enabled;
     }
-    */
-    
+
+    /**
+     * Set to false in order to disable a light and have it filtered out from being included in rendering.
+     *
+     * @param enabled true to enable and false to disable the light.
+     */
+    public void setEnabled(boolean enabled) {
+        this.enabled = enabled;
+    }
+
     /**
      * Determines if the light intersects with the given bounding box.
      * <p>
@@ -227,7 +228,9 @@ public abstract class Light implements Savable, Cloneable {
     @Override
     public Light clone(){
         try {
-            return (Light) super.clone();
+            Light l = (Light) super.clone();
+            l.color = color.clone();
+            return l;
         } catch (CloneNotSupportedException ex) {
             throw new AssertionError();
         }

+ 8 - 1
jme3-core/src/main/java/com/jme3/light/PointLight.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012, 2015 jMonkeyEngine
+ * Copyright (c) 2009-2012, 2015-2016 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -241,4 +241,11 @@ public class PointLight extends Light {
             this.invRadius = 0;
         }
     }
+
+    @Override
+    public PointLight clone() {
+        PointLight p = (PointLight)super.clone();
+        p.position = position.clone();
+        return p;
+    }
 }

+ 9 - 1
jme3-core/src/main/java/com/jme3/light/SpotLight.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012, 2015 jMonkeyEngine
+ * Copyright (c) 2009-2012, 2015-2016 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -448,5 +448,13 @@ public class SpotLight extends Light {
             this.invSpotRange = 0;
         }
     }
+
+    @Override
+    public SpotLight clone() {
+        SpotLight s = (SpotLight)super.clone();
+        s.direction = direction.clone();
+        s.position = position.clone();
+        return s;
+    }
 }
 

+ 2 - 0
jme3-core/src/main/java/com/jme3/material/Material.java

@@ -1236,12 +1236,14 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
         oc.write(def.getAssetName(), "material_def", null);
         oc.write(additionalState, "render_state", null);
         oc.write(transparent, "is_transparent", false);
+        oc.write(name, "name", null);
         oc.writeStringSavableMap(paramValues, "parameters", null);
     }
 
     public void read(JmeImporter im) throws IOException {
         InputCapsule ic = im.getCapsule(this);
 
+        name = ic.readString("name", null);
         additionalState = (RenderState) ic.readSavable("render_state", null);
         transparent = ic.readBoolean("is_transparent", false);
 

+ 11 - 17
jme3-core/src/main/java/com/jme3/renderer/RenderManager.java

@@ -34,12 +34,8 @@ package com.jme3.renderer;
 import com.jme3.light.DefaultLightFilter;
 import com.jme3.light.LightFilter;
 import com.jme3.light.LightList;
-import com.jme3.material.Material;
-import com.jme3.material.MaterialDef;
-import com.jme3.material.RenderState;
-import com.jme3.material.Technique;
-import com.jme3.material.TechniqueDef;
-import com.jme3.math.*;
+import com.jme3.material.*;
+import com.jme3.math.Matrix4f;
 import com.jme3.post.SceneProcessor;
 import com.jme3.profile.AppProfiler;
 import com.jme3.profile.AppStep;
@@ -55,6 +51,7 @@ import com.jme3.shader.UniformBindingManager;
 import com.jme3.system.NullRenderer;
 import com.jme3.system.Timer;
 import com.jme3.util.SafeArrayList;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -533,7 +530,6 @@ public class RenderManager {
             lightFilter.filterLights(g, filteredLightList);
             lightList = filteredLightList;
         }
-        
 
         //if forcedTechnique we try to force it for render,
         //if it does not exists in the mat def, we check for forcedMaterial and render the geom if not null
@@ -556,7 +552,7 @@ public class RenderManager {
                 forcedRenderState = tmpRs;
 
                 //Reverted this part from revision 6197
-                //If forcedTechnique does not exists, and frocedMaterial is not set, the geom MUST NOT be rendered
+                //If forcedTechnique does not exists, and forcedMaterial is not set, the geom MUST NOT be rendered
             } else if (forcedMaterial != null) {
                 // use forced material
                 forcedMaterial.render(g, lightList, this);
@@ -641,10 +637,8 @@ public class RenderManager {
      * <p>
      * In addition to enqueuing the visible geometries, this method
      * also scenes which cast or receive shadows, by putting them into the
-     * RenderQueue's 
-     * {@link RenderQueue#addToShadowQueue(com.jme3.scene.Geometry, com.jme3.renderer.queue.RenderQueue.ShadowMode) 
-     * shadow queue}. Each Spatial which has its 
-     * {@link Spatial#setShadowMode(com.jme3.renderer.queue.RenderQueue.ShadowMode) shadow mode}
+     * RenderQueue's {@link RenderQueue#renderShadowQueue(GeometryList, RenderManager, Camera, boolean) shadow queue}.
+     * Each Spatial which has its {@link Spatial#setShadowMode(com.jme3.renderer.queue.RenderQueue.ShadowMode) shadow mode}
      * set to not off, will be put into the appropriate shadow queue, note that
      * this process does not check for frustum culling on any 
      * {@link ShadowMode#Cast shadow casters}, as they don't have to be
@@ -784,7 +778,8 @@ public class RenderManager {
      * @param singlePassLightBatchSize the number of lights.
      */
     public void setSinglePassLightBatchSize(int singlePassLightBatchSize) {
-        this.singlePassLightBatchSize = singlePassLightBatchSize;
+        // Ensure the batch size is no less than 1
+        this.singlePassLightBatchSize = singlePassLightBatchSize < 1 ? 1 : singlePassLightBatchSize;
     }
     
     
@@ -990,13 +985,12 @@ public class RenderManager {
      * (see {@link #renderTranslucentQueue(com.jme3.renderer.ViewPort) })</li>
      * <li>If any objects remained in the render queue, they are removed
      * from the queue. This is generally objects added to the 
-     * {@link RenderQueue#renderShadowQueue(com.jme3.renderer.queue.RenderQueue.ShadowMode, com.jme3.renderer.RenderManager, com.jme3.renderer.Camera, boolean) 
-     * shadow queue}
+     * {@link RenderQueue#renderShadowQueue(GeometryList, RenderManager, Camera, boolean) shadow queue}
      * which were not rendered because of a missing shadow renderer.</li>
      * </ul>
      * 
-     * @param vp
-     * @param tpf 
+     * @param vp View port to render
+     * @param tpf Time per frame value
      */
     public void renderViewPort(ViewPort vp, float tpf) {
         if (!vp.isEnabled()) {

+ 0 - 1
jme3-core/src/main/java/com/jme3/renderer/Statistics.java

@@ -36,7 +36,6 @@ import com.jme3.shader.Shader;
 import com.jme3.texture.FrameBuffer;
 import com.jme3.texture.Image;
 import com.jme3.util.IntMap;
-import java.util.HashSet;
 
 /**
  * The statistics class allows tracking of real-time rendering statistics.

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

+ 5 - 0
jme3-core/src/main/java/com/jme3/scene/Node.java

@@ -687,6 +687,11 @@ public class Node extends Spatial {
 //            childClone.parent = nodeClone;
 //            nodeClone.children.add(childClone);
 //        }
+
+        // Reset the fields of the clone that should be in a 'new' state.
+        nodeClone.updateList = null;
+        nodeClone.updateListValid = false; // safe because parent is nulled out in super.clone()
+            
         return nodeClone;
     }
 

+ 1 - 1
jme3-core/src/main/java/com/jme3/system/AppSettings.java

@@ -963,7 +963,7 @@ public final class AppSettings extends HashMap<String, Object> {
         return getString("SettingsDialogImage");
     }
 
-    public boolean getGammaCorrection() {
+    public boolean isGammaCorrection() {
         return getBoolean("GammaCorrection");
     }
     

+ 19 - 0
jme3-core/src/main/java/com/jme3/util/BufferUtils.java

@@ -401,6 +401,25 @@ public final class BufferUtils {
         vector.z = buf.get(index * 3 + 2);
     }
 
+    /**
+     * Updates the values of the given vector from the specified buffer at the
+     * index provided.
+     * 
+     * @param vector
+     *            the vector to set data on
+     * @param buf
+     *            the buffer to read from
+     * @param index
+     *            the position (in terms of vectors, not floats) to read from
+     *            the buf
+     */
+    public static void populateFromBuffer(Vector4f vector, FloatBuffer buf, int index) {
+        vector.x = buf.get(index * 4);
+        vector.y = buf.get(index * 4 + 1);
+        vector.z = buf.get(index * 4 + 2);
+        vector.w = buf.get(index * 4 + 3);
+    }
+
     /**
      * Generates a Vector3f array from the given FloatBuffer.
      * 

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

+ 4 - 3
jme3-core/src/main/resources/Common/MatDefs/Misc/Particle.vert

@@ -32,11 +32,12 @@ void main(){
     #ifdef POINT_SPRITE
         vec4 worldPos = g_WorldMatrix * pos;
         float d = distance(g_CameraPosition.xyz, worldPos.xyz);
-        gl_PointSize = max(1.0, (inSize * SIZE_MULTIPLIER * m_Quadratic) / d);
+        float size = (inSize * SIZE_MULTIPLIER * m_Quadratic) / d;
+        gl_PointSize = max(1.0, size);
 
         //vec4 worldViewPos = g_WorldViewMatrix * pos;
         //gl_PointSize = (inSize * SIZE_MULTIPLIER * m_Quadratic)*100.0 / worldViewPos.z;
 
-        color.a *= min(gl_PointSize, 1.0);
+        color.a *= min(size, 1.0);
     #endif
-}
+}

+ 19 - 0
jme3-core/src/main/resources/joystick-mapping.properties

@@ -95,6 +95,25 @@ Gamepad\ F310\ (Controller).ry=rz
 # keeps it from confusing the .rx mapping.
 Gamepad\ F310\ (Controller).z=trigger
 
+# Logitech F310 gamepad with dip switch XInput for Windows 10
+Controller\ (Gamepad\ F310).0=2
+Controller\ (Gamepad\ F310).1=1
+Controller\ (Gamepad\ F310).2=3
+Controller\ (Gamepad\ F310).3=0
+
+Controller\ (Gamepad\ F310).6=8
+Controller\ (Gamepad\ F310).7=9
+
+Controller\ (Gamepad\ F310).8=10
+Controller\ (Gamepad\ F310).9=11
+
+Controller\ (Gamepad\ F310).rx=z
+Controller\ (Gamepad\ F310).ry=rz
+
+# requires custom code to support trigger buttons but this
+# keeps it from confusing the .rx mapping.
+Controller\ (Gamepad\ F310).z=trigger
+
 # Alternate version of the XBOX 360 controller
 XBOX\ 360\ For\ Windows\ (Controller).0=2
 XBOX\ 360\ For\ Windows\ (Controller).1=1

+ 1 - 1
jme3-core/src/tools/java/jme3tools/optimize/TextureAtlas.java

@@ -155,7 +155,7 @@ public class TextureAtlas {
                 return false;
             } else {
                 if (normal != null && normal.getKey() != null) {
-                    addTexture(diffuse, "NormalMap", keyName);
+                    addTexture(normal, "NormalMap", keyName);
                 }
                 if (specular != null && specular.getKey() != null) {
                     addTexture(specular, "SpecularMap", keyName);

+ 1 - 1
jme3-desktop/src/main/java/com/jme3/app/SettingsDialog.java

@@ -360,7 +360,7 @@ public final class SettingsDialog extends JFrame {
         vsyncBox.setSelected(source.isVSync());
         
         gammaBox = new JCheckBox(resourceBundle.getString("checkbox.gamma"));
-        gammaBox.setSelected(source.getGammaCorrection());
+        gammaBox.setSelected(source.isGammaCorrection());
         
         gbc = new GridBagConstraints();
         gbc.weightx = 0.5;

+ 72 - 0
jme3-examples/src/main/java/jme3test/app/TestEnqueueRunnable.java

@@ -0,0 +1,72 @@
+package jme3test.app;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.shape.Box;
+
+/**
+ * @author john01dav
+ */
+public class TestEnqueueRunnable extends SimpleApplication{
+    private ExampleAsyncTask exampleAsyncTask;
+    
+    public static void main(String[] args){
+        new TestEnqueueRunnable().start();
+    }
+    
+    @Override
+    public void simpleInitApp(){
+        Geometry geom = new Geometry("Box", new Box(1, 1, 1));
+        Material material = new Material(getAssetManager(), "/Common/MatDefs/Misc/Unshaded.j3md");
+        material.setColor("Color", ColorRGBA.Blue); //a color is needed to start with
+        geom.setMaterial(material);
+        getRootNode().attachChild(geom);
+        
+        exampleAsyncTask = new ExampleAsyncTask(material);
+        exampleAsyncTask.getThread().start();
+    }
+
+    @Override
+    public void destroy(){
+        exampleAsyncTask.endTask();
+        super.destroy();
+    }
+    
+    private class ExampleAsyncTask implements Runnable{
+        private final Thread thread;
+        private final Material material;
+        private volatile boolean running = true;
+
+        public ExampleAsyncTask(Material material){
+            this.thread = new Thread(this);
+            this.material = material;
+        }
+
+        public Thread getThread(){
+            return thread;
+        }
+        
+        public void run(){
+            while(running){
+                enqueue(new Runnable(){ //primary usage of this in real applications would use lambda expressions which are unavailable at java 6
+                    public void run(){
+                        material.setColor("Color", ColorRGBA.randomColor());
+                    }
+                });
+                
+                try{
+                    Thread.sleep(1000);
+                }catch(InterruptedException e){}
+            }
+        }
+        
+        public void endTask(){
+            running = false;
+            thread.interrupt();
+        }
+        
+    }
+    
+}

+ 22 - 21
jme3-examples/src/main/java/jme3test/light/TestManyLightsSingle.java

@@ -65,25 +65,23 @@ public class TestManyLightsSingle extends SimpleApplication {
         TestManyLightsSingle app = new TestManyLightsSingle();
         app.start();
     }
-    
+
     /**
      * Switch mode with space bar at run time
      */
     TechniqueDef.LightMode lm = TechniqueDef.LightMode.SinglePass;
-    int lightNum = 6;
 
     @Override
     public void simpleInitApp() {
         renderManager.setPreferredLightMode(lm);
-        renderManager.setSinglePassLightBatchSize(lightNum);
-
+        renderManager.setSinglePassLightBatchSize(6);
 
         flyCam.setMoveSpeed(10);
 
         Node scene = (Node) assetManager.loadModel("Scenes/ManyLights/Main.scene");
         rootNode.attachChild(scene);
         Node n = (Node) rootNode.getChild(0);
-        LightList lightList = n.getWorldLightList();
+        final LightList lightList = n.getWorldLightList();
         final Geometry g = (Geometry) n.getChild("Grid-geom-1");
 
         g.getMaterial().setColor("Ambient", new ColorRGBA(0.2f, 0.2f, 0.2f, 1f));
@@ -152,8 +150,6 @@ public class TestManyLightsSingle extends SimpleApplication {
 //        guiNode.setCullHint(CullHint.Always);
 
 
-
-
         flyCam.setDragToRotate(true);
         flyCam.setMoveSpeed(50);
 
@@ -168,27 +164,35 @@ public class TestManyLightsSingle extends SimpleApplication {
                         helloText.setText("(Multi pass)");
                     } else {
                         lm = TechniqueDef.LightMode.SinglePass;
-                        helloText.setText("(Single pass) nb lights per batch : " + lightNum);
+                        helloText.setText("(Single pass) nb lights per batch : " + renderManager.getSinglePassLightBatchSize());
                     }
                     renderManager.setPreferredLightMode(lm);
-                    reloadScene(g,boxGeo,cubeNodes);
+                    reloadScene(g, boxGeo, cubeNodes);
                 }
                 if (name.equals("lightsUp") && isPressed) {
-                    lightNum++;
-                    renderManager.setSinglePassLightBatchSize(lightNum);
-                    helloText.setText("(Single pass) nb lights per batch : " + lightNum);
+                    renderManager.setSinglePassLightBatchSize(renderManager.getSinglePassLightBatchSize() + 1);
+                    helloText.setText("(Single pass) nb lights per batch : " + renderManager.getSinglePassLightBatchSize());
                 }
                 if (name.equals("lightsDown") && isPressed) {
-                    lightNum--;
-                    renderManager.setSinglePassLightBatchSize(lightNum);
-                    helloText.setText("(Single pass) nb lights per batch : " + lightNum);
+                    renderManager.setSinglePassLightBatchSize(renderManager.getSinglePassLightBatchSize() - 1);
+                    helloText.setText("(Single pass) nb lights per batch : " + renderManager.getSinglePassLightBatchSize());
+                }
+                if (name.equals("toggleOnOff") && isPressed) {
+                    for (final Light light : lightList) {
+                        if (light instanceof AmbientLight) {
+                            continue;
+                        }
+
+                        light.setEnabled(!light.isEnabled());
+                    }
                 }
             }
-        }, "toggle", "lightsUp", "lightsDown");
+        }, "toggle", "lightsUp", "lightsDown", "toggleOnOff");
 
         inputManager.addMapping("toggle", new KeyTrigger(KeyInput.KEY_SPACE));
         inputManager.addMapping("lightsUp", new KeyTrigger(KeyInput.KEY_UP));
         inputManager.addMapping("lightsDown", new KeyTrigger(KeyInput.KEY_DOWN));
+        inputManager.addMapping("toggleOnOff", new KeyTrigger(KeyInput.KEY_L));
 
 
         SpotLight spot = new SpotLight();
@@ -215,12 +219,9 @@ public class TestManyLightsSingle extends SimpleApplication {
         guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt");
         helloText = new BitmapText(guiFont, false);
         helloText.setSize(guiFont.getCharSet().getRenderedSize());
-        helloText.setText("(Single pass) nb lights per batch : " + lightNum);
+        helloText.setText("(Single pass) nb lights per batch : " + renderManager.getSinglePassLightBatchSize());
         helloText.setLocalTranslation(300, helloText.getLineHeight(), 0);
         guiNode.attachChild(helloText);
-
-
-
     }
 
     protected void reloadScene(Geometry g, Geometry boxGeo, Node cubeNodes) {
@@ -234,7 +235,7 @@ public class TestManyLightsSingle extends SimpleApplication {
             cubeNodes.setMaterial(m);
         }
     }
-    
+
     BitmapText helloText;
     long time;
     long nbFrames;

+ 1 - 1
jme3-jbullet/src/main/java/com/jme3/bullet/objects/PhysicsVehicle.java

@@ -456,7 +456,7 @@ public class PhysicsVehicle extends PhysicsRigidBody {
     /**
      * Get the current forward vector of the vehicle in world coordinates
      * @param vector The object to write the forward vector values to.
-     * Passing null will cause a new {@link Vector3f) to be created.
+     * Passing null will cause a new {@link Vector3f} to be created.
      * @return The forward vector
      */
     public Vector3f getForwardVector(Vector3f vector) {

+ 19 - 18
jme3-jogl/src/main/java/com/jme3/system/jogl/JoglContext.java

@@ -54,6 +54,7 @@ import com.jme3.system.AppSettings;
 import com.jme3.system.JmeContext;
 import com.jme3.system.NanoTimer;
 import com.jme3.system.NativeLibraryLoader;
+import com.jme3.system.NullRenderer;
 import com.jme3.system.SystemListener;
 import com.jme3.system.Timer;
 
@@ -69,9 +70,9 @@ import com.jogamp.opengl.GLContext;
 public abstract class JoglContext implements JmeContext {
 
     private static final Logger logger = Logger.getLogger(JoglContext.class.getName());
-    
+
     protected static final String THREAD_NAME = "jME3 Main";
-    
+
     protected AtomicBoolean created = new AtomicBoolean(false);
     protected AtomicBoolean renderable = new AtomicBoolean(false);
     protected final Object createdLock = new Object();
@@ -91,7 +92,7 @@ public abstract class JoglContext implements JmeContext {
             NativeLibraryLoader.loadNativeLibrary("bulletjme", true);
         }
     }
-    
+
     @Override
 	public void setSystemListener(SystemListener listener){
         this.listener = listener;
@@ -101,7 +102,7 @@ public abstract class JoglContext implements JmeContext {
 	public void setSettings(AppSettings settings) {
         this.settings.copyFrom(settings);
     }
-    
+
     @Override
 	public boolean isRenderable(){
         return renderable.get();
@@ -160,50 +161,50 @@ public abstract class JoglContext implements JmeContext {
             }
         }
     }
-    
+
     protected void initContextFirstTime(){
         if (GLContext.getCurrent().getGLVersionNumber().getMajor() < 2) {
-            throw new RendererException("OpenGL 2.0 or higher is " + 
+            throw new RendererException("OpenGL 2.0 or higher is " +
                                         "required for jMonkeyEngine");
         }
-        
+
         if (settings.getRenderer().startsWith("JOGL")) {
         	com.jme3.renderer.opengl.GL gl = new JoglGL();
         	GLExt glext = new JoglGLExt();
         	GLFbo glfbo = new JoglGLFbo();
-            
+
             if (settings.getBoolean("GraphicsDebug")) {
                 gl    = new GLDebugDesktop(gl, glext, glfbo);
                 glext = (GLExt) gl;
                 glfbo = (GLFbo) gl;
             }
-            
+
             if (settings.getBoolean("GraphicsTiming")) {
                 GLTimingState timingState = new GLTimingState();
                 gl    = (com.jme3.renderer.opengl.GL) GLTiming.createGLTiming(gl, timingState, GL.class, GL2.class, GL3.class, GL4.class);
                 glext = (GLExt) GLTiming.createGLTiming(glext, timingState, GLExt.class);
                 glfbo = (GLFbo) GLTiming.createGLTiming(glfbo, timingState, GLFbo.class);
             }
-                  
+
             if (settings.getBoolean("GraphicsTrace")) {
                 gl    = (com.jme3.renderer.opengl.GL) GLTracer.createDesktopGlTracer(gl, GL.class, GL2.class, GL3.class, GL4.class);
                 glext = (GLExt) GLTracer.createDesktopGlTracer(glext, GLExt.class);
                 glfbo = (GLFbo) GLTracer.createDesktopGlTracer(glfbo, GLFbo.class);
             }
-            
+
             renderer = new GLRenderer(gl, glext, glfbo);
             renderer.initialize();
         } else {
             throw new UnsupportedOperationException("Unsupported renderer: " + settings.getRenderer());
         }
-        
+
         if (GLContext.getCurrentGL().isExtensionAvailable("GL_ARB_debug_output") && settings.getBoolean("GraphicsDebug")) {
         	GLContext.getCurrent().enableGLDebugMessage(true);
         	GLContext.getCurrent().addGLDebugListener(new JoglGLDebugOutputHandler());
         }
-        
-        renderer.setMainFrameBufferSrgb(settings.getGammaCorrection());
-        renderer.setLinearizeSrgbImages(settings.getGammaCorrection());
+
+        renderer.setMainFrameBufferSrgb(settings.isGammaCorrection());
+        renderer.setLinearizeSrgbImages(settings.isGammaCorrection());
 
         // Init input
         if (keyInput != null) {
@@ -241,7 +242,7 @@ public abstract class JoglContext implements JmeContext {
             createdLock.notifyAll();
         }
     }
-    
+
     protected int determineMaxSamples(int requestedSamples) {
         GL gl = GLContext.getCurrentGL();
         if (gl.hasFullFBOSupport()) {
@@ -257,7 +258,7 @@ public abstract class JoglContext implements JmeContext {
             }
         }
     }
-    
+
     protected int getNumSamplesToUse() {
         int samples = 0;
         if (settings.getSamples() > 1){
@@ -268,7 +269,7 @@ public abstract class JoglContext implements JmeContext {
                         "Couldn''t satisfy antialiasing samples requirement: x{0}. "
                         + "Video hardware only supports: x{1}",
                         new Object[]{samples, supportedSamples});
-                
+
                 samples = supportedSamples;
             }
         }

+ 53 - 52
jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglContext.java

@@ -29,7 +29,6 @@
  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
-
 package com.jme3.system.lwjgl;
 
 import com.jme3.input.lwjgl.JInputJoyInput;
@@ -53,6 +52,7 @@ import com.jme3.renderer.opengl.GLTiming;
 import com.jme3.renderer.opengl.GLTimingState;
 import com.jme3.renderer.opengl.GLTracer;
 import com.jme3.system.*;
+import java.io.File;
 
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.logging.Level;
@@ -69,7 +69,7 @@ public abstract class LwjglContext implements JmeContext {
     private static final Logger logger = Logger.getLogger(LwjglContext.class.getName());
 
     protected static final String THREAD_NAME = "jME3 Main";
-    
+
     protected AtomicBoolean created = new AtomicBoolean(false);
     protected AtomicBoolean renderable = new AtomicBoolean(false);
     protected final Object createdLock = new Object();
@@ -82,18 +82,18 @@ public abstract class LwjglContext implements JmeContext {
     protected Timer timer;
     protected SystemListener listener;
 
-    public void setSystemListener(SystemListener listener){
+    public void setSystemListener(SystemListener listener) {
         this.listener = listener;
     }
 
     protected void printContextInitInfo() {
-        logger.log(Level.INFO, "LWJGL {0} context running on thread {1}\n" +
-                               " * Graphics Adapter: {2}\n" +
-                               " * Driver Version: {3}\n" +
-                               " * Scaling Factor: {4}",
-                               new Object[]{ Sys.getVersion(), Thread.currentThread().getName(), 
-                                             Display.getAdapter(), Display.getVersion(),
-                                             Display.getPixelScaleFactor() });
+        logger.log(Level.INFO, "LWJGL {0} context running on thread {1}\n"
+                + " * Graphics Adapter: {2}\n"
+                + " * Driver Version: {3}\n"
+                + " * Scaling Factor: {4}",
+                new Object[]{Sys.getVersion(), Thread.currentThread().getName(),
+                    Display.getAdapter(), Display.getVersion(),
+                    Display.getPixelScaleFactor()});
     }
 
     protected ContextAttribs createContextAttribs() {
@@ -113,7 +113,7 @@ public abstract class LwjglContext implements JmeContext {
             return null;
         }
     }
-    
+
     protected int determineMaxSamples(int requestedSamples) {
         try {
             // If we already have a valid context, determine samples using current
@@ -131,13 +131,13 @@ public abstract class LwjglContext implements JmeContext {
         } catch (LWJGLException ex) {
             listener.handleError("Failed to check if display is current", ex);
         }
-        
+
         if ((Pbuffer.getCapabilities() & Pbuffer.PBUFFER_SUPPORTED) == 0) {
             // No pbuffer, assume everything is supported.
             return Integer.MAX_VALUE;
         } else {
             Pbuffer pb = null;
-            
+
             // OpenGL2 method: Create pbuffer and query samples
             // from GL_ARB_framebuffer_object or GL_EXT_framebuffer_multisample.
             try {
@@ -155,13 +155,14 @@ public abstract class LwjglContext implements JmeContext {
             } catch (LWJGLException ex) {
                 // Something else failed.
                 return Integer.MAX_VALUE;
-            } finally { 
+            } finally {
                 if (pb != null) {
                     pb.destroy();
                 }
             }
         }
     }
+
     protected void loadNatives() {
         if (JmeSystem.isLowPermissions()) {
             return;
@@ -178,10 +179,10 @@ public abstract class LwjglContext implements JmeContext {
         }
         NativeLibraryLoader.loadNativeLibrary("lwjgl", true);
     }
-    
+
     protected int getNumSamplesToUse() {
         int samples = 0;
-        if (settings.getSamples() > 1){
+        if (settings.getSamples() > 1) {
             samples = settings.getSamples();
             int supportedSamples = determineMaxSamples(samples);
             if (supportedSamples < samples) {
@@ -189,62 +190,62 @@ public abstract class LwjglContext implements JmeContext {
                         "Couldn''t satisfy antialiasing samples requirement: x{0}. "
                         + "Video hardware only supports: x{1}",
                         new Object[]{samples, supportedSamples});
-                
+
                 samples = supportedSamples;
             }
         }
         return samples;
     }
 
-    protected void initContextFirstTime(){
+    protected void initContextFirstTime() {
         if (!GLContext.getCapabilities().OpenGL20) {
-            throw new RendererException("OpenGL 2.0 or higher is " + 
-                                        "required for jMonkeyEngine");
+            throw new RendererException("OpenGL 2.0 or higher is "
+                    + "required for jMonkeyEngine");
         }
-        
+
         if (settings.getRenderer().equals(AppSettings.LWJGL_OPENGL2)
-         || settings.getRenderer().equals(AppSettings.LWJGL_OPENGL3)) {
+                || settings.getRenderer().equals(AppSettings.LWJGL_OPENGL3)) {
             GL gl = new LwjglGL();
             GLExt glext = new LwjglGLExt();
             GLFbo glfbo;
-            
+
             if (GLContext.getCapabilities().OpenGL30) {
                 glfbo = new LwjglGLFboGL3();
             } else {
                 glfbo = new LwjglGLFboEXT();
             }
-            
+
             if (settings.getBoolean("GraphicsDebug")) {
-                gl    = new GLDebugDesktop(gl, glext, glfbo);
+                gl = new GLDebugDesktop(gl, glext, glfbo);
                 glext = (GLExt) gl;
                 glfbo = (GLFbo) gl;
             }
-            
+
             if (settings.getBoolean("GraphicsTiming")) {
                 GLTimingState timingState = new GLTimingState();
-                gl    = (GL) GLTiming.createGLTiming(gl, timingState, GL.class, GL2.class, GL3.class, GL4.class);
+                gl = (GL) GLTiming.createGLTiming(gl, timingState, GL.class, GL2.class, GL3.class, GL4.class);
                 glext = (GLExt) GLTiming.createGLTiming(glext, timingState, GLExt.class);
                 glfbo = (GLFbo) GLTiming.createGLTiming(glfbo, timingState, GLFbo.class);
             }
-                  
+
             if (settings.getBoolean("GraphicsTrace")) {
-                gl    = (GL) GLTracer.createDesktopGlTracer(gl, GL.class, GL2.class, GL3.class, GL4.class);
+                gl = (GL) GLTracer.createDesktopGlTracer(gl, GL.class, GL2.class, GL3.class, GL4.class);
                 glext = (GLExt) GLTracer.createDesktopGlTracer(glext, GLExt.class);
                 glfbo = (GLFbo) GLTracer.createDesktopGlTracer(glfbo, GLFbo.class);
             }
-            
+
             renderer = new GLRenderer(gl, glext, glfbo);
             renderer.initialize();
         } else {
             throw new UnsupportedOperationException("Unsupported renderer: " + settings.getRenderer());
         }
-        
+
         if (GLContext.getCapabilities().GL_ARB_debug_output && settings.getBoolean("GraphicsDebug")) {
             ARBDebugOutput.glDebugMessageCallbackARB(new ARBDebugOutputCallback(new LwjglGLDebugOutputHandler()));
         }
-        
-        renderer.setMainFrameBufferSrgb(settings.getGammaCorrection());
-        renderer.setLinearizeSrgbImages(settings.getGammaCorrection());
+
+        renderer.setMainFrameBufferSrgb(settings.isGammaCorrection());
+        renderer.setLinearizeSrgbImages(settings.isGammaCorrection());
 
         // Init input
         if (keyInput != null) {
@@ -260,42 +261,42 @@ public abstract class LwjglContext implements JmeContext {
         }
     }
 
-    public void internalDestroy(){
+    public void internalDestroy() {
         renderer = null;
         timer = null;
         renderable.set(false);
-        synchronized (createdLock){
+        synchronized (createdLock) {
             created.set(false);
             createdLock.notifyAll();
         }
     }
-    
-    public void internalCreate(){
+
+    public void internalCreate() {
         timer = new LwjglTimer();
-        
-        synchronized (createdLock){
+
+        synchronized (createdLock) {
             created.set(true);
             createdLock.notifyAll();
         }
-        
-        if (renderable.get()){
+
+        if (renderable.get()) {
             initContextFirstTime();
-        }else{
+        } else {
             assert getType() == Type.Canvas;
         }
     }
 
-    public void create(){
+    public void create() {
         create(false);
     }
 
-    public void destroy(){
+    public void destroy() {
         destroy(false);
     }
 
-    protected void waitFor(boolean createdVal){
-        synchronized (createdLock){
-            while (created.get() != createdVal){
+    protected void waitFor(boolean createdVal) {
+        synchronized (createdLock) {
+            while (created.get() != createdVal) {
                 try {
                     createdLock.wait();
                 } catch (InterruptedException ex) {
@@ -304,11 +305,11 @@ public abstract class LwjglContext implements JmeContext {
         }
     }
 
-    public boolean isCreated(){
+    public boolean isCreated() {
         return created.get();
     }
-    
-    public boolean isRenderable(){
+
+    public boolean isRenderable() {
         return renderable.get();
     }
 
@@ -316,7 +317,7 @@ public abstract class LwjglContext implements JmeContext {
         this.settings.copyFrom(settings);
     }
 
-    public AppSettings getSettings(){
+    public AppSettings getSettings() {
         return settings;
     }
 

+ 7 - 7
jme3-lwjgl3/build.gradle

@@ -2,14 +2,14 @@ if (!hasProperty('mainClass')) {
     ext.mainClass = ''
 }
 
-repositories {
-    maven {
-        url "https://oss.sonatype.org/content/repositories/snapshots"
-    }
-}
+def lwjglVersion = '3.0.0b'
 
 dependencies {
     compile project(':jme3-core')
     compile project(':jme3-desktop')
-    compile files('lib/lwjgl-3.0.0b-35.jar', 'lib/lwjgl-3.0.0b-35-natives.jar')
-}
+
+    compile "org.lwjgl:lwjgl:${lwjglVersion}"
+    compile "org.lwjgl:lwjgl-platform:${lwjglVersion}:natives-windows"
+    compile "org.lwjgl:lwjgl-platform:${lwjglVersion}:natives-linux"
+    compile "org.lwjgl:lwjgl-platform:${lwjglVersion}:natives-osx"
+}

BIN
jme3-lwjgl3/lib/lwjgl-3.0.0b-35-natives.jar


BIN
jme3-lwjgl3/lib/lwjgl-3.0.0b-35.jar


+ 22 - 18
jme3-lwjgl3/src/main/java/com/jme3/audio/lwjgl/LwjglALC.java

@@ -32,32 +32,38 @@
 package com.jme3.audio.lwjgl;
 
 import com.jme3.audio.openal.ALC;
+import java.nio.IntBuffer;
 import org.lwjgl.openal.ALC10;
 import org.lwjgl.openal.ALContext;
 import org.lwjgl.openal.ALDevice;
-
-import java.nio.IntBuffer;
-
-import static org.lwjgl.openal.ALC10.alcGetContextsDevice;
-import static org.lwjgl.openal.ALC10.alcGetCurrentContext;
+import org.lwjgl.openal.SOFTPauseDevice;
 
 public class LwjglALC implements ALC {
 
     private ALDevice device;
     private ALContext context;
 
+    private long contextId;
+    private long deviceId;
+
     public void createALC() {
         device = ALDevice.create();
         context = ALContext.create(device);
+        context.makeCurrent();
+
+        contextId = ALC10.alcGetCurrentContext();
+        deviceId = ALC10.alcGetContextsDevice(contextId);
     }
 
     public void destroyALC() {
         if (context != null) {
             context.destroy();
+            context = null;
         }
 
         if (device != null) {
             device.destroy();
+            device = null;
         }
     }
 
@@ -66,31 +72,29 @@ public class LwjglALC implements ALC {
     }
 
     public String alcGetString(final int parameter) {
-        final long context = alcGetCurrentContext();
-        final long device = alcGetContextsDevice(context);
-        return ALC10.alcGetString(device, parameter);
+        return ALC10.alcGetString(deviceId, parameter);
     }
 
     public boolean alcIsExtensionPresent(final String extension) {
-        final long context = alcGetCurrentContext();
-        final long device = alcGetContextsDevice(context);
-        return ALC10.alcIsExtensionPresent(device, extension);
+        return ALC10.alcIsExtensionPresent(deviceId, extension);
     }
 
     public void alcGetInteger(final int param, final IntBuffer buffer, final int size) {
-        if (buffer.position() != 0) throw new AssertionError();
-        if (buffer.limit() != size) throw new AssertionError();
-
-        final long context = alcGetCurrentContext();
-        final long device = alcGetContextsDevice(context);
-        final int value = ALC10.alcGetInteger(device, param);
-        //buffer.put(value);
+        if (buffer.position() != 0) {
+            throw new AssertionError();
+        }
+        if (buffer.limit() != size) {
+            throw new AssertionError();
+        }
+        ALC10.alcGetIntegerv(deviceId, param, buffer);
     }
 
     public void alcDevicePauseSOFT() {
+        SOFTPauseDevice.alcDevicePauseSOFT(deviceId);
     }
 
     public void alcDeviceResumeSOFT() {
+        SOFTPauseDevice.alcDeviceResumeSOFT(deviceId);
     }
 
 }

+ 2 - 1
jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwKeyInput.java

@@ -66,7 +66,8 @@ public class GlfwKeyInput implements KeyInput {
         glfwSetKeyCallback(context.getWindowHandle(), keyCallback = new GLFWKeyCallback() {
             @Override
             public void invoke(long window, int key, int scancode, int action, int mods) {
-                final KeyInputEvent evt = new KeyInputEvent(scancode, (char) key, GLFW_PRESS == action, GLFW_REPEAT == action);
+                int jmeKey = GlfwKeyMap.toJmeKeyCode(key);
+                final KeyInputEvent evt = new KeyInputEvent(jmeKey, (char) key, GLFW_PRESS == action, GLFW_REPEAT == action);
                 evt.setTime(getInputTimeNanos());
                 keyInputEvents.add(evt);
             }

+ 171 - 0
jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwKeyMap.java

@@ -0,0 +1,171 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.input.lwjgl;
+
+import static org.lwjgl.glfw.GLFW.*;
+import static com.jme3.input.KeyInput.*;
+
+public class GlfwKeyMap {
+
+    private static final int[] glfwToJmeKeyMap = new int[GLFW_KEY_LAST + 1];
+
+    private static void reg(int jmeKey, int glfwKey) {
+        glfwToJmeKeyMap[glfwKey] = jmeKey;
+    }
+
+    static {
+        reg(KEY_ESCAPE, GLFW_KEY_ESCAPE);
+        reg(KEY_1, GLFW_KEY_1);
+        reg(KEY_2, GLFW_KEY_2);
+        reg(KEY_3, GLFW_KEY_3);
+        reg(KEY_4, GLFW_KEY_4);
+        reg(KEY_5, GLFW_KEY_5);
+        reg(KEY_6, GLFW_KEY_6);
+        reg(KEY_7, GLFW_KEY_7);
+        reg(KEY_8, GLFW_KEY_8);
+        reg(KEY_9, GLFW_KEY_9);
+        reg(KEY_0, GLFW_KEY_0);
+        reg(KEY_MINUS, GLFW_KEY_MINUS);
+        reg(KEY_EQUALS, GLFW_KEY_EQUAL);
+        reg(KEY_BACK, GLFW_KEY_BACKSPACE);
+        reg(KEY_TAB, GLFW_KEY_TAB);
+        reg(KEY_Q, GLFW_KEY_Q);
+        reg(KEY_W, GLFW_KEY_W);
+        reg(KEY_E, GLFW_KEY_E);
+        reg(KEY_R, GLFW_KEY_R);
+        reg(KEY_T, GLFW_KEY_T);
+        reg(KEY_Y, GLFW_KEY_Y);
+        reg(KEY_U, GLFW_KEY_U);
+        reg(KEY_I, GLFW_KEY_I);
+        reg(KEY_O, GLFW_KEY_O);
+        reg(KEY_P, GLFW_KEY_P);
+        reg(KEY_LBRACKET, GLFW_KEY_LEFT_BRACKET);
+        reg(KEY_RBRACKET, GLFW_KEY_RIGHT_BRACKET);
+        reg(KEY_RETURN, GLFW_KEY_ENTER);
+        reg(KEY_LCONTROL, GLFW_KEY_LEFT_CONTROL);
+        reg(KEY_A, GLFW_KEY_A);
+        reg(KEY_S, GLFW_KEY_S);
+        reg(KEY_D, GLFW_KEY_D);
+        reg(KEY_F, GLFW_KEY_F);
+        reg(KEY_G, GLFW_KEY_G);
+        reg(KEY_H, GLFW_KEY_H);
+        reg(KEY_J, GLFW_KEY_J);
+        reg(KEY_K, GLFW_KEY_K);
+        reg(KEY_L, GLFW_KEY_L);
+        reg(KEY_SEMICOLON, GLFW_KEY_SEMICOLON);
+        reg(KEY_APOSTROPHE, GLFW_KEY_APOSTROPHE);
+        reg(KEY_GRAVE, GLFW_KEY_GRAVE_ACCENT);
+        reg(KEY_LSHIFT, GLFW_KEY_LEFT_SHIFT);
+        reg(KEY_BACKSLASH, GLFW_KEY_BACKSLASH);
+        reg(KEY_Z, GLFW_KEY_Z);
+        reg(KEY_X, GLFW_KEY_X);
+        reg(KEY_C, GLFW_KEY_C);
+        reg(KEY_V, GLFW_KEY_V);
+        reg(KEY_B, GLFW_KEY_B);
+        reg(KEY_N, GLFW_KEY_N);
+        reg(KEY_M, GLFW_KEY_M);
+        reg(KEY_COMMA, GLFW_KEY_COMMA);
+        reg(KEY_PERIOD, GLFW_KEY_PERIOD);
+        reg(KEY_SLASH, GLFW_KEY_SLASH);
+        reg(KEY_RSHIFT, GLFW_KEY_RIGHT_SHIFT);
+        reg(KEY_MULTIPLY, GLFW_KEY_KP_MULTIPLY);
+        reg(KEY_LMENU, GLFW_KEY_LEFT_ALT);
+        reg(KEY_SPACE, GLFW_KEY_SPACE);
+        reg(KEY_CAPITAL, GLFW_KEY_CAPS_LOCK);
+        reg(KEY_F1, GLFW_KEY_F1);
+        reg(KEY_F2, GLFW_KEY_F2);
+        reg(KEY_F3, GLFW_KEY_F3);
+        reg(KEY_F4, GLFW_KEY_F4);
+        reg(KEY_F5, GLFW_KEY_F5);
+        reg(KEY_F6, GLFW_KEY_F6);
+        reg(KEY_F7, GLFW_KEY_F7);
+        reg(KEY_F8, GLFW_KEY_F8);
+        reg(KEY_F9, GLFW_KEY_F9);
+        reg(KEY_F10, GLFW_KEY_F10);
+        reg(KEY_NUMLOCK, GLFW_KEY_NUM_LOCK);
+        reg(KEY_SCROLL, GLFW_KEY_SCROLL_LOCK);
+        reg(KEY_NUMPAD7, GLFW_KEY_KP_7);
+        reg(KEY_NUMPAD8, GLFW_KEY_KP_8);
+        reg(KEY_NUMPAD9, GLFW_KEY_KP_9);
+        reg(KEY_SUBTRACT, GLFW_KEY_KP_SUBTRACT);
+        reg(KEY_NUMPAD4, GLFW_KEY_KP_4);
+        reg(KEY_NUMPAD5, GLFW_KEY_KP_5);
+        reg(KEY_NUMPAD6, GLFW_KEY_KP_6);
+        reg(KEY_ADD, GLFW_KEY_KP_ADD);
+        reg(KEY_NUMPAD1, GLFW_KEY_KP_1);
+        reg(KEY_NUMPAD2, GLFW_KEY_KP_2);
+        reg(KEY_NUMPAD3, GLFW_KEY_KP_3);
+        reg(KEY_NUMPAD0, GLFW_KEY_KP_0);
+        reg(KEY_DECIMAL, GLFW_KEY_KP_DECIMAL);
+        reg(KEY_F11, GLFW_KEY_F11);
+        reg(KEY_F12, GLFW_KEY_F12);
+        reg(KEY_F13, GLFW_KEY_F13);
+        reg(KEY_F14, GLFW_KEY_F14);
+        reg(KEY_F15, GLFW_KEY_F15);
+        //reg(KEY_KANA, GLFW_KEY_);
+        //reg(KEY_CONVERT, GLFW_KEY_);
+        //reg(KEY_NOCONVERT, GLFW_KEY_);
+        //reg(KEY_YEN, GLFW_KEY_);
+        //reg(KEY_NUMPADEQUALS, GLFW_KEY_);
+        //reg(KEY_CIRCUMFLEX, GLFW_KEY_);
+        //reg(KEY_AT, GLFW_KEY_);
+        //reg(KEY_COLON, GLFW_KEY_);
+        //reg(KEY_UNDERLINE, GLFW_KEY_);
+        //reg(KEY_KANJI, GLFW_KEY_);
+        //reg(KEY_STOP, GLFW_KEY_);
+        //reg(KEY_AX, GLFW_KEY_);
+        //reg(KEY_UNLABELED, GLFW_KEY_);
+        reg(KEY_NUMPADENTER, GLFW_KEY_KP_ENTER);
+        reg(KEY_RCONTROL, GLFW_KEY_RIGHT_CONTROL);
+        //reg(KEY_NUMPADCOMMA, GLFW_KEY_);
+        reg(KEY_DIVIDE, GLFW_KEY_KP_DIVIDE);
+        reg(KEY_SYSRQ, GLFW_KEY_PRINT_SCREEN);
+        reg(KEY_RMENU, GLFW_KEY_RIGHT_ALT);
+        reg(KEY_PAUSE, GLFW_KEY_PAUSE);
+        reg(KEY_HOME, GLFW_KEY_HOME);
+        reg(KEY_UP, GLFW_KEY_UP);
+        reg(KEY_PRIOR, GLFW_KEY_PAGE_UP);
+        reg(KEY_LEFT, GLFW_KEY_LEFT);
+        reg(KEY_RIGHT, GLFW_KEY_RIGHT);
+        reg(KEY_END, GLFW_KEY_END);
+        reg(KEY_DOWN, GLFW_KEY_DOWN);
+        reg(KEY_NEXT, GLFW_KEY_PAGE_DOWN);
+        reg(KEY_INSERT, GLFW_KEY_INSERT);
+        reg(KEY_DELETE, GLFW_KEY_DELETE);
+        reg(KEY_LMETA, GLFW_KEY_LEFT_SUPER);
+        reg(KEY_RMETA, GLFW_KEY_RIGHT_SUPER);
+    }
+
+    public static int toJmeKeyCode(int glfwKey) {
+        return glfwToJmeKeyMap[glfwKey];
+    }
+}

+ 91 - 47
jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/GlfwMouseInput.java

@@ -38,16 +38,22 @@ import com.jme3.input.RawInputListener;
 import com.jme3.input.event.MouseButtonEvent;
 import com.jme3.input.event.MouseMotionEvent;
 import com.jme3.system.lwjgl.LwjglWindow;
+import com.jme3.util.BufferUtils;
 import org.lwjgl.glfw.GLFWCursorPosCallback;
 import org.lwjgl.glfw.GLFWMouseButtonCallback;
 import org.lwjgl.glfw.GLFWScrollCallback;
 
 import java.nio.ByteBuffer;
+import java.nio.IntBuffer;
+import java.util.HashMap;
 import java.util.LinkedList;
+import java.util.Map;
 import java.util.Queue;
 import java.util.logging.Logger;
 
 import static org.lwjgl.glfw.GLFW.*;
+import org.lwjgl.glfw.GLFWImage;
+import org.lwjgl.system.MemoryUtil;
 
 /**
  * Captures mouse input using GLFW callbacks. It then temporarily stores these in event queues which are processed in the
@@ -74,57 +80,70 @@ public class GlfwMouseInput implements MouseInput {
     private Queue<MouseMotionEvent> mouseMotionEvents = new LinkedList<MouseMotionEvent>();
     private Queue<MouseButtonEvent> mouseButtonEvents = new LinkedList<MouseButtonEvent>();
 
-    public GlfwMouseInput(final LwjglWindow context) {
+    private Map<JmeCursor, Long> jmeToGlfwCursorMap = new HashMap<JmeCursor, Long>();
+
+    public GlfwMouseInput(LwjglWindow context) {
         this.context = context;
     }
 
+    private void onCursorPos(long window, double xpos, double ypos) {
+        int xDelta;
+        int yDelta;
+        int x = (int) Math.round(xpos);
+        int y = context.getSettings().getHeight() - (int) Math.round(ypos);
+
+        if (mouseX == 0) {
+            mouseX = x;
+        }
+
+        if (mouseY == 0) {
+            mouseY = y;
+        }
+
+        xDelta = x - mouseX;
+        yDelta = y - mouseY;
+        mouseX = x;
+        mouseY = y;
+
+        if (xDelta != 0 || yDelta != 0) {
+            final MouseMotionEvent mouseMotionEvent = new MouseMotionEvent(x, y, xDelta, yDelta, mouseWheel, 0);
+            mouseMotionEvent.setTime(getInputTimeNanos());
+            mouseMotionEvents.add(mouseMotionEvent);
+        }
+    }
+
+    private void onWheelScroll(long window, double xOffset, double yOffset) {
+        mouseWheel += yOffset;
+        final MouseMotionEvent mouseMotionEvent = new MouseMotionEvent(mouseX, mouseY, 0, 0, mouseWheel, (int) Math.round(yOffset));
+        mouseMotionEvent.setTime(getInputTimeNanos());
+        mouseMotionEvents.add(mouseMotionEvent);
+    }
+
+    private void onMouseButton(final long window, final int button, final int action, final int mods) {
+        final MouseButtonEvent mouseButtonEvent = new MouseButtonEvent(convertButton(button), action == GLFW_PRESS, mouseX, mouseY);
+        mouseButtonEvent.setTime(getInputTimeNanos());
+        mouseButtonEvents.add(mouseButtonEvent);
+    }
+
     public void initialize() {
         glfwSetCursorPosCallback(context.getWindowHandle(), cursorPosCallback = new GLFWCursorPosCallback() {
             @Override
             public void invoke(long window, double xpos, double ypos) {
-                int xDelta;
-                int yDelta;
-                int x = (int) Math.round(xpos);
-                int y = context.getSettings().getHeight() - (int) Math.round(ypos);
-
-                if (mouseX == 0) {
-                    mouseX = x;
-                }
-
-                if (mouseY == 0) {
-                    mouseY = y;
-                }
-
-                xDelta = x - mouseX;
-                yDelta = y - mouseY;
-                mouseX = x;
-                mouseY = y;
-
-                if (xDelta != 0 || yDelta != 0) {
-                    final MouseMotionEvent mouseMotionEvent = new MouseMotionEvent(x, y, xDelta, yDelta, mouseWheel, 0);
-                    mouseMotionEvent.setTime(getInputTimeNanos());
-                    mouseMotionEvents.add(mouseMotionEvent);
-                }
+                onCursorPos(window, xpos, ypos);
             }
         });
 
         glfwSetScrollCallback(context.getWindowHandle(), scrollCallback = new GLFWScrollCallback() {
             @Override
             public void invoke(final long window, final double xOffset, final double yOffset) {
-                mouseWheel += yOffset;
-
-                final MouseMotionEvent mouseMotionEvent = new MouseMotionEvent(mouseX, mouseY, 0, 0, mouseWheel, (int) Math.round(yOffset));
-                mouseMotionEvent.setTime(getInputTimeNanos());
-                mouseMotionEvents.add(mouseMotionEvent);
+                onWheelScroll(window, xOffset, yOffset);
             }
         });
 
         glfwSetMouseButtonCallback(context.getWindowHandle(), mouseButtonCallback = new GLFWMouseButtonCallback() {
             @Override
             public void invoke(final long window, final int button, final int action, final int mods) {
-                final MouseButtonEvent mouseButtonEvent = new MouseButtonEvent(convertButton(button), action == GLFW_PRESS, mouseX, mouseY);
-                mouseButtonEvent.setTime(getInputTimeNanos());
-                mouseButtonEvents.add(mouseButtonEvent);
+                onMouseButton(window, button, action, mods);
             }
         });
 
@@ -160,6 +179,10 @@ public class GlfwMouseInput implements MouseInput {
         scrollCallback.release();
         mouseButtonCallback.release();
 
+        for (long glfwCursor : jmeToGlfwCursorMap.values()) {
+            glfwDestroyCursor(glfwCursor);
+        }
+
         logger.fine("Mouse destroyed.");
     }
 
@@ -185,31 +208,52 @@ public class GlfwMouseInput implements MouseInput {
         return (long) (glfwGetTime() * 1000000000);
     }
 
-    public void setNativeCursor(final JmeCursor jmeCursor) {
+    private long createGlfwCursor(JmeCursor jmeCursor) {
+        GLFWImage glfwImage = new GLFWImage(BufferUtils.createByteBuffer(GLFWImage.SIZEOF));
+
+        // TODO: currently animated cursors are not supported
+        IntBuffer imageData = jmeCursor.getImagesData();
+        ByteBuffer buf = BufferUtils.createByteBuffer(imageData.capacity());
+        buf.asIntBuffer().put(imageData);
+
+        glfwImage.set(jmeCursor.getWidth(), jmeCursor.getHeight(), buf);
+
+        return glfwCreateCursor(glfwImage, jmeCursor.getXHotSpot(), jmeCursor.getYHotSpot());
+    }
+
+    public void setNativeCursor(JmeCursor jmeCursor) {
         if (jmeCursor != null) {
-            final ByteBuffer byteBuffer = org.lwjgl.BufferUtils.createByteBuffer(jmeCursor.getImagesData().capacity());
-            byteBuffer.asIntBuffer().put(jmeCursor.getImagesData().array());
-            final long cursor = glfwCreateCursor(byteBuffer, jmeCursor.getXHotSpot(), jmeCursor.getYHotSpot());
-            glfwSetCursor(context.getWindowHandle(), cursor);
+            Long glfwCursor = jmeToGlfwCursorMap.get(jmeCursor);
+
+            if (glfwCursor == null) {
+                glfwCursor = createGlfwCursor(jmeCursor);
+                jmeToGlfwCursorMap.put(jmeCursor, glfwCursor);
+            }
+
+            glfwSetCursor(context.getWindowHandle(), glfwCursor);
+        } else {
+            glfwSetCursor(context.getWindowHandle(), MemoryUtil.NULL);
         }
     }
 
     /**
-     * Simply converts the GLFW button code to a JME button code. If there is no match it just returns the GLFW button
-     * code. Bare in mind GLFW supports 8 different mouse buttons.
+     * Simply converts the GLFW button code to a JME button code. If there is no
+     * match it just returns the GLFW button code. Bear in mind GLFW supports 8
+     * different mouse buttons.
      *
      * @param glfwButton the raw GLFW button index.
      * @return the mapped {@link MouseInput} button id.
      */
     private int convertButton(final int glfwButton) {
-        if (glfwButton == GLFW_MOUSE_BUTTON_LEFT) {
-            return MouseInput.BUTTON_LEFT;
-        } else if(glfwButton == GLFW_MOUSE_BUTTON_MIDDLE) {
-            return MouseInput.BUTTON_MIDDLE;
-        } else if(glfwButton == GLFW_MOUSE_BUTTON_RIGHT) {
-            return MouseInput.BUTTON_RIGHT;
+        switch (glfwButton) {
+            case GLFW_MOUSE_BUTTON_LEFT:
+                return MouseInput.BUTTON_LEFT;
+            case GLFW_MOUSE_BUTTON_MIDDLE:
+                return MouseInput.BUTTON_MIDDLE;
+            case GLFW_MOUSE_BUTTON_RIGHT:
+                return MouseInput.BUTTON_RIGHT;
+            default:
+                return glfwButton;
         }
-
-        return glfwButton;
     }
 }

+ 16 - 12
jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java

@@ -43,9 +43,7 @@ import com.jme3.renderer.lwjgl.LwjglGLFboEXT;
 import com.jme3.renderer.lwjgl.LwjglGLFboGL3;
 import com.jme3.renderer.opengl.*;
 import com.jme3.system.*;
-import org.lwjgl.Sys;
 import org.lwjgl.glfw.GLFW;
-import org.lwjgl.opengl.ARBDebugOutput;
 import org.lwjgl.opengl.ARBFramebufferObject;
 import org.lwjgl.opengl.EXTFramebufferMultisample;
 import org.lwjgl.opengl.GLCapabilities;
@@ -53,9 +51,10 @@ import org.lwjgl.opengl.GLCapabilities;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.logging.Level;
 import java.util.logging.Logger;
+import static org.lwjgl.glfw.GLFW.GLFW_TRUE;
+import org.lwjgl.opengl.ARBDebugOutput;
 
 import static org.lwjgl.opengl.GL.createCapabilities;
-import static org.lwjgl.opengl.GL11.GL_TRUE;
 import static org.lwjgl.opengl.GL11.glGetInteger;
 
 /**
@@ -84,16 +83,16 @@ public abstract class LwjglContext implements JmeContext {
     }
 
     protected void printContextInitInfo() {
-        logger.log(Level.INFO, "LWJGL {0} context running on thread {1}\n" +
-                        " * Graphics Adapter: GLFW {2}",
-                new Object[]{Sys.getVersion(), Thread.currentThread().getName(), GLFW.glfwGetVersionString()});
+        logger.log(Level.INFO, "LWJGL {0} context running on thread {1}\n"
+                + " * Graphics Adapter: GLFW {2}",
+                new Object[]{org.lwjgl.Version.getVersion(), Thread.currentThread().getName(), GLFW.glfwGetVersionString()});
     }
 
     protected int determineMaxSamples() {
         // If we already have a valid context, determine samples using current context.
-        if (GLFW.glfwExtensionSupported("GL_ARB_framebuffer_object") == GL_TRUE) {
+        if (GLFW.glfwExtensionSupported("GL_ARB_framebuffer_object") == GLFW_TRUE) {
             return glGetInteger(ARBFramebufferObject.GL_MAX_SAMPLES);
-        } else if (GLFW.glfwExtensionSupported("GL_EXT_framebuffer_multisample") == GL_TRUE) {
+        } else if (GLFW.glfwExtensionSupported("GL_EXT_framebuffer_multisample") == GLFW_TRUE) {
             return glGetInteger(EXTFramebufferMultisample.GL_MAX_SAMPLES_EXT);
         }
 
@@ -180,11 +179,11 @@ public abstract class LwjglContext implements JmeContext {
         }
 
         if (capabilities.GL_ARB_debug_output && settings.getBoolean("GraphicsDebug")) {
-            ARBDebugOutput.glDebugMessageCallbackARB(new LwjglGLDebugOutputHandler(), 0); // User param is zero. Not sure what we could use that for.
+            ARBDebugOutput.glDebugMessageCallbackARB(new LwjglGLDebugOutputHandler(), 0);
         }
 
-        renderer.setMainFrameBufferSrgb(settings.getGammaCorrection());
-        renderer.setLinearizeSrgbImages(settings.getGammaCorrection());
+        renderer.setMainFrameBufferSrgb(settings.isGammaCorrection());
+        renderer.setLinearizeSrgbImages(settings.isGammaCorrection());
 
         // Init input
         if (keyInput != null) {
@@ -198,7 +197,6 @@ public abstract class LwjglContext implements JmeContext {
         if (joyInput != null) {
             joyInput.initialize();
         }
-
         renderable.set(true);
     }
 
@@ -240,26 +238,32 @@ public abstract class LwjglContext implements JmeContext {
         }
     }
 
+    @Override
     public boolean isCreated() {
         return created.get();
     }
 
+    @Override
     public boolean isRenderable() {
         return renderable.get();
     }
 
+    @Override
     public void setSettings(AppSettings settings) {
         this.settings.copyFrom(settings);
     }
 
+    @Override
     public AppSettings getSettings() {
         return settings;
     }
 
+    @Override
     public Renderer getRenderer() {
         return renderer;
     }
 
+    @Override
     public Timer getTimer() {
         return timer;
     }

+ 84 - 45
jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java

@@ -43,7 +43,6 @@ import com.jme3.system.AppSettings;
 import com.jme3.system.JmeContext;
 import com.jme3.system.JmeSystem;
 import com.jme3.system.NanoTimer;
-import org.lwjgl.Sys;
 import org.lwjgl.glfw.*;
 
 import java.awt.*;
@@ -52,6 +51,7 @@ import java.nio.ByteBuffer;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.logging.Level;
 import java.util.logging.Logger;
+import org.lwjgl.Version;
 
 import static org.lwjgl.glfw.GLFW.*;
 import static org.lwjgl.opengl.GL11.GL_FALSE;
@@ -72,7 +72,7 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable {
     protected boolean wasActive = false;
     protected boolean autoFlush = true;
     protected boolean allowSwapBuffers = false;
-    private long window = -1;
+    private long window = NULL;
     private final JmeContext.Type type;
     private int frameRateLimit = -1;
     private double frameSleepTime;
@@ -102,7 +102,7 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable {
      * @param title the title to set
      */
     public void setTitle(final String title) {
-        if (created.get() && window != -1) {
+        if (created.get() && window != NULL) {
             glfwSetWindowTitle(window, title);
         }
     }
@@ -127,45 +127,45 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable {
         glfwSetErrorCallback(errorCallback = new GLFWErrorCallback() {
             @Override
             public void invoke(int error, long description) {
-                final String message = Callbacks.errorCallbackDescriptionString(description);
+                final String message = GLFWErrorCallback.getDescription(description);
                 listener.handleError(message, new Exception(message));
             }
         });
 
-        if (glfwInit() != GL_TRUE) {
+        if (glfwInit() != GLFW_TRUE) {
             throw new IllegalStateException("Unable to initialize GLFW");
         }
 
         glfwDefaultWindowHints();
-        glfwWindowHint(GLFW_VISIBLE, GL_FALSE);
 
-        // TODO: Add support for monitor selection
-        long monitor = NULL;
-
-        if (settings.isFullscreen()) {
-            monitor = glfwGetPrimaryMonitor();
+        if (settings.getRenderer().equals(AppSettings.LWJGL_OPENGL3)) {
+            glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
+            glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
+            glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
+            glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
+        } else {
+            glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);
+            glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
         }
 
-        final ByteBuffer videoMode = glfwGetVideoMode(glfwGetPrimaryMonitor());
-
-        if (settings.getWidth() <= 0 || settings.getHeight() <= 0) {
-            settings.setResolution(GLFWvidmode.width(videoMode), GLFWvidmode.height(videoMode));
+        if (settings.getBoolean("RendererDebug")) {
+            glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GLFW_TRUE);
         }
 
-        window = glfwCreateWindow(settings.getWidth(), settings.getHeight(), settings.getTitle(), monitor, NULL);
-
-        if (window == NULL) {
-            throw new RuntimeException("Failed to create the GLFW window");
+        if (settings.isGammaCorrection()) {
+            glfwWindowHint(GLFW_SRGB_CAPABLE, GLFW_TRUE);
         }
 
-        glfwWindowHint(GLFW_RESIZABLE, settings.isResizable() ? GL_TRUE : GL_FALSE);
+        glfwWindowHint(GLFW_VISIBLE, GL_FALSE);
+        glfwWindowHint(GLFW_RESIZABLE, settings.isResizable() ? GLFW_TRUE : GLFW_FALSE);
+
+        glfwWindowHint(GLFW_DOUBLE_BUFFER, GLFW_TRUE);
         glfwWindowHint(GLFW_DEPTH_BITS, settings.getDepthBits());
         glfwWindowHint(GLFW_STENCIL_BITS, settings.getStencilBits());
         glfwWindowHint(GLFW_SAMPLES, settings.getSamples());
-        glfwWindowHint(GLFW_STEREO, settings.useStereo3D() ? GL_TRUE : GL_FALSE);
+        glfwWindowHint(GLFW_STEREO, settings.useStereo3D() ? GLFW_TRUE : GLFW_FALSE);
         glfwWindowHint(GLFW_REFRESH_RATE, settings.getFrequency());
 
-        // Not sure how else to support bits per pixel
         if (settings.getBitsPerPixel() == 24) {
             glfwWindowHint(GLFW_RED_BITS, 8);
             glfwWindowHint(GLFW_GREEN_BITS, 8);
@@ -178,6 +178,34 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable {
 
         glfwWindowHint(GLFW_ALPHA_BITS, settings.getAlphaBits());
 
+        // TODO: Add support for monitor selection
+        long monitor = NULL;
+
+        if (settings.isFullscreen()) {
+            monitor = glfwGetPrimaryMonitor();
+        }
+
+        final GLFWVidMode videoMode = glfwGetVideoMode(glfwGetPrimaryMonitor());
+
+        if (settings.getWidth() <= 0 || settings.getHeight() <= 0) {
+            settings.setResolution(videoMode.width(), videoMode.height());
+        }
+
+        window = glfwCreateWindow(settings.getWidth(), settings.getHeight(), settings.getTitle(), monitor, NULL);
+
+        if (window == NULL) {
+            throw new RuntimeException("Failed to create the GLFW window");
+        }
+
+        // Add a resize callback which delegates to the listener
+        glfwSetWindowSizeCallback(window, windowSizeCallback = new GLFWWindowSizeCallback() {
+            @Override
+            public void invoke(final long window, final int width, final int height) {
+                settings.setResolution(width, height);
+                listener.reshape(width, height);
+            }
+        });
+
         glfwSetWindowFocusCallback(window, windowFocusCallback = new GLFWWindowFocusCallback() {
             @Override
             public void invoke(final long window, final int focused) {
@@ -197,8 +225,10 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable {
         });
 
         // Center the window
-        if (!settings.isFullscreen() && Type.Display.equals(type)) {
-            glfwSetWindowPos(window, (GLFWvidmode.width(videoMode) - settings.getWidth()) / 2, (GLFWvidmode.height(videoMode) - settings.getHeight()) / 2);
+        if (!settings.isFullscreen()) {
+            glfwSetWindowPos(window,
+                    (videoMode.width() - settings.getWidth()) / 2,
+                    (videoMode.height() - settings.getHeight()) / 2);
         }
 
         // Make the OpenGL context current
@@ -216,14 +246,7 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable {
             glfwShowWindow(window);
         }
 
-        // Add a resize callback which delegates to the listener
-        glfwSetWindowSizeCallback(window, windowSizeCallback = new GLFWWindowSizeCallback() {
-            @Override
-            public void invoke(final long window, final int width, final int height) {
-                settings.setResolution(width, height);
-                listener.reshape(width, height);
-            }
-        });
+        glfwShowWindow(window);
 
         allowSwapBuffers = settings.isSwapBuffers();
 
@@ -239,12 +262,24 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable {
                 renderer.cleanup();
             }
 
-            errorCallback.release();
-            windowSizeCallback.release();
-            windowFocusCallback.release();
+            if (errorCallback != null) {
+                errorCallback.release();
+                errorCallback = null;
+            }
+
+            if (windowSizeCallback != null) {
+                windowSizeCallback.release();
+                windowSizeCallback = null;
+            }
 
-            if (window != 0) {
+            if (windowFocusCallback != null) {
+                windowFocusCallback.release();
+                windowFocusCallback = null;
+            }
+
+            if (window != NULL) {
                 glfwDestroyWindow(window);
+                window = NULL;
             }
         } catch (Exception ex) {
             listener.handleError("Failed to destroy context", ex);
@@ -296,8 +331,9 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable {
             super.internalCreate();
         } catch (Exception ex) {
             try {
-                if (window != -1) {
+                if (window != NULL) {
                     glfwDestroyWindow(window);
+                    window = NULL;
                 }
             } catch (Exception ex2) {
                 LOGGER.log(Level.WARNING, null, ex2);
@@ -318,6 +354,7 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable {
         // If a restart is required, lets recreate the context.
         if (needRestart.getAndSet(false)) {
             try {
+                destroyContext();
                 createContext(settings);
             } catch (Exception ex) {
                 LOGGER.log(Level.SEVERE, "Failed to set display settings!", ex);
@@ -346,8 +383,6 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable {
             }
         }
 
-        glfwPollEvents();
-
         // Subclasses just call GLObjectManager clean up objects here
         // it is safe .. for now.
         if (renderer != null) {
@@ -377,6 +412,8 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable {
                 }
             }
         }
+
+        glfwPollEvents();
     }
 
     private void setFrameRateLimit(int frameRateLimit) {
@@ -389,11 +426,12 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable {
      */
 
     protected void deinitInThread() {
+        listener.destroy();
+
         destroyContext();
+        super.internalDestroy();
 
-        listener.destroy();
         LOGGER.fine("Display destroyed.");
-        super.internalDestroy();
     }
 
     public void run() {
@@ -403,7 +441,7 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable {
         }
 
         loadNatives();
-        LOGGER.log(Level.FINE, "Using LWJGL {0}", Sys.getVersion());
+        LOGGER.log(Level.FINE, "Using LWJGL {0}", Version.getVersion());
 
         if (!initInThread()) {
             LOGGER.log(Level.SEVERE, "Display initialization failed. Cannot continue.");
@@ -411,15 +449,16 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable {
         }
 
         while (true) {
-            if (glfwWindowShouldClose(window) == GL_TRUE) {
-                listener.requestClose(false);
-            }
 
             runLoop();
 
             if (needClose.get()) {
                 break;
             }
+
+            if (glfwWindowShouldClose(window) == GL_TRUE) {
+                listener.requestClose(false);
+            }
         }
 
         deinitInThread();

+ 31 - 22
jme3-networking/src/main/java/com/jme3/network/base/DefaultClient.java

@@ -301,35 +301,38 @@ public class DefaultClient implements Client
 
     protected void closeConnections( DisconnectInfo info )
     {
-        if( !isRunning )
-            return;
+        synchronized(this) {
+            if( !isRunning )
+                return;
 
-        if( services.isStarted() ) {
-            // Let the services get a chance to stop before we
-            // kill the connection.
-            services.stop();
-        }
+            if( services.isStarted() ) {
+                // Let the services get a chance to stop before we
+                // kill the connection.
+                services.stop();
+            }
         
-        // Send a close message
+            // Send a close message
     
-        // Tell the thread it's ok to die
-        for( ConnectorAdapter ca : channels ) {
-            if( ca == null )
-                continue;
-            ca.close();
-        }
+            // Tell the thread it's ok to die
+            for( ConnectorAdapter ca : channels ) {
+                if( ca == null )
+                    continue;
+                ca.close();
+            }
         
-        // Wait for the threads?
+            // Wait for the threads?
 
-        // Just in case we never fully connected
-        connecting.countDown();
+            // Just in case we never fully connected
+            connecting.countDown();
         
-        fireDisconnected(info);
+            isRunning = false;
         
-        isRunning = false;
+            // Terminate the services
+            services.terminate();            
+        }
         
-        // Terminate the services
-        services.terminate();            
+        // Make sure we aren't synched while firing events
+        fireDisconnected(info);        
     }         
 
     @Override
@@ -462,11 +465,17 @@ public class DefaultClient implements Client
                 this.id = (int)crm.getId();
                 log.log( Level.FINE, "Connection established, id:{0}.", this.id );
                 connecting.countDown();
-                fireConnected();
+                //fireConnected();
             } else {
                 // Else it's a message letting us know that the 
                 // hosted services have been started
                 startServices();
+ 
+                // Delay firing 'connected' until the services have all
+                // been started to avoid odd race conditions.  If there is some
+                // need to get some kind of event before the services have been
+                // started then we should create a new event step.               
+                fireConnected();
             }
             return;
         } else if( m instanceof ChannelInfoMessage ) {

+ 1 - 1
jme3-networking/src/main/java/com/jme3/network/base/DefaultServer.java

@@ -608,7 +608,7 @@ public class DefaultServer implements Server
             // should always already be closed through all paths that I
             // can conceive... but it doesn't hurt to be sure. 
             for( Endpoint p : channels ) {
-                if( p == null ) 
+                if( p == null || !p.isConnected() ) 
                     continue;
                 p.close();
             }

+ 7 - 3
jme3-networking/src/main/java/com/jme3/network/base/KernelAdapter.java

@@ -112,6 +112,8 @@ public class KernelAdapter extends Thread
         
         // Kill the kernel
         kernel.terminate();
+        
+        join();
     }
 
     protected void reportError( Endpoint p, Object context, Exception e )
@@ -119,9 +121,11 @@ public class KernelAdapter extends Thread
         // Should really be queued up so the outer thread can
         // retrieve them.  For now we'll just log it.  FIXME
         log.log( Level.SEVERE, "Unhandled error, endpoint:" + p + ", context:" + context, e );
-        
-        // In lieu of other options, at least close the endpoint
-        p.close();
+
+        if( p.isConnected() ) {
+            // In lieu of other options, at least close the endpoint
+            p.close();
+        }
     }                                                      
 
     protected HostedConnection getConnection( Endpoint p )

+ 1 - 1
jme3-networking/src/main/java/com/jme3/network/base/MessageProtocol.java

@@ -181,7 +181,7 @@ public class MessageProtocol
             Message m = (Message)obj;
             messages.add(m);
         } catch( IOException e ) {
-            throw new RuntimeException( "Error deserializing object, clas ID:" + buffer.getShort(0), e );   
+            throw new RuntimeException( "Error deserializing object, class ID:" + buffer.getShort(0), e );   
         }         
     }
 }

+ 12 - 0
jme3-networking/src/main/java/com/jme3/network/kernel/AbstractKernel.java

@@ -76,6 +76,18 @@ public abstract class AbstractKernel implements Kernel
         log.log( Level.SEVERE, "Unhanddled kernel error", e );
     }
 
+    protected void wakeupReader() {
+        // If there are no pending messages then add one so that the
+        // kernel-user knows to wake up if it is only listening for
+        // envelopes.
+        if( !hasEnvelopes() ) {
+            // Note: this is not really a race condition.  At worst, our
+            // event has already been handled by now and it does no harm
+            // to check again.
+            addEnvelope( EVENTS_PENDING );
+        }
+    }
+
     protected long nextEndpointId()
     {
         return nextId.getAndIncrement();

+ 4 - 9
jme3-networking/src/main/java/com/jme3/network/kernel/tcp/SelectorKernel.java

@@ -106,6 +106,9 @@ public class SelectorKernel extends AbstractKernel
         try {
             thread.close();
             thread = null;
+            
+            // Need to let any caller waiting for a read() wakeup 
+            wakeupReader();       
         } catch( IOException e ) {
             throw new KernelException( "Error closing host connection:" + address, e );
         }
@@ -164,15 +167,7 @@ public class SelectorKernel extends AbstractKernel
         // Enqueue an endpoint event for the listeners
         addEvent( EndpointEvent.createRemove( this, p ) );
 
-        // If there are no pending messages then add one so that the
-        // kernel-user knows to wake up if it is only listening for
-        // envelopes.
-        if( !hasEnvelopes() ) {
-            // Note: this is not really a race condition.  At worst, our
-            // event has already been handled by now and it does no harm
-            // to check again.
-            addEnvelope( EVENTS_PENDING );
-        }
+        wakeupReader();
     }
 
     /**

+ 5 - 10
jme3-networking/src/main/java/com/jme3/network/kernel/udp/UdpKernel.java

@@ -110,6 +110,9 @@ public class UdpKernel extends AbstractKernel
             thread.close();
             writer.shutdown();
             thread = null;
+            
+            // Need to let any caller waiting for a read() wakeup 
+            wakeupReader();       
         } catch( IOException e ) {
             throw new KernelException( "Error closing host connection:" + address, e );
         }
@@ -169,16 +172,8 @@ public class UdpKernel extends AbstractKernel
         log.log( Level.FINE, "Socket endpoints size:{0}", socketEndpoints.size() );
 
         addEvent( EndpointEvent.createRemove( this, p ) );
-        
-        // If there are no pending messages then add one so that the
-        // kernel-user knows to wake up if it is only listening for
-        // envelopes.
-        if( !hasEnvelopes() ) {
-            // Note: this is not really a race condition.  At worst, our
-            // event has already been handled by now and it does no harm
-            // to check again.
-            addEnvelope( EVENTS_PENDING );
-        }
+ 
+        wakeupReader();       
     }
 
     protected void newData( DatagramPacket packet )

+ 5 - 3
jme3-networking/src/main/java/com/jme3/network/message/SerializerRegistrationsMessage.java

@@ -146,15 +146,17 @@ public class SerializerRegistrationsMessage extends AbstractMessage {
             // that also run their own servers but realistically they would have
             // to disable the ServerSerializerRegistrationsServer anyway.
             if( compiled != null ) {
-                log.log( Level.INFO, "Skipping registration as registry is locked, presumably by a local server process.");
+                log.log(Level.INFO, "Skipping registration as registry is locked, presumably by a local server process.");
                 return;
             }
         }
         
+        log.log(Level.FINE, "Registering {0} classes...", registrations.length);
         for( Registration reg : registrations ) {
-            log.log( Level.INFO, "Registering:{0}", reg);
+            log.log(Level.INFO, "Registering:{0}", reg);
             reg.register();
         }
+        log.log(Level.FINE, "Done registering serializable classes.");
     }
     
     @Serializable
@@ -187,7 +189,7 @@ public class SerializerRegistrationsMessage extends AbstractMessage {
                     serializer = (Serializer)serializerType.newInstance();                    
                 }
                 SerializerRegistration result = Serializer.registerClassForId(id, type, serializer);
-                log.log( Level.FINE, "   result:{0}", result);                
+                log.log(Level.FINE, "   result:{0}", result);                
             } catch( ClassNotFoundException e ) {
                 throw new RuntimeException( "Class not found attempting to register:" + this, e );
             } catch( InstantiationException e ) {

+ 18 - 2
jme3-networking/src/main/java/com/jme3/network/serializing/Serializer.java

@@ -130,7 +130,7 @@ public abstract class Serializer {
         registerClass(IdentityHashMap.class,            new MapSerializer());
         registerClass(TreeMap.class,                    new MapSerializer());
         registerClass(WeakHashMap.class,                new MapSerializer());
-        
+ 
         registerClass(Enum.class,      new EnumSerializer());
         registerClass(GZIPCompressedMessage.class, new GZIPSerializer());
         registerClass(ZIPCompressedMessage.class, new ZIPSerializer());
@@ -331,7 +331,7 @@ public abstract class Serializer {
     @SuppressWarnings("unchecked")
     public static SerializerRegistration getSerializerRegistration(Class cls, boolean failOnMiss) {
         SerializerRegistration reg = classRegistrations.get(cls);
-
+        
         if (reg != null) return reg;
 
         for (Map.Entry<Class, SerializerRegistration> entry : classRegistrations.entrySet()) {
@@ -425,6 +425,22 @@ public abstract class Serializer {
             return;
         }
         SerializerRegistration reg = writeClass(buffer, object.getClass());
+        
+        // If the caller (or us) has registered a generic base class (like Enum)
+        // that is meant to steer automatic resolution for things like FieldSerializer
+        // that have final classes in fields... then there are cases where the exact 
+        // type isn't known by the outer class.  (Think of a message object
+        // that has an Object field but tries to send an Enum subclass in it.)
+        // In that case, the SerializerRegistration object we get back isn't
+        // really going to be capable of recreating the object on the other
+        // end because it won't know what class to use.  This only comes up
+        // in writeclassAndObejct() because we just wrote an ID to a more generic
+        // class than will be readable on the other end.  The check is simple, though.
+        if( reg.getType() != object.getClass() ) {
+            throw new IllegalArgumentException("Class has not been registered:" 
+                    + object.getClass() + " but resolved to generic serializer for:" + reg.getType());
+        } 
+
         reg.getSerializer().writeObject(buffer, object);
     }
 

+ 3 - 2
jme3-networking/src/main/java/com/jme3/network/serializing/serializers/EnumSerializer.java

@@ -48,8 +48,9 @@ public class EnumSerializer extends Serializer {
 
             if (ordinal == -1) return null;
             T[] enumConstants = c.getEnumConstants();
-            if (enumConstants == null)
-                throw new SerializerException( "Class has no enum constants:" + c );
+            if (enumConstants == null) {
+                throw new SerializerException("Class has no enum constants:" + c + "  Ordinal:" + ordinal);
+            }
             return enumConstants[ordinal];
         } catch (IndexOutOfBoundsException ex) {
             return null;

+ 34 - 5
jme3-networking/src/main/java/com/jme3/network/serializing/serializers/FieldSerializer.java

@@ -34,11 +34,14 @@ package com.jme3.network.serializing.serializers;
 import com.jme3.network.serializing.Serializer;
 import com.jme3.network.serializing.SerializerException;
 import java.io.IOException;
+import java.lang.reflect.Constructor;
 import java.lang.reflect.Field;
 import java.lang.reflect.Modifier;
 import java.nio.BufferOverflowException;
 import java.nio.ByteBuffer;
 import java.util.*;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 /**
  * The field serializer is the default serializer used for custom class.
@@ -46,16 +49,35 @@ import java.util.*;
  * @author Lars Wesselius, Nathan Sweet
  */
 public class FieldSerializer extends Serializer {
+    
+    static final Logger log = Logger.getLogger(FieldSerializer.class.getName());
+
     private static Map<Class, SavedField[]> savedFields = new HashMap<Class, SavedField[]>();
+    private static Map<Class, Constructor> savedCtors = new HashMap<Class, Constructor>();
 
     protected void checkClass(Class clazz) {
     
         // See if the class has a public no-arg constructor
         try {
-            clazz.getConstructor();
+            savedCtors.put(clazz, clazz.getConstructor());
+            return;
+        } catch( NoSuchMethodException e ) {
+            //throw new RuntimeException( "Registration error: no-argument constructor not found on:" + clazz ); 
+        }
+        
+        // See if it has a non-public no-arg constructor
+        try {
+            Constructor ctor = clazz.getDeclaredConstructor();
+            
+            // Make sure we can call it later.
+            ctor.setAccessible(true);
+             
+            savedCtors.put(clazz, ctor);
+            return;
         } catch( NoSuchMethodException e ) {
-            throw new RuntimeException( "Registration error: no-argument constructor not found on:" + clazz ); 
-        } 
+        }
+        
+        throw new RuntimeException( "Registration error: no-argument constructor not found on:" + clazz );  
     }        
     
     public void initialize(Class clazz) {
@@ -121,7 +143,8 @@ public class FieldSerializer extends Serializer {
 
         T object;
         try {
-            object = c.newInstance();
+            Constructor<T> ctor = (Constructor<T>)savedCtors.get(c);
+            object = ctor.newInstance();
         } catch (Exception e) {
             throw new SerializerException( "Error creating object of type:" + c, e );
         }
@@ -129,6 +152,9 @@ public class FieldSerializer extends Serializer {
         for (SavedField savedField : fields) {
             Field field = savedField.field;
             Serializer serializer = savedField.serializer;
+            if( log.isLoggable(Level.FINER) ) {
+                log.log(Level.FINER, "Reading field:{0} using serializer:{1}", new Object[]{field, serializer});
+            }
             Object value;
 
             if (serializer != null) {
@@ -164,9 +190,12 @@ public class FieldSerializer extends Serializer {
             try {
                 val = savedField.field.get(object);
             } catch (IllegalAccessException e) {
-                e.printStackTrace();
+                throw new SerializerException("Unable to access field:" + savedField.field + " on:" + object, e);
             }
             Serializer serializer = savedField.serializer;
+            if( log.isLoggable(Level.FINER) ) {
+                log.log(Level.FINER, "Writing field:{0} using serializer:{1}", new Object[]{savedField.field, serializer});
+            }
 
             try {
                 if (serializer != null) {

+ 5 - 0
jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiRegistry.java

@@ -351,6 +351,11 @@ public class RmiRegistry {
         }
         
         public Object invoke( short procId, Object[] args ) {
+            if( log.isLoggable(Level.FINEST) ) {
+                log.finest("SharedObject->invoking:" + classInfo.getMethod(procId) 
+                           + " on:" + object 
+                           + " with:" + (args == null ? "null" : Arrays.asList(args))); 
+            }
             return classInfo.getMethod(procId).invoke(object, args);
         }
     }

+ 6 - 1
jme3-networking/src/main/java/com/jme3/network/service/rpc/RpcConnection.java

@@ -184,7 +184,7 @@ public class RpcConnection {
     
         if( log.isLoggable(Level.FINEST) ) {
             log.log(Level.FINEST, "handleMessage({0})", msg);
-        }    
+        }
         RpcHandler handler = handlers.get(msg.getObjectId());
         try {
             if( handler == null ) {
@@ -225,6 +225,7 @@ public class RpcConnection {
     private class ResponseHolder {
         private Object response;
         private String error;
+        private Throwable exception;
         private RpcCallMessage msg;
         boolean received = false;
  
@@ -235,6 +236,7 @@ public class RpcConnection {
         public synchronized void setResponse( RpcResponseMessage msg ) {
             this.response = msg.getResult();
             this.error = msg.getError();
+            this.exception = msg.getThrowable();
             this.received = true;
             notifyAll();
         }
@@ -250,6 +252,9 @@ public class RpcConnection {
             if( error != null ) {
                 throw new RuntimeException("Error calling remote procedure:" + msg + "\n" + error);
             }
+            if( exception != null ) {
+                throw new RuntimeException("Error calling remote procedure:" + msg, exception);
+            } 
             return response;              
         }
         

+ 32 - 6
jme3-networking/src/main/java/com/jme3/network/service/rpc/msg/RpcResponseMessage.java

@@ -34,6 +34,7 @@ package com.jme3.network.service.rpc.msg;
 
 import com.jme3.network.AbstractMessage;
 import com.jme3.network.serializing.Serializable;
+import com.jme3.network.serializing.Serializer;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 
@@ -50,6 +51,7 @@ public class RpcResponseMessage extends AbstractMessage {
     private long msgId;
     private Object result;
     private String error;
+    private Object exception; // if it was serializable
 
     public RpcResponseMessage() {
     }
@@ -61,12 +63,31 @@ public class RpcResponseMessage extends AbstractMessage {
 
     public RpcResponseMessage( long msgId, Throwable t ) {
         this.msgId = msgId;
-         
-        StringWriter sOut = new StringWriter();
-        PrintWriter out = new PrintWriter(sOut);
-        t.printStackTrace(out);
-        out.close();
-        this.error = sOut.toString();
+ 
+        // See if the exception is serializable
+        if( isSerializable(t) ) {
+            // Can send the exception itself
+            this.exception = t;
+        } else {
+            // We'll compose all of the info into a string           
+            StringWriter sOut = new StringWriter();
+            PrintWriter out = new PrintWriter(sOut);
+            t.printStackTrace(out);
+            out.close();
+            this.error = sOut.toString();
+        }
+    }
+ 
+    public static boolean isSerializable( Throwable error ) {
+        if( error == null ) {
+            return false;
+        }
+        for( Throwable t = error; t != null; t = t.getCause() ) {
+            if( Serializer.getExactSerializerRegistration(t.getClass()) == null ) {
+                return false;
+            }
+        }
+        return true; 
     }
  
     public long getMessageId() {
@@ -81,10 +102,15 @@ public class RpcResponseMessage extends AbstractMessage {
         return error;
     }
     
+    public Throwable getThrowable() {
+        return (Throwable)exception;
+    }
+    
     @Override
     public String toString() {
         return getClass().getSimpleName() + "[#" + msgId + ", result=" + result
                                           + (error != null ? ", error=" + error : "")
+                                          + (exception != null ? ", exception=" + exception : "")
                                           + "]";
     }
 }

+ 1 - 1
jme3-networking/src/main/java/com/jme3/network/service/serializer/ServerSerializerRegistrationsService.java

@@ -65,7 +65,7 @@ public class ServerSerializerRegistrationsService extends AbstractHostedService
     public void connectionAdded(Server server, HostedConnection hc) {
         // Just in case
         super.connectionAdded(server, hc);
-        
+ 
         // Send the client the registration information
         hc.send(SerializerRegistrationsMessage.INSTANCE);
     }

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

+ 2 - 2
sdk/jme3-assetpack-support/src/com/jme3/gde/assetpack/browser/Bundle.properties

@@ -1,6 +1,6 @@
 CTL_AssetPackBrowserAction=AssetPackBrowser
-CTL_AssetPackBrowserTopComponent=AssetPackBrowser Window
-HINT_AssetPackBrowserTopComponent=This is a AssetPackBrowser window
+CTL_AssetPackBrowserTopComponent=AssetPackBrowser
+HINT_AssetPackBrowserTopComponent=The AssetPackBrowser allows easy managing of your AssetPacks
 AssetPackBrowserTopComponent.jTextField1.text=search
 AssetPackBrowserTopComponent.jButton1.text=update
 AssetPackBrowserTopComponent.jButton2.text=online assetpacks

+ 2 - 2
sdk/jme3-core/src/com/jme3/gde/core/appstates/AppStateExplorerTopComponent.java

@@ -67,8 +67,8 @@ persistenceType = TopComponent.PERSISTENCE_ALWAYS)
 preferredID = "AppStateExplorerTopComponent")
 @Messages({
     "CTL_AppStateExplorerAction=AppStateExplorer",
-    "CTL_AppStateExplorerTopComponent=AppStateExplorer Window",
-    "HINT_AppStateExplorerTopComponent=This is a AppStateExplorer window"
+    "CTL_AppStateExplorerTopComponent=AppStateExplorer",
+    "HINT_AppStateExplorerTopComponent=The AppStateExplorer provides an Overview over your current AppState"
 })
 public final class AppStateExplorerTopComponent extends TopComponent implements ExplorerManager.Provider {
 

+ 2 - 2
sdk/jme3-core/src/com/jme3/gde/core/filters/Bundle.properties

@@ -1,3 +1,3 @@
 CTL_FilterExplorerAction=FilterExplorer
-CTL_FilterExplorerTopComponent=FilterExplorer Window
-HINT_FilterExplorerTopComponent=This is a FilterExplorer window
+CTL_FilterExplorerTopComponent=FilterExplorer
+HINT_FilterExplorerTopComponent=The FilterExplorer provides an Overview over your current Filter

+ 2 - 2
sdk/jme3-core/src/com/jme3/gde/core/sceneexplorer/Bundle.properties

@@ -1,4 +1,4 @@
 CTL_SceneExplorerAction=SceneExplorer
-CTL_SceneExplorerTopComponent=SceneExplorer Window
-HINT_SceneExplorerTopComponent=This is a SceneExplorer window
+CTL_SceneExplorerTopComponent=SceneExplorer
+HINT_SceneExplorerTopComponent=The SceneExplorer provides an Overview over the SceneGraph of your Scene.
 SceneExplorerTopComponent.jButton1.text=update

+ 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() {

+ 65 - 20
sdk/jme3-terrain-editor/src/com/jme3/gde/terraineditor/sky/SkyboxWizardPanel2.java

@@ -32,14 +32,16 @@
 package com.jme3.gde.terraineditor.sky;
 
 import com.jme3.math.Vector3f;
+import com.jme3.texture.Image;
 import com.jme3.texture.Texture;
 import java.awt.Component;
 import javax.swing.event.ChangeListener;
 import org.openide.WizardDescriptor;
+import org.openide.WizardValidationException;
 import org.openide.util.HelpCtx;
 
 @SuppressWarnings({"unchecked", "rawtypes"})
-public class SkyboxWizardPanel2 implements WizardDescriptor.Panel {
+public class SkyboxWizardPanel2 implements WizardDescriptor.ValidatingPanel<WizardDescriptor> {
 
     /**
      * The visual component that displays this panel. If you need to access the
@@ -76,10 +78,12 @@ public class SkyboxWizardPanel2 implements WizardDescriptor.Panel {
         // fireChangeEvent();
         // and uncomment the complicated stuff below.
     }
-
+    
+    @Override
     public final void addChangeListener(ChangeListener l) {
     }
-
+    
+    @Override
     public final void removeChangeListener(ChangeListener l) {
     }
     /*
@@ -105,14 +109,56 @@ public class SkyboxWizardPanel2 implements WizardDescriptor.Panel {
     }
     }
      */
-
+    
+    @Override
+    public void validate() throws WizardValidationException {
+        SkyboxVisualPanel2 sky = (SkyboxVisualPanel2)component;
+        
+        /* Check if there are empty textures */
+        if (multipleTextures) {
+            if (sky.getEditorNorth().getAsText() == null) { throw new WizardValidationException(null, " Texture North: Missing texture!", null); }
+            if (sky.getEditorSouth().getAsText() == null) { throw new WizardValidationException(null, " Texture South: Missing texture!", null); }
+            if (sky.getEditorWest().getAsText() == null) { throw new WizardValidationException(null, " Texture West: Missing texture!", null); }
+            if (sky.getEditorEast().getAsText() == null) { throw new WizardValidationException(null, " Texture East: Missing texture!", null); }
+            if (sky.getEditorTop().getAsText() == null) { throw new WizardValidationException(null, " Texture Top: Missing texture!", null); }
+            if (sky.getEditorBottom().getAsText() == null) { throw new WizardValidationException(null, " Texture Bottom: Missing texture!", null); }
+            
+            /* Prevent Null-Pointer Exception. If this is triggered, the Texture has no Image or the AssetKey is invalid (which should never happen) */
+            if (sky.getEditorNorth().getValue() == null || ((Texture)sky.getEditorNorth().getValue()).getImage() == null) { throw new WizardValidationException(null, " Texture North: Cannot load texture!", null); }
+            if (sky.getEditorSouth().getValue() == null || ((Texture)sky.getEditorSouth().getValue()).getImage() == null) { throw new WizardValidationException(null, " Texture South: Cannot load texture!", null); }
+            if (sky.getEditorWest().getValue() == null || ((Texture)sky.getEditorWest().getValue()).getImage() == null) { throw new WizardValidationException(null, " Texture West: Cannot load texture!", null); }
+            if (sky.getEditorEast().getValue() == null || ((Texture)sky.getEditorEast().getValue()).getImage() == null) { throw new WizardValidationException(null, " Texture East: Cannot load texture!", null); }
+            if (sky.getEditorTop().getValue() == null || ((Texture)sky.getEditorTop().getValue()).getImage() == null) { throw new WizardValidationException(null, " Texture Top: Cannot load texture!", null); }
+            if (sky.getEditorBottom().getValue() == null || ((Texture)sky.getEditorBottom().getValue()).getImage() == null) { throw new WizardValidationException(null, " Texture Bottom: Cannot load texture!", null); }
+            
+            /* Check for squares */
+            Image I = ((Texture)sky.getEditorNorth().getValue()).getImage();
+            if (I.getWidth() != I.getHeight()) { throw new WizardValidationException(null, " Texture North: Image has to be a square (width == height)!", null); }
+            I = ((Texture)sky.getEditorSouth().getValue()).getImage();
+            if (I.getWidth() != I.getHeight()) { throw new WizardValidationException(null, " Texture South: Image has to be a square (width == height)!", null); }
+            I = ((Texture)sky.getEditorWest().getValue()).getImage();
+            if (I.getWidth() != I.getHeight()) { throw new WizardValidationException(null, " Texture West: Image has to be a square (width == height)!", null); }
+            I = ((Texture)sky.getEditorEast().getValue()).getImage();
+            if (I.getWidth() != I.getHeight()) { throw new WizardValidationException(null, " Texture East: Image has to be a square (width == height)!", null); }
+            I = ((Texture)sky.getEditorTop().getValue()).getImage();
+            if (I.getWidth() != I.getHeight()) { throw new WizardValidationException(null, " Texture Top: Image has to be a square (width == height)!", null); }
+            I = ((Texture)sky.getEditorBottom().getValue()).getImage();
+            if (I.getWidth() != I.getHeight()) { throw new WizardValidationException(null, " Texture Bottom: Image has to be a square (width == height)!", null); }
+        } else {
+            if (sky.getEditorSingle().getAsText() == null){ throw new WizardValidationException(null, " Single Texture: Missing texture!", null); }
+            if (sky.getEditorSingle().getValue() == null || ((Texture)sky.getEditorSingle().getValue()).getImage() == null){ throw new WizardValidationException(null, " Single Texture: Cannot load texture!", null); }
+            Image I = ((Texture)sky.getEditorSingle().getValue()).getImage();
+            if (I.getWidth() != I.getHeight()) { throw new WizardValidationException(null, " Single Texture: Image has to be a square (width == height)!", null); }
+        }
+    }
+    
     // You can use a settings object to keep track of state. Normally the
     // settings object will be the WizardDescriptor, so you can use
     // WizardDescriptor.getProperty & putProperty to store information entered
     // by the user.
-    public void readSettings(Object settings) {
-        WizardDescriptor wiz = (WizardDescriptor) settings;
-        multipleTextures = (Boolean)wiz.getProperty("multipleTextures");
+    @Override
+    public void readSettings(WizardDescriptor settings) {
+        multipleTextures = (Boolean)settings.getProperty("multipleTextures");
         SkyboxVisualPanel2 comp = (SkyboxVisualPanel2) getComponent();
         if (multipleTextures) {
             comp.getMultipleTexturePanel().setVisible(true);
@@ -124,28 +170,27 @@ public class SkyboxWizardPanel2 implements WizardDescriptor.Panel {
     }
 
     @Override
-    public void storeSettings(Object settings) {
-        WizardDescriptor wiz = (WizardDescriptor) settings;
+    public void storeSettings(WizardDescriptor settings) {
         SkyboxVisualPanel2 comp = (SkyboxVisualPanel2) getComponent();
         if (multipleTextures) {
-            wiz.putProperty("textureSouth", (Texture)comp.getEditorSouth().getValue());
-            wiz.putProperty("textureNorth", (Texture)comp.getEditorNorth().getValue());
-            wiz.putProperty("textureEast", (Texture)comp.getEditorEast().getValue());
-            wiz.putProperty("textureWest", (Texture)comp.getEditorWest().getValue());
-            wiz.putProperty("textureTop", (Texture)comp.getEditorTop().getValue());
-            wiz.putProperty("textureBottom", (Texture)comp.getEditorBottom().getValue());
+            settings.putProperty("textureSouth", (Texture)comp.getEditorSouth().getValue());
+            settings.putProperty("textureNorth", (Texture)comp.getEditorNorth().getValue());
+            settings.putProperty("textureEast", (Texture)comp.getEditorEast().getValue());
+            settings.putProperty("textureWest", (Texture)comp.getEditorWest().getValue());
+            settings.putProperty("textureTop", (Texture)comp.getEditorTop().getValue());
+            settings.putProperty("textureBottom", (Texture)comp.getEditorBottom().getValue());
             float x = new Float(comp.getNormal1X().getText());
             float y = new Float(comp.getNormal1Y().getText());
             float z = new Float(comp.getNormal1Z().getText());
-            wiz.putProperty("normalScale", new Vector3f(x,y,z) );
+            settings.putProperty("normalScale", new Vector3f(x,y,z) );
         } else {
-            wiz.putProperty("textureSingle", (Texture)comp.getEditorSingle().getValue());
+            settings.putProperty("textureSingle", (Texture)comp.getEditorSingle().getValue());
             float x = new Float(comp.getNormal2X().getText());
             float y = new Float(comp.getNormal2Y().getText());
             float z = new Float(comp.getNormal2Z().getText());
-            wiz.putProperty("normalScale", new Vector3f(x,y,z) );
-            wiz.putProperty("envMapType", comp.getEnvMapType());         
-            wiz.putProperty("flipY", comp.getFlipYCheckBox().isSelected());
+            settings.putProperty("normalScale", new Vector3f(x,y,z) );
+            settings.putProperty("envMapType", comp.getEnvMapType());         
+            settings.putProperty("flipY", comp.getFlipYCheckBox().isSelected());
         }
     }
 }

+ 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

+ 97 - 43
version.gradle

@@ -3,27 +3,31 @@
  =====================
  
  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: 1
+ * NBM Revision: 0
  * NBM UC Suffix: stable/3.1/plugins
  
  Final Release
+ * git tag: v3.1.0
  * Full Version: 3.1
  * POM Version: 3.1.0
- * NBM Revision: 5
+ * NBM Revision: 0
  * NBM UC Suffix: stable/3.1/plugins
  */ 
 
@@ -52,59 +56,109 @@ ext {
     jmeNbmUcSuffix  = "unknown"
 }
 
+def getReleaseInfo(String tag) {
+    if (tag == null) {
+        // not a tagged commit
+        return null;
+    }
+    if (!tag.startsWith("v")) {
+        // syntax error
+        return null;
+    }
+    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;
+    }
+
+    if (mainVersion.size() == 0) {
+        // syntax error
+        return null;
+    }
+
+    parts = mainVersion.split("\\.");
+    if (parts.size() != 3) {
+        // syntax error
+        return null;
+    }
+
+    String baseVersion = parts[0] + "." + parts[1];
+
+    return [
+        "tag" : tag,
+        "baseVersion" : baseVersion,
+        "mainVersion" : mainVersion,
+        "prerelease" :  prerelease,
+        "releaseName" : releaseName,
+        "releaseSuffix": (prerelease ? "-${releaseName}": "")
+    ]
+}
+
 task configureVersionInfo {
     try {
         def grgit = Grgit.open(project.file('.'))
-        jmeRevision = grgit.log(includes:['HEAD']).size()
-        jmeGitHash = grgit.head().id
-        jmeShortGitHash = grgit.head().abbreviatedId
+        def head = grgit.head()
+        jmeRevision = grgit.log(includes: [head]).size()
+        jmeGitHash = head.id
+        jmeShortGitHash = head.abbreviatedId
         jmeBranchName = grgit.branch.current.name
-        jmeGitTag = grgit.describe()
-        if (jmeGitTag == null) jmeGitTag = ""
-        
-        if (System.env.TRAVIS_BRANCH != null) {
-            jmeBranchName = System.env.TRAVIS_BRANCH
-        }
-        if (System.env.TRAVIS_TAG != null) {
-            jmeGitTag = System.env.TRAVIS_TAG
-        }
-        if (System.env.TRAVIS_PULL_REQUEST != null && 
-            System.env.TRAVIS_PULL_REQUEST != "false") {
-            jmeBranchName += "-pr-" + System.env.TRAVIS_PULL_REQUEST
-        }
+        jmeGitTag = grgit.tag.list().find { it.commit == head }
         
-        jmeFullVersion = jmeMainVersion
-        jmePomVersion  = jmeVersion
-        
-        if (jmeBranchName != "master") {
-            jmeFullVersion += "-${jmeBranchName}"
-            jmePomVersion  += "-${jmeBranchName}"
-            
-            jmeNbmUcSuffix = "${jmeBranchName}-"
+        if (jmeGitTag != null) {
+            jmeGitTag = jmeGitTag.name
         } else {
-            jmeNbmUcSuffix = ""
+            jmeGitTag = System.env.TRAVIS_TAG
         }
-        
-        if (jmeVersionTag == "SNAPSHOT") {
-            jmeNbmUcSuffix += "nightly"
+
+        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 {
-            jmeNbmUcSuffix += "stable"
-        }
-        
-        jmeNbmUcSuffix += "/" + jmeMainVersion + "/plugins"
-        
-        if (jmeVersionTag == "SNAPSHOT") {
+            // SNAPSHOT
+            jmeFullVersion = jmeMainVersion
+            jmePomVersion  = jmeVersion
+            if (System.env.TRAVIS_BRANCH != null) {
+                jmeBranchName = System.env.TRAVIS_BRANCH
+            }
+            if (System.env.TRAVIS_PULL_REQUEST != null && 
+                System.env.TRAVIS_PULL_REQUEST != "false") {
+                jmeBranchName += "-pr-" + System.env.TRAVIS_PULL_REQUEST
+            }
+            if (jmeBranchName != "master") {
+                jmeFullVersion += "-${jmeBranchName}"
+                jmePomVersion  += "-${jmeBranchName}"
+                jmeNbmUcSuffix = "${jmeBranchName}-"
+            } else {
+                jmeNbmUcSuffix = ""
+            }
+            jmeNbmUcSuffix += "nightly/" + jmeMainVersion + "/plugins"
             jmeFullVersion += "-${jmeRevision}"
             jmePomVersion  += "-SNAPSHOT"
             jmeNbmRevision = jmeRevision
-        } else if (jmeVersionTag == "") {
-            jmeNbmRevision = jmeVersionTagID
-        } else {
-            jmeFullVersion += "-${jmeVersionTag}"
-            jmePomVersion  += "-${jmeVersionTag}"
-            jmeNbmRevision = jmeVersionTagID
         }
-        
+            
         logger.warn("Full Version: ${jmeFullVersion}")
         logger.warn("POM Version: ${jmePomVersion}")
         logger.warn("NBM Revision: ${jmeNbmRevision}")