Shaded Triangles 阴影三角形

In the previous chapter, we developed an algorithm to draw a triangle filled with a solid color. Our goal for this chapter is to draw a shaded triangle—that is, a triangle filled with a color gradient.
在上一章中,我们开发了一种算法来绘制一个用纯色填充的三角形。本章的目标是画一个有阴影的三角形,也就是一个充满颜色渐变的三角形。

Defining Our Problem 界定我们的问题

We want to fill the triangle with different shades of a single color. It will look like Figure 8-1.
我们要用一种颜色的不同深浅来填充这个三角形。它将看起来像图8-1。

We need a more formal definition of what we’re trying to draw. To do this, we’ll assign a real value h to each vertex, denoting the intensity of the color at the vertex. h is in the [0.0,1.0] range, where 0.0 represents the darkest possible shade (that is, black) and 1.0 represents the brightest possible shade (that is, the original color—not white!).
我们需要对我们要画的东西做一个更正式的定义。为了做到这一点,我们将给每个顶点分配一个实值hℎ,表示该顶点的颜色强度。hℎ在[0.0,1.0][ 0.0 , 1.0]范围内,其中0.0 0.0代表可能的最暗阴影(即黑色),1.0 1.0代表可能的最亮阴影(即原始颜色,而不是白色!)。

Figure 8-1: A shaded triangle

To compute the exact color shade of a pixel given the base color of the triangle C and the intensity at that pixel h, we’ll multiply channel-wise: Ch=(RCh,GCh,BCh). Therefore h=0.0 yields pure black, h=1.0 yields the original color C, and h=0.5 yields a color half as bright as the original one.
为了计算一个像素的确切颜色,给定三角形的基色 C 𝐶和该像素的强度 h ℎ,我们将进行通道相乘。C h =( R C ⋅h, G C ⋅h, B C ⋅h) 𝐶 ℎ =( 𝑅 ℎ , 𝐺 𝐶 ⋅ ℎ , 𝐵 ℎ ) 。因此,h=0.0 ℎ=0.0 得到纯黑,h=1.0 ℎ=1.0 得到原始颜色 C 𝐶,h=0.5 ℎ=0.5 得到的颜色是原始颜色的一半。

Computing Edge Shading 计算边缘阴影

In order to draw a shaded triangle, all we need to do is compute a value of h for each pixel of the triangle, compute the corresponding shade of the color, and paint the pixel. Easy!
为了画一个有阴影的三角形,我们需要做的就是为三角形的每个像素计算一个hℎ的值,计算出相应的颜色的阴影,然后画出这个像素。很简单!

At this point, however, we only know the values of h for the triangle vertices, because we chose them. How do we compute values of h for the rest of the triangle?
然而,在这一点上,我们只知道三角形顶点的hℎ值,因为我们选择了这些顶点。我们如何计算三角形其他部分的hℎ值?

Let’s start with the edges of the triangle. Consider the edge AB. We know hA and hB. What happens at M, the midpoint of AB? Since we want the intensity to vary smoothly from A to B, the value of hM must be between hA and hB. Since M is in the middle of AB, why not choose hM to be in the middle of hA and hB—that is, their average?
让我们从三角形的边开始。考虑边AB 𝐴 𝐵。我们知道h Aℎ 𝐴和h Bℎ 𝐵。在M𝑀,即AB𝐴𝐵的中点会发生什么?由于我们希望强度从A𝐴到B𝐵平滑变化,h Mℎ𝑀的值必须在h Aℎ𝐴和h Bℎ𝐵之间。既然M𝑀在AB𝐴𝐵的中间,为什么不选择h Mℎ𝑀在h Aℎ𝐴和h Bℎ𝐵的中间--即它们的平均值?

More formally, we have a function h=f(P) that gives each point P an intensity value h; we know its values at A and B, h(A)=hA and h(B)=hB, respectively. We want this function to be smooth. Since we know nothing else about h=f(P), we can choose any function that is compatible with what we do know, such as a linear function (Figure 8-2).
更正式地说,我们有一个函数h=f(P) ℎ = 𝑓 ( 𝑃 ) ,给每一个点P𝑃一个强度值h ℎ。我们知道它在A𝐴和B𝐵的值,h(A)= h A ℎ ( 𝐴 ) = ℎ 𝐴,h(B)= h B ℎ ( 𝐵 ) = ℎ 𝐵,分别。我们希望这个函数是平滑的。由于我们对h=f(P) ℎ = 𝑓 ( 𝑃 )一无所知,我们可以选择任何与我们所知道的相符的函数,如线性函数(图8-2)。

