/*
- * Copyright (c) 2009-2010 jMonkeyEngine
+ * Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-
package com.jme3.animation;
import com.jme3.math.FastMath;
private float speed;
private float timeBlendFrom;
private float speedBlendFrom;
+ private boolean notified=false;
private LoopMode loopMode, loopModeBlendFrom;
private float blendAmount = 1f;
private float blendRate = 0;
-
+
private static float clampWrapTime(float t, float max, LoopMode loopMode){
- if (max == Float.POSITIVE_INFINITY)
- return t;
+ if (t == 0) {
+ return 0; // prevent division by 0 errors
+ }
- if (t < 0f){
- //float tMod = -(-t % max);
- switch (loopMode){
- case DontLoop:
- return 0;
- case Cycle:
- return t;
- case Loop:
- return max - t;
- }
- }else if (t > max){
- switch (loopMode){
- case DontLoop:
- return max;
- case Cycle:
- return /*-max;*/-(2f * max - t);
- case Loop:
- return t - max;
- }
+ switch (loopMode) {
+ case Cycle:
+ boolean sign = ((int) (t / max) % 2) != 0;
+ float result;
+
+// if (t < 0){
+// result = sign ? t % max : -(max + (t % max));
+// } else {
+ // NOTE: This algorithm seems stable for both high and low
+ // tpf so for now its a keeper.
+ result = sign ? -(max - (t % max)) : t % max;
+// }
+
+// if (result <= 0 || result >= max) {
+// System.out.println("SIGN: " + sign + ", RESULT: " + result + ", T: " + t + ", M: " + max);
+// }
+
+ return result;
+ case DontLoop:
+ return t > max ? max : (t < 0 ? 0 : t);
+ case Loop:
+ return t % max;
}
-
return t;
+
+
+// if (max == Float.POSITIVE_INFINITY)
+// return t;
+//
+// if (t < 0f){
+// //float tMod = -(-t % max);
+// switch (loopMode){
+// case DontLoop:
+// return 0;
+// case Cycle:
+// return t;
+// case Loop:
+// return max - t;
+// }
+// }else if (t > max){
+// switch (loopMode){
+// case DontLoop:
+// return max;
+// case Cycle:
+// return -(2f * max - t) % max;
+// case Loop:
+// return t % max;
+// }
+// }
+//
+// return t;
}
AnimChannel(AnimControl control){
*/
public void setAnim(String name, float blendTime){
if (name == null)
- throw new NullPointerException();
+ throw new IllegalArgumentException("name cannot be null");
if (blendTime < 0f)
throw new IllegalArgumentException("blendTime cannot be less than zero");
loopModeBlendFrom = loopMode;
blendAmount = 0f;
blendRate = 1f / blendTime;
+ }else{
+ blendFrom = null;
}
animation = anim;
time = 0;
speed = 1f;
loopMode = LoopMode.Loop;
+ notified = false;
}
/**
BitSet getAffectedBones(){
return affectedBones;
}
+
+ public void reset(boolean rewind){
+ if(rewind){
+ setTime(0);
+ if(control.getSkeleton()!=null){
+ control.getSkeleton().resetAndUpdate();
+ }else{
+ TempVars vars = TempVars.get();
+ update(0, vars);
+ vars.release();
+ }
+ }
+ animation = null;
+ // System.out.println("Setting notified false");
+ notified = false;
+ }
void update(float tpf, TempVars vars) {
if (animation == null)
return;
- if (blendFrom != null){
+ if (blendFrom != null && blendAmount != 1.0f){
+ // The blendFrom anim is set, the actual animation
+ // playing will be set
+// blendFrom.setTime(timeBlendFrom, 1f, control, this, vars);
blendFrom.setTime(timeBlendFrom, 1f - blendAmount, control, this, vars);
- //blendFrom.setTime(timeBlendFrom, control.skeleton, 1f - blendAmount, affectedBones);
+
timeBlendFrom += tpf * speedBlendFrom;
timeBlendFrom = clampWrapTime(timeBlendFrom,
blendFrom.getLength(),
blendFrom = null;
}
}
-
+
animation.setTime(time, blendAmount, control, this, vars);
- //animation.setTime(time, control.skeleton, blendAmount, affectedBones);
time += tpf * speed;
if (animation.getLength() > 0){
- if (time >= animation.getLength()) {
- control.notifyAnimCycleDone(this, animation.getName());
- } else if (time < 0) {
+ if (!notified && (time >= animation.getLength() || time < 0)) {
+ if (loopMode == LoopMode.DontLoop) {
+ // Note that this flag has to be set before calling the notify
+ // since the notify may start a new animation and then unset
+ // the flag.
+ notified = true;
+ }
control.notifyAnimCycleDone(this, animation.getName());
}
}
time = clampWrapTime(time, animation.getLength(), loopMode);
if (time < 0){
+ // Negative time indicates that speed should be inverted
+ // (for cycle loop mode only)
time = -time;
speed = -speed;
}
-
-
}
}
/*\r
- * Copyright (c) 2009-2010 jMonkeyEngine\r
+ * Copyright (c) 2009-2012 jMonkeyEngine\r
* All rights reserved.\r
*\r
* Redistribution and use in source and binary forms, with or without\r
*/\r
package com.jme3.animation;\r
\r
-import com.jme3.export.JmeExporter;\r
-import com.jme3.export.JmeImporter;\r
-import com.jme3.export.InputCapsule;\r
-import com.jme3.export.OutputCapsule;\r
-import com.jme3.export.Savable;\r
+import com.jme3.export.*;\r
import com.jme3.renderer.RenderManager;\r
import com.jme3.renderer.ViewPort;\r
import com.jme3.scene.Mesh;\r
import java.util.ArrayList;\r
import java.util.Collection;\r
import java.util.HashMap;\r
+import java.util.Map.Entry;\r
\r
/**\r
* <code>AnimControl</code> is a Spatial control that allows manipulation\r
* Skeleton object must contain corresponding data for the targets' weight buffers.\r
*/\r
Skeleton skeleton;\r
-\r
/** only used for backward compatibility */\r
@Deprecated\r
private SkeletonControl skeletonControl;\r
-\r
/**\r
* List of animations\r
*/\r
- HashMap<String, Animation> animationMap;\r
-\r
+ HashMap<String, Animation> animationMap = new HashMap<String, Animation>();\r
/**\r
* Animation channels\r
*/\r
private transient ArrayList<AnimChannel> channels = new ArrayList<AnimChannel>();\r
-\r
/**\r
* Animation event listeners\r
*/\r
AnimControl clone = (AnimControl) super.clone();\r
clone.spatial = spatial;\r
clone.channels = new ArrayList<AnimChannel>();\r
- \r
- if (skeleton != null){\r
+ clone.listeners = new ArrayList<AnimEventListener>();\r
+\r
+ if (skeleton != null) {\r
clone.skeleton = new Skeleton(skeleton);\r
}\r
- \r
- // animationMap is reference-copied, animation data should be shared\r
- // to reduce memory usage.\r
+\r
+ // animationMap is cloned, but only ClonableTracks will be cloned as they need a reference to a cloned spatial\r
+ for (Entry<String, Animation> animEntry : animationMap.entrySet()) {\r
+ clone.animationMap.put(animEntry.getKey(), animEntry.getValue().cloneForSpatial(spatial));\r
+ }\r
\r
return clone;\r
} catch (CloneNotSupportedException ex) {\r
* @see AnimControl#createChannel()\r
*/\r
public void clearChannels() {\r
+ for (AnimChannel animChannel : channels) {\r
+ for (AnimEventListener list : listeners) {\r
+ list.onAnimCycleDone(this, animChannel, animChannel.getAnimationName());\r
+ }\r
+ }\r
channels.clear();\r
}\r
\r
*/\r
@Override\r
public void setSpatial(Spatial spatial) {\r
- if (spatial == null && skeletonControl != null){\r
+ if (spatial == null && skeletonControl != null) {\r
this.spatial.removeControl(skeletonControl);\r
}\r
\r
\r
return a.getLength();\r
}\r
- \r
+\r
/**\r
* Internal use only.\r
*/\r
@Override\r
protected void controlUpdate(float tpf) {\r
- if (skeleton != null){\r
+ if (skeleton != null) {\r
skeleton.reset(); // reset skeleton to bind pose\r
}\r
\r
}\r
vars.release();\r
\r
- if (skeleton != null){\r
+ if (skeleton != null) {\r
skeleton.updateWorldVectors();\r
}\r
}\r
super.read(im);\r
InputCapsule in = im.getCapsule(this);\r
skeleton = (Skeleton) in.readSavable("skeleton", null);\r
- animationMap = (HashMap<String, Animation>) in.readStringSavableMap("animations", null);\r
+ HashMap<String, Animation> loadedAnimationMap = (HashMap<String, Animation>) in.readStringSavableMap("animations", null);\r
+ if (loadedAnimationMap != null) {\r
+ animationMap = loadedAnimationMap;\r
+ }\r
\r
- if (im.getFormatVersion() == 0){\r
+ if (im.getFormatVersion() == 0) {\r
// Changed for backward compatibility with j3o files generated \r
// before the AnimControl/SkeletonControl split.\r
- \r
+\r
// If we find a target mesh array the AnimControl creates the \r
// SkeletonControl for old files and add it to the spatial. \r
// When backward compatibility won't be needed anymore this can deleted \r
/*
- * Copyright (c) 2009-2010 jMonkeyEngine
+ * Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-
package com.jme3.animation;
/**
/*
- * Copyright (c) 2009-2011 jMonkeyEngine
+ * Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
*/
package com.jme3.animation;
-import java.io.IOException;
-
-import com.jme3.export.InputCapsule;
-import com.jme3.export.JmeExporter;
-import com.jme3.export.JmeImporter;
-import com.jme3.export.OutputCapsule;
-import com.jme3.export.Savable;
+import com.jme3.export.*;
+import com.jme3.scene.Spatial;
+import com.jme3.util.SafeArrayList;
import com.jme3.util.TempVars;
+import java.io.IOException;
/**
* The animation class updates the animation target with the tracks of a given type.
* @author Kirill Vainer, Marcin Roguski (Kaelthas)
*/
public class Animation implements Savable, Cloneable {
-
+
/**
* The name of the animation.
*/
private String name;
-
/**
* The length of the animation.
*/
private float length;
-
/**
* The tracks of the animation.
*/
- private Track[] tracks;
-
+ private SafeArrayList<Track> tracks = new SafeArrayList<Track>(Track.class);
+
/**
* Serialization-only. Do not use.
*/
- public Animation() {}
-
+ public Animation() {
+ }
+
/**
* Creates a new <code>Animation</code> with the given name and length.
*
this.name = name;
this.length = length;
}
-
+
/**
* The name of the bone animation
* @return name of the bone animation
*/
public String getName() {
- return name;
+ return name;
}
-
+
/**
* Returns the length in seconds of this animation
*
* @return the length in seconds of this animation
*/
public float getLength() {
- return length;
+ return length;
}
-
+
/**
* This method sets the current time of the animation.
* This method behaves differently for every known track type.
* @param channel the animation channel
*/
void setTime(float time, float blendAmount, AnimControl control, AnimChannel channel, TempVars vars) {
- if (tracks == null)
+ if (tracks == null) {
return;
-
- for (int i = 0; i < tracks.length; i++){
- tracks[i].setTime(time, blendAmount, control, channel, vars);
}
-
- /*
- if (tracks != null && tracks.length > 0) {
- Track<?> trackInstance = tracks[0];
-
- if (trackInstance instanceof SpatialTrack) {
- Spatial spatial = control.getSpatial();
- if (spatial != null) {
- ((SpatialTrack) tracks[0]).setTime(time, spatial, blendAmount);
- }
- } else if (trackInstance instanceof BoneTrack) {
- BitSet affectedBones = channel.getAffectedBones();
- Skeleton skeleton = control.getSkeleton();
- for (int i = 0; i < tracks.length; ++i) {
- if (affectedBones == null || affectedBones.get(((BoneTrack) tracks[i]).getTargetIndex())) {
- ((BoneTrack) tracks[i]).setTime(time, skeleton, blendAmount);
- }
- }
- } else if (trackInstance instanceof PoseTrack) {
- Spatial spatial = control.getSpatial();
- List<Mesh> meshes = new ArrayList<Mesh>();
- this.getMeshes(spatial, meshes);
- if (meshes.size() > 0) {
- Mesh[] targets = meshes.toArray(new Mesh[meshes.size()]);
- for (int i = 0; i < tracks.length; ++i) {
- ((PoseTrack) tracks[i]).setTime(time, targets, blendAmount);
- }
- }
- }
+
+ for (Track track : tracks) {
+ track.setTime(time, blendAmount, control, channel, vars);
}
- */
}
-
+
/**
* Set the {@link Track}s to be used by this animation.
- * <p>
- * The array should be organized so that the appropriate Track can
- * be retrieved based on a bone index.
*
- * @param tracks The tracks to set.
+ * @param tracksArray The tracks to set.
+ */
+ public void setTracks(Track[] tracksArray) {
+ for (Track track : tracksArray) {
+ tracks.add(track);
+ }
+ }
+
+ /**
+ * Adds a track to this animation
+ * @param track the track to add
*/
- public void setTracks(Track[] tracks){
- this.tracks = tracks;
+ public void addTrack(Track track) {
+ tracks.add(track);
}
-
+
+ /**
+ * removes a track from this animation
+ * @param track the track to remove
+ */
+ public void removeTrack(Track track) {
+ tracks.remove(track);
+ if (track instanceof ClonableTrack) {
+ ((ClonableTrack) track).cleanUp();
+ }
+ }
+
/**
* Returns the tracks set in {@link #setTracks(com.jme3.animation.Track[]) }.
*
* @return the tracks set previously
*/
public Track[] getTracks() {
- return tracks;
+ return tracks.getArray();
}
-
+
/**
* This method creates a clone of the current object.
* @return a clone of the current object
*/
- @Override
- public Animation clone() {
+ @Override
+ public Animation clone() {
try {
Animation result = (Animation) super.clone();
- result.tracks = tracks.clone();
- for (int i = 0; i < tracks.length; ++i) {
- result.tracks[i] = this.tracks[i].clone();
+ result.tracks = new SafeArrayList<Track>(Track.class);
+ for (Track track : tracks) {
+ result.tracks.add(track.clone());
+ }
+ return result;
+ } catch (CloneNotSupportedException e) {
+ throw new AssertionError();
+ }
+ }
+
+ /**
+ *
+ * @param spat
+ * @return
+ */
+ public Animation cloneForSpatial(Spatial spat) {
+ try {
+ Animation result = (Animation) super.clone();
+ result.tracks = new SafeArrayList<Track>(Track.class);
+ for (Track track : tracks) {
+ if (track instanceof ClonableTrack) {
+ result.tracks.add(((ClonableTrack) track).cloneForSpatial(spat));
+ } else {
+ result.tracks.add(track);
+ }
}
return result;
} catch (CloneNotSupportedException e) {
public String toString() {
return getClass().getSimpleName() + "[name=" + name + ", length=" + length + ']';
}
-
- @Override
+
+ @Override
public void write(JmeExporter ex) throws IOException {
OutputCapsule out = ex.getCapsule(this);
out.write(name, "name", null);
out.write(length, "length", 0f);
- out.write(tracks, "tracks", null);
+ out.write(tracks.getArray(), "tracks", null);
}
@Override
InputCapsule in = im.getCapsule(this);
name = in.readString("name", null);
length = in.readFloat("length", 0f);
-
+
Savable[] arr = in.readSavableArray("tracks", null);
- tracks = new Track[arr.length];
- System.arraycopy(arr, 0, tracks, 0, arr.length);
+ if (arr != null) {
+ // NOTE: Backward compat only .. Some animations have no
+ // tracks set at all even though it makes no sense.
+ // Since there's a null check in setTime(),
+ // its only appropriate that the check is made here as well.
+ tracks = new SafeArrayList<Track>(Track.class);
+ for (Savable savable : arr) {
+ tracks.add((Track) savable);
+ }
+ }
}
}
--- /dev/null
+/*
+ * Copyright (c) 2009-2012 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.animation;
+
+import com.jme3.math.FastMath;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Transform;
+import com.jme3.math.Vector3f;
+
+/**
+ * A convenience class to easily setup a spatial keyframed animation
+ * you can add some keyFrames for a given time or a given keyFrameIndex, for translation rotation and scale.
+ * The animationHelper will then generate an appropriate SpatialAnimation by interpolating values between the keyFrames.
+ * <br><br>
+ * Usage is : <br>
+ * - Create the AnimationHelper<br>
+ * - add some keyFrames<br>
+ * - call the buildAnimation() method that will retruna new Animation<br>
+ * - add the generated Animation to any existing AnimationControl<br>
+ * <br><br>
+ * Note that the first keyFrame (index 0) is defaulted with the identy transforms.
+ * If you want to change that you have to replace this keyFrame with any transform you want.
+ *
+ * @author Nehon
+ */
+public class AnimationFactory {
+
+ /**
+ * step for splitting rotation that have a n ange above PI/2
+ */
+ private final static float EULER_STEP = FastMath.QUARTER_PI * 3;
+
+ /**
+ * enum to determine the type of interpolation
+ */
+ private enum Type {
+
+ Translation, Rotation, Scale;
+ }
+
+ /**
+ * Inner Rotation type class to kep track on a rotation Euler angle
+ */
+ protected class Rotation {
+
+ /**
+ * The rotation Quaternion
+ */
+ Quaternion rotation = new Quaternion();
+ /**
+ * This rotation expressed in Euler angles
+ */
+ Vector3f eulerAngles = new Vector3f();
+ /**
+ * the index of the parent key frame is this keyFrame is a splitted rotation
+ */
+ int masterKeyFrame = -1;
+
+ public Rotation() {
+ rotation.loadIdentity();
+ }
+
+ void set(Quaternion rot) {
+ rotation.set(rot);
+ float[] a = new float[3];
+ rotation.toAngles(a);
+ eulerAngles.set(a[0], a[1], a[2]);
+ }
+
+ void set(float x, float y, float z) {
+ float[] a = {x, y, z};
+ rotation.fromAngles(a);
+ eulerAngles.set(x, y, z);
+ }
+ }
+ /**
+ * Name of the animation
+ */
+ protected String name;
+ /**
+ * frames per seconds
+ */
+ protected int fps;
+ /**
+ * Animation duration in seconds
+ */
+ protected float duration;
+ /**
+ * total number of frames
+ */
+ protected int totalFrames;
+ /**
+ * time per frame
+ */
+ protected float tpf;
+ /**
+ * Time array for this animation
+ */
+ protected float[] times;
+ /**
+ * Translation array for this animation
+ */
+ protected Vector3f[] translations;
+ /**
+ * rotation array for this animation
+ */
+ protected Quaternion[] rotations;
+ /**
+ * scales array for this animation
+ */
+ protected Vector3f[] scales;
+ /**
+ * The map of keyFrames to compute the animation. The key is the index of the frame
+ */
+ protected Vector3f[] keyFramesTranslation;
+ protected Vector3f[] keyFramesScale;
+ protected Rotation[] keyFramesRotation;
+
+ /**
+ * Creates and AnimationHelper
+ * @param duration the desired duration for the resulting animation
+ * @param name the name of the resulting animation
+ */
+ public AnimationFactory(float duration, String name) {
+ this(duration, name, 30);
+ }
+
+ /**
+ * Creates and AnimationHelper
+ * @param duration the desired duration for the resulting animation
+ * @param name the name of the resulting animation
+ * @param fps the number of frames per second for this animation (default is 30)
+ */
+ public AnimationFactory(float duration, String name, int fps) {
+ this.name = name;
+ this.duration = duration;
+ this.fps = fps;
+ totalFrames = (int) (fps * duration) + 1;
+ tpf = 1 / (float) fps;
+ times = new float[totalFrames];
+ translations = new Vector3f[totalFrames];
+ rotations = new Quaternion[totalFrames];
+ scales = new Vector3f[totalFrames];
+ keyFramesTranslation = new Vector3f[totalFrames];
+ keyFramesTranslation[0] = new Vector3f();
+ keyFramesScale = new Vector3f[totalFrames];
+ keyFramesScale[0] = new Vector3f(1, 1, 1);
+ keyFramesRotation = new Rotation[totalFrames];
+ keyFramesRotation[0] = new Rotation();
+
+ }
+
+ /**
+ * Adds a key frame for the given Transform at the given time
+ * @param time the time at which the keyFrame must be inserted
+ * @param transform the transforms to use for this keyFrame
+ */
+ public void addTimeTransform(float time, Transform transform) {
+ addKeyFrameTransform((int) (time / tpf), transform);
+ }
+
+ /**
+ * Adds a key frame for the given Transform at the given keyFrame index
+ * @param keyFrameIndex the index at which the keyFrame must be inserted
+ * @param transform the transforms to use for this keyFrame
+ */
+ public void addKeyFrameTransform(int keyFrameIndex, Transform transform) {
+ addKeyFrameTranslation(keyFrameIndex, transform.getTranslation());
+ addKeyFrameScale(keyFrameIndex, transform.getScale());
+ addKeyFrameRotation(keyFrameIndex, transform.getRotation());
+ }
+
+ /**
+ * Adds a key frame for the given translation at the given time
+ * @param time the time at which the keyFrame must be inserted
+ * @param translation the translation to use for this keyFrame
+ */
+ public void addTimeTranslation(float time, Vector3f translation) {
+ addKeyFrameTranslation((int) (time / tpf), translation);
+ }
+
+ /**
+ * Adds a key frame for the given translation at the given keyFrame index
+ * @param keyFrameIndex the index at which the keyFrame must be inserted
+ * @param translation the translation to use for this keyFrame
+ */
+ public void addKeyFrameTranslation(int keyFrameIndex, Vector3f translation) {
+ Vector3f t = getTranslationForFrame(keyFrameIndex);
+ t.set(translation);
+ }
+
+ /**
+ * Adds a key frame for the given rotation at the given time<br>
+ * This can't be used if the interpolated angle is higher than PI (180°)<br>
+ * Use {@link #addTimeRotationAngles(float time, float x, float y, float z)} instead that uses Euler angles rotations.<br> *
+ * @param time the time at which the keyFrame must be inserted
+ * @param rotation the rotation Quaternion to use for this keyFrame
+ * @see #addTimeRotationAngles(float time, float x, float y, float z)
+ */
+ public void addTimeRotation(float time, Quaternion rotation) {
+ addKeyFrameRotation((int) (time / tpf), rotation);
+ }
+
+ /**
+ * Adds a key frame for the given rotation at the given keyFrame index<br>
+ * This can't be used if the interpolated angle is higher than PI (180°)<br>
+ * Use {@link #addKeyFrameRotationAngles(int keyFrameIndex, float x, float y, float z)} instead that uses Euler angles rotations.
+ * @param keyFrameIndex the index at which the keyFrame must be inserted
+ * @param rotation the rotation Quaternion to use for this keyFrame
+ * @see #addKeyFrameRotationAngles(int keyFrameIndex, float x, float y, float z)
+ */
+ public void addKeyFrameRotation(int keyFrameIndex, Quaternion rotation) {
+ Rotation r = getRotationForFrame(keyFrameIndex);
+ r.set(rotation);
+ }
+
+ /**
+ * Adds a key frame for the given rotation at the given time.<br>
+ * Rotation is expressed by Euler angles values in radians.<br>
+ * Note that the generated rotation will be stored as a quaternion and interpolated using a spherical linear interpolation (slerp)<br>
+ * Hence, this method may create intermediate keyFrames if the interpolation angle is higher than PI to ensure continuity in animation<br>
+ *
+ * @param time the time at which the keyFrame must be inserted
+ * @param x the rotation around the x axis (aka yaw) in radians
+ * @param y the rotation around the y axis (aka roll) in radians
+ * @param z the rotation around the z axis (aka pitch) in radians
+ */
+ public void addTimeRotationAngles(float time, float x, float y, float z) {
+ addKeyFrameRotationAngles((int) (time / tpf), x, y, z);
+ }
+
+ /**
+ * Adds a key frame for the given rotation at the given key frame index.<br>
+ * Rotation is expressed by Euler angles values in radians.<br>
+ * Note that the generated rotation will be stored as a quaternion and interpolated using a spherical linear interpolation (slerp)<br>
+ * Hence, this method may create intermediate keyFrames if the interpolation angle is higher than PI to ensure continuity in animation<br>
+ *
+ * @param keyFrameIndex the index at which the keyFrame must be inserted
+ * @param x the rotation around the x axis (aka yaw) in radians
+ * @param y the rotation around the y axis (aka roll) in radians
+ * @param z the rotation around the z axis (aka pitch) in radians
+ */
+ public void addKeyFrameRotationAngles(int keyFrameIndex, float x, float y, float z) {
+ Rotation r = getRotationForFrame(keyFrameIndex);
+ r.set(x, y, z);
+
+ // if the delta of euler angles is higher than PI, we create intermediate keyframes
+ // since we are using quaternions and slerp for rotation interpolation, we cannot interpolate over an angle higher than PI
+ int prev = getPreviousKeyFrame(keyFrameIndex, keyFramesRotation);
+ if (prev != -1) {
+ //previous rotation keyframe
+ Rotation prevRot = keyFramesRotation[prev];
+ //the maximum delta angle (x,y or z)
+ float delta = Math.max(Math.abs(x - prevRot.eulerAngles.x), Math.abs(y - prevRot.eulerAngles.y));
+ delta = Math.max(delta, Math.abs(z - prevRot.eulerAngles.z));
+ //if delta > PI we have to create intermediates key frames
+ if (delta >= FastMath.PI) {
+ //frames delta
+ int dF = keyFrameIndex - prev;
+ //angle per frame for x,y ,z
+ float dXAngle = (x - prevRot.eulerAngles.x) / (float) dF;
+ float dYAngle = (y - prevRot.eulerAngles.y) / (float) dF;
+ float dZAngle = (z - prevRot.eulerAngles.z) / (float) dF;
+
+ // the keyFrame step
+ int keyStep = (int) (((float) (dF)) / delta * (float) EULER_STEP);
+ // the current keyFrame
+ int cursor = prev + keyStep;
+ while (cursor < keyFrameIndex) {
+ //for each step we create a new rotation by interpolating the angles
+ Rotation dr = getRotationForFrame(cursor);
+ dr.masterKeyFrame = keyFrameIndex;
+ dr.set(prevRot.eulerAngles.x + cursor * dXAngle, prevRot.eulerAngles.y + cursor * dYAngle, prevRot.eulerAngles.z + cursor * dZAngle);
+ cursor += keyStep;
+ }
+
+ }
+ }
+
+ }
+
+ /**
+ * Adds a key frame for the given scale at the given time
+ * @param time the time at which the keyFrame must be inserted
+ * @param scale the scale to use for this keyFrame
+ */
+ public void addTimeScale(float time, Vector3f scale) {
+ addKeyFrameScale((int) (time / tpf), scale);
+ }
+
+ /**
+ * Adds a key frame for the given scale at the given keyFrame index
+ * @param keyFrameIndex the index at which the keyFrame must be inserted
+ * @param scale the scale to use for this keyFrame
+ */
+ public void addKeyFrameScale(int keyFrameIndex, Vector3f scale) {
+ Vector3f s = getScaleForFrame(keyFrameIndex);
+ s.set(scale);
+ }
+
+ /**
+ * returns the translation for a given frame index
+ * creates the translation if it doesn't exists
+ * @param keyFrameIndex index
+ * @return the translation
+ */
+ private Vector3f getTranslationForFrame(int keyFrameIndex) {
+ if (keyFrameIndex < 0 || keyFrameIndex > totalFrames) {
+ throw new ArrayIndexOutOfBoundsException("keyFrameIndex must be between 0 and " + totalFrames + " (received " + keyFrameIndex + ")");
+ }
+ Vector3f v = keyFramesTranslation[keyFrameIndex];
+ if (v == null) {
+ v = new Vector3f();
+ keyFramesTranslation[keyFrameIndex] = v;
+ }
+ return v;
+ }
+
+ /**
+ * returns the scale for a given frame index
+ * creates the scale if it doesn't exists
+ * @param keyFrameIndex index
+ * @return the scale
+ */
+ private Vector3f getScaleForFrame(int keyFrameIndex) {
+ if (keyFrameIndex < 0 || keyFrameIndex > totalFrames) {
+ throw new ArrayIndexOutOfBoundsException("keyFrameIndex must be between 0 and " + totalFrames + " (received " + keyFrameIndex + ")");
+ }
+ Vector3f v = keyFramesScale[keyFrameIndex];
+ if (v == null) {
+ v = new Vector3f();
+ keyFramesScale[keyFrameIndex] = v;
+ }
+ return v;
+ }
+
+ /**
+ * returns the rotation for a given frame index
+ * creates the rotation if it doesn't exists
+ * @param keyFrameIndex index
+ * @return the rotation
+ */
+ private Rotation getRotationForFrame(int keyFrameIndex) {
+ if (keyFrameIndex < 0 || keyFrameIndex > totalFrames) {
+ throw new ArrayIndexOutOfBoundsException("keyFrameIndex must be between 0 and " + totalFrames + " (received " + keyFrameIndex + ")");
+ }
+ Rotation v = keyFramesRotation[keyFrameIndex];
+ if (v == null) {
+ v = new Rotation();
+ keyFramesRotation[keyFrameIndex] = v;
+ }
+ return v;
+ }
+
+ /**
+ * Creates an Animation based on the keyFrames previously added to the helper.
+ * @return the generated animation
+ */
+ public Animation buildAnimation() {
+ interpolateTime();
+ interpolate(keyFramesTranslation, Type.Translation);
+ interpolate(keyFramesRotation, Type.Rotation);
+ interpolate(keyFramesScale, Type.Scale);
+
+ SpatialTrack spatialTrack = new SpatialTrack(times, translations, rotations, scales);
+
+ //creating the animation
+ Animation spatialAnimation = new Animation(name, duration);
+ spatialAnimation.setTracks(new SpatialTrack[]{spatialTrack});
+
+ return spatialAnimation;
+ }
+
+ /**
+ * interpolates time values
+ */
+ private void interpolateTime() {
+ for (int i = 0; i < totalFrames; i++) {
+ times[i] = i * tpf;
+ }
+ }
+
+ /**
+ * Interpolates over the key frames for the given keyFrame array and the given type of transform
+ * @param keyFrames the keyFrames array
+ * @param type the type of transforms
+ */
+ private void interpolate(Object[] keyFrames, Type type) {
+ int i = 0;
+ while (i < totalFrames) {
+ //fetching the next keyFrame index transform in the array
+ int key = getNextKeyFrame(i, keyFrames);
+ if (key != -1) {
+ //computing the frame span to interpolate over
+ int span = key - i;
+ //interating over the frames
+ for (int j = i; j <= key; j++) {
+ // computing interpolation value
+ float val = (float) (j - i) / (float) span;
+ //interpolationg depending on the transform type
+ switch (type) {
+ case Translation:
+ translations[j] = FastMath.interpolateLinear(val, (Vector3f) keyFrames[i], (Vector3f) keyFrames[key]);
+ break;
+ case Rotation:
+ Quaternion rot = new Quaternion();
+ rotations[j] = rot.slerp(((Rotation) keyFrames[i]).rotation, ((Rotation) keyFrames[key]).rotation, val);
+ break;
+ case Scale:
+ scales[j] = FastMath.interpolateLinear(val, (Vector3f) keyFrames[i], (Vector3f) keyFrames[key]);
+ break;
+ }
+ }
+ //jumping to the next keyFrame
+ i = key;
+ } else {
+ //No more key frame, filling the array witht he last transform computed.
+ for (int j = i; j < totalFrames; j++) {
+
+ switch (type) {
+ case Translation:
+ translations[j] = ((Vector3f) keyFrames[i]).clone();
+ break;
+ case Rotation:
+ rotations[j] = ((Quaternion) ((Rotation) keyFrames[i]).rotation).clone();
+ break;
+ case Scale:
+ scales[j] = ((Vector3f) keyFrames[i]).clone();
+ break;
+ }
+ }
+ //we're done
+ i = totalFrames;
+ }
+ }
+ }
+
+ /**
+ * Get the index of the next keyFrame that as a transform
+ * @param index the start index
+ * @param keyFrames the keyFrames array
+ * @return the index of the next keyFrame
+ */
+ private int getNextKeyFrame(int index, Object[] keyFrames) {
+ for (int i = index + 1; i < totalFrames; i++) {
+ if (keyFrames[i] != null) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Get the index of the previous keyFrame that as a transform
+ * @param index the start index
+ * @param keyFrames the keyFrames array
+ * @return the index of the previous keyFrame
+ */
+ private int getPreviousKeyFrame(int index, Object[] keyFrames) {
+ for (int i = index - 1; i >= 0; i--) {
+ if (keyFrames[i] != null) {
+ return i;
+ }
+ }
+ return -1;
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2009-2012 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.animation;
+
+import com.jme3.audio.AudioNode;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.util.TempVars;
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * AudioTrack is a track to add to an existing animation, to paly a sound during
+ * an animations for example : gun shot, foot step, shout, etc...
+ *
+ * usage is
+ * <pre>
+ * AnimControl control model.getControl(AnimControl.class);
+ * AudioTrack track = new AudioTrack(existionAudioNode, control.getAnim("TheAnim").getLength());
+ * control.getAnim("TheAnim").addTrack(track);
+ * </pre>
+ *
+ * This is mostly intended for short sounds, playInstance will be called on the
+ * AudioNode at time 0 + startOffset.
+ *
+ *
+ * @author Nehon
+ */
+public class AudioTrack implements ClonableTrack {
+
+ private static final Logger logger = Logger.getLogger(AudioTrack.class.getName());
+ private AudioNode audio;
+ private float startOffset = 0;
+ private float length = 0;
+ private boolean initialized = false;
+ private boolean started = false;
+ private boolean played = false;
+
+ //Animation listener to stop the sound when the animation ends or is changed
+ private class OnEndListener implements AnimEventListener {
+
+ public void onAnimCycleDone(AnimControl control, AnimChannel channel, String animName) {
+ stop();
+ }
+
+ public void onAnimChange(AnimControl control, AnimChannel channel, String animName) {
+ }
+ }
+
+ /**
+ * default constructor for serialization only
+ */
+ public AudioTrack() {
+ }
+
+ /**
+ * Creates an AudioTrack
+ *
+ * @param audio the AudioNode
+ * @param length the length of the track (usually the length of the
+ * animation you want to add the track to)
+ */
+ public AudioTrack(AudioNode audio, float length) {
+ this.audio = audio;
+ this.length = length;
+ setUserData(this);
+ }
+
+ /**
+ * Creates an AudioTrack
+ *
+ * @param audio the AudioNode
+ * @param length the length of the track (usually the length of the
+ * animation you want to add the track to)
+ * @param startOffset the time in second when the sound will be played after
+ * the animation starts (default is 0)
+ */
+ public AudioTrack(AudioNode audio, float length, float startOffset) {
+ this(audio, length);
+ this.startOffset = startOffset;
+ }
+
+ /**
+ * Internal use only
+ *
+ * @see Track#setTime(float, float, com.jme3.animation.AnimControl,
+ * com.jme3.animation.AnimChannel, com.jme3.util.TempVars)
+ */
+ public void setTime(float time, float weight, AnimControl control, AnimChannel channel, TempVars vars) {
+
+ if (time >= length) {
+ return;
+ }
+ if (!initialized) {
+ control.addListener(new OnEndListener());
+ initialized = true;
+ }
+ if (!started && time >= startOffset) {
+ started = true;
+ audio.playInstance();
+ }
+ }
+
+ //stops the sound
+ private void stop() {
+ audio.stop();
+ started = false;
+ }
+
+ /**
+ * Retruns the length of the track
+ *
+ * @return length of the track
+ */
+ public float getLength() {
+ return length;
+ }
+
+ /**
+ * Clone this track
+ *
+ * @return
+ */
+ @Override
+ public Track clone() {
+ return new AudioTrack(audio, length, startOffset);
+ }
+
+ /**
+ * This method clone the Track and search for the cloned counterpart of the
+ * original audio node in the given cloned spatial. The spatial is assumed
+ * to be the Spatial holding the AnimControl controling the animation using
+ * this Track.
+ *
+ * @param spatial the Spatial holding the AnimControl
+ * @return the cloned Track with proper reference
+ */
+ public Track cloneForSpatial(Spatial spatial) {
+ AudioTrack audioTrack = new AudioTrack();
+ audioTrack.length = this.length;
+ audioTrack.startOffset = this.startOffset;
+
+ //searching for the newly cloned AudioNode
+ audioTrack.audio = findAudio(spatial);
+ if (audioTrack.audio == null) {
+ logger.log(Level.WARNING, "{0} was not found in {1} or is not bound to this track", new Object[]{audio.getName(), spatial.getName()});
+ audioTrack.audio = audio;
+ }
+
+ //setting user data on the new AudioNode and marking it with a reference to the cloned Track.
+ setUserData(audioTrack);
+
+ return audioTrack;
+ }
+
+ /**
+ * recursive function responsible for finding the newly cloned AudioNode
+ *
+ * @param spat
+ * @return
+ */
+ private AudioNode findAudio(Spatial spat) {
+ if (spat instanceof AudioNode) {
+ //spat is an AudioNode
+ AudioNode em = (AudioNode) spat;
+ //getting the UserData TrackInfo so check if it should be attached to this Track
+ TrackInfo t = (TrackInfo) em.getUserData("TrackInfo");
+ if (t != null && t.getTracks().contains(this)) {
+ return em;
+ }
+ return null;
+
+ } else if (spat instanceof Node) {
+ for (Spatial child : ((Node) spat).getChildren()) {
+ AudioNode em = findAudio(child);
+ if (em != null) {
+ return em;
+ }
+ }
+ }
+ return null;
+ }
+
+ private void setUserData(AudioTrack audioTrack) {
+ //fetching the UserData TrackInfo.
+ TrackInfo data = (TrackInfo) audioTrack.audio.getUserData("TrackInfo");
+
+ //if it does not exist, we create it and attach it to the AudioNode.
+ if (data == null) {
+ data = new TrackInfo();
+ audioTrack.audio.setUserData("TrackInfo", data);
+ }
+
+ //adding the given Track to the TrackInfo.
+ data.addTrack(audioTrack);
+ }
+
+ public void cleanUp() {
+ TrackInfo t = (TrackInfo) audio.getUserData("TrackInfo");
+ t.getTracks().remove(this);
+ if (!t.getTracks().isEmpty()) {
+ audio.setUserData("TrackInfo", null);
+ }
+ }
+
+ /**
+ *
+ * @return the audio node used by this track
+ */
+ public AudioNode getAudio() {
+ return audio;
+ }
+
+ /**
+ * sets the audio node to be used for this track
+ *
+ * @param audio
+ */
+ public void setAudio(AudioNode audio) {
+ if (this.audio != null) {
+ TrackInfo data = (TrackInfo) audio.getUserData("TrackInfo");
+ data.getTracks().remove(this);
+ }
+ this.audio = audio;
+ setUserData(this);
+ }
+
+ /**
+ *
+ * @return the start offset of the track
+ */
+ public float getStartOffset() {
+ return startOffset;
+ }
+
+ /**
+ * set the start offset of the track
+ *
+ * @param startOffset
+ */
+ public void setStartOffset(float startOffset) {
+ this.startOffset = startOffset;
+ }
+
+ /**
+ * Internal use only serialization
+ *
+ * @param ex exporter
+ * @throws IOException exception
+ */
+ public void write(JmeExporter ex) throws IOException {
+ OutputCapsule out = ex.getCapsule(this);
+ out.write(audio, "audio", null);
+ out.write(length, "length", 0);
+ out.write(startOffset, "startOffset", 0);
+ }
+
+ /**
+ * Internal use only serialization
+ *
+ * @param im importer
+ * @throws IOException Exception
+ */
+ public void read(JmeImporter im) throws IOException {
+ InputCapsule in = im.getCapsule(this);
+ audio = (AudioNode) in.readSavable("audio", null);
+ length = in.readFloat("length", length);
+ startOffset = in.readFloat("startOffset", 0);
+ }
+}
/*
- * Copyright (c) 2009-2010 jMonkeyEngine
+ * Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
*/
package com.jme3.animation;
-import com.jme3.export.JmeExporter;
-import com.jme3.export.JmeImporter;
-import com.jme3.export.InputCapsule;
-import com.jme3.export.OutputCapsule;
-import com.jme3.export.Savable;
-import com.jme3.math.Matrix3f;
-import com.jme3.math.Matrix4f;
-import com.jme3.math.Quaternion;
-import com.jme3.math.Transform;
-import com.jme3.math.Vector3f;
+import com.jme3.export.*;
+import com.jme3.math.*;
import com.jme3.scene.Node;
import com.jme3.util.TempVars;
import java.io.IOException;
* Animation transforms are not applied to this bone when enabled.
*/
private boolean userControl = false;
- private boolean useModelSpaceVectors = false;
/**
* The attachment node.
*/
private Vector3f worldPos = new Vector3f();
private Quaternion worldRot = new Quaternion();
private Vector3f worldScale = new Vector3f();
- //used for getCombinedTransform
+
+ // Used for getCombinedTransform
private Transform tmpTransform = new Transform();
+
+ /**
+ * Used to handle blending from one animation to another.
+ * See {@link #blendAnimTransforms(com.jme3.math.Vector3f, com.jme3.math.Quaternion, com.jme3.math.Vector3f, float)}
+ * on how this variable is used.
+ */
+ private transient float currentWeightSum = -1;
/**
* Creates a new bone with the given name.
* world transform with this bones' local transform.
*/
public final void updateWorldVectors() {
- if (true || !useModelSpaceVectors) {
- if (parent != null) {
- //rotation
- parent.worldRot.mult(localRot, worldRot);
-
- //scale
- //For scale parent scale is not taken into account!
- // worldScale.set(localScale);
- parent.worldScale.mult(localScale, worldScale);
-
- //translation
- //scale and rotation of parent affect bone position
- parent.worldRot.mult(localPos, worldPos);
- worldPos.multLocal(parent.worldScale);
- worldPos.addLocal(parent.worldPos);
+ if (currentWeightSum == 1f) {
+ currentWeightSum = -1;
+ } else if (currentWeightSum != -1f) {
+ // Apply the weight to the local transform
+ if (currentWeightSum == 0) {
+ localRot.set(initialRot);
+ localPos.set(initialPos);
+ localScale.set(initialScale);
} else {
- worldRot.set(localRot);
- worldPos.set(localPos);
- worldScale.set(localScale);
+ float invWeightSum = 1f - currentWeightSum;
+ localRot.nlerp(initialRot, invWeightSum);
+ localPos.interpolate(initialPos, invWeightSum);
+ localScale.interpolate(initialScale, invWeightSum);
}
+
+ // Future invocations of transform blend will start over.
+ currentWeightSum = -1;
+ }
+
+ if (parent != null) {
+ //rotation
+ parent.worldRot.mult(localRot, worldRot);
+
+ //scale
+ //For scale parent scale is not taken into account!
+ // worldScale.set(localScale);
+ parent.worldScale.mult(localScale, worldScale);
+
+ //translation
+ //scale and rotation of parent affect bone position
+ parent.worldRot.mult(localPos, worldPos);
+ worldPos.multLocal(parent.worldScale);
+ worldPos.addLocal(parent.worldPos);
+ } else {
+ worldRot.set(localRot);
+ worldPos.set(localPos);
+ worldScale.set(localScale);
}
+
if (attachNode != null) {
attachNode.setLocalTranslation(worldPos);
attachNode.setLocalRotation(worldRot);
// TODO: add scale here ???
worldPos.set(translation);
worldRot.set(rotation);
+
+ //if there is an attached Node we need to set it's local transforms too.
+ if(attachNode != null){
+ attachNode.setLocalTranslation(translation);
+ attachNode.setLocalRotation(rotation);
+ }
}
/**
* Attach models and effects to this node to make
* them follow this bone's motions.
*/
- public Node getAttachmentsNode() {
+ Node getAttachmentsNode() {
if (attachNode == null) {
attachNode = new Node(name + "_attachnode");
attachNode.setUserData("AttachedBone", this);
}
}
+ /**
+ * Blends the given animation transform onto the bone's local transform.
+ * <p>
+ * Subsequent calls of this method stack up, with the final transformation
+ * of the bone computed at {@link #updateWorldVectors() } which resets
+ * the stack.
+ * <p>
+ * E.g. a single transform blend with weight = 0.5 followed by an
+ * updateWorldVectors() call will result in final transform = transform * 0.5.
+ * Two transform blends with weight = 0.5 each will result in the two
+ * transforms blended together (nlerp) with blend = 0.5.
+ *
+ * @param translation The translation to blend in
+ * @param rotation The rotation to blend in
+ * @param scale The scale to blend in
+ * @param weight The weight of the transform to apply. Set to 1.0 to prevent
+ * any other transform from being applied until updateWorldVectors().
+ */
void blendAnimTransforms(Vector3f translation, Quaternion rotation, Vector3f scale, float weight) {
if (userControl) {
return;
}
-
- TempVars vars = TempVars.get();
-// assert vars.lock();
-
- Vector3f tmpV = vars.vect1;
- Vector3f tmpV2 = vars.vect2;
- Quaternion tmpQ = vars.quat1;
-
- //location
- tmpV.set(initialPos).addLocal(translation);
- localPos.interpolate(tmpV, weight);
-
- //rotation
- tmpQ.set(initialRot).multLocal(rotation);
- localRot.nlerp(tmpQ, weight);
-
- //scale
- if (scale != null) {
- tmpV2.set(initialScale).multLocal(scale);
- localScale.interpolate(tmpV2, weight);
+
+ if (weight == 0) {
+ // Do not apply this transform at all.
+ return;
}
-
- vars.release();
+ if (currentWeightSum == 1){
+ return; // More than 2 transforms are being blended
+ } else if (currentWeightSum == -1 || currentWeightSum == 0) {
+ // Set the transform fully
+ localPos.set(initialPos).addLocal(translation);
+ localRot.set(initialRot).multLocal(rotation);
+ if (scale != null) {
+ localScale.set(initialScale).multLocal(scale);
+ }
+ // Set the weight. It will be applied in updateWorldVectors().
+ currentWeightSum = weight;
+ } else {
+ // The weight is already set.
+ // Blend in the new transform.
+ TempVars vars = TempVars.get();
+
+ Vector3f tmpV = vars.vect1;
+ Vector3f tmpV2 = vars.vect2;
+ Quaternion tmpQ = vars.quat1;
+
+ tmpV.set(initialPos).addLocal(translation);
+ localPos.interpolate(tmpV, weight);
+
+ tmpQ.set(initialRot).multLocal(rotation);
+ localRot.nlerp(tmpQ, weight);
+
+ if (scale != null) {
+ tmpV2.set(initialScale).multLocal(scale);
+ localScale.interpolate(tmpV2, weight);
+ }
+
+ // Ensures no new weights will be blended in the future.
+ currentWeightSum = 1;
+
+ vars.release();
+ }
}
/**
return this.toString(0);
}
- public boolean isUseModelSpaceVectors() {
- return useModelSpaceVectors;
- }
-
- public void setUseModelSpaceVectors(boolean useModelSpaceVectors) {
- this.useModelSpaceVectors = useModelSpaceVectors;
- }
-
@Override
@SuppressWarnings("unchecked")
public void read(JmeImporter im) throws IOException {
/*
- * Copyright (c) 2009-2010 jMonkeyEngine
+ * Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
/*
- * Copyright (c) 2009-2010 jMonkeyEngine
+ * Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
*/
package com.jme3.animation;
-import com.jme3.export.JmeExporter;
-import com.jme3.export.JmeImporter;
-import com.jme3.export.InputCapsule;
-import com.jme3.export.OutputCapsule;
-import com.jme3.export.Savable;
+import com.jme3.export.*;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.util.TempVars;
* The transforms can be interpolated in some method from the keyframes.
*
* @param time the current time of the animation
- * @param skeleton the skeleton to which the bone belong
* @param weight the weight of the animation
+ * @param control
+ * @param channel
+ * @param vars
*/
public void setTime(float time, float weight, AnimControl control, AnimChannel channel, TempVars vars) {
BitSet affectedBones = channel.getAffectedBones();
tempS.interpolate(tempS2, blend);
}
- if (weight != 1f) {
+// if (weight != 1f) {
target.blendAnimTransforms(tempV, tempQ, scales != null ? tempS : null, weight);
- } else {
- target.setAnimTransforms(tempV, tempQ, scales != null ? tempS : null);
- }
+// } else {
+// target.setAnimTransforms(tempV, tempQ, scales != null ? tempS : null);
+// }
}
/**
--- /dev/null
+/*
+ * Copyright (c) 2009-2012 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.animation;
+
+import com.jme3.scene.Spatial;
+
+/**
+ * An interface that allow to clone a Track for a given Spatial.
+ * The spatial fed to the method is the Spatial holding the AnimControl controling the Animation using this track.
+ *
+ * Implement this interface only if you make your own Savable Track and that the track has a direct reference to a Spatial in the scene graph.
+ * This Spatial is assumed to be a child of the spatial holding the AnimControl.
+ *
+ *
+ * @author Nehon
+ */
+public interface ClonableTrack extends Track {
+
+ /**
+ * Allows to clone the track for a given Spatial.
+ * The spatial fed to the method is the Spatial holding the AnimControl controling the Animation using this track.
+ * This method will be called during the loading process of a j3o model by the assetManager.
+ * The assetManager keeps the original model in cache and returns a clone of the model.
+ *
+ * This method prupose is to find the cloned reference of the original spatial which it refers to in the cloned model.
+ *
+ * See EffectTrack for a proper implementation.
+ *
+ * @param spatial the spatial holding the AnimControl
+ * @return the cloned Track
+ */
+ public Track cloneForSpatial(Spatial spatial);
+
+ /**
+ * Method responsible of cleaning UserData on referenced Spatials when the Track is deleted
+ */
+ public void cleanUp();
+}
/*\r
- * Copyright (c) 2009-2010 jMonkeyEngine\r
+ * Copyright (c) 2009-2012 jMonkeyEngine\r
* All rights reserved.\r
*\r
* Redistribution and use in source and binary forms, with or without\r
/*\r
- * Copyright (c) 2009-2010 jMonkeyEngine\r
+ * Copyright (c) 2009-2012 jMonkeyEngine\r
* All rights reserved.\r
*\r
* Redistribution and use in source and binary forms, with or without\r
*/\r
package com.jme3.animation;\r
\r
-import java.io.IOException;\r
-\r
-import com.jme3.export.InputCapsule;\r
-import com.jme3.export.JmeExporter;\r
-import com.jme3.export.JmeImporter;\r
-import com.jme3.export.OutputCapsule;\r
-import com.jme3.export.Savable;\r
+import com.jme3.export.*;\r
import com.jme3.math.Quaternion;\r
+import java.io.IOException;\r
\r
/**\r
* Serialize and compress {@link Quaternion}[] by indexing same values\r
/*\r
- * Copyright (c) 2009-2010 jMonkeyEngine\r
+ * Copyright (c) 2009-2012 jMonkeyEngine\r
* All rights reserved.\r
*\r
* Redistribution and use in source and binary forms, with or without\r
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\r
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\r
*/\r
-\r
package com.jme3.animation;\r
\r
-import java.io.IOException;\r
-\r
-import com.jme3.export.InputCapsule;\r
-import com.jme3.export.JmeExporter;\r
-import com.jme3.export.JmeImporter;\r
-import com.jme3.export.OutputCapsule;\r
-import com.jme3.export.Savable;\r
+import com.jme3.export.*;\r
import com.jme3.math.Vector3f;\r
+import java.io.IOException;\r
\r
/**\r
* Serialize and compress Vector3f[] by indexing same values\r
--- /dev/null
+/*
+ * Copyright (c) 2009-2012 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.animation;
+
+import com.jme3.effect.ParticleEmitter;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.ViewPort;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.Spatial.CullHint;
+import com.jme3.scene.control.AbstractControl;
+import com.jme3.scene.control.Control;
+import com.jme3.util.TempVars;
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * EffectTrack is a track to add to an existing animation, to emmit particles
+ * during animations for example : exhausts, dust raised by foot steps, shock
+ * waves, lightnings etc...
+ *
+ * usage is
+ * <pre>
+ * AnimControl control model.getControl(AnimControl.class);
+ * EffectTrack track = new EffectTrack(existingEmmitter, control.getAnim("TheAnim").getLength());
+ * control.getAnim("TheAnim").addTrack(track);
+ * </pre>
+ *
+ * if the emitter has emmits 0 particles per seconds emmitAllPArticles will be
+ * called on it at time 0 + startOffset. if it he it has more it will start
+ * emmit normally at time 0 + startOffset.
+ *
+ *
+ * @author Nehon
+ */
+public class EffectTrack implements ClonableTrack {
+
+ private static final Logger logger = Logger.getLogger(EffectTrack.class.getName());
+ private ParticleEmitter emitter;
+ private float startOffset = 0;
+ private float particlesPerSeconds = 0;
+ private float length = 0;
+ private boolean emitted = false;
+ private boolean initialized = false;
+ //control responsible for disable and cull the emitter once all particles are gone
+ private KillParticleControl killParticles = new KillParticleControl();
+
+ public static class KillParticleControl extends AbstractControl {
+
+ ParticleEmitter emitter;
+ boolean stopRequested = false;
+ boolean remove = false;
+
+ public KillParticleControl() {
+ }
+
+ @Override
+ public void setSpatial(Spatial spatial) {
+ super.setSpatial(spatial);
+ if (spatial != null) {
+ if (spatial instanceof ParticleEmitter) {
+ emitter = (ParticleEmitter) spatial;
+ } else {
+ throw new IllegalArgumentException("KillParticleEmitter can only ba attached to ParticleEmitter");
+ }
+ }
+
+
+ }
+
+ @Override
+ protected void controlUpdate(float tpf) {
+ if (remove) {
+ emitter.removeControl(this);
+ return;
+ }
+ if (emitter.getNumVisibleParticles() == 0) {
+ emitter.setCullHint(CullHint.Always);
+ emitter.setEnabled(false);
+ emitter.removeControl(this);
+ stopRequested = false;
+ }
+ }
+
+ @Override
+ protected void controlRender(RenderManager rm, ViewPort vp) {
+ }
+
+ @Override
+ public Control cloneForSpatial(Spatial spatial) {
+
+ KillParticleControl c = new KillParticleControl();
+ //this control should be removed as it shouldn't have been persisted in the first place
+ //In the quest to find the less hackish solution to achieve this,
+ //making it remove itself from the spatial in the first update loop when loaded was the less bad.
+ c.remove = true;
+ c.setSpatial(spatial);
+ return c;
+
+ }
+ };
+
+ //Anim listener that stops the Emmitter when the animation is finished or changed.
+ private class OnEndListener implements AnimEventListener {
+
+ public void onAnimCycleDone(AnimControl control, AnimChannel channel, String animName) {
+ stop();
+ }
+
+ public void onAnimChange(AnimControl control, AnimChannel channel, String animName) {
+ }
+ }
+
+ /**
+ * default constructor only for serialization
+ */
+ public EffectTrack() {
+ }
+
+ /**
+ * Creates and EffectTrack
+ *
+ * @param emitter the emmitter of the track
+ * @param length the length of the track (usually the length of the
+ * animation you want to add the track to)
+ */
+ public EffectTrack(ParticleEmitter emitter, float length) {
+ this.emitter = emitter;
+ //saving particles per second value
+ this.particlesPerSeconds = emitter.getParticlesPerSec();
+ //setting the emmitter to not emmit.
+ this.emitter.setParticlesPerSec(0);
+ this.length = length;
+ //Marking the emitter with a reference to this track for further use in deserialization.
+ setUserData(this);
+
+ }
+
+ /**
+ * Creates and EffectTrack
+ *
+ * @param emitter the emmitter of the track
+ * @param length the length of the track (usually the length of the
+ * animation you want to add the track to)
+ * @param startOffset the time in second when the emitter will be triggerd
+ * after the animation starts (default is 0)
+ */
+ public EffectTrack(ParticleEmitter emitter, float length, float startOffset) {
+ this(emitter, length);
+ this.startOffset = startOffset;
+ }
+
+ /**
+ * Internal use only
+ *
+ * @see Track#setTime(float, float, com.jme3.animation.AnimControl,
+ * com.jme3.animation.AnimChannel, com.jme3.util.TempVars)
+ */
+ public void setTime(float time, float weight, AnimControl control, AnimChannel channel, TempVars vars) {
+
+ if (time >= length) {
+ return;
+ }
+ //first time adding the Animation listener to stop the track at the end of the animation
+ if (!initialized) {
+ control.addListener(new OnEndListener());
+ initialized = true;
+ }
+ //checking fo time to trigger the effect
+ if (!emitted && time >= startOffset) {
+ emitted = true;
+ emitter.setCullHint(CullHint.Dynamic);
+ emitter.setEnabled(true);
+ //if the emitter has 0 particles per seconds emmit all particles in one shot
+ if (particlesPerSeconds == 0) {
+ emitter.emitAllParticles();
+ if (!killParticles.stopRequested) {
+ emitter.addControl(killParticles);
+ killParticles.stopRequested = true;
+ }
+ } else {
+ //else reset its former particlePerSec value to let it emmit.
+ emitter.setParticlesPerSec(particlesPerSeconds);
+ }
+ }
+ }
+
+ //stops the emmiter to emit.
+ private void stop() {
+ emitter.setParticlesPerSec(0);
+ emitted = false;
+ if (!killParticles.stopRequested) {
+ emitter.addControl(killParticles);
+ killParticles.stopRequested = true;
+ }
+
+ }
+
+ /**
+ * Retruns the length of the track
+ *
+ * @return length of the track
+ */
+ public float getLength() {
+ return length;
+ }
+
+ /**
+ * Clone this track
+ *
+ * @return
+ */
+ @Override
+ public Track clone() {
+ return new EffectTrack(emitter, length, startOffset);
+ }
+
+ /**
+ * This method clone the Track and search for the cloned counterpart of the
+ * original emmitter in the given cloned spatial. The spatial is assumed to
+ * be the Spatial holding the AnimControl controling the animation using
+ * this Track.
+ *
+ * @param spatial the Spatial holding the AnimControl
+ * @return the cloned Track with proper reference
+ */
+ public Track cloneForSpatial(Spatial spatial) {
+ EffectTrack effectTrack = new EffectTrack();
+ effectTrack.particlesPerSeconds = this.particlesPerSeconds;
+ effectTrack.length = this.length;
+ effectTrack.startOffset = this.startOffset;
+
+ //searching for the newly cloned ParticleEmitter
+ effectTrack.emitter = findEmitter(spatial);
+ if (effectTrack.emitter == null) {
+ logger.log(Level.WARNING, "{0} was not found in {1} or is not bound to this track", new Object[]{emitter.getName(), spatial.getName()});
+ effectTrack.emitter = emitter;
+ }
+
+ removeUserData(this);
+ //setting user data on the new emmitter and marking it with a reference to the cloned Track.
+ setUserData(effectTrack);
+ effectTrack.emitter.setParticlesPerSec(0);
+ return effectTrack;
+ }
+
+ /**
+ * recursive function responsible for finding the newly cloned Emitter
+ *
+ * @param spat
+ * @return
+ */
+ private ParticleEmitter findEmitter(Spatial spat) {
+ if (spat instanceof ParticleEmitter) {
+ //spat is a PArticleEmitter
+ ParticleEmitter em = (ParticleEmitter) spat;
+ //getting the UserData TrackInfo so check if it should be attached to this Track
+ TrackInfo t = (TrackInfo) em.getUserData("TrackInfo");
+ if (t != null && t.getTracks().contains(this)) {
+ return em;
+ }
+ return null;
+
+ } else if (spat instanceof Node) {
+ for (Spatial child : ((Node) spat).getChildren()) {
+ ParticleEmitter em = findEmitter(child);
+ if (em != null) {
+ return em;
+ }
+ }
+ }
+ return null;
+ }
+
+ public void cleanUp() {
+ TrackInfo t = (TrackInfo) emitter.getUserData("TrackInfo");
+ t.getTracks().remove(this);
+ if (t.getTracks().isEmpty()) {
+ emitter.setUserData("TrackInfo", null);
+ }
+ }
+
+ /**
+ *
+ * @return the emitter used by this track
+ */
+ public ParticleEmitter getEmitter() {
+ return emitter;
+ }
+
+ /**
+ * Sets the Emitter to use in this track
+ *
+ * @param emitter
+ */
+ public void setEmitter(ParticleEmitter emitter) {
+ if (this.emitter != null) {
+ TrackInfo data = (TrackInfo) emitter.getUserData("TrackInfo");
+ data.getTracks().remove(this);
+ }
+ this.emitter = emitter;
+ //saving particles per second value
+ this.particlesPerSeconds = emitter.getParticlesPerSec();
+ //setting the emmitter to not emmit.
+ this.emitter.setParticlesPerSec(0);
+ setUserData(this);
+ }
+
+ /**
+ *
+ * @return the start offset of the track
+ */
+ public float getStartOffset() {
+ return startOffset;
+ }
+
+ /**
+ * set the start offset of the track
+ *
+ * @param startOffset
+ */
+ public void setStartOffset(float startOffset) {
+ this.startOffset = startOffset;
+ }
+
+ private void setUserData(EffectTrack effectTrack) {
+ //fetching the UserData TrackInfo.
+ TrackInfo data = (TrackInfo) effectTrack.emitter.getUserData("TrackInfo");
+
+ //if it does not exist, we create it and attach it to the emitter.
+ if (data == null) {
+ data = new TrackInfo();
+ effectTrack.emitter.setUserData("TrackInfo", data);
+ }
+
+ //adding the given Track to the TrackInfo.
+ data.addTrack(effectTrack);
+
+
+ }
+
+ private void removeUserData(EffectTrack effectTrack) {
+ //fetching the UserData TrackInfo.
+ TrackInfo data = (TrackInfo) effectTrack.emitter.getUserData("TrackInfo");
+
+ //if it does not exist, we create it and attach it to the emitter.
+ if (data == null) {
+ return;
+ }
+
+ //removing the given Track to the TrackInfo.
+ data.getTracks().remove(effectTrack);
+
+
+ }
+
+ /**
+ * Internal use only serialization
+ *
+ * @param ex exporter
+ * @throws IOException exception
+ */
+ public void write(JmeExporter ex) throws IOException {
+ OutputCapsule out = ex.getCapsule(this);
+ //reseting the particle emission rate on the emitter before saving.
+ emitter.setParticlesPerSec(particlesPerSeconds);
+ out.write(emitter, "emitter", null);
+ out.write(particlesPerSeconds, "particlesPerSeconds", 0);
+ out.write(length, "length", 0);
+ out.write(startOffset, "startOffset", 0);
+ //Setting emission rate to 0 so that this track can go on being used.
+ emitter.setParticlesPerSec(0);
+ }
+
+ /**
+ * Internal use only serialization
+ *
+ * @param im importer
+ * @throws IOException Exception
+ */
+ public void read(JmeImporter im) throws IOException {
+ InputCapsule in = im.getCapsule(this);
+ this.particlesPerSeconds = in.readFloat("particlesPerSeconds", 0);
+ //reading the emitter even if the track will then reference its cloned counter part if it's loaded with the assetManager.
+ //This also avoid null pointer exception if the model is not loaded via the AssetManager.
+ emitter = (ParticleEmitter) in.readSavable("emitter", null);
+ emitter.setParticlesPerSec(0);
+ //if the emitter was saved with a KillParticleControl we remove it.
+// Control c = emitter.getControl(KillParticleControl.class);
+// if(c!=null){
+// emitter.removeControl(c);
+// }
+ //emitter.removeControl(KillParticleControl.class);
+ length = in.readFloat("length", length);
+ startOffset = in.readFloat("startOffset", 0);
+ }
+}
/*
- * Copyright (c) 2009-2010 jMonkeyEngine
+ * Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-
package com.jme3.animation;
/**
/*
- * Copyright (c) 2009-2010 jMonkeyEngine
+ * Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-
package com.jme3.animation;
-import java.io.IOException;
-import java.nio.FloatBuffer;
-
-import com.jme3.export.InputCapsule;
-import com.jme3.export.JmeExporter;
-import com.jme3.export.JmeImporter;
-import com.jme3.export.OutputCapsule;
-import com.jme3.export.Savable;
+import com.jme3.export.*;
import com.jme3.math.Vector3f;
import com.jme3.util.BufferUtils;
+import java.io.IOException;
+import java.nio.FloatBuffer;
/**
* A pose is a list of offsets that say where a mesh vertices should be for this pose.
this.indices = indices;
}
+ /**
+ * Serialization-only. Do not use.
+ */
+ public Pose()
+ {
+ }
+
public int getTargetMeshIndex(){
return targetMeshIndex;
}
* This method creates a clone of the current object.
* @return a clone of the current object
*/
+ @Override
public Pose clone() {
- try {
- Pose result = (Pose) super.clone();
+ try {
+ Pose result = (Pose) super.clone();
result.indices = this.indices.clone();
- if(this.offsets!=null) {
- result.offsets = new Vector3f[this.offsets.length];
- for(int i=0;i<this.offsets.length;++i) {
- result.offsets[i] = this.offsets[i].clone();
- }
+ if (this.offsets != null) {
+ result.offsets = new Vector3f[this.offsets.length];
+ for (int i = 0; i < this.offsets.length; ++i) {
+ result.offsets[i] = this.offsets[i].clone();
+ }
}
- return result;
+ return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
- }
+ }
public void write(JmeExporter e) throws IOException {
OutputCapsule out = e.getCapsule(this);
InputCapsule in = i.getCapsule(this);
name = in.readString("name", "");
targetMeshIndex = in.readInt("meshIndex", -1);
- offsets = (Vector3f[]) in.readSavableArray("offsets", null);
indices = in.readIntArray("indices", null);
+
+ Savable[] readSavableArray = in.readSavableArray("offsets", null);
+ if (readSavableArray != null) {
+ offsets = new Vector3f[readSavableArray.length];
+ System.arraycopy(readSavableArray, 0, offsets, 0, readSavableArray.length);
+ }
}
}
/*
- * Copyright (c) 2009-2010 jMonkeyEngine
+ * Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-
package com.jme3.animation;
-import com.jme3.export.JmeExporter;
-import com.jme3.export.JmeImporter;
-import com.jme3.export.InputCapsule;
-import com.jme3.export.OutputCapsule;
-import com.jme3.export.Savable;
+import com.jme3.export.*;
import com.jme3.scene.Mesh;
import com.jme3.scene.VertexBuffer;
import com.jme3.scene.VertexBuffer.Type;
}
/**
+ * Serialization-only. Do not use.
+ */
+ public PoseFrame()
+ {
+ }
+
+ /**
* This method creates a clone of the current object.
* @return a clone of the current object
*/
public void read(JmeImporter i) throws IOException {
InputCapsule in = i.getCapsule(this);
- poses = (Pose[]) in.readSavableArray("poses", null);
weights = in.readFloatArray("weights", null);
+
+ Savable[] readSavableArray = in.readSavableArray("poses", null);
+ if (readSavableArray != null) {
+ poses = new Pose[readSavableArray.length];
+ System.arraycopy(readSavableArray, 0, poses, 0, readSavableArray.length);
+ }
}
}
this.frames = frames;
}
+ /**
+ * Serialization-only. Do not use.
+ */
+ public PoseTrack()
+ {
+ }
+
private void applyFrame(Mesh target, int frameIndex, float weight){
PoseFrame frame = frames[frameIndex];
VertexBuffer pb = target.getBuffer(Type.Position);
public void read(JmeImporter i) throws IOException {
InputCapsule in = i.getCapsule(this);
targetMeshIndex = in.readInt("meshIndex", 0);
- frames = (PoseFrame[]) in.readSavableArray("frames", null);
times = in.readFloatArray("times", null);
+
+ Savable[] readSavableArray = in.readSavableArray("frames", null);
+ if (readSavableArray != null) {
+ frames = new PoseFrame[readSavableArray.length];
+ System.arraycopy(readSavableArray, 0, frames, 0, readSavableArray.length);
+ }
}
}
/*
- * Copyright (c) 2009-2010 jMonkeyEngine
+ * Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
*/
package com.jme3.animation;
-import com.jme3.export.JmeExporter;
-import com.jme3.export.JmeImporter;
-import com.jme3.export.InputCapsule;
-import com.jme3.export.OutputCapsule;
-import com.jme3.export.Savable;
+import com.jme3.export.*;
import com.jme3.math.Matrix4f;
import com.jme3.util.TempVars;
import java.io.IOException;
-
import java.util.ArrayList;
import java.util.List;
/*
- * To change this template, choose Tools | Templates
- * and open the template in the editor.
+ * Copyright (c) 2009-2012 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.animation;
-import com.jme3.export.InputCapsule;
-import com.jme3.export.JmeExporter;
-import com.jme3.export.JmeImporter;
-import com.jme3.export.OutputCapsule;
-import com.jme3.export.Savable;
+import com.jme3.export.*;
+import com.jme3.material.Material;
import com.jme3.math.FastMath;
import com.jme3.math.Matrix4f;
import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.RendererException;
import com.jme3.renderer.ViewPort;
-import com.jme3.scene.Geometry;
-import com.jme3.scene.Mesh;
-import com.jme3.scene.Node;
-import com.jme3.scene.Spatial;
-import com.jme3.scene.UserData;
-import com.jme3.scene.VertexBuffer;
+import com.jme3.scene.*;
import com.jme3.scene.VertexBuffer.Type;
import com.jme3.scene.control.AbstractControl;
import com.jme3.scene.control.Control;
+import com.jme3.shader.VarType;
+import com.jme3.util.SafeArrayList;
import com.jme3.util.TempVars;
import java.io.IOException;
+import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
-import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
/**
- * The Skeleton control deforms a model according to a skeleton,
- * It handles the computation of the deformation matrices and performs
- * the transformations on the mesh
- *
+ * The Skeleton control deforms a model according to a skeleton, It handles the
+ * computation of the deformation matrices and performs the transformations on
+ * the mesh
+ *
* @author Rémy Bouquet Based on AnimControl by Kirill Vainer
*/
public class SkeletonControl extends AbstractControl implements Cloneable {
/**
- * The skeleton of the model
+ * The skeleton of the model.
*/
private Skeleton skeleton;
/**
* List of targets which this controller effects.
*/
- private Mesh[] targets;
+ private SafeArrayList<Mesh> targets = new SafeArrayList<Mesh>(Mesh.class);
/**
- * Used to track when a mesh was updated. Meshes are only updated
- * if they are visible in at least one camera.
+ * Used to track when a mesh was updated. Meshes are only updated if they
+ * are visible in at least one camera.
*/
private boolean wasMeshUpdated = false;
+
+ /**
+ * User wishes to use hardware skinning if available.
+ */
+ private transient boolean hwSkinningDesired = false;
+
+ /**
+ * Hardware skinning is currently being used.
+ */
+ private transient boolean hwSkinningEnabled = false;
+
+ /**
+ * Hardware skinning was tested on this GPU, results
+ * are stored in {@link #hwSkinningSupported} variable.
+ */
+ private transient boolean hwSkinningTested = false;
+
+ /**
+ * If hardware skinning was {@link #hwSkinningTested tested}, then
+ * this variable will be set to true if supported, and false if otherwise.
+ */
+ private transient boolean hwSkinningSupported = false;
+
+ /**
+ * Bone offset matrices, recreated each frame
+ */
+ private transient Matrix4f[] offsetMatrices;
+ /**
+ * Material references used for hardware skinning
+ */
+ private Set<Material> materials = new HashSet<Material>();
/**
* Serialization only. Do not use.
public SkeletonControl() {
}
+ private void switchToHardware() {
+ // Next full 10 bones (e.g. 30 on 24 bones)
+ int numBones = ((skeleton.getBoneCount() / 10) + 1) * 10;
+ for (Material m : materials) {
+ m.setInt("NumberOfBones", numBones);
+ }
+ for (Mesh mesh : targets) {
+ if (mesh.isAnimated()) {
+ mesh.prepareForAnim(false);
+ }
+ }
+ }
+
+ private void switchToSoftware() {
+ for (Material m : materials) {
+ if (m.getParam("NumberOfBones") != null) {
+ m.clearParam("NumberOfBones");
+ }
+ }
+ for (Mesh mesh : targets) {
+ if (mesh.isAnimated()) {
+ mesh.prepareForAnim(true);
+ }
+ }
+ }
+
+ private boolean testHardwareSupported(RenderManager rm) {
+ for (Material m : materials) {
+ // Some of the animated mesh(es) do not support hardware skinning,
+ // so it is not supported by the model.
+ if (m.getMaterialDef().getMaterialParam("NumberOfBones") == null) {
+ Logger.getLogger(SkeletonControl.class.getName()).log(Level.WARNING,
+ "Not using hardware skinning for {0}, " +
+ "because material {1} doesn''t support it.",
+ new Object[]{spatial, m.getMaterialDef().getName()});
+
+ return false;
+ }
+ }
+
+ switchToHardware();
+
+ try {
+ rm.preloadScene(spatial);
+ return true;
+ } catch (RendererException e) {
+ Logger.getLogger(SkeletonControl.class.getName()).log(Level.WARNING, "Could not enable HW skinning due to shader compile error:", e);
+ return false;
+ }
+ }
+
/**
- * Creates a skeleton control.
- * The list of targets will be acquired automatically when
- * the control is attached to a node.
+ * Specifies if hardware skinning is preferred. If it is preferred and
+ * supported by GPU, it shall be enabled, if its not preferred, or not
+ * supported by GPU, then it shall be disabled.
+ *
+ * @see #isHardwareSkinningUsed()
+ */
+ public void setHardwareSkinningPreferred(boolean preferred) {
+ hwSkinningDesired = preferred;
+ }
+
+ /**
+ * @return True if hardware skinning is preferable to software skinning.
+ * Set to false by default.
*
+ * @see #setHardwareSkinningPreferred(boolean)
+ */
+ public boolean isHardwareSkinningPreferred() {
+ return hwSkinningDesired;
+ }
+
+ /**
+ * @return True is hardware skinning is activated and is currently used, false otherwise.
+ */
+ public boolean isHardwareSkinningUsed() {
+ return hwSkinningEnabled;
+ }
+
+ /**
+ * Creates a skeleton control. The list of targets will be acquired
+ * automatically when the control is attached to a node.
+ *
* @param skeleton the skeleton
*/
public SkeletonControl(Skeleton skeleton) {
this.skeleton = skeleton;
}
-
+
/**
* Creates a skeleton control.
- *
+ *
* @param targets the meshes controlled by the skeleton
* @param skeleton the skeleton
*/
@Deprecated
- SkeletonControl(Mesh[] targets, Skeleton skeleton){
+ SkeletonControl(Mesh[] targets, Skeleton skeleton) {
this.skeleton = skeleton;
- this.targets = targets;
- }
-
- private boolean isMeshAnimated(Mesh mesh){
- return mesh.getBuffer(Type.BindPosePosition) != null;
+ this.targets = new SafeArrayList<Mesh>(Mesh.class, Arrays.asList(targets));
}
- private Mesh[] findTargets(Node node){
- Mesh sharedMesh = null;
- ArrayList<Mesh> animatedMeshes = new ArrayList<Mesh>();
-
- for (Spatial child : node.getChildren()){
- if (!(child instanceof Geometry)){
- continue; // could be an attachment node, ignore.
- }
-
- Geometry geom = (Geometry) child;
-
- // is this geometry using a shared mesh?
- Mesh childSharedMesh = geom.getUserData(UserData.JME_SHAREDMESH);
-
- if (childSharedMesh != null){
- // Don't bother with non-animated shared meshes
- if (isMeshAnimated(childSharedMesh)){
- // child is using shared mesh,
- // so animate the shared mesh but ignore child
- if (sharedMesh == null){
- sharedMesh = childSharedMesh;
- }else if (sharedMesh != childSharedMesh){
- throw new IllegalStateException("Two conflicting shared meshes for " + node);
+
+ private void findTargets(Node node) {
+ Mesh sharedMesh = null;
+
+ for (Spatial child : node.getChildren()) {
+ if (child instanceof Geometry) {
+ Geometry geom = (Geometry) child;
+
+ // is this geometry using a shared mesh?
+ Mesh childSharedMesh = geom.getUserData(UserData.JME_SHAREDMESH);
+
+ if (childSharedMesh != null) {
+ // Don’t bother with non-animated shared meshes
+ if (childSharedMesh.isAnimated()) {
+ // child is using shared mesh,
+ // so animate the shared mesh but ignore child
+ if (sharedMesh == null) {
+ sharedMesh = childSharedMesh;
+ } else if (sharedMesh != childSharedMesh) {
+ throw new IllegalStateException("Two conflicting shared meshes for " + node);
+ }
+ materials.add(geom.getMaterial());
+ }
+ } else {
+ Mesh mesh = geom.getMesh();
+ if (mesh.isAnimated()) {
+ targets.add(mesh);
+ materials.add(geom.getMaterial());
}
}
- }else{
- Mesh mesh = geom.getMesh();
- if (isMeshAnimated(mesh)){
- animatedMeshes.add(mesh);
- }
+ } else if (child instanceof Node) {
+ findTargets((Node) child);
}
}
-
- if (sharedMesh != null){
- animatedMeshes.add(sharedMesh);
+
+ if (sharedMesh != null) {
+ targets.add(sharedMesh);
}
-
- return animatedMeshes.toArray(new Mesh[animatedMeshes.size()]);
+
}
-
+
@Override
- public void setSpatial(Spatial spatial){
+ public void setSpatial(Spatial spatial) {
super.setSpatial(spatial);
- if (spatial != null){
- Node node = (Node) spatial;
- targets = findTargets(node);
- }else{
- targets = null;
+ updateTargetsAndMaterials(spatial);
+ }
+
+ private void controlRenderSoftware() {
+ resetToBind(); // reset morph meshes to bind pose
+
+ offsetMatrices = skeleton.computeSkinningMatrices();
+
+ for (Mesh mesh : targets) {
+ // NOTE: This assumes that code higher up
+ // Already ensured those targets are animated
+ // otherwise a crash will happen in skin update
+ softwareSkinUpdate(mesh, offsetMatrices);
+ }
+ }
+
+ private void controlRenderHardware() {
+ offsetMatrices = skeleton.computeSkinningMatrices();
+ for (Material m : materials) {
+ m.setParam("BoneMatrices", VarType.Matrix4Array, offsetMatrices);
}
}
+
@Override
protected void controlRender(RenderManager rm, ViewPort vp) {
if (!wasMeshUpdated) {
- resetToBind(); // reset morph meshes to bind pose
-
- Matrix4f[] offsetMatrices = skeleton.computeSkinningMatrices();
-
- // if hardware skinning is supported, the matrices and weight buffer
- // will be sent by the SkinningShaderLogic object assigned to the shader
- for (int i = 0; i < targets.length; i++) {
- // NOTE: This assumes that code higher up
- // Already ensured those targets are animated
- // otherwise a crash will happen in skin update
- //if (isMeshAnimated(targets[i])) {
- softwareSkinUpdate(targets[i], offsetMatrices);
- //}
+ updateTargetsAndMaterials(spatial);
+
+ // Prevent illegal cases. These should never happen.
+ assert hwSkinningTested || (!hwSkinningTested && !hwSkinningSupported && !hwSkinningEnabled);
+ assert !hwSkinningEnabled || (hwSkinningEnabled && hwSkinningTested && hwSkinningSupported);
+
+ if (hwSkinningDesired && !hwSkinningTested) {
+ hwSkinningTested = true;
+ hwSkinningSupported = testHardwareSupported(rm);
+
+ if (hwSkinningSupported) {
+ hwSkinningEnabled = true;
+
+ Logger.getLogger(SkeletonControl.class.getName()).log(Level.INFO, "Hardware skinning engaged for " + spatial);
+ } else {
+ switchToSoftware();
+ }
+ } else if (hwSkinningDesired && hwSkinningSupported && !hwSkinningEnabled) {
+ switchToHardware();
+ hwSkinningEnabled = true;
+ } else if (!hwSkinningDesired && hwSkinningEnabled) {
+ switchToSoftware();
+ hwSkinningEnabled = false;
+ }
+
+ if (hwSkinningEnabled) {
+ controlRenderHardware();
+ } else {
+ controlRenderSoftware();
}
wasMeshUpdated = true;
@Override
protected void controlUpdate(float tpf) {
wasMeshUpdated = false;
- }
+ }
+ //only do this for software updates
void resetToBind() {
- for (Mesh mesh : targets){
- if (isMeshAnimated(mesh)) {
- VertexBuffer bi = mesh.getBuffer(Type.BoneIndex);
- ByteBuffer bib = (ByteBuffer) bi.getData();
- if (!bib.hasArray()) {
+ for (Mesh mesh : targets) {
+ if (mesh.isAnimated()) {
+ Buffer bwBuff = mesh.getBuffer(Type.BoneWeight).getData();
+ Buffer biBuff = mesh.getBuffer(Type.BoneIndex).getData();
+ if (!biBuff.hasArray() || !bwBuff.hasArray()) {
mesh.prepareForAnim(true); // prepare for software animation
}
VertexBuffer bindPos = mesh.getBuffer(Type.BindPosePosition);
nb.clear();
bpb.clear();
bnb.clear();
+
+ //reseting bind tangents if there is a bind tangent buffer
+ VertexBuffer bindTangents = mesh.getBuffer(Type.BindPoseTangent);
+ if (bindTangents != null) {
+ VertexBuffer tangents = mesh.getBuffer(Type.Tangent);
+ FloatBuffer tb = (FloatBuffer) tangents.getData();
+ FloatBuffer btb = (FloatBuffer) bindTangents.getData();
+ tb.clear();
+ btb.clear();
+ tb.put(btb).clear();
+ }
+
+
pb.put(bpb).clear();
nb.put(bnb).clear();
}
Node clonedNode = (Node) spatial;
AnimControl ctrl = spatial.getControl(AnimControl.class);
SkeletonControl clone = new SkeletonControl();
- clone.setSpatial(clonedNode);
clone.skeleton = ctrl.getSkeleton();
- // Fix animated targets for the cloned node
- clone.targets = findTargets(clonedNode);
- // Fix attachments for the cloned node
+ clone.setSpatial(clonedNode);
+
+ // Fix attachments for the cloned node
for (int i = 0; i < clonedNode.getQuantity(); i++) {
// go through attachment nodes, apply them to correct bone
Spatial child = clonedNode.getChild(i);
}
/**
- *
+ *
* @param boneName the name of the bone
- * @return the node attached to this bone
+ * @return the node attached to this bone
*/
public Node getAttachmentsNode(String boneName) {
Bone b = skeleton.getBone(boneName);
/**
* returns the skeleton of this control
- * @return
+ *
+ * @return
*/
public Skeleton getSkeleton() {
return skeleton;
}
/**
- * sets the skeleton for this control
- * @param skeleton
+ * returns a copy of array of the targets meshes of this control
+ *
+ * @return
*/
-// public void setSkeleton(Skeleton skeleton) {
-// this.skeleton = skeleton;
-// }
+ public Mesh[] getTargets() {
+ return targets.toArray(new Mesh[targets.size()]);
+ }
/**
- * returns the targets meshes of this control
- * @return
+ * Update the mesh according to the given transformation matrices
+ *
+ * @param mesh then mesh
+ * @param offsetMatrices the transformation matrices to apply
*/
- public Mesh[] getTargets() {
- return targets;
+ private void softwareSkinUpdate(Mesh mesh, Matrix4f[] offsetMatrices) {
+
+ VertexBuffer tb = mesh.getBuffer(Type.Tangent);
+ if (tb == null) {
+ //if there are no tangents use the classic skinning
+ applySkinning(mesh, offsetMatrices);
+ } else {
+ //if there are tangents use the skinning with tangents
+ applySkinningTangents(mesh, offsetMatrices, tb);
+ }
+
+
}
/**
- * sets the target meshes of this control
- * @param targets
+ * Method to apply skinning transforms to a mesh's buffers
+ *
+ * @param mesh the mesh
+ * @param offsetMatrices the offset matices to apply
*/
-// public void setTargets(Mesh[] targets) {
-// this.targets = targets;
-// }
+ private void applySkinning(Mesh mesh, Matrix4f[] offsetMatrices) {
+ int maxWeightsPerVert = mesh.getMaxNumWeights();
+ if (maxWeightsPerVert <= 0) {
+ throw new IllegalStateException("Max weights per vert is incorrectly set!");
+ }
- private void softwareSkinUpdate(Mesh mesh, Matrix4f[] offsetMatrices) {
+ int fourMinusMaxWeights = 4 - maxWeightsPerVert;
+
+ // NOTE: This code assumes the vertex buffer is in bind pose
+ // resetToBind() has been called this frame
+ VertexBuffer vb = mesh.getBuffer(Type.Position);
+ FloatBuffer fvb = (FloatBuffer) vb.getData();
+ fvb.rewind();
+
+ VertexBuffer nb = mesh.getBuffer(Type.Normal);
+ FloatBuffer fnb = (FloatBuffer) nb.getData();
+ fnb.rewind();
+
+ // get boneIndexes and weights for mesh
+ ByteBuffer ib = (ByteBuffer) mesh.getBuffer(Type.BoneIndex).getData();
+ FloatBuffer wb = (FloatBuffer) mesh.getBuffer(Type.BoneWeight).getData();
+
+ ib.rewind();
+ wb.rewind();
+
+ float[] weights = wb.array();
+ byte[] indices = ib.array();
+ int idxWeights = 0;
+
+ TempVars vars = TempVars.get();
+
+ float[] posBuf = vars.skinPositions;
+ float[] normBuf = vars.skinNormals;
+
+ int iterations = (int) FastMath.ceil(fvb.limit() / ((float) posBuf.length));
+ int bufLength = posBuf.length;
+ for (int i = iterations - 1; i >= 0; i--) {
+ // read next set of positions and normals from native buffer
+ bufLength = Math.min(posBuf.length, fvb.remaining());
+ fvb.get(posBuf, 0, bufLength);
+ fnb.get(normBuf, 0, bufLength);
+ int verts = bufLength / 3;
+ int idxPositions = 0;
+
+ // iterate vertices and apply skinning transform for each effecting bone
+ for (int vert = verts - 1; vert >= 0; vert--) {
+ // Skip this vertex if the first weight is zero.
+ if (weights[idxWeights] == 0) {
+ idxPositions += 3;
+ idxWeights += 4;
+ continue;
+ }
+
+ float nmx = normBuf[idxPositions];
+ float vtx = posBuf[idxPositions++];
+ float nmy = normBuf[idxPositions];
+ float vty = posBuf[idxPositions++];
+ float nmz = normBuf[idxPositions];
+ float vtz = posBuf[idxPositions++];
+
+ float rx = 0, ry = 0, rz = 0, rnx = 0, rny = 0, rnz = 0;
+
+ for (int w = maxWeightsPerVert - 1; w >= 0; w--) {
+ float weight = weights[idxWeights];
+ Matrix4f mat = offsetMatrices[indices[idxWeights++] & 0xff];
+
+ rx += (mat.m00 * vtx + mat.m01 * vty + mat.m02 * vtz + mat.m03) * weight;
+ ry += (mat.m10 * vtx + mat.m11 * vty + mat.m12 * vtz + mat.m13) * weight;
+ rz += (mat.m20 * vtx + mat.m21 * vty + mat.m22 * vtz + mat.m23) * weight;
+
+ rnx += (nmx * mat.m00 + nmy * mat.m01 + nmz * mat.m02) * weight;
+ rny += (nmx * mat.m10 + nmy * mat.m11 + nmz * mat.m12) * weight;
+ rnz += (nmx * mat.m20 + nmy * mat.m21 + nmz * mat.m22) * weight;
+ }
+
+ idxWeights += fourMinusMaxWeights;
+
+ idxPositions -= 3;
+ normBuf[idxPositions] = rnx;
+ posBuf[idxPositions++] = rx;
+ normBuf[idxPositions] = rny;
+ posBuf[idxPositions++] = ry;
+ normBuf[idxPositions] = rnz;
+ posBuf[idxPositions++] = rz;
+ }
+
+ fvb.position(fvb.position() - bufLength);
+ fvb.put(posBuf, 0, bufLength);
+ fnb.position(fnb.position() - bufLength);
+ fnb.put(normBuf, 0, bufLength);
+ }
+
+ vars.release();
+
+ vb.updateData(fvb);
+ nb.updateData(fnb);
+
+ }
+
+ /**
+ * Specific method for skinning with tangents to avoid cluttering the
+ * classic skinning calculation with null checks that would slow down the
+ * process even if tangents don't have to be computed. Also the iteration
+ * has additional indexes since tangent has 4 components instead of 3 for
+ * pos and norm
+ *
+ * @param maxWeightsPerVert maximum number of weights per vertex
+ * @param mesh the mesh
+ * @param offsetMatrices the offsetMaytrices to apply
+ * @param tb the tangent vertexBuffer
+ */
+ private void applySkinningTangents(Mesh mesh, Matrix4f[] offsetMatrices, VertexBuffer tb) {
int maxWeightsPerVert = mesh.getMaxNumWeights();
+
if (maxWeightsPerVert <= 0) {
throw new IllegalStateException("Max weights per vert is incorrectly set!");
}
fvb.rewind();
VertexBuffer nb = mesh.getBuffer(Type.Normal);
+
FloatBuffer fnb = (FloatBuffer) nb.getData();
fnb.rewind();
+
+ FloatBuffer ftb = (FloatBuffer) tb.getData();
+ ftb.rewind();
+
+
// get boneIndexes and weights for mesh
ByteBuffer ib = (ByteBuffer) mesh.getBuffer(Type.BoneIndex).getData();
FloatBuffer wb = (FloatBuffer) mesh.getBuffer(Type.BoneWeight).getData();
float[] posBuf = vars.skinPositions;
float[] normBuf = vars.skinNormals;
+ float[] tanBuf = vars.skinTangents;
- int iterations = (int) FastMath.ceil(fvb.capacity() / ((float) posBuf.length));
- int bufLength = posBuf.length * 3;
+ int iterations = (int) FastMath.ceil(fvb.limit() / ((float) posBuf.length));
+ int bufLength = 0;
+ int tanLength = 0;
for (int i = iterations - 1; i >= 0; i--) {
// read next set of positions and normals from native buffer
bufLength = Math.min(posBuf.length, fvb.remaining());
+ tanLength = Math.min(tanBuf.length, ftb.remaining());
fvb.get(posBuf, 0, bufLength);
fnb.get(normBuf, 0, bufLength);
+ ftb.get(tanBuf, 0, tanLength);
int verts = bufLength / 3;
int idxPositions = 0;
+ //tangents has their own index because of the 4 components
+ int idxTangents = 0;
// iterate vertices and apply skinning transform for each effecting bone
for (int vert = verts - 1; vert >= 0; vert--) {
+ // Skip this vertex if the first weight is zero.
+ if (weights[idxWeights] == 0) {
+ idxTangents += 4;
+ idxPositions += 3;
+ idxWeights += 4;
+ continue;
+ }
+
float nmx = normBuf[idxPositions];
float vtx = posBuf[idxPositions++];
float nmy = normBuf[idxPositions];
float nmz = normBuf[idxPositions];
float vtz = posBuf[idxPositions++];
- float rx = 0, ry = 0, rz = 0, rnx = 0, rny = 0, rnz = 0;
+ float tnx = tanBuf[idxTangents++];
+ float tny = tanBuf[idxTangents++];
+ float tnz = tanBuf[idxTangents++];
+
+ // skipping the 4th component of the tangent since it doesn't have to be transformed
+ idxTangents++;
+
+ float rx = 0, ry = 0, rz = 0, rnx = 0, rny = 0, rnz = 0, rtx = 0, rty = 0, rtz = 0;
for (int w = maxWeightsPerVert - 1; w >= 0; w--) {
float weight = weights[idxWeights];
rnx += (nmx * mat.m00 + nmy * mat.m01 + nmz * mat.m02) * weight;
rny += (nmx * mat.m10 + nmy * mat.m11 + nmz * mat.m12) * weight;
rnz += (nmx * mat.m20 + nmy * mat.m21 + nmz * mat.m22) * weight;
+
+ rtx += (tnx * mat.m00 + tny * mat.m01 + tnz * mat.m02) * weight;
+ rty += (tnx * mat.m10 + tny * mat.m11 + tnz * mat.m12) * weight;
+ rtz += (tnx * mat.m20 + tny * mat.m21 + tnz * mat.m22) * weight;
}
idxWeights += fourMinusMaxWeights;
idxPositions -= 3;
+
normBuf[idxPositions] = rnx;
posBuf[idxPositions++] = rx;
normBuf[idxPositions] = rny;
posBuf[idxPositions++] = ry;
normBuf[idxPositions] = rnz;
posBuf[idxPositions++] = rz;
+
+ idxTangents -= 4;
+
+ tanBuf[idxTangents++] = rtx;
+ tanBuf[idxTangents++] = rty;
+ tanBuf[idxTangents++] = rtz;
+
+ //once again skipping the 4th component of the tangent
+ idxTangents++;
}
fvb.position(fvb.position() - bufLength);
fvb.put(posBuf, 0, bufLength);
fnb.position(fnb.position() - bufLength);
fnb.put(normBuf, 0, bufLength);
+ ftb.position(ftb.position() - tanLength);
+ ftb.put(tanBuf, 0, tanLength);
}
vars.release();
vb.updateData(fvb);
nb.updateData(fnb);
+ tb.updateData(ftb);
+
-// mesh.updateBound();
}
@Override
public void write(JmeExporter ex) throws IOException {
super.write(ex);
OutputCapsule oc = ex.getCapsule(this);
- oc.write(targets, "targets", null);
oc.write(skeleton, "skeleton", null);
+ //Targets and materials don't need to be saved, they'll be gathered on each frame
}
@Override
public void read(JmeImporter im) throws IOException {
super.read(im);
InputCapsule in = im.getCapsule(this);
- Savable[] sav = in.readSavableArray("targets", null);
- if (sav != null) {
- targets = new Mesh[sav.length];
- System.arraycopy(sav, 0, targets, 0, sav.length);
- }
skeleton = (Skeleton) in.readSavable("skeleton", null);
}
+
+ private void updateTargetsAndMaterials(Spatial spatial) {
+ targets.clear();
+ materials.clear();
+ if (spatial != null && spatial instanceof Node) {
+ Node node = (Node) spatial;
+ findTargets(node);
+ }
+ }
}
+/*\r
+ * Copyright (c) 2009-2012 jMonkeyEngine\r
+ * All rights reserved.\r
+ *\r
+ * Redistribution and use in source and binary forms, with or without\r
+ * modification, are permitted provided that the following conditions are\r
+ * met:\r
+ *\r
+ * * Redistributions of source code must retain the above copyright\r
+ * notice, this list of conditions and the following disclaimer.\r
+ *\r
+ * * Redistributions in binary form must reproduce the above copyright\r
+ * notice, this list of conditions and the following disclaimer in the\r
+ * documentation and/or other materials provided with the distribution.\r
+ *\r
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors\r
+ * may be used to endorse or promote products derived from this software\r
+ * without specific prior written permission.\r
+ *\r
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\r
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED\r
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\r
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR\r
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\r
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\r
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\r
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\r
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\r
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\r
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\r
+ */\r
package com.jme3.animation;\r
\r
/**\r
+/*\r
+ * Copyright (c) 2009-2012 jMonkeyEngine\r
+ * All rights reserved.\r
+ *\r
+ * Redistribution and use in source and binary forms, with or without\r
+ * modification, are permitted provided that the following conditions are\r
+ * met:\r
+ *\r
+ * * Redistributions of source code must retain the above copyright\r
+ * notice, this list of conditions and the following disclaimer.\r
+ *\r
+ * * Redistributions in binary form must reproduce the above copyright\r
+ * notice, this list of conditions and the following disclaimer in the\r
+ * documentation and/or other materials provided with the distribution.\r
+ *\r
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors\r
+ * may be used to endorse or promote products derived from this software\r
+ * without specific prior written permission.\r
+ *\r
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\r
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED\r
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\r
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR\r
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\r
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\r
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\r
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\r
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\r
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\r
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\r
+ */\r
package com.jme3.animation;\r
\r
-import java.io.IOException;\r
-\r
import com.jme3.export.InputCapsule;\r
import com.jme3.export.JmeExporter;\r
import com.jme3.export.JmeImporter;\r
import com.jme3.math.Vector3f;\r
import com.jme3.scene.Spatial;\r
import com.jme3.util.TempVars;\r
+import java.io.IOException;\r
+import java.util.Arrays;\r
\r
/**\r
* This class represents the track for spatial animation.\r
* \r
* @param time\r
* the current time of the animation\r
- * @param spatial\r
- * the spatial that should be animated with this track\r
*/\r
public void setTime(float time, float weight, AnimControl control, AnimChannel channel, TempVars vars) {\r
Spatial spatial = control.getSpatial();\r
\r
int lastFrame = times.length - 1;\r
if (time < 0 || lastFrame == 0) {\r
- rotations.get(0, tempQ);\r
- translations.get(0, tempV);\r
+ if (rotations != null)\r
+ rotations.get(0, tempQ);\r
+ if (translations != null)\r
+ translations.get(0, tempV);\r
if (scales != null) {\r
scales.get(0, tempS);\r
}\r
} else if (time >= times[lastFrame]) {\r
- rotations.get(lastFrame, tempQ);\r
- translations.get(lastFrame, tempV);\r
+ if (rotations != null)\r
+ rotations.get(lastFrame, tempQ);\r
+ if (translations != null)\r
+ translations.get(lastFrame, tempV);\r
if (scales != null) {\r
scales.get(lastFrame, tempS);\r
}\r
\r
float blend = (time - times[startFrame]) / (times[endFrame] - times[startFrame]);\r
\r
- rotations.get(startFrame, tempQ);\r
- translations.get(startFrame, tempV);\r
+ if (rotations != null)\r
+ rotations.get(startFrame, tempQ);\r
+ if (translations != null)\r
+ translations.get(startFrame, tempV);\r
if (scales != null) {\r
scales.get(startFrame, tempS);\r
}\r
- rotations.get(endFrame, tempQ2);\r
- translations.get(endFrame, tempV2);\r
+ if (rotations != null)\r
+ rotations.get(endFrame, tempQ2);\r
+ if (translations != null)\r
+ translations.get(endFrame, tempV2);\r
if (scales != null) {\r
scales.get(endFrame, tempS2);\r
}\r
tempS.interpolate(tempS2, blend);\r
}\r
\r
- spatial.setLocalTranslation(tempV);\r
- spatial.setLocalRotation(tempQ);\r
+ if (translations != null)\r
+ spatial.setLocalTranslation(tempV);\r
+ if (rotations != null)\r
+ spatial.setLocalRotation(tempQ);\r
if (scales != null) {\r
spatial.setLocalScale(tempS);\r
}\r
throw new RuntimeException("BoneTrack with no keyframes!");\r
}\r
\r
- assert times.length == translations.length\r
- && times.length == rotations.length;\r
-\r
this.times = times;\r
- this.translations = new CompactVector3Array();\r
- this.translations.add(translations);\r
- this.translations.freeze();\r
- this.rotations = new CompactQuaternionArray();\r
- this.rotations.add(rotations);\r
- this.rotations.freeze();\r
-\r
+ if (translations != null) {\r
+ assert times.length == translations.length;\r
+ this.translations = new CompactVector3Array();\r
+ this.translations.add(translations);\r
+ this.translations.freeze();\r
+ }\r
+ if (rotations != null) {\r
+ assert times.length == rotations.length;\r
+ this.rotations = new CompactQuaternionArray();\r
+ this.rotations.add(rotations);\r
+ this.rotations.freeze();\r
+ }\r
if (scales != null) {\r
assert times.length == scales.length;\r
- \r
this.scales = new CompactVector3Array();\r
this.scales.add(scales);\r
this.scales.freeze();\r
- \r
- \r
}\r
}\r
\r
* @return the array of rotations of this track\r
*/\r
public Quaternion[] getRotations() {\r
- return rotations.toObjectArray();\r
+ return rotations == null ? null : rotations.toObjectArray();\r
}\r
\r
/**\r
* @return the array of translations of this track\r
*/\r
public Vector3f[] getTranslations() {\r
- return translations.toObjectArray();\r
+ return translations == null ? null : translations.toObjectArray();\r
}\r
\r
/**\r
public SpatialTrack clone() {\r
int tablesLength = times.length;\r
\r
- float[] times = this.times.clone();\r
- Vector3f[] sourceTranslations = this.getTranslations();\r
- Quaternion[] sourceRotations = this.getRotations();\r
- Vector3f[] sourceScales = this.getScales();\r
-\r
- Vector3f[] translations = new Vector3f[tablesLength];\r
- Quaternion[] rotations = new Quaternion[tablesLength];\r
- Vector3f[] scales = new Vector3f[tablesLength];\r
- for (int i = 0; i < tablesLength; ++i) {\r
- translations[i] = sourceTranslations[i].clone();\r
- rotations[i] = sourceRotations[i].clone();\r
- scales[i] = sourceScales != null ? sourceScales[i].clone() : new Vector3f(1.0f, 1.0f, 1.0f);\r
- }\r
+ float[] timesCopy = this.times.clone();\r
+ Vector3f[] translationsCopy = this.getTranslations() == null ? null : Arrays.copyOf(this.getTranslations(), tablesLength);\r
+ Quaternion[] rotationsCopy = this.getRotations() == null ? null : Arrays.copyOf(this.getRotations(), tablesLength);\r
+ Vector3f[] scalesCopy = this.getScales() == null ? null : Arrays.copyOf(this.getScales(), tablesLength);\r
+\r
//need to use the constructor here because of the final fields used in this class\r
- return new SpatialTrack(times, translations, rotations, scales);\r
+ return new SpatialTrack(timesCopy, translationsCopy, rotationsCopy, scalesCopy);\r
}\r
\r
@Override\r
/*
- * Copyright (c) 2009-2010 jMonkeyEngine
+ * Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
--- /dev/null
+/*
+ * Copyright (c) 2009-2012 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.animation;
+
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.export.Savable;
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * This class is intended as a UserData added to a Spatial that is referenced by a Track.
+ * (ParticleEmitter for EffectTrack and AudioNode for AudioTrack)
+ * It holds the list of tracks that are directly referencing the Spatial.
+ *
+ * This is used when loading a Track to find the cloned reference of a Spatial in the cloned model returned by the assetManager.
+ *
+ * @author Nehon
+ */
+public class TrackInfo implements Savable {
+
+ ArrayList<Track> tracks = new ArrayList<Track>();
+
+ public TrackInfo() {
+ }
+
+ public void write(JmeExporter ex) throws IOException {
+ OutputCapsule c = ex.getCapsule(this);
+ c.writeSavableArrayList(tracks, "tracks", null);
+ }
+
+ public void read(JmeImporter im) throws IOException {
+ InputCapsule c = im.getCapsule(this);
+ tracks = c.readSavableArrayList("tracks", null);
+ }
+
+ public ArrayList<Track> getTracks() {
+ return tracks;
+ }
+
+ public void addTrack(Track track) {
+ tracks.add(track);
+ }
+}
All bones <em>must</em> have a bind pose transformation before they can be
animated. To set the bind pose for the skeleton, set the local (relative
to parent) transformations for all the bones using the call
-{@link com.jme3.animation.Bone#setBindTransforms(com.jme3.math.Vector3f, com.jme3.math.Quaternion) }.
+{@link com.jme3.animation.Bone#setBindTransforms(com.jme3.math.Vector3f, com.jme3.math.Quaternion, com.jme3.math.Vector3f) }.
Then call {@link com.jme3.animation.Skeleton#updateWorldVectors() } followed by
{@link com.jme3.animation.Skeleton#setBindingPose() }. <br>
<p>