RTR_Chapter_4 下

投影

  在真正渲染一个场景之前,场景中所有的相关物体都需要被投影到某个平面上,或者是某个简单空间中。在投影变换完成之后,才会进行裁剪操作和渲染操作。

  到目前为止,本章节中所涉及的诸多变换,并不会对齐次坐标的 w w w分量(第四个分量)产生影响;也就是说,点和向量在经过变换之后,仍然会保持它们的类型:点的 w w w分量为1,向量的 w w w分量为0。同时,这些变换矩阵的第四行(最底行)都是 ( 0 , 0 , 0 , 1 ) (0,0,0,1) (0,0,0,1)。而现在所要讨论的投影变换,将会涉及到对齐次坐标 w w w分量的修改。对于透视投影矩阵(perspective projection matrice)而言,其第四行包含了对向量和点类型的操作;而且在变换之后通常还需要进行均匀化(homogenization)过程,也就是说, w w w分量在透视投影变换之后可能并不为0或者1,因此需要将齐次坐标的每个分量都除以 w w w分量,这样才能获得非齐次的点坐标。而对于正交投影(orthographic projection),本小节首先会进行讨论这种投影方式,这种投影方式比较简单而且会经常使用,但它并不会对齐次坐标的 w w w分量产生影响。

  在本小节中,假设在经过观察变换之后,相机会看向负 z z z轴,同时 y y y轴指向上, x x x轴指向右,即一个标准的右手坐标系。有些书籍和一些图形API(例如DirectX)中,在这里会使用一个左手坐标系,即相机看向正 z z z轴。使用哪一种手性的坐标系都是可以的,最终也会生成相同的效果。

正交投影

 正交投影的一个特征是,投影之前的平行线,在投影之后仍然会保持平行。这也就意味着,当使用正交投影来观察一个场景的时候,无论场景中的物体距离相机多远,它们的大小都不会发生改变。矩阵 P o \mathbf{P}_o Po是一个简单的正交投影矩阵,它将点坐标的 x , y x,y x,y分量保持不变,然后将 z z z分量直接归零,即投影到平面 z = 0 z=0 z=0上。其具体形式如下:

P o = ( 1 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 ) (4.62) \mathbf{P}_{o}= \left(\begin{array}{cccc} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 \end{array}\right) \tag{4.62} Po= 1000010000000001 (4.62)

  图4.17展示了正交投影的效果。由于矩阵 P o \mathbf{P}_o Po的行列式为0,因此它是不可逆的,换句话说,将一个物体从三维空间变换到二维平面上,是没有办法将丢失的维度信息恢复的。使用这样的正交投影观察场景会遇到一个问题,那就是它会将在视口内的所有点(无论 z z z坐标是正数还是负数)都投影到投影平面上;通常会将点坐标的 z z z分量( x , y x,y x,y分量也有相应的限制,即视口的长宽)限制到一个区间内,一般使用 n n n(近裁剪平面,也叫做前平面或者hither)和 f f f(远裁剪平面,也叫做后平面或者yon)来进行表示。这里对可视空间进行了限制,这也是下一步变换的目的之一。

在这里插入图片描述

图4.17 由方程4.62生成的简单正交投影,图中展示了这个过程的三个观察视图。这个投影可以看作是观察者沿着负 z z z轴进行观察,这意味着这个投影操作实际上只是省略了(或者设置为零) z z z坐标,同时保持 x x x y y y坐标不变。请注意,位于平面 z = 0 z = 0 z=0两边的物体,都会被投影到投影平面上。

  通常会使用一个六元组( l , r , b , t , n , f l, r, b, t, n, f l,r,b,t,n,f)来描述一个正交投影矩阵,它们分别代表了左侧、右侧、底部、顶部、近裁剪平面以及远裁剪平面。这个矩阵会将代表可视空间的轴对齐包围盒(axis-aligned bounding box,简称AABB)转换为一个位于原点的轴对齐立方体。这个AABB的最小角是 ( l , b , n ) (l, b, n) (l,b,n),最大角是 ( r , t , f ) (r, t, f) (r,t,f)。这里需要额外注意的是,由于相机此时看向的是负 z z z轴,因此 n > f n > f n>f。而通常人们的直觉是:表示近距离的数值应当要比一个表示远距离的数值小,因此这里一般会让用户按照直观感受来设置远近裁剪平面的数值,然后在程序中再对它们进行相应的调整。

  在OpenGL中,这个轴对齐立方体的范围是从 ( − 1 , − 1 , − 1 ) (-1,-1,-1) (1,1,1) ( 1 , 1 , 1 ) (1, 1, 1) (1,1,1);而在DirectX中这个范围则是 ( − 1 , − 1 , 0 ) (-1,-1,0) (1,1,0) ( 1 , 1 , 1 ) (1, 1, 1) (1,1,1)。这个立方体被称为规则观察体(canonical view volume,CVV),此时所在的空间被称为规范化设备坐标系(normalized device coordinates,NDC;也叫做齐次裁剪空间)。整个变换过程如图4.18所示。将观察空间(view space)转换为NDC空间的原因是,这样可以使得裁剪操作更加高效,也使得裁剪操作有了统一的前置标准。

