ReadNodeHeirarchy(stMeshData, pAnimation, AnimationTime, pNode, mxIdentity);
UINT nNumBones = (UINT)stMeshData.m_arBoneDatas.GetCount();
for (UINT i = 0; i < nNumBones; i++)
{
arTransforms.Add(stMeshData.m_arBoneDatas[i].m_mxFinalTransformation);
}
}
### 10.2、递归遍历骨骼树ReadNodeHeirarchy
ReadNodeHeirarchy函数,是个比较老套的“先根序”“骨架”树结构递归遍历算法。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210718122208375.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQwMzgxNDM=,size_16,color_FFFFFF,t_70)
首先,需要将当前骨骼的默认变换矩阵(pNode->mTransformation)读取出来,依据D3D祖传左手坐标系的习惯,需要做个转置先。接着根据当前骨骼名称,查找(FindNodeAnim)动画数据中对应的关键帧节点。如果没有找到,其实按照之前所述,这往往表示当前骨骼可能是个“匿名”节点,仅作为“骨架”中的中间过渡连接节点,其默认变换矩阵即等于该骨骼最终的变换矩阵。
如果找到了当前骨骼对应的动画关键帧数组数据(Assimp中称为"通道mChannels"),那么根据“时间点AnimationTime”,先找到对应的关键帧数据元素,必要时进行插值,计算出该骨骼在当前帧需要的基本变换(SQT):缩放、旋转、位移对应的分量,再按D3D祖传左手坐标系顺序进行“变换复合”(OpenGL是相反的顺序,又因为我们直接使用的DirectXMath库,所以就不需要转置操作了),然后按顺序把矩阵乘起来(mxNodeTransformation = mxScaling \* mxRotationM \* mxTranslationM; ),最后用这个矩阵替代当前骨骼的变换矩阵,作为骨骼最终的变换矩阵。
接着,将当前骨骼变换矩阵乘以其父骨骼的变换矩阵( mxGlobalTransformation = mxNodeTransformation \* mxParentTransform),就得到了当前骨骼相对于整个模型空间的变换矩阵。然后根据骨骼名称,找到之前所述的骨骼数组中当前骨骼对应的元素索引,先将数组中的“逆位姿绑定矩阵”乘以一个当前骨骼变换矩阵再乘以整个模型的变换矩阵:
stMeshData.m_arBoneDatas[nBoneIndex].m_mxFinalTransformation
= stMeshData.m_arBoneDatas[nBoneIndex].m_mxBoneOffset
* mxGlobalTransformation
* stMeshData.m_mxModel;
就得到了最终的骨骼相对于世界空间中的变换矩阵。
void ReadNodeHeirarchy(ST_GRS_MESH_DATA& stMeshData
, const aiAnimation* pAnimation
, FLOAT AnimationTime
, const aiNode* pNode
, const XMMATRIX& mxParentTransform)
{
XMMATRIX mxNodeTransformation = XMMatrixIdentity();
MXEqual(mxNodeTransformation, pNode->mTransformation);
mxNodeTransformation = XMMatrixTranspose(mxNodeTransformation);
CStringA strNodeName(pNode->mName.data);
const aiNodeAnim\* pNodeAnim = FindNodeAnim(pAnimation, strNodeName);
if ( pNodeAnim )
{
// 缩放
XMVECTOR vScaling = {};
CalcInterpolatedScaling(vScaling, AnimationTime, pNodeAnim);
XMMATRIX mxScaling = XMMatrixScalingFromVector(vScaling);
// 四元数旋转
XMVECTOR vRotationQ = {};
CalcInterpolatedRotation(vRotationQ, AnimationTime, pNodeAnim);
XMMATRIX mxRotationM = XMMatrixRotationQuaternion(vRotationQ);
// 位移
XMVECTOR vTranslation = {};
CalcInterpolatedPosition(vTranslation, AnimationTime, pNodeAnim);
XMMATRIX mxTranslationM = XMMatrixTranslationFromVector(vTranslation);
// 骨骼动画中 最经典的 SQT 组合变换
mxNodeTransformation = mxScaling \* mxRotationM \* mxTranslationM;
}
XMMATRIX mxGlobalTransformation = mxNodeTransformation \* mxParentTransform;
UINT nBoneIndex = 0;
if (stMeshData.m_mapName2Bone.Lookup(strNodeName, nBoneIndex))
{
stMeshData.m_arBoneDatas[nBoneIndex].m_mxFinalTransformation
= stMeshData.m_arBoneDatas[nBoneIndex].m_mxBoneOffset
\* mxGlobalTransformation
\* stMeshData.m_mxModel;
}
for (UINT i = 0; i < pNode->mNumChildren; i++)
{
ReadNodeHeirarchy(stMeshData
, pAnimation
, AnimationTime
, pNode->mChildren[i]
, mxGlobalTransformation);
}
}
当前骨骼的最终世界空间变换矩阵计算出来后,首先存储到全局骨骼矩阵数组元素的成员中(stMeshData.m\_arBoneDatas[nBoneIndex].m\_mxFinalTransformation),接着就是循环递归遍历当前骨骼的子骨骼数组,并将这个矩阵作为子骨骼的父骨骼矩阵传入,因为所有骨骼的变换都是相对于自己的局部坐标系设定的,所以计算最终变换时,需要使用父骨骼的变换矩阵将自己变换到父骨骼的坐标空间中。这样“骨骼树”的最终含义就明确了,其实它就是骨骼的子坐标空间的层层级联。
最后需要注意的是stMeshData.m\_mxModel,这个模型的最终变换矩阵,本章示例中直接来自于模型导入数据中的根节点的变换矩阵,这个矩阵往往被设定为一个单位矩阵。当需要对模型进行进一步的整体变换时,还需要若干变换(缩放、旋转、位移等)矩阵继续右乘这个矩阵。
### 10.3、关键帧数据解算和插值
关键帧数据的解算,在本章示例中其实就是一个数组线性查找过程。
具体的,首先根据骨架中具体的骨骼名称找到对应的动画通道数据,也就是该骨骼对应的完整动作的关键帧数组:
const aiNodeAnim* FindNodeAnim(const aiAnimation* pAnimation, const CStringA strNodeName)
{
for (UINT i = 0; i < pAnimation->mNumChannels; i++)
{
if ( CStringA(pAnimation->mChannels[i]->mNodeName.data) == strNodeName)
{
return pAnimation->mChannels[i];
}
}
return nullptr;
}
接着要判断一下aiNodeAnim\* pNodeAnim(即pAnimation->mChannels,通道)中是否有多帧数据,也就是说看看这个数组元素数量是否大于1,根据Assimp的约定,一般情况下如果pAnimation->mChannels对应元素的指针不为空,那么至少都会有1帧变换数据,所以不会有0个元素的情况出现。如果是1帧数据的,那么就不管AnimationTime值,而直接返回这一帧数据即可。
在aiNodeAnim中,每组变换数据:缩放、旋转、位移都是分开存放的。一般情况下,这三个数组的大小是一致的,也就是说每一帧中都同时拥有这三个变换的数据,这也是一般的关键帧骨骼动画的基本要求。但要注意的是,并不总是这样。所以示例代码中也是分开这三个变换数据来处理的。而处理的逻辑基本都是一致的,即先查找对应AnimationTime时间点对应变换数据的数组索引(注意特殊设计的查找,总是保证找到的索引值小于数组上限-1,总是使得下一索引有效!),再根据当前帧索引(PositionIndex),+1得到下一帧索引(NextPositionIndex),计算两帧之间的时间差(DeltaTime),接着利用AnimationTime-当前帧的时间点值(pNodeAnim->mPositionKeys[PositionIndex].mTime),然后除以刚才计算得到的时差(DeltaTime)值,就得到了在两帧之间插值的t值(Factor)。最后利用对应的线性插值函数,在两帧各自的变换数据之间根据Factor值进行插值。
void CalcInterpolatedPosition(XMVECTOR& mxOut
, FLOAT AnimationTime
, const aiNodeAnim* pNodeAnim)
{
if (pNodeAnim->mNumPositionKeys == 1)
{
VectorEqual(mxOut, pNodeAnim->mPositionKeys[0].mValue);
return;
}
UINT PositionIndex = 0;
if (! FindPosition(AnimationTime, pNodeAnim, PositionIndex))
{// 当前时间段内没有位移的变换,默认返回0.0位移
mxOut = XMVectorSet(0.0f,0.0f,0.0f,0.0f);
return;
}
UINT NextPositionIndex = (PositionIndex + 1);
ATLASSERT(NextPositionIndex < pNodeAnim->mNumPositionKeys);
FLOAT DeltaTime
= (FLOAT)(pNodeAnim->mPositionKeys[NextPositionIndex].mTime
- pNodeAnim->mPositionKeys[PositionIndex].mTime);
FLOAT Factor
= (AnimationTime - (FLOAT)pNodeAnim->mPositionKeys[PositionIndex].mTime) / DeltaTime;
ATLASSERT(Factor >= 0.0f && Factor <= 1.0f);
VectorLerp(mxOut
, pNodeAnim->mPositionKeys[PositionIndex].mValue
, pNodeAnim->mPositionKeys[NextPositionIndex].mValue
, Factor);
}
void CalcInterpolatedRotation(XMVECTOR& mxOut, FLOAT AnimationTime, const aiNodeAnim* pNodeAnim)
{
if (pNodeAnim->mNumRotationKeys == 1)
{
QuaternionEqual(mxOut, pNodeAnim->mRotationKeys[0].mValue);
return;
}
UINT RotationIndex = 0;
if (!FindRotation(AnimationTime, pNodeAnim, RotationIndex))
{// 当前时间段内没有旋转变换,默认返回0.0旋转
mxOut = XMVectorSet(0.0f,0.0f,0.0f,0.0f);
return;
}
UINT NextRotationIndex = (RotationIndex + 1);
ATLASSERT(NextRotationIndex < pNodeAnim->mNumRotationKeys);
FLOAT DeltaTime = (FLOAT)(pNodeAnim->mRotationKeys[NextRotationIndex].mTime
- pNodeAnim->mRotationKeys[RotationIndex].mTime);
FLOAT Factor = (AnimationTime - (FLOAT)pNodeAnim->mRotationKeys[RotationIndex].mTime) / DeltaTime;
ATLASSERT(Factor >= 0.0f && Factor <= 1.0f);
QuaternionSlerp(mxOut
, pNodeAnim->mRotationKeys[RotationIndex].mValue
, pNodeAnim->mRotationKeys[NextRotationIndex].mValue
, Factor);
XMQuaternionNormalize(mxOut);
}
void CalcInterpolatedScaling(XMVECTOR& mxOut, FLOAT AnimationTime, const aiNodeAnim* pNodeAnim)
{
if ( pNodeAnim->mNumScalingKeys == 1 )
{
VectorEqual(mxOut, pNodeAnim->mScalingKeys[0].mValue);
return;
}
UINT ScalingIndex = 0;
if (!FindScaling(AnimationTime, pNodeAnim, ScalingIndex))
{// 当前时间帧没有缩放变换,返回 1.0缩放比例
mxOut = XMVectorSet(1.0f, 1.0f, 1.0f, 1.0f);
return;
}
UINT NextScalingIndex = (ScalingIndex + 1);
ATLASSERT(NextScalingIndex < pNodeAnim->mNumScalingKeys);
FLOAT DeltaTime = (FLOAT)(pNodeAnim->mScalingKeys[NextScalingIndex].mTime - pNodeAnim->mScalingKeys[ScalingIndex].mTime);
FLOAT Factor = (AnimationTime - (FLOAT)pNodeAnim->mScalingKeys[ScalingIndex].mTime) / DeltaTime;
ATLASSERT(Factor >= 0.0f && Factor <= 1.0f);
VectorLerp(mxOut
, pNodeAnim->mScalingKeys[ScalingIndex].mValue
, pNodeAnim->mScalingKeys[NextScalingIndex].mValue
, Factor);
}
关于Scale变换、Position变换的线性插值比较容易理解,最终使用DirectXMath库中的向量线性插值函数XMVectorLerp即可(为了方便兼容Assimp,做了简单封装,即函数VectorLerp)。而对于旋转变换(即Quaternion四元数,按前面所述,其实理解为方位变换更合适),则使用了四元数的球面线性插值,同样DirectXMath中也为我们准备了对应的函数XMQuaternionSlerp(一样做了简单封装,即函数QuaternionSlerp)。
最后关键帧搜索函数的代码如下:
BOOL FindPosition(FLOAT AnimationTime, const aiNodeAnim* pNodeAnim, UINT& nPosIndex)
{
nPosIndex = 0;
if (!(pNodeAnim->mNumPositionKeys > 0))
{
return FALSE;
}
for ( UINT i = 0; i < pNodeAnim->mNumPositionKeys - 1; i++ )
{
// 严格判断时间Tick是否在两个关键帧之间
if ( ( AnimationTime >= (FLOAT)pNodeAnim->mPositionKeys[i].mTime )
&& ( AnimationTime < (FLOAT)pNodeAnim->mPositionKeys[i + 1].mTime) )
{
nPosIndex = i;
return TRUE;
}
}
return FALSE;
}
BOOL FindRotation(FLOAT AnimationTime, const aiNodeAnim* pNodeAnim, UINT& nRotationIndex)
{
nRotationIndex = 0;
if (!(pNodeAnim->mNumRotationKeys > 0))
{
return FALSE;
}
for (UINT i = 0; i < pNodeAnim->mNumRotationKeys - 1; i++)
{
// 严格判断时间Tick是否在两个关键帧之间
if ( (AnimationTime >= (FLOAT)pNodeAnim->mRotationKeys[i].mTime )
&& (AnimationTime < (FLOAT)pNodeAnim->mRotationKeys[i + 1].mTime) )
{
nRotationIndex = i;
return TRUE;
}
}
return FALSE;
}
BOOL FindScaling(FLOAT AnimationTime, const aiNodeAnim* pNodeAnim, UINT& nScalingIndex)
{
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
[外链图片转存中…(img-1K2BOfZU-1714149191955)]
[外链图片转存中…(img-h6MQKyRJ-1714149191955)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新