Animated Model Processor

本文介绍如何创建自定义模型处理器处理动画模型,提取骨骼和动画数据,并将其存储为易于使用的格式。文章详细介绍了重写处理方法、提取骨骼动画的过程,以及如何读写自定义数据。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Animated Model Processor

现在你需要创建一个新的模型处理器(model processor)扩展XNA默认的模型处理器。你将使用这个新模型处理器处理动画模型,提取骨骼和动画,并将它们存储为一个AnimatedModelData对象。

要创建一个新模型处理器你应创建一个叫做AnimatedModelProcessorWin的新的素材管道扩展库(Content Pipeline Extension Library)项目。这个库项目来自于一个新素材处理器类(content processor class),并会自动将素材管道组件(Content Pipeline assemblyMicrosoft.Xna. Framework.Content. Pipeline)添加到项目中。因为你将使用AnimatedModelContentWin库(上一部分建立的)存储动画数据,所以你还需要将这个库添加到项目中。代码如下:

[ContentProcessor]

public class ContentProcessor1 : ContentProcessor<TInput, TOutput>

{

    public override TOutput Process(TInput input, ContentProcessorContext context)

    {

        // TODO throw new NotImplementedException(); 

    }

默认素材处理器类扩展了ContentProcessor类,它作为任何素材管道处理器的基类,用来处理类型为Tinput的对象并输出为类型为TOutput的新对象。因为你对创建一个新的素材处理器不感兴趣,只想扩展某些功能,所以你应扩展一个已有的素材处理器而不是ContentProcessor类。在这里,你将扩展ModelProcessor类,它是默认的模型处理器类。你还要把你的新素材处理器类重命名为AnimatedModelProcessor。以下是代码:

[ContentProcessor]

public class AnimatedModelProcessor : ModelProcessor

{

    public static string TEXTURES_PATH = "Textures/"; 

    public static string EFFECTS_PATH = "Effects/"; 

    public static string EFFECT_FILENAME = "AnimatedModel.fx"; 

    

    public override ModelContent Process(NodeContent input, ContentProcessorContext context)

