3D动画的骨骼与蒙皮原理,可参看《游戏引擎架构》11章中骨骼和蒙皮的相关章节。本文结合cocos2d源码分析骨骼与蒙皮的实现,因未经过严格验证,可能存在谬误,欢迎指正。
上一篇文章将的3D骨骼,实际上是不会被渲染出来的,真正渲染出来的是“皮肤”。通过把皮肤绑定到骨骼的过程叫做蒙皮。皮肤实际上就是用网格顶点和纹理渲染出来我们看到的人物的外表,皮肤(网格顶点)的运动是受到它所绑定的骨骼的运动的影响的,一个顶点可以绑定一个或多个骨骼,每个骨骼都有控制该顶点的权重。本文分析一下cocos2d如何实现这一过程。
我们先看回orc.c3t文件,在《加载c3t文件》中说过,c3t文件的node字段下,有两种节点,以"skeleton": false和"skeleton": true区分,在《Sprite3D之骨骼》一文中分析的是"skeleton": true,也就是骨骼节点,它下面的transform字段实际上是该子关节坐标系在它的父关节坐标系的相对位置,而"skeleton": false中的节点下的transform字段,实际上是绑定姿势下该关节坐标系到世界坐标系的逆矩阵,摘一段c3t文件内容如下:
"nodes": [
{
"id": "Object005",
"skeleton": false,
"transform": [ 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, -1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, -0.142358, 9.629485, 1.274491, 1.000000],
"parts": [
{
"meshpartid": "shape1_part1",
"materialid": "base",
"bones": [
{
"node": "Bip001 Spine1",
"transform": [-0.000000, 0.000002, -1.000000, 0.000000, 0.087949, 0.996125, 0.000002, 0.000000, 0.996125, -0.087949, -0.000000, 0.000000, 1.297510, -1.435822, -0.000002, 1.000000]
},
{
"node": "Bip001 L UpperArm",
"transform": [-0.650007, -0.056867, -0.757798, 0.000000, 0.011354, -0.997812, 0.065139, 0.000000, -0.759844, 0.033737, 0.649230, 0.000000, -1.529892, 1.318230, -4.160798, 1.000000]
},
这里有个概念,什么叫绑定姿势,简单的说,绑定姿势就是不用任何蒙皮和骨骼技术来渲染的点,也就是我们对c3t文件中的顶点和纹理坐标数据直接从到openGL中,不做任何变换的情况下渲染出来的画面。如我把顶点着色器cc3D_PositionTex_vert里面的骨骼变换坐标注释掉,如下:
//省略....
void main()
{
// vec4 position = getPosition(); //使用蒙皮骨骼技术变换坐标点,先注释
// gl_Position = CC_MVPMatrix * position;
vec4 position = vec4(a_position, 1.0);
gl_Position = CC_MVPMatrix * position; //直接采用输入的坐标点
TextureCoordOut = a_texCoord;
TextureCoordOut.y = 1.0 - TextureCoordOut.y;
}
渲染出来的画面是:
这就是绑定姿势,而当我们想让它做出各种各样的动作时,则需要使用骨骼蒙皮技术,骨骼蒙皮技术简单说就是,把网格顶点“绑定”到一个或多个关节上,人物做动作时,则改变关节的位置,而网格顶点跟随着关节运动。
而一开始的顶点数据都是在模型空间中的(但在cocos2d中它直接就是世界坐标系),如果我们要把顶点绑定到骨骼上,那么,应该先把这些顶点转换到绑定姿势下的骨骼坐标系(也叫关节空间),这个转换矩阵,也就是上面截图的c3t文件的各个transform矩阵,它也是绑定姿势下的关节空间到模型空间的逆矩阵。然后,当要做运动时,就移动关节空间,而顶点会跟随关节空间移动。具体原理可以详细阅读《游戏引擎架构》书中11.5.2节(蒙皮的数学),截一段书上的推导,非常形象:
因此,每个骨骼下都有一个蒙皮矩阵,它由有两个矩阵相乘得到,一个是上一节中提到的,表示子关节空间在父关节空间的相对位置的矩阵,该矩阵主要用来将关节空间变换回模型空间,也就是图中的Cj->M;另一个是在绑定姿势下的关节空间到模型空间的逆矩阵(也就是,在模型空间中的点和它相乘后,就变换为关节空间的点),该矩阵主要用来将模型空间中的顶点变换到关节空间,也就是图中的(Bj->M)-1。它们从c3t文件中加载后,最终都保存在Bone3D类的成员中,Bone3D对应着一个关节,它的部分成员如下:
class Bone3D
{
Mat4 _invBindPose; //在绑定姿势下的关节空间到模型空间的逆矩阵,也就是上面的(Bj->M)-1,它在设计模型时就计算好并且在运行后是不会改变的,来自c3t文件的("skeleton": false)的node节点的transfrom
Mat4 _oriPose; //初始姿势下它自身坐标系在父关节坐标系的相对位置的矩阵,来自c3t文件的("skeleton": true)的node节点的transfrom
Bone3D* _parent; //父关节
Mat4 _world; //来自父节点的_local乘以自身的_local,它实际上就是上面的Cj->M
Mat4 _local; //初始化时来自_oriPose,通过在运行时动态更新它的值来实现骨骼运动
}
之前提到,一个网格顶点可以绑定到一个或多个关节(一般最多是4个),对应的四个蒙皮矩阵组成的数组称为矩阵调色板,当要渲染一个网格顶点时,矩阵调色板便要传送至着色器中。我们先看下c3t文件中传入到着色器的顶点属性,下面是c3t文件的片段:
{
"version": "0.3",
"id": "",
"meshes": [
{
"attributes": [{
"size": 3,
"type": "GL_FLOAT",
"attribute": "VERTEX_ATTRIB_POSITION" //顶点位置
}, {
"size": 3,
"type": "GL_FLOAT",
"attribute": "VERTEX_ATTRIB_NORMAL" //法线
}, {
"size": 2,
"type": "GL_FLOAT",
"attribute": "VERTEX_ATTRIB_TEX_COORD" //纹理坐标
}, {
"size": 4,
"type": "GL_FLOAT",
"attribute": "VERTEX_ATTRIB_BLEND_WEIGHT" //绑定的关节的权重
}, {
"size": 4,
"type": "GL_FLOAT",
"attribute": "VERTEX_ATTRIB_BLEND_INDEX" //绑定关节的索引
}],
"vertices": [
-4.087269, -0.284269, 2.467412, -0.182764, -0.799652, 0.571974, 0.309707, 0.734820, 0.500000, 0.500000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000,
一个顶点的内存图如下:
我们可以看下着色器代码:
const char* cc3D_SkinPositionTex_vert = R"(
attribute vec3 a_position;
attribute vec4 a_blendWeight;
attribute vec4 a_blendIndex;
attribute vec2 a_texCoord;
const int SKINNING_JOINT_COUNT = 60;
// Uniforms
uniform vec4 u_matrixPalette[SKINNING_JOINT_COUNT * 3]; //矩阵调色板
// Varyings
varying vec2 TextureCoordOut;
vec4 getPosition()
{
//关节0的权重
float blendWeight = a_blendWeight[0];
//关节0索引
int matrixIndex = int (a_blendIndex[0]) * 3;
//u_matrixPalette为关节0的蒙皮矩阵,blendWeight为关节0的权重
vec4 matrixPalette1 = u_matrixPalette[matrixIndex] * blendWeight;
vec4 matrixPalette2 = u_matrixPalette[matrixIndex + 1] * blendWeight;
vec4 matrixPalette3 = u_matrixPalette[matrixIndex + 2] * blendWeight;
//关节1的权重
blendWeight = a_blendWeight[1];
if (blendWeight > 0.0)
{
//关节1索引
matrixIndex = int(a_blendIndex[1]) * 3;
//u_matrixPalette为关节1的蒙皮矩阵,blendWeight为关节1的权重
matrixPalette1 += u_matrixPalette[matrixIndex] * blendWeight;
matrixPalette2 += u_matrixPalette[matrixIndex + 1] * blendWeight;
matrixPalette3 += u_matrixPalette[matrixIndex + 2] * blendWeight;
//关节2的权重
blendWeight = a_blendWeight[2];
if (blendWeight > 0.0)
{
//关节2索引
matrixIndex = int(a_blendIndex[2]) * 3;
//u_matrixPalette为关节2的蒙皮矩阵,blendWeight为关节2的权重
matrixPalette1 += u_matrixPalette[matrixIndex] * blendWeight;
matrixPalette2 += u_matrixPalette[matrixIndex + 1] * blendWeight;
matrixPalette3 += u_matrixPalette[matrixIndex + 2] * blendWeight;
//关节3的权重
blendWeight = a_blendWeight[3];
if (blendWeight > 0.0)
{
//关节3索引
matrixIndex = int(a_blendIndex[3]) * 3;
//u_matrixPalette为关节3的蒙皮矩阵,blendWeight为关节3的权重
matrixPalette1 += u_matrixPalette[matrixIndex] * blendWeight;
matrixPalette2 += u_matrixPalette[matrixIndex + 1] * blendWeight;
matrixPalette3 += u_matrixPalette[matrixIndex + 2] * blendWeight;
}
}
}
vec4 _skinnedPosition;
vec4 position = vec4(a_position, 1.0);
//得到最终的蒙皮位置
_skinnedPosition.x = dot(position, matrixPalette1);
_skinnedPosition.y = dot(position, matrixPalette2);
_skinnedPosition.z = dot(position, matrixPalette3);
_skinnedPosition.w = position.w;
return _skinnedPosition;
}
void main()
{
vec4 position = getPosition();
gl_Position = CC_MVPMatrix * position;
TextureCoordOut = a_texCoord;
TextureCoordOut.y = 1.0 - TextureCoordOut.y;
}
)";
矩阵调色板是通过uniform统一变量传入的,它传入的代码为:
void Mesh::draw(Renderer* renderer, float globalZOrder, const Mat4& transform, uint32_t flags, unsigned int lightMask, const Vec4& color, bool forceDepthWrite)
{
//省略...
if (_skin)
programState->setUniformVec4v("u_matrixPalette", (GLsizei)_skin->getMatrixPaletteSize(), _skin->getMatrixPalette());
//省略...
}
getMatrixPalette()函数代码为:
Vec4* MeshSkin::getMatrixPalette()
{
if (_matrixPalette == nullptr)
{
_matrixPalette = new (std::nothrow) Vec4[_skinBones.size() * PALETTE_ROWS];
}
int i = 0, paletteIndex = 0;
static Mat4 t;
for (auto it : _skinBones ) //遍历所有关节
{
//it->getWorldMat()实际就是该关节空间转换到世界空间的矩阵,也就是上面的Cj->M,invBindPoses[i++]就是在绑定姿势下的关节空间到模型空间的逆矩阵,也就是上面的(Bj->M)-1。
Mat4::multiply(it->getWorldMat(), _invBindPoses[i++], &t);
_matrixPalette[paletteIndex++].set(t.m[0], t.m[4], t.m[8], t.m[12]);
_matrixPalette[paletteIndex++].set(t.m[1], t.m[5], t.m[9], t.m[13]);
_matrixPalette[paletteIndex++].set(t.m[2], t.m[6], t.m[10], t.m[14]);
}
return _matrixPalette;
}
最后说下,it->getWorldMat()得到的结果是根据关节运动而变化的,从而影响网格顶点最终的位置,让整个物体动起来,这个在后面再分析。
网友评论