模型的骨骼动画技术讲解.docx

上传人:b****4 文档编号:24254059 上传时间:2023-05-25 格式:DOCX 页数:17 大小:56.40KB
下载 相关 举报
模型的骨骼动画技术讲解.docx_第1页
第1页 / 共17页
模型的骨骼动画技术讲解.docx_第2页
第2页 / 共17页
模型的骨骼动画技术讲解.docx_第3页
第3页 / 共17页
模型的骨骼动画技术讲解.docx_第4页
第4页 / 共17页
模型的骨骼动画技术讲解.docx_第5页
第5页 / 共17页
点击查看更多>>
下载资源
资源描述

模型的骨骼动画技术讲解.docx

《模型的骨骼动画技术讲解.docx》由会员分享,可在线阅读,更多相关《模型的骨骼动画技术讲解.docx(17页珍藏版)》请在冰豆网上搜索。

模型的骨骼动画技术讲解.docx

模型的骨骼动画技术讲解

模型的骨骼动画技术讲解

骨骼动画实际上是两部分的过程。

第一个由美术执行,第二个由程序员(或者你写的引擎)执行。

第一部分发生在建模软件中,称为建模。

这里发生的是术定义了网格下面骨骼的骨架。

网格代表物体(无论是人类,怪物还是其他物体)的皮肤,骨骼用于移动网格物体,以模拟现实世界中的实际运动,这通过将每个顶点分配给一个或多个骨头来完成。

当顶点被分配给

骨骼时,定义了权重,该权重确定骨骼在移动时对顶点的影响量。

通常的做法是使所有权重

的总和1(每个顶点)。

例如,如果一个顶点位于两个骨骼之间,我们可能希望将每个骨骼的权重分配为0.5,因为我们希望骨骼在顶点上的影响相等。

然而,如果顶点完全在单个骨骼的影响之内,那么权重将为1(这意味着骨骼自主地控制顶点的运动)。

这是一个在混合器中创建的骨骼结构的例子:

我们上面看到的是动画的重要组成部分,美术将骨骼结构组合在一起,并为每个动画类型

(“步行”,“跑步”,“死亡”等)定义了一组关键帧。

关键帧包含沿着动画路径的关键点的

所有骨骼的变换。

图形引擎在关键帧的变换之间进行插值,并在它们之间创建平滑的运动。

用于骨骼动画的骨骼结构通常是继承的,这意味着骨骼有一个孩子/父母关系,所以创

建了一根骨头。

除了根骨之外,每个骨骼都有一个父母。

例如,在人体的情况下,您可以

将后骨分配为具有诸如手臂和腿部以及手指骨的儿童骨骼的根部。

当父骨骼移动时,它也

移动其所有的孩子,但是当孩子的骨骼移动时,它不会移动它的父母(我们的手指可以移动

而不移动手,但是当手移动它移动所有的手指)。

从实践的角度来看,这意味着当我们处理骨骼的变换时,我们需要将它与从它引导到根的所有父骨骼的转换结合起来。

我们不会再进一步讨论装备,它是一个复杂的主题,并且在图形程序员的领域之外。

模软件有先进的工具来帮助美术做这项工作,你需要成为一个很好的美术来创造一个好看的

网格和骨架。

让我们看看图形引擎需要做什么才能制作骨架动画。

第一阶段是用顶点骨骼信息来提取顶点缓冲区。

有几个选项可用,但我们将要做的很

简单。

对于每个顶点,我们将添加一个插槽阵列,其中每个插槽包含骨骼ID和权重。

了使我们的生活更简单,我们将使用具有四个插槽的数组,这意味着没有顶点可以受到四个

以上的骨骼的影响。

如果您要加载更多骨骼的模型,则需要调整阵列大小,但是对于作为

本博文一部分的Doom3模型,四个骨骼就足够了。

所以我们的新顶点结构将如下所示:

POSitiOn

TeXtUreCOOrdinateS

NOrmal

