Skinned Mesh Animation Using Matrices(使用矩阵的蒙皮网格动画)

本文详细介绍了使用矩阵进行蒙皮网格动画的技术,包括骨骼框架的结构、动画数据处理、骨骼和权重的影响,以及如何通过计算矩阵实现动画过程。文章特别强调了在DirectX中矩阵乘法的顺序,并探讨了骨骼、顶点和动画关键帧在动画过程中的作用,以及如何通过矩阵将顶点从骨骼空间转换到根框架空间,以便进行渲染。
摘要由CSDN通过智能技术生成

Skinned Mesh Animation Using Matrices(使用矩阵的蒙皮网格动画)

原文链接:https://www.gamedev.net/tutorials/graphics-programming-and-theory/skinned-mesh-animation-using-matrices-r3577/
  这篇文章讲述了一种使用矩阵的蒙皮网格动画。游戏通常使用人物动画来实现行走、奔跑、射击等动作。这些人物动画通常使用蒙皮网格动画来渲染。可以使用多种建模软件(比如blender)来创建这种蒙皮网格和动画序列。然后这些模型就可以用一种与多个API导入兼容的格式导出。这篇文章概述了导入数据后的应用程序中的动画过程。建模、装配、动画、导出或导入的过程不在讨论范围内。

Notes:
  为简明起见,术语SRT被用作按照顺序依次应用缩放操作(Scale operation)、旋转操作(Rotatation operation)和平移操作(Translate operation)的过程的缩写。
  对于大多数应用程序,这是执行这些操作以实现预期结果的顺序。它对应的矩阵乘法的顺序因API而异。
  本文所示的矩阵乘法和向量矩阵乘法的顺序是DirectX所使用的顺序,并假定为“行向量”矩阵。
比如:

// order of multiplication in DirectX 
FinalVector = vector * ScaleMat * RotationMat * TranslationMat

在OpenGL中,这些乘法的顺序可能会被颠倒,因为OpenGL应用程序通常使用“列向量”矩阵。也就是说,要实现向量缩放,然后是旋转,然后是平移,方程将是:

// order of multiplication in OpenGL 
FinalVector = TranslationMat * RotationMat * ScaleMat * vector

在DirectX和OpenGL中,操作的最终结果是相同的——矢量位置按该顺序首先缩放、旋转和平移。
  设置蒙皮网格的动画依赖于缩放、旋转和平移(SRT)空间中的位置可以由矩阵表示的原理。此外,SRT的有序序列可以由矩阵序列相乘得到的单个矩阵来表示,每个矩阵表示单个SRT。例如,final-matrix =SRTmatrix1(rot1后接trans1)* SRTmatrix2(rot2后接trans2)。在该示例中,当final-matrix应用到某个向量时,final-matrix将导致该向量旋转(rot1),然后平移(trans1),然后旋转(rot2),然后平移(trans2)。

动画蒙皮网格的组件

“动画”是指移动或看起来像活着一样移动。“Skinned”是指具有框架或“bone”层次结构的网格,通过该框架或“bone”层次结构,可以在空间中的不同位置渲染网格顶点,以模拟运动,例如跑步、挥舞等。“mesh”是指形成要绘制的面(三角形、四边形)的顶点(空间中的位置)的集合,可能以不同的方向(SRT)绘制。

什么是“bone”?

之所以使用“框架”一词,是因为它指的是数学上的“参照系”,或者是相对于(例如)世界的方向(SRT)。术语“骨骼”经常被用来代替“框架”,因为这个概念可以用来模拟骨骼如何导致周围的皮肤移动。如果手臂抬起,手臂的骨骼会导致骨骼周围的皮肤向上移动。然而,术语“骨”意味着有一个长度与之相关。本文中描述的动画类型中使用的骨骼帧没有关联的长度。尽管骨骼的“长度”可能被认为是骨骼与它的某个子骨骼之间的距离(比如arm骨骼与hand骨骼距离13英寸),但是用于蒙皮网格的骨骼可能有多个子骨骼,并且这些子骨骼不必与父骨骼处于相同的距离。例如,人类角色有一块颈部骨骼,左右肩骨,都是脊骨的子骨骼,这并不罕见。这些子骨骼与脊椎骨骼的距离不必相同。
在这里插入图片描述

