【计算机动画】蒙皮实验

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

理论在前一篇已经讲完了,前面也实现了基本的关节动画,现在要实现的是,使用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中直接计算就好了:

没有顶点混合:

混合后:

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

时间: 2024-11-07 04:31:45

【计算机动画】蒙皮实验的相关文章

【计算机动画】蒙皮笔记

最近刚开始接触计算机动画,一片空白,尝试做了一下3D蒙皮,发现数学真是差得难以想象,矩阵的什么的全部忘记了,真是接下来该恶补一下基础了…… 在这里把遇到的几个问题记录一下. 主要参考书目:<计算机动画算法与技术><3D游戏>(英国人写的书已经绝版了)<3D游戏引擎设计——实时计算机图形学应用方法> 关节就是一个变换矩阵,在层次储存中,一般会从根节点开始一层层的储存关节矩阵: 所以每个关节储存的矩阵都是建立根关节之上的. 以3个关节为例,转换如下: —(T0世界矩阵)—&

如何使用GNS3和Cisco IOU搭建路由交换实验-IOU篇

前面介绍了GNS3的概念,安装,配置和使用,本篇将介绍怎么利用GNS3配置IOU从而实现使用GNS3和Cisco IOU搭建路由交换实验. 由于本篇篇幅较长,所以先过一下大纲: 1. IOU模拟环境介绍 2. IOU软件环境的准备 3. VMware虚拟机的安装.导入和配置 4. IOU镜像的上传 5. GNS3的配置 6. IOU模拟环境的实现 IOU模拟环境介绍 IOU即IOS running in Unix,最初是由思科内部人员开发来测试IOS的平台,后来流传到互联网经网友改进有了后来的W

广外第二周的实验报告来这里分享一下

虽然有很多还不知道怎么改,可能也有很多测试不到的地方,但是通过这个星期我好歹了解了好多东西啊 实验报告 通过本次学习,能了解到VC6.0.是用于将已生成的C++语言源程序代码转换为计算机能读懂的目标代码,计算机用的均是二进制代码.编辑完成后它首先生成扩展名为obj的文件(程序编译后的二进制文件),若想进行之后的链接.运行过程,必须不断修改源程序文件至完全正确. 在文件→新建→源代码这里可以新建源代码:新建→打开项目或文件中可以打开以前写过的源代码或者程序:运行→编译中即把所打源文件转换为二进制代

webservice实验一

实验目的:安装jdk1.6_21以后的版本,利用JAX-WS API自己发布webservice并调用,以及用wsimport生成webservice客户端代码调用一个免费的web服务(如webxml.com.cn上的获取手机归属地的服务). 一.webservice原理了解 webservice是一种通用的跨语言跨平台的数据交互方式,之所以能够做到这一点,是因为它的底层实现机制是依赖于HTTP协议以及XML格式这些开发的标准.webservice使用SOAP(simple object acc

Cisco ISE + Windows Server 2008 实验

实验说明:此实验参考生产环境中某部分环境搭建而成,此环境Windows Server 2008用于登录用户.MAC等账号的认证,Cisco ISE用于认证授权等,无线部分利用VMWLC + Cisco 1702AP测试测试.因为为实验环境,整体网络架构所有节点为单点:Cisco ISE部分功能没有应用上,如测试PC端的补丁.防毒补丁.设备认证等(此部分在生产环境上实施),下图为此实验的网络架构图. Windows AD:  172.16.1.199 Cisco VMISE: 172.16.1.1

CentOS系统启动及内核大破坏模拟实验

讲过了centos的启动流程,此时是不是想来点破坏呢?那就尽情的玩耍吧,记得在实验之前拍个快照,万一哪个环节错误恢复不回来了呢,毕竟数据无价,话不多说,开始. 一.删除伪系统根.(ramdisk文件) (1)模拟误操作删除ramdisk文件. ①模拟误删除initramfs-3.10.0-514.el7.x86_64.img文件. ②为当前正在使用的内核重新制作ramdisk文件 格式为:mkinitrd /boot/initramfs-$(uname -r).img $(uname -r) (

PCB实验与检修杂记

2017-08-24 有ABCD四个焊点,diagram上A与B相连,C与D相连,B与C属于同一组引脚.现根据实验要求将B.C短接,结果测得,B与C导通而A与C不导通,这与常识违背. 经过逐步缩小范围发现,C点上方留出的测量点和焊台的测量结果有所不同,经过分析,可知C点焊脚与焊台虚焊,测量时再表笔的压力下两者导通:而表笔一但离开C点,则两者不导通.即测量引入了误差. 对于此问题解决的思路: 1.稳扎稳打,逻辑分析,逐步缩小范围.2.测量引脚上方留出的点比直接测量引脚更可靠.

2062326 齐力锋 实验四《Java面向对象程序设计Android开发》实验报告

北京电子科技学院(BESTI) 实 验 报 告 课程: 程序设计与数据结构  班级: 1623  姓名: 齐力锋 学号: 20162326 成绩: 指导教师: 娄嘉鹏/王志强 实验日期: 2017年5月26日 实验密级:非密级 预习程度: 优良 实验时间: 2 Hours 仪器组次: 必修/选修: 必修 实验序号: 04 实验名称:Java面向对象程序设计Android开发 实验内容 1.Android Stuidio的安装测试: 参考<Java和Android开发学习指南(第二版)(EPUBI

实验三+036+吴心怡

1)被测项目界面. 2)测试用例设计表 等价类 输入条件 有效等价类 编号 无效等价类 编号 年 1912≤year≤2050 ① year<1912 ② Year>2050 ③ 非数字 月 Month= 1,3,5,7,8,10,12 ④ Month<1 ⑤ Month= 4,6,9,11 month>12 ⑥ Month= 2 非数字 日 1≤ day ≤31 ⑦ day<1 ⑧ Day>311 ⑨ 非数字 测试用例表 测试用例编号 输入数据 预期输出 实际结果 通