3D动画的骨骼与蒙皮原理,可参看《游戏引擎架构》11章中骨骼和蒙皮的相关章节。本文结合cocos2d源码分析骨骼与蒙皮的实现,因未经过严格验证,可能存在谬误,欢迎指正。
本文先介绍骨骼,下一篇介绍蒙皮。骨骼,又称关节,是由一根根刚性骨头(bone)以父子关系构建起来的,它实际上是不会被渲染出来的,真正渲染出来的是“皮肤”,皮肤就是以网格顶点加上贴图。关节的真正作用,是控制绑定在它上面的顶点的运动。
仍然以orc.c3t文件为例说明。在前面的文章《加载c3t文件》中说过,在c3t文件中的node字段下,有两种类型的节点数据,以"skeleton": false或"skeleton": true来区分。"skeleton": false对应的节点主要用于蒙皮,主要记录每个关节的对应的一个重要的变换矩阵,下一篇介绍。本文先讲"skeleton": true的节点,也就是关节节点,它记录了关节的父子关系。截了一段orc.c3t文件的内容如下:
{
"id": "Bip001",
"skeleton": true,
"transform": [ 0.257833, -0.087156, -0.962250, 0.000000, -0.965926, 0.000000, -0.258818, 0.000000, 0.022557, 0.996195, -0.084186, 0.000000, -0.495035, 3.925042, 0.780696, 1.000000],
"children": [
{
"id": "Bip001 Footsteps",
"skeleton": true,
"transform": [ 0.257833, -0.965926, 0.022557, 0.000000, 0.962250, 0.258818, 0.084186, 0.000000, -0.087156, 0.000000, 0.996195, 0.000000, 0.350998, -0.000000, -4.011928, 1.000000]
},
{
"id": "Bip001 Pelvis",
"skeleton": true,
"transform": [ 0.000330, 0.086823, 0.996224, 0.000000, 0.999971, -0.007595, 0.000330, 0.000000, 0.007595, 0.996195, -0.086823, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000],
"children": [
{
"id": "Bip001 Spine",
"skeleton": true,
"transform": [ 0.891669, 0.422665, -0.162118, 0.000000, -0.443728, 0.886952, -0.128147, 0.000000, 0.089627, 0.186200, 0.978415, 0.000000, 1.392380, -0.001139, -0.121047, 1.000000],
"children": [
{
"id": "Bip001 Spine1",
"skeleton": true,
"transform": [ 0.981055, 0.172988, -0.087216, 0.000000, -0.179690, 0.980789, -0.075921, 0.000000, 0.072407, 0.090155, 0.993292, 0.000000, 2.265768, -0.002554, 0.000198, 1.000000],
"children": [
{
//...省略
"skeleton": true的节点,他们之间是有父子关系的。在orc.c3t文件中,它的最上层的关节,id为Bip001,它有两个children,分别是“Bip001 Footsteps”和“Bip001 Pelvis”,“Bip001 Pelvis”下又有“Bip001 Spine”等等。我照着关节的父子关系简单画了一个图,纯粹是因为好玩,还挺萌的...
图中的箭头,是由父关节指向子关节。从Pelvis(盆骨)开始(它的父节点Bip001图中没画出来),往上是Spine(脊椎),Spine1,neck(脖子),head(头),脖子往左右两边是Clavicle(锁骨),UpperArm(上臂),Forearm(前臂),hand(手),Finger(手指)...,盆骨往下是Thigh(大腿),Calf(小腿),Foot(脚),Toe(脚趾)。
在《加载c3t文件》中分析过,c3t文件首先把骨骼节点加载到NodeDatas的成员skeleton,接着,在Sprite3D的初始化函数中,用它来初始化Sprite3D的_skeleton成员,代码如下:
bool Sprite3D::initFrom(const NodeDatas& nodeDatas, const MeshDatas& meshdatas, const MaterialDatas& materialdatas)
{
for(const auto& it : meshdatas.meshDatas) {
if(it) {
// Mesh* mesh = Mesh::create(*it);
// _meshes.pushBack(mesh);
auto meshvertex = MeshVertexData::create(*it); //创建网格顶点
_meshVertexDatas.pushBack(meshvertex);
}
}
_skeleton = Skeleton3D::create(nodeDatas.skeleton); //创建_skeleton
CC_SAFE_RETAIN(_skeleton);
auto size = nodeDatas.nodes.size();
for(const auto& it : nodeDatas.nodes) {
if(it) {
createNode(it, this, materialdatas, size == 1);
}
}
for(const auto& it : nodeDatas.skeleton) {
if(it) {
createAttachSprite3DNode(it,materialdatas);
}
}
genMaterial();
return true;
}
Skeleton3D::create函数如下:
Skeleton3D* Skeleton3D::create(const std::vector& skeletondata)
{
auto skeleton = new (std::nothrow) Skeleton3D();
for (const auto& it : skeletondata) { //遍历skeletondata
auto bone = skeleton->createBone3D(*it); //创建Bone3D
bone->resetPose(); //复位绑定姿势,就是把_oriPose赋给_local
skeleton->_rootBones.pushBack(bone);
}
skeleton->autorelease();
return skeleton;
}
createBone3D函数如下:
Bone3D* Skeleton3D::createBone3D(const NodeData& nodedata)
{
auto bone = Bone3D::create(nodedata.id);
for (const auto& it : nodedata.children) { //遍历nodedata的子节点
auto child = createBone3D(*it); //创建当前节点的子节点
bone->addChildBone(child); //添加到当前节点的_children数组
child->_parent = bone; //把子节点的_parent设为当前节点
}
_bones.pushBack(bone); //把当前节点放进_bones数组
bone->_oriPose = nodedata.transform; //transform对应的就是c3t文件中的transform字段,他表示模型最初始的姿势
return bone;
}
初始化完成之后,大概是这个样子:
现在我们再回到c3t文件,我们看到每个bone下,都有一个transform字段,它是一个4x4矩阵,例如
"children": [
{
"id": "Bip001 Spine1",
"skeleton": true,
"transform": [ 0.981055, 0.172988, -0.087216, 0.000000, -0.179690, 0.980789, -0.075921, 0.000000, 0.072407, 0.090155, 0.993292, 0.000000, 2.265768, -0.002554, 0.000198, 1.000000],
把这个矩阵法换个好看的格式:
3d数学中,4x4矩阵可以描述3d坐标下的缩放,旋转,切变,平移等运动,也就是仿射变换。放射变换就是线性变换加上平移运动,左上角的3x3矩阵可以描述缩放,旋转,切变等线性变换,第4列描述平移运动。像上面的矩阵,经它变换后,x轴正方向移动2.265768个单位,y轴负方向移动0.002554单位,z轴移动0.000198单位。如果仔细观察左上角的3x3矩阵,也就是去掉平移剩下来的线性变换,我们会发现每一行的模都是1,如0.981055,-0.179690,0.072407,这三个数的平方和是1。这表明这个变换没有缩放,只有旋转。也就是说这是一个刚体变换。
我们还知道,在3d场景中,变换坐标系中的点,与直接变换坐标系,实际上是一样的,只不过变换矩阵互为逆矩阵。在3d骨骼架构中,每一个关节都有一个transform矩阵,我们可以把这个矩阵理解为一个空间坐标系,它是相对于它的父关节的坐标系定义的。如上面的矩阵,子坐标系的x轴在父关节的空间坐标的朝向为(0.981055, 0.172988, -0.087216),y轴的朝向为(-0.179690, 0.980789, -0.075921),z轴的朝向为(0.072407, 0.090155, 0.993292),子坐标系的原点相对于父坐标系平移了(2.265768, -0.002554, 0.000198)。
具体的原理可以参考《游戏引擎架构》11章骨骼相关说明,我也简单画了个图:
由图所示,最大的那坐标系是世界坐标系,rootbone是根关节的坐标系(叫它M1),它是在世界坐标系下定义的,bone2是rootbone的子关节,它的坐标系(叫它M2)是在rootbone坐标系下定义的,bone3是bone2的子关节,它的坐标系(叫它M3)是在bone2坐标系下定义的。如果我们想求bone3坐标系下的点在世界坐标系下的位置,我们只需要对这个点的向量乘以M3 * M2 * M1.