    {

        protected override MaterialContent ConvertMaterial( MaterialContent material, ContentProcessorContext context) 

        {

         ... 

         }

    }

}

ModelProcessor类中有很多方法可以重写,但处理动画模型只需重写ProcessConvertMaterial方法。主要方法是Process方法,这个方法需要将一个NodeContent对象——包含网格、骨骼和动画——转换为ModelContent对象——存储XNA模型对象的数据。除了Process方法,还要调用ConvertMaterial方法处理模型材质。

重写默认的处理方法

这部分你将重写ModelProcessor类的Process方法,这个方法用以处理模型。你还将创建两个新方法提取模型骨骼和动画:ExtractSkeletonAndAnimations方法和ExtractAnimations方法,ExtractAnimationsExtractSkeletonAndAnimations方法中调用。以下是代码:

public override ModelContent Process(NodeContent input, ContentProcessorContext context)

{

    // Process the model with the default processor 

    ModelContent model = base.Process(input, context); 

    

    // Now extract the model skeleton and all its animations 

    AnimatedModelData animatedModelData = ExtractSkeletonAndAnimations(input, context); 

    

    // Stores the skeletal animation data in the model 

    Dictionary<string, object> dictionary = new Dictionary<string, object>(); 

    dictionary.Add("AnimatedModelData", animatedModelData); 

    model.Tag = dictionary; 

    return model; 

}

Process方法开始,你调用它的基类ModelProcessor,接着,调用ExtractSkeletonAndAnimations方法处理输入的NodeContent并返回一个包含模型骨骼和动画的AnimatedModelData对象。最后,创建一个dictionary将一个字符映射到对象,将AnimatedModelData添加到这个dictionary,将这个dictionary设置在ModelContent对象的Tag属性中。XNAModel类有一个Tag属性可以将自定义的数据添加到模型中。使用dictionary作为Tag属性,你可以将不同的自定义对象添加到Model类中,并可以实时通过使用string查询到这些对象。

注意你设置在ModelContent对象Tag属性中的数据会一起存储在二进制的XNB文件中,当使用content manager载入模型时这些数据会重新还原。

提取模型骨骼

ExtractSkeletonAndAnimations方法将一个root NodeContent对象作为输入,这个对象可能包含MeshContentBoneContent作为它的子节点(children)。要提取模型骨骼,你首先要在root NodeContent中找到骨骼的root bone,任何执行深度搜寻depth traverse,创建bone的集合。XNAMeshHelper类提供了一些方法帮你完成这个处理过程:

// Find the root bone node 

BoneContent skeleton = MeshHelper.FindSkeleton(input); 

// Transform the hierarchy in a list (depth traversal) 

IList<BoneContent> boneList = MeshHelper.FlattenSkeleton(skeleton);

你可以使用MeshHelper类的FindSkeleton方法找到骨骼的root bone。然后你需要使用深度搜索将骨骼树转换为集合。可以使用MeshHelper类的FindSkeleton方法做这件事。结果是bone的集合,每个boneBoneContent类的一个对象。注意在集合中的bone的顺序和网格顶点的索引顺序是相同的。

对集合中的每个bone,你将它的本地配置(local configuration)、反绝对配置(inverse absolute configuration)和父索引存储在bind pose中。你可以从BoneContent对象的Transform AbsoluteTransform属性中读取本地配置和绝对配置,你可以使用XNAMatrix类的Invert方法计算反绝对配置。

bonesBindPose[i] = boneList[i].Transform; 

bonesInverseBindPose[i] = Matrix.Invert(boneList[i].AbsoluteTransform);

以下是ExtractSkeletonAndAnimations 方法的完整代码:

private AnimatedModelData 

ExtractSkeletonAndAnimations(NodeContent input, ContentProcessorContext context)

{

    // Find the root bone node BoneContent 

    skeleton = MeshHelper.FindSkeleton(input); 

    

    // Transform the hierarchy in a list (depth traversal) 

    IList<BoneContent> boneList =MeshHelper.FlattenSkeleton(skeleton); 

    context.Logger.LogImportantMessage("{0} bones found.", boneList.Count); 

    

    // Create skeleton bind pose, inverse bind pose, and parent array 

    Matrix[] bonesBindPose = new Matrix[boneList.Count]; 

    Matrix[] bonesInverseBindPose = new Matrix[boneList.Count]; 

    int[] bonesParentIndex = new int[boneList.Count]; 

    List<string> boneNameList = new List(boneList.Count); // Extract and store the data needed from the bone list for (int i= 0; i< boneList.Count; i++) { bonesBindPose[i] = boneList[i].Transform; bonesInverseBindPose[i] = Matrix.Invert(boneList[i].AbsoluteTransform); int parentIndex =boneNameList.IndexOf(boneList[i].Parent.Name); bonesParentIndex[i] = parentIndex; boneNameList.Add(boneList[i].Name); } // Extract all animations AnimationData[] animations = ExtractAnimations( skeleton.Animations,boneNameList, context); return new AnimatedModelData(bonesBindPose, bonesInverseBindPose, bonesParentIndex, animations); }

在提取模型骨骼后,你需要调用ExtractAnimations方法提取模型动画。

提取模型动画

所有模型动画存储在一个动画dictionary中,这个dictionary映射到一个包含指向一个AnimationContent对象的动画名称、动画数据的字符串。你可以从模型骨骼的root node(类型为BoneContent)的Animations属性中获得动画dictionary。注意素材管道有自己的类存储模型动画数据:AnimationContentAnimationChannelAnimationKeyframe类。AnimationContent类存储以一个AnimationChannel对象的数组的形式存储一个完整的模型动画,每个AnimationChannel对象以AnimationKeyframe对象数组的形式存储单个bone的动画。当你将bone一起存储在单个数组中时,XNAAnimationContent类也会单独存储每个bone的动画。

你可以遍历动画dictionaryAnimationContent对象提取模型动画,对每个找到的动画你需要遍历它们的bone channels(可以从Channels属性中获得),提取所有动画关键帧(可以从Keyframes属性中获得)。下面是ExtractAnimations方法的代码:

private AnimationData[] ExtractAnimations( AnimationContentDictionary animationDictionary, 

        List<string> boneNameList, ContentProcessorContext context) 

{

    context.Logger.LogImportantMessage("{0} animations found.",animationDictionary.Count); 

    AnimationData[] animations = new AnimationData[animationDictionary.Count]; 

    int count = 0; 

    foreach (AnimationContent animationContent in animationDictionary.Values)

    {

        // Store all keyframes of the animation 

        List<Keyframe> keyframes = new List<Keyframe>(); 

        

        // Go through all animation channels 

        // Each bone has its own channel 

        foreach (string animationKey in animationContent.Channels.Keys)

        {

            AnimationChannel animationChannel =animationContent.Channels[animationKey]; 

            int boneIndex = boneNameList.IndexOf(animationKey); 

            foreach (AnimationKeyframe keyframe in animationChannel) 

                keyframes.Add(new Keyframe( keyframe.Time, boneIndex, keyframe.Transform)); 

        }

        

        // Sort all animation frames by time 

        keyframes.Sort(); 

        animations[count++] = new AnimationData(animationContent.Name, animationContent.Duration, keyframes.ToArray());

    }

    return animations;

}

当存储了动画的所有关键帧后,你需要将它们排序。因为关键帧是存储在一个List中的,所以你可以使用Sort方法。别忘了你前面在Keyframe类中实现了Icomparable接口,可以使用它们的time属性对关键帧进行排序。

现在你提取了模型骨骼和动画并将它们存储在一个友好的格式中,下面准备写入XNB文件。注意因为List泛型类和Icomparable接口是由.NET Framework而不是XNA提供的,所以你可以在C# 帮助文件中它们的相关信息。

读取和写入自定义数据

你创建的用来存储模型骨骼动画数据的AnimatedModelProcessor使用自定义的对象(AnimatedModelDataAnimationDataKeyframe类)。素材管道需要从二进制文件中读取和写入这些对象,但素材管道不知道如何读取和写入这些自定义对象。

要定义如何读取骨骼动画数据和写入至二进制文件中,你需要为每个用来存储骨骼动画数据的类创建一个content type reader和一个content type writer。这里,你需要为AnimatedModelDataAnimationDataKeyframe类创建一个content type reader和一个content type writer,你可以通过扩展XNAContentTypeReaderContentTypeWriter类创建content type readercontent type writer

Content Type Writer

要创建content type writer你需要在AnimatedModelProcessorWin项目中添加一个名为AnimatedModelDataWriter的新Content Type Writercontent type writer类只需添加到model processor项目中。你要在content type writer中添加三个新类:KeyframeWriterAnimationDataWriterAnimatedModelDataWriter类,这三个类用来为KeyframeAnimationDataAnimatedModelData类写入数据。每个类都需要扩展ContentTypeWriter类并重写Write方法。

ContentTypeWriter类的Write方法接受两个参数。第一个是ContentWriter,用来将对象的数据写入二进制文件,第二个参数是要写入的对象。在Write方法内,你应使用ContentWriter对象写入所有类中的属性。注意对象的写入顺序是很重要的,它们必须和读取的顺序相同。以下是KeyframeWriterAnimationDataWriterAnimatedModelDataWriter类的代码:

[ContentTypeWriter]

public class KeyframeWriter : ContentTypeWriter<Keyframe>

{

    protected override void Write(ContentWriter output, Keyframe value) 

    {

        output.WriteObject(value.Time); 

        output.Write(value.Bone); 

        output.Write(value.Transform);

    }

    

    public override string GetRuntimeReader(TargetPlatform targetPlatform) 

    {

        return typeof(KeyframeReader).AssemblyQualifiedName;

    }

}

[ContentTypeWriter]

public class AnimationDataWriter : ContentTypeWriter<AnimationData>

{

    protected override void Write(ContentWriter output, AnimationData value) 

    { 

        output.Write(value.Name); 

        output.WriteObject(value.Duration); 

        output.WriteObject(value.Keyframes); 

        

        public override string GetRuntimeReader(TargetPlatform targetPlatform)

        {

            return typeof(AnimationDataReader).AssemblyQualifiedName; 

        }

    }

}

[ContentTypeWriter] 

public class AnimatedModelDataWriter : ContentTypeWriter<AnimatedModelData>

{

    protected override void Write(ContentWriter output, AnimatedModelData value)

    {

        output.WriteObject(value.BonesBindPose); 

        output.WriteObject(value.BonesInverseBindPose); 

        output.WriteObject(value.BonesParent); 

        output.WriteObject(value.Animations); 

    }

    

    public override string GetRuntimeReader(TargetPlatform targetPlatform) 

    {

        return typeof(AnimatedModelDataReader).AssemblyQualifiedName; 

    }

}

ContentType Reader

要创建content type reader你需要在AnimatedModelProcessorWin项目中添加一个名为AnimatedModelDataReader的新Content Type Reader。与content type writer类不同,游戏程序需要只需content type reader实时加载动画数据。 你需要创建三个新的类-KeyframeReaderAnimationDataReaderAnimatedModelDataReader-它们用来读取KeyframeAnimationDataAnimatedModelData类的数据。每个类都需要扩展ContentTypeReader 类并重写Read方法。

ContentTypeReader类的Read方法接受两个参数。第一个是ContentReader,用来从二进制文件读取对象数据,第二个参数是指向对象示例的引用。因为你还没创建对象,所以第二个参数总是null。再次注意读取对象的顺序应与写入的顺序相同。下面是KeyframeReaderAnimationDataReaderAnimatedModelDataReader类的代码:

public class KeyframeReader : ContentTypeReader<Keyframe>

{

    protected override Keyframe Read(ContentReader input, Keyframe existingInstance)

    {

        TimeSpan time = input.ReadObject<TimeSpan>(); 

        int boneIndex = input.ReadInt32(); 

        Matrix transform = input.ReadMatrix(); 

        return new Keyframe(time, boneIndex, transform); 

    }

}

        

public class AnimationDataReader : ContentTypeReader<AnimationData>

{

    protected override AnimationData Read(ContentReader input, AnimationData existingInstance) 

    {

        string name = input.ReadString(); 

        TimeSpan duration = input.ReadObject<TimeSpan>(); 

        Keyframe[] keyframes = input.ReadObject<Keyframe[]>(); 

        return new AnimationData(name, duration, keyframes);

    }

}

        

public class AnimatedModelDataReader :ContentTypeReader<AnimatedModelData>

{

    protected override AnimatedModelData Read(ContentReader input, AnimatedModelData existingInstance)

    {

        Matrix[] bonesBindPose = input.ReadObject<Matrix[]>(); 

        Matrix[] bonesInverseBindPose = input.ReadObject<Matrix[]>();

        int[] bonesParent = input.ReadObject<int[]>(); 

        AnimationData[] animations =input.ReadObject<AnimationData[]>(); 

        return new AnimatedModelData(bonesBindPose, bonesInverseBindPose, bonesParent, animations); 

    }

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值