OpenGL中的坐标系

 原文地址:http://blog.csdn.net/ronintao/article/details/9157221

一、前言  

坐标系应该是任何图像系统的基石。在学习Cocos2D的过程中,对着《权威指南》上草草结束的坐标系介绍,实在是看的一头雾水,找了本OpenGL书把这块研究了一下,大致算是清楚了其中的一些基本概念。这里总结一下,作为记录。

 

二、数学基础

1、齐次坐标

        (对应图形学一书的第4.5节)

        在三维空间中的一个点,通常用p = (x, y ,z) 这样的三维坐标来表示,如果要对这样一个点进行如下的简单的变换(即仿射变换,下面会提到)。那么其形式可以如下表示:

           (式2.1 - 1)

      或者用表示为

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. P2 = M * P1 + C  (式2.1 - 2)  

        这样表示的变换存在一种不便:需要乘法和加法两次计算才能完成转换。回想多项式乘法会发现,(a+b)^n 的展开式相当之长,这样,当变换的次数很多时,这样的计算就会很复杂且不直观。

        由此,引入了齐次坐标的概念:对于某个三维坐标点(x, y, z),增加一维 w != 0,并对原三维坐标进行同样的缩放,形成新的四维坐标(wx, wy, wz, w),即是所谓齐次坐标。

        引入了齐次坐标后,原式2.1-2 就可以改写为:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. P2' = M' * P1'  (式2.1 -3)  

       这样,只需要用乘法就可以完成所有的任务了。

 

2、点、向量 的齐次坐标表示

       前面的齐次坐标表示实际上是“点”的齐次坐标表示。且在接触到投影矩阵之前(即在模型视图矩阵阶段),对于一个坐标点 P = (wx, wy, wz, w) 的 w值都是1,也必须是1。

       而对于向量,若其原三维表示为 v = (x, y , z), 则在齐次坐标下的表示为 v' = (x, y, z, 0)。即对于向量来说,齐次坐标表示就是在第四维上填0

       进一步的,从原来上来说,这里的第四维坐标w,实际上表示是否含有坐标轴原点O的信息。(坐标点可以理解为原点O移动向量p后的结果)

 

3、仿射变换

       仿射变换的数学定义是:在几何中,一个向量空间进行一次线性变换并接上一个平移,变换为另一个向量空间,可以用式2.1-1表示。

       那么在齐次坐标下, 由于在前面了解到点的齐次坐标表示都是 (x, y, z, 1),第四维必须是1。所以仿射变换可以表示为:

          (式2.3 - 1)

 

4、基本的仿射变换

       复杂的仿射变换可以通过多次进行基本的仿射变换来完成。这些基本的原子变换包括:平移、缩放、剪切、旋转变换。(Donald的书在这个地方讲的更数学一点,更加严谨)

4.1  平移

      M =  

        其中,(mx, my, mz) 是平移量,也是新坐标系(移动后)的原点O,在原坐标系(移动前)下的坐标值。

        注意,上面关于原点这个表述很有意思。如果变换看做点的移动,那么右边(式2.1-2中的P2)是原来的坐标,左边(P1)是新的坐标。但是如果看做是坐标系的移动,那么对于同一个点P,P2是老坐标,P1是新坐标。

        在我们这里,新的原点O,其老坐标就是P2。

 

4.2  缩放

        M = 

       其中,(Sx, Sy, Sz) 是缩放比例。很简单不解释了。

 

4.3  剪切

        也有翻译为错位变换的,定义是某一维上(如Y)引入另一维的影响(如X),效果是方形变平行四边形。可以看 http://baike.baidu.com/view/2424073.htm。根据两个维度的选择,矩阵略有不同。如果是Y上引入X的影响,则矩阵为:

         M = 

       当f = 1时,效果图如下:

 

4.4  旋转

        放在最后当然是最麻烦的。由于绕任意轴的旋转很难表示,所以实际上复杂的旋转又被继续分解,最基本的旋转是绕某一个轴进行的旋转。

4.4.1 基本的旋转

        首先定义旋转的正方向:(右手规则)用右手握住轴,大拇指指向轴的正方向,则四指所指方向为旋转的正方向。(也就是逆时针方向,以后基于法向量的逆时针也可以用这个法则判定)。

 

       那么沿着某个轴旋转 β° 的话,那么其矩阵如下:

