欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

opengl 坐标的理解

程序员文章站 2022-03-13 10:52:58
...

【目标】:学习OpenGL中的坐标系统。

 

【参考】:

1、《计算机图形学(OpenGL版) (第三版)》 Francis著 (本文主要涉及第三章~第七章)

2、《计算机图形学(OpenGL版) (第三版)》 Donald著

 

一、前言

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

 

二、数学基础

1、齐次坐标

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

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

       opengl 坐标的理解    (式2.1 - 1)

      或者用表示为

  1. P2 = M * P1 + C  (式2.1 - 2)  
  P2 = M * P1 + C  (式2.1 - 2)

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

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

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

  1. P2‘ = M’ * P1’  (式2.1 -3)  
   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。所以仿射变换可以表示为:

      opengl 坐标的理解    (式2.3 - 1)

 

4、基本的仿射变换

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

4.1  平移

      M =  opengl 坐标的理解

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

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

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

 

4.2  缩放

        M = opengl 坐标的理解

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

 

4.3  剪切

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

         M = opengl 坐标的理解

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

opengl 坐标的理解

 

4.4  旋转

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

4.4.1 基本的旋转

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

opengl 坐标的理解

 

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

opengl 坐标的理解

4.4.2 复合的旋转

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

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

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

(3)沿z轴旋转β°

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

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

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

        那么

  1. P2 = R(β) * P1  
  2. T(α) * P2 = R(β) * (T(α) * P1)  
  3. P2 = T(-α) * R(β) * T(α) * P1  
   P2 = R(β) * P1
   T(α) * P2 = R(β) * (T(α) * P1)
   P2 = T(-α) * R(β) * T(α) * P1

 

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

 

5、基本仿射变换的复合

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

  1. P2 = Mb * Ma * P1  
   P2 = Mb * Ma * P1

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

 

三、OpenGL坐标系

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

 

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

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

opengl 坐标的理解

下面具体说明各个步骤

 

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 = opengl 坐标的理解

         那么,局部坐标系下的原点(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 = opengl 坐标的理解

        求逆矩阵得到

        V = opengl 坐标的理解

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

 

  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. }  
#include "AllHead.h"

//通过实践掌握视点矩阵(glulookat)

static void init() {
    glClearColor(1, 1, 1, 0);
}

static void display() {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    //do nothing
    glFlush();
}

static void reshape(int w, int h) {
    glViewport(0, 0, w, h);
    //投影矩阵设为单位矩阵
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();

    //模型视点矩阵先设置为单位矩阵
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    printMatrix(GL_MODELVIEW_MATRIX);
    std::cout << "set look at !" << std::endl;
    //摄像机位于(1,0,0)
    gluLookAt(1, 0, 0, 0, 0, 0, 0, 1, 0);
    printMatrix(GL_MODELVIEW_MATRIX);
}

void LCG_cp07_pageNone_testViewPlane(int argc, char ** argv) {
    setupWindow(argc, argv, GLUT_RGB | GLUT_SINGLE, INTPAIR(600, 400));
    init();
    glutReshapeFunc(reshape);
    glutDisplayFunc(display);
    glutMainLoop();
}

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


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

4.1  基础

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

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

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

       P3 = Mp * P2 (式 3-4)

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

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

opengl 坐标的理解

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


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】。所以默认的正投影矩阵实际上是 

  1. glOrtho(-1, 1,   -1, 1,   1, -1);   
 glOrtho(-1, 1,   -1, 1,   1, -1); 


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

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

       这里贴一个例子:

  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. }  
#include "AllHead.h"

//通过实验掌握投影变换

typedef std::pair<GLfloat, GLfloat>  GloatPair;

static void init() {
    glClearColor(1, 1, 1, 0);
    //打开深度测试,这样才能看到遮盖的效果
    glClearDepth(1.0f);
    glEnable(GL_DEPTH_TEST);
}

