3d程序经常要用到矩阵算法,
比较常见的如旋转矩阵,平移矩阵,以及投影矩阵
opengl与d3d均有对应的api进行相应的操作。

本文主要介绍一下投影矩阵,
(在阅读irricht与ogre代码时碰到了一些问题,发现视截体是根据投影矩阵计算出来的,

其实也可以根据视角与摄影机的位置与朝向,计算出视截体的,quake就是这么做的,

另外处理纹理阴影时,也需要对投影矩阵有一定程度的了解)



程序内存中保存的是x y z这样的三维数据,最终的显示结果,却只有x y这两个维度。



将三维空间的点转换成屏幕(显存)的二维数据,就是通过投影矩阵来完成的.


投影矩阵其实很简单。



想像一下,近平面与远平面,与原点组成了一个金字塔,



所有的视截体内在三维点,全部投影到近平面上,得到一个二维点


(z变成深度值,对点投影在二维平面上没有影响,这点很重要,在后继的阐述中将会发现,z的值是非线性的,


越接近近平面,值被放大,接近远平面的值则放大系数变小)



再来理一下三维点转换成二维点的过程:


1 世界坐标转换成观察坐标


2 投影变换


3 从坐标中去除齐次坐标的成份(表达的不清楚,实际上是除以点的z坐标值,后面会阐述)



经过旋转平移,我们可以直接假定摄影机就位于0,0,0 对着 0,0,-1(即z轴负向)


而视截体则为一个缺顶的金字塔,由6个平面构成(上下左右,远平面z=n,近平面z=f)


我们假设视角为a(在计算中,这个好像没什么用), 近平面宽w 高 h (远平面不用管,只要知道远平面的z=f即可)


则位于视截体内部的一个点 (x, y, z) 它在近平面上的投影 (x1, y1)


可以表示如下(根据相似三角形)


x1 = x * (n/z)


y1 = y * (n/z)



然后x1 y1 和 z(没有z1) 再转换成标准视空间(一个从 -1,-1,0 延伸到 1,1,1的轴对齐矩形)


即最终的数据(x2, y2, z2) x2 y2取值一定是从-1,1之间 z2的取值则为0,1


按比例变换即可


x2 = x1 / w


y2 = y1 / h





x2 = (x * (n/z))/w


y2 = (y * (n/z))/h


所以投影矩阵其实是一个缩放矩阵,


这里分母包含z,也就是上文提到的齐次坐标的问题,投影矩阵里都是常数(w,h给定情况下)


改写上面的的等式


x2 * z = (x*n)/w


y2 * z = (y*n)/w



然后是z2


因为引入了齐次坐标,自然也希望来个 z2 * z = p*z + q,以便投影矩阵就是一个缩放矩阵


由于 z2取值是 0,1


即 z=n 时 z2等于 0


即 z=f 时 z2等于 1


我们假设这样的一个等式


z2 * z = p * z + q (p q是常数)


分别将z2 = 0, z = n  与 z2 = 1, z = f 代入,即可解出这个方程p 与 q 的值


可以得到


p = f / (f - n)


q = - (f * n) / (f - n)



上面提到过 z2是非线性的,即,接近近平面,被放大,接近远平面,放大倍数变小,在远平面上的点无放大(z2=1)


如果按严格的比例变换,z2的真实值应该是


z2_real = (z - n)/(f - n)  (z2_real严格处在0,1之间)


再来看我们假设求得的z2


p q代入


z2 = (f/z) *( (z-n)/ (f-n) )  ---- 这里笔者做了一些变换,以便说明问题



f/z就是放大的倍数(就是上文反复提到的) 而且 z2也一定处于(0,1)之间,并且单调递增


因为z2并不影响投影结果,它只表征深度,所以这样的变换满足3d绘图需求的。



于是有



x2 * z = (x*n)/w


y2 * z = (y*n)/h


z2 * z = pz + q  (p,q已经在上文求出) ---- <标记1>



则点(x,y,z,1) 这里加上w,因为要解决齐次坐标问题,经过这样一个投影矩阵,变成



(x2*z,  y2*z, z2*z,  z),  去除齐次坐标后 (x2, y2, z2, 1) 就变换到标准视空间了(也就是最终绘图的依据)



现在给出投影矩阵



  n/w, 0,   0, 0    --- 此处的w是宽除2


  0,   n/h, 0, 0     ----  此处h同上


  0,   0,   p, q


  0,   0,   1, 0


 


