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
)