- 注意!本文是在下几年前入门期间所写(young and naive),其中许多表述可能不正确,为防止误导,请各位读者仔细鉴别。
骨骼动画
这篇文章对应的是龙书的最后一章,里面用到的模型和动画文件格式都不是fbx,所以具体怎么读取我们不用去关心,知道怎么用就行了,格式变一下原理还是一样的。
动画原理简介
首先骨骼是个树状的存储结构,父骨骼动的时候每个子骨骼都会跟着动,然后每个骨骼有自己的坐标空间,要把点从骨骼空间变换到坐标空间,必须从当前骨骼开始一路向上乘上每个父骨骼的变换矩阵最后才变换到世界空间
但是这样算的话,我们每个骨骼都去乘父骨骼的变换矩阵,就重复算了很多次,如果我们自上而下地计算变换矩阵,就可以省下重复的计算。我用们i表示第i根骨骼,p表示父骨骼,toRoot表示变换到根的变换矩阵,toParent表示到父骨骼空间的变换矩阵,那么有
这样我们就可以自顶向下地计算出每根骨骼到root的变换矩阵。
然后如果要把模型局部坐标系下(也就是绑定空间下)的点变换到已经播放了动画的root空间下,我们要先把点从绑定空间变换到骨骼的空间,再从空间空间变换到root空间,每根骨骼都有自己的空间,我们把将点从绑定空间变换到骨骼的空间的矩阵叫做offset矩阵,从骨骼空间变换到root空间的矩阵则是上面说的toRoot矩阵,如下图所示。
可以看出动画的信息其实是包含在toRoot矩阵里的,offset矩阵则是固定的,也就是根据骨骼相对于绑定空间的位置算出来的,toRoot则是我们在播放动画的时候每帧都要更新的。我们把这两个矩阵的乘积叫做Final矩阵,简记作Fi,如下
然后如果我们要从上至下地计算Final矩阵,我们必须要保证遍历一次骨骼列表的时候父骨骼一定在子骨骼前面,也就是要拓扑排序好的骨骼列表才能这样计算,下面是一个可行的例子
可以看到这种情况下我们是可以直接迭代计算final矩阵的,因为所有父骨骼都比子骨骼更早出现。
顶点混合
现在假如我们有了所有骨骼的Final矩阵,我们要怎么计算顶点位置呢?下面的demo里因为情况简单所以最多只允许一个顶点被四根骨骼影响,如果最多4根骨骼,每根骨骼的权重为wi,权重的和为1,那么顶点当前位置该这样计算
然后法线和切线应该这样变换
这里我们认为只有各向等比例的缩放,实际上如果有不等比例的缩放,我们这里应该乘的是Final矩阵的逆转置。
骨骼动画Demo
这里我们用一个m3d文件来实现一个动画demo,这个m3d文件里包含了蒙皮好的模型、骨骼信息、动画,可以用M3DLoader来读取。
接下来给出关键部分的代码。
首先封装一些动画相关的类
struct Keyframe
{
Keyframe();
~Keyframe();
float TimePos;
DirectX::XMFLOAT3 Translation;
DirectX::XMFLOAT3 Scale;
DirectX::XMFLOAT4 RotationQuat;
};
struct BoneAnimation
{
float GetStartTime()const;
float GetEndTime()const;
void Interpolate(float t, DirectX::XMFLOAT4X4& M)const;
std::vector<Keyframe> Keyframes;
};
struct AnimationClip
{
float GetClipStartTime()const;
float GetClipEndTime()const;
void Interpolate(float t, std::vector<DirectX::XMFLOAT4X4>& boneTransforms)const;
std::vector<BoneAnimation> BoneAnimations;
};
class SkinnedData
{
public:
UINT BoneCount()const;
float GetClipStartTime(const std::string& clipName)const;
float GetClipEndTime(const std::string& clipName)const;
void Set(
std::vector<int>& boneHierarchy,
std::vector<DirectX::XMFLOAT4X4>& boneOffsets,
std::unordered_map<std::string, AnimationClip>& animations);
// In a real project, you'd want to cache the result if there was a chance
// that you were calling this several times with the same clipName at
// the same timePos.
void GetFinalTransforms(const std::string& clipName, float timePos,
std::vector<DirectX::XMFLOAT4X4>& finalTransforms)const;
private:
// Gives parentIndex of ith bone.
std::vector<int> mBoneHierarchy;
std::vector<DirectX::XMFLOAT4X4> mBoneOffsets;
std::unordered_map<std::string, AnimationClip> mAnimations;
};
一个BoneAnimation对应的是一个骨骼上的一串动画,一个AnimationClip则是由一组BoneAnimation组成,对应骨骼模型上每根骨骼的动画。而一个SkinnedData则包含了很多个AnimationClip,并且每个有一个字符串名字,存在一个unordered map里,此外,SkinnedData还包含了一个整数数组来存对应父骨骼的标号,-1表示这根骨骼是root骨骼,然后SkinnedData里还存了每根骨骼的Offset矩阵,这个矩阵就是骨骼对应在绑定空间下的变换矩阵。
然后BoneAnimiation插值的实现和上一章一样,而AnimationClip的插值则是循环对这个AnimationClip里的每个BoneAnimation进行插值。
void BoneAnimation::Interpolate(float t, XMFLOAT4X4& M)const
{
if( t <= Keyframes.front().TimePos )
{
XMVECTOR S = XMLoadFloat3(&Keyframes.front().Scale);
XMVECTOR P = XMLoadFloat3(&Keyframes.front().Translation);
XMVECTOR Q = XMLoadFloat4(&Keyframes.front().RotationQuat);
XMVECTOR zero = XMVectorSet(0.0f, 0.0f, 0.0f, 1.0f);
XMStoreFloat4x4(&M, XMMatrixAffineTransformation(S, zero, Q, P));
}
else if( t >= Keyframes.back().TimePos )
{
XMVECTOR S = XMLoadFloat3(&Keyframes.back().Scale);
XMVECTOR P = XMLoadFloat3(&Keyframes.back().Translation);
XMVECTOR Q = XMLoadFloat4(&Keyframes.back().RotationQuat);
XMVECTOR zero = XMVectorSet(0.0f, 0.0f, 0.0f, 1.0f);
XMStoreFloat4x4(&M, XMMatrixAffineTransformation(S, zero, Q, P));
}
else
{
for(UINT i = 0; i < Keyframes.size()-1; ++i)
{
if( t >= Keyframes[i].TimePos && t <= Keyframes[i+1].TimePos )
{
float lerpPercent = (t - Keyframes[i].TimePos) / (Keyframes[i+1].TimePos - Keyframes[i].TimePos);
XMVECTOR s0 = XMLoadFloat3(&Keyframes[i].Scale);
XMVECTOR s1 = XMLoadFloat3(&Keyframes[i+1].Scale);
XMVECTOR p0 = XMLoadFloat3(&Keyframes[i].Translation);
XMVECTOR p1 = XMLoadFloat3(&Keyframes[i+1].Translation);
XMVECTOR q0 = XMLoadFloat4(&Keyframes[i].RotationQuat);
XMVECTOR q1 = XMLoadFloat4(&Keyframes[i+1].RotationQuat);
XMVECTOR S = XMVectorLerp(s0, s1, lerpPercent);
XMVECTOR P = XMVectorLerp(p0, p1, lerpPercent);
XMVECTOR Q