Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二十三章:角色动画

原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二十三章:角色动画



学习目标

  1. 熟悉蒙皮动画的术语;
  2. 学习网格层级变换在数学理论,以及如何遍历基于树结构的网格层级;
  3. 理解顶点混合的想法以及数学理论;
  4. 学习如何从文件加载动画数据;
  5. 学习如何在D3D中实现角色动画。


1 框架的层级结构

Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二十三章:角色动画


1.1 数学公式

例如,有下面的结构:

Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二十三章:角色动画

每根子骨骼的坐标系都可以跟父骨骼关联,第一根骨骼与世界坐标系关联:

Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二十三章:角色动画

如果矩阵A0是第一根骨骼的世界变换矩阵,A1是第二根骨骼变换到第一根骨骼的矩阵,往后依次类推,那么第i根骨骼变换到世界坐标系的变换矩阵就是:

Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二十三章:角色动画

在我们上述的例子中,M2 = A2A1A0, M1 = A1A0 and M0 = A0,就是每根骨骼对于的世界坐标系变换矩阵:

Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二十三章:角色动画



2 蒙皮网格


2.1 定义

Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二十三章:角色动画

高光的那整条骨骼链叫做骨架(skeleton)。3D几何模型叫做皮肤(skin)。皮肤顶点与绑定空间相关联(整个皮肤相关联的局部坐标系)。每个骨骼影响一系列子皮肤的位置和形状。


2.2 重置骨骼到根空间的变换公式

和上述不同的地方是,把各个骨骼变换到世界坐标系的矩阵拆解开,先找到变换到根空间的矩阵,然后变换到世界坐标系;第二个不同点是从下往上,这样比从上往下更高效。第n根骨骼的变换如下:

Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二十三章:角色动画

这里p是骨骼i的父骨骼的编号,toRootp从p的局部坐标系映射到根局部坐标系。


2.3 抵消变换(Offset Transform)

有一个小问题是,被骨骼影响的顶点并不在骨骼坐标系统中,而是在绑定空间中。所以在应用公式对顶点进行变换之前,我们先要将顶点从绑定空间变换到影响它的骨骼的空间中,所以叫抵消变换(offset transformation)。

Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二十三章:角色动画

所以现在可以定义一个最终变换:

Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二十三章:角色动画


2.4 对骨架进行动画

我们定义了一个骨架动画的类SkinnedData.h/.cpp在Skinned Mesh Demo中。

我们首先对每个骨骼单独在局部坐标系移动,然后考虑其父节点的移动,然后变换到根空间。

我们定义一些列动画的动画片段(animation clip):

///<summary>
/// Examples of AnimationClips are "Walk", "Run", "Attack", "Defend".
/// An AnimationClip requires a BoneAnimation for every bone to form
/// the animation clip.
///</summary>
struct AnimationClip
{
// Smallest end time over all bones in this clip.
float GetClipStartTime()const; // Largest end time over all bones in this clip.
float GetClipEndTime()const; // Loops over each BoneAnimation in the clip and interpolates
// the animation.
void Interpolate(float t, std::vector<XMFLOAT4X4>& boneTransforms)const; // Animation for each bone.
std::vector<BoneAnimation> BoneAnimations;
};

我们可以使用unordered_map保存这些片段:

std::unordered_map<std::string, AnimationClip> mAnimations;
AnimationClip& clip = mAnimations["attack"];

最终,每个骨骼需要抵消变换矩阵,并且需要一个数据结构表示骨架结构。所以我们骨骼动画最终数据结构如下:

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;
};

2.5 计算最终变换

我们使用一个整形数组模拟骨架层级,第i个元素值是第i个骨骼的父骨骼ID,并且对应第i个offset transform,并且对应骨骼动画中的第i个骨骼的动画:

int parentIndex = mBoneHierarchy[i];
int grandParentIndex = mBoneHierarchy[parentIndex];
XMFLOAT4X4 offset = mBoneOffsets[grandParentIndex];
AnimationClip& clip = mAnimations["attack"];
BoneAnimation& anim = clip.BoneAnimations[grandParentIndex];

所以我们可以这样计算每个骨骼的最终变换:

