Chapter 10: Mesh Skinning
Deforming a mesh to match an animated pose is called skinning.
Mesh Skinning是基于Animation Pose,改变Mesh去匹配Pose的过程,可以通过Shader进行GPU Skinning,也可以用CPU进行C++这边的Skinning,我以前还写过一篇文章比较它俩的特点。
本章重点:
- skinned mesh与non-skinned mesh的区别
- 了解整个skinning pipeline
- 实现skeleton类
- 什么是bind pose以及怎么从glTF文件里读取skeleton的bind pose
- 实现skinned mesh类,以及从glTF里读取skinned mesh数据
- 实现GPU蒙皮和CPU蒙皮
Exploring meshes
一般模型会自带一个静态的Mesh,这个Mesh一般是T pose或者A pose,当一个模型的Mesh被创建出来的时候,往往也会在mesh里创建一个skeleton,mesh上的每个点都会绑定到skeleton上至少一个的joints上,这个mesh点绑定骨骼的过程叫做rigging ,此时的骨骼的pose,会匹配这个模型的静态Mesh,此时模型的Pose(也就是Skeleton对应的Pose)叫做Bind Pose,很多时候,T pose或者A pose就是Bind Pose,如下图所示:
Bind pose与rest pose 很多时候二者是同一个Pose,但是二者实际不是一个东西,不一定是同一个Pose,具体的可以看附录
Understanding skinning
Skinning is the process of specifying which vertex should be deformed by which bone.
有两种Skinning,rigid skinning和smooth skinning,前者的每个vertex只受一个bone影响,后者的每个vertex受多个bone影响。
在处理数据的时候,有两种做法,两种其实都可以,因为Vertex和Bone可以是多对多的交叉关系:
- 由于一个点可能受一块或多块Bone影响,一种是每个Vertex存储其Bone的id
- 另一种则相反,是每块Bone记录它所影响的顶点
本书的做法是,每个顶点存受影响的Bones的信息,也就是第一种做法
就Rigid Skinning而言,由于每个Vertex只受一个Bone影响,所以这里其实可以在Vertex Attribute里把Bone的Id存下来,就类似于顶点pos、normal、texCoord一样,毕竟它的size是固定的(不过像Unity里有的,一个Vertex最多受4个Bone影响,感觉也可以作为Vertex Attribute,无非是四个int值,可以用特殊的int -1代表不受影响,但总之size是固定的)
The vertex transformation pipeline
Space refers to transforming a vertex by a matrix. For example, if you have a projection matrix, it would transform a vertex into NDC space:
- When a mesh is created, all its vertices are in what is called model space.
- A model space vertex is multiplied by the model matrix, which puts it into world space.
- A world space vertex is multiplied by the view matrix to put it into camera space.
- A camera space vertex is multiplied by the projection matrix to move it into NDC space.
skinning过程,还有一个额外的顶点变换,就是把顶点从skin space转换到model space,要加在上面四个步骤的最前面,整体流程如下:
- 加载模型的Mesh,每个顶点存的是Model Space的坐标
- 每个model space的顶点坐标会被转化为skin space(我之前看的书里是,存了个矩阵,这个矩阵可以把顶点从model space转换到对应的bone的bone space,如果受多个bone影响,会分别对应一个矩阵)
- 把每个model space的顶点坐标,再乘以WVP矩阵
Exploring rigid skinning
To skin a mesh, each vertex needs to be multiplied by the inverse bind pose transform of the joint it belongs to.
这里认为,bind pose就是对应的角色的static mesh,上面这句话的重点在inverse bind pose transform of the joint,也就是说,为了把顶点的坐标,从model space下,转换到对应bone的joint space下,需要左乘对应joint的model transform矩阵的逆。
推导一下应该也不难,假设顶点坐标为P,P本来存的是Model Space的坐标,现在有个Joint,其Model Transform矩阵为M,假设顶点在Joint的skin space下的local坐标为L,那么有:
M * L = P
由此可以得到:
L = M.Inverse() * P
所以要把顶点坐标转换到Bone Space(Skin Space)下,只需要乘以Bone的Model矩阵的逆即可。
如果这里的P存储的是World Space的坐标,也是一样的,无非是W * L = P ,那就乘以Bone的World Space矩阵的逆即可。根据这个,也可以得到一个结论,如果想把物体A的世界坐标P,改为物体B的Local 坐标,那么左乘B的世界矩阵的逆即可。
如下图所示是一个错误的Mesh,这种情况下,最常见的错误是Joint的逆矩阵与Animated Pose之间的矩阵顺序乘反了(The most common reason for seeing a mesh such as this is that there has been an error in the multiplication order of the inverse bind pose and animated pose)
The rigid skinning pipeline
如下图所示,一个是static mesh上的点的绘制过程,另一个是动画的dynamic mesh上的点的绘制过程:
Model Space的点-> 转到Bone Space下 -> 再配合Joint Hierarchy算出新的Model Space的点(左乘Joint的Model Transform) -> WVP转换
虽然一通转换,顶点从Model Space还是转回了Model Space,但这个Mesh却是根据Animation Pose进行了deformation(This results in the vertex being in local space again, but it is deformed to the animated pose)。
rigid skinning的缺点
The problem with rigid skinning is bending joints.
像肘关节这种地方的动画,会很不自然,如下图所示:
Exploring smooth skinning
Think of smooth skinning as skinning a mesh multiple times and blending the results.
Generally, after four bones, the influence of each additional bone is not visible. This is convenient as it lets you use the ivec4 and vec4 structs to add influences and weights to vertices. 所以一般都是最多四个
下图展示了这片区域的Mesh,受两块权重为0.5的Bone的影响的结果,左右两边的图分别是只受一个Bone影响,确实很像是两个result的Blend: 这种Blend方法,叫做linear blend skinning (LBS),是目前最常用的实现动画蒙皮的技术。
此时的顶点属性,是这样:
- The position (vec3)
- The normal (vec3)
- The texture coordinate (vec2)
- The joint influences (ivec4)
- The influence weights (vec4)
顺便提一句,glTF里的nodes都是可以有skinned mesh作为attachments的,但是这里为了方便学习,默认glTF里的模型只有一个skinned mesh,而且其默认的位置就是从世界坐标系的0,0开始
Implementing skeletons
The concept of a skeleton is to combine data that is shared between animated models into a single structure.
上一章的内容是实现Pose和Clip类,这里面其实也有skeleton的东西,但是它们都是存的int数组来表示这个joints的数组,比如Pose类是这么存储skeleton的hierarchy的:
std::vector<Transform> mJoints;
std::vector<int> mParents;
所以这节索性把这玩意儿单独抽象为一个类,那我理解的是,后面的Clip和Pose类是不是也要改,尤其是Pose类?
对于动画里的角色而已,同一个角色可以生成多个Instance,但是其实它们的Bind Pose都是一样的,感觉甚至可以认为这是一个Read-only的共享的东西,除了Bind Pose是共享的,Inverse Bind Pose(也就是它们的逆矩阵),还有Joint的名字,都是共享的,所以这里单独创建了Skeleton对象,用于存储这些共享的东西。
这里的Skeleton不仅仅只是Joint的Hierarchy,还有Bind Pose和Inverse Bind Pose,所以说比前面的Pose类里的俩数组内容丰富得多。一些其他的说法,把这里的Skeleton类叫做Rig或者Armature
感觉Skeleton类应该有以下内容:
std::vector<Transform> mBindPose ,表示Joints数组,里面存了BindPosestd::vector<mat4> mInverseBindPose ,与上面数组一一对应,里面存了InverseBindPose- 一些API,比如取到什么Joint的Model Trans,也就是一层层算下来的Transform
看了下书里的代码,发现这里的Skeleton的Hierarchy还是基于Pose来存的,既然Pose已经定义好了,没必要自己再写一遍,这是Skeleton类的声明:
#ifndef _H_SKELETON_
#define _H_SKELETON_
#include "Pose.h"
#include "mat4.h"
#include <vector>
#include <string>
class Skeleton
{
protected:
Pose mRestPose;
Pose mBindPose;
std::vector<mat4> mInvBindPose;
std::vector<std::string> mJointNames;
protected:
void UpdateInverseBindPose();
public:
Skeleton();
Skeleton(const Pose& rest, const Pose& bind, const std::vector<std::string>& names);
void Set(const Pose& rest, const Pose& bind, const std::vector<std::string>& names);
Pose& GetBindPose();
Pose& GetRestPose();
std::vector<mat4>& GetInvBindPose();
std::vector<std::string>& GetJointNames();
std::string& GetJointName(unsigned int index);
};
#endif
有点意思,这里的Skeleton本质上就是一个BindPose,还附带了BindPose上的每个Joint的Model矩阵的逆
下面是类实现的代码:
#include "Skeleton.h"
Skeleton::Skeleton() { }
Skeleton::Skeleton(const Pose& rest, const Pose& bind, const std::vector<std::string>& names)
{
Set(rest, bind, names);
}
void Skeleton::Set(const Pose& rest, const Pose& bind, const std::vector<std::string>& names)
{
mRestPose = rest;
mBindPose = bind;
mJointNames = names;
UpdateInverseBindPose();
}
void Skeleton::UpdateInverseBindPose()
{
unsigned int size = mBindPose.Size();
mInvBindPose.resize(size);
for (unsigned int i = 0; i < size; ++i)
{
Transform world = mBindPose.GetGlobalTransform(i);
mInvBindPose[i] = inverse(transformToMat4(world));
}
}
Pose& Skeleton::GetBindPose()
{
return mBindPose;
}
Pose& Skeleton::GetRestPose()
{
return mRestPose;
}
std::vector<mat4>& Skeleton::GetInvBindPose()
{
return mInvBindPose;
}
std::vector<std::string>& Skeleton::GetJointNames()
{
return mJointNames;
}
std::string& Skeleton::GetJointName(unsigned int idx)
{
return mJointNames[idx];
}
glTF – loading the bind pose
bind pose就是一个Pose,这里需要获取到这个Pose的数据,Pose由俩数组组成,一个代表Joints的Hierarchy,一个代表Joints的LocalTransform。
但是glTF里是没有Bind Pose这个概念的,它的各个node的默认Transform是Rest Pose的数据,不一定是Bind Pose数据。不过它有一个skin对象,如下图所示,node记录了它的引用。这个skin里存储了还原Bind Pose的相关信息,skin对象里装了一个矩阵数组,每个矩阵与Skeleton里的一个joint对应,矩阵代表了Joint的WorldTransform的逆矩阵(WorldTransform是模型空间里的,也可叫ModelTransforn):
有了这个数组,就可以创建出自己的Bind Pose了,不过并不是模型里所有的Joints都会绑定到Mesh上,也就是说可能有的皮肤完全不受这个Joint影响,这种情况下,Skin里不会基类他的信息,但是我一个Pose又必须要给每个Joint设置LocalTransform的值,所以这里需要使用前面读进来的Rest Pose作为默认值。
相关代码如下:
Pose LoadBindPose(cgltf_data* data)
{
Pose restPose = LoadRestPose(data);
unsigned int numBones = restPose.Size();
std::vector<Transform> worldBindPose(numBones);
for (unsigned int i = 0; i < numBones; ++i)
worldBindPose[i] = restPose.GetGlobalTransform(i);
unsigned int numSkins = (unsigned int)data->skins_count;
for (unsigned int i = 0; i < numSkins; ++i)
{
cgltf_skin* skin = &(data->skins[i]);
std::vector<float> invBindAccessor;
GLTFHelpers::GetScalarValues(invBindAccessor, 16, *skin->inverse_bind_matrices);
unsigned int numJoints = (unsigned int)skin->joints_count;
for (unsigned int j = 0; j < numJoints; ++j)
{
float* matrix = &(invBindAccessor[j * 16]);
mat4 invBindMatrix = mat4(matrix);
mat4 bindMatrix = inverse(invBindMatrix);
Transform bindTransform = mat4ToTransform(bindMatrix);
cgltf_node* jointNode = skin->joints[j];
int jointIndex = GLTFHelpers::GetNodeIndex(jointNode, data->nodes, numBones);
worldBindPose[jointIndex] = bindTransform;
}
}
Pose bindPose = restPose;
for (unsigned int i = 0; i < numBones; ++i)
{
Transform current = worldBindPose[i];
int p = bindPose.GetParent(i);
if (p >= 0)
{
Transform parent = worldBindPose[p];
current = combine(inverse(parent), current);
}
bindPose.SetLocalTransform(i, current);
}
return bindPose;
}
glTF – loading a skeleton
这里的Skeleton主要就是俩Pose,然后把names收集了一下,目前已经定义好了LoadRestPose 和LoadBindPose 函数,所以在GLTFLoader.cpp里把从glTF文件里Load Skeleton的函数封装了起来:
Skeleton LoadSkeleton(cgltf_data* data)
{
return Skeleton(LoadRestPose(data), LoadBindPose(data), LoadJointNames(data)
);
Mesh类实现
这里的Mesh类主要是为了实现GPU Skinning和CPU Skinning,接口如下:
#ifndef _H_MESH_
#define _H_MESH_
#include "vec2.h"
#include "vec3.h"
#include "vec4.h"
#include "mat4.h"
#include <vector>
#include "Attribute.h"
#include "IndexBuffer.h"
#include "Skeleton.h"
#include "Pose.h"
class Mesh
{
protected:
std::vector<vec3> mPosition;
std::vector<vec3> mNormal;
std::vector<vec2> mTexCoord;
std::vector<vec4> mWeights;
std::vector<ivec4> mInfluences;
std::vector<unsigned int> mIndices;
protected:
Attribute<vec3>* mPosAttrib;
Attribute<vec3>* mNormAttrib;
Attribute<vec2>* mUvAttrib;
Attribute<vec4>* mWeightAttrib;
Attribute<ivec4>* mInfluenceAttrib;
IndexBuffer* mIndexBuffer;
protected:
std::vector<vec3> mSkinnedPosition;
std::vector<vec3> mSkinnedNormal;
std::vector<mat4> mPosePalette;
public:
Mesh();
Mesh(const Mesh&);
Mesh& operator=(const Mesh&);
~Mesh();
std::vector<vec3>& GetPosition();
std::vector<vec3>& GetNormal();
std::vector<vec2>& GetTexCoord();
std::vector<vec4>& GetWeights();
std::vector<ivec4>& GetInfluences();
std::vector<unsigned int>& GetIndices();
void CPUSkin(Skeleton& skeleton, Pose& pose);
void UpdateOpenGLBuffers();
void Bind(int position, int normal, int texCoord, int weight, int influcence);
void Draw();
void DrawInstanced(unsigned int numInstances);
void UnBind(int position, int normal, int texCoord, int weight, int influcence);
};
#endif
下面是具体部分的实现代码:
#include "Mesh.h"
#include "Draw.h"
#include "Transform.h"
Mesh::Mesh()
{
mPosAttrib = new Attribute<vec3>();
mNormAttrib = new Attribute<vec3>();
mUvAttrib = new Attribute<vec2>();
mWeightAttrib = new Attribute<vec4>();
mInfluenceAttrib = new Attribute<ivec4>();
mIndexBuffer = new IndexBuffer();
}
Mesh::Mesh(const Mesh& other)
{
mPosAttrib = new Attribute<vec3>();
mNormAttrib = new Attribute<vec3>();
mUvAttrib = new Attribute<vec2>();
mWeightAttrib = new Attribute<vec4>();
mInfluenceAttrib = new Attribute<ivec4>();
mIndexBuffer = new IndexBuffer();
*this = other;
}
Mesh& Mesh::operator=(const Mesh& other)
{
if (this == &other)
return *this;
mPosition = other.mPosition;
mNormal = other.mNormal;
mTexCoord = other.mTexCoord;
mWeights = other.mWeights;
mInfluences = other.mInfluences;
mIndices = other.mIndices;
UpdateOpenGLBuffers();
return *this;
}
Mesh::~Mesh()
{
delete mPosAttrib;
delete mNormAttrib;
delete mUvAttrib;
delete mWeightAttrib;
delete mInfluenceAttrib;
delete mIndexBuffer;
}
std::vector<vec3>& Mesh::GetPosition()
{
return mPosition;
}
std::vector<vec3>& Mesh::GetNormal()
{
return mNormal;
}
std::vector<vec2>& Mesh::GetTexCoord()
{
return mTexCoord;
}
std::vector<vec4>& Mesh::GetWeights()
{
return mWeights;
}
std::vector<ivec4>& Mesh::GetInfluences()
{
return mInfluences;
}
std::vector<unsigned int>& Mesh::GetIndices()
{
return mIndices;
}
void Mesh::UpdateOpenGLBuffers()
{
if (mPosition.size() > 0)
mPosAttrib->Set(mPosition);
if (mNormal.size() > 0)
mNormAttrib->Set(mNormal);
if (mTexCoord.size() > 0)
mUvAttrib->Set(mTexCoord);
if (mWeights.size() > 0)
mWeightAttrib->Set(mWeights);
if (mInfluences.size() > 0)
mInfluenceAttrib->Set(mInfluences);
if (mIndices.size() > 0)
mIndexBuffer->Set(mIndices);
}
void Mesh::Bind(int position, int normal, int texCoord, int weight, int influcence)
{
if (position >= 0)
mPosAttrib->BindTo(position);
if (normal >= 0)
mNormAttrib->BindTo(normal);
if (texCoord >= 0)
mUvAttrib->BindTo(texCoord);
if (weight >= 0)
mWeightAttrib->BindTo(weight);
if (influcence >= 0)
mInfluenceAttrib->BindTo(influcence);
}
void Mesh::Draw()
{
if (mIndices.size() > 0)
::Draw(*mIndexBuffer, DrawMode::Triangles);
else
::Draw(mPosition.size(), DrawMode::Triangles);
}
void Mesh::DrawInstanced(unsigned int numInstances)
{
if (mIndices.size() > 0)
::DrawInstanced(*mIndexBuffer, DrawMode::Triangles, numInstances);
else
::DrawInstanced(mPosition.size(), DrawMode::Triangles, numInstances);
}
void Mesh::UnBind(int position, int normal, int texCoord, int weight, int influcence)
{
if (position >= 0)
mPosAttrib->UnBindFrom(position);
if (normal >= 0)
mNormAttrib->UnBindFrom(normal);
if (texCoord >= 0)
mUvAttrib->UnBindFrom(texCoord);
if (weight >= 0)
mWeightAttrib->UnBindFrom(weight);
if (influcence >= 0)
mInfluenceAttrib->UnBindFrom(influcence);
}
CPU Skinning
CPU Skinning的特点
CPU skinning is useful if the platform you are developing for has a limited number of uniform registers or a small uniform buffer.
当硬件的uniform寄存器较少,或者uniform buffer比较小的时候,适合使用CPU Skinning。使用这种方式时,需要保存两份Animated Mesh,一份是静态的Mesh,也就是Bind Pose下的Mesh,属性为mPosition和mNormal,另一份是动态的Mesh,这里叫做mSkinnedPosition和mSkinnedNormal
它保存了两份Mesh,一份是Bind Pose对应的Static Mesh,一份是动态的Skinned Mesh,它这里用了俩数组来表示:
std::vector<vec3> mPosition;
std::vector<vec3> mSkinnedPosition;
std::vector<vec3> mNormal;
std::vector<vec3> mSkinnedNormal;
实现CPU Skinning(Matrix Palette)
其实思路很简单,每个Vertex,乘以BindPose里对应的Inverse矩阵,把点转换到Bone Space,然后再左乘该Bone的World Transform即可,然后乘以对应的权重因子,累加即可。
累加的这个矩阵,可以使用在Point 和Normal 上,这种算法跟GPU Skinning类似,可以说是Matrix Palette算法(算出一个Skin Mat4,类似于调色板)
void Mesh::CPUSkin(Skeleton& skeleton, Pose& pose)
{
unsigned int numVerts = (unsigned int)mPosition.size();
if (numVerts == 0)
return;
mSkinnedPosition.resize(numVerts);
mSkinnedNormal.resize(numVerts);
pose.GetMatrixPalette(mPosePalette);
std::vector<mat4> invPosePalette = skeleton.GetInvBindPose();
for (unsigned int i = 0; i < numVerts; ++i)
{
ivec4& j = mInfluences[i];
vec4& w = mWeights[i];
mat4 m0 = (mPosePalette[j.x] * invPosePalette[j.x]) * w.x;
mat4 m1 = (mPosePalette[j.y] * invPosePalette[j.y]) * w.y;
mat4 m2 = (mPosePalette[j.z] * invPosePalette[j.z]) * w.z;
mat4 m3 = (mPosePalette[j.w] * invPosePalette[j.w]) * w.w;
mat4 skin = m0 + m1 + m2 + m3;
mSkinnedPosition[i] = transformPoint(skin, mPosition[i]);
mSkinnedNormal[i] = transformVector(skin, mNormal[i]);
}
mPosAttrib->Set(mSkinnedPosition);
mNormAttrib->Set(mSkinnedNormal);
}
实现CPU Skinning(使用Matrix先算, 再最后融合)
void Mesh::CPUSkin(Skeleton& skeleton, Pose& pose)
{
unsigned int numVerts = (unsigned int)mPosition.size();
if (numVerts == 0)
return;
mSkinnedPosition.resize(numVerts);
mSkinnedNormal.resize(numVerts);
Pose& bindPose = skeleton.GetBindPose();
for (unsigned int i = 0; i < numVerts; ++i)
{
ivec4& joint = mInfluences[i];
vec4& weight = mWeights[i];
Transform skin0 = combine(pose[joint.x], inverse(bindPose[joint.x]));
vec3 p0 = transformPoint(skin0, mPosition[i]);
vec3 n0 = transformVector(skin0, mNormal[i]);
Transform skin1 = combine(pose[joint.y], inverse(bindPose[joint.y]));
vec3 p1 = transformPoint(skin1, mPosition[i]);
vec3 n1 = transformVector(skin1, mNormal[i]);
Transform skin2 = combine(pose[joint.z], inverse(bindPose[joint.z]));
vec3 p2 = transformPoint(skin2, mPosition[i]);
vec3 n2 = transformVector(skin2, mNormal[i]);
Transform skin3 = combine(pose[joint.w], inverse(bindPose[joint.w]));
vec3 p3 = transformPoint(skin3, mPosition[i]);
vec3 n3 = transformVector(skin3, mNormal[i]);
mSkinnedPosition[i] = p0 * weight.x + p1 * weight.y + p2 * weight.z + p3 * weight.w;
mSkinnedNormal[i] = n0 * weight.x + n1 * weight.y + n2 * weight.z + n3 * weight.w;
}
mPosAttrib->Set(mSkinnedPosition);
mNormAttrib->Set(mSkinnedNormal);
}
glTF – loading meshes
primitives in glTF
参考:https://github.com/KhronosGroup/glTF/blob/main/specification/1.0/README.md
In glTF, meshes are defined as arrays of primitives. Primitives correspond to the data required for GL draw calls. Primitives specify one or more attributes, corresponding to the vertex attributes used in the draw calls. Indexed primitives also define an indices property. Attributes and indices are defined as accessors. Each primitive also specifies a material and a primitive type that corresponds to the GL primitive type (e.g., triangle set).
glTF文件里,Mesh其实就是promitives的数组,primitives它可以被认为是submesh,它等同于GL里的调用一次Draw Call需要的数据。比如下面这个文件:
"primitives": [
{
"attributes": {
"NORMAL": "accessor_25",
"POSITION": "accessor_23",
"TEXCOORD_0": "accessor_27"
},
"indices": "accessor_21",
"material": "blinn3-fx",
"mode": 4
}
]
当primitive里定义了indices时,它等同于GL里的DrawElements函数,没有定义时,等同于GL的DrawArrays函数。
这里的可选的attributes类型有:
- POSITION
- NORMAL
- TEXCOORD
- COLOR
- JOINT
- WEIGHT
- [semantic]_[set_index],比如TEXCOORD_0、TEXCOORD_1
这个函数涉及到glTF文件的读取和解析,这里依然放在GLTFLoad的头文件和cpp文件,代码如下:
std::vector<Mesh> LoadMeshes(cgltf_data* data);
std::vector<Mesh> LoadMeshes(cgltf_data* data)
{
std::vector<Mesh> result;
cgltf_node* nodes = data->nodes;
unsigned int nodeCount = (unsigned int)data->nodes_count;
for (unsigned int i = 0; i < nodeCount; ++i)
{
cgltf_node* node = &nodes[i];
if (node->mesh == 0 || node->skin == 0)
continue;
unsigned int numPrims = (unsigned int)node->mesh->primitives_count;
for (unsigned int j = 0; j < numPrims; ++j)
{
result.push_back(Mesh());
Mesh& mesh = result[result.size() - 1];
cgltf_primitive* primitive = &node->mesh->primitives[j];
unsigned int numAttributes = (unsigned int)primitive->attributes_count;
for (unsigned int k = 0; k < numAttributes; ++k)
{
cgltf_attribute* attribute = &primitive->attributes[k];
GLTFHelpers::MeshFromAttribute(mesh, *attribute, node->skin, nodes, nodeCount);
}
if (primitive->indices != 0)
{
unsigned int indexCount = (unsigned int)primitive->indices->count;
std::vector<unsigned int>& indices = mesh.GetIndices();
indices.resize(indexCount);
for (unsigned int k = 0; k < indexCount; ++k)
indices[k] = (unsigned int)cgltf_accessor_read_index(primitive->indices, k);
}
mesh.UpdateOpenGLBuffers();
}
}
return result;
}
namespace GLTFHelpers
{
void MeshFromAttribute(Mesh& outMesh, cgltf_attribute& attribute, cgltf_skin* skin, cgltf_node* nodes, unsigned int nodeCount)
{
cgltf_attribute_type attribType = attribute.type;
cgltf_accessor& accessor = *attribute.data;
unsigned int componentCount = 0;
if (accessor.type == cgltf_type_vec2)
componentCount = 2;
else if (accessor.type == cgltf_type_vec3)
componentCount = 3;
else if (accessor.type == cgltf_type_vec4)
componentCount = 4;
std::vector<float> values;
GetScalarValues(values, componentCount, accessor);
std::vector<vec3>& positions = outMesh.GetPosition();
std::vector<vec3>& normals = outMesh.GetNormal();
std::vector<vec2>& texCoords = outMesh.GetTexCoord();
std::vector<ivec4>& influences = outMesh.GetInfluences();
std::vector<vec4>& weights = outMesh.GetWeights();
for (unsigned int i = 0; i < (unsigned int)accessor.count; ++i)
{
int index = i * componentCount;
switch (attribType)
{
case cgltf_attribute_type_position:
positions.push_back(vec3(values[index + 0], values[index + 1], values[index + 2]));
break;
case cgltf_attribute_type_texcoord:
texCoords.push_back(vec2(values[index + 0], values[index + 1]));
break;
case cgltf_attribute_type_weights:
weights.push_back(vec4(values[index + 0], values[index + 1], values[index + 2], values[index + 3]));
break;
case cgltf_attribute_type_normal:
{
vec3 normal = vec3(values[index + 0], values[index + 1], values[index + 2]);
if (lenSq(normal) < 0.000001f)
normal = vec3(0, 1, 0);
normals.push_back(normalized(normal));
}
break;
case cgltf_attribute_type_joints:
{
ivec4 joints((int)(values[index + 0] + 0.5f),
(int)(values[index + 1] + 0.5f),
(int)(values[index + 2] + 0.5f),
(int)(values[index + 3] + 0.5f)
);
joints.x = std::max(0, GetNodeIndex(skin->joints[joints.x], nodes, nodeCount));
joints.y = std::max(0, GetNodeIndex(skin->joints[joints.y], nodes, nodeCount));
joints.z = std::max(0, GetNodeIndex(skin->joints[joints.z], nodes, nodeCount));
joints.w = std::max(0, GetNodeIndex(skin->joints[joints.w], nodes, nodeCount));
influences.push_back(joints);
}
break;
}
}
}
}
Implementing GPU skinning
GPU skinning其实就是把相关的矩阵信息,作为Uniform数组传给Vertex Shader了,这里创建一个skinned.vert :
#version 330 core
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
in vec3 position;
in vec3 normal;
in vec2 texCoord;
in vec4 weights;// 额外的顶点属性1
in ivec4 joints;// 额外的顶点属性2
// 两个Uniform数组
uniform mat4 pose[120]; // 代表parent joint的world trans
uniform mat4 invBindPose[120];//代表转换到parent joint的local space的offset矩阵
// 这里是重点,对于Skinned Mesh,其ModelSpace下的顶点坐标和法向量都需要重新计算
// 因为这个Mesh变化了
out vec4 newModelPos;
out vec3 newNorm;
out vec2 uv; // 注意,uv是不需要变化的(为啥?)
void main()
{
mat m0 = pose[joints.x] * invBindPose[joints.x] * weights.x;
mat m1 = pose[joints.y] * invBindPose[joints.y] * weights.y;
mat m2 = pose[joints.z] * invBindPose[joints.z] * weights.z;
mat m3 = pose[joints.w] * invBindPose[joints.w] * weights.w;
mat4 pallete = m0 + m1 + m2 + m3;
gl_Position = projection * view * model * pallete * position;// 算出屏幕上的样子
// 计算真实的ModelSpace下的坐标,供后续进行着色和光照计算
newModelPos = (model * pallete * vec4(position, 1.0f)).xyz;
newNorm = (model * pallete * vec4(normal, 0.0f)).xyz;
// 注意,顶点即使进行了Deformation,但它对应贴图的TexCoord是不改变的
uv = texCoord;
}
总结
本章重点:
- Rest Pose与Bind Pose的区别,一个是默认的Pose,一个是绑骨时的Pose
- Skeleton类的本质:两个Pose,或者一个Pose(当Rest Pose与Bind Pose相同时)
- Rigid Skinning与Smooth Skinning
- GPU Skinning和CPU Skinning
- 使用glTF读取Mesh
- 这节课的GitHub上的Sample里展示了使用GPU和CPU蒙皮的例子
附录
关于Bind Pose的Transform是Local还是World的问题
关于Bind Pose,它是个正常又不正常的Pose,说它正常是因为它也是Pose类的对象,本质就是两个vector,一个存储int,代表joints的hierarchy,一个存储LocalTransform,代表各个Joints在这个Pose下的Transform数据;说它不正常是因为它是一个特殊的Pose,人物在这个Pose的状态下,对应的mesh跟static mesh一模一样,不会产生任何deformation
所以说,Pose怎么存,它就是怎么存,这里就是存的LocalTransform
Rest Pose和Bind Pose的区别
参考:https://download.autodesk.com/us/fbx/sdkdocs/fbx_sdk_help/files/fbxsdkref/class_k_fbx_pose.html 参考:https://knowledge.autodesk.com/support/maya/learn-explore/caas/CloudHelp/cloudhelp/2020/ENU/Maya-CharacterAnimation/files/GUID-36808BCC-ACF9-4A9E-B0D8-B8F509FEC0D5-htm.html
The only pose that does not cause deformations to the skin is the bind pose.
The Bind Pose gives you the transformation of the nodes at the moment of the binding operation when no deformation occurs. The Rest Pose is a snapshot of a node transformation. A Rest Pose can be used to store the position of every node of a character at a certain point in time.
Bind Pose强调的是绑定师在绑定Mesh到Joints时所用的Pose,模型static mesh上的每个点,会在这个过程中,加上各自的skin信息,也就是它受哪些骨骼和各自权重的影响,在这种下binding下的Pose称之为Bind Pose,而Rest Pose表示的是一个pose,它表示人物模型的初始状态,它一般是A Pose或者T Pose,表示Default Pose,它不一定是BInd Pose,比如一个模型的Bind Pose可以是T Pose,但是他的Rest Pose,也就是模型的默认Pose,可以是A Pose
The Bind Pose holds the transformation (translation, rotation and scaling) matrix of all the nodes implied in a link deformation. This includes the geometry being deformed, the links deforming the geometry, and recursively all the ancestors nodes of the link. The Bind Pose gives you the transformation of the nodes at the moment of the binding operation when no deformation occurs.
The Rest Pose is a snapshot of a node transformation. A Rest Pose can be used to store the position of every node of a character at a certain point in time. This pose can then be used as a reference position for animation tasks, like editing walk cycles.
One difference between the two modes is in the validation performed before adding an item and the kind of matrix stored. In “Bind Pose” mode, the matrix is assumed to be defined in the global space, while in “Rest Pose” the type of the matrix may be specified by the caller. So local system matrices can be used.
关于glTF里的skin节点
如下图所示: 在这里面,node的hierarchy可以代表skeleton的hierarchy,每个node记录了mesh和skin俩对象的引用,skin用于基于skeleton pose来deform mesh,实现动画效果(The skin contains further information about how the mesh is deformed based on the current skeleton pose. )
如下所示,是一群nodes的数组:
"nodes": [
{
"name" :
"Skinned mesh node",
"mesh": 0
"skin": 0,
},
...
{
"name": "Torso",
"children":
[ 2, 3, 4, 5, 6 ]
"rotation": [...],
"scale": [ ...],
"translation": [...]
},
...
{
"name": "LegL",
"children": [ 7 ],
...
},
...
{
"name": "FootL",
...
},
...
],
这些Nodes组成下图所示的Hierarchy:
对应的Mesh在glTF里写作:
"meshes": [
{
"primitives": [
{
"attributes": {
"POSITION": 0,
"JOINTS_0": 1,
"WEIGHTS_0": 2
...
},
]
}
],
然后是skins的数据,可以看到是一个元素为矩阵的数组,这个数组代表Joints的Hierarchy,还有与每个矩阵对应的Joint的id:
"skins": [
{
"inverseBindMatrices": 12,
"joints": [ 1, 2, 3 ... ]
}
],
这里的inverseBindMatrices是个对Accessor的引用,里面为每个Joint存了个矩阵
浮点数存int
glTF文件里的所有data都是用float存的,而Skin数据里,顶点会受Joint的影响,会用int记录Joint的id,这里仍然存为了浮点数,只是在返回的时候是这么处理的:
int output = (int)(intput + 0.5f);
glTF文件里的Mesh和primitives
参考:https://github.com/KhronosGroup/glTF-Tutorials/blob/master/gltfTutorial/gltfTutorial_009_Meshes.md
Simple Meshes
{
"scene": 0,
"scenes" : [
{
"nodes" : [ 0, 1]
}
],
"nodes" : [
{
"mesh" : 0
},
{
"mesh" : 0,
"translation" : [ 1.0, 0.0, 0.0 ]
}
],
"meshes" : [
{
"primitives" : [ {
"attributes" : {
"POSITION" : 1,
"NORMAL" : 2
},
"indices" : 0
} ]
}
],
"buffers" : [
{
"uri" : "data:application/octet-stream;base64,AAABAAIAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8=",
"byteLength" : 80
}
],
"bufferViews" : [
{
"buffer" : 0,
"byteOffset" : 0,
"byteLength" : 6,
"target" : 34963
},
{
"buffer" : 0,
"byteOffset" : 8,
"byteLength" : 72,
"target" : 34962
}
],
"accessors" : [
{
"bufferView" : 0,
"byteOffset" : 0,
"componentType" : 5123,
"count" : 3,
"type" : "SCALAR",
"max" : [ 2 ],
"min" : [ 0 ]
},
{
"bufferView" : 1,
"byteOffset" : 0,
"componentType" : 5126,
"count" : 3,
"type" : "VEC3",
"max" : [ 1.0, 1.0, 0.0 ],
"min" : [ 0.0, 0.0, 0.0 ]
},
{
"bufferView" : 1,
"byteOffset" : 36,
"componentType" : 5126,
"count" : 3,
"type" : "VEC3",
"max" : [ 0.0, 0.0, 1.0 ],
"min" : [ 0.0, 0.0, 1.0 ]
}
],
"asset" : {
"version" : "2.0"
}
}
渲染出来如下图所示:
Mesh primitives
参考:https://github.com/KhronosGroup/glTF-Tutorials/blob/master/gltfTutorial/gltfTutorial_005_BuffersBufferViewsAccessors.md
前面提到的Mesh的primitives定义如下:
"meshes" : [
{
"primitives" : [ {
"attributes" : {
"POSITION" : 1,
"NORMAL" : 2
},
"indices" : 0
} ]
}
]
"accessors" : [
{
"bufferView" : 0,
"byteOffset" : 0,
"componentType" : 5123,
"count" : 3,
"type" : "SCALAR",
"max" : [ 2 ],
"min" : [ 0 ]
},
...
}
Each mesh contains an array of mesh.primitive objects. These mesh primitive objects are smaller parts or building blocks of a larger object. A mesh primitive summarizes all information about how the respective part of the object will be rendered.
这里的Mesh里的Primitive对象里包含了attributes和indices数组,感觉显然不是我之前理解的OpenGL里的Primitive,我看了下,这里有个Mesh.Primitive.mode,里面有POINTS、TRIANGLES这些枚举,跟我理解的OpenGL的Primitive类型是一样的。
glTF里的每个mesh由mesh.primitive对象组成,它这里的primitive,我感觉理解为subMesh可能更合适一点,比如上面的JSON文件,其实是一个Mesh,它描述了顶点属性和对应的顶点数组,如下图所示,其实只有一个点:
accessors与attribute的关系
参考:https://blog.csdn.net/alexhu2010q/article/details/121012638 参考:https://stackoverflow.com/questions/64546908/properties-of-the-stride-in-a-gltf-file 参考:https://www.khronos.org/files/gltf20-reference-guide.pdf
This accessor references a buffer view and the buffer view references a buffer.The buffer view describes what is in a buffer. If a buffer contains the information for glBufferData, then a buffer view contains some of the parameters to call glVertexAttribPointer. An accessor stores higher-level information, it describes the type of data you are dealing with
accessors (in glTF) are like vertex attributes in OpenGL/WebGL, and are allowed to interleave.
accessors用于描述顶点Buffer里的Layout
比如说一个meshes信息:
"meshes": [
{
"primitives": [
{
"mode": 4,
"indices": 0,
"attributes": {
"POSITION": 1,
"NORMAL": 2
},
"material": 2
}
]
}
],
The meshes may contain multiple mesh primitives. These refer to the geometry data that is required for rendering the mesh. Each mesh primitive has a rendering mode, which is a constant indicating whether it should be rendered as POINTS, LINES, or TRIANGLES. The primitive also refers to indices and the attributes of the vertices, using the indices of the accessors for this data. The material that should be used for rendering is also given, by the index of the material.
Skinning的过程中,TexCoord需要改变吗
这里是没改的,也就是说,即使Vertex上的每个点的位置进行了Deformation,其对应贴图的TexCoord也是不改变的
|