光栅化
计算机中表示图形有两种方式,一种是点阵表示,一种是顶点表示。
左图,图形的顶点表示;右图,图形的点阵表示
点阵表示是光栅显示系统显示时所需要的表示形式,光栅化便是将顶点表示转换为点阵表示的过程。而点阵表示转换为顶点表示的过程属于图像识别的范畴,这里不做介绍。
什么是光栅显示?见下图。
左图,离散的像素点;中图,左上角2x2像素块中每个像素块的坐标;右图,左上角2x2像素块中每个像素点的坐标
为了将图形的顶点表示转换为点阵表示,光栅化要解决两个问题:
- 哪些像素点需要显示
- 需要显示的像素点显示成什么颜色(属性)
光栅化处理过程可以用伪代码表示如下:
for (y = 0; y < screen_height; y++)
for (x = 0; x < screen_width; x++)
if (inside(tri, x+0.5, y+0.5)) // 如果点(x+0.5,y+0.5)在三角形内部
image[x][y] = render(tri, x+0.5, y+0.5);
我们知道,光栅化只处理基本图元:点、线和三角形。因为其他图元都可以转换成这三种基本图元。本文只介绍三角形的光栅化过程。
对于三角形而言,需要解决的两个问题可以更加具象地描述为:
- 对于一个给定的像素点,怎样判定其在三角形的内部还是三角形的外部
- 对于一个给定的三角形内部的像素点,其颜色(属性)应该是多少
对于一个给定的像素点,怎样判定其在三角形的内部还是三角形的外部
向量的叉乘
两个向量的叉乘是一个向量,其方向垂直于两原始向量且满足右手定则,而大小等于投影的平行四边形的面积。
\(\vec{v_1}\times\vec{v_2}=\vec{u}|v_1||v_2|cos\theta\)
在笛卡尔坐标系中,设向量\(\vec{v_1}\)为\(\small \begin{pmatrix} x_1 \\ y_1 \\ z_1 \end{pmatrix}\),\(\vec{v_2}\)为\(\small \begin{pmatrix} x_2 \\ y_2 \\ z_2 \end{pmatrix}\)。则:
\(\vec{v_1}\times\vec{v_2}=\small \begin{pmatrix} y_1z_2-z_1y_2 \\ z_1x_2-x_1z_2 \\ x_1y_2-y_1x_2 \end{pmatrix}\)
点和线的关系
在笛卡尔坐标系中\(z=0\)时\(x\)轴和\(y\)轴所组成的平面上取两个点,\(P_0(x_0, y_0)\)和\(P_1(x_1, y_1)\)。然后在该平面再任取一个点\(P(x, y)\),怎样判定点\(P\)在向量\(\vec{P_0P_1}\)的左侧还是右侧?
已知叉乘公式:
\(\vec{v_1}\times\vec{v_2}=\small \begin{pmatrix} y_1z_2-z_1y_2 \\ z_1x_2-x_1z_2 \\ x_1y_2-y_1x_2 \end{pmatrix}\)
且当前平面所有点的\(Z\)值为0,则:
\(\vec{v_1}\times\vec{v_2}=\small \begin{pmatrix} 0 \\ 0 \\ x_1y_2-y_1x_2 \end{pmatrix}\)
又\(\vec{P_0P_1}=\small \begin{pmatrix} x_1-x_0 \\ y_1-y_0 \end{pmatrix},\vec{P_0P}=\small \begin{pmatrix} x-x_0 \\ y-y_0 \end{pmatrix}\),则:
\(\vec{P_0P_1}\times\vec{P_0P}=\small \begin{pmatrix} 0 \\ 0 \\ (x_1-x_0)(y-y_0)-(y_1-y_0)(x-x_0) \end{pmatrix}\)
利用叉乘的性质(满足右手定则),我们只需要判断\(\vec{P_0P_1}\times\vec{P_0P}\)的\(Z\)分量的正负即可:正则在右侧,负则在左侧。
记\(f(x,y)\)为\(Z\)分量的值,则:
\(f(x,y)=(x_1-x_0)(y-y_0)-(y_1-y_0)(x-x_0)\)
如果\(f(x,y)>0\),则\(P\)在\(\vec{P_0P_1}\)的右侧;
如果\(f(x,y)<0\),则\(P\)在\(\vec{P_0P_1}\)的左侧;
如果\(f(x,y)=0\),则\(P\)在\(\vec{P_0P_1}\)所在的直线上。
注意:这里\(f(x,y)\)的绝对值等于\(\triangle P_0P_1P\)的面积的两倍。后面章节会用到这个性质。
点和面(三角形)的关系
在笛卡尔坐标系中\(z=0\)时\(x\)轴和\(y\)轴所组成的平面上取三个点,\(P_0(x_0, y_0)\),\(P_1(x_1, y_1)\)和\(P_2(x_2, y_2)\)。然后在该平面再任取一个点\(P(x, y)\),怎样判定点\(P\)在\(\triangle P_0P_1P_2\)的内部还是外部?
在上一章节的基础上,我们可以很容易得到:
如果点\(P\)在\(\vec{P_0P_1}\)的右侧,且也在\(\vec{P_1P_2}\)和\(\vec{P_2P_0}\)的右侧,则\(P\)在三角形内部;
如果点\(P\)在\(\vec{P_0P_1}\)的左侧,或者在\(\vec{P_1P_2}\)或\(\vec{P_2P_0}\)的左侧,则\(P\)在三角形外部;
可以看到此时的三角形的三个顶点是顺时针排列的,但如果是逆时针排列呢?
同顺时针的三角形的推理过程可以知道:逆时针的三角形的情况下,判断标准和顺时针的三角形截然相反。
- 如果是顺时针方向给定三个顶点的三角形,则
- 如果点\(P\)在\(\vec{P_0P_1}\)的右侧,且也在\(\vec{P_1P_2}\)和\(\vec{P_2P_0}\)的右侧,则\(P\)在三角形内部
- 如果点\(P\)在\(\vec{P_0P_1}\)的左侧,或者在\(\vec{P_1P_2}\)或\(\vec{P_2P_0}\)的左侧,则\(P\)在三角形外部
- 否则,\(P\)在三角形的某一条边上或者某一个顶点上
- 如果是逆时针方向给定三个顶点的三角形,则
- 如果点\(P\)在\(\vec{P_0P_1}\)的左侧,且也在\(\vec{P_1P_2}\)和\(\vec{P_2P_0}\)的左侧,则\(P\)在三角形内部
- 如果点\(P\)在\(\vec{P_0P_1}\)的右侧,或者在\(\vec{P_1P_2}\)或\(\vec{P_2P_0}\)的右侧,则\(P\)在三角形外部
- 否则,\(P\)在三角形的某一条边上或者某一个顶点上
也可以简单描述为:
- 如果点\(P\)与三角形的三条边叉乘的结果有为0的,则其在边上或者顶点上
- 如果点\(P\)与三角形的三条边叉乘的结果符号相同,则其在内部
- 如果点\(P\)与三角形的三条边叉乘的结果符号不同,则其在外部
光栅化中的应用
记三角形的三个顶点分别为\(v_0,v_1,v_2\),坐标分别为\((x_0,y_0),(x_1,y_1),(x_2,y_2)\)。任取一点\(p(x,y)\),其与\(\vec{v_0v_1},\vec{v_1v_2},\vec{v_2v_0}\)的叉乘分别为:
\(f_a(x,y)=(x_1-x_0)(y-y_0)-(y_1-y_0)(x-x_0)\)
\(f_b(x,y)=(x_2-x_1)(y-y_1)-(y_2-y_1)(x-x_1)\)
\(f_c(x,y)=(x_0-x_2)(y-y_2)-(y_0-y_2)(x-x_2)\)
展开,可得:
\(f_a(x,y)=(y_0-y_1 )x+(x_1-x_0 )y+x_0 y_1-x_1 y_0\)
\(f_b(x,y)=(y_1-y_2 )x+(x_2-x_1 )y+x_1 y_2-x_2 y_1\)
\(f_c(x,y)=(y_2-y_0 )x+(x_0-x_2 )y+x_2 y_0-x_0 y_2\)
因为三角形的三个顶点坐标是已知的,所以可以简写为:
\(f_a (x,y)=A x+B y+C,A=y_0-y_1,B=x_1-x_0,C=x_0 y_1-x_1 y_0\)
\(f_b (x,y)=A x+B y+C,A=y_1-y_2,B=x_2-x_1,C=x_1 y_2-x_2 y_1\)
\(f_c (x,y)=A x+B y+C,A=y_2-y_0,B=x_0-x_2,C=x_2 y_0-x_0 y_2\)
根据上一章节的结论可知:如果\(f_a(x,y),f_b(x,y),f_c(x,y)\)符号相同,则点\(P(x,y)\)在三角形内部或三角形上。否则,在三角形外部。
可以看到在判断一个点是否在三角形内部所做的计算还是非常多的,那么是否需要对所有的像素点都做这样的运算呢?
现我们假设\(P(x,y)\)在三角形内部,那将\(P(x+1,y)\)代入\(f_a(x,y)\),得:
\(f_a(x+1,y)=A(x+1)+By+C=Ax+By+C+A=f_a(x,y)+A\)
同理可得:
\(f_a(x,y+1)=f_a(x,y)+B\)
所以,我们只需要对一个点做一次复杂运算,其他点可以通过简单的加法来判断其与三角形的位置关系。
遍历方式
对于一个给定的三角形,是否需要对屏幕上所有的像素点都做一次判断?
很容易想到的一个办法是:只对三角形所在包围盒里的所有像素点做判断。这样可以减少很多无用的计算。
下图展示了常见的一些遍历方法。
着重介绍下上图中左下角所示的遍历方式:逐TILE遍历。
TILE是nxn个像素组成的像素块,如果某个TILE的四个角所在的像素点都在三角形外,则可以判定这个TILE中所有像素点都在三角形外。这样做的好处很明显:减少了计算量。
那这个TILE划分多大合适呢?
假设现有一个包围盒大小为64x64的三角形。有一种做法是:
- TILE大小设置为32x32。如果某个TILE的四个角像素有一个或者多个在三角形内部,则继续2;否则丢弃该TILE。
- TILE大小设置为16x16。如果某个TILE的四个角像素有一个或者多个在三角形内部,则继续3;否则丢弃该TILE。
- TILE大小设置为 8x8 。如果某个TILE的四个角像素有一个或者多个在三角形内部,则继续4;否则丢弃该TILE。
- TILE大小设置为 4x4 。如果某个TILE的四个角像素有一个或者多个在三角形内部,则继续5;否则丢弃该TILE。
- TILE大小设置为 2x2 。此时与逐像素判断效果相同。
对于一个给定的三角形内部的像素点,其颜色(属性)应该是多少
已知三角形的三个顶点的颜色分别为红色、绿色和蓝色,现要填充整个三角形,我们期望三角形内部像素点的颜色是平衡过渡的(渐变)。
此时,我们需要用到一个非常有用的概念:重心坐标。
重心坐标
我们先看直线的重心坐标。
在直线段\(\overline{P_0P_1}\)上任取一点\(P\),则\(P\)的重心坐标表示如下:
\((u,v)=(\frac{\overline{P_0P}}{\overline{P_0P_1}},\frac{\overline{PP_1}}{\overline{P_0P_1}})\)
可以看到:\(u+v=1\)。
当\(u=v=\frac{1}{2}\)时,\(P\)是直线段的重心。
类似的,在\(\triangle P_0P_1P_2\)中任取一点\(P\),则\(P\)的重心坐标表示如下(推导过程点击这里):
\((i,j,k)=(\frac{\triangle{PP_1P_2}}{\triangle{P_0P_1P_2}},\frac{\triangle{P_1PP_3}}{\triangle{P_0P_1P_2}},\frac{\triangle{P_0P_1P}}{\triangle{P_0P_1P_2}})\)
可以看到:\(i+j+k=1\)。
当\(i=j=k=\frac{1}{3}\)时,\(P\)是三角形的重心。
随着三角形中任意一点\(P\)的位置变化,重心坐标中\(i\)和\(j\)的变化。
属性插值
有了三角形中任意一点\(P\)的重心坐标\((i,j,k)\),那么我们可以使用它计算出该点的属性值:
\(a=ia_0+ja_1+(1-i-j)a_2\)
同样的,我们来关注下计算重心坐标的计算量。
\((i,j,k)=(\frac{\triangle{PP_1P_2}}{\triangle{P_0P_1P_2}},\frac{\triangle{P_1PP_3}}{\triangle{P_0P_1P_2}},\frac{\triangle{P_0P_1P}}{\triangle{P_0P_1P_2}})\)
对\(\triangle{P_0P_1P_2}\)而言,它的面积只算一次即可,三角形中所有像素点的重心坐标计算都可以重用该值。
对\(\triangle{PP_1P_2}\)、\(\triangle{PP_1P_2}\)和\(\triangle{PP_1P_2}\)而言,他们的面积在前面判断\(P\)是否在三角形内部已经得到了,参看《点和线的关系》。
可见,这种方式的计算量很少。
那光栅化中那些属性需要做插值呢?
- 颜色
- 深度
- 纹理坐标
透视矫正
如果将一个不平行于xy平面的三角形透视投影到xy平面,然后做插值,计算出各像素点的属性后得到的图像存在透视扭曲的现象。
左图,原纹理;中图,透视扭曲;右图,透视矫正后的结果
产生透视扭曲的原因,见下图:
直接在屏幕空间对属性做线性插值可能存在透视扭曲现象
为了便于说明,上图只展示了2D空间的一条线投影到1D的平面的情况。3D投影到2D平面与此类似,可以把上图当做三角形的一条扫描线的投影情况看待。
2D空间的直线段\(\overline{AB}\)的两个端点分别投影到1D屏幕的\(a\)和\(b\)。
顶点的属性记为\(intensity\),则在\(A\)点:\(intensity=0.0\),在\(B\)点:\(intensity=1.0\)。
由于\(a\)和\(b\)分别由\(A\)和\(B\)投影而来,自然的,\(a.intensity=A.intensity=0.0,b.intensity=B.intensity=1.0\)。
\(c\)是\(\overline{ab}\)的中点。
如果我们在屏幕空间做线性插值,那么\(c\)点的\(intensity=0.5\)。
而\(c\)是\(C\)投影而来,从图中可以明显看到\(C\)并不是\(\overline{AB}\)的中点。也就是说:
\(c.intensity=0.5,C.intensity≠0.5 \Rightarrow c.intensity≠C.intensity\)
此时,便产生了透视扭曲的现象。
根据上图并利用相似三角形的性质可以推导出(推导过程参看这里):
\(\frac{1}{Z_t}=\frac{1}{Z_1}+s(\frac{1}{Z_2}-\frac{1}{Z_1})\)
通过上式可知,屏幕上的点\(c\)的\(z\)值可以通过线性插值\(\frac{1}{Z_1}\)和\(\frac{1}{Z_2}\)得到。这是一个非常重要的性质。
基于前面的推导所得,代入\(I_t=I_1+t(I_2-I_1)\),可以得到\(C\)的属性\(I_t\):
\(\frac{I_t}{Z_t}=\frac{I_1}{Z_1}+s(\frac{I_2}{Z_2}-\frac{I_1}{Z_1})\)
通过上式可知,屏幕上的点\(c\)的属性可以通过线性插值\(\frac{I_1}{Z_1}\)和\(\frac{I_2}{Z_2}\)之后再乘以\(Z_1\)得到。