Sfoglia il codice sorgente

Merge branch 'master' into PBRisComing

Nehon 9 anni fa
parent
commit
754c256a66
100 ha cambiato i file con 3956 aggiunte e 1276 eliminazioni
  1. 1 1
      common.gradle
  2. 1 1
      jme3-android/src/main/java/com/jme3/input/android/AndroidTouchInput.java
  3. 10 5
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/Edge.java
  4. 0 11
      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. 2 1
      jme3-core/src/main/java/com/jme3/animation/ClonableTrack.java
  18. 34 0
      jme3-core/src/main/java/com/jme3/animation/EffectTrack.java
  19. 12 1
      jme3-core/src/main/java/com/jme3/animation/Skeleton.java
  20. 27 1
      jme3-core/src/main/java/com/jme3/animation/SkeletonControl.java
  21. 17 1
      jme3-core/src/main/java/com/jme3/animation/TrackInfo.java
  22. 22 9
      jme3-core/src/main/java/com/jme3/app/StatsView.java
  23. 14 12
      jme3-core/src/main/java/com/jme3/app/state/ScreenshotAppState.java
  24. 31 6
      jme3-core/src/main/java/com/jme3/cinematic/events/MotionEvent.java
  25. 18 1
      jme3-core/src/main/java/com/jme3/effect/ParticleEmitter.java
  26. 21 1
      jme3-core/src/main/java/com/jme3/input/ChaseCamera.java
  27. 38 18
      jme3-core/src/main/java/com/jme3/light/LightList.java
  28. 32 3
      jme3-core/src/main/java/com/jme3/material/MatParam.java
  29. 49 9
      jme3-core/src/main/java/com/jme3/material/RenderState.java
  30. 17 13
      jme3-core/src/main/java/com/jme3/math/Spline.java
  31. 4 2
      jme3-core/src/main/java/com/jme3/post/FilterPostProcessor.java
  32. 1 1
      jme3-core/src/main/java/com/jme3/renderer/RenderContext.java
  33. 6 2
      jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java
  34. 15 2
      jme3-core/src/main/java/com/jme3/scene/AssetLinkNode.java
  35. 73 33
      jme3-core/src/main/java/com/jme3/scene/BatchNode.java
  36. 13 1
      jme3-core/src/main/java/com/jme3/scene/CameraNode.java
  37. 63 52
      jme3-core/src/main/java/com/jme3/scene/Geometry.java
  38. 15 3
      jme3-core/src/main/java/com/jme3/scene/LightNode.java
  39. 215 179
      jme3-core/src/main/java/com/jme3/scene/Mesh.java
  40. 79 65
      jme3-core/src/main/java/com/jme3/scene/Node.java
  41. 114 66
      jme3-core/src/main/java/com/jme3/scene/Spatial.java
  42. 17 1
      jme3-core/src/main/java/com/jme3/scene/control/AbstractControl.java
  43. 7 6
      jme3-core/src/main/java/com/jme3/scene/control/BillboardControl.java
  44. 8 7
      jme3-core/src/main/java/com/jme3/scene/control/CameraControl.java
  45. 8 7
      jme3-core/src/main/java/com/jme3/scene/control/LightControl.java
  46. 13 2
      jme3-core/src/main/java/com/jme3/scene/control/LodControl.java
  47. 14 0
      jme3-core/src/main/java/com/jme3/scene/control/UpdateControl.java
  48. 68 57
      jme3-core/src/main/java/com/jme3/scene/instancing/InstancedGeometry.java
  49. 94 41
      jme3-core/src/main/java/com/jme3/scene/instancing/InstancedNode.java
  50. 46 1
      jme3-core/src/main/java/com/jme3/shadow/AbstractShadowFilter.java
  51. 85 20
      jme3-core/src/main/java/com/jme3/shadow/AbstractShadowRenderer.java
  52. 2 0
      jme3-core/src/main/java/com/jme3/shadow/DirectionalLightShadowRenderer.java
  53. 2 0
      jme3-core/src/main/java/com/jme3/system/JmeSystemDelegate.java
  54. 48 11
      jme3-core/src/main/java/com/jme3/util/IntMap.java
  55. 81 0
      jme3-core/src/main/java/com/jme3/util/clone/CloneFunction.java
  56. 348 0
      jme3-core/src/main/java/com/jme3/util/clone/Cloner.java
  57. 58 0
      jme3-core/src/main/java/com/jme3/util/clone/IdentityCloneFunction.java
  58. 99 0
      jme3-core/src/main/java/com/jme3/util/clone/JmeCloneable.java
  59. 70 0
      jme3-core/src/main/java/com/jme3/util/clone/ListCloneFunction.java
  60. 7 10
      jme3-core/src/main/resources/Common/MatDefs/Light/Lighting.j3md
  61. 2 3
      jme3-core/src/main/resources/Common/MatDefs/Light/SPLighting.frag
  62. 2 4
      jme3-core/src/main/resources/Common/MatDefs/Light/SPLighting.vert
  63. 6 2
      jme3-core/src/main/resources/Common/MatDefs/Misc/Unshaded.j3md
  64. 14 4
      jme3-core/src/main/resources/Common/MatDefs/Shadow/PostShadow.frag
  65. 6 2
      jme3-core/src/main/resources/Common/MatDefs/Shadow/PostShadow.j3md
  66. 27 7
      jme3-core/src/main/resources/Common/MatDefs/Shadow/PostShadow.vert
  67. 0 80
      jme3-core/src/main/resources/Common/MatDefs/Shadow/PostShadow15.frag
  68. 0 82
      jme3-core/src/main/resources/Common/MatDefs/Shadow/PostShadow15.vert
  69. 31 2
      jme3-core/src/main/resources/Common/MatDefs/Shadow/PostShadowFilter.frag
  70. 7 3
      jme3-core/src/main/resources/Common/MatDefs/Shadow/PostShadowFilter.j3md
  71. 42 8
      jme3-core/src/main/resources/Common/MatDefs/Shadow/PostShadowFilter15.frag
  72. 153 102
      jme3-core/src/main/resources/Common/ShaderLib/Shadows.glsllib
  73. 0 242
      jme3-core/src/main/resources/Common/ShaderLib/Shadows15.glsllib
  74. 2 1
      jme3-core/src/main/resources/com/jme3/system/version.properties
  75. 21 4
      jme3-core/src/plugins/java/com/jme3/material/plugins/J3MLoader.java
  76. 2 0
      jme3-core/src/test/java/com/jme3/material/plugins/J3MLoaderTest.java
  77. 2 1
      jme3-core/src/test/resources/texture-parameters-newstyle.j3m
  78. 1 1
      jme3-effects/src/main/resources/Common/MatDefs/SSAO/normal.vert
  79. 3 3
      jme3-examples/src/main/java/jme3test/TestChooser.java
  80. 367 0
      jme3-examples/src/main/java/jme3test/app/TestCloner.java
  81. 15 1
      jme3-examples/src/main/java/jme3test/bullet/PhysicsHoverControl.java
  82. 1 1
      jme3-examples/src/main/java/jme3test/bullet/TestHoveringTank.java
  83. 1 1
      jme3-examples/src/main/java/jme3test/collision/TestMousePick.java
  84. 29 10
      jme3-examples/src/main/java/jme3test/light/TestDirectionalLightShadow.java
  85. 22 3
      jme3-examples/src/main/java/jme3test/light/TestPointLightShadows.java
  86. 12 0
      jme3-examples/src/main/java/jme3test/light/TestPssmShadow.java
  87. 1 1
      jme3-examples/src/main/java/jme3test/light/TestSpotLight.java
  88. 1 6
      jme3-examples/src/main/java/jme3test/light/TestSpotLightShadows.java
  89. 1 1
      jme3-examples/src/main/java/jme3test/light/TestSpotLightTerrain.java
  90. 0 1
      jme3-examples/src/main/java/jme3test/light/TestTangentGenBadModels.java
  91. 100 0
      jme3-examples/src/main/java/jme3test/light/TestTangentSpace.java
  92. 6 6
      jme3-examples/src/main/java/jme3test/model/shape/TestDebugShapes.java
  93. 14 0
      jme3-examples/src/main/java/jme3test/network/TestChatClient.java
  94. 20 1
      jme3-examples/src/main/java/jme3test/network/TestChatServer.java
  95. 87 0
      jme3-examples/src/main/java/jme3test/scene/TestLineWidthRenderState.java
  96. 1 0
      jme3-plugins/build.gradle
  97. 78 0
      jme3-plugins/src/main/java/com/jme3/material/plugin/export/material/J3MExporter.java
  98. 383 0
      jme3-plugins/src/main/java/com/jme3/material/plugin/export/material/J3MOutputCapsule.java
  99. 82 0
      jme3-plugins/src/main/java/com/jme3/material/plugin/export/material/J3MRenderStateOutputCapsule.java
  100. 99 0
      jme3-plugins/src/main/java/com/jme3/material/plugin/export/material/J3MRootOutputCapsule.java

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

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

+ 10 - 5
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/Edge.java