在公共框架层次结构中,除根框架外的每个框架都有一个父框架。这使得整个层次结构可以用一个矩阵进行缩放、旋转和平移。请看下面的框架层次结构。紧靠在另一个框架下的框架名称缩进表示子-父关系。例如,髋部框架(Hip)是根框架(Root frame)的子框架。左大腿框架(Left Thigh)是臀部框架(Hip)的子框架。但请注意,脊椎框架(Spine)与臀部框架(Hip)处于同一缩进水平。这表明脊椎(Spine)是臀部框架(Hip)的兄弟,也是根框架(Root frame)的子骨骼。如果根框架(Root frame)应用了SRT,那么SRT会沿着整个树从一个子树传播到另一个子树。例如,如果根框架(Root frame)被旋转,臀部(Hip)被根框架(Root frame)旋转,左大腿(Left Thigh)被臀部(Hip)旋转,等等。同样,如果根框架被平移,整个层次被平移。在动画蒙皮网格中,SRT矩阵可应用于层次中的任何框架。该SRT将仅传播到框架的子框架,通过框架的子框架传播到子框架的子框架等。对该框架的父框架没有影响。这样,左上臂可以向上摆动。左下臂和左手也会向上旋转。但是,左锁骨、脊柱和根架不会移动,而且挥舞的动作会如人们所预料的那样出现。
框架层次结构示例如下(原文没有缩进,译者猜测缩进如下):
Root frame
 Hip
  Left Thigh
   Left Shin
    Left Foot
  Right Thigh
   Right Shin
    Right Foot
 Spine
 Neck
 Head
  Left Clavicle
   Left Upper Arm
    Left Lower Arm
     Left Hand
  Right Clavicle
   Right Upper Arm
    Right Lower Arm
     Right Hand
应该注意的是,其他对象,如建筑起重机,或办公椅,其中座椅和椅背可以独立旋转,可以在完全相同的方式动画。但是,蒙皮过程的优点是,在顶点具有适当的骨骼权重的情况下,网格可以平滑变形,就像以非线性方式弯曲一样。例如,当手臂抬起时,胸部和肩部之间的皮肤似乎在伸展。如果一只手被旋转,看起来下手臂和手之间的皮肤会拉伸和旋转。本文讨论了使用矩阵的蒙皮网格动画中实现的框架(骨骼)。在动画过程中,每个框架在单个渲染周期中都有多个与其关联的矩阵。例如,每个骨骼都有一个与该骨骼相对于根框架的SRT相关的“偏移”矩阵;每个骨骼都有一组动画“key点”(矩阵),用于在动画期间通过该骨骼和父骨骼的相对位置确定该骨骼的方向;每个骨骼都有一个动画矩阵和一个“最终”矩阵。这看起来可能很复杂,但可以通过一步一步地分析这个过程来理解。

骨骼框架的结构

在继续之前,请考虑如何在代码中实际表示“框架”的示例。下面使用的伪代码看起来很像C或C++,但是很容易转换成程序员觉得更熟悉的其他语言。

struct Frame { 
	string Name; // the frame or "bone" name 
	Matrix TransformationMatrix; // to be used for local animation matrix 
	MeshContainer MeshData; // perhaps only one or two frames will have mesh data 
	FrameArray Children; // pointers or references to each child frame of this frame 
	Matrix ToParent; // the local transform from bone-space to bone's parent-space 
	Matrix ToRoot; // from bone-space to root-frame space
 }; 

通常,每个框架都有一个名称。在上面的层次结构中,所有的框架(根、髋、左大腿等)都将有一个与上面类似的结构体,并且结构体中的名称将对应于层次结构中的名称。框架结构的其他成员及其用途将在本文后面进行更详细的描述。

动画数据以及如何使用它

