Merging the procedural animation controllers and emitters into Model

functionality, so that they will be visible in the model viewer.


git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@4046 542714f4-19e9-0310-aa3c-eee0fc999fb1
This commit is contained in:
Andrzej Kapolka
2006-04-21 23:10:23 +00:00
parent 7ce2b38ad5
commit f6608ea528
5 changed files with 438 additions and 28 deletions
+119 -23
View File
@@ -36,6 +36,7 @@ import java.nio.ByteOrder;
import java.nio.MappedByteBuffer; import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel; import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.Properties; import java.util.Properties;
@@ -45,6 +46,7 @@ import com.jme.math.Quaternion;
import com.jme.math.Vector3f; import com.jme.math.Vector3f;
import com.jme.renderer.CloneCreator; import com.jme.renderer.CloneCreator;
import com.jme.scene.Controller; import com.jme.scene.Controller;
import com.jme.scene.Node;
import com.jme.scene.Spatial; import com.jme.scene.Spatial;
import com.threerings.jme.Log; import com.threerings.jme.Log;
@@ -58,6 +60,13 @@ public class Model extends ModelNode
* for non-repeating animations) or cancelled. */ * for non-repeating animations) or cancelled. */
public interface AnimationObserver 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. * 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). */ /** The animation transforms (one transform per target per frame). */
public transient Transform[][] transforms; 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. * Rebinds this animation for a prototype instance.
* *
@@ -223,8 +240,8 @@ public class Model extends ModelNode
} }
ois.close(); ois.close();
// set the reference transforms before any animations are applied // initialize the model as a prototype
model.setReferenceTransforms(); model.initPrototype();
return model; return model;
} }
@@ -253,6 +270,16 @@ public class Model extends ModelNode
return _props; 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 * Adds an animation to the model's library. This should only be called by
* the model compiler. * the model compiler.
@@ -268,38 +295,65 @@ public class Model extends ModelNode
/** /**
* Returns the names of the model's animations. * Returns the names of the model's animations.
*/ */
public String[] getAnimations () public String[] getAnimationNames ()
{ {
if (_prototype != null) { if (_prototype != null) {
return _prototype.getAnimations(); return _prototype.getAnimationNames();
} }
return (_anims == null) ? new String[0] : return (_anims == null) ? new String[0] :
_anims.keySet().toArray(new String[_anims.size()]); _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) { if (_prototype != null) {
stopAnimation(); 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); Animation anim = _anims.get(name);
if (anim != null) { if (anim != null) {
startAnimation(name, anim); return anim;
return;
} }
if (_prototype != null) { if (_prototype != null) {
Animation panim = _prototype._anims.get(name); Animation panim = _prototype._anims.get(name);
if (panim != null) { if (panim != null) {
_anims.put(name, anim = panim.rebind(_pnodes)); _anims.put(name, anim = panim.rebind(_pnodes));
startAnimation(name, anim); return anim;
return;
} }
} }
Log.warning("Requested unknown animation [name=" + Log.warning("Requested unknown animation [name=" + name + "].");
name + "]."); return null;
} }
/** /**
@@ -347,6 +401,20 @@ public class Model extends ModelNode
_animObservers.remove(obs); _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. * Writes this model out to a file.
*/ */
@@ -371,6 +439,7 @@ public class Model extends ModelNode
super.writeExternal(out); super.writeExternal(out);
out.writeObject(_props); out.writeObject(_props);
out.writeObject(_anims); out.writeObject(_anims);
out.writeObject(getControllers());
} }
@Override // documentation inherited @Override // documentation inherited
@@ -380,6 +449,10 @@ public class Model extends ModelNode
super.readExternal(in); super.readExternal(in);
_props = (Properties)in.readObject(); _props = (Properties)in.readObject();
_anims = (HashMap<String, Animation>)in.readObject(); _anims = (HashMap<String, Animation>)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("displaylistid");
_ccreator.addProperty("bound"); _ccreator.addProperty("bound");
} }
return (Model)_ccreator.createCopy(); Model instance = (Model)_ccreator.createCopy();
instance.initInstance();
return instance;
} }
@Override // documentation inherited @Override // documentation inherited
@@ -443,16 +518,16 @@ public class Model extends ModelNode
} }
/** /**
* Starts the supplied animation. * Initializes the per-instance state of this model.
*/ */
protected void startAnimation (String name, Animation anim) protected void initInstance ()
{ {
_anim = anim; // initialize the controllers
_animName = name; for (Object ctrl : getControllers()) {
_fidx = 0; if (ctrl instanceof ModelController) {
_nidx = 1; ((ModelController)ctrl).init(this);
_fdir = +1; }
_elapsed = 0f; }
} }
/** /**
@@ -541,10 +616,31 @@ public class Model extends ModelNode
/** The frame portion elapsed since the start of the current frame. */ /** The frame portion elapsed since the start of the current frame. */
protected float _elapsed; protected float _elapsed;
/** The child node that contains the model's emissions in world space. */
protected Node _emissionNode;
/** Animation completion listeners. */ /** Animation completion listeners. */
protected ObserverList<AnimationObserver> _animObservers = protected ObserverList<AnimationObserver> _animObservers =
new ObserverList<AnimationObserver>(ObserverList.FAST_UNSAFE_NOTIFY); new ObserverList<AnimationObserver>(ObserverList.FAST_UNSAFE_NOTIFY);
/** Used to notify observers of animation initiation. */
protected class AnimStartedOp
implements ObserverList.ObserverOp<AnimationObserver>
{
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. */ /** Used to notify observers of animation completion. */
protected class AnimCompletedOp protected class AnimCompletedOp
implements ObserverList.ObserverOp<AnimationObserver> implements ObserverList.ObserverOp<AnimationObserver>
@@ -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<String>();
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<String>)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
* <code>null</code> for all of them. */
protected HashSet<String> _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;
}
};
}
@@ -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();
}
@@ -38,9 +38,11 @@ import com.jme.scene.Spatial;
import com.jme.util.geom.BufferUtils; import com.jme.util.geom.BufferUtils;
import com.samskivert.util.PropertiesUtil; import com.samskivert.util.PropertiesUtil;
import com.samskivert.util.StringUtil;
import com.threerings.jme.Log; import com.threerings.jme.Log;
import com.threerings.jme.model.Model; import com.threerings.jme.model.Model;
import com.threerings.jme.model.ModelController;
import com.threerings.jme.model.ModelMesh; import com.threerings.jme.model.ModelMesh;
import com.threerings.jme.model.ModelNode; import com.threerings.jme.model.ModelNode;
import com.threerings.jme.model.SkinMesh; 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; 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. */ /** Converts a boxed Integer list to an unboxed int array. */
protected static int[] toArray (ArrayList<Integer> list) protected static int[] toArray (ArrayList<Integer> list)
{ {
@@ -156,7 +156,6 @@ public class ModelViewer extends JmeCanvasApp
new AbstractAction(_msg.get("m.anim_start")) { new AbstractAction(_msg.get("m.anim_start")) {
public void actionPerformed (ActionEvent e) { public void actionPerformed (ActionEvent e) {
_model.startAnimation((String)_animbox.getSelectedItem()); _model.startAnimation((String)_animbox.getSelectedItem());
_animstop.setEnabled(true);
} }
})); }));
_animctrls.add(_animstop = new JButton( _animctrls.add(_animstop = new JButton(
@@ -328,7 +327,7 @@ public class ModelViewer extends JmeCanvasApp
_status.setText(_msg.get("m.compiling_model", file)); _status.setText(_msg.get("m.compiling_model", file));
Model model = CompileModelTask.compileModel(file); Model model = CompileModelTask.compileModel(file);
if (model != null) { if (model != null) {
model.setReferenceTransforms(); model.initPrototype();
setModel(model, file); setModel(model, file);
return; return;
} }
@@ -388,7 +387,7 @@ public class ModelViewer extends JmeCanvasApp
_model.updateRenderState(); _model.updateRenderState();
// configure the animation panel // configure the animation panel
String[] anims = _model.getAnimations(); String[] anims = _model.getAnimationNames();
if (anims.length == 0) { if (anims.length == 0) {
_animctrls.setVisible(false); _animctrls.setVisible(false);
return; return;
@@ -439,9 +438,13 @@ public class ModelViewer extends JmeCanvasApp
/** The currently loaded model. */ /** The currently loaded model. */
protected Model _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 = protected Model.AnimationObserver _animobs =
new Model.AnimationObserver() { new Model.AnimationObserver() {
public boolean animationStarted (Model model, String name) {
_animstop.setEnabled(true);
return true;
}
public boolean animationCompleted (Model model, String name) { public boolean animationCompleted (Model model, String name) {
_animstop.setEnabled(false); _animstop.setEnabled(false);
return true; return true;