用XNA实现蒙皮动画,基础是官方的程序例子Skinned Model
http://create.msdn.com/en-US/education/catalog/sample/skinned_model
简单的中文解说在:
http://shiba.hpe.sh.cn/jiaoyanzu/wuli/showArticle.aspx?articleId=673&classId=4
http://shiba.hpe.sh.cn/jiaoyanzu/wuli/soft/xna.aspx?classId=4这是一个学习XNA的好地方,可以学到很多东西,推荐一下。
去年做了一个动画的程序,当时也没有写出来,今天突然有人问起,发现说不出来了,于是痛定思痛,决定晚上回来回忆总结一下,再把程序翻出来看一看。
模型
首先,模型用的是FBX格式的,并且要包含动作。
(我以前也以为蒙皮动画是程序员编程做出来的,后来才知道不是这样的,是美工做出来,我只是播放一下。。)
用记事本打开模型,要找到“;Takes and animation section”这样的字样,然后后面的Takes里面就都是动作了。
一般一个动作由
“Take: "Take 001" {”
开始,到
“;Constraints animation
;----------------------------------------------------
}”
结束。
多个动作的时候就连在后面,序号递增(不知道为什么,我们美工做出来一个模型只有一套动作,所以需要把相应部分手工复制出来粘贴在后面,改成不一样的序号,这样才做成一个模型带很多个动作)。
貌似Take 001也可以换成别的名字,我是说不用“Take“。但是人家本来就用了这个词可能有一定的意义吧。。
我一般都把这三个名称对应了,不知道不一样行不行。
Current: "Take 002"
Take: "Take 002" {
FileName: "Take_002.tak"
拿官方网站的程序说的,模型放入以后,需要更改属性,把Content Processor改为SkinnedModelProcessor.
内容管道
对于自定义的内容管道我不太熟悉,知道个大概就是把素材进行处理,整理导入。
用XNA的时候,初学者使用一般的模型不用选择Content Processor属性,导入的时候会有默认值,自然就导入了。
如果有特殊需要才需要自己写内容管道,这样就得在解决方案中新建一个类型为Content Pipeline Extension Library的项目。(具体怎么写就先不赘述了)。
关于骨骼动画的内容管道,官方示例中已经写好了,所以直接用或者拿来修改一下就行。
在SkinnedModelProcessor这个类里面,ConvertMaterial这个函数里,指定模型的Effect为SkinnedModel.fx,还指定了纹理,如果有需要还可以加入法线贴图什么的。
官方示例基本部分
Ok,有了模型,也顺利导入了,就可以播放了。
官方示例中的SkinnedModelWindows这个项目就是已经写好的一个播放动画必须要用的程序,里面包含了相应的函数,如果在自己的项目中想用的话,可以直接添加DLL,然后调用就行了。
但是想要做一些功能扩展的话,还是需要好好改一下这部分程序的。
SkinnedModelWindows这个项目里面有4个类:AnimationClip,AnimationPlayer,Keyframe,SkinningData,主要有意思的部分在这个类里:AnimationPlayer。
这个类主要起作用的函数有:
public AnimationPlayer(SkinningData skinningData)
构造函数,skinningData是从模型中获取的,然后初始化矩阵组boneTransforms、worldTransforms和skinTransforms。(关于这三个数组稍后会有说明。)
public void StartClip(AnimationClip clip)
开始播放一个片段,做一些准备工作。
public void Update(TimeSpan time, bool relativeToCurrentTime, Matrix rootTransform)
可以看到更新函数不过是调用了下面三个函数:
public void UpdateBoneTransforms(TimeSpan time, bool relativeToCurrentTime)
更新Bone矩阵,这里的Bone矩阵里面存储的是每一个骨骼相对于父骨骼的移动(还有旋转),只是一个相对量,因为骨骼关系是互相连接并且牵动的,父骨骼的移动必然会导致子骨骼的移动,
详细的解说请见:http://shiba.hpe.sh.cn/jiaoyanzu/wuli/showArticle.aspx?articleId=422&classId=4
public void UpdateWorldTransforms(Matrix rootTransform)
更新World矩阵,从这里就可以看到,World矩阵就是就是骨骼的世界矩阵,将根节点的矩阵与外部的rootTransform相乘,这样就指定了模型整体的移动位置和选装情况,然后在这个函数里,每一个骨骼都用自己的Bone矩阵乘以父骨骼的World矩阵,得到自己的World矩阵。
public void UpdateSkinTransforms()
更新Skin矩阵,进行蒙皮。
这个部分我是这么理解的:
由于Bone矩阵和World矩阵存储的都是骨骼信息,但是实际绘制的时候不是绘制骨头,而是皮肤,所以需要进行这个“蒙皮”操作,主要是通过在Shader里对顶点位置进行改变而实现的。
这个Skin矩阵数组,是把World矩阵中的内容转换到骨骼自身坐标系得到,是这整个AnimationPlayer类要输出的结果,外部获取它们后放入SkinnedModel.fx这个文件中对顶点进行变换。
示例如何使用的部分,类名:SkinningSampleGame
可以看到在Draw函数里面,首先获取Skin矩阵:
Matrix[] bones = animationPlayer.GetSkinTransforms();
然后把它传入shader:
effect.Parameters["Bones"].SetValue(bones);
注意这里和之后的shader中,把矩阵称作Bones,要明确它其实说的是Skin矩阵。
在这里再说一下shader中比较重要的内容:(文件SkinnedModel.fx)
顶点着色器VS_OUTPUT VertexShader(VS_INPUT input)
里面,进行顶点位置的转换,注意input结构中包含了与当前顶点相关的四个骨骼序号BoneIndices和相应的权重BoneWeights。
然后根据权重对四个有关的矩阵混合即得到了当前顶点的蒙皮矩阵,然后对顶点位置进行转换即可,之后才是一般常规的什么World,View,Projection转换。
当然着色器里面还可以加入一些其他的光照啊法线贴图啊之类的效果,这些都是附加项。
Ok啦,知识就是这么多,按照官方程序的例子,下载下来什么也不用动,按个F5就可以跑了,有个人走啊走的。
程序扩展功能
为了让一个模型包含多个动作,并且动作播放可以控制,可以停在动作中间的位置,不同动作切换的时候有动作之间的平滑效果,就需要做很多改动了,主要就是在AnimationPlayer这个类里面,需要写出一些函数来。
1.首先,可以将程序改成由时间控制的,然后设计出不同的播放方案:如Loop,Once,Keeping等等。
2.需要定义切换动作的函数,以从外部控制何时切换.
关于动作,每一个动作有自己的Clip,
这里的Clip即是模型中的Take数据,如下,可以靠名字读入:
AnimationClip toClip = skinningData.AnimationClips[“Take 00X”]
然后每一个动作,即Clip中包含了若干个关键帧,如果有必要,可以找出相应动作的关键帧序号。关键帧中包含了时间信息。
这样做就可以想办法把一个clip中的动作分解开,也更加容易控制当前进行的动作。
3.矩阵更新。
首先由播放方案控制时间的更新,时间的起点和终点由当前动作决定。
然后根据时间和clip信息更新Bone矩阵
//获取当前动作的bone矩阵 boneTransforms = GetBonesFromClipAndTime(currentClipValue, currentTime);
其中函数如下:
/// <summary>
/// 获取指定Clip指定时间的一组bones矩阵
/// </summary>
/// <param name="clip"></param>
/// <param name="ts"></param>
/// <returns></returns>
private Matrix[] GetBonesFromClipAndTime(AnimationClip clip,TimeSpan ts)
{
IList<Keyframe> keyframeList;
keyframeList = clip.Keyframes;
Matrix[] xforms = new Matrix[skinningDataValue.BindPose.Count];
skinningDataValue.BindPose.CopyTo(xforms, 0);
int keyNum = 0;
while (keyNum < keyframeList.Count)
{
Keyframe key = keyframeList[keyNum];
if (key.Time > ts)
{
break;
}
xforms[key.Bone] = key.Transform;
keyNum++;
}
return xforms;
}
不能直接只运用当前时间点所对应的关键帧,我怀疑是因为每一个关键帧里只包含了在这一关键帧里有变化的骨骼位置。
所以某一时间点的动作是由于之前很多个关键帧的很多动作造成的。
其他World矩阵和Skin矩阵更新用原来的就好,不需要改动。
4.动作平滑融合
如果动作的切换有一定的平滑过渡就不会显得很突然,比如挥手动作和刷牙的动作如果有个切换,就不会显得突然,而是平滑过渡。
用于平滑融合动作的函数如下,主要进行Bone矩阵的的融合,其中fromTransforms参数传入上个动作的最后一组Bone矩阵,toTransforms传入下个动作的开始时刻的一组
Bone矩阵,如下调用:
boneTransforms = BlendTransforms(boneTransforms, GetBonesFromClipAndTime(nextClip, nextStartTime));
其中blend参数在融合开始之后便开始进行0到1的随时间的增长,当Blend切换到1之后,平滑过渡完成,开始正常播放下一个动作。
/// <summary>
/// 混合两组矩阵
/// </summary>
/// <param name="fromTransforms">本来矩阵</param>
/// <param name="toTransforms">目标矩阵</param>
/// <returns>混合后矩阵</returns>
private Matrix[] BlendTransforms(Matrix[] fromTransforms, Matrix[] toTransforms)
{
for (int i = 0; i < fromTransforms.Length; ++i)
{
//vertexTranslate and vertexScale
Vector3 vt1;
Vector3 vs1;
Quaternion q1;
fromTransforms[i].Decompose(out vs1, out q1, out vt1);
Vector3 vt2;
Vector3 vs2;
Quaternion q2;
toTransforms[i].Decompose(out vs2, out q2, out vt2);
Vector3 vtBlend = Vector3.Lerp(vt1, vt2, blend);
Vector3 vsBlend = Vector3.Lerp(vs1, vs2, blend);
Quaternion qBlend = Quaternion.Slerp(q1, q2, blend);
toTransforms[i] = Matrix.CreateScale(vsBlend) * Matrix.CreateFromQuaternion(qBlend) * Matrix.CreateTranslation(vtBlend);
}
return toTransforms;
}
结束语
基本就这么多。总算整理了一下,望指点。