第12章 骨骼动画

[color=red]书<<Beginning.XNA.3.0.Game.Programming.From.Novice.to.Professional>>第12章,图片未贴出,原译文在附近中[/color]


[b]骨骼动画[/b]
虽然有些场景大部分由静态物体构成,可能你想在游戏中为角色或npc使用一些动画模型.你可以通过多种方式来创建动画模型.比如说,一个赛车游戏,车辆可以是一个动画模型因为车轮的转动及车辆的移动.你可以只通过寻找符合车轮的网格(mesh)并让其绕自己的轴旋转来形成此类型的简单动画.但,当你需要创建一个动画主角(跑,跳,落下等),这种类型的动画就复杂多了.这是因为你需要修改角色(被称为蒙皮的)网格(mesh).本章关注角色动画技术.让我们先看看2种主要类型的动画.
动画类型
图12-1展示了角色行走的动画序列.共有5帧.每帧表示不同的角色结构.每个动画帧都有一定时间,定义了多长时间后角色结构发生变化.最终,循环播放形成动画,第一帧和最后一帧必须相同.
有2种不同类型的动画:帧动画和骨骼动画.分别适用于不同的情况并有自己的优势和不足.
图12-1.在此角色行走动画中,角色网格每一帧都会改变.
关键帧动画
在关键帧动画中,需要为每一帧保存一个静态的模型网格,如果你打算做图12-1类似的动画,你将需要导出4个不同静态的网格(第5个与第1个一样).之所以称为帧动画是因为只有帧才有变化-关键帧被导出.
12-1所示动画,为了平滑效果你不得不在2帧之间加入一些缓冲,(缓冲:参考2帧间生成中间帧),但不必自己动手创建所有的帧,因为你可以通过在首末2帧间插值来得到它们.比如,使用线性插值,首末2帧网格中每个顶点的位置都将被计算.
当你载入动画模型,在储存了少量关键帧的模型上,为了更平滑你可以创建更多的帧,并将这些存放在内存中.以准备用来渲染动画.
关键帧动画的优点是速度快,因为所有的插值计算都是提前完成的.所以的动画帧都被储存在内存中,动画过程中你只需在每一帧切换到下一个网格.而不足之处此方法需要在内存中储存模型的所有网格.如果模型需要100个动画帧,它需要储存100次网格.
在一个有上百个不同模型的场景中,当它们共享相同的动画,关键帧动画很有用.在一个有不同模型使用不同动画的场景中,关键帧动画需要太多的内存.
在xna中使用关键帧动画很容易,因为xna已经有处理静态模型的类.你可以使用Model类来将关键帧动画视为静态模型数组来处理.为每一动画帧储存一个模型对象.
骨骼动画
另一个处理模型动画的方式是骨骼动画.此过程中,骨骼模型是按骨来构成的,模型中每一个可以运动的部分都有一个骨骼:一个骨骼处理肩膀,一个处理上臂,一个处理前臂,一个处理手部,及手指.
每一个骨骼需要知道它连接在哪一个父骨骼上及如何连接在那,骨骼可以绕此点旋转.模型的所有顶点都属于骨骼.它们的位置是按骨骼关系来定义.如,旋转一个骨骼将旋转附属在骨骼上的所有顶点.
骨骼动画也是按关键帧方式来工作,每一帧,所有旋转的骨骼被储存(与关键帧动画储存所有的顶点不同).这样为了完成关键帧动画的功能,你需要对各个骨骼之间旋转角度进行插值.
为了创建模型网格,骨骼及动画你可以使用不同的支持骨骼模型工具,如3ds max,maya,blender等.建模完成后,还需要将它们导出一个支持骨骼的格式.xna支持X(DirectX)和FBX(Autodesk)格式都支持骨骼.图12-2说明模型网格和骨骼.
骨骼动画比关键帧动画有优势.它允许动画简单混合,这样同一时间你可以应用不同的动画.如你可以将2个不同的动画应用到模型上如同12-2,一个动画让模式行走(旋转腿部骨骼),及另一个动画将使模型环绕看(旋转颈部骨骼).在关键帧动画,你只能一个动画行走另一个动画旋转头部,但如果你不清楚哪些顶点属于腿部和头部,很难组合它们.
骨骼动画也允许从一个骨骼对象连接到另一个对象上.如,如果你有一个角色捡起一把剑,你将剑的骨骼(从剑是一个可移动的物体,它便是一个骨骼)连接到角色的手部骨骼上,这可以使剑随角色的手部移动.
当今,骨骼动画比关键帧动画的应用更广泛.记住本章我们研究骨骼动画.
Xna本身不支持骨骼动画.默认的模型处理器内容管道(ContentPipeline)提取了模型的顶点和骨骼,但丢弃了动画数据.

图12-2 模型网格和骨骼
骨架和骨骼的表现
当你在xna中应用骨骼之前,你应该明白骨骼模型的结构及他们如何被展现和储存的.
有2中不同的方式来储存模型的骨骼:使用骨骼(bones)和使用关节(joints).如,3ds Max使用骨骼来表现骨架,Maya使用关节来表现骨架.但模型导出为xna兼容的格式时(X或FBX),就没什么不同了,骨架都是用骨骼来表现的.
因此,你将使用骨骼来表现及储存骨架.每一个骨骼储存了初始位置和旋转,定义了位置及如何附属到父骨骼上.也储存了它的尺寸,尺寸是指骨骼的当前位置到其子骨骼位置的距离.所以骨骼必须有一个终端骨来确定骨架的结束.
图12-3使用骨架来表示上臂骨架.注意手骨之后必须有一个终端骨来确定手骨的尺寸和上臂骨架的结束.再注意,手指不能分开移动,因为他们没有自己的骨骼.最后,每个模型有一个根(root)骨,这是模型的主要部分.作为根也是一个骨骼,它还是一个单独的,不可移动的部分.对于角色根应该是 躯干.
每个骨骼的位置和方位关系到它的父骨.如手骨的方位和位置是根据前臂的位置和方位来定义的,而前臂的位置和方位是由上臂来定义的,重复这个步骤直到到达根骨.有了这个概念你可以修改任何骨骼以影响它的后面关联的骨骼.如果左肩膀骨移动或旋转,它的后代骨骼都将移动或旋转.
为了储存骨架,你需要储存每个骨骼信息(位置和方位)及它们在骨架中的层次.当你渲染模型时层次是必须的,因为那时你需要找到每个顶点的绝对3d位置,我们可以简单的称之为绝对位置(absolute position).如,前臂顶点的绝对位置是从原点处的顶点.乘上前臂骨骼信息,上臂骨骼信息,左肩旁骨骼信息和根骨骼信息.方便的是只要每个骨骼都储存了自己的信息,xna将为你计算绝对位置.
骨骼的信息储存为一个矩阵.骨架的层次被存为一个骨骼列表,每一个矩阵连接到它的父骨骼.

