diff --git a/src/java/com/threerings/jme/model/Model.java b/src/java/com/threerings/jme/model/Model.java index 4ad7fb71b..b71b1d2ec 100644 --- a/src/java/com/threerings/jme/model/Model.java +++ b/src/java/com/threerings/jme/model/Model.java @@ -36,6 +36,7 @@ import java.nio.ByteOrder; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; +import java.util.ArrayList; import java.util.HashMap; import java.util.Properties; @@ -45,6 +46,7 @@ import com.jme.math.Quaternion; import com.jme.math.Vector3f; import com.jme.renderer.CloneCreator; import com.jme.scene.Controller; +import com.jme.scene.Node; import com.jme.scene.Spatial; import com.threerings.jme.Log; @@ -58,6 +60,13 @@ public class Model extends ModelNode * for non-repeating animations) or cancelled. */ public interface AnimationObserver { + /** + * Called when an animation has started. + * + * @return true to remain on the observer list, false to remove self + */ + public boolean animationStarted (Model model, String anim); + /** * Called when a non-repeating animation has finished. * @@ -90,6 +99,14 @@ public class Model extends ModelNode /** The animation transforms (one transform per target per frame). */ public transient Transform[][] transforms; + /** + * Returns this animation's duration in seconds. + */ + public float getDuration () + { + return (float)transforms.length / frameRate; + } + /** * Rebinds this animation for a prototype instance. * @@ -223,8 +240,8 @@ public class Model extends ModelNode } ois.close(); - // set the reference transforms before any animations are applied - model.setReferenceTransforms(); + // initialize the model as a prototype + model.initPrototype(); return model; } @@ -253,6 +270,16 @@ public class Model extends ModelNode return _props; } + /** + * Initializes this model as prototype. Only necessary when the prototype + * was not loaded through {@link #readFromFile}. + */ + public void initPrototype () + { + setReferenceTransforms(); + initInstance(); + } + /** * Adds an animation to the model's library. This should only be called by * the model compiler. @@ -268,38 +295,65 @@ public class Model extends ModelNode /** * Returns the names of the model's animations. */ - public String[] getAnimations () + public String[] getAnimationNames () { if (_prototype != null) { - return _prototype.getAnimations(); + return _prototype.getAnimationNames(); } return (_anims == null) ? new String[0] : _anims.keySet().toArray(new String[_anims.size()]); } /** - * Starts the named animation. + * Checks whether the unit has an animation with the given name. */ - public void startAnimation (String name) + public boolean hasAnimation (String name) { - if (_anim != null) { - stopAnimation(); + if (_prototype != null) { + return _prototype.hasAnimation(name); } + return (_anims == null) ? false : _anims.containsKey(name); + } + + /** + * Starts the named animation. + * + * @return a reference to the started animation + */ + public Animation startAnimation (String name) + { + Animation anim = getAnimation(name); + if (anim == null) { + return null; + } + _anim = anim; + _animName = name; + _fidx = 0; + _nidx = 1; + _fdir = +1; + _elapsed = 0f; + _animObservers.apply(new AnimStartedOp(_animName)); + return anim; + } + + /** + * Gets a reference to the animation with the given name. + */ + public Animation getAnimation (String name) + { Animation anim = _anims.get(name); if (anim != null) { - startAnimation(name, anim); - return; + return anim; } if (_prototype != null) { Animation panim = _prototype._anims.get(name); if (panim != null) { _anims.put(name, anim = panim.rebind(_pnodes)); - startAnimation(name, anim); - return; + return anim; } } - Log.warning("Requested unknown animation [name=" + - name + "]."); + Log.warning("Requested unknown animation [name=" + name + "]."); + return null; } /** @@ -347,6 +401,20 @@ public class Model extends ModelNode _animObservers.remove(obs); } + /** + * Returns a reference to the node that contains this model's emissions + * (in world space, so the emissions do not move with the model). This + * node is created and added when this method is first called. + */ + public Node getEmissionNode () + { + if (_emissionNode == null) { + attachChild(_emissionNode = new Node("emissions")); + _emissionNode.setTransformable(false); + } + return _emissionNode; + } + /** * Writes this model out to a file. */ @@ -371,6 +439,7 @@ public class Model extends ModelNode super.writeExternal(out); out.writeObject(_props); out.writeObject(_anims); + out.writeObject(getControllers()); } @Override // documentation inherited @@ -380,6 +449,10 @@ public class Model extends ModelNode super.readExternal(in); _props = (Properties)in.readObject(); _anims = (HashMap)in.readObject(); + ArrayList controllers = (ArrayList)in.readObject(); + for (Object ctrl : controllers) { + addController((Controller)ctrl); + } } /** @@ -410,7 +483,9 @@ public class Model extends ModelNode _ccreator.addProperty("displaylistid"); _ccreator.addProperty("bound"); } - return (Model)_ccreator.createCopy(); + Model instance = (Model)_ccreator.createCopy(); + instance.initInstance(); + return instance; } @Override // documentation inherited @@ -441,18 +516,18 @@ public class Model extends ModelNode // update children super.updateWorldData(time); } - + /** - * Starts the supplied animation. + * Initializes the per-instance state of this model. */ - protected void startAnimation (String name, Animation anim) + protected void initInstance () { - _anim = anim; - _animName = name; - _fidx = 0; - _nidx = 1; - _fdir = +1; - _elapsed = 0f; + // initialize the controllers + for (Object ctrl : getControllers()) { + if (ctrl instanceof ModelController) { + ((ModelController)ctrl).init(this); + } + } } /** @@ -541,10 +616,31 @@ public class Model extends ModelNode /** The frame portion elapsed since the start of the current frame. */ protected float _elapsed; + /** The child node that contains the model's emissions in world space. */ + protected Node _emissionNode; + /** Animation completion listeners. */ protected ObserverList _animObservers = new ObserverList(ObserverList.FAST_UNSAFE_NOTIFY); + /** Used to notify observers of animation initiation. */ + protected class AnimStartedOp + implements ObserverList.ObserverOp + { + public AnimStartedOp (String name) + { + _name = name; + } + + public boolean apply (AnimationObserver obs) + { + return obs.animationStarted(Model.this, _name); + } + + /** The name of the animation started. */ + protected String _name; + } + /** Used to notify observers of animation completion. */ protected class AnimCompletedOp implements ObserverList.ObserverOp diff --git a/src/java/com/threerings/jme/model/ModelController.java b/src/java/com/threerings/jme/model/ModelController.java new file mode 100644 index 000000000..ad458f5c7 --- /dev/null +++ b/src/java/com/threerings/jme/model/ModelController.java @@ -0,0 +1,147 @@ +// +// $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.Externalizable; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Properties; + +import com.jme.renderer.CloneCreator; +import com.jme.scene.Controller; +import com.jme.scene.Node; +import com.jme.scene.Spatial; + +import com.samskivert.util.StringUtil; + +/** + * The superclass of procedural animation controllers for models. + */ +public abstract class ModelController extends Controller + implements Externalizable +{ + /** + * Configures this controller based on the supplied (sub-)properties and + * controller target. + */ + public void configure (Properties props, Spatial target) + { + _target = target; + String[] anims = StringUtil.parseStringArray( + props.getProperty("animations", "")); + if (anims.length == 0) { + return; + } + _animations = new HashSet(); + Collections.addAll(_animations, anims); + } + + /** + * Initializes this controller. + */ + public void init (Model model) + { + model.addAnimationObserver(_animobs); + if (_animations != null) { + setActive(false); + } + } + + @Override // documentation inherited + public Controller putClone (Controller store, CloneCreator properties) + { + if (store == null) { + return null; + } + ModelController mstore = (ModelController)store; + mstore._target = (Spatial)properties.originalToCopy.get(_target); + if (mstore._target == null) { + properties.originalToCopy.put(_target, + mstore._target = _target.putClone(null, properties)); + } + return mstore; + } + + // documentation inherited from interface Externalizable + public void writeExternal (ObjectOutput out) + throws IOException + { + out.writeObject(_target); + out.writeObject(_animations); + } + + // documentation inherited from interface Externalizable + public void readExternal (ObjectInput in) + throws IOException, ClassNotFoundException + { + _target = (Spatial)in.readObject(); + _animations = (HashSet)in.readObject(); + } + + /** + * Called when an animation is started on the model. + */ + protected void animationStarted (String anim) + { + if (_animations != null && _animations.contains(anim)) { + setActive(true); + } + } + + /** + * Called when an animation is stopped on the model. + */ + protected void animationStopped (String anim) + { + if (_animations != null) { + setActive(false); + } + } + + /** The target to control. */ + protected Spatial _target; + + /** The animations for which this controller should be active, or + * null for all of them. */ + protected HashSet _animations; + + /** Listens to the model's animation state. */ + protected Model.AnimationObserver _animobs = + new Model.AnimationObserver() { + public boolean animationStarted (Model model, String anim) { + ModelController.this.animationStarted(anim); + return true; + } + public boolean animationCompleted (Model model, String anim) { + animationStopped(anim); + return true; + } + public boolean animationCancelled (Model model, String anim) { + animationStopped(anim); + return true; + } + }; +} diff --git a/src/java/com/threerings/jme/model/Rotator.java b/src/java/com/threerings/jme/model/Rotator.java new file mode 100644 index 000000000..5d5db0214 --- /dev/null +++ b/src/java/com/threerings/jme/model/Rotator.java @@ -0,0 +1,126 @@ +// +// $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 java.util.Properties; + +import com.jme.math.Quaternion; +import com.jme.math.Vector3f; +import com.jme.renderer.CloneCreator; +import com.jme.scene.Controller; +import com.jme.scene.Spatial; + +import com.samskivert.util.StringUtil; + +import com.threerings.jme.Log; + +/** + * A procedural animation that rotates a node around at a constant angular + * velocity. + */ +public class Rotator extends ModelController +{ + @Override // documentation inherited + public void configure (Properties props, Spatial target) + { + super.configure(props, target); + String axisstr = props.getProperty("axis", "x"), + rpsstr = props.getProperty("radpersec", "3.14"); + if (axisstr.equalsIgnoreCase("x")) { + _axis = Vector3f.UNIT_X; + } else if (axisstr.equalsIgnoreCase("y")) { + _axis = Vector3f.UNIT_Y; + } else if (axisstr.equalsIgnoreCase("z")) { + _axis = Vector3f.UNIT_Z; + } else { + float[] axis = StringUtil.parseFloatArray(axisstr); + if (axis != null && axis.length == 3) { + _axis = new Vector3f(axis[0], axis[1], + axis[2]).normalizeLocal(); + + } else { + Log.warning("Invalid rotation axis [axis=" + axisstr + "]."); + } + } + try { + _radpersec = Float.parseFloat(rpsstr); + } catch (NumberFormatException e) { + Log.warning("Invalid rotation rate [radpersec=" + rpsstr + "]."); + } + } + + // documentation inherited + public void update (float time) + { + if (!isActive()) { + return; + } + _rot.fromAngleNormalAxis(time * _radpersec, _axis); + _target.getLocalRotation().multLocal(_rot); + } + + @Override // documentation inherited + public Controller putClone (Controller store, CloneCreator properties) + { + Rotator rstore; + if (store == null) { + rstore = new Rotator(); + } else { + rstore = (Rotator)store; + } + super.putClone(rstore, properties); + rstore._axis = _axis; + rstore._radpersec = _radpersec; + return rstore; + } + + // documentation inherited from interface Externalizable + public void writeExternal (ObjectOutput out) + throws IOException + { + super.writeExternal(out); + out.writeObject(_axis); + out.writeFloat(_radpersec); + } + + // documentation inherited from interface Externalizable + public void readExternal (ObjectInput in) + throws IOException, ClassNotFoundException + { + super.readExternal(in); + _axis = (Vector3f)in.readObject(); + _radpersec = in.readFloat(); + } + + /** The axis about which to rotate. */ + protected Vector3f _axis; + + /** The velocity at which to rotate in radians per second. */ + protected float _radpersec; + + /** A temporary quaternion. */ + protected Quaternion _rot = new Quaternion(); +} diff --git a/src/java/com/threerings/jme/tools/ModelDef.java b/src/java/com/threerings/jme/tools/ModelDef.java index 5d7867879..ee289cc85 100644 --- a/src/java/com/threerings/jme/tools/ModelDef.java +++ b/src/java/com/threerings/jme/tools/ModelDef.java @@ -38,9 +38,11 @@ import com.jme.scene.Spatial; import com.jme.util.geom.BufferUtils; import com.samskivert.util.PropertiesUtil; +import com.samskivert.util.StringUtil; import com.threerings.jme.Log; import com.threerings.jme.model.Model; +import com.threerings.jme.model.ModelController; import com.threerings.jme.model.ModelMesh; import com.threerings.jme.model.ModelNode; import com.threerings.jme.model.SkinMesh; @@ -358,9 +360,45 @@ public class ModelDef } } + // create any controllers listed + String[] controllers = StringUtil.parseStringArray( + props.getProperty("controllers", "")); + for (int ii = 0; ii < controllers.length; ii++) { + Spatial target = nodes.get(controllers[ii]); + if (target == null) { + Log.warning("Missing controller node [name=" + + controllers[ii] + "]."); + continue; + } + ModelController ctrl = createController( + PropertiesUtil.getSubProperties(props, controllers[ii]), + target); + if (ctrl != null) { + model.addController(ctrl); + } + } + return model; } + /** Creates, configures, and returns a model controller. */ + protected ModelController createController ( + Properties props, Spatial target) + { + // attempt to create an instance of the controller + ModelController ctrl; + String cname = props.getProperty("class", ""); + try { + ctrl = (ModelController)Class.forName(cname).newInstance(); + } catch (Exception e) { + Log.warning("Error instantiating controller [class=" + cname + + ", error=" + e + "]."); + return null; + } + ctrl.configure(props, target); + return ctrl; + } + /** Converts a boxed Integer list to an unboxed int array. */ protected static int[] toArray (ArrayList list) { diff --git a/src/java/com/threerings/jme/tools/ModelViewer.java b/src/java/com/threerings/jme/tools/ModelViewer.java index 951045e29..b6b27084a 100644 --- a/src/java/com/threerings/jme/tools/ModelViewer.java +++ b/src/java/com/threerings/jme/tools/ModelViewer.java @@ -156,7 +156,6 @@ public class ModelViewer extends JmeCanvasApp 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( @@ -328,7 +327,7 @@ public class ModelViewer extends JmeCanvasApp _status.setText(_msg.get("m.compiling_model", file)); Model model = CompileModelTask.compileModel(file); if (model != null) { - model.setReferenceTransforms(); + model.initPrototype(); setModel(model, file); return; } @@ -388,7 +387,7 @@ public class ModelViewer extends JmeCanvasApp _model.updateRenderState(); // configure the animation panel - String[] anims = _model.getAnimations(); + String[] anims = _model.getAnimationNames(); if (anims.length == 0) { _animctrls.setVisible(false); return; @@ -439,9 +438,13 @@ public class ModelViewer extends JmeCanvasApp /** The currently loaded model. */ protected Model _model; - /** Disables the stop button when animations stop. */ + /** Enables and disables the stop button when animations start and stop. */ protected Model.AnimationObserver _animobs = new Model.AnimationObserver() { + public boolean animationStarted (Model model, String name) { + _animstop.setEnabled(true); + return true; + } public boolean animationCompleted (Model model, String name) { _animstop.setEnabled(false); return true;