在计算机图形学中,需要扫描三角形以进行光栅化。比如渲染引擎的计算过程,需要计算从每个像素发出去的光线是否能够碰撞到某个三角形面片,这个时候需要将3D的三角形,按照几何透视原理,投影到2D的光源空间上,然后扫描每个三角形。
重心坐标系插值算法
Barycentric Coordinates即重心坐标系插值算法,能够计算2D平面上的任意一点到三个顶点的相对位置,然后依次判断这个点是否在三角形内部,如果在三角形内部,就给这个像素图上颜色,这时就可以完成三角形光栅化。
已知三角形的三个顶点A,B,C,设任意一点P能够表示成
P
=
A
+
u
⋅
A
B
⃗
+
v
⋅
A
C
⃗
=
A
+
u
⋅
(
B
−
A
)
+
v
⋅
(
C
−
A
)
=
(
1
−
u
−
v
)
⋅
A
+
u
⋅
B
+
v
⋅
C
P=A+u\cdot\vec{AB}+v\cdot\vec{AC}\\ =A+u\cdot (B-A)+v\cdot(C-A)\\ =(1-u-v)\cdot A+u\cdot B+v\cdot C
P=A+u⋅AB+v⋅AC=A+u⋅(B−A)+v⋅(C−A)=(1−u−v)⋅A+u⋅B+v⋅C
这样就能产生一个方程
u
⋅
A
B
⃗
+
v
⋅
A
C
⃗
+
P
A
⃗
=
0
{
u
⋅
A
B
⃗
x
+
v
⋅
A
C
⃗
x
+
P
A
⃗
x
=
0
u
⋅
A
B
⃗
y
+
v
⋅
A
C
⃗
y
+
P
A
⃗
y
=
0
u\cdot\vec{AB}+v\cdot\vec{AC}+\vec{PA}=0\\ \left\{\begin{aligned}\\ u\cdot\vec{AB}_x+v\cdot\vec{AC}_x+\vec{PA}_x=0\\ u\cdot\vec{AB}_y+v\cdot\vec{AC}_y+\vec{PA}_y=0\\ \end{aligned}\right.
u⋅AB+v⋅AC+PA=0{u⋅ABx+v⋅ACx+PAx=0u⋅ABy+v⋅ACy+PAy=0
只有
u
u
u和
v
v
v两个未知量,所以可以直接解出方程,不过可以直接使用向量叉积运算来简化这个求解过程,设两个向量分别为
v
e
c
1
=
[
A
B
⃗
x
,
A
C
⃗
x
,
P
A
⃗
x
]
v
e
c
2
=
[
A
B
⃗
y
,
A
C
⃗
y
,
P
A
⃗
y
]
vec_1=[\vec{AB}_x,\vec{AC}_x,\vec{PA}_x]\\ vec_2=[\vec{AB}_y,\vec{AC}_y,\vec{PA}_y]
vec1=[ABx,ACx,PAx]vec2=[ABy,ACy,PAy]
直接计算这两个向量的叉积,就可以知道最后的
u
u
u和
v
v
v的值了
v
e
c
3
=
c
r
o
s
s
(
v
e
c
1
,
v
e
c
2
)
=
[
a
,
b
,
c
]
[
u
,
v
,
1
]
=
[
a
/
c
,
b
/
c
,
1
]
vec_3=cross(vec_1,vec_2)=[a,b,c]\\ [u,v,1]=[a/c,b/c,1]
vec3=cross(vec1,vec2)=[a,b,c][u,v,1]=[a/c,b/c,1]
这时只要判断,如果
{
0
<
=
u
<
=
1
0
<
=
v
<
=
1
0
<
=
(
u
+
v
)
<
=
1
\left\{\begin{aligned}\\ 0<=u<=1\\ 0<=v<=1\\ 0<=(u+v)<=1 \end{aligned}\right.
⎩⎪⎨⎪⎧0<=u<=10<=v<=10<=(u+v)<=1
那么点P必定在三角形内部。这个方法还有一个优点就是能够很方便的计算出纹理贴图的坐标,并且拟合插值出中间的颜色值。
这个算法的缺点是没有利用上相邻像素往往很相似的假设条件,所以每个像素点都是独立计算的,不过也很容易直接应用在GPU并行计算的程序上,缺点是这样带来了很高的计算量。使用taichi语言实现并行程序,这时的三角形扫描速度很慢,1百万个三角形需要30秒的时间才能扫描完,以下是这个算法的程序,这个速度显然是远远不够的,因为著名的虚幻5引擎《UE5》可以一秒钟可以处理几十亿个三角形。笔者在此基础上也进行了改进,调整了数据内存的布局,提升缓存命中率,可以达到一分钟扫描三亿个三角形。
def triangle_rasterization(i: ti.i32):
# 将三角形光栅化
for i, j, k in ti.ndrange(pixels.shape[0], pixels.shape[1], (i*1000, (i+1)*1000)):
vec_1 = ti.Vector([triangles[k][0, 1] - triangles[k][0, 0],
triangles[k][0, 2] - triangles[k][0, 0],
triangles[k][0, 0] - i])
vec_2 = ti.Vector([triangles[k][1, 1] - triangles[k][1, 0],
triangles[k][1, 2] - triangles[k][1, 0],
triangles[k][1, 0] - j])
vec_3 = ti.cross(vec_1, vec_2)
u = vec_3[0] / vec_3[2]
v = vec_3[1] / vec_3[2]
if 0 <= u and u <= 1 and 0 <= v and v <= 1 and 0 <= (1-u-v) and (1-u-v) <= 1:
pixels[i, j] = ti.Vector([255, 0, 0])
以下是扫描十万个三角形生成的图片
扫描线算法
扫描线算法会利用到像素之间相似性的规律,只需要计算出两个交点,在两个交点之间的很多像素都是在三角形内部,不需要对每个像素都单独进行判断,所以可以减少很多计算量。主要将三角形分成平顶或者平底的,然后任意三角形可以拆分成两个三角形的组合。
之后逐行扫描平顶或者平底三角形,光栅化平底三角形的原理很简单,就是从上往下画横线。在图里我们取任意的一条光栅化直线,这条直线左边的端点x值为XL,右边的为XR。