前言:Rasterizer做什么
本篇
简单地
讨论一个问题:
给出图元(三角形/线/点)的顶点坐标及颜色,DirectX 3D(D3D)/OpenGL(OGL)如何确定最终在屏幕上画出的结果?
从D3D Pipeline看[1],这些工作是由光栅化(Rasterizater)做的,光栅化是D3D/OGL Pipeline中关键的固定功能阶段,其前面的Shader经过基于Vertex/Patch/Geometry的处理后,以为图元基本单位,输出图元各顶点的位置坐标及各种属性(如颜色,纹理坐标等),交由Raster
(1)确定图元覆盖到的所有屏幕像素(2)插值计算出这些像素的属性
。
Raster为这些像素触发像素着色器(Pixel Shader),将结果输出给像素着色器进一步处理。
一个例子
这里假设
(1)输入的顶点是屏幕坐标(严格讲,Raster还需要做透视除法及viewport变换才得到屏幕坐标,可认为Shader里完成了这两个步骤),且每个顶点只有Color一种属性;
(2)只开启了顶点着色器和像素着色器,并且对于输入的Color,着色器直接将之输出。
以一个简单的三角形作为例子,其中
V0=(4.5,0.5),
Color0=(255,0,0)
;
V1=(11.5,11.5), Color1=(0,0,255);
V2=(0.5,5.5), Color2=(0,255,0);
如下图
1. 判断哪些像素被三角形覆盖到
为减少计算,可先通过三个顶点的坐标先确定一个包围三角形的bounding rect,遍历bounding rect中每个像素(x, y),逐个像素判断其是否被覆盖,亦即判断其中心坐标是否同时落在三角形三条边是否都满足ax+by+c≥0(这是最直观简洁的方法,硬件通常会尽可能加入优化,或者使用类似Bresenham的算法)。
为此,首先计算三条边:V0V1, V1V2, V2V0的线方程ax+by+c=0:
注意V0v1V2呈顺时针,每条线的法向量(a,b)指向三角形内部。如果按逆时针的方向计算三条边的方程,则像素落在三角形内部应对应ax+by+c≤0。
但问题在于如何确定是顺时针还是逆时针呢?在下一节会提到。
2. 确定每个像素的颜色
对于像素属性的确定,OpenGL的specification给出了具体的说明[2],并且与D3D的实现是完全一致的(如果不考虑坐标系差异所带来影响的话)
首先,任意多边形的面积为
其中n为多边形顶点个数,xi和yi表示第i个顶点的屏幕坐标,xy右上角带圆圈的加法表示(i+1)mod n。
对于三角形,其面积的2倍为:
我们知道,向量的叉乘结果等于对应平行四边形的面积,令向量V0V1=(x1-x0, y1-y0, 0),V0V2=(x2-x0, y2-y0, 0),则V0V1 X V0V2 = (0, 0, Area)。
我们将V0V1及V0V2的第3个分量取为0,因为这里只关心结果中z方向的分量。
根据叉乘的几何意义,Area>0表示V0V1V2成顺时针,Area=0表示三点共线,Area<0表示V0V1V2成逆时针。上一节中方向的判定问题得以解决,但Area的作用不止于此。
D3D和OGL确定像素颜色的方法是相同的:以三个顶点的颜色及重心坐标作为权重系数插值得到任意像素的颜色
对于任意像素O,设三角形V2OV1,V0OV2,V1OV0的面积分别为A,B,C
则重心坐标
注意到计算三个小三角形面积时,所按照的顶点方向跟大三角形一致,即Area的符号是一样的,所以会有a+b+c=1
对于像素O
最终得到的结果:
我们看到,计算每个像素的颜色时,涉及到计算3个三角形的面积,可以做一些简化减少计算量,假设P为颜色通道的某个分量,则:
其中
只需计算一次。
这种简化也给出重心坐标差值的另一种解释,相当于以
种子属性值P0,属性值在x/y方向的
变化率px/py作为基础,通过目标位置(x,y)到种子坐标(x0, y0)的偏移,累加两个方向上变化量得到目标属性值。
4.点的光栅化
对于点,像素的颜色与输入顶点颜色一致,因此只需考虑覆盖了哪些像素。
光栅化把点看做中心位于(x, y)宽度为w(对于D3D,w=1,对于OGL,w≥1)的矩形。OGL支持Wide Point,而D3D中线宽度和Point大小只能是1,可能因为D3D认为既然四边形可以拆分成三角形便不做特殊支持。
这里有个特殊问题需要处理,OGL以及D3D10及以上版本,像素中心坐标是(N.5, N.5)形式,这样若一个顶点的位置坐标为整数(并且对于OGL当线宽为奇数时),它会覆盖4个像素的中心,比如下图,要画哪个像素好呢?
4.1 D3D 点光栅化规则
D3D将点按z字形分成两个三角形做光栅化,而三角形的光栅化遵守
TOP-LEFT规则:
在屏幕上,若某条边位于三角形的左侧,则这条边称为LEFT边;若某条边是平行边,且位于三角形的上侧,则这条边称为TOP边。简单地讲,LEFT边是“左侧的边”,TOP边是“上面的平行边”。
D3D规定:
(1)如果一个像素中心刚好落在三角形的一条边上,则仅当这条边为TOP或LEFT边时才画该像素;
(2)如果一个像素中心刚好落在三角形两条边的交点,则仅当两条边分别为TOP和LEFT边时才画该像素。
TOP-LEFT规则保证了当两个三角形有重合边时,像素不会被重复渲染。对于拆分成两个三角形的点,则保证了一个顶点只覆盖一个像素。
综上,当代表顶点的矩形
(1)只覆盖一个像素中心,则画出该像素;
(2)覆盖左右两个像素中心,则画左边的;
(3)覆盖上下两个像素中心,则画上边的;
(4)覆盖四个像素中心,则画左上角的。
4.2 OGL 点光栅化规则
OGL通过调整矩形中心的位置来处理同样的问题[3]:
若宽度w为奇数,则矩形中心调整为
当宽度为偶数时,矩形中心调整为
可以看出,中心经调整后的矩形只能覆盖到w*w个像素,这说明OGL也有对应的规则。
这里需要提到OGL的坐标系,典型OGL坐标系是以Render Target的左下角为原点(较新的OGL版本通过Clip Control Origin Mode同时支持LOWER_LEFT和UPPER_LEFT两种原点形式)。可以推出,在这种坐标系下,OGL对应的规则是TOP-RIGHT:
5.线的光栅化
这里所指的线其实是线段,在不开启抗混叠的情况下,D3D和OGL对线的光栅化基本一致,我们暂且把讨论限定在线宽w=1的情况(OGL支持大于1的线宽,不过很好从线宽为1的情况扩展)。
5.1确定覆盖的像素
有意思的是,线的形状是跟斜率相关的。按斜率可将线分为两种:(1)x-major,-1≤斜率≤1(2)其他斜率的线称为y-major。
顾名思义,x-major在x方向上跨度较大,y-major在y方向上跨度较大。
x-major线代表的区域可看做将线段V0V1沿y方向偏移+1/2和-1/2得到的两线段构成的平行四边形;
y-major线代表的区域与之类似,
可以看出,相比三角形,线仅需要算出两条边的方程,并限制Bounding Rect在x/y方向上的范围。
Diamond-Exit规则:
与三角形类似,当一条线的终点与另一条线的起点重合时,需要避免重复渲染。Diamond-Exit规则解决了这个问题:从起点沿连线走到终点,仅当从某个像素的棱形区域中离开时,才认为这条线覆盖到像素。.其中,Diamond是指由pixel矩形四条边中点构成的棱形。如下图,当画线V0V1和V1V2时,V0所在像素被覆盖,V2所在像素没被覆盖,V1所在的像素只被渲染一次。
5.2确定像素的颜色
与三角形类似,像素的颜色可通过插值得到:
其中,
这是D3D和OGL关于变化率最原始的定义,看起来不是很好理解,但我们可以从另一个近似等效的角度理解:
由于
从而
因而,对于x-major线,变化率近似相当于:
因而,对于y-major线,变化率近似相当于:
结束语
一开始简单地假设每个顶点只有Color一个属性,并且Pixel Shader直接将输入的Color作为输出。而实际上,Color最终的确定是任意灵活的:比如每个Vertex带有texture coordinate属性,而Pixel Shader通过像素的texture coordinate采样确定出最终的Color。但无论何种属性,其插值的方式是一致的。
本篇只是简单讨论了Raster最基本的工作,关于Raster其他诸如CULL,裁剪以及在插值时的“透视矫正”, 下一篇会继续总结。
参考
[1]https://msdn.microsoft.com/en-us/library/windows/desktop/ff476882(v=vs.85).aspx
[2]opengl 4.5 compatibility profile:https://www.opengl.org/registry/doc/glspec45.compatibility.pdf, Chapter 14.6.1
[3]opengl 4.5 compatibility profile:https://www.opengl.org/registry/doc/glspec45.compatibility.pdf, Chapter 14.4.1