Blend end of anim with start

Aug 24, 2008 at 11:36 PM
Is there a way to blend end of animation with start of it ? Or to just leave some "free space" at end of animation and so it interpolates last keyframe in animation with keyframe that is at the start of animation? If I speak unclear, I'm sorry and will try to explain myself, just ask questions.

As i know animation goes like that - interpolate between keyframes... but it does not interpolate start keyframe with end keyframe, even if there are some "free space" left at the end of animation... So maybe i could do a check like "if (is last keyframe in animation) { next keyframe = first keyframe in animation; }" If this or something like this is possible, where should i look in code to do it? Also, big thanks for this library =)
Coordinator
Aug 29, 2008 at 1:47 AM
Hi,

If I'm not wrong the end and the beginning of the animation (its last and first frame) are interpolated! The problem is that they are interpolated based on the difference of the animation time and the last frame time.

For example, if you last keyframe has time 10, and the animation finishes on time 10 it will not be interpolated (since there's no time to interpolate). But if you last frame has time 10 and the animation finishes on time 12, it will use this to interpolate between the last and first frame.
Sep 13, 2008 at 9:47 PM
Actualy it does not interpolate it... I tested... with and without animation splitting, i have 100 frames, last frame is 80 and then 20 without keyframes, but it just stands still at the time 80-100 :'(
Oct 22, 2008 at 2:07 AM
Edited Oct 22, 2008 at 2:48 AM
I may have a solution to your problem Stormii.

I ran into the same issue where animations I was getting from my boss didn't have the padding that Mr Evangelista was talking about and he didn't want me to edit the animations if I could help it.  I made a few additional modifications to the AnimationController class and came up with a quick and ugly fix.

If I get the chance I'd like to clean up what I did so its not quite so messy but if you still need a solution this should do the trick.  If you have any questions feel free to ask, I'm really tired and may not be explaining everything clearly.  I also have not extensively tested these changes so they may still have a bug or two in them.

I added a boolean _fadeWhileLooping mutated with the property FadeWhileLooping to flag an animation for interpolation between the end of the animation and the next loop.  The duration of the interpolation between the end and beginning of the animation is controlled with the LoopFadeTime property.

The following code contains the modified version of the AnimationController.cs file that Mr Evangelista wrote and contains a few additions to allow for interpolation to a specific time in an animation as well as the interpolation from the end of an animation to the beginning of the same animation without animation padding and animation pausing.  I hope this helps with the issues you've been having.

//*** Begin AnimationController.cs ***

using System;
using Microsoft.Xna.Framework;

namespace XNAnimation.Controllers
{
    /// <summary>
    /// Controls how animations are played and interpolated.
    /// </summary>
    public class AnimationController : IAnimationController, IBlendable
    {
        #region member fields

        private SkinnedModelBoneCollection skeleton;
        private Pose[] localBonePoses;
        private Matrix[] skinnedBoneTransforms;

        private AnimationClip animationClip;
        private TimeSpan time;
        private float speed;
        private bool loopEnabled;
        private PlaybackMode playbackMode;

        private float blendWeight;

        // Interpolation mode fields
        private InterpolationMode translationInterpolation;
        private InterpolationMode orientationInterpolation;
        private InterpolationMode scaleInterpolation;

        // CrossFade fields
        private bool crossFadeEnabled;
        private AnimationClip crossFadeAnimationClip;
        private float crossFadeInterpolationAmount;
        private TimeSpan crossFadeTime;
        private TimeSpan crossFadeElapsedTime;
        private InterpolationMode crossFadeTranslationInterpolation;
        private InterpolationMode crossFadeOrientationInterpolation;
        private InterpolationMode crossFadeScaleInterpolation;
        private TimeSpan _startTime;                                    // destination time of the clip being tweened to
        private bool _fadeWhileLooping;                                 // if enabled crossfade between the end and beginning of an animation
        private TimeSpan _loopFadeTime;                                 // duration of crossfade between the end and beginning of an animaiton
        private bool _fadeLooping;                                      // manages the animation's state of interpolating the next loop or not


        private bool hasFinished;
        private bool isPlaying;
        private bool _isPaused;

        #endregion

        #region Properties

        public bool FadeWhileLooping
        {
            get { return _fadeWhileLooping; }
            set { _fadeWhileLooping = value; }
        }

        public TimeSpan LoopFadeTime
        {
            get { return _loopFadeTime; }
            set { _loopFadeTime = value; }
        }

        public bool FadeLooping
        {
            get { return _fadeLooping; }
            set { _fadeLooping = value; }
        }

        public bool IsPaused
        {
            get { return _isPaused; }
            set { _isPaused = value; }
        }

        public TimeSpan StartTime
        {
            get { return _startTime; }
            set { _startTime = value; }
        }

        /// <inheritdoc />
        public AnimationClip AnimationClip
        {
            get { return animationClip; }
        }

        /// <inheritdoc />
        public TimeSpan Time
        {
            get { return time; }
            set { time = value; }
        }

        /// <inheritdoc />
        public float Speed
        {
            get { return speed; }
            set
            {
                if (speed < 0)
                    throw new ArgumentException("Speed must be a positive value");

                speed = value;
            }
        }

        /// <inheritdoc />
        public bool LoopEnabled
        {
            get { return loopEnabled; }
            set
            {
                loopEnabled = value;

                if (hasFinished && loopEnabled)
                    hasFinished = false;
            }
        }

        /// <inheritdoc />
        public PlaybackMode PlaybackMode
        {
            get { return playbackMode; }
            set { playbackMode = value; }
        }

        /// <inheritdoc />
        public InterpolationMode TranslationInterpolation
        {
            get { return translationInterpolation; }
            set { translationInterpolation = value; }
        }

        /// <inheritdoc />
        public InterpolationMode OrientationInterpolation
        {
            get { return orientationInterpolation; }
            set { orientationInterpolation = value; }
        }

        /// <inheritdoc />
        public InterpolationMode ScaleInterpolation
        {
            get { return scaleInterpolation; }
            set { scaleInterpolation = value; }
        }

        /// <inheritdoc />
        public bool HasFinished
        {
            get { return hasFinished; }
        }

        /// <inheritdoc />
        public bool IsPlaying
        {
            get { return isPlaying; }
        }

        /// <inheritdoc />
        public Pose[] LocalBonePoses
        {
            get { return localBonePoses; }
        }

        /// <inheritdoc />
        public Matrix[] SkinnedBoneTransforms
        {
            get { return skinnedBoneTransforms; }
        }

        /// <inheritdoc />
        public float BlendWeight
        {
            get { return blendWeight; }
            set { blendWeight = value; }
        }

        #endregion

        /// <summary>Initializes a new instance of the
        /// <see cref="T:XNAnimation.Controllers.AnimationController" />
        /// class.
        /// </summary>
        /// <param name="skeleton">The skeleton of the model to be animated</param>
        public AnimationController(SkinnedModelBoneCollection skeleton)
        {
            this.skeleton = skeleton;
            localBonePoses = new Pose[skeleton.Count];
            skinnedBoneTransforms = new Matrix[skeleton.Count];
            skeleton[0].CopyBindPoseTo(localBonePoses);

            time = TimeSpan.Zero;
            speed = 1.0f;
            loopEnabled = true;
            playbackMode = PlaybackMode.Forward;

            FadeWhileLooping = true;
            LoopFadeTime = TimeSpan.FromSeconds(0.3);

            blendWeight = 1.0f;

            translationInterpolation = InterpolationMode.None;
            orientationInterpolation = InterpolationMode.None;
            scaleInterpolation = InterpolationMode.None;

            crossFadeEnabled = false;
            crossFadeInterpolationAmount = 0.0f;
            crossFadeTime = TimeSpan.Zero;
            crossFadeElapsedTime = TimeSpan.Zero;

            hasFinished = false;
            isPlaying = false;
        }

        /// <summary>
        /// Base method for starting an animation clip at time zero.
        /// </summary>
        public void StartClip(AnimationClip aClip)
        {
            this.animationClip = aClip;
            hasFinished = false;
            isPlaying = true;

            time = TimeSpan.Zero;
            skeleton[0].CopyBindPoseTo(localBonePoses);

            if (FadeLooping)
            {
                FadeLooping = false;
            }
        }

        /// <summary>
        /// Overloaded method for starting an animation clip at any time.
        /// </summary>
        public void StartClip(AnimationClip aClip, TimeSpan aStartTime)
        {
            this.animationClip = aClip;
            hasFinished = false;
            isPlaying = true;

            time = CheckClipDuration(aClip, aStartTime, false);
            skeleton[0].CopyBindPoseTo(localBonePoses);

            if (FadeLooping)
            {
                FadeLooping = false;
            }
        }

        /// <inheritdoc />
        public void PlayClip(AnimationClip animationClip)
        {
            this.animationClip = animationClip;

            if (time < animationClip.Duration)
            {
                hasFinished = false;
                isPlaying = true;
            }
        }

        /// <summary>
        /// Base method for starting a tween between the current AnimationClip and the first frame
        /// of another AnimationClip.
        /// </summary>
        public void CrossFade(AnimationClip aClip, TimeSpan fadeTime)
        {
            StartTime = TimeSpan.Zero;

            CrossFade(aClip, fadeTime, InterpolationMode.Linear, InterpolationMode.Linear,
                InterpolationMode.Linear);
        }

        /// <summary>
        /// Overloaded method for starting a tween between the current AnimationClip and a specific
        /// time in another AnimationClip.
        /// </summary>
        public void CrossFade(AnimationClip aClip, TimeSpan fadeTime, TimeSpan aStartTime)
        {
            StartTime = CheckClipDuration(aClip, aStartTime, false);
           
            CrossFade(aClip, fadeTime, InterpolationMode.Linear, InterpolationMode.Linear,
                InterpolationMode.Linear);
        }

        /// <summary>
        /// Overloaded method for tweening between one AnimationClip and another with interpolation data.
        /// </summary>
        public void CrossFade(AnimationClip aClip,
                              TimeSpan fadeTime,
                              InterpolationMode translationInterpolation,
                              InterpolationMode orientationInterpolation,
                              InterpolationMode scaleInterpolation)
        {
            if (crossFadeEnabled)
            {
                crossFadeInterpolationAmount = 0;
                crossFadeTime = TimeSpan.Zero;
                crossFadeElapsedTime = TimeSpan.Zero;

                StartClip(crossFadeAnimationClip, StartTime);
            }

            crossFadeAnimationClip = aClip;
            crossFadeTime = fadeTime;
            crossFadeElapsedTime = TimeSpan.Zero;

            crossFadeTranslationInterpolation = translationInterpolation;
            crossFadeOrientationInterpolation = orientationInterpolation;
            crossFadeScaleInterpolation = scaleInterpolation;

            crossFadeEnabled = true;
        }

        // check a time against a AnimationClip's duration
        // return a TimeSpan of 0 or the AnimationClip's duration if the targetStartTime is greater than the clip's duration
        public TimeSpan CheckClipDuration(AnimationClip aClip, TimeSpan targetStartTime, Boolean returnClosestTime)
        {
            return targetStartTime < aClip.Duration ? targetStartTime : returnClosestTime ? aClip.Duration : TimeSpan.Zero;
        }

        /*
        public bool GetCurrentKeyframeTransform(string animationChannelName, out Matrix transform)
        {
            if (animationClip == null)
                throw new InvalidOperationException("animationClip");

            // Check if this animation has the desired channel
            AnimationChannel animationChannel;
            if (!animationClip.Channels.TryGetValue(animationChannelName, out animationChannel))
            {
                //keyframeTransform = Matrix.Identity;
                transform = Matrix.Identity;
                return false;
            }

            if (interpolationMode == InterpolationMode.None)
            {
                AnimationChannelKeyframe keyframe = animationChannel.GetKeyframeByTime(time);
                transform = keyframe.Transform;
            }
            else
            {
                int keyframeIndex = animationChannel.GetKeyframeIndexByTime(time);
                int nextKeyframeIndex = (keyframeIndex + 1) % animationChannel.Count;

                AnimationChannelKeyframe pose1 = animationChannel[keyframeIndex];
                AnimationChannelKeyframe pose2 = animationChannel[nextKeyframeIndex];

                long timeBetweenPoses;
                // Looping
                if (keyframeIndex == (animationChannel.Count - 1) && nextKeyframeIndex == 0)
                    if (loopEnabled)
                        timeBetweenPoses = animationClip.Duration.Ticks - pose1.Time.Ticks;
                    else
                        timeBetweenPoses = 0;
                else
                    timeBetweenPoses = pose2.Time.Ticks - pose1.Time.Ticks;

                if (timeBetweenPoses > 0)
                {
                    long elapsedPoseTime = time.Ticks - pose1.Time.Ticks;
                    float lerpFactor = elapsedPoseTime / (float) timeBetweenPoses;

                    if (interpolationMode == InterpolationMode.Linear)
                    {
                        //Matrix lerpMatrix;
                        //Matrix.Subtract(ref pose2.Transform, ref pose1.Transform, out lerpMatrix);
                        //Matrix.Multiply(ref lerpMatrix, lerpFactor, out lerpMatrix);
                        transform = pose1.Transform +
                            (pose2.Transform - pose1.Transform) * lerpFactor;
                        //Matrix.Lerp(ref pose1.Transform, ref pose2.Transform, lerpFactor);
                    }
                        //else if (interpolationMode == InterpolationMode.Spherical)
                    else
                    {
                        Vector3 temp;
                        Vector3 pose1Translation, pose2Translation;
                        Quaternion pose1Quaternion, pose2Quaternion;
                        pose1.Transform.Decompose(out temp, out pose1Quaternion, out pose1Translation);
                        pose2.Transform.Decompose(out temp, out pose2Quaternion, out pose2Translation);

                        Vector3 translation =
                            Vector3.SmoothStep(pose1Translation, pose2Translation, lerpFactor);
                        Quaternion rotation =
                            Quaternion.Slerp(pose1Quaternion, pose2Quaternion, lerpFactor);

                        Matrix.
                        Matrix slerpMatrix = Matrix.CreateFromQuaternion(rotation);
                        slerpMatrix.Translation = translation;
                        transform = slerpMatrix;
                    }
                }
                else
                    transform = pose1.Transform;
            }

            return true;
        }
        */

        /// <inheritdoc />
        public void Update(TimeSpan elapsedTime, Matrix parent)
        {
            if (hasFinished)
                return;

            // Scale the elapsed time
            TimeSpan scaledElapsedTime = IsPaused ? TimeSpan.Zero : TimeSpan.FromTicks((long)(elapsedTime.Ticks * speed));

            if (animationClip != null)
            {
                if(!FadeLooping)
                    UpdateAnimationTime(scaledElapsedTime);
               
                if (crossFadeEnabled)
                    UpdateCrossFadeTime(scaledElapsedTime);

                UpdateChannelPoses();
            }

            UpdateAbsoluteBoneTransforms(ref parent);
        }

        /// <summary>
        /// Updates the CrossFade time
        /// </summary>
        /// <param name="elapsedTime">Time elapsed since the last update.</param>
        private void UpdateCrossFadeTime(TimeSpan elapsedTime)
        {
            crossFadeElapsedTime += elapsedTime;

            if (crossFadeElapsedTime > crossFadeTime)
            {
                crossFadeEnabled = false;
                crossFadeInterpolationAmount = 0;
                crossFadeTime = TimeSpan.Zero;
                crossFadeElapsedTime = TimeSpan.Zero;

                StartClip(crossFadeAnimationClip, StartTime);
            }
            else
                crossFadeInterpolationAmount = crossFadeElapsedTime.Ticks / (float) crossFadeTime.Ticks;
        }

        /// <summary>
        /// Updates the animation clip time.
        /// </summary>
        /// <param name="elapsedTime">Time elapsed since the last update.</param>
        private void UpdateAnimationTime(TimeSpan elapsedTime)
        {
            // Ajust controller time
            if (playbackMode == PlaybackMode.Forward)
            {
                time += elapsedTime;

                if (FadeWhileLooping && time >= animationClip.Duration)
                {
                    CrossFade(this.animationClip, LoopFadeTime);
                    FadeLooping = true;
                }
                else
                    CheckAnimationEnd(elapsedTime);

            }
            else
            {
                time -= elapsedTime;

                if (FadeWhileLooping && time <= TimeSpan.Zero)
                {
                    CrossFade(this.animationClip, LoopFadeTime, animationClip.Duration);
                    FadeLooping = true;
                }
                else
                    CheckAnimationEnd(elapsedTime);
            }
        }

        private void CheckAnimationEnd(TimeSpan elapsedTime)
        {
            // Animation finished
            if (time < TimeSpan.Zero || time > animationClip.Duration)
            {
                if (loopEnabled)
                {
                    if (time > animationClip.Duration)
                        while (time > animationClip.Duration)
                            time -= animationClip.Duration;
                    else
                        while (time < TimeSpan.Zero)
                            time += animationClip.Duration;

                    // Copy bind pose on animation restart
                    skeleton[0].CopyBindPoseTo(localBonePoses);
                }
                else
                {
                    if (time > animationClip.Duration)
                        time = animationClip.Duration;
                    else
                        time = TimeSpan.Zero;

                    isPlaying = false;
                    hasFinished = true;
                }
            }
        }

        /// <summary>
        /// Updates the pose of all skeleton's bones.
        /// </summary>
        private void UpdateChannelPoses()
        {
            AnimationChannel animationChannel;
            Pose channelPose;

            for (int i = 0; i < localBonePoses.Length; i++)
            {
                // Search for the current channel in the current animation clip
                string animationChannelName = skeleton[i].Name;
                if (animationClip.Channels.TryGetValue(animationChannelName, out animationChannel))
                {
                    InterpolateChannelPose(animationChannel, time, out channelPose);
                    localBonePoses[i] = channelPose;
                }

                // If CrossFade is enabled blend this channel in two animation clips
                if (crossFadeEnabled)
                {
                    // Search for the current channel in the cross fade clip
                    if (crossFadeAnimationClip.Channels.TryGetValue(animationChannelName,
                            out animationChannel))
                    {
                        InterpolateChannelPose(animationChannel, StartTime, out channelPose);
                    }
                    else
                        channelPose = skeleton[i].BindPose;

                    // Interpolate each channel with the cross fade animation
                    localBonePoses[i] =
                        Pose.Interpolate(localBonePoses[i], channelPose, crossFadeInterpolationAmount,
                            crossFadeTranslationInterpolation, crossFadeOrientationInterpolation,
                            crossFadeScaleInterpolation);
                }
            }
        }

        /// <summary>
        /// Retrieves and interpolates the pose of an animation channel.
        /// </summary>
        /// <param name="animationChannel">Name of the animation channel.</param>
        /// <param name="animationTime">Current animation clip time.</param>
        /// <param name="outPose">The output interpolated pose.</param>
        private void InterpolateChannelPose(AnimationChannel animationChannel, TimeSpan animationTime,
            out Pose outPose)
        {
            if (translationInterpolation == InterpolationMode.None &&
                orientationInterpolation == InterpolationMode.None &&
                    scaleInterpolation == InterpolationMode.None)
            {
                int keyframeIndex = animationChannel.GetKeyframeIndexByTime(animationTime);
                outPose = animationChannel[keyframeIndex].Pose;
            }
            else
            {
                int keyframeIndex = animationChannel.GetKeyframeIndexByTime(animationTime);
                int nextKeyframeIndex = (keyframeIndex + 1) % animationChannel.Count;

                AnimationChannelKeyframe keyframe1 = animationChannel[keyframeIndex];
                AnimationChannelKeyframe keyframe2 = animationChannel[nextKeyframeIndex];

                // Calculate the time between the keyframes considering loop
                long keyframeDuration;
                if (keyframeIndex == (animationChannel.Count - 1))
                    keyframeDuration = animationClip.Duration.Ticks - keyframe1.Time.Ticks;
                else
                    keyframeDuration = keyframe2.Time.Ticks - keyframe1.Time.Ticks;

                // Interpolate when duration higher than zero
                if (keyframeDuration > 0)
                {
                    long elapsedKeyframeTime = animationTime.Ticks - keyframe1.Time.Ticks;
                    float lerpFactor = elapsedKeyframeTime / (float) keyframeDuration;

                    outPose =
                        Pose.Interpolate(keyframe1.Pose, keyframe2.Pose, lerpFactor,
                            translationInterpolation, orientationInterpolation, scaleInterpolation);
                }
                // Otherwise don't interpolate
                else
                    outPose = keyframe1.Pose;
            }
        }

        /// <summary>
        /// Calculates the final configuration of all skeleton's bones used to transform
        /// the model's mesh.
        /// </summary>
        /// <param name="parent"></param>
        private void UpdateAbsoluteBoneTransforms(ref Matrix parent)
        {
            Matrix poseTransform;

            // Calculate the pose matrix
            poseTransform = Matrix.CreateFromQuaternion(localBonePoses[0].Orientation);
            poseTransform.Translation = localBonePoses[0].Translation;

            /*
            if (localBonePoses[0].Orientation == Quaternion.Identity)
            {
                System.Console.WriteLine("Pose 0");
            }
            */

            // Scale vectors
            poseTransform.M11 *= localBonePoses[0].Scale.X;
            poseTransform.M21 *= localBonePoses[0].Scale.X;
            poseTransform.M31 *= localBonePoses[0].Scale.X;
            poseTransform.M12 *= localBonePoses[0].Scale.Y;
            poseTransform.M22 *= localBonePoses[0].Scale.Y;
            poseTransform.M32 *= localBonePoses[0].Scale.Y;
            poseTransform.M13 *= localBonePoses[0].Scale.Z;
            poseTransform.M23 *= localBonePoses[0].Scale.Z;
            poseTransform.M33 *= localBonePoses[0].Scale.Z;

            // TODO Use and test the scale
            //poseTransform.Scale = localBonePoses[0].Scale;

            // Calculate the absolute bone transform
            skinnedBoneTransforms[0] = poseTransform * parent;
            for (int i = 1; i < skinnedBoneTransforms.Length; i++)
            {
                // Calculate the pose matrix
                poseTransform = Matrix.CreateFromQuaternion(localBonePoses[i].Orientation);
                poseTransform.Translation = localBonePoses[i].Translation;

                /*
                if (localBonePoses[i].Orientation == Quaternion.Identity)
                {
                    System.Console.WriteLine("Pose " + i);
                }
                */

                // Scale vectors
                poseTransform.M11 *= localBonePoses[i].Scale.X;
                poseTransform.M21 *= localBonePoses[i].Scale.X;
                poseTransform.M31 *= localBonePoses[i].Scale.X;
                poseTransform.M12 *= localBonePoses[i].Scale.Y;
                poseTransform.M22 *= localBonePoses[i].Scale.Y;
                poseTransform.M32 *= localBonePoses[i].Scale.Y;
                poseTransform.M13 *= localBonePoses[i].Scale.Z;
                poseTransform.M23 *= localBonePoses[i].Scale.Z;
                poseTransform.M33 *= localBonePoses[i].Scale.Z;

                int parentIndex = skeleton[i].Parent.Index;
                skinnedBoneTransforms[i] = poseTransform * skinnedBoneTransforms[parentIndex];
            }

            // Calculate final bone transform
            for (int i = 0; i < skinnedBoneTransforms.Length; i++)
            {
                skinnedBoneTransforms[i] = skeleton[i].InverseBindPoseTransform *
                    skinnedBoneTransforms[i];
            }
        }
    }
}

//*** End AnimationController.cs ***

- Neal Alpert
Oct 24, 2008 at 1:21 AM
After a bit of testing the changes seem fairly stable with one very notable exception.

If you attempt to set the FadeWhileLooping property to true for an animation that has the same ending frame as the starting frame an exception will be thrown.

- Neal