4.4.2 复合的旋转

        那么,对于任意给定的轴 v = (x, y, z, 0),旋转β°是怎样得到的呢?总体的思路是这样的:

(1)先平移,使得旋转轴通过原点(opengl不需要考虑这一步,glrotatef的旋转轴都是从原点出发的)

(2)沿x轴和y轴旋转,使得旋转轴与Z轴重合

(3)沿z轴旋转β°

(4)做第二步的反操作,依次沿y轴和x轴进行反旋转

(5)做第一步的反操作,反向平移

        总体来说,理论依据来源于:M(β) * M(-β) = E,即旋转矩阵可逆,且正向原子旋转和反向原子旋转互为逆矩阵。

        那么

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. P2 = R(β) * P1  
  2. T(α) * P2 = R(β) * (T(α) * P1)  
  3. P2 = T(-α) * R(β) * T(α) * P1  

 

       而左乘一个矩阵,就代表着在原有变换基础上继续变换(后面也会讲到)。更具体的推导这里略去,可以参考Donald书上的5.11.2节。最后的结论也太长,这里也不附了。

 

5、基本仿射变换的复合

       其实前面也提到了,如果先做一个变换a,再做一个变换b,那么其复合的变换矩阵就是:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. P2 = Mb * Ma * P1  

      注意是即可,原理也很明显,不再证明。

 

三、OpenGL坐标系

 研究任何坐标系(非欧的不清楚),只要把握住以下三点:1、原点;2、坐标轴正方向;3、坐标单位。以下均按照这个思路研究。

 

1、OpenGL坐标系转换的大致流程

一般使用摄像来做比来描述这个流程,Donald书上289页的一张流水线图则从数学上解释了这个流程。两者合并起来是这样的:

下面具体说明各个步骤

 

2、摆放物体(模型变换):局部坐标系 -> 世界坐标系

        世界坐标系是右手坐标系,正方向无意义,单位是1,原点坐标是指定的(即无意义)。局部坐标系也是一样(不过单位长度和世界坐标系中的单位可能存在比例关系)。

        举个例子,给定一个世界,我们平移(x, y, z),然后缩放(Sx, Sy, Sz)。在变换过的原点位置放了一个单位大小的立方体。那么立方体的局部坐标就是(0, 0, 0),在局部坐标系中的大小是(1, 1, 1)。但是他在世界坐标系中的坐标是(x, y, z),大小是(Sx, Sy, Sz)。

        这里世界坐标系中各个要素的“无意义”是我的理解。意思是说,这个世界坐标系是预先给定的,不变化的,在这个阶段是和我们的电脑屏幕上的像素坐标没有关系的。就好像我们的地球,使用经纬度为坐标,但实际上任意指定经纬度的起止点和单位都是可以的,这就是世界坐标系。而后面的物体、摄像机,都是基于这个坐标系摆放的。

         而针对局部坐标系,从代码上讲,我们使用glVertex3f设定的坐标值实际上是局部坐标系下的值。在不对模型矩阵进行任何变换的时候,这个坐标系是和世界坐标系重合的。

         这个阶段的变换,主要包括 glTranslae (平移)、glScale (缩放)、glRotate (旋转)【剪切不知道是什么】。在经过这样一系列变换之后,局部坐标系上的某个点P0(x0, y0, z0, 1),会被摆放到世界坐标系上的P1(x1, y1, z1, 1)点:

         P1 = M * P0 (式3-2)

         模型变换矩阵 M 可以参考从“数学基础”一节里面给出的结果。例如,调用 glTranslated(1, 0, 0) 将局部坐标系向 x 轴正方向移动了1个单位,那么

        M = 

         那么,局部坐标系下的原点(0, 0, 0, 1),实际上就是世界坐标系下的(1, 0, 0, 1),和我们平移的结果相比显然是正确的。【这里可以再体会一下“新坐标系(移动后)的原点O,在原坐标系(移动前)下的坐标值”这句话,虽然世界坐标系是事先给定的,但是坐标的值给出的却总是局部坐标系下的值,其在真实世界的位置,是需要通过求解才能得到的】

 

3、摆放摄像机(观察变换):世界坐标系 -> 视点坐标系

