diff --git a/rsrc/i18n/jme/viewer.properties b/rsrc/i18n/jme/viewer.properties
index 046285628..6efc1f945 100644
--- a/rsrc/i18n/jme/viewer.properties
+++ b/rsrc/i18n/jme/viewer.properties
@@ -16,6 +16,11 @@ m.view_bounds = Show Bounds
m.view_normals = Show Normals
m.view_light = Rotate Light...
+m.animation_mode = Animation Mode
+m.mode_flipbook = Flipbook
+m.mode_morph = Morph
+m.mode_skin = Skin
+
m.load_title = Select Model to Load
m.load_filter = Model Files (*.properties, *.dat)
diff --git a/src/java/com/threerings/jme/model/Model.java b/src/java/com/threerings/jme/model/Model.java
index 3923e3f93..5ec28bab6 100644
--- a/src/java/com/threerings/jme/model/Model.java
+++ b/src/java/com/threerings/jme/model/Model.java
@@ -63,6 +63,11 @@ import com.threerings.jme.Log;
*/
public class Model extends ModelNode
{
+ /** 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
@@ -109,6 +114,9 @@ public class Model extends ModelNode
/** 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.
*/
@@ -134,9 +142,32 @@ public class Model extends ModelNode
(Spatial)pnodes.get(transformTargets[ii]);
}
anim.animId = animId;
+ anim.stored = stored;
return anim;
}
+ /**
+ * Applies the transforms for a frame of this animation.
+ */
+ public void applyFrame (int fidx)
+ {
+ Transform[] xforms = transforms[fidx];
+ for (int ii = 0; ii < transformTargets.length; ii++) {
+ xforms[ii].apply(transformTargets[ii]);
+ }
+ }
+
+ /**
+ * Blends the transforms between two frames of this animation.
+ */
+ public void blendFrames (int fidx, int nidx, float alpha)
+ {
+ Transform[] xforms = transforms[fidx], nxforms = transforms[nidx];
+ for (int ii = 0; ii < transformTargets.length; ii++) {
+ xforms[ii].blend(nxforms[ii], alpha, transformTargets[ii]);
+ }
+ }
+
private void writeObject (ObjectOutputStream out)
throws IOException
{
@@ -357,11 +388,12 @@ public class Model extends ModelNode
*/
public void initPrototype ()
{
- // assign identifiers to the animations
+ // initialize shared transient animation state
if (_anims != null) {
int nextId = 1;
for (Animation anim : _anims.values()) {
anim.animId = nextId++;
+ anim.stored = new boolean[anim.transforms.length];
}
}
setReferenceTransforms();
@@ -408,25 +440,21 @@ public class Model extends ModelNode
}
/**
- * Sets the resolution at which to quantize animations over time.
- * This should be set on the prototype before any animations are
- * started or any instances are created.
- *
- * @param the temporal quantization rate in frames per second, or
- * 0 to disable quantization
+ * Sets the animation mode to use for this model. This should be set
+ * on the prototype before any animations are started or any instances
+ * are created.
*/
- public void setAnimationResolution (float resolution)
+ public void setAnimationMode (AnimationMode mode)
{
- _animResolution = resolution;
+ _animMode = mode;
}
/**
- * Returns the resolution at which animations are quantized over time,
- * or 0 if quantization is disabled.
+ * Returns the animation mode configured for this model.
*/
- public float getAnimationResolution ()
+ public AnimationMode getAnimationMode ()
{
- return _animResolution;
+ return _animMode;
}
/**
@@ -687,7 +715,7 @@ public class Model extends ModelNode
mstore._anims = new HashMap();
}
mstore._pnodes = (HashMap)properties.originalToCopy.clone();
- mstore._animResolution = _animResolution;
+ mstore._animMode = _animMode;
return mstore;
}
@@ -765,9 +793,9 @@ public class Model extends ModelNode
*/
protected void updateAnimation (float time)
{
- float res = (_animResolution == 0f) ?
- 0f : (_anim.frameRate / _animResolution);
- if (_elapsed < res && _elapsed > 0f) {
+ // no need to update between frames for flipbook animation
+ if (_animMode == AnimationMode.FLIPBOOK && _elapsed > 0f &&
+ _elapsed < 1f) {
_elapsed += (time * _anim.frameRate);
return;
}
@@ -777,23 +805,40 @@ public class Model extends ModelNode
advanceFrameCounter();
_elapsed -= 1f;
}
- float qelapsed = (res == 0f) ?
- _elapsed : (res * (int)(_elapsed / res));
// update the target transforms and animation frame if not outside the
// view frustum
if (!_outside) {
- Spatial[] targets = _anim.transformTargets;
- Transform[] xforms = _anim.transforms[_fidx],
- nxforms = _anim.transforms[_nidx];
- for (int ii = 0; ii < targets.length; ii++) {
- xforms[ii].blend(nxforms[ii], qelapsed, targets[ii]);
- }
-
- if (res != 0f) {
- int frameId = (_anim.animId << 16) |
- (int)((_fidx + _elapsed)/res);
- setAnimationFrame(frameId);
+ if (_animMode == AnimationMode.FLIPBOOK) {
+ int frameId = (_anim.animId << 16) | _fidx;
+ _anim.applyFrame(_fidx);
+ if (!_anim.stored[_fidx]) {
+ storeMeshFrame(frameId, false);
+ updateWorldData(0f);
+ _anim.stored[_fidx] = true;
+ }
+ setMeshFrame(frameId);
+
+ } else if (_animMode == AnimationMode.MORPH) {
+ int frameId1 = (_anim.animId << 16) | _fidx,
+ frameId2 = (_anim.animId << 16) | _nidx;
+ if (!_anim.stored[_fidx]) {
+ storeMeshFrame(frameId1, true);
+ _anim.applyFrame(_fidx);
+ updateWorldData(0f);
+ _anim.stored[_fidx] = true;
+ }
+ if (!_anim.stored[_nidx]) {
+ storeMeshFrame(frameId2, true);
+ _anim.applyFrame(_nidx);
+ updateWorldData(0f);
+ _anim.stored[_nidx] = true;
+ }
+ _anim.blendFrames(_fidx, _nidx, _elapsed);
+ blendMeshFrames(frameId1, frameId2, _elapsed);
+
+ } else { // _animMode == AnimationMode.SKIN
+ _anim.blendFrames(_fidx, _nidx, _elapsed);
}
}
@@ -872,8 +917,8 @@ public class Model extends ModelNode
* instances. */
protected CloneCreator _ccreator;
- /** The resolution of the model's animations in frames per second. */
- protected float _animResolution;
+ /** The animation mode to use for this model. */
+ protected AnimationMode _animMode;
/** For instances, maps prototype nodes to their corresponding instance
* nodes. */
diff --git a/src/java/com/threerings/jme/model/ModelMesh.java b/src/java/com/threerings/jme/model/ModelMesh.java
index cc8f5a21f..fdb936a60 100644
--- a/src/java/com/threerings/jme/model/ModelMesh.java
+++ b/src/java/com/threerings/jme/model/ModelMesh.java
@@ -343,7 +343,19 @@ public class ModelMesh extends TriMesh
}
// documentation inherited from interface ModelSpatial
- public void setAnimationFrame (int frameId)
+ public void storeMeshFrame (int frameId, boolean blend)
+ {
+ // no-op
+ }
+
+ // documentation inherited from interface ModelSpatial
+ public void setMeshFrame (int frameId)
+ {
+ // no-op
+ }
+
+ // documentation inherited from interface ModelSpatial
+ public void blendMeshFrames (int frameId1, int frameId2, float alpha)
{
// no-op
}
diff --git a/src/java/com/threerings/jme/model/ModelNode.java b/src/java/com/threerings/jme/model/ModelNode.java
index 73209a58a..9af6b79e0 100644
--- a/src/java/com/threerings/jme/model/ModelNode.java
+++ b/src/java/com/threerings/jme/model/ModelNode.java
@@ -249,12 +249,35 @@ public class ModelNode extends Node
}
// documentation inherited from interface ModelSpatial
- public void setAnimationFrame (int frameId)
+ public void storeMeshFrame (int frameId, boolean blend)
{
for (int ii = 0, nn = getQuantity(); ii < nn; ii++) {
Spatial child = getChild(ii);
if (child instanceof ModelSpatial) {
- ((ModelSpatial)child).setAnimationFrame(frameId);
+ ((ModelSpatial)child).storeMeshFrame(frameId, blend);
+ }
+ }
+ }
+
+ // documentation inherited from interface ModelSpatial
+ public void setMeshFrame (int frameId)
+ {
+ for (int ii = 0, nn = getQuantity(); ii < nn; ii++) {
+ Spatial child = getChild(ii);
+ if (child instanceof ModelSpatial) {
+ ((ModelSpatial)child).setMeshFrame(frameId);
+ }
+ }
+ }
+
+ // documentation inherited from interface ModelSpatial
+ public void blendMeshFrames (int frameId1, int frameId2, float alpha)
+ {
+ for (int ii = 0, nn = getQuantity(); ii < nn; ii++) {
+ Spatial child = getChild(ii);
+ if (child instanceof ModelSpatial) {
+ ((ModelSpatial)child).blendMeshFrames(
+ frameId1, frameId2, alpha);
}
}
}
diff --git a/src/java/com/threerings/jme/model/ModelSpatial.java b/src/java/com/threerings/jme/model/ModelSpatial.java
index c4ce1203b..bb85ce6d7 100644
--- a/src/java/com/threerings/jme/model/ModelSpatial.java
+++ b/src/java/com/threerings/jme/model/ModelSpatial.java
@@ -64,15 +64,28 @@ public interface ModelSpatial
public void resolveTextures (TextureProvider tprov);
/**
- * Recursively sets the quantized animation frame. For skinned meshes,
- * the first time the frame is set causes the current skin to be stored
- * in a table shared by all instances. At subsequent times, the stored
- * skin is displayed.
+ * Recursively requests that the current state of all skinned meshes be
+ * stored as an animation frame on the next update.
*
* @param frameId the frame id, which uniquely identifies one frame of
* one animation
+ * @param blend whether or not the stored frames will be retrieved by
+ * calls to {@link #blendAnimationFrames} as opposed to
+ * {@link #setAnimationFrames}
*/
- public void setAnimationFrame (int frameId);
+ public void storeMeshFrame (int frameId, boolean blend);
+
+ /**
+ * Recursively switches all skinned meshes to a stored animation frame for
+ * the next update.
+ */
+ public void setMeshFrame (int frameId);
+
+ /**
+ * Recursively blends all skinned meshes between two stored animation
+ * frames for the next update.
+ */
+ public void blendMeshFrames (int frameId1, int frameId2, float alpha);
/**
* Creates or populates and returns a clone of this object using the given
diff --git a/src/java/com/threerings/jme/model/SkinMesh.java b/src/java/com/threerings/jme/model/SkinMesh.java
index dc09bada7..f7756a563 100644
--- a/src/java/com/threerings/jme/model/SkinMesh.java
+++ b/src/java/com/threerings/jme/model/SkinMesh.java
@@ -179,7 +179,7 @@ public class SkinMesh extends ModelMesh
storeOriginalBuffers();
// initialize the quantized frame table
- _frames = new HashIntMap();
+ _frames = new HashIntMap