上一篇博客实现了cpu skin(蒙皮)的骨骼动画,接下来我们要实现 gpu skin(蒙皮) 的骨骼动画
首先我们需要知道什么是蒙皮,在上一博客实现的骨骼动画中,哪一步是蒙皮 。在模型文件中可以读到的数据是顶点和骨骼的对应关系,从动画文件中可以读到每一帧骨骼的数据,而根据每一帧骨骼的数据计算出顶点位置的过程就叫做蒙皮,也就是对应上一篇博客代码中的ComputeVertPos函数。另外还要知道为什么要在gpu蒙皮, 它比在cpu蒙皮优势在哪里 。 其实读者如果有对照着上一篇博客自己实现过cpu skin,就会发现在每一帧都需要将模型所有的顶点信息计算出来重新上传到GPU,而随便一一个模型可能都有成千上万个顶点, 这是一个相对比较耗时的事情。本来在 可编程渲染管线出来之前,我们只能这么做 ,但是可编程渲染管线的出现,允许我们先将网格的顶点信息传递到GPU内存,然后将经过动画处理的骨骼矩阵传递给GPU,并在顶点着色器里计算动画的顶点位置和法线,然后在进行后续渲染 。相比每一帧向GPU发送成千上万个顶点,每一帧发送所有骨骼的变换矩阵要更加省时。而且另一方面,对于大量的矩阵运算,GPU的运算效率要比CPU高很多,这是由二者的硬件结构所决定的。我们知道什么是GPU 蒙皮 以及 为什么要在gpu 蒙皮,接下里我们就要实现在上一篇博客的基础上实现GPU 蒙皮
首先我们需要修改一下顶点的数据结构,在顶点着色器中计算顶点的位置,这需要将顶点关联的骨骼数据全部上传给GPU,所以我们新加了两个额外的数据 boneWeights 和 boneIndexs,其中boneWeights用于存储权重,每个顶点最多可以存储四个权重,也就是每个顶点最多可以加权四个骨骼,通常四个骨骼足以使网格的顶点动起来,Unity游戏引擎的骨骼动画好像也是最多四个骨骼,这里使用四分量浮点值来存储。而骨骼的索引储存在boneIndexs中,用于从骨骼结构中读取对应骨骼的数据,数据结构如下
struct Vertex
{
glm::vec3 pos; // 顶点在模型空间中的位置
glm::vec2 texcoord;// 纹理坐标
glm::vec4 boneWeights;// 所关联骨骼的权重
glm::vec4 boneIndexs; // 所关联骨骼的索引
int startWeight;//所关联权重的起始索引
int weightCount;// 所关联的权重总数
};
其他的数据结构如下
struct Joint
{
string name; //骨骼名称
int parent_ID; //父骨骼在骨骼层次结构中的索引
glm::vec3 pos; //初始姿势时骨骼在模型空间中的位置
glm::quat orient;//初始姿势时骨骼坐标空间相对模型空间的旋转
};
typedef vector<Joint> JointList;
struct Vertex
{
glm::vec3 pos; // 顶点在模型空间中的位置
glm::vec2 texcoord;// 纹理坐标
glm::vec4 boneWeights;// 所关联骨骼的权重
glm::vec4 boneIndexs; // 所关联骨骼的索引
int startWeight;//所关联权重的起始索引
int weightCount;// 所关联的权重总数
};
typedef vector<Vertex> VertexList;
struct Weight
{
int joint_ID;// 与该权重关联的骨骼在骨骼层次结构中的索引
float bias; // 权重占比
glm::vec3 pos; // 权重在所关联骨骼坐标空间中的位置
};
typedef vector<Weight> WeightList;
typedef vector<glm::mat4> MatrixList;
typedef vector<unsigned int> IndexBuffer;
struct Mesh
{
string shader; // 纹理
unsigned int texID; //纹理缓冲对象
unsigned int VAO, VBO, EBO;// 顶点数组对象,顶点缓冲对象,索引缓冲对象
VertexList verts; // 顶点数组(这里用数组来表示集合,数据结构是vertor)
WeightList weights;//权重数组
IndexBuffer indexBuffer;// 索引数组
};
typedef vector<Mesh> MeshList;
class TestMD5 {
public:
TestMD5();
~TestMD5();
bool LoadModel(string& path);
unsigned int LoadTexture(string& path);
void CreateVertexBuffer(Mesh& mesh);
void ComputeMatrix(Mesh& mesh, const FrameSkeleton& skeleton);
void BuildBindPose(JointList& jointList);
void PrepareMesh(Mesh& mesh);
void Update(float deltaTime);
void Render(ShaderC shader);
MD5Animation animation; //动画类 后面会详细讲到
private:
int numJonints; // 骨骼数量
int numMeshes; //mesh数量
JointList jointList;// 骨骼数组
MeshList meshList;// mesh数组
MatrixList inverseBindPose;
MatrixList animatedBones;
};
另外从上一篇博客中可以知道我们从模型文件中读取到的是顶点与骨骼的对应关系,并没有顶点的位置数据,那么我们想要将渲染出整个mesh 就需要选定一个姿势(其实是处于该姿势时的骨骼架构)来计算出所有顶点的模型位置,而这个姿势就是所谓的绑定姿势,例如unity人型动画的绑定姿势就是 T-Pos ,在本文中 我们选择模型的默认姿势,也就从我们在上一篇博客中一开始加载出来的那个不会动的模型所处的姿势。这个我们放在BuildBindPose函数内实现 ,在每一个mesh数据被读取出来时 调用 PrepareMesh(mesh)函数 函数的实现如下
void TestMD5::PrepareMesh(Mesh& mesh)
{
for (unsigned int i = 0;i < mesh.verts.size();i++)
{
Vertex& vert = mesh.verts[i];
vert.pos = glm::vec3(0);
vert.boneIndexs = glm::vec4(0);
vert.boneWeights = glm::vec4(0);
for (int j = 0;j < vert.weightCount;j++)
{
Weight& weight = mesh.weights[vert.startWeight + j];
Joint& joint = jointList[weight.joint_ID];
glm::vec3 rotPos = joint.orient * weight.pos;
vert.pos += (joint.pos + rotPos) * weight.bias;
vert.boneWeights[j] = weight.bias;
vert.boneIndexs[j] = weight.joint_ID;
}
}
}
上面的函数除了计算出绑定姿势mesh所有顶点的位置以外,还设置了顶点关联的骨骼索引以及骨骼的权重。
在选定了绑定姿势之后,我们需要计算出在绑定姿势下 模型空间到每一个骨骼空间的变换矩阵,因为在动画的播放中,无论骨骼如何变换,顶点在骨骼空间的位置是不会变化的,也就是说 我们可以将绑定姿势下顶点的模型空间位置(PosModel)传递到顶点着色器,再将模型空间到骨骼的变换矩阵传递到顶点着色器,就可以计算出顶点在骨骼空间下的位置(PosBone),然后Update函数中我们再计算出当前时刻所有骨骼到模型空间的变换矩阵,用该变换矩阵乘以顶点在骨骼空间下的位置,就可以得到当前时刻顶点在模型空间中的位置了 ,总结一下整个过程 顶点的坐标空间变换就是 模型空间 -> 骨骼空间 - > 模型空间 。接下来我们需要先计算出绑定姿势时 模型空间到每一个骨骼空间的变换矩阵。这个我们放在BuildBindPose函数里面做,在骨骼数据加载时调用,并将计算出来的矩阵保存到inverseBindPose 。 代码如下
void TestMD5::BuildBindPose(JointList& jointList)
{
inverseBindPose.clear();
JointList::const_iterator iter = jointList.begin();
while (iter != jointList.end())
{
const Joint& joint = (*iter);
glm::mat4 boneMatrix(1.0f);
glm::mat4 boneTranstion = glm::translate(boneMatrix, joint.pos);
glm::mat4 b