3.1  理论

        这个阶段实际上就是调用函数 gluLookAt(eye, center, up)。通过这个操作,将摄像机摆放在了eye的位置,镜头的上方与up在一个平面上(这个说法不严谨,严谨的看下面的),视线指向center。

        视点坐标系就是为摄像机服务的坐标系,原点在摄像机位置(即eye处),它仍然是右手坐标系。三个坐标轴(u, v, n) 如下确定:

        (1)视线轴 z : 从 center 点 指向 eye 点。注意:是和视线方向相反的

        (2)水平轴 x : X = up × Z (叉乘是右手规则)

        (3)上方轴 y : Y = Z × X。 y指向的方向通常和 up 很接近。如果 up本身与视线方向恰好垂直时,两者重合。

        至于单位,肯定是单位1,但是是世界坐标系下的单位1呢,还是局部坐标系下的单位1呢?(两者的区别在于前者不受glScale影响)。另外一个问题, gluLookAt 的参数。这里面指定了三个参数 eye,center,up。前面已经有了两个坐标系:世界坐标系和局部坐标系,那么这里的坐标点是以什么坐标系为参照的呢?

        这还是要从视点变换的地位说起。在OpenGL中,并没有专门的视点矩阵和模型矩阵,两者是合一的,称为 GL_MODELVIEW 。不过两者实质上还是属于不同的流程,要先从局部变换到世界坐标系,然后再从世界坐标系变换到视点坐标系。如果接着上面的公式3-2,那么一个局部坐标系下的点P0,在映射为世界坐标系下的点P1后,又会被转换为视点坐标系下的点 P2:

         P2 = V * M * P0 (式3-3)

         这里的V是视点变换矩阵。

         这样的话,前面的两个问题就可以回答了:单位是世界坐标系下的单位1(这个无伤大雅),坐标是世界坐标系下的坐标(这个很关键)。【至于为什么只使用一个矩阵,我是这样理解的:世界坐标系相当于只是一个中间变量,最后映入眼帘的,还是基于视点坐标系的世界,所以 V * M 可以合并,导致OpenGL中使用一个矩阵来描述这个过程。】

         但是这里的视点变换矩阵V的确定方法,又与M不同,如果我们设定 gluLookAt(0, 0, 1,   0, 0, 0,   0, 1, 0); 即视点坐标系只是将世界坐标系平移了 (0, 0, 1),那么是否 V 就是[1, 0, 0, 0;   0, 1, 0, 0;  0, 0, 1, 1; 0, 0, 0, 0] 呢?答案是错的,其矩阵实际上是和平移(0, 0, -1)的变换矩阵相同。这是为什么呢?再回到那句话:

         “如果看做是坐标系的移动,那么对于同一个点P,左边P2是老坐标,右边P1是新坐标。”

         对做上面变换的一个场景中,考虑世界坐标系中的原点。原坐标值(0, 0, 0, 1)是老坐标,现在坐标轴平移了(0, 0, 1),想要知道的是在新坐标系(视点坐标系)下的新坐标P1。那么根据

        P2 = Translate * P1 (translate是平移(0, 0, 1)的变换矩阵)

        显然,V 是 Translate 的逆矩阵。这其实在物理上也容易理解:移动摄像机,相当于反向移动物体

         这段比较麻烦,举个例子印证一下。


3.2  例子:视点矩阵的影响

        做这样一个操作:假设世界坐标系和局部坐标系重合,然后定义 gluLookAt(1, 0, 0, 0, 0, 0, 0, 1, 0); 即站在(1, 0, 0)点看着(0, 0, 0)点。这样的视点变换相当于做了两件事:首先沿x轴平移了1个单位,然后绕y轴旋转了-90°,使得新的z轴和原来的x轴负方向重合。那么其变换矩阵,根据平移和旋转的定义,就应该是

        Translate = 

        求逆矩阵得到

        V = 

       可以用这样一段代码得到验证:

 

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. #include "AllHead.h"  
  2.   
  3. //通过实践掌握视点矩阵(glulookat)  
  4.   
  5. static void init() {  
  6.     glClearColor(1, 1, 1, 0);  
  7. }  
  8.   
  9. static void display() {  
  10.     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);  
  11.   
  12.     //do nothing  
  13.     glFlush();  
  14. }  
  15.   
  16. static void reshape(int w, int h) {  
  17.     glViewport(0, 0, w, h);  
  18.     //投影矩阵设为单位矩阵  
  19.     glMatrixMode(GL_PROJECTION);  
  20.     glLoadIdentity();  
  21.   
  22.     //模型视点矩阵先设置为单位矩阵  
  23.     glMatrixMode(GL_MODELVIEW);  
  24.     glLoadIdentity();  
  25.     printMatrix(GL_MODELVIEW_MATRIX);  
  26.     std::cout << "set look at !" << std::endl;  
  27.     //摄像机位于(1,0,0)  
  28.     gluLookAt(1, 0, 0, 0, 0, 0, 0, 1, 0);  
  29.     printMatrix(GL_MODELVIEW_MATRIX);  
  30. }  
  31.   
  32. void LCG_cp07_pageNone_testViewPlane(int argc, char ** argv) {  
  33.     setupWindow(argc, argv, GLUT_RGB | GLUT_SINGLE, INTPAIR(600, 400));  
  34.     init();  
  35.     glutReshapeFunc(reshape);  
  36.     glutDisplayFunc(display);  
  37.     glutMainLoop();  
  38. }  

        可以看到打印出来的结果和我们的预期是一样的。