void SkinnedData::GetFinalTransforms(const std::string& clipName,
float timePos, std::vector<XMFLOAT4X4>& finalTransforms)const
{
UINT numBones = mBoneOffsets.size();
std::vector<XMFLOAT4X4> toParentTransforms(numBones); // Interpolate all the bones of this clip at the given time instance.
auto clip = mAnimations.find(clipName);
clip->second.Interpolate(timePos, toParentTransforms); //
// Traverse the hierarchy and transform all the bones to the
// root space.
//
std::vector<XMFLOAT4X4> toRootTransforms(numBones); // The root bone has index 0. The root bone has no parent, so
// its toRootTransform is just its local bone transform.
toRootTransforms[0] = toParentTransforms[0]; // Now find the toRootTransform of the children.
for(UINT i = 1; i < numBones; ++i)
{
XMMATRIX toParent = XMLoadFloat4x4(&toParentTransforms[i]);
int parentIndex = mBoneHierarchy[i];
XMMATRIX parentToRoot = XMLoadFloat4x4(&toRootTransforms[parentIndex]);
XMMATRIX toRoot = XMMatrixMultiply(toParent, parentToRoot);
XMStoreFloat4x4(&toRootTransforms[i], toRoot);
} // Premultiply by the bone offset transform to get the final transform.
for(UINT i = 0; i < numBones; ++i)
{
XMMATRIX offset = XMLoadFloat4x4(&mBoneOffsets[i]);
XMMATRIX toRoot = XMLoadFloat4x4(&toRootTransforms[i]);
XMStoreFloat4x4(&finalTransforms[i], XMMatrixMultiply(offset, toRoot));
}
}

当我们遍历骨骼的时候,我们需要查看父节点的to-root变换矩阵:

int parentIndex = mBoneHierarchy[i];
XMMATRIX parentToRoot = XMLoadFloat4x4(&toRootTransforms[parentIndex]);

它需要在访问前,已经创建好相关数据。我们的3D示例程序中的文件已经写好了这些数据:

ParentIndexOfBone0: -1
ParentIndexOfBone1: 0
ParentIndexOfBone2: 0
ParentIndexOfBone3: 2
ParentIndexOfBone4: 3
ParentIndexOfBone5: 4
ParentIndexOfBone6: 5
ParentIndexOfBone7: 6
ParentIndexOfBone8: 5
ParentIndexOfBone9: 8


3 顶点混合

对覆盖在骨架上的皮肤的顶点做动画,叫做顶点混合。

顶点混合的策略是:我们有一个基于骨骼的骨架,但是皮肤是一个连续的网格,并且可能有1个到多个骨骼同时影响一个顶点;最终的变换由多个骨骼影响权重平均后得到,这样就可以在关节处有一个平滑的过渡,它可以让皮肤看起来有弹性,如下图:

Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二十三章:角色动画

实际应用中,[Möller08]支持,通常情况下我们不需要多余4跟骨骼影响同一个顶点。所以我们的设计会考虑到最多4个骨骼影响同一个顶点。所以为了实现顶点混合,角色网格还是连续的网格,每个顶点包含4个骨骼矩阵画板的索引(指向4个最终变换矩阵);另外每个顶点也包含4个权重对应用每个骨骼的影响权重。所以我们定义下面的顶点结构来实现顶点混合(skinned mesh)。

Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二十三章:角色动画

那么顶点最终的位置就可以通过权重计算如下:

Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二十三章:角色动画

其中w0 + w1 + w2 + w3 = 1,法线和切线的计算也类似:

Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二十三章:角色动画

这里我们假设F矩阵不包含非均匀变换,并且在变换法线的时候我们需要使用逆转置矩阵。

下面的顶点着色器代码片段展示了具有4个最大骨骼影响的顶点混合的主要代码:

cbuffer cbSkinned : register(b1)
{
// Max support of 96 bones per character.
float4x4 gBoneTransforms[96];
}; struct VertexIn
{
float3 PosL : POSITION;
float3 NormalL : NORMAL;
float2 TexC : TEXCOORD;
float4 TangentL : TANGENT; #ifdef SKINNED
float3 BoneWeights : WEIGHTS;
uint4 BoneIndices : BONEINDICES;
#endif
}; struct VertexOut
{
float4 PosH : SV_POSITION;
float4 ShadowPosH : POSITION0;
float4 SsaoPosH : POSITION1;
float3 PosW : POSITION2;
float3 NormalW : NORMAL;
float3 TangentW : TANGENT;
float2 TexC : TEXCOORD;
}; VertexOut VS(VertexIn vin)
{
VertexOut vout = (VertexOut)0.0f; // Fetch the material data.
MaterialData matData = gMaterialData[gMaterialIndex]; #ifdef SKINNED
float weights[4] = { 0.0f, 0.0f, 0.0f, 0.0f };
weights[0] = vin.BoneWeights.x;
weights[1] = vin.BoneWeights.y;
weights[2] = vin.BoneWeights.z;
weights[3] = 1.0f - weights[0] - weights[1] - weights[2]; float3 posL = float3(0.0f, 0.0f, 0.0f);
float3 normalL = float3(0.0f, 0.0f, 0.0f);
float3 tangentL = float3(0.0f, 0.0f, 0.0f); for(int i = 0; i < 4; ++i)
{
// Assume no nonuniform scaling when transforming normals, so
// that we do not have to use the inversetranspose.
posL += weights[i] * mul(float4(vin.PosL, 1.0f),
gBoneTransforms[vin.BoneIndices[i]]).xyz; normalL += weights[i] * mul(vin.NormalL,
(float3x3)gBoneTransforms[vin.BoneIndices[i]]); tangentL += weights[i] * mul(vin.TangentL.xyz,
(float3x3)gBoneTransforms[vin.BoneIndices[i]]);
} vin.PosL = posL;
vin.NormalL = normalL;
vin.TangentL.xyz = tangentL;
#endif // Transform to world space.
float4 posW = mul(float4(vin.PosL, 1.0f), gWorld);
vout.PosW = posW.xyz; // Assumes nonuniform scaling; otherwise, need to
// use inverse-transpose of world matrix.
vout.NormalW = mul(vin.NormalL, (float3x3)gWorld);
vout.TangentW = mul(vin.TangentL, (float3x3)gWorld); // Transform to homogeneous clip space.
vout.PosH = mul(posW, gViewProj); // Generate projective tex-coords to project SSAO map onto scene.
vout.SsaoPosH = mul(posW, gViewProjTex); // Output vertex attributes for interpolation across triangle.
float4 texC = mul(float4(vin.TexC, 0.0f, 1.0f), gTexTransform);
vout.TexC = mul(texC, matData.MatTransform).xy; // Generate projective tex-coords to project shadow map onto scene.
vout.ShadowPosH = mul(posW, gShadowTransform); return vout;
}


4 从文件加载动画数据

我们使用的文件格式是.m3d(“model 3D.” 一个text文件),这个格式是用来简化加载和阅读,也不是优化。并且这个格式只用于本书。


4.1 文件头

文件头定义了组成模型的材质,顶点,三角形,骨骼和动画的个数:

***************m3d-File-Header***************
#Materials 3
#Vertices 3121
#Triangles 4062
#Bones 44
#AnimationClips 15

4.2 材质

下一块是一个材质列表,例如:

***************Materials*********************
Name: soldier_head
Diffuse: 1 1 1
Fresnel0: 0.05 0.05 0.05
Roughness: 0.5
AlphaClip: 0
MaterialTypeName: Skinned
DiffuseMap: head_diff.dds
NormalMap: head_norm.dds
Name: soldier_jacket
Diffuse: 1 1 1
Fresnel0: 0.05 0.05 0.05
Roughness: 0.8
AlphaClip: 0
MaterialTypeName: Skinned
DiffuseMap: jacket_diff.dds
NormalMap: jacket_norm.dds

MaterialTypeName参数代表加载哪个着色器代码


4.3 子集合

一个网格有1到多个子集合,一个子集是一组由同一个材质渲染的三角形,比如下面的汽车模型:

Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二十三章:角色动画

第i个子集对应使用第i个材质:

