XNA动画的实现7——优化动画的播放

优化1:使用四元数表示旋转

前面的文章中我们使用欧拉角表示旋转,需要三个浮点数、分别表示绕x、y、z轴的旋转或Yaw、Pitch、Roll,本文我们使用四元数表示旋转,四元数需要4个浮点数,虽然比欧拉角多使用一个浮点数导致耗费的内存增加,但四元数可以平滑插值,避免使用欧拉角带来的抖动、万向节锁的缺陷。

四元数背后的数学比较复杂,大致可以理解成一个旋转轴加一个旋转量,其中前三个分量表示旋转轴,第四个分量表示绕这个轴的旋转值。例如绕y轴旋转90度,用欧拉角表示为(0, MathHelper.ToRadians(90), 0),与它等价的四元数为(sinθ/2nx,sinθ/2ny, sinθ/2nz cosθ/2),其中n为旋转轴向量,θ为旋转角度,即(0,0.707,0,0.707),在XNA框架已经内置了创建四元数的方法,我们使用Quaternion.CreateFromYawPitchRoll(MathHelper.ToRadians(90), 0, 0)就可以生成这个值,使用Quaternion.CreateFromAxisAngle(),Quaternion.CreateFromRotationMatrix()方法亦可。

关键帧类Keyframe也要做相应的调整,将:

public Vector3 Rotation{ get;private set;}

修改为:

public Quaternion Rotation { get; internal set; } 

还有一个变化是将private变为internal,这是为了优化2中的使用,使它可以在类库内部可写。

动画播放类AnimationPlayerBase中的插值算法也需做相应调整,将:

rotation = Vector3.Lerp(keyframes[CurrentKeyframeIndex].Rotation,
keyframes[CurrentKeyframeIndex + 1].Rotation, amt);
transform = Matrix.CreateScale(scale) *Matrix.CreateFromYawPitchRoll(rotation.Y, rotation.X, rotation.Z) *Matrix.CreateTranslation(position);

修改为:

rotation = Quaternion.Slerp(keyframes[CurrentKeyframeIndex].Rotation,
keyframes[CurrentKeyframeIndex + 1].Rotation, amt);
transform = Matrix.CreateScale(scale) *Matrix.CreateFromQuaternion (rotation) *Matrix.CreateTranslation(position);

使用了Quaternion的Slerp方法替代了Vector3的Lerp方法。

优化2:在手动定义的动画数据上附加模型原始变换矩阵

在上一篇文章中我们已经提到,因为模型的原始变换矩阵是不变的,我们完全可以事先将它附加到关键帧中,这样在更新动画数据时就可以减少计算量。

我们在类库AnimationClassLibrary中添加一个辅助类AnimationHelper,代码如下:

/// <summary>
/// 动画处理的辅助类。
/// </summary>
public class AnimationHelper
{
    /// <summary>
    /// 将模型的初始变换矩阵附加到每个关键帧上。
    /// </summary>
    /// <param name="animationData">要处理的动画数据,类型为AnimationData</param>
    /// <param name="originalModelTransform">模型的初始变换矩阵</param>
    public static void AddModelTransform(AnimationData animationData,Matrix[] originalModelTransform)
    {
        foreach (KeyValuePair<string, AnimationClip> animation in animationData.ModelAnimationClips)
        {
            ProcessAnimationClip(animation.Value, originalModelTransform);
        }
    }

    private static void ProcessAnimationClip(AnimationClip animationClip, Matrix[] originalModelTransform)
    {
        // 遍历动画片段的每个关键帧
        foreach (Keyframe keyframe in animationClip.Keyframes)
        {
            // 获取关键帧中的BoneIndex数据
            int boneIndex = keyframe.BoneIndex;
            // 获取关键帧中的位置、缩放、旋转的数据
            Vector3 position = keyframe.Position;
            Vector3 scale = keyframe.Scale;
            Quaternion rotation = keyframe.Rotation;
            // 根据以上信息还原变换矩阵
            Matrix originalTransform = Matrix.CreateScale(scale) *
                Matrix.CreateFromQuaternion(rotation) * Matrix.CreateTranslation(position);
            // 附加模型的原始变换矩阵生成经过处理的变换矩阵
            Matrix newTransform = originalTransform * originalModelTransform[boneIndex];
            // 将这个新矩阵还原为缩放、旋转、位置信息
            newTransform.Decompose(out scale, out rotation, out position);
            // 将新的信息写入当前处理的关键帧中
            keyframe.Position = position;
            keyframe.Rotation = rotation;
            keyframe.Scale = scale;
        }
    }