static void reshape(int w, int h) {
    glViewport(0, 0, w, h);
}

static void drawRectangle(const GloatPair & leftTop, const GloatPair & rightButtom, GLfloat z) {
    glBegin(GL_QUADS);
    glVertex3f(leftTop.first, leftTop.second, z);
    glVertex3f(rightButtom.first, leftTop.second, z);
    glVertex3f(rightButtom.first, rightButtom.second, z);
    glVertex3f(leftTop.first, rightButtom.second, z);
    glEnd();
}

static void display() {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    //设置与否对本实例没有影响,不过可以开启看看
    //glMatrixMode(GL_MODELVIEW);
    //glLoadIdentity();
    //gluLookAt(0,0,0.5, 0,0,0, 0,1,0);
    //std::cout << "print model view" << std::endl;
    //printMatrix(GL_MODELVIEW_MATRIX);

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(-2, 2, -2, 2, 5, -5);

    std::cout << "print projection " << std::endl;
    printMatrix(GL_PROJECTION_MATRIX);

    //画两个方块
    glColor3f(1, 0, 0); //先是个红的
    drawRectangle(GloatPair(-0.5, 0.5), GloatPair(0.7, -0.7), 0.2);
    glColor3f(0, 1, 0); //再在背面画个绿色的
    drawRectangle(GloatPair(-0.7, 0.7), GloatPair(0.5, -0.5), -0.2);

    glFlush();

}

void LCG_cp07_pageNone_testProjectionMatrix(int argc, char ** argv) {
    setupWindow(argc, argv, GLUT_SINGLE | GLUT_RGB, INTPAIR(400, 400));
    init();
    glutReshapeFunc(reshape);
    glutDisplayFunc(display);
    glutMainLoop();
}


可以通过修改这里的 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的值来做深度测试,代码如下(仅提供与上面的程序不同的部分):

 

  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. }  
static const GLfloat PROJECTION_MATRIX [16] =  {
       1,   0,   0,  0,
       0,   1,   0,  0,
       0,   0,  -1,  0,
    -0.5, 0.5,   0,  1
};

static void display() {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glMultMatrixf(PROJECTION_MATRIX);

    std::cout << "print projection " << std::endl;
    printMatrix(GL_PROJECTION_MATRIX);

    //画两个方块
    glColor3f(1, 0, 0); //先是个红的
    drawRectangle(GloatPair(-0.5, 0.5), GloatPair(0.7, -0.7), 0.2);
    glColor3f(0, 1, 0); //再在背面画个绿色的
    drawRectangle(GloatPair(-0.7, 0.7), GloatPair(0.5, -0.5), -0.2);

    glFlush();

}

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


4.4  透视投影

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

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

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

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

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

opengl 坐标的理解

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

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

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

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

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

  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. }  
static void testPerspectiveProjection() {
    log("before");
    printMatrix(GL_PROJECTION_MATRIX);

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    //两种调用,效果相同,注意near和far必须为正,但实际指的是z轴的负坐标
    //glFrustum(-1, 1, -1, 1, 1, 2);
    gluPerspective(90, 1, 1, 2);

    log("after");
    printMatrix(GL_PROJECTION_MATRIX);


    //在-1.5处画个方块
    glColor3f(1, 0, 0);
    drawRectangle(-1.4, 1.4, -1.4, 1.4, -1.5);
}

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 结果是:

opengl 坐标的理解

         时刻牢记:这里的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) 设置的情况。

       opengl 坐标的理解

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

        opengl 坐标的理解

        但是如果以这样的结论去构造矩阵,就会碰到麻烦:变换矩阵要求对所有的点都适用,但是针对不同的点(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,具体的方法可以在下面看到:

opengl 坐标的理解

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

       opengl 坐标的理解

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

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

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

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

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

opengl 坐标的理解

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

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

opengl 坐标的理解

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


6、展示照片:视口

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

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


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

opengl 坐标的理解

转载出处: http://blog.csdn.net/ronintao/article/details/9157221

【目标】:学习OpenGL中的坐标系统。

 

【参考】:

1、《计算机图形学(OpenGL版) (第三版)》 Francis著 (本文主要涉及第三章~第七章)

2、《计算机图形学(OpenGL版) (第三版)》 Donald著

 

一、前言

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

 

二、数学基础

1、齐次坐标

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

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

       opengl 坐标的理解    (式2.1 - 1)

      或者用表示为

  1. P2 = M * P1 + C  (式2.1 - 2)  
  P2 = M * P1 + C  (式2.1 - 2)

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

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

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

  1. P2‘ = M’ * P1’  (式2.1 -3)  
   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。所以仿射变换可以表示为:

      opengl 坐标的理解    (式2.3 - 1)

 

4、基本的仿射变换

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

4.1  平移

      M =  opengl 坐标的理解

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

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

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

 

4.2  缩放

        M = opengl 坐标的理解

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

 

4.3  剪切

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

         M = opengl 坐标的理解

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

opengl 坐标的理解

 

4.4  旋转

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

4.4.1 基本的旋转

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

opengl 坐标的理解

 

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

opengl 坐标的理解

4.4.2 复合的旋转

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

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

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

(3)沿z轴旋转β°

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

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

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

        那么

  1. P2 = R(β) * P1  
  2. T(α) * P2 = R(β) * (T(α) * P1)  
  3. P2 = T(-α) * R(β) * T(α) * P1  
   P2 = R(β) * P1
   T(α) * P2 = R(β) * (T(α) * P1)
   P2 = T(-α) * R(β) * T(α) * P1

 

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

 

5、基本仿射变换的复合

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

  1. P2 = Mb * Ma * P1  
   P2 = Mb * Ma * P1

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

 

三、OpenGL坐标系

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

 

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

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

opengl 坐标的理解

下面具体说明各个步骤

 

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 = opengl 坐标的理解

         那么,局部坐标系下的原点(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 = opengl 坐标的理解

        求逆矩阵得到

        V = opengl 坐标的理解

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

 

  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. }  
#include "AllHead.h"

//通过实践掌握视点矩阵(glulookat)

static void init() {
    glClearColor(1, 1, 1, 0);
}

static void display() {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    //do nothing
    glFlush();
}

static void reshape(int w, int h) {
    glViewport(0, 0, w, h);
    //投影矩阵设为单位矩阵
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();

    //模型视点矩阵先设置为单位矩阵
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    printMatrix(GL_MODELVIEW_MATRIX);
    std::cout << "set look at !" << std::endl;
    //摄像机位于(1,0,0)
    gluLookAt(1, 0, 0, 0, 0, 0, 0, 1, 0);
    printMatrix(GL_MODELVIEW_MATRIX);
}

void LCG_cp07_pageNone_testViewPlane(int argc, char ** argv) {
    setupWindow(argc, argv, GLUT_RGB | GLUT_SINGLE, INTPAIR(600, 400));
    init();
    glutReshapeFunc(reshape);
    glutDisplayFunc(display);
    glutMainLoop();
}

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


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

4.1  基础

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

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

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

       P3 = Mp * P2 (式 3-4)

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

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

opengl 坐标的理解

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


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】。所以默认的正投影矩阵实际上是 

  1. glOrtho(-1, 1,   -1, 1,   1, -1);   
 glOrtho(-1, 1,   -1, 1,   1, -1); 


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

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

       这里贴一个例子:

  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. }  
#include "AllHead.h"

//通过实验掌握投影变换

typedef std::pair<GLfloat, GLfloat>  GloatPair;

static void init() {
    glClearColor(1, 1, 1, 0);
    //打开深度测试,这样才能看到遮盖的效果
    glClearDepth(1.0f);
    glEnable(GL_DEPTH_TEST);
}