***************SubsetTable*******************
SubsetID: 0 VertexStart: 0 VertexCount: 3915 FaceStart: 0 FaceCount: 7230
SubsetID: 1 VertexStart: 3915 VertexCount: 2984 FaceStart: 7230 FaceCount: 4449
SubsetID: 2 VertexStart: 6899 VertexCount: 4270 FaceStart: 11679 FaceCount: 6579
SubsetID: 3 VertexStart: 11169 VertexCount: 2305 FaceStart: 18258 FaceCount: 3807
SubsetID: 4 VertexStart: 13474 VertexCount: 274 FaceStart: 22065 FaceCount: 442

4.4 顶点数据和三角形

下面的2个数据块是顶点和索引:

***************Vertices**********************
Position: -14.34667 90.44742 -12.08929
Tangent: -0.3069077 0.2750875 0.9111171 1
Normal: -0.3731041 -0.9154652 0.150721
Tex-Coords: 0.21795 0.105219
BlendWeights: 0.483457 0.483457 0.0194 0.013686
BlendIndices: 3 2 39 34
Position: -15.87868 94.60355 9.362272
Tangent: -0.3069076 0.2750875 0.9111172 1
Normal: -0.3731041 -0.9154652 0.150721
Tex-Coords: 0.278234 0.091931
BlendWeights: 0.4985979 0.4985979 0.002804151 0
BlendIndices: 39 2 3 0
… ***************Triangles*********************
0 1 2
3 4 5
6 7 8
9 10 11
12 13 14


4.5 骨骼偏移变换

骨骼偏移变换块,保存每个骨骼的对应的矩阵:

***************BoneOffsets*******************
BoneOffset0 -0.8669753 0.4982096 0.01187624 0
0.04897417 0.1088907 -0.9928461 0
-0.4959392 -0.8601914 -0.118805 0
-10.94755 -14.61919 90.63506 1
BoneOffset1 1 4.884964E-07 3.025227E-07 0
-3.145564E-07 2.163151E-07 -1 0
4.884964E-07 0.9999997 -9.59325E-08 0
3.284225 7.236738 1.556451 1


4.6 骨架

骨架数据块保存了骨骼列表,它的整数参数代表父骨骼的索引:

***************BoneHierarchy*****************
ParentIndexOfBone0: -1
ParentIndexOfBone1: 0
ParentIndexOfBone2: 1
ParentIndexOfBone3: 2
ParentIndexOfBone4: 3
ParentIndexOfBone5: 4
ParentIndexOfBone6: 5
ParentIndexOfBone7: 6
ParentIndexOfBone8: 7
ParentIndexOfBone9: 7
ParentIndexOfBone10: 7
ParentIndexOfBone11: 7
ParentIndexOfBone12: 6
ParentIndexOfBone13: 12


4.7 动画数据

最后一个数据块是动画片段。每一个动画包含一个可读取的名称,和一个骨架的关键帧列表:

***************AnimationClips****************
AnimationClip run_loop
{
Bone0 #Keyframes: 18
{
Time: 0
Pos: 2.538344 101.6727 -0.52932
Scale: 1 1 1
Quat: 0.4042651 0.3919331 -0.5853591 0.5833637
Time: 0.0666666
Pos: 0.81979 109.6893 -1.575387
Scale: 0.9999998 0.9999998 0.9999998
Quat: 0.4460441 0.3467651 -0.5356012 0.6276384

}
Bone1 #Keyframes: 18
{
Time: 0
Pos: 36.48329 1.210869 92.7378
Scale: 1 1 1
Quat: 0.126642 0.1367731 0.69105 0.6983587
Time: 0.0666666
Pos: 36.30672 -2.835898 93.15854
Scale: 1 1 1
Quat: 0.1284061 0.1335271 0.6239273 0.7592083

}

}
AnimationClip walk_loop
{
Bone0 #Keyframes: 33
{
Time: 0
Pos: 1.418595 98.13201 -0.051082
Scale: 0.9999985 0.999999 0.9999991
Quat: 0.3164562 0.6437552 -0.6428624 0.2686314
Time: 0.0333333
Pos: 0.956079 96.42985 -0.047988
Scale: 0.9999999 0.9999999 0.9999999
Quat: 0.3250651 0.6395872 -0.6386833 0.2781091

}
Bone1 #Keyframes: 33
{
Time: 0
Pos: -5.831432 2.521564 93.75848
Scale: 0.9999995 0.9999995 1
Quat: -0.033817 -0.000631005 0.9097761 0.4137191
Time: 0.0333333
Pos: -5.688324 2.551427 93.71078
Scale: 0.9999998 0.9999998 1
Quat: -0.033202 -0.0006390021 0.903874 0.426508

}

}

