XNA动画的实现6——编写自定义导入器处理模型自带的动画数据
上一篇文章中我们手动定义了动画数据,然后将它们保存为一个xml文件,运行程序时执行用Content进行加载就可以重用这些动画数据。但是手动处理很难实现复杂的动画,通常都是借助3D应用程序生成复杂的动画的,然后在导出模型文件时就可以包含这些动画数据。这篇文章主要就是介绍如何通过Content项目将这些嵌在模型文件中的动画数据提取出来。每种模型格式的动画数据不尽相同,以下是用.x文件为例介绍的。
这篇文章主要参考自XNA官网的Custom Model Rigid and Skinned Animations示例(http://create.msdn.com/en-US/education/catalog/sample/custom_model_rigid_and_skinned)。但是这个示例在播放有层次结构的模型是会有点问题,所以做了适当修改。
动画模型文件的创建
首先在3DS MAX中打开上几篇文章中一直使用的HierarchicalMan.max文件,不过这次我们还需添加动画数据,我们实现了在0至30帧之间让身体转动,31至61帧之间让头部转动,62至92帧之间让身体和头部同时转动,具体做法请参见X文件的导出系列4——关键帧动画,然后将它导出为x文件,我命名为AnimationMan.x。下图为导出选项的Animation选项卡截图:

从导出选项中我们可以看出,我们只需指定0、30、31、61、62、92关键帧的数据,导出时会自动进行插值操作,默认的采样频率为30,即每隔1/30秒进行一次插值操作。从上述截图中我们发现还可以不进行插值操作,只输出这个6个帧,如果你这样做的话,为了不使动画产生跳跃的感觉,你需要在XNA程序中实现代码进行插值操作,这个代码其实在前几篇文章我们一直在用。
导出为x文件后,让我们看一下内容:
[…]
Frame Body
{
[…]
Frame Head
{
[…]
}
}
AnimationSet TurnBody
{
Animation Anim-Body
{
{ Body }
AnimationKey
{
4;
31;
0;16;1.000000,0.000000,0.000000,0.000000,0.000000,1.000000,0.000000,0.000000,0.000000,0.000000,1.000000,
0.000000,0.000000,1.000000,0.000000,1.000000;;,
[…]
}
}
Animation Anim-Head
{
{ Head }
AnimationKey
{
4;
31;
0 ;
16;1.000000,0.000000,0.000000,0.000000,0.000000,1.000000,0.000000,0.000000,0.000000,0.000000,
1.000000,0.000000,0.000000,2.000000,0.000000,1.000000;;,
[…]
}
}
}
AnimationSet TurnHead
{
Animation Anim-Body
{
{ Body }
AnimationKey
{
4;
31;
0;16;1.000000,0.000000,0.000000,0.000000,0.000000,1.000000,0.000000,0.000000,0.000000,0.000000,
1.000000,0.000000,0.000000,1.000000,0.000000,1.000000;;,
}
}
Animation Anim-Head
{
{ Head }
AnimationKey
{
4;
31;
0;16;1.000000,0.000000,0.000000,0.000000,0.000000,1.000000,0.000000,0.000000,0.000000,
0.000000,1.000000,0.000000,0.000000,2.000000,0.000000,1.000000;;,
[…]
}
}
}
AnimationSet TurnBodyHead
{
Animation Anim-Body
{
{ Body }
AnimationKey
{
4;
31;
0;16;1.000000,0.000000,0.000000,0.000000,0.000000,1.000000,0.000000,0.000000,0.000000,0.000000,1.000000,
0.000000,0.000000,1.000000,0.000000,1.000000;;,
[…]
}
}
Animation Anim-Head
{
{ Head }
AnimationKey
{
4;
31;
0;16;1.000000,0.000000,0.000000,0.000000,0.000000,1.000000,0.000000,0.000000,0.000000,0.000000,1.000000,
0.000000,0.000000,2.000000,0.000000,1.000000;;,
[…]
}
}
}
从文件中我们可以看出,相对于HierarchicalMan.x,AnimationMan.x的内容多出三个AnimationSet,分别对应TurnBody,TurnHead和TurnBodyHead三个动画,在内部的AnimationKey节点中存储了动画变换的矩阵数据。
现在的问题是,如何提取出这些数据并将它们转换为XNA程序可用的AnimationData类,这样就可以使用上一篇文章的代码进行播放了。
编写自定义内容处理器
我们知道,XNA并不能直接使用外部文件格式,而是必须首先通过内容管道转换为中间格式xnb,处理过程可以用下图表示:

根据上图,我们可知当.x文件导入到Content项目时,它会自动调用X File - XNA Framework内容导入器将它转换为Content DOM Types(这种情况是NodeContent(http://msdn.microsoft.com/en-us/library/microsoft.xna.framework.content.pipeline.graphics.nodecontent.aspx)),然后调用Model - XNA Framework内容处理器输出为Processor Output Types(这种情况是ModelContent(http://msdn.microsoft.com/en-us/library/microsoft.xna.framework.content.pipeline.processors.modelcontent.aspx)),最后编译为xnb文件。
内容导入器实际上已经读取了动画数据,只不过默认的内容处理器并不会进行处理,所以使用默认的处理器,你无法在XNA程序中获取动画数据,因此你必须编写自定义的内容导入器。
想要理解原理,你必须进行逐步调试,由于Content项目在运行时并不会被调用,所以依靠简单的设置断点的方法无效,以前在38.蒙皮动画模型内容处理器这篇文章中介绍过调试Content项目的方法,但在XNA4.0之下好像失效了,而在XNA4.0帮助文件中Tips for Developing Custom Importers and Processors(http://msdn.microsoft.com/en-us/library/ff827628.aspx)的Debugging Custom Importers and Processors的第3步我又看不懂,而且下面的留言中也提到还缺一个步骤。不过在http://badcorporatelogo.wordpress.com/2010/10/31/xna-content-pipeline-debugging-4-0/中可以下载一个Visual Studio项目模板(DebugPipeline.vsix本地下载),安装后就可以在新建项目中找到Debug XNA Content Pileline(4.0)模板,如下图所示:

在你的解决方案中添加这个项目,设为启动项目。

然后打开Program.cs文件,修改两个常量ProjectToDebug和SingleItem,它们分别表示Content项目和要调试的模型文件,你需要根据自己机器上的情况进行修改:

然后打开内容管道项目CustomModelAnimationPipeline中的AnimatedModelProcessor.cs文件,在其中设置断点,现在我们就可以对内容管道进行调试了:

下面我们一步一步来分析过程:。
在经过内容导入器处理后,.x文件的内容已经转换为了NodeContent的形式,首先调用ValidateMesh(input, context, null)方法对模型进行检测,确保它包含的动画数据是可以被这个内容处理器进行处理。具体的说,具体地说,若MeshContent的父节点为一个BoneContent,且它的Name不为null,就是不符合要求的模型,这个处理器无法处理。ValidateMesh方法的具体代码不是重点,所以不列了。
然后的代码是:
// 调用ModelProcessor基类获取模型数据
ModelContent model = base.Process(input, context);
// 模型内部的ModelMesh的动画片段集合
Dictionary<string, ModelAnimationClip> modelAnimationClips = new Dictionary<string, ModelAnimationClip>();
// 对动画数据进行处理
ProcessAnimations(input, model, modelAnimationClips);
// 将动画数据存储在模型的Tag属性中
model.Tag = new ModelData(modelAnimationClips);
return model;
关键就是通过ProcessAnimations方法将动画数据放在模型的Tag属性中,这样XNA程序就可以从Tag属性获取动画数据播放动画了。
最复杂的是ProcessAnimations方法,要理解这个方法,首先应对input参数的具体内容有清晰的认识,它是理解后继内容的基础,input就是导入器的输出结果,它是一个NodeContent,我画了一张图显示了里面的关键数据:
下面是ProcessAnimations方法的代码:
static void ProcessAnimations(
NodeContent input,
ModelContent model,
Dictionary<string, ModelAnimationClip> modelAnimationClips)
{
// 构建一个表,此表将bone的名称映射到它的索引
// 这个表不包含Name为null的bone
Dictionary<string, int> boneMap = new Dictionary<string, int>();
for (int i = 0; i < model.Bones.Count; i++)
{
string boneName = model.Bones[i].Name;
if (!string.IsNullOrEmpty(boneName))
boneMap.Add(boneName, i);
}
// 提取物体根节点的动画,然后将它们放置在rootAnimationClips集合中
foreach (KeyValuePair<string, AnimationContent> animation in input.Animations)
{
ModelAnimationClip processed = ProcessRootAnimation(animation.Value, model.Bones[0].Name);
modelAnimationClips.Add(animation.Key, processed);
}
// 根节点之下所有的动画名称,放置在animationNames集合中
List<string> animationNames = new List<string>();
AddAnimationNodes(animationNames, input);
// 提取根节点下的所有动画,然后将它们放置在modelAnimationClips集合中
foreach (string key in animationNames)
{
ModelAnimationClip processed = ProcessAnimation(key, boneMap, input, model);
// 如果key相同,则将key相同的两个ModelAnimationClip进行合并
if(modelAnimationClips .ContainsKey (key))
modelAnimationClips[key].Merge(processed);
else
modelAnimationClips.Add(key, processed);
}
}
首先将Name不为null的Bone名称和索引的映射关系保存在boneMap中,对AnimationMan.x这个文件,保存的数据为(Body,0)和(Head,2)。
然后将根节点input的动画数据保存在rootAnimationClips中,其中调用的ProcessRootAnimation方法代码如下:
public static ModelAnimationClip ProcessRootAnimation(AnimationContent animation, string name)
{
List<Modelkeyframe> keyframes = new List<ModelKeyframe>();
// animation的类型为AnimationContent,它包含一个名为Channels、类型为AnimationChannelDictionary的
// 属性,AnimationChannelDictionary就是一个Dictionary,它的键是动画名称,值为AnimationChannel类型
// 的关键帧数据,而AnimationChannel继承自ICollection<AnimationKeyframe>。
// 根节点动画控制bone的根节点
AnimationChannel channel = animation.Channels[name];
// 填充当前根节点动画的关键帧
foreach (AnimationKeyframe keyframe in channel)
{
keyframes.Add(new ModelKeyframe(0, keyframe.Time, keyframe.Transform));
}
// 根据关键帧的时间进行排序
keyframes.Sort(CompareKeyframeTimes);
if (keyframes.Count == 0)
throw new InvalidContentException("Animation数据没有包含关键帧。");
if (animation.Duration <= TimeSpan.Zero)
throw new InvalidContentException("Animation的时间为零。");
return new ModelAnimationClip(animation.Duration, keyframes);
}
处理完根节点动画,然后处理子节点的动画。首先需要调用AddAnimationNodes方法将子节点包含动画名称放置在animationNames集合中,代码如下:
///<summary>
/// 获取根节点之下所有的动画名称
/// </summary>
static void AddAnimationNodes(List<string> animationNames, NodeContent node)
{
foreach (NodeContent childNode in node.Children)
{
// 如果节点动画数据不包含关键帧,则忽略这个这个节点
foreach (string key in childNode.Animations.Keys)
{
if (!animationNames.Contains(key))
animationNames.Add(key);
}
AddAnimationNodes(animationNames, childNode);
}
}
对于AnimationMan.x这个文件,获得的内容是(“TurnBodyHead”,”TurnHead”)。有了这个数据,我们就可以调用ProcessAnimation方法将子节点的动画数据保存在modelAnimationClips集合中,代码如下:
static ModelAnimationClip ProcessAnimation(
string animationName,
Dictionary<string, int> boneMap,
NodeContent input,
ModelContent model)
{
List<ModelKeyframe> keyframes = new List<ModelKeyframe>();
TimeSpan duration = TimeSpan.Zero;
AddTransformationNodes(animationName, boneMap, input, keyframes, ref duration);
// 根据关键帧的时间进行排序
keyframes.Sort(CompareKeyframeTimes);
if (keyframes.Count == 0)
throw new InvalidContentException("Animation数据没有包含关键帧。");
if (duration <= TimeSpan.Zero)
throw new InvalidContentException("Animation的时间为零。");
return new ModelAnimationClip(duration, keyframes);
}
在ProcessAnimation方法中,调用了AddTransformationNodes方法获取关键帧数据,代码如下:
/// <summary>
/// 获取子节点下所有动画的数据
/// </summary>
static void AddTransformationNodes(
string animationName,
Dictionary<string, int> boneMap,
NodeContent input,
List<ModelKeyframe> keyframes,
ref TimeSpan duration)
{
// 遍历所有子节点
foreach (NodeContent childNode in input.Children)
{
// 如果当前所处理的子节点不包含动画数据则不进行后继处理
if (childNode.Animations.ContainsKey(animationName))
{
AnimationChannel childChannel = childNode.Animations[animationName].
Channels[childNode.Name];
// 取动画播放时间的最大值作为播放时间
if (childNode.Animations[animationName].Duration != duration)
{
if (duration < childNode.Animations[animationName].Duration)
duration = childNode.Animations[animationName].Duration;
}
int boneIndex;
if (!boneMap.TryGetValue(childNode.Name, out boneIndex))
{
throw new InvalidContentException(string.Format(
"在bone '{0}'中找到动画,但它不是模型的一部分.",
childNode.Name));
}
foreach (AnimationKeyframe keyframe in childChannel)
{
keyframes.Add(new ModelKeyframe(boneIndex, keyframe.Time, keyframe.Transform));
}
}
AddTransformationNodes(animationName, boneMap, childNode, keyframes, ref duration);
}
}
至此整个处理过程分析完毕。
动画数据类库的变化
因为现在的关键帧中的变换是直接用Matrix表示的,所以动画数据类和播放类都要进行调整,为了便于使用,设置了几个文件夹,截图如下:

将适用于本文的动画数据类放置在ModelAnimationObject文件夹,动画播放类放置在AnimationPlayer文件夹中。
与上一篇文章的动画数据类区别不大,动画播放类更简单,因为它无需插值操作。代码较长就不列出了,请看源代码。
但这里有个区别值得一提,在上一篇文章中播放施加在模型内部的自定义动画数据的AnimationPlayer类的代码如下:
public class AnimationPlayer:AnimationPlayerBase
{
Model model;
Matrix[] originalModelTransforms;
public override void Update(GameTime gameTime)
{
[…]
int currentBoneIndex = CurrentClip.Keyframes[CurrentKeyframeIndex].BoneIndex;
model.Bones[currentBoneIndex].Transform = transform * originalModelTransforms[currentBoneIndex];
}
}
从代码中我们可以看到,需要引用模型和它的初始变换矩阵,然后用计算得出的transform矩阵乘以对应bone的初始变换矩阵,这个原理在第一篇文章中我们已经讲过了。
而这篇文章中播放施加在模型内部的自带动画数据的RigidAnimationPlayer类代码如下:
public class RigidAnimationPlayer : ModelAnimationPlayerBase
{
Model model;
protected override void SetKeyframe(ModelKeyframe keyframe)
{
this.model.Bones[keyframe.BoneIndex].Transform = keyframe.Transform;
}
}
我们发现这个类不需要模型原始变换矩阵的信息,直接将变换矩阵赋予要变换的Bone,这是因为在使用插件导出x文件时已经将原始变换矩阵的信息附加在变换矩阵上了。
是不是上一个类也可以采取同样的方法,答案是肯定的,我们将在下一篇文章中加以讨论。
在XNA程序中的使用
有了自定义动画处理器,我们需要在Content项目中添加对它的引用,然后手动将AnimationMan.x的处理器改为Animated Model Processor,运行程序后在Model的Tag属性中就包含了ModelAnimationData类型的动画数据。 因此首先在XNA程序中需要添加ModelAnimationData对象和播放类:
ModelAnimationData modelData; RigidAnimationPlayer rigidPlayer;
然后在LoadContent方法中进行初始化:
protected override void LoadContent()
{
[…]
// 加载动画数据
modelData = rigidModel.Tag as ModelAnimationData;
// 初始化动画播放类
rigidPlayer = new RigidAnimationPlayer(ref rigidModel);
}
在Update方法中添加控制代码:
protected override void Update(GameTime gameTime)
{
[…]
// 按数字键1、2、3播放不同的动画
if ((IsNewKeyPress(Keys.D1)))
{
rigidPlayer.StartClip(modelData.ModelAnimationClips["TurnBody"],true ,0.5f,0,16);
playingSkinned = false;
}
if ((IsNewKeyPress(Keys.D2)))
{
rigidPlayer.StartClip(modelData.ModelAnimationClips["TurnHead"], false, 1.0f);
playingSkinned = false;
}
if ((IsNewKeyPress(Keys.D3)))
{
rigidPlayer.StartClip(modelData.ModelAnimationClips["TurnBodyHead"], false, 1.0f);
playingSkinned = false;
}
rigidPlayer.Update(gameTime);
skinnedPlayer.Update(gameTime);
base.Update(gameTime);
}
在源代码中我还添加了播放蒙皮动画模型的代码,因为已经在以前的文章38.蒙皮动画模型内容处理器介绍过了,所以就不写教程了。
程序截图如下:

下一篇文章我们将对目前的代码作出优化。
文件下载(已下载 1129 次)发布时间:2011/9/9 上午7:09:44 阅读次数:8809
