(翻译,图片也来自原文)

一、概述

绝大部分计算机的显示器是二维的(a 2D surface)。在OpenGL中一个3D场景需要被投影到屏幕上成为一个2D图像(image)。这称为投影变换(参见),需要用到投影矩阵(projection matrix)。

首先,投影矩阵会把所有顶点坐标从eye coordinates(观察空间,eye space或view space)变换到裁剪坐标(clip coordinated,属于裁剪空间,clip space)。然后,这些裁剪坐标被变换到标准化设备坐标(normalized device coordinates, NDC,即坐标范围在-1到1之间),这一步是通过用用裁剪坐标的\(w_c\)分量除裁剪坐标实现的

因此,我们要记住投影矩阵干了两件事: 裁剪clipping(即frustum culling,视景体剔除)和生成NDC。下文会讲述如何根据6个参数(left, right, bottom, top, near和far边界值)来构建投影矩阵。

注意视景体剔出(也即clipping)是在裁剪坐标下完成的,是早于用\(w_c\)(即上面提到的\(w\)分量,c表示clipping)除裁剪坐标的(它会生成NDC)。裁剪坐标\(x_c, y_c, z_c\)会与\(w_c\)进行比较。如果裁剪坐标比\(-w_c\)小或者比\(w_c\)大,则丢弃这个顶点(vertex)。即经裁剪后剩余的顶点的裁剪坐标满足:\(-w_c < x_c, y_c, z_c < w_c\)。OpenGL会成发生裁剪的地方生成新的边,如下图1,一个三角形经裁后,成了一个梯形,两条红色的边就是裁剪后新生成的。



(图1. 一个被视体裁剪的三角形)

一般常用的有透视投影和正交投影,相应地也就有两种投影矩阵。

二、透视投影(Perspective Projection)



(图2. 透视投影中的视景体和标准化设备坐标NDC)

在透视投影中,一个3D point是在一个截头锥体中(truncated pyramid frustum,上面图2左图,即一个棱台),会被映射到一个立方体(NDC坐标空间)中,x坐标范围从[1, r]变成了[-1, 1],y坐标范围从[b, t]变成了[-1, 1],z坐标从[-n, -f]变成了[-1, 1]。

注意在view space中(即eye coordinate),OpenGL使用的是右手坐标系(上面图2左图),但是在NDC中使用的是左手坐标系(上面图2右图)。这样的话,在view space中camera位于坐标原点看向-z轴,而在NDC中camera是看向+z轴的。上面图2中的n表示近裁剪面(near plane),是正值。因为glFrustum()接受的near、far的值是正的,所以在在构造投影矩阵时,要为它们取负(negate them)。

在OpenGL中,view space(又称为eye space)中的一个3D point被投影到近裁剪面(此处用近裁剪面作投影平面,projection plane)上。下图3和图4显示了eye space中的一个点\((x_e, y_e, z_e)\)是怎样被投影成近裁剪面上的一个点\((x_p, y_p, z_p)\)。



(图3. 视景体的俯视图)



(图4.视景体的侧视图)

从视景体的俯视图(图3)看,x轴坐标\(x_e\)被映射成为\(x_p\),而\(x_p\)可以根据三角形相似形计算出来:

\[\frac{x_p}{x_e}=\frac{-n}{z_e} \Longrightarrow x_p = \frac{-n\cdot x_e}{z_e}=\frac{n\cdot x_e}{-z_e}
\]

从视景体的侧视图(图4)看,可以用相似的方法计算出\(y_p\):

\[\frac{y_p}{y_e}=\frac{-n}{z_e} \Longrightarrow y_p = \frac{-n\cdot y_e}{z_e}=\frac{n\cdot y_e}{-z_e}
\]

注意\(x_p\)和\(y_p\)都依赖\(z_e\)并与\(-z_e\)成反比。这是构建投影矩阵的第一个线索。在eye coordinates被投影矩阵乘后,得到的裁剪坐标仍然是齐次坐标(homogeneous coordinates)。最终它需要除以裁剪坐标的w分量,才能变成标准化设备坐标(NDC)。

