上一部分我们主要讲了骨骼动画的原理,现在我们来对其进行实现:
一、实现原理
首先从骨骼动画的机制推导出驱动骨骼动画需要的数据:
- 模型骨骼树的结构数据,描述模型都有哪些骨骼,以及骨骼之间的连接关系。
- 每个顶点需要这些额外数据: 该顶点依附于哪些骨骼(即被哪些骨骼影响),以及每根骨骼的影响权重。
- 骨骼动画的关键帧数据,每个关键帧数据会保存每个骨骼在这个关键帧时间节点时相对父骨骼节点的变换矩阵, 每个关键帧还会保存自己对应的时间点。
- 动画通用数据: 名称, 持续时间等。
上述数据就足以驱动一个骨骼动画了,下面是在Vulkan环境下驱动骨骼动画的大致的数据解析流程:
- 首先对骨骼进行一次总体索引,为每根骨骼顺序分配一个数值ID,这个ID很重要,是后面运转过程中索引该骨骼数据的唯一ID,还要保存骨骼名称和ID的映射表,这是为了在遍历骨骼树能根据骨骼名称得到骨骼ID进而索引到骨骼数据。因为一般从模型读取出来的骨骼会有名称,但是不会有ID。
- 顶点的位移需要在顶点着色器中完成,顶点依附于哪些骨骼(骨骼ID)和这些骨骼的影响权重都要作为顶点属性传输到顶点着色器。需要注意的是,GPU显存是有限的,一个顶点被越多的骨骼影响,它所需要传输的骨骼属性就越多。由于着色器的特殊性,骨骼属性的大小取决于依附最多骨骼顶点的骨骼数量。因此顶点能最多能依附几根骨骼是需要考虑的。
- 对骨骼树进行遍历,结合当前的时间点得到骨骼在其动画运行轨迹中的前后关键帧,然后插值计算出在这个时间点骨骼的相对父类变化矩阵(一般可能会被拆分为平移/旋转/缩放三个子轨迹),和父类的模型变化矩阵相乘后即可得到骨骼在当前时间点的模型变换矩阵,同样流程处理该骨骼的子节点骨骼,直到算出所有骨骼在该时间节点的模型变换矩阵。
- 每个骨骼的世界坐标变换矩阵也需要传输/更新给顶点,因为顶点着色器的特殊性,我们不可能给顶点只发送影响它的骨骼的矩阵,只能将所有的骨骼的矩阵作为uniform矩阵数组进行传输,然后顶点根据自己的骨骼ID从全局uniform数组中取出影响自己的骨骼的矩阵信息。需要注意的是,OpenGL着色器中uniform数组不是无限大的,这意味着骨骼的总数是受限的,在制作模型时需要考虑这一点。
- 真正的骨骼位移运算发生在顶点着色器中,有了前面几步提供的数据,只需要根据该顶点的骨骼权重结合骨骼变化矩阵就能得到符合当前动画轨迹的模型变换矩阵,作用到该顶点上即可。
- 步骤3~5不断重复,就可以观察到骨骼动画效果。
二、实现步骤
我们主要使用assimp对骨骼模型进行解析,并自定义到我们的数据结构中
2.1 骨骼蒙皮类定义
// 每个网格的最大骨骼数量
// 在皮肤着色器中不能高于相同的常量
#define MAX_BONES 64
// 每个顶点关联的最大骨数
#define MAX_BONES_PER_VERTEX 4
// 每个顶点骨骼id和权重
struct VertexBoneData
{
std::array<uint32_t, MAX_BONES_PER_VERTEX> IDs;
std::array<float, MAX_BONES_PER_VERTEX> weights;
// Ad bone weighting to vertex info
void add(uint32_t boneID, float weight)
{
for (uint32_t i = 0; i < MAX_BONES_PER_VERTEX; i++)
{
if (weights[i] == 0.0f)
{
IDs[i] = boneID;
weights[i] = weight;
return;
}
}
}
};
// 骨骼中储存的矩阵信息
struct BoneInfo
{
aiMatrix4x4 offset;
aiMatrix4x4 finalTransformation;
BoneInfo()
{
offset = aiMatrix4x4();
finalTransformation = aiMatrix4x4();
};
};
class SkinnedMesh
{
public:
// 骨骼相关数据
// 映射骨骼名称与索引
std::map<std::string, uint32_t> boneMapping;
// 所有骨骼数据
std::vector<BoneInfo> boneInfo;
// 当前使用的骨骼数
uint32_t numBones = 0;
// 根节点逆变换矩阵
aiMatrix4x4 globalInverseTransform;
// 每个顶点具备的骨骼信息
std::vector<VertexBoneData> bones;
// Bone transformations
std::vector<aiMatrix4x4> boneTransforms;
// 动画速度
float animationSpeed = 0.75f;
// 当前活动的动画
aiAnimation* pAnimation;
// vulkan顶点缓冲数据
vks::Model vertexBuffer;
// 存储对ASSIMP场景的引用,以便在动画期间访问它的属性
Assimp::Importer Importer;
const aiScene* scene;
// 按索引设置活动动画
void setAnimation(uint32_t animationIndex)
{
assert(animationIndex < scene->mNumAnimations);
pAnimation = scene->mAnimations[animationIndex];
}
// 从ASSIMP网格加载骨骼信息
void loadBones(const aiMesh* pMesh, uint32_t vertexOffset, std::vector<VertexBoneData>& Bones)
{
for (uint32_t i = 0; i < pMesh->mNumBones; i++)
{
uint32_t index = 0;
assert(pMesh->mNumBones <= MAX_BONES);
std::string name(pMesh->mBones[i]->mName.data);
if (boneMapping.find(name) == boneMapping.end())
{
// 骨骼节点不存在,增加新骨
index = numBones;
numBones++;
BoneInfo bone;
boneInfo.push_back(bone);
boneInfo[index].offset = pMesh->mBones[i]->mOffsetMatrix;
boneMapping[name] = index;
}
else
{
index = boneMapping[name];
}
for (uint32_t j = 0; j < pMesh->mBones[i]->mNumWeights; j++)
{
uint32_t vertexID = vertexOffset + pMesh->mBones[i]->mWeights[j].mVertexId;
Bones[vertexID].add(index, pMesh->mBones[i]->mWeights[j].mWeight);
}
}
boneTransforms.resize(numBones);
}
// 给定动画时间递归骨骼转换
void update(float time)
{
float TicksPerSecond = (float)(scene->mAnimations[0]->mTicksPerSecond != 0 ? scene->mAnimations[0]->mTicksPerSecond : 25.0f);
float TimeInTicks = time * TicksPerSecond;
float AnimationTime = fmod(TimeInTicks, (float)scene->mAnimations[0]->mDuration);
aiMatrix4x4 identity = aiMatrix4x4();
readNodeHierarchy(AnimationTime, scene->mRootNode, identity);
for (uint32_t i = 0; i < boneTransforms.size(); i++)
{
boneTransforms[i] = boneInfo[i].finalTransformation;
}
}
~SkinnedMesh()
{
vertexBuffer.vertices.destroy();
vertexBuffer.indices.destroy();
}
private:
// 查找给定节点的动画
const aiNodeAnim* findNodeAnim(const aiAnimation* animation, const std::string nodeName)
{
for (uint32_t i = 0; i < animation->mNumChannels; i++)
{
const aiNodeAnim* nodeAnim = animation->mChannels[i];
if (std::string(nodeAnim->mNodeName.data) == nodeName)
{
return nodeAnim;
}
}
return nullptr;
}
// 返回一个在当前帧和下一帧之间插入转换的4x4矩阵
aiMatrix4x4 interpolateTranslation(float time, const aiNodeAnim* pNodeAnim)
{
aiVector3D translation;
if (pNodeAnim->mNumPositionKeys == 1)
{
translation = pNodeAnim->mPositionKeys[0].mValue;
}
else
{
uint32_t frameIndex = 0;
for (uint32_t i = 0; i < pNodeAnim->mNumPositionKeys - 1; i++)
{
if (time < (float)pNodeAnim->mPositionKeys[i + 1].mTime)
{
frameIndex = i;
break;
}
}
aiVectorKey currentFrame = pNodeAnim->mPositionKeys[frameIndex];
aiVectorKey nextFrame = pNodeAnim->mPositionKeys[(frameIndex + 1) % pNodeAnim->mNumPositionKeys];
float delta = (time - (float)currentFrame.mTime) / (float)(nextFrame.mTime - currentFrame.mTime);
const aiVector3D& start = currentFrame.mValue;
const aiVector3D& end = nextFrame.mValue;
translation = (start + delta * (end - start));
}
aiMatrix4x4 mat;
aiMatrix4x4::Translation(translation, mat);
return mat;
}
//返回一个在当前帧和下一帧之间插入旋转的4x4矩阵
aiMatrix4x4 interpolateRotation(float time, const aiNodeAnim* pNodeAnim)
{
aiQuaternion rotation;
if (pNodeAnim->mNumRotationKeys == 1)
{
rotation = pNodeAnim->mRotationKeys[0].mValue;
}
else
{
uint32_t frameIndex = 0;
for (uint32_t i = 0; i < pNodeAnim->mNumRotationKeys - 1; i++)
{
if (time < (float)pNodeAnim->mRotationKeys[i + 1].mTime)
{
frameIndex = i;
break;
}
}
aiQuatKey currentFrame = pNodeAnim->mRotationKeys[frameIndex];
aiQuatKey nextFrame = pNodeAnim->mRotationKeys[(frameIndex + 1) % pNodeAnim->mNumRotationKeys];
float delta = (time - (float)currentFrame.mTime) / (float)(nextFrame.mTime - currentFrame.mTime);
const aiQuaternion& start = currentFrame.mValue;
const aiQuaternion& end = nextFrame.mValue;
aiQuaternion::Interpolate(rotation, start, end, delta);
rotation.Normalize();
}
aiMatrix4x4 mat(rotation.GetMatrix());
return mat;
}
// 返回一个在当前帧和下一帧之间插入缩放的4x4矩阵
aiMatrix4x4 interpolateScale(float time, const aiNodeAnim* pNodeAnim)
{
aiVector3D scale;
if (pNodeAnim->mNumScalingKeys == 1)
{
scale = pNodeAnim->mScalingKeys[0].mValue;
}
else
{
uint32_t frameIndex = 0;
for (uint32_t i = 0; i < pNodeAnim->mNumScalingKeys - 1; i++)
{
if (time < (float)pNodeAnim->mScalingKeys[i + 1].mTime)
{
frameIndex = i;
break;
}
}
aiVectorKey currentFrame = pNodeAnim->mScalingKeys[frameIndex];
aiVectorKey nextFrame = pNodeAnim->mScalingKeys[(frameIndex + 1) % pNodeAnim->mNumScalingKeys];
float delta = (time - (float)currentFrame.mTime) / (float)(nextFrame.mTime - currentFrame.mTime);
const aiVector3D& start = currentFrame.mValue;
const aiVector3D& end = nextFrame.mValue;
scale = (start + delta * (end - start));
}
aiMatrix4x4 mat;
aiMatrix4x4::Scaling(scale, mat);
return mat;
}
// 获取当前动画时间的节点层次结构
void readNodeHierarchy(float AnimationTime, const aiNode* pNode, const aiMatrix4x4& ParentTransform)
{
std::string NodeName(pNode->mName.data);
aiMatrix4x4 NodeTransformation(pNode->mTransformation);
const aiNodeAnim* pNodeAnim = findNodeAnim(pAnimation, NodeName);
if (pNodeAnim)
{
// 在当前帧和下一帧之间得到插值矩阵
aiMatrix4x4 matScale = interpolateScale(AnimationTime, pNodeAnim);
aiMatrix4x4 matRotation = interpolateRotation(AnimationTime, pNodeAnim);
aiMatrix4x4 matTranslation = interpolateTranslation(AnimationTime, pNodeAnim);
NodeTransformation = matTranslation * matRotation * matScale;
}
aiMatrix4x4 GlobalTransformation = ParentTransform * NodeTransformation;
if (boneMapping.find(NodeName) != boneMapping.end())
{
uint32_t BoneIndex = boneMapping[NodeName];
boneInfo[BoneIndex].finalTransformation = globalInverseTransform * GlobalTransformation * boneInfo[BoneIndex].offset;
}
for (uint32_t i = 0; i < pNode->mNumChildren; i++)
{
readNodeHierarchy(AnimationTime, pNode->mChildren[i], GlobalTransformation);
}
}
};
2.2 加载模型骨骼树数据
我们接下来主要根据assimp来解析对应的骨骼数据:
// 根据assimp读取的数据加载一个网格
void loadMesh()
{
skinnedMesh = new SkinnedMesh();
std::string filename = "D://models//goblin.dae";
skinnedMesh->scene = skinnedMesh->Importer.ReadFile(filename.c_str(), 0);
skinnedMesh->setAnimation(0);
// 设置骨骼
// 每个顶点有一个顶点骨骼结构
uint32_t vertexCount(0);
for (uint32_t m = 0; m < skinnedMesh->scene->mNumMeshes; m++) {
vertexCount += skinnedMesh->scene->mMeshes[m]->mNumVertices;
};
skinnedMesh->bones.resize(vertexCount);
// 存储根节点的全局逆变换矩阵
skinnedMesh->globalInverseTransform = skinnedMesh->scene->mRootNode->mTransformation;
skinnedMesh->globalInverseTransform.Inverse();
// 骨骼数据(权重和IDs)
uint32_t vertexBase(0);
for (uint32_t m = 0; m < skinnedMesh->scene->mNumMeshes; m++) {
aiMesh *paiMesh = skinnedMesh->scene->mMeshes[m];
if (paiMesh->mNumBones > 0) {
skinnedMesh->loadBones(paiMesh, vertexBase, skinnedMesh->bones);
}
vertexBase += skinnedMesh->scene->mMeshes[m]->mNumVertices;
}
// 创建顶点缓冲数据
std::vector<Vertex> vertexBuffer;
// 遍历文件中的所有网格并提取这个模型中使用的顶点信息
vertexBase = 0;
for (uint32_t m = 0; m < skinnedMesh->scene->mNumMeshes; m++) {
for (uint32_t v = 0; v < skinnedMesh->scene->mMeshes[m]->mNumVertices; v++) {
Vertex vertex;
vertex.pos = glm::make_vec3(&skinnedMesh->scene->mMeshes[m]->mVertices[v].x);
vertex.normal = glm::make_vec3(&skinnedMesh->scene->mMeshes[m]->mNormals[v].x);
vertex.uv = glm::make_vec2(&skinnedMesh->scene->mMeshes[m]->mTextureCoords[0][v].x);
vertex.color = (skinnedMesh->scene->mMeshes[m]->HasVertexColors(0)) ? glm::make_vec3(&skinnedMesh->scene->mMeshes[m]->mColors[0][v].r) : glm::vec3(1.0f);
// 获取骨骼权重和id
for (uint32_t j = 0; j < MAX_BONES_PER_VERTEX; j++) {
vertex.boneWeights[j] = skinnedMesh->bones[vertexBase + v].weights[j];
vertex.boneIDs[j] = skinnedMesh->bones[vertexBase + v].IDs[j];
}
vertexBuffer.push_back(vertex);
}
vertexBase += skinnedMesh->scene->mMeshes[m]->mNumVertices;
}
VkDeviceSize vertexBufferSize = vertexBuffer.size() * sizeof(Vertex);
// Generate index buffer from loaded mesh file 从加载的网格文件生成索引缓冲区
std::vector<uint32_t> indexBuffer;
for (uint32_t m = 0; m < skinnedMesh->scene->mNumMeshes; m++) {
uint32_t indexBase = static_cast<uint32_t>(indexBuffer.size());
for (uint32_t f = 0; f < skinnedMesh->scene->mMeshes[m]->mNumFaces; f++) {
for (uint32_t i = 0; i < 3; i++)
{
indexBuffer.push_back(skinnedMesh->scene->mMeshes[m]->mFaces[f].mIndices[i] + indexBase);
}
}
}
VkDeviceSize indexBufferSize = indexBuffer.size() * sizeof(uint32_t);
skinnedMesh->vertexBuffer.indexCount = static_cast<uint32_t>(indexBuffer.size());
...
//创建buffer等数据
...
}
2.3 更新骨骼变化矩阵
void updateUniformBuffers(bool viewChanged)
{
// 常规uniform数据更新
if (viewChanged)
{
const glm::vec3 scale = glm::vec3(0.0025f);
uboVS.projection = camera.matrices.perspective;
uboVS.view = camera.matrices.view;
uboVS.viewPos = glm::vec4(camera.position, 0.0f) * glm::vec4(-1.0f);
uboVS.model = glm::rotate(glm::mat4(1.0f), glm::radians(90.0f), glm::vec3(1.0f, 0.0f, 0.0f));
uboVS.model = glm::scale(uboVS.model, scale);
uboFloor.projection = camera.matrices.perspective;
uboFloor.view = camera.matrices.view;
uboFloor.model = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 4.5f, 0.0f));
uboFloor.model = glm::rotate(uboFloor.model, glm::radians(90.0f), glm::vec3(1.0f, 0.0f, 0.0f));
uboFloor.model = glm::scale(uboFloor.model, scale);
uboFloor.viewPos = glm::vec4(camera.position, 0.0f) * glm::vec4(-1.0f);
}
// 更新骨骼矩阵数据
// 其中runningTime根据程序运行时间
skinnedMesh->update(runningTime);
for (uint32_t i = 0; i < skinnedMesh->boneTransforms.size(); i++)
{
uboVS.bones[i] = glm::transpose(glm::make_mat4(&skinnedMesh->boneTransforms[i].a1));
}
uniformBuffers.mesh.copyTo(&uboVS, sizeof(uboVS));
// 通过修改uv数据,实现地面移动效果
uboFloor.uvOffset.t -= 0.25f * skinnedMesh->animationSpeed * frameTimer;
uniformBuffers.floor.copyTo(&uboFloor, sizeof(uboFloor));
}
2.4 模型顶点着色器实现
我们实现骨骼动画主要就是在顶点着色器对每个顶点数据进行骨骼矩阵加成得到不同位置数据以达到动态效果,其顶点着色器如下:
#version 450
layout (location = 0) in vec3 inPos;
layout (location = 1) in vec3 inNormal;
layout (location = 2) in vec2 inUV;
layout (location = 3) in vec3 inColor;
layout (location = 4) in vec4 inBoneWeights;
layout (location = 5) in ivec4 inBoneIDs;
#define MAX_BONES 64
layout (binding = 0) uniform UBO
{
mat4 projection;
mat4 view;
mat4 model;
mat4 bones[MAX_BONES];
vec4 lightPos;
vec4 viewPos;
} ubo;
layout (location = 0) out vec3 outNormal;
layout (location = 1) out vec3 outColor;
layout (location = 2) out vec2 outUV;
layout (location = 3) out vec3 outViewVec;
layout (location = 4) out vec3 outLightVec;
out gl_PerVertex
{
vec4 gl_Position;
};
void main()
{
//骨骼节点矩阵
mat4 boneTransform = ubo.bones[inBoneIDs[0]] * inBoneWeights[0];
boneTransform += ubo.bones[inBoneIDs[1]] * inBoneWeights[1];
boneTransform += ubo.bones[inBoneIDs[2]] * inBoneWeights[2];
boneTransform += ubo.bones[inBoneIDs[3]] * inBoneWeights[3];
outColor = inColor;
outUV = inUV;
gl_Position = ubo.projection * ubo.view * ubo.model * boneTransform * vec4(inPos.xyz, 1.0);
vec4 pos = ubo.model * vec4(inPos, 1.0);
outNormal = mat3(boneTransform) * inNormal;
outLightVec = ubo.lightPos.xyz - pos.xyz;
outViewVec = ubo.viewPos.xyz - pos.xyz;
}
运行后可见开头所见模型动画。