前面的三行都很好理解,根据<标记1>处的三个等式得来的,


最后一行的1,就是为了引入齐次坐标z.


以便能opengl能正确地去除z



这样的一个矩阵,最后计算结果x2 y2受z影响,就能表现出透视的效果,z2也正确地在0,1之间单调递增(非线性)。



ps:


d3d的标准视空间是 -1,-1,0 1,1,1


opengl的标准的视空间好像是 -1,-1,-1 到 1,1,1 (z的取值好像是-1,1)但这不影响相应的公式推导


w, h的值可以用a与n组合得出(还有纵横比),这也是opengl与d3d那个计算投影矩阵函数要传的参数 a, 纵横比, n, f


附上代码片断与注释

void matrix4::gen_pers_proj_matrix(ant_f32 fovy, ant_f32 aspect, ant_f32 z_near, ant_f32 z_far, matrix4& out_mtx)
{
	/*
	* 假设视截体内的点(x, y, z) 投影到标准视空间里的(x_p, y_p, z_p)
	* 需要把视截体里的点,映身到标准视空间里(-1, -1, 0) (1, 1, 1)
	* 即投影之后 x_p y_p取值为(-1, 1) z_p的取值为(0, 1)
	* 根据相似三角形,投影形成的 x_p y_p肯定与z有关,所以矩阵中需要包含一个齐次空间因子,以便做矩阵乘法后w取值为z
	*
	* 2n/w, 0,    0, 0
	* 0,    2n/h, 0, 0   --- x y只是单纯的缩放(根据相似三角形)
	* 0,    0,    p, q   --- 这两个参数纯粹为了矩阵运算是造出来的,在几何上不存在意义,它们能保证变换后的z_p在(0,1)区间单调递增,并且z会与比例值放大(f/z)倍,这也是很多3d编程书籍中提到的放大倍数
	* 0,    0,    1, 0   --- 1添加齐次空间因子,最终(x, y, z)经过矩阵运算会变成(x_p * z, y_p * z, z_p * z, z),去除齐次坐标因子后得到(x_p, y_p, z_p, 1)
	*
	* p = f / (f - n)
	* q = - (f * n) / (f - n)
	*
	* 以上是d3d的情况,opengl的标准视空间是 (-1, -1, -1) (1, 1, 1),即 z_p的取值范围是(-1, 1)
	* 并且z是负值,因为opengl默认是位于(0, 0, 0)并且看向z负方向,相应的缩放系数会变成
	*  x_p * (-z) = (2n/w) * x
	*  y_p * (-z) = (2n/h) * y
	*  z_p * (-z) = p_o * z + q_o   --- 这里要做一个变通 z虽然是负的,但解的时候,要假设 z在(n,f) 然后z_p对应(-1,1)
	*
	* 解得 p_o = (f+n)/(f-n)  q_o=(-2fn)(f-n),  然后考虑z实际是处于(-f, -n)这个区间,则 p_o要取 -(f+n)/(f-n)
	* 对应的矩阵
	*
	*  2n/w, 0,    0,   0
	*  0,    2n/h, 0,   0
	*  0,    0,    p_o, -1  --- 这个取-1,是因为齐次坐标因子此时是 -z;
	*  0,    0,    q_o, 0   
	*/

	/* 2n/h 实际上就是tan(fovy/2) */
	ant_f32 h = tan(fovy * ANT_AR_CONVERT * 0.5f);
	ant_f32 w = h / aspect;//aspect不可为零
	ant_f32 p = (z_far + z_near) / (z_near - z_far);//分母不可为零
	ant_f32 q = (2 * z_far * z_near) / (z_near - z_far);//分母不可为零

	out_mtx.m_[0] = w;
	out_mtx.m_[1] = 0;
	out_mtx.m_[2] = 0;
	out_mtx.m_[3] = 0;

	out_mtx.m_[4] = 0;
	out_mtx.m_[5] = h;
	out_mtx.m_[6] = 0;
	out_mtx.m_[7] = 0;

	/*  */
	out_mtx.m_[8] = 0;
	out_mtx.m_[9] = 0;
	out_mtx.m_[10] = p;
	out_mtx.m_[11] = -1;

	out_mtx.m_[12] = 0;
	out_mtx.m_[13] = 0;
	out_mtx.m_[14] = q;
	out_mtx.m_[15] = 0;
}