BOneIDO∣Weight0

BOneIDI∣Weight1

BOneID2[Weight2

BOneID3IWeight3

骨骼ID是骨转换数组的索引,这些变换将被应用在WVP矩阵之前的位置和正常(即它们

将顶点从“骨空间”转换成局部空间)。

权重将用于将几个骨骼的变换组合成单个变换,并且在任何情况下,总权重必须正好为1(建模软件的事情)。

通常,我们将在动画关键帧之

间进行插值,并在每个帧中更新骨骼变换数组。

骨骼转换阵列的创建方式通常是棘手的部分。

变换被设置在一个历史结构(即树)中,

通常的做法是在树中的每个节点中具有缩放向量,旋转四元数和平移向量。

实际上,每个

节点都包含这些项目的数组。

数组中的每个条目都必须有一个时间戳。

应用时间与其中一

个时间戳完全匹配的情况可能很少,因此我们的代码必须能够插值缩放/旋转/转换,以便在

应用程序的时间点获得正确的转换。

我们对每个节点从当前骨到根进行相同的过程,并将

这个变换链相加在一起以获得最终结果。

我们为每个骨骼做这些,然后更新着色器。

到目前为止,我们谈到的一切都是非常通用的。

但是这是一个关于使用ASSimP的骨

骼动画的博文,所以我们需要再次进入该库,读者可以自行下载一个ASSimP库,看看如何

使用它进行皮肤化。

ASSimP的好处是它支持从多种格式加载骨骼信息。

不好的是,您仍

然需要对其创建的数据结构进行相当多的工作,以生成您为着色器所需的骨骼转换。

后面给读者介绍一下关于ASSimP类的加载,一切都包含在aiScene类中(当我们导入网格

文件时我们得到的对象),aiScene包含一组aiMesh对象。

aiMesh是模型的一部分,并在顶点级别包含位置,法线,纹理坐标等内容。

现在我们看到aiMesh还包含一个aiBone对象

的数组。

毫无疑问,aiBOne代表网格骨架中的一个骨骼,每个骨骼都有一个名字,通过它可以在骨骼层级(见下文),顶点权重数组和4x4偏移矩阵中找到,我们需要这个矩阵的原因是因为顶点存储在通常的本地空间中,这意味着即使没有骨架动画,我们现有的代码库也

可以加载模型并正确渲染。

但是,骨干变化在骨骼空间中发挥作用(每个骨骼都有自己的空

间,这就是为什么我们需要将变换加在一起)。

因此,偏移矩阵的工作将顶点位置从网格的

局部空间移动到该特定骨骼的骨空间。

顶点权重数组是事物开始变得有趣的地方,该数组中的每个条目都包含aiMesh中顶点

数组的索引(请注意,顶点分布在几个长度相同的数组中)和权重。

所有顶点权重的总和

必须为1,但是要找到它们,您需要遍历所有骨骼,并将权重累加到每个特定顶点的列表中。

在我们的顶点级别构建骨骼信息之后,我们需要处理骨骼变换层级并生成将加载到着色

器中的最终转换,下图显示相关数据结构:

aiNode

StnngNamE

mat4x4TranSfOrmatiOn

aiNode*Parent

a⅛Node*ChiIdrenO

aiAnimation

doubleDUratiOn

ClOUbIeTiCkSPerSeCOnd

ChannetSo

StringNafne

VeCtor3DPOSttionSn

quaternionRotationsQ

VeCtOr3DSca)iπgs∏

再次,我们从aiScene开始,aiScene对象包含一个指向aiNode类对象的指针,该对象是一个节点层级的根(换句话说-一棵树),树中的每个节点都有一个指向其父项的指针以及指向其子节点的数组,这样我们可以方便地来回遍历树。

另外,节点执行从节点空间变换到

其父节点空间的变换矩阵。

最后,节点可能有也可能没有一个名字。

如果一个节点表示父

进制中的骨骼,则节点名称必须与骨骼名称相匹配。

但是有时节点没有名称(这意味着没

有相应的骨骼),而且他们的工作只是帮助模型分解模型并且沿着一些中间变换。

最后一块拼图是aiAnimation数组,它也存储在aiScene对象中,单个aiAnimation对

象表示一系列动画帧,例如"WaIk”,“run”,“shoot”等。

通过在帧之间进行内插,我们得

到与动画名称相匹配的所需视觉效果。

动画的持续时间为每秒钟的秒数(例如每秒100个

刻度和25个刻度,代表4秒动画),这有助于我们对进程进行时间调整,以使动画在每个硬

件上看起来相同。

另外,动画还有一个名为通道的aiNodeAnim对象的数组。

每个通道实

际上都是骨骼,全部是它的转变。

该通道包含一个名称,该名称必须与其他一个节点在层

级和三个转换数组中匹配。

为了计算特定时间点的最终骨骼变换,我们需要在这三个阵列中的每一个中找到与时间

匹配的两个入口,并在它们之间插值。

那么我们需要将转换组合成一个矩阵。

做完之后,

我们需要在根中找到相应的节点。

然后我们需要相应的通道为父,并进行相同的插值过程。

我们把这两个变化相乘合起来,直到我们达到根的层级。

加载模型的源代码实现如下:

[cpp]viewplaincopy在CODE上查看代码片派生到我的代码片

boolMesh:

:

LoadMesh(conststring&Filename)

{

//Releasethepreviouslyloadedmesh(ifitexists)

Clear();

//CreatetheVAO

glGenVertexArrays(1,&m_VAO);

glBindVertexArray(m_VAO);

//CreatethebuffersfortheverticesattributesglGenBuffers(ARRAY_SIZE_IN_ELEMENTS(m_Buffers),m_Buffers);

boolRet=false;

m_pScene=m_Importer.ReadFile(Filename.c_str(),aiProcess_Triangulate|aiProcess_GenSmoothNormals|

aiProcess_FlipUVs);

if(m_pScene){

m_GlobalInverseTransform=m_pScene->mRootNode->mTransformation;m_GlobalInverseTransform.Inverse();

Ret=InitFromScene(m_pScene,Filename);

}

else{

printf("Errorparsing'%s':

'%s'\n",Filename.c_str(),m_Importer.GetErrorString());

}

//MakesuretheVAOisnotchangedfromtheoutside

glBindVertexArray(0);

returnRet;

}

这是更新到Mesh类的入口点,更改标记为粗体,有一些我们需要注意的变化。

一个是导入和aiScene对象现在是类成员,而不是堆栈变量。

(关于阿Assimp模型的加载会在后面博客中讲解)原因是在运行时,我们将一次又一次地返回到aiScene对象,因此我们需要扩展导入器和场景的范围。

在一个真实的游戏中,您可能想要复制所需的东西,并以更优化的格式存储。

第二个变化是提取,反转和存储了根的层级转换矩阵,我们继续看下去。

请注意,矩

阵逆的代码已从Assimp库复制到我们的Matrix4f类中。

源代码的实现如下所示:

[cpp]viewplaincopy在CODE上查看代码片派生到我的代码片

(mesh.h)

structVertexBoneData

{

uintIDs[NUM_BONES_PER_VEREX];

floatWeights[NUM_BONES_PER_VEREX];

}

(mesh.cpp)

boolMesh:

:

InitFromScene(constaiScene*pScene,conststring&Filename)

{

vectorBones;

Bones.resize(NumVertices);

glBindBuffer(GL_ARRAY_BUFFER,m_Buffers[BONE_VB]);glBufferData(GL_ARRAY_BUFFER,sizeof(Bones[0])*Bones.size(),&Bones[0],GL_STATIC_DRAW);

glEnableVertexAttribArray(BONE_ID_LOCATION);glVertexAttribIPointer(BONE_ID_LOCATION,4,GL_INT,sizeof(VertexBoneData),(constGLvoid*)0);

glEnableVertexAttribArray(BONE_WEIGHT_LOCATION);glVertexAttribPointer(BONE_WEIGHT_LOCATION,4,GL_FLOAT,GL_FALSE,sizeof(VertexBoneData),(constGLvoid*)16);

}上面的结构包含了我们在顶点级别所需要的一切,默认情况下,我们有足够的存储空间用于四个骨骼(每个骨骼的ID和权重)。

VertexBoneData的结构就像这样,使之简单的传递给着色器。

我们已经分别在位置0,1和2处获得了位置,纹理坐标和法线。

因此,我们配

置的VAO来绑定位置3处的骨骼ID和位置4处的权重。

请注意,我们使用glVertexAttribIPointer而不是glVertexAttribPointer来绑定ID非常重要。

原因是ID是整数而不是浮点。

注意这一点,否则您将在着色器中收到损坏的数据。

[cpp]viewplaincopy在CODE上查看代码片派生到我的代码片

(mesh.cpp)

voidMesh:

:

LoadBones(uintMeshIndex,constaiMesh*pMesh,vector&Bones)

{

for(uinti=0;imNumBones;i++){

uintBoneIndex=0;

stringBoneName(pMesh->mBones[i]->mName.data);

if(m_BoneMapping.find(BoneName)==m_BoneMapping.end()){

BoneIndex=m_NumBones;m_NumBones++;

BoneInfobi;

m_BoneInfo.push_back(bi);

}

else{

BoneIndex=m_BoneMapping[BoneName];

}

m_BoneMapping[BoneName]=BoneIndex;

m_BoneInfo[BoneIndex].BoneOffset=pMesh->mBones[i]->mOffsetMatrix;

for(uintj=0;jmBones[i]->mNumWeights;j++){

uintVertexID=m_Entries[MeshIndex].BaseVertex+pMesh->mBones[i]->mWeights[j].mVertexId;

floatWeight=pMesh->mBones[i]->mWeights[j].mWeight;Bones[VertexID].AddBoneData(BoneIndex,Weight);

}

}

}

上述函数加载单个aiMesh对象的顶点骨骼信息。

它由Mesh:

:

InitMesh()调用。

除了填充VertexBoneData结构之外,此功能还可以更新骨骼名称和骨骼ID(由此功能管理的

运行索引)之间的映射,并将偏移矩阵存储在基于骨骼ID的向量中。

注意如何计算顶点ID。

由于顶点ID与单个网格相关,并且我们将所有网格存储在单个向量中,因此将当前aiMesh的基本顶点ID从mWeights数组中添加到顶点ID以获取绝对顶点ID。

[cpp]viewplaincopy在CODE上查看代码片派生到我的代码片voidMesh:

:

VertexBoneData:

:

AddBoneData(uintBoneID,floatWeight){

for(uinti=0;i

if(Weights[i]==0.0){

IDs[i]=BoneID;

Weights[i]=Weight;

return;

}

}

//shouldnevergethere-morebonesthanwehavespacefor

assert(0);

}

此功能函数在VertexBoneData结构中找到一个空闲插槽,并将骨骼ID和权重放在其中。

某些顶点将受到少于四个骨骼的影响,但是由于非现有骨骼的权重保持为零,这意味着我们可以对任意数量的骨骼使用相同的权重计算。

[cpp]viewplaincopy在CODE上查看代码片派生到我的代码片

Matrix4fMesh:

:

BoneTransform(floatTimeInSeconds,vector&Transforms)

{

Matrix4fIdentity;

Identity.InitIdentity();

floatTicksPerSecond

=m_pScene->mAnimations[0]->mTicksPerSecond!

=0?

m_pScene->mAnimations[0]->mTicksPerSecond:

25.0f;

floatTimeInTicks=TimeInSeconds*TicksPerSecond;

floatAnimationTime=fmod(TimeInTicks,m_pScene->mAnimations[0]->mDuration);

ReadNodeHeirarchy(AnimationTime,m_pScene->mRootNode,Identity);

Transforms.resize(m_NumBones);

for(uinti=0;i

Transforms[i]=m_BoneInfo[i].FinalTransformation;

}

}

在程序启动期间加载网格时,在前面看到的顶点级别的骨骼信息的加载仅完成一次。

现在我们来到第二部分,它计算每一帧进入着色器的骨骼变换,上述功能是切入点。

我们找到动画周期内的相对时间,并处理节点层级,最终是将一组转换返回给调用函数。

[cpp]viewplaincopy在CODE上查看代码片派生到我的代码片

voidMesh:

:

ReadNodeHeirarchy(floatAnimationTime,constaiNode*pNode,constMatrix4f&

ParentTransform)

{

stringNodeName(pNode->mName.data);

constaiAnimation*pAnimation=m_pScene->mAnimations[0];

Matrix4fNodeTransformation(pNode->mTransformation);

constaiNodeAnim*pNodeAnim=FindNodeAnim(pAnimation,NodeName);

if(pNodeAnim){

//InterpolatescalingandgeneratescalingtransformationmatrixaiVector3DScaling;

CalcInterpolatedScaling(Scaling,AnimationTime,pNodeAnim);

Matrix4fScalingM;ScalingM.InitScaleTransform(Scaling.x,Scaling.y,Scaling.z);

//InterpolaterotationandgeneraterotationtransformationmatrixaiQuaternionRotationQ;

CalcInterpolatedRotation(RotationQ,AnimationTime,pNodeAnim);

Matrix4fRotationM=Matrix4f(RotationQ.GetMatrix());

//InterpolatetranslationandgeneratetranslationtransformationmatrixaiVector3DTation;

CalcInterpolatedPosition(Translation,AnimationTime,pNodeAnim);Matrix4fTranslationM;

TranslationM.InitTranslationTransform(Translation.x,Translation.y,Translation.z);

//CombinetheabovetransformationsNodeTransformation=TranslationM*RotationM*ScalingM;

}

Matrix4fGlobalTransformation=ParentTransform*NodeTransformation;

if(m_BoneMapping.find(NodeName)!

=m_BoneMapping.end()){

uintBoneIndex=m_BoneMapping[NodeName];m_BoneInfo[BoneIndex].FinalTransformation=m_GlobalInverseTransformGlobalTransformation*m_BoneInfo[BoneIndex].BoneOffset;

}

for(uinti=0;imNumChildren;i++){

ReadNodeHeirarchy(AnimationTime,pNode->mChildren[i],GlobalTransformation);

}

}此函数遍历节点树,并根据指定的动画时间生成每个节点/骨骼的最终变换。

它的意义在于

它假定网格只有一个动画序列并且是有限的。

如果你想支持多个动画,你需要告诉它的动画名称并在m_pScene->mAnimations[]数组中搜索它,上面的代码对于我们使用的演示网格是足够好的。

从节点中的mTransformation成员初始化节点变换,如果节点不对应于骨骼,那么这是其最终的转换。

如果我们用生成的矩阵来覆盖它,这样做如下:

首先我们在动画的通道数组中搜索节点名称,然后我们基于动画时间内插缩放矢量,旋转四元数和平移矢量。

我们将它们组合成一个矩阵,并将其与我们得到的矩阵相乘(称为GlobablTransformation),此函数是递归的,并且以GlobalTransformation参数为单位矩阵为根节点进行调用。

每个节点递归地为其所有子节点调用此函数,并将其自身的变换作为GlobalTransformation传递。

我们从顶部开始会得到每个节点的组合转换链。

m_BoneMapping数组将节点名称映射到我们生成的索引中,我们将该索引用作存储m_BoneInfo数组,最后的变换计算如下:

我们从节点偏移矩阵开始,将顶点从其局部空间位

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > IT计算机 > 互联网

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1