/* * 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 return a 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 identity 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 angle 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; } }