39.蒙皮动画模型SkinningModelSceneNode类
上一个教程我们已经使用自定义的内容处理器将动画数据放在了模型的Tag属性中,这个教程介绍如何在XNA程序中使用这些数据,但在这之前,你应该对动画的基本原理有所了解。
简单动画的实现
实现原理可以参考4.9 使Bone独立运动:模型动画(也可以参考本网站的简单动画-坦克1、层次模型动画—坦克2、36.实现模型跟随地形的效果以及XNA官网的Simple Animation示例(http://creators.xna.com/en-US/sample/simpleanimation))。
下面以层次模型动画—坦克2中的模型为例,如果我想让炮管上下旋转,要进行的操作主要有两步:
一.改变bone的Transform矩阵
我们需要首先获取炮管的bone初始变换矩阵,这个矩阵即炮管相对于它的父bone(炮塔)的偏移,这可以用以下代码获得:
Matrix GunTransform=tankModel.Bones["Gun"].Transform;
你也可以使用参考4.9 使Bone独立运动:模型动画中的方法,即创建一个Matrix数组,将模型的所有bone变换都复制到这个数组:
originalTransforms = new Matrix[tankModel.Bones.Count]; tankModel.CopyBoneTransformsTo(originalTransforms);
这样炮管的bone变换矩阵也可以使用originalTransforms[2]的形式获得。
然后将自定义变换矩阵施加在这个初始变换矩阵上,例如:
tankModel.Bones["Gun"].Transform = Matrix.CreateRotationX(GunUpDown) * GunTransform;
其中GunUpDown是一个位于Update中的变量,可以随时间增加或减少,通过这种方法我们就可以改变bone的初始变换矩阵了。
二.计算bone的绝对变换矩阵
第一步计算出的变换矩阵是相对于父bone而言的,本例中即相对于炮塔的偏移,但绘制的最终结果是在绝对世界空间中的,所以需要将这个变换矩阵乘以它的父bone的相对变换矩阵,再乘以父bone的父bone的相对变换矩阵直至根bone。但XNA自带一个CopyAbsoluteBoneTransformsTo方法可以帮助你计算每个bone的绝对变换矩阵,因此,炮管的世界矩阵即它的父bone的绝对世界矩阵。所以你需要在代码中定义一个存储所有bone绝对变换矩阵的Matrix数组:
modelTransforms = new Matrix[myModel.Bones.Count];
并在Draw方法中调用CopyAbsoluteBoneTransformsTo方法更新这个矩阵数组:
tankModel.CopyAbsoluteBoneTransformsTo(modelTransforms);
从第一步可以看出,如果我设置一个Matrix数组替换掉Matrix.CreateRotationX(GunUpDown) ,在每次绘制时依次使用这个数组的一个元素,也可以实现动画的效果,这个Matrix数组本质上就是关键帧!因此,对于X文件的导出系列4——关键帧动画中制作的模型,我们完全可以自己编写一个内容处理器,将关键帧信息包含在模型的Tag属性中,然后在XNA代码中实现动画的播放。不过这样做好像意义不大,我们下面将要处理更复杂的带蒙皮信息的动画。
SkinningModelSceneNode类的实现
要实现蒙皮动画模型,主要操作不是二步而是三步,第三步还要更新变换顶点位置的最终矩阵,而这个矩阵会被传递到effect进行顶点坐标的变换。XNA官网的例子将这三步放置在了SkinnedModelWindows项目的AnimationPlayer.cs类中,对应UpdateBoneTransforms,UpdateWorldTransforms,UpdateSkinTransforms三个方法。但官网的例子功能没有《Beginning XNA》中的11.4.4 AnimatedModel的Update方法功能多,它还实现了是否循环动画和动画播放速度的控制,而且还可以施加自定义的变换,所以我的引擎主要是参考《Beginning XNA》的示例。以下是SkinningModelSceneNode类的代码:
namespace StunEngine.SceneNodes
{
///
/// 支持蒙皮动画的模型类,从ModelSceneNode继承
///
public class SkinningModelSceneNode :ModelSceneNode
{
///
/// 当前播放的动画片段
///
AnimationClip currentAnimationClip;
///
/// 当前播放的动画片段的索引
///
int currentAnimationId=-1;
///
/// 当前动画片段经历的时间,默认为动画片段的开始时刻
///
TimeSpan currentTimeValue;
///
/// 当前动画片段的当前关键帧索引,默认为第一帧
///
int currentKeyframe;
///
/// 保存动画片段名称的集合
///
public List animationList;
///
/// 动画片段是否已经结束
///
public bool IsAnimationFinished
{
get
{
return (currentTimeValue >= currentAnimationClip.Duration);
}
}
///
/// 是否可以循环播放动画
///
public bool EnableAnimationLoop
{
get;set;
}
///
/// 获取或设置动画播放速度
///
public float AnimationSpeed
{
get;set;
}
///
/// 自定义的bone的变换矩阵数组
///
Matrix[] customBoneTransforms;
///
/// 获取或设置自定义的bone的变换矩阵数组
///
public Matrix[] CustomBoneTransforms
{
get { return customBoneTransforms; }
set { customBoneTransforms = value; }
}
///
/// bone的变换矩阵数组
///
Matrix[] boneTransforms;
///
/// bone的绝对变换矩阵数组
///
Matrix[] worldTransforms;
///
/// 变换顶点位置的最终矩阵,这个矩阵要传递到effect中进行处理
///
Matrix[] skinTransforms;
///
/// 动画数据
///
SkinningData skinningData;
///
/// 创建一个SkinningModelSceneNode对象。
///
/// 引擎
/// 所属场景
/// 模型名称
public SkinningModelSceneNode(StunXnaGE engine, Scene setScene, string modelAssetName)
: base(engine, setScene, modelAssetName)
{
animationList = new List();
EnableAnimationLoop = true;
AnimationSpeed = 1.0f;
}
public override void Initialize()
{
base.Initialize();
for (int i = 0; i < materials.Length; i++)
{
materials[i].CurrentTechniqueName = "SkinnedModelTechnique";
}
}
internal override void LoadContent()
{
base.LoadContent();
// 从模型的Tag属性中提取SkinningData信息
skinningData = model.Tag as SkinningData;
if (skinningData == null)
throw new InvalidOperationException
("这个模型的Tag属性不包含SkinningData");
// 初始化矩阵数组
customBoneTransforms = new Matrix[skinningData.BindPose.Count];
boneTransforms = new Matrix[skinningData.BindPose.Count];
worldTransforms = new Matrix[skinningData.BindPose.Count];
skinTransforms = new Matrix[skinningData.BindPose.Count];
// 将动画片段名称添加到一个集合,这样我们就可以使用索引访问动画片段
foreach (KeyValuePair entry in skinningData.AnimationClips)
{
animationList.Add(entry.Key);
}
// 初始化bone变换
skinningData.BindPose.CopyTo(boneTransforms, 0);
// 初始化customBoneTransforms矩阵数组
for (int i = 0; i < customBoneTransforms.Length; i++)
{
customBoneTransforms[i] = Matrix.Identity;
}
}
public override void Update(GameTime gameTime)
{
base.Update(gameTime);
// 更新动画使用的三个矩阵
UpdateAnimation(gameTime);
}
///
/// 更新动画使用的三个矩阵
///
/// 时间
public void UpdateAnimation(GameTime time)
{
currentTimeValue += new TimeSpan((long)(time.ElapsedGameTime.Ticks * AnimationSpeed));
// 动画处理的任务分成三个部分
if (currentAnimationClip != null)
{
// --------------------------------------------------------
// 第一部分:根据当前播放时间更新boneTransforms矩阵数组
// ---------------------------------------------------------
// 如果播放时间大于动画片段长度且允许循环播放,则重新播放动画片段
if (currentTimeValue > currentAnimationClip.Duration && EnableAnimationLoop)
{
ResetAnimation();
}
IList keyframes = currentAnimationClip.Keyframes;
// 读取截止到播放时间内的关键帧
while (currentKeyframe < keyframes.Count && keyframes[currentKeyframe].Time <= currentTimeValue)
{
int boneIndex = keyframes[currentKeyframe].BoneIndex;
boneTransforms[boneIndex] = keyframes[currentKeyframe].Transform;
currentKeyframe++;
}
}
// 将自定义的bone变换施加在所有bone上
for (int i = 0; i < customBoneTransforms.Length; i++)
{
worldTransforms[i] = customBoneTransforms[i] * boneTransforms[i];
}
// --------------------------------------------------------
// 第二部分:计算所有bone的绝对变换矩阵
// --------------------------------------------------------
// 计算根节点绝对矩阵
worldTransforms[0] = worldTransforms[0] * pose.ScaleMatrix * pose.RotationMatrix * pose.TranslateMatrix;
// 计算子节点绝对矩阵
for (int boneIndex = 1; boneIndex < worldTransforms.Length; boneIndex++)
{
int parentBone = skinningData.SkeletonHierarchy[boneIndex];
worldTransforms[boneIndex] = worldTransforms[boneIndex] * worldTransforms[parentBone];
}
// --------------------------------------------------------
// 第三部分:计算变换顶点的矩阵
// --------------------------------------------------------
// 顶点坐标需要乘以bone的逆矩阵才能转换到bone的坐标空间中
for (int boneIndex = 0; boneIndex < skinTransforms.Length; boneIndex++)
{
skinTransforms[boneIndex] = skinningData.InverseBindPose[boneIndex] * worldTransforms[boneIndex];
}
}
///
/// 重置动画片段
///
private void ResetAnimation()
{
currentTimeValue = TimeSpan.Zero;
currentKeyframe = 0;
skinningData.BindPose.CopyTo(boneTransforms, 0);
}
public override int Draw(GameTime gameTime)
{
for (int i = 0; i < materials.Length; i++)
{
materials[i].EffectInstance.Parameters["Bones"].SetValue(skinTransforms);
}
return base.Draw(gameTime);
}
///
/// 播放指定名称的动画片段,循环播放,无需等待上一个动画结束
///
///
public void SetAnimationClip(string animationName)
{
SetAnimationClip(animationName, true, false);
}
///
/// 播放指定名称的动画片段
///
/// 动画片段编号
/// 是否循环播放动画片段
/// 是否等待当前动画片段结束
public void SetAnimationClip(string animationName, bool enableLoop, bool waitFinish)
{
if(animationList.IndexOf (animationName)==-1)
throw new InvalidOperationException("指定的动画片段不存在");
else
currentAnimationId = animationList.IndexOf(animationName);
if (waitFinish && !IsAnimationFinished)
return;
currentAnimationClip = skinningData.AnimationClips[animationName];
EnableAnimationLoop = enableLoop;
}
///
/// 播放指定编号的动画片段,循环播放,无需等待上一个动画结束
///
/// 动画片段编号
public void SetAnimationClip(int animationId)
{
SetAnimationClip(animationId, true, false);
}
///
/// 播放指定编号的动画片段
///
/// 动画片段编号
/// 是否循环播放动画片段
/// 是否等待当前动画片段结束
public void SetAnimationClip(int animationId, bool enableLoop, bool waitFinish)
{
if (currentAnimationId != animationId&&animationId >=0&&animationId < animationList.Count)
{
if (waitFinish && !IsAnimationFinished)
return;
currentAnimationClip = skinningData.AnimationClips[animationList[animationId]];
EnableAnimationLoop = enableLoop;
currentAnimationId = animationId;
}
}
}
}
原理请参考代码注释,更详细的解释参见11.4.1 载入动画模型,11.4.2 骨骼动画公式,11.4.4 AnimatedModel的Update方法。
对应蒙皮动画模型的SkinnedModelTechnique代码如下:
// 使用shader 2.0时在一个pass中能够绘制的bone矩阵的最大数量。如果改变了这个值,在SkinnedModelProcessor.cs中也要做对应的改变。
#define MaxBones 59
float4x4 Bones[MaxBones];
// 顶点着色器输入结构
struct SkinningModelVS_INPUT
{
float4 Position : POSITION0;
float3 Normal : NORMAL0;
float2 TexCoords : TEXCOORD0;
float4 BoneIndices : BLENDINDICES0;
float4 BoneWeights : BLENDWEIGHT0;
};
// 顶点着色器输出结构
struct SkinningModelVS_OUTPUT
{
float4 Position : POSITION0;
float3 Lighting : COLOR0;
float2 TexCoords : TEXCOORD0;
};
// 顶点着色器
SkinningModelVS_OUTPUT VertexShader(SkinningModelVS_INPUT input)
{
SkinningModelVS_OUTPUT output;
// 混合带权重的bone矩阵
float4x4 skinTransform = 0;
skinTransform += Bones[input.BoneIndices.x] * input.BoneWeights.x;
skinTransform += Bones[input.BoneIndices.y] * input.BoneWeights.y;
skinTransform += Bones[input.BoneIndices.z] * input.BoneWeights.z;
skinTransform += Bones[input.BoneIndices.w] * input.BoneWeights.w;
// 使用这个矩阵获取顶点位置
float4 position = mul(input.Position, skinTransform);
output.Position = mul(mul(position, gView), gProjection);
// 变换顶点法线并计算光照
float3 normal = normalize(mul(input.Normal, skinTransform));
output.Lighting = max(dot(-normal, gLights[0].direction), 0) * gLights[0].color;
output.TexCoords = input.TexCoords;
return output;
}
// 像素着色器输入结构
struct SkinningModelPS_INPUT
{
float3 Lighting : COLOR0;
float2 TexCoords : TEXCOORD0;
};
// 像素着色器
float4 PixelShader(SkinningModelPS_INPUT input) : COLOR0
{
float4 diffuseColor = 0;
float3 finalColor = 0;
// 漫反射颜色来自于漫反射纹理或材质,首先将漫反射颜色设置为材质的漫反射颜色。
diffuseColor = float4(gMaterialDiffuse, 1.0);
//如果包含纹理,则gDiffuseEnabled为true,漫反射颜色取自纹理的颜色
if(gDiffuseEnabled)
{
diffuseColor = tex2D(textureSampler, input.TexCoords* gDiffuseUVTile);
}
//----------------------------------------
// 添加环境光颜色和自发光颜色
//----------------------------------------
finalColor += (diffuseColor.rgb*input.Lighting*gAmbient);
finalColor += gMaterialEmissive;
return float4(finalColor, diffuseColor.a);
}
technique SkinnedModelTechnique
{
pass SkinnedModelPass
{
VertexShader = compile vs_3_0 VertexShader();
PixelShader = compile ps_3_0 PixelShader();
}
}
更详细的解释请参见11.4.5 AnimatedModel Effect。
MaxBones 59的解释
使用蒙皮骨骼动画时对模型的骨头数量的限制是不超过59,原因是Shader model 2.0有256个常量寄存器,在用于相机和光照之后,保守的估计还能支持59个4x4矩阵。但是Shader model 3.0也只支持256个常量寄存器,所以并不能提高这个数量,试了一下,60是可以的,61以上就无法显示内容了,但制作模型时骨头数量往往轻易地就超过了59,该如何解决?你可以到XNA官网论坛http://forums.xna.com/forums/上搜索,总结下来有这样几个方法:
1.制作模型的时候就注意减少骨头的数量,这是最好的方法,其实只要不是太复杂的模型,59个骨头足够用了。
2.将skinTransforms矩阵从4*4形式压缩成4*3形式,这样可以支持更多的bone,具体如何实现我不是很清楚,但是使用kW X-port插件导出X文件时会帮你做这个处理,这也是为什么X文件的导出系列5——蒙皮动画模型中的最后一个怪物行走模型只有用kW X-port插件导出的模型才能使用的原因。
3.将模型分成几个批次绘制,每个批次不超过59个bone。这个我认为很难,但能解决所有问题。可以使用XNA Animation Component Library(http://animationcomponents.codeplex.com/releases/view/2810)实现这个操作,但问题是这个库文件好像没有开放源代码,只有dll文件,最后更新时间为2007年4月2日,此页面上下载的MeshSplitter.cs处理的就是这个操作,没有深入研究过。
单元测试
单元测试代码位于TestGame项目的TestSkinningModelSceneNode方法中:
////// 测试SkinningModelSceneNode类 /// public static void TestSkinningModelSceneNode() { SkinningModelSceneNode simpleMan= null; SkinningModelSceneNode AlienSkeletonAnimationX = null; SkinningModelSceneNode AlienBipedAnimationKW = null; SkinningModelSceneNode dude = null; Vector3 scale, position; int animationId2 =0; int animationId3 =0; int animationId4 =0; string animationName = "RaiseLeft"; TestGame.Start("测试SkinningModelSceneNode类", delegate { // 在左侧放置一个简单的人物模型,它包含提左脚RaiseLeft和提右脚RaiseRight的动画 simpleMan = new SkinningModelSceneNode(TestGame.engine, TestGame.scene, "Models//manSkinningX"); TestGame.scene.AddNode(simpleMan); scale = new Vector3(0.2f, 0.2f, 0.2f); simpleMan.Pose.SetScale(ref scale); position = new Vector3(-6.0f, 0.5f, 0); simpleMan.Pose.SetPosition(ref position); // 在左侧放置一个怪物模型,在3DSMAX中使用骨骼制作,它包含提左脚RaiseLeft和提右脚RaiseRight的动画 AlienSkeletonAnimationX = new SkinningModelSceneNode(TestGame.engine, TestGame.scene, "Models//AlienSkeletonAnimationX"); TestGame.scene.AddNode(AlienSkeletonAnimationX); scale = new Vector3(0.02f, 0.02f, 0.02f); AlienSkeletonAnimationX.Pose.SetScale(ref scale); position = new Vector3(-2.0f, 0.5f, 0); AlienSkeletonAnimationX.Pose.SetPosition(ref position); // 在右侧放置一个怪物模型,在3DSMAX中使用Biped制作,它包含一个行走动画,显示还是有点小问题 AlienBipedAnimationKW = new SkinningModelSceneNode(TestGame.engine, TestGame.scene, "Models//AlienBipedAnimationKW"); TestGame.scene.AddNode(AlienBipedAnimationKW); scale = new Vector3(0.02f, 0.02f, 0.02f); AlienBipedAnimationKW.Pose.SetScale(ref scale); position = new Vector3(2.0f, 0.0f, 0); AlienBipedAnimationKW.Pose.SetPosition(ref position); // 在右侧放置一个士兵模型,他包含Idle,Run,Aim,Shoot四个动画 dude = new SkinningModelSceneNode(TestGame.engine, TestGame.scene, "Models//PlayerMarine"); TestGame.scene.AddNode(dude); scale = new Vector3(0.2f, 0.2f, 0.2f); dude.Pose.SetScale(ref scale); position = new Vector3(6.0f, 0.5f, 0); dude.Pose.SetPosition(ref position); TestGame.scene.IsShowMouse = false; }, delegate { // 按数字键1切换第一个模型的动画 if (Input.KeyboardKeyJustPressed (Keys.D1)) { if (animationName == "RaiseLeft") animationName = "RaiseRight"; else animationName = "RaiseLeft"; simpleMan.SetAnimationClip(animationName); } // 按数字键2切换第二个模型的动画 if (Input.KeyboardKeyJustPressed(Keys.D2)) { AlienSkeletonAnimationX.SetAnimationClip(animationId2%AlienSkeletonAnimationX.animationList .Count ); animationId2 += 1; } // 按数字键3切换第三个模型的动画 if (Input.KeyboardKeyJustPressed(Keys.D3)) { AlienBipedAnimationKW.SetAnimationClip(animationId3 % AlienSkeletonAnimationX.animationList.Count); animationId3 += 1; } // 按数字键4切换第四个模型的动画 if (Input.KeyboardKeyJustPressed(Keys.D4)) { dude.SetAnimationClip(animationId4 % dude.animationList.Count); animationId4 += 1; } // 按数字键5给第四个模型施加一个自定义的变换,让他的头旋转 if (Input.KeyboardKeyJustPressed(Keys.D5)) { if(dude.CustomBoneTransforms[3] == Matrix.CreateRotationX(1.0f)) dude.CustomBoneTransforms[3] = Matrix.Identity ; else dude.CustomBoneTransforms[3] = Matrix.CreateRotationX(1.0f); } // 按数字键6切换动画播放速度 if (Input.KeyboardKeyJustPressed(Keys.D6)) { if(dude.AnimationSpeed ==1.0f) simpleMan.AnimationSpeed =AlienSkeletonAnimationX.AnimationSpeed =AlienBipedAnimationKW.AnimationSpeed =dude.AnimationSpeed = 2.0f; else simpleMan.AnimationSpeed = AlienSkeletonAnimationX.AnimationSpeed = AlienBipedAnimationKW.AnimationSpeed = dude.AnimationSpeed = 1.0f; } }); }
截图如下:

发布时间:2010/6/9 下午4:41:23 阅读次数:8638