4、观察者的所见:视点坐标系 -> 投影坐标系

4.1  基础

       【这一步是和下面的裁剪紧密结合的,这里认为是分开的两步,且只讨论和投影相关的部分】

        投影就是将摄像机在三维的视点坐标系中的所见,映射到一张二维的投影平面(照片)上的过程。这样看来,似乎投影坐标系应该是二维的啦?不过为了方便深度测试等操作,实际上投影坐标系仍然是三维的。不过其z轴坐标值的意义已经不是那么重要了。

        既然是坐标系变换,那么根据前面的经验,这里显然又会有一个变换矩阵,称之为 投影矩阵Mp。在OpenGL中是 GL_PROJECTION。则有:

       P3 = Mp * P2 (式 3-4)

       这里的P2是视点坐标系下的坐标值。【这里扯一句题外话,如果给定原坐标系要素和变换矩阵,是否可以唯一确定新坐标系的要素呢?答案是肯定的,因为原点和单位向量都可以通过变换求解出来】

      那么,这个Mp 要如何确定呢?这就要说到几种不同的投影方式了。


      具体来说,有以上三大类投影。都比较形象就不解释了。下面分别来看三种投影的变换矩阵。


4.2  正投影

        最简单的就是这种投影。在OpenGL中使用 glOrtho(left,  right, bottom, top,  near, far); 来进行设置。这里的几个参数主要是关系到下面的剪裁和规范化的,对于投影本身只有一点影响:以 x 轴坐标为例,若 left <= right,则 Xp = X,但是若  left > right, 那么就有 Xp = -X。

       这里另外需要注意的是 (near, far) 这对变量。由于Opengl默认的坐标系中,Z轴是从屏幕指向用户的,所以实际上坐标越大,距离用户的距离就越近,所以实际上的近平面是 -near,远平面点是 -far 【参考 http://baike.baidu.com/view/1280554.htm】。所以默认的正投影矩阵实际上是 

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. glOrtho(-1, 1,   -1, 1,   1, -1);   


       (注意这里和Donald书上317页的结论有出入,但实际测试gluOrtho2D的结论也是和我这里相同的)

       正投影的投影矩阵比较简单,基本就是单位矩阵,当左右什么的发生逆向的时候,对应分量就变为-1。当然 GL_PROJECTION 里面还包含一些后面的因素。

       这里贴一个例子:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. #include "AllHead.h"  
  2.   
  3. //通过实验掌握投影变换  
  4.   
  5. typedef std::pair<GLfloat, GLfloat>  GloatPair;  
  6.   
  7. static void init() {  
  8.     glClearColor(1, 1, 1, 0);  
  9.     //打开深度测试,这样才能看到遮盖的效果  
  10.     glClearDepth(1.0f);  
  11.     glEnable(GL_DEPTH_TEST);  
  12. }  
  13.   
  14. static void reshape(int w, int h) {  
  15.     glViewport(0, 0, w, h);  
  16. }  
  17.   
  18. static void drawRectangle(const GloatPair & leftTop, const GloatPair & rightButtom, GLfloat z) {  
  19.     glBegin(GL_QUADS);  
  20.     glVertex3f(leftTop.first, leftTop.second, z);  
  21.     glVertex3f(rightButtom.first, leftTop.second, z);  
  22.     glVertex3f(rightButtom.first, rightButtom.second, z);  
  23.     glVertex3f(leftTop.first, rightButtom.second, z);  
  24.     glEnd();  
  25. }  
  26.   
  27. static void display() {  
  28.     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);  
  29.   
  30.     //设置与否对本实例没有影响,不过可以开启看看  
  31.     //glMatrixMode(GL_MODELVIEW);  
  32.     //glLoadIdentity();  
  33.     //gluLookAt(0,0,0.5, 0,0,0, 0,1,0);  
  34.     //std::cout << "print model view" << std::endl;  
  35.     //printMatrix(GL_MODELVIEW_MATRIX);  
  36.   
  37.     glMatrixMode(GL_PROJECTION);  
  38.     glLoadIdentity();  
  39.     glOrtho(-2, 2, -2, 2, 5, -5);  
  40.   
  41.     std::cout << "print projection " << std::endl;  
  42.     printMatrix(GL_PROJECTION_MATRIX);  
  43.   
  44.     //画两个方块  
  45.     glColor3f(1, 0, 0); //先是个红的  
  46.     drawRectangle(GloatPair(-0.5, 0.5), GloatPair(0.7, -0.7), 0.2);  
  47.     glColor3f(0, 1, 0); //再在背面画个绿色的  
  48.     drawRectangle(GloatPair(-0.7, 0.7), GloatPair(0.5, -0.5), -0.2);  
  49.   
  50.     glFlush();  
  51.   
  52. }  
  53.   
  54. void LCG_cp07_pageNone_testProjectionMatrix(int argc, char ** argv) {  
  55.     setupWindow(argc, argv, GLUT_SINGLE | GLUT_RGB, INTPAIR(400, 400));  
  56.     init();  
  57.     glutReshapeFunc(reshape);  
  58.     glutDisplayFunc(display);  
  59.     glutMainLoop();  
  60. }  


