Bläddra i källkod

Merge pull request #467 from jMonkeyEngine/tdl-mpo

TDL/MPO
Kirill Vainer 9 år sedan
förälder
incheckning
ec9f8100c3
50 ändrade filer med 3987 tillägg och 1621 borttagningar
  1. 13 129
      .gitignore
  2. 6 0
      common.gradle
  3. 4 1
      jme3-core/src/main/java/com/jme3/animation/SkeletonControl.java
  4. 0 9
      jme3-core/src/main/java/com/jme3/asset/AssetManager.java
  5. 0 33
      jme3-core/src/main/java/com/jme3/asset/DesktopAssetManager.java
  6. 0 4
      jme3-core/src/main/java/com/jme3/material/MatParam.java
  7. 151 0
      jme3-core/src/main/java/com/jme3/material/MatParamOverride.java
  8. 0 6
      jme3-core/src/main/java/com/jme3/material/MatParamTexture.java
  9. 137 355
      jme3-core/src/main/java/com/jme3/material/Material.java
  10. 3 0
      jme3-core/src/main/java/com/jme3/material/RenderState.java
  11. 102 145
      jme3-core/src/main/java/com/jme3/material/Technique.java
  12. 215 72
      jme3-core/src/main/java/com/jme3/material/TechniqueDef.java
  13. 97 0
      jme3-core/src/main/java/com/jme3/material/logic/DefaultTechniqueDefLogic.java
  14. 178 0
      jme3-core/src/main/java/com/jme3/material/logic/MultiPassLightingLogic.java
  15. 218 0
      jme3-core/src/main/java/com/jme3/material/logic/SinglePassLightingLogic.java
  16. 157 0
      jme3-core/src/main/java/com/jme3/material/logic/StaticPassLightingLogic.java
  17. 97 0
      jme3-core/src/main/java/com/jme3/material/logic/TechniqueDefLogic.java
  18. 19 10
      jme3-core/src/main/java/com/jme3/renderer/RenderManager.java
  19. 24 10
      jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java
  20. 10 0
      jme3-core/src/main/java/com/jme3/renderer/queue/GeometryList.java
  21. 3 2
      jme3-core/src/main/java/com/jme3/renderer/queue/OpaqueComparator.java
  22. 4 1
      jme3-core/src/main/java/com/jme3/scene/Mesh.java
  23. 18 25
      jme3-core/src/main/java/com/jme3/scene/Node.java
  24. 125 35
      jme3-core/src/main/java/com/jme3/scene/Spatial.java
  25. 179 286
      jme3-core/src/main/java/com/jme3/shader/DefineList.java
  26. 56 23
      jme3-core/src/main/java/com/jme3/shader/Shader.java
  27. 30 17
      jme3-core/src/main/java/com/jme3/shader/ShaderGenerator.java
  28. 0 201
      jme3-core/src/main/java/com/jme3/shader/ShaderKey.java
  29. 82 33
      jme3-core/src/main/java/com/jme3/shader/Uniform.java
  30. 3 2
      jme3-core/src/main/java/com/jme3/shader/UniformBindingManager.java
  31. 4 3
      jme3-core/src/main/java/com/jme3/system/NullContext.java
  32. 1 1
      jme3-core/src/main/java/com/jme3/system/NullRenderer.java
  33. 6 13
      jme3-core/src/main/resources/Common/MatDefs/Light/Lighting.j3md
  34. 61 5
      jme3-core/src/plugins/java/com/jme3/material/plugins/J3MLoader.java
  35. 5 4
      jme3-core/src/plugins/java/com/jme3/material/plugins/ShaderNodeLoaderDelegate.java
  36. 52 0
      jme3-core/src/test/java/com/jme3/asset/LoadShaderSourceTest.java
  37. 538 0
      jme3-core/src/test/java/com/jme3/material/MaterialMatParamOverrideTest.java
  38. 38 0
      jme3-core/src/test/java/com/jme3/math/FastMathTest.java
  39. 342 0
      jme3-core/src/test/java/com/jme3/renderer/OpaqueComparatorTest.java
  40. 173 0
      jme3-core/src/test/java/com/jme3/scene/MPOTestUtils.java
  41. 278 0
      jme3-core/src/test/java/com/jme3/scene/SceneMatParamOverrideTest.java
  42. 300 0
      jme3-core/src/test/java/com/jme3/shader/DefineListTest.java
  43. 78 0
      jme3-core/src/test/java/com/jme3/system/MockJmeSystemDelegate.java
  44. 55 0
      jme3-core/src/test/java/com/jme3/system/TestUtil.java
  45. 12 12
      jme3-core/src/tools/java/jme3tools/shadercheck/ShaderCheck.java
  46. 14 6
      jme3-desktop/src/main/java/com/jme3/system/JmeDesktopSystem.java
  47. 0 156
      jme3-examples/src/main/java/jme3test/app/TestNativeLoader.java
  48. 92 0
      jme3-examples/src/main/java/jme3test/material/TestMatParamOverride.java
  49. 3 3
      jme3-examples/src/main/java/jme3test/material/TestShaderNodes.java
  50. 4 19
      jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglContext.java

