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 阅读次数:7353