上一篇讨论了D3D/OGL如何将Shader输出的图元顶点坐标及属性转化为其所覆盖像素和像素的属性。本篇将继续地讨论光栅化其他步骤相关的细节。包括Face Cull,Fill Mode,Clipping, 透视矫正等。
对应显示存储中的一块数据(Surface),存储着用于画到屏幕上每个像素的颜色、深度等数据。
由一个二维矩形区域和一维的z范围构成,表示要显示渲染输出的区域,它是NDC立方体所映射的区域。例如RenderTarget大小为64*64,当设置Viewport左上角为(32,32),大小为32*32时,我们将在Render Target的右下角1/4的区域画图。
D3D和OGL的屏幕坐标系有所区别,D3D将NDC中y的范围[-1,1]映射到Viewport的[Viewport.Top+Viewport.Height, Viewport.Top],例如下图中(假设Viewport区域与Render Target相等)Viewport.Top=0,Viewport.Height=-1,NDC中y=-1映射到屏幕上y=8,y=1映射到屏幕上y=0。
由下图可知,D3D的坐标系原点位于屏幕的左上角,像素的长宽为1,像素中心的坐标小数位均为0.5。
下图是OGL的坐标系,其坐标原点位于屏幕的左下角(在新的OGL版本中,可通过Clip Control选择坐标原点为左上角或左下角),其余特征与D3D类似。
流水线模拟照相机成像的原理,将三维模型成像到二维屏幕上。一台选好焦距的照相机,将具有一定范围的其视野区域,同样流水线中具有“视椎体”,它由上,下,左,右,近,远6个平面围成,这些平面分别对应该方向所能观察到的范围边界。Raster首先确定一个图元是否完全落于视椎体外面,如果是则直接丢弃该图元(Frustum Cull),因为落在视椎体外面的图元经投影后不会覆盖屏幕上任何像素。
即使一些图元落在了视椎体内部,也有可能可以被提前丢弃,例如在一些封闭模型中,背面的三角形是看不见的。
容易知道,当模型被拆分后的三角形都具有相同的顶点绕向(即逆时针或顺时针)时,从固定角度观察时,其正面和背面的三角形绕向是相反的。因此,我们通过Area的符号可确定三角形是逆时针(CCW,Counter ClockWise)或顺时针(Counter ClockWise),再结合应用拆分模型中正面对应的绕向(这种对应关系称为Winding),最终确定三角形是否为Back Face。对应地,有些情况下要做Front Face Cull,例如镜头位于封闭物体的内部。利用Frustum Cull和Face Cull提前避免了不必要的渲染,可以提高硬件的性能。
在OGL中,可分别为Front Face和Back Face指定不同的Fill Mode。因此确定三角形Face后,可进一步确定其Fill Mode,Fill Mode包括三种:(1)Point,表示只渲染三角形的三个顶点(2)Wireframe,表示只渲染三条边(3)Solid,表示渲染整个三角形内部区域。
当图元通过了Frustum Cull和Face Cull。图元仍可能部分落在视椎体外。D3D/OGLRaster会通过六个面对图元进行裁剪,构造新的图元进行渲染。
需要注意的是,裁剪是在裁剪空间下(投影变换之前)做的,裁剪空间是四维的齐次空间下做的。上图仅是给出一个直观的几何概念,例如在齐次坐标系下做zNear/zFar clip时,分别是把z裁到z=-w和z=w,经过投影变换和viewport transform后才分别得到z=zNear和z=Far的顶点。
对于z而言,进行裁剪可保证准确性的,例如下面的例子,在裁剪坐标系中(在裁剪空间系下,z落在视椎体中相当于z落在z=W和z=-w两面围成的区域中),v0落在镜头后面,即z<0,经投影变换后将落在V’0,V1位于视椎体内,投影后落在V’1,这样我们看到的将是区域V’0 V’1。但事实上从镜头向z方向看时,看到的应该在V’1下方。
如果在投影变换前,对z做裁剪,
经过viewport transform之后,通常z的范围在[0, 1]或[-1,1]中(前者是D3D的范围,后者是OGL的常用范围)。与之不同,x/y的范围可以是任意大的float数,而考虑到精度上限制以及计算的效率等问题,Raster中计算边的方程使用具有具体范围的顶点数,例如D3D10的x/y使用定点格式s15.8(即一个符号位,15个整数位,8个小数位),这样最大的表示范围大约为[-2^15, 2^15],因此,将XY做裁剪可以保证得到这种范围。
伪代码:
4 Clipping: Left, Right, Bottom, Top. Example: Left Clipping For (each two vertices in BufIn) { if (v0_left_out && v1_ left_out) //here v0,v1 means first and second vertex of the pair Both are outside, continue for the next vertex pair; else if (v0_ left_out && v1_ left_in) { Swap v0 and v1, i.e. regard original v0 as v1 and regard original v1 as v0 Interpolate from v0 to v1 for each attribute: v’ = v0 + s(v1-v0); Put v’ into BufOut } else if (v0_ left_in && v1_ left_out) { Interpolate from v0 to v1 for each attribute: v’ = v0 + s(v1-v0); Put v0 and v’ into BufOut } else // (v0_ left_in && v1_ left_in) Both are inside, Put v0 into BufOut and continue; }
|
一个例子:
理论上,属性插值时用到的权重系数(三角形的重心坐标和线的插值系数)应该通过Eye Space坐标计算。但光栅化所处理的像素位于屏幕上,所使用的权重系数也是在屏幕坐标系下算出的。投影变换并非线性变换,使用上述两种插值权重系数所插值的结果往往是不同的。例如下面的例子
Eye Space中的两点V0,V1以及其连线上的点V,投影到屏幕上分别为V‘0,V’1和V‘。
在Eye Space下,
而在Screen Space下:
这样导致插值结果的失真称为“投影畸变”,当包含纹理坐标属性时其导致的失真更加明显,该那么如何处理(x,y)投影变换所引入的非线性的影响呢?
我们看一下OGL的specification,对于三角形,用(a,b,c)表示像素的重心坐标,则插值公式是:
可以看到,只有z是按照之前所说的方法直接插值的。下面较为复杂的插值公式是考虑了透视矫正的,但它是如何得来的呢?
我们首先看看,固定管线中投影变换和透视除法联合起来对于ZW做了什么:
投影变换
最终我们得到:
由于屏幕空间上的z是NDC空间的线性变换,这里暂且不区分它们,我们集中在插值的准确性上。
接下来,我们看两个重要结论并给出其证明
(1)1/Ze在NDC坐标系中可按线性插值。
假设裁剪空间和NDC空间下的插值系数分别为t和s,根据投影变换的几何特点可知:
把最后的等式表示成插值的计算式,并带入前面两式的结果,得
整理上面的等式,经过加减法并约去相同因式,可得到
从而
即
结论得证。
(2)P/Ze在NDC坐标系中可按线性插值。
代入结论1,整理得
因此,P/Ze在NDC坐标系中可按线性插值。
至此,让我们再回头看OGL specification的插值公式
对于三角形
对于线
我们知道,Raster中为每个插值出z(即存到depth buffer中的z),其实等于A/Ze+B,由结论1知,1/Ze可在屏幕坐标系下线性插值。
有结论2知,对于普通的属性,可以在屏幕坐标系下插值得到P/w(上文提到,透视除法时会将非Const属性除以w)。但我们最终需要的实际上是P!注意到,1/w也是可以在屏幕坐标下线性插值的,因此可以同时先插值得到每个像素的1/w,然后将得到的P/w除以1/w。
在某种程度上,zw其实是顶点的特殊的属性。
本篇继续覆盖光栅化整体经过的步骤,但并未考虑MSAA的情况,后续会继续做总结。