bc7fd397fb
git-svn-id: svn+ssh://src.earth.threerings.net/nenya/trunk@1034 ed5b42cb-e716-0410-a449-f6a68f950b19
1195 lines
37 KiB
Java
1195 lines
37 KiB
Java
//
|
|
// $Id$
|
|
//
|
|
// Nenya library - tools for developing networked games
|
|
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
|
|
// http://code.google.com/p/nenya/
|
|
//
|
|
// 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.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.FileOutputStream;
|
|
import java.io.IOException;
|
|
|
|
import java.nio.FloatBuffer;
|
|
|
|
import java.util.Collections;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Properties;
|
|
|
|
import com.google.common.collect.Maps;
|
|
import com.google.common.collect.Sets;
|
|
|
|
import com.jme.bounding.BoundingVolume;
|
|
import com.jme.math.Quaternion;
|
|
import com.jme.math.Vector3f;
|
|
import com.jme.renderer.Camera;
|
|
import com.jme.renderer.Renderer;
|
|
import com.jme.scene.Controller;
|
|
import com.jme.scene.Node;
|
|
import com.jme.scene.Spatial;
|
|
import com.jme.util.export.JMEExporter;
|
|
import com.jme.util.export.JMEImporter;
|
|
import com.jme.util.export.InputCapsule;
|
|
import com.jme.util.export.OutputCapsule;
|
|
import com.jme.util.export.Savable;
|
|
import com.jme.util.export.binary.BinaryExporter;
|
|
import com.jme.util.export.binary.BinaryImporter;
|
|
|
|
import com.samskivert.util.ArrayUtil;
|
|
import com.samskivert.util.ObserverList;
|
|
import com.samskivert.util.PropertiesUtil;
|
|
import com.samskivert.util.RandomUtil;
|
|
import com.samskivert.util.StringUtil;
|
|
|
|
import com.threerings.jme.util.SpatialVisitor;
|
|
|
|
import static com.threerings.jme.Log.log;
|
|
|
|
/**
|
|
* The base node for models.
|
|
*/
|
|
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
|
|
{
|
|
/**
|
|
* 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.
|
|
*
|
|
* @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 Savable
|
|
{
|
|
/** 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;
|
|
|
|
/** Any nodes visible that never move within the model. */
|
|
public Spatial[] staticTargets;
|
|
|
|
/** The transformation targets of the animation. */
|
|
public Spatial[] transformTargets;
|
|
|
|
/** The animation transforms (one transform per target per frame). */
|
|
public transient Transform[][] transforms;
|
|
|
|
/** 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.
|
|
*/
|
|
public float getDuration ()
|
|
{
|
|
return (float)transforms.length / frameRate;
|
|
}
|
|
|
|
/**
|
|
* Rebinds this animation for a prototype instance.
|
|
*
|
|
* @param pnodes a mapping from prototype nodes to instance nodes
|
|
*/
|
|
public Animation rebind (Map<ModelSpatial, ModelSpatial> pnodes)
|
|
{
|
|
Animation anim = new Animation();
|
|
anim.frameRate = frameRate;
|
|
anim.repeatType = repeatType;
|
|
anim.staticTargets = rebind(staticTargets, pnodes);
|
|
anim.transformTargets = rebind(transformTargets, pnodes);
|
|
anim.transforms = transforms;
|
|
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]);
|
|
}
|
|
}
|
|
|
|
// documentation inherited
|
|
public Class<?> getClassTag ()
|
|
{
|
|
return getClass();
|
|
}
|
|
|
|
// documentation inherited
|
|
public void read (JMEImporter im)
|
|
throws IOException
|
|
{
|
|
InputCapsule capsule = im.getCapsule(this);
|
|
frameRate = capsule.readInt("frameRate", 0);
|
|
repeatType = capsule.readInt("repeatType", Controller.RT_CLAMP);
|
|
staticTargets = ArrayUtil.copy(capsule.readSavableArray(
|
|
"staticTargets", null), new Spatial[0]);
|
|
transformTargets = ArrayUtil.copy(capsule.readSavableArray(
|
|
"transformTargets", null), new Spatial[0]);
|
|
FloatBuffer pxforms = capsule.readFloatBuffer("transforms", null);
|
|
transforms = new Transform[pxforms.capacity() /
|
|
Transform.PACKED_SIZE / transformTargets.length][];
|
|
for (int ii = 0; ii < transforms.length; ii++) {
|
|
Transform[] frame = transforms[ii] =
|
|
new Transform[transformTargets.length];
|
|
for (int jj = 0; jj < frame.length; jj++) {
|
|
frame[jj] = new Transform(pxforms);
|
|
}
|
|
}
|
|
}
|
|
|
|
// documentation inherited
|
|
public void write (JMEExporter ex)
|
|
throws IOException
|
|
{
|
|
OutputCapsule capsule = ex.getCapsule(this);
|
|
capsule.write(frameRate, "frameRate", 0);
|
|
capsule.write(repeatType, "repeatType", Controller.RT_CLAMP);
|
|
capsule.write(staticTargets, "staticTargets", null);
|
|
capsule.write(transformTargets, "transformTargets", null);
|
|
FloatBuffer pxforms = FloatBuffer.allocate(
|
|
transforms.length * transformTargets.length * Transform.PACKED_SIZE);
|
|
for (Transform[] frame : transforms) {
|
|
for (Transform xform : frame) {
|
|
xform.writeToBuffer(pxforms);
|
|
}
|
|
}
|
|
pxforms.rewind();
|
|
capsule.write(pxforms, "transforms", null);
|
|
}
|
|
|
|
protected Spatial[] rebind (Spatial[] targets, Map<ModelSpatial, ModelSpatial> pnodes)
|
|
{
|
|
Spatial[] ntargets = new Spatial[targets.length];
|
|
for (int ii = 0; ii < targets.length; ii++) {
|
|
ntargets[ii] = (Spatial)pnodes.get(targets[ii]);
|
|
}
|
|
return ntargets;
|
|
}
|
|
|
|
private static final long serialVersionUID = 1;
|
|
}
|
|
|
|
/** A frame element that manipulates the target's transform. */
|
|
public static final class Transform
|
|
{
|
|
/** The number of floats required to store a packed transform. */
|
|
public static final int PACKED_SIZE = 3 + 4 + 3;
|
|
|
|
public Transform (
|
|
Vector3f translation, Quaternion rotation, Vector3f scale)
|
|
{
|
|
_translation = translation;
|
|
_rotation = rotation;
|
|
_scale = scale;
|
|
}
|
|
|
|
public Transform (FloatBuffer buf)
|
|
{
|
|
_translation = new Vector3f(buf.get(), buf.get(), buf.get());
|
|
_rotation = new Quaternion(buf.get(), buf.get(), buf.get(),
|
|
buf.get());
|
|
_scale = new Vector3f(buf.get(), buf.get(), buf.get());
|
|
}
|
|
|
|
public void apply (Spatial target)
|
|
{
|
|
target.getLocalTranslation().set(_translation);
|
|
target.getLocalRotation().set(_rotation);
|
|
target.getLocalScale().set(_scale);
|
|
}
|
|
|
|
/**
|
|
* Blends between this transform and the next, applying the result to
|
|
* the given target.
|
|
*
|
|
* @param alpha the blend factor: 0.0 for entirely this frame, 1.0 for
|
|
* entirely the next
|
|
*/
|
|
public void blend (Transform next, float alpha, Spatial target)
|
|
{
|
|
target.getLocalTranslation().interpolate(_translation,
|
|
next._translation, alpha);
|
|
target.getLocalRotation().slerp(_rotation, next._rotation, alpha);
|
|
target.getLocalScale().interpolate(_scale, next._scale, alpha);
|
|
}
|
|
|
|
/**
|
|
* Writes this transform to the current position in the supplied
|
|
* buffer.
|
|
*/
|
|
public void writeToBuffer (FloatBuffer buf)
|
|
{
|
|
buf.put(_translation.x);
|
|
buf.put(_translation.y);
|
|
buf.put(_translation.z);
|
|
|
|
buf.put(_rotation.x);
|
|
buf.put(_rotation.y);
|
|
buf.put(_rotation.z);
|
|
buf.put(_rotation.w);
|
|
|
|
buf.put(_scale.x);
|
|
buf.put(_scale.y);
|
|
buf.put(_scale.z);
|
|
}
|
|
|
|
/** The transform at this frame. */
|
|
protected Vector3f _translation, _scale;
|
|
protected Quaternion _rotation;
|
|
}
|
|
|
|
/** Customized clone creator for models. */
|
|
public static class CloneCreator
|
|
{
|
|
/** A shared seed used to select textures consistently. */
|
|
public int random;
|
|
|
|
/** Maps original objects to their copies. */
|
|
public Map<ModelSpatial, ModelSpatial> originalToCopy = Maps.newHashMap();
|
|
|
|
public CloneCreator (Model toCopy)
|
|
{
|
|
_toCopy = toCopy;
|
|
addProperty("vertices");
|
|
addProperty("colors");
|
|
addProperty("normals");
|
|
addProperty("texcoords");
|
|
addProperty("vboinfo");
|
|
addProperty("indices");
|
|
addProperty("obbtree");
|
|
addProperty("displaylistid");
|
|
addProperty("bound");
|
|
}
|
|
|
|
/**
|
|
* Sets the named property.
|
|
*/
|
|
public void addProperty (String name)
|
|
{
|
|
_properties.add(name);
|
|
}
|
|
|
|
/**
|
|
* Clears the named property.
|
|
*/
|
|
public void removeProperty (String name)
|
|
{
|
|
_properties.remove(name);
|
|
}
|
|
|
|
/**
|
|
* Checks whether the named property has been set.
|
|
*/
|
|
public boolean isSet (String name)
|
|
{
|
|
return _properties.contains(name);
|
|
}
|
|
|
|
/**
|
|
* Creates a new copy of the target model.
|
|
*/
|
|
public Model createCopy ()
|
|
{
|
|
random = RandomUtil.getInt(Integer.MAX_VALUE);
|
|
Model copy = (Model)_toCopy.putClone(null, this);
|
|
originalToCopy.clear(); // make sure no references remain
|
|
return copy;
|
|
}
|
|
|
|
/** The model to copy. */
|
|
protected Model _toCopy;
|
|
|
|
/** The set of added properties. */
|
|
protected HashSet<String> _properties = Sets.newHashSet();
|
|
}
|
|
|
|
/**
|
|
* Attempts to read a model from the specified file.
|
|
*/
|
|
public static Model readFromFile (File file)
|
|
throws IOException
|
|
{
|
|
// read the serialized model and its children
|
|
FileInputStream fis = new FileInputStream(file);
|
|
Model model = (Model)BinaryImporter.getInstance().load(fis);
|
|
fis.close();
|
|
|
|
// initialize the model as a prototype
|
|
model.initPrototype();
|
|
|
|
return model;
|
|
}
|
|
|
|
/**
|
|
* No-arg constructor for deserialization.
|
|
*/
|
|
public Model ()
|
|
{
|
|
}
|
|
|
|
/**
|
|
* Standard constructor.
|
|
*/
|
|
public Model (String name, Properties props)
|
|
{
|
|
super(name);
|
|
_props = props;
|
|
}
|
|
|
|
/**
|
|
* Returns a reference to the properties of the model.
|
|
*/
|
|
public Properties getProperties ()
|
|
{
|
|
return _props;
|
|
}
|
|
|
|
/**
|
|
* Initializes this model as prototype. Only necessary when the prototype
|
|
* was not loaded through {@link #readFromFile}.
|
|
*/
|
|
public void initPrototype ()
|
|
{
|
|
// 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();
|
|
cullInvisibleNodes();
|
|
initInstance();
|
|
}
|
|
|
|
/**
|
|
* Returns the names of the model's variant configurations.
|
|
*/
|
|
public String[] getVariantNames ()
|
|
{
|
|
return StringUtil.parseStringArray(_props.getProperty("variants", ""));
|
|
}
|
|
|
|
/**
|
|
* Adds an animation to the model's library. This should only be called by
|
|
* the model compiler.
|
|
*/
|
|
public void addAnimation (String name, Animation anim)
|
|
{
|
|
if (_anims == null) {
|
|
_anims = Maps.newHashMap();
|
|
}
|
|
_anims.put(name, anim);
|
|
|
|
// store the original transforms
|
|
Transform[] oxforms = new Transform[anim.transformTargets.length];
|
|
for (int ii = 0; ii < anim.transformTargets.length; ii++) {
|
|
Spatial target = anim.transformTargets[ii];
|
|
oxforms[ii] = new Transform(
|
|
new Vector3f(target.getLocalTranslation()),
|
|
new Quaternion(target.getLocalRotation()),
|
|
new Vector3f(target.getLocalScale()));
|
|
}
|
|
|
|
// run through every frame of the animation, expanding the bounding
|
|
// volumes of any deformable meshes
|
|
for (int ii = 0; ii < anim.transforms.length; ii++) {
|
|
for (int jj = 0; jj < anim.transforms[ii].length; jj++) {
|
|
anim.transforms[ii][jj].apply(anim.transformTargets[jj]);
|
|
}
|
|
updateWorldData(0f);
|
|
expandModelBounds();
|
|
}
|
|
|
|
// restore the original transforms
|
|
for (int ii = 0; ii < anim.transformTargets.length; ii++) {
|
|
oxforms[ii].apply(anim.transformTargets[ii]);
|
|
}
|
|
updateWorldData(0f);
|
|
}
|
|
|
|
/**
|
|
* 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 setAnimationMode (AnimationMode mode)
|
|
{
|
|
_animMode = mode;
|
|
}
|
|
|
|
/**
|
|
* Returns the animation mode configured for this model.
|
|
*/
|
|
public AnimationMode getAnimationMode ()
|
|
{
|
|
return _animMode;
|
|
}
|
|
|
|
/**
|
|
* Returns the names of the model's animations.
|
|
*/
|
|
public String[] getAnimationNames ()
|
|
{
|
|
if (_prototype != null) {
|
|
return _prototype.getAnimationNames();
|
|
}
|
|
return (_anims == null) ? new String[0] :
|
|
_anims.keySet().toArray(new String[_anims.size()]);
|
|
}
|
|
|
|
/**
|
|
* Checks whether the unit has an animation with the given name.
|
|
*/
|
|
public boolean hasAnimation (String name)
|
|
{
|
|
if (_prototype != null) {
|
|
return _prototype.hasAnimation(name);
|
|
}
|
|
return (_anims == null) ? false : _anims.containsKey(name);
|
|
}
|
|
|
|
/**
|
|
* Starts the named animation.
|
|
*
|
|
* @return the duration of the started animation (for looping animations,
|
|
* the duration of one cycle), or -1 if the animation was not found
|
|
*/
|
|
public float startAnimation (String name)
|
|
{
|
|
return startAnimation (name, 0, +1);
|
|
}
|
|
|
|
/**
|
|
* Starts the named animation.
|
|
*
|
|
* @param fidx the frame to start with
|
|
* @param fdir the direction to go (+1 forward, -1 backward)
|
|
*
|
|
* @return the duration of the started animation (for looping animations,
|
|
* the duration of one cycle), or -1 if the animation was not found
|
|
*/
|
|
public float startAnimation (String name, int fidx, int fdir)
|
|
{
|
|
Animation anim = getAnimation(name);
|
|
if (anim == null) {
|
|
return -1f;
|
|
}
|
|
if (_anim != null) {
|
|
_animObservers.apply(new AnimCancelledOp(_animName));
|
|
}
|
|
|
|
// first cull all model nodes, then re-activate the ones in the target lists
|
|
cullModelNodes();
|
|
for (Spatial target : anim.staticTargets) {
|
|
((ModelNode)target).updateCullMode();
|
|
}
|
|
for (Spatial target : anim.transformTargets) {
|
|
((ModelNode)target).updateCullMode();
|
|
}
|
|
|
|
_paused = false;
|
|
_anim = anim;
|
|
_animName = name;
|
|
_fidx = fidx;
|
|
_nidx = fidx;
|
|
_fdir = fdir;
|
|
_elapsed = 0f;
|
|
advanceFrameCounter();
|
|
_animObservers.apply(new AnimStartedOp(_animName));
|
|
return anim.getDuration() / _animSpeed;
|
|
}
|
|
|
|
/**
|
|
* Fast-forwards the current animation by the given number of seconds.
|
|
*/
|
|
public void fastForwardAnimation (float time)
|
|
{
|
|
updateAnimation(time);
|
|
}
|
|
|
|
/**
|
|
* Gets a reference to the animation with the given name.
|
|
*/
|
|
public Animation getAnimation (String name)
|
|
{
|
|
if (_anims == null) {
|
|
return null;
|
|
}
|
|
Animation anim = _anims.get(name);
|
|
if (anim != null) {
|
|
return anim;
|
|
}
|
|
if (_prototype != null) {
|
|
Animation panim = _prototype._anims.get(name);
|
|
if (panim != null) {
|
|
_anims.put(name, anim = panim.rebind(_pnodes));
|
|
return anim;
|
|
}
|
|
}
|
|
log.warning("Requested unknown animation [name=" + name + "].");
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Returns a reference to the currently running animation, or
|
|
* <code>null</code> if no animation is running.
|
|
*/
|
|
public Animation getAnimation ()
|
|
{
|
|
return _anim;
|
|
}
|
|
|
|
/**
|
|
* Stops the currently running animation.
|
|
*/
|
|
public void stopAnimation ()
|
|
{
|
|
if (_anim == null) {
|
|
return;
|
|
}
|
|
if (_outside) {
|
|
// make sure the meshes are in the right places when we come back into view
|
|
updateMeshes();
|
|
}
|
|
_paused = false;
|
|
_anim = null;
|
|
_animObservers.apply(new AnimCancelledOp(_animName));
|
|
}
|
|
|
|
/**
|
|
* Sets the pause state of the animation.
|
|
*/
|
|
public void pauseAnimation (boolean pause)
|
|
{
|
|
_paused = pause;
|
|
}
|
|
|
|
/**
|
|
* Returns the pause state of the animation.
|
|
*/
|
|
public boolean isAnimationPaused ()
|
|
{
|
|
return _paused;
|
|
}
|
|
|
|
/**
|
|
* Causes the animation to start running in reverse.
|
|
*/
|
|
public void reverseAnimation ()
|
|
{
|
|
_fdir *= -1;
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
/**
|
|
* 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") {
|
|
public void updateWorldVectors () {
|
|
worldTranslation.set(localTranslation);
|
|
worldRotation.set(localRotation);
|
|
worldScale.set(localScale);
|
|
}
|
|
});
|
|
}
|
|
return _emissionNode;
|
|
}
|
|
|
|
/**
|
|
* Writes this model out to a file.
|
|
*/
|
|
public void writeToFile (File file)
|
|
throws IOException
|
|
{
|
|
// start out by writing this node and its children
|
|
FileOutputStream fos = new FileOutputStream(file);
|
|
BinaryExporter.getInstance().save(this, fos);
|
|
fos.close();
|
|
}
|
|
|
|
@Override
|
|
public void read (JMEImporter im)
|
|
throws IOException
|
|
{
|
|
super.read(im);
|
|
InputCapsule capsule = im.getCapsule(this);
|
|
String[] propNames = capsule.readStringArray("propNames", null),
|
|
propValues = capsule.readStringArray("propValues", null);
|
|
_props = new Properties();
|
|
for (int ii = 0; ii < propNames.length; ii++) {
|
|
_props.setProperty(propNames[ii], propValues[ii]);
|
|
}
|
|
String[] animNames = capsule.readStringArray("animNames", null);
|
|
if (animNames != null) {
|
|
Savable[] animValues = capsule.readSavableArray(
|
|
"animValues", null);
|
|
_anims = Maps.newHashMap();
|
|
for (int ii = 0; ii < animNames.length; ii++) {
|
|
_anims.put(animNames[ii], (Animation)animValues[ii]);
|
|
}
|
|
} else {
|
|
_anims = null;
|
|
}
|
|
List<?> controllers = capsule.readSavableArrayList("controllers", null);
|
|
if (controllers != null) {
|
|
for (Object ctrl : controllers) {
|
|
addController((Controller)ctrl);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void write (JMEExporter ex)
|
|
throws IOException
|
|
{
|
|
// don't serialize the emission node; it contains transient geometry
|
|
// created by controllers
|
|
if (_emissionNode != null) {
|
|
detachChild(_emissionNode);
|
|
}
|
|
super.write(ex);
|
|
if (_emissionNode != null) {
|
|
attachChild(_emissionNode);
|
|
}
|
|
OutputCapsule capsule = ex.getCapsule(this);
|
|
capsule.write(_props.keySet().toArray(new String[_props.size()]),
|
|
"propNames", null);
|
|
capsule.write(_props.values().toArray(new String[_props.size()]),
|
|
"propValues", null);
|
|
if (_anims != null) {
|
|
capsule.write(_anims.keySet().toArray(
|
|
new String[_anims.size()]), "animNames", null);
|
|
capsule.write(_anims.values().toArray(
|
|
new Animation[_anims.size()]), "animValues", null);
|
|
}
|
|
capsule.writeSavableArrayList(getControllers(), "controllers", null);
|
|
}
|
|
|
|
@Override
|
|
public void resolveTextures (TextureProvider tprov)
|
|
{
|
|
super.resolveTextures(tprov);
|
|
for (Object ctrl : getControllers()) {
|
|
if (ctrl instanceof ModelController) {
|
|
((ModelController)ctrl).resolveTextures(tprov);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a new prototype using the given variant configuration.
|
|
* Use {@link #createInstance} on the returned prototype to create
|
|
* additional instances of the variant.
|
|
*/
|
|
public Model createPrototype (String variant)
|
|
{
|
|
if (_prototype != null) {
|
|
return _prototype.createPrototype(variant);
|
|
}
|
|
// create an instance and rebind all animations
|
|
final Model prototype = createInstance();
|
|
if (_anims != null) {
|
|
for (Map.Entry<String, Animation> entry : _anims.entrySet()) {
|
|
prototype._anims.put(entry.getKey(),
|
|
entry.getValue().rebind(prototype._pnodes));
|
|
}
|
|
}
|
|
prototype._prototype = null;
|
|
prototype._pnodes = null;
|
|
|
|
// reconfigure meshes with new variant type
|
|
if (variant != null) {
|
|
prototype._props = PropertiesUtil.getFilteredProperties(
|
|
_props, variant);
|
|
new SpatialVisitor<ModelMesh>(ModelMesh.class) {
|
|
public void visit (ModelMesh mesh) {
|
|
mesh.reconfigure(PropertiesUtil.getFilteredProperties(
|
|
prototype._props, mesh.getParent().getName()));
|
|
}
|
|
}.traverse(prototype);
|
|
}
|
|
return prototype;
|
|
}
|
|
|
|
/**
|
|
* Creates and returns a new instance of this model.
|
|
*/
|
|
public Model createInstance ()
|
|
{
|
|
if (_prototype != null) {
|
|
return _prototype.createInstance();
|
|
}
|
|
if (_ccreator == null) {
|
|
_ccreator = new CloneCreator(this);
|
|
}
|
|
Model instance = _ccreator.createCopy();
|
|
instance.initInstance();
|
|
return instance;
|
|
}
|
|
|
|
/**
|
|
* Locks the transforms and bounds of this model in the expectation that it
|
|
* will never be moved from its current position.
|
|
*/
|
|
public void lockInstance ()
|
|
{
|
|
// collect the instance's animation and controller targets and lock
|
|
// recursively
|
|
HashSet<Spatial> targets = Sets.newHashSet();
|
|
for (String aname : getAnimationNames()) {
|
|
Collections.addAll(targets, getAnimation(aname).transformTargets);
|
|
}
|
|
for (Object ctrl : getControllers()) {
|
|
if (ctrl instanceof ModelController) {
|
|
targets.add(((ModelController)ctrl).getTarget());
|
|
}
|
|
}
|
|
lockInstance(targets);
|
|
}
|
|
|
|
@Override
|
|
public Spatial putClone (Spatial store, CloneCreator properties)
|
|
{
|
|
Model mstore = (Model)properties.originalToCopy.get(this);
|
|
if (mstore != null) {
|
|
return mstore;
|
|
} else if (store == null) {
|
|
mstore = new Model(getName(), _props);
|
|
} else {
|
|
mstore = (Model)store;
|
|
}
|
|
// don't clone the emission node, as it contains transient geometry
|
|
if (_emissionNode != null) {
|
|
detachChild(_emissionNode);
|
|
}
|
|
super.putClone(mstore, properties);
|
|
if (_emissionNode != null) {
|
|
attachChild(_emissionNode);
|
|
}
|
|
mstore._prototype = this;
|
|
if (_anims != null) {
|
|
mstore._anims = Maps.newHashMap();
|
|
}
|
|
mstore._pnodes = Maps.newHashMap(properties.originalToCopy);
|
|
mstore._animMode = _animMode;
|
|
return mstore;
|
|
}
|
|
|
|
@Override
|
|
public void updateGeometricState (float time, boolean initiator)
|
|
{
|
|
// if we were not visible the last time we were rendered, don't do a
|
|
// full update; just update the world bound and wait until we come
|
|
// into view
|
|
boolean wasOutside = _outside;
|
|
_outside = isOutsideFrustum() && worldBound != null;
|
|
|
|
// slow evvvverything down by the animation speed
|
|
time *= _animSpeed;
|
|
if (_anim != null) {
|
|
updateAnimation(time);
|
|
}
|
|
|
|
// update controllers and children with accumulated time
|
|
_accum += time;
|
|
if (_outside) {
|
|
if ((lockedMode & LOCKED_TRANSFORMS) != 0) {
|
|
return; // world bound will not changed
|
|
}
|
|
if (!wasOutside) {
|
|
storeWorldBound();
|
|
}
|
|
updateWorldVectors();
|
|
worldBound = _storedBound.transform(getWorldRotation(),
|
|
getWorldTranslation(), getWorldScale(), worldBound);
|
|
|
|
} else {
|
|
super.updateGeometricState(_shouldAccumulate ? _accum : time,
|
|
initiator);
|
|
_accum = 0f;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onDraw (Renderer r)
|
|
{
|
|
// if we switch from invisible to visible, we have to do a last-minute
|
|
// full update (which only works if our meshes are enqueued)
|
|
super.onDraw(r);
|
|
if (_outside && !isOutsideFrustum()) {
|
|
updateWorldData(0f);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Transforms the current world bound into model space and stores it for
|
|
* when the model is offscreen.
|
|
*/
|
|
protected void storeWorldBound ()
|
|
{
|
|
// update the bounds with an identity transform (which will be
|
|
// overwritten after this method is called)
|
|
getWorldRotation().loadIdentity();
|
|
getWorldTranslation().set(Vector3f.ZERO);
|
|
getWorldScale().set(Vector3f.UNIT_XYZ);
|
|
for (int ii = 0, nn = getQuantity(); ii < nn; ii++) {
|
|
getChild(ii).updateGeometricState(0f, false);
|
|
}
|
|
updateWorldBound();
|
|
|
|
_storedBound = worldBound.clone(_storedBound);
|
|
}
|
|
|
|
/**
|
|
* Determines whether this node was determined to be entirely outside the
|
|
* view frustum.
|
|
*/
|
|
protected boolean isOutsideFrustum ()
|
|
{
|
|
for (Node node = this; node != null; node = node.getParent()) {
|
|
if (node.getLastFrustumIntersection() == Camera.OUTSIDE_FRUSTUM) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Initializes the per-instance state of this model.
|
|
*/
|
|
protected void initInstance ()
|
|
{
|
|
// initialize the controllers
|
|
for (Object ctrl : getControllers()) {
|
|
if (ctrl instanceof ModelController) {
|
|
ModelController mctrl = (ModelController)ctrl;
|
|
mctrl.init(this);
|
|
_shouldAccumulate |= mctrl.shouldAccumulate();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates the model's state according to the current animation.
|
|
*/
|
|
protected void updateAnimation (float time)
|
|
{
|
|
// no need to update between frames for flipbook animation
|
|
if (_animMode == AnimationMode.FLIPBOOK && _elapsed > 0f &&
|
|
_elapsed < 1f) {
|
|
_elapsed += (time * _anim.frameRate);
|
|
return;
|
|
}
|
|
|
|
// advance the frame counter if necessary
|
|
while (_elapsed > 1f) {
|
|
if (!_paused) {
|
|
advanceFrameCounter();
|
|
_elapsed -= 1f;
|
|
} else {
|
|
_elapsed = 1f;
|
|
}
|
|
}
|
|
|
|
// update the target transforms and animation frame if not outside the
|
|
// view frustum
|
|
if (!_outside) {
|
|
updateMeshes();
|
|
}
|
|
|
|
// if the next index is the same as this one, we are finished
|
|
if (_fidx == _nidx && !_paused) {
|
|
_anim = null;
|
|
_animObservers.apply(new AnimCompletedOp(_animName));
|
|
return;
|
|
}
|
|
_elapsed += (time * _anim.frameRate);
|
|
}
|
|
|
|
/**
|
|
* 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.max(0, Math.min(_nidx + _fdir, nframes - 1));
|
|
|
|
} else if (_anim.repeatType == Controller.RT_WRAP) {
|
|
// % is not a modulo operator, so is not guaranteed to be positive
|
|
_nidx = (_nidx + _fdir) % nframes;
|
|
if (_nidx < 0) {
|
|
_nidx += nframes;
|
|
}
|
|
|
|
} else { // _anim.repeatType == Controller.RT_CYCLE
|
|
if ((_nidx + _fdir) < 0 || (_nidx + _fdir) >= nframes) {
|
|
_fdir *= -1; // reverse direction
|
|
}
|
|
_nidx += _fdir;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates the states of the model's meshes according to the animation state.
|
|
*/
|
|
protected void updateMeshes ()
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
|
|
/** A reference to the prototype, or <code>null</code> if this is a prototype. */
|
|
protected Model _prototype;
|
|
|
|
/** For prototype models, a customized clone creator used to generate instances. */
|
|
protected CloneCreator _ccreator;
|
|
|
|
/** The animation mode to use for this model. */
|
|
protected AnimationMode _animMode;
|
|
|
|
/** For instances, maps prototype nodes to their corresponding instance nodes. */
|
|
protected Map<ModelSpatial, ModelSpatial> _pnodes;
|
|
|
|
/** The model properties. */
|
|
protected Properties _props;
|
|
|
|
/** The model animations. */
|
|
protected HashMap<String, Animation> _anims;
|
|
|
|
/** The currently running animation, or <code>null</code> for none. */
|
|
protected Animation _anim;
|
|
|
|
/** The name of the currently running animation, if any. */
|
|
protected String _animName;
|
|
|
|
/** The current animation speed multiplier. */
|
|
protected float _animSpeed = 1f;
|
|
|
|
/** 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;
|
|
|
|
/** The pause status of this model. */
|
|
protected boolean _paused;
|
|
|
|
/** The amount of update time accumulated while outside of view frustum. */
|
|
protected float _accum;
|
|
|
|
/** Whether or not we should accumulate update time while out of view. */
|
|
protected boolean _shouldAccumulate;
|
|
|
|
/** The child node that contains the model's emissions in world space. */
|
|
protected Node _emissionNode;
|
|
|
|
/** The model space bounding volume stored for use when the model is
|
|
* offscreen. */
|
|
protected BoundingVolume _storedBound;
|
|
|
|
/** Whether or not we were outside the frustum at the last update. */
|
|
protected boolean _outside;
|
|
|
|
/** Animation completion listeners. */
|
|
protected ObserverList<AnimationObserver> _animObservers =
|
|
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;
|
|
}
|
|
|
|
// documentation inherited from interface ObserverOp
|
|
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<AnimationObserver>
|
|
{
|
|
public AnimCompletedOp (String name)
|
|
{
|
|
_name = name;
|
|
}
|
|
|
|
// documentation inherited from interface ObserverOp
|
|
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<AnimationObserver>
|
|
{
|
|
public AnimCancelledOp (String name)
|
|
{
|
|
_name = name;
|
|
}
|
|
|
|
// documentation inherited from interface ObserverOp
|
|
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;
|
|
}
|