下面的代码展示了我们如何读取这些数据:

void M3DLoader::ReadAnimationClips(std::ifstream& fin,
UINT numBones,
UINT numAnimationClips,
std::unordered_map<std::string,
AnimationClip>& animations)
{
std::string ignore;
fin >> ignore; // AnimationClips header text for(UINT clipIndex = 0; clipIndex < numAnimationClips; ++clipIndex)
{
std::string clipName;
fin >> ignore >> clipName;
fin >> ignore; // { AnimationClip clip;
clip.BoneAnimations.resize(numBones);
for(UINT boneIndex = 0; boneIndex < numBones; ++boneIndex)
{
ReadBoneKeyframes(fin, numBones,
clip.BoneAnimations[boneIndex]);
} fin >> ignore; // }
animations[clipName] = clip;
}
} void M3DLoader::ReadBoneKeyframes(std::ifstream& fin,
UINT numBones,
BoneAnimation& boneAnimation)
{
std::string ignore;
UINT numKeyframes = 0;
fin >> ignore >> ignore >> numKeyframes;
fin >> ignore; // { boneAnimation.Keyframes.resize(numKeyframes); for(UINT i = 0; i < numKeyframes; ++i)
{
float t = 0.0f;
XMFLOAT3 p(0.0f, 0.0f, 0.0f);
XMFLOAT3 s(1.0f, 1.0f, 1.0f);
XMFLOAT4 q(0.0f, 0.0f, 0.0f, 1.0f); fin >> ignore >> t;
fin >> ignore >> p.x >> p.y >> p.z;
fin >> ignore >> s.x >> s.y >> s.z;
fin >> ignore >> q.x >> q.y >> q.z >> q.w; boneAnimation.Keyframes[i].TimePos = t;
boneAnimation.Keyframes[i].Translation = p;
boneAnimation.Keyframes[i].Scale = s;
boneAnimation.Keyframes[i].RotationQuat = q;
} fin >> ignore; // }
}

4.8 M3D加载器

加载器的完整代码在LoadM3D.h/.cpp,其中加载函数:

bool M3DLoader::LoadM3d(const std::string& filename,
std::vector<SkinnedVertex>& vertices,
std::vector<USHORT>& indices,
std::vector<Subset>& subsets,
std::vector<M3dMaterial>& mats,
SkinnedData& skinInfo)
{
std::ifstream fin(filename);
UINT numMaterials = 0;
UINT numVertices = 0;
UINT numTriangles = 0;
UINT numBones = 0;
UINT numAnimationClips = 0;
std::string ignore; if( fin )
{
fin >> ignore; // file header text
fin >> ignore >> numMaterials;
fin >> ignore >> numVertices;
fin >> ignore >> numTriangles;
fin >> ignore >> numBones;
fin >> ignore >> numAnimationClips; std::vector<XMFLOAT4X4> boneOffsets;
std::vector<int> boneIndexToParentIndex; std::unordered_map<std::string, AnimationClip> animations; ReadMaterials(fin, numMaterials, mats);
ReadSubsetTable(fin, numMaterials, subsets);
ReadSkinnedVertices(fin, numVertices, vertices);
ReadTriangles(fin, numTriangles, indices);
ReadBoneOffsets(fin, numBones, boneOffsets);
ReadBoneHierarchy(fin, numBones, boneIndexToParentIndex);
ReadAnimationClips(fin, numBones, numAnimationClips, animations);
skinInfo.Set(boneIndexToParentIndex, boneOffsets, animations); return true;
} return false;
}


5 角色动画Demo

正如在之前的着色器代码中所示,最终骨骼变换矩阵保存在常量缓冲中:

cbuffer cbSkinned : register(b1)
{
// Max support of 96 bones per character.
float4x4 gBoneTransforms[96];
};