图12-3.手臂骨架.层次从根开始,到终端骨结束,每一个骨骼都是前一个骨骼的后代.所有骨骼的开始位置画为正方形,并且它们在下一个骨骼处结束(下一个正方形)
关于变形一词
每个骨骼都需要储存位置(连接到父骨骼的哪里)及旋转(如何连接到那里).10章的'物体变形'一节解释是一个矩阵完全的保存了位置和旋转的组合,这也是为什么每个骨骼要储存一个矩阵(指明它的父骨骼).另一个重要的原因是为了储存这些信息所以用一个矩阵.
为了解释这个原因,思考下你如何来渲染角色的手部.这要求你知道手部每个顶点的绝对位置.手部的每个顶点的位置是相对于手骨作为原点的位置(见12-3).为了获取每个顶点的绝对3d位置,你将需要使用手部矩阵完成第一个变换.这个操作会相当的费力,因为需要变化每一个顶点.
方便的是,矩阵一个数学属性是通过乘上不同的矩阵,最后的矩阵将包含所有的矩阵变换.在你渲染手部之前,你首先将通过乘上所有的父骨骼矩阵来计算总(绝对)矩阵.这样,xna只需用一个矩阵来变化所有手部顶点.
为模型动画扩展内容管道
Xna有一个定义良好的内容管道,用来从硬盘载入你的素材(图形,模型,声音等).内容管道被分离进不同的层次,划分为2个阶段:
1从硬盘读取元素的素材,处理并储存生成的二进制文件.这只在编译阶段运作.
2从二进制载入处理过的数据,直接在你游戏中使用.每次运行游戏时便会执行.
这样划分的利益是双重的.第一,它确保了繁重的计算工作不需要每次都重复.第二,二进制文件pc,xbox360,zune都可以读.普通的素材不能跨平台,当是二进制文件时就可以跨平台了.
图12-4说明了内容管道如何导入,处理,序列化(写二进制),及反序列化(读二进制)模型文件.第一阶段中,模型由对应的模型导入器导入,每个模型导入器将模型的数据转换为xna文档对象模型(DOM)格式.输出的模型导入器是一个根NodeContent对象,它描述了一个有自己的坐标系及可以加入子对象的图形类型.2个类继承NodeContent:MeshContent和BoneContent.这样从模型导入器输出的根NodeContent对象就可以用一些NodeContent,MeshContent,BoneContent的子对象了.

图 12-4.Xna内容管道-用于导入,处理,编译及读取游戏模型的类
模型被导入后,它们可以被相应的内容处理器处理,如ModelProcessor.为导入X和FBX你可以定义2个导入器,但只用ModelProcessor来处理导入器的输出.
模型处理器接收一个由模型导入器生成的根NodeContent的参数,返回一个ModelContent对象.默认的模型处理器返回默认的ModelContent对象.其中包含顶点,骨骼数据但没动画信息.
第一阶段的终点,被处理的数据储存进XNA二进制文件.为了将ModelContent对象储存进XNB文件,ModelContent及里面的每个对象必须有自己的ContentTypeWriter.ContentTypeWriter定义了如何将ModelContent里面的数据写入XNB文件.
第二阶段运行时,ContentManager读取二进制XNB文件及为XNB文件中的每个对象使用正确的ContentTypeReader.
因为xna内容管道没有支持有骨骼动画的模型,你需要扩展内容管道,加入骨骼动画的支持.注意内容管道支持部分的骨骼动画,因为它可以从x,fbx中导入骨骼信息,但没处理包含在其中的骨骼信息.
为了加入骨骼动画,你需要扩展默认的模型处理器,使它处理并储存模型的骨骼和动画.为此,你需要创建几个类来存放骨骼动画数据(每个模型的骨骼和动画).因为xna不知如何来序列和反序列自定义的类,你将需要为每个自定义的类定义一对ContentTypeWriter及ContentTypeReader.
图12-5说明了你需要创建扩展内容管道的类和加入动画骨骼功能需要创建的类.

图12.5 内容管道扩展,用来支持骨骼动画
创建动画数据类
你将建立存储骨骼动画数据的类,在动画模型处理时储存动画数据,在运行时载入数据.
创建一个WindowsGamelibrary的项目命名为AnimationModelContentWin.模型处理器将使用这个库中的类在windows平台储存骨骼动画数据.如果你的游戏面向的是Windows平台,这个库将在运行时载入骨骼动画数据.
如果你的目标是xbox360,你需要创建一个Xbox360GameLibrary命名AnimationModelContentXbox.与库AnimationModelContentWin的文件相同,但Xbox360应用用它在运行时载入骨骼动画.不管怎样你都需要AnimationModelContentWin工程甚至你是xbox360平台,因为导入和处理的原始模型文件是在windows下,这个功能包含那些类的定义.
需要创建下面的类:
1Keyframe,储存骨骼动画的动画帧,每一个动画帧储存骨架中骨骼的信息.
2AnimationData,储存keyframe数组,用来组成完成的动画(如跑动,跳跃等)
3AnimatedModelData,储存蘑菇骨骼(骨骼和层次)及AnimationData数组,包含所有的骨骼动画.
创建Keyframe
Keyframe的职责是为骨架中骨骼储存一个动画帧.动画帧必须关联到动画骨骼,新的信息(位置和方位)关联到动画骨骼,以及新的信息应该被接受的时间.注意你使用关键帧来改变原始骨骼信息,改变它当前的信息形成新的信息.用xna的Matrix类来储存骨骼信息,及用TimeSpan来储存动画时间(关键帧应该被播放的时间).
还需要储存骨骼的id,以便在AnimatedModelData类bones数组进行引用.Keyframe类代码如下:
public class Keyframe : IComparable
{
int boneIndex;
TimeSpan time;
Matrix transform;
// Properties...
public TimeSpan Time
{
get { return time; }
set { time = value; }
}
public int Bone
{
get { return boneIndex; }
set { boneIndex = value; }
}public Matrix Transform
{
get { return transform; }
set { transform = value; }
}
public Keyframe(TimeSpan time, int boneIndex, Matrix transform)
{
this.time = time;
this.boneIndex = boneIndex;
this.transform = transform;
}
public int CompareTo(object obj)
{
Keyframe keyframe = obj as Keyframe;
if (obj == null)
throw new ArgumentException("Object is not a Keyframe.");
return time.CompareTo(keyframe.Time);
}
}
在Keyframe类中,为了进行Keyframe对象的比较实现了接口IComparable.你将在c#的排序功能中使用此比较功能,根据帧的时间来排序.为了实现IComparer接口,需要加入CompareTo方法.此方法接收一个要与当前对象进行比较的对象,若当前比参数对象大则返回1,否则返回-1,相同返回0.Keyframe对象用time属性来进行比较.
创建AnimationData类
AnimationData类的职责是储存完整的模型动画(如跑动,跳动,等).将动画储存为Keyframe数组,除此之外还需要储存其他的有用信息,如动画名称及时间.AnimationData代码:
public class AnimationData
{
string name;
TimeSpan duration;
Keyframe[] keyframes;
public string Name
{
get { return name; }
set { name = value; }
}
public TimeSpan Duration
{
get { return duration; }
set { duration = value; }
}
public Keyframe[] Keyframes
{
get { return keyframes; }
set { keyframes = value; }
}
public AnimationData(string name, TimeSpan duration,
Keyframe[] keyframes)
{
this.name = name;
this.duration = duration;
this.keyframes = keyframes;
}
}
创建AnimatedModelData类
AnimatedModelData类职责是储存模型骨架和动画.将模型骨架储存为骨骼的数组,每个骨骼以矩阵的形式来表现.通过深度遍历模型骨架的方式来创立骨骼数组.此深度遍历从根骨骼开始直到最深层次的骨骼,所有骨骼遍历完毕按原路返回.图12-6显示的深度遍历,返回根骨骼,左肩膀,左前臂,左手,左终端骨,右肩膀,右前臂,右手,右终端骨.