下面的数据表明需要什么信息来设置蒙皮网格的动画。并非所有数据都包含在框架层次结构中。通常,框架层次包含描述骨骼结构、网格结构以及骨骼和网格顶点之间关系的数据。所有这些数据都表示静止姿势中的蒙皮网格。动画数据通常单独存储和访问。这是有意的,因为每一组动画数据表示网格对应的单个动作,例如,行走动画数据对应“行走”、奔跑动画数据对应“奔跑”等。这些动画集可能不止一个,但每个集都可以用于给相同的框架层次设置动画。蒙皮网格动画所需的总数据包括:

  • 处于某个“姿势”或“静止”位置的网格,可能包含在框架中;
  • 框架层次结构。层次结构通常包括:一个根框架;对于每个框架,都有一个list或者array用于保存指向该框架的子框架的指针;对于每个框架,有一个标识,表示该框架是否包含网格;对于每个框架,有一个或多个存储框架相关的SRTs矩阵
  • 一些存放影响骨骼数据的数组(various arrays of influence bone data)。主要包含:偏移矩阵;顶点索引的数组,以及骨骼影响该顶点的权重。
  • 动画的数组,主要包含:一个标识,表示这个动画应用于哪个骨骼;“keys”的数组,每个key主要包含:一个表示动画序列中的时间的滴答数(tick count) ;该滴答数对应的SRTs矩阵
  • 一个标识,表示每秒应该处理多少个ticks

网格

网格数据由顶点数组、相对于根框架(或包含网格的框架)的位置以及其他数据(如法向量、纹理坐标等)组成。顶点位置通常处于某种姿势或静止位置(译者:比如常见的T-Pose)。如果网格数据属于某个框架的部分,而不是根框架,则蒙皮过程必须考虑到这一点。这在下面的偏移矩阵中会讨论。动画网格通常在着色器或效果(effect)中渲染。这些着色器期望在渲染过程中以特定顺序输入顶点信息。将导入的逐顶点数据转换为与特定着色器或效果兼容的顶点格式的过程超出了本文的范围。

初始化数据

上面我们了解到了蒙皮网格动画所需的数据,下面我们接着概述使动画成为可能的过程。许多必需的数据是从外部文件加载的。将创建一个框架层次结构,并参照上面的框架结构,至少填充以下数据:Name(名称)、MeshData(不是所有框架)、children(如果框架有子框架)和ToParent矩阵。每个帧的ToRoot矩阵初始化如下:

// given this function ... 
function CalcToRootMatrix( Frame frame, Matrix parentMatrix ) { 
        // transform from frame-space to root-frame-space through the parent's ToRoot matrix 
        frame.ToRoot = frame.ToParent * parentMatrix; 
        for each Child in frame: {
            CalcToRootMatrix( Child, frame.ToRoot ); 
        } 
}

        // ... calculate all the Frame ToRoot matrices 
        CalcToRootMatrix( RootFrame, IdentityMatrix ); 
        // the root frame has no parent

递归函数CalcToRootMatrix可能第一眼看上去很难,但是下面的表达式可以表示出该函数做了些什么:

frame.ToRoot = frame.ToParent * frame-parent.ToParent * frame-parent-parent.ToParent * ... * RootFrame.ToRoot

动画所需的一些数据将仅应用于那些影响网格顶点的骨骼。在渲染过程中,将通过骨骼索引而不是骨骼名称来访问该数据。SkinInfo对象或函数通过使用骨骼影响数据数组,来提供该数组中影响骨骼的数量[SkinInfo.NumBones()]并可以返回给定骨骼索引对应的骨骼名称[SkinInfo.GetBoneName(boneIndex)]。

从初始化过程中稍微转移一下注意力

虽然不是初始化过程的一部分,但是理解框架的TransformationMatrix用于什么,将有助于理解为什么动画需要一组“偏移”矩阵。通过使用动画数组在每个渲染周期中填充TransformationMatrix。“动画控制器”对象或函数使用动画数据来计算这些变换,并将结果存储在每个框架的TransformationMatrix中。
该矩阵将顶点从骨骼的参考框架转换为骨骼的父骨骼的动画参考框架。与应用于“姿势”位置的ToParent矩阵类似,此矩阵应用于“动画”位置-网格在动画期间的显示方式。可以将此矩阵视为“骨骼将如何从姿势位置更改为动画位置”。在下图中,角色的手将被影响骨骼稍微旋转。
在这里插入图片描述