@@ -215,8 +215,8 @@ public class Edge {
     
 	/**
 	 * The method computes the crossing pint of this edge and another edge. If
-	 * there is no crossing then null is returned. This method also allows to
-	 * get the crossing point of the straight lines that contain these edges if
+	 * there is no crossing then null is returned. Also null is returned if the edges are parallel.
+	 * This method also allows to get the crossing point of the straight lines that contain these edges if
 	 * you set the 'extend' parameter to true.
 	 * 
 	 * @param edge
@@ -227,7 +227,7 @@ public class Edge {
 	 * @param extendSecondEdge
 	 *            set to <b>true</b> to find a crossing point along the whole
 	 *            straight that contains the given edge
-	 * @return cross point on null if none exist
+	 * @return cross point on null if none exist or the edges are parallel
 	 */
 	public Vector3f getCrossPoint(Edge edge, boolean extendThisEdge, boolean extendSecondEdge) {
 		Vector3d P1 = new Vector3d(this.getFirstVertex());
@@ -235,6 +235,11 @@ public class Edge {
 		Vector3d u = new Vector3d(this.getSecondVertex()).subtract(P1).normalizeLocal();
 		Vector3d v = new Vector3d(edge.getSecondVertex()).subtract(P2).normalizeLocal();
 		
+		if(Math.abs(u.dot(v)) >= 1 - FastMath.DBL_EPSILON) {
+			// the edges are parallel; do not care about the crossing point
+			return null;
+		}
+		
 		double t1 = 0, t2 = 0;
 		if(u.x == 0 && v.x == 0) {
 			t2 = (u.z * (P2.y - P1.y) - u.y * (P2.z - P1.z)) / (u.y * v.z - u.z * v.y);
@@ -262,11 +267,11 @@ public class Edge {
 			// the lines cross, check if p1 and p2 are within the edges
             Vector3d p = p1.subtract(P1);
             double cos = p.dot(u) / p.length();
-            if (extendThisEdge || p.length()<= FastMath.FLT_EPSILON || cos >= 1 - FastMath.FLT_EPSILON && p.length() <= this.getLength()) {
+            if (extendThisEdge || p.length()<= FastMath.FLT_EPSILON || cos >= 1 - FastMath.FLT_EPSILON && p.length() - this.getLength() <= FastMath.FLT_EPSILON) {
                 // p1 is inside the first edge, lets check the other edge now
                 p = p2.subtract(P2);
                 cos = p.dot(v) / p.length();
-                if(extendSecondEdge || p.length()<= FastMath.FLT_EPSILON || cos >= 1 - FastMath.FLT_EPSILON && p.length() <= edge.getLength()) {
+                if(extendSecondEdge || p.length()<= FastMath.FLT_EPSILON || cos >= 1 - FastMath.FLT_EPSILON && p.length() - edge.getLength() <= FastMath.FLT_EPSILON) {
                 	return p1.toVector3f();
                 }
             }

+ 0 - 11
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());
@@ -279,16 +278,6 @@ public class Face implements Comparator<Integer> {
                 // two special cases will improve the computations speed
                 if(face.getIndexes().size() == 3) {
                 	triangulatedFaces.add(face.getIndexes().clone());
-                } else if(face.getIndexes().size() == 4) {
-                	// in case face has 4 verts we use the plain triangulation
-                	indexes[0] = face.getIndex(0);
-                    indexes[1] = face.getIndex(1);
-                    indexes[2] = face.getIndex(2);
-                	triangulatedFaces.add(new IndexesLoop(indexes));
-                	
-                    indexes[1] = face.getIndex(2);
-                    indexes[2] = face.getIndex(3);
-                	triangulatedFaces.add(new IndexesLoop(indexes));
                 } else {
                 	int previousIndex1 = -1, previousIndex2 = -1, previousIndex3 = -1;
                     while (face.vertexCount() > 0) {

+ 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

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

+ 22 - 9
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;
@@ -67,7 +69,7 @@ public class StatsView extends Node implements Control {
     private int[] statData;
 
     private boolean enabled = true;
-    
+
     private final StringBuilder stringBuilder = new StringBuilder();
 
     public StatsView(String name, AssetManager manager, Statistics stats){
@@ -93,32 +95,43 @@ public class StatsView extends Node implements Control {
     public float getHeight() {
         return statText.getLineHeight() * statLabels.length;
     }
-    
+
     public void update(float tpf) {
-    
-        if (!isEnabled()) 
+
+        if (!isEnabled())
             return;
-            
+
         statistics.getData(statData);
         stringBuilder.setLength(0);
-        
-        // Need to walk through it backwards, as the first label 
+
+        // Need to walk through it backwards, as the first label
         // should appear at the bottom, not the top.
         for (int i = statLabels.length - 1; i >= 0; i--) {
             stringBuilder.append(statLabels[i]).append(" = ").append(statData[i]).append('\n');
         }
         statText.setText(stringBuilder);
-        
+
         // Moved to ResetStatsState to make sure it is
         // done even if there is no StatsView or the StatsView
         // is disable.
         //statistics.clearFrame();
     }
 
+    @Override
     public Control cloneForSpatial(Spatial spatial) {
         return (Control) spatial;
     }
 
+    @Override
+    public StatsView 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();
+        }
+    } 
 }

+ 31 - 6
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;
@@ -118,7 +120,6 @@ public class MotionEvent extends AbstractCinematicEvent implements Control {
      */
     public MotionEvent(Spatial spatial, MotionPath path) {
         super();
-        this.spatial = spatial;
         spatial.addControl(this);
         this.path = path;
     }
@@ -130,7 +131,6 @@ public class MotionEvent extends AbstractCinematicEvent implements Control {
      */
     public MotionEvent(Spatial spatial, MotionPath path, float initialDuration) {
         super(initialDuration);
-        this.spatial = spatial;
         spatial.addControl(this);
         this.path = path;
     }
@@ -142,7 +142,6 @@ public class MotionEvent extends AbstractCinematicEvent implements Control {
      */
     public MotionEvent(Spatial spatial, MotionPath path, LoopMode loopMode) {
         super();
-        this.spatial = spatial;
         spatial.addControl(this);
         this.path = path;
         this.loopMode = loopMode;
@@ -155,7 +154,6 @@ public class MotionEvent extends AbstractCinematicEvent implements Control {
      */
     public MotionEvent(Spatial spatial, MotionPath path, float initialDuration, LoopMode loopMode) {
         super(initialDuration);
-        this.spatial = spatial;
         spatial.addControl(this);
         this.path = path;
         this.loopMode = loopMode;
@@ -274,8 +272,10 @@ public class MotionEvent extends AbstractCinematicEvent implements Control {
      * @param spatial
      * @return
      */
+    @Override
     public Control cloneForSpatial(Spatial spatial) {
-        MotionEvent control = new MotionEvent(spatial, path);
+        MotionEvent control = new MotionEvent();
+        control.setPath(path);
         control.playState = playState;
         control.currentWayPoint = currentWayPoint;
         control.currentValue = currentValue;
@@ -291,6 +291,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

+ 38 - 18
jme3-core/src/main/java/com/jme3/light/LightList.java

@@ -33,6 +33,8 @@ package com.jme3.light;
 
 import com.jme3.export.*;
 import com.jme3.scene.Spatial;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import com.jme3.util.SortUtil;
 import java.io.IOException;
 import java.util.*;
@@ -40,10 +42,10 @@ import java.util.*;
 /**
  * <code>LightList</code> is used internally by {@link Spatial}s to manage
  * lights that are attached to them.
- * 
+ *
  * @author Kirill Vainer
  */
-public final class LightList implements Iterable<Light>, Savable, Cloneable {
+public final class LightList implements Iterable<Light>, Savable, Cloneable, JmeCloneable {
 
     private Light[] list, tlist;
     private float[] distToOwner;
@@ -74,7 +76,7 @@ public final class LightList implements Iterable<Light>, Savable, Cloneable {
 
     /**
      * Creates a <code>LightList</code> for the given {@link Spatial}.
-     * 
+     *
      * @param owner The spatial owner
      */
     public LightList(Spatial owner) {
@@ -87,7 +89,7 @@ public final class LightList implements Iterable<Light>, Savable, Cloneable {
 
     /**
      * Set the owner of the LightList. Only used for cloning.
-     * @param owner 
+     * @param owner
      */
     public void setOwner(Spatial owner){
         this.owner = owner;
@@ -118,7 +120,7 @@ public final class LightList implements Iterable<Light>, Savable, Cloneable {
 
     /**
      * Remove the light at the given index.
-     * 
+     *
      * @param index
      */
     public void remove(int index){
@@ -139,7 +141,7 @@ public final class LightList implements Iterable<Light>, Savable, Cloneable {
 
     /**
      * Removes the given light from the LightList.
-     * 
+     *
      * @param l the light to remove
      */
     public void remove(Light l){
@@ -187,12 +189,12 @@ public final class LightList implements Iterable<Light>, Savable, Cloneable {
 
     /**
      * Sorts the elements in the list according to their Comparator.
-     * There are two reasons why lights should be resorted. 
-     * First, if the lights have moved, that means their distance to 
-     * the spatial changed. 
-     * Second, if the spatial itself moved, it means the distance from it to 
+     * There are two reasons why lights should be resorted.
+     * First, if the lights have moved, that means their distance to
+     * the spatial changed.
+     * Second, if the spatial itself moved, it means the distance from it to
      * the individual lights might have changed.
-     * 
+     *
      *
      * @param transformChanged Whether the spatial's transform has changed
      */
@@ -252,7 +254,7 @@ public final class LightList implements Iterable<Light>, Savable, Cloneable {
                 list[p] = parent.list[i];
                 distToOwner[p] = Float.NEGATIVE_INFINITY;
             }
-            
+
             listSize = local.listSize + parent.listSize;
         }else{
             listSize = local.listSize;
@@ -261,7 +263,7 @@ public final class LightList implements Iterable<Light>, Savable, Cloneable {
 
     /**
      * Returns an iterator that can be used to iterate over this LightList.
-     * 
+     *
      * @return an iterator that can be used to iterate over this LightList.
      */
     public Iterator<Light> iterator() {
@@ -276,10 +278,10 @@ public final class LightList implements Iterable<Light>, Savable, Cloneable {
             public Light next() {
                 if (!hasNext())
                     throw new NoSuchElementException();
-                
+
                 return list[index++];
             }
-            
+
             public void remove() {
                 LightList.this.remove(--index);
             }
@@ -290,7 +292,7 @@ public final class LightList implements Iterable<Light>, Savable, Cloneable {
     public LightList clone(){
         try{
             LightList clone = (LightList) super.clone();
-            
+
             clone.owner = null;
             clone.list = list.clone();
             clone.distToOwner = distToOwner.clone();
@@ -302,6 +304,24 @@ public final class LightList implements Iterable<Light>, Savable, Cloneable {
         }
     }
 
+    @Override
+    public LightList jmeClone() {
+        try{
+            LightList clone = (LightList)super.clone();
+            clone.tlist = null; // list used for sorting only
+            return clone;
+        }catch (CloneNotSupportedException ex){
+            throw new AssertionError();
+        }
+    }
+
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) {
+        this.owner = cloner.clone(owner);
+        this.list = cloner.clone(list);
+        this.distToOwner = cloner.clone(distToOwner);
+    }
+
     public void write(JmeExporter ex) throws IOException {
         OutputCapsule oc = ex.getCapsule(this);
 //        oc.write(owner, "owner", null);
@@ -319,7 +339,7 @@ public final class LightList implements Iterable<Light>, Savable, Cloneable {
 
         List<Light> lights = ic.readSavableArrayList("lights", null);
         listSize = lights.size();
-        
+
         // NOTE: make sure the array has a length of at least 1
         int arraySize = Math.max(DEFAULT_SIZE, listSize);
         list = new Light[arraySize];
@@ -328,7 +348,7 @@ public final class LightList implements Iterable<Light>, Savable, Cloneable {
         for (int i = 0; i < listSize; i++){
             list[i] = lights.get(i);
         }
-        
+
         Arrays.fill(distToOwner, Float.NEGATIVE_INFINITY);
     }
 

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

+ 49 - 9
jme3-core/src/main/java/com/jme3/material/RenderState.java

@@ -311,6 +311,8 @@ public class RenderState implements Cloneable, Savable {
     boolean applyPolyOffset = true;
     boolean stencilTest = false;
     boolean applyStencilTest = false;
+    float lineWidth = 1;
+    boolean applyLineWidth = false;
     TestFunction depthFunc = TestFunction.LessOrEqual;
     //by default depth func will be applied anyway if depth test is applied
     boolean applyDepthFunc = false;    
@@ -350,6 +352,9 @@ public class RenderState implements Cloneable, Savable {
         oc.write(backStencilDepthPassOperation, "backStencilDepthPassOperation", StencilOperation.Keep);
         oc.write(frontStencilFunction, "frontStencilFunction", TestFunction.Always);
         oc.write(backStencilFunction, "backStencilFunction", TestFunction.Always);
+        oc.write(depthFunc, "depthFunc", TestFunction.LessOrEqual);
+        oc.write(alphaFunc, "alphaFunc", TestFunction.Greater);
+        oc.write(lineWidth, "lineWidth", 1);
 
         // Only "additional render state" has them set to false by default
         oc.write(applyPointSprite, "applyPointSprite", true);
@@ -364,8 +369,7 @@ public class RenderState implements Cloneable, Savable {
         oc.write(applyPolyOffset, "applyPolyOffset", true);
         oc.write(applyDepthFunc, "applyDepthFunc", true);
         oc.write(applyAlphaFunc, "applyAlphaFunc", false);
-        oc.write(depthFunc, "depthFunc", TestFunction.LessOrEqual);
-        oc.write(alphaFunc, "alphaFunc", TestFunction.Greater);  
+        oc.write(applyLineWidth, "applyLineWidth", true);
 
     }
 
@@ -394,6 +398,8 @@ public class RenderState implements Cloneable, Savable {
         backStencilFunction = ic.readEnum("backStencilFunction", TestFunction.class, TestFunction.Always);
         depthFunc = ic.readEnum("depthFunc", TestFunction.class, TestFunction.LessOrEqual);
         alphaFunc = ic.readEnum("alphaFunc", TestFunction.class, TestFunction.Greater);
+        lineWidth = ic.readFloat("lineWidth", 1);
+
 
         applyPointSprite = ic.readBoolean("applyPointSprite", true);
         applyWireFrame = ic.readBoolean("applyWireFrame", true);
@@ -407,6 +413,8 @@ public class RenderState implements Cloneable, Savable {
         applyPolyOffset = ic.readBoolean("applyPolyOffset", true);
         applyDepthFunc = ic.readBoolean("applyDepthFunc", true);
         applyAlphaFunc = ic.readBoolean("applyAlphaFunc", false);
+        applyLineWidth = ic.readBoolean("applyLineWidth", true);
+
         
     }
 
@@ -528,6 +536,10 @@ public class RenderState implements Cloneable, Savable {
             }
         }
 
+        if(lineWidth != rs.lineWidth){
+            return false;
+        }
+
         return true;
     }
 
@@ -803,8 +815,17 @@ public class RenderState implements Cloneable, Savable {
         this.alphaFunc = alphaFunc;
         cachedHashCode = -1;
     }
-    
-    
+
+    /**
+     * Sets the mesh line width.
+     * This is to use in conjunction with {@link #setWireframe(boolean)} or with a mesh in {@link Mesh.Mode#Lines} mode.
+     * @param lineWidth the line width.
+     */
+    public void setLineWidth(float lineWidth) {
+        this.lineWidth = lineWidth;
+        this.applyLineWidth = true;
+        cachedHashCode = -1;
+    }
 
     /**
      * Check if stencil test is enabled.
@@ -1118,8 +1139,16 @@ public class RenderState implements Cloneable, Savable {
     public TestFunction getAlphaFunc() {
         return alphaFunc;
     }
-    
-    
+
+    /**
+     * returns the wireframe line width
+     *
+     * @return the line width
+     */
+    public float getLineWidth() {
+        return lineWidth;
+    }
+
 
     public boolean isApplyAlphaFallOff() {
         return applyAlphaFallOff;
@@ -1168,8 +1197,10 @@ public class RenderState implements Cloneable, Savable {
     public boolean isApplyAlphaFunc() {
         return applyAlphaFunc;
     }
-    
-    
+
+    public boolean isApplyLineWidth() {
+        return applyLineWidth;
+    }
 
     /**
      *
@@ -1200,6 +1231,7 @@ public class RenderState implements Cloneable, Savable {
             hash = 79 * hash + (this.backStencilDepthPassOperation != null ? this.backStencilDepthPassOperation.hashCode() : 0);
             hash = 79 * hash + (this.frontStencilFunction != null ? this.frontStencilFunction.hashCode() : 0);
             hash = 79 * hash + (this.backStencilFunction != null ? this.backStencilFunction.hashCode() : 0);
+            hash = 79 * hash + Float.floatToIntBits(this.lineWidth);
             cachedHashCode = hash;
         }
         return cachedHashCode;
@@ -1324,6 +1356,11 @@ public class RenderState implements Cloneable, Savable {
             state.frontStencilFunction = frontStencilFunction;
             state.backStencilFunction = backStencilFunction;
         }
+        if (additionalState.applyLineWidth) {
+            state.lineWidth = additionalState.lineWidth;
+        } else {
+            state.lineWidth = lineWidth;
+        }
         state.cachedHashCode = -1;
         return state;
     }
@@ -1351,6 +1388,7 @@ public class RenderState implements Cloneable, Savable {
         backStencilFunction = state.backStencilFunction;
         depthFunc = state.depthFunc;
         alphaFunc = state.alphaFunc;
+        lineWidth = state.lineWidth;
 
         applyPointSprite = true;
         applyWireFrame =  true;
@@ -1364,6 +1402,7 @@ public class RenderState implements Cloneable, Savable {
         applyPolyOffset =  true;
         applyDepthFunc =  true;
         applyAlphaFunc =  false;
+        applyLineWidth = true;
     }
 
     @Override
@@ -1392,7 +1431,8 @@ public class RenderState implements Cloneable, Savable {
                 + "\noffsetEnabled=" + offsetEnabled
                 + "\napplyPolyOffset=" + applyPolyOffset
                 + "\noffsetFactor=" + offsetFactor
-                + "\noffsetUnits=" + offsetUnits      
+                + "\noffsetUnits=" + offsetUnits
+                + "\nlineWidth=" + lineWidth
                 + "\n]";
     }
 }

+ 17 - 13
jme3-core/src/main/java/com/jme3/math/Spline.java

@@ -90,7 +90,7 @@ public class Spline implements Savable {
         type = splineType;
         this.curveTension = curveTension;
         this.cycle = cycle;
-        this.computeTotalLentgh();
+        this.computeTotalLength();
     }
 
     /**
@@ -116,7 +116,7 @@ public class Spline implements Savable {
         this.controlPoints.addAll(controlPoints);
         this.curveTension = curveTension;
         this.cycle = cycle;
-        this.computeTotalLentgh();
+        this.computeTotalLength();
     }
     
     /**
@@ -144,7 +144,7 @@ public class Spline implements Savable {
         	this.weights[i] = controlPoint.w;
         }
         CurveAndSurfaceMath.prepareNurbsKnots(knots, basisFunctionDegree);
-        this.computeTotalLentgh();
+        this.computeTotalLength();
     }
 
     private void initCatmullRomWayPoints(List<Vector3f> list) {
@@ -186,7 +186,7 @@ public class Spline implements Savable {
             controlPoints.add(controlPoints.get(0).clone());
         }
         if (controlPoints.size() > 1) {
-            this.computeTotalLentgh();
+            this.computeTotalLength();
         }
     }
 
@@ -197,7 +197,7 @@ public class Spline implements Savable {
     public void removeControlPoint(Vector3f controlPoint) {
         controlPoints.remove(controlPoint);
         if (controlPoints.size() > 1) {
-            this.computeTotalLentgh();
+            this.computeTotalLength();
         }
     }
     
@@ -209,7 +209,7 @@ public class Spline implements Savable {
     /**
      * This method computes the total length of the curve.
      */
-    private void computeTotalLentgh() {
+    private void computeTotalLength() {
         totalLength = 0;
         float l = 0;
         if (segmentsLength == null) {
@@ -317,7 +317,7 @@ public class Spline implements Savable {
     public void setCurveTension(float curveTension) {
         this.curveTension = curveTension;
         if(type==SplineType.CatmullRom && !getControlPoints().isEmpty()) {            
-        	this.computeTotalLentgh();
+        	this.computeTotalLength();
         }
     }
 
@@ -342,7 +342,7 @@ public class Spline implements Savable {
     				controlPoints.add(controlPoints.get(0));
     			}
     			this.cycle = cycle;
-    			this.computeTotalLentgh();
+    			this.computeTotalLength();
     		} else {
     			this.cycle = cycle;
     		}
@@ -369,7 +369,7 @@ public class Spline implements Savable {
      */
     public void setType(SplineType type) {
         this.type = type;
-        this.computeTotalLentgh();
+        this.computeTotalLength();
     }
 
     /**
@@ -435,9 +435,13 @@ public class Spline implements Savable {
         OutputCapsule oc = ex.getCapsule(this);
         oc.writeSavableArrayList((ArrayList) controlPoints, "controlPoints", null);
         oc.write(type, "type", SplineType.CatmullRom);
-        float list[] = new float[segmentsLength.size()];
-        for (int i = 0; i < segmentsLength.size(); i++) {
-            list[i] = segmentsLength.get(i);
+        
+        float list[] = null;
+        if (segmentsLength != null) {
+            list = new float[segmentsLength.size()];
+            for (int i = 0; i < segmentsLength.size(); i++) {
+                list[i] = segmentsLength.get(i);
+            }
         }
         oc.write(list, "segmentsLength", null);
 
@@ -454,7 +458,7 @@ public class Spline implements Savable {
     public void read(JmeImporter im) throws IOException {
         InputCapsule in = im.getCapsule(this);
 
-        controlPoints = (ArrayList<Vector3f>) in.readSavableArrayList("wayPoints", null);
+        controlPoints = (ArrayList<Vector3f>) in.readSavableArrayList("controlPoints", new ArrayList<Vector3f>()); /* Empty List as default, prevents null pointers */
         float list[] = in.readFloatArray("segmentsLength", null);
         if (list != null) {
             segmentsLength = new ArrayList<Float>();

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

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

@@ -101,7 +101,7 @@ public class RenderContext {
     public float pointSize = 1;
     
     /**
-     * @see Mesh#setLineWidth(float) 
+     * @see RenderState#setLineWidth(float)
      */
     public float lineWidth = 1;
 

+ 6 - 2
jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java

@@ -779,6 +779,10 @@ public final class GLRenderer implements Renderer {
                 gl.glDisable(GL.GL_STENCIL_TEST);
             }
         }
+        if (context.lineWidth != state.getLineWidth()) {
+            gl.glLineWidth(state.getLineWidth());
+            context.lineWidth = state.getLineWidth();
+        }
     }
 
     private int convertStencilOperation(StencilOperation stencilOp) {
@@ -2681,8 +2685,8 @@ public final class GLRenderer implements Renderer {
             return;
         }
 
-
-        if (context.lineWidth != mesh.getLineWidth()) {
+        //this is kept for backward compatibility.
+        if (mesh.getLineWidth() != -1 && context.lineWidth != mesh.getLineWidth()) {
             gl.glLineWidth(mesh.getLineWidth());
             context.lineWidth = mesh.getLineWidth();
         }

+ 15 - 2
jme3-core/src/main/java/com/jme3/scene/AssetLinkNode.java

@@ -39,6 +39,7 @@ import com.jme3.export.JmeExporter;
 import com.jme3.export.JmeImporter;
 import com.jme3.export.OutputCapsule;
 import com.jme3.export.binary.BinaryImporter;
+import com.jme3.util.clone.Cloner;
 import com.jme3.util.SafeArrayList;
 import java.io.IOException;
 import java.util.*;
@@ -50,7 +51,7 @@ import java.util.logging.Logger;
  * The AssetLinkNode does not store its children when exported to file.
  * Instead, you can add a list of AssetKeys that will be loaded and attached
  * when the AssetLinkNode is restored.
- * 
+ *
  * @author normenhansen
  */
 public class AssetLinkNode extends Node {
@@ -70,6 +71,18 @@ public class AssetLinkNode extends Node {
         assetLoaderKeys.add(key);
     }
 
+    /**
+     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
+     */
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) {
+        // This is a change in behavior because the old version did not clone
+        // this list... changes to one clone would be reflected in all.
+        // I think that's probably undesirable. -pspeed
+        this.assetLoaderKeys = cloner.clone(assetLoaderKeys);
+        this.assetChildren = new HashMap<ModelKey, Spatial>();
+    }
+
     /**
      * Add a "linked" child. These are loaded from the assetManager when the
      * AssetLinkNode is loaded from a binary file.
@@ -166,7 +179,7 @@ public class AssetLinkNode extends Node {
                 children.add(child);
                 assetChildren.put(modelKey, child);
             } else {
-                Logger.getLogger(this.getClass().getName()).log(Level.WARNING, "Cannot locate {0} for asset link node {1}", 
+                Logger.getLogger(this.getClass().getName()).log(Level.WARNING, "Cannot locate {0} for asset link node {1}",
                                                                     new Object[]{ modelKey, key });
             }
         }

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

@@ -48,6 +48,8 @@ import com.jme3.math.Vector3f;
 import com.jme3.scene.mesh.IndexBuffer;
 import com.jme3.util.SafeArrayList;
 import com.jme3.util.TempVars;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 
 /**
  * BatchNode holds geometries that are a batched version of all the geometries that are in its sub scenegraph.
@@ -60,7 +62,7 @@ import com.jme3.util.TempVars;
  * 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.
  * To integrate them in the batch you have to call the batch() method again on the batchNode.
- * 
+ *
  * TODO normal or tangents or both looks a bit weird
  * TODO more automagic (batch when needed in the updateLogicalState)
  * @author Nehon
@@ -77,7 +79,7 @@ public class BatchNode extends GeometryGroupNode {
      */
     protected Map<Geometry, Batch> batchesByGeom = new HashMap<Geometry, Batch>();
     /**
-     * used to store transformed vectors before proceeding to a bulk put into the FloatBuffer 
+     * used to store transformed vectors before proceeding to a bulk put into the FloatBuffer
      */
     private float[] tmpFloat;
     private float[] tmpFloatN;
@@ -96,7 +98,7 @@ public class BatchNode extends GeometryGroupNode {
     public BatchNode(String name) {
         super(name);
     }
-    
+
     @Override
     public void onTransformChange(Geometry geom) {
         updateSubBatch(geom);
@@ -123,7 +125,7 @@ public class BatchNode extends GeometryGroupNode {
     protected Matrix4f getTransformMatrix(Geometry g){
         return g.cachedWorldMat;
     }
-    
+
     protected void updateSubBatch(Geometry bg) {
         Batch batch = batchesByGeom.get(bg);
         if (batch != null) {
@@ -134,13 +136,13 @@ public class BatchNode extends GeometryGroupNode {
             FloatBuffer posBuf = (FloatBuffer) pvb.getData();
             VertexBuffer nvb = mesh.getBuffer(VertexBuffer.Type.Normal);
             FloatBuffer normBuf = (FloatBuffer) nvb.getData();
-          
+
             VertexBuffer opvb = origMesh.getBuffer(VertexBuffer.Type.Position);
             FloatBuffer oposBuf = (FloatBuffer) opvb.getData();
             VertexBuffer onvb = origMesh.getBuffer(VertexBuffer.Type.Normal);
             FloatBuffer onormBuf = (FloatBuffer) onvb.getData();
             Matrix4f transformMat = getTransformMatrix(bg);
-            
+
             if (mesh.getBuffer(VertexBuffer.Type.Tangent) != null) {
 
                 VertexBuffer tvb = mesh.getBuffer(VertexBuffer.Type.Tangent);
@@ -184,12 +186,12 @@ public class BatchNode extends GeometryGroupNode {
             }
             batches.clear();
             batchesByGeom.clear();
-        }        
+        }
         //only reset maxVertCount if there is something new to batch
         if (matMap.size() > 0) {
             maxVertCount = 0;
         }
-        
+
         for (Map.Entry<Material, List<Geometry>> entry : matMap.entrySet()) {
             Mesh m = new Mesh();
             Material material = entry.getKey();
@@ -255,7 +257,7 @@ public class BatchNode extends GeometryGroupNode {
 
     /**
      * recursively visit the subgraph and unbatch geometries
-     * @param s 
+     * @param s
      */
     private void unbatchSubGraph(Spatial s) {
         if (s instanceof Node) {
@@ -269,8 +271,8 @@ public class BatchNode extends GeometryGroupNode {
             }
         }
     }
-    
-    
+
+
     private void gatherGeometries(Map<Material, List<Geometry>> map, Spatial n, boolean rebatch) {
 
         if (n instanceof Geometry) {
@@ -283,7 +285,7 @@ public class BatchNode extends GeometryGroupNode {
                     }
                     List<Geometry> list = map.get(g.getMaterial());
                     if (list == null) {
-                        //trying to compare materials with the isEqual method 
+                        //trying to compare materials with the isEqual method
                         for (Map.Entry<Material, List<Geometry>> mat : map.entrySet()) {
                             if (g.getMaterial().contentEquals(mat.getKey())) {
                                 list = mat.getValue();
@@ -331,7 +333,7 @@ public class BatchNode extends GeometryGroupNode {
     /**
      * Sets the material to the all the batches of this BatchNode
      * use setMaterial(Material material,int batchIndex) to set a material to a specific batch
-     * 
+     *
      * @param material the material to use for this geometry
      */
     @Override
@@ -341,12 +343,12 @@ public class BatchNode extends GeometryGroupNode {
 
     /**
      * 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) 
+     *
+     * @see #setMaterial(com.jme3.material.Material)
      */
     public Material getMaterial() {
         if (!batches.isEmpty()) {
@@ -359,7 +361,7 @@ public class BatchNode extends GeometryGroupNode {
     /**
      * Merges all geometries in the collection into
      * the output mesh. Does not take into account materials.
-     * 
+     *
      * @param geometries
      * @param outMesh
      */
@@ -383,7 +385,7 @@ public class BatchNode extends GeometryGroupNode {
                 maxVertCount = geom.getVertexCount();
             }
             Mesh.Mode listMode;
-            float listLineWidth = 1f;
+            //float listLineWidth = 1f;
             int components;
             switch (geom.getMesh().getMode()) {
                 case Points:
@@ -394,7 +396,7 @@ public class BatchNode extends GeometryGroupNode {
                 case LineStrip:
                 case Lines:
                     listMode = Mesh.Mode.Lines;
-                    listLineWidth = geom.getMesh().getLineWidth();
+                    //listLineWidth = geom.getMesh().getLineWidth();
                     components = 2;
                     break;
                 case TriangleFan:
@@ -418,7 +420,7 @@ public class BatchNode extends GeometryGroupNode {
                 formatForBuf[vb.getBufferType().ordinal()] = vb.getFormat();
                 normForBuf[vb.getBufferType().ordinal()] = vb.isNormalized();
             }
-            
+
             maxWeights = Math.max(maxWeights, geom.getMesh().getMaxNumWeights());
 
             if (mode != null && mode != listMode) {
@@ -426,19 +428,20 @@ public class BatchNode extends GeometryGroupNode {
                         + " primitive types: " + mode + " != " + listMode);
             }
             mode = listMode;
-            if (mode == Mesh.Mode.Lines) {
-                if (lineWidth != 1f && listLineWidth != lineWidth) {
-                    throw new UnsupportedOperationException("When using Mesh Line mode, cannot combine meshes with different line width "
-                            + lineWidth + " != " + listLineWidth);
-                }
-                lineWidth = listLineWidth;
-            }
+            //Not needed anymore as lineWidth is now in RenderState and will be taken into account when merging according to the material
+//            if (mode == Mesh.Mode.Lines) {
+//                if (lineWidth != 1f && listLineWidth != lineWidth) {
+//                    throw new UnsupportedOperationException("When using Mesh Line mode, cannot combine meshes with different line width "
+//                            + lineWidth + " != " + listLineWidth);
+//                }
+//                lineWidth = listLineWidth;
+//            }
             compsForBuf[VertexBuffer.Type.Index.ordinal()] = components;
         }
 
         outMesh.setMaxNumWeights(maxWeights);
         outMesh.setMode(mode);
-        outMesh.setLineWidth(lineWidth);
+        //outMesh.setLineWidth(lineWidth);
         if (totalVerts >= 65536) {
             // make sure we create an UnsignedInt buffer so we can fit all of the meshes
             formatForBuf[VertexBuffer.Type.Index.ordinal()] = VertexBuffer.Format.UnsignedInt;
@@ -585,7 +588,7 @@ public class BatchNode extends GeometryGroupNode {
         int offset = start * 3;
         int tanOffset = start * 4;
 
-        
+
         bindBufPos.rewind();
         bindBufNorm.rewind();
         bindBufTangents.rewind();
@@ -661,10 +664,10 @@ public class BatchNode extends GeometryGroupNode {
         vars.release();
     }
 
-    protected class Batch {
+    protected class Batch implements JmeCloneable {
         /**
          * update the batchesByGeom map for this batch with the given List of geometries
-         * @param list 
+         * @param list
          */
         void updateGeomList(List<Geometry> list) {
             for (Geometry geom : list) {
@@ -674,6 +677,25 @@ public class BatchNode extends GeometryGroupNode {
             }
         }
         Geometry geometry;
+
+        public final Geometry getGeometry() {
+            return geometry;
+        }
+
+        @Override
+        public Batch jmeClone() {
+            try {
+                return (Batch)super.clone();
+            } catch (CloneNotSupportedException ex) {
+                throw new AssertionError();
+            }
+        }
+
+        @Override
+        public void cloneFields( Cloner cloner, Object original ) {
+            this.geometry = cloner.clone(geometry);
+        }
+
     }
 
     protected void setNeedsFullRebatch(boolean needsFullRebatch) {
@@ -699,7 +721,25 @@ public class BatchNode extends GeometryGroupNode {
         }
         return clone;
     }
-    
+
+    /**
+     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
+     */
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) {
+        this.batches = cloner.clone(batches);
+        this.tmpFloat = cloner.clone(tmpFloat);
+        this.tmpFloatN = cloner.clone(tmpFloatN);
+        this.tmpFloatT = cloner.clone(tmpFloatT);
+
+
+        HashMap<Geometry, Batch> newBatchesByGeom = new HashMap<Geometry, Batch>();
+        for( Map.Entry<Geometry, Batch> e : batchesByGeom.entrySet() ) {
+            newBatchesByGeom.put(cloner.clone(e.getKey()), cloner.clone(e.getValue()));
+        }
+        this.batchesByGeom = newBatchesByGeom;
+    }
+
     @Override
     public int collideWith(Collidable other, CollisionResults results) {
         int total = 0;

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

@@ -36,6 +36,7 @@ import com.jme3.export.JmeImporter;
 import com.jme3.renderer.Camera;
 import com.jme3.scene.control.CameraControl;
 import com.jme3.scene.control.CameraControl.ControlDirection;
+import com.jme3.util.clone.Cloner;
 import java.io.IOException;
 
 /**
@@ -93,7 +94,18 @@ public class CameraNode extends Node {
 //        this.lookAt(position, upVector);
 //        camControl.getCamera().lookAt(position, upVector);
 //    }
-    
+
+    /**
+     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
+     */
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) {
+        // A change in behavior... I think previously CameraNode was probably
+        // not really cloneable... or at least its camControl would be pointing
+        // to the wrong control. -pspeed
+        this.camControl = cloner.clone(camControl);
+    }
+
     @Override
     public void read(JmeImporter im) throws IOException {
         super.read(im);

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

@@ -43,6 +43,7 @@ import com.jme3.material.Material;
 import com.jme3.math.Matrix4f;
 import com.jme3.renderer.Camera;
 import com.jme3.scene.VertexBuffer.Type;
+import com.jme3.util.clone.Cloner;
 import com.jme3.util.TempVars;
 import java.io.IOException;
 import java.util.Queue;
@@ -54,12 +55,12 @@ import java.util.logging.Logger;
  * contains the geometric data for rendering objects. It manages all rendering
  * information such as a {@link Material} object to define how the surface
  * should be shaded and the {@link Mesh} data to contain the actual geometry.
- * 
+ *
  * @author Kirill Vainer
  */
 public class Geometry extends Spatial {
 
-    // Version #1: removed shared meshes. 
+    // Version #1: removed shared meshes.
     // models loaded with shared mesh will be automatically fixed.
     public static final int SAVABLE_VERSION = 1;
     private static final Logger logger = Logger.getLogger(Geometry.class.getName());
@@ -71,19 +72,19 @@ public class Geometry extends Spatial {
      */
     protected boolean ignoreTransform = false;
     protected transient Matrix4f cachedWorldMat = new Matrix4f();
-    
+
     /**
      * Specifies which {@link GeometryGroupNode} this <code>Geometry</code>
      * is managed by.
      */
     protected GeometryGroupNode groupNode;
-    
+
     /**
-     * The start index of this <code>Geometry's</code> inside 
+     * The start index of this <code>Geometry's</code> inside
      * the {@link GeometryGroupNode}.
      */
     protected int startIndex = -1;
-        
+
     /**
      * Serialization only. Do not use.
      */
@@ -95,37 +96,37 @@ public class Geometry extends Spatial {
      * Create a geometry node without any mesh data.
      * Both the mesh and the material are null, the geometry
      * cannot be rendered until those are set.
-     * 
+     *
      * @param name The name of this geometry
      */
     public Geometry(String name) {
         super(name);
-        
+
         // For backwards compatibility, only clear the "requires
         // update" flag if we are not a subclass of Geometry.
         // This prevents subclass from silently failing to receive
         // updates when they upgrade.
-        setRequiresUpdates(Geometry.class != getClass()); 
+        setRequiresUpdates(Geometry.class != getClass());
     }
 
     /**
      * Create a geometry node with mesh data.
      * The material of the geometry is null, it cannot
      * be rendered until it is set.
-     * 
+     *
      * @param name The name of this geometry
      * @param mesh The mesh data for this geometry
      */
     public Geometry(String name, Mesh mesh) {
         this(name);
-        
+
         if (mesh == null) {
             throw new IllegalArgumentException("mesh cannot be null");
         }
 
         this.mesh = mesh;
     }
-    
+
     @Override
     public boolean checkCulling(Camera cam) {
         if (isGrouped()) {
@@ -137,8 +138,8 @@ public class Geometry extends Spatial {
 
     /**
      * @return If ignoreTransform mode is set.
-     * 
-     * @see Geometry#setIgnoreTransform(boolean) 
+     *
+     * @see Geometry#setIgnoreTransform(boolean)
      */
     public boolean isIgnoreTransform() {
         return ignoreTransform;
@@ -156,7 +157,7 @@ public class Geometry extends Spatial {
      * Level 0 indicates that the default index buffer should be used,
      * levels [1, LodLevels + 1] represent the levels set on the mesh
      * with {@link Mesh#setLodLevels(com.jme3.scene.VertexBuffer[]) }.
-     * 
+     *
      * @param lod The lod level to set
      */
     @Override
@@ -170,7 +171,7 @@ public class Geometry extends Spatial {
         }
 
         lodLevel = lod;
-        
+
         if (isGrouped()) {
             groupNode.onMeshChange(this);
         }
@@ -178,7 +179,7 @@ public class Geometry extends Spatial {
 
     /**
      * Returns the LOD level set with {@link #setLodLevel(int) }.
-     * 
+     *
      * @return the LOD level set
      */
     public int getLodLevel() {
@@ -187,10 +188,10 @@ public class Geometry extends Spatial {
 
     /**
      * Returns this geometry's mesh vertex count.
-     * 
+     *
      * @return this geometry's mesh vertex count.
-     * 
-     * @see Mesh#getVertexCount() 
+     *
+     * @see Mesh#getVertexCount()
      */
     public int getVertexCount() {
         return mesh.getVertexCount();
@@ -198,10 +199,10 @@ public class Geometry extends Spatial {
 
     /**
      * Returns this geometry's mesh triangle count.
-     * 
+     *
      * @return this geometry's mesh triangle count.
-     * 
-     * @see Mesh#getTriangleCount() 
+     *
+     * @see Mesh#getTriangleCount()
      */
     public int getTriangleCount() {
         return mesh.getTriangleCount();
@@ -209,9 +210,9 @@ public class Geometry extends Spatial {
 
     /**
      * Sets the mesh to use for this geometry when rendering.
-     * 
+     *
      * @param mesh the mesh to use for this geometry
-     * 
+     *
      * @throws IllegalArgumentException If mesh is null
      */
     public void setMesh(Mesh mesh) {
@@ -221,7 +222,7 @@ public class Geometry extends Spatial {
 
         this.mesh = mesh;
         setBoundRefresh();
-        
+
         if (isGrouped()) {
             groupNode.onMeshChange(this);
         }
@@ -229,10 +230,10 @@ public class Geometry extends Spatial {
 
     /**
      * Returns the mesh to use for this geometry
-     * 
+     *
      * @return the mesh to use for this geometry
-     * 
-     * @see #setMesh(com.jme3.scene.Mesh) 
+     *
+     * @see #setMesh(com.jme3.scene.Mesh)
      */
     public Mesh getMesh() {
         return mesh;
@@ -240,13 +241,13 @@ public class Geometry extends Spatial {
 
     /**
      * Sets the material to use for this geometry.
-     * 
+     *
      * @param material the material to use for this geometry
      */
     @Override
     public void setMaterial(Material material) {
         this.material = material;
-        
+
         if (isGrouped()) {
             groupNode.onMaterialChange(this);
         }
@@ -254,10 +255,10 @@ public class Geometry extends Spatial {
 
     /**
      * Returns the material that is used for this geometry.
-     * 
+     *
      * @return the material that is used for this geometry
-     * 
-     * @see #setMaterial(com.jme3.material.Material) 
+     *
+     * @see #setMaterial(com.jme3.material.Material)
      */
     public Material getMaterial() {
         return material;
@@ -310,9 +311,9 @@ public class Geometry extends Spatial {
         computeWorldMatrix();
 
         if (isGrouped()) {
-            groupNode.onTransformChange(this);   
+            groupNode.onTransformChange(this);
         }
-        
+
         // geometry requires lights to be sorted
         worldLights.sort(true);
     }
@@ -326,9 +327,9 @@ public class Geometry extends Spatial {
 
     /**
      * Associate this <code>Geometry</code> with a {@link GeometryGroupNode}.
-     * 
+     *
      * Should only be called by the parent {@link GeometryGroupNode}.
-     * 
+     *
      * @param node Which {@link GeometryGroupNode} to associate with.
      * @param startIndex The starting index of this geometry in the group.
      */
@@ -336,26 +337,26 @@ public class Geometry extends Spatial {
         if (isGrouped()) {
             unassociateFromGroupNode();
         }
-        
+
         this.groupNode = node;
         this.startIndex = startIndex;
     }
 
     /**
-     * Removes the {@link GeometryGroupNode} association from this 
+     * Removes the {@link GeometryGroupNode} association from this
      * <code>Geometry</code>.
-     * 
+     *
      * Should only be called by the parent {@link GeometryGroupNode}.
      */
     public void unassociateFromGroupNode() {
         if (groupNode != null) {
-            // Once the geometry is removed 
+            // Once the geometry is removed
             // from the parent, the group node needs to be updated.
             groupNode.onGeometryUnassociated(this);
             groupNode = null;
-            
+
             // change the default to -1 to make error detection easier
-            startIndex = -1; 
+            startIndex = -1;
         }
     }
 
@@ -367,7 +368,7 @@ public class Geometry extends Spatial {
     @Override
     protected void setParent(Node parent) {
         super.setParent(parent);
-        
+
         // If the geometry is managed by group node we need to unassociate.
         if (parent == null && isGrouped()) {
             unassociateFromGroupNode();
@@ -413,7 +414,7 @@ public class Geometry extends Spatial {
      * {@link Geometry#getWorldTransform() world transform} of this geometry.
      * In order to receive updated values, you must call {@link Geometry#computeWorldMatrix() }
      * before using this method.
-     * 
+     *
      * @return Matrix to transform from local space to world space
      */
     public Matrix4f getWorldMatrix() {
@@ -425,7 +426,7 @@ public class Geometry extends Spatial {
      * This alters the bound used on the mesh as well via
      * {@link Mesh#setBound(com.jme3.bounding.BoundingVolume) } and
      * forces the world bounding volume to be recomputed.
-     * 
+     *
      * @param modelBound The model bound to set
      */
     @Override
@@ -472,15 +473,15 @@ public class Geometry extends Spatial {
     }
 
     /**
-     * Determine whether this <code>Geometry</code> is managed by a 
+     * Determine whether this <code>Geometry</code> is managed by a
      * {@link GeometryGroupNode} or not.
-     * 
+     *
      * @return True if managed by a {@link GeometryGroupNode}.
      */
     public boolean isGrouped() {
         return groupNode != null;
     }
-    
+
     /**
      * @deprecated Use {@link #isGrouped()} instead.
      */
@@ -499,14 +500,14 @@ public class Geometry extends Spatial {
     @Override
     public Geometry clone(boolean cloneMaterial) {
         Geometry geomClone = (Geometry) super.clone(cloneMaterial);
-        
+
         // This geometry is managed,
         // but the cloned one is not attached to anything, hence not managed.
         if (geomClone.isGrouped()) {
             geomClone.groupNode = null;
             geomClone.startIndex = -1;
         }
-        
+
         geomClone.cachedWorldMat = cachedWorldMat.clone();
         if (material != null) {
             if (cloneMaterial) {
@@ -546,6 +547,16 @@ public class Geometry extends Spatial {
         return geomClone;
     }
 
+    /**
+     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
+     */
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) {
+        this.mesh = cloner.clone(mesh);
+        this.material = cloner.clone(material);
+        this.groupNode = cloner.clone(groupNode);
+    }
+
     @Override
     public void write(JmeExporter ex) throws IOException {
         super.write(ex);

+ 15 - 3
jme3-core/src/main/java/com/jme3/scene/LightNode.java

@@ -36,11 +36,12 @@ import com.jme3.export.JmeImporter;
 import com.jme3.light.Light;
 import com.jme3.scene.control.LightControl;
 import com.jme3.scene.control.LightControl.ControlDirection;
+import com.jme3.util.clone.Cloner;
 import java.io.IOException;
 
 /**
  * <code>LightNode</code> is used to link together a {@link Light} object
- * with a {@link Node} object. 
+ * with a {@link Node} object.
  *
  * @author Tim8Dev
  */
@@ -66,7 +67,7 @@ public class LightNode extends Node {
 
     /**
      * Enable or disable the <code>LightNode</code> functionality.
-     * 
+     *
      * @param enabled If false, the functionality of LightNode will
      * be disabled.
      */
@@ -93,7 +94,18 @@ public class LightNode extends Node {
     public Light getLight() {
         return lightControl.getLight();
     }
-    
+
+    /**
+     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
+     */
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) {
+        // A change in behavior... I think previously LightNode was probably
+        // not really cloneable... or at least its lightControl would be pointing
+        // to the wrong control. -pspeed
+        this.lightControl = cloner.clone(lightControl);
+    }
+
     @Override
     public void read(JmeImporter im) throws IOException {
         super.read(im);

File diff suppressed because it is too large
+ 215 - 179
jme3-core/src/main/java/com/jme3/scene/Mesh.java


+ 79 - 65
jme3-core/src/main/java/com/jme3/scene/Node.java

@@ -40,6 +40,7 @@ import com.jme3.export.Savable;
 import com.jme3.material.Material;
 import com.jme3.util.SafeArrayList;
 import com.jme3.util.TempVars;
+import com.jme3.util.clone.Cloner;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
@@ -53,7 +54,7 @@ import java.util.logging.Logger;
  * node maintains a collection of children and handles merging said children
  * into a single bound to allow for very fast culling of multiple nodes. Node
  * allows for any number of children to be attached.
- * 
+ *
  * @author Mark Powell
  * @author Gregg Patton
  * @author Joshua Slack
@@ -62,26 +63,26 @@ public class Node extends Spatial {
 
     private static final Logger logger = Logger.getLogger(Node.class.getName());
 
-    /** 
+    /**
      * This node's children.
      */
     protected SafeArrayList<Spatial> children = new SafeArrayList<Spatial>(Spatial.class);
 
     /**
      * If this node is a root, this list will contain the current
-     * set of children (and children of children) that require 
+     * set of children (and children of children) that require
      * updateLogicalState() to be called as indicated by their
      * requiresUpdate() method.
      */
     private SafeArrayList<Spatial> updateList = null;
-    
+
     /**
      * False if the update list requires rebuilding.  This is Node.class
      * specific and therefore not included as part of the Spatial update flags.
      * A flag is used instead of nulling the updateList to avoid reallocating
      * a whole list every time the scene graph changes.
-     */     
-    private boolean updateListValid = false;    
+     */
+    private boolean updateListValid = false;
 
     /**
      * Serialization only. Do not use.
@@ -93,29 +94,29 @@ public class Node extends Spatial {
     /**
      * Constructor instantiates a new <code>Node</code> with a default empty
      * list for containing children.
-     * 
+     *
      * @param name the name of the scene element. This is required for
      * identification and comparison purposes.
      */
     public Node(String name) {
         super(name);
-        
+
         // For backwards compatibility, only clear the "requires
         // update" flag if we are not a subclass of Node.
         // This prevents subclass from silently failing to receive
         // updates when they upgrade.
-        setRequiresUpdates(Node.class != getClass()); 
+        setRequiresUpdates(Node.class != getClass());
     }
 
     /**
-     * 
+     *
      * <code>getQuantity</code> returns the number of children this node
      * maintains.
-     * 
+     *
      * @return the number of children this node maintains.
      */
     public int getQuantity() {
-        return children.size();        
+        return children.size();
     }
 
     @Override
@@ -143,7 +144,7 @@ public class Node extends Spatial {
     @Override
     protected void updateWorldBound(){
         super.updateWorldBound();
-        
+
         // for a node, the world bound is a combination of all it's children
         // bounds
         BoundingVolume resultBound = null;
@@ -167,7 +168,7 @@ public class Node extends Spatial {
     protected void setParent(Node parent) {
         if( this.parent == null && parent != null ) {
             // We were a root before and now we aren't... make sure if
-            // we had an updateList then we clear it completely to 
+            // we had an updateList then we clear it completely to
             // avoid holding the dead array.
             updateList = null;
             updateListValid = false;
@@ -204,15 +205,15 @@ public class Node extends Spatial {
             return updateList;
         }
         if( updateList == null ) {
-            updateList = new SafeArrayList<Spatial>(Spatial.class);            
+            updateList = new SafeArrayList<Spatial>(Spatial.class);
         } else {
             updateList.clear();
         }
 
         // Build the list
         addUpdateChildren(updateList);
-        updateListValid = true;       
-        return updateList;   
+        updateListValid = true;
+        return updateList;
     }
 
     @Override
@@ -238,7 +239,7 @@ public class Node extends Spatial {
             // This branch has no geometric state that requires updates.
             return;
         }
-        
+
         if ((refreshFlags & RF_LIGHTLIST) != 0){
             updateWorldLightList();
         }
@@ -250,7 +251,7 @@ public class Node extends Spatial {
         }
 
         refreshFlags &= ~RF_CHILD_LIGHTLIST;
-        
+
         if (!children.isEmpty()) {
             // the important part- make sure child geometric state is refreshed
             // first before updating own world bound. This saves
@@ -260,7 +261,7 @@ public class Node extends Spatial {
             for (Spatial child : children.getArray()) {
                 child.updateGeometricState();
             }
-        }            
+        }
 
         if ((refreshFlags & RF_BOUND) != 0){
             updateWorldBound();
@@ -272,7 +273,7 @@ public class Node extends Spatial {
     /**
      * <code>getTriangleCount</code> returns the number of triangles contained
      * in all sub-branches of this node that contain geometry.
-     * 
+     *
      * @return the triangle count of this branch.
      */
     @Override
@@ -286,11 +287,11 @@ public class Node extends Spatial {
 
         return count;
     }
-    
+
     /**
      * <code>getVertexCount</code> returns the number of vertices contained
      * in all sub-branches of this node that contain geometry.
-     * 
+     *
      * @return the vertex count of this branch.
      */
     @Override
@@ -311,7 +312,7 @@ public class Node extends Spatial {
      * returned.
      * <br>
      * If the child already had a parent it is detached from that former parent.
-     * 
+     *
      * @param child
      *            the child to attach to this node.
      * @return the number of children maintained by this node.
@@ -320,15 +321,15 @@ public class Node extends Spatial {
     public int attachChild(Spatial child) {
         return attachChildAt(child, children.size());
     }
-    
+
     /**
-     * 
+     *
      * <code>attachChildAt</code> attaches a child to this node at an index. This node
      * becomes the child's parent. The current number of children maintained is
      * returned.
      * <br>
      * If the child already had a parent it is detached from that former parent.
-     * 
+     *
      * @param child
      *            the child to attach to this node.
      * @return the number of children maintained by this node.
@@ -344,7 +345,7 @@ public class Node extends Spatial {
             }
             child.setParent(this);
             children.add(index, child);
-            
+
             // XXX: Not entirely correct? Forces bound update up the
             // tree stemming from the attached child. Also forces
             // transform update down the tree-
@@ -354,17 +355,17 @@ public class Node extends Spatial {
                 logger.log(Level.FINE,"Child ({0}) attached to this node ({1})",
                         new Object[]{child.getName(), getName()});
             }
-            
+
             invalidateUpdateList();
         }
-        
+
         return children.size();
     }
 
     /**
      * <code>detachChild</code> removes a given child from the node's list.
      * This child will no longer be maintained.
-     * 
+     *
      * @param child
      *            the child to remove.
      * @return the index the child was at. -1 if the child was not in the list.
@@ -379,16 +380,16 @@ public class Node extends Spatial {
                 detachChildAt(index);
             }
             return index;
-        } 
-            
-        return -1;        
+        }
+
+        return -1;
     }
 
     /**
      * <code>detachChild</code> removes a given child from the node's list.
      * This child will no longe be maintained. Only the first child with a
      * matching name is removed.
-     * 
+     *
      * @param childName
      *            the child to remove.
      * @return the index the child was at. -1 if the child was not in the list.
@@ -408,10 +409,10 @@ public class Node extends Spatial {
     }
 
     /**
-     * 
+     *
      * <code>detachChildAt</code> removes a child at a given index. That child
      * is returned for saving purposes.
-     * 
+     *
      * @param index
      *            the index of the child to be removed.
      * @return the child at the supplied index.
@@ -432,14 +433,14 @@ public class Node extends Spatial {
             child.setTransformRefresh();
             // lights are also inherited from parent
             child.setLightListRefresh();
-            
+
             invalidateUpdateList();
         }
         return child;
     }
 
     /**
-     * 
+     *
      * <code>detachAllChildren</code> removes all children attached to this
      * node.
      */
@@ -458,7 +459,7 @@ public class Node extends Spatial {
      * in this node's list of children.
      * @param sp
      *          The spatial to look up
-     * @return 
+     * @return
      *          The index of the spatial in the node's children, or -1
      *          if the spatial is not attached to this node
      */
@@ -468,7 +469,7 @@ public class Node extends Spatial {
 
     /**
      * More efficient than e.g detaching and attaching as no updates are needed.
-     * 
+     *
      * @param index1 The index of the first child to swap
      * @param index2 The index of the second child to swap
      */
@@ -481,9 +482,9 @@ public class Node extends Spatial {
     }
 
     /**
-     * 
+     *
      * <code>getChild</code> returns a child at a given index.
-     * 
+     *
      * @param i
      *            the index to retrieve the child from.
      * @return the child at a specified index.
@@ -497,13 +498,13 @@ public class Node extends Spatial {
      * given name (case sensitive.) This method does a depth first recursive
      * search of all descendants of this node, it will return the first spatial
      * found with a matching name.
-     * 
+     *
      * @param name
      *            the name of the child to retrieve. If null, we'll return null.
      * @return the child if found, or null.
      */
     public Spatial getChild(String name) {
-        if (name == null) 
+        if (name == null)
             return null;
 
         for (Spatial child : children.getArray()) {
@@ -518,11 +519,11 @@ public class Node extends Spatial {
         }
         return null;
     }
-    
+
     /**
      * determines if the provided Spatial is contained in the children list of
      * this node.
-     * 
+     *
      * @param spat
      *            the child object to look for.
      * @return true if the object is contained, false otherwise.
@@ -566,39 +567,39 @@ public class Node extends Spatial {
 
     public int collideWith(Collidable other, CollisionResults results){
         int total = 0;
-        
+
         // optimization: try collideWith BoundingVolume to avoid possibly redundant tests on children
-        // number 4 in condition is somewhat arbitrary. When there is only one child, the boundingVolume test is redundant at all. 
+        // number 4 in condition is somewhat arbitrary. When there is only one child, the boundingVolume test is redundant at all.
         // The idea is when there are few children, it can be too expensive to test boundingVolume first.
         /*
         I'm removing this change until some issues can be addressed and I really
         think it needs to be implemented a better way anyway.
-        
+
         First, it causes issues for anyone doing collideWith() with BoundingVolumes
         and expecting it to trickle down to the children.  For example, children
         with BoundingSphere bounding volumes and collideWith(BoundingSphere).  Doing
         a collision check at the parent level then has to do a BoundingSphere to BoundingBox
         collision which isn't resolved.  (Having to come up with a collision point in that
         case is tricky and the first sign that this is the wrong approach.)
-        
+
         Second, the rippling changes this caused to 'optimize' collideWith() for this
         special use-case are another sign that this approach was a bit dodgy.  The whole
         idea of calculating a full collision just to see if the two shapes collide at all
         is very wasteful.
-        
+
         A proper implementation should support a simpler boolean check that doesn't do
         all of that calculation.  For example, if 'other' is also a BoundingVolume (ie: 99.9%
         of all non-Ray cases) then a direct BV to BV intersects() test can be done.  So much
         faster.  And if 'other' _is_ a Ray then the BV.intersects(Ray) call can be done.
-        
+
         I don't have time to do it right now but I'll at least un-break a bunch of peoples'
         code until it can be 'optimized' properly.  Hopefully it's not too late to back out
-        the other dodgy ripples this caused.  -pspeed (hindsight-expert ;)) 
-        
+        the other dodgy ripples this caused.  -pspeed (hindsight-expert ;))
+
         Note: the code itself is relatively simple to implement but I don't have time to
         a) test it, and b) see if '> 4' is still a decent check for it.  Could be it's fast
         enough to do all the time for > 1.
-        
+
         if (children.size() > 4)
         {
           BoundingVolume bv = this.getWorldBound();
@@ -642,7 +643,7 @@ public class Node extends Spatial {
      * @return Non-null, but possibly 0-element, list of matching Spatials (also Instances extending Spatials).
      *
      * @see java.util.regex.Pattern
-     * @see Spatial#matches(java.lang.Class, java.lang.String) 
+     * @see Spatial#matches(java.lang.Class, java.lang.String)
      */
     @SuppressWarnings("unchecked")
     public <T extends Spatial>List<T> descendantMatches(
@@ -662,7 +663,7 @@ public class Node extends Spatial {
     /**
      * Convenience wrapper.
      *
-     * @see #descendantMatches(java.lang.Class, java.lang.String) 
+     * @see #descendantMatches(java.lang.Class, java.lang.String)
      */
     public <T extends Spatial>List<T> descendantMatches(
             Class<T> spatialSubclass) {
@@ -672,7 +673,7 @@ public class Node extends Spatial {
     /**
      * Convenience wrapper.
      *
-     * @see #descendantMatches(java.lang.Class, java.lang.String) 
+     * @see #descendantMatches(java.lang.Class, java.lang.String)
      */
     public <T extends Spatial>List<T> descendantMatches(String nameRegex) {
         return descendantMatches(null, nameRegex);
@@ -691,7 +692,7 @@ public class Node extends Spatial {
         // Reset the fields of the clone that should be in a 'new' state.
         nodeClone.updateList = null;
         nodeClone.updateListValid = false; // safe because parent is nulled out in super.clone()
-            
+
         return nodeClone;
     }
 
@@ -707,6 +708,19 @@ public class Node extends Spatial {
         return nodeClone;
     }
 
+    /**
+     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
+     */
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) {
+        this.children = cloner.clone(children);
+
+        // Only the outer cloning thing knows whether this should be nulled
+        // or not... after all, we might be cloning a root node in which case
+        // cloning this list is fine.
+        this.updateList = cloner.clone(updateList);
+    }
+
     @Override
     public void write(JmeExporter e) throws IOException {
         super.write(e);
@@ -718,8 +732,8 @@ public class Node extends Spatial {
         // XXX: Load children before loading itself!!
         // This prevents empty children list if controls query
         // it in Control.setSpatial().
-        
-        children = new SafeArrayList( Spatial.class, 
+
+        children = new SafeArrayList( Spatial.class,
                                       e.getCapsule(this).readSavableArrayList("children", null) );
 
         // go through children and set parent to this node
@@ -728,7 +742,7 @@ public class Node extends Spatial {
                 child.parent = this;
             }
         }
-        
+
         super.read(e);
     }
 
@@ -749,7 +763,7 @@ public class Node extends Spatial {
             }
         }
     }
-    
+
     @Override
     public void depthFirstTraversal(SceneGraphVisitor visitor) {
         for (Spatial child : children.getArray()) {
@@ -757,7 +771,7 @@ public class Node extends Spatial {
         }
         visitor.visit(this);
     }
-    
+
     @Override
     protected void breadthFirstTraversal(SceneGraphVisitor visitor, Queue<Spatial> queue) {
         queue.addAll(children);

+ 114 - 66
jme3-core/src/main/java/com/jme3/scene/Spatial.java

@@ -47,6 +47,8 @@ import com.jme3.renderer.queue.RenderQueue;
 import com.jme3.renderer.queue.RenderQueue.Bucket;
 import com.jme3.renderer.queue.RenderQueue.ShadowMode;
 import com.jme3.scene.control.Control;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import com.jme3.util.SafeArrayList;
 import com.jme3.util.TempVars;
 import java.io.IOException;
@@ -63,17 +65,17 @@ import java.util.logging.Logger;
  * @author Joshua Slack
  * @version $Revision: 4075 $, $Data$
  */
-public abstract class Spatial implements Savable, Cloneable, Collidable, CloneableSmartAsset {
+public abstract class Spatial implements Savable, Cloneable, Collidable, CloneableSmartAsset, JmeCloneable {
 
     private static final Logger logger = Logger.getLogger(Spatial.class.getName());
 
     /**
-     * Specifies how frustum culling should be handled by 
+     * Specifies how frustum culling should be handled by
      * this spatial.
      */
     public enum CullHint {
 
-        /** 
+        /**
          * Do whatever our parent does. If no parent, default to {@link #Dynamic}.
          */
         Inherit,
@@ -83,13 +85,13 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
          * Camera planes whether or not this Spatial should be culled.
          */
         Dynamic,
-        /** 
+        /**
          * Always cull this from the view, throwing away this object
          * and any children from rendering commands.
          */
         Always,
         /**
-         * Never cull this from view, always draw it. 
+         * Never cull this from view, always draw it.
          * Note that we will still get culled if our parent is culled.
          */
         Never;
@@ -100,15 +102,15 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      */
     public enum BatchHint {
 
-        /** 
+        /**
          * Do whatever our parent does. If no parent, default to {@link #Always}.
          */
         Inherit,
-        /** 
+        /**
          * This spatial will always be batched when attached to a BatchNode.
          */
         Always,
-        /** 
+        /**
          * This spatial will never be batched when attached to a BatchNode.
          */
         Never;
@@ -118,12 +120,12 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      */
     protected static final int RF_TRANSFORM = 0x01, // need light resort + combine transforms
                                RF_BOUND = 0x02,
-                               RF_LIGHTLIST = 0x04, // changes in light lists 
+                               RF_LIGHTLIST = 0x04, // changes in light lists
                                RF_CHILD_LIGHTLIST = 0x08; // some child need geometry update
-    
+
     protected CullHint cullHint = CullHint.Inherit;
     protected BatchHint batchHint = BatchHint.Inherit;
-    /** 
+    /**
      * Spatial's bounding volume relative to the world.
      */
     protected BoundingVolume worldBound;
@@ -132,7 +134,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      */
     protected LightList localLights;
     protected transient LightList worldLights;
-    /** 
+    /**
      * This spatial's name.
      */
     protected String name;
@@ -147,11 +149,11 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
     protected HashMap<String, Savable> userData = null;
     /**
      * Used for smart asset caching
-     * 
-     * @see AssetKey#useSmartCache() 
+     *
+     * @see AssetKey#useSmartCache()
      */
     protected AssetKey key;
-    /** 
+    /**
      * Spatial's parent, or null if it has none.
      */
     protected transient Node parent;
@@ -174,7 +176,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
     /**
      * Serialization only. Do not use.
      * Not really. This class is never instantiated directly but the
-     * subclasses like to use the no-arg constructor for their own 
+     * subclasses like to use the no-arg constructor for their own
      * no-arg constructor... which is technically weaker than
      * forward supplying defaults.
      */
@@ -192,7 +194,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      */
     protected Spatial(String name) {
         this.name = name;
-        
+
         localTransform = new Transform();
         worldTransform = new Transform();
 
@@ -219,13 +221,13 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
     boolean requiresUpdates() {
         return requiresUpdates | !controls.isEmpty();
     }
-    
+
     /**
-     * Subclasses can call this with true to denote that they require 
+     * Subclasses can call this with true to denote that they require
      * updateLogicalState() to be called even if they contain no controls.
      * Setting this to false reverts to the default behavior of only
      * updating if the spatial has controls.  This is not meant to
-     * indicate dynamic state in any way and must be called while 
+     * indicate dynamic state in any way and must be called while
      * unattached or an IllegalStateException is thrown.  It is designed
      * to be called during object construction and then never changed, ie:
      * it's meant to be subclass specific state and not runtime state.
@@ -251,12 +253,12 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
         // override it for more optimal behavior.  Node and Geometry will override
         // it to false if the class is Node.class or Geometry.class.
         // This means that all subclasses will default to the old behavior
-        // unless they opt in. 
+        // unless they opt in.
         if( parent != null ) {
-            throw new IllegalStateException("setRequiresUpdates() cannot be called once attached."); 
+            throw new IllegalStateException("setRequiresUpdates() cannot be called once attached.");
         }
         this.requiresUpdates = f;
-    } 
+    }
 
     /**
      * Indicate that the transform of this spatial has changed and that
@@ -269,13 +271,13 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
 
     protected void setLightListRefresh() {
         refreshFlags |= RF_LIGHTLIST;
-        
+
         // Make sure next updateGeometricState() visits this branch
         // to update lights.
         Spatial p = parent;
         while (p != null) {
             //if (p.refreshFlags != 0) {
-                // any refresh flag is sufficient, 
+                // any refresh flag is sufficient,
                 // as each propagates to the root Node
 
                 // 2015/2/8:
@@ -283,16 +285,16 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
                 // or getWorldTransform() activates a "partial refresh"
                 // which does not update the lights but does clear
                 // the refresh flags on the ancestors!
-            
-            //    return; 
+
+            //    return;
             //}
-            
+
             if ((p.refreshFlags & RF_CHILD_LIGHTLIST) != 0) {
                 // The parent already has this flag,
                 // so must all ancestors.
                 return;
             }
-            
+
             p.refreshFlags |= RF_CHILD_LIGHTLIST;
             p = p.parent;
         }
@@ -315,10 +317,10 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
             p = p.parent;
         }
     }
-    
+
     /**
      * (Internal use only) Forces a refresh of the given types of data.
-     * 
+     *
      * @param transforms Refresh world transform based on parents'
      * @param bounds Refresh bounding volume data based on child nodes
      * @param lights Refresh light list based on parents'
@@ -401,9 +403,9 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
     /**
      * Returns the local {@link LightList}, which are the lights
      * that were directly attached to this <code>Spatial</code> through the
-     * {@link #addLight(com.jme3.light.Light) } and 
+     * {@link #addLight(com.jme3.light.Light) } and
      * {@link #removeLight(com.jme3.light.Light) } methods.
-     * 
+     *
      * @return The local light list
      */
     public LightList getLocalLightList() {
@@ -414,7 +416,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      * Returns the world {@link LightList}, containing the lights
      * combined from all this <code>Spatial's</code> parents up to and including
      * this <code>Spatial</code>'s lights.
-     * 
+     *
      * @return The combined world light list
      */
     public LightList getWorldLightList() {
@@ -502,14 +504,14 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      * <code>lookAt</code> is a convenience method for auto-setting the local
      * rotation based on a position in world space and an up vector. It computes the rotation
      * to transform the z-axis to point onto 'position' and the y-axis to 'up'.
-     * Unlike {@link Quaternion#lookAt(com.jme3.math.Vector3f, com.jme3.math.Vector3f) } 
+     * Unlike {@link Quaternion#lookAt(com.jme3.math.Vector3f, com.jme3.math.Vector3f) }
      * this method takes a world position to look at and not a relative direction.
      *
      * Note : 28/01/2013 this method has been fixed as it was not taking into account the parent rotation.
      * This was resulting in improper rotation when the spatial had rotated parent nodes.
-     * This method is intended to work in world space, so no matter what parent graph the 
+     * This method is intended to work in world space, so no matter what parent graph the
      * spatial has, it will look at the given position in world space.
-     * 
+     *
      * @param position
      *            where to look at in terms of world coordinates
      * @param upVector
@@ -522,10 +524,10 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
         TempVars vars = TempVars.get();
 
         Vector3f compVecA = vars.vect4;
-      
+
         compVecA.set(position).subtractLocal(worldTranslation);
-        getLocalRotation().lookAt(compVecA, upVector);        
-        
+        getLocalRotation().lookAt(compVecA, upVector);
+
         if ( getParent() != null ) {
             Quaternion rot=vars.quat1;
             rot =  rot.set(parent.getWorldRotation()).inverseLocal().multLocal(getLocalRotation());
@@ -579,7 +581,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
     }
 
     /**
-     * Computes the world transform of this Spatial in the most 
+     * Computes the world transform of this Spatial in the most
      * efficient manner possible.
      */
     void checkDoTransformUpdate() {
@@ -670,7 +672,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      * @param vp The ViewPort to which the Spatial is being rendered to.
      *
      * @see Spatial#addControl(com.jme3.scene.control.Control)
-     * @see Spatial#getControl(java.lang.Class) 
+     * @see Spatial#getControl(java.lang.Class)
      */
     public void runControlRender(RenderManager rm, ViewPort vp) {
         if (controls.isEmpty()) {
@@ -686,26 +688,26 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      * Add a control to the list of controls.
      * @param control The control to add.
      *
-     * @see Spatial#removeControl(java.lang.Class) 
+     * @see Spatial#removeControl(java.lang.Class)
      */
     public void addControl(Control control) {
         boolean before = requiresUpdates();
         controls.add(control);
         control.setSpatial(this);
         boolean after = requiresUpdates();
-        
+
         // If the requirement to be updated has changed
         // then we need to let the parent node know so it
         // can rebuild its update list.
         if( parent != null && before != after ) {
-            parent.invalidateUpdateList();   
+            parent.invalidateUpdateList();
         }
     }
 
     /**
      * Removes the first control that is an instance of the given class.
      *
-     * @see Spatial#addControl(com.jme3.scene.control.Control) 
+     * @see Spatial#addControl(com.jme3.scene.control.Control)
      */
     public void removeControl(Class<? extends Control> controlType) {
         boolean before = requiresUpdates();
@@ -717,23 +719,23 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
             }
         }
         boolean after = requiresUpdates();
-        
+
         // If the requirement to be updated has changed
         // then we need to let the parent node know so it
         // can rebuild its update list.
         if( parent != null && before != after ) {
-            parent.invalidateUpdateList();   
+            parent.invalidateUpdateList();
         }
     }
 
     /**
      * Removes the given control from this spatial's controls.
-     * 
+     *
      * @param control The control to remove
      * @return True if the control was successfully removed. False if the
      * control is not assigned to this spatial.
      *
-     * @see Spatial#addControl(com.jme3.scene.control.Control) 
+     * @see Spatial#addControl(com.jme3.scene.control.Control)
      */
     public boolean removeControl(Control control) {
         boolean before = requiresUpdates();
@@ -743,14 +745,14 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
         }
 
         boolean after = requiresUpdates();
-        
+
         // If the requirement to be updated has changed
         // then we need to let the parent node know so it
         // can rebuild its update list.
         if( parent != null && before != after ) {
-            parent.invalidateUpdateList();   
+            parent.invalidateUpdateList();
         }
-        
+
         return result;
     }
 
@@ -761,7 +763,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      * @param controlType The superclass of the control to look for.
      * @return The first instance in the list of the controlType class, or null.
      *
-     * @see Spatial#addControl(com.jme3.scene.control.Control) 
+     * @see Spatial#addControl(com.jme3.scene.control.Control)
      */
     public <T extends Control> T getControl(Class<T> controlType) {
         for (Control c : controls.getArray()) {
@@ -790,7 +792,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
     /**
      * @return The number of controls attached to this Spatial.
      * @see Spatial#addControl(com.jme3.scene.control.Control)
-     * @see Spatial#removeControl(java.lang.Class) 
+     * @see Spatial#removeControl(java.lang.Class)
      */
     public int getNumControls() {
         return controls.size();
@@ -815,7 +817,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      * Calling this when the Spatial is attached to a node
      * will cause undefined results. User code should only call this
      * method on Spatials having no parent.
-     * 
+     *
      * @see Spatial#getWorldLightList()
      * @see Spatial#getWorldTransform()
      * @see Spatial#getWorldBound()
@@ -835,7 +837,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
         if ((refreshFlags & RF_BOUND) != 0) {
             updateWorldBound();
         }
-        
+
         assert refreshFlags == 0;
     }
 
@@ -1067,9 +1069,9 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
 
     /**
      * <code>removeLight</code> removes the given light from the Spatial.
-     * 
+     *
      * @param light The light to remove.
-     * @see Spatial#addLight(com.jme3.light.Light) 
+     * @see Spatial#addLight(com.jme3.light.Light)
      */
     public void removeLight(Light light) {
         localLights.remove(light);
@@ -1264,7 +1266,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      * All controls will be cloned using the Control.cloneForSpatial method
      * on the clone.
      *
-     * @see Mesh#cloneForAnim() 
+     * @see Mesh#cloneForAnim()
      */
     public Spatial clone(boolean cloneMaterial) {
         try {
@@ -1328,7 +1330,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      * All controls will be cloned using the Control.cloneForSpatial method
      * on the clone.
      *
-     * @see Mesh#cloneForAnim() 
+     * @see Mesh#cloneForAnim()
      */
     @Override
     public Spatial clone() {
@@ -1344,13 +1346,59 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      */
     public abstract Spatial deepClone();
 
+    /**
+     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
+     */
+    @Override
+    public Spatial jmeClone() {
+        try {
+            Spatial clone = (Spatial)super.clone();
+            return clone;
+        } catch (CloneNotSupportedException ex) {
+            throw new AssertionError();
+        }
+    }
+
+    /**
+     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
+     */
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) {
+
+        // Clone all of the fields that need fix-ups and/or potential
+        // sharing.
+        this.parent = cloner.clone(parent);
+        this.worldBound = cloner.clone(worldBound);
+        this.worldLights = cloner.clone(worldLights);
+        this.localLights = cloner.clone(localLights);
+        this.worldTransform = cloner.clone(worldTransform);
+        this.localTransform = cloner.clone(localTransform);
+        this.controls = cloner.clone(controls);
+
+        // Cloner doesn't handle maps on its own just yet.
+        // Note: this is more advanced cloning than the old clone() method
+        // did because it just shallow cloned the map.  In this case, we want
+        // to avoid all of the nasty cloneForSpatial() fixup style code that
+        // used to inject stuff into the clone's user data.  By using cloner
+        // to clone the user data we get this automatically.
+        userData = (HashMap<String, Savable>)userData.clone();
+        for( Map.Entry<String, Savable> e : userData.entrySet() ) {
+            Savable value = e.getValue();
+            if( value instanceof Cloneable ) {
+                // Note: all JmeCloneable objects are also Cloneable so this
+                // catches both cases.
+                e.setValue(cloner.clone(value));
+            }
+        }
+    }
+
     public void setUserData(String key, Object data) {
         if (userData == null) {
             userData = new HashMap<String, Savable>();
         }
 
         if(data == null){
-            userData.remove(key);            
+            userData.remove(key);
         }else if (data instanceof Savable) {
             userData.put(key, (Savable) data);
         } else {
@@ -1445,7 +1493,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
         //changed for backward compatibility with j3o files generated before the AnimControl/SkeletonControl split
         //the AnimControl creates the SkeletonControl for old files and add it to the spatial.
         //The SkeletonControl must be the last in the stack so we add the list of all other control before it.
-        //When backward compatibility won't be needed anymore this can be replaced by : 
+        //When backward compatibility won't be needed anymore this can be replaced by :
         //controls = ic.readSavableArrayList("controlsList", null));
         controls.addAll(0, ic.readSavableArrayList("controlsList", null));
 
@@ -1508,9 +1556,9 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
     /**
      * <code>setQueueBucket</code> determines at what phase of the
      * rendering process this Spatial will rendered. See the
-     * {@link Bucket} enum for an explanation of the various 
+     * {@link Bucket} enum for an explanation of the various
      * render queue buckets.
-     * 
+     *
      * @param queueBucket
      *            The bucket to use for this Spatial.
      */
@@ -1595,7 +1643,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      *
      * @return store if not null, otherwise, a new matrix containing the result.
      *
-     * @see Spatial#getWorldTransform() 
+     * @see Spatial#getWorldTransform()
      */
     public Matrix4f getLocalToWorldMatrix(Matrix4f store) {
         if (store == null) {

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

+ 68 - 57
jme3-core/src/main/java/com/jme3/scene/instancing/InstancedGeometry.java

@@ -47,19 +47,20 @@ import com.jme3.scene.VertexBuffer.Type;
 import com.jme3.scene.VertexBuffer.Usage;
 import com.jme3.util.BufferUtils;
 import com.jme3.util.TempVars;
+import com.jme3.util.clone.Cloner;
 import java.io.IOException;
 import java.nio.FloatBuffer;
 import java.util.ArrayList;
 import java.util.Arrays;
 
 public class InstancedGeometry extends Geometry {
-    
+
     private static final int INSTANCE_SIZE = 16;
-    
+
     private VertexBuffer[] globalInstanceData;
     private VertexBuffer transformInstanceData;
     private Geometry[] geometries = new Geometry[1];
-    
+
     private int firstUnusedIndex = 0;
 
     /**
@@ -71,12 +72,12 @@ public class InstancedGeometry extends Geometry {
         setBatchHint(BatchHint.Never);
         setMaxNumInstances(1);
     }
-    
+
     /**
      * Creates instanced geometry with the specified mode and name.
-     * 
-     * @param name The name of the spatial. 
-     * 
+     *
+     * @param name The name of the spatial.
+     *
      * @see Spatial#Spatial(java.lang.String)
      */
     public InstancedGeometry(String name) {
@@ -85,57 +86,57 @@ public class InstancedGeometry extends Geometry {
         setBatchHint(BatchHint.Never);
         setMaxNumInstances(1);
     }
-    
+
     /**
-     * Global user specified per-instance data. 
-     * 
+     * Global user specified per-instance data.
+     *
      * By default set to <code>null</code>, specify an array of VertexBuffers
      * via {@link #setGlobalUserInstanceData(com.jme3.scene.VertexBuffer[]) }.
-     * 
-     * @return global user specified per-instance data. 
-     * @see #setGlobalUserInstanceData(com.jme3.scene.VertexBuffer[]) 
+     *
+     * @return global user specified per-instance data.
+     * @see #setGlobalUserInstanceData(com.jme3.scene.VertexBuffer[])
      */
     public VertexBuffer[] getGlobalUserInstanceData() {
         return globalInstanceData;
     }
-    
+
     /**
      * Specify global user per-instance data.
-     * 
+     *
      * By default set to <code>null</code>, specify an array of VertexBuffers
      * that contain per-instance vertex attributes.
-     * 
+     *
      * @param globalInstanceData global user per-instance data.
-     * 
-     * @throws IllegalArgumentException If one of the VertexBuffers is not 
+     *
+     * @throws IllegalArgumentException If one of the VertexBuffers is not
      * {@link VertexBuffer#setInstanced(boolean) instanced}.
      */
     public void setGlobalUserInstanceData(VertexBuffer[] globalInstanceData) {
         this.globalInstanceData = globalInstanceData;
     }
-    
+
     /**
      * Specify camera specific user per-instance data.
-     * 
+     *
      * @param transformInstanceData The transforms for each instance.
      */
     public void setTransformUserInstanceData(VertexBuffer transformInstanceData) {
         this.transformInstanceData = transformInstanceData;
     }
-    
+
     /**
      * Return user per-instance transform data.
-     * 
+     *
      * @return The per-instance transform data.
      *
-     * @see #setTransformUserInstanceData(com.jme3.scene.VertexBuffer) 
+     * @see #setTransformUserInstanceData(com.jme3.scene.VertexBuffer)
      */
     public VertexBuffer getTransformUserInstanceData() {
         return transformInstanceData;
     }
-    
-    private void updateInstance(Matrix4f worldMatrix, float[] store, 
-                                int offset, Matrix3f tempMat3, 
+
+    private void updateInstance(Matrix4f worldMatrix, float[] store,
+                                int offset, Matrix3f tempMat3,
                                 Quaternion tempQuat) {
         worldMatrix.toRotationMatrix(tempMat3);
         tempMat3.invertLocal();
@@ -164,17 +165,17 @@ public class InstancedGeometry extends Geometry {
         store[offset + 14] = worldMatrix.m23;
         store[offset + 15] = tempQuat.getW();
     }
-    
+
     /**
      * Set the maximum amount of instances that can be rendered by this
      * instanced geometry when mode is set to auto.
-     * 
+     *
      * This re-allocates internal structures and therefore should be called
-     * only when necessary. 
-     * 
+     * only when necessary.
+     *
      * @param maxNumInstances The maximum number of instances that can be
      * rendered.
-     * 
+     *
      * @throws IllegalStateException If mode is set to manual.
      * @throws IllegalArgumentException If maxNumInstances is zero or negative
      */
@@ -182,14 +183,14 @@ public class InstancedGeometry extends Geometry {
         if (maxNumInstances < 1) {
             throw new IllegalArgumentException("maxNumInstances must be 1 or higher");
         }
-        
+
         Geometry[] originalGeometries = geometries;
         this.geometries = new Geometry[maxNumInstances];
-        
+
         if (originalGeometries != null) {
             System.arraycopy(originalGeometries, 0, geometries, 0, originalGeometries.length);
         }
-        
+
         // Resize instance data.
         if (transformInstanceData != null) {
             BufferUtils.destroyDirectBuffer(transformInstanceData.getData());
@@ -203,7 +204,7 @@ public class InstancedGeometry extends Geometry {
                     BufferUtils.createFloatBuffer(geometries.length * INSTANCE_SIZE));
         }
     }
-    
+
     public int getMaxNumInstances() {
         return geometries.length;
     }
@@ -211,12 +212,12 @@ public class InstancedGeometry extends Geometry {
     public int getActualNumInstances() {
         return firstUnusedIndex;
     }
-    
+
     private void swap(int idx1, int idx2) {
         Geometry g = geometries[idx1];
         geometries[idx1] = geometries[idx2];
         geometries[idx2] = g;
-        
+
         if (geometries[idx1] != null) {
             InstancedNode.setGeometryStartIndex2(geometries[idx1], idx1);
         }
@@ -224,7 +225,7 @@ public class InstancedGeometry extends Geometry {
             InstancedNode.setGeometryStartIndex2(geometries[idx2], idx2);
         }
     }
-    
+
     private void sanitize(boolean insideEntriesNonNull) {
         if (firstUnusedIndex >= geometries.length) {
             throw new AssertionError();
@@ -234,7 +235,7 @@ public class InstancedGeometry extends Geometry {
                 if (geometries[i] == null) {
                     if (insideEntriesNonNull) {
                         throw new AssertionError();
-                    }  
+                    }
                 } else if (InstancedNode.getGeometryStartIndex2(geometries[i]) != i) {
                     throw new AssertionError();
                 }
@@ -245,55 +246,55 @@ public class InstancedGeometry extends Geometry {
             }
         }
     }
-    
+
     public void updateInstances() {
         FloatBuffer fb = (FloatBuffer) transformInstanceData.getData();
         fb.limit(fb.capacity());
         fb.position(0);
-        
+
         TempVars vars = TempVars.get();
         {
             float[] temp = vars.matrixWrite;
-            
+
             for (int i = 0; i < firstUnusedIndex; i++) {
                 Geometry geom = geometries[i];
 
                 if (geom == null) {
                     geom = geometries[firstUnusedIndex - 1];
-                    
+
                     if (geom == null) {
                         throw new AssertionError();
                     }
-                    
+
                     swap(i, firstUnusedIndex - 1);
-                    
+
                     while (geometries[firstUnusedIndex -1] == null) {
                         firstUnusedIndex--;
                     }
                 }
-                
+
                 Matrix4f worldMatrix = geom.getWorldMatrix();
                 updateInstance(worldMatrix, temp, 0, vars.tempMat3, vars.quat1);
                 fb.put(temp);
             }
         }
         vars.release();
-        
+
         fb.flip();
-        
+
         if (fb.limit() / INSTANCE_SIZE != firstUnusedIndex) {
             throw new AssertionError();
         }
 
         transformInstanceData.updateData(fb);
     }
-    
+
     public void deleteInstance(Geometry geom) {
         int idx = InstancedNode.getGeometryStartIndex2(geom);
         InstancedNode.setGeometryStartIndex2(geom, -1);
-        
+
         geometries[idx] = null;
-        
+
         if (idx == firstUnusedIndex - 1) {
             // Deleting the last element.
             // Move index back.
@@ -309,12 +310,12 @@ public class InstancedGeometry extends Geometry {
             // Deleting element in the middle
         }
     }
-    
+
     public void addInstance(Geometry geometry) {
         if (geometry == null) {
             throw new IllegalArgumentException("geometry cannot be null");
         }
-       
+
         // Take an index from the end.
         if (firstUnusedIndex + 1 >= geometries.length) {
             // No more room.
@@ -323,15 +324,15 @@ public class InstancedGeometry extends Geometry {
 
         int freeIndex = firstUnusedIndex;
         firstUnusedIndex++;
-        
+
         geometries[freeIndex] = geometry;
         InstancedNode.setGeometryStartIndex2(geometry, freeIndex);
     }
-    
+
     public Geometry[] getGeometries() {
         return geometries;
     }
-    
+
     public VertexBuffer[] getAllInstanceData() {
         ArrayList<VertexBuffer> allData = new ArrayList();
         if (transformInstanceData != null) {
@@ -343,6 +344,16 @@ public class InstancedGeometry extends Geometry {
         return allData.toArray(new VertexBuffer[allData.size()]);
     }
 
+    /**
+     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
+     */
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) {
+        this.globalInstanceData = cloner.clone(globalInstanceData);
+        this.transformInstanceData = cloner.clone(transformInstanceData);
+        this.geometries = cloner.clone(geometries);
+    }
+
     @Override
     public void write(JmeExporter exporter) throws IOException {
         super.write(exporter);
@@ -350,7 +361,7 @@ public class InstancedGeometry extends Geometry {
         //capsule.write(currentNumInstances, "cur_num_instances", 1);
         capsule.write(geometries, "geometries", null);
     }
-    
+
     @Override
     public void read(JmeImporter importer) throws IOException {
         super.read(importer);

+ 94 - 41
jme3-core/src/main/java/com/jme3/scene/instancing/InstancedNode.java

@@ -44,20 +44,23 @@ 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;
+import java.util.Map;
 
 public class InstancedNode extends GeometryGroupNode {
-    
+
     static int getGeometryStartIndex2(Geometry geom) {
         return getGeometryStartIndex(geom);
     }
-    
+
     static void setGeometryStartIndex2(Geometry geom, int startIndex) {
         setGeometryStartIndex(geom, startIndex);
     }
-    
-    private static final class InstanceTypeKey implements Cloneable {
+
+    private static final class InstanceTypeKey implements Cloneable, JmeCloneable {
 
         Mesh mesh;
         Material material;
@@ -68,7 +71,7 @@ public class InstancedNode extends GeometryGroupNode {
             this.material = material;
             this.lodLevel = lodLevel;
         }
-        
+
         public InstanceTypeKey(){
         }
 
@@ -95,7 +98,7 @@ public class InstancedNode extends GeometryGroupNode {
             }
             return true;
         }
-        
+
         @Override
         public InstanceTypeKey clone() {
             try {
@@ -104,65 +107,94 @@ public class InstancedNode extends GeometryGroupNode {
                 throw new AssertionError();
             }
         }
+
+        @Override
+        public Object jmeClone() {
+            try {
+                return super.clone();
+            } catch( CloneNotSupportedException e ) {
+                throw new AssertionError();
+            }
+        }
+
+        @Override
+        public void cloneFields( Cloner cloner, Object original ) {
+            this.mesh = cloner.clone(mesh);
+            this.material = cloner.clone(material);
+        }
     }
-    
-    private static class InstancedNodeControl implements Control {
+
+    private static class InstancedNodeControl implements Control, JmeCloneable {
 
         private InstancedNode node;
-        
+
         public InstancedNodeControl() {
         }
-        
+
         public InstancedNodeControl(InstancedNode node) {
             this.node = node;
         }
-        
+
         @Override
         public Control cloneForSpatial(Spatial spatial) {
-            return this; 
+            return this;
             // WARNING: Sets wrong control on spatial. Will be
             // 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){
         }
-        
+
         public void update(float tpf){
         }
-        
+
         public void render(RenderManager rm, ViewPort vp) {
             node.renderFromControl();
         }
-        
+
         public void write(JmeExporter ex) throws IOException {
         }
 
         public void read(JmeImporter im) throws IOException {
         }
     }
-    
+
     protected InstancedNodeControl control;
-    
-    protected HashMap<Geometry, InstancedGeometry> igByGeom 
+
+    protected HashMap<Geometry, InstancedGeometry> igByGeom
             = new HashMap<Geometry, InstancedGeometry>();
-    
+
     private InstanceTypeKey lookUp = new InstanceTypeKey();
-    
-    private HashMap<InstanceTypeKey, InstancedGeometry> instancesMap = 
+
+    private HashMap<InstanceTypeKey, InstancedGeometry> instancesMap =
             new HashMap<InstanceTypeKey, InstancedGeometry>();
-    
+
     public InstancedNode() {
         super();
         // NOTE: since we are deserializing,
         // the control is going to be added automatically here.
     }
-    
+
     public InstancedNode(String name) {
         super(name);
         control = new InstancedNodeControl(this);
         addControl(control);
     }
-    
+
     private void renderFromControl() {
         for (InstancedGeometry ig : instancesMap.values()) {
             ig.updateInstances();
@@ -191,7 +223,7 @@ public class InstancedNode extends GeometryGroupNode {
 
         return ig;
     }
-    
+
     private void addToInstancedGeometry(Geometry geom) {
         Material material = geom.getMaterial();
         MatParam param = material.getParam("UseInstancing");
@@ -200,20 +232,20 @@ public class InstancedNode extends GeometryGroupNode {
                     + "parameter to true on the material prior "
                     + "to adding it to InstancedNode");
         }
-        
+
         InstancedGeometry ig = lookUpByGeometry(geom);
         igByGeom.put(geom, ig);
         geom.associateWithGroupNode(this, 0);
         ig.addInstance(geom);
     }
-    
+
     private void removeFromInstancedGeometry(Geometry geom) {
         InstancedGeometry ig = igByGeom.remove(geom);
         if (ig != null) {
             ig.deleteInstance(geom);
         }
     }
-    
+
     private void relocateInInstancedGeometry(Geometry geom) {
         InstancedGeometry oldIG = igByGeom.get(geom);
         InstancedGeometry newIG = lookUpByGeometry(geom);
@@ -226,7 +258,7 @@ public class InstancedNode extends GeometryGroupNode {
             igByGeom.put(geom, newIG);
         }
     }
-    
+
     private void ungroupSceneGraph(Spatial s) {
         if (s instanceof Node) {
             for (Spatial sp : ((Node) s).getChildren()) {
@@ -237,14 +269,14 @@ public class InstancedNode extends GeometryGroupNode {
             if (g.isGrouped()) {
                 // Will invoke onGeometryUnassociated automatically.
                 g.unassociateFromGroupNode();
-                
+
                 if (InstancedNode.getGeometryStartIndex(g) != -1) {
                     throw new AssertionError();
                 }
             }
         }
     }
-    
+
     @Override
     public Spatial detachChildAt(int index) {
         Spatial s = super.detachChildAt(index);
@@ -253,7 +285,7 @@ public class InstancedNode extends GeometryGroupNode {
         }
         return s;
     }
-    
+
     private void instance(Spatial n) {
         if (n instanceof Geometry) {
             Geometry g = (Geometry) n;
@@ -269,20 +301,20 @@ public class InstancedNode extends GeometryGroupNode {
             }
         }
     }
-    
+
     public void instance() {
         instance(this);
     }
-    
+
     @Override
     public Node clone() {
         return clone(true);
     }
-    
+
     @Override
     public Node clone(boolean cloneMaterials) {
         InstancedNode clone = (InstancedNode)super.clone(cloneMaterials);
-        
+
         if (instancesMap.size() > 0) {
             // Remove all instanced geometries from the clone
             for (int i = 0; i < clone.children.size(); i++) {
@@ -296,7 +328,7 @@ public class InstancedNode extends GeometryGroupNode {
                 }
             }
         }
-        
+
         // remove original control from the clone
         clone.controls.remove(this.control);
 
@@ -307,12 +339,33 @@ public class InstancedNode extends GeometryGroupNode {
         clone.lookUp = new InstanceTypeKey();
         clone.igByGeom = new HashMap<Geometry, InstancedGeometry>();
         clone.instancesMap = new HashMap<InstanceTypeKey, InstancedGeometry>();
-        
+
         clone.instance();
-        
+
         return clone;
     }
-    
+
+    /**
+     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
+     */
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) {
+        this.control = cloner.clone(control);
+        this.lookUp = cloner.clone(lookUp);
+
+        HashMap<Geometry, InstancedGeometry> newIgByGeom = new HashMap<Geometry, InstancedGeometry>();
+        for( Map.Entry<Geometry, InstancedGeometry> e : igByGeom.entrySet() ) {
+            newIgByGeom.put(cloner.clone(e.getKey()), cloner.clone(e.getValue()));
+        }
+        this.igByGeom = newIgByGeom;
+
+        HashMap<InstanceTypeKey, InstancedGeometry> newInstancesMap = new HashMap<InstanceTypeKey, InstancedGeometry>();
+        for( Map.Entry<InstanceTypeKey, InstancedGeometry> e : instancesMap.entrySet() ) {
+            newInstancesMap.put(cloner.clone(e.getKey()), cloner.clone(e.getValue()));
+        }
+        this.instancesMap = newInstancesMap;
+    }
+
     @Override
     public void onTransformChange(Geometry geom) {
         // Handled automatically

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

+ 48 - 11
jme3-core/src/main/java/com/jme3/util/IntMap.java

@@ -32,19 +32,21 @@
 package com.jme3.util;
 
 import com.jme3.util.IntMap.Entry;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.NoSuchElementException;
 
 /**
  * Similar to a {@link Map} except that ints are used as keys.
- * 
+ *
  * Taken from <a href="http://code.google.com/p/skorpios/">http://code.google.com/p/skorpios/</a>
- * 
- * @author Nate 
+ *
+ * @author Nate
  */
-public final class IntMap<T> implements Iterable<Entry<T>>, Cloneable {
-            
+public final class IntMap<T> implements Iterable<Entry<T>>, Cloneable, JmeCloneable {
+
     private Entry[] table;
     private final float loadFactor;
     private int size, mask, capacity, threshold;
@@ -93,6 +95,26 @@ public final class IntMap<T> implements Iterable<Entry<T>>, Cloneable {
         return null;
     }
 
+    /**
+     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
+     */
+    @Override
+    public Object jmeClone() {
+        try {
+            return super.clone();
+        } catch (CloneNotSupportedException ex) {
+            throw new AssertionError();
+        }
+    }
+
+    /**
+     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
+     */
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) {
+        this.table = cloner.clone(table);
+    }
+
     public boolean containsValue(Object value) {
         Entry[] table = this.table;
         for (int i = table.length; i-- > 0;){
@@ -228,7 +250,7 @@ public final class IntMap<T> implements Iterable<Entry<T>>, Cloneable {
             idx = 0;
             el = 0;
         }
-        
+
         public boolean hasNext() {
             return el < size;
         }
@@ -255,20 +277,20 @@ public final class IntMap<T> implements Iterable<Entry<T>>, Cloneable {
                 // the entry was null. find another non-null entry.
                 cur = table[++idx];
             } while (cur == null);
-            
+
             Entry e = cur;
             cur = cur.next;
             el ++;
-            
+
             return e;
         }
 
         public void remove() {
         }
-        
+
     }
-    
-    public static final class Entry<T> implements Cloneable {
+
+    public static final class Entry<T> implements Cloneable, JmeCloneable {
 
         final int key;
         T value;
@@ -303,5 +325,20 @@ public final class IntMap<T> implements Iterable<Entry<T>>, Cloneable {
             }
             return null;
         }
+
+        @Override
+        public Object jmeClone() {
+            try {
+                return super.clone();
+            } catch (CloneNotSupportedException ex) {
+                throw new AssertionError();
+            }
+        }
+
+        @Override
+        public void cloneFields( Cloner cloner, Object original ) {
+            this.value = cloner.clone(value);
+            this.next = cloner.clone(next);
+        }
     }
 }

+ 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 - 0
jme3-core/src/main/java/com/jme3/util/clone/IdentityCloneFunction.java

@@ -0,0 +1,58 @@
+/*
+ * 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 - 0
jme3-core/src/main/java/com/jme3/util/clone/ListCloneFunction.java

@@ -0,0 +1,70 @@
+/*
+ * 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)));
+        }
+    }
+}
+

+ 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 {
@@ -214,26 +216,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 {
@@ -248,6 +243,7 @@ MaterialDef Phong Lighting {
             POINTLIGHT : LightViewProjectionMatrix5
             NUM_BONES : NumberOfBones
             INSTANCING : UseInstancing
+            BACKFACE_SHADOWS: BackfaceShadows
         }
 
         ForcedRenderState {
@@ -266,6 +262,7 @@ MaterialDef Phong Lighting {
             WorldMatrix
             ViewProjectionMatrix
             ViewMatrix
+            NormalMatrix
         }
 
         Defines {

+ 2 - 3
jme3-core/src/main/resources/Common/MatDefs/Light/SPLighting.frag

@@ -41,8 +41,7 @@ varying vec3 SpecularSum;
   
 #ifdef NORMALMAP
   uniform sampler2D m_NormalMap;   
-  varying vec3 vTangent;
-  varying vec3 vBinormal;
+  varying vec4 vTangent;
 #endif
 varying vec3 vNormal;
 
@@ -71,7 +70,7 @@ uniform float m_Shininess;
 void main(){
     #if !defined(VERTEX_LIGHTING)
         #if defined(NORMALMAP)
-            mat3 tbnMat = mat3(normalize(vTangent.xyz) , normalize(vBinormal.xyz) , normalize(vNormal.xyz));
+             mat3 tbnMat = mat3(vTangent.xyz, vTangent.w * cross( (vNormal), (vTangent.xyz)), vNormal.xyz);
 
             if (!gl_FrontFacing)
             {

+ 2 - 4
jme3-core/src/main/resources/Common/MatDefs/Light/SPLighting.vert

@@ -39,8 +39,7 @@ attribute vec3 inNormal;
     varying vec3 vPos;
     #ifdef NORMALMAP
         attribute vec4 inTangent;
-        varying vec3 vTangent;
-        varying vec3 vBinormal;
+        varying vec4 vTangent;
     #endif
 #else
     #ifdef COLORRAMP
@@ -104,8 +103,7 @@ void main(){
   
        
     #if defined(NORMALMAP) && !defined(VERTEX_LIGHTING)
-      vTangent = TransformNormal(modelSpaceTan);
-      vBinormal = cross(wvNormal, vTangent)* inTangent.w;      
+      vTangent = vec4(TransformNormal(modelSpaceTan).xyz,inTangent.w);
       vNormal = wvNormal;         
       vPos = wvPosition;
     #elif !defined(VERTEX_LIGHTING)

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

+ 31 - 2
jme3-core/src/main/resources/Common/MatDefs/Shadow/PostShadowFilter.frag

@@ -18,14 +18,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
 
@@ -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

+ 2 - 1
jme3-core/src/main/resources/com/jme3/system/version.properties

@@ -1,11 +1,12 @@
 # THIS IS AN AUTO-GENERATED FILE..
 # DO NOT MODIFY!
-build.date=1900-01-01
+build.date=2016-03-25
 git.revision=0
 git.branch=unknown
 git.hash=
 git.hash.short=
 git.tag=
 name.full=jMonkeyEngine 3.1.0-UNKNOWN
+version.full=3.1.0-UNKNOWN
 version.number=3.1.0
 version.tag=SNAPSHOT

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

@@ -180,9 +180,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) {
@@ -198,7 +212,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 ")) {
@@ -455,6 +469,8 @@ public class J3MLoader implements AssetLoader {
             renderState.setDepthFunc(RenderState.TestFunction.valueOf(split[1]));
         }else if (split[0].equals("AlphaFunc")){
             renderState.setAlphaFunc(RenderState.TestFunction.valueOf(split[1]));
+        }else if (split[0].equals("LineWidth")){
+            renderState.setLineWidth(Float.parseFloat(split[1]));
         } else {
             throw new MatParseException(null, split[0], statement);
         }
@@ -636,6 +652,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"
      }
 }

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

@@ -20,5 +20,5 @@ void main(void)
        Skinning_Compute(modelSpacePos,modelSpaceNormals);
    #endif
    normal = normalize(TransformNormal(modelSpaceNormals));
-   gl_Position = g_WorldViewProjectionMatrix * modelSpacePos;
+   gl_Position = TransformWorldViewProjection(modelSpacePos);
 }

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

+ 15 - 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);
@@ -179,6 +192,7 @@ public class PhysicsHoverControl extends PhysicsVehicle implements PhysicsContro
     }
 
     public void setPhysicsSpace(PhysicsSpace space) {
+        createVehicle(space);
         if (space == null) {
             if (this.space != null) {
                 this.space.removeCollisionObject(this);

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

@@ -147,13 +147,13 @@ public class TestHoveringTank extends SimpleApplication implements AnalogListene
         spaceCraft.setLocalRotation(new Quaternion(new float[]{0, 0.01f, 0}));
 
         hoverControl = new PhysicsHoverControl(colShape, 500);
-        hoverControl.setCollisionGroup(PhysicsCollisionObject.COLLISION_GROUP_02);
 
         spaceCraft.addControl(hoverControl);
 
 
         rootNode.attachChild(spaceCraft);
         getPhysicsSpace().add(hoverControl);
+        hoverControl.setCollisionGroup(PhysicsCollisionObject.COLLISION_GROUP_02);
 
         ChaseCamera chaseCam = new ChaseCamera(cam, inputManager);
         spaceCraft.addControl(chaseCam);

+ 1 - 1
jme3-examples/src/main/java/jme3test/collision/TestMousePick.java

@@ -128,12 +128,12 @@ public class TestMousePick extends SimpleApplication {
     /** A red ball that marks the last spot that was "hit" by the "shot". */
     protected void initMark() {
         Arrow arrow = new Arrow(Vector3f.UNIT_Z.mult(2f));
-        arrow.setLineWidth(3);
 
         //Sphere sphere = new Sphere(30, 30, 0.2f);
         mark = new Geometry("BOOM!", arrow);
         //mark = new Geometry("BOOM!", sphere);
         Material mark_mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        mark_mat.getAdditionalRenderState().setLineWidth(3);
         mark_mat.setColor("Color", ColorRGBA.Red);
         mark.setMaterial(mark_mat);
     }

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

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

@@ -72,7 +72,6 @@ public class TestTangentGenBadModels extends SimpleApplication {
                     "debug tangents geom",
                     TangentBinormalGenerator.genTbnLines(g.getMesh(), 0.2f)
                 );
-                debug.getMesh().setLineWidth(1);
                 debug.setMaterial(debugMat);
                 debug.setCullHint(Spatial.CullHint.Never);
                 debug.setLocalTransform(g.getWorldTransform());

+ 100 - 0
jme3-examples/src/main/java/jme3test/light/TestTangentSpace.java

@@ -0,0 +1,100 @@
+package jme3test.light;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.input.KeyInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.light.DirectionalLight;
+import com.jme3.material.*;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.util.TangentBinormalGenerator;
+import com.jme3.util.mikktspace.MikktspaceTangentGenerator;
+
+/**
+ * test
+ *
+ * @author normenhansen
+ */
+public class TestTangentSpace extends SimpleApplication {
+    
+    public static void main(String[] args) {
+        TestTangentSpace app = new TestTangentSpace();
+        app.start();
+    }
+    
+    private Node debugNode = new Node("debug");
+    
+    @Override
+    public void simpleInitApp() {
+        renderManager.setSinglePassLightBatchSize(2);
+        renderManager.setPreferredLightMode(TechniqueDef.LightMode.SinglePass);
+        initView();
+        
+        Spatial s = assetManager.loadModel("Models/Test/BasicCubeLow.obj");
+        rootNode.attachChild(s);
+
+        Material m = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+        m.setTexture("NormalMap", assetManager.loadTexture("Models/Test/Normal_pixel.png"));
+
+        Geometry g = (Geometry)s;
+        Geometry g2 = (Geometry) g.deepClone();
+        g2.move(5, 0, 0);
+        g.getParent().attachChild(g2);
+
+        g.setMaterial(m);
+        g2.setMaterial(m);
+
+        //Regular tangent generation (left geom)
+        TangentBinormalGenerator.generate(g2.getMesh(), true);
+
+        //MikkTSPace Tangent generation (right geom)        
+
+        MikktspaceTangentGenerator.generate(g);
+        
+        createDebugTangents(g2);
+        createDebugTangents(g);
+        
+        inputManager.addListener(new ActionListener() {
+            @Override
+            public void onAction(String name, boolean isPressed, float tpf) {
+                if (name.equals("toggleDebug") && isPressed) {
+                    if (debugNode.getParent() == null) {
+                        rootNode.attachChild(debugNode);
+                    } else {
+                        debugNode.removeFromParent();
+                    }
+                }
+            }
+        }, "toggleDebug");
+
+        inputManager.addMapping("toggleDebug", new KeyTrigger(KeyInput.KEY_SPACE));
+        
+        
+        DirectionalLight dl = new DirectionalLight(new Vector3f(-1, -1, -1).normalizeLocal());
+        rootNode.addLight(dl);
+    }
+    
+    private void initView() {
+        viewPort.setBackgroundColor(ColorRGBA.DarkGray);
+        cam.setLocation(new Vector3f(8.569681f, 3.335546f, 5.4372444f));
+        cam.setRotation(new Quaternion(-0.07608022f, 0.9086564f, -0.18992864f, -0.3639813f));
+        flyCam.setMoveSpeed(10);
+    }
+    
+    private void createDebugTangents(Geometry geom) {
+        Geometry debug = new Geometry(
+                "Debug " + geom.getName(),
+                TangentBinormalGenerator.genTbnLines(geom.getMesh(), 0.8f)
+        );
+        Material debugMat = assetManager.loadMaterial("Common/Materials/VertexColor.j3m");
+        debug.setMaterial(debugMat);
+        debug.setCullHint(Spatial.CullHint.Never);
+        debug.getLocalTranslation().set(geom.getWorldTranslation());
+        debugNode.attachChild(debug);
+    }
+}

+ 6 - 6
jme3-examples/src/main/java/jme3test/model/shape/TestDebugShapes.java

@@ -50,10 +50,11 @@ public class TestDebugShapes extends SimpleApplication {
         app.start();
     }
 
-    public Geometry putShape(Mesh shape, ColorRGBA color){
+    public Geometry putShape(Mesh shape, ColorRGBA color, float lineWidth){
         Geometry g = new Geometry("shape", shape);
         Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
         mat.getAdditionalRenderState().setWireframe(true);
+        mat.getAdditionalRenderState().setLineWidth(lineWidth);
         mat.setColor("Color", color);
         g.setMaterial(mat);
         rootNode.attachChild(g);
@@ -62,20 +63,19 @@ public class TestDebugShapes extends SimpleApplication {
 
     public void putArrow(Vector3f pos, Vector3f dir, ColorRGBA color){
         Arrow arrow = new Arrow(dir);
-        arrow.setLineWidth(4); // make arrow thicker
-        putShape(arrow, color).setLocalTranslation(pos);
+        putShape(arrow, color, 4).setLocalTranslation(pos);
     }
 
     public void putBox(Vector3f pos, float size, ColorRGBA color){
-        putShape(new WireBox(size, size, size), color).setLocalTranslation(pos);
+        putShape(new WireBox(size, size, size), color, 1).setLocalTranslation(pos);
     }
 
     public void putGrid(Vector3f pos, ColorRGBA color){
-        putShape(new Grid(6, 6, 0.2f), color).center().move(pos);
+        putShape(new Grid(6, 6, 0.2f), color, 1).center().move(pos);
     }
 
     public void putSphere(Vector3f pos, ColorRGBA color){
-        putShape(new WireSphere(1), color).setLocalTranslation(pos);
+        putShape(new WireSphere(1), color, 1).setLocalTranslation(pos);
     }
 
     @Override

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

+ 87 - 0
jme3-examples/src/main/java/jme3test/scene/TestLineWidthRenderState.java

@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2009-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 jme3test.scene;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.input.KeyInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.shape.Box;
+
+public class TestLineWidthRenderState extends SimpleApplication {
+
+    private Material mat;
+
+    public static void main(String[] args){
+        TestLineWidthRenderState app = new TestLineWidthRenderState();
+        app.start();
+    }
+
+
+
+    @Override
+    public void simpleInitApp() {
+        setDisplayFps(false);
+        setDisplayStatView(false);
+        cam.setLocation(new Vector3f(5.5826545f, 3.6192513f, 8.016988f));
+        cam.setRotation(new Quaternion(-0.04787097f, 0.9463123f, -0.16569641f, -0.27339742f));
+
+        Box b = new Box(1, 1, 1);
+        Geometry geom = new Geometry("Box", b);
+        mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        mat.setColor("Color", ColorRGBA.Blue);
+        mat.getAdditionalRenderState().setWireframe(true);
+        mat.getAdditionalRenderState().setLineWidth(2);
+        geom.setMaterial(mat);
+        rootNode.attachChild(geom);
+
+        inputManager.addListener(new ActionListener() {
+            @Override
+            public void onAction(String name, boolean isPressed, float tpf) {
+                if(name.equals("up") && isPressed){
+                    mat.getAdditionalRenderState().setLineWidth(mat.getAdditionalRenderState().getLineWidth() + 1);
+                }
+                if(name.equals("down") && isPressed){
+                    mat.getAdditionalRenderState().setLineWidth(Math.max(mat.getAdditionalRenderState().getLineWidth() - 1, 1));
+                }
+            }
+        }, "up", "down");
+        inputManager.addMapping("up", new KeyTrigger(KeyInput.KEY_U));
+        inputManager.addMapping("down", new KeyTrigger(KeyInput.KEY_J));
+    }
+}

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

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

@@ -0,0 +1,82 @@
+/*
+ * 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");
+        NAME_MAP.put( "lineWidth", "LineWidth");
+    }
+    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);
+        }
+    }
+
+}

Some files were not shown because too many files changed in this diff