+ 13 - 129
.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,113 +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
-.DS_Store
-/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

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

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

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

+ 3 - 0
jme3-core/src/main/java/com/jme3/material/RenderState.java

@@ -822,6 +822,9 @@ public class RenderState implements Cloneable, Savable {
      * @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;

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

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

+ 24 - 10
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());
         }
 
@@ -964,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;
@@ -978,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:
@@ -2689,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;
         }
 
-        //this is kept for backward compatibility.
-        if (mesh.getLineWidth() != -1 && context.lineWidth != mesh.getLineWidth()) {
+        if (count > 1 && !caps.contains(Caps.MeshInstancing)) {
+            throw new RendererException("Mesh instancing is not supported by the video hardware");
+        }
+
+        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.

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

@@ -175,7 +175,7 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
     private IntMap<VertexBuffer> buffers = new IntMap<VertexBuffer>();
     private VertexBuffer[] lodLevels;
     private float pointSize = 1;
-    private float lineWidth = -1;
+    private float lineWidth = 1;
 
     private transient int vertexArrayID = -1;
 
@@ -634,6 +634,9 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
      */
     @Deprecated
     public void setLineWidth(float lineWidth) {
+        if (lineWidth < 1f) {
+            throw new IllegalArgumentException("lineWidth must be greater than or equal to 1.0");
+        }
         this.lineWidth = lineWidth;
     }
 

+ 18 - 25
jme3-core/src/main/java/com/jme3/scene/Node.java

@@ -75,7 +75,6 @@ public class Node extends Spatial {
      * 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.
@@ -100,7 +99,6 @@ public class Node extends Spatial {
      */
     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
@@ -141,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;
@@ -239,11 +248,13 @@ public class Node extends Spatial {
             // This branch has no geometric state that requires updates.
             return;
         }
-
         if ((refreshFlags & RF_LIGHTLIST) != 0){
             updateWorldLightList();
         }
 
+        if ((refreshFlags & RF_MATPARAM_OVERRIDE) != 0) {
+            updateMatParamOverrides();
+        }
         if ((refreshFlags & RF_TRANSFORM) != 0){
             // combine with parent transforms- same for all spatial
             // subclasses.
@@ -251,7 +262,6 @@ public class Node extends Spatial {
         }
 
         refreshFlags &= ~RF_CHILD_LIGHTLIST;
-
         if (!children.isEmpty()) {
             // the important part- make sure child geometric state is refreshed
             // first before updating own world bound. This saves
@@ -287,7 +297,6 @@ 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.
@@ -321,7 +330,6 @@ 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
@@ -345,20 +353,18 @@ 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();
     }
 
@@ -433,7 +439,8 @@ public class Node extends Spatial {
             child.setTransformRefresh();
             // lights are also inherited from parent
             child.setLightListRefresh();
-
+            child.setMatParamOverrideRefresh();
+            
             invalidateUpdateList();
         }
         return child;
@@ -519,7 +526,6 @@ public class Node extends Spatial {
         }
         return null;
     }
-
     /**
      * determines if the provided Spatial is contained in the children list of
      * this node.
@@ -567,39 +573,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.
         // 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 ;))
-
         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();
@@ -692,7 +691,6 @@ 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;
     }
 
@@ -732,7 +730,6 @@ public class Node extends Spatial {
         // cloning this list is fine.
         this.updateList = cloner.clone(updateList);
     }
-
     @Override
     public void write(JmeExporter e) throws IOException {
         super.write(e);
@@ -744,7 +741,6 @@ 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,
                                       e.getCapsule(this).readSavableArrayList("children", null) );
 
@@ -754,7 +750,6 @@ public class Node extends Spatial {
                 child.parent = this;
             }
         }
-
         super.read(e);
     }
 
@@ -775,7 +770,6 @@ public class Node extends Spatial {
             }
         }
     }
-
     @Override
     public void depthFirstTraversal(SceneGraphVisitor visitor) {
         for (Spatial child : children.getArray()) {
@@ -783,7 +777,6 @@ public class Node extends Spatial {
         }
         visitor.visit(this);
     }
-
     @Override
     protected void breadthFirstTraversal(SceneGraphVisitor visitor, Queue<Spatial> queue) {
         queue.addAll(children);

+ 125 - 35
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;
@@ -121,9 +122,10 @@ 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_LIGHTLIST = 0x04, // changes in light lists 
+                               RF_CHILD_LIGHTLIST = 0x08, // some child need geometry update
+                               RF_MATPARAM_OVERRIDE = 0x10;
+    
     protected CullHint cullHint = CullHint.Inherit;
     protected BatchHint batchHint = BatchHint.Inherit;
     /**
@@ -135,7 +137,11 @@ 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.
      */
     protected String name;
@@ -195,13 +201,15 @@ 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<MatParamOverride>();
+        worldOverrides = new ArrayList<MatParamOverride>();
+
         refreshFlags |= RF_BOUND;
     }
 
@@ -222,7 +230,6 @@ 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
      * updateLogicalState() to be called even if they contain no controls.
@@ -272,35 +279,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.
@@ -318,7 +323,6 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
             p = p.parent;
         }
     }
