From a0ff0d14573795698c27d90f5e6428c07cc849ec Mon Sep 17 00:00:00 2001 From: Andrzej Kapolka Date: Tue, 18 Apr 2006 01:36:19 +0000 Subject: [PATCH] Skinned animation now works, although there are still some optimizations to be made. git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@4027 542714f4-19e9-0310-aa3c-eee0fc999fb1 --- rsrc/i18n/jme/viewer.properties | 8 +- .../com/threerings/jme/model/BoneNode.java | 98 ---------- src/java/com/threerings/jme/model/Model.java | 179 +++++++++++++++++- .../com/threerings/jme/model/ModelMesh.java | 20 +- .../com/threerings/jme/model/ModelNode.java | 60 +++++- .../threerings/jme/model/ModelSpatial.java | 7 + .../com/threerings/jme/model/SkinMesh.java | 121 ++++++++++-- .../threerings/jme/tools/AnimationDef.java | 19 +- .../jme/tools/CompileModelTask.java | 19 +- .../com/threerings/jme/tools/ModelDef.java | 53 ++---- .../com/threerings/jme/tools/ModelViewer.java | 115 +++++++---- .../jme/tools/xml/AnimationParser.java | 1 + .../threerings/jme/tools/xml/ModelParser.java | 6 - src/ms/TRAnimationExporter.mcr | 16 +- src/ms/TRModelExporter.mcr | 18 +- 15 files changed, 504 insertions(+), 236 deletions(-) delete mode 100644 src/java/com/threerings/jme/model/BoneNode.java diff --git a/rsrc/i18n/jme/viewer.properties b/rsrc/i18n/jme/viewer.properties index 0ace712e9..440ea8611 100644 --- a/rsrc/i18n/jme/viewer.properties +++ b/rsrc/i18n/jme/viewer.properties @@ -9,12 +9,14 @@ m.file_menu = File m.file_load = Load Model... m.file_quit = Quit -m.anim_menu = Animations -m.anim_stop = Stop - m.load_title = Select Model to Load m.load_filter = Model Files (*.properties, *.dat) +m.anim_select = Animations: +m.anim_start = Start +m.anim_stop = Stop +m.anim_speed = Speed: + m.compiling_model = Compiling {0}... m.loading_model = Loading {0}... m.loaded_model = Loaded {0} diff --git a/src/java/com/threerings/jme/model/BoneNode.java b/src/java/com/threerings/jme/model/BoneNode.java deleted file mode 100644 index f6c0c93cd..000000000 --- a/src/java/com/threerings/jme/model/BoneNode.java +++ /dev/null @@ -1,98 +0,0 @@ -// -// $Id$ -// -// Narya library - tools for developing networked games -// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved -// http://www.threerings.net/code/narya/ -// -// This library is free software; you can redistribute it and/or modify it -// under the terms of the GNU Lesser General Public License as published -// by the Free Software Foundation; either version 2.1 of the License, or -// (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -package com.threerings.jme.model; - -import java.io.IOException; -import java.io.ObjectInput; -import java.io.ObjectOutput; - -import com.jme.scene.Node; - -import com.jme.math.Quaternion; -import com.jme.math.TransformMatrix; -import com.jme.math.Vector3f; - -import com.threerings.jme.Log; - -/** - * Represents a bone in a scene. Bones can have simple meshes as children, - * or they can be used to deform {@link SkinMesh}es. - */ -public class BoneNode extends ModelNode -{ - /** - * No-arg constructor for deserialization. - */ - public BoneNode () - { - } - - /** - * Default constructor. - * - * @param name the name of the node - */ - public BoneNode (String name) - { - super(name); - } - - @Override // documentation inherited - public void setReferenceTransforms () - { - super.setReferenceTransforms(); - - // store the inverse of the reference transform - _invRefTransform.set(worldRotation, worldTranslation); - _invRefTransform.setScale(worldScale); - _invRefTransform.inverse(); - } - - @Override // documentation inherited - public void updateWorldVectors () - { - super.updateWorldVectors(); - _skinTransform.set(worldRotation, worldTranslation); - _skinTransform.setScale(worldScale); - _skinTransform.multLocal(_invRefTransform, _tempStore); - } - - /** - * Returns a reference to the matrix that transforms vertices in the - * reference position to vertices in the current position. - */ - public TransformMatrix getSkinTransform () - { - return _skinTransform; - } - - /** The inverse of the bone's world space reference transform. */ - protected TransformMatrix _invRefTransform = new TransformMatrix(); - - /** The bone's current skin transform. */ - protected TransformMatrix _skinTransform = new TransformMatrix(); - - /** A temporary vector for transformations. */ - protected Vector3f _tempStore = new Vector3f(); - - private static final long serialVersionUID = 1; -} diff --git a/src/java/com/threerings/jme/model/Model.java b/src/java/com/threerings/jme/model/Model.java index f28d8ed22..98a3346cc 100644 --- a/src/java/com/threerings/jme/model/Model.java +++ b/src/java/com/threerings/jme/model/Model.java @@ -39,8 +39,11 @@ import java.nio.channels.FileChannel; import java.util.HashMap; import java.util.Properties; +import com.samskivert.util.ObserverList; + import com.jme.math.Quaternion; import com.jme.math.Vector3f; +import com.jme.scene.Controller; import com.jme.scene.Spatial; import com.threerings.jme.Log; @@ -50,10 +53,36 @@ import com.threerings.jme.Log; */ public class Model extends ModelNode { + /** Lets listeners know when animations are completed (which only happens + * for non-repeating animations) or cancelled. */ + public interface AnimationObserver + { + /** + * 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. + * + * @return true to remain on the observer list, false to remove self + */ + public boolean animationCancelled (Model model, String anim); + } + /** An animation for the model. */ public static class Animation implements Serializable { + /** 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; + /** The transformation targets of the animation. */ public Spatial[] transformTargets; @@ -173,6 +202,10 @@ public class Model extends ModelNode model.readBuffers(fc); } ois.close(); + + // set the reference transforms before any animations are applied + model.setReferenceTransforms(); + return model; } @@ -225,13 +258,20 @@ public class Model extends ModelNode */ public void startAnimation (String name) { + if (_anim != null) { + stopAnimation(); + } _anim = _anims.get(name); if (_anim == null) { Log.warning("Requested unknown animation [name=" + name + "]."); return; } - _fidx = -1; + _animName = name; + _fidx = 0; + _nidx = 1; + _fdir = +1; + _elapsed = 0f; } /** @@ -239,7 +279,44 @@ public class Model extends ModelNode */ public void stopAnimation () { + if (_anim == null) { + return; + } _anim = null; + _animObservers.apply(new AnimCancelledOp(_animName)); + } + + /** + * Sets the animation speed, which acts as a multiplier for the frame rate + * of each animation. + */ + public void setAnimationSpeed (float speed) + { + _animSpeed = speed; + } + + /** + * Returns the currently configured animation speed. + */ + public float getAnimationSpeed () + { + return _animSpeed; + } + + /** + * Adds an animation observer. + */ + public void addAnimationObserver (AnimationObserver obs) + { + _animObservers.add(obs); + } + + /** + * Removes an animation observer. + */ + public void removeAnimationObserver (AnimationObserver obs) + { + _animObservers.remove(obs); } /** @@ -293,7 +370,49 @@ public class Model extends ModelNode */ protected void updateAnimation (float time) { + // advance the frame counter if necessary + while (_elapsed > 1f) { + advanceFrameCounter(); + _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]); + } + + // if the next index is the same as this one, we are finished + if (_fidx == _nidx) { + _anim = null; + _animObservers.apply(new AnimCompletedOp(_animName)); + return; + } + + _elapsed += (time * _anim.frameRate * _animSpeed); + } + + /** + * Advances the frame counter by one frame. + */ + protected void advanceFrameCounter () + { + _fidx = _nidx; + int nframes = _anim.transforms.length; + if (_anim.repeatType == Controller.RT_CLAMP) { + _nidx = Math.min(_nidx + 1, nframes - 1); + + } else if (_anim.repeatType == Controller.RT_CYCLE) { + _nidx = (_nidx + 1) % nframes; + + } else { // _anim.repeatType == Controller.RT_WRAP + if ((_nidx + _fdir) < 0 || (_nidx + _fdir) >= nframes) { + _fdir *= -1; // reverse direction + } + _nidx += _fdir; + } } /** The model properties. */ @@ -305,14 +424,60 @@ public class Model extends ModelNode /** The currently running animation, or null for none. */ protected Animation _anim; - /** The last frame index. */ - protected int _fidx; + /** The name of the currently running animation, if any. */ + protected String _animName; - /** The time corresponding to the last frame. */ - protected float _ftime; + /** The current animation speed multiplier. */ + protected float _animSpeed = 1f; - /** Identifies a transform frame element. */ - protected static final byte TRANSFORM_ELEMENT = 0; + /** 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; + + /** Animation completion listeners. */ + protected ObserverList _animObservers = + new ObserverList(ObserverList.FAST_UNSAFE_NOTIFY); + + /** Used to notify observers of animation completion. */ + protected class AnimCompletedOp + implements ObserverList.ObserverOp + { + public AnimCompletedOp (String name) + { + _name = name; + } + + 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 + { + public AnimCancelledOp (String name) + { + _name = name; + } + + public boolean apply (AnimationObserver obs) + { + return obs.animationCancelled(Model.this, _name); + } + + /** The name of the animation cancelled. */ + protected String _name; + } private static final long serialVersionUID = 1; } diff --git a/src/java/com/threerings/jme/model/ModelMesh.java b/src/java/com/threerings/jme/model/ModelMesh.java index 84c8e05d1..7d9a5f081 100644 --- a/src/java/com/threerings/jme/model/ModelMesh.java +++ b/src/java/com/threerings/jme/model/ModelMesh.java @@ -71,16 +71,6 @@ public class ModelMesh extends TriMesh super(name); } - /** - * Creates and populates a mesh. - */ - public ModelMesh ( - String name, FloatBuffer vertices, FloatBuffer normals, - FloatBuffer colors, FloatBuffer tcoords, IntBuffer indices) - { - super(name, vertices, normals, colors, tcoords, indices); - } - /** * Configures this mesh based on the given (sub-)properties. */ @@ -138,7 +128,7 @@ public class ModelMesh extends TriMesh } } - // documentation inherited + @Override // documentation inherited public void reconstruct ( FloatBuffer vertices, FloatBuffer normals, FloatBuffer colors, FloatBuffer textures, IntBuffer indices) @@ -152,7 +142,7 @@ public class ModelMesh extends TriMesh _indexBufferSize = (indices == null) ? 0 : indices.capacity(); } - // documentation inherited + @Override // documentation inherited public void reconstruct ( FloatBuffer vertices, FloatBuffer normals, FloatBuffer colors, FloatBuffer textures) @@ -203,6 +193,12 @@ public class ModelMesh extends TriMesh _transparent = in.readBoolean(); } + // documentation inherited from interface ModelSpatial + public void setReferenceTransforms () + { + // no-op + } + // documentation inherited from interface ModelSpatial public void resolveTextures (TextureProvider tprov) { diff --git a/src/java/com/threerings/jme/model/ModelNode.java b/src/java/com/threerings/jme/model/ModelNode.java index f4246443d..52f26ddd7 100644 --- a/src/java/com/threerings/jme/model/ModelNode.java +++ b/src/java/com/threerings/jme/model/ModelNode.java @@ -32,6 +32,7 @@ import java.nio.channels.FileChannel; import java.util.ArrayList; +import com.jme.math.Matrix4f; import com.jme.math.Quaternion; import com.jme.math.Vector3f; import com.jme.scene.Node; @@ -62,18 +63,39 @@ public class ModelNode extends Node } /** - * Recursively sets the reference transforms for any {@link BoneNode}s in - * the model. + * Returns a reference to the model space transform of the node. */ + public Matrix4f getModelTransform () + { + return _modelTransform; + } + + // documentation inherited from interface ModelSpatial public void setReferenceTransforms () { + updateWorldVectors(); for (Object child : getChildren()) { - if (child instanceof ModelNode) { - ((ModelNode)child).setReferenceTransforms(); + if (child instanceof ModelSpatial) { + ((ModelSpatial)child).setReferenceTransforms(); } } } + @Override // documentation inherited + public void updateWorldVectors () + { + super.updateWorldVectors(); + if (parent instanceof ModelNode) { + setTransform(getLocalTranslation(), getLocalRotation(), + getLocalScale(), _localTransform); + ((ModelNode)parent).getModelTransform().mult(_localTransform, + _modelTransform); + + } else { + _modelTransform.loadIdentity(); + } + } + // documentation inherited from interface Externalizable public void writeExternal (ObjectOutput out) throws IOException @@ -141,5 +163,35 @@ public class ModelNode extends Node } } + /** + * 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.set(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(); + private static final long serialVersionUID = 1; } diff --git a/src/java/com/threerings/jme/model/ModelSpatial.java b/src/java/com/threerings/jme/model/ModelSpatial.java index d3681a1b2..84c0365d3 100644 --- a/src/java/com/threerings/jme/model/ModelSpatial.java +++ b/src/java/com/threerings/jme/model/ModelSpatial.java @@ -27,11 +27,18 @@ import java.io.IOException; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; +import com.jme.math.Matrix4f; + /** * Contains method common to both {@link ModelNode}s and {@link ModelMesh}es. */ public interface ModelSpatial { + /** + * Recursively sets the reference transforms for any bones in the model. + */ + public void setReferenceTransforms (); + /** * Recursively resolves texture references using the given provider. */ diff --git a/src/java/com/threerings/jme/model/SkinMesh.java b/src/java/com/threerings/jme/model/SkinMesh.java index 2f914836b..12619390e 100644 --- a/src/java/com/threerings/jme/model/SkinMesh.java +++ b/src/java/com/threerings/jme/model/SkinMesh.java @@ -26,9 +26,14 @@ import java.io.ObjectInput; import java.io.ObjectOutput; import java.io.Serializable; +import java.nio.ByteBuffer; import java.nio.FloatBuffer; import java.nio.IntBuffer; +import com.jme.math.Matrix4f; +import com.jme.math.Vector3f; +import com.jme.util.geom.BufferUtils; + import com.threerings.jme.Log; /** @@ -45,13 +50,16 @@ public class SkinMesh extends ModelMesh public int[] indices; /** The bones influencing this group. */ - public BoneNode[] bones; + public ModelNode[] bones; /** The array of interleaved weights (of length indices.length * * bones.length): weights for first vertex, weights for second, * etc. */ public float[] weights; + /** The inverses of the bones' mesh space reference transforms. */ + public transient Matrix4f[] invRefTransforms; + private static final long serialVersionUID = 1; } @@ -70,16 +78,6 @@ public class SkinMesh extends ModelMesh super(name); } - /** - * Creates and populates a mesh. - */ - public SkinMesh ( - String name, FloatBuffer vertices, FloatBuffer normals, - FloatBuffer color, FloatBuffer texture, IntBuffer indices) - { - super(name, vertices, normals, color, texture, indices); - } - /** * Sets the weight groups that determine how vertices are affected by * bones. @@ -97,6 +95,18 @@ public class SkinMesh extends ModelMesh return _weightGroups; } + @Override // documentation inherited + public void reconstruct ( + ByteBuffer vertices, ByteBuffer normals, ByteBuffer colors, + ByteBuffer textures, ByteBuffer indices) + { + super.reconstruct(vertices, normals, colors, textures, indices); + + // replace the vertex and normal buffers with working buffers + setVertexBuffer(BufferUtils.clone(_ovbuf = getVertexBuffer())); + setNormalBuffer(BufferUtils.clone(_onbuf = getNormalBuffer())); + } + @Override // documentation inherited public void writeExternal (ObjectOutput out) throws IOException @@ -113,6 +123,36 @@ public class SkinMesh extends ModelMesh _weightGroups = (WeightGroup[])in.readObject(); } + @Override // documentation inherited + public void setReferenceTransforms () + { + updateWorldVectors(); + _modelTransform.invert(_transform); + for (int ii = 0; ii < _weightGroups.length; ii++) { + WeightGroup group = _weightGroups[ii]; + group.invRefTransforms = new Matrix4f[group.bones.length]; + for (int jj = 0; jj < group.bones.length; jj++) { + group.invRefTransforms[jj] = _transform.mult( + group.bones[jj].getModelTransform()).invertLocal(); + } + } + } + + @Override // documentation inherited + public void updateWorldVectors () + { + super.updateWorldVectors(); + if (parent instanceof ModelNode) { + ModelNode.setTransform(getLocalTranslation(), getLocalRotation(), + getLocalScale(), _transform); + ((ModelNode)parent).getModelTransform().mult(_transform, + _modelTransform); + + } else { + _modelTransform.loadIdentity(); + } + } + @Override // documentation inherited public void updateWorldData (float time) { @@ -120,15 +160,72 @@ public class SkinMesh extends ModelMesh if (_weightGroups == null) { return; } + _modelTransform.invert(_transform); // deform the mesh according to the positions of the bones + ModelNode[] bones; + int idx; + int[] indices; + float[] weights; + Matrix4f xform; + Matrix4f[] invRefXforms; + float weight; + FloatBuffer vbuf = getVertexBuffer(), nbuf = getNormalBuffer(); for (int ii = 0; ii < _weightGroups.length; ii++) { - + bones = _weightGroups[ii].bones; + invRefXforms = _weightGroups[ii].invRefTransforms; + if (_transforms == null || _transforms.length < bones.length) { + _transforms = new Matrix4f[bones.length]; + } + for (int jj = 0; jj < bones.length; jj++) { + if (_transforms[jj] == null) { + _transforms[jj] = new Matrix4f(); + } + _transform.mult(bones[jj].getModelTransform(), + _transforms[jj]); + _transforms[jj].multLocal(invRefXforms[jj]); + } + indices = _weightGroups[ii].indices; + weights = _weightGroups[ii].weights; + for (int jj = 0, ww = 0; jj < indices.length; jj++) { + idx = indices[jj]; + BufferUtils.populateFromBuffer(_overtex, _ovbuf, idx); + BufferUtils.populateFromBuffer(_onormal, _onbuf, idx); + _vertex.zero(); + _normal.zero(); + for (int kk = 0; kk < bones.length; kk++) { + xform = _transforms[kk]; + weight = weights[ww++]; + _vertex.addLocal( + xform.mult(_overtex, _tmp).mult(weight)); + _normal.addLocal( + xform.multAcross(_onormal, _tmp).mult(weight)); + } + BufferUtils.setInBuffer(_vertex, vbuf, idx); + BufferUtils.setInBuffer(_normal, nbuf, idx); + } } } /** The groups of vertices influenced by different sets of bones. */ protected WeightGroup[] _weightGroups; + /** The original (undeformed) vertex and normal buffers. */ + protected FloatBuffer _ovbuf, _onbuf; + + /** The node's transform in model space. */ + protected Matrix4f _modelTransform = new Matrix4f(); + + /** A working array for transforms. */ + protected Matrix4f[] _transforms; + + /** Working vectors. */ + protected Vector3f _overtex = new Vector3f(), _vertex = new Vector3f(), + _onormal = new Vector3f(), _normal = new Vector3f(), + _tmp = new Vector3f(); + + /** Working transform. */ + protected Matrix4f _transform = new Matrix4f(); + 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 9e6960320..6e1fdba90 100644 --- a/src/java/com/threerings/jme/tools/AnimationDef.java +++ b/src/java/com/threerings/jme/tools/AnimationDef.java @@ -28,6 +28,7 @@ import java.util.Properties; import com.jme.math.Quaternion; import com.jme.math.Vector3f; +import com.jme.scene.Controller; import com.jme.scene.Spatial; import com.threerings.jme.Log; @@ -38,6 +39,9 @@ import com.threerings.jme.model.Model; */ 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 { @@ -125,7 +129,7 @@ public class AnimationDef * Creates the "live" animation object that will be serialized with the * object. * - * @param props the model properties + * @param props the animation properties * @param nodes the nodes in the model, mapped by name */ public Model.Animation createAnimation ( @@ -137,8 +141,19 @@ public class AnimationDef frames.get(ii).addTransformTargets(nodes, targets); } - // collect all transforms + // create and configure the animation Model.Animation anim = new Model.Animation(); + anim.frameRate = frameRate; + String rtype = props.getProperty("repeat_type", "clamp"); + if (rtype.equals("cycle")) { + anim.repeatType = Controller.RT_CYCLE; + } else if (rtype.equals("wrap")) { + anim.repeatType = Controller.RT_WRAP; + } else { + anim.repeatType = Controller.RT_CLAMP; + } + + // collect all transforms anim.transformTargets = targets.toArray(new Spatial[targets.size()]); anim.transforms = new Model.Transform[frames.size()][targets.size()]; for (int ii = 0; ii < anim.transforms.length; ii++) { diff --git a/src/java/com/threerings/jme/tools/CompileModelTask.java b/src/java/com/threerings/jme/tools/CompileModelTask.java index f10d4a6cb..f2d7a6893 100644 --- a/src/java/com/threerings/jme/tools/CompileModelTask.java +++ b/src/java/com/threerings/jme/tools/CompileModelTask.java @@ -38,9 +38,9 @@ import org.apache.tools.ant.types.FileSet; import com.jme.scene.Spatial; +import com.samskivert.util.PropertiesUtil; import com.samskivert.util.StringUtil; -import com.threerings.jme.model.BoneNode; import com.threerings.jme.model.Model; import com.threerings.jme.model.ModelMesh; import com.threerings.jme.model.ModelNode; @@ -82,12 +82,12 @@ public class CompileModelTask extends Task in.close(); // locate the animations, if any - String[] actions = - StringUtil.parseStringArray(props.getProperty("actions", "")); - File[] afiles = new File[actions.length]; + String[] anims = + StringUtil.parseStringArray(props.getProperty("animations", "")); + File[] afiles = new File[anims.length]; File dir = source.getParentFile(); - for (int ii = 0; ii < actions.length; ii++) { - afiles[ii] = new File(dir, actions[ii] + ".xml"); + for (int ii = 0; ii < anims.length; ii++) { + afiles[ii] = new File(dir, anims[ii] + ".xml"); if (afiles[ii].lastModified() >= target.lastModified()) { needsUpdate = true; } @@ -102,11 +102,12 @@ public class CompileModelTask extends Task HashMap nodes = new HashMap(); Model model = mdef.createModel(props, nodes); - // load the actions, if any - for (int ii = 0; ii < actions.length; ii++) { + // 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(actions[ii], adef.createAnimation(props, nodes)); + model.addAnimation(anims[ii], adef.createAnimation( + PropertiesUtil.getSubProperties(props, anims[ii]), nodes)); } // 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 8499f9b25..bfd6ce504 100644 --- a/src/java/com/threerings/jme/tools/ModelDef.java +++ b/src/java/com/threerings/jme/tools/ModelDef.java @@ -40,7 +40,6 @@ import com.jme.util.geom.BufferUtils; import com.samskivert.util.PropertiesUtil; import com.threerings.jme.Log; -import com.threerings.jme.model.BoneNode; import com.threerings.jme.model.Model; import com.threerings.jme.model.ModelMesh; import com.threerings.jme.model.ModelNode; @@ -178,17 +177,17 @@ public class ModelDef super.resolveReferences(nodes); // divide the vertices up by weight groups - HashMap, WeightGroupDef> groups = - new HashMap, WeightGroupDef>(); + HashMap, WeightGroupDef> groups = + new HashMap, WeightGroupDef>(); for (int ii = 0, nn = vertices.size(); ii < nn; ii++) { SkinVertex svertex = (SkinVertex)vertices.get(ii); - HashSet bones = svertex.getBonesAndNormalize(nodes); + HashSet bones = svertex.getBones(nodes); WeightGroupDef group = groups.get(bones); if (group == null) { groups.put(bones, group = new WeightGroupDef()); } group.indices.add(ii); - for (BoneNode bone : bones) { + for (ModelNode bone : bones) { group.weights.add(svertex.getWeight(bone)); } } @@ -197,12 +196,12 @@ public class ModelDef SkinMesh.WeightGroup[] wgroups = new SkinMesh.WeightGroup[groups.size()]; int ii = 0; - for (Map.Entry, WeightGroupDef> entry : + for (Map.Entry, WeightGroupDef> entry : groups.entrySet()) { SkinMesh.WeightGroup wgroup = new SkinMesh.WeightGroup(); wgroup.indices = toArray(entry.getValue().indices); - HashSet bones = entry.getKey(); - wgroup.bones = bones.toArray(new BoneNode[bones.size()]); + HashSet bones = entry.getKey(); + wgroup.bones = bones.toArray(new ModelNode[bones.size()]); wgroup.weights = toArray(entry.getValue().weights); wgroups[ii++] = wgroup; } @@ -220,16 +219,6 @@ public class ModelDef } } - /** A bone that influences skinned meshes. */ - public static class BoneNodeDef extends NodeDef - { - // documentation inherited - public Spatial createSpatial (Properties props) - { - return new BoneNode(name); - } - } - /** A basic vertex. */ public static class Vertex { @@ -279,30 +268,21 @@ public class ModelDef } } - /** Finds the bone nodes influencing this vertex and normalizes the - * weights. */ - public HashSet getBonesAndNormalize ( - HashMap nodes) + /** Finds the bone nodes influencing this vertex. */ + public HashSet getBones (HashMap nodes) { - HashSet bones = new HashSet(); - float totalWeight = 0f; + HashSet bones = new HashSet(); for (BoneWeight bweight : boneWeights) { Spatial node = nodes.get(bweight.bone); - if (node instanceof BoneNode) { - bones.add((BoneNode)node); - totalWeight += bweight.weight; - } - } - if (totalWeight > 0f && totalWeight < 1f) { - for (BoneWeight bweight : boneWeights) { - bweight.weight /= totalWeight; + if (node instanceof ModelNode) { + bones.add((ModelNode)node); } } return bones; } /** Returns the weight of the given bone. */ - public float getWeight (BoneNode bone) + public float getWeight (ModelNode bone) { String name = bone.getName(); for (BoneWeight bweight : boneWeights) { @@ -339,7 +319,9 @@ public class ModelDef public void addSpatial (SpatialDef spatial) { - spatials.add(spatial); + // put nodes before meshes so that bones are updated before skin + spatials.add(spatial instanceof NodeDef ? 0 : spatials.size(), + spatial); } /** @@ -352,6 +334,9 @@ public class ModelDef { Model model = new Model(props.getProperty("name", "model"), props); + // set the overall scale + model.setLocalScale(Float.parseFloat(props.getProperty("scale", "1"))); + // start by creating the spatials and mapping them to their names for (int ii = 0, nn = spatials.size(); ii < nn; ii++) { Spatial spatial = spatials.get(ii).getSpatial(props); diff --git a/src/java/com/threerings/jme/tools/ModelViewer.java b/src/java/com/threerings/jme/tools/ModelViewer.java index 07a8c5749..7838c4bff 100644 --- a/src/java/com/threerings/jme/tools/ModelViewer.java +++ b/src/java/com/threerings/jme/tools/ModelViewer.java @@ -25,6 +25,7 @@ import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.Point; import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; @@ -42,7 +43,10 @@ import java.util.logging.Level; import javax.swing.AbstractAction; import javax.swing.Action; +import javax.swing.BorderFactory; +import javax.swing.DefaultComboBoxModel; import javax.swing.JButton; +import javax.swing.JComboBox; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JLabel; @@ -51,7 +55,10 @@ import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; +import javax.swing.JSlider; import javax.swing.KeyStroke; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; import javax.swing.filechooser.FileFilter; import com.jme.image.Texture; @@ -67,6 +74,7 @@ import com.jme.util.LoggingSystem; import com.jme.util.TextureManager; import com.samskivert.swing.GroupLayout; +import com.samskivert.swing.Spacer; import com.samskivert.util.Config; import com.threerings.util.MessageBundle; @@ -134,24 +142,44 @@ public class ModelViewer extends JmeCanvasApp KeyStroke.getKeyStroke(KeyEvent.VK_Q, KeyEvent.CTRL_MASK)); file.add(quit); - _amenu = new JMenu(_msg.get("m.anim_menu")); - _amenu.setMnemonic(KeyEvent.VK_A); - menu.add(_amenu); - _amenu.setVisible(false); - _stop = new AbstractAction(_msg.get("m.anim_stop")) { - public void actionPerformed (ActionEvent e) { - _model.stopAnimation(); - } - }; - _stop.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_S); - _stop.putValue(Action.ACCELERATOR_KEY, - KeyStroke.getKeyStroke(KeyEvent.VK_S, KeyEvent.CTRL_MASK)); - _frame.getContentPane().add(getCanvas(), BorderLayout.CENTER); + JPanel bpanel = new JPanel(new BorderLayout()); + _frame.getContentPane().add(bpanel, BorderLayout.SOUTH); + + _animctrls = new JPanel(); + _animctrls.setBorder(BorderFactory.createEtchedBorder()); + bpanel.add(_animctrls, BorderLayout.NORTH); + _animctrls.add(new JLabel(_msg.get("m.anim_select"))); + _animctrls.add(_animbox = new JComboBox()); + _animctrls.add(new JButton( + new AbstractAction(_msg.get("m.anim_start")) { + public void actionPerformed (ActionEvent e) { + _model.startAnimation((String)_animbox.getSelectedItem()); + _animstop.setEnabled(true); + } + })); + _animctrls.add(_animstop = new JButton( + new AbstractAction(_msg.get("m.anim_stop")) { + public void actionPerformed (ActionEvent e) { + _model.stopAnimation(); + } + })); + _animstop.setEnabled(false); + _animctrls.add(new Spacer(50, 1)); + _animctrls.add(new JLabel(_msg.get("m.anim_speed"))); + _animctrls.add(_animspeed = new JSlider(-100, +100, 0)); + _animspeed.addChangeListener(new ChangeListener() { + public void stateChanged (ChangeEvent e) { + updateAnimationSpeed(); + } + }); + _animctrls.setVisible(false); + _status = new JLabel(" "); _status.setHorizontalAlignment(JLabel.LEFT); - _frame.getContentPane().add(_status, BorderLayout.SOUTH); + _status.setBorder(BorderFactory.createEtchedBorder()); + bpanel.add(_status, BorderLayout.SOUTH); _frame.pack(); _frame.setVisible(true); @@ -300,6 +328,7 @@ public class ModelViewer extends JmeCanvasApp _status.setText(_msg.get("m.compiling_model", file)); Model model = CompileModelTask.compileModel(file); if (model != null) { + model.setReferenceTransforms(); setModel(model, file); return; } @@ -331,7 +360,6 @@ public class ModelViewer extends JmeCanvasApp _ctx.getGeometry().detachChild(_model); } _ctx.getGeometry().attachChild(_model = model); - _model.setLocalScale(0.04f); // resolve the textures from the file's directory final File dir = file.getParentFile(); @@ -358,24 +386,26 @@ public class ModelViewer extends JmeCanvasApp }); _model.updateRenderState(); - // create buttons for the model's animations + // configure the animation panel String[] anims = _model.getAnimations(); - _amenu.removeAll(); if (anims.length == 0) { - _amenu.setVisible(false); + _animctrls.setVisible(false); return; } - _amenu.setVisible(true); - for (int ii = 0; ii < anims.length; ii++) { - final String anim = anims[ii]; - _amenu.add(new AbstractAction(anim) { - public void actionPerformed (ActionEvent e) { - _model.startAnimation(anim); - } - }); - } - _amenu.addSeparator(); - _amenu.add(_stop); + _model.addAnimationObserver(_animobs); + _animctrls.setVisible(true); + _animbox.setModel(new DefaultComboBoxModel(anims)); + updateAnimationSpeed(); + } + + /** + * Updates the model's animation speed based on the position of the + * animation speed slider. + */ + protected void updateAnimationSpeed () + { + _model.setAnimationSpeed( + FastMath.pow(2f, _animspeed.getValue() / 50f)); } /** The translation bundle. */ @@ -387,11 +417,17 @@ public class ModelViewer extends JmeCanvasApp /** The viewer frame. */ protected JFrame _frame; - /** The animation menu. */ - protected JMenu _amenu; + /** The animation controls. */ + protected JPanel _animctrls; - /** The stop animation action. */ - protected Action _stop; + /** The animation selector. */ + protected JComboBox _animbox; + + /** The "stop animation" button. */ + protected JButton _animstop; + + /** The animation speed slider. */ + protected JSlider _animspeed; /** The status bar. */ protected JLabel _status; @@ -402,6 +438,19 @@ public class ModelViewer extends JmeCanvasApp /** The currently loaded model. */ protected Model _model; + /** Disables the stop button when animations stop. */ + protected Model.AnimationObserver _animobs = + new Model.AnimationObserver() { + public boolean animationCompleted (Model model, String name) { + _animstop.setEnabled(false); + return true; + } + public boolean animationCancelled (Model model, String name) { + _animstop.setEnabled(false); + return true; + } + }; + /** Moves the camera using mouse input. */ protected class MouseOrbiter extends MouseAdapter implements MouseMotionListener, MouseWheelListener diff --git a/src/java/com/threerings/jme/tools/xml/AnimationParser.java b/src/java/com/threerings/jme/tools/xml/AnimationParser.java index 4d2f10d7a..5da48a964 100644 --- a/src/java/com/threerings/jme/tools/xml/AnimationParser.java +++ b/src/java/com/threerings/jme/tools/xml/AnimationParser.java @@ -44,6 +44,7 @@ public class AnimationParser // add the rules String anim = "animation"; _digester.addObjectCreate(anim, AnimationDef.class.getName()); + _digester.addRule(anim, new SetPropertyFieldsRule()); _digester.addSetNext(anim, "setAnimation", AnimationDef.class.getName()); diff --git a/src/java/com/threerings/jme/tools/xml/ModelParser.java b/src/java/com/threerings/jme/tools/xml/ModelParser.java index 460e592b1..e55469fa3 100644 --- a/src/java/com/threerings/jme/tools/xml/ModelParser.java +++ b/src/java/com/threerings/jme/tools/xml/ModelParser.java @@ -65,12 +65,6 @@ public class ModelParser _digester.addSetNext(node, "addSpatial", ModelDef.SpatialDef.class.getName()); - String bnode = model + "/boneNode"; - _digester.addObjectCreate(bnode, ModelDef.BoneNodeDef.class.getName()); - _digester.addRule(bnode, new SetPropertyFieldsRule()); - _digester.addSetNext(bnode, "addSpatial", - ModelDef.SpatialDef.class.getName()); - String vertex = tmesh + "/vertex", svertex = smesh + "/vertex"; _digester.addObjectCreate(vertex, ModelDef.Vertex.class.getName()); _digester.addObjectCreate(svertex, diff --git a/src/ms/TRAnimationExporter.mcr b/src/ms/TRAnimationExporter.mcr index 59ba38dc5..e4f939d9f 100644 --- a/src/ms/TRAnimationExporter.mcr +++ b/src/ms/TRAnimationExporter.mcr @@ -37,18 +37,20 @@ macroScript TRAnimationExporter category:"File" \ -- Writes a single node transform fn writeTransform node outFile = ( format " \n" to:outFile ) -- Writes a single animation frame fn writeFrame nodes outFile = ( format " \n" to:outFile - for node in nodes do in coordsys parent ( - writeTransform node outFile - ) + writeTransform node outFile format " \n\n" to:outFile ) @@ -57,7 +59,7 @@ macroScript TRAnimationExporter category:"File" \ ( outFile = createfile fileName format "\n\n" to:outFile - format "\n\n" to:outFile + format "\n\n" frameRate to:outFile local nodes if selection.count > 0 then ( nodes = selection diff --git a/src/ms/TRModelExporter.mcr b/src/ms/TRModelExporter.mcr index dc4abb0f9..35cc5650c 100644 --- a/src/ms/TRModelExporter.mcr +++ b/src/ms/TRModelExporter.mcr @@ -103,22 +103,20 @@ macroScript TRModelExporter category:"File" \ ) else ( kind = "triMesh" ) - ) else if node.boneEnable then ( - kind = "boneNode" ) else ( kind = "node" ) format " <% name=\"%\"" kind node.name to:outFile + xform = node.transform if node.parent != undefined do ( + xform = xform * (inverse node.parent.transform) format " parent=\"%\"" node.parent.name to:outFile ) - in coordsys parent ( - writePoint3Attr " translation" node.transform.translationPart \ - outFile - writeQuatAttr " rotation" (inverse node.transform.rotationPart) \ - outFile - writePoint3Attr " scale" node.transform.scalePart outFile - ) + writePoint3Attr " translation" xform.translationPart \ + outFile + writeQuatAttr " rotation" (inverse xform.rotationPart) \ + outFile + writePoint3Attr " scale" xform.scalePart outFile if isMesh then ( format ">\n" to:outFile if isProperty node #skin do ( @@ -140,6 +138,7 @@ macroScript TRModelExporter category:"File" \ format "\n\n" to:outFile format "\n\n" to:outFile local nodes + oldsel = selection as array if selection.count > 0 then ( nodes = selection ) else ( @@ -148,6 +147,7 @@ macroScript TRModelExporter category:"File" \ for node in nodes do ( writeNode node outFile ) + select oldsel format "\n" to:outFile close outFile )