diff --git a/jme3-core/src/main/java/com/jme3/material/MatParamOverride.java b/jme3-core/src/main/java/com/jme3/material/MatParamOverride.java index af46eba7e..0843f3093 100644 --- a/jme3-core/src/main/java/com/jme3/material/MatParamOverride.java +++ b/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); } diff --git a/jme3-core/src/main/java/com/jme3/scene/Node.java b/jme3-core/src/main/java/com/jme3/scene/Node.java index 35526c3ce..992da344c 100644 --- a/jme3-core/src/main/java/com/jme3/scene/Node.java +++ b/jme3-core/src/main/java/com/jme3/scene/Node.java @@ -75,7 +75,6 @@ public class Node extends Spatial { * requiresUpdate() method. */ private SafeArrayList 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; } - /** * getVertexCount 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()); } - /** * * attachChildAt 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 queue) { queue.addAll(children); diff --git a/jme3-core/src/main/java/com/jme3/scene/Spatial.java b/jme3-core/src/main/java/com/jme3/scene/Spatial.java index 6f77f530d..6392c24d1 100644 --- a/jme3-core/src/main/java/com/jme3/scene/Spatial.java +++ b/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 localOverrides; + protected ArrayList 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(); + worldOverrides = new ArrayList(); 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 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 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(worldOverrides); + clone.localOverrides = new ArrayList(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(); + } + worldOverrides = new ArrayList(); //changed for backward compatibility with j3o files generated before the AnimControl/SkeletonControl split //the AnimControl creates the SkeletonControl for old files and add it to the spatial. //The SkeletonControl must be the last in the stack so we add the list of all other control before it. diff --git a/jme3-core/src/test/java/com/jme3/scene/MPOTestUtils.java b/jme3-core/src/test/java/com/jme3/scene/MPOTestUtils.java new file mode 100644 index 000000000..2eb87fbde --- /dev/null +++ b/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 actualOverrides = new HashSet(); + for (MatParamOverride override : scene.getWorldOverrides()) { + actualOverrides.add(override); + } + + Set expectedOverrides = new HashSet(); + 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); + } +} diff --git a/jme3-core/src/test/java/com/jme3/scene/SceneMatParamOverrideTest.java b/jme3-core/src/test/java/com/jme3/scene/SceneMatParamOverrideTest.java new file mode 100644 index 000000000..df1f58744 --- /dev/null +++ b/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 clonedOverrides = clonedScene.getChild("A").getLocalOverrides(); + ArrayList 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)); + } +}