在这里插入图片描述

图4.18 将一个AABB转换为一个规则观察体。左侧图中的AABB首先会进行平移,使其几何中心位于原点处;然后再对AABB进行缩放,使其变成右图中的规则观察体。

  在转换为规则观察体之后,需要渲染的几何体顶点会被这个立方体所裁剪。位于立方体内的几何体最终会被保留,然后通过屏幕映射的方式,将剩余的正方形区域渲染到屏幕上。详细的正交投影矩阵如下所示:

P o = S ( s ) T ( t ) = ( 2 r − l 0 0 0 0 2 t − b 0 0 0 0 2 f − n 0 0 0 0 1 ) ( 1 0 0 − l + r 2 0 1 0 − t + b 2 0 0 1 − f + n 2 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 ) . (4.63) \begin{aligned} \mathbf{P}_{o}=\mathbf{S}(\mathbf{s}) \mathbf{T}(\mathbf{t}) &= \left(\begin{array}{cccc} \dfrac{2}{r-l} & 0 & 0 & 0 \\ 0 & \dfrac{2}{t-b} & 0 & 0 \\ 0 & 0 & \dfrac{2}{f-n} & 0 \\ 0 & 0 & 0 & 1 \end{array}\right) \left(\begin{array}{cccc} 1 & 0 & 0 & -\dfrac{l+r}{2} \\ 0 & 1 & 0 & -\dfrac{t+b}{2} \\ 0 & 0 & 1 & -\dfrac{f+n}{2} \\ 0 & 0 & 0 & 1 \end{array}\right) \\ &=\left(\begin{array}{cccc} \dfrac{2}{r-l} & 0 & 0 & -\dfrac{r+l}{r-l} \\[2mm] 0 & \dfrac{2}{t-b} & 0 & -\dfrac{t+b}{t-b} \\[2mm] 0 & 0 & \dfrac{2}{f-n} & -\dfrac{f+n}{f-n} \\[2mm] 0 & 0 & 0 & 1\end{array}\right) . \end{aligned}\tag{4.63} Po=S(s)T(t)= rl20000tb20000fn200001 1000010000102l+r2t+b2f+n1 = rl20000tb20000fn20rlr+ltbt+bfnf+n1 .(4.63)

  如方程4.63所示,整条投影矩阵 P o \mathbf{P}_{o} Po可以写成一个平移矩阵 T ( t ) \mathbf{T(t)} T(t)和一个缩放矩阵 S ( s ) \mathbf{S(s)} S(s)的组合,其中:

s = ( 2 / ( r − l ) , 2 / ( t − b ) , 2 / ( f − n ) ) , t = ( − ( r + l ) / 2 , − ( t + b ) / 2 , − ( f + n ) / 2 ) \mathbf{s}=(2 /(r-l), 2 /(t-b), 2 /(f-n)),\\[2mm] \mathbf{t}=(-(r+l) / 2,-(t+b) / 2,-(f+n) / 2) s=(2/(rl),2/(tb),2/(fn)),t=((r+l)/2,(t+b)/2,(f+n)/2)

  这个矩阵是可逆的,其逆矩阵为:

P o − 1 = T ( − t ) S ( ( r − l ) / 2 , ( t − b ) / 2 , ( f − n ) / 2 ) \mathbf{P}_{o}^{-1}=\mathbf{T}(-\mathbf{t}) \mathbf{S}((r-l) / 2,(t-b) / 2,(f-n) / 2) Po1=T(t)S((rl)/2,(tb)/2,(fn)/2)

当且仅当 n ≠ f n \ne f n=f l ≠ r l \ne r l=r t ≠ b t \ne b t=b;否则,不存在逆矩阵。

  在计算机图形学中,在投影变换之后通常会使用一个左手系,即视口的 x x x轴指向右方, y y y轴指向上方, z z z轴指向视口内侧。由于AABB的远裁剪平面 z z z值要比近裁剪平面小,因此正交投影通常还包会含一个镜像的变换操作。为了看到这个镜像变换操作,这里假设原始AABB和最终变换的规则观察体的尺寸是一样的,此时AABB的范围会从对应 ( l , b , n ) (l, b, n) (l,b,n) ( − 1 , − 1 , 1 ) (-1,-1,1) (1,1,1)到对应 ( r , t , f ) (r, t, f) (r,t,f) ( 1 , 1 , − 1 ) (1, 1, -1) (1,1,1)。将参数带入方程4.63,可得:

