光栅化

计算机中表示图形有两种方式,一种是点阵表示,一种是顶点表示。

左图,图形的顶点表示;右图,图形的点阵表示

点阵表示是光栅显示系统显示时所需要的表示形式,光栅化便是将顶点表示转换为点阵表示的过程。而点阵表示转换为顶点表示的过程属于图像识别的范畴,这里不做介绍。

什么是光栅显示?见下图。

(0, 0)
(0, 0)
(0, 1)
(0, 1)
(1, 0)
(1, 0)
(1, 1)
(1, 1)
(0.5, 0.5)
(0.5, 0.5)
(0.5, 1.5)
(0.5, 1.5)
(1.5, 0.5)
(1.5, 0.5)
(1.5, 1.5)
(1.5, 1.5)
Viewer does not support full SVG 1.1

左图,离散的像素点;中图,左上角2x2像素块中每个像素块的坐标;右图,左上角2x2像素块中每个像素点的坐标

为了将图形的顶点表示转换为点阵表示,光栅化要解决两个问题:

  1. 哪些像素点需要显示
  2. 需要显示的像素点显示成什么颜色(属性)

光栅化处理过程可以用伪代码表示如下:

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);

我们知道,光栅化只处理基本图元:点、线和三角形。因为其他图元都可以转换成这三种基本图元。本文只介绍三角形的光栅化过程。

对于三角形而言,需要解决的两个问题可以更加具象地描述为:

  1. 对于一个给定的像素点,怎样判定其在三角形的内部还是三角形的外部
  2. 对于一个给定的三角形内部的像素点,其颜色(属性)应该是多少

对于一个给定的像素点,怎样判定其在三角形的内部还是三角形的外部

向量的叉乘

两个向量的叉乘是一个向量,其方向垂直于两原始向量且满足右手定则,而大小等于投影的平行四边形的面积。

V1
V1
V2
V2
u
u
θ
θ
V1 X V2
V1 X V2
Viewer does not support full SVG 1.1

\(\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}\)的左侧还是右侧?

P0
P0
P1
P1
左侧
左侧
右侧
右侧
P
P
x轴
x轴
y轴
y轴
z轴
z轴
Viewer does not support full SVG 1.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\)分量的正负即可:正则在右侧,负则在左侧。

P0
P0
P1
P1
左侧
左侧
右侧
右侧
P
P
x轴
x轴
y轴
y轴
z轴
z轴
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Viewer does not support full SVG 1.1

\(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
P
x轴
x轴
y轴
y轴
P0
P0
P1
P1
P2
P2
z轴
z轴
Viewer does not support full SVG 1.1

可以看到此时的三角形的三个顶点是顺时针排列的,但如果是逆时针排列呢?

同顺时针的三角形的推理过程可以知道:逆时针的三角形的情况下,判断标准和顺时针的三角形截然相反。

P
P
x轴
x轴
y轴
y轴
P0
P0
P2
P2
P1
P1
z轴
z轴
Viewer does not support full SVG 1.1

  • 如果是顺时针方向给定三个顶点的三角形,则
    • 如果点\(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的三角形。有一种做法是:

  1. TILE大小设置为32x32。如果某个TILE的四个角像素有一个或者多个在三角形内部,则继续2;否则丢弃该TILE。
  2. TILE大小设置为16x16。如果某个TILE的四个角像素有一个或者多个在三角形内部,则继续3;否则丢弃该TILE。
  3. TILE大小设置为 8x8 。如果某个TILE的四个角像素有一个或者多个在三角形内部,则继续4;否则丢弃该TILE。
  4. TILE大小设置为 4x4 。如果某个TILE的四个角像素有一个或者多个在三角形内部,则继续5;否则丢弃该TILE。
  5. TILE大小设置为 2x2 。此时与逐像素判断效果相同。

x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
Viewer does not support full SVG 1.1

对于一个给定的三角形内部的像素点,其颜色(属性)应该是多少

已知三角形的三个顶点的颜色分别为红色、绿色和蓝色,现要填充整个三角形,我们期望三角形内部像素点的颜色是平衡过渡的(渐变)。

此时,我们需要用到一个非常有用的概念:重心坐标。

重心坐标

我们先看直线的重心坐标。

在直线段\(\overline{P_0P_1}\)上任取一点\(P\),则\(P\)的重心坐标表示如下:

\((u,v)=(\frac{\overline{P_0P}}{\overline{P_0P_1}},\frac{\overline{PP_1}}{\overline{P_0P_1}})\)

P0
P0
P1
P1
P
P
Viewer does not support full SVG 1.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}})\)

P0
P0
P1
P1
P2
P2
P
P
Viewer does not support full SVG 1.1

可以看到:\(i+j+k=1\)

\(i=j=k=\frac{1}{3}\)时,\(P\)是三角形的重心。

P0
P0
P1
P1
P2
P2
P
P
i=0
i=0
i=1
i=1
i
i
j=0
j=0
j=1
j=1
j
j
Viewer does not support full SVG 1.1

随着三角形中任意一点\(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\)是否在三角形内部已经得到了,参看《点和线的关系》。

可见,这种方式的计算量很少。

那光栅化中那些属性需要做插值呢?

  1. 颜色
  2. 深度
  3. 纹理坐标

透视矫正

如果将一个不平行于xy平面的三角形透视投影到xy平面,然后做插值,计算出各像素点的属性后得到的图像存在透视扭曲的现象。

左图,原纹理;中图,透视扭曲;右图,透视矫正后的结果

产生透视扭曲的原因,见下图:

virtual
camera
virtual...
image plan
(screen)
image plan...
b,intensity=1.0
b,intensity=1.0
a,intensity=0.0
a,intensity=0.0
c,intensity=0.5
c,intensity=0.5
A,intensity=0.0
A,intensity=0.0
C,intensity≠0.5
C,intensity≠0.5
B,intensity=1.0
B,intensity=1.0
line AB
line AB
Viewer does not support full SVG 1.1

直接在屏幕空间对属性做线性插值可能存在透视扭曲现象

为了便于说明,上图只展示了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\)

此时,便产生了透视扭曲的现象。

virtual
camera
(0,0)
virtual...
image plan
(screen)
image plan...
b(u2,d)
b(u2,d)
a(u1,d)
a(u1,d)
c(us,d)
c(us,d)
A(x1,z1),attribute=I1
A(x1,z1),attribute=I1
C(xt,zt),attribute=It
C(xt,zt),attribute=It
B(x2,z2),attribute=I2
B(x2,z2),attribute=I2
line AB
line AB
x
x
-z
-z
s
s
1-s
1-s
t
t
1-t
1-t
d
d
0 ≤ s ≤1,  0 ≤ t ≤ 1
0 ≤ s ≤1,  0 ≤ t ≤ 1
Viewer does not support full SVG 1.1

根据上图并利用相似三角形的性质可以推导出(推导过程参看这里):

\(\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\)得到。

抗锯齿

共享边

小三角形

posted @ 2020-07-22 18:33  专注于GPU的程序员  阅读(947)  评论(0编辑  收藏  举报