cocos2d源码分析(十三):Sprite3D之骨骼

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;
}

初始化完成之后,大概是这个样子:


cocos2d源码分析(十三):Sprite3D之骨骼_第1张图片

现在我们再回到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章骨骼相关说明,我也简单画了个图:

cocos2d源码分析(十三):Sprite3D之骨骼_第2张图片

由图所示,最大的那坐标系是世界坐标系,rootbone是根关节的坐标系(叫它M1),它是在世界坐标系下定义的,bone2是rootbone的子关节,它的坐标系(叫它M2)是在rootbone坐标系下定义的,bone3是bone2的子关节,它的坐标系(叫它M3)是在bone2坐标系下定义的。如果我们想求bone3坐标系下的点在世界坐标系下的位置,我们只需要对这个点的向量乘以M3 * M2 * M1.

你可能感兴趣的:(cocos2d源码分析(十三):Sprite3D之骨骼)