我们最终是要将三维的物体绘制到平面上,那么首先我们需要明白的是如何在屏幕上绘制二维的物体。而在二维的多边形中,三角形无疑是最特别的一种,它具有以下特性:
- 是边数最少多边形
- 所有多边形可以都可拆解成三角形
- 即使在三维空间中,三角形的所有顶点也必然在同一平面上
所以我们的软件渲染器的第一步,就是要在屏幕上绘制出一个三角形。绘制三角形的方式多种多样,而光栅化主要采用的是一种叫做重心坐标的方法。
重心坐标
如上图所示,重心坐标的定义为,对于平面内任意一点P,都可以用三角形的三个顶点表示,即
P
=
α
A
+
β
B
+
γ
C
\displaystyle P\ =\ αA\ +\ βB\ +\ γC
P = αA + βB + γC,并且还满足
α
+
β
+
γ
=
1
\displaystyle α\ +\ β\ +\ γ\ =\ 1
α + β + γ = 1。而如果P点在三角形内,那么还会满足
(
0
⩽
α
⩽
1
,
0
⩽
β
⩽
1
,
0
⩽
γ
⩽
1
)
\displaystyle ( 0\leqslant α\leqslant 1,\ 0\leqslant β\leqslant 1,\ 0\leqslant γ\leqslant 1)
(0⩽α⩽1, 0⩽β⩽1, 0⩽γ⩽1)。可以类比求两点间一点公式来更好的理解:
P
=
α
A
+
(
1
−
α
)
B
\displaystyle P\ =\ αA\ +\ ( 1\ -\ α) B
P = αA + (1 − α)B。
光栅化的过程其实就是求屏幕上每个像素的颜色的过程,所以我们可以遍历三角形可能覆盖的所有点作为点P,求出其重心坐标
(
α
,
β
,
γ
)
\displaystyle(\ α\ ,\ β\ ,\ γ\ )
( α , β , γ ),然后P点的任意属性(颜色,uv,深度等等)我们都可以通过三角形的三个顶点根据重心坐标插值求得了。下面我们来推导下重心坐标该怎么求。
公式推导
根据重心坐标的定义,我们可以把公式改写为
P
=
(
1
−
β
−
γ
)
A
+
β
B
+
γ
C
\begin{aligned} P\ =\ ( 1\ -\ β\ -\ γ) A\ +\ βB\ +\ γC \end{aligned}
P = (1 − β − γ)A + βB + γC 我们还可以进一步将其转换为关于三个向量的公式
P
A
→
+
β
A
B
→
+
γ
A
C
→
=
0
⃗
\begin{aligned} \overrightarrow{PA} \ +\ β\overrightarrow{AB} \ +\ γ\overrightarrow{AC} \ =\ \vec{0} \end{aligned}
PA + βAB + γAC = 0 这个公式还可以进一步拆成两个公式,分别计算x,y
P
A
→
x
+
β
A
B
→
x
+
γ
A
C
→
x
=
0
\begin{aligned} \overrightarrow{PA}_{x} +\ β\overrightarrow{AB}_{x} +\ γ\overrightarrow{AC}_{x} \ =\ 0 \end{aligned}
PAx+ βABx+ γACx = 0
P
A
→
y
+
β
A
B
→
y
+
γ
A
C
→
y
=
0
\begin{aligned} \overrightarrow{PA}_{y} +\ β\overrightarrow{AB}_{y} +\ γ\overrightarrow{AC}_{y} \ =\ 0 \end{aligned}
PAy+ βABy+ γACy = 0 这两个公式可以再转为矩阵形式表示
[
β
γ
1
]
[
A
B
→
x
A
C
→
x
P
A
→
x
]
=
0
\begin{bmatrix} β & γ & 1 \end{bmatrix}\begin{bmatrix} \overrightarrow{AB}_{x}\\ \overrightarrow{AC}_{x}\\ \overrightarrow{PA}_{x} \end{bmatrix} \ =0
[βγ1]⎣⎢⎢⎡ABxACxPAx⎦⎥⎥⎤ =0
[
β
γ
1
]
[
A
B
→
y
A
C
→
y
P
A
→
y
]
=
0
\begin{bmatrix} β & γ & 1 \end{bmatrix}\begin{bmatrix} \overrightarrow{AB}_{y}\\ \overrightarrow{AC}_{y}\\ \overrightarrow{PA}_{y} \end{bmatrix} \ =0
[βγ1]⎣⎢⎢⎡AByACyPAy⎦⎥⎥⎤ =0 从上面的公式可以看出,向量
(
β
,
γ
,
1
)
\displaystyle ( β,\ γ,\ 1)
(β, γ, 1)分别与向量
(
A
B
→
x
,
A
C
→
x
,
P
A
→
x
)
\displaystyle (\overrightarrow{AB}_{x} ,\ \overrightarrow{AC}_{x} ,\ \overrightarrow{PA}_{x})
(ABx, ACx, PAx)和向量
(
A
B
→
y
,
A
C
→
y
,
P
A
→
y
)
\displaystyle (\overrightarrow{AB}_{y} ,\ \overrightarrow{AC}_{y} ,\ \overrightarrow{PA}_{y})
(ABy, ACy, PAy)垂直,即是这两个向量的叉乘。假定
(
A
B
→
x
,
A
C
→
x
,
P
A
→
x
)
\displaystyle (\overrightarrow{AB}_{x} ,\ \overrightarrow{AC}_{x} ,\ \overrightarrow{PA}_{x})
(ABx, ACx, PAx)和向量
(
A
B
→
y
,
A
C
→
y
,
P
A
→
y
)
\displaystyle (\overrightarrow{AB}_{y} ,\ \overrightarrow{AC}_{y} ,\ \overrightarrow{PA}_{y})
(ABy, ACy, PAy)的叉乘为向量
u
\displaystyle u
u,那么
β
=
u
.
x
/
u
.
z
\displaystyle β\ =\ u.x\ /\ u.z
β = u.x / u.z,
γ
=
u
.
y
/
u
.
z
\displaystyle γ\ =\ u.y\ /\ u.z
γ = u.y / u.z。如果
u
.
z
\displaystyle u.z
u.z为0,则说明该三角形其实已经退化成了线段。
代码实现
Vector3 DrawUtil::CalcuBarycentric(Vector2 *pts, Vector2 point) {
Vector3 temp[2];
for (int i = 0; i < 2; i++) {
temp[i][0] = pts[1][i] - pts[0][i];
temp[i][1] = pts[2][i] - pts[0][i];
temp[i][2] = pts[0][i] - point[i];
}
Vector3 u = temp[0].cross(temp[1]);
if (std::abs(u.z) > 1e-2) {
return Vector3(1.0f - (u.x + u.y) / u.z, u.x / u.z, u.y / u.z);
}
return Vector3(-1.0f, 1.0f, 1.0f);
}
总结
在绘制三角形这一章中,我们着重介绍了重心坐标的定义和推导,有了重心坐标,我们就可以通过三角形三个顶点的各种属性,插值得到三角形内任一点的各种属性。这对于光栅化渲染器是有着相当广泛的应用的,举个例子:
Z-Buffer,即深度缓存,当三角形投影到了屏幕上后,它的z值我们并不是直接舍弃不用,相反,我们可以用z值来判断三角形距屏幕的距离,离屏幕近的肯定是会覆盖后面的。但是在空间中,两个三角形是完全可能互相穿插的,所以我们真正要比较的是像素所对应的三角形内的那一点的远近关系,而这一点的z值则是通过对三角形三个顶点z值进行插值得到的。Z-buffer则是用来记录某一像素最近深度的,在绘制过程中如果遇到大于缓存中深度的片段,就会直接舍弃。