游戏引擎开发实践(动画篇)

骨骼构建

骨骼信息读取

骨骼数据包含

  • 骨骼名称
  • 每个骨骼的父骨骼索引
  • 每个骨骼相对于父骨骼的变换(平移、旋转、缩放)
  • 整个骨架的变换矩阵
1
2
3
4
5
6
7
8
9
	std::vector<std::string> m_BoneNames;
	std::vector<uint32_t> m_ParentBoneIndices;
	// rest pose of skeleton. All in bone-local space (i.e. translation/rotation/scale relative to parent)
	std::vector<glm::vec3> m_BoneTranslations;
	std::vector<glm::quat> m_BoneRotations;
	std::vector<glm::vec3> m_BoneScales;
	// The skeleton itself can have a transform
	// Notably this happens if the whole "armature" is rotated or scaled in DCC tool
	glm::mat4 m_Transform;

另外骨骼信息应该独属于Mesh,应该用unique_ptr包裹指针

第一步直接把所以SubMesh的骨骼信息提取到Vector,这一步只是把Name提取出来了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void BoneHierarchy::ExtractBones()
{
	// Note: ASSIMP does not appear to support import of digital content files that contain _only_ an armature/skeleton and no mesh.
	for (uint32_t meshIndex = 0; meshIndex < m_Scene->mNumMeshes; ++meshIndex)
	{
		const aiMesh* mesh = m_Scene->mMeshes[meshIndex];
		for (uint32_t boneIndex = 0; boneIndex < mesh->mNumBones; ++boneIndex)
		{
			m_Bones.emplace(mesh->mBones[boneIndex]->mName.C_Str());
		}
	}

	// Extract also any nodes that are animated (but don't have any skin bound to them)
	for (uint32_t animationIndex = 0; animationIndex < m_Scene->mNumAnimations; ++animationIndex)
	{
		const aiAnimation* animation = m_Scene->mAnimations[animationIndex];
		for (uint32_t channelIndex = 0; channelIndex < animation->mNumChannels; ++channelIndex)
		{
			const aiNodeAnim* nodeAnim = animation->mChannels[channelIndex];
			m_Bones.emplace(nodeAnim->mNodeName.C_Str());
		}
	}
}

