zoukankan      html  css  js  c++  java
  • Skeletal Model and Skinning Animation

    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只是出于演示目的,并未经过任何优化,请根据实际情况参考使用。

  • 相关阅读:
    第34天-文件_system (2013.09.04)
    第33天-文件I/O _2(2013.09.03)
    小项目 : 计算库函数中单词的个数第30天
    第32天-文件I/O _1(2013.09.02)
    嵌入式培训学习历程第二十九天
    大作业 :学生信息管理系统。。。
    嵌入式培训学习历程第二十六天
    读取一个文件中哪一行 的一个参数
    LINUX C 语言 快速获取调用SHELL命令后的结果
    C语言制造一个随机数
  • 原文地址:https://www.cnblogs.com/clayman/p/1458991.html
Copyright © 2011-2022 走看看