系列文章目录
简介:Computer Graphics From Scratch-《从零开始的计算机图形学》简介
第一章: Computer Graphics From Scratch - Chapter 1 介绍性概念
第二章:Computer Graphics From Scratch - Chapter 2 基本光线追踪
第三章:Computer Graphics From Scratch - Chapter 3 光照
第四章:Computer Graphics From Scratch - Chapter 4 阴影和反射
第五章:Computer Graphics From Scratch - Chapter 5 扩展光线追踪
第六章:Computer Graphics From Scratch - Chapter 6 线条
Chapter 7
- 系列文章目录
- Filled Triangle - 实心三角形
- 一、Drawing Wireframe Triangles - 绘制线框三角形
- 二、Drawing Filled Triangles - 绘制填充三角形
- 三、Summary - 概括
Filled Triangle - 实心三角形
In the previous chapter, we took our first steps toward drawing simple shapes—namely, straight line segments—using only PutPixel and an algorithm based on simple math. In this chapter, we’ll reuse some of the math to draw something more interesting: a filled triangle.
在上一章中,我们迈出了绘制简单形状的第一步,即仅使用PutPixel
和基于简单数学的算法绘制直线段。在本章中,我们将重用一些数学来绘制更有趣的东西:实心三角形。
一、Drawing Wireframe Triangles - 绘制线框三角形
我们可以使用DrawLine
方法绘制三角形的轮廓:
DrawWireframeTriangle (P0, P1, P2, color)
{
DrawLine(P0, P1, color);
DrawLine(P1, P2, color);
DrawLine(P2, P0, color);
}
这种轮廓被称为线框,因为它看起来像一个由线条组成的三角形,如图7-1所示:
图 7-1: A wireframe triangle with vertices (–200,–250), (200,50), and (20,250)
这是一个充满希望的开始!接下来我们将探讨如何用颜色填充三角形。
二、Drawing Filled Triangles - 绘制填充三角形
我们想画一个三角形,用我们选择的颜色填充。正如计算机图形学中的情况一样,解决这个问题的方法不止一种。我们将通过将填充三角形视为水平线段的集合来绘制填充三角形,这些线段在绘制时看起来像三角形。图7-2显示了如果我们可以看到单独的线段,这样的三角形会是什么样子。
图7-2:使用水平线段绘制填充三角形
以下是我们想要做的事情的一个非常粗略的近似值:
for each horizontal line y between the triangle's top and bottom
compute x_left and x_right for this y
DrawLine(x_left, y, x_right, y)
让我们从“三角形的顶部和底部之间”开始。三角形由其三个顶点 P 0 P_0 P0、 P 1 P_1 P1和 P 2 P_2 P2定义。如果 我们 通过增加 y y y值对这些点进行排序,使得 y 0 ≤ y 1 ≤ y 2 y_0≤y_1≤y_2 y0≤y1≤y2,则三角形所占的y值范围为 [ y 0 , y 2 ] [y_0,y_2] [y0,y2]:
if y1 < y0 { swap(P1, P0) }
if y2 < y0 { swap(P2, P0) }
if y2 < y1 { swap(P2, P1) }
通过这种方式对顶点进行排序可以使事情变得更简单:在这样做之后,我们可以始终假设 P 0 P_0 P0是三角形的最低点, P 2 P_2 P2是最高点,因此我们不必处理所有可能的排序。
接下来我们要计算x_left
和x_right
数组。这有点棘手,因为三角形有三条边,而不是两条。然而,仅考虑
y
y
y的值,我们总是有从
P
0
P_0
P0到
P
2
P_2
P2的“高”边,以及从
P
0
P_0
P0至
P
1
P_1
P1和
P
1
P_1
P1至
P
2
P_2
P2的两个“短”边。
当
y
0
=
y
1
y_0=y_1
y0=y1或
y
1
=
y
2
y_1=y_2
y1=y2 时有一种特殊情况,即三角形的一条边是水平的。当这种情况发生时,另两个边的高度相同,因此任何一个都可以被认为是“高”边。我们应该选择右边还是左边?幸运的是,这并不重要;
该算法将支持从左到右和从右到左的水平线,因此我们可以坚持我们的定义,即“高”侧是从
P
0
P_0
P0到
P
2
P_2
P2的一侧。
x_right
的值将来自高边或连接短边;x_left
的值将来自另一个集合。我们将从计算三条边的
x
x
x值开始。因为我们将绘制水平线段,所以我们希望每个
y
y
y值都有一个
x
x
x值;这意味着我们可以使用插值计算这些值,其中
y
y
y为自变量,
x
x
x为因变量:
x01 = Interpolate(y0, x0, y1, x1)
x12 = Interpolate(y1, x1, y2, x2)
x02 = Interpolate(y0, x0, y2, x2)
其中一侧的 x x x值为 x 02 x02 x02;另一侧的值来自 x 01 x01 x01和 x 12 x12 x12的串联。注意,有一个重复的 x 01 x01 x01和 x 12 x12 x12中的值: y 1 y1 y1的 x x x值既是 x 01 x01 x01的最后一个值,也是 x 12 x12 x12的第一个值。我们只需要去掉其中一个(我们任意选择 x 01 x01 x01的最后一个值),然后连接数组:
remove_last(x01)
x012 = x01 + x12
我们最终得到了
x
02
x02
x02和
x
012
x012
x012,我们需要确定哪个是x_left
,哪个是x_right
。要做到这一点,我们可以选择任意一条水平线(例如,中间的一条),并比较
x
02
x02
x02和
x
012
x012
x012中的
x
x
x值:如果
x
02
x02
x02中的
x
x
x小于
x
012
x012
x012中,那么我们知道
x
02
x02
x02必须是x_left
;否则,它必须是x_right
。
m = floor(x02.length / 2)
if x02[m] < x012[m] {
x_left = x02
x_right = x012
} else {
x_left = x012
x_right = x02
}
现在我们有了绘制水平段所需的所有数据。我们可以用DrawLine
来做这个。然而,DrawLine是一个非常通用的函数,在这种情况下,我们总是从左到右绘制水平线,因此使用简单的for循环更有效。这也让我们对我们绘制的每个像素有了更多的“控制”,这在接下来的章节中特别有用。
示例7-1显示了完整的DrawFilledTriangle。
DrawFilledTriangle (P0, P1, P2, color)
{
❶ // Sort the points so that y0 <= y1 <= y2
if y1 < y0 { swap(P1, P0) }
if y2 < y0 { swap(P2, P0) }
if y2 < y1 { swap(P2, P1) }
❷ // Compute the x coordinates of the triangle edges
x01 = Interpolate(y0, x0, y1, x1)
x12 = Interpolate(y1, x1, y2, x2)
x02 = Interpolate(y0, x0, y2, x2)
❸ // Concatenate the short sides
remove_last(x01)
x012 = x01 + x12
❹ // Determine which is left and which is right
m = floor(x012.length / 2)
if x02[m] < x012[m]
{
x_left = x02
x_right = x012
}
else
{
x_left = x012
x_right = x02
}
❺ // Draw the horizontal segments
for y = y0 to y2
{
for x = x_left[y - y0] to x_right[y - y0]
{
canvas.PutPixel(x, y, color)
}
}
}
示例7-1:绘制填充三角形的函数
让我们看看这里发生了什么。函数接收三个顶点,以任意顺序将三角形作为参数。我们的算法需要他们按从下到上的顺序排列,所以我们这样排序❶.
接下来,我们计算三条边的每个y值的x值❷, 并连接阵列;
从两个“短”的侧面❸. 然后我们计算出哪个是x_left,哪个是x_right❹.
最后,对于顶部和在三角形的底部,我们得到它的左x坐标和右x坐标,并绘制逐像素分段❺.
结果如图7-3所示;出于验证目的,我们调用DrawFilledTriangle
,然后使用相同坐标但不同颜色的DrawWireframeTriangle
。无论何时都要验证结果,这是查找代码中错误的一种非常有效的方法!
图7-3:带线框边缘的实心三角形,用于验证
您可以在https://gabrielgambetta.com/cgfs/triangle-demo;
您可能会注意到三角形的黑色轮廓与绿色内部区域不完全匹配;这在三角形的右下边缘尤其明显。这是因为DrawLine
正在计算该边的
y
=
f
(
x
)
y=f(x)
y=f(x),但是
DrawTriangle
正在计算
x
=
f
(
y
)
x=f(y)
x=f(y),由于四舍五入,这可能会产生稍微不同的结果。这是一种我们愿意接受的近似误差,以使我们的渲染算法更快。
三、Summary - 概括
在本章中,我们开发了一种在画布上绘制填充三角形的算法。这是绘制线段的一个步骤。我们还学会了将三角形视为一组可以单独使用的水平线段。
在下一章中,我们将扩展数学和算法,以绘制一个填充有颜色渐变的三角形;算法背后的数学和推理将是本书中开发的其他功能的关键。