Преглед изворни кода

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

jmekaelthas пре 9 година
родитељ
комит
fa3ea41a8d
100 измењених фајлова са 5043 додато и 979 уклоњено
  1. 1 1
      common.gradle
  2. 0 1
      gradle.properties
  3. 1 1
      jme3-android/src/main/java/com/jme3/input/android/AndroidTouchInput.java
  4. 0 1
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/Face.java
  5. 23 20
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/MeshHelper.java
  6. 9 1
      jme3-bullet/src/common/java/com/jme3/bullet/control/AbstractPhysicsControl.java
  7. 12 1
      jme3-bullet/src/common/java/com/jme3/bullet/control/BetterCharacterControl.java
  8. 27 1
      jme3-bullet/src/common/java/com/jme3/bullet/control/CharacterControl.java
  9. 23 1
      jme3-bullet/src/common/java/com/jme3/bullet/control/GhostControl.java
  10. 15 1
      jme3-bullet/src/common/java/com/jme3/bullet/control/KinematicRagdollControl.java
  11. 37 1
      jme3-bullet/src/common/java/com/jme3/bullet/control/RigidBodyControl.java
  12. 61 1
      jme3-bullet/src/common/java/com/jme3/bullet/control/VehicleControl.java
  13. 4 0
      jme3-bullet/src/main/java/com/jme3/bullet/collision/shapes/ConeCollisionShape.java
  14. 31 1
      jme3-core/src/main/java/com/jme3/animation/AnimControl.java
  15. 30 1
      jme3-core/src/main/java/com/jme3/animation/Animation.java
  16. 24 1
      jme3-core/src/main/java/com/jme3/animation/AudioTrack.java
  17. 7 11
      jme3-core/src/main/java/com/jme3/animation/Bone.java
  18. 2 1
      jme3-core/src/main/java/com/jme3/animation/ClonableTrack.java
  19. 34 0
      jme3-core/src/main/java/com/jme3/animation/EffectTrack.java
  20. 12 1
      jme3-core/src/main/java/com/jme3/animation/Skeleton.java
  21. 27 1
      jme3-core/src/main/java/com/jme3/animation/SkeletonControl.java
  22. 17 1
      jme3-core/src/main/java/com/jme3/animation/TrackInfo.java
  23. 14 1
      jme3-core/src/main/java/com/jme3/app/StatsView.java
  24. 14 12
      jme3-core/src/main/java/com/jme3/app/state/ScreenshotAppState.java
  25. 1 1
      jme3-core/src/main/java/com/jme3/asset/AssetKey.java
  26. 34 4
      jme3-core/src/main/java/com/jme3/audio/AudioNode.java
  27. 29 1
      jme3-core/src/main/java/com/jme3/cinematic/events/MotionEvent.java
  28. 18 1
      jme3-core/src/main/java/com/jme3/effect/ParticleEmitter.java
  29. 21 1
      jme3-core/src/main/java/com/jme3/input/ChaseCamera.java
  30. 32 3
      jme3-core/src/main/java/com/jme3/material/MatParam.java
  31. 4 2
      jme3-core/src/main/java/com/jme3/post/FilterPostProcessor.java
  32. 38 95
      jme3-core/src/main/java/com/jme3/scene/BatchNode.java
  33. 1 1
      jme3-core/src/main/java/com/jme3/scene/Geometry.java
  34. 1 1
      jme3-core/src/main/java/com/jme3/scene/GeometryGroupNode.java
  35. 17 1
      jme3-core/src/main/java/com/jme3/scene/control/AbstractControl.java
  36. 7 6
      jme3-core/src/main/java/com/jme3/scene/control/BillboardControl.java
  37. 8 7
      jme3-core/src/main/java/com/jme3/scene/control/CameraControl.java
  38. 8 7
      jme3-core/src/main/java/com/jme3/scene/control/LightControl.java
  39. 13 2
      jme3-core/src/main/java/com/jme3/scene/control/LodControl.java
  40. 14 0
      jme3-core/src/main/java/com/jme3/scene/control/UpdateControl.java
  41. 18 2
      jme3-core/src/main/java/com/jme3/scene/instancing/InstancedNode.java
  42. 2 1
      jme3-core/src/main/java/com/jme3/scene/shape/Cylinder.java
  43. 46 1
      jme3-core/src/main/java/com/jme3/shadow/AbstractShadowFilter.java
  44. 85 20
      jme3-core/src/main/java/com/jme3/shadow/AbstractShadowRenderer.java
  45. 2 0
      jme3-core/src/main/java/com/jme3/shadow/DirectionalLightShadowRenderer.java
  46. 2 0
      jme3-core/src/main/java/com/jme3/system/JmeSystemDelegate.java
  47. 15 27
      jme3-core/src/main/java/com/jme3/util/BufferUtils.java
  48. 81 0
      jme3-core/src/main/java/com/jme3/util/clone/CloneFunction.java
  49. 348 0
      jme3-core/src/main/java/com/jme3/util/clone/Cloner.java
  50. 58 63
      jme3-core/src/main/java/com/jme3/util/clone/IdentityCloneFunction.java
  51. 99 0
      jme3-core/src/main/java/com/jme3/util/clone/JmeCloneable.java
  52. 70 55
      jme3-core/src/main/java/com/jme3/util/clone/ListCloneFunction.java
  53. 97 0
      jme3-core/src/main/java/com/jme3/util/mikktspace/MikkTSpaceContext.java
  54. 100 0
      jme3-core/src/main/java/com/jme3/util/mikktspace/MikkTSpaceImpl.java
  55. 1717 0
      jme3-core/src/main/java/com/jme3/util/mikktspace/MikktspaceTangentGenerator.java
  56. 7 10
      jme3-core/src/main/resources/Common/MatDefs/Light/Lighting.j3md
  57. 6 2
      jme3-core/src/main/resources/Common/MatDefs/Misc/Unshaded.j3md
  58. 14 4
      jme3-core/src/main/resources/Common/MatDefs/Shadow/PostShadow.frag
  59. 6 2
      jme3-core/src/main/resources/Common/MatDefs/Shadow/PostShadow.j3md
  60. 27 7
      jme3-core/src/main/resources/Common/MatDefs/Shadow/PostShadow.vert
  61. 0 80
      jme3-core/src/main/resources/Common/MatDefs/Shadow/PostShadow15.frag
  62. 0 82
      jme3-core/src/main/resources/Common/MatDefs/Shadow/PostShadow15.vert
  63. 29 0
      jme3-core/src/main/resources/Common/MatDefs/Shadow/PostShadowFilter.frag
  64. 7 3
      jme3-core/src/main/resources/Common/MatDefs/Shadow/PostShadowFilter.j3md
  65. 42 8
      jme3-core/src/main/resources/Common/MatDefs/Shadow/PostShadowFilter15.frag
  66. 153 102
      jme3-core/src/main/resources/Common/ShaderLib/Shadows.glsllib
  67. 0 242
      jme3-core/src/main/resources/Common/ShaderLib/Shadows15.glsllib
  68. 19 4
      jme3-core/src/plugins/java/com/jme3/material/plugins/J3MLoader.java
  69. 2 0
      jme3-core/src/test/java/com/jme3/material/plugins/J3MLoaderTest.java
  70. 2 1
      jme3-core/src/test/resources/texture-parameters-newstyle.j3m
  71. 2 2
      jme3-effects/src/main/resources/Common/MatDefs/SSAO/normal.vert
  72. 9 5
      jme3-examples/build.gradle
  73. 3 3
      jme3-examples/src/main/java/jme3test/TestChooser.java
  74. 367 0
      jme3-examples/src/main/java/jme3test/app/TestCloner.java
  75. 14 1
      jme3-examples/src/main/java/jme3test/bullet/PhysicsHoverControl.java
  76. 29 10
      jme3-examples/src/main/java/jme3test/light/TestDirectionalLightShadow.java
  77. 22 3
      jme3-examples/src/main/java/jme3test/light/TestPointLightShadows.java
  78. 12 0
      jme3-examples/src/main/java/jme3test/light/TestPssmShadow.java
  79. 1 1
      jme3-examples/src/main/java/jme3test/light/TestSpotLight.java
  80. 1 6
      jme3-examples/src/main/java/jme3test/light/TestSpotLightShadows.java
  81. 1 1
      jme3-examples/src/main/java/jme3test/light/TestSpotLightTerrain.java
  82. 14 0
      jme3-examples/src/main/java/jme3test/network/TestChatClient.java
  83. 20 1
      jme3-examples/src/main/java/jme3test/network/TestChatServer.java
  84. 11 18
      jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java
  85. 15 11
      jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java
  86. 1 0
      jme3-plugins/build.gradle
  87. 78 0
      jme3-plugins/src/main/java/com/jme3/material/plugin/export/material/J3MExporter.java
  88. 383 0
      jme3-plugins/src/main/java/com/jme3/material/plugin/export/material/J3MOutputCapsule.java
  89. 81 0
      jme3-plugins/src/main/java/com/jme3/material/plugin/export/material/J3MRenderStateOutputCapsule.java
  90. 99 0
      jme3-plugins/src/main/java/com/jme3/material/plugin/export/material/J3MRootOutputCapsule.java
  91. 115 0
      jme3-plugins/src/test/java/com/jme3/material/plugin/TestMaterialWrite.java
  92. 10 0
      jme3-terrain/src/main/java/com/jme3/terrain/geomipmap/NormalRecalcControl.java
  93. 26 1
      jme3-terrain/src/main/java/com/jme3/terrain/geomipmap/TerrainLodControl.java
  94. 4 4
      jme3-testdata/src/main/resources/Models/Sign Post/Sign Post.j3m
  95. BIN
      jme3-testdata/src/main/resources/Models/Test/CornellBox.j3o
  96. 0 1
      sdk/BasicGameTemplate/MANIFEST.MF
  97. 0 0
      sdk/BasicGameTemplate/assets/Interface/.keep
  98. 0 0
      sdk/BasicGameTemplate/assets/MatDefs/.keep
  99. 0 0
      sdk/BasicGameTemplate/assets/Materials/.keep
  100. 0 0
      sdk/BasicGameTemplate/assets/Models/.keep

+ 1 - 1
common.gradle

@@ -8,7 +8,7 @@ apply plugin: 'maven'
 group = 'org.jmonkeyengine'
 version = jmePomVersion
 