第二步一套代码从assimp中读取了骨骼信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
void BoneHierarchy::TraverseNode(aiNode* node, Skeleton* skeleton, const glm::mat4& parentTransform)
{
	if (m_Bones.find(node->mName.C_Str()) != m_Bones.end())
	{
		skeleton->SetTransform(parentTransform);
		aiNode* parent = node->mParent;
		while (parent)
		{
			parent->mTransformation = aiMatrix4x4();
			parent = parent->mParent;
		}
		TraverseBone(node, skeleton, Skeleton::NullIndex);
	}
	else
	{
		auto transform = parentTransform * Utils::Mat4FromAIMatrix4x4(node->mTransformation);
		for (uint32_t nodeIndex = 0; nodeIndex < node->mNumChildren; ++nodeIndex)
		{
			TraverseNode(node->mChildren[nodeIndex], skeleton, transform);
		}
	}
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void BoneHierarchy::TraverseBone(aiNode* node, Skeleton* skeleton, uint32_t parentIndex)
{
	uint32_t boneIndex = skeleton->AddBone(node->mName.C_Str(), parentIndex, Utils::Mat4FromAIMatrix4x4(node->mTransformation));
	for (uint32_t nodeIndex = 0; nodeIndex < node->mNumChildren; ++nodeIndex)
	{
		if (m_Bones.find(node->mChildren[nodeIndex]->mName.C_Str()) != m_Bones.end())
		{
			TraverseBone(node->mChildren[nodeIndex], skeleton, boneIndex);
		}
		else
		{
			// do not traverse any further.
			// It is not supported to have a non-bone and then more bones below it.
		}
	}
}

蒙皮权重处理

首先定义骨骼权重信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
	struct BoneInfluence
	{
		uint32_t BoneInfoIndices[4] = { 0, 0, 0, 0 };  // 影响当前顶点的骨骼索引(最多4个)
		float Weights[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; // 对应骨骼的影响权重(最多4个)

		void AddBoneData(uint32_t boneInfoIndex, float weight)
		{
			if (weight < 0.0f || weight > 1.0f)
			{
				HZ_CORE_WARN("Vertex bone weight is out of range. We will clamp it to [0, 1] (BoneID={0}, Weight={1})", boneInfoIndex, weight);
				weight = std::clamp(weight, 0.0f, 1.0f);
			}
			if (weight > 0.0f)
			{
				for (size_t i = 0; i < 4; i++)
				{
					if (Weights[i] == 0.0f)
					{
						BoneInfoIndices[i] = boneInfoIndex;
						Weights[i] = weight;
						return;
					}
				}

				// Note: when importing from assimp we are passing aiProcess_LimitBoneWeights which automatically keeps only the top N (where N defaults to 4)
				//       bone weights (and normalizes the sum to 1), which is exactly what we want.
				//       So, we should never get here.
				HZ_CORE_WARN("Vertex has more than four bones affecting it, extra bone influences will be discarded (BoneID={0}, Weight={1})", boneInfoIndex, weight);
			}
		}

		void NormalizeWeights()
		{
			float sumWeights = 0.0f;
			for (size_t i = 0; i < 4; i++)
			{
				sumWeights += Weights[i];
			}
			if (sumWeights > 0.0f)
			{
				for (size_t i = 0; i < 4; i++)
				{
					Weights[i] /= sumWeights;
				}
			}
		}
	};

另外还要骨骼信息

1
2
3
4
5
6
7
8
struct BoneInfo
{
	glm::mat4 InverseBindPose;
	uint32_t BoneIndex;
	BoneInfo(const glm::mat4& inverseBindPose, uint32_t boneIndex)
		: InverseBindPose(inverseBindPose), BoneIndex(boneIndex) {
	}
};

下来遍历每个SubMesh的骨骼信息来处理每个顶点的权重信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
				// skinning weights
		meshSource->m_Skeleton = AssimpAnimationImporter::ImportSkeleton(scene);
		if (meshSource->HasSkeleton())
		{
			HZ_CORE_INFO_TAG("Mesh", "开始处理骨骼信息");
			meshSource->m_BoneInfluences.resize(meshSource->m_Vertices.size());
			for (uint32_t m = 0; m < scene->mNumMeshes; m++)
			{
				aiMesh* mesh = scene->mMeshes[m];
				Submesh& submesh = meshSource->m_Submeshes[m];

				if (mesh->mNumBones > 0)
				{
					submesh.IsRigged = true;
					for (uint32_t i = 0; i < mesh->mNumBones; i++)
					{
						aiBone* bone = mesh->mBones[i];
						bool hasNonZeroWeight = false;
						for (size_t j = 0; j < bone->mNumWeights; j++)
						{
							if (bone->mWeights[j].mWeight > 0.000001f)
							{
								hasNonZeroWeight = true;
								break;
							}
						}
						if (!hasNonZeroWeight)
							continue;

						// Find bone in skeleton
						uint32_t boneIndex = meshSource->m_Skeleton->GetBoneIndex(bone->mName.C_Str());
						if (boneIndex == Skeleton::NullIndex)
						{
							HZ_CORE_ERROR_TAG("Animation", "Could not find mesh bone '{}' in skeleton!", bone->mName.C_Str());
						}

						uint32_t boneInfoIndex = ~0;
						for (size_t j = 0; j < meshSource->m_BoneInfo.size(); ++j)
						{
							if (meshSource->m_BoneInfo[j].BoneIndex == boneIndex)
							{
								boneInfoIndex = static_cast<uint32_t>(j);
								break;
							}
						}
						if (boneInfoIndex == ~0)
						{
							boneInfoIndex = static_cast<uint32_t>(meshSource->m_BoneInfo.size());
							const auto& boneInfo = meshSource->m_BoneInfo.emplace_back(Utils::Mat4FromAIMatrix4x4(bone->mOffsetMatrix), boneIndex);
							HZ_CORE_INFO_TAG("Mesh", "BoneInfo for bone '{0}'", bone->mName.C_Str());
							HZ_CORE_INFO_TAG("Mesh", "  SubMeshIndex = {0}", m);
							HZ_CORE_INFO_TAG("Mesh", "  BoneIndex = {0}", boneIndex);
							glm::vec3 translation;
							glm::quat rotationQuat;
							glm::vec3 scale;
							Math::DecomposeTransform(boneInfo.InverseBindPose, translation, rotationQuat, scale);
							glm::vec3 rotation = glm::degrees(glm::eulerAngles(rotationQuat));
							HZ_CORE_INFO_TAG("Mesh", "  Inverse Bind Pose = {");
							HZ_CORE_INFO("    translation: ({0:8.4f}, {1:8.4f}, {2:8.4f})", translation.x, translation.y, translation.z);
							HZ_CORE_INFO("    rotation:    ({0:8.4f}, {1:8.4f}, {2:8.4f})", rotation.x, rotation.y, rotation.z);
							HZ_CORE_INFO("    scale:       ({0:8.4f}, {1:8.4f}, {2:8.4f})", scale.x, scale.y, scale.z);
							HZ_CORE_INFO("  }");
						}

						for (size_t j = 0; j < bone->mNumWeights; j++)
						{
							int VertexID = submesh.BaseVertex + bone->mWeights[j].mVertexId;
							float Weight = bone->mWeights[j].mWeight;
							meshSource->m_BoneInfluences[VertexID].AddBoneData(boneInfoIndex, Weight);
						}
					}
				}
			}

			for (auto& boneInfluence : meshSource->m_BoneInfluences)
			{
				boneInfluence.NormalizeWeights();
			}
		}

至此为MeshSource导入了骨骼信息,封装为

1
2
	std::vector<BoneInfluence> m_BoneInfluences; // 每个顶点对应的骨骼影响数据
	std::vector<BoneInfo> m_BoneInfo; // 骨骼信息

动画构建

GLTF的动画结构

  • 采样器:定义了有几个关键帧,以及每个关键帧对应的骨骼运动信息
  • 通道:指明一根骨骼由哪个采样器控制,以及控制方式(平移、旋转、缩放)

使用游戏引擎制作动画时的关键帧是统一设置了所有骨骼的变换信息,GLTF对每个骨骼单独控制,节省了很多空间,因为可能两个关键帧之间某些骨骼并没有变换,但是统一存储还得存一遍。下面是一个开火动画的例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
 "animations" : [    
        {
            "name" : "Fire", // 开火动画的例子
            "samplers" : [
                {
                    "input" : 7, // 存储的是所有关键帧的时间点(7只是访问accessors 数组的索引)
                    "interpolation" : "LINEAR", // 采用线性插值
                    "output" : 8 // 与input里一一对应的关键帧的数据
                },
                {
                    "input" : 7,
                    "interpolation" : "LINEAR",
                    "output" : 9
                }
            ]
            "channels" : [
                {
                    "sampler" : 0, // 指定一个采样器
                    "target" : {
                        "node" : 5, // 指定这个采样器作用哪个骨骼
                        "path" : "translation" // 作用的方式
                    }
                },
                {
                    "sampler" : 1,
                    "target" : {
                        "node" : 5,
                        "path" : "rotation"
                    }
                }
            ]
        }
    ]

动画数据本质就是存储每个关键帧的骨骼变换数据,GLTF这样存储是一种优化手段

GLTF动画数据导入

定义关键帧

1
2
3
4
5
6
template<typename T> struct KeyFrame
{
	float FrameTime;
	T Value;
	KeyFrame(const float frameTime, const T& value) : FrameTime(frameTime), Value(value) {}
};

定义Channel结构,对于一组关键帧,存储一根骨骼每个关键帧的变换数据,以及骨骼的Index(捕捉了 “单根骨骼的完整动画轨迹”)

1
2
3
4
5
6
7
struct Channel
{
	std::vector<KeyFrame<glm::vec3>> Translations;
	std::vector<KeyFrame<glm::quat>> Rotations;
	std::vector<KeyFrame<glm::vec3>> Scales;
	uint32_t Index;
};

下面给出如何封装所有骨骼的Channel

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
static std::vector<Channel> ImportChannels(aiAnimation* anim, const Skeleton& skeleton, const bool isMaskedRootMotion, const glm::vec3& rootTranslationMask, float rootRotationMask)
{
	std::vector<Channel> channels;

	std::unordered_map<std::string_view, uint32_t> boneIndices;
	std::unordered_set<uint32_t> rootBoneIndices;
	for (uint32_t i = 0; i < skeleton.GetNumBones(); ++i)
	{
		boneIndices.emplace(skeleton.GetBoneName(i), i + 1);  // 0 is reserved for root motion channel    boneIndices are base=1
		if (skeleton.GetParentBoneIndex(i) == Skeleton::NullIndex)
			rootBoneIndices.emplace(i + 1);
	}

	std::map<uint32_t, aiNodeAnim*> validChannels;
	for (uint32_t channelIndex = 0; channelIndex < anim->mNumChannels; ++channelIndex)
	{
		aiNodeAnim* nodeAnim = anim->mChannels[channelIndex];
		auto it = boneIndices.find(nodeAnim->mNodeName.C_Str());
		if (it != boneIndices.end())
		{
			validChannels.emplace(it->second, nodeAnim);   // validChannels.first is base=1,  .second is node pointer
		}
	}

	channels.resize(skeleton.GetNumBones() + 1);   // channels is base=1

	// channels don't necessarily have first frame at time zero.
	// We can just generate a dummy key frame, but that can end up looking a bit odd (in particular,
	// looping animations appear to pause for a split second each time they loop and encounter the
	// dummy key frame.
	// So instead, we clip the animation time horizon.
	double firstFrameDelta = DBL_MAX;
	double animationDuration = anim->mDuration;
	for (uint32_t boneIndex = 1; boneIndex < channels.size(); ++boneIndex)
	{
		if (auto validChannel = validChannels.find(boneIndex); validChannel != validChannels.end())
		{
			auto nodeAnim = validChannel->second;
			if (nodeAnim->mNumPositionKeys > 0)
				firstFrameDelta = std::min(firstFrameDelta, nodeAnim->mPositionKeys[0].mTime);

			if (nodeAnim->mNumRotationKeys > 0)
				firstFrameDelta = std::min(firstFrameDelta, nodeAnim->mRotationKeys[0].mTime);

			if (nodeAnim->mNumScalingKeys > 0)
				firstFrameDelta = std::min(firstFrameDelta, nodeAnim->mScalingKeys[0].mTime);
		}
	}

	anim->mDuration -= firstFrameDelta;

	// The rest of the code assumes non-zero animation duration.
	// Enforce that here.
	if (anim->mDuration <= 0.0) {
		anim->mDuration = 1.0;
	}

	for (uint32_t boneIndex = 1; boneIndex < channels.size(); ++boneIndex)
	{
		Channel& channel = channels[boneIndex];
		channel.Index = boneIndex;
		if (auto validChannel = validChannels.find(boneIndex); validChannel != validChannels.end())
		{
			auto nodeAnim = validChannel->second;
			channel.Translations.reserve(nodeAnim->mNumPositionKeys + 2); // +2 because worst case we insert two more keys
			channel.Rotations.reserve(nodeAnim->mNumRotationKeys + 2);
			channel.Scales.reserve(nodeAnim->mNumScalingKeys + 2);
			// Note: There is no need to check for duplicate keys (i.e. multiple keys all at same frame time)
			//       because Assimp throws these out for us
			for (uint32_t keyIndex = 0; keyIndex < nodeAnim->mNumPositionKeys; ++keyIndex)
			{
				aiVectorKey key = nodeAnim->mPositionKeys[keyIndex];
				float frameTime = std::clamp(static_cast<float>((key.mTime - firstFrameDelta) / anim->mDuration), 0.0f, 1.0f);
				if ((keyIndex == 0) && (frameTime > 0.0f))
				{
					channels[boneIndex].Translations.emplace_back(0.0f, glm::vec3{ static_cast<float>(key.mValue.x), static_cast<float>(key.mValue.y), static_cast<float>(key.mValue.z) });
				}
				channel.Translations.emplace_back(frameTime, glm::vec3{ static_cast<float>(key.mValue.x), static_cast<float>(key.mValue.y), static_cast<float>(key.mValue.z) });
			}
			if (channel.Translations.empty())
			{
				HZ_CORE_WARN_TAG("Animation", "No translation track found for bone '{}'", skeleton.GetBoneName(boneIndex - 1));
				channel.Translations = { {0.0f, glm::vec3{0.0f}}, {1.0f, glm::vec3{0.0f}} };
			}
			else if (channel.Translations.back().FrameTime < 1.0f)
			{
				channel.Translations.emplace_back(1.0f, channel.Translations.back().Value);
			}
			for (uint32_t keyIndex = 0; keyIndex < nodeAnim->mNumRotationKeys; ++keyIndex)
			{
				aiQuatKey key = nodeAnim->mRotationKeys[keyIndex];
				float frameTime = std::clamp(static_cast<float>((key.mTime - firstFrameDelta) / anim->mDuration), 0.0f, 1.0f);
				// WARNING: constructor parameter order for a quat is still WXYZ even if you have defined GLM_FORCE_QUAT_DATA_XYZW
				if ((keyIndex == 0) && (frameTime > 0.0f))
				{
					channel.Rotations.emplace_back(0.0f, glm::quat{ static_cast<float>(key.mValue.w), static_cast<float>(key.mValue.x), static_cast<float>(key.mValue.y), static_cast<float>(key.mValue.z) });
				}
				channel.Rotations.emplace_back(frameTime, glm::quat{ static_cast<float>(key.mValue.w), static_cast<float>(key.mValue.x), static_cast<float>(key.mValue.y), static_cast<float>(key.mValue.z) });
				HZ_CORE_ASSERT(fabs(glm::length(channels[boneIndex].Rotations.back().Value) - 1.0f) < 0.00001f);   // check rotations are normalized (I think assimp ensures this, but not 100% sure)
			}
			if (channel.Rotations.empty())
			{
				HZ_CORE_WARN_TAG("Animation", "No rotation track found for bone '{}'", skeleton.GetBoneName(boneIndex - 1));
				channel.Rotations = { {0.0f, glm::quat{1.0f, 0.0f, 0.0f, 0.0f}}, {1.0f, glm::quat{1.0f, 0.0f, 0.0f, 0.0f}} };
			}
			else if (channel.Rotations.back().FrameTime < 1.0f)
			{
				channel.Rotations.emplace_back(1.0f, channel.Rotations.back().Value);
			}
			for (uint32_t keyIndex = 0; keyIndex < nodeAnim->mNumScalingKeys; ++keyIndex)
			{
				aiVectorKey key = nodeAnim->mScalingKeys[keyIndex];
				float frameTime = std::clamp(static_cast<float>((key.mTime - firstFrameDelta) / anim->mDuration), 0.0f, 1.0f);
				if (keyIndex == 0 && frameTime > 0.0f)
				{
					channel.Scales.emplace_back(0.0f, glm::vec3{ static_cast<float>(key.mValue.x), static_cast<float>(key.mValue.y), static_cast<float>(key.mValue.z) });
				}
				channel.Scales.emplace_back(frameTime, glm::vec3{ static_cast<float>(key.mValue.x), static_cast<float>(key.mValue.y), static_cast<float>(key.mValue.z) });
			}
			if (channel.Scales.empty())
			{
				HZ_CORE_WARN_TAG("Animation", "No scale track found for bone '{}'", skeleton.GetBoneName(boneIndex - 1));
				channel.Scales = { {0.0f, glm::vec3{1.0f}}, {1.0f, glm::vec3{1.0f}} };
			}
			else if (channel.Scales.back().FrameTime < 1.0f)
			{
				channel.Scales.emplace_back(1.0f, channels[boneIndex].Scales.back().Value);
			}
		}
		else
		{
			HZ_CORE_WARN_TAG("Animation", "No animation tracks found for bone '{}'", skeleton.GetBoneName(boneIndex - 1));

			auto translation = skeleton.GetBoneTranslations().at(boneIndex - 1);
			auto rotation = skeleton.GetBoneRotations().at(boneIndex - 1);
			auto scale = skeleton.GetBoneScales().at(boneIndex - 1);

			channel.Translations = { {0.0f, translation}, {1.0f, translation} };
			channel.Rotations = { {0.0f, rotation}, {1.0f, rotation} };
			channel.Scales = { {0.0f, scale}, {1.0f, scale} };
		}
	}

	// Create root motion channel.
	// If isMaskedRootMotion is true, then root motion channel is created by filtering components of the first channel.
	// Otherwise root motion channel is copied as-is from the first channel.
	//
	// Root motion is then removed from all "root" channels (so it doesn't get applied twice)

	HZ_CORE_ASSERT(!rootBoneIndices.empty()); // Can't see how this would ever be false!
	HZ_CORE_ASSERT(rootBoneIndices.find(1) != rootBoneIndices.end()); // First bone must be a root!

	Channel& root = channels[0];
	root.Index = 0;
	if (isMaskedRootMotion)
	{
		for (auto& translation : channels[1].Translations)
		{
			root.Translations.emplace_back(translation.FrameTime, translation.Value * rootTranslationMask);
			translation.Value *= (glm::vec3(1.0f) - rootTranslationMask);
			translation.Value += root.Translations.front().Value;
		}
		for (auto& rotation : channels[1].Rotations)
		{
			if (rootRotationMask > 0.0f)
			{
				auto angleY = Utils::AngleAroundYAxis(rotation.Value);
				root.Rotations.emplace_back(rotation.FrameTime, glm::quat{ glm::cos(angleY * 0.5f), glm::vec3{0.0f, 1.0f, 0.0f} *glm::sin(angleY * 0.5f) });
				rotation.Value = glm::conjugate(glm::quat(glm::cos(angleY * 0.5f), glm::vec3{ 0.0f, 1.0f, 0.0f } *glm::sin(angleY * 0.5f))) * rotation.Value;
				rotation.Value *= root.Rotations.front().Value;
			}
			else
			{
				root.Rotations.emplace_back(rotation.FrameTime, glm::quat{ 1.0f, 0.0f, 0.0f, 0.0f });
			}
		}
	}
	else
	{
		root.Translations = channels[1].Translations;
		root.Rotations = channels[1].Rotations;
		channels[1].Translations = { {0.0f, root.Translations.front().Value}, {1.0f, root.Translations.front().Value} };
		channels[1].Rotations = { {0.0f, root.Rotations.front().Value}, {1.0f, root.Rotations.front().Value} };
	}
	root.Scales = { {0.0f, glm::vec3{1.0f}}, {1.0f, glm::vec3{1.0f}} };

	// It is possible that there is more than one "root" bone in the asset.
	// We need to remove the root motion from all of them (otherwise those bones will move twice as fast when root motion is applied)
	for (const auto rootBoneIndex : rootBoneIndices)
	{
		// we already removed root motion from the first bone, above
		if (rootBoneIndex != 1)
		{
			for (auto& translation : channels[rootBoneIndex].Translations)
			{
				// sample root channel at this translation's frametime
				for (size_t rootFrame = 0; rootFrame < root.Translations.size() - 1; ++rootFrame)
				{
					if (root.Translations[rootFrame + 1].FrameTime >= translation.FrameTime)
					{
						const float alpha = (translation.FrameTime - root.Translations[rootFrame].FrameTime) / (root.Translations[rootFrame + 1].FrameTime - root.Translations[rootFrame].FrameTime);
						translation.Value -= glm::mix(root.Translations[rootFrame].Value, root.Translations[rootFrame + 1].Value, alpha);
						translation.Value += root.Translations.front().Value;
						break;
					}
				}
			}

			for (auto& rotation : channels[rootBoneIndex].Rotations)
			{
				// sample root channel at the this rotation's frametime
				for (size_t rootFrame = 0; rootFrame < root.Rotations.size() - 1; ++rootFrame)
				{
					if (root.Rotations[rootFrame + 1].FrameTime >= rotation.FrameTime)
					{
						const float alpha = (rotation.FrameTime - root.Rotations[rootFrame].FrameTime) / (root.Rotations[rootFrame + 1].FrameTime - root.Rotations[rootFrame].FrameTime);
						rotation.Value = glm::normalize(glm::conjugate(glm::slerp(root.Rotations[rootFrame].Value, root.Rotations[rootFrame + 1].Value, alpha)) * rotation.Value);
						rotation.Value *= root.Rotations.front().Value;
						break;
					}
				}
			}
		}
	}
	return channels;
}

压缩算法(如 ACL)要求所有骨骼的关键帧数量和时间戳严格对齐,否则无法批量处理。但是GLTF导入的骨骼关键帧是不同的采样器得到的,不一定一致,所以需要统一化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
	void SanitizeChannels(std::vector<Channel>& channels)
	{
		uint32_t maxNumFrames = 2; // The rest of the code requires each channel to have at least 2 frames
		for (const auto& channel : channels)
		{
			maxNumFrames = std::max(maxNumFrames, static_cast<uint32_t>(channel.Translations.size()));
			maxNumFrames = std::max(maxNumFrames, static_cast<uint32_t>(channel.Rotations.size()));
			maxNumFrames = std::max(maxNumFrames, static_cast<uint32_t>(channel.Scales.size()));
		}

		float frameInterval = 1.0f / (maxNumFrames - 1);

		// loop over all channels and change them so they all have maxNumFrames frames.
		// add new frames where necessary by interpolating between existing frames.
		for (auto& channel : channels)
		{
			Channel newChannel;

			uint32_t translationIndex = 1;
			newChannel.Translations.reserve(maxNumFrames);
			newChannel.Translations.emplace_back(channel.Translations[0]);
			for (uint32_t i = 1; i < maxNumFrames - 1; ++i)
			{
				float frameTime = i * frameInterval;
				while ((translationIndex < channel.Translations.size()) && (channel.Translations[translationIndex].FrameTime < frameTime))
				{
					++translationIndex;
				}
				const float t = (frameTime - channel.Translations[translationIndex - 1].FrameTime) / (channel.Translations[translationIndex].FrameTime - channel.Translations[translationIndex - 1].FrameTime);
				newChannel.Translations.emplace_back(frameTime, glm::mix(channel.Translations[translationIndex - 1].Value, channel.Translations[translationIndex].Value, t));
			}
			newChannel.Translations.emplace_back(channel.Translations.back());

			uint32_t rotationIndex = 1;
			newChannel.Rotations.reserve(maxNumFrames);
			newChannel.Rotations.emplace_back(channel.Rotations[0]);
			for (uint32_t i = 1; i < maxNumFrames - 1; ++i)
			{
				float frameTime = i * frameInterval;
				while ((rotationIndex < channel.Rotations.size()) && (channel.Rotations[rotationIndex].FrameTime < frameTime))
				{
					++rotationIndex;
				}
				const float t = (frameTime - channel.Rotations[rotationIndex - 1].FrameTime) / (channel.Rotations[rotationIndex].FrameTime - channel.Rotations[rotationIndex - 1].FrameTime);
				newChannel.Rotations.emplace_back(frameTime, glm::slerp(channel.Rotations[rotationIndex - 1].Value, channel.Rotations[rotationIndex].Value, t));
			}
			newChannel.Rotations.emplace_back(channel.Rotations.back());

			uint32_t scaleIndex = 1;
			newChannel.Scales.reserve(maxNumFrames);
			newChannel.Scales.emplace_back(channel.Scales[0]);
			for (uint32_t i = 1; i < maxNumFrames - 1; ++i)
			{
				float frameTime = i * frameInterval;
				while ((scaleIndex < channel.Scales.size()) && (channel.Scales[scaleIndex].FrameTime < frameTime))
				{
					++scaleIndex;
				}
				const float t = (frameTime - channel.Scales[scaleIndex - 1].FrameTime) / (channel.Scales[scaleIndex].FrameTime - channel.Scales[scaleIndex - 1].FrameTime);
				newChannel.Scales.emplace_back(frameTime, glm::mix(channel.Scales[scaleIndex - 1].Value, channel.Scales[scaleIndex].Value, t));
			}
			newChannel.Scales.emplace_back(channel.Scales.back());

			channel.Translations = std::move(newChannel.Translations);
			channel.Rotations = std::move(newChannel.Rotations);
			channel.Scales = std::move(newChannel.Scales);
		}
	}

进一步使用acl压缩动画数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
	acl::error_result CompressChannels(const std::vector<Channel>& channels, const float fps, const Skeleton& skeleton, acl::compressed_tracks*& outCompressedTracks)
	{
		acl::iallocator& allocator = Utils::GetAnimationAllocator();
		uint32_t numTracks = static_cast<uint32_t>(channels.size());
		uint32_t numSamples = static_cast<uint32_t>(channels[0].Translations.size());
		acl::track_array_qvvf rawTrackList(allocator, numTracks);

		for (uint32_t i = 0; i < numTracks; ++i)
		{
			acl::track_desc_transformf desc;
			desc.output_index = i;
			desc.parent_index = (i == 0) ? acl::k_invalid_track_index : skeleton.GetParentBoneIndex(i - 1) + 1;  // 0 is root motion channel, 1..numBones are in the skeleton
			desc.precision = 0.0001f;
			desc.shell_distance = 3.0f;

			acl::track_qvvf rawTrack = acl::track_qvvf::make_reserve(desc, allocator, numSamples, fps);
			for (uint32_t j = 0; j < numSamples; ++j)
			{
				const auto& translation = channels[i].Translations[j].Value;
				const auto& rotation = channels[i].Rotations[j].Value;
				const auto& scale = channels[i].Scales[j].Value;

				rawTrack[j].rotation = rtm::quat_set(rotation.x, rotation.y, rotation.z, rotation.w);
				rawTrack[j].translation = rtm::vector_set(translation.x, translation.y, translation.z);
				rawTrack[j].scale = rtm::vector_set(scale.x, scale.y, scale.z);
			}
			rawTrackList[i] = std::move(rawTrack);
		}

		acl::pre_process_settings_t preProcessSettings;
		preProcessSettings.actions = acl::pre_process_actions::recommended;
		preProcessSettings.precision_policy = acl::pre_process_precision_policy::lossy;
		acl::qvvf_transform_error_metric error_metric;
		preProcessSettings.error_metric = &error_metric;
		acl::error_result result = acl::pre_process_track_list(allocator, preProcessSettings, rawTrackList);
		if (result.any())
		{
			return result;
		}

		acl::compression_settings compressSettings = acl::get_default_compression_settings();
		compressSettings.error_metric = &error_metric;
		acl::output_stats stats{ acl::stat_logging::none };
		result = acl::compress_track_list(allocator, rawTrackList, compressSettings, outCompressedTracks, stats);

		return result;
	}

ACL动画压缩

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
acl::error_result CompressChannels(const std::vector<Channel>& channels, const float fps, const Skeleton& skeleton, acl::compressed_tracks*& outCompressedTracks)
{
	acl::iallocator& allocator = Utils::GetAnimationAllocator();
	uint32_t numTracks = static_cast<uint32_t>(channels.size());
	uint32_t numSamples = static_cast<uint32_t>(channels[0].Translations.size());
	acl::track_array_qvvf rawTrackList(allocator, numTracks);

	for (uint32_t i = 0; i < numTracks; ++i)
	{
		acl::track_desc_transformf desc;
		desc.output_index = i;
		desc.parent_index = (i == 0) ? acl::k_invalid_track_index : skeleton.GetParentBoneIndex(i - 1) + 1;  // 0 is root motion channel, 1..numBones are in the skeleton
		desc.precision = 0.0001f;
		desc.shell_distance = 3.0f;

		acl::track_qvvf rawTrack = acl::track_qvvf::make_reserve(desc, allocator, numSamples, fps);
		for (uint32_t j = 0; j < numSamples; ++j)
		{
			const auto& translation = channels[i].Translations[j].Value;
			const auto& rotation = channels[i].Rotations[j].Value;
			const auto& scale = channels[i].Scales[j].Value;

			rawTrack[j].rotation = rtm::quat_set(rotation.x, rotation.y, rotation.z, rotation.w);
			rawTrack[j].translation = rtm::vector_set(translation.x, translation.y, translation.z);
			rawTrack[j].scale = rtm::vector_set(scale.x, scale.y, scale.z);
		}
		rawTrackList[i] = std::move(rawTrack);
	}

	acl::pre_process_settings_t preProcessSettings;
	preProcessSettings.actions = acl::pre_process_actions::recommended;
	preProcessSettings.precision_policy = acl::pre_process_precision_policy::lossy;
	acl::qvvf_transform_error_metric error_metric;
	preProcessSettings.error_metric = &error_metric;
	acl::error_result result = acl::pre_process_track_list(allocator, preProcessSettings, rawTrackList);
	if (result.any())
	{
		return result;
	}

	acl::compression_settings compressSettings = acl::get_default_compression_settings();
	compressSettings.error_metric = &error_metric;
	acl::output_stats stats{ acl::stat_logging::none };
	result = acl::compress_track_list(allocator, rawTrackList, compressSettings, outCompressedTracks, stats);

	return result;
}

到这里模型的骨骼信息和动画信息都封装好了

渲染动画

动画相关组件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
struct DynamicMeshComponent
{
	AssetHandle MeshSource = 0;
};
struct SubmeshComponent
{
	AssetHandle Mesh;
	std::vector<UUID> BoneEntityIds; 
	uint32_t SubmeshIndex = 0;
	bool Visible = true;

	SubmeshComponent() = default;
	SubmeshComponent(const SubmeshComponent& other)
		: Mesh(other.Mesh), BoneEntityIds(other.BoneEntityIds), SubmeshIndex(other.SubmeshIndex), Visible(other.Visible)
	{
	}
	SubmeshComponent(AssetHandle mesh, uint32_t submeshIndex = 0)
		: Mesh(mesh), SubmeshIndex(submeshIndex)
	{
	}
};
struct AnimationComponent
{
    AssetHandle Mesh;
    const Animation* CurrentAnimation; 
    float CurrentTime = 0.0f;
    bool IsLooping = true; 
    Pose CurrentPose;      

    std::vector<UUID> BoneEntityIds; 
    AnimationComponent() = default;
    AnimationComponent(AssetHandle mesh):Mesh(mesh)
    {
    }
};

收集动画Mesh

定义骨骼UBO,同实例化渲染的设计,通过MeshKey来区分不一样的DrawCall,所有Submesh只要材质一样就会被实例化调用,另外用StorageBufferSet平铺骨骼变换矩阵

1
2
3
4
5
6
7
8
		using BoneTransforms = std::array<glm::mat4, 100>; // Note: 100 == MAX_BONES from the shaders
		struct BoneTransformsMapData
		{
			std::vector<BoneTransforms> BoneTransformsData;
			uint32_t BoneTransformsBaseIndex = 0;
		};
		std::map<MeshKey, BoneTransformsMapData> m_MeshBoneTransformsMap;
		Ref<StorageBufferSet> m_SBSBoneTransforms;

Shader中,通过a_BoneIndices传递该顶点绑定的骨骼,a_BoneWeights传递权重。这些信息在Mesh导入时已经被封装。通过顶点缓冲区导入

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#version 450 core
#ifdef VERTEX_SHADER
layout(binding = 0) uniform UniformBufferObject {
    mat4 view;
    mat4 proj;
	float width;
	float height;
	float Near;
	float Far;
} ubo;
const int MAX_BONES = 100;
const int MAX_ANIMATED_MESHES = 1024;
layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inNormal;
layout(location = 2) in vec3 inTangent;
layout(location = 3) in vec3 inBinormal; 
layout(location = 4) in vec2 inTexCoord;
layout(location = 5) in vec4 a_MRow0;
layout(location = 6) in vec4 a_MRow1;
layout(location = 7) in vec4 a_MRow2;
// Bone influences
layout(location = 8) in ivec4 a_BoneIndices;
layout(location = 9) in vec4 a_BoneWeights;
layout (std140, set = 0, binding = 1) readonly buffer BoneTransforms
{
	mat4 BoneTransforms[MAX_BONES * MAX_ANIMATED_MESHES];
} r_BoneTransforms;

layout(push_constant) uniform BoneTransformIndex
{
	uint Base;
} u_BoneTransformIndex;


layout(location = 0) out vec3 fragWorldPos;
layout(location = 1) out vec3 fragWorldNormal;
layout(location = 2) out vec3 fragWorldTangent;
layout(location = 3) out vec3 fragWorldBinormal;
layout(location = 4) out vec2 fragTexCoord;

void main() {
    mat4 transform = mat4(
		vec4(a_MRow0.x, a_MRow1.x, a_MRow2.x, 0.0),
		vec4(a_MRow0.y, a_MRow1.y, a_MRow2.y, 0.0),
		vec4(a_MRow0.z, a_MRow1.z, a_MRow2.z, 0.0),
		vec4(a_MRow0.w, a_MRow1.w, a_MRow2.w, 1.0)
	);
	mat4 boneTransform = r_BoneTransforms.BoneTransforms[(u_BoneTransformIndex.Base + gl_InstanceIndex) * MAX_BONES + a_BoneIndices[0]] * a_BoneWeights[0];
	boneTransform     += r_BoneTransforms.BoneTransforms[(u_BoneTransformIndex.Base + gl_InstanceIndex) * MAX_BONES + a_BoneIndices[1]] * a_BoneWeights[1];
	boneTransform     += r_BoneTransforms.BoneTransforms[(u_BoneTransformIndex.Base + gl_InstanceIndex) * MAX_BONES + a_BoneIndices[2]] * a_BoneWeights[2];
	boneTransform     += r_BoneTransforms.BoneTransforms[(u_BoneTransformIndex.Base + gl_InstanceIndex) * MAX_BONES + a_BoneIndices[3]] * a_BoneWeights[3];

    vec4 worldPos = transform * boneTransform * vec4(inPosition, 1.0);
    fragWorldPos = worldPos.xyz;
		
    mat3 modelRot = mat3(transform);
    mat3 boneRot = mat3(boneTransform);
    fragWorldNormal = normalize(modelRot * boneRot * inNormal);
    fragWorldTangent = normalize(modelRot * boneRot * inTangent);
    fragWorldBinormal = normalize(modelRot * boneRot * inBinormal);
    fragTexCoord = inTexCoord;
    gl_Position = ubo.proj * ubo.view * worldPos;
}
#endif
📚 文章数: 72 ✍️ 总字数: 245.55K