美文网首页
cocos2d源码分析(十四):Sprite3D之蒙皮

cocos2d源码分析(十四):Sprite3D之蒙皮

作者: 奔向火星005 | 来源:发表于2019-02-22 21:40 被阅读0次

    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()得到的结果是根据关节运动而变化的,从而影响网格顶点最终的位置,让整个物体动起来,这个在后面再分析。

    相关文章

      网友评论

          本文标题:cocos2d源码分析(十四):Sprite3D之蒙皮

          本文链接:https://www.haomeiwen.com/subject/ypvajqtx.html