aboutsummaryrefslogtreecommitdiffstats
path: root/ardor3d-animation/src
diff options
context:
space:
mode:
authorneothemachine <[email protected]>2012-12-05 17:03:16 +0100
committerneothemachine <[email protected]>2012-12-05 17:03:16 +0100
commit9dd02f103042cb8a196f8a3ed2278da443e345bf (patch)
tree422449f0c62ff9518316ce5d4219bb2b12f0ed15 /ardor3d-animation/src
parent2b26b12fd794de0f03a064a10024a3d9f5583756 (diff)
move all files from trunk to root folder
Diffstat (limited to 'ardor3d-animation/src')
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/AnimationApplier.java64
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/AnimationListener.java28
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/AnimationManager.java603
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/AnimationUpdateStateListener.java19
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/AttachmentPoint.java113
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/Joint.java135
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/PoseListener.java26
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/Skeleton.java116
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/SkeletonPose.java297
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/SkinPoseApplyLogic.java29
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/SkinnedMesh.java756
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/SkinnedMeshCombineLogic.java85
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/AbstractTwoPartSource.java53
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/BinaryLERPSource.java192
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/BlendTreeSource.java59
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/ClipSource.java110
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/ExclusiveClipSource.java103
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/FrozenTreeSource.java57
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/InclusiveClipSource.java106
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/ManagedTransformSource.java183
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/SimpleAnimationApplier.java123
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/AbstractAnimationChannel.java213
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/AnimationClip.java194
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/AnimationClipInstance.java147
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/GuaranteedTriggerChannel.java138
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/InterpolatedDoubleChannel.java167
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/InterpolatedFloatChannel.java167
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/JointChannel.java185
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/JointData.java1
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/TransformChannel.java320
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/TransformData.java190
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/TriggerCallback.java30
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/TriggerChannel.java166
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/TriggerData.java76
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/layer/AnimationLayer.java367
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/layer/LayerBlender.java52
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/layer/LayerLERPBlender.java69
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/AbstractFiniteState.java103
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/AbstractTransitionState.java148
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/AbstractTwoStateLerpTransition.java212
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/FadeTransitionState.java74
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/FrozenTransitionState.java72
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/IgnoreTransitionState.java55
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/ImmediateTransitionState.java63
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/StateOwner.java30
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/SteadyState.java225
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/SyncFadeTransitionState.java50
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/loader/ImportClipMap.java38
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/loader/InputStore.java23
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/loader/JSLayerImporter.java66
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/loader/OutputClipSourceMap.java39
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/loader/OutputStore.java49
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/util/LoggingMap.java149
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/util/MissingCallback.java17
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/util/SkeletalDebugger.java347
-rw-r--r--ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/util/SkinUtils.java160
-rw-r--r--ardor3d-animation/src/main/resources/com/ardor3d/extension/animation/skeletal/skinning_gpu.frag19
-rw-r--r--ardor3d-animation/src/main/resources/com/ardor3d/extension/animation/skeletal/skinning_gpu.vert36
-rw-r--r--ardor3d-animation/src/main/resources/com/ardor3d/extension/animation/skeletal/skinning_gpu_mat4.vert39
-rw-r--r--ardor3d-animation/src/main/resources/com/ardor3d/extension/animation/skeletal/skinning_gpu_texture.frag25
-rw-r--r--ardor3d-animation/src/main/resources/com/ardor3d/extension/animation/skeletal/skinning_gpu_texture.vert41
-rw-r--r--ardor3d-animation/src/main/resources/com/ardor3d/extension/animation/skeletal/state/loader/functions.js355
62 files changed, 8174 insertions, 0 deletions
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/AnimationApplier.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/AnimationApplier.java
new file mode 100644
index 0000000..ced1a80
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/AnimationApplier.java
@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal;
+
+import com.ardor3d.extension.animation.skeletal.clip.TriggerCallback;
+import com.ardor3d.scenegraph.Spatial;
+
+/**
+ * Describes a class that can take information from a manager and its current layers and state and apply it to a given
+ * SkeletonPose. The class should not update or modify the manager, but should merely request current state (usually via
+ * <i>manager.getCurrentSourceData();</i>)
+ */
+public interface AnimationApplier {
+
+ /**
+ * Apply the current status of the manager to our SkeletonPose.
+ *
+ * @param applyToPose
+ * the pose to apply to
+ * @param manager
+ * the animation manager to pull state from.
+ */
+ void applyTo(SkeletonPose applyToPose, AnimationManager manager);
+
+ /**
+ * Apply the current status of the manager to non-skeletal assets.
+ *
+ * @param root
+ * the root of the scene graph we will apply to.
+ * @param manager
+ * the animation manager to pull state from.
+ */
+ void apply(Spatial root, AnimationManager manager);
+
+ /**
+ * Add a trigger callback to our callback list.
+ *
+ * @param key
+ * the key to add a callback to
+ * @param callback
+ * the callback logic to add.
+ */
+ void addTriggerCallback(final String key, final TriggerCallback callback);
+
+ /**
+ * Remove a trigger callback from our callback list for a specific key.
+ *
+ * @param key
+ * the key to remove from
+ * @param callback
+ * the callback logic to remove.
+ * @return true if the callback was found to remove
+ */
+ boolean removeTriggerCallback(final String key, final TriggerCallback callback);
+
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/AnimationListener.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/AnimationListener.java
new file mode 100644
index 0000000..4259e9a
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/AnimationListener.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal;
+
+import com.ardor3d.extension.animation.skeletal.clip.AnimationClipInstance;
+
+/**
+ * Describes a class interested in receiving notice when an animation has changed state.
+ */
+public interface AnimationListener {
+
+ /**
+ * Called when an animation reaches the end of its complete play time (maxTime * loops)
+ *
+ * @param source
+ * The animation clip instance that finished.
+ */
+ void animationFinished(AnimationClipInstance source);
+
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/AnimationManager.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/AnimationManager.java
new file mode 100644
index 0000000..9bda101
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/AnimationManager.java
@@ -0,0 +1,603 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal;
+
+import java.util.List;
+import java.util.Map;
+
+import com.ardor3d.extension.animation.skeletal.clip.AnimationClip;
+import com.ardor3d.extension.animation.skeletal.clip.AnimationClipInstance;
+import com.ardor3d.extension.animation.skeletal.layer.AnimationLayer;
+import com.ardor3d.extension.animation.skeletal.state.AbstractFiniteState;
+import com.ardor3d.extension.animation.skeletal.util.LoggingMap;
+import com.ardor3d.scenegraph.Spatial;
+import com.ardor3d.util.ReadOnlyTimer;
+import com.ardor3d.util.Timer;
+import com.google.common.collect.Lists;
+import com.google.common.collect.MapMaker;
+
+/**
+ * <p>
+ * AnimationManager describes and maintains an animation system. It tracks one or more layered animation state machines
+ * (AnimationLayer) and uses their combined result to update one or more poses (via a set AnimationApplier.)
+ * AnimationClips used in these layers are instanced and tracked specifically for this manager.
+ * </p>
+ * <p>
+ * By default, an animation manager has a single base animation layer. Other layers may be added to this. It is
+ * important that the base layer (the layer at index 0) always has a full set of data to put a skeleton pose into a
+ * valid state.
+ * </p>
+ */
+public class AnimationManager {
+
+ public enum AnimationUpdateState {
+ Play, Pause, Stop
+ }
+
+ /**
+ * A timer to use as our "global" time keeper. All animation sources under this manager will use this timer as their
+ * time reference.
+ */
+ protected ReadOnlyTimer _globalTimer;
+
+ /** The pose(s) this manager manipulates on update. */
+ protected List<SkeletonPose> _applyToPoses;
+
+ /** The root of a scenegraph we can look for transform animation targets under. */
+ protected final Spatial _sceneRoot;
+
+ /** Local instance information for any clips referenced by the layers/blend trees in this manager. */
+ protected final Map<AnimationClip, AnimationClipInstance> _clipInstances = new MapMaker().weakKeys().makeMap();
+
+ /** A logic object responsible for taking animation data and applying it to skeleton poses. */
+ protected AnimationApplier _applier;
+
+ /** Our animation layers. */
+ protected final List<AnimationLayer> _layers = Lists.newArrayList();
+
+ /**
+ * A map of key->Double values, allowing control over elements under this manager without needing precise knowledge
+ * of the layout of those layers, blend trees, etc. Missing keys will return 0.0 and log a warning.
+ */
+ protected final LoggingMap<String, Double> _valuesStore = new LoggingMap<String, Double>();
+
+ /**
+ * The throttle rate of animation. Default is 60fps (1/60.0). Set to 0 to disable throttling.
+ */
+ protected double _updateRate = 1.0 / 60.0;
+
+ /**
+ * The global time we last processed an animation. (To use when checking our throttle.)
+ */
+ protected double _lastUpdate = 0.0;
+
+ /**
+ * Sets the current animationState used to control if animation is playing, pausing or stopped.
+ */
+ protected AnimationUpdateState _currentAnimationState = AnimationUpdateState.Play;
+
+ /**
+ * boolean flag to allow stop state to be updated one last time.
+ */
+ protected boolean _canSetStopState = false;
+
+ /**
+ * boolean flag to reset Clips automatically once they are stopped.
+ */
+ protected boolean _resetClipsOnStop = false;
+
+ /**
+ * Listeners for changes to this manager's AnimationUpdateState.
+ */
+ protected final List<AnimationUpdateStateListener> _updateStateListeners = Lists.newArrayList();
+
+ /**
+ * Construct a new AnimationManager.
+ *
+ * @param globalTimer
+ * the timer to use for global time keeping.
+ * @param pose
+ * a pose to update. Optional if we won't be animating a {@link SkinnedMesh}.
+ */
+ public AnimationManager(final ReadOnlyTimer globalTimer, final SkeletonPose pose) {
+ this(globalTimer, pose, null);
+ }
+
+ /**
+ * Construct a new AnimationManager.
+ *
+ * @param globalTimer
+ * the timer to use for global time keeping.
+ * @param pose
+ * a pose to update. Optional if we won't be animating a {@link SkinnedMesh}.
+ * @param sceneRoot
+ * a root we will use to search for spatials when doing transform animations.
+ */
+ public AnimationManager(final ReadOnlyTimer globalTimer, final SkeletonPose pose, final Spatial sceneRoot) {
+ _globalTimer = globalTimer;
+ _sceneRoot = sceneRoot;
+
+ // add our base layer
+ final AnimationLayer layer = new AnimationLayer(AnimationLayer.BASE_LAYER_NAME);
+ layer.setManager(this);
+ _layers.add(layer);
+
+ if (pose != null) {
+ _applyToPoses = Lists.newArrayList(pose);
+ } else {
+ _applyToPoses = Lists.newArrayList();
+ }
+
+ _valuesStore.setLogOnReplace(false);
+ _valuesStore.setDefaultValue(0.0);
+ }
+
+ /**
+ * @return the "local time", in seconds reported by our global timer.
+ */
+ public double getCurrentGlobalTime() {
+ return _globalTimer.getTimeInSeconds();
+ }
+
+ /**
+ * @return the timer used by this manager for global time keeping.
+ */
+ public ReadOnlyTimer getGlobalTimer() {
+ return _globalTimer;
+ }
+
+ /**
+ * @param timer
+ * the timer to be used by this manager for global time keeping.
+ */
+ public void setGlobalTimer(final Timer timer) {
+ _globalTimer = timer;
+ }
+
+ /**
+ *
+ * @return True if clips will reset if the currentUpdateState is Stop.
+ */
+ public boolean isResetClipsOnStop() {
+ return _resetClipsOnStop;
+ }
+
+ /**
+ * @param resetClipsOnStop
+ * True if clips are to be reset when currentUpdateState is Stop, false otherwise.
+ *
+ *
+ */
+ public void setResetClipsOnStop(final boolean resetClipsOnStop) {
+ _resetClipsOnStop = resetClipsOnStop;
+ }
+
+ public void play() {
+ setAnimationUpdateState(AnimationUpdateState.Play);
+ }
+
+ public void pause() {
+ setAnimationUpdateState(AnimationUpdateState.Pause);
+ }
+
+ public void stop() {
+ setAnimationUpdateState(AnimationUpdateState.Stop);
+ }
+
+ public boolean isPlaying() {
+ return _currentAnimationState == AnimationUpdateState.Play;
+ }
+
+ public boolean isPaused() {
+ return _currentAnimationState == AnimationUpdateState.Pause;
+ }
+
+ public boolean isStopped() {
+ return _currentAnimationState == AnimationUpdateState.Stop;
+ }
+
+ /**
+ * @param newAnimationState
+ * the new animation state in the animation Manager.
+ */
+ public void setAnimationUpdateState(final AnimationUpdateState newAnimationState) {
+ if (newAnimationState == _currentAnimationState) {
+ // ignore if unchanged.
+ return;
+ }
+ final double currentTime = _globalTimer.getTimeInSeconds();
+ if (newAnimationState == AnimationUpdateState.Pause) {
+ if (_currentAnimationState == AnimationUpdateState.Stop) {
+ // ignore a non-allowed situation
+ return;
+ }
+
+ // Keep track of current time so we can resume active clips
+ _lastUpdate = currentTime;
+ } else if (newAnimationState == AnimationUpdateState.Play) {
+ // reset instances
+ if (_currentAnimationState == AnimationUpdateState.Pause) {
+ final double offset = currentTime - _lastUpdate;
+ for (final AnimationClipInstance instance : _clipInstances.values()) {
+ if (instance.isActive()) {
+ instance.setStartTime(instance.getStartTime() + offset);
+ }
+ }
+ } else {
+ // if newState is check if we will restart clips.
+ if (_resetClipsOnStop) {
+ for (final AnimationClipInstance instance : _clipInstances.values()) {
+ if (instance.isActive()) {
+ instance.setStartTime(currentTime);
+ }
+ }
+ }
+
+ }
+ } else {
+ for (final AnimationClipInstance instance : _clipInstances.values()) {
+ if (instance.isActive()) {
+ instance.setStartTime(currentTime);
+ }
+ }
+ }
+
+ final AnimationUpdateState oldState = _currentAnimationState;
+ _currentAnimationState = newAnimationState;
+
+ // Let listeners know we have changed state.
+ fireAnimationUpdateStateChange(oldState);
+ }
+
+ /**
+ * Notify any listeners of the state change
+ *
+ * @param oldState
+ * previous state
+ */
+ protected void fireAnimationUpdateStateChange(final AnimationUpdateState oldState) {
+ for (final AnimationUpdateStateListener listener : _updateStateListeners) {
+ listener.stateChanged(oldState, _currentAnimationState);
+ }
+ }
+
+ /**
+ * Add an AnimationUpdateStateListener to this manager.
+ *
+ * @param listener
+ * the listener to add.
+ */
+ public void addAnimationUpdateStateListener(final AnimationUpdateStateListener listener) {
+ _updateStateListeners.add(listener);
+ }
+
+ /**
+ * Remove an AnimationUpdateStateListener from this manager.
+ *
+ * @param listener
+ * the listener to remove.
+ * @return true if the listener was found
+ */
+ public boolean removeAnimationUpdateStateListener(final AnimationUpdateStateListener listener) {
+ return _updateStateListeners.remove(listener);
+ }
+
+ /**
+ * Remove any AnimationUpdateStateListeners registered with this manager.
+ */
+ public void clearAnimationUpdateStateListeners() {
+ _updateStateListeners.clear();
+ }
+
+ /**
+ * @return the currentAnimationState.
+ */
+ public AnimationUpdateState getAnimationUpdateState() {
+ return _currentAnimationState;
+ }
+
+ /**
+ * @param pose
+ * a pose to add to be updated by this manager.
+ */
+ public void addPose(final SkeletonPose pose) {
+ _applyToPoses.add(pose);
+ }
+
+ /**
+ * @param pose
+ * the pose to remove from this manager.
+ * @return true if the pose was found to be removed.
+ */
+ public boolean removePose(final SkeletonPose pose) {
+ return _applyToPoses.remove(pose);
+ }
+
+ /**
+ * @param pose
+ * a pose to look for
+ * @return true if the pose was found in this manager.
+ */
+ public boolean containsPose(final SkeletonPose pose) {
+ return _applyToPoses.contains(pose);
+ }
+
+ /**
+ * @return the number of poses managed by this manager.
+ */
+ public int getPoseCount() {
+ return _applyToPoses.size();
+ }
+
+ /**
+ * @param index
+ * the index to pull the pose from.
+ * @return pose at the given index
+ */
+ public SkeletonPose getSkeletonPose(final int index) {
+ return _applyToPoses.get(index);
+ }
+
+ /**
+ * @return the logic object responsible for taking animation data and applying it to skeleton poses.
+ */
+ public AnimationApplier getApplier() {
+ return _applier;
+ }
+
+ /**
+ * @param applier
+ * a logic object to be responsible for taking animation data and applying it to skeleton poses.
+ */
+ public void setApplier(final AnimationApplier applier) {
+ _applier = applier;
+ }
+
+ /**
+ * Move associated layers forward to the current global time and then apply the associated animation data to any
+ * SkeletonPoses set on the manager.
+ */
+ public void update() {
+
+ if (_currentAnimationState != AnimationUpdateState.Play) {
+ if (_resetClipsOnStop) {
+ if (_currentAnimationState == AnimationUpdateState.Stop && !_canSetStopState) {
+ _canSetStopState = true;
+ } else {
+ // pause state or reset update has occurred
+ return;
+ }
+ } else {
+ // stop update without reseting
+ return;
+ }
+ } else {
+ _canSetStopState = false;
+ }
+ // grab current global time
+ final double globalTime = _globalTimer.getTimeInSeconds();
+
+ // check throttle
+ if (_updateRate != 0.0) {
+ if (globalTime - _lastUpdate < _updateRate) {
+ return;
+ }
+
+ // we subtract a bit to maintain our desired rate, even if there are some gc pauses, etc.
+ _lastUpdate = globalTime - (globalTime - _lastUpdate) % _updateRate;
+ }
+
+ // move the time forward on the layers
+ for (int i = 0; i < _layers.size(); ++i) {
+ final AnimationLayer layer = _layers.get(i);
+ final AbstractFiniteState state = layer.getCurrentState();
+ if (state != null) {
+ state.update(globalTime, layer);
+ }
+ }
+
+ // call apply on blend module, passing in pose
+ if (!_applyToPoses.isEmpty()) {
+ for (int i = 0; i < _applyToPoses.size(); ++i) {
+ final SkeletonPose pose = _applyToPoses.get(i);
+ _applier.applyTo(pose, this);
+ }
+ }
+
+ // apply for non-pose related assets
+ _applier.apply(_sceneRoot, this);
+
+ // post update to clear states
+ for (int i = 0; i < _layers.size(); ++i) {
+ final AnimationLayer layer = _layers.get(i);
+ final AbstractFiniteState state = layer.getCurrentState();
+ if (state != null) {
+ state.postUpdate(layer);
+ }
+ }
+ }
+
+ /**
+ * Retrieve and track an instance of an animation clip to be used with this manager.
+ *
+ * @param clip
+ * the clip to instance.
+ * @return our new clip instance.
+ */
+ public AnimationClipInstance getClipInstance(final AnimationClip clip) {
+ AnimationClipInstance instance = _clipInstances.get(clip);
+ if (instance == null) {
+ instance = new AnimationClipInstance();
+ instance.setStartTime(_globalTimer.getTimeInSeconds());
+ _clipInstances.put(clip, instance);
+ }
+
+ return instance;
+ }
+
+ /**
+ * Retrieve an existing clip instance being tracked by this manager.
+ *
+ * @param clipName
+ * the name of the clip to find an existing instance of. Case sensitive.
+ * @return our existing clip instance, or null if we were not tracking a clip of the given name.
+ */
+ public AnimationClipInstance findClipInstance(final String clipName) {
+ for (final AnimationClip clip : _clipInstances.keySet()) {
+ if (clipName.equals(clip.getName())) {
+ return _clipInstances.get(clip);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Retrieve an existing clip tracked by this manager.
+ *
+ * @param clipName
+ * the name of the clip to find. Case sensitive.
+ * @return our existing clip, or null if we were not tracking a clip of the given name.
+ */
+ public AnimationClip findAnimationClip(final String clipName) {
+ for (final AnimationClip clip : _clipInstances.keySet()) {
+ if (clipName.equals(clip.getName())) {
+ return clip;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Rewind and reactivate the clip instance associated with the given clip.
+ *
+ * @param clip
+ * the clip to pull the instance for.
+ * @param globalStartTime
+ * the time to set the clip instance's start as.
+ */
+ public void resetClipInstance(final AnimationClip clip, final double globalStartTime) {
+ final AnimationClipInstance instance = getClipInstance(clip);
+ if (instance != null) {
+ instance.setStartTime(globalStartTime);
+ instance.setActive(true);
+ }
+ }
+
+ /**
+ * @param index
+ * the index of the layer to retrieve.
+ * @return the animation layer at that index, or null of index is outside the bounds of our list of layers.
+ */
+ public AnimationLayer getAnimationLayer(final int index) {
+ if (index < 0 || index >= _layers.size()) {
+ return null;
+ }
+ return _layers.get(index);
+ }
+
+ /**
+ * @param layerName
+ * the name of the layer to find.
+ * @return the first animation layer with a matching name, or null of none are found.
+ */
+ public AnimationLayer findAnimationLayer(final String layerName) {
+ for (final AnimationLayer layer : _layers) {
+ if (layerName.equals(layer.getName())) {
+ return layer;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Add a new layer to our list of animation layers.
+ *
+ * @param layer
+ * the layer to add.
+ * @return the index of our added layer in our list of animation layers.
+ */
+ public int addAnimationLayer(final AnimationLayer layer) {
+ _layers.add(layer);
+ layer.setManager(this);
+ return _layers.size() - 1;
+ }
+
+ /**
+ * Insert a given animation layer into our list of layers.
+ *
+ * @param layer
+ * the layer to insert.
+ * @param index
+ * the index to insert at. Moves any layers at that index over by one before inserting.
+ */
+ public void insertAnimationLayer(final AnimationLayer layer, final int index) {
+ _layers.add(index, layer);
+ layer.setManager(this);
+ }
+
+ /**
+ * @param layer
+ * a layer to remove.
+ * @return true if the layer is found to remove.
+ */
+ public boolean removeAnimationLayer(final AnimationLayer layer) {
+ return _layers.remove(layer);
+ }
+
+ /**
+ * @return our bottom most layer. This layer should always consist of a full skeletal pose data.
+ */
+ public AnimationLayer getBaseAnimationLayer() {
+ return _layers.get(0);
+ }
+
+ /**
+ * @return the amount of time in seconds between frame rate updates. (throttle) default is 60fps (1.0/60.0).
+ */
+ public double getUpdateRate() {
+ return _updateRate;
+ }
+
+ /**
+ * @param updateRate
+ * the new throttle rate. Default is 60fps (1.0/60.0). Set to 0 to disable throttling.
+ */
+ public void setUpdateRate(final double updateRate) {
+ _updateRate = updateRate;
+ }
+
+ /**
+ * @return the current source data from the layers of this manager.
+ */
+ public Map<String, ? extends Object> getCurrentSourceData() {
+ // set up our layer blending.
+ for (int i = 0; i < _layers.size() - 1; i++) {
+ final AnimationLayer layerA = _layers.get(i);
+ final AnimationLayer layerB = _layers.get(i + 1);
+ layerB.updateLayerBlending(layerA);
+ }
+
+ return _layers.get(_layers.size() - 1).getCurrentSourceData();
+ }
+
+ public LoggingMap<String, Double> getValuesStore() {
+ return _valuesStore;
+ }
+
+ /**
+ * @return the Map containing the AnimationClips and their respective AnimationClipInstances.
+ */
+ public Map<AnimationClip, AnimationClipInstance> getClipInstancesStore() {
+ return _clipInstances;
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/AnimationUpdateStateListener.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/AnimationUpdateStateListener.java
new file mode 100644
index 0000000..0e6a6fc
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/AnimationUpdateStateListener.java
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal;
+
+import com.ardor3d.extension.animation.skeletal.AnimationManager.AnimationUpdateState;
+
+public interface AnimationUpdateStateListener {
+
+ public void stateChanged(AnimationUpdateState oldState, AnimationUpdateState newState);
+
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/AttachmentPoint.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/AttachmentPoint.java
new file mode 100644
index 0000000..56e34ec
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/AttachmentPoint.java
@@ -0,0 +1,113 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal;
+
+import com.ardor3d.math.Transform;
+import com.ardor3d.math.type.ReadOnlyTransform;
+import com.ardor3d.scenegraph.Spatial;
+
+/**
+ * A pose listener whose purpose is to update the transform of a Spatial to align with a specific joint of the
+ * SkeletonPose we are listening to. Note that this is in the coordinate space of the pose, so the managed spatial
+ * should be a sibling to the skin mesh we are attaching to, or likewise similarly transformed.
+ */
+public class AttachmentPoint implements PoseListener {
+
+ /** The index of the joint we are listening to. */
+ private int _jointIndex;
+
+ /** The spatial we are moving around. */
+ private Spatial _attachment;
+
+ /** An offset from the joint's position to use when placing the spatial attachment. */
+ private final Transform _offset = new Transform();
+
+ /** A scratch-paper transform object for calculations. */
+ private final Transform _store = new Transform();
+
+ /** A name used to identify this attachment point. */
+ private String _name;
+
+ /**
+ * Create a new attachment point.
+ *
+ * @param name
+ * used to identify this attachment point.
+ */
+ public AttachmentPoint(final String name) {
+ setName(name);
+ }
+
+ /**
+ * Create a new attachment point.
+ *
+ * @param name
+ * used to identify this attachment point.
+ * @param jointIndex
+ * the joint index to listen to.
+ * @param attachment
+ * the spatial to manage transformation of.
+ * @param offset
+ * an offset to add to the joint's transform before updating the attachment spatial.
+ */
+ public AttachmentPoint(final String name, final int jointIndex, final Spatial attachment,
+ final ReadOnlyTransform offset) {
+ this(name);
+ setJointIndex(jointIndex);
+ setAttachment(attachment);
+ setOffset(offset);
+ }
+
+ public String getName() {
+ return _name;
+ }
+
+ public void setName(final String name) {
+ _name = name;
+ }
+
+ public Spatial getAttachment() {
+ return _attachment;
+ }
+
+ public void setAttachment(final Spatial attachment) {
+ _attachment = attachment;
+ }
+
+ public int getJointIndex() {
+ return _jointIndex;
+ }
+
+ public void setJointIndex(final int jointIndex) {
+ _jointIndex = jointIndex;
+ }
+
+ public ReadOnlyTransform getOffset() {
+ return _offset;
+ }
+
+ public void setOffset(final ReadOnlyTransform offset) {
+ _offset.set(offset);
+ }
+
+ /**
+ * Move our managed spatial to align with the referenced joint's position in the given pose, modified by our offset.
+ * See class javadoc for more information.
+ */
+ public void poseUpdated(final SkeletonPose pose) {
+ // only update if we have something attached.
+ if (_attachment != null) {
+ final Transform t = pose.getGlobalJointTransforms()[_jointIndex];
+ t.multiply(_offset, _store);
+ _attachment.setTransform(_store);
+ }
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/Joint.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/Joint.java
new file mode 100644
index 0000000..b5a0e4e
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/Joint.java
@@ -0,0 +1,135 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+
+import com.ardor3d.annotation.SavableFactory;
+import com.ardor3d.math.Transform;
+import com.ardor3d.math.type.ReadOnlyTransform;
+import com.ardor3d.util.export.InputCapsule;
+import com.ardor3d.util.export.OutputCapsule;
+import com.ardor3d.util.export.Savable;
+
+/**
+ * Representation of a Joint in a Skeleton. Meant to be used within a specific Skeleton object.
+ */
+@SavableFactory(factoryMethod = "initSavable")
+public class Joint implements Savable {
+ /** Root node ID */
+ public static final short NO_PARENT = Short.MIN_VALUE;
+
+ /** The inverse transform of this Joint in its bind position. */
+ private final Transform _inverseBindPose = new Transform(Transform.IDENTITY);
+
+ /** A name, for display or debugging purposes. */
+ private final String _name;
+
+ protected short _index;
+
+ /** Index of our parent Joint, or NO_PARENT if we are the root. */
+ protected short _parentIndex;
+
+ /**
+ * Construct a new Joint object using the given name.
+ *
+ * @param name
+ * the name
+ */
+ public Joint(final String name) {
+ _name = name;
+ }
+
+ /**
+ * @return the inverse of the joint space -> model space transformation.
+ */
+ public ReadOnlyTransform getInverseBindPose() {
+ return _inverseBindPose;
+ }
+
+ public void setInverseBindPose(final ReadOnlyTransform inverseBindPose) {
+ _inverseBindPose.set(inverseBindPose);
+ }
+
+ /**
+ * @return the human-readable name of this joint.
+ */
+ public String getName() {
+ return _name;
+ }
+
+ /**
+ * Set the index of this joint's parent within the containing Skeleton's joint array.
+ *
+ * @param parentIndex
+ * the index, or NO_PARENT if this Joint is root (has no parent)
+ */
+ public void setParentIndex(final short parentIndex) {
+ _parentIndex = parentIndex;
+ }
+
+ public short getParentIndex() {
+ return _parentIndex;
+ }
+
+ public void setIndex(final short index) {
+ _index = index;
+ }
+
+ public short getIndex() {
+ return _index;
+ }
+
+ @Override
+ public String toString() {
+ return "Joint: '" + getName() + "'";
+ }
+
+ // /////////////////
+ // Methods for Savable
+ // /////////////////
+
+ public Class<? extends Joint> getClassTag() {
+ return this.getClass();
+ }
+
+ public void write(final OutputCapsule capsule) throws IOException {
+ capsule.write(_name, "name", null);
+ capsule.write(_index, "index", (short) 0);
+ capsule.write(_parentIndex, "parentIndex", (short) 0);
+ capsule.write(_inverseBindPose, "inverseBindPose", (Savable) Transform.IDENTITY);
+ }
+
+ public void read(final InputCapsule capsule) throws IOException {
+ final String name = capsule.readString("name", null);
+ try {
+ final Field field1 = Joint.class.getDeclaredField("_name");
+ field1.setAccessible(true);
+ field1.set(this, name);
+ } catch (final Exception e) {
+ e.printStackTrace();
+ }
+
+ _index = capsule.readShort("index", (short) 0);
+ _parentIndex = capsule.readShort("parentIndex", (short) 0);
+
+ setInverseBindPose((ReadOnlyTransform) capsule.readSavable("inverseBindPose", (Savable) Transform.IDENTITY));
+ }
+
+ public static Joint initSavable() {
+ return new Joint();
+ }
+
+ protected Joint() {
+ _name = null;
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/PoseListener.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/PoseListener.java
new file mode 100644
index 0000000..780395c
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/PoseListener.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal;
+
+/**
+ * Describes a class interested in being notified of SkeletonPose updates.
+ */
+public interface PoseListener {
+
+ /**
+ * Call-back method on skeleton pose updates.
+ *
+ * @param pose
+ * the pose that was updated.
+ */
+ void poseUpdated(SkeletonPose pose);
+
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/Skeleton.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/Skeleton.java
new file mode 100644
index 0000000..ba4d69e
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/Skeleton.java
@@ -0,0 +1,116 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+
+import com.ardor3d.annotation.SavableFactory;
+import com.ardor3d.util.export.CapsuleUtils;
+import com.ardor3d.util.export.InputCapsule;
+import com.ardor3d.util.export.OutputCapsule;
+import com.ardor3d.util.export.Savable;
+
+/**
+ * Describes a collection of Joints. This class represents the hierarchy of a Skeleton and its original aspect (via the
+ * Joint class). This does not support posing the joints in any way... Use with a SkeletonPose to describe a skeleton in
+ * a specific pose.
+ */
+@SavableFactory(factoryMethod = "initSavable")
+public class Skeleton implements Savable {
+
+ /**
+ * An array of Joints associated with this Skeleton.
+ */
+ private final Joint[] _joints;
+
+ /** A name, for display or debugging purposes. */
+ private final String _name;
+
+ /**
+ *
+ * @param name
+ * A name, for display or debugging purposes
+ * @param joints
+ * An array of Joints associated with this Skeleton.
+ */
+ public Skeleton(final String name, final Joint[] joints) {
+ _name = name;
+ _joints = joints;
+ }
+
+ /**
+ * @return the human-readable name of this skeleton.
+ */
+ public String getName() {
+ return _name;
+ }
+
+ /**
+ * @return the array of Joints that make up this skeleton.
+ */
+ public Joint[] getJoints() {
+ return _joints;
+ }
+
+ /**
+ *
+ * @param jointName
+ * name of the joint to locate. Case sensitive.
+ * @return the index of the joint, if found, or -1 if not.
+ */
+ public int findJointByName(final String jointName) {
+ for (int i = 0; i < _joints.length; i++) {
+ if (jointName.equals(_joints[i].getName())) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ // /////////////////
+ // Methods for Savable
+ // /////////////////
+
+ public Class<? extends Skeleton> getClassTag() {
+ return this.getClass();
+ }
+
+ public void write(final OutputCapsule capsule) throws IOException {
+ capsule.write(_name, "name", null);
+ capsule.write(_joints, "joints", null);
+ }
+
+ public void read(final InputCapsule capsule) throws IOException {
+ final String name = capsule.readString("name", null);
+ final Joint[] joints = CapsuleUtils.asArray(capsule.readSavableArray("joints", null), Joint.class);
+ try {
+ final Field field1 = Skeleton.class.getDeclaredField("_name");
+ field1.setAccessible(true);
+ field1.set(this, name);
+
+ final Field field2 = Skeleton.class.getDeclaredField("_joints");
+ field2.setAccessible(true);
+ field2.set(this, joints);
+ } catch (final Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static Skeleton initSavable() {
+ return new Skeleton();
+ }
+
+ protected Skeleton() {
+ _name = null;
+ _joints = null;
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/SkeletonPose.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/SkeletonPose.java
new file mode 100644
index 0000000..cb1ea7a
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/SkeletonPose.java
@@ -0,0 +1,297 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.List;
+
+import com.ardor3d.annotation.SavableFactory;
+import com.ardor3d.math.Matrix4;
+import com.ardor3d.math.Transform;
+import com.ardor3d.util.export.CapsuleUtils;
+import com.ardor3d.util.export.InputCapsule;
+import com.ardor3d.util.export.OutputCapsule;
+import com.ardor3d.util.export.Savable;
+import com.google.common.collect.Lists;
+
+/**
+ * Joins a Skeleton with an array of joint poses. This allows the skeleton to exist and be reused between multiple
+ * instances of poses.
+ */
+@SavableFactory(factoryMethod = "initSavable")
+public class SkeletonPose implements Savable {
+
+ /** The skeleton being "posed". */
+ private final Skeleton _skeleton;
+
+ /** Local transforms for the joints of the associated skeleton. */
+ private final Transform[] _localTransforms;
+
+ /** Global transforms for the joints of the associated skeleton. Not saved to savable. */
+ private transient final Transform[] _globalTransforms;
+
+ /**
+ * A palette of matrices used in skin deformation - basically the global transform X the inverse bind pose
+ * transform. Not saved to savable.
+ */
+ private transient final Matrix4[] _matrixPalette;
+
+ /**
+ * The list of elements interested in notification when this SkeletonPose updates. Not saved to savable.
+ */
+ private transient final List<PoseListener> _poseListeners = Lists.newArrayListWithCapacity(1);
+
+ /**
+ * Construct a new SkeletonPose using the given Skeleton.
+ *
+ * @param skeleton
+ * the skeleton to use.
+ */
+ public SkeletonPose(final Skeleton skeleton) {
+ assert skeleton != null : "skeleton must not be null.";
+
+ _skeleton = skeleton;
+ final int jointCount = _skeleton.getJoints().length;
+
+ // init local transforms
+ _localTransforms = new Transform[jointCount];
+ for (int i = 0; i < jointCount; i++) {
+ _localTransforms[i] = new Transform();
+ }
+
+ // init global transforms
+ _globalTransforms = new Transform[jointCount];
+ for (int i = 0; i < jointCount; i++) {
+ _globalTransforms[i] = new Transform();
+ }
+
+ // init palette
+ _matrixPalette = new Matrix4[jointCount];
+ for (int i = 0; i < jointCount; i++) {
+ _matrixPalette[i] = new Matrix4();
+ }
+
+ // start off in bind pose.
+ setToBindPose();
+ }
+
+ /**
+ * @return the skeleton posed by this object.
+ */
+ public Skeleton getSkeleton() {
+ return _skeleton;
+ }
+
+ /**
+ * @return an array of local space transforms for each of the skeleton's joints.
+ */
+ public Transform[] getLocalJointTransforms() {
+ return _localTransforms;
+ }
+
+ /**
+ * @return an array of global space transforms for each of the skeleton's joints. This does not take into account
+ * any transformation of the SkeletonMesh using the pose.
+ */
+ public Transform[] getGlobalJointTransforms() {
+ return _globalTransforms;
+ }
+
+ /**
+ * @return an array of global space transforms for each of the skeleton's joints.
+ */
+ public Matrix4[] getMatrixPalette() {
+ return _matrixPalette;
+ }
+
+ /**
+ * Register a PoseListener on this SkeletonPose.
+ *
+ * @param listener
+ * the PoseListener
+ */
+ public void addPoseListener(final PoseListener listener) {
+ _poseListeners.add(listener);
+ }
+
+ /**
+ * Remove a PoseListener from this SkeletonPose.
+ *
+ * @param listener
+ * the PoseListener
+ */
+ public void removePoseListener(final PoseListener listener) {
+ _poseListeners.remove(listener);
+ }
+
+ /**
+ * Clear all PoseListeners registered on this SkeletonPose.
+ */
+ public void clearListeners() {
+ _poseListeners.clear();
+ }
+
+ /**
+ * Update the global and palette transforms of our posed joints based on the current local joint transforms.
+ */
+ public void updateTransforms() {
+ final Transform temp = Transform.fetchTempInstance();
+ // we go in update array order, which ensures parent global transforms are updated before child.
+ // final int[] orders = _skeleton.getJointOrders();
+ final int nrJoints = _skeleton.getJoints().length;
+ // for (int i = 0; i < orders.length; i++) {
+ for (int i = 0; i < nrJoints; i++) {
+ // the joint index
+ final int index = i;
+
+ // find our parent
+ final short parentIndex = _skeleton.getJoints()[index].getParentIndex();
+ if (parentIndex != Joint.NO_PARENT) {
+ // we have a parent, so take us from local->parent->model space by multiplying by parent's local->model
+ // space transform.
+ _globalTransforms[parentIndex].multiply(_localTransforms[index], _globalTransforms[index]);
+ } else {
+ // no parent so just set global to the local transform
+ _globalTransforms[index].set(_localTransforms[index]);
+ }
+
+ // at this point we have a local->model space transform for this joint, for skinning we multiply this by the
+ // joint's inverse bind pose (joint->model space, inverted). This gives us a transform that can take a
+ // vertex from bind pose (model space) to current pose (model space).
+ _globalTransforms[index].multiply(_skeleton.getJoints()[index].getInverseBindPose(), temp);
+ temp.getHomogeneousMatrix(_matrixPalette[index]);
+ }
+ Transform.releaseTempInstance(temp);
+ firePoseUpdated();
+ }
+
+ /**
+ * Update our local joint transforms so that they reflect the skeleton in bind pose.
+ */
+ public void setToBindPose() {
+ final Transform temp = Transform.fetchTempInstance();
+ // go through our local transforms
+ for (int i = 0; i < _localTransforms.length; i++) {
+ // Set us to the bind pose
+ _localTransforms[i].set(_skeleton.getJoints()[i].getInverseBindPose());
+ // then invert.
+ _localTransforms[i].invert(_localTransforms[i]);
+
+ // At this point we are in model space, so we need to remove our parent's transform (if we have one.)
+ final short parentIndex = _skeleton.getJoints()[i].getParentIndex();
+ if (parentIndex != Joint.NO_PARENT) {
+ // We remove the parent's transform simply by multiplying by its inverse bind pose. Done! :)
+ _skeleton.getJoints()[parentIndex].getInverseBindPose().multiply(_localTransforms[i], temp);
+ _localTransforms[i].set(temp);
+ }
+ }
+ Transform.releaseTempInstance(temp);
+ updateTransforms();
+ firePoseUpdated();
+ }
+
+ /**
+ * Notify any registered PoseListeners that this pose has been "updated".
+ */
+ public void firePoseUpdated() {
+ for (int i = _poseListeners.size(); --i >= 0;) {
+ // Pull out pose
+ final PoseListener listener = _poseListeners.get(i);
+
+ // notify
+ listener.poseUpdated(this);
+ }
+ }
+
+ public SkeletonPose makeCopy() {
+ final SkeletonPose copy = new SkeletonPose(_skeleton);
+
+ int i = 0;
+ for (final Transform t : _localTransforms) {
+ copy._localTransforms[i++] = t.clone();
+ }
+ i = 0;
+ for (final Transform t : _globalTransforms) {
+ copy._globalTransforms[i++] = t.clone();
+ }
+ i = 0;
+ for (final Matrix4 m : _matrixPalette) {
+ copy._matrixPalette[i++] = m.clone();
+ }
+
+ return copy;
+ }
+
+ // /////////////////
+ // Methods for Savable
+ // /////////////////
+
+ public Class<? extends SkeletonPose> getClassTag() {
+ return this.getClass();
+ }
+
+ public void write(final OutputCapsule capsule) throws IOException {
+ capsule.write(_skeleton, "skeleton", null);
+ capsule.write(_localTransforms, "localTransforms", null);
+ }
+
+ public void read(final InputCapsule capsule) throws IOException {
+ final Skeleton skeleton = (Skeleton) capsule.readSavable("skeleton", null);
+ final Transform[] localTransforms = CapsuleUtils.asArray(capsule.readSavableArray("localTransforms", null),
+ Transform.class);
+ try {
+ final Field field1 = SkeletonPose.class.getDeclaredField("_skeleton");
+ field1.setAccessible(true);
+ field1.set(this, skeleton);
+
+ final int jointCount = _skeleton.getJoints().length;
+
+ // init local transforms
+ final Field field2 = SkeletonPose.class.getDeclaredField("_localTransforms");
+ field2.setAccessible(true);
+ field2.set(this, localTransforms);
+
+ // init global transforms
+ final Transform[] globalTransforms = new Transform[jointCount];
+ for (int i = 0; i < jointCount; i++) {
+ globalTransforms[i] = new Transform();
+ }
+ final Field field3 = SkeletonPose.class.getDeclaredField("_globalTransforms");
+ field3.setAccessible(true);
+ field3.set(this, globalTransforms);
+
+ // init palette
+ final Matrix4[] matrixPalette = new Matrix4[jointCount];
+ for (int i = 0; i < jointCount; i++) {
+ matrixPalette[i] = new Matrix4();
+ }
+ final Field field4 = SkeletonPose.class.getDeclaredField("_matrixPalette");
+ field4.setAccessible(true);
+ field4.set(this, matrixPalette);
+
+ updateTransforms();
+ } catch (final Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static SkeletonPose initSavable() {
+ return new SkeletonPose();
+ }
+
+ protected SkeletonPose() {
+ _skeleton = null;
+ _localTransforms = null;
+ _globalTransforms = null;
+ _matrixPalette = null;
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/SkinPoseApplyLogic.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/SkinPoseApplyLogic.java
new file mode 100644
index 0000000..177a1fc
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/SkinPoseApplyLogic.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal;
+
+/**
+ * Custom logic for how a skin should react when it is told its pose has updated. This might include throttling skin
+ * application, ignoring skin application when the skin is outside of the camera view, etc.
+ */
+public interface SkinPoseApplyLogic {
+
+ /**
+ * Apply, in some way, the given pose to the given mesh.
+ *
+ * @param skinnedMesh
+ * the mesh to apply to.
+ * @param pose
+ * the pose to apply.
+ */
+ void doApply(SkinnedMesh skinnedMesh, SkeletonPose pose);
+
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/SkinnedMesh.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/SkinnedMesh.java
new file mode 100644
index 0000000..aef1d9a
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/SkinnedMesh.java
@@ -0,0 +1,756 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal;
+
+import java.io.IOException;
+import java.nio.FloatBuffer;
+import java.util.TreeSet;
+
+import com.ardor3d.bounding.CollisionTreeManager;
+import com.ardor3d.extension.animation.skeletal.util.SkinUtils;
+import com.ardor3d.math.Matrix4;
+import com.ardor3d.renderer.IndexMode;
+import com.ardor3d.renderer.Renderer;
+import com.ardor3d.renderer.state.GLSLShaderObjectsState;
+import com.ardor3d.scenegraph.FloatBufferData;
+import com.ardor3d.scenegraph.IndexBufferData;
+import com.ardor3d.scenegraph.Mesh;
+import com.ardor3d.scenegraph.MeshData;
+import com.ardor3d.util.export.InputCapsule;
+import com.ardor3d.util.export.OutputCapsule;
+import com.ardor3d.util.export.Savable;
+import com.ardor3d.util.geom.BufferUtils;
+import com.google.common.collect.Sets;
+
+/**
+ * Mesh supporting deformation via skeletal animation.
+ */
+public class SkinnedMesh extends Mesh implements PoseListener {
+
+ /**
+ * Number of weights per vertex.
+ */
+ protected int _weightsPerVert = 1;
+
+ /**
+ * If true and we are using gpu skinning, we'll reorder our weights for matrix attribute use.
+ */
+ protected boolean _gpuUseMatrixAttribute = false;
+
+ /**
+ * Size to pad our attributes to. If we are using matrices (see {@link #setGpuUseMatrixAttribute(boolean)}) then
+ * this is the size of an edge of the matrix. eg. 4 would mean either a vec4 or a mat4 object is expected in the
+ * shader.
+ */
+ protected int _gpuAttributeSize = 4;
+
+ /**
+ * Storage for per vertex joint indices. There should be "weightsPerVert" entries per vertex.
+ */
+ protected short[] _jointIndices;
+ protected FloatBufferData _jointIndicesBuf;
+
+ /**
+ * Storage for per vertex joint indices. These should already be normalized (all joints affecting the vertex add to
+ * 1.) There should be "weightsPerVert" entries per vertex.
+ */
+ protected float[] _weights;
+ protected FloatBufferData _weightsBuf;
+
+ /**
+ * The original bind pose form of this SkinnedMesh. When doing CPU skinning, this will be used as a source and the
+ * destination will go into the normal _meshData field for rendering. For GPU skinning, _meshData will be ignored
+ * and only _bindPose will be sent to the card.
+ */
+ protected MeshData _bindPoseData = new MeshData();
+
+ /**
+ * The current skeleton pose we are targeting.
+ */
+ protected SkeletonPose _currentPose;
+
+ /**
+ * Flag for switching between GPU and CPU skinning.
+ */
+ protected boolean _useGPU;
+
+ /**
+ * The shader state to update with GLSL attributes/uniforms related to GPU skinning. See class doc for more.
+ */
+ protected GLSLShaderObjectsState _gpuShader;
+
+ /**
+ * <p>
+ * Flag for enabling automatically updating the skin's model bound when the pose changes. Only effective in CPU
+ * skinning mode. Default is false as this is currently expensive.
+ * </p>
+ *
+ * XXX: If we can find a better way to update the bounds, maybe we should make this default to true or remove this
+ * altogether.
+ */
+ protected boolean _autoUpdateSkinBound = false;
+
+ /**
+ * Custom update apply logic.
+ */
+ protected SkinPoseApplyLogic _customApplier = null;
+
+ /**
+ * Constructs a new SkinnedMesh.
+ */
+ public SkinnedMesh() {
+ super();
+ }
+
+ /**
+ * Constructs a new SkinnedMesh with a given name.
+ *
+ * @param name
+ * the name of the skinned mesh.
+ */
+ public SkinnedMesh(final String name) {
+ super(name);
+ }
+
+ /**
+ * @return the bind pose MeshData object used by this skinned mesh.
+ */
+ public MeshData getBindPoseData() {
+ return _bindPoseData;
+ }
+
+ /**
+ * Sets the bind pose mesh data object used by this skinned mesh.
+ *
+ * @param poseData
+ * the new bind pose
+ */
+ public void setBindPoseData(final MeshData poseData) {
+ _bindPoseData = poseData;
+ }
+
+ /**
+ * @return the number of weights and jointIndices this skin uses per vertex.
+ */
+ public int getWeightsPerVert() {
+ return _weightsPerVert;
+ }
+
+ /**
+ * @param weightsPerVert
+ * the number of weights and jointIndices this skin should use per vertex. Make sure this value matches
+ * up with the contents of jointIndices and weights.
+ */
+ public void setWeightsPerVert(final int weightsPerVert) {
+ _weightsPerVert = weightsPerVert;
+ }
+
+ /**
+ * @return true if we should use a matrix to send joints and weights to a gpu shader.
+ */
+ public boolean isGpuUseMatrixAttribute() {
+ return _gpuUseMatrixAttribute;
+ }
+
+ /**
+ * @param useMatrix
+ * true if we should use a matrix to send joints and weights to a gpu shader.
+ */
+ public void setGpuUseMatrixAttribute(final boolean useMatrix) {
+ _gpuUseMatrixAttribute = useMatrix;
+ }
+
+ /**
+ * @return size to pad our attributes to. If we are using matrices (see {@link #setGpuUseMatrixAttribute(boolean)})
+ * then this is the size of an edge of the matrix. eg. 4 would mean either a vec4 or a mat4 object is
+ * expected in the shader.
+ */
+ public int getGpuAttributeSize() {
+ return _gpuAttributeSize;
+ }
+
+ /**
+ * @param size
+ * Size to pad our attributes to. If we are using matrices (see
+ * {@link #setGpuUseMatrixAttribute(boolean)}) then this is the size of an edge of the matrix. eg. 4
+ * would mean either a vec4 or a mat4 object is expected in the shader.
+ */
+ public void setGpuAttributeSize(final int size) {
+ _gpuAttributeSize = size;
+ }
+
+ /**
+ * @return this skinned mesh's joint influences as indices into a Skeleton's Joint array.
+ * @see #setJointIndices(short[])
+ */
+ public short[] getJointIndices() {
+ return _jointIndices;
+ }
+
+ /**
+ * Sets the joint indices used by this skinned mesh to compute mesh deformation. Each entry is interpreted as an
+ * 16bit signed integer index into a Skeleton's Joint.
+ *
+ * @param jointIndices
+ */
+ public void setJointIndices(final short[] jointIndices) {
+ _jointIndices = jointIndices;
+ if (_jointIndices != null && _jointIndicesBuf != null) {
+ recreateJointAttributeBuffer();
+ }
+ }
+
+ /**
+ * @return this skinned mesh's joint weights.
+ * @see #setWeights(FloatBuffer)
+ */
+ public float[] getWeights() {
+ return _weights;
+ }
+
+ /**
+ * Sets the joint weights used by this skinned mesh.
+ *
+ * @param weights
+ * the new weights.
+ */
+ public void setWeights(final float[] weights) {
+ _weights = weights;
+ if (_weights != null && _weightsBuf != null) {
+ recreateWeightAttributeBuffer();
+ }
+ }
+
+ /**
+ * @return a representation of the pose and skeleton to use for morphing this mesh.
+ */
+ public SkeletonPose getCurrentPose() {
+ return _currentPose;
+ }
+
+ /**
+ * @param currentPose
+ * the representation responsible for the pose and skeleton to use for morphing this mesh.
+ */
+ public void setCurrentPose(final SkeletonPose currentPose) {
+ if (_currentPose != null) {
+ _currentPose.removePoseListener(this);
+ }
+ _currentPose = currentPose;
+ if (_currentPose != null) {
+ _currentPose.addPoseListener(this);
+ }
+ }
+
+ /**
+ * @return true if we should automatically update our model bounds when our pose updates. If useGPU is true, bounds
+ * are ignored.
+ */
+ public boolean isAutoUpdateSkinBounds() {
+ return _autoUpdateSkinBound;
+ }
+
+ /**
+ * @param autoUpdateSkinBound
+ * true if we should automatically update our model bounds when our pose updates. If useGPU is true,
+ * bounds are ignored.
+ */
+ public void setAutoUpdateSkinBounds(final boolean autoUpdateSkinBound) {
+ _autoUpdateSkinBound = autoUpdateSkinBound;
+ }
+
+ /**
+ * @return true if we are doing skinning on the card (GPU) or false if on the CPU.
+ */
+ public boolean isUseGPU() {
+ return _useGPU;
+ }
+
+ /**
+ * This should be set after setting up gpu attribute params.
+ *
+ * @param useGPU
+ * true if we should do skinning on the card (GPU) or false if on the CPU.
+ */
+ public void setUseGPU(final boolean useGPU) {
+ _useGPU = useGPU;
+ }
+
+ protected void updateWeightsAndJointsOnGPUShader() {
+ if (_gpuShader != null) {
+ if (_weightsBuf == null) {
+ recreateWeightAttributeBuffer();
+ }
+ if (_jointIndicesBuf == null) {
+ recreateJointAttributeBuffer();
+ }
+ if (!_gpuUseMatrixAttribute) {
+ _gpuShader.setAttributePointer("Weights", getGpuAttributeSize(), false, 0, _weightsBuf);
+ _gpuShader.setAttributePointer("JointIDs", getGpuAttributeSize(), false, 0, _jointIndicesBuf);
+ } else {
+ _gpuShader.setAttributePointerMatrix("Weights", getGpuAttributeSize(), false, _weightsBuf);
+ _gpuShader.setAttributePointerMatrix("JointIDs", getGpuAttributeSize(), false, _jointIndicesBuf);
+ }
+ }
+ }
+
+ protected void updateJointPaletteOnGPUShader() {
+ if (_gpuShader != null && _currentPose != null) {
+ _gpuShader.setUniform("JointPalette", _currentPose.getMatrixPalette(), true);
+ }
+ }
+
+ /**
+ * @return the shader being used for GPU skinning. Must first have been set via
+ * {@link #setGPUShader(GLSLShaderObjectsState)}
+ */
+ public GLSLShaderObjectsState getGPUShader() {
+ return _gpuShader;
+ }
+
+ /**
+ * @param shaderState
+ * the shader to use for GPU skinning. Should be set up to accept vec4 attributes "Weights" and
+ * "JointIDs" and a mat4[] uniform called "JointPalette". Applies the renderstate to this mesh as well.
+ */
+ public void setGPUShader(final GLSLShaderObjectsState shaderState) {
+ _gpuShader = shaderState;
+ setRenderState(_gpuShader);
+ }
+
+ /**
+ * @return any custom apply logic set on this skin or null if default logic is used.
+ * @see #setCustomApplier(SkinPoseApplyLogic)
+ */
+ public SkinPoseApplyLogic getCustomApplier() {
+ return _customApplier;
+ }
+
+ /**
+ * Set custom logic for how this skin should react when it is told its pose has updated. This might include
+ * throttling skin application, ignoring skin application when the skin is outside of the camera view, etc. If null,
+ * (the default) the skin will always apply the new pose and optionally update the model bound.
+ *
+ * @param customApplier
+ * the new custom logic, or null to use the default behavior.
+ */
+ public void setCustomApplier(final SkinPoseApplyLogic customApplier) {
+ _customApplier = customApplier;
+ }
+
+ /**
+ * Apply skinning values for CPU skinning.
+ */
+ public void applyPose() {
+ if (!isUseGPU() && _currentPose != null) {
+ // Get a handle to the source and dest vertices buffers
+ final FloatBuffer bindVerts = _bindPoseData.getVertexBuffer();
+ FloatBuffer storeVerts = _meshData.getVertexBuffer();
+ bindVerts.rewind();
+ if (storeVerts == null || storeVerts.capacity() != bindVerts.capacity()) {
+ storeVerts = BufferUtils.createFloatBuffer(bindVerts.capacity());
+ _meshData.setVertexBuffer(storeVerts);
+ } else {
+ storeVerts.rewind();
+ }
+
+ // Get a handle to the source and dest normals buffers
+ final FloatBuffer bindNorms = _bindPoseData.getNormalBuffer();
+ FloatBuffer storeNorms = _meshData.getNormalBuffer();
+ if (bindNorms != null) {
+ bindNorms.rewind();
+
+ if (storeNorms == null || storeNorms.capacity() < bindNorms.capacity()) {
+ storeNorms = BufferUtils.createFloatBuffer(bindNorms.capacity());
+ _meshData.setNormalBuffer(storeNorms);
+ } else {
+ storeNorms.rewind();
+ }
+ }
+
+ Matrix4 jntMat;
+ double bindVX, bindVY, bindVZ;
+ double bindNX = 0, bindNY = 0, bindNZ = 0;
+ double vSumX, vSumY, vSumZ;
+ double nSumX = 0, nSumY = 0, nSumZ = 0;
+ double tempX, tempY, tempZ;
+ float weight;
+ int jointIndex;
+
+ // Cycle through each vertex
+ for (int i = 0; i < _bindPoseData.getVertexCount(); i++) {
+ // zero out our sum var
+ vSumX = 0;
+ vSumY = 0;
+ vSumZ = 0;
+
+ // Grab the bind pose vertex Vbp from _bindPoseData
+ bindVX = bindVerts.get();
+ bindVY = bindVerts.get();
+ bindVZ = bindVerts.get();
+
+ // See if we should do the corresponding normal as well
+ if (bindNorms != null) {
+ // zero out our sum var
+ nSumX = 0;
+ nSumY = 0;
+ nSumZ = 0;
+
+ // Grab the bind pose norm Nbp from _bindPoseData
+ bindNX = bindNorms.get();
+ bindNY = bindNorms.get();
+ bindNZ = bindNorms.get();
+ }
+
+ // for each joint where the weight != 0
+ for (int j = 0; j < getWeightsPerVert(); j++) {
+ final int index = i * getWeightsPerVert() + j;
+ if (_weights[index] == 0) {
+ continue;
+ }
+
+ jointIndex = _jointIndices[index];
+ jntMat = _currentPose.getMatrixPalette()[jointIndex];
+ weight = _weights[index];
+
+ // Multiply our vertex by the matrix palette entry
+ tempX = jntMat.getM00() * bindVX + jntMat.getM01() * bindVY + jntMat.getM02() * bindVZ
+ + jntMat.getM03();
+ tempY = jntMat.getM10() * bindVX + jntMat.getM11() * bindVY + jntMat.getM12() * bindVZ
+ + jntMat.getM13();
+ tempZ = jntMat.getM20() * bindVX + jntMat.getM21() * bindVY + jntMat.getM22() * bindVZ
+ + jntMat.getM23();
+
+ // Sum, weighted.
+ vSumX += tempX * weight;
+ vSumY += tempY * weight;
+ vSumZ += tempZ * weight;
+
+ if (bindNorms != null) {
+ // Multiply our normal by the matrix palette entry
+ tempX = jntMat.getM00() * bindNX + jntMat.getM01() * bindNY + jntMat.getM02() * bindNZ;
+ tempY = jntMat.getM10() * bindNX + jntMat.getM11() * bindNY + jntMat.getM12() * bindNZ;
+ tempZ = jntMat.getM20() * bindNX + jntMat.getM21() * bindNY + jntMat.getM22() * bindNZ;
+
+ // Sum, weighted.
+ nSumX += tempX * weight;
+ nSumY += tempY * weight;
+ nSumZ += tempZ * weight;
+ }
+ }
+
+ // Store sum into _meshData
+ storeVerts.put((float) vSumX).put((float) vSumY).put((float) vSumZ);
+
+ if (bindNorms != null) {
+ storeNorms.put((float) nSumX).put((float) nSumY).put((float) nSumZ);
+ }
+ }
+
+ _meshData.getVertexCoords().setNeedsRefresh(true);
+ if (bindNorms != null) {
+ _meshData.getNormalCoords().setNeedsRefresh(true);
+ }
+ }
+ }
+
+ /**
+ * Override render to allow for GPU/CPU switch
+ */
+ @Override
+ public void render(final Renderer renderer) {
+ if (!_useGPU) {
+ // render as normal
+ super.render(renderer);
+ } else {
+ // update shader attributes / uniforms.
+ updateWeightsAndJointsOnGPUShader();
+ updateJointPaletteOnGPUShader();
+
+ // render using the bind pose.
+ super.render(renderer, getBindPoseData());
+ }
+ }
+
+ /**
+ * Calls to apply our pose on pose update.
+ */
+ public void poseUpdated(final SkeletonPose pose) {
+ // custom behavior?
+ if (_customApplier != null) {
+ _customApplier.doApply(this, pose);
+ }
+
+ // Just run our default behavior
+ else {
+ // update our pose
+ applyPose();
+
+ // update our model bounds
+ if (!isUseGPU() && isAutoUpdateSkinBounds()) {
+ updateModelBound();
+ }
+ }
+ }
+
+ @Override
+ public void updateModelBound() {
+ super.updateModelBound();
+ // if we make our model bound accurate, also make the collision tree accurate
+ CollisionTreeManager.INSTANCE.removeCollisionTree(this);
+ }
+
+ public void recreateJointAttributeBuffer() {
+ final float[] data;
+ if (isGpuUseMatrixAttribute()) {
+ data = SkinUtils.reorderAndPad(SkinUtils.convertToFloat(_jointIndices), getWeightsPerVert(),
+ getGpuAttributeSize());
+ } else {
+ data = SkinUtils.pad(SkinUtils.convertToFloat(_jointIndices), getWeightsPerVert(), getGpuAttributeSize());
+ }
+ _jointIndicesBuf = new FloatBufferData(BufferUtils.createFloatBuffer(data), getGpuAttributeSize());
+ }
+
+ public void recreateWeightAttributeBuffer() {
+ final float[] data;
+ if (isGpuUseMatrixAttribute()) {
+ data = SkinUtils.reorderAndPad(_weights, getWeightsPerVert(), getGpuAttributeSize());
+ } else {
+ data = SkinUtils.pad(_weights, getWeightsPerVert(), getGpuAttributeSize());
+ }
+ _weightsBuf = new FloatBufferData(BufferUtils.createFloatBuffer(data), getGpuAttributeSize());
+ }
+
+ @Override
+ public SkinnedMesh makeCopy(final boolean shareGeometricData) {
+ final SkinnedMesh skin = (SkinnedMesh) super.makeCopy(shareGeometricData);
+
+ // we don't want to share mesh data, just bind pose
+ if (shareGeometricData) {
+ // overriding parent's reuse
+ skin._meshData = _meshData.makeCopy();
+ // reuse
+ skin._bindPoseData = _bindPoseData;
+ } else {
+ skin._bindPoseData = _bindPoseData.makeCopy();
+ }
+
+ skin._weightsPerVert = _weightsPerVert;
+ skin._useGPU = _useGPU;
+ skin._gpuShader = _gpuShader;
+ skin._gpuUseMatrixAttribute = _gpuUseMatrixAttribute;
+ skin._gpuAttributeSize = _gpuAttributeSize;
+ skin._autoUpdateSkinBound = _autoUpdateSkinBound;
+ skin._customApplier = _customApplier;
+
+ // bring across arrays
+ if (shareGeometricData) {
+ skin._weights = _weights;
+ skin._weightsBuf = _weightsBuf;
+ skin._jointIndices = _jointIndices;
+ skin._jointIndicesBuf = _jointIndicesBuf;
+ } else {
+ skin._weights = new float[_weights.length];
+ System.arraycopy(_weights, 0, skin._weights, 0, _weights.length);
+ skin._jointIndices = new short[_jointIndices.length];
+ System.arraycopy(_jointIndices, 0, skin._jointIndices, 0, _jointIndices.length);
+ }
+
+ skin._currentPose = _currentPose;
+
+ // make sure pose listener added
+ if (skin._currentPose != null) {
+ skin._currentPose.addPoseListener(skin);
+ }
+
+ return skin;
+ }
+
+ @Override
+ public void reorderIndices(final IndexBufferData<?> newIndices, final IndexMode[] modes, final int[] lengths) {
+ super.reorderIndices(newIndices, modes, lengths);
+ _bindPoseData.setIndices(newIndices);
+ _bindPoseData.setIndexModes(modes);
+ _bindPoseData.setIndexLengths(lengths);
+ }
+
+ @Override
+ public void reorderVertexData(final int[] newVertexOrder) {
+ if (_meshData != null) {
+ reorderVertexData(newVertexOrder, _meshData);
+ }
+ reorderVertexData(newVertexOrder, _bindPoseData);
+
+ // reorder weight/joint information
+ final float[] weights = new float[_weights.length];
+ final short[] jointIndices = new short[_jointIndices.length];
+
+ for (int i = 0; i < _bindPoseData.getVertexCount(); i++) {
+ for (int j = 0; j < _weightsPerVert; j++) {
+ final int oldIndex = i * _weightsPerVert + j;
+ final int newIndex = newVertexOrder[i] * _weightsPerVert + j;
+ weights[newIndex] = _weights[oldIndex];
+ jointIndices[newIndex] = _jointIndices[oldIndex];
+ }
+ }
+
+ setWeights(weights);
+ setJointIndices(jointIndices);
+ }
+
+ /**
+ * Rewrites the weights on this SkinnedMesh, if necessary, to reduce the number of weights per vert to the given
+ * max. This is done by dropping the least significant weight and balancing the remainder to total 1.0 again.
+ *
+ * @param maxCount
+ * the desired maximum weightsPerVert. If this is >= the current weightsPerVert, this method is a NOOP.
+ */
+ public void constrainWeightCount(final int maxCount) {
+ if (maxCount >= _weightsPerVert) {
+ return;
+ }
+
+ // Generate new joint and weight buffers
+ final int vcount = _weights.length / _weightsPerVert;
+ final short[] joints = new short[vcount * maxCount];
+ final float[] weights = new float[vcount * maxCount];
+
+ final TreeSet<JointWeight> weightSort = Sets.newTreeSet();
+ // Walk through old data vertex by vertex
+ int index;
+ for (int i = 0; i < vcount; i++) {
+ weightSort.clear();
+ for (int j = 0; j < _weightsPerVert; j++) {
+ index = i * _weightsPerVert + j;
+ weightSort.add(new JointWeight(_jointIndices[index], _weights[index]));
+ }
+ // go through and grab the top values
+ float totalWeight = 0;
+ index = 0;
+ for (final JointWeight jw : weightSort) {
+ if (index < maxCount) {
+ if (jw.weight > 0) {
+ totalWeight += jw.weight;
+ joints[i * maxCount + index] = jw.joint;
+ weights[i * maxCount + index] = jw.weight;
+ index++;
+ }
+ } else {
+ break;
+ }
+ }
+ if (totalWeight > 0) {
+ // normalize
+ for (int j = 0; j < maxCount; j++) {
+ weights[i * maxCount + j] /= totalWeight;
+ }
+ }
+ }
+ _weightsPerVert = maxCount;
+ setJointIndices(joints);
+ setWeights(weights);
+ }
+
+ // /////////////////
+ // Methods for Savable
+ // /////////////////
+
+ @Override
+ public Class<? extends SkinnedMesh> getClassTag() {
+ return this.getClass();
+ }
+
+ @Override
+ public void write(final OutputCapsule capsule) throws IOException {
+ super.write(capsule);
+ capsule.write(_weightsPerVert, "weightsPerVert", 1);
+ capsule.write(_jointIndices, "jointIndices", null);
+ capsule.write(_weights, "weights", null);
+ capsule.write(_bindPoseData, "bindPoseData", null);
+ capsule.write(_currentPose, "currentPose", null);
+ capsule.write(_useGPU, "useGPU", false);
+ capsule.write(_gpuShader, "gpuShader", null);
+ capsule.write(_gpuAttributeSize, "gpuAttributeSize", 4);
+ capsule.write(_gpuUseMatrixAttribute, "gpuUseMatrixAttribute", false);
+ capsule.write(_autoUpdateSkinBound, "autoUpdateSkinBound", false);
+ if (_customApplier instanceof Savable) {
+ capsule.write((Savable) _customApplier, "customApplier", null);
+ }
+ }
+
+ @Override
+ public void read(final InputCapsule capsule) throws IOException {
+ super.read(capsule);
+ _weightsPerVert = capsule.readInt("weightsPerVert", 1);
+ _jointIndices = capsule.readShortArray("jointIndices", null);
+ _weights = capsule.readFloatArray("weights", null);
+ _bindPoseData = (MeshData) capsule.readSavable("bindPoseData", null);
+ _currentPose = (SkeletonPose) capsule.readSavable("currentPose", null);
+ _useGPU = capsule.readBoolean("useGPU", false);
+ _gpuShader = (GLSLShaderObjectsState) capsule.readSavable("gpuShader", null);
+ _gpuAttributeSize = capsule.readInt("gpuAttributeSize", 4);
+ _gpuUseMatrixAttribute = capsule.readBoolean("gpuUseMatrixAttribute", false);
+ _autoUpdateSkinBound = capsule.readBoolean("autoUpdateSkinBound", false);
+ final SkinPoseApplyLogic customApplier = (SkinPoseApplyLogic) capsule.readSavable("customApplier", null);
+ if (customApplier != null) {
+ _customApplier = customApplier;
+ }
+
+ // make sure pose listener added
+ if (_currentPose != null) {
+ _currentPose.addPoseListener(this);
+ }
+ }
+
+ class JointWeight implements Comparable<JointWeight> {
+ short joint;
+ float weight;
+
+ public JointWeight(final short joint, final float weight) {
+ this.joint = joint;
+ this.weight = weight;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+
+ // only care about joint
+ result += 31 * result + joint;
+
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof JointWeight)) {
+ return false;
+ }
+ final JointWeight comp = (JointWeight) o;
+ // only care about joint
+ return joint == comp.joint;
+ }
+
+ @Override
+ public int compareTo(final JointWeight o) {
+ if (o.weight < weight) {
+ return -1;
+ } else if (o.weight > weight) {
+ return 1;
+ } else {
+ return o.joint - joint;
+ }
+ }
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/SkinnedMeshCombineLogic.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/SkinnedMeshCombineLogic.java
new file mode 100644
index 0000000..9b57f10
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/SkinnedMeshCombineLogic.java
@@ -0,0 +1,85 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal;
+
+import com.ardor3d.extension.animation.skeletal.util.SkinUtils;
+import com.ardor3d.scenegraph.Mesh;
+import com.ardor3d.util.geom.MeshCombiner.MeshCombineLogic;
+
+/**
+ * Logic for combining multiple SkinnedMesh objects into one. Should only be used where the pose of each SkinnedMesh is
+ * the same.
+ */
+public class SkinnedMeshCombineLogic extends MeshCombineLogic {
+ protected int totalWeightJointEntries = 0, weightsPerVert = 0;
+ private final SkinnedMesh _mesh;
+
+ public SkinnedMeshCombineLogic() {
+ this("combined");
+ }
+
+ public SkinnedMeshCombineLogic(final String name) {
+ _mesh = new SkinnedMesh(name);
+ _mesh.setBindPoseData(data);
+ }
+
+ @Override
+ public SkinnedMesh getMesh() {
+ // copy and set here so it's the right size.
+ _mesh.setMeshData(data.makeCopy());
+ _mesh.setWeightsPerVert(weightsPerVert);
+ return _mesh;
+ }
+
+ @Override
+ public void addSource(final Mesh mesh) {
+ if (!(mesh instanceof SkinnedMesh)) {
+ return;
+ }
+
+ final SkinnedMesh skMesh = (SkinnedMesh) mesh;
+ if (first) {
+ _mesh.setCurrentPose(skMesh.getCurrentPose());
+ }
+
+ super.addSource(skMesh);
+
+ weightsPerVert = Math.max(weightsPerVert, skMesh.getWeightsPerVert());
+ totalWeightJointEntries += skMesh.getWeights().length / skMesh.getWeightsPerVert();
+ }
+
+ @Override
+ public void initDataBuffers() {
+ super.initDataBuffers();
+
+ // init weight/joint indices
+ final int length = totalWeightJointEntries * weightsPerVert;
+ _mesh.setWeights(new float[length]);
+ _mesh.setJointIndices(new short[length]);
+ }
+
+ @Override
+ public void combineSources() {
+ super.combineSources();
+
+ // combine weights
+ int currentIndex = 0;
+ for (final Mesh m : sources) {
+ final SkinnedMesh skMesh = (SkinnedMesh) m;
+ final float[] wData = SkinUtils.pad(skMesh.getWeights(), skMesh.getWeightsPerVert(), weightsPerVert);
+ final short[] jData = SkinUtils.pad(skMesh.getJointIndices(), skMesh.getWeightsPerVert(), weightsPerVert);
+
+ System.arraycopy(wData, 0, _mesh.getWeights(), currentIndex, wData.length);
+ System.arraycopy(jData, 0, _mesh.getJointIndices(), currentIndex, jData.length);
+ currentIndex += wData.length;
+ }
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/AbstractTwoPartSource.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/AbstractTwoPartSource.java
new file mode 100644
index 0000000..677520e
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/AbstractTwoPartSource.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.blendtree;
+
+/**
+ * Abstract blend tree source that takes two child sources and a blend weight [0.0, 1.0]. The subclass is responsible
+ * for implementing how these two sources are combined.
+ */
+public abstract class AbstractTwoPartSource implements BlendTreeSource {
+ /** Our first source. */
+ private BlendTreeSource _sourceA;
+
+ /** Our second source. */
+ private BlendTreeSource _sourceB;
+
+ /**
+ * A key into the related AnimationManager's values store for pulling blend weighting. What blend weighting is used
+ * for is up to the subclass.
+ */
+ private String _blendKey;
+
+ public BlendTreeSource getSourceA() {
+ return _sourceA;
+ }
+
+ public BlendTreeSource getSourceB() {
+ return _sourceB;
+ }
+
+ public String getBlendKey() {
+ return _blendKey;
+ }
+
+ public void setBlendKey(final String blendKey) {
+ _blendKey = blendKey;
+ }
+
+ public void setSourceA(final BlendTreeSource sourceA) {
+ _sourceA = sourceA;
+ }
+
+ public void setSourceB(final BlendTreeSource sourceB) {
+ _sourceB = sourceB;
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/BinaryLERPSource.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/BinaryLERPSource.java
new file mode 100644
index 0000000..fec63a8
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/BinaryLERPSource.java
@@ -0,0 +1,192 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.blendtree;
+
+import java.util.Map;
+import java.util.Map.Entry;
+
+import com.ardor3d.extension.animation.skeletal.AnimationManager;
+import com.ardor3d.extension.animation.skeletal.clip.TransformData;
+import com.ardor3d.math.MathUtils;
+import com.google.common.collect.Maps;
+
+/**
+ * <p>
+ * Takes two blend sources and uses linear interpolation to merge TransformData values. If one of the sources is null,
+ * or does not have a key that the other does, we disregard weighting and use the non-null side's full value.
+ * </p>
+ * <p>
+ * Source data that is not TransformData is not combined, rather A's value will always be used unless it is null.
+ * </p>
+ */
+public class BinaryLERPSource extends AbstractTwoPartSource {
+
+ /**
+ * Construct a new lerp source. The two sub sources should be set separately before use.
+ */
+ public BinaryLERPSource() {}
+
+ /**
+ * Construct a new lerp source using the supplied sources.
+ *
+ * @param sourceA
+ * our first source.
+ * @param sourceB
+ * our second source.
+ */
+ public BinaryLERPSource(final BlendTreeSource sourceA, final BlendTreeSource sourceB) {
+ setSourceA(sourceA);
+ setSourceB(sourceB);
+ }
+
+ public Map<String, ? extends Object> getSourceData(final AnimationManager manager) {
+ // grab our data maps from the two sources
+ final Map<String, ? extends Object> sourceAData = getSourceA() != null ? getSourceA().getSourceData(manager)
+ : null;
+ final Map<String, ? extends Object> sourceBData = getSourceB() != null ? getSourceB().getSourceData(manager)
+ : null;
+
+ return BinaryLERPSource
+ .combineSourceData(sourceAData, sourceBData, manager.getValuesStore().get(getBlendKey()));
+ }
+
+ public boolean setTime(final double globalTime, final AnimationManager manager) {
+ // set our time on the two sub sources
+ boolean foundActive = false;
+ if (getSourceA() != null) {
+ foundActive |= getSourceA().setTime(globalTime, manager);
+ }
+ if (getSourceB() != null) {
+ foundActive |= getSourceB().setTime(globalTime, manager);
+ }
+ return foundActive;
+ }
+
+ public void resetClips(final AnimationManager manager, final double globalStartTime) {
+ // reset our two sub sources
+ if (getSourceA() != null) {
+ getSourceA().resetClips(manager, globalStartTime);
+ }
+ if (getSourceB() != null) {
+ getSourceB().resetClips(manager, globalStartTime);
+ }
+ }
+
+ /**
+ * Combines two sets of source data maps by matching elements with the same key. Map values of type TransformData
+ * are combined via linear interpolation. Other value types are not combined, rather the value from source A is used
+ * unless null. Keys that exist only in one map or the other are preserved in the resulting map.
+ *
+ * @param sourceAData
+ * our first source map
+ * @param sourceBData
+ * our second source map
+ * @param blendWeight
+ * our blend weight - used to perform linear interpolation on TransformData values.
+ * @return our combined data map.
+ */
+ public static Map<String, ? extends Object> combineSourceData(final Map<String, ? extends Object> sourceAData,
+ final Map<String, ? extends Object> sourceBData, final Double blendWeight) {
+ return BinaryLERPSource.combineSourceData(sourceAData, sourceBData, blendWeight, null);
+ }
+
+ public static Map<String, ? extends Object> combineSourceData(final Map<String, ? extends Object> sourceAData,
+ final Map<String, ? extends Object> sourceBData, final double blendWeight, final Map<String, Object> store) {
+ // XXX: Should blendWeight of 0 or 1 disable non transform data from B/A respectively? Currently blendWeight is
+ // ignored in such.
+
+ if (sourceBData == null) {
+ return sourceAData;
+ } else if (sourceAData == null) {
+ return sourceBData;
+ }
+
+ Map<String, Object> rVal = store;
+ if (rVal == null) {
+ rVal = Maps.newHashMap();
+ }
+
+ for (final Entry<String, ? extends Object> entryAData : sourceAData.entrySet()) {
+ final String key = entryAData.getKey();
+ final Object dataA = entryAData.getValue();
+ final Object dataB = sourceBData.get(key);
+ if (dataA instanceof float[]) {
+ BinaryLERPSource.blendFloatValue(rVal, key, blendWeight, (float[]) dataA, (float[]) dataB);
+ continue;
+ } else if (dataA instanceof double[]) {
+ BinaryLERPSource.blendDoubleValue(rVal, key, blendWeight, (double[]) dataA, (double[]) dataB);
+ continue;
+ } else if (!(dataA instanceof TransformData)) {
+ // A will always override if not null.
+ rVal.put(key, dataA);
+ continue;
+ }
+
+ // Grab the transform data for each clip
+ final TransformData transformA = (TransformData) dataA;
+ final TransformData transformB = (TransformData) dataB;
+ if (transformB != null) {
+ rVal.put(key, transformA.blend(transformB, blendWeight, (TransformData) rVal.get(key)));
+ } else {
+ rVal.put(key, transformA);
+ }
+ }
+ for (final Entry<String, ? extends Object> entryBData : sourceBData.entrySet()) {
+ final String key = entryBData.getKey();
+ if (rVal.containsKey(key)) {
+ continue;
+ }
+ rVal.put(key, entryBData.getValue());
+ }
+
+ return rVal;
+ }
+
+ protected static void blendFloatValue(final Map<String, Object> rVal, final String key, final double blendWeight,
+ final float[] dataA, final float[] dataB) {
+ if (dataB == null) {
+ rVal.put(key, dataA);
+ } else {
+ float[] store = (float[]) rVal.get(key);
+ if (store == null) {
+ store = new float[1];
+ rVal.put(key, store);
+ }
+ store[0] = MathUtils.lerp((float) blendWeight, dataA[0], dataB[0]);
+ }
+ }
+
+ protected static void blendDoubleValue(final Map<String, Object> rVal, final String key, final double blendWeight,
+ final double[] dataA, final double[] dataB) {
+ if (dataB == null) {
+ rVal.put(key, dataA);
+ } else {
+ double[] store = (double[]) rVal.get(key);
+ if (store == null) {
+ store = new double[1];
+ rVal.put(key, store);
+ }
+ store[0] = MathUtils.lerp(blendWeight, dataA[0], dataB[0]);
+ }
+ }
+
+ @Override
+ public boolean isActive(final AnimationManager manager) {
+ boolean foundActive = false;
+ if (getSourceA() != null) {
+ foundActive |= getSourceA().isActive(manager);
+ }
+ if (getSourceB() != null) {
+ foundActive |= getSourceB().isActive(manager);
+ }
+ return foundActive;
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/BlendTreeSource.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/BlendTreeSource.java
new file mode 100644
index 0000000..7dfcbb9
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/BlendTreeSource.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.blendtree;
+
+import java.util.Map;
+
+import com.ardor3d.extension.animation.skeletal.AnimationManager;
+
+/**
+ * Represents a node in a blend tree.
+ */
+public interface BlendTreeSource {
+
+ /**
+ * @param manager
+ * the manager this is being called from.
+ * @return a map of source information from the blend tree node.
+ */
+ Map<String, ? extends Object> getSourceData(AnimationManager manager);
+
+ /**
+ * Move any clips or animation information to the given global time.
+ *
+ * @param globalTime
+ * our new "global" timeline time.
+ * @param manager
+ * the manager this is being called from.
+ * @return true if we found at least one active clip in the tree
+ */
+ boolean setTime(double globalTime, AnimationManager manager);
+
+ /**
+ * Reset any clips in this tree. This sets the start time to the given time and sets it active.
+ *
+ * @param manager
+ * the manager to use in resetting the clips.
+ * @param globalStartTime
+ * the new start time to use.
+ */
+ void resetClips(AnimationManager manager, double globalStartTime);
+
+ /**
+ * Check if there are still active clips in the tree.
+ *
+ * @param manager
+ * the manager this is being called from.
+ * @return true if we found at least one active clip in the tree
+ */
+ boolean isActive(AnimationManager manager);
+
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/ClipSource.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/ClipSource.java
new file mode 100644
index 0000000..9674c8f
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/ClipSource.java
@@ -0,0 +1,110 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.blendtree;
+
+import java.util.Map;
+
+import com.ardor3d.extension.animation.skeletal.AnimationManager;
+import com.ardor3d.extension.animation.skeletal.clip.AnimationClip;
+import com.ardor3d.extension.animation.skeletal.clip.AnimationClipInstance;
+import com.ardor3d.math.MathUtils;
+
+/**
+ * A blend tree leaf node that samples and returns values from the channels of an AnimationClip.
+ */
+public class ClipSource implements BlendTreeSource {
+
+ /** Our clip to sample from. This may be shared with other clip sources, etc. */
+ protected AnimationClip _clip;
+
+ /**
+ * Construct a new ClipSource. Clip and Manager must be set separately before use.
+ */
+ public ClipSource() {}
+
+ /**
+ * Construct a new ClipSource using the given data.
+ *
+ * @param clip
+ * the clip to use.
+ * @param manager
+ * the manager to track clip state with.
+ */
+ public ClipSource(final AnimationClip clip, final AnimationManager manager) {
+ setClip(clip);
+
+ // init instance
+ manager.getClipInstance(clip);
+ }
+
+ public AnimationClip getClip() {
+ return _clip;
+ }
+
+ public void setClip(final AnimationClip clip) {
+ _clip = clip;
+ }
+
+ @Override
+ public Map<String, ? extends Object> getSourceData(final AnimationManager manager) {
+ return manager.getClipInstance(getClip()).getChannelData();
+ }
+
+ /**
+ * Sets the current time on our AnimationClip instance, accounting for looping and time scaling.
+ */
+ public boolean setTime(final double globalTime, final AnimationManager manager) {
+ final AnimationClipInstance instance = manager.getClipInstance(_clip);
+ if (instance.isActive()) {
+ double clockTime = instance.getTimeScale() * (globalTime - instance.getStartTime());
+
+ final double maxTime = _clip.getMaxTimeIndex();
+ if (maxTime <= 0) {
+ return false;
+ }
+
+ // Check for looping.
+ if (instance.getLoopCount() == Integer.MAX_VALUE || instance.getLoopCount() > 1
+ && maxTime * instance.getLoopCount() >= Math.abs(clockTime)) {
+ if (clockTime < 0) {
+ clockTime = maxTime + clockTime % maxTime;
+ } else {
+ clockTime %= maxTime;
+ }
+ } else if (clockTime < 0) {
+ clockTime = maxTime + clockTime;
+ }
+
+ // Check for past max time
+ if (clockTime > maxTime || clockTime < 0) {
+ clockTime = MathUtils.clamp(clockTime, 0, maxTime);
+ // signal to any listeners that we have ended our animation.
+ instance.fireAnimationFinished();
+ // deactivate this instance of the clip
+ instance.setActive(false);
+ }
+
+ // update the clip with the correct clip local time.
+ _clip.update(clockTime, instance);
+ }
+ return instance.isActive();
+ }
+
+ public void resetClips(final AnimationManager manager, final double globalStartTime) {
+ manager.resetClipInstance(_clip, globalStartTime);
+ }
+
+ @Override
+ public boolean isActive(final AnimationManager manager) {
+ final AnimationClipInstance instance = manager.getClipInstance(_clip);
+ return instance.isActive() && _clip.getMaxTimeIndex() > 0;
+ }
+} \ No newline at end of file
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/ExclusiveClipSource.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/ExclusiveClipSource.java
new file mode 100644
index 0000000..3d65439
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/ExclusiveClipSource.java
@@ -0,0 +1,103 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.blendtree;
+
+import java.util.List;
+import java.util.Map;
+
+import com.ardor3d.extension.animation.skeletal.AnimationManager;
+import com.ardor3d.extension.animation.skeletal.clip.AnimationClip;
+import com.ardor3d.extension.animation.skeletal.clip.JointChannel;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+/**
+ * Similar to a ClipSource, this class samples and returns values from the channels of an AnimationClip.
+ * ExclusiveClipSource further filters this result set, excluding a given set of channels by name.
+ */
+public class ExclusiveClipSource extends ClipSource {
+
+ /** Our List of channels to exclude by name. */
+ private final List<String> _disabledChannels = Lists.newArrayList();
+
+ /**
+ * Construct a new source. Clip and Manager must be set separately before use.
+ */
+ public ExclusiveClipSource() {}
+
+ /**
+ * Construct a new source using the given clip and manager.
+ *
+ * @param clip
+ * our source clip.
+ * @param manager
+ * the manager used to track clip state.
+ */
+ public ExclusiveClipSource(final AnimationClip clip, final AnimationManager manager) {
+ super(clip, manager);
+ }
+
+ /**
+ * Clears all disabled channels/joints from this source.
+ */
+ public void clearDisabled() {
+ _disabledChannels.clear();
+ }
+
+ /**
+ * @param disabledChannels
+ * a list of channel names to exclude when returning clip results.
+ */
+ public void addDisabledChannels(final String... disabledChannels) {
+ for (final String channelName : disabledChannels) {
+ if (!_disabledChannels.contains(channelName)) {
+ _disabledChannels.add(channelName);
+ }
+ }
+ }
+
+ /**
+ * @param disabledJoints
+ * a list of joint indices to exclude when returning clip results. These are converted to channel names
+ * and stored in our disabledChannels list.
+ */
+ public void addDisabledJoints(final int... disabledJoints) {
+ for (final int i : disabledJoints) {
+ final String channelName = JointChannel.JOINT_CHANNEL_NAME + i;
+ if (!_disabledChannels.contains(channelName)) {
+ _disabledChannels.add(channelName);
+ }
+ }
+ }
+
+ /**
+ * @return a COPY of the disabled channel list.
+ */
+ public ImmutableList<String> getDisabledChannels() {
+ return ImmutableList.copyOf(_disabledChannels);
+ }
+
+ @Override
+ public Map<String, ? extends Object> getSourceData(final AnimationManager manager) {
+ final Map<String, ? extends Object> orig = super.getSourceData(manager);
+
+ // make a copy, removing specific channels
+ final Map<String, ? extends Object> data = Maps.newHashMap(orig);
+ if (_disabledChannels != null) {
+ for (final String key : _disabledChannels) {
+ data.remove(key);
+ }
+ }
+
+ return data;
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/FrozenTreeSource.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/FrozenTreeSource.java
new file mode 100644
index 0000000..192d522
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/FrozenTreeSource.java
@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.blendtree;
+
+import java.util.Map;
+
+import com.ardor3d.extension.animation.skeletal.AnimationManager;
+
+/**
+ * A blend tree node that does not update any clips or sources below it in the blend tree. This is useful for freezing
+ * an animation, often for purposes of transitioning between two unrelated animations.
+ */
+public class FrozenTreeSource implements BlendTreeSource {
+
+ /** Our sub source. */
+ private final BlendTreeSource _source;
+
+ /** The time we are frozen at. */
+ private final double _time;
+
+ public FrozenTreeSource(final BlendTreeSource source, final double frozenTime) {
+ _source = source;
+ _time = frozenTime;
+ }
+
+ public Map<String, ? extends Object> getSourceData(final AnimationManager manager) {
+ return _source.getSourceData(manager);
+ }
+
+ /**
+ * Ignores the command to reset our subtree.
+ */
+ public void resetClips(final AnimationManager manager, final double globalStartTime) {
+ _source.resetClips(manager, 0);
+ }
+
+ /**
+ * Ignores the command to set time on our subtree
+ */
+ public boolean setTime(final double globalTime, final AnimationManager manager) {
+ _source.setTime(_time, manager);
+ return true;
+ }
+
+ @Override
+ public boolean isActive(final AnimationManager manager) {
+ return true;
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/InclusiveClipSource.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/InclusiveClipSource.java
new file mode 100644
index 0000000..f8a260f
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/InclusiveClipSource.java
@@ -0,0 +1,106 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.blendtree;
+
+import java.util.List;
+import java.util.Map;
+
+import com.ardor3d.extension.animation.skeletal.AnimationManager;
+import com.ardor3d.extension.animation.skeletal.clip.AnimationClip;
+import com.ardor3d.extension.animation.skeletal.clip.JointChannel;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+/**
+ * Similar to a ClipSource, this class samples and returns values from the channels of an AnimationClip.
+ * InclusiveClipSource further filters this result set, excluding any channels whose names do not match those in our
+ * enabledChannels list.
+ */
+public class InclusiveClipSource extends ClipSource {
+
+ /** Our List of channels to include by name. */
+ private final List<String> _enabledChannels = Lists.newArrayList();
+
+ /**
+ * Construct a new source. Clip and Manager must be set separately before use.
+ */
+ public InclusiveClipSource() {}
+
+ /**
+ * Construct a new source using the given clip and manager.
+ *
+ * @param clip
+ * our source clip.
+ * @param manager
+ * the manager used to track clip state.
+ */
+ public InclusiveClipSource(final AnimationClip clip, final AnimationManager manager) {
+ super(clip, manager);
+ }
+
+ /**
+ * Clears all enabled channels/joints from this source.
+ */
+ public void clearEnabled() {
+ _enabledChannels.clear();
+ }
+
+ /**
+ * @param enabledChannels
+ * a list of channel names to include when returning clip results.
+ */
+ public void addEnabledChannels(final String... enabledChannels) {
+ for (final String channelName : enabledChannels) {
+ if (!_enabledChannels.contains(channelName)) {
+ _enabledChannels.add(channelName);
+ }
+ }
+ }
+
+ /**
+ * @param enabledJoints
+ * a list of joint indices to include when returning clip results. These are converted to channel names
+ * and stored in our enabledChannels list.
+ */
+ public void addEnabledJoints(final int... enabledJoints) {
+ for (final int i : enabledJoints) {
+ final String channelName = JointChannel.JOINT_CHANNEL_NAME + i;
+ if (!_enabledChannels.contains(channelName)) {
+ _enabledChannels.add(channelName);
+ }
+ }
+ }
+
+ /**
+ * @return a COPY of the enabled channel list.
+ */
+ public ImmutableList<String> getEnabledChannels() {
+ return ImmutableList.copyOf(_enabledChannels);
+ }
+
+ @Override
+ public Map<String, ? extends Object> getSourceData(final AnimationManager manager) {
+ final Map<String, ? extends Object> orig = super.getSourceData(manager);
+
+ // make a copy, only bringing across specific channels
+ final Map<String, Object> data = Maps.newHashMap();
+ if (_enabledChannels != null) {
+ for (final String key : _enabledChannels) {
+ if (orig.containsKey(key)) {
+ data.put(key, orig.get(key));
+ }
+ }
+ }
+
+ return data;
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/ManagedTransformSource.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/ManagedTransformSource.java
new file mode 100644
index 0000000..6530783
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/ManagedTransformSource.java
@@ -0,0 +1,183 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.blendtree;
+
+import java.util.Map;
+
+import com.ardor3d.extension.animation.skeletal.AnimationManager;
+import com.ardor3d.extension.animation.skeletal.SkeletonPose;
+import com.ardor3d.extension.animation.skeletal.clip.AnimationClip;
+import com.ardor3d.extension.animation.skeletal.clip.JointChannel;
+import com.ardor3d.extension.animation.skeletal.clip.JointData;
+import com.ardor3d.math.type.ReadOnlyQuaternion;
+import com.ardor3d.math.type.ReadOnlyVector3;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+
+/**
+ * This tree source maintains its own source data, which can be modified directly using setJointXXX. This source is
+ * meant to be used for controlling a particular joint or set of joints programmatically.
+ */
+public class ManagedTransformSource implements BlendTreeSource {
+
+ /** Our local source data. */
+ private final Map<String, JointData> data = Maps.newHashMap();
+
+ /** optional: name of source we were initialized from, if given. */
+ private String sourceName;
+
+ /**
+ * Set the local source transform data for a given joint index.
+ *
+ * @param jointIndex
+ * our joint index value.
+ * @param jointData
+ * the joint transform data. This object is copied into the local store.
+ */
+ public void setJointTransformData(final int jointIndex, final JointData jointData) {
+ final String key = JointChannel.JOINT_CHANNEL_NAME + jointIndex;
+ // reuse TransformData object
+ if (!data.containsKey(key)) {
+ data.put(key, new JointData(jointData));
+ } else {
+ final JointData old = data.get(key);
+ old.set(jointData);
+ }
+ }
+
+ /**
+ * Sets a translation to the local transformdata for a given joint index.
+ *
+ * @param jointIndex
+ * our joint index value.
+ * @param translation
+ * the translation to set
+ */
+ public void setJointTranslation(final int jointIndex, final ReadOnlyVector3 translation) {
+ final String key = JointChannel.JOINT_CHANNEL_NAME + jointIndex;
+ JointData tData = data.get(key);
+ if (tData == null) {
+ tData = new JointData(jointIndex);
+ data.put(key, tData);
+ }
+
+ tData.setTranslation(translation);
+ }
+
+ /**
+ * Sets a scale to the local transformdata for a given joint index.
+ *
+ * @param jointIndex
+ * our joint index value.
+ * @param scale
+ * the scale to set
+ */
+ public void setJointScale(final int jointIndex, final ReadOnlyVector3 scale) {
+ final String key = JointChannel.JOINT_CHANNEL_NAME + jointIndex;
+ JointData tData = data.get(key);
+ if (tData == null) {
+ tData = new JointData(jointIndex);
+ data.put(key, tData);
+ }
+
+ tData.setScale(scale);
+ }
+
+ /**
+ * Sets a rotation to the local transformdata for a given joint index.
+ *
+ * @param jointIndex
+ * our joint index value.
+ * @param rotation
+ * the rotation to set
+ */
+ public void setJointRotation(final int jointIndex, final ReadOnlyQuaternion rotation) {
+ final String key = JointChannel.JOINT_CHANNEL_NAME + jointIndex;
+ JointData tData = data.get(key);
+ if (tData == null) {
+ tData = new JointData(jointIndex);
+ data.put(key, tData);
+ }
+
+ tData.setRotation(rotation);
+ }
+
+ /**
+ * Returns an immutable COPY of our local source data.
+ */
+ public Map<String, JointData> getSourceData(final AnimationManager manager) {
+ return ImmutableMap.copyOf(data);
+ }
+
+ /**
+ * Does nothing.
+ */
+ public boolean setTime(final double globalTime, final AnimationManager manager) {
+ return true;
+ }
+
+ /**
+ * Does nothing.
+ */
+ public void resetClips(final AnimationManager manager, final double globalStartTime) {
+ ; // ignore
+ }
+
+ /**
+ * Does nothing.
+ */
+ @Override
+ public boolean isActive(final AnimationManager manager) {
+ return true;
+ }
+
+ /**
+ * Setup transform data on this source, using the first frame from a specific clip and jointNames from a specific
+ * pose.
+ *
+ * @param pose
+ * the pose to sample joints from
+ * @param clip
+ * the animation clip to pull data from
+ * @param jointNames
+ * the names of the joints to find indices of.
+ */
+ public void initJointsByName(final SkeletonPose pose, final AnimationClip clip, final String... jointNames) {
+ for (final String name : jointNames) {
+ final int jointIndex = pose.getSkeleton().findJointByName(name);
+ setJointTransformData(jointIndex, ((JointChannel) clip.findChannelByName(JointChannel.JOINT_CHANNEL_NAME
+ + jointIndex)).getJointData(0, new JointData(jointIndex)));
+ }
+ }
+
+ /**
+ * Setup transform data for specific joints on this source, using the first frame from a given clip.
+ *
+ * @param clip
+ * the animation clip to pull data from
+ * @param jointIndices
+ * the indices of the joints to initialize data for.
+ */
+ public void initJointsById(final AnimationClip clip, final int... jointIndices) {
+ for (final int jointIndex : jointIndices) {
+ setJointTransformData(jointIndex, ((JointChannel) clip.findChannelByName(JointChannel.JOINT_CHANNEL_NAME
+ + jointIndex)).getJointData(0, new JointData(jointIndex)));
+ }
+ }
+
+ public String getSourceName() {
+ return sourceName;
+ }
+
+ public void setSourceName(final String name) {
+ sourceName = name;
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/SimpleAnimationApplier.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/SimpleAnimationApplier.java
new file mode 100644
index 0000000..31fef67
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/blendtree/SimpleAnimationApplier.java
@@ -0,0 +1,123 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.blendtree;
+
+import java.util.Map;
+
+import com.ardor3d.extension.animation.skeletal.AnimationApplier;
+import com.ardor3d.extension.animation.skeletal.AnimationManager;
+import com.ardor3d.extension.animation.skeletal.SkeletonPose;
+import com.ardor3d.extension.animation.skeletal.clip.JointData;
+import com.ardor3d.extension.animation.skeletal.clip.TransformData;
+import com.ardor3d.extension.animation.skeletal.clip.TriggerCallback;
+import com.ardor3d.extension.animation.skeletal.clip.TriggerData;
+import com.ardor3d.scenegraph.Node;
+import com.ardor3d.scenegraph.Spatial;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.MapMaker;
+import com.google.common.collect.Multimap;
+
+/**
+ * Very simple applier. Just applies joint transform data, calls any callbacks and updates the pose's global transforms.
+ */
+public class SimpleAnimationApplier implements AnimationApplier {
+
+ private final Multimap<String, TriggerCallback> _triggerCallbacks = ArrayListMultimap.create(0, 0);
+
+ private final Map<String, Spatial> _spatialCache = new MapMaker().weakValues().makeMap();
+
+ @Override
+ public void apply(final Spatial root, final AnimationManager manager) {
+ if (root == null) {
+ return;
+ }
+ final Map<String, ? extends Object> data = manager.getCurrentSourceData();
+
+ // cycle through, pulling out and applying those we know about
+ if (data != null) {
+ for (final String key : data.keySet()) {
+ final Object value = data.get(key);
+ if (value instanceof JointData) { // ignore
+ } else if (value instanceof TransformData) {
+ final TransformData transformData = (TransformData) value;
+ final Spatial applyTo = findChild(root, key);
+ if (applyTo != null) {
+ transformData.applyTo(applyTo);
+ }
+ }
+ }
+ }
+ }
+
+ private Spatial findChild(final Spatial root, final String key) {
+ if (_spatialCache.containsKey(key)) {
+ return _spatialCache.get(key);
+ }
+ if (key.equals(root.getName())) {
+ _spatialCache.put(key, root);
+ return root;
+ } else if (root instanceof Node) {
+ final Spatial spat = ((Node) root).getChild(key);
+ if (spat != null) {
+ _spatialCache.put(key, spat);
+ return spat;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void applyTo(final SkeletonPose applyToPose, final AnimationManager manager) {
+ final Map<String, ? extends Object> data = manager.getCurrentSourceData();
+
+ // cycle through, pulling out and applying those we know about
+ if (data != null) {
+ for (final Object value : data.values()) {
+ if (value instanceof JointData) {
+ final JointData jointData = (JointData) value;
+ if (jointData.getJointIndex() >= 0) {
+ jointData.applyTo(applyToPose.getLocalJointTransforms()[jointData.getJointIndex()]);
+ }
+ } else if (value instanceof TriggerData) {
+ final TriggerData trigger = (TriggerData) value;
+ if (trigger.isArmed()) {
+ try {
+ // pull callback(s) for the current trigger key, if exists, and call.
+ for (final String curTrig : trigger.getCurrentTriggers()) {
+ for (final TriggerCallback cb : _triggerCallbacks.get(curTrig)) {
+ cb.doTrigger(applyToPose, manager);
+ }
+ }
+ } finally {
+ trigger.setArmed(false);
+ }
+ }
+ }
+ }
+
+ applyToPose.updateTransforms();
+ }
+ }
+
+ public void clearSpatialCache() {
+ _spatialCache.clear();
+ }
+
+ @Override
+ public void addTriggerCallback(final String key, final TriggerCallback callback) {
+ _triggerCallbacks.put(key, callback);
+ }
+
+ @Override
+ public boolean removeTriggerCallback(final String key, final TriggerCallback callback) {
+ return _triggerCallbacks.remove(key, callback);
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/AbstractAnimationChannel.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/AbstractAnimationChannel.java
new file mode 100644
index 0000000..199f80e
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/AbstractAnimationChannel.java
@@ -0,0 +1,213 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.clip;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+
+import com.ardor3d.util.export.InputCapsule;
+import com.ardor3d.util.export.OutputCapsule;
+import com.ardor3d.util.export.Savable;
+
+/**
+ * Base class for animation channels. An animation channel describes a single element of an animation (such as the
+ * movement of a single joint, or the play back of a specific sound, etc.) These channels are grouped together in an
+ * AnimationClip to describe a full animation.
+ */
+public abstract class AbstractAnimationChannel implements Savable {
+
+ /** The name of this channel. */
+ protected final String _channelName;
+
+ /** Our time indices. Each index of the array should contain a value that is > the value in the previous index. */
+ protected final float[] _times;
+
+ /**
+ * Construct a new channel.
+ *
+ * @param channelName
+ * the name of our channel. This is immutable to this instance of the class.
+ * @param times
+ * our time indices. Copied into the channel.
+ */
+ public AbstractAnimationChannel(final String channelName, final float[] times) {
+ _channelName = channelName;
+ _times = times == null ? null : new float[times.length];
+ if (_times != null) {
+ System.arraycopy(times, 0, _times, 0, times.length);
+ }
+ }
+
+ /**
+ * @return the number of samples in this channel.
+ */
+ public int getSampleCount() {
+ return _times.length;
+ }
+
+ public String getChannelName() {
+ return _channelName;
+ }
+
+ public float[] getTimes() {
+ return _times;
+ }
+
+ /**
+ * Update the given applyTo object with information from this channel at the given time position.
+ *
+ * @param clockTime
+ * the current local clip time (where 0 == start of clip)
+ * @param applyTo
+ * the Object to apply to. The type of the object and what data is set will depend on the Channel
+ * subclass.
+ */
+ public void updateSample(final double clockTime, final Object applyTo) {
+ if (_times.length == 0) {
+ return;
+ }
+ // figure out what frames we are between and by how much
+ final int lastFrame = _times.length - 1;
+ if (clockTime < 0 || _times.length == 1) {
+ setCurrentSample(0, 0.0, applyTo);
+ } else if (clockTime >= _times[lastFrame]) {
+ setCurrentSample(lastFrame, 0.0, applyTo);
+ } else {
+ int startFrame = 0;
+
+ for (int i = 0; i < _times.length - 1; i++) {
+ if (_times[i] < clockTime) {
+ startFrame = i;
+ }
+ }
+ final double progressPercent = (clockTime - _times[startFrame])
+ / (_times[startFrame + 1] - _times[startFrame]);
+
+ setCurrentSample(startFrame, progressPercent, applyTo);
+ }
+ }
+
+ /**
+ * Sets data on the given applyTo Object for the given sampleIndex and a percent progress towards the sample
+ * following it.
+ *
+ * @param sampleIndex
+ * the sample to pull information from.
+ * @param progressPercent
+ * a value [0.0, 1.0] representing progress from sampleIndex to sampleIndex+1
+ * @param applyTo
+ * the data object to apply this channel's information to.
+ */
+ public abstract void setCurrentSample(int sampleIndex, double progressPercent, Object applyTo);
+
+ /**
+ * @param instance
+ * the instance creating and storing the state data object.
+ * @return an Object suitable for storing information for this type of animation channel.
+ */
+ public abstract Object createStateDataObject(AnimationClipInstance instance);
+
+ /**
+ * Returns a new channel of the same name and content as this, but trimmed to just the times between start and end
+ * sample.
+ *
+ * @param startSample
+ * the sample to start with (inclusive). Sample counting starts at 0.
+ * @param endSample
+ * the sample to end with (inclusive). max is getSampleCount() - 1.
+ * @return the new channel.
+ */
+ public AbstractAnimationChannel getSubchannelBySample(final int startSample, final int endSample) {
+ return getSubchannelBySample(getChannelName(), startSample, endSample);
+ }
+
+ /**
+ *
+ * Returns a new channel of the same content as this, but trimmed to just the times between start and end sample.
+ *
+ * @param name
+ * the new name for our subchannel.
+ * @param startSample
+ * the sample to start with (inclusive). Sample counting starts at 0.
+ * @param endSample
+ * the sample to end with (inclusive). max is getSampleCount() - 1.
+ * @return the new channel.
+ * @throws IllegalArgumentException
+ * if start > end or end >= getSampleCount.
+ */
+ public abstract AbstractAnimationChannel getSubchannelBySample(String name, int startSample, int endSample);
+
+ /**
+ * Returns a new channel of the same name and content as this, but trimmed to just the times between start and end
+ * times.
+ *
+ * @param startTime
+ * the time to start with (inclusive)
+ * @param endTime
+ * the time to end with (inclusive)
+ * @return the new channel.
+ */
+ public AbstractAnimationChannel getSubchannelByTime(final float startTime, final float endTime) {
+ return getSubchannelByTime(getChannelName(), startTime, endTime);
+ }
+
+ /**
+ *
+ * Returns a new channel of the same content as this, but trimmed to just the times between start and end sample.
+ *
+ * @param name
+ * the new name for our subchannel.
+ * @param startTime
+ * the time to start with (inclusive)
+ * @param endTime
+ * the time to end with (inclusive)
+ * @return the new channel.
+ * @throws IllegalArgumentException
+ * if start > end or end >= getSampleCount.
+ */
+ public abstract AbstractAnimationChannel getSubchannelByTime(String name, float startTime, float endTime);
+
+ /**
+ * @return the local time index of the last sample in this channel.
+ */
+ public float getMaxTime() {
+ if (_times.length == 0) {
+ return 0;
+ } else {
+ return _times[_times.length - 1];
+ }
+ }
+
+ // /////////////////
+ // Methods for Savable
+ // /////////////////
+
+ public void write(final OutputCapsule capsule) throws IOException {
+ capsule.write(_channelName, "channelName", null);
+ capsule.write(_times, "times", null);
+ }
+
+ public void read(final InputCapsule capsule) throws IOException {
+ final String channelName = capsule.readString("channelName", null);
+ final float[] times = capsule.readFloatArray("times", null);
+ try {
+ final Field field1 = AbstractAnimationChannel.class.getDeclaredField("_channelName");
+ field1.setAccessible(true);
+ field1.set(this, channelName);
+
+ final Field field2 = AbstractAnimationChannel.class.getDeclaredField("_times");
+ field2.setAccessible(true);
+ field2.set(this, times);
+ } catch (final Exception e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/AnimationClip.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/AnimationClip.java
new file mode 100644
index 0000000..cb10165
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/AnimationClip.java
@@ -0,0 +1,194 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.clip;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.List;
+
+import com.ardor3d.annotation.SavableFactory;
+import com.ardor3d.util.export.InputCapsule;
+import com.ardor3d.util.export.OutputCapsule;
+import com.ardor3d.util.export.Savable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+/**
+ * AnimationClip manages a set of animation channels as a single clip entity.
+ */
+@SavableFactory(factoryMethod = "initSavable")
+public class AnimationClip implements Savable {
+
+ /** A referenceable name for this clip. In general, this should be unique. */
+ private final String _name;
+
+ /** A list of animation channels managed by this clip. */
+ private final List<AbstractAnimationChannel> _channels;
+
+ /** A max time value for this clip, pulled from our managed channels. */
+ private transient float _maxTime = 0;
+
+ /**
+ * Construct a new animation clip with no channels.
+ */
+ public AnimationClip(final String name) {
+ _name = name;
+ _channels = Lists.newArrayList();
+ }
+
+ /**
+ * Construct a new animation clip, copying in a given list of channels.
+ *
+ * @param channels
+ * a list of channels to shallow copy locally.
+ */
+ public AnimationClip(final String name, final List<AbstractAnimationChannel> channels) {
+ _name = name;
+ _channels = Lists.newArrayList(channels);
+ updateMaxTimeIndex();
+ }
+
+ /**
+ * @return the referenceable name for this clip. In general, this should be unique.
+ */
+ public String getName() {
+ return _name;
+ }
+
+ /**
+ * Update an instance of this clip.
+ *
+ * @param clockTime
+ * the current local clip time (where 0 == start of clip)
+ * @param instance
+ * the instance record to update.
+ */
+ public void update(final double clockTime, final AnimationClipInstance instance) {
+ // Go through each channel and update clipState
+ for (int i = 0; i < _channels.size(); ++i) {
+ final AbstractAnimationChannel channel = _channels.get(i);
+ final Object applyTo = instance.getApplyTo(channel);
+ channel.updateSample(clockTime, applyTo);
+ }
+ }
+
+ /**
+ * Add a channel to this clip.
+ *
+ * @param channel
+ * the channel to add.
+ */
+ public void addChannel(final AbstractAnimationChannel channel) {
+ _channels.add(channel);
+ updateMaxTimeIndex();
+ }
+
+ /**
+ * Locate a channel in this clip using its channel name.
+ *
+ * @param channelName
+ * the name to match against.
+ * @return the first channel with a name matching the given channelName, or null if no matches are found.
+ */
+ public AbstractAnimationChannel findChannelByName(final String channelName) {
+ for (final AbstractAnimationChannel channel : _channels) {
+ if (channelName.equals(channel.getChannelName())) {
+ return channel;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Remove a given channel from this clip.
+ *
+ * @param channel
+ * the channel to remove.
+ * @return true if this clip had the given channel and it was removed.
+ */
+ public boolean removeChannel(final AbstractAnimationChannel channel) {
+ final boolean rVal = _channels.remove(channel);
+ updateMaxTimeIndex();
+ return rVal;
+ }
+
+ /**
+ * @return an immutable copy of the channels in this clip.
+ */
+ public ImmutableList<AbstractAnimationChannel> getChannels() {
+ return ImmutableList.copyOf(_channels);
+ }
+
+ /**
+ * @return the maximum (local) time value of this clip, as described by the channels it manages.
+ */
+ public float getMaxTimeIndex() {
+ return _maxTime;
+ }
+
+ /**
+ * Update our max time value to match the max time in our managed animation channels.
+ */
+ private void updateMaxTimeIndex() {
+ _maxTime = 0;
+ float max;
+ for (final AbstractAnimationChannel channel : _channels) {
+ max = channel.getMaxTime();
+ if (max > _maxTime) {
+ _maxTime = max;
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "AnimationClip [channel count=" + _channels.size() + ", max time=" + _maxTime + "]";
+ }
+
+ // /////////////////
+ // Methods for Savable
+ // /////////////////
+
+ public Class<? extends AnimationClip> getClassTag() {
+ return this.getClass();
+ }
+
+ public void write(final OutputCapsule capsule) throws IOException {
+ capsule.write(_name, "name", null);
+ capsule.writeSavableList(_channels, "channels", null);
+ }
+
+ public void read(final InputCapsule capsule) throws IOException {
+ final String name = capsule.readString("name", null);
+ try {
+ final Field field1 = AnimationClip.class.getDeclaredField("_name");
+ field1.setAccessible(true);
+ field1.set(this, name);
+ } catch (final Exception e) {
+ e.printStackTrace();
+ }
+
+ _channels.clear();
+ final List<Savable> channels = capsule.readSavableList("channels", null);
+ for (final Savable channel : channels) {
+ _channels.add((AbstractAnimationChannel) channel);
+ }
+ updateMaxTimeIndex();
+ }
+
+ public static AnimationClip initSavable() {
+ return new AnimationClip();
+ }
+
+ private AnimationClip() {
+ this(null);
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/AnimationClipInstance.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/AnimationClipInstance.java
new file mode 100644
index 0000000..14f8816
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/AnimationClipInstance.java
@@ -0,0 +1,147 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.clip;
+
+import java.util.List;
+import java.util.Map;
+
+import com.ardor3d.extension.animation.skeletal.AnimationListener;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+/**
+ * Maintains state information about an instance of a specific animation clip, such as time scaling applied, active
+ * flag, start time of the instance, etc.
+ */
+public class AnimationClipInstance {
+
+ /** Active flag - if true, the instance is currently playing. */
+ private boolean _active = true;
+
+ /** Number of loops this clip should play. */
+ private int _loopCount = 0;
+
+ /**
+ * A scale value to apply to our timing. Values greater than 1 will speed up playback, less than 1 will slow down
+ * playback. Negative values can be used to reverse playback.
+ */
+ private double _timeScale = 1.0;
+
+ /** The global start time of our clip instance. */
+ private double _startTime = 0.0;
+
+ /** Map of channel name -> state tracking objects. */
+ private final Map<String, Object> _clipStateObjects = Maps.newHashMap();
+
+ /** List of callbacks for animation events. */
+ private List<AnimationListener> animationListeners = null;
+
+ /**
+ * Add an animation listener to our callback list.
+ *
+ * @param animationListener
+ * the listener to add.
+ */
+ public void addAnimationListener(final AnimationListener animationListener) {
+ if (animationListeners == null) {
+ animationListeners = Lists.newArrayList();
+ }
+ animationListeners.add(animationListener);
+ }
+
+ /**
+ * Remove an animation listener from our callback list.
+ *
+ * @param animationListener
+ * the listener to remove.
+ * @return true if the listener was found in our list
+ */
+ public boolean removeAnimationListener(final AnimationListener animationListener) {
+ if (animationListeners == null) {
+ return false;
+ }
+ final boolean rVal = animationListeners.remove(animationListener);
+ if (animationListeners.isEmpty()) {
+ animationListeners = null;
+ }
+ return rVal;
+ }
+
+ /**
+ * @return an immutable copy of the list of action listeners.
+ */
+ public List<AnimationListener> getAnimationListeners() {
+ if (animationListeners == null) {
+ return ImmutableList.of();
+ }
+ return ImmutableList.copyOf(animationListeners);
+ }
+
+ public boolean isActive() {
+ return _active;
+ }
+
+ public void setActive(final boolean active) {
+ _active = active;
+ }
+
+ public int getLoopCount() {
+ return _loopCount;
+ }
+
+ public void setLoopCount(final int loopCount) {
+ _loopCount = loopCount;
+ }
+
+ public double getTimeScale() {
+ return _timeScale;
+ }
+
+ public void setTimeScale(final double timeScale) {
+ _timeScale = timeScale;
+ }
+
+ public double getStartTime() {
+ return _startTime;
+ }
+
+ public void setStartTime(final double startTime) {
+ _startTime = startTime;
+ }
+
+ public Object getApplyTo(final AbstractAnimationChannel channel) {
+ final String channelName = channel.getChannelName();
+ Object rVal = _clipStateObjects.get(channelName);
+ if (rVal == null) {
+ rVal = channel.createStateDataObject(this);
+ _clipStateObjects.put(channelName, rVal);
+ }
+ return rVal;
+ }
+
+ public Map<String, Object> getChannelData() {
+ return _clipStateObjects;
+ }
+
+ /**
+ * Tell any animation listeners on this instance that the associated clip has finished playing.
+ */
+ public void fireAnimationFinished() {
+ if (animationListeners == null) {
+ return;
+ }
+
+ for (final AnimationListener animationListener : animationListeners) {
+ animationListener.animationFinished(this);
+ }
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/GuaranteedTriggerChannel.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/GuaranteedTriggerChannel.java
new file mode 100644
index 0000000..394a585
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/GuaranteedTriggerChannel.java
@@ -0,0 +1,138 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.clip;
+
+import java.util.List;
+
+import com.ardor3d.annotation.SavableFactory;
+import com.google.common.collect.Lists;
+
+/**
+ * An animation source channel consisting of keyword samples indicating when a specific trigger condition is met. Each
+ * channel can only be in one keyword "state" at a given moment in time. This channel guarantees that if we skip over a
+ * sample in this channel, we'll still arm it after that fact. This channel should only be used with non-looping,
+ * forward moving clips.
+ */
+@SavableFactory(factoryMethod = "initSavable")
+public class GuaranteedTriggerChannel extends TriggerChannel {
+
+ /**
+ * Construct a new GuaranteedTriggerChannel.
+ *
+ * @param channelName
+ * the name of this channel.
+ * @param times
+ * the time samples
+ * @param keys
+ * our key samples. Entries may be null. Should have as many entries as the times array.
+ */
+ public GuaranteedTriggerChannel(final String channelName, final float[] times, final String[] keys) {
+ super(channelName, times, keys);
+ }
+
+ @Override
+ public void setCurrentSample(final int sampleIndex, final double progressPercent, final Object applyTo) {
+ final TriggerData triggerData = (TriggerData) applyTo;
+
+ final int oldIndex = triggerData.getCurrentIndex();
+
+ // arm trigger
+ final int newIndex = progressPercent != 1.0 ? sampleIndex : sampleIndex + 1;
+ if (oldIndex == newIndex) {
+ triggerData.arm(newIndex, _keys[newIndex]);
+ } else {
+ final List<String> triggers = Lists.newArrayList();
+ for (int i = oldIndex + 1; i <= newIndex; i++) {
+ if (_keys[i] != null) {
+ triggers.add(_keys[i]);
+ }
+ }
+ triggerData.arm(newIndex, triggers.toArray(new String[triggers.size()]));
+ }
+ }
+
+ @Override
+ public AbstractAnimationChannel getSubchannelBySample(final String name, final int startSample, final int endSample) {
+ if (startSample > endSample) {
+ throw new IllegalArgumentException("startSample > endSample");
+ }
+ if (endSample >= getSampleCount()) {
+ throw new IllegalArgumentException("endSample >= getSampleCount()");
+ }
+
+ final int samples = endSample - startSample + 1;
+ final float[] times = new float[samples];
+ final String[] keys = new String[samples];
+
+ for (int i = 0; i <= samples; i++) {
+ times[i] = _times[i + startSample];
+ keys[i] = _keys[i + startSample];
+ }
+
+ return new GuaranteedTriggerChannel(name, times, keys);
+ }
+
+ @Override
+ public AbstractAnimationChannel getSubchannelByTime(final String name, final float startTime, final float endTime) {
+ if (startTime > endTime) {
+ throw new IllegalArgumentException("startTime > endTime");
+ }
+ final List<Float> times = Lists.newArrayList();
+ final List<String> keys = Lists.newArrayList();
+
+ final TriggerData tData = new TriggerData();
+
+ // Add start sample
+ updateSample(startTime, tData);
+ times.add(0f);
+ keys.add(tData.getCurrentTrigger());
+
+ // Add mid samples
+ for (int i = 0; i < getSampleCount(); i++) {
+ final float time = _times[i];
+ updateSample(time, tData);
+ if (time > startTime && time < endTime) {
+ times.add(time - startTime);
+ keys.add(_keys[i]);
+ }
+ }
+
+ // Add end sample
+ updateSample(endTime, tData);
+ times.add(endTime - startTime);
+ keys.add(tData.getCurrentTrigger());
+
+ final float[] timesArray = new float[times.size()];
+ int i = 0;
+ for (final float time : times) {
+ timesArray[i++] = time;
+ }
+ // return
+ return new GuaranteedTriggerChannel(name, timesArray, keys.toArray(new String[keys.size()]));
+ }
+
+ // /////////////////
+ // Methods for Savable
+ // /////////////////
+
+ @Override
+ public Class<? extends GuaranteedTriggerChannel> getClassTag() {
+ return this.getClass();
+ }
+
+ public static GuaranteedTriggerChannel initSavable() {
+ return new GuaranteedTriggerChannel();
+ }
+
+ protected GuaranteedTriggerChannel() {
+ super(null, null, null);
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/InterpolatedDoubleChannel.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/InterpolatedDoubleChannel.java
new file mode 100644
index 0000000..40bbce4
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/InterpolatedDoubleChannel.java
@@ -0,0 +1,167 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.clip;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.List;
+
+import com.ardor3d.math.MathUtils;
+import com.ardor3d.util.export.InputCapsule;
+import com.ardor3d.util.export.OutputCapsule;
+import com.google.common.collect.Lists;
+
+/**
+ * An animation source channel consisting of double value samples. These samples are interpolated between key frames.
+ * Potential uses for this channel include extracting and using forward motion from walk animations, animating colors or
+ * texture coordinates, etc.
+ */
+public class InterpolatedDoubleChannel extends AbstractAnimationChannel {
+
+ /** Our key samples. */
+ protected final double[] _values;
+
+ /**
+ * Construct a new InterpolatedDoubleChannel.
+ *
+ * @param channelName
+ * the name of this channel.
+ * @param times
+ * the time samples
+ * @param keys
+ * our key samples. Entries may be null. Should have as many entries as the times array.
+ */
+ public InterpolatedDoubleChannel(final String channelName, final float[] times, final double[] values) {
+ super(channelName, times);
+ _values = values == null ? null : new double[values.length];
+ if (_values != null) {
+ System.arraycopy(values, 0, _values, 0, values.length);
+ }
+ }
+
+ public double[] getValues() {
+ return _values;
+ }
+
+ @Override
+ public void setCurrentSample(final int sampleIndex, final double progressPercent, final Object applyTo) {
+ final double[] store = (double[]) applyTo;
+
+ // set key
+ store[0] = MathUtils.lerp(progressPercent, _values[sampleIndex], _values[sampleIndex + 1]);
+ }
+
+ @Override
+ public double[] createStateDataObject(final AnimationClipInstance instance) {
+ return new double[1];
+ }
+
+ @Override
+ public InterpolatedDoubleChannel getSubchannelBySample(final String name, final int startSample, final int endSample) {
+ if (startSample > endSample) {
+ throw new IllegalArgumentException("startSample > endSample");
+ }
+ if (endSample >= getSampleCount()) {
+ throw new IllegalArgumentException("endSample >= getSampleCount()");
+ }
+
+ final int samples = endSample - startSample + 1;
+ final float[] times = new float[samples];
+ final double[] values = new double[samples];
+
+ for (int i = 0; i <= samples; i++) {
+ times[i] = _times[i + startSample];
+ values[i] = _values[i + startSample];
+ }
+
+ return new InterpolatedDoubleChannel(name, times, values);
+ }
+
+ @Override
+ public InterpolatedDoubleChannel getSubchannelByTime(final String name, final float startTime, final float endTime) {
+ if (startTime > endTime) {
+ throw new IllegalArgumentException("startTime > endTime");
+ }
+ final List<Float> times = Lists.newArrayList();
+ final List<Double> keys = Lists.newArrayList();
+
+ final double[] data = new double[1];
+
+ // Add start sample
+ updateSample(startTime, data);
+ times.add(0f);
+ keys.add(data[0]);
+
+ // Add mid samples
+ for (int i = 0; i < getSampleCount(); i++) {
+ final float time = _times[i];
+ updateSample(time, data);
+ if (time > startTime && time < endTime) {
+ times.add(time - startTime);
+ keys.add(_values[i]);
+ }
+ }
+
+ // Add end sample
+ updateSample(endTime, data);
+ times.add(endTime - startTime);
+ keys.add(data[0]);
+
+ final float[] timesArray = new float[times.size()];
+ int i = 0;
+ for (final float time : times) {
+ timesArray[i++] = time;
+ }
+ // return
+ final double[] values = new double[keys.size()];
+ i = 0;
+ for (final double val : keys) {
+ values[i++] = val;
+ }
+ return new InterpolatedDoubleChannel(name, timesArray, values);
+ }
+
+ // /////////////////
+ // Methods for Savable
+ // /////////////////
+
+ public Class<? extends InterpolatedDoubleChannel> getClassTag() {
+ return this.getClass();
+ }
+
+ @Override
+ public void write(final OutputCapsule capsule) throws IOException {
+ super.write(capsule);
+ capsule.write(_values, "values", null);
+ }
+
+ @Override
+ public void read(final InputCapsule capsule) throws IOException {
+ super.read(capsule);
+ final double[] values = capsule.readDoubleArray("values", null);
+ try {
+ final Field field1 = TriggerChannel.class.getDeclaredField("_values");
+ field1.setAccessible(true);
+ field1.set(this, values);
+ } catch (final Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static InterpolatedDoubleChannel initSavable() {
+ return new InterpolatedDoubleChannel();
+ }
+
+ protected InterpolatedDoubleChannel() {
+ super(null, null);
+ _values = null;
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/InterpolatedFloatChannel.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/InterpolatedFloatChannel.java
new file mode 100644
index 0000000..a0ff9f6
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/InterpolatedFloatChannel.java
@@ -0,0 +1,167 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.clip;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.List;
+
+import com.ardor3d.math.MathUtils;
+import com.ardor3d.util.export.InputCapsule;
+import com.ardor3d.util.export.OutputCapsule;
+import com.google.common.collect.Lists;
+
+/**
+ * An animation source channel consisting of float value samples. These samples are interpolated between key frames.
+ * Potential uses for this channel include extracting and using forward motion from walk animations, animating colors or
+ * texture coordinates, etc.
+ */
+public class InterpolatedFloatChannel extends AbstractAnimationChannel {
+
+ /** Our key samples. */
+ protected final float[] _values;
+
+ /**
+ * Construct a new InterpolatedFloatChannel.
+ *
+ * @param channelName
+ * the name of this channel.
+ * @param times
+ * the time samples
+ * @param values
+ * our value samples. Entries may be null. Should have as many entries as the times array.
+ */
+ public InterpolatedFloatChannel(final String channelName, final float[] times, final float[] values) {
+ super(channelName, times);
+ _values = values == null ? null : new float[values.length];
+ if (_values != null) {
+ System.arraycopy(values, 0, _values, 0, values.length);
+ }
+ }
+
+ public float[] getValues() {
+ return _values;
+ }
+
+ @Override
+ public void setCurrentSample(final int sampleIndex, final double progressPercent, final Object applyTo) {
+ final float[] store = (float[]) applyTo;
+
+ // set key
+ store[0] = MathUtils.lerp((float) progressPercent, _values[sampleIndex], _values[sampleIndex + 1]);
+ }
+
+ @Override
+ public float[] createStateDataObject(final AnimationClipInstance instance) {
+ return new float[1];
+ }
+
+ @Override
+ public InterpolatedFloatChannel getSubchannelBySample(final String name, final int startSample, final int endSample) {
+ if (startSample > endSample) {
+ throw new IllegalArgumentException("startSample > endSample");
+ }
+ if (endSample >= getSampleCount()) {
+ throw new IllegalArgumentException("endSample >= getSampleCount()");
+ }
+
+ final int samples = endSample - startSample + 1;
+ final float[] times = new float[samples];
+ final float[] values = new float[samples];
+
+ for (int i = 0; i <= samples; i++) {
+ times[i] = _times[i + startSample];
+ values[i] = _values[i + startSample];
+ }
+
+ return new InterpolatedFloatChannel(name, times, values);
+ }
+
+ @Override
+ public InterpolatedFloatChannel getSubchannelByTime(final String name, final float startTime, final float endTime) {
+ if (startTime > endTime) {
+ throw new IllegalArgumentException("startTime > endTime");
+ }
+ final List<Float> times = Lists.newArrayList();
+ final List<Float> keys = Lists.newArrayList();
+
+ final float[] data = new float[1];
+
+ // Add start sample
+ updateSample(startTime, data);
+ times.add(0f);
+ keys.add(data[0]);
+
+ // Add mid samples
+ for (int i = 0; i < getSampleCount(); i++) {
+ final float time = _times[i];
+ updateSample(time, data);
+ if (time > startTime && time < endTime) {
+ times.add(time - startTime);
+ keys.add(_values[i]);
+ }
+ }
+
+ // Add end sample
+ updateSample(endTime, data);
+ times.add(endTime - startTime);
+ keys.add(data[0]);
+
+ final float[] timesArray = new float[times.size()];
+ int i = 0;
+ for (final float time : times) {
+ timesArray[i++] = time;
+ }
+ // return
+ final float[] values = new float[keys.size()];
+ i = 0;
+ for (final float val : keys) {
+ values[i++] = val;
+ }
+ return new InterpolatedFloatChannel(name, timesArray, values);
+ }
+
+ // /////////////////
+ // Methods for Savable
+ // /////////////////
+
+ public Class<? extends InterpolatedFloatChannel> getClassTag() {
+ return this.getClass();
+ }
+
+ @Override
+ public void write(final OutputCapsule capsule) throws IOException {
+ super.write(capsule);
+ capsule.write(_values, "values", null);
+ }
+
+ @Override
+ public void read(final InputCapsule capsule) throws IOException {
+ super.read(capsule);
+ final float[] values = capsule.readFloatArray("values", null);
+ try {
+ final Field field1 = TriggerChannel.class.getDeclaredField("_values");
+ field1.setAccessible(true);
+ field1.set(this, values);
+ } catch (final Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static InterpolatedFloatChannel initSavable() {
+ return new InterpolatedFloatChannel();
+ }
+
+ protected InterpolatedFloatChannel() {
+ super(null, null);
+ _values = null;
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/JointChannel.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/JointChannel.java
new file mode 100644
index 0000000..9f3c8cd
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/JointChannel.java
@@ -0,0 +1,185 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.clip;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+
+import com.ardor3d.annotation.SavableFactory;
+import com.ardor3d.extension.animation.skeletal.Joint;
+import com.ardor3d.math.type.ReadOnlyQuaternion;
+import com.ardor3d.math.type.ReadOnlyTransform;
+import com.ardor3d.math.type.ReadOnlyVector3;
+import com.ardor3d.util.export.InputCapsule;
+import com.ardor3d.util.export.OutputCapsule;
+
+/**
+ * Transform animation channel, specifically geared towards describing the motion of skeleton joints.
+ */
+@SavableFactory(factoryMethod = "initSavable")
+public class JointChannel extends TransformChannel {
+
+ /** A name prepended to joint indices to identify them as joint channels. */
+ public static final String JOINT_CHANNEL_NAME = "_jnt";
+
+ /** The human readable version of the name. */
+ private final String _jointName;
+
+ /** The joint index. */
+ private int _jointIndex;
+
+ /**
+ * Construct a new JointChannel.
+ *
+ * @param joint
+ * the joint to pull name and index from.
+ * @param times
+ * our time offset values.
+ * @param rotations
+ * the rotations to set on this channel at each time offset.
+ * @param translations
+ * the translations to set on this channel at each time offset.
+ * @param scales
+ * the scales to set on this channel at each time offset.
+ */
+ public JointChannel(final Joint joint, final float[] times, final ReadOnlyQuaternion[] rotations,
+ final ReadOnlyVector3[] translations, final ReadOnlyVector3[] scales) {
+ super(JointChannel.JOINT_CHANNEL_NAME + joint.getIndex(), times, rotations, translations, scales);
+ _jointName = joint.getName();
+ _jointIndex = joint.getIndex();
+ }
+
+ /**
+ * Construct a new JointChannel.
+ *
+ * @param jointName
+ * the human readable name of the joint
+ * @param jointIndex
+ * the index of the joint.
+ * @param times
+ * our time offset values.
+ * @param rotations
+ * the rotations to set on this channel at each time offset.
+ * @param translations
+ * the translations to set on this channel at each time offset.
+ * @param scales
+ * the scales to set on this channel at each time offset.
+ */
+ public JointChannel(final String jointName, final int jointIndex, final float[] times,
+ final ReadOnlyQuaternion[] rotations, final ReadOnlyVector3[] translations, final ReadOnlyVector3[] scales) {
+ super(JointChannel.JOINT_CHANNEL_NAME + jointIndex, times, rotations, translations, scales);
+ _jointName = jointName;
+ _jointIndex = jointIndex;
+ }
+
+ /**
+ * Construct a new JointChannel.
+ *
+ * @param joint
+ * the index of the joint.
+ * @param times
+ * our time offset values.
+ * @param transforms
+ * the transform to set on this channel at each time offset.
+ */
+ public JointChannel(final Joint joint, final float[] times, final ReadOnlyTransform[] transforms) {
+ super(JointChannel.JOINT_CHANNEL_NAME + joint.getIndex(), times, transforms);
+ _jointName = joint.getName();
+ _jointIndex = joint.getIndex();
+ }
+
+ /**
+ * @return the human readable version of the associated joint's name.
+ */
+ public String getJointName() {
+ return _jointName;
+ }
+
+ /**
+ * @return the joint index this channel targets.
+ */
+ public int getJointIndex() {
+ return _jointIndex;
+ }
+
+ @Override
+ protected JointChannel newChannel(final String name, final float[] times, final ReadOnlyQuaternion[] rotations,
+ final ReadOnlyVector3[] translations, final ReadOnlyVector3[] scales) {
+ return new JointChannel(_jointName, _jointIndex, times, rotations, translations, scales);
+ }
+
+ @Override
+ public void setCurrentSample(final int sampleIndex, final double progressPercent, final Object applyTo) {
+ super.setCurrentSample(sampleIndex, progressPercent, applyTo);
+
+ final JointData jointData = (JointData) applyTo;
+ jointData.setJointIndex(_jointIndex);
+ }
+
+ @Override
+ public JointData createStateDataObject(final AnimationClipInstance instance) {
+ return new JointData();
+ }
+
+ public JointData getJointData(final int index, final JointData store) {
+ JointData rVal = store;
+ if (rVal == null) {
+ rVal = new JointData();
+ }
+ super.getTransformData(index, rVal);
+ rVal.setJointIndex(_jointIndex);
+ return rVal;
+ }
+
+ // /////////////////
+ // Methods for Savable
+ // /////////////////
+
+ @Override
+ public Class<? extends JointChannel> getClassTag() {
+ return this.getClass();
+ }
+
+ @Override
+ public void write(final OutputCapsule capsule) throws IOException {
+ super.write(capsule);
+ capsule.write(_jointName, "jointName", null);
+ }
+
+ @Override
+ public void read(final InputCapsule capsule) throws IOException {
+ super.read(capsule);
+ final String jointName = capsule.readString("jointName", null);
+ try {
+ final Field field1 = JointChannel.class.getDeclaredField("_jointName");
+ field1.setAccessible(true);
+ field1.set(this, jointName);
+ } catch (final Exception e) {
+ e.printStackTrace();
+ }
+
+ if (_channelName.startsWith(JointChannel.JOINT_CHANNEL_NAME)) {
+ _jointIndex = Integer.parseInt(_channelName.substring(JointChannel.JOINT_CHANNEL_NAME.length()));
+ } else {
+ _jointIndex = -1;
+ }
+ }
+
+ public static JointChannel initSavable() {
+ return new JointChannel();
+ }
+
+ protected JointChannel() {
+ super();
+ _jointName = null;
+ _jointIndex = -1;
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/JointData.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/JointData.java
new file mode 100644
index 0000000..af97ebd
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/JointData.java
@@ -0,0 +1 @@
+/** * Copyright (c) 2008-2012 Ardor Labs, Inc. * * This file is part of Ardor3D. * * Ardor3D is free software: you can redistribute it and/or modify it * under the terms of its license which may be found in the accompanying * LICENSE file or at <http://www.ardor3d.com/LICENSE>. */ package com.ardor3d.extension.animation.skeletal.clip; import java.io.IOException; import com.ardor3d.util.export.InputCapsule; import com.ardor3d.util.export.OutputCapsule; /** * Describes transform of a joint. */ public class JointData extends TransformData { private int _jointIndex; /** * Construct a new, identity joint data object. */ public JointData(final int index) { _jointIndex = index; } /** * Construct a new joint data object, copying the value of the given source. * * @param source * our source to copy. * @throws NullPointerException * if source is null. */ public JointData(final JointData source) { set(source); } /** * Construct a new, identity joint data object. */ public JointData() {} /** * Copy the source's values into this transform data object. * * @param source * our source to copy. * @throws NullPointerException * if source is null. */ public void set(final JointData source) { super.set(source); _jointIndex = source._jointIndex; } public int getJointIndex() { return _jointIndex; } public void setJointIndex(final int jointIndex) { _jointIndex = jointIndex; } /** * Blend this transform with the given transform. * * @param blendTo * The transform to blend to * @param blendWeight * The blend weight * @param store * The transform store. * @return The blended transform. */ @Override public TransformData blend(final TransformData blendTo, final double blendWeight, final TransformData store) { TransformData rVal = store; if (rVal == null) { rVal = new JointData(_jointIndex); } else if (rVal instanceof JointData) { ((JointData) rVal).setJointIndex(_jointIndex); } return super.blend(blendTo, blendWeight, rVal); } // ///////////////// // Methods for Savable // ///////////////// @Override public void write(final OutputCapsule capsule) throws IOException { super.write(capsule); capsule.write(_jointIndex, "jointIndex", 0); } @Override public void read(final InputCapsule capsule) throws IOException { super.read(capsule); _jointIndex = capsule.readInt("jointIndex", 0); } } \ No newline at end of file
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/TransformChannel.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/TransformChannel.java
new file mode 100644
index 0000000..07598f5
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/TransformChannel.java
@@ -0,0 +1,320 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.clip;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.List;
+import java.util.logging.Logger;
+
+import com.ardor3d.annotation.SavableFactory;
+import com.ardor3d.math.Quaternion;
+import com.ardor3d.math.Vector3;
+import com.ardor3d.math.type.ReadOnlyQuaternion;
+import com.ardor3d.math.type.ReadOnlyTransform;
+import com.ardor3d.math.type.ReadOnlyVector3;
+import com.ardor3d.util.export.CapsuleUtils;
+import com.ardor3d.util.export.InputCapsule;
+import com.ardor3d.util.export.OutputCapsule;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+/**
+ * An animation channel consisting of a series of transforms interpolated over time.
+ */
+@SavableFactory(factoryMethod = "initSavable")
+public class TransformChannel extends AbstractAnimationChannel {
+
+ private static final Logger logger = Logger.getLogger(TransformChannel.class.getName());
+
+ // XXX: Perhaps we could optimize memory by reusing sample objects that are the same from one index to the next.
+ // XXX: Could then also optimize execution time by checking object equality (==) and skipping (s)lerps.
+
+ /** Our rotation samples. */
+ private final ReadOnlyQuaternion[] _rotations;
+
+ /** Our translation samples. */
+ private final ReadOnlyVector3[] _translations;
+
+ /** Our scale samples. */
+ private final ReadOnlyVector3[] _scales;
+
+ private final Quaternion _compQuat1 = new Quaternion();
+ private final Quaternion _compQuat2 = new Quaternion();
+ private final Vector3 _compVect1 = new Vector3();
+
+ /**
+ * Construct a new TransformChannel.
+ *
+ * @param channelName
+ * our name.
+ * @param times
+ * our time offset values.
+ * @param rotations
+ * the rotations to set on this channel at each time offset.
+ * @param translations
+ * the translations to set on this channel at each time offset.
+ * @param scales
+ * the scales to set on this channel at each time offset.
+ */
+ public TransformChannel(final String channelName, final float[] times, final ReadOnlyQuaternion[] rotations,
+ final ReadOnlyVector3[] translations, final ReadOnlyVector3[] scales) {
+ super(channelName, times);
+
+ if (rotations.length != times.length || translations.length != times.length || scales.length != times.length) {
+ throw new IllegalArgumentException("All provided arrays must be the same length! Channel: " + channelName);
+ }
+
+ // Construct our data
+ _rotations = new ReadOnlyQuaternion[rotations.length];
+ int i = 0;
+ for (final ReadOnlyQuaternion q : rotations) {
+ _rotations[i++] = new Quaternion(q);
+ }
+ _translations = new ReadOnlyVector3[translations.length];
+ i = 0;
+ for (final ReadOnlyVector3 v : translations) {
+ _translations[i++] = new Vector3(v);
+ }
+ _scales = new ReadOnlyVector3[scales.length];
+ i = 0;
+ for (final ReadOnlyVector3 v : scales) {
+ _scales[i++] = new Vector3(v);
+ }
+ }
+
+ /**
+ * Construct a new TransformChannel.
+ *
+ * @param channelName
+ * our name.
+ * @param times
+ * our time offset values.
+ * @param transforms
+ * the transform to set on this channel at each time offset. These are separated into rotation, scale and
+ * translation components. Note that supplying transforms with non-rotational matrices (with built in
+ * shear, scale.) will produce a warning and may not give you the expected result.
+ */
+ public TransformChannel(final String channelName, final float[] times, final ReadOnlyTransform[] transforms) {
+ super(channelName, times);
+
+ // Construct our data
+ _rotations = new ReadOnlyQuaternion[transforms.length];
+ _translations = new ReadOnlyVector3[transforms.length];
+ _scales = new ReadOnlyVector3[transforms.length];
+
+ for (int i = 0; i < transforms.length; i++) {
+ final ReadOnlyTransform transform = transforms[i];
+ if (!transform.isRotationMatrix()) {
+ TransformChannel.logger.warning("TransformChannel '" + channelName
+ + "' supplied transform with non-rotational matrices. May have unexpected results.");
+ }
+ _rotations[i] = new Quaternion().fromRotationMatrix(transform.getMatrix()).normalizeLocal();
+ _translations[i] = new Vector3(transform.getTranslation());
+ _scales[i] = new Vector3(transform.getScale());
+ }
+ }
+
+ @Override
+ public void setCurrentSample(final int sampleIndex, final double progressPercent, final Object applyTo) {
+ final TransformData transformData = (TransformData) applyTo;
+
+ // shortcut if we are fully on one sample or the next
+ if (progressPercent == 0.0f) {
+ transformData.setRotation(_rotations[sampleIndex]);
+ transformData.setTranslation(_translations[sampleIndex]);
+ transformData.setScale(_scales[sampleIndex]);
+ return;
+ } else if (progressPercent == 1.0f) {
+ transformData.setRotation(_rotations[sampleIndex + 1]);
+ transformData.setTranslation(_translations[sampleIndex + 1]);
+ transformData.setScale(_scales[sampleIndex + 1]);
+ return;
+ }
+
+ // Apply (s)lerp and set in transform
+ _compQuat1.slerpLocal(_rotations[sampleIndex], _rotations[sampleIndex + 1], progressPercent, _compQuat2);
+ transformData.setRotation(_compQuat1);
+
+ _compVect1.lerpLocal(_translations[sampleIndex], _translations[sampleIndex + 1], progressPercent);
+ transformData.setTranslation(_compVect1);
+ _compVect1.lerpLocal(_scales[sampleIndex], _scales[sampleIndex + 1], progressPercent);
+ transformData.setScale(_compVect1);
+ }
+
+ /**
+ * Apply a specific index of this channel to a TransformData object.
+ *
+ * @param index
+ * the index to grab.
+ * @param store
+ * the TransformData to store in. If null, a new one is created.
+ * @return our resulting TransformData.
+ */
+ public TransformData getTransformData(final int index, final TransformData store) {
+ TransformData rVal = store;
+ if (rVal == null) {
+ rVal = new TransformData();
+ }
+ rVal.setRotation(_rotations[index]);
+ rVal.setScale(_scales[index]);
+ rVal.setTranslation(_translations[index]);
+ return rVal;
+ }
+
+ @Override
+ public TransformChannel getSubchannelBySample(final String name, final int startSample, final int endSample) {
+ if (startSample > endSample) {
+ throw new IllegalArgumentException("startSample > endSample");
+ }
+ if (endSample >= getSampleCount()) {
+ throw new IllegalArgumentException("endSample >= getSampleCount()");
+ }
+
+ final int samples = endSample - startSample + 1;
+ final float[] times = new float[samples];
+ final ReadOnlyQuaternion[] rotations = new ReadOnlyQuaternion[samples];
+ final ReadOnlyVector3[] translations = new ReadOnlyVector3[samples];
+ final ReadOnlyVector3[] scales = new ReadOnlyVector3[samples];
+
+ for (int i = 0; i < samples; i++) {
+ times[i] = _times[i + startSample];
+ rotations[i] = _rotations[i + startSample];
+ translations[i] = _translations[i + startSample];
+ scales[i] = _scales[i + startSample];
+ }
+
+ return newChannel(name, times, rotations, translations, scales);
+ }
+
+ @Override
+ public AbstractAnimationChannel getSubchannelByTime(final String name, final float startTime, final float endTime) {
+ if (startTime > endTime) {
+ throw new IllegalArgumentException("startTime > endTime");
+ }
+ final List<Float> times = Lists.newArrayList();
+ final List<ReadOnlyQuaternion> rotations = Lists.newArrayList();
+ final List<ReadOnlyVector3> translations = Lists.newArrayList();
+ final List<ReadOnlyVector3> scales = Lists.newArrayList();
+
+ final TransformData tData = new TransformData();
+
+ // Add start sample
+ updateSample(startTime, tData);
+ times.add(0f);
+ rotations.add(tData.getRotation());
+ translations.add(tData.getTranslation());
+ scales.add(tData.getScale());
+
+ // Add mid samples
+ for (int i = 0; i < getSampleCount(); i++) {
+ final float time = _times[i];
+ if (time > startTime && time < endTime) {
+ times.add(time - startTime);
+ rotations.add(_rotations[i]);
+ translations.add(_translations[i]);
+ scales.add(_scales[i]);
+ }
+ }
+
+ // Add end sample
+ updateSample(endTime, tData);
+ times.add(endTime - startTime);
+ rotations.add(tData.getRotation());
+ translations.add(tData.getTranslation());
+ scales.add(tData.getScale());
+
+ final float[] timesArray = new float[times.size()];
+ int i = 0;
+ for (final float time : times) {
+ timesArray[i++] = time;
+ }
+ // return
+ return newChannel(name, timesArray, rotations.toArray(new ReadOnlyQuaternion[rotations.size()]),
+ translations.toArray(new ReadOnlyVector3[translations.size()]),
+ scales.toArray(new ReadOnlyVector3[scales.size()]));
+ }
+
+ public ImmutableList<ReadOnlyVector3> getTranslations() {
+ return ImmutableList.copyOf(_translations);
+ }
+
+ public ImmutableList<ReadOnlyVector3> getScales() {
+ return ImmutableList.copyOf(_scales);
+ }
+
+ public ImmutableList<ReadOnlyQuaternion> getRotations() {
+ return ImmutableList.copyOf(_rotations);
+ }
+
+ protected TransformChannel newChannel(final String name, final float[] times, final ReadOnlyQuaternion[] rotations,
+ final ReadOnlyVector3[] translations, final ReadOnlyVector3[] scales) {
+ return new TransformChannel(name, times, rotations, translations, scales);
+ }
+
+ @Override
+ public TransformData createStateDataObject(final AnimationClipInstance instance) {
+ return new TransformData();
+ }
+
+ // /////////////////
+ // Methods for Savable
+ // /////////////////
+
+ public Class<? extends TransformChannel> getClassTag() {
+ return this.getClass();
+ }
+
+ @Override
+ public void write(final OutputCapsule capsule) throws IOException {
+ super.write(capsule);
+ capsule.write(CapsuleUtils.asSavableArray(_rotations), "rotations", null);
+ capsule.write(CapsuleUtils.asSavableArray(_scales), "scales", null);
+ capsule.write(CapsuleUtils.asSavableArray(_translations), "translations", null);
+ }
+
+ @Override
+ public void read(final InputCapsule capsule) throws IOException {
+ super.read(capsule);
+ final ReadOnlyQuaternion[] rotations = CapsuleUtils.asArray(capsule.readSavableArray("rotations", null),
+ ReadOnlyQuaternion.class);
+ final ReadOnlyVector3[] scales = CapsuleUtils.asArray(capsule.readSavableArray("scales", null),
+ ReadOnlyVector3.class);
+ final ReadOnlyVector3[] translations = CapsuleUtils.asArray(capsule.readSavableArray("translations", null),
+ ReadOnlyVector3.class);
+ try {
+ final Field field1 = TransformChannel.class.getDeclaredField("_rotations");
+ field1.setAccessible(true);
+ field1.set(this, rotations);
+
+ final Field field2 = TransformChannel.class.getDeclaredField("_scales");
+ field2.setAccessible(true);
+ field2.set(this, scales);
+
+ final Field field3 = TransformChannel.class.getDeclaredField("_translations");
+ field3.setAccessible(true);
+ field3.set(this, translations);
+ } catch (final Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static TransformChannel initSavable() {
+ return new TransformChannel();
+ }
+
+ protected TransformChannel() {
+ super(null, null);
+ _rotations = null;
+ _translations = null;
+ _scales = null;
+ }
+} \ No newline at end of file
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/TransformData.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/TransformData.java
new file mode 100644
index 0000000..15d4bc3
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/TransformData.java
@@ -0,0 +1,190 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.clip;
+
+import java.io.IOException;
+
+import com.ardor3d.math.Quaternion;
+import com.ardor3d.math.Transform;
+import com.ardor3d.math.Vector3;
+import com.ardor3d.math.type.ReadOnlyQuaternion;
+import com.ardor3d.math.type.ReadOnlyVector3;
+import com.ardor3d.scenegraph.Spatial;
+import com.ardor3d.util.export.InputCapsule;
+import com.ardor3d.util.export.OutputCapsule;
+import com.ardor3d.util.export.Savable;
+
+/**
+ * Describes a relative transform as a Quaternion-Vector-Vector tuple. We use QVV to make it simpler to do LERP
+ * blending.
+ */
+public class TransformData implements Savable {
+
+ /** Our rotation. */
+ private final Quaternion _rotation = new Quaternion(Quaternion.IDENTITY);
+
+ /** Our scale. */
+ private final Vector3 _scale = new Vector3(Vector3.ONE);
+
+ /** Our translation. */
+ private final Vector3 _translation = new Vector3(Vector3.ZERO);
+
+ /**
+ * Construct a new, identity transform data object.
+ */
+ public TransformData() {}
+
+ /**
+ * Construct a new transform data object, copying the value of the given source.
+ *
+ * @param source
+ * our source to copy.
+ * @throws NullPointerException
+ * if source is null.
+ */
+ public TransformData(final TransformData source) {
+ set(source);
+ }
+
+ /**
+ * Copy the source's values into this transform data object.
+ *
+ * @param source
+ * our source to copy.
+ * @throws NullPointerException
+ * if source is null.
+ */
+ public void set(final TransformData source) {
+ _rotation.set(source.getRotation());
+ _scale.set(source.getScale());
+ _translation.set(source.getTranslation());
+ }
+
+ public Quaternion getRotation() {
+ return _rotation;
+ }
+
+ public void setRotation(final ReadOnlyQuaternion rotation) {
+ _rotation.set(rotation);
+ }
+
+ public void setRotation(final double x, final double y, final double z, final double w) {
+ _rotation.set(x, y, z, w);
+ }
+
+ public Vector3 getScale() {
+ return _scale;
+ }
+
+ public void setScale(final ReadOnlyVector3 scale) {
+ _scale.set(scale);
+ }
+
+ public void setScale(final double x, final double y, final double z) {
+ _scale.set(x, y, z);
+ }
+
+ public Vector3 getTranslation() {
+ return _translation;
+ }
+
+ public void setTranslation(final ReadOnlyVector3 translation) {
+ _translation.set(translation);
+ }
+
+ public void setTranslation(final double x, final double y, final double z) {
+ _translation.set(x, y, z);
+ }
+
+ public void applyTo(final Transform transform) {
+ transform.setIdentity();
+ transform.setRotation(getRotation());
+ transform.setScale(getScale());
+ transform.setTranslation(getTranslation());
+ }
+
+ public void applyTo(final Spatial spat) {
+ spat.setTransform(Transform.IDENTITY);
+ spat.setRotation(getRotation());
+ spat.setScale(getScale());
+ spat.setTranslation(getTranslation());
+ }
+
+ /**
+ * Blend this transform with the given transform.
+ *
+ * @param blendTo
+ * The transform to blend to
+ * @param blendWeight
+ * The blend weight
+ * @param store
+ * The transform store.
+ * @return The blended transform.
+ */
+ public TransformData blend(final TransformData blendTo, final double blendWeight, final TransformData store) {
+ TransformData tData = store;
+ if (tData == null) {
+ tData = new TransformData();
+ }
+
+ double weight, scaleX = 0.0, scaleY = 0.0, scaleZ = 0.0, transX = 0.0, transY = 0.0, transZ = 0.0;
+ Vector3 vectorData;
+
+ weight = 1 - blendWeight;
+
+ vectorData = getTranslation();
+ transX += vectorData.getX() * weight;
+ transY += vectorData.getY() * weight;
+ transZ += vectorData.getZ() * weight;
+
+ vectorData = getScale();
+ scaleX += vectorData.getX() * weight;
+ scaleY += vectorData.getY() * weight;
+ scaleZ += vectorData.getZ() * weight;
+
+ weight = blendWeight;
+
+ vectorData = blendTo.getTranslation();
+ transX += vectorData.getX() * weight;
+ transY += vectorData.getY() * weight;
+ transZ += vectorData.getZ() * weight;
+
+ vectorData = blendTo.getScale();
+ scaleX += vectorData.getX() * weight;
+ scaleY += vectorData.getY() * weight;
+ scaleZ += vectorData.getZ() * weight;
+
+ tData.setScale(scaleX, scaleY, scaleZ);
+ tData.setTranslation(transX, transY, transZ);
+ Quaternion.slerp(_rotation, blendTo.getRotation(), weight, tData._rotation);
+ return tData;
+ }
+
+ // /////////////////
+ // Methods for Savable
+ // /////////////////
+
+ public Class<? extends TransformData> getClassTag() {
+ return this.getClass();
+ }
+
+ public void write(final OutputCapsule capsule) throws IOException {
+ capsule.write(_rotation, "rotation", new Quaternion(Quaternion.IDENTITY));
+ capsule.write(_scale, "scale", new Vector3(Vector3.ONE));
+ capsule.write(_translation, "translation", new Vector3(Vector3.ZERO));
+ }
+
+ public void read(final InputCapsule capsule) throws IOException {
+ setRotation((Quaternion) capsule.readSavable("rotation", new Quaternion(Quaternion.IDENTITY)));
+ setScale((Vector3) capsule.readSavable("scale", new Vector3(Vector3.ONE)));
+ setTranslation((Vector3) capsule.readSavable("rotation", new Vector3(Vector3.ZERO)));
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/TriggerCallback.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/TriggerCallback.java
new file mode 100644
index 0000000..292c497
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/TriggerCallback.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.clip;
+
+import com.ardor3d.extension.animation.skeletal.AnimationManager;
+import com.ardor3d.extension.animation.skeletal.SkeletonPose;
+
+/**
+ * Callback interface for logic to execute when a Trigger from a TriggerChannel is encountered.
+ */
+public interface TriggerCallback {
+
+ /**
+ * Called once per encounter of a TriggerParam. Not guaranteed to be called if, for example, the window defined in
+ * the TriggerParam is very small and/or the frame rate is really bad.
+ *
+ * @param applyToPose
+ * @param manager
+ */
+ void doTrigger(SkeletonPose applyToPose, AnimationManager manager);
+
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/TriggerChannel.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/TriggerChannel.java
new file mode 100644
index 0000000..e6af818
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/TriggerChannel.java
@@ -0,0 +1,166 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.clip;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.List;
+
+import com.ardor3d.annotation.SavableFactory;
+import com.ardor3d.util.export.InputCapsule;
+import com.ardor3d.util.export.OutputCapsule;
+import com.google.common.collect.Lists;
+
+/**
+ * An animation source channel consisting of keyword samples indicating when a specific trigger condition is met. Each
+ * channel can only be in one keyword "state" at a given moment in time.
+ */
+@SavableFactory(factoryMethod = "initSavable")
+public class TriggerChannel extends AbstractAnimationChannel {
+
+ /** Our key samples. */
+ protected final String[] _keys;
+
+ /**
+ * Construct a new TriggerChannel.
+ *
+ * @param channelName
+ * the name of this channel.
+ * @param times
+ * the time samples
+ * @param keys
+ * our key samples. Entries may be null. Should have as many entries as the times array.
+ */
+ public TriggerChannel(final String channelName, final float[] times, final String[] keys) {
+ super(channelName, times);
+ _keys = keys == null ? null : new String[keys.length];
+ if (_keys != null) {
+ System.arraycopy(keys, 0, _keys, 0, keys.length);
+ }
+ }
+
+ @Override
+ public TriggerData createStateDataObject(final AnimationClipInstance instance) {
+ return new TriggerData();
+ }
+
+ /**
+ * @return our keys array
+ */
+ public String[] getKeys() {
+ return _keys;
+ }
+
+ @Override
+ public void setCurrentSample(final int sampleIndex, final double progressPercent, final Object applyTo) {
+ final TriggerData triggerData = (TriggerData) applyTo;
+
+ // set key
+ final int index = progressPercent != 1.0 ? sampleIndex : sampleIndex + 1;
+ triggerData.arm(index, _keys[index]);
+ }
+
+ @Override
+ public AbstractAnimationChannel getSubchannelBySample(final String name, final int startSample, final int endSample) {
+ if (startSample > endSample) {
+ throw new IllegalArgumentException("startSample > endSample");
+ }
+ if (endSample >= getSampleCount()) {
+ throw new IllegalArgumentException("endSample >= getSampleCount()");
+ }
+
+ final int samples = endSample - startSample + 1;
+ final float[] times = new float[samples];
+ final String[] keys = new String[samples];
+
+ for (int i = 0; i <= samples; i++) {
+ times[i] = _times[i + startSample];
+ keys[i] = _keys[i + startSample];
+ }
+
+ return new TriggerChannel(name, times, keys);
+ }
+
+ @Override
+ public AbstractAnimationChannel getSubchannelByTime(final String name, final float startTime, final float endTime) {
+ if (startTime > endTime) {
+ throw new IllegalArgumentException("startTime > endTime");
+ }
+ final List<Float> times = Lists.newArrayList();
+ final List<String> keys = Lists.newArrayList();
+
+ final TriggerData tData = new TriggerData();
+
+ // Add start sample
+ updateSample(startTime, tData);
+ times.add(0f);
+ keys.add(tData.getCurrentTrigger());
+
+ // Add mid samples
+ for (int i = 0; i < getSampleCount(); i++) {
+ final float time = _times[i];
+ updateSample(time, tData);
+ if (time > startTime && time < endTime) {
+ times.add(time - startTime);
+ keys.add(_keys[i]);
+ }
+ }
+
+ // Add end sample
+ updateSample(endTime, tData);
+ times.add(endTime - startTime);
+ keys.add(tData.getCurrentTrigger());
+
+ final float[] timesArray = new float[times.size()];
+ int i = 0;
+ for (final float time : times) {
+ timesArray[i++] = time;
+ }
+ // return
+ return new TriggerChannel(name, timesArray, keys.toArray(new String[keys.size()]));
+ }
+
+ // /////////////////
+ // Methods for Savable
+ // /////////////////
+
+ public Class<? extends TriggerChannel> getClassTag() {
+ return this.getClass();
+ }
+
+ @Override
+ public void write(final OutputCapsule capsule) throws IOException {
+ super.write(capsule);
+ capsule.write(_keys, "keys", null);
+ }
+
+ @Override
+ public void read(final InputCapsule capsule) throws IOException {
+ super.read(capsule);
+ final String[] keys = capsule.readStringArray("keys", null);
+ try {
+ final Field field1 = TriggerChannel.class.getDeclaredField("_keys");
+ field1.setAccessible(true);
+ field1.set(this, keys);
+ } catch (final Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static TriggerChannel initSavable() {
+ return new TriggerChannel();
+ }
+
+ protected TriggerChannel() {
+ super(null, null);
+ _keys = null;
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/TriggerData.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/TriggerData.java
new file mode 100644
index 0000000..7c0220f
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/clip/TriggerData.java
@@ -0,0 +1,76 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.clip;
+
+import java.util.List;
+
+import com.google.common.collect.Lists;
+
+/**
+ * Transient class that maintains the current triggers and armed status for a TriggerChannel.
+ */
+public class TriggerData {
+
+ /** The current trigger name. */
+ private final List<String> _currentTriggers = Lists.newArrayList();
+
+ /**
+ * The current channel sample index. We keep this to make sure we don't miss two channels in a row with the same
+ * trigger name.
+ */
+ private int _currentIndex = -1;
+
+ /** If true, we are armed - we have had a trigger set and have not executed it. */
+ private boolean _armed = false;
+
+ public List<String> getCurrentTriggers() {
+ return _currentTriggers;
+ }
+
+ public String getCurrentTrigger() {
+ return _currentTriggers.isEmpty() ? null : _currentTriggers.get(_currentTriggers.size() - 1);
+ }
+
+ public int getCurrentIndex() {
+ return _currentIndex;
+ }
+
+ public void setArmed(final boolean armed) {
+ _armed = armed;
+ }
+
+ public boolean isArmed() {
+ return _armed;
+ }
+
+ /**
+ * Try to set a given trigger/index as armed. If we already have this trigger and index set, we don't change the
+ * state of armed.
+ *
+ * @param trigger
+ * our trigger name
+ * @param index
+ * our sample index
+ */
+ public synchronized void arm(final int index, final String... triggers) {
+ if (triggers == null || triggers.length == 0) {
+ _currentTriggers.clear();
+ _armed = false;
+ } else if (index != _currentIndex) {
+ _currentTriggers.clear();
+ for (final String t : triggers) {
+ _currentTriggers.add(t);
+ }
+ _armed = true;
+ }
+ _currentIndex = index;
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/layer/AnimationLayer.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/layer/AnimationLayer.java
new file mode 100644
index 0000000..eebf755
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/layer/AnimationLayer.java
@@ -0,0 +1,367 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.layer;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Logger;
+
+import com.ardor3d.extension.animation.skeletal.AnimationManager;
+import com.ardor3d.extension.animation.skeletal.state.AbstractFiniteState;
+import com.ardor3d.extension.animation.skeletal.state.AbstractTransitionState;
+import com.ardor3d.extension.animation.skeletal.state.StateOwner;
+import com.ardor3d.extension.animation.skeletal.state.SteadyState;
+import com.google.common.collect.Maps;
+
+/**
+ * Animation layers are essentially independent state machines, managed by a single AnimationManager. Each maintains a
+ * set of possible "steady states" - main states that the layer can be in. The layer can only be in one state at any
+ * given time. It may transition between states, provided that a path is defined for transition from the current state
+ * to the desired one.
+ */
+public class AnimationLayer implements StateOwner {
+
+ /** The layer name of the default base layer. */
+ public static final String BASE_LAYER_NAME = "-BASE_LAYER-";
+
+ /** our class logger */
+ private static final Logger logger = Logger.getLogger(AnimationLayer.class.getName());
+
+ /** Our animation states */
+ private final Map<String, SteadyState> _steadyStates = Maps.newHashMap();
+
+ /** Our current animation state */
+ private AbstractFiniteState _currentState;
+
+ /** our parent manager */
+ private AnimationManager _manager;
+
+ /** our layer blending module */
+ private LayerBlender _layerBlender;
+
+ /** the name of this layer, used for identification, so best if unique. */
+ private final String _name;
+
+ /** A map of general transitions for moving from the current state to another. */
+ private final Map<String, AbstractTransitionState> _transitions = Maps.newHashMap();
+
+ /**
+ * Construct a new AnimationLayer.
+ *
+ * @param name
+ * the name of this layer, used for id purposes.
+ */
+ public AnimationLayer(final String name) {
+ _name = name;
+ }
+
+ /**
+ * Force the current state of the machine to the steady state with the given name. Used to set the FSM's initial
+ * state.
+ *
+ * @param stateName
+ * the name of our state. If null, or is not present in this state machine, the current state is not
+ * changed.
+ * @param rewind
+ * if true, the clip(s) in the given state will be rewound by setting its start time to the current time
+ * and setting it active.
+ * @return true if succeeds
+ */
+ public boolean setCurrentState(final String stateName, final boolean rewind) {
+ if (stateName != null) {
+ final AbstractFiniteState state = _steadyStates.get(stateName);
+ if (state != null) {
+ setCurrentState(state, rewind);
+ return true;
+ } else {
+ AnimationLayer.logger.warning("unable to find SteadyState named: " + stateName);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Sets the current finite state to the given state. Generally for transitional state use.
+ *
+ * @param state
+ * our new state. If null, then no state is currently set on this layer.
+ * @param rewind
+ * if true, the clip(s) in the given state will be rewound by setting its start time to the current time
+ * and setting it active.
+ */
+ public void setCurrentState(final AbstractFiniteState state, final boolean rewind) {
+ _currentState = state;
+ if (state != null) {
+ state.setLastStateOwner(this);
+ if (rewind) {
+ state.resetClips(_manager);
+ }
+ }
+ }
+
+ /**
+ * Set the currently playing state on this layer to null.
+ */
+ public void clearCurrentState() {
+ setCurrentState((AbstractFiniteState) null, false);
+ }
+
+ /**
+ * Attempt to perform a transition. First, check the current state to see if it has a transition for the given key.
+ * If not, check this layer for a general purpose transition. If no transition is found, this does nothing.
+ *
+ * @param key
+ * the transition key, a string key used to look up a transition in the current animation state.
+ * @return true if there is a current state and we were able to do the given transition.
+ */
+ public boolean doTransition(final String key) {
+ final AbstractFiniteState state = getCurrentState();
+ // see if current state has a transition
+ if (state instanceof SteadyState) {
+ final SteadyState steadyState = (SteadyState) state;
+ AbstractFiniteState nextState = steadyState.doTransition(key, this);
+ if (nextState == null) {
+ // no transition found, check if there is a global transition
+ AbstractTransitionState transition = _transitions.get(key);
+ if (transition == null) {
+ transition = _transitions.get("*");
+ }
+ if (transition != null) {
+ nextState = transition.doTransition(state, this);
+ }
+ }
+
+ if (nextState != null) {
+ if (nextState != state) {
+ setCurrentState(nextState, false);
+ return true;
+ }
+ }
+ } else if (state == null) {
+ // check if there is a global transition
+ AbstractTransitionState transition = _transitions.get(key);
+ if (transition == null) {
+ transition = _transitions.get("*");
+ }
+ if (transition != null) {
+ setCurrentState(transition.doTransition(state, this), true);
+ return true;
+ }
+ }
+
+ // no transition found
+ return false;
+ }
+
+ /**
+ * @return a set containing the names of our steady states.
+ */
+ public Set<String> getSteadyStateNames() {
+ return _steadyStates.keySet();
+ }
+
+ /**
+ * @return the current active finite state in this machine.
+ */
+ public AbstractFiniteState getCurrentState() {
+ return _currentState;
+ }
+
+ /**
+ * @param stateName
+ * the name of the steady state we are looking for.
+ * @return our animation state, or null if none is found.
+ */
+ public SteadyState getSteadyState(final String stateName) {
+ return _steadyStates.get(stateName);
+ }
+
+ /**
+ * Add a new steady state to this layer.
+ *
+ * @param state
+ * the state to add.
+ */
+ public void addSteadyState(final SteadyState state) {
+ if (state == null) {
+ throw new IllegalArgumentException("state must not be null.");
+ }
+ _steadyStates.put(state.getName(), state);
+ }
+
+ /**
+ * Remove the given steady state from our layer
+ *
+ * @param state
+ * the state to remove
+ * @return true if the state was found for removal.
+ */
+ public boolean removeSteadyState(final SteadyState state) {
+ return _steadyStates.remove(state.getName()) != null;
+ }
+
+ /**
+ * Sets a reference back to the manager associated with this layer. Generally this is handled by the
+ * AnimationManager itself as layers are added to it.
+ *
+ * @param manager
+ * the animation manager.
+ */
+ public void setManager(final AnimationManager manager) {
+ _manager = manager;
+ }
+
+ /**
+ * @return the manager associated with this layer.
+ */
+ public AnimationManager getManager() {
+ return _manager;
+ }
+
+ /**
+ * @return the name of this layer, used for identification, so best if unique.
+ */
+ public String getName() {
+ return _name;
+ }
+
+ /**
+ * @return a source data mapping for the channels involved in the current state/transition of this layer.
+ */
+ public Map<String, ? extends Object> getCurrentSourceData() {
+ if (getLayerBlender() != null) {
+ return getLayerBlender().getBlendedSourceData(getManager());
+ }
+
+ final AbstractFiniteState state = getCurrentState();
+ if (state != null) {
+ return state.getCurrentSourceData(getManager());
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Update the layer blender in this animation layer to properly point to the previous layer.
+ *
+ * @param previousLayer
+ * the layer before this layer in the animation manager.
+ */
+ public void updateLayerBlending(final AnimationLayer previousLayer) {
+ final LayerBlender blender = getLayerBlender();
+ if (blender != null) {
+ blender.setLayerA(previousLayer);
+ blender.setLayerB(this);
+ }
+ }
+
+ /**
+ * @param layerBlender
+ * the layer blender to use for combining this layer's contents with others in the animation manager.
+ */
+ public void setLayerBlender(final LayerBlender layerBlender) {
+ _layerBlender = layerBlender;
+ }
+
+ /**
+ * @return the layer blender used for combining this layer's contents with others in the animation manager.
+ */
+ public LayerBlender getLayerBlender() {
+ return _layerBlender;
+ }
+
+ @Override
+ public void replaceState(final AbstractFiniteState currentState, final AbstractFiniteState newState) {
+ if (getCurrentState() == currentState) {
+ setCurrentState(newState, false);
+ }
+ }
+
+ /**
+ * Add a new general transition to this layer.
+ *
+ * @param keyword
+ * the reference key for the added transition.
+ * @param state
+ * the transition state to add.
+ * @throws IllegalArgumentException
+ * if keyword or state are null.
+ */
+ public void addTransition(final String keyword, final AbstractTransitionState state) {
+ if (state == null) {
+ throw new IllegalArgumentException("state must not be null.");
+ }
+ if (keyword == null) {
+ throw new IllegalArgumentException("keyword must not be null.");
+ }
+ _transitions.put(keyword, state);
+ }
+
+ /**
+ *
+ * @param keyword
+ * the reference key for the transition state we wish to pull from this layer.
+ * @return the transition related to the given keyword, or null if none are found.
+ * @throws IllegalArgumentException
+ * if keyword is null.
+ */
+ public AbstractTransitionState getTransition(final String keyword) {
+ if (keyword == null) {
+ throw new IllegalArgumentException("keyword must not be null.");
+ }
+ return _transitions.get(keyword);
+ }
+
+ /**
+ * @return a Set of the transition state keywords used by this layer.
+ */
+ public Set<String> getTransitionKeywords() {
+ return _transitions.keySet();
+ }
+
+ /**
+ * Remove a transition state by keyword.
+ *
+ * @param keyword
+ * the reference key for the transition state we wish to remove from this layer.
+ * @return the removed transition, or null if none was found by the given keyword.
+ * @throws IllegalArgumentException
+ * if keyword is null.
+ */
+ public AbstractTransitionState removeTransition(final String keyword) {
+ if (keyword == null) {
+ throw new IllegalArgumentException("keyword must not be null.");
+ }
+ return _transitions.remove(keyword);
+ }
+
+ /**
+ * Remove the first instance of a specific transition state from this layer.
+ *
+ * @param transition
+ * the transition state we wish to remove from this steady state.
+ * @return true if we found and removed the given transition.
+ * @throws IllegalArgumentException
+ * if transition is null.
+ */
+ public boolean removeTransition(final AbstractTransitionState transition) {
+ if (transition == null) {
+ throw new IllegalArgumentException("transition must not be null.");
+ }
+ for (final String keyword : _transitions.keySet()) {
+ if (_transitions.get(keyword).equals(transition)) {
+ _transitions.remove(keyword);
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/layer/LayerBlender.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/layer/LayerBlender.java
new file mode 100644
index 0000000..77075c1
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/layer/LayerBlender.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.layer;
+
+import java.util.Map;
+
+import com.ardor3d.extension.animation.skeletal.AnimationManager;
+
+/**
+ * Describes a class capable of blending together two AnimationLayers in some way.
+ */
+public interface LayerBlender {
+
+ /**
+ * @param layer
+ * our first layer
+ */
+ void setLayerA(AnimationLayer layer);
+
+ /**
+ * @param layer
+ * our second layer
+ */
+ void setLayerB(AnimationLayer layer);
+
+ /**
+ * @param key
+ * a String for retrieving the blend weight from the layer manager's values store.
+ */
+ void setBlendKey(String key);
+
+ /**
+ * @return the String for retrieving the blend weight from the layer manager's values store.
+ */
+ String getBlendKey();
+
+ /**
+ * @param manager
+ * the manager this is being called from
+ * @return a key-value map representing the blended data from both animation layers.
+ */
+ Map<String, ? extends Object> getBlendedSourceData(AnimationManager manager);
+
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/layer/LayerLERPBlender.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/layer/LayerLERPBlender.java
new file mode 100644
index 0000000..d1c8043
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/layer/LayerLERPBlender.java
@@ -0,0 +1,69 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.layer;
+
+import java.util.Map;
+
+import com.ardor3d.extension.animation.skeletal.AnimationManager;
+import com.ardor3d.extension.animation.skeletal.blendtree.BinaryLERPSource;
+
+/**
+ * <p>
+ * A layer blender that uses linear interpolation to merge the results of two layers.
+ * </p>
+ * See also {@link BinaryLERPSource#combineSourceData(Map, Map, double)}
+ */
+public class LayerLERPBlender implements LayerBlender {
+
+ /** A key into the related AnimationManager's values store for pulling blend weight. */
+ private String _blendKey;
+
+ /** Our first layer... generally the "prior" layer. */
+ private AnimationLayer _layerA;
+
+ /** Our second layer... generally the layer we were added to. */
+ private AnimationLayer _layerB;
+
+ public String getBlendKey() {
+ return _blendKey;
+ }
+
+ public void setBlendKey(final String blendKey) {
+ _blendKey = blendKey;
+ }
+
+ public AnimationLayer getLayerA() {
+ return _layerA;
+ }
+
+ public void setLayerA(final AnimationLayer layer) {
+ _layerA = layer;
+ }
+
+ public AnimationLayer getLayerB() {
+ return _layerB;
+ }
+
+ public void setLayerB(final AnimationLayer layer) {
+ _layerB = layer;
+ }
+
+ public Map<String, ? extends Object> getBlendedSourceData(final AnimationManager manager) {
+ // grab our data maps from the two layers...
+ // set A
+ final Map<String, ? extends Object> sourceAData = getLayerA().getCurrentSourceData();
+ // set B
+ final Map<String, ? extends Object> sourceBData = getLayerB().getCurrentState() != null ? getLayerB()
+ .getCurrentState().getCurrentSourceData(manager) : null;
+
+ return BinaryLERPSource.combineSourceData(sourceAData, sourceBData, manager.getValuesStore().get(_blendKey));
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/AbstractFiniteState.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/AbstractFiniteState.java
new file mode 100644
index 0000000..0353f93
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/AbstractFiniteState.java
@@ -0,0 +1,103 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.state;
+
+import java.util.Map;
+
+import com.ardor3d.extension.animation.skeletal.AnimationManager;
+import com.ardor3d.extension.animation.skeletal.layer.AnimationLayer;
+
+/**
+ * Base class for a finite state in our finite state machine.
+ */
+public abstract class AbstractFiniteState {
+
+ /** The global time this state was last set to use as a start reference. Meant for subclass use only. */
+ private double _globalStartTime = 0;
+
+ /**
+ * The last holder of this state. Used when we are transitioning and need to ask someone to swap us with another
+ * state.
+ */
+ private StateOwner _lastOwner;
+
+ /**
+ * Reset the clip instances held by this state's blend tree (or other leaf nodes in our blend tree) to a start time
+ * using the current global time from the given manager.
+ *
+ * @param manager
+ * our manager.
+ */
+ public void resetClips(final AnimationManager manager) {
+ resetClips(manager, manager.getCurrentGlobalTime());
+ }
+
+ /**
+ * Reset the clip instances held by this state's blend tree (or other leaf nodes in our blend tree) to given start
+ * time.
+ *
+ * @param manager
+ * our manager.
+ * @param globalStartTime
+ * the new start time to use.
+ */
+ public void resetClips(final AnimationManager manager, final double globalStartTime) {
+ _globalStartTime = globalStartTime;
+ }
+
+ /**
+ * Update this state using the current global time.
+ *
+ * @param globalTime
+ * the current global time.
+ * @param layer
+ * the layer this state belongs to.
+ */
+ public abstract void update(final double globalTime, final AnimationLayer layer);
+
+ /**
+ * Post update. If the state has no more clips and no end transition, this will clear this state from the layer.
+ *
+ * @param layer
+ * the layer this state belongs to.
+ */
+ public abstract void postUpdate(final AnimationLayer layer);
+
+ /**
+ * @return the current map of source channel data for this layer.
+ */
+ public abstract Map<String, ? extends Object> getCurrentSourceData(AnimationManager manager);
+
+ /**
+ * @param owner
+ * the last holder of this state. Used when we are transitioning and need to ask someone to swap us with
+ * another state. Generally only called by the AnimationLayer or by transitioning states that reference
+ * other states.
+ */
+ public void setLastStateOwner(final StateOwner owner) {
+ _lastOwner = owner;
+ }
+
+ /**
+ * @return the last holder of this state.
+ * @see #setLastStateOwner(StateOwner)
+ */
+ public StateOwner getLastStateOwner() {
+ return _lastOwner;
+ }
+
+ /**
+ * @return the global time this state was last set to use as a start reference. Meant for subclass use only.
+ */
+ protected double getGlobalStartTime() {
+ return _globalStartTime;
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/AbstractTransitionState.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/AbstractTransitionState.java
new file mode 100644
index 0000000..66efc81
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/AbstractTransitionState.java
@@ -0,0 +1,148 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.state;
+
+import com.ardor3d.extension.animation.skeletal.layer.AnimationLayer;
+
+/**
+ * Base class for transition states - states responsible for moving between other finite states.
+ */
+public abstract class AbstractTransitionState extends AbstractFiniteState {
+
+ /**
+ * @see {@link AbstractTransitionState#setStartWindow(double)}
+ */
+ private double _startWindow = -1;
+
+ /**
+ * @see {@link AbstractTransitionState#setEndWindow(double)}
+ */
+ private double _endWindow = -1;
+
+ /**
+ * The name of the steady state we want the Animation Layer to be in at the end of the transition.
+ */
+ private final String _targetState;
+
+ /**
+ * Construct a new transition state.
+ *
+ * @param targetState
+ * the name of the steady state we want the Animation Layer to be in at the end of the transition.
+ */
+ protected AbstractTransitionState(final String targetState) {
+ _targetState = targetState;
+ }
+
+ /**
+ * @return the name of the steady state we want the Animation Layer to be in at the end of the transition.
+ */
+ public String getTargetState() {
+ return _targetState;
+ }
+
+ /**
+ * @param startWindow
+ * our new start window value. If greater than 0, this transition is only valid if the current time is >=
+ * startWindow. Note that animations are separate from states, so time scaling an animation will not
+ * affect transition windows directly and must be factored into the start/end values.
+ */
+ public void setStartWindow(final double startWindow) {
+ _startWindow = startWindow;
+ }
+
+ /**
+ * @return our start window value.
+ * @see #setStartWindow(double)
+ */
+ public double getStartWindow() {
+ return _startWindow;
+ }
+
+ /**
+ * @param endWindow
+ * our new end window value. If greater than 0, this transition is only valid if the current time is <=
+ * endWindow. Note that animations are separate from states, so time scaling an animation will not affect
+ * transition windows directly and must be factored into the start/end values.
+ */
+ public void setEndWindow(final double endWindow) {
+ _endWindow = endWindow;
+ }
+
+ /**
+ * @return our end window value.
+ * @see #setEndWindow(double)
+ */
+ public double getEndWindow() {
+ return _endWindow;
+ }
+
+ /**
+ * Request that this state perform a transition to another.
+ *
+ * @param callingState
+ * the state calling for this transition.
+ * @param layer
+ * the layer our state belongs to.
+ * @return the new state to transition to. May be null if the transition was not possible or was ignored for some
+ * reason.
+ */
+ public final AbstractFiniteState doTransition(final AbstractFiniteState callingState, final AnimationLayer layer) {
+ if (layer.getCurrentState() == null) {
+ return null;
+ }
+ final double time = layer.getManager().getCurrentGlobalTime() - layer.getCurrentState().getGlobalStartTime();
+ if (isInTimeWindow(time)) {
+ return getTransitionState(callingState, layer);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * @param localTime
+ * the state's local time
+ * @return true if the given time lands within our window.
+ */
+ private boolean isInTimeWindow(final double localTime) {
+ if (getStartWindow() <= 0) {
+ if (getEndWindow() <= 0) {
+ // no window, so true
+ return true;
+ } else {
+ // just check end
+ return localTime <= getEndWindow();
+ }
+ } else {
+ if (getEndWindow() <= 0) {
+ // just check start
+ return localTime >= getStartWindow();
+ } else if (getStartWindow() <= getEndWindow()) {
+ // check between start and end
+ return getStartWindow() <= localTime && localTime <= getEndWindow();
+ } else {
+ // start is greater than end, so there are two windows.
+ return localTime >= getStartWindow() || localTime <= getEndWindow();
+ }
+ }
+ }
+
+ /**
+ * Do the transition logic for this transition state.
+ *
+ * @param callingState
+ * the state calling for this transition.
+ * @param layer
+ * the layer our state belongs to.
+ * @return the state to transition to. Often ourselves.
+ */
+ abstract AbstractFiniteState getTransitionState(AbstractFiniteState callingState, AnimationLayer layer);
+} \ No newline at end of file
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/AbstractTwoStateLerpTransition.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/AbstractTwoStateLerpTransition.java
new file mode 100644
index 0000000..57d9d6f
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/AbstractTwoStateLerpTransition.java
@@ -0,0 +1,212 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.state;
+
+import java.util.Map;
+
+import com.ardor3d.extension.animation.skeletal.AnimationManager;
+import com.ardor3d.extension.animation.skeletal.blendtree.BinaryLERPSource;
+import com.ardor3d.extension.animation.skeletal.layer.AnimationLayer;
+import com.ardor3d.math.MathUtils;
+import com.google.common.collect.Maps;
+
+/**
+ * An abstract transition state that blends between two other states.
+ */
+public abstract class AbstractTwoStateLerpTransition extends AbstractTransitionState implements StateOwner {
+
+ /**
+ * Describes how blending should be applied over the span of the transition.
+ */
+ public enum BlendType {
+ /** Blend linearly. */
+ Linear,
+
+ /** Blend using a cubic S-curve: 3t^2 - 2t^3 */
+ SCurve3,
+
+ /** Blend using a quintic S-curve: 6t^5 - 15t^4 + 10t^3 */
+ SCurve5;
+ }
+
+ /** Our initial or start state. */
+ private AbstractFiniteState _stateA;
+
+ /** Our target or end state. */
+ private AbstractFiniteState _stateB;
+
+ /** The length of time for the transition. */
+ private double _fadeTime = 0;
+
+ /** The global time when the transition started. */
+ private double _start = 0;
+
+ /**
+ * A percentage value of how much of each state to blend for our final result, generated based on time and blend
+ * type.
+ */
+ private double _percent = 0;
+
+ /** The method to use in determining how much of each state to use based on the current time of the transition. */
+ private BlendType _type = BlendType.Linear;
+
+ /** The blended source data. */
+ private Map<String, Object> _sourceData;
+
+ /**
+ * Construct a new AbstractTwoStateLerpTransition.
+ *
+ * @param targetState
+ * the name of the steady state we want the Animation Layer to be in at the end of the transition.
+ * @param fadeTime
+ * the amount of time we should take to do the transition.
+ * @param type
+ * the way we should interpolate the weighting during the transition.
+ */
+ protected AbstractTwoStateLerpTransition(final String targetState, final double fadeTime, final BlendType type) {
+ super(targetState);
+ setFadeTime(fadeTime);
+ setBlendType(type);
+ }
+
+ public AbstractFiniteState getStateA() {
+ return _stateA;
+ }
+
+ /**
+ * @param stateA
+ * sets the start state. Updates the state's owner to point to this transition.
+ */
+ public void setStateA(final AbstractFiniteState stateA) {
+ if (stateA == this) {
+ throw new IllegalArgumentException("Can not set state A to self.");
+ }
+ _stateA = stateA;
+ if (_stateA != null) {
+ _stateA.setLastStateOwner(this);
+ }
+
+ // clear the _sourceData, the new state probably has different transform data
+ if (_sourceData != null) {
+ _sourceData.clear();
+ }
+ }
+
+ public AbstractFiniteState getStateB() {
+ return _stateB;
+ }
+
+ /**
+ * @param stateA
+ * sets the end state. Updates the state's owner to point to this transition.
+ */
+ public void setStateB(final AbstractFiniteState stateB) {
+ if (stateB == this) {
+ throw new IllegalArgumentException("Can not set state B to self.");
+ }
+ _stateB = stateB;
+ if (_stateB != null) {
+ _stateB.setLastStateOwner(this);
+ }
+
+ // clear the _sourceData, the new state probably has different transform data
+ if (_sourceData != null) {
+ _sourceData.clear();
+ }
+ }
+
+ public void setFadeTime(final double fadeTime) {
+ _fadeTime = fadeTime;
+ }
+
+ public double getFadeTime() {
+ return _fadeTime;
+ }
+
+ public void setBlendType(final BlendType type) {
+ _type = type;
+ }
+
+ public BlendType getBlendType() {
+ return _type;
+ }
+
+ protected void setStart(final double start) {
+ _start = start;
+ }
+
+ protected double getStart() {
+ return _start;
+ }
+
+ protected void setPercent(final double percent) {
+ _percent = percent;
+ }
+
+ protected double getPercent() {
+ return _percent;
+ }
+
+ @Override
+ public void update(final double globalTime, final AnimationLayer layer) {
+ final double currentTime = globalTime - getStart();
+
+ // if we're outside the fade time...
+ if (currentTime > getFadeTime()) {
+ // transition over to end state
+ getLastStateOwner().replaceState(this, getStateB());
+ return;
+ }
+
+ // figure out our weight using time, total time and fade type
+ final double percent = currentTime / getFadeTime();
+
+ switch (getBlendType()) {
+ case SCurve3:
+ setPercent(MathUtils.scurve3(percent));
+ break;
+ case SCurve5:
+ setPercent(MathUtils.scurve5(percent));
+ break;
+ case Linear:
+ default:
+ setPercent(percent);
+ break;
+ }
+ }
+
+ @Override
+ public Map<String, ? extends Object> getCurrentSourceData(final AnimationManager manager) {
+ // grab our data maps from the two states
+ final Map<String, ? extends Object> sourceAData = getStateA() != null ? getStateA().getCurrentSourceData(
+ manager) : null;
+ final Map<String, ? extends Object> sourceBData = getStateB() != null ? getStateB().getCurrentSourceData(
+ manager) : null;
+
+ // reuse previous _sourceData transforms to avoid re-creating
+ // too many new transform data objects. This assumes that a
+ // same state always returns the same transform data objects.
+ if (_sourceData == null) {
+ _sourceData = Maps.newHashMap();
+ }
+ return BinaryLERPSource.combineSourceData(sourceAData, sourceBData, getPercent(), _sourceData);
+ }
+
+ public void replaceState(final AbstractFiniteState currentState, final AbstractFiniteState newState) {
+ if (newState != null) {
+ if (getStateA() == currentState) {
+ setStateA(newState);
+ } else if (getStateB() == currentState) {
+ setStateB(newState);
+ }
+ }
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/FadeTransitionState.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/FadeTransitionState.java
new file mode 100644
index 0000000..70020fe
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/FadeTransitionState.java
@@ -0,0 +1,74 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.state;
+
+import com.ardor3d.extension.animation.skeletal.layer.AnimationLayer;
+
+/**
+ * A transition that blends over a given time from one animation state to another, beginning the target clip from local
+ * time 0 at the start of the transition. This is best used with two clips that have similar motions.
+ */
+public class FadeTransitionState extends AbstractTwoStateLerpTransition {
+
+ /**
+ * Construct a new FadeTransitionState.
+ *
+ * @param targetState
+ * the name of the steady state we want the Animation Layer to be in at the end of the transition.
+ * @param fadeTime
+ * the amount of time we should take to do the transition.
+ * @param type
+ * the way we should interpolate the weighting during the transition.
+ */
+ public FadeTransitionState(final String targetState, final double fadeTime, final BlendType type) {
+ super(targetState, fadeTime, type);
+ }
+
+ @Override
+ public AbstractFiniteState getTransitionState(final AbstractFiniteState callingState, final AnimationLayer layer) {
+ // grab current time as our start
+ setStart(layer.getManager().getCurrentGlobalTime());
+ // set "current" start state
+ setStateA(callingState);
+ // set "target" end state
+ setStateB(layer.getSteadyState(getTargetState()));
+ if (getStateB() == null) {
+ return null;
+ }
+ // restart end state.
+ getStateB().resetClips(layer.getManager(), getStart());
+ return this;
+ }
+
+ @Override
+ public void update(final double globalTime, final AnimationLayer layer) {
+ super.update(globalTime, layer);
+
+ // update both of our states
+ if (getStateA() != null) {
+ getStateA().update(globalTime, layer);
+ }
+ if (getStateB() != null) {
+ getStateB().update(globalTime, layer);
+ }
+ }
+
+ @Override
+ public void postUpdate(final AnimationLayer layer) {
+ // post update both of our states
+ if (getStateA() != null) {
+ getStateA().postUpdate(layer);
+ }
+ if (getStateB() != null) {
+ getStateB().postUpdate(layer);
+ }
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/FrozenTransitionState.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/FrozenTransitionState.java
new file mode 100644
index 0000000..355020a
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/FrozenTransitionState.java
@@ -0,0 +1,72 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.state;
+
+import com.ardor3d.extension.animation.skeletal.layer.AnimationLayer;
+
+/**
+ * <p>
+ * A two state transition that freezes the starting state at its current position and blends that over time with a
+ * target state. The target state moves forward in time during the blend as normal.
+ * </p>
+ *
+ * XXX: Might be able to make this more efficient by capturing the getCurrentSourceData of stateA and reusing.
+ */
+public class FrozenTransitionState extends AbstractTwoStateLerpTransition {
+
+ /**
+ * Construct a new FrozenTransitionState.
+ *
+ * @param the
+ * name of the steady state we want the Animation Layer to be in at the end of the transition.
+ * @param fadeTime
+ * the amount of time we should take to do the transition.
+ * @param type
+ * the way we should interpolate the weighting during the transition.
+ */
+ public FrozenTransitionState(final String targetState, final double fadeTime, final BlendType type) {
+ super(targetState, fadeTime, type);
+ }
+
+ @Override
+ public AbstractFiniteState getTransitionState(final AbstractFiniteState callingState, final AnimationLayer layer) {
+ // grab current time as our start
+ setStart(layer.getManager().getCurrentGlobalTime());
+ // set "frozen" start state
+ setStateA(callingState);
+ // set "target" end state
+ setStateB(layer.getSteadyState(getTargetState()));
+ if (getStateB() == null) {
+ return null;
+ }
+ // restart end state.
+ getStateB().resetClips(layer.getManager(), getStart());
+ return this;
+ }
+
+ @Override
+ public void update(final double globalTime, final AnimationLayer layer) {
+ super.update(globalTime, layer);
+
+ // update only the B state - the first is frozen
+ if (getStateB() != null) {
+ getStateB().update(globalTime, layer);
+ }
+ }
+
+ @Override
+ public void postUpdate(final AnimationLayer layer) {
+ // update only the B state - the first is frozen
+ if (getStateB() != null) {
+ getStateB().postUpdate(layer);
+ }
+ };
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/IgnoreTransitionState.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/IgnoreTransitionState.java
new file mode 100644
index 0000000..f9a8625
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/IgnoreTransitionState.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.state;
+
+import java.util.Map;
+
+import com.ardor3d.extension.animation.skeletal.AnimationManager;
+import com.ardor3d.extension.animation.skeletal.layer.AnimationLayer;
+
+/**
+ * Dummy transition - does not change current state.
+ */
+public class IgnoreTransitionState extends AbstractTransitionState {
+
+ /**
+ * Construct a new transition state.
+ *
+ * @param targetState
+ * the name of the state to transition to.
+ */
+ public IgnoreTransitionState() {
+ super(null);
+ }
+
+ @Override
+ public AbstractFiniteState getTransitionState(final AbstractFiniteState callingState, final AnimationLayer layer) {
+ // return calling state.
+ return callingState;
+ }
+
+ /**
+ * Ignored.
+ */
+ @Override
+ public Map<String, ? extends Object> getCurrentSourceData(final AnimationManager manager) {
+ return null;
+ }
+
+ /**
+ * Ignored.
+ */
+ @Override
+ public void update(final double globalTime, final AnimationLayer layer) {}
+
+ @Override
+ public void postUpdate(final AnimationLayer layer) {}
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/ImmediateTransitionState.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/ImmediateTransitionState.java
new file mode 100644
index 0000000..58b8ccd
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/ImmediateTransitionState.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.state;
+
+import java.util.Map;
+
+import com.ardor3d.extension.animation.skeletal.AnimationManager;
+import com.ardor3d.extension.animation.skeletal.layer.AnimationLayer;
+import com.google.common.collect.Maps;
+
+/**
+ * Cuts directly to the set target state, without any intermediate transition action.
+ */
+public class ImmediateTransitionState extends AbstractTransitionState {
+
+ /**
+ * Construct a new transition state.
+ *
+ * @param targetState
+ * the name of the state to transition to.
+ */
+ public ImmediateTransitionState(final String targetState) {
+ super(targetState);
+ }
+
+ @Override
+ public AbstractFiniteState getTransitionState(final AbstractFiniteState callingState, final AnimationLayer layer) {
+ // Pull our state from the layer
+ final AbstractFiniteState state = layer.getSteadyState(getTargetState());
+ if (state == null) {
+ return null;
+ }
+ // Reset to start
+ state.resetClips(layer.getManager());
+ // return state.
+ return state;
+ }
+
+ /**
+ * Ignored.
+ */
+ @Override
+ public Map<String, ? extends Object> getCurrentSourceData(final AnimationManager manager) {
+ return Maps.newHashMap();
+ }
+
+ /**
+ * Ignored.
+ */
+ @Override
+ public void update(final double globalTime, final AnimationLayer layer) {}
+
+ @Override
+ public void postUpdate(final AnimationLayer layer) {}
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/StateOwner.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/StateOwner.java
new file mode 100644
index 0000000..9de029e
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/StateOwner.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.state;
+
+/**
+ * Describes a class that holds onto and directly utilizes an AnimationState. This interface is meant to act as a
+ * callback so that states can properly handle certain types of transitions such as end transitions - setting new states
+ * into the correct place in the later/state hierarchy.
+ */
+public interface StateOwner {
+
+ /**
+ * Replace the given current state with the given new state
+ *
+ * @param currentState
+ * the state to replace
+ * @param newState
+ * the state to replace it with.
+ */
+ void replaceState(AbstractFiniteState currentState, AbstractFiniteState newState);
+
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/SteadyState.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/SteadyState.java
new file mode 100644
index 0000000..992d83d
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/SteadyState.java
@@ -0,0 +1,225 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.state;
+
+import java.util.Map;
+import java.util.Set;
+
+import com.ardor3d.extension.animation.skeletal.AnimationManager;
+import com.ardor3d.extension.animation.skeletal.blendtree.BlendTreeSource;
+import com.ardor3d.extension.animation.skeletal.layer.AnimationLayer;
+import com.google.common.collect.Maps;
+
+/**
+ * A "steady" state is an animation state that is concrete and stand-alone (vs. a state that handles transitioning
+ * between two states, for example.)
+ */
+public class SteadyState extends AbstractFiniteState {
+
+ /** The name of this state. */
+ private final String _name;
+
+ /** A map of possible transitions for moving from this state to another. */
+ private final Map<String, AbstractTransitionState> _transitions = Maps.newHashMap();
+
+ /** A transition to use if we reach the end of this state. May be null. */
+ private AbstractTransitionState _endTransition;
+
+ /** Our state may be a blend of multiple clips, etc. This is the root of our blend tree. */
+ private BlendTreeSource _sourceTree;
+
+ /**
+ * Create a new steady state.
+ *
+ * @param name
+ * the name of our new state. Immutable.
+ */
+ public SteadyState(final String name) {
+ _name = name;
+ }
+
+ /**
+ * @return the name of this state.
+ */
+ public String getName() {
+ return _name;
+ }
+
+ /**
+ * @return the transition to use if we reach the end of this state. May be null.
+ */
+ public AbstractTransitionState getEndTransition() {
+ return _endTransition;
+ }
+
+ /**
+ * @param endTransition
+ * a transition to use if we reach the end of this state. May be null.
+ */
+ public void setEndTransition(final AbstractTransitionState endTransition) {
+ _endTransition = endTransition;
+ }
+
+ /**
+ * @return the root of our blend tree
+ */
+ public BlendTreeSource getSourceTree() {
+ return _sourceTree;
+ }
+
+ /**
+ * @param tree
+ * the new root of our blend tree
+ */
+ public void setSourceTree(final BlendTreeSource tree) {
+ _sourceTree = tree;
+ }
+
+ /**
+ * Add a new possible transition to this state.
+ *
+ * @param keyword
+ * the reference key for the added transition.
+ * @param state
+ * the transition state to add.
+ * @throws IllegalArgumentException
+ * if keyword or state are null.
+ */
+ public void addTransition(final String keyword, final AbstractTransitionState state) {
+ if (state == null) {
+ throw new IllegalArgumentException("state must not be null.");
+ }
+ if (keyword == null) {
+ throw new IllegalArgumentException("keyword must not be null.");
+ }
+ _transitions.put(keyword, state);
+ }
+
+ /**
+ *
+ * @param keyword
+ * the reference key for the transition state we wish to pull from this steady state.
+ * @return the transition related to the given keyword, or null if none are found.
+ * @throws IllegalArgumentException
+ * if keyword is null.
+ */
+ public AbstractTransitionState getTransition(final String keyword) {
+ if (keyword == null) {
+ throw new IllegalArgumentException("keyword must not be null.");
+ }
+ return _transitions.get(keyword);
+ }
+
+ /**
+ * @return a Set of the transition state keywords used by this steady state.
+ */
+ public Set<String> getTransitionKeywords() {
+ return _transitions.keySet();
+ }
+
+ /**
+ * Remove a transition state by keyword.
+ *
+ * @param keyword
+ * the reference key for the transition state we wish to remove from this steady state.
+ * @return the removed transition, or null if none was found by the given keyword.
+ * @throws IllegalArgumentException
+ * if keyword is null.
+ */
+ public AbstractTransitionState removeTransition(final String keyword) {
+ if (keyword == null) {
+ throw new IllegalArgumentException("keyword must not be null.");
+ }
+ return _transitions.remove(keyword);
+ }
+
+ /**
+ * Remove the first instance of a specific transition state from this steady state.
+ *
+ * @param transition
+ * the transition state we wish to remove from this steady state.
+ * @return true if we found and removed the given transition.
+ * @throws IllegalArgumentException
+ * if transition is null.
+ */
+ public boolean removeTransition(final AbstractTransitionState transition) {
+ if (transition == null) {
+ throw new IllegalArgumentException("transition must not be null.");
+ }
+ for (final String keyword : _transitions.keySet()) {
+ if (_transitions.get(keyword).equals(transition)) {
+ _transitions.remove(keyword);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Request that this state transition to another.
+ *
+ * @param key
+ * a key to match against a map of possible transitions.
+ * @param layer
+ * the layer our state belongs to.
+ * @return the new state to transition to. May be null if the transition was not possible or was ignored for some
+ * reason.
+ */
+ public AbstractFiniteState doTransition(final String key, final AnimationLayer layer) {
+ AbstractTransitionState state = _transitions.get(key);
+ if (state == null) {
+ state = _transitions.get("*");
+ } else {
+ return state.doTransition(this, layer);
+ }
+ return null;
+ }
+
+ @Override
+ public void update(final double globalTime, final AnimationLayer layer) {
+ if (!getSourceTree().setTime(globalTime, layer.getManager())) {
+ final StateOwner lastOwner = getLastStateOwner();
+ if (_endTransition != null) {
+ // time to move to end transition
+ final AbstractFiniteState newState = _endTransition.doTransition(this, layer);
+ if (newState != null) {
+ newState.resetClips(layer.getManager());
+ newState.update(globalTime, layer);
+ }
+ if (this != newState) {
+ lastOwner.replaceState(this, newState);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void postUpdate(final AnimationLayer layer) {
+ if (!getSourceTree().isActive(layer.getManager())) {
+ final StateOwner lastOwner = getLastStateOwner();
+ if (_endTransition == null) {
+ // we're done. end.
+ lastOwner.replaceState(this, null);
+ }
+ }
+ }
+
+ @Override
+ public Map<String, ? extends Object> getCurrentSourceData(final AnimationManager manager) {
+ return getSourceTree().getSourceData(manager);
+ }
+
+ @Override
+ public void resetClips(final AnimationManager manager, final double globalStartTime) {
+ super.resetClips(manager, globalStartTime);
+ getSourceTree().resetClips(manager, globalStartTime);
+ }
+} \ No newline at end of file
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/SyncFadeTransitionState.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/SyncFadeTransitionState.java
new file mode 100644
index 0000000..a6a59bb
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/SyncFadeTransitionState.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.state;
+
+import com.ardor3d.extension.animation.skeletal.layer.AnimationLayer;
+
+/**
+ * A transition that blends over a given time from one animation state to another, synchronizing the target state to the
+ * initial state's start time. This is best used with two clips that have similar motions.
+ */
+public class SyncFadeTransitionState extends FadeTransitionState {
+
+ /**
+ * Construct a new SyncFadeTransitionState.
+ *
+ * @param targetState
+ * the name of the steady state we want the Animation Layer to be in at the end of the transition.
+ * @param fadeTime
+ * the amount of time we should take to do the transition.
+ * @param type
+ * the way we should interpolate the weighting during the transition.
+ */
+ public SyncFadeTransitionState(final String targetState, final double fadeTime, final BlendType type) {
+ super(targetState, fadeTime, type);
+ }
+
+ @Override
+ public AbstractFiniteState getTransitionState(final AbstractFiniteState callingState, final AnimationLayer layer) {
+ // grab current time as our start
+ setStart(layer.getManager().getCurrentGlobalTime());
+ // set "current" start state
+ setStateA(callingState);
+ // set "target" end state
+ setStateB(layer.getSteadyState(getTargetState()));
+ if (getStateB() == null) {
+ return null;
+ }
+ // grab current state's start time and set on end state
+ getStateB().resetClips(layer.getManager(), getStateA().getGlobalStartTime());
+ return this;
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/loader/ImportClipMap.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/loader/ImportClipMap.java
new file mode 100644
index 0000000..d8ba845
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/loader/ImportClipMap.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.state.loader;
+
+import java.util.logging.Logger;
+
+import com.ardor3d.extension.animation.skeletal.clip.AnimationClip;
+import com.ardor3d.extension.animation.skeletal.util.LoggingMap;
+
+/**
+ * This class essentially just wraps a String->Animation HashMap, providing extra logging when a clip is not found, or
+ * duplicate clips are added.
+ */
+public class ImportClipMap extends LoggingMap<String, AnimationClip> {
+
+ /** our class logger */
+ private static final Logger logger = Logger.getLogger(ImportClipMap.class.getName());
+
+ /**
+ * Add a clip to the store. Logs a warning if a clip by the same name was already in the store.
+ *
+ * @param clip
+ * the clip to add.
+ */
+ public void put(final AnimationClip clip) {
+ if (_wrappedMap.put(clip.getName(), clip) != null && isLogOnReplace()) {
+ ImportClipMap.logger.warning("Replaced clip in ImportClipStore with same name. " + clip.getName());
+ }
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/loader/InputStore.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/loader/InputStore.java
new file mode 100644
index 0000000..20ba2bc
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/loader/InputStore.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.state.loader;
+
+/**
+ * Storage class for items required during Layer import.
+ */
+public class InputStore {
+
+ private final ImportClipMap _clips = new ImportClipMap();
+
+ public ImportClipMap getClips() {
+ return _clips;
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/loader/JSLayerImporter.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/loader/JSLayerImporter.java
new file mode 100644
index 0000000..f31e8fc
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/loader/JSLayerImporter.java
@@ -0,0 +1,66 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.state.loader;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+import javax.script.ScriptEngine;
+import javax.script.ScriptEngineManager;
+import javax.script.ScriptException;
+
+import com.ardor3d.extension.animation.skeletal.AnimationManager;
+import com.ardor3d.util.resource.ResourceLocatorTool;
+import com.ardor3d.util.resource.ResourceSource;
+
+/**
+ * Import utility used for reading and constructing AnimationLayer information for a manager using JavaScript.
+ */
+public final class JSLayerImporter {
+
+ /**
+ * Populate a manager with layer information.
+ *
+ * @param layersFile
+ * the script file to read from.
+ * @param manager
+ * the manager to add layer information to.
+ * @param input
+ * the input store object, holding things like AnimationClips that the layers might need for
+ * construction.
+ * @return an output store object
+ * @throws IOException
+ * if there is a problem accessing the contents of the layersFile.
+ * @throws ScriptException
+ * if the script given has syntax/parse errors.
+ */
+ public static OutputStore addLayers(final ResourceSource layersFile, final AnimationManager manager,
+ final InputStore input) throws IOException, ScriptException {
+ final OutputStore output = new OutputStore();
+ final ScriptEngineManager mgr = new ScriptEngineManager();
+ final ScriptEngine jsEngine = mgr.getEngineByExtension("js");
+
+ jsEngine.put("MANAGER", manager);
+ jsEngine.put("INPUTSTORE", input);
+ jsEngine.put("OUTPUTSTORE", output);
+
+ // load our helper functions first...
+ jsEngine.eval(new InputStreamReader(ResourceLocatorTool.getClassPathResourceAsStream(JSLayerImporter.class,
+ "com/ardor3d/extension/animation/skeletal/state/loader/functions.js")));
+
+ // Add our user data...
+ jsEngine.eval(new InputStreamReader(layersFile.openStream()));
+
+ // return our output store, which may have useful items such as attachment points, etc.
+ return output;
+ }
+
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/loader/OutputClipSourceMap.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/loader/OutputClipSourceMap.java
new file mode 100644
index 0000000..cc98260
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/loader/OutputClipSourceMap.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.state.loader;
+
+import java.util.logging.Logger;
+
+import com.ardor3d.extension.animation.skeletal.blendtree.ClipSource;
+import com.ardor3d.extension.animation.skeletal.util.LoggingMap;
+
+/**
+ * This class essentially just wraps a String->ClipSource HashMap, providing extra logging when a ClipSource is not
+ * found, or duplicate ClipSources are added.
+ */
+public class OutputClipSourceMap extends LoggingMap<String, ClipSource> {
+
+ /** our class logger */
+ private static final Logger logger = Logger.getLogger(OutputClipSourceMap.class.getName());
+
+ /**
+ * Add a ClipSource to the store. Logs a warning if a source by the same name was already in the store.
+ *
+ * @param source
+ * the clip source to add.
+ */
+ public void put(final ClipSource source) {
+ final String key = source.getClip().getName();
+ if (_wrappedMap.put(key, source) != null) {
+ OutputClipSourceMap.logger.warning("Replaced clip source in OutputClipSourceMap with same name. " + key);
+ }
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/loader/OutputStore.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/loader/OutputStore.java
new file mode 100644
index 0000000..b72b548
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/state/loader/OutputStore.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.state.loader;
+
+import java.util.List;
+
+import com.ardor3d.extension.animation.skeletal.AttachmentPoint;
+import com.google.common.collect.Lists;
+
+/**
+ * Storage class for items created during Layer import.
+ */
+public class OutputStore {
+
+ /** List of attachment points created during layer import. */
+ private final List<AttachmentPoint> _attachments = Lists.newArrayList();
+
+ /** List of animation clip sources encountered during layer import. */
+ private final OutputClipSourceMap _usedClipSources = new OutputClipSourceMap();
+
+ public void addAttachmentPoint(final AttachmentPoint attach) {
+ _attachments.add(attach);
+ }
+
+ public List<AttachmentPoint> getAttachmentPoints() {
+ return _attachments;
+ }
+
+ public AttachmentPoint findAttachmentPoint(final String name) {
+ for (final AttachmentPoint attach : _attachments) {
+ if (name.equals(attach.getName())) {
+ return attach;
+ }
+ }
+ return null;
+ }
+
+ public OutputClipSourceMap getClipSources() {
+ return _usedClipSources;
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/util/LoggingMap.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/util/LoggingMap.java
new file mode 100644
index 0000000..ab7ea19
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/util/LoggingMap.java
@@ -0,0 +1,149 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.util;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Logger;
+
+import com.google.common.collect.Maps;
+
+/**
+ * This class essentially just wraps a KEY->VALUE HashMap, providing extra logging when a VALUE is not found, or
+ * duplicate VALUE objects are added. An optional callback may be provided to try to load values not present in this
+ * map. These are loaded using the string representation of the key and casting the return from Object to the value
+ * class. If the value is still null, a default value is returned.
+ */
+public class LoggingMap<KEY, VALUE> {
+
+ /** our class logger */
+ private static final Logger logger = Logger.getLogger(LoggingMap.class.getName());
+
+ /** Our map of values. */
+ protected final Map<KEY, VALUE> _wrappedMap = Maps.newHashMap();
+
+ /** If not null, this callback is asked to load the missing value using the key. */
+ private MissingCallback<KEY, VALUE> _missCallback = null;
+
+ /** A default value to return if a key is requested that does not exist. Defaults to null. */
+ private VALUE _defaultValue = null;
+
+ /** If true, we'll log anytime we set a key/value where the key already existed in the map. Defaults to true. */
+ private boolean _logOnReplace = true;
+
+ /** If true, we'll log anytime we try to retrieve a value by a key that is not in the map. Defaults to true. */
+ private boolean _logOnMissing = true;
+
+ /**
+ * Add a value to the store. Logs a warning if a value by the same key was already in the store and logOnReplace is
+ * true.
+ *
+ * @param key
+ * the key to add.
+ * @param value
+ * the value to add.
+ */
+ public void put(final KEY key, final VALUE value) {
+ if (_wrappedMap.put(key, value) != null) {
+ if (isLogOnReplace()) {
+ LoggingMap.logger.warning("Replaced value in map with same key. " + key);
+ }
+ }
+ }
+
+ /**
+ * Retrieves a value from our store by key. Logs a warning if a value by that key is not found and logOnMissing is
+ * true. If missing, defaultValue is returned.
+ *
+ * @param key
+ * the key of the value to find.
+ * @return the associated value, or null if none is found.
+ */
+ public VALUE get(final KEY key) {
+ VALUE value = _wrappedMap.get(key);
+ // value is null? ask callback.
+ if (value == null && getMissCallback() != null) {
+ value = getMissCallback().getValue(key);
+ if (value != null) {
+ // save for next time.
+ _wrappedMap.put(key, value);
+ }
+ }
+ // value still null...
+ if (value == null) {
+ if (isLogOnMissing()) {
+ LoggingMap.logger.warning("Value not found with key: " + key + " Returning defaultValue: "
+ + _defaultValue);
+ }
+ return getDefaultValue();
+ }
+ return value;
+ }
+
+ /**
+ * Removes the mapping for the given key.
+ *
+ * @param key
+ * the key of the value to remove.
+ * @return the previously associated value, or null if none was found.
+ */
+ public VALUE remove(final KEY key) {
+ return _wrappedMap.remove(key);
+ }
+
+ /**
+ * @return the number of key-value pairs stored in this object.
+ */
+ public int size() {
+ return _wrappedMap.size();
+ }
+
+ public void setDefaultValue(final VALUE defaultValue) {
+ this._defaultValue = defaultValue;
+ }
+
+ public VALUE getDefaultValue() {
+ return _defaultValue;
+ }
+
+ public void setLogOnReplace(final boolean logOnReplace) {
+ this._logOnReplace = logOnReplace;
+ }
+
+ public boolean isLogOnReplace() {
+ return _logOnReplace;
+ }
+
+ public void setLogOnMissing(final boolean logOnMissing) {
+ this._logOnMissing = logOnMissing;
+ }
+
+ public boolean isLogOnMissing() {
+ return _logOnMissing;
+ }
+
+ public MissingCallback<KEY, VALUE> getMissCallback() {
+ return _missCallback;
+ }
+
+ public void setMissCallback(final MissingCallback<KEY, VALUE> missCallback) {
+ _missCallback = missCallback;
+ }
+
+ public Set<KEY> keySet() {
+ return _wrappedMap.keySet();
+ }
+
+ public Collection<VALUE> values() {
+ return _wrappedMap.values();
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/util/MissingCallback.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/util/MissingCallback.java
new file mode 100644
index 0000000..391f7aa
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/util/MissingCallback.java
@@ -0,0 +1,17 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.util;
+
+public interface MissingCallback<KEY, VALUE> {
+
+ VALUE getValue(KEY key);
+
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/util/SkeletalDebugger.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/util/SkeletalDebugger.java
new file mode 100644
index 0000000..9ac819f
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/util/SkeletalDebugger.java
@@ -0,0 +1,347 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.util;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import com.ardor3d.bounding.BoundingSphere;
+import com.ardor3d.bounding.BoundingVolume;
+import com.ardor3d.extension.animation.skeletal.Joint;
+import com.ardor3d.extension.animation.skeletal.Skeleton;
+import com.ardor3d.extension.animation.skeletal.SkeletonPose;
+import com.ardor3d.extension.animation.skeletal.SkinnedMesh;
+import com.ardor3d.math.ColorRGBA;
+import com.ardor3d.math.MathUtils;
+import com.ardor3d.math.Matrix3;
+import com.ardor3d.math.Quaternion;
+import com.ardor3d.math.Transform;
+import com.ardor3d.math.Vector3;
+import com.ardor3d.math.type.ReadOnlyColorRGBA;
+import com.ardor3d.renderer.Camera;
+import com.ardor3d.renderer.Renderer;
+import com.ardor3d.renderer.queue.RenderBucketType;
+import com.ardor3d.renderer.state.WireframeState;
+import com.ardor3d.renderer.state.ZBufferState;
+import com.ardor3d.scenegraph.Node;
+import com.ardor3d.scenegraph.Spatial;
+import com.ardor3d.scenegraph.hint.LightCombineMode;
+import com.ardor3d.scenegraph.hint.TextureCombineMode;
+import com.ardor3d.scenegraph.shape.Pyramid;
+import com.ardor3d.scenegraph.shape.Sphere;
+import com.ardor3d.ui.text.BMText.Align;
+import com.ardor3d.ui.text.BasicText;
+
+/**
+ * Utility useful for drawing Skeletons found in a scene.
+ */
+public class SkeletalDebugger {
+ public static double BONE_RATIO = .05;
+ public static double JOINT_RATIO = .075;
+ public static double LABEL_RATIO = .5;
+
+ protected static final BoundingSphere measureSphere = new BoundingSphere();
+ protected static final BasicText jointText = BasicText.createDefaultTextLabel("", "");
+ static {
+ // No lighting, replace texturing
+ SkeletalDebugger.jointText.getSceneHints().setLightCombineMode(LightCombineMode.Off);
+ SkeletalDebugger.jointText.getSceneHints().setTextureCombineMode(TextureCombineMode.Replace);
+ // Do not queue... draw right away.
+ SkeletalDebugger.jointText.getSceneHints().setRenderBucketType(RenderBucketType.Skip);
+
+ SkeletalDebugger.jointText.setDefaultColor(ColorRGBA.YELLOW);
+ SkeletalDebugger.jointText.setAlign(Align.Center);
+
+ SkeletalDebugger.jointText.updateGeometricState(0);
+ }
+
+ /**
+ * Traverse the given scene and draw the currently posed Skeleton of any SkinnedMesh we encounter.
+ *
+ * @param scene
+ * the scene
+ * @param renderer
+ * the Renderer to draw with.
+ */
+ public static void drawSkeletons(final Spatial scene, final Renderer renderer) {
+ SkeletalDebugger.drawSkeletons(scene, renderer, false, false);
+ }
+
+ /**
+ * Traverse the given scene and draw the currently posed Skeleton of any SkinnedMesh we encounter. If showLabels is
+ * true, joint names will be drawn over the joints.
+ *
+ * @param scene
+ * the scene
+ * @param renderer
+ * the Renderer to draw with.
+ * @param allowSkeletonRedraw
+ * if true, we will draw the skeleton for every skinnedmesh we encounter, even if two skinnedmeshes are
+ * on the same skeleton.
+ * @param showLabels
+ * show the names of the joints over them.
+ */
+ public static void drawSkeletons(final Spatial scene, final Renderer renderer, final boolean allowSkeletonRedraw,
+ final boolean showLabels) {
+ SkeletalDebugger.drawSkeletons(scene, renderer, allowSkeletonRedraw, showLabels, new HashSet<Skeleton>());
+ }
+
+ private static void drawSkeletons(final Spatial scene, final Renderer renderer, final boolean allowSkeletonRedraw,
+ final boolean showLabels, final Set<Skeleton> alreadyDrawn) {
+ assert scene != null : "scene must not be null.";
+
+ // Check if we are a skinned mesh
+ boolean doChildren = true;
+ if (scene instanceof SkinnedMesh) {
+ final SkeletonPose pose = ((SkinnedMesh) scene).getCurrentPose();
+ if (pose != null && (allowSkeletonRedraw || !alreadyDrawn.contains(pose.getSkeleton()))) {
+ // If we're in view, go ahead and draw our associated skeleton pose
+ final Camera cam = Camera.getCurrentCamera();
+ final int state = cam.getPlaneState();
+ if (cam.contains(scene.getWorldBound()) != Camera.FrustumIntersect.Outside) {
+ SkeletalDebugger.drawSkeleton(pose, scene, renderer, showLabels);
+ alreadyDrawn.add(pose.getSkeleton());
+ } else {
+ doChildren = false;
+ }
+ cam.setPlaneState(state);
+ }
+ }
+
+ // Recurse down the scene if we're a Node and we were not flagged to ignore children.
+ if (doChildren && scene instanceof Node) {
+ final Node n = (Node) scene;
+ if (n.getNumberOfChildren() != 0) {
+ for (int i = n.getNumberOfChildren(); --i >= 0;) {
+ SkeletalDebugger.drawSkeletons(n.getChild(i), renderer, allowSkeletonRedraw, showLabels,
+ alreadyDrawn);
+ }
+ }
+ }
+ }
+
+ /**
+ * Draw a skeleton in a specific pose.
+ *
+ * @param pose
+ * the posed skeleton to draw
+ * @param scene
+ * @param renderer
+ * the Renderer to draw with.
+ * @param showLabels
+ * show the names of the joints over them.
+ */
+ private static void drawSkeleton(final SkeletonPose pose, final Spatial scene, final Renderer renderer,
+ final boolean showLabels) {
+ final Joint[] joints = pose.getSkeleton().getJoints();
+ final Transform[] globals = pose.getGlobalJointTransforms();
+
+ for (int i = 0, max = joints.length; i < max; i++) {
+ SkeletalDebugger.drawJoint(globals[i], scene, renderer);
+ final short parentIndex = joints[i].getParentIndex();
+
+ if (parentIndex != Joint.NO_PARENT) {
+ SkeletalDebugger.drawBone(globals[parentIndex], globals[i], scene, renderer);
+ }
+ }
+
+ if (showLabels) {
+ final boolean inOrtho = renderer.isInOrthoMode();
+ if (!inOrtho) {
+ renderer.setOrtho();
+ }
+ final Transform store = Transform.fetchTempInstance();
+ final Vector3 point = Vector3.fetchTempInstance();
+ for (int i = 0, max = joints.length; i < max; i++) {
+ SkeletalDebugger.jointText.setText(i + ". " + joints[i].getName());
+
+ final Transform t = scene.getWorldTransform().multiply(globals[i], store);
+ point.zero();
+ SkeletalDebugger.jointText.setTranslation(Camera.getCurrentCamera().getScreenCoordinates(
+ t.applyForward(point)));
+
+ final double size = SkeletalDebugger.LABEL_RATIO;
+ SkeletalDebugger.jointText.setScale(size, size, -size);
+
+ SkeletalDebugger.jointText.draw(renderer);
+ }
+ Transform.releaseTempInstance(store);
+ Vector3.releaseTempInstance(point);
+ if (!inOrtho) {
+ renderer.unsetOrtho();
+ }
+ }
+ }
+
+ /** Our bone shape. */
+ private static final Pyramid bone = new Pyramid("bone", 1, 1);
+ static {
+ // Alter the primitive to better represent our bone.
+ // Set color to white
+ SkeletalDebugger.setBoneColor(ColorRGBA.WHITE);
+ // Rotate the vertices of our bone to point along the Z axis instead of the Y.
+ SkeletalDebugger.bone.getMeshData().rotatePoints(
+ new Quaternion().fromAngleAxis(90 * MathUtils.DEG_TO_RAD, Vector3.UNIT_X));
+ // Drop the normals
+ SkeletalDebugger.bone.getMeshData().setNormalBuffer(null);
+
+ // No lighting or texturing
+ SkeletalDebugger.bone.getSceneHints().setLightCombineMode(LightCombineMode.Off);
+ SkeletalDebugger.bone.getSceneHints().setTextureCombineMode(TextureCombineMode.Off);
+ // Do not queue... draw right away.
+ SkeletalDebugger.bone.getSceneHints().setRenderBucketType(RenderBucketType.Skip);
+ // Draw in wire frame mode.
+ SkeletalDebugger.bone.setRenderState(new WireframeState());
+ // Respect existing zbuffer, and write into it
+ SkeletalDebugger.bone.setRenderState(new ZBufferState());
+ // Update our bone and make it ready for use.
+ SkeletalDebugger.bone.updateGeometricState(0);
+ }
+
+ /**
+ * Draw a single bone using the given world-space joint transformations.
+ *
+ * @param start
+ * our parent joint transform
+ * @param end
+ * our child joint transform
+ * @param scene
+ * @param renderer
+ * the Renderer to draw with.
+ */
+ private static void drawBone(final Transform start, final Transform end, final Spatial scene,
+ final Renderer renderer) {
+ // Determine our start and end points
+ final Vector3 stPnt = Vector3.fetchTempInstance();
+ final Vector3 endPnt = Vector3.fetchTempInstance();
+ start.applyForward(Vector3.ZERO, stPnt);
+ end.applyForward(Vector3.ZERO, endPnt);
+
+ // determine distance and use as a scale to elongate the bone
+ double scale = stPnt.distance(endPnt);
+ if (scale == 0) {
+ scale = MathUtils.ZERO_TOLERANCE;
+ }
+ final BoundingVolume vol = scene.getWorldBound();
+ double size = 1.0;
+ if (vol != null) {
+ SkeletalDebugger.measureSphere.setCenter(vol.getCenter());
+ SkeletalDebugger.measureSphere.setRadius(0);
+ SkeletalDebugger.measureSphere.mergeLocal(vol);
+ size = SkeletalDebugger.BONE_RATIO * SkeletalDebugger.measureSphere.getRadius();
+ }
+ SkeletalDebugger.bone.setWorldTransform(Transform.IDENTITY);
+ SkeletalDebugger.bone.setWorldScale(size, size, scale);
+
+ // determine center point of bone (translation).
+ final Vector3 store = Vector3.fetchTempInstance();
+ SkeletalDebugger.bone.setWorldTranslation(stPnt.add(endPnt, store).divideLocal(2.0));
+ Vector3.releaseTempInstance(store);
+
+ // Orient bone to point along axis formed by start and end points.
+ final Matrix3 orient = Matrix3.fetchTempInstance();
+ orient.lookAt(endPnt.subtractLocal(stPnt).normalizeLocal(), Vector3.UNIT_Y);
+ final Quaternion q = new Quaternion().fromRotationMatrix(orient);
+ q.normalizeLocal();
+ SkeletalDebugger.bone.setWorldRotation(q);
+
+ // Offset with skin transform
+ SkeletalDebugger.bone.setWorldTransform(scene.getWorldTransform().multiply(
+ SkeletalDebugger.bone.getWorldTransform(), null));
+
+ // Release some temp vars.
+ Matrix3.releaseTempInstance(orient);
+ Vector3.releaseTempInstance(stPnt);
+ Vector3.releaseTempInstance(endPnt);
+
+ // Draw our bone!
+ SkeletalDebugger.bone.draw(renderer);
+ }
+
+ /**
+ * Set the color of the joint label object used in showing joint names.
+ *
+ * @param color
+ * the new color to use for joint labels.
+ */
+ public static void setJointLabelColor(final ReadOnlyColorRGBA color) {
+ SkeletalDebugger.jointText.setDefaultColor(color);
+ }
+
+ /**
+ * Set the color of the bone object used in skeleton drawing.
+ *
+ * @param color
+ * the new color to use for skeleton bones.
+ */
+ public static void setBoneColor(final ReadOnlyColorRGBA color) {
+ SkeletalDebugger.bone.setSolidColor(color);
+ }
+
+ /** Our joint shape. */
+ private static final Sphere joint = new Sphere("joint", 3, 4, 0.5);
+ static {
+ // Alter the primitive to better represent our joint.
+ // Set color to cyan
+ SkeletalDebugger.setJointColor(ColorRGBA.RED);
+ // Drop the normals
+ SkeletalDebugger.joint.getMeshData().setNormalBuffer(null);
+
+ // No lighting or texturing
+ SkeletalDebugger.joint.getSceneHints().setLightCombineMode(LightCombineMode.Off);
+ SkeletalDebugger.joint.getSceneHints().setTextureCombineMode(TextureCombineMode.Off);
+ // Do not queue... draw right away.
+ SkeletalDebugger.joint.getSceneHints().setRenderBucketType(RenderBucketType.Skip);
+ // Draw in wire frame mode.
+ SkeletalDebugger.joint.setRenderState(new WireframeState());
+ // Respect existing zbuffer, and write into it
+ SkeletalDebugger.joint.setRenderState(new ZBufferState());
+ // Update our joint and make it ready for use.
+ SkeletalDebugger.joint.updateGeometricState(0);
+ }
+ private static Transform spTransform = new Transform();
+ private static Matrix3 spMatrix = new Matrix3();
+
+ /**
+ * Draw a single Joint using the given world-space joint transform.
+ *
+ * @param jntTransform
+ * our joint transform
+ * @param scene
+ * @param renderer
+ * the Renderer to draw with.
+ */
+ private static void drawJoint(final Transform jntTransform, final Spatial scene, final Renderer renderer) {
+ final BoundingVolume vol = scene.getWorldBound();
+ double size = 1.0;
+ if (vol != null) {
+ SkeletalDebugger.measureSphere.setCenter(vol.getCenter());
+ SkeletalDebugger.measureSphere.setRadius(0);
+ SkeletalDebugger.measureSphere.mergeLocal(vol);
+ size = SkeletalDebugger.BONE_RATIO * SkeletalDebugger.measureSphere.getRadius();
+ }
+ scene.getWorldTransform().multiply(jntTransform, SkeletalDebugger.spTransform);
+ SkeletalDebugger.spTransform.getMatrix().scale(new Vector3(size, size, size), SkeletalDebugger.spMatrix);
+ SkeletalDebugger.spTransform.setRotation(SkeletalDebugger.spMatrix);
+ SkeletalDebugger.joint.setWorldTransform(SkeletalDebugger.spTransform);
+ SkeletalDebugger.joint.draw(renderer);
+ }
+
+ /**
+ * Set the color of the joint object used in skeleton drawing.
+ *
+ * @param color
+ * the new color to use for skeleton joints.
+ */
+ public static void setJointColor(final ReadOnlyColorRGBA color) {
+ SkeletalDebugger.joint.setSolidColor(color);
+ }
+}
diff --git a/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/util/SkinUtils.java b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/util/SkinUtils.java
new file mode 100644
index 0000000..d94373c
--- /dev/null
+++ b/ardor3d-animation/src/main/java/com/ardor3d/extension/animation/skeletal/util/SkinUtils.java
@@ -0,0 +1,160 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.animation.skeletal.util;
+
+import com.ardor3d.extension.animation.skeletal.SkinnedMesh;
+import com.ardor3d.scenegraph.Spatial;
+import com.ardor3d.scenegraph.visitor.Visitor;
+
+/**
+ * General utility methods useful for Skin manipulation.
+ */
+public class SkinUtils {
+
+ /**
+ * Simple utility to turn on / off bounding volume updating on skinned mesh objects in a given scenegraph.
+ *
+ * @param root
+ * the root node on the scenegraph
+ * @param doUpdate
+ * if true, skinned mesh objects will automatically update their model bounds when applying pose.
+ */
+ public static void setAutoUpdateBounds(final Spatial root, final boolean doUpdate) {
+ root.acceptVisitor(new Visitor() {
+ public void visit(final Spatial spatial) {
+ // we only care about SkinnedMesh
+ if (spatial instanceof SkinnedMesh) {
+ ((SkinnedMesh) spatial).setAutoUpdateSkinBounds(doUpdate);
+ }
+ }
+ }, true);
+ }
+
+ /**
+ * Convert a short array to a float array
+ *
+ * @param shorts
+ * the short values
+ * @return our new float array
+ */
+ public static float[] convertToFloat(final short... shorts) {
+ final float[] rval = new float[shorts.length];
+ for (int i = 0; i < rval.length; i++) {
+ rval[i] = shorts[i];
+ }
+ return rval;
+ }
+
+ /**
+ * Rearrange the data from data per element, to a list of matSide x matSide matrices, output by row as such:
+ *
+ * row0element0, row0element1, row0element2...<br>
+ * row1element0, row1element1, row1element2...<br>
+ * row2element0, row2element1, row2element2...<br>
+ *
+ * If there is not enough values in the source data to fill out a row, 0 is used.
+ *
+ * @param src
+ * our source data, stored as element0, element1, etc.
+ * @param srcElementSize
+ * the number of values per element in our source data
+ * @param matSide
+ * the size of the matrix edge... eg. 4 would mean a 4x4 matrix.
+ * @return our new data array.
+ */
+ public static float[] reorderAndPad(final float[] src, final int srcElementSize, final int matSide) {
+ final int elements = src.length / srcElementSize;
+ final float[] rVal = new float[elements * matSide * matSide];
+
+ // size of each attribute (a row from each element)
+ final int length = matSide * elements;
+
+ for (int i = 0; i < elements; i++) {
+ // index into src for our element data
+ final int srcStart = i * srcElementSize;
+ // index into a destination row.
+ final int dstOffset = i * matSide;
+
+ // Go through each row of the current src element. Go through only as many rows of data as we have.
+ // (eg. if size is 6 and matSide is 4, we only need to go through j=0 and j=1)
+ for (int j = 0; j <= (srcElementSize - 1) / matSide; j++) {
+ // How much to copy. Generally matSide, except for last bit of data.
+ final int copySize = Math.min(srcElementSize - j * matSide, matSide);
+ // Copy the data from src to rVal
+ System.arraycopy(src, srcStart + j * matSide, rVal, j * length + dstOffset, copySize);
+ }
+ }
+
+ return rVal;
+ }
+
+ /**
+ * Expand out our src data so that each attribute is a certain size, padding with 0's as needed. If src is already
+ * correct size, we just return that without creating a new data array.
+ *
+ * @param src
+ * our source data, stored as element0, element1, etc.
+ * @param srcElementSize
+ * the number of values per element in our source data
+ * @param attribSize
+ * the desired size of each element in the return array.
+ * @return the padded array.
+ */
+ public static float[] pad(final float[] src, final int srcElementSize, final int attribSize) {
+ if (srcElementSize == attribSize) {
+ return src;
+ }
+ final int elements = src.length / srcElementSize;
+ final float[] rVal = new float[elements * attribSize];
+
+ for (int i = 0; i < elements; i++) {
+ // index into src for our element data
+ final int srcStart = i * srcElementSize;
+ // index into rVal to store
+ final int dstStart = i * attribSize;
+ // Copy the data from src to rVal
+ System.arraycopy(src, srcStart, rVal, dstStart, srcElementSize);
+ }
+
+ return rVal;
+ }
+
+ /**
+ * Expand out our src data so that each attribute is a certain size, padding with 0's as needed. If src is already
+ * correct size, we just return that without creating a new data array.
+ *
+ * @param src
+ * our source data, stored as element0, element1, etc.
+ * @param srcElementSize
+ * the number of values per element in our source data
+ * @param attribSize
+ * the desired size of each element in the return array.
+ * @return the padded array.
+ */
+ public static short[] pad(final short[] src, final int srcElementSize, final int attribSize) {
+ if (srcElementSize == attribSize) {
+ return src;
+ }
+ final int elements = src.length / srcElementSize;
+ final short[] rVal = new short[elements * attribSize];
+
+ for (int i = 0; i < elements; i++) {
+ // index into src for our element data
+ final int srcStart = i * srcElementSize;
+ // index into rVal to store
+ final int dstStart = i * attribSize;
+ // Copy the data from src to rVal
+ System.arraycopy(src, srcStart, rVal, dstStart, srcElementSize);
+ }
+
+ return rVal;
+ }
+}
diff --git a/ardor3d-animation/src/main/resources/com/ardor3d/extension/animation/skeletal/skinning_gpu.frag b/ardor3d-animation/src/main/resources/com/ardor3d/extension/animation/skeletal/skinning_gpu.frag
new file mode 100644
index 0000000..dba85b4
--- /dev/null
+++ b/ardor3d-animation/src/main/resources/com/ardor3d/extension/animation/skeletal/skinning_gpu.frag
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2008-2010 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+/**
+ * This fragment shader is purely for demonstration. It colors the faces based on their normal vectors.
+ */
+
+varying vec3 N;
+
+void main(void) {
+ gl_FragColor = vec4(abs(normalize(N)),1.0);
+}
diff --git a/ardor3d-animation/src/main/resources/com/ardor3d/extension/animation/skeletal/skinning_gpu.vert b/ardor3d-animation/src/main/resources/com/ardor3d/extension/animation/skeletal/skinning_gpu.vert
new file mode 100644
index 0000000..1f7c760
--- /dev/null
+++ b/ardor3d-animation/src/main/resources/com/ardor3d/extension/animation/skeletal/skinning_gpu.vert
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2008-2010 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+/**
+ * Uses a vec4 for joints and indices, allowing for up to 4 bone influences
+ * per vertex.
+ */
+
+attribute vec4 Weights;
+attribute vec4 JointIDs;
+
+uniform mat4 JointPalette[50];
+
+varying vec3 N;
+
+void main(void) {
+ mat4 mat = mat4(0.0);
+
+ mat += JointPalette[int(JointIDs[0])] * Weights[0];
+ mat += JointPalette[int(JointIDs[1])] * Weights[1];
+ mat += JointPalette[int(JointIDs[2])] * Weights[2];
+ mat += JointPalette[int(JointIDs[3])] * Weights[3];
+
+ gl_Position = gl_ModelViewProjectionMatrix * (mat * gl_Vertex);
+
+ N = gl_NormalMatrix * (mat3(mat[0].xyz,mat[1].xyz,mat[2].xyz) * gl_Normal);
+
+ gl_TexCoord[0] = gl_MultiTexCoord0;
+}
diff --git a/ardor3d-animation/src/main/resources/com/ardor3d/extension/animation/skeletal/skinning_gpu_mat4.vert b/ardor3d-animation/src/main/resources/com/ardor3d/extension/animation/skeletal/skinning_gpu_mat4.vert
new file mode 100644
index 0000000..8e02d6e
--- /dev/null
+++ b/ardor3d-animation/src/main/resources/com/ardor3d/extension/animation/skeletal/skinning_gpu_mat4.vert
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2008-2010 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+/**
+ * Uses a mat4 for joints and indices, allowing for up to 16 bone influences
+ * per vertex. (but using a total of 8 vertex attributes to do so)
+ */
+
+attribute mat4 Weights;
+attribute mat4 JointIDs;
+
+uniform mat4 JointPalette[50];
+
+varying vec3 N;
+
+void main(void) {
+ mat4 mat = mat4(0.0);
+
+ for (int i = 0; i < 4; i++) {
+ vec4 w = Weights[i];
+ vec4 d = JointIDs[i];
+ for (int j = 0; j < 4; j++) {
+ mat += JointPalette[int(d[j])] * w[j];
+ }
+ }
+
+ gl_Position = gl_ModelViewProjectionMatrix * (mat * gl_Vertex);
+
+ N = gl_NormalMatrix * (mat3(mat[0].xyz,mat[1].xyz,mat[2].xyz) * gl_Normal);
+
+ gl_TexCoord[0] = gl_MultiTexCoord0;
+}
diff --git a/ardor3d-animation/src/main/resources/com/ardor3d/extension/animation/skeletal/skinning_gpu_texture.frag b/ardor3d-animation/src/main/resources/com/ardor3d/extension/animation/skeletal/skinning_gpu_texture.frag
new file mode 100644
index 0000000..afb5dc9
--- /dev/null
+++ b/ardor3d-animation/src/main/resources/com/ardor3d/extension/animation/skeletal/skinning_gpu_texture.frag
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2008-2010 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+/**
+ * This fragment shader is purely for demonstration. It colors the faces based on their normal vectors.
+ */
+
+uniform sampler2D texture;
+
+varying vec3 transformedLightDirection;
+varying vec3 N;
+
+void main(void) {
+ // simplest lighting possible to get similar effect as non-gpu version
+ float lighting = max(dot(normalize(N),normalize(transformedLightDirection)), 0.0) * 0.65 + 0.1;
+
+ gl_FragColor = texture2D(texture, gl_TexCoord[0].st) * vec4(lighting);
+}
diff --git a/ardor3d-animation/src/main/resources/com/ardor3d/extension/animation/skeletal/skinning_gpu_texture.vert b/ardor3d-animation/src/main/resources/com/ardor3d/extension/animation/skeletal/skinning_gpu_texture.vert
new file mode 100644
index 0000000..fbd1748
--- /dev/null
+++ b/ardor3d-animation/src/main/resources/com/ardor3d/extension/animation/skeletal/skinning_gpu_texture.vert
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2008-2010 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+/**
+ * Uses a vec4 for joints and indices, allowing for up to 4 bone influences
+ * per vertex.
+ */
+
+attribute vec4 Weights;
+attribute vec4 JointIDs;
+
+uniform mat4 JointPalette[50];
+
+uniform vec3 lightDirection;
+
+varying vec3 transformedLightDirection;
+varying vec3 N;
+
+void main(void) {
+ mat4 mat = mat4(0.0);
+
+ mat += JointPalette[int(JointIDs[0])] * Weights[0];
+ mat += JointPalette[int(JointIDs[1])] * Weights[1];
+ mat += JointPalette[int(JointIDs[2])] * Weights[2];
+ mat += JointPalette[int(JointIDs[3])] * Weights[3];
+
+ transformedLightDirection = gl_NormalMatrix * lightDirection;
+
+ N = gl_NormalMatrix * (mat3(mat[0].xyz,mat[1].xyz,mat[2].xyz) * gl_Normal);
+
+ gl_TexCoord[0] = gl_MultiTexCoord0;
+
+ gl_Position = gl_ModelViewProjectionMatrix * (mat * gl_Vertex);
+}
diff --git a/ardor3d-animation/src/main/resources/com/ardor3d/extension/animation/skeletal/state/loader/functions.js b/ardor3d-animation/src/main/resources/com/ardor3d/extension/animation/skeletal/state/loader/functions.js
new file mode 100644
index 0000000..4e0b2cd
--- /dev/null
+++ b/ardor3d-animation/src/main/resources/com/ardor3d/extension/animation/skeletal/state/loader/functions.js
@@ -0,0 +1,355 @@
+importPackage(Packages.com.ardor3d.extension.animation.skeletal);
+importPackage(Packages.com.ardor3d.extension.animation.skeletal.blendtree);
+importPackage(Packages.com.ardor3d.extension.animation.skeletal.clip);
+importPackage(Packages.com.ardor3d.extension.animation.skeletal.layer);
+importPackage(Packages.com.ardor3d.extension.animation.skeletal.state);
+
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+/**
+ * Parse a SteadyState Java object from the given json data structure.
+ *
+ * @param json
+ * the json data structure
+ * @return the new SteadyState
+ */
+function _steadyState(json) {
+ var state = new SteadyState(json.name);
+
+ // check if we are simple and just have a clip
+ if (json.clip) {
+ // convert clip to source
+ var clip = INPUTSTORE.clips.get(json.clip);
+ state.sourceTree = new ClipSource(clip, MANAGER);
+ OUTPUTSTORE.clipSources.put(state.sourceTree);
+ }
+ // else we should have a tree
+ else if (json.tree) {
+ state.sourceTree = _treeSource(json.tree);
+ }
+
+ if (json.endTransition) {
+ // parse end transition
+ state.endTransition = _transitionState(json.endTransition);
+ }
+
+ // look for a set of transitions
+ if (json.transitions) {
+ for ( var key in json.transitions) {
+ // parse and add transition
+ state.addTransition(key, _transitionState(json.transitions[key]));
+ }
+ }
+
+ var layerName=AnimationLayer.BASE_LAYER_NAME;
+ if (json.layer) {
+ layerName = json.layer;
+ }
+
+ MANAGER.findAnimationLayer(layerName).addSteadyState(state);
+
+ return state;
+}
+
+/**
+ * Parse an AbstractTransitionState Java object from the given json data array.
+ *
+ * @param args
+ * the json data array
+ * @return the new AbstractTransitionState
+ */
+function _transitionState(args) {
+ var type = args[2];
+ var transition;
+
+ // based on our "type", create our transition state...
+ switch (type) {
+ case 'fade':
+ transition = new FadeTransitionState(args[3], args[4],
+ AbstractTwoStateLerpTransition.BlendType.valueOf(args[5]));
+ break;
+ case 'syncfade':
+ transition = new SyncFadeTransitionState(args[3], args[4],
+ AbstractTwoStateLerpTransition.BlendType.valueOf(args[5]));
+ break;
+ case 'frozen':
+ transition = new FrozenTransitionState(args[3], args[4],
+ AbstractTwoStateLerpTransition.BlendType.valueOf(args[5]));
+ break;
+ case 'immediate':
+ transition = new ImmediateTransitionState(args[3]);
+ break;
+ case 'ignore':
+ transition = new IgnoreTransitionState();
+ break;
+ default:
+ return null;
+ }
+
+ // pull a start window, if set
+ if (args[0] != '-') {
+ transition.startWindow = args[0];
+ }
+
+ // pull an end window, if set
+ if (args[1] != '-') {
+ transition.endWindow = args[1];
+ }
+
+ return transition;
+}
+
+/**
+ * Parse a BlendTreeSource Java object from the given json data structure.
+ *
+ * @param json
+ * the json data structure
+ * @return the new BlendTreeSource
+ */
+function _treeSource(json) {
+
+ // look for the source type
+ var source;
+ var root;
+ // ClipSource
+ if (json.clip) {
+ root = json.clip;
+ var clip = INPUTSTORE.clips.get(root.name);
+ // create source
+ source = new ClipSource(clip, MANAGER);
+ OUTPUTSTORE.clipSources.put(source);
+ _populateClipSource(source, clip, root);
+ return source;
+ }
+ // InclusiveClipSource
+ else if (json.inclusiveClip) {
+ root = json.inclusiveClip;
+ var clip = INPUTSTORE.clips.get(root.name);
+ // create source
+ source = new InclusiveClipSource(clip, MANAGER);
+ OUTPUTSTORE.clipSources.put(source);
+ _populateClipSource(source, clip, root);
+ // add channels/joints
+ if (root.channels)
+ source.addEnabledChannels(root.channels);
+ if (root.joints)
+ source.addEnabledJoints(root.joints);
+ return source;
+ }
+ // ExclusiveClipSource
+ else if (json.exclusiveClip) {
+ root = json.exclusiveClip;
+ var clip = INPUTSTORE.clips.get(root.name);
+ // create source
+ source = new ExclusiveClipSource(clip, MANAGER);
+ OUTPUTSTORE.clipSources.put(source);
+ _populateClipSource(source, clip, root);
+ // add channels/joints
+ if (root.channels)
+ source.addDisabledChannels(root.channels);
+ if (root.joints)
+ source.addDisabledJoints(root.joints);
+ return source;
+ }
+ // BinaryLERPSource
+ else if (json.lerp) {
+ root = json.lerp;
+ // get child source A
+ var sourceA = _treeSource(root.childA);
+ // get child source B
+ var sourceB = _treeSource(root.childB);
+ // create source
+ source = new BinaryLERPSource(sourceA, sourceB);
+ // pull weight info
+ if (root.blendKey) {
+ source.setBlendKey(root.blendKey);
+ var weight = (root.blendWeight) ? root.blendWeight : 0;
+ MANAGER.valuesStore.put(source.blendKey, weight);
+ }
+ return source;
+ }
+ // ManagedTransformSource
+ else if (json.managed) {
+ root = json.managed;
+ // create source
+ source = new ManagedTransformSource();
+ // if we are asked to, init joint positions from initial position of a
+ // clip
+ if (root.initFromClip) {
+ // store name for future use.
+ source.setSourceName(root.initFromClip.clip);
+
+ // get clip
+ var clip = INPUTSTORE.clips.get(root.initFromClip.clip);
+ if (root.initFromClip.jointNames) {
+ source.initJointsByName(MANAGER.getSkeletonPose(0), clip,
+ root.initFromClip.jointNames);
+ }
+ if (root.initFromClip.jointIds) {
+ source.initJointsById(clip, root.initFromClip.jointIds);
+ }
+ }
+ return source;
+ }
+ // FrozenTreeSource
+ else if (json.frozen) {
+ root = json.frozen;
+ // get child source
+ var childSource = _treeSource(root.child);
+ // read time
+ var time = (root.time) ? root.time : 0;
+ // create source
+ source = new FrozenTreeSource(childSource, time);
+ return source;
+ }
+
+ return null;
+}
+
+/**
+ * Populate derivatives of ClipSource with instance values such as time scale.
+ *
+ * @param source
+ * our ClipSource-based class.
+ * @param clip
+ * our AnimationClip
+ * @param root
+ * our json root data object.
+ * @return void
+ */
+function _populateClipSource(source, clip, root) {
+ // clip instance params...
+ // add time scaling, if present
+ if (root.timeScale) {
+ MANAGER.getClipInstance(clip).setTimeScale(root.timeScale);
+ }
+ // add loop count
+ if (root.loopCount) {
+ MANAGER.getClipInstance(clip).setLoopCount(root.loopCount);
+ }
+ // add active flag
+ if (root.active) {
+ MANAGER.getClipInstance(clip).setActive(root.active);
+ }
+
+ return;
+}
+
+/**
+ * Parse an AnimationLayer Java object from the given json data structure.
+ *
+ * @param json
+ * the json data structure
+ * @return the new AnimationLayer
+ */
+function _animationLayer(json) {
+ var layer = new AnimationLayer(json.name);
+
+ if (json.blendType) {
+ var blender;
+ // grab based on blend type...
+ switch (json.blendType) {
+ case 'lerp':
+ blender = new LayerLERPBlender();
+ layer.setLayerBlender(blender);
+
+ // pull weight
+ if (json.blendKey) {
+ blender.setBlendKey(json.blendKey);
+ var weight = (json.blendWeight) ? json.blendWeight : 0;
+ MANAGER.valuesStore.put(blender.blendKey, weight);
+ }
+ break;
+ }
+ }
+
+ // look for a set of transitions
+ if (json.transitions) {
+ for ( var key in json.transitions) {
+ // parse and add transition
+ layer.addTransition(key, _transitionState(json.transitions[key]));
+ }
+ }
+
+ MANAGER.addAnimationLayer(layer);
+
+ return layer;
+}
+
+/**
+ * Create an attachment stub.
+ *
+ * @param attachName
+ * a name to call this attachment point, used for finding it later in
+ * the code.
+ * @param jointName
+ * the name of the joint to attach to.
+ * @param poseIndex
+ * the index of the pose in our Manager to use. Usually 0.
+ * @param transform
+ * a joint offset as a com.ardor3d.math.Transform object. If null,
+ * identity is used.
+ */
+function _addAttachment(attachName, jointName, poseIndex, transform) {
+ var attach = new AttachmentPoint(attachName);
+ var pose = MANAGER.getSkeletonPose(poseIndex);
+ var jointIndex = pose.getSkeleton().findJointByName(jointName);
+ attach.setJointIndex(jointIndex);
+ attach.setOffset(transform != null ? transform
+ : com.ardor3d.math.Transform.IDENTITY);
+ pose.addPoseListener(attach);
+ OUTPUTSTORE.addAttachmentPoint(attach);
+
+ return;
+}
+
+ /**
+ * Create a trigger channel and add it to a clip. The channel data and
+ * clip name are specified in the json data structure.
+ *
+ * @param json
+ * the json data structure
+ */
+ function _addTriggerChannel(json) {
+ var triggerChannel = new TriggerChannel(json.triggerChannel.name,
+ json.triggerChannel.times, json.triggerChannel.keys);
+
+ var clip = INPUTSTORE.clips.get(json.clip);
+ clip.addChannel(triggerChannel);
+ }
+
+ /**
+ * Create a trigger channel and add it to a clip. The channel data and
+ * clip name are specified in the json data structure.
+ *
+ * @param json
+ * the json data structure
+ */
+ function _addGuaranteedTriggerChannel(json) {
+ var triggerChannel = new GuaranteedTriggerChannel(json.triggerChannel.name,
+ json.triggerChannel.times, json.triggerChannel.keys);
+
+ var clip = INPUTSTORE.clips.get(json.clip);
+ clip.addChannel(triggerChannel);
+ }
+
+/**
+ * Add a trigger callback to the current manager's Animation applier.
+ *
+ * @param key
+ * a String indicating the key to look for in order to trigger
+ * the given callback.
+ * @param callback
+ * the callback object - must be an instance of TriggerCallback
+ */
+function _addTriggerCallback(key, callback) {
+ MANAGER.getApplier().addTriggerCallback(key, callback);
+} \ No newline at end of file