-
     /**
      * (Internal use only) Forces a refresh of the given types of data.
      *
@@ -424,6 +428,29 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
         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.
@@ -525,10 +552,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);
-
         if ( getParent() != null ) {
             Quaternion rot=vars.quat1;
             rot =  rot.set(parent.getWorldRotation()).inverseLocal().multLocal(getLocalRotation());
@@ -555,15 +580,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.
@@ -696,7 +769,6 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
         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.
@@ -720,7 +792,6 @@ 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.
@@ -746,14 +817,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();
         }
-
         return result;
     }
 
@@ -838,7 +907,10 @@ 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;
     }
 
@@ -1292,6 +1364,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
         // the transforms and stuff get refreshed.
         clone.setTransformRefresh();
         clone.setLightListRefresh();
+        clone.setMatParamOverrideRefresh();
 
         return clone;
     }
@@ -1312,6 +1385,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.
@@ -1333,6 +1413,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++) {
@@ -1419,6 +1500,8 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
         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.
@@ -1515,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);
@@ -1538,6 +1622,12 @@ 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<MatParamOverride>();
+        }
+        worldOverrides = new ArrayList<MatParamOverride>();
+
         //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.

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

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

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

+ 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

+ 61 - 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);
     }
 
@@ -495,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");
         }
@@ -560,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");
         }
@@ -579,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{

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

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

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

+ 0 - 156
jme3-examples/src/main/java/jme3test/app/TestNativeLoader.java

@@ -1,156 +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 jme3test.app;
-
-import com.jme3.system.NativeLibraryLoader;
-import java.io.File;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-/**
- * Try to load some natives.
- * 
- * @author Kirill Vainer
- */
-public class TestNativeLoader {
-    
-    private static final File WORKING_FOLDER = new File(System.getProperty("user.dir"));
-    
-    private static void tryLoadLwjgl() {
-        NativeLibraryLoader.loadNativeLibrary("lwjgl", true);
-        System.out.println("Succeeded in loading LWJGL.\n\tVersion: " + 
-                           org.lwjgl.Sys.getVersion());
-    }
-    
-    private static void tryLoadJinput() {
-        NativeLibraryLoader.loadNativeLibrary("jinput", true);
-        NativeLibraryLoader.loadNativeLibrary("jinput-dx8", true);
-        
-        net.java.games.input.ControllerEnvironment ce =
-            net.java.games.input.ControllerEnvironment.getDefaultEnvironment();
-        if (ce.isSupported()) {
-            net.java.games.input.Controller[] c =
-                    ce.getControllers();
-            
-            System.out.println("Succeeded in loading JInput.\n\tVersion: " + 
-                            net.java.games.util.Version.getVersion());
-        }
-    }
-    
-    private static void tryLoadOpenAL() {
-        NativeLibraryLoader.loadNativeLibrary("openal", true);
-        
-        try {
-            org.lwjgl.openal.AL.create();
-            String renderer = org.lwjgl.openal.AL10.alGetString(org.lwjgl.openal.AL10.AL_RENDERER);
-            String vendor = org.lwjgl.openal.AL10.alGetString(org.lwjgl.openal.AL10.AL_VENDOR);
-            String version = org.lwjgl.openal.AL10.alGetString(org.lwjgl.openal.AL10.AL_VERSION);
-            System.out.println("Succeeded in loading OpenAL.");
-            System.out.println("\tVersion: " + version);
-        } catch (org.lwjgl.LWJGLException ex) {
-            throw new RuntimeException(ex);
-        } finally {
-            if (org.lwjgl.openal.AL.isCreated()) {
-                org.lwjgl.openal.AL.destroy();
-            }
-        }
-    }
-    
-    private static void tryLoadOpenGL() {
-        org.lwjgl.opengl.Pbuffer pb = null;
-        try {
-            pb = new org.lwjgl.opengl.Pbuffer(1, 1, new org.lwjgl.opengl.PixelFormat(0, 0, 0), null);
-            pb.makeCurrent();
-            String version = org.lwjgl.opengl.GL11.glGetString(org.lwjgl.opengl.GL11.GL_VERSION);
-            System.out.println("Succeeded in loading OpenGL.\n\tVersion: " + version);
-        } catch (org.lwjgl.LWJGLException ex) {
-            throw new RuntimeException(ex);
-        } finally {
-            if (pb != null) {
-                pb.destroy();
-            }
-        }
-    }
-    
-    private static void tryLoadBulletJme() {
-        if (NativeLibraryLoader.isUsingNativeBullet()) {
-            NativeLibraryLoader.loadNativeLibrary("bulletjme", true);
-
-            com.jme3.bullet.PhysicsSpace physSpace = new com.jme3.bullet.PhysicsSpace();
-
-            System.out.println("Succeeded in loading BulletJme.");
-        } else {
-            System.out.println("Native bullet not included. Cannot test loading.");
-        }
-    }
-    
-    private static void cleanupNativesFolder(File folder) {
-        for (File file : folder.listFiles()) {
-            String lowerCaseName = file.getName().toLowerCase();
-            if (lowerCaseName.contains("lwjgl") ||
-                lowerCaseName.contains("jinput") ||
-                lowerCaseName.contains("openal") ||
-                lowerCaseName.contains("bulletjme")) {
-                file.delete();
-            }
-        }
-    }
-    
-    public static void main(String[] args) {
-        Logger.getLogger("").getHandlers()[0].setLevel(Level.WARNING);
-        Logger.getLogger(NativeLibraryLoader.class.getName()).setLevel(Level.ALL);
-        
-        // Get a bit more output from LWJGL about issues.
-        // System.setProperty("org.lwjgl.util.Debug", "true");
-        
-        // Extracting to working folder is no brainer. 
-        // Choose some random path, then load LWJGL.
-        File customNativesFolder = new File("CustomNativesFolder");
-        customNativesFolder.mkdirs();
-        
-        if (!customNativesFolder.isDirectory()) {
-            throw new IllegalStateException("Failed to make custom natives folder");
-        }
-        
-        // Let's cleanup our folders first.
-        cleanupNativesFolder(WORKING_FOLDER);
-        cleanupNativesFolder(customNativesFolder);
-        
-        NativeLibraryLoader.setCustomExtractionFolder(customNativesFolder.getAbsolutePath());
-        
-        tryLoadLwjgl();
-        tryLoadOpenGL();
-        tryLoadOpenAL();
-        tryLoadJinput();
-        tryLoadBulletJme();
-    }
-}

