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
experimental^2^2
Kirill Vainer 9 years ago
parent 280733c1ce
commit 2b35f288c2
  1. 4
      jme3-core/src/main/java/com/jme3/material/MatParamOverride.java
  2. 41
      jme3-core/src/main/java/com/jme3/scene/Node.java
  3. 97
      jme3-core/src/main/java/com/jme3/scene/Spatial.java
  4. 174
      jme3-core/src/test/java/com/jme3/scene/MPOTestUtils.java
  5. 204
      jme3-core/src/test/java/com/jme3/scene/SceneMatParamOverrideTest.java

@ -35,6 +35,10 @@ import com.jme3.shader.VarType;
public final class MatParamOverride extends MatParam { public final class MatParamOverride extends MatParam {
public MatParamOverride() {
super();
}
public MatParamOverride(VarType type, String name, Object value) { public MatParamOverride(VarType type, String name, Object value) {
super(type, name, value); super(type, name, value);
} }

@ -75,7 +75,6 @@ public class Node extends Spatial {
* requiresUpdate() method. * requiresUpdate() method.
*/ */
private SafeArrayList<Spatial> updateList = null; private SafeArrayList<Spatial> updateList = null;
/** /**
* False if the update list requires rebuilding. This is Node.class * False if the update list requires rebuilding. This is Node.class
* specific and therefore not included as part of the Spatial update flags. * 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) { public Node(String name) {
super(name); super(name);
// For backwards compatibility, only clear the "requires // For backwards compatibility, only clear the "requires
// update" flag if we are not a subclass of Node. // update" flag if we are not a subclass of Node.
// This prevents subclass from silently failing to receive // 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 @Override
protected void updateWorldBound(){ protected void updateWorldBound(){
super.updateWorldBound(); super.updateWorldBound();
// for a node, the world bound is a combination of all it's children // for a node, the world bound is a combination of all it's children
// bounds // bounds
BoundingVolume resultBound = null; BoundingVolume resultBound = null;
@ -239,11 +248,13 @@ public class Node extends Spatial {
// This branch has no geometric state that requires updates. // This branch has no geometric state that requires updates.
return; return;
} }
if ((refreshFlags & RF_LIGHTLIST) != 0){ if ((refreshFlags & RF_LIGHTLIST) != 0){
updateWorldLightList(); updateWorldLightList();
} }
if ((refreshFlags & RF_MATPARAM_OVERRIDE) != 0) {
updateMatParamOverrides();
}
if ((refreshFlags & RF_TRANSFORM) != 0){ if ((refreshFlags & RF_TRANSFORM) != 0){
// combine with parent transforms- same for all spatial // combine with parent transforms- same for all spatial
// subclasses. // subclasses.
@ -251,7 +262,6 @@ public class Node extends Spatial {
} }
refreshFlags &= ~RF_CHILD_LIGHTLIST; refreshFlags &= ~RF_CHILD_LIGHTLIST;
if (!children.isEmpty()) { if (!children.isEmpty()) {
// the important part- make sure child geometric state is refreshed // the important part- make sure child geometric state is refreshed
// first before updating own world bound. This saves // first before updating own world bound. This saves
@ -287,7 +297,6 @@ public class Node extends Spatial {
return count; return count;
} }
/** /**
* <code>getVertexCount</code> returns the number of vertices contained * <code>getVertexCount</code> returns the number of vertices contained
* in all sub-branches of this node that contain geometry. * in all sub-branches of this node that contain geometry.
@ -321,7 +330,6 @@ public class Node extends Spatial {
public int attachChild(Spatial child) { public int attachChild(Spatial child) {
return attachChildAt(child, children.size()); return attachChildAt(child, children.size());
} }
/** /**
* *
* <code>attachChildAt</code> attaches a child to this node at an index. This node * <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); child.setParent(this);
children.add(index, child); children.add(index, child);
// XXX: Not entirely correct? Forces bound update up the // XXX: Not entirely correct? Forces bound update up the
// tree stemming from the attached child. Also forces // tree stemming from the attached child. Also forces
// transform update down the tree- // transform update down the tree-
child.setTransformRefresh(); child.setTransformRefresh();
child.setLightListRefresh(); child.setLightListRefresh();
child.setMatParamOverrideRefresh();
if (logger.isLoggable(Level.FINE)) { if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE,"Child ({0}) attached to this node ({1})", logger.log(Level.FINE,"Child ({0}) attached to this node ({1})",
new Object[]{child.getName(), getName()}); new Object[]{child.getName(), getName()});
} }
invalidateUpdateList(); invalidateUpdateList();
} }
return children.size(); return children.size();
} }
@ -433,6 +439,7 @@ public class Node extends Spatial {
child.setTransformRefresh(); child.setTransformRefresh();
// lights are also inherited from parent // lights are also inherited from parent
child.setLightListRefresh(); child.setLightListRefresh();
child.setMatParamOverrideRefresh();
invalidateUpdateList(); invalidateUpdateList();
} }
@ -519,7 +526,6 @@ public class Node extends Spatial {
} }
return null; return null;
} }
/** /**
* determines if the provided Spatial is contained in the children list of * determines if the provided Spatial is contained in the children list of
* this node. * this node.
@ -567,39 +573,32 @@ public class Node extends Spatial {
public int collideWith(Collidable other, CollisionResults results){ public int collideWith(Collidable other, CollisionResults results){
int total = 0; int total = 0;
// optimization: try collideWith BoundingVolume to avoid possibly redundant tests on children // optimization: try collideWith BoundingVolume to avoid possibly redundant tests on children
// number 4 in condition is somewhat arbitrary. When there is only one child, the boundingVolume test is redundant at all. // number 4 in condition is somewhat arbitrary. When there is only one child, the boundingVolume test is redundant at all.
// The idea is when there are few children, it can be too expensive to test boundingVolume first. // 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 I'm removing this change until some issues can be addressed and I really
think it needs to be implemented a better way anyway. think it needs to be implemented a better way anyway.
First, it causes issues for anyone doing collideWith() with BoundingVolumes First, it causes issues for anyone doing collideWith() with BoundingVolumes
and expecting it to trickle down to the children. For example, children and expecting it to trickle down to the children. For example, children
with BoundingSphere bounding volumes and collideWith(BoundingSphere). Doing with BoundingSphere bounding volumes and collideWith(BoundingSphere). Doing
a collision check at the parent level then has to do a BoundingSphere to BoundingBox 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 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.) case is tricky and the first sign that this is the wrong approach.)
Second, the rippling changes this caused to 'optimize' collideWith() for this 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 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 idea of calculating a full collision just to see if the two shapes collide at all
is very wasteful. is very wasteful.
A proper implementation should support a simpler boolean check that doesn't do 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% 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 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. 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' 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 code until it can be 'optimized' properly. Hopefully it's not too late to back out
the other dodgy ripples this caused. -pspeed (hindsight-expert ;)) the other dodgy ripples this caused. -pspeed (hindsight-expert ;))
Note: the code itself is relatively simple to implement but I don't have time to 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 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. enough to do all the time for > 1.
if (children.size() > 4) if (children.size() > 4)
{ {
BoundingVolume bv = this.getWorldBound(); 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. // Reset the fields of the clone that should be in a 'new' state.
nodeClone.updateList = null; nodeClone.updateList = null;
nodeClone.updateListValid = false; // safe because parent is nulled out in super.clone() nodeClone.updateListValid = false; // safe because parent is nulled out in super.clone()
return nodeClone; return nodeClone;
} }
@ -732,7 +730,6 @@ public class Node extends Spatial {
// cloning this list is fine. // cloning this list is fine.
this.updateList = cloner.clone(updateList); this.updateList = cloner.clone(updateList);
} }
@Override @Override
public void write(JmeExporter e) throws IOException { public void write(JmeExporter e) throws IOException {
super.write(e); super.write(e);
@ -744,7 +741,6 @@ public class Node extends Spatial {
// XXX: Load children before loading itself!! // XXX: Load children before loading itself!!
// This prevents empty children list if controls query // This prevents empty children list if controls query
// it in Control.setSpatial(). // it in Control.setSpatial().
children = new SafeArrayList( Spatial.class, children = new SafeArrayList( Spatial.class,
e.getCapsule(this).readSavableArrayList("children", null) ); e.getCapsule(this).readSavableArrayList("children", null) );
@ -754,7 +750,6 @@ public class Node extends Spatial {
child.parent = this; child.parent = this;
} }
} }
super.read(e); super.read(e);
} }
@ -775,7 +770,6 @@ public class Node extends Spatial {
} }
} }
} }
@Override @Override
public void depthFirstTraversal(SceneGraphVisitor visitor) { public void depthFirstTraversal(SceneGraphVisitor visitor) {
for (Spatial child : children.getArray()) { for (Spatial child : children.getArray()) {
@ -783,7 +777,6 @@ public class Node extends Spatial {
} }
visitor.visit(this); visitor.visit(this);
} }
@Override @Override
protected void breadthFirstTraversal(SceneGraphVisitor visitor, Queue<Spatial> queue) { protected void breadthFirstTraversal(SceneGraphVisitor visitor, Queue<Spatial> queue) {
queue.addAll(children); queue.addAll(children);

@ -123,7 +123,8 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
protected static final int RF_TRANSFORM = 0x01, // need light resort + combine transforms protected static final int RF_TRANSFORM = 0x01, // need light resort + combine transforms
RF_BOUND = 0x02, RF_BOUND = 0x02,
RF_LIGHTLIST = 0x04, // changes in light lists 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 CullHint cullHint = CullHint.Inherit;
protected BatchHint batchHint = BatchHint.Inherit; protected BatchHint batchHint = BatchHint.Inherit;
@ -136,6 +137,9 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
*/ */
protected LightList localLights; protected LightList localLights;
protected transient LightList worldLights; protected transient LightList worldLights;
protected ArrayList<MatParamOverride> localOverrides;
protected ArrayList<MatParamOverride> worldOverrides;
/** /**
* This spatial's name. * This spatial's name.
*/ */
@ -196,13 +200,14 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
*/ */
protected Spatial(String name) { protected Spatial(String name) {
this.name = name; this.name = name;
localTransform = new Transform(); localTransform = new Transform();
worldTransform = new Transform(); worldTransform = new Transform();
localLights = new LightList(this); localLights = new LightList(this);
worldLights = new LightList(this); worldLights = new LightList(this);
localOverrides = new ArrayList<MatParamOverride>();
worldOverrides = new ArrayList<MatParamOverride>();
refreshFlags |= RF_BOUND; refreshFlags |= RF_BOUND;
} }
@ -223,7 +228,6 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
boolean requiresUpdates() { boolean requiresUpdates() {
return requiresUpdates | !controls.isEmpty(); return requiresUpdates | !controls.isEmpty();
} }
/** /**
* Subclasses can call this with true to denote that they require * Subclasses can call this with true to denote that they require
* updateLogicalState() to be called even if they contain no controls. * 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() { protected void setLightListRefresh() {
refreshFlags |= RF_LIGHTLIST; refreshFlags |= RF_LIGHTLIST;
// Make sure next updateGeometricState() visits this branch // Make sure next updateGeometricState() visits this branch
// to update lights. // to update lights.
Spatial p = parent; Spatial p = parent;
while (p != null) { 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) { if ((p.refreshFlags & RF_CHILD_LIGHTLIST) != 0) {
// The parent already has this flag, // The parent already has this flag,
// so must all ancestors. // so must all ancestors.
return; return;
} }
p.refreshFlags |= RF_CHILD_LIGHTLIST; p.refreshFlags |= RF_CHILD_LIGHTLIST;
p = p.parent; 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 * Indicate that the bounding of this spatial has changed and that
* a refresh is required. * a refresh is required.
@ -319,7 +320,6 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
p = p.parent; p = p.parent;
} }
} }
/** /**
* (Internal use only) Forces a refresh of the given types of data. * (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. * @return The list of local material parameter overrides.
*/ */
public ArrayList<MatParamOverride> getLocalOverrides() { 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. * @return The list of world material parameter overrides.
*/ */
public ArrayList<MatParamOverride> getWorldOverrides() { 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(); TempVars vars = TempVars.get();
Vector3f compVecA = vars.vect4; Vector3f compVecA = vars.vect4;
compVecA.set(position).subtractLocal(worldTranslation); compVecA.set(position).subtractLocal(worldTranslation);
getLocalRotation().lookAt(compVecA, upVector); getLocalRotation().lookAt(compVecA, upVector);
if ( getParent() != null ) { if ( getParent() != null ) {
Quaternion rot=vars.quat1; Quaternion rot=vars.quat1;
rot = rot.set(parent.getWorldRotation()).inverseLocal().multLocal(getLocalRotation()); rot = rot.set(parent.getWorldRotation()).inverseLocal().multLocal(getLocalRotation());
@ -579,13 +577,47 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
worldLights.update(localLights, null); worldLights.update(localLights, null);
refreshFlags &= ~RF_LIGHTLIST; refreshFlags &= ~RF_LIGHTLIST;
} else { } else {
if ((parent.refreshFlags & RF_LIGHTLIST) == 0) { assert (parent.refreshFlags & RF_LIGHTLIST) == 0;
worldLights.update(localLights, parent.worldLights); worldLights.update(localLights, parent.worldLights);
refreshFlags &= ~RF_LIGHTLIST; refreshFlags &= ~RF_LIGHTLIST;
}
}
protected void updateMatParamOverrides() {
refreshFlags &= ~RF_MATPARAM_OVERRIDE;
worldOverrides.clear();
if (parent == null) {
worldOverrides.addAll(localOverrides);
} else { } else {
assert false; 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();
} }
/** /**
@ -720,7 +752,6 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
controls.add(control); controls.add(control);
control.setSpatial(this); control.setSpatial(this);
boolean after = requiresUpdates(); boolean after = requiresUpdates();
// If the requirement to be updated has changed // If the requirement to be updated has changed
// then we need to let the parent node know so it // then we need to let the parent node know so it
// can rebuild its update list. // can rebuild its update list.
@ -744,7 +775,6 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
} }
} }
boolean after = requiresUpdates(); boolean after = requiresUpdates();
// If the requirement to be updated has changed // If the requirement to be updated has changed
// then we need to let the parent node know so it // then we need to let the parent node know so it
// can rebuild its update list. // can rebuild its update list.
@ -770,14 +800,12 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
} }
boolean after = requiresUpdates(); boolean after = requiresUpdates();
// If the requirement to be updated has changed // If the requirement to be updated has changed
// then we need to let the parent node know so it // then we need to let the parent node know so it
// can rebuild its update list. // can rebuild its update list.
if( parent != null && before != after ) { if( parent != null && before != after ) {
parent.invalidateUpdateList(); parent.invalidateUpdateList();
} }
return result; return result;
} }
@ -862,6 +890,9 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
if ((refreshFlags & RF_BOUND) != 0) { if ((refreshFlags & RF_BOUND) != 0) {
updateWorldBound(); updateWorldBound();
} }
if ((refreshFlags & RF_MATPARAM_OVERRIDE) != 0) {
updateMatParamOverrides();
}
assert refreshFlags == 0; assert refreshFlags == 0;
} }
@ -1336,6 +1367,8 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
clone.localLights.setOwner(clone); clone.localLights.setOwner(clone);
clone.worldLights.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. // No need to force cloned to update.
// This node already has the refresh flags // This node already has the refresh flags
// set below so it will have to update anyway. // 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(shadowMode, "shadow_mode", ShadowMode.Inherit);
capsule.write(localTransform, "transform", Transform.IDENTITY); capsule.write(localTransform, "transform", Transform.IDENTITY);
capsule.write(localLights, "lights", null); capsule.write(localLights, "lights", null);
capsule.writeSavableArrayList(localOverrides, "overrides", null);
// Shallow clone the controls array to convert its type. // Shallow clone the controls array to convert its type.
capsule.writeSavableArrayList(new ArrayList(controls), "controlsList", null); 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 = (LightList) ic.readSavable("lights", null);
localLights.setOwner(this); 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 //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 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. //The SkeletonControl must be the last in the stack so we add the list of all other control before it.

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

@ -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));
}
}
Loading…
Cancel
Save