本篇文章承接上文:乱弹OpenGL中的矩阵变换(上) 。上篇中,我尝试从一种不太成熟的OpenGL世界观(注意,或许也是3D渲染世界的世界观)来认识这个OpenGL世界,向自己澄清了一些概念(尽管不够高清)。这里我想继续从矩阵数学的角度想一想:究竟模型位置怎么在世界中各个空间的坐标系统中“转换”?——ZwqXin.com
本文来源于 ZwqXin (http://www.zwqxin.cn/), 转载请注明
原文地址:http://www.zwqxin.cn/archives/opengl/opengl-matrix-what-2.html
OpenGL中的坐标系是右手坐标系,矩阵按列优先存储。这很让人混乱,因为线代里接触的都是行优先存储,突然就这么column-major了.....最初还窃认为该不该从向量角度让头脑清晰,后来看别人文章说这跟向量没啥关系,完全是一种规范:如同右手坐标系,只是一种规范。好吧,列就列啦:
mt0 mt4 mt8 mt12
mt1 mt5 mt9 mt13
mt2 mt6 mt10 mt14
mt3 mt7 mt11 mt15
有点怪是不?当然啦,连C语言学2维数组时都是row-major的,OpenGL却是先一竖下来,再来下一竖(column-major)...(考虑一块内存,第一个格子存mt0,第二存mt1.....)这就是OpenGL矩阵存储方式。上篇提到,模型变换(Mode-Translation),视图变换(View-Translation)乃至投影变换,这些决定了各个空间之不同的"针对坐标系的变换",实质产生出来的是一个左乘矩阵,这些矩阵就是这样存储的。如果你看过任何一本3D图形学的入门书前两章,那你应该知道模型变换(在OpenGL中反映为glTranslate,glScale,glRotate)所使用的矩阵。譬如我这里先让苹果绕z轴逆转个角度Θ,再让它变成原大小的A倍,再右移10单位:(P.S.翻译成OpenGL世界的语言:在模型空间坐标系确定的位置基础上,让世界空间坐标系绕z轴顺转角度Θ,坍缩成原来一半大小,并左移10单位,注意是坍缩后1单位表示的长度跟之前的不一样了。)
A*cosΘ | -A*sinΘ | 0 | 10 |
A*sinΘ | A*cosΘ | 0 | 0 |
0 | 0 | A*1 | 0 |
0 | 0 | 0 | 1 |
x
* y = M * P(point of Apple)
z
1
其中M是变换矩阵,点P(x,y,z,1)是苹果的其中一个点的坐标(记得吗?这个坐标是在模型空间确定的并且不变),它是一个竖向量形式的点。为什么是竖向量呢,因为OpenGL矩阵的左乘:(4X4)*(4X1)=(4X1)嘛,这是线形代数知识,(A行数XA列数)*(B行数XB列数)中,只有A列数=B行数,这两个矩阵(向量)才可以相乘。那为什么要左乘?因为OpenGL(点)向量是列向量!哈哈,不是忽悠,这根本就是“规范问题”,譬如D3D就全采取相反的,不多说。最后,思考者应该已经把上面那个矩阵跟再上面那个“模板”对应起来了吧(OPENGL在内存中把相乘的结果——这16个数按mt0=A*cosΘ, mt1=A*sinΘ, mt2=0, mt3=0, mt4=-A*sinΘ, mt5=A*cosΘ, mt6=0......这样的竖的顺序存放)。再譬如我在之前的ShadowVolume Demo中就这样定义了一个4X4矩阵类,并这样定义了一个矩阵(就是上面的mt嘛)左乘某点向量的方法:
- class CMatrix16
- {
- public:
- float mt[16];
- inline CVector4D operator*(CVector4D & vec4) {
- return CVector4D(mt[0] * vec4.x + mt[4] * vec4.y + mt[8] * vec4.z + mt[12] * vec4.w ,
- mt[1] * vec4.x + mt[5] * vec4.y + mt[9] * vec4.z + mt[13] * vec4.w ,
- mt[2] * vec4.x + mt[6] * vec4.y + mt[10]* vec4.z + mt[14] * vec4.w ,
- mt[3] * vec4.x + mt[7] * vec4.y + mt[11]* vec4.z + mt[15] * vec4.w );
- };
- };
- glMatrixMode(GL_MODEL);
- glTranslatef(10,0,0);
- glScalef(A,A,A);
- glRotatef(Θ, 0,0,1)
- DrawApple() ;
请从下往上看。你问为什么顺序是倒着的呢?这跟上篇遗留下来的问题很相似。现在经过模型变换来到了世界空间,接下来是视图变换了。设当前相机得出的左乘矩阵为V,那么下一步,相似地,就是V*(MP)=(VMP)。上篇说过,OpenGL把模型变换和视图变换合并为“模型视图变换”,也就是说,OpenGL只生成一个左乘矩阵VM,上面的两步乘法,在OpenGL中其实一步到位:VM*P=(VMP)。我们来到了视图空间。真正代码在此:
- gluLookat(eye,look,up);
- glMatrixMode(GL_MODELVIEW);
- glLoadIdentity();
- glTranslatef(10,0,0);
- glScalef(A,A,A);
- glRotatef(Θ, 0,0,1)
- DrawApple() ;
接下来由视景体设置,视口设置,投影处理得出的左乘矩阵是J,那么相似地,我们继续左乘:J*(VMP)=(JVMP),来到屏幕空间——OpenGL世界的出口(之后的往窗口坐标系映射等等已经不算OpenGL的工作了)。
- glViewport(0,0,width,height);//设置视口(视窗)大小
- glMatrixMode(GL_PROJECTION));
- glLoadIdentity();
- gluPerspective(fov,aspect,near,far);
- gluLookat(eye,look,up);
- glMatrixMode(GL_MODELVIEW);
- glLoadIdentity();
- glTranslatef(10,0,0);
- glScalef(A,A,A);
- glRotatef(Θ, 0,0,1)
- DrawApple() ;
是不是回到了上篇的结尾呢?恩,我从矩阵意义上再梳理了一次流程。然后,可以回答为什么代码是倒过来写的了吧?——这说明你很清楚,代码是按顺序从上往下执行的。没错,OpenGL中执行这些代码也是从上往下执行的。所以你看看吧,这次要从上往下看:第1句:视窗设置,没什么,它只剔除,不直接产生矩阵(而且还得在下面投影变换处发挥作用);第2句:接下来要作投影变换啦;第3句:glLoadIdentity(),哈哈!这才是重点——无论眼前的是什么,它都能把它初始化成一个单位矩阵!而这里的这个单位矩阵,是一切的开端,它确实就是一个单位矩阵(I)了,后面第4句视景体的设置产生的投影变换矩阵(J)右乘它:J= I*J;接下来第5句相机设置,第6句表示模型视图变换的开始,第7句又一个glLoadIdentity(),继续右乘:J= I*J*I,这有什么用啊?呵呵,因为接下来“模型视图变换”产生的矩阵VM要继续右乘这个结果,直接让两个变换矩阵相乘可以是可以,那么如果没有投影变换呢?呵呵,这是有可能的,先不说平时我们渲染,就我们哪天突然要在模型空间搞事(譬如上次ShadowVolumeDemo那样“回光返照”),单单应用“模型视图变换”就关联不到最初那个glLoadIdentity()了。所以让一个glLoadIdentity()在作变换前出现是好的:VM = I*VM,实际上所有变换前加一个glLoadIdentity()是好习惯。
好吧,整理一下:假设把苹果旋转,放大,移动的相应变换矩阵是R,S,T,相机那个是L,那么这里就是 VM =L*T*S*R,也是倒过来的吧呵呵。按代码顺序的话右乘起来是这样的:(JVM)= I*J*I*VM =I *J*I*L*S*T,等式右边项是严格按照相应代码顺序来右乘的。为什么这里变右乘了?事实上我们说OpenGL的矩阵左乘规范,它跟代码顺序是两码事,矩阵左乘是从逻辑上来说的(也就是上篇和本篇我在开讲代码执行顺序前讲述的那些故事的逻辑,是属于OpenGL世界的逻辑),代码顺序是属于编译器的哦。好了,这个(JVM)就是要把处于模型空间的物体转移到屏幕上来——这时候它才和苹果的顶点相乘:(JVMP)=(JVM)*P,看好了,也是右乘,当然的了——结果是,苹果那么多顶点在一次渲染周期内只被“用”了一次!要知道,得出(JVM)结果的计算只有那么几个矩阵相乘运算,但是苹果N个顶点就有N个(JVM)*P的相乘运算哦!如果代码执行顺序相反你觉得会怎样?“几个N相乘”这么多次运算哦!
这里还有概念要澄清:
1.我说了,苹果也好,其他任何画出来的“东西”也好,它们坐标是在给出顶点坐标的时候就确定在模型空间的了,变换的是位置,也许说它们变换了还不对——因为真正被变换的是坐标系:你上面也看到了,苹果的坐标是最后右乘上去的,之前一直变换的是最初那个单位矩阵,单位矩阵的变换也代表了最初各空间重合的OpenGL世界的坐标系——上篇说过,它的变换是反着模型的(当然也反着单位矩阵),这里也不妨说,它受变换的阶段顺序都是反着模型的——而且它是按照编译器执行顺序变换出来的,是真正的,真实存在的变换:OpenGL中所有的变换,都是在变换坐标系。
2.说法问题,“坐标”和“位置”,一路看下来你也知道,我不把它们当一回事(不想再罗嗦了恩)。但是更通常的叫法(虽然容易引起歧义但说得广泛),是把“坐标”(固定于模型空间的那个,模型刚刚在世界出现时候的位置- -看,又罗嗦了)称为“局部坐标(Local-Coordinate)”,把世界空间里面那个“位置”叫“世界坐标(World-Coordinate)”,也有直接简称它为坐标的(这样的明显不是高手),因为它最能被感知,也许你在了解什么模型空间模型变换等等等等这些概念前,唯一认识的就是世界坐标——它就标识着3D渲染窗口里那个虚拟3D世界不是么?恩,所以说世界空间是OpenGL世界中近乎“正常”的空间,类比一下,如果把我们人类所生活的空间叫“世界空间”的话,小说中描绘的到处扭曲的异次元空间就叫“模型空间”呀“屏幕空间”呀之类的了(笑)。遗憾的是OpenGL不直接给我们提供这个空间中各物体的坐标系位置呢~不怕,我们还有逆矩阵!
3.等有缘的你来发挖啦!错误啦认识问题啦,随便提。这里是ZwqXin: www.zwqxin.com, 我的3D旅途记录簿。(纪念今天2009.2.5.BLOG上轨道啦。)
最后...还有glPushMatrix()和glPopMatrix()呢?相信大家用得很多,也知道用处:在它们之外的东西不受它们里面的那些模型变换代码影响。譬如下面这里,苹果跟上面一样也是做那些变换。可是橙呢?旋转和放大对它无影响,它只是受到移动了(OpenGL世界说法是,对应坐标系移动了)。
- glMatrixMode(GL_MODELVIEW);
- glLoadIdentity();
- glTranslatef(10,0,0);
- glPushMatrix();
- glScalef(A,A,A);
- glRotatef(Θ, 0,0,1);
- DrawApple() ;
- glPopMatrix();
- DrawOrange() ;
不知道我的讲解有没有人满意。我自己倒是满意了。哈哈。后面我还会有文章说说逆矩阵运算的问题和作用,并赞NEHE,有兴趣的不妨CLICK一CLICK.
本文来源于 ZwqXin (http://www.zwqxin.cn/), 转载请注明
原文地址:http://www.zwqxin.cn/archives/opengl/opengl-matrix-what-2.html