Selaa lähdekoodia

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

jmekaelthas 9 vuotta sitten
vanhempi
commit
c07ef80d43
100 muutettua tiedostoa jossa 6713 lisäystä ja 3114 poistoa
  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/
-/.nb-gradle/private/
-/.nb-gradle/profiles/private/
+/.nb-gradle/
 /.idea/
 /dist/
 /build/
+/bin/
 /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-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-2.82-r2704/
 /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_texture_plugins_AndroidNativeImageLoader.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/bulletjme.dll
 !/jme3-bullet-native/libs/native/osx/x86/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_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.') {
     classifier = 'sources'
     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
      */
-    protected Application app = null;
+    protected LegacyApplication app = null;
 
     /**
      * 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 class DataObject {
-        protected Application app = null;
+        protected LegacyApplication app = null;
     }
 
     @Override
@@ -241,7 +241,7 @@ public class AndroidHarness extends Activity implements TouchListener, DialogInt
             try {
                 if (app == null) {
                     @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();
                 }
 

+ 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;
     final private String ESCAPE_EVENT = "TouchEscape";
     private boolean firstDrawFrame = true;
-    private Application app = null;
+    private LegacyApplication app = null;
     private int viewWidth = 0;
     private int viewHeight = 0;
 
@@ -258,7 +258,7 @@ public class AndroidHarnessFragment extends Fragment implements
         try {
             if (app == null) {
                 @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();
             }
 

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

@@ -40,6 +40,7 @@ jclass jmeClasses::PhysicsSpace;
 jmethodID jmeClasses::PhysicsSpace_preTick;
 jmethodID jmeClasses::PhysicsSpace_postTick;
 jmethodID jmeClasses::PhysicsSpace_addCollisionEvent;
+jmethodID jmeClasses::PhysicsSpace_notifyCollisionGroupListeners;
 
 jclass jmeClasses::PhysicsGhostObject;
 jmethodID jmeClasses::PhysicsGhostObject_addOverlappingObject;
@@ -137,6 +138,7 @@ void jmeClasses::initJavaClasses(JNIEnv* env) {
     PhysicsSpace_preTick = env->GetMethodID(PhysicsSpace, "preTick_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_notifyCollisionGroupListeners = env->GetMethodID(PhysicsSpace, "notifyCollisionGroupListeners_native","(Lcom/jme3/bullet/collision/PhysicsCollisionObject;Lcom/jme3/bullet/collision/PhysicsCollisionObject;)Z");
     if (env->ExceptionCheck()) {
         env->Throw(env->ExceptionOccurred());
         return;

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

@@ -46,6 +46,7 @@ public:
     static jmethodID PhysicsSpace_addCollisionEvent;
     static jclass PhysicsGhostObject;
     static jmethodID PhysicsGhostObject_addOverlappingObject;
+    static jmethodID PhysicsSpace_notifyCollisionGroupListeners;
 
     static jclass Vector3f;
     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 *up1 = (jmeUserPointer*) co1 -> getUserPointer();
                 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'
                     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());
         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

+ 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() {
             KillParticleControl c = new KillParticleControl();
             //this control should be removed as it shouldn't have been persisted in the first place
-            //In the quest to find the less hackish solution to achieve this, 
-            //making it remove itself from the spatial in the first update loop when loaded was the less bad. 
+            //In the quest to find the less hackish solution to achieve this,
+            //making it remove itself from the spatial in the first update loop when loaded was the less bad.
             c.remove = true;
             c.spatial = spatial;
             return c;
-        }     
-
-        @Override   
-        public void cloneFields( Cloner cloner, Object original ) { 
-            this.spatial = cloner.clone(spatial);
         }
-         
+
         @Override
         protected void controlRender(RenderManager rm, ViewPort vp) {
         }
@@ -143,8 +138,8 @@ public class EffectTrack implements ClonableTrack {
 
             KillParticleControl c = new KillParticleControl();
             //this control should be removed as it shouldn't have been persisted in the first place
-            //In the quest to find the less hackish solution to achieve this, 
-            //making it remove itself from the spatial in the first update loop when loaded was the less bad. 
+            //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.setSpatial(spatial);
             return c;
@@ -261,7 +256,7 @@ public class EffectTrack implements ClonableTrack {
     public float[] getKeyFrameTimes() {
         return new float[] { startOffset };
     }
-    
+
     /**
      * Clone this track
      *
@@ -302,21 +297,21 @@ public class EffectTrack implements ClonableTrack {
         return effectTrack;
     }
 
-    @Override   
+    @Override
     public Object jmeClone() {
         try {
             return super.clone();
         } catch( CloneNotSupportedException e ) {
             throw new RuntimeException("Error cloning", e);
         }
-    }     
+    }
 
 
-    @Override   
-    public void cloneFields( Cloner cloner, Object original ) { 
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) {
         this.emitter = cloner.clone(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
      */
     private Set<Material> materials = new HashSet<Material>();
-
+    
     /**
      * Serialization only. Do not use.
      */
