Browse Source

Created an AnimationHelper to ease the creation process of a spatial animation, based on key frames interpolation.

git-svn-id: https://jmonkeyengine.googlecode.com/svn/trunk@8955 75d07b2b-3a1a-0410-a2c5-0572b91ccdca
rem..om 14 years ago
parent
commit
7de07d056c

+ 16 - 14
engine/src/core/com/jme3/animation/AnimControl.java

@@ -70,21 +70,17 @@ public final class AnimControl extends AbstractControl implements Cloneable {
      * Skeleton object must contain corresponding data for the targets' weight buffers.
      */
     Skeleton skeleton;
-
     /** only used for backward compatibility */
     @Deprecated
     private SkeletonControl skeletonControl;
-
     /**
      * List of animations
      */
     HashMap<String, Animation> animationMap;
-
     /**
      * Animation channels
      */
     private transient ArrayList<AnimChannel> channels = new ArrayList<AnimChannel>();
-
     /**
      * Animation event listeners
      */
@@ -117,14 +113,14 @@ public final class AnimControl extends AbstractControl implements Cloneable {
             clone.spatial = spatial;
             clone.channels = new ArrayList<AnimChannel>();
             clone.listeners = new ArrayList<AnimEventListener>();
-            
-            if (skeleton != null){
+
+            if (skeleton != null) {
                 clone.skeleton = new Skeleton(skeleton);
             }
-            
+
             // animationMap is reference-copied, animation data should be shared
             // to reduce memory usage.
-            
+
             return clone;
         } catch (CloneNotSupportedException ex) {
             throw new AssertionError();
@@ -147,6 +143,9 @@ public final class AnimControl extends AbstractControl implements Cloneable {
      * such named animation exists.
      */
     public Animation getAnim(String name) {
+        if (animationMap == null) {
+            animationMap = new HashMap<String, Animation>();
+        }
         return animationMap.get(name);
     }
 
@@ -156,6 +155,9 @@ public final class AnimControl extends AbstractControl implements Cloneable {
      * @param anim The animation to add.
      */
     public void addAnim(Animation anim) {
+        if (animationMap == null) {
+            animationMap = new HashMap<String, Animation>();
+        }
         animationMap.put(anim.getName(), anim);
     }
 
@@ -273,7 +275,7 @@ public final class AnimControl extends AbstractControl implements Cloneable {
      */
     @Override
     public void setSpatial(Spatial spatial) {
-        if (spatial == null && skeletonControl != null){
+        if (spatial == null && skeletonControl != null) {
             this.spatial.removeControl(skeletonControl);
         }
 
@@ -313,13 +315,13 @@ public final class AnimControl extends AbstractControl implements Cloneable {
 
         return a.getLength();
     }
-    
+
     /**
      * Internal use only.
      */
     @Override
     protected void controlUpdate(float tpf) {
-        if (skeleton != null){
+        if (skeleton != null) {
             skeleton.reset(); // reset skeleton to bind pose
         }
 
@@ -329,7 +331,7 @@ public final class AnimControl extends AbstractControl implements Cloneable {
         }
         vars.release();
 
-        if (skeleton != null){
+        if (skeleton != null) {
             skeleton.updateWorldVectors();
         }
     }
@@ -356,10 +358,10 @@ public final class AnimControl extends AbstractControl implements Cloneable {
         skeleton = (Skeleton) in.readSavable("skeleton", null);
         animationMap = (HashMap<String, Animation>) in.readStringSavableMap("animations", null);
 
-        if (im.getFormatVersion() == 0){
+        if (im.getFormatVersion() == 0) {
             // Changed for backward compatibility with j3o files generated 
             // before the AnimControl/SkeletonControl split.
-            
+
             // If we find a target mesh array the AnimControl creates the 
             // SkeletonControl for old files and add it to the spatial.        
             // When backward compatibility won't be needed anymore this can deleted        

+ 494 - 0
engine/src/core/com/jme3/animation/AnimationHelper.java

@@ -0,0 +1,494 @@
+/*
+ * Copyright (c) 2009-2011 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 AnimationHelper {
+
+    /**
+     * 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 AnimationHelper(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 AnimationHelper(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);
+        //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;
+    }
+}

+ 85 - 0
engine/src/test/jme3test/model/anim/TestAnimationHelper.java

@@ -0,0 +1,85 @@
+package jme3test.model.anim;
+
+import com.jme3.animation.AnimControl;
+import com.jme3.animation.AnimationHelper;
+import com.jme3.app.SimpleApplication;
+import com.jme3.light.AmbientLight;
+import com.jme3.light.DirectionalLight;
+import com.jme3.math.FastMath;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Node;
+import com.jme3.scene.shape.Box;
+import com.jme3.util.TangentBinormalGenerator;
+
+public class TestAnimationHelper extends SimpleApplication {
+
+    public static void main(String[] args) {
+        TestSpatialAnim app = new TestSpatialAnim();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+
+        AmbientLight al = new AmbientLight();
+        rootNode.addLight(al);
+
+        DirectionalLight dl = new DirectionalLight();
+        dl.setDirection(Vector3f.UNIT_XYZ.negate());
+        rootNode.addLight(dl);
+
+        // Create model
+        Box box = new Box(1, 1, 1);
+        Geometry geom = new Geometry("box", box);
+        geom.setMaterial(assetManager.loadMaterial("Textures/Terrain/BrickWall/BrickWall.j3m"));
+        Node model = new Node("model");
+        model.attachChild(geom);
+
+        Box child = new Box(0.5f, 0.5f, 0.5f);
+        Geometry childGeom = new Geometry("box", child);
+        childGeom.setMaterial(assetManager.loadMaterial("Textures/Terrain/BrickWall/BrickWall.j3m"));
+        Node childModel = new Node("childmodel");
+        childModel.setLocalTranslation(2, 2, 2);
+        childModel.attachChild(childGeom);
+        model.attachChild(childModel);
+        TangentBinormalGenerator.generate(model);
+
+        //creating quite complex animation witht the AnimationHelper
+        // animation of 6 seconds named "anim" and with 25 frames per second
+        AnimationHelper animationHelper = new AnimationHelper(6, "anim", 25);
+        
+        //creating a translation keyFrame at time = 3 with a translation on the x axis of 5 WU        
+        animationHelper.addTimeTranslation(3, new Vector3f(5, 0, 0));
+        //reseting the translation to the start position at time = 6
+        animationHelper.addTimeTranslation(6, new Vector3f(0, 0, 0));
+
+        //Creating a scale keyFrame at time = 2 with the unit scale.
+        animationHelper.addTimeScale(2, new Vector3f(1, 1, 1));
+        //Creating a scale keyFrame at time = 4 scaling to 1.5
+        animationHelper.addTimeScale(4, new Vector3f(1.5f, 1.5f, 1.5f));
+        //reseting the scale to the start value at time = 5
+        animationHelper.addTimeScale(5, new Vector3f(1, 1, 1));
+
+        
+        //Creating a rotation keyFrame at time = 0.5 of quarter PI around the Z axis
+        animationHelper.addTimeRotation(0.5f,new Quaternion().fromAngleAxis(FastMath.QUARTER_PI, Vector3f.UNIT_Z));
+        //rotating back to initial rotation value at time = 1
+        animationHelper.addTimeRotation(1,Quaternion.IDENTITY);
+        //Creating a rotation keyFrame at time = 2. Note that i used the Euler angle version because the angle is higher than PI
+        //this should result in a complete revolution of the spatial around the x axis in 1 second (from 1 to 2)
+        animationHelper.addTimeRotationAngles(2, FastMath.TWO_PI,0, 0);
+
+
+        AnimControl control = new AnimControl();
+        control.addAnim(animationHelper.buildAnimation());
+
+        model.addControl(control);
+
+        rootNode.attachChild(model);
+
+        //run animation
+        control.createChannel().setAnim("anim");
+    }
+}