static void reshape(int w, int h) {
    glViewport(0, 0, w, h);
}

static void drawRectangle(const GloatPair & leftTop, const GloatPair & rightButtom, GLfloat z) {
    glBegin(GL_QUADS);
    glVertex3f(leftTop.first, leftTop.second, z);
    glVertex3f(rightButtom.first, leftTop.second, z);
    glVertex3f(rightButtom.first, rightButtom.second, z);
    glVertex3f(leftTop.first, rightButtom.second, z);
    glEnd();
}

static void display() {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    //设置与否对本实例没有影响,不过可以开启看看
    //glMatrixMode(GL_MODELVIEW);
    //glLoadIdentity();
    //gluLookAt(0,0,0.5, 0,0,0, 0,1,0);
    //std::cout << "print model view" << std::endl;
    //printMatrix(GL_MODELVIEW_MATRIX);

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(-2, 2, -2, 2, 5, -5);

    std::cout << "print projection " << std::endl;
    printMatrix(GL_PROJECTION_MATRIX);

    //画两个方块
    glColor3f(1, 0, 0); //先是个红的
    drawRectangle(GloatPair(-0.5, 0.5), GloatPair(0.7, -0.7), 0.2);
    glColor3f(0, 1, 0); //再在背面画个绿色的
    drawRectangle(GloatPair(-0.7, 0.7), GloatPair(0.5, -0.5), -0.2);

    glFlush();

}

void LCG_cp07_pageNone_testProjectionMatrix(int argc, char ** argv) {
    setupWindow(argc, argv, GLUT_SINGLE | GLUT_RGB, INTPAIR(400, 400));
    init();
    glutReshapeFunc(reshape);
    glutDisplayFunc(display);
    glutMainLoop();
}


可以通过修改这里的 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的值来做深度测试,代码如下(仅提供与上面的程序不同的部分):

 

  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. }  
static const GLfloat PROJECTION_MATRIX [16] =  {
       1,   0,   0,  0,
       0,   1,   0,  0,
       0,   0,  -1,  0,
    -0.5, 0.5,   0,  1
};

static void display() {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glMultMatrixf(PROJECTION_MATRIX);

    std::cout << "print projection " << std::endl;
    printMatrix(GL_PROJECTION_MATRIX);

    //画两个方块
    glColor3f(1, 0, 0); //先是个红的
    drawRectangle(GloatPair(-0.5, 0.5), GloatPair(0.7, -0.7), 0.2);
    glColor3f(0, 1, 0); //再在背面画个绿色的
    drawRectangle(GloatPair(-0.7, 0.7), GloatPair(0.5, -0.5), -0.2);

    glFlush();

}

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


4.4  透视投影

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

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

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

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

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

opengl 坐标的理解

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

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

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

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

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

  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. }  
static void testPerspectiveProjection() {
    log("before");
    printMatrix(GL_PROJECTION_MATRIX);

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    //两种调用,效果相同,注意near和far必须为正,但实际指的是z轴的负坐标
    //glFrustum(-1, 1, -1, 1, 1, 2);
    gluPerspective(90, 1, 1, 2);

    log("after");
    printMatrix(GL_PROJECTION_MATRIX);


    //在-1.5处画个方块
    glColor3f(1, 0, 0);
    drawRectangle(-1.4, 1.4, -1.4, 1.4, -1.5);
}

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 结果是:

opengl 坐标的理解

         时刻牢记:这里的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) 设置的情况。

       opengl 坐标的理解

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

        opengl 坐标的理解

        但是如果以这样的结论去构造矩阵,就会碰到麻烦:变换矩阵要求对所有的点都适用,但是针对不同的点(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,具体的方法可以在下面看到:

opengl 坐标的理解

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

       opengl 坐标的理解

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

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

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

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

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

opengl 坐标的理解

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

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

opengl 坐标的理解

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


6、展示照片:视口

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

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


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

opengl 坐标的理解

相关标签: 图形