From 072926f8144a19cba3ac5ca685e674c650d6041c Mon Sep 17 00:00:00 2001 From: Andrzej Kapolka Date: Tue, 1 May 2007 23:36:23 +0000 Subject: [PATCH] Merge meshes in animated models by finding ones that maintain the same relative position throughout all animations. Also, don't include transforms in the animations for nodes that never move. git-svn-id: svn+ssh://src.earth.threerings.net/nenya/trunk@218 ed5b42cb-e716-0410-a449-f6a68f950b19 --- src/java/com/threerings/jme/model/Model.java | 270 +++++++------- .../com/threerings/jme/model/ModelMesh.java | 69 ---- .../com/threerings/jme/model/ModelNode.java | 87 ++--- .../com/threerings/jme/model/SkinMesh.java | 101 ++--- .../threerings/jme/tools/AnimationDef.java | 80 ++-- .../threerings/jme/tools/CompileModel.java | 26 +- .../com/threerings/jme/tools/ModelDef.java | 350 +++++++++++++++--- src/java/com/threerings/jme/util/JmeUtil.java | 27 ++ 8 files changed, 620 insertions(+), 390 deletions(-) diff --git a/src/java/com/threerings/jme/model/Model.java b/src/java/com/threerings/jme/model/Model.java index a7bf4b62..312d6e82 100644 --- a/src/java/com/threerings/jme/model/Model.java +++ b/src/java/com/threerings/jme/model/Model.java @@ -69,11 +69,11 @@ import com.threerings.jme.util.SpatialVisitor; */ public class Model extends ModelNode { - /** The supported types of animation in decreasing order of complexity. */ + /** The supported types of animation in decreasing order of complexity. */ public enum AnimationMode { SKIN, MORPH, FLIPBOOK }; - + /** Lets listeners know when animations are completed (which only happens * for non-repeating animations) or cancelled. */ public interface AnimationObserver @@ -84,14 +84,14 @@ public class Model extends ModelNode * @return true to remain on the observer list, false to remove self */ public boolean animationStarted (Model model, String anim); - + /** * Called when a non-repeating animation has finished. * * @return true to remain on the observer list, false to remove self */ public boolean animationCompleted (Model model, String anim); - + /** * Called when an animation has been cancelled. * @@ -99,30 +99,33 @@ public class Model extends ModelNode */ public boolean animationCancelled (Model model, String anim); } - + /** An animation for the model. */ public static class Animation implements Savable { /** The rate of the animation in frames per second. */ public int frameRate; - + /** The animation repeat type ({@link Controller#RT_CLAMP}, * {@link Controller#RT_CYCLE}, or {@link Controller#RT_WRAP}). */ public int repeatType; - + + /** Any nodes visible that never move within the model. */ + public Spatial[] staticTargets; + /** The transformation targets of the animation. */ public Spatial[] transformTargets; - + /** The animation transforms (one transform per target per frame). */ public transient Transform[][] transforms; - + /** Uniquely identifies this animation within the model. */ public transient int animId; - + /** For each frame, whether the frame has been stored in meshes. */ public transient boolean[] stored; - + /** * Returns this animation's duration in seconds. */ @@ -130,7 +133,7 @@ public class Model extends ModelNode { return (float)transforms.length / frameRate; } - + /** * Rebinds this animation for a prototype instance. * @@ -141,17 +144,14 @@ public class Model extends ModelNode Animation anim = new Animation(); anim.frameRate = frameRate; anim.repeatType = repeatType; + anim.staticTargets = rebind(staticTargets, pnodes); + anim.transformTargets = rebind(transformTargets, pnodes); anim.transforms = transforms; - anim.transformTargets = new Spatial[transformTargets.length]; - for (int ii = 0; ii < transformTargets.length; ii++) { - anim.transformTargets[ii] = - (Spatial)pnodes.get(transformTargets[ii]); - } anim.animId = animId; anim.stored = stored; return anim; } - + /** * Applies the transforms for a frame of this animation. */ @@ -162,7 +162,7 @@ public class Model extends ModelNode xforms[ii].apply(transformTargets[ii]); } } - + /** * Blends the transforms between two frames of this animation. */ @@ -173,13 +173,13 @@ public class Model extends ModelNode xforms[ii].blend(nxforms[ii], alpha, transformTargets[ii]); } } - + // documentation inherited public Class getClassTag () { return getClass(); } - + // documentation inherited public void read (JMEImporter im) throws IOException @@ -187,6 +187,8 @@ public class Model extends ModelNode InputCapsule capsule = im.getCapsule(this); frameRate = capsule.readInt("frameRate", 0); repeatType = capsule.readInt("repeatType", Controller.RT_CLAMP); + staticTargets = ArrayUtil.copy(capsule.readSavableArray( + "staticTargets", null), new Spatial[0]); transformTargets = ArrayUtil.copy(capsule.readSavableArray( "transformTargets", null), new Spatial[0]); FloatBuffer pxforms = capsule.readFloatBuffer("transforms", null); @@ -208,9 +210,10 @@ public class Model extends ModelNode OutputCapsule capsule = ex.getCapsule(this); capsule.write(frameRate, "frameRate", 0); capsule.write(repeatType, "repeatType", Controller.RT_CLAMP); + capsule.write(staticTargets, "staticTargets", null); capsule.write(transformTargets, "transformTargets", null); - FloatBuffer pxforms = FloatBuffer.allocate(transforms.length * - transformTargets.length * Transform.PACKED_SIZE); + FloatBuffer pxforms = FloatBuffer.allocate( + transforms.length * transformTargets.length * Transform.PACKED_SIZE); for (Transform[] frame : transforms) { for (Transform xform : frame) { xform.writeToBuffer(pxforms); @@ -219,16 +222,25 @@ public class Model extends ModelNode pxforms.rewind(); capsule.write(pxforms, "transforms", null); } - + + protected Spatial[] rebind (Spatial[] targets, HashMap pnodes) + { + Spatial[] ntargets = new Spatial[targets.length]; + for (int ii = 0; ii < targets.length; ii++) { + ntargets[ii] = (Spatial)pnodes.get(targets[ii]); + } + return ntargets; + } + private static final long serialVersionUID = 1; } - + /** A frame element that manipulates the target's transform. */ public static final class Transform { /** The number of floats required to store a packed transform. */ public static final int PACKED_SIZE = 3 + 4 + 3; - + public Transform ( Vector3f translation, Quaternion rotation, Vector3f scale) { @@ -236,7 +248,7 @@ public class Model extends ModelNode _rotation = rotation; _scale = scale; } - + public Transform (FloatBuffer buf) { _translation = new Vector3f(buf.get(), buf.get(), buf.get()); @@ -244,14 +256,14 @@ public class Model extends ModelNode buf.get()); _scale = new Vector3f(buf.get(), buf.get(), buf.get()); } - + public void apply (Spatial target) { target.getLocalTranslation().set(_translation); target.getLocalRotation().set(_rotation); target.getLocalScale().set(_scale); } - + /** * Blends between this transform and the next, applying the result to * the given target. @@ -266,7 +278,7 @@ public class Model extends ModelNode target.getLocalRotation().slerp(_rotation, next._rotation, alpha); target.getLocalScale().interpolate(_scale, next._scale, alpha); } - + /** * Writes this transform to the current position in the supplied * buffer. @@ -276,31 +288,31 @@ public class Model extends ModelNode buf.put(_translation.x); buf.put(_translation.y); buf.put(_translation.z); - + buf.put(_rotation.x); buf.put(_rotation.y); buf.put(_rotation.z); buf.put(_rotation.w); - + buf.put(_scale.x); buf.put(_scale.y); buf.put(_scale.z); } - + /** The transform at this frame. */ protected Vector3f _translation, _scale; protected Quaternion _rotation; } - + /** Customized clone creator for models. */ public static class CloneCreator { /** A shared seed used to select textures consistently. */ public int random; - + /** Maps original objects to their copies. */ public HashMap originalToCopy = new HashMap(); - + public CloneCreator (Model toCopy) { _toCopy = toCopy; @@ -314,7 +326,7 @@ public class Model extends ModelNode addProperty("displaylistid"); addProperty("bound"); } - + /** * Sets the named property. */ @@ -322,7 +334,7 @@ public class Model extends ModelNode { _properties.add(name); } - + /** * Clears the named property. */ @@ -330,7 +342,7 @@ public class Model extends ModelNode { _properties.remove(name); } - + /** * Checks whether the named property has been set. */ @@ -338,7 +350,7 @@ public class Model extends ModelNode { return _properties.contains(name); } - + /** * Creates a new copy of the target model. */ @@ -349,14 +361,14 @@ public class Model extends ModelNode originalToCopy.clear(); // make sure no references remain return copy; } - + /** The model to copy. */ protected Model _toCopy; - + /** The set of added properties. */ protected HashSet _properties = new HashSet(); } - + /** * Attempts to read a model from the specified file. */ @@ -367,20 +379,20 @@ public class Model extends ModelNode FileInputStream fis = new FileInputStream(file); Model model = (Model)BinaryImporter.getInstance().load(fis); fis.close(); - + // initialize the model as a prototype model.initPrototype(); - + return model; } - + /** * No-arg constructor for deserialization. */ public Model () { } - + /** * Standard constructor. */ @@ -389,7 +401,7 @@ public class Model extends ModelNode super(name); _props = props; } - + /** * Returns a reference to the properties of the model. */ @@ -397,7 +409,7 @@ public class Model extends ModelNode { return _props; } - + /** * Initializes this model as prototype. Only necessary when the prototype * was not loaded through {@link #readFromFile}. @@ -416,7 +428,7 @@ public class Model extends ModelNode cullInvisibleNodes(); initInstance(); } - + /** * Returns the names of the model's variant configurations. */ @@ -424,7 +436,7 @@ public class Model extends ModelNode { return StringUtil.parseStringArray(_props.getProperty("variants", "")); } - + /** * Adds an animation to the model's library. This should only be called by * the model compiler. @@ -435,7 +447,7 @@ public class Model extends ModelNode _anims = new HashMap(); } _anims.put(name, anim); - + // store the original transforms Transform[] oxforms = new Transform[anim.transformTargets.length]; for (int ii = 0; ii < anim.transformTargets.length; ii++) { @@ -445,7 +457,7 @@ public class Model extends ModelNode new Quaternion(target.getLocalRotation()), new Vector3f(target.getLocalScale())); } - + // run through every frame of the animation, expanding the bounding // volumes of any deformable meshes for (int ii = 0; ii < anim.transforms.length; ii++) { @@ -455,14 +467,14 @@ public class Model extends ModelNode updateWorldData(0f); expandModelBounds(); } - + // restore the original transforms for (int ii = 0; ii < anim.transformTargets.length; ii++) { oxforms[ii].apply(anim.transformTargets[ii]); } updateWorldData(0f); } - + /** * Sets the animation mode to use for this model. This should be set * on the prototype before any animations are started or any instances @@ -472,7 +484,7 @@ public class Model extends ModelNode { _animMode = mode; } - + /** * Returns the animation mode configured for this model. */ @@ -480,7 +492,7 @@ public class Model extends ModelNode { return _animMode; } - + /** * Returns the names of the model's animations. */ @@ -492,7 +504,7 @@ public class Model extends ModelNode return (_anims == null) ? new String[0] : _anims.keySet().toArray(new String[_anims.size()]); } - + /** * Checks whether the unit has an animation with the given name. */ @@ -503,7 +515,7 @@ public class Model extends ModelNode } return (_anims == null) ? false : _anims.containsKey(name); } - + /** * Starts the named animation. * @@ -533,14 +545,16 @@ public class Model extends ModelNode if (_anim != null) { _animObservers.apply(new AnimCancelledOp(_animName)); } - - // first cull all model nodes, then re-activate the ones in the - // target list + + // first cull all model nodes, then re-activate the ones in the target lists cullModelNodes(); + for (Spatial target : anim.staticTargets) { + ((ModelNode)target).updateCullMode(); + } for (Spatial target : anim.transformTargets) { ((ModelNode)target).updateCullMode(); } - + _paused = false; _anim = anim; _animName = name; @@ -552,7 +566,7 @@ public class Model extends ModelNode _animObservers.apply(new AnimStartedOp(_animName)); return anim.getDuration() / _animSpeed; } - + /** * Fast-forwards the current animation by the given number of seconds. */ @@ -560,7 +574,7 @@ public class Model extends ModelNode { updateAnimation(time); } - + /** * Gets a reference to the animation with the given name. */ @@ -583,7 +597,7 @@ public class Model extends ModelNode Log.warning("Requested unknown animation [name=" + name + "]."); return null; } - + /** * Returns a reference to the currently running animation, or * null if no animation is running. @@ -592,7 +606,7 @@ public class Model extends ModelNode { return _anim; } - + /** * Stops the currently running animation. */ @@ -633,7 +647,7 @@ public class Model extends ModelNode { _fdir *= -1; } - + /** * Sets the animation speed, which acts as a multiplier for the frame rate * of each animation. @@ -642,7 +656,7 @@ public class Model extends ModelNode { _animSpeed = speed; } - + /** * Returns the currently configured animation speed. */ @@ -650,7 +664,7 @@ public class Model extends ModelNode { return _animSpeed; } - + /** * Adds an animation observer. */ @@ -658,7 +672,7 @@ public class Model extends ModelNode { _animObservers.add(obs); } - + /** * Removes an animation observer. */ @@ -666,7 +680,7 @@ public class Model extends ModelNode { _animObservers.remove(obs); } - + /** * Returns a reference to the node that contains this model's emissions * (in world space, so the emissions do not move with the model). This @@ -685,7 +699,7 @@ public class Model extends ModelNode } return _emissionNode; } - + /** * Writes this model out to a file. */ @@ -697,7 +711,7 @@ public class Model extends ModelNode BinaryExporter.getInstance().save(this, fos); fos.close(); } - + @Override // documentation inherited public void read (JMEImporter im) throws IOException @@ -729,7 +743,7 @@ public class Model extends ModelNode } } } - + @Override // documentation inherited public void write (JMEExporter ex) throws IOException @@ -756,7 +770,7 @@ public class Model extends ModelNode } capsule.writeSavableArrayList(getControllers(), "controllers", null); } - + @Override // documentation inherited public void resolveTextures (TextureProvider tprov) { @@ -767,7 +781,7 @@ public class Model extends ModelNode } } } - + /** * Creates a new prototype using the given variant configuration. * Use {@link #createInstance} on the returned prototype to create @@ -788,7 +802,7 @@ public class Model extends ModelNode } prototype._prototype = null; prototype._pnodes = null; - + // reconfigure meshes with new variant type if (variant != null) { prototype._props = PropertiesUtil.getFilteredProperties( @@ -802,10 +816,10 @@ public class Model extends ModelNode } return prototype; } - + /** * Creates and returns a new instance of this model. - */ + */ public Model createInstance () { if (_prototype != null) { @@ -838,7 +852,7 @@ public class Model extends ModelNode } lockInstance(targets); } - + @Override // documentation inherited public Spatial putClone (Spatial store, CloneCreator properties) { @@ -866,7 +880,7 @@ public class Model extends ModelNode mstore._animMode = _animMode; return mstore; } - + @Override // documentation inherited public void updateGeometricState (float time, boolean initiator) { @@ -875,13 +889,13 @@ public class Model extends ModelNode // into view boolean wasOutside = _outside; _outside = isOutsideFrustum() && worldBound != null; - + // slow evvvverything down by the animation speed time *= _animSpeed; if (_anim != null) { updateAnimation(time); } - + // update controllers and children with accumulated time _accum += time; if (_outside) { @@ -894,14 +908,14 @@ public class Model extends ModelNode updateWorldVectors(); worldBound = _storedBound.transform(getWorldRotation(), getWorldTranslation(), getWorldScale(), worldBound); - + } else { super.updateGeometricState(_shouldAccumulate ? _accum : time, initiator); _accum = 0f; } } - + @Override // documentation inherited public void onDraw (Renderer r) { @@ -912,7 +926,7 @@ public class Model extends ModelNode updateWorldData(0f); } } - + /** * Transforms the current world bound into model space and stores it for * when the model is offscreen. @@ -928,10 +942,10 @@ public class Model extends ModelNode getChild(ii).updateGeometricState(0f, false); } updateWorldBound(); - + _storedBound = worldBound.clone(_storedBound); } - + /** * Determines whether this node was determined to be entirely outside the * view frustum. @@ -945,7 +959,7 @@ public class Model extends ModelNode } return false; } - + /** * Initializes the per-instance state of this model. */ @@ -960,7 +974,7 @@ public class Model extends ModelNode } } } - + /** * Updates the model's state according to the current animation. */ @@ -972,7 +986,7 @@ public class Model extends ModelNode _elapsed += (time * _anim.frameRate); return; } - + // advance the frame counter if necessary while (_elapsed > 1f) { if (!_paused) { @@ -982,13 +996,13 @@ public class Model extends ModelNode _elapsed = 1f; } } - + // update the target transforms and animation frame if not outside the // view frustum if (!_outside) { - updateMeshes(); + updateMeshes(); } - + // if the next index is the same as this one, we are finished if (_fidx == _nidx && !_paused) { _anim = null; @@ -997,7 +1011,7 @@ public class Model extends ModelNode } _elapsed += (time * _anim.frameRate); } - + /** * Advances the frame counter by one frame. */ @@ -1007,14 +1021,14 @@ public class Model extends ModelNode int nframes = _anim.transforms.length; if (_anim.repeatType == Controller.RT_CLAMP) { _nidx = Math.max(0, Math.min(_nidx + _fdir, nframes - 1)); - + } else if (_anim.repeatType == Controller.RT_WRAP) { // % is not a modulo operator, so is not guaranteed to be positive _nidx = (_nidx + _fdir) % nframes; if (_nidx < 0) { _nidx += nframes; } - + } else { // _anim.repeatType == Controller.RT_CYCLE if ((_nidx + _fdir) < 0 || (_nidx + _fdir) >= nframes) { _fdir *= -1; // reverse direction @@ -1022,7 +1036,7 @@ public class Model extends ModelNode _nidx += _fdir; } } - + /** * Updates the states of the model's meshes according to the animation state. */ @@ -1037,7 +1051,7 @@ public class Model extends ModelNode _anim.stored[_fidx] = true; } setMeshFrame(frameId); - + } else if (_animMode == AnimationMode.MORPH) { int frameId1 = (_anim.animId << 16) | _fidx, frameId2 = (_anim.animId << 16) | _nidx; @@ -1055,74 +1069,74 @@ public class Model extends ModelNode } _anim.blendFrames(_fidx, _nidx, _elapsed); blendMeshFrames(frameId1, frameId2, _elapsed); - + } else { // _animMode == AnimationMode.SKIN _anim.blendFrames(_fidx, _nidx, _elapsed); } } - + /** A reference to the prototype, or null if this is a * prototype. */ protected Model _prototype; - + /** For prototype models, a customized clone creator used to generate * instances. */ protected CloneCreator _ccreator; - + /** The animation mode to use for this model. */ protected AnimationMode _animMode; - + /** For instances, maps prototype nodes to their corresponding instance * nodes. */ protected HashMap _pnodes; - + /** The model properties. */ protected Properties _props; - + /** The model animations. */ protected HashMap _anims; - + /** The currently running animation, or null for none. */ protected Animation _anim; - + /** The name of the currently running animation, if any. */ protected String _animName; - + /** The current animation speed multiplier. */ protected float _animSpeed = 1f; - + /** The index of the current and next frames. */ protected int _fidx, _nidx; - + /** The direction for wrapping animations (+1 forward, -1 backward). */ protected int _fdir; - + /** The frame portion elapsed since the start of the current frame. */ protected float _elapsed; /** The pause status of this model. */ protected boolean _paused; - + /** The amount of update time accumulated while outside of view frustum. */ protected float _accum; - + /** Whether or not we should accumulate update time while out of view. */ protected boolean _shouldAccumulate; - + /** The child node that contains the model's emissions in world space. */ protected Node _emissionNode; - + /** The model space bounding volume stored for use when the model is * offscreen. */ protected BoundingVolume _storedBound; - + /** Whether or not we were outside the frustum at the last update. */ protected boolean _outside; - + /** Animation completion listeners. */ protected ObserverList _animObservers = new ObserverList(ObserverList.FAST_UNSAFE_NOTIFY); - + /** Used to notify observers of animation initiation. */ protected class AnimStartedOp implements ObserverList.ObserverOp @@ -1131,17 +1145,17 @@ public class Model extends ModelNode { _name = name; } - + // documentation inherited from interface ObserverOp public boolean apply (AnimationObserver obs) { return obs.animationStarted(Model.this, _name); } - + /** The name of the animation started. */ protected String _name; } - + /** Used to notify observers of animation completion. */ protected class AnimCompletedOp implements ObserverList.ObserverOp @@ -1150,17 +1164,17 @@ public class Model extends ModelNode { _name = name; } - + // documentation inherited from interface ObserverOp public boolean apply (AnimationObserver obs) { return obs.animationCompleted(Model.this, _name); } - + /** The name of the animation completed. */ protected String _name; } - + /** Used to notify observers of animation cancellation. */ protected class AnimCancelledOp implements ObserverList.ObserverOp @@ -1169,13 +1183,13 @@ public class Model extends ModelNode { _name = name; } - + // documentation inherited from interface ObserverOp public boolean apply (AnimationObserver obs) { return obs.animationCancelled(Model.this, _name); } - + /** The name of the animation cancelled. */ protected String _name; } diff --git a/src/java/com/threerings/jme/model/ModelMesh.java b/src/java/com/threerings/jme/model/ModelMesh.java index 8f12b8b4..3f06ebb2 100644 --- a/src/java/com/threerings/jme/model/ModelMesh.java +++ b/src/java/com/threerings/jme/model/ModelMesh.java @@ -144,51 +144,6 @@ public class ModelMesh extends TriMesh Boolean.parseBoolean(tprops.getProperty("translucent")); } - /** - * Returns an opaque object representing this mesh's attributes. The - * object will implement {@link Object#hashCode} and {@link Object#equals} - * so that meshes with the same attributes will have identical keys. - */ - public Object getAttributeKey () - { - return new AttributeKey(_textureKey, _textures, _sphereMapped, - _filterMode, _mipMapMode, _compress, _emissiveMap, _emissive, _additive, - _solid, _transparent, _alphaThreshold, _translucent); - } - - /** - * Merges another mesh into this one. The mesh is assumed to be in the same coordinate system - * and have the same attributes. - */ - public void merge (ModelMesh mesh) - { - TriangleBatch batch = (TriangleBatch)getBatch(0); - TriangleBatch mbatch = (TriangleBatch)mesh.getBatch(0); - - int vcount = batch.getVertexCount(); - batch.setVertexBuffer(concatenate(batch.getVertexBuffer(), mbatch.getVertexBuffer())); - batch.setNormalBuffer(concatenate(batch.getNormalBuffer(), mbatch.getNormalBuffer())); - if (batch.getColorBuffer() != null && mbatch.getColorBuffer() != null) { - batch.setColorBuffer(concatenate(batch.getColorBuffer(), mbatch.getColorBuffer())); - } - batch.setTextureBuffer(concatenate( - batch.getTextureBuffer(0), mbatch.getTextureBuffer(0)), 0); - for (int ii = 1, nn = getTextureCount(); ii < nn; ii++) { - batch.setTextureBuffer(batch.getTextureBuffer(0), ii); - } - - IntBuffer ibuf = batch.getIndexBuffer(), mibuf = mbatch.getIndexBuffer(); - IntBuffer nibuf = BufferUtils.createIntBuffer(ibuf.capacity() + mibuf.capacity()); - ibuf.clear(); - nibuf.put(ibuf); - mibuf.clear(); - while (mibuf.hasRemaining()) { - nibuf.put(mibuf.get() + vcount); - } - nibuf.clear(); - batch.setIndexBuffer(nibuf); - } - /** * Adjusts the vertices and the transform of the mesh so that the mesh's * position lies at the center of its bounding volume. @@ -819,30 +774,6 @@ public class ModelMesh extends TriMesh new RenderState[RenderState.RS_MAX_STATE]; } - /** A wrapper around an array of attributes that uses {@link Arrays#deepHashCode} and - * {@link Arrays#deepEquals} for hashing/comparison. */ - protected static class AttributeKey - { - public AttributeKey (Object... attrs) - { - _attrs = attrs; - } - - @Override // documentation inherited - public int hashCode () - { - return Arrays.deepHashCode(_attrs); - } - - @Override // documentation inherited - public boolean equals (Object other) - { - return Arrays.deepEquals(_attrs, ((AttributeKey)other)._attrs); - } - - protected Object[] _attrs; - } - /** The name of the texture specified in the model file, which acts as a * property key and a default value. */ protected String _textureKey; diff --git a/src/java/com/threerings/jme/model/ModelNode.java b/src/java/com/threerings/jme/model/ModelNode.java index 03cd1175..a6139eae 100644 --- a/src/java/com/threerings/jme/model/ModelNode.java +++ b/src/java/com/threerings/jme/model/ModelNode.java @@ -41,6 +41,7 @@ import com.jme.util.export.InputCapsule; import com.jme.util.export.OutputCapsule; import com.threerings.jme.Log; +import com.threerings.jme.util.JmeUtil; /** * A {@link Node} with a serialization mechanism tailored to stored models. @@ -55,7 +56,7 @@ public class ModelNode extends Node { super("node"); } - + /** * Standard constructor. */ @@ -63,14 +64,14 @@ public class ModelNode extends Node { super(name); } - + @Override // documentation inherited public int hashCode () { // hash on the name rather than the identity for consistent ordering return getName().hashCode(); } - + /** * Recursively searches the scene graph rooted at this node for a * node with the provided name. @@ -90,7 +91,7 @@ public class ModelNode extends Node } return null; } - + /** * Returns a reference to the model space transform of the node. */ @@ -98,7 +99,7 @@ public class ModelNode extends Node { return _modelTransform; } - + @Override // documentation inherited public void updateWorldData (float time) { @@ -108,7 +109,7 @@ public class ModelNode extends Node super.updateWorldData(time); } } - + @Override // documentation inherited public void updateWorldBound () { @@ -117,22 +118,22 @@ public class ModelNode extends Node super.updateWorldBound(); } } - + @Override // documentation inherited public void updateWorldVectors () { super.updateWorldVectors(); if (parent instanceof ModelNode) { - setTransform(getLocalTranslation(), getLocalRotation(), + JmeUtil.setTransform(getLocalTranslation(), getLocalRotation(), getLocalScale(), _localTransform); ((ModelNode)parent).getModelTransform().mult(_localTransform, _modelTransform); - + } else { _modelTransform.loadIdentity(); } } - + // documentation inherited from interface ModelSpatial public Spatial putClone (Spatial store, Model.CloneCreator properties) { @@ -178,7 +179,7 @@ public class ModelNode extends Node mstore._hasVisibleDescendants = _hasVisibleDescendants; return mstore; } - + @Override // documentation inherited public void read (JMEImporter im) throws IOException @@ -198,7 +199,7 @@ public class ModelNode extends Node } } } - + @Override // documentation inherited public void write (JMEExporter ex) throws IOException @@ -210,7 +211,7 @@ public class ModelNode extends Node capsule.write(getLocalScale(), "localScale", null); capsule.writeSavableArrayList(getChildren(), "children", null); } - + // documentation inherited from interface ModelSpatial public void expandModelBounds () { @@ -221,7 +222,7 @@ public class ModelNode extends Node } } } - + // documentation inherited from interface ModelSpatial public void setReferenceTransforms () { @@ -233,7 +234,7 @@ public class ModelNode extends Node } } } - + // documentation inherited from interface ModelSpatial public void lockStaticMeshes ( Renderer renderer, boolean useVBOs, boolean useDisplayLists) @@ -246,7 +247,7 @@ public class ModelNode extends Node } } } - + // documentation inherited from interface ModelSpatial public void resolveTextures (TextureProvider tprov) { @@ -257,7 +258,7 @@ public class ModelNode extends Node } } } - + // documentation inherited from interface ModelSpatial public void storeMeshFrame (int frameId, boolean blend) { @@ -268,7 +269,7 @@ public class ModelNode extends Node } } } - + // documentation inherited from interface ModelSpatial public void setMeshFrame (int frameId) { @@ -279,7 +280,7 @@ public class ModelNode extends Node } } } - + // documentation inherited from interface ModelSpatial public void blendMeshFrames (int frameId1, int frameId2, float alpha) { @@ -291,7 +292,7 @@ public class ModelNode extends Node } } } - + /** * Sets whether this node should be culled even if it has mesh * descendants. @@ -300,7 +301,7 @@ public class ModelNode extends Node { _forceCull = force; } - + /** * Checks whether this node should be culled even if it has mesh * descendants. @@ -309,7 +310,7 @@ public class ModelNode extends Node { return _forceCull; } - + /** * Sets the cull state of any nodes that do not contain geometric * descendants to {@link CULL_ALWAYS} so that they don't waste @@ -330,7 +331,7 @@ public class ModelNode extends Node updateCullMode(); return _hasVisibleDescendants; } - + /** * Locks the transforms and bounds of this instance with the assumption * that the position will never change. @@ -344,14 +345,14 @@ public class ModelNode extends Node { 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(); } @@ -364,7 +365,7 @@ public class ModelNode extends Node return false; } } - + /** * Recursively culls all model nodes under this one in preparation for * activating the ones listed in an animation. @@ -379,7 +380,7 @@ public class ModelNode extends Node } } } - + /** * Makes this node visible if and only if it has visible descendants and * has not been specifically culled. @@ -389,42 +390,16 @@ public class ModelNode extends Node setCullMode((_hasVisibleDescendants && !_forceCull) ? CULL_INHERIT : CULL_ALWAYS); } - - /** - * Sets a matrix to the transform defined by the given translation, - * rotation, and scale values. - */ - protected static Matrix4f setTransform ( - Vector3f translation, Quaternion rotation, Vector3f scale, - Matrix4f result) - { - result.setRotationQuaternion(rotation); - result.setTranslation(translation); - - result.m00 *= scale.x; - result.m01 *= scale.y; - result.m02 *= scale.z; - - result.m10 *= scale.x; - result.m11 *= scale.y; - result.m12 *= scale.z; - - result.m20 *= scale.x; - result.m21 *= scale.y; - result.m22 *= scale.z; - - return result; - } - + /** The node's transform in local and model space. */ protected Matrix4f _localTransform = new Matrix4f(), _modelTransform = new Matrix4f(); /** Whether or not this node has mesh descendants. */ protected boolean _hasVisibleDescendants; - + /** If true, cull the node even if it has mesh descendants. */ protected boolean _forceCull; - + private static final long serialVersionUID = 1; } diff --git a/src/java/com/threerings/jme/model/SkinMesh.java b/src/java/com/threerings/jme/model/SkinMesh.java index 781b4224..ded220c8 100644 --- a/src/java/com/threerings/jme/model/SkinMesh.java +++ b/src/java/com/threerings/jme/model/SkinMesh.java @@ -54,6 +54,7 @@ import com.samskivert.util.ArrayUtil; import com.samskivert.util.HashIntMap; import com.threerings.jme.Log; +import com.threerings.jme.util.JmeUtil; /** * A triangle mesh that deforms according to a bone hierarchy. @@ -67,10 +68,10 @@ public class SkinMesh extends ModelMesh { /** The number of vertices in this weight group. */ public int vertexCount; - + /** The bones influencing this group. */ public Bone[] bones; - + /** The array of interleaved weights (of length vertexCount * * boneIndices.length): weights for first vertex, weights for * second, etc. */ @@ -92,13 +93,13 @@ public class SkinMesh extends ModelMesh wgroup.weights = weights; return wgroup; } - + // documentation inherited public Class getClassTag () { return getClass(); } - + // documentation inherited public void read (JMEImporter im) throws IOException @@ -119,7 +120,7 @@ public class SkinMesh extends ModelMesh capsule.write(bones, "bones", null); capsule.write(weights, "weights", null); } - + private static final long serialVersionUID = 1; } @@ -129,24 +130,24 @@ public class SkinMesh extends ModelMesh { /** The node that defines the bone's position. */ public ModelNode node; - + /** The inverse of the bone's model space reference transform. */ public transient Matrix4f invRefTransform; - + /** The bone's current transform in model space. */ public transient Matrix4f transform; - + public Bone (ModelNode node) { this(); this.node = node; } - + public Bone () { transform = new Matrix4f(); } - + /** * Rebinds this bone for a prototype instance. * @@ -159,13 +160,13 @@ public class SkinMesh extends ModelMesh bone.transform = new Matrix4f(); return bone; } - + // documentation inherited public Class getClassTag () { return getClass(); } - + // documentation inherited public void read (JMEImporter im) throws IOException @@ -181,17 +182,17 @@ public class SkinMesh extends ModelMesh OutputCapsule capsule = ex.getCapsule(this); capsule.write(node, "node", null); } - + private static final long serialVersionUID = 1; } - + /** * No-arg constructor for deserialization. */ public SkinMesh () { } - + /** * Creates an empty mesh. */ @@ -199,7 +200,7 @@ public class SkinMesh extends ModelMesh { super(name); } - + /** * Sets the array of weight groups that determine how bones affect * each vertex. @@ -207,7 +208,7 @@ public class SkinMesh extends ModelMesh public void setWeightGroups (WeightGroup[] weightGroups) { _weightGroups = weightGroups; - + // compile a list of all referenced bones HashSet bones = new HashSet(); for (WeightGroup group : weightGroups) { @@ -215,18 +216,18 @@ public class SkinMesh extends ModelMesh } _bones = bones.toArray(new Bone[bones.size()]); } - + @Override // documentation inherited public void reconstruct ( FloatBuffer vertices, FloatBuffer normals, FloatBuffer colors, FloatBuffer textures, IntBuffer indices) { super.reconstruct(vertices, normals, colors, textures, indices); - + // initialize the quantized frame table _frames = new HashIntMap(); } - + @Override // documentation inherited public Spatial putClone (Spatial store, Model.CloneCreator properties) { @@ -264,7 +265,7 @@ public class SkinMesh extends ModelMesh mstore._nbuf = new float[_nbuf.length]; return mstore; } - + @Override // documentation inherited public void read (JMEImporter im) throws IOException @@ -274,7 +275,7 @@ public class SkinMesh extends ModelMesh setWeightGroups(ArrayUtil.copy(capsule.readSavableArray( "weightGroups", null), new WeightGroup[0])); } - + @Override // documentation inherited public void write (JMEExporter ex) throws IOException @@ -283,7 +284,7 @@ public class SkinMesh extends ModelMesh OutputCapsule capsule = ex.getCapsule(this); capsule.write(_weightGroups, "weightGroups", null); } - + @Override // documentation inherited public void expandModelBounds () { @@ -292,14 +293,14 @@ public class SkinMesh extends ModelMesh updateModelBound(); getBatch(0).getModelBound().mergeLocal(obound); } - + @Override // documentation inherited public void setReferenceTransforms () { _invRefTransform = new Matrix4f(); if (parent instanceof ModelNode) { Matrix4f transform = new Matrix4f(); - ModelNode.setTransform(getLocalTranslation(), getLocalRotation(), + JmeUtil.setTransform(getLocalTranslation(), getLocalRotation(), getLocalScale(), transform); ((ModelNode)parent).getModelTransform().mult(transform, _invRefTransform); @@ -310,7 +311,7 @@ public class SkinMesh extends ModelMesh _invRefTransform.mult(bone.node.getModelTransform()).invert(); } } - + @Override // documentation inherited public void lockStaticMeshes ( Renderer renderer, boolean useVBOs, boolean useDisplayLists) @@ -325,14 +326,14 @@ public class SkinMesh extends ModelMesh } _useDisplayLists = useDisplayLists && !_translucent; } - + @Override // documentation inherited public void storeMeshFrame (int frameId, boolean blend) { _storeFrameId = frameId; _storeBlend = blend; } - + @Override // documentation inherited public void setMeshFrame (int frameId) { @@ -346,7 +347,7 @@ public class SkinMesh extends ModelMesh getBatch(0).updateRenderState(); } } - + @Override // documentation inherited public void blendMeshFrames (int frameId1, int frameId2, float alpha) { @@ -359,7 +360,7 @@ public class SkinMesh extends ModelMesh nbuf.rewind(); nbuf.put(_nbuf); } - + @Override // documentation inherited public void updateWorldData (float time) { @@ -373,7 +374,7 @@ public class SkinMesh extends ModelMesh bone.transform); bone.transform.multLocal(bone.invRefTransform); } - + // deform the mesh according to the positions of the bones (this code // is ugly as sin because it's optimized at a low level) Bone[] bones; @@ -397,11 +398,11 @@ public class SkinMesh extends ModelMesh for (kk = 0; kk < bones.length; kk++) { m = bones[kk].transform; weight = weights[ww++]; - + vx += (ovx*m.m00 + ovy*m.m01 + ovz*m.m02 + m.m03) * weight; vy += (ovx*m.m10 + ovy*m.m11 + ovz*m.m12 + m.m13) * weight; vz += (ovx*m.m20 + ovy*m.m21 + ovz*m.m22 + m.m23) * weight; - + nx += (onx*m.m00 + ony*m.m01 + onz*m.m02) * weight; ny += (onx*m.m10 + ony*m.m11 + onz*m.m12) * weight; nz += (onx*m.m20 + ony*m.m21 + onz*m.m22) * weight; @@ -414,7 +415,7 @@ public class SkinMesh extends ModelMesh _nbuf[bidx++] = nz; } } - + // if skinning in real time, copy the data from arrays to buffers; // otherwise, store the mesh as an animation frame if (_storeFrameId == 0) { @@ -428,7 +429,7 @@ public class SkinMesh extends ModelMesh _storeFrameId = -1; } } - + /** * Stores the current frame data for later use. */ @@ -465,12 +466,12 @@ public class SkinMesh extends ModelMesh _frames.put(_storeFrameId, tbatch); } } - + @Override // documentation inherited protected void storeOriginalBuffers () { super.storeOriginalBuffers(); - + FloatBuffer vbuf = getVertexBuffer(0), nbuf = getNormalBuffer(0); vbuf.rewind(); nbuf.rewind(); @@ -479,19 +480,19 @@ public class SkinMesh extends ModelMesh _vbuf = new float[_ovbuf.length]; _nbuf = new float[_onbuf.length]; } - + /** A stored frame used for linear blending. */ protected static class BlendFrame { /** The skinned vertex and normal values. */ public float[] vbuf, nbuf; - + public BlendFrame (float[] vbuf, float[] nbuf) { this.vbuf = vbuf; this.nbuf = nbuf; } - + public void blend ( BlendFrame next, float alpha, float[] rvbuf, float[] rnbuf) { @@ -502,26 +503,26 @@ public class SkinMesh extends ModelMesh rnbuf[ii] = nbuf[ii] * ialpha + nnbuf[ii] * alpha; } } - } - + } + /** Pre-skinned {@link TriangleBatch}es or {@link BlendFrame}s shared * between all instances corresponding to frame ids from * {@link #storeAnimationFrame}. */ protected HashIntMap _frames; - + /** Whether or to use display lists if VBOs are unavailable for quantized * meshes. */ protected boolean _useDisplayLists; - + /** The inverse of the model space reference transform. */ protected Matrix4f _invRefTransform; - + /** The groups of vertices influenced by different sets of bones. */ protected WeightGroup[] _weightGroups; - + /** The bones referenced by the weight groups. */ protected Bone[] _bones; - + /** The original (undeformed) vertex and normal buffers and the deformed * versions. */ protected float[] _onbuf, _ovbuf, _nbuf; @@ -530,12 +531,12 @@ public class SkinMesh extends ModelMesh * and skin the mesh as normal. If -1, a frame has been stored and thus * skinning should only take place when further frames are requested. */ protected int _storeFrameId; - + /** Whether or not the stored frame id will be used for blending. */ protected boolean _storeBlend; - + /** A dummy mesh that simply hold transformation values. */ protected static final TriMesh DUMMY_MESH = new TriMesh(); - + private static final long serialVersionUID = 1; } diff --git a/src/java/com/threerings/jme/tools/AnimationDef.java b/src/java/com/threerings/jme/tools/AnimationDef.java index 4b1d5c91..c81cbff2 100644 --- a/src/java/com/threerings/jme/tools/AnimationDef.java +++ b/src/java/com/threerings/jme/tools/AnimationDef.java @@ -26,15 +26,19 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Properties; +import com.jme.math.Matrix4f; import com.jme.math.Quaternion; import com.jme.math.Vector3f; import com.jme.scene.Controller; +import com.jme.scene.Node; import com.jme.scene.Spatial; import com.threerings.jme.Log; import com.threerings.jme.model.Model; import com.threerings.jme.util.JmeUtil; +import com.threerings.jme.tools.ModelDef.TransformNode; + /** * A basic representation for keyframe animations. */ @@ -42,35 +46,39 @@ public class AnimationDef { /** The rate of the animation in frames per second. */ public int frameRate; - + /** A single frame of the animation. */ public static class FrameDef { /** Transform for affected nodes. */ public ArrayList transforms = new ArrayList(); - + public void addTransform (TransformDef transform) { transforms.add(transform); } - - /** Adds all transform targets in this frame to the supplied set. */ + + /** Adds all transform targets in this frame to the supplied sets. */ public void addTransformTargets ( - HashMap nodes, HashSet targets) + HashMap nodes, HashMap tnodes, + HashSet staticTargets, HashSet transformTargets) { for (int ii = 0, nn = transforms.size(); ii < nn; ii++) { String name = transforms.get(ii).name; Spatial target = nodes.get(name); - if (target != null) { - targets.add(target); + if (target == null) { + continue; + } + TransformNode tnode = tnodes.get(name); + if (tnode.baseLocalTransform != null) { + staticTargets.add(target); } else { - Log.debug("Missing animation target [name=" + name + - "]."); + transformTargets.add(target); } } } - + /** Returns the array of transforms for this frame. */ public Model.Transform[] getTransforms (Spatial[] targets) { @@ -81,7 +89,7 @@ public class AnimationDef } return mtransforms; } - + /** Returns the transform for the supplied target. */ protected Model.Transform getTransform (Spatial target) { @@ -95,18 +103,18 @@ public class AnimationDef return null; } } - + /** A transform for a single node. */ public static class TransformDef { /** The name of the affected node. */ public String name; - + /** The transformation parameters. */ public float[] translation; public float[] rotation; public float[] scale; - + /** Returns the live transform object. */ public Model.Transform getTransform () { @@ -117,15 +125,36 @@ public class AnimationDef new Vector3f(scale[0], scale[1], scale[2])); } } - + /** The individual frames of the animation. */ public ArrayList frames = new ArrayList(); - + public void addFrame (FrameDef frame) { frames.add(frame); } - + + /** + * Runs through each frame of the animation, updating the transforms of the preprocessing + * node to keep track of which transforms diverge from their original states. + */ + public void filterTransforms (Node root, HashMap nodes) + { + for (FrameDef frame : frames) { + for (TransformDef transform : frame.transforms) { + TransformNode node = nodes.get(transform.name); + if (node != null) { + node.setLocalTransform( + transform.translation, transform.rotation, transform.scale); + } + } + root.updateGeometricState(0f, true); + for (TransformNode node : nodes.values()) { + node.cullDivergentTransforms(); + } + } + } + /** * Creates the "live" animation object that will be serialized with the * object. @@ -134,27 +163,30 @@ public class AnimationDef * @param nodes the nodes in the model, mapped by name */ public Model.Animation createAnimation ( - Properties props, HashMap nodes) + Properties props, HashMap nodes, HashMap tnodes) { // find all affected nodes - HashSet targets = new HashSet(); + HashSet staticTargets = new HashSet(), + transformTargets = new HashSet(); for (int ii = 0, nn = frames.size(); ii < nn; ii++) { - frames.get(ii).addTransformTargets(nodes, targets); + frames.get(ii).addTransformTargets(nodes, tnodes, staticTargets, transformTargets); } - + // create and configure the animation Model.Animation anim = new Model.Animation(); anim.frameRate = frameRate; anim.repeatType = JmeUtil.parseRepeatType(props.getProperty("repeat_type"), Controller.RT_CLAMP); - + // collect all transforms - anim.transformTargets = targets.toArray(new Spatial[targets.size()]); - anim.transforms = new Model.Transform[frames.size()][targets.size()]; + anim.staticTargets = staticTargets.toArray(new Spatial[staticTargets.size()]); + anim.transformTargets = transformTargets.toArray(new Spatial[transformTargets.size()]); + anim.transforms = new Model.Transform[frames.size()][transformTargets.size()]; for (int ii = 0; ii < anim.transforms.length; ii++) { anim.transforms[ii] = frames.get(ii).getTransforms(anim.transformTargets); } + return anim; } } diff --git a/src/java/com/threerings/jme/tools/CompileModel.java b/src/java/com/threerings/jme/tools/CompileModel.java index 6cdaa29d..5fa8aec9 100644 --- a/src/java/com/threerings/jme/tools/CompileModel.java +++ b/src/java/com/threerings/jme/tools/CompileModel.java @@ -32,6 +32,7 @@ import java.util.HashMap; import java.util.Properties; import java.util.logging.Level; +import com.jme.scene.Node; import com.jme.scene.Spatial; import com.jme.util.LoggingSystem; import com.jmex.model.XMLparser.Converters.DummyDisplaySystem; @@ -43,6 +44,7 @@ import com.threerings.jme.model.Model; import com.threerings.jme.model.ModelMesh; import com.threerings.jme.model.ModelNode; import com.threerings.jme.model.SkinMesh; +import com.threerings.jme.tools.ModelDef.TransformNode; import com.threerings.jme.tools.xml.AnimationParser; import com.threerings.jme.tools.xml.ModelParser; @@ -106,18 +108,32 @@ public class CompileModel System.out.println("Compiling to " + target + "..."); - // load the model content + // parse the model and animations ModelDef mdef = _mparser.parseModel(content.toString()); + AnimationDef[] adefs = new AnimationDef[anims.length]; + for (int ii = 0; ii < adefs.length; ii++) { + System.out.println(" Adding " + afiles[ii] + "..."); + adefs[ii] = _aparser.parseAnimation(afiles[ii].toString()); + } + + // preprocess the model and animations to determine which nodes never move, which never + // move within an animation, and which never move with respect to others + HashMap tnodes = new HashMap(); + Node troot = mdef.createTransformTree(props, tnodes); + for (AnimationDef adef : adefs) { + adef.filterTransforms(troot, tnodes); + } + mdef.mergeSpatials(tnodes); + + // load the model content HashMap nodes = new HashMap(); Model model = mdef.createModel(props, nodes); model.initPrototype(); // load the animations, if any for (int ii = 0; ii < anims.length; ii++) { - System.out.println(" Adding " + afiles[ii] + "..."); - AnimationDef adef = _aparser.parseAnimation(afiles[ii].toString()); - model.addAnimation(anims[ii], adef.createAnimation( - PropertiesUtil.getSubProperties(props, anims[ii]), nodes)); + model.addAnimation(anims[ii], adefs[ii].createAnimation( + PropertiesUtil.getSubProperties(props, anims[ii]), nodes, tnodes)); } // write and return the model diff --git a/src/java/com/threerings/jme/tools/ModelDef.java b/src/java/com/threerings/jme/tools/ModelDef.java index 1d29fa75..5b0eb004 100644 --- a/src/java/com/threerings/jme/tools/ModelDef.java +++ b/src/java/com/threerings/jme/tools/ModelDef.java @@ -39,13 +39,18 @@ import java.util.Set; import com.jme.bounding.BoundingBox; import com.jme.bounding.BoundingSphere; import com.jme.math.FastMath; +import com.jme.math.Matrix4f; +import com.jme.math.Quaternion; import com.jme.math.Vector3f; +import com.jme.scene.Node; import com.jme.scene.Spatial; import com.jme.scene.batch.GeomBatch; import com.jme.util.geom.BufferUtils; +import com.samskivert.util.ObjectUtil; import com.samskivert.util.PropertiesUtil; import com.samskivert.util.StringUtil; +import com.samskivert.util.Tuple; import com.threerings.jme.Log; import com.threerings.jme.model.Model; @@ -53,6 +58,7 @@ import com.threerings.jme.model.ModelController; import com.threerings.jme.model.ModelMesh; import com.threerings.jme.model.ModelNode; import com.threerings.jme.model.SkinMesh; +import com.threerings.jme.util.JmeUtil; import com.threerings.jme.util.SpatialVisitor; /** @@ -75,6 +81,21 @@ public class ModelDef public float[] rotation; public float[] scale; + /** + * Stores the names of all bones referenced by this spatial in the supplied set. + */ + public void getBoneNames (HashSet bones) + { + // nothing by default + } + + /** Checks whether it's possible (disregarding issues of transformation) to merge + * the specified spatial into this one. */ + public abstract boolean canMerge (Properties props, SpatialDef other); + + /** Merges another spatial into this one. */ + public abstract void merge (SpatialDef other, Matrix4f xform); + /** Returns a JME node for this definition. */ public Spatial getSpatial (Properties props) { @@ -155,6 +176,42 @@ public class ModelDef tcoords = tcoords || vertex.tcoords != null; } + // documentation inherited + public boolean canMerge (Properties props, SpatialDef other) + { + if (getClass() != other.getClass()) { + return false; // require exact class match + } + TriMeshDef omesh = (TriMeshDef)other; + return solid == omesh.solid && transparent == omesh.transparent && + ObjectUtil.equals(texture, omesh.texture) && + PropertiesUtil.getSubProperties(props, name).equals( + PropertiesUtil.getSubProperties(props, omesh.name)); + } + + // documentation inherited + public void merge (SpatialDef other, Matrix4f xform) + { + TriMeshDef omesh = (TriMeshDef)other; + + // prepend the inverse of the offset transformation + xform = getOffsetTransform().invertLocal().multLocal(xform); + + // and append the other's offset + xform.multLocal(omesh.getOffsetTransform()); + + // extract the rotation to transform normals + Quaternion xrot = xform.toRotationQuat(); + + // transform the vertices and add them in + for (Vertex vertex : omesh.vertices) { + vertex.transform(xform, xrot); + } + for (int idx : omesh.indices) { + addVertex(omesh.vertices.get(idx)); + } + } + // documentation inherited public Spatial createSpatial (Properties props) { @@ -167,6 +224,24 @@ public class ModelDef return node; } + /** Gets the matrix representing the offset transform. */ + protected Matrix4f getOffsetTransform () + { + Vector3f otrans = new Vector3f(), oscale = new Vector3f(1f, 1f, 1f); + Quaternion orot = new Quaternion(); + if (offsetTranslation != null) { + otrans.set(offsetTranslation[0], offsetTranslation[1], offsetTranslation[2]); + } + if (offsetRotation != null) { + orot.set(offsetRotation[0], offsetRotation[1], offsetRotation[2], + offsetRotation[3]); + } + if (offsetScale != null) { + oscale.set(offsetScale[0], offsetScale[1], offsetScale[2]); + } + return JmeUtil.setTransform(otrans, orot, oscale, new Matrix4f()); + } + /** Creates the mesh to attach to the node. */ protected ModelMesh createMesh () { @@ -229,6 +304,14 @@ public class ModelDef /** A triangle mesh that deforms according to bone positions. */ public static class SkinMeshDef extends TriMeshDef { + @Override // documentation inherited + public void getBoneNames (HashSet bones) + { + for (Vertex vertex : vertices) { + bones.addAll(((SkinVertex)vertex).boneWeights.keySet()); + } + } + @Override // documentation inherited protected ModelMesh createMesh () { @@ -250,6 +333,7 @@ public class ModelDef HashMap bones = new HashMap(); int ii = 0; + int mweights = 0, tweights = 0; for (Map.Entry, WeightGroupDef> entry : _groups.entrySet()) { SkinMesh.WeightGroup wgroup = new SkinMesh.WeightGroup(); @@ -267,6 +351,8 @@ public class ModelDef wgroup.bones[jj++] = bone; } wgroup.weights = toArray(entry.getValue().weights); + tweights += wgroup.bones.length; + mweights = Math.max(wgroup.bones.length, mweights); wgroups[ii++] = wgroup; } ((SkinMesh)_mesh).setWeightGroups(wgroups); @@ -315,6 +401,18 @@ public class ModelDef /** A generic node. */ public static class NodeDef extends SpatialDef { + // documentation inherited + public boolean canMerge (Properties props, SpatialDef other) + { + return false; + } + + // documentation inherited + public void merge (SpatialDef other, Matrix4f xform) + { + throw new UnsupportedOperationException(); + } + // documentation inherited public Spatial createSpatial (Properties props) { @@ -329,6 +427,21 @@ public class ModelDef public float[] normal; public float[] tcoords; + public void transform (Matrix4f xform, Quaternion xrot) + { + Vector3f xvec = new Vector3f(location[0], location[1], location[2]); + xform.mult(xvec, xvec); + location[0] = xvec.x; + location[1] = xvec.y; + location[2] = xvec.z; + + xvec.set(normal[0], normal[1], normal[2]); + xrot.mult(xvec, xvec); + normal[0] = xvec.x; + normal[1] = xvec.y; + normal[2] = xvec.z; + } + public void setInBuffers ( FloatBuffer vbuf, FloatBuffer nbuf, FloatBuffer tbuf) { @@ -418,6 +531,99 @@ public class ModelDef public ArrayList weights = new ArrayList(); } + /** Contains the transform of a node for preprocessing. */ + public static class TransformNode extends Node + { + /** The source definition. */ + public SpatialDef spatial; + + /** If true, this node is referenced by name (as a bone or parent) and cannot be merged + * into another. */ + public boolean referenced; + + /** If true, this node is a controller target; nodes beneath it can only be merged with + * other descendants. */ + public boolean controlled; + + /** The node's current local transform. */ + public Matrix4f localTransform = new Matrix4f(); + + /** The node's current world space transform. */ + public Matrix4f worldTransform = new Matrix4f(); + + /** The node's local transform in the original model, or null if the node's + * transform has diverged from the original. */ + public Matrix4f baseLocalTransform; + + /** The relative transforms between this and all other loosely compatible nodes not yet + * eliminated. As soon as the relative transform diverges in the course of preprocessing + * an animation, the node/transform pair is removed from the list. */ + public ArrayList> relativeTransforms; + + public TransformNode (SpatialDef spatial) + { + super(spatial.name); + this.spatial = spatial; + setLocalTransform(spatial.translation, spatial.rotation, spatial.scale); + } + + public void setLocalTransform (float[] translation, float[] rotation, float[] scale) + { + getLocalTranslation().set(translation[0], translation[1], translation[2]); + getLocalRotation().set(rotation[0], rotation[1], rotation[2], rotation[3]); + getLocalScale().set(scale[0], scale[1], scale[2]); + JmeUtil.setTransform( + getLocalTranslation(), getLocalRotation(), getLocalScale(), localTransform); + } + + @Override // documentation inherited + public void updateWorldVectors () + { + super.updateWorldVectors(); + JmeUtil.setTransform( + getWorldTranslation(), getWorldRotation(), getWorldScale(), worldTransform); + } + + public boolean canMerge (Properties props, TransformNode onode) + { + // nodes must have same controlled ancestor + return !onode.referenced && spatial.canMerge(props, onode.spatial) && + getControlledAncestor() == onode.getControlledAncestor(); + } + + protected Node getControlledAncestor () + { + Node ref = this; + while (ref instanceof TransformNode && !((TransformNode)ref).controlled) { + ref = ref.getParent(); + } + return ref; + } + + public void cullDivergentTransforms () + { + if (baseLocalTransform != null && !epsilonEquals(localTransform, baseLocalTransform)) { + baseLocalTransform = null; + } + for (Iterator> it = relativeTransforms.iterator(); + it.hasNext(); ) { + Tuple tuple = it.next(); + if (!epsilonEquals(getRelativeTransform(tuple.left), tuple.right)) { + it.remove(); + } + } + } + + public Matrix4f getRelativeTransform (TransformNode other) + { + // return the matrix that takes vertices from the space of the other node + // into the space of this one + Matrix4f inv = new Matrix4f(); + worldTransform.invert(inv); + return inv.mult(other.worldTransform); + } + } + /** The meshes and bones comprising the model. */ public ArrayList spatials = new ArrayList(); @@ -428,6 +634,85 @@ public class ModelDef spatial); } + /** + * Creates and returns a transform tree representing the model for preprocessing. + */ + public Node createTransformTree (Properties props, HashMap nodes) + { + // create the nodes and map them by name + for (SpatialDef spatial : spatials) { + nodes.put(spatial.name, new TransformNode(spatial)); + } + + // resolve the parents and collect the names of the bones + Node root = new Node("root"); + HashSet bones = new HashSet(); + for (TransformNode node : nodes.values()) { + if (node.spatial.parent == null) { + root.attachChild(node); + } else { + TransformNode pnode = nodes.get(node.spatial.parent); + if (pnode != null) { + pnode.attachChild(node); + pnode.referenced = true; + } + } + node.spatial.getBoneNames(bones); + } + + // mark the bones as referenced + for (String name : bones) { + TransformNode node = nodes.get(name); + if (node != null) { + node.referenced = true; + } + } + + // mark the controlled nodes + String[] controllers = StringUtil.parseStringArray(props.getProperty("controllers", "")); + for (String controller : controllers) { + Properties subProps = PropertiesUtil.getSubProperties(props, controller); + TransformNode node = nodes.get(subProps.getProperty("node", controller)); + if (node != null) { + node.referenced = node.controlled = true; + } + } + + // store the base transforms and relative transforms for merge candidates + root.updateGeometricState(0f, true); + for (TransformNode node : nodes.values()) { + node.baseLocalTransform = new Matrix4f(node.localTransform); + node.relativeTransforms = new ArrayList>(); + for (TransformNode onode : nodes.values()) { + if (node == onode || !node.canMerge(props, onode)) { + continue; + } + node.relativeTransforms.add(new Tuple( + onode, node.getRelativeTransform(onode))); + } + } + + return root; + } + + /** + * Merges compatible meshes that retain the same relative transform throughout all animations. + */ + public void mergeSpatials (HashMap nodes) + { + for (TransformNode node : nodes.values()) { + if (!spatials.contains(node.spatial)) { + continue; + } + for (Tuple tuple : node.relativeTransforms) { + if (spatials.contains(tuple.left.spatial)) { + node.spatial.merge(tuple.left.spatial, tuple.right); + spatials.remove(tuple.left.spatial); + } + } + } + } + /** * Creates the model node defined herein. * @@ -475,11 +760,6 @@ public class ModelDef } } - // in non-animated models, merge meshes with the same attributes - if (StringUtil.isBlank(props.getProperty("animations"))) { - mergeEquivalentMeshes(model, nodes, referenced); - } - // get rid of any nodes that serve no purpose pruneUnusedNodes(model, nodes, referenced); @@ -526,63 +806,17 @@ public class ModelDef return referenced.contains(node) || hasValidChildren; } - /** Merges meshes with the same attributes. */ - protected void mergeEquivalentMeshes ( - Model model, HashMap nodes, final HashSet referenced) + /** Determines whether a pair of matrices are "close enough" to equal. */ + public static boolean epsilonEquals (Matrix4f m1, Matrix4f m2) { - final HashMap> meshes = - new HashMap>(); - new SpatialVisitor(ModelMesh.class) { - public void traverse (Spatial spatial) { - // cut out when we encounter referenced nodes - if (!referenced.contains(spatial)) { - spatial.updateWorldVectors(); - super.traverse(spatial); + for (int ii = 0; ii < 4; ii++) { + for (int jj = 0; jj < 4; jj++) { + if (FastMath.abs(m1.get(ii, jj) - m2.get(ii, jj)) > 0.0001f) { + return false; } } - protected void visit (ModelMesh mesh) { - // make sure we don't try this with skinned meshes - if (mesh instanceof SkinMesh) { - return; - } - Object key = mesh.getAttributeKey(); - ArrayList list = meshes.get(key); - if (list == null) { - meshes.put(key, list = new ArrayList()); - } - list.add(mesh); - } - }.traverse(model); - - for (ArrayList list : meshes.values()) { - // the first in the list becomes the base - ModelMesh base = list.get(0); - flattenTransforms(base); - model.attachChild(base); - - // the rest are merged with it - for (int ii = 1, nn = list.size(); ii < nn; ii++) { - ModelMesh mesh = list.get(ii); - flattenTransforms(mesh); - mesh.getParent().detachChild(mesh); - base.merge(mesh); - } - - // update the model bound and recenter - base.updateModelBound(); - base.centerVertices(); } - } - - /** Transforms the mesh's vertices and normals into model coordinates. */ - protected static void flattenTransforms (ModelMesh mesh) - { - GeomBatch batch = mesh.getBatch(0); - batch.getWorldCoords(batch.getVertexBuffer()); - batch.getWorldNormals(batch.getNormalBuffer()); - mesh.getLocalTranslation().zero(); - mesh.getLocalRotation().loadIdentity(); - mesh.getLocalScale().set(Vector3f.UNIT_XYZ); + return true; } /** Converts a boxed Integer list to an unboxed int array. */ diff --git a/src/java/com/threerings/jme/util/JmeUtil.java b/src/java/com/threerings/jme/util/JmeUtil.java index 0fce6fab..e9ce41c9 100644 --- a/src/java/com/threerings/jme/util/JmeUtil.java +++ b/src/java/com/threerings/jme/util/JmeUtil.java @@ -30,6 +30,8 @@ import java.nio.IntBuffer; import org.lwjgl.opengl.ARBShaderObjects; +import com.jme.math.Matrix4f; +import com.jme.math.Quaternion; import com.jme.math.Vector3f; import com.jme.scene.Controller; import com.jme.scene.state.GLSLShaderObjectsState; @@ -111,6 +113,31 @@ public class JmeUtil } } + /** + * Sets a matrix to the transform defined by the given translation, + * rotation, and scale values. + */ + public static Matrix4f setTransform ( + Vector3f translation, Quaternion rotation, Vector3f scale, Matrix4f result) + { + result.setRotationQuaternion(rotation); + result.setTranslation(translation); + + result.m00 *= scale.x; + result.m01 *= scale.y; + result.m02 *= scale.z; + + result.m10 *= scale.x; + result.m11 *= scale.y; + result.m12 *= scale.z; + + result.m20 *= scale.x; + result.m21 *= scale.y; + result.m22 *= scale.z; + + return result; + } + /** * Attempts to parse a string containing an axis: either "x", "y", or "z", or three * comma-delimited values representing an axis vector. The value returned may be one of