Rasterization
一、总体过程以及思路
光栅化的本质是基于对象的渲染,这种渲染的思路以每个图元为出发点,根据图元来计算每个像素点的着色,与此相对光追是以像素点为出发点,每个像素独立计算其着色。光栅化的过程如下,这个过程也就是所谓的渲染管线:
- 顶点处理:其实就是投影(也就是Projection文件中所写的过程),在这个过程最终生成了要处理的图元作为下一步的输入。
- 光栅化:以图元和顶点为基础,通过变化来把图元的颜色和各种属性计算/插值到像素,形成片元,片元就是带有各种数学的像素点。
- 片元处理:这个过程就是shader工作的过程,此处不做详细解释,这个文件重点在光栅化,输出的结果也汇入片元的属性中。
- 混合:根据片元中的属性来进行绘画,只有这一步才会真的生成画在屏幕上的图形,z-buffer的处理在这一步。
二、直线的光栅化(中点算法)
对于直线来讲,其实只需要关系画出来的问题,三维的复杂场景一般不会出现纯直线的图形,就是在二维情况下的讨论。
只说斜率属于(0, + ∞ +\infty +∞)对于直线上一点(x,y),下一步要画的点一定是(x+1,y)和(x+1,y+1)的其中一个,至于怎么确定,方法如下:
- 计算直线方程f(x,y)
- 将(x+1,y+0.5)带入f(x,y)
- 如果直线在该点之上,则画(x+1,y+1),否则画(x+1,y)
很明显思路就是把看直线在两者终点上面还是下面,所以叫做中点算法。
三、三角形的光栅化
这一步其实就是光栅化的核心,因为三维的物体模型大多用三角形的面来组成。步骤如下:
- 在世界空间中对三角形中的像素点所有点进行插值
- 处理三角形边界颜色计算问题
- 根据上一步中的结果映射到纹理空间
这里面的世界空间就是摄像机所在的三维空间,纹理空间就是要映射到的二维平面,这和屏幕的二维空间不同,是指纹理分布的二维空间。这个过程只对三角形中间的插值点管用,因为三角形的顶点的纹理坐标一定是知道的。
3.1 对世界空间的三角形插值
基本思路是根据三角形三个顶点的属性,然后对三角形内部的所有点进行一个平滑的插值,这个属性现在以颜色为例。
3.1.1 barycentric 坐标系
这个坐标系的思路如下:把三角形的两条边作为基,便可以表示三角形内的任何一个点,而且坐标一定都属于(0,1)之内,凡是坐标中为1的点就是顶点,也就是坐标轴为其对边。
以这个思想,三角形内的任何一点,都可以表示为(a,b,c),如果三角形的顶点分别为1,2,3.则点1的坐标为(1,0,0),点2的坐标为(0,1,0),点3的坐标为(0,0,1)。点p的坐标(a,b,c)的计算如下:
a
=
f
23
(
p
)
/
f
23
(
p
i
o
n
t
1
)
(
1
)
a = f_{23}(p)/f_{23}(piont1) \qquad\qquad (1)
a=f23(p)/f23(piont1)(1)
其他两个值的计算和(1)式类似。
故对于三角形内的任意一点
p
=
(
a
,
b
,
c
)
p =(a,b,c)
p=(a,b,c),如果已知其顶点的颜色
c
i
c_i
ci,则
p
p
p点的颜色可进行如下插值:
c
p
=
a
⋅
c
1
+
b
⋅
c
2
+
c
⋅
c
3
c_p=a\cdot c_1 + b \cdot c_2 + c\cdot c_3
cp=a⋅c1+b⋅c2+c⋅c3
3.1.2 对于边界问题的处理
如果两个三角形有相同的边,那这条边按照哪个三角形进行计算就成了问题,最糟糕的做法就是把这条边空出来不着色,因为这样会留下很多三角形之间的缝隙显示在屏幕上,所以可以按照其中某一个三角形进行着色,至于是哪个无所谓,只要选定就好了。
选定三角形的方法可以选择屏幕外固定一点,判断这一点和重叠边相对的顶点是否在重叠边的同一边,无论选择和相对顶点在同一遍的三角形,还是不在同一边的三角形都无所谓,关键是要选一个。
3.2 对于纹理空间的映射
为什么会有这一步呢?因为顶点处理输出的图元信息都是符合透视的近大远小,但是如果把世界空间的点 [ x y z w ] \left[\begin{matrix} x\\ y \\z \\w \end{matrix}\right] xyzw 的属性直接映射到纹理空间的点 [ x y ] \left[\begin{matrix} x\\y \end{matrix}\right] [xy]就会出现属性的扭曲,因为到纹理空间的这一步映射只是简单的正交投影,而没有符合近大远小的规定,相当于图形虽然近大远小,但是颜色变化却是均匀的不符合图形的变化。
为了解决这一问题,思路就是把到纹理空间的坐标 [ u v ] \left[\begin{matrix} u\\v \end{matrix}\right] [uv]这两个分量也作为三角形的属性进行插值,最后插值的结果才是点 [ x y z w ] \left[\begin{matrix} x\\ y \\z \\w \end{matrix}\right] xyzw 应该映射到的地方。具体的计算方法如下:
-
把点 [ x y z w ] \left[\begin{matrix} x\\ y \\z \\w \end{matrix}\right] xyzw 齐次化为 [ x / w y / w z / w 1 ] = [ x s y s z s 1 ] \left[\begin{matrix} x/w\\ y/w \\z/w \\1 \end{matrix}\right] = \left[\begin{matrix} x_s\\ y_s \\z_s \\1 \end{matrix}\right] x/wy/wz/w1 = xsyszs1 ,包括顶点也做类似处理
-
计算 ( u s , v s ) (u_s,v_s) (us,vs),如下
u s = a ⋅ u 1 / w 1 + b ⋅ u 2 / w 2 + c ⋅ u 3 / w 3 v s = a ⋅ v 1 / w 1 + b ⋅ v 2 / w 2 + c ⋅ v 3 / w 3 u_s = a\cdot u_1/w_1 + b \cdot u_2/w_2 +c\cdot u_3/w_3 \\ v_s = a\cdot v_1/w_1 + b \cdot v_2/w_2 +c\cdot v_3/w_3 us=a⋅u1/w1+b⋅u2/w2+c⋅u3/w3vs=a⋅v1/w1+b⋅v2/w2+c⋅v3/w3
其中 ( u i , v i ) (u_i,v_i) (ui,vi)为三角形的三个顶点的纹理坐标 -
计算 w w w,如下:
$w = a\cdot w_1 + b \cdot w_2 +c\cdot w_3 $
-
u = u S ⋅ w v = v S ⋅ w u = u_S \cdot w \\v = v_S \cdot w u=uS⋅wv=vS⋅w
为什么要多出第三步和第四步呢,因为并不能确定纹理空间的坐标,也就是给了你纹理你不知道往物体哪里贴,给了你属性你也不知道加载在纹理的哪个地方,因为纹理空间的坐标是和 [ x y z w ] \left[\begin{matrix} x\\ y \\z \\w \end{matrix}\right] xyzw 带着w的那个缩放一致的,也就是说纹理空间的坐标如果作为 [ x y z w ] \left[\begin{matrix} x\\ y \\z \\w \end{matrix}\right] xyzw 的一个属性有这种形式: [ x y z w u v ] \left[\begin{matrix} x\\ y \\z \\w \\ u \\v \end{matrix}\right] xyzwuv ,而屏幕空间的坐标实际上是这种形式 [ x / w y / w z / w 1 u s v s ] \left[\begin{matrix} x/w\\ y/w \\z/w \\1 \\ u_s \\ v_s \end{matrix}\right] x/wy/wz/w1usvs ,所以必须把w乘回去才能得到纹理空间坐标。最后要把处理完成的 ( u , v ) (u,v) (u,v)画在 ( x s , y s ) (x_s,y_s) (xs,ys)上。
不从数学角度,从实际的目的角度来说,纹理本身是贴在物体上,画在屏幕上的。所以纹理到底贴哪,只能根据物体的三角形面进行插值,但是贴完以后画在屏幕上哪,就需要把贴完纹理后的物体映射到屏幕上,这里就是简单的正交投影所以就是 ( x s , y s ) (x_s,y_s) (xs,ys)。
四、z-buffer
关于shader的工作和其他片元处理的细节这里不做过多阐述,之说最后混合阶段的z-buffer。
z-buffer的诞生来源于这么一个问题,那就是物体远近的问题,远处的物体会被近处的物体遮挡,从而在像素上所画的东西应该以近处的物体为准,那么,只需要把深度/远近作为片元的一个属性,遇到更小的深度就替换原有的片元就可以了,这个缓冲就叫做z-buffer。