P o = ( 1 0 0 0 0 1 0 0 0 0 − 1 0 0 0 0 1 ) (4.64) \mathbf{P}_{o}= \left(\begin{array}{cccc} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & -1 & 0 \\ 0 & 0 & 0 & 1 \end{array}\right) \tag{4.64} Po= 1000010000100001 (4.64)

  此时的正交投影矩阵是一个镜像变换矩阵,正是这个镜像操作会将右手系的观察空间(相机看向负 z z z轴)变换为左手系的齐次裁剪空间。

  OpenGL会将点坐标的深度值( z z z分量)映射到 [ − 1 , 1 ] [-1,1] [1,1]中(不用进行额外处理),而DirectX则是将其映射到 [ 0 , 1 ] [0,1] [0,1]中。为了获得在DirectX中使用的正交投影矩阵,可以在正交投影变换之后,通过应用一个简单的缩放和平移矩阵来完成这个映射操作,即先将范围 [ − 1 , 1 ] [-1,1] [1,1]缩放为原来的一半即 [ − 0.5 , 0.5 ] [-0.5,0.5] [0.5,0.5],再将其沿着 z z z轴正方形平移0.5个单位到 [ 0 , 1 ] [0,1] [0,1]。这个缩放平移矩阵具体形式如下:

M s t = ( 1 0 0 0 0 1 0 0 0 0 0.5 0.5 0 0 0 1 ) (4.65) \mathbf{M}_{s t}=\left(\begin{array}{cccc}1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0.5 & 0.5 \\ 0 & 0 & 0 & 1\end{array}\right) \tag{4.65} Mst= 10000100000.50000.51 (4.65)

  现在将正交投影的变换矩阵,与这个对深度值进行缩放和平移的矩阵结合在一起,可以得到最终的正交投影变换矩阵,根据深度值映射的不同可能会有细微的区别,在DirectX中,这个矩阵的具体形式如下:

P o [ 0 , 1 ] = ( 2 r − l 0 0 − r + l r − l 0 2 t − b 0 − t + b t − b 0 0 1 f − n − n f − n 0 0 0 1 ) (4.66) \mathbf{P}_{o[0,1]}= \left(\begin{array}{cccc} \frac{2}{r-l} & 0 & 0 & -\frac{r+l}{r-l} \\[2mm] 0 & \frac{2}{t-b} & 0 & -\frac{t+b}{t-b} \\[2mm] 0 & 0 & \frac{1}{f-n} & -\frac{n}{f-n} \\[2mm] 0 & 0 & 0 & 1 \end{array}\right) \tag{4.66} Po[0,1]= rl20000tb20000fn10rlr+ltbt+bfnn1 (4.66)

  在DirectX中一般会使用这个矩阵的转置矩阵,因为DirectX使用了行优先(row-major)的规则来写入矩阵。

透视投影

  透视投影要比正交投影更加复杂,在计算机图形程序中也更加常用。在透视投影中,投影变换前的平行线在投影之后通常就不再平行了,相反,这些平行线可能会在它们的尽头汇聚成一个点。透视投影更加符合人眼观察这个世界的模式,因为它具有近大远小的特点。

  首先先从最简单的情况开始,推导投影到 z = − d , d > 0 z=-d,d>0 z=d,d>0平面上的透视投影矩阵。由于在投影变换之前还需要进行一次观察变换,这里为了简化这个转换过程的理解难度,所以直接在世界空间中进行推导。在这个最基本的推导完成之后,会将其扩展为更加常规的矩阵形式,例如OpenGL中所使用的透视投影矩阵。

  假设现在的相机位于坐标原点处,希望将点 p \mathbf{p} p投影到平面 z = − d , d > 0 z=-d,d>0 z=d,d>0上,最终生成一个新的顶点 q = ( q x , q y , − d ) \mathbf{q}=\left(q_{x}, q_{y},-d\right) q=(qx,qy,d),这个过程如图4.19所示。通过图中的相似三角形,可以推导出点 q \mathbf{q} q x x x分量,如下所示:

q x p x = − d p z ⟺ q x = − d p x p z . (4.67) \frac{q_{x}}{p_{x}}=\frac{-d}{p_{z}} \quad \Longleftrightarrow \quad q_{x}=-d \frac{p_{x}}{p_{z}}. \tag{4.67} pxqx=pzdqx=dpzpx.(4.67)
在这里插入图片描述