可以通过修改这里的 glOrtho(-2, 2, -2, 2, 5, -5); 来进行实验。

 

4.3  斜投影

       在OpenGL中貌似没有直接的斜投影方法。那么我们用直接指定矩阵的方法也可以达到目的。不用被4.1开头的非平行平面的斜投影吓到,实际上只需要考虑平行平面的投影效果就可以求取投影矩阵了。

       例如上面例子中画的两个方块,如果希望其重合,且红的遮盖绿的。那么由于两个方块实际上是相等大小的。所以只需要考虑正方形中心的变换:(0.1,  -0.1,  0.2)【红色的中心】 ->  (-0.1,  0.1, -0.2)【绿色的中心】。那么投影线都是平行于这条线的。

         假设我们的投影平面是z = 0 平面,容易得到 投影的关系是 Xp = X - Z/2  ; Yp = Y + Z/2,和正投影一样,我们保留z的值来做深度测试,代码如下(仅提供与上面的程序不同的部分):

 

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. static const GLfloat PROJECTION_MATRIX [16] =  {  
  2.        1,   0,   0,  0,  
  3.        0,   1,   0,  0,  
  4.        0,   0,  -1,  0,  
  5.     -0.5, 0.5,   0,  1  
  6. };  
  7.   
  8. static void display() {  
  9.     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);  
  10.   
  11.     glMatrixMode(GL_PROJECTION);  
  12.     glLoadIdentity();  
  13.     glMultMatrixf(PROJECTION_MATRIX);  
  14.   
  15.     std::cout << "print projection " << std::endl;  
  16.     printMatrix(GL_PROJECTION_MATRIX);  
  17.   
  18.     //画两个方块  
  19.     glColor3f(1, 0, 0); //先是个红的  
  20.     drawRectangle(GloatPair(-0.5, 0.5), GloatPair(0.7, -0.7), 0.2);  
  21.     glColor3f(0, 1, 0); //再在背面画个绿色的  
  22.     drawRectangle(GloatPair(-0.7, 0.7), GloatPair(0.5, -0.5), -0.2);  
  23.   
  24.     glFlush();  
  25.   
  26. }  

     使用glMultMatrixf来直接指定矩阵。注意这里的 PROJECTION_MATRIX 实际上是所需要的矩阵的转置。更加复杂的斜投影在这里就不继续研究了。