所以我们需要添加新的常量缓冲:

struct SkinnedConstants
{
DirectX::XMFLOAT4X4 BoneTransforms[96];
}; std::unique_ptr<UploadBuffer<SkinnedConstants>> SkinnedCB = nullptr; SkinnedCB = std::make_unique<UploadBuffer<SkinnedConstants>>(
device, skinnedObjectCount, true);

我们需要对每一个动画角色添加SkinnedConstants,我们定义下面的结构:

struct SkinnedModelInstance
{
SkinnedData* SkinnedInfo = nullptr; // Storage for final transforms at the given time position.
std::vector<DirectX::XMFLOAT4X4> FinalTransforms; // Current animation clip.
std::string ClipName; // Animation time position.
float TimePos = 0.0f; // Call every frame to increment the animation.
void UpdateSkinnedAnimation(float dt)
{
TimePos += dt; // Loop animation
if(TimePos > SkinnedInfo->GetClipEndTime(ClipName))
TimePos = 0.0f; // Called every frame and increments the time position,
// interpolates the animations for each bone based on
// the current animation clip, and generates the final
// transforms which are ultimately set to the effect
// for processing in the vertex shader.
SkinnedInfo->GetFinalTransforms(ClipName, TimePos, FinalTransforms);
}
};

然后我们添加下面的数据到我们的渲染项目(render-item)结构中:

struct RenderItem
{
[…]
// Index to bone transformation constant buffer.
// Only applicable to skinned render-items.
UINT SkinnedCBIndex = -1; // Pointer to the animation instance associated with this render item.
// nullptr if this render-item is not animated by skinned mesh.
SkinnedModelInstance* SkinnedModelInst = nullptr;
[…]
};

每帧我们更新动画角色实例:

void SkinnedMeshApp::UpdateSkinnedCBs(const GameTimer& gt)
{
auto currSkinnedCB = mCurrFrameResource->SkinnedCB.get(); // We only have one skinned model being animated.
mSkinnedModelInst->UpdateSkinnedAnimation(gt.DeltaTime());
SkinnedConstants skinnedConstants;
std::copy(std::begin(mSkinnedModelInst->FinalTransforms),
std::end(mSkinnedModelInst->FinalTransforms), &skinnedConstants.BoneTransforms[0]);
currSkinnedCB->CopyData(0, skinnedConstants);
}

当我们渲染这些渲染项目的时候,我们绑定关联的最终骨骼变换进去:

if(ri->SkinnedModelInst != nullptr)
{
D3D12_GPU_VIRTUAL_ADDRESS skinnedCBAddress =
skinnedCB->GetGPUVirtualAddress() +
ri->SkinnedCBIndex*skinnedCBByteSize; cmdList->SetGraphicsRootConstantBufferView(1, skinnedCBAddress);
}
else
{
cmdList->SetGraphicsRootConstantBufferView(1, 0);
}

下面是本Demo截图,其中源动画模型和纹理都是取自Direct SDK并转换为.m3d格式:

Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二十三章:角色动画



6 总结

  1. 骨架是由树状父子结构的骨骼组成;
  2. 每个骨骼基于自己的局部坐标系运动,每个局部坐标系又与父骨骼的局部坐标系关联;所以我们可以创建一个to-parent矩阵,变换到父骨骼局部坐标系,直到变换到世界坐标系;
  3. to-root矩阵可以通过toRooti = toParenti计算;
  4. 骨骼偏移(bone-offset)变换使顶点有绑定空间变换到骨骼空间,它是基于每个骨骼的;
  5. 对顶点做动画叫顶点混合,每个顶点可以由多个骨骼基于权重影响,最终变换可以由v′ = w0vF0 + w1vF1 + w2vF2 + w3vF3计算,其中w0 + w1 + w2 + w3 = 1,它可以让皮肤动画更自然;
  6. 为了实现顶点混合,我们将每个骨骼的最终变换矩阵保存在一个列表中,最后放到常量缓冲中;然后对于顶点,保存矩阵索引列表和权重列表即可进行计算。


7 练习

上一篇:2.TypeScript 基础入门(二)


下一篇:[HeadFirst-HTMLCSS学习笔记][第十四章交互活动]