图4.19 上图是用于推导透视投影矩阵的几何符号。点 p \mathbf{p} p投影到平面 z = − d , d > 0 z=-d,d>0 z=d,d>0上,最终生成一个新的顶点 q \mathbf{q} q。投影是从相机的角度执行的,在本例中相机位于原点。使用右图中的相似三角形,可以推导出点 q \mathbf{q} q x x x分量。

  同理可以推导出点 q \mathbf{q} q的其他分量,如 q y = − d p y / p z q_{y}=-d p_{y} / p_{z} qy=dpy/pz q z = − d q_{z}=-d qz=d。将上述公式整合在一起,就可以获得这个透视投影矩阵 P p \mathbf{P}_p Pp,即:

P p = ( 1 0 0 0 0 1 0 0 0 0 1 0 0 0 − 1 / d 0 ) (4.68) \mathbf{P}_{p}= \left(\begin{array}{cccc} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & -1 / d & 0 \end{array}\right) \tag{4.68} Pp= 100001000011/d0000 (4.68)

  方程4.68中的透视变换矩阵可以产生正确的透视投影,具体方程如下:

q = P p p = ( 1 0 0 0 0 1 0 0 0 0 1 0 0 0 − 1 / d 0 ) ( p x p y p z 1 ) = ( p x p y p z − p z / d ) ⇒ ( − d p x / p z − d p y / p z − d 1 ) (4.69) \begin{array}{} \mathbf{q}=\mathbf{P}_{p} \mathbf{p}&= \left(\begin{array}{} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & -1 / d & 0 \end{array}\right) \left(\begin{array}{c} p_{x} \\ p_{y} \\ p_{z} \\ 1 \end{array}\right)\\[2mm] &=\left(\begin{array}{c}p_{x} \\ p_{y} \\ p_{z} \\ -p_{z} / d \end{array}\right) \Rightarrow \left(\begin{array}{c} -d p_{x} / p_{z} \\ -d p_{y} / p_{z} \\ -d \\ 1 \end{array}\right) \tag{4.69} \end{array}{} q=Ppp= 100001000011/d0000 pxpypz1 = pxpypzpz/d dpx/pzdpy/pzd1 (4.69)

  方程4.69中的最后一步,将新顶点的所有分量都除以 w w w分量(在这个例子中是 − p z / d -p_z /d pz/d),从而将 w w w分量设置为1。由于将顶点投影到了 z = − d , d > 0 z=-d,d>0 z=d,d>0上,因此新顶点的 z z z分量始终都为 − d -d d

  在直觉上,是很容易理解为什么齐次坐标可以用于表示投影操作的。这个齐次化过程(将 w w w分量设置为1)的一种几何解释是:这个过程会将点 ( p x , p y , p z ) \left(p_{x}, p_{y}, p_{z}\right) (px,py,pz)投影到 w = 1 w=1 w=1所在的平面上。

  与正交投影类似,透视投影并没有真正地将所有物体都投影到了一个平面上(这个过程是不可逆的),而是将视锥体变换成了一个规则观察体。在透视投影的时候,会假设视锥体从 z = n z=n z=n开始,并在 z = f z=f z=f结束,其中 0 > n > f 0>n>f 0>n>f。在 z = n z=n z=n这个平面上,视锥体的截面是一个长方形,其最小角(左下角)是 ( l , b , n ) (l, b, n) (l,b,n),最大角(右上角)是 ( r , t , f ) (r, t, f) (r,t,f),如图4.20所示。透视投影的整个过程,可以这样理解:首先将视锥体的远裁剪平面按一定规则,缩放到与近裁剪平面一样的尺寸,即将视锥体变成一个长方体;然后再按照正交投影的方式,将其变换成一个规则观察体。

在这里插入图片描述

图4.20 透视投影矩阵 P p \mathbf{P}_{p} Pp将视锥体转换为一个标准立方体,也叫做规则观察体。

  参数( l , r , b , t , n , f l, r, b, t, n, f l,r,b,t,n,f)决定了相机视锥体的范围,视野的水平视场角由视锥体的左右平面(即 l , r l, r l,r)所决定;同理视野的垂直视场角由视锥体的上下平面(即 b , t b, t b,t)所决定。视场角越大,相机能够看到的内容也就越多。我们也可以通过设定 r ≠ − l , t ≠ − b r \ne -l, t \ne -b r=l,t=b来构建一个不对称的视锥体,这种视锥体通常会用于3D立体观察(3D电影)和虚拟现实(VR)中。

  视场角(field of view,FOV)是提供场景感的重要因素,与电脑屏幕相比,眼睛本身就有一个物理上的视场角(人类单眼的水平视场角最大可达156度,双眼的水平视场角最大可达188度;人类两眼的重合视场角为124度,单眼的舒适视场角为60度;当集中注意力时,视场角约为25度。),其中水平视场角的计算方法为:

ϕ = 2 arctan ⁡ ( w / ( 2 d ) ) (4.70) \phi=2 \arctan (w /(2 d)) \tag{4.70} ϕ=2arctan(w/(2d))(4.70)

  其中 ϕ \phi ϕ是视场角, w w w是物体垂直于视线的宽度, d d d是物体到相机的距离。例如:一个25英寸(对角线距离)的显示器大约宽22英寸,如果这个显示器在距离12英寸远的地方,那么水平视场角应为 8 5 ∘ 85^{\circ} 85;20英寸远时水平视场角为 5 8 ∘ 58^{\circ} 58;30英寸远时水平视场角为 4 0 ∘ 40^{\circ} 40。这个视场角的计算公式也可以用于将相机镜头尺寸转换为视场角,例如:相机的感光元件宽$35mm $,镜头长 50 m m 50mm 50mm,则其视场角为 ϕ = 2 arctan ⁡ ( 36 / ( 2 ⋅ 50 ) ) = 39. 6 ∘ \phi=2 \arctan (36 /(2 \cdot 50))=39.6^{\circ} ϕ=2arctan(36/(250))=39.6

  如果使用比人眼物理视场角更小的视场角,会减弱透视的感觉,因为观察者在场景中的视野会被放大;如果使用一个更大的视场角的话,会使得场景中的物体看起来很扭曲(例如使用相机的广角镜头),尤其是在靠近屏幕边缘的地方,会夸大近距离物体的比例。然而,视场角越大,意味着视野越广阔,可以让观察者感觉看到的物体更大,更加令人印象深刻;其优势在于可以为用户提供更多的环境信息。

  使用透视投影矩阵来将视锥体转换为规则观察体,这个矩阵的具体形式如下:

P p = ( 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 ) (4.71) \mathbf{P}_{p}= \left(\begin{array}{cccc} \dfrac{2 n}{r-l} & 0 & -\dfrac{r+l}{r-l} & 0 \\[2mm] 0 & \dfrac{2 n}{t-b} & -\dfrac{t+b}{t-b} & 0 \\[2mm] 0 & 0 & \dfrac{f+n}{f-n} & -\dfrac{2 f n}{f-n} \\[2mm] 0 & 0 & 1 & 0 \end{array}\right) \tag{4.71} Pp= rl2n0000tb2n00rlr+ltbt+bfnf+n100fn2fn0 (4.71)

  在使用这个矩阵对一个点进行透视投影之后,会获得一个新的顶点 q = ( q x , q y , q z , q w ) T \mathbf{q}=\left(q_{x}, q_{y}, q_{z}, q_{w}\right)^{T} q=(qx,qy,qz,qw)T,这个新顶点的 w w w分量在变换之后通常并会在0-1之间,为了获得这个三维的投影点 q \mathbf{q} q,需要将该点的四个分量都除以 w w w分量,即:

p = ( q x q w , q y q w , q z q w , 1 ) (4.72) \mathbf{p}=\left( \frac{q_{x}}{q_{w}}, \frac{q_{y}}{q_{w}}, \frac{q_{z}}{q_{w}}, 1\right) \tag{4.72} p=(qwqx,qwqy,qwqz,1)(4.72)

  矩阵 P p \mathbf{P}_{p} Pp总会确保在平面 z = f z=f z=f上的点被映射到平面 z = + 1 z=+1 z=+1上;在平面 z = n z=n z=n上的点被映射到平面 z = − 1 z=-1 z=1上。

  超出近裁剪平面和远裁剪平面的物体会被裁剪,因此并不会出现在场景中。也可以将透视投影的远裁剪平面设置在无穷远处( f → ∞ f \rightarrow \infty f),那么方程4.71中的矩阵将变成如下形式:

P p = ( 2 n r − l 0 − r + l r − l 0 0 2 n t − b − t + b t − b 0 0 0 1 − 2 n 0 0 1 0 ) (4.73) \mathbf{P}_{p}=\left(\begin{array}{cccc}\dfrac{2 n}{r-l} & 0 & -\dfrac{r+l}{r-l} & 0 \\ 0 & \dfrac{2 n}{t-b} & -\dfrac{t+b}{t-b} & 0 \\ 0 & 0 & 1 & -2 n \\ 0 & 0 & 1 & 0\end{array}\right) \tag{4.73} Pp= rl2n0000tb2n00rlr+ltbt+b11002n0 (4.73)

  综上所述,在投影变换(包括正交投影和透视投影)之后,还会进行裁剪操作和齐次化操作(homogenization,坐标除以 w w w分量),最终将其转换到NDC空间中(规范化设备坐标系,normalized device coordinate)。

  为了获得可以在OpenGL中使用的透视变换矩阵,首先需要将方程4.71中的矩阵乘以 S ( 1 , 1 , − 1 , 1 ) \mathbf{S}(1,1,-1,1) S(1,1,1,1),这与正交投影中的操作类似,仅仅是将矩阵的第三列取反。在这个镜像操作之后,远近裁剪平面的值都会变为正数,即 0 < n ′ < f ′ 0<n^{\prime}<f^{\prime} 0<n<f,这也比较符合用户的直觉经验(距离越远,数字越大)。此时的 n ′ , f ′ n^{\prime},f^{\prime} n,f代表了远近裁剪平面沿观察方向(负 z z z轴)上的距离,下面是以供参考的OpenGL透视变换矩阵:

P OpenGL  = ( 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 ) . (4.74) \mathbf{P}_{\text {OpenGL }}= \left(\begin{array}{cccc} \dfrac{2 n^{\prime}}{r-l} & 0 & \dfrac{r+l}{r-l} & 0 \\[2mm] 0 & \dfrac{2 n^{\prime}}{t-b} & \dfrac{t+b}{t-b} & 0 \\[2mm] 0 & 0 & -\dfrac{f^{\prime}+n^{\prime}}{f^{\prime}-n^{\prime}} & -\dfrac{2 f^{\prime} n^{\prime}}{f^{\prime}-n^{\prime}}\\[2mm] 0 & 0 & -1 & 0 \end{array}\right). \tag{4.74} POpenGL = rl2n0000tb2n00rlr+ltbt+bfnf+n100fn2fn0 .(4.74)

  还有一个更简单的矩阵设置方法,即只提供垂直视场角 ϕ \phi ϕ和宽高比 a = w / h a=w / h a=w/h(其中 w : w i d t h w:width w:width h : h e i g h t h:height h:height,它们代表了屏幕的分辨率),以及取反之后的远近裁剪平面 n ′ , f ′ n^{\prime},f^{\prime} n,f,这样可以将上述方程简化改写为:

P OpenGL  = ( c / a 0 0 0 0 c 0 0 0 0 − f ′ + n ′ f ′ − n ′ − 2 f ′ n ′ f ′ − n ′ 0 0 − 1 0 ) , (4.75) \mathbf{P}_{\text {OpenGL }}= \left(\begin{array}{cccc} c / a & 0 & 0 & 0 \\[2mm] 0 & c & 0 & 0 \\[2mm] 0 & 0 & -\dfrac{f^{\prime}+n^{\prime}}{f^{\prime}-n^{\prime}} & -\dfrac{2 f^{\prime} n^{\prime}}{f^{\prime}-n^{\prime}} \\[2mm] 0 & 0 & -1 & 0 \end{array}\right), \tag{4.75} POpenGL = c/a0000c0000fnf+n100fn2fn0 ,(4.75)

  其中 c = 1.0 / tan ⁡ ( ϕ / 2 ) c=1.0 / \tan (\phi / 2) c=1.0/tan(ϕ/2),这个矩阵过去是使用 g l u P e r s p e c t i v e ( ) \mathsf{gluPerspective()} gluPerspective()函数生成的,它是OpenGL实用工具库的一部分(OpenGL Utility Library,简称GLU),但是现在已经过时了。

  有些图形API(例如DirectX)会将近裁剪平面映射到平面 z = 0 z=0 z=0上(而不是平面 z = − 1 z=-1 z=1上),同时将远裁剪平面映射到平面 z = 1 z=1 z=1上。另外,DirectX使用了左手坐标系来定义其投影矩阵(即观察空间是一个左手系),这意味着在DirectX中,相机在经过观察变换之后,会看向正 z z z轴,其远近裁剪平面的值都是正数。下面是DirectX中的透视投影矩阵:

P p [ 0 , 1 ] = ( 2 n ′ r − l 0 − r + l r − l 0 0 2 n ′ t − b − t + b t − b 0 0 0 f ′ f ′ − n ′ − f ′ n ′ f ′ − n ′ 0 0 1 0 ) (4.76) \mathbf{P}_{p[0,1]}= \left(\begin{array}{cccc}\dfrac{2 n^{\prime}}{r-l} & 0 & -\dfrac{r+l}{r-l} & 0 \\[2mm] 0 & \dfrac{2 n^{\prime}}{t-b} & -\dfrac{t+b}{t-b} & 0 \\[2mm] 0 & 0 & \dfrac{f^{\prime}}{f^{\prime}-n^{\prime}} & -\dfrac{f^{\prime} n^{\prime}}{f^{\prime}-n^{\prime}} \\[2mm] 0 & 0 & 1 & 0 \end{array}\right) \tag{4.76} Pp[0,1]= rl2n0000tb2n00rlr+ltbt+bfnf100fnfn0 (4.76)

  由于DirectX在其文档中使用行优先来表示变换矩阵,因此通常会使用这个矩阵的转置矩阵。

  透视投影带来的一个影响是,计算出的深度值并不会随着输入的 p z p_z pz值线性变化,使用方程4.74-4.76中的任意一个透视变换矩阵对点 p \mathbf{p} p进行变换,可以获得一个新的顶点 v \mathbf{v} v

v = P p = ( ⋯ ⋯ d p z + e ± p z ) (4.77) \mathbf{v}=\mathbf{P} \mathbf{p}=\left(\begin{array}{c}\cdots \\ \cdots \\ d p_{z}+e \\ \pm p_{z}\end{array}\right) \tag{4.77} v=Pp= dpz+e±pz (4.77)

  这里直接忽略了 v x , v y v_x,v_y vxvy,其中的常数 d , e d,e d,e取决于所使用的矩阵,如果我们使用方程4.74中的矩阵,则其中的常数为:

d = − ( f ′ + n ′ ) / ( f ′ − n ′ ) , e = − 2 f ′ n ′ / ( f ′ − n ′ ) , v w = − p z . d=-\left(f^{\prime}+n^{\prime}\right) /\left(f^{\prime}-n^{\prime}\right),\\[2mm] e=-2 f^{\prime} n^{\prime} /\left(f^{\prime}-n^{\prime}\right),\\[2mm] v_{w}=-p_{z}. d=(f+n)/(fn),e=2fn/(fn),vw=pz.

  为了将这个点转换到NDC空间中,需要让点 v \mathbf{v} v的各个分量除以 w w w分量,即:

z N D C = d p z + e − p z = d − e p z (4.78) z_{\mathrm{NDC}}=\frac{d p_{z}+e}{-p_{z}}=d-\frac{e}{p_{z}} \tag{4.78} zNDC=pzdpz+e=dpze(4.78)

  在OpenGL中, z N D C z_{\mathrm{NDC}} zNDC的范围是 [ − 1 , + 1 ] [-1,+1] [1,+1];从方程4.78中可以看出,输出的深度值 z N D C z_{\mathrm{NDC}} zNDC和输入的 p z p_z pz成反比。

  例如:如果此时用户设定的 n ′ = 10 , f ′ = 110 n^{\prime}=10,f^{\prime}=110 n=10,f=110,并且 p z p_z pz位于沿负 z z z轴60个单位时(即 n ′ , f ′ n^{\prime},f^{\prime} n,f的中点),那么此时该点的NDC坐标深度为0.833,而不是 [ − 1 , + 1 ] [-1,+1] [1,+1]的中点0。图4.21展示了随着近裁剪平面位置( n ′ n^{\prime} n)发生变化时,所对应NDC坐标的深度变化。

在这里插入图片描述

图4.21 随着近裁剪平面位置 ( n ′ ) (n^{\prime}) n发生变化时,所对应NDC坐标的深度变化。这里保持 f ′ − n ′ f^{\prime}-n^{\prime} fn的值为常数100。随着近裁剪平面距离原点越来越近,靠近远裁剪平面的点只占据了NDC深度空间的一小部分,这使得z-buffer在较远距离上的物体深度表示变得不那么精确。

  有一些方法可以用来提高深度值的精度,其中反向z-buffer(reversed z)是一种比较常用的方法,当使用浮点数据来表示z-buffer的时候,反向z-buffer会存储 1 − z N D C 1-z_{\mathrm{NDC}} 1zNDC的值;当使用整数来表示z-buffer的时候,反向z-buffer会反向存储 z N D C z_{\mathrm{NDC}} zNDC的值,图4.22展示了不同情况下的对比结果。Reed (网站为Depth Precision Visualized | NVIDIA Developer)通过模拟发现:使用浮点数的反向z-buffer可以提供最好的准确率;而反向z-buffer也是整数深度缓冲区(通常是24位整数)的首选方法。对于标准映射而言(即不使用反向z-buffer),正如Upchurch和Desbrun所建议的那样,在变换中使用分离的投影矩阵可以降低误差率。例如:相对于使用组合矩阵 T p , T = P M \mathbf{T} \mathbf{p},\mathbf{T}=\mathbf{P M} Tp,T=PM进行变换,最好是使用分离的矩阵 P ( M p ) \mathbf{P}(\mathbf{M p}) P(Mp)。同时,在 [ 0.5 , 1 ] [0.5,1] [0.5,1]的范围内,由于fp32的尾数(定点数)为23位,这使得fp32(32位浮点数)和int24(24位整型)具有相近的准确性。之所以让 z N D C z_{\mathrm{NDC}} zNDC 1 / p z 1 / p_{z} 1/pz成正比,是因为这样设计可以让硬件实现变得更加简单,同时还可以使得深度压缩变的效率更高。

