【目标】:学习OpenGL中的坐标系统。
【参考】:
1、《计算机图形学(OpenGL版) (第三版)》 Francis著 (本文主要涉及第三章~第七章)
2、《计算机图形学(OpenGL版) (第三版)》 Donald著
一、前言
坐标系应该是任何图像系统的基石。在学习Cocos2D的过程中,对着《权威指南》上草草结束的坐标系介绍,实在是看的一头雾水,找了本OpenGL书把这块研究了一下,大致算是清楚了其中的一些基本概念。这里总结一下,作为记录。
二、数学基础
1、齐次坐标
(对应图形学一书的第4.5节)
在三维空间中的一个点,通常用p = (x, y ,z) 这样的三维坐标来表示,如果要对这样一个点进行如下的简单的变换(即仿射变换,下面会提到)。那么其形式可以如下表示:
(式2.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 就可以改写为:
- 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。所以仿射变换可以表示为:
(式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,即旋转矩阵可逆,且正向原子旋转和反向原子旋转互为逆矩阵。
那么
- P2 = R(β) * P1
- T(α) * P2 = R(β) * (T(α) * P1)
- 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,那么其复合的变换矩阵就是:
- P2 = Mb * Ma * P1
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 =
可以用这样一段代码得到验证:
- #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();
- }
#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 要如何确定呢?这就要说到几种不同的投影方式了。
具体来说,有以上三大类投影。都比较形象就不解释了。下面分别来看三种投影的变换矩阵。
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】。所以默认的正投影矩阵实际上是
- glOrtho(-1, 1, -1, 1, 1, -1);
glOrtho(-1, 1, -1, 1, 1, -1);
(注意这里和Donald书上317页的结论有出入,但实际测试gluOrtho2D的结论也是和我这里相同的)
正投影的投影矩阵比较简单,基本就是单位矩阵,当左右什么的发生逆向的时候,对应分量就变为-1。当然 GL_PROJECTION 里面还包含一些后面的因素。这里贴一个例子:
- #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();
- }
#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的值来做深度测试,代码如下(仅提供与上面的程序不同的部分):
- 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();
- }
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)
两种实际上差不多,这里只介绍后面一种。
这张图是在网上下的。注意这张图画的非常有技巧。left,right,bottom,top都是实实在在的坐标值,但是near和far是个距离,这就意味着这两个值不应该为负,实际上也确实如此,如果设置为负,则此次设置是不起作用的。
从上面的视点坐标系的分析可以理解,这里的摄像机摆放在视点坐标系的原点(他也就是书上所说的投影参考点),视线指向 z 轴负方向。剪裁的近平面和原平面的深度都为正,所以实际上这些平面的Z坐标都为负(在视点坐标系下)。即有:Znear = - near, Zfar = -far。
而投影的平面,这条语句实际上是选择的近平面,这样可以减少很多麻烦的计算。
如果只是理解这种投影的概念,那么到这里基本也就够了,不过如果想要确定下面透视投影矩阵,就需要真正用到齐次坐标,而且要牵涉到规范化的部分。所以放在下一节一起讲。
这里贴一小段测试代码以供备份,非常简单,display的时候调用即可,log是我自己封装的函数。
- 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);
- }
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 结果是:
时刻牢记:这里的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。
到这里为止,基于坐标系的变换就结束了。
最后再把变化的过程贴一遍