2024年最新DirectX12(D3D12)基础教程(十七)(4),2024年最新太现实了

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化资料的朋友,可以戳这里获取

	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)
{
nScalingIndex = 0;
if (!(pNodeAnim->mNumScalingKeys > 0))
{
return FALSE;
}

for (UINT i = 0; i < pNodeAnim->mNumScalingKeys - 1; i++)
{
	// 严格判断时间Tick是否在两个关键帧之间
	if ( ( AnimationTime >= (FLOAT)pNodeAnim->mScalingKeys[i].mTime )
		&& ( AnimationTime < (FLOAT)pNodeAnim->mScalingKeys[i + 1].mTime) )
	{
		nScalingIndex = i;
		return TRUE;
	}
}
return FALSE;

}


  上面的搜索代码可能会出问题,即当倒数第一帧的时间点mTime值刚好等于或大于AnimationTime时,可能会错误的返回索引0,从而使得动画又从头开始,导致动画出现“跳跃”感。正常情况下,这种情况不会出现,因为AnimationTime再传入时,已经对整个动画持续时间(pAnimation->mDuration)做了取余操作(fmod(TimeInTicks, (FLOAT)pAnimation->mDuration);),所以不会出现这样的错误。但不排除有些模型中,可能动画持续时间与具体的帧的总时间点不一致而导致此问题。此时可以考虑使用最后帧的mTime值取代mDuration值,对TimeInTicks做取余操作。


### 10.4、生成关键帧骨骼变换矩阵


  计算得到插值帧基本变换“向量”后,就是要使用这些“向量”来生成对应的变换矩阵。幸运的是DirectXMath库中同样为我们封装好了这些函数,只需要调用一下即可:



// 缩放
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;
// OpenGL:TranslationM* RotationM* ScalingM;


### 10.5、关于性能的一些考虑


  关于整个动画数据的解算过程就介绍完毕了,虽然这个过程很“经典”,但本章示例中的代码基本上还是按照Assimp导入数据的结构来设计的算法,基本上保持了一种“原汁原味”“经典”的味道,这样做的目的即让大家能够轻松并且牢固的掌握骨骼动画计算的整个过程及基本原理。


  实际的正式代码中,这个过程需要做很多优化,具体的优化方法,就不多说了,以防内容太多,分散精力导致大家学习效果的下降。这里只提几个基本的思路:


  1、之前的篇章中已经说过,可以将Assimp导入的模型数据理解为是一种中间数据(ID),那么这其实也是说,我们完全可以按照提高性能的要求来设计对应的自有格式的数据结构来做一次“预转换”,比如将树形结构的“骨架树”拆分为线性结构,并且将分离在骨架(aiNode树)以及aiAnimation中的信息整合为一个完整的信息,不需要每次都去根据名称查找每组数据。(如下图,使用先序遍历的线性结构存储即可,当然要反过来在子节点中存储父节点的指针,方便级联的计算。)  


![img](https://img-blog.csdnimg.cn/img_convert/4f7055dfb2fe9ab1e6a3b757bc29f8c3.png)
![img](https://img-blog.csdnimg.cn/img_convert/0c4150d9e2be3fdf5832a7cc0e985e0c.png)

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

-Xbzd6Xdj-1715647492608)]
[外链图片转存中...(img-gYDKSnNR-1715647492608)]

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值