【计算机动画】蒙皮实验

最近学习计算机动画,看书千万遍都不如动手做一遍,实现了一下完整的人物蒙皮,遇到一些问题,现在把完整的实现过程记一下。

理论在前一篇已经讲完了,前面也实现了基本的关节动画,现在要实现的是,使用3Dmax建立一个完整的模型,然后使用它来进行蒙皮。

我的基础工具写的不是很完善,为了不分心去写其他的东西,就用了一些很可笑的数据储存方式和各种硬编码,见怪不怪,以后空了去整理。

我定义了一个结构体叫做Boxman,里面存放的东西包括:

  • 关节树
  • 关节旋转矩阵
  • mesh引用
  • 存放各个关节到世界变换的矩阵
  • 硬编码的偏置矩阵

我们在初始化的时候就将mesh传递给Boxman,mesh包含的数据包括哪些点接受哪些骨骼的影响,结合预先硬编码的偏置矩阵就可以使用mesh对骨骼进行蒙皮了。

我用多叉树存放了一个人的骨骼。树的结构上篇已经画了,就不多说。树的节点定义如下:

struct Node
{
   //各个关节的矩阵 
    vmath::mat4 mat;
    //关节标识
    int bn;
    //子节点,这里最多也就四个
    Node* next[4];
    //构造函数
    Node()
    {
        bn = 0;
        next[0] = next[1] = next[2] = next[3] = 0;
    }
};

Boxman的定义如下:

struct Boxman
{
       //各个关节的矩阵,元素0是偏移矩阵,元素1是旋转矩阵,这个矩阵会实时变化
    vmath::mat4 root[2];
    vmath::mat4 left_leg_up[2];
    vmath::mat4 left_leg_down[2];
    vmath::mat4 right_leg_up[2];
    vmath::mat4 right_leg_down[2];
    vmath::mat4 body_middle[2];
    vmath::mat4 body_up[2];
    vmath::mat4 left_arm_up[2];
    vmath::mat4 left_arm_down[2];
    vmath::mat4 right_arm_up[2];
    vmath::mat4 right_arm_down[2];
        //mesh引用
    WIPModel3D * model_ref;
       //骨骼层次结构
    Node* hroot;
        //到世界坐标的最终矩阵
    vmath::mat4 mats[11];
}

在初始化的时候就初始化那些和绑定有关的数据,这里说的绑定数据就是mesh和骨骼相互关联的数据。一般在绑定骨骼的时候,动画师会把模型作为一个T姿态,然后将骨骼一一绑定并赋予权重,这个时候其实就是用来确定偏移矩阵、骨骼影响、权重等等绑定数据的(恩,至少我是这么理解的)。

Boxman的初始化代码如下:

Boxman(WIPModel3D * model_ref)
    {
        //初始化所有的最终结果矩阵为单位矩阵,因为之后会将矩阵注意乘上去
        for(int i=0;i<11;i++)
        {
            mats[i] = vmath::mat4::identity();
        }

        //mesh引用
        this->model_ref = model_ref;
        //初始化偏移矩阵,偏移矩阵是事先硬编码设定好的
        root[0] = vmath::translate(0.f,-40.f,0.f);
        left_leg_up[0] = vmath::translate(3.75f,-40.f,0.f);
        left_leg_down[0] = vmath::translate(3.75f,-20.f,0.f);
        right_leg_up[0] = vmath::translate(-3.75f,-40.f,0.f);
        right_leg_down[0] = vmath::translate(-3.75f,-20.f,0.f);
        body_middle[0] = vmath::translate(0.f,-55.f,0.f);
        body_up[0] = vmath::translate(0.f,-70.f,0.f);
        left_arm_up[0] = vmath::translate(10.f,-70.f,0.f);
        left_arm_down[0] = vmath::translate(10.f,-55.f,0.f);
        right_arm_up[0] = vmath::translate(-10.f,-70.f,0.f);
        right_arm_down[0] = vmath::translate(-10.f,-55.f,0.f);
        //初始化旋转矩阵
        root[1] = vmath::rotate(0.f,0.f,1.f,0.f);
        left_leg_up[1] = vmath::rotate(0.f,0.f,1.f,0.f);
        left_leg_down[1] = vmath::rotate(0.f,0.f,1.f,0.f);
        right_leg_up[1] = vmath::rotate(0.f,0.f,1.f,0.f);
        right_leg_down[1] = vmath::rotate(0.f,0.f,1.f,0.f);
        body_middle[1] = vmath::rotate(0.f,0.f,1.f,0.f);
        body_up[1] = vmath::rotate(0.f,0.f,1.f,0.f);
        left_arm_up[1] = vmath::rotate(0.f,0.f,1.f,0.f);
        left_arm_down[1] = vmath::rotate(0.f,0.f,1.f,0.f);
        right_arm_up[1] = vmath::rotate(0.f,0.f,1.f,0.f);
        right_arm_down[1] = vmath::rotate(0.f,0.f,1.f,0.f);
  
        //创建骨骼层次树,并赋予关节矩阵和标识
        Node* mroot = new Node;
        mroot->mat = vmath::translate(0.f,40.f,0.f)*root[1];
        mroot->bn = 0;

        Node* mleft_leg_up = new Node;
        mleft_leg_up->mat = vmath::translate(-3.75f,0.f,0.f)*left_leg_up[1];
        mleft_leg_up->bn = 1;

        Node* mleft_leg_down = new Node;
        mleft_leg_down->mat = vmath::translate(0.f,-20.f,0.f)*left_leg_down[1];
        mleft_leg_down->bn = 2;

        Node* mright_leg_up = new Node;
        mright_leg_up->mat = vmath::translate(3.75f,0.f,0.f)*right_leg_up[1];
        mright_leg_up->bn = 3;

        Node* mright_leg_down = new Node;
        mright_leg_down->mat = vmath::translate(0.f,-20.f,0.f)*right_leg_down[1];
        mright_leg_down->bn = 4;

        Node* mbody_middle = new Node;
        mbody_middle->mat = vmath::translate(0.f,15.f,0.f)*body_middle[1];
        mbody_middle->bn = 5;

        Node* mbody_up = new Node;
        mbody_up->mat = vmath::translate(0.f,15.f,0.f)*body_up[1];
        mbody_up->bn = 6;

        Node* mleft_arm_up = new Node;
        mleft_arm_up->mat = vmath::translate(-10.f,15.f,0.f)*left_arm_up[1];
        mleft_arm_up->bn = 7;

        Node* mleft_arm_down = new Node;
        mleft_arm_down->mat = vmath::translate(0.f,-15.f,0.f)*left_arm_down[1];
        mleft_arm_down->bn = 8;

        Node* mright_arm_up = new Node;
        mright_arm_up->mat = vmath::translate(10.f,15.f,0.f)*right_arm_up[1];
        mright_arm_up->bn = 9;

        Node* mright_arm_down = new Node;
        mright_arm_down->mat = vmath::translate(0.f,-15.f,0.f)*right_arm_down[1];
        mright_arm_down->bn = 10;

        mroot->next[0] = mleft_leg_up;
        mroot->next[1] = mright_leg_up;
        mroot->next[2] = mbody_middle;

        mleft_leg_up->next[0] = mleft_leg_down;
        mright_leg_up->next[0] = mright_leg_down;

        mbody_middle->next[0] = mleft_arm_up;
        mbody_middle->next[1] = mright_arm_up;
        mbody_middle->next[2] = mbody_up;

        mleft_arm_up->next[0] = mleft_arm_down;
        mright_arm_up->next[0] = mright_arm_down;

        hroot = mroot;
    }

初始化好这个Boxman就可以开始考虑计算的问题了,我比较傻缺,所以用的都是写傻缺方法,关节旋转我就是直接对关节的矩阵乘以一个旋转矩阵,然后递归的计算骨骼层次树把所有的矩阵实时计算存到Boxman::mats[11]里面去。然后在draw的时候直接把这些矩阵传到shader里面,进行对应的计算就好了。

遍历层次树的代码很简单,由于我要同时计算新的旋转矩阵,所以有点长:

void push(Node* n,vmath::mat4 m)
    {
        if(n)
        {
            switch (n->bn)
            {
            case 0:
                n->mat *= root[1];
                break;
            case 1:
                n->mat*=left_leg_up[1];
                break;
            case 2:
                n->mat*=left_leg_down[1];
                break;
            case 3:
                n->mat*=right_leg_up[1];
                break;
            case 4:
                n->mat*=right_leg_down[1];
                break;
            case 5:
                n->mat*=body_middle[1];
                break;
            case 6:
                n->mat *= body_up[1];
                break;
            case 7:
                n->mat*=left_arm_up[1];
                break;
            case 8:
                n->mat*=left_arm_down[1];
                break;
            case 9:
                n->mat*=right_arm_up[1];
                break;
            case 10:
                n->mat*=right_arm_down[1];
                break;
            default:
                break;
            }

            mats[n->bn] = m*n->mat*mats[n->bn];
        }
        else
        {
            return;
        }
        for(int i=0;i<4;i++)
        {
            if(n->next[i])
            {
                push(n->next[i],mats[n->bn]); 
            }
        }
    }

    void calc(Node* node)
    {
        push(node,vmath::mat4::identity());
    }    

draw就简单了,无非就是把一堆矩阵全部传入shader算就行了,比较傻缺:

void draw()
    {
            boxman_shader.begin();
            boxman_shader.set_uniform_matrix("m00",root[0]);
            boxman_shader.set_uniform_matrix("m10",left_leg_up[0]);
            boxman_shader.set_uniform_matrix("m20",left_leg_down[0]);
            boxman_shader.set_uniform_matrix("m30",right_leg_up[0]);
            boxman_shader.set_uniform_matrix("m40",right_leg_down[0]);
            boxman_shader.set_uniform_matrix("m50",body_middle[0]);
            boxman_shader.set_uniform_matrix("m60",body_up[0]);
            boxman_shader.set_uniform_matrix("m70",left_arm_up[0]);
            boxman_shader.set_uniform_matrix("m80",left_arm_down[0]);
            boxman_shader.set_uniform_matrix("m90",right_arm_up[0]);
            boxman_shader.set_uniform_matrix("m100",right_arm_down[0]);
            boxman_shader.set_uniform_matrix("m0",mats[0]);
            boxman_shader.set_uniform_matrix("m1",mats[1]);
            boxman_shader.set_uniform_matrix("m2",mats[2]);
            boxman_shader.set_uniform_matrix("m3",mats[3]);
            boxman_shader.set_uniform_matrix("m4",mats[4]);
            boxman_shader.set_uniform_matrix("m5",mats[5]);
            boxman_shader.set_uniform_matrix("m6",mats[6]);
            boxman_shader.set_uniform_matrix("m7",mats[7]);
            boxman_shader.set_uniform_matrix("m8",mats[8]);
            boxman_shader.set_uniform_matrix("m9",mats[9]);
            boxman_shader.set_uniform_matrix("m11",mats[10]);
            boxman_shader.set_uniform_matrix("mv_matrix",mv_matrix);
            boxman_shader.set_uniform_matrix("proj_matrix",proj_matrix);
            boxman_shader.set_uniform_i("vn",0);
            model_ref->renderer(GL_TRIANGLES);
            glPointSize(20);
            glDrawArrays(GL_POINTS,0,1);
            boxman_shader.end();
            glDisable(GL_CULL_FACE);
            glPolygonMode(GL_FRONT_AND_BACK   ,GL_LINE   );
            boxman_shader.begin();
            boxman_shader.set_uniform_matrix("m00",root[0]);
            boxman_shader.set_uniform_matrix("m10",left_leg_up[0]);
            boxman_shader.set_uniform_matrix("m20",left_leg_down[0]);
            boxman_shader.set_uniform_matrix("m30",right_leg_up[0]);
            boxman_shader.set_uniform_matrix("m40",right_leg_down[0]);
            boxman_shader.set_uniform_matrix("m50",body_middle[0]);
            boxman_shader.set_uniform_matrix("m60",body_up[0]);
            boxman_shader.set_uniform_matrix("m70",left_arm_up[0]);
            boxman_shader.set_uniform_matrix("m80",left_arm_down[0]);
            boxman_shader.set_uniform_matrix("m90",right_arm_up[0]);
            boxman_shader.set_uniform_matrix("m100",right_arm_down[0]);
            boxman_shader.set_uniform_matrix("m0",mats[0]);
            boxman_shader.set_uniform_matrix("m1",mats[1]);
            boxman_shader.set_uniform_matrix("m2",mats[2]);
            boxman_shader.set_uniform_matrix("m3",mats[3]);
            boxman_shader.set_uniform_matrix("m4",mats[4]);
            boxman_shader.set_uniform_matrix("m5",mats[5]);
            boxman_shader.set_uniform_matrix("m6",mats[6]);
            boxman_shader.set_uniform_matrix("m7",mats[7]);
            boxman_shader.set_uniform_matrix("m8",mats[8]);
            boxman_shader.set_uniform_matrix("m9",mats[9]);
            boxman_shader.set_uniform_matrix("m11",mats[10]);
            boxman_shader.set_uniform_matrix("mv_matrix",mv_matrix);
            boxman_shader.set_uniform_matrix("proj_matrix",proj_matrix);
            boxman_shader.set_uniform_i("vn",1);
            model_ref->renderer(GL_TRIANGLES);
            glPointSize(20);
            glDrawArrays(GL_POINTS,0,1);
            boxman_shader.end();
            glDisable(GL_CULL_FACE);
            glPolygonMode(GL_FRONT_AND_BACK   ,GL_FILL   );
            for(int i=0;i<11;i++)
        {
            mats[i] = vmath::mat4::identity();
        }
            //重置所有旋转矩阵以便下一帧更新
            body_up[1] = vmath::mat4::identity();
            root[1] = vmath::mat4::identity();
            left_leg_up[1] = vmath::mat4::identity();
            left_leg_down[1] = vmath::mat4::identity();
            right_leg_up[1] = vmath::mat4::identity();
            right_leg_down[1] = vmath::mat4::identity();
            body_middle[1] = vmath::mat4::identity();

            left_arm_up[1] = vmath::mat4::identity();
            left_arm_down[1] = vmath::mat4::identity();
            right_arm_up[1] = vmath::mat4::identity();
            right_arm_down[1] = vmath::mat4::identity();
    }

这里有两个drawcall的原因是,要绘制一次法线,而法线的显示模式和模型的显示模式不一样。

在运行的时候操作关节直接更新旋转矩阵就行了,下面就是更新头部旋转的接口:

    void rotate_head(float degree)
    {
        body_up[1] = vmath::rotate(degree,0.f,1.f,0.f);
    }

Shader的实现就是很简单的乘以矩阵做变换就好,因为没有权重,要加进去又比较困难,我又做了一个傻缺决定,直接把顶点的属于哪个骨骼控制直接写到顶点数据的x上,原因是我的模型数据建的很精确,所有的坐标数据小数点3位之后全是0.于是顶点数据变成了这样:

v  -5.5060 70.0000 -5.5000
v  -5.5060 81.0000 -5.5000
v  5.5060 81.0000 -5.5000
v  5.5060 70.0000 -5.5000
v  -5.5060 70.0000 5.5000
v  5.5060 70.0000 5.5000
v  5.5060 81.0000 5.5000
v  -5.5060 81.0000 5.5000

