Various performance tweaks: cull nodes with no mesh children, allow

locking the transforms and bounds of models whose positions will not 
change, skip/postpone updates for models that aren't in view.


git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@4078 542714f4-19e9-0310-aa3c-eee0fc999fb1
This commit is contained in:
Andrzej Kapolka
2006-04-29 03:24:15 +00:00
parent a7bb7bb4dd
commit 2df0c56f2a
9 changed files with 258 additions and 28 deletions
+137 -16
View File
@@ -38,13 +38,19 @@ import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Properties;
import com.samskivert.util.ObserverList;
import com.jme.bounding.BoundingVolume;
import com.jme.math.FastMath;
import com.jme.math.Matrix4f;
import com.jme.math.Quaternion;
import com.jme.math.Vector3f;
import com.jme.renderer.Camera;
import com.jme.renderer.CloneCreator;
import com.jme.renderer.Renderer;
import com.jme.scene.Controller;
import com.jme.scene.Node;
import com.jme.scene.Spatial;
@@ -277,6 +283,7 @@ public class Model extends ModelNode
public void initPrototype ()
{
setReferenceTransforms();
cullInvisibleNodes();
initInstance();
}
@@ -318,13 +325,14 @@ public class Model extends ModelNode
/**
* Starts the named animation.
*
* @return a reference to the started animation
* @return the duration of the started animation (for looping animations,
* the duration of one cycle), or -1 if the animation was not found
*/
public Animation startAnimation (String name)
public float startAnimation (String name)
{
Animation anim = getAnimation(name);
if (anim == null) {
return null;
return -1f;
}
_anim = anim;
_animName = name;
@@ -333,7 +341,7 @@ public class Model extends ModelNode
_fdir = +1;
_elapsed = 0f;
_animObservers.apply(new AnimStartedOp(_animName));
return anim;
return anim.getDuration() / _animSpeed;
}
/**
@@ -485,11 +493,29 @@ public class Model extends ModelNode
return instance;
}
/**
* Locks the transforms and bounds of this model in the expectation that it
* will never be moved from its current position.
*/
public void lockInstance ()
{
// collect the controller targets and lock recursively
HashSet<Spatial> targets = new HashSet<Spatial>();
for (Object ctrl : getControllers()) {
if (ctrl instanceof ModelController) {
targets.add(((ModelController)ctrl).getTarget());
}
}
lockInstance(targets);
}
@Override // documentation inherited
public Spatial putClone (Spatial store, CloneCreator properties)
{
Model mstore;
if (store == null) {
Model mstore = (Model)properties.originalToCopy.get(this);
if (mstore != null) {
return mstore;
} else if (store == null) {
mstore = new Model(getName(), _props);
} else {
mstore = (Model)store;
@@ -499,21 +525,64 @@ public class Model extends ModelNode
if (_anims != null) {
mstore._anims = new HashMap<String, Animation>();
}
mstore._pnodes = properties.originalToCopy;
mstore._pnodes = (HashMap)properties.originalToCopy.clone();
return mstore;
}
@Override // documentation inherited
public void updateWorldData (float time)
public void updateGeometricState (float time, boolean initiator)
{
// if we were not visible the last time we were rendered, don't do a
// full update; just update the world bound and wait until we come
// into view
boolean wasOutside = _outside;
_outside = isOutsideFrustum() && worldBound != null;
// slow evvvverything down by the animation speed
time *= _animSpeed;
if (_anim != null) {
updateAnimation(time);
}
// update children
super.updateWorldData(time);
// update controllers and children with accumulated time
_accum += time;
if (_outside) {
if (!wasOutside) {
updateModelBound();
}
updateWorldVectors();
worldBound = _modelBound.transform(getWorldRotation(),
getWorldTranslation(), getWorldScale(), worldBound);
} else {
super.updateGeometricState(_accum, initiator);
_accum = 0f;
}
}
@Override // documentation inherited
public void onDraw (Renderer r)
{
// if we switch from invisible to visible, we have to do a last-minute
// full update (which only works if our meshes are enqueued)
super.onDraw(r);
if (_outside && !isOutsideFrustum()) {
updateWorldData(0f);
}
}
/**
* Determines whether this node was determined to be entirely outside the
* view frustum.
*/
protected boolean isOutsideFrustum ()
{
for (Node node = this; node != null; node = node.getParent()) {
if (node.getLastFrustumIntersection() == Camera.OUTSIDE_FRUSTUM) {
return true;
}
}
return false;
}
/**
@@ -540,12 +609,14 @@ public class Model extends ModelNode
_elapsed -= 1f;
}
// update the target transforms
Spatial[] targets = _anim.transformTargets;
Transform[] xforms = _anim.transforms[_fidx],
nxforms = _anim.transforms[_nidx];
for (int ii = 0; ii < targets.length; ii++) {
xforms[ii].blend(nxforms[ii], _elapsed, targets[ii]);
// update the target transforms if not outside the view frustum
if (!_outside) {
Spatial[] targets = _anim.transformTargets;
Transform[] xforms = _anim.transforms[_fidx],
nxforms = _anim.transforms[_nidx];
for (int ii = 0; ii < targets.length; ii++) {
xforms[ii].blend(nxforms[ii], _elapsed, targets[ii]);
}
}
// if the next index is the same as this one, we are finished
@@ -579,6 +650,42 @@ public class Model extends ModelNode
}
}
/**
* Sets the model bound based on the current world bound.
*/
protected void updateModelBound ()
{
if (worldBound == null) {
return;
}
setTransform(getWorldTranslation(), getWorldRotation(),
getWorldScale(), _xform);
_xform.invertLocal();
_xform.toTranslationVector(_trans);
extractScale(_xform, _scale);
_xform.toRotationQuat(_rot);
_modelBound = worldBound.transform(_rot, _trans, _scale, _modelBound);
}
/**
* Extracts the scale factor from the given transform and normalizes it.
*/
protected static void extractScale (Matrix4f m, Vector3f scale)
{
scale.x = FastMath.sqrt(m.m00*m.m00 + m.m01*m.m01 + m.m02*m.m02);
m.m00 /= scale.x;
m.m01 /= scale.x;
m.m02 /= scale.x;
scale.y = FastMath.sqrt(m.m10*m.m10 + m.m11*m.m11 + m.m12*m.m12);
m.m10 /= scale.y;
m.m11 /= scale.y;
m.m12 /= scale.y;
scale.z = FastMath.sqrt(m.m20*m.m20 + m.m21*m.m21 + m.m22*m.m22);
m.m20 /= scale.z;
m.m21 /= scale.z;
m.m22 /= scale.z;
}
/** A reference to the prototype, or <code>null</code> if this is a
* prototype. */
protected Model _prototype;
@@ -615,9 +722,23 @@ public class Model extends ModelNode
/** The frame portion elapsed since the start of the current frame. */
protected float _elapsed;
/** The amount of update time accumulated while outside of view frustum. */
protected float _accum;
/** The child node that contains the model's emissions in world space. */
protected Node _emissionNode;
/** The model space bounding volume. */
protected BoundingVolume _modelBound;
/** Whether or not we were outside the frustum at the last update. */
protected boolean _outside;
/** Temporary transform variables. */
protected Matrix4f _xform = new Matrix4f();
protected Vector3f _trans = new Vector3f(), _scale = new Vector3f();
protected Quaternion _rot = new Quaternion();
/** Animation completion listeners. */
protected ObserverList<AnimationObserver> _animObservers =
new ObserverList<AnimationObserver>(ObserverList.FAST_UNSAFE_NOTIFY);
@@ -59,6 +59,14 @@ public abstract class ModelController extends Controller
Collections.addAll(_animations, anims);
}
/**
* Returns a reference to the controller's target.
*/
public Spatial getTarget ()
{
return _target;
}
/**
* Resolves any textures required by the controller.
*/
@@ -186,8 +186,10 @@ public class ModelMesh extends TriMesh
@Override // documentation inherited
public Spatial putClone (Spatial store, CloneCreator properties)
{
ModelMesh mstore;
if (store == null) {
ModelMesh mstore = (ModelMesh)properties.originalToCopy.get(this);
if (mstore != null) {
return mstore;
} else if (store == null) {
mstore = new ModelMesh(getName());
} else {
mstore = (ModelMesh)store;
@@ -213,6 +215,14 @@ public class ModelMesh extends TriMesh
return mstore;
}
@Override // documentation inherited
public void updateWorldVectors ()
{
if (!_transformLocked) {
super.updateWorldVectors();
}
}
// documentation inherited from interface Externalizable
public void writeExternal (ObjectOutput out)
throws IOException
@@ -401,6 +411,16 @@ public class ModelMesh extends TriMesh
reconstruct(vbbuf, nbbuf, cbbuf, tbbuf, ibbuf);
}
/**
* Locks the transform and bounds of this mesh on the assumption that its
* position will not change.
*/
protected void lockInstance ()
{
lockBounds();
_transformLocked = true;
}
/**
* Imposes the specified order on the given buffer of 32 bit values.
*/
@@ -455,6 +475,11 @@ public class ModelMesh extends TriMesh
/** For prototype meshes, the resolved texture states. */
protected TextureState[] _tstates;
/** Whether or not the transform has been locked. This operates in a
* slightly different way than JME's locking, in that it allows applying
* transformations to display lists. */
protected boolean _transformLocked;
/** The shared state for back face culling. */
protected static CullState _backCull;
@@ -31,6 +31,7 @@ import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.HashSet;
import com.jme.math.Matrix4f;
import com.jme.math.Quaternion;
@@ -92,6 +93,16 @@ public class ModelNode extends Node
return _modelTransform;
}
@Override // documentation inherited
public void updateWorldData (float time)
{
// we use locked bounds as an indication that we can skip the update
// altogether
if ((lockedMode & LOCKED_BOUNDS) == 0) {
super.updateWorldData(time);
}
}
@Override // documentation inherited
public void updateWorldVectors ()
{
@@ -110,13 +121,16 @@ public class ModelNode extends Node
@Override // documentation inherited
public Spatial putClone (Spatial store, CloneCreator properties)
{
ModelNode mstore;
if (store == null) {
ModelNode mstore = (ModelNode)properties.originalToCopy.get(this);
if (mstore != null) {
return mstore;
} else if (store == null) {
mstore = new ModelNode(getName());
} else {
mstore = (ModelNode)store;
}
super.putClone(mstore, properties);
mstore.cullMode = cullMode;
return mstore;
}
@@ -210,6 +224,62 @@ public class ModelNode extends Node
}
}
/**
* Sets the cull state of any nodes that do not contain geometric
* descendants to {@link CULL_ALWAYS} so that they don't waste
* rendering time.
*
* @return true if this node should be drawn, false if it contains
* no mesh descendants
*/
protected boolean cullInvisibleNodes ()
{
boolean hasVisibleDescendants = false;
for (int ii = 0, nn = getQuantity(); ii < nn; ii++) {
Spatial child = getChild(ii);
if (!(child instanceof ModelNode) ||
((ModelNode)child).cullInvisibleNodes()) {
hasVisibleDescendants = true;
}
}
setCullMode(hasVisibleDescendants ? CULL_INHERIT : CULL_ALWAYS);
return hasVisibleDescendants;
}
/**
* Locks the transforms and bounds of this instance with the assumption
* that the position will never change.
*
* @param targets the targets of the model's controllers, which determine
* the subset of nodes that can be locked
* @return true if this node is a target or contains any targets, otherwise
* false
*/
protected boolean lockInstance (HashSet<Spatial> targets)
{
updateWorldVectors();
lockedMode |= LOCKED_TRANSFORMS;
boolean containsTargets = false;
for (int ii = 0, nn = getQuantity(); ii < nn; ii++) {
Spatial child = getChild(ii);
if (targets.contains(child) || (child instanceof ModelNode &&
((ModelNode)child).lockInstance(targets))) {
containsTargets = true;
} else if (child instanceof ModelMesh) {
((ModelMesh)child).lockInstance();
}
}
if (containsTargets) {
return true;
} else {
updateWorldBound();
lockedMode |= LOCKED_BOUNDS;
return false;
}
}
/**
* Sets a matrix to the transform defined by the given translation,
* rotation, and scale values.
@@ -27,8 +27,6 @@ import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.HashMap;
import com.jme.math.Matrix4f;
import com.jme.renderer.Renderer;
import com.jme.scene.Spatial;
@@ -143,8 +143,10 @@ public class SkinMesh extends ModelMesh
@Override // documentation inherited
public Spatial putClone (Spatial store, CloneCreator properties)
{
SkinMesh mstore;
if (store == null) {
SkinMesh mstore = (SkinMesh)properties.originalToCopy.get(this);
if (mstore != null) {
return mstore;
} else if (store == null) {
mstore = new SkinMesh(getName());
} else {
mstore = (SkinMesh)store;
@@ -64,7 +64,7 @@ public class AnimationDef
if (target != null) {
targets.add(target);
} else {
Log.warning("Missing animation target [name=" + name +
Log.debug("Missing animation target [name=" + name +
"].");
}
}
@@ -436,7 +436,7 @@ public class ModelDef
}
// get rid of any nodes that serve no purpose
pruneUnusedNodes(model, referenced);
pruneUnusedNodes(model, nodes, referenced);
return model;
}
@@ -461,16 +461,18 @@ public class ModelDef
/** Recursively removes any unused nodes. */
protected boolean pruneUnusedNodes (
ModelNode node, HashSet<Spatial> referenced)
ModelNode node, HashMap<String, Spatial> nodes,
HashSet<Spatial> referenced)
{
boolean hasValidChildren = false;
for (Iterator it = node.getChildren().iterator(); it.hasNext(); ) {
Spatial child = (Spatial)it.next();
if (!(child instanceof ModelNode) ||
pruneUnusedNodes((ModelNode)child, referenced)) {
pruneUnusedNodes((ModelNode)child, nodes, referenced)) {
hasValidChildren = true;
} else {
it.remove();
nodes.remove(child.getName());
}
}
return referenced.contains(node) || hasValidChildren;
@@ -62,6 +62,7 @@ import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.filechooser.FileFilter;
import com.jme.bounding.BoundingBox;
import com.jme.image.Texture;
import com.jme.light.DirectionalLight;
import com.jme.math.FastMath;
@@ -294,6 +295,8 @@ public class ModelViewer extends JmeCanvasApp
Line grid = new Line("grid", points, null, null, null);
grid.getDefaultColor().set(0.25f, 0.25f, 0.25f, 1f);
grid.setLightCombineMode(LightState.OFF);
grid.setModelBound(new BoundingBox());
grid.updateModelBound();
_ctx.getGeometry().attachChild(grid);
grid.updateRenderState();
@@ -453,6 +456,7 @@ public class ModelViewer extends JmeCanvasApp
}
_ctx.getGeometry().attachChild(_model = model);
_model.lockStaticMeshes(_ctx.getRenderer(), true, true);
_model.getLocalTranslation().set(-20f, 0f, 0f);
// resolve the textures from the file's directory
final File dir = file.getParentFile();