+ 92 - 0
jme3-examples/src/main/java/jme3test/material/TestMatParamOverride.java

@@ -0,0 +1,92 @@
+/*
+ * 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 jme3test.material;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.input.KeyInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.material.MatParamOverride;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.shape.Box;
+import com.jme3.shader.VarType;
+
+/**
+ * Test if {@link MatParamOverride}s are working correctly.
+ *
+ * @author Kirill Vainer
+ */
+public class TestMatParamOverride extends SimpleApplication {
+
+    private Box box = new Box(1, 1, 1);
+    private MatParamOverride override = new MatParamOverride(VarType.Vector4, "Color", ColorRGBA.Yellow);
+
+    public static void main(String[] args) {
+        TestMatParamOverride app = new TestMatParamOverride();
+        app.start();
+    }
+
+    private void createBox(float location, ColorRGBA color) {
+        Geometry geom = new Geometry("Box", box);
+        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        mat.setColor("Color", color);
+        geom.setMaterial(mat);
+        geom.move(location, 0, 0);
+        rootNode.attachChild(geom);
+    }
+
+    @Override
+    public void simpleInitApp() {
+        inputManager.setCursorVisible(true);
+
+        createBox(-3, ColorRGBA.Red);
+        createBox(0, ColorRGBA.Green);
+        createBox(3, ColorRGBA.Blue);
+
+        inputManager.addMapping("override", new KeyTrigger(KeyInput.KEY_SPACE));
+        inputManager.addListener(new ActionListener() {
+            @Override
+            public void onAction(String name, boolean isPressed, float tpf) {
+                if (name.equals("override") && isPressed) {
+                    if (!rootNode.getLocalMatParamOverrides().isEmpty()) {
+                        rootNode.clearMatParamOverrides();
+                    } else {
+                        rootNode.addMatParamOverride(override);
+                    }
+                    System.out.println(rootNode.getLocalMatParamOverrides());
+                }
+            }
+        }, "override");
+    }
+}