Figure 8-2: A linear function h(P), compatible with what we know about h(A) and h(B)

This is suspiciously similar to the situation in the previous chapter: we had a linear function x=f(y), we knew the values of this function at the vertices of the triangle, and we wanted to compute values of x along its sides. We can compute values of h along the sides of the triangle in a very similar way, using Interpolate with y as the independent variable (the values we know) and h as the dependent variable (the values we want):
这与上一章的情况很相似:我们有一个线性函数x=f(y) 𝑥=𝑓 ( 𝑦 ),我们知道这个函数在三角形各顶点的值,我们想计算x 𝑥沿边的值。我们可以用非常类似的方法计算h ℎ沿三角形各边的值,用 Interpolate 作为自变量(我们知道的值),h作为因变量(我们想要的值)。

x01 = Interpolate(y0, x0, y1, x1)
h01 = Interpolate(y0, h0, y1, h1)

x12 = Interpolate(y1, x1, y2, x2)
h12 = Interpolate(y1, h1, y2, h2)

x02 = Interpolate(y0, x0, y2, x2)
h02 = Interpolate(y0, h0, y2, h2)

Next, we concatenated the x arrays for the “short” sides and then determined which of x02 and x012 was x_left and which was x_right. Again, we can do something very similar here for the h vectors.
接下来,我们将 "短 "边的x𝑥数组连接起来,然后确定 x02x012 中哪个是 x_left ,哪个是 x_right 。同样,我们可以对hℎ向量做一些非常类似的事情。

However, we will always use the x values to determine which side is left and which side is right, and the h values will just “follow along.” x and h are properties of actual points on the screen, so we can’t freely mix-and-match left- and right-side values.
然而,我们总是用x𝑥的值来确定哪边是左,哪边是右,而hℎ的值只是 "跟随"。x𝑥和hℎ是屏幕上实际点的属性,所以我们不能自由地混合和匹配左边和右边的值。

We can code this as follows:
我们可以对其进行编码,如下所示。

// Concatenate the short sides
remove_last(x01)
x012 = x01 + x12

remove_last(h01)
h012 = h01 + h12

// Determine which is left and which is right
m = floor(x012.length / 2)
if x02[m] < x012[m] {
    x_left = x02
    h_left = h02

    x_right = x012
    h_right = h012
} else {
    x_left = x012
    h_left = h012

    x_right = x02
    h_right = h02
}

This is very similar to the relevant section of the code in the previous chapter (Listing 7-1), except that every time we do something with an x vector, we do the same with the corresponding h vector.
这与上一章中的相关代码部分(清单7-1)非常相似,只是每次我们对 x 向量做什么,都会对相应的 h 向量做同样的事。

Computing Interior Shading 计算室内遮阳

The last step is drawing the actual horizontal segments. For each segment, we know x_left and x_right, as in the previous chapter; now we also know h_left and h_right. But this time we can’t just iterate from left to right and draw every pixel with the base color: we need to compute a value of h for each pixel of the segment.
最后一步是绘制实际的水平线段。对于每一段,我们知道 x_leftx_right ,就像上一章一样;现在我们还知道 h_lefth_right 。但这次我们不能只是从左到右迭代,用基色绘制每个像素:我们需要为线段的每个像素计算一个hℎ值。

Again, we can assume h varies linearly with x, and use Interpolate to compute these values. In this case, the independent variable is x, and it goes from the x_left value to the x_right value of the specific horizontal segment we’re shading; the dependent variable is h, and its corresponding values for x_left and x_right are h_left and h_right for that segment:
同样,我们可以假设hℎ与x𝑥呈线性变化,并使用 Interpolate 来计算这些值。在这种情况下,自变量是x𝑥,它从 x_left 值到 x_right 值的特定水平段,我们正在着色;因变量是hℎ,它的 x_leftx_right 的对应值是该段的 h_lefth_right

x_left_this_y = x_left[y - y0]
h_left_this_y = h_left[y - y0]

x_right_this_y = x_right[y - y0]
h_right_this_y = h_right[y - y0]

h_segment = Interpolate(x_left_this_y, h_left_this_y,
                        x_right_this_y, h_right_this_y)

Or, expressed in a more compact way:
或者,以一种更紧凑的方式来表达。

h_segment = Interpolate(x_left[y - y0], h_left[y - y0],
                        x_right[y - y0], h_right[y - y0])

Now it’s just a matter of computing the color for each pixel and painting it! Listing 8-1 shows the complete pseudocode for DrawShadedTriangle.
现在只需计算每个像素的颜色,并为其上色即可清单8-1显示了 DrawShadedTriangle 的完整伪代码。

