本文共 9670 字,大约阅读时间需要 32 分钟。
本文结合基础的显卡渲染流程,解释了如何推导透视矩阵,本文不涉及正交矩阵的计算。
如下图1所示,在一个3D物体的渲染过程中,需要进行多次的坐标空间的转换。其中前3次的坐标空间转换用到的变换矩阵分辨是World Matrix
(又名Model Matrix), View Matrix
, Projection Matrix
。这三次坐标空间的转换通常发生在vertex shader
中,以unity shader为例,在vertex shader
我们通常通过调用unity提供的UnityObjectToClipPos
函数完成这三次转换。而后面的两次转换,即Clip Space-> NDC Space-> Screen Space
是在从vertex shader到fragment shader
的裁剪插值过程中由硬件自动完成的。更准确地说,是在先进行了裁剪插值过程,然后进行了两次坐标空间转换的过程。 进行裁剪插值和坐标变换的过程是一个不可配置的过程,也就是说硬件会强制进行上述两次的坐标空间的转换。以unity shader为例,编程人员需要使用SV_POSITION
标记vertex shader
的一个float4类型的返回值来告诉硬件这个变量就是需要进行两次坐标空间转换的变量,请在裁剪插值时对其进行处理。所以在fragment shader
中读到的被SV_POSITION
标记的变量已经是在Screen Space
中了。
Model Matrix
和View Matrix
相对于理解Projection Matrix
的更简单一些,这主要是因为Model Space-> World Space-> View Space
的转换都是发生在3D坐标空间之间,而Project Matrix
本意是要完成从3D坐标空间到2D坐标空间的映射,即从View Space-> Clip Space
。但如龙书《3d_game_programming_with_DirectX11.pdf》5.6.3 所描述,矩阵这样一个转换形式不能够完整地表达出一个3D坐标空间到2D坐标空间的变换。所以整个投影变换被拆成了两部分,一部分由Projection Matrix
完成View Space
到Clip Space
的转换(Clip Space
还是一个3D坐标空间),另一个部分由硬件完成Clip Space
到NDC Space
的转换(NDC Space
是一个2D坐标空间了)。所以计算Projection Matrix
需要考虑到和其后续硬件工作的约定,这一定程度上增加了对Projection Matrix
的理解难度。为了更好地理解Projection Matrix
,下文将从数学原理入手依次说明投影变换到底想要做什么工作、为什么需要在坐标变换过程中引入硬件及硬件到底做了什么工作、和投影变换相关的一些应用问题。 通常情况下,使用View Space
中的一个截头椎体(Frustum)来描述用做投影变换的“相机”。DirectX和OpenGL在相机的朝向上略有不同:DirectX中所有3D坐标空间中都是用左手坐标系,DirectX中相机位于View Space
坐标原点,看向z轴
正方向;OpenGL中View Space
使用的是右手坐标系,其余都是左手坐标系,相机位于坐标原点,看向z轴
的负方向。这一小节中接下来提到的所有坐标变换都是在DirectX环境进行的。常用描述Frustum的方式有两种,我们这里只讨论如下图2所示的其中一种,即:近平面n,远平面f,沿着x轴负方向看过去的垂直夹角α,宽高比r。需要注意的是,①在View Space
中,近平面和远平面是和xy-平面
平行的,所以n、f是近平面和远平面到坐标原点的距离;②宽高比r是投影平面宽w和高度h的比值,也可以说是Frustum的宽高比,因为这两个比值是一样的,为了后续描述方便这里通一称为投影平面宽高比;③投影平面到坐标原点的距离为d,投影平面并不是Frustum的一部分,但在后续的计算中有着重要的作用。这样,通过四元组(n,f,α,r)
就能够唯一确定一个Frustum,也就能够进行View Space
到投屏平面的映射的了。这里之所以没有说是到NDC Space
的映射是因为在此处探讨数学原理的时候,我们只关心一个3D物体是怎么样被映射到一个2D平面上的。 至于NDC Space
、Screen Space
等探讨完数学原理之后其概念会顺势变得清晰可见了。
NDC Space
。 x ′ = x h 2 z tan ( α 2 ) w 2 = x h z w tan ( α 2 ) = x r z tan ( α 2 ) ∈ [ − 1, 1 ] 公式6 y ′ = y h 2 z tan ( α 2 ) h 2 = y z tan ( α 2 ) ∈ [ − 1, 1 ] 公式7 x'=\frac{\frac{xh}{2z\tan \left( \frac{\alpha}{2}\right)}}{\frac{w}{2}}=\frac{xh}{zw\tan \left( \frac{\alpha}{2} \right)}=\frac{x}{rz\tan \left( \frac{\alpha}{2}\right)} \in \left[-\text{1,}1\right] \quad \text{公式6} \\ y'=\frac{\frac{yh}{2z\tan \left(\frac{\alpha}{2} \right)}}{\frac{h}{2}}=\frac{y}{z\tan \left(\frac {\alpha}{2}\right)}\in \left[-\text{1,}1\right] \quad \text{公式7} x′=2w2ztan(2α)xh=zwtan(2α)xh=rztan(2α)x∈[−1,1]公式6y′=2h2ztan(2α)yh=ztan(2α)y∈[−1,1]公式7 仅仅知道了A点x,y坐标是如何映射还远远不够,因为Frustum中可能有多个点同时映射到投影平面上面的同一个点。试想一条View Space
中的从原点发出的穿过Frustum的射线,射线上面的所有点都会映射到投影平面上面的同一位置,这就需要知道映射到投影平面同一位置的所有Frustum点中,哪一个是在“最前面”的。最容易想到是存储View Space
中的点的z值,这样的直接存储z值或者z经过线性变换的buffer叫做W-Buffer
(大概因为z值一般存储在float4的w字段,所以叫w-buffer)。一些比较老的硬件支持W-Buffer
,比较新的硬件很多都不再支持W-Buffer
转而使用存储 1 z \frac{1}{z} z1或者 1 z \frac{1}{z} z1线性变换值的方式,即Z-Buffer
。这主要是因为W-Buffer
在View Space
中是线性的,但是在Screen Space
中不是线性的,而Z-Buffer
在View Space
中不是线性的,而在Screen Space
中是线性的,即下表 |Buffer类型|View Space|Screen Space| |-----| |W-Buffer|线性的|非线性的| |Z-Buffer|非线性的|线性的| Screen Space
中是线性的?线性的意义又是什么?为了弄懂这个问题,设有如下图5所示的在View Space
中的一个平行于z轴的线段PQ,线段PQ到Z的距离为 y 0 y_0 y0, A ( y 0 , z ) A(y_0,z) A(y0,z)是PQ上面一点, A ′ ( y ′ , d ) A'(y',d) A′(y′,d)是点A’在投影平面上的投影点。根据三角形相似性很容易知道
Screen Space
只是进行了缩放这样的线性变换,所以最终 1 z \frac{1}{z} z1在Screen Space
中是线性的。那么在Screen Space
中是线性的有什么意义呢?主要的意义还是能够简化硬件设计,节省成本。由于光栅化是在Screen Space
中进行的,在进行光栅化的时候,在 1 z \frac{1}{z} z1是线性的这样的条件下,硬件只需简单的进行一次线性插值就能够片元三角形顶点的 1 z \frac{1}{z} z1值求得三角形内部一点的 1 z \frac{1}{z} z1的值了。此外,在Shadow Map和Post Processing中一个具有Screen Space
线性的 1 z \frac{1}{z} z1也具有重大意义(?待考证)。 不管是Z-Buffer
还是W-Buffer
,用来存储深度信息的Buffer都被称作深度缓冲区(Depth-Buffer),深度缓冲区中存储的值叫做深度值。不同的硬件API设定的深度缓冲区的取值范围不同,譬如DirectX要求的范围是[0,1],即近平面n处的深度值是0,远平面f处的深度值是1;而OpenGL设定的深度缓冲区的取值范围是[-1,1]。在选用DirectX作为探讨环境的情况下,因为取值范围不同是不能够直接将 1 z ∈ [ 1 f , 1 n ] \frac{1}{z}\in \left[\frac{1}{f},\frac{1}{n}\right] z1∈[f1,n1]写入深度缓冲区的。为了适应DirectX关于深度缓冲区取值范围的约定,根据 1 z \frac{1}{z} z1在Screen Space
中是线性的属性,只需要对 1 z \frac{1}{z} z1做一次线性变换即可。那么最终设定点A坐标z到深度值的映射关系为 f ( 1 z ) = a + b 1 z f\left(\frac{1}{z}\right)=a+b\frac{1}{z} f(z1)=a+bz1,并且根据以上讨论已知,当z为n时 f ( n ) = 0 f\left(n\right)=0 f(n)=0,当z为f时 f ( f ) = 1 f\left(f\right)=1 f(f)=1。解二元一次方程组可以最终求得 a = f f − n 公 式 9 b = − n f f − n 公 式 10 f ( z ) = f f − n + − n f f − n 1 z 公 式 11 a=\frac{f}{f-n} \quad 公式9 \\ b=\frac{-nf}{f-n} \quad 公式10 \\ f\left(z \right)=\frac{f}{f-n}+\frac{-nf}{f-n}\frac{1}{z} \quad 公式11 a=f−nf公式9b=f−n−nf公式10f(z)=f−nf+f−n−nfz1公式11 上一小节中我们推导出了View Space
中的点A到NDC Space
是的点A’的坐标映射关系。但是因为View Space
到NDC Space
之间的转换不是一个线性转换,所以找不到有效的矩阵表达形式(只有线性变换才有矩阵表达形式,参照龙书3.1 - 3.4)。通过观察公式6、7、11可以看到,如果将转换因子提出一个公约数z,剩下的部分是一个线性的表达式!这也是目前实际应用的做法,即将公式6、7、11拆解成两个部分:①线性变换的部分:可以转换一个矩阵表达形式,约定这个矩阵叫做投影矩阵(Projection Matrix,简称P)。;②非线性变换的部分:非线性变换的部分就是除以z,并且非线性的部分会有硬件自动帮我们做,这个除以z的流程叫做齐次除法(homogeneous divide)*。
View Space
中的点A到Screen Space
中的点 A s A_s As的变换的。如下图6所示,点A通过右乘投影矩阵P被转换到Clip Space
中得到点 A p A_p Ap。P.m34的值为1,故而A的z值被复制到了 A p A_p Ap的第四个分量w上。此时 A p A_p Ap的四个坐标分量应该满足条件 − z ⩽ x p ⩽ z , − z ⩽ y p ⩽ z , 0 ⩽ z p ⩽ z , 0 ⩽ z -z\leqslant x_p\leqslant z,-z\leqslant y_p\leqslant z,0\leqslant z_p\leqslant z,0\leqslant z −z⩽xp⩽z,−z⩽yp⩽z,0⩽zp⩽z,0⩽z。如果 A p A_p Ap不满足这样的条件,就说明A不再Frustum中,那么 A p A_p Ap就会被裁剪掉(如果有必要会有新的恰好在裁剪边缘的点生成)。然后会对 A p A_p Ap做齐次除法,即对 A p A_p Ap的每一个分量都除以它的第四个分量,数值上来看就是A的z值,进而得到在NDC Space
中的点A’(x’,y’,z’,1)。点A’的前三个分量的取值范围是 x ′ ∈ [ − 1, 1 ] , y ′ ∈ [ − 1, 1 ] , z ′ ∈ [ 0 , 1 ] x'\in \left[-\text{1,}1\right],y'\in \left[-\text{1,}1\right],z'\in \left[0,1\right] x′∈[−1,1],y′∈[−1,1],z′∈[0,1]。随后一个被称作View Port Transform的线性变换将NDC Space
中的点A’转换成Screen Space
中的点 A s A_s As,然后硬件会对片元进行光栅化(也就是线性插值)操作。这些转换中,只有View Space
到Clip Space
的转换是通过软件(也就是Shader代码)控制的,后续的Clip Space-> NDC Space-> Screen Space
都是由硬件自动完成的,不可配置的过程。 了解了硬件工作原理就能够更好的理解投影矩阵P。譬如P.m34只能是1么?通过上文可以知道,P.m34可以是任何大于0的值。回想,DirectX在View Space
中使用的是左手坐标系,所以Frustum中的所有点的z值都是大于0的,需要在图6的裁剪阶段满足 0 ⩽ A p . w 0\leqslant A_p.w 0⩽Ap.w,就需要P.m34满足大于0的条件。此外还需要P的其他元素都乘上m34,这样在图6中的齐次除法阶段正好将m34约去得到正确的x’,y’,和z’。刚刚描述的m34(> 0)不为1的P如下所示 [ m 34 r tan ( α 2 ) 0 0 0 0 m 34 tan ( α 2 ) 0 0 0 0 f m 34 f − n m 34 0 0 − n f m 34 f − n 0 ] \left[\begin{matrix} \frac{m_{34}}{r\tan \left(\frac{\alpha}{2}\right)}& 0& 0& 0\\ 0& \frac{m_{34}}{\tan \left(\frac{\alpha}{2}\right)}& 0& 0\\ 0& 0& \frac{fm_{34}}{f-n}& m_{34}\\ 0& 0& \frac{-nfm_{34}}{f-n}& 0\\ \end{matrix}\right] ⎣⎢⎢⎢⎢⎡rtan(2α)m340000tan(2α)m340000f−nfm34f−n−nfm3400m340⎦⎥⎥⎥⎥⎤ 在OpenGL环境下,可以直接照搬上文中DirectX的推导的过程。不过需要注意的,OpenGL环境下View Space
使用的左手坐标系,并且OpenGL环境下使用坐标列向量左乘投影矩阵。在OpenGL环境下投影矩阵如下,同上理 m 34 m_{34} m34是一个小于0的事情,通常情况下取 -1。
转载地址:http://msdws.baihongyu.com/