diff --git a/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java b/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java index 1515136ef..1bdb41176 100644 --- a/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java +++ b/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) ic.readStringSavableMap("animClipMap", new HashMap()); - actions = (Map) ic.readStringSavableMap("actions", new HashMap()); } @Override @@ -163,6 +166,5 @@ public class AnimComposer extends AbstractControl { super.write(ex); OutputCapsule oc = ex.getCapsule(this); oc.writeStringSavableMap(animClipMap, "animClipMap", new HashMap()); - oc.writeStringSavableMap(actions, "actions", new HashMap()); } } diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/AbstractTween.java b/jme3-core/src/main/java/com/jme3/anim/tween/AbstractTween.java index aa3407268..272ec8dae 100644 --- a/jme3-core/src/main/java/com/jme3/anim/tween/AbstractTween.java +++ b/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); - } } - \ No newline at end of file diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/AnimClipTween.java b/jme3-core/src/main/java/com/jme3/anim/tween/AnimClipTween.java index 384abe31e..a36936244 100644 --- a/jme3-core/src/main/java/com/jme3/anim/tween/AnimClipTween.java +++ b/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; diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/Tween.java b/jme3-core/src/main/java/com/jme3/anim/tween/Tween.java index 450eb5f44..9ed4752c8 100644 --- a/jme3-core/src/main/java/com/jme3/anim/tween/Tween.java +++ b/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 diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/Tweens.java b/jme3-core/src/main/java/com/jme3/anim/tween/Tweens.java new file mode 100644 index 000000000..b369cadce --- /dev/null +++ b/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. + *

+ *

For example:

+ *
Tweens.callTweenMethod(1, myObject, "foo", "bar")
+ *

Would work for any of the following method signatures:

+ *
+     *    void foo( float t, String arg )
+     *    void foo( double t, String arg )
+     *    void foo( String arg, float t )
+     *    void foo( String arg, double t )
+     *  
+ */ + 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) + "]"; + } + } +} diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java index 9c7037715..ea283f8ec 100644 --- a/jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java +++ b/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); - } } diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendAction.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendAction.java new file mode 100644 index 000000000..eb6afe01f --- /dev/null +++ b/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; + } +} diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendSpace.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendSpace.java new file mode 100644 index 000000000..956e74c3b --- /dev/null +++ b/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); +} diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/LinearBlendSpace.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/LinearBlendSpace.java new file mode 100644 index 000000000..0c086d1e8 --- /dev/null +++ b/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; + } +} diff --git a/jme3-core/src/main/java/com/jme3/anim/util/Primitives.java b/jme3-core/src/main/java/com/jme3/anim/util/Primitives.java new file mode 100644 index 000000000..f61a996f8 --- /dev/null +++ b/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> PRIMITIVE_TO_WRAPPER_TYPE; + + static { + Map, 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. + *

+ *

+     *     wrap(int.class) == Integer.class
+     *     wrap(Integer.class) == Integer.class
+     *     wrap(String.class) == String.class
+     * 
+ */ + public static Class wrap(Class type) { + if (type == null) { + throw new IllegalArgumentException("type is null"); + } + + // cast is safe: long.class and Long.class are both of type Class + @SuppressWarnings("unchecked") + Class wrapped = (Class) PRIMITIVE_TO_WRAPPER_TYPE.get(type); + return (wrapped == null) ? type : wrapped; + } +} diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java index 6f6f0a4c9..a2c40dd60 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java +++ b/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 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;