    /// <summary>
    /// 将模型的初始变换矩阵附加到每个关键帧上。
    /// </summary>
    /// <param name="animationData">要处理的动画数据,类型为ModelAnimationData</param>
    /// <param name="originalModelTransform">模型的初始变换矩阵</param>
    public static void AddModelTransform(ModelAnimationData animationData, Matrix[] originalModelTransform)
    {
        foreach (KeyValuePair<string, ModelAnimationClip> animation in animationData.ModelAnimationClips)
        {
            ProcessModelAnimationClip(animation.Value, originalModelTransform);
        }
    }

    private static void ProcessModelAnimationClip(ModelAnimationClip animationClip, Matrix[] originalModelTransform)
    {
        // 遍历动画片段的每个关键帧
        foreach (ModelKeyframe keyframe in animationClip.Keyframes)
        {
           // 附加模型的原始变换矩阵生成经过处理的变换矩阵
            Matrix newTransform = keyframe.Transform * originalModelTransform[keyframe.BoneIndex];
            keyframe.Transform = newTransform;
        }
    }
}

AddModelTransform有两个重载方法,分别处理AnimationData和ModelAnimationData。从代码中我们可以看到,因为ModelKeyframe使用矩阵表示变换,它的转换代码比Keyframe简单地多,而处理Keyframe时需要首先将平移、旋转和缩放信息首先转换为一个矩阵,在附加变换矩阵,然后将变换后的矩阵重新转换为平移、旋转和缩放信息。

在XNA程序中,必须在创建AnimationData对象或ModelAnimationData之后调用这个方法进行动画数据的再处理:

AnimationHelper.AddModelTransform(animationData, originalModelTransforms); 

经过这样的处理,在AnimationPlayer中我们可以将代码简化为:

/// <summary>
/// 播放ModelMesh动画的类,它播放的是AnimationData类型的动画数据。
/// </summary>
public class AnimationPlayer
{
    /// <summary>
    /// 更新动画数据
    /// </summary>
    public void Update()
    {
        [...]
       
        model.Bones[CurrentClip.Keyframes[CurrentKeyframeIndex].BoneIndex].Transform = transform;
    }
}

与前面的文章相比无需传递模型初始变换矩阵、也无需进行矩阵相乘的操作,提高了处理速度。

注意:如果你忘了使用AddModelTransform方法对动画数据进行再处理,动画播放时显示会不正常,通常表现为模型的初始位置、旋转发生偏移。

优化3:编写自定义内容处理器将AnimationData转换为ModelAnimationData

现在我们有两种动画数据:AnimationData和ModelAnimationData,其中AnimationData储存的是未经插值的平移、缩放和旋转信息,插值操作是在AnimationPlayer类中实时进行的,而ModelAnimationData储存的是已经经过插值的矩阵变换信息,在ModelAnimationPlayer中只需根据时间设置对应的关键帧即可,同样的动画效果ModelAnimationData数据肯定比AnimationData大。用计算机语言说来,就是用时间换空间还是用空间换时间的问题。对于实时图形程序来说,速度是关键,所以通常采用空间换时间的做法,即推荐使用ModelAnimationData。但是手工设置ModelAnimationData的关键帧几乎是不可能的,因此,下面我们需要实现手动设置AnimationData的关键帧,然后自动转换为ModelAnimationData。

要做到这点,最好的方法是编写一个自定义内容处理器。 在XNA动画的实现5——通过ContentManager加载动画数据的最后我们编写一个自定义内容导入器AnimationDataImporter,实现了一个最简单的功能:自动识别后缀名为Animation的动画数据文件,现在需要做一点微小的调整:

[ContentImporter(".Animation", DisplayName = "AnimationData Importer", DefaultProcessor = "AnimationData Processor")]
public class AnimationDataImporter : ContentImporter<AnimationData>
{
    […]
}

在属性中添加了一个参数:DefaultProcessor = "AnimationData Processor”,意思就是这个导入器导入的数据默认使用内容处理期AnimationData Processor处理。 下面我们就来编写一个AnimationData Processor处理器。

首先需要在AnimationPipeline添加Content Processor,我命名为AnimationDataProcessor.cs。

内容处理器

代码如下:

/// <summary>
/// 这个处理器类将AnimationData转换为ModelAnimationData,并根据参数自动进行插值运算。
/// </summary>
[ContentProcessor(DisplayName = "AnimationData Processor")]
public class AnimationDataProcessor : ContentProcessor<AnimationData, ModelAnimationData>
{
    private float rate = 30.0f;

    [DisplayName("Rate")]
    [DefaultValue(30.0f)]
    [Description("插值的采样频率,默认为每秒进行30次插值运算。")]
    public float Rate
    {
        get { return rate; }
        set { rate = value; }
    }

    LerpMode lerpMode = LerpMode.Linear;

    [DisplayName("LerpMode")]
    [DefaultValue(LerpMode.Linear)]
    [Description("插值模式,分为线性插值和曲线插值,默认为线性插值。")]
    public LerpMode LerpMode
    {
        get { return lerpMode; }
        set { lerpMode = value; }
    }       
    