Bone-frame Rotation 如果将骨骼的变换矩阵应用于其影响的顶点,则位于根框架空间中的顶点位置将在根框架空间中旋转。

在这里插入图片描述

但是需要另一个矩阵,将顶点转换为骨骼空间,以便正确应用旋转。

回到初始化 - 偏移矩阵

偏移矩阵已经由许多文件格式直接提供,不需要为每个影响骨骼显式计算偏移矩阵。提供以下信息是为了理解偏移矩阵的用途。(译者个人理解:偏移矩阵就是顶点从它原本对应的骨骼框架坐标系,变换到某个影响骨骼框架坐标系的变换矩阵)
要使顶点在动画期间由影响骨骼正确变换,必须将该顶点从根框架中的“姿势”位置变换到影响骨骼的姿势框架。一旦变换,骨骼的TransformationMatrix可以应用于“顶点将如何从姿势位置更改为动画位置”,如上面显示的Bone-frame Rotation 所示。在“姿势”位置,顶点到骨骼空间的变换称为“偏移”矩阵。如果网格位于父框架而不是根框架中,则必须先将其从父框架转换到根框架空间,然后才能从上一段中描述的“根框架中的姿势”位置进行转换。幸运的是,使用网格的父框架ToRoot矩阵很容易实现这一点。每个影响骨骼框架都有一个ToRoot矩阵,但它从骨骼空间转换到根框架空间。这种转变的逆过程是需要的。矩阵数学提供了这样一种转换——矩阵的逆。简单地说,无论顶点与矩阵相乘做什么,顶点与矩阵的逆相乘做相反的事情。偏移矩阵的数组可以按如下方式计算://函数用于在层次结构中搜索名为“frameName”的帧并返回对该帧的引用

Frame FindFrame( Frame frame, string frameName ) 
{  
    Frame tmpFrame; 
    if ( frame.Name == frameName ) return frame; 
    for each Child in frame 
    { 
        if ( (tmpFrame = FindFrame( Child, frameName )) != NULL ) return tmpFrame; 
    }
    
    return NULL; 
}
// Note: MeshFrame.ToRoot is the transform for moving the mesh into root-frame space. 
function CalculateOffsetMatrix( Index boneIndex ) 
{
    string boneName = SkinInfo.GetBoneName( boneIndex ); 
    Frame boneFrame = FindFrame( root_frame, boneName );
    
    // error check for boneFrame == NULL 
    if desired offsetMatrix[ boneIndex ] = MeshFrame.ToRoot * MatrixInverse( boneFrame.ToRoot ); 
} 

// generate all the offset matrices 
for( int i = 0; i < SkinInfo.NumBones(); i++ ) 
    CalculateOffsetMatrix( i );

下面是一个偏移矩阵的伪表达式:

offsetMatrix = MeshFrame.ToRoot * Inverse( bone.ToParent * parent.ToParent * ... * root.ToParent )

将逆变换化简后的伪表达式:

offsetMatrix = MeshFrame.ToRoot * root.ToSomeChild * Child.ToAnotherChild * ... * boneParent.ToInfluenceBone

因为偏移矩阵只需要从“姿势”位置数据中计算,他们只需要被计算一次。

根框架

“根”是一个框架,所有其他骨骼都将它作为其父对象(译者:这里的父对象不一定是父子也有可能是骨骼的爷爷骨骼、祖父骨骼等等)。也就是说,每个骨骼都有一个父骨骼或爷爷骨骼(等)是根框架。如果根框架被缩放、旋转和平移,则整个层次结构将被缩放、旋转和平移。根框架可以相对于网格定位在任何位置。但是,对于已设置动画的角色网格,将其放置在角色的中线(例如,双脚之间)更为方便,就好像它躺在地面上一样。对于飞行物体,将根框架定位在角色的重心上可能更方便。这些细节在建模过程中确定。根框架没有父骨骼。它只有子骨骼。

骨骼和它的子骨骼