4.4  透视投影

        经过了上面的洗礼,透视投影也没什么难以理解的概念了。透视投影的核心就是近大远小。显然一个物体如果就放在视点位置,那么就是无穷大了(比如你的眼皮)。所以视点的观察半径一般是取非零值的。

      一般设定透视投影的方法有两种:

     1、gluPerspective(theta, aspect, near, far)

     2、glFrustum(left, right, bottom, top, near, far)

     两种实际上差不多,这里只介绍后面一种。


       这张图是在网上下的。注意这张图画的非常有技巧。left,right,bottom,top都是实实在在的坐标值,但是near和far是个距离,这就意味着这两个值不应该为负,实际上也确实如此,如果设置为负,则此次设置是不起作用的。

       从上面的视点坐标系的分析可以理解,这里的摄像机摆放在视点坐标系的原点(他也就是书上所说的投影参考点),视线指向 z 轴负方向。剪裁的近平面和原平面的深度都为正,所以实际上这些平面的Z坐标都为负(在视点坐标系下)。即有:Znear = - near, Zfar = -far。

        而投影的平面,这条语句实际上是选择的近平面,这样可以减少很多麻烦的计算。

        如果只是理解这种投影的概念,那么到这里基本也就够了,不过如果想要确定下面透视投影矩阵,就需要真正用到齐次坐标,而且要牵涉到规范化的部分。所以放在下一节一起讲。

        这里贴一小段测试代码以供备份,非常简单,display的时候调用即可,log是我自己封装的函数。

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. static void testPerspectiveProjection() {  
  2.     log("before");  
  3.     printMatrix(GL_PROJECTION_MATRIX);  
  4.   
  5.     glMatrixMode(GL_PROJECTION);  
  6.     glLoadIdentity();  
  7.     //两种调用,效果相同,注意near和far必须为正,但实际指的是z轴的负坐标  
  8.     //glFrustum(-1, 1, -1, 1, 1, 2);  
  9.     gluPerspective(90, 1, 1, 2);  
  10.   
  11.     log("after");  
  12.     printMatrix(GL_PROJECTION_MATRIX);  
  13.   
  14.   
  15.     //在-1.5处画个方块  
  16.     glColor3f(1, 0, 0);  
  17.     drawRectangle(-1.4, 1.4, -1.4, 1.4, -1.5);  
  18. }  

5、裁剪照片:规范化

5.1  为什么要规范化

        世界虽大,但是能放在一张照片里面显示的东西是有限的。所以要对所见到的世界进行裁剪,取出想要的部分。

        但是在算法上实现裁剪是很复杂的。为了方便这一流程,首先将需要的部分规范化,缩放到一个单位大小的格子里面,然后再用一个单元大小的相框去丈量,将相框以外的全部舍弃,这就是规范化和裁剪的一个主要出发点和思路。

        对应的物理世界中的场景,就是将照相机(老式的似乎更加贴近)的所见缩放到一张标准大小的底片上,超过底片大小的部分全都不会显示出来。

        那么在OpenGL中,照相机的所见实际上已经被转换成了投影平面上的一张二维图像(各个点上也带有z轴信息),现在要处理的就是这个东西。


5.2  规范化的目标

        既然要规范化,那么就得先有一个规范。前面在投影部分也已经看到,每种投影,都有一个剪裁空间,称之为观察体,对正投影来说是一个立方体,对斜投影来说是一个平行六面体,对透视投影来说是一个棱台。如果一个观察体是一个x、y、z坐标范围都是 [-1, 1] 的立方体,则称之为规范化立方体,这个就是所谓的规范。那么,将原来的观察体,映射到规范化立方体的过程,就是规范化。

        一个格外需要注意的地方是,由于后面的屏幕坐标系通常是左手坐标系,所以这里的规范化观察体也使用左手坐标系,意味着 x 轴和 y 轴没有改变,但是 z 轴的正方向转了个。这带来的结果是,在这样的坐标系下,z 的坐标值越小,距离观察者(也就是你)越近。实际上,在opengl中,进行规范化之后,近裁剪平面的z轴坐标是 -1,远裁剪平面的z轴坐标是1。


