OpenGL Projection Matrix
原文地址:原文
Overview
电脑显示器是一个二维平面, 而OpenGL渲染出来的场景却是三维的 , 所以必须要投影到二维的电脑屏幕上. 可以使用 GL_PROJCETION matrix 来进行投影转换. 首先,它把顶点数据从 eye coordinates(视点坐标) 转换到 clip coordinates(裁剪坐标). 在将这些坐标除以(w) 坐标分量来转换到NDC(标准化设备坐标)上
因此,我们应该清楚 裁剪(可视平截头体的裁剪) 和 NDC 的转换都包含在了 GL_PROJECTION 矩阵中了. 下一段将会说明如何通过l,r,b,t,n,f(左,右,下,上,近和远)这六个边界值来构建透视矩阵.
注意 可视平截头体的裁剪(裁剪) 是在 clip coordinates(裁剪坐标) 中, 在除以 (w_c) 前执行的.
clip coordinates 的 (x_c,y_c,z_c) 会通过 (w_c) 来测试, 如果任何一个大于(w_c) 或 小于(-w_c), 那么这个顶点就会被丢弃.
当有丢弃时,OpenGL 将会重新构建多边形的边.
一个被平截头体剪裁的三角形
在透视投影中, truncated pyramid frustum (eye coordinates) (截锥体平截头体(视点坐标)) 中 三维的点呗映射到一个立方体(NDC); 三个坐标分量分别映射到 ([-1,1]. x: [l,r] => [-1,1], y:[b,t] => [-1,1], z:[n,f] => [-1,1])
注意 eye coordinates 是定义在右手坐标中的, 但 NDC 是在左手坐标中的. 因此 源点摄像机在 eye space (视觉空间) 中看向 (z轴) 负向 而在 NDC中 看向(z轴) 正向. 由于 glFrustum() 对由进到远只接受正值, 我们需要在构造 GL_PROJECTION 矩阵时 对他们取反.
截锥体平截头体和标准化设备坐标
在OpenGL在, 在 eye space 中的三维点 会被投影到 near plane(projection plane) (近平面(投影平面)) 上, 下面这张图显示了如何将 (p(x_e,y_e,z_e)) 投影到 (p(x_p,y_p,z_p)) 上.
视锥体的顶视图
视锥体的侧视图
在顶视图中,(x_e),eye space 中的的x坐标, 通过相似三角形的比值的映射到 (x_p);
(frac{x_p}{x_e} = frac{-n}{z_e})
(x_p = frac{-n*x_e}{z_e} = frac{n*x_e}{-z_e})
在侧视图中,(y_p)也是通过相似的方法计算出来的;
(frac{y_p}{y_e} = frac{-n}{z_e})
(y_p = frac{-n*y_e}{z_e} = frac{n*y_e}{-z_e})
注意(x_p和y_p)都与(z_e)有关; 与(-z_e)成反比即除以(-z_e). 这是构造 GL_PROJECTION 矩阵的第一条线索. 在 eye coordinates 与 GL_PROJECTION 矩阵相乘完成变换后, clip coordinates 依然是 homogeneous coordinates(齐次坐标). 最终将其除以他的w分量来得到NDC.(更多细节OpenGL Transformation)
因此,我们可以用 (-z_e) 作为 clip coordinates 的 w分量. 于是 GL_PROJECTION 矩阵的第四行变成了 ((0,0,-1,0).)
接下来,我们通过线性关系把 (x_p,y_p) 映射到 NDC中的(x_n,y_n)上; ([l,r] => [-1,1] 和 [b,t] => [-1,1])
把(x_p)映射到(x_n)
把(y_p)映射到(y_n)
然后把(x_p和y_p变量代换到上述式子中)
(x_n = frac{2x_p}{r-l}-frac{r+l}{r-l} (x_p = frac{nxe}{-z_e}))
(x_n = frac{2*frac{nx_e}{-z_e}}{r-l} - frac{r+l}{r-l})
(x_n = frac{2n*x_e}{(r-l)(-z_e)} - frac{r+l}{r-l})
(x_n = frac{frac{2n}{r-l}*x_e}{-z_e} - frac{r+l}{r-l})
(x_n = frac{frac{2n}{r-l}*x_e}{-z_e} + frac{frac{r+l}{r-l}*z_e} {-z_e})
(x_n = (frac{2n}{r-l}*x_e+frac{r+l}{r-l}*z_e)/-z_e = x_c / -z_e)
同理可得
(y_n = (frac{2n}{t-b}*y_e+frac{t+b}{t-b}*z_e)/-z_e = y_c/-z_e)
注意我们使每个方程的两个项都除以(-z_e) 来表示perspective division(透视除法)((x_c/w_c,y_c/w_c)). 而且我们之前已经把(w_c设成了-z_e)了, 所以括号内的项已经是 clip coordinates 的 (x_c和y_c)了.
于是得到了GL_PROJECTION 矩阵的前两行
现在我们只剩GL_PROJECTION 矩阵的第三行需要解出了. 但解(z_n)并不像其他坐标那样简单,因为 eye space 的(z_e)总是被投影到近平面的-n上. 但我们为了 clipping(裁剪)和 depth test(深度测试)需要保证z坐标的唯一性.,而且还要能够反投影(还原变换). 因为z并不依赖于x和y坐标,我们借用w分量 来找到 (z_n和z_e)之间的关系. 因此我们可以像下面这样来指定 GL_PROJECTION 矩阵的第三行.
在 eye space中, (w_e) 等于1. 因此等式变成
(z_n = frac{Az_e+B}{-z_e})
我们用((z_e,z_n))的关系来找到系数A和B;将((-n,-1)和(-f,1))回代到上式中
(left{egin{matrix}
frac{-An+B}{n} = -1\
frac{-Af+B}{f} = 1
end{matrix}
ight.
ightarrow
left{egin{matrix}
-An+B = -n ;(1)\
-Af+B = f ;(2)
end{matrix}
ight.)
重写等式(1);
(B=An-n ; (1'))
将B带入(2);
(-Af +(An-n) = f ; (2))
$-(f-n)A = f + n (
)A = -frac{f+n}{f-n}$
把A回代到(1)中;
((frac{f+n}{f-n})n + B = -n ; (1))
(B = -n - (frac{f+n}{f-n})n = -(1+frac{f+n}{f-n})n = -(frac{f-n+f+n}{f-n})n = -frac{2fn}{f-n})
解出A和B后就可以得到(Z_e和Z_n)的关系;
(Z_n = frac{-frac{f+n}{f-n}z_e - frac{2fn}{f-n}}{-z_e} ; (3))
最终解出了整个 GL_PROJECTION 矩阵
这个投影矩阵是一种通用形式,如果可视平截头体是对称的,即(r=-l 和 t=-b),可以对其化简
在继续之前,请观察一下等式(3)中(z_e和z_n)的关系,你会发现他们并不是线性关系而是分式关系,这意味着在近平面会有非常高的精度而远平面的精度很低.如果([-n,-f])的范围很大,就会导致深度精度问题(z-fighting(深度冲突)); 在远平面(z_e)值小改动不会影响到(z_n)的值. 所以(n和f)的距离应该越小越好从而减少深度缓冲的精度问题.
深度缓冲精度的比较
Orthographic Projection(正射投影)
为正射投影构造 GL_PROJECTION 矩阵 比透视模式下的要简单的多
正射视锥和标准化设备坐标(NDC)
eye space 中所有的(x_e,y_e和z_e)分量都线性映射到 NDC. 我们只需要把长方体视锥缩放正方体,然后把它移动到原点. 让我们来通过线性关系来解出 GL_PROJECTION 里的元素吧.
把(x_e映射到x_n)
把(y_e映射到y_n)
把(z_e映射到z_n)
由于在正射投影中不再需要w分量, GL_PROJECTION 矩阵的第四行 保留成(0,0,0,1), 于是可以得到 正射投影的 GL_PROJECTION 矩阵
同样的,如果视锥是对称的,即(r=-l 和 t=-b),可以对其化简