Clayman's Graphics Corner

DirectX,Shader & Game Engine Programming

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

Skeletal Model and Skinning Animation

仅供个人学习使用,请勿转载,勿用于任何商业用途。 

      为了方便讨论,先定义几个术语:Model,一系列MeshPart(MP)(类似d3d里的subset)的集合;MeshPart,组成Model的单元,包含定义几何体的实际数据以及材质,也是最小的渲染单元。 

      第一个问题,为什么Model需要由多个MeshPart组成,如何划定一个Model分成几个MP?理想情况下,Model所包含的MP越少越好,最好是一个Model只包含一个MP,这样一次DrawPrimitive/DrawIndexedPrimitive,就能完成整个模型的渲染。但通常有两种情况需要把Model分为不同MP:1,材质(纹理,材质参数)不相同的部分;2,能独立移动的部分。

     通常情况下,作为程序员,你不必关心如何划分Model,模型师会做好一切,并且保存为文件给你使用,你所要关心的是如何找出文件中所保存的Model和MP。假设导出文件里储存的就是图中这个模型,那么可能遇到3种情况:1, 文件把模型记录为14个不同的model,它们共同组成了另外一个model(人);2,文件中只包含一个model,但这个model有14个MP;3,这种情况则是前两种混合的结果。

 

 

上面是两个不同的fbx文件,左边那个是以第三种方式组织的,右边那个则是以第1种方式组织。图里Geometry和我们所说的MP概念类似,但不完全一样,所以数值上看起来有些奇怪。

     如何导入模型超出了本文的讨论范围,现在假设你已经通过某种方式把模型数据加载到程序里,得到了14个MP,但仅仅有几何体数据是不够的。如果查看导出文件中的数据,会发现所有MP都以自己的局部坐标来保存顶点。也就是说如果你直接渲染这14个MP,他们会全部重叠在原点。因此,还需要知道每个MP在这个model中的位置。

 


      上图三个Lcl开头的数据就是fbx文件中记录的MP变换信息。Lcl表示局部变换,这里的局部相对于谁呢?相对于他的父节点。这里就需要引入一个新概念skeletal structure或者bone hierarchy,前者指模型的实际拓扑结构,后者指对这种拓扑结构的描述。具体来说,图中你看到的人物轮廓形象,就是这个模型的skeletal structure,而 “手链接到手臂,后手臂链接到肩膀”这样的描述,就是bone hierarchy。描述bone hierarchies最直观(但并非最优化)的方式就是用Tree。显然,对同一skeletal structure的描述可以有无数多种,依据你选择哪个部分为root,不同的描述方式之间并没有本质上的差别,通常,模型文件里都会包含特定的bone hierarchy数据。为了方便讨论,下文不再对structure和bone hierarchy做区分。最后解释一下另外一个术语bone/join(两者其实是一样的),这是一个很容易误导人的概念,对计算机模型来说,其实并没有bone这种东西,通常所说的bone应该包含两个概念:变换以及连接。假设你用Tree来保存模型的hierarchy信息,每个node里保存了相应的变换信息(比如matrix),那么可以认为每个node就是一个bone。但也有可能用一个数组来保存hierarchy信息,再用另外一个数据记录所有Matrix,这时就很难说哪一部分是bone。为了方便,下文的bone只表示变换,或者说代表一个matrix。

 

     假设你已经通过某种方式导入了hierarchy信息,以躯干为根节点,有以下数据结构:
Trunk
|
|-----neck----head
|
|-----left shoulder----left arm---left hand
|
|………………….

      那么现在已经有足够数据计算每个MP的world matrix,并且渲染它们了,假设在(0,0,0)点渲染这个模型:

Code

     你不必总是通过这样的方式来计算每个MP的实际世界坐标。对于静态模型来说,最好的方式是在加载模型或者预处理的过程中,就把顶点变换到模型空间中,假设图中的模型是个木头人,可以这样预处理他头上的顶点:

Code

 现在,如果在x,y,z点渲染这个模型,只需要:

worldMatrix = Matrix.CreateTranslate(x,y,z);
drawMP(worldMatrix, trunkData)
drawMP(worldMatrix, neckData)
drawMP(worldMatrix, headData)
……………………………….

完全省略了一步步通过父节点,计算当前MP实际世界坐标的步骤,同时也不再需要保存模型的hierarchy信息。
对于部分动态模型来说,可以预先把局部变换转变为模型空间的变换,比如:

trunkModelMat = localTrunkMat;
neckModelMat 
= localNeckMat * trunkModelMat;
headModelMat 
= localHeadMat * neckModelMatrix;
…………………..

在x,y,z点渲染这个模型时:

worldMatrix = Matrix.CreateTranslate(x,y,z);
drawMP(trunkModelMat 
* worldMatrix, trunkData)
drawMP(neckModelMat 
* worldMatrix, neckData)
drawMP(headModelMat 
* worldMatrix, headData)
    这种方法同样不需要保存模型的hierarchy信息。

    如果这两种方法那么好,那为什么还要讲解最初那种复杂的方法呢?应为最初的方法是最通用的,无论什么样的模型都可以处理。预变换顶点的方法通常只适用于静态模型。至于预计算变换,假设你希望通过程序修改了手臂的位置,手掌也自动随之一起移动的话,显然就无能为力了,因为我们已经丢失了hierarchy信息。此时,必须独立计算手掌应该如何移动,这比简单的通过父节点计算出变换复杂的多。

     目前,我们已经知道了如何渲染skeletal model,进一步渲染skeletal animation也就非常简单了。
     先来看看如何描述动画。最基本的模型动画有三种形式:位移变化,缩放变化和旋转变化。只要记录下动画时间内每个时刻的变换信息,也就是关键帧,就能重现这个动画:
dictionary mpMatrices;   //每一帧里所有mp的local matrix
dictionary keyFrames;    //一个动画序列中的所有帧
float currentTime;
currentMpMatrices 
= keyFrames[currentTime];
worldMatrix 
= Matrix.CreateTranslate(x,y,z);
trunkMatrix 
= currentMpMatrices[trunk] * worldMatrix;
neckMatrix 
= currentMpMatrices[neck] * TrunkMatrix;
headMatrix 
= currentMpMatrices[head] * headMatrix;
……………………………..
drawMP(trunkMatrix, trunkData)
drawMP(neckMatrix, neckData)
      目前我们知道了如何渲染skeletal model和skeletal animation,但它们通常只适用于刚体,也就是不会发生形变的模型,比如汽车,机器人。对于人或者动物这样更高级的对像来说,需要使用skinning animation。对skinning mesh来说,一个MP中的顶点不再只受到一个bone的影响,而有可能最多受到n个bone的影响,为了兼顾效率,实时计算中通常1<n<= 4。比如肩膀部位的顶点会受到脖子,躯干,手臂等部分骨骼的影响。此外,每个MP也不一定再对应着一个bone,有可能包含多个。假设用一个数组来保存模型中的所有骨骼:matrix[] bones;

      每个顶点就必须记录下它将受到哪几个骨骼的影响,这称为boneIndex。此外,每个顶点还需要记录每个bone对它影响的权重,称为boneWeight,所有权重的和必须为1。可以用独立的数据结构来保存boneIndex和boneWeight,但最常见的还是把它们作为顶点数据的一部分,可能使用这样的顶点结构:

struct Vertex
{
 vector3 position;
 vector3 normal;
 vector2 UV
 vector4 boneIndex;
 vector4 boneWeight;
 ……other data…..
}
计算顶点最终位置的公式为:
position;
resultPos;
foreach boneIndex in VertexBoneindices
    resultPos 
= position * bones[boneIndex] * boneWeight;

    现在问题来了,由于我们不知道一个MP中的某个顶点究竟受到哪几个bone的影响,所以不能再像之前那样,只为每个MP提供一个matrix就渲染。而是需要把整个bone数组都提供给MP。当然,要先计算出正确的matrix才行:
Matrix[] bones = Matrix[14];   //假设每个mp仍然只包含一个bone
worldMatrix = Matrix.CreateTranslate(x,y,z);
bones[
0= trunkMatrix =localTrunkMatrix * worldMatrix;
bones[
1= neckMatrix = neckLocalMatrix * TrunkMatrix;
bones[
2= headMatrix = neckMatrix * headMatrix;
bones[
3= leftShoulder = leftShoudlerlocalMatrix * TrunkMatrix;
……………………………..
drawMP(bones, trunkData)
drawMP(bones, neckData)

注意,每个bone在数组中的位置必须和顶点中记录的相同。

    把渲染skeletion animation和skinning mesh的技术结合起来,就得到了skinning animation。相关方法就不再详细讨论了。
    最后附上GPU渲染skinning mesh的HLSL代码:

float4x4 View;
float4x4 Projection;
float4x4 Bones[MaxBones];

// Vertex shader input structure.
struct VS_INPUT
{
    float4 Position : POSITION0;
    float3 Normal : NORMAL0;
    float2 TexCoord : TEXCOORD0;
    float4 BoneIndices : BLENDINDICES0;
    float4 BoneWeights : BLENDWEIGHT0;
};

// Vertex shader program.
VS_OUTPUT VertexShader(VS_INPUT input)
{
    VS_OUTPUT output;
    
    
// Blend between the weighted bone matrices.
    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;
    
    
// Skin the vertex position.
    float4 position = mul(input.Position, skinTransform);
    
    output.Position 
= mul(mul(position, View), Projection);

    
// Skin the vertex normal, then compute lighting.
    float3 normal = normalize(mul(input.Normal, skinTransform));
.
}

 

ps:注意,文中所有伪代码以及所用数据结构,shader只是出于演示目的,并未经过任何优化,请根据实际情况参考使用。

 

posted on 2009-05-17 22:24  clayman  阅读(2427)  评论(1编辑  收藏  举报