\[\left(
\begin{matrix}
x_{clip}\\
y_{clip}\\
z_{clip}\\
w_{clip}
\end{matrix}
\right) = M_{projection}\cdot \left(
\begin{matrix}
x_{eye}\\
y_{eye}\\
z_{eye}\\
w_{eye}
\end{matrix}
\right),
\left(
\begin{matrix}
x_{ndc}\\
y_{ndc}\\
z_{ndc}
\end{matrix}
\right)=\left(
\begin{matrix}
\frac{x_{clip}}{w_{clip}}\\
\frac{y_{clip}}{w_{clip}}\\
\frac{z_{clip}}{w_{clip}}
\end{matrix}
\right)
\]

因此,我们可以把裁剪坐标的w分量设置为\(-z_e\),则投影矩阵第4行变为(0, 0, -1, 0)。

\[\left(
\begin{matrix}
x_c\\
y_c\\
z_c\\
w_c
\end{matrix}
\right)=\left(
\begin{matrix}
\cdot & \cdot &\cdot &\cdot\\
\cdot & \cdot &\cdot &\cdot\\
\cdot & \cdot &\cdot &\cdot\\
0 & 0 & -1 & 0
\end{matrix}
\right) \left(
\begin{matrix}
x_e\\
y_e\\
z_e\\
w_e
\end{matrix}
\right),
\therefore w_c=-z_e
\]

接下来,我们把刚计算得到的\(x_p, y_p\)线性地(with linear relationship)映射到NDC中的\(x_n, y_n\)(这里的n表示NDC):\([l, r] \Rightarrow [-1, 1]\),\([b, t] \Rightarrow [-1, 1]\)。



(图5. 把\(x_p\)映射到\(x_n\))

因为\(x_p\)和\(x_n\)之间是线性映射关系,如图5,所以可设两者之间的映射函数为:

\[x_n = \frac{1-(-1)}{r-l}\cdot + \beta
\]

把\((x_p, x_n) = (r, l)\)代入上面方程得:

\[1 = \frac{2r}{r-l}+\beta
\]

所以

\[\begin{equation}
\begin{aligned}
\beta&=1 - \frac{2r}{r-l}=\frac{r-l}{r-l} - \frac{2r}{r-l}\\
&=\frac{r-l-2r}{r-l}=\frac{-r-l}{r-l}=-\frac{r+l}{r-l}
\end{aligned}
\end{equation}
\\
\therefore x_n=\frac{2x_p}{r-l}-\frac{r+l}{r-l}
\]

同理,可以求出\(y_p\)和\(y_n\)之间的关系表达式,如图6及以下公式:



(图6.把\(y_p\)映射到\(y_n\))

\[y_n = \frac{1-(-1)}{t-b}\cdot y_p + \beta
\]

用$ (y_p, y_n)=(t,1)$代入上式得

\[1 = \frac{2t}{t-b}+\beta\\

\begin{equation}
\begin{aligned}
\beta &= 1 - \frac{2t}{t-b} = \frac{t-b}{t-b} - \frac{2t}{t-b}\\
&=\frac{t-b-2t}{t-b}=\frac{-t-b}{t-b}=-\frac{t+b}{t-b}
\end{aligned}
\end{equation}
\\
\therefore y_n=\frac{2y_p}{t-b}-\frac{t+b}{t-b}
\]

接下来,把上上面求得的\(x_p=\frac{nx_e}{-z_e}\)和\(y_p=\frac{ny_e}{-z_e}\)代入刚刚求到的线性关系式得:

\[\begin{equation}
\begin{aligned}
x_n &= \frac{2x_p}{r-l}-\frac{r+l}{r-l}\\
&= \frac{2\cdot \frac{n\cdot x_e}{-z_e}}{r-l}-\frac{r+l}{r-l}\\
&= \frac{2n\cdot x_e}{(r-l)(-z_e)} - \frac{r+l}{r-l}\\
&= \frac{\frac{2n}{r-l}\cdot x_e}{-z_e} - \frac{r+l}{r-l}\\
&= \frac{\frac{2n}{r-l}\cdot x_e}{-z_e} + \frac{\frac{r+l}{r-l}\cdot z_e}{-z_e}\\
&= \left. \left(\underbrace{\frac{2n}{r-l}\cdot x_e + \frac{r+l}{r-l}\cdot z_e}_{x_c}\right) \middle/ (-z_e) \right.
\end{aligned}
\end{equation}
\]
\[\begin{equation}
\begin{aligned}
y_n &= \frac{2y_p}{t-b} - \frac{t+b}{t-b}\\
&= \frac{2\cdot \frac{n\cdot y_e}{-z_e}}{t-b} - \frac{t+b}{t-b}\\
&= \frac{2n\cdot y_e}{(t-b)(-z_e)} - \frac{t+b}{t-b}\\
&= \frac{\frac{2n}{t-b}\cdot y_e}{-z_e} - \frac{t+b}{t-b}\\
&= \frac{\frac{2n}{t-b}\cdot y_e}{-z_e} + \frac{\frac{t+b}{t-b}\cdot z_e}{-z_e}\\
&= \left. \left(\underbrace{\frac{2n}{t-b}\cdot y_e + \frac{t+b}{t-b}\cdot z_e}_{y_c}\right) \middle/ (-z_e) \right.
\end{aligned}
\end{equation}
\]

注意上面刚刚求得的\(x_n, y_n\)是NDC坐标,而NDC应该是由裁剪坐标除以\(w_c\)得到,也即透视除法(perspective division), \((x_c/w_c, y_c/w_c)\)。又因为,之前我们把\(w_c\)的值设置为\(-z_e\),所以上面\(x_n, y_n\)表达式中括号里的部分表示裁剪空间的坐标\(x_c, y_c\)。

加上上面的两个方程,我们可以找到投影矩阵的第1行和第2行:

\[\begin{equation}
\left(
\begin{matrix}
x_c\\
y_c\\
z_c\\
w_c
\end{matrix}
\right)
=\left(
\begin{matrix}
\frac{2n}{r-l} & 0 & \frac{r+l}{r-l} & 0\\
0 & \frac{2n}{t-b} & \frac{t+b}{t-b} & 0\\
\cdot & \cdot & \cdot & \cdot\\
0 & 0 & -1 & 0
\end{matrix}
\right)
\left(
\begin{matrix}
x_e\\
y_e\\
z_e\\
w_e
\end{matrix}
\right)
\end{equation}
\]

现在矩阵只剩下第三行是待求解的。在eye space中\(z_e\)总是被投影到近裁剪面(near plane)上,即值总是为-n。但是我们为了完成裁剪(clipping)和深度测试(depth test),每一个顶点应该具有不同的z值。此外,投影变换应该是可逆的。既然我们知道z不依赖于x和y的值,那么我们就借用w分量来找到\(z_n\)和\(z_e\)之间的关系。因此,我们可以指定第三行长这样:

\[\begin{equation}
\left(
\begin{matrix}
x_c\\
y_c\\
z_c\\
w_c
\end{matrix}
\right)
=\left(
\begin{matrix}
\frac{2n}{r-l} & 0 & \frac{r+l}{r-l} & 0\\
0 & \frac{2n}{t-b} & \frac{t+b}{t-b} & 0\\
0 & 0 & A & B\\
0 & 0 & -1 & 0
\end{matrix}
\right)
\left(
\begin{matrix}
x_e\\
y_e\\
z_e\\
w_e
\end{matrix}
\right)
\end{equation},
z_n=z_c/w_c=\frac{Az_e + Bw_e}{-z_e}
\]

因为在eye space中,\(w_e\)总是等于1,因此:

\[z_n = \frac{Az_e + B}{-z_e}
\]

(注意,\(w_c = -z_e, w_e=1\)别搞混淆了)

为了找到系数A和B,我们把\((z_e, z_n)\)之间的关系: (-n, -1)和(-f, 1),代入上面这个等式中,得到:

\[\begin{equation}
\left\{
\begin{array}{lr}
\frac{-An+B}{n} = -1 & \\
\frac{-Af+B}{f} = 1 &
\end{array}
\right.
\end{equation}
\\
\Downarrow
\]
\[\begin{equation}
\left\{
\begin{array}{lr}
-An + B = -n & (1)\\
-Af + B = f & (2)
\end{array}
\right.
\end{equation}
\]

由方程(1)可得:

\[\begin{equation}
\begin{array}{lr}
B=An-n & (1')
\end{array}
\end{equation}
\]

把方程(1')代入到方程(2),可解出A:

\[\begin{equation}
\begin{array}{lr}
-Af + (An-n) = f & (2')\\
-(f-n)A=f+n &\\
A=-\frac{f+n}{f-n}&
\end{array}
\end{equation}
\]

把A的值代入方程(1')可求得B:

\[\begin{equation}
\begin{aligned}
B &=-n - \left(\frac{f+n}{f-n}\right)n=-\left(1+\frac{f+n}{f-n}\right)n\\
&= -\frac{2fn}{f-n}
\end{aligned}
\end{equation}
\]

有了A和B,则\(z_e\)和\(z_n\)之间的关系表达式为:

\[\begin{equation}
\begin{aligned}
z_n = \frac{-\frac{f+n}{f-n}z_e - \frac{2fn}{f-n}}{-z_e} &\quad (3)
\end{aligned}
\end{equation}
\]

最后,完整的投影矩阵为:

\[\left(
\begin{matrix}
\frac{2n}{r-l} & 0 & \frac{r+l}{r-l} & 0\\
0 & \frac{2n}{t-b} & \frac{t+b}{t-b} & 0\\
0 & 0 & \frac{-(f+n)}{f-n} & \frac{-2fn}{f-n}\\
0 & 0 & -1 & 0
\end{matrix}
\right)
\]

上面这是一个通用视景体的投影矩阵。当视景体是对称时,即r=-l, t=-b,则:

\[\begin{equation}
\left\{
\begin{array}{lr}
r+l=0\\
r-l=2r
\end{array}
\right.
\end{equation}
\]
\[\begin{equation}
\left\{
\begin{array}{lr}
t+b=0\\
t-b=2t
\end{array}
\right.
\end{equation}
\]

故投影矩阵可以简化为:

\[\left(
\begin{matrix}
\frac{n}{r} & 0 & 0 & 0\\
0 & \frac{n}{t} & 0 & 0\\
0 & 0 & \frac{-(f+n)}{f-n} & \frac{-2fn}{f-n}\\
0 & 0 & -1 & 0
\end{matrix}
\right)
\]

透视投影矩阵我们已经求出来了,在继续往下探讨之前,请再看一下上面的方程(3),即:

\[\begin{equation}
\begin{aligned}
z_n = \frac{-\frac{f+n}{f-n}z_e - \frac{2fn}{f-n}}{-z_e} &\quad (3)
\end{aligned}
\end{equation}
\]

可以看到它是一个有理函数(rational function),且是一个非线性函数。这意味着在近裁剪面(near plane)附近,它具有很高的精度(very high precision),而在远裁剪面(far plane)附近具有非常小的精度(very little precision)。如果[-n, -f]的范围比较大,它会造成深度值精度问题(z-fighting),即可能在离far plane比较近的地方,当\(z_e\)的值差异较小时,它们对应的\(z_n\)值相同,或者说当一个\(z_e\)值发生小的变化时,对应的\(z_n\)不受影响(即值不变)。这会产生错误的视觉效果。如下面图7所示,在远裁剪面附近,\(z_n\)的值几乎不随\(z_e\)发生变化。



(图7. 深度缓存的精度比较)

一些避免z-fighting的方法:

  • 首先也是最重要的技巧是不要把物体放的太近。即使是视觉效果上贴在一块的物体,也可以把它们稍微分开一点,只要肉眼看不到即可。
  • 把近裁剪面设置的尽可能远。因为上面说过,离近裁剪面近的地方,精度会高。但这样可能造成离camera很近的物体被裁剪掉。这需要大量实验才能找到适合的距离。
  • 尽量缩短n和f之间的距离。这和上一条其实一样。
  • 使用更高精度的depth buffer。现在一般depth bufer中depth value使用16, 24或32 bit的flotas。大部分系统使用的是24 bits的floats。因此可以改成使用32 bits的depth buffer。但这样会增加一点性能负担。

三、正交投影

构建正交投影矩阵相对来说会简单一些。



(图8. 正交投影视景体及对应的NDC)

在eye space中,所有\(x_e, y_e, z_e\)分量是线性映射到NDC中的。我们只需要把一个长方体(rectangular volume)所表达的体积缩放成一个立方体(cube),并把它移动到原点(如图8)。下面我们将使用线性映射关系(linear relationship)来找到正交投影矩阵的各个元素。



(图9. 把\(x_e\)映射到\(x_n\))

\[\begin{equation}
\begin{aligned}
x_n &= \frac{1-(-1)}{r-l}\cdot x_e + \beta\\
1&=\frac{2r}{r-l} + \beta, (substitute (r, 1) for (x_e, x_n))\\
\beta &= 1 - \frac{2r}{r-l}=-\frac{r+l}{r-l}\\
\therefore x_n &= \frac{2}{r-l}\cdot x_e - \frac{r+l}{r-l}
\end{aligned}
\end{equation}
\]



(图10. 把\(y_e\)映射到\(y_n\))

\[\begin{equation}
\begin{aligned}
y_n &= \frac{1-(-1)}{t-b}\cdot y_e + \beta\\
1 &= \frac{2t}{t-b}+\beta, (substitute (t, 1) for (y_e, y_n))\\
\beta &= 1 - \frac{2t}{t-b} = -\frac{t+b}{t-b}\\
\therefore y_n &= \frac{2}{t-b}\cdot y_e - \frac{t+b}{t-b}
\end{aligned}
\end{equation}
\]



(图11. 把\(z_e\)映射到\(z_n\))

\[\begin{equation}
\begin{aligned}
z_n &= \frac{1-(-1)}{-f-(-n)}\cdot z_e + \beta\\
1 &=\frac{2f}{f-n} + \beta, (substitute (-f, 1) for (z_e, z_n))\\
\beta &= 1 - \frac{2f}{f-n}=-\frac{f+n}{f-n}\\
\therefore z_n &= \frac{-2}{f-n}\cdot z_e - \frac{f+n}{f-n}
\end{aligned}
\end{equation}
\]

因为对于正交投影w分量不是必须的,所以正交投影矩阵的第4行为(0, 0, 0, 1)。因此完整的正交投影矩阵为:

\[\left(
\begin{matrix}
\frac{2}{r-l} & 0 & 0 & -\frac{r+l}{r-l}\\
0 & \frac{2}{t-b} & 0 & -\frac{t+b}{t-b}\\
0 & 0 & \frac{-2}{f-n} & -\frac{f+n}{f-n}\\
0 & 0 & 0 & 1
\end{matrix}
\right)
\]

如果视景体对称的话,即r=-l, t=-b, 则:

\[\begin{equation}
\left\{
\begin{array}{lr}
r+l=0 \\
r-l=2r
\end{array}
\right.
\end{equation}
\]
\[\begin{equation}
\left\{
\begin{array}{lr}
t+b=0 \\
t-b=2r
\end{array}
\right.
\end{equation}
\]

故正交投影矩阵被简化为:

\[\left(
\begin{matrix}
\frac{1}{r} & 0 & 0 & 0\\
0 & \frac{1}{t} & 0 & 0\\
0 & 0 & \frac{-2}{f-n} & -\frac{f+n}{f-n}\\
0 & 0 & 0 & 1
\end{matrix}
\right)
\]

首发于我的知乎专栏

References:

OpenGL投影矩阵(Projection Matrix)构造方法的更多相关文章

  1. OpenGL投影矩阵【转】

    OpenGL投影矩阵 概述 透视投影 正交投影 概述 计算机显示器是一个2D平面.OpenGL渲染的3D场景必须以2D图像方式投影到计算机屏幕上.GL_PROJECTION矩阵用于该投影变换.首先,它 ...

  2. 【脚下生根】之深度探索安卓OpenGL投影矩阵

    世界变化真快,前段时间windows开发技术热还在如火如荼,web技术就开始来势汹汹,正当web呈现欣欣向荣之际,安卓小机器人,咬过一口的苹果,winPhone开发平台又如闪电般划破了混沌的web世界 ...

  3. OpenGL投影矩阵

    概述 透视投影 正交投影 概述 计算机显示器是一个2D平面.OpenGL渲染的3D场景必须以2D图像方式投影到计算机屏幕上.GL_PROJECTION矩阵用于该投影变换.首先,它将所有定点数据从观察坐 ...

  4. 关于Opengl投影矩阵

    读 http://www.songho.ca/opengl/gl_projectionmatrix.html 0.投影矩阵的功能: 将眼睛空间中的坐标点 [图A的视椎体]     映射到     一个 ...

  5. DX与OpenGL投影矩阵的区别

    之前学习DX和OpenGL时到是知道一点,但是没仔细研究过,只是跟着教程抄个公式就过了,看双API引擎时发现转换时是个问题,必须搞懂,gamedev上找了个解释,希望用得上. https://www. ...

  6. OpenGL中两种计算投影矩阵的函数

    OpenGL无意间同时看到两种创建投影矩阵的写法,可以说它们完成的是同样的功能,但写法完全不同,可以观摩一下什么叫做异曲同工之妙... 第一种: gltMakeShadowMatrix函数是重点 // ...

  7. OpenGL中投影矩阵基础知识

    投影矩阵元素Projection Matrix 投影矩阵构建: 当f趋向于正无穷时: 一个重要的事实是,当f趋于正无穷时,在剪裁空间中点的z坐标跟w坐标相等.计算方法如下: 经过透视除法后,z坐标变为 ...

  8. Android OpenGL ES(六)----进入三维在代码中创建投影矩阵和旋转矩阵

    我们如今准备好在代码中加入透视投影了. Android的Matrix类为它准备了两个方法------frustumM()和perspectiveM(). 不幸的是.frustumM()的个缺陷,它会影 ...

  9. [OpenGL](翻译+补充)投影矩阵的推导

    1.简介 基本是翻译和补充 http://www.songho.ca/opengl/gl_projectionmatrix.html 计算机显示器是一个2D的平面,一个3D的场景要被OpenGL渲染必 ...

随机推荐

  1. 在 Spring Boot 配置 Kafka 安全认证

    spring: kafka: bootstrap-servers: IP:端口 listener: missing-topics-fatal: false properties: sasl: mech ...

  2. 用java以正确的姿势刷CSP

    许多程序算法考试中,用java是个不错的选择,它几乎实现了所有c++能实现的,所以越来越受Acmer的欢迎.总结一下用到的一些技巧和方法.更多关于csp的可参考海岛blog|皮卡丘 1. 输出 规格化 ...

  3. .pfx和.Cer 证书

    通常情况下,作为文件形式存在的证书一般有三种格式: 第一种:带有私钥的证书,由Public Key Cryptography Standards #12,PKCS#12标准定义,包含了公钥和私钥的二进 ...

  4. Spark3.0中Dates和Timestamps

    Spark3.0使用的是预公历,而之前都是儒略历和公历的混合(即1582年之前的日期使用儒略历,1582年之后使用公历,java.sql.Date这个API用的就是这种,而Java8里使用java.t ...

  5. Java模拟表单POST上传文件

    JAVA模拟表单POST上传文件 import java.awt.image.BufferedImage;import java.awt.image.ColorModel;import java.io ...

  6. 搞定MySQL安装难安装贵问题

    背景 本方案解决了windows下安装MySQL过程繁琐的问题. 是真正的免安装绿色方法,不用配环境变量,不用执行install命令,不用配置my.ini文件. 步骤 下载 下载mysql-8.0.2 ...

  7. 第二篇:docker 简单入门(二)

    本篇目录 写在最前面的话 最常用的docker命令 获取远程仓库镜像 写在最前面的话 如上图大家看到的这样,以后此类文章请到其他平台查阅,由于博客园提示说,内容太多简单,所以以后简单的内容我会放在cs ...

  8. Greenplum 性能优化之路 --(二)存储格式

    一.存储格式介绍 Greenplum(以下简称 GP)有2种存储格式,Heap 表和 AO 表(AORO 表,AOCO 表). Heap 表:这种存储格式是从 PostgreSQL 继承而来的,目前是 ...

  9. css精髓:这些布局你都学废了吗?

    前言 最近忙里偷闲,给自己加油充电的时候,发现自己脑海中布局这块非常的凌乱混杂,于是花了一些时间将一些常用的布局及其实现方法整理梳理了出来,在这里,分享给大家. 单列布局 单列布局是最常用的一种布局, ...

  10. 一文搞懂Java引用拷贝、浅拷贝、深拷贝

    微信搜一搜 「bigsai」 专注于Java和数据结构与算法的铁铁 文章收录在github/bigsai-algorithm 在开发.刷题.面试中,我们可能会遇到将一个对象的属性赋值到另一个对象的情况 ...