DrawShadedTriangle (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 and h values of the triangle edges
    x01 = Interpolate(y0, x0, y1, x1)
    h01 = Interpolate(y0, h0, y1, h1)

    x12 = Interpolate(y1, x1, y2, x2)
    h12 = Interpolate(y1, h1, y2, h2)

    x02 = Interpolate(y0, x0, y2, x2)
    h02 = Interpolate(y0, h0, y2, h2)

    // Concatenate the short sides
    remove_last(x01)
    x012 = x01 + x12

    remove_last(h01)
    h012 = h01 + h12

    // Determine which is left and which is right
    m = floor(x012.length / 2)
    if x02[m] < x012[m] {
        x_left = x02
        h_left = h02

        x_right = x012
        h_right = h012
    } else {
        x_left = x012
        h_left = h012

        x_right = x02
        h_right = h02
    }

    // Draw the horizontal segments
   ❷for y = y0 to y2 {
        x_l = x_left[y - y0]
        x_r = x_right[y - y0]

       ❸h_segment = Interpolate(x_l, h_left[y - y0], x_r, h_right[y - y0])
        for x = x_l to x_r {
           ❹shaded_color = color * h_segment[x - x_l]
            canvas.PutPixel(x, y, shaded_color)
        }
    }
}
Listing 8-1: A function for drawing shaded triangles
清单8-1:一个绘制阴影三角形的函数

The pseudocode for this function is very similar to that for the function developed in the previous chapter (Listing 7-1). Before the horizontal segment loop ❷, we manipulate the x vectors and the h vectors in similar ways, as explained above. Inside the loop, we have an extra call to Interpolate ❸ to compute the h values for every pixel in the current horizontal segment. Finally, in the inner loop we use the interpolated values of h to compute a color for each pixel ❹.
这个函数的伪码与上一章开发的函数(清单7-1)非常相似。在水平段循环❷之前,我们以类似的方式处理x𝑥向量和hℎ向量,如上所述。在循环内,我们有一个额外的调用 Interpolate ❸ 来计算当前水平段中每个像素的 h ℎ 值。最后,在内循环中,我们使用hℎ的内插值来计算每个像素❹的颜色。

Note that we’re sorting the triangle vertices as before ❶. However, we now consider these vertices and their attributes, such as the intensity value h, to be an indivisible whole; that is, swapping the coordinates of two vertices must also swap their attributes.
请注意,我们像以前一样对三角形顶点进行排序 ❶。然而,我们现在认为这些顶点和它们的属性,例如强度值hℎ,是一个不可分割的整体;也就是说,交换两个顶点的坐标也必须交换它们的属性。

Source code and live demo >>
源代码和现场演示 >>

Summary

In this chapter, we’ve extended the triangle-drawing code developed in the previous chapter to support smoothly shaded triangles. Note that we can still use it to draw single color triangles by using 1.0 as the value of h for all three vertices.
在这一章中,我们扩展了上一章开发的三角形绘制代码,以支持平滑着色的三角形。请注意,我们仍然可以用它来绘制单色三角形,方法是对所有三个顶点使用1.0作为hℎ的值。

The idea behind this algorithm is actually more general than it seems. The fact that h is an intensity value has no impact on the “shape” of the algorithm; we assign meaning to this value only at the very end, when we’re about to call PutPixel. This means we could use this algorithm to compute the value of any attribute of the vertices of the triangle, for every pixel of the triangle, as long as we assume this value varies linearly on the screen.
这个算法背后的想法实际上比它看起来更普遍。hℎ是一个强度值,这对算法的 "形状 "没有任何影响;我们只是在最后,当我们要调用 PutPixel 时,才给这个值赋予了意义。这意味着我们可以用这个算法来计算三角形顶点的任何属性的值,对于三角形的每个像素,只要我们假设这个值在屏幕上是线性变化的。

We will indeed use this algorithm to improve the visual appearance of our triangles in the upcoming chapters. For this reason, it’s a good idea to make sure you really understand this algorithm before proceeding further.
在接下来的章节中,我们确实会使用这个算法来改善我们的三角形的视觉外观。出于这个原因,在进一步开展工作之前,最好确保你真正理解这个算法。

In the next chapter, however, we take a small detour. Having mastered the drawing of triangles on a 2D canvas, we will turn our attention to the third dimension.
不过,在下一章中,我们要绕一个小弯。在掌握了在二维画布上绘制三角形的方法后,我们将把注意力转向三维空间。