From 2df0c56f2a0f5aab4946c790f45482f2153a1b26 Mon Sep 17 00:00:00 2001 From: Andrzej Kapolka Date: Sat, 29 Apr 2006 03:24:15 +0000 Subject: [PATCH] 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 --- src/java/com/threerings/jme/model/Model.java | 153 ++++++++++++++++-- .../threerings/jme/model/ModelController.java | 8 + .../com/threerings/jme/model/ModelMesh.java | 29 +++- .../com/threerings/jme/model/ModelNode.java | 74 ++++++++- .../threerings/jme/model/ModelSpatial.java | 2 - .../com/threerings/jme/model/SkinMesh.java | 6 +- .../threerings/jme/tools/AnimationDef.java | 2 +- .../com/threerings/jme/tools/ModelDef.java | 8 +- .../com/threerings/jme/tools/ModelViewer.java | 4 + 9 files changed, 258 insertions(+), 28 deletions(-) diff --git a/src/java/com/threerings/jme/model/Model.java b/src/java/com/threerings/jme/model/Model.java index c9fb6e2a1..37b0e013f 100644 --- a/src/java/com/threerings/jme/model/Model.java +++ b/src/java/com/threerings/jme/model/Model.java @@ -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 targets = new HashSet(); + 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(); } - 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 null 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 _animObservers = new ObserverList(ObserverList.FAST_UNSAFE_NOTIFY); diff --git a/src/java/com/threerings/jme/model/ModelController.java b/src/java/com/threerings/jme/model/ModelController.java index 45556f019..d4e86df7e 100644 --- a/src/java/com/threerings/jme/model/ModelController.java +++ b/src/java/com/threerings/jme/model/ModelController.java @@ -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. */ diff --git a/src/java/com/threerings/jme/model/ModelMesh.java b/src/java/com/threerings/jme/model/ModelMesh.java index 99ab60ee0..f0d44ccfa 100644 --- a/src/java/com/threerings/jme/model/ModelMesh.java +++ b/src/java/com/threerings/jme/model/ModelMesh.java @@ -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; diff --git a/src/java/com/threerings/jme/model/ModelNode.java b/src/java/com/threerings/jme/model/ModelNode.java index 854cc7d95..cd847b1b3 100644 --- a/src/java/com/threerings/jme/model/ModelNode.java +++ b/src/java/com/threerings/jme/model/ModelNode.java @@ -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 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. diff --git a/src/java/com/threerings/jme/model/ModelSpatial.java b/src/java/com/threerings/jme/model/ModelSpatial.java index d93c18357..b73ee1d77 100644 --- a/src/java/com/threerings/jme/model/ModelSpatial.java +++ b/src/java/com/threerings/jme/model/ModelSpatial.java @@ -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; diff --git a/src/java/com/threerings/jme/model/SkinMesh.java b/src/java/com/threerings/jme/model/SkinMesh.java index 70fb521a5..b2c12f46d 100644 --- a/src/java/com/threerings/jme/model/SkinMesh.java +++ b/src/java/com/threerings/jme/model/SkinMesh.java @@ -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; diff --git a/src/java/com/threerings/jme/tools/AnimationDef.java b/src/java/com/threerings/jme/tools/AnimationDef.java index 6e1fdba90..709e91174 100644 --- a/src/java/com/threerings/jme/tools/AnimationDef.java +++ b/src/java/com/threerings/jme/tools/AnimationDef.java @@ -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 + "]."); } } diff --git a/src/java/com/threerings/jme/tools/ModelDef.java b/src/java/com/threerings/jme/tools/ModelDef.java index a4fc29bdb..7892f8225 100644 --- a/src/java/com/threerings/jme/tools/ModelDef.java +++ b/src/java/com/threerings/jme/tools/ModelDef.java @@ -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 referenced) + ModelNode node, HashMap nodes, + HashSet 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; diff --git a/src/java/com/threerings/jme/tools/ModelViewer.java b/src/java/com/threerings/jme/tools/ModelViewer.java index 90ae783cc..d3201c406 100644 --- a/src/java/com/threerings/jme/tools/ModelViewer.java +++ b/src/java/com/threerings/jme/tools/ModelViewer.java @@ -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();