【计算机动画】蒙皮实验
最近学习计算机动画,看书千万遍都不如动手做一遍,实现了一下完整的人物蒙皮,遇到一些问题,现在把完整的实现过程记一下。
理论在前一篇已经讲完了,前面也实现了基本的关节动画,现在要实现的是,使用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中直接计算就好了:
没有顶点混合:
混合后:
这些动画姿势都是手动旋转出来的,关于动画数据怎么回事,目前还不是很懂。