在前段时间的OpenGL平面坐标与世界坐标的互转博文中,了解了世界坐标worldCoord向相机坐标系转换方法,相机坐标如何获得投影坐标,投影坐标如何转换屏幕坐标,我们已经很清楚啦。可是有时我们下面代码:
// 设置投影矩阵
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(45.0f, (GLfloat)w/(GLfloat)h, 0.1f, 100.0f);
当我们看到gluPerspective时,会有疑问,他是怎么转换为4×4的投影矩阵的呐!
我们先看一下gluPerspective 的参数含义:
void gluPerspective(
GLdouble fovy, //角度 眼睛睁开的角度
GLdouble aspect, //视景体的宽高比
GLdouble zNear, //沿z轴方向的两裁面之间的距离的近处(正数)
GLdouble zFar //沿z轴方向的两裁面之间的距离的远处(正数)
)
对于眼睛角度的解释可以查看:
http://www.cppblog.com/COOOOOOOOL/archive/2009/12/28/104255.html
但是看完这篇文章还是不能理解gluPerspective都做了什么?
直到看到这篇文章投影矩阵的推导 (OpenGL D3D)终于搞懂了gluPerspective的原理,下面为部分内容。
gluPerspective思想竟然是相似三角形原理。世界坐标经过相机矩阵的变换,顶点被变换到了相机空间。这个时候的多边形也许会被视锥体裁剪,但在这个不规则的体中进行裁剪并非那么容易的事情,所以经过图形学前辈们的精心分析,裁剪被安排到规则观察体(CanonicalView Volume, CVV)中进行,CVV是一个正方体,x, y, z的范围都是[-1,1],多边形裁剪就是用这个规则体完成的。所以,事实上是透视投影变换由两步组成:
1) 用透视变换矩阵把顶点从视锥体中变换到裁剪空间的CVV中。
2) CVV裁剪完成后进行投影变换。
我们一步一步来,我们先从一个方向考察投影关系。
上图是右手坐标系中顶点在相机空间中的情形。设P(x,z)是经过相机变换之后的点,视锥体由eye——眼睛位置,np——近裁剪平面,fp——远裁剪平面组成。N是眼睛到近裁剪平面的距离,F是眼睛到远裁剪平面的距离。投影面可以选择任何平行于近裁剪平面的平面,这里我们选择近裁剪平面作为投影平面。设P’(x’,z’)是投影之后的点,则有z’ = -N。通过相似三角形性质,我们有关系:
同理,有
这样,我们便得到了P投影后的点P’
从上面可以看出,投影的结果z’始终等于-N,在投影面上。实际上,z’对于投影后的P’已经没有意义了,这个信息点已经没用了。但对于3D图形管线来说,为了便于进行后面的片元操作,例如z缓冲消隐算法,有必要把投影之前的z保存下来,方便后面使用。因此,我们利用这个没用的信息点存储z,处理成:
这个形式最大化地使用了3个信息点,达到了最原始的投影变换的目的,但是它太直白了,有一点蛮干的意味,我感觉我们最终的结果不应该是它,你说呢?我们开始结合CVV进行思考,把它写得在数学上更优雅一致,更易于程序处理。假入能够把上面写成这个形式:
那么我们就可以非常方便的用矩阵以及齐次坐标理论来表达投影变换:
其中
哈,看到了齐次坐标的使用,这对于你来说已经不陌生了吧?这个新的形式不仅达到了上面原始投影变换的目的,而且使用了齐次坐标理论,使得处理更加规范化。注意在把 变成 的一步我们是使用齐次坐标变普通坐标的规则完成的。这一步在透视投影过程中称为透视除法(Perspective Division),这是透视投影变换的第2步,经过这一步,就丢弃了原始的z值(得到了CVV中对应的z值,后面解释),顶点才算完成了投影。而在这两步之间的就是CVV裁剪过程,所以裁剪空间使用的是齐次坐标 ,主要原因在于透视除法会损失一些必要的信息(如原始z,第4个-z保留的)从而使裁剪变得更加难以处理,这里我们不讨论CVV裁剪的细节,只关注透视投影变换的两步。
矩阵
就是我们投影矩阵的第一个版本。你一定会问为什么要把z写成
有两个原因:
1) P’的3个代数分量统一地除以分母-z,易于使用齐次坐标变为普通坐标来完成,使得处理更加一致、高效。
2)后面的CVV是一个x,y,z的范围都为[-1,1]的规则体,便于进行多边形裁剪。而我们可以适当的选择系数a和b,使得 这个式子在z = -N的时候值为-1,而在z = -F的时候值为1,从而在z方向上构建CVV。
接下来我们就求出a和b:
这样我们就得到了透视投影矩阵的第一个版本:
使用这个版本的透视投影矩阵可以从z方向上构建CVV,但是x和y方向仍然没有限制在[-1,1]中,我们的透视投影矩阵的下一个版本就要解决这个问题。
为了能在x和y方向把顶点从Frustum情形变成CVV情形,我们开始对x和y进行处理。先来观察我们目前得到的最终变换结果:
我们知道-Nx / z的有效范围是投影平面的左边界值(记为left)和右边界值(记为right),即[left, right],-Ny / z则为[bottom, top]。而现在我们想把-Nx / z属于[left, right]映射到x属于[-1, 1]中,-Ny / z属于[bottom, top]映射到y属于[-1, 1]中。你想到了什么?哈,就是我们简单的线性插值,你都已经掌握了!我们解决掉它:
则我们得到了最终的投影点:
下面要做的就是从这个新形式出发反推出下一个版本的透视投影矩阵。注意到 是 经过透视除法的形式,而P’只变化了x和y分量的形式,az+b和-z是不变的,则我们做透视除法的逆处理——给P’每个分量乘上-z,得到
而这个结果又是这么来的:
则我们最终得到:
上面是一般情况,我们要把它变成特殊性版本,即gluPerspective,它是一种左右对称的投影形式,因此我们从对x和y进行插值的那一步来看:
那一步来看:
销掉两边的1/2,得到:
则我们反推出透视投影矩阵:
这就是gluPerspective的投影矩阵了。
代码实现:
// 4x4 矩阵 - 列主序
//0 4 8 12
//1 5 9 13
//2 6 10 14
//3 7 11 15
typedef float M3DMatrix44f[16]; // A 4 X 4 矩阵, 列主序 (floats) - OpenGL 样式
// 获取投影矩阵
void PerspectiveToPorjMatrix(float fFov, float fAspect, float fNear, float fFar, M3DMatrix44f projMatrix)
{
float xmin, xmax, ymin, ymax; // 近剪切面范围
// 近剪切面范围计算
ymax = fNear * float(tan( fFov * M3D_PI / 360.0 ));
ymin = -ymax;
xmin = ymin * fAspect;
xmax = -xmin;
// 构造投影矩阵
memset(projMatrix, 0, sizeof(M3DMatrix44f));
projMatrix[0] = (2.0f * fNear)/(xmax - xmin);
projMatrix[5] = (2.0f * fNear)/(ymax - ymin);
projMatrix[8] = (xmax + xmin) / (xmax - xmin);
projMatrix[9] = (ymax + ymin) / (ymax - ymin);
projMatrix[10] = -((fFar + fNear)/(fFar - fNear));
projMatrix[11] = -1.0f;
projMatrix[14] = -((2.0f * fFar * fNear)/(fFar - fNear));
projMatrix[15] = 0.0f;
}
与gluPerspective比较:
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(45.0f, (GLfloat)w/(GLfloat)h, 0.1f, 100.0f);
GLfloat projection[16];
glGetFloatv(GL_PROJECTION_MATRIX, projection);
结果相同。
那么glFrustum,glOrtho() , gluOrtho2D, gluLookAt()函数呐?
以后我们将继续讨论!