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(); } @Override // documentation inherited @@ -278,10 +278,37 @@ public class SkinMesh extends ModelMesh } @Override // documentation inherited - public void setAnimationFrame (int frameId) + public void storeMeshFrame (int frameId, boolean blend) { - // switch to the specified frame id on next update - _updateFrameId = frameId; + _storeFrameId = frameId; + _storeBlend = blend; + } + + @Override // documentation inherited + public void setMeshFrame (int frameId) + { + TriangleBatch batch = getBatch(0), + tbatch = (TriangleBatch)_frames.get(frameId); + if (batch instanceof SharedBatch) { + ((SharedBatch)batch).setTarget(tbatch); + } else { + clearBatches(); + addBatch(new SharedBatch(tbatch)); + getBatch(0).updateRenderState(); + } + } + + @Override // documentation inherited + public void blendMeshFrames (int frameId1, int frameId2, float alpha) + { + BlendFrame frame1 = (BlendFrame)_frames.get(frameId1), + frame2 = (BlendFrame)_frames.get(frameId2); + frame1.blend(frame2, alpha, _vbuf, _nbuf); + FloatBuffer vbuf = getVertexBuffer(0), nbuf = getNormalBuffer(0); + vbuf.rewind(); + vbuf.put(_vbuf); + nbuf.rewind(); + nbuf.put(_nbuf); } @Override // documentation inherited @@ -303,54 +330,9 @@ public class SkinMesh extends ModelMesh public void updateWorldData (float time) { super.updateWorldData(time); - if (_weightGroups == null) { + if (_weightGroups == null || _storeFrameId == -1) { return; } - // determine if we are using/updating a quantized frame - TriangleBatch lockBatch = null; - if (_updateFrameId > 0) { - if (_currentFrameId == _updateFrameId) { - return; - } - TriangleBatch batch = getBatch(0), - tbatch = _frames.get(_updateFrameId); - boolean reskin = false; - if (tbatch == null) { - _frames.put(_updateFrameId, tbatch = new TriangleBatch()); - tbatch.setParentGeom(DUMMY_MESH); - tbatch.setColorBuffer(batch.getColorBuffer()); - tbatch.setTextureBuffer(batch.getTextureBuffer(0), 0); - tbatch.setIndexBuffer(batch.getIndexBuffer()); - tbatch.setVertexBuffer( - BufferUtils.createFloatBuffer(_ovbuf.length)); - tbatch.setNormalBuffer( - BufferUtils.createFloatBuffer(_onbuf.length)); - VBOInfo ovboinfo = batch.getVBOInfo(); - if (ovboinfo != null) { - VBOInfo vboinfo = new VBOInfo(true); - vboinfo.setVBOIndexEnabled(true); - vboinfo.setVBOColorID(ovboinfo.getVBOColorID()); - vboinfo.setVBOTextureID(0, ovboinfo.getVBOTextureID(0)); - vboinfo.setVBOIndexID(ovboinfo.getVBOIndexID()); - tbatch.setVBOInfo(vboinfo); - } else if (_useDisplayLists) { - lockBatch = tbatch; - } - reskin = true; - } - if (batch instanceof SharedBatch) { - ((SharedBatch)batch).setTarget(tbatch); - } else { - clearBatches(); - addBatch(new SharedBatch(tbatch)); - getBatch(0).updateRenderState(); - } - _currentFrameId = _updateFrameId; - if (!reskin) { - return; - } - } - // update the bone transforms _modelTransform.invert(_transform); for (Bone bone : _bones) { @@ -399,17 +381,49 @@ public class SkinMesh extends ModelMesh } } - // copy it from array to buffer - FloatBuffer vbuf = getVertexBuffer(0), nbuf = getNormalBuffer(0); - vbuf.rewind(); - vbuf.put(_vbuf); - nbuf.rewind(); - nbuf.put(_nbuf); - - // compile the target batch to a display list if requested - if (lockBatch != null) { - lockBatch.lockMeshes( - DisplaySystem.getDisplaySystem().getRenderer()); + // if skinning in real time, copy the data from arrays to buffers; + // otherwise, store the mesh as an animation frame + if (_storeFrameId == 0) { + FloatBuffer vbuf = getVertexBuffer(0), nbuf = getNormalBuffer(0); + vbuf.rewind(); + vbuf.put(_vbuf); + nbuf.rewind(); + nbuf.put(_nbuf); + } else { + storeFrame(); + _storeFrameId = -1; + } + } + + /** + * Stores the current frame data for later use. + */ + protected void storeFrame () + { + if (_storeBlend) { + _frames.put(_storeFrameId, new BlendFrame( + (float[])_vbuf.clone(), (float[])_nbuf.clone())); + } else { + TriangleBatch batch = getBatch(0), tbatch = new TriangleBatch(); + tbatch.setParentGeom(DUMMY_MESH); + tbatch.setColorBuffer(batch.getColorBuffer()); + tbatch.setTextureBuffer(batch.getTextureBuffer(0), 0); + tbatch.setIndexBuffer(batch.getIndexBuffer()); + tbatch.setVertexBuffer(BufferUtils.createFloatBuffer(_vbuf)); + tbatch.setNormalBuffer(BufferUtils.createFloatBuffer(_nbuf)); + VBOInfo ovboinfo = batch.getVBOInfo(); + if (ovboinfo != null) { + VBOInfo vboinfo = new VBOInfo(true); + vboinfo.setVBOIndexEnabled(true); + vboinfo.setVBOColorID(ovboinfo.getVBOColorID()); + vboinfo.setVBOTextureID(0, ovboinfo.getVBOTextureID(0)); + vboinfo.setVBOIndexID(ovboinfo.getVBOIndexID()); + tbatch.setVBOInfo(vboinfo); + } else if (_useDisplayLists) { + tbatch.lockMeshes( + DisplaySystem.getDisplaySystem().getRenderer()); + } + _frames.put(_storeFrameId, tbatch); } } @@ -427,9 +441,34 @@ public class SkinMesh extends ModelMesh _nbuf = new float[_onbuf.length]; } - /** Pre-skinned meshes shared between all instances corresponding to - * frame ids from {@link #setAnimationFrame}. */ - protected HashIntMap _frames; + /** 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) + { + float[] nvbuf = next.vbuf, nnbuf = next.nbuf; + float ialpha = 1f - alpha; + for (int ii = 0, nn = vbuf.length; ii < nn; ii++) { + rvbuf[ii] = vbuf[ii] * ialpha + nvbuf[ii] * alpha; + 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. */ @@ -445,8 +484,13 @@ public class SkinMesh extends ModelMesh * versions. */ protected float[] _ovbuf, _onbuf, _vbuf, _nbuf; - /** The desired and current frame id, or -1 for continuous animation. */ - protected int _updateFrameId = -1, _currentFrameId; + /** The frame id to store on the next update. If 0, don't store any frame + * 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; /** The node's transform in model space. */ protected Matrix4f _modelTransform = new Matrix4f(); diff --git a/src/java/com/threerings/jme/tools/ModelViewer.java b/src/java/com/threerings/jme/tools/ModelViewer.java index f3d24de38..ca77e7c36 100644 --- a/src/java/com/threerings/jme/tools/ModelViewer.java +++ b/src/java/com/threerings/jme/tools/ModelViewer.java @@ -46,6 +46,8 @@ import java.util.logging.Level; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.BorderFactory; +import javax.swing.ButtonGroup; +import javax.swing.ButtonModel; import javax.swing.DefaultComboBoxModel; import javax.swing.JButton; import javax.swing.JCheckBoxMenuItem; @@ -59,6 +61,7 @@ import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; +import javax.swing.JRadioButtonMenuItem; import javax.swing.JSlider; import javax.swing.KeyStroke; import javax.swing.event.ChangeEvent; @@ -205,6 +208,38 @@ public class ModelViewer extends JmeCanvasApp view.add(_normals); view.addSeparator(); + JMenu amode = new JMenu(_msg.get("m.animation_mode")); + final JRadioButtonMenuItem flipbook = + new JRadioButtonMenuItem(_msg.get("m.mode_flipbook")), + morph = new JRadioButtonMenuItem(_msg.get("m.mode_morph")), + skin = new JRadioButtonMenuItem(_msg.get("m.mode_skin")); + ButtonGroup mgroup = new ButtonGroup() { + public void setSelected (ButtonModel model, boolean b) { + super.setSelected(model, b); + if (b) { + if (flipbook.isSelected()) { + _animMode = Model.AnimationMode.FLIPBOOK; + } else if (morph.isSelected()) { + _animMode = Model.AnimationMode.MORPH; + } else { + _animMode = Model.AnimationMode.SKIN; + } + if (_loaded != null) { + loadModel(_loaded); // reload + } + } + } + }; + mgroup.add(flipbook); + mgroup.add(morph); + mgroup.add(skin); + mgroup.setSelected(skin.getModel(), true); + amode.add(skin); + amode.add(morph); + amode.add(flipbook); + view.add(amode); + view.addSeparator(); + Action rlight = new AbstractAction(_msg.get("m.view_light")) { public void actionPerformed (ActionEvent e) { if (_rldialog == null) { @@ -447,6 +482,7 @@ public class ModelViewer extends JmeCanvasApp throw new Exception(_msg.get("m.invalid_type")); } _status.setText(_msg.get("m.loaded_model", fpath)); + _loaded = file; } catch (Exception e) { e.printStackTrace(); @@ -494,6 +530,7 @@ public class ModelViewer extends JmeCanvasApp _ctx.getGeometry().detachChild(_model); } _ctx.getGeometry().attachChild(_model = model); + _model.setAnimationMode(_animMode); _model.lockStaticMeshes(_ctx.getRenderer(), true, true); // resolve the textures from the file's directory @@ -558,6 +595,9 @@ public class ModelViewer extends JmeCanvasApp /** The path of the initial model to load. */ protected String _path; + /** The last model successfully loaded. */ + protected File _loaded; + /** The viewer frame. */ protected JFrame _frame; @@ -582,6 +622,9 @@ public class ModelViewer extends JmeCanvasApp /** The model file chooser. */ protected JFileChooser _chooser; + /** The desired animation mode. */ + protected Model.AnimationMode _animMode; + /** The light rotation dialog. */ protected RotateLightDialog _rldialog;