@@ -204,6 +204,9 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl
      * @param skeleton the skeleton
      */
     public SkeletonControl(Skeleton skeleton) {
+        if (skeleton == null) {
+            throw new IllegalArgumentException("skeleton cannot be null");
+        }
         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.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.input.InputManager;
 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>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.
-     * 
+     *
      * @return The lost focus behavior of the application.
      */
-    public LostFocusBehavior getLostFocusBehavior() {
-        return lostFocusBehavior;
-    }
-    
+    public LostFocusBehavior getLostFocusBehavior();
+
     /**
      * 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.
      * 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;
-    }
-    
+    public void setLostFocusBehavior(LostFocusBehavior lostFocusBehavior);
+
     /**
      * Returns true if pause on lost focus is enabled, false otherwise.
      *
      * @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.
@@ -153,52 +94,10 @@ public class Application implements SystemListener {
      *
      * @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 = 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.
@@ -210,321 +109,83 @@ public class Application implements SystemListener {
      *
      * @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
      * 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();
-        }
+    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.
      */
-    public AssetManager getAssetManager(){
-        return assetManager;
-    }
+    public AssetManager getAssetManager();
 
     /**
      * @return the {@link InputManager input manager}.
      */
-    public InputManager getInputManager(){
-        return inputManager;
-    }
+    public InputManager getInputManager();
 
     /**
      * @return the {@link AppStateManager app state manager}
      */
-    public AppStateManager getStateManager() {
-        return stateManager;
-    }
+    public AppStateManager getStateManager();
 
     /**
      * @return the {@link RenderManager render manager}
      */
-    public RenderManager getRenderManager() {
-        return renderManager;
-    }
+    public RenderManager getRenderManager();
 
     /**
      * @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
      */
-    public AudioRenderer getAudioRenderer() {
-        return audioRenderer;
-    }
+    public AudioRenderer getAudioRenderer();
 
     /**
      * @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
      */
-    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.
-     * 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.
-     * 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
      * 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;
-    }
+    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.
@@ -533,10 +194,7 @@ public class Application implements SystemListener {
      * applied immediately; calling this method forces the context
      * 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
@@ -546,102 +204,14 @@ public class Application implements SystemListener {
      *
      * @see #stop(boolean)
      */
-    public void stop(){
-        stop(false);
-    }
+    public void stop();
 
     /**
      * 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);
-    }
+    public void stop(boolean waitFor);
 
     /**
      * 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.
      * 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;
-    }
-    
+    public <V> Future<V> enqueue(Callable<V> callable);
+
     /**
      * Enqueues a runnable object to execute in the jME3
      * rendering thread.
@@ -666,109 +232,16 @@ public class Application implements SystemListener {
      * 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();
-    }
+    public void enqueue(Runnable runnable);
 
     /**
      * @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;
-        }
+    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>M</td><td>- Display memory usage in the console.</td></tr>
  * </table>
- * 
+ *
  * 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>
  */
-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_CAMERA_POS = DebugKeysAppState.INPUT_MAPPING_CAMERA_POS;
     public static final String INPUT_MAPPING_MEMORY = DebugKeysAppState.INPUT_MAPPING_MEMORY;
     public static final String INPUT_MAPPING_HIDE_STATS = "SIMPLEAPP_HideStats";
-                                                                         
+
     protected Node rootNode = new Node("Root Node");
     protected Node guiNode = new Node("Gui Node");
     protected BitmapText fpsText;
@@ -77,7 +77,7 @@ public abstract class SimpleApplication extends Application {
     protected FlyByCamera flyCam;
     protected boolean showSettings = true;
     private AppActionListener actionListener = new AppActionListener();
-    
+
     private class AppActionListener implements ActionListener {
 
         public void onAction(String name, boolean value, float tpf) {
@@ -100,15 +100,7 @@ public abstract class SimpleApplication extends Application {
     }
 
     public SimpleApplication( AppState... initialStates ) {
-        super();
-        
-        if (initialStates != null) {
-            for (AppState a : initialStates) {
-                if (a != null) {
-                    stateManager.attach(a);
-                }
-            }
-        }
+        super(initialStates);
     }
 
     @Override
@@ -193,7 +185,7 @@ public abstract class SimpleApplication extends Application {
         guiViewPort.attachScene(guiNode);
 
         if (inputManager != null) {
-        
+
             // We have to special-case the FlyCamAppState because too
             // many SimpleApplication subclasses expect it to exist in
             // 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) {
                 flyCam = new FlyByCamera(cam);
                 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) {
@@ -210,10 +202,10 @@ public abstract class SimpleApplication extends Application {
 
             if (stateManager.getState(StatsAppState.class) != null) {
                 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) {
@@ -230,37 +222,37 @@ public abstract class SimpleApplication extends Application {
     @Override
     public void update() {
         if (prof!=null) prof.appStep(AppStep.BeginFrame);
-        
+
         super.update(); // makes sure to execute AppTasks
         if (speed == 0 || paused) {
             return;
         }
 
         float tpf = timer.getTimePerFrame() * speed;
-        
+
         // update states
         if (prof!=null) prof.appStep(AppStep.StateManagerUpdate);
         stateManager.update(tpf);
 
         // simple update and root node
         simpleUpdate(tpf);
- 
+
         if (prof!=null) prof.appStep(AppStep.SpatialUpdate);
         rootNode.updateLogicalState(tpf);
         guiNode.updateLogicalState(tpf);
-        
+
         rootNode.updateGeometricState();
         guiNode.updateGeometricState();
-        
+
         // render states
         if (prof!=null) prof.appStep(AppStep.StateManagerRender);
         stateManager.render(renderManager);
-        
+
         if (prof!=null) prof.appStep(AppStep.RenderFrame);
         renderManager.render(tpf, context.isRenderable());
         simpleRender(renderManager);
         stateManager.postRender();
-                
+
         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
- *  using the node and font parameters provided.  
+ *  using the node and font parameters provided.
  *
  *  @author    Paul Speed
  */
@@ -58,7 +58,7 @@ public class StatsAppState extends AbstractAppState {
     private boolean showFps = true;
     private boolean showStats = true;
     private boolean darkenBehind = true;
-    
+
     protected Node guiNode;
     protected float secondCounter = 0.0f;
     protected int frameCounter = 0;
@@ -68,7 +68,7 @@ public class StatsAppState extends AbstractAppState {
     protected Geometry darkenStats;
 
     public StatsAppState() {
-    }    
+    }
 
     public StatsAppState( Node guiNode, BitmapFont guiFont ) {
         this.guiNode = guiNode;
@@ -89,7 +89,7 @@ public class StatsAppState extends AbstractAppState {
     public BitmapText getFpsText() {
         return fpsText;
     }
-    
+
     public StatsView getStatsView() {
         return statsView;
     }
@@ -110,7 +110,7 @@ public class StatsAppState extends AbstractAppState {
             if (darkenFps != null) {
                 darkenFps.setCullHint(showFps && darkenBehind ? CullHint.Never : CullHint.Always);
             }
-            
+
         }
     }
 
@@ -138,7 +138,7 @@ public class StatsAppState extends AbstractAppState {
     public void initialize(AppStateManager stateManager, Application app) {
         super.initialize(stateManager, app);
         this.app = app;
-               
+
         if (app instanceof SimpleApplication) {
             SimpleApplication simpleApp = (SimpleApplication)app;
             if (guiNode == null) {
@@ -147,21 +147,21 @@ public class StatsAppState extends AbstractAppState {
             if (guiFont == null ) {
                 guiFont = simpleApp.guiFont;
             }
-        } 
-        
+        }
+
         if (guiNode == null) {
             throw new RuntimeException( "No guiNode specific and cannot be automatically determined." );
-        } 
-        
+        }
+
         if (guiFont == null) {
             guiFont = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
         }
-        
-        loadFpsText();  
-        loadStatsView();      
+
+        loadFpsText();
+        loadStatsView();
         loadDarken();
     }
-            
+
     /**
      * Attaches FPS statistics to guiNode and displays it on the screen.
      *
@@ -170,12 +170,12 @@ public class StatsAppState extends AbstractAppState {
         if (fpsText == null) {
             fpsText = new BitmapText(guiFont, false);
         }
-        
+
         fpsText.setLocalTranslation(0, fpsText.getLineHeight(), 0);
         fpsText.setText("Frames per second");
         fpsText.setCullHint(showFps ? CullHint.Never : CullHint.Always);
         guiNode.attachChild(fpsText);
-        
+
     }
 
     /**
@@ -184,53 +184,53 @@ public class StatsAppState extends AbstractAppState {
      *
      */
     public void loadStatsView() {
-        statsView = new StatsView("Statistics View", 
-                                  app.getAssetManager(), 
+        statsView = new StatsView("Statistics View",
+                                  app.getAssetManager(),
                                   app.getRenderer().getStatistics());
         // move it up so it appears above fps text
         statsView.setLocalTranslation(0, fpsText.getLineHeight(), 0);
         statsView.setEnabled(showStats);
-        statsView.setCullHint(showStats ? CullHint.Never : CullHint.Always);        
+        statsView.setCullHint(showStats ? CullHint.Never : CullHint.Always);
         guiNode.attachChild(statsView);
     }
-        
+
     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.getAdditionalRenderState().setBlendMode(BlendMode.Alpha);
-        
+
         darkenFps = new Geometry("StatsDarken", new Quad(200, fpsText.getLineHeight()));
         darkenFps.setMaterial(mat);
         darkenFps.setLocalTranslation(0, 0, -1);
         darkenFps.setCullHint(showFps && darkenBehind ? CullHint.Never : CullHint.Always);
         guiNode.attachChild(darkenFps);
-        
+
         darkenStats = new Geometry("StatsDarken", new Quad(200, statsView.getHeight()));
         darkenStats.setMaterial(mat);
         darkenStats.setLocalTranslation(0, fpsText.getHeight(), -1);
         darkenStats.setCullHint(showStats && darkenBehind ? CullHint.Never : CullHint.Always);
         guiNode.attachChild(darkenStats);
     }
-    
+
     @Override
     public void setEnabled(boolean enabled) {
         super.setEnabled(enabled);
-        
+
         if (enabled) {
             fpsText.setCullHint(showFps ? CullHint.Never : CullHint.Always);
             darkenFps.setCullHint(showFps && darkenBehind ? CullHint.Never : CullHint.Always);
             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);
         } else {
             fpsText.setCullHint(CullHint.Always);
             darkenFps.setCullHint(CullHint.Always);
             statsView.setEnabled(false);
-            statsView.setCullHint(CullHint.Always);        
+            statsView.setCullHint(CullHint.Always);
             darkenStats.setCullHint(CullHint.Always);
         }
     }
-    
+
     @Override
     public void update(float tpf) {
         if (showFps) {
@@ -241,14 +241,14 @@ public class StatsAppState extends AbstractAppState {
                 fpsText.setText("Frames per second: " + fps);
                 secondCounter = 0.0f;
                 frameCounter = 0;
-            }          
+            }
         }
     }
 
     @Override
     public void cleanup() {
         super.cleanup();
-        
+
         guiNode.detachChild(statsView);
         guiNode.detachChild(fpsText);
         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 boolean enabled = true;
-    
+
     private final StringBuilder stringBuilder = new StringBuilder();
 
     public StatsView(String name, AssetManager manager, Statistics stats){
@@ -95,22 +95,22 @@ public class StatsView extends Node implements Control, JmeCloneable {
     public float getHeight() {
         return statText.getLineHeight() * statLabels.length;
     }
-    
+
     public void update(float tpf) {
-    
-        if (!isEnabled()) 
+
+        if (!isEnabled())
             return;
-            
+
         statistics.getData(statData);
         stringBuilder.setLength(0);
-        
-        // Need to walk through it backwards, as the first label 
+
+        // Need to walk through it backwards, as the first label
         // should appear at the bottom, not the top.
         for (int i = statLabels.length - 1; i >= 0; i--) {
             stringBuilder.append(statLabels[i]).append(" = ").append(statData[i]).append('\n');
         }
         statText.setText(stringBuilder);
-        
+
         // Moved to ResetStatsState to make sure it is
         // done even if there is no StatsView or the StatsView
         // is disable.
@@ -122,16 +122,16 @@ public class StatsView extends Node implements Control, JmeCloneable {
         return (Control) spatial;
     }
 
-    @Override   
-    public Object jmeClone() {
+    @Override
+    public StatsView jmeClone() {
         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.");
     }
-         
+
     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.scene.Spatial;
 import com.jme3.scene.plugins.OBJLoader;
-import com.jme3.shader.Shader;
 import com.jme3.shader.ShaderGenerator;
-import com.jme3.shader.ShaderKey;
 import com.jme3.texture.Texture;
 import com.jme3.texture.plugins.TGALoader;
 import java.io.IOException;
@@ -320,13 +318,6 @@ public interface AssetManager {
      */
     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,
      * 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;
 
 import com.jme3.asset.cache.AssetCache;
-import com.jme3.asset.cache.SimpleAssetCache;
 import com.jme3.audio.AudioData;
 import com.jme3.audio.AudioKey;
 import com.jme3.font.BitmapFont;
@@ -42,9 +41,7 @@ import com.jme3.renderer.Caps;
 import com.jme3.scene.Spatial;
 import com.jme3.shader.Glsl100ShaderGenerator;
 import com.jme3.shader.Glsl150ShaderGenerator;
-import com.jme3.shader.Shader;
 import com.jme3.shader.ShaderGenerator;
-import com.jme3.shader.ShaderKey;
 import com.jme3.system.JmeSystem;
 import com.jme3.texture.Texture;
 import java.io.IOException;
@@ -431,36 +428,6 @@ public class DesktopAssetManager implements AssetManager {
         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}
      */

+ 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.scene.Node;
 import com.jme3.util.PlaceholderAssets;
+import com.jme3.util.clone.Cloner;
 import java.io.IOException;
 import java.util.logging.Level;
 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) }.
- * 
+ *
  * @author normenhansen
  * @author Kirill Vainer
  */
@@ -99,15 +100,15 @@ public class AudioNode extends Node implements AudioSource {
          * {@link AudioNode#play() } is called.
          */
         Playing,
-        
+
         /**
          * The audio node is currently paused.
          */
         Paused,
-        
+
         /**
          * 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.
          */
         Stopped,
@@ -121,14 +122,14 @@ public class AudioNode extends Node implements AudioSource {
 
     /**
      * Creates a new <code>AudioNode</code> with the given data and key.
-     * 
+     *
      * @param audioData The audio data contains the audio track to play.
      * @param audioKey The audio key that was used to load the AudioData
      */
     public AudioNode(AudioData audioData, AudioKey audioKey) {
         setAudioData(audioData, audioKey);
     }
-    
+
     /**
      * Creates a new <code>AudioNode</code> with the given audio file.
      * @param assetManager The asset manager to use to load the audio file
@@ -142,16 +143,16 @@ public class AudioNode extends Node implements AudioSource {
 
     /**
      * Creates a new <code>AudioNode</code> with the given audio file.
-     * 
+     *
      * @param assetManager The asset manager to use to load the audio file
      * @param name The filename of the audio file
-     * @param 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.
      * @param streamCache If stream is also true, then this specifies if
      * 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.
-     * 
+     *
      * @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) {
@@ -161,12 +162,12 @@ public class AudioNode extends Node implements AudioSource {
 
     /**
      * Creates a new <code>AudioNode</code> with the given audio file.
-     * 
+     *
      * @param assetManager The asset manager to use to load the audio file
      * @param name The filename of the audio file
-     * @param 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.
-     * 
+     *
      * @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) {
@@ -175,20 +176,20 @@ public class AudioNode extends Node implements AudioSource {
 
     /**
      * Creates a new <code>AudioNode</code> with the given audio file.
-     * 
+     *
      * @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 name The filename of the audio file
-     * 
+     *
      * @deprecated AudioRenderer parameter is ignored.
      */
     public AudioNode(AudioRenderer audioRenderer, AssetManager assetManager, String name) {
         this(assetManager, name, DataType.Buffer);
     }
-    
+
     /**
      * Creates a new <code>AudioNode</code> with the given audio file.
-     * 
+     *
      * @param assetManager The asset manager to use to load the audio file
      * @param name The filename of the audio file
      * @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) {
         this(assetManager, name, DataType.Buffer);
     }
-    
+
     protected AudioRenderer getRenderer() {
         AudioRenderer result = AudioContext.getAudioRenderer();
         if( result == null )
             throw new IllegalStateException( "No audio renderer available, make sure call is being performed on render thread." );
-        return result;            
+        return result;
     }
-    
+
     /**
      * 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
      * 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.
      */
     public void playInstance(){
@@ -226,21 +227,21 @@ public class AudioNode extends Node implements AudioSource {
         }
         getRenderer().playSourceInstance(this);
     }
-    
+
     /**
      * Stop playing the audio that was started with {@link AudioNode#play() }.
      */
     public void stop(){
         getRenderer().stopSource(this);
     }
-    
+
     /**
      * Pause the audio that was started with {@link AudioNode#play() }.
      */
     public void pause(){
         getRenderer().pauseSource(this);
     }
-    
+
     /**
      * Do not use.
      */
@@ -261,7 +262,7 @@ public class AudioNode extends Node implements AudioSource {
 
     /**
      * @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() {
         return dryFilter;
@@ -269,14 +270,14 @@ public class AudioNode extends Node implements AudioSource {
 
     /**
      * 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.
-     * 
+     *
      * See the relevent documentation for the {@link Filter} to determine
      * the effect.
-     * 
+     *
      * @param dryFilter The filter to set, or null to disable dry filter.
      */
     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
      * can only be called once, if for example the audio node was initialized
      * without an {@link AudioData}.
-     * 
+     *
      * @param audioData The audio data contains the audio track to play.
      * @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) }
      * 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() }
      * or {@link AudioNode#stop() } methods are called.
      */
@@ -339,7 +340,7 @@ public class AudioNode extends Node implements AudioSource {
         else
             return data.getDataType();
     }
-    
+
     /**
      * @return True if the audio will keep looping after it is done playing,
      * 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.
-     * 
+     *
      * @param loop True if the audio should keep looping after it is done playing.
      */
     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.
-     * 
-     * @see AudioNode#setPitch(float) 
+     *
+     * @see AudioNode#setPitch(float)
      */
     public float getPitch() {
         return pitch;
@@ -372,7 +373,7 @@ public class AudioNode extends Node implements AudioSource {
     /**
      * Set the pitch of the audio, also the speed of playback.
      * The value must be between 0.5 and 2.0.
-     * 
+     *
      * @param pitch The pitch to set.
      * @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.
-     * 
+     *
      * @see AudioNode#setVolume(float)
      */
     public float getVolume() {
@@ -397,9 +398,9 @@ public class AudioNode extends Node implements AudioSource {
 
     /**
      * Set the volume of this audio node.
-     * 
+     *
      * The volume is specified as gain. 1.0 is the default.
-     * 
+     *
      * @param volume The volume to set.
      * @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.
-     * 
+     *
      * @param timeOffset The time offset
      * @throws IllegalArgumentException If timeOffset is negative
      */
@@ -439,7 +440,7 @@ public class AudioNode extends Node implements AudioSource {
             play();
         }
     }
-    
+
     @Override
     public float getPlaybackTime() {
         if (channel >= 0)
@@ -451,10 +452,10 @@ public class AudioNode extends Node implements AudioSource {
     public Vector3f getPosition() {
         return getWorldTranslation();
     }
-    
+
     /**
      * @return The velocity of the audio node.
-     * 
+     *
      * @see AudioNode#setVelocity(com.jme3.math.Vector3f)
      */
     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
      * to be in meters. Does nothing if the audio node is not positional.
-     * 
+     *
      * @param velocity The velocity to set.
      * @see AudioNode#setPositional(boolean)
      */
@@ -476,7 +477,7 @@ public class AudioNode extends Node implements AudioSource {
 
     /**
      * @return True if reverb is enabled, otherwise false.
-     * 
+     *
      * @see AudioNode#setReverbEnabled(boolean)
      */
     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.
      * Does nothing if the audio node is not positional.
      * <br/>
-     * When enabled, the audio environment set with 
+     * When enabled, the audio environment set with
      * {@link AudioRenderer#setEnvironment(com.jme3.audio.Environment) }
      * will apply a reverb effect to the audio playing from this audio node.
-     * 
+     *
      * @param reverbEnabled True to enable reverb.
      */
     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.
-     * 
-     * @see AudioNode#setReverbFilter(com.jme3.audio.Filter) 
+     *
+     * @see AudioNode#setReverbFilter(com.jme3.audio.Filter)
      */
     public Filter getReverbFilter() {
         return reverbFilter;
@@ -515,7 +516,7 @@ public class AudioNode extends Node implements AudioSource {
      * The reverb filter will influence the reverberations
      * of the audio node playing. This only has an effect if
      * reverb is enabled.
-     * 
+     *
      * @param reverbFilter The reverb filter to set.
      * @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.
-     * 
+     *
      * @see AudioNode#setMaxDistance(float)
      */
     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
      * very quickly then set ref distance very short and leave this distance
      * very long.
-     * 
+     *
      * @param maxDistance The maximum playing distance.
      * @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.
-     * 
-     * @see AudioNode#setRefDistance(float) 
+     *
+     * @see AudioNode#setRefDistance(float)
      */
     public float getRefDistance() {
         return refDistance;
@@ -574,7 +575,7 @@ public class AudioNode extends Node implements AudioSource {
      * <br/>
      * The reference playing distance is the distance at which the
      * audio node will be exactly half of its volume.
-     * 
+     *
      * @param refDistance The reference playing distance.
      * @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
-     * 
-     * @see AudioNode#setDirectional(boolean) 
+     *
+     * @see AudioNode#setDirectional(boolean)
      */
     public boolean isDirectional() {
         return directional;
@@ -601,10 +602,10 @@ public class AudioNode extends Node implements AudioSource {
      * Set the audio node to be directional.
      * Does nothing if the audio node is not positional.
      * <br/>
-     * After setting directional, you should call 
+     * After setting directional, you should call
      * {@link AudioNode#setDirection(com.jme3.math.Vector3f) }
      * to set the audio node's direction.
-     * 
+     *
      * @param directional If the audio node is directional
      */
     public void setDirectional(boolean directional) {
@@ -615,7 +616,7 @@ public class AudioNode extends Node implements AudioSource {
 
     /**
      * @return The direction of this audio node.
-     * 
+     *
      * @see AudioNode#setDirection(com.jme3.math.Vector3f)
      */
     public Vector3f getDirection() {
@@ -625,9 +626,9 @@ public class AudioNode extends Node implements AudioSource {
     /**
      * Set the direction of this audio node.
      * 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) {
         this.direction = direction;
@@ -637,8 +638,8 @@ public class AudioNode extends Node implements AudioSource {
 
     /**
      * @return The directional audio node, cone inner angle.
-     * 
-     * @see AudioNode#setInnerAngle(float) 
+     *
+     * @see AudioNode#setInnerAngle(float)
      */
     public float getInnerAngle() {
         return innerAngle;
@@ -647,7 +648,7 @@ public class AudioNode extends Node implements AudioSource {
     /**
      * Set the directional audio node cone inner angle.
      * Does nothing if the audio node is not directional.
-     * 
+     *
      * @param innerAngle The cone inner angle.
      */
     public void setInnerAngle(float innerAngle) {
@@ -658,8 +659,8 @@ public class AudioNode extends Node implements AudioSource {
 
     /**
      * @return The directional audio node, cone outer angle.
-     * 
-     * @see AudioNode#setOuterAngle(float) 
+     *
+     * @see AudioNode#setOuterAngle(float)
      */
     public float getOuterAngle() {
         return outerAngle;
@@ -668,7 +669,7 @@ public class AudioNode extends Node implements AudioSource {
     /**
      * Set the directional audio node cone outer angle.
      * Does nothing if the audio node is not directional.
-     * 
+     *
      * @param outerAngle The cone outer angle.
      */
     public void setOuterAngle(float outerAngle) {
@@ -679,8 +680,8 @@ public class AudioNode extends Node implements AudioSource {
 
     /**
      * @return True if the audio node is positional.
-     * 
-     * @see AudioNode#setPositional(boolean) 
+     *
+     * @see AudioNode#setPositional(boolean)
      */
     public boolean isPositional() {
         return positional;
@@ -690,7 +691,7 @@ public class AudioNode extends Node implements AudioSource {
      * Set the audio node as positional.
      * The position, velocity, and distance parameters effect positional
      * audio nodes. Set to false if the audio node should play in "headspace".
-     * 
+     *
      * @param positional True if the audio node should be positional, otherwise
      * false if it should be headspace.
      */
@@ -707,7 +708,7 @@ public class AudioNode extends Node implements AudioSource {
         if ((refreshFlags & RF_TRANSFORM) != 0){
             updatePos = true;
         }
-        
+
         super.updateGeometricState();
 
         if (updatePos && channel >= 0)
@@ -717,13 +718,37 @@ public class AudioNode extends Node implements AudioSource {
     @Override
     public AudioNode clone(){
         AudioNode clone = (AudioNode) super.clone();
-        
+
         clone.direction = direction.clone();
         clone.velocity  = velocity.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
     public void write(JmeExporter ex) throws IOException {
         super.write(ex);
@@ -745,7 +770,7 @@ public class AudioNode extends Node implements AudioSource {
         oc.write(direction, "direction", null);
         oc.write(innerAngle, "inner_angle", 360);
         oc.write(outerAngle, "outer_angle", 360);
-        
+
         oc.write(positional, "positional", false);
     }
 
@@ -753,7 +778,7 @@ public class AudioNode extends Node implements AudioSource {
     public void read(JmeImporter im) throws IOException {
         super.read(im);
         InputCapsule ic = im.getCapsule(this);
-        
+
         // NOTE: In previous versions of jME3, audioKey was actually
         // written with the name "key". This has been changed
         // 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{
             audioKey = (AudioKey) ic.readSavable("audio_key", null);
         }
-        
+
         loop = ic.readBoolean("looping", false);
         volume = ic.readFloat("volume", 1);
         pitch = ic.readFloat("pitch", 1);
@@ -779,9 +804,9 @@ public class AudioNode extends Node implements AudioSource {
         direction = (Vector3f) ic.readSavable("direction", null);
         innerAngle = ic.readFloat("inner_angle", 360);
         outerAngle = ic.readFloat("outer_angle", 360);
-        
+
         positional = ic.readBoolean("positional", false);
-        
+
         if (audioKey != null) {
             try {
                 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.
  *
  * 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 float currentValue;
     protected Vector3f direction = new Vector3f();
-    protected Vector3f lookAt;
+    protected Vector3f lookAt = null;
     protected Vector3f upVector = Vector3f.UNIT_Y;
-    protected Quaternion rotation;
+    protected Quaternion rotation = null;
     protected Direction directionType = Direction.None;
     protected MotionPath path;
     private boolean isControl = true;
@@ -120,7 +120,6 @@ public class MotionEvent extends AbstractCinematicEvent implements Control, JmeC
      */
     public MotionEvent(Spatial spatial, MotionPath path) {
         super();
-        this.spatial = spatial;
         spatial.addControl(this);
         this.path = path;
     }
@@ -132,7 +131,6 @@ public class MotionEvent extends AbstractCinematicEvent implements Control, JmeC
      */
     public MotionEvent(Spatial spatial, MotionPath path, float initialDuration) {
         super(initialDuration);
-        this.spatial = spatial;
         spatial.addControl(this);
         this.path = path;
     }
@@ -144,7 +142,6 @@ public class MotionEvent extends AbstractCinematicEvent implements Control, JmeC
      */
     public MotionEvent(Spatial spatial, MotionPath path, LoopMode loopMode) {
         super();
-        this.spatial = spatial;
         spatial.addControl(this);
         this.path = path;
         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) {
         super(initialDuration);
-        this.spatial = spatial;
         spatial.addControl(this);
         this.path = path;
         this.loopMode = loopMode;
@@ -213,9 +209,9 @@ public class MotionEvent extends AbstractCinematicEvent implements Control, JmeC
     public void write(JmeExporter ex) throws IOException {
         super.write(ex);
         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(rotation, "rotation", Quaternion.IDENTITY);
+        oc.write(rotation, "rotation", null);
         oc.write(directionType, "directionType", Direction.None);
         oc.write(path, "path", null);
     }
@@ -224,9 +220,9 @@ public class MotionEvent extends AbstractCinematicEvent implements Control, JmeC
     public void read(JmeImporter im) throws IOException {
         super.read(im);
         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);
-        rotation = (Quaternion) in.readSavable("rotation", Quaternion.IDENTITY);
+        rotation = (Quaternion) in.readSavable("rotation", null);
         directionType = in.readEnum("directionType", Direction.class, Direction.None);
         path = (MotionPath) in.readSavable("path", null);
     }
@@ -278,14 +274,15 @@ public class MotionEvent extends AbstractCinematicEvent implements Control, JmeC
      */
     @Override
     public Control cloneForSpatial(Spatial spatial) {
-        MotionEvent control = new MotionEvent(spatial, path);
+        MotionEvent control = new MotionEvent();
+        control.setPath(path);
         control.playState = playState;
         control.currentWayPoint = currentWayPoint;
         control.currentValue = currentValue;
         control.direction = direction.clone();
-        control.lookAt = lookAt.clone();
+        control.lookAt = lookAt;
         control.upVector = upVector.clone();
-        control.rotation = rotation.clone();
+        control.rotation = rotation;
         control.initialDuration = initialDuration;
         control.speed = speed;
         control.loopMode = loopMode;
@@ -302,9 +299,9 @@ public class MotionEvent extends AbstractCinematicEvent implements Control, JmeC
         control.currentWayPoint = currentWayPoint;
         control.currentValue = currentValue;
         control.direction = direction.clone();
-        control.lookAt = lookAt.clone();
+        control.lookAt = lookAt;
         control.upVector = upVector.clone();
-        control.rotation = rotation.clone();
+        control.rotation = rotation;
         control.initialDuration = initialDuration;
         control.speed = speed;
         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,
  * such as fire, smoke, explosions and much more.
  * <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 ParticleEmitter#setParticleInfluencer(com.jme3.effect.influencers.ParticleInfluencer) }.
  * By default the implementation {@link DefaultParticleInfluencer} is used.
- * 
+ *
  * @author Kirill Vainer
  */
 public class ParticleEmitter extends Geometry {
@@ -100,7 +100,7 @@ public class ParticleEmitter extends Geometry {
     private Vector3f faceNormal = new Vector3f(Vector3f.NAN);
     private int imagesX = 1;
     private int imagesY = 1;
-   
+
     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 float startSize = 0.2f;
@@ -127,20 +127,20 @@ public class ParticleEmitter extends Geometry {
             // fixed automatically by ParticleEmitter.clone() method.
         }
 
-        @Override   
+        @Override
         public Object jmeClone() {
             try {
                 return super.clone();
             } catch( CloneNotSupportedException e ) {
                 throw new RuntimeException("Error cloning", e);
             }
-        }     
+        }
 
-        @Override   
-        public void cloneFields( Cloner cloner, Object original ) { 
+        @Override
+        public void cloneFields( Cloner cloner, Object original ) {
             this.parentEmitter = cloner.clone(parentEmitter);
         }
-             
+
         public void setSpatial(Spatial spatial) {
         }
 
@@ -174,6 +174,13 @@ public class ParticleEmitter extends Geometry {
 
     @Override
     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);
         clone.shape = shape.deepClone();
 
@@ -211,6 +218,44 @@ public class ParticleEmitter extends Geometry {
         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) {
         super(name);
         setBatchHint(BatchHint.Never);
@@ -225,7 +270,7 @@ public class ParticleEmitter extends Geometry {
 
         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
         shape = shape.deepClone();
         particleInfluencer = particleInfluencer.clone();
@@ -267,10 +312,10 @@ public class ParticleEmitter extends Geometry {
 
     /**
      * 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.
-     * 
+     *
      * @see 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.
-     * 
-     * @return the {@link ParticleInfluencer} that influences this 
+     *
+     * @return the {@link ParticleInfluencer} that influences this
      * particle emitter.
-     * 
+     *
      * @see ParticleInfluencer
      */
     public ParticleInfluencer getParticleInfluencer() {
@@ -292,12 +337,12 @@ public class ParticleEmitter extends Geometry {
 
     /**
      * Returns 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 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() {
         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() {
         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,
      * then particles that have already spawned won't be effected by this
      * motion. If set to false, the particles will emit in local space
      * and when the emitter is moved, so are all the particles that
      * 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) {
         this.setIgnoreTransform(worldSpace);
@@ -353,7 +398,7 @@ public class ParticleEmitter extends Geometry {
 
     /**
      * Returns the number of visible particles (spawned but not dead).
-     * 
+     *
      * @return the number of visible particles
      */
     public int getNumVisibleParticles() {
@@ -365,7 +410,7 @@ public class ParticleEmitter extends Geometry {
      * Set the maximum amount of particles that
      * can exist at the same time with this emitter.
      * Calling this method many times is not recommended.
-     * 
+     *
      * @param numParticles the maximum amount of particles that
      * 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).
-     * 
+     *
      * <p>
      * This includes both existing and non-existing particles.
      * The size of the array is set to the <code>numParticles</code> value
      * specified in the constructor or {@link ParticleEmitter#setNumParticles(int) }
-     * method. 
-     * 
+     * method.
+     *
      * @return a list of all particles.
      */
     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() {
         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
      * will face the camera, but for some effects (e.g shockwave) it may
      * 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.
-     * 
+     *
      * @return the rotation speed in radians/sec for particles.
-     * 
-     * @see ParticleEmitter#setRotateSpeed(float) 
+     *
+     * @see ParticleEmitter#setRotateSpeed(float)
      */
     public float getRotateSpeed() {
         return rotateSpeed;
@@ -449,7 +494,7 @@ public class ParticleEmitter extends Geometry {
     /**
      * Set the rotation speed in radians/sec for particles
      * spawned after the invocation of this method.
-     * 
+     *
      * @param rotateSpeed the rotation speed in radians/sec for particles
      * spawned after the invocation of this method.
      */
@@ -459,12 +504,12 @@ public class ParticleEmitter extends Geometry {
 
     /**
      * Returns true if every particle spawned
-     * should have a random facing angle. 
-     * 
+     * should have a random facing angle.
+     *
      * @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() {
         return randomAngle;
@@ -472,8 +517,8 @@ public class ParticleEmitter extends Geometry {
 
     /**
      * 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
      * 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
      * image.
-     * 
+     *
      * @return True if every particle spawned should get a random
      * image.
-     * 
-     * @see ParticleEmitter#setSelectRandomImage(boolean) 
+     *
+     * @see ParticleEmitter#setSelectRandomImage(boolean)
      */
     public boolean isSelectRandomImage() {
         return selectRandomImage;
@@ -498,7 +543,7 @@ public class ParticleEmitter extends Geometry {
      * Set to true if every particle spawned
      * should get a random image from a pool of images constructed from
      * the texture, with X by Y possible images.
-     * 
+     *
      * <p>By default, X and Y are equal
      * 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) }
@@ -506,7 +551,7 @@ public class ParticleEmitter extends Geometry {
      * 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
      * the particle reaches its end of life.
-     * 
+     *
      * @param selectRandomImage True if every particle spawned should get a random
      * image.
      */
@@ -516,10 +561,10 @@ public class ParticleEmitter extends Geometry {
 
     /**
      * Check 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() {
         return facingVelocity;
@@ -528,11 +573,11 @@ public class ParticleEmitter extends Geometry {
     /**
      * Set to true if particles spawned should face
      * their velocity (or direction to which they are moving towards).
-     * 
+     *
      * <p>This is typically used for e.g spark effects.
-     * 
+     *
      * @param followVelocity True if particles spawned should face their velocity.
-     * 
+     *
      */
     public void setFacingVelocity(boolean followVelocity) {
         this.facingVelocity = followVelocity;
@@ -540,10 +585,10 @@ public class ParticleEmitter extends Geometry {
 
     /**
      * Get 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() {
         return endColor;
@@ -551,12 +596,12 @@ public class ParticleEmitter extends Geometry {
 
     /**
      * Set the end color of the particles spawned.
-     * 
+     *
      * <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
      * to its end of life.
-     * 
+     *
      * @param endColor the end color of the particles spawned.
      */
     public void setEndColor(ColorRGBA endColor) {
@@ -565,10 +610,10 @@ public class ParticleEmitter extends Geometry {
 
     /**
      * Get 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() {
         return endSize;
@@ -576,12 +621,12 @@ public class ParticleEmitter extends Geometry {
 
     /**
      * Set the end size of the particles spawned.
-     * 
+     *
      * <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
      * to its end of life.
-     * 
+     *
      * @param endSize the end size of the particles spawned.
      */
     public void setEndSize(float endSize) {
@@ -590,10 +635,10 @@ public class ParticleEmitter extends Geometry {
 
     /**
      * Get the gravity vector.
-     * 
+     *
      * @return the gravity vector.
-     * 
-     * @see ParticleEmitter#setGravity(com.jme3.math.Vector3f) 
+     *
+     * @see ParticleEmitter#setGravity(com.jme3.math.Vector3f)
      */
     public Vector3f getGravity() {
         return gravity;
@@ -601,7 +646,7 @@ public class ParticleEmitter extends Geometry {
 
     /**
      * This method sets the gravity vector.
-     * 
+     *
      * @param gravity the gravity vector
      */
     public void setGravity(Vector3f gravity) {
@@ -610,7 +655,7 @@ public class ParticleEmitter extends Geometry {
 
     /**
      * Sets the gravity vector.
-     * 
+     *
      * @param x the x component of the gravity vector
      * @param y the y 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.
-     * 
+     *
      * @return the high value of life.
-     * 
-     * @see ParticleEmitter#setHighLife(float) 
+     *
+     * @see ParticleEmitter#setHighLife(float)
      */
     public float getHighLife() {
         return highLife;
@@ -634,10 +679,10 @@ public class ParticleEmitter extends Geometry {
 
     /**
      * Set the high value of life.
-     * 
+     *
      * <p>The particle's lifetime/expiration
      * is determined by randomly selecting a time between low life and high life.
-     * 
+     *
      * @param highLife the high value of life.
      */
     public void setHighLife(float highLife) {
@@ -646,10 +691,10 @@ public class ParticleEmitter extends Geometry {
 
     /**
      * Get 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() {
         return imagesX;
@@ -657,11 +702,11 @@ public class ParticleEmitter extends Geometry {
 
     /**
      * Set the number of images along the X axis (width).
-     * 
+     *
      * <p>To determine
      * how multiple particle images are selected and used, see the
      * {@link ParticleEmitter#setSelectRandomImage(boolean) } method.
-     * 
+     *
      * @param imagesX the number of images along the X axis (width).
      */
     public void setImagesX(int imagesX) {
@@ -671,10 +716,10 @@ public class ParticleEmitter extends Geometry {
 
     /**
      * Get 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() {
         return imagesY;
@@ -682,10 +727,10 @@ public class ParticleEmitter extends Geometry {
 
     /**
      * Set the number of images along the Y axis (height).
-     * 
+     *
      * <p>To determine how multiple particle images are selected and used, see the
      * {@link ParticleEmitter#setSelectRandomImage(boolean) } method.
-     * 
+     *
      * @param imagesY the number of images along the Y axis (height).
      */
     public void setImagesY(int imagesY) {
@@ -695,10 +740,10 @@ public class ParticleEmitter extends Geometry {
 
     /**
      * Get the low value of life.
-     * 
+     *
      * @return the low value of life.
-     * 
-     * @see ParticleEmitter#setLowLife(float) 
+     *
+     * @see ParticleEmitter#setLowLife(float)
      */
     public float getLowLife() {
         return lowLife;
@@ -706,10 +751,10 @@ public class ParticleEmitter extends Geometry {
 
     /**
      * Set the low value of life.
-     * 
+     *
      * <p>The particle's lifetime/expiration
      * is determined by randomly selecting a time between low life and high life.
-     * 
+     *
      * @param lowLife the low value of life.
      */
     public void setLowLife(float lowLife) {
@@ -719,11 +764,11 @@ public class ParticleEmitter extends Geometry {
     /**
      * Get the number of particles to spawn per
      * second.
-     * 
+     *
      * @return the number of particles to spawn per
      * second.
-     * 
-     * @see ParticleEmitter#setParticlesPerSec(float) 
+     *
+     * @see ParticleEmitter#setParticlesPerSec(float)
      */
     public float getParticlesPerSec() {
         return particlesPerSec;
@@ -732,7 +777,7 @@ public class ParticleEmitter extends Geometry {
     /**
      * Set the number of particles to spawn per
      * second.
-     * 
+     *
      * @param particlesPerSec the number of particles to spawn per
      * second.
      */
@@ -740,13 +785,13 @@ public class ParticleEmitter extends Geometry {
         this.particlesPerSec = particlesPerSec;
         timeDifference = 0;
     }
-    
+
     /**
      * Get 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() {
         return startColor;
@@ -754,11 +799,11 @@ public class ParticleEmitter extends Geometry {
 
     /**
      * Set the start color of the particles spawned.
-     * 
+     *
      * <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
      * to its end of life.
-     * 
+     *
      * @param startColor the start color of the particles spawned
      */
     public void setStartColor(ColorRGBA startColor) {
@@ -767,10 +812,10 @@ public class ParticleEmitter extends Geometry {
 
     /**
      * Get 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() {
         return startSize;
@@ -778,11 +823,11 @@ public class ParticleEmitter extends Geometry {
 
     /**
      * Set the start size of the particles spawned.
-     * 
+     *
      * <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
      * to its end of life.
-     * 
+     *
      * @param startSize the start size of the particles spawned.
      */
     public void setStartSize(float startSize) {
@@ -805,10 +850,10 @@ public class ParticleEmitter extends Geometry {
      * gravity.
      *
      * @deprecated
-     * This method is deprecated. 
+     * This method is deprecated.
      * Use ParticleEmitter.getParticleInfluencer().setInitialVelocity(initialVelocity); instead.
      *
-     * @see ParticleEmitter#setVelocityVariation(float) 
+     * @see ParticleEmitter#setVelocityVariation(float)
      * @see ParticleEmitter#setGravity(float)
      */
     @Deprecated
@@ -818,7 +863,7 @@ public class ParticleEmitter extends Geometry {
 
     /**
      * @deprecated
-     * This method is deprecated. 
+     * This method is deprecated.
      * Use ParticleEmitter.getParticleInfluencer().getVelocityVariation(); instead.
      * @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
      * the velocity given in {@link ParticleEmitter#setStartVel(com.jme3.math.Vector3f) },
      * and 1 means particles are to spawn with a completely random velocity.
-     * 
+     *
      * @deprecated
-     * This method is deprecated. 
+     * This method is deprecated.
      * Use ParticleEmitter.getParticleInfluencer().setVelocityVariation(variation); instead.
      */
     @Deprecated
@@ -923,7 +968,7 @@ public class ParticleEmitter extends Geometry {
 
         vars.release();
     }
-    
+
     /**
      * Instantly kills all active particles, after this method is called, all
      * particles will be dead and no longer visible.
@@ -935,12 +980,12 @@ public class ParticleEmitter extends Geometry {
             }
         }
     }
-    
+
     /**
      * Kills the particle at the given index.
-     * 
+     *
      * @param index The index of the particle to kill
-     * @see #getParticles() 
+     * @see #getParticles()
      */
     public void killParticle(int index){
         freeParticle(index);
@@ -995,7 +1040,7 @@ public class ParticleEmitter extends Geometry {
             p.imageIndex = (int) (b * imagesX * imagesY);
         }
     }
-    
+
     private void updateParticleState(float tpf) {
         // Force world transform to update
         this.getWorldTransform();
@@ -1028,7 +1073,7 @@ public class ParticleEmitter extends Geometry {
                 firstUnUsed++;
             }
         }
-        
+
         // Spawns particles within the tpf timeslot with proper age
         float interval = 1f / particlesPerSec;
         float originalTpf = tpf;
@@ -1065,10 +1110,10 @@ public class ParticleEmitter extends Geometry {
 
     /**
      * Set to enable or disable the particle emitter
-     * 
+     *
      * <p>When a particle is
      * disabled, it will be "frozen in time" and not update.
-     * 
+     *
      * @param enabled True to enable the particle emitter
      */
     public void setEnabled(boolean enabled) {
@@ -1077,10 +1122,10 @@ public class ParticleEmitter extends Geometry {
 
     /**
      * Check 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() {
         return enabled;
@@ -1088,7 +1133,7 @@ public class ParticleEmitter extends Geometry {
 
     /**
      * Callback from Control.update(), do not use.
-     * @param tpf 
+     * @param tpf
      */
     public void updateFromControl(float tpf) {
         if (enabled) {
@@ -1098,9 +1143,9 @@ public class ParticleEmitter extends Geometry {
 
     /**
      * Callback from Control.render(), do not use.
-     * 
+     *
      * @param rm
-     * @param vp 
+     * @param vp
      */
     private void renderFromControl(RenderManager rm, ViewPort vp) {
         Camera cam = vp.getCamera();
@@ -1236,7 +1281,7 @@ public class ParticleEmitter extends Geometry {
                 gravity.y = ic.readFloat("gravity", 0);
             }
         } else {
-            // since the parentEmitter is not loaded, it must be 
+            // since the parentEmitter is not loaded, it must be
             // loaded separately
             control = getControl(ParticleEmitterControl.class);
             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.math.FastMath;
 import com.jme3.math.Vector3f;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 
 /**
@@ -49,7 +51,7 @@ import java.io.IOException;
  */
 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
     public static final int SAVABLE_VERSION = 1;
     /** Temporary variable used to help with calculations. */
@@ -94,7 +96,7 @@ public class DefaultParticleInfluencer implements ParticleInfluencer {
             initialVelocity = (Vector3f) ic.readSavable("startVelocity", Vector3f.ZERO.clone());
         }else{
             initialVelocity = (Vector3f) ic.readSavable("initialVelocity", Vector3f.ZERO.clone());
-        }       
+        }
         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
     public void setInitialVelocity(Vector3f 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.JmeImporter;
 import com.jme3.math.Vector3f;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 
 /**
@@ -83,4 +85,23 @@ public class EmptyParticleInfluencer implements ParticleInfluencer {
             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.math.FastMath;
 import com.jme3.math.Matrix3f;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 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.export.Savable;
 import com.jme3.math.Vector3f;
+import com.jme3.util.clone.JmeCloneable;
 
 /**
  * An interface that defines the methods to affect initial velocity of the particles.
  * @author Marcin Roguski (Kaelthas)
  */
-public interface ParticleInfluencer extends Savable, Cloneable {
+public interface ParticleInfluencer extends Savable, Cloneable, JmeCloneable {
 
     /**
      * 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.math.FastMath;
 import com.jme3.math.Vector3f;
+import com.jme3.util.clone.Cloner;
 import java.io.IOException;
 
 /**
@@ -81,7 +82,7 @@ public class RadialParticleInfluencer extends DefaultParticleInfluencer {
 
     /**
      * the origin used for computing the radial velocity direction
-     * @param origin 
+     * @param origin
      */
     public void setOrigin(Vector3f origin) {
         this.origin = origin;
@@ -97,7 +98,7 @@ public class RadialParticleInfluencer extends DefaultParticleInfluencer {
 
     /**
      * the radial velocity
-     * @param radialVelocity 
+     * @param radialVelocity
      */
     public void setRadialVelocity(float 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
-     * @return 
+     * @return
      */
     public boolean isHorizontal() {
         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
-     * @param horizontal 
+     * @param horizontal
      */
     public void setHorizontal(boolean 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
     public void write(JmeExporter ex) throws IOException {
         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.math.FastMath;
 import com.jme3.math.Vector3f;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 
 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() {
         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.VertexBuffer.Type;
 import com.jme3.util.BufferUtils;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 import java.util.ArrayList;
 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
     public void write(JmeExporter ex) throws IOException {
         OutputCapsule oc = ex.getCapsule(this);
@@ -180,7 +203,7 @@ public class EmitterMeshVertexShape implements EmitterShape {
     public void read(JmeImporter im) throws IOException {
         InputCapsule ic = im.getCapsule(this);
         this.vertices = ic.readSavableArrayList("vertices", null);
-        
+
         List<List<Vector3f>> tmpNormals = ic.readSavableArrayList("normals", null);
         if (tmpNormals != null){
             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.OutputCapsule;
 import com.jme3.math.Vector3f;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 
 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
     public void getRandomPoint(Vector3f store) {
         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.math.Vector3f;
+import com.jme3.util.clone.JmeCloneable;
 
 /**
  * This interface declares methods used by all shapes that represent particle emitters.
  * @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.

+ 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.math.FastMath;
 import com.jme3.math.Vector3f;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 
 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
     public void getRandomPoint(Vector3f store) {
         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.renderer.RenderManager;
 import com.jme3.scene.Node;
+import com.jme3.util.clone.Cloner;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -84,6 +85,27 @@ public class BitmapText extends Node {
         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() {
         return font;
     }
@@ -115,10 +137,10 @@ public class BitmapText extends Node {
      *
      * @param text String to change text to
      */
-    public void setText(String text) {            
+    public void setText(String text) {
         text = text == null ? "" : text;
 
-        if (text == block.getText() || block.getText().equals(text)) { 	
+        if (text == block.getText() || block.getText().equals(text)) {
             return;
         }
 
@@ -126,24 +148,24 @@ public class BitmapText extends Node {
         The problem with the below block is that StringBlock carries
         pretty much all of the text-related state of the BitmapText such
         as size, text box, alignment, etc.
-        
+
         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.
-        
+
         If block.setText("") doesn't do the right thing then that's where
         the fix should go because StringBlock carries too much information to
         be blown away every time.  -pspeed
-        
+
         Change was made:
         http://code.google.com/p/jmonkeyengine/source/detail?spec=svn9389&r=9389
         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
-        
+
         // If the text is empty, reset
         if (text.isEmpty()) {
             detachAllChildren();
-                
+
             for (int page = 0; page < textPages.length; page++) {
                 textPages[page] = new BitmapTextPage(font, true, page);
                 attachChild(textPages[page]);
@@ -153,7 +175,7 @@ public class BitmapText extends Node {
             letters = new Letters(font, block, letters.getQuad().isRightToLeft());
         }
         */
-            
+
         // Update the text content
         block.setText(text);
         letters.setText(text);
@@ -185,7 +207,7 @@ public class BitmapText extends Node {
         letters.invalidate(); // TODO: Don't have to align.
         needRefresh = true;
     }
-    
+
     /**
      *  Sets an overall alpha that will be applied to all
      *  letters.  If the alpha passed is -1 then alpha reverts
@@ -196,7 +218,7 @@ public class BitmapText extends Node {
     public void setAlpha(float alpha) {
         letters.setBaseAlpha(alpha);
         needRefresh = true;
-    }    
+    }
 
     public float getAlpha() {
         return letters.getBaseAlpha();
@@ -414,17 +436,17 @@ public class BitmapText extends Node {
         if( mp == null ) {
             return null;
         }
-        return (ColorRGBA)mp.getValue(); 
+        return (ColorRGBA)mp.getValue();
     }
 
     public void render(RenderManager rm, ColorRGBA color) {
         for (BitmapTextPage page : textPages) {
             Material mat = page.getMaterial();
             mat.setTexture("ColorMap", page.getTexture());
-            //ColorRGBA original = getColor(mat, "Color");            
+            //ColorRGBA original = getColor(mat, "Color");
             //mat.setColor("Color", color);
             mat.render(page, rm);
-            
+
             //if( original == null ) {
             //    mat.clearParam("Color");
             //} else {

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

@@ -123,6 +123,13 @@ class BitmapTextPage extends Geometry {
         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) {
         pageQuads.clear();
         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 float baseAlpha = -1;
     private String plainText;
-    
+
     Letters(BitmapFont font, StringBlock bound, boolean rightToLeft) {
         final String text = bound.getText();
         this.block = bound;
@@ -78,10 +78,10 @@ class Letters {
                     // Give the letter a default color if
                     // one has been provided.
                     l.setColor( baseColor );
-                }                
+                }
             }
         }
-        
+
         LinkedList<Range> ranges = colorTags.getTags();
         if (!ranges.isEmpty()) {
             for (int i = 0; i < ranges.size()-1; i++) {
@@ -92,7 +92,7 @@ class Letters {
             Range end = ranges.getLast();
             setColor(end.start, plainText.length(), end.color);
         }
-        
+
         invalidate();
     }
 
@@ -103,17 +103,17 @@ class Letters {
     LetterQuad getTail() {
         return tail;
     }
-    
+
     void update() {
         LetterQuad l = head;
         int lineCount = 1;
         BitmapCharacter ellipsis = font.getCharSet().getCharacter(block.getEllipsisChar());
         float ellipsisWidth = ellipsis!=null? ellipsis.getWidth()*getScale(): 0;
- 
+
         while (!l.isTail()) {
             if (l.isInvalid()) {
                 l.update(block);
-                
+
                 if (l.isInvalid(block)) {
                     switch (block.getLineWrapMode()) {
                     case Character:
@@ -144,7 +144,7 @@ class Letters {
                             }
                         }
                         break;
-                    case NoWrap: 
+                    case NoWrap:
                         LetterQuad cursor = l.getPrevious();
                         while (cursor.isInvalid(block, ellipsisWidth) && !cursor.isLineStart()) {
                             cursor = cursor.getPrevious();
@@ -158,10 +158,10 @@ class Letters {
                             cursor = cursor.getNext();
                         }
                         break;
-                    case Clip: 
+                    case Clip:
                         // Clip the character that falls out of bounds
                         l.clip(block);
-                        
+
                         // Clear the rest up to the next line feed.
                         for( LetterQuad q = l.getNext(); !q.isTail() && !q.isLineFeed(); q = q.getNext() ) {
                             q.setBitmapChar(null);
@@ -178,12 +178,12 @@ class Letters {
             }
             l = l.getNext();
         }
-        
+
         align();
         block.setLineCount(lineCount);
         rewind();
     }
-    
+
     private void align() {
         final Align alignment = block.getAlignment();
         final VAlign valignment = block.getVerticalAlignment();
@@ -233,7 +233,7 @@ class Letters {
         l.invalidate();
         l.update(block); // TODO: update from l
     }
-    
+
     float getCharacterX0() {
         return current.getX0();
     }
@@ -241,54 +241,54 @@ class Letters {
     float getCharacterY0() {
         return current.getY0();
     }
-    
+
     float getCharacterX1() {
         return current.getX1();
     }
-    
+
     float getCharacterY1() {
         return current.getY1();
     }
-    
+
     float getCharacterAlignX() {
         return current.getAlignX();
     }
-    
+
     float getCharacterAlignY() {
         return current.getAlignY();
     }
-    
+
     float getCharacterWidth() {
         return current.getWidth();
     }
-    
+
     float getCharacterHeight() {
         return current.getHeight();
     }
-    
+
     public boolean nextCharacter() {
         if (current.isTail())
             return false;
         current = current.getNext();
         return true;
     }
-    
+
     public int getCharacterSetPage() {
         return current.getBitmapChar().getPage();
     }
-    
+
     public LetterQuad getQuad() {
         return current;
     }
-    
+
     public void rewind() {
         current = head;
     }
-    
+
     public void invalidate() {
         invalidate(head);
     }
-    
+
     public void invalidate(LetterQuad cursor) {
         totalWidth = -1;
         totalHeight = -1;
@@ -298,7 +298,7 @@ class Letters {
             cursor = cursor.getNext();
         }
     }
-    
+
     float getScale() {
         return block.getSize() / font.getCharSet().getRenderedSize();
     }
@@ -306,7 +306,7 @@ class Letters {
     public boolean isPrintable() {
         return current.getBitmapChar() != null;
     }
-    
+
     float getTotalWidth() {
         validateSize();
         return totalWidth;
@@ -316,7 +316,7 @@ class Letters {
         validateSize();
         return totalHeight;
     }
-    
+
     void validateSize() {
         if (totalWidth < 0) {
             LetterQuad l = head;
@@ -371,11 +371,11 @@ class Letters {
             cursor = cursor.getNext();
         }
     }
- 
+
     float getBaseAlpha() {
         return baseAlpha;
     }
-    
+
     void setBaseAlpha( float alpha ) {        this.baseAlpha = alpha;
         colorTags.setBaseAlpha(alpha);
 
@@ -409,7 +409,7 @@ class Letters {
                 setColor(end.start, plainText.length(), end.color);
             }
         }
-        
+
         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;
     }
 
+    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.
      * <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.scene.Spatial;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 import com.jme3.util.SortUtil;
 import java.io.IOException;
 import java.util.*;
@@ -40,10 +42,10 @@ import java.util.*;
 /**
  * <code>LightList</code> is used internally by {@link Spatial}s to manage
  * lights that are attached to them.
- * 
+ *
  * @author Kirill Vainer
  */
-public final class LightList implements Iterable<Light>, Savable, Cloneable {
+public final class LightList implements Iterable<Light>, Savable, Cloneable, JmeCloneable {
 
     private Light[] list, tlist;
     private float[] distToOwner;
@@ -74,7 +76,7 @@ public final class LightList implements Iterable<Light>, Savable, Cloneable {
 
     /**
      * Creates a <code>LightList</code> for the given {@link Spatial}.
-     * 
+     *
      * @param owner The spatial owner
      */
     public LightList(Spatial owner) {
@@ -87,7 +89,7 @@ public final class LightList implements Iterable<Light>, Savable, Cloneable {
 
     /**
      * Set the owner of the LightList. Only used for cloning.
-     * @param owner 
+     * @param owner
      */
     public void setOwner(Spatial owner){
         this.owner = owner;
@@ -118,7 +120,7 @@ public final class LightList implements Iterable<Light>, Savable, Cloneable {
 
     /**
      * Remove the light at the given index.
-     * 
+     *
      * @param index
      */
     public void remove(int index){
@@ -139,7 +141,7 @@ public final class LightList implements Iterable<Light>, Savable, Cloneable {
 
     /**
      * Removes the given light from the LightList.
-     * 
+     *
      * @param l the light to remove
      */
     public void remove(Light l){
@@ -187,12 +189,12 @@ public final class LightList implements Iterable<Light>, Savable, Cloneable {
 
     /**
      * Sorts the elements in the list according to their Comparator.
-     * There are two reasons why lights should be resorted. 
-     * First, if the lights have moved, that means their distance to 
-     * the spatial changed. 
-     * Second, if the spatial itself moved, it means the distance from it to 
+     * There are two reasons why lights should be resorted.
+     * First, if the lights have moved, that means their distance to
+     * the spatial changed.
+     * Second, if the spatial itself moved, it means the distance from it to
      * the individual lights might have changed.
-     * 
+     *
      *
      * @param transformChanged Whether the spatial's transform has changed
      */
@@ -252,7 +254,7 @@ public final class LightList implements Iterable<Light>, Savable, Cloneable {
                 list[p] = parent.list[i];
                 distToOwner[p] = Float.NEGATIVE_INFINITY;
             }
-            
+
             listSize = local.listSize + parent.listSize;
         }else{
             listSize = local.listSize;
@@ -261,7 +263,7 @@ public final class LightList implements Iterable<Light>, Savable, Cloneable {
 
     /**
      * Returns an iterator that can be used to iterate over this LightList.
-     * 
+     *
      * @return an iterator that can be used to iterate over this LightList.
      */
     public Iterator<Light> iterator() {
@@ -276,10 +278,10 @@ public final class LightList implements Iterable<Light>, Savable, Cloneable {
             public Light next() {
                 if (!hasNext())
                     throw new NoSuchElementException();
-                
+
                 return list[index++];
             }
-            
+
             public void remove() {
                 LightList.this.remove(--index);
             }
@@ -290,7 +292,7 @@ public final class LightList implements Iterable<Light>, Savable, Cloneable {
     public LightList clone(){
         try{
             LightList clone = (LightList) super.clone();
-            
+
             clone.owner = null;
             clone.list = list.clone();
             clone.distToOwner = distToOwner.clone();
@@ -302,6 +304,24 @@ public final class LightList implements Iterable<Light>, Savable, Cloneable {
         }
     }
 
+    @Override
+    public LightList jmeClone() {
+        try{
+            LightList clone = (LightList)super.clone();
+            clone.tlist = null; // list used for sorting only
+            return clone;
+        }catch (CloneNotSupportedException ex){
+            throw new AssertionError();
+        }
+    }
+
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) {
+        this.owner = cloner.clone(owner);
+        this.list = cloner.clone(list);
+        this.distToOwner = cloner.clone(distToOwner);
+    }
+
     public void write(JmeExporter ex) throws IOException {
         OutputCapsule oc = ex.getCapsule(this);
 //        oc.write(owner, "owner", null);
@@ -319,7 +339,7 @@ public final class LightList implements Iterable<Light>, Savable, Cloneable {
 
         List<Light> lights = ic.readSavableArrayList("lights", null);
         listSize = lights.size();
-        
+
         // NOTE: make sure the array has a length of at least 1
         int arraySize = Math.max(DEFAULT_SIZE, listSize);
         list = new Light[arraySize];
@@ -328,7 +348,7 @@ public final class LightList implements Iterable<Light>, Savable, Cloneable {
         for (int i = 0; i < listSize; i++){
             list[i] = lights.get(i);
         }
-        
+
         Arrays.fill(distToOwner, Float.NEGATIVE_INFINITY);
     }
 

+ 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.export.*;
 import com.jme3.math.*;
-import com.jme3.renderer.Renderer;
 import com.jme3.shader.VarType;
 import com.jme3.texture.Texture;
 import com.jme3.texture.Texture.WrapMode;
@@ -129,9 +128,6 @@ public class MatParam implements Savable, Cloneable {
         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

+ 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;
     }
 
-    @Override
-    public void apply(Renderer r, Technique technique) {
-        TechniqueDef techDef = technique.getDef();
-        r.setTexture(getUnit(), getTextureValue());
-        technique.updateUniformParam(getPrefixedName(), getVarType(), getUnit());
-    }
 
     @Override
     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.RenderManager;
 import com.jme3.renderer.Renderer;
-import com.jme3.renderer.RendererException;
 import com.jme3.renderer.queue.RenderQueue.Bucket;
 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.Uniform;
+import com.jme3.shader.UniformBindingManager;
 import com.jme3.shader.VarType;
+import com.jme3.texture.Image;
 import com.jme3.texture.Texture;
 import com.jme3.texture.image.ColorSpace;
 import com.jme3.util.ListMap;
-import com.jme3.util.TempVars;
 import java.io.IOException;
 import java.util.*;
 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 RenderState additiveLight = new RenderState();
     private static final RenderState depthOnly = new RenderState();
-    private static final Quaternion nullDirLight = new Quaternion(0, -1, 0, -1);
 
     static {
         depthOnly.setDepthTest(true);
@@ -175,22 +172,29 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
      * @return The sorting ID used for sorting geometries for rendering.
      */
     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++) {
                 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;
     }
@@ -215,6 +219,8 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
                 mat.paramValues.put(entry.getKey(), entry.getValue().clone());
             }
 
+            mat.sortingId = -1;
+            
             return mat;
         } catch (CloneNotSupportedException 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)
      */
-    public ListMap getParamsMap() {
+    public ListMap<String, MatParam> getParamsMap() {
         return paramValues;
     }
 
@@ -695,257 +701,6 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
         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.
      * <p>
@@ -974,9 +729,8 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
         Technique tech = techniques.get(name);
         // When choosing technique, we choose one that
         // supports all the caps.
-        EnumSet<Caps> rendererCaps = renderManager.getRenderer().getCaps();
         if (tech == null) {
-
+            EnumSet<Caps> rendererCaps = renderManager.getRenderer().getCaps();
             if (name.equals("Default")) {
                 List<TechniqueDef> techDefs = def.getDefaultTechniques();
                 if (techDefs == null || techDefs.isEmpty()) {
@@ -1025,20 +779,71 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
         }
 
         technique = tech;
-        tech.makeCurrent(def.getAssetManager(), true, rendererCaps, renderManager);
+        tech.notifyTechniqueSwitched();
 
         // shader was changed
         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.
      * <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
      * 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) {
@@ -1141,80 +949,46 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
      * </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 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);
-        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.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 {
         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 stencilTest = false;
     boolean applyStencilTest = false;
+    float lineWidth = 1;
+    boolean applyLineWidth = false;
     TestFunction depthFunc = TestFunction.LessOrEqual;
     //by default depth func will be applied anyway if depth test is applied
     boolean applyDepthFunc = false;    
@@ -350,6 +352,9 @@ public class RenderState implements Cloneable, Savable {
         oc.write(backStencilDepthPassOperation, "backStencilDepthPassOperation", StencilOperation.Keep);
         oc.write(frontStencilFunction, "frontStencilFunction", TestFunction.Always);
         oc.write(backStencilFunction, "backStencilFunction", TestFunction.Always);
+        oc.write(depthFunc, "depthFunc", TestFunction.LessOrEqual);
+        oc.write(alphaFunc, "alphaFunc", TestFunction.Greater);
+        oc.write(lineWidth, "lineWidth", 1);
 
         // Only "additional render state" has them set to false by default
         oc.write(applyPointSprite, "applyPointSprite", true);
@@ -364,8 +369,7 @@ public class RenderState implements Cloneable, Savable {
         oc.write(applyPolyOffset, "applyPolyOffset", true);
         oc.write(applyDepthFunc, "applyDepthFunc", true);
         oc.write(applyAlphaFunc, "applyAlphaFunc", false);
-        oc.write(depthFunc, "depthFunc", TestFunction.LessOrEqual);
-        oc.write(alphaFunc, "alphaFunc", TestFunction.Greater);  
+        oc.write(applyLineWidth, "applyLineWidth", true);
 
     }
 
@@ -394,6 +398,8 @@ public class RenderState implements Cloneable, Savable {
         backStencilFunction = ic.readEnum("backStencilFunction", TestFunction.class, TestFunction.Always);
         depthFunc = ic.readEnum("depthFunc", TestFunction.class, TestFunction.LessOrEqual);
         alphaFunc = ic.readEnum("alphaFunc", TestFunction.class, TestFunction.Greater);
+        lineWidth = ic.readFloat("lineWidth", 1);
+
 
         applyPointSprite = ic.readBoolean("applyPointSprite", true);
         applyWireFrame = ic.readBoolean("applyWireFrame", true);
@@ -407,6 +413,8 @@ public class RenderState implements Cloneable, Savable {
         applyPolyOffset = ic.readBoolean("applyPolyOffset", true);
         applyDepthFunc = ic.readBoolean("applyDepthFunc", true);
         applyAlphaFunc = ic.readBoolean("applyAlphaFunc", false);
+        applyLineWidth = ic.readBoolean("applyLineWidth", true);
+
         
     }
 
@@ -528,6 +536,10 @@ public class RenderState implements Cloneable, Savable {
             }
         }
 
+        if(lineWidth != rs.lineWidth){
+            return false;
+        }
+
         return true;
     }
 
@@ -803,8 +815,20 @@ public class RenderState implements Cloneable, Savable {
         this.alphaFunc = alphaFunc;
         cachedHashCode = -1;
     }
-    
-    
+
+    /**
+     * Sets the mesh line width.
+     * This is to use in conjunction with {@link #setWireframe(boolean)} or with a mesh in {@link Mesh.Mode#Lines} mode.
+     * @param lineWidth the line width.
+     */
+    public void setLineWidth(float lineWidth) {
+        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.
@@ -1118,8 +1142,16 @@ public class RenderState implements Cloneable, Savable {
     public TestFunction getAlphaFunc() {
         return alphaFunc;
     }
-    
-    
+
+    /**
+     * returns the wireframe line width
+     *
+     * @return the line width
+     */
+    public float getLineWidth() {
+        return lineWidth;
+    }
+
 
     public boolean isApplyAlphaFallOff() {
         return applyAlphaFallOff;
@@ -1168,8 +1200,10 @@ public class RenderState implements Cloneable, Savable {
     public boolean isApplyAlphaFunc() {
         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.frontStencilFunction != null ? this.frontStencilFunction.hashCode() : 0);
             hash = 79 * hash + (this.backStencilFunction != null ? this.backStencilFunction.hashCode() : 0);
+            hash = 79 * hash + Float.floatToIntBits(this.lineWidth);
             cachedHashCode = hash;
         }
         return cachedHashCode;
@@ -1324,6 +1359,11 @@ public class RenderState implements Cloneable, Savable {
             state.frontStencilFunction = frontStencilFunction;
             state.backStencilFunction = backStencilFunction;
         }
+        if (additionalState.applyLineWidth) {
+            state.lineWidth = additionalState.lineWidth;
+        } else {
+            state.lineWidth = lineWidth;
+        }
         state.cachedHashCode = -1;
         return state;
     }
@@ -1351,6 +1391,7 @@ public class RenderState implements Cloneable, Savable {
         backStencilFunction = state.backStencilFunction;
         depthFunc = state.depthFunc;
         alphaFunc = state.alphaFunc;
+        lineWidth = state.lineWidth;
 
         applyPointSprite = true;
         applyWireFrame =  true;
@@ -1364,6 +1405,7 @@ public class RenderState implements Cloneable, Savable {
         applyPolyOffset =  true;
         applyDepthFunc =  true;
         applyAlphaFunc =  false;
+        applyLineWidth = true;
     }
 
     @Override
@@ -1392,7 +1434,8 @@ public class RenderState implements Cloneable, Savable {
                 + "\noffsetEnabled=" + offsetEnabled
                 + "\napplyPolyOffset=" + applyPolyOffset
                 + "\noffsetFactor=" + offsetFactor
-                + "\noffsetUnits=" + offsetUnits      
+                + "\noffsetUnits=" + offsetUnits
+                + "\nlineWidth=" + lineWidth
                 + "\n]";
     }
 }

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

@@ -31,27 +31,30 @@
  */
 package com.jme3.material;
 
+import com.jme3.material.logic.TechniqueDefLogic;
 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.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.EnumSet;
 import java.util.List;
-import java.util.logging.Logger;
 
 /**
  * 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
@@ -63,14 +66,8 @@ public class Technique /* implements Savable */ {
     public Technique(Material owner, TechniqueDef def) {
         this.owner = owner;
         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() {
-        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;
 
+import com.jme3.material.logic.TechniqueDefLogic;
+import com.jme3.asset.AssetManager;
 import com.jme3.export.*;
 import com.jme3.renderer.Caps;
 import com.jme3.shader.*;
+import com.jme3.shader.Shader.ShaderType;
 
 import java.io.IOException;
 import java.util.*;
@@ -93,11 +96,17 @@ public class TechniqueDef implements Savable {
 
     private EnumSet<Caps> requiredCaps = EnumSet.noneOf(Caps.class);
     private String name;
-
+    private int sortId;
+    
     private EnumMap<Shader.ShaderType,String> shaderLanguages;
     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 List<ShaderNode> shaderNodes;
     private ShaderGenerationInfo shaderGenerationInfo;
@@ -106,10 +115,10 @@ public class TechniqueDef implements Savable {
     private RenderState renderState;
     private RenderState forcedRenderState;
 
-    private LightMode lightMode   = LightMode.Disable;
+    private LightMode lightMode = LightMode.Disable;
     private ShadowMode shadowMode = ShadowMode.Disable;
+    private TechniqueDefLogic logic;
 
-    private HashMap<String, String> defineParams;
     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>
      * for default techniques.
      */
-    public TechniqueDef(String name){
+    public TechniqueDef(String name, int sortId){
         this();
+        this.sortId = sortId;
         this.name = name == null ? "Default" : name;
     }
 
     /**
      * 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) {
         this.lightMode = lightMode;
     }
+    
+    public void setLogic(TechniqueDefLogic logic) {
+        this.logic = logic;
+    }
 
+    public TechniqueDefLogic getLogic() {
+        return logic;
+    }
+    
     /**
      * Returns the shadow mode.
      * @return the shadow mode.
@@ -224,14 +254,6 @@ public class TechniqueDef implements Savable {
         return noRender;
     }
 
-    /**
-     * @deprecated jME3 always requires shaders now
-     */
-    @Deprecated
-    public boolean isUsingShaders(){
-        return true;
-    }
-
     /**
      * Returns true if this technique uses Shader Nodes, false otherwise.
      *
@@ -273,34 +295,24 @@ public class TechniqueDef implements Savable {
         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.
      *
@@ -310,60 +322,186 @@ public class TechniqueDef implements Savable {
      * @see #addShaderParamDefine(java.lang.String, java.lang.String)
      */
     public String getShaderParamDefine(String paramName){
-        if (defineParams == null) {
+        Integer defineId = paramToDefineId.get(paramName);
+        if (defineId != null) {
+            return defineNames.get(defineId);
+        } else {
             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.
      * <p>
      * Any time the material parameter on the parent material is altered,
      * 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 paramType The type of the material parameter to link to.
      * @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.TessellationEvaluation), "tsevalLanguage", null);
 
-        oc.write(presetDefines, "presetDefines", null);
+        oc.write(shaderPrologue, "shaderPrologue", null);
         oc.write(lightMode, "lightMode", LightMode.Disable);
         oc.write(shadowMode, "shadowMode", ShadowMode.Disable);
         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.TessellationControl,ic.readString("tsctrlName", 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);
         shadowMode = ic.readEnum("shadowMode", ShadowMode.class, ShadowMode.Disable);
         renderState = (RenderState) ic.readSavable("renderState", null);
@@ -547,9 +685,14 @@ public class TechniqueDef implements Savable {
         this.shaderGenerationInfo = shaderGenerationInfo;
     }
 
-    //todo: make toString return something usefull
     @Override
     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;
         this.curveTension = curveTension;
         this.cycle = cycle;
-        this.computeTotalLentgh();
+        this.computeTotalLength();
     }
 
     /**
@@ -116,7 +116,7 @@ public class Spline implements Savable {
         this.controlPoints.addAll(controlPoints);
         this.curveTension = curveTension;
         this.cycle = cycle;
-        this.computeTotalLentgh();
+        this.computeTotalLength();
     }
     
     /**
@@ -144,7 +144,7 @@ public class Spline implements Savable {
         	this.weights[i] = controlPoint.w;
         }
         CurveAndSurfaceMath.prepareNurbsKnots(knots, basisFunctionDegree);
-        this.computeTotalLentgh();
+        this.computeTotalLength();
     }
 
     private void initCatmullRomWayPoints(List<Vector3f> list) {
@@ -186,7 +186,7 @@ public class Spline implements Savable {
             controlPoints.add(controlPoints.get(0).clone());
         }
         if (controlPoints.size() > 1) {
-            this.computeTotalLentgh();
+            this.computeTotalLength();
         }
     }
 
@@ -197,7 +197,7 @@ public class Spline implements Savable {
     public void removeControlPoint(Vector3f controlPoint) {
         controlPoints.remove(controlPoint);
         if (controlPoints.size() > 1) {
-            this.computeTotalLentgh();
+            this.computeTotalLength();
         }
     }
     
@@ -209,7 +209,7 @@ public class Spline implements Savable {
     /**
      * This method computes the total length of the curve.
      */
-    private void computeTotalLentgh() {
+    private void computeTotalLength() {
         totalLength = 0;
         float l = 0;
         if (segmentsLength == null) {
@@ -317,7 +317,7 @@ public class Spline implements Savable {
     public void setCurveTension(float curveTension) {
         this.curveTension = curveTension;
         if(type==SplineType.CatmullRom && !getControlPoints().isEmpty()) {            
-        	this.computeTotalLentgh();
+        	this.computeTotalLength();
         }
     }
 
@@ -342,7 +342,7 @@ public class Spline implements Savable {
     				controlPoints.add(controlPoints.get(0));
     			}
     			this.cycle = cycle;
-    			this.computeTotalLentgh();
+    			this.computeTotalLength();
     		} else {
     			this.cycle = cycle;
     		}
@@ -369,7 +369,7 @@ public class Spline implements Savable {
      */
     public void setType(SplineType type) {
         this.type = type;
-        this.computeTotalLentgh();
+        this.computeTotalLength();
     }
 
     /**
@@ -435,9 +435,13 @@ public class Spline implements Savable {
         OutputCapsule oc = ex.getCapsule(this);
         oc.writeSavableArrayList((ArrayList) controlPoints, "controlPoints", null);
         oc.write(type, "type", SplineType.CatmullRom);
-        float list[] = new float[segmentsLength.size()];
-        for (int i = 0; i < segmentsLength.size(); i++) {
-            list[i] = segmentsLength.get(i);
+        
+        float list[] = null;
+        if (segmentsLength != null) {
+            list = new float[segmentsLength.size()];
+            for (int i = 0; i < segmentsLength.size(); i++) {
+                list[i] = segmentsLength.get(i);
+            }
         }
         oc.write(list, "segmentsLength", null);
 
@@ -454,7 +458,7 @@ public class Spline implements Savable {
     public void read(JmeImporter im) throws IOException {
         InputCapsule in = im.getCapsule(this);
 
-        controlPoints = (ArrayList<Vector3f>) in.readSavableArrayList("wayPoints", null);
+        controlPoints = (ArrayList<Vector3f>) in.readSavableArrayList("controlPoints", new ArrayList<Vector3f>()); /* Empty List as default, prevents null pointers */
         float list[] = in.readFloatArray("segmentsLength", null);
         if (list != null) {
             segmentsLength = new ArrayList<Float>();

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

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

+ 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.LightFilter;
 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.profile.AppProfiler;
 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.ShadowMode;
 import com.jme3.scene.*;
-import com.jme3.shader.Uniform;
+import com.jme3.shader.Shader;
 import com.jme3.shader.UniformBinding;
 import com.jme3.shader.UniformBindingManager;
 import com.jme3.system.NullRenderer;
 import com.jme3.system.Timer;
 import com.jme3.util.SafeArrayList;
-
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -480,8 +483,8 @@ public class RenderManager {
      * Updates the given list of uniforms with {@link UniformBinding uniform bindings}
      * 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);
             lightList = filteredLightList;
         }
+        
 
         //if forcedTechnique we try to force it for render,
         //if it does not exists in the mat def, we check for forcedMaterial and render the geom if not null
@@ -612,7 +616,9 @@ public class RenderManager {
 
             gm.getMaterial().preload(this);
             Mesh mesh = gm.getMesh();
-            if (mesh != null) {
+            if (mesh != null
+                    && mesh.getVertexCount() != 0
+                    && mesh.getTriangleCount() != 0) {
                 for (VertexBuffer vb : mesh.getBufferList().getArray()) {
                     if (vb.getData() != null && vb.getUsage() != VertexBuffer.Usage.CpuOnly) {
                         renderer.updateBufferData(vb);
@@ -637,8 +643,10 @@ public class RenderManager {
      * <p>
      * In addition to enqueuing the visible geometries, this method
      * also scenes which cast or receive shadows, by putting them into the
-     * RenderQueue's {@link RenderQueue#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
      * this process does not check for frustum culling on any 
      * {@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>
      * <li>If any objects remained in the render queue, they are removed
      * from the queue. This is generally objects added to the 
-     * {@link RenderQueue#renderShadowQueue(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>
      * </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 glBindVertexArray(int param1); /// 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 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);
         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("\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());
         }
 
@@ -779,6 +790,10 @@ public final class GLRenderer implements Renderer {
                 gl.glDisable(GL.GL_STENCIL_TEST);
             }
         }
+        if (context.lineWidth != state.getLineWidth()) {
+            gl.glLineWidth(state.getLineWidth());
+            context.lineWidth = state.getLineWidth();
+        }
     }
 
     private int convertStencilOperation(StencilOperation stencilOp) {
@@ -960,12 +975,12 @@ public final class GLRenderer implements Renderer {
                 gl.glUniform1i(loc, b.booleanValue() ? GL.GL_TRUE : GL.GL_FALSE);
                 break;
             case Matrix3:
-                fb = (FloatBuffer) uniform.getValue();
+                fb = uniform.getMultiData();
                 assert fb.remaining() == 9;
                 gl.glUniformMatrix3(loc, false, fb);
                 break;
             case Matrix4:
-                fb = (FloatBuffer) uniform.getValue();
+                fb = uniform.getMultiData();
                 assert fb.remaining() == 16;
                 gl.glUniformMatrix4(loc, false, fb);
                 break;
@@ -974,23 +989,23 @@ public final class GLRenderer implements Renderer {
                 gl.glUniform1(loc, ib);
                 break;
             case FloatArray:
-                fb = (FloatBuffer) uniform.getValue();
+                fb = uniform.getMultiData();
                 gl.glUniform1(loc, fb);
                 break;
             case Vector2Array:
-                fb = (FloatBuffer) uniform.getValue();
+                fb = uniform.getMultiData();
                 gl.glUniform2(loc, fb);
                 break;
             case Vector3Array:
-                fb = (FloatBuffer) uniform.getValue();
+                fb = uniform.getMultiData();
                 gl.glUniform3(loc, fb);
                 break;
             case Vector4Array:
-                fb = (FloatBuffer) uniform.getValue();
+                fb = uniform.getMultiData();
                 gl.glUniform4(loc, fb);
                 break;
             case Matrix4Array:
-                fb = (FloatBuffer) uniform.getValue();
+                fb = uniform.getMultiData();
                 gl.glUniformMatrix4(loc, false, fb);
                 break;
             case Int:
@@ -1438,11 +1453,19 @@ public final class GLRenderer implements Renderer {
             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) {
@@ -2677,12 +2700,15 @@ public final class GLRenderer implements Renderer {
     }
 
     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;
         }
 
+        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());
             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;
     }
 
+    /**
+     * 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.
      *

+ 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;
     }
 
+    @Override
     public int compare(Geometry o1, Geometry o2) {
         Material m1 = o1.getMaterial();
         Material m2 = o2.getMaterial();
-
-        int compareResult = m2.getSortId() - m1.getSortId();
+        
+        int compareResult = Integer.compare(m1.getSortId(), m2.getSortId());
         if (compareResult == 0){
             // use the same shader.
             // 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.OutputCapsule;
 import com.jme3.export.binary.BinaryImporter;
+import com.jme3.util.clone.Cloner;
 import com.jme3.util.SafeArrayList;
 import java.io.IOException;
 import java.util.*;
@@ -50,7 +51,7 @@ import java.util.logging.Logger;
  * The AssetLinkNode does not store its children when exported to file.
  * Instead, you can add a list of AssetKeys that will be loaded and attached
  * when the AssetLinkNode is restored.
- * 
+ *
  * @author normenhansen
  */
 public class AssetLinkNode extends Node {
@@ -70,6 +71,20 @@ public class AssetLinkNode extends Node {
         assetLoaderKeys.add(key);
     }
 
+    /**
+     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
+     */
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) {
+        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
      * AssetLinkNode is loaded from a binary file.
@@ -166,7 +181,7 @@ public class AssetLinkNode extends Node {
                 children.add(child);
                 assetChildren.put(modelKey, child);
             } else {
-                Logger.getLogger(this.getClass().getName()).log(Level.WARNING, "Cannot locate {0} for asset link node {1}", 
+                Logger.getLogger(this.getClass().getName()).log(Level.WARNING, "Cannot locate {0} for asset link node {1}",
                                                                     new Object[]{ modelKey, key });
             }
         }

+ 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.util.SafeArrayList;
 import com.jme3.util.TempVars;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 
 /**
  * BatchNode holds geometries that are a batched version of all the geometries that are in its sub scenegraph.
@@ -60,7 +62,7 @@ import com.jme3.util.TempVars;
  * Sub geoms can be removed but it may be slower than the normal spatial removing
  * Sub geoms can be added after the batch() method has been called but won't be batched and will just be rendered as normal geometries.
  * To integrate them in the batch you have to call the batch() method again on the batchNode.
- * 
+ *
  * TODO normal or tangents or both looks a bit weird
  * TODO more automagic (batch when needed in the updateLogicalState)
  * @author Nehon
@@ -77,7 +79,7 @@ public class BatchNode extends GeometryGroupNode {
      */
     protected Map<Geometry, Batch> batchesByGeom = new HashMap<Geometry, Batch>();
     /**
-     * used to store transformed vectors before proceeding to a bulk put into the FloatBuffer 
+     * used to store transformed vectors before proceeding to a bulk put into the FloatBuffer
      */
     private float[] tmpFloat;
     private float[] tmpFloatN;
@@ -96,7 +98,7 @@ public class BatchNode extends GeometryGroupNode {
     public BatchNode(String name) {
         super(name);
     }
-    
+
     @Override
     public void onTransformChange(Geometry geom) {
         updateSubBatch(geom);
@@ -123,7 +125,7 @@ public class BatchNode extends GeometryGroupNode {
     protected Matrix4f getTransformMatrix(Geometry g){
         return g.cachedWorldMat;
     }
-    
+
     protected void updateSubBatch(Geometry bg) {
         Batch batch = batchesByGeom.get(bg);
         if (batch != null) {
@@ -134,13 +136,13 @@ public class BatchNode extends GeometryGroupNode {
             FloatBuffer posBuf = (FloatBuffer) pvb.getData();
             VertexBuffer nvb = mesh.getBuffer(VertexBuffer.Type.Normal);
             FloatBuffer normBuf = (FloatBuffer) nvb.getData();
-          
+
             VertexBuffer opvb = origMesh.getBuffer(VertexBuffer.Type.Position);
             FloatBuffer oposBuf = (FloatBuffer) opvb.getData();
             VertexBuffer onvb = origMesh.getBuffer(VertexBuffer.Type.Normal);
             FloatBuffer onormBuf = (FloatBuffer) onvb.getData();
             Matrix4f transformMat = getTransformMatrix(bg);
-            
+
             if (mesh.getBuffer(VertexBuffer.Type.Tangent) != null) {
 
                 VertexBuffer tvb = mesh.getBuffer(VertexBuffer.Type.Tangent);
@@ -184,12 +186,12 @@ public class BatchNode extends GeometryGroupNode {
             }
             batches.clear();
             batchesByGeom.clear();
-        }        
+        }
         //only reset maxVertCount if there is something new to batch
         if (matMap.size() > 0) {
             maxVertCount = 0;
         }
-        
+
         for (Map.Entry<Material, List<Geometry>> entry : matMap.entrySet()) {
             Mesh m = new Mesh();
             Material material = entry.getKey();
@@ -255,7 +257,7 @@ public class BatchNode extends GeometryGroupNode {
 
     /**
      * recursively visit the subgraph and unbatch geometries
-     * @param s 
+     * @param s
      */
     private void unbatchSubGraph(Spatial s) {
         if (s instanceof Node) {
@@ -269,8 +271,8 @@ public class BatchNode extends GeometryGroupNode {
             }
         }
     }
-    
-    
+
+
     private void gatherGeometries(Map<Material, List<Geometry>> map, Spatial n, boolean rebatch) {
 
         if (n instanceof Geometry) {
@@ -283,7 +285,7 @@ public class BatchNode extends GeometryGroupNode {
                     }
                     List<Geometry> list = map.get(g.getMaterial());
                     if (list == null) {
-                        //trying to compare materials with the isEqual method 
+                        //trying to compare materials with the isEqual method
                         for (Map.Entry<Material, List<Geometry>> mat : map.entrySet()) {
                             if (g.getMaterial().contentEquals(mat.getKey())) {
                                 list = mat.getValue();
@@ -331,7 +333,7 @@ public class BatchNode extends GeometryGroupNode {
     /**
      * Sets the material to the all the batches of this BatchNode
      * use setMaterial(Material material,int batchIndex) to set a material to a specific batch
-     * 
+     *
      * @param material the material to use for this geometry
      */
     @Override
@@ -341,12 +343,12 @@ public class BatchNode extends GeometryGroupNode {
 
     /**
      * Returns the material that is used for the first batch of this BatchNode
-     * 
+     *
      * use getMaterial(Material material,int batchIndex) to get a material from a specific batch
-     * 
+     *
      * @return the material that is used for the first batch of this BatchNode
-     * 
-     * @see #setMaterial(com.jme3.material.Material) 
+     *
+     * @see #setMaterial(com.jme3.material.Material)
      */
     public Material getMaterial() {
         if (!batches.isEmpty()) {
@@ -359,7 +361,7 @@ public class BatchNode extends GeometryGroupNode {
     /**
      * Merges all geometries in the collection into
      * the output mesh. Does not take into account materials.
-     * 
+     *
      * @param geometries
      * @param outMesh
      */
@@ -383,7 +385,7 @@ public class BatchNode extends GeometryGroupNode {
                 maxVertCount = geom.getVertexCount();
             }
             Mesh.Mode listMode;
-            float listLineWidth = 1f;
+            //float listLineWidth = 1f;
             int components;
             switch (geom.getMesh().getMode()) {
                 case Points:
@@ -394,7 +396,7 @@ public class BatchNode extends GeometryGroupNode {
                 case LineStrip:
                 case Lines:
                     listMode = Mesh.Mode.Lines;
-                    listLineWidth = geom.getMesh().getLineWidth();
+                    //listLineWidth = geom.getMesh().getLineWidth();
                     components = 2;
                     break;
                 case TriangleFan:
@@ -418,7 +420,7 @@ public class BatchNode extends GeometryGroupNode {
                 formatForBuf[vb.getBufferType().ordinal()] = vb.getFormat();
                 normForBuf[vb.getBufferType().ordinal()] = vb.isNormalized();
             }
-            
+
             maxWeights = Math.max(maxWeights, geom.getMesh().getMaxNumWeights());
 
             if (mode != null && mode != listMode) {
@@ -426,19 +428,20 @@ public class BatchNode extends GeometryGroupNode {
                         + " primitive types: " + mode + " != " + listMode);
             }
             mode = listMode;
-            if (mode == Mesh.Mode.Lines) {
-                if (lineWidth != 1f && listLineWidth != lineWidth) {
-                    throw new UnsupportedOperationException("When using Mesh Line mode, cannot combine meshes with different line width "
-                            + lineWidth + " != " + listLineWidth);
-                }
-                lineWidth = listLineWidth;
-            }
+            //Not needed anymore as lineWidth is now in RenderState and will be taken into account when merging according to the material
+//            if (mode == Mesh.Mode.Lines) {
+//                if (lineWidth != 1f && listLineWidth != lineWidth) {
+//                    throw new UnsupportedOperationException("When using Mesh Line mode, cannot combine meshes with different line width "
+//                            + lineWidth + " != " + listLineWidth);
+//                }
+//                lineWidth = listLineWidth;
+//            }
             compsForBuf[VertexBuffer.Type.Index.ordinal()] = components;
         }
 
         outMesh.setMaxNumWeights(maxWeights);
         outMesh.setMode(mode);
-        outMesh.setLineWidth(lineWidth);
+        //outMesh.setLineWidth(lineWidth);
         if (totalVerts >= 65536) {
             // make sure we create an UnsignedInt buffer so we can fit all of the meshes
             formatForBuf[VertexBuffer.Type.Index.ordinal()] = VertexBuffer.Format.UnsignedInt;
@@ -585,7 +588,7 @@ public class BatchNode extends GeometryGroupNode {
         int offset = start * 3;
         int tanOffset = start * 4;
 
-        
+
         bindBufPos.rewind();
         bindBufNorm.rewind();
         bindBufTangents.rewind();
@@ -661,10 +664,10 @@ public class BatchNode extends GeometryGroupNode {
         vars.release();
     }
 
-    protected class Batch {
+    protected class Batch implements JmeCloneable {
         /**
          * update the batchesByGeom map for this batch with the given List of geometries
-         * @param list 
+         * @param list
          */
         void updateGeomList(List<Geometry> list) {
             for (Geometry geom : list) {
@@ -674,10 +677,25 @@ public class BatchNode extends GeometryGroupNode {
             }
         }
         Geometry geometry;
-        
+
         public final Geometry getGeometry() {
             return geometry;
         }
+
+        @Override
+        public Batch jmeClone() {
+            try {
+                return (Batch)super.clone();
+            } catch (CloneNotSupportedException ex) {
+                throw new AssertionError();
+            }
+        }
+
+        @Override
+        public void cloneFields( Cloner cloner, Object original ) {
+            this.geometry = cloner.clone(geometry);
+        }
+
     }
 
     protected void setNeedsFullRebatch(boolean needsFullRebatch) {
@@ -703,7 +721,27 @@ public class BatchNode extends GeometryGroupNode {
         }
         return clone;
     }
-    
+
+    /**
+     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
+     */
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) {
+        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
     public int collideWith(Collidable other, CollisionResults results) {
         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.scene.control.CameraControl;
 import com.jme3.scene.control.CameraControl.ControlDirection;
+import com.jme3.util.clone.Cloner;
 import java.io.IOException;
 
 /**
@@ -93,7 +94,20 @@ public class CameraNode extends Node {
 //        this.lookAt(position, upVector);
 //        camControl.getCamera().lookAt(position, upVector);
 //    }
-    
+
+    /**
+     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
+     */
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) {
+        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
     public void read(JmeImporter im) throws IOException {
         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.renderer.Camera;
 import com.jme3.scene.VertexBuffer.Type;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.IdentityCloneFunction;
 import com.jme3.util.TempVars;
 import java.io.IOException;
 import java.util.Queue;
@@ -54,12 +56,12 @@ import java.util.logging.Logger;
  * contains the geometric data for rendering objects. It manages all rendering
  * information such as a {@link Material} object to define how the surface
  * should be shaded and the {@link Mesh} data to contain the actual geometry.
- * 
+ *
  * @author Kirill Vainer
  */
 public class Geometry extends Spatial {
 
-    // Version #1: removed shared meshes. 
+    // Version #1: removed shared meshes.
     // models loaded with shared mesh will be automatically fixed.
     public static final int SAVABLE_VERSION = 1;
     private static final Logger logger = Logger.getLogger(Geometry.class.getName());
@@ -71,19 +73,19 @@ public class Geometry extends Spatial {
      */
     protected boolean ignoreTransform = false;
     protected transient Matrix4f cachedWorldMat = new Matrix4f();
-    
+
     /**
      * Specifies which {@link GeometryGroupNode} this <code>Geometry</code>
      * is managed by.
      */
     protected GeometryGroupNode groupNode;
-    
+
     /**
-     * The start index of this <code>Geometry's</code> inside 
+     * The start index of this <code>Geometry's</code> inside
      * the {@link GeometryGroupNode}.
      */
     protected int startIndex = -1;
-        
+
     /**
      * Serialization only. Do not use.
      */
@@ -95,37 +97,37 @@ public class Geometry extends Spatial {
      * Create a geometry node without any mesh data.
      * Both the mesh and the material are null, the geometry
      * cannot be rendered until those are set.
-     * 
+     *
      * @param name The name of this geometry
      */
     public Geometry(String name) {
         super(name);
-        
+
         // For backwards compatibility, only clear the "requires
         // update" flag if we are not a subclass of Geometry.
         // This prevents subclass from silently failing to receive
         // updates when they upgrade.
-        setRequiresUpdates(Geometry.class != getClass()); 
+        setRequiresUpdates(Geometry.class != getClass());
     }
 
     /**
      * Create a geometry node with mesh data.
      * The material of the geometry is null, it cannot
      * be rendered until it is set.
-     * 
+     *
      * @param name The name of this geometry
      * @param mesh The mesh data for this geometry
      */
     public Geometry(String name, Mesh mesh) {
         this(name);
-        
+
         if (mesh == null) {
             throw new IllegalArgumentException("mesh cannot be null");
         }
 
         this.mesh = mesh;
     }
-    
+
     @Override
     public boolean checkCulling(Camera cam) {
         if (isGrouped()) {
@@ -137,8 +139,8 @@ public class Geometry extends Spatial {
 
     /**
      * @return If ignoreTransform mode is set.
-     * 
-     * @see Geometry#setIgnoreTransform(boolean) 
+     *
+     * @see Geometry#setIgnoreTransform(boolean)
      */
     public boolean isIgnoreTransform() {
         return ignoreTransform;
@@ -156,7 +158,7 @@ public class Geometry extends Spatial {
      * Level 0 indicates that the default index buffer should be used,
      * levels [1, LodLevels + 1] represent the levels set on the mesh
      * with {@link Mesh#setLodLevels(com.jme3.scene.VertexBuffer[]) }.
-     * 
+     *
      * @param lod The lod level to set
      */
     @Override
@@ -170,7 +172,7 @@ public class Geometry extends Spatial {
         }
 
         lodLevel = lod;
-        
+
         if (isGrouped()) {
             groupNode.onMeshChange(this);
         }
@@ -178,7 +180,7 @@ public class Geometry extends Spatial {
 
     /**
      * Returns the LOD level set with {@link #setLodLevel(int) }.
-     * 
+     *
      * @return the LOD level set
      */
     public int getLodLevel() {
@@ -187,10 +189,10 @@ public class Geometry extends Spatial {
 
     /**
      * Returns this geometry's mesh vertex count.
-     * 
+     *
      * @return this geometry's mesh vertex count.
-     * 
-     * @see Mesh#getVertexCount() 
+     *
+     * @see Mesh#getVertexCount()
      */
     public int getVertexCount() {
         return mesh.getVertexCount();
@@ -198,10 +200,10 @@ public class Geometry extends Spatial {
 
     /**
      * Returns this geometry's mesh triangle count.
-     * 
+     *
      * @return this geometry's mesh triangle count.
-     * 
-     * @see Mesh#getTriangleCount() 
+     *
+     * @see Mesh#getTriangleCount()
      */
     public int getTriangleCount() {
         return mesh.getTriangleCount();
@@ -209,9 +211,9 @@ public class Geometry extends Spatial {
 
     /**
      * Sets the mesh to use for this geometry when rendering.
-     * 
+     *
      * @param mesh the mesh to use for this geometry
-     * 
+     *
      * @throws IllegalArgumentException If mesh is null
      */
     public void setMesh(Mesh mesh) {
@@ -221,7 +223,7 @@ public class Geometry extends Spatial {
 
         this.mesh = mesh;
         setBoundRefresh();
-        
+
         if (isGrouped()) {
             groupNode.onMeshChange(this);
         }
@@ -229,10 +231,10 @@ public class Geometry extends Spatial {
 
     /**
      * Returns the mesh to use for this geometry
-     * 
+     *
      * @return the mesh to use for this geometry
-     * 
-     * @see #setMesh(com.jme3.scene.Mesh) 
+     *
+     * @see #setMesh(com.jme3.scene.Mesh)
      */
     public Mesh getMesh() {
         return mesh;
@@ -240,13 +242,13 @@ public class Geometry extends Spatial {
 
     /**
      * Sets the material to use for this geometry.
-     * 
+     *
      * @param material the material to use for this geometry
      */
     @Override
     public void setMaterial(Material material) {
         this.material = material;
-        
+
         if (isGrouped()) {
             groupNode.onMaterialChange(this);
         }
@@ -254,10 +256,10 @@ public class Geometry extends Spatial {
 
     /**
      * Returns the material that is used for this geometry.
-     * 
+     *
      * @return the material that is used for this geometry
-     * 
-     * @see #setMaterial(com.jme3.material.Material) 
+     *
+     * @see #setMaterial(com.jme3.material.Material)
      */
     public Material getMaterial() {
         return material;
@@ -310,18 +312,18 @@ public class Geometry extends Spatial {
         computeWorldMatrix();
 
         if (isGrouped()) {
-            groupNode.onTransformChange(this);   
+            groupNode.onTransformChange(this);
         }
-        
+
         // geometry requires lights to be sorted
         worldLights.sort(true);
     }
 
     /**
      * Associate this <code>Geometry</code> with a {@link GeometryGroupNode}.
-     * 
+     *
      * Should only be called by the parent {@link GeometryGroupNode}.
-     * 
+     *
      * @param node Which {@link GeometryGroupNode} to associate with.
      * @param startIndex The starting index of this geometry in the group.
      */
@@ -329,26 +331,26 @@ public class Geometry extends Spatial {
         if (isGrouped()) {
             unassociateFromGroupNode();
         }
-        
+
         this.groupNode = node;
         this.startIndex = startIndex;
     }
 
     /**
-     * Removes the {@link GeometryGroupNode} association from this 
+     * Removes the {@link GeometryGroupNode} association from this
      * <code>Geometry</code>.
-     * 
+     *
      * Should only be called by the parent {@link GeometryGroupNode}.
      */
     public void unassociateFromGroupNode() {
         if (groupNode != null) {
-            // Once the geometry is removed 
+            // Once the geometry is removed
             // from the parent, the group node needs to be updated.
             groupNode.onGeometryUnassociated(this);
             groupNode = null;
-            
+
             // change the default to -1 to make error detection easier
-            startIndex = -1; 
+            startIndex = -1;
         }
     }
 
@@ -360,7 +362,7 @@ public class Geometry extends Spatial {
     @Override
     protected void setParent(Node parent) {
         super.setParent(parent);
-        
+
         // If the geometry is managed by group node we need to unassociate.
         if (parent == null && isGrouped()) {
             unassociateFromGroupNode();
@@ -406,7 +408,7 @@ public class Geometry extends Spatial {
      * {@link Geometry#getWorldTransform() world transform} of this geometry.
      * In order to receive updated values, you must call {@link Geometry#computeWorldMatrix() }
      * before using this method.
-     * 
+     *
      * @return Matrix to transform from local space to world space
      */
     public Matrix4f getWorldMatrix() {
@@ -418,7 +420,7 @@ public class Geometry extends Spatial {
      * This alters the bound used on the mesh as well via
      * {@link Mesh#setBound(com.jme3.bounding.BoundingVolume) } and
      * forces the world bounding volume to be recomputed.
-     * 
+     *
      * @param modelBound The model bound to set
      */
     @Override
@@ -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.
-     * 
+     *
      * @return True if managed by a {@link GeometryGroupNode}.
      */
     public boolean isGrouped() {
         return groupNode != null;
     }
-    
+
     /**
      * @deprecated Use {@link #isGrouped()} instead.
      */
@@ -491,15 +493,22 @@ public class Geometry extends Spatial {
      */
     @Override
     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);
-        
+
         // This geometry is managed,
         // but the cloned one is not attached to anything, hence not managed.
         if (geomClone.isGrouped()) {
             geomClone.groupNode = null;
             geomClone.startIndex = -1;
         }
-        
+
         geomClone.cachedWorldMat = cachedWorldMat.clone();
         if (material != null) {
             if (cloneMaterial) {
@@ -534,11 +543,58 @@ public class Geometry extends Spatial {
      */
     @Override
     public Spatial deepClone() {
+        return super.deepClone();
+    }
+
+    public Spatial oldDeepClone() {
         Geometry geomClone = clone(true);
         geomClone.mesh = mesh.deepClone();
         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
     public void write(JmeExporter ex) throws IOException {
         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.scene.control.LightControl;
 import com.jme3.scene.control.LightControl.ControlDirection;
+import com.jme3.util.clone.Cloner;
 import java.io.IOException;
 
 /**
  * <code>LightNode</code> is used to link together a {@link Light} object
- * with a {@link Node} object. 
+ * with a {@link Node} object.
  *
  * @author Tim8Dev
  */
@@ -66,7 +67,7 @@ public class LightNode extends Node {
 
     /**
      * Enable or disable the <code>LightNode</code> functionality.
-     * 
+     *
      * @param enabled If false, the functionality of LightNode will
      * be disabled.
      */
@@ -93,7 +94,20 @@ public class LightNode extends Node {
     public Light getLight() {
         return lightControl.getLight();
     }
-    
+
+    /**
+     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
+     */
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) {
+        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
     public void read(JmeImporter im) throws IOException {
         super.read(im);

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 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.util.SafeArrayList;
 import com.jme3.util.TempVars;
+import com.jme3.util.clone.Cloner;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
@@ -53,7 +54,7 @@ import java.util.logging.Logger;
  * node maintains a collection of children and handles merging said children
  * into a single bound to allow for very fast culling of multiple nodes. Node
  * allows for any number of children to be attached.
- * 
+ *
  * @author Mark Powell
  * @author Gregg Patton
  * @author Joshua Slack
@@ -62,26 +63,25 @@ public class Node extends Spatial {
 
     private static final Logger logger = Logger.getLogger(Node.class.getName());
 
-    /** 
+    /**
      * This node's children.
      */
     protected SafeArrayList<Spatial> children = new SafeArrayList<Spatial>(Spatial.class);
 
     /**
      * If this node is a root, this list will contain the current
-     * set of children (and children of children) that require 
+     * set of children (and children of children) that require
      * updateLogicalState() to be called as indicated by their
      * requiresUpdate() method.
      */
     private SafeArrayList<Spatial> updateList = null;
-    
     /**
      * False if the update list requires rebuilding.  This is Node.class
      * specific and therefore not included as part of the Spatial update flags.
      * A flag is used instead of nulling the updateList to avoid reallocating
      * a whole list every time the scene graph changes.
-     */     
-    private boolean updateListValid = false;    
+     */
+    private boolean updateListValid = false;
 
     /**
      * Serialization only. Do not use.
@@ -93,29 +93,28 @@ public class Node extends Spatial {
     /**
      * Constructor instantiates a new <code>Node</code> with a default empty
      * list for containing children.
-     * 
+     *
      * @param name the name of the scene element. This is required for
      * identification and comparison purposes.
      */
     public Node(String name) {
         super(name);
-        
         // For backwards compatibility, only clear the "requires
         // update" flag if we are not a subclass of Node.
         // This prevents subclass from silently failing to receive
         // updates when they upgrade.
-        setRequiresUpdates(Node.class != getClass()); 
+        setRequiresUpdates(Node.class != getClass());
     }
 
     /**
-     * 
+     *
      * <code>getQuantity</code> returns the number of children this node
      * maintains.
-     * 
+     *
      * @return the number of children this node maintains.
      */
     public int getQuantity() {
-        return children.size();        
+        return children.size();
     }
 
     @Override
@@ -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
     protected void updateWorldBound(){
         super.updateWorldBound();
-        
         // for a node, the world bound is a combination of all it's children
         // bounds
         BoundingVolume resultBound = null;
@@ -167,7 +177,7 @@ public class Node extends Spatial {
     protected void setParent(Node parent) {
         if( this.parent == null && parent != null ) {
             // We were a root before and now we aren't... make sure if
-            // we had an updateList then we clear it completely to 
+            // we had an updateList then we clear it completely to
             // avoid holding the dead array.
             updateList = null;
             updateListValid = false;
@@ -204,15 +214,15 @@ public class Node extends Spatial {
             return updateList;
         }
         if( updateList == null ) {
-            updateList = new SafeArrayList<Spatial>(Spatial.class);            
+            updateList = new SafeArrayList<Spatial>(Spatial.class);
         } else {
             updateList.clear();
         }
 
         // Build the list
         addUpdateChildren(updateList);
-        updateListValid = true;       
-        return updateList;   
+        updateListValid = true;
+        return updateList;
     }
 
     @Override
@@ -238,19 +248,19 @@ public class Node extends Spatial {
             // This branch has no geometric state that requires updates.
             return;
         }
-        
         if ((refreshFlags & RF_LIGHTLIST) != 0){
             updateWorldLightList();
         }
-
         if ((refreshFlags & RF_TRANSFORM) != 0){
             // combine with parent transforms- same for all spatial
             // subclasses.
             updateWorldTransforms();
         }
+        if ((refreshFlags & RF_MATPARAM_OVERRIDE) != 0) {
+            updateMatParamOverrides();
+        }
 
         refreshFlags &= ~RF_CHILD_LIGHTLIST;
-        
         if (!children.isEmpty()) {
             // the important part- make sure child geometric state is refreshed
             // first before updating own world bound. This saves
@@ -260,7 +270,7 @@ public class Node extends Spatial {
             for (Spatial child : children.getArray()) {
                 child.updateGeometricState();
             }
-        }            
+        }
 
         if ((refreshFlags & RF_BOUND) != 0){
             updateWorldBound();
@@ -272,7 +282,7 @@ public class Node extends Spatial {
     /**
      * <code>getTriangleCount</code> returns the number of triangles contained
      * in all sub-branches of this node that contain geometry.
-     * 
+     *
      * @return the triangle count of this branch.
      */
     @Override
@@ -286,11 +296,10 @@ public class Node extends Spatial {
 
         return count;
     }
-    
     /**
      * <code>getVertexCount</code> returns the number of vertices contained
      * in all sub-branches of this node that contain geometry.
-     * 
+     *
      * @return the vertex count of this branch.
      */
     @Override
@@ -311,7 +320,7 @@ public class Node extends Spatial {
      * returned.
      * <br>
      * If the child already had a parent it is detached from that former parent.
-     * 
+     *
      * @param child
      *            the child to attach to this node.
      * @return the number of children maintained by this node.
@@ -320,15 +329,14 @@ public class Node extends Spatial {
     public int attachChild(Spatial child) {
         return attachChildAt(child, children.size());
     }
-    
     /**
-     * 
+     *
      * <code>attachChildAt</code> attaches a child to this node at an index. This node
      * becomes the child's parent. The current number of children maintained is
      * returned.
      * <br>
      * If the child already had a parent it is detached from that former parent.
-     * 
+     *
      * @param child
      *            the child to attach to this node.
      * @return the number of children maintained by this node.
@@ -344,27 +352,25 @@ public class Node extends Spatial {
             }
             child.setParent(this);
             children.add(index, child);
-            
             // XXX: Not entirely correct? Forces bound update up the
             // tree stemming from the attached child. Also forces
             // transform update down the tree-
             child.setTransformRefresh();
             child.setLightListRefresh();
+            child.setMatParamOverrideRefresh();
             if (logger.isLoggable(Level.FINE)) {
                 logger.log(Level.FINE,"Child ({0}) attached to this node ({1})",
                         new Object[]{child.getName(), getName()});
             }
-            
             invalidateUpdateList();
         }
-        
         return children.size();
     }
 
     /**
      * <code>detachChild</code> removes a given child from the node's list.
      * This child will no longer be maintained.
-     * 
+     *
      * @param child
      *            the child to remove.
      * @return the index the child was at. -1 if the child was not in the list.
@@ -379,16 +385,16 @@ public class Node extends Spatial {
                 detachChildAt(index);
             }
             return index;
-        } 
-            
-        return -1;        
+        }
+
+        return -1;
     }
 
     /**
      * <code>detachChild</code> removes a given child from the node's list.
      * This child will no longe be maintained. Only the first child with a
      * matching name is removed.
-     * 
+     *
      * @param childName
      *            the child to remove.
      * @return the index the child was at. -1 if the child was not in the list.
@@ -408,10 +414,10 @@ public class Node extends Spatial {
     }
 
     /**
-     * 
+     *
      * <code>detachChildAt</code> removes a child at a given index. That child
      * is returned for saving purposes.
-     * 
+     *
      * @param index
      *            the index of the child to be removed.
      * @return the child at the supplied index.
@@ -432,6 +438,7 @@ public class Node extends Spatial {
             child.setTransformRefresh();
             // lights are also inherited from parent
             child.setLightListRefresh();
+            child.setMatParamOverrideRefresh();
             
             invalidateUpdateList();
         }
@@ -439,7 +446,7 @@ public class Node extends Spatial {
     }
 
     /**
-     * 
+     *
      * <code>detachAllChildren</code> removes all children attached to this
      * node.
      */
@@ -458,7 +465,7 @@ public class Node extends Spatial {
      * in this node's list of children.
      * @param sp
      *          The spatial to look up
-     * @return 
+     * @return
      *          The index of the spatial in the node's children, or -1
      *          if the spatial is not attached to this node
      */
@@ -468,7 +475,7 @@ public class Node extends Spatial {
 
     /**
      * More efficient than e.g detaching and attaching as no updates are needed.
-     * 
+     *
      * @param index1 The index of the first child to swap
      * @param index2 The index of the second child to swap
      */
@@ -481,9 +488,9 @@ public class Node extends Spatial {
     }
 
     /**
-     * 
+     *
      * <code>getChild</code> returns a child at a given index.
-     * 
+     *
      * @param i
      *            the index to retrieve the child from.
      * @return the child at a specified index.
@@ -497,13 +504,13 @@ public class Node extends Spatial {
      * given name (case sensitive.) This method does a depth first recursive
      * search of all descendants of this node, it will return the first spatial
      * found with a matching name.
-     * 
+     *
      * @param name
      *            the name of the child to retrieve. If null, we'll return null.
      * @return the child if found, or null.
      */
     public Spatial getChild(String name) {
-        if (name == null) 
+        if (name == null)
             return null;
 
         for (Spatial child : children.getArray()) {
@@ -518,11 +525,10 @@ public class Node extends Spatial {
         }
         return null;
     }
-    
     /**
      * determines if the provided Spatial is contained in the children list of
      * this node.
-     * 
+     *
      * @param spat
      *            the child object to look for.
      * @return true if the object is contained, false otherwise.
@@ -566,39 +572,32 @@ public class Node extends Spatial {
 
     public int collideWith(Collidable other, CollisionResults results){
         int total = 0;
-        
         // optimization: try collideWith BoundingVolume to avoid possibly redundant tests on children
-        // number 4 in condition is somewhat arbitrary. When there is only one child, the boundingVolume test is redundant at all. 
+        // number 4 in condition is somewhat arbitrary. When there is only one child, the boundingVolume test is redundant at all.
         // The idea is when there are few children, it can be too expensive to test boundingVolume first.
         /*
         I'm removing this change until some issues can be addressed and I really
         think it needs to be implemented a better way anyway.
-        
         First, it causes issues for anyone doing collideWith() with BoundingVolumes
         and expecting it to trickle down to the children.  For example, children
         with BoundingSphere bounding volumes and collideWith(BoundingSphere).  Doing
         a collision check at the parent level then has to do a BoundingSphere to BoundingBox
         collision which isn't resolved.  (Having to come up with a collision point in that
         case is tricky and the first sign that this is the wrong approach.)
-        
         Second, the rippling changes this caused to 'optimize' collideWith() for this
         special use-case are another sign that this approach was a bit dodgy.  The whole
         idea of calculating a full collision just to see if the two shapes collide at all
         is very wasteful.
-        
         A proper implementation should support a simpler boolean check that doesn't do
         all of that calculation.  For example, if 'other' is also a BoundingVolume (ie: 99.9%
         of all non-Ray cases) then a direct BV to BV intersects() test can be done.  So much
         faster.  And if 'other' _is_ a Ray then the BV.intersects(Ray) call can be done.
-        
         I don't have time to do it right now but I'll at least un-break a bunch of peoples'
         code until it can be 'optimized' properly.  Hopefully it's not too late to back out
-        the other dodgy ripples this caused.  -pspeed (hindsight-expert ;)) 
-        
+        the other dodgy ripples this caused.  -pspeed (hindsight-expert ;))
         Note: the code itself is relatively simple to implement but I don't have time to
         a) test it, and b) see if '> 4' is still a decent check for it.  Could be it's fast
         enough to do all the time for > 1.
-        
         if (children.size() > 4)
         {
           BoundingVolume bv = this.getWorldBound();
@@ -642,7 +641,7 @@ public class Node extends Spatial {
      * @return Non-null, but possibly 0-element, list of matching Spatials (also Instances extending Spatials).
      *
      * @see java.util.regex.Pattern
-     * @see Spatial#matches(java.lang.Class, java.lang.String) 
+     * @see Spatial#matches(java.lang.Class, java.lang.String)
      */
     @SuppressWarnings("unchecked")
     public <T extends Spatial>List<T> descendantMatches(
@@ -662,7 +661,7 @@ public class Node extends Spatial {
     /**
      * Convenience wrapper.
      *
-     * @see #descendantMatches(java.lang.Class, java.lang.String) 
+     * @see #descendantMatches(java.lang.Class, java.lang.String)
      */
     public <T extends Spatial>List<T> descendantMatches(
             Class<T> spatialSubclass) {
@@ -672,7 +671,7 @@ public class Node extends Spatial {
     /**
      * Convenience wrapper.
      *
-     * @see #descendantMatches(java.lang.Class, java.lang.String) 
+     * @see #descendantMatches(java.lang.Class, java.lang.String)
      */
     public <T extends Spatial>List<T> descendantMatches(String nameRegex) {
         return descendantMatches(null, nameRegex);
@@ -691,12 +690,21 @@ public class Node extends Spatial {
         // Reset the fields of the clone that should be in a 'new' state.
         nodeClone.updateList = null;
         nodeClone.updateListValid = false; // safe because parent is nulled out in super.clone()
-            
         return nodeClone;
     }
 
     @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();
         nodeClone.children = new SafeArrayList<Spatial>(Spatial.class);
         for (Spatial child : children){
@@ -707,6 +715,20 @@ public class Node extends Spatial {
         return nodeClone;
     }
 
+    /**
+     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
+     */
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) {
+        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
     public void write(JmeExporter e) throws IOException {
         super.write(e);
@@ -718,8 +740,7 @@ public class Node extends Spatial {
         // XXX: Load children before loading itself!!
         // This prevents empty children list if controls query
         // it in Control.setSpatial().
-        
-        children = new SafeArrayList( Spatial.class, 
+        children = new SafeArrayList( Spatial.class,
                                       e.getCapsule(this).readSavableArrayList("children", null) );
 
         // go through children and set parent to this node
@@ -728,7 +749,6 @@ public class Node extends Spatial {
                 child.parent = this;
             }
         }
-        
         super.read(e);
     }
 
@@ -749,7 +769,6 @@ public class Node extends Spatial {
             }
         }
     }
-    
     @Override
     public void depthFirstTraversal(SceneGraphVisitor visitor) {
         for (Spatial child : children.getArray()) {
@@ -757,7 +776,6 @@ public class Node extends Spatial {
         }
         visitor.visit(this);
     }
-    
     @Override
     protected void breadthFirstTraversal(SceneGraphVisitor visitor, Queue<Spatial> queue) {
         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.light.Light;
 import com.jme3.light.LightList;
+import com.jme3.material.MatParamOverride;
 import com.jme3.material.Material;
 import com.jme3.math.*;
 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.ShadowMode;
 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.TempVars;
 import java.io.IOException;
@@ -63,17 +67,17 @@ import java.util.logging.Logger;
  * @author Joshua Slack
  * @version $Revision: 4075 $, $Data$
  */
-public abstract class Spatial implements Savable, Cloneable, Collidable, CloneableSmartAsset {
+public abstract class Spatial implements Savable, Cloneable, Collidable, CloneableSmartAsset, JmeCloneable {
 
     private static final Logger logger = Logger.getLogger(Spatial.class.getName());
 
     /**
-     * Specifies how frustum culling should be handled by 
+     * Specifies how frustum culling should be handled by
      * this spatial.
      */
     public enum CullHint {
 
-        /** 
+        /**
          * Do whatever our parent does. If no parent, default to {@link #Dynamic}.
          */
         Inherit,
@@ -83,13 +87,13 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
          * Camera planes whether or not this Spatial should be culled.
          */
         Dynamic,
-        /** 
+        /**
          * Always cull this from the view, throwing away this object
          * and any children from rendering commands.
          */
         Always,
         /**
-         * Never cull this from view, always draw it. 
+         * Never cull this from view, always draw it.
          * Note that we will still get culled if our parent is culled.
          */
         Never;
@@ -100,15 +104,15 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      */
     public enum BatchHint {
 
-        /** 
+        /**
          * Do whatever our parent does. If no parent, default to {@link #Always}.
          */
         Inherit,
-        /** 
+        /**
          * This spatial will always be batched when attached to a BatchNode.
          */
         Always,
-        /** 
+        /**
          * This spatial will never be batched when attached to a BatchNode.
          */
         Never;
@@ -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
                                RF_BOUND = 0x02,
                                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 BatchHint batchHint = BatchHint.Inherit;
-    /** 
+    /**
      * Spatial's bounding volume relative to the world.
      */
     protected BoundingVolume worldBound;
@@ -132,6 +137,10 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      */
     protected LightList localLights;
     protected transient LightList worldLights;
+
+    protected ArrayList<MatParamOverride> localOverrides;
+    protected ArrayList<MatParamOverride> worldOverrides;
+
     /** 
      * This spatial's name.
      */
@@ -147,11 +156,11 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
     protected HashMap<String, Savable> userData = null;
     /**
      * Used for smart asset caching
-     * 
-     * @see AssetKey#useSmartCache() 
+     *
+     * @see AssetKey#useSmartCache()
      */
     protected AssetKey key;
-    /** 
+    /**
      * Spatial's parent, or null if it has none.
      */
     protected transient Node parent;
@@ -174,7 +183,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
     /**
      * Serialization only. Do not use.
      * Not really. This class is never instantiated directly but the
-     * subclasses like to use the no-arg constructor for their own 
+     * subclasses like to use the no-arg constructor for their own
      * no-arg constructor... which is technically weaker than
      * forward supplying defaults.
      */
@@ -192,13 +201,14 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      */
     protected Spatial(String name) {
         this.name = name;
-        
         localTransform = new Transform();
         worldTransform = new Transform();
 
         localLights = new LightList(this);
         worldLights = new LightList(this);
 
+        localOverrides = new ArrayList<>();
+        worldOverrides = new ArrayList<>();
         refreshFlags |= RF_BOUND;
     }
 
@@ -219,13 +229,12 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
     boolean requiresUpdates() {
         return requiresUpdates | !controls.isEmpty();
     }
-    
     /**
-     * Subclasses can call this with true to denote that they require 
+     * Subclasses can call this with true to denote that they require
      * updateLogicalState() to be called even if they contain no controls.
      * Setting this to false reverts to the default behavior of only
      * updating if the spatial has controls.  This is not meant to
-     * indicate dynamic state in any way and must be called while 
+     * indicate dynamic state in any way and must be called while
      * unattached or an IllegalStateException is thrown.  It is designed
      * to be called during object construction and then never changed, ie:
      * it's meant to be subclass specific state and not runtime state.
@@ -251,12 +260,12 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
         // override it for more optimal behavior.  Node and Geometry will override
         // it to false if the class is Node.class or Geometry.class.
         // This means that all subclasses will default to the old behavior
-        // unless they opt in. 
+        // unless they opt in.
         if( parent != null ) {
-            throw new IllegalStateException("setRequiresUpdates() cannot be called once attached."); 
+            throw new IllegalStateException("setRequiresUpdates() cannot be called once attached.");
         }
         this.requiresUpdates = f;
-    } 
+    }
 
     /**
      * Indicate that the transform of this spatial has changed and that
@@ -269,35 +278,33 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
 
     protected void setLightListRefresh() {
         refreshFlags |= RF_LIGHTLIST;
-        
         // Make sure next updateGeometricState() visits this branch
         // to update lights.
         Spatial p = parent;
         while (p != null) {
-            //if (p.refreshFlags != 0) {
-                // any refresh flag is sufficient, 
-                // 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) {
                 // The parent already has this flag,
                 // so must all ancestors.
                 return;
             }
-            
             p.refreshFlags |= RF_CHILD_LIGHTLIST;
             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
      * a refresh is required.
@@ -315,10 +322,9 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
             p = p.parent;
         }
     }
-    
     /**
      * (Internal use only) Forces a refresh of the given types of data.
-     * 
+     *
      * @param transforms Refresh world transform based on parents'
      * @param bounds Refresh bounding volume data based on child nodes
      * @param lights Refresh light list based on parents'
@@ -401,9 +407,9 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
     /**
      * Returns the local {@link LightList}, which are the lights
      * that were directly attached to this <code>Spatial</code> through the
-     * {@link #addLight(com.jme3.light.Light) } and 
+     * {@link #addLight(com.jme3.light.Light) } and
      * {@link #removeLight(com.jme3.light.Light) } methods.
-     * 
+     *
      * @return The local light list
      */
     public LightList getLocalLightList() {
@@ -414,13 +420,36 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      * Returns the world {@link LightList}, containing the lights
      * combined from all this <code>Spatial's</code> parents up to and including
      * this <code>Spatial</code>'s lights.
-     * 
+     *
      * @return The combined world light list
      */
     public LightList getWorldLightList() {
         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
      * 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
      * rotation based on a position in world space and an up vector. It computes the rotation
      * to transform the z-axis to point onto 'position' and the y-axis to 'up'.
-     * Unlike {@link Quaternion#lookAt(com.jme3.math.Vector3f, com.jme3.math.Vector3f) } 
+     * Unlike {@link Quaternion#lookAt(com.jme3.math.Vector3f, com.jme3.math.Vector3f) }
      * this method takes a world position to look at and not a relative direction.
      *
      * Note : 28/01/2013 this method has been fixed as it was not taking into account the parent rotation.
      * This was resulting in improper rotation when the spatial had rotated parent nodes.
-     * This method is intended to work in world space, so no matter what parent graph the 
+     * This method is intended to work in world space, so no matter what parent graph the
      * spatial has, it will look at the given position in world space.
-     * 
+     *
      * @param position
      *            where to look at in terms of world coordinates
      * @param upVector
@@ -522,10 +551,8 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
         TempVars vars = TempVars.get();
 
         Vector3f compVecA = vars.vect4;
-      
         compVecA.set(position).subtractLocal(worldTranslation);
-        getLocalRotation().lookAt(compVecA, upVector);        
-        
+        getLocalRotation().lookAt(compVecA, upVector);
         if ( getParent() != null ) {
             Quaternion rot=vars.quat1;
             rot =  rot.set(parent.getWorldRotation()).inverseLocal().multLocal(getLocalRotation());
@@ -552,15 +579,63 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
             worldLights.update(localLights, null);
             refreshFlags &= ~RF_LIGHTLIST;
         } 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().
      * 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.
      */
     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.
      *
      * @see Spatial#addControl(com.jme3.scene.control.Control)
-     * @see Spatial#getControl(java.lang.Class) 
+     * @see Spatial#getControl(java.lang.Class)
      */
     public void runControlRender(RenderManager rm, ViewPort vp) {
         if (controls.isEmpty()) {
@@ -686,26 +761,25 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      * Add a control to the list of controls.
      * @param control The control to add.
      *
-     * @see Spatial#removeControl(java.lang.Class) 
+     * @see Spatial#removeControl(java.lang.Class)
      */
     public void addControl(Control control) {
         boolean before = requiresUpdates();
         controls.add(control);
         control.setSpatial(this);
         boolean after = requiresUpdates();
-        
         // If the requirement to be updated has changed
         // then we need to let the parent node know so it
         // can rebuild its update list.
         if( parent != null && before != after ) {
-            parent.invalidateUpdateList();   
+            parent.invalidateUpdateList();
         }
     }
 
     /**
      * Removes the first control that is an instance of the given class.
      *
-     * @see Spatial#addControl(com.jme3.scene.control.Control) 
+     * @see Spatial#addControl(com.jme3.scene.control.Control)
      */
     public void removeControl(Class<? extends Control> controlType) {
         boolean before = requiresUpdates();
@@ -717,23 +791,22 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
             }
         }
         boolean after = requiresUpdates();
-        
         // If the requirement to be updated has changed
         // then we need to let the parent node know so it
         // can rebuild its update list.
         if( parent != null && before != after ) {
-            parent.invalidateUpdateList();   
+            parent.invalidateUpdateList();
         }
     }
 
     /**
      * Removes the given control from this spatial's controls.
-     * 
+     *
      * @param control The control to remove
      * @return True if the control was successfully removed. False if the
      * control is not assigned to this spatial.
      *
-     * @see Spatial#addControl(com.jme3.scene.control.Control) 
+     * @see Spatial#addControl(com.jme3.scene.control.Control)
      */
     public boolean removeControl(Control control) {
         boolean before = requiresUpdates();
@@ -743,14 +816,12 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
         }
 
         boolean after = requiresUpdates();
-        
         // If the requirement to be updated has changed
         // then we need to let the parent node know so it
         // can rebuild its update list.
         if( parent != null && before != after ) {
-            parent.invalidateUpdateList();   
+            parent.invalidateUpdateList();
         }
-        
         return result;
     }
 
@@ -761,7 +832,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      * @param controlType The superclass of the control to look for.
      * @return The first instance in the list of the controlType class, or null.
      *
-     * @see Spatial#addControl(com.jme3.scene.control.Control) 
+     * @see Spatial#addControl(com.jme3.scene.control.Control)
      */
     public <T extends Control> T getControl(Class<T> controlType) {
         for (Control c : controls.getArray()) {
@@ -790,7 +861,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
     /**
      * @return The number of controls attached to this Spatial.
      * @see Spatial#addControl(com.jme3.scene.control.Control)
-     * @see Spatial#removeControl(java.lang.Class) 
+     * @see Spatial#removeControl(java.lang.Class)
      */
     public int getNumControls() {
         return controls.size();
@@ -815,7 +886,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      * Calling this when the Spatial is attached to a node
      * will cause undefined results. User code should only call this
      * method on Spatials having no parent.
-     * 
+     *
      * @see Spatial#getWorldLightList()
      * @see Spatial#getWorldTransform()
      * @see Spatial#getWorldBound()
@@ -835,6 +906,9 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
         if ((refreshFlags & RF_BOUND) != 0) {
             updateWorldBound();
         }
+        if ((refreshFlags & RF_MATPARAM_OVERRIDE) != 0) {
+            updateMatParamOverrides();
+        }
         
         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.
-     * 
+     *
      * @param light The light to remove.
-     * @see Spatial#addLight(com.jme3.light.Light) 
+     * @see Spatial#addLight(com.jme3.light.Light)
      */
     public void removeLight(Light light) {
         localLights.remove(light);
@@ -1261,12 +1335,43 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      * Note that meshes of geometries are not cloned explicitly, they
      * 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 {
             Spatial clone = (Spatial) super.clone();
             if (worldBound != null) {
@@ -1279,6 +1384,13 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
             clone.localLights.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.
             // This node already has the refresh flags
             // 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.setTransformRefresh();
             clone.setLightListRefresh();
+            clone.setMatParamOverrideRefresh();
 
             clone.controls = new SafeArrayList<Control>(Control.class);
             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
      * on the clone.
      *
-     * @see Mesh#cloneForAnim() 
+     * @see Mesh#cloneForAnim()
      */
     @Override
     public Spatial clone() {
@@ -1342,7 +1455,73 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      *
      * @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) {
         if (userData == null) {
@@ -1350,7 +1529,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
         }
 
         if(data == null){
-            userData.remove(key);            
+            userData.remove(key);
         }else if (data instanceof Savable) {
             userData.put(key, (Savable) data);
         } else {
@@ -1419,6 +1598,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
         capsule.write(shadowMode, "shadow_mode", ShadowMode.Inherit);
         capsule.write(localTransform, "transform", Transform.IDENTITY);
         capsule.write(localLights, "lights", null);
+        capsule.writeSavableArrayList(localOverrides, "overrides", null);
 
         // Shallow clone the controls array to convert its type.
         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.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
         //the AnimControl creates the SkeletonControl for old files and add it to the spatial.
         //The SkeletonControl must be the last in the stack so we add the list of all other control before it.
-        //When backward compatibility won't be needed anymore this can be replaced by : 
+        //When backward compatibility won't be needed anymore this can be replaced by :
         //controls = ic.readSavableArrayList("controlsList", null));
         controls.addAll(0, ic.readSavableArrayList("controlsList", null));
 
@@ -1508,9 +1694,9 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
     /**
      * <code>setQueueBucket</code> determines at what phase of the
      * rendering process this Spatial will rendered. See the
-     * {@link Bucket} enum for an explanation of the various 
+     * {@link Bucket} enum for an explanation of the various
      * render queue buckets.
-     * 
+     *
      * @param queueBucket
      *            The bucket to use for this Spatial.
      */
@@ -1595,7 +1781,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      *
      * @return store if not null, otherwise, a new matrix containing the result.
      *
-     * @see Spatial#getWorldTransform() 
+     * @see Spatial#getWorldTransform()
      */
     public Matrix4f getLocalToWorldMatrix(Matrix4f store) {
         if (store == null) {

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

+ 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 java.io.IOException;
 import java.util.HashMap;
+import java.util.Map;
 
 public class InstancedNode extends GeometryGroupNode {
-    
+
     static int getGeometryStartIndex2(Geometry geom) {
         return getGeometryStartIndex(geom);
     }
-    
+
     static void setGeometryStartIndex2(Geometry geom, int startIndex) {
         setGeometryStartIndex(geom, startIndex);
     }
-    
-    private static final class InstanceTypeKey implements Cloneable {
+
+    private static final class InstanceTypeKey implements Cloneable, JmeCloneable {
 
         Mesh mesh;
         Material material;
@@ -70,7 +71,7 @@ public class InstancedNode extends GeometryGroupNode {
             this.material = material;
             this.lodLevel = lodLevel;
         }
-        
+
         public InstanceTypeKey(){
         }
 
@@ -97,7 +98,7 @@ public class InstancedNode extends GeometryGroupNode {
             }
             return true;
         }
-        
+
         @Override
         public InstanceTypeKey clone() {
             try {
@@ -106,26 +107,41 @@ public class InstancedNode extends GeometryGroupNode {
                 throw new AssertionError();
             }
         }
+
+        @Override
+        public Object jmeClone() {
+            try {
+                return super.clone();
+            } catch( CloneNotSupportedException e ) {
+                throw new AssertionError();
+            }
+        }
+
+        @Override
+        public void cloneFields( Cloner cloner, Object original ) {
+            this.mesh = cloner.clone(mesh);
+            this.material = cloner.clone(material);
+        }
     }
-    
+
     private static class InstancedNodeControl implements Control, JmeCloneable {
 
         private InstancedNode node;
-        
+
         public InstancedNodeControl() {
         }
-        
+
         public InstancedNodeControl(InstancedNode node) {
             this.node = node;
         }
-        
+
         @Override
         public Control cloneForSpatial(Spatial spatial) {
-            return this; 
+            return this;
             // WARNING: Sets wrong control on spatial. Will be
             // fixed automatically by InstancedNode.clone() method.
         }
-        
+
         @Override
         public Object jmeClone() {
             try {
@@ -133,52 +149,52 @@ public class InstancedNode extends GeometryGroupNode {
             } catch( CloneNotSupportedException e ) {
                 throw new RuntimeException("Error cloning control", e);
             }
-        }     
+        }
 
         @Override
-        public void cloneFields( Cloner cloner, Object original ) { 
+        public void cloneFields( Cloner cloner, Object original ) {
             this.node = cloner.clone(node);
         }
-         
+
         public void setSpatial(Spatial spatial){
         }
-        
+
         public void update(float tpf){
         }
-        
+
         public void render(RenderManager rm, ViewPort vp) {
             node.renderFromControl();
         }
-        
+
         public void write(JmeExporter ex) throws IOException {
         }
 
         public void read(JmeImporter im) throws IOException {
         }
     }
-    
+
     protected InstancedNodeControl control;
-    
-    protected HashMap<Geometry, InstancedGeometry> igByGeom 
+
+    protected HashMap<Geometry, InstancedGeometry> igByGeom
             = new HashMap<Geometry, InstancedGeometry>();
-    
+
     private InstanceTypeKey lookUp = new InstanceTypeKey();
-    
-    private HashMap<InstanceTypeKey, InstancedGeometry> instancesMap = 
+
+    private HashMap<InstanceTypeKey, InstancedGeometry> instancesMap =
             new HashMap<InstanceTypeKey, InstancedGeometry>();
-    
+
     public InstancedNode() {
         super();
         // NOTE: since we are deserializing,
         // the control is going to be added automatically here.
     }
-    
+
     public InstancedNode(String name) {
         super(name);
         control = new InstancedNodeControl(this);
         addControl(control);
     }
-    
+
     private void renderFromControl() {
         for (InstancedGeometry ig : instancesMap.values()) {
             ig.updateInstances();
@@ -207,7 +223,7 @@ public class InstancedNode extends GeometryGroupNode {
 
         return ig;
     }
-    
+
     private void addToInstancedGeometry(Geometry geom) {
         Material material = geom.getMaterial();
         MatParam param = material.getParam("UseInstancing");
@@ -216,20 +232,20 @@ public class InstancedNode extends GeometryGroupNode {
                     + "parameter to true on the material prior "
                     + "to adding it to InstancedNode");
         }
-        
+
         InstancedGeometry ig = lookUpByGeometry(geom);
         igByGeom.put(geom, ig);
         geom.associateWithGroupNode(this, 0);
         ig.addInstance(geom);
     }
-    
+
     private void removeFromInstancedGeometry(Geometry geom) {
         InstancedGeometry ig = igByGeom.remove(geom);
         if (ig != null) {
             ig.deleteInstance(geom);
         }
     }
-    
+
     private void relocateInInstancedGeometry(Geometry geom) {
         InstancedGeometry oldIG = igByGeom.get(geom);
         InstancedGeometry newIG = lookUpByGeometry(geom);
@@ -242,7 +258,7 @@ public class InstancedNode extends GeometryGroupNode {
             igByGeom.put(geom, newIG);
         }
     }
-    
+
     private void ungroupSceneGraph(Spatial s) {
         if (s instanceof Node) {
             for (Spatial sp : ((Node) s).getChildren()) {
@@ -253,14 +269,14 @@ public class InstancedNode extends GeometryGroupNode {
             if (g.isGrouped()) {
                 // Will invoke onGeometryUnassociated automatically.
                 g.unassociateFromGroupNode();
-                
+
                 if (InstancedNode.getGeometryStartIndex(g) != -1) {
                     throw new AssertionError();
                 }
             }
         }
     }
-    
+
     @Override
     public Spatial detachChildAt(int index) {
         Spatial s = super.detachChildAt(index);
@@ -269,7 +285,7 @@ public class InstancedNode extends GeometryGroupNode {
         }
         return s;
     }
-    
+
     private void instance(Spatial n) {
         if (n instanceof Geometry) {
             Geometry g = (Geometry) n;
@@ -285,20 +301,20 @@ public class InstancedNode extends GeometryGroupNode {
             }
         }
     }
-    
+
     public void instance() {
         instance(this);
     }
-    
+
     @Override
     public Node clone() {
         return clone(true);
     }
-    
+
     @Override
     public Node clone(boolean cloneMaterials) {
         InstancedNode clone = (InstancedNode)super.clone(cloneMaterials);
-        
+
         if (instancesMap.size() > 0) {
             // Remove all instanced geometries from the clone
             for (int i = 0; i < clone.children.size(); i++) {
@@ -312,7 +328,7 @@ public class InstancedNode extends GeometryGroupNode {
                 }
             }
         }
-        
+
         // remove original control from the clone
         clone.controls.remove(this.control);
 
@@ -323,12 +339,35 @@ public class InstancedNode extends GeometryGroupNode {
         clone.lookUp = new InstanceTypeKey();
         clone.igByGeom = new HashMap<Geometry, InstancedGeometry>();
         clone.instancesMap = new HashMap<InstanceTypeKey, InstancedGeometry>();
-        
+
         clone.instance();
-        
+
         return clone;
     }
-    
+
+    /**
+     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
+     */
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) {
+        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
     public void onTransformChange(Geometry geom) {
         // 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()) {
-            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 (!isVarying(info, v)) {
                     declareVariable(source, v);
@@ -397,6 +397,11 @@ public class Glsl100ShaderGenerator extends ShaderGenerator {
         source.append(mapping.getLeftVariable().getNameSpace());
         source.append("_");
         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. 
         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.
      */
-    private ArrayList<ShaderSource> shaderSourceList;
+    private final ArrayList<ShaderSource> shaderSourceList;
 
     /**
      * 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.
      */
-    private IntMap<Attribute> attribs;
+    private final IntMap<Attribute> attribs;
 
     /**
      * Type of shader. The shader will control the pipeline of it's type.
      */
     public static enum ShaderType {
+
         /**
          * Control fragment rasterization. (e.g color of pixel).
          */
-        Fragment,
-
+        Fragment("frag"),
         /**
          * 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
      * after this constructor for the shader to be usable.
      */
     public Shader(){
         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){
             shaderSourceList.add( (ShaderSource)source.createDestructableClone() );
         }
+        
+        uniforms = null;
+        boundUniforms = null;
+        attribs = null;
     }
 
     /**
@@ -248,6 +265,18 @@ public final class Shader extends NativeObject {
         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){
         assert name.startsWith("m_") || name.startsWith("g_");
         Uniform uniform = uniforms.get(name);
@@ -277,6 +306,10 @@ public final class Shader extends NativeObject {
     public ListMap<String, Uniform> getUniformMap(){
         return uniforms;
     }
+    
+    public ArrayList<Uniform> getBoundUniforms() {
+        return boundUniforms;
+    }
 
     public Collection<ShaderSource> getSources(){
         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;
     /**
-     * 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
@@ -70,8 +70,8 @@ public abstract class ShaderGenerator {
         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
      */
-    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.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;
     }
 
@@ -109,6 +114,14 @@ public abstract class ShaderGenerator {
      * @return the code of the generated vertex shader
      */
     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;
 
         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;
 
+    @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
     public String toString(){
         StringBuilder sb = new StringBuilder();
@@ -102,6 +126,10 @@ public class Uniform extends ShaderVariable {
     public Object getValue(){
         return value;
     }
+    
+    public FloatBuffer getMultiData() {
+        return multiData;
+    }
 
     public boolean isSetByCurrentMaterial() {
         return setByCurrentMaterial;
@@ -111,21 +139,6 @@ public class Uniform extends ShaderVariable {
         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(){
         updateNeeded = true;
 
@@ -182,27 +195,43 @@ public class Uniform extends ShaderVariable {
         }
 
         if (value == null) {
-            throw new NullPointerException();
+            throw new IllegalArgumentException("for uniform " + name + ": value cannot be null");
         }
 
         setByCurrentMaterial = true;
 
         switch (type){
             case Matrix3:
+                if (value.equals(this.value)) {
+                    return;
+                }
                 Matrix3f m3 = (Matrix3f) value;
                 if (multiData == null) {
                     multiData = BufferUtils.createFloatBuffer(9);
                 }
                 m3.fillFloatBuffer(multiData, true);
                 multiData.clear();
+                if (this.value == null) {
+                    this.value = new Matrix3f(m3);
+                } else {
+                    ((Matrix3f)this.value).set(m3);
+                }
                 break;
             case Matrix4:
+                if (value.equals(this.value)) {
+                    return;
+                }
                 Matrix4f m4 = (Matrix4f) value;
                 if (multiData == null) {
                     multiData = BufferUtils.createFloatBuffer(16);
                 }
                 m4.fillFloatBuffer(multiData, true);
                 multiData.clear();
+                if (this.value == null) {
+                    this.value = new Matrix4f(m4);
+                } else {
+                    ((Matrix4f)this.value).copy(m4);
+                }
                 break;
             case IntArray:
                 int[] ia = (int[]) value;
@@ -283,11 +312,32 @@ public class Uniform extends ShaderVariable {
                 }
                 multiData.clear();
                 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
             case Int:
             case Float:
             case Boolean:
-                if (this.value != null && this.value.equals(value)) {
+                if (value.equals(this.value)) {
                     return;
                 }
                 this.value = value;
@@ -297,39 +347,38 @@ public class Uniform extends ShaderVariable {
                 break;
         }
 
-        if (multiData != null) {
-            this.value = multiData;
-        }
+//        if (multiData != null) {
+//            this.value = multiData;
+//        }
 
         varType = type;
         updateNeeded = true;
     }
 
     public void setVector4Length(int length){
-        if (location == -1)
+        if (location == -1) {
             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;
         updateNeeded = true;
         setByCurrentMaterial = true;
     }
 
     public void setVector4InArray(float x, float y, float z, float w, int index){
-        if (location == -1)
+        if (location == -1) {
             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;
         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.RenderManager;
 import com.jme3.system.Timer;
-import java.util.List;
+import java.util.ArrayList;
 
 /**
  * <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}
      * 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++) {
             Uniform u = params.get(i);
             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"),
     Texture2D(false,true,"sampler2D|sampler2DShadow"),
     Texture3D(false,true,"sampler3D"),
-    TextureArray(false,true,"sampler2DArray"),
+    TextureArray(false,true,"sampler2DArray|sampler2DArrayShadow"),
     TextureCubeMap(false,true,"samplerCube"),
     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(){
         initInThread();
 
-        while (!needClose.get()){
+        do {
             listener.update();
 
-            if (frameRate > 0)
+            if (frameRate > 0) {
                 sync(frameRate);
-        }
+            }
+        } while (!needClose.get());
 
         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 {
 
-    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();
 
     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 slot = SLOT_UNDEF;
         int face = -1;
+        int layer = -1;
         
         /**
          * @return The image format of the render buffer.
@@ -160,6 +161,10 @@ public class FrameBuffer extends NativeObject {
                 return "BufferTarget[format=" + format + "]";
             }
         }
+
+        public int getLayer() {
+            return this.layer;
+        }
     }
 
     /**
@@ -324,6 +329,19 @@ public class FrameBuffer extends NativeObject {
         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.
      * This automatically clears all existing textures added previously
@@ -369,6 +387,31 @@ public class FrameBuffer extends NativeObject {
         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.
      * If MRT is enabled, then each subsequently added texture can be
@@ -412,7 +455,20 @@ public class FrameBuffer extends NativeObject {
         depthBuf.tex = tex;
         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. 
      */

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

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

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

@@ -43,7 +43,7 @@ import java.util.*;
  *  the list is changing.</p>
  *
  *  <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"
  *  case by buffering them as a normal ArrayList until the next time the contents
  *  are accessed.</p>
@@ -63,16 +63,16 @@ import java.util.*;
  *  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
  *  original snapshot.
- *  </ul>  
+ *  </ul>
  *
  *  @version   $Revision$
  *  @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
     // 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.
 
     // 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
     //       for the classes like Node and Spatial to use to manage
     //       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 E[] backingArray;
     private int size = 0;
- 
+
     public SafeArrayList(Class<E> elementType) {
-        this.elementType = elementType;        
+        this.elementType = elementType;
     }
-    
+
     public SafeArrayList(Class<E> elementType, Collection<? extends E> c) {
-        this.elementType = elementType;        
+        this.elementType = elementType;
         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) {
-        return (T[])java.lang.reflect.Array.newInstance(type, size);               
+        return (T[])java.lang.reflect.Array.newInstance(type, 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
      *  is guaranteed not to change through further List manipulation.
@@ -114,10 +132,10 @@ public class SafeArrayList<E> implements List<E> {
     public final E[] getArray() {
         if( backingArray != null )
             return backingArray;
-            
+
         if( buffer == null ) {
             backingArray = createArray(0);
-        } else {            
+        } else {
             // Only keep the array or the buffer but never both at
             // the same time.  1) it saves space, 2) it keeps the rest
             // of the code safer.
@@ -126,35 +144,35 @@ public class SafeArrayList<E> implements List<E> {
         }
         return backingArray;
     }
- 
+
     protected final List<E> getBuffer() {
         if( buffer != null )
             return buffer;
-            
+
         if( backingArray == null ) {
             buffer = new ArrayList();
-        } else {       
+        } else {
             // Only keep the array or the buffer but never both at
             // 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) );
             backingArray = null;
         }
         return buffer;
     }
-    
+
     public final int size() {
-        return size;            
+        return size;
     }
-    
+
     public final boolean isEmpty() {
         return size == 0;
     }
-    
+
     public boolean contains(Object o) {
         return indexOf(o) >= 0;
     }
-    
+
     public Iterator<E> iterator() {
         return listIterator();
     }
@@ -162,70 +180,70 @@ public class SafeArrayList<E> implements List<E> {
     public Object[] toArray() {
         return getArray();
     }
-    
+
     public <T> T[] toArray(T[] a) {
- 
+
         E[] array = getArray();
         if (a.length < array.length) {
             return (T[])Arrays.copyOf(array, array.length, a.getClass());
-        } 
- 
+        }
+
         System.arraycopy( array, 0, a, 0, array.length );
-        
+
         if (a.length > array.length) {
             a[array.length] = null;
         }
-           
+
         return a;
     }
-    
+
     public boolean add(E e) {
         boolean result = getBuffer().add(e);
         size = getBuffer().size();
         return result;
     }
-    
+
     public boolean remove(Object o) {
         boolean result = getBuffer().remove(o);
         size = getBuffer().size();
         return result;
     }
-    
+
     public boolean containsAll(Collection<?> c) {
         return Arrays.asList(getArray()).containsAll(c);
     }
-    
+
     public boolean addAll(Collection<? extends E> c) {
         boolean result = getBuffer().addAll(c);
         size = getBuffer().size();
         return result;
     }
-    
+
     public boolean addAll(int index, Collection<? extends E> c) {
         boolean result = getBuffer().addAll(index, c);
         size = getBuffer().size();
         return result;
     }
-    
+
     public boolean removeAll(Collection<?> c) {
         boolean result = getBuffer().removeAll(c);
         size = getBuffer().size();
         return result;
     }
-    
+
     public boolean retainAll(Collection<?> c) {
         boolean result = getBuffer().retainAll(c);
         size = getBuffer().size();
         return result;
     }
-    
+
     public void clear() {
         getBuffer().clear();
         size = 0;
     }
-    
+
     public boolean equals(Object o) {
-        if( o == this ) 
+        if( o == this )
             return true;
         if( !(o instanceof List) ) //covers null too
             return false;
@@ -240,9 +258,9 @@ public class SafeArrayList<E> implements List<E> {
             if( o1 == null || !o1.equals(o2) )
                 return false;
         }
-        return !(i1.hasNext() || !i2.hasNext());            
+        return !(i1.hasNext() || !i2.hasNext());
     }
-    
+
     public int hashCode() {
         // Exactly the hash code described in the List interface, basically
         E[] array = getArray();
@@ -252,30 +270,30 @@ public class SafeArrayList<E> implements List<E> {
         }
         return result;
     }
-    
+
     public final E get(int index) {
         if( backingArray != null )
             return backingArray[index];
         if( buffer != null )
             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) {
         return getBuffer().set(index, element);
     }
-    
+
     public void add(int index, E element) {
         getBuffer().add(index, element);
         size = getBuffer().size();
     }
-    
+
     public E remove(int index) {
         E result = getBuffer().remove(index);
         size = getBuffer().size();
         return result;
     }
-    
+
     public int indexOf(Object o) {
         E[] array = getArray();
         for( int i = 0; i < array.length; i++ ) {
@@ -289,7 +307,7 @@ public class SafeArrayList<E> implements List<E> {
         }
         return -1;
     }
-    
+
     public int lastIndexOf(Object o) {
         E[] array = getArray();
         for( int i = array.length - 1; i >= 0; i-- ) {
@@ -303,29 +321,29 @@ public class SafeArrayList<E> implements List<E> {
         }
         return -1;
     }
-    
+
     public ListIterator<E> listIterator() {
         return new ArrayIterator<E>(getArray(), 0);
     }
-    
+
     public ListIterator<E> listIterator(int index) {
         return new ArrayIterator<E>(getArray(), index);
     }
-    
+
     public List<E> subList(int fromIndex, int toIndex) {
-    
+
         // 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);
         return Collections.unmodifiableList(raw);
     }
- 
+
     public String toString() {
- 
+
         E[] array = getArray();
         if( array.length == 0 ) {
             return "[]";
         }
-        
+
         StringBuilder sb = new StringBuilder();
         sb.append('[');
         for( int i = 0; i < array.length; i++ ) {
@@ -337,63 +355,63 @@ public class SafeArrayList<E> implements List<E> {
         sb.append(']');
         return sb.toString();
     }
- 
+
     protected class ArrayIterator<E> implements ListIterator<E> {
         private E[] array;
         private int next;
         private int lastReturned;
-        
+
         protected ArrayIterator( E[] array, int index ) {
             this.array = array;
             this.next = index;
             this.lastReturned = -1;
         }
-        
+
         public boolean hasNext() {
             return next != array.length;
         }
-        
+
         public E next() {
             if( !hasNext() )
                 throw new NoSuchElementException();
             lastReturned = next++;
             return array[lastReturned];
         }
-        
+
         public boolean hasPrevious() {
-            return next != 0;           
-        }        
-        
+            return next != 0;
+        }
+
         public E previous() {
             if( !hasPrevious() )
                 throw new NoSuchElementException();
             lastReturned = --next;
             return array[lastReturned];
         }
-        
+
         public int nextIndex() {
-            return next;       
+            return next;
         }
-        
+
         public int previousIndex() {
             return next - 1;
         }
-        
+
         public void remove() {
             // This operation is not so easy to do but we will fake it.
             // The issue is that the backing list could be completely
             // 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
             // elements then we'll remove one of those instead.  Either
             // way, none of those changes are reflected in this iterator.
             SafeArrayList.this.remove( array[lastReturned] );
         }
-        
+
         public void set(E e) {
             throw new UnsupportedOperationException();
         }
-        
+
         public void add(E e) {
             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.util.HashMap;
 import java.util.IdentityHashMap;
+import java.util.logging.Logger;
+import java.util.logging.Level;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
@@ -49,7 +51,7 @@ import java.util.concurrent.ConcurrentHashMap;
  *  <p>By default, objects that do not implement JmeCloneable will
  *  be treated like normal Java Cloneable objects.  If the object does
  *  not implement the JmeCloneable or the regular JDK Cloneable interfaces
- *  AND has no special handling defined then an IllegalArgumentException 
+ *  AND has no special handling defined then an IllegalArgumentException
  *  will be thrown.</p>
  *
  *  <p>Enhanced object cloning is done in a two step process.  First,
@@ -60,7 +62,7 @@ import java.util.concurrent.ConcurrentHashMap;
  *  can easily have a regular shallow clone implementation just like any
  *  normal Java objects.  Second, the deep cloning of fields happens after
  *  creation wich means that the clone is available to future field cloning
- *  to resolve circular references.</p> 
+ *  to resolve circular references.</p>
  *
  *  <p>Similar to Java serialization, the handling of specific object
  *  types can be customized.  This allows certain objects to be cloned gracefully
@@ -87,31 +89,33 @@ import java.util.concurrent.ConcurrentHashMap;
  *  Foo fooClone = cloner.clone(foo);
  *  cloner.clearIndex(); // prepare it for reuse
  *  Foo fooClone2 = cloner.clone(foo);
- * 
+ *
  *  // Example 2: using the utility method that self-instantiates a temporary cloner.
  *  Foo fooClone = Cloner.deepClone(foo);
- *   
+ *
  *  </pre>
  *
  *  @author    Paul Speed
  */
 public class Cloner {
- 
+
+    static Logger log = Logger.getLogger(Cloner.class.getName());
+
     /**
      *  Keeps track of the objects that have been cloned so far.
-     */   
+     */
     private IdentityHashMap<Object, Object> index = new IdentityHashMap<Object, Object>();
- 
+
     /**
      *  Custom functions for cloning objects.
      */
     private Map<Class, CloneFunction> functions = new HashMap<Class, CloneFunction>();
- 
+
     /**
      *  Cache the clone methods once for all cloners.
-     */   
+     */
     private static final Map<Class, Method> methodCache = new ConcurrentHashMap<>();
- 
+
     /**
      *  Creates a new cloner with only default clone functions and an empty
      *  object index.
@@ -121,41 +125,41 @@ public class Cloner {
         ListCloneFunction listFunction = new ListCloneFunction();
         functions.put(java.util.ArrayList.class, listFunction);
         functions.put(java.util.LinkedList.class, listFunction);
-        functions.put(java.util.concurrent.CopyOnWriteArrayList.class, listFunction); 
+        functions.put(java.util.concurrent.CopyOnWriteArrayList.class, listFunction);
         functions.put(java.util.Vector.class, listFunction);
         functions.put(java.util.Stack.class, listFunction);
         functions.put(com.jme3.util.SafeArrayList.class, listFunction);
     }
- 
+
     /**
      *  Convenience utility function that creates a new Cloner, uses it to
      *  deep clone the object, and then returns the result.
      */
     public static <T> T deepClone( T object ) {
         return new Cloner().clone(object);
-    }     
- 
+    }
+
     /**
      *  Deeps clones the specified object, reusing previous clones when possible.
-     * 
+     *
      *  <p>Object cloning priority works as follows:</p>
      *  <ul>
      *  <li>If the object has already been cloned then its clone is returned.
      *  <li>If there is a custom CloneFunction then it is called to clone the object.
-     *  <li>If the object implements Cloneable then its clone() method is called, arrays are 
+     *  <li>If the object implements Cloneable then its clone() method is called, arrays are
      *      deep cloned with entries passing through clone().
      *  <li>If the object implements JmeCloneable then its cloneFields() method is called on the
      *      clone.
-     *  <li>Else an IllegalArgumentException is thrown. 
+     *  <li>Else an IllegalArgumentException is thrown.
      *  </ul>
      *
      *  Note: objects returned by this method may not have yet had their cloneField()
      *  method called.
-     */   
+     */
     public <T> T clone( T object ) {
         return clone(object, true);
     }
- 
+
     /**
      *  Internal method to work around a Java generics typing issue by
      *  isolating the 'bad' case into a method with suppressed warnings.
@@ -167,20 +171,20 @@ public class Cloner {
         // Wrapping it in a method at least isolates the warning suppression
         return (Class<T>)object.getClass();
     }
- 
+
     /**
      *  Deeps clones the specified object, reusing previous clones when possible.
-     * 
+     *
      *  <p>Object cloning priority works as follows:</p>
      *  <ul>
      *  <li>If the object has already been cloned then its clone is returned.
-     *  <li>If useFunctions is true and there is a custom CloneFunction then it is 
+     *  <li>If useFunctions is true and there is a custom CloneFunction then it is
      *      called to clone the object.
-     *  <li>If the object implements Cloneable then its clone() method is called, arrays are 
+     *  <li>If the object implements Cloneable then its clone() method is called, arrays are
      *      deep cloned with entries passing through clone().
      *  <li>If the object implements JmeCloneable then its cloneFields() method is called on the
      *      clone.
-     *  <li>Else an IllegalArgumentException is thrown. 
+     *  <li>Else an IllegalArgumentException is thrown.
      *  </ul>
      *
      *  <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()
      *  method called.
-     */   
+     */
     public <T> T clone( T object, boolean useFunctions ) {
+
         if( object == 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
         Object clone = index.get(object);
         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.
-        CloneFunction<T> f = getCloneFunction(type); 
+        CloneFunction<T> f = getCloneFunction(type);
         if( f != null ) {
             T result = f.cloneObject(this, object);
-            
+
             // Store the object in the identity map so that any circular references
-            // are resolvable. 
-            index.put(object, result); 
-            
+            // are resolvable.
+            index.put(object, result);
+
             // Now call the function again to deep clone the fields
             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() ) {
-            // Perform an array clone        
+            // Perform an array clone
             clone = arrayClone(object);
-            
+
             // Array clone already indexes the clone
         } else if( object instanceof JmeCloneable ) {
             // Use the two-step cloning semantics
             clone = ((JmeCloneable)object).jmeClone();
-            
+
             // Store the object in the identity map so that any circular references
             // are resolvable
-            index.put(object, clone); 
-            
+            index.put(object, clone);
+
             ((JmeCloneable)clone).cloneFields(this, object);
         } else if( object instanceof Cloneable ) {
-            
+
             // Perform a regular Java shallow clone
             try {
                 clone = javaClone(object);
             } catch( CloneNotSupportedException e ) {
                 throw new IllegalArgumentException("Object is not cloneable, type:" + type, e);
             }
-            
+
             // Store the object in the identity map so that any circular references
             // are resolvable
-            index.put(object, clone); 
+            index.put(object, clone);
         } else {
             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);
     }
- 
+
     /**
-     *  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 ) {
         if( function == null ) {
@@ -262,24 +291,59 @@ public class Cloner {
             functions.put(type, function);
         }
     }
- 
+
     /**
      *  Returns a previously registered clone function for the specified type or null
      *  if there is no custom clone function for the type.
-     */ 
+     */
     @SuppressWarnings("unchecked")
     public <T> CloneFunction<T> getCloneFunction( Class<T> type ) {
-        return (CloneFunction<T>)functions.get(type); 
-    } 
- 
+        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
      *  cloning operation.
      */
     public void clearIndex() {
         index.clear();
-    }     
- 
+    }
+
     /**
      *  Performs a raw shallow Java clone using reflection.  This call does NOT
      *  check against the clone index and so will return new objects every time
@@ -287,51 +351,54 @@ public class Cloner {
      *  not ever, depending on the caller) get resolved.
      *
      *  <p>This method is provided as a convenient way for CloneFunctions to call
-     *  clone() and objects without necessarily knowing their real type.</p>  
-     */   
+     *  clone() and objects without necessarily knowing their real type.</p>
+     */
     public <T> T javaClone( T object ) throws CloneNotSupportedException {
+        if( object == null ) {
+            return null;
+        }
         Method m = methodCache.get(object.getClass());
         if( m == null ) {
             try {
                 // Lookup the method and cache it
                 m = object.getClass().getMethod("clone");
-            } catch( NoSuchMethodException e ) {            
+            } catch( NoSuchMethodException e ) {
                 throw new CloneNotSupportedException("No public clone method found for:" + object.getClass());
             }
             methodCache.put(object.getClass(), m);
-            
+
             // Note: yes we might cache the method twice... but so what?
         }
- 
+
         try {
-            Class<? extends T> type = objectClass(object);       
+            Class<? extends T> type = objectClass(object);
             return type.cast(m.invoke(object));
         } catch( IllegalAccessException | InvocationTargetException e ) {
             throw new RuntimeException("Error cloning object of type:" + object.getClass(), e);
-        }          
+        }
     }
-    
+
     /**
      *  Clones a primitive array by coping it and clones an object
      *  array by coping it and then running each of its values through
      *  Cloner.clone().
      */
     protected <T> T arrayClone( T object ) {
- 
+
         // Java doesn't support the cloning of arrays through reflection unless
         // you open access to Object's protected clone array... which requires
         // elevated privileges.  So we will do a work-around that is slightly less
         // elegant.
         // This should be 100% allowed without a case but Java generics
         // is not that smart
-        Class<T> type = objectClass(object); 
+        Class<T> type = objectClass(object);
         Class elementType = type.getComponentType();
-        int size = Array.getLength(object); 
+        int size = Array.getLength(object);
         Object clone = Array.newInstance(elementType, size);
- 
+
         // Store the clone for later lookups
-        index.put(object, clone); 
-        
+        index.put(object, clone);
+
         if( elementType.isPrimitive() ) {
             // Then our job is a bit easier
             System.arraycopy(object, 0, clone, 0, size);
@@ -341,8 +408,8 @@ public class Cloner {
                 Object element = clone(Array.get(object, i));
                 Array.set(clone, i, element);
             }
-        }           
-        
+        }
+
         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
         Boolean UseInstancing
 
-        Boolean BackfaceShadows: false
+        Boolean BackfaceShadows : false
     }
 
- Technique {
+    Technique {
         LightMode SinglePass
         
         VertexShader GLSL100:   Common/MatDefs/Light/SPLighting.vert
@@ -149,7 +149,7 @@ MaterialDef Phong Lighting {
             SEPARATE_TEXCOORD : SeparateTexCoord
             DISCARD_ALPHA : AlphaDiscardThreshold
             USE_REFLECTION : EnvMap
-            SPHERE_MAP : SphereMap  
+            SPHERE_MAP : EnvMapAsSphereMap  
             NUM_BONES : NumberOfBones                        
             INSTANCING : UseInstancing
         }
@@ -188,7 +188,7 @@ MaterialDef Phong Lighting {
             SEPARATE_TEXCOORD : SeparateTexCoord
             DISCARD_ALPHA : AlphaDiscardThreshold
             USE_REFLECTION : EnvMap
-            SPHERE_MAP : SphereMap  
+            SPHERE_MAP : EnvMapAsSphereMap  
             NUM_BONES : NumberOfBones                        
             INSTANCING : UseInstancing
         }
@@ -209,7 +209,6 @@ MaterialDef Phong Lighting {
         }
 
         Defines {
-            COLOR_MAP : ColorMap
             DISCARD_ALPHA : AlphaDiscardThreshold
             NUM_BONES : NumberOfBones
             INSTANCING : UseInstancing
@@ -234,8 +233,7 @@ MaterialDef Phong Lighting {
             HARDWARE_SHADOWS : HardwareShadows
             FILTER_MODE : FilterMode
             PCFEDGE : PCFEdge
-            DISCARD_ALPHA : AlphaDiscardThreshold           
-            COLOR_MAP : ColorMap
+            DISCARD_ALPHA : AlphaDiscardThreshold
             SHADOWMAP_SIZE : ShadowMapSize
             FADE : FadeInfo
             PSSM : Splits
@@ -268,8 +266,7 @@ MaterialDef Phong Lighting {
             HARDWARE_SHADOWS : HardwareShadows
             FILTER_MODE : FilterMode
             PCFEDGE : PCFEdge
-            DISCARD_ALPHA : AlphaDiscardThreshold           
-            COLOR_MAP : ColorMap
+            DISCARD_ALPHA : AlphaDiscardThreshold
             SHADOWMAP_SIZE : ShadowMapSize
             FADE : FadeInfo
             PSSM : Splits
@@ -343,10 +340,6 @@ MaterialDef Phong Lighting {
         Defines {
             VERTEX_COLOR : UseVertexColor
             MATERIAL_COLORS : UseMaterialColors
-            V_TANGENT : VTangent
-            MINNAERT  : Minnaert
-            WARDISO   : WardIso
-
             DIFFUSEMAP : DiffuseMap
             NORMALMAP : NormalMap
             SPECULARMAP : SpecularMap

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

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

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

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

+ 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_LightViewProjectionMatrix5;
 #else
+    uniform vec3 m_LightDir;
     #ifndef PSSM    
-        uniform vec3 m_LightPos;    
-        uniform vec3 m_LightDir;       
+        uniform vec3 m_LightPos;
     #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;
 
+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.material.*;
 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.Vector2f;
 import com.jme3.math.Vector3f;
+import com.jme3.shader.DefineList;
 import com.jme3.shader.Shader;
 import com.jme3.shader.VarType;
 import com.jme3.texture.Texture;
@@ -73,6 +77,7 @@ public class J3MLoader implements AssetLoader {
     private Material material;
     private TechniqueDef technique;
     private RenderState renderState;
+    private ArrayList<String> presetDefines = new ArrayList<String>();
 
     private EnumMap<Shader.ShaderType, String> shaderLanguage;
     private EnumMap<Shader.ShaderType, String> shaderName;
@@ -115,6 +120,10 @@ public class J3MLoader implements AssetLoader {
             throw new IOException("LightMode statement syntax incorrect");
         }
         LightMode lm = LightMode.valueOf(split[1]);
+        if (lm == LightMode.FixedPipeline) {
+            throw new UnsupportedOperationException("OpenGL1 is not supported");
+        }
+        
         technique.setLightMode(lm);
     }
 
@@ -458,6 +467,8 @@ public class J3MLoader implements AssetLoader {
             renderState.setDepthFunc(RenderState.TestFunction.valueOf(split[1]));
         }else if (split[0].equals("AlphaFunc")){
             renderState.setAlphaFunc(RenderState.TestFunction.valueOf(split[1]));
+        }else if (split[0].equals("LineWidth")){
+            renderState.setLineWidth(Float.parseFloat(split[1]));
         } else {
             throw new MatParseException(null, split[0], statement);
         }
@@ -493,10 +504,22 @@ public class J3MLoader implements AssetLoader {
     private void readDefine(String statement) throws IOException{
         String[] split = statement.split(":");
         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){
-            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{
             throw new IOException("Define syntax incorrect");
         }
@@ -558,15 +581,28 @@ public class J3MLoader implements AssetLoader {
         }
         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{
         isUseNodes = false;
         String[] split = techStat.getLine().split(whitespacePattern);
+        
         if (split.length == 1) {
-            technique = new TechniqueDef(null);
+            String techniqueUniqueName = materialDef.getAssetName() + "@Default";
+            technique = new TechniqueDef(null, techniqueUniqueName.hashCode());
         } else if (split.length == 2) {
             String techName = split[1];
-            technique = new TechniqueDef(techName);
+            String techniqueUniqueName = materialDef.getAssetName() + "@" + techName;
+            technique = new TechniqueDef(techName, techniqueUniqueName.hashCode());
         } else {
             throw new IOException("Technique statement syntax incorrect");
         }
@@ -577,18 +613,40 @@ public class J3MLoader implements AssetLoader {
 
         if(isUseNodes){
             nodesLoaderDelegate.computeConditions();
+            
             //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");
         }
 
         if (shaderName.containsKey(Shader.ShaderType.Vertex) && shaderName.containsKey(Shader.ShaderType.Fragment)) {
             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);
         technique = null;
         shaderLanguage.clear();
         shaderName.clear();
+        presetDefines.clear();
     }
 
     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.ShaderUtils;
 import com.jme3.shader.UniformBinding;
+import com.jme3.shader.VarType;
 import com.jme3.shader.VariableMapping;
 import com.jme3.util.blockparser.Statement;
 import java.io.IOException;
@@ -583,7 +584,7 @@ public class ShaderNodeLoaderDelegate {
                     //multiplicity is not an int attempting to find for a material parameter.
                     MatParam mp = findMatParam(multiplicity);
                     if (mp != null) {
-                        addDefine(multiplicity);
+                        addDefine(multiplicity, VarType.Int);
                         multiplicity = multiplicity.toUpperCase();
                     } else {
                         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
      */
-    public void addDefine(String paramName) {
+    public void addDefine(String paramName, VarType paramType) {
         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) {
             MatParam param = findMatParam(string);
             if (param != null) {
-                addDefine(param.getName());
+                addDefine(param.getName(), param.getVarType());
             } else {
                 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.setType(var.getType());
+            right.setMultiplicity(var.getMultiplicity());
             mapping.setRightVariable(right);            
             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 static org.junit.Assert.assertEquals;
+import org.junit.Ignore;
+
 /**
  * Verifies that algorithms in {@link FastMath} are working correctly.
  * 
@@ -56,4 +59,39 @@ public class FastMathTest {
             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.TechniqueDef;
 import com.jme3.material.plugins.J3MLoader;
+import com.jme3.renderer.Caps;
 import com.jme3.shader.DefineList;
 import com.jme3.shader.Shader;
-import com.jme3.shader.ShaderKey;
 import com.jme3.shader.plugins.GLSLLoader;
 import com.jme3.system.JmeSystem;
+import java.util.EnumSet;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -33,23 +34,22 @@ public class ShaderCheck {
         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);
-        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();
                 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());
             }
         }
+        throw new UnsupportedOperationException();
     }
           
     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 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 Canvas canvas;
-    protected Application app;
+    protected LegacyApplication app;
 
     protected String appClass;
     protected URL appCfg = null;
@@ -103,7 +103,7 @@ public class AppletHarness extends Applet {
         JmeSystem.setLowPermissions(true);
 
         try{
-            Class<? extends Application> clazz = (Class<? extends Application>) Class.forName(appClass);
+            Class<? extends LegacyApplication> clazz = (Class<? extends LegacyApplication>) Class.forName(appClass);
             app = clazz.newInstance();
         }catch (ClassNotFoundException ex){
             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 java.awt.EventQueue;
 import java.awt.Graphics2D;
+import java.awt.GraphicsEnvironment;
 import java.awt.RenderingHints;
 import java.awt.geom.AffineTransform;
 import java.awt.image.AffineTransformOp;
@@ -116,12 +117,16 @@ public class JmeDesktopSystem extends JmeSystemDelegate {
 
     @Override
     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
@@ -129,6 +134,9 @@ public class JmeDesktopSystem extends JmeSystemDelegate {
         if (SwingUtilities.isEventDispatchThread()) {
             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);
         settings.copyFrom(sourceSettings);
@@ -333,27 +341,13 @@ public class JmeDesktopSystem extends JmeSystemDelegate {
         if (initialized) {
             return;
         }
-
         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

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä