All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.jme3.anim.AnimFactory Maven / Gradle / Ivy

There is a newer version: 3.7.0-stable
Show newest version
/*
 * Copyright (c) 2009-2021 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.anim;

import com.jme3.anim.util.HasLocalTransform;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Transform;
import com.jme3.math.Vector3f;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

/**
 * A convenience class to smoothly animate a Spatial using translation,
 * rotation, and scaling.
 *
 * Add keyframes for translation, rotation, and scaling. Invoking
 * {@link #buildAnimation(com.jme3.anim.util.HasLocalTransform)} will then
 * generate an AnimClip that interpolates among the keyframes.
 *
 * By default, the first keyframe (index=0) has an identity Transform. You can
 * override this by replacing the first keyframe with different Transform.
 *
 * For a loop animation, make sure the final transform matches the starting one.
 * Because of a heuristic used by
 * {@link com.jme3.math.Quaternion#slerp(com.jme3.math.Quaternion, com.jme3.math.Quaternion, float)},
 * it's possible for
 * {@link #buildAnimation(com.jme3.anim.util.HasLocalTransform)} to negate the
 * final rotation. To prevent an unwanted rotation at the end of the loop, you
 * may need to add intermediate rotation keyframes.
 *
 * Inspired by Nehon's {@link com.jme3.animation.AnimationFactory}.
 */
public class AnimFactory {

    /**
     * clip duration (in seconds)
     */
    final private float duration;
    /**
     * frame/sample rate for the clip (in frames per second)
     */
    final private float fps;
    /**
     * rotations that define the clip
     */
    final private Map rotations = new TreeMap<>();
    /**
     * scales that define the clip
     */
    final private Map scales = new TreeMap<>();
    /**
     * translations that define the clip
     */
    final private Map translations = new TreeMap<>();
    /**
     * name for the resulting clip
     */
    final private String name;

    /**
     * Instantiate an AnimFactory with an identity transform at t=0.
     *
     * @param duration the duration for the clip (in seconds, >0)
     * @param name the name for the resulting clip
     * @param fps the frame rate for the clip (in frames per second, >0)
     */
    public AnimFactory(float duration, String name, float fps) {
        if (!(duration > 0f)) {
            throw new IllegalArgumentException("duration must be positive");
        }
        if (!(fps > 0f)) {
            throw new IllegalArgumentException("FPS must be positive");
        }

        this.name = name;
        this.duration = duration;
        this.fps = fps;
        /*
         * Add the initial Transform.
         */
        Transform transform = new Transform();
        translations.put(0f, transform.getTranslation());
        rotations.put(0f, transform.getRotation());
        scales.put(0f, transform.getScale());
    }

    /**
     * Add a keyframe for the specified rotation at the specified index.
     *
     * @param keyFrameIndex the keyframe in which full rotation should be
     * achieved (≥0)
     * @param rotation the local rotation to apply to the target (not null,
     * non-zero norm, unaffected)
     */
    public void addKeyFrameRotation(int keyFrameIndex, Quaternion rotation) {
        float animationTime = keyFrameIndex / fps;
        addTimeRotation(animationTime, rotation);
    }

    /**
     * Add a keyframe for the specified scaling at the specified index.
     *
     * @param keyFrameIndex the keyframe in which full scaling should be
     * achieved (≥0)
     * @param scale the local scaling to apply to the target (not null,
     * unaffected)
     */
    public void addKeyFrameScale(int keyFrameIndex, Vector3f scale) {
        float animationTime = keyFrameIndex / fps;
        addTimeScale(animationTime, scale);
    }

    /**
     * Add a keyframe for the specified Transform at the specified index.
     *
     * @param keyFrameIndex the keyframe in which the full Transform should be
     * achieved (≥0)
     * @param transform the local Transform to apply to the target (not null,
     * unaffected)
     */
    public void addKeyFrameTransform(int keyFrameIndex, Transform transform) {
        float time = keyFrameIndex / fps;
        addTimeTransform(time, transform);
    }

