2-Assimp读取动画
在看完GAMES104动画系统那一部分后,我也想写一个简单的动画系统,又要开一个坑了!本文将简要介绍如何用Assimp库去读取动画信息。
用Assimp读取动画信息
在之前我们使用Assimp库读取了模型的几何和纹理信息,实际上还能读取模型的动画信息。
获取动画剪辑Clip信息
下图展示了Assimp库是如何存储动画信息的:
在aiScene
中,存储了一个aiAnimation
数组Animations[]
。单个aiAnimation
可看作一个动画剪辑(Animation Clip),它包含动画的名称,持续时间,帧率,关键帧等信息。其中关键帧信息存放在aiNodeAnim
数组Channels[]
中,单个aiNodeAnim
包含当前关键帧的位移、旋转、缩放等数据。
获取多关节影响的权重
此外,由于是多关节绑定的蒙皮动画,单个顶点可能受到多个关节的影响,这里我们假定单个顶点最多受4个关节影响。来看看Assimp是如何存储关节对顶点影响的权重。
在aiMesh
中有一个aiBone
数组。aiBone
的aiVertexWeight[]
则包含了当前关节对一些顶点的影响权重。还有一个叫做offsetMatrix
的成员,它是一个4x4的矩阵,用于将处于初始绑定姿势的顶点从模型空间变换到关节的局部空间,即蒙皮矩阵。
代码实现
接下来就可以进行代码实现了。
拓展模型读取部分
首先需要拓展之前写的模型读取部分。对于顶点信息,之前只有位置,法向量和纹理坐标,现在额外添加了影响到该顶点的关节数组和对应权重:
#ifndef MAX_BONE_INFLUENCE
#define MAX_BONE_INFLUENCE 4
#endif
struct MeshVertex
{
glm::vec3 Position = glm::vec3(0.0f);
glm::vec3 Normal = glm::vec3(0.0f);
glm::vec2 TexCoords = glm::vec2(0.0f);
// 影响该顶点的关节们
std::array<int, MAX_BONE_INFLUENCE> m_BoneIDs;
// 关节对应的权重
std::array<float, MAX_BONE_INFLUENCE> m_BoneWeights;
};
拓展顶点信息后,别忘了将它们传输给GPU(这里直接搬教程了,和我代码不一样,详见我的项目github):
// ids
glEnableVertexAttribArray(3);
glVertexAttribIPointer(3, 4, GL_INT, sizeof(Vertex), (void*)offsetof(Vertex, m_BoneIDs));
// weights
glEnableVertexAttribArray(4);
glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex),
(void*)offsetof(Vertex, m_Weights));
需要注意的是m_BoneIDs
是int类型,需要用到glVertexAttribIPointer
。
接下来添加结构体BoneInfo
,用于存放aiBone
里的关节数据:
struct BoneInfo
{
int id;
glm::mat4 offset; // 将顶点从模型空间变换到关节局部空间的矩阵
};
然后拓展模型读取部分,实现数据的读取和存储:
// 模型读取类 Object.h
// 对象骨骼动画属性
std::map<std::string, BoneInfo> m_boneInfoMap;
int m_boneCounter = 0;
// getter & setters
std::map<std::string, BoneInfo>& getBoneInfoMap() { return m_boneInfoMap; }
int getBoneCount() const { return m_boneCounter; }
// 初始化骨骼数据
void InitVertexBoneData(MeshVertex& vertex)
{
for (int i = 0; i < MAX_BONE_INFLUENCE; ++i)
{
vertex.m_BoneIDs[i] = -1;
vertex.m_BoneIDs[i] = 0.0f;
}
}
// 为顶点设置骨骼影响权重
void SetVertexBoneData(MeshVertex& vertex, int boneID, float weight)
{
for (int i = 0; i < MAX_BONE_INFLUENCE; ++i)
{
if (vertex.m_BoneIDs[i] < 0)
{
vertex.m_BoneIDs[i] = boneID;
vertex.m_BoneWeights[i] = weight;
break;
}
}
}
// 读取并设置顶点的骨骼数据
void GLObject::ExtractBoneWeightForVertices(std::vector<MeshVertex>& vertices, aiMesh* mesh, const aiScene* scene)
{
for (int boneIdx = 0; boneIdx < mesh->mNumBones; ++boneIdx)
{
int boneID = -1;
std::string boneName = mesh->mBones[boneIdx]->mName.C_Str();
if (!m_boneInfoMap.contains(boneName))
{
BoneInfo newBoneInfo;
newBoneInfo.id = m_boneCounter++;
newBoneInfo.offset = AssimpGLMHelpers::GetGLMMat4(mesh->mBones[boneIdx]->mOffsetMatrix);
m_boneInfoMap[boneName] = newBoneInfo;
boneID = newBoneInfo.id;
}
else
{
boneID = m_boneInfoMap[boneName].id;
}
if (boneID == -1)
{
LOG_ERROR(std::format("[{}]: Cannot get boneID!", __FUNCTION__));
__debugbreak();
}
auto weights = mesh->mBones[boneIdx]->mWeights;
int numWeights = mesh->mBones[boneIdx]->mNumWeights;
for (int weightsIdx = 0; weightsIdx < numWeights; ++weightsIdx)
{
int vertexID = weights[weightsIdx].mVertexId;
float weight = weights[weightsIdx].mWeight;
if (vertexID > vertices.size())
{
LOG_ERROR(std::format("[{}]: vertices array out of bound!", __FUNCTION__));
__debugbreak();
}
SetVertexBoneData(vertices[vertexID], boneID, weight);
}
}
}
Mesh processMesh(aiMesh* mesh, const aiScene* scene)
{
...
// 处理顶点信息
for (unsigned int i = 0; i < mesh->mNumVertices; ++i)
{
...
// 骨骼动画数据初始化
InitVertexBoneData(vertex);
vertices.push_back(vertex);
}
// 处理骨骼动画数据
if (mesh->HasBones())
{
ExtractBoneWeightForVertices(vertices, mesh, scene);
}
else
{
LOG_DEBUG(std::format("[{}]: This mesh has no bones for skinning animation.", __FUNCTION__));
}
...
return {vertices, indices, textures, &m_modelData.pCustom->texturesLoaded};
}
实现初版动画类
接下来尝试实现动画类的初版,这些动画类应该可以让我们播放一个动画剪辑(Animation Clip)。以下是要实现的类:
- Bone类:存储从
aiNodeAnim
中读出来的所有关键帧数据,也会给当前动画播放时间进行SRT变换的关键帧插值。 - AssimpNodeData类:这个结构体将帮助我们从Assimp中独立出我们自己的动画类。
- Animation类:读取
aiAnimation
中的数据,创建关节层次结构。 - Animator类:读取
AssimpNodeData
的层次信息,用递归方式插值所有关节,然后准备我们需要的最终关节变换矩阵。
Bone类
// Bone.h
// ======================= 关键帧定义 =======================
struct KeyTranslate
{
glm::vec3 translate;
float timeStamp;
KeyTranslate(const glm::vec3& _translate, float _timeStamp)
: translate(_translate), timeStamp(_timeStamp) {}
};
struct KeyRotate
{
glm::quat orientation;
float timeStamp;
KeyRotate(const glm::quat& _orientation, float _timeStamp)
: orientation(_orientation), timeStamp(_timeStamp) {}
};
struct KeyScale
{
glm::vec3 scale;
float timeStamp;
KeyScale(const glm::vec3& _scale, float _timeStamp)
: scale(_scale), timeStamp(_timeStamp) {}
};
// ======================= 关键帧定义 =======================
class Bone
{
public:
// 从aiNodeAnim->channel中读取该骨骼/关节的关键帧数据
Bone(const std::string& name, int ID, const aiNodeAnim* channel);
// 根据动画的当前时间给SRT变换插值,并更新该骨骼/关节的局部空间M矩阵
void Update(float animationTime);
// ======================= Getters & Setters =======================
// 根据给定播放时间获取最近位置的关键平移帧索引
int GetTranslateIndex(float animationTime) const;
// 根据给定播放时间获取最近位置的关键旋转帧索引
int GetRotateIndex(float animationTime) const;
// 根据给定播放时间获取最近位置的关键缩放帧索引
int GetScaleIndex(float animationTime) const;
std::string GetBoneName() const { return m_name; }
glm::mat4 GetLocalTransform() const { return m_localTransform; }
// ======================= Getters & Setters =======================
private:
// 获取用于Lerp和SLerp的标准化值
float GetScaleFactor(float lastTimeStamp, float nextTimeStamp, float animationTime) const;
// 使用简单线性插值获取当前播放时间的平移矩阵
glm::mat4 InterpolateTranslate(float animationTime);
// 使用简单线性插值获取当前播放时间的旋转矩阵
glm::mat4 InterpolateRotate(float animationTime);
// 使用简单线性插值获取当前播放时间的缩放矩阵
glm::mat4 InterpolateScale(float animationTime);
private:
int m_ID;
std::string m_name;
int m_numTranslates;
int m_numRotates;
int m_numScales;
std::vector<KeyTranslate> m_keyTranslations;
std::vector<KeyRotate> m_keyRotations;
std::vector<KeyScale> m_keyScales;
glm::mat4 m_localTransform;
};
// Bone.cpp
Bone::Bone(const std::string& name, int ID, const aiNodeAnim* channel)
: m_ID(ID), m_name(name), m_localTransform(glm::mat4(1.0f))
{
m_numTranslates = channel->mNumPositionKeys;
for (int posIdx = 0; posIdx < m_numTranslates; ++posIdx)
{
aiVector3D aiPosition = channel->mPositionKeys[posIdx].mValue;
float timeStamp = channel->mPositionKeys[posIdx].mTime;
m_keyTranslations.push_back({AssimpGLMHelpers::GetGLMVec3(aiPosition), timeStamp});
}
m_numRotates = channel->mNumRotationKeys;
for (int rotIdx = 0; rotIdx < m_numRotates; ++rotIdx)
{
aiQuaternion aiRotation = channel->mRotationKeys[rotIdx].mValue;
float timeStamp = channel->mRotationKeys[rotIdx].mTime;
m_keyRotations.push_back({AssimpGLMHelpers::GetGLMQuat(aiRotation), timeStamp});
}
m_numScales = channel->mNumScalingKeys;
for (int scaleIdx = 0; scaleIdx < m_numScales; ++scaleIdx)
{
aiVector3D aiScaling = channel->mScalingKeys[scaleIdx].mValue;
float timeStamp = channel->mScalingKeys[scaleIdx].mTime;
m_keyScales.push_back({AssimpGLMHelpers::GetGLMVec3(aiScaling), timeStamp});
}
}
void Bone::Update(float animationTime)
{
glm::mat4 translate = InterpolateTranslate(animationTime);
glm::mat4 rotate = InterpolateRotate(animationTime);
glm::mat4 scale = InterpolateScale(animationTime);
m_localTransform = translate * rotate * scale;
}
int Bone::GetTranslateIndex(float animationTime) const
{
for (int idx = 0; idx < m_numTranslates - 1; ++idx)
{
if (animationTime < m_keyTranslations[idx + 1].timeStamp)
return idx;
}
LOG_ERROR(std::format("[{}]: Invalid animationTime: {}!", __FUNCTION__, animationTime));
__debugbreak();
return -1;
}
int Bone::GetRotateIndex(float animationTime) const
{
for (int idx = 0; idx < m_numRotates - 1; ++idx)
{
if (animationTime < m_keyRotations[idx + 1].timeStamp)
return idx;
}
LOG_ERROR(std::format("[{}]: Invalid animationTime: {}!", __FUNCTION__, animationTime));
__debugbreak();
return -1;
}
int Bone::GetScaleIndex(float animationTime) const
{
for (int idx = 0; idx < m_numScales - 1; ++idx)
{
if (animationTime < m_keyScales[idx + 1].timeStamp)
return idx;
}
LOG_ERROR(std::format("[{}]: Invalid animationTime: {}!", __FUNCTION__, animationTime));
__debugbreak();
return -1;
}
float Bone::GetScaleFactor(float lastTimeStamp, float nextTimeStamp, float animationTime) const
{
float scaleFactor = 0.0f;
float midWayLength = animationTime - lastTimeStamp;
float framesDiff = nextTimeStamp - lastTimeStamp;
scaleFactor = midWayLength / framesDiff;
return scaleFactor;
}
glm::mat4 Bone::InterpolateTranslate(float animationTime)
{
if (m_numTranslates == 1)
return glm::translate(glm::mat4(1.0f), m_keyTranslations[0].translate);
int p0Idx = GetTranslateIndex(animationTime);
int p1Idx = p0Idx + 1;
float scaleFactor = GetScaleFactor(m_keyTranslations[p0Idx].timeStamp,
m_keyTranslations[p1Idx].timeStamp,
animationTime);
glm::vec3 finalTranslate = glm::mix(m_keyTranslations[p0Idx].translate,
m_keyTranslations[p1Idx].translate,
scaleFactor);
return glm::translate(glm::mat4(1.0f), finalTranslate);
}
glm::mat4 Bone::InterpolateRotate(float animationTime)
{
if (m_numRotates == 1)
{
auto rotate = glm::normalize(m_keyRotations[0].orientation);
return glm::toMat4(rotate);
}
int p0Idx = GetRotateIndex(animationTime);
int p1Idx = p0Idx + 1;
float scaleFactor = GetScaleFactor(m_keyRotations[p0Idx].timeStamp,
m_keyRotations[p1Idx].timeStamp,
animationTime);
glm::quat finalRotate = glm::slerp(m_keyRotations[p0Idx].orientation,
m_keyRotations[p1Idx].orientation,
scaleFactor);
finalRotate = glm::normalize(finalRotate);
return glm::toMat4(finalRotate);
}
glm::mat4 Bone::InterpolateScale(float animationTime)
{
if (m_numRotates == 1)
return glm::scale(glm::mat4(1.0f), m_keyScales[0].scale);
int p0Idx = GetScaleIndex(animationTime);
int p1Idx = p0Idx + 1;
float scaleFactor = GetScaleFactor(m_keyScales[p0Idx].timeStamp,
m_keyScales[p1Idx].timeStamp,
animationTime);
glm::vec3 finalScale = glm::mix(m_keyScales[p0Idx].scale,
m_keyScales[p1Idx].scale,
scaleFactor);
return glm::scale(glm::mat4(1.0f), finalScale);
}
根据上述代码,首先定义了关键帧中的3种SRT变换,每种变换的结构体都包含了一个关键帧时间戳和变换信息。然后在Update()
中根据当前动画播放时间调整该骨骼/关节的局部模型变换矩阵。相关插值则使用Lerp或SLerp实现,其中插值用到的缩放因子GetScaleFactor()
则根据上一关键帧和下一关键帧的时间戳与当前时间进行求比计算:
Animation类
// Animation.h
// 用于和Assimp库解耦的节点类
struct AssimpNodeData
{
glm::mat4 transformation;
std::string name;
int childrenCount;
std::vector<AssimpNodeData> children;
};
class Animation
{
public:
Animation() = default;
Animation(const std::string& animationPath, GLObject* model);
Bone* FindBone(const std::string& name);
inline float GetTicksPerSecond() const { return m_ticksPerSecond; }
inline float GetDuration() const { return m_duration; }
inline const AssimpNodeData& GetRootNode() { return m_rootNode; }
inline const std::map<std::string, BoneInfo>& GetBoneIDMap() { return m_boneInfoMap; }
private:
void ReadMissingBones(const aiAnimation* animation, GLObject& model);
void ReadHeirarchyData(AssimpNodeData& dest, const aiNode* src);
private:
float m_duration;
float m_ticksPerSecond;
std::unordered_map<std::string, Bone> m_name2Bones;
AssimpNodeData m_rootNode;
std::map<std::string, BoneInfo> m_boneInfoMap;
};
// Animation.cpp
Animation::Animation(const std::string& animationPath, GLObject* model)
{
Assimp::Importer importer;
const aiScene* scene = importer.ReadFile(animationPath, aiProcess_Triangulate);
assert(scene && scene->mRootNode);
auto animation = scene->mAnimations[0];
m_duration = animation->mDuration;
m_ticksPerSecond = animation->mTicksPerSecond;
ReadHeirarchyData(m_rootNode, scene->mRootNode);
ReadMissingBones(animation, *model);
}
Bone* Animation::FindBone(const std::string& name)
{
if (m_name2Bones.contains(name))
return &m_name2Bones[name];
return nullptr;
}
void Animation::ReadMissingBones(const aiAnimation* animation, GLObject& model)
{
int size = animation->mNumChannels;
auto& boneInfoMap = model.getBoneInfoMap();
int& boneCount = model.getBoneCount();
for (int i = 0; i < size; ++i)
{
auto channel = animation->mChannels[i];
std::string boneName = channel->mNodeName.data;
if (!boneInfoMap.contains(boneName))
{
boneInfoMap[boneName].id = boneCount;
boneCount++;
}
m_name2Bones[boneName] = Bone(channel->mNodeName.data, boneInfoMap[channel->mNodeName.data].id, channel);
}
m_boneInfoMap = boneInfoMap;
}
void Animation::ReadHeirarchyData(AssimpNodeData& dest, const aiNode* src)
{
assert(src);
dest.name = src->mName.data;
dest.transformation = AssimpGLMHelpers::GetGLMMat4(src->mTransformation);
dest.childrenCount = src->mNumChildren;
for (int i = 0; i < src->mNumChildren; ++i)
{
AssimpNodeData newData;
ReadHeirarchyData(newData, src->mChildren[i]);
dest.children.push_back(newData);
}
}
对于Animation
,它需要动画文件地址和播放此动画的模型类初始化,一个Animation
包括了一个动画序列信息,例如动画的持续时间m_duration
和动画的速度m_ticksPerSecond
。
Ps:这个类可能还要改改,让它集成至模型类中。
Animator类
// Animator.h
class Animator
{
public:
Animator(Animation* animation);
void UpdateAnimation(float dt);
void PlayAnimation(Animation* pAnimation);
void CalculateBoneTransform(const AssimpNodeData* node, glm::mat4 parentTransform);
std::vector<glm::mat4> GetFinalBoneMatrices(){ return m_finalBoneMatrices; }
private:
std::vector<glm::mat4> m_finalBoneMatrices;
Animation* m_currentAnimation;
float m_currentTime;
float m_deltaTime;
};
// Animator.cpp
Animator::Animator(Animation* animation)
{
m_currentTime = 0.0f;
m_currentAnimation = animation;
m_finalBoneMatrices = std::vector<glm::mat4>(100, glm::mat4(1.0f));
}
void Animator::UpdateAnimation(float dt)
{
m_deltaTime = dt;
if (m_currentAnimation)
{
m_currentTime += m_currentAnimation->GetTicksPerSecond() * dt;
m_currentTime = fmod(m_currentTime, m_currentAnimation->GetDuration());
CalculateBoneTransform(&m_currentAnimation->GetRootNode(), glm::mat4(1.0f));
}
}
void Animator::PlayAnimation(Animation* pAnimation)
{
m_currentAnimation = pAnimation;
m_currentTime = 0.0f;
}
void Animator::CalculateBoneTransform(const AssimpNodeData* node, glm::mat4 parentTransform)
{
std::string nodeName = node->name;
glm::mat4 nodeTransform = node->transformation;
Bone* bone = m_currentAnimation->FindBone(nodeName);
if (bone)
{
bone->Update(m_currentTime);
nodeTransform = bone->GetLocalTransform();
}
glm::mat4 globalTransform = parentTransform * nodeTransform;
auto boneInfoMap = m_currentAnimation->GetBoneIDMap();
if (boneInfoMap.contains(nodeName))
{
int index = boneInfoMap[nodeName].id;
glm::mat4 offset = boneInfoMap[nodeName].offset;
m_finalBoneMatrices[index] = globalTransform * offset;
}
for (int i = 0; i < node->childrenCount; ++i)
CalculateBoneTransform(&node->children[i], globalTransform);
}
Animator
类则会管理动画剪辑序列Animation
的播放,更新等。
编写Shader
然后是编写shader,由于动画变换的是顶点,编写顶点着色器就行了:
#version 330 core
layout(location = 0) in vec3 position;
layout(location = 1) in vec3 normal;
layout(location = 2) in vec2 texCoords;
layout(location = 3) in ivec4 boneIds;
layout(location = 4) in vec4 weights;
uniform mat4 u_MVP;
uniform mat4 u_Model;
uniform mat3 u_Normal;
out vs_objOUT
{
vec3 Normal;
vec3 FragPos;
vec2 TexCoords;
} vs_objData;
const int MAX_BONES = 100;
const int MAX_BONE_INFLUENCE = 4;
uniform mat4 finalBonesMatrices[MAX_BONES];
void main()
{
vec4 totalPosition = vec4(0.0f);
vec3 totalNormal = vec3(0.0f);
for (int i = 0; i < MAX_BONE_INFLUENCE; ++i)
{
if (boneIds[i] == -1)
continue;
if (boneIds[i] >= MAX_BONES)
{
totalPosition = vec4(position, 1.0f);
totalNormal = normal;
break;
}
vec4 localPosition = finalBonesMatrices[boneIds[i]] * vec4(position, 1.0f);
totalPosition += localPosition * weights[i];
vec3 localNormal = mat3(finalBonesMatrices[boneIds[i]]) * normal;
totalNormal += localNormal * weights[i];
}
vs_objData.Normal = u_Normal * normalize(totalNormal);
vs_objData.FragPos = vec3(u_Model * totalPosition);
vs_objData.TexCoords = texCoords;
gl_Position = u_MVP * totalPosition;
};
对每个顶点按权重添加对应关节的影响,包括位置和法向量。
使用的时候,需要初始化Animator和Animation,然后在更新逻辑帧的时候使用animator.UpdateAnimation()
,在更新渲染帧的时候更新shader的最终关节矩阵,复刻教程的结果如下:
Ps:代码结构需要改改,例如动画序列帧Animation
和动画控制器Animator
可以放到角色模型类中。
参考资料
- LearnOpenGL - Skeletal Animation
- 《C++ Game Animation Programming》