计算机显示器是一个2D平面。OpenGL渲染时必须将一个3D的场景投影在屏幕上成为一个2D的图片。GL_PROJECTION矩阵的就是用作这种投影变换。首先,其将所有的顶点数据从视觉坐标系(眼坐标系)转化到裁剪空间。然后,通过除以裁剪坐标系中的w分量,将转化后的裁剪坐标变换成标准设备坐标系(NDC)。
因此,我们必须记住GL_PROJECTION 集成了裁剪和转化成标准设备坐标系的功能。下面的模块讲述了,如何从以下6个参数建立投影矩阵:left、right、bottom、top、near和far等边界值。
需要注意的是,裁剪空间中的平头截体的裁剪过程发生在,除以w c w c 分量之前。将裁剪坐标 x c x c , y c y c 和z c z c 和w c w c 进行比较,如果其中任意一个值小于w c w c 或者大于w c w c ,那么这个顶点将会被裁剪掉,渲染时会丢弃这个顶点。
− w c < x c , y c , z c < w c − w c < x c , y c , z c < w c
当裁剪发生时,OpenGL会重新构建裁剪的边界和边线。
透视投影#
在透视投影时,裁剪截体是一个锥形的,然后被映射到标准设备坐标系中(一个立方体),NDC的x、y和z轴的范围都是从-1至+1。
注意,视觉坐标系是右手坐标系,而NDC使用的是左手坐标系,它们的z值方向是相反的,在视觉坐标系中,摄像机在原点,观察方向为z轴的负值方向,但是在NDC中,摄像机的观察方向却是z轴的正值方向。因此,glFrustum()的near和far参数必须大于0,所以在构建GL_PROJECTION矩阵时,我们必须对它们取反。
在 OpenGL 中,视觉空间中的 3D 点被投影到近平面(投影平面)上。下图显示了视觉空间中的一个点 ( x e , y e ( x e , y e , z e z e ) 是如何在近平面上投影到 ( x p , y p , z p ) ( x p , y p , z p ) 的。
从视雉体的顶视图,眼睛空间的 x x 坐标, x e x e 映射到 x p x p ,这是通过使用相似三角形的相似计算的;
x p x e = − n z e x p = − n ⋅ x e z e = n ⋅ x e − z e x p x e = − n z e x p = − n ⋅ x e z e = n ⋅ x e − z e
从视锥体的侧视图来看, y p y p 也以类似的方式计算;
y p y e = − n z e y p = − n ⋅ y e z e = n ⋅ y e − z e y p y e = − n z e y p = − n ⋅ y e z e = n ⋅ y e − z e
注意 x p x p 和 y p y p 都依赖于 z e z e ; 它们与 − z e − z e 呈反比。换句话说,它们都被 − z e − z e 除以。这是构建GLPROJECTION矩阵的第一个线索。当视觉坐标系的坐标通过乘以GL PROJECTION矩阵转化成裁剪坐标,转化后的裁剪坐标任然是一个齐次坐标 。最后通过除以w分量,转化成标准设备坐标系(NDC)。(可以在OpenGL变换 中了解更多细节。)
⎛ ⎜
⎜
⎜
⎜ ⎝ x clip y clip z clip w clip ⎞ ⎟
⎟
⎟
⎟ ⎠ = M p r o j e c t i o n ⋅ ⎛ ⎜
⎜
⎜
⎜ ⎝ x eye y eye z eye w eye ⎞ ⎟
⎟
⎟
⎟ ⎠ , ⎛ ⎜ ⎝ x ndc y ndc z ndc ⎞ ⎟ ⎠ = ⎛ ⎜ ⎝ x clip / w clip y clip / w clip z clip / w clip ⎞ ⎟ ⎠ ( x clip y clip z clip w clip ) = M p r o j e c t i o n ⋅ ( x eye y eye z eye w eye ) , ( x ndc y ndc z ndc ) = ( x clip / w clip y clip / w clip z clip / w clip )
因此,我们可以将剪辑坐标的 w w 分量设置为 − z e ∘ − z e ∘ 并且,矩阵GL_PROJECTION 4 变为 ( 0 , 0 , − 1 , 0 ) ( 0 , 0 , − 1 , 0 ) 。
⎛ ⎜
⎜
⎜ ⎝ x c y c z c w c ⎞ ⎟
⎟
⎟ ⎠ = ⎛ ⎜
⎜
⎜ ⎝ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ 0 0 − 1 0 ⎞ ⎟
⎟
⎟ ⎠ ⎛ ⎜
⎜
⎜ ⎝ x e y e z e w e ⎞ ⎟
⎟
⎟ ⎠ , ∴ w c = − z e ( x c y c z c w c ) = ( ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ 0 0 − 1 0 ) ( x e y e z e w e ) , ∴ w c = − z e
接下来,我们将 x p x p 和 y p y p 映射到具有线性关系的 N D C N D C 的 x n x n 和 y n y n 。[ 1 , r ] ⇒ [ − 1 , 1 ] [ 1 , r ] ⇒ [ − 1 , 1 ] 和 [ b , t ] ⇒ [ − 1 , 1 ] [ b , t ] ⇒ [ − 1 , 1 ]
然后,我们将 x p x p 和 y p y p 替换为上述方程。
请注意,我们使每个方程的两个项都可以被 − z e − z e 整除以进行透视除法 ( x C / w C , y C / w C ) ( x C / w C , y C / w C ) 。我们之前将 w c w c 设置为 − z e − z e , 括号内的术语将变为剪辑坐标的 x c x c 和 y c y c
通过这些等式,我们可以得出GL_PROJECTION矩阵的第一行和第二行。
⎛ ⎜
⎜
⎜ ⎝ x c y c z c w c ⎞ ⎟
⎟
⎟ ⎠ = ⎛ ⎜
⎜
⎜
⎜
⎜ ⎝ 2 n r − l 0 r + l r − l 0 0 2 n t − b t + b t − b 0 0 0 − 1 0 ⎞ ⎟
⎟
⎟
⎟
⎟ ⎠ ⎛ ⎜
⎜
⎜ ⎝ x e y e z e w e ⎞ ⎟
⎟
⎟ ⎠ ( x c y c z c w c ) = ( 2 n r − l 0 r + l r − l 0 0 2 n t − b t + b t − b 0 0 0 − 1 0 ) ( x e y e z e w e )
现在,我们只有第 3 行GL_PROJECTION矩阵要求解。找到 z n z n 与其他的有点不同,因为眼睛空间中的 z e z e 总是在近平面上投影到− n − n 。但是我们需要唯一的 z z 值来进行裁剪和深度测试。另外,我们应该能够取消投影 (逆变换) 它。 由于我们知道 z z 不依赖于 x x 或 y y 值,因此我们借用 w w 分量来查找 z n z n 和 z e z e 之间的关系。因此,我们可以像这样指定
⎛ ⎜
⎜
⎜ ⎝ x c y c z c w c ⎞ ⎟
⎟
⎟ ⎠ = ⎛ ⎜
⎜
⎜
⎜
⎜ ⎝ 2 n r − l 0 r + l r − l 0 0 2 n t − b t + b t − b 0 0 0 A B 0 0 − 1 0 ⎞ ⎟
⎟
⎟
⎟
⎟ ⎠ ⎛ ⎜
⎜
⎜ ⎝ x e y e z e w e ⎞ ⎟
⎟
⎟ ⎠ ( x c y c z c w c ) = ( 2 n r − l 0 r + l r − l 0 0 2 n t − b t + b t − b 0 0 0 A B 0 0 − 1 0 ) ( x e y e z e w e )
z n = z c / w c = A z e + B w e − z e z n = z c / w c = A z e + B w e − z e
在视觉空间中, w e w e 等于 1 。因此,等式变为;
z n = A z e + B − z e z n = A z e + B − z e
为了找到系数 A A 和 B B ,我们使用 ( z e , z n ) ( z e , z n ) 关系; ( − n , − 1 ) ( − n , − 1 ) 和 ( − f , 1 ) ( − f , 1 ) ,并将它们放入上面的等式中。
⎧ ⎨ ⎩ − A n + B n = − 1 − A f + B f = 1 → { − A n + B = − n − A f + B = f { − A n + B n = − 1 − A f + B f = 1 → { − A n + B = − n − A f + B = f
为了求解A 和B 的方程,重写方程(1)为B
B = A n − n B = A n − n
将方程(1')替换为方程(2)中的B, 然后求解A;
− A f + ( A n − n ) = f − ( f − n ) A = f + n A = − f + n f − n − A f + ( A n − n ) = f − ( f − n ) A = f + n A = − f + n f − n
将A 放入方程 (1) 中以查找B ;
( f + n f − n ) n + B = − n B = − n − ( f + n f − n ) n = − ( 1 + f + n f − n ) n = − ( f − n + f + n f − n ) n = − 2 f n f − n ( f + n f − n ) n + B = − n B = − n − ( f + n f − n ) n = − ( 1 + f + n f − n ) n = − ( f − n + f + n f − n ) n = − 2 f n f − n
我们找到了 A A 和 B B 。因此, z e z e 和 z n z n 之间的关系变为;
z n = − f + n f − n z e − 2 f n f − n − z e z n = − f + n f − n z e − 2 f n f − n − z e
最后,我们找到了GL_PROJECTION矩阵的所有条目。完整的投影矩阵是
⎛ ⎜
⎜
⎜
⎜
⎜
⎜ ⎝ 2 n r − l 0 r + l r − l 0 0 2 n t − b t + b t − b 0 0 0 − ( f + n ) f − n − 2 f n f − n 0 0 − 1 0 ⎞ ⎟
⎟
⎟
⎟
⎟
⎟ ⎠ ( 2 n r − l 0 r + l r − l 0 0 2 n t − b t + b t − b 0 0 0 − ( f + n ) f − n − 2 f n f − n 0 0 − 1 0 )
此投影矩阵适用于一般视雉体。如果观看体积是对称的,即 r = − l r = − l 和 t = − b t = − b ,则可以将其简化为;
{ r + l = 0 r − l = 2 r (width) , { t + b = 0 t − b = 2 t (height) { r + l = 0 r − l = 2 r (width) , { t + b = 0 t − b = 2 t (height)
⎛ ⎜
⎜
⎜
⎜
⎜ ⎝ n r 0 0 0 0 n t 0 0 0 0 − ( f + n ) f − n − 2 f n f − n 0 0 − 1 0 ⎞ ⎟
⎟
⎟
⎟
⎟ ⎠ ( n r 0 0 0 0 n t 0 0 0 0 − ( f + n ) f − n − 2 f n f − n 0 0 − 1 0 )
在我们继续之前,请再次看一下 z e z e 和 z n z n 之间的关系。您注意到它是一个有理函数,并且是 z e z e 和 z n z n 之间 的非线性关系。这意味着在近平面上具有非常高的精度,但在远端平面上的精度非常低。如果范围 [ − n , − f ] [ − n , − f ] 变大, 则会导致深度精度问题(z-fighting); z e z e 在远平面周围的微小变化不会影响 z n z n 值。所以n和f的差值尽量小,以尽量减少深度缓冲区精度问题。
正交投影#
眼球空间中所有的 x e 、 y e x e 、 y e 和 z e z e 分量都线性映射到 NDC。我们只需要将矩形体积缩放到立方体,然后将其移动到原点。让我们找出使用线性关系GL_PROJECTION的元素。
因为正射投影不需要w分量,所以矩阵的第四行仍然为(0, 0, 0, 1)。因此完整的GL_PROJECTION正射投影矩阵如下:
⎛ ⎜
⎜
⎜
⎜
⎜
⎜ ⎝ 2 r − l 0 0 − r + l r − l 0 2 t − b 0 − t + b t − b 0 0 − 2 f − n − f + n f − n 0 0 0 1 ⎞ ⎟
⎟
⎟
⎟
⎟
⎟ ⎠ ( 2 r − l 0 0 − r + l r − l 0 2 t − b 0 − t + b t − b 0 0 − 2 f − n − f + n f − n 0 0 0 1 )
如果观看体积对称,则可以进一步简化它, r = − l r = − l 并且 t = − b t = − b 。
{ r + l = 0 r − l = 2 r (width) , { t + b = 0 t − b = 2 t (height) { r + l = 0 r − l = 2 r (width) , { t + b = 0 t − b = 2 t (height)
结果
⎛ ⎜
⎜
⎜
⎜
⎜ ⎝ 1 r 0 0 0 0 1 t 0 0 0 0 − 2 f − n − f + n f − n 0 0 0 1 ⎞ ⎟
⎟
⎟
⎟
⎟ ⎠ ( 1 r 0 0 0 0 1 t 0 0 0 0 − 2 f − n − f + n f − n 0 0 0 1 )
相关参考#
OpenGL Projection Matrix (songho.ca)
3D图形学中的矩阵变换(三) | 王鹏飞 (pengfeixc.com)
观察空间到裁剪空间的投影矩阵推导 · JDreamHeart-知识汇总
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现