前面两天画了点和线,今天我们来画一个最简单也是最强大的面——三角形。
本文主要讲解三角形绘制算法的推导和思路(只涉及到一点点的向量知识),最后会给出代码实现,大家放心的看下去就好。
1.如何画一个三角形?
在正式开始这一小节前,我们先想一下如何利用上一节的画线算法绘制一个实心的三角形。
假设现在平面内有三个不共线的点组成一个三角形,我们可以利用上一节的直线算法轻易的连接三角形的三条边,这时候我们会生成一个空心的、封闭的三角形。
那么这时候问题就转换为,如何把这个空心的三角形变为一个实心的三角形?
我想大家这时候已经有思路了,就是一行一行地扫描像素,把两个边界点之间的像素全部涂满上色就可以了。
这个方法肯定是可以的,但是实现不是很优雅,也不是业内的主流实现方式。因为基于行扫描的算法不是本文的重点,所以详细的推导和代码实现就不提供了,感兴趣的同学可以自己尝试实现一下。
2.利用向量叉乘画三角形
开始本节前先简单复习一下向量叉乘的几何意义。
2.1 数学推导
在三维空间中,两个三维向量 a \mathbf {a} a 和 b \mathbf {b} b 做叉乘,会得到一个和已知两个向量垂直的新向量 a × b \mathbf {a} \times \mathbf {b} a×b。
既然叉乘产生的是一个新向量,那么它肯定有个方向,我们一般用右手定则来判断:将右手食指指向 a \mathbf {a} a 的方向、中指指向 b \mathbf {b} b 的方向,则此时拇指的方向即为 a × b \mathbf {a} \times \mathbf {b} a×b 的方向。
综上所述,我们可以对向量叉乘做一个严谨的定义:
a × b = ∥ a ∥ ∥ b ∥ sin ( θ ) n {\displaystyle \mathbf {a} \times \mathbf {b} =\|\mathbf {a} \|\|\mathbf {b} \|\sin(\theta )\ \mathbf {n} } a×b=∥a∥∥b∥sin(θ) n
其中 θ \theta θ 表示 a \mathbf {a} a 和 b \mathbf {b} b 在它们所定义的平面上的夹角( 0 ∘ ≤ θ ≤ 18 0 ∘ 0^{\circ }\leq \theta \leq 180^{\circ} 0∘≤θ≤180∘)。 ∥ a ∥ {\displaystyle \|\mathbf {a} \|} ∥a∥ 和 ∥ b ∥ \displaystyle \|\mathbf {b} \| ∥b∥ 是向量 a \mathbf {a} a 和 b \mathbf {b} b 的模长,而 n \mathbf{n} n 则是一个与 a \mathbf {a} a 、 b \mathbf {b} b 所构成的平面垂直的单位向量,方向由右手定则决定。
有上面的理论,我们就可以判断两个向量的相对位置:
- a \mathbf {a} a 向量叉乘 b \mathbf {b} b 向量,如果值为正,则表示 b \mathbf {b} b 向量在 a \mathbf {a} a 向量左侧
- a \mathbf {a} a 向量叉乘 b \mathbf {b} b 向量,如果值为负,则表示 b \mathbf {b} b 向量在 a \mathbf {a} a 向量右侧
- a \mathbf {a} a 向量叉乘 b \mathbf {b} b 向量,如果值为零,则表示 b \mathbf {b} b 向量与 a \mathbf {a} a 向量共线
会判断两条线的相对位置了,我们可以做个理论迁移,利用向量叉乘判断点和三角形的位置关系。
例如下面这里例子,对于三角形 Δ A B C \Delta ABC ΔABC 来说,把三条边看作 A B → \overrightarrow{A B} AB、 B C → \overrightarrow{B C} BC、 C A → \overrightarrow{C A} CA 三条首尾相连的向量,平面内有一个点 P P P,我们通过向量叉乘来判断相对位置:
- A B → × A P → \overrightarrow{A B} \times \overrightarrow{A P} AB×AP,值为正,故 P P P 在 A B AB AB 左侧
- B C → × B P → \overrightarrow{B C} \times \overrightarrow{B P} BC×BP,值为正,故 P P P 在 B C BC BC 左侧
- C A → × C P → \overrightarrow{C A} \times \overrightarrow{C P} CA×CP,值为正,故 P P P 在 A C AC AC 左侧
综合以上三个限制条件,我们可以判断 P P P 在 Δ A B C \Delta ABC ΔABC 内。
如果上面三个计算中有值为负的情况,说明 P P P 在三角形外;如果有值为 0 的情况,说明 P P P 在三角形的边或顶点上。
2.2 代码实现
理论基础复习完了,我们就可以写代码了。代码实现相当简单,我们构建一个函数 crossProduct
,传入三角形的三个顶点和平面上的任意一点 P P P,然后根据四个顶点构建出向量计算叉乘就可以了:
// 利用叉乘判断是否在三角形内部
Vec3i crossProduct(Vec2i *pts, Vec2i P) {
// 构建出三角形 ABC 三条边的向量
Vec2i AB(pts[1].x - pts[0].x, pts[1].y - pts[0].y);
Vec2i BC(pts[2].x - pts[1].x, pts[2].y - pts[1].y);
Vec2i CA(pts[0].x - pts[2].x, pts[0].y - pts[2].y);
// 三角形三个顶点和 P 链接形成的向量
Vec2i AP(P.x - pts[0].x, P.y - pts[0].y);
Vec2i BP(P.x - pts[1].x, P.y - pts[1].y);
Vec2i CP(P.x - pts[2].x, P.y - pts[2].y);
return Vec3i(AB^AP, BC^BP, CA^CP);
}
代码非常的简单,我们跑一个简单的例子验证一下:
void drawSingleTriangle() {
// 图片的宽高
int width = 200;
int height = 200;
TGAImage frame(width, height, TGAImage::RGB);
Vec2i pts[3] = {
Vec2i(10, 10), Vec2i(150, 30)