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 f269446a5..e37984e62 100644 --- a/jme3-core/src/main/java/com/jme3/scene/Node.java +++ b/jme3-core/src/main/java/com/jme3/scene/Node.java @@ -140,6 +140,18 @@ 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(); @@ -243,6 +255,10 @@ public class Node extends Spatial { updateWorldLightList(); } + if ((refreshFlags & RF_MATPARAM_OVERRIDE) != 0) { + updateMatParamOverrides(); + } + if ((refreshFlags & RF_TRANSFORM) != 0){ // combine with parent transforms- same for all spatial // subclasses. @@ -350,6 +366,7 @@ public class Node extends Spatial { // 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()}); @@ -432,6 +449,7 @@ public class Node extends Spatial { child.setTransformRefresh(); // lights are also inherited from parent child.setLightListRefresh(); + child.setMatParamOverrideRefresh(); invalidateUpdateList(); } 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 bd8eb7ff6..225193486 100644 --- a/jme3-core/src/main/java/com/jme3/scene/Spatial.java +++ b/jme3-core/src/main/java/com/jme3/scene/Spatial.java @@ -120,7 +120,8 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab protected static final int RF_TRANSFORM = 0x01, // need light resort + combine transforms RF_BOUND = 0x02, RF_LIGHTLIST = 0x04, // changes in light lists - RF_CHILD_LIGHTLIST = 0x08; // some child need geometry update + RF_CHILD_LIGHTLIST = 0x08, // some child need geometry update + RF_MATPARAM_OVERRIDE = 0x10; protected CullHint cullHint = CullHint.Inherit; protected BatchHint batchHint = BatchHint.Inherit; @@ -133,6 +134,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. */ @@ -200,6 +205,9 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab localLights = new LightList(this); worldLights = new LightList(this); + localOverrides = new ArrayList(); + worldOverrides = new ArrayList(); + refreshFlags |= RF_BOUND; } @@ -275,19 +283,6 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab // 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. @@ -299,6 +294,19 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab } } + 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. @@ -428,7 +436,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; } /** @@ -442,7 +450,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; } /** @@ -576,13 +584,47 @@ 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(); } /** @@ -859,6 +901,9 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab if ((refreshFlags & RF_BOUND) != 0) { updateWorldBound(); } + if ((refreshFlags & RF_MATPARAM_OVERRIDE) != 0) { + updateMatParamOverrides(); + } assert refreshFlags == 0; } @@ -1303,6 +1348,9 @@ 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. @@ -1443,6 +1491,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); @@ -1466,6 +1515,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(); + } + 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)); + } +}