    /**
     * Add a keyframe for the specified translation at the specified index.
     *
     * @param keyFrameIndex the keyframe in which full translation should be
     * achieved (≥0)
     * @param offset the local translation to apply to the target (not null,
     * unaffected)
     */
    public void addKeyFrameTranslation(int keyFrameIndex, Vector3f offset) {
        float time = keyFrameIndex / fps;
        addTimeTranslation(time, offset);
    }

    /**
     * Add a keyframe for the specified rotation at the specified time.
     *
     * @param time the animation time when full rotation should be achieved
     * (≥0, ≤duration)
     * @param rotation the local rotation to apply to the target (not null,
     * non-zero norm, unaffected)
     */
    public void addTimeRotation(float time, Quaternion rotation) {
        if (!(time >= 0f && time <= duration)) {
            throw new IllegalArgumentException("animation time out of range");
        }
        float norm = rotation.norm();
        if (norm == 0f) {
            throw new IllegalArgumentException("rotation cannot have norm=0");
        }

        float normalizingFactor = 1f / FastMath.sqrt(norm);
        Quaternion normalized = rotation.mult(normalizingFactor);
        rotations.put(time, normalized);
    }

    /**
     * Add a keyframe for the specified rotation at the specified time, based on
     * Tait-Bryan angles. Note that this is NOT equivalent to
     * {@link com.jme3.animation.AnimationFactory#addTimeRotationAngles(float, float, float, float)}.
     *
     * @param time the animation time when full rotation should be achieved
     * (≥0, ≤duration)
     * @param xAngle the X angle (in radians)
     * @param yAngle the Y angle (in radians)
     * @param zAngle the Z angle (in radians)
     */
    public void addTimeRotation(float time, float xAngle, float yAngle,
            float zAngle) {
        if (!(time >= 0f && time <= duration)) {
            throw new IllegalArgumentException("animation time out of range");
        }

        Quaternion quat = new Quaternion().fromAngles(xAngle, yAngle, zAngle);
        rotations.put(time, quat);
    }

    /**
     * Add a keyframe for the specified scale at the specified time.
     *
     * @param time the animation time when full scaling should be achieved
     * (≥0, ≤duration)
     * @param scale the local scaling to apply to the target (not null,
     * unaffected)
     */
    public void addTimeScale(float time, Vector3f scale) {
        if (!(time >= 0f && time <= duration)) {
            throw new IllegalArgumentException("animation time out of range");
        }

        Vector3f clone = scale.clone();
        scales.put(time, clone);
    }

    /**
     * Add a keyframe for the specified Transform at the specified time.
     *
     * @param time the animation time when the full Transform should be achieved
     * (≥0, ≤duration)
     * @param transform the local Transform to apply to the target (not null,
     * unaffected)
     */
    public void addTimeTransform(float time, Transform transform) {
        if (!(time >= 0f && time <= duration)) {
            throw new IllegalArgumentException("animation time out of range");
        }

        Vector3f translation = transform.getTranslation(null);
        translations.put(time, translation);
        rotations.put(time, transform.getRotation(null));
        scales.put(time, transform.getScale(null));
    }

    /**
     * Add a keyframe for the specified translation at the specified time.
     *
     * @param time the animation time when the full translation should be
     * achieved (≥0, ≤duration)
     * @param offset the local translation to apply to the target (not null,
     * unaffected)
     */
    public void addTimeTranslation(float time, Vector3f offset) {
        if (!(time >= 0f && time <= duration)) {
            throw new IllegalArgumentException("animation time out of range");
        }

        Vector3f clone = offset.clone();
        translations.put(time, clone);
    }