+ 3 - 3
jme3-examples/src/main/java/jme3test/material/TestShaderNodes.java

@@ -30,9 +30,9 @@ public class TestShaderNodes extends SimpleApplication {
         mat.selectTechnique("Default", renderManager);
         Technique t = mat.getActiveTechnique();
 
-        for (Shader.ShaderSource shaderSource : t.getShader().getSources()) {
-            System.out.println(shaderSource.getSource());
-        }
+//        for (Shader.ShaderSource shaderSource : t.getShader().getSources()) {
+//            System.out.println(shaderSource.getSource());
+//        }
 
         
         mat.setColor("Color", ColorRGBA.Yellow);

+ 4 - 19
jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglContext.java

@@ -29,6 +29,7 @@
  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
+
 package com.jme3.system.lwjgl;
 
 import com.jme3.input.lwjgl.JInputJoyInput;
@@ -69,7 +70,6 @@ public abstract class LwjglContext implements JmeContext {
     private static final Logger logger = Logger.getLogger(LwjglContext.class.getName());
 
     protected static final String THREAD_NAME = "jME3 Main";
-
     protected AtomicBoolean created = new AtomicBoolean(false);
     protected AtomicBoolean renderable = new AtomicBoolean(false);
     protected final Object createdLock = new Object();
@@ -113,7 +113,6 @@ public abstract class LwjglContext implements JmeContext {
             return null;
         }
     }
-
     protected int determineMaxSamples(int requestedSamples) {
         try {
             // If we already have a valid context, determine samples using current
@@ -131,13 +130,11 @@ public abstract class LwjglContext implements JmeContext {
         } catch (LWJGLException ex) {
             listener.handleError("Failed to check if display is current", ex);
         }
-
         if ((Pbuffer.getCapabilities() & Pbuffer.PBUFFER_SUPPORTED) == 0) {
             // No pbuffer, assume everything is supported.
             return Integer.MAX_VALUE;
         } else {
             Pbuffer pb = null;
-
             // OpenGL2 method: Create pbuffer and query samples
             // from GL_ARB_framebuffer_object or GL_EXT_framebuffer_multisample.
             try {
@@ -162,7 +159,6 @@ public abstract class LwjglContext implements JmeContext {
             }
         }
     }
-
     protected void loadNatives() {
         if (JmeSystem.isLowPermissions()) {
             return;
@@ -179,7 +175,6 @@ public abstract class LwjglContext implements JmeContext {
         }
         NativeLibraryLoader.loadNativeLibrary("lwjgl", true);
     }
-
     protected int getNumSamplesToUse() {
         int samples = 0;
         if (settings.getSamples() > 1) {
@@ -190,7 +185,6 @@ public abstract class LwjglContext implements JmeContext {
                         "Couldn''t satisfy antialiasing samples requirement: x{0}. "
                         + "Video hardware only supports: x{1}",
                         new Object[]{samples, supportedSamples});
-
                 samples = supportedSamples;
             }
         }
@@ -202,48 +196,43 @@ public abstract class LwjglContext implements JmeContext {
             throw new RendererException("OpenGL 2.0 or higher is "
                     + "required for jMonkeyEngine");
         }
-
+        
         if (settings.getRenderer().equals(AppSettings.LWJGL_OPENGL2)
                 || settings.getRenderer().equals(AppSettings.LWJGL_OPENGL3)) {
             GL gl = new LwjglGL();
             GLExt glext = new LwjglGLExt();
             GLFbo glfbo;
-
+            
             if (GLContext.getCapabilities().OpenGL30) {
                 glfbo = new LwjglGLFboGL3();
             } else {
                 glfbo = new LwjglGLFboEXT();
             }
-
+            
             if (settings.getBoolean("GraphicsDebug")) {
                 gl = new GLDebugDesktop(gl, glext, glfbo);
                 glext = (GLExt) gl;
                 glfbo = (GLFbo) gl;
             }
-
             if (settings.getBoolean("GraphicsTiming")) {
                 GLTimingState timingState = new GLTimingState();
                 gl = (GL) GLTiming.createGLTiming(gl, timingState, GL.class, GL2.class, GL3.class, GL4.class);
                 glext = (GLExt) GLTiming.createGLTiming(glext, timingState, GLExt.class);
                 glfbo = (GLFbo) GLTiming.createGLTiming(glfbo, timingState, GLFbo.class);
             }
-
             if (settings.getBoolean("GraphicsTrace")) {
                 gl = (GL) GLTracer.createDesktopGlTracer(gl, GL.class, GL2.class, GL3.class, GL4.class);
                 glext = (GLExt) GLTracer.createDesktopGlTracer(glext, GLExt.class);
                 glfbo = (GLFbo) GLTracer.createDesktopGlTracer(glfbo, GLFbo.class);
             }
-
             renderer = new GLRenderer(gl, glext, glfbo);
             renderer.initialize();
         } else {
             throw new UnsupportedOperationException("Unsupported renderer: " + settings.getRenderer());
         }
-
         if (GLContext.getCapabilities().GL_ARB_debug_output && settings.getBoolean("GraphicsDebug")) {
             ARBDebugOutput.glDebugMessageCallbackARB(new ARBDebugOutputCallback(new LwjglGLDebugOutputHandler()));
         }
-
         renderer.setMainFrameBufferSrgb(settings.isGammaCorrection());
         renderer.setLinearizeSrgbImages(settings.isGammaCorrection());
 
@@ -270,15 +259,12 @@ public abstract class LwjglContext implements JmeContext {
             createdLock.notifyAll();
         }
     }
-
     public void internalCreate() {
         timer = new LwjglTimer();
-
         synchronized (createdLock) {
             created.set(true);
             createdLock.notifyAll();
         }
-
         if (renderable.get()) {
             initContextFirstTime();
         } else {
@@ -308,7 +294,6 @@ public abstract class LwjglContext implements JmeContext {
     public boolean isCreated() {
         return created.get();
     }
-
     public boolean isRenderable() {
         return renderable.get();
     }