5.3  正投影的规范化

        前面虽然是在透视投影中提到的规范化,不过还是先从简单的说起。代码中通过 glOrtho(left, right, bottom, top, near, far); 来指定了一个观察体,这个观察体本来就是一个立方体,x 的范围是 [left, right],y 的范围是 [bottom, top],z 的范围是 [-far, -near],现在要将其放到一个左手坐标系下的规范化立方体中,只需要进行平移和缩放。虽然坐标轴体系发生了变换,不过实际上只是 z 轴坐标取了个反,所以变动也很容易得到。综合前面投影的变换,最后的矩阵 GL_PROJECTION 结果是:


         时刻牢记:这里的near和far,在原视点坐标系中的代表的 z 轴坐标是 -near 和 -far。

         再回想4.2中的例子,红色方块的z轴坐标是0.2,绿色的是-0.2,在原视点坐标系中,红色的距离视点更近(这样的值可能还不太容易理解,如果换成红色-0.6、绿色-1就更明显了)。如果对GL_PROJECTION 调用 glLoadIdentity(),对应上面矩阵可以知道 near = 1, far = -1。换算成原视点坐标系下 z 轴坐标就是 近平面z = -1,远平面 z = 1。正好是逆着视线的,所以应该看到绿色遮盖红色。

        另一方面,直接将红绿点的坐标进行转换,红色最终的z轴坐标是 0.2,绿色是-0.2,从5.2上最后一句可以知道,坐标值越小,距离观察者越近,所以绿色更近,也应该看到绿色遮盖红色。这样的判断和程序运行的结果是完全相同的。


5.4 透视投影的规范化

       斜投影这里不打算研究了,以后用到的时候再说吧。直接来看透视投影。这里考虑使用 glFrustum(left, right, bottom, top, near, far) 设置的情况。

       

        以上图为例,(x, y, z) 投影到观察平面的 (x', y', z')。显然 z' = Znear,这张图给出了 y-z 平面的剖面,容易看出  y’ = y ÷ z × z‘ = y ÷ z × Znear。同理可以得到  x' = x ÷ z × Znear。即

        

        但是如果以这样的结论去构造矩阵,就会碰到麻烦:变换矩阵要求对所有的点都适用,但是针对不同的点(x, y, z),其缩放比例却和 z 的值有关,这样的话,普通意义上来说,就无法构造变换矩阵了。

        这个时候,就体现出了齐次坐标的好处:这里实际上只需要保存一个额外的变量z,那么为什么不用用第四个坐标去记录Z值呢?事实上,这是完全可行的。回忆2.1中的说明,齐次坐标是四维坐标(wx, wy, wz, w),只不过我们前面一直使用的w = 1罢了。

        接下去的问题是,现在 w = z了,那么 z 坐标必须进行变换,否则在齐次化的时候,就会始终为1。和正投影一样,z轴的值本身并不具有意义,但是其范围和大小是有意义的。变换的最终目标是 Znear -> -1, Zfar -> 1,越近的点的z值越小。要做到这一点,只需要利用一下变换前的 w = 1,具体的方法可以在下面看到:


       这里的a和b是两个待定参数,利用Znear -> -1, Zfar -> 1,可以求取a和b的值:a = (Zfar +Znear) / (Zfar - Znear),b = 2 * Zfar * Znear / (Zfar - Znear)。即

       

       下一步就是要将(x', y', z', w')规范化。显然通过缩放和平移就可以做到,现在已知的条件有以下这些: 

       (1)对x, left -> -1,right ->1

       (2)对y, bottom -> -1, top -> 1,上面两条实际上是和正投影类似的。

       (3)对 z,已经在 [-1, 1] 范围了 不变即可。

      这样,很容易得到这一步的变换矩阵


      这个时候基本上就可以算是结束了,不过注意到  glFrustum 里面设置的 near 值必须为正,而 Znear 实际就是负,所以按照现在的变换矩阵求出来的 w 一般是负的。好在由于是齐次的,所以四个维度上都乘个 -1  就可以解决问题。

      考虑到 near = -Znear, far = - Zfar,替换进变换矩阵,所以最后的变换矩阵是这样的:


       可以随手写个例子验证一下,使用4.4中的例程即可。


6、展示照片:视口

        OK,万事俱备,终于到了最后一步,将图片展示出来。我们现在手上有的是一张单位大小的照片。现在只需要将它冲洗到指定的大小,并摆放在指定的位置。使用的方法就是 glViewport(x,y,  width, height),调用这个方法之后,会将照片的左下角摆放在(x,y)点,并将其缩放,使得原来单位大小的图片,放大到宽为 width,高为 height。

        到这里为止,基于坐标系的变换就结束了。


        最后再把变化的过程贴一遍

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值