    /**
     * Create an AnimClip based on the keyframes added to this factory.
     *
     * @param target the target for this clip (which is typically a Spatial)
     * @return a new clip
     */
    public AnimClip buildAnimation(HasLocalTransform target) {
        Set times = new TreeSet<>();
        for (int frameI = 0;; ++frameI) {
            float time = frameI / fps;
            if (time > duration) {
                break;
            }
            times.add(time);
        }
        times.addAll(rotations.keySet());
        times.addAll(scales.keySet());
        times.addAll(translations.keySet());

        int numFrames = times.size();
        float[] timeArray = new float[numFrames];
        Vector3f[] translateArray = new Vector3f[numFrames];
        Quaternion[] rotArray = new Quaternion[numFrames];
        Vector3f[] scaleArray = new Vector3f[numFrames];

        int iFrame = 0;
        for (float time : times) {
            timeArray[iFrame] = time;
            translateArray[iFrame] = interpolateTranslation(time);
            rotArray[iFrame] = interpolateRotation(time);
            scaleArray[iFrame] = interpolateScale(time);

            ++iFrame;
        }

        AnimTrack[] tracks = new AnimTrack[1];
        tracks[0] = new TransformTrack(target, timeArray, translateArray,
                rotArray, scaleArray);
        AnimClip result = new AnimClip(name);
        result.setTracks(tracks);

        return result;
    }

    /**
     * Interpolate successive rotation keyframes for the specified time.
     *
     * @param keyFrameTime the animation time (in seconds, ≥0)
     * @return a new instance
     */
    private Quaternion interpolateRotation(float keyFrameTime) {
        assert keyFrameTime >= 0f && keyFrameTime <= duration;

        float prev = 0f;
        float next = duration;
        for (float key : rotations.keySet()) {
            if (key <= keyFrameTime && key > prev) {
                prev = key;
            }
            if (key >= keyFrameTime && key < next) {
                next = key;
            }
        }
        assert prev <= next;
        Quaternion prevRotation = rotations.get(prev);

        Quaternion result = new Quaternion();
        if (prev == next || !rotations.containsKey(next)) {
            result.set(prevRotation);

        } else { // interpolate
            float fraction = (keyFrameTime - prev) / (next - prev);
            assert fraction >= 0f && fraction <= 1f;
            Quaternion nextRotation = rotations.get(next);
            result.slerp(prevRotation, nextRotation, fraction);
            /*
             * XXX slerp() sometimes negates nextRotation,
             * but usually that's okay because nextRotation and its negative
             * both represent the same rotation.
             */
        }

        return result;
    }

    /**
     * Interpolate successive scale keyframes for the specified time.
     *
     * @param keyFrameTime the animation time (in seconds, ≥0)
     * @return a new instance
     */
    private Vector3f interpolateScale(float keyFrameTime) {
        assert keyFrameTime >= 0f && keyFrameTime <= duration;

        float prev = 0f;
        float next = duration;
        for (float key : scales.keySet()) {
            if (key <= keyFrameTime && key > prev) {
                prev = key;
            }
            if (key >= keyFrameTime && key < next) {
                next = key;
            }
        }
        assert prev <= next;
        Vector3f prevScale = scales.get(prev);

        Vector3f result = new Vector3f();
        if (prev == next || !scales.containsKey(next)) {
            result.set(prevScale);

        } else { // interpolate
            float fraction = (keyFrameTime - prev) / (next - prev);
            assert fraction >= 0f && fraction <= 1f;
            Vector3f nextScale = scales.get(next);
            result.interpolateLocal(prevScale, nextScale, fraction);
        }

        return result;
    }

    /**
     * Interpolate successive translation keyframes for the specified time.
     *
     * @param keyFrameTime the animation time (in seconds, ≥0)
     * @return a new instance
     */
    private Vector3f interpolateTranslation(float keyFrameTime) {
        float prev = 0f;
        float next = duration;
        for (float key : translations.keySet()) {
            if (key <= keyFrameTime && key > prev) {
                prev = key;
            }
            if (key >= keyFrameTime && key < next) {
                next = key;
            }
        }
        assert prev <= next;
        Vector3f prevTranslation = translations.get(prev);

        Vector3f result = new Vector3f();
        if (prev == next || !translations.containsKey(next)) {
            result.set(prevTranslation);

        } else { // interpolate
            float fraction = (keyFrameTime - prev) / (next - prev);
            assert fraction >= 0f && fraction <= 1f;
            Vector3f nextTranslation = translations.get(next);
            result.interpolateLocal(prevTranslation, nextTranslation, fraction);
        }

        return result;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy