« 乱弹OpenGL中的矩阵变换(上)Shadow Volume 阴影锥技术之探Ⅳ »

乱弹OpenGL中的矩阵变换(下)

本篇文章承接上文:乱弹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嘛)左乘某点向量的方法:

  1. class CMatrix16
  2. public:
  3.     float mt[16];
  4.  
  5.    inline CVector4D operator*(CVector4D & vec4) { 
  6. return CVector4D(mt[0] * vec4.x + mt[4] * vec4.y + mt[8] * vec4.z + mt[12] * vec4.w ,
  7.      mt[1] * vec4.x + mt[5] * vec4.y + mt[9] * vec4.z + mt[13] * vec4.w ,
  8.     mt[2] * vec4.x + mt[6] * vec4.y + mt[10]* vec4.z + mt[14] * vec4.w ,
  9.      mt[3] * vec4.x + mt[7] * vec4.y + mt[11]* vec4.z + mt[15] * vec4.w );       
  10.  
  11.    };
  12.  
  13. };
然后,我们回头看之前那条式子,得出的结果是(A*cosΘ*x - A*sinΘ*y + 10,A*sinΘ*x + A*cosΘ*y,A*z,1),这是点P的新位置,注意,再说一次,是“位置”而不是“坐标”,点P坐标依然是(x,y,z,1)。(当然,你还可以用这样的概念来区分。)我姑且把结果记作(MP),所有点的结果构成了苹果在世界空间的位置。好了,看看代码:
  1. glMatrixMode(GL_MODEL);
  2. glTranslatef(10,0,0);
  3. glScalef(A,A,A);
  4. glRotatef(Θ, 0,0,1)
  5.   
  6. DrawApple() ;

请从下往上看。你问为什么顺序是倒着的呢?这跟上篇遗留下来的问题很相似。现在经过模型变换来到了世界空间,接下来是视图变换了。设当前相机得出的左乘矩阵为V,那么下一步,相似地,就是V*(MP)=(VMP)。上篇说过,OpenGL把模型变换和视图变换合并为“模型视图变换”,也就是说,OpenGL只生成一个左乘矩阵VM,上面的两步乘法,在OpenGL中其实一步到位:VM*P=(VMP)。我们来到了视图空间。真正代码在此:

  1. gluLookat(eye,look,up);
  2.  
  3. glMatrixMode(GL_MODELVIEW);
  4. glLoadIdentity();  
  5.  
  6. glTranslatef(10,0,0); 
  7. glScalef(A,A,A); 
  8. glRotatef(Θ, 0,0,1) 
  9.    
  10. DrawApple() ;

接下来由视景体设置,视口设置,投影处理得出的左乘矩阵是J,那么相似地,我们继续左乘:J*(VMP)=(JVMP),来到屏幕空间——OpenGL世界的出口(之后的往窗口坐标系映射等等已经不算OpenGL的工作了)。

 
  1. glViewport(0,0,width,height);//设置视口(视窗)大小
  2.   
  3. glMatrixMode(GL_PROJECTION));  
  4. glLoadIdentity(); 
  5. gluPerspective(fov,aspect,near,far);
  6.  
  7. gluLookat(eye,look,up); 
  8.  
  9. glMatrixMode(GL_MODELVIEW); 
  10. glLoadIdentity();   
  11.  
  12. glTranslatef(10,0,0);  
  13. glScalef(A,A,A);  
  14. glRotatef(Θ, 0,0,1)  
  15.     
  16. 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世界说法是,对应坐标系移动了)。

 
  1. glMatrixMode(GL_MODELVIEW);  
  2. glLoadIdentity();    
  3.   
  4. glTranslatef(10,0,0);  
  5.  
  6. glPushMatrix();
  7.   glScalef(A,A,A);   
  8.   glRotatef(Θ, 0,0,1);       
  9.   DrawApple() ;
  10. glPopMatrix();
  11.  
  12. DrawOrange() ;

不知道我的讲解有没有人满意。我自己倒是满意了。哈哈。后面我还会有文章说说逆矩阵运算的问题和作用,并赞NEHE,有兴趣的不妨CLICK一CLICK.

本文来源于 ZwqXin (http://www.zwqxin.cn/), 转载请注明
      原文地址:http://www.zwqxin.cn/archives/opengl/opengl-matrix-what-2.html

  • quote 1.zjun86
  • 关于矩阵变换的问题,opengl其实也是行存储的,但是矩阵变换的时候只能用矩阵乘法变换,矩阵乘法是
    第一个矩阵的第i行乘以第二个矩阵的第j列的和为最终矩阵的i行j列的值,所以变换矩阵需要把行和列变换过来在乘当前矩阵才能得到变换的矩阵。所以opengl其实也是行存储的呢!
    hustlaofan 于 2011-8-10 12:09:51 回复
    这位仁兄说的有道理,我也是这么觉得的,可以自己写个glulookat,得到列存储的视图矩阵与opengl的glulookat得到的矩阵比较下,就知道了
  • 2009-7-2 11:29:46 回复该留言
  • quote 2.zwqxin
  • 阁下是在哪里看到资料的呢?opengl又何必做这么麻烦不讨好的事情哦?
  • 2009-7-10 8:26:59 回复该留言
  • quote 3.请教个问题
  • 我读入些点坐标,并显示在OPENGL中。然后我变换这些点的位置,比如用
    glTranslated(mx, my, mz);
    glRotatef(tx,1,0,0);
    glRotatef(ty,0,1,0);
    glRotatef(tz,0,0,1);
    然后我要将变化后的坐标存起来怎么存啊?谢谢了~
    zwqxin 于 2009-7-31 11:35:42 回复
    进行模型变换后,模型在世界空间下的坐标.
    事实上OPENGL只是给你的点坐标(原始的模型坐标)左乘了个齐次模型矩阵.
    可在程序中另外加插如下代码尝试一下:
    glPushMatrix();
    glLoadIdentity();
    glTranslated(mx, my, mz);
    glRotatef(tx,1,0,0);
    glRotatef(ty,0,1,0);
    glRotatef(tz,0,0,1);
    glGetFloatv(GL_MODELVIEW_MATRIX,current_mvmatrix);
    //current_mvmatrix[16]
    glPopMatrix();
    再用current_mvmatrix与你的原始点坐标直接相乘得结果
    P.S.如果只有平移操作就简单了,只记录平移量则可.
    谢谢你了 于 2009-7-31 17:50:35 回复
    非常感谢,弄好了。。呵呵~ 能把你邮箱给我吗?以后说不定还要请教你呢,现在在做一个项目,刚开始弄OPENGL,还多基本概念理解不透。。我的邮箱:
    zyulou@gmail.com
    zwqxin 于 2009-8-11 17:23:07 回复
    在About里有的...gotomyxin@live.com
  • 2009-7-31 10:52:43 回复该留言
  • quote 4.我应该没有计算错误
  • 你的大名是通过同学得知的。最近刚开始学习OpenGL,也一直在看你写的东西,首先说声谢谢。在这篇文章中提到的第二个式子中:

    A*cosΘ -sinΘ 0 10
    sinΘ A*cosΘ 0 0
    0 0 A*1 0
    0 0 0 1
    它是多个矩阵的相乘结果,如果我没有计算错误的话,这个矩阵应该是:

    A*cosΘ -A*sinΘ 0 10
    A*sinΘ A*cosΘ 0 0
    0 0 A*1 0
    0 0 0 1




    zwqxin 于 2009-9-27 0:58:09 回复
    恩,感谢阁下指出错误。
    你是正确的。
    在下马上更改。
  • 2009-9-26 23:04:24 回复该留言

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

IE下本页面显示有问题?

→点击地址栏右侧【兼容视图】←

日历

Search

网站分类

最新评论及回复

最近发表

Powered By Z-Blog 1.8 Walle Build 100427

Copyright 2008-2024 ZwqXin. All Rights Reserved. Theme edited from ipati.