一些骨骼与一组顶点相关联,它们会影响这些顶点。“影响”意味着骨骼移动时顶点将随之移动。例如,较低的手臂骨骼(lower arm bone)可能会影响网格中从肘部到手腕的顶点。当下臂骨骼旋转(相对于上臂)时,这些顶点也随之旋转。下臂骨的移动也会导致手骨和手指骨的移动。受手骨和手指骨骼影响的顶点也将随之移动。包含上臂或网格其他部分的顶点不随下臂移动。如上所述,层次中的某些框架可能不会影响任何顶点。它们也许仍然可以称为“骨骼”,但不能称为“影响骨骼”。这些框架在动画期间仍然确定其子框架的方向,并且仍然对层次结构中的每一框架进行变换矩阵计算。
”姿势“位置中的网格和骨架结构  ”姿势“位置中的网格和骨架结构

骨骼影响和骨骼权重

此数据是一个数组,指定哪些骨骼影响哪个顶点,以及影响程度(通过什么权重因子)。对于每个骨骼,该数据是一些数据对:顶点索引和介于0和1之间的浮点数。顶点索引是网格顶点数组中的位置(网格在“姿势”位置中的位置)。权重是骨骼移动时顶点位置相对于骨骼移动的“多少”。如果多个骨骼影响同一顶点,则骨骼权重之和必须等于1才能正确渲染。例如,如果两个骨骼影响一个顶点,并且其中一个骨骼的骨骼权重为0.3,则另一个骨骼权重必须为0.7。最终顶点位置计算中的一些代码利用了这一要求。见下文。这些数据通常包含在SkinInfo对象中并由其维护。如上所述,SkinInfo对象如何执行其任务的详细信息超出了本项目的范围。

动画过程

上面的讨论描述了“姿势”位置的框架层次结构,为动画做好了准备。实时设置网格动画所需的数据通常与层次数据分离。这种分离是有意的。一组动画数据表示角色的单个动作:跑步、行走等。通过将动画数据从框架层次中分离出来,可以使用多组动画数据(每个动画数据都可以应用于“姿势”位置)来将角色的动作从跑步改为行走等,或者同时使用,例如“边跑边射击”。
动画数据通常与AnimationController对象一起存储并由其维护,可能只是一系列应用程序函数。动画控制器的工作细节超出了项目的范围。但是,下面将描述AnimationController执行的一些任务。单个角色动作的动画数据称为动画集,通常由框架动画数组组成。下面的伪代码是为了更好地理解结构,不应该被解释为可编译代码。动画集的组织可以类似于以下内容:

struct AnimationSet 
{ 
    string animSetName; 

    // for multiple sets, allows selection of actions 
    AnimationArray animations; 
} 
struct Animation 
{ 
    string frameName; 

    // look familiar? 
    AnimationKeysArray keyFrames; 
} 
struct AnimationKey 
{ 
    TimeCode keyTime; 
    Vector Scale, Translation; 
    Quaternion Rotation; 
}

Animation Keys

每个帧都与其关联了一组动画“keys”。这些keys在动画序列期间特定时间定义框架相对于其父框架方向(而不是根框架)的方向。“时间”可以表示为整数、时钟滴答计数。(时间也可以是浮点数字,表示相对于动画开始的时间)。
通常至少有2个这样的“定时”keys,一个用于动画的开始(count=0),一个用于动画的结尾(count=整个序列的滴答数)。在开始和结束计数之间,动画期间可能有不同计数的keys,定义自最后一个关键点(或序列内的时间)以来骨骼方向的更改,例如,对于一个100计数的动画:在计数0时,手臂骨骼降低。在第50点,手臂骨被抬高。在计数100时,手臂骨被降低到原来的位置。
在动画中,手臂开始下降,在计数50时平稳上升到其上升位置,在计数100时平稳下降到其原始位置。Keys通常存储为缩放和平移的向量,以及旋转的四元数,以使插值关键点的过程更容易。也就是说,在上述100计数的动画示例中,计数25处骨骼的SRT将在计数0处的Key和计数50处的Key之间的某处进行插值(计算)。
如果key存储为向量和四元数,则根据“上一个”key和“下一个”key的缩放、旋转和平移的插值计算矩阵(在示例中,从“上一个”key为计数0和“下一个”key为计数50)这些插值通常作为四元数的NLERP(标准化线性插值,
Normalized Linear IntERPolation)或SLERP(球面线性插值,
Spherical Linear intERPolation)和向量的LERP(线性插值,
Linear intERPolation)计算。
由于连续周期间的旋转变化通常很小,NLERP产生了令人满意的结果,而且速度更快。如果key被存储为矩阵,则“上一个”key和“下一个”key的key矩阵将分别分解为四元数和两个向量。对四元数和向量进行插值后,从结果四元数和向量计算出矩阵。然而,这不是一个可靠的做法,因为分解包含非均匀标度(non-uniform scaling)的矩阵可能会产生错误的结果。
当计算框架的矩阵(对于动画序列中的特定计数)时,该矩阵将存储在该框架的框架结构中,作为TranformationMatrix。如上所述,动画数据与框架层次结构分开存储和维护,因此可以使用上面示例的FindFrame()函数将每个框架矩阵存储在适当的位置。在每个渲染周期中,AnimationController可能执行以下操作:

function CalulateTransformationMatrices( TimeCode deltaTime ) 
{
    TimeCode keyFrameTime = startTime + deltaTime; 
    for each animation in AnimationSet:
    {
        Matrix frameTransform = CalculateFromAnimationKeys( keyFrameTime, animation.frameName ); 
        Frame frame = FindFrame( rootFrame, animation.frameName );
        frame.TransformationMatrix = frameTransform;
    } 
}

每秒的滴答数(Ticks per second)

蒙皮网格将实时设置动画以呈现给用户。”实时”的单位是秒,而不是滴答数。如果一个100计数的动画(升高和降低手臂)需要3秒,那么每秒的滴答声将等于100滴答/3秒,或者大约每秒33滴答声。若要开始动画,请将记号计数设置为0。
在场景的每次渲染过程中,自上次渲染以来的增量时间(通常只有几毫秒)将乘以每秒的记号数,并且记号数将按该值递增。对于要循环的动画,例如手臂的连续摆动,必须调整记号计数。也就是说,随着时间的推移,滴答数最终会超过100。
当这种情况发生时,滴答数将减少100,并且该过程将继续。也就是说,如果滴答数计数增加到107,则滴答数计数将调整为7,并且动画将重复。

还有更多的矩阵需要计算

真的吗?真的吗??是的,更多矩阵计算。上述偏移矩阵将顶点转换为对应的影响骨骼框架空间,该框架的TransformationMatrix可以应用于在该框架空间中对顶点进行动画变换。现在的任务是将顶点从框架动画空间转换为根框架动画空间,以便可以渲染顶点。
计算框架动画空间到根框架动画空间的转换过程可以使用与CalcToRootMatrix函数非常类似的例程来完成,该函数生成“姿势”框架空间到“姿势”根空间的变换。以下将计算“框架动画空间”到“根框架动画空间”的变换。
与其构建另一个完整数组来为每个框架保存根框架动画空间变换,不如考虑:Frame.TransformationMatrix能从框架空间转换为根空间。

// given this function ... f
unction CalcCombinedMatrix( Frame frame, Matrix parentMatrix ) 
{ 
    // transform from frame-space to root-frame-space through the parent's ToRoot matrix 
    frame.TransformationMatrix = frame.TransformationMatrix * parentMatrix; 
    for each Child in frame: {
        CalcCombinedMatrix( Child, frame.TransformationMatrix );
    }  
}
    // ... calculate all the Frame to-root animation matrices 
    CalcCombinedMatrix( RootFrame, IdentityMatrix );

(译者:上述计算就是通过深度优先遍历计算每个框架到根框架的变换矩阵。在执行这个计算之前,每个框架的TransformationMatrix存储的是由AnimationKey得到的相对于父框架的变换矩阵,因此需要上述计算,才能得到每个框架的TransformationMatrix对应的是从该框架到根框架的变换矩阵)

我们到了吗?