    public override ModelAnimationData Process(AnimationData input, ContentProcessorContext context)
    {
        Dictionary<string, ModelAnimationClip> modelAnimationClips = new Dictionary<string,ModelAnimationClip>();
        foreach (KeyValuePair<string, AnimationClip> animationDictionary in input.ModelAnimationClips)
        {
            ModelAnimationClip modelAnimationClip;
            ConvertAnimationData(animationDictionary.Value , out modelAnimationClip);

            modelAnimationClips.Add(animationDictionary.Key , modelAnimationClip); 
        }

        return new ModelAnimationData(modelAnimationClips, rootAnimationClips);
    }

    /// <summary>
    /// 将AnimationClip类型的动画数据转换为ModelAnimationClip类型的动画数据
    /// </summary>
    private void ConvertAnimationData(AnimationClip animationClip, out ModelAnimationClip modelAnimationClip)
    {
        // 获取当前处理的AnimationClip中的关键帧集合
        List<Keyframe> keyframes = animationClip.Keyframes;
        // 对上述关键帧需要依次按BoneIndex、Time属性进行排序。
        // 此处使用了Linq语法,比起泛型排序更为简洁。
        var query = from keyframe in keyframes
                    orderby keyframe.BoneIndex ,keyframe.Time 
                select keyframe;
        keyframes = query.ToList();
        // 经过处理的关键帧集合,它的类型为ModelKeyframe,接下来的过程要将新的数据填充
        // 到这个集合中。
        List<ModelKeyframe> outKeyframes = new List<ModelKeyframe>();
        
        
        int currentKeyframeIndex = 0;
        // 手动添加第一帧的关键帧数据
        AddKeyframe(keyframes[0], outKeyframes);

        // 遍历所有关键帧
        while (currentKeyframeIndex < keyframes.Count - 1)
        {
            Keyframe currentKeyframe = keyframes[currentKeyframeIndex];
            Keyframe nextKeyframe = keyframes[currentKeyframeIndex + 1];

            // 如果当前关键帧的BoneIndex与下一帧不同,则不能对这两者之间进行插值,
            // 否则动画是不正确的。
            if (currentKeyframe.BoneIndex != nextKeyframe.BoneIndex)
            {
                // 直接添加下一帧的关键帧然后继续进行下一次循环
                AddKeyframe(nextKeyframe, outKeyframes);
                currentKeyframeIndex++;
                continue;
            }

            // 设置一些临时变量
            Vector3 position, scale;
            Quaternion rotation;
            TimeSpan ElapsedTime = TimeSpan.Zero;
            TimeSpan Interval = nextKeyframe.Time - currentKeyframe.Time;

            #region 依次在当前关键帧和下一个关键帧之间进行插值操作

            while (ElapsedTime < Interval)
            {
                ElapsedTime += TimeSpan.FromSeconds(1 / rate);

                // 如果已经超过最后一个关键帧,则手动添加下一个关键帧的数据
                if (ElapsedTime > Interval)
                {
                    AddKeyframe(nextKeyframe, outKeyframes);
                    // 跳出while (ElapsedTime < Interval)
                    break;
                }

                // 获取播放到当前关键帧至下一个关键帧之间的位置,介于0和1之间
                float amt = (float)(ElapsedTime.TotalSeconds / Interval.TotalSeconds);
                // 根据插值模式对位置进行插值运算
                if (lerpMode == LerpMode.Linear)
                    position = Vector3.Lerp(currentKeyframe.Position,
                        nextKeyframe.Position, amt);
                else
                    position = catmullRom3D(
                        keyframes[wrap(currentKeyframeIndex - 1, keyframes.Count - 1)].Position,
                        keyframes[wrap(currentKeyframeIndex, keyframes.Count - 1)].Position,
                        keyframes[wrap(currentKeyframeIndex + 1, keyframes.Count - 1)].Position,
                        keyframes[wrap(currentKeyframeIndex + 2, keyframes.Count - 1)].Position, amt);

                // 曲线运动中的旋转和缩放仍然是线性插值
                rotation = Quaternion.Slerp(currentKeyframe.Rotation,
                    nextKeyframe.Rotation, amt);
                scale = Vector3.Lerp(currentKeyframe.Scale,
                    nextKeyframe.Scale, amt);

                // 计算变换矩阵
                Matrix transform = Matrix.CreateScale(scale) *
                    Matrix.CreateFromQuaternion(rotation) *
                    Matrix.CreateTranslation(position);

                ModelKeyframe keyframe = new ModelKeyframe(currentKeyframe .BoneIndex , ElapsedTime + currentKeyframe.Time, transform);
                outKeyframes.Add(keyframe);
            }
            #endregion

            currentKeyframeIndex++;
        }

        // 处理完毕还需要对输出的ModelKeyframe集合按Time属性排序。
        outKeyframes.Sort(CompareKeyframeTimes);
        modelAnimationClip = new ModelAnimationClip(animationClip.Duration, outKeyframes);
    }

