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)模板,如下图所示:

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,我画了一张图显示了里面的关键数据:

input数据结构

 下面是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表示的,所以动画数据类和播放类都要进行调整,为了便于使用,设置了几个文件夹,截图如下:

AnimationClassLibrary类库

将适用于本文的动画数据类放置在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  阅读次数:8107

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

沪 ICP 备 18037240 号-1

沪公网安备 31011002002865 号