已经很接近了,但我们还没有在一个地方得到我们需要的所有信息。利用偏移矩阵将顶点位置变换为框架姿态空间。顶点位置可以用Frame.TransformationMatrix从框架动画空间转换到根动画空间。
为了供着色器(或其他渲染例程)使用,我们需要一个矩阵数组来执行上述两个操作。但是,我们只需要影响骨骼的矩阵。因此,我们可以使用一个矩阵数组叫做FinalMatrix作为OffsetMatrix和TransformationMatrix的乘积。但是,它只需要创建一次,因为它可以在每个渲染周期重复使用。计算最终变换的步骤如下:

// Given a FinalMatrix array.. 
function CalculateFinalMatrix( int boneIndex ) 
{ 
    string boneName = SkinInfo.GetBoneName( boneIndex ); 
    Frame boneFrame = FindFrame( root_frame, boneName ); 
    // error check for boneFrame == NULL if desired 
    FinalMatrix[ boneIndex ] = OffsetMatrix[ boneIndex ] * boneFrame.TransformationMatrix; 
} 

// generate all the final matrices 
for( int i = 0; i < SkinInfo.NumBones(); i++ ) 
    CalculateFinalMatrix( i );

它们是如何协同工作的

我们终于准备好了,可以实际渲染动画蒙皮网格了!对于每个渲染周期,将执行以下顺序:
1.动画“时间”将递增。该增量时间转换为滴答数(tick count)。
2.对于每个框架,都会根据框架的key计算定时key矩阵(timed-key-matrix)。如果滴答数为“介于”两个key之间,则计算的矩阵是下一个较小滴答计数的key,以及下一个较高的勾数的key对应的插值(译者:就是该key所在最小区间的边界值,比如keys=[0, 10,15,20,100],需要计算的key=12,则它所在的最小区间为[10,15])。
3.当计算出所有框架层次定时key矩阵时,将每个框架的定时key矩阵与其父框架的定时key矩阵相结合(译者:如“还有更多的矩阵需要计算”中所示)。
4.最终的变换(FinalMatrix)将计算并存储在一个数组中。
5.下一个操作通常在顶点着色器中执行,因为GPU硬件在执行所需计算时更有效,尽管应用程序可以在系统内存中完成。

着色器通过将FinalMatrix阵列复制到GPU以及其他需要的数据(如model、view、projection变换、光照位置和参数、纹理数据等)来初始化。每个网格顶点都乘以影响该顶点的骨骼的FinalMatrix,然后乘以骨骼的权重。
这些计算结果被相加,从而得到一个权重混合位置。如果顶点有相关的法线向量,则会进行类似的计算和求和。然后,加权位置(见下文)乘以model、view、projection矩阵,将其从根框架空间转换为世界空间到齐次剪辑空间。如上所述,合理的渲染要求顶点的混合权重(影响骨骼的权重)的总和为1。
强制执行该假设的唯一方法是确保模型在导入动画应用程序之前正确创建。然而,一个简单的代码可以帮助并减少必须传递给顶点位置计算的骨骼权重大小。如下:

// numInfluenceBones is the number of bones which influence the vertex 
// Depending on the vertex structure passed to the shader, it may passed in the vertex structure 
// or be set as a shader constant 
float fLastWeight = 1; 
float fWeight; vector vertexPos( 0 ); 

// start empty 
for (int i=0; i < numInfluenceBones-1; i++) // N.B., the last boneweight is not need! 
{ 
    fWeight = boneWeight[ i ]; 
    vertexPos += inputVertexPos * final_transform[ i ] * fWeight; 
    fLastWeight -= fWeight; 
} 
vertexPos += inputVertexPos * final_transform [ numInfluenceBones - 1 ] * fLastWeight;

总结

蒙皮网格的数据加载到应用程序或由应用程序计算。该数据由以下数据组成:
-网格顶点数据。对于每个顶点:相对于骨骼层次的框架的位置
-框架层次数据。对于每个框架:框架的子框架、偏移矩阵、动画key框架数据
-骨骼影响数据。通常以每个骨骼的数组形式列出骨骼影响的每个顶点的索引和权重。

上面描述的许多操作可以组合成较少的步骤并且以其他方式简化。本文的目的是提供使用矩阵设置蒙皮网格动画所涉及的过程的描述,但不一定以有效的方式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值