图12-6 骨架层次示例
骨架的骨骼储存在凝固姿势(bind pose)的结构中.凝固姿势是,那些连接到模型网格骨骼准备开始进行动画的姿势.当模型没有动画或动画刚开始,模型的所有骨骼处于凝固姿势.
在AnimatedModelData类中,你应该创建2个Matrix数组来储存骨架的骨骼,1个骨架骨骼层次的int数组,一个用来储存模型动画的AnimationData数组.
AnimatedModelData代码如下:
public class AnimatedModelData
{
Matrix[] bonesBindPose;
Matrix[] bonesInverseBindPose;
int[] bonesParent;
AnimationData[] animations;
// Properties ...
public int[] BonesParent
{
get { return bonesParent; }
set { bonesParent = value; }
}
public Matrix[] BonesBindPose
{
get { return bonesBindPose; }
set { bonesBindPose = value; }
}
public Matrix[] BonesInverseBindPose
{
get { return bonesInverseBindPose; }
set { bonesInverseBindPose = value; }
}public AnimationData[] Animations
{
get { return animations; }
set { animations = value; }
}
public AnimatedModelData(Matrix[] bonesBindPose,
Matrix[] bonesInverseBindPose, int[] bonesParent,
AnimationData[] animations)
{
this.bonesParent = bonesParent;
this.bonesBindPose = bonesBindPose;
this.bonesInverseBindPose = bonesInverseBindPose;
this.animations = animations;
}
}
在AnimatedModelData类,bonesBindPose数组包含了每个骨骼凝重姿势的局部结构(关联到它的父骨骼).bonesInverseBindPose数组包含了每个骨骼在凝固姿势时的反向绝对结构(绝对是指针对于3d世界而不是它的父骨骼).bonesParent属性储存了每个骨骼的父骨骼索引.最好animations储存了模型的动画.
你使用骨骼的反向绝对结构来将连接到骨骼的所有顶点从模型坐标系转换到当前骨骼的坐标系,其中需要使用变换动画.在骨骼动画表达式一节中详述.
创建动画模型处理器
现在准备创建自己的动画模型管道.你将通过扩展xna默认模型处理器来创建一个新的模型处理器.你将使用新处理器来接收导入器的生成的输出,提前骨骼和动画并将其储存在AnimatedModelData对象中.
注意:如果我没仔细阅读你可能会想,是否我需要写一个新的导入器?这是不需要的因为默认的x和fbx导入器同样提取了动画数据.
为新的模型处理器创建一个新的内容管道扩展库(Content Pipeline Extension Library)项目命名为:AnimatedModelProcessrorWin.内容管道扩展库新建时会为你加入一个新的内容处理类.并将需要的组装件(assembly)加入了(Microsoft.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类型的对象.但是记住现在我们不是创建新的内容处理器只需继承一个已有的处理器即可.因此,你必须扩展一个已有的内容处理器.现在你需要继续xna的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类有多个可以重写的放,本例中只需要重新Process和ConvertMaterial方法.Process方法是住方法,用来处理模型.此方法将NodeContent(具有网格,骨骼和动画)转换为ModelContent对象并储存在Model对象中.在此过程中,调用ConvertMaterial方法来处理模型的材质.
重写默认的Process方法
本节中,你将重写ModelProcessor的用来处理模型的Process方法.此外你还需要创建2个新方法用来提取模型的骨骼和动画,分别名为:ExtractSkeletonAndAnimations和ExtractAnimations,
ExtractSkeletionAndAnimations里调用ExtractAnimations方法.新的Process方法如下:
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方法开始处,你调用base.Process(),将NodeContent转换成标准的ModelContent对象,这包含所有的顶点,效果,纹理,及骨骼信息.
接下来,你调用ExtractSkeletonAndAnimations方法,它执行NodeContent对象的第二次处理并返回包含骨骼和动画的AnimatedModelData对象.最后,创建一个字典将字符串和对象关联起来,将AnimatedModelData加入到字典,并将其储存在ModelContent对象的Tag属性中.XNA的Model类有一个Tag属性,它允许将自定义的数据加入到模型.将字典作为Tag属性,你可以给Model类添加多个自定义对象,并在运行时使用字符串来查询.
注意储存在ModelContent.的Tag属性中的数据稍后会一起随模型数据储存在二进制的XNB文件中.当模型被内容控制器载入时,数据被恢复.
提取模型的骨骼
ExtractSkeletonAndAnimations方法接收根NodeContent对象作为输入,可能根中有MeshContent和BoneContent对象,目前还谈之过早.为了提取模型的骨骼,你首先需要找到骨架的根骨骼,它在根NodeContent中,接着需要深度遍历骨架,创建一系列骨骼.Xna的MeshHelper类提供了一些帮助方法:
// 找到根骨骼节点
BoneContent skeleton = MeshHelper.FindSkeleton(input);
// 将层次转换为列表(深度遍历)
IList<BoneContent> boneList = MeshHelper.FlattenSkeleton(skeleton);
你可以使用MeshHelper的FindSkeleton方腊找到骨架的根骨骼.接着需要将骨架树使用深度搜索转为列表(list),使用MeshHelper类的FlattenSkeleton方法来完成此操作.结果为BoneContent类的骨骼对象的列表,注意此列表中的骨骼顺序与网格顶点索引顺序一致.
创建的列表中的每个骨骼你都希望在凝固姿势中储存自身的配置,并且它在凝固姿势中是反转的绝对的配置,并是父骨骼的索引.你可以通过属性Transform和AbsoluteTransform来获取骨骼的自身和绝对的配置,并且你可以使用Matrix.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<string>(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方法来提取骨骼的动画,下节详述.
提取骨骼动画
导入器已经字典的方式储存了模型动画,key为动画名称,value为包含动画数据AnimationContent对象.你可以访问从模型骨架的BoneContent根节点访问Animations属性来获取动画字典.注意模型管道有自己的类来存储模型动画数据:AnimationContent,AnimationChannel,及AnimationKeyframe.
AnimationContent类以AnimationChannel数组的形式储存了完整的模型动画,每一个AnimationChannel对象以AnimationKeyframe数组的形式储存了单个骨骼的动画.当你将他们储存在单个数组中时,xna的AnimationContent类储存分别储存了每个骨骼的动画.
下面是提高动画的通常的几个步骤:
1 从动画字典中遍历AnimationContent对象,其包含完成的动画如走动和跳跃
2通过Channels属性遍历完整的动画通道.
3通过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中并且你的KeyFrame类实现了IComparable接口,你可以用Sort方法来将他们进行排序.记住KeyFrame类实现IComparable接口是按KeyFrame的time属性来进行排序的.
此时,你已经将模型骨骼和动画提取出来并储存成一个友好的格式,便于从xnb文件写入或读取.
注意 你可以通过c#的帮助文件来查看泛型的List.因为它是.net框架的类而非xna.
读写自定义的数据
刚才创建的AnimatedModelProcessor使用了一些自定义的用户对象(AnimatedModelData,AnimationData,和Keyframe类).如之前解析的,内容管道需要读写从2进制中读写这些对象,但内容管道却不知道如何来读写这些对象.
为了定义骨架动画数据如何从二进制中进行读写你必须为骨架动画的每个自定义类定义一个内容类型读取器和内容类型写入器.本例中你必须为AnimationModelData,AnimationData,Keyframe创建一个新的内容类型读取器和新的内容写入器.可以通过几次CotentTypeReader和ContentTypeWriter来创建读入器和写入器.
创建内容类型写入器
为了创建内容类型写入器,在AnimatedModelProcessor工程中添加一个新的空文件,命名为AnimatedModelDataWriter.你需要加入3个新类到内容类型写入器文件:KeyframeWriter,AnimationDataWriter,和AnimatedModelDataWriter类,用他们来告诉xna如何来序列化Keyframe,AnimationData和AnimatedModelData类.这些类都需要继承ContentTypeWriter并重新Write方法.
ContentTypwWriter的Write方法接收2个参数.第一个是ContentWriter对象,使用其将对象的数据写入到2进制文件,第2个是被写入的对象.必须使用ContentWriter来序列化类的所有属性.注意将对象写入2进制的顺序是很重要的,写入顺序必须和读取的顺序一致.下面是KeyframeWriter,AnimationDataWriter及AnimatedModelDataWriter的代码:
[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;
}
}
确认每个写入器都写入了需要的数据.更多是,每个写入器都应该提供一个GetRuntimeReader方法,指明相应的类型读取器的名字,此读取器可以反序列化数据并恢复为对象.
这个名字应该唯一定义一个类型读取器,它包含命名空间,版本号,文化等.如一个完整的字符串来表明类型读取器 AnimatedModelContent.AnimatedModelDataReader,AnimatedModelContentWin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null,是通过类型读取器的AssemblyQualifiedName属性获取到的.
创建内容类型读取器
在AnimatedModelContentWin工程加入一个新的空文件,命名为AnimatedModelDataReader.不像内容类型写入器,写入器工作在编译时,游戏应用需要在运行时调用内容读取器.
像定义你的内容写入器一样,你需要创建3个类:KeyframeReader,AnimationDataReader及AnimatedModelData.每一个都需要继承ContentTypeReader并重写Read方法.
ContentTypeReader类的Read方法接收2个参数.第1个是ContentReader,被用于从2进制文件中读取对象数据,第2个参数是对象实例的一个引用.在你创建对象之前第2个参数始终是null.再次注意,Read放读取的顺序必须和写入器的写入对象的顺序一致.下面是KeyframeReader,AnimationDataReader及AnimatedModelDataReader类:
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);
}
}
在xna中使用AnimatedModel类
本节,你将创建类用来在运行时接收从内容管道传来的骨架动画.将其命名为AnimatedModel,将提供载入动画模型,播放并更新动画,绘制动画.通过定义它的属性来创建AnimatedModel类.
动画模型如xna的Model对象有一样被载入,在Tag属性中储存了一个字典,字典包含有AnimatedModelData对象.这样,Model类包含了模型的网格和效果,AnimatedModelData类包含了模型的骨骼和动画.你声明Model类型的model属性和AnimatedModelData类型的animatedModel属性,并且将模型的世界变换(包含它的位置,旋转及在绝对3d中缩放)储存进Transformation中.
Model model;
AnimatedModelData animatedModelData;
Transformation transformation;
你继续声明一些属性来处理如何复制动画.声明activeAnimation属性来储存当前正播放的动画,activeAnimationKeyframeIndex和activeAnimationTime属性来分别储存当前动画的帧,时间:
AnimationData activeAnimation;
int activeAnimationKeyframe;
TimeSpan activeAnimationTime;
还需要声明2个属性以便可以配置动画的速度及循环.它们是enableAnimationLoop和animationSpeed属性:
bool enableAnimationLoop;
float animationSpeed;
在骨骼模型中,还需要一些临时的矩阵数组来储存当前骨架的骨骼配置.声明bones属性来储存当前每个骨骼的配置,因为骨骼配置会在动画播放后被修改.声明bonesAbsolute属性来储存每个骨骼的绝对配置,运行时计算bones数组和需要进行动画的骨骼.最后,声明bonesAnimation属性来储存每个骨骼的最终变换,这需要组合变化,而组合变换需要将顶点放置在骨骼的坐标系并使用每个骨骼的绝对配置来进行动画.(我们将在"骨骼动画表达式"节中解释骨骼动画的细节)
Matrix[] bones;
Matrix[] bonesAbsolute;
Matrix[] bonesAnimation;
为了将自定义的变换应用到骨骼上,还需要定义另一个矩阵数组.你使用这些
自定义的变化来修改骨架中非动画部分的骨骼.这很重要,因为这样可以获得更多的特性(是我们更喜欢骨骼动画的一个主要原因).如,当角色行走循环时你可以放低一个胳膊.
Matrix[] bonesTransform;
最后,你需要声明2个属性来储存之后要创建的动画模型效果和材质:
AnimatedModelEffect animatedModelEffect;
LightMaterial lightMaterial;
你创建AnimatedModelEffect类来封装动画模型效果,并使用第9章LightMaterial类来配置它.
载入一个动画的模型
在你尝试载入你的模型前,确保编译了新的内容管道.接着拖一个动画模型文件到Contents目录下,并选择它.在屏幕右侧底部的属性对话框,从可用的处理器列表中选择AnimatedModel处理器.接着使用内容管理器进行载入.
现在你需要检查载入的模型是不是一个有效的动画模型-就是检查模型的Tag属性是否包含一个有AnimatedModelData的字典:
model = Game.Content.Load<Model>(
GameAssetsPath.MODELS_PATH + modelFileName);
// Get the dictionary
Dictionary<string, object> modelTag =
(Dictionary<string, object>)model.Tag;
if (modelTag == null)
throw new InvalidOperationException(
"This is not a valid animated model.");
// Get the AnimatedModelData from the dictionary
if (modelTag.ContainsKey("AnimatedModelData"))
animatedModelData = (AnimatedModelData)
modelTag["AnimatedModelData"];
else
throw new InvalidOperationException(
"This is not a valid animated model.");
载入模型后,你应该初始化一些遍历来配置并再生模型的动画.默认的模型动画是AnimatedModelData对象的Animations数组第一个,并储存在activeAnimation属性中.
if (animatedModelData.Animations.Length > 0)
activeAnimation = animatedModelData.Animations[0];
初始动画帧和时间分别储存在activeAnimationKeyframe和activeAnimationTime属性,通过配置animationSpeed属性来控制动画速度:
// Default animation configuration
animationSpeed = 1.0f;
activeAnimationKeyframe = 0;
activeAnimationTime = TimeSpan.Zero;
当模型开始进行动画时,我们使用一些临时的矩阵数组来计算每个骨骼最终的配置.在此处来创建矩阵数组因为数组的大小应该和骨架中的骨骼数目一致.你应该用骨骼的配置来初始化bones数组并将其储存在AnimatedModelData中.在bonesTransform中储存相同的矩阵,此时先不管重叠在其上的其他移动.
// Temporary matrices used to animate the bones
bones = new Matrix[animatedModelData.BonesBindPose.Length];
bonesAbsolute = new Matrix[animatedModelData.BonesBindPose.Length];
bonesAnimation = new Matrix[animatedModelData.BonesBindPose.Length];
// Used to apply custom transformation over the bones
bonesTransform = new Matrix[animatedModelData.BonesBindPose.Length];
for (int i = 0; i < bones.Length; i++)
{
bones[i] = animatedModelData.BonesBindPose[i];
bonesTransform[i] = Matrix.Identity;
}
最后将获取模型动画,并将其封装在AnimatedModelEffect中:
// Get the animated model effect - shared by all meshes
animatedModelEffect = new AnimatedModelEffect(model.Meshes[0].Effects[0]);
// Create a default material
lightMaterial = new LightMaterial();
注意,渲染模型的效果被所有模型网格共享.下面是完整的AnimatedModel类Load方法:
public void Load(string modelFileName)
{
if (!isInitialized)
Initialize();
model = Game.Content.Load<Model>(
GameAssetsPath.MODELS_PATH + modelFileName);
// Get the dictionary
Dictionary<string, object> modelTag =
(Dictionary<string, object>)model.Tag;
if (modelTag == null) throw new InvalidOperationException(
"This is not a valid animated model.");
// Get the AnimatedModelData from the dictionary
if (modelTag.ContainsKey("AnimatedModelData"))
animatedModelData = (AnimatedModelData)
modelTag["AnimatedModelData"];
else
throw new InvalidOperationException(
"This is not a valid animated model.");
骨骼动画等式
本节回顾下一些概念和用于骨骼动画中的数学等式.骨骼动画是由很多关键帧组成,每个关键帧都储存了骨骼的配置(它的方位和位置)和持续进行动画的时间.在每次间隔时,我们使用一个或多个关键帧来改变骨架中骨骼的配置.图12-7显示了图12-3中骨架中的动画,当左肩膀骨骼方位改变后,将影响所有的子骨骼.
为了达到图12-7的效果,你只需改变左肩膀骨的关键帧动画.虽然最后左肩膀骨的子骨都改变了,但他们还是与左肩膀骨保存相同的关系.就是说左肩膀的子骨们新的配置你不需要储存.因为你可以通过左肩膀骨新的配置来计算他们.注意,当你需要更新模型是,你应该计算每个骨骼的绝对配置,并且用这些骨骼来转换网格的顶点.

图12-7 左肩膀骨动画,与图12-3的原始骨架比比较.注意所有的后代骨骼都改变了
接下来,我们来看看模型动画时被用来变换模型网格的数学等式.将使用这些等式来更新和渲染模型.
为了使用GPU来完成高级计算,你需要在HLSL效果中实现一些等式.为了实现平滑的骨骼动画,每帧都需要计算顶点的每个位置,这种操作正适合顶点着色器(vertex shader)来处理.章节"创建动画模型效果"中,你会发现需要的hlsl代码.
转换网格顶点
一些简单的模型,每个顶点既是一个单独的骨骼.这种方法会使网格产生破裂.如当角色弯曲他的胳膊,肘部头处将出现破裂.为了解决这个问题,多个骨骼使用更多的顶点.并且每个顶点都有单独的比重定义,以描述它依附骨骼的程度.如一个在上臂中心处的顶点,几乎完全的属于上臂.一个靠近肘部的顶点有50%属于上臂,50%属于前臂.
你可以计算最终网格顶点的位置,它只受一个骨骼的影响,公式:

等式中,PF是顶点的最终位置,P'0是顶点的初始位置,Bone是包含影响顶点的骨骼绝对配置,W是顶点占此骨骼的比重.本例中顶点只被一个骨骼影响,所以比重是1.0(就是100%).公式显示了你该如何来计算顶点最终位置:通过包含骨骼绝对配置的矩阵来变换顶点的初始位置.
注意:顶点的比重需要归一化;就是说顶点比重之和应该是1.因为3d顶点是向量,并且此处的操作与多向量间的线性插值没什么两样.像别的向量一样,一个简单的插值为了平均2个向量,你需要将他们的和除以2.结果就是0.5.
公式之前顶点的初始位置必须与骨骼处于同一坐标系.记住当顶点连接到骨骼上时,所有的骨骼都在凝固位置,并且所有的骨骼动画都处于初始的凝固位置.你可以通过乘以骨骼反转矩阵将顶点原始坐标(位置储存在顶点内)转换到骨骼凝固姿势坐标系,等式如下:

等式中,P'0是顶点在骨骼凝固姿势坐标系的初始位置.P0顶点对象坐标系的位置(位置储存在顶点)Bone-1BindPose是骨骼在凝固姿势的绝对配置的反转矩阵.为了将顶点放入骨骼坐标系,你需要将顶点与骨骼在凝固姿势的反转矩阵相乘.使用之前的2个等式,你可以使用骨骼来实现所有顶点的动画.
组合骨骼变换
前节中第一个等式不允许操作多个骨骼.为了计算多个骨骼情况下顶点的最终位置,你需要分别计算影响顶点的每个骨骼.这时你可以以顶点的最终位置之和来计算顶点的最终位置.得到每个骨骼对此顶点的影响.下面等式计算多个骨骼影响下的顶点位置.

注意:被用来变换顶点比重的和必须等于1.最后,下面显示了用来变换网格顶点的等式.

注意在此等式中,你需要首先计算出用来变换顶点的矩阵的和的平均值.这样顶点只需要被变换一次.
更新AnimatedModel类
模型动画播放时,代码需要不断的根据关键帧更新所有骨骼的方位,而关键帧包含一个新的关于骨骼自身坐标系配置.你将使用cpu与gpu一起来处理骨骼动画.在cpu上计算骨骼矩阵(矩阵[Bone–1BindPose * Bonei],上节中的等式),因为这里没多少骨骼.将为每个顶点使用gpu来组合骨骼矩阵并使用结果矩阵来变换定的的位置.
为了在cpu上处理动画过程,你将为AnimatedModel创建一个Update方法,同时,为了在gpu上处理动画进程,你还需要为动画模型创建一个新的效果,在cpu中你可以将模型动画划分为3个主要部分:
1,更加当前播放的动画和时间来更新骨架中的骨骼.
2,为每个骨骼计算绝对坐标.
3,使用计算出的最终骨骼矩阵来变换顶点.
从动画进程的第一部分开始,计算当前动画的时间.通过将动画逝去时间(Ticks上次更新的时间)累计来增加动画时间,从而完成此操作,这里逝去的时间是动画速度的比例:
activeAnimationTime += new TimeSpan(
(long)(time.ElapsedGameTime.Ticks * animationSpeed));
这时你需要通过比较activeAnimationTime与当前动画的时间段来确定当前的动画是否结束.如果enableAnimationLoop为true,你可以重置动画时间:
// Loop the animation
if (activeAnimationTime > activeAnimation.Duration && enableAnimationLoop)
{
long elapsedTicks = activeAnimationTime.Ticks % activeAnimation.Duration.Ticks;
activeAnimationTime = new TimeSpan(elapsedTicks);
activeAnimationKeyframe = 0;
}
接着,需要检查是否是动画的第一更新.注意,你需要将骨骼恢复到他们凝固姿势:
// Put the bind pose in the bones in the beginning of the animation
if (activeAnimationKeyframe == 0)
{
for (int i = 0; i < bones.Length; i++)
bones[i] = animatedModelData.BonesBindPose[i];
}
为了再生动画,你循环遍历当前模型动画的关键帧,当activeAnimationTime大于关键帧时间时,更新模型的骨骼:
// 浏览所有的动画关键帧知道到达当前时间
// 这是没错的,因为之前你已经将关键帧进行了排序
int index = 0;
Keyframe[] keyframes = activeAnimation.Keyframes;
while (index < keyframes.Length && keyframes[index].Time <= activeAnimationTime)
{
int boneIndex = keyframes[index].Bone;
bones[boneIndex] = keyframes[index].Transform * bonesTransform[boneIndex];
index++;
}
activeAnimationKeyframe = index - 1;
动画处理的第2部分,你需要循环遍历所有骨骼的矩阵并计算他们的绝对配置.因为骨架的骨骼数组是深度遍历而构成,在此数组中父骨骼的索引不可能比自身索引大.因此,你可以按顺序来遍历每一个元素,计算每个元素的最终位置而不需担心变化产生的影响,因为每个元素的父元素的位置已经被计算过了.注意第一个骨骼已经处于绝对坐标系了(因为它没有父骨骼),但你可以通过一个自定义的矩阵来变换它.
// 用绝对坐标来填充骨骼
bonesAbsolute[0] = bones[0] * parent;
for (int i = 1; i < bonesAnimation.Length; i++)
{
int boneParent = animatedModelData.BonesParent[i];
// 这里我们使用父骨骼来变换一个子骨骼
bonesAbsolute[i] = bones[i] * bonesAbsolute[boneParent];
}
最后,你通过乘以骨骼在其凝固姿势反转矩阵和当前绝对位置来计算其最终位置,最后的等式已经在上节中:
/*
我们使用经过计算的骨骼矩阵转换网格顶点之前,我们需要将顶点放置到骨骼坐标系中用于连接它.
*/
for (int i = 0; i < bonesAnimation.Length; i++)
{
bonesAnimation[i] = animatedModelData.BonesInverseBindPose[i] *
bonesAbsolute[i];
}
下面是AnimatedModel类完成的Update方法
private void UpdateAnimation(GameTime time, Matrix parent)
{
activeAnimationTime += new TimeSpan(
(long)(time.ElapsedGameTime.Ticks * animationSpeed));
if (activeAnimation != null)
{
// 遍历骨骼
if (activeAnimationTime >
activeAnimation.Duration && enableAnimationLoop)
{
long elapsedTicks = activeAnimationTime.Ticks %
activeAnimation.Duration.Ticks;
activeAnimationTime = new TimeSpan(elapsedTicks);
activeAnimationKeyframe = 0;
}
// 每次动画开始时将局部凝固姿势放入骨骼数组
if (activeAnimationKeyframe == 0)
{
for (int i = 0; i < bones.Length; i++)
bones[i] = animatedModelData.BonesBindPose[i];
}
/*
浏览所有的骨骼关键帧直到到达当前时间.这是可能的,因为我们在模型处理时已经将关键帧进行了排序.
*/
int index = 0;
Keyframe[] keyframes = activeAnimation.Keyframes;
while (index < keyframes.Length &&
keyframes[index].Time <= activeAnimationTime)
{
int boneIndex = keyframes[index].Bone;
bones[boneIndex] = keyframes[index].Transform *
bonesTransform[boneIndex];
index++;
}activeAnimationKeyframe = index - 1;
}
// 计算骨骼的绝对坐标
bonesAbsolute[0] = bones[0] * parent;
for (int i = 1; i < bonesAnimation.Length; i++)
{
int boneParent = animatedModelData.BonesParent[i];
// 根据父骨骼的配置转换子骨骼
bonesAbsolute[i] = bones[i] * bonesAbsolute[boneParent];
}
// 在转换顶点之前,先把顶点放入骨骼的坐标系
for (int i = 0; i < bonesAnimation.Length; i++)
{
bonesAnimation[i] = animatedModelData.BonesInverseBindPose[i]
* bonesAbsolute[i];
}
}
创建AnimatedModel效果
每一次间隔,你需要根据当前骨骼模型来变换(动画)模型网格.gpu上的模型变换的优势是比cpu快的多.因为gpu为繁杂的操作(如大量并发的乘除)进行了优化.
本节中,你将为动画模型渲染创建一个效果,用来将模型的顶点转换到顶点着色器.此效果也将支持2个全方向的光影和纹理.
统一定义变量
如前面你所学到的那样,使用自定义的效果时用同一的变量定义将会很好.这些是xna程序需要设置的变量,并在顶点和像素渲染整个帧期间保持不同.
现在开始定义一些常用的变量,这和之前的章节所说的没什么特殊的:
//矩阵
float4x4 matW : World;
float4x4 matV : View;
float4x4 matVI : ViewInverse;
float4x4 matWV : WorldView;
float4x4 matWVP : WorldViewProjection;
//材质
float3 diffuseColor;
float3 specularColor;
float specularPower;
//光
float3 ambientLightColor;
float3 light1Position;
float3 light1Color;
float3 light2Position;
float3 light2Color;
你需要这些来实现3d到2d变换及光线计算.更重要的是,本章的,是统一的变量:
#define SHADER20_MAX_BONES 58
Float4x4 matBones[SHADER20_MAX_BONES];
这表示你的xna程序应该定义一个矩阵数组.毋庸置疑这将是本章中当前模型的骨骼矩阵.在着色模型2.0中,一个单独的数组只能包含最多58个矩阵.
最后,加入纹理遍历和采样器:
//纹理
float2 uv0Tile;
texture diffuseTexture1 : Diffuse;
sampler2D diffuse1Sampler = sampler_state {
texture = <diffuseTexture1>;
MagFilter = Linear;
MinFilter = Linear;
MipFilter = Linear;
};
本例中你只需要一个纹理:你模型的纹理.
创建顶点着色结构
顶点着色器接收的每个顶点都希望有顶点的位置,法线,纹理坐标及骨骼索引和比重.每个顶点会受到4个骨骼索引比重的影响.如果顶点依附在单独的骨骼,其余三个比重将是0.
顶点的索引和比重属性由默认的xna模型处理器ModelProcessor类处理.
struct a2v
{
float4 position : POSITION;
float3 normal : NORMAL;
float2 uv0 : TEXCOORD0;
float4 boneIndex : BLENDINDICES0;
float4 boneWeight : BLENDWEIGHT0;
};
创建从顶点着色到像素顶点的结构
顶点着色的输出包含顶点的最终位置是本章关注的主题.除最终的位置,还包含了法线,纹理坐标,视野向量,2个光向量.
struct v2f
{
float4 hposition : POSITION;
float2 uv0 : TEXCOORD0;
float3 normal : TEXCOORD1;
float3 lightVec1 : TEXCOORD2;
float3 lightVec2 : TEXCOORD3;
float3 eyeVec : TEXCOORD4;
};
顶点着色的强制行为产生顶点的2d屏幕坐标,其是通过组合视野(view)矩阵和投影(projection)矩阵来变换定义的绝对3d位置而来.然而这暗示你首先要找到绝对3d位置,而3d位置储存在顶点并作为顶点着色的入参,由于其关联的骨骼所定义.因此,着色器首先需要通过组合骨骼的变换将此位置从骨骼坐标系转换到物体坐标系或模型坐标系.此时,位置由关联模型的原点来定义.为了获取绝对3d位置或世界位置,你需要将其转换为世界空间,这需要通过世界矩阵(world)来完成,它包含了3d世界中物体的位置和坐标.你知道了世界空间中的3d位置后,你最终将使用view和projection矩阵将其转换到屏幕空间.
AnimatedModel 顶点处理
在顶点着色中,你应该首先计算最终骨骼矩阵,用其来变换顶点的位置和法线.最终变换的公式在前面的"骨骼动画等式"中.本节,每个顶点依赖4个骨骼矩阵,以表明每个骨骼矩阵的比重.记住之前说过的这些骨骼矩阵在UpdateAnimation方法每帧开始时被更新.
//计算最终骨骼变换矩阵
float4x4 matTransform = matBones[IN.boneIndex.x] * IN.boneWeight.x;
matTransform += matBones[IN.boneIndex.y] * IN.boneWeight.y;
matTransform += matBones[IN.boneIndex.z] * IN.boneWeight.z;
float finalWeight = 1.0f - (IN.boneWeight.x + IN.boneWeight.y + IN.boneWeight.z);
matTransform += matBones[IN.boneIndex.w] * finalWeight;
接着,你使用最终骨骼矩阵来变换顶点位置和法线,并将结果储存在postion变量.你计算的是模型的顶点的最终3d位置.之后,通过world,view,projection组合而成的矩阵来转换它.为的是将模型移动到绝对的3d世界位置和从绝对3d位置变换到2d屏幕坐标.
//变换顶点和法线
float4 position = mul(IN.position, matTransform);
float3 normal = mul(IN.normal, matTransform);
OUT.hposition = mul(position, matWVP);
OUT.normal = mul(normal, matWV);
注意:因为world,view,projection矩阵对与整个模型都是一样的,他们经常被组合到一个称为WVP的矩阵.
最后你计算视野向量和2个光线向量.它们将用作像素着色器的光线中:
//计算光线和眼睛向量
float4 worldPosition = mul(position, matW);
OUT.eyeVec = mul(matVI[3].xyz - worldPosition, matV);
OUT.lightVec1 = mul(light1Position - worldPosition, matV);
OUT.lightVec2 = mul(light2Position - worldPosition, matV);
OUT.uv0 = IN.uv0;
这里是完成的顶点处理代码:
v2f animatedModelVS(a2v IN)
{
v2f OUT;
// 计算最终的骨骼变换矩阵
float4x4 matTransform = matBones[IN.boneIndex.x] *
IN.boneWeight.x;
matTransform += matBones[IN.boneIndex.y] * IN.boneWeight.y;
matTransform += matBones[IN.boneIndex.z] * IN.boneWeight.z;
float finalWeight = 1.0f - (IN.boneWeight.x + IN.boneWeight.y +
IN.boneWeight.z);
matTransform += matBones[IN.boneIndex.w] * finalWeight;// Transform vertex and normal
float4 position = mul(IN.position, matTransform);
float3 normal = mul(IN.normal, matTransform);
OUT.hposition = mul(position, matWVP);
OUT.normal = mul(normal, matWV);
// 计算光线和眼睛向量
float4 worldPosition = mul(position, matW);
OUT.eyeVec = mul(matVI[3].xyz - worldPosition, matV);
OUT.lightVec1 = mul(light1Position - worldPosition, matV);
OUT.lightVec2 = mul(light2Position - worldPosition, matV);
OUT.uv0 = IN.uv0;
return OUT;
}
AnimatedModel的像素处理
像素着色处理的所有像素(在三角内部)数据是在三角形三个顶点间插值完成的.因此,像素着色第一要处理的是归一化所有的向量,这样才可能执行正确的光线计算,确保他们的长度是1:
归一化所有的输入向量
float3 normal = normalize(IN.normal);
float3 eyeVec = normalize(IN.eyeVec);
float3 lightVec1 = normalize(IN.lightVec1);
float3 lightVec2 = normalize(IN.lightVec2);
float3 halfVec1 = normalize(lightVec1 + eyeVec);
float3 halfVec2 = normalize(lightVec2 + eyeVec);
此时,你有了计算光线所需要的所有向量.现在你将使用冯氏(Phong)算法来计算光线:
//计算每个光线的反射光和镜面高光
float3 diffuseColor1, diffuseColor2;
float3 specularColor1, specularColor2;
phongShading(normal, lightVec1, halfwayVec1, light1Color,
diffuseColor1, specularColor1);
phongShading(normal, lightVec2, halfwayVec2, light2Color,
diffuseColor2, specularColor2);
之后,从纹理中读取像素颜色:
Float4 materialColor = tex2D(diffuse1Sampler,IN.uv0);
最后,你计算每个像素的最终颜色,用从光源处得到的反射和高光分量来组合像素的颜色:
float4 finalColor;
finalColor.a = 1.0f;
finalColor.rgb = materialColor *
( (diffuseColor1+diffuseColor2) * diffuseColor +
ambientLightColor) + (specularColor1 + specularColor2) *
specularColor ;
The code for the phongShading function is shown in Chapter 11. The final pixel shader
code follows:
float4 animatedModelPS(v2f IN): COLOR0
{
// 归一化所有的输入向量
float3 normal = normalize(IN.normal);
float3 eyeVec = normalize(IN.eyeVec);
float3 lightVec1 = normalize(IN.lightVec1);
float3 lightVec2 = normalize(IN.lightVec2);
float3 halfwayVec1 = normalize(lightVec1 + eyeVec);
float3 halfwayVec2 = normalize(lightVec2 + eyeVec);
// 为每个光线计算反射和高光
float3 diffuseColor1, diffuseColor2;
float3 specularColor1, specularColor2;
phongShading(normal, lightVec1, halfwayVec1,
light1Color, diffuseColor1, specularColor1);
phongShading(normal, lightVec2, halfwayVec2,
light2Color, diffuseColor2, specularColor2);

// 读取纹理反射颜色
float4 materialColor = tex2D(diffuse1Sampler, IN.uv0);
// Phong lighting result
float4 finalColor;
finalColor.a = 1.0f;
finalColor.rgb = materialColor *
( (diffuseColor1+diffuseColor2) * diffuseColor +
ambientLightColor) + (specularColor1+specularColor2) *
specularColor ;
return finalColor;
}
现在,剩下的工作是定义一个用于顶点和像素着色技术(technique).
technique AnimatedModel
{
pass p0
{
VertexShader = compile vs_2_0 animatedModelVS();
PixelShader = compile ps_2_a animatedModelPS();
}
}
转换网格效果
你需要使用前面所创建的效果来渲染模型.xna的模型处理用ConvertMaterial方法,当模型网格的材质被发现时它会被调用.你将在自定义的内容管道中重写这个方法.
CovertMaterial方法接收一个MaterialContent对象的入参,其储存被网格使用的材质内容.当一个模型在没有效果情况下被导出,它只有一些基础的材质信息,如颜色和纹理.在本例中,入参MaterialContent是BasicMaterialContent类的一个实例.如果模型使用一个效果而导出,材质参数就是EffectMaterialContent类的一个实例了.
为了改变模型使用的材质,你需要重写ConvertMaterial方法,并转换BasicMaterialContent为EffectMaterialContent,生成你所创建动画模型的效果.下面的代码显示了ConvertMaterail方法,你应该将其加入到模型处理器中.
protected override MaterialContent ConvertMaterial(
MaterialContent material, ContentProcessorContext context)
{
BasicMaterialContent basicMaterial = material
as BasicMaterialContent;
if (basicMaterial == null)
context.Logger.LogImportantMessage(
"This mesh doesn't have a valid basic material.");
// Only process meshes with basic material
// Otherwise the mesh must use the correct effect (AnimatedModel.fx)
if (basicMaterial != null)
{
EffectMaterialContent effectMaterial =
new EffectMaterialContent();
effectMaterial.Effect =
new ExternalReference<EffectContent>(
SHADERS_PATH + SHADER_FILENAME);// Correct the texture path
if (basicMaterial.Texture != null)
{
string textureFileName = Path.GetFileName(
basicMaterial.Texture.Filename);
effectMaterial.Textures.Add("diffuseTexture1",
new ExternalReference<TextureContent>(
TEXTURES_PATH + textureFileName));
}
return base.ConvertMaterial(effectMaterial, context);
}
else
return base.ConvertMaterial(material, context);
}
当使用正确路径将BasicMaterialContent转变为EffectMaterialContent时,模型的默认材质将被再次传递到新创建的效果中.
渲染模型
因为动画模型是包含自定义效果xna模型,它很容易被渲染.首先,你需要配置动画模型的效果(9章讲过),这时你只需遍历它所有的网格,调用它们的Draw方法,下面的代码显示了AnimatedModel类的Draw方法:
public override void Draw(GameTime gameTime)
{
SetEffectMaterial();
for (int i = 0; i < model.Meshes.Count; i++)
{
model.Meshes[i].Draw();
}
}
总结
本章,你学习了如何扩展xna的内容管道以让它支持骨骼动画模型,并且如何创建一个类在运行时处理动画模型.同时你也复习了一些骨骼动画模型观念和数学公式.
下章,你将看到如何组合所有的从第8章开始的观念,来创建一个真实的3d游戏,简单的tps游戏.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

gamebox1

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值