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