瀏覽代碼

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

jmekaelthas 9 年之前
父節點
當前提交
c07ef80d43
共有 100 個文件被更改,包括 6713 次插入3114 次删除
  1. 13 128
      .gitignore
  2. 6 0
      common.gradle
  3. 3 3
      jme3-android/src/main/java/com/jme3/app/AndroidHarness.java
  4. 2 2
      jme3-android/src/main/java/com/jme3/app/AndroidHarnessFragment.java
  5. 2 0
      jme3-bullet-native/src/native/cpp/jmeClasses.cpp
  6. 1 0
      jme3-bullet-native/src/native/cpp/jmeClasses.h
  7. 22 2
      jme3-bullet-native/src/native/cpp/jmePhysicsSpace.cpp
  8. 15 0
      jme3-bullet/src/main/java/com/jme3/bullet/PhysicsSpace.java
  9. 12 17
      jme3-core/src/main/java/com/jme3/animation/EffectTrack.java
  10. 4 1
      jme3-core/src/main/java/com/jme3/animation/SkeletonControl.java
  11. 47 574
      jme3-core/src/main/java/com/jme3/app/Application.java
  12. 793 0
      jme3-core/src/main/java/com/jme3/app/LegacyApplication.java
  13. 17 25
      jme3-core/src/main/java/com/jme3/app/SimpleApplication.java
  14. 30 30
      jme3-core/src/main/java/com/jme3/app/StatsAppState.java
  15. 14 14
      jme3-core/src/main/java/com/jme3/app/StatsView.java
  16. 0 9
      jme3-core/src/main/java/com/jme3/asset/AssetManager.java
  17. 0 33
      jme3-core/src/main/java/com/jme3/asset/DesktopAssetManager.java
  18. 121 96
      jme3-core/src/main/java/com/jme3/audio/AudioNode.java
  19. 13 16
      jme3-core/src/main/java/com/jme3/cinematic/events/MotionEvent.java
  20. 179 134
      jme3-core/src/main/java/com/jme3/effect/ParticleEmitter.java
  21. 33 2
      jme3-core/src/main/java/com/jme3/effect/influencers/DefaultParticleInfluencer.java
  22. 21 0
      jme3-core/src/main/java/com/jme3/effect/influencers/EmptyParticleInfluencer.java
  23. 2 0
      jme3-core/src/main/java/com/jme3/effect/influencers/NewtonianParticleInfluencer.java
  24. 2 1
      jme3-core/src/main/java/com/jme3/effect/influencers/ParticleInfluencer.java
  25. 17 4
      jme3-core/src/main/java/com/jme3/effect/influencers/RadialParticleInfluencer.java
  26. 23 0
      jme3-core/src/main/java/com/jme3/effect/shapes/EmitterBoxShape.java
  27. 24 1
      jme3-core/src/main/java/com/jme3/effect/shapes/EmitterMeshVertexShape.java
  28. 22 0
      jme3-core/src/main/java/com/jme3/effect/shapes/EmitterPointShape.java
  29. 2 1
      jme3-core/src/main/java/com/jme3/effect/shapes/EmitterShape.java
  30. 22 0
      jme3-core/src/main/java/com/jme3/effect/shapes/EmitterSphereShape.java
  31. 36 14
      jme3-core/src/main/java/com/jme3/font/BitmapText.java
  32. 7 0
      jme3-core/src/main/java/com/jme3/font/BitmapTextPage.java
  33. 31 31
      jme3-core/src/main/java/com/jme3/font/Letters.java
  34. 16 0
      jme3-core/src/main/java/com/jme3/light/Light.java
  35. 38 18
      jme3-core/src/main/java/com/jme3/light/LightList.java
  36. 0 4
      jme3-core/src/main/java/com/jme3/material/MatParam.java
  37. 151 0
      jme3-core/src/main/java/com/jme3/material/MatParamOverride.java
  38. 0 6
      jme3-core/src/main/java/com/jme3/material/MatParamTexture.java
  39. 137 355
      jme3-core/src/main/java/com/jme3/material/Material.java
  40. 52 9
      jme3-core/src/main/java/com/jme3/material/RenderState.java
  41. 102 145
      jme3-core/src/main/java/com/jme3/material/Technique.java
  42. 215 72
      jme3-core/src/main/java/com/jme3/material/TechniqueDef.java
  43. 97 0
      jme3-core/src/main/java/com/jme3/material/logic/DefaultTechniqueDefLogic.java
  44. 178 0
      jme3-core/src/main/java/com/jme3/material/logic/MultiPassLightingLogic.java
  45. 218 0
      jme3-core/src/main/java/com/jme3/material/logic/SinglePassLightingLogic.java
  46. 157 0
      jme3-core/src/main/java/com/jme3/material/logic/StaticPassLightingLogic.java
  47. 97 0
      jme3-core/src/main/java/com/jme3/material/logic/TechniqueDefLogic.java
  48. 17 13
      jme3-core/src/main/java/com/jme3/math/Spline.java
  49. 1 1
      jme3-core/src/main/java/com/jme3/renderer/RenderContext.java
  50. 19 10
      jme3-core/src/main/java/com/jme3/renderer/RenderManager.java
  51. 1 0
      jme3-core/src/main/java/com/jme3/renderer/opengl/GL3.java
  52. 6 0
      jme3-core/src/main/java/com/jme3/renderer/opengl/GLDebugDesktop.java
  53. 40 14
      jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java
  54. 10 0
      jme3-core/src/main/java/com/jme3/renderer/queue/GeometryList.java
  55. 3 2
      jme3-core/src/main/java/com/jme3/renderer/queue/OpaqueComparator.java
  56. 17 2
      jme3-core/src/main/java/com/jme3/scene/AssetLinkNode.java
  57. 72 34
      jme3-core/src/main/java/com/jme3/scene/BatchNode.java
  58. 15 1
      jme3-core/src/main/java/com/jme3/scene/CameraNode.java
  59. 108 52
      jme3-core/src/main/java/com/jme3/scene/Geometry.java
  60. 17 3
      jme3-core/src/main/java/com/jme3/scene/LightNode.java
  61. 219 178
      jme3-core/src/main/java/com/jme3/scene/Mesh.java
  62. 84 66
      jme3-core/src/main/java/com/jme3/scene/Node.java
  63. 269 83
      jme3-core/src/main/java/com/jme3/scene/Spatial.java
  64. 1 0
      jme3-core/src/main/java/com/jme3/scene/VertexBuffer.java
  65. 70 57
      jme3-core/src/main/java/com/jme3/scene/instancing/InstancedGeometry.java
  66. 82 43
      jme3-core/src/main/java/com/jme3/scene/instancing/InstancedNode.java
  67. 179 286
      jme3-core/src/main/java/com/jme3/shader/DefineList.java
  68. 6 1
      jme3-core/src/main/java/com/jme3/shader/Glsl100ShaderGenerator.java
  69. 56 23
      jme3-core/src/main/java/com/jme3/shader/Shader.java
  70. 30 17
      jme3-core/src/main/java/com/jme3/shader/ShaderGenerator.java
  71. 0 201
      jme3-core/src/main/java/com/jme3/shader/ShaderKey.java
  72. 82 33
      jme3-core/src/main/java/com/jme3/shader/Uniform.java
  73. 3 2
      jme3-core/src/main/java/com/jme3/shader/UniformBindingManager.java
  74. 1 1
      jme3-core/src/main/java/com/jme3/shader/VarType.java
  75. 4 3
      jme3-core/src/main/java/com/jme3/system/NullContext.java
  76. 1 1
      jme3-core/src/main/java/com/jme3/system/NullRenderer.java
  77. 56 0
      jme3-core/src/main/java/com/jme3/texture/FrameBuffer.java
  78. 48 11
      jme3-core/src/main/java/com/jme3/util/IntMap.java
  79. 91 73
      jme3-core/src/main/java/com/jme3/util/SafeArrayList.java
  80. 141 74
      jme3-core/src/main/java/com/jme3/util/clone/Cloner.java
  81. 6 13
      jme3-core/src/main/resources/Common/MatDefs/Light/Lighting.j3md
  82. 2 3
      jme3-core/src/main/resources/Common/MatDefs/Light/SPLighting.frag
  83. 2 4
      jme3-core/src/main/resources/Common/MatDefs/Light/SPLighting.vert
  84. 2 2
      jme3-core/src/main/resources/Common/MatDefs/Shadow/PostShadowFilter.frag
  85. 1 0
      jme3-core/src/main/resources/com/jme3/system/.gitignore
  86. 0 11
      jme3-core/src/main/resources/com/jme3/system/version.properties
  87. 63 5
      jme3-core/src/plugins/java/com/jme3/material/plugins/J3MLoader.java
  88. 6 4
      jme3-core/src/plugins/java/com/jme3/material/plugins/ShaderNodeLoaderDelegate.java
  89. 52 0
      jme3-core/src/test/java/com/jme3/asset/LoadShaderSourceTest.java
  90. 538 0
      jme3-core/src/test/java/com/jme3/material/MaterialMatParamOverrideTest.java
  91. 38 0
      jme3-core/src/test/java/com/jme3/math/FastMathTest.java
  92. 342 0
      jme3-core/src/test/java/com/jme3/renderer/OpaqueComparatorTest.java
  93. 173 0
      jme3-core/src/test/java/com/jme3/scene/MPOTestUtils.java
  94. 278 0
      jme3-core/src/test/java/com/jme3/scene/SceneMatParamOverrideTest.java
  95. 300 0
      jme3-core/src/test/java/com/jme3/shader/DefineListTest.java
  96. 78 0
      jme3-core/src/test/java/com/jme3/system/MockJmeSystemDelegate.java
  97. 55 0
      jme3-core/src/test/java/com/jme3/system/TestUtil.java
  98. 12 12
      jme3-core/src/tools/java/jme3tools/shadercheck/ShaderCheck.java
  99. 4 4
      jme3-desktop/src/main/java/com/jme3/app/AppletHarness.java
  100. 18 24
      jme3-desktop/src/main/java/com/jme3/system/JmeDesktopSystem.java

+ 13 - 128
.gitignore

@@ -1,34 +1,22 @@
+**/nbproject/private/
 /.gradle/
 /.gradle/
-/.nb-gradle/private/
-/.nb-gradle/profiles/private/
+/.nb-gradle/
 /.idea/
 /.idea/
 /dist/
 /dist/
 /build/
 /build/
+/bin/
 /netbeans/
 /netbeans/
-/sdk/jdks/local/
-/jme3-core/build/
+/.classpath
+/.project
+/.settings
+*.dll
+*.so
+*.jnilib
+*.dylib
+*.iml
+.DS_Store
 /jme3-core/src/main/resources/com/jme3/system/version.properties
 /jme3-core/src/main/resources/com/jme3/system/version.properties
-/jme3-plugins/build/
-/jme3-desktop/build/
-/jme3-android-native/build/
-/jme3-android/build/
-/jme3-android-examples/build/
-/jme3-blender/build/
-/jme3-effects/build/
-/jme3-bullet/build/
-/jme3-terrain/build/
-/jme3-bullet-native/build/
-/jme3-bullet-native-android/build/
-/jme3-jogg/build/
-/jme3-jbullet/build/
-/jme3-lwjgl/build/
-/jme3-networking/build/
-/jme3-niftygui/build/
-/jme3-testdata/build/
-/jme3-examples/build/
-/jme3-jogl/build/
-/jme3-ios/build/
-/jme3-gl-autogen/build/
+/jme3-*/build/
 /jme3-bullet-native/bullet.zip
 /jme3-bullet-native/bullet.zip
 /jme3-bullet-native/bullet-2.82-r2704/
 /jme3-bullet-native/bullet-2.82-r2704/
 /jme3-android-native/openal-soft/
 /jme3-android-native/openal-soft/
@@ -38,112 +26,9 @@
 /jme3-android-native/src/native/jme_decode/com_jme3_audio_plugins_NativeVorbisFile.h
 /jme3-android-native/src/native/jme_decode/com_jme3_audio_plugins_NativeVorbisFile.h
 /jme3-android-native/src/native/jme_decode/com_jme3_texture_plugins_AndroidNativeImageLoader.h
 /jme3-android-native/src/native/jme_decode/com_jme3_texture_plugins_AndroidNativeImageLoader.h
 /jme3-android-native/stb_image.h
 /jme3-android-native/stb_image.h
-/sdk/jme3-tests-template/src/com/jme3/gde/templates/tests/JmeTestsProject.zip
-/sdk/jme3-tests-template/src/com/jme3/gde/templates/tests/JME3TestsAndroidProject.zip
-/sdk/jme3-project-testdata/release/
-/sdk/JME3TestsTemplateAndroid/src/jme3test/
-/sdk/JME3TestsTemplate/src/jme3test/
-/sdk/build/
-/sdk/jme3-core-baselibs/release/
-/sdk/jme3-core-libraries/release/
-/sdk/jme3-project-baselibs/release/
-/sdk/jme3-project-libraries/release/
-/sdk/jme3-codepalette/build/
-/sdk/jme3-core-libraries/build/
-/sdk/jme3-code-check/build/
-/sdk/jme3-core-baselibs/build/
-/sdk/jme3-documentation/build/
-/sdk/jme3-core-updatecenters/build/
-/sdk/jme3-project-testdata/build/
-/sdk/jme3-project-libraries/build/
-/sdk/jme3-project-baselibs/build/
-/sdk/jme3-templates/build/
-/sdk/jme3-texture-editor/build/
-/sdk/jme3-tests-template/build/
-/sdk/jme3-upgrader/build/
-/sdk/jme3-core/build/
-/sdk/jme3-obfuscate/build/
-/sdk/jme3-gui/build/
-/sdk/jme3-cinematics/build/
-/sdk/jme3-terrain-editor/build/
-/sdk/jme3-lwjgl-applet/build/
-/sdk/jme3-blender/build/
-/sdk/jme3-navmesh-gen/build/
-/sdk/jme3-angelfont/build/
-/sdk/jme3-materialeditor/build/
-/sdk/jme3-android/build/
-/sdk/jme3-desktop-executables/build/
-/sdk/jme3-ogrexml/build/
-/sdk/jme3-ogretools/build/
-/sdk/jme3-scenecomposer/build/
-/sdk/jme3-assetpack-support/build/
-/sdk/jme3-model-importer/build/
-/sdk/jme3-wavefront/build/
-/sdk/jme3-vehicle-creator/build/
-/sdk/jme3-welcome-screen/build/
-/sdk/jme3-glsl-support/build/
-/sdk/jme3-dark-laf/build/
-/sdk/nbproject/private/
-/sdk/jme3-scenecomposer/nbproject/private/
-/sdk/jme3-core/nbproject/private/
-/sdk/jme3-core-baselibs/nbproject/private/
-/sdk/jme3-welcome-screen/nbproject/private/
-/sdk/jme3-lwjgl-applet/nbproject/private/
-/sdk/jme3-ogrexml/nbproject/private/
-/sdk/jme3-upgrader/nbproject/private/
-/sdk/jme3-obfuscate/nbproject/private/
-/sdk/jme3-navmesh-gen/nbproject/private/
-/sdk/jme3-wavefront/nbproject/private/
-/sdk/jme3-project-libraries/nbproject/private/
-/sdk/jme3-ogretools/nbproject/private/
-/sdk/jme3-assetpack-support/nbproject/private/
-/sdk/jme3-cinematics/nbproject/private/
-/sdk/jme3-model-importer/nbproject/private/
-/sdk/jme3-desktop-executables/nbproject/private/
-/sdk/jme3-glsl-support/nbproject/private/
-/sdk/jme3-android/nbproject/private/
-/sdk/jme3-angelfont/nbproject/private/
-/sdk/jme3-codepalette/nbproject/private/
-/sdk/jme3-documentation/nbproject/private/
-/sdk/jme3-vehicle-creator/nbproject/private/
-/sdk/jme3-code-check/nbproject/private/
-/sdk/jme3-blender/nbproject/private/
-/sdk/jme3-core-libraries/nbproject/private/
-/sdk/jme3-core-updatecenters/nbproject/private/
-/sdk/jme3-gui/nbproject/private/
-/sdk/jme3-materialeditor/nbproject/private/
-/sdk/jme3-project-baselibs/nbproject/private/
-/sdk/jme3-project-testdata/nbproject/private/
-/sdk/jme3-templates/nbproject/private/
-/sdk/jme3-terrain-editor/nbproject/private/
-/sdk/jme3-tests-template/nbproject/private/
-/sdk/jme3-texture-editor/nbproject/private/
-/sdk/JME3TestsTemplate/nbproject/private/
-/sdk/JME3TestsTemplateAndroid/nbproject/private/
-/bin
-/.classpath
-/.project
-/.settings
-*.dll
-*.so
-*.jnilib
-*.dylib
-*.iml
-/sdk/dist/
 !/jme3-bullet-native/libs/native/windows/x86_64/bulletjme.dll
 !/jme3-bullet-native/libs/native/windows/x86_64/bulletjme.dll
 !/jme3-bullet-native/libs/native/windows/x86/bulletjme.dll
 !/jme3-bullet-native/libs/native/windows/x86/bulletjme.dll
 !/jme3-bullet-native/libs/native/osx/x86/libbulletjme.dylib
 !/jme3-bullet-native/libs/native/osx/x86/libbulletjme.dylib
 !/jme3-bullet-native/libs/native/osx/x86_64/libbulletjme.dylib
 !/jme3-bullet-native/libs/native/osx/x86_64/libbulletjme.dylib
 !/jme3-bullet-native/libs/native/linux/x86/libbulletjme.so
 !/jme3-bullet-native/libs/native/linux/x86/libbulletjme.so
 !/jme3-bullet-native/libs/native/linux/x86_64/libbulletjme.so
 !/jme3-bullet-native/libs/native/linux/x86_64/libbulletjme.so
-/.nb-gradle/
-/sdk/ant-jme/nbproject/private/
-/sdk/nbi/stub/ext/engine/nbproject/private/
-/sdk/nbi/stub/ext/components/products/jdk/nbproject/private/
-/sdk/nbi/stub/ext/components/products/blender/nbproject/private/
-/sdk/nbi/stub/ext/components/products/helloworld/nbproject/private/
-/sdk/BasicGameTemplate/nbproject/private/
-/sdk/nbi/stub/ext/components/products/jdk/build/
-/sdk/nbi/stub/ext/components/products/jdk/dist/
-/sdk/jme3-dark-laf/nbproject/private/
-jme3-lwjgl3/build/

+ 6 - 0
common.gradle

@@ -51,6 +51,12 @@ javadoc {
     }
     }
 }
 }
 
 
+test {
+    testLogging {
+        exceptionFormat = 'full'
+    }
+}
+
 task sourcesJar(type: Jar, dependsOn: classes, description: 'Creates a jar from the source files.') {
 task sourcesJar(type: Jar, dependsOn: classes, description: 'Creates a jar from the source files.') {
     classifier = 'sources'
     classifier = 'sources'
     from sourceSets*.allSource
     from sourceSets*.allSource

+ 3 - 3
jme3-android/src/main/java/com/jme3/app/AndroidHarness.java

@@ -50,7 +50,7 @@ public class AndroidHarness extends Activity implements TouchListener, DialogInt
     /**
     /**
      * The jme3 application object
      * The jme3 application object
      */
      */
-    protected Application app = null;
+    protected LegacyApplication app = null;
 
 
     /**
     /**
      * Sets the desired RGB size for the surfaceview.  16 = RGB565, 24 = RGB888.
      * Sets the desired RGB size for the surfaceview.  16 = RGB565, 24 = RGB888.
@@ -178,7 +178,7 @@ public class AndroidHarness extends Activity implements TouchListener, DialogInt
     private boolean inConfigChange = false;
     private boolean inConfigChange = false;
 
 
     private class DataObject {
     private class DataObject {
-        protected Application app = null;
+        protected LegacyApplication app = null;
     }
     }
 
 
     @Override
     @Override
@@ -241,7 +241,7 @@ public class AndroidHarness extends Activity implements TouchListener, DialogInt
             try {
             try {
                 if (app == null) {
                 if (app == null) {
                     @SuppressWarnings("unchecked")
                     @SuppressWarnings("unchecked")
-                    Class<? extends Application> clazz = (Class<? extends Application>) Class.forName(appClass);
+                    Class<? extends LegacyApplication> clazz = (Class<? extends LegacyApplication>) Class.forName(appClass);
                     app = clazz.newInstance();
                     app = clazz.newInstance();
                 }
                 }
 
 

+ 2 - 2
jme3-android/src/main/java/com/jme3/app/AndroidHarnessFragment.java

@@ -207,7 +207,7 @@ public class AndroidHarnessFragment extends Fragment implements
     protected ImageView splashImageView = null;
     protected ImageView splashImageView = null;
     final private String ESCAPE_EVENT = "TouchEscape";
     final private String ESCAPE_EVENT = "TouchEscape";
     private boolean firstDrawFrame = true;
     private boolean firstDrawFrame = true;
-    private Application app = null;
+    private LegacyApplication app = null;
     private int viewWidth = 0;
     private int viewWidth = 0;
     private int viewHeight = 0;
     private int viewHeight = 0;
 
 
@@ -258,7 +258,7 @@ public class AndroidHarnessFragment extends Fragment implements
         try {
         try {
             if (app == null) {
             if (app == null) {
                 @SuppressWarnings("unchecked")
                 @SuppressWarnings("unchecked")
-                Class<? extends Application> clazz = (Class<? extends Application>) Class.forName(appClass);
+                Class<? extends LegacyApplication> clazz = (Class<? extends LegacyApplication>) Class.forName(appClass);
                 app = clazz.newInstance();
                 app = clazz.newInstance();
             }
             }
 
 

+ 2 - 0
jme3-bullet-native/src/native/cpp/jmeClasses.cpp

@@ -40,6 +40,7 @@ jclass jmeClasses::PhysicsSpace;
 jmethodID jmeClasses::PhysicsSpace_preTick;
 jmethodID jmeClasses::PhysicsSpace_preTick;
 jmethodID jmeClasses::PhysicsSpace_postTick;
 jmethodID jmeClasses::PhysicsSpace_postTick;
 jmethodID jmeClasses::PhysicsSpace_addCollisionEvent;
 jmethodID jmeClasses::PhysicsSpace_addCollisionEvent;
+jmethodID jmeClasses::PhysicsSpace_notifyCollisionGroupListeners;
 
 
 jclass jmeClasses::PhysicsGhostObject;
 jclass jmeClasses::PhysicsGhostObject;
 jmethodID jmeClasses::PhysicsGhostObject_addOverlappingObject;
 jmethodID jmeClasses::PhysicsGhostObject_addOverlappingObject;
@@ -137,6 +138,7 @@ void jmeClasses::initJavaClasses(JNIEnv* env) {
     PhysicsSpace_preTick = env->GetMethodID(PhysicsSpace, "preTick_native", "(F)V");
     PhysicsSpace_preTick = env->GetMethodID(PhysicsSpace, "preTick_native", "(F)V");
     PhysicsSpace_postTick = env->GetMethodID(PhysicsSpace, "postTick_native", "(F)V");
     PhysicsSpace_postTick = env->GetMethodID(PhysicsSpace, "postTick_native", "(F)V");
     PhysicsSpace_addCollisionEvent = env->GetMethodID(PhysicsSpace, "addCollisionEvent_native","(Lcom/jme3/bullet/collision/PhysicsCollisionObject;Lcom/jme3/bullet/collision/PhysicsCollisionObject;J)V");
     PhysicsSpace_addCollisionEvent = env->GetMethodID(PhysicsSpace, "addCollisionEvent_native","(Lcom/jme3/bullet/collision/PhysicsCollisionObject;Lcom/jme3/bullet/collision/PhysicsCollisionObject;J)V");
+    PhysicsSpace_notifyCollisionGroupListeners = env->GetMethodID(PhysicsSpace, "notifyCollisionGroupListeners_native","(Lcom/jme3/bullet/collision/PhysicsCollisionObject;Lcom/jme3/bullet/collision/PhysicsCollisionObject;)Z");
     if (env->ExceptionCheck()) {
     if (env->ExceptionCheck()) {
         env->Throw(env->ExceptionOccurred());
         env->Throw(env->ExceptionOccurred());
         return;
         return;

+ 1 - 0
jme3-bullet-native/src/native/cpp/jmeClasses.h

@@ -46,6 +46,7 @@ public:
     static jmethodID PhysicsSpace_addCollisionEvent;
     static jmethodID PhysicsSpace_addCollisionEvent;
     static jclass PhysicsGhostObject;
     static jclass PhysicsGhostObject;
     static jmethodID PhysicsGhostObject_addOverlappingObject;
     static jmethodID PhysicsGhostObject_addOverlappingObject;
+    static jmethodID PhysicsSpace_notifyCollisionGroupListeners;
 
 
     static jclass Vector3f;
     static jclass Vector3f;
     static jmethodID Vector3f_set;
     static jmethodID Vector3f_set;

+ 22 - 2
jme3-bullet-native/src/native/cpp/jmePhysicsSpace.cpp

@@ -187,8 +187,28 @@ void jmePhysicsSpace::createPhysicsSpace(jfloat minX, jfloat minY, jfloat minZ,
                 jmeUserPointer *up0 = (jmeUserPointer*) co0 -> getUserPointer();
                 jmeUserPointer *up0 = (jmeUserPointer*) co0 -> getUserPointer();
                 jmeUserPointer *up1 = (jmeUserPointer*) co1 -> getUserPointer();
                 jmeUserPointer *up1 = (jmeUserPointer*) co1 -> getUserPointer();
                 if (up0 != NULL && up1 != NULL) {
                 if (up0 != NULL && up1 != NULL) {
-                    collides = (up0->group & up1->groups) != 0;
-                    collides = collides && (up1->group & up0->groups);
+                    collides = (up0->group & up1->groups) != 0 || (up1->group & up0->groups) != 0;
+                    
+                    if(collides){
+                        jmePhysicsSpace *dynamicsWorld = (jmePhysicsSpace *)up0->space;
+                        JNIEnv* env = dynamicsWorld->getEnv();
+                        jobject javaPhysicsSpace = env->NewLocalRef(dynamicsWorld->getJavaPhysicsSpace());
+                        jobject javaCollisionObject0 = env->NewLocalRef(up0->javaCollisionObject);
+                        jobject javaCollisionObject1 = env->NewLocalRef(up1->javaCollisionObject);
+                        
+                        jboolean notifyResult = env->CallBooleanMethod(javaPhysicsSpace, jmeClasses::PhysicsSpace_notifyCollisionGroupListeners, javaCollisionObject0, javaCollisionObject1);
+                        
+                        env->DeleteLocalRef(javaPhysicsSpace);
+                        env->DeleteLocalRef(javaCollisionObject0);
+                        env->DeleteLocalRef(javaCollisionObject1);
+
+                        if (env->ExceptionCheck()) {
+                            env->Throw(env->ExceptionOccurred());
+                            return collides;
+                        }
+                        
+                        collides = (bool) notifyResult;
+                    }
 
 
                     //add some additional logic here that modified 'collides'
                     //add some additional logic here that modified 'collides'
                     return collides;
                     return collides;

+ 15 - 0
jme3-bullet/src/main/java/com/jme3/bullet/PhysicsSpace.java

@@ -335,6 +335,21 @@ public class PhysicsSpace {
 //        System.out.println("addCollisionEvent:"+node.getObjectId()+" "+ node1.getObjectId());
 //        System.out.println("addCollisionEvent:"+node.getObjectId()+" "+ node1.getObjectId());
         collisionEvents.add(eventFactory.getEvent(PhysicsCollisionEvent.TYPE_PROCESSED, node, node1, manifoldPointObjectId));
         collisionEvents.add(eventFactory.getEvent(PhysicsCollisionEvent.TYPE_PROCESSED, node, node1, manifoldPointObjectId));
     }
     }
+    
+    private boolean notifyCollisionGroupListeners_native(PhysicsCollisionObject node, PhysicsCollisionObject node1){
+        PhysicsCollisionGroupListener listener = collisionGroupListeners.get(node.getCollisionGroup());
+        PhysicsCollisionGroupListener listener1 = collisionGroupListeners.get(node1.getCollisionGroup());
+        boolean result = true;
+        
+        if(listener != null){
+            result = listener.collide(node, node1);
+        }
+        if(listener1 != null && node.getCollisionGroup() != node1.getCollisionGroup()){
+            result = listener1.collide(node, node1) && result;
+        }
+        
+        return result;
+    }
 
 
     /**
     /**
      * updates the physics space
      * updates the physics space

+ 12 - 17
jme3-core/src/main/java/com/jme3/animation/EffectTrack.java

@@ -118,22 +118,17 @@ public class EffectTrack implements ClonableTrack {
             }
             }
         }
         }
 
 
-        @Override   
+        @Override
         public Object jmeClone() {
         public Object jmeClone() {
             KillParticleControl c = new KillParticleControl();
             KillParticleControl c = new KillParticleControl();
             //this control should be removed as it shouldn't have been persisted in the first place
             //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. 
+            //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.remove = true;
             c.spatial = spatial;
             c.spatial = spatial;
             return c;
             return c;
-        }     
-
-        @Override   
-        public void cloneFields( Cloner cloner, Object original ) { 
-            this.spatial = cloner.clone(spatial);
         }
         }
-         
+
         @Override
         @Override
         protected void controlRender(RenderManager rm, ViewPort vp) {
         protected void controlRender(RenderManager rm, ViewPort vp) {
         }
         }
@@ -143,8 +138,8 @@ public class EffectTrack implements ClonableTrack {
 
 
             KillParticleControl c = new KillParticleControl();
             KillParticleControl c = new KillParticleControl();
             //this control should be removed as it shouldn't have been persisted in the first place
             //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. 
+            //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.remove = true;
             c.setSpatial(spatial);
             c.setSpatial(spatial);
             return c;
             return c;
@@ -261,7 +256,7 @@ public class EffectTrack implements ClonableTrack {
     public float[] getKeyFrameTimes() {
     public float[] getKeyFrameTimes() {
         return new float[] { startOffset };
         return new float[] { startOffset };
     }
     }
-    
+
     /**
     /**
      * Clone this track
      * Clone this track
      *
      *
@@ -302,21 +297,21 @@ public class EffectTrack implements ClonableTrack {
         return effectTrack;
         return effectTrack;
     }
     }
 
 
-    @Override   
+    @Override
     public Object jmeClone() {
     public Object jmeClone() {
         try {
         try {
             return super.clone();
             return super.clone();
         } catch( CloneNotSupportedException e ) {
         } catch( CloneNotSupportedException e ) {
             throw new RuntimeException("Error cloning", e);
             throw new RuntimeException("Error cloning", e);
         }
         }
-    }     
+    }
 
 
 
 
-    @Override   
-    public void cloneFields( Cloner cloner, Object original ) { 
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) {
         this.emitter = cloner.clone(emitter);
         this.emitter = cloner.clone(emitter);
     }
     }
-         
+
     /**
     /**
      * recursive function responsible for finding the newly cloned Emitter
      * recursive function responsible for finding the newly cloned Emitter
      *
      *

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

@@ -111,7 +111,7 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl
      * Material references used for hardware skinning
      * Material references used for hardware skinning
      */
      */
     private Set<Material> materials = new HashSet<Material>();
     private Set<Material> materials = new HashSet<Material>();
-
+    
     /**
     /**
      * Serialization only. Do not use.
      * Serialization only. Do not use.
      */
      */
@@ -204,6 +204,9 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl
      * @param skeleton the skeleton
      * @param skeleton the skeleton
      */
      */
     public SkeletonControl(Skeleton skeleton) {
     public SkeletonControl(Skeleton skeleton) {
+        if (skeleton == null) {
+            throw new IllegalArgumentException("skeleton cannot be null");
+        }
         this.skeleton = skeleton;
         this.skeleton = skeleton;
     }
     }
 
 

+ 47 - 574
jme3-core/src/main/java/com/jme3/app/Application.java

@@ -33,112 +33,53 @@ package com.jme3.app;
 
 
 import com.jme3.app.state.AppStateManager;
 import com.jme3.app.state.AppStateManager;
 import com.jme3.asset.AssetManager;
 import com.jme3.asset.AssetManager;
-import com.jme3.audio.AudioContext;
 import com.jme3.audio.AudioRenderer;
 import com.jme3.audio.AudioRenderer;
 import com.jme3.audio.Listener;
 import com.jme3.audio.Listener;
-import com.jme3.input.*;
-import com.jme3.math.Vector3f;
+import com.jme3.input.InputManager;
 import com.jme3.profile.AppProfiler;
 import com.jme3.profile.AppProfiler;
-import com.jme3.profile.AppStep;
 import com.jme3.renderer.Camera;
 import com.jme3.renderer.Camera;
 import com.jme3.renderer.RenderManager;
 import com.jme3.renderer.RenderManager;
 import com.jme3.renderer.Renderer;
 import com.jme3.renderer.Renderer;
 import com.jme3.renderer.ViewPort;
 import com.jme3.renderer.ViewPort;
 import com.jme3.system.*;
 import com.jme3.system.*;
-import com.jme3.system.JmeContext.Type;
-import java.net.MalformedURLException;
-import java.net.URL;
 import java.util.concurrent.Callable;
 import java.util.concurrent.Callable;
-import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.Future;
 import java.util.concurrent.Future;
-import java.util.logging.Level;
-import java.util.logging.Logger;
 
 
 /**
 /**
- * The <code>Application</code> class represents an instance of a
- * real-time 3D rendering jME application.
- *
- * An <code>Application</code> provides all the tools that are commonly used in jME3
- * applications.
- *
- * jME3 applications *SHOULD NOT EXTEND* this class but extend {@link com.jme3.app.SimpleApplication} instead.
- *
+ * The <code>Application</code> interface represents the minimum exposed
+ * capabilities of a concrete jME3 application.
  */
  */
-public class Application implements SystemListener {
-
-    private static final Logger logger = Logger.getLogger(Application.class.getName());
-
-    protected AssetManager assetManager;
-
-    protected AudioRenderer audioRenderer;
-    protected Renderer renderer;
-    protected RenderManager renderManager;
-    protected ViewPort viewPort;
-    protected ViewPort guiViewPort;
-
-    protected JmeContext context;
-    protected AppSettings settings;
-    protected Timer timer = new NanoTimer();
-    protected Camera cam;
-    protected Listener listener;
-
-    protected boolean inputEnabled = true;
-    protected LostFocusBehavior lostFocusBehavior = LostFocusBehavior.ThrottleOnLostFocus;
-    protected float speed = 1f;
-    protected boolean paused = false;
-    protected MouseInput mouseInput;
-    protected KeyInput keyInput;
-    protected JoyInput joyInput;
-    protected TouchInput touchInput;
-    protected InputManager inputManager;
-    protected AppStateManager stateManager;
-
-    protected AppProfiler prof;
-
-    private final ConcurrentLinkedQueue<AppTask<?>> taskQueue = new ConcurrentLinkedQueue<AppTask<?>>();
-
-    /**
-     * Create a new instance of <code>Application</code>.
-     */
-    public Application(){
-        initStateManager();
-    }
+public interface Application {
 
 
     /**
     /**
      * Determine the application's behavior when unfocused.
      * Determine the application's behavior when unfocused.
-     * 
+     *
      * @return The lost focus behavior of the application.
      * @return The lost focus behavior of the application.
      */
      */
-    public LostFocusBehavior getLostFocusBehavior() {
-        return lostFocusBehavior;
-    }
-    
+    public LostFocusBehavior getLostFocusBehavior();
+
     /**
     /**
      * Change the application's behavior when unfocused.
      * Change the application's behavior when unfocused.
-     * 
-     * By default, the application will 
-     * {@link LostFocusBehavior#ThrottleOnLostFocus throttle the update loop} 
+     *
+     * By default, the application will
+     * {@link LostFocusBehavior#ThrottleOnLostFocus throttle the update loop}
      * so as to not take 100% CPU usage when it is not in focus, e.g.
      * so as to not take 100% CPU usage when it is not in focus, e.g.
      * alt-tabbed, minimized, or obstructed by another window.
      * alt-tabbed, minimized, or obstructed by another window.
-     * 
+     *
      * @param lostFocusBehavior The new lost focus behavior to use.
      * @param lostFocusBehavior The new lost focus behavior to use.
-     * 
+     *
      * @see LostFocusBehavior
      * @see LostFocusBehavior
      */
      */
-    public void setLostFocusBehavior(LostFocusBehavior lostFocusBehavior) {
-        this.lostFocusBehavior = lostFocusBehavior;
-    }
-    
+    public void setLostFocusBehavior(LostFocusBehavior lostFocusBehavior);
+
     /**
     /**
      * Returns true if pause on lost focus is enabled, false otherwise.
      * Returns true if pause on lost focus is enabled, false otherwise.
      *
      *
      * @return true if pause on lost focus is enabled
      * @return true if pause on lost focus is enabled
      *
      *
-     * @see #getLostFocusBehavior() 
+     * @see #getLostFocusBehavior()
      */
      */
-    public boolean isPauseOnLostFocus() {
-        return getLostFocusBehavior() == LostFocusBehavior.PauseOnLostFocus;
-    }
+    public boolean isPauseOnLostFocus();
 
 
     /**
     /**
      * Enable or disable pause on lost focus.
      * Enable or disable pause on lost focus.
@@ -153,52 +94,10 @@ public class Application implements SystemListener {
      *
      *
      * @param pauseOnLostFocus True to enable pause on lost focus, false
      * @param pauseOnLostFocus True to enable pause on lost focus, false
      * otherwise.
      * otherwise.
-     * 
+     *
      * @see #setLostFocusBehavior(com.jme3.app.LostFocusBehavior)
      * @see #setLostFocusBehavior(com.jme3.app.LostFocusBehavior)
      */
      */
-    public void setPauseOnLostFocus(boolean pauseOnLostFocus) {
-        if (pauseOnLostFocus) {
-            setLostFocusBehavior(LostFocusBehavior.PauseOnLostFocus);
-        } else {
-            setLostFocusBehavior(LostFocusBehavior.Disabled);
-        }
-    }
-
-    @Deprecated
-    public void setAssetManager(AssetManager assetManager){
-        if (this.assetManager != null)
-            throw new IllegalStateException("Can only set asset manager"
-                                          + " before initialization.");
-
-        this.assetManager = assetManager;
-    }
-
-    private void initAssetManager(){
-        URL assetCfgUrl = null;
-        
-        if (settings != null){
-            String assetCfg = settings.getString("AssetConfigURL");
-            if (assetCfg != null){
-                try {
-                    assetCfgUrl = new URL(assetCfg);
-                } catch (MalformedURLException ex) {
-                }
-                if (assetCfgUrl == null) {
-                    assetCfgUrl = Application.class.getClassLoader().getResource(assetCfg);
-                    if (assetCfgUrl == null) {
-                        logger.log(Level.SEVERE, "Unable to access AssetConfigURL in asset config:{0}", assetCfg);
-                        return;
-                    }
-                }
-            }
-        }
-        if (assetCfgUrl == null) {
-            assetCfgUrl = JmeSystem.getPlatformAssetConfigURL();
-        }
-        if (assetManager == null){
-            assetManager = JmeSystem.newAssetManager(assetCfgUrl);
-        }
-    }
+    public void setPauseOnLostFocus(boolean pauseOnLostFocus);
 
 
     /**
     /**
      * Set the display settings to define the display created.
      * Set the display settings to define the display created.
@@ -210,321 +109,83 @@ public class Application implements SystemListener {
      *
      *
      * @param settings The settings to set.
      * @param settings The settings to set.
      */
      */
-    public void setSettings(AppSettings settings){
-        this.settings = settings;
-        if (context != null && settings.useInput() != inputEnabled){
-            // may need to create or destroy input based
-            // on settings change
-            inputEnabled = !inputEnabled;
-            if (inputEnabled){
-                initInput();
-            }else{
-                destroyInput();
-            }
-        }else{
-            inputEnabled = settings.useInput();
-        }
-    }
+    public void setSettings(AppSettings settings);
 
 
     /**
     /**
      * Sets the Timer implementation that will be used for calculating
      * Sets the Timer implementation that will be used for calculating
      * frame times.  By default, Application will use the Timer as returned
      * frame times.  By default, Application will use the Timer as returned
      * by the current JmeContext implementation.
      * by the current JmeContext implementation.
      */
      */
-    public void setTimer(Timer timer){
-        this.timer = timer;
-
-        if (timer != null) {
-            timer.reset();
-        }
-
-        if (renderManager != null) {
-            renderManager.setTimer(timer);
-        }
-    }
-
-    public Timer getTimer(){
-        return timer;
-    }
-
-    private void initDisplay(){
-        // aquire important objects
-        // from the context
-        settings = context.getSettings();
-
-        // Only reset the timer if a user has not already provided one
-        if (timer == null) {
-            timer = context.getTimer();
-        }
-
-        renderer = context.getRenderer();
-    }
-
-    private void initAudio(){
-        if (settings.getAudioRenderer() != null && context.getType() != Type.Headless){
-            audioRenderer = JmeSystem.newAudioRenderer(settings);
-            audioRenderer.initialize();
-            AudioContext.setAudioRenderer(audioRenderer);
-
-            listener = new Listener();
-            audioRenderer.setListener(listener);
-        }
-    }
-
-    /**
-     * Creates the camera to use for rendering. Default values are perspective
-     * projection with 45° field of view, with near and far values 1 and 1000
-     * units respectively.
-     */
-    private void initCamera(){
-        cam = new Camera(settings.getWidth(), settings.getHeight());
-
-        cam.setFrustumPerspective(45f, (float)cam.getWidth() / cam.getHeight(), 1f, 1000f);
-        cam.setLocation(new Vector3f(0f, 0f, 10f));
-        cam.lookAt(new Vector3f(0f, 0f, 0f), Vector3f.UNIT_Y);
-
-        renderManager = new RenderManager(renderer);
-        //Remy - 09/14/2010 setted the timer in the renderManager
-        renderManager.setTimer(timer);
-        
-        if (prof != null) {
-            renderManager.setAppProfiler(prof);
-        }
-        
-        viewPort = renderManager.createMainView("Default", cam);
-        viewPort.setClearFlags(true, true, true);
-
-        // Create a new cam for the gui
-        Camera guiCam = new Camera(settings.getWidth(), settings.getHeight());
-        guiViewPort = renderManager.createPostView("Gui Default", guiCam);
-        guiViewPort.setClearFlags(false, false, false);
-    }
-
-    /**
-     * Initializes mouse and keyboard input. Also
-     * initializes joystick input if joysticks are enabled in the
-     * AppSettings.
-     */
-    private void initInput(){
-        mouseInput = context.getMouseInput();
-        if (mouseInput != null)
-            mouseInput.initialize();
-
-        keyInput = context.getKeyInput();
-        if (keyInput != null)
-            keyInput.initialize();
-
-        touchInput = context.getTouchInput();
-        if (touchInput != null)
-            touchInput.initialize();
-
-        if (!settings.getBoolean("DisableJoysticks")){
-            joyInput = context.getJoyInput();
-            if (joyInput != null)
-                joyInput.initialize();
-        }
+    public void setTimer(Timer timer);
 
 
-        inputManager = new InputManager(mouseInput, keyInput, joyInput, touchInput);
-    }
-
-    private void initStateManager(){
-        stateManager = new AppStateManager(this);
-
-        // Always register a ResetStatsState to make sure
-        // that the stats are cleared every frame
-        stateManager.attach(new ResetStatsState());
-    }
+    public Timer getTimer();
 
 
     /**
     /**
      * @return The {@link AssetManager asset manager} for this application.
      * @return The {@link AssetManager asset manager} for this application.
      */
      */
-    public AssetManager getAssetManager(){
-        return assetManager;
-    }
+    public AssetManager getAssetManager();
 
 
     /**
     /**
      * @return the {@link InputManager input manager}.
      * @return the {@link InputManager input manager}.
      */
      */
-    public InputManager getInputManager(){
-        return inputManager;
-    }
+    public InputManager getInputManager();
 
 
     /**
     /**
      * @return the {@link AppStateManager app state manager}
      * @return the {@link AppStateManager app state manager}
      */
      */
-    public AppStateManager getStateManager() {
-        return stateManager;
-    }
+    public AppStateManager getStateManager();
 
 
     /**
     /**
      * @return the {@link RenderManager render manager}
      * @return the {@link RenderManager render manager}
      */
      */
-    public RenderManager getRenderManager() {
-        return renderManager;
-    }
+    public RenderManager getRenderManager();
 
 
     /**
     /**
      * @return The {@link Renderer renderer} for the application
      * @return The {@link Renderer renderer} for the application
      */
      */
-    public Renderer getRenderer(){
-        return renderer;
-    }
+    public Renderer getRenderer();
 
 
     /**
     /**
      * @return The {@link AudioRenderer audio renderer} for the application
      * @return The {@link AudioRenderer audio renderer} for the application
      */
      */
-    public AudioRenderer getAudioRenderer() {
-        return audioRenderer;
-    }
+    public AudioRenderer getAudioRenderer();
 
 
     /**
     /**
      * @return The {@link Listener listener} object for audio
      * @return The {@link Listener listener} object for audio
      */
      */
-    public Listener getListener() {
-        return listener;
-    }
+    public Listener getListener();
 
 
     /**
     /**
      * @return The {@link JmeContext display context} for the application
      * @return The {@link JmeContext display context} for the application
      */
      */
-    public JmeContext getContext(){
-        return context;
-    }
+    public JmeContext getContext();
 
 
     /**
     /**
-     * @return The {@link Camera camera} for the application
+     * @return The main {@link Camera camera} for the application
      */
      */
-    public Camera getCamera(){
-        return cam;
-    }
-
-    /**
-     * Starts the application in {@link Type#Display display} mode.
-     *
-     * @see #start(com.jme3.system.JmeContext.Type)
-     */
-    public void start(){
-        start(JmeContext.Type.Display, false);
-    }
-    
-    /**
-     * Starts the application in {@link Type#Display display} mode.
-     *
-     * @see #start(com.jme3.system.JmeContext.Type)
-     */
-    public void start(boolean waitFor){
-        start(JmeContext.Type.Display, waitFor);
-    }
+    public Camera getCamera();
 
 
     /**
     /**
      * Starts the application.
      * Starts the application.
-     * Creating a rendering context and executing
-     * the main loop in a separate thread.
      */
      */
-    public void start(JmeContext.Type contextType) {
-        start(contextType, false);
-    }
-    
+    public void start();
+
     /**
     /**
      * Starts the application.
      * Starts the application.
-     * Creating a rendering context and executing
-     * the main loop in a separate thread.
      */
      */
-    public void start(JmeContext.Type contextType, boolean waitFor){
-        if (context != null && context.isCreated()){
-            logger.warning("start() called when application already created!");
-            return;
-        }
-
-        if (settings == null){
-            settings = new AppSettings(true);
-        }
-
-        logger.log(Level.FINE, "Starting application: {0}", getClass().getName());
-        context = JmeSystem.newContext(settings, contextType);
-        context.setSystemListener(this);
-        context.create(waitFor);
-    }
+    public void start(boolean waitFor);
 
 
     /**
     /**
      * Sets an AppProfiler hook that will be called back for
      * Sets an AppProfiler hook that will be called back for
      * specific steps within a single update frame.  Value defaults
      * specific steps within a single update frame.  Value defaults
      * to null.
      * to null.
      */
      */
-    public void setAppProfiler(AppProfiler prof) {
-        this.prof = prof;
-        if (renderManager != null) {
-            renderManager.setAppProfiler(prof);
-        }
-    }
- 
-    /**
-     * Returns the current AppProfiler hook, or null if none is set.
-     */   
-    public AppProfiler getAppProfiler() {
-        return prof;
-    }
+    public void setAppProfiler(AppProfiler prof);
 
 
     /**
     /**
-     * Initializes the application's canvas for use.
-     * <p>
-     * After calling this method, cast the {@link #getContext() context} to
-     * {@link JmeCanvasContext},
-     * then acquire the canvas with {@link JmeCanvasContext#getCanvas() }
-     * and attach it to an AWT/Swing Frame.
-     * The rendering thread will start when the canvas becomes visible on
-     * screen, however if you wish to start the context immediately you
-     * may call {@link #startCanvas() } to force the rendering thread
-     * to start.
-     *
-     * @see JmeCanvasContext
-     * @see Type#Canvas
-     */
-    public void createCanvas(){
-        if (context != null && context.isCreated()){
-            logger.warning("createCanvas() called when application already created!");
-            return;
-        }
-
-        if (settings == null){
-            settings = new AppSettings(true);
-        }
-
-        logger.log(Level.FINE, "Starting application: {0}", getClass().getName());
-        context = JmeSystem.newContext(settings, JmeContext.Type.Canvas);
-        context.setSystemListener(this);
-    }
-
-    /**
-     * Starts the rendering thread after createCanvas() has been called.
-     * <p>
-     * Same as calling startCanvas(false)
-     *
-     * @see #startCanvas(boolean)
-     */
-    public void startCanvas(){
-        startCanvas(false);
-    }
-
-    /**
-     * Starts the rendering thread after createCanvas() has been called.
-     * <p>
-     * Calling this method is optional, the canvas will start automatically
-     * when it becomes visible.
-     *
-     * @param waitFor If true, the current thread will block until the
-     * rendering thread is running
-     */
-    public void startCanvas(boolean waitFor){
-        context.create(waitFor);
-    }
-
-    /**
-     * Internal use only.
+     * Returns the current AppProfiler hook, or null if none is set.
      */
      */
-    public void reshape(int w, int h){
-        renderManager.notifyReshape(w, h);
-    }
+    public AppProfiler getAppProfiler();
 
 
     /**
     /**
      * Restarts the context, applying any changed settings.
      * Restarts the context, applying any changed settings.
@@ -533,10 +194,7 @@ public class Application implements SystemListener {
      * applied immediately; calling this method forces the context
      * applied immediately; calling this method forces the context
      * to restart, applying the new settings.
      * to restart, applying the new settings.
      */
      */
-    public void restart(){
-        context.setSettings(settings);
-        context.restart();
-    }
+    public void restart();
 
 
     /**
     /**
      * Requests the context to close, shutting down the main loop
      * Requests the context to close, shutting down the main loop
@@ -546,102 +204,14 @@ public class Application implements SystemListener {
      *
      *
      * @see #stop(boolean)
      * @see #stop(boolean)
      */
      */
-    public void stop(){
-        stop(false);
-    }
+    public void stop();
 
 
     /**
     /**
      * Requests the context to close, shutting down the main loop
      * Requests the context to close, shutting down the main loop
      * and making necessary cleanup operations.
      * and making necessary cleanup operations.
      * After the application has stopped, it cannot be used anymore.
      * After the application has stopped, it cannot be used anymore.
      */
      */
-    public void stop(boolean waitFor){
-        logger.log(Level.FINE, "Closing application: {0}", getClass().getName());
-        context.destroy(waitFor);
-    }
-
-    /**
-     * Do not call manually.
-     * Callback from ContextListener.
-     * <p>
-     * Initializes the <code>Application</code>, by creating a display and
-     * default camera. If display settings are not specified, a default
-     * 640x480 display is created. Default values are used for the camera;
-     * perspective projection with 45° field of view, with near
-     * and far values 1 and 1000 units respectively.
-     */
-    public void initialize(){
-        if (assetManager == null){
-            initAssetManager();
-        }
-
-        initDisplay();
-        initCamera();
-
-        if (inputEnabled){
-            initInput();
-        }
-        initAudio();
-
-        // update timer so that the next delta is not too large
-//        timer.update();
-        timer.reset();
-
-        // user code here..
-    }
-
-    /**
-     * Internal use only.
-     */
-    public void handleError(String errMsg, Throwable t){
-        // Print error to log.
-        logger.log(Level.SEVERE, errMsg, t);
-        // Display error message on screen if not in headless mode
-        if (context.getType() != JmeContext.Type.Headless) {
-            if (t != null) {
-                JmeSystem.showErrorDialog(errMsg + "\n" + t.getClass().getSimpleName() +
-                        (t.getMessage() != null ? ": " +  t.getMessage() : ""));
-            } else {
-                JmeSystem.showErrorDialog(errMsg);
-            }
-        }
-
-        stop(); // stop the application
-    }
-
-    /**
-     * Internal use only.
-     */
-    public void gainFocus(){
-        if (lostFocusBehavior != LostFocusBehavior.Disabled) {
-            if (lostFocusBehavior == LostFocusBehavior.PauseOnLostFocus) {
-                paused = false;
-            }
-            context.setAutoFlushFrames(true);
-            if (inputManager != null) {
-                inputManager.reset();
-            }
-        }
-    }
-
-    /**
-     * Internal use only.
-     */
-    public void loseFocus(){
-        if (lostFocusBehavior != LostFocusBehavior.Disabled){
-            if (lostFocusBehavior == LostFocusBehavior.PauseOnLostFocus) {
-                paused = true;
-            }
-            context.setAutoFlushFrames(false);
-        }
-    }
-
-    /**
-     * Internal use only.
-     */
-    public void requestClose(boolean esc){
-        context.destroy(false);
-    }
+    public void stop(boolean waitFor);
 
 
     /**
     /**
      * Enqueues a task/callable object to execute in the jME3
      * Enqueues a task/callable object to execute in the jME3
@@ -650,15 +220,11 @@ public class Application implements SystemListener {
      * Callables are executed right at the beginning of the main loop.
      * Callables are executed right at the beginning of the main loop.
      * They are executed even if the application is currently paused
      * They are executed even if the application is currently paused
      * or out of focus.
      * or out of focus.
-     * 
+     *
      * @param callable The callable to run in the main jME3 thread
      * @param callable The callable to run in the main jME3 thread
      */
      */
-    public <V> Future<V> enqueue(Callable<V> callable) {
-        AppTask<V> task = new AppTask<V>(callable);
-        taskQueue.add(task);
-        return task;
-    }
-    
+    public <V> Future<V> enqueue(Callable<V> callable);
+
     /**
     /**
      * Enqueues a runnable object to execute in the jME3
      * Enqueues a runnable object to execute in the jME3
      * rendering thread.
      * rendering thread.
@@ -666,109 +232,16 @@ public class Application implements SystemListener {
      * Runnables are executed right at the beginning of the main loop.
      * Runnables are executed right at the beginning of the main loop.
      * They are executed even if the application is currently paused
      * They are executed even if the application is currently paused
      * or out of focus.
      * or out of focus.
-     * 
+     *
      * @param runnable The runnable to run in the main jME3 thread
      * @param runnable The runnable to run in the main jME3 thread
-     */    
-    public void enqueue(Runnable runnable){
-        enqueue(new RunnableWrapper(runnable));
-    }
-
-    /**
-     * Runs tasks enqueued via {@link #enqueue(Callable)}
-     */
-    protected void runQueuedTasks() {
-	  AppTask<?> task;
-        while( (task = taskQueue.poll()) != null ) {
-            if (!task.isCancelled()) {
-                task.invoke();
-            }
-        }
-    }
-
-    /**
-     * Do not call manually.
-     * Callback from ContextListener.
-     */
-    public void update(){
-        // Make sure the audio renderer is available to callables
-        AudioContext.setAudioRenderer(audioRenderer);
-
-        if (prof!=null) prof.appStep(AppStep.QueuedTasks);
-        runQueuedTasks();
-
-        if (speed == 0 || paused)
-            return;
-
-        timer.update();
-
-        if (inputEnabled){
-            if (prof!=null) prof.appStep(AppStep.ProcessInput);
-            inputManager.update(timer.getTimePerFrame());
-        }
-
-        if (audioRenderer != null){
-            if (prof!=null) prof.appStep(AppStep.ProcessAudio);
-            audioRenderer.update(timer.getTimePerFrame());
-        }
-
-        // user code here..
-    }
-
-    protected void destroyInput(){
-        if (mouseInput != null)
-            mouseInput.destroy();
-
-        if (keyInput != null)
-            keyInput.destroy();
-
-        if (joyInput != null)
-            joyInput.destroy();
-
-        if (touchInput != null)
-            touchInput.destroy();
-
-        inputManager = null;
-    }
-
-    /**
-     * Do not call manually.
-     * Callback from ContextListener.
      */
      */
-    public void destroy(){
-        stateManager.cleanup();
-
-        destroyInput();
-        if (audioRenderer != null)
-            audioRenderer.cleanup();
-
-        timer.reset();
-    }
+    public void enqueue(Runnable runnable);
 
 
     /**
     /**
      * @return The GUI viewport. Which is used for the on screen
      * @return The GUI viewport. Which is used for the on screen
      * statistics and FPS.
      * statistics and FPS.
      */
      */
-    public ViewPort getGuiViewPort() {
-        return guiViewPort;
-    }
-
-    public ViewPort getViewPort() {
-        return viewPort;
-    }
-
-    private class RunnableWrapper implements Callable{
-        private final Runnable runnable;
-
-        public RunnableWrapper(Runnable runnable){
-            this.runnable = runnable;
-        }
+    public ViewPort getGuiViewPort();
 
 
-        @Override
-        public Object call(){
-            runnable.run();
-            return null;
-        }
-        
-    }
-    
+    public ViewPort getViewPort();
 }
 }

+ 793 - 0
jme3-core/src/main/java/com/jme3/app/LegacyApplication.java

@@ -0,0 +1,793 @@
+/*
+ * 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 com.jme3.app;
+
+import com.jme3.app.state.AppState;
+import com.jme3.app.state.AppStateManager;
+import com.jme3.asset.AssetManager;
+import com.jme3.audio.AudioContext;
+import com.jme3.audio.AudioRenderer;
+import com.jme3.audio.Listener;
+import com.jme3.input.*;
+import com.jme3.math.Vector3f;
+import com.jme3.profile.AppProfiler;
+import com.jme3.profile.AppStep;
+import com.jme3.renderer.Camera;
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.Renderer;
+import com.jme3.renderer.ViewPort;
+import com.jme3.system.*;
+import com.jme3.system.JmeContext.Type;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.Future;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * The <code>LegacyApplication</code> class represents an instance of a
+ * real-time 3D rendering jME application.
+ *
+ * An <code>LegacyApplication</code> provides all the tools that are commonly used in jME3
+ * applications.
+ *
+ * jME3 applications *SHOULD NOT EXTEND* this class but extend {@link com.jme3.app.SimpleApplication} instead.
+ *
+ */
+public class LegacyApplication implements Application, SystemListener {
+
+    private static final Logger logger = Logger.getLogger(LegacyApplication.class.getName());
+
+    protected AssetManager assetManager;
+
+    protected AudioRenderer audioRenderer;
+    protected Renderer renderer;
+    protected RenderManager renderManager;
+    protected ViewPort viewPort;
+    protected ViewPort guiViewPort;
+
+    protected JmeContext context;
+    protected AppSettings settings;
+    protected Timer timer = new NanoTimer();
+    protected Camera cam;
+    protected Listener listener;
+
+    protected boolean inputEnabled = true;
+    protected LostFocusBehavior lostFocusBehavior = LostFocusBehavior.ThrottleOnLostFocus;
+    protected float speed = 1f;
+    protected boolean paused = false;
+    protected MouseInput mouseInput;
+    protected KeyInput keyInput;
+    protected JoyInput joyInput;
+    protected TouchInput touchInput;
+    protected InputManager inputManager;
+    protected AppStateManager stateManager;
+
+    protected AppProfiler prof;
+
+    private final ConcurrentLinkedQueue<AppTask<?>> taskQueue = new ConcurrentLinkedQueue<AppTask<?>>();
+
+    /**
+     * Create a new instance of <code>LegacyApplication</code>.
+     */
+    public LegacyApplication() {
+        this((AppState[])null);
+    }
+
+    /**
+     * Create a new instance of <code>LegacyApplication</code>, preinitialized
+     * with the specified set of app states.
+     */
+    public LegacyApplication( AppState... initialStates ) {
+        initStateManager();
+
+        if (initialStates != null) {
+            for (AppState a : initialStates) {
+                if (a != null) {
+                    stateManager.attach(a);
+                }
+            }
+        }
+    }
+
+    /**
+     * Determine the application's behavior when unfocused.
+     *
+     * @return The lost focus behavior of the application.
+     */
+    public LostFocusBehavior getLostFocusBehavior() {
+        return lostFocusBehavior;
+    }
+
+    /**
+     * Change the application's behavior when unfocused.
+     *
+     * By default, the application will
+     * {@link LostFocusBehavior#ThrottleOnLostFocus throttle the update loop}
+     * so as to not take 100% CPU usage when it is not in focus, e.g.
+     * alt-tabbed, minimized, or obstructed by another window.
+     *
+     * @param lostFocusBehavior The new lost focus behavior to use.
+     *
+     * @see LostFocusBehavior
+     */
+    public void setLostFocusBehavior(LostFocusBehavior lostFocusBehavior) {
+        this.lostFocusBehavior = lostFocusBehavior;
+    }
+
+    /**
+     * Returns true if pause on lost focus is enabled, false otherwise.
+     *
+     * @return true if pause on lost focus is enabled
+     *
+     * @see #getLostFocusBehavior()
+     */
+    public boolean isPauseOnLostFocus() {
+        return getLostFocusBehavior() == LostFocusBehavior.PauseOnLostFocus;
+    }
+
+    /**
+     * Enable or disable pause on lost focus.
+     * <p>
+     * By default, pause on lost focus is enabled.
+     * If enabled, the application will stop updating
+     * when it loses focus or becomes inactive (e.g. alt-tab).
+     * For online or real-time applications, this might not be preferable,
+     * so this feature should be set to disabled. For other applications,
+     * it is best to keep it on so that CPU usage is not used when
+     * not necessary.
+     *
+     * @param pauseOnLostFocus True to enable pause on lost focus, false
+     * otherwise.
+     *
+     * @see #setLostFocusBehavior(com.jme3.app.LostFocusBehavior)
+     */
+    public void setPauseOnLostFocus(boolean pauseOnLostFocus) {
+        if (pauseOnLostFocus) {
+            setLostFocusBehavior(LostFocusBehavior.PauseOnLostFocus);
+        } else {
+            setLostFocusBehavior(LostFocusBehavior.Disabled);
+        }
+    }
+
+    @Deprecated
+    public void setAssetManager(AssetManager assetManager){
+        if (this.assetManager != null)
+            throw new IllegalStateException("Can only set asset manager"
+                                          + " before initialization.");
+
+        this.assetManager = assetManager;
+    }
+
+    private void initAssetManager(){
+        URL assetCfgUrl = null;
+
+        if (settings != null){
+            String assetCfg = settings.getString("AssetConfigURL");
+            if (assetCfg != null){
+                try {
+                    assetCfgUrl = new URL(assetCfg);
+                } catch (MalformedURLException ex) {
+                }
+                if (assetCfgUrl == null) {
+                    assetCfgUrl = LegacyApplication.class.getClassLoader().getResource(assetCfg);
+                    if (assetCfgUrl == null) {
+                        logger.log(Level.SEVERE, "Unable to access AssetConfigURL in asset config:{0}", assetCfg);
+                        return;
+                    }
+                }
+            }
+        }
+        if (assetCfgUrl == null) {
+            assetCfgUrl = JmeSystem.getPlatformAssetConfigURL();
+        }
+        if (assetManager == null){
+            assetManager = JmeSystem.newAssetManager(assetCfgUrl);
+        }
+    }
+
+    /**
+     * Set the display settings to define the display created.
+     * <p>
+     * Examples of display parameters include display pixel width and height,
+     * color bit depth, z-buffer bits, anti-aliasing samples, and update frequency.
+     * If this method is called while the application is already running, then
+     * {@link #restart() } must be called to apply the settings to the display.
+     *
+     * @param settings The settings to set.
+     */
+    public void setSettings(AppSettings settings){
+        this.settings = settings;
+        if (context != null && settings.useInput() != inputEnabled){
+            // may need to create or destroy input based
+            // on settings change
+            inputEnabled = !inputEnabled;
+            if (inputEnabled){
+                initInput();
+            }else{
+                destroyInput();
+            }
+        }else{
+            inputEnabled = settings.useInput();
+        }
+    }
+
+    /**
+     * Sets the Timer implementation that will be used for calculating
+     * frame times.  By default, Application will use the Timer as returned
+     * by the current JmeContext implementation.
+     */
+    public void setTimer(Timer timer){
+        this.timer = timer;
+
+        if (timer != null) {
+            timer.reset();
+        }
+
+        if (renderManager != null) {
+            renderManager.setTimer(timer);
+        }
+    }
+
+    public Timer getTimer(){
+        return timer;
+    }
+
+    private void initDisplay(){
+        // aquire important objects
+        // from the context
+        settings = context.getSettings();
+
+        // Only reset the timer if a user has not already provided one
+        if (timer == null) {
+            timer = context.getTimer();
+        }
+
+        renderer = context.getRenderer();
+    }
+
+    private void initAudio(){
+        if (settings.getAudioRenderer() != null && context.getType() != Type.Headless){
+            audioRenderer = JmeSystem.newAudioRenderer(settings);
+            audioRenderer.initialize();
+            AudioContext.setAudioRenderer(audioRenderer);
+
+            listener = new Listener();
+            audioRenderer.setListener(listener);
+        }
+    }
+
+    /**
+     * Creates the camera to use for rendering. Default values are perspective
+     * projection with 45° field of view, with near and far values 1 and 1000
+     * units respectively.
+     */
+    private void initCamera(){
+        cam = new Camera(settings.getWidth(), settings.getHeight());
+
+        cam.setFrustumPerspective(45f, (float)cam.getWidth() / cam.getHeight(), 1f, 1000f);
+        cam.setLocation(new Vector3f(0f, 0f, 10f));
+        cam.lookAt(new Vector3f(0f, 0f, 0f), Vector3f.UNIT_Y);
+
+        renderManager = new RenderManager(renderer);
+        //Remy - 09/14/2010 setted the timer in the renderManager
+        renderManager.setTimer(timer);
+
+        if (prof != null) {
+            renderManager.setAppProfiler(prof);
+        }
+
+        viewPort = renderManager.createMainView("Default", cam);
+        viewPort.setClearFlags(true, true, true);
+
+        // Create a new cam for the gui
+        Camera guiCam = new Camera(settings.getWidth(), settings.getHeight());
+        guiViewPort = renderManager.createPostView("Gui Default", guiCam);
+        guiViewPort.setClearFlags(false, false, false);
+    }
+
+    /**
+     * Initializes mouse and keyboard input. Also
+     * initializes joystick input if joysticks are enabled in the
+     * AppSettings.
+     */
+    private void initInput(){
+        mouseInput = context.getMouseInput();
+        if (mouseInput != null)
+            mouseInput.initialize();
+
+        keyInput = context.getKeyInput();
+        if (keyInput != null)
+            keyInput.initialize();
+
+        touchInput = context.getTouchInput();
+        if (touchInput != null)
+            touchInput.initialize();
+
+        if (!settings.getBoolean("DisableJoysticks")){
+            joyInput = context.getJoyInput();
+            if (joyInput != null)
+                joyInput.initialize();
+        }
+
+        inputManager = new InputManager(mouseInput, keyInput, joyInput, touchInput);
+    }
+
+    private void initStateManager(){
+        stateManager = new AppStateManager(this);
+
+        // Always register a ResetStatsState to make sure
+        // that the stats are cleared every frame
+        stateManager.attach(new ResetStatsState());
+    }
+
+    /**
+     * @return The {@link AssetManager asset manager} for this application.
+     */
+    public AssetManager getAssetManager(){
+        return assetManager;
+    }
+
+    /**
+     * @return the {@link InputManager input manager}.
+     */
+    public InputManager getInputManager(){
+        return inputManager;
+    }
+
+    /**
+     * @return the {@link AppStateManager app state manager}
+     */
+    public AppStateManager getStateManager() {
+        return stateManager;
+    }
+
+    /**
+     * @return the {@link RenderManager render manager}
+     */
+    public RenderManager getRenderManager() {
+        return renderManager;
+    }
+
+    /**
+     * @return The {@link Renderer renderer} for the application
+     */
+    public Renderer getRenderer(){
+        return renderer;
+    }
+
+    /**
+     * @return The {@link AudioRenderer audio renderer} for the application
+     */
+    public AudioRenderer getAudioRenderer() {
+        return audioRenderer;
+    }
+
+    /**
+     * @return The {@link Listener listener} object for audio
+     */
+    public Listener getListener() {
+        return listener;
+    }
+
+    /**
+     * @return The {@link JmeContext display context} for the application
+     */
+    public JmeContext getContext(){
+        return context;
+    }
+
+    /**
+     * @return The {@link Camera camera} for the application
+     */
+    public Camera getCamera(){
+        return cam;
+    }
+
+    /**
+     * Starts the application in {@link Type#Display display} mode.
+     *
+     * @see #start(com.jme3.system.JmeContext.Type)
+     */
+    public void start(){
+        start(JmeContext.Type.Display, false);
+    }
+
+    /**
+     * Starts the application in {@link Type#Display display} mode.
+     *
+     * @see #start(com.jme3.system.JmeContext.Type)
+     */
+    public void start(boolean waitFor){
+        start(JmeContext.Type.Display, waitFor);
+    }
+
+    /**
+     * Starts the application.
+     * Creating a rendering context and executing
+     * the main loop in a separate thread.
+     */
+    public void start(JmeContext.Type contextType) {
+        start(contextType, false);
+    }
+
+    /**
+     * Starts the application.
+     * Creating a rendering context and executing
+     * the main loop in a separate thread.
+     */
+    public void start(JmeContext.Type contextType, boolean waitFor){
+        if (context != null && context.isCreated()){
+            logger.warning("start() called when application already created!");
+            return;
+        }
+
+        if (settings == null){
+            settings = new AppSettings(true);
+        }
+
+        logger.log(Level.FINE, "Starting application: {0}", getClass().getName());
+        context = JmeSystem.newContext(settings, contextType);
+        context.setSystemListener(this);
+        context.create(waitFor);
+    }
+
+    /**
+     * Sets an AppProfiler hook that will be called back for
+     * specific steps within a single update frame.  Value defaults
+     * to null.
+     */
+    public void setAppProfiler(AppProfiler prof) {
+        this.prof = prof;
+        if (renderManager != null) {
+            renderManager.setAppProfiler(prof);
+        }
+    }
+
+    /**
+     * Returns the current AppProfiler hook, or null if none is set.
+     */
+    public AppProfiler getAppProfiler() {
+        return prof;
+    }
+
+    /**
+     * Initializes the application's canvas for use.
+     * <p>
+     * After calling this method, cast the {@link #getContext() context} to
+     * {@link JmeCanvasContext},
+     * then acquire the canvas with {@link JmeCanvasContext#getCanvas() }
+     * and attach it to an AWT/Swing Frame.
+     * The rendering thread will start when the canvas becomes visible on
+     * screen, however if you wish to start the context immediately you
+     * may call {@link #startCanvas() } to force the rendering thread
+     * to start.
+     *
+     * @see JmeCanvasContext
+     * @see Type#Canvas
+     */
+    public void createCanvas(){
+        if (context != null && context.isCreated()){
+            logger.warning("createCanvas() called when application already created!");
+            return;
+        }
+
+        if (settings == null){
+            settings = new AppSettings(true);
+        }
+
+        logger.log(Level.FINE, "Starting application: {0}", getClass().getName());
+        context = JmeSystem.newContext(settings, JmeContext.Type.Canvas);
+        context.setSystemListener(this);
+    }
+
+    /**
+     * Starts the rendering thread after createCanvas() has been called.
+     * <p>
+     * Same as calling startCanvas(false)
+     *
+     * @see #startCanvas(boolean)
+     */
+    public void startCanvas(){
+        startCanvas(false);
+    }
+
+    /**
+     * Starts the rendering thread after createCanvas() has been called.
+     * <p>
+     * Calling this method is optional, the canvas will start automatically
+     * when it becomes visible.
+     *
+     * @param waitFor If true, the current thread will block until the
+     * rendering thread is running
+     */
+    public void startCanvas(boolean waitFor){
+        context.create(waitFor);
+    }
+
+    /**
+     * Internal use only.
+     */
+    public void reshape(int w, int h){
+        if (renderManager != null) {
+            renderManager.notifyReshape(w, h);
+        }
+    }
+
+    /**
+     * Restarts the context, applying any changed settings.
+     * <p>
+     * Changes to the {@link AppSettings} of this Application are not
+     * applied immediately; calling this method forces the context
+     * to restart, applying the new settings.
+     */
+    public void restart(){
+        context.setSettings(settings);
+        context.restart();
+    }
+
+    /**
+     * Requests the context to close, shutting down the main loop
+     * and making necessary cleanup operations.
+     *
+     * Same as calling stop(false)
+     *
+     * @see #stop(boolean)
+     */
+    public void stop(){
+        stop(false);
+    }
+
+    /**
+     * Requests the context to close, shutting down the main loop
+     * and making necessary cleanup operations.
+     * After the application has stopped, it cannot be used anymore.
+     */
+    public void stop(boolean waitFor){
+        logger.log(Level.FINE, "Closing application: {0}", getClass().getName());
+        context.destroy(waitFor);
+    }
+
+    /**
+     * Do not call manually.
+     * Callback from ContextListener.
+     * <p>
+     * Initializes the <code>Application</code>, by creating a display and
+     * default camera. If display settings are not specified, a default
+     * 640x480 display is created. Default values are used for the camera;
+     * perspective projection with 45° field of view, with near
+     * and far values 1 and 1000 units respectively.
+     */
+    public void initialize(){
+        if (assetManager == null){
+            initAssetManager();
+        }
+
+        initDisplay();
+        initCamera();
+
+        if (inputEnabled){
+            initInput();
+        }
+        initAudio();
+
+        // update timer so that the next delta is not too large
+//        timer.update();
+        timer.reset();
+
+        // user code here..
+    }
+
+    /**
+     * Internal use only.
+     */
+    public void handleError(String errMsg, Throwable t){
+        // Print error to log.
+        logger.log(Level.SEVERE, errMsg, t);
+        // Display error message on screen if not in headless mode
+        if (context.getType() != JmeContext.Type.Headless) {
+            if (t != null) {
+                JmeSystem.showErrorDialog(errMsg + "\n" + t.getClass().getSimpleName() +
+                        (t.getMessage() != null ? ": " +  t.getMessage() : ""));
+            } else {
+                JmeSystem.showErrorDialog(errMsg);
+            }
+        }
+
+        stop(); // stop the application
+    }
+
+    /**
+     * Internal use only.
+     */
+    public void gainFocus(){
+        if (lostFocusBehavior != LostFocusBehavior.Disabled) {
+            if (lostFocusBehavior == LostFocusBehavior.PauseOnLostFocus) {
+                paused = false;
+            }
+            context.setAutoFlushFrames(true);
+            if (inputManager != null) {
+                inputManager.reset();
+            }
+        }
+    }
+
+    /**
+     * Internal use only.
+     */
+    public void loseFocus(){
+        if (lostFocusBehavior != LostFocusBehavior.Disabled){
+            if (lostFocusBehavior == LostFocusBehavior.PauseOnLostFocus) {
+                paused = true;
+            }
+            context.setAutoFlushFrames(false);
+        }
+    }
+
+    /**
+     * Internal use only.
+     */
+    public void requestClose(boolean esc){
+        context.destroy(false);
+    }
+
+    /**
+     * Enqueues a task/callable object to execute in the jME3
+     * rendering thread.
+     * <p>
+     * Callables are executed right at the beginning of the main loop.
+     * They are executed even if the application is currently paused
+     * or out of focus.
+     *
+     * @param callable The callable to run in the main jME3 thread
+     */
+    public <V> Future<V> enqueue(Callable<V> callable) {
+        AppTask<V> task = new AppTask<V>(callable);
+        taskQueue.add(task);
+        return task;
+    }
+
+    /**
+     * Enqueues a runnable object to execute in the jME3
+     * rendering thread.
+     * <p>
+     * Runnables are executed right at the beginning of the main loop.
+     * They are executed even if the application is currently paused
+     * or out of focus.
+     *
+     * @param runnable The runnable to run in the main jME3 thread
+     */
+    public void enqueue(Runnable runnable){
+        enqueue(new RunnableWrapper(runnable));
+    }
+
+    /**
+     * Runs tasks enqueued via {@link #enqueue(Callable)}
+     */
+    protected void runQueuedTasks() {
+	  AppTask<?> task;
+        while( (task = taskQueue.poll()) != null ) {
+            if (!task.isCancelled()) {
+                task.invoke();
+            }
+        }
+    }
+
+    /**
+     * Do not call manually.
+     * Callback from ContextListener.
+     */
+    public void update(){
+        // Make sure the audio renderer is available to callables
+        AudioContext.setAudioRenderer(audioRenderer);
+
+        if (prof!=null) prof.appStep(AppStep.QueuedTasks);
+        runQueuedTasks();
+
+        if (speed == 0 || paused)
+            return;
+
+        timer.update();
+
+        if (inputEnabled){
+            if (prof!=null) prof.appStep(AppStep.ProcessInput);
+            inputManager.update(timer.getTimePerFrame());
+        }
+
+        if (audioRenderer != null){
+            if (prof!=null) prof.appStep(AppStep.ProcessAudio);
+            audioRenderer.update(timer.getTimePerFrame());
+        }
+
+        // user code here..
+    }
+
+    protected void destroyInput(){
+        if (mouseInput != null)
+            mouseInput.destroy();
+
+        if (keyInput != null)
+            keyInput.destroy();
+
+        if (joyInput != null)
+            joyInput.destroy();
+
+        if (touchInput != null)
+            touchInput.destroy();
+
+        inputManager = null;
+    }
+
+    /**
+     * Do not call manually.
+     * Callback from ContextListener.
+     */
+    public void destroy(){
+        stateManager.cleanup();
+
+        destroyInput();
+        if (audioRenderer != null)
+            audioRenderer.cleanup();
+
+        timer.reset();
+    }
+
+    /**
+     * @return The GUI viewport. Which is used for the on screen
+     * statistics and FPS.
+     */
+    public ViewPort getGuiViewPort() {
+        return guiViewPort;
+    }
+
+    public ViewPort getViewPort() {
+        return viewPort;
+    }
+
+    private class RunnableWrapper implements Callable{
+        private final Runnable runnable;
+
+        public RunnableWrapper(Runnable runnable){
+            this.runnable = runnable;
+        }
+
+        @Override
+        public Object call(){
+            runnable.run();
+            return null;
+        }
+
+    }
+
+}

+ 17 - 25
jme3-core/src/main/java/com/jme3/app/SimpleApplication.java

@@ -59,17 +59,17 @@ import com.jme3.system.JmeSystem;
  * <tr><td>C</td><td>- Display the camera position and rotation in the console.</td></tr>
  * <tr><td>C</td><td>- Display the camera position and rotation in the console.</td></tr>
  * <tr><td>M</td><td>- Display memory usage in the console.</td></tr>
  * <tr><td>M</td><td>- Display memory usage in the console.</td></tr>
  * </table>
  * </table>
- * 
+ *
  * A {@link com.jme3.app.FlyCamAppState} is by default attached as well and can
  * A {@link com.jme3.app.FlyCamAppState} is by default attached as well and can
  * be removed by calling <code>stateManager.detach( stateManager.getState(FlyCamAppState.class) );</code>
  * be removed by calling <code>stateManager.detach( stateManager.getState(FlyCamAppState.class) );</code>
  */
  */
-public abstract class SimpleApplication extends Application {
+public abstract class SimpleApplication extends LegacyApplication {
 
 
     public static final String INPUT_MAPPING_EXIT = "SIMPLEAPP_Exit";
     public static final String INPUT_MAPPING_EXIT = "SIMPLEAPP_Exit";
     public static final String INPUT_MAPPING_CAMERA_POS = DebugKeysAppState.INPUT_MAPPING_CAMERA_POS;
     public static final String INPUT_MAPPING_CAMERA_POS = DebugKeysAppState.INPUT_MAPPING_CAMERA_POS;
     public static final String INPUT_MAPPING_MEMORY = DebugKeysAppState.INPUT_MAPPING_MEMORY;
     public static final String INPUT_MAPPING_MEMORY = DebugKeysAppState.INPUT_MAPPING_MEMORY;
     public static final String INPUT_MAPPING_HIDE_STATS = "SIMPLEAPP_HideStats";
     public static final String INPUT_MAPPING_HIDE_STATS = "SIMPLEAPP_HideStats";
-                                                                         
+
     protected Node rootNode = new Node("Root Node");
     protected Node rootNode = new Node("Root Node");
     protected Node guiNode = new Node("Gui Node");
     protected Node guiNode = new Node("Gui Node");
     protected BitmapText fpsText;
     protected BitmapText fpsText;
@@ -77,7 +77,7 @@ public abstract class SimpleApplication extends Application {
     protected FlyByCamera flyCam;
     protected FlyByCamera flyCam;
     protected boolean showSettings = true;
     protected boolean showSettings = true;
     private AppActionListener actionListener = new AppActionListener();
     private AppActionListener actionListener = new AppActionListener();
-    
+
     private class AppActionListener implements ActionListener {
     private class AppActionListener implements ActionListener {
 
 
         public void onAction(String name, boolean value, float tpf) {
         public void onAction(String name, boolean value, float tpf) {
@@ -100,15 +100,7 @@ public abstract class SimpleApplication extends Application {
     }
     }
 
 
     public SimpleApplication( AppState... initialStates ) {
     public SimpleApplication( AppState... initialStates ) {
-        super();
-        
-        if (initialStates != null) {
-            for (AppState a : initialStates) {
-                if (a != null) {
-                    stateManager.attach(a);
-                }
-            }
-        }
+        super(initialStates);
     }
     }
 
 
     @Override
     @Override
@@ -193,7 +185,7 @@ public abstract class SimpleApplication extends Application {
         guiViewPort.attachScene(guiNode);
         guiViewPort.attachScene(guiNode);
 
 
         if (inputManager != null) {
         if (inputManager != null) {
-        
+
             // We have to special-case the FlyCamAppState because too
             // We have to special-case the FlyCamAppState because too
             // many SimpleApplication subclasses expect it to exist in
             // many SimpleApplication subclasses expect it to exist in
             // simpleInit().  But at least it only gets initialized if
             // simpleInit().  But at least it only gets initialized if
@@ -201,7 +193,7 @@ public abstract class SimpleApplication extends Application {
             if (stateManager.getState(FlyCamAppState.class) != null) {
             if (stateManager.getState(FlyCamAppState.class) != null) {
                 flyCam = new FlyByCamera(cam);
                 flyCam = new FlyByCamera(cam);
                 flyCam.setMoveSpeed(1f); // odd to set this here but it did it before
                 flyCam.setMoveSpeed(1f); // odd to set this here but it did it before
-                stateManager.getState(FlyCamAppState.class).setCamera( flyCam ); 
+                stateManager.getState(FlyCamAppState.class).setCamera( flyCam );
             }
             }
 
 
             if (context.getType() == Type.Display) {
             if (context.getType() == Type.Display) {
@@ -210,10 +202,10 @@ public abstract class SimpleApplication extends Application {
 
 
             if (stateManager.getState(StatsAppState.class) != null) {
             if (stateManager.getState(StatsAppState.class) != null) {
                 inputManager.addMapping(INPUT_MAPPING_HIDE_STATS, new KeyTrigger(KeyInput.KEY_F5));
                 inputManager.addMapping(INPUT_MAPPING_HIDE_STATS, new KeyTrigger(KeyInput.KEY_F5));
-                inputManager.addListener(actionListener, INPUT_MAPPING_HIDE_STATS);            
+                inputManager.addListener(actionListener, INPUT_MAPPING_HIDE_STATS);
             }
             }
-            
-            inputManager.addListener(actionListener, INPUT_MAPPING_EXIT);            
+
+            inputManager.addListener(actionListener, INPUT_MAPPING_EXIT);
         }
         }
 
 
         if (stateManager.getState(StatsAppState.class) != null) {
         if (stateManager.getState(StatsAppState.class) != null) {
@@ -230,37 +222,37 @@ public abstract class SimpleApplication extends Application {
     @Override
     @Override
     public void update() {
     public void update() {
         if (prof!=null) prof.appStep(AppStep.BeginFrame);
         if (prof!=null) prof.appStep(AppStep.BeginFrame);
-        
+
         super.update(); // makes sure to execute AppTasks
         super.update(); // makes sure to execute AppTasks
         if (speed == 0 || paused) {
         if (speed == 0 || paused) {
             return;
             return;
         }
         }
 
 
         float tpf = timer.getTimePerFrame() * speed;
         float tpf = timer.getTimePerFrame() * speed;
-        
+
         // update states
         // update states
         if (prof!=null) prof.appStep(AppStep.StateManagerUpdate);
         if (prof!=null) prof.appStep(AppStep.StateManagerUpdate);
         stateManager.update(tpf);
         stateManager.update(tpf);
 
 
         // simple update and root node
         // simple update and root node
         simpleUpdate(tpf);
         simpleUpdate(tpf);
- 
+
         if (prof!=null) prof.appStep(AppStep.SpatialUpdate);
         if (prof!=null) prof.appStep(AppStep.SpatialUpdate);
         rootNode.updateLogicalState(tpf);
         rootNode.updateLogicalState(tpf);
         guiNode.updateLogicalState(tpf);
         guiNode.updateLogicalState(tpf);
-        
+
         rootNode.updateGeometricState();
         rootNode.updateGeometricState();
         guiNode.updateGeometricState();
         guiNode.updateGeometricState();
-        
+
         // render states
         // render states
         if (prof!=null) prof.appStep(AppStep.StateManagerRender);
         if (prof!=null) prof.appStep(AppStep.StateManagerRender);
         stateManager.render(renderManager);
         stateManager.render(renderManager);
-        
+
         if (prof!=null) prof.appStep(AppStep.RenderFrame);
         if (prof!=null) prof.appStep(AppStep.RenderFrame);
         renderManager.render(tpf, context.isRenderable());
         renderManager.render(tpf, context.isRenderable());
         simpleRender(renderManager);
         simpleRender(renderManager);
         stateManager.postRender();
         stateManager.postRender();
-                
+
         if (prof!=null) prof.appStep(AppStep.EndFrame);
         if (prof!=null) prof.appStep(AppStep.EndFrame);
     }
     }
 
 

+ 30 - 30
jme3-core/src/main/java/com/jme3/app/StatsAppState.java

@@ -46,7 +46,7 @@ import com.jme3.scene.shape.Quad;
 
 
 /**
 /**
  *  Displays stats in SimpleApplication's GUI node or
  *  Displays stats in SimpleApplication's GUI node or
- *  using the node and font parameters provided.  
+ *  using the node and font parameters provided.
  *
  *
  *  @author    Paul Speed
  *  @author    Paul Speed
  */
  */
@@ -58,7 +58,7 @@ public class StatsAppState extends AbstractAppState {
     private boolean showFps = true;
     private boolean showFps = true;
     private boolean showStats = true;
     private boolean showStats = true;
     private boolean darkenBehind = true;
     private boolean darkenBehind = true;
-    
+
     protected Node guiNode;
     protected Node guiNode;
     protected float secondCounter = 0.0f;
     protected float secondCounter = 0.0f;
     protected int frameCounter = 0;
     protected int frameCounter = 0;
@@ -68,7 +68,7 @@ public class StatsAppState extends AbstractAppState {
     protected Geometry darkenStats;
     protected Geometry darkenStats;
 
 
     public StatsAppState() {
     public StatsAppState() {
-    }    
+    }
 
 
     public StatsAppState( Node guiNode, BitmapFont guiFont ) {
     public StatsAppState( Node guiNode, BitmapFont guiFont ) {
         this.guiNode = guiNode;
         this.guiNode = guiNode;
@@ -89,7 +89,7 @@ public class StatsAppState extends AbstractAppState {
     public BitmapText getFpsText() {
     public BitmapText getFpsText() {
         return fpsText;
         return fpsText;
     }
     }
-    
+
     public StatsView getStatsView() {
     public StatsView getStatsView() {
         return statsView;
         return statsView;
     }
     }
@@ -110,7 +110,7 @@ public class StatsAppState extends AbstractAppState {
             if (darkenFps != null) {
             if (darkenFps != null) {
                 darkenFps.setCullHint(showFps && darkenBehind ? CullHint.Never : CullHint.Always);
                 darkenFps.setCullHint(showFps && darkenBehind ? CullHint.Never : CullHint.Always);
             }
             }
-            
+
         }
         }
     }
     }
 
 
@@ -138,7 +138,7 @@ public class StatsAppState extends AbstractAppState {
     public void initialize(AppStateManager stateManager, Application app) {
     public void initialize(AppStateManager stateManager, Application app) {
         super.initialize(stateManager, app);
         super.initialize(stateManager, app);
         this.app = app;
         this.app = app;
-               
+
         if (app instanceof SimpleApplication) {
         if (app instanceof SimpleApplication) {
             SimpleApplication simpleApp = (SimpleApplication)app;
             SimpleApplication simpleApp = (SimpleApplication)app;
             if (guiNode == null) {
             if (guiNode == null) {
@@ -147,21 +147,21 @@ public class StatsAppState extends AbstractAppState {
             if (guiFont == null ) {
             if (guiFont == null ) {
                 guiFont = simpleApp.guiFont;
                 guiFont = simpleApp.guiFont;
             }
             }
-        } 
-        
+        }
+
         if (guiNode == null) {
         if (guiNode == null) {
             throw new RuntimeException( "No guiNode specific and cannot be automatically determined." );
             throw new RuntimeException( "No guiNode specific and cannot be automatically determined." );
-        } 
-        
+        }
+
         if (guiFont == null) {
         if (guiFont == null) {
             guiFont = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
             guiFont = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
         }
         }
-        
-        loadFpsText();  
-        loadStatsView();      
+
+        loadFpsText();
+        loadStatsView();
         loadDarken();
         loadDarken();
     }
     }
-            
+
     /**
     /**
      * Attaches FPS statistics to guiNode and displays it on the screen.
      * Attaches FPS statistics to guiNode and displays it on the screen.
      *
      *
@@ -170,12 +170,12 @@ public class StatsAppState extends AbstractAppState {
         if (fpsText == null) {
         if (fpsText == null) {
             fpsText = new BitmapText(guiFont, false);
             fpsText = new BitmapText(guiFont, false);
         }
         }
-        
+
         fpsText.setLocalTranslation(0, fpsText.getLineHeight(), 0);
         fpsText.setLocalTranslation(0, fpsText.getLineHeight(), 0);
         fpsText.setText("Frames per second");
         fpsText.setText("Frames per second");
         fpsText.setCullHint(showFps ? CullHint.Never : CullHint.Always);
         fpsText.setCullHint(showFps ? CullHint.Never : CullHint.Always);
         guiNode.attachChild(fpsText);
         guiNode.attachChild(fpsText);
-        
+
     }
     }
 
 
     /**
     /**
@@ -184,53 +184,53 @@ public class StatsAppState extends AbstractAppState {
      *
      *
      */
      */
     public void loadStatsView() {
     public void loadStatsView() {
-        statsView = new StatsView("Statistics View", 
-                                  app.getAssetManager(), 
+        statsView = new StatsView("Statistics View",
+                                  app.getAssetManager(),
                                   app.getRenderer().getStatistics());
                                   app.getRenderer().getStatistics());
         // move it up so it appears above fps text
         // move it up so it appears above fps text
         statsView.setLocalTranslation(0, fpsText.getLineHeight(), 0);
         statsView.setLocalTranslation(0, fpsText.getLineHeight(), 0);
         statsView.setEnabled(showStats);
         statsView.setEnabled(showStats);
-        statsView.setCullHint(showStats ? CullHint.Never : CullHint.Always);        
+        statsView.setCullHint(showStats ? CullHint.Never : CullHint.Always);
         guiNode.attachChild(statsView);
         guiNode.attachChild(statsView);
     }
     }
-        
+
     public void loadDarken() {
     public void loadDarken() {
-        Material mat = new Material(app.assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
         mat.setColor("Color", new ColorRGBA(0,0,0,0.5f));
         mat.setColor("Color", new ColorRGBA(0,0,0,0.5f));
         mat.getAdditionalRenderState().setBlendMode(BlendMode.Alpha);
         mat.getAdditionalRenderState().setBlendMode(BlendMode.Alpha);
-        
+
         darkenFps = new Geometry("StatsDarken", new Quad(200, fpsText.getLineHeight()));
         darkenFps = new Geometry("StatsDarken", new Quad(200, fpsText.getLineHeight()));
         darkenFps.setMaterial(mat);
         darkenFps.setMaterial(mat);
         darkenFps.setLocalTranslation(0, 0, -1);
         darkenFps.setLocalTranslation(0, 0, -1);
         darkenFps.setCullHint(showFps && darkenBehind ? CullHint.Never : CullHint.Always);
         darkenFps.setCullHint(showFps && darkenBehind ? CullHint.Never : CullHint.Always);
         guiNode.attachChild(darkenFps);
         guiNode.attachChild(darkenFps);
-        
+
         darkenStats = new Geometry("StatsDarken", new Quad(200, statsView.getHeight()));
         darkenStats = new Geometry("StatsDarken", new Quad(200, statsView.getHeight()));
         darkenStats.setMaterial(mat);
         darkenStats.setMaterial(mat);
         darkenStats.setLocalTranslation(0, fpsText.getHeight(), -1);
         darkenStats.setLocalTranslation(0, fpsText.getHeight(), -1);
         darkenStats.setCullHint(showStats && darkenBehind ? CullHint.Never : CullHint.Always);
         darkenStats.setCullHint(showStats && darkenBehind ? CullHint.Never : CullHint.Always);
         guiNode.attachChild(darkenStats);
         guiNode.attachChild(darkenStats);
     }
     }
-    
+
     @Override
     @Override
     public void setEnabled(boolean enabled) {
     public void setEnabled(boolean enabled) {
         super.setEnabled(enabled);
         super.setEnabled(enabled);
-        
+
         if (enabled) {
         if (enabled) {
             fpsText.setCullHint(showFps ? CullHint.Never : CullHint.Always);
             fpsText.setCullHint(showFps ? CullHint.Never : CullHint.Always);
             darkenFps.setCullHint(showFps && darkenBehind ? CullHint.Never : CullHint.Always);
             darkenFps.setCullHint(showFps && darkenBehind ? CullHint.Never : CullHint.Always);
             statsView.setEnabled(showStats);
             statsView.setEnabled(showStats);
-            statsView.setCullHint(showStats ? CullHint.Never : CullHint.Always);        
+            statsView.setCullHint(showStats ? CullHint.Never : CullHint.Always);
             darkenStats.setCullHint(showStats && darkenBehind ? CullHint.Never : CullHint.Always);
             darkenStats.setCullHint(showStats && darkenBehind ? CullHint.Never : CullHint.Always);
         } else {
         } else {
             fpsText.setCullHint(CullHint.Always);
             fpsText.setCullHint(CullHint.Always);
             darkenFps.setCullHint(CullHint.Always);
             darkenFps.setCullHint(CullHint.Always);
             statsView.setEnabled(false);
             statsView.setEnabled(false);
-            statsView.setCullHint(CullHint.Always);        
+            statsView.setCullHint(CullHint.Always);
             darkenStats.setCullHint(CullHint.Always);
             darkenStats.setCullHint(CullHint.Always);
         }
         }
     }
     }
-    
+
     @Override
     @Override
     public void update(float tpf) {
     public void update(float tpf) {
         if (showFps) {
         if (showFps) {
@@ -241,14 +241,14 @@ public class StatsAppState extends AbstractAppState {
                 fpsText.setText("Frames per second: " + fps);
                 fpsText.setText("Frames per second: " + fps);
                 secondCounter = 0.0f;
                 secondCounter = 0.0f;
                 frameCounter = 0;
                 frameCounter = 0;
-            }          
+            }
         }
         }
     }
     }
 
 
     @Override
     @Override
     public void cleanup() {
     public void cleanup() {
         super.cleanup();
         super.cleanup();
-        
+
         guiNode.detachChild(statsView);
         guiNode.detachChild(statsView);
         guiNode.detachChild(fpsText);
         guiNode.detachChild(fpsText);
         guiNode.detachChild(darkenFps);
         guiNode.detachChild(darkenFps);

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

@@ -69,7 +69,7 @@ public class StatsView extends Node implements Control, JmeCloneable {
     private int[] statData;
     private int[] statData;
 
 
     private boolean enabled = true;
     private boolean enabled = true;
-    
+
     private final StringBuilder stringBuilder = new StringBuilder();
     private final StringBuilder stringBuilder = new StringBuilder();
 
 
     public StatsView(String name, AssetManager manager, Statistics stats){
     public StatsView(String name, AssetManager manager, Statistics stats){
@@ -95,22 +95,22 @@ public class StatsView extends Node implements Control, JmeCloneable {
     public float getHeight() {
     public float getHeight() {
         return statText.getLineHeight() * statLabels.length;
         return statText.getLineHeight() * statLabels.length;
     }
     }
-    
+
     public void update(float tpf) {
     public void update(float tpf) {
-    
-        if (!isEnabled()) 
+
+        if (!isEnabled())
             return;
             return;
-            
+
         statistics.getData(statData);
         statistics.getData(statData);
         stringBuilder.setLength(0);
         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.
         // should appear at the bottom, not the top.
         for (int i = statLabels.length - 1; i >= 0; i--) {
         for (int i = statLabels.length - 1; i >= 0; i--) {
             stringBuilder.append(statLabels[i]).append(" = ").append(statData[i]).append('\n');
             stringBuilder.append(statLabels[i]).append(" = ").append(statData[i]).append('\n');
         }
         }
         statText.setText(stringBuilder);
         statText.setText(stringBuilder);
-        
+
         // Moved to ResetStatsState to make sure it is
         // Moved to ResetStatsState to make sure it is
         // done even if there is no StatsView or the StatsView
         // done even if there is no StatsView or the StatsView
         // is disable.
         // is disable.
@@ -122,16 +122,16 @@ public class StatsView extends Node implements Control, JmeCloneable {
         return (Control) spatial;
         return (Control) spatial;
     }
     }
 
 
-    @Override   
-    public Object jmeClone() {
+    @Override
+    public StatsView jmeClone() {
         throw new UnsupportedOperationException("Not yet implemented.");
         throw new UnsupportedOperationException("Not yet implemented.");
-    }     
+    }
 
 
-    @Override   
-    public void cloneFields( Cloner cloner, Object original ) { 
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) {
         throw new UnsupportedOperationException("Not yet implemented.");
         throw new UnsupportedOperationException("Not yet implemented.");
     }
     }
-         
+
     public void setSpatial(Spatial spatial) {
     public void setSpatial(Spatial spatial) {
     }
     }
 
 

+ 0 - 9
jme3-core/src/main/java/com/jme3/asset/AssetManager.java

@@ -41,9 +41,7 @@ import com.jme3.post.FilterPostProcessor;
 import com.jme3.renderer.Caps;
 import com.jme3.renderer.Caps;
 import com.jme3.scene.Spatial;
 import com.jme3.scene.Spatial;
 import com.jme3.scene.plugins.OBJLoader;
 import com.jme3.scene.plugins.OBJLoader;
-import com.jme3.shader.Shader;
 import com.jme3.shader.ShaderGenerator;
 import com.jme3.shader.ShaderGenerator;
-import com.jme3.shader.ShaderKey;
 import com.jme3.texture.Texture;
 import com.jme3.texture.Texture;
 import com.jme3.texture.plugins.TGALoader;
 import com.jme3.texture.plugins.TGALoader;
 import java.io.IOException;
 import java.io.IOException;
@@ -320,13 +318,6 @@ public interface AssetManager {
      */
      */
     public Material loadMaterial(String name);
     public Material loadMaterial(String name);
 
 
-    /**
-     * Loads shader file(s), shouldn't be used by end-user in most cases.
-     *
-     * @see AssetManager#loadAsset(com.jme3.asset.AssetKey)
-     */
-    public Shader loadShader(ShaderKey key);
-
     /**
     /**
      * Load a font file. Font files are in AngelCode text format,
      * Load a font file. Font files are in AngelCode text format,
      * and are with the extension "fnt".
      * and are with the extension "fnt".

+ 0 - 33
jme3-core/src/main/java/com/jme3/asset/DesktopAssetManager.java

@@ -32,7 +32,6 @@
 package com.jme3.asset;
 package com.jme3.asset;
 
 
 import com.jme3.asset.cache.AssetCache;
 import com.jme3.asset.cache.AssetCache;
-import com.jme3.asset.cache.SimpleAssetCache;
 import com.jme3.audio.AudioData;
 import com.jme3.audio.AudioData;
 import com.jme3.audio.AudioKey;
 import com.jme3.audio.AudioKey;
 import com.jme3.font.BitmapFont;
 import com.jme3.font.BitmapFont;
@@ -42,9 +41,7 @@ import com.jme3.renderer.Caps;
 import com.jme3.scene.Spatial;
 import com.jme3.scene.Spatial;
 import com.jme3.shader.Glsl100ShaderGenerator;
 import com.jme3.shader.Glsl100ShaderGenerator;
 import com.jme3.shader.Glsl150ShaderGenerator;
 import com.jme3.shader.Glsl150ShaderGenerator;
-import com.jme3.shader.Shader;
 import com.jme3.shader.ShaderGenerator;
 import com.jme3.shader.ShaderGenerator;
-import com.jme3.shader.ShaderKey;
 import com.jme3.system.JmeSystem;
 import com.jme3.system.JmeSystem;
 import com.jme3.texture.Texture;
 import com.jme3.texture.Texture;
 import java.io.IOException;
 import java.io.IOException;
@@ -431,36 +428,6 @@ public class DesktopAssetManager implements AssetManager {
         return loadFilter(new FilterKey(name));
         return loadFilter(new FilterKey(name));
     }
     }
 
 
-    /**
-     * Load a vertex/fragment shader combo.
-     *
-     * @param key
-     * @return the loaded {@link Shader}
-     */
-    public Shader loadShader(ShaderKey key){
-        // cache abuse in method
-        // that doesn't use loaders/locators
-        AssetCache cache = handler.getCache(SimpleAssetCache.class);
-        Shader shader = (Shader) cache.getFromCache(key);
-        if (shader == null){
-            if (key.isUsesShaderNodes()) {
-                if(shaderGenerator == null){
-                    throw new UnsupportedOperationException("ShaderGenerator was not initialized, make sure assetManager.getGenerator(caps) has been called");
-                }
-                shader = shaderGenerator.generateShader();
-            } else {
-                shader = new Shader();
-                shader.initialize();
-                for (Shader.ShaderType shaderType : key.getUsedShaderPrograms()) {
-                    shader.addSource(shaderType,key.getShaderProgramName(shaderType),(String) loadAsset(new AssetKey(key.getShaderProgramName(shaderType))),key.getDefines().getCompiled(),key.getShaderProgramLanguage(shaderType));
-                }
-            }
-
-            cache.addToCache(key, shader);
-        }
-        return shader;
-    }
-
     /**
     /**
      * {@inheritDoc}
      * {@inheritDoc}
      */
      */

+ 121 - 96
jme3-core/src/main/java/com/jme3/audio/AudioNode.java

@@ -41,26 +41,27 @@ import com.jme3.export.OutputCapsule;
 import com.jme3.math.Vector3f;
 import com.jme3.math.Vector3f;
 import com.jme3.scene.Node;
 import com.jme3.scene.Node;
 import com.jme3.util.PlaceholderAssets;
 import com.jme3.util.PlaceholderAssets;
+import com.jme3.util.clone.Cloner;
 import java.io.IOException;
 import java.io.IOException;
 import java.util.logging.Level;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import java.util.logging.Logger;
 
 
 /**
 /**
- * An <code>AudioNode</code> is a scene Node which can play audio assets. 
- * 
- * An AudioNode is either positional or ambient, with positional being the 
- * default. Once a positional node is attached to the scene, its location and 
- * velocity relative to the {@link Listener} affect how it sounds when played. 
- * Positional nodes can only play monoaural (single-channel) assets, not stereo 
- * ones. 
- * 
- * An ambient AudioNode plays in "headspace", meaning that the node's location 
- * and velocity do not affect how it sounds when played. Ambient audio nodes can 
- * play stereo assets. 
- * 
- * The "positional" property of an AudioNode can be set via 
+ * An <code>AudioNode</code> is a scene Node which can play audio assets.
+ *
+ * An AudioNode is either positional or ambient, with positional being the
+ * default. Once a positional node is attached to the scene, its location and
+ * velocity relative to the {@link Listener} affect how it sounds when played.
+ * Positional nodes can only play monoaural (single-channel) assets, not stereo
+ * ones.
+ *
+ * An ambient AudioNode plays in "headspace", meaning that the node's location
+ * and velocity do not affect how it sounds when played. Ambient audio nodes can
+ * play stereo assets.
+ *
+ * The "positional" property of an AudioNode can be set via
  * {@link AudioNode#setPositional(boolean) }.
  * {@link AudioNode#setPositional(boolean) }.
- * 
+ *
  * @author normenhansen
  * @author normenhansen
  * @author Kirill Vainer
  * @author Kirill Vainer
  */
  */
@@ -99,15 +100,15 @@ public class AudioNode extends Node implements AudioSource {
          * {@link AudioNode#play() } is called.
          * {@link AudioNode#play() } is called.
          */
          */
         Playing,
         Playing,
-        
+
         /**
         /**
          * The audio node is currently paused.
          * The audio node is currently paused.
          */
          */
         Paused,
         Paused,
-        
+
         /**
         /**
          * The audio node is currently stopped.
          * The audio node is currently stopped.
-         * This will be set if {@link AudioNode#stop() } is called 
+         * This will be set if {@link AudioNode#stop() } is called
          * or the audio has reached the end of the file.
          * or the audio has reached the end of the file.
          */
          */
         Stopped,
         Stopped,
@@ -121,14 +122,14 @@ public class AudioNode extends Node implements AudioSource {
 
 
     /**
     /**
      * Creates a new <code>AudioNode</code> with the given data and key.
      * Creates a new <code>AudioNode</code> with the given data and key.
-     * 
+     *
      * @param audioData The audio data contains the audio track to play.
      * @param audioData The audio data contains the audio track to play.
      * @param audioKey The audio key that was used to load the AudioData
      * @param audioKey The audio key that was used to load the AudioData
      */
      */
     public AudioNode(AudioData audioData, AudioKey audioKey) {
     public AudioNode(AudioData audioData, AudioKey audioKey) {
         setAudioData(audioData, audioKey);
         setAudioData(audioData, audioKey);
     }
     }
-    
+
     /**
     /**
      * Creates a new <code>AudioNode</code> with the given audio file.
      * Creates a new <code>AudioNode</code> with the given audio file.
      * @param assetManager The asset manager to use to load the audio file
      * @param assetManager The asset manager to use to load the audio file
@@ -142,16 +143,16 @@ public class AudioNode extends Node implements AudioSource {
 
 
     /**
     /**
      * Creates a new <code>AudioNode</code> with the given audio file.
      * Creates a new <code>AudioNode</code> with the given audio file.
-     * 
+     *
      * @param assetManager The asset manager to use to load the audio file
      * @param assetManager The asset manager to use to load the audio file
      * @param name The filename of the audio file
      * @param name The filename of the audio file
-     * @param stream If true, the audio will be streamed gradually from disk, 
+     * @param stream If true, the audio will be streamed gradually from disk,
      *               otherwise, it will be buffered.
      *               otherwise, it will be buffered.
      * @param streamCache If stream is also true, then this specifies if
      * @param streamCache If stream is also true, then this specifies if
      * the stream cache is used. When enabled, the audio stream will
      * the stream cache is used. When enabled, the audio stream will
-     * be read entirely but not decoded, allowing features such as 
+     * be read entirely but not decoded, allowing features such as
      * seeking, looping and determining duration.
      * seeking, looping and determining duration.
-     * 
+     *
      * @deprecated Use {@link AudioNode#AudioNode(com.jme3.asset.AssetManager, java.lang.String, com.jme3.audio.AudioData.DataType)} instead
      * @deprecated Use {@link AudioNode#AudioNode(com.jme3.asset.AssetManager, java.lang.String, com.jme3.audio.AudioData.DataType)} instead
      */
      */
     public AudioNode(AssetManager assetManager, String name, boolean stream, boolean streamCache) {
     public AudioNode(AssetManager assetManager, String name, boolean stream, boolean streamCache) {
@@ -161,12 +162,12 @@ public class AudioNode extends Node implements AudioSource {
 
 
     /**
     /**
      * Creates a new <code>AudioNode</code> with the given audio file.
      * Creates a new <code>AudioNode</code> with the given audio file.
-     * 
+     *
      * @param assetManager The asset manager to use to load the audio file
      * @param assetManager The asset manager to use to load the audio file
      * @param name The filename of the audio file
      * @param name The filename of the audio file
-     * @param stream If true, the audio will be streamed gradually from disk, 
+     * @param stream If true, the audio will be streamed gradually from disk,
      *               otherwise, it will be buffered.
      *               otherwise, it will be buffered.
-     * 
+     *
      * @deprecated Use {@link AudioNode#AudioNode(com.jme3.asset.AssetManager, java.lang.String, com.jme3.audio.AudioData.DataType)} instead
      * @deprecated Use {@link AudioNode#AudioNode(com.jme3.asset.AssetManager, java.lang.String, com.jme3.audio.AudioData.DataType)} instead
      */
      */
     public AudioNode(AssetManager assetManager, String name, boolean stream) {
     public AudioNode(AssetManager assetManager, String name, boolean stream) {
@@ -175,20 +176,20 @@ public class AudioNode extends Node implements AudioSource {
 
 
     /**
     /**
      * Creates a new <code>AudioNode</code> with the given audio file.
      * Creates a new <code>AudioNode</code> with the given audio file.
-     * 
+     *
      * @param audioRenderer The audio renderer to use for playing. Cannot be null.
      * @param audioRenderer The audio renderer to use for playing. Cannot be null.
      * @param assetManager The asset manager to use to load the audio file
      * @param assetManager The asset manager to use to load the audio file
      * @param name The filename of the audio file
      * @param name The filename of the audio file
-     * 
+     *
      * @deprecated AudioRenderer parameter is ignored.
      * @deprecated AudioRenderer parameter is ignored.
      */
      */
     public AudioNode(AudioRenderer audioRenderer, AssetManager assetManager, String name) {
     public AudioNode(AudioRenderer audioRenderer, AssetManager assetManager, String name) {
         this(assetManager, name, DataType.Buffer);
         this(assetManager, name, DataType.Buffer);
     }
     }
-    
+
     /**
     /**
      * Creates a new <code>AudioNode</code> with the given audio file.
      * Creates a new <code>AudioNode</code> with the given audio file.
-     * 
+     *
      * @param assetManager The asset manager to use to load the audio file
      * @param assetManager The asset manager to use to load the audio file
      * @param name The filename of the audio file
      * @param name The filename of the audio file
      * @deprecated Use {@link AudioNode#AudioNode(com.jme3.asset.AssetManager, java.lang.String, com.jme3.audio.AudioData.DataType) } instead
      * @deprecated Use {@link AudioNode#AudioNode(com.jme3.asset.AssetManager, java.lang.String, com.jme3.audio.AudioData.DataType) } instead
@@ -196,14 +197,14 @@ public class AudioNode extends Node implements AudioSource {
     public AudioNode(AssetManager assetManager, String name) {
     public AudioNode(AssetManager assetManager, String name) {
         this(assetManager, name, DataType.Buffer);
         this(assetManager, name, DataType.Buffer);
     }
     }
-    
+
     protected AudioRenderer getRenderer() {
     protected AudioRenderer getRenderer() {
         AudioRenderer result = AudioContext.getAudioRenderer();
         AudioRenderer result = AudioContext.getAudioRenderer();
         if( result == null )
         if( result == null )
             throw new IllegalStateException( "No audio renderer available, make sure call is being performed on render thread." );
             throw new IllegalStateException( "No audio renderer available, make sure call is being performed on render thread." );
-        return result;            
+        return result;
     }
     }
-    
+
     /**
     /**
      * Start playing the audio.
      * Start playing the audio.
      */
      */
@@ -217,7 +218,7 @@ public class AudioNode extends Node implements AudioSource {
     /**
     /**
      * Start playing an instance of this audio. This method can be used
      * Start playing an instance of this audio. This method can be used
      * to play the same <code>AudioNode</code> multiple times. Note
      * to play the same <code>AudioNode</code> multiple times. Note
-     * that changes to the parameters of this AudioNode will not effect the 
+     * that changes to the parameters of this AudioNode will not effect the
      * instances already playing.
      * instances already playing.
      */
      */
     public void playInstance(){
     public void playInstance(){
@@ -226,21 +227,21 @@ public class AudioNode extends Node implements AudioSource {
         }
         }
         getRenderer().playSourceInstance(this);
         getRenderer().playSourceInstance(this);
     }
     }
-    
+
     /**
     /**
      * Stop playing the audio that was started with {@link AudioNode#play() }.
      * Stop playing the audio that was started with {@link AudioNode#play() }.
      */
      */
     public void stop(){
     public void stop(){
         getRenderer().stopSource(this);
         getRenderer().stopSource(this);
     }
     }
-    
+
     /**
     /**
      * Pause the audio that was started with {@link AudioNode#play() }.
      * Pause the audio that was started with {@link AudioNode#play() }.
      */
      */
     public void pause(){
     public void pause(){
         getRenderer().pauseSource(this);
         getRenderer().pauseSource(this);
     }
     }
-    
+
     /**
     /**
      * Do not use.
      * Do not use.
      */
      */
@@ -261,7 +262,7 @@ public class AudioNode extends Node implements AudioSource {
 
 
     /**
     /**
      * @return The {#link Filter dry filter} that is set.
      * @return The {#link Filter dry filter} that is set.
-     * @see AudioNode#setDryFilter(com.jme3.audio.Filter) 
+     * @see AudioNode#setDryFilter(com.jme3.audio.Filter)
      */
      */
     public Filter getDryFilter() {
     public Filter getDryFilter() {
         return dryFilter;
         return dryFilter;
@@ -269,14 +270,14 @@ public class AudioNode extends Node implements AudioSource {
 
 
     /**
     /**
      * Set the dry filter to use for this audio node.
      * Set the dry filter to use for this audio node.
-     * 
-     * When {@link AudioNode#setReverbEnabled(boolean) reverb} is used, 
-     * the dry filter will only influence the "dry" portion of the audio, 
+     *
+     * When {@link AudioNode#setReverbEnabled(boolean) reverb} is used,
+     * the dry filter will only influence the "dry" portion of the audio,
      * e.g. not the reverberated parts of the AudioNode playing.
      * e.g. not the reverberated parts of the AudioNode playing.
-     * 
+     *
      * See the relevent documentation for the {@link Filter} to determine
      * See the relevent documentation for the {@link Filter} to determine
      * the effect.
      * the effect.
-     * 
+     *
      * @param dryFilter The filter to set, or null to disable dry filter.
      * @param dryFilter The filter to set, or null to disable dry filter.
      */
      */
     public void setDryFilter(Filter dryFilter) {
     public void setDryFilter(Filter dryFilter) {
@@ -289,7 +290,7 @@ public class AudioNode extends Node implements AudioSource {
      * Set the audio data to use for the audio. Note that this method
      * Set the audio data to use for the audio. Note that this method
      * can only be called once, if for example the audio node was initialized
      * can only be called once, if for example the audio node was initialized
      * without an {@link AudioData}.
      * without an {@link AudioData}.
-     * 
+     *
      * @param audioData The audio data contains the audio track to play.
      * @param audioData The audio data contains the audio track to play.
      * @param audioKey The audio key that was used to load the AudioData
      * @param audioKey The audio key that was used to load the AudioData
      */
      */
@@ -303,7 +304,7 @@ public class AudioNode extends Node implements AudioSource {
     }
     }
 
 
     /**
     /**
-     * @return The {@link AudioData} set previously with 
+     * @return The {@link AudioData} set previously with
      * {@link AudioNode#setAudioData(com.jme3.audio.AudioData, com.jme3.audio.AudioKey) }
      * {@link AudioNode#setAudioData(com.jme3.audio.AudioData, com.jme3.audio.AudioKey) }
      * or any of the constructors that initialize the audio data.
      * or any of the constructors that initialize the audio data.
      */
      */
@@ -312,7 +313,7 @@ public class AudioNode extends Node implements AudioSource {
     }
     }
 
 
     /**
     /**
-     * @return The {@link Status} of the audio node. 
+     * @return The {@link Status} of the audio node.
      * The status will be changed when either the {@link AudioNode#play() }
      * The status will be changed when either the {@link AudioNode#play() }
      * or {@link AudioNode#stop() } methods are called.
      * or {@link AudioNode#stop() } methods are called.
      */
      */
@@ -339,7 +340,7 @@ public class AudioNode extends Node implements AudioSource {
         else
         else
             return data.getDataType();
             return data.getDataType();
     }
     }
-    
+
     /**
     /**
      * @return True if the audio will keep looping after it is done playing,
      * @return True if the audio will keep looping after it is done playing,
      * otherwise, false.
      * otherwise, false.
@@ -351,7 +352,7 @@ public class AudioNode extends Node implements AudioSource {
 
 
     /**
     /**
      * Set the looping mode for the audio node. The default is false.
      * Set the looping mode for the audio node. The default is false.
-     * 
+     *
      * @param loop True if the audio should keep looping after it is done playing.
      * @param loop True if the audio should keep looping after it is done playing.
      */
      */
     public void setLooping(boolean loop) {
     public void setLooping(boolean loop) {
@@ -362,8 +363,8 @@ public class AudioNode extends Node implements AudioSource {
 
 
     /**
     /**
      * @return The pitch of the audio, also the speed of playback.
      * @return The pitch of the audio, also the speed of playback.
-     * 
-     * @see AudioNode#setPitch(float) 
+     *
+     * @see AudioNode#setPitch(float)
      */
      */
     public float getPitch() {
     public float getPitch() {
         return pitch;
         return pitch;
@@ -372,7 +373,7 @@ public class AudioNode extends Node implements AudioSource {
     /**
     /**
      * Set the pitch of the audio, also the speed of playback.
      * Set the pitch of the audio, also the speed of playback.
      * The value must be between 0.5 and 2.0.
      * The value must be between 0.5 and 2.0.
-     * 
+     *
      * @param pitch The pitch to set.
      * @param pitch The pitch to set.
      * @throws IllegalArgumentException If pitch is not between 0.5 and 2.0.
      * @throws IllegalArgumentException If pitch is not between 0.5 and 2.0.
      */
      */
@@ -388,7 +389,7 @@ public class AudioNode extends Node implements AudioSource {
 
 
     /**
     /**
      * @return The volume of this audio node.
      * @return The volume of this audio node.
-     * 
+     *
      * @see AudioNode#setVolume(float)
      * @see AudioNode#setVolume(float)
      */
      */
     public float getVolume() {
     public float getVolume() {
@@ -397,9 +398,9 @@ public class AudioNode extends Node implements AudioSource {
 
 
     /**
     /**
      * Set the volume of this audio node.
      * Set the volume of this audio node.
-     * 
+     *
      * The volume is specified as gain. 1.0 is the default.
      * The volume is specified as gain. 1.0 is the default.
-     * 
+     *
      * @param volume The volume to set.
      * @param volume The volume to set.
      * @throws IllegalArgumentException If volume is negative
      * @throws IllegalArgumentException If volume is negative
      */
      */
@@ -422,7 +423,7 @@ public class AudioNode extends Node implements AudioSource {
 
 
     /**
     /**
      * Set the time offset in the sound sample when to start playing.
      * Set the time offset in the sound sample when to start playing.
-     * 
+     *
      * @param timeOffset The time offset
      * @param timeOffset The time offset
      * @throws IllegalArgumentException If timeOffset is negative
      * @throws IllegalArgumentException If timeOffset is negative
      */
      */
@@ -439,7 +440,7 @@ public class AudioNode extends Node implements AudioSource {
             play();
             play();
         }
         }
     }
     }
-    
+
     @Override
     @Override
     public float getPlaybackTime() {
     public float getPlaybackTime() {
         if (channel >= 0)
         if (channel >= 0)
@@ -451,10 +452,10 @@ public class AudioNode extends Node implements AudioSource {
     public Vector3f getPosition() {
     public Vector3f getPosition() {
         return getWorldTranslation();
         return getWorldTranslation();
     }
     }
-    
+
     /**
     /**
      * @return The velocity of the audio node.
      * @return The velocity of the audio node.
-     * 
+     *
      * @see AudioNode#setVelocity(com.jme3.math.Vector3f)
      * @see AudioNode#setVelocity(com.jme3.math.Vector3f)
      */
      */
     public Vector3f getVelocity() {
     public Vector3f getVelocity() {
@@ -464,7 +465,7 @@ public class AudioNode extends Node implements AudioSource {
     /**
     /**
      * Set the velocity of the audio node. The velocity is expected
      * Set the velocity of the audio node. The velocity is expected
      * to be in meters. Does nothing if the audio node is not positional.
      * to be in meters. Does nothing if the audio node is not positional.
-     * 
+     *
      * @param velocity The velocity to set.
      * @param velocity The velocity to set.
      * @see AudioNode#setPositional(boolean)
      * @see AudioNode#setPositional(boolean)
      */
      */
@@ -476,7 +477,7 @@ public class AudioNode extends Node implements AudioSource {
 
 
     /**
     /**
      * @return True if reverb is enabled, otherwise false.
      * @return True if reverb is enabled, otherwise false.
-     * 
+     *
      * @see AudioNode#setReverbEnabled(boolean)
      * @see AudioNode#setReverbEnabled(boolean)
      */
      */
     public boolean isReverbEnabled() {
     public boolean isReverbEnabled() {
@@ -487,10 +488,10 @@ public class AudioNode extends Node implements AudioSource {
      * Set to true to enable reverberation effects for this audio node.
      * Set to true to enable reverberation effects for this audio node.
      * Does nothing if the audio node is not positional.
      * Does nothing if the audio node is not positional.
      * <br/>
      * <br/>
-     * When enabled, the audio environment set with 
+     * When enabled, the audio environment set with
      * {@link AudioRenderer#setEnvironment(com.jme3.audio.Environment) }
      * {@link AudioRenderer#setEnvironment(com.jme3.audio.Environment) }
      * will apply a reverb effect to the audio playing from this audio node.
      * will apply a reverb effect to the audio playing from this audio node.
-     * 
+     *
      * @param reverbEnabled True to enable reverb.
      * @param reverbEnabled True to enable reverb.
      */
      */
     public void setReverbEnabled(boolean reverbEnabled) {
     public void setReverbEnabled(boolean reverbEnabled) {
@@ -502,8 +503,8 @@ public class AudioNode extends Node implements AudioSource {
 
 
     /**
     /**
      * @return Filter for the reverberations of this audio node.
      * @return Filter for the reverberations of this audio node.
-     * 
-     * @see AudioNode#setReverbFilter(com.jme3.audio.Filter) 
+     *
+     * @see AudioNode#setReverbFilter(com.jme3.audio.Filter)
      */
      */
     public Filter getReverbFilter() {
     public Filter getReverbFilter() {
         return reverbFilter;
         return reverbFilter;
@@ -515,7 +516,7 @@ public class AudioNode extends Node implements AudioSource {
      * The reverb filter will influence the reverberations
      * The reverb filter will influence the reverberations
      * of the audio node playing. This only has an effect if
      * of the audio node playing. This only has an effect if
      * reverb is enabled.
      * reverb is enabled.
-     * 
+     *
      * @param reverbFilter The reverb filter to set.
      * @param reverbFilter The reverb filter to set.
      * @see AudioNode#setDryFilter(com.jme3.audio.Filter)
      * @see AudioNode#setDryFilter(com.jme3.audio.Filter)
      */
      */
@@ -527,7 +528,7 @@ public class AudioNode extends Node implements AudioSource {
 
 
     /**
     /**
      * @return Max distance for this audio node.
      * @return Max distance for this audio node.
-     * 
+     *
      * @see AudioNode#setMaxDistance(float)
      * @see AudioNode#setMaxDistance(float)
      */
      */
     public float getMaxDistance() {
     public float getMaxDistance() {
@@ -545,7 +546,7 @@ public class AudioNode extends Node implements AudioSource {
      * get any quieter than at that distance.  If you want a sound to fall-off
      * get any quieter than at that distance.  If you want a sound to fall-off
      * very quickly then set ref distance very short and leave this distance
      * very quickly then set ref distance very short and leave this distance
      * very long.
      * very long.
-     * 
+     *
      * @param maxDistance The maximum playing distance.
      * @param maxDistance The maximum playing distance.
      * @throws IllegalArgumentException If maxDistance is negative
      * @throws IllegalArgumentException If maxDistance is negative
      */
      */
@@ -561,8 +562,8 @@ public class AudioNode extends Node implements AudioSource {
 
 
     /**
     /**
      * @return The reference playing distance for the audio node.
      * @return The reference playing distance for the audio node.
-     * 
-     * @see AudioNode#setRefDistance(float) 
+     *
+     * @see AudioNode#setRefDistance(float)
      */
      */
     public float getRefDistance() {
     public float getRefDistance() {
         return refDistance;
         return refDistance;
@@ -574,7 +575,7 @@ public class AudioNode extends Node implements AudioSource {
      * <br/>
      * <br/>
      * The reference playing distance is the distance at which the
      * The reference playing distance is the distance at which the
      * audio node will be exactly half of its volume.
      * audio node will be exactly half of its volume.
-     * 
+     *
      * @param refDistance The reference playing distance.
      * @param refDistance The reference playing distance.
      * @throws  IllegalArgumentException If refDistance is negative
      * @throws  IllegalArgumentException If refDistance is negative
      */
      */
@@ -590,8 +591,8 @@ public class AudioNode extends Node implements AudioSource {
 
 
     /**
     /**
      * @return True if the audio node is directional
      * @return True if the audio node is directional
-     * 
-     * @see AudioNode#setDirectional(boolean) 
+     *
+     * @see AudioNode#setDirectional(boolean)
      */
      */
     public boolean isDirectional() {
     public boolean isDirectional() {
         return directional;
         return directional;
@@ -601,10 +602,10 @@ public class AudioNode extends Node implements AudioSource {
      * Set the audio node to be directional.
      * Set the audio node to be directional.
      * Does nothing if the audio node is not positional.
      * Does nothing if the audio node is not positional.
      * <br/>
      * <br/>
-     * After setting directional, you should call 
+     * After setting directional, you should call
      * {@link AudioNode#setDirection(com.jme3.math.Vector3f) }
      * {@link AudioNode#setDirection(com.jme3.math.Vector3f) }
      * to set the audio node's direction.
      * to set the audio node's direction.
-     * 
+     *
      * @param directional If the audio node is directional
      * @param directional If the audio node is directional
      */
      */
     public void setDirectional(boolean directional) {
     public void setDirectional(boolean directional) {
@@ -615,7 +616,7 @@ public class AudioNode extends Node implements AudioSource {
 
 
     /**
     /**
      * @return The direction of this audio node.
      * @return The direction of this audio node.
-     * 
+     *
      * @see AudioNode#setDirection(com.jme3.math.Vector3f)
      * @see AudioNode#setDirection(com.jme3.math.Vector3f)
      */
      */
     public Vector3f getDirection() {
     public Vector3f getDirection() {
@@ -625,9 +626,9 @@ public class AudioNode extends Node implements AudioSource {
     /**
     /**
      * Set the direction of this audio node.
      * Set the direction of this audio node.
      * Does nothing if the audio node is not directional.
      * Does nothing if the audio node is not directional.
-     * 
-     * @param direction 
-     * @see AudioNode#setDirectional(boolean) 
+     *
+     * @param direction
+     * @see AudioNode#setDirectional(boolean)
      */
      */
     public void setDirection(Vector3f direction) {
     public void setDirection(Vector3f direction) {
         this.direction = direction;
         this.direction = direction;
@@ -637,8 +638,8 @@ public class AudioNode extends Node implements AudioSource {
 
 
     /**
     /**
      * @return The directional audio node, cone inner angle.
      * @return The directional audio node, cone inner angle.
-     * 
-     * @see AudioNode#setInnerAngle(float) 
+     *
+     * @see AudioNode#setInnerAngle(float)
      */
      */
     public float getInnerAngle() {
     public float getInnerAngle() {
         return innerAngle;
         return innerAngle;
@@ -647,7 +648,7 @@ public class AudioNode extends Node implements AudioSource {
     /**
     /**
      * Set the directional audio node cone inner angle.
      * Set the directional audio node cone inner angle.
      * Does nothing if the audio node is not directional.
      * Does nothing if the audio node is not directional.
-     * 
+     *
      * @param innerAngle The cone inner angle.
      * @param innerAngle The cone inner angle.
      */
      */
     public void setInnerAngle(float innerAngle) {
     public void setInnerAngle(float innerAngle) {
@@ -658,8 +659,8 @@ public class AudioNode extends Node implements AudioSource {
 
 
     /**
     /**
      * @return The directional audio node, cone outer angle.
      * @return The directional audio node, cone outer angle.
-     * 
-     * @see AudioNode#setOuterAngle(float) 
+     *
+     * @see AudioNode#setOuterAngle(float)
      */
      */
     public float getOuterAngle() {
     public float getOuterAngle() {
         return outerAngle;
         return outerAngle;
@@ -668,7 +669,7 @@ public class AudioNode extends Node implements AudioSource {
     /**
     /**
      * Set the directional audio node cone outer angle.
      * Set the directional audio node cone outer angle.
      * Does nothing if the audio node is not directional.
      * Does nothing if the audio node is not directional.
-     * 
+     *
      * @param outerAngle The cone outer angle.
      * @param outerAngle The cone outer angle.
      */
      */
     public void setOuterAngle(float outerAngle) {
     public void setOuterAngle(float outerAngle) {
@@ -679,8 +680,8 @@ public class AudioNode extends Node implements AudioSource {
 
 
     /**
     /**
      * @return True if the audio node is positional.
      * @return True if the audio node is positional.
-     * 
-     * @see AudioNode#setPositional(boolean) 
+     *
+     * @see AudioNode#setPositional(boolean)
      */
      */
     public boolean isPositional() {
     public boolean isPositional() {
         return positional;
         return positional;
@@ -690,7 +691,7 @@ public class AudioNode extends Node implements AudioSource {
      * Set the audio node as positional.
      * Set the audio node as positional.
      * The position, velocity, and distance parameters effect positional
      * The position, velocity, and distance parameters effect positional
      * audio nodes. Set to false if the audio node should play in "headspace".
      * audio nodes. Set to false if the audio node should play in "headspace".
-     * 
+     *
      * @param positional True if the audio node should be positional, otherwise
      * @param positional True if the audio node should be positional, otherwise
      * false if it should be headspace.
      * false if it should be headspace.
      */
      */
@@ -707,7 +708,7 @@ public class AudioNode extends Node implements AudioSource {
         if ((refreshFlags & RF_TRANSFORM) != 0){
         if ((refreshFlags & RF_TRANSFORM) != 0){
             updatePos = true;
             updatePos = true;
         }
         }
-        
+
         super.updateGeometricState();
         super.updateGeometricState();
 
 
         if (updatePos && channel >= 0)
         if (updatePos && channel >= 0)
@@ -717,13 +718,37 @@ public class AudioNode extends Node implements AudioSource {
     @Override
     @Override
     public AudioNode clone(){
     public AudioNode clone(){
         AudioNode clone = (AudioNode) super.clone();
         AudioNode clone = (AudioNode) super.clone();
-        
+
         clone.direction = direction.clone();
         clone.direction = direction.clone();
         clone.velocity  = velocity.clone();
         clone.velocity  = velocity.clone();
-        
+
         return clone;
         return clone;
     }
     }
-    
+
+    /**
+     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
+     */
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) {
+        super.cloneFields(cloner, original);
+
+        this.direction = cloner.clone(direction);
+        this.velocity = cloner.clone(velocity);
+
+        // Change in behavior: the filters were not cloned before meaning
+        // that two cloned audio nodes would share the same filter instance.
+        // While settings will only be applied when the filter is actually
+        // set, I think it's probably surprising to callers if the values of
+        // a filter change from one AudioNode when a different AudioNode's
+        // filter attributes are updated.
+        // Plus if they disable and re-enable the thing using the filter then
+        // the settings get reapplied and it might be surprising to have them
+        // suddenly be strange.
+        // ...so I'll clone them.  -pspeed
+        this.dryFilter = cloner.clone(dryFilter);
+        this.reverbFilter = cloner.clone(reverbFilter);
+    }
+
     @Override
     @Override
     public void write(JmeExporter ex) throws IOException {
     public void write(JmeExporter ex) throws IOException {
         super.write(ex);
         super.write(ex);
@@ -745,7 +770,7 @@ public class AudioNode extends Node implements AudioSource {
         oc.write(direction, "direction", null);
         oc.write(direction, "direction", null);
         oc.write(innerAngle, "inner_angle", 360);
         oc.write(innerAngle, "inner_angle", 360);
         oc.write(outerAngle, "outer_angle", 360);
         oc.write(outerAngle, "outer_angle", 360);
-        
+
         oc.write(positional, "positional", false);
         oc.write(positional, "positional", false);
     }
     }
 
 
@@ -753,7 +778,7 @@ public class AudioNode extends Node implements AudioSource {
     public void read(JmeImporter im) throws IOException {
     public void read(JmeImporter im) throws IOException {
         super.read(im);
         super.read(im);
         InputCapsule ic = im.getCapsule(this);
         InputCapsule ic = im.getCapsule(this);
-        
+
         // NOTE: In previous versions of jME3, audioKey was actually
         // NOTE: In previous versions of jME3, audioKey was actually
         // written with the name "key". This has been changed
         // written with the name "key". This has been changed
         // to "audio_key" in case Spatial's key will be written as "key".
         // to "audio_key" in case Spatial's key will be written as "key".
@@ -762,7 +787,7 @@ public class AudioNode extends Node implements AudioSource {
         }else{
         }else{
             audioKey = (AudioKey) ic.readSavable("audio_key", null);
             audioKey = (AudioKey) ic.readSavable("audio_key", null);
         }
         }
-        
+
         loop = ic.readBoolean("looping", false);
         loop = ic.readBoolean("looping", false);
         volume = ic.readFloat("volume", 1);
         volume = ic.readFloat("volume", 1);
         pitch = ic.readFloat("pitch", 1);
         pitch = ic.readFloat("pitch", 1);
@@ -779,9 +804,9 @@ public class AudioNode extends Node implements AudioSource {
         direction = (Vector3f) ic.readSavable("direction", null);
         direction = (Vector3f) ic.readSavable("direction", null);
         innerAngle = ic.readFloat("inner_angle", 360);
         innerAngle = ic.readFloat("inner_angle", 360);
         outerAngle = ic.readFloat("outer_angle", 360);
         outerAngle = ic.readFloat("outer_angle", 360);
-        
+
         positional = ic.readBoolean("positional", false);
         positional = ic.readBoolean("positional", false);
-        
+
         if (audioKey != null) {
         if (audioKey != null) {
             try {
             try {
                 data = im.getAssetManager().loadAsset(audioKey);
                 data = im.getAssetManager().loadAsset(audioKey);

+ 13 - 16
jme3-core/src/main/java/com/jme3/cinematic/events/MotionEvent.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2016 jMonkeyEngine
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without
  * Redistribution and use in source and binary forms, with or without
@@ -64,9 +64,9 @@ public class MotionEvent extends AbstractCinematicEvent implements Control, JmeC
     protected int currentWayPoint;
     protected int currentWayPoint;
     protected float currentValue;
     protected float currentValue;
     protected Vector3f direction = new Vector3f();
     protected Vector3f direction = new Vector3f();
-    protected Vector3f lookAt;
+    protected Vector3f lookAt = null;
     protected Vector3f upVector = Vector3f.UNIT_Y;
     protected Vector3f upVector = Vector3f.UNIT_Y;
-    protected Quaternion rotation;
+    protected Quaternion rotation = null;
     protected Direction directionType = Direction.None;
     protected Direction directionType = Direction.None;
     protected MotionPath path;
     protected MotionPath path;
     private boolean isControl = true;
     private boolean isControl = true;
@@ -120,7 +120,6 @@ public class MotionEvent extends AbstractCinematicEvent implements Control, JmeC
      */
      */
     public MotionEvent(Spatial spatial, MotionPath path) {
     public MotionEvent(Spatial spatial, MotionPath path) {
         super();
         super();
-        this.spatial = spatial;
         spatial.addControl(this);
         spatial.addControl(this);
         this.path = path;
         this.path = path;
     }
     }
@@ -132,7 +131,6 @@ public class MotionEvent extends AbstractCinematicEvent implements Control, JmeC
      */
      */
     public MotionEvent(Spatial spatial, MotionPath path, float initialDuration) {
     public MotionEvent(Spatial spatial, MotionPath path, float initialDuration) {
         super(initialDuration);
         super(initialDuration);
-        this.spatial = spatial;
         spatial.addControl(this);
         spatial.addControl(this);
         this.path = path;
         this.path = path;
     }
     }
@@ -144,7 +142,6 @@ public class MotionEvent extends AbstractCinematicEvent implements Control, JmeC
      */
      */
     public MotionEvent(Spatial spatial, MotionPath path, LoopMode loopMode) {
     public MotionEvent(Spatial spatial, MotionPath path, LoopMode loopMode) {
         super();
         super();
-        this.spatial = spatial;
         spatial.addControl(this);
         spatial.addControl(this);
         this.path = path;
         this.path = path;
         this.loopMode = loopMode;
         this.loopMode = loopMode;
@@ -157,7 +154,6 @@ public class MotionEvent extends AbstractCinematicEvent implements Control, JmeC
      */
      */
     public MotionEvent(Spatial spatial, MotionPath path, float initialDuration, LoopMode loopMode) {
     public MotionEvent(Spatial spatial, MotionPath path, float initialDuration, LoopMode loopMode) {
         super(initialDuration);
         super(initialDuration);
-        this.spatial = spatial;
         spatial.addControl(this);
         spatial.addControl(this);
         this.path = path;
         this.path = path;
         this.loopMode = loopMode;
         this.loopMode = loopMode;
@@ -213,9 +209,9 @@ public class MotionEvent extends AbstractCinematicEvent implements Control, JmeC
     public void write(JmeExporter ex) throws IOException {
     public void write(JmeExporter ex) throws IOException {
         super.write(ex);
         super.write(ex);
         OutputCapsule oc = ex.getCapsule(this);
         OutputCapsule oc = ex.getCapsule(this);
-        oc.write(lookAt, "lookAt", Vector3f.ZERO);
+        oc.write(lookAt, "lookAt", null);
         oc.write(upVector, "upVector", Vector3f.UNIT_Y);
         oc.write(upVector, "upVector", Vector3f.UNIT_Y);
-        oc.write(rotation, "rotation", Quaternion.IDENTITY);
+        oc.write(rotation, "rotation", null);
         oc.write(directionType, "directionType", Direction.None);
         oc.write(directionType, "directionType", Direction.None);
         oc.write(path, "path", null);
         oc.write(path, "path", null);
     }
     }
@@ -224,9 +220,9 @@ public class MotionEvent extends AbstractCinematicEvent implements Control, JmeC
     public void read(JmeImporter im) throws IOException {
     public void read(JmeImporter im) throws IOException {
         super.read(im);
         super.read(im);
         InputCapsule in = im.getCapsule(this);
         InputCapsule in = im.getCapsule(this);
-        lookAt = (Vector3f) in.readSavable("lookAt", Vector3f.ZERO);
+        lookAt = (Vector3f) in.readSavable("lookAt", null);
         upVector = (Vector3f) in.readSavable("upVector", Vector3f.UNIT_Y);
         upVector = (Vector3f) in.readSavable("upVector", Vector3f.UNIT_Y);
-        rotation = (Quaternion) in.readSavable("rotation", Quaternion.IDENTITY);
+        rotation = (Quaternion) in.readSavable("rotation", null);
         directionType = in.readEnum("directionType", Direction.class, Direction.None);
         directionType = in.readEnum("directionType", Direction.class, Direction.None);
         path = (MotionPath) in.readSavable("path", null);
         path = (MotionPath) in.readSavable("path", null);
     }
     }
@@ -278,14 +274,15 @@ public class MotionEvent extends AbstractCinematicEvent implements Control, JmeC
      */
      */
     @Override
     @Override
     public Control cloneForSpatial(Spatial spatial) {
     public Control cloneForSpatial(Spatial spatial) {
-        MotionEvent control = new MotionEvent(spatial, path);
+        MotionEvent control = new MotionEvent();
+        control.setPath(path);
         control.playState = playState;
         control.playState = playState;
         control.currentWayPoint = currentWayPoint;
         control.currentWayPoint = currentWayPoint;
         control.currentValue = currentValue;
         control.currentValue = currentValue;
         control.direction = direction.clone();
         control.direction = direction.clone();
-        control.lookAt = lookAt.clone();
+        control.lookAt = lookAt;
         control.upVector = upVector.clone();
         control.upVector = upVector.clone();
-        control.rotation = rotation.clone();
+        control.rotation = rotation;
         control.initialDuration = initialDuration;
         control.initialDuration = initialDuration;
         control.speed = speed;
         control.speed = speed;
         control.loopMode = loopMode;
         control.loopMode = loopMode;
@@ -302,9 +299,9 @@ public class MotionEvent extends AbstractCinematicEvent implements Control, JmeC
         control.currentWayPoint = currentWayPoint;
         control.currentWayPoint = currentWayPoint;
         control.currentValue = currentValue;
         control.currentValue = currentValue;
         control.direction = direction.clone();
         control.direction = direction.clone();
-        control.lookAt = lookAt.clone();
+        control.lookAt = lookAt;
         control.upVector = upVector.clone();
         control.upVector = upVector.clone();
-        control.rotation = rotation.clone();
+        control.rotation = rotation;
         control.initialDuration = initialDuration;
         control.initialDuration = initialDuration;
         control.speed = speed;
         control.speed = speed;
         control.loopMode = loopMode;
         control.loopMode = loopMode;

+ 179 - 134
jme3-core/src/main/java/com/jme3/effect/ParticleEmitter.java

@@ -65,12 +65,12 @@ import java.io.IOException;
  * Particle emitters can be used to simulate various kinds of phenomena,
  * Particle emitters can be used to simulate various kinds of phenomena,
  * such as fire, smoke, explosions and much more.
  * such as fire, smoke, explosions and much more.
  * <p>
  * <p>
- * Particle emitters have many properties which are used to control the 
- * simulation. The interpretation of these properties depends on the 
+ * Particle emitters have many properties which are used to control the
+ * simulation. The interpretation of these properties depends on the
  * {@link ParticleInfluencer} that has been assigned to the emitter via
  * {@link ParticleInfluencer} that has been assigned to the emitter via
  * {@link ParticleEmitter#setParticleInfluencer(com.jme3.effect.influencers.ParticleInfluencer) }.
  * {@link ParticleEmitter#setParticleInfluencer(com.jme3.effect.influencers.ParticleInfluencer) }.
  * By default the implementation {@link DefaultParticleInfluencer} is used.
  * By default the implementation {@link DefaultParticleInfluencer} is used.
- * 
+ *
  * @author Kirill Vainer
  * @author Kirill Vainer
  */
  */
 public class ParticleEmitter extends Geometry {
 public class ParticleEmitter extends Geometry {
@@ -100,7 +100,7 @@ public class ParticleEmitter extends Geometry {
     private Vector3f faceNormal = new Vector3f(Vector3f.NAN);
     private Vector3f faceNormal = new Vector3f(Vector3f.NAN);
     private int imagesX = 1;
     private int imagesX = 1;
     private int imagesY = 1;
     private int imagesY = 1;
-   
+
     private ColorRGBA startColor = new ColorRGBA(0.4f, 0.4f, 0.4f, 0.5f);
     private ColorRGBA startColor = new ColorRGBA(0.4f, 0.4f, 0.4f, 0.5f);
     private ColorRGBA endColor = new ColorRGBA(0.1f, 0.1f, 0.1f, 0.0f);
     private ColorRGBA endColor = new ColorRGBA(0.1f, 0.1f, 0.1f, 0.0f);
     private float startSize = 0.2f;
     private float startSize = 0.2f;
@@ -127,20 +127,20 @@ public class ParticleEmitter extends Geometry {
             // fixed automatically by ParticleEmitter.clone() method.
             // fixed automatically by ParticleEmitter.clone() method.
         }
         }
 
 
-        @Override   
+        @Override
         public Object jmeClone() {
         public Object jmeClone() {
             try {
             try {
                 return super.clone();
                 return super.clone();
             } catch( CloneNotSupportedException e ) {
             } catch( CloneNotSupportedException e ) {
                 throw new RuntimeException("Error cloning", e);
                 throw new RuntimeException("Error cloning", e);
             }
             }
-        }     
+        }
 
 
-        @Override   
-        public void cloneFields( Cloner cloner, Object original ) { 
+        @Override
+        public void cloneFields( Cloner cloner, Object original ) {
             this.parentEmitter = cloner.clone(parentEmitter);
             this.parentEmitter = cloner.clone(parentEmitter);
         }
         }
-             
+
         public void setSpatial(Spatial spatial) {
         public void setSpatial(Spatial spatial) {
         }
         }
 
 
@@ -174,6 +174,13 @@ public class ParticleEmitter extends Geometry {
 
 
     @Override
     @Override
     public ParticleEmitter clone(boolean cloneMaterial) {
     public ParticleEmitter clone(boolean cloneMaterial) {
+        return (ParticleEmitter)super.clone(cloneMaterial);
+    }
+
+    /**
+     *  The old clone() method that did not use the new Cloner utility.
+     */
+    public ParticleEmitter oldClone(boolean cloneMaterial) {
         ParticleEmitter clone = (ParticleEmitter) super.clone(cloneMaterial);
         ParticleEmitter clone = (ParticleEmitter) super.clone(cloneMaterial);
         clone.shape = shape.deepClone();
         clone.shape = shape.deepClone();
 
 
@@ -211,6 +218,44 @@ public class ParticleEmitter extends Geometry {
         return clone;
         return clone;
     }
     }
 
 
+    /**
+     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
+     */
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) {
+        super.cloneFields(cloner, original);
+
+        this.shape = cloner.clone(shape);
+        this.control = cloner.clone(control);
+        this.faceNormal = cloner.clone(faceNormal);
+        this.startColor = cloner.clone(startColor);
+        this.endColor = cloner.clone(endColor);
+        this.particleInfluencer = cloner.clone(particleInfluencer);
+
+        // change in behavior: gravity was not cloned before -pspeed
+        this.gravity = cloner.clone(gravity);
+
+        // So, simply setting the mesh type will cause all kinds of things
+        // to happen:
+        // 1) the new mesh gets created.
+        // 2) it is set to the Geometry
+        // 3) the particles array is recreated because setNumParticles()
+        //
+        // ...so this should be equivalent but simpler than half of the old clone()
+        // method.  Note: we do not ever want to share particleMesh so we do not
+        // clone it at all.
+        setMeshType(meshType);
+
+        // change in behavior: temp and lastPos were not cloned before...
+        // perhaps because it was believed that 'transient' fields were exluded
+        // from cloning?  (they aren't)
+        // If it was ok for these to be shared because of how they are used
+        // then they could just as well be made static... else I think it's clearer
+        // to clone them.
+        this.temp = cloner.clone(temp);
+        this.lastPos = cloner.clone(lastPos);
+    }
+
     public ParticleEmitter(String name, Type type, int numParticles) {
     public ParticleEmitter(String name, Type type, int numParticles) {
         super(name);
         super(name);
         setBatchHint(BatchHint.Never);
         setBatchHint(BatchHint.Never);
@@ -225,7 +270,7 @@ public class ParticleEmitter extends Geometry {
 
 
         meshType = type;
         meshType = type;
 
 
-        // Must create clone of shape/influencer so that a reference to a static is 
+        // Must create clone of shape/influencer so that a reference to a static is
         // not maintained
         // not maintained
         shape = shape.deepClone();
         shape = shape.deepClone();
         particleInfluencer = particleInfluencer.clone();
         particleInfluencer = particleInfluencer.clone();
@@ -267,10 +312,10 @@ public class ParticleEmitter extends Geometry {
 
 
     /**
     /**
      * Set the {@link ParticleInfluencer} to influence this particle emitter.
      * Set the {@link ParticleInfluencer} to influence this particle emitter.
-     * 
-     * @param particleInfluencer the {@link ParticleInfluencer} to influence 
+     *
+     * @param particleInfluencer the {@link ParticleInfluencer} to influence
      * this particle emitter.
      * this particle emitter.
-     * 
+     *
      * @see ParticleInfluencer
      * @see ParticleInfluencer
      */
      */
     public void setParticleInfluencer(ParticleInfluencer particleInfluencer) {
     public void setParticleInfluencer(ParticleInfluencer particleInfluencer) {
@@ -278,12 +323,12 @@ public class ParticleEmitter extends Geometry {
     }
     }
 
 
     /**
     /**
-     * Returns the {@link ParticleInfluencer} that influences this 
+     * Returns the {@link ParticleInfluencer} that influences this
      * particle emitter.
      * particle emitter.
-     * 
-     * @return the {@link ParticleInfluencer} that influences this 
+     *
+     * @return the {@link ParticleInfluencer} that influences this
      * particle emitter.
      * particle emitter.
-     * 
+     *
      * @see ParticleInfluencer
      * @see ParticleInfluencer
      */
      */
     public ParticleInfluencer getParticleInfluencer() {
     public ParticleInfluencer getParticleInfluencer() {
@@ -292,12 +337,12 @@ public class ParticleEmitter extends Geometry {
 
 
     /**
     /**
      * Returns the mesh type used by the particle emitter.
      * Returns the mesh type used by the particle emitter.
-     * 
-     * 
+     *
+     *
      * @return the mesh type used by the particle emitter.
      * @return the mesh type used by the particle emitter.
-     * 
+     *
      * @see #setMeshType(com.jme3.effect.ParticleMesh.Type)
      * @see #setMeshType(com.jme3.effect.ParticleMesh.Type)
-     * @see ParticleEmitter#ParticleEmitter(java.lang.String, com.jme3.effect.ParticleMesh.Type, int) 
+     * @see ParticleEmitter#ParticleEmitter(java.lang.String, com.jme3.effect.ParticleMesh.Type, int)
      */
      */
     public ParticleMesh.Type getMeshType() {
     public ParticleMesh.Type getMeshType() {
         return meshType;
         return meshType;
@@ -325,26 +370,26 @@ public class ParticleEmitter extends Geometry {
     }
     }
 
 
     /**
     /**
-     * Returns true if particles should spawn in world space. 
-     * 
-     * @return true if particles should spawn in world space. 
-     * 
-     * @see ParticleEmitter#setInWorldSpace(boolean) 
+     * Returns true if particles should spawn in world space.
+     *
+     * @return true if particles should spawn in world space.
+     *
+     * @see ParticleEmitter#setInWorldSpace(boolean)
      */
      */
     public boolean isInWorldSpace() {
     public boolean isInWorldSpace() {
         return worldSpace;
         return worldSpace;
     }
     }
 
 
     /**
     /**
-     * Set to true if particles should spawn in world space. 
-     * 
+     * Set to true if particles should spawn in world space.
+     *
      * <p>If set to true and the particle emitter is moved in the scene,
      * <p>If set to true and the particle emitter is moved in the scene,
      * then particles that have already spawned won't be effected by this
      * then particles that have already spawned won't be effected by this
      * motion. If set to false, the particles will emit in local space
      * motion. If set to false, the particles will emit in local space
      * and when the emitter is moved, so are all the particles that
      * and when the emitter is moved, so are all the particles that
      * were emitted previously.
      * were emitted previously.
-     * 
-     * @param worldSpace true if particles should spawn in world space. 
+     *
+     * @param worldSpace true if particles should spawn in world space.
      */
      */
     public void setInWorldSpace(boolean worldSpace) {
     public void setInWorldSpace(boolean worldSpace) {
         this.setIgnoreTransform(worldSpace);
         this.setIgnoreTransform(worldSpace);
@@ -353,7 +398,7 @@ public class ParticleEmitter extends Geometry {
 
 
     /**
     /**
      * Returns the number of visible particles (spawned but not dead).
      * Returns the number of visible particles (spawned but not dead).
-     * 
+     *
      * @return the number of visible particles
      * @return the number of visible particles
      */
      */
     public int getNumVisibleParticles() {
     public int getNumVisibleParticles() {
@@ -365,7 +410,7 @@ public class ParticleEmitter extends Geometry {
      * Set the maximum amount of particles that
      * Set the maximum amount of particles that
      * can exist at the same time with this emitter.
      * can exist at the same time with this emitter.
      * Calling this method many times is not recommended.
      * Calling this method many times is not recommended.
-     * 
+     *
      * @param numParticles the maximum amount of particles that
      * @param numParticles the maximum amount of particles that
      * can exist at the same time with this emitter.
      * can exist at the same time with this emitter.
      */
      */
@@ -387,13 +432,13 @@ public class ParticleEmitter extends Geometry {
 
 
     /**
     /**
      * Returns a list of all particles (shouldn't be used in most cases).
      * Returns a list of all particles (shouldn't be used in most cases).
-     * 
+     *
      * <p>
      * <p>
      * This includes both existing and non-existing particles.
      * This includes both existing and non-existing particles.
      * The size of the array is set to the <code>numParticles</code> value
      * The size of the array is set to the <code>numParticles</code> value
      * specified in the constructor or {@link ParticleEmitter#setNumParticles(int) }
      * specified in the constructor or {@link ParticleEmitter#setNumParticles(int) }
-     * method. 
-     * 
+     * method.
+     *
      * @return a list of all particles.
      * @return a list of all particles.
      */
      */
     public Particle[] getParticles() {
     public Particle[] getParticles() {
@@ -401,11 +446,11 @@ public class ParticleEmitter extends Geometry {
     }
     }
 
 
     /**
     /**
-     * Get the normal which particles are facing. 
-     * 
-     * @return the normal which particles are facing. 
-     * 
-     * @see ParticleEmitter#setFaceNormal(com.jme3.math.Vector3f) 
+     * Get the normal which particles are facing.
+     *
+     * @return the normal which particles are facing.
+     *
+     * @see ParticleEmitter#setFaceNormal(com.jme3.math.Vector3f)
      */
      */
     public Vector3f getFaceNormal() {
     public Vector3f getFaceNormal() {
         if (Vector3f.isValidVector(faceNormal)) {
         if (Vector3f.isValidVector(faceNormal)) {
@@ -416,8 +461,8 @@ public class ParticleEmitter extends Geometry {
     }
     }
 
 
     /**
     /**
-     * Sets the normal which particles are facing. 
-     * 
+     * Sets the normal which particles are facing.
+     *
      * <p>By default, particles
      * <p>By default, particles
      * will face the camera, but for some effects (e.g shockwave) it may
      * will face the camera, but for some effects (e.g shockwave) it may
      * be necessary to face a specific direction instead. To restore
      * be necessary to face a specific direction instead. To restore
@@ -437,10 +482,10 @@ public class ParticleEmitter extends Geometry {
 
 
     /**
     /**
      * Returns the rotation speed in radians/sec for particles.
      * Returns the rotation speed in radians/sec for particles.
-     * 
+     *
      * @return the rotation speed in radians/sec for particles.
      * @return the rotation speed in radians/sec for particles.
-     * 
-     * @see ParticleEmitter#setRotateSpeed(float) 
+     *
+     * @see ParticleEmitter#setRotateSpeed(float)
      */
      */
     public float getRotateSpeed() {
     public float getRotateSpeed() {
         return rotateSpeed;
         return rotateSpeed;
@@ -449,7 +494,7 @@ public class ParticleEmitter extends Geometry {
     /**
     /**
      * Set the rotation speed in radians/sec for particles
      * Set the rotation speed in radians/sec for particles
      * spawned after the invocation of this method.
      * spawned after the invocation of this method.
-     * 
+     *
      * @param rotateSpeed the rotation speed in radians/sec for particles
      * @param rotateSpeed the rotation speed in radians/sec for particles
      * spawned after the invocation of this method.
      * spawned after the invocation of this method.
      */
      */
@@ -459,12 +504,12 @@ public class ParticleEmitter extends Geometry {
 
 
     /**
     /**
      * Returns true if every particle spawned
      * Returns true if every particle spawned
-     * should have a random facing angle. 
-     * 
+     * should have a random facing angle.
+     *
      * @return true if every particle spawned
      * @return true if every particle spawned
-     * should have a random facing angle. 
-     * 
-     * @see ParticleEmitter#setRandomAngle(boolean) 
+     * should have a random facing angle.
+     *
+     * @see ParticleEmitter#setRandomAngle(boolean)
      */
      */
     public boolean isRandomAngle() {
     public boolean isRandomAngle() {
         return randomAngle;
         return randomAngle;
@@ -472,8 +517,8 @@ public class ParticleEmitter extends Geometry {
 
 
     /**
     /**
      * Set to true if every particle spawned
      * Set to true if every particle spawned
-     * should have a random facing angle. 
-     * 
+     * should have a random facing angle.
+     *
      * @param randomAngle if every particle spawned
      * @param randomAngle if every particle spawned
      * should have a random facing angle.
      * should have a random facing angle.
      */
      */
@@ -484,11 +529,11 @@ public class ParticleEmitter extends Geometry {
     /**
     /**
      * Returns true if every particle spawned should get a random
      * Returns true if every particle spawned should get a random
      * image.
      * image.
-     * 
+     *
      * @return True if every particle spawned should get a random
      * @return True if every particle spawned should get a random
      * image.
      * image.
-     * 
-     * @see ParticleEmitter#setSelectRandomImage(boolean) 
+     *
+     * @see ParticleEmitter#setSelectRandomImage(boolean)
      */
      */
     public boolean isSelectRandomImage() {
     public boolean isSelectRandomImage() {
         return selectRandomImage;
         return selectRandomImage;
@@ -498,7 +543,7 @@ public class ParticleEmitter extends Geometry {
      * Set to true if every particle spawned
      * Set to true if every particle spawned
      * should get a random image from a pool of images constructed from
      * should get a random image from a pool of images constructed from
      * the texture, with X by Y possible images.
      * the texture, with X by Y possible images.
-     * 
+     *
      * <p>By default, X and Y are equal
      * <p>By default, X and Y are equal
      * to 1, thus allowing only 1 possible image to be selected, but if the
      * to 1, thus allowing only 1 possible image to be selected, but if the
      * particle is configured with multiple images by using {@link ParticleEmitter#setImagesX(int) }
      * particle is configured with multiple images by using {@link ParticleEmitter#setImagesX(int) }
@@ -506,7 +551,7 @@ public class ParticleEmitter extends Geometry {
      * can be selected. Setting to false will cause each particle to have an animation
      * can be selected. Setting to false will cause each particle to have an animation
      * of images displayed, starting at image 1, and going until image X*Y when
      * of images displayed, starting at image 1, and going until image X*Y when
      * the particle reaches its end of life.
      * the particle reaches its end of life.
-     * 
+     *
      * @param selectRandomImage True if every particle spawned should get a random
      * @param selectRandomImage True if every particle spawned should get a random
      * image.
      * image.
      */
      */
@@ -516,10 +561,10 @@ public class ParticleEmitter extends Geometry {
 
 
     /**
     /**
      * Check if particles spawned should face their velocity.
      * Check if particles spawned should face their velocity.
-     * 
+     *
      * @return True if particles spawned should face their velocity.
      * @return True if particles spawned should face their velocity.
-     * 
-     * @see ParticleEmitter#setFacingVelocity(boolean) 
+     *
+     * @see ParticleEmitter#setFacingVelocity(boolean)
      */
      */
     public boolean isFacingVelocity() {
     public boolean isFacingVelocity() {
         return facingVelocity;
         return facingVelocity;
@@ -528,11 +573,11 @@ public class ParticleEmitter extends Geometry {
     /**
     /**
      * Set to true if particles spawned should face
      * Set to true if particles spawned should face
      * their velocity (or direction to which they are moving towards).
      * their velocity (or direction to which they are moving towards).
-     * 
+     *
      * <p>This is typically used for e.g spark effects.
      * <p>This is typically used for e.g spark effects.
-     * 
+     *
      * @param followVelocity True if particles spawned should face their velocity.
      * @param followVelocity True if particles spawned should face their velocity.
-     * 
+     *
      */
      */
     public void setFacingVelocity(boolean followVelocity) {
     public void setFacingVelocity(boolean followVelocity) {
         this.facingVelocity = followVelocity;
         this.facingVelocity = followVelocity;
@@ -540,10 +585,10 @@ public class ParticleEmitter extends Geometry {
 
 
     /**
     /**
      * Get the end color of the particles spawned.
      * Get the end color of the particles spawned.
-     * 
+     *
      * @return the end color of the particles spawned.
      * @return the end color of the particles spawned.
-     * 
-     * @see ParticleEmitter#setEndColor(com.jme3.math.ColorRGBA) 
+     *
+     * @see ParticleEmitter#setEndColor(com.jme3.math.ColorRGBA)
      */
      */
     public ColorRGBA getEndColor() {
     public ColorRGBA getEndColor() {
         return endColor;
         return endColor;
@@ -551,12 +596,12 @@ public class ParticleEmitter extends Geometry {
 
 
     /**
     /**
      * Set the end color of the particles spawned.
      * Set the end color of the particles spawned.
-     * 
+     *
      * <p>The
      * <p>The
      * particle color at any time is determined by blending the start color
      * particle color at any time is determined by blending the start color
      * and end color based on the particle's current time of life relative
      * and end color based on the particle's current time of life relative
      * to its end of life.
      * to its end of life.
-     * 
+     *
      * @param endColor the end color of the particles spawned.
      * @param endColor the end color of the particles spawned.
      */
      */
     public void setEndColor(ColorRGBA endColor) {
     public void setEndColor(ColorRGBA endColor) {
@@ -565,10 +610,10 @@ public class ParticleEmitter extends Geometry {
 
 
     /**
     /**
      * Get the end size of the particles spawned.
      * Get the end size of the particles spawned.
-     * 
+     *
      * @return the end size of the particles spawned.
      * @return the end size of the particles spawned.
-     * 
-     * @see ParticleEmitter#setEndSize(float) 
+     *
+     * @see ParticleEmitter#setEndSize(float)
      */
      */
     public float getEndSize() {
     public float getEndSize() {
         return endSize;
         return endSize;
@@ -576,12 +621,12 @@ public class ParticleEmitter extends Geometry {
 
 
     /**
     /**
      * Set the end size of the particles spawned.
      * Set the end size of the particles spawned.
-     * 
+     *
      * <p>The
      * <p>The
      * particle size at any time is determined by blending the start size
      * particle size at any time is determined by blending the start size
      * and end size based on the particle's current time of life relative
      * and end size based on the particle's current time of life relative
      * to its end of life.
      * to its end of life.
-     * 
+     *
      * @param endSize the end size of the particles spawned.
      * @param endSize the end size of the particles spawned.
      */
      */
     public void setEndSize(float endSize) {
     public void setEndSize(float endSize) {
@@ -590,10 +635,10 @@ public class ParticleEmitter extends Geometry {
 
 
     /**
     /**
      * Get the gravity vector.
      * Get the gravity vector.
-     * 
+     *
      * @return the gravity vector.
      * @return the gravity vector.
-     * 
-     * @see ParticleEmitter#setGravity(com.jme3.math.Vector3f) 
+     *
+     * @see ParticleEmitter#setGravity(com.jme3.math.Vector3f)
      */
      */
     public Vector3f getGravity() {
     public Vector3f getGravity() {
         return gravity;
         return gravity;
@@ -601,7 +646,7 @@ public class ParticleEmitter extends Geometry {
 
 
     /**
     /**
      * This method sets the gravity vector.
      * This method sets the gravity vector.
-     * 
+     *
      * @param gravity the gravity vector
      * @param gravity the gravity vector
      */
      */
     public void setGravity(Vector3f gravity) {
     public void setGravity(Vector3f gravity) {
@@ -610,7 +655,7 @@ public class ParticleEmitter extends Geometry {
 
 
     /**
     /**
      * Sets the gravity vector.
      * Sets the gravity vector.
-     * 
+     *
      * @param x the x component of the gravity vector
      * @param x the x component of the gravity vector
      * @param y the y component of the gravity vector
      * @param y the y component of the gravity vector
      * @param z the z component of the gravity vector
      * @param z the z component of the gravity vector
@@ -623,10 +668,10 @@ public class ParticleEmitter extends Geometry {
 
 
     /**
     /**
      * Get the high value of life.
      * Get the high value of life.
-     * 
+     *
      * @return the high value of life.
      * @return the high value of life.
-     * 
-     * @see ParticleEmitter#setHighLife(float) 
+     *
+     * @see ParticleEmitter#setHighLife(float)
      */
      */
     public float getHighLife() {
     public float getHighLife() {
         return highLife;
         return highLife;
@@ -634,10 +679,10 @@ public class ParticleEmitter extends Geometry {
 
 
     /**
     /**
      * Set the high value of life.
      * Set the high value of life.
-     * 
+     *
      * <p>The particle's lifetime/expiration
      * <p>The particle's lifetime/expiration
      * is determined by randomly selecting a time between low life and high life.
      * is determined by randomly selecting a time between low life and high life.
-     * 
+     *
      * @param highLife the high value of life.
      * @param highLife the high value of life.
      */
      */
     public void setHighLife(float highLife) {
     public void setHighLife(float highLife) {
@@ -646,10 +691,10 @@ public class ParticleEmitter extends Geometry {
 
 
     /**
     /**
      * Get the number of images along the X axis (width).
      * Get the number of images along the X axis (width).
-     * 
+     *
      * @return the number of images along the X axis (width).
      * @return the number of images along the X axis (width).
-     * 
-     * @see ParticleEmitter#setImagesX(int) 
+     *
+     * @see ParticleEmitter#setImagesX(int)
      */
      */
     public int getImagesX() {
     public int getImagesX() {
         return imagesX;
         return imagesX;
@@ -657,11 +702,11 @@ public class ParticleEmitter extends Geometry {
 
 
     /**
     /**
      * Set the number of images along the X axis (width).
      * Set the number of images along the X axis (width).
-     * 
+     *
      * <p>To determine
      * <p>To determine
      * how multiple particle images are selected and used, see the
      * how multiple particle images are selected and used, see the
      * {@link ParticleEmitter#setSelectRandomImage(boolean) } method.
      * {@link ParticleEmitter#setSelectRandomImage(boolean) } method.
-     * 
+     *
      * @param imagesX the number of images along the X axis (width).
      * @param imagesX the number of images along the X axis (width).
      */
      */
     public void setImagesX(int imagesX) {
     public void setImagesX(int imagesX) {
@@ -671,10 +716,10 @@ public class ParticleEmitter extends Geometry {
 
 
     /**
     /**
      * Get the number of images along the Y axis (height).
      * Get the number of images along the Y axis (height).
-     * 
+     *
      * @return the number of images along the Y axis (height).
      * @return the number of images along the Y axis (height).
-     * 
-     * @see ParticleEmitter#setImagesY(int) 
+     *
+     * @see ParticleEmitter#setImagesY(int)
      */
      */
     public int getImagesY() {
     public int getImagesY() {
         return imagesY;
         return imagesY;
@@ -682,10 +727,10 @@ public class ParticleEmitter extends Geometry {
 
 
     /**
     /**
      * Set the number of images along the Y axis (height).
      * Set the number of images along the Y axis (height).
-     * 
+     *
      * <p>To determine how multiple particle images are selected and used, see the
      * <p>To determine how multiple particle images are selected and used, see the
      * {@link ParticleEmitter#setSelectRandomImage(boolean) } method.
      * {@link ParticleEmitter#setSelectRandomImage(boolean) } method.
-     * 
+     *
      * @param imagesY the number of images along the Y axis (height).
      * @param imagesY the number of images along the Y axis (height).
      */
      */
     public void setImagesY(int imagesY) {
     public void setImagesY(int imagesY) {
@@ -695,10 +740,10 @@ public class ParticleEmitter extends Geometry {
 
 
     /**
     /**
      * Get the low value of life.
      * Get the low value of life.
-     * 
+     *
      * @return the low value of life.
      * @return the low value of life.
-     * 
-     * @see ParticleEmitter#setLowLife(float) 
+     *
+     * @see ParticleEmitter#setLowLife(float)
      */
      */
     public float getLowLife() {
     public float getLowLife() {
         return lowLife;
         return lowLife;
@@ -706,10 +751,10 @@ public class ParticleEmitter extends Geometry {
 
 
     /**
     /**
      * Set the low value of life.
      * Set the low value of life.
-     * 
+     *
      * <p>The particle's lifetime/expiration
      * <p>The particle's lifetime/expiration
      * is determined by randomly selecting a time between low life and high life.
      * is determined by randomly selecting a time between low life and high life.
-     * 
+     *
      * @param lowLife the low value of life.
      * @param lowLife the low value of life.
      */
      */
     public void setLowLife(float lowLife) {
     public void setLowLife(float lowLife) {
@@ -719,11 +764,11 @@ public class ParticleEmitter extends Geometry {
     /**
     /**
      * Get the number of particles to spawn per
      * Get the number of particles to spawn per
      * second.
      * second.
-     * 
+     *
      * @return the number of particles to spawn per
      * @return the number of particles to spawn per
      * second.
      * second.
-     * 
-     * @see ParticleEmitter#setParticlesPerSec(float) 
+     *
+     * @see ParticleEmitter#setParticlesPerSec(float)
      */
      */
     public float getParticlesPerSec() {
     public float getParticlesPerSec() {
         return particlesPerSec;
         return particlesPerSec;
@@ -732,7 +777,7 @@ public class ParticleEmitter extends Geometry {
     /**
     /**
      * Set the number of particles to spawn per
      * Set the number of particles to spawn per
      * second.
      * second.
-     * 
+     *
      * @param particlesPerSec the number of particles to spawn per
      * @param particlesPerSec the number of particles to spawn per
      * second.
      * second.
      */
      */
@@ -740,13 +785,13 @@ public class ParticleEmitter extends Geometry {
         this.particlesPerSec = particlesPerSec;
         this.particlesPerSec = particlesPerSec;
         timeDifference = 0;
         timeDifference = 0;
     }
     }
-    
+
     /**
     /**
      * Get the start color of the particles spawned.
      * Get the start color of the particles spawned.
-     * 
+     *
      * @return the start color of the particles spawned.
      * @return the start color of the particles spawned.
-     * 
-     * @see ParticleEmitter#setStartColor(com.jme3.math.ColorRGBA) 
+     *
+     * @see ParticleEmitter#setStartColor(com.jme3.math.ColorRGBA)
      */
      */
     public ColorRGBA getStartColor() {
     public ColorRGBA getStartColor() {
         return startColor;
         return startColor;
@@ -754,11 +799,11 @@ public class ParticleEmitter extends Geometry {
 
 
     /**
     /**
      * Set the start color of the particles spawned.
      * Set the start color of the particles spawned.
-     * 
+     *
      * <p>The particle color at any time is determined by blending the start color
      * <p>The particle color at any time is determined by blending the start color
      * and end color based on the particle's current time of life relative
      * and end color based on the particle's current time of life relative
      * to its end of life.
      * to its end of life.
-     * 
+     *
      * @param startColor the start color of the particles spawned
      * @param startColor the start color of the particles spawned
      */
      */
     public void setStartColor(ColorRGBA startColor) {
     public void setStartColor(ColorRGBA startColor) {
@@ -767,10 +812,10 @@ public class ParticleEmitter extends Geometry {
 
 
     /**
     /**
      * Get the start color of the particles spawned.
      * Get the start color of the particles spawned.
-     * 
+     *
      * @return the start color of the particles spawned.
      * @return the start color of the particles spawned.
-     * 
-     * @see ParticleEmitter#setStartSize(float) 
+     *
+     * @see ParticleEmitter#setStartSize(float)
      */
      */
     public float getStartSize() {
     public float getStartSize() {
         return startSize;
         return startSize;
@@ -778,11 +823,11 @@ public class ParticleEmitter extends Geometry {
 
 
     /**
     /**
      * Set the start size of the particles spawned.
      * Set the start size of the particles spawned.
-     * 
+     *
      * <p>The particle size at any time is determined by blending the start size
      * <p>The particle size at any time is determined by blending the start size
      * and end size based on the particle's current time of life relative
      * and end size based on the particle's current time of life relative
      * to its end of life.
      * to its end of life.
-     * 
+     *
      * @param startSize the start size of the particles spawned.
      * @param startSize the start size of the particles spawned.
      */
      */
     public void setStartSize(float startSize) {
     public void setStartSize(float startSize) {
@@ -805,10 +850,10 @@ public class ParticleEmitter extends Geometry {
      * gravity.
      * gravity.
      *
      *
      * @deprecated
      * @deprecated
-     * This method is deprecated. 
+     * This method is deprecated.
      * Use ParticleEmitter.getParticleInfluencer().setInitialVelocity(initialVelocity); instead.
      * Use ParticleEmitter.getParticleInfluencer().setInitialVelocity(initialVelocity); instead.
      *
      *
-     * @see ParticleEmitter#setVelocityVariation(float) 
+     * @see ParticleEmitter#setVelocityVariation(float)
      * @see ParticleEmitter#setGravity(float)
      * @see ParticleEmitter#setGravity(float)
      */
      */
     @Deprecated
     @Deprecated
@@ -818,7 +863,7 @@ public class ParticleEmitter extends Geometry {
 
 
     /**
     /**
      * @deprecated
      * @deprecated
-     * This method is deprecated. 
+     * This method is deprecated.
      * Use ParticleEmitter.getParticleInfluencer().getVelocityVariation(); instead.
      * Use ParticleEmitter.getParticleInfluencer().getVelocityVariation(); instead.
      * @return the initial velocity variation factor
      * @return the initial velocity variation factor
      */
      */
@@ -833,9 +878,9 @@ public class ParticleEmitter extends Geometry {
      * from 0 to 1, where 0 means particles are to spawn with exactly
      * from 0 to 1, where 0 means particles are to spawn with exactly
      * the velocity given in {@link ParticleEmitter#setStartVel(com.jme3.math.Vector3f) },
      * the velocity given in {@link ParticleEmitter#setStartVel(com.jme3.math.Vector3f) },
      * and 1 means particles are to spawn with a completely random velocity.
      * and 1 means particles are to spawn with a completely random velocity.
-     * 
+     *
      * @deprecated
      * @deprecated
-     * This method is deprecated. 
+     * This method is deprecated.
      * Use ParticleEmitter.getParticleInfluencer().setVelocityVariation(variation); instead.
      * Use ParticleEmitter.getParticleInfluencer().setVelocityVariation(variation); instead.
      */
      */
     @Deprecated
     @Deprecated
@@ -923,7 +968,7 @@ public class ParticleEmitter extends Geometry {
 
 
         vars.release();
         vars.release();
     }
     }
-    
+
     /**
     /**
      * Instantly kills all active particles, after this method is called, all
      * Instantly kills all active particles, after this method is called, all
      * particles will be dead and no longer visible.
      * particles will be dead and no longer visible.
@@ -935,12 +980,12 @@ public class ParticleEmitter extends Geometry {
             }
             }
         }
         }
     }
     }
-    
+
     /**
     /**
      * Kills the particle at the given index.
      * Kills the particle at the given index.
-     * 
+     *
      * @param index The index of the particle to kill
      * @param index The index of the particle to kill
-     * @see #getParticles() 
+     * @see #getParticles()
      */
      */
     public void killParticle(int index){
     public void killParticle(int index){
         freeParticle(index);
         freeParticle(index);
@@ -995,7 +1040,7 @@ public class ParticleEmitter extends Geometry {
             p.imageIndex = (int) (b * imagesX * imagesY);
             p.imageIndex = (int) (b * imagesX * imagesY);
         }
         }
     }
     }
-    
+
     private void updateParticleState(float tpf) {
     private void updateParticleState(float tpf) {
         // Force world transform to update
         // Force world transform to update
         this.getWorldTransform();
         this.getWorldTransform();
@@ -1028,7 +1073,7 @@ public class ParticleEmitter extends Geometry {
                 firstUnUsed++;
                 firstUnUsed++;
             }
             }
         }
         }
-        
+
         // Spawns particles within the tpf timeslot with proper age
         // Spawns particles within the tpf timeslot with proper age
         float interval = 1f / particlesPerSec;
         float interval = 1f / particlesPerSec;
         float originalTpf = tpf;
         float originalTpf = tpf;
@@ -1065,10 +1110,10 @@ public class ParticleEmitter extends Geometry {
 
 
     /**
     /**
      * Set to enable or disable the particle emitter
      * Set to enable or disable the particle emitter
-     * 
+     *
      * <p>When a particle is
      * <p>When a particle is
      * disabled, it will be "frozen in time" and not update.
      * disabled, it will be "frozen in time" and not update.
-     * 
+     *
      * @param enabled True to enable the particle emitter
      * @param enabled True to enable the particle emitter
      */
      */
     public void setEnabled(boolean enabled) {
     public void setEnabled(boolean enabled) {
@@ -1077,10 +1122,10 @@ public class ParticleEmitter extends Geometry {
 
 
     /**
     /**
      * Check if a particle emitter is enabled for update.
      * Check if a particle emitter is enabled for update.
-     * 
+     *
      * @return True if a particle emitter is enabled for update.
      * @return True if a particle emitter is enabled for update.
-     * 
-     * @see ParticleEmitter#setEnabled(boolean) 
+     *
+     * @see ParticleEmitter#setEnabled(boolean)
      */
      */
     public boolean isEnabled() {
     public boolean isEnabled() {
         return enabled;
         return enabled;
@@ -1088,7 +1133,7 @@ public class ParticleEmitter extends Geometry {
 
 
     /**
     /**
      * Callback from Control.update(), do not use.
      * Callback from Control.update(), do not use.
-     * @param tpf 
+     * @param tpf
      */
      */
     public void updateFromControl(float tpf) {
     public void updateFromControl(float tpf) {
         if (enabled) {
         if (enabled) {
@@ -1098,9 +1143,9 @@ public class ParticleEmitter extends Geometry {
 
 
     /**
     /**
      * Callback from Control.render(), do not use.
      * Callback from Control.render(), do not use.
-     * 
+     *
      * @param rm
      * @param rm
-     * @param vp 
+     * @param vp
      */
      */
     private void renderFromControl(RenderManager rm, ViewPort vp) {
     private void renderFromControl(RenderManager rm, ViewPort vp) {
         Camera cam = vp.getCamera();
         Camera cam = vp.getCamera();
@@ -1236,7 +1281,7 @@ public class ParticleEmitter extends Geometry {
                 gravity.y = ic.readFloat("gravity", 0);
                 gravity.y = ic.readFloat("gravity", 0);
             }
             }
         } else {
         } else {
-            // since the parentEmitter is not loaded, it must be 
+            // since the parentEmitter is not loaded, it must be
             // loaded separately
             // loaded separately
             control = getControl(ParticleEmitterControl.class);
             control = getControl(ParticleEmitterControl.class);
             control.parentEmitter = this;
             control.parentEmitter = this;

+ 33 - 2
jme3-core/src/main/java/com/jme3/effect/influencers/DefaultParticleInfluencer.java

@@ -39,6 +39,8 @@ import com.jme3.export.JmeImporter;
 import com.jme3.export.OutputCapsule;
 import com.jme3.export.OutputCapsule;
 import com.jme3.math.FastMath;
 import com.jme3.math.FastMath;
 import com.jme3.math.Vector3f;
 import com.jme3.math.Vector3f;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 import java.io.IOException;
 
 
 /**
 /**
@@ -49,7 +51,7 @@ import java.io.IOException;
  */
  */
 public class DefaultParticleInfluencer implements ParticleInfluencer {
 public class DefaultParticleInfluencer implements ParticleInfluencer {
 
 
-    //Version #1 : changed startVelocity to initialvelocity for consistency with accessors 
+    //Version #1 : changed startVelocity to initialvelocity for consistency with accessors
     //and also changed it in serialization
     //and also changed it in serialization
     public static final int SAVABLE_VERSION = 1;
     public static final int SAVABLE_VERSION = 1;
     /** Temporary variable used to help with calculations. */
     /** Temporary variable used to help with calculations. */
@@ -94,7 +96,7 @@ public class DefaultParticleInfluencer implements ParticleInfluencer {
             initialVelocity = (Vector3f) ic.readSavable("startVelocity", Vector3f.ZERO.clone());
             initialVelocity = (Vector3f) ic.readSavable("startVelocity", Vector3f.ZERO.clone());
         }else{
         }else{
             initialVelocity = (Vector3f) ic.readSavable("initialVelocity", Vector3f.ZERO.clone());
             initialVelocity = (Vector3f) ic.readSavable("initialVelocity", Vector3f.ZERO.clone());
-        }       
+        }
         velocityVariation = ic.readFloat("variation", 0.2f);
         velocityVariation = ic.readFloat("variation", 0.2f);
     }
     }
 
 
@@ -109,6 +111,35 @@ public class DefaultParticleInfluencer implements ParticleInfluencer {
         }
         }
     }
     }
 
 
+    /**
+     *  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.initialVelocity = cloner.clone(initialVelocity);
+
+        // Change in behavior: I'm cloning 'for real' the 'temp' field because
+        // otherwise it will be shared across all clones.  Note: if this is
+        // ok because of how its used then it might as well be static and let
+        // everything share it.
+        // Note 2: transient fields _are_ cloned just like anything else so
+        // thinking it wouldn't get cloned is also not right.
+        // -pspeed
+        this.temp = cloner.clone(temp);
+    }
+
     @Override
     @Override
     public void setInitialVelocity(Vector3f initialVelocity) {
     public void setInitialVelocity(Vector3f initialVelocity) {
         this.initialVelocity.set(initialVelocity);
         this.initialVelocity.set(initialVelocity);

+ 21 - 0
jme3-core/src/main/java/com/jme3/effect/influencers/EmptyParticleInfluencer.java

@@ -36,6 +36,8 @@ import com.jme3.effect.shapes.EmitterShape;
 import com.jme3.export.JmeExporter;
 import com.jme3.export.JmeExporter;
 import com.jme3.export.JmeImporter;
 import com.jme3.export.JmeImporter;
 import com.jme3.math.Vector3f;
 import com.jme3.math.Vector3f;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 import java.io.IOException;
 
 
 /**
 /**
@@ -83,4 +85,23 @@ public class EmptyParticleInfluencer implements ParticleInfluencer {
             throw new AssertionError();
             throw new AssertionError();
         }
         }
     }
     }
+
+    /**
+     *  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 ) {
+    }
 }
 }

+ 2 - 0
jme3-core/src/main/java/com/jme3/effect/influencers/NewtonianParticleInfluencer.java

@@ -39,6 +39,8 @@ import com.jme3.export.JmeImporter;
 import com.jme3.export.OutputCapsule;
 import com.jme3.export.OutputCapsule;
 import com.jme3.math.FastMath;
 import com.jme3.math.FastMath;
 import com.jme3.math.Matrix3f;
 import com.jme3.math.Matrix3f;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 import java.io.IOException;
 
 
 /**
 /**

+ 2 - 1
jme3-core/src/main/java/com/jme3/effect/influencers/ParticleInfluencer.java

@@ -36,12 +36,13 @@ import com.jme3.effect.ParticleEmitter;
 import com.jme3.effect.shapes.EmitterShape;
 import com.jme3.effect.shapes.EmitterShape;
 import com.jme3.export.Savable;
 import com.jme3.export.Savable;
 import com.jme3.math.Vector3f;
 import com.jme3.math.Vector3f;
+import com.jme3.util.clone.JmeCloneable;
 
 
 /**
 /**
  * An interface that defines the methods to affect initial velocity of the particles.
  * An interface that defines the methods to affect initial velocity of the particles.
  * @author Marcin Roguski (Kaelthas)
  * @author Marcin Roguski (Kaelthas)
  */
  */
-public interface ParticleInfluencer extends Savable, Cloneable {
+public interface ParticleInfluencer extends Savable, Cloneable, JmeCloneable {
 
 
     /**
     /**
      * This method influences the particle.
      * This method influences the particle.

+ 17 - 4
jme3-core/src/main/java/com/jme3/effect/influencers/RadialParticleInfluencer.java

@@ -38,6 +38,7 @@ import com.jme3.export.JmeImporter;
 import com.jme3.export.OutputCapsule;
 import com.jme3.export.OutputCapsule;
 import com.jme3.math.FastMath;
 import com.jme3.math.FastMath;
 import com.jme3.math.Vector3f;
 import com.jme3.math.Vector3f;
+import com.jme3.util.clone.Cloner;
 import java.io.IOException;
 import java.io.IOException;
 
 
 /**
 /**
@@ -81,7 +82,7 @@ public class RadialParticleInfluencer extends DefaultParticleInfluencer {
 
 
     /**
     /**
      * the origin used for computing the radial velocity direction
      * the origin used for computing the radial velocity direction
-     * @param origin 
+     * @param origin
      */
      */
     public void setOrigin(Vector3f origin) {
     public void setOrigin(Vector3f origin) {
         this.origin = origin;
         this.origin = origin;
@@ -97,7 +98,7 @@ public class RadialParticleInfluencer extends DefaultParticleInfluencer {
 
 
     /**
     /**
      * the radial velocity
      * the radial velocity
-     * @param radialVelocity 
+     * @param radialVelocity
      */
      */
     public void setRadialVelocity(float radialVelocity) {
     public void setRadialVelocity(float radialVelocity) {
         this.radialVelocity = radialVelocity;
         this.radialVelocity = radialVelocity;
@@ -105,7 +106,7 @@ public class RadialParticleInfluencer extends DefaultParticleInfluencer {
 
 
     /**
     /**
      * nullify y component of particle velocity to make the effect expand only on x and z axis
      * nullify y component of particle velocity to make the effect expand only on x and z axis
-     * @return 
+     * @return
      */
      */
     public boolean isHorizontal() {
     public boolean isHorizontal() {
         return horizontal;
         return horizontal;
@@ -113,12 +114,24 @@ public class RadialParticleInfluencer extends DefaultParticleInfluencer {
 
 
     /**
     /**
      * nullify y component of particle velocity to make the effect expand only on x and z axis
      * nullify y component of particle velocity to make the effect expand only on x and z axis
-     * @param horizontal 
+     * @param horizontal
      */
      */
     public void setHorizontal(boolean horizontal) {
     public void setHorizontal(boolean horizontal) {
         this.horizontal = horizontal;
         this.horizontal = horizontal;
     }
     }
 
 
+    /**
+     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
+     */
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) {
+        super.cloneFields(cloner, original);
+
+        // Change in behavior: the old origin was not cloned -pspeed
+        this.origin = cloner.clone(origin);
+    }
+
+
     @Override
     @Override
     public void write(JmeExporter ex) throws IOException {
     public void write(JmeExporter ex) throws IOException {
         super.write(ex);
         super.write(ex);

+ 23 - 0
jme3-core/src/main/java/com/jme3/effect/shapes/EmitterBoxShape.java

@@ -37,6 +37,8 @@ import com.jme3.export.JmeImporter;
 import com.jme3.export.OutputCapsule;
 import com.jme3.export.OutputCapsule;
 import com.jme3.math.FastMath;
 import com.jme3.math.FastMath;
 import com.jme3.math.Vector3f;
 import com.jme3.math.Vector3f;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 import java.io.IOException;
 
 
 public class EmitterBoxShape implements EmitterShape {
 public class EmitterBoxShape implements EmitterShape {
@@ -86,6 +88,27 @@ public class EmitterBoxShape implements EmitterShape {
         }
         }
     }
     }
 
 
+    /**
+     *  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.min = cloner.clone(min);
+        this.len = cloner.clone(len);
+    }
+
     public Vector3f getMin() {
     public Vector3f getMin() {
         return min;
         return min;
     }
     }

+ 24 - 1
jme3-core/src/main/java/com/jme3/effect/shapes/EmitterMeshVertexShape.java

@@ -40,6 +40,8 @@ import com.jme3.math.Vector3f;
 import com.jme3.scene.Mesh;
 import com.jme3.scene.Mesh;
 import com.jme3.scene.VertexBuffer.Type;
 import com.jme3.scene.VertexBuffer.Type;
 import com.jme3.util.BufferUtils;
 import com.jme3.util.BufferUtils;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashMap;
@@ -168,6 +170,27 @@ public class EmitterMeshVertexShape implements EmitterShape {
         }
         }
     }
     }
 
 
+    /**
+     *  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.vertices = cloner.clone(vertices);
+        this.normals = cloner.clone(normals);
+    }
+
     @Override
     @Override
     public void write(JmeExporter ex) throws IOException {
     public void write(JmeExporter ex) throws IOException {
         OutputCapsule oc = ex.getCapsule(this);
         OutputCapsule oc = ex.getCapsule(this);
@@ -180,7 +203,7 @@ public class EmitterMeshVertexShape implements EmitterShape {
     public void read(JmeImporter im) throws IOException {
     public void read(JmeImporter im) throws IOException {
         InputCapsule ic = im.getCapsule(this);
         InputCapsule ic = im.getCapsule(this);
         this.vertices = ic.readSavableArrayList("vertices", null);
         this.vertices = ic.readSavableArrayList("vertices", null);
-        
+
         List<List<Vector3f>> tmpNormals = ic.readSavableArrayList("normals", null);
         List<List<Vector3f>> tmpNormals = ic.readSavableArrayList("normals", null);
         if (tmpNormals != null){
         if (tmpNormals != null){
             this.normals = tmpNormals;
             this.normals = tmpNormals;

+ 22 - 0
jme3-core/src/main/java/com/jme3/effect/shapes/EmitterPointShape.java

@@ -35,6 +35,8 @@ import com.jme3.export.JmeExporter;
 import com.jme3.export.JmeImporter;
 import com.jme3.export.JmeImporter;
 import com.jme3.export.OutputCapsule;
 import com.jme3.export.OutputCapsule;
 import com.jme3.math.Vector3f;
 import com.jme3.math.Vector3f;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 import java.io.IOException;
 
 
 public class EmitterPointShape implements EmitterShape {
 public class EmitterPointShape implements EmitterShape {
@@ -59,6 +61,26 @@ public class EmitterPointShape implements EmitterShape {
         }
         }
     }
     }
 
 
+    /**
+     *  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.point = cloner.clone(point);
+    }
+
     @Override
     @Override
     public void getRandomPoint(Vector3f store) {
     public void getRandomPoint(Vector3f store) {
         store.set(point);
         store.set(point);

+ 2 - 1
jme3-core/src/main/java/com/jme3/effect/shapes/EmitterShape.java

@@ -33,12 +33,13 @@ package com.jme3.effect.shapes;
 
 
 import com.jme3.export.Savable;
 import com.jme3.export.Savable;
 import com.jme3.math.Vector3f;
 import com.jme3.math.Vector3f;
+import com.jme3.util.clone.JmeCloneable;
 
 
 /**
 /**
  * This interface declares methods used by all shapes that represent particle emitters.
  * This interface declares methods used by all shapes that represent particle emitters.
  * @author Kirill
  * @author Kirill
  */
  */
-public interface EmitterShape extends Savable, Cloneable {
+public interface EmitterShape extends Savable, Cloneable, JmeCloneable {
 
 
     /**
     /**
      * This method fills in the initial position of the particle.
      * This method fills in the initial position of the particle.

+ 22 - 0
jme3-core/src/main/java/com/jme3/effect/shapes/EmitterSphereShape.java

@@ -37,6 +37,8 @@ import com.jme3.export.JmeImporter;
 import com.jme3.export.OutputCapsule;
 import com.jme3.export.OutputCapsule;
 import com.jme3.math.FastMath;
 import com.jme3.math.FastMath;
 import com.jme3.math.Vector3f;
 import com.jme3.math.Vector3f;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 import java.io.IOException;
 
 
 public class EmitterSphereShape implements EmitterShape {
 public class EmitterSphereShape implements EmitterShape {
@@ -71,6 +73,26 @@ public class EmitterSphereShape implements EmitterShape {
         }
         }
     }
     }
 
 
+    /**
+     *  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.center = cloner.clone(center);
+    }
+
     @Override
     @Override
     public void getRandomPoint(Vector3f store) {
     public void getRandomPoint(Vector3f store) {
         do {
         do {

+ 36 - 14
jme3-core/src/main/java/com/jme3/font/BitmapText.java

@@ -38,6 +38,7 @@ import com.jme3.material.Material;
 import com.jme3.math.ColorRGBA;
 import com.jme3.math.ColorRGBA;
 import com.jme3.renderer.RenderManager;
 import com.jme3.renderer.RenderManager;
 import com.jme3.scene.Node;
 import com.jme3.scene.Node;
+import com.jme3.util.clone.Cloner;
 import java.util.regex.Matcher;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.regex.Pattern;
 
 
@@ -84,6 +85,27 @@ public class BitmapText extends Node {
         return clone;
         return clone;
     }
     }
 
 
+    /**
+     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
+     */
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) {
+        super.cloneFields(cloner, original);
+
+        for( int i = 0; i < textPages.length; i++ ) {
+            textPages[i] = cloner.clone(textPages[i]);
+        }
+        this.block = cloner.clone(block);
+
+        // Change in behavior: The 'letters' field was not cloned or recreated
+        // before.  I'm not sure how this worked and suspect BitmapText was just
+        // not cloneable if you planned to change the text later. -pspeed
+        this.letters = new Letters(font, block, letters.getQuad().isRightToLeft());
+
+        // Just noticed BitmapText is not even writable/readable really...
+        // so I guess cloning doesn't come up that often.
+    }
+
     public BitmapFont getFont() {
     public BitmapFont getFont() {
         return font;
         return font;
     }
     }
@@ -115,10 +137,10 @@ public class BitmapText extends Node {
      *
      *
      * @param text String to change text to
      * @param text String to change text to
      */
      */
-    public void setText(String text) {            
+    public void setText(String text) {
         text = text == null ? "" : text;
         text = text == null ? "" : text;
 
 
-        if (text == block.getText() || block.getText().equals(text)) { 	
+        if (text == block.getText() || block.getText().equals(text)) {
             return;
             return;
         }
         }
 
 
@@ -126,24 +148,24 @@ public class BitmapText extends Node {
         The problem with the below block is that StringBlock carries
         The problem with the below block is that StringBlock carries
         pretty much all of the text-related state of the BitmapText such
         pretty much all of the text-related state of the BitmapText such
         as size, text box, alignment, etc.
         as size, text box, alignment, etc.
-        
+
         I'm not sure why this change was needed and the commit message was
         I'm not sure why this change was needed and the commit message was
-        not entirely helpful because it purports to fix a problem that I've 
+        not entirely helpful because it purports to fix a problem that I've
         never encountered.
         never encountered.
-        
+
         If block.setText("") doesn't do the right thing then that's where
         If block.setText("") doesn't do the right thing then that's where
         the fix should go because StringBlock carries too much information to
         the fix should go because StringBlock carries too much information to
         be blown away every time.  -pspeed
         be blown away every time.  -pspeed
-        
+
         Change was made:
         Change was made:
         http://code.google.com/p/jmonkeyengine/source/detail?spec=svn9389&r=9389
         http://code.google.com/p/jmonkeyengine/source/detail?spec=svn9389&r=9389
         Diff:
         Diff:
         http://code.google.com/p/jmonkeyengine/source/diff?path=/trunk/engine/src/core/com/jme3/font/BitmapText.java&format=side&r=9389&old_path=/trunk/engine/src/core/com/jme3/font/BitmapText.java&old=8843
         http://code.google.com/p/jmonkeyengine/source/diff?path=/trunk/engine/src/core/com/jme3/font/BitmapText.java&format=side&r=9389&old_path=/trunk/engine/src/core/com/jme3/font/BitmapText.java&old=8843
-        
+
         // If the text is empty, reset
         // If the text is empty, reset
         if (text.isEmpty()) {
         if (text.isEmpty()) {
             detachAllChildren();
             detachAllChildren();
-                
+
             for (int page = 0; page < textPages.length; page++) {
             for (int page = 0; page < textPages.length; page++) {
                 textPages[page] = new BitmapTextPage(font, true, page);
                 textPages[page] = new BitmapTextPage(font, true, page);
                 attachChild(textPages[page]);
                 attachChild(textPages[page]);
@@ -153,7 +175,7 @@ public class BitmapText extends Node {
             letters = new Letters(font, block, letters.getQuad().isRightToLeft());
             letters = new Letters(font, block, letters.getQuad().isRightToLeft());
         }
         }
         */
         */
-            
+
         // Update the text content
         // Update the text content
         block.setText(text);
         block.setText(text);
         letters.setText(text);
         letters.setText(text);
@@ -185,7 +207,7 @@ public class BitmapText extends Node {
         letters.invalidate(); // TODO: Don't have to align.
         letters.invalidate(); // TODO: Don't have to align.
         needRefresh = true;
         needRefresh = true;
     }
     }
-    
+
     /**
     /**
      *  Sets an overall alpha that will be applied to all
      *  Sets an overall alpha that will be applied to all
      *  letters.  If the alpha passed is -1 then alpha reverts
      *  letters.  If the alpha passed is -1 then alpha reverts
@@ -196,7 +218,7 @@ public class BitmapText extends Node {
     public void setAlpha(float alpha) {
     public void setAlpha(float alpha) {
         letters.setBaseAlpha(alpha);
         letters.setBaseAlpha(alpha);
         needRefresh = true;
         needRefresh = true;
-    }    
+    }
 
 
     public float getAlpha() {
     public float getAlpha() {
         return letters.getBaseAlpha();
         return letters.getBaseAlpha();
@@ -414,17 +436,17 @@ public class BitmapText extends Node {
         if( mp == null ) {
         if( mp == null ) {
             return null;
             return null;
         }
         }
-        return (ColorRGBA)mp.getValue(); 
+        return (ColorRGBA)mp.getValue();
     }
     }
 
 
     public void render(RenderManager rm, ColorRGBA color) {
     public void render(RenderManager rm, ColorRGBA color) {
         for (BitmapTextPage page : textPages) {
         for (BitmapTextPage page : textPages) {
             Material mat = page.getMaterial();
             Material mat = page.getMaterial();
             mat.setTexture("ColorMap", page.getTexture());
             mat.setTexture("ColorMap", page.getTexture());
-            //ColorRGBA original = getColor(mat, "Color");            
+            //ColorRGBA original = getColor(mat, "Color");
             //mat.setColor("Color", color);
             //mat.setColor("Color", color);
             mat.render(page, rm);
             mat.render(page, rm);
-            
+
             //if( original == null ) {
             //if( original == null ) {
             //    mat.clearParam("Color");
             //    mat.clearParam("Color");
             //} else {
             //} else {

+ 7 - 0
jme3-core/src/main/java/com/jme3/font/BitmapTextPage.java

@@ -123,6 +123,13 @@ class BitmapTextPage extends Geometry {
         return clone;
         return clone;
     }
     }
 
 
+    // Here is where one might add JmeCloneable related stuff except
+    // the old clone() method doesn't actually bother to clone anything.
+    // The arrays and the pageQuads are shared across all BitmapTextPage
+    // clones and it doesn't seem to bother anything.  That means the
+    // fields could probably just as well be static... but this code is
+    // all very fragile.  I'm not tipping that particular boat today. -pspeed
+
     void assemble(Letters quads) {
     void assemble(Letters quads) {
         pageQuads.clear();
         pageQuads.clear();
         quads.rewind();
         quads.rewind();

+ 31 - 31
jme3-core/src/main/java/com/jme3/font/Letters.java

@@ -53,7 +53,7 @@ class Letters {
     private ColorRGBA baseColor = null;
     private ColorRGBA baseColor = null;
     private float baseAlpha = -1;
     private float baseAlpha = -1;
     private String plainText;
     private String plainText;
-    
+
     Letters(BitmapFont font, StringBlock bound, boolean rightToLeft) {
     Letters(BitmapFont font, StringBlock bound, boolean rightToLeft) {
         final String text = bound.getText();
         final String text = bound.getText();
         this.block = bound;
         this.block = bound;
@@ -78,10 +78,10 @@ class Letters {
                     // Give the letter a default color if
                     // Give the letter a default color if
                     // one has been provided.
                     // one has been provided.
                     l.setColor( baseColor );
                     l.setColor( baseColor );
-                }                
+                }
             }
             }
         }
         }
-        
+
         LinkedList<Range> ranges = colorTags.getTags();
         LinkedList<Range> ranges = colorTags.getTags();
         if (!ranges.isEmpty()) {
         if (!ranges.isEmpty()) {
             for (int i = 0; i < ranges.size()-1; i++) {
             for (int i = 0; i < ranges.size()-1; i++) {
@@ -92,7 +92,7 @@ class Letters {
             Range end = ranges.getLast();
             Range end = ranges.getLast();
             setColor(end.start, plainText.length(), end.color);
             setColor(end.start, plainText.length(), end.color);
         }
         }
-        
+
         invalidate();
         invalidate();
     }
     }
 
 
@@ -103,17 +103,17 @@ class Letters {
     LetterQuad getTail() {
     LetterQuad getTail() {
         return tail;
         return tail;
     }
     }
-    
+
     void update() {
     void update() {
         LetterQuad l = head;
         LetterQuad l = head;
         int lineCount = 1;
         int lineCount = 1;
         BitmapCharacter ellipsis = font.getCharSet().getCharacter(block.getEllipsisChar());
         BitmapCharacter ellipsis = font.getCharSet().getCharacter(block.getEllipsisChar());
         float ellipsisWidth = ellipsis!=null? ellipsis.getWidth()*getScale(): 0;
         float ellipsisWidth = ellipsis!=null? ellipsis.getWidth()*getScale(): 0;
- 
+
         while (!l.isTail()) {
         while (!l.isTail()) {
             if (l.isInvalid()) {
             if (l.isInvalid()) {
                 l.update(block);
                 l.update(block);
-                
+
                 if (l.isInvalid(block)) {
                 if (l.isInvalid(block)) {
                     switch (block.getLineWrapMode()) {
                     switch (block.getLineWrapMode()) {
                     case Character:
                     case Character:
@@ -144,7 +144,7 @@ class Letters {
                             }
                             }
                         }
                         }
                         break;
                         break;
-                    case NoWrap: 
+                    case NoWrap:
                         LetterQuad cursor = l.getPrevious();
                         LetterQuad cursor = l.getPrevious();
                         while (cursor.isInvalid(block, ellipsisWidth) && !cursor.isLineStart()) {
                         while (cursor.isInvalid(block, ellipsisWidth) && !cursor.isLineStart()) {
                             cursor = cursor.getPrevious();
                             cursor = cursor.getPrevious();
@@ -158,10 +158,10 @@ class Letters {
                             cursor = cursor.getNext();
                             cursor = cursor.getNext();
                         }
                         }
                         break;
                         break;
-                    case Clip: 
+                    case Clip:
                         // Clip the character that falls out of bounds
                         // Clip the character that falls out of bounds
                         l.clip(block);
                         l.clip(block);
-                        
+
                         // Clear the rest up to the next line feed.
                         // Clear the rest up to the next line feed.
                         for( LetterQuad q = l.getNext(); !q.isTail() && !q.isLineFeed(); q = q.getNext() ) {
                         for( LetterQuad q = l.getNext(); !q.isTail() && !q.isLineFeed(); q = q.getNext() ) {
                             q.setBitmapChar(null);
                             q.setBitmapChar(null);
@@ -178,12 +178,12 @@ class Letters {
             }
             }
             l = l.getNext();
             l = l.getNext();
         }
         }
-        
+
         align();
         align();
         block.setLineCount(lineCount);
         block.setLineCount(lineCount);
         rewind();
         rewind();
     }
     }
-    
+
     private void align() {
     private void align() {
         final Align alignment = block.getAlignment();
         final Align alignment = block.getAlignment();
         final VAlign valignment = block.getVerticalAlignment();
         final VAlign valignment = block.getVerticalAlignment();
@@ -233,7 +233,7 @@ class Letters {
         l.invalidate();
         l.invalidate();
         l.update(block); // TODO: update from l
         l.update(block); // TODO: update from l
     }
     }
-    
+
     float getCharacterX0() {
     float getCharacterX0() {
         return current.getX0();
         return current.getX0();
     }
     }
@@ -241,54 +241,54 @@ class Letters {
     float getCharacterY0() {
     float getCharacterY0() {
         return current.getY0();
         return current.getY0();
     }
     }
-    
+
     float getCharacterX1() {
     float getCharacterX1() {
         return current.getX1();
         return current.getX1();
     }
     }
-    
+
     float getCharacterY1() {
     float getCharacterY1() {
         return current.getY1();
         return current.getY1();
     }
     }
-    
+
     float getCharacterAlignX() {
     float getCharacterAlignX() {
         return current.getAlignX();
         return current.getAlignX();
     }
     }
-    
+
     float getCharacterAlignY() {
     float getCharacterAlignY() {
         return current.getAlignY();
         return current.getAlignY();
     }
     }
-    
+
     float getCharacterWidth() {
     float getCharacterWidth() {
         return current.getWidth();
         return current.getWidth();
     }
     }
-    
+
     float getCharacterHeight() {
     float getCharacterHeight() {
         return current.getHeight();
         return current.getHeight();
     }
     }
-    
+
     public boolean nextCharacter() {
     public boolean nextCharacter() {
         if (current.isTail())
         if (current.isTail())
             return false;
             return false;
         current = current.getNext();
         current = current.getNext();
         return true;
         return true;
     }
     }
-    
+
     public int getCharacterSetPage() {
     public int getCharacterSetPage() {
         return current.getBitmapChar().getPage();
         return current.getBitmapChar().getPage();
     }
     }
-    
+
     public LetterQuad getQuad() {
     public LetterQuad getQuad() {
         return current;
         return current;
     }
     }
-    
+
     public void rewind() {
     public void rewind() {
         current = head;
         current = head;
     }
     }
-    
+
     public void invalidate() {
     public void invalidate() {
         invalidate(head);
         invalidate(head);
     }
     }
-    
+
     public void invalidate(LetterQuad cursor) {
     public void invalidate(LetterQuad cursor) {
         totalWidth = -1;
         totalWidth = -1;
         totalHeight = -1;
         totalHeight = -1;
@@ -298,7 +298,7 @@ class Letters {
             cursor = cursor.getNext();
             cursor = cursor.getNext();
         }
         }
     }
     }
-    
+
     float getScale() {
     float getScale() {
         return block.getSize() / font.getCharSet().getRenderedSize();
         return block.getSize() / font.getCharSet().getRenderedSize();
     }
     }
@@ -306,7 +306,7 @@ class Letters {
     public boolean isPrintable() {
     public boolean isPrintable() {
         return current.getBitmapChar() != null;
         return current.getBitmapChar() != null;
     }
     }
-    
+
     float getTotalWidth() {
     float getTotalWidth() {
         validateSize();
         validateSize();
         return totalWidth;
         return totalWidth;
@@ -316,7 +316,7 @@ class Letters {
         validateSize();
         validateSize();
         return totalHeight;
         return totalHeight;
     }
     }
-    
+
     void validateSize() {
     void validateSize() {
         if (totalWidth < 0) {
         if (totalWidth < 0) {
             LetterQuad l = head;
             LetterQuad l = head;
@@ -371,11 +371,11 @@ class Letters {
             cursor = cursor.getNext();
             cursor = cursor.getNext();
         }
         }
     }
     }
- 
+
     float getBaseAlpha() {
     float getBaseAlpha() {
         return baseAlpha;
         return baseAlpha;
     }
     }
-    
+
     void setBaseAlpha( float alpha ) {        this.baseAlpha = alpha;
     void setBaseAlpha( float alpha ) {        this.baseAlpha = alpha;
         colorTags.setBaseAlpha(alpha);
         colorTags.setBaseAlpha(alpha);
 
 
@@ -409,7 +409,7 @@ class Letters {
                 setColor(end.start, plainText.length(), end.color);
                 setColor(end.start, plainText.length(), end.color);
             }
             }
         }
         }
-        
+
         invalidate();
         invalidate();
     }
     }
 
 

+ 16 - 0
jme3-core/src/main/java/com/jme3/light/Light.java

@@ -184,6 +184,22 @@ public abstract class Light implements Savable, Cloneable {
         this.enabled = enabled;
         this.enabled = enabled;
     }
     }
 
 
+    public boolean isFrustumCheckNeeded() {
+      return frustumCheckNeeded;
+    }
+
+    public void setFrustumCheckNeeded(boolean frustumCheckNeeded) {
+      this.frustumCheckNeeded = frustumCheckNeeded;
+    }
+
+    public boolean isIntersectsFrustum() {
+      return intersectsFrustum;
+    }
+
+    public void setIntersectsFrustum(boolean intersectsFrustum) {
+      this.intersectsFrustum = intersectsFrustum;
+    }
+    
     /**
     /**
      * Determines if the light intersects with the given bounding box.
      * Determines if the light intersects with the given bounding box.
      * <p>
      * <p>

+ 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.export.*;
 import com.jme3.scene.Spatial;
 import com.jme3.scene.Spatial;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import com.jme3.util.SortUtil;
 import com.jme3.util.SortUtil;
 import java.io.IOException;
 import java.io.IOException;
 import java.util.*;
 import java.util.*;
@@ -40,10 +42,10 @@ import java.util.*;
 /**
 /**
  * <code>LightList</code> is used internally by {@link Spatial}s to manage
  * <code>LightList</code> is used internally by {@link Spatial}s to manage
  * lights that are attached to them.
  * lights that are attached to them.
- * 
+ *
  * @author Kirill Vainer
  * @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 Light[] list, tlist;
     private float[] distToOwner;
     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}.
      * Creates a <code>LightList</code> for the given {@link Spatial}.
-     * 
+     *
      * @param owner The spatial owner
      * @param owner The spatial owner
      */
      */
     public LightList(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.
      * Set the owner of the LightList. Only used for cloning.
-     * @param owner 
+     * @param owner
      */
      */
     public void setOwner(Spatial owner){
     public void setOwner(Spatial owner){
         this.owner = owner;
         this.owner = owner;
@@ -118,7 +120,7 @@ public final class LightList implements Iterable<Light>, Savable, Cloneable {
 
 
     /**
     /**
      * Remove the light at the given index.
      * Remove the light at the given index.
-     * 
+     *
      * @param index
      * @param index
      */
      */
     public void remove(int 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.
      * Removes the given light from the LightList.
-     * 
+     *
      * @param l the light to remove
      * @param l the light to remove
      */
      */
     public void remove(Light l){
     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.
      * 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.
      * the individual lights might have changed.
-     * 
+     *
      *
      *
      * @param transformChanged Whether the spatial's transform has 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];
                 list[p] = parent.list[i];
                 distToOwner[p] = Float.NEGATIVE_INFINITY;
                 distToOwner[p] = Float.NEGATIVE_INFINITY;
             }
             }
-            
+
             listSize = local.listSize + parent.listSize;
             listSize = local.listSize + parent.listSize;
         }else{
         }else{
             listSize = local.listSize;
             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.
      * Returns an iterator that can be used to iterate over this LightList.
-     * 
+     *
      * @return 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() {
     public Iterator<Light> iterator() {
@@ -276,10 +278,10 @@ public final class LightList implements Iterable<Light>, Savable, Cloneable {
             public Light next() {
             public Light next() {
                 if (!hasNext())
                 if (!hasNext())
                     throw new NoSuchElementException();
                     throw new NoSuchElementException();
-                
+
                 return list[index++];
                 return list[index++];
             }
             }
-            
+
             public void remove() {
             public void remove() {
                 LightList.this.remove(--index);
                 LightList.this.remove(--index);
             }
             }
@@ -290,7 +292,7 @@ public final class LightList implements Iterable<Light>, Savable, Cloneable {
     public LightList clone(){
     public LightList clone(){
         try{
         try{
             LightList clone = (LightList) super.clone();
             LightList clone = (LightList) super.clone();
-            
+
             clone.owner = null;
             clone.owner = null;
             clone.list = list.clone();
             clone.list = list.clone();
             clone.distToOwner = distToOwner.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 {
     public void write(JmeExporter ex) throws IOException {
         OutputCapsule oc = ex.getCapsule(this);
         OutputCapsule oc = ex.getCapsule(this);
 //        oc.write(owner, "owner", null);
 //        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);
         List<Light> lights = ic.readSavableArrayList("lights", null);
         listSize = lights.size();
         listSize = lights.size();
-        
+
         // NOTE: make sure the array has a length of at least 1
         // NOTE: make sure the array has a length of at least 1
         int arraySize = Math.max(DEFAULT_SIZE, listSize);
         int arraySize = Math.max(DEFAULT_SIZE, listSize);
         list = new Light[arraySize];
         list = new Light[arraySize];
@@ -328,7 +348,7 @@ public final class LightList implements Iterable<Light>, Savable, Cloneable {
         for (int i = 0; i < listSize; i++){
         for (int i = 0; i < listSize; i++){
             list[i] = lights.get(i);
             list[i] = lights.get(i);
         }
         }
-        
+
         Arrays.fill(distToOwner, Float.NEGATIVE_INFINITY);
         Arrays.fill(distToOwner, Float.NEGATIVE_INFINITY);
     }
     }
 
 

+ 0 - 4
jme3-core/src/main/java/com/jme3/material/MatParam.java

@@ -34,7 +34,6 @@ package com.jme3.material;
 import com.jme3.asset.TextureKey;
 import com.jme3.asset.TextureKey;
 import com.jme3.export.*;
 import com.jme3.export.*;
 import com.jme3.math.*;
 import com.jme3.math.*;
-import com.jme3.renderer.Renderer;
 import com.jme3.shader.VarType;
 import com.jme3.shader.VarType;
 import com.jme3.texture.Texture;
 import com.jme3.texture.Texture;
 import com.jme3.texture.Texture.WrapMode;
 import com.jme3.texture.Texture.WrapMode;
@@ -129,9 +128,6 @@ public class MatParam implements Savable, Cloneable {
         this.value = value;
         this.value = value;
     }
     }
 
 
-    void apply(Renderer r, Technique technique) {
-        technique.updateUniformParam(getPrefixedName(), getVarType(), getValue());
-    }
 
 
     /**
     /**
      * Returns the material parameter value as it would appear in a J3M
      * Returns the material parameter value as it would appear in a J3M

+ 151 - 0
jme3-core/src/main/java/com/jme3/material/MatParamOverride.java

@@ -0,0 +1,151 @@
+/*
+ * Copyright (c) 2009-2016 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.material;
+
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.scene.Spatial;
+import com.jme3.shader.VarType;
+import java.io.IOException;
+
+/**
+ * <code>MatParamOverride</code> is a mechanism by which
+ * {@link MatParam material parameters} can be overridden on the scene graph.
+ * <p>
+ * A scene branch which has a <code>MatParamOverride</code> applied to it will
+ * cause all material parameters with the same name and type to have their value
+ * replaced with the value set on the <code>MatParamOverride</code>. If those
+ * parameters are mapped to a define, then the define will be overridden as well
+ * using the same rules as the ones used for regular material parameters.
+ * <p>
+ * <code>MatParamOverrides</code> are applied to a {@link Spatial} via the
+ * {@link Spatial#addMatParamOverride(com.jme3.material.MatParamOverride)}
+ * method. They are propagated to child <code>Spatials</code> via
+ * {@link Spatial#updateGeometricState()} similar to how lights are propagated.
+ * <p>
+ * Example:<br>
+ * <pre>
+ * {@code
+ *
+ * Geometry box = new Geometry("Box", new Box(1,1,1));
+ * Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+ * mat.setColor("Color", ColorRGBA.Blue);
+ * box.setMaterial(mat);
+ * rootNode.attachChild(box);
+ *
+ * // ... later ...
+ * MatParamOverride override = new MatParamOverride(Type.Vector4, "Color", ColorRGBA.Red);
+ * rootNode.addMatParamOverride(override);
+ *
+ * // After adding the override to the root node, the box becomes red.
+ * }
+ * </pre>
+ *
+ * @author Kirill Vainer
+ * @see Spatial#addMatParamOverride(com.jme3.material.MatParamOverride)
+ * @see Spatial#getWorldMatParamOverrides()
+ */
+public final class MatParamOverride extends MatParam {
+
+    private boolean enabled = true;
+
+    /**
+     * Serialization only. Do not use.
+     */
+    public MatParamOverride() {
+        super();
+    }
+
+    /**
+     * Create a new <code>MatParamOverride</code>.
+     *
+     * Overrides are created enabled by default.
+     *
+     * @param type The type of parameter.
+     * @param name The name of the parameter.
+     * @param value The value to set the material parameter to.
+     */
+    public MatParamOverride(VarType type, String name, Object value) {
+        super(type, name, value);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        return super.equals(obj) && this.enabled == ((MatParamOverride) obj).enabled;
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = super.hashCode();
+        hash = 59 * hash + (enabled ? 1 : 0);
+        return hash;
+    }
+
+    /**
+     * Determine if the <code>MatParamOverride</code> is enabled or disabled.
+     *
+     * @return true if enabled, false if disabled.
+     * @see #setEnabled(boolean)
+     */
+    public boolean isEnabled() {
+        return enabled;
+    }
+
+    /**
+     * Enable or disable this <code>MatParamOverride</code>.
+     *
+     * When disabled, the override will continue to propagate through the scene
+     * graph like before, but it will have no effect on materials. Overrides are
+     * enabled by default.
+     *
+     * @param enabled Whether to enable or disable this override.
+     */
+    public void setEnabled(boolean enabled) {
+        this.enabled = enabled;
+    }
+
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        super.write(ex);
+        OutputCapsule oc = ex.getCapsule(this);
+        oc.write(enabled, "enabled", true);
+    }
+
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        super.read(im);
+        InputCapsule ic = im.getCapsule(this);
+        enabled = ic.readBoolean("enabled", true);
+    }
+}

+ 0 - 6
jme3-core/src/main/java/com/jme3/material/MatParamTexture.java

@@ -100,12 +100,6 @@ public class MatParamTexture extends MatParam {
         return unit;
         return unit;
     }
     }
 
 
-    @Override
-    public void apply(Renderer r, Technique technique) {
-        TechniqueDef techDef = technique.getDef();
-        r.setTexture(getUnit(), getTextureValue());
-        technique.updateUniformParam(getPrefixedName(), getVarType(), getUnit());
-    }
 
 
     @Override
     @Override
     public void write(JmeExporter ex) throws IOException {
     public void write(JmeExporter ex) throws IOException {

+ 137 - 355
jme3-core/src/main/java/com/jme3/material/Material.java

@@ -44,18 +44,16 @@ import com.jme3.math.*;
 import com.jme3.renderer.Caps;
 import com.jme3.renderer.Caps;
 import com.jme3.renderer.RenderManager;
 import com.jme3.renderer.RenderManager;
 import com.jme3.renderer.Renderer;
 import com.jme3.renderer.Renderer;
-import com.jme3.renderer.RendererException;
 import com.jme3.renderer.queue.RenderQueue.Bucket;
 import com.jme3.renderer.queue.RenderQueue.Bucket;
 import com.jme3.scene.Geometry;
 import com.jme3.scene.Geometry;
-import com.jme3.scene.Mesh;
-import com.jme3.scene.instancing.InstancedGeometry;
 import com.jme3.shader.Shader;
 import com.jme3.shader.Shader;
 import com.jme3.shader.Uniform;
 import com.jme3.shader.Uniform;
+import com.jme3.shader.UniformBindingManager;
 import com.jme3.shader.VarType;
 import com.jme3.shader.VarType;
+import com.jme3.texture.Image;
 import com.jme3.texture.Texture;
 import com.jme3.texture.Texture;
 import com.jme3.texture.image.ColorSpace;
 import com.jme3.texture.image.ColorSpace;
 import com.jme3.util.ListMap;
 import com.jme3.util.ListMap;
-import com.jme3.util.TempVars;
 import java.io.IOException;
 import java.io.IOException;
 import java.util.*;
 import java.util.*;
 import java.util.logging.Level;
 import java.util.logging.Level;
@@ -79,7 +77,6 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
     private static final Logger logger = Logger.getLogger(Material.class.getName());
     private static final Logger logger = Logger.getLogger(Material.class.getName());
     private static final RenderState additiveLight = new RenderState();
     private static final RenderState additiveLight = new RenderState();
     private static final RenderState depthOnly = new RenderState();
     private static final RenderState depthOnly = new RenderState();
-    private static final Quaternion nullDirLight = new Quaternion(0, -1, 0, -1);
 
 
     static {
     static {
         depthOnly.setDepthTest(true);
         depthOnly.setDepthTest(true);
@@ -175,22 +172,29 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
      * @return The sorting ID used for sorting geometries for rendering.
      * @return The sorting ID used for sorting geometries for rendering.
      */
      */
     public int getSortId() {
     public int getSortId() {
-        Technique t = getActiveTechnique();
-        if (sortingId == -1 && t != null && t.getShader() != null) {
-            int texId = -1;
+        if (sortingId == -1 && technique != null) {
+            sortingId = technique.getSortId() << 16;
+            int texturesSortId = 17;
             for (int i = 0; i < paramValues.size(); i++) {
             for (int i = 0; i < paramValues.size(); i++) {
                 MatParam param = paramValues.getValue(i);
                 MatParam param = paramValues.getValue(i);
-                if (param instanceof MatParamTexture) {
-                    MatParamTexture tex = (MatParamTexture) param;
-                    if (tex.getTextureValue() != null && tex.getTextureValue().getImage() != null) {
-                        if (texId == -1) {
-                            texId = 0;
-                        }
-                        texId += tex.getTextureValue().getImage().getId() % 0xff;
-                    }
+                if (!param.getVarType().isTextureType()) {
+                    continue;
+                }
+                Texture texture = (Texture) param.getValue();
+                if (texture == null) {
+                    continue;
+                }
+                Image image = texture.getImage();
+                if (image == null) {
+                    continue;
+                }
+                int textureId = image.getId();
+                if (textureId == -1) {
+                    textureId = 0;
                 }
                 }
+                texturesSortId = texturesSortId * 23 + textureId;
             }
             }
-            sortingId = texId + t.getShader().getId() * 1000;
+            sortingId |= texturesSortId & 0xFFFF;
         }
         }
         return sortingId;
         return sortingId;
     }
     }
@@ -215,6 +219,8 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
                 mat.paramValues.put(entry.getKey(), entry.getValue().clone());
                 mat.paramValues.put(entry.getKey(), entry.getValue().clone());
             }
             }
 
 
+            mat.sortingId = -1;
+            
             return mat;
             return mat;
         } catch (CloneNotSupportedException ex) {
         } catch (CloneNotSupportedException ex) {
             throw new AssertionError(ex);
             throw new AssertionError(ex);
@@ -444,7 +450,7 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
      *
      *
      * @see #setParam(java.lang.String, com.jme3.shader.VarType, java.lang.Object)
      * @see #setParam(java.lang.String, com.jme3.shader.VarType, java.lang.Object)
      */
      */
-    public ListMap getParamsMap() {
+    public ListMap<String, MatParam> getParamsMap() {
         return paramValues;
         return paramValues;
     }
     }
 
 
@@ -695,257 +701,6 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
         setParam(name, VarType.Vector4, value);
         setParam(name, VarType.Vector4, value);
     }
     }
 
 
-    private ColorRGBA getAmbientColor(LightList lightList, boolean removeLights) {
-        ambientLightColor.set(0, 0, 0, 1);
-        for (int j = 0; j < lightList.size(); j++) {
-            Light l = lightList.get(j);
-            if (l instanceof AmbientLight) {
-                ambientLightColor.addLocal(l.getColor());
-                if(removeLights){
-                    lightList.remove(l);
-                }
-            }
-        }
-        ambientLightColor.a = 1.0f;
-        return ambientLightColor;
-    }
-
-    private static void renderMeshFromGeometry(Renderer renderer, Geometry geom) {
-        Mesh mesh = geom.getMesh();
-        int lodLevel = geom.getLodLevel();
-        if (geom instanceof InstancedGeometry) {
-            InstancedGeometry instGeom = (InstancedGeometry) geom;
-            int numInstances = instGeom.getActualNumInstances();
-            if (numInstances == 0) {
-                return;
-            }
-            if (renderer.getCaps().contains(Caps.MeshInstancing)) {
-                renderer.renderMesh(mesh, lodLevel, numInstances, instGeom.getAllInstanceData());
-            } else {
-                throw new RendererException("Mesh instancing is not supported by the video hardware");
-            }
-        } else {
-            renderer.renderMesh(mesh, lodLevel, 1, null);
-        }
-    }
-
-    /**
-     * Uploads the lights in the light list as two uniform arrays.<br/><br/> *
-     * <p>
-     * <code>uniform vec4 g_LightColor[numLights];</code><br/> //
-     * g_LightColor.rgb is the diffuse/specular color of the light.<br/> //
-     * g_Lightcolor.a is the type of light, 0 = Directional, 1 = Point, <br/> //
-     * 2 = Spot. <br/> <br/>
-     * <code>uniform vec4 g_LightPosition[numLights];</code><br/> //
-     * g_LightPosition.xyz is the position of the light (for point lights)<br/>
-     * // or the direction of the light (for directional lights).<br/> //
-     * g_LightPosition.w is the inverse radius (1/r) of the light (for
-     * attenuation) <br/> </p>
-     */
-    protected int updateLightListUniforms(Shader shader, Geometry g, LightList lightList, int numLights, RenderManager rm, int startIndex) {
-        if (numLights == 0) { // this shader does not do lighting, ignore.
-            return 0;
-        }
-
-        Uniform lightData = shader.getUniform("g_LightData");
-        lightData.setVector4Length(numLights * 3);//8 lights * max 3
-        Uniform ambientColor = shader.getUniform("g_AmbientLightColor");
-
-
-        if (startIndex != 0) {
-            // apply additive blending for 2nd and future passes
-            rm.getRenderer().applyRenderState(additiveLight);
-            ambientColor.setValue(VarType.Vector4, ColorRGBA.Black);
-        }else{
-            ambientColor.setValue(VarType.Vector4, getAmbientColor(lightList,true));
-        }
-
-        int lightDataIndex = 0;
-        TempVars vars = TempVars.get();
-        Vector4f tmpVec = vars.vect4f1;
-        int curIndex;
-        int endIndex = numLights + startIndex;
-        for (curIndex = startIndex; curIndex < endIndex && curIndex < lightList.size(); curIndex++) {
-
-
-                Light l = lightList.get(curIndex);
-                if(l.getType() == Light.Type.Ambient){
-                    endIndex++;
-                    continue;
-                }
-                ColorRGBA color = l.getColor();
-                //Color
-                lightData.setVector4InArray(color.getRed(),
-                        color.getGreen(),
-                        color.getBlue(),
-                        l.getType().getId(),
-                        lightDataIndex);
-                lightDataIndex++;
-
-                switch (l.getType()) {
-                    case Directional:
-                        DirectionalLight dl = (DirectionalLight) l;
-                        Vector3f dir = dl.getDirection();
-                        //Data directly sent in view space to avoid a matrix mult for each pixel
-                        tmpVec.set(dir.getX(), dir.getY(), dir.getZ(), 0.0f);
-                        rm.getCurrentCamera().getViewMatrix().mult(tmpVec, tmpVec);
-//                        tmpVec.divideLocal(tmpVec.w);
-//                        tmpVec.normalizeLocal();
-                        lightData.setVector4InArray(tmpVec.getX(), tmpVec.getY(), tmpVec.getZ(), -1, lightDataIndex);
-                        lightDataIndex++;
-                        //PADDING
-                        lightData.setVector4InArray(0,0,0,0, lightDataIndex);
-                        lightDataIndex++;
-                        break;
-                    case Point:
-                        PointLight pl = (PointLight) l;
-                        Vector3f pos = pl.getPosition();
-                        float invRadius = pl.getInvRadius();
-                        tmpVec.set(pos.getX(), pos.getY(), pos.getZ(), 1.0f);
-                        rm.getCurrentCamera().getViewMatrix().mult(tmpVec, tmpVec);
-                        //tmpVec.divideLocal(tmpVec.w);
-                        lightData.setVector4InArray(tmpVec.getX(), tmpVec.getY(), tmpVec.getZ(), invRadius, lightDataIndex);
-                        lightDataIndex++;
-                        //PADDING
-                        lightData.setVector4InArray(0,0,0,0, lightDataIndex);
-                        lightDataIndex++;
-                        break;
-                    case Spot:
-                        SpotLight sl = (SpotLight) l;
-                        Vector3f pos2 = sl.getPosition();
-                        Vector3f dir2 = sl.getDirection();
-                        float invRange = sl.getInvSpotRange();
-                        float spotAngleCos = sl.getPackedAngleCos();
-                        tmpVec.set(pos2.getX(), pos2.getY(), pos2.getZ(),  1.0f);
-                        rm.getCurrentCamera().getViewMatrix().mult(tmpVec, tmpVec);
-                       // tmpVec.divideLocal(tmpVec.w);
-                        lightData.setVector4InArray(tmpVec.getX(), tmpVec.getY(), tmpVec.getZ(), invRange, lightDataIndex);
-                        lightDataIndex++;
-
-                        //We transform the spot direction in view space here to save 5 varying later in the lighting shader
-                        //one vec4 less and a vec4 that becomes a vec3
-                        //the downside is that spotAngleCos decoding happens now in the frag shader.
-                        tmpVec.set(dir2.getX(), dir2.getY(), dir2.getZ(),  0.0f);
-                        rm.getCurrentCamera().getViewMatrix().mult(tmpVec, tmpVec);
-                        tmpVec.normalizeLocal();
-                        lightData.setVector4InArray(tmpVec.getX(), tmpVec.getY(), tmpVec.getZ(), spotAngleCos, lightDataIndex);
-                        lightDataIndex++;
-                        break;
-                    default:
-                        throw new UnsupportedOperationException("Unknown type of light: " + l.getType());
-                }
-        }
-        vars.release();
-        //Padding of unsued buffer space
-        while(lightDataIndex < numLights * 3) {
-            lightData.setVector4InArray(0f, 0f, 0f, 0f, lightDataIndex);
-            lightDataIndex++;
-        }
-        return curIndex;
-    }
-
-    protected void renderMultipassLighting(Shader shader, Geometry g, LightList lightList, RenderManager rm) {
-
-        Renderer r = rm.getRenderer();
-        Uniform lightDir = shader.getUniform("g_LightDirection");
-        Uniform lightColor = shader.getUniform("g_LightColor");
-        Uniform lightPos = shader.getUniform("g_LightPosition");
-        Uniform ambientColor = shader.getUniform("g_AmbientLightColor");
-        boolean isFirstLight = true;
-        boolean isSecondLight = false;
-
-        for (int i = 0; i < lightList.size(); i++) {
-            Light l = lightList.get(i);
-            if (l instanceof AmbientLight) {
-                continue;
-            }
-
-            if (isFirstLight) {
-                // set ambient color for first light only
-                ambientColor.setValue(VarType.Vector4, getAmbientColor(lightList, false));
-                isFirstLight = false;
-                isSecondLight = true;
-            } else if (isSecondLight) {
-                ambientColor.setValue(VarType.Vector4, ColorRGBA.Black);
-                // apply additive blending for 2nd and future lights
-                r.applyRenderState(additiveLight);
-                isSecondLight = false;
-            }
-
-            TempVars vars = TempVars.get();
-            Quaternion tmpLightDirection = vars.quat1;
-            Quaternion tmpLightPosition = vars.quat2;
-            ColorRGBA tmpLightColor = vars.color;
-            Vector4f tmpVec = vars.vect4f1;
-
-            ColorRGBA color = l.getColor();
-            tmpLightColor.set(color);
-            tmpLightColor.a = l.getType().getId();
-            lightColor.setValue(VarType.Vector4, tmpLightColor);
-
-            switch (l.getType()) {
-                case Directional:
-                    DirectionalLight dl = (DirectionalLight) l;
-                    Vector3f dir = dl.getDirection();
-                    //FIXME : there is an inconstency here due to backward
-                    //compatibility of the lighting shader.
-                    //The directional light direction is passed in the
-                    //LightPosition uniform. The lighting shader needs to be
-                    //reworked though in order to fix this.
-                    tmpLightPosition.set(dir.getX(), dir.getY(), dir.getZ(), -1);
-                    lightPos.setValue(VarType.Vector4, tmpLightPosition);
-                    tmpLightDirection.set(0, 0, 0, 0);
-                    lightDir.setValue(VarType.Vector4, tmpLightDirection);
-                    break;
-                case Point:
-                    PointLight pl = (PointLight) l;
-                    Vector3f pos = pl.getPosition();
-                    float invRadius = pl.getInvRadius();
-
-                    tmpLightPosition.set(pos.getX(), pos.getY(), pos.getZ(), invRadius);
-                    lightPos.setValue(VarType.Vector4, tmpLightPosition);
-                    tmpLightDirection.set(0, 0, 0, 0);
-                    lightDir.setValue(VarType.Vector4, tmpLightDirection);
-                    break;
-                case Spot:
-                    SpotLight sl = (SpotLight) l;
-                    Vector3f pos2 = sl.getPosition();
-                    Vector3f dir2 = sl.getDirection();
-                    float invRange = sl.getInvSpotRange();
-                    float spotAngleCos = sl.getPackedAngleCos();
-
-                    tmpLightPosition.set(pos2.getX(), pos2.getY(), pos2.getZ(), invRange);
-                    lightPos.setValue(VarType.Vector4, tmpLightPosition);
-
-                    //We transform the spot direction in view space here to save 5 varying later in the lighting shader
-                    //one vec4 less and a vec4 that becomes a vec3
-                    //the downside is that spotAngleCos decoding happens now in the frag shader.
-                    tmpVec.set(dir2.getX(), dir2.getY(), dir2.getZ(), 0);
-                    rm.getCurrentCamera().getViewMatrix().mult(tmpVec, tmpVec);
-                    tmpLightDirection.set(tmpVec.getX(), tmpVec.getY(), tmpVec.getZ(), spotAngleCos);
-
-                    lightDir.setValue(VarType.Vector4, tmpLightDirection);
-
-                    break;
-                default:
-                    throw new UnsupportedOperationException("Unknown type of light: " + l.getType());
-            }
-            vars.release();
-            r.setShader(shader);
-            renderMeshFromGeometry(r, g);
-        }
-
-        if (isFirstLight) {
-            // Either there are no lights at all, or only ambient lights.
-            // Render a dummy "normal light" so we can see the ambient color.
-            ambientColor.setValue(VarType.Vector4, getAmbientColor(lightList, false));
-            lightColor.setValue(VarType.Vector4, ColorRGBA.BlackNoAlpha);
-            lightPos.setValue(VarType.Vector4, nullDirLight);
-            r.setShader(shader);
-            renderMeshFromGeometry(r, g);
-        }
-    }
-
     /**
     /**
      * Select the technique to use for rendering this material.
      * Select the technique to use for rendering this material.
      * <p>
      * <p>
@@ -974,9 +729,8 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
         Technique tech = techniques.get(name);
         Technique tech = techniques.get(name);
         // When choosing technique, we choose one that
         // When choosing technique, we choose one that
         // supports all the caps.
         // supports all the caps.
-        EnumSet<Caps> rendererCaps = renderManager.getRenderer().getCaps();
         if (tech == null) {
         if (tech == null) {
-
+            EnumSet<Caps> rendererCaps = renderManager.getRenderer().getCaps();
             if (name.equals("Default")) {
             if (name.equals("Default")) {
                 List<TechniqueDef> techDefs = def.getDefaultTechniques();
                 List<TechniqueDef> techDefs = def.getDefaultTechniques();
                 if (techDefs == null || techDefs.isEmpty()) {
                 if (techDefs == null || techDefs.isEmpty()) {
@@ -1025,20 +779,71 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
         }
         }
 
 
         technique = tech;
         technique = tech;
-        tech.makeCurrent(def.getAssetManager(), true, rendererCaps, renderManager);
+        tech.notifyTechniqueSwitched();
 
 
         // shader was changed
         // shader was changed
         sortingId = -1;
         sortingId = -1;
     }
     }
 
 
-    private void autoSelectTechnique(RenderManager rm) {
-        if (technique == null) {
-            selectTechnique("Default", rm);
-        } else {
-            technique.makeCurrent(def.getAssetManager(), false, rm.getRenderer().getCaps(), rm);
+    private void updateShaderMaterialParameters(Renderer renderer, Shader shader, List<MatParamOverride> overrides) {
+        int unit = 0;
+
+        if (overrides != null) {
+            for (MatParamOverride override : overrides) {
+                VarType type = override.getVarType();
+
+                MatParam paramDef = def.getMaterialParam(override.getName());
+                if (paramDef == null || paramDef.getVarType() != type || !override.isEnabled()) {
+                    continue;
+                }
+
+                Uniform uniform = shader.getUniform(override.getPrefixedName());
+                if (override.getValue() != null) {
+                    if (type.isTextureType()) {
+                        renderer.setTexture(unit, (Texture) override.getValue());
+                        uniform.setValue(VarType.Int, unit);
+                        unit++;
+                    } else {
+                        uniform.setValue(type, override.getValue());
+                    }
+                } else {
+                    uniform.clearValue();
+                }
+            }
+        }
+
+        for (int i = 0; i < paramValues.size(); i++) {
+            MatParam param = paramValues.getValue(i);
+            VarType type = param.getVarType();
+            Uniform uniform = shader.getUniform(param.getPrefixedName());
+
+            if (uniform.isSetByCurrentMaterial()) {
+                continue;
+            }
+
+            if (type.isTextureType()) {
+                renderer.setTexture(unit, (Texture) param.getValue());
+                uniform.setValue(VarType.Int, unit);
+                unit++;
+            } else {
+                uniform.setValue(type, param.getValue());
+            }
         }
         }
+
     }
     }
 
 
+    private void updateRenderState(RenderManager renderManager, Renderer renderer, TechniqueDef techniqueDef) {
+        if (renderManager.getForcedRenderState() != null) {
+            renderer.applyRenderState(renderManager.getForcedRenderState());
+        } else {
+            if (techniqueDef.getRenderState() != null) {
+                renderer.applyRenderState(techniqueDef.getRenderState().copyMergedTo(additionalState, mergedRenderState));
+            } else {
+                renderer.applyRenderState(RenderState.DEFAULT.copyMergedTo(additionalState, mergedRenderState));
+            }
+        }
+    }
+    
     /**
     /**
      * Preloads this material for the given render manager.
      * Preloads this material for the given render manager.
      * <p>
      * <p>
@@ -1046,20 +851,23 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
      * used for rendering, there won't be any delay since the material has
      * used for rendering, there won't be any delay since the material has
      * been already been setup for rendering.
      * been already been setup for rendering.
      *
      *
-     * @param rm The render manager to preload for
+     * @param renderManager The render manager to preload for
      */
      */
-    public void preload(RenderManager rm) {
-        autoSelectTechnique(rm);
-
-        Renderer r = rm.getRenderer();
-        TechniqueDef techDef = technique.getDef();
+    public void preload(RenderManager renderManager) {
+        if (technique == null) {
+            selectTechnique("Default", renderManager);
+        }
+        TechniqueDef techniqueDef = technique.getDef();
+        Renderer renderer = renderManager.getRenderer();
+        EnumSet<Caps> rendererCaps = renderer.getCaps();
 
 
-        Collection<MatParam> params = paramValues.values();
-        for (MatParam param : params) {
-            param.apply(r, technique);
+        if (techniqueDef.isNoRender()) {
+            return;
         }
         }
 
 
-        r.setShader(technique.getShader());
+        Shader shader = technique.makeCurrent(renderManager, null, null, rendererCaps);
+        updateShaderMaterialParameters(renderer, shader, null);
+        renderManager.getRenderer().setShader(shader);
     }
     }
 
 
     private void clearUniformsSetByCurrent(Shader shader) {
     private void clearUniformsSetByCurrent(Shader shader) {
@@ -1141,80 +949,46 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
      * </ul>
      * </ul>
      * </ul>
      * </ul>
      *
      *
-     * @param geom The geometry to render
+     * @param geometry The geometry to render
      * @param lights Presorted and filtered light list to use for rendering
      * @param lights Presorted and filtered light list to use for rendering
-     * @param rm The render manager requesting the rendering
+     * @param renderManager The render manager requesting the rendering
      */
      */
-    public void render(Geometry geom, LightList lights, RenderManager rm) {
-        autoSelectTechnique(rm);
-        TechniqueDef techDef = technique.getDef();
-
-        if (techDef.isNoRender()) return;
-
-        Renderer r = rm.getRenderer();
-
-        if (rm.getForcedRenderState() != null) {
-            r.applyRenderState(rm.getForcedRenderState());
-        } else {
-            if (techDef.getRenderState() != null) {
-                r.applyRenderState(techDef.getRenderState().copyMergedTo(additionalState, mergedRenderState));
-            } else {
-                r.applyRenderState(RenderState.DEFAULT.copyMergedTo(additionalState, mergedRenderState));
-            }
-        }
-
-
-        // update camera and world matrices
-        // NOTE: setWorldTransform should have been called already
-
-        // reset unchanged uniform flag
-        clearUniformsSetByCurrent(technique.getShader());
-        rm.updateUniformBindings(technique.getWorldBindUniforms());
-
-
-        // setup textures and uniforms
-        for (int i = 0; i < paramValues.size(); i++) {
-            MatParam param = paramValues.getValue(i);
-            param.apply(r, technique);
+    public void render(Geometry geometry, LightList lights, RenderManager renderManager) {
+        if (technique == null) {
+            selectTechnique("Default", renderManager);
         }
         }
-
-        Shader shader = technique.getShader();
-
-        // send lighting information, if needed
-        switch (techDef.getLightMode()) {
-            case Disable:
-                break;
-            case SinglePass:
-                int nbRenderedLights = 0;
-                resetUniformsNotSetByCurrent(shader);
-                if (lights.size() == 0) {
-                    nbRenderedLights = updateLightListUniforms(shader, geom, lights, rm.getSinglePassLightBatchSize(), rm, 0);
-                    r.setShader(shader);
-                    renderMeshFromGeometry(r, geom);
-                } else {
-                    while (nbRenderedLights < lights.size()) {
-                        nbRenderedLights = updateLightListUniforms(shader, geom, lights, rm.getSinglePassLightBatchSize(), rm, nbRenderedLights);
-                        r.setShader(shader);
-                        renderMeshFromGeometry(r, geom);
-                    }
-                }
-                return;
-            case FixedPipeline:
-                throw new IllegalArgumentException("OpenGL1 is not supported");
-            case MultiPass:
-                // NOTE: Special case!
-                resetUniformsNotSetByCurrent(shader);
-                renderMultipassLighting(shader, geom, lights, rm);
-                // very important, notice the return statement!
-                return;
+        
+        TechniqueDef techniqueDef = technique.getDef();
+        Renderer renderer = renderManager.getRenderer();
+        EnumSet<Caps> rendererCaps = renderer.getCaps();
+        
+        if (techniqueDef.isNoRender()) {
+            return;
         }
         }
 
 
-        // upload and bind shader
-        // any unset uniforms will be set to 0
+        // Apply render state
+        updateRenderState(renderManager, renderer, techniqueDef);
+
+        // Get world overrides
+        List<MatParamOverride> overrides = geometry.getWorldMatParamOverrides();
+
+        // Select shader to use
+        Shader shader = technique.makeCurrent(renderManager, overrides, lights, rendererCaps);
+        
+        // Begin tracking which uniforms were changed by material.
+        clearUniformsSetByCurrent(shader);
+        
+        // Set uniform bindings
+        renderManager.updateUniformBindings(shader);
+        
+        // Set material parameters
+        updateShaderMaterialParameters(renderer, shader, geometry.getWorldMatParamOverrides());
+        
+        // Clear any uniforms not changed by material.
         resetUniformsNotSetByCurrent(shader);
         resetUniformsNotSetByCurrent(shader);
-        r.setShader(shader);
-
-        renderMeshFromGeometry(r, geom);
+        
+        // Delegate rendering to the technique
+        technique.render(renderManager, shader, geometry, lights);
     }
     }
 
 
     /**
     /**
@@ -1239,6 +1013,14 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
         oc.write(name, "name", null);
         oc.write(name, "name", null);
         oc.writeStringSavableMap(paramValues, "parameters", null);
         oc.writeStringSavableMap(paramValues, "parameters", null);
     }
     }
+    
+    @Override
+    public String toString() {
+        return "Material[name=" + name + 
+                ", def=" + def.getName() + 
+                ", tech=" + technique.getDef().getName() + 
+                "]";
+    }
 
 
     public void read(JmeImporter im) throws IOException {
     public void read(JmeImporter im) throws IOException {
         InputCapsule ic = im.getCapsule(this);
         InputCapsule ic = im.getCapsule(this);

+ 52 - 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 applyPolyOffset = true;
     boolean stencilTest = false;
     boolean stencilTest = false;
     boolean applyStencilTest = false;
     boolean applyStencilTest = false;
+    float lineWidth = 1;
+    boolean applyLineWidth = false;
     TestFunction depthFunc = TestFunction.LessOrEqual;
     TestFunction depthFunc = TestFunction.LessOrEqual;
     //by default depth func will be applied anyway if depth test is applied
     //by default depth func will be applied anyway if depth test is applied
     boolean applyDepthFunc = false;    
     boolean applyDepthFunc = false;    
@@ -350,6 +352,9 @@ public class RenderState implements Cloneable, Savable {
         oc.write(backStencilDepthPassOperation, "backStencilDepthPassOperation", StencilOperation.Keep);
         oc.write(backStencilDepthPassOperation, "backStencilDepthPassOperation", StencilOperation.Keep);
         oc.write(frontStencilFunction, "frontStencilFunction", TestFunction.Always);
         oc.write(frontStencilFunction, "frontStencilFunction", TestFunction.Always);
         oc.write(backStencilFunction, "backStencilFunction", 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
         // Only "additional render state" has them set to false by default
         oc.write(applyPointSprite, "applyPointSprite", true);
         oc.write(applyPointSprite, "applyPointSprite", true);
@@ -364,8 +369,7 @@ public class RenderState implements Cloneable, Savable {
         oc.write(applyPolyOffset, "applyPolyOffset", true);
         oc.write(applyPolyOffset, "applyPolyOffset", true);
         oc.write(applyDepthFunc, "applyDepthFunc", true);
         oc.write(applyDepthFunc, "applyDepthFunc", true);
         oc.write(applyAlphaFunc, "applyAlphaFunc", false);
         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);
         backStencilFunction = ic.readEnum("backStencilFunction", TestFunction.class, TestFunction.Always);
         depthFunc = ic.readEnum("depthFunc", TestFunction.class, TestFunction.LessOrEqual);
         depthFunc = ic.readEnum("depthFunc", TestFunction.class, TestFunction.LessOrEqual);
         alphaFunc = ic.readEnum("alphaFunc", TestFunction.class, TestFunction.Greater);
         alphaFunc = ic.readEnum("alphaFunc", TestFunction.class, TestFunction.Greater);
+        lineWidth = ic.readFloat("lineWidth", 1);
+
 
 
         applyPointSprite = ic.readBoolean("applyPointSprite", true);
         applyPointSprite = ic.readBoolean("applyPointSprite", true);
         applyWireFrame = ic.readBoolean("applyWireFrame", true);
         applyWireFrame = ic.readBoolean("applyWireFrame", true);
@@ -407,6 +413,8 @@ public class RenderState implements Cloneable, Savable {
         applyPolyOffset = ic.readBoolean("applyPolyOffset", true);
         applyPolyOffset = ic.readBoolean("applyPolyOffset", true);
         applyDepthFunc = ic.readBoolean("applyDepthFunc", true);
         applyDepthFunc = ic.readBoolean("applyDepthFunc", true);
         applyAlphaFunc = ic.readBoolean("applyAlphaFunc", false);
         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;
         return true;
     }
     }
 
 
@@ -803,8 +815,20 @@ public class RenderState implements Cloneable, Savable {
         this.alphaFunc = alphaFunc;
         this.alphaFunc = alphaFunc;
         cachedHashCode = -1;
         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) {
+        if (lineWidth < 1f) {
+            throw new IllegalArgumentException("lineWidth must be greater than or equal to 1.0");
+        }
+        this.lineWidth = lineWidth;
+        this.applyLineWidth = true;
+        cachedHashCode = -1;
+    }
 
 
     /**
     /**
      * Check if stencil test is enabled.
      * Check if stencil test is enabled.
@@ -1118,8 +1142,16 @@ public class RenderState implements Cloneable, Savable {
     public TestFunction getAlphaFunc() {
     public TestFunction getAlphaFunc() {
         return alphaFunc;
         return alphaFunc;
     }
     }
-    
-    
+
+    /**
+     * returns the wireframe line width
+     *
+     * @return the line width
+     */
+    public float getLineWidth() {
+        return lineWidth;
+    }
+
 
 
     public boolean isApplyAlphaFallOff() {
     public boolean isApplyAlphaFallOff() {
         return applyAlphaFallOff;
         return applyAlphaFallOff;
@@ -1168,8 +1200,10 @@ public class RenderState implements Cloneable, Savable {
     public boolean isApplyAlphaFunc() {
     public boolean isApplyAlphaFunc() {
         return applyAlphaFunc;
         return applyAlphaFunc;
     }
     }
-    
-    
+
+    public boolean isApplyLineWidth() {
+        return applyLineWidth;
+    }
 
 
     /**
     /**
      *
      *
@@ -1200,6 +1234,7 @@ public class RenderState implements Cloneable, Savable {
             hash = 79 * hash + (this.backStencilDepthPassOperation != null ? this.backStencilDepthPassOperation.hashCode() : 0);
             hash = 79 * hash + (this.backStencilDepthPassOperation != null ? this.backStencilDepthPassOperation.hashCode() : 0);
             hash = 79 * hash + (this.frontStencilFunction != null ? this.frontStencilFunction.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 + (this.backStencilFunction != null ? this.backStencilFunction.hashCode() : 0);
+            hash = 79 * hash + Float.floatToIntBits(this.lineWidth);
             cachedHashCode = hash;
             cachedHashCode = hash;
         }
         }
         return cachedHashCode;
         return cachedHashCode;
@@ -1324,6 +1359,11 @@ public class RenderState implements Cloneable, Savable {
             state.frontStencilFunction = frontStencilFunction;
             state.frontStencilFunction = frontStencilFunction;
             state.backStencilFunction = backStencilFunction;
             state.backStencilFunction = backStencilFunction;
         }
         }
+        if (additionalState.applyLineWidth) {
+            state.lineWidth = additionalState.lineWidth;
+        } else {
+            state.lineWidth = lineWidth;
+        }
         state.cachedHashCode = -1;
         state.cachedHashCode = -1;
         return state;
         return state;
     }
     }
@@ -1351,6 +1391,7 @@ public class RenderState implements Cloneable, Savable {
         backStencilFunction = state.backStencilFunction;
         backStencilFunction = state.backStencilFunction;
         depthFunc = state.depthFunc;
         depthFunc = state.depthFunc;
         alphaFunc = state.alphaFunc;
         alphaFunc = state.alphaFunc;
+        lineWidth = state.lineWidth;
 
 
         applyPointSprite = true;
         applyPointSprite = true;
         applyWireFrame =  true;
         applyWireFrame =  true;
@@ -1364,6 +1405,7 @@ public class RenderState implements Cloneable, Savable {
         applyPolyOffset =  true;
         applyPolyOffset =  true;
         applyDepthFunc =  true;
         applyDepthFunc =  true;
         applyAlphaFunc =  false;
         applyAlphaFunc =  false;
+        applyLineWidth = true;
     }
     }
 
 
     @Override
     @Override
@@ -1392,7 +1434,8 @@ public class RenderState implements Cloneable, Savable {
                 + "\noffsetEnabled=" + offsetEnabled
                 + "\noffsetEnabled=" + offsetEnabled
                 + "\napplyPolyOffset=" + applyPolyOffset
                 + "\napplyPolyOffset=" + applyPolyOffset
                 + "\noffsetFactor=" + offsetFactor
                 + "\noffsetFactor=" + offsetFactor
-                + "\noffsetUnits=" + offsetUnits      
+                + "\noffsetUnits=" + offsetUnits
+                + "\nlineWidth=" + lineWidth
                 + "\n]";
                 + "\n]";
     }
     }
 }
 }

+ 102 - 145
jme3-core/src/main/java/com/jme3/material/Technique.java

@@ -31,27 +31,30 @@
  */
  */
 package com.jme3.material;
 package com.jme3.material;
 
 
+import com.jme3.material.logic.TechniqueDefLogic;
 import com.jme3.asset.AssetManager;
 import com.jme3.asset.AssetManager;
+import com.jme3.light.LightList;
+import com.jme3.material.TechniqueDef.LightMode;
 import com.jme3.renderer.Caps;
 import com.jme3.renderer.Caps;
 import com.jme3.renderer.RenderManager;
 import com.jme3.renderer.RenderManager;
-import com.jme3.shader.*;
+import com.jme3.scene.Geometry;
+import com.jme3.shader.DefineList;
+import com.jme3.shader.Shader;
+import com.jme3.shader.VarType;
+import com.jme3.util.ListMap;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.EnumSet;
 import java.util.EnumSet;
 import java.util.List;
 import java.util.List;
-import java.util.logging.Logger;
 
 
 /**
 /**
  * Represents a technique instance.
  * Represents a technique instance.
  */
  */
-public class Technique /* implements Savable */ {
+public final class Technique {
 
 
-    private static final Logger logger = Logger.getLogger(Technique.class.getName());
-    private TechniqueDef def;
-    private Material owner;
-    private ArrayList<Uniform> worldBindUniforms;
-    private DefineList defines;
-    private Shader shader;
-    private boolean needReload = true;
+    private final TechniqueDef def;
+    private final Material owner;
+    private final DefineList paramDefines;
+    private final DefineList dynamicDefines;
 
 
     /**
     /**
      * Creates a new technique instance that implements the given
      * Creates a new technique instance that implements the given
@@ -63,14 +66,8 @@ public class Technique /* implements Savable */ {
     public Technique(Material owner, TechniqueDef def) {
     public Technique(Material owner, TechniqueDef def) {
         this.owner = owner;
         this.owner = owner;
         this.def = def;
         this.def = def;
-        this.worldBindUniforms = new ArrayList<Uniform>();
-        this.defines = new DefineList();
-    }
-
-    /**
-     * Serialization only. Do not use.
-     */
-    public Technique() {
+        this.paramDefines = def.createDefineList();
+        this.dynamicDefines = def.createDefineList();
     }
     }
 
 
     /**
     /**
@@ -85,157 +82,117 @@ public class Technique /* implements Savable */ {
     }
     }
 
 
     /**
     /**
-     * Returns the shader currently used by this technique instance.
-     * <p>
-     * Shaders are typically loaded dynamically when the technique is first
-     * used, therefore, this variable will most likely be null most of the time.
-     * 
-     * @return the shader currently used by this technique instance.
+     * Called by the material to tell the technique a parameter was modified.
+     * Specify <code>null</code> for value if the param is to be cleared.
      */
      */
-    public Shader getShader() {
-        return shader;
-    }
+    final void notifyParamChanged(String paramName, VarType type, Object value) {
+        Integer defineId = def.getShaderParamDefineId(paramName);
+
+        if (defineId == null) {
+            return;
+        }
 
 
+        paramDefines.set(defineId, type, value);
+    }
+    
     /**
     /**
-     * Returns a list of uniforms that implements the world parameters
-     * that were requested by the material definition.
-     * 
-     * @return a list of uniforms implementing the world parameters.
+     * Called by the material to tell the technique that it has been made
+     * current.
+     * The technique updates dynamic defines based on the
+     * currently set material parameters.
      */
      */
-    public List<Uniform> getWorldBindUniforms() {
-        return worldBindUniforms;
+    final void notifyTechniqueSwitched() {
+        ListMap<String, MatParam> paramMap = owner.getParamsMap();
+        paramDefines.clear();
+        for (int i = 0; i < paramMap.size(); i++) {
+            MatParam param = paramMap.getValue(i);
+            notifyParamChanged(param.getName(), param.getVarType(), param.getValue());
+        }
     }
     }
 
 
     /**
     /**
-     * Called by the material to tell the technique a parameter was modified.
-     * Specify <code>null</code> for value if the param is to be cleared.
+     * Called by the material to determine which shader to use for rendering.
+     * 
+     * The {@link TechniqueDefLogic} is used to determine the shader to use
+     * based on the {@link LightMode}.
+     * 
+     * @param renderManager The render manager for which the shader is to be selected.
+     * @param rendererCaps The renderer capabilities which the shader should support.
+     * @return A compatible shader.
      */
      */
-    void notifyParamChanged(String paramName, VarType type, Object value) {
-        // Check if there's a define binding associated with this
-        // parameter.
-        String defineName = def.getShaderParamDefine(paramName);
-        if (defineName != null) {
-            // There is a define. Change it on the define list.
-            // The "needReload" variable will determine
-            // if the shader will be reloaded when the material
-            // is rendered.
-            
-            if (value == null) {
-                // Clear the define.
-                needReload = defines.remove(defineName) || needReload;
-            } else {
-                // Set the define.
-                needReload = defines.set(defineName, type, value) || needReload;
+    Shader makeCurrent(RenderManager renderManager, List<MatParamOverride> overrides,
+            LightList lights, EnumSet<Caps> rendererCaps) {
+        TechniqueDefLogic logic = def.getLogic();
+        AssetManager assetManager = owner.getMaterialDef().getAssetManager();
+
+        dynamicDefines.clear();
+        dynamicDefines.setAll(paramDefines);
+
+        if (overrides != null) {
+            for (MatParamOverride override : overrides) {
+                if (!override.isEnabled()) {
+                    continue;
+                }
+                Integer defineId = def.getShaderParamDefineId(override.name);
+                if (defineId != null) {
+                    if (def.getDefineIdType(defineId) == override.type) {
+                        dynamicDefines.set(defineId, override.type, override.value);
+                    }
+                }
             }
             }
         }
         }
-    }
 
 
-    void updateUniformParam(String paramName, VarType type, Object value) {
-        if (paramName == null) {
-            throw new IllegalArgumentException();
-        }
-        
-        Uniform u = shader.getUniform(paramName);
-        switch (type) {
-            case TextureBuffer:
-            case Texture2D: // fall intentional
-            case Texture3D:
-            case TextureArray:
-            case TextureCubeMap:
-            case Int:
-                u.setValue(VarType.Int, value);
-                break;
-            default:
-                u.setValue(type, value);
-                break;
-        }
+        return logic.makeCurrent(assetManager, renderManager, rendererCaps, lights, dynamicDefines);
     }
     }
-
+    
     /**
     /**
-     * Returns true if the technique must be reloaded.
-     * <p>
-     * If a technique needs to reload, then the {@link Material} should
-     * call {@link #makeCurrent(com.jme3.asset.AssetManager) } on this
-     * technique.
+     * Render the technique according to its {@link TechniqueDefLogic}.
      * 
      * 
-     * @return true if the technique must be reloaded.
+     * @param renderManager The render manager to perform the rendering against.
+     * @param shader The shader that was selected in 
+     * {@link #makeCurrent(com.jme3.renderer.RenderManager, java.util.EnumSet)}.
+     * @param geometry The geometry to render
+     * @param lights Lights which influence the geometry.
      */
      */
-    public boolean isNeedReload() {
-        return needReload;
+    void render(RenderManager renderManager, Shader shader, Geometry geometry, LightList lights) {
+        TechniqueDefLogic logic = def.getLogic();
+        logic.render(renderManager, shader, geometry, lights);
     }
     }
-
+    
     /**
     /**
-     * Prepares the technique for use by loading the shader and setting
-     * the proper defines based on material parameters.
+     * Get the {@link DefineList} for dynamic defines.
      * 
      * 
-     * @param assetManager The asset manager to use for loading shaders.
+     * Dynamic defines are used to implement material parameter -> define
+     * bindings as well as {@link TechniqueDefLogic} specific functionality.
+     * 
+     * @return all dynamic defines.
      */
      */
-    public void makeCurrent(AssetManager assetManager, boolean techniqueSwitched, EnumSet<Caps> rendererCaps, RenderManager rm) {
-        if (techniqueSwitched) {
-            if (defines.update(owner.getParamsMap(), def)) {
-                needReload = true;
-            }
-            if (getDef().getLightMode() == TechniqueDef.LightMode.SinglePass) {
-                defines.set("SINGLE_PASS_LIGHTING", VarType.Boolean, true);
-                defines.set("NB_LIGHTS", VarType.Int, rm.getSinglePassLightBatchSize() * 3);
-            } else {
-                defines.set("SINGLE_PASS_LIGHTING", VarType.Boolean, null);
-            }
-        }
-
-        if (needReload) {
-            loadShader(assetManager,rendererCaps);
-        }
-    }
-
-    private void loadShader(AssetManager manager,EnumSet<Caps> rendererCaps) {
-        
-        ShaderKey key = new ShaderKey(getAllDefines(),def.getShaderProgramLanguages(),def.getShaderProgramNames());
-        
-        if (getDef().isUsingShaderNodes()) {                 
-           manager.getShaderGenerator(rendererCaps).initialize(this);           
-           key.setUsesShaderNodes(true);
-        }   
-        shader = manager.loadShader(key);
-
-        // register the world bound uniforms
-        worldBindUniforms.clear();
-        if (def.getWorldBindings() != null) {
-           for (UniformBinding binding : def.getWorldBindings()) {
-               Uniform uniform = shader.getUniform("g_" + binding.name());
-               uniform.setBinding(binding);
-               worldBindUniforms.add(uniform);
-           }
-        }        
-        needReload = false;
+    public DefineList getDynamicDefines() {
+        return dynamicDefines;
     }
     }
     
     
     /**
     /**
-     * Computes the define list
-     * @return the complete define list
+     * @return nothing.
+     *
+     * @deprecated Preset defines are precompiled into
+       * {@link TechniqueDef#getShaderPrologue()}, whereas
+     * dynamic defines are available via {@link #getParamDefines()}.
      */
      */
+    @Deprecated
     public DefineList getAllDefines() {
     public DefineList getAllDefines() {
-        DefineList allDefines = new DefineList();
-        allDefines.addFrom(def.getShaderPresetDefines());
-        allDefines.addFrom(defines);
-        return allDefines;
-    } 
-    
-    /*
-    public void write(JmeExporter ex) throws IOException {
-        OutputCapsule oc = ex.getCapsule(this);
-        oc.write(def, "def", null);
-        oc.writeSavableArrayList(worldBindUniforms, "worldBindUniforms", null);
-        oc.write(defines, "defines", null);
-        oc.write(shader, "shader", null);
+        throw new UnsupportedOperationException();
     }
     }
-
-    public void read(JmeImporter im) throws IOException {
-        InputCapsule ic = im.getCapsule(this);
-        def = (TechniqueDef) ic.readSavable("def", null);
-        worldBindUniforms = ic.readSavableArrayList("worldBindUniforms", null);
-        defines = (DefineList) ic.readSavable("defines", null);
-        shader = (Shader) ic.readSavable("shader", null);
+    
+    /**
+     * Compute the sort ID. Similar to {@link Object#hashCode()} but used
+     * for sorting geometries for rendering.
+     * 
+     * @return the sort ID for this technique instance.
+     */
+    public int getSortId() {
+        int hash = 17;
+        hash = hash * 23 + def.getSortId();
+        hash = hash * 23 + paramDefines.hashCode();
+        return hash;
     }
     }
-    */
 }
 }

+ 215 - 72
jme3-core/src/main/java/com/jme3/material/TechniqueDef.java

@@ -31,9 +31,12 @@
  */
  */
 package com.jme3.material;
 package com.jme3.material;
 
 
+import com.jme3.material.logic.TechniqueDefLogic;
+import com.jme3.asset.AssetManager;
 import com.jme3.export.*;
 import com.jme3.export.*;
 import com.jme3.renderer.Caps;
 import com.jme3.renderer.Caps;
 import com.jme3.shader.*;
 import com.jme3.shader.*;
+import com.jme3.shader.Shader.ShaderType;
 
 
 import java.io.IOException;
 import java.io.IOException;
 import java.util.*;
 import java.util.*;
@@ -93,11 +96,17 @@ public class TechniqueDef implements Savable {
 
 
     private EnumSet<Caps> requiredCaps = EnumSet.noneOf(Caps.class);
     private EnumSet<Caps> requiredCaps = EnumSet.noneOf(Caps.class);
     private String name;
     private String name;
-
+    private int sortId;
+    
     private EnumMap<Shader.ShaderType,String> shaderLanguages;
     private EnumMap<Shader.ShaderType,String> shaderLanguages;
     private EnumMap<Shader.ShaderType,String> shaderNames;
     private EnumMap<Shader.ShaderType,String> shaderNames;
 
 
-    private DefineList presetDefines;
+    private String shaderPrologue;
+    private ArrayList<String> defineNames;
+    private ArrayList<VarType> defineTypes;
+    private HashMap<String, Integer> paramToDefineId;
+    private final HashMap<DefineList, Shader> definesToShaderMap;
+    
     private boolean usesNodes = false;
     private boolean usesNodes = false;
     private List<ShaderNode> shaderNodes;
     private List<ShaderNode> shaderNodes;
     private ShaderGenerationInfo shaderGenerationInfo;
     private ShaderGenerationInfo shaderGenerationInfo;
@@ -106,10 +115,10 @@ public class TechniqueDef implements Savable {
     private RenderState renderState;
     private RenderState renderState;
     private RenderState forcedRenderState;
     private RenderState forcedRenderState;
 
 
-    private LightMode lightMode   = LightMode.Disable;
+    private LightMode lightMode = LightMode.Disable;
     private ShadowMode shadowMode = ShadowMode.Disable;
     private ShadowMode shadowMode = ShadowMode.Disable;
+    private TechniqueDefLogic logic;
 
 
-    private HashMap<String, String> defineParams;
     private ArrayList<UniformBinding> worldBinds;
     private ArrayList<UniformBinding> worldBinds;
 
 
     /**
     /**
@@ -120,17 +129,30 @@ public class TechniqueDef implements Savable {
      * @param name The name of the technique, should be set to <code>null</code>
      * @param name The name of the technique, should be set to <code>null</code>
      * for default techniques.
      * for default techniques.
      */
      */
-    public TechniqueDef(String name){
+    public TechniqueDef(String name, int sortId){
         this();
         this();
+        this.sortId = sortId;
         this.name = name == null ? "Default" : name;
         this.name = name == null ? "Default" : name;
     }
     }
 
 
     /**
     /**
      * Serialization only. Do not use.
      * Serialization only. Do not use.
      */
      */
-    public TechniqueDef(){
-        shaderLanguages=new EnumMap<Shader.ShaderType, String>(Shader.ShaderType.class);
-        shaderNames=new EnumMap<Shader.ShaderType, String>(Shader.ShaderType.class);
+    public TechniqueDef() {
+        shaderLanguages = new EnumMap<Shader.ShaderType, String>(Shader.ShaderType.class);
+        shaderNames = new EnumMap<Shader.ShaderType, String>(Shader.ShaderType.class);
+        defineNames = new ArrayList<String>();
+        defineTypes = new ArrayList<VarType>();
+        paramToDefineId = new HashMap<String, Integer>();
+        definesToShaderMap = new HashMap<DefineList, Shader>();
+    }
+    
+    /**
+     * @return A unique sort ID. 
+     * No other technique definition can have the same ID.
+     */
+    public int getSortId() {
+        return sortId;
     }
     }
 
 
     /**
     /**
@@ -162,7 +184,15 @@ public class TechniqueDef implements Savable {
     public void setLightMode(LightMode lightMode) {
     public void setLightMode(LightMode lightMode) {
         this.lightMode = lightMode;
         this.lightMode = lightMode;
     }
     }
+    
+    public void setLogic(TechniqueDefLogic logic) {
+        this.logic = logic;
+    }
 
 
+    public TechniqueDefLogic getLogic() {
+        return logic;
+    }
+    
     /**
     /**
      * Returns the shadow mode.
      * Returns the shadow mode.
      * @return the shadow mode.
      * @return the shadow mode.
@@ -224,14 +254,6 @@ public class TechniqueDef implements Savable {
         return noRender;
         return noRender;
     }
     }
 
 
-    /**
-     * @deprecated jME3 always requires shaders now
-     */
-    @Deprecated
-    public boolean isUsingShaders(){
-        return true;
-    }
-
     /**
     /**
      * Returns true if this technique uses Shader Nodes, false otherwise.
      * Returns true if this technique uses Shader Nodes, false otherwise.
      *
      *
@@ -273,34 +295,24 @@ public class TechniqueDef implements Savable {
         requiredCaps.add(fragCap);
         requiredCaps.add(fragCap);
     }
     }
 
 
-
     /**
     /**
-     * Sets the shaders that this technique definition will use.
-     *
-     * @param shaderNames EnumMap containing all shader names for this stage
-     * @param shaderLanguages EnumMap containing all shader languages for this stage
+     * Set a string which is prepended to every shader used by this technique.
+     * 
+     * Typically this is used for preset defines.
+     * 
+     * @param shaderPrologue The prologue to append before the technique's shaders.
      */
      */
-    public void setShaderFile(EnumMap<Shader.ShaderType, String> shaderNames, EnumMap<Shader.ShaderType, String> shaderLanguages) {
-        requiredCaps.clear();
-
-        for (Shader.ShaderType shaderType : shaderNames.keySet()) {
-            String language = shaderLanguages.get(shaderType);
-            String shaderFile = shaderNames.get(shaderType);
-
-            this.shaderLanguages.put(shaderType, language);
-            this.shaderNames.put(shaderType, shaderFile);
-
-            Caps vertCap = Caps.valueOf(language);
-            requiredCaps.add(vertCap);
-
-            if (shaderType.equals(Shader.ShaderType.Geometry)) {
-                requiredCaps.add(Caps.GeometryShader);
-            } else if (shaderType.equals(Shader.ShaderType.TessellationControl)) {
-                requiredCaps.add(Caps.TesselationShader);
-            }
-        }
+    public void setShaderPrologue(String shaderPrologue) {
+        this.shaderPrologue = shaderPrologue;
     }
     }
-
+    
+    /**
+     * @return the shader prologue which is prepended to every shader.
+     */
+    public String getShaderPrologue() {
+        return shaderPrologue;
+    }
+    
     /**
     /**
      * Returns the define name which the given material parameter influences.
      * Returns the define name which the given material parameter influences.
      *
      *
@@ -310,60 +322,186 @@ public class TechniqueDef implements Savable {
      * @see #addShaderParamDefine(java.lang.String, java.lang.String)
      * @see #addShaderParamDefine(java.lang.String, java.lang.String)
      */
      */
     public String getShaderParamDefine(String paramName){
     public String getShaderParamDefine(String paramName){
-        if (defineParams == null) {
+        Integer defineId = paramToDefineId.get(paramName);
+        if (defineId != null) {
+            return defineNames.get(defineId);
+        } else {
             return null;
             return null;
         }
         }
-        return defineParams.get(paramName);
+    }
+    
+    /**
+     * Get the define ID for a given material parameter.
+     *
+     * @param paramName The parameter name to look up
+     * @return The define ID, or null if not found.
+     */
+    public Integer getShaderParamDefineId(String paramName) {
+        return paramToDefineId.get(paramName);
     }
     }
 
 
+    /**
+     * Get the type of a particular define.
+     *
+     * @param defineId The define ID to lookup.
+     * @return The type of the define, or null if not found.
+     */
+    public VarType getDefineIdType(int defineId) {
+        return defineId < defineTypes.size() ? defineTypes.get(defineId) : null;
+    }
+    
     /**
     /**
      * Adds a define linked to a material parameter.
      * Adds a define linked to a material parameter.
      * <p>
      * <p>
      * Any time the material parameter on the parent material is altered,
      * Any time the material parameter on the parent material is altered,
      * the appropriate define on the technique will be modified as well.
      * the appropriate define on the technique will be modified as well.
-     * See the method
-     * {@link DefineList#set(java.lang.String, com.jme3.shader.VarType, java.lang.Object) }
-     * on the exact details of how the material parameter changes the define.
+     * When set, the material parameter will be mapped to an integer define, 
+     * typically <code>1</code> if it is set, unless it is an integer or a float,
+     * in which case it will converted into an integer.
      *
      *
      * @param paramName The name of the material parameter to link to.
      * @param paramName The name of the material parameter to link to.
+     * @param paramType The type of the material parameter to link to.
      * @param defineName The name of the define parameter, e.g. USE_LIGHTING
      * @param defineName The name of the define parameter, e.g. USE_LIGHTING
      */
      */
-    public void addShaderParamDefine(String paramName, String defineName){
-        if (defineParams == null) {
-            defineParams = new HashMap<String, String>();
+    public void addShaderParamDefine(String paramName, VarType paramType, String defineName){
+        int defineId = defineNames.size();
+        
+        if (defineId >= DefineList.MAX_DEFINES) {
+            throw new IllegalStateException("Cannot have more than " + 
+                    DefineList.MAX_DEFINES + " defines on a technique.");
         }
         }
-        defineParams.put(paramName, defineName);
+        
+        paramToDefineId.put(paramName, defineId);
+        defineNames.add(defineName);
+        defineTypes.add(paramType);
     }
     }
 
 
     /**
     /**
-     * Returns the {@link DefineList} for the preset defines.
+     * Add an unmapped define which can only be set by define ID.
+     * 
+     * Unmapped defines are used by technique renderers to 
+     * configure the shader internally before rendering.
+     * 
+     * @param defineName The define name to create
+     * @return The define ID of the created define
+     */
+    public int addShaderUnmappedDefine(String defineName, VarType defineType) {
+        int defineId = defineNames.size();
+        
+        if (defineId >= DefineList.MAX_DEFINES) {
+            throw new IllegalStateException("Cannot have more than " + 
+                    DefineList.MAX_DEFINES + " defines on a technique.");
+        }
+        
+        defineNames.add(defineName);
+        defineTypes.add(defineType);
+        return defineId;
+    }
+
+    /**
+     * Get the names of all defines declared on this technique definition.
      *
      *
-     * @return the {@link DefineList} for the preset defines.
+     * The defines are returned in order of declaration.
      *
      *
-     * @see #addShaderPresetDefine(java.lang.String, com.jme3.shader.VarType, java.lang.Object)
+     * @return the names of all defines declared.
      */
      */
-    public DefineList getShaderPresetDefines() {
-        return presetDefines;
+    public String[] getDefineNames() {
+        return defineNames.toArray(new String[0]);
     }
     }
 
 
     /**
     /**
-     * Adds a preset define.
-     * <p>
-     * Preset defines do not depend upon any parameters to be activated,
-     * they are always passed to the shader as long as this technique is used.
+     * Get the types of all defines declared on this technique definition.
      *
      *
-     * @param defineName The name of the define parameter, e.g. USE_LIGHTING
-     * @param type The type of the define. See
-     * {@link DefineList#set(java.lang.String, com.jme3.shader.VarType, java.lang.Object) }
-     * to see why it matters.
+     * The types are returned in order of declaration.
      *
      *
-     * @param value The value of the define
+     * @return the types of all defines declared.
      */
      */
-    public void addShaderPresetDefine(String defineName, VarType type, Object value){
-        if (presetDefines == null) {
-            presetDefines = new DefineList();
+    public VarType[] getDefineTypes() {
+        return defineTypes.toArray(new VarType[0]);
+    }
+    
+    /**
+     * Create a define list with the size matching the number
+     * of defines on this technique.
+     * 
+     * @return a define list with the size matching the number
+     * of defines on this technique.
+     */
+    public DefineList createDefineList() {
+        return new DefineList(defineNames.size());
+    }
+    
+    private Shader loadShader(AssetManager assetManager, EnumSet<Caps> rendererCaps, DefineList defines) {
+        StringBuilder sb = new StringBuilder();
+        sb.append(shaderPrologue);
+        defines.generateSource(sb, defineNames, defineTypes);
+        String definesSourceCode = sb.toString();
+
+        Shader shader;
+        if (isUsingShaderNodes()) {
+            ShaderGenerator shaderGenerator = assetManager.getShaderGenerator(rendererCaps);
+            if (shaderGenerator == null) {
+                throw new UnsupportedOperationException("ShaderGenerator was not initialized, "
+                        + "make sure assetManager.getGenerator(caps) has been called");
+            }
+            shaderGenerator.initialize(this);
+            shader = shaderGenerator.generateShader(definesSourceCode);
+        } else {
+            shader = new Shader();
+            for (ShaderType type : ShaderType.values()) {
+                String language = shaderLanguages.get(type);
+                String shaderSourceAssetName = shaderNames.get(type);
+                if (language == null || shaderSourceAssetName == null) {
+                    continue;
+                }
+                String shaderSourceCode = (String) assetManager.loadAsset(shaderSourceAssetName);
+                shader.addSource(type, shaderSourceAssetName, shaderSourceCode, definesSourceCode, language);
+            }
+        }
+
+        if (getWorldBindings() != null) {
+           for (UniformBinding binding : getWorldBindings()) {
+               shader.addUniformBinding(binding);
+           }
+        }
+        
+        return shader;
+    }
+    
+    public Shader getShader(AssetManager assetManager, EnumSet<Caps> rendererCaps, DefineList defines) {
+          Shader shader = definesToShaderMap.get(defines);
+          if (shader == null) {
+              shader = loadShader(assetManager, rendererCaps, defines);
+              definesToShaderMap.put(defines.deepClone(), shader);
+          }
+          return shader;
+     }
+    
+    /**
+     * Sets the shaders that this technique definition will use.
+     *
+     * @param shaderNames EnumMap containing all shader names for this stage
+     * @param shaderLanguages EnumMap containing all shader languages for this stage
+     */
+    public void setShaderFile(EnumMap<Shader.ShaderType, String> shaderNames, EnumMap<Shader.ShaderType, String> shaderLanguages) {
+        requiredCaps.clear();
+
+        for (Shader.ShaderType shaderType : shaderNames.keySet()) {
+            String language = shaderLanguages.get(shaderType);
+            String shaderFile = shaderNames.get(shaderType);
+
+            this.shaderLanguages.put(shaderType, language);
+            this.shaderNames.put(shaderType, shaderFile);
+
+            Caps vertCap = Caps.valueOf(language);
+            requiredCaps.add(vertCap);
+
+            if (shaderType.equals(Shader.ShaderType.Geometry)) {
+                requiredCaps.add(Caps.GeometryShader);
+            } else if (shaderType.equals(Shader.ShaderType.TessellationControl)) {
+                requiredCaps.add(Caps.TesselationShader);
+            }
         }
         }
-        presetDefines.set(defineName, type, value);
     }
     }
 
 
     /**
     /**
@@ -467,7 +605,7 @@ public class TechniqueDef implements Savable {
         oc.write(shaderLanguages.get(Shader.ShaderType.TessellationControl), "tsctrlLanguage", null);
         oc.write(shaderLanguages.get(Shader.ShaderType.TessellationControl), "tsctrlLanguage", null);
         oc.write(shaderLanguages.get(Shader.ShaderType.TessellationEvaluation), "tsevalLanguage", null);
         oc.write(shaderLanguages.get(Shader.ShaderType.TessellationEvaluation), "tsevalLanguage", null);
 
 
-        oc.write(presetDefines, "presetDefines", null);
+        oc.write(shaderPrologue, "shaderPrologue", null);
         oc.write(lightMode, "lightMode", LightMode.Disable);
         oc.write(lightMode, "lightMode", LightMode.Disable);
         oc.write(shadowMode, "shadowMode", ShadowMode.Disable);
         oc.write(shadowMode, "shadowMode", ShadowMode.Disable);
         oc.write(renderState, "renderState", null);
         oc.write(renderState, "renderState", null);
@@ -490,7 +628,7 @@ public class TechniqueDef implements Savable {
         shaderNames.put(Shader.ShaderType.Geometry,ic.readString("geomName", null));
         shaderNames.put(Shader.ShaderType.Geometry,ic.readString("geomName", null));
         shaderNames.put(Shader.ShaderType.TessellationControl,ic.readString("tsctrlName", null));
         shaderNames.put(Shader.ShaderType.TessellationControl,ic.readString("tsctrlName", null));
         shaderNames.put(Shader.ShaderType.TessellationEvaluation,ic.readString("tsevalName", null));
         shaderNames.put(Shader.ShaderType.TessellationEvaluation,ic.readString("tsevalName", null));
-        presetDefines = (DefineList) ic.readSavable("presetDefines", null);
+        shaderPrologue = ic.readString("shaderPrologue", null);
         lightMode = ic.readEnum("lightMode", LightMode.class, LightMode.Disable);
         lightMode = ic.readEnum("lightMode", LightMode.class, LightMode.Disable);
         shadowMode = ic.readEnum("shadowMode", ShadowMode.class, ShadowMode.Disable);
         shadowMode = ic.readEnum("shadowMode", ShadowMode.class, ShadowMode.Disable);
         renderState = (RenderState) ic.readSavable("renderState", null);
         renderState = (RenderState) ic.readSavable("renderState", null);
@@ -547,9 +685,14 @@ public class TechniqueDef implements Savable {
         this.shaderGenerationInfo = shaderGenerationInfo;
         this.shaderGenerationInfo = shaderGenerationInfo;
     }
     }
 
 
-    //todo: make toString return something usefull
     @Override
     @Override
     public String toString() {
     public String toString() {
-        return "TechniqueDef{" + "requiredCaps=" + requiredCaps + ", name=" + name /*+ ", vertName=" + vertName + ", fragName=" + fragName + ", vertLanguage=" + vertLanguage + ", fragLanguage=" + fragLanguage */+ ", presetDefines=" + presetDefines + ", usesNodes=" + usesNodes + ", shaderNodes=" + shaderNodes + ", shaderGenerationInfo=" + shaderGenerationInfo + ", renderState=" + renderState + ", forcedRenderState=" + forcedRenderState + ", lightMode=" + lightMode + ", shadowMode=" + shadowMode + ", defineParams=" + defineParams + ", worldBinds=" + worldBinds + ", noRender=" + noRender + '}';
+        return "TechniqueDef[name=" + name
+                + ", requiredCaps=" + requiredCaps
+                + ", noRender=" + noRender
+                + ", lightMode=" + lightMode
+                + ", usesNodes=" + usesNodes
+                + ", renderState=" + renderState
+                + ", forcedRenderState=" + forcedRenderState + "]";
     }
     }
 }
 }

+ 97 - 0
jme3-core/src/main/java/com/jme3/material/logic/DefaultTechniqueDefLogic.java

@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.material.logic;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.light.AmbientLight;
+import com.jme3.light.Light;
+import com.jme3.light.LightList;
+import com.jme3.material.TechniqueDef;
+import com.jme3.math.ColorRGBA;
+import com.jme3.renderer.Caps;
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.Renderer;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.instancing.InstancedGeometry;
+import com.jme3.shader.DefineList;
+import com.jme3.shader.Shader;
+import java.util.EnumSet;
+
+public class DefaultTechniqueDefLogic implements TechniqueDefLogic {
+
+    protected final TechniqueDef techniqueDef;
+
+    public DefaultTechniqueDefLogic(TechniqueDef techniqueDef) {
+        this.techniqueDef = techniqueDef;
+    }
+
+    @Override
+    public Shader makeCurrent(AssetManager assetManager, RenderManager renderManager,
+            EnumSet<Caps> rendererCaps, LightList lights, DefineList defines) {
+        return techniqueDef.getShader(assetManager, rendererCaps, defines);
+    }
+
+    public static void renderMeshFromGeometry(Renderer renderer, Geometry geom) {
+        Mesh mesh = geom.getMesh();
+        int lodLevel = geom.getLodLevel();
+        if (geom instanceof InstancedGeometry) {
+            InstancedGeometry instGeom = (InstancedGeometry) geom;
+            renderer.renderMesh(mesh, lodLevel, instGeom.getActualNumInstances(),
+                    instGeom.getAllInstanceData());
+        } else {
+            renderer.renderMesh(mesh, lodLevel, 1, null);
+        }
+    }
+
+    protected static ColorRGBA getAmbientColor(LightList lightList, boolean removeLights, ColorRGBA ambientLightColor) {
+        ambientLightColor.set(0, 0, 0, 1);
+        for (int j = 0; j < lightList.size(); j++) {
+            Light l = lightList.get(j);
+            if (l instanceof AmbientLight) {
+                ambientLightColor.addLocal(l.getColor());
+                if (removeLights) {
+                    lightList.remove(l);
+                }
+            }
+        }
+        ambientLightColor.a = 1.0f;
+        return ambientLightColor;
+    }
+
+    @Override
+    public void render(RenderManager renderManager, Shader shader, Geometry geometry, LightList lights) {
+        Renderer renderer = renderManager.getRenderer();
+        renderer.setShader(shader);
+        renderMeshFromGeometry(renderer, geometry);
+    }
+}

+ 178 - 0
jme3-core/src/main/java/com/jme3/material/logic/MultiPassLightingLogic.java

@@ -0,0 +1,178 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.material.logic;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.light.AmbientLight;
+import com.jme3.light.DirectionalLight;
+import com.jme3.light.Light;
+import com.jme3.light.LightList;
+import com.jme3.light.PointLight;
+import com.jme3.light.SpotLight;
+import com.jme3.material.RenderState;
+import com.jme3.material.TechniqueDef;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.FastMath;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+import com.jme3.math.Vector4f;
+import com.jme3.renderer.Caps;
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.Renderer;
+import com.jme3.scene.Geometry;
+import com.jme3.shader.DefineList;
+import com.jme3.shader.Shader;
+import com.jme3.shader.Uniform;
+import com.jme3.shader.VarType;
+import com.jme3.util.TempVars;
+import java.util.EnumSet;
+
+public final class MultiPassLightingLogic extends DefaultTechniqueDefLogic {
+
+    private static final RenderState ADDITIVE_LIGHT = new RenderState();
+    private static final Quaternion NULL_DIR_LIGHT = new Quaternion(0, -1, 0, -1);
+    
+    private final ColorRGBA ambientLightColor = new ColorRGBA(0, 0, 0, 1);
+    
+    static {
+        ADDITIVE_LIGHT.setBlendMode(RenderState.BlendMode.AlphaAdditive);
+        ADDITIVE_LIGHT.setDepthWrite(false);
+    }
+    
+    public MultiPassLightingLogic(TechniqueDef techniqueDef) {
+        super(techniqueDef);
+    }
+
+    @Override
+    public void render(RenderManager renderManager, Shader shader, Geometry geometry, LightList lights) {
+        Renderer r = renderManager.getRenderer();
+        Uniform lightDir = shader.getUniform("g_LightDirection");
+        Uniform lightColor = shader.getUniform("g_LightColor");
+        Uniform lightPos = shader.getUniform("g_LightPosition");
+        Uniform ambientColor = shader.getUniform("g_AmbientLightColor");
+        boolean isFirstLight = true;
+        boolean isSecondLight = false;
+        
+        getAmbientColor(lights, false, ambientLightColor);
+
+        for (int i = 0; i < lights.size(); i++) {
+            Light l = lights.get(i);
+            if (l instanceof AmbientLight) {
+                continue;
+            }
+
+            if (isFirstLight) {
+                // set ambient color for first light only
+                ambientColor.setValue(VarType.Vector4, ambientLightColor);
+                isFirstLight = false;
+                isSecondLight = true;
+            } else if (isSecondLight) {
+                ambientColor.setValue(VarType.Vector4, ColorRGBA.Black);
+                // apply additive blending for 2nd and future lights
+                r.applyRenderState(ADDITIVE_LIGHT);
+                isSecondLight = false;
+            }
+
+            TempVars vars = TempVars.get();
+            Quaternion tmpLightDirection = vars.quat1;
+            Quaternion tmpLightPosition = vars.quat2;
+            ColorRGBA tmpLightColor = vars.color;
+            Vector4f tmpVec = vars.vect4f1;
+
+            ColorRGBA color = l.getColor();
+            tmpLightColor.set(color);
+            tmpLightColor.a = l.getType().getId();
+            lightColor.setValue(VarType.Vector4, tmpLightColor);
+
+            switch (l.getType()) {
+                case Directional:
+                    DirectionalLight dl = (DirectionalLight) l;
+                    Vector3f dir = dl.getDirection();
+                    //FIXME : there is an inconstency here due to backward
+                    //compatibility of the lighting shader.
+                    //The directional light direction is passed in the
+                    //LightPosition uniform. The lighting shader needs to be
+                    //reworked though in order to fix this.
+                    tmpLightPosition.set(dir.getX(), dir.getY(), dir.getZ(), -1);
+                    lightPos.setValue(VarType.Vector4, tmpLightPosition);
+                    tmpLightDirection.set(0, 0, 0, 0);
+                    lightDir.setValue(VarType.Vector4, tmpLightDirection);
+                    break;
+                case Point:
+                    PointLight pl = (PointLight) l;
+                    Vector3f pos = pl.getPosition();
+                    float invRadius = pl.getInvRadius();
+
+                    tmpLightPosition.set(pos.getX(), pos.getY(), pos.getZ(), invRadius);
+                    lightPos.setValue(VarType.Vector4, tmpLightPosition);
+                    tmpLightDirection.set(0, 0, 0, 0);
+                    lightDir.setValue(VarType.Vector4, tmpLightDirection);
+                    break;
+                case Spot:
+                    SpotLight sl = (SpotLight) l;
+                    Vector3f pos2 = sl.getPosition();
+                    Vector3f dir2 = sl.getDirection();
+                    float invRange = sl.getInvSpotRange();
+                    float spotAngleCos = sl.getPackedAngleCos();
+
+                    tmpLightPosition.set(pos2.getX(), pos2.getY(), pos2.getZ(), invRange);
+                    lightPos.setValue(VarType.Vector4, tmpLightPosition);
+
+                    //We transform the spot direction in view space here to save 5 varying later in the lighting shader
+                    //one vec4 less and a vec4 that becomes a vec3
+                    //the downside is that spotAngleCos decoding happens now in the frag shader.
+                    tmpVec.set(dir2.getX(), dir2.getY(), dir2.getZ(), 0);
+                    renderManager.getCurrentCamera().getViewMatrix().mult(tmpVec, tmpVec);
+                    tmpLightDirection.set(tmpVec.getX(), tmpVec.getY(), tmpVec.getZ(), spotAngleCos);
+
+                    lightDir.setValue(VarType.Vector4, tmpLightDirection);
+
+                    break;
+                default:
+                    throw new UnsupportedOperationException("Unknown type of light: " + l.getType());
+            }
+            vars.release();
+            r.setShader(shader);
+            renderMeshFromGeometry(r, geometry);
+        }
+
+        if (isFirstLight) {
+            // Either there are no lights at all, or only ambient lights.
+            // Render a dummy "normal light" so we can see the ambient color.
+            ambientColor.setValue(VarType.Vector4, getAmbientColor(lights, false, ambientLightColor));
+            lightColor.setValue(VarType.Vector4, ColorRGBA.BlackNoAlpha);
+            lightPos.setValue(VarType.Vector4, NULL_DIR_LIGHT);
+            r.setShader(shader);
+            renderMeshFromGeometry(r, geometry);
+        }
+    }
+}

+ 218 - 0
jme3-core/src/main/java/com/jme3/material/logic/SinglePassLightingLogic.java

@@ -0,0 +1,218 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.material.logic;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.light.DirectionalLight;
+import com.jme3.light.Light;
+import com.jme3.light.LightList;
+import com.jme3.light.PointLight;
+import com.jme3.light.SpotLight;
+import com.jme3.material.RenderState;
+import com.jme3.material.RenderState.BlendMode;
+import com.jme3.material.TechniqueDef;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Vector3f;
+import com.jme3.math.Vector4f;
+import com.jme3.renderer.Caps;
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.Renderer;
+import com.jme3.scene.Geometry;
+import com.jme3.shader.DefineList;
+import com.jme3.shader.Shader;
+import com.jme3.shader.Uniform;
+import com.jme3.shader.VarType;
+import com.jme3.util.TempVars;
+import java.util.EnumSet;
+
+public final class SinglePassLightingLogic extends DefaultTechniqueDefLogic {
+
+    private static final String DEFINE_SINGLE_PASS_LIGHTING = "SINGLE_PASS_LIGHTING";
+    private static final String DEFINE_NB_LIGHTS = "NB_LIGHTS";
+    private static final RenderState ADDITIVE_LIGHT = new RenderState();
+
+    private final ColorRGBA ambientLightColor = new ColorRGBA(0, 0, 0, 1);
+
+    static {
+        ADDITIVE_LIGHT.setBlendMode(BlendMode.AlphaAdditive);
+        ADDITIVE_LIGHT.setDepthWrite(false);
+    }
+
+    private final int singlePassLightingDefineId;
+    private final int nbLightsDefineId;
+
+    public SinglePassLightingLogic(TechniqueDef techniqueDef) {
+        super(techniqueDef);
+        singlePassLightingDefineId = techniqueDef.addShaderUnmappedDefine(DEFINE_SINGLE_PASS_LIGHTING, VarType.Boolean);
+        nbLightsDefineId = techniqueDef.addShaderUnmappedDefine(DEFINE_NB_LIGHTS, VarType.Int);
+    }
+
+    @Override
+    public Shader makeCurrent(AssetManager assetManager, RenderManager renderManager,
+            EnumSet<Caps> rendererCaps, LightList lights, DefineList defines) {
+        defines.set(nbLightsDefineId, renderManager.getSinglePassLightBatchSize() * 3);
+        defines.set(singlePassLightingDefineId, true);
+        return super.makeCurrent(assetManager, renderManager, rendererCaps, lights, defines);
+    }
+
+    /**
+     * Uploads the lights in the light list as two uniform arrays.<br/><br/> *
+     * <p>
+     * <code>uniform vec4 g_LightColor[numLights];</code><br/> //
+     * g_LightColor.rgb is the diffuse/specular color of the light.<br/> //
+     * g_Lightcolor.a is the type of light, 0 = Directional, 1 = Point, <br/> //
+     * 2 = Spot. <br/> <br/>
+     * <code>uniform vec4 g_LightPosition[numLights];</code><br/> //
+     * g_LightPosition.xyz is the position of the light (for point lights)<br/>
+     * // or the direction of the light (for directional lights).<br/> //
+     * g_LightPosition.w is the inverse radius (1/r) of the light (for
+     * attenuation) <br/> </p>
+     */
+    protected int updateLightListUniforms(Shader shader, Geometry g, LightList lightList, int numLights, RenderManager rm, int startIndex) {
+        if (numLights == 0) { // this shader does not do lighting, ignore.
+            return 0;
+        }
+
+        Uniform lightData = shader.getUniform("g_LightData");
+        lightData.setVector4Length(numLights * 3);//8 lights * max 3
+        Uniform ambientColor = shader.getUniform("g_AmbientLightColor");
+
+
+        if (startIndex != 0) {
+            // apply additive blending for 2nd and future passes
+            rm.getRenderer().applyRenderState(ADDITIVE_LIGHT);
+            ambientColor.setValue(VarType.Vector4, ColorRGBA.Black);
+        } else {
+            ambientColor.setValue(VarType.Vector4, getAmbientColor(lightList, true, ambientLightColor));
+        }
+
+        int lightDataIndex = 0;
+        TempVars vars = TempVars.get();
+        Vector4f tmpVec = vars.vect4f1;
+        int curIndex;
+        int endIndex = numLights + startIndex;
+        for (curIndex = startIndex; curIndex < endIndex && curIndex < lightList.size(); curIndex++) {
+
+            Light l = lightList.get(curIndex);
+            if (l.getType() == Light.Type.Ambient) {
+                endIndex++;
+                continue;
+            }
+            ColorRGBA color = l.getColor();
+            //Color
+            lightData.setVector4InArray(color.getRed(),
+                    color.getGreen(),
+                    color.getBlue(),
+                    l.getType().getId(),
+                    lightDataIndex);
+            lightDataIndex++;
+
+            switch (l.getType()) {
+                case Directional:
+                    DirectionalLight dl = (DirectionalLight) l;
+                    Vector3f dir = dl.getDirection();
+                    //Data directly sent in view space to avoid a matrix mult for each pixel
+                    tmpVec.set(dir.getX(), dir.getY(), dir.getZ(), 0.0f);
+                    rm.getCurrentCamera().getViewMatrix().mult(tmpVec, tmpVec);
+//                        tmpVec.divideLocal(tmpVec.w);
+//                        tmpVec.normalizeLocal();
+                    lightData.setVector4InArray(tmpVec.getX(), tmpVec.getY(), tmpVec.getZ(), -1, lightDataIndex);
+                    lightDataIndex++;
+                    //PADDING
+                    lightData.setVector4InArray(0, 0, 0, 0, lightDataIndex);
+                    lightDataIndex++;
+                    break;
+                case Point:
+                    PointLight pl = (PointLight) l;
+                    Vector3f pos = pl.getPosition();
+                    float invRadius = pl.getInvRadius();
+                    tmpVec.set(pos.getX(), pos.getY(), pos.getZ(), 1.0f);
+                    rm.getCurrentCamera().getViewMatrix().mult(tmpVec, tmpVec);
+                    //tmpVec.divideLocal(tmpVec.w);
+                    lightData.setVector4InArray(tmpVec.getX(), tmpVec.getY(), tmpVec.getZ(), invRadius, lightDataIndex);
+                    lightDataIndex++;
+                    //PADDING
+                    lightData.setVector4InArray(0, 0, 0, 0, lightDataIndex);
+                    lightDataIndex++;
+                    break;
+                case Spot:
+                    SpotLight sl = (SpotLight) l;
+                    Vector3f pos2 = sl.getPosition();
+                    Vector3f dir2 = sl.getDirection();
+                    float invRange = sl.getInvSpotRange();
+                    float spotAngleCos = sl.getPackedAngleCos();
+                    tmpVec.set(pos2.getX(), pos2.getY(), pos2.getZ(), 1.0f);
+                    rm.getCurrentCamera().getViewMatrix().mult(tmpVec, tmpVec);
+                    // tmpVec.divideLocal(tmpVec.w);
+                    lightData.setVector4InArray(tmpVec.getX(), tmpVec.getY(), tmpVec.getZ(), invRange, lightDataIndex);
+                    lightDataIndex++;
+
+                    //We transform the spot direction in view space here to save 5 varying later in the lighting shader
+                    //one vec4 less and a vec4 that becomes a vec3
+                    //the downside is that spotAngleCos decoding happens now in the frag shader.
+                    tmpVec.set(dir2.getX(), dir2.getY(), dir2.getZ(), 0.0f);
+                    rm.getCurrentCamera().getViewMatrix().mult(tmpVec, tmpVec);
+                    tmpVec.normalizeLocal();
+                    lightData.setVector4InArray(tmpVec.getX(), tmpVec.getY(), tmpVec.getZ(), spotAngleCos, lightDataIndex);
+                    lightDataIndex++;
+                    break;
+                default:
+                    throw new UnsupportedOperationException("Unknown type of light: " + l.getType());
+            }
+        }
+        vars.release();
+        //Padding of unsued buffer space
+        while(lightDataIndex < numLights * 3) {
+            lightData.setVector4InArray(0f, 0f, 0f, 0f, lightDataIndex);
+            lightDataIndex++;
+        }
+        return curIndex;
+    }
+
+    @Override
+    public void render(RenderManager renderManager, Shader shader, Geometry geometry, LightList lights) {
+        int nbRenderedLights = 0;
+        Renderer renderer = renderManager.getRenderer();
+        int batchSize = renderManager.getSinglePassLightBatchSize();
+        if (lights.size() == 0) {
+            updateLightListUniforms(shader, geometry, lights, batchSize, renderManager, 0);
+            renderer.setShader(shader);
+            renderMeshFromGeometry(renderer, geometry);
+        } else {
+            while (nbRenderedLights < lights.size()) {
+                nbRenderedLights = updateLightListUniforms(shader, geometry, lights, batchSize, renderManager, nbRenderedLights);
+                renderer.setShader(shader);
+                renderMeshFromGeometry(renderer, geometry);
+            }
+        }
+    }
+}

+ 157 - 0
jme3-core/src/main/java/com/jme3/material/logic/StaticPassLightingLogic.java

@@ -0,0 +1,157 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.material.logic;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.light.DirectionalLight;
+import com.jme3.light.Light;
+import com.jme3.light.Light.Type;
+import com.jme3.light.LightList;
+import com.jme3.light.PointLight;
+import com.jme3.light.SpotLight;
+import com.jme3.material.TechniqueDef;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.Caps;
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.Renderer;
+import com.jme3.scene.Geometry;
+import com.jme3.shader.DefineList;
+import com.jme3.shader.Shader;
+import com.jme3.shader.Uniform;
+import com.jme3.shader.VarType;
+import java.util.ArrayList;
+import java.util.EnumSet;
+
+/**
+ * Rendering logic for static pass.
+ *
+ * @author Kirill Vainer
+ */
+public final class StaticPassLightingLogic extends DefaultTechniqueDefLogic {
+
+    private static final String DEFINE_NUM_DIR_LIGHTS = "NUM_DIR_LIGHTS";
+    private static final String DEFINE_NUM_POINT_LIGHTS = "NUM_POINT_LIGHTS";
+    private static final String DEFINE_NUM_SPOT_LIGHTS = "NUM_SPOT_LIGHTS";
+
+    private final int numDirLightsDefineId;
+    private final int numPointLightsDefineId;
+    private final int numSpotLightsDefineId;
+
+    private final ArrayList<DirectionalLight> tempDirLights = new ArrayList<DirectionalLight>();
+    private final ArrayList<PointLight> tempPointLights = new ArrayList<PointLight>();
+    private final ArrayList<SpotLight> tempSpotLights = new ArrayList<SpotLight>();
+
+    private final ColorRGBA ambientLightColor = new ColorRGBA(0, 0, 0, 1);
+
+    public StaticPassLightingLogic(TechniqueDef techniqueDef) {
+        super(techniqueDef);
+
+        numDirLightsDefineId = techniqueDef.addShaderUnmappedDefine(DEFINE_NUM_DIR_LIGHTS, VarType.Int);
+        numPointLightsDefineId = techniqueDef.addShaderUnmappedDefine(DEFINE_NUM_POINT_LIGHTS, VarType.Int);
+        numSpotLightsDefineId = techniqueDef.addShaderUnmappedDefine(DEFINE_NUM_SPOT_LIGHTS, VarType.Int);
+    }
+
+    @Override
+    public Shader makeCurrent(AssetManager assetManager, RenderManager renderManager,
+            EnumSet<Caps> rendererCaps, LightList lights, DefineList defines) {
+
+        // TODO: if it ever changes that render isn't called
+        // right away with the same geometry after makeCurrent, it would be
+        // a problem.
+        // Do a radix sort.
+        tempDirLights.clear();
+        tempPointLights.clear();
+        tempSpotLights.clear();
+        for (Light light : lights) {
+            switch (light.getType()) {
+                case Directional:
+                    tempDirLights.add((DirectionalLight) light);
+                    break;
+                case Point:
+                    tempPointLights.add((PointLight) light);
+                    break;
+                case Spot:
+                    tempSpotLights.add((SpotLight) light);
+                    break;
+            }
+        }
+
+        defines.set(numDirLightsDefineId, tempDirLights.size());
+        defines.set(numPointLightsDefineId, tempPointLights.size());
+        defines.set(numSpotLightsDefineId, tempSpotLights.size());
+
+        return techniqueDef.getShader(assetManager, rendererCaps, defines);
+    }
+
+    private void updateLightListUniforms(Shader shader, LightList lights) {
+        Uniform ambientColor = shader.getUniform("g_AmbientLightColor");
+        ambientColor.setValue(VarType.Vector4, getAmbientColor(lights, true, ambientLightColor));
+
+        Uniform lightData = shader.getUniform("g_LightData");
+
+        int index = 0;
+        for (DirectionalLight light : tempDirLights) {
+            ColorRGBA color = light.getColor();
+            Vector3f dir = light.getDirection();
+            lightData.setVector4InArray(color.r, color.g, color.b, 1f, index++);
+            lightData.setVector4InArray(dir.x, dir.y, dir.z, 1f, index++);
+        }
+
+        for (PointLight light : tempPointLights) {
+            ColorRGBA color = light.getColor();
+            Vector3f pos = light.getPosition();
+            lightData.setVector4InArray(color.r, color.g, color.b, 1f, index++);
+            lightData.setVector4InArray(pos.x, pos.y, pos.z, 1f, index++);
+        }
+
+        for (SpotLight light : tempSpotLights) {
+            ColorRGBA color = light.getColor();
+            Vector3f pos = light.getPosition();
+            Vector3f dir = light.getDirection();
+            float invRange = light.getInvSpotRange();
+            float spotAngleCos = light.getPackedAngleCos();
+            lightData.setVector4InArray(color.r, color.g, color.b, 1f, index++);
+            lightData.setVector4InArray(pos.x, pos.y, pos.z, invRange, index++);
+            lightData.setVector4InArray(dir.x, dir.y, dir.z, spotAngleCos, index++);
+        }
+    }
+
+    @Override
+    public void render(RenderManager renderManager, Shader shader, Geometry geometry, LightList lights) {
+        Renderer renderer = renderManager.getRenderer();
+        updateLightListUniforms(shader, lights);
+        renderer.setShader(shader);
+        renderMeshFromGeometry(renderer, geometry);
+    }
+
+}

+ 97 - 0
jme3-core/src/main/java/com/jme3/material/logic/TechniqueDefLogic.java

@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.material.logic;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.light.LightList;
+import com.jme3.material.TechniqueDef.LightMode;
+import com.jme3.renderer.Caps;
+import com.jme3.renderer.RenderManager;
+import com.jme3.scene.Geometry;
+import com.jme3.shader.DefineList;
+import com.jme3.shader.Shader;
+import com.jme3.shader.Uniform;
+import com.jme3.shader.UniformBinding;
+import com.jme3.texture.Texture;
+import java.util.EnumSet;
+
+/**
+ * <code>TechniqueDefLogic</code> is used to customize how 
+ * a material should be rendered.
+ * 
+ * Typically used to implement {@link LightMode lighting modes}.
+ * Implementations can register 
+ * {@link TechniqueDef#addShaderUnmappedDefine(java.lang.String) unmapped defines} 
+ * in their constructor and then later set them based on the geometry 
+ * or light environment being rendered.
+ * 
+ * @author Kirill Vainer
+ */
+public interface TechniqueDefLogic {
+    
+    /**
+     * Determine the shader to use for the given geometry / material combination.
+     * 
+     * @param assetManager The asset manager to use for loading shader source code,
+     * shader nodes, and and lookup textures.
+     * @param renderManager The render manager for which rendering is to be performed.
+     * @param rendererCaps Renderer capabilities. The returned shader must
+     * support these capabilities.
+     * @param lights The lights with which the geometry shall be rendered. This
+     * list must not include culled lights.
+     * @param defines The define list used by the technique, any 
+     * {@link TechniqueDef#addShaderUnmappedDefine(java.lang.String) unmapped defines}
+     * should be set here to change shader behavior.
+     * 
+     * @return The shader to use for rendering.
+     */
+    public Shader makeCurrent(AssetManager assetManager, RenderManager renderManager, 
+            EnumSet<Caps> rendererCaps, LightList lights, DefineList defines);
+    
+    /**
+     * Requests that the <code>TechniqueDefLogic</code> renders the given geometry.
+     * 
+     * Fixed material functionality such as {@link RenderState}, 
+     * {@link MatParam material parameters}, and 
+     * {@link UniformBinding uniform bindings}
+     * have already been applied by the material, however, 
+     * {@link RenderState}, {@link Uniform uniforms}, {@link Texture textures},
+     * can still be overriden.
+     * 
+     * @param renderManager The render manager to perform the rendering against.
+     * * @param shader The shader that was selected by this logic in 
+     * {@link #makeCurrent(com.jme3.asset.AssetManager, com.jme3.renderer.RenderManager, java.util.EnumSet, com.jme3.shader.DefineList)}.
+     * @param geometry The geometry to render
+     * @param lights Lights which influence the geometry.
+     */
+    public void render(RenderManager renderManager, Shader shader, Geometry geometry, LightList lights);
+}

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

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

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

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

+ 19 - 10
jme3-core/src/main/java/com/jme3/renderer/RenderManager.java

@@ -34,8 +34,12 @@ package com.jme3.renderer;
 import com.jme3.light.DefaultLightFilter;
 import com.jme3.light.DefaultLightFilter;
 import com.jme3.light.LightFilter;
 import com.jme3.light.LightFilter;
 import com.jme3.light.LightList;
 import com.jme3.light.LightList;
-import com.jme3.material.*;
-import com.jme3.math.Matrix4f;
+import com.jme3.material.Material;
+import com.jme3.material.MaterialDef;
+import com.jme3.material.RenderState;
+import com.jme3.material.Technique;
+import com.jme3.material.TechniqueDef;
+import com.jme3.math.*;
 import com.jme3.post.SceneProcessor;
 import com.jme3.post.SceneProcessor;
 import com.jme3.profile.AppProfiler;
 import com.jme3.profile.AppProfiler;
 import com.jme3.profile.AppStep;
 import com.jme3.profile.AppStep;
@@ -45,13 +49,12 @@ import com.jme3.renderer.queue.RenderQueue;
 import com.jme3.renderer.queue.RenderQueue.Bucket;
 import com.jme3.renderer.queue.RenderQueue.Bucket;
 import com.jme3.renderer.queue.RenderQueue.ShadowMode;
 import com.jme3.renderer.queue.RenderQueue.ShadowMode;
 import com.jme3.scene.*;
 import com.jme3.scene.*;
-import com.jme3.shader.Uniform;
+import com.jme3.shader.Shader;
 import com.jme3.shader.UniformBinding;
 import com.jme3.shader.UniformBinding;
 import com.jme3.shader.UniformBindingManager;
 import com.jme3.shader.UniformBindingManager;
 import com.jme3.system.NullRenderer;
 import com.jme3.system.NullRenderer;
 import com.jme3.system.Timer;
 import com.jme3.system.Timer;
 import com.jme3.util.SafeArrayList;
 import com.jme3.util.SafeArrayList;
-
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Collections;
 import java.util.List;
 import java.util.List;
@@ -480,8 +483,8 @@ public class RenderManager {
      * Updates the given list of uniforms with {@link UniformBinding uniform bindings}
      * Updates the given list of uniforms with {@link UniformBinding uniform bindings}
      * based on the current world state.
      * based on the current world state.
      */
      */
-    public void updateUniformBindings(List<Uniform> params) {
-        uniformBindingManager.updateUniformBindings(params);
+    public void updateUniformBindings(Shader shader) {
+        uniformBindingManager.updateUniformBindings(shader);
     }
     }
 
 
     /**
     /**
@@ -530,6 +533,7 @@ public class RenderManager {
             lightFilter.filterLights(g, filteredLightList);
             lightFilter.filterLights(g, filteredLightList);
             lightList = filteredLightList;
             lightList = filteredLightList;
         }
         }
+        
 
 
         //if forcedTechnique we try to force it for render,
         //if forcedTechnique we try to force it for render,
         //if it does not exists in the mat def, we check for forcedMaterial and render the geom if not null
         //if it does not exists in the mat def, we check for forcedMaterial and render the geom if not null
@@ -612,7 +616,9 @@ public class RenderManager {
 
 
             gm.getMaterial().preload(this);
             gm.getMaterial().preload(this);
             Mesh mesh = gm.getMesh();
             Mesh mesh = gm.getMesh();
-            if (mesh != null) {
+            if (mesh != null
+                    && mesh.getVertexCount() != 0
+                    && mesh.getTriangleCount() != 0) {
                 for (VertexBuffer vb : mesh.getBufferList().getArray()) {
                 for (VertexBuffer vb : mesh.getBufferList().getArray()) {
                     if (vb.getData() != null && vb.getUsage() != VertexBuffer.Usage.CpuOnly) {
                     if (vb.getData() != null && vb.getUsage() != VertexBuffer.Usage.CpuOnly) {
                         renderer.updateBufferData(vb);
                         renderer.updateBufferData(vb);
@@ -637,8 +643,10 @@ public class RenderManager {
      * <p>
      * <p>
      * In addition to enqueuing the visible geometries, this method
      * In addition to enqueuing the visible geometries, this method
      * also scenes which cast or receive shadows, by putting them into the
      * also scenes which cast or receive shadows, by putting them into the
-     * RenderQueue's {@link RenderQueue#renderShadowQueue(GeometryList, RenderManager, Camera, boolean) shadow queue}.
-     * Each Spatial which has its {@link Spatial#setShadowMode(com.jme3.renderer.queue.RenderQueue.ShadowMode) shadow mode}
+     * RenderQueue's 
+     * {@link RenderQueue#addToShadowQueue(com.jme3.scene.Geometry, com.jme3.renderer.queue.RenderQueue.ShadowMode) 
+     * shadow queue}. Each Spatial which has its 
+     * {@link Spatial#setShadowMode(com.jme3.renderer.queue.RenderQueue.ShadowMode) shadow mode}
      * set to not off, will be put into the appropriate shadow queue, note that
      * set to not off, will be put into the appropriate shadow queue, note that
      * this process does not check for frustum culling on any 
      * this process does not check for frustum culling on any 
      * {@link ShadowMode#Cast shadow casters}, as they don't have to be
      * {@link ShadowMode#Cast shadow casters}, as they don't have to be
@@ -985,7 +993,8 @@ public class RenderManager {
      * (see {@link #renderTranslucentQueue(com.jme3.renderer.ViewPort) })</li>
      * (see {@link #renderTranslucentQueue(com.jme3.renderer.ViewPort) })</li>
      * <li>If any objects remained in the render queue, they are removed
      * <li>If any objects remained in the render queue, they are removed
      * from the queue. This is generally objects added to the 
      * from the queue. This is generally objects added to the 
-     * {@link RenderQueue#renderShadowQueue(GeometryList, RenderManager, Camera, boolean) shadow queue}
+     * {@link RenderQueue#renderShadowQueue(com.jme3.renderer.queue.RenderQueue.ShadowMode, com.jme3.renderer.RenderManager, com.jme3.renderer.Camera, boolean) 
+     * shadow queue}
      * which were not rendered because of a missing shadow renderer.</li>
      * which were not rendered because of a missing shadow renderer.</li>
      * </ul>
      * </ul>
      * 
      * 

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

@@ -58,6 +58,7 @@ public interface GL3 extends GL2 {
     public void glBindFragDataLocation(int param1, int param2, String param3); /// GL3+
     public void glBindFragDataLocation(int param1, int param2, String param3); /// GL3+
     public void glBindVertexArray(int param1); /// GL3+
     public void glBindVertexArray(int param1); /// GL3+
     public void glDeleteVertexArrays(IntBuffer arrays); /// GL3+
     public void glDeleteVertexArrays(IntBuffer arrays); /// GL3+
+    public void glFramebufferTextureLayer(int param1, int param2, int param3, int param4, int param5); /// GL3+
     public void glGenVertexArrays(IntBuffer param1); /// GL3+
     public void glGenVertexArrays(IntBuffer param1); /// GL3+
     public String glGetString(int param1, int param2); /// GL3+
     public String glGetString(int param1, int param2); /// GL3+
 }
 }

+ 6 - 0
jme3-core/src/main/java/com/jme3/renderer/opengl/GLDebugDesktop.java

@@ -94,4 +94,10 @@ public class GLDebugDesktop extends GLDebugES implements GL2, GL3, GL4 {
         gl4.glPatchParameter(count);
         gl4.glPatchParameter(count);
         checkError();
         checkError();
     }
     }
+
+    @Override
+    public void glFramebufferTextureLayer(int param1, int param2, int param3, int param4, int param5) {
+        gl3.glFramebufferTextureLayer(param1, param2, param3, param4, param5);
+        checkError();
+    }
 }
 }

+ 40 - 14
jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java

@@ -474,6 +474,17 @@ public final class GLRenderer implements Renderer {
             {
             {
                 sb.append("\t").append(cap.toString()).append("\n");
                 sb.append("\t").append(cap.toString()).append("\n");
             }
             }
+            
+            sb.append("\nHardware limits: \n");
+            for (Limits limit : Limits.values()) {
+                Integer value = limits.get(limit);
+                if (value == null) {
+                    value = 0;
+                }
+                sb.append("\t").append(limit.name()).append(" = ")
+                  .append(value).append("\n");
+            }
+            
             logger.log(Level.FINE, sb.toString());
             logger.log(Level.FINE, sb.toString());
         }
         }
 
 
@@ -779,6 +790,10 @@ public final class GLRenderer implements Renderer {
                 gl.glDisable(GL.GL_STENCIL_TEST);
                 gl.glDisable(GL.GL_STENCIL_TEST);
             }
             }
         }
         }
+        if (context.lineWidth != state.getLineWidth()) {
+            gl.glLineWidth(state.getLineWidth());
+            context.lineWidth = state.getLineWidth();
+        }
     }
     }
 
 
     private int convertStencilOperation(StencilOperation stencilOp) {
     private int convertStencilOperation(StencilOperation stencilOp) {
@@ -960,12 +975,12 @@ public final class GLRenderer implements Renderer {
                 gl.glUniform1i(loc, b.booleanValue() ? GL.GL_TRUE : GL.GL_FALSE);
                 gl.glUniform1i(loc, b.booleanValue() ? GL.GL_TRUE : GL.GL_FALSE);
                 break;
                 break;
             case Matrix3:
             case Matrix3:
-                fb = (FloatBuffer) uniform.getValue();
+                fb = uniform.getMultiData();
                 assert fb.remaining() == 9;
                 assert fb.remaining() == 9;
                 gl.glUniformMatrix3(loc, false, fb);
                 gl.glUniformMatrix3(loc, false, fb);
                 break;
                 break;
             case Matrix4:
             case Matrix4:
-                fb = (FloatBuffer) uniform.getValue();
+                fb = uniform.getMultiData();
                 assert fb.remaining() == 16;
                 assert fb.remaining() == 16;
                 gl.glUniformMatrix4(loc, false, fb);
                 gl.glUniformMatrix4(loc, false, fb);
                 break;
                 break;
@@ -974,23 +989,23 @@ public final class GLRenderer implements Renderer {
                 gl.glUniform1(loc, ib);
                 gl.glUniform1(loc, ib);
                 break;
                 break;
             case FloatArray:
             case FloatArray:
-                fb = (FloatBuffer) uniform.getValue();
+                fb = uniform.getMultiData();
                 gl.glUniform1(loc, fb);
                 gl.glUniform1(loc, fb);
                 break;
                 break;
             case Vector2Array:
             case Vector2Array:
-                fb = (FloatBuffer) uniform.getValue();
+                fb = uniform.getMultiData();
                 gl.glUniform2(loc, fb);
                 gl.glUniform2(loc, fb);
                 break;
                 break;
             case Vector3Array:
             case Vector3Array:
-                fb = (FloatBuffer) uniform.getValue();
+                fb = uniform.getMultiData();
                 gl.glUniform3(loc, fb);
                 gl.glUniform3(loc, fb);
                 break;
                 break;
             case Vector4Array:
             case Vector4Array:
-                fb = (FloatBuffer) uniform.getValue();
+                fb = uniform.getMultiData();
                 gl.glUniform4(loc, fb);
                 gl.glUniform4(loc, fb);
                 break;
                 break;
             case Matrix4Array:
             case Matrix4Array:
-                fb = (FloatBuffer) uniform.getValue();
+                fb = uniform.getMultiData();
                 gl.glUniformMatrix4(loc, false, fb);
                 gl.glUniformMatrix4(loc, false, fb);
                 break;
                 break;
             case Int:
             case Int:
@@ -1438,11 +1453,19 @@ public final class GLRenderer implements Renderer {
             setupTextureParams(0, tex);
             setupTextureParams(0, tex);
         }
         }
 
 
-        glfbo.glFramebufferTexture2DEXT(GLFbo.GL_FRAMEBUFFER_EXT,
-                convertAttachmentSlot(rb.getSlot()),
-                convertTextureType(tex.getType(), image.getMultiSamples(), rb.getFace()),
-                image.getId(),
-                0);
+        if (rb.getLayer() < 0){
+            glfbo.glFramebufferTexture2DEXT(GLFbo.GL_FRAMEBUFFER_EXT,
+                    convertAttachmentSlot(rb.getSlot()),
+                    convertTextureType(tex.getType(), image.getMultiSamples(), rb.getFace()),
+                    image.getId(),
+                    0);
+        } else {
+            gl3.glFramebufferTextureLayer(GLFbo.GL_FRAMEBUFFER_EXT, 
+                    convertAttachmentSlot(rb.getSlot()), 
+                    image.getId(), 
+                    0,
+                    rb.getLayer());
+        }
     }
     }
 
 
     public void updateFrameBufferAttachment(FrameBuffer fb, RenderBuffer rb) {
     public void updateFrameBufferAttachment(FrameBuffer fb, RenderBuffer rb) {
@@ -2677,12 +2700,15 @@ public final class GLRenderer implements Renderer {
     }
     }
 
 
     public void renderMesh(Mesh mesh, int lod, int count, VertexBuffer[] instanceData) {
     public void renderMesh(Mesh mesh, int lod, int count, VertexBuffer[] instanceData) {
-        if (mesh.getVertexCount() == 0) {
+        if (mesh.getVertexCount() == 0 || mesh.getTriangleCount() == 0 || count == 0) {
             return;
             return;
         }
         }
 
 
+        if (count > 1 && !caps.contains(Caps.MeshInstancing)) {
+            throw new RendererException("Mesh instancing is not supported by the video hardware");
+        }
 
 
-        if (context.lineWidth != mesh.getLineWidth()) {
+        if (mesh.getLineWidth() != 1f && context.lineWidth != mesh.getLineWidth()) {
             gl.glLineWidth(mesh.getLineWidth());
             gl.glLineWidth(mesh.getLineWidth());
             context.lineWidth = mesh.getLineWidth();
             context.lineWidth = mesh.getLineWidth();
         }
         }

+ 10 - 0
jme3-core/src/main/java/com/jme3/renderer/queue/GeometryList.java

@@ -99,6 +99,16 @@ public class GeometryList implements Iterable<Geometry>{
         return size;
         return size;
     }
     }
 
 
+    /**
+     * Sets the element at the given index.
+     * 
+     * @param index The index to set
+     * @param value The value
+     */
+    public void set(int index, Geometry value) {
+        geometries[index] = value;
+    }
+    
     /**
     /**
      * Returns the element at the given index.
      * Returns the element at the given index.
      *
      *

+ 3 - 2
jme3-core/src/main/java/com/jme3/renderer/queue/OpaqueComparator.java

@@ -69,11 +69,12 @@ public class OpaqueComparator implements GeometryComparator {
         return spat.queueDistance;
         return spat.queueDistance;
     }
     }
 
 
+    @Override
     public int compare(Geometry o1, Geometry o2) {
     public int compare(Geometry o1, Geometry o2) {
         Material m1 = o1.getMaterial();
         Material m1 = o1.getMaterial();
         Material m2 = o2.getMaterial();
         Material m2 = o2.getMaterial();
-
-        int compareResult = m2.getSortId() - m1.getSortId();
+        
+        int compareResult = Integer.compare(m1.getSortId(), m2.getSortId());
         if (compareResult == 0){
         if (compareResult == 0){
             // use the same shader.
             // use the same shader.
             // sort front-to-back then.
             // sort front-to-back then.

+ 17 - 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.JmeImporter;
 import com.jme3.export.OutputCapsule;
 import com.jme3.export.OutputCapsule;
 import com.jme3.export.binary.BinaryImporter;
 import com.jme3.export.binary.BinaryImporter;
+import com.jme3.util.clone.Cloner;
 import com.jme3.util.SafeArrayList;
 import com.jme3.util.SafeArrayList;
 import java.io.IOException;
 import java.io.IOException;
 import java.util.*;
 import java.util.*;
@@ -50,7 +51,7 @@ import java.util.logging.Logger;
  * The AssetLinkNode does not store its children when exported to file.
  * 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
  * Instead, you can add a list of AssetKeys that will be loaded and attached
  * when the AssetLinkNode is restored.
  * when the AssetLinkNode is restored.
- * 
+ *
  * @author normenhansen
  * @author normenhansen
  */
  */
 public class AssetLinkNode extends Node {
 public class AssetLinkNode extends Node {
@@ -70,6 +71,20 @@ public class AssetLinkNode extends Node {
         assetLoaderKeys.add(key);
         assetLoaderKeys.add(key);
     }
     }
 
 
+    /**
+     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
+     */
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) {
+        super.cloneFields(cloner, 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
      * Add a "linked" child. These are loaded from the assetManager when the
      * AssetLinkNode is loaded from a binary file.
      * AssetLinkNode is loaded from a binary file.
@@ -166,7 +181,7 @@ public class AssetLinkNode extends Node {
                 children.add(child);
                 children.add(child);
                 assetChildren.put(modelKey, child);
                 assetChildren.put(modelKey, child);
             } else {
             } 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 });
                                                                     new Object[]{ modelKey, key });
             }
             }
         }
         }

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

+ 15 - 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.renderer.Camera;
 import com.jme3.scene.control.CameraControl;
 import com.jme3.scene.control.CameraControl;
 import com.jme3.scene.control.CameraControl.ControlDirection;
 import com.jme3.scene.control.CameraControl.ControlDirection;
+import com.jme3.util.clone.Cloner;
 import java.io.IOException;
 import java.io.IOException;
 
 
 /**
 /**
@@ -93,7 +94,20 @@ public class CameraNode extends Node {
 //        this.lookAt(position, upVector);
 //        this.lookAt(position, upVector);
 //        camControl.getCamera().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 ) {
+        super.cloneFields(cloner, 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
     @Override
     public void read(JmeImporter im) throws IOException {
     public void read(JmeImporter im) throws IOException {
         super.read(im);
         super.read(im);

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

@@ -43,6 +43,8 @@ import com.jme3.material.Material;
 import com.jme3.math.Matrix4f;
 import com.jme3.math.Matrix4f;
 import com.jme3.renderer.Camera;
 import com.jme3.renderer.Camera;
 import com.jme3.scene.VertexBuffer.Type;
 import com.jme3.scene.VertexBuffer.Type;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.IdentityCloneFunction;
 import com.jme3.util.TempVars;
 import com.jme3.util.TempVars;
 import java.io.IOException;
 import java.io.IOException;
 import java.util.Queue;
 import java.util.Queue;
@@ -54,12 +56,12 @@ import java.util.logging.Logger;
  * contains the geometric data for rendering objects. It manages all rendering
  * contains the geometric data for rendering objects. It manages all rendering
  * information such as a {@link Material} object to define how the surface
  * 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.
  * should be shaded and the {@link Mesh} data to contain the actual geometry.
- * 
+ *
  * @author Kirill Vainer
  * @author Kirill Vainer
  */
  */
 public class Geometry extends Spatial {
 public class Geometry extends Spatial {
 
 
-    // Version #1: removed shared meshes. 
+    // Version #1: removed shared meshes.
     // models loaded with shared mesh will be automatically fixed.
     // models loaded with shared mesh will be automatically fixed.
     public static final int SAVABLE_VERSION = 1;
     public static final int SAVABLE_VERSION = 1;
     private static final Logger logger = Logger.getLogger(Geometry.class.getName());
     private static final Logger logger = Logger.getLogger(Geometry.class.getName());
@@ -71,19 +73,19 @@ public class Geometry extends Spatial {
      */
      */
     protected boolean ignoreTransform = false;
     protected boolean ignoreTransform = false;
     protected transient Matrix4f cachedWorldMat = new Matrix4f();
     protected transient Matrix4f cachedWorldMat = new Matrix4f();
-    
+
     /**
     /**
      * Specifies which {@link GeometryGroupNode} this <code>Geometry</code>
      * Specifies which {@link GeometryGroupNode} this <code>Geometry</code>
      * is managed by.
      * is managed by.
      */
      */
     protected GeometryGroupNode groupNode;
     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}.
      * the {@link GeometryGroupNode}.
      */
      */
     protected int startIndex = -1;
     protected int startIndex = -1;
-        
+
     /**
     /**
      * Serialization only. Do not use.
      * Serialization only. Do not use.
      */
      */
@@ -95,37 +97,37 @@ public class Geometry extends Spatial {
      * Create a geometry node without any mesh data.
      * Create a geometry node without any mesh data.
      * Both the mesh and the material are null, the geometry
      * Both the mesh and the material are null, the geometry
      * cannot be rendered until those are set.
      * cannot be rendered until those are set.
-     * 
+     *
      * @param name The name of this geometry
      * @param name The name of this geometry
      */
      */
     public Geometry(String name) {
     public Geometry(String name) {
         super(name);
         super(name);
-        
+
         // For backwards compatibility, only clear the "requires
         // For backwards compatibility, only clear the "requires
         // update" flag if we are not a subclass of Geometry.
         // update" flag if we are not a subclass of Geometry.
         // This prevents subclass from silently failing to receive
         // This prevents subclass from silently failing to receive
         // updates when they upgrade.
         // updates when they upgrade.
-        setRequiresUpdates(Geometry.class != getClass()); 
+        setRequiresUpdates(Geometry.class != getClass());
     }
     }
 
 
     /**
     /**
      * Create a geometry node with mesh data.
      * Create a geometry node with mesh data.
      * The material of the geometry is null, it cannot
      * The material of the geometry is null, it cannot
      * be rendered until it is set.
      * be rendered until it is set.
-     * 
+     *
      * @param name The name of this geometry
      * @param name The name of this geometry
      * @param mesh The mesh data for this geometry
      * @param mesh The mesh data for this geometry
      */
      */
     public Geometry(String name, Mesh mesh) {
     public Geometry(String name, Mesh mesh) {
         this(name);
         this(name);
-        
+
         if (mesh == null) {
         if (mesh == null) {
             throw new IllegalArgumentException("mesh cannot be null");
             throw new IllegalArgumentException("mesh cannot be null");
         }
         }
 
 
         this.mesh = mesh;
         this.mesh = mesh;
     }
     }
-    
+
     @Override
     @Override
     public boolean checkCulling(Camera cam) {
     public boolean checkCulling(Camera cam) {
         if (isGrouped()) {
         if (isGrouped()) {
@@ -137,8 +139,8 @@ public class Geometry extends Spatial {
 
 
     /**
     /**
      * @return If ignoreTransform mode is set.
      * @return If ignoreTransform mode is set.
-     * 
-     * @see Geometry#setIgnoreTransform(boolean) 
+     *
+     * @see Geometry#setIgnoreTransform(boolean)
      */
      */
     public boolean isIgnoreTransform() {
     public boolean isIgnoreTransform() {
         return ignoreTransform;
         return ignoreTransform;
@@ -156,7 +158,7 @@ public class Geometry extends Spatial {
      * Level 0 indicates that the default index buffer should be used,
      * Level 0 indicates that the default index buffer should be used,
      * levels [1, LodLevels + 1] represent the levels set on the mesh
      * levels [1, LodLevels + 1] represent the levels set on the mesh
      * with {@link Mesh#setLodLevels(com.jme3.scene.VertexBuffer[]) }.
      * with {@link Mesh#setLodLevels(com.jme3.scene.VertexBuffer[]) }.
-     * 
+     *
      * @param lod The lod level to set
      * @param lod The lod level to set
      */
      */
     @Override
     @Override
@@ -170,7 +172,7 @@ public class Geometry extends Spatial {
         }
         }
 
 
         lodLevel = lod;
         lodLevel = lod;
-        
+
         if (isGrouped()) {
         if (isGrouped()) {
             groupNode.onMeshChange(this);
             groupNode.onMeshChange(this);
         }
         }
@@ -178,7 +180,7 @@ public class Geometry extends Spatial {
 
 
     /**
     /**
      * Returns the LOD level set with {@link #setLodLevel(int) }.
      * Returns the LOD level set with {@link #setLodLevel(int) }.
-     * 
+     *
      * @return the LOD level set
      * @return the LOD level set
      */
      */
     public int getLodLevel() {
     public int getLodLevel() {
@@ -187,10 +189,10 @@ public class Geometry extends Spatial {
 
 
     /**
     /**
      * Returns this geometry's mesh vertex count.
      * Returns this geometry's mesh vertex count.
-     * 
+     *
      * @return this geometry's mesh vertex count.
      * @return this geometry's mesh vertex count.
-     * 
-     * @see Mesh#getVertexCount() 
+     *
+     * @see Mesh#getVertexCount()
      */
      */
     public int getVertexCount() {
     public int getVertexCount() {
         return mesh.getVertexCount();
         return mesh.getVertexCount();
@@ -198,10 +200,10 @@ public class Geometry extends Spatial {
 
 
     /**
     /**
      * Returns this geometry's mesh triangle count.
      * Returns this geometry's mesh triangle count.
-     * 
+     *
      * @return this geometry's mesh triangle count.
      * @return this geometry's mesh triangle count.
-     * 
-     * @see Mesh#getTriangleCount() 
+     *
+     * @see Mesh#getTriangleCount()
      */
      */
     public int getTriangleCount() {
     public int getTriangleCount() {
         return mesh.getTriangleCount();
         return mesh.getTriangleCount();
@@ -209,9 +211,9 @@ public class Geometry extends Spatial {
 
 
     /**
     /**
      * Sets the mesh to use for this geometry when rendering.
      * Sets the mesh to use for this geometry when rendering.
-     * 
+     *
      * @param mesh the mesh to use for this geometry
      * @param mesh the mesh to use for this geometry
-     * 
+     *
      * @throws IllegalArgumentException If mesh is null
      * @throws IllegalArgumentException If mesh is null
      */
      */
     public void setMesh(Mesh mesh) {
     public void setMesh(Mesh mesh) {
@@ -221,7 +223,7 @@ public class Geometry extends Spatial {
 
 
         this.mesh = mesh;
         this.mesh = mesh;
         setBoundRefresh();
         setBoundRefresh();
-        
+
         if (isGrouped()) {
         if (isGrouped()) {
             groupNode.onMeshChange(this);
             groupNode.onMeshChange(this);
         }
         }
@@ -229,10 +231,10 @@ public class Geometry extends Spatial {
 
 
     /**
     /**
      * Returns the mesh to use for this geometry
      * Returns the mesh to use for this geometry
-     * 
+     *
      * @return 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() {
     public Mesh getMesh() {
         return mesh;
         return mesh;
@@ -240,13 +242,13 @@ public class Geometry extends Spatial {
 
 
     /**
     /**
      * Sets the material to use for this geometry.
      * Sets the material to use for this geometry.
-     * 
+     *
      * @param material the material to use for this geometry
      * @param material the material to use for this geometry
      */
      */
     @Override
     @Override
     public void setMaterial(Material material) {
     public void setMaterial(Material material) {
         this.material = material;
         this.material = material;
-        
+
         if (isGrouped()) {
         if (isGrouped()) {
             groupNode.onMaterialChange(this);
             groupNode.onMaterialChange(this);
         }
         }
@@ -254,10 +256,10 @@ public class Geometry extends Spatial {
 
 
     /**
     /**
      * Returns the material that is used for this geometry.
      * Returns the material that is used for this geometry.
-     * 
+     *
      * @return 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() {
     public Material getMaterial() {
         return material;
         return material;
@@ -310,18 +312,18 @@ public class Geometry extends Spatial {
         computeWorldMatrix();
         computeWorldMatrix();
 
 
         if (isGrouped()) {
         if (isGrouped()) {
-            groupNode.onTransformChange(this);   
+            groupNode.onTransformChange(this);
         }
         }
-        
+
         // geometry requires lights to be sorted
         // geometry requires lights to be sorted
         worldLights.sort(true);
         worldLights.sort(true);
     }
     }
 
 
     /**
     /**
      * Associate this <code>Geometry</code> with a {@link GeometryGroupNode}.
      * Associate this <code>Geometry</code> with a {@link GeometryGroupNode}.
-     * 
+     *
      * Should only be called by the parent {@link GeometryGroupNode}.
      * Should only be called by the parent {@link GeometryGroupNode}.
-     * 
+     *
      * @param node Which {@link GeometryGroupNode} to associate with.
      * @param node Which {@link GeometryGroupNode} to associate with.
      * @param startIndex The starting index of this geometry in the group.
      * @param startIndex The starting index of this geometry in the group.
      */
      */
@@ -329,26 +331,26 @@ public class Geometry extends Spatial {
         if (isGrouped()) {
         if (isGrouped()) {
             unassociateFromGroupNode();
             unassociateFromGroupNode();
         }
         }
-        
+
         this.groupNode = node;
         this.groupNode = node;
         this.startIndex = startIndex;
         this.startIndex = startIndex;
     }
     }
 
 
     /**
     /**
-     * Removes the {@link GeometryGroupNode} association from this 
+     * Removes the {@link GeometryGroupNode} association from this
      * <code>Geometry</code>.
      * <code>Geometry</code>.
-     * 
+     *
      * Should only be called by the parent {@link GeometryGroupNode}.
      * Should only be called by the parent {@link GeometryGroupNode}.
      */
      */
     public void unassociateFromGroupNode() {
     public void unassociateFromGroupNode() {
         if (groupNode != null) {
         if (groupNode != null) {
-            // Once the geometry is removed 
+            // Once the geometry is removed
             // from the parent, the group node needs to be updated.
             // from the parent, the group node needs to be updated.
             groupNode.onGeometryUnassociated(this);
             groupNode.onGeometryUnassociated(this);
             groupNode = null;
             groupNode = null;
-            
+
             // change the default to -1 to make error detection easier
             // change the default to -1 to make error detection easier
-            startIndex = -1; 
+            startIndex = -1;
         }
         }
     }
     }
 
 
@@ -360,7 +362,7 @@ public class Geometry extends Spatial {
     @Override
     @Override
     protected void setParent(Node parent) {
     protected void setParent(Node parent) {
         super.setParent(parent);
         super.setParent(parent);
-        
+
         // If the geometry is managed by group node we need to unassociate.
         // If the geometry is managed by group node we need to unassociate.
         if (parent == null && isGrouped()) {
         if (parent == null && isGrouped()) {
             unassociateFromGroupNode();
             unassociateFromGroupNode();
@@ -406,7 +408,7 @@ public class Geometry extends Spatial {
      * {@link Geometry#getWorldTransform() world transform} of this geometry.
      * {@link Geometry#getWorldTransform() world transform} of this geometry.
      * In order to receive updated values, you must call {@link Geometry#computeWorldMatrix() }
      * In order to receive updated values, you must call {@link Geometry#computeWorldMatrix() }
      * before using this method.
      * before using this method.
-     * 
+     *
      * @return Matrix to transform from local space to world space
      * @return Matrix to transform from local space to world space
      */
      */
     public Matrix4f getWorldMatrix() {
     public Matrix4f getWorldMatrix() {
@@ -418,7 +420,7 @@ public class Geometry extends Spatial {
      * This alters the bound used on the mesh as well via
      * This alters the bound used on the mesh as well via
      * {@link Mesh#setBound(com.jme3.bounding.BoundingVolume) } and
      * {@link Mesh#setBound(com.jme3.bounding.BoundingVolume) } and
      * forces the world bounding volume to be recomputed.
      * forces the world bounding volume to be recomputed.
-     * 
+     *
      * @param modelBound The model bound to set
      * @param modelBound The model bound to set
      */
      */
     @Override
     @Override
@@ -465,15 +467,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.
      * {@link GeometryGroupNode} or not.
-     * 
+     *
      * @return True if managed by a {@link GeometryGroupNode}.
      * @return True if managed by a {@link GeometryGroupNode}.
      */
      */
     public boolean isGrouped() {
     public boolean isGrouped() {
         return groupNode != null;
         return groupNode != null;
     }
     }
-    
+
     /**
     /**
      * @deprecated Use {@link #isGrouped()} instead.
      * @deprecated Use {@link #isGrouped()} instead.
      */
      */
@@ -491,15 +493,22 @@ public class Geometry extends Spatial {
      */
      */
     @Override
     @Override
     public Geometry clone(boolean cloneMaterial) {
     public Geometry clone(boolean cloneMaterial) {
+        return (Geometry)super.clone(cloneMaterial);
+    }
+
+    /**
+     *  The old clone() method that did not use the new Cloner utility.
+     */
+    public Geometry oldClone(boolean cloneMaterial) {
         Geometry geomClone = (Geometry) super.clone(cloneMaterial);
         Geometry geomClone = (Geometry) super.clone(cloneMaterial);
-        
+
         // This geometry is managed,
         // This geometry is managed,
         // but the cloned one is not attached to anything, hence not managed.
         // but the cloned one is not attached to anything, hence not managed.
         if (geomClone.isGrouped()) {
         if (geomClone.isGrouped()) {
             geomClone.groupNode = null;
             geomClone.groupNode = null;
             geomClone.startIndex = -1;
             geomClone.startIndex = -1;
         }
         }
-        
+
         geomClone.cachedWorldMat = cachedWorldMat.clone();
         geomClone.cachedWorldMat = cachedWorldMat.clone();
         if (material != null) {
         if (material != null) {
             if (cloneMaterial) {
             if (cloneMaterial) {
@@ -534,11 +543,58 @@ public class Geometry extends Spatial {
      */
      */
     @Override
     @Override
     public Spatial deepClone() {
     public Spatial deepClone() {
+        return super.deepClone();
+    }
+
+    public Spatial oldDeepClone() {
         Geometry geomClone = clone(true);
         Geometry geomClone = clone(true);
         geomClone.mesh = mesh.deepClone();
         geomClone.mesh = mesh.deepClone();
         return geomClone;
         return geomClone;
     }
     }
 
 
+    /**
+     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
+     */
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) {
+        super.cloneFields(cloner, original);
+
+        // If this is a grouped node and if our group node is
+        // also cloned then we'll grab it's reference.
+        if( groupNode != null ) {
+            if( cloner.isCloned(groupNode) ) {
+                // Then resolve the reference
+                this.groupNode = cloner.clone(groupNode);
+            } else {
+                // We are on our own now
+                this.groupNode = null;
+                this.startIndex = -1;
+            }
+
+            // The above is based on the fact that if we were
+            // cloning the hierarchy that contained the parent
+            // group then it would have been shallow cloned before
+            // this child.  Can't really be otherwise.
+        }
+
+        this.cachedWorldMat = cloner.clone(cachedWorldMat);
+
+        // See if we are doing a shallow clone or a deep mesh clone
+        boolean shallowClone = (cloner.getCloneFunction(Mesh.class) instanceof IdentityCloneFunction);
+
+        // See if we clone the mesh using the special animation
+        // semi-deep cloning
+        if( shallowClone && mesh != null && mesh.getBuffer(Type.BindPosePosition) != null ) {
+            // Then we need to clone the mesh a little deeper
+            this.mesh = mesh.cloneForAnim();
+        } else {
+            // Do whatever the cloner wants to do about it
+            this.mesh = cloner.clone(mesh);
+        }
+
+        this.material = cloner.clone(material);
+    }
+
     @Override
     @Override
     public void write(JmeExporter ex) throws IOException {
     public void write(JmeExporter ex) throws IOException {
         super.write(ex);
         super.write(ex);

+ 17 - 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.light.Light;
 import com.jme3.scene.control.LightControl;
 import com.jme3.scene.control.LightControl;
 import com.jme3.scene.control.LightControl.ControlDirection;
 import com.jme3.scene.control.LightControl.ControlDirection;
+import com.jme3.util.clone.Cloner;
 import java.io.IOException;
 import java.io.IOException;
 
 
 /**
 /**
  * <code>LightNode</code> is used to link together a {@link Light} object
  * <code>LightNode</code> is used to link together a {@link Light} object
- * with a {@link Node} object. 
+ * with a {@link Node} object.
  *
  *
  * @author Tim8Dev
  * @author Tim8Dev
  */
  */
@@ -66,7 +67,7 @@ public class LightNode extends Node {
 
 
     /**
     /**
      * Enable or disable the <code>LightNode</code> functionality.
      * Enable or disable the <code>LightNode</code> functionality.
-     * 
+     *
      * @param enabled If false, the functionality of LightNode will
      * @param enabled If false, the functionality of LightNode will
      * be disabled.
      * be disabled.
      */
      */
@@ -93,7 +94,20 @@ public class LightNode extends Node {
     public Light getLight() {
     public Light getLight() {
         return lightControl.getLight();
         return lightControl.getLight();
     }
     }
-    
+
+    /**
+     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
+     */
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) {
+        super.cloneFields(cloner, 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
     @Override
     public void read(JmeImporter im) throws IOException {
     public void read(JmeImporter im) throws IOException {
         super.read(im);
         super.read(im);

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


+ 84 - 66
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.material.Material;
 import com.jme3.util.SafeArrayList;
 import com.jme3.util.SafeArrayList;
 import com.jme3.util.TempVars;
 import com.jme3.util.TempVars;
+import com.jme3.util.clone.Cloner;
 import java.io.IOException;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.List;
@@ -53,7 +54,7 @@ import java.util.logging.Logger;
  * node maintains a collection of children and handles merging said children
  * 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
  * into a single bound to allow for very fast culling of multiple nodes. Node
  * allows for any number of children to be attached.
  * allows for any number of children to be attached.
- * 
+ *
  * @author Mark Powell
  * @author Mark Powell
  * @author Gregg Patton
  * @author Gregg Patton
  * @author Joshua Slack
  * @author Joshua Slack
@@ -62,26 +63,25 @@ public class Node extends Spatial {
 
 
     private static final Logger logger = Logger.getLogger(Node.class.getName());
     private static final Logger logger = Logger.getLogger(Node.class.getName());
 
 
-    /** 
+    /**
      * This node's children.
      * This node's children.
      */
      */
     protected SafeArrayList<Spatial> children = new SafeArrayList<Spatial>(Spatial.class);
     protected SafeArrayList<Spatial> children = new SafeArrayList<Spatial>(Spatial.class);
 
 
     /**
     /**
      * If this node is a root, this list will contain the current
      * 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
      * updateLogicalState() to be called as indicated by their
      * requiresUpdate() method.
      * requiresUpdate() method.
      */
      */
     private SafeArrayList<Spatial> updateList = null;
     private SafeArrayList<Spatial> updateList = null;
-    
     /**
     /**
      * False if the update list requires rebuilding.  This is Node.class
      * False if the update list requires rebuilding.  This is Node.class
      * specific and therefore not included as part of the Spatial update flags.
      * 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 flag is used instead of nulling the updateList to avoid reallocating
      * a whole list every time the scene graph changes.
      * a whole list every time the scene graph changes.
-     */     
-    private boolean updateListValid = false;    
+     */
+    private boolean updateListValid = false;
 
 
     /**
     /**
      * Serialization only. Do not use.
      * Serialization only. Do not use.
@@ -93,29 +93,28 @@ public class Node extends Spatial {
     /**
     /**
      * Constructor instantiates a new <code>Node</code> with a default empty
      * Constructor instantiates a new <code>Node</code> with a default empty
      * list for containing children.
      * list for containing children.
-     * 
+     *
      * @param name the name of the scene element. This is required for
      * @param name the name of the scene element. This is required for
      * identification and comparison purposes.
      * identification and comparison purposes.
      */
      */
     public Node(String name) {
     public Node(String name) {
         super(name);
         super(name);
-        
         // For backwards compatibility, only clear the "requires
         // For backwards compatibility, only clear the "requires
         // update" flag if we are not a subclass of Node.
         // update" flag if we are not a subclass of Node.
         // This prevents subclass from silently failing to receive
         // This prevents subclass from silently failing to receive
         // updates when they upgrade.
         // updates when they upgrade.
-        setRequiresUpdates(Node.class != getClass()); 
+        setRequiresUpdates(Node.class != getClass());
     }
     }
 
 
     /**
     /**
-     * 
+     *
      * <code>getQuantity</code> returns the number of children this node
      * <code>getQuantity</code> returns the number of children this node
      * maintains.
      * maintains.
-     * 
+     *
      * @return the number of children this node maintains.
      * @return the number of children this node maintains.
      */
      */
     public int getQuantity() {
     public int getQuantity() {
-        return children.size();        
+        return children.size();
     }
     }
 
 
     @Override
     @Override
@@ -140,10 +139,21 @@ public class Node extends Spatial {
         }
         }
     }
     }
 
 
+    @Override
+    protected void setMatParamOverrideRefresh() {
+        super.setMatParamOverrideRefresh();
+        for (Spatial child : children.getArray()) {
+            if ((child.refreshFlags & RF_MATPARAM_OVERRIDE) != 0) {
+                continue;
+            }
+
+            child.setMatParamOverrideRefresh();
+        }
+    }
+
     @Override
     @Override
     protected void updateWorldBound(){
     protected void updateWorldBound(){
         super.updateWorldBound();
         super.updateWorldBound();
-        
         // for a node, the world bound is a combination of all it's children
         // for a node, the world bound is a combination of all it's children
         // bounds
         // bounds
         BoundingVolume resultBound = null;
         BoundingVolume resultBound = null;
@@ -167,7 +177,7 @@ public class Node extends Spatial {
     protected void setParent(Node parent) {
     protected void setParent(Node parent) {
         if( this.parent == null && parent != null ) {
         if( this.parent == null && parent != null ) {
             // We were a root before and now we aren't... make sure if
             // 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.
             // avoid holding the dead array.
             updateList = null;
             updateList = null;
             updateListValid = false;
             updateListValid = false;
@@ -204,15 +214,15 @@ public class Node extends Spatial {
             return updateList;
             return updateList;
         }
         }
         if( updateList == null ) {
         if( updateList == null ) {
-            updateList = new SafeArrayList<Spatial>(Spatial.class);            
+            updateList = new SafeArrayList<Spatial>(Spatial.class);
         } else {
         } else {
             updateList.clear();
             updateList.clear();
         }
         }
 
 
         // Build the list
         // Build the list
         addUpdateChildren(updateList);
         addUpdateChildren(updateList);
-        updateListValid = true;       
-        return updateList;   
+        updateListValid = true;
+        return updateList;
     }
     }
 
 
     @Override
     @Override
@@ -238,19 +248,19 @@ public class Node extends Spatial {
             // This branch has no geometric state that requires updates.
             // This branch has no geometric state that requires updates.
             return;
             return;
         }
         }
-        
         if ((refreshFlags & RF_LIGHTLIST) != 0){
         if ((refreshFlags & RF_LIGHTLIST) != 0){
             updateWorldLightList();
             updateWorldLightList();
         }
         }
-
         if ((refreshFlags & RF_TRANSFORM) != 0){
         if ((refreshFlags & RF_TRANSFORM) != 0){
             // combine with parent transforms- same for all spatial
             // combine with parent transforms- same for all spatial
             // subclasses.
             // subclasses.
             updateWorldTransforms();
             updateWorldTransforms();
         }
         }
+        if ((refreshFlags & RF_MATPARAM_OVERRIDE) != 0) {
+            updateMatParamOverrides();
+        }
 
 
         refreshFlags &= ~RF_CHILD_LIGHTLIST;
         refreshFlags &= ~RF_CHILD_LIGHTLIST;
-        
         if (!children.isEmpty()) {
         if (!children.isEmpty()) {
             // the important part- make sure child geometric state is refreshed
             // the important part- make sure child geometric state is refreshed
             // first before updating own world bound. This saves
             // first before updating own world bound. This saves
@@ -260,7 +270,7 @@ public class Node extends Spatial {
             for (Spatial child : children.getArray()) {
             for (Spatial child : children.getArray()) {
                 child.updateGeometricState();
                 child.updateGeometricState();
             }
             }
-        }            
+        }
 
 
         if ((refreshFlags & RF_BOUND) != 0){
         if ((refreshFlags & RF_BOUND) != 0){
             updateWorldBound();
             updateWorldBound();
@@ -272,7 +282,7 @@ public class Node extends Spatial {
     /**
     /**
      * <code>getTriangleCount</code> returns the number of triangles contained
      * <code>getTriangleCount</code> returns the number of triangles contained
      * in all sub-branches of this node that contain geometry.
      * in all sub-branches of this node that contain geometry.
-     * 
+     *
      * @return the triangle count of this branch.
      * @return the triangle count of this branch.
      */
      */
     @Override
     @Override
@@ -286,11 +296,10 @@ public class Node extends Spatial {
 
 
         return count;
         return count;
     }
     }
-    
     /**
     /**
      * <code>getVertexCount</code> returns the number of vertices contained
      * <code>getVertexCount</code> returns the number of vertices contained
      * in all sub-branches of this node that contain geometry.
      * in all sub-branches of this node that contain geometry.
-     * 
+     *
      * @return the vertex count of this branch.
      * @return the vertex count of this branch.
      */
      */
     @Override
     @Override
@@ -311,7 +320,7 @@ public class Node extends Spatial {
      * returned.
      * returned.
      * <br>
      * <br>
      * If the child already had a parent it is detached from that former parent.
      * If the child already had a parent it is detached from that former parent.
-     * 
+     *
      * @param child
      * @param child
      *            the child to attach to this node.
      *            the child to attach to this node.
      * @return the number of children maintained by this node.
      * @return the number of children maintained by this node.
@@ -320,15 +329,14 @@ public class Node extends Spatial {
     public int attachChild(Spatial child) {
     public int attachChild(Spatial child) {
         return attachChildAt(child, children.size());
         return attachChildAt(child, children.size());
     }
     }
-    
     /**
     /**
-     * 
+     *
      * <code>attachChildAt</code> attaches a child to this node at an index. This node
      * <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
      * becomes the child's parent. The current number of children maintained is
      * returned.
      * returned.
      * <br>
      * <br>
      * If the child already had a parent it is detached from that former parent.
      * If the child already had a parent it is detached from that former parent.
-     * 
+     *
      * @param child
      * @param child
      *            the child to attach to this node.
      *            the child to attach to this node.
      * @return the number of children maintained by this node.
      * @return the number of children maintained by this node.
@@ -344,27 +352,25 @@ public class Node extends Spatial {
             }
             }
             child.setParent(this);
             child.setParent(this);
             children.add(index, child);
             children.add(index, child);
-            
             // XXX: Not entirely correct? Forces bound update up the
             // XXX: Not entirely correct? Forces bound update up the
             // tree stemming from the attached child. Also forces
             // tree stemming from the attached child. Also forces
             // transform update down the tree-
             // transform update down the tree-
             child.setTransformRefresh();
             child.setTransformRefresh();
             child.setLightListRefresh();
             child.setLightListRefresh();
+            child.setMatParamOverrideRefresh();
             if (logger.isLoggable(Level.FINE)) {
             if (logger.isLoggable(Level.FINE)) {
                 logger.log(Level.FINE,"Child ({0}) attached to this node ({1})",
                 logger.log(Level.FINE,"Child ({0}) attached to this node ({1})",
                         new Object[]{child.getName(), getName()});
                         new Object[]{child.getName(), getName()});
             }
             }
-            
             invalidateUpdateList();
             invalidateUpdateList();
         }
         }
-        
         return children.size();
         return children.size();
     }
     }
 
 
     /**
     /**
      * <code>detachChild</code> removes a given child from the node's list.
      * <code>detachChild</code> removes a given child from the node's list.
      * This child will no longer be maintained.
      * This child will no longer be maintained.
-     * 
+     *
      * @param child
      * @param child
      *            the child to remove.
      *            the child to remove.
      * @return the index the child was at. -1 if the child was not in the list.
      * @return the index the child was at. -1 if the child was not in the list.
@@ -379,16 +385,16 @@ public class Node extends Spatial {
                 detachChildAt(index);
                 detachChildAt(index);
             }
             }
             return index;
             return index;
-        } 
-            
-        return -1;        
+        }
+
+        return -1;
     }
     }
 
 
     /**
     /**
      * <code>detachChild</code> removes a given child from the node's list.
      * <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
      * This child will no longe be maintained. Only the first child with a
      * matching name is removed.
      * matching name is removed.
-     * 
+     *
      * @param childName
      * @param childName
      *            the child to remove.
      *            the child to remove.
      * @return the index the child was at. -1 if the child was not in the list.
      * @return the index the child was at. -1 if the child was not in the list.
@@ -408,10 +414,10 @@ public class Node extends Spatial {
     }
     }
 
 
     /**
     /**
-     * 
+     *
      * <code>detachChildAt</code> removes a child at a given index. That child
      * <code>detachChildAt</code> removes a child at a given index. That child
      * is returned for saving purposes.
      * is returned for saving purposes.
-     * 
+     *
      * @param index
      * @param index
      *            the index of the child to be removed.
      *            the index of the child to be removed.
      * @return the child at the supplied index.
      * @return the child at the supplied index.
@@ -432,6 +438,7 @@ public class Node extends Spatial {
             child.setTransformRefresh();
             child.setTransformRefresh();
             // lights are also inherited from parent
             // lights are also inherited from parent
             child.setLightListRefresh();
             child.setLightListRefresh();
+            child.setMatParamOverrideRefresh();
             
             
             invalidateUpdateList();
             invalidateUpdateList();
         }
         }
@@ -439,7 +446,7 @@ public class Node extends Spatial {
     }
     }
 
 
     /**
     /**
-     * 
+     *
      * <code>detachAllChildren</code> removes all children attached to this
      * <code>detachAllChildren</code> removes all children attached to this
      * node.
      * node.
      */
      */
@@ -458,7 +465,7 @@ public class Node extends Spatial {
      * in this node's list of children.
      * in this node's list of children.
      * @param sp
      * @param sp
      *          The spatial to look up
      *          The spatial to look up
-     * @return 
+     * @return
      *          The index of the spatial in the node's children, or -1
      *          The index of the spatial in the node's children, or -1
      *          if the spatial is not attached to this node
      *          if the spatial is not attached to this node
      */
      */
@@ -468,7 +475,7 @@ public class Node extends Spatial {
 
 
     /**
     /**
      * More efficient than e.g detaching and attaching as no updates are needed.
      * More efficient than e.g detaching and attaching as no updates are needed.
-     * 
+     *
      * @param index1 The index of the first child to swap
      * @param index1 The index of the first child to swap
      * @param index2 The index of the second child to swap
      * @param index2 The index of the second child to swap
      */
      */
@@ -481,9 +488,9 @@ public class Node extends Spatial {
     }
     }
 
 
     /**
     /**
-     * 
+     *
      * <code>getChild</code> returns a child at a given index.
      * <code>getChild</code> returns a child at a given index.
-     * 
+     *
      * @param i
      * @param i
      *            the index to retrieve the child from.
      *            the index to retrieve the child from.
      * @return the child at a specified index.
      * @return the child at a specified index.
@@ -497,13 +504,13 @@ public class Node extends Spatial {
      * given name (case sensitive.) This method does a depth first recursive
      * given name (case sensitive.) This method does a depth first recursive
      * search of all descendants of this node, it will return the first spatial
      * search of all descendants of this node, it will return the first spatial
      * found with a matching name.
      * found with a matching name.
-     * 
+     *
      * @param name
      * @param name
      *            the name of the child to retrieve. If null, we'll return null.
      *            the name of the child to retrieve. If null, we'll return null.
      * @return the child if found, or null.
      * @return the child if found, or null.
      */
      */
     public Spatial getChild(String name) {
     public Spatial getChild(String name) {
-        if (name == null) 
+        if (name == null)
             return null;
             return null;
 
 
         for (Spatial child : children.getArray()) {
         for (Spatial child : children.getArray()) {
@@ -518,11 +525,10 @@ public class Node extends Spatial {
         }
         }
         return null;
         return null;
     }
     }
-    
     /**
     /**
      * determines if the provided Spatial is contained in the children list of
      * determines if the provided Spatial is contained in the children list of
      * this node.
      * this node.
-     * 
+     *
      * @param spat
      * @param spat
      *            the child object to look for.
      *            the child object to look for.
      * @return true if the object is contained, false otherwise.
      * @return true if the object is contained, false otherwise.
@@ -566,39 +572,32 @@ public class Node extends Spatial {
 
 
     public int collideWith(Collidable other, CollisionResults results){
     public int collideWith(Collidable other, CollisionResults results){
         int total = 0;
         int total = 0;
-        
         // optimization: try collideWith BoundingVolume to avoid possibly redundant tests on children
         // 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.
         // 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
         I'm removing this change until some issues can be addressed and I really
         think it needs to be implemented a better way anyway.
         think it needs to be implemented a better way anyway.
-        
         First, it causes issues for anyone doing collideWith() with BoundingVolumes
         First, it causes issues for anyone doing collideWith() with BoundingVolumes
         and expecting it to trickle down to the children.  For example, children
         and expecting it to trickle down to the children.  For example, children
         with BoundingSphere bounding volumes and collideWith(BoundingSphere).  Doing
         with BoundingSphere bounding volumes and collideWith(BoundingSphere).  Doing
         a collision check at the parent level then has to do a BoundingSphere to BoundingBox
         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
         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.)
         case is tricky and the first sign that this is the wrong approach.)
-        
         Second, the rippling changes this caused to 'optimize' collideWith() for this
         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
         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
         idea of calculating a full collision just to see if the two shapes collide at all
         is very wasteful.
         is very wasteful.
-        
         A proper implementation should support a simpler boolean check that doesn't do
         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%
         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
         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.
         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'
         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
         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
         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
         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.
         enough to do all the time for > 1.
-        
         if (children.size() > 4)
         if (children.size() > 4)
         {
         {
           BoundingVolume bv = this.getWorldBound();
           BoundingVolume bv = this.getWorldBound();
@@ -642,7 +641,7 @@ public class Node extends Spatial {
      * @return Non-null, but possibly 0-element, list of matching Spatials (also Instances extending Spatials).
      * @return Non-null, but possibly 0-element, list of matching Spatials (also Instances extending Spatials).
      *
      *
      * @see java.util.regex.Pattern
      * @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")
     @SuppressWarnings("unchecked")
     public <T extends Spatial>List<T> descendantMatches(
     public <T extends Spatial>List<T> descendantMatches(
@@ -662,7 +661,7 @@ public class Node extends Spatial {
     /**
     /**
      * Convenience wrapper.
      * 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(
     public <T extends Spatial>List<T> descendantMatches(
             Class<T> spatialSubclass) {
             Class<T> spatialSubclass) {
@@ -672,7 +671,7 @@ public class Node extends Spatial {
     /**
     /**
      * Convenience wrapper.
      * 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) {
     public <T extends Spatial>List<T> descendantMatches(String nameRegex) {
         return descendantMatches(null, nameRegex);
         return descendantMatches(null, nameRegex);
@@ -691,12 +690,21 @@ public class Node extends Spatial {
         // Reset the fields of the clone that should be in a 'new' state.
         // Reset the fields of the clone that should be in a 'new' state.
         nodeClone.updateList = null;
         nodeClone.updateList = null;
         nodeClone.updateListValid = false; // safe because parent is nulled out in super.clone()
         nodeClone.updateListValid = false; // safe because parent is nulled out in super.clone()
-            
         return nodeClone;
         return nodeClone;
     }
     }
 
 
     @Override
     @Override
-    public Spatial deepClone(){
+    public Spatial deepClone() {
+        Node nodeClone = (Node)super.deepClone();
+
+        // 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;
+    }
+
+    public Spatial oldDeepClone(){
         Node nodeClone = (Node) super.clone();
         Node nodeClone = (Node) super.clone();
         nodeClone.children = new SafeArrayList<Spatial>(Spatial.class);
         nodeClone.children = new SafeArrayList<Spatial>(Spatial.class);
         for (Spatial child : children){
         for (Spatial child : children){
@@ -707,6 +715,20 @@ public class Node extends Spatial {
         return nodeClone;
         return nodeClone;
     }
     }
 
 
+    /**
+     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
+     */
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) {
+        super.cloneFields(cloner, 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
     @Override
     public void write(JmeExporter e) throws IOException {
     public void write(JmeExporter e) throws IOException {
         super.write(e);
         super.write(e);
@@ -718,8 +740,7 @@ public class Node extends Spatial {
         // XXX: Load children before loading itself!!
         // XXX: Load children before loading itself!!
         // This prevents empty children list if controls query
         // This prevents empty children list if controls query
         // it in Control.setSpatial().
         // it in Control.setSpatial().
-        
-        children = new SafeArrayList( Spatial.class, 
+        children = new SafeArrayList( Spatial.class,
                                       e.getCapsule(this).readSavableArrayList("children", null) );
                                       e.getCapsule(this).readSavableArrayList("children", null) );
 
 
         // go through children and set parent to this node
         // go through children and set parent to this node
@@ -728,7 +749,6 @@ public class Node extends Spatial {
                 child.parent = this;
                 child.parent = this;
             }
             }
         }
         }
-        
         super.read(e);
         super.read(e);
     }
     }
 
 
@@ -749,7 +769,6 @@ public class Node extends Spatial {
             }
             }
         }
         }
     }
     }
-    
     @Override
     @Override
     public void depthFirstTraversal(SceneGraphVisitor visitor) {
     public void depthFirstTraversal(SceneGraphVisitor visitor) {
         for (Spatial child : children.getArray()) {
         for (Spatial child : children.getArray()) {
@@ -757,7 +776,6 @@ public class Node extends Spatial {
         }
         }
         visitor.visit(this);
         visitor.visit(this);
     }
     }
-    
     @Override
     @Override
     protected void breadthFirstTraversal(SceneGraphVisitor visitor, Queue<Spatial> queue) {
     protected void breadthFirstTraversal(SceneGraphVisitor visitor, Queue<Spatial> queue) {
         queue.addAll(children);
         queue.addAll(children);

+ 269 - 83
jme3-core/src/main/java/com/jme3/scene/Spatial.java

@@ -38,6 +38,7 @@ import com.jme3.collision.Collidable;
 import com.jme3.export.*;
 import com.jme3.export.*;
 import com.jme3.light.Light;
 import com.jme3.light.Light;
 import com.jme3.light.LightList;
 import com.jme3.light.LightList;
+import com.jme3.material.MatParamOverride;
 import com.jme3.material.Material;
 import com.jme3.material.Material;
 import com.jme3.math.*;
 import com.jme3.math.*;
 import com.jme3.renderer.Camera;
 import com.jme3.renderer.Camera;
@@ -47,6 +48,9 @@ import com.jme3.renderer.queue.RenderQueue;
 import com.jme3.renderer.queue.RenderQueue.Bucket;
 import com.jme3.renderer.queue.RenderQueue.Bucket;
 import com.jme3.renderer.queue.RenderQueue.ShadowMode;
 import com.jme3.renderer.queue.RenderQueue.ShadowMode;
 import com.jme3.scene.control.Control;
 import com.jme3.scene.control.Control;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.IdentityCloneFunction;
+import com.jme3.util.clone.JmeCloneable;
 import com.jme3.util.SafeArrayList;
 import com.jme3.util.SafeArrayList;
 import com.jme3.util.TempVars;
 import com.jme3.util.TempVars;
 import java.io.IOException;
 import java.io.IOException;
@@ -63,17 +67,17 @@ import java.util.logging.Logger;
  * @author Joshua Slack
  * @author Joshua Slack
  * @version $Revision: 4075 $, $Data$
  * @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());
     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.
      * this spatial.
      */
      */
     public enum CullHint {
     public enum CullHint {
 
 
-        /** 
+        /**
          * Do whatever our parent does. If no parent, default to {@link #Dynamic}.
          * Do whatever our parent does. If no parent, default to {@link #Dynamic}.
          */
          */
         Inherit,
         Inherit,
@@ -83,13 +87,13 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
          * Camera planes whether or not this Spatial should be culled.
          * Camera planes whether or not this Spatial should be culled.
          */
          */
         Dynamic,
         Dynamic,
-        /** 
+        /**
          * Always cull this from the view, throwing away this object
          * Always cull this from the view, throwing away this object
          * and any children from rendering commands.
          * and any children from rendering commands.
          */
          */
         Always,
         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.
          * Note that we will still get culled if our parent is culled.
          */
          */
         Never;
         Never;
@@ -100,15 +104,15 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      */
      */
     public enum BatchHint {
     public enum BatchHint {
 
 
-        /** 
+        /**
          * Do whatever our parent does. If no parent, default to {@link #Always}.
          * Do whatever our parent does. If no parent, default to {@link #Always}.
          */
          */
         Inherit,
         Inherit,
-        /** 
+        /**
          * This spatial will always be batched when attached to a BatchNode.
          * This spatial will always be batched when attached to a BatchNode.
          */
          */
         Always,
         Always,
-        /** 
+        /**
          * This spatial will never be batched when attached to a BatchNode.
          * This spatial will never be batched when attached to a BatchNode.
          */
          */
         Never;
         Never;
@@ -119,11 +123,12 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
     protected static final int RF_TRANSFORM = 0x01, // need light resort + combine transforms
     protected static final int RF_TRANSFORM = 0x01, // need light resort + combine transforms
                                RF_BOUND = 0x02,
                                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
+                               RF_CHILD_LIGHTLIST = 0x08, // some child need geometry update
+                               RF_MATPARAM_OVERRIDE = 0x10;
     
     
     protected CullHint cullHint = CullHint.Inherit;
     protected CullHint cullHint = CullHint.Inherit;
     protected BatchHint batchHint = BatchHint.Inherit;
     protected BatchHint batchHint = BatchHint.Inherit;
-    /** 
+    /**
      * Spatial's bounding volume relative to the world.
      * Spatial's bounding volume relative to the world.
      */
      */
     protected BoundingVolume worldBound;
     protected BoundingVolume worldBound;
@@ -132,6 +137,10 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      */
      */
     protected LightList localLights;
     protected LightList localLights;
     protected transient LightList worldLights;
     protected transient LightList worldLights;
+
+    protected ArrayList<MatParamOverride> localOverrides;
+    protected ArrayList<MatParamOverride> worldOverrides;
+
     /** 
     /** 
      * This spatial's name.
      * This spatial's name.
      */
      */
@@ -147,11 +156,11 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
     protected HashMap<String, Savable> userData = null;
     protected HashMap<String, Savable> userData = null;
     /**
     /**
      * Used for smart asset caching
      * Used for smart asset caching
-     * 
-     * @see AssetKey#useSmartCache() 
+     *
+     * @see AssetKey#useSmartCache()
      */
      */
     protected AssetKey key;
     protected AssetKey key;
-    /** 
+    /**
      * Spatial's parent, or null if it has none.
      * Spatial's parent, or null if it has none.
      */
      */
     protected transient Node parent;
     protected transient Node parent;
@@ -174,7 +183,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
     /**
     /**
      * Serialization only. Do not use.
      * Serialization only. Do not use.
      * Not really. This class is never instantiated directly but the
      * 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
      * no-arg constructor... which is technically weaker than
      * forward supplying defaults.
      * forward supplying defaults.
      */
      */
@@ -192,13 +201,14 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      */
      */
     protected Spatial(String name) {
     protected Spatial(String name) {
         this.name = name;
         this.name = name;
-        
         localTransform = new Transform();
         localTransform = new Transform();
         worldTransform = new Transform();
         worldTransform = new Transform();
 
 
         localLights = new LightList(this);
         localLights = new LightList(this);
         worldLights = new LightList(this);
         worldLights = new LightList(this);
 
 
+        localOverrides = new ArrayList<>();
+        worldOverrides = new ArrayList<>();
         refreshFlags |= RF_BOUND;
         refreshFlags |= RF_BOUND;
     }
     }
 
 
@@ -219,13 +229,12 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
     boolean requiresUpdates() {
     boolean requiresUpdates() {
         return requiresUpdates | !controls.isEmpty();
         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.
      * updateLogicalState() to be called even if they contain no controls.
      * Setting this to false reverts to the default behavior of only
      * Setting this to false reverts to the default behavior of only
      * updating if the spatial has controls.  This is not meant to
      * 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
      * unattached or an IllegalStateException is thrown.  It is designed
      * to be called during object construction and then never changed, ie:
      * to be called during object construction and then never changed, ie:
      * it's meant to be subclass specific state and not runtime state.
      * it's meant to be subclass specific state and not runtime state.
@@ -251,12 +260,12 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
         // override it for more optimal behavior.  Node and Geometry will override
         // override it for more optimal behavior.  Node and Geometry will override
         // it to false if the class is Node.class or Geometry.class.
         // it to false if the class is Node.class or Geometry.class.
         // This means that all subclasses will default to the old behavior
         // This means that all subclasses will default to the old behavior
-        // unless they opt in. 
+        // unless they opt in.
         if( parent != null ) {
         if( parent != null ) {
-            throw new IllegalStateException("setRequiresUpdates() cannot be called once attached."); 
+            throw new IllegalStateException("setRequiresUpdates() cannot be called once attached.");
         }
         }
         this.requiresUpdates = f;
         this.requiresUpdates = f;
-    } 
+    }
 
 
     /**
     /**
      * Indicate that the transform of this spatial has changed and that
      * Indicate that the transform of this spatial has changed and that
@@ -269,35 +278,33 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
 
 
     protected void setLightListRefresh() {
     protected void setLightListRefresh() {
         refreshFlags |= RF_LIGHTLIST;
         refreshFlags |= RF_LIGHTLIST;
-        
         // Make sure next updateGeometricState() visits this branch
         // Make sure next updateGeometricState() visits this branch
         // to update lights.
         // to update lights.
         Spatial p = parent;
         Spatial p = parent;
         while (p != null) {
         while (p != null) {
-            //if (p.refreshFlags != 0) {
-                // any refresh flag is sufficient, 
-                // as each propagates to the root Node
-
-                // 2015/2/8:
-                // This is not true, because using e.g. getWorldBound()
-                // or getWorldTransform() activates a "partial refresh"
-                // which does not update the lights but does clear
-                // the refresh flags on the ancestors!
-            
-            //    return; 
-            //}
-            
             if ((p.refreshFlags & RF_CHILD_LIGHTLIST) != 0) {
             if ((p.refreshFlags & RF_CHILD_LIGHTLIST) != 0) {
                 // The parent already has this flag,
                 // The parent already has this flag,
                 // so must all ancestors.
                 // so must all ancestors.
                 return;
                 return;
             }
             }
-            
             p.refreshFlags |= RF_CHILD_LIGHTLIST;
             p.refreshFlags |= RF_CHILD_LIGHTLIST;
             p = p.parent;
             p = p.parent;
         }
         }
     }
     }
 
 
+    protected void setMatParamOverrideRefresh() {
+        refreshFlags |= RF_MATPARAM_OVERRIDE;
+        Spatial p = parent;
+        while (p != null) {
+            if ((p.refreshFlags & RF_MATPARAM_OVERRIDE) != 0) {
+                return;
+            }
+
+            p.refreshFlags |= RF_MATPARAM_OVERRIDE;
+            p = p.parent;
+        }
+    }
+
     /**
     /**
      * Indicate that the bounding of this spatial has changed and that
      * Indicate that the bounding of this spatial has changed and that
      * a refresh is required.
      * a refresh is required.
@@ -315,10 +322,9 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
             p = p.parent;
             p = p.parent;
         }
         }
     }
     }
-    
     /**
     /**
      * (Internal use only) Forces a refresh of the given types of data.
      * (Internal use only) Forces a refresh of the given types of data.
-     * 
+     *
      * @param transforms Refresh world transform based on parents'
      * @param transforms Refresh world transform based on parents'
      * @param bounds Refresh bounding volume data based on child nodes
      * @param bounds Refresh bounding volume data based on child nodes
      * @param lights Refresh light list based on parents'
      * @param lights Refresh light list based on parents'
@@ -401,9 +407,9 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
     /**
     /**
      * Returns the local {@link LightList}, which are the lights
      * Returns the local {@link LightList}, which are the lights
      * that were directly attached to this <code>Spatial</code> through the
      * 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.
      * {@link #removeLight(com.jme3.light.Light) } methods.
-     * 
+     *
      * @return The local light list
      * @return The local light list
      */
      */
     public LightList getLocalLightList() {
     public LightList getLocalLightList() {
@@ -414,13 +420,36 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      * Returns the world {@link LightList}, containing the lights
      * Returns the world {@link LightList}, containing the lights
      * combined from all this <code>Spatial's</code> parents up to and including
      * combined from all this <code>Spatial's</code> parents up to and including
      * this <code>Spatial</code>'s lights.
      * this <code>Spatial</code>'s lights.
-     * 
+     *
      * @return The combined world light list
      * @return The combined world light list
      */
      */
     public LightList getWorldLightList() {
     public LightList getWorldLightList() {
         return worldLights;
         return worldLights;
     }
     }
 
 
+    /**
+     * Get the local material parameter overrides.
+     *
+     * @return The list of local material parameter overrides.
+     */
+    public List<MatParamOverride> getLocalMatParamOverrides() {
+        return localOverrides;
+    }
+
+    /**
+     * Get the world material parameter overrides.
+     *
+     * Note that this list is only updated on a call to
+     * {@link #updateGeometricState()}. After update, the world overrides list
+     * will contain the {@link #getParent() parent's} world overrides combined
+     * with this spatial's {@link #getLocalMatParamOverrides() local overrides}.
+     *
+     * @return The list of world material parameter overrides.
+     */
+    public List<MatParamOverride> getWorldMatParamOverrides() {
+        return worldOverrides;
+    }
+
     /**
     /**
      * <code>getWorldRotation</code> retrieves the absolute rotation of the
      * <code>getWorldRotation</code> retrieves the absolute rotation of the
      * Spatial.
      * Spatial.
@@ -502,14 +531,14 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      * <code>lookAt</code> is a convenience method for auto-setting the local
      * <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
      * 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'.
      * 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.
      * 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.
      * 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 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.
      * spatial has, it will look at the given position in world space.
-     * 
+     *
      * @param position
      * @param position
      *            where to look at in terms of world coordinates
      *            where to look at in terms of world coordinates
      * @param upVector
      * @param upVector
@@ -522,10 +551,8 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
         TempVars vars = TempVars.get();
         TempVars vars = TempVars.get();
 
 
         Vector3f compVecA = vars.vect4;
         Vector3f compVecA = vars.vect4;
-      
         compVecA.set(position).subtractLocal(worldTranslation);
         compVecA.set(position).subtractLocal(worldTranslation);
-        getLocalRotation().lookAt(compVecA, upVector);        
-        
+        getLocalRotation().lookAt(compVecA, upVector);
         if ( getParent() != null ) {
         if ( getParent() != null ) {
             Quaternion rot=vars.quat1;
             Quaternion rot=vars.quat1;
             rot =  rot.set(parent.getWorldRotation()).inverseLocal().multLocal(getLocalRotation());
             rot =  rot.set(parent.getWorldRotation()).inverseLocal().multLocal(getLocalRotation());
@@ -552,15 +579,63 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
             worldLights.update(localLights, null);
             worldLights.update(localLights, null);
             refreshFlags &= ~RF_LIGHTLIST;
             refreshFlags &= ~RF_LIGHTLIST;
         } else {
         } else {
-            if ((parent.refreshFlags & RF_LIGHTLIST) == 0) {
-                worldLights.update(localLights, parent.worldLights);
-                refreshFlags &= ~RF_LIGHTLIST;
-            } else {
-                assert false;
-            }
+            assert (parent.refreshFlags & RF_LIGHTLIST) == 0;
+            worldLights.update(localLights, parent.worldLights);
+            refreshFlags &= ~RF_LIGHTLIST;
+        }
+    }
+
+    protected void updateMatParamOverrides() {
+        refreshFlags &= ~RF_MATPARAM_OVERRIDE;
+
+        worldOverrides.clear();
+        if (parent == null) {
+            worldOverrides.addAll(localOverrides);
+        } else {
+            assert (parent.refreshFlags & RF_MATPARAM_OVERRIDE) == 0;
+            worldOverrides.addAll(localOverrides);
+            worldOverrides.addAll(parent.worldOverrides);
+        }
+    }
+
+    /**
+     * Adds a local material parameter override.
+     *
+     * @param override The override to add.
+     * @see MatParamOverride
+     */
+    public void addMatParamOverride(MatParamOverride override) {
+        if (override == null) {
+            throw new IllegalArgumentException("override cannot be null");
+        }
+        localOverrides.add(override);
+        setMatParamOverrideRefresh();
+    }
+
+    /**
+     * Remove a local material parameter override if it exists.
+     *
+     * @param override The override to remove.
+     * @see MatParamOverride
+     */
+    public void removeMatParamOverride(MatParamOverride override) {
+        if (localOverrides.remove(override)) {
+            setMatParamOverrideRefresh();
         }
         }
     }
     }
 
 
+    /**
+     * Remove all local material parameter overrides.
+     *
+     * @see #addMatParamOverride(com.jme3.material.MatParamOverride)
+     */
+    public void clearMatParamOverrides() {
+        if (!localOverrides.isEmpty()) {
+            setMatParamOverrideRefresh();
+        }
+        localOverrides.clear();
+    }
+
     /**
     /**
      * Should only be called from updateGeometricState().
      * Should only be called from updateGeometricState().
      * In most cases should not be subclassed.
      * In most cases should not be subclassed.
@@ -579,7 +654,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.
      * efficient manner possible.
      */
      */
     void checkDoTransformUpdate() {
     void checkDoTransformUpdate() {
@@ -670,7 +745,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      * @param vp The ViewPort to which the Spatial is being rendered to.
      * @param vp The ViewPort to which the Spatial is being rendered to.
      *
      *
      * @see Spatial#addControl(com.jme3.scene.control.Control)
      * @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) {
     public void runControlRender(RenderManager rm, ViewPort vp) {
         if (controls.isEmpty()) {
         if (controls.isEmpty()) {
@@ -686,26 +761,25 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      * Add a control to the list of controls.
      * Add a control to the list of controls.
      * @param control The control to add.
      * @param control The control to add.
      *
      *
-     * @see Spatial#removeControl(java.lang.Class) 
+     * @see Spatial#removeControl(java.lang.Class)
      */
      */
     public void addControl(Control control) {
     public void addControl(Control control) {
         boolean before = requiresUpdates();
         boolean before = requiresUpdates();
         controls.add(control);
         controls.add(control);
         control.setSpatial(this);
         control.setSpatial(this);
         boolean after = requiresUpdates();
         boolean after = requiresUpdates();
-        
         // If the requirement to be updated has changed
         // If the requirement to be updated has changed
         // then we need to let the parent node know so it
         // then we need to let the parent node know so it
         // can rebuild its update list.
         // can rebuild its update list.
         if( parent != null && before != after ) {
         if( parent != null && before != after ) {
-            parent.invalidateUpdateList();   
+            parent.invalidateUpdateList();
         }
         }
     }
     }
 
 
     /**
     /**
      * Removes the first control that is an instance of the given class.
      * 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) {
     public void removeControl(Class<? extends Control> controlType) {
         boolean before = requiresUpdates();
         boolean before = requiresUpdates();
@@ -717,23 +791,22 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
             }
             }
         }
         }
         boolean after = requiresUpdates();
         boolean after = requiresUpdates();
-        
         // If the requirement to be updated has changed
         // If the requirement to be updated has changed
         // then we need to let the parent node know so it
         // then we need to let the parent node know so it
         // can rebuild its update list.
         // can rebuild its update list.
         if( parent != null && before != after ) {
         if( parent != null && before != after ) {
-            parent.invalidateUpdateList();   
+            parent.invalidateUpdateList();
         }
         }
     }
     }
 
 
     /**
     /**
      * Removes the given control from this spatial's controls.
      * Removes the given control from this spatial's controls.
-     * 
+     *
      * @param control The control to remove
      * @param control The control to remove
      * @return True if the control was successfully removed. False if the
      * @return True if the control was successfully removed. False if the
      * control is not assigned to this spatial.
      * 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) {
     public boolean removeControl(Control control) {
         boolean before = requiresUpdates();
         boolean before = requiresUpdates();
@@ -743,14 +816,12 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
         }
         }
 
 
         boolean after = requiresUpdates();
         boolean after = requiresUpdates();
-        
         // If the requirement to be updated has changed
         // If the requirement to be updated has changed
         // then we need to let the parent node know so it
         // then we need to let the parent node know so it
         // can rebuild its update list.
         // can rebuild its update list.
         if( parent != null && before != after ) {
         if( parent != null && before != after ) {
-            parent.invalidateUpdateList();   
+            parent.invalidateUpdateList();
         }
         }
-        
         return result;
         return result;
     }
     }
 
 
@@ -761,7 +832,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      * @param controlType The superclass of the control to look for.
      * @param controlType The superclass of the control to look for.
      * @return The first instance in the list of the controlType class, or null.
      * @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) {
     public <T extends Control> T getControl(Class<T> controlType) {
         for (Control c : controls.getArray()) {
         for (Control c : controls.getArray()) {
@@ -790,7 +861,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
     /**
     /**
      * @return The number of controls attached to this Spatial.
      * @return The number of controls attached to this Spatial.
      * @see Spatial#addControl(com.jme3.scene.control.Control)
      * @see Spatial#addControl(com.jme3.scene.control.Control)
-     * @see Spatial#removeControl(java.lang.Class) 
+     * @see Spatial#removeControl(java.lang.Class)
      */
      */
     public int getNumControls() {
     public int getNumControls() {
         return controls.size();
         return controls.size();
@@ -815,7 +886,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      * Calling this when the Spatial is attached to a node
      * Calling this when the Spatial is attached to a node
      * will cause undefined results. User code should only call this
      * will cause undefined results. User code should only call this
      * method on Spatials having no parent.
      * method on Spatials having no parent.
-     * 
+     *
      * @see Spatial#getWorldLightList()
      * @see Spatial#getWorldLightList()
      * @see Spatial#getWorldTransform()
      * @see Spatial#getWorldTransform()
      * @see Spatial#getWorldBound()
      * @see Spatial#getWorldBound()
@@ -835,6 +906,9 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
         if ((refreshFlags & RF_BOUND) != 0) {
         if ((refreshFlags & RF_BOUND) != 0) {
             updateWorldBound();
             updateWorldBound();
         }
         }
+        if ((refreshFlags & RF_MATPARAM_OVERRIDE) != 0) {
+            updateMatParamOverrides();
+        }
         
         
         assert refreshFlags == 0;
         assert refreshFlags == 0;
     }
     }
@@ -1067,9 +1141,9 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
 
 
     /**
     /**
      * <code>removeLight</code> removes the given light from the Spatial.
      * <code>removeLight</code> removes the given light from the Spatial.
-     * 
+     *
      * @param light The light to remove.
      * @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) {
     public void removeLight(Light light) {
         localLights.remove(light);
         localLights.remove(light);
@@ -1261,12 +1335,43 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      * Note that meshes of geometries are not cloned explicitly, they
      * Note that meshes of geometries are not cloned explicitly, they
      * are shared if static, or specially cloned if animated.
      * are shared if static, or specially cloned if animated.
      *
      *
-     * All controls will be cloned using the Control.cloneForSpatial method
-     * on the clone.
-     *
-     * @see Mesh#cloneForAnim() 
+     * @see Mesh#cloneForAnim()
+     */
+    public Spatial clone( boolean cloneMaterial ) {
+
+        // Setup the cloner for the type of cloning we want to do.
+        Cloner cloner = new Cloner();
+
+        // First, we definitely do not want to clone our own parent
+        cloner.setClonedValue(parent, null);
+
+        // If we aren't cloning materials then we will make sure those
+        // aren't cloned also
+        if( !cloneMaterial ) {
+            cloner.setCloneFunction(Material.class, new IdentityCloneFunction<Material>());
+        }
+
+        // By default the meshes are not cloned.  The geometry
+        // may choose to selectively force them to be cloned but
+        // normally they will be shared
+        cloner.setCloneFunction(Mesh.class, new IdentityCloneFunction<Mesh>());
+
+        // Clone it!
+        Spatial clone = cloner.clone(this);
+
+        // Because we've nulled the parent out we need to make sure
+        // the transforms and stuff get refreshed.
+        clone.setTransformRefresh();
+        clone.setLightListRefresh();
+        clone.setMatParamOverrideRefresh();
+
+        return clone;
+    }
+
+    /**
+     *  The old clone() method that did not use the new Cloner utility.
      */
      */
-    public Spatial clone(boolean cloneMaterial) {
+    public Spatial oldClone(boolean cloneMaterial) {
         try {
         try {
             Spatial clone = (Spatial) super.clone();
             Spatial clone = (Spatial) super.clone();
             if (worldBound != null) {
             if (worldBound != null) {
@@ -1279,6 +1384,13 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
             clone.localLights.setOwner(clone);
             clone.localLights.setOwner(clone);
             clone.worldLights.setOwner(clone);
             clone.worldLights.setOwner(clone);
 
 
+            clone.worldOverrides = new ArrayList<MatParamOverride>();
+            clone.localOverrides = new ArrayList<MatParamOverride>();
+
+            for (MatParamOverride override : localOverrides) {
+                clone.localOverrides.add((MatParamOverride) override.clone());
+            }
+
             // No need to force cloned to update.
             // No need to force cloned to update.
             // This node already has the refresh flags
             // This node already has the refresh flags
             // set below so it will have to update anyway.
             // set below so it will have to update anyway.
@@ -1300,6 +1412,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
             clone.setBoundRefresh();
             clone.setBoundRefresh();
             clone.setTransformRefresh();
             clone.setTransformRefresh();
             clone.setLightListRefresh();
             clone.setLightListRefresh();
+            clone.setMatParamOverrideRefresh();
 
 
             clone.controls = new SafeArrayList<Control>(Control.class);
             clone.controls = new SafeArrayList<Control>(Control.class);
             for (int i = 0; i < controls.size(); i++) {
             for (int i = 0; i < controls.size(); i++) {
@@ -1328,7 +1441,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      * All controls will be cloned using the Control.cloneForSpatial method
      * All controls will be cloned using the Control.cloneForSpatial method
      * on the clone.
      * on the clone.
      *
      *
-     * @see Mesh#cloneForAnim() 
+     * @see Mesh#cloneForAnim()
      */
      */
     @Override
     @Override
     public Spatial clone() {
     public Spatial clone() {
@@ -1342,7 +1455,73 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      *
      *
      * @see Spatial#clone()
      * @see Spatial#clone()
      */
      */
-    public abstract Spatial deepClone();
+    public Spatial deepClone() {
+        // Setup the cloner for the type of cloning we want to do.
+        Cloner cloner = new Cloner();
+
+        // First, we definitely do not want to clone our own parent
+        cloner.setClonedValue(parent, null);
+
+        Spatial clone = cloner.clone(this);
+
+        // Because we've nulled the parent out we need to make sure
+        // the transforms and stuff get refreshed.
+        clone.setTransformRefresh();
+        clone.setLightListRefresh();
+        clone.setMatParamOverrideRefresh();
+
+        return clone;
+    }
+
+    /**
+     *  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.worldOverrides = cloner.clone(worldOverrides);
+        this.localOverrides = cloner.clone(localOverrides);
+        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.
+        if( userData != null ) {
+            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) {
     public void setUserData(String key, Object data) {
         if (userData == null) {
         if (userData == null) {
@@ -1350,7 +1529,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
         }
         }
 
 
         if(data == null){
         if(data == null){
-            userData.remove(key);            
+            userData.remove(key);
         }else if (data instanceof Savable) {
         }else if (data instanceof Savable) {
             userData.put(key, (Savable) data);
             userData.put(key, (Savable) data);
         } else {
         } else {
@@ -1419,6 +1598,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
         capsule.write(shadowMode, "shadow_mode", ShadowMode.Inherit);
         capsule.write(shadowMode, "shadow_mode", ShadowMode.Inherit);
         capsule.write(localTransform, "transform", Transform.IDENTITY);
         capsule.write(localTransform, "transform", Transform.IDENTITY);
         capsule.write(localLights, "lights", null);
         capsule.write(localLights, "lights", null);
+        capsule.writeSavableArrayList(localOverrides, "overrides", null);
 
 
         // Shallow clone the controls array to convert its type.
         // Shallow clone the controls array to convert its type.
         capsule.writeSavableArrayList(new ArrayList(controls), "controlsList", null);
         capsule.writeSavableArrayList(new ArrayList(controls), "controlsList", null);
@@ -1442,10 +1622,16 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
         localLights = (LightList) ic.readSavable("lights", null);
         localLights = (LightList) ic.readSavable("lights", null);
         localLights.setOwner(this);
         localLights.setOwner(this);
 
 
+        localOverrides = ic.readSavableArrayList("overrides", null);
+        if (localOverrides == null) {
+            localOverrides = new ArrayList<>();
+        }
+        worldOverrides = new ArrayList<>();
+
         //changed for backward compatibility with j3o files generated before the AnimControl/SkeletonControl split
         //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 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.
         //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 = ic.readSavableArrayList("controlsList", null));
         controls.addAll(0, ic.readSavableArrayList("controlsList", null));
         controls.addAll(0, ic.readSavableArrayList("controlsList", null));
 
 
@@ -1508,9 +1694,9 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
     /**
     /**
      * <code>setQueueBucket</code> determines at what phase of the
      * <code>setQueueBucket</code> determines at what phase of the
      * rendering process this Spatial will rendered. See 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.
      * render queue buckets.
-     * 
+     *
      * @param queueBucket
      * @param queueBucket
      *            The bucket to use for this Spatial.
      *            The bucket to use for this Spatial.
      */
      */
@@ -1595,7 +1781,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      *
      *
      * @return store if not null, otherwise, a new matrix containing the result.
      * @return store if not null, otherwise, a new matrix containing the result.
      *
      *
-     * @see Spatial#getWorldTransform() 
+     * @see Spatial#getWorldTransform()
      */
      */
     public Matrix4f getLocalToWorldMatrix(Matrix4f store) {
     public Matrix4f getLocalToWorldMatrix(Matrix4f store) {
         if (store == null) {
         if (store == null) {

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

@@ -522,6 +522,7 @@ public class VertexBuffer extends NativeObject implements Savable, Cloneable {
 //            throw new UnsupportedOperationException("Data has already been sent. Cannot set usage.");
 //            throw new UnsupportedOperationException("Data has already been sent. Cannot set usage.");
 
 
         this.usage = usage;
         this.usage = usage;
+        this.setUpdateNeeded();
     }
     }
 
 
     /**
     /**

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

+ 82 - 43
jme3-core/src/main/java/com/jme3/scene/instancing/InstancedNode.java

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

+ 179 - 286
jme3-core/src/main/java/com/jme3/shader/DefineList.java

@@ -1,286 +1,179 @@
-/*
- * 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 com.jme3.shader;
-
-import com.jme3.export.*;
-import com.jme3.material.MatParam;
-import com.jme3.material.TechniqueDef;
-import com.jme3.util.ListMap;
-
-import java.io.IOException;
-import java.util.Map;
-import java.util.TreeMap;
-
-public final class DefineList implements Savable, Cloneable {
-
-    private static final String ONE = "1";
-    
-    private TreeMap<String, String> defines = new TreeMap<String, String>();
-    private String compiled = null;
-    private int cachedHashCode = 0;
-
-    public void write(JmeExporter ex) throws IOException{
-        OutputCapsule oc = ex.getCapsule(this);
-
-        String[] keys = new String[defines.size()];
-        String[] vals = new String[defines.size()];
-
-        int i = 0;
-        for (Map.Entry<String, String> define : defines.entrySet()){
-            keys[i] = define.getKey();
-            vals[i] = define.getValue();
-            i++;
-        }
-
-        oc.write(keys, "keys", null);
-        oc.write(vals, "vals", null);
-    }
-
-    public void read(JmeImporter im) throws IOException{
-        InputCapsule ic = im.getCapsule(this);
-
-        String[] keys = ic.readStringArray("keys", null);
-        String[] vals = ic.readStringArray("vals", null);
-        for (int i = 0; i < keys.length; i++){
-            defines.put(keys[i], vals[i]);
-        }
-    }
-
-    public void clear() {
-        defines.clear();
-        compiled = "";
-        cachedHashCode = 0;
-    }
-
-    public String get(String key){
-        return defines.get(key);
-    }
-    
-    @Override
-    public DefineList clone() {
-        try {
-            DefineList clone = (DefineList) super.clone();
-            clone.cachedHashCode = 0;
-            clone.compiled = null;
-            clone.defines = (TreeMap<String, String>) defines.clone();
-            return clone;
-        } catch (CloneNotSupportedException ex) {
-            throw new AssertionError();
-        }
-    }
-
-    public boolean set(String key, VarType type, Object val){    
-        if (val == null){
-            defines.remove(key);
-            compiled = null;
-            cachedHashCode = 0;
-            return true;
-        }
-
-        switch (type){
-            case Boolean:
-                if (((Boolean) val).booleanValue()) {
-                    // same literal, != will work
-                    if (defines.put(key, ONE) != ONE) {
-                        compiled = null;
-                        cachedHashCode = 0;
-                        return true;
-                    }
-                } else if (defines.containsKey(key)) {
-                    defines.remove(key);
-                    compiled = null;
-                    cachedHashCode = 0;
-                    return true;
-                }
-                
-                break;
-            case Float:
-            case Int:
-                String newValue = val.toString();
-                String original = defines.put(key, newValue);
-                if (!val.equals(original)) {
-                    compiled = null;
-                    cachedHashCode = 0;
-                    return true;            
-                }
-                break;
-            default:
-                // same literal, != will work
-                if (defines.put(key, ONE) != ONE) {  
-                    compiled = null;
-                    cachedHashCode = 0;
-                    return true;            
-                }
-                break;
-        }
-        
-        return false;
-    }
-
-    public boolean remove(String key){   
-        if (defines.remove(key) != null) {
-            compiled = null;
-            cachedHashCode = 0;
-            return true;
-        }
-        return false;
-    }
-
-    public void addFrom(DefineList other){    
-        if (other == null) {
-            return;
-        }
-        compiled = null;
-        cachedHashCode = 0;
-        defines.putAll(other.defines);
-    }
-
-    public String getCompiled(){
-        if (compiled == null){
-            StringBuilder sb = new StringBuilder();
-            for (Map.Entry<String, String> entry : defines.entrySet()){
-                sb.append("#define ").append(entry.getKey()).append(" ");
-                sb.append(entry.getValue()).append('\n');
-            }
-            compiled = sb.toString();
-        }
-        return compiled;
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        final DefineList other = (DefineList) obj;
-        return defines.equals(other.defines);
-    }
-    
-    /**
-     * Update defines if the define list changed based on material parameters.
-     * @param params
-     * @param def
-     * @return true if defines was updated
-     */
-    public boolean update(ListMap params, TechniqueDef def){
-        if(equalsParams(params, def)){
-            return false;
-        }
-         // Defines were changed, update define list
-        clear();
-        for(int i=0;i<params.size();i++) {
-            MatParam param = (MatParam)params.getValue(i);
-            String defineName = def.getShaderParamDefine(param.getName());
-            if (defineName != null) {
-                set(defineName, param.getVarType(), param.getValue());
-            }
-        }
-        return true;
-    }
-    
-    private boolean equalsParams(ListMap params, TechniqueDef def) {
-        
-        int size = 0;
-
-        for(int i = 0; i < params.size() ; i++ ) {
-            MatParam param = (MatParam)params.getValue(i);
-            String key = def.getShaderParamDefine(param.getName());
-            if (key != null) {
-                Object val = param.getValue();
-                if (val != null) {
-
-                    switch (param.getVarType()) {
-                    case Boolean: {
-                        String current = defines.get(key);
-                        if (((Boolean) val).booleanValue()) {
-                            if (current == null || current != ONE) {
-                                return false;
-                            }
-                            size++;
-                        } else {
-                            if (current != null) {
-                                return false;
-                            }
-                        }
-                    }
-                        break;
-                    case Float:
-                    case Int: {
-                        String newValue = val.toString();
-                        String current = defines.get(key);
-                        if (!newValue.equals(current)) {
-                            return false;
-                        }
-                        size++;
-                    }
-                        break;
-                    default: {
-                        if (!defines.containsKey(key)) {
-                            return false;
-                        }
-                        size++;
-                    }
-                        break;
-                    }
-
-                }
-
-            }
-        }
-
-        if (size != defines.size()) {
-            return false;
-        }
-
-        return true;
-    }
-    
-    @Override
-    public int hashCode() {
-        if (cachedHashCode == 0) {
-            cachedHashCode = defines.hashCode();
-        }
-        return cachedHashCode;
-    }
-
-    @Override
-    public String toString(){
-        StringBuilder sb = new StringBuilder();
-        int i = 0;
-        for (Map.Entry<String, String> entry : defines.entrySet()) {
-            sb.append(entry.getKey()).append("=").append(entry.getValue());
-            if (i != defines.size() - 1) {
-                sb.append(", ");
-            }
-            i++;
-        }
-        return sb.toString();
-    }
-
-}
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.shader;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * The new define list.
+ * 
+ * @author Kirill Vainer
+ */
+public final class DefineList {
+
+    public static final int MAX_DEFINES = 64;
+
+    private long hash;
+    private final int[] vals;
+
+    public DefineList(int numValues) {
+        if (numValues < 0 || numValues > MAX_DEFINES) {
+            throw new IllegalArgumentException("numValues must be between 0 and 64");
+        }
+        vals = new int[numValues];
+    }
+    
+    private DefineList(DefineList original) {
+        this.hash = original.hash;
+        this.vals = new int[original.vals.length];
+        System.arraycopy(original.vals, 0, vals, 0, vals.length);
+    }
+
+    public void set(int id, int val) {
+        assert 0 <= id && id < 64;
+        if (val != 0) {
+            hash |=  (1L << id);
+        } else {
+            hash &= ~(1L << id);
+        }
+        vals[id] = val;
+    }
+    
+    public void set(int id, float val) {
+        set(id, Float.floatToIntBits(val));
+    }
+    
+    public void set(int id, boolean val) {
+        set(id, val ? 1 : 0);
+    }
+
+    public void set(int id, VarType type, Object value) {
+        if (value == null) {
+            set(id, 0);
+            return;
+        }
+
+        switch (type) {
+            case Int:
+                set(id, (Integer) value);
+                break;
+            case Float:
+                set(id, (Float) value);
+                break;
+            case Boolean:
+                set(id, ((Boolean) value));
+                break;
+            default:
+                set(id, 1);
+                break;
+        }
+    }
+
+    public void setAll(DefineList other) {
+        for (int i = 0; i < other.vals.length; i++) {
+            if (other.vals[i] != 0) {
+                vals[i] = other.vals[i];
+            }
+        }
+    }
+
+    public void clear() {
+        hash = 0;
+        Arrays.fill(vals, 0);
+    }
+
+    public boolean getBoolean(int id) {
+        return vals[id] != 0;
+    }
+    
+    public float getFloat(int id) {
+        return Float.intBitsToFloat(vals[id]);
+    }
+    
+    public int getInt(int id) {
+        return vals[id];
+    }
+    
+    @Override
+    public int hashCode() {
+        return (int)((hash >> 32) ^ hash);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+         DefineList o = (DefineList) other;
+         if (hash == o.hash) {
+             for (int i = 0; i < vals.length; i++) {
+                  if (vals[i] != o.vals[i]) return false;
+             }
+             return true;
+         }
+         return false;
+    }
+
+    public DefineList deepClone() {
+         return new DefineList(this);
+    }
+    
+    public void generateSource(StringBuilder sb, List<String> defineNames, List<VarType> defineTypes) {
+        for (int i = 0; i < vals.length; i++) {
+            if (vals[i] != 0) {
+                String defineName = defineNames.get(i);
+                
+                sb.append("#define ");
+                sb.append(defineName);
+                sb.append(" ");
+                
+                if (defineTypes != null && defineTypes.get(i) == VarType.Float) {
+                    float val = Float.intBitsToFloat(vals[i]);
+                    if (Float.isInfinite(val) || Float.isNaN(val)) {
+                        throw new IllegalArgumentException(
+                                "GLSL does not support NaN "
+                                + "or Infinite float literals");
+                    }
+                    sb.append(val);
+                } else {
+                    sb.append(vals[i]);
+                }
+                
+                sb.append("\n");
+            }
+        }
+    }
+    
+    public String generateSource(List<String> defineNames, List<VarType> defineTypes) {
+        StringBuilder sb = new StringBuilder();
+        generateSource(sb, defineNames, defineTypes);
+        return sb.toString();
+    }
+}

+ 6 - 1
jme3-core/src/main/java/com/jme3/shader/Glsl100ShaderGenerator.java

@@ -258,7 +258,7 @@ public class Glsl100ShaderGenerator extends ShaderGenerator {
         }
         }
        
        
         for (ShaderNodeVariable var : shaderNode.getDefinition().getOutputs()) {
         for (ShaderNodeVariable var : shaderNode.getDefinition().getOutputs()) {
-            ShaderNodeVariable v = new ShaderNodeVariable(var.getType(), shaderNode.getName(), var.getName());
+            ShaderNodeVariable v = new ShaderNodeVariable(var.getType(), shaderNode.getName(), var.getName(), var.getMultiplicity());
             if (!declaredInputs.contains(shaderNode.getName() + "_" + var.getName())) {
             if (!declaredInputs.contains(shaderNode.getName() + "_" + var.getName())) {
                 if (!isVarying(info, v)) {
                 if (!isVarying(info, v)) {
                     declareVariable(source, v);
                     declareVariable(source, v);
@@ -397,6 +397,11 @@ public class Glsl100ShaderGenerator extends ShaderGenerator {
         source.append(mapping.getLeftVariable().getNameSpace());
         source.append(mapping.getLeftVariable().getNameSpace());
         source.append("_");
         source.append("_");
         source.append(mapping.getLeftVariable().getName());
         source.append(mapping.getLeftVariable().getName());
+        if (mapping.getLeftVariable().getMultiplicity() != null){
+            source.append("[");
+            source.append(mapping.getLeftVariable().getMultiplicity());
+            source.append("]");
+        }
         
         
         //left swizzle, the variable can't be declared and assigned on the same line. 
         //left swizzle, the variable can't be declared and assigned on the same line. 
         if (mapping.getLeftSwizzling().length() > 0) {
         if (mapping.getLeftSwizzling().length() > 0) {

+ 56 - 23
jme3-core/src/main/java/com/jme3/shader/Shader.java

@@ -45,44 +45,63 @@ public final class Shader extends NativeObject {
     /**
     /**
      * A list of all shader sources currently attached.
      * A list of all shader sources currently attached.
      */
      */
-    private ArrayList<ShaderSource> shaderSourceList;
+    private final ArrayList<ShaderSource> shaderSourceList;
 
 
     /**
     /**
      * Maps uniform name to the uniform variable.
      * Maps uniform name to the uniform variable.
      */
      */
-    private ListMap<String, Uniform> uniforms;
+    private final ListMap<String, Uniform> uniforms;
+    
+    /**
+     * Uniforms bound to {@link UniformBinding}s.
+     * 
+     * Managed by the {@link UniformBindingManager}.
+     */
+    private final ArrayList<Uniform> boundUniforms;
 
 
     /**
     /**
      * Maps attribute name to the location of the attribute in the shader.
      * Maps attribute name to the location of the attribute in the shader.
      */
      */
-    private IntMap<Attribute> attribs;
+    private final IntMap<Attribute> attribs;
 
 
     /**
     /**
      * Type of shader. The shader will control the pipeline of it's type.
      * Type of shader. The shader will control the pipeline of it's type.
      */
      */
     public static enum ShaderType {
     public static enum ShaderType {
+
         /**
         /**
          * Control fragment rasterization. (e.g color of pixel).
          * Control fragment rasterization. (e.g color of pixel).
          */
          */
-        Fragment,
-
+        Fragment("frag"),
         /**
         /**
          * Control vertex processing. (e.g transform of model to clip space)
          * Control vertex processing. (e.g transform of model to clip space)
          */
          */
-        Vertex,
-
+        Vertex("vert"),
         /**
         /**
-         * Control geometry assembly. (e.g compile a triangle list from input data)
+         * Control geometry assembly. (e.g compile a triangle list from input
+         * data)
          */
          */
-        Geometry,
+        Geometry("geom"),
         /**
         /**
-         * Controls tesselation factor (e.g how often a input patch should be subdivided)
+         * Controls tesselation factor (e.g how often a input patch should be
+         * subdivided)
          */
          */
-        TessellationControl,
+        TessellationControl("tsctrl"),
         /**
         /**
-         * Controls tesselation transform (e.g similar to the vertex shader, but required to mix inputs manual)
+         * Controls tesselation transform (e.g similar to the vertex shader, but
+         * required to mix inputs manual)
          */
          */
-        TessellationEvaluation;
+        TessellationEvaluation("tseval");
+
+        private String extension;
+        
+        public String getExtension() {
+            return extension;
+        }
+        
+        private ShaderType(String extension) {
+            this.extension = extension;
+        }
     }
     }
 
 
     /**
     /**
@@ -195,22 +214,16 @@ public final class Shader extends NativeObject {
         }
         }
     }
     }
 
 
-    /**
-     * Initializes the shader for use, must be called after the 
-     * constructor without arguments is used.
-     */
-    public void initialize() {
-        shaderSourceList = new ArrayList<ShaderSource>();
-        uniforms = new ListMap<String, Uniform>();
-        attribs = new IntMap<Attribute>();
-    }
-    
     /**
     /**
      * Creates a new shader, {@link #initialize() } must be called
      * Creates a new shader, {@link #initialize() } must be called
      * after this constructor for the shader to be usable.
      * after this constructor for the shader to be usable.
      */
      */
     public Shader(){
     public Shader(){
         super();
         super();
+        shaderSourceList = new ArrayList<ShaderSource>();
+        uniforms = new ListMap<String, Uniform>();
+        attribs = new IntMap<Attribute>();
+        boundUniforms = new ArrayList<Uniform>();
     }
     }
 
 
     /**
     /**
@@ -225,6 +238,10 @@ public final class Shader extends NativeObject {
         for (ShaderSource source : s.shaderSourceList){
         for (ShaderSource source : s.shaderSourceList){
             shaderSourceList.add( (ShaderSource)source.createDestructableClone() );
             shaderSourceList.add( (ShaderSource)source.createDestructableClone() );
         }
         }
+        
+        uniforms = null;
+        boundUniforms = null;
+        attribs = null;
     }
     }
 
 
     /**
     /**
@@ -248,6 +265,18 @@ public final class Shader extends NativeObject {
         setUpdateNeeded();
         setUpdateNeeded();
     }
     }
 
 
+    public void addUniformBinding(UniformBinding binding){
+        String uniformName = "g_" + binding.name();
+        Uniform uniform = uniforms.get(uniformName);
+        if (uniform == null) {
+            uniform = new Uniform();
+            uniform.name = uniformName;
+            uniform.binding = binding;
+            uniforms.put(uniformName, uniform);
+            boundUniforms.add(uniform);
+        }
+    }
+    
     public Uniform getUniform(String name){
     public Uniform getUniform(String name){
         assert name.startsWith("m_") || name.startsWith("g_");
         assert name.startsWith("m_") || name.startsWith("g_");
         Uniform uniform = uniforms.get(name);
         Uniform uniform = uniforms.get(name);
@@ -277,6 +306,10 @@ public final class Shader extends NativeObject {
     public ListMap<String, Uniform> getUniformMap(){
     public ListMap<String, Uniform> getUniformMap(){
         return uniforms;
         return uniforms;
     }
     }
+    
+    public ArrayList<Uniform> getBoundUniforms() {
+        return boundUniforms;
+    }
 
 
     public Collection<ShaderSource> getSources(){
     public Collection<ShaderSource> getSources(){
         return shaderSourceList;
         return shaderSourceList;

+ 30 - 17
jme3-core/src/main/java/com/jme3/shader/ShaderGenerator.java

@@ -57,9 +57,9 @@ public abstract class ShaderGenerator {
      */
      */
     protected int indent;
     protected int indent;
     /**
     /**
-     * the technique to use for the shader generation
+     * the technique def to use for the shader generation
      */
      */
-    protected Technique technique = null;    
+    protected TechniqueDef techniqueDef = null;    
 
 
     /**
     /**
      * Build a shaderGenerator
      * Build a shaderGenerator
@@ -70,8 +70,8 @@ public abstract class ShaderGenerator {
         this.assetManager = assetManager;        
         this.assetManager = assetManager;        
     }
     }
     
     
-    public void initialize(Technique technique){
-        this.technique = technique;
+    public void initialize(TechniqueDef techniqueDef){
+        this.techniqueDef = techniqueDef;
     }
     }
     
     
     /**
     /**
@@ -79,24 +79,29 @@ public abstract class ShaderGenerator {
      *
      *
      * @return a Shader program
      * @return a Shader program
      */
      */
-    public Shader generateShader() {
-        if(technique == null){
-            throw new UnsupportedOperationException("The shaderGenerator was not properly initialized, call initialize(Technique) before any generation");
+    public Shader generateShader(String definesSourceCode) {
+        if (techniqueDef == null) {
+            throw new UnsupportedOperationException("The shaderGenerator was not "
+                    + "properly initialized, call "
+                    + "initialize(TechniqueDef) before any generation");
         }
         }
 
 
-        DefineList defines = technique.getAllDefines();
-        TechniqueDef def = technique.getDef();
-        ShaderGenerationInfo info = def.getShaderGenerationInfo();
-
-        String vertexSource = buildShader(def.getShaderNodes(), info, ShaderType.Vertex);
-        String fragmentSource = buildShader(def.getShaderNodes(), info, ShaderType.Fragment);
+        String techniqueName = techniqueDef.getName();
+        ShaderGenerationInfo info = techniqueDef.getShaderGenerationInfo();
 
 
         Shader shader = new Shader();
         Shader shader = new Shader();
-        shader.initialize();
-        shader.addSource(Shader.ShaderType.Vertex, technique.getDef().getName() + ".vert", vertexSource, defines.getCompiled(), getLanguageAndVersion(ShaderType.Vertex));
-        shader.addSource(Shader.ShaderType.Fragment, technique.getDef().getName() + ".frag", fragmentSource, defines.getCompiled(), getLanguageAndVersion(ShaderType.Fragment));
+        for (ShaderType type : ShaderType.values()) {
+            String extension = type.getExtension();
+            String language = getLanguageAndVersion(type);
+            String shaderSourceCode = buildShader(techniqueDef.getShaderNodes(), info, type);
+            
+            if (shaderSourceCode != null) {
+                String shaderSourceAssetName = techniqueName + "." + extension;
+                shader.addSource(type, shaderSourceAssetName, shaderSourceCode, definesSourceCode, language);
+            }
+        }
         
         
-        technique = null;
+        techniqueDef = null;
         return shader;
         return shader;
     }
     }
 
 
@@ -109,6 +114,14 @@ public abstract class ShaderGenerator {
      * @return the code of the generated vertex shader
      * @return the code of the generated vertex shader
      */
      */
     protected String buildShader(List<ShaderNode> shaderNodes, ShaderGenerationInfo info, ShaderType type) {
     protected String buildShader(List<ShaderNode> shaderNodes, ShaderGenerationInfo info, ShaderType type) {
+        if (type == ShaderType.TessellationControl ||
+            type == ShaderType.TessellationEvaluation || 
+            type == ShaderType.Geometry) {
+            // TODO: Those are not supported.
+            // Too much code assumes that type is either Vertex or Fragment
+            return null;
+        }
+        
         indent = 0;
         indent = 0;
 
 
         StringBuilder sourceDeclaration = new StringBuilder();
         StringBuilder sourceDeclaration = new StringBuilder();

+ 0 - 201
jme3-core/src/main/java/com/jme3/shader/ShaderKey.java

@@ -1,201 +0,0 @@
-/*
- * 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 com.jme3.shader;
-
-import com.jme3.asset.AssetKey;
-import com.jme3.export.InputCapsule;
-import com.jme3.export.JmeExporter;
-import com.jme3.export.JmeImporter;
-import com.jme3.export.OutputCapsule;
-import java.io.IOException;
-import java.util.EnumMap;
-import java.util.Set;
-
-public class ShaderKey extends AssetKey<Shader> {
-
-    protected EnumMap<Shader.ShaderType,String> shaderLanguage;
-    protected EnumMap<Shader.ShaderType,String> shaderName;
-    protected DefineList defines;
-    protected int cachedHashedCode = 0;
-    protected boolean usesShaderNodes = false;
-
-    public ShaderKey(){
-        shaderLanguage=new EnumMap<Shader.ShaderType, String>(Shader.ShaderType.class);
-        shaderName=new EnumMap<Shader.ShaderType, String>(Shader.ShaderType.class);
-    }
-
-    public ShaderKey(DefineList defines, EnumMap<Shader.ShaderType,String> shaderLanguage,EnumMap<Shader.ShaderType,String> shaderName){
-        super("");
-        this.name = reducePath(getShaderName(Shader.ShaderType.Vertex));
-        this.shaderLanguage=new EnumMap<Shader.ShaderType, String>(Shader.ShaderType.class);
-        this.shaderName=new EnumMap<Shader.ShaderType, String>(Shader.ShaderType.class);
-        this.defines = defines;
-        for (Shader.ShaderType shaderType : shaderName.keySet()) {
-            this.shaderName.put(shaderType,shaderName.get(shaderType));
-            this.shaderLanguage.put(shaderType,shaderLanguage.get(shaderType));
-        }
-    }
-    
-    @Override
-    public ShaderKey clone() {
-        ShaderKey clone = (ShaderKey) super.clone();
-        clone.cachedHashedCode = 0;
-        clone.defines = defines.clone();
-        return clone;
-    }
-    
-    @Override
-    public String toString(){
-        //todo:
-        return "V="+name+";";
-    }
-    
-    private final String getShaderName(Shader.ShaderType type) {
-        if (shaderName == null) {
-            return "";
-        }
-        String shName = shaderName.get(type);
-        return shName != null ? shName : "";
-    }
-
-    //todo: make equals and hashCode work
-    @Override
-    public boolean equals(Object obj) {
-        final ShaderKey other = (ShaderKey) obj;
-        if (name.equals(other.name) && getShaderName(Shader.ShaderType.Fragment).equals(other.getShaderName(Shader.ShaderType.Fragment))){
-            if (defines != null && other.defines != null) {
-                return defines.equals(other.defines);
-            } else if (defines != null || other.defines != null) {
-                return false;
-            } else {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    @Override
-    public int hashCode() {
-        if (cachedHashedCode == 0) {
-            int hash = 7;
-            hash = 41 * hash + name.hashCode();
-            hash = 41 * hash + getShaderName(Shader.ShaderType.Fragment).hashCode();
-            hash = getShaderName(Shader.ShaderType.Geometry) == null ? hash : 41 * hash + getShaderName(Shader.ShaderType.Geometry).hashCode();
-            hash = getShaderName(Shader.ShaderType.TessellationControl) == null ? hash : 41 * hash + getShaderName(Shader.ShaderType.TessellationControl).hashCode();
-            hash = getShaderName(Shader.ShaderType.TessellationEvaluation) == null ? hash : 41 * hash + getShaderName(Shader.ShaderType.TessellationEvaluation).hashCode();
-            hash = 41 * hash + (defines != null ? defines.hashCode() : 0);
-            cachedHashedCode = hash;
-        }
-        return cachedHashedCode;
-    }
-
-    public DefineList getDefines() {
-        return defines;
-    }
-
-    public String getVertName(){
-        return getShaderName(Shader.ShaderType.Vertex);
-    }
-
-    public String getFragName() {
-        return getShaderName(Shader.ShaderType.Fragment);
-    }
-
-    /**
-     * @deprecated Use {@link #getVertexShaderLanguage() } instead.
-     */
-    @Deprecated
-    public String getLanguage() {
-        return shaderLanguage.get(Shader.ShaderType.Vertex);
-    }
-    
-    public String getVertexShaderLanguage() { 
-        return shaderLanguage.get(Shader.ShaderType.Vertex);
-    }
-    
-    public String getFragmentShaderLanguage() {
-        return shaderLanguage.get(Shader.ShaderType.Vertex);
-    }
-
-    public boolean isUsesShaderNodes() {
-        return usesShaderNodes;
-    }
-
-    public void setUsesShaderNodes(boolean usesShaderNodes) {
-        this.usesShaderNodes = usesShaderNodes;
-    }
-
-    public Set<Shader.ShaderType> getUsedShaderPrograms(){
-        return shaderName.keySet();
-    }
-
-    public String getShaderProgramLanguage(Shader.ShaderType shaderType){
-        return shaderLanguage.get(shaderType);
-    }
-
-    public String getShaderProgramName(Shader.ShaderType shaderType){
-        return getShaderName(shaderType);
-    }
-
-    @Override
-    public void write(JmeExporter ex) throws IOException{
-        super.write(ex);
-        OutputCapsule oc = ex.getCapsule(this);
-        oc.write(shaderName.get(Shader.ShaderType.Fragment), "fragment_name", null);
-        oc.write(shaderName.get(Shader.ShaderType.Geometry), "geometry_name", null);
-        oc.write(shaderName.get(Shader.ShaderType.TessellationControl), "tessControl_name", null);
-        oc.write(shaderName.get(Shader.ShaderType.TessellationEvaluation), "tessEval_name", null);
-        oc.write(shaderLanguage.get(Shader.ShaderType.Vertex), "language", null);
-        oc.write(shaderLanguage.get(Shader.ShaderType.Fragment), "frag_language", null);
-        oc.write(shaderLanguage.get(Shader.ShaderType.Geometry), "geom_language", null);
-        oc.write(shaderLanguage.get(Shader.ShaderType.TessellationControl), "tsctrl_language", null);
-        oc.write(shaderLanguage.get(Shader.ShaderType.TessellationEvaluation), "tseval_language", null);
-
-    }
-
-    @Override
-    public void read(JmeImporter im) throws IOException{
-        super.read(im);
-        InputCapsule ic = im.getCapsule(this);
-        shaderName.put(Shader.ShaderType.Vertex,name);
-        shaderName.put(Shader.ShaderType.Fragment,ic.readString("fragment_name", null));
-        shaderName.put(Shader.ShaderType.Geometry,ic.readString("geometry_name", null));
-        shaderName.put(Shader.ShaderType.TessellationControl,ic.readString("tessControl_name", null));
-        shaderName.put(Shader.ShaderType.TessellationEvaluation,ic.readString("tessEval_name", null));
-        shaderLanguage.put(Shader.ShaderType.Vertex,ic.readString("language", null));
-        shaderLanguage.put(Shader.ShaderType.Fragment,ic.readString("frag_language", null));
-        shaderLanguage.put(Shader.ShaderType.Geometry,ic.readString("geom_language", null));
-        shaderLanguage.put(Shader.ShaderType.TessellationControl,ic.readString("tsctrl_language", null));
-        shaderLanguage.put(Shader.ShaderType.TessellationEvaluation,ic.readString("tseval_language", null));
-    }
-
-}

+ 82 - 33
jme3-core/src/main/java/com/jme3/shader/Uniform.java

@@ -70,6 +70,30 @@ public class Uniform extends ShaderVariable {
      */
      */
     protected boolean setByCurrentMaterial = false;
     protected boolean setByCurrentMaterial = false;
 
 
+    @Override
+    public int hashCode() {
+        int hash = 5;
+        hash = 31 * hash + (this.value != null ? this.value.hashCode() : 0);
+        hash = 31 * hash + (this.varType != null ? this.varType.hashCode() : 0);
+        hash = 31 * hash + (this.binding != null ? this.binding.hashCode() : 0);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        final Uniform other = (Uniform) obj;
+        if (this.value != other.value && (this.value == null || !this.value.equals(other.value))) {
+            return false;
+        }
+        return this.binding == other.binding && this.varType == other.varType;
+    }
+
     @Override
     @Override
     public String toString(){
     public String toString(){
         StringBuilder sb = new StringBuilder();
         StringBuilder sb = new StringBuilder();
@@ -102,6 +126,10 @@ public class Uniform extends ShaderVariable {
     public Object getValue(){
     public Object getValue(){
         return value;
         return value;
     }
     }
+    
+    public FloatBuffer getMultiData() {
+        return multiData;
+    }
 
 
     public boolean isSetByCurrentMaterial() {
     public boolean isSetByCurrentMaterial() {
         return setByCurrentMaterial;
         return setByCurrentMaterial;
@@ -111,21 +139,6 @@ public class Uniform extends ShaderVariable {
         setByCurrentMaterial = false;
         setByCurrentMaterial = false;
     }
     }
 
 
-    private static void setVector4(Vector4f vec, Object value) {
-        if (value instanceof ColorRGBA) {
-            ColorRGBA color = (ColorRGBA) value;
-            vec.set(color.r, color.g, color.b, color.a);
-        } else if (value instanceof Quaternion) {
-            Quaternion quat = (Quaternion) value;
-            vec.set(quat.getX(), quat.getY(), quat.getZ(), quat.getW());
-        } else if (value instanceof Vector4f) {
-            Vector4f vec4 = (Vector4f) value;
-            vec.set(vec4);
-        } else{
-            throw new IllegalArgumentException();
-        }
-    }
-    
     public void clearValue(){
     public void clearValue(){
         updateNeeded = true;
         updateNeeded = true;
 
 
@@ -182,27 +195,43 @@ public class Uniform extends ShaderVariable {
         }
         }
 
 
         if (value == null) {
         if (value == null) {
-            throw new NullPointerException();
+            throw new IllegalArgumentException("for uniform " + name + ": value cannot be null");
         }
         }
 
 
         setByCurrentMaterial = true;
         setByCurrentMaterial = true;
 
 
         switch (type){
         switch (type){
             case Matrix3:
             case Matrix3:
+                if (value.equals(this.value)) {
+                    return;
+                }
                 Matrix3f m3 = (Matrix3f) value;
                 Matrix3f m3 = (Matrix3f) value;
                 if (multiData == null) {
                 if (multiData == null) {
                     multiData = BufferUtils.createFloatBuffer(9);
                     multiData = BufferUtils.createFloatBuffer(9);
                 }
                 }
                 m3.fillFloatBuffer(multiData, true);
                 m3.fillFloatBuffer(multiData, true);
                 multiData.clear();
                 multiData.clear();
+                if (this.value == null) {
+                    this.value = new Matrix3f(m3);
+                } else {
+                    ((Matrix3f)this.value).set(m3);
+                }
                 break;
                 break;
             case Matrix4:
             case Matrix4:
+                if (value.equals(this.value)) {
+                    return;
+                }
                 Matrix4f m4 = (Matrix4f) value;
                 Matrix4f m4 = (Matrix4f) value;
                 if (multiData == null) {
                 if (multiData == null) {
                     multiData = BufferUtils.createFloatBuffer(16);
                     multiData = BufferUtils.createFloatBuffer(16);
                 }
                 }
                 m4.fillFloatBuffer(multiData, true);
                 m4.fillFloatBuffer(multiData, true);
                 multiData.clear();
                 multiData.clear();
+                if (this.value == null) {
+                    this.value = new Matrix4f(m4);
+                } else {
+                    ((Matrix4f)this.value).copy(m4);
+                }
                 break;
                 break;
             case IntArray:
             case IntArray:
                 int[] ia = (int[]) value;
                 int[] ia = (int[]) value;
@@ -283,11 +312,32 @@ public class Uniform extends ShaderVariable {
                 }
                 }
                 multiData.clear();
                 multiData.clear();
                 break;
                 break;
+            case Vector4:
+                if (value.equals(this.value)) {
+                    return;
+                }
+                if (value instanceof ColorRGBA) {
+                    if (this.value == null) {
+                        this.value = new ColorRGBA();
+                    }
+                    ((ColorRGBA) this.value).set((ColorRGBA) value);
+                } else if (value instanceof Vector4f) {
+                    if (this.value == null) {
+                        this.value = new Vector4f();
+                    }
+                    ((Vector4f) this.value).set((Vector4f) value);
+                } else {
+                    if (this.value == null) {
+                        this.value = new Quaternion();
+                    }
+                    ((Quaternion) this.value).set((Quaternion) value);
+                }
+                break;
                 // Only use check if equals optimization for primitive values
                 // Only use check if equals optimization for primitive values
             case Int:
             case Int:
             case Float:
             case Float:
             case Boolean:
             case Boolean:
-                if (this.value != null && this.value.equals(value)) {
+                if (value.equals(this.value)) {
                     return;
                     return;
                 }
                 }
                 this.value = value;
                 this.value = value;
@@ -297,39 +347,38 @@ public class Uniform extends ShaderVariable {
                 break;
                 break;
         }
         }
 
 
-        if (multiData != null) {
-            this.value = multiData;
-        }
+//        if (multiData != null) {
+//            this.value = multiData;
+//        }
 
 
         varType = type;
         varType = type;
         updateNeeded = true;
         updateNeeded = true;
     }
     }
 
 
     public void setVector4Length(int length){
     public void setVector4Length(int length){
-        if (location == -1)
+        if (location == -1) {
             return;
             return;
-
-        FloatBuffer fb = (FloatBuffer) value;
-        if (fb == null || fb.capacity() < length * 4) {
-            value = BufferUtils.createFloatBuffer(length * 4);
         }
         }
-
+        
+        multiData = BufferUtils.ensureLargeEnough(multiData, length * 4);
+        value = multiData;
         varType = VarType.Vector4Array;
         varType = VarType.Vector4Array;
         updateNeeded = true;
         updateNeeded = true;
         setByCurrentMaterial = true;
         setByCurrentMaterial = true;
     }
     }
 
 
     public void setVector4InArray(float x, float y, float z, float w, int index){
     public void setVector4InArray(float x, float y, float z, float w, int index){
-        if (location == -1)
+        if (location == -1) {
             return;
             return;
+        }
 
 
-        if (varType != null && varType != VarType.Vector4Array)
-            throw new IllegalArgumentException("Expected a "+varType.name()+" value!");
+        if (varType != null && varType != VarType.Vector4Array) {
+            throw new IllegalArgumentException("Expected a " + varType.name() + " value!");
+        }
 
 
-        FloatBuffer fb = (FloatBuffer) value;
-        fb.position(index * 4);
-        fb.put(x).put(y).put(z).put(w);
-        fb.rewind();
+        multiData.position(index * 4);
+        multiData.put(x).put(y).put(z).put(w);
+        multiData.rewind();
         updateNeeded = true;
         updateNeeded = true;
         setByCurrentMaterial = true;
         setByCurrentMaterial = true;
     }
     }

+ 3 - 2
jme3-core/src/main/java/com/jme3/shader/UniformBindingManager.java

@@ -36,7 +36,7 @@ import com.jme3.math.*;
 import com.jme3.renderer.Camera;
 import com.jme3.renderer.Camera;
 import com.jme3.renderer.RenderManager;
 import com.jme3.renderer.RenderManager;
 import com.jme3.system.Timer;
 import com.jme3.system.Timer;
-import java.util.List;
+import java.util.ArrayList;
 
 
 /**
 /**
  * <code>UniformBindingManager</code> helps {@link RenderManager} to manage
  * <code>UniformBindingManager</code> helps {@link RenderManager} to manage
@@ -84,7 +84,8 @@ public class UniformBindingManager {
      * Updates the given list of uniforms with {@link UniformBinding uniform bindings}
      * Updates the given list of uniforms with {@link UniformBinding uniform bindings}
      * based on the current world state.
      * based on the current world state.
      */
      */
-    public void updateUniformBindings(List<Uniform> params) {
+    public void updateUniformBindings(Shader shader) {
+        ArrayList<Uniform> params = shader.getBoundUniforms();
         for (int i = 0; i < params.size(); i++) {
         for (int i = 0; i < params.size(); i++) {
             Uniform u = params.get(i);
             Uniform u = params.get(i);
             switch (u.getBinding()) {
             switch (u.getBinding()) {

+ 1 - 1
jme3-core/src/main/java/com/jme3/shader/VarType.java

@@ -55,7 +55,7 @@ public enum VarType {
     TextureBuffer(false,true,"sampler1D|sampler1DShadow"),
     TextureBuffer(false,true,"sampler1D|sampler1DShadow"),
     Texture2D(false,true,"sampler2D|sampler2DShadow"),
     Texture2D(false,true,"sampler2D|sampler2DShadow"),
     Texture3D(false,true,"sampler3D"),
     Texture3D(false,true,"sampler3D"),
-    TextureArray(false,true,"sampler2DArray"),
+    TextureArray(false,true,"sampler2DArray|sampler2DArrayShadow"),
     TextureCubeMap(false,true,"samplerCube"),
     TextureCubeMap(false,true,"samplerCube"),
     Int("int");
     Int("int");
 
 

+ 4 - 3
jme3-core/src/main/java/com/jme3/system/NullContext.java

@@ -128,12 +128,13 @@ public class NullContext implements JmeContext, Runnable {
     public void run(){
     public void run(){
         initInThread();
         initInThread();
 
 
-        while (!needClose.get()){
+        do {
             listener.update();
             listener.update();
 
 
-            if (frameRate > 0)
+            if (frameRate > 0) {
                 sync(frameRate);
                 sync(frameRate);
-        }
+            }
+        } while (!needClose.get());
 
 
         deinitInThread();
         deinitInThread();
 
 

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

@@ -51,7 +51,7 @@ import com.jme3.texture.Texture;
 
 
 public class NullRenderer implements Renderer {
 public class NullRenderer implements Renderer {
 
 
-    private static final EnumSet<Caps> caps = EnumSet.noneOf(Caps.class);
+    private static final EnumSet<Caps> caps = EnumSet.allOf(Caps.class);
     private static final Statistics stats = new Statistics();
     private static final Statistics stats = new Statistics();
 
 
     public void initialize() {
     public void initialize() {

+ 56 - 0
jme3-core/src/main/java/com/jme3/texture/FrameBuffer.java

@@ -97,6 +97,7 @@ public class FrameBuffer extends NativeObject {
         int id = -1;
         int id = -1;
         int slot = SLOT_UNDEF;
         int slot = SLOT_UNDEF;
         int face = -1;
         int face = -1;
+        int layer = -1;
         
         
         /**
         /**
          * @return The image format of the render buffer.
          * @return The image format of the render buffer.
@@ -160,6 +161,10 @@ public class FrameBuffer extends NativeObject {
                 return "BufferTarget[format=" + format + "]";
                 return "BufferTarget[format=" + format + "]";
             }
             }
         }
         }
+
+        public int getLayer() {
+            return this.layer;
+        }
     }
     }
 
 
     /**
     /**
@@ -324,6 +329,19 @@ public class FrameBuffer extends NativeObject {
         addColorTexture(tex);
         addColorTexture(tex);
     }
     }
     
     
+    /**
+     * Set the color texture array to use for this framebuffer.
+     * This automatically clears all existing textures added previously
+     * with {@link FrameBuffer#addColorTexture } and adds this texture as the
+     * only target.
+     * 
+     * @param tex The color texture array to set.
+     */
+    public void setColorTexture(TextureArray tex, int layer){
+        clearColorTargets();
+        addColorTexture(tex, layer);
+    }
+    
     /**
     /**
      * Set the color texture to use for this framebuffer.
      * Set the color texture to use for this framebuffer.
      * This automatically clears all existing textures added previously
      * This automatically clears all existing textures added previously
@@ -369,6 +387,31 @@ public class FrameBuffer extends NativeObject {
         colorBufs.add(colorBuf);
         colorBufs.add(colorBuf);
     }
     }
     
     
+    /**
+     * Add a color texture array to use for this framebuffer.
+     * If MRT is enabled, then each subsequently added texture can be
+     * rendered to through a shader that writes to the array <code>gl_FragData</code>.
+     * If MRT is not enabled, then the index set with {@link FrameBuffer#setTargetIndex(int) }
+     * is rendered to by the shader.
+     * 
+     * @param tex The texture array to add.
+     */
+    public void addColorTexture(TextureArray tex, int layer) {
+        if (id != -1)
+            throw new UnsupportedOperationException("FrameBuffer already initialized.");
+
+        Image img = tex.getImage();
+        checkSetTexture(tex, false);
+
+        RenderBuffer colorBuf = new RenderBuffer();
+        colorBuf.slot = colorBufs.size();
+        colorBuf.tex = tex;
+        colorBuf.format = img.getFormat();
+        colorBuf.layer = layer;
+
+        colorBufs.add(colorBuf);
+    }
+    
      /**
      /**
      * Add a color texture to use for this framebuffer.
      * Add a color texture to use for this framebuffer.
      * If MRT is enabled, then each subsequently added texture can be
      * If MRT is enabled, then each subsequently added texture can be
@@ -412,7 +455,20 @@ public class FrameBuffer extends NativeObject {
         depthBuf.tex = tex;
         depthBuf.tex = tex;
         depthBuf.format = img.getFormat();
         depthBuf.format = img.getFormat();
     }
     }
+    public void setDepthTexture(TextureArray tex, int layer){
+        if (id != -1)
+            throw new UnsupportedOperationException("FrameBuffer already initialized.");
 
 
+        Image img = tex.getImage();
+        checkSetTexture(tex, true);
+        
+        depthBuf = new RenderBuffer();
+        depthBuf.slot = img.getFormat().isDepthStencilFormat() ?  SLOT_DEPTH_STENCIL : SLOT_DEPTH;
+        depthBuf.tex = tex;
+        depthBuf.format = img.getFormat();
+        depthBuf.layer = layer;
+    }
+    
     /**
     /**
      * @return The number of color buffers attached to this texture. 
      * @return The number of color buffers attached to this texture. 
      */
      */

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

@@ -32,19 +32,21 @@
 package com.jme3.util;
 package com.jme3.util;
 
 
 import com.jme3.util.IntMap.Entry;
 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.Iterator;
 import java.util.Map;
 import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.NoSuchElementException;
 
 
 /**
 /**
  * Similar to a {@link Map} except that ints are used as keys.
  * 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>
  * 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 Entry[] table;
     private final float loadFactor;
     private final float loadFactor;
     private int size, mask, capacity, threshold;
     private int size, mask, capacity, threshold;
@@ -93,6 +95,26 @@ public final class IntMap<T> implements Iterable<Entry<T>>, Cloneable {
         return null;
         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) {
     public boolean containsValue(Object value) {
         Entry[] table = this.table;
         Entry[] table = this.table;
         for (int i = table.length; i-- > 0;){
         for (int i = table.length; i-- > 0;){
@@ -228,7 +250,7 @@ public final class IntMap<T> implements Iterable<Entry<T>>, Cloneable {
             idx = 0;
             idx = 0;
             el = 0;
             el = 0;
         }
         }
-        
+
         public boolean hasNext() {
         public boolean hasNext() {
             return el < size;
             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.
                 // the entry was null. find another non-null entry.
                 cur = table[++idx];
                 cur = table[++idx];
             } while (cur == null);
             } while (cur == null);
-            
+
             Entry e = cur;
             Entry e = cur;
             cur = cur.next;
             cur = cur.next;
             el ++;
             el ++;
-            
+
             return e;
             return e;
         }
         }
 
 
         public void remove() {
         public void remove() {
         }
         }
-        
+
     }
     }
-    
-    public static final class Entry<T> implements Cloneable {
+
+    public static final class Entry<T> implements Cloneable, JmeCloneable {
 
 
         final int key;
         final int key;
         T value;
         T value;
@@ -303,5 +325,20 @@ public final class IntMap<T> implements Iterable<Entry<T>>, Cloneable {
             }
             }
             return null;
             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);
+        }
     }
     }
 }
 }

+ 91 - 73
jme3-core/src/main/java/com/jme3/util/SafeArrayList.java

@@ -43,7 +43,7 @@ import java.util.*;
  *  the list is changing.</p>
  *  the list is changing.</p>
  *
  *
  *  <p>All modifications, including set() operations will cause a copy of the
  *  <p>All modifications, including set() operations will cause a copy of the
- *  data to be created that replaces the old version.  Because this list is 
+ *  data to be created that replaces the old version.  Because this list is
  *  not designed for threading concurrency it further optimizes the "many modifications"
  *  not designed for threading concurrency it further optimizes the "many modifications"
  *  case by buffering them as a normal ArrayList until the next time the contents
  *  case by buffering them as a normal ArrayList until the next time the contents
  *  are accessed.</p>
  *  are accessed.</p>
@@ -63,16 +63,16 @@ import java.util.*;
  *  Even after ListIterator.remove() or Iterator.remove() is called, this change
  *  Even after ListIterator.remove() or Iterator.remove() is called, this change
  *  is not reflected in the iterator instance as it is still refering to its
  *  is not reflected in the iterator instance as it is still refering to its
  *  original snapshot.
  *  original snapshot.
- *  </ul>  
+ *  </ul>
  *
  *
  *  @version   $Revision$
  *  @version   $Revision$
  *  @author    Paul Speed
  *  @author    Paul Speed
  */
  */
-public class SafeArrayList<E> implements List<E> {
-    
+public class SafeArrayList<E> implements List<E>, Cloneable {
+
     // Implementing List directly to avoid accidentally acquiring
     // Implementing List directly to avoid accidentally acquiring
     // incorrect or non-optimal behavior from AbstractList.  For
     // incorrect or non-optimal behavior from AbstractList.  For
-    // example, the default iterator() method will not work for 
+    // example, the default iterator() method will not work for
     // this list.
     // this list.
 
 
     // Note: given the particular use-cases this was intended,
     // Note: given the particular use-cases this was intended,
@@ -81,30 +81,48 @@ public class SafeArrayList<E> implements List<E> {
     //       SafeArrayList-specific methods could then be exposed
     //       SafeArrayList-specific methods could then be exposed
     //       for the classes like Node and Spatial to use to manage
     //       for the classes like Node and Spatial to use to manage
     //       the list.  This was the callers couldn't remove a child
     //       the list.  This was the callers couldn't remove a child
-    //       without it being detached properly, for example.      
+    //       without it being detached properly, for example.
 
 
-    private Class<E> elementType; 
+    private Class<E> elementType;
     private List<E> buffer;
     private List<E> buffer;
     private E[] backingArray;
     private E[] backingArray;
     private int size = 0;
     private int size = 0;
- 
+
     public SafeArrayList(Class<E> elementType) {
     public SafeArrayList(Class<E> elementType) {
-        this.elementType = elementType;        
+        this.elementType = elementType;
     }
     }
-    
+
     public SafeArrayList(Class<E> elementType, Collection<? extends E> c) {
     public SafeArrayList(Class<E> elementType, Collection<? extends E> c) {
-        this.elementType = elementType;        
+        this.elementType = elementType;
         addAll(c);
         addAll(c);
     }
     }
 
 
+    public SafeArrayList<E> clone() {
+        try {
+            SafeArrayList<E> clone = (SafeArrayList<E>)super.clone();
+
+            // Clone whichever backing store is currently active
+            if( backingArray != null ) {
+                clone.backingArray = backingArray.clone();
+            }
+            if( buffer != null ) {
+                clone.buffer = (List<E>)((ArrayList<E>)buffer).clone();
+            }
+
+            return clone;
+        } catch( CloneNotSupportedException e ) {
+            throw new AssertionError();
+        }
+    }
+
     protected final <T> T[] createArray(Class<T> type, int size) {
     protected final <T> T[] createArray(Class<T> type, int size) {
-        return (T[])java.lang.reflect.Array.newInstance(type, size);               
+        return (T[])java.lang.reflect.Array.newInstance(type, size);
     }
     }
-    
+
     protected final E[] createArray(int size) {
     protected final E[] createArray(int size) {
-        return createArray(elementType, size); 
+        return createArray(elementType, size);
     }
     }
- 
+
     /**
     /**
      *  Returns a current snapshot of this List's backing array that
      *  Returns a current snapshot of this List's backing array that
      *  is guaranteed not to change through further List manipulation.
      *  is guaranteed not to change through further List manipulation.
@@ -114,10 +132,10 @@ public class SafeArrayList<E> implements List<E> {
     public final E[] getArray() {
     public final E[] getArray() {
         if( backingArray != null )
         if( backingArray != null )
             return backingArray;
             return backingArray;
-            
+
         if( buffer == null ) {
         if( buffer == null ) {
             backingArray = createArray(0);
             backingArray = createArray(0);
-        } else {            
+        } else {
             // Only keep the array or the buffer but never both at
             // Only keep the array or the buffer but never both at
             // the same time.  1) it saves space, 2) it keeps the rest
             // the same time.  1) it saves space, 2) it keeps the rest
             // of the code safer.
             // of the code safer.
@@ -126,35 +144,35 @@ public class SafeArrayList<E> implements List<E> {
         }
         }
         return backingArray;
         return backingArray;
     }
     }
- 
+
     protected final List<E> getBuffer() {
     protected final List<E> getBuffer() {
         if( buffer != null )
         if( buffer != null )
             return buffer;
             return buffer;
-            
+
         if( backingArray == null ) {
         if( backingArray == null ) {
             buffer = new ArrayList();
             buffer = new ArrayList();
-        } else {       
+        } else {
             // Only keep the array or the buffer but never both at
             // Only keep the array or the buffer but never both at
             // the same time.  1) it saves space, 2) it keeps the rest
             // the same time.  1) it saves space, 2) it keeps the rest
-            // of the code safer.            
+            // of the code safer.
             buffer = new ArrayList( Arrays.asList(backingArray) );
             buffer = new ArrayList( Arrays.asList(backingArray) );
             backingArray = null;
             backingArray = null;
         }
         }
         return buffer;
         return buffer;
     }
     }
-    
+
     public final int size() {
     public final int size() {
-        return size;            
+        return size;
     }
     }
-    
+
     public final boolean isEmpty() {
     public final boolean isEmpty() {
         return size == 0;
         return size == 0;
     }
     }
-    
+
     public boolean contains(Object o) {
     public boolean contains(Object o) {
         return indexOf(o) >= 0;
         return indexOf(o) >= 0;
     }
     }
-    
+
     public Iterator<E> iterator() {
     public Iterator<E> iterator() {
         return listIterator();
         return listIterator();
     }
     }
@@ -162,70 +180,70 @@ public class SafeArrayList<E> implements List<E> {
     public Object[] toArray() {
     public Object[] toArray() {
         return getArray();
         return getArray();
     }
     }
-    
+
     public <T> T[] toArray(T[] a) {
     public <T> T[] toArray(T[] a) {
- 
+
         E[] array = getArray();
         E[] array = getArray();
         if (a.length < array.length) {
         if (a.length < array.length) {
             return (T[])Arrays.copyOf(array, array.length, a.getClass());
             return (T[])Arrays.copyOf(array, array.length, a.getClass());
-        } 
- 
+        }
+
         System.arraycopy( array, 0, a, 0, array.length );
         System.arraycopy( array, 0, a, 0, array.length );
-        
+
         if (a.length > array.length) {
         if (a.length > array.length) {
             a[array.length] = null;
             a[array.length] = null;
         }
         }
-           
+
         return a;
         return a;
     }
     }
-    
+
     public boolean add(E e) {
     public boolean add(E e) {
         boolean result = getBuffer().add(e);
         boolean result = getBuffer().add(e);
         size = getBuffer().size();
         size = getBuffer().size();
         return result;
         return result;
     }
     }
-    
+
     public boolean remove(Object o) {
     public boolean remove(Object o) {
         boolean result = getBuffer().remove(o);
         boolean result = getBuffer().remove(o);
         size = getBuffer().size();
         size = getBuffer().size();
         return result;
         return result;
     }
     }
-    
+
     public boolean containsAll(Collection<?> c) {
     public boolean containsAll(Collection<?> c) {
         return Arrays.asList(getArray()).containsAll(c);
         return Arrays.asList(getArray()).containsAll(c);
     }
     }
-    
+
     public boolean addAll(Collection<? extends E> c) {
     public boolean addAll(Collection<? extends E> c) {
         boolean result = getBuffer().addAll(c);
         boolean result = getBuffer().addAll(c);
         size = getBuffer().size();
         size = getBuffer().size();
         return result;
         return result;
     }
     }
-    
+
     public boolean addAll(int index, Collection<? extends E> c) {
     public boolean addAll(int index, Collection<? extends E> c) {
         boolean result = getBuffer().addAll(index, c);
         boolean result = getBuffer().addAll(index, c);
         size = getBuffer().size();
         size = getBuffer().size();
         return result;
         return result;
     }
     }
-    
+
     public boolean removeAll(Collection<?> c) {
     public boolean removeAll(Collection<?> c) {
         boolean result = getBuffer().removeAll(c);
         boolean result = getBuffer().removeAll(c);
         size = getBuffer().size();
         size = getBuffer().size();
         return result;
         return result;
     }
     }
-    
+
     public boolean retainAll(Collection<?> c) {
     public boolean retainAll(Collection<?> c) {
         boolean result = getBuffer().retainAll(c);
         boolean result = getBuffer().retainAll(c);
         size = getBuffer().size();
         size = getBuffer().size();
         return result;
         return result;
     }
     }
-    
+
     public void clear() {
     public void clear() {
         getBuffer().clear();
         getBuffer().clear();
         size = 0;
         size = 0;
     }
     }
-    
+
     public boolean equals(Object o) {
     public boolean equals(Object o) {
-        if( o == this ) 
+        if( o == this )
             return true;
             return true;
         if( !(o instanceof List) ) //covers null too
         if( !(o instanceof List) ) //covers null too
             return false;
             return false;
@@ -240,9 +258,9 @@ public class SafeArrayList<E> implements List<E> {
             if( o1 == null || !o1.equals(o2) )
             if( o1 == null || !o1.equals(o2) )
                 return false;
                 return false;
         }
         }
-        return !(i1.hasNext() || !i2.hasNext());            
+        return !(i1.hasNext() || !i2.hasNext());
     }
     }
-    
+
     public int hashCode() {
     public int hashCode() {
         // Exactly the hash code described in the List interface, basically
         // Exactly the hash code described in the List interface, basically
         E[] array = getArray();
         E[] array = getArray();
@@ -252,30 +270,30 @@ public class SafeArrayList<E> implements List<E> {
         }
         }
         return result;
         return result;
     }
     }
-    
+
     public final E get(int index) {
     public final E get(int index) {
         if( backingArray != null )
         if( backingArray != null )
             return backingArray[index];
             return backingArray[index];
         if( buffer != null )
         if( buffer != null )
             return buffer.get(index);
             return buffer.get(index);
-        throw new IndexOutOfBoundsException( "Index:" + index + ", Size:0" );        
+        throw new IndexOutOfBoundsException( "Index:" + index + ", Size:0" );
     }
     }
-    
+
     public E set(int index, E element) {
     public E set(int index, E element) {
         return getBuffer().set(index, element);
         return getBuffer().set(index, element);
     }
     }
-    
+
     public void add(int index, E element) {
     public void add(int index, E element) {
         getBuffer().add(index, element);
         getBuffer().add(index, element);
         size = getBuffer().size();
         size = getBuffer().size();
     }
     }
-    
+
     public E remove(int index) {
     public E remove(int index) {
         E result = getBuffer().remove(index);
         E result = getBuffer().remove(index);
         size = getBuffer().size();
         size = getBuffer().size();
         return result;
         return result;
     }
     }
-    
+
     public int indexOf(Object o) {
     public int indexOf(Object o) {
         E[] array = getArray();
         E[] array = getArray();
         for( int i = 0; i < array.length; i++ ) {
         for( int i = 0; i < array.length; i++ ) {
@@ -289,7 +307,7 @@ public class SafeArrayList<E> implements List<E> {
         }
         }
         return -1;
         return -1;
     }
     }
-    
+
     public int lastIndexOf(Object o) {
     public int lastIndexOf(Object o) {
         E[] array = getArray();
         E[] array = getArray();
         for( int i = array.length - 1; i >= 0; i-- ) {
         for( int i = array.length - 1; i >= 0; i-- ) {
@@ -303,29 +321,29 @@ public class SafeArrayList<E> implements List<E> {
         }
         }
         return -1;
         return -1;
     }
     }
-    
+
     public ListIterator<E> listIterator() {
     public ListIterator<E> listIterator() {
         return new ArrayIterator<E>(getArray(), 0);
         return new ArrayIterator<E>(getArray(), 0);
     }
     }
-    
+
     public ListIterator<E> listIterator(int index) {
     public ListIterator<E> listIterator(int index) {
         return new ArrayIterator<E>(getArray(), index);
         return new ArrayIterator<E>(getArray(), index);
     }
     }
-    
+
     public List<E> subList(int fromIndex, int toIndex) {
     public List<E> subList(int fromIndex, int toIndex) {
-    
+
         // So far JME doesn't use subList that I can see so I'm nerfing it.
         // So far JME doesn't use subList that I can see so I'm nerfing it.
         List<E> raw =  Arrays.asList(getArray()).subList(fromIndex, toIndex);
         List<E> raw =  Arrays.asList(getArray()).subList(fromIndex, toIndex);
         return Collections.unmodifiableList(raw);
         return Collections.unmodifiableList(raw);
     }
     }
- 
+
     public String toString() {
     public String toString() {
- 
+
         E[] array = getArray();
         E[] array = getArray();
         if( array.length == 0 ) {
         if( array.length == 0 ) {
             return "[]";
             return "[]";
         }
         }
-        
+
         StringBuilder sb = new StringBuilder();
         StringBuilder sb = new StringBuilder();
         sb.append('[');
         sb.append('[');
         for( int i = 0; i < array.length; i++ ) {
         for( int i = 0; i < array.length; i++ ) {
@@ -337,63 +355,63 @@ public class SafeArrayList<E> implements List<E> {
         sb.append(']');
         sb.append(']');
         return sb.toString();
         return sb.toString();
     }
     }
- 
+
     protected class ArrayIterator<E> implements ListIterator<E> {
     protected class ArrayIterator<E> implements ListIterator<E> {
         private E[] array;
         private E[] array;
         private int next;
         private int next;
         private int lastReturned;
         private int lastReturned;
-        
+
         protected ArrayIterator( E[] array, int index ) {
         protected ArrayIterator( E[] array, int index ) {
             this.array = array;
             this.array = array;
             this.next = index;
             this.next = index;
             this.lastReturned = -1;
             this.lastReturned = -1;
         }
         }
-        
+
         public boolean hasNext() {
         public boolean hasNext() {
             return next != array.length;
             return next != array.length;
         }
         }
-        
+
         public E next() {
         public E next() {
             if( !hasNext() )
             if( !hasNext() )
                 throw new NoSuchElementException();
                 throw new NoSuchElementException();
             lastReturned = next++;
             lastReturned = next++;
             return array[lastReturned];
             return array[lastReturned];
         }
         }
-        
+
         public boolean hasPrevious() {
         public boolean hasPrevious() {
-            return next != 0;           
-        }        
-        
+            return next != 0;
+        }
+
         public E previous() {
         public E previous() {
             if( !hasPrevious() )
             if( !hasPrevious() )
                 throw new NoSuchElementException();
                 throw new NoSuchElementException();
             lastReturned = --next;
             lastReturned = --next;
             return array[lastReturned];
             return array[lastReturned];
         }
         }
-        
+
         public int nextIndex() {
         public int nextIndex() {
-            return next;       
+            return next;
         }
         }
-        
+
         public int previousIndex() {
         public int previousIndex() {
             return next - 1;
             return next - 1;
         }
         }
-        
+
         public void remove() {
         public void remove() {
             // This operation is not so easy to do but we will fake it.
             // This operation is not so easy to do but we will fake it.
             // The issue is that the backing list could be completely
             // The issue is that the backing list could be completely
             // different than the one this iterator is a snapshot of.
             // different than the one this iterator is a snapshot of.
-            // We'll just remove(element) which in most cases will be 
+            // We'll just remove(element) which in most cases will be
             // correct.  If the list had earlier .equals() equivalent
             // correct.  If the list had earlier .equals() equivalent
             // elements then we'll remove one of those instead.  Either
             // elements then we'll remove one of those instead.  Either
             // way, none of those changes are reflected in this iterator.
             // way, none of those changes are reflected in this iterator.
             SafeArrayList.this.remove( array[lastReturned] );
             SafeArrayList.this.remove( array[lastReturned] );
         }
         }
-        
+
         public void set(E e) {
         public void set(E e) {
             throw new UnsupportedOperationException();
             throw new UnsupportedOperationException();
         }
         }
-        
+
         public void add(E e) {
         public void add(E e) {
             throw new UnsupportedOperationException();
             throw new UnsupportedOperationException();
         }
         }

+ 141 - 74
jme3-core/src/main/java/com/jme3/util/clone/Cloner.java

@@ -37,6 +37,8 @@ import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Method;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.IdentityHashMap;
 import java.util.IdentityHashMap;
+import java.util.logging.Logger;
+import java.util.logging.Level;
 import java.util.Map;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentHashMap;
 
 
@@ -49,7 +51,7 @@ import java.util.concurrent.ConcurrentHashMap;
  *  <p>By default, objects that do not implement JmeCloneable will
  *  <p>By default, objects that do not implement JmeCloneable will
  *  be treated like normal Java Cloneable objects.  If the object does
  *  be treated like normal Java Cloneable objects.  If the object does
  *  not implement the JmeCloneable or the regular JDK Cloneable interfaces
  *  not implement the JmeCloneable or the regular JDK Cloneable interfaces
- *  AND has no special handling defined then an IllegalArgumentException 
+ *  AND has no special handling defined then an IllegalArgumentException
  *  will be thrown.</p>
  *  will be thrown.</p>
  *
  *
  *  <p>Enhanced object cloning is done in a two step process.  First,
  *  <p>Enhanced object cloning is done in a two step process.  First,
@@ -60,7 +62,7 @@ import java.util.concurrent.ConcurrentHashMap;
  *  can easily have a regular shallow clone implementation just like any
  *  can easily have a regular shallow clone implementation just like any
  *  normal Java objects.  Second, the deep cloning of fields happens after
  *  normal Java objects.  Second, the deep cloning of fields happens after
  *  creation wich means that the clone is available to future field cloning
  *  creation wich means that the clone is available to future field cloning
- *  to resolve circular references.</p> 
+ *  to resolve circular references.</p>
  *
  *
  *  <p>Similar to Java serialization, the handling of specific object
  *  <p>Similar to Java serialization, the handling of specific object
  *  types can be customized.  This allows certain objects to be cloned gracefully
  *  types can be customized.  This allows certain objects to be cloned gracefully
@@ -87,31 +89,33 @@ import java.util.concurrent.ConcurrentHashMap;
  *  Foo fooClone = cloner.clone(foo);
  *  Foo fooClone = cloner.clone(foo);
  *  cloner.clearIndex(); // prepare it for reuse
  *  cloner.clearIndex(); // prepare it for reuse
  *  Foo fooClone2 = cloner.clone(foo);
  *  Foo fooClone2 = cloner.clone(foo);
- * 
+ *
  *  // Example 2: using the utility method that self-instantiates a temporary cloner.
  *  // Example 2: using the utility method that self-instantiates a temporary cloner.
  *  Foo fooClone = Cloner.deepClone(foo);
  *  Foo fooClone = Cloner.deepClone(foo);
- *   
+ *
  *  </pre>
  *  </pre>
  *
  *
  *  @author    Paul Speed
  *  @author    Paul Speed
  */
  */
 public class Cloner {
 public class Cloner {
- 
+
+    static Logger log = Logger.getLogger(Cloner.class.getName());
+
     /**
     /**
      *  Keeps track of the objects that have been cloned so far.
      *  Keeps track of the objects that have been cloned so far.
-     */   
+     */
     private IdentityHashMap<Object, Object> index = new IdentityHashMap<Object, Object>();
     private IdentityHashMap<Object, Object> index = new IdentityHashMap<Object, Object>();
- 
+
     /**
     /**
      *  Custom functions for cloning objects.
      *  Custom functions for cloning objects.
      */
      */
     private Map<Class, CloneFunction> functions = new HashMap<Class, CloneFunction>();
     private Map<Class, CloneFunction> functions = new HashMap<Class, CloneFunction>();
- 
+
     /**
     /**
      *  Cache the clone methods once for all cloners.
      *  Cache the clone methods once for all cloners.
-     */   
+     */
     private static final Map<Class, Method> methodCache = new ConcurrentHashMap<>();
     private static final Map<Class, Method> methodCache = new ConcurrentHashMap<>();
- 
+
     /**
     /**
      *  Creates a new cloner with only default clone functions and an empty
      *  Creates a new cloner with only default clone functions and an empty
      *  object index.
      *  object index.
@@ -121,41 +125,41 @@ public class Cloner {
         ListCloneFunction listFunction = new ListCloneFunction();
         ListCloneFunction listFunction = new ListCloneFunction();
         functions.put(java.util.ArrayList.class, listFunction);
         functions.put(java.util.ArrayList.class, listFunction);
         functions.put(java.util.LinkedList.class, listFunction);
         functions.put(java.util.LinkedList.class, listFunction);
-        functions.put(java.util.concurrent.CopyOnWriteArrayList.class, listFunction); 
+        functions.put(java.util.concurrent.CopyOnWriteArrayList.class, listFunction);
         functions.put(java.util.Vector.class, listFunction);
         functions.put(java.util.Vector.class, listFunction);
         functions.put(java.util.Stack.class, listFunction);
         functions.put(java.util.Stack.class, listFunction);
         functions.put(com.jme3.util.SafeArrayList.class, listFunction);
         functions.put(com.jme3.util.SafeArrayList.class, listFunction);
     }
     }
- 
+
     /**
     /**
      *  Convenience utility function that creates a new Cloner, uses it to
      *  Convenience utility function that creates a new Cloner, uses it to
      *  deep clone the object, and then returns the result.
      *  deep clone the object, and then returns the result.
      */
      */
     public static <T> T deepClone( T object ) {
     public static <T> T deepClone( T object ) {
         return new Cloner().clone(object);
         return new Cloner().clone(object);
-    }     
- 
+    }
+
     /**
     /**
      *  Deeps clones the specified object, reusing previous clones when possible.
      *  Deeps clones the specified object, reusing previous clones when possible.
-     * 
+     *
      *  <p>Object cloning priority works as follows:</p>
      *  <p>Object cloning priority works as follows:</p>
      *  <ul>
      *  <ul>
      *  <li>If the object has already been cloned then its clone is returned.
      *  <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 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 
+     *  <li>If the object implements Cloneable then its clone() method is called, arrays are
      *      deep cloned with entries passing through clone().
      *      deep cloned with entries passing through clone().
      *  <li>If the object implements JmeCloneable then its cloneFields() method is called on the
      *  <li>If the object implements JmeCloneable then its cloneFields() method is called on the
      *      clone.
      *      clone.
-     *  <li>Else an IllegalArgumentException is thrown. 
+     *  <li>Else an IllegalArgumentException is thrown.
      *  </ul>
      *  </ul>
      *
      *
      *  Note: objects returned by this method may not have yet had their cloneField()
      *  Note: objects returned by this method may not have yet had their cloneField()
      *  method called.
      *  method called.
-     */   
+     */
     public <T> T clone( T object ) {
     public <T> T clone( T object ) {
         return clone(object, true);
         return clone(object, true);
     }
     }
- 
+
     /**
     /**
      *  Internal method to work around a Java generics typing issue by
      *  Internal method to work around a Java generics typing issue by
      *  isolating the 'bad' case into a method with suppressed warnings.
      *  isolating the 'bad' case into a method with suppressed warnings.
@@ -167,20 +171,20 @@ public class Cloner {
         // Wrapping it in a method at least isolates the warning suppression
         // Wrapping it in a method at least isolates the warning suppression
         return (Class<T>)object.getClass();
         return (Class<T>)object.getClass();
     }
     }
- 
+
     /**
     /**
      *  Deeps clones the specified object, reusing previous clones when possible.
      *  Deeps clones the specified object, reusing previous clones when possible.
-     * 
+     *
      *  <p>Object cloning priority works as follows:</p>
      *  <p>Object cloning priority works as follows:</p>
      *  <ul>
      *  <ul>
      *  <li>If the object has already been cloned then its clone is returned.
      *  <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 
+     *  <li>If useFunctions is true and there is a custom CloneFunction then it is
      *      called to clone the object.
      *      called to clone the object.
-     *  <li>If the object implements Cloneable then its clone() method is called, arrays are 
+     *  <li>If the object implements Cloneable then its clone() method is called, arrays are
      *      deep cloned with entries passing through clone().
      *      deep cloned with entries passing through clone().
      *  <li>If the object implements JmeCloneable then its cloneFields() method is called on the
      *  <li>If the object implements JmeCloneable then its cloneFields() method is called on the
      *      clone.
      *      clone.
-     *  <li>Else an IllegalArgumentException is thrown. 
+     *  <li>Else an IllegalArgumentException is thrown.
      *  </ul>
      *  </ul>
      *
      *
      *  <p>The abililty to selectively use clone functions is useful when
      *  <p>The abililty to selectively use clone functions is useful when
@@ -188,72 +192,97 @@ public class Cloner {
      *
      *
      *  Note: objects returned by this method may not have yet had their cloneField()
      *  Note: objects returned by this method may not have yet had their cloneField()
      *  method called.
      *  method called.
-     */   
+     */
     public <T> T clone( T object, boolean useFunctions ) {
     public <T> T clone( T object, boolean useFunctions ) {
+
         if( object == null ) {
         if( object == null ) {
             return null;
             return null;
         }
         }
-        Class<T> type = objectClass(object); 
-        
+
+        if( log.isLoggable(Level.FINER) ) {
+            log.finer("cloning:" + object.getClass() + "@" + System.identityHashCode(object));
+        }
+
+        Class<T> type = objectClass(object);
+
         // Check the index to see if we already have it
         // Check the index to see if we already have it
         Object clone = index.get(object);
         Object clone = index.get(object);
         if( clone != null ) {
         if( clone != null ) {
-            return type.cast(clone); 
+            if( log.isLoggable(Level.FINER) ) {
+                log.finer("cloned:" + object.getClass() + "@" + System.identityHashCode(object)
+                            + " as cached:" + clone.getClass() + "@" + System.identityHashCode(clone));
+            }
+            return type.cast(clone);
         }
         }
-        
+
         // See if there is a custom function... that trumps everything.
         // See if there is a custom function... that trumps everything.
-        CloneFunction<T> f = getCloneFunction(type); 
+        CloneFunction<T> f = getCloneFunction(type);
         if( f != null ) {
         if( f != null ) {
             T result = f.cloneObject(this, object);
             T result = f.cloneObject(this, object);
-            
+
             // Store the object in the identity map so that any circular references
             // Store the object in the identity map so that any circular references
-            // are resolvable. 
-            index.put(object, result); 
-            
+            // are resolvable.
+            index.put(object, result);
+
             // Now call the function again to deep clone the fields
             // Now call the function again to deep clone the fields
             f.cloneFields(this, result, object);
             f.cloneFields(this, result, object);
-            
-            return result;           
+
+            if( log.isLoggable(Level.FINER) ) {
+                if( result == null ) {
+                    log.finer("cloned:" + object.getClass() + "@" + System.identityHashCode(object)
+                                + " as transformed:null");
+                } else {
+                    log.finer("clone:" + object.getClass() + "@" + System.identityHashCode(object)
+                                + " as transformed:" + result.getClass() + "@" + System.identityHashCode(result));
+                }
+            }
+            return result;
         }
         }
- 
+
         if( object.getClass().isArray() ) {
         if( object.getClass().isArray() ) {
-            // Perform an array clone        
+            // Perform an array clone
             clone = arrayClone(object);
             clone = arrayClone(object);
-            
+
             // Array clone already indexes the clone
             // Array clone already indexes the clone
         } else if( object instanceof JmeCloneable ) {
         } else if( object instanceof JmeCloneable ) {
             // Use the two-step cloning semantics
             // Use the two-step cloning semantics
             clone = ((JmeCloneable)object).jmeClone();
             clone = ((JmeCloneable)object).jmeClone();
-            
+
             // Store the object in the identity map so that any circular references
             // Store the object in the identity map so that any circular references
             // are resolvable
             // are resolvable
-            index.put(object, clone); 
-            
+            index.put(object, clone);
+
             ((JmeCloneable)clone).cloneFields(this, object);
             ((JmeCloneable)clone).cloneFields(this, object);
         } else if( object instanceof Cloneable ) {
         } else if( object instanceof Cloneable ) {
-            
+
             // Perform a regular Java shallow clone
             // Perform a regular Java shallow clone
             try {
             try {
                 clone = javaClone(object);
                 clone = javaClone(object);
             } catch( CloneNotSupportedException e ) {
             } catch( CloneNotSupportedException e ) {
                 throw new IllegalArgumentException("Object is not cloneable, type:" + type, e);
                 throw new IllegalArgumentException("Object is not cloneable, type:" + type, e);
             }
             }
-            
+
             // Store the object in the identity map so that any circular references
             // Store the object in the identity map so that any circular references
             // are resolvable
             // are resolvable
-            index.put(object, clone); 
+            index.put(object, clone);
         } else {
         } else {
             throw new IllegalArgumentException("Object is not cloneable, type:" + type);
             throw new IllegalArgumentException("Object is not cloneable, type:" + type);
         }
         }
-        
+
+        if( log.isLoggable(Level.FINER) ) {
+            log.finer("cloned:" + object.getClass() + "@" + System.identityHashCode(object)
+                        + " as " + clone.getClass() + "@" + System.identityHashCode(clone));
+        }
         return type.cast(clone);
         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.
+     *  Sets a custom CloneFunction for implementations of the specified Java type.  Some
+     *  inheritance checks are made but no disambiguation is performed.
+     *  <p>Note: in the general case, it is better to register against specific classes and
+     *  not super-classes or super-interfaces unless you know specifically that they are cloneable.</p>
+     *  <p>By default ListCloneFunction is registered for ArrayList, LinkedList, CopyOnWriteArrayList,
+     *  Vector, Stack, and JME's SafeArrayList.</p>
      */
      */
     public <T> void setCloneFunction( Class<T> type, CloneFunction<T> function ) {
     public <T> void setCloneFunction( Class<T> type, CloneFunction<T> function ) {
         if( function == null ) {
         if( function == null ) {
@@ -262,24 +291,59 @@ public class Cloner {
             functions.put(type, function);
             functions.put(type, function);
         }
         }
     }
     }
- 
+
     /**
     /**
      *  Returns a previously registered clone function for the specified type or null
      *  Returns a previously registered clone function for the specified type or null
      *  if there is no custom clone function for the type.
      *  if there is no custom clone function for the type.
-     */ 
+     */
     @SuppressWarnings("unchecked")
     @SuppressWarnings("unchecked")
     public <T> CloneFunction<T> getCloneFunction( Class<T> type ) {
     public <T> CloneFunction<T> getCloneFunction( Class<T> type ) {
-        return (CloneFunction<T>)functions.get(type); 
-    } 
- 
+        CloneFunction<T> result = (CloneFunction<T>)functions.get(type);
+        if( result == null ) {
+            // Do a more exhaustive search
+            for( Map.Entry<Class, CloneFunction> e : functions.entrySet() ) {
+                if( e.getKey().isAssignableFrom(type) ) {
+                    result = e.getValue();
+                    break;
+                }
+            }
+            if( result != null ) {
+                // Cache it for later
+                functions.put(type, result);
+            }
+        }
+        return result;
+    }
+
+    /**
+     *  Forces an object to be added to the indexing cache such that attempts
+     *  to clone the 'original' will always result in the 'clone' being returned.
+     *  This can be used to stub out specific values from being cloned or to
+     *  force global shared instances to be used even if the object is cloneable
+     *  normally.
+     */
+    public <T> void setClonedValue( T original, T clone ) {
+        index.put(original, clone);
+    }
+
+    /**
+     *  Returns true if the specified object has already been cloned
+     *  by this cloner during this session.  Cloned objects are cached
+     *  for later use and it's sometimes convenient to know if some
+     *  objects have already been cloned.
+     */
+    public boolean isCloned( Object o ) {
+        return index.containsKey(o);
+    }
+
     /**
     /**
      *  Clears the object index allowing the cloner to be reused for a brand new
      *  Clears the object index allowing the cloner to be reused for a brand new
      *  cloning operation.
      *  cloning operation.
      */
      */
     public void clearIndex() {
     public void clearIndex() {
         index.clear();
         index.clear();
-    }     
- 
+    }
+
     /**
     /**
      *  Performs a raw shallow Java clone using reflection.  This call does NOT
      *  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
      *  check against the clone index and so will return new objects every time
@@ -287,51 +351,54 @@ public class Cloner {
      *  not ever, depending on the caller) get resolved.
      *  not ever, depending on the caller) get resolved.
      *
      *
      *  <p>This method is provided as a convenient way for CloneFunctions to call
      *  <p>This method is provided as a convenient way for CloneFunctions to call
-     *  clone() and objects without necessarily knowing their real type.</p>  
-     */   
+     *  clone() and objects without necessarily knowing their real type.</p>
+     */
     public <T> T javaClone( T object ) throws CloneNotSupportedException {
     public <T> T javaClone( T object ) throws CloneNotSupportedException {
+        if( object == null ) {
+            return null;
+        }
         Method m = methodCache.get(object.getClass());
         Method m = methodCache.get(object.getClass());
         if( m == null ) {
         if( m == null ) {
             try {
             try {
                 // Lookup the method and cache it
                 // Lookup the method and cache it
                 m = object.getClass().getMethod("clone");
                 m = object.getClass().getMethod("clone");
-            } catch( NoSuchMethodException e ) {            
+            } catch( NoSuchMethodException e ) {
                 throw new CloneNotSupportedException("No public clone method found for:" + object.getClass());
                 throw new CloneNotSupportedException("No public clone method found for:" + object.getClass());
             }
             }
             methodCache.put(object.getClass(), m);
             methodCache.put(object.getClass(), m);
-            
+
             // Note: yes we might cache the method twice... but so what?
             // Note: yes we might cache the method twice... but so what?
         }
         }
- 
+
         try {
         try {
-            Class<? extends T> type = objectClass(object);       
+            Class<? extends T> type = objectClass(object);
             return type.cast(m.invoke(object));
             return type.cast(m.invoke(object));
         } catch( IllegalAccessException | InvocationTargetException e ) {
         } catch( IllegalAccessException | InvocationTargetException e ) {
             throw new RuntimeException("Error cloning object of type:" + object.getClass(), e);
             throw new RuntimeException("Error cloning object of type:" + object.getClass(), e);
-        }          
+        }
     }
     }
-    
+
     /**
     /**
      *  Clones a primitive array by coping it and clones an object
      *  Clones a primitive array by coping it and clones an object
      *  array by coping it and then running each of its values through
      *  array by coping it and then running each of its values through
      *  Cloner.clone().
      *  Cloner.clone().
      */
      */
     protected <T> T arrayClone( T object ) {
     protected <T> T arrayClone( T object ) {
- 
+
         // Java doesn't support the cloning of arrays through reflection unless
         // Java doesn't support the cloning of arrays through reflection unless
         // you open access to Object's protected clone array... which requires
         // you open access to Object's protected clone array... which requires
         // elevated privileges.  So we will do a work-around that is slightly less
         // elevated privileges.  So we will do a work-around that is slightly less
         // elegant.
         // elegant.
         // This should be 100% allowed without a case but Java generics
         // This should be 100% allowed without a case but Java generics
         // is not that smart
         // is not that smart
-        Class<T> type = objectClass(object); 
+        Class<T> type = objectClass(object);
         Class elementType = type.getComponentType();
         Class elementType = type.getComponentType();
-        int size = Array.getLength(object); 
+        int size = Array.getLength(object);
         Object clone = Array.newInstance(elementType, size);
         Object clone = Array.newInstance(elementType, size);
- 
+
         // Store the clone for later lookups
         // Store the clone for later lookups
-        index.put(object, clone); 
-        
+        index.put(object, clone);
+
         if( elementType.isPrimitive() ) {
         if( elementType.isPrimitive() ) {
             // Then our job is a bit easier
             // Then our job is a bit easier
             System.arraycopy(object, 0, clone, 0, size);
             System.arraycopy(object, 0, clone, 0, size);
@@ -341,8 +408,8 @@ public class Cloner {
                 Object element = clone(Array.get(object, i));
                 Object element = clone(Array.get(object, i));
                 Array.set(clone, i, element);
                 Array.set(clone, i, element);
             }
             }
-        }           
-        
+        }
+
         return type.cast(clone);
         return type.cast(clone);
     }
     }
 }
 }

+ 6 - 13
jme3-core/src/main/resources/Common/MatDefs/Light/Lighting.j3md

@@ -114,10 +114,10 @@ MaterialDef Phong Lighting {
         //For instancing
         //For instancing
         Boolean UseInstancing
         Boolean UseInstancing
 
 
-        Boolean BackfaceShadows: false
+        Boolean BackfaceShadows : false
     }
     }
 
 
- Technique {
+    Technique {
         LightMode SinglePass
         LightMode SinglePass
         
         
         VertexShader GLSL100:   Common/MatDefs/Light/SPLighting.vert
         VertexShader GLSL100:   Common/MatDefs/Light/SPLighting.vert
@@ -149,7 +149,7 @@ MaterialDef Phong Lighting {
             SEPARATE_TEXCOORD : SeparateTexCoord
             SEPARATE_TEXCOORD : SeparateTexCoord
             DISCARD_ALPHA : AlphaDiscardThreshold
             DISCARD_ALPHA : AlphaDiscardThreshold
             USE_REFLECTION : EnvMap
             USE_REFLECTION : EnvMap
-            SPHERE_MAP : SphereMap  
+            SPHERE_MAP : EnvMapAsSphereMap  
             NUM_BONES : NumberOfBones                        
             NUM_BONES : NumberOfBones                        
             INSTANCING : UseInstancing
             INSTANCING : UseInstancing
         }
         }
@@ -188,7 +188,7 @@ MaterialDef Phong Lighting {
             SEPARATE_TEXCOORD : SeparateTexCoord
             SEPARATE_TEXCOORD : SeparateTexCoord
             DISCARD_ALPHA : AlphaDiscardThreshold
             DISCARD_ALPHA : AlphaDiscardThreshold
             USE_REFLECTION : EnvMap
             USE_REFLECTION : EnvMap
-            SPHERE_MAP : SphereMap  
+            SPHERE_MAP : EnvMapAsSphereMap  
             NUM_BONES : NumberOfBones                        
             NUM_BONES : NumberOfBones                        
             INSTANCING : UseInstancing
             INSTANCING : UseInstancing
         }
         }
@@ -209,7 +209,6 @@ MaterialDef Phong Lighting {
         }
         }
 
 
         Defines {
         Defines {
-            COLOR_MAP : ColorMap
             DISCARD_ALPHA : AlphaDiscardThreshold
             DISCARD_ALPHA : AlphaDiscardThreshold
             NUM_BONES : NumberOfBones
             NUM_BONES : NumberOfBones
             INSTANCING : UseInstancing
             INSTANCING : UseInstancing
@@ -234,8 +233,7 @@ MaterialDef Phong Lighting {
             HARDWARE_SHADOWS : HardwareShadows
             HARDWARE_SHADOWS : HardwareShadows
             FILTER_MODE : FilterMode
             FILTER_MODE : FilterMode
             PCFEDGE : PCFEdge
             PCFEDGE : PCFEdge
-            DISCARD_ALPHA : AlphaDiscardThreshold           
-            COLOR_MAP : ColorMap
+            DISCARD_ALPHA : AlphaDiscardThreshold
             SHADOWMAP_SIZE : ShadowMapSize
             SHADOWMAP_SIZE : ShadowMapSize
             FADE : FadeInfo
             FADE : FadeInfo
             PSSM : Splits
             PSSM : Splits
@@ -268,8 +266,7 @@ MaterialDef Phong Lighting {
             HARDWARE_SHADOWS : HardwareShadows
             HARDWARE_SHADOWS : HardwareShadows
             FILTER_MODE : FilterMode
             FILTER_MODE : FilterMode
             PCFEDGE : PCFEdge
             PCFEDGE : PCFEdge
-            DISCARD_ALPHA : AlphaDiscardThreshold           
-            COLOR_MAP : ColorMap
+            DISCARD_ALPHA : AlphaDiscardThreshold
             SHADOWMAP_SIZE : ShadowMapSize
             SHADOWMAP_SIZE : ShadowMapSize
             FADE : FadeInfo
             FADE : FadeInfo
             PSSM : Splits
             PSSM : Splits
@@ -343,10 +340,6 @@ MaterialDef Phong Lighting {
         Defines {
         Defines {
             VERTEX_COLOR : UseVertexColor
             VERTEX_COLOR : UseVertexColor
             MATERIAL_COLORS : UseMaterialColors
             MATERIAL_COLORS : UseMaterialColors
-            V_TANGENT : VTangent
-            MINNAERT  : Minnaert
-            WARDISO   : WardIso
-
             DIFFUSEMAP : DiffuseMap
             DIFFUSEMAP : DiffuseMap
             NORMALMAP : NormalMap
             NORMALMAP : NormalMap
             SPECULARMAP : SpecularMap
             SPECULARMAP : SpecularMap

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

@@ -41,8 +41,7 @@ varying vec3 SpecularSum;
   
   
 #ifdef NORMALMAP
 #ifdef NORMALMAP
   uniform sampler2D m_NormalMap;   
   uniform sampler2D m_NormalMap;   
-  varying vec3 vTangent;
-  varying vec3 vBinormal;
+  varying vec4 vTangent;
 #endif
 #endif
 varying vec3 vNormal;
 varying vec3 vNormal;
 
 
@@ -71,7 +70,7 @@ uniform float m_Shininess;
 void main(){
 void main(){
     #if !defined(VERTEX_LIGHTING)
     #if !defined(VERTEX_LIGHTING)
         #if defined(NORMALMAP)
         #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)
             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;
     varying vec3 vPos;
     #ifdef NORMALMAP
     #ifdef NORMALMAP
         attribute vec4 inTangent;
         attribute vec4 inTangent;
-        varying vec3 vTangent;
-        varying vec3 vBinormal;
+        varying vec4 vTangent;
     #endif
     #endif
 #else
 #else
     #ifdef COLORRAMP
     #ifdef COLORRAMP
@@ -104,8 +103,7 @@ void main(){
   
   
        
        
     #if defined(NORMALMAP) && !defined(VERTEX_LIGHTING)
     #if defined(NORMALMAP) && !defined(VERTEX_LIGHTING)
-      vTangent = TransformNormal(modelSpaceTan);
-      vBinormal = cross(wvNormal, vTangent)* inTangent.w;      
+      vTangent = vec4(TransformNormal(modelSpaceTan).xyz,inTangent.w);
       vNormal = wvNormal;         
       vNormal = wvNormal;         
       vPos = wvPosition;
       vPos = wvPosition;
     #elif !defined(VERTEX_LIGHTING)
     #elif !defined(VERTEX_LIGHTING)

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

@@ -25,9 +25,9 @@ uniform vec2 g_ResolutionInverse;
     uniform mat4 m_LightViewProjectionMatrix4;
     uniform mat4 m_LightViewProjectionMatrix4;
     uniform mat4 m_LightViewProjectionMatrix5;
     uniform mat4 m_LightViewProjectionMatrix5;
 #else
 #else
+    uniform vec3 m_LightDir;
     #ifndef PSSM    
     #ifndef PSSM    
-        uniform vec3 m_LightPos;    
-        uniform vec3 m_LightDir;       
+        uniform vec3 m_LightPos;
     #endif
     #endif
 #endif
 #endif
 
 

+ 1 - 0
jme3-core/src/main/resources/com/jme3/system/.gitignore

@@ -0,0 +1 @@
+version.properties 

+ 0 - 11
jme3-core/src/main/resources/com/jme3/system/version.properties

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

+ 63 - 5
jme3-core/src/plugins/java/com/jme3/material/plugins/J3MLoader.java

@@ -31,6 +31,9 @@
  */
  */
 package com.jme3.material.plugins;
 package com.jme3.material.plugins;
 
 
+import com.jme3.material.logic.MultiPassLightingLogic;
+import com.jme3.material.logic.SinglePassLightingLogic;
+import com.jme3.material.logic.DefaultTechniqueDefLogic;
 import com.jme3.asset.*;
 import com.jme3.asset.*;
 import com.jme3.material.*;
 import com.jme3.material.*;
 import com.jme3.material.RenderState.BlendMode;
 import com.jme3.material.RenderState.BlendMode;
@@ -40,6 +43,7 @@ import com.jme3.material.TechniqueDef.ShadowMode;
 import com.jme3.math.ColorRGBA;
 import com.jme3.math.ColorRGBA;
 import com.jme3.math.Vector2f;
 import com.jme3.math.Vector2f;
 import com.jme3.math.Vector3f;
 import com.jme3.math.Vector3f;
+import com.jme3.shader.DefineList;
 import com.jme3.shader.Shader;
 import com.jme3.shader.Shader;
 import com.jme3.shader.VarType;
 import com.jme3.shader.VarType;
 import com.jme3.texture.Texture;
 import com.jme3.texture.Texture;
@@ -73,6 +77,7 @@ public class J3MLoader implements AssetLoader {
     private Material material;
     private Material material;
     private TechniqueDef technique;
     private TechniqueDef technique;
     private RenderState renderState;
     private RenderState renderState;
+    private ArrayList<String> presetDefines = new ArrayList<String>();
 
 
     private EnumMap<Shader.ShaderType, String> shaderLanguage;
     private EnumMap<Shader.ShaderType, String> shaderLanguage;
     private EnumMap<Shader.ShaderType, String> shaderName;
     private EnumMap<Shader.ShaderType, String> shaderName;
@@ -115,6 +120,10 @@ public class J3MLoader implements AssetLoader {
             throw new IOException("LightMode statement syntax incorrect");
             throw new IOException("LightMode statement syntax incorrect");
         }
         }
         LightMode lm = LightMode.valueOf(split[1]);
         LightMode lm = LightMode.valueOf(split[1]);
+        if (lm == LightMode.FixedPipeline) {
+            throw new UnsupportedOperationException("OpenGL1 is not supported");
+        }
+        
         technique.setLightMode(lm);
         technique.setLightMode(lm);
     }
     }
 
 
@@ -458,6 +467,8 @@ public class J3MLoader implements AssetLoader {
             renderState.setDepthFunc(RenderState.TestFunction.valueOf(split[1]));
             renderState.setDepthFunc(RenderState.TestFunction.valueOf(split[1]));
         }else if (split[0].equals("AlphaFunc")){
         }else if (split[0].equals("AlphaFunc")){
             renderState.setAlphaFunc(RenderState.TestFunction.valueOf(split[1]));
             renderState.setAlphaFunc(RenderState.TestFunction.valueOf(split[1]));
+        }else if (split[0].equals("LineWidth")){
+            renderState.setLineWidth(Float.parseFloat(split[1]));
         } else {
         } else {
             throw new MatParseException(null, split[0], statement);
             throw new MatParseException(null, split[0], statement);
         }
         }
@@ -493,10 +504,22 @@ public class J3MLoader implements AssetLoader {
     private void readDefine(String statement) throws IOException{
     private void readDefine(String statement) throws IOException{
         String[] split = statement.split(":");
         String[] split = statement.split(":");
         if (split.length == 1){
         if (split.length == 1){
-            // add preset define
-            technique.addShaderPresetDefine(split[0].trim(), VarType.Boolean, true);
+            String defineName = split[0].trim();
+            presetDefines.add(defineName);
         }else if (split.length == 2){
         }else if (split.length == 2){
-            technique.addShaderParamDefine(split[1].trim(), split[0].trim());
+            String defineName = split[0].trim();
+            String paramName = split[1].trim();
+            MatParam param = materialDef.getMaterialParam(paramName);
+            if (param == null) {
+                logger.log(Level.WARNING, "In technique ''{0}'':\n"
+                        + "Define ''{1}'' mapped to non-existent"
+                        + " material parameter ''{2}'', ignoring.",
+                        new Object[]{technique.getName(), defineName, paramName});
+                return;
+            }
+            
+            VarType paramType = param.getVarType();
+            technique.addShaderParamDefine(paramName, paramType, defineName);
         }else{
         }else{
             throw new IOException("Define syntax incorrect");
             throw new IOException("Define syntax incorrect");
         }
         }
@@ -558,15 +581,28 @@ public class J3MLoader implements AssetLoader {
         }
         }
         material.setTransparent(parseBoolean(split[1]));
         material.setTransparent(parseBoolean(split[1]));
     }
     }
+    
+    private static String createShaderPrologue(List<String> presetDefines) {
+        DefineList dl = new DefineList(presetDefines.size());
+        for (int i = 0; i < presetDefines.size(); i++) {
+            dl.set(i, 1);
+        }
+        StringBuilder sb = new StringBuilder();
+        dl.generateSource(sb, presetDefines, null);
+        return sb.toString();
+    }
 
 
     private void readTechnique(Statement techStat) throws IOException{
     private void readTechnique(Statement techStat) throws IOException{
         isUseNodes = false;
         isUseNodes = false;
         String[] split = techStat.getLine().split(whitespacePattern);
         String[] split = techStat.getLine().split(whitespacePattern);
+        
         if (split.length == 1) {
         if (split.length == 1) {
-            technique = new TechniqueDef(null);
+            String techniqueUniqueName = materialDef.getAssetName() + "@Default";
+            technique = new TechniqueDef(null, techniqueUniqueName.hashCode());
         } else if (split.length == 2) {
         } else if (split.length == 2) {
             String techName = split[1];
             String techName = split[1];
-            technique = new TechniqueDef(techName);
+            String techniqueUniqueName = materialDef.getAssetName() + "@" + techName;
+            technique = new TechniqueDef(techName, techniqueUniqueName.hashCode());
         } else {
         } else {
             throw new IOException("Technique statement syntax incorrect");
             throw new IOException("Technique statement syntax incorrect");
         }
         }
@@ -577,18 +613,40 @@ public class J3MLoader implements AssetLoader {
 
 
         if(isUseNodes){
         if(isUseNodes){
             nodesLoaderDelegate.computeConditions();
             nodesLoaderDelegate.computeConditions();
+            
             //used for caching later, the shader here is not a file.
             //used for caching later, the shader here is not a file.
+            
+            // KIRILL 9/19/2015
+            // Not sure if this is needed anymore, since shader caching
+            // is now done by TechniqueDef.
             technique.setShaderFile(technique.hashCode() + "", technique.hashCode() + "", "GLSL100", "GLSL100");
             technique.setShaderFile(technique.hashCode() + "", technique.hashCode() + "", "GLSL100", "GLSL100");
         }
         }
 
 
         if (shaderName.containsKey(Shader.ShaderType.Vertex) && shaderName.containsKey(Shader.ShaderType.Fragment)) {
         if (shaderName.containsKey(Shader.ShaderType.Vertex) && shaderName.containsKey(Shader.ShaderType.Fragment)) {
             technique.setShaderFile(shaderName, shaderLanguage);
             technique.setShaderFile(shaderName, shaderLanguage);
         }
         }
+        
+        technique.setShaderPrologue(createShaderPrologue(presetDefines));
+        
+        switch (technique.getLightMode()) {
+            case Disable:
+                technique.setLogic(new DefaultTechniqueDefLogic(technique));
+                break;
+            case MultiPass:
+                technique.setLogic(new MultiPassLightingLogic(technique));
+                break;
+            case SinglePass:
+                technique.setLogic(new SinglePassLightingLogic(technique));
+                break;
+            default:
+                throw new UnsupportedOperationException();
+        }
 
 
         materialDef.addTechniqueDef(technique);
         materialDef.addTechniqueDef(technique);
         technique = null;
         technique = null;
         shaderLanguage.clear();
         shaderLanguage.clear();
         shaderName.clear();
         shaderName.clear();
+        presetDefines.clear();
     }
     }
 
 
     private void loadFromRoot(List<Statement> roots) throws IOException{
     private void loadFromRoot(List<Statement> roots) throws IOException{

+ 6 - 4
jme3-core/src/plugins/java/com/jme3/material/plugins/ShaderNodeLoaderDelegate.java

@@ -44,6 +44,7 @@ import com.jme3.shader.ShaderNodeDefinition;
 import com.jme3.shader.ShaderNodeVariable;
 import com.jme3.shader.ShaderNodeVariable;
 import com.jme3.shader.ShaderUtils;
 import com.jme3.shader.ShaderUtils;
 import com.jme3.shader.UniformBinding;
 import com.jme3.shader.UniformBinding;
+import com.jme3.shader.VarType;
 import com.jme3.shader.VariableMapping;
 import com.jme3.shader.VariableMapping;
 import com.jme3.util.blockparser.Statement;
 import com.jme3.util.blockparser.Statement;
 import java.io.IOException;
 import java.io.IOException;
@@ -583,7 +584,7 @@ public class ShaderNodeLoaderDelegate {
                     //multiplicity is not an int attempting to find for a material parameter.
                     //multiplicity is not an int attempting to find for a material parameter.
                     MatParam mp = findMatParam(multiplicity);
                     MatParam mp = findMatParam(multiplicity);
                     if (mp != null) {
                     if (mp != null) {
-                        addDefine(multiplicity);
+                        addDefine(multiplicity, VarType.Int);
                         multiplicity = multiplicity.toUpperCase();
                         multiplicity = multiplicity.toUpperCase();
                     } else {
                     } else {
                         throw new MatParseException("Wrong multiplicity for variable" + mapping.getLeftVariable().getName() + ". " + multiplicity + " should be an int or a declared material parameter.", statement);
                         throw new MatParseException("Wrong multiplicity for variable" + mapping.getLeftVariable().getName() + ". " + multiplicity + " should be an int or a declared material parameter.", statement);
@@ -625,9 +626,9 @@ public class ShaderNodeLoaderDelegate {
      *
      *
      * @param paramName
      * @param paramName
      */
      */
-    public void addDefine(String paramName) {
+    public void addDefine(String paramName, VarType paramType) {
         if (techniqueDef.getShaderParamDefine(paramName) == null) {
         if (techniqueDef.getShaderParamDefine(paramName) == null) {
-            techniqueDef.addShaderParamDefine(paramName, paramName.toUpperCase());
+            techniqueDef.addShaderParamDefine(paramName, paramType, paramName.toUpperCase());
         }
         }
     }
     }
 
 
@@ -660,7 +661,7 @@ public class ShaderNodeLoaderDelegate {
         for (String string : defines) {
         for (String string : defines) {
             MatParam param = findMatParam(string);
             MatParam param = findMatParam(string);
             if (param != null) {
             if (param != null) {
-                addDefine(param.getName());
+                addDefine(param.getName(), param.getVarType());
             } else {
             } else {
                 throw new MatParseException("Invalid condition, condition must match a Material Parameter named " + cond, statement);
                 throw new MatParseException("Invalid condition, condition must match a Material Parameter named " + cond, statement);
             }
             }
@@ -752,6 +753,7 @@ public class ShaderNodeLoaderDelegate {
             }
             }
             right.setNameSpace(node.getName());
             right.setNameSpace(node.getName());
             right.setType(var.getType());
             right.setType(var.getType());
+            right.setMultiplicity(var.getMultiplicity());
             mapping.setRightVariable(right);            
             mapping.setRightVariable(right);            
             storeVaryings(node, mapping.getRightVariable());
             storeVaryings(node, mapping.getRightVariable());
 
 

+ 52 - 0
jme3-core/src/test/java/com/jme3/asset/LoadShaderSourceTest.java

@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.asset;
+
+import com.jme3.asset.plugins.ClasspathLocator;
+import com.jme3.shader.plugins.GLSLLoader;
+import com.jme3.system.JmeSystem;
+import com.jme3.system.MockJmeSystemDelegate;
+import org.junit.Test;
+
+public class LoadShaderSourceTest {
+
+    @Test
+    public void testLoadShaderSource() {
+        JmeSystem.setSystemDelegate(new MockJmeSystemDelegate());
+        AssetManager assetManager = new DesktopAssetManager();
+        assetManager.registerLocator(null, ClasspathLocator.class);
+        assetManager.registerLoader(GLSLLoader.class, "frag");
+        String showNormals = (String) assetManager.loadAsset("Common/MatDefs/Misc/ShowNormals.frag");
+        System.out.println(showNormals);
+    }
+    
+}

+ 538 - 0
jme3-core/src/test/java/com/jme3/material/MaterialMatParamOverrideTest.java

@@ -0,0 +1,538 @@
+/*
+ * Copyright (c) 2009-2016 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.material;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.light.LightList;
+import com.jme3.math.Matrix4f;
+import com.jme3.renderer.RenderManager;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.shape.Box;
+import com.jme3.shader.Shader;
+import com.jme3.shader.Uniform;
+import com.jme3.shader.VarType;
+import java.util.Arrays;
+import java.util.HashSet;
+import org.junit.Assert;
+import org.junit.Test;
+
+import static com.jme3.scene.MPOTestUtils.*;
+import com.jme3.shader.DefineList;
+import com.jme3.system.NullRenderer;
+import com.jme3.system.TestUtil;
+import com.jme3.texture.Image.Format;
+import com.jme3.texture.Texture;
+import com.jme3.texture.Texture2D;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Validates how {@link MatParamOverride MPOs} work on the material level.
+ *
+ * @author Kirill Vainer
+ */
+public class MaterialMatParamOverrideTest {
+
+    private static final HashSet<String> IGNORED_UNIFORMS = new HashSet<String>(
+            Arrays.asList(new String[]{"m_ParallaxHeight", "m_Shininess", "m_BackfaceShadows"}));
+
+    @Test
+    public void testBoolMpoOnly() {
+        material("Common/MatDefs/Light/Lighting.j3md");
+        inputMpo(mpoBool("UseMaterialColors", true));
+        outDefines(def("MATERIAL_COLORS", VarType.Boolean, true));
+        outUniforms(uniform("UseMaterialColors", VarType.Boolean, true));
+    }
+
+    @Test
+    public void testBoolMpOnly() {
+        material("Common/MatDefs/Light/Lighting.j3md");
+        inputMp(mpoBool("UseMaterialColors", true));
+        outDefines(def("MATERIAL_COLORS", VarType.Boolean, true));
+        outUniforms(uniform("UseMaterialColors", VarType.Boolean, true));
+    }
+
+    @Test
+    public void testBoolOverrideFalse() {
+        material("Common/MatDefs/Light/Lighting.j3md");
+        inputMp(mpoBool("UseMaterialColors", true));
+        inputMpo(mpoBool("UseMaterialColors", false));
+        outDefines();
+        outUniforms(uniform("UseMaterialColors", VarType.Boolean, false));
+    }
+
+    @Test
+    public void testBoolOverrideTrue() {
+        material("Common/MatDefs/Light/Lighting.j3md");
+        inputMp(mpoBool("UseMaterialColors", false));
+        inputMpo(mpoBool("UseMaterialColors", true));
+        outDefines(def("MATERIAL_COLORS", VarType.Boolean, true));
+        outUniforms(uniform("UseMaterialColors", VarType.Boolean, true));
+    }
+
+    @Test
+    public void testFloatMpoOnly() {
+        material("Common/MatDefs/Light/Lighting.j3md");
+        inputMpo(mpoFloat("AlphaDiscardThreshold", 3.12f));
+        outDefines(def("DISCARD_ALPHA", VarType.Float, 3.12f));
+        outUniforms(uniform("AlphaDiscardThreshold", VarType.Float, 3.12f));
+    }
+
+    @Test
+    public void testFloatMpOnly() {
+        material("Common/MatDefs/Light/Lighting.j3md");
+        inputMp(mpoFloat("AlphaDiscardThreshold", 3.12f));
+        outDefines(def("DISCARD_ALPHA", VarType.Float, 3.12f));
+        outUniforms(uniform("AlphaDiscardThreshold", VarType.Float, 3.12f));
+    }
+
+    @Test
+    public void testFloatOverride() {
+        material("Common/MatDefs/Light/Lighting.j3md");
+        inputMp(mpoFloat("AlphaDiscardThreshold", 3.12f));
+        inputMpo(mpoFloat("AlphaDiscardThreshold", 2.79f));
+        outDefines(def("DISCARD_ALPHA", VarType.Float, 2.79f));
+        outUniforms(uniform("AlphaDiscardThreshold", VarType.Float, 2.79f));
+    }
+
+    @Test
+    public void testMpoDisable() {
+        material("Common/MatDefs/Light/Lighting.j3md");
+        inputMp(mpoFloat("AlphaDiscardThreshold", 3.12f));
+
+        MatParamOverride override = mpoFloat("AlphaDiscardThreshold", 2.79f);
+        inputMpo(override);
+        outDefines(def("DISCARD_ALPHA", VarType.Float, 2.79f));
+        outUniforms(uniform("AlphaDiscardThreshold", VarType.Float, 2.79f));
+
+        reset();
+        override.setEnabled(false);
+        outDefines(def("DISCARD_ALPHA", VarType.Float, 3.12f));
+        outUniforms(uniform("AlphaDiscardThreshold", VarType.Float, 3.12f));
+
+        reset();
+        override.setEnabled(true);
+        outDefines(def("DISCARD_ALPHA", VarType.Float, 2.79f));
+        outUniforms(uniform("AlphaDiscardThreshold", VarType.Float, 2.79f));
+    }
+
+    @Test
+    public void testIntMpoOnly() {
+        material("Common/MatDefs/Light/Lighting.j3md");
+        inputMpo(mpoInt("NumberOfBones", 1234));
+        outDefines(def("NUM_BONES", VarType.Int, 1234));
+        outUniforms(uniform("NumberOfBones", VarType.Int, 1234));
+    }
+
+    @Test
+    public void testIntMpOnly() {
+        material("Common/MatDefs/Light/Lighting.j3md");
+        inputMp(mpoInt("NumberOfBones", 1234));
+        outDefines(def("NUM_BONES", VarType.Int, 1234));
+        outUniforms(uniform("NumberOfBones", VarType.Int, 1234));
+    }
+
+    @Test
+    public void testIntOverride() {
+        material("Common/MatDefs/Light/Lighting.j3md");
+        inputMp(mpoInt("NumberOfBones", 1234));
+        inputMpo(mpoInt("NumberOfBones", 4321));
+        outDefines(def("NUM_BONES", VarType.Int, 4321));
+        outUniforms(uniform("NumberOfBones", VarType.Int, 4321));
+    }
+
+    @Test
+    public void testMatrixArray() {
+        Matrix4f[] matrices = new Matrix4f[]{
+            new Matrix4f()
+        };
+
+        material("Common/MatDefs/Light/Lighting.j3md");
+        inputMpo(mpoMatrix4Array("BoneMatrices", matrices));
+        outDefines();
+        outUniforms(uniform("BoneMatrices", VarType.Matrix4Array, matrices));
+    }
+
+    @Test
+    public void testNonExistentParameter() {
+        material("Common/MatDefs/Light/Lighting.j3md");
+        inputMpo(mpoInt("NonExistent", 4321));
+        outDefines();
+        outUniforms();
+    }
+
+    @Test
+    public void testWrongType() {
+        material("Common/MatDefs/Light/Lighting.j3md");
+        inputMpo(mpoInt("UseMaterialColors", 4321));
+        outDefines();
+        outUniforms();
+    }
+
+    @Test
+    public void testParamOnly() {
+        material("Common/MatDefs/Light/Lighting.j3md");
+        inputMpo(mpoFloat("ShadowMapSize", 3.12f));
+        outDefines();
+        outUniforms(uniform("ShadowMapSize", VarType.Float, 3.12f));
+    }
+
+    @Test
+    public void testRemove() {
+        material("Common/MatDefs/Light/Lighting.j3md");
+
+        reset();
+        inputMp(mpoInt("NumberOfBones", 1234));
+        outDefines(def("NUM_BONES", VarType.Int, 1234));
+        outUniforms(uniform("NumberOfBones", VarType.Int, 1234));
+
+        reset();
+        inputMpo(mpoInt("NumberOfBones", 4321));
+        outDefines(def("NUM_BONES", VarType.Int, 4321));
+        outUniforms(uniform("NumberOfBones", VarType.Int, 4321));
+
+        reset();
+        geometry.clearMatParamOverrides();
+        geometry.updateGeometricState();
+        outDefines(def("NUM_BONES", VarType.Int, 1234));
+        outUniforms(uniform("NumberOfBones", VarType.Int, 1234));
+
+        reset();
+        geometry.getMaterial().clearParam("NumberOfBones");
+        outDefines();
+        outUniforms();
+
+        reset();
+        inputMpo(mpoInt("NumberOfBones", 4321));
+        outDefines(def("NUM_BONES", VarType.Int, 4321));
+        outUniforms(uniform("NumberOfBones", VarType.Int, 4321));
+
+        reset();
+        inputMp(mpoInt("NumberOfBones", 1234));
+        outDefines(def("NUM_BONES", VarType.Int, 4321));
+        outUniforms(uniform("NumberOfBones", VarType.Int, 4321));
+    }
+
+    public void testRemoveOverride() {
+        material("Common/MatDefs/Light/Lighting.j3md");
+
+        reset();
+        inputMp(mpoInt("NumberOfBones", 1234));
+        outDefines(def("NUM_BONES", VarType.Int, 1234));
+        outUniforms(uniform("NumberOfBones", VarType.Int, 1234));
+
+        reset();
+        inputMpo(mpoInt("NumberOfBones", 4321));
+        outDefines(def("NUM_BONES", VarType.Int, 4321));
+        outUniforms(uniform("NumberOfBones", VarType.Int, 4321));
+
+        reset();
+        geometry.clearMatParamOverrides();
+        outDefines(def("NUM_BONES", VarType.Int, 1234));
+        outUniforms(uniform("NumberOfBones", VarType.Int, 1234));
+    }
+
+    @Test
+    public void testRemoveMpoOnly() {
+        material("Common/MatDefs/Light/Lighting.j3md");
+
+        reset();
+        inputMpo(mpoInt("NumberOfBones", 4321));
+        outDefines(def("NUM_BONES", VarType.Int, 4321));
+        outUniforms(uniform("NumberOfBones", VarType.Int, 4321));
+
+        reset();
+        geometry.clearMatParamOverrides();
+        geometry.updateGeometricState();
+        outDefines();
+        outUniforms();
+    }
+
+    @Test
+    public void testTextureMpoOnly() {
+        material("Common/MatDefs/Light/Lighting.j3md");
+        Texture2D tex = new Texture2D(128, 128, Format.RGBA8);
+
+        inputMpo(mpoTexture2D("DiffuseMap", tex));
+        outDefines(def("DIFFUSEMAP", VarType.Texture2D, tex));
+        outUniforms(uniform("DiffuseMap", VarType.Int, 0));
+        outTextures(tex);
+    }
+
+    @Test
+    public void testTextureOverride() {
+        material("Common/MatDefs/Light/Lighting.j3md");
+        Texture2D tex1 = new Texture2D(128, 128, Format.RGBA8);
+        Texture2D tex2 = new Texture2D(128, 128, Format.RGBA8);
+
+        inputMp(mpoTexture2D("DiffuseMap", tex1));
+        inputMpo(mpoTexture2D("DiffuseMap", tex2));
+
+        outDefines(def("DIFFUSEMAP", VarType.Texture2D, tex2));
+        outUniforms(uniform("DiffuseMap", VarType.Int, 0));
+        outTextures(tex2);
+    }
+
+    @Test
+    public void testRemoveTexture() {
+        material("Common/MatDefs/Light/Lighting.j3md");
+        Texture2D tex = new Texture2D(128, 128, Format.RGBA8);
+
+        reset();
+        inputMpo(mpoTexture2D("DiffuseMap", tex));
+        outDefines(def("DIFFUSEMAP", VarType.Texture2D, tex));
+        outUniforms(uniform("DiffuseMap", VarType.Int, 0));
+        outTextures(tex);
+
+        reset();
+        geometry.clearMatParamOverrides();
+        geometry.updateGeometricState();
+        outDefines();
+        outUniforms();
+        outTextures();
+    }
+
+    @Test
+    public void testRemoveTextureOverride() {
+        material("Common/MatDefs/Light/Lighting.j3md");
+        Texture2D tex1 = new Texture2D(128, 128, Format.RGBA8);
+        Texture2D tex2 = new Texture2D(128, 128, Format.RGBA8);
+
+        reset();
+        inputMp(mpoTexture2D("DiffuseMap", tex1));
+        outDefines(def("DIFFUSEMAP", VarType.Texture2D, tex1));
+        outUniforms(uniform("DiffuseMap", VarType.Int, 0));
+        outTextures(tex1);
+
+        reset();
+        inputMpo(mpoTexture2D("DiffuseMap", tex2));
+        outDefines(def("DIFFUSEMAP", VarType.Texture2D, tex2));
+        outUniforms(uniform("DiffuseMap", VarType.Int, 0));
+        outTextures(tex2);
+
+        reset();
+        geometry.clearMatParamOverrides();
+        geometry.updateGeometricState();
+        outDefines(def("DIFFUSEMAP", VarType.Texture2D, tex1));
+        outUniforms(uniform("DiffuseMap", VarType.Int, 0));
+        outTextures(tex1);
+    }
+
+    private static final class Define {
+
+        public String name;
+        public VarType type;
+        public Object value;
+
+        @Override
+        public int hashCode() {
+            int hash = 3;
+            hash = 89 * hash + this.name.hashCode();
+            hash = 89 * hash + this.type.hashCode();
+            hash = 89 * hash + this.value.hashCode();
+            return hash;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            final Define other = (Define) obj;
+            return this.name.equals(other.name) && this.type.equals(other.type) && this.value.equals(other.value);
+        }
+    }
+
+    private final Geometry geometry = new Geometry("geometry", new Box(1, 1, 1));
+    private final LightList lightList = new LightList(geometry);
+
+    private final NullRenderer renderer = new NullRenderer() {
+        @Override
+        public void setShader(Shader shader) {
+            MaterialMatParamOverrideTest.this.usedShader = shader;
+            evaluated = true;
+        }
+
+        @Override
+        public void setTexture(int unit, Texture texture) {
+            MaterialMatParamOverrideTest.this.usedTextures[unit] = texture;
+        }
+    };
+    private final RenderManager renderManager = new RenderManager(renderer);
+
+    private boolean evaluated = false;
+    private Shader usedShader = null;
+    private final Texture[] usedTextures = new Texture[32];
+
+    private void inputMp(MatParam... params) {
+        if (evaluated) {
+            throw new IllegalStateException();
+        }
+        Material mat = geometry.getMaterial();
+        for (MatParam param : params) {
+            mat.setParam(param.getName(), param.getVarType(), param.getValue());
+        }
+    }
+
+    private void inputMpo(MatParamOverride... overrides) {
+        if (evaluated) {
+            throw new IllegalStateException();
+        }
+        for (MatParamOverride override : overrides) {
+            geometry.addMatParamOverride(override);
+        }
+        geometry.updateGeometricState();
+    }
+
+    private void reset() {
+        evaluated = false;
+        usedShader = null;
+        Arrays.fill(usedTextures, null);
+    }
+
+    private Define def(String name, VarType type, Object value) {
+        Define d = new Define();
+        d.name = name;
+        d.type = type;
+        d.value = value;
+        return d;
+    }
+
+    private Uniform uniform(String name, VarType type, Object value) {
+        Uniform u = new Uniform();
+        u.setName("m_" + name);
+        u.setValue(type, value);
+        return u;
+    }
+
+    private void material(String path) {
+        AssetManager assetManager = TestUtil.createAssetManager();
+        geometry.setMaterial(new Material(assetManager, path));
+    }
+
+    private void evaluateTechniqueDef() {
+        Assert.assertFalse(evaluated);
+        Material mat = geometry.getMaterial();
+        mat.render(geometry, lightList, renderManager);
+        Assert.assertTrue(evaluated);
+    }
+
+    private void outTextures(Texture... textures) {
+        for (int i = 0; i < usedTextures.length; i++) {
+            if (i < textures.length) {
+                Assert.assertSame(textures[i], usedTextures[i]);
+            } else {
+                Assert.assertNull(usedTextures[i]);
+            }
+        }
+    }
+
+    private void outDefines(Define... expectedDefinesArray) {
+        Map<String, Define> nameToDefineMap = new HashMap<String, Define>();
+        for (Define define : expectedDefinesArray) {
+            nameToDefineMap.put(define.name, define);
+        }
+
+        if (!evaluated) {
+            evaluateTechniqueDef();
+        }
+
+        Material mat = geometry.getMaterial();
+        Technique tech = mat.getActiveTechnique();
+        TechniqueDef def = tech.getDef();
+        DefineList actualDefines = tech.getDynamicDefines();
+
+        String[] defineNames = def.getDefineNames();
+        VarType[] defineTypes = def.getDefineTypes();
+
+        Assert.assertEquals(defineNames.length, defineTypes.length);
+
+        for (int index = 0; index < defineNames.length; index++) {
+            String name = defineNames[index];
+            VarType type = defineTypes[index];
+            Define expectedDefine = nameToDefineMap.remove(name);
+            Object expectedValue = null;
+
+            if (expectedDefine != null) {
+                Assert.assertEquals(expectedDefine.type, type);
+                expectedValue = expectedDefine.value;
+            }
+
+            switch (type) {
+                case Boolean:
+                    if (expectedValue != null) {
+                        Assert.assertEquals((boolean) (Boolean) expectedValue, actualDefines.getBoolean(index));
+                    } else {
+                        Assert.assertEquals(false, actualDefines.getBoolean(index));
+                    }
+                    break;
+                case Int:
+                    if (expectedValue != null) {
+                        Assert.assertEquals((int) (Integer) expectedValue, actualDefines.getInt(index));
+                    } else {
+                        Assert.assertEquals(0, actualDefines.getInt(index));
+                    }
+                    break;
+                case Float:
+                    if (expectedValue != null) {
+                        Assert.assertEquals((float) (Float) expectedValue, actualDefines.getFloat(index), 0f);
+                    } else {
+                        Assert.assertEquals(0f, actualDefines.getFloat(index), 0f);
+                    }
+                    break;
+                default:
+                    if (expectedValue != null) {
+                        Assert.assertEquals(1, actualDefines.getInt(index));
+                    } else {
+                        Assert.assertEquals(0, actualDefines.getInt(index));
+                    }
+                    break;
+            }
+        }
+
+        Assert.assertTrue(nameToDefineMap.isEmpty());
+    }
+
+    private void outUniforms(Uniform... uniforms) {
+        HashSet<Uniform> actualUniforms = new HashSet<>();
+
+        for (Uniform uniform : usedShader.getUniformMap().values()) {
+            if (uniform.getName().startsWith("m_")
+                    && !IGNORED_UNIFORMS.contains(uniform.getName())) {
+                actualUniforms.add(uniform);
+            }
+        }
+
+        HashSet<Uniform> expectedUniforms = new HashSet<>(Arrays.asList(uniforms));
+
+        if (!expectedUniforms.equals(actualUniforms)) {
+            Assert.fail("Uniform lists must match: " + expectedUniforms + " != " + actualUniforms);
+        }
+    }
+}

+ 38 - 0
jme3-core/src/test/java/com/jme3/math/FastMathTest.java

@@ -33,6 +33,9 @@ package com.jme3.math;
 
 
 import org.junit.Test;
 import org.junit.Test;
 
 
+import static org.junit.Assert.assertEquals;
+import org.junit.Ignore;
+
 /**
 /**
  * Verifies that algorithms in {@link FastMath} are working correctly.
  * Verifies that algorithms in {@link FastMath} are working correctly.
  * 
  * 
@@ -56,4 +59,39 @@ public class FastMathTest {
             assert nextPowerOf2 == nearestPowerOfTwoSlow(i);
             assert nextPowerOf2 == nearestPowerOfTwoSlow(i);
         }
         }
     }
     }
+    
+    private static int fastCounterClockwise(Vector2f p0, Vector2f p1, Vector2f p2) {
+        float result = (p1.x - p0.x) * (p2.y - p1.y) - (p1.y - p0.y) * (p2.x - p1.x);
+        return (int) Math.signum(result);
+    }
+    
+    private static Vector2f randomVector() {
+        return new Vector2f(FastMath.nextRandomFloat(),
+                            FastMath.nextRandomFloat());
+    }
+    
+    @Ignore
+    @Test
+    public void testCounterClockwise() {
+        for (int i = 0; i < 100; i++) {
+            Vector2f p0 = randomVector();
+            Vector2f p1 = randomVector();
+            Vector2f p2 = randomVector();
+
+            int fastResult = fastCounterClockwise(p0, p1, p2);
+            int slowResult = FastMath.counterClockwise(p0, p1, p2);
+            
+            assert fastResult == slowResult;
+        }
+        
+        // duplicate test
+        Vector2f p0 = new Vector2f(0,0);
+        Vector2f p1 = new Vector2f(0,0);
+        Vector2f p2 = new Vector2f(0,1);
+        
+        int fastResult = fastCounterClockwise(p0, p1, p2);
+        int slowResult = FastMath.counterClockwise(p0, p1, p2);
+        
+        assertEquals(slowResult, fastResult);
+    }
 }
 }

+ 342 - 0
jme3-core/src/test/java/com/jme3/renderer/OpaqueComparatorTest.java

@@ -0,0 +1,342 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.renderer;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.renderer.queue.GeometryList;
+import com.jme3.renderer.queue.OpaqueComparator;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.shape.Box;
+import com.jme3.system.TestUtil;
+import com.jme3.texture.Image;
+import com.jme3.texture.Image.Format;
+import com.jme3.texture.Texture;
+import com.jme3.texture.Texture2D;
+import com.jme3.texture.image.ColorSpace;
+import com.jme3.util.BufferUtils;
+import java.nio.ByteBuffer;
+import java.util.HashSet;
+import java.util.Set;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+public class OpaqueComparatorTest {
+    
+    private final Mesh mesh = new Box(1,1,1);
+    private Camera cam = new Camera(1, 1);
+    private RenderManager renderManager;
+    private AssetManager assetManager;
+    private OpaqueComparator comparator = new OpaqueComparator();
+    
+    @Before
+    public void setUp() {
+        assetManager = TestUtil.createAssetManager();
+        renderManager = TestUtil.createRenderManager();
+        comparator.setCamera(cam);
+    }
+    
+    /**
+     * Given a correctly sorted list of materials, check if the 
+     * opaque comparator can sort a reversed list of them.
+     * 
+     * Each material will be cloned so that none of them will be equal to each other
+     * in reference, forcing the comparator to compare the material sort ID.
+     * 
+     * E.g. for a list of materials A, B, C, the following list will be generated:
+     * <pre>C, B, A, C, B, A, C, B, A</pre>, it should result in
+     * <pre>A, A, A, B, B, B, C, C, C</pre>.
+     * 
+     * @param materials The pre-sorted list of materials to check sorting for.
+     */
+    private void testSort(Material ... materials) {
+        GeometryList gl = new GeometryList(comparator);
+        for (int g = 0; g < 5; g++) {
+            for (int i = materials.length - 1; i >= 0; i--) {
+                Geometry geom = new Geometry("geom", mesh);
+                Material clonedMaterial = materials[i].clone();
+                
+                if (materials[i].getActiveTechnique() != null) {
+                    String techniqueName = materials[i].getActiveTechnique().getDef().getName();
+                    clonedMaterial.selectTechnique(techniqueName, renderManager);
+                } else {
+                    clonedMaterial.selectTechnique("Default", renderManager);
+                }
+                
+                geom.setMaterial(clonedMaterial);
+                gl.add(geom);
+            }
+        }
+        gl.sort();
+        
+        for (int i = 0; i < gl.size(); i++) {
+            Material mat = gl.get(i).getMaterial();
+            String sortId = Integer.toHexString(mat.getSortId()).toUpperCase();
+            System.out.print(sortId + "\t");
+            System.out.println(mat);
+        }
+        
+        Set<String> alreadySeen = new HashSet<String>();
+        Material current = null;
+        for (int i = 0; i < gl.size(); i++) {
+            Material mat = gl.get(i).getMaterial();
+            if (current == null) {
+                current = mat;
+            } else if (!current.getName().equals(mat.getName())) {
+                assert !alreadySeen.contains(mat.getName());
+                alreadySeen.add(current.getName());
+                current = mat;
+            }
+        }
+        
+        for (int i = 0; i < materials.length; i++) {
+            for (int g = 0; g < 5; g++) {
+                int index = i * 5 + g;
+                Material mat1 = gl.get(index).getMaterial();
+                Material mat2 = materials[i];
+                assert mat1.getName().equals(mat2.getName()) : 
+                       mat1.getName() + " != " + mat2.getName() + " for index " + index;
+            }
+        }
+    }
+    
+    @Test
+    public void testSortByMaterialDef() {
+        Material lightingMat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+        Material particleMat = new Material(assetManager, "Common/MatDefs/Misc/Particle.j3md");
+        Material unshadedMat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        Material skyMat      = new Material(assetManager, "Common/MatDefs/Misc/Sky.j3md");
+        
+        lightingMat.setName("MatLight");
+        particleMat.setName("MatParticle");
+        unshadedMat.setName("MatUnshaded");
+        skyMat.setName("MatSky");
+        testSort(skyMat, lightingMat, particleMat, unshadedMat);
+    }
+    
+    @Test
+    public void testSortByTechnique() {
+        Material lightingMatDefault = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+        Material lightingPreShadow = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+        Material lightingPostShadow = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+        Material lightingMatPreNormalPass = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+        Material lightingMatGBuf = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+        Material lightingMatGlow = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+        
+        lightingMatDefault.setName("TechDefault");
+        lightingMatDefault.selectTechnique("Default", renderManager);
+        
+        lightingPostShadow.setName("TechPostShad");
+        lightingPostShadow.selectTechnique("PostShadow", renderManager);
+        
+        lightingPreShadow.setName("TechPreShad");
+        lightingPreShadow.selectTechnique("PreShadow", renderManager);
+        
+        lightingMatPreNormalPass.setName("TechNorm");
+        lightingMatPreNormalPass.selectTechnique("PreNormalPass", renderManager);
+        
+        lightingMatGBuf.setName("TechGBuf");
+        lightingMatGBuf.selectTechnique("GBuf", renderManager);
+        
+        lightingMatGlow.setName("TechGlow");
+        lightingMatGlow.selectTechnique("Glow", renderManager);
+        
+        testSort(lightingMatGlow, lightingPreShadow, lightingMatPreNormalPass,
+                 lightingMatDefault, lightingPostShadow, lightingMatGBuf);
+    }
+    
+    @Test(expected = AssertionError.class)
+    public void testNoSortByParam() {
+        Material sameMat1 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        Material sameMat2 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        
+        sameMat1.setName("MatRed");
+        sameMat1.setColor("Color", ColorRGBA.Red);
+        
+        sameMat2.setName("MatBlue");
+        sameMat2.setColor("Color", ColorRGBA.Blue);
+        
+        testSort(sameMat1, sameMat2);
+    }
+    
+    private Texture createTexture(String name) {
+        ByteBuffer bb = BufferUtils.createByteBuffer(3);
+        Image image = new Image(Format.RGB8, 1, 1, bb, ColorSpace.sRGB);
+        Texture2D texture = new Texture2D(image);
+        texture.setName(name);
+        return texture;
+    }
+    
+    @Test
+    public void testSortByTexture() {
+        Material texture1Mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        Material texture2Mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        Material texture3Mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        
+        Texture tex1 = createTexture("A");
+        tex1.getImage().setId(1);
+        
+        Texture tex2 = createTexture("B");
+        tex2.getImage().setId(2);
+        
+        Texture tex3 = createTexture("C");
+        tex3.getImage().setId(3);
+        
+        texture1Mat.setName("TexA");
+        texture1Mat.setTexture("ColorMap", tex1);
+        
+        texture2Mat.setName("TexB");
+        texture2Mat.setTexture("ColorMap", tex2);
+        
+        texture3Mat.setName("TexC");
+        texture3Mat.setTexture("ColorMap", tex3);
+        
+        testSort(texture1Mat, texture2Mat, texture3Mat);
+    }
+    
+    @Test
+    public void testSortByShaderDefines() {
+        Material lightingMat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+        Material lightingMatVColor = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+        Material lightingMatVLight = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+        Material lightingMatTC = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+        Material lightingMatVColorLight = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+        Material lightingMatTCVColorLight = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+        
+        lightingMat.setName("DefNone");
+        
+        lightingMatVColor.setName("DefVC");
+        lightingMatVColor.setBoolean("UseVertexColor", true);
+        
+        lightingMatVLight.setName("DefVL");
+        lightingMatVLight.setBoolean("VertexLighting", true);
+        
+        lightingMatTC.setName("DefTC");
+        lightingMatTC.setBoolean("SeparateTexCoord", true);
+        
+        lightingMatVColorLight.setName("DefVCVL");
+        lightingMatVColorLight.setBoolean("UseVertexColor", true);
+        lightingMatVColorLight.setBoolean("VertexLighting", true);
+        
+        lightingMatTCVColorLight.setName("DefVCVLTC");
+        lightingMatTCVColorLight.setBoolean("UseVertexColor", true);
+        lightingMatTCVColorLight.setBoolean("VertexLighting", true);
+        lightingMatTCVColorLight.setBoolean("SeparateTexCoord", true);
+        
+        testSort(lightingMat, lightingMatVColor, lightingMatVLight,
+                 lightingMatVColorLight, lightingMatTC, lightingMatTCVColorLight);
+    }
+    
+    @Test
+    public void testSortByAll() {
+        Material matBase1 = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+        Material matBase2 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        
+        Texture texBase = createTexture("BASE");
+        texBase.getImage().setId(1);
+        Texture tex1 = createTexture("1");
+        tex1.getImage().setId(2);
+        Texture tex2 = createTexture("2");
+        tex2.getImage().setId(3);
+        
+        matBase1.setName("BASE");
+        matBase1.selectTechnique("Default", renderManager);
+        matBase1.setBoolean("UseVertexColor", true);
+        matBase1.setTexture("DiffuseMap", texBase);
+        
+        Material mat1100 = matBase1.clone();
+        mat1100.setName("1100");
+        mat1100.selectTechnique("PreShadow", renderManager);
+        
+        Material mat1101 = matBase1.clone();
+        mat1101.setName("1101");
+        mat1101.selectTechnique("PreShadow", renderManager);
+        mat1101.setTexture("DiffuseMap", tex1);
+        
+        Material mat1102 = matBase1.clone();
+        mat1102.setName("1102");
+        mat1102.selectTechnique("PreShadow", renderManager);
+        mat1102.setTexture("DiffuseMap", tex2);
+        
+        Material mat1110 = matBase1.clone();
+        mat1110.setName("1110");
+        mat1110.selectTechnique("PreShadow", renderManager);
+        mat1110.setFloat("AlphaDiscardThreshold", 2f);
+        
+        Material mat1120 = matBase1.clone();
+        mat1120.setName("1120");
+        mat1120.selectTechnique("PreShadow", renderManager);
+        mat1120.setBoolean("UseInstancing", true);
+        
+        Material mat1121 = matBase1.clone();
+        mat1121.setName("1121");
+        mat1121.selectTechnique("PreShadow", renderManager);
+        mat1121.setBoolean("UseInstancing", true);
+        mat1121.setTexture("DiffuseMap", tex1);
+        
+        Material mat1122 = matBase1.clone();
+        mat1122.setName("1122");
+        mat1122.selectTechnique("PreShadow", renderManager);
+        mat1122.setBoolean("UseInstancing", true);
+        mat1122.setTexture("DiffuseMap", tex2);
+        
+        Material mat1140 = matBase1.clone();
+        mat1140.setName("1140");
+        mat1140.selectTechnique("PreShadow", renderManager);
+        mat1140.setFloat("AlphaDiscardThreshold", 2f);
+        mat1140.setBoolean("UseInstancing", true);
+        
+        Material mat1200 = matBase1.clone();
+        mat1200.setName("1200");
+        mat1200.selectTechnique("PostShadow", renderManager);
+        
+        Material mat1210 = matBase1.clone();
+        mat1210.setName("1210");
+        mat1210.selectTechnique("PostShadow", renderManager);
+        mat1210.setFloat("AlphaDiscardThreshold", 2f);
+        
+        Material mat1220 = matBase1.clone();
+        mat1220.setName("1220");
+        mat1220.selectTechnique("PostShadow", renderManager);
+        mat1220.setBoolean("UseInstancing", true);
+        
+        Material mat2000 = matBase2.clone();
+        mat2000.setName("2000");
+        
+        testSort(mat1100, mat1101, mat1102, mat1110, 
+                 mat1120, mat1121, mat1122, mat1140, 
+                 mat1200, mat1210, mat1220, mat2000);
+    }
+}

+ 173 - 0
jme3-core/src/test/java/com/jme3/scene/MPOTestUtils.java

@@ -0,0 +1,173 @@
+/*
+ * Copyright (c) 2009-2016 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene;
+
+import com.jme3.material.MatParamOverride;
+import com.jme3.math.Matrix4f;
+import com.jme3.renderer.Camera;
+import com.jme3.shader.VarType;
+import com.jme3.texture.Texture2D;
+import java.lang.reflect.Field;
+import java.util.HashSet;
+import java.util.Set;
+import static org.junit.Assert.assertEquals;
+
+public class MPOTestUtils {
+
+    private static final Camera DUMMY_CAM = new Camera(640, 480);
+
+    private static final SceneGraphVisitor VISITOR = new SceneGraphVisitor() {
+        @Override
+        public void visit(Spatial spatial) {
+            validateSubScene(spatial);
+        }
+    };
+
+    private static void validateSubScene(Spatial scene) {
+        scene.checkCulling(DUMMY_CAM);
+
+        Set<MatParamOverride> actualOverrides = new HashSet<MatParamOverride>();
+        for (MatParamOverride override : scene.getWorldMatParamOverrides()) {
+            actualOverrides.add(override);
+        }
+
+        Set<MatParamOverride> expectedOverrides = new HashSet<MatParamOverride>();
+        Spatial current = scene;
+        while (current != null) {
+            for (MatParamOverride override : current.getLocalMatParamOverrides()) {
+                expectedOverrides.add(override);
+            }
+            current = current.getParent();
+        }
+
+        assertEquals("For " + scene, expectedOverrides, actualOverrides);
+    }
+    
+    public static void validateScene(Spatial scene) {
+        scene.updateGeometricState();
+        scene.depthFirstTraversal(VISITOR);
+    }
+
+    public static MatParamOverride mpoInt(String name, int value) {
+        return new MatParamOverride(VarType.Int, name, value);
+    }
+
+    public static MatParamOverride mpoBool(String name, boolean value) {
+        return new MatParamOverride(VarType.Boolean, name, value);
+    }
+
+    public static MatParamOverride mpoFloat(String name, float value) {
+        return new MatParamOverride(VarType.Float, name, value);
+    }
+
+    public static MatParamOverride mpoMatrix4Array(String name, Matrix4f[] value) {
+        return new MatParamOverride(VarType.Matrix4Array, name, value);
+    }
+
+    public static MatParamOverride mpoTexture2D(String name, Texture2D texture) {
+        return new MatParamOverride(VarType.Texture2D, name, texture);
+    }
+
+    private static int getRefreshFlags(Spatial scene) {
+        try {
+            Field refreshFlagsField = Spatial.class.getDeclaredField("refreshFlags");
+            refreshFlagsField.setAccessible(true);
+            return (Integer) refreshFlagsField.get(scene);
+        } catch (NoSuchFieldException ex) {
+            throw new AssertionError(ex);
+        } catch (SecurityException ex) {
+            throw new AssertionError(ex);
+        } catch (IllegalArgumentException ex) {
+            throw new AssertionError(ex);
+        } catch (IllegalAccessException ex) {
+            throw new AssertionError(ex);
+        }
+    }
+
+    private static void dumpSceneRF(Spatial scene, String indent, boolean last, int refreshFlagsMask) {
+        StringBuilder sb = new StringBuilder();
+
+        sb.append(indent);
+        if (last) {
+            if (!indent.isEmpty()) {
+                sb.append("└─");
+            } else {
+                sb.append("  ");
+            }
+            indent += "  ";
+        } else {
+            sb.append("├─");
+            indent += "│ ";
+        }
+        sb.append(scene.getName());
+        int rf = getRefreshFlags(scene) & refreshFlagsMask;
+        if (rf != 0) {
+            sb.append("(");
+            if ((rf & 0x1) != 0) {
+                sb.append("T");
+            }
+            if ((rf & 0x2) != 0) {
+                sb.append("B");
+            }
+            if ((rf & 0x4) != 0) {
+                sb.append("L");
+            }
+            if ((rf & 0x8) != 0) {
+                sb.append("l");
+            }
+            if ((rf & 0x10) != 0) {
+                sb.append("O");
+            }
+            sb.append(")");
+        }
+
+        if (!scene.getLocalMatParamOverrides().isEmpty()) {
+            sb.append(" [MPO]");
+        }
+
+        System.out.println(sb);
+
+        if (scene instanceof Node) {
+            Node node = (Node) scene;
+            int childIndex = 0;
+            for (Spatial child : node.getChildren()) {
+                boolean childLast = childIndex == node.getQuantity() - 1;
+                dumpSceneRF(child, indent, childLast, refreshFlagsMask);
+                childIndex++;
+            }
+        }
+    }
+
+    public static void dumpSceneRF(Spatial scene, int refreshFlagsMask) {
+        dumpSceneRF(scene, "", true, refreshFlagsMask);
+    }
+}

+ 278 - 0
jme3-core/src/test/java/com/jme3/scene/SceneMatParamOverrideTest.java

@@ -0,0 +1,278 @@
+/*
+ * Copyright (c) 2009-2016 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.export.binary.BinaryExporter;
+import com.jme3.material.MatParamOverride;
+import org.junit.Test;
+
+import static com.jme3.scene.MPOTestUtils.*;
+import static org.junit.Assert.*;
+
+import com.jme3.system.TestUtil;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Validates how {@link MatParamOverride MPOs} work on the scene level.
+ *
+ * @author Kirill Vainer
+ */
+public class SceneMatParamOverrideTest {
+
+    private static Node createDummyScene() {
+        Node scene = new Node("Scene Node");
+
+        Node a = new Node("A");
+        Node b = new Node("B");
+
+        Node c = new Node("C");
+        Node d = new Node("D");
+
+        Node e = new Node("E");
+        Node f = new Node("F");
+
+        Node g = new Node("G");
+        Node h = new Node("H");
+        Node j = new Node("J");
+
+        scene.attachChild(a);
+        scene.attachChild(b);
+
+        a.attachChild(c);
+        a.attachChild(d);
+
+        b.attachChild(e);
+        b.attachChild(f);
+
+        c.attachChild(g);
+        c.attachChild(h);
+        c.attachChild(j);
+
+        return scene;
+    }
+
+    @Test
+    public void testOverrides_Empty() {
+        Node n = new Node("Node");
+        assertTrue(n.getLocalMatParamOverrides().isEmpty());
+        assertTrue(n.getWorldMatParamOverrides().isEmpty());
+
+        n.updateGeometricState();
+        assertTrue(n.getLocalMatParamOverrides().isEmpty());
+        assertTrue(n.getWorldMatParamOverrides().isEmpty());
+    }
+
+    @Test
+    public void testOverrides_AddRemove() {
+        MatParamOverride override = mpoBool("Test", true);
+        Node n = new Node("Node");
+
+        n.removeMatParamOverride(override);
+        assertTrue(n.getLocalMatParamOverrides().isEmpty());
+        assertTrue(n.getWorldMatParamOverrides().isEmpty());
+
+        n.addMatParamOverride(override);
+
+        assertSame(n.getLocalMatParamOverrides().get(0), override);
+        assertTrue(n.getWorldMatParamOverrides().isEmpty());
+        n.updateGeometricState();
+
+        assertSame(n.getLocalMatParamOverrides().get(0), override);
+        assertSame(n.getWorldMatParamOverrides().get(0), override);
+
+        n.removeMatParamOverride(override);
+        assertTrue(n.getLocalMatParamOverrides().isEmpty());
+        assertSame(n.getWorldMatParamOverrides().get(0), override);
+
+        n.updateGeometricState();
+        assertTrue(n.getLocalMatParamOverrides().isEmpty());
+        assertTrue(n.getWorldMatParamOverrides().isEmpty());
+    }
+
+    @Test
+    public void testOverrides_Clear() {
+        MatParamOverride override = mpoBool("Test", true);
+        Node n = new Node("Node");
+
+        n.clearMatParamOverrides();
+        assertTrue(n.getLocalMatParamOverrides().isEmpty());
+        assertTrue(n.getWorldMatParamOverrides().isEmpty());
+
+        n.addMatParamOverride(override);
+        n.clearMatParamOverrides();
+        assertTrue(n.getLocalMatParamOverrides().isEmpty());
+        assertTrue(n.getWorldMatParamOverrides().isEmpty());
+
+        n.addMatParamOverride(override);
+        n.updateGeometricState();
+        n.clearMatParamOverrides();
+        assertTrue(n.getLocalMatParamOverrides().isEmpty());
+        assertSame(n.getWorldMatParamOverrides().get(0), override);
+
+        n.updateGeometricState();
+        assertTrue(n.getLocalMatParamOverrides().isEmpty());
+        assertTrue(n.getWorldMatParamOverrides().isEmpty());
+
+        n.addMatParamOverride(override);
+        n.clearMatParamOverrides();
+        n.updateGeometricState();
+        assertTrue(n.getLocalMatParamOverrides().isEmpty());
+        assertTrue(n.getWorldMatParamOverrides().isEmpty());
+    }
+
+    @Test
+    public void testOverrides_AddAfterAttach() {
+        Node scene = createDummyScene();
+        scene.updateGeometricState();
+
+        Node root = new Node("Root Node");
+        root.updateGeometricState();
+
+        root.attachChild(scene);
+        scene.getChild("A").addMatParamOverride(mpoInt("val", 5));
+
+        validateScene(root);
+    }
+
+    @Test
+    public void testOverrides_AddBeforeAttach() {
+        Node scene = createDummyScene();
+        scene.getChild("A").addMatParamOverride(mpoInt("val", 5));
+        scene.updateGeometricState();
+
+        Node root = new Node("Root Node");
+        root.updateGeometricState();
+
+        root.attachChild(scene);
+
+        validateScene(root);
+    }
+
+    @Test
+    public void testOverrides_RemoveBeforeAttach() {
+        Node scene = createDummyScene();
+        scene.updateGeometricState();
+
+        Node root = new Node("Root Node");
+        root.updateGeometricState();
+
+        scene.getChild("A").addMatParamOverride(mpoInt("val", 5));
+        validateScene(scene);
+
+        scene.getChild("A").clearMatParamOverrides();
+        validateScene(scene);
+
+        root.attachChild(scene);
+        validateScene(root);
+    }
+
+    @Test
+    public void testOverrides_RemoveAfterAttach() {
+        Node scene = createDummyScene();
+        scene.updateGeometricState();
+
+        Node root = new Node("Root Node");
+        root.updateGeometricState();
+
+        scene.getChild("A").addMatParamOverride(mpoInt("val", 5));
+
+        root.attachChild(scene);
+        validateScene(root);
+
+        scene.getChild("A").clearMatParamOverrides();
+        validateScene(root);
+    }
+
+    @Test
+    public void testOverrides_IdenticalNames() {
+        Node scene = createDummyScene();
+
+        scene.getChild("A").addMatParamOverride(mpoInt("val", 5));
+        scene.getChild("C").addMatParamOverride(mpoInt("val", 7));
+
+        validateScene(scene);
+    }
+
+    @Test
+    public void testOverrides_CloningScene_DoesntCloneMPO() {
+        Node originalScene = createDummyScene();
+
+        originalScene.getChild("A").addMatParamOverride(mpoInt("int", 5));
+        originalScene.getChild("A").addMatParamOverride(mpoBool("bool", true));
+        originalScene.getChild("A").addMatParamOverride(mpoFloat("float", 3.12f));
+
+        Node clonedScene = originalScene.clone(false);
+
+        validateScene(clonedScene);
+        validateScene(originalScene);
+
+        List<MatParamOverride> clonedOverrides = clonedScene.getChild("A").getLocalMatParamOverrides();
+        List<MatParamOverride> originalOverrides = originalScene.getChild("A").getLocalMatParamOverrides();
+
+        assertNotSame(clonedOverrides, originalOverrides);
+        assertEquals(clonedOverrides, originalOverrides);
+
+        for (int i = 0; i < clonedOverrides.size(); i++) {
+            assertNotSame(clonedOverrides.get(i), originalOverrides.get(i));
+            assertEquals(clonedOverrides.get(i), originalOverrides.get(i));
+        }
+    }
+
+    @Test
+    public void testOverrides_SaveAndLoad_KeepsMPOs() {
+        MatParamOverride override = mpoInt("val", 5);
+        Node scene = createDummyScene();
+        scene.getChild("A").addMatParamOverride(override);
+
+        AssetManager assetManager = TestUtil.createAssetManager();
+        Node loadedScene = BinaryExporter.saveAndLoad(assetManager, scene);
+
+        Node root = new Node("Root Node");
+        root.attachChild(loadedScene);
+        validateScene(root);
+        validateScene(scene);
+
+        assertNotSame(override, loadedScene.getChild("A").getLocalMatParamOverrides().get(0));
+        assertEquals(override, loadedScene.getChild("A").getLocalMatParamOverrides().get(0));
+    }
+
+    @Test
+    public void testEquals() {
+        assertEquals(mpoInt("val", 5), mpoInt("val", 5));
+        assertEquals(mpoBool("val", true), mpoBool("val", true));
+        assertNotEquals(mpoInt("val", 5), mpoInt("val", 6));
+        assertNotEquals(mpoInt("val1", 5), mpoInt("val2", 5));
+        assertNotEquals(mpoBool("val", true), mpoInt("val", 1));
+    }
+}

+ 300 - 0
jme3-core/src/test/java/com/jme3/shader/DefineListTest.java

@@ -0,0 +1,300 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.shader;
+
+import com.jme3.math.FastMath;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class DefineListTest {
+    
+    private static final List<String> DEFINE_NAMES = Arrays.asList("BOOL_VAR", "INT_VAR", "FLOAT_VAR");
+    private static final List<VarType> DEFINE_TYPES = Arrays.asList(VarType.Boolean, VarType.Int, VarType.Float);
+    private static final int NUM_DEFINES = DEFINE_NAMES.size();
+    private static final int BOOL_VAR = 0;
+    private static final int INT_VAR = 1;
+    private static final int FLOAT_VAR = 2;
+    private static final DefineList EMPTY = new DefineList(NUM_DEFINES);
+
+    @Test
+    public void testHashCollision() {
+        DefineList dl1 = new DefineList(64);
+        DefineList dl2 = new DefineList(64);
+        
+        // Try to cause a hash collision
+        // (since bit #32 is aliased to bit #1 in 32-bit ints)
+        dl1.set(0, 123);
+        dl1.set(32, 0);
+        
+        dl2.set(32, 0);
+        dl2.set(0, 123);
+        
+        assert dl1.hashCode() == dl2.hashCode();
+        assert dl1.equals(dl2);
+    }
+    
+    @Test
+    public void testGetSet() {
+        DefineList dl = new DefineList(NUM_DEFINES);
+        
+        assertFalse(dl.getBoolean(BOOL_VAR));
+        assertEquals(dl.getInt(INT_VAR), 0);
+        assertEquals(dl.getFloat(FLOAT_VAR), 0f, 0f);
+        
+        dl.set(BOOL_VAR, true);
+        dl.set(INT_VAR, -1);
+        dl.set(FLOAT_VAR, Float.NaN);
+        
+        assertTrue(dl.getBoolean(BOOL_VAR));
+        assertEquals(dl.getInt(INT_VAR), -1);
+        assertTrue(Float.isNaN(dl.getFloat(FLOAT_VAR)));
+    }
+    
+    private String generateSource(DefineList dl) {
+        StringBuilder sb = new StringBuilder();
+        dl.generateSource(sb, DEFINE_NAMES, DEFINE_TYPES);
+        return sb.toString();
+    }
+    
+    @Test
+    public void testSourceInitial() {
+        DefineList dl = new DefineList(NUM_DEFINES);
+        assert dl.hashCode() == 0;
+        assert generateSource(dl).equals("");
+    }
+    
+    @Test
+    public void testSourceBooleanDefine() {
+        DefineList dl = new DefineList(NUM_DEFINES);
+
+        dl.set(BOOL_VAR, true);
+        assert dl.hashCode() == 1;
+        assert generateSource(dl).equals("#define BOOL_VAR 1\n");
+        
+        dl.set(BOOL_VAR, false);
+        assert dl.hashCode() == 0;
+        assert generateSource(dl).equals("");
+    }
+    
+    @Test
+    public void testSourceIntDefine() {
+        DefineList dl = new DefineList(NUM_DEFINES);
+
+        int hashCodeWithInt = 1 << INT_VAR;
+        
+        dl.set(INT_VAR, 123);
+        assert dl.hashCode() == hashCodeWithInt;
+        assert generateSource(dl).equals("#define INT_VAR 123\n");
+        
+        dl.set(INT_VAR, 0);
+        assert dl.hashCode() == 0;
+        assert generateSource(dl).equals("");
+        
+        dl.set(INT_VAR, -99);
+        assert dl.hashCode() == hashCodeWithInt;
+        assert generateSource(dl).equals("#define INT_VAR -99\n");
+        
+        dl.set(INT_VAR, Integer.MAX_VALUE);
+        assert dl.hashCode() == hashCodeWithInt;
+        assert generateSource(dl).equals("#define INT_VAR 2147483647\n");
+    }
+    
+    @Test
+    public void testSourceFloatDefine() {
+        DefineList dl = new DefineList(NUM_DEFINES);
+
+        dl.set(FLOAT_VAR, 1f);
+        assert dl.hashCode() == (1 << FLOAT_VAR);
+        assert generateSource(dl).equals("#define FLOAT_VAR 1.0\n");
+        
+        dl.set(FLOAT_VAR, 0f);
+        assert dl.hashCode() == 0;
+        assert generateSource(dl).equals("");
+        
+        dl.set(FLOAT_VAR, -1f);
+        assert generateSource(dl).equals("#define FLOAT_VAR -1.0\n");
+        
+        dl.set(FLOAT_VAR, FastMath.FLT_EPSILON);
+        assert generateSource(dl).equals("#define FLOAT_VAR 1.1920929E-7\n");
+        
+        dl.set(FLOAT_VAR, FastMath.PI);
+        assert generateSource(dl).equals("#define FLOAT_VAR 3.1415927\n");
+        
+        try {
+            dl.set(FLOAT_VAR, Float.NaN);
+            generateSource(dl);
+            assert false;
+        } catch (IllegalArgumentException ex) { }
+        
+        try {
+            dl.set(FLOAT_VAR, Float.POSITIVE_INFINITY);
+            generateSource(dl);
+            assert false;
+        } catch (IllegalArgumentException ex) { }
+        
+        try {
+            dl.set(FLOAT_VAR, Float.NEGATIVE_INFINITY);
+            generateSource(dl);
+            assert false;
+        } catch (IllegalArgumentException ex) { }
+    }
+    
+    @Test
+    public void testEqualsAndHashCode() {
+        DefineList dl1 = new DefineList(NUM_DEFINES);
+        DefineList dl2 = new DefineList(NUM_DEFINES);
+        
+        assertTrue(dl1.hashCode() == 0);
+        assertEquals(dl1, dl2);
+        
+        dl1.set(BOOL_VAR, true);
+        
+        assertTrue(dl1.hashCode() == 1);
+        assertNotSame(dl1, dl2);
+        
+        dl2.set(BOOL_VAR, true);
+        
+        assertEquals(dl1, dl2);
+        
+        dl1.set(INT_VAR, 2);
+        
+        assertTrue(dl1.hashCode() == (1|2));
+        assertNotSame(dl1, dl2);
+        
+        dl2.set(INT_VAR, 2);
+        
+        assertEquals(dl1, dl2);
+        
+        dl1.set(BOOL_VAR, false);
+        
+        assertTrue(dl1.hashCode() == 2);
+        assertNotSame(dl1, dl2);
+    }
+    
+    @Test
+    public void testDeepClone() {
+        DefineList dl1 = new DefineList(NUM_DEFINES);
+        DefineList dl2 = dl1.deepClone();
+        
+        assertFalse(dl1 == dl2);
+        assertTrue(dl1.equals(dl2));
+        assertTrue(dl1.hashCode() == dl2.hashCode());
+        
+        dl1.set(BOOL_VAR, true);
+        dl2 = dl1.deepClone();
+        
+        assertTrue(dl1.equals(dl2));
+        assertTrue(dl1.hashCode() == dl2.hashCode());
+        
+        dl1.set(INT_VAR, 123);
+        
+        assertFalse(dl1.equals(dl2));
+        assertFalse(dl1.hashCode() == dl2.hashCode());
+        
+        dl2 = dl1.deepClone();
+        
+        assertTrue(dl1.equals(dl2));
+        assertTrue(dl1.hashCode() == dl2.hashCode());
+    }
+    
+    @Test
+    public void testGenerateSource() {
+        DefineList dl = new DefineList(NUM_DEFINES);
+        
+        assertEquals("", generateSource(dl));
+        
+        dl.set(BOOL_VAR, true);
+        
+        assertEquals("#define BOOL_VAR 1\n", generateSource(dl));
+        
+        dl.set(INT_VAR, 123);
+        
+        assertEquals("#define BOOL_VAR 1\n" + 
+                     "#define INT_VAR 123\n", generateSource(dl));
+        
+        dl.set(BOOL_VAR, false);
+        
+        assertEquals("#define INT_VAR 123\n", generateSource(dl));
+        
+        dl.set(BOOL_VAR, true);
+        
+        // should have predictable ordering based on defineId
+        assertEquals("#define BOOL_VAR 1\n" + 
+                     "#define INT_VAR 123\n", generateSource(dl));
+    }
+    
+    private static String doLookup(HashMap<DefineList, String> map, boolean boolVal, int intVal, float floatVal) {
+        DefineList dl = new DefineList(NUM_DEFINES);
+        dl.set(BOOL_VAR, boolVal);
+        dl.set(INT_VAR, intVal);
+        dl.set(FLOAT_VAR, floatVal);
+        return map.get(dl);
+    }
+    
+    @Test
+    public void testHashLookup() {
+        String STR_EMPTY          = "This is an empty define list";
+        String STR_INT            = "This define list has an int value";
+        String STR_BOOL           = "This define list just has boolean value set";
+        String STR_BOOL_INT       = "This define list has both a boolean and int value";
+        String STR_BOOL_INT_FLOAT = "This define list has a boolean, int, and float value";
+        
+        HashMap<DefineList, String> map = new HashMap<DefineList, String>();
+        
+        DefineList lookup = new DefineList(NUM_DEFINES);
+        
+        map.put(lookup.deepClone(), STR_EMPTY);
+        
+        lookup.set(BOOL_VAR, true);
+        map.put(lookup.deepClone(), STR_BOOL);
+        
+        lookup.set(BOOL_VAR, false);
+        lookup.set(INT_VAR, 123);
+        map.put(lookup.deepClone(), STR_INT);
+        
+        lookup.set(BOOL_VAR, true);
+        map.put(lookup.deepClone(), STR_BOOL_INT);
+        
+        lookup.set(FLOAT_VAR, FastMath.PI);
+        map.put(lookup.deepClone(), STR_BOOL_INT_FLOAT);
+        
+        assertEquals(doLookup(map, false, 0, 0f), STR_EMPTY);
+        assertEquals(doLookup(map, false, 123, 0f), STR_INT);
+        assertEquals(doLookup(map, true, 0, 0f), STR_BOOL);
+        assertEquals(doLookup(map, true, 123, 0f), STR_BOOL_INT);
+        assertEquals(doLookup(map, true, 123, FastMath.PI), STR_BOOL_INT_FLOAT);
+    }
+}

+ 78 - 0
jme3-core/src/test/java/com/jme3/system/MockJmeSystemDelegate.java

@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.system;
+
+import com.jme3.audio.AudioRenderer;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URL;
+import java.nio.ByteBuffer;
+
+public class MockJmeSystemDelegate extends JmeSystemDelegate {
+
+    @Override
+    public void writeImageFile(OutputStream outStream, String format, ByteBuffer imageData, int width, int height) throws IOException {
+    }
+
+    @Override
+    public void showErrorDialog(String message) {
+    }
+
+    @Override
+    public boolean showSettingsDialog(AppSettings sourceSettings, boolean loadFromRegistry) {
+        return false;
+    }
+
+    @Override
+    public URL getPlatformAssetConfigURL() {
+        return Thread.currentThread().getContextClassLoader().getResource("com/jme3/asset/General.cfg");
+    }
+
+    @Override
+    public JmeContext newContext(AppSettings settings, JmeContext.Type contextType) {
+        return null;
+    }
+
+    @Override
+    public AudioRenderer newAudioRenderer(AppSettings settings) {
+        return null;
+    }
+
+    @Override
+    public void initialize(AppSettings settings) {
+    }
+
+    @Override
+    public void showSoftKeyboard(boolean show) {
+    }
+    
+}

+ 55 - 0
jme3-core/src/test/java/com/jme3/system/TestUtil.java

@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2009-2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.system;
+
+import com.jme3.asset.AssetConfig;
+import com.jme3.asset.AssetManager;
+import com.jme3.asset.DesktopAssetManager;
+import com.jme3.renderer.RenderManager;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class TestUtil {
+    
+    static {
+        JmeSystem.setSystemDelegate(new MockJmeSystemDelegate());
+    }
+    
+    public static AssetManager createAssetManager() {
+        Logger.getLogger(AssetConfig.class.getName()).setLevel(Level.OFF);
+        return new DesktopAssetManager(true);
+    }
+    
+    public static RenderManager createRenderManager() {
+        return new RenderManager(new NullRenderer());
+    }
+}

+ 12 - 12
jme3-core/src/tools/java/jme3tools/shadercheck/ShaderCheck.java

@@ -6,11 +6,12 @@ import com.jme3.asset.plugins.FileLocator;
 import com.jme3.material.MaterialDef;
 import com.jme3.material.MaterialDef;
 import com.jme3.material.TechniqueDef;
 import com.jme3.material.TechniqueDef;
 import com.jme3.material.plugins.J3MLoader;
 import com.jme3.material.plugins.J3MLoader;
+import com.jme3.renderer.Caps;
 import com.jme3.shader.DefineList;
 import com.jme3.shader.DefineList;
 import com.jme3.shader.Shader;
 import com.jme3.shader.Shader;
-import com.jme3.shader.ShaderKey;
 import com.jme3.shader.plugins.GLSLLoader;
 import com.jme3.shader.plugins.GLSLLoader;
 import com.jme3.system.JmeSystem;
 import com.jme3.system.JmeSystem;
+import java.util.EnumSet;
 import java.util.logging.Level;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import java.util.logging.Logger;
 
 
@@ -33,23 +34,22 @@ public class ShaderCheck {
         assetManager.registerLoader(GLSLLoader.class, "vert", "frag","geom","tsctrl","tseval","glsllib");
         assetManager.registerLoader(GLSLLoader.class, "vert", "frag","geom","tsctrl","tseval","glsllib");
     }
     }
     
     
-    private static void checkMatDef(String matdefName){
+    private static void checkMatDef(String matdefName) {
         MaterialDef def = (MaterialDef) assetManager.loadAsset(matdefName);
         MaterialDef def = (MaterialDef) assetManager.loadAsset(matdefName);
-        for (TechniqueDef techDef : def.getDefaultTechniques()){
-            DefineList dl = new DefineList();
-            dl.addFrom(techDef.getShaderPresetDefines());
-            ShaderKey shaderKey = new ShaderKey(dl,techDef.getShaderProgramLanguages(),techDef.getShaderProgramNames());
-
-            Shader shader = assetManager.loadShader(shaderKey);
-
-            for (Validator validator : validators){
+        EnumSet<Caps> rendererCaps = EnumSet.noneOf(Caps.class);
+        rendererCaps.add(Caps.GLSL100);
+        for (TechniqueDef techDef : def.getDefaultTechniques()) {
+            DefineList defines = techDef.createDefineList();
+            Shader shader = techDef.getShader(assetManager, rendererCaps, defines);
+            for (Validator validator : validators) {
                 StringBuilder sb = new StringBuilder();
                 StringBuilder sb = new StringBuilder();
                 validator.validate(shader, sb);
                 validator.validate(shader, sb);
-                System.out.println("==== Validator: " + validator.getName() + " " + 
-                                        validator.getInstalledVersion() + " ====");
+                System.out.println("==== Validator: " + validator.getName() + " "
+                        + validator.getInstalledVersion() + " ====");
                 System.out.println(sb.toString());
                 System.out.println(sb.toString());
             }
             }
         }
         }
+        throw new UnsupportedOperationException();
     }
     }
           
           
     public static void main(String[] args){
     public static void main(String[] args){

+ 4 - 4
jme3-desktop/src/main/java/com/jme3/app/AppletHarness.java

@@ -50,12 +50,12 @@ import javax.swing.SwingUtilities;
  */
  */
 public class AppletHarness extends Applet {
 public class AppletHarness extends Applet {
 
 
-    public static final HashMap<Application, Applet> appToApplet
-                         = new HashMap<Application, Applet>();
+    public static final HashMap<LegacyApplication, Applet> appToApplet
+                         = new HashMap<LegacyApplication, Applet>();
 
 
     protected JmeCanvasContext context;
     protected JmeCanvasContext context;
     protected Canvas canvas;
     protected Canvas canvas;
-    protected Application app;
+    protected LegacyApplication app;
 
 
     protected String appClass;
     protected String appClass;
     protected URL appCfg = null;
     protected URL appCfg = null;
@@ -103,7 +103,7 @@ public class AppletHarness extends Applet {
         JmeSystem.setLowPermissions(true);
         JmeSystem.setLowPermissions(true);
 
 
         try{
         try{
-            Class<? extends Application> clazz = (Class<? extends Application>) Class.forName(appClass);
+            Class<? extends LegacyApplication> clazz = (Class<? extends LegacyApplication>) Class.forName(appClass);
             app = clazz.newInstance();
             app = clazz.newInstance();
         }catch (ClassNotFoundException ex){
         }catch (ClassNotFoundException ex){
             ex.printStackTrace();
             ex.printStackTrace();

+ 18 - 24
jme3-desktop/src/main/java/com/jme3/system/JmeDesktopSystem.java

@@ -43,6 +43,7 @@ import com.jme3.system.JmeContext.Type;
 import com.jme3.util.Screenshots;
 import com.jme3.util.Screenshots;
 import java.awt.EventQueue;
 import java.awt.EventQueue;
 import java.awt.Graphics2D;
 import java.awt.Graphics2D;
+import java.awt.GraphicsEnvironment;
 import java.awt.RenderingHints;
 import java.awt.RenderingHints;
 import java.awt.geom.AffineTransform;
 import java.awt.geom.AffineTransform;
 import java.awt.image.AffineTransformOp;
 import java.awt.image.AffineTransformOp;
@@ -116,12 +117,16 @@ public class JmeDesktopSystem extends JmeSystemDelegate {
 
 
     @Override
     @Override
     public void showErrorDialog(String message) {
     public void showErrorDialog(String message) {
-        final String msg = message;
-        EventQueue.invokeLater(new Runnable() {
-            public void run() {
-                ErrorDialog.showDialog(msg);
-            }
-        });
+        if (!GraphicsEnvironment.isHeadless()) {
+            final String msg = message;
+            EventQueue.invokeLater(new Runnable() {
+                public void run() {
+                    ErrorDialog.showDialog(msg);
+                }
+            });
+        } else {
+            System.err.println("[JME ERROR] " + message);
+        }
     }
     }
 
 
     @Override
     @Override
@@ -129,6 +134,9 @@ public class JmeDesktopSystem extends JmeSystemDelegate {
         if (SwingUtilities.isEventDispatchThread()) {
         if (SwingUtilities.isEventDispatchThread()) {
             throw new IllegalStateException("Cannot run from EDT");
             throw new IllegalStateException("Cannot run from EDT");
         }
         }
+        if (GraphicsEnvironment.isHeadless()) {
+            throw new IllegalStateException("Cannot show dialog in headless environment");
+        }
 
 
         final AppSettings settings = new AppSettings(false);
         final AppSettings settings = new AppSettings(false);
         settings.copyFrom(sourceSettings);
         settings.copyFrom(sourceSettings);
@@ -333,27 +341,13 @@ public class JmeDesktopSystem extends JmeSystemDelegate {
         if (initialized) {
         if (initialized) {
             return;
             return;
         }
         }
-
         initialized = true;
         initialized = true;
-        try {
-            if (!lowPermissions) {
-                // can only modify logging settings
-                // if permissions are available
-//                JmeFormatter formatter = new JmeFormatter();
-//                Handler fileHandler = new FileHandler("jme.log");
-//                fileHandler.setFormatter(formatter);
-//                Logger.getLogger("").addHandler(fileHandler);
-//                Handler consoleHandler = new ConsoleHandler();
-//                consoleHandler.setFormatter(formatter);
-//                Logger.getLogger("").removeHandler(Logger.getLogger("").getHandlers()[0]);
-//                Logger.getLogger("").addHandler(consoleHandler);
+        logger.log(Level.INFO, getBuildInfo());
+        if (!lowPermissions) {
+            if (NativeLibraryLoader.isUsingNativeBullet()) {
+                NativeLibraryLoader.loadNativeLibrary("bulletjme", true);
             }
             }
-//        } catch (IOException ex){
-//            logger.log(Level.SEVERE, "I/O Error while creating log file", ex);
-        } catch (SecurityException ex) {
-            logger.log(Level.SEVERE, "Security error in creating log file", ex);
         }
         }
-        logger.log(Level.INFO, getBuildInfo());
     }
     }
 
 
     @Override
     @Override

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