Explorar o código

Added a BlendAction that allows to blend animations

Rémy Bouquet %!s(int64=7) %!d(string=hai) anos
pai
achega
2fc3bf5cfd

+ 6 - 4
jme3-core/src/main/java/com/jme3/anim/AnimComposer.java

@@ -3,6 +3,8 @@ package com.jme3.anim;
 import com.jme3.anim.tween.AnimClipTween;
 import com.jme3.anim.tween.Tween;
 import com.jme3.anim.tween.action.Action;
+import com.jme3.anim.tween.action.BlendAction;
+import com.jme3.anim.tween.action.BlendSpace;
 import com.jme3.anim.tween.action.SequenceAction;
 import com.jme3.export.*;
 import com.jme3.renderer.RenderManager;
@@ -92,8 +94,10 @@ public class AnimComposer extends AbstractControl {
         return action;
     }
 
-    public Action actionBlended(String name, Tween... tweens) {
-        return null;
+    public BlendAction actionBlended(String name, BlendSpace blendSpace, Tween... tweens) {
+        BlendAction action = new BlendAction(blendSpace, tweens);
+        actions.put(name, action);
+        return action;
     }
 
     public void reset() {
@@ -155,7 +159,6 @@ public class AnimComposer extends AbstractControl {
         super.read(im);
         InputCapsule ic = im.getCapsule(this);
         animClipMap = (Map<String, AnimClip>) ic.readStringSavableMap("animClipMap", new HashMap<String, AnimClip>());
-        actions = (Map<String, Action>) ic.readStringSavableMap("actions", new HashMap<String, Action>());
     }
 
     @Override
@@ -163,6 +166,5 @@ public class AnimComposer extends AbstractControl {
         super.write(ex);
         OutputCapsule oc = ex.getCapsule(this);
         oc.writeStringSavableMap(animClipMap, "animClipMap", new HashMap<String, AnimClip>());
-        oc.writeStringSavableMap(actions, "actions", new HashMap<String, Action>());
     }
 }

+ 0 - 13
jme3-core/src/main/java/com/jme3/anim/tween/AbstractTween.java

@@ -94,17 +94,4 @@ public abstract class AbstractTween implements Tween {
     }
 
     protected abstract void doInterpolate(double t);
-
-    @Override
-    public void write(JmeExporter ex) throws IOException {
-        OutputCapsule oc = ex.getCapsule(this);
-        oc.write(length, "length", 0);
-    }
-
-    @Override
-    public void read(JmeImporter im) throws IOException {
-        InputCapsule ic = im.getCapsule(this);
-        length = ic.readDouble("length", 0);
-    }
 }
- 

+ 4 - 16
jme3-core/src/main/java/com/jme3/anim/tween/AnimClipTween.java

