Jelajahi Sumber

MPO: implement propagation and add test

Conflicts:
	jme3-core/src/main/java/com/jme3/scene/Node.java
	jme3-core/src/main/java/com/jme3/scene/Spatial.java
Kirill Vainer 9 tahun lalu
induk
melakukan
2b35f288c2

+ 5 - 1
jme3-core/src/main/java/com/jme3/material/MatParamOverride.java

@@ -34,7 +34,11 @@ package com.jme3.material;
 import com.jme3.shader.VarType;
 
 public final class MatParamOverride extends MatParam {
-    
+
+    public MatParamOverride() {
+        super();
+    }
+
     public MatParamOverride(VarType type, String name, Object value) {
         super(type, name, value);
     }

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

+ 76 - 37
jme3-core/src/main/java/com/jme3/scene/Spatial.java

@@ -122,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;
     /**
@@ -136,7 +137,10 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      */
     protected LightList localLights;
     protected transient LightList worldLights;
-    /**
+    protected ArrayList<MatParamOverride> localOverrides;
+    protected ArrayList<MatParamOverride> worldOverrides;
+
+    /** 
      * This spatial's name.
      */
     protected String name;
@@ -196,13 +200,14 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      */
     protected Spatial(String name) {
         this.name = name;
-
         localTransform = new Transform();
         worldTransform = new Transform();
 
         localLights = new LightList(this);
         worldLights = new LightList(this);
 
+        localOverrides = new ArrayList<MatParamOverride>();
+        worldOverrides = new ArrayList<MatParamOverride>();
         refreshFlags |= RF_BOUND;
     }
 
@@ -223,7 +228,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.
@@ -273,35 +277,32 @@ 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.
@@ -319,7 +320,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.
      *
@@ -431,7 +431,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      * @return The list of local material parameter overrides.
      */
     public ArrayList<MatParamOverride> getLocalOverrides() {
-        return null;
+        return localOverrides;
     }
 
     /**
@@ -445,7 +445,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      * @return The list of world material parameter overrides.
      */
     public ArrayList<MatParamOverride> getWorldOverrides() {
-        return null;
+        return worldOverrides;
     }
 
     /**
@@ -549,10 +549,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());
@@ -579,15 +577,49 @@ 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 #getLocalOverrides()
+     */
+    public void addMatParamOverride(MatParamOverride override) {
+        localOverrides.add(override);
+        setMatParamOverrideRefresh();
+    }
+
+    public void removeMatParamOverride(MatParamOverride override) {
+        if (worldOverrides.remove(override)) {
+            setMatParamOverrideRefresh();
+        }
+    }
+
+    public void clearMatParamOverrides() {
+        if (!worldOverrides.isEmpty()) {
+            setMatParamOverrideRefresh();
+        }
+        worldOverrides.clear();
+    }
+
     /**
      * Should only be called from updateGeometricState().
      * In most cases should not be subclassed.
@@ -720,7 +752,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.
@@ -744,7 +775,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.
@@ -770,14 +800,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;
     }
 
@@ -862,7 +890,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;
     }
 
@@ -1336,6 +1367,8 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
             clone.localLights.setOwner(clone);
             clone.worldLights.setOwner(clone);
 
+            clone.worldOverrides = new ArrayList<MatParamOverride>(worldOverrides);
+            clone.localOverrides = new ArrayList<MatParamOverride>(localOverrides);
             // No need to force cloned to update.
             // This node already has the refresh flags
             // set below so it will have to update anyway.
@@ -1539,6 +1572,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);
@@ -1562,6 +1596,11 @@ 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.

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

@@ -0,0 +1,174 @@
+/*
+ * 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 static com.jme3.shader.VarType.Texture2D;
+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.getWorldOverrides()) {
+            actualOverrides.add(override);
+        }
+
+        Set<MatParamOverride> expectedOverrides = new HashSet<MatParamOverride>();
+        Spatial current = scene;
+        while (current != null) {
+            for (MatParamOverride override : current.getLocalOverrides()) {
+                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.getLocalOverrides().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);
+    }
+}

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

@@ -0,0 +1,204 @@
+/*
+ * 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;
+
+
+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_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);
+
+        ArrayList<MatParamOverride> clonedOverrides = clonedScene.getChild("A").getLocalOverrides();
+        ArrayList<MatParamOverride> originalOverrides = originalScene.getChild("A").getLocalOverrides();
+
+        assertNotSame(clonedOverrides, originalOverrides);
+        assertEquals(clonedOverrides, originalOverrides);
+
+        for (int i = 0; i < clonedOverrides.size(); i++) {
+            assertSame(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").getLocalOverrides().get(0));
+        assertEquals(override, loadedScene.getChild("A").getLocalOverrides().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));
+    }
+}