diff options
author | neothemachine <[email protected]> | 2012-12-05 17:03:16 +0100 |
---|---|---|
committer | neothemachine <[email protected]> | 2012-12-05 17:03:16 +0100 |
commit | 9dd02f103042cb8a196f8a3ed2278da443e345bf (patch) | |
tree | 422449f0c62ff9518316ce5d4219bb2b12f0ed15 /ardor3d-animation/src | |
parent | 2b26b12fd794de0f03a064a10024a3d9f5583756 (diff) |
move all files from trunk to root folder
Diffstat (limited to 'ardor3d-animation/src')
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 |