在这里插入图片描述

图4.22 使用不同方法设置DirectX变换之后的深度缓冲,即 z N D C ∈ [ 0 , + 1 ] z_{\mathrm{NDC}} \in [0,+1] zNDC[0,+1]。左上角:使用标准的整数类型深度缓冲,这里使用了4位整数进行演示(因此y轴上有16个标记)。右上角:将远裁剪平面设置为 ∞ \infty x , y x,y x,y轴只发生了微小的移动,这意味着这样做并不会损失太多精度。左下角:使用了包含3个指数位和3个尾数位的浮点类型深度缓冲。可以看到浮点数在 y y y轴上的分布并不是均匀的,而在 x x x轴上这个拥挤现象会更加严重。右下角:使用了反向的浮点类型深度缓冲,即存储了 1 − z N D C 1-z_{\mathrm{NDC}} 1zNDC,其分布表现良好(均匀)。

  Lloyd 提出,可以使用深度值的对数来提高阴影贴图(shadow map)的精度。Lauritzen等人提出可以使用前一帧的z-buffer来确定当前帧的最大近裁剪平面和最小远裁剪平面。Kemen提出,对于屏幕空间中的深度,可以对每个顶点使用下列的重映射:

z = w ( log ⁡ 2 ( max ⁡ ( 1 0 − 6 , 1 + w ) ) f c − 1 ) , [  OpenGL]  z = w log ⁡ 2 ( max ⁡ ( 1 0 − 6 , 1 + w ) ) f c / 2 , [  DirectX  ] (4.79) \begin{array}{ll}z=w\left(\log _{2}\left(\max \left(10^{-6}, 1+w\right)\right) f_{c}-1\right), & {[\text { OpenGL] }} \\ z=w \log _{2}\left(\max \left(10^{-6}, 1+w\right)\right) f_{c} / 2, & {[\text { DirectX }]}\end{array} \tag{4.79} z=w(log2(max(106,1+w))fc1),z=wlog2(max(106,1+w))fc/2,[ OpenGL] [ DirectX ](4.79)

  其中 w w w是顶点经过投影矩阵变换之后,点坐标的 w w w分量, z z z是顶点着色器输出的 z z z值;方程中的常数 f c = 2 / log ⁡ 2 ( f + 1 ) f_{c}=2 / \log _{2}(f+1) fc=2/log2(f+1),其中 f f f是远裁剪平面的值。当这个变换(方程4.79)仅应用于顶点着色器中的,在GPU的光栅化阶段中,片元的深度值仍然会使用在三角形顶点的非线性深度之间进行线性插值获得。由于对数函数是一个单调函数,因此只要分段线性插值(piecewise linear interpolation)与精确的非线性插值之间,所获得的深度值差异很小,那么遮挡剔除硬件与深度压缩技术就仍然可以使用;在具有足够的曲面细分时,上述结论在大多数情况都是成立的。但是,上述方程也可以用于对每个片元进行变换,在顶点着色器中输出顶点 e = 1 + w e=1+w e=1+w的值,然后在光栅化阶段,让GPU在三角形上进行插值,从而获得其他片元的 e e e值。然后在像素着色器中使用公式 log ⁡ 2 ( e i ) f c / 2 \log _{2}\left(e_{i}\right) f_{c} / 2 log2(ei)fc/2来对片元的深度进行修改,其中 e i e_i ei是对三角形顶点上 e e e值进行插值获得的。当GPU不支持浮点类型的z-buffer,并且所渲染场景的深度很大时,这是一个很好的替代方法。

  Cozzi 提出可以使用多个视锥体,从而将精度提高到任何所期望的准确率。这个方法的核心思路是,将大的视锥体在深度方向上划分成若干个不重叠的小视锥体,这些小视锥体结合在一起就是原来的大视锥体。这些小视锥体会按照从后往前的顺序进行渲染,首先会清除颜色缓冲和深度缓冲,然后将所有需要渲染的物体,分类到与之重叠的每个小视锥体中;而对于每个小视锥体,则会生成各自的投影矩阵并清除自身的深度缓冲,然后渲染每个与小视锥体重叠的物体。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值