    // 将关键帧插入到ModelKeyframe集合中
    private void AddKeyframe(Keyframe keyframe, List<ModelKeyframe> outKeyframes)
    {
        // 计算变换矩阵
        Matrix transform = Matrix.CreateScale(keyframe.Scale) *
            Matrix.CreateFromQuaternion(keyframe.Rotation) *
            Matrix.CreateTranslation(keyframe.Position);
        ModelKeyframe ModelKeyframe = new ModelKeyframe(keyframe.BoneIndex, keyframe.Time, transform);
        outKeyframes.Add(ModelKeyframe);
    }

    // 按照关键帧的Time属性升序排序的Comparison方法
    private static int CompareKeyframeTimes(ModelKeyframe a, ModelKeyframe b)
    {
        return a.Time.CompareTo(b.Time);
    }

    // 曲线插值计算
    Vector3 catmullRom3D(Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float amt)
    {
        return new Vector3(
            MathHelper.CatmullRom(v1.X, v2.X, v3.X, v4.X, amt),
            MathHelper.CatmullRom(v1.Y, v2.Y, v3.Y, v4.Y, amt),
            MathHelper.CatmullRom(v1.Z, v2.Z, v3.Z, v4.Z, amt));
    }

    // 辅助方法,将输入的"value"参数在[0, max]范围内循环
    int wrap(int value, int max)
    {
        while (value > max)
            value -= max;

        while (value < 0)
            value += max;

        return value;
    }
}

其实里面的插值代码完全照搬自AnimationPlayer,只不过现在的插值处理是在DesignTime就已经完成,无需在RunTime进行。

但这里有一个新问题是以前没有遇到过的:若一个AnimationClip中的Keyframe的BoneIndex不同应该如何处理?代码不进行调整插值的结果是错误的。为了说明这个问题我制作了一个新的模型HierarchicalMan2.X,相比于前面使用的HierarchicalMan.X,在蓝色的头上加一顶绿帽子(?!对不起,制作时没想那么多,发现后懒得修改了),如下图所示:

模型截图

现在想要实现的动画是在1秒内转头(BoneIndex为2)90度,同时帽子(BoneIndex为4)相对于头也转90度,而对应的关键帧是放在一个AnimationClip中。要能正确的进行插值操作,需要根据BoneIndex对关键帧集合进行分组,然后针对每一组进行插值操作。具体代码请看源代码。

在内容处理器中还公开了两个属性:Rate和LerpMode,你可以在属性面板中调整这两个参数控制插值的过程。

调整参数

现在,当你在内容管道导入只含关键帧的AnimationData数据后,就会自动转换为ModelAnimationData数据,然后使用ModelAnimationPlayer播放这个动画,注意别忘了加载后使用AnimationHelper.AddModelTransform()方法将模型的初始变换矩阵附加在ModelKeyframe的变换矩阵上。

总结

最后总结一下目前为止的代码实现动画播放的几种方法:

1.手动设置Keyframe,生成AnimationClip,进一步生成AnimationData。然后调用AnimationHelper.AddModelTransform()附加模型初始变换,若动画是施加在模型Root上,使用AnimationPlayer播放这个动画。

2.将AnimationData串行化为后缀名为Animation的动画脚本文件,然后将这个文件加载到Content项目中,手动将Content Processor设置为No Processing Required,使用Content.Load<AnimationData>方法加载这个动画,然后调用AnimationHelper.AddModelTransform()附加模型初始变换,最后使用AnimationPlayer播放这个动画。

3.与上一个方法类似,不同之处在于保持默认的处理器不变,使用Content.Load<ModelAnimationData>方法加载这个动画,最后使用RigidAnimationPlayer播放这个动画。

4.若模型已经包含动画数据,则使用代码modelData = Model.Tag as ModelAnimationData;从模型的Tag属性中获取动画数据,然后使用RigidAnimationPlayer播放这个动画。

5.若是蒙皮动画模型,则使用代码skinningData = skinnedModel.Tag as SkinningAnimationData;获取动画数据,然后使用RigidAnimationPlayer播放这个动画。

在第4、5中方法中你可以同时附加手动定义的动画数据。

文件下载(已下载 1335 次)

发布时间:2011/9/15 上午7:18:25  阅读次数:6834

2006 - 2024,推荐分辨率 1024*768 以上,推荐浏览器 Chrome、Edge 等现代浏览器,截止 2021 年 12 月 5 日的访问次数:1872 万 9823 站长邮箱

沪 ICP 备 18037240 号-1

沪公网安备 31011002002865 号