那个060就代表这个顶点会被骨骼6影响……这个数据在shader中取出来在减去就好了……不过不修正形状变化也不大的>_<

shader代码如下:

#version 410 core
layout (location = 1) in vec3 a_normal;
layout (location = 2) in vec4 a_position;
layout (location = 3) in vec4 a_weight;
//一大堆uniform矩阵
uniform int bone;
uniform int blend;

out vec3 normal;
out vec3 onormal;
out vec4 ccc;

vec4 red = vec4(1.0,0.0,0.0,1.0);

void main()
{
    vec4 new_p = a_position;
    int l = int(a_position.x*10);
        
    float x = float(a_position.x*10-l);
    
    float temp ;
       //加0.5修正精度丢失造成的错误
    if(x*100>=0)
        temp = x*100+0.5;
    else
        temp = x*100-0.5;
    int xx = abs(int(temp));

    new_p.x = float(a_position.x - x);
        
    switch(xx)
    {
        case 0:
        
        gl_Position = mv_matrix*m0*m00*a_position;
        break;
        case 1:
        
        gl_Position = mv_matrix*m1*m10*a_position;
        break;
        case 2:
        
        gl_Position = mv_matrix*m2*m20*a_position;
        break;
        case 3:
        
        gl_Position = mv_matrix*m3*m30*a_position;
        break;
        case 4:
        
        gl_Position = mv_matrix*m4*m40*a_position;
        break;
        case 5:
        
        gl_Position = mv_matrix*m5*m50*a_position;
        break;
        case 6:
        
        gl_Position = mv_matrix*m6*m60*a_position;
        break;
        case 7:
        
        gl_Position = mv_matrix*m7*m70*a_position;
        break;
        case 8:
        
        gl_Position = mv_matrix*m8*m80*a_position;
        break;
        case 9:
        
        gl_Position = mv_matrix*m9*m90*a_position;
        break;
        case 10:
        gl_Position = mv_matrix*m11*m100*a_position;
        break;
        
    }
    
    normal = normalize(mat3(mv_matrix)*a_normal);
    onormal = a_normal;
}

这里没有透视矩阵,因为我使用了Geometry Shader来生成法线,透视矩阵放到那里面去了。

#version 410 core

layout (triangles) in;
layout (triangle_strip,max_vertices = 3) out;
uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
uniform int vn;
in vec3 onormal[];
in vec3 normal[];
out vec3 normalo;
void main()
{
    vec4 position;
    int i;
    
    
    for(i=0;i<gl_in.length();i++)
    {
        if(vn==0)
        {
            vec3 n = normal[i];
            normalo = n;
            gl_Position = proj_matrix*gl_in[i].gl_Position;
            EmitVertex();
        }
        else
        {
            vec3 on = onormal[i];
            vec3 n = normal[i];
            normalo = n;
            gl_Position = proj_matrix*gl_in[i].gl_Position;
            EmitVertex();
            EmitVertex();
            position = gl_in[i].gl_Position + 6*vec4(n,0.f);
            gl_Position = proj_matrix*position;
            EmitVertex();
            EndPrimitive();
        }
    }
    EndPrimitive();
}

最后在主程序中,每一帧calc一次,再draw一次就可以基本实现功能了。

最后上个图吧(红色那些是面法线):

我就没有去设计如何对每个顶点的权值,要这么做,我只能手动去设置,但是顶点实在太多时间有限就没有加入顶点混合功能。我另外使用一个顶点和关节都很少的模型写了顶点混合,原理都是你一样的,只要合适的加入权重,在shader中直接计算就好了:

没有顶点混合:

混合后:

这些动画姿势都是手动旋转出来的,关于动画数据怎么回事,目前还不是很懂。

原文地址:https://www.cnblogs.com/wubugui/p/4397348.html