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