-sourceCompatibility = '1.6'
+sourceCompatibility = '1.7'
 [compileJava, compileTestJava]*.options*.encoding = 'UTF-8'
 
 repositories {

+ 0 - 1
gradle.properties

@@ -11,7 +11,6 @@ jmeVersionTagID = 0
 buildJavaDoc = true
 
 # specify if SDK and Native libraries get built
-buildSdkProject = true
 buildNativeProjects = false
 buildAndroidExamples = false
 

+ 1 - 1
jme3-android/src/main/java/com/jme3/input/android/AndroidTouchInput.java

@@ -430,7 +430,7 @@ public class AndroidTouchInput implements TouchInput {
             return;
         }
 
-        logger.log(Level.INFO, "event: {0}", event);
+        //logger.log(Level.INFO, "event: {0}", event);
 
         inputEventQueue.add(event);
         if (event instanceof TouchEvent) {

+ 0 - 1
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/Face.java

@@ -146,7 +146,6 @@ public class Face implements Comparator<Integer> {
     /**
      * @return current indexes of the face (if it is already triangulated then more than one index group will be in the result list)
      */
-    @SuppressWarnings("unchecked")
     public List<List<Integer>> getCurrentIndexes() {
         if (triangulatedFaces == null) {
             return Arrays.asList(indexes.getAll());

+ 23 - 20
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/MeshHelper.java

@@ -286,30 +286,33 @@ public class MeshHelper extends AbstractBlenderHelper {
         List<Map<String, Float>> result = new ArrayList<Map<String, Float>>();
 
         Structure parent = blenderContext.peekParent();
-        Structure defbase = (Structure) parent.getFieldValue("defbase");
-        List<String> groupNames = new ArrayList<String>();
-        List<Structure> defs = defbase.evaluateListBase();
-        for (Structure def : defs) {
-            groupNames.add(def.getFieldValue("name").toString());
-        }
+        if(parent != null) {
+        	// the mesh might be saved without its parent (it is then unused)
+        	Structure defbase = (Structure) parent.getFieldValue("defbase");
+            List<String> groupNames = new ArrayList<String>();
+            List<Structure> defs = defbase.evaluateListBase();
+            for (Structure def : defs) {
+                groupNames.add(def.getFieldValue("name").toString());
+            }
 
-        Pointer pDvert = (Pointer) meshStructure.getFieldValue("dvert");// dvert = DeformVERTices
-        if (pDvert.isNotNull()) {// assigning weights and bone indices
-            List<Structure> dverts = pDvert.fetchData();
-            for (Structure dvert : dverts) {
-                Map<String, Float> weightsForVertex = new HashMap<String, Float>();
-                Pointer pDW = (Pointer) dvert.getFieldValue("dw");
-                if (pDW.isNotNull()) {
-                    List<Structure> dw = pDW.fetchData();
-                    for (Structure deformWeight : dw) {
-                        int groupIndex = ((Number) deformWeight.getFieldValue("def_nr")).intValue();
-                        float weight = ((Number) deformWeight.getFieldValue("weight")).floatValue();
-                        String groupName = groupNames.get(groupIndex);
+            Pointer pDvert = (Pointer) meshStructure.getFieldValue("dvert");// dvert = DeformVERTices
+            if (pDvert.isNotNull()) {// assigning weights and bone indices
+                List<Structure> dverts = pDvert.fetchData();
+                for (Structure dvert : dverts) {
+                    Map<String, Float> weightsForVertex = new HashMap<String, Float>();
+                    Pointer pDW = (Pointer) dvert.getFieldValue("dw");
+                    if (pDW.isNotNull()) {
+                        List<Structure> dw = pDW.fetchData();
+                        for (Structure deformWeight : dw) {
+                            int groupIndex = ((Number) deformWeight.getFieldValue("def_nr")).intValue();
+                            float weight = ((Number) deformWeight.getFieldValue("weight")).floatValue();
+                            String groupName = groupNames.get(groupIndex);
 
-                        weightsForVertex.put(groupName, weight);
+                            weightsForVertex.put(groupName, weight);
+                        }
                     }
+                    result.add(weightsForVertex);
                 }
-                result.add(weightsForVertex);
             }
         }
         return result;

+ 9 - 1
jme3-bullet/src/common/java/com/jme3/bullet/control/AbstractPhysicsControl.java

@@ -41,6 +41,8 @@ import com.jme3.math.Vector3f;
 import com.jme3.renderer.RenderManager;
 import com.jme3.renderer.ViewPort;
 import com.jme3.scene.Spatial;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 
 /**
@@ -49,7 +51,7 @@ import java.io.IOException;
  *
  * @author normenhansen
  */
-public abstract class AbstractPhysicsControl implements PhysicsControl {
+public abstract class AbstractPhysicsControl implements PhysicsControl, JmeCloneable {
 
     private final Quaternion tmp_inverseWorldRotation = new Quaternion();
     protected Spatial spatial;
@@ -161,6 +163,12 @@ public abstract class AbstractPhysicsControl implements PhysicsControl {
 
     }
     
+    @Override   
+    public void cloneFields( Cloner cloner, Object original ) { 
+        this.spatial = cloner.clone(spatial);
+        createSpatialData(this.spatial);
+    }
+         
     public void setSpatial(Spatial spatial) {
         if (this.spatial != null && this.spatial != spatial) {
             removeSpatialData(this.spatial);

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

@@ -50,6 +50,8 @@ import com.jme3.renderer.ViewPort;
 import com.jme3.scene.Spatial;
 import com.jme3.scene.control.Control;
 import com.jme3.util.TempVars;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 import java.util.List;
 import java.util.logging.Level;
@@ -68,7 +70,7 @@ import java.util.logging.Logger;
  *
  * @author normenhansen
  */
-public class BetterCharacterControl extends AbstractPhysicsControl implements PhysicsTickListener {
+public class BetterCharacterControl extends AbstractPhysicsControl implements PhysicsTickListener, JmeCloneable {
 
     protected static final Logger logger = Logger.getLogger(BetterCharacterControl.class.getName());
     protected PhysicsRigidBody rigidBody;
@@ -663,12 +665,21 @@ public class BetterCharacterControl extends AbstractPhysicsControl implements Ph
         rigidBody.setUserObject(null);
     }
 
+    @Override
     public Control cloneForSpatial(Spatial spatial) {
         BetterCharacterControl control = new BetterCharacterControl(radius, height, mass);
         control.setJumpForce(jumpForce);
         return control;
     }
 
+    @Override
+    public Object jmeClone() {
+        BetterCharacterControl control = new BetterCharacterControl(radius, height, mass);
+        control.setJumpForce(jumpForce);
+        control.spatial = this.spatial;
+        return control;
+    }     
+
     @Override
     public void write(JmeExporter ex) throws IOException {
         super.write(ex);

+ 27 - 1
jme3-bullet/src/common/java/com/jme3/bullet/control/CharacterControl.java

@@ -44,13 +44,15 @@ import com.jme3.renderer.RenderManager;
 import com.jme3.renderer.ViewPort;
 import com.jme3.scene.Spatial;
 import com.jme3.scene.control.Control;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 
 /**
  * You might want to try <code>BetterCharacterControl</code> as well.
  * @author normenhansen
  */
-public class CharacterControl extends PhysicsCharacter implements PhysicsControl {
+public class CharacterControl extends PhysicsCharacter implements PhysicsControl, JmeCloneable {
 
     protected Spatial spatial;
     protected boolean enabled = true;
@@ -87,6 +89,7 @@ public class CharacterControl extends PhysicsCharacter implements PhysicsControl
         return spatial.getWorldTranslation();
     }
 
+    @Override
     public Control cloneForSpatial(Spatial spatial) {
         CharacterControl control = new CharacterControl(collisionShape, stepHeight);
         control.setCcdMotionThreshold(getCcdMotionThreshold());
@@ -103,6 +106,29 @@ public class CharacterControl extends PhysicsCharacter implements PhysicsControl
         return control;
     }
 
+    @Override
+    public Object jmeClone() {
+        CharacterControl control = new CharacterControl(collisionShape, stepHeight);
+        control.setCcdMotionThreshold(getCcdMotionThreshold());
+        control.setCcdSweptSphereRadius(getCcdSweptSphereRadius());
+        control.setCollideWithGroups(getCollideWithGroups());
+        control.setCollisionGroup(getCollisionGroup());
+        control.setFallSpeed(getFallSpeed());
+        control.setGravity(getGravity());
+        control.setJumpSpeed(getJumpSpeed());
+        control.setMaxSlope(getMaxSlope());
+        control.setPhysicsLocation(getPhysicsLocation());
+        control.setUpAxis(getUpAxis());
+        control.setApplyPhysicsLocal(isApplyPhysicsLocal());
+        control.spatial = this.spatial;
+        return control;
+    }     
+
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) { 
+        this.spatial = cloner.clone(spatial);
+    }
+         
     public void setSpatial(Spatial spatial) {
         this.spatial = spatial;
         setUserObject(spatial);

+ 23 - 1
jme3-bullet/src/common/java/com/jme3/bullet/control/GhostControl.java

@@ -44,6 +44,8 @@ import com.jme3.renderer.RenderManager;
 import com.jme3.renderer.ViewPort;
 import com.jme3.scene.Spatial;
 import com.jme3.scene.control.Control;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 
 /**
@@ -51,7 +53,7 @@ import java.io.IOException;
  * overlaps with other physics objects (e.g. aggro radius).
  * @author normenhansen
  */
-public class GhostControl extends PhysicsGhostObject implements PhysicsControl {
+public class GhostControl extends PhysicsGhostObject implements PhysicsControl, JmeCloneable {
 
     protected Spatial spatial;
     protected boolean enabled = true;
@@ -93,6 +95,7 @@ public class GhostControl extends PhysicsGhostObject implements PhysicsControl {
         return spatial.getWorldRotation();
     }
 
+    @Override
     public Control cloneForSpatial(Spatial spatial) {
         GhostControl control = new GhostControl(collisionShape);
         control.setCcdMotionThreshold(getCcdMotionThreshold());
@@ -105,6 +108,25 @@ public class GhostControl extends PhysicsGhostObject implements PhysicsControl {
         return control;
     }
 
+    @Override   
+    public Object jmeClone() {
+        GhostControl control = new GhostControl(collisionShape);
+        control.setCcdMotionThreshold(getCcdMotionThreshold());
+        control.setCcdSweptSphereRadius(getCcdSweptSphereRadius());
+        control.setCollideWithGroups(getCollideWithGroups());
+        control.setCollisionGroup(getCollisionGroup());
+        control.setPhysicsLocation(getPhysicsLocation());
+        control.setPhysicsRotation(getPhysicsRotationMatrix());
+        control.setApplyPhysicsLocal(isApplyPhysicsLocal());
+        control.spatial = this.spatial;
+        return control;
+    }     
+
+    @Override   
+    public void cloneFields( Cloner cloner, Object original ) { 
+        this.spatial = cloner.clone(spatial);
+    }
+         
     public void setSpatial(Spatial spatial) {
         this.spatial = spatial;
         setUserObject(spatial);

+ 15 - 1
jme3-bullet/src/common/java/com/jme3/bullet/control/KinematicRagdollControl.java

@@ -61,6 +61,8 @@ import com.jme3.scene.Node;
 import com.jme3.scene.Spatial;
 import com.jme3.scene.control.Control;
 import com.jme3.util.TempVars;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 import java.util.*;
 import java.util.logging.Level;
@@ -92,7 +94,7 @@ import java.util.logging.Logger;
  *
  * @author Normen Hansen and Rémy Bouquet (Nehon)
  */
-public class KinematicRagdollControl extends AbstractPhysicsControl implements PhysicsCollisionListener {
+public class KinematicRagdollControl extends AbstractPhysicsControl implements PhysicsCollisionListener, JmeCloneable {
 
     protected static final Logger logger = Logger.getLogger(KinematicRagdollControl.class.getName());
     protected List<RagdollCollisionListener> listeners;
@@ -910,6 +912,7 @@ public class KinematicRagdollControl extends AbstractPhysicsControl implements P
     public void render(RenderManager rm, ViewPort vp) {
     }
 
+    @Override
     public Control cloneForSpatial(Spatial spatial) {
         KinematicRagdollControl control = new KinematicRagdollControl(preset, weightThreshold);
         control.setMode(mode);
@@ -919,6 +922,17 @@ public class KinematicRagdollControl extends AbstractPhysicsControl implements P
         return control;
     }
    
+    @Override   
+    public Object jmeClone() {
+        KinematicRagdollControl control = new KinematicRagdollControl(preset, weightThreshold);        
+        control.setMode(mode);
+        control.setRootMass(rootMass);
+        control.setWeightThreshold(weightThreshold);
+        control.setApplyPhysicsLocal(applyLocal);
+        control.spatial = this.spatial;
+        return control;
+    }     
+
     public Vector3f setIKTarget(Bone bone, Vector3f worldPos, int chainLength) {
         Vector3f target = worldPos.subtract(targetModel.getWorldTranslation());
         ikTargets.put(bone.getName(), target);

+ 37 - 1
jme3-bullet/src/common/java/com/jme3/bullet/control/RigidBodyControl.java

@@ -51,13 +51,15 @@ import com.jme3.scene.Spatial;
 import com.jme3.scene.control.Control;
 import com.jme3.scene.shape.Box;
 import com.jme3.scene.shape.Sphere;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 
 /**
  *
  * @author normenhansen
  */
-public class RigidBodyControl extends PhysicsRigidBody implements PhysicsControl {
+public class RigidBodyControl extends PhysicsRigidBody implements PhysicsControl, JmeCloneable {
 
     protected Spatial spatial;
     protected boolean enabled = true;
@@ -89,6 +91,7 @@ public class RigidBodyControl extends PhysicsRigidBody implements PhysicsControl
         super(shape, mass);
     }
 
+    @Override
     public Control cloneForSpatial(Spatial spatial) {
         RigidBodyControl control = new RigidBodyControl(collisionShape, mass);
         control.setAngularFactor(getAngularFactor());
@@ -115,6 +118,39 @@ public class RigidBodyControl extends PhysicsRigidBody implements PhysicsControl
         return control;
     }
 
+    @Override   
+    public Object jmeClone() {
+        RigidBodyControl control = new RigidBodyControl(collisionShape, mass);
+        control.setAngularFactor(getAngularFactor());
+        control.setAngularSleepingThreshold(getAngularSleepingThreshold());
+        control.setCcdMotionThreshold(getCcdMotionThreshold());
+        control.setCcdSweptSphereRadius(getCcdSweptSphereRadius());
+        control.setCollideWithGroups(getCollideWithGroups());
+        control.setCollisionGroup(getCollisionGroup());
+        control.setDamping(getLinearDamping(), getAngularDamping());
+        control.setFriction(getFriction());
+        control.setGravity(getGravity());
+        control.setKinematic(isKinematic());
+        control.setKinematicSpatial(isKinematicSpatial());
+        control.setLinearSleepingThreshold(getLinearSleepingThreshold());
+        control.setPhysicsLocation(getPhysicsLocation(null));
+        control.setPhysicsRotation(getPhysicsRotationMatrix(null));
+        control.setRestitution(getRestitution());
+
+        if (mass > 0) {
+            control.setAngularVelocity(getAngularVelocity());
+            control.setLinearVelocity(getLinearVelocity());
+        }
+        control.setApplyPhysicsLocal(isApplyPhysicsLocal());
+        control.spatial = this.spatial;
+        return control;
+    }     
+
+    @Override   
+    public void cloneFields( Cloner cloner, Object original ) { 
+        this.spatial = cloner.clone(spatial);
+    }
+         
     public void setSpatial(Spatial spatial) {
         this.spatial = spatial;
         setUserObject(spatial);

+ 61 - 1
jme3-bullet/src/common/java/com/jme3/bullet/control/VehicleControl.java

@@ -46,6 +46,8 @@ import com.jme3.renderer.ViewPort;
 import com.jme3.scene.Node;
 import com.jme3.scene.Spatial;
 import com.jme3.scene.control.Control;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 import java.util.Iterator;
 
@@ -53,7 +55,7 @@ import java.util.Iterator;
  *
  * @author normenhansen
  */
-public class VehicleControl extends PhysicsVehicle implements PhysicsControl {
+public class VehicleControl extends PhysicsVehicle implements PhysicsControl, JmeCloneable {
 
     protected Spatial spatial;
     protected boolean enabled = true;
@@ -106,6 +108,7 @@ public class VehicleControl extends PhysicsVehicle implements PhysicsControl {
         return spatial.getWorldRotation();
     }
 
+    @Override
     public Control cloneForSpatial(Spatial spatial) {
         VehicleControl control = new VehicleControl(collisionShape, mass);
         control.setAngularFactor(getAngularFactor());
@@ -155,6 +158,63 @@ public class VehicleControl extends PhysicsVehicle implements PhysicsControl {
         return control;
     }
 
+    @Override   
+    public Object jmeClone() {
+        VehicleControl control = new VehicleControl(collisionShape, mass);
+        control.setAngularFactor(getAngularFactor());
+        control.setAngularSleepingThreshold(getAngularSleepingThreshold());
+        control.setAngularVelocity(getAngularVelocity());
+        control.setCcdMotionThreshold(getCcdMotionThreshold());
+        control.setCcdSweptSphereRadius(getCcdSweptSphereRadius());
+        control.setCollideWithGroups(getCollideWithGroups());
+        control.setCollisionGroup(getCollisionGroup());
+        control.setDamping(getLinearDamping(), getAngularDamping());
+        control.setFriction(getFriction());
+        control.setGravity(getGravity());
+        control.setKinematic(isKinematic());
+        control.setLinearSleepingThreshold(getLinearSleepingThreshold());
+        control.setLinearVelocity(getLinearVelocity());
+        control.setPhysicsLocation(getPhysicsLocation());
+        control.setPhysicsRotation(getPhysicsRotationMatrix());
+        control.setRestitution(getRestitution());
+
+        control.setFrictionSlip(getFrictionSlip());
+        control.setMaxSuspensionTravelCm(getMaxSuspensionTravelCm());
+        control.setSuspensionStiffness(getSuspensionStiffness());
+        control.setSuspensionCompression(tuning.suspensionCompression);
+        control.setSuspensionDamping(tuning.suspensionDamping);
+        control.setMaxSuspensionForce(getMaxSuspensionForce());
+    
+        for (Iterator<VehicleWheel> it = wheels.iterator(); it.hasNext();) {
+            VehicleWheel wheel = it.next();
+            VehicleWheel newWheel = control.addWheel(wheel.getLocation(), wheel.getDirection(), wheel.getAxle(), wheel.getRestLength(), wheel.getRadius(), wheel.isFrontWheel());
+            newWheel.setFrictionSlip(wheel.getFrictionSlip());
+            newWheel.setMaxSuspensionTravelCm(wheel.getMaxSuspensionTravelCm());
+            newWheel.setSuspensionStiffness(wheel.getSuspensionStiffness());
+            newWheel.setWheelsDampingCompression(wheel.getWheelsDampingCompression());
+            newWheel.setWheelsDampingRelaxation(wheel.getWheelsDampingRelaxation());
+            newWheel.setMaxSuspensionForce(wheel.getMaxSuspensionForce());
+
+            // Copy the wheel spatial reference directly for now.  They'll
+            // get fixed up in the cloneFields() method
+            newWheel.setWheelSpatial(wheel.getWheelSpatial());
+        }
+        control.setApplyPhysicsLocal(isApplyPhysicsLocal());
+        
+        control.spatial = spatial;
+        return control;
+    }     
+
+    @Override   
+    public void cloneFields( Cloner cloner, Object original ) {
+        this.spatial = cloner.clone(spatial);
+         
+        for( VehicleWheel wheel : wheels ) {
+            Spatial spatial = cloner.clone(wheel.getWheelSpatial());
+            wheel.setWheelSpatial(spatial);
+        }        
+    }
+         
     public void setSpatial(Spatial spatial) {
         this.spatial = spatial;
         setUserObject(spatial);

+ 4 - 0
jme3-bullet/src/main/java/com/jme3/bullet/collision/shapes/ConeCollisionShape.java

@@ -70,6 +70,10 @@ public class ConeCollisionShape extends CollisionShape {
     public float getRadius() {
         return radius;
     }
+    
+    public float getHeight() {
+        return height;
+    }
 
     public void write(JmeExporter ex) throws IOException {
         super.write(ex);

+ 31 - 1
jme3-core/src/main/java/com/jme3/animation/AnimControl.java

@@ -38,11 +38,14 @@ import com.jme3.scene.Mesh;
 import com.jme3.scene.Spatial;
 import com.jme3.scene.control.AbstractControl;
 import com.jme3.scene.control.Control;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import com.jme3.util.TempVars;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.Map;
 import java.util.Map.Entry;
 
 /**
@@ -65,7 +68,7 @@ import java.util.Map.Entry;
  *
  * @author Kirill Vainer
  */
-public final class AnimControl extends AbstractControl implements Cloneable {
+public final class AnimControl extends AbstractControl implements Cloneable, JmeCloneable {
 
     /**
      * Skeleton object must contain corresponding data for the targets' weight buffers.
@@ -108,6 +111,7 @@ public final class AnimControl extends AbstractControl implements Cloneable {
     /**
      * Internal use only.
      */
+    @Override
     public Control cloneForSpatial(Spatial spatial) {
         try {
             AnimControl clone = (AnimControl) super.clone();
@@ -130,6 +134,32 @@ public final class AnimControl extends AbstractControl implements Cloneable {
         }
     }
 
+    @Override   
+    public Object jmeClone() {
+        AnimControl clone = (AnimControl) super.jmeClone();
+        clone.channels = new ArrayList<AnimChannel>();
+        clone.listeners = new ArrayList<AnimEventListener>();
+
+        return clone;
+    }     
+
+    @Override   
+    public void cloneFields( Cloner cloner, Object original ) {
+        super.cloneFields(cloner, original);
+        
+        this.skeleton = cloner.clone(skeleton);
+ 
+        // Note cloneForSpatial() never actually cloned the animation map... just its reference       
+        HashMap<String, Animation> newMap = new HashMap<>();
+         
+        // animationMap is cloned, but only ClonableTracks will be cloned as they need a reference to a cloned spatial
+        for( Map.Entry<String, Animation> e : animationMap.entrySet() ) {
+            newMap.put(e.getKey(), cloner.clone(e.getValue()));
+        }
+        
+        this.animationMap = newMap;
+    }
+         
     /**
      * @param animations Set the animations that this <code>AnimControl</code>
      * will be capable of playing. The animations should be compatible

+ 30 - 1
jme3-core/src/main/java/com/jme3/animation/Animation.java

@@ -35,6 +35,8 @@ import com.jme3.export.*;
 import com.jme3.scene.Spatial;
 import com.jme3.util.SafeArrayList;
 import com.jme3.util.TempVars;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 
 /**
@@ -42,7 +44,7 @@ import java.io.IOException;
  * 
  * @author Kirill Vainer, Marcin Roguski (Kaelthas)
  */
-public class Animation implements Savable, Cloneable {
+public class Animation implements Savable, Cloneable, JmeCloneable {
 
     /** 
      * The name of the animation. 
@@ -190,6 +192,33 @@ public class Animation implements Savable, Cloneable {
         }
     }
 
+    @Override   
+    public Object jmeClone() {
+        try {
+            return super.clone();
+        } catch( CloneNotSupportedException e ) {
+            throw new RuntimeException("Error cloning", e);
+        }
+    }     
+
+    @Override   
+    public void cloneFields( Cloner cloner, Object original ) {
+         
+        // There is some logic here that I'm copying but I'm not sure if
+        // it's a mistake or not.  If a track is not a CloneableTrack then it
+        // isn't cloned at all... even though they all implement clone() methods. -pspeed
+        SafeArrayList<Track> newTracks = new SafeArrayList<>(Track.class);
+        for( Track track : tracks ) {
+            if( track instanceof ClonableTrack ) {
+                newTracks.add(cloner.clone(track));
+            } else {
+                // this is the part that seems fishy 
+                newTracks.add(track);
+            }
+        }
+        this.tracks = newTracks;
+    }
+         
     @Override
     public String toString() {
         return getClass().getSimpleName() + "[name=" + name + ", length=" + length + ']';

+ 24 - 1
jme3-core/src/main/java/com/jme3/animation/AudioTrack.java

@@ -39,6 +39,8 @@ import com.jme3.export.OutputCapsule;
 import com.jme3.scene.Node;
 import com.jme3.scene.Spatial;
 import com.jme3.util.TempVars;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 import java.util.logging.Level;
 import java.util.logging.Logger;
@@ -174,6 +176,7 @@ public class AudioTrack implements ClonableTrack {
      * @param spatial the Spatial holding the AnimControl
      * @return the cloned Track with proper reference
      */
+    @Override
     public Track cloneForSpatial(Spatial spatial) {
         AudioTrack audioTrack = new AudioTrack();
         audioTrack.length = this.length;
@@ -192,7 +195,27 @@ public class AudioTrack implements ClonableTrack {
         return audioTrack;
     }
 
-    /**
+    @Override   
+    public Object jmeClone() {
+        try {
+            return super.clone();
+        } catch( CloneNotSupportedException e ) {
+            throw new RuntimeException("Error cloning", e);
+        }
+    }     
+
+
+    @Override   
+    public void cloneFields( Cloner cloner, Object original ) {
+        // Duplicating the old cloned state from cloneForSpatial()
+        this.initialized = false;
+        this.started = false;
+        this.played = false; 
+        this.audio = cloner.clone(audio);
+    }
+         
+         
+    /**    
      * recursive function responsible for finding the newly cloned AudioNode
      *
      * @param spat

+ 7 - 11
jme3-core/src/main/java/com/jme3/animation/Bone.java

@@ -139,11 +139,7 @@ public final class Bone implements Savable {
     /**
      * Special-purpose copy constructor. 
      * <p>
-     * Only copies the name and bind pose from the original.
-     * <p>
-     * WARNING: Local bind pose and world inverse bind pose transforms shallow 
-     * copied. Modifying that data on the original bone will cause it to
-     * be recomputed on any cloned bones.
+     * Only copies the name, user control state and bind pose transforms from the original.
      * <p>
      * The rest of the data is <em>NOT</em> copied, as it will be
      * generated automatically when the bone is animated.
@@ -155,13 +151,13 @@ public final class Bone implements Savable {
 
         userControl = source.userControl;
 
-        bindPos = source.bindPos;
-        bindRot = source.bindRot;
-        bindScale = source.bindScale;
+        bindPos = source.bindPos.clone();
+        bindRot = source.bindRot.clone();
+        bindScale = source.bindScale.clone();
 
-        modelBindInversePos = source.modelBindInversePos;
-        modelBindInverseRot = source.modelBindInverseRot;
-        modelBindInverseScale = source.modelBindInverseScale;
+        modelBindInversePos = source.modelBindInversePos.clone();
+        modelBindInverseRot = source.modelBindInverseRot.clone();
+        modelBindInverseScale = source.modelBindInverseScale.clone();
 
         // parent and children will be assigned manually..
     }

+ 2 - 1
jme3-core/src/main/java/com/jme3/animation/ClonableTrack.java

@@ -32,6 +32,7 @@
 package com.jme3.animation;
 
 import com.jme3.scene.Spatial;
+import com.jme3.util.clone.JmeCloneable;
 
 /**
  * An interface that allow to clone a Track for a given Spatial.
@@ -43,7 +44,7 @@ import com.jme3.scene.Spatial;
  *
  * @author Nehon
  */
-public interface ClonableTrack extends Track {
+public interface ClonableTrack extends Track, JmeCloneable {
 
     /**
      * Allows to clone the track for a given Spatial.

+ 34 - 0
jme3-core/src/main/java/com/jme3/animation/EffectTrack.java

@@ -44,6 +44,8 @@ import com.jme3.scene.Spatial.CullHint;
 import com.jme3.scene.control.AbstractControl;
 import com.jme3.scene.control.Control;
 import com.jme3.util.TempVars;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 import java.util.logging.Level;
 import java.util.logging.Logger;
@@ -116,6 +118,22 @@ public class EffectTrack implements ClonableTrack {
             }
         }
 
+        @Override   
+        public Object jmeClone() {
+            KillParticleControl c = new KillParticleControl();
+            //this control should be removed as it shouldn't have been persisted in the first place
+            //In the quest to find the less hackish solution to achieve this, 
+            //making it remove itself from the spatial in the first update loop when loaded was the less bad. 
+            c.remove = true;
+            c.spatial = spatial;
+            return c;
+        }     
+
+        @Override   
+        public void cloneFields( Cloner cloner, Object original ) { 
+            this.spatial = cloner.clone(spatial);
+        }
+         
         @Override
         protected void controlRender(RenderManager rm, ViewPort vp) {
         }
@@ -263,6 +281,7 @@ public class EffectTrack implements ClonableTrack {
      * @param spatial the Spatial holding the AnimControl
      * @return the cloned Track with proper reference
      */
+    @Override
     public Track cloneForSpatial(Spatial spatial) {
         EffectTrack effectTrack = new EffectTrack();
         effectTrack.particlesPerSeconds = this.particlesPerSeconds;
@@ -283,6 +302,21 @@ public class EffectTrack implements ClonableTrack {
         return effectTrack;
     }
 
+    @Override   
+    public Object jmeClone() {
+        try {
+            return super.clone();
+        } catch( CloneNotSupportedException e ) {
+            throw new RuntimeException("Error cloning", e);
+        }
+    }     
+
+
+    @Override   
+    public void cloneFields( Cloner cloner, Object original ) { 
+        this.emitter = cloner.clone(emitter);
+    }
+         
     /**
      * recursive function responsible for finding the newly cloned Emitter
      *

+ 12 - 1
jme3-core/src/main/java/com/jme3/animation/Skeleton.java

@@ -34,6 +34,8 @@ package com.jme3.animation;
 import com.jme3.export.*;
 import com.jme3.math.Matrix4f;
 import com.jme3.util.TempVars;
+import com.jme3.util.clone.JmeCloneable;
+import com.jme3.util.clone.Cloner;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
@@ -45,7 +47,7 @@ import java.util.List;
  * 
  * @author Kirill Vainer
  */
-public final class Skeleton implements Savable {
+public final class Skeleton implements Savable, JmeCloneable {
 
     private Bone[] rootBones;
     private Bone[] boneList;
@@ -118,6 +120,15 @@ public final class Skeleton implements Savable {
     public Skeleton() {
     }
 
+    @Override   
+    public Object jmeClone() {
+        return new Skeleton(this);
+    }     
+
+    @Override   
+    public void cloneFields( Cloner cloner, Object original ) {
+    }
+
     private void createSkinningMatrices() {
         skinningMatrixes = new Matrix4f[boneList.length];
         for (int i = 0; i < skinningMatrixes.length; i++) {

+ 27 - 1
jme3-core/src/main/java/com/jme3/animation/SkeletonControl.java

@@ -46,6 +46,8 @@ import com.jme3.scene.control.Control;
 import com.jme3.shader.VarType;
 import com.jme3.util.SafeArrayList;
 import com.jme3.util.TempVars;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 import java.nio.Buffer;
 import java.nio.ByteBuffer;
@@ -63,7 +65,7 @@ import java.util.logging.Logger;
  *
  * @author Rémy Bouquet Based on AnimControl by Kirill Vainer
  */
-public class SkeletonControl extends AbstractControl implements Cloneable {
+public class SkeletonControl extends AbstractControl implements Cloneable, JmeCloneable {
 
     /**
      * The skeleton of the model.
@@ -345,6 +347,7 @@ public class SkeletonControl extends AbstractControl implements Cloneable {
         }
     }
 
+    @Override
     public Control cloneForSpatial(Spatial spatial) {
         Node clonedNode = (Node) spatial;
         SkeletonControl clone = new SkeletonControl();
@@ -385,6 +388,29 @@ public class SkeletonControl extends AbstractControl implements Cloneable {
         return clone;
     }
 
+    @Override   
+    public Object jmeClone() {
+        return super.jmeClone();
+    }     
+
+    @Override   
+    public void cloneFields( Cloner cloner, Object original ) {
+        super.cloneFields(cloner, original);
+         
+        this.skeleton = cloner.clone(skeleton);
+        
+        // If the targets were cloned then this will clone them.  If the targets
+        // were shared then this will share them.
+        this.targets = cloner.clone(targets);
+        
+        // Not automatic set cloning yet
+        Set<Material> newMaterials = new HashSet<Material>();
+        for( Material m : this.materials ) {
+            newMaterials.add(cloner.clone(m));
+        }
+        this.materials = newMaterials;
+    }
+         
     /**
      *
      * @param boneName the name of the bone

+ 17 - 1
jme3-core/src/main/java/com/jme3/animation/TrackInfo.java

@@ -36,6 +36,8 @@ import com.jme3.export.JmeExporter;
 import com.jme3.export.JmeImporter;
 import com.jme3.export.OutputCapsule;
 import com.jme3.export.Savable;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 import java.util.ArrayList;
 
@@ -48,7 +50,7 @@ import java.util.ArrayList;
  *
  * @author Nehon
  */
-public class TrackInfo implements Savable {
+public class TrackInfo implements Savable, JmeCloneable {
 
     ArrayList<Track> tracks = new ArrayList<Track>();
 
@@ -72,4 +74,18 @@ public class TrackInfo implements Savable {
     public void addTrack(Track track) {
         tracks.add(track);
     }
+    
+    @Override   
+    public Object jmeClone() {
+        try {
+            return super.clone();
+        } catch( CloneNotSupportedException e ) {
+            throw new RuntimeException("Error cloning", e);
+        }
+    }     
+
+    @Override   
+    public void cloneFields( Cloner cloner, Object original ) {
+        this.tracks = cloner.clone(tracks); 
+    }             
 }

+ 14 - 1
jme3-core/src/main/java/com/jme3/app/StatsView.java

@@ -41,6 +41,8 @@ import com.jme3.renderer.queue.RenderQueue.Bucket;
 import com.jme3.scene.Node;
 import com.jme3.scene.Spatial;
 import com.jme3.scene.control.Control;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 
 /**
  * The <code>StatsView</code> provides a heads-up display (HUD) of various
@@ -58,7 +60,7 @@ import com.jme3.scene.control.Control;
  * rootNode.attachChild(statsView);<br/>
  * </code>
  */
-public class StatsView extends Node implements Control {
+public class StatsView extends Node implements Control, JmeCloneable {
 
     private BitmapText statText;
     private Statistics statistics;
@@ -115,10 +117,21 @@ public class StatsView extends Node implements Control {
         //statistics.clearFrame();
     }
 
+    @Override
     public Control cloneForSpatial(Spatial spatial) {
         return (Control) spatial;
     }
 
+    @Override   
+    public Object jmeClone() {
+        throw new UnsupportedOperationException("Not yet implemented.");
+    }     
+
+    @Override   
+    public void cloneFields( Cloner cloner, Object original ) { 
+        throw new UnsupportedOperationException("Not yet implemented.");
+    }
+         
     public void setSpatial(Spatial spatial) {
     }
 

+ 14 - 12
jme3-core/src/main/java/com/jme3/app/state/ScreenshotAppState.java

@@ -249,21 +249,23 @@ public class ScreenshotAppState extends AbstractAppState implements ActionListen
             }
             logger.log(Level.FINE, "Saving ScreenShot to: {0}", file.getAbsolutePath());
 
-            OutputStream outStream = null;
             try {
-                outStream = new FileOutputStream(file);
-                JmeSystem.writeImageFile(outStream, "png", outBuf, width, height);
+                writeImageFile(file);
             } catch (IOException ex) {
                 logger.log(Level.SEVERE, "Error while saving screenshot", ex);
-            } finally {
-                if (outStream != null){
-                    try {
-                        outStream.close();
-                    } catch (IOException ex) {
-                        logger.log(Level.SEVERE, "Error while saving screenshot", ex);
-                    }
-                }
-            }
+            }                
         }
     }
+    
+    /**
+     *  Called by postFrame() once the screen has been captured to outBuf.
+     */
+    protected void writeImageFile( File file ) throws IOException {
+        OutputStream outStream = new FileOutputStream(file);
+        try {
+            JmeSystem.writeImageFile(outStream, "png", outBuf, width, height);
+        } finally {
+            outStream.close();
+        }
+    } 
 }

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

+ 29 - 1
jme3-core/src/main/java/com/jme3/cinematic/events/MotionEvent.java

@@ -47,6 +47,8 @@ import com.jme3.renderer.RenderManager;
 import com.jme3.renderer.ViewPort;
 import com.jme3.scene.Spatial;
 import com.jme3.scene.control.Control;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 
 /**
@@ -56,7 +58,7 @@ import java.io.IOException;
  *
  * @author Nehon
  */
-public class MotionEvent extends AbstractCinematicEvent implements Control {
+public class MotionEvent extends AbstractCinematicEvent implements Control, JmeCloneable {
 
     protected Spatial spatial;
     protected int currentWayPoint;
@@ -274,6 +276,7 @@ public class MotionEvent extends AbstractCinematicEvent implements Control {
      * @param spatial
      * @return
      */
+    @Override
     public Control cloneForSpatial(Spatial spatial) {
         MotionEvent control = new MotionEvent(spatial, path);
         control.playState = playState;
@@ -291,6 +294,31 @@ public class MotionEvent extends AbstractCinematicEvent implements Control {
         return control;
     }
 
+    @Override   
+    public Object jmeClone() {
+        MotionEvent control = new MotionEvent();
+        control.path = path;
+        control.playState = playState;
+        control.currentWayPoint = currentWayPoint;
+        control.currentValue = currentValue;
+        control.direction = direction.clone();
+        control.lookAt = lookAt.clone();
+        control.upVector = upVector.clone();
+        control.rotation = rotation.clone();
+        control.initialDuration = initialDuration;
+        control.speed = speed;
+        control.loopMode = loopMode;
+        control.directionType = directionType;
+        control.spatial = spatial;
+
+        return control;
+    }     
+
+    @Override   
+    public void cloneFields( Cloner cloner, Object original ) { 
+        this.spatial = cloner.clone(spatial);
+    }
+         
     @Override
     public void onPlay() {
         traveledDistance = 0;

+ 18 - 1
jme3-core/src/main/java/com/jme3/effect/ParticleEmitter.java

@@ -54,6 +54,8 @@ import com.jme3.scene.Geometry;
 import com.jme3.scene.Spatial;
 import com.jme3.scene.control.Control;
 import com.jme3.util.TempVars;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 
 /**
@@ -108,7 +110,7 @@ public class ParticleEmitter extends Geometry {
     private transient Vector3f temp = new Vector3f();
     private transient Vector3f lastPos;
 
-    public static class ParticleEmitterControl implements Control {
+    public static class ParticleEmitterControl implements Control, JmeCloneable {
 
         ParticleEmitter parentEmitter;
 
@@ -119,11 +121,26 @@ public class ParticleEmitter extends Geometry {
             this.parentEmitter = parentEmitter;
         }
 
+        @Override
         public Control cloneForSpatial(Spatial spatial) {
             return this; // WARNING: Sets wrong control on spatial. Will be
             // fixed automatically by ParticleEmitter.clone() method.
         }
 
+        @Override   
+        public Object jmeClone() {
+            try {
+                return super.clone();
+            } catch( CloneNotSupportedException e ) {
+                throw new RuntimeException("Error cloning", e);
+            }
+        }     
+
+        @Override   
+        public void cloneFields( Cloner cloner, Object original ) { 
+            this.parentEmitter = cloner.clone(parentEmitter);
+        }
+             
         public void setSpatial(Spatial spatial) {
         }
 

+ 21 - 1
jme3-core/src/main/java/com/jme3/input/ChaseCamera.java

@@ -43,13 +43,15 @@ import com.jme3.renderer.RenderManager;
 import com.jme3.renderer.ViewPort;
 import com.jme3.scene.Spatial;
 import com.jme3.scene.control.Control;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 
 /**
  * A camera that follows a spatial and can turn around it by dragging the mouse
  * @author nehon
  */
-public class ChaseCamera implements ActionListener, AnalogListener, Control {
+public class ChaseCamera implements ActionListener, AnalogListener, Control, JmeCloneable {
 
     protected Spatial target = null;
     protected float minVerticalRotation = 0.00f;
@@ -567,6 +569,7 @@ public class ChaseCamera implements ActionListener, AnalogListener, Control {
      * @param spatial
      * @return
      */
+    @Override
     public Control cloneForSpatial(Spatial spatial) {
         ChaseCamera cc = new ChaseCamera(cam, spatial, inputManager);
         cc.setMaxDistance(getMaxDistance());
@@ -574,6 +577,23 @@ public class ChaseCamera implements ActionListener, AnalogListener, Control {
         return cc;
     }
 
+    @Override   
+    public Object jmeClone() {
+        ChaseCamera cc = new ChaseCamera(cam, inputManager);
+        cc.target = target;
+        cc.setMaxDistance(getMaxDistance());
+        cc.setMinDistance(getMinDistance());
+        return cc;
+    }     
+
+    @Override   
+    public void cloneFields( Cloner cloner, Object original ) { 
+        this.target = cloner.clone(target);
+        computePosition();
+        prevPos = new Vector3f(target.getWorldTranslation());
+        cam.setLocation(pos);
+    }
+         
     /**
      * Sets the spacial for the camera control, should only be used internally
      * @param spatial

+ 32 - 3
jme3-core/src/main/java/com/jme3/material/MatParam.java

@@ -248,16 +248,45 @@ When arrays can be inserted in J3M files
                 if (texKey.isFlipY()) {
                     ret += "Flip ";
                 }
-                if (texVal.getWrap(Texture.WrapAxis.S) == WrapMode.Repeat) {
-                    ret += "Repeat ";
+
+                //Wrap mode
+                ret += getWrapMode(texVal, Texture.WrapAxis.S);
+                ret += getWrapMode(texVal, Texture.WrapAxis.T);
+                ret += getWrapMode(texVal, Texture.WrapAxis.R);
+
+                //Min and Mag filter
+                Texture.MinFilter def =  Texture.MinFilter.BilinearNoMipMaps;
+                if(texVal.getImage().hasMipmaps() || texKey.isGenerateMips()){
+                    def = Texture.MinFilter.Trilinear;
+                }
+                if(texVal.getMinFilter() != def){
+                    ret += "Min" + texVal.getMinFilter().name()+ " ";
+                }
+
+                if(texVal.getMagFilter() != Texture.MagFilter.Bilinear){
+                    ret += "Mag" + texVal.getMagFilter().name()+ " ";
                 }
 
-                return ret + texKey.getName();
+                return ret + "\"" + texKey.getName() + "\"";
             default:
                 return null; // parameter type not supported in J3M
         }
     }
 
+    private String getWrapMode(Texture texVal, Texture.WrapAxis axis) {
+        WrapMode mode = WrapMode.EdgeClamp;
+        try{
+            mode = texVal.getWrap(axis);
+        }catch (IllegalArgumentException e){
+            //this axis doesn't exist on the texture
+            return "";
+        }
+        if(mode != WrapMode.EdgeClamp){
+            return"Wrap"+ mode.name() + "_" + axis.name() + " ";
+        }
+        return "";
+    }
+
     @Override
     public MatParam clone() {
         try {

+ 4 - 2
jme3-core/src/main/java/com/jme3/post/FilterPostProcessor.java

@@ -401,8 +401,10 @@ public class FilterPostProcessor implements SceneProcessor, Savable {
             viewPort.getCamera().setViewPort(left, right, bottom, top);
             viewPort.setOutputFrameBuffer(outputBuffer);
             viewPort = null;
-            
-            renderFrameBuffer.dispose();
+
+            if(renderFrameBuffer != null){
+                renderFrameBuffer.dispose();
+            }
             if(depthTexture!=null){
                depthTexture.getImage().dispose();
             }

+ 38 - 95
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>();
     /**
@@ -115,10 +116,9 @@ public class BatchNode extends GeometryGroupNode {
     }
 
     @Override
-    public void onGeoemtryUnassociated(Geometry geom) {
+    public void onGeometryUnassociated(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,11 @@ public class BatchNode extends GeometryGroupNode {
                 }
             }
         }
-        Geometry geometry;        
+        Geometry geometry;
+        
+        public final Geometry getGeometry() {
+            return geometry;
+        }
     }
 
     protected void setNeedsFullRebatch(boolean needsFullRebatch) {
@@ -771,4 +703,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;
+    }
 }

+ 1 - 1
jme3-core/src/main/java/com/jme3/scene/Geometry.java

@@ -344,7 +344,7 @@ public class Geometry extends Spatial {
         if (groupNode != null) {
             // Once the geometry is removed 
             // from the parent, the group node needs to be updated.
-            groupNode.onGeoemtryUnassociated(this);
+            groupNode.onGeometryUnassociated(this);
             groupNode = null;
             
             // change the default to -1 to make error detection easier

+ 1 - 1
jme3-core/src/main/java/com/jme3/scene/GeometryGroupNode.java

@@ -83,5 +83,5 @@ public abstract class GeometryGroupNode extends Node {
      * 
      * @param geom The Geometry which is being unassociated.
      */
-    public abstract void onGeoemtryUnassociated(Geometry geom);
+    public abstract void onGeometryUnassociated(Geometry geom);
 }

+ 17 - 1
jme3-core/src/main/java/com/jme3/scene/control/AbstractControl.java

@@ -38,6 +38,8 @@ import com.jme3.export.OutputCapsule;
 import com.jme3.renderer.RenderManager;
 import com.jme3.renderer.ViewPort;
 import com.jme3.scene.Spatial;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 
 /**
@@ -45,7 +47,7 @@ import java.io.IOException;
  *
  * @author Kirill Vainer
  */
-public abstract class AbstractControl implements Control {
+public abstract class AbstractControl implements Control, JmeCloneable {
 
     protected boolean enabled = true;
     protected Spatial spatial;
@@ -105,6 +107,20 @@ public abstract class AbstractControl implements Control {
         } 
     }
 
+    @Override
+    public Object jmeClone() {
+        try {
+            return super.clone();
+        } catch( CloneNotSupportedException e ) {
+            throw new RuntimeException( "Can't clone control for spatial", e );
+        }
+    }     
+
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) { 
+        this.spatial = cloner.clone(spatial);
+    }
+         
     public void update(float tpf) {
         if (!enabled)
             return;

+ 7 - 6
jme3-core/src/main/java/com/jme3/scene/control/BillboardControl.java

@@ -86,12 +86,13 @@ public class BillboardControl extends AbstractControl {
         alignment = Alignment.Screen;
     }
 
-    public Control cloneForSpatial(Spatial spatial) {
-        BillboardControl control = new BillboardControl();
-        control.alignment = this.alignment;
-        control.setSpatial(spatial);
-        return control;
-    }
+    // default implementation from AbstractControl is equivalent
+    //public Control cloneForSpatial(Spatial spatial) {
+    //    BillboardControl control = new BillboardControl();
+    //    control.alignment = this.alignment;
+    //    control.setSpatial(spatial);
+    //    return control;
+    //}
 
     @Override
     protected void controlUpdate(float tpf) {

+ 8 - 7
jme3-core/src/main/java/com/jme3/scene/control/CameraControl.java

@@ -136,13 +136,14 @@ public class CameraControl extends AbstractControl {
         // nothing to do
     }
 
-    @Override
-    public Control cloneForSpatial(Spatial newSpatial) {
-        CameraControl control = new CameraControl(camera, controlDir);
-        control.setSpatial(newSpatial);
-        control.setEnabled(isEnabled());
-        return control;
-    }
+    // default implementation from AbstractControl is equivalent
+    //@Override
+    //public Control cloneForSpatial(Spatial newSpatial) {
+    //    CameraControl control = new CameraControl(camera, controlDir);
+    //    control.setSpatial(newSpatial);
+    //    control.setEnabled(isEnabled());
+    //    return control;
+    //}
     private static final String CONTROL_DIR_NAME = "controlDir";
     private static final String CAMERA_NAME = "camera";
     

+ 8 - 7
jme3-core/src/main/java/com/jme3/scene/control/LightControl.java

@@ -167,13 +167,14 @@ public class LightControl extends AbstractControl {
         // nothing to do
     }
 
-    @Override
-    public Control cloneForSpatial(Spatial newSpatial) {
-        LightControl control = new LightControl(light, controlDir);
-        control.setSpatial(newSpatial);
-        control.setEnabled(isEnabled());
-        return control;
-    }
+    // default implementation from AbstractControl is equivalent
+    //@Override
+    //public Control cloneForSpatial(Spatial newSpatial) {
+    //    LightControl control = new LightControl(light, controlDir);
+    //    control.setSpatial(newSpatial);
+    //    control.setEnabled(isEnabled());
+    //    return control;
+    //}
     private static final String CONTROL_DIR_NAME = "controlDir";
     private static final String LIGHT_NAME = "light";
     

+ 13 - 2
jme3-core/src/main/java/com/jme3/scene/control/LodControl.java

@@ -43,6 +43,8 @@ import com.jme3.renderer.ViewPort;
 import com.jme3.scene.Geometry;
 import com.jme3.scene.Mesh;
 import com.jme3.scene.Spatial;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 
 /**
@@ -56,7 +58,7 @@ import java.io.IOException;
  * and will update the spatial's LOD if the camera has moved by a specified
  * amount.
  */
-public class LodControl extends AbstractControl implements Cloneable {
+public class LodControl extends AbstractControl implements Cloneable, JmeCloneable {
 
     private float trisPerPixel = 1f;
     private float distTolerance = 1f;
@@ -140,7 +142,16 @@ public class LodControl extends AbstractControl implements Cloneable {
         clone.lastLevel = 0;
         clone.numTris = numTris != null ? numTris.clone() : null;
         return clone;
-   }
+    }
+
+    @Override
+    public Object jmeClone() {
+        LodControl clone = (LodControl)super.jmeClone();
+        clone.lastDistance = 0;
+        clone.lastLevel = 0;
+        clone.numTris = numTris != null ? numTris.clone() : null;
+        return clone;
+    }     
 
     @Override
     protected void controlUpdate(float tpf) {

+ 14 - 0
jme3-core/src/main/java/com/jme3/scene/control/UpdateControl.java

@@ -35,6 +35,8 @@ import com.jme3.app.AppTask;
 import com.jme3.renderer.RenderManager;
 import com.jme3.renderer.ViewPort;
 import com.jme3.scene.Spatial;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.Future;
@@ -85,6 +87,7 @@ public class UpdateControl extends AbstractControl {
         
     }
 
+    @Override
     public Control cloneForSpatial(Spatial newSpatial) {
         UpdateControl control = new UpdateControl(); 
         control.setSpatial(newSpatial);
@@ -93,4 +96,15 @@ public class UpdateControl extends AbstractControl {
         return control;
     }
     
+    @Override
+    public Object jmeClone() {
+        UpdateControl clone = (UpdateControl)super.jmeClone();
+        
+        // This is kind of questionable since the tasks aren't cloned and have
+        // no reference to the new spatial or anything.  They'll get run again
+        // but it's not clear to me why that would be desired.  I'm doing it
+        // because the old cloneForSpatial() code does.  FIXME?   -pspeed
+        clone.taskQueue.addAll(taskQueue);
+        return clone;
+    }     
 }

+ 18 - 2
jme3-core/src/main/java/com/jme3/scene/instancing/InstancedNode.java

@@ -44,6 +44,8 @@ import com.jme3.scene.control.Control;
 import com.jme3.export.JmeExporter;
 import com.jme3.export.JmeImporter;
 import com.jme3.material.MatParam;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 import java.util.HashMap;
 
@@ -106,7 +108,7 @@ public class InstancedNode extends GeometryGroupNode {
         }
     }
     
-    private static class InstancedNodeControl implements Control {
+    private static class InstancedNodeControl implements Control, JmeCloneable {
 
         private InstancedNode node;
         
@@ -124,6 +126,20 @@ public class InstancedNode extends GeometryGroupNode {
             // fixed automatically by InstancedNode.clone() method.
         }
         
+        @Override
+        public Object jmeClone() {
+            try {
+                return super.clone();
+            } catch( CloneNotSupportedException e ) {
+                throw new RuntimeException("Error cloning control", e);
+            }
+        }     
+
+        @Override
+        public void cloneFields( Cloner cloner, Object original ) { 
+            this.node = cloner.clone(node);
+        }
+         
         public void setSpatial(Spatial spatial){
         }
         
@@ -329,7 +345,7 @@ public class InstancedNode extends GeometryGroupNode {
     }
 
     @Override
-    public void onGeoemtryUnassociated(Geometry geom) {
+    public void onGeometryUnassociated(Geometry geom) {
         removeFromInstancedGeometry(geom);
     }
 }

+ 2 - 1
jme3-core/src/main/java/com/jme3/scene/shape/Cylinder.java

@@ -211,7 +211,7 @@ public class Cylinder extends Mesh {
      */
     public void updateGeometry(int axisSamples, int radialSamples,
             float radius, float radius2, float height, boolean closed, boolean inverted) {
-        this.axisSamples = axisSamples + (closed ? 2 : 0);
+        this.axisSamples = axisSamples;
         this.radialSamples = radialSamples;
         this.radius = radius;
         this.radius2 = radius2;
@@ -222,6 +222,7 @@ public class Cylinder extends Mesh {
 //        VertexBuffer pvb = getBuffer(Type.Position);
 //        VertexBuffer nvb = getBuffer(Type.Normal);
 //        VertexBuffer tvb = getBuffer(Type.TexCoord);
+        axisSamples += (closed ? 2 : 0);
 
         // Vertices
         int vertCount = axisSamples * (radialSamples + 1) + (closed ? 2 : 0);

+ 46 - 1
jme3-core/src/main/java/com/jme3/shadow/AbstractShadowFilter.java

@@ -37,6 +37,7 @@ import com.jme3.export.JmeExporter;
 import com.jme3.export.JmeImporter;
 import com.jme3.export.OutputCapsule;
 import com.jme3.material.Material;
+import com.jme3.material.RenderState;
 import com.jme3.math.Matrix4f;
 import com.jme3.math.Vector4f;
 import com.jme3.post.Filter;
@@ -44,6 +45,7 @@ import com.jme3.renderer.RenderManager;
 import com.jme3.renderer.ViewPort;
 import com.jme3.renderer.queue.RenderQueue;
 import com.jme3.texture.FrameBuffer;
+
 import java.io.IOException;
 
 /**
@@ -74,6 +76,9 @@ public abstract class AbstractShadowFilter<T extends AbstractShadowRenderer> ext
         material = new Material(manager, "Common/MatDefs/Shadow/PostShadowFilter.j3md");       
         this.shadowRenderer = shadowRenderer;
         this.shadowRenderer.setPostShadowMaterial(material);
+
+        //this is legacy setting for shadows with backface shadows
+        this.shadowRenderer.setRenderBackFacesShadows(true);
     }
 
     @Override
@@ -126,7 +131,7 @@ public abstract class AbstractShadowFilter<T extends AbstractShadowRenderer> ext
       /**
      * How far the shadows are rendered in the view
      *
-     * @see setShadowZExtend(float zFar)
+     * @see #setShadowZExtend(float zFar)
      * @return shadowZExtend
      */
     public float getShadowZExtend() {
@@ -248,6 +253,46 @@ public abstract class AbstractShadowFilter<T extends AbstractShadowRenderer> ext
         shadowRenderer.setEdgeFilteringMode(filterMode);
     }
 
+    /**
+     *
+     * !! WARNING !! this parameter is defaulted to true for the ShadowFilter.
+     * Setting it to true, may produce edges artifacts on shadows.     *
+     *
+     * Set to true if you want back faces shadows on geometries.
+     * Note that back faces shadows will be blended over dark lighten areas and may produce overly dark lighting.
+     *
+     * Setting this parameter will override this parameter for ALL materials in the scene.
+     * This also will automatically adjust the faceCullMode and the PolyOffset of the pre shadow pass.
+     * You can modify them by using {@link #getPreShadowForcedRenderState()}
+     *
+     * If you want to set it differently for each material in the scene you have to use the ShadowRenderer instead
+     * of the shadow filter.
+     *
+     * @param renderBackFacesShadows true or false.
+     */
+    public void setRenderBackFacesShadows(Boolean renderBackFacesShadows) {
+        shadowRenderer.setRenderBackFacesShadows(renderBackFacesShadows);
+    }
+
+    /**
+     * if this filter renders back faces shadows
+     * @return true if this filter renders back faces shadows
+     */
+    public boolean isRenderBackFacesShadows() {
+        return shadowRenderer.isRenderBackFacesShadows();
+    }
+
+    /**
+     * returns the pre shadows pass render state.
+     * use it to adjust the RenderState parameters of the pre shadow pass.
+     * Note that this will be overriden if the preShadow technique in the material has a ForcedRenderState
+     * @return the pre shadow render state.
+     */
+    public RenderState getPreShadowForcedRenderState() {
+        return shadowRenderer.getPreShadowForcedRenderState();
+    }
+
+
     /**
      * returns the the edge filtering mode
      *

+ 85 - 20
jme3-core/src/main/java/com/jme3/shadow/AbstractShadowRenderer.java

@@ -31,10 +31,6 @@
  */
 package com.jme3.shadow;
 
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
 import com.jme3.asset.AssetManager;
 import com.jme3.export.InputCapsule;
 import com.jme3.export.JmeExporter;
@@ -42,6 +38,7 @@ import com.jme3.export.JmeImporter;
 import com.jme3.export.OutputCapsule;
 import com.jme3.export.Savable;
 import com.jme3.material.Material;
+import com.jme3.material.RenderState;
 import com.jme3.math.ColorRGBA;
 import com.jme3.math.Matrix4f;
 import com.jme3.math.Vector2f;
@@ -67,6 +64,10 @@ import com.jme3.texture.Texture.ShadowCompareMode;
 import com.jme3.texture.Texture2D;
 import com.jme3.ui.Picture;
 
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * abstract shadow renderer that holds commons feature to have for a shadow
  * renderer
@@ -92,6 +93,9 @@ public abstract class AbstractShadowRenderer implements SceneProcessor, Savable
     protected EdgeFilteringMode edgeFilteringMode = EdgeFilteringMode.Bilinear;
     protected CompareMode shadowCompareMode = CompareMode.Hardware;
     protected Picture[] dispPic;
+    protected RenderState forcedRenderState = new RenderState();
+    protected Boolean renderBackFacesShadows;
+
     /**
      * true if the fallback material should be used, otherwise false
      */
@@ -181,6 +185,14 @@ public abstract class AbstractShadowRenderer implements SceneProcessor, Savable
         setShadowCompareMode(shadowCompareMode);
         setEdgeFilteringMode(edgeFilteringMode);
         setShadowIntensity(shadowIntensity);
+        initForcedRenderState();
+    }
+
+    protected void initForcedRenderState() {
+        forcedRenderState.setFaceCullMode(RenderState.FaceCullMode.Front);
+        forcedRenderState.setColorWrite(false);
+        forcedRenderState.setDepthWrite(true);
+        forcedRenderState.setDepthTest(true);
     }
 
     /**
@@ -356,9 +368,7 @@ public abstract class AbstractShadowRenderer implements SceneProcessor, Savable
      * rendered in the shadow map
      *
      * @param shadowMapIndex the index of the shadow map being rendered
-     * @param sceneOccluders the occluders of the whole scene
-     * @param sceneReceivers the receivers of the whole scene
-     * @param shadowMapOcculders
+     * @param shadowMapOccluders the list of occluders
      * @return
      */
     protected abstract GeometryList getOccludersToRender(int shadowMapIndex, GeometryList shadowMapOccluders);
@@ -425,9 +435,11 @@ public abstract class AbstractShadowRenderer implements SceneProcessor, Savable
 
         renderManager.getRenderer().setFrameBuffer(shadowFB[shadowMapIndex]);
         renderManager.getRenderer().clearBuffers(true, true, true);
+        renderManager.setForcedRenderState(forcedRenderState);
 
         // render shadow casters to shadow map
         viewPort.getQueue().renderShadowQueue(shadowMapOccluders, renderManager, shadowCam, true);
+        renderManager.setForcedRenderState(null);
     }
     boolean debugfrustums = false;
 
@@ -535,18 +547,7 @@ public abstract class AbstractShadowRenderer implements SceneProcessor, Savable
     private void setMatParams(GeometryList l) {
         //iteration throught all the geometries of the list to gather the materials
 
-        matCache.clear();
-        for (int i = 0; i < l.size(); i++) {
-            Material mat = l.get(i).getMaterial();
-            //checking if the material has the post technique and adding it to the material cache
-            if (mat.getMaterialDef().getTechniqueDef(postTechniqueName) != null) {
-                if (!matCache.contains(mat)) {
-                    matCache.add(mat);
-                }
-            } else {
-                needsfallBackMaterial = true;
-            }
-        }
+        buildMatCache(l);
 
         //iterating through the mat cache and setting the parameters
         for (Material mat : matCache) {
@@ -566,6 +567,10 @@ public abstract class AbstractShadowRenderer implements SceneProcessor, Savable
             if (fadeInfo != null) {
                mat.setVector2("FadeInfo", fadeInfo);
             }
+            if(renderBackFacesShadows != null){
+                mat.setBoolean("BackfaceShadows", renderBackFacesShadows);
+            }
+
             setMaterialParameters(mat);
         }
 
@@ -577,6 +582,21 @@ public abstract class AbstractShadowRenderer implements SceneProcessor, Savable
 
     }
 
+    private void buildMatCache(GeometryList l) {
+        matCache.clear();
+        for (int i = 0; i < l.size(); i++) {
+            Material mat = l.get(i).getMaterial();
+            //checking if the material has the post technique and adding it to the material cache
+            if (mat.getMaterialDef().getTechniqueDef(postTechniqueName) != null) {
+                if (!matCache.contains(mat)) {
+                    matCache.add(mat);
+                }
+            } else {
+                needsfallBackMaterial = true;
+            }
+        }
+    }
+
     /**
      * for internal use only
      */
@@ -587,7 +607,10 @@ public abstract class AbstractShadowRenderer implements SceneProcessor, Savable
             postshadowMat.setTexture(shadowMapStringCache[j], shadowMaps[j]);
         }
         if (fadeInfo != null) {
-              postshadowMat.setVector2("FadeInfo", fadeInfo);
+            postshadowMat.setVector2("FadeInfo", fadeInfo);
+        }
+        if(renderBackFacesShadows != null){
+            postshadowMat.setBoolean("BackfaceShadows", renderBackFacesShadows);
         }
     }
     
@@ -730,6 +753,48 @@ public abstract class AbstractShadowRenderer implements SceneProcessor, Savable
     @Deprecated
     public void setFlushQueues(boolean flushQueues) {}
 
+
+    /**
+     * returns the pre shadows pass render state.
+     * use it to adjust the RenderState parameters of the pre shadow pass.
+     * Note that this will be overriden if the preShadow technique in the material has a ForcedRenderState
+     * @return the pre shadow render state.
+     */
+    public RenderState getPreShadowForcedRenderState() {
+        return forcedRenderState;
+    }
+
+    /**
+     * Set to true if you want back faces shadows on geometries.
+     * Note that back faces shadows will be blended over dark lighten areas and may produce overly dark lighting.
+     *
+     * Also note that setting this parameter will override this parameter for ALL materials in the scene.
+     * You can alternatively change this parameter on a single material using {@link Material#setBoolean(String, boolean)}
+     *
+     * This also will automatically adjust the faceCullMode and the PolyOffset of the pre shadow pass.
+     * You can modify them by using {@link #getPreShadowForcedRenderState()}
+     *
+     * @param renderBackFacesShadows true or false.
+     */
+    public void setRenderBackFacesShadows(Boolean renderBackFacesShadows) {
+        this.renderBackFacesShadows = renderBackFacesShadows;
+        if(renderBackFacesShadows) {
+            getPreShadowForcedRenderState().setPolyOffset(5, 3);
+            getPreShadowForcedRenderState().setFaceCullMode(RenderState.FaceCullMode.Back);
+        }else{
+            getPreShadowForcedRenderState().setPolyOffset(0, 0);
+            getPreShadowForcedRenderState().setFaceCullMode(RenderState.FaceCullMode.Front);
+        }
+    }
+
+    /**
+     * if this processor renders back faces shadows
+     * @return true if this processor renders back faces shadows
+     */
+    public boolean isRenderBackFacesShadows() {
+        return renderBackFacesShadows != null?renderBackFacesShadows:false;
+    }
+
     /**
      * De-serialize this instance, for example when loading from a J3O file.
      *

+ 2 - 0
jme3-core/src/main/java/com/jme3/shadow/DirectionalLightShadowRenderer.java

@@ -215,6 +215,7 @@ public class DirectionalLightShadowRenderer extends AbstractShadowRenderer {
     @Override
     protected void setMaterialParameters(Material material) {
         material.setColor("Splits", splits);
+        material.setVector3("LightDir", light.getDirection());
         if (fadeInfo != null) {
             material.setVector2("FadeInfo", fadeInfo);
         }
@@ -224,6 +225,7 @@ public class DirectionalLightShadowRenderer extends AbstractShadowRenderer {
     protected void clearMaterialParameters(Material material) {
         material.clearParam("Splits");
         material.clearParam("FadeInfo");
+        material.clearParam("LightDir");
     }
 
     /**

+ 2 - 0
jme3-core/src/main/java/com/jme3/system/JmeSystemDelegate.java

@@ -157,6 +157,8 @@ public abstract class JmeSystemDelegate {
             return false;
         } else if (arch.equals("aarch64")) {
             return true;
+        } else if (arch.equals("armv7") || arch.equals("armv7l")) {
+            return false;
         } else if (arch.equals("arm")) {
             return false;
         } else {

+ 15 - 27
jme3-core/src/main/java/com/jme3/util/BufferUtils.java

@@ -51,7 +51,6 @@ import java.nio.IntBuffer;
 import java.nio.LongBuffer;
 import java.nio.ShortBuffer;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -1268,7 +1267,6 @@ public final class BufferUtils {
             System.out.println(store.toString());
         }
     }
-    private static final AtomicBoolean loadedMethods = new AtomicBoolean(false);
     private static Method cleanerMethod = null;
     private static Method cleanMethod = null;
     private static Method viewedBufferMethod = null;
@@ -1288,31 +1286,23 @@ public final class BufferUtils {
         }
     }
 
-    private static void loadCleanerMethods() {
-        // If its already true, exit, if not, set it to true.
-        if (BufferUtils.loadedMethods.getAndSet(true)) {
-            return;
+    static {
+        // Oracle JRE / OpenJDK
+        cleanerMethod = loadMethod("sun.nio.ch.DirectBuffer", "cleaner");
+        cleanMethod = loadMethod("sun.misc.Cleaner", "clean");
+        viewedBufferMethod = loadMethod("sun.nio.ch.DirectBuffer", "viewedBuffer");
+        if (viewedBufferMethod == null) {
+            // They changed the name in Java 7 (???)
+            viewedBufferMethod = loadMethod("sun.nio.ch.DirectBuffer", "attachment");
         }
-        // This could potentially be called many times if used from multiple
-        // threads
-        synchronized (BufferUtils.loadedMethods) {
-            // Oracle JRE / OpenJDK
-            cleanerMethod = loadMethod("sun.nio.ch.DirectBuffer", "cleaner");
-            cleanMethod = loadMethod("sun.misc.Cleaner", "clean");
-            viewedBufferMethod = loadMethod("sun.nio.ch.DirectBuffer", "viewedBuffer");
-            if (viewedBufferMethod == null) {
-                // They changed the name in Java 7 (???)
-                viewedBufferMethod = loadMethod("sun.nio.ch.DirectBuffer", "attachment");
-            }
 
-            // Apache Harmony
-            ByteBuffer bb = BufferUtils.createByteBuffer(1);
-            Class<?> clazz = bb.getClass();
-            try {
-                freeMethod = clazz.getMethod("free");
-            } catch (NoSuchMethodException ex) {
-            } catch (SecurityException ex) {
-            }
+        // Apache Harmony
+        ByteBuffer bb = BufferUtils.createByteBuffer(1);
+        Class<?> clazz = bb.getClass();
+        try {
+            freeMethod = clazz.getMethod("free");
+        } catch (NoSuchMethodException ex) {
+        } catch (SecurityException ex) {
         }
     }
 
@@ -1333,8 +1323,6 @@ public final class BufferUtils {
             return;
         }
 
-        BufferUtils.loadCleanerMethods();
-
         try {
             if (freeMethod != null) {
                 freeMethod.invoke(toBeDestroyed);

+ 81 - 0
jme3-core/src/main/java/com/jme3/util/clone/CloneFunction.java

@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2016 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.util.clone;
+
+
+/**
+ *  Provides custom cloning for a particular object type.  Once
+ *  registered with the Cloner, this function object will be called twice
+ *  for any cloned object that matches the class for which it was registered.
+ *  It will first call cloneObject() to shallow clone the object and then call
+ *  cloneFields()  to deep clone the object's values.
+ *
+ *  <p>This two step process is important because this is what allows
+ *  circular references in the cloned object graph.</p>
+ *
+ *  @author    Paul Speed
+ */
+public interface CloneFunction<T> {
+
+    /**
+     *  Performs a shallow clone of the specified object.  This is similar
+     *  to the JmeCloneable.clone() method in semantics and is the first part
+     *  of a two part cloning process.  Once the shallow clone is created, it
+     *  is cached and CloneFunction.cloneFields() is called.  In this way, 
+     *  the CloneFunction interface can completely take over the JmeCloneable
+     *  style cloning for an object that doesn't otherwise implement that interface.
+     *
+     *  @param cloner The cloner performing the cloning operation.
+     *  @param original The original object that needs to be cloned.
+     */
+    public T cloneObject( Cloner cloner, T original );
+ 
+ 
+    /**
+     *  Performs a deep clone of the specified clone's fields.  This is similar
+     *  to the JmeCloneable.cloneFields() method in semantics and is the second part
+     *  of a two part cloning process.  Once the shallow clone is created, it
+     *  is cached and CloneFunction.cloneFields() is called.  In this way, 
+     *  the CloneFunction interface can completely take over the JmeCloneable
+     *  style cloning for an object that doesn't otherwise implement that interface.
+     * 
+     *  @param cloner The cloner performing the cloning operation.
+     *  @param clone The clone previously returned from cloneObject().
+     *  @param original The original object that was cloned.  This is provided for
+     *              the very special case where field cloning needs to refer to
+     *              the original object.  Mostly the necessary fields should already
+     *              be on the clone.
+     */
+    public void cloneFields( Cloner cloner, T clone, T original );
+     
+}

+ 348 - 0
jme3-core/src/main/java/com/jme3/util/clone/Cloner.java

@@ -0,0 +1,348 @@
+/*
+ * Copyright (c) 2016 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.util.clone;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ *  A deep clone utility that provides similar object-graph-preserving
+ *  qualities to typical serialization schemes.  An internal registry
+ *  of cloned objects is kept to be used by other objects in the deep
+ *  clone process that implement JmeCloneable.
+ *
+ *  <p>By default, objects that do not implement JmeCloneable will
+ *  be treated like normal Java Cloneable objects.  If the object does
+ *  not implement the JmeCloneable or the regular JDK Cloneable interfaces
+ *  AND has no special handling defined then an IllegalArgumentException 
+ *  will be thrown.</p>
+ *
+ *  <p>Enhanced object cloning is done in a two step process.  First,
+ *  the object is cloned using the normal Java clone() method and stored
+ *  in the clone registry.  After that, if it implements JmeCloneable then
+ *  its cloneFields() method is called to deep clone any of the fields.
+ *  This two step process has a few benefits.  First, it means that objects
+ *  can easily have a regular shallow clone implementation just like any
+ *  normal Java objects.  Second, the deep cloning of fields happens after
+ *  creation wich means that the clone is available to future field cloning
+ *  to resolve circular references.</p> 
+ *
+ *  <p>Similar to Java serialization, the handling of specific object
+ *  types can be customized.  This allows certain objects to be cloned gracefully
+ *  even if they aren't normally Cloneable.  This can also be used as a
+ *  sort of filter to keep certain types of objects from being cloned.
+ *  (For example, adding the IdentityCloneFunction for Mesh.class would cause
+ *  all mesh instances to be shared with the original object graph.)</p>
+ *
+ *  <p>By default, the Cloner registers serveral default clone functions
+ *  as follows:</p>
+ *  <ul>
+ *  <li>java.util.ArrayList: ListCloneFunction
+ *  <li>java.util.LinkedList: ListCloneFunction
+ *  <li>java.util.concurrent.CopyOnWriteArrayList: ListCloneFunction
+ *  <li>java.util.Vector: ListCloneFunction
+ *  <li>java.util.Stack: ListCloneFunction
+ *  <li>com.jme3.util.SafeArrayList: ListCloneFunction
+ *  </ul>
+ *
+ *  <p>Usage:</p>
+ *  <pre>
+ *  // Example 1: using an instantiated, reusable cloner.
+ *  Cloner cloner = new Cloner();
+ *  Foo fooClone = cloner.clone(foo);
+ *  cloner.clearIndex(); // prepare it for reuse
+ *  Foo fooClone2 = cloner.clone(foo);
+ * 
+ *  // Example 2: using the utility method that self-instantiates a temporary cloner.
+ *  Foo fooClone = Cloner.deepClone(foo);
+ *   
+ *  </pre>
+ *
+ *  @author    Paul Speed
+ */
+public class Cloner {
+ 
+    /**
+     *  Keeps track of the objects that have been cloned so far.
+     */   
+    private IdentityHashMap<Object, Object> index = new IdentityHashMap<Object, Object>();
+ 
+    /**
+     *  Custom functions for cloning objects.
+     */
+    private Map<Class, CloneFunction> functions = new HashMap<Class, CloneFunction>();
+ 
+    /**
+     *  Cache the clone methods once for all cloners.
+     */   
+    private static final Map<Class, Method> methodCache = new ConcurrentHashMap<>();
+ 
+    /**
+     *  Creates a new cloner with only default clone functions and an empty
+     *  object index.
+     */
+    public Cloner() {
+        // Register some standard types
+        ListCloneFunction listFunction = new ListCloneFunction();
+        functions.put(java.util.ArrayList.class, listFunction);
+        functions.put(java.util.LinkedList.class, listFunction);
+        functions.put(java.util.concurrent.CopyOnWriteArrayList.class, listFunction); 
+        functions.put(java.util.Vector.class, listFunction);
+        functions.put(java.util.Stack.class, listFunction);
+        functions.put(com.jme3.util.SafeArrayList.class, listFunction);
+    }
+ 
+    /**
+     *  Convenience utility function that creates a new Cloner, uses it to
+     *  deep clone the object, and then returns the result.
+     */
+    public static <T> T deepClone( T object ) {
+        return new Cloner().clone(object);
+    }     
+ 
+    /**
+     *  Deeps clones the specified object, reusing previous clones when possible.
+     * 
+     *  <p>Object cloning priority works as follows:</p>
+     *  <ul>
+     *  <li>If the object has already been cloned then its clone is returned.
+     *  <li>If there is a custom CloneFunction then it is called to clone the object.
+     *  <li>If the object implements Cloneable then its clone() method is called, arrays are 
+     *      deep cloned with entries passing through clone().
+     *  <li>If the object implements JmeCloneable then its cloneFields() method is called on the
+     *      clone.
+     *  <li>Else an IllegalArgumentException is thrown. 
+     *  </ul>
+     *
+     *  Note: objects returned by this method may not have yet had their cloneField()
+     *  method called.
+     */   
+    public <T> T clone( T object ) {
+        return clone(object, true);
+    }
+ 
+    /**
+     *  Internal method to work around a Java generics typing issue by
+     *  isolating the 'bad' case into a method with suppressed warnings.
+     */
+    @SuppressWarnings("unchecked")
+    private <T> Class<T> objectClass( T object ) {
+        // This should be 100% allowed without a cast but Java generics
+        // is not that smart sometimes.
+        // Wrapping it in a method at least isolates the warning suppression
+        return (Class<T>)object.getClass();
+    }
+ 
+    /**
+     *  Deeps clones the specified object, reusing previous clones when possible.
+     * 
+     *  <p>Object cloning priority works as follows:</p>
+     *  <ul>
+     *  <li>If the object has already been cloned then its clone is returned.
+     *  <li>If useFunctions is true and there is a custom CloneFunction then it is 
+     *      called to clone the object.
+     *  <li>If the object implements Cloneable then its clone() method is called, arrays are 
+     *      deep cloned with entries passing through clone().
+     *  <li>If the object implements JmeCloneable then its cloneFields() method is called on the
+     *      clone.
+     *  <li>Else an IllegalArgumentException is thrown. 
+     *  </ul>
+     *
+     *  <p>The abililty to selectively use clone functions is useful when
+     *  being called from a clone function.</p>
+     *
+     *  Note: objects returned by this method may not have yet had their cloneField()
+     *  method called.
+     */   
+    public <T> T clone( T object, boolean useFunctions ) {
+        if( object == null ) {
+            return null;
+        }
+        Class<T> type = objectClass(object); 
+        
+        // Check the index to see if we already have it
+        Object clone = index.get(object);
+        if( clone != null ) {
+            return type.cast(clone); 
+        }
+        
+        // See if there is a custom function... that trumps everything.
+        CloneFunction<T> f = getCloneFunction(type); 
+        if( f != null ) {
+            T result = f.cloneObject(this, object);
+            
+            // Store the object in the identity map so that any circular references
+            // are resolvable. 
+            index.put(object, result); 
+            
+            // Now call the function again to deep clone the fields
+            f.cloneFields(this, result, object);
+            
+            return result;           
+        }
+ 
+        if( object.getClass().isArray() ) {
+            // Perform an array clone        
+            clone = arrayClone(object);
+            
+            // Array clone already indexes the clone
+        } else if( object instanceof JmeCloneable ) {
+            // Use the two-step cloning semantics
+            clone = ((JmeCloneable)object).jmeClone();
+            
+            // Store the object in the identity map so that any circular references
+            // are resolvable
+            index.put(object, clone); 
+            
+            ((JmeCloneable)clone).cloneFields(this, object);
+        } else if( object instanceof Cloneable ) {
+            
+            // Perform a regular Java shallow clone
+            try {
+                clone = javaClone(object);
+            } catch( CloneNotSupportedException e ) {
+                throw new IllegalArgumentException("Object is not cloneable, type:" + type, e);
+            }
+            
+            // Store the object in the identity map so that any circular references
+            // are resolvable
+            index.put(object, clone); 
+        } else {
+            throw new IllegalArgumentException("Object is not cloneable, type:" + type);
+        }
+        
+        return type.cast(clone);
+    }
+ 
+    /**
+     *  Sets a custom CloneFunction for the exact Java type.  Note: no inheritence
+     *  checks are performed so a function must be registered for each specific type
+     *  that it handles.  By default ListCloneFunction is registered for 
+     *  ArrayList, LinkedList, CopyOnWriteArrayList, Vector, Stack, and JME's SafeArrayList.
+     */
+    public <T> void setCloneFunction( Class<T> type, CloneFunction<T> function ) {
+        if( function == null ) {
+            functions.remove(type);
+        } else {
+            functions.put(type, function);
+        }
+    }
+ 
+    /**
+     *  Returns a previously registered clone function for the specified type or null
+     *  if there is no custom clone function for the type.
+     */ 
+    @SuppressWarnings("unchecked")
+    public <T> CloneFunction<T> getCloneFunction( Class<T> type ) {
+        return (CloneFunction<T>)functions.get(type); 
+    } 
+ 
+    /**
+     *  Clears the object index allowing the cloner to be reused for a brand new
+     *  cloning operation.
+     */
+    public void clearIndex() {
+        index.clear();
+    }     
+ 
+    /**
+     *  Performs a raw shallow Java clone using reflection.  This call does NOT
+     *  check against the clone index and so will return new objects every time
+     *  it is called.  That's because these are shallow clones and have not (and may
+     *  not ever, depending on the caller) get resolved.
+     *
+     *  <p>This method is provided as a convenient way for CloneFunctions to call
+     *  clone() and objects without necessarily knowing their real type.</p>  
+     */   
+    public <T> T javaClone( T object ) throws CloneNotSupportedException {
+        Method m = methodCache.get(object.getClass());
+        if( m == null ) {
+            try {
+                // Lookup the method and cache it
+                m = object.getClass().getMethod("clone");
+            } catch( NoSuchMethodException e ) {            
+                throw new CloneNotSupportedException("No public clone method found for:" + object.getClass());
+            }
+            methodCache.put(object.getClass(), m);
+            
+            // Note: yes we might cache the method twice... but so what?
+        }
+ 
+        try {
+            Class<? extends T> type = objectClass(object);       
+            return type.cast(m.invoke(object));
+        } catch( IllegalAccessException | InvocationTargetException e ) {
+            throw new RuntimeException("Error cloning object of type:" + object.getClass(), e);
+        }          
+    }
+    
+    /**
+     *  Clones a primitive array by coping it and clones an object
+     *  array by coping it and then running each of its values through
+     *  Cloner.clone().
+     */
+    protected <T> T arrayClone( T object ) {
+ 
+        // Java doesn't support the cloning of arrays through reflection unless
+        // you open access to Object's protected clone array... which requires
+        // elevated privileges.  So we will do a work-around that is slightly less
+        // elegant.
+        // This should be 100% allowed without a case but Java generics
+        // is not that smart
+        Class<T> type = objectClass(object); 
+        Class elementType = type.getComponentType();
+        int size = Array.getLength(object); 
+        Object clone = Array.newInstance(elementType, size);
+ 
+        // Store the clone for later lookups
+        index.put(object, clone); 
+        
+        if( elementType.isPrimitive() ) {
+            // Then our job is a bit easier
+            System.arraycopy(object, 0, clone, 0, size);
+        } else {
+            // Else it's an object array so we'll clone it and its children
+            for( int i = 0; i < size; i++ ) {
+                Object element = clone(Array.get(object, i));
+                Array.set(clone, i, element);
+            }
+        }           
+        
+        return type.cast(clone);
+    }
+}

+ 58 - 63
sdk/jme3-terrain-editor/src/com/jme3/gde/terraineditor/FlatHeightmap.java → jme3-core/src/main/java/com/jme3/util/clone/IdentityCloneFunction.java

@@ -1,63 +1,58 @@
-/*
- * Copyright (c) 2009-2010 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.gde.terraineditor;
-
-import com.jme3.terrain.heightmap.AbstractHeightMap;
-
-/**
- * Very simple heightmap class that is a heightmap of floats that is size*size
- * in size, and has height values of zero.
- *
- * @author bowens
- */
-public class FlatHeightmap extends AbstractHeightMap {
-
-    private int size;
-    private float[] heightmapData;
-
-    public FlatHeightmap(int size) {
-        this.size = size;
-    }
-
-    @Override
-    public boolean load() {
-        heightmapData = new float[size*size];
-        return true;
-    }
-
-    @Override
-    public float[] getHeightMap() {
-        return heightmapData;
-    }
-
-}
+/*
+ * Copyright (c) 2016 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.util.clone;
+
+
+/**
+ *  A CloneFunction implementation that simply returns the 
+ *  the passed object without cloning it.  This is useful for
+ *  forcing some object types (like Meshes) to be shared between
+ *  the original and cloned object graph.
+ *
+ *  @author    Paul Speed
+ */
+public class IdentityCloneFunction<T> implements CloneFunction<T> {
+
+    /**
+     *  Returns the object directly.
+     */
+    public T cloneObject( Cloner cloner, T object ) {
+        return object;
+    }
+ 
+    /**
+     *  Does nothing.
+     */    
+    public void cloneFields( Cloner cloner, T clone, T object ) {
+    }
+}

+ 99 - 0
jme3-core/src/main/java/com/jme3/util/clone/JmeCloneable.java

@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2016 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.util.clone;
+
+
+/**
+ *  Indicates an object that wishes to more actively participate in the
+ *  two-part deep copying process provided by the Cloner.  Objects implementing
+ *  this interface can access the already cloned object graph to resolve
+ *  their local dependencies in a way that will be equivalent to the
+ *  original object graph.  In other words, if two objects in the graph
+ *  share the same target reference then the cloned version will share
+ *  the cloned reference. 
+ *  
+ *  <p>For example, if an object wishes to deep clone one of its fields
+ *  then it will call cloner.clone(object) instead of object.clone().
+ *  The cloner will keep track of any clones already created for 'object'
+ *  and return that instead of a new clone.</p>
+ *
+ *  <p>Cloning of a JmeCloneable object is done in two parts.  First,
+ *  the standard Java clone() method is called to create a shallow clone
+ *  of the object.  Second, the cloner wil lcall the cloneFields() method
+ *  to let the object deep clone any of its fields that should be cloned.</p>
+ *
+ *  <p>This two part process is necessary to facilitate circular references.
+ *  When an object calls cloner.clone() during its cloneFields() method, it
+ *  may get only a shallow copy that will be filled in later.</p>
+ *
+ *  @author    Paul Speed
+ */
+public interface JmeCloneable extends Cloneable {
+
+    /**
+     *  Performs a regular shallow clone of the object.  Some fields
+     *  may also be cloned but generally only if they will never be
+     *  shared with other objects.  (For example, local Vector3fs and so on.)
+     *
+     *  <p>This method is separate from the regular clone() method
+     *  so that objects might still maintain their own regular java clone()
+     *  semantics (perhaps even using Cloner for those methods).  However,
+     *  because Java's clone() has specific features in the sense of Object's
+     *  clone() implementation, it's usually best to have some path for
+     *  subclasses to bypass the public clone() method that might be cloning
+     *  fields and instead get at the superclass protected clone() methods.
+     *  For example, through super.jmeClone() or another protected clone
+     *  method that some base class eventually calls super.clone() in.</p>
+     */
+    public Object jmeClone();     
+
+    /**
+     *  Implemented to perform deep cloning for this object, resolving
+     *  local cloned references using the specified cloner.  The object
+     *  can call cloner.clone(fieldValue) to deep clone any of its fields.
+     * 
+     *  <p>Note: during normal clone operations the original object
+     *  will not be needed as the clone has already had all of the fields
+     *  shallow copied.</p>
+     *
+     *  @param cloner The cloner that is performing the cloning operation.  The 
+     *              cloneFields method can call back into the cloner to make
+     *              clones if its subordinate fields.     
+     *  @param original The original object from which this object was cloned.
+     *              This is provided for the very rare case that this object needs
+     *              to refer to its original for some reason.  In general, all of
+     *              the relevant values should have been transferred during the
+     *              shallow clone and this object need merely clone what it wants.
+     */
+    public void cloneFields( Cloner cloner, Object original ); 
+}

+ 70 - 55
sdk/jme3-core/src/com/jme3/gde/core/util/Beans.java → jme3-core/src/main/java/com/jme3/util/clone/ListCloneFunction.java

@@ -1,55 +1,70 @@
-/*
- * Copyright (c) 2003-2012 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.gde.core.util;
-
-import java.lang.reflect.InvocationTargetException;
-import org.apache.commons.beanutils.BeanUtils;
-import org.openide.util.Exceptions;
-
-/**
- *
- * @author normenhansen
- */
-public class Beans {
-
-    public static boolean copyProperties(Object dest, Object src) {
-        try {
-            BeanUtils.copyProperties(dest, src);
-            return true;
-        } catch (IllegalAccessException ex) {
-            Exceptions.printStackTrace(ex);
-        } catch (InvocationTargetException ex) {
-            Exceptions.printStackTrace(ex);
-        }
-        return false;
-    }
-}
+/*
+ * Copyright (c) 2016 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.util.clone;
+
+import java.util.List;
+
+/**
+ *  A CloneFunction implementation that deep clones a list by
+ *  creating a new list and cloning its values using the cloner.
+ *
+ *  @author    Paul Speed
+ */
+public class ListCloneFunction<T extends List> implements CloneFunction<T> {
+
+    public T cloneObject( Cloner cloner, T object ) {         
+        try {
+            T clone = cloner.javaClone(object);         
+            return clone;
+        } catch( CloneNotSupportedException e ) {
+            throw new IllegalArgumentException("Clone not supported for type:" + object.getClass(), e);
+        }
+    }
+     
+    /**
+     *  Clones the elements of the list.
+     */    
+    @SuppressWarnings("unchecked")
+    public void cloneFields( Cloner cloner, T clone, T object ) {
+        for( int i = 0; i < clone.size(); i++ ) {
+            // Need to clone the clones... because T might
+            // have done something special in its clone method that
+            // we will have to adhere to.  For example, clone may have nulled
+            // out some things or whatever that might be implementation specific.
+            // At any rate, if it's a proper clone then the clone will already
+            // have shallow versions of the elements that we can clone.
+            clone.set(i, cloner.clone(clone.get(i)));
+        }
+    }
+}
+

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

+ 7 - 10
jme3-core/src/main/resources/Common/MatDefs/Light/Lighting.j3md

@@ -113,6 +113,8 @@ MaterialDef Phong Lighting {
                 
         //For instancing
         Boolean UseInstancing
+
+        Boolean BackfaceShadows: false
     }
 
  Technique {
@@ -213,26 +215,19 @@ MaterialDef Phong Lighting {
             INSTANCING : UseInstancing
         }
 
-        ForcedRenderState {
-            FaceCull Off
-            DepthTest On
-            DepthWrite On
-            PolyOffset 5 3
-            ColorWrite Off
-        }
-
     }
 
 
     Technique PostShadow15{
-        VertexShader GLSL150:   Common/MatDefs/Shadow/PostShadow15.vert
-        FragmentShader GLSL150: Common/MatDefs/Shadow/PostShadow15.frag
+        VertexShader GLSL150:   Common/MatDefs/Shadow/PostShadow.vert
+        FragmentShader GLSL150: Common/MatDefs/Shadow/PostShadow.frag
 
         WorldParameters {
             WorldViewProjectionMatrix
             WorldMatrix
             ViewProjectionMatrix
             ViewMatrix
+            NormalMatrix
         }
 
         Defines {
@@ -247,6 +242,7 @@ MaterialDef Phong Lighting {
             POINTLIGHT : LightViewProjectionMatrix5
             NUM_BONES : NumberOfBones
             INSTANCING : UseInstancing
+            BACKFACE_SHADOWS: BackfaceShadows
         }
 
         ForcedRenderState {
@@ -265,6 +261,7 @@ MaterialDef Phong Lighting {
             WorldMatrix
             ViewProjectionMatrix
             ViewMatrix
+            NormalMatrix
         }
 
         Defines {

+ 6 - 2
jme3-core/src/main/resources/Common/MatDefs/Misc/Unshaded.j3md

@@ -51,6 +51,8 @@ MaterialDef Unshaded {
         Float PCFEdge
 
         Float ShadowMapSize
+
+        Boolean BackfaceShadows: true
     }
 
     Technique {
@@ -147,8 +149,8 @@ MaterialDef Unshaded {
 
 
     Technique PostShadow15{
-        VertexShader GLSL150:   Common/MatDefs/Shadow/PostShadow15.vert
-        FragmentShader GLSL150: Common/MatDefs/Shadow/PostShadow15.frag
+        VertexShader GLSL150:   Common/MatDefs/Shadow/PostShadow.vert
+        FragmentShader GLSL150: Common/MatDefs/Shadow/PostShadow.frag
 
         WorldParameters {
             WorldViewProjectionMatrix
@@ -169,6 +171,7 @@ MaterialDef Unshaded {
             POINTLIGHT : LightViewProjectionMatrix5
             NUM_BONES : NumberOfBones
 	    INSTANCING : UseInstancing
+	        BACKFACE_SHADOWS: BackfaceShadows
         }
 
         ForcedRenderState {
@@ -201,6 +204,7 @@ MaterialDef Unshaded {
             POINTLIGHT : LightViewProjectionMatrix5
             NUM_BONES : NumberOfBones
             INSTANCING : UseInstancing
+            BACKFACE_SHADOWS: BackfaceShadows
         }
 
         ForcedRenderState {

+ 14 - 4
jme3-core/src/main/resources/Common/MatDefs/Shadow/PostShadow.frag

@@ -1,4 +1,5 @@
 #import "Common/ShaderLib/Shadows.glsllib"
+#import "Common/ShaderLib/GLSLCompat.glsllib"
 
 #if defined(PSSM) || defined(FADE)
 varying float shadowPosition;
@@ -8,6 +9,9 @@ varying vec4 projCoord0;
 varying vec4 projCoord1;
 varying vec4 projCoord2;
 varying vec4 projCoord3;
+#ifndef BACKFACE_SHADOWS
+    varying float nDotL;
+#endif
 
 #ifdef POINTLIGHT
     varying vec4 projCoord4;
@@ -45,9 +49,15 @@ void main(){
         if(alpha<=m_AlphaDiscardThreshold){
             discard;
         }
+    #endif
 
+    #ifndef BACKFACE_SHADOWS
+        if(nDotL > 0.0){
+            discard;
+        }
     #endif
-     
+
+
     float shadow = 1.0;
  
     #ifdef POINTLIGHT         
@@ -70,11 +80,11 @@ void main(){
     #endif   
 
     #ifdef FADE
-      shadow = max(0.0,mix(shadow,1.0,(shadowPosition - m_FadeInfo.x) * m_FadeInfo.y));    
+        shadow = max(0.0,mix(shadow,1.0,(shadowPosition - m_FadeInfo.x) * m_FadeInfo.y));
     #endif
-    shadow = shadow * m_ShadowIntensity + (1.0 - m_ShadowIntensity);
 
-  gl_FragColor = vec4(shadow, shadow, shadow, 1.0);
+    shadow = shadow * m_ShadowIntensity + (1.0 - m_ShadowIntensity);
+    gl_FragColor = vec4(shadow, shadow, shadow, 1.0);
 
 }
 

+ 6 - 2
jme3-core/src/main/resources/Common/MatDefs/Shadow/PostShadow.j3md

@@ -29,12 +29,14 @@ MaterialDef Post Shadow {
         Float PCFEdge
 
         Float ShadowMapSize
+
+        Boolean BackfaceShadows: false
 		
     }
 
     Technique {
-        VertexShader GLSL150:   Common/MatDefs/Shadow/PostShadow15.vert
-        FragmentShader GLSL150: Common/MatDefs/Shadow/PostShadow15.frag
+        VertexShader GLSL150:   Common/MatDefs/Shadow/PostShadow.vert
+        FragmentShader GLSL150: Common/MatDefs/Shadow/PostShadow.frag
 
         WorldParameters {
             WorldViewProjectionMatrix
@@ -49,6 +51,7 @@ MaterialDef Post Shadow {
             FADE : FadeInfo
             PSSM : Splits
             POINTLIGHT : LightViewProjectionMatrix5
+            BACKFACE_SHADOWS: BackfaceShadows
         }
 
         RenderState {
@@ -75,6 +78,7 @@ MaterialDef Post Shadow {
             FADE : FadeInfo
             PSSM : Splits
             POINTLIGHT : LightViewProjectionMatrix5
+            BACKFACE_SHADOWS: BackfaceShadows
         }
 
         RenderState {

+ 27 - 7
jme3-core/src/main/resources/Common/MatDefs/Shadow/PostShadow.vert

@@ -1,11 +1,12 @@
 #import "Common/ShaderLib/Instancing.glsllib"
 #import "Common/ShaderLib/Skinning.glsllib"
+#import "Common/ShaderLib/GLSLCompat.glsllib"
+
 uniform mat4 m_LightViewProjectionMatrix0;
 uniform mat4 m_LightViewProjectionMatrix1;
 uniform mat4 m_LightViewProjectionMatrix2;
 uniform mat4 m_LightViewProjectionMatrix3;
 
-uniform vec3 m_LightPos; 
 
 varying vec4 projCoord0;
 varying vec4 projCoord1;
@@ -15,12 +16,14 @@ varying vec4 projCoord3;
 #ifdef POINTLIGHT
     uniform mat4 m_LightViewProjectionMatrix4;
     uniform mat4 m_LightViewProjectionMatrix5;
+    uniform vec3 m_LightPos;
     varying vec4 projCoord4;
     varying vec4 projCoord5;
     varying vec4 worldPos;
 #else
+    uniform vec3 m_LightDir;
     #ifndef PSSM
-        uniform vec3 m_LightDir; 
+        uniform vec3 m_LightPos;
         varying float lightDot;
     #endif
 #endif
@@ -28,12 +31,15 @@ varying vec4 projCoord3;
 #if defined(PSSM) || defined(FADE)
 varying float shadowPosition;
 #endif
-varying vec3 lightVec;
 
 varying vec2 texCoord;
-
 attribute vec3 inPosition;
 
+#ifndef BACKFACE_SHADOWS
+    attribute vec3 inNormal;
+    varying float nDotL;
+#endif
+
 #ifdef DISCARD_ALPHA
     attribute vec2 inTexCoord;
 #endif
@@ -51,16 +57,17 @@ void main(){
        Skinning_Compute(modelSpacePos);
    #endif
     gl_Position = TransformWorldViewProjection(modelSpacePos);
+    vec3 lightDir;
 
     #if defined(PSSM) || defined(FADE)
-         shadowPosition = gl_Position.z;
+        shadowPosition = gl_Position.z;
     #endif  
 
     #ifndef POINTLIGHT
         vec4 worldPos=vec4(0.0);
     #endif
     // get the vertex in world space
-    worldPos = g_WorldMatrix * modelSpacePos;
+    worldPos = TransformWorld(modelSpacePos);
 
     #ifdef DISCARD_ALPHA
        texCoord = inTexCoord;
@@ -75,8 +82,21 @@ void main(){
         projCoord5 = biasMat * m_LightViewProjectionMatrix5 * worldPos;
     #else
         #ifndef PSSM
-            vec3 lightDir = worldPos.xyz - m_LightPos;
+            //Spot light
+            lightDir = worldPos.xyz - m_LightPos;
             lightDot = dot(m_LightDir,lightDir);
         #endif
     #endif
+
+    #ifndef BACKFACE_SHADOWS
+        vec3 normal = normalize(TransformWorld(vec4(inNormal,0.0))).xyz;
+        #ifdef POINTLIGHT
+            lightDir = worldPos.xyz - m_LightPos;
+        #else
+            #ifdef PSSM
+               lightDir = m_LightDir;
+            #endif
+        #endif
+        nDotL = dot(normal, lightDir);
+    #endif
 }

+ 0 - 80
jme3-core/src/main/resources/Common/MatDefs/Shadow/PostShadow15.frag

@@ -1,80 +0,0 @@
-#import "Common/ShaderLib/Shadows15.glsllib"
-
-out vec4 outFragColor;
-
-#if defined(PSSM) || defined(FADE)
-in float shadowPosition;
-#endif
-
-in vec4 projCoord0;
-in vec4 projCoord1;
-in vec4 projCoord2;
-in vec4 projCoord3;
-
-#ifdef POINTLIGHT
-    in vec4 projCoord4;
-    in vec4 projCoord5;
-    in vec4 worldPos;
-    uniform vec3 m_LightPos; 
-#else
-    #ifndef PSSM        
-        in float lightDot;
-    #endif
-#endif
-
-#ifdef DISCARD_ALPHA
-    #ifdef COLOR_MAP
-        uniform sampler2D m_ColorMap;
-    #else    
-        uniform sampler2D m_DiffuseMap;
-    #endif
-    uniform float m_AlphaDiscardThreshold;
-    varying vec2 texCoord;
-#endif
-
-#ifdef FADE
-uniform vec2 m_FadeInfo;
-#endif
-
-void main(){
-
-    #ifdef DISCARD_ALPHA
-        #ifdef COLOR_MAP
-             float alpha = texture2D(m_ColorMap,texCoord).a;
-        #else    
-             float alpha = texture2D(m_DiffuseMap,texCoord).a;
-        #endif
-      
-        if(alpha < m_AlphaDiscardThreshold){
-            discard;
-        }
-    #endif
- 
-    float shadow = 1.0;
-    #ifdef POINTLIGHT         
-            shadow = getPointLightShadows(worldPos, m_LightPos,
-                           m_ShadowMap0,m_ShadowMap1,m_ShadowMap2,m_ShadowMap3,m_ShadowMap4,m_ShadowMap5,
-                           projCoord0, projCoord1, projCoord2, projCoord3, projCoord4, projCoord5);
-    #else
-       #ifdef PSSM
-            shadow = getDirectionalLightShadows(m_Splits, shadowPosition,
-                           m_ShadowMap0,m_ShadowMap1,m_ShadowMap2,m_ShadowMap3,
-                           projCoord0, projCoord1, projCoord2, projCoord3);
-       #else
-            //spotlight
-            if(lightDot < 0){
-                outFragColor =  vec4(1.0);
-                return;
-            }
-            shadow = getSpotLightShadows(m_ShadowMap0,projCoord0);
-       #endif
-    #endif   
- 
-    #ifdef FADE
-            shadow = max(0.0,mix(shadow,1.0,(shadowPosition - m_FadeInfo.x) * m_FadeInfo.y));    
-    #endif
-      
-    shadow = shadow * m_ShadowIntensity + (1.0 - m_ShadowIntensity); 
-    outFragColor =  vec4(shadow, shadow, shadow, 1.0);
-}
-

+ 0 - 82
jme3-core/src/main/resources/Common/MatDefs/Shadow/PostShadow15.vert

@@ -1,82 +0,0 @@
-#import "Common/ShaderLib/Instancing.glsllib"
-#import "Common/ShaderLib/Skinning.glsllib"
-uniform mat4 m_LightViewProjectionMatrix0;
-uniform mat4 m_LightViewProjectionMatrix1;
-uniform mat4 m_LightViewProjectionMatrix2;
-uniform mat4 m_LightViewProjectionMatrix3;
-
-
-out vec4 projCoord0;
-out vec4 projCoord1;
-out vec4 projCoord2;
-out vec4 projCoord3;
-
-#ifdef POINTLIGHT
-    uniform mat4 m_LightViewProjectionMatrix4;
-    uniform mat4 m_LightViewProjectionMatrix5;
-    out vec4 projCoord4;
-    out vec4 projCoord5;
-    out vec4 worldPos;
-#else
-    #ifndef PSSM
-        uniform vec3 m_LightPos; 
-        uniform vec3 m_LightDir; 
-        out float lightDot;
-    #endif
-#endif
-
-#if defined(PSSM) || defined(FADE)
-    out float shadowPosition;
-#endif
-out vec3 lightVec;
-
-out vec2 texCoord;
-
-in vec3 inPosition;
-
-#ifdef DISCARD_ALPHA
-    in vec2 inTexCoord;
-#endif
-
-const mat4 biasMat = mat4(0.5, 0.0, 0.0, 0.0,
-                          0.0, 0.5, 0.0, 0.0,
-                          0.0, 0.0, 0.5, 0.0,
-                          0.5, 0.5, 0.5, 1.0);
-
-
-void main(){
-   vec4 modelSpacePos = vec4(inPosition, 1.0);
-  
-   #ifdef NUM_BONES
-       Skinning_Compute(modelSpacePos);
-   #endif
-    gl_Position =  TransformWorldViewProjection(modelSpacePos);
-    
-    #if defined(PSSM) || defined(FADE)
-        shadowPosition = gl_Position.z;
-    #endif
-
-    #ifndef POINTLIGHT        
-        vec4 worldPos=vec4(0.0);
-    #endif
-    // get the vertex in world space
-    worldPos = TransformWorld(modelSpacePos);
-
-    #ifdef DISCARD_ALPHA
-       texCoord = inTexCoord;
-    #endif
-    // populate the light view matrices array and convert vertex to light viewProj space
-    projCoord0 = biasMat * m_LightViewProjectionMatrix0 * worldPos;
-    projCoord1 = biasMat * m_LightViewProjectionMatrix1 * worldPos;
-    projCoord2 = biasMat * m_LightViewProjectionMatrix2 * worldPos;
-    projCoord3 = biasMat * m_LightViewProjectionMatrix3 * worldPos;
-    #ifdef POINTLIGHT
-        projCoord4 = biasMat * m_LightViewProjectionMatrix4 * worldPos;
-        projCoord5 = biasMat * m_LightViewProjectionMatrix5 * worldPos;
-    #else        
-        #ifndef PSSM
-            vec3 lightDir = worldPos.xyz - m_LightPos;
-            lightDot = dot(m_LightDir,lightDir);
-        #endif
-    #endif
-}

+ 29 - 0
jme3-core/src/main/resources/Common/MatDefs/Shadow/PostShadowFilter.frag

@@ -18,6 +18,8 @@ uniform mat4 m_LightViewProjectionMatrix1;
 uniform mat4 m_LightViewProjectionMatrix2;
 uniform mat4 m_LightViewProjectionMatrix3;
 
+uniform vec2 g_ResolutionInverse;
+
 #ifdef POINTLIGHT
     uniform vec3 m_LightPos;
     uniform mat4 m_LightViewProjectionMatrix4;
@@ -39,6 +41,19 @@ vec3 getPosition(in float depth, in vec2 uv){
     return pos.xyz / pos.w;
 }
 
+vec3 approximateNormal(in vec4 worldPos,in vec2 texCoord){
+    float step = g_ResolutionInverse.x ;
+    float stepy = g_ResolutionInverse.y ;
+    float depth2 = texture2D(m_DepthTexture,texCoord + vec2(step,-stepy)).r;
+    float depth3 = texture2D(m_DepthTexture,texCoord + vec2(-step,-stepy)).r;
+    vec4 worldPos2 = vec4(getPosition(depth2,texCoord + vec2(step,-stepy)),1.0);
+    vec4 worldPos3 = vec4(getPosition(depth3,texCoord + vec2(-step,-stepy)),1.0);
+
+    vec3 v1 = (worldPos - worldPos2).xyz;
+    vec3 v2 = (worldPos3 - worldPos2).xyz;
+    return normalize(cross(v1, v2));
+}
+
 void main(){    
     #if !defined( RENDER_SHADOWS )
           gl_FragColor = texture2D(m_Texture,texCoord);
@@ -48,6 +63,7 @@ void main(){
     float depth = texture2D(m_DepthTexture,texCoord).r;
     vec4 color = texture2D(m_Texture,texCoord);
 
+
     //Discard shadow computation on the sky
     if(depth == 1.0){
         gl_FragColor = color;
@@ -56,6 +72,19 @@ void main(){
 
     // get the vertex in world space
     vec4 worldPos = vec4(getPosition(depth,texCoord),1.0);
+    vec3 normal = approximateNormal(worldPos, texCoord);
+
+    vec3 lightDir;
+    #ifdef PSSM
+        lightDir = m_LightDir;
+    #else
+        lightDir = worldPos.xyz - m_LightPos;
+    #endif
+    float ndotl = dot(normal, lightDir);
+    if(ndotl > -0.0){
+        gl_FragColor = color;
+        return;
+    }
    
      #if (!defined(POINTLIGHT) && !defined(PSSM))
           vec3 lightDir = worldPos.xyz - m_LightPos;

+ 7 - 3
jme3-core/src/main/resources/Common/MatDefs/Shadow/PostShadowFilter.j3md

@@ -38,13 +38,15 @@ MaterialDef Post Shadow {
         Texture2D Texture        
         Texture2D DepthTexture
 
+        Boolean BackfaceShadows: true
     }
 
     Technique {
         VertexShader GLSL150:   Common/MatDefs/Shadow/PostShadowFilter15.vert
         FragmentShader GLSL150: Common/MatDefs/Shadow/PostShadowFilter15.frag
 
-        WorldParameters {           
+        WorldParameters {
+            ResolutionInverse
         }
 
         Defines {
@@ -59,7 +61,7 @@ MaterialDef Post Shadow {
             POINTLIGHT : LightViewProjectionMatrix5
             //if no shadow map don't render shadows
             RENDER_SHADOWS : ShadowMap0
-
+            BACKFACE_SHADOWS : BackfaceShadows
         }
       
     }
@@ -68,7 +70,8 @@ MaterialDef Post Shadow {
         VertexShader GLSL100:   Common/MatDefs/Shadow/PostShadowFilter.vert
         FragmentShader GLSL100: Common/MatDefs/Shadow/PostShadowFilter.frag
 
-        WorldParameters {         
+        WorldParameters {
+            ResolutionInverse
         }
 
         Defines {
@@ -79,6 +82,7 @@ MaterialDef Post Shadow {
             FADE : FadeInfo
             PSSM : Splits
             POINTLIGHT : LightViewProjectionMatrix5
+            BACKFACE_SHADOWS : BackfaceShadows
         }
       
     }

+ 42 - 8
jme3-core/src/main/resources/Common/MatDefs/Shadow/PostShadowFilter15.frag

@@ -1,5 +1,5 @@
 #import "Common/ShaderLib/MultiSample.glsllib"
-#import "Common/ShaderLib/Shadows15.glsllib"
+#import "Common/ShaderLib/Shadows.glsllib"
 
 
 uniform COLORTEXTURE m_Texture;
@@ -20,14 +20,16 @@ uniform mat4 m_LightViewProjectionMatrix1;
 uniform mat4 m_LightViewProjectionMatrix2;
 uniform mat4 m_LightViewProjectionMatrix3;
 
+uniform vec2 g_ResolutionInverse;
+
 #ifdef POINTLIGHT
     uniform vec3 m_LightPos;
     uniform mat4 m_LightViewProjectionMatrix4;
     uniform mat4 m_LightViewProjectionMatrix5;
 #else
+    uniform vec3 m_LightDir;
     #ifndef PSSM    
-        uniform vec3 m_LightPos;    
-        uniform vec3 m_LightDir;       
+        uniform vec3 m_LightPos;
     #endif
 #endif
 
@@ -41,6 +43,23 @@ vec3 getPosition(in float depth, in vec2 uv){
     return pos.xyz / pos.w;
 }
 
+#ifndef BACKFACE_SHADOWS
+    vec3 approximateNormal(in float depth,in vec4 worldPos,in vec2 texCoord, in int numSample){
+        float step = g_ResolutionInverse.x ;
+        float stepy = g_ResolutionInverse.y ;
+        float depth1 = fetchTextureSample(m_DepthTexture,texCoord + vec2(-step,stepy),numSample).r;
+        float depth2 = fetchTextureSample(m_DepthTexture,texCoord + vec2(step,stepy),numSample).r;
+        vec3 v1, v2;
+        vec4 worldPos1 = vec4(getPosition(depth1,texCoord + vec2(-step,stepy)),1.0);
+        vec4 worldPos2 = vec4(getPosition(depth2,texCoord + vec2(step,stepy)),1.0);
+
+        v1 = normalize((worldPos1 - worldPos)).xyz;
+        v2 =  normalize((worldPos2 - worldPos)).xyz;
+        return normalize(cross(v2, v1));
+
+    }
+#endif
+
 vec4 main_multiSample(in int numSample){
     float depth = fetchTextureSample(m_DepthTexture,texCoord,numSample).r;//getDepth(m_DepthTexture,texCoord).r;
     vec4 color = fetchTextureSample(m_Texture,texCoord,numSample);
@@ -52,12 +71,27 @@ vec4 main_multiSample(in int numSample){
     
     // get the vertex in world space
     vec4 worldPos = vec4(getPosition(depth,texCoord),1.0);
-  
+
+
+    vec3 lightDir;
+    #ifdef PSSM
+        lightDir = m_LightDir;
+    #else
+        lightDir = worldPos.xyz - m_LightPos;
+    #endif
+
+    #ifndef BACKFACE_SHADOWS
+        vec3 normal = approximateNormal(depth, worldPos, texCoord, numSample);
+        float ndotl = dot(normal, lightDir);
+        if(ndotl > 0.0){
+            return color;
+        }
+    #endif
+
     #if (!defined(POINTLIGHT) && !defined(PSSM))
-          vec3 lightDir = worldPos.xyz - m_LightPos;
-          if( dot(m_LightDir,lightDir)<0){
-             return color;
-          }         
+        if( dot(m_LightDir,lightDir)<0){
+         return color;
+        }
     #endif
 
     // populate the light view matrices array and convert vertex to light viewProj space

+ 153 - 102
jme3-core/src/main/resources/Common/ShaderLib/Shadows.glsllib

@@ -1,22 +1,57 @@
-#ifdef HARDWARE_SHADOWS
-    #define SHADOWMAP sampler2DShadow
-    #define SHADOWCOMPARE(tex,coord) shadow2DProj(tex, coord).r 
-#else
-    #define SHADOWMAP sampler2D
-    #define SHADOWCOMPARE(tex,coord) step(coord.z, texture2DProj(tex, coord).r)
-#endif
+#if __VERSION__ >= 130
+    // Because gpu_shader5 is actually where those
+    // gather functions are declared to work on shadowmaps
+    #extension GL_ARB_gpu_shader5 : enable
+    #define IVEC2 ivec2
+    #ifdef HARDWARE_SHADOWS
+        #define SHADOWMAP sampler2DShadow
+        #define SHADOWCOMPAREOFFSET(tex,coord,offset) textureProjOffset(tex, coord, offset)
+        #define SHADOWCOMPARE(tex,coord) textureProj(tex, coord)
+        #define SHADOWGATHER(tex,coord) textureGather(tex, coord.xy, coord.z)
+    #else
+        #define SHADOWMAP sampler2D
+        #define SHADOWCOMPAREOFFSET(tex,coord,offset) step(coord.z, textureProjOffset(tex, coord, offset).r)
+        #define SHADOWCOMPARE(tex,coord) step(coord.z, textureProj(tex, coord).r)
+        #define SHADOWGATHER(tex,coord) step(coord.z, textureGather(tex, coord.xy))
+    #endif
 
-#if FILTER_MODE == 0
-    #define GETSHADOW Shadow_DoShadowCompare
-    #define KERNEL 1.0
-#elif FILTER_MODE == 1
+    #if FILTER_MODE == 0
+        #define GETSHADOW Shadow_Nearest
+        #define KERNEL 1.0
+    #elif FILTER_MODE == 1
+        #ifdef HARDWARE_SHADOWS
+            #define GETSHADOW Shadow_Nearest
+        #else
+            #define GETSHADOW Shadow_DoBilinear_2x2
+        #endif
+        #define KERNEL 1.0
+    #endif
+#else
+    #define IVEC2 vec2
     #ifdef HARDWARE_SHADOWS
-        #define GETSHADOW Shadow_DoShadowCompare
+        #define SHADOWMAP sampler2DShadow
+        #define SHADOWCOMPARE(tex,coord) shadow2DProj(tex, coord).r
     #else
-        #define GETSHADOW Shadow_DoBilinear_2x2
+        #define SHADOWMAP sampler2D
+        #define SHADOWCOMPARE(tex,coord) step(coord.z, texture2DProj(tex, coord).r)
     #endif
-    #define KERNEL 1.0
-#elif FILTER_MODE == 2
+
+    #if FILTER_MODE == 0
+        #define GETSHADOW Shadow_DoShadowCompare
+        #define KERNEL 1.0
+    #elif FILTER_MODE == 1
+        #ifdef HARDWARE_SHADOWS
+            #define GETSHADOW Shadow_DoShadowCompare
+        #else
+            #define GETSHADOW Shadow_DoBilinear_2x2
+        #endif
+        #define KERNEL 1.0
+    #endif
+
+
+#endif
+
+#if FILTER_MODE == 2
     #define GETSHADOW Shadow_DoDither_2x2
     #define KERNEL 1.0
 #elif FILTER_MODE == 3
@@ -30,14 +65,13 @@
     #define KERNEL 8.0
 #endif
 
-
 uniform SHADOWMAP m_ShadowMap0;
 uniform SHADOWMAP m_ShadowMap1;
 uniform SHADOWMAP m_ShadowMap2;
 uniform SHADOWMAP m_ShadowMap3;
 #ifdef POINTLIGHT
-uniform SHADOWMAP m_ShadowMap4;
-uniform SHADOWMAP m_ShadowMap5;
+    uniform SHADOWMAP m_ShadowMap4;
+    uniform SHADOWMAP m_ShadowMap5;
 #endif
 
 #ifdef PSSM
@@ -49,73 +83,91 @@ uniform float m_ShadowIntensity;
 const vec2 pixSize2 = vec2(1.0 / SHADOWMAP_SIZE);
 float shadowBorderScale = 1.0;
 
-float Shadow_DoShadowCompareOffset(SHADOWMAP tex, vec4 projCoord, vec2 offset){
-    vec4 coord = vec4(projCoord.xy + offset.xy * pixSize2 * shadowBorderScale, projCoord.zw);
-    return SHADOWCOMPARE(tex, coord);
-}
-
-float Shadow_DoShadowCompare(SHADOWMAP tex, vec4 projCoord){
+float Shadow_DoShadowCompare(in SHADOWMAP tex,in vec4 projCoord){
     return SHADOWCOMPARE(tex, projCoord);
 }
 
-float Shadow_BorderCheck(vec2 coord){
+float Shadow_BorderCheck(in vec2 coord){
     // Fastest, "hack" method (uses 4-5 instructions)
     vec4 t = vec4(coord.xy, 0.0, 1.0);
     t = step(t.wwxy, t.xyzz);
     return dot(t,t);
 }
 
-float Shadow_Nearest(SHADOWMAP tex, vec4 projCoord){
+float Shadow_Nearest(in SHADOWMAP tex,in vec4 projCoord){
     float border = Shadow_BorderCheck(projCoord.xy);
     if (border > 0.0){
         return 1.0;
     }
-    return Shadow_DoShadowCompare(tex,projCoord);
+    return SHADOWCOMPARE(tex, projCoord);
+}
+
+float Shadow_DoShadowCompareOffset(in SHADOWMAP tex,in vec4 projCoord,in vec2 offset){
+    vec4 coord = vec4(projCoord.xy + offset.xy * pixSize2 * shadowBorderScale, projCoord.zw);
+    return SHADOWCOMPARE(tex, coord);
 }
 
-float Shadow_DoDither_2x2(SHADOWMAP tex, vec4 projCoord){
+
+float Shadow_DoDither_2x2(in SHADOWMAP tex, in vec4 projCoord){
     float border = Shadow_BorderCheck(projCoord.xy);
     if (border > 0.0)
         return 1.0;
-  
 
     float shadow = 0.0;
-    vec2 o = mod(floor(gl_FragCoord.xy), 2.0);
-    shadow += Shadow_DoShadowCompareOffset(tex,projCoord,vec2(-1.5,  1.5) + o);
-    shadow += Shadow_DoShadowCompareOffset(tex,projCoord,vec2( 0.5,  1.5) + o);
-    shadow += Shadow_DoShadowCompareOffset(tex,projCoord,vec2(-1.5, -0.5) + o);
-    shadow += Shadow_DoShadowCompareOffset(tex,projCoord,vec2( 0.5, -0.5) + o);
-    shadow *= 0.25 ;
+    IVEC2 o = IVEC2(mod(floor(gl_FragCoord.xy), 2.0));
+    shadow += Shadow_DoShadowCompareOffset(tex, projCoord, (vec2(-1.5, 1.5)+o));
+    shadow += Shadow_DoShadowCompareOffset(tex, projCoord, (vec2( 0.5, 1.5)+o));
+    shadow += Shadow_DoShadowCompareOffset(tex, projCoord, (vec2(-1.5, -0.5)+o));
+    shadow += Shadow_DoShadowCompareOffset(tex, projCoord, (vec2( 0.5, -0.5)+o));
+    shadow *= 0.25;
     return shadow;
 }
 
-float Shadow_DoBilinear_2x2(SHADOWMAP tex, vec4 projCoord){
+float Shadow_DoBilinear_2x2(in SHADOWMAP tex, in vec4 projCoord){
     float border = Shadow_BorderCheck(projCoord.xy);
-    if (border > 0.0)
+    if (border > 0.0){
         return 1.0;
+    }
+
     vec4 gather = vec4(0.0);
-    gather.x = Shadow_DoShadowCompareOffset(tex, projCoord, vec2(0.0, 0.0));
-    gather.y = Shadow_DoShadowCompareOffset(tex, projCoord, vec2(1.0, 0.0));
-    gather.z = Shadow_DoShadowCompareOffset(tex, projCoord, vec2(0.0, 1.0));
-    gather.w = Shadow_DoShadowCompareOffset(tex, projCoord, vec2(1.0, 1.0));
-
-    vec2 f = fract( projCoord.xy * SHADOWMAP_SIZE );
-    vec2 mx = mix( gather.xz, gather.yw, f.x );
-    return mix( mx.x, mx.y, f.y );
+    #if __VERSION__ >= 130
+        #ifdef GL_ARB_gpu_shader5
+            vec4 coord = vec4(projCoord.xyz / projCoord.www,0.0);
+            gather = SHADOWGATHER(tex, coord);
+        #else
+            gather.x = SHADOWCOMPAREOFFSET(tex, projCoord, ivec2(0, 1));
+            gather.y = SHADOWCOMPAREOFFSET(tex, projCoord, ivec2(1, 1));
+            gather.z = SHADOWCOMPAREOFFSET(tex, projCoord, ivec2(1, 0));
+            gather.w = SHADOWCOMPAREOFFSET(tex, projCoord, ivec2(0, 0));
+        #endif
+    #else
+        gather.x = Shadow_DoShadowCompareOffset(tex, projCoord, vec2(0.0, 0.0));
+        gather.y = Shadow_DoShadowCompareOffset(tex, projCoord, vec2(1.0, 0.0));
+        gather.z = Shadow_DoShadowCompareOffset(tex, projCoord, vec2(0.0, 1.0));
+        gather.w = Shadow_DoShadowCompareOffset(tex, projCoord, vec2(1.0, 1.0));
+    #endif
+
+   vec2 f = fract( projCoord.xy * SHADOWMAP_SIZE );
+   vec2 mx = mix( gather.wx, gather.zy, f.x );
+   return mix( mx.x, mx.y, f.y );
 }
 
-float Shadow_DoPCF(SHADOWMAP tex, vec4 projCoord){
+float Shadow_DoPCF(in SHADOWMAP tex,in vec4 projCoord){
+
     float shadow = 0.0;
     float border = Shadow_BorderCheck(projCoord.xy);
     if (border > 0.0)
         return 1.0;
+
     float bound = KERNEL * 0.5 - 0.5;
     bound *= PCFEDGE;
     for (float y = -bound; y <= bound; y += PCFEDGE){
         for (float x = -bound; x <= bound; x += PCFEDGE){
-            shadow += clamp(Shadow_DoShadowCompareOffset(tex,projCoord,vec2(x,y)) +
-                            border,
-                            0.0, 1.0);
+            #if __VERSION__ < 130
+                shadow += clamp(Shadow_DoShadowCompareOffset(tex,projCoord,vec2(x,y)) + border, 0.0, 1.0);
+            #else
+                shadow += Shadow_DoShadowCompareOffset(tex, projCoord, vec2(x,y));
+            #endif
         }
     }
 
@@ -123,51 +175,51 @@ float Shadow_DoPCF(SHADOWMAP tex, vec4 projCoord){
     return shadow;
 }
 
-
 //12 tap poisson disk
-    const vec2 poissonDisk0 =  vec2(-0.1711046, -0.425016);
-    const vec2 poissonDisk1 =  vec2(-0.7829809, 0.2162201);
-    const vec2 poissonDisk2 =  vec2(-0.2380269, -0.8835521);
-    const vec2 poissonDisk3 =  vec2(0.4198045, 0.1687819);
-    const vec2 poissonDisk4 =  vec2(-0.684418, -0.3186957);
-    const vec2 poissonDisk5 =  vec2(0.6026866, -0.2587841);
-    const vec2 poissonDisk6 =  vec2(-0.2412762, 0.3913516);
-    const vec2 poissonDisk7 =  vec2(0.4720655, -0.7664126);
-    const vec2 poissonDisk8 =  vec2(0.9571564, 0.2680693);
-    const vec2 poissonDisk9 =  vec2(-0.5238616, 0.802707);
-    const vec2 poissonDisk10 = vec2(0.5653144, 0.60262);
-    const vec2 poissonDisk11 = vec2(0.0123658, 0.8627419);
-
-float Shadow_DoPCFPoisson(SHADOWMAP tex, vec4 projCoord){   
+const vec2 poissonDisk0 =  vec2(-0.1711046, -0.425016);
+const vec2 poissonDisk1 =  vec2(-0.7829809, 0.2162201);
+const vec2 poissonDisk2 =  vec2(-0.2380269, -0.8835521);
+const vec2 poissonDisk3 =  vec2(0.4198045, 0.1687819);
+const vec2 poissonDisk4 =  vec2(-0.684418, -0.3186957);
+const vec2 poissonDisk5 =  vec2(0.6026866, -0.2587841);
+const vec2 poissonDisk6 =  vec2(-0.2412762, 0.3913516);
+const vec2 poissonDisk7 =  vec2(0.4720655, -0.7664126);
+const vec2 poissonDisk8 =  vec2(0.9571564, 0.2680693);
+const vec2 poissonDisk9 =  vec2(-0.5238616, 0.802707);
+const vec2 poissonDisk10 = vec2(0.5653144, 0.60262);
+const vec2 poissonDisk11 = vec2(0.0123658, 0.8627419);
+
+
+float Shadow_DoPCFPoisson(in SHADOWMAP tex, in vec4 projCoord){
     float shadow = 0.0;
     float border = Shadow_BorderCheck(projCoord.xy);
-    if (border > 0.0)
+    if (border > 0.0){
         return 1.0;
+    }
 
-    vec2 texelSize = vec2( 4.0 * PCFEDGE * shadowBorderScale);        
-    
-     shadow += Shadow_DoShadowCompareOffset(tex, projCoord , poissonDisk0 * texelSize);
-     shadow += Shadow_DoShadowCompareOffset(tex, projCoord , poissonDisk1 * texelSize);
-     shadow += Shadow_DoShadowCompareOffset(tex, projCoord , poissonDisk2 * texelSize);
-     shadow += Shadow_DoShadowCompareOffset(tex, projCoord , poissonDisk3 * texelSize);
-     shadow += Shadow_DoShadowCompareOffset(tex, projCoord , poissonDisk4 * texelSize);
-     shadow += Shadow_DoShadowCompareOffset(tex, projCoord , poissonDisk5 * texelSize);
-     shadow += Shadow_DoShadowCompareOffset(tex, projCoord , poissonDisk6 * texelSize);
-     shadow += Shadow_DoShadowCompareOffset(tex, projCoord , poissonDisk7 * texelSize);
-     shadow += Shadow_DoShadowCompareOffset(tex, projCoord , poissonDisk8 * texelSize);
-     shadow += Shadow_DoShadowCompareOffset(tex, projCoord , poissonDisk9 * texelSize);
-     shadow += Shadow_DoShadowCompareOffset(tex, projCoord , poissonDisk10 * texelSize);
-     shadow += Shadow_DoShadowCompareOffset(tex, projCoord , poissonDisk11 * texelSize);
-
-    shadow = shadow * 0.08333333333;//this is divided by 12
-    return shadow;
-}
+    vec2 texelSize = pixSize2 * 4.0 * PCFEDGE * shadowBorderScale;
+
+    shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk0 * texelSize, projCoord.zw));
+    shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk1 * texelSize, projCoord.zw));
+    shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk2 * texelSize, projCoord.zw));
+    shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk3 * texelSize, projCoord.zw));
+    shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk4 * texelSize, projCoord.zw));
+    shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk5 * texelSize, projCoord.zw));
+    shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk6 * texelSize, projCoord.zw));
+    shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk7 * texelSize, projCoord.zw));
+    shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk8 * texelSize, projCoord.zw));
+    shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk9 * texelSize, projCoord.zw));
+    shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk10 * texelSize, projCoord.zw));
+    shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk11 * texelSize, projCoord.zw));
 
+    //this is divided by 12
+    return shadow * 0.08333333333;
+}
 
-#ifdef POINTLIGHT       
-    float getPointLightShadows(vec4 worldPos,vec3 lightPos,
-                           SHADOWMAP shadowMap0,SHADOWMAP shadowMap1,SHADOWMAP shadowMap2,SHADOWMAP shadowMap3,SHADOWMAP shadowMap4,SHADOWMAP shadowMap5,
-                           vec4 projCoord0,vec4 projCoord1,vec4 projCoord2,vec4 projCoord3,vec4 projCoord4,vec4 projCoord5){
+#ifdef POINTLIGHT
+    float getPointLightShadows(in vec4 worldPos,in vec3 lightPos,
+                           in SHADOWMAP shadowMap0,in SHADOWMAP shadowMap1,in SHADOWMAP shadowMap2,in SHADOWMAP shadowMap3,in SHADOWMAP shadowMap4,in SHADOWMAP shadowMap5,
+                           in vec4 projCoord0,in vec4 projCoord1,in vec4 projCoord2,in vec4 projCoord3,in vec4 projCoord4,in vec4 projCoord5){
         float shadow = 1.0;
         vec3 vect = worldPos.xyz - lightPos;
         vec3 absv= abs(vect);
@@ -190,42 +242,41 @@ float Shadow_DoPCFPoisson(SHADOWMAP tex, vec4 projCoord){
            }else{
                shadow = GETSHADOW(shadowMap5, projCoord5 / projCoord5.w);
            }
-        }  
+        }
         return shadow;
     }
 #else
  #ifdef PSSM
-    float getDirectionalLightShadows(vec4 splits,float shadowPosition,
-                                    SHADOWMAP shadowMap0,SHADOWMAP shadowMap1,SHADOWMAP shadowMap2,SHADOWMAP shadowMap3,
-                                    vec4 projCoord0,vec4 projCoord1,vec4 projCoord2,vec4 projCoord3){    
-        float shadow = 1.0;   
+    float getDirectionalLightShadows(in vec4 splits,in float shadowPosition,
+                                    in SHADOWMAP shadowMap0,in SHADOWMAP shadowMap1,in SHADOWMAP shadowMap2,in SHADOWMAP shadowMap3,
+                                    in vec4 projCoord0,in vec4 projCoord1,in vec4 projCoord2,in vec4 projCoord3){
+        float shadow = 1.0;
         if(shadowPosition < splits.x){
-            shadow = GETSHADOW(shadowMap0, projCoord0 );   
+            shadow = GETSHADOW(shadowMap0, projCoord0 );
         }else if( shadowPosition <  splits.y){
             shadowBorderScale = 0.5;
-            shadow = GETSHADOW(shadowMap1, projCoord1);  
+            shadow = GETSHADOW(shadowMap1, projCoord1);
         }else if( shadowPosition <  splits.z){
             shadowBorderScale = 0.25;
-            shadow = GETSHADOW(shadowMap2, projCoord2); 
+            shadow = GETSHADOW(shadowMap2, projCoord2);
         }else if( shadowPosition <  splits.w){
             shadowBorderScale = 0.125;
-            shadow = GETSHADOW(shadowMap3, projCoord3); 
+            shadow = GETSHADOW(shadowMap3, projCoord3);
         }
         return shadow;
     }
  #else
-    float getSpotLightShadows(SHADOWMAP shadowMap, vec4 projCoord){
-        float shadow = 1.0;         
+    float getSpotLightShadows(in SHADOWMAP shadowMap,in  vec4 projCoord){
+        float shadow = 1.0;
         projCoord /= projCoord.w;
-        shadow = GETSHADOW(shadowMap, projCoord);
-        
+        shadow = GETSHADOW(shadowMap,projCoord);
+
         //a small falloff to make the shadow blend nicely into the not lighten
-        //we translate the texture coordinate value to a -1,1 range so the length 
+        //we translate the texture coordinate value to a -1,1 range so the length
         //of the texture coordinate vector is actually the radius of the lighten area on the ground
         projCoord = projCoord * 2.0 - 1.0;
         float fallOff = ( length(projCoord.xy) - 0.9 ) / 0.1;
         return mix(shadow,1.0,clamp(fallOff,0.0,1.0));
-
     }
  #endif
 #endif

+ 0 - 242
jme3-core/src/main/resources/Common/ShaderLib/Shadows15.glsllib

@@ -1,242 +0,0 @@
-// Because gpu_shader5 is actually where those
-// gather functions are declared to work on shadowmaps
-#extension GL_ARB_gpu_shader5 : enable
-
-#ifdef HARDWARE_SHADOWS
-    #define SHADOWMAP sampler2DShadow
-    #define SHADOWCOMPAREOFFSET(tex,coord,offset) textureProjOffset(tex, coord, offset)
-    #define SHADOWCOMPARE(tex,coord) textureProj(tex, coord)
-    #define SHADOWGATHER(tex,coord) textureGather(tex, coord.xy, coord.z)
-#else
-    #define SHADOWMAP sampler2D
-    #define SHADOWCOMPAREOFFSET(tex,coord,offset) step(coord.z, textureProjOffset(tex, coord, offset).r)
-    #define SHADOWCOMPARE(tex,coord) step(coord.z, textureProj(tex, coord).r) 
-    #define SHADOWGATHER(tex,coord) step(coord.z, textureGather(tex, coord.xy))
-#endif
-
-
-#if FILTER_MODE == 0
-    #define GETSHADOW Shadow_Nearest
-    #define KERNEL 1.0
-#elif FILTER_MODE == 1
-    #ifdef HARDWARE_SHADOWS
-        #define GETSHADOW Shadow_Nearest
-    #else
-        #define GETSHADOW Shadow_DoBilinear_2x2
-    #endif
-    #define KERNEL 1.0
-#elif FILTER_MODE == 2
-    #define GETSHADOW Shadow_DoDither_2x2
-    #define KERNEL 1.0
-#elif FILTER_MODE == 3
-    #define GETSHADOW Shadow_DoPCF
-    #define KERNEL 4.0
-#elif FILTER_MODE == 4
-    #define GETSHADOW Shadow_DoPCFPoisson
-    #define KERNEL 4.0
-#elif FILTER_MODE == 5
-    #define GETSHADOW Shadow_DoPCF
-    #define KERNEL 8.0
-#endif
-
-
-
-uniform SHADOWMAP m_ShadowMap0;
-uniform SHADOWMAP m_ShadowMap1;
-uniform SHADOWMAP m_ShadowMap2;
-uniform SHADOWMAP m_ShadowMap3;
-#ifdef POINTLIGHT
-uniform SHADOWMAP m_ShadowMap4;
-uniform SHADOWMAP m_ShadowMap5;
-#endif
-
-#ifdef PSSM
-uniform vec4 m_Splits;
-#endif
-uniform float m_ShadowIntensity;
-
-const vec2 pixSize2 = vec2(1.0 / SHADOWMAP_SIZE);
-float shadowBorderScale = 1.0;
-
-float Shadow_BorderCheck(in vec2 coord){
-    // Fastest, "hack" method (uses 4-5 instructions)
-    vec4 t = vec4(coord.xy, 0.0, 1.0);
-    t = step(t.wwxy, t.xyzz);
-    return dot(t,t);  
-}
-
-float Shadow_Nearest(in SHADOWMAP tex, in vec4 projCoord){
-    float border = Shadow_BorderCheck(projCoord.xy);
-    if (border > 0.0){
-        return 1.0;
-    }
-    return SHADOWCOMPARE(tex,projCoord);
-}
-
-float Shadow_DoDither_2x2(in SHADOWMAP tex, in vec4 projCoord){
-    float border = Shadow_BorderCheck(projCoord.xy);
-    if (border > 0.0)
-        return 1.0;
-
-    vec2 pixSize = pixSize2 * shadowBorderScale;
-    
-    float shadow = 0.0;
-    ivec2 o = ivec2(mod(floor(gl_FragCoord.xy), 2.0));
-    shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy+pixSize*(vec2(-1.5, 1.5)+o), projCoord.zw));
-    shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy+pixSize*(vec2( 0.5, 1.5)+o), projCoord.zw));
-    shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy+pixSize*(vec2(-1.5, -0.5)+o), projCoord.zw));
-    shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy+pixSize*(vec2( 0.5, -0.5)+o), projCoord.zw));
-    shadow *= 0.25;
-    return shadow;
-}
-
-float Shadow_DoBilinear_2x2(in SHADOWMAP tex, in vec4 projCoord){
-    float border = Shadow_BorderCheck(projCoord.xy);
-    if (border > 0.0)
-        return 1.0;
-  
-    #ifdef GL_ARB_gpu_shader5
-        vec4 coord = vec4(projCoord.xyz / projCoord.www,0.0);
-        vec4 gather = SHADOWGATHER(tex, coord);
-    #else
-        vec4 gather = vec4(0.0);
-        gather.x = SHADOWCOMPAREOFFSET(tex, projCoord, ivec2(0, 1));
-        gather.y = SHADOWCOMPAREOFFSET(tex, projCoord, ivec2(1, 1));
-        gather.z = SHADOWCOMPAREOFFSET(tex, projCoord, ivec2(1, 0));
-        gather.w = SHADOWCOMPAREOFFSET(tex, projCoord, ivec2(0, 0));
-    #endif
-
-   vec2 f = fract( projCoord.xy * SHADOWMAP_SIZE );   
-   vec2 mx = mix( gather.wx, gather.zy, f.x );
-   return mix( mx.x, mx.y, f.y );
-}
-
-float Shadow_DoPCF(in SHADOWMAP tex, in vec4 projCoord){    
-
-    vec2 pixSize = pixSize2 * shadowBorderScale;  
-    float shadow = 0.0;
-    float border = Shadow_BorderCheck(projCoord.xy);
-    if (border > 0.0)
-        return 1.0;
-
-    float bound = KERNEL * 0.5 - 0.5;
-    bound *= PCFEDGE;
-    for (float y = -bound; y <= bound; y += PCFEDGE){
-        for (float x = -bound; x <= bound; x += PCFEDGE){
-            vec4 coord = vec4(projCoord.xy + vec2(x,y) * pixSize, projCoord.zw);
-            shadow += SHADOWCOMPARE(tex, coord);
-        }
-    }
-
-    shadow = shadow / (KERNEL * KERNEL);
-    return shadow;
-}
-
-
-//12 tap poisson disk
-    const vec2 poissonDisk0 =  vec2(-0.1711046, -0.425016);
-    const vec2 poissonDisk1 =  vec2(-0.7829809, 0.2162201);
-    const vec2 poissonDisk2 =  vec2(-0.2380269, -0.8835521);
-    const vec2 poissonDisk3 =  vec2(0.4198045, 0.1687819);
-    const vec2 poissonDisk4 =  vec2(-0.684418, -0.3186957);
-    const vec2 poissonDisk5 =  vec2(0.6026866, -0.2587841);
-    const vec2 poissonDisk6 =  vec2(-0.2412762, 0.3913516);
-    const vec2 poissonDisk7 =  vec2(0.4720655, -0.7664126);
-    const vec2 poissonDisk8 =  vec2(0.9571564, 0.2680693);
-    const vec2 poissonDisk9 =  vec2(-0.5238616, 0.802707);
-    const vec2 poissonDisk10 = vec2(0.5653144, 0.60262);
-    const vec2 poissonDisk11 = vec2(0.0123658, 0.8627419);
-
-
-float Shadow_DoPCFPoisson(in SHADOWMAP tex, in vec4 projCoord){  
-
-    float shadow = 0.0;
-    float border = Shadow_BorderCheck(projCoord.xy);
-    if (border > 0.0){
-        return 1.0;
-    }
-     
-    vec2 texelSize = pixSize2 * 4.0 * PCFEDGE * shadowBorderScale;        
-    
-    shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk0 * texelSize, projCoord.zw));
-    shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk1 * texelSize, projCoord.zw));
-    shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk2 * texelSize, projCoord.zw));
-    shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk3 * texelSize, projCoord.zw));
-    shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk4 * texelSize, projCoord.zw));
-    shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk5 * texelSize, projCoord.zw));
-    shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk6 * texelSize, projCoord.zw));
-    shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk7 * texelSize, projCoord.zw));
-    shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk8 * texelSize, projCoord.zw));
-    shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk9 * texelSize, projCoord.zw));
-    shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk10 * texelSize, projCoord.zw));
-    shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk11 * texelSize, projCoord.zw));
-
-    //this is divided by 12
-    return shadow * 0.08333333333;
-}
-
-#ifdef POINTLIGHT       
-    float getPointLightShadows(in vec4 worldPos,in vec3 lightPos,
-                           in SHADOWMAP shadowMap0,in SHADOWMAP shadowMap1,in SHADOWMAP shadowMap2,in SHADOWMAP shadowMap3,in SHADOWMAP shadowMap4,in SHADOWMAP shadowMap5,
-                           in vec4 projCoord0,in vec4 projCoord1,in vec4 projCoord2,in vec4 projCoord3,in vec4 projCoord4,in vec4 projCoord5){
-        float shadow = 1.0;
-        vec3 vect = worldPos.xyz - lightPos;
-        vec3 absv= abs(vect);
-        float maxComp = max(absv.x,max(absv.y,absv.z));
-        if(maxComp == absv.y){
-           if(vect.y < 0.0){
-               shadow = GETSHADOW(shadowMap0, projCoord0 / projCoord0.w);
-           }else{
-               shadow = GETSHADOW(shadowMap1, projCoord1 / projCoord1.w);
-           }
-        }else if(maxComp == absv.z){
-           if(vect.z < 0.0){
-               shadow = GETSHADOW(shadowMap2, projCoord2 / projCoord2.w);
-           }else{
-               shadow = GETSHADOW(shadowMap3, projCoord3 / projCoord3.w);
-           }
-        }else if(maxComp == absv.x){
-           if(vect.x < 0.0){
-               shadow = GETSHADOW(shadowMap4, projCoord4 / projCoord4.w);
-           }else{
-               shadow = GETSHADOW(shadowMap5, projCoord5 / projCoord5.w);
-           }
-        }  
-        return shadow;
-    }
-#else
- #ifdef PSSM
-    float getDirectionalLightShadows(in vec4 splits,in float shadowPosition,
-                                    in SHADOWMAP shadowMap0,in SHADOWMAP shadowMap1,in SHADOWMAP shadowMap2,in SHADOWMAP shadowMap3,
-                                    in vec4 projCoord0,in vec4 projCoord1,in vec4 projCoord2,in vec4 projCoord3){    
-        float shadow = 1.0;   
-        if(shadowPosition < splits.x){
-            shadow = GETSHADOW(shadowMap0, projCoord0 );   
-        }else if( shadowPosition <  splits.y){
-            shadowBorderScale = 0.5;
-            shadow = GETSHADOW(shadowMap1, projCoord1);  
-        }else if( shadowPosition <  splits.z){
-            shadowBorderScale = 0.25;
-            shadow = GETSHADOW(shadowMap2, projCoord2); 
-        }else if( shadowPosition <  splits.w){
-            shadowBorderScale = 0.125;
-            shadow = GETSHADOW(shadowMap3, projCoord3); 
-        }
-        return shadow;
-    }
- #else
-    float getSpotLightShadows(in SHADOWMAP shadowMap,in  vec4 projCoord){
-        float shadow = 1.0;     
-        projCoord /= projCoord.w;
-        shadow = GETSHADOW(shadowMap,projCoord);
-        
-        //a small falloff to make the shadow blend nicely into the not lighten
-        //we translate the texture coordinate value to a -1,1 range so the length 
-        //of the texture coordinate vector is actually the radius of the lighten area on the ground
-        projCoord = projCoord * 2.0 - 1.0;
-        float fallOff = ( length(projCoord.xy) - 0.9 ) / 0.1;
-        return mix(shadow,1.0,clamp(fallOff,0.0,1.0));
-
-    }
- #endif
-#endif

+ 19 - 4
jme3-core/src/plugins/java/com/jme3/material/plugins/J3MLoader.java

@@ -169,9 +169,23 @@ public class J3MLoader implements AssetLoader {
         return matchList;
     }
 
-    private boolean isTexturePathDeclaredTheTraditionalWay(final int numberOfValues, final int numberOfTextureOptions, final String texturePath) {
-        return (numberOfValues > 1 && (texturePath.startsWith("Flip Repeat ") || texturePath.startsWith("Flip ") ||
-                texturePath.startsWith("Repeat ") || texturePath.startsWith("Repeat Flip "))) || numberOfTextureOptions == 0;
+    private boolean isTexturePathDeclaredTheTraditionalWay(final List<TextureOptionValue> optionValues, final String texturePath) {
+        final boolean startsWithOldStyle = texturePath.startsWith("Flip Repeat ") || texturePath.startsWith("Flip ") ||
+                texturePath.startsWith("Repeat ") || texturePath.startsWith("Repeat Flip ");
+
+        if (!startsWithOldStyle) {
+            return false;
+        }
+
+        if (optionValues.size() == 1 && (optionValues.get(0).textureOption == TextureOption.Flip || optionValues.get(0).textureOption == TextureOption.Repeat)) {
+            return true;
+        } else if (optionValues.size() == 2 && optionValues.get(0).textureOption == TextureOption.Flip && optionValues.get(1).textureOption == TextureOption.Repeat) {
+            return true;
+        } else if (optionValues.size() == 2 && optionValues.get(0).textureOption == TextureOption.Repeat && optionValues.get(1).textureOption == TextureOption.Flip) {
+            return true;
+        }
+
+        return false;
     }
 
     private Texture parseTextureType(final VarType type, final String value) {
@@ -187,7 +201,7 @@ public class J3MLoader implements AssetLoader {
             String texturePath = value.trim();
 
             // If there are no valid "new" texture options specified but the path is split into several parts, lets parse the old way.
-            if (isTexturePathDeclaredTheTraditionalWay(textureValues.size(), textureOptionValues.size(), texturePath)) {
+            if (isTexturePathDeclaredTheTraditionalWay(textureOptionValues, texturePath)) {
                 boolean flipY = false;
 
                 if (texturePath.startsWith("Flip Repeat ") || texturePath.startsWith("Repeat Flip ")) {
@@ -623,6 +637,7 @@ public class J3MLoader implements AssetLoader {
 
             material = new Material(def);
             material.setKey(key);
+            material.setName(split[0].trim());
 //            material.setAssetName(fileName);
         }else if (split.length == 1){
             if (extending){

+ 2 - 0
jme3-core/src/test/java/com/jme3/material/plugins/J3MLoaderTest.java

@@ -80,6 +80,7 @@ public class J3MLoaderTest {
         final Texture textureMin = Mockito.mock(Texture.class);
         final Texture textureMag = Mockito.mock(Texture.class);
         final Texture textureCombined = Mockito.mock(Texture.class);
+        final Texture textureLooksLikeOldStyle = Mockito.mock(Texture.class);
 
         final TextureKey textureKeyNoParameters = setupMockForTexture("Empty", "empty.png", false, textureNoParameters);
         final TextureKey textureKeyFlip = setupMockForTexture("Flip", "flip.png", true, textureFlip);
@@ -88,6 +89,7 @@ public class J3MLoaderTest {
         setupMockForTexture("Min", "min.png", false, textureMin);
         setupMockForTexture("Mag", "mag.png", false, textureMag);
         setupMockForTexture("Combined", "combined.png", true, textureCombined);
+        setupMockForTexture("LooksLikeOldStyle", "oldstyle.png", true, textureLooksLikeOldStyle);
 
         j3MLoader.load(assetInfo);
 

+ 2 - 1
jme3-core/src/test/resources/texture-parameters-newstyle.j3m

@@ -6,6 +6,7 @@ Material Test : matdef.j3md {
          Min: MinTrilinear "min.png"
          Mag: MagBilinear "mag.png"
          RepeatAxis: WrapRepeat_T "repeat-axis.png"
-         Combined: MagNearest MinBilinearNoMipMaps Flip WrapRepeat "combined.png"
+         Combined: Flip MagNearest MinBilinearNoMipMaps WrapRepeat "combined.png"
+         LooksLikeOldStyle: Flip WrapRepeat "oldstyle.png"
      }
 }

+ 2 - 2
jme3-effects/src/main/resources/Common/MatDefs/SSAO/normal.vert

@@ -19,6 +19,6 @@ void main(void)
    #ifdef NUM_BONES
        Skinning_Compute(modelSpacePos,modelSpaceNormals);
    #endif
-   normal = normalize(g_NormalMatrix * modelSpaceNormals);
-   gl_Position = g_WorldViewProjectionMatrix * modelSpacePos;
+   normal = normalize(TransformNormal(modelSpaceNormals));
+   gl_Position = TransformWorldViewProjection(modelSpacePos);
 }

+ 9 - 5
jme3-examples/build.gradle

@@ -5,11 +5,15 @@ if (!hasProperty('mainClass')) {
 }
 
 task run(dependsOn: 'build', type:JavaExec) {
-   main = mainClass
-   classpath = sourceSets.main.runtimeClasspath   
-   if( assertions  == "true" ){
-       enableAssertions = true;
-   }
+    main = mainClass
+    classpath = sourceSets.main.runtimeClasspath
+    if (System.properties['os.name'].toLowerCase().contains('mac')) {
+        jvmArgs "-XstartOnFirstThread"
+        jvmArgs "-Djava.awt.headless=true"
+    }
+    if( assertions  == "true" ){
+        enableAssertions = true;
+    }
 }
 
 dependencies {

+ 3 - 3
jme3-examples/src/main/java/jme3test/TestChooser.java

@@ -260,8 +260,8 @@ public class TestChooser extends JDialog {
                     for (int i = 0; i < appClass.length; i++) {
                 	    Class<?> clazz = (Class)appClass[i];
                 		try {
-                			Object app = clazz.newInstance();
-                			if (app instanceof Application) {
+                			if (Application.class.isAssignableFrom(clazz)) {
+                    			Object app = clazz.newInstance();
                 			    if (app instanceof SimpleApplication) {
                 			        final Method settingMethod = clazz.getMethod("setShowSettings", boolean.class);
                 			        settingMethod.invoke(app, showSetting);
@@ -283,7 +283,7 @@ public class TestChooser extends JDialog {
                 			    }
                 			} else {
                                 final Method mainMethod = clazz.getMethod("main", (new String[0]).getClass());
-                                mainMethod.invoke(app, new Object[]{new String[0]});
+                                mainMethod.invoke(clazz, new Object[]{new String[0]});
                 			}
                 			// wait for destroy
                 			System.gc();

+ 367 - 0
jme3-examples/src/main/java/jme3test/app/TestCloner.java

@@ -0,0 +1,367 @@
+/*
+ * Copyright (c) 2016 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 jme3test.app;
+
+import java.util.*;
+
+import com.jme3.util.clone.*;
+
+/**
+ *
+ *
+ *  @author    Paul Speed
+ */
+public class TestCloner {
+    
+    public static void main( String... args ) {
+        
+        System.out.println("Clone test:");
+        
+        Cloner cloner = new Cloner();
+        
+        RegularObject ro = new RegularObject(42);
+        System.out.println("Regular Object:" + ro);
+        RegularObject roCloneLegacy = ro.clone();
+        System.out.println("Regular Object Clone:" + roCloneLegacy);
+        RegularObject roClone = cloner.clone(ro);
+        System.out.println("cloner: Regular Object Clone:" + roClone);
+ 
+        System.out.println("------------------------------------");
+        System.out.println();
+        
+        cloner = new Cloner();       
+        RegularSubclass rsc = new RegularSubclass(69, "test");
+        System.out.println("Regular subclass:" + rsc);
+        RegularSubclass rscCloneLegacy = (RegularSubclass)rsc.clone();
+        System.out.println("Regular subclass Clone:" + rscCloneLegacy);
+        RegularSubclass rscClone = cloner.clone(rsc);
+        System.out.println("cloner: Regular subclass Clone:" + rscClone);
+        
+        System.out.println("------------------------------------");
+        System.out.println();
+ 
+        cloner = new Cloner();       
+        Parent parent = new Parent("Foo", 34);
+        System.out.println("Parent:" + parent);
+        Parent parentCloneLegacy = parent.clone();
+        System.out.println("Parent Clone:" + parentCloneLegacy);
+        Parent parentClone = cloner.clone(parent);
+        System.out.println("cloner: Parent Clone:" + parentClone);
+         
+        System.out.println("------------------------------------");
+        System.out.println();
+        
+        cloner = new Cloner();
+        GraphNode root = new GraphNode("root");
+        GraphNode child1 = root.addLink("child1");
+        GraphNode child2 = root.addLink("child2");        
+        GraphNode shared = child1.addLink("shared");
+        child2.addLink(shared);
+        
+        // Add a circular reference to get fancy
+        shared.addLink(root);
+        
+        System.out.println("Simple graph:");
+        root.dump("  ");
+        
+        GraphNode rootClone = cloner.clone(root);
+        System.out.println("clone:");  
+        rootClone.dump("  ");
+        
+        System.out.println("original:");
+        root.dump("  ");
+ 
+        GraphNode reclone = Cloner.deepClone(root);
+        System.out.println("reclone:");  
+        reclone.dump("  ");
+        
+        System.out.println("------------------------------------");
+        System.out.println();
+        cloner = new Cloner();
+        
+        ArrayHolder arrays = new ArrayHolder(5, 3, 7, 3, 7, 2, 1, 4);
+        System.out.println("Array holder:" + arrays);       
+        ArrayHolder arraysClone = cloner.clone(arrays);
+        System.out.println("Array holder clone:" + arraysClone);       
+ 
+           
+        
+    }
+    
+    public static class RegularObject implements Cloneable {
+        protected int i;
+        
+        public RegularObject( int i ) {
+            this.i = i;
+        }
+ 
+        public RegularObject clone() {
+            try {
+                return (RegularObject)super.clone();
+            } catch( CloneNotSupportedException e ) {
+                throw new RuntimeException(e);
+            }
+        }
+        
+        public String toString() {
+            return getClass().getSimpleName() + "@" + System.identityHashCode(this) 
+                    + "[i=" + i + "]";
+        }
+    }
+    
+    public static class RegularSubclass extends RegularObject {
+        protected String name;
+        
+        public RegularSubclass( int i, String name ) {
+            super(i);
+            this.name = name;
+        }
+               
+        public String toString() {
+            return getClass().getSimpleName() + "@" + System.identityHashCode(this) 
+                    + "[i=" + i + ", name=" + name + "]";
+        }
+    }
+    
+    public static class Parent implements Cloneable, JmeCloneable {
+        
+        private RegularObject ro;
+        private RegularSubclass rsc;
+        
+        public Parent( String name, int age ) {
+            this.ro = new RegularObject(age);
+            this.rsc = new RegularSubclass(age, name);
+        }
+        
+        public Parent clone() {
+            try {
+                return (Parent)super.clone();
+            } catch( CloneNotSupportedException e ) {
+                throw new RuntimeException(e);
+            }
+        }
+        
+        public Parent jmeClone() {
+            // Ok to delegate to clone() in this case because no deep
+            // cloning is done there.
+            return clone();
+        }
+ 
+        public void cloneFields( Cloner cloner, Object original ) {
+            this.ro = cloner.clone(ro);
+            this.rsc = cloner.clone(rsc);
+        } 
+        
+        public String toString() {
+            return getClass().getSimpleName() + "@" + System.identityHashCode(this)
+                    + "[ro=" + ro + ", rsc=" + rsc + "]";
+        }
+    }
+    
+    public static class GraphNode implements Cloneable, JmeCloneable {
+ 
+        private String name;       
+        private List<GraphNode> links = new ArrayList<>();
+        
+        public GraphNode( String name ) {
+            this.name = name;
+        }
+        
+        public void dump( String indent ) {
+            dump(indent, new HashSet<GraphNode>());        
+        }
+        
+        private void dump( String indent, Set<GraphNode> visited ) {
+            if( visited.contains(this) ) {
+                // already been here
+                System.out.println(indent + this + " ** circular.");
+                return;
+            } 
+            System.out.println(indent + this);
+            visited.add(this);
+            for( GraphNode n : links ) {
+                n.dump(indent + "    ", visited);
+            }
+            visited.remove(this);
+        }
+        
+        public GraphNode addLink( String name ) {
+            GraphNode node = new GraphNode(name);
+            links.add(node);
+            return node;
+        }
+        
+        public GraphNode addLink( GraphNode node ) {
+            links.add(node);
+            return node;
+        }
+        
+        public List<GraphNode> getLinks() {
+            return links;
+        }
+        
+        public GraphNode jmeClone() {
+            try {
+                return (GraphNode)super.clone();
+            } catch( CloneNotSupportedException e ) {
+                throw new RuntimeException(e);
+            }
+        }
+ 
+        public void cloneFields( Cloner cloner, Object original ) {
+            this.links = cloner.clone(links);
+        } 
+        
+        public String toString() {
+            return getClass().getSimpleName() + "@" + System.identityHashCode(this)
+                    + "[name=" + name + "]";
+        }
+    }
+    
+    public static class ArrayHolder implements JmeCloneable {
+    
+        private int[] intArray;
+        private int[][] intArray2D;
+        private Object[] objects;
+        private RegularObject[] regularObjects;
+        private String[] strings;
+ 
+        public ArrayHolder( int... values ) {
+            this.intArray = values;
+            this.intArray2D = new int[values.length][2];
+            for( int i = 0; i < values.length; i++ ) {
+                intArray2D[i][0] = values[i] + 1;
+                intArray2D[i][1] = values[i] * 2;
+            }
+            this.objects = new Object[values.length];
+            this.regularObjects = new RegularObject[values.length];
+            this.strings = new String[values.length];
+            for( int i = 0; i < values.length; i++ ) {
+                objects[i] = values[i];
+                regularObjects[i] = new RegularObject(values[i]);
+                strings[i] = String.valueOf(values[i]);   
+            }
+        }
+        
+        public ArrayHolder jmeClone() {
+            try {
+                return (ArrayHolder)super.clone();
+            } catch( CloneNotSupportedException e ) {
+                throw new RuntimeException(e);
+            }
+        }
+ 
+        public void cloneFields( Cloner cloner, Object original ) {
+            intArray = cloner.clone(intArray);
+            intArray2D = cloner.clone(intArray2D);
+            
+            // Boxed types are not cloneable so this will fail
+            //objects = cloner.clone(objects);
+            
+            regularObjects = cloner.clone(regularObjects);
+            
+            // Strings are also not cloneable
+            //strings = cloner.clone(strings);
+        }
+        
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append("intArray=" + intArray);
+            for( int i = 0; i < intArray.length; i++ ) {
+                if( i == 0 ) {
+                    sb.append("[");
+                } else {
+                    sb.append(", ");
+                }
+                sb.append(intArray[i]);
+            }
+            sb.append("], ");
+            
+            sb.append("intArray2D=" + intArray2D);
+            for( int i = 0; i < intArray2D.length; i++ ) {
+                if( i == 0 ) {
+                    sb.append("[");
+                } else {
+                    sb.append(", ");
+                }
+                sb.append("intArray2D[" + i + "]=" + intArray2D[i]);
+                for( int j = 0; j < 2; j++ ) {
+                    if( j == 0 ) {
+                        sb.append("[");
+                    } else {
+                        sb.append(", ");
+                    }
+                    sb.append(intArray2D[i][j]);                    
+                }
+                sb.append("], ");
+            }
+            sb.append("], ");
+            
+            sb.append("objectArray=" + objects);
+            for( int i = 0; i < objects.length; i++ ) {
+                if( i == 0 ) {
+                    sb.append("[");
+                } else {
+                    sb.append(", ");
+                }
+                sb.append(objects[i]);
+            }
+            sb.append("], ");
+            
+            sb.append("objectArray=" + regularObjects);
+            for( int i = 0; i < regularObjects.length; i++ ) {
+                if( i == 0 ) {
+                    sb.append("[");
+                } else {
+                    sb.append(", ");
+                }
+                sb.append(regularObjects[i]);
+            }
+            sb.append("], ");
+            
+            sb.append("stringArray=" + strings);
+            for( int i = 0; i < strings.length; i++ ) {
+                if( i == 0 ) {
+                    sb.append("[");
+                } else {
+                    sb.append(", ");
+                }
+                sb.append(strings[i]);
+            }
+            sb.append("]");
+            
+            return getClass().getSimpleName() + "@" + System.identityHashCode(this)
+                    + "[" + sb + "]";
+        }       
+    }
+}

+ 14 - 1
jme3-examples/src/main/java/jme3test/bullet/PhysicsHoverControl.java

@@ -46,13 +46,15 @@ import com.jme3.renderer.RenderManager;
 import com.jme3.renderer.ViewPort;
 import com.jme3.scene.Spatial;
 import com.jme3.scene.control.Control;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 
 /**
  * PhysicsHoverControl uses a RayCast Vehicle with "slippery wheels" to simulate a hovering tank
  * @author normenhansen
  */
-public class PhysicsHoverControl extends PhysicsVehicle implements PhysicsControl, PhysicsTickListener {
+public class PhysicsHoverControl extends PhysicsVehicle implements PhysicsControl, PhysicsTickListener, JmeCloneable {
 
     protected Spatial spatial;
     protected boolean enabled = true;
@@ -94,10 +96,21 @@ public class PhysicsHoverControl extends PhysicsVehicle implements PhysicsContro
         createWheels();
     }
 
+    @Override
     public Control cloneForSpatial(Spatial spatial) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
 
+    @Override   
+    public Object jmeClone() {
+        throw new UnsupportedOperationException("Not yet implemented.");
+    }     
+
+    @Override   
+    public void cloneFields( Cloner cloner, Object original ) { 
+        throw new UnsupportedOperationException("Not yet implemented.");
+    }
+         
     public void setSpatial(Spatial spatial) {
         this.spatial = spatial;
         setUserObject(spatial);

+ 29 - 10
jme3-examples/src/main/java/jme3test/light/TestDirectionalLightShadow.java

@@ -40,12 +40,14 @@ import com.jme3.input.controls.KeyTrigger;
 import com.jme3.light.AmbientLight;
 import com.jme3.light.DirectionalLight;
 import com.jme3.material.Material;
+import com.jme3.material.RenderState;
 import com.jme3.math.ColorRGBA;
 import com.jme3.math.FastMath;
 import com.jme3.math.Quaternion;
 import com.jme3.math.Vector2f;
 import com.jme3.math.Vector3f;
 import com.jme3.post.FilterPostProcessor;
+import com.jme3.post.ssao.SSAOFilter;
 import com.jme3.renderer.queue.RenderQueue.ShadowMode;
 import com.jme3.scene.Geometry;
 import com.jme3.scene.Spatial;
@@ -69,6 +71,7 @@ public class TestDirectionalLightShadow extends SimpleApplication implements Act
     private Geometry ground;
     private Material matGroundU;
     private Material matGroundL;
+    private AmbientLight al;
 
     public static void main(String[] args) {
         TestDirectionalLightShadow app = new TestDirectionalLightShadow();
@@ -99,7 +102,7 @@ public class TestDirectionalLightShadow extends SimpleApplication implements Act
         mat[0] = assetManager.loadMaterial("Common/Materials/RedColor.j3m");
         mat[1] = assetManager.loadMaterial("Textures/Terrain/Pond/Pond.j3m");
         mat[1].setBoolean("UseMaterialColors", true);
-        mat[1].setColor("Ambient", ColorRGBA.White.mult(0.5f));
+        mat[1].setColor("Ambient", ColorRGBA.White);
         mat[1].setColor("Diffuse", ColorRGBA.White.clone());
 
 
@@ -110,9 +113,14 @@ public class TestDirectionalLightShadow extends SimpleApplication implements Act
         TangentBinormalGenerator.generate(obj[1]);
         TangentBinormalGenerator.generate(obj[0]);
 
+        Spatial t = obj[0].clone(false);
+        t.setLocalScale(10f);
+        t.setMaterial(mat[1]);
+        rootNode.attachChild(t);
+        t.setLocalTranslation(0, 25, 0);
 
         for (int i = 0; i < 60; i++) {
-            Spatial t = obj[FastMath.nextRandomInt(0, obj.length - 1)].clone(false);
+            t = obj[FastMath.nextRandomInt(0, obj.length - 1)].clone(false);
             t.setLocalScale(FastMath.nextRandomFloat() * 10f);
             t.setMaterial(mat[FastMath.nextRandomInt(0, mat.length - 1)]);
             rootNode.attachChild(t);
@@ -142,8 +150,8 @@ public class TestDirectionalLightShadow extends SimpleApplication implements Act
         rootNode.addLight(l);
 
 
-        AmbientLight al = new AmbientLight();
-        al.setColor(ColorRGBA.White.mult(0.5f));
+        al = new AmbientLight();
+        al.setColor(ColorRGBA.White.mult(0.02f));
         rootNode.addLight(al);
 
         Spatial sky = SkyFactory.createSky(assetManager, "Scenes/Beach/FullskiesSunset0068.dds", false);
@@ -156,8 +164,11 @@ public class TestDirectionalLightShadow extends SimpleApplication implements Act
     @Override
     public void simpleInitApp() {
         // put the camera in a bad position
-        cam.setLocation(new Vector3f(65.25412f, 44.38738f, 9.087874f));
-        cam.setRotation(new Quaternion(0.078139365f, 0.050241485f, -0.003942559f, 0.9956679f));
+//        cam.setLocation(new Vector3f(65.25412f, 44.38738f, 9.087874f));
+//        cam.setRotation(new Quaternion(0.078139365f, 0.050241485f, -0.003942559f, 0.9956679f));
+
+        cam.setLocation(new Vector3f(3.3720117f, 42.838284f, -83.43792f));
+        cam.setRotation(new Quaternion(0.13833192f, -0.08969371f, 0.012581267f, 0.9862358f));
 
         flyCam.setMoveSpeed(100);
 
@@ -166,7 +177,7 @@ public class TestDirectionalLightShadow extends SimpleApplication implements Act
         dlsr = new DirectionalLightShadowRenderer(assetManager, SHADOWMAP_SIZE, 3);
         dlsr.setLight(l);
         dlsr.setLambda(0.55f);
-        dlsr.setShadowIntensity(0.6f);
+        dlsr.setShadowIntensity(0.8f);
         dlsr.setEdgeFilteringMode(EdgeFilteringMode.Nearest);
         dlsr.displayDebug();
         viewPort.addProcessor(dlsr);
@@ -174,7 +185,7 @@ public class TestDirectionalLightShadow extends SimpleApplication implements Act
         dlsf = new DirectionalLightShadowFilter(assetManager, SHADOWMAP_SIZE, 3);
         dlsf.setLight(l);
         dlsf.setLambda(0.55f);
-        dlsf.setShadowIntensity(0.6f);
+        dlsf.setShadowIntensity(0.8f);
         dlsf.setEdgeFilteringMode(EdgeFilteringMode.Nearest);
         dlsf.setEnabled(false);
 
@@ -205,10 +216,11 @@ public class TestDirectionalLightShadow extends SimpleApplication implements Act
         inputManager.addMapping("fwd", new KeyTrigger(KeyInput.KEY_PGUP));
         inputManager.addMapping("back", new KeyTrigger(KeyInput.KEY_PGDN));
         inputManager.addMapping("pp", new KeyTrigger(KeyInput.KEY_P));
+        inputManager.addMapping("backShadows", new KeyTrigger(KeyInput.KEY_B));
 
 
         inputManager.addListener(this, "lambdaUp", "lambdaDown", "ThicknessUp", "ThicknessDown",
-                "switchGroundMat", "debug", "up", "down", "right", "left", "fwd", "back", "pp", "stabilize", "distance");
+                "switchGroundMat", "debug", "up", "down", "right", "left", "fwd", "back", "pp", "stabilize", "distance", "ShadowUp", "ShadowDown", "backShadows");
 
         ShadowTestUIManager uiMan = new ShadowTestUIManager(assetManager, dlsr, dlsf, guiNode, inputManager, viewPort);
 
@@ -255,12 +267,19 @@ public class TestDirectionalLightShadow extends SimpleApplication implements Act
             dlsf.setLambda(dlsr.getLambda() - 0.01f);
             System.out.println("Lambda : " + dlsr.getLambda());
         }
-
+        if ((name.equals("ShadowUp") || name.equals("ShadowDown")) && keyPressed) {
+            al.setColor(ColorRGBA.White.mult((1 - dlsr.getShadowIntensity()) * 0.2f));
+        }
 
         if (name.equals("debug") && keyPressed) {
             dlsr.displayFrustum();
         }
 
+        if (name.equals("backShadows") && keyPressed) {
+            dlsr.setRenderBackFacesShadows(!dlsr.isRenderBackFacesShadows());
+            dlsf.setRenderBackFacesShadows(!dlsf.isRenderBackFacesShadows());
+        }
+
         if (name.equals("stabilize") && keyPressed) {
             dlsr.setEnabledStabilization(!dlsr.isEnabledStabilization());
             dlsf.setEnabledStabilization(!dlsf.isEnabledStabilization());

+ 22 - 3
jme3-examples/src/main/java/jme3test/light/TestPointLightShadows.java

@@ -32,7 +32,10 @@
 package jme3test.light;
 
 import com.jme3.app.SimpleApplication;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.light.AmbientLight;
 import com.jme3.light.PointLight;
+import com.jme3.math.ColorRGBA;
 import com.jme3.math.Quaternion;
 import com.jme3.math.Vector3f;
 import com.jme3.post.FilterPostProcessor;
@@ -45,7 +48,7 @@ import com.jme3.shadow.EdgeFilteringMode;
 import com.jme3.shadow.PointLightShadowFilter;
 import com.jme3.shadow.PointLightShadowRenderer;
 
-public class TestPointLightShadows extends SimpleApplication {
+public class TestPointLightShadows extends SimpleApplication implements ActionListener{
     public static final int SHADOWMAP_SIZE = 512;
 
     public static void main(String[] args) {
@@ -55,13 +58,19 @@ public class TestPointLightShadows extends SimpleApplication {
     Node lightNode;
     PointLightShadowRenderer plsr;
     PointLightShadowFilter plsf;
+    AmbientLight al;
 
     @Override
-    public void simpleInitApp() {
+    public void simpleInitApp () {
         flyCam.setMoveSpeed(10);
         cam.setLocation(new Vector3f(0.040581334f, 1.7745866f, 6.155161f));
         cam.setRotation(new Quaternion(4.3868728E-5f, 0.9999293f, -0.011230096f, 0.0039059948f));
 
+        al = new AmbientLight(ColorRGBA.White.mult(0.02f));
+        rootNode.addLight(al);
+
+
+
 
         Node scene = (Node) assetManager.loadModel("Models/Test/CornellBox.j3o");
         scene.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
@@ -89,6 +98,7 @@ public class TestPointLightShadows extends SimpleApplication {
         plsr.setEdgeFilteringMode(EdgeFilteringMode.PCF4);
         plsr.setShadowZExtend(15);
         plsr.setShadowZFadeLength(5);
+        plsr.setShadowIntensity(0.9f);
        // plsr.setFlushQueues(false);
         //plsr.displayFrustum();
         plsr.displayDebug();
@@ -99,18 +109,27 @@ public class TestPointLightShadows extends SimpleApplication {
         plsf.setLight((PointLight) scene.getLocalLightList().get(0));    
         plsf.setShadowZExtend(15);
         plsf.setShadowZFadeLength(5);
+        plsf.setShadowIntensity(0.8f);
         plsf.setEdgeFilteringMode(EdgeFilteringMode.PCF4);
         plsf.setEnabled(false);
 
         FilterPostProcessor fpp = new FilterPostProcessor(assetManager);
         fpp.addFilter(plsf);
         viewPort.addProcessor(fpp);
-              
+        inputManager.addListener(this,"ShadowUp","ShadowDown");
         ShadowTestUIManager uiMan = new ShadowTestUIManager(assetManager, plsr, plsf, guiNode, inputManager, viewPort);
+
     }
 
     @Override
     public void simpleUpdate(float tpf) {
  //      lightNode.move(FastMath.cos(tpf) * 0.4f, 0, FastMath.sin(tpf) * 0.4f);
     }
+
+    @Override
+    public void onAction(String name, boolean isPressed, float tpf) {
+        if ((name.equals("ShadowUp") || name.equals("ShadowDown")) && isPressed) {
+            al.setColor(ColorRGBA.White.mult((1 - plsr.getShadowIntensity()) * 0.2f));
+        }
+    }
 }

+ 12 - 0
jme3-examples/src/main/java/jme3test/light/TestPssmShadow.java

@@ -67,6 +67,8 @@ import com.jme3.texture.Texture;
 import com.jme3.texture.Texture.WrapMode;
 import com.jme3.util.SkyFactory;
 import com.jme3.util.TangentBinormalGenerator;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 
 public class TestPssmShadow extends SimpleApplication implements ActionListener {
@@ -249,10 +251,20 @@ public class TestPssmShadow extends SimpleApplication implements ActionListener
             time = 0;
         }
 
+        @Override   
+        public Object jmeClone() {
+            return null;
+        }     
+
+        @Override   
+        public void cloneFields( Cloner cloner, Object original ) { 
+        }
+             
         @Override
         protected void controlRender(RenderManager rm, ViewPort vp) {
         }
 
+        @Override
         public Control cloneForSpatial(Spatial spatial) {
             return null;
         }

+ 1 - 1
jme3-examples/src/main/java/jme3test/light/TestSpotLight.java

@@ -58,7 +58,7 @@ public class TestSpotLight extends SimpleApplication {
     Geometry lightMdl;
     public void setupLighting(){
       AmbientLight al=new AmbientLight();
-      al.setColor(ColorRGBA.White.mult(0.8f));
+      al.setColor(ColorRGBA.White.mult(0.02f));
       rootNode.addLight(al);
         
       spot=new SpotLight();

+ 1 - 6
jme3-examples/src/main/java/jme3test/light/TestSpotLightShadows.java

@@ -65,7 +65,7 @@ public class TestSpotLightShadows extends SimpleApplication {
 
     public void setupLighting() {
         AmbientLight al = new AmbientLight();
-        al.setColor(ColorRGBA.White.mult(0.3f));
+        al.setColor(ColorRGBA.White.mult(0.02f));
         rootNode.addLight(al);
 
         rootNode.setShadowMode(ShadowMode.CastAndReceive);
@@ -132,11 +132,6 @@ public class TestSpotLightShadows extends SimpleApplication {
         }, "stop");
 
         inputManager.addMapping("stop", new KeyTrigger(KeyInput.KEY_1));
-        
-        MaterialDebugAppState s = new MaterialDebugAppState();
-        s.registerBinding("Common/MatDefs/Shadow/PostShadow15.frag", rootNode);
-        s.registerBinding(new KeyTrigger(KeyInput.KEY_R), rootNode);
-        stateManager.attach(s);
         flyCam.setDragToRotate(true);
     }
 

+ 1 - 1
jme3-examples/src/main/java/jme3test/light/TestSpotLightTerrain.java

@@ -95,7 +95,7 @@ public class TestSpotLightTerrain extends SimpleApplication {
         rootNode.addLight(sl);
 
         AmbientLight ambLight = new AmbientLight();
-        ambLight.setColor(new ColorRGBA(0.8f, 0.8f, 0.8f, 0.2f));
+        ambLight.setColor(ColorRGBA.Black);
         rootNode.addLight(ambLight);
 
         cam.setLocation(new Vector3f(-41.219646f, -84.8363f, -171.67267f));

+ 14 - 0
jme3-examples/src/main/java/jme3test/network/TestChatClient.java

@@ -41,6 +41,8 @@ import java.awt.Component;
 import java.awt.Dimension;
 import java.awt.event.ActionEvent;
 import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 import javax.swing.*;
 import jme3test.network.TestChatServer.ChatMessage;
 
@@ -115,6 +117,18 @@ public class TestChatClient extends JFrame {
 
     public static void main(String... args) throws Exception {
     
+        // Increate the logging level for networking...
+        System.out.println("Setting logging to max");
+        Logger networkLog = Logger.getLogger("com.jme3.network"); 
+        networkLog.setLevel(Level.FINEST);
+ 
+        // And we have to tell JUL's handler also   
+        // turn up logging in a very convoluted way
+        Logger rootLog = Logger.getLogger("");
+        if( rootLog.getHandlers().length > 0 ) {
+            rootLog.getHandlers()[0].setLevel(Level.FINEST);
+        }
+                
         // Note: in JME 3.1 this is generally unnecessary as the server will
         // send a message with all server-registered classes.
         // TestChatServer.initializeClasses();

+ 20 - 1
jme3-examples/src/main/java/jme3test/network/TestChatServer.java

@@ -31,6 +31,9 @@
  */
 package jme3test.network;
 
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
 import com.jme3.network.*;
 import com.jme3.network.serializing.Serializable;
 import com.jme3.network.serializing.Serializer;
@@ -56,11 +59,15 @@ public class TestChatServer {
     private boolean isRunning;
     
     public TestChatServer() throws IOException {
-        initializeClasses();
 
         // Use this to test the client/server name version check
         this.server = Network.createServer(NAME, VERSION, PORT, UDP_PORT);
 
+        // Initialize our own messages only after the server has been created.
+        // It registers some additional messages with the serializer by default
+        // that need to go before custom messages.
+        initializeClasses();
+
         ChatHandler handler = new ChatHandler();
         server.addMessageListener(handler, ChatMessage.class);
         
@@ -121,6 +128,18 @@ public class TestChatServer {
     }
 
     public static void main(String... args) throws Exception {
+ 
+        // Increate the logging level for networking...
+        System.out.println("Setting logging to max");
+        Logger networkLog = Logger.getLogger("com.jme3.network"); 
+        networkLog.setLevel(Level.FINEST);
+ 
+        // And we have to tell JUL's handler also   
+        // turn up logging in a very convoluted way
+        Logger rootLog = Logger.getLogger("");
+        if( rootLog.getHandlers().length > 0 ) {
+            rootLog.getHandlers()[0].setLevel(Level.FINEST);
+        }        
     
         TestChatServer chatServer = new TestChatServer();
         chatServer.start();

+ 11 - 18
jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java

@@ -99,24 +99,6 @@ public abstract class LwjglContext implements JmeContext {
         return Integer.MAX_VALUE;
     }
 
-    protected void loadNatives() {
-        if (JmeSystem.isLowPermissions()) {
-            return;
-        }
-
-        if ("LWJGL".equals(settings.getAudioRenderer())) {
-            NativeLibraryLoader.loadNativeLibrary("openal-lwjgl3", true);
-        }
-
-        if (NativeLibraryLoader.isUsingNativeBullet()) {
-            NativeLibraryLoader.loadNativeLibrary("bulletjme", true);
-        }
-
-        NativeLibraryLoader.loadNativeLibrary("glfw-lwjgl3", true);
-        NativeLibraryLoader.loadNativeLibrary("jemalloc-lwjgl3", true);
-        NativeLibraryLoader.loadNativeLibrary("lwjgl3", true);
-    }
-
     protected int getNumSamplesToUse() {
         int samples = 0;
         if (settings.getSamples() > 1) {
@@ -134,6 +116,17 @@ public abstract class LwjglContext implements JmeContext {
         return samples;
     }
 
+    protected void loadNatives() {
+        if (JmeSystem.isLowPermissions()) {
+            return;
+        }
+
+        if (NativeLibraryLoader.isUsingNativeBullet()) {
+            NativeLibraryLoader.loadNativeLibrary("bulletjme", true);
+        }
+    }
+
+
     protected void initContextFirstTime() {
         final GLCapabilities capabilities = createCapabilities(settings.getRenderer().equals(AppSettings.LWJGL_OPENGL3));
 

+ 15 - 11
jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java

@@ -81,6 +81,8 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable {
     private GLFWWindowSizeCallback windowSizeCallback;
     private GLFWWindowFocusCallback windowFocusCallback;
 
+    private Thread mainThread;
+
     public LwjglWindow(final JmeContext.Type type) {
         if (!JmeContext.Type.Display.equals(type) && !JmeContext.Type.OffscreenSurface.equals(type) && !JmeContext.Type.Canvas.equals(type)) {
             throw new IllegalArgumentException("Unsupported type '" + type.name() + "' provided");
@@ -210,7 +212,6 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable {
             @Override
             public void invoke(final long window, final int focused) {
                 final boolean focus = (focused == GL_TRUE);
-
                 if (wasActive != focus) {
                     if (!wasActive) {
                         listener.gainFocus();
@@ -241,10 +242,6 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable {
             glfwSwapInterval(0);
         }
 
-        // Make the window visible
-        if (Type.Display.equals(type)) {
-            glfwShowWindow(window);
-        }
 
         glfwShowWindow(window);
 
@@ -286,17 +283,16 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable {
         }
     }
 
+    @Override
     public void create(boolean waitFor) {
         if (created.get()) {
             LOGGER.warning("create() called when display is already created!");
             return;
         }
 
-        new Thread(this, THREAD_NAME).start();
-
-        if (waitFor) {
-            waitFor(true);
-        }
+        // NOTE: this is required for Mac OS X!
+        mainThread = Thread.currentThread();
+        run();
     }
 
     /**
@@ -307,6 +303,7 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable {
             if (!JmeSystem.isLowPermissions()) {
                 // Enable uncaught exception handler only for current thread
                 Thread.currentThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
+                    @Override
                     public void uncaughtException(Thread thread, Throwable thrown) {
                         listener.handleError("Uncaught exception thrown in " + thread.toString(), thrown);
                         if (needClose.get()) {
@@ -318,6 +315,8 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable {
                 });
             }
 
+            loadNatives();
+
             timer = new NanoTimer();
 
             // For canvas, this will create a pbuffer,
@@ -434,13 +433,13 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable {
         LOGGER.fine("Display destroyed.");
     }
 
+    @Override
     public void run() {
         if (listener == null) {
             throw new IllegalStateException("SystemListener is not set on context!"
                     + "Must set with JmeContext.setSystemListener().");
         }
 
-        loadNatives();
         LOGGER.log(Level.FINE, "Using LWJGL {0}", Version.getVersion());
 
         if (!initInThread()) {
@@ -497,6 +496,11 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable {
     public void destroy(boolean waitFor) {
         needClose.set(true);
 
+        if (mainThread == Thread.currentThread()) {
+            // Ignore waitFor.
+            return;
+        }
+
         if (waitFor) {
             waitFor(false);
         }

+ 1 - 0
jme3-plugins/build.gradle

@@ -14,4 +14,5 @@ sourceSets {
 
 dependencies {
     compile project(':jme3-core')
+    testCompile project(':jme3-desktop')
 }

+ 78 - 0
jme3-plugins/src/main/java/com/jme3/material/plugin/export/material/J3MExporter.java

@@ -0,0 +1,78 @@
+/*
+ * 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.material.plugin.export.material;
+
+import com.jme3.export.JmeExporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.export.Savable;
+import com.jme3.material.Material;
+import com.jme3.material.MaterialDef;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.nio.charset.Charset;
+
+/**
+ * Saves a Material to a j3m file with proper formatting.
+ *
+ * usage is :
+ * <pre>
+ *     J3MExporter exporter = new J3MExporter();
+ *     exporter.save(material, myFile);
+ *     //or
+ *     exporter.save(material, myOutputStream);
+ * </pre>
+ *
+ * @author tsr
+ * @author nehon (documentation and safety check)
+ */
+public class J3MExporter implements JmeExporter {
+
+    private final J3MRootOutputCapsule rootCapsule;
+
+    /**
+     * Create a J3MExporter
+     */
+    public J3MExporter() {
+        rootCapsule = new J3MRootOutputCapsule(this);
+    }
+
+    @Override
+    public void save(Savable object, OutputStream f) throws IOException {
+
+        if (!(object instanceof Material)) {
+            throw new IllegalArgumentException("J3MExporter can only save com.jme3.material.Material class");
+        }
+
+        OutputStreamWriter out = new OutputStreamWriter(f, Charset.forName("UTF-8"));
+
+        rootCapsule.clear();
+        object.write(this);
+        rootCapsule.writeToStream(out);
+
+        out.flush();
+    }
+
+    @Override
+    public void save(Savable object, File f) throws IOException {
+        try (FileOutputStream fos = new FileOutputStream(f)) {
+            save(object, fos);
+        }
+    }
+
+    @Override
+    public OutputCapsule getCapsule(Savable object) {
+        if ((object instanceof Material) || (object instanceof MaterialDef)) {
+            return rootCapsule;
+        }
+
+        return rootCapsule.getCapsule(object);
+    }
+
+}

+ 383 - 0
jme3-plugins/src/main/java/com/jme3/material/plugin/export/material/J3MOutputCapsule.java

@@ -0,0 +1,383 @@
+/*
+ * 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.material.plugin.export.material;
+
+import com.jme3.asset.TextureKey;
+import com.jme3.export.OutputCapsule;
+import com.jme3.export.Savable;
+import com.jme3.material.MatParam;
+import com.jme3.material.MatParamTexture;
+import com.jme3.math.*;
+import com.jme3.shader.VarType;
+import com.jme3.texture.Texture;
+import com.jme3.texture.Texture.WrapMode;
+import com.jme3.util.IntMap;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.nio.ByteBuffer;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.nio.ShortBuffer;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ *
+ * @author tsr
+ */
+public class J3MOutputCapsule implements OutputCapsule {
+
+    private final HashMap<String, String> parameters;
+    protected final J3MExporter exporter;
+
+    public J3MOutputCapsule(J3MExporter exporter) {
+        this.exporter = exporter;
+        parameters = new HashMap<>();
+    }
+
+    public void writeToStream(OutputStreamWriter out) throws IOException {
+        for (String key : parameters.keySet()) {
+            out.write("      ");
+            writeParameter(out, key, parameters.get(key));
+            out.write("\n");
+        }
+    }
+
+    protected void writeParameter(OutputStreamWriter out, String name, String value) throws IOException {
+        out.write(name);
+        out.write(" : ");
+        out.write(value);
+    }
+
+    public void clear() {
+        parameters.clear();
+    }
+
+    protected void putParameter(String name, String value) {
+        parameters.put(name, value);
+    }
+
+    @Override
+    public void write(boolean value, String name, boolean defVal) throws IOException {
+        if (value == defVal) {
+            return;
+        }
+
+        putParameter(name, ((value) ? "On" : "Off"));
+    }
+
+    @Override
+    public void writeStringSavableMap(Map<String, ? extends Savable> map, String name, Map<String, ? extends Savable> defVal) throws IOException {
+        for (String key : map.keySet()) {
+            Savable value = map.get(key);
+            if (defVal == null || !defVal.containsKey(key) || !defVal.get(key).equals(value)) {
+                putParameter(key, format(value));
+            }
+        }
+    }
+
+    protected String format(Savable value) {
+        if (value instanceof MatParamTexture) {
+            return formatMatParamTexture((MatParamTexture) value);
+        }
+        if (value instanceof MatParam) {
+            return formatMatParam((MatParam) value);
+        }
+
+        throw new UnsupportedOperationException(value.getClass() + ": Not supported yet.");
+    }
+
+    private String formatMatParam(MatParam param){
+        VarType type = param.getVarType();
+        Object val = param.getValue();
+        switch (type) {
+            case Boolean:
+            case Float:
+            case Int:
+                return val.toString();
+            case Vector2:
+                Vector2f v2 = (Vector2f) val;
+                return v2.getX() + " " + v2.getY();
+            case Vector3:
+                Vector3f v3 = (Vector3f) val;
+                return v3.getX() + " " + v3.getY() + " " + v3.getZ();
+            case Vector4:
+                // can be either ColorRGBA, Vector4f or Quaternion
+                if (val instanceof Vector4f) {
+                    Vector4f v4 = (Vector4f) val;
+                    return v4.getX() + " " + v4.getY() + " "
+                            + v4.getZ() + " " + v4.getW();
+                } else if (val instanceof ColorRGBA) {
+                    ColorRGBA color = (ColorRGBA) val;
+                    return color.getRed() + " " + color.getGreen() + " "
+                            + color.getBlue() + " " + color.getAlpha();
+                } else if (val instanceof Quaternion) {
+                    Quaternion quat = (Quaternion) val;
+                    return quat.getX() + " " + quat.getY() + " "
+                            + quat.getZ() + " " + quat.getW();
+                } else {
+                    throw new UnsupportedOperationException("Unexpected Vector4 type: " + val);
+                }
+
+            default:
+                return null; // parameter type not supported in J3M
+        }
+    }
+
+    protected static String formatMatParamTexture(MatParamTexture param) {
+        StringBuilder ret = new StringBuilder();
+        Texture tex = (Texture) param.getValue();
+        TextureKey key;
+        if (tex != null) {
+            key = (TextureKey) tex.getKey();
+
+            if (key != null && key.isFlipY()) {
+                ret.append("Flip ");
+            }
+
+            ret.append(formatWrapMode(tex, Texture.WrapAxis.S));
+            ret.append(formatWrapMode(tex, Texture.WrapAxis.T));
+            ret.append(formatWrapMode(tex, Texture.WrapAxis.R));
+
+            //Min and Mag filter
+            Texture.MinFilter def = Texture.MinFilter.BilinearNoMipMaps;
+            if (tex.getImage().hasMipmaps() || (key != null && key.isGenerateMips())) {
+                def = Texture.MinFilter.Trilinear;
+            }
+            if (tex.getMinFilter() != def) {
+                ret.append("Min").append(tex.getMinFilter().name()).append(" ");
+            }
+
+            if (tex.getMagFilter() != Texture.MagFilter.Bilinear) {
+                ret.append("Mag").append(tex.getMagFilter().name()).append(" ");
+            }
+
+            ret.append("\"").append(key.getName()).append("\"");
+        }
+
+        return ret.toString();
+    }
+
+    protected static String formatWrapMode(Texture texVal, Texture.WrapAxis axis) {
+        WrapMode mode;
+        try {
+            mode = texVal.getWrap(axis);
+        } catch (IllegalArgumentException e) {
+            //this axis doesn't exist on the texture
+            return "";
+        }
+        if (mode != WrapMode.EdgeClamp) {
+            return "Wrap" + mode.name() + "_" + axis.name() + " ";
+        }
+        return "";
+    }
+
+    @Override
+    public void write(Enum value, String name, Enum defVal) throws IOException {
+        if (value == defVal) {
+            return;
+        }
+
+        putParameter(name, value.toString());
+    }
+
+    @Override
+    public void write(float value, String name, float defVal) throws IOException {
+        if (value == defVal) {
+            return;
+        }
+
+        putParameter(name, Float.toString(value));
+    }
+
+    @Override
+    public void write(float[] value, String name, float[] defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void write(float[][] value, String name, float[][] defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void write(double value, String name, double defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void write(double[] value, String name, double[] defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void write(double[][] value, String name, double[][] defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void write(long value, String name, long defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void write(long[] value, String name, long[] defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void write(long[][] value, String name, long[][] defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void write(short value, String name, short defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void write(short[] value, String name, short[] defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void write(short[][] value, String name, short[][] defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void write(boolean[] value, String name, boolean[] defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void write(boolean[][] value, String name, boolean[][] defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void write(String value, String name, String defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void write(String[] value, String name, String[] defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void write(String[][] value, String name, String[][] defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void write(BitSet value, String name, BitSet defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void write(Savable object, String name, Savable defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void write(Savable[] objects, String name, Savable[] defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void write(Savable[][] objects, String name, Savable[][] defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void writeSavableArrayList(ArrayList array, String name, ArrayList defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void writeSavableArrayListArray(ArrayList[] array, String name, ArrayList[] defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void writeSavableArrayListArray2D(ArrayList[][] array, String name, ArrayList[][] defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void writeFloatBufferArrayList(ArrayList<FloatBuffer> array, String name, ArrayList<FloatBuffer> defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void writeByteBufferArrayList(ArrayList<ByteBuffer> array, String name, ArrayList<ByteBuffer> defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void writeSavableMap(Map<? extends Savable, ? extends Savable> map, String name, Map<? extends Savable, ? extends Savable> defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void writeIntSavableMap(IntMap<? extends Savable> map, String name, IntMap<? extends Savable> defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void write(FloatBuffer value, String name, FloatBuffer defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void write(IntBuffer value, String name, IntBuffer defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void write(ByteBuffer value, String name, ByteBuffer defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void write(ShortBuffer value, String name, ShortBuffer defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void write(byte value, String name, byte defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void write(byte[] value, String name, byte[] defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void write(byte[][] value, String name, byte[][] defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void write(int value, String name, int defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void write(int[] value, String name, int[] defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public void write(int[][] value, String name, int[][] defVal) throws IOException {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+}

+ 81 - 0
jme3-plugins/src/main/java/com/jme3/material/plugin/export/material/J3MRenderStateOutputCapsule.java

@@ -0,0 +1,81 @@
+/*
+ * 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.material.plugin.export.material;
+
+import com.jme3.export.OutputCapsule;
+import com.jme3.export.Savable;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.util.HashMap;
+
+/**
+ *
+ * @author tsr
+ */
+public class J3MRenderStateOutputCapsule extends J3MOutputCapsule {    
+    protected final static HashMap<String, String> NAME_MAP;
+    protected String offsetUnit;
+    
+    static {
+        NAME_MAP = new HashMap<>();
+        NAME_MAP.put( "wireframe", "Wireframe");
+        NAME_MAP.put( "cullMode", "FaceCull");
+        NAME_MAP.put( "depthWrite", "DepthWrite");
+        NAME_MAP.put( "depthTest", "DepthTest");
+        NAME_MAP.put( "blendMode", "Blend");
+        NAME_MAP.put( "alphaFallOff", "AlphaTestFalloff");
+        NAME_MAP.put( "offsetFactor", "PolyOffset");
+        NAME_MAP.put( "colorWrite", "ColorWrite");
+        NAME_MAP.put( "pointSprite", "PointSprite");
+        NAME_MAP.put( "depthFunc", "DepthFunc");
+        NAME_MAP.put( "alphaFunc", "AlphaFunc");
+    }
+    public J3MRenderStateOutputCapsule(J3MExporter exporter) {
+        super(exporter);
+    }
+
+    public OutputCapsule getCapsule(Savable object) {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+    
+    @Override
+    public void clear() {
+        super.clear();
+        offsetUnit = "";
+    }
+
+    @Override
+    public void writeToStream(OutputStreamWriter out) throws IOException {
+        out.write("    AdditionalRenderState {\n");
+        super.writeToStream(out);
+        out.write("    }\n");
+    }  
+    
+    @Override
+    protected void writeParameter(OutputStreamWriter out, String name, String value) throws IOException {
+        out.write(name);
+        out.write(" ");        
+        out.write(value);
+        
+        if( "PolyOffset".equals(name) ) {
+            out.write(" ");
+            out.write(offsetUnit);
+        }        
+    }
+    
+    @Override
+    protected void putParameter(String name, String value ) {
+        if( "offsetUnits".equals(name) ) {
+            offsetUnit = value;
+            return;
+        }
+        
+        if( !NAME_MAP.containsKey(name) )
+            return;
+        
+        super.putParameter(NAME_MAP.get(name), value);
+    }
+}

+ 99 - 0
jme3-plugins/src/main/java/com/jme3/material/plugin/export/material/J3MRootOutputCapsule.java

@@ -0,0 +1,99 @@
+/*
+ * 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.material.plugin.export.material;
+
+import com.jme3.export.OutputCapsule;
+import com.jme3.export.Savable;
+
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.util.HashMap;
+
+/**
+ * @author tsr
+ */
+public class J3MRootOutputCapsule extends J3MOutputCapsule {
+
+    private final HashMap<Savable, J3MOutputCapsule> outCapsules;
+    private String name;
+    private String materialDefinition;
+    private Boolean isTransparent;
+
+    public J3MRootOutputCapsule(J3MExporter exporter) {
+        super(exporter);
+        outCapsules = new HashMap<>();
+    }
+
+    @Override
+    public void clear() {
+        super.clear();
+        isTransparent = null;
+        name = "";
+        materialDefinition = "";
+        outCapsules.clear();
+
+    }
+
+    public OutputCapsule getCapsule(Savable object) {
+        if (!outCapsules.containsKey(object)) {
+            outCapsules.put(object, new J3MRenderStateOutputCapsule(exporter));
+        }
+
+        return outCapsules.get(object);
+    }
+
+    @Override
+    public void writeToStream(OutputStreamWriter out) throws IOException {
+        out.write("Material " + name + " : " + materialDefinition + " {\n\n");
+        if (isTransparent != null)
+            out.write("    Transparent " + ((isTransparent) ? "On" : "Off") + "\n\n");
+
+        out.write("    MaterialParameters {\n");
+        super.writeToStream(out);
+        out.write("    }\n\n");
+
+        for (J3MOutputCapsule c : outCapsules.values()) {
+            c.writeToStream(out);
+        }
+        out.write("}\n");
+    }
+
+    @Override
+    public void write(String value, String name, String defVal) throws IOException {
+        switch (name) {
+            case "material_def":
+                materialDefinition = value;
+                break;
+            case "name":
+                this.name = value;
+                break;
+            default:
+                throw new UnsupportedOperationException(name + " string material parameter not supported yet");
+        }
+    }
+
+    @Override
+    public void write(boolean value, String name, boolean defVal) throws IOException {
+        if( value == defVal)
+            return;
+
+        switch (name) {
+            case "is_transparent":
+                isTransparent = value;
+                break;
+            default:
+                throw new UnsupportedOperationException(name + " boolean material parameter not supported yet");
+        }
+    }
+
+    @Override
+    public void write(Savable object, String name, Savable defVal) throws IOException {
+        if(object != null && !object.equals(defVal)) {
+            object.write(exporter);
+        }
+    }
+
+}

+ 115 - 0
jme3-plugins/src/test/java/com/jme3/material/plugin/TestMaterialWrite.java

@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) 2009-2016 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.material.plugin;
+
+import com.jme3.asset.AssetInfo;
+import com.jme3.asset.AssetKey;
+import com.jme3.asset.AssetManager;
+import com.jme3.material.Material;
+import com.jme3.material.RenderState;
+import com.jme3.material.plugin.export.material.J3MExporter;
+import com.jme3.material.plugins.J3MLoader;
+import com.jme3.math.ColorRGBA;
+import com.jme3.system.JmeSystem;
+import static org.junit.Assert.*;
+
+import com.jme3.texture.Texture;
+import com.jme3.texture.Texture2D;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Scanner;
+
+public class TestMaterialWrite {
+
+    private AssetManager assetManager;
+
+    @Before
+    public void init() {
+        assetManager = JmeSystem.newAssetManager(
+                TestMaterialWrite.class.getResource("/com/jme3/asset/Desktop.cfg"));
+
+
+    }
+
+
+    @Test
+    public void testWriteMat() throws Exception {
+
+        Material mat = new Material(assetManager,"Common/MatDefs/Light/Lighting.j3md");
+
+        mat.setBoolean("UseMaterialColors", true);
+        mat.setColor("Diffuse", ColorRGBA.White);
+        mat.setColor("Ambient", ColorRGBA.DarkGray);
+        mat.setFloat("AlphaDiscardThreshold", 0.5f);
+
+        mat.setFloat("Shininess", 2.5f);
+
+        Texture tex = assetManager.loadTexture("Common/Textures/MissingTexture.png");
+        tex.setMagFilter(Texture.MagFilter.Nearest);
+        tex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
+        tex.setWrap(Texture.WrapAxis.S, Texture.WrapMode.Repeat);
+        tex.setWrap(Texture.WrapAxis.T, Texture.WrapMode.MirroredRepeat);
+
+        mat.setTexture("DiffuseMap", tex);
+        mat.getAdditionalRenderState().setDepthWrite(false);
+        mat.getAdditionalRenderState().setDepthTest(false);
+        mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
+
+        final ByteArrayOutputStream stream = new ByteArrayOutputStream();
+
+        J3MExporter exporter = new J3MExporter();
+        try {
+            exporter.save(mat, stream);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+
+        System.err.println(stream.toString());
+
+        J3MLoader loader = new J3MLoader();
+        AssetInfo info = new AssetInfo(assetManager, new AssetKey("test")) {
+            @Override
+            public InputStream openStream() {
+                return new ByteArrayInputStream(stream.toByteArray());
+            }
+        };
+        Material mat2 = (Material)loader.load(info);
+
+        assertTrue(mat.contentEquals(mat2));
+    }
+
+}

+ 10 - 0
jme3-terrain/src/main/java/com/jme3/terrain/geomipmap/NormalRecalcControl.java

@@ -40,6 +40,8 @@ import com.jme3.renderer.ViewPort;
 import com.jme3.scene.Spatial;
 import com.jme3.scene.control.AbstractControl;
 import com.jme3.scene.control.Control;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 
 
@@ -67,6 +69,14 @@ public class NormalRecalcControl extends AbstractControl {
 
     }
 
+    @Override   
+    public Object jmeClone() {
+        NormalRecalcControl control = (NormalRecalcControl)super.jmeClone();
+        control.setEnabled(true);
+        return control; 
+    }     
+
+    @Override
     public Control cloneForSpatial(Spatial spatial) {
         NormalRecalcControl control = new NormalRecalcControl(terrain);
         control.setSpatial(spatial);

+ 26 - 1
jme3-terrain/src/main/java/com/jme3/terrain/geomipmap/TerrainLodControl.java

@@ -46,6 +46,8 @@ import com.jme3.scene.control.Control;
 import com.jme3.terrain.Terrain;
 import com.jme3.terrain.geomipmap.lodcalc.DistanceLodCalculator;
 import com.jme3.terrain.geomipmap.lodcalc.LodCalculator;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -298,9 +300,32 @@ public class TerrainLodControl extends AbstractControl {
     
     
     
+ 
+    @Override   
+    public Object jmeClone() {
+        if (spatial instanceof Terrain) {
+            TerrainLodControl cloned = new TerrainLodControl((Terrain) spatial, cameras);
+            cloned.setLodCalculator(lodCalculator.clone());
+            cloned.spatial = spatial;
+            return cloned;
+        }
+        return null;
+    }     
+
+    @Override   
+    public void cloneFields( Cloner cloner, Object original ) {
+        this.lodCalculator = cloner.clone(lodCalculator);
+ 
+        try {       
+            // Not deep clone of the cameras themselves
+            this.cameras = cloner.javaClone(cameras);
+        } catch( CloneNotSupportedException e ) {
+            throw new RuntimeException("Error cloning", e);
+        } 
+    }     
     
     
-    
+    @Override
     public Control cloneForSpatial(Spatial spatial) {
         if (spatial instanceof Terrain) {
             List<Camera> cameraClone = new ArrayList<Camera>();

+ 4 - 4
jme3-testdata/src/main/resources/Models/Sign Post/Sign Post.j3m

@@ -1,11 +1,11 @@
 Material Signpost : Common/MatDefs/Light/Lighting.j3md {
     MaterialParameters {
          Shininess: 4.0
-         DiffuseMap:  Models/Sign Post/Sign Post.jpg
-         NormalMap:   Models/Sign Post/Sign Post_normal.jpg
-         SpecularMap: Models/Sign Post/Sign Post_specular.jpg
+         DiffuseMap:  "Models/Sign Post/Sign Post.jpg"
+         NormalMap:   "Models/Sign Post/Sign Post_normal.jpg"
+         SpecularMap: "Models/Sign Post/Sign Post_specular.jpg"
          UseMaterialColors : true
-         Ambient  : 0.5 0.5 0.5 1.0
+         Ambient  : 1.0 1.0 1.0 1.0
          Diffuse  : 1.0 1.0 1.0 1.0
          Specular : 1.0 1.0 1.0 1.0
     }

BIN
jme3-testdata/src/main/resources/Models/Test/CornellBox.j3o


+ 0 - 1
sdk/BasicGameTemplate/MANIFEST.MF

@@ -1 +0,0 @@
-X-Comment: Created with jMonkeyPlatform

+ 0 - 0
sdk/BasicGameTemplate/assets/Interface/.keep


+ 0 - 0
sdk/BasicGameTemplate/assets/MatDefs/.keep


+ 0 - 0
sdk/BasicGameTemplate/assets/Materials/.keep


+ 0 - 0
sdk/BasicGameTemplate/assets/Models/.keep


Неке датотеке нису приказане због велике количине промена