@@ -43,6 +43,10 @@ public class AnimClipTween implements Tween, Weighted, JmeCloneable {
         if (parentAction != null) {
             weight = parentAction.getWeightForTween(this);
         }
+        if (weight == 0) {
+            //weight is 0 let's not interpolate
+            return t < clip.getLength();
+        }
         TransformTrack[] tracks = clip.getTracks();
         for (TransformTrack track : tracks) {
             HasLocalTransform target = track.getTarget();
@@ -60,17 +64,6 @@ public class AnimClipTween implements Tween, Weighted, JmeCloneable {
         return t < clip.getLength();
     }
 
-    @Override
-    public void write(JmeExporter ex) throws IOException {
-        OutputCapsule oc = ex.getCapsule(this);
-        oc.write(clip, "clip", null);
-    }
-
-    @Override
-    public void read(JmeImporter im) throws IOException {
-        InputCapsule ic = im.getCapsule(this);
-        clip = (AnimClip) ic.readSavable("clip", null);
-    }
 
     @Override
     public Object jmeClone() {
@@ -87,11 +80,6 @@ public class AnimClipTween implements Tween, Weighted, JmeCloneable {
         clip = cloner.clone(clip);
     }
 
-//    @Override
-//    public void setWeight(float weight) {
-//        this.weight = weight;
-//    }
-
     @Override
     public void setParentAction(Action action) {
         this.parentAction = action;

+ 1 - 1
jme3-core/src/main/java/com/jme3/anim/tween/Tween.java

@@ -46,7 +46,7 @@ import com.jme3.export.Savable;
  *
  * @author Paul Speed
  */
-public interface Tween extends Savable, Cloneable {
+public interface Tween extends Cloneable {
 
     /**
      * Returns the length of the tween.  If 't' represents time in

+ 603 - 0
jme3-core/src/main/java/com/jme3/anim/tween/Tweens.java

@@ -0,0 +1,603 @@
+/*
+ * $Id$
+ * 
+ * Copyright (c) 2015, Simsilica, LLC
+ * All rights reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions 
+ * are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright 
+ *    notice, this list of conditions and the following disclaimer.
+ * 
+ * 2. 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.
+ * 
+ * 3. Neither the name of the copyright holder 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 HOLDER 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.anim.tween;
+
+import com.jme3.anim.util.Primitives;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Static utility methods for creating common generic Tween objects.
+ *
+ * @author Paul Speed
+ */
+public class Tweens {
+
+    static Logger log = Logger.getLogger(Tweens.class.getName());
+
+    private static final CurveFunction SMOOTH = new SmoothStep();
+    private static final CurveFunction SINE = new Sine();
+
+    /**
+     * Creates a tween that will interpolate over an entire sequence
+     * of tweens in order.
+     */
+    public static Tween sequence(Tween... delegates) {
+        return new Sequence(delegates);
+    }
+
+    /**
+     * Creates a tween that will interpolate over an entire list
+     * of tweens in parallel, ie: all tweens will be run at the same
+     * time.
+     */
+    public static Tween parallel(Tween... delegates) {
+        return new Parallel(delegates);
+    }
+
+    /**
+     * Creates a tween that will perform a no-op until the length
+     * has expired.
+     */
+    public static Tween delay(double length) {
+        return new Delay(length);
+    }
+
+    /**
+     * Creates a tween that scales the specified delegate tween or tweens
+     * to the desired length.  If more than one tween is specified then they
+     * are wrapped in a sequence using the sequence() method.
+     */
+    public static Tween stretch(double desiredLength, Tween... delegates) {
+        if (delegates.length == 1) {
+            return new Stretch(delegates[0], desiredLength);
+        }
+        return new Stretch(sequence(delegates), desiredLength);
+    }
+
+    /**
+     * Creates a tween that uses a sine function to smooth step the time value
+     * for the specified delegate tween or tweens.  These 'curved' wrappers
+     * can be used to smooth the interpolation of another tween.
+     */
+    public static Tween sineStep(Tween... delegates) {
+        if (delegates.length == 1) {
+            return new Curve(delegates[0], SINE);
+        }
+        return new Curve(sequence(delegates), SINE);
+    }
+
+    /**
+     * Creates a tween that uses a hermite function to smooth step the time value
+     * for the specified delegate tween or tweens.  This is similar to GLSL's
+     * smoothstep().  These 'curved' wrappers can be used to smooth the interpolation
+     * of another tween.
+     */
+    public static Tween smoothStep(Tween... delegates) {
+        if (delegates.length == 1) {
+            return new Curve(delegates[0], SMOOTH);
+        }
+        return new Curve(sequence(delegates), SMOOTH);
+    }
+
+    /**
+     * Creates a Tween that will call the specified method and optional arguments
+     * whenever supplied a time value greater than or equal to 0.  This creates
+     * an "instant" tween of length 0.
+     */
+    public static Tween callMethod(Object target, String method, Object... args) {
+        return new CallMethod(target, method, args);
+    }
+
+    /**
+     * Creates a Tween that will call the specified method and optional arguments,
+     * including the time value scaled between 0 and 1.  The method must take
+     * a float or double value as its first or last argument, in addition to whatever
+     * optional arguments are specified.
+     * <p>
+     * <p>For example:</p>
+     * <pre>Tweens.callTweenMethod(1, myObject, "foo", "bar")</pre>
+     * <p>Would work for any of the following method signatures:</p>
+     * <pre>
+     *    void foo( float t, String arg )
+     *    void foo( double t, String arg )
+     *    void foo( String arg, float t )
+     *    void foo( String arg, double t )
+     *  </pre>
+     */
+    public static Tween callTweenMethod(double length, Object target, String method, Object... args) {
+        return new CallTweenMethod(length, target, method, args);
+    }
+
+    private static interface CurveFunction {
+        public double curve(double input);
+    }
+
+    /**
+     * Curve function for Hermite interpolation ala GLSL smoothstep().
+     */
+    private static class SmoothStep implements CurveFunction {
+
+        @Override
+        public double curve(double t) {
+            if (t < 0) {
+                return 0;
+            } else if (t > 1) {
+                return 1;
+            }
+            return t * t * (3 - 2 * t);
+        }
+    }
+
+    private static class Sine implements CurveFunction {
+
+        @Override
+        public double curve(double t) {
+            if (t < 0) {
+                return 0;
+            } else if (t > 1) {
+                return 1;
+            }
+            // Sine starting at -90 will go from -1 to 1 through 0 
+            double result = Math.sin(t * Math.PI - Math.PI * 0.5);
+            return (result + 1) * 0.5;
+        }
+    }
+
+    private static class Curve implements Tween {
+        private final Tween delegate;
+        private final CurveFunction func;
+        private final double length;
+
+        public Curve(Tween delegate, CurveFunction func) {
+            this.delegate = delegate;
+            this.func = func;
+            this.length = delegate.getLength();
+        }
+
+        @Override
+        public double getLength() {
+            return length;
+        }
+
+        @Override
+        public boolean interpolate(double t) {
+            // Sanity check the inputs
+            if (t < 0) {
+                return true;
+            }
+
+            if (length == 0) {
+                // Caller did something strange but we'll allow it
+                return delegate.interpolate(t);
+            }
+
+            t = func.curve(t / length);
+            return delegate.interpolate(t * length);
+        }
+
+        @Override
+        public String toString() {
+            return getClass().getSimpleName() + "[delegate=" + delegate + ", func=" + func + "]";
+        }
+    }
+
+    private static class Sequence implements Tween {
+        private final Tween[] delegates;
+        private int current = 0;
+        private double baseTime;
+        private double length;
+
+        public Sequence(Tween... delegates) {
+            this.delegates = delegates;
+            for (Tween t : delegates) {
+                length += t.getLength();
+            }
+        }
+
+        @Override
+        public double getLength() {
+            return length;
+        }
+
+        @Override
+        public boolean interpolate(double t) {
+
+            // Sanity check the inputs
+            if (t < 0) {
+                return true;
+            }
+
+            if (t < baseTime) {
+                // We've rolled back before the current sequence step
+                // which means we need to reset and start forward
+                // again.  We have no idea how to 'roll back' and
+                // this is the only way to maintain consistency.
+                // The only 'normal' case where this happens is when looping
+                // in which case a full rollback is appropriate.
+                current = 0;
+                baseTime = 0;
+            }
+
+            if (current >= delegates.length) {
+                return false;
+            }
+
+            // Skip any that are done
+            while (!delegates[current].interpolate(t - baseTime)) {
+                // Time to go to the next one
+                baseTime += delegates[current].getLength();
+                current++;
+                if (current >= delegates.length) {
+                    return false;
+                }
+            }
+
+            return true;
+        }
+
+        @Override
+        public String toString() {
+            return getClass().getSimpleName() + "[delegates=" + Arrays.asList(delegates) + "]";
+        }
+    }
+
+    private static class Parallel implements Tween {
+        private final Tween[] delegates;
+        private final boolean[] done;
+        private double length;
+        private double lastTime;
+
+        public Parallel(Tween... delegates) {
+            this.delegates = delegates;
+            done = new boolean[delegates.length];
+
+            for (Tween t : delegates) {
+                if (t.getLength() > length) {
+                    length = t.getLength();
+                }
+            }
+        }
+
+        @Override
+        public double getLength() {
+            return length;
+        }
+
+        protected void reset() {
+            for (int i = 0; i < done.length; i++) {
+                done[i] = false;
+            }
+        }
+
+        @Override
+        public boolean interpolate(double t) {
+            // Sanity check the inputs
+            if (t < 0) {
+                return true;
+            }
+
+            if (t < lastTime) {
+                // We've rolled back before the last time we were given.
+                // This means we may have 'done'ed a few tasks that now
+                // need to be run again.  Better to just reset and start
+                // over.  As mentioned in the Sequence task, the only 'normal'
+                // use-case for time rolling backwards is when looping.  And
+                // in that case, we want to start from the beginning anyway.
+                reset();
+            }
+            lastTime = t;
+
+            int runningCount = delegates.length;
+            for (int i = 0; i < delegates.length; i++) {
+                if (!done[i]) {
+                    done[i] = !delegates[i].interpolate(t);
+                }
+                if (done[i]) {
+                    runningCount--;
+                }
+            }
+            return runningCount > 0;
+        }
+
+        @Override
+        public String toString() {
+            return getClass().getSimpleName() + "[delegates=" + Arrays.asList(delegates) + "]";
+        }
+    }
+
+    private static class Delay extends AbstractTween {
+
+        public Delay(double length) {
+            super(length);
+        }
+
+        @Override
+        protected void doInterpolate(double t) {
+        }
+    }
+
+    private static class Stretch implements Tween {
+
+        private final Tween delegate;
+        private final double length;
+        private final double scale;
+
+        public Stretch(Tween delegate, double length) {
+            this.delegate = delegate;
+            this.length = length;
+
+            // Caller desires delegate to be 'length' instead of
+            // it's actual length so we will calculate a time scale
+            // If the desired length is longer than delegate's then
+            // we need to feed time in slower, ie: scale < 1
+            if (length != 0) {
+                this.scale = delegate.getLength() / length;
+            } else {
+                this.scale = 0;
+            }
+        }
+
+        @Override
+        public double getLength() {
+            return length;
+        }
+
+        @Override
+        public boolean interpolate(double t) {
+            if (t < 0) {
+                return true;
+            }
+            if (length > 0) {
+                t *= scale;
+            } else {
+                t = length;
+            }
+            return delegate.interpolate(t);
+        }
+
+        @Override
+        public String toString() {
+            return getClass().getSimpleName() + "[delegate=" + delegate + ", length=" + length + "]";
+        }
+    }
+
+    private static class CallMethod extends AbstractTween {
+
+        private Object target;
+        private Method method;
+        private Object[] args;
+
+        public CallMethod(Object target, String methodName, Object... args) {
+            super(0);
+            if (target == null) {
+                throw new IllegalArgumentException("Target cannot be null.");
+            }
+            this.target = target;
+            this.args = args;
+
+            // Lookup the method
+            if (args == null) {
+                this.method = findMethod(target.getClass(), methodName);
+            } else {
+                this.method = findMethod(target.getClass(), methodName, args);
+            }
+            if (this.method == null) {
+                throw new IllegalArgumentException("Method not found for:" + methodName + " on type:" + target.getClass());
+            }
+            this.method.setAccessible(true);
+        }
+
+        private static Method findMethod(Class type, String name, Object... args) {
+            for (Method m : type.getDeclaredMethods()) {
+                if (!Objects.equals(m.getName(), name)) {
+                    continue;
+                }
+                Class[] paramTypes = m.getParameterTypes();
+                if (paramTypes.length != args.length) {
+                    continue;
+                }
+                int matches = 0;
+                for (int i = 0; i < args.length; i++) {
+                    if (paramTypes[i].isInstance(args[i])
+                            || Primitives.wrap(paramTypes[i]).isInstance(args[i])) {
+                        matches++;
+                    }
+                }
+                if (matches == args.length) {
+                    return m;
+                }
+            }
+            if (type.getSuperclass() != null) {
+                return findMethod(type.getSuperclass(), name, args);
+            }
+            return null;
+        }
+
+        @Override
+        protected void doInterpolate(double t) {
+            try {
+                method.invoke(target, args);
+            } catch (IllegalAccessException e) {
+                throw new RuntimeException("Error running method:" + method + " for object:" + target, e);
+            } catch (InvocationTargetException e) {
+                throw new RuntimeException("Error running method:" + method + " for object:" + target, e);
+            }
+        }
+
+        @Override
+        public String toString() {
+            return getClass().getSimpleName() + "[method=" + method + ", parms=" + Arrays.asList(args) + "]";
+        }
+    }
+
+    private static class CallTweenMethod extends AbstractTween {
+
+        private Object target;
+        private Method method;
+        private Object[] args;
+        private int tIndex = -1;
+        private boolean isFloat = false;
+
+        public CallTweenMethod(double length, Object target, String methodName, Object... args) {
+            super(length);
+            if (target == null) {
+                throw new IllegalArgumentException("Target cannot be null.");
+            }
+            this.target = target;
+
+            // Lookup the method
+            this.method = findMethod(target.getClass(), methodName, args);
+            if (this.method == null) {
+                throw new IllegalArgumentException("Method not found for:" + methodName + " on type:" + target.getClass());
+            }
+            this.method.setAccessible(true);
+
+            // So now setup the real args list
+            this.args = new Object[args.length + 1];
+            if (tIndex == 0) {
+                for (int i = 0; i < args.length; i++) {
+                    this.args[i + 1] = args[i];
+                }
+            } else {
+                for (int i = 0; i < args.length; i++) {
+                    this.args[i] = args[i];
+                }
+            }
+        }
+
+        private static boolean isFloatType(Class type) {
+            return type == Float.TYPE || type == Float.class;
+        }
+
+        private static boolean isDoubleType(Class type) {
+            return type == Double.TYPE || type == Double.class;
+        }
+
+        private Method findMethod(Class type, String name, Object... args) {
+            for (Method m : type.getDeclaredMethods()) {
+                if (!Objects.equals(m.getName(), name)) {
+                    continue;
+                }
+                Class[] paramTypes = m.getParameterTypes();
+                if (paramTypes.length != args.length + 1) {
+                    if (log.isLoggable(Level.FINE)) {
+                        log.log(Level.FINE, "Param lengths of [" + m + "] differ.  method arg count:" + paramTypes.length + "  lookging for:" + (args.length + 1));
+                    }
+                    continue;
+                }
+
+                // We accept the 't' parameter as either first or last
+                // so we'll see which one matches.
+                if (isFloatType(paramTypes[0]) || isDoubleType(paramTypes[0])) {
+                    // Try it as the first parameter 
+                    int matches = 0;
+
+                    for (int i = 1; i < paramTypes.length; i++) {
+                        if (paramTypes[i].isInstance(args[i - 1])) {
+                            matches++;
+                        }
+                    }
+                    if (matches == args.length) {
+                        // Then this is our method and this is how we are configured
+                        tIndex = 0;
+                        isFloat = isFloatType(paramTypes[0]);
+                    } else {
+                        if (log.isLoggable(Level.FINE)) {
+                            log.log(Level.FINE, m + " Leading float check failed because of type mismatches, for:" + m);
+                        }
+                    }
+                }
+                if (tIndex >= 0) {
+                    return m;
+                }
+
+                // Else try it at the end                
+                int last = paramTypes.length - 1;
+                if (isFloatType(paramTypes[last]) || isDoubleType(paramTypes[last])) {
+                    int matches = 0;
+
+                    for (int i = 0; i < last; i++) {
+                        if (paramTypes[i].isInstance(args[i])) {
+                            matches++;
+                        }
+                    }
+                    if (matches == args.length) {
+                        // Then this is our method and this is how we are configured
+                        tIndex = last;
+                        isFloat = isFloatType(paramTypes[last]);
+                        return m;
+                    } else {
+                        if (log.isLoggable(Level.FINE)) {
+                            log.log(Level.FINE, "Trailing float check failed because of type mismatches, for:" + m);
+                        }
+                    }
+                }
+            }
+            if (type.getSuperclass() != null) {
+                return findMethod(type.getSuperclass(), name, args);
+            }
+            return null;
+        }
+
+        @Override
+        protected void doInterpolate(double t) {
+            try {
+                if (isFloat) {
+                    args[tIndex] = (float) t;
+                } else {
+                    args[tIndex] = t;
+                }
+                method.invoke(target, args);
+            } catch (IllegalAccessException e) {
+                throw new RuntimeException("Error running method:" + method + " for object:" + target, e);
+            } catch (InvocationTargetException e) {
+                throw new RuntimeException("Error running method:" + method + " for object:" + target, e);
+            }
+        }
+
+        @Override
+        public String toString() {
+            return getClass().getSimpleName() + "[method=" + method + ", parms=" + Arrays.asList(args) + "]";
+        }
+    }
+}

+ 1 - 16
jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java

@@ -2,10 +2,7 @@ package com.jme3.anim.tween.action;
 
 import com.jme3.anim.tween.Tween;
 import com.jme3.anim.util.Weighted;
-import com.jme3.export.InputCapsule;
-import com.jme3.export.JmeExporter;
-import com.jme3.export.JmeImporter;
-import com.jme3.export.OutputCapsule;
+import com.jme3.export.*;
 
 import java.io.IOException;
 
@@ -49,16 +46,4 @@ public abstract class Action implements Tween, Weighted {
     public void setParentAction(Action parentAction) {
         this.parentAction = parentAction;
     }
-
-    @Override
-    public void read(JmeImporter im) throws IOException {
-        InputCapsule ic = im.getCapsule(this);
-        tweens = (Tween[]) ic.readSavableArray("tweens", null);
-    }
-
-    @Override
-    public void write(JmeExporter ex) throws IOException {
-        OutputCapsule oc = ex.getCapsule(this);
-        oc.write(tweens, "tweens", null);
-    }
 }

+ 80 - 0
jme3-core/src/main/java/com/jme3/anim/tween/action/BlendAction.java

@@ -0,0 +1,80 @@
+package com.jme3.anim.tween.action;
+
+import com.jme3.anim.tween.Tween;
+import com.jme3.anim.tween.Tweens;
+
+public class BlendAction extends Action {
+
+
+    private Tween firstActiveTween;
+    private Tween secondActiveTween;
+    private BlendSpace blendSpace;
+    private float blendWeight;
+
+    public BlendAction(BlendSpace blendSpace, Tween... tweens) {
+        super(tweens);
+        this.blendSpace = blendSpace;
+        blendSpace.setBlendAction(this);
+
+        for (Tween tween : tweens) {
+            if (tween.getLength() > length) {
+                length = tween.getLength();
+            }
+        }
+
+        //Blending effect maybe unexpected when blended animation don't have the same length
+        //Stretching any tween that doesn't have the same length.
+        for (int i = 0; i < tweens.length; i++) {
+            if (tweens[i].getLength() != length) {
+                tweens[i] = Tweens.stretch(length, tweens[i]);
+            }
+        }
+
+    }
+
+    @Override
+    public float getWeightForTween(Tween tween) {
+        blendWeight = blendSpace.getWeight();
+        if (tween == firstActiveTween) {
+            return 1f;
+        }
+        return weight * blendWeight;
+    }
+
+    @Override
+    public boolean doInterpolate(double t) {
+        if (firstActiveTween == null) {
+            blendSpace.getWeight();
+        }
+
+        boolean running = this.firstActiveTween.interpolate(t);
+        this.secondActiveTween.interpolate(t);
+
+        if (!running) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public void reset() {
+
+    }
+
+    protected Tween[] getTweens() {
+        return tweens;
+    }
+
+    public BlendSpace getBlendSpace() {
+        return blendSpace;
+    }
+
+    protected void setFirstActiveTween(Tween firstActiveTween) {
+        this.firstActiveTween = firstActiveTween;
+    }
+
+    protected void setSecondActiveTween(Tween secondActiveTween) {
+        this.secondActiveTween = secondActiveTween;
+    }
+}

+ 12 - 0
jme3-core/src/main/java/com/jme3/anim/tween/action/BlendSpace.java

@@ -0,0 +1,12 @@
+package com.jme3.anim.tween.action;
+
+import com.jme3.anim.tween.action.BlendAction;
+
+public interface BlendSpace {
+
+    public void setBlendAction(BlendAction action);
+
+    public float getWeight();
+
+    public void setValue(float value);
+}

+ 50 - 0
jme3-core/src/main/java/com/jme3/anim/tween/action/LinearBlendSpace.java

@@ -0,0 +1,50 @@
+package com.jme3.anim.tween.action;
+
+import com.jme3.anim.tween.Tween;
+
+public class LinearBlendSpace implements BlendSpace {
+
+    private BlendAction action;
+    private float value;
+    private float maxValue;
+    private float step;
+
+    public LinearBlendSpace(float maxValue) {
+        this.maxValue = maxValue;
+
+    }
+
+    @Override
+    public void setBlendAction(BlendAction action) {
+        this.action = action;
+        Tween[] tweens = action.getTweens();
+        step = maxValue / (float) (tweens.length - 1);
+    }
+
+    @Override
+    public float getWeight() {
+        Tween[] tweens = action.getTweens();
+        float lowStep = 0, highStep = 0;
+        int lowIndex = 0, highIndex = 0;
+        for (int i = 0; i < tweens.length && highStep < value; i++) {
+            lowStep = highStep;
+            lowIndex = i;
+            highStep += step;
+        }
+        highIndex = lowIndex + 1;
+
+        action.setFirstActiveTween(tweens[lowIndex]);
+        action.setSecondActiveTween(tweens[highIndex]);
+
+        if (highStep == lowStep) {
+            return 0;
+        }
+
+        return (value - lowStep) / (highStep - lowStep);
+    }
+
+    @Override
+    public void setValue(float value) {
+        this.value = value;
+    }
+}

+ 56 - 0
jme3-core/src/main/java/com/jme3/anim/util/Primitives.java

@@ -0,0 +1,56 @@
+package com.jme3.anim.util;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * This is a guava method used in {@link com.jme3.anim.tween.Tweens} class.
+ * Maybe we should just add guava as a dependency in the engine...
+ * //TODO do something about this.
+ */
+public class Primitives {
+
+    /**
+     * A map from primitive types to their corresponding wrapper types.
+     */
+    private static final Map<Class<?>, Class<?>> PRIMITIVE_TO_WRAPPER_TYPE;
+
+    static {
+        Map<Class<?>, Class<?>> primToWrap = new HashMap<>(16);
+
+        primToWrap.put(boolean.class, Boolean.class);
+        primToWrap.put(byte.class, Byte.class);
+        primToWrap.put(char.class, Character.class);
+        primToWrap.put(double.class, Double.class);
+        primToWrap.put(float.class, Float.class);
+        primToWrap.put(int.class, Integer.class);
+        primToWrap.put(long.class, Long.class);
+        primToWrap.put(short.class, Short.class);
+        primToWrap.put(void.class, Void.class);
+
+        PRIMITIVE_TO_WRAPPER_TYPE = Collections.unmodifiableMap(primToWrap);
+    }
+
+    /**
+     * Returns the corresponding wrapper type of {@code type} if it is a primitive type; otherwise
+     * returns {@code type} itself. Idempotent.
+     * <p>
+     * <pre>
+     *     wrap(int.class) == Integer.class
+     *     wrap(Integer.class) == Integer.class
+     *     wrap(String.class) == String.class
+     * </pre>
+     */
+    public static <T> Class<T> wrap(Class<T> type) {
+        if (type == null) {
+            throw new IllegalArgumentException("type is null");
+        }
+
+        // cast is safe: long.class and Long.class are both of type Class<Long>
+        @SuppressWarnings("unchecked")
+        Class<T> wrapped = (Class<T>) PRIMITIVE_TO_WRAPPER_TYPE.get(type);
+        return (wrapped == null) ? type : wrapped;
+    }
+}

+ 32 - 0
jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java

@@ -2,11 +2,14 @@ package jme3test.model.anim;
 
 import com.jme3.anim.AnimComposer;
 import com.jme3.anim.SkinningControl;
+import com.jme3.anim.tween.action.BlendAction;
+import com.jme3.anim.tween.action.LinearBlendSpace;
 import com.jme3.anim.util.AnimMigrationUtils;
 import com.jme3.app.ChaseCameraAppState;
 import com.jme3.app.SimpleApplication;
 import com.jme3.input.KeyInput;
 import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.AnalogListener;
 import com.jme3.input.controls.KeyTrigger;
 import com.jme3.light.AmbientLight;
 import com.jme3.light.DirectionalLight;
@@ -27,6 +30,8 @@ public class TestAnimMigration extends SimpleApplication {
     AnimComposer composer;
     LinkedList<String> anims = new LinkedList<>();
     boolean playAnim = false;
+    BlendAction action;
+    float blendValue = 2f;
 
     public static void main(String... argv) {
         TestAnimMigration app = new TestAnimMigration();
@@ -117,6 +122,26 @@ public class TestAnimMigration extends SimpleApplication {
                 }
             }
         }, "toggleArmature");
+
+        inputManager.addMapping("blendUp", new KeyTrigger(KeyInput.KEY_UP));
+        inputManager.addMapping("blendDown", new KeyTrigger(KeyInput.KEY_DOWN));
+        inputManager.addListener(new AnalogListener() {
+
+            @Override
+            public void onAnalog(String name, float value, float tpf) {
+                if (name.equals("blendUp")) {
+                    blendValue += value;
+                    blendValue = FastMath.clamp(blendValue, 0, 4);
+                    action.getBlendSpace().setValue(blendValue);
+                }
+                if (name.equals("blendDown")) {
+                    blendValue -= value;
+                    blendValue = FastMath.clamp(blendValue, 0, 4);
+                    action.getBlendSpace().setValue(blendValue);
+                }
+                System.err.println(blendValue);
+            }
+        }, "blendUp", "blendDown");
     }
 
     private void setupModel(Spatial model) {
@@ -138,6 +163,12 @@ public class TestAnimMigration extends SimpleApplication {
                     composer.tweenFromClip("Run"),
                     composer.tweenFromClip("Jumping"));
 
+            action = composer.actionBlended("Blend", new LinearBlendSpace(4),
+                    composer.tweenFromClip("Walk"),
+                    composer.tweenFromClip("Jumping"));
+
+            action.getBlendSpace().setValue(2);
+
 //            composer.actionSequence("Sequence",
 //                    composer.tweenFromClip("Walk"),
 //                    composer.tweenFromClip("Dodge"),
@@ -145,6 +176,7 @@ public class TestAnimMigration extends SimpleApplication {
 
 
             anims.addFirst("Sequence");
+            anims.addFirst("Blend");
 
             if (anims.isEmpty()) {
                 return;