Shading

Let’s continue making our images more realistic; in this chapter, we’ll examine how to add lights to the scene and how to illuminate the objects it contains. First, let’s look at a bit of terminology.
让我们继续使我们的图像更加逼真;在这一章中,我们将研究如何为场景添加灯光以及如何照亮它所包含的物体。首先,让我们看一下术语。

Shading vs. Illumination 阴影与光照

The title of this chapter is “Shading," not”Illumination"; these are two different but closely related concepts. Illumination refers to the math and algorithms necessary to compute the effect of light on a single point in the scene; shading deals with techniques that extend the effect of light on a discrete set of points to entire objects.
本章的标题是 "Shading",而不是 "Illumination";这是两个不同但密切相关的概念。照明是指计算光线对场景中某一点的影响所必需的数学和算法;阴影是指将光线对离散点的影响扩展到整个物体的技术。

In Chapter 3 (Light), we looked at all we need to know about illumination. We can define ambient, point, and directional lights, and we can compute the
在第三章(光)中,我们看了所有我们需要知道的关于照明的知识。我们可以定义环境光、点光和定向光,我们可以计算出

illumination at any point in the scene given its position and a surface normal at that point:
给出场景中任何一点的位置和该点的表面法线,就可以得到该点的照度。

IP=IA+i=1nIi[N,Li|N||Li|+(Ri,V|Ri||V|)s]
I P = I A + ∑ i=1 n I i ⋅ ⎡ ⎣ ⎢ ⟨ N ⃗ , L i → ⟩ | N ⃗ || L i → | + ⎛ ⎝ ⟨ R i → , V ⟩ | R i → || V ⃗ | ⎞ ⎠ s ⎤ ⎦⎥ 𝐼 𝑃 = 𝐼 𝐴 + ∑𝑖 = 1𝑛 𝐼 𝑖⋅ [ ⟨ 𝑁 → 。𝐿 𝑖 → ⟩ | 𝑁 | 𝐿 𝑖 → | + ( ⟨ 𝑅 𝑖 → , 𝑉 → ⟩ | 𝑅 𝑠 ]

This illumination equation expresses how light illuminates a point in the scene. The way this worked in our raytracer is exactly the same way it works in our rasterizer.
这个照度方程表达了光线如何照亮场景中的一个点。这在我们的光线追踪器中的工作方式与在光栅器中的工作方式完全相同。

The more interesting part, which we’ll explore in this chapter, is how to extend the “illumination at a point” algorithms we developed into “illumination at every point of a triangle” algorithms.
更有趣的是,我们将在本章中探讨如何将我们开发的 "点上的照明 "算法扩展为 "三角形每一点上的照明 "算法。

Flat Shading 平坦的阴影

Let’s start simple. Since we can compute illumination at a point, we can just pick any point in a triangle (for example, its center), compute the illumination at that point, and use it to shade the whole triangle. To do the actual shading, we can multiply the color of the triangle by the illumination value. Figure 13-1 shows the results.
让我们从简单的开始。由于我们可以计算一个点的照度,我们可以在一个三角形中任意选取一个点(例如,它的中心),计算该点的照度,然后用它来为整个三角形着色。为了进行实际的着色,我们可以将三角形的颜色乘以照度值。图13-1显示了这个结果。

Figure 13-1: In flat shading, we compute illumination at the center of the triangle and use it for the entire triangle.

The results are promising. Every point in a triangle has the same normal, so as long as a light is reasonably far from it, the light vectors for every point are approximately parallel and every point receives approximately the same amount of light. The discontinuity between the two triangles that make up each side of the cube, especially visible on the green face in Figure 13-1, is a consequence of the light vectors being approximately, but not exactly, parallel.
结果是有希望的。三角形中的每一个点都有相同的法线,所以只要一束光离它有合理的距离,每一个点的光矢量都是近似平行的,每一个点收到的光量都是近似相同的。构成立方体每条边的两个三角形之间的不连续性,特别是在图13-1中的绿色面上可见,是光矢量近似平行但不完全平行的结果。

So what happens if we try this technique with an object for which every point has a different normal, like the sphere in Figure 13-2?
那么,如果我们对一个每一个点都有不同法线的物体尝试这种技术,比如图13-2中的球体,会发生什么?

Figure 13-2: Flat shading works reasonably well for objects with flat faces, but not so well for objects that are supposed to be curved.

Not so good. It is very obvious that the object is not a true sphere, but an approximation made out of flat, triangular patches. Because this kind of illumination makes curved objects look flat, it’s called flat shading.
不是那么好。很明显,这个物体不是一个真正的球体,而是一个由扁平的三角形斑块组成的近似物。因为这种照明使弯曲的物体看起来很平坦,所以被称为平面阴影。

Gouraud Shading 古劳德着色法

How can we remove these discontinuities in lighting? Instead of computing illumination only at the center of a triangle, we can compute illumination at its three vertices. This gives us three illumination values between 0.0 and 1.0, one for each vertex of the triangle. This puts us in exactly the same situation as Chapter 8 (Shaded Triangles): we can use DrawShadedTriangle directly, using the illumination values as the “intensity” attribute.
我们怎样才能消除这些不连续的光照呢?我们可以在三角形的三个顶点上计算照度,而不是只计算三角形中心的照度。这就给了我们三个在0.0 0.0和1.0 1.0之间的照度值,三角形的每个顶点都有一个。这使我们处于与第八章(阴影三角形)完全相同的情况:我们可以直接使用 DrawShadedTriangle ,把照度值作为 "强度 "属性。

This technique is called Gouraud shading, after Henri Gouraud, who came up with the idea in 1971. Figure 13-3 shows the results of applying it to the cube and the sphere.
这种技术被称为Gouraud着色,是以1971年提出这个想法的Henri Gouraud命名的。图13-3显示了将其应用于立方体和球体的结果。

Figure 13-3: In Gouraud shading, we compute illumination at the vertices of the triangle and interpolate them across its surface.

The cube looks better: the discontinuity is gone, because both triangles of each face share two vertices and they have the same normal, so the illumination at these two vertices is identical for both triangles.
这个立方体看起来更好了:不连续的现象消失了,因为每个面的两个三角形都有两个顶点,而且它们有相同的法线,所以这两个顶点的照度对两个三角形都是相同的。

The sphere, however, still looks faceted, and the discontinuities on its surface look really wrong. This shouldn’t be surprising: we’re treating the sphere as a collection of flat surfaces. In particular, despite every triangle sharing vertices with its neighboring triangles, they have different normals. Figure 13-4 shows the problem.
然而,球体看起来仍然是面状的,而且其表面的不连续性看起来真的很不对劲。这并不奇怪:我们把球体当作一个平面的集合。特别是,尽管每个三角形与相邻的三角形共享顶点,但它们的法线是不同的。图13-4显示了这个问题。

Figure 13-4: We get two different values for the illumination at the shared vertex, because they depend on the normals of the triangles, which are different.

Let’s take a step back. The fact that we’re using flat triangles to represent a curved object is a limitation of our techniques, not a property of the object itself.
让我们退一步讲。我们用平面三角形来表示一个弯曲的物体,这是我们技术的限制,而不是物体本身的属性。

Each vertex in the sphere model corresponds to a point on the sphere, but the triangles they define are just an approximation of its surface. It would be a good idea to make the vertices in the model represent the points in the sphere as closely as possible. That means, among other things, using the actual sphere normals for each vertex, as shown in Figure 13-5.
球体模型中的每个顶点都对应于球体上的一个点,但它们定义的三角形只是球体表面的一个近似值。让模型中的顶点尽可能地代表球体中的点是一个好主意。这意味着,除其他外,为每个顶点使用实际的球体法线,如图13-5所示。

Figure 13-5: We can give each vertex the normal of the curved surface it represents.

Note that this doesn’t apply to the cube; even though triangles share vertex positions, each face needs to be shaded independently of the others. There’s no single “correct” normal for the vertices of a cube.
请注意,这不适用于立方体;即使三角形共享顶点位置,每个面也需要独立于其他面进行着色。对于立方体的顶点来说,没有单一的 "正确 "法线。

Our renderer has no way to know whether a model is supposed to be an approximation of a curved object or the exact representation of a flat one. After all, a cube is a very crude approximation of a sphere! To solve this, we’ll make the triangle normals part of the model, so its designer can make this decision.
我们的渲染器没有办法知道一个模型应该是曲线物体的近似值还是平面物体的精确表示。毕竟,一个立方体是一个非常粗糙的球体的近似值!为了解决这个问题,我们将三角形法线作为模型的一部分。为了解决这个问题,我们将使三角形的法线成为模型的一部分,这样它的设计者就可以做出这个决定。

Some objects, like the sphere, have a single normal per vertex. Other objects, like the cube, have a different normal for each triangle that uses the vertex. So we can’t make the normals a property of the vertices; they need to be a property of the triangles that use them:
有些物体,如球体,每个顶点有一个法线。其他物体,比如立方体,每个使用该顶点的三角形都有不同的法线。所以我们不能让法线成为顶点的属性;它们需要成为使用它们的三角形的属性。

model {
    name = cube
    vertices {
        0 = (-1, -1, -1)
        1 = (-1, -1,  1)
        2 = (-1,  1,  1)
        ...
    }
    triangles {
        0 = {
            vertices = [0, 1, 2]
            normals = [(-1, 0, 0), (-1, 0, 0), (-1, 0, 0)]
        }
        ...
    }
}

Figure 13-6 shows the scene rendered using Gouraud shading and the appropriate vertex normals.
图13-6显示了使用Gouraud阴影和适当的顶点法线渲染的场景。

Figure 13-6: Gouraud shading with normal vectors specified in the model. The cubes still look like cubes, and the sphere now looks like a sphere.

The cubes still look like cubes, and the sphere now looks remarkably like a sphere. In fact, you can only tell it’s made out of triangles by looking at its outline. This could be improved by using more, smaller triangles, at the expense of requiring more computing power.
立方体看起来仍然是立方体,而球体现在看起来非常像球体。事实上,你只能通过观察它的轮廓来判断它是由三角形组成的。这可以通过使用更多、更小的三角形来改进,但需要更多的计算能力。

Gouraud shading starts breaking down when we try to render shiny objects, though; the specular highlight on the sphere is decidedly unrealistic.
不过,当我们试图渲染有光泽的物体时,Gouraud阴影开始崩溃;球体上的镜面高光明显不现实。

This is an indication of a more general problem. When we move a point light very close to a big face, we’d naturally expect it to look brighter and the specular effects to become more pronounced; however, Gouraud shading produces the exact opposite (Figure 13-7).
这说明了一个更普遍的问题。当我们把一个点光源移到离大脸很近的地方时,我们自然会期望它看起来更亮,镜面效果更明显;然而,Gouraud阴影产生的效果正好相反(图13-7)。

Figure 13-7: Contrary to our expectations, the closer the point light is to a face, the darker it looks.

We expect points near the center of the triangle to receive a lot of light, because L and N are roughly parallel. However, we’re not computing lighting at the center of the triangle, but at its vertices. There, the closer the light is to the surface, the bigger the angle with the normal, so they receive little illumination. This means that every interior pixel will end up with an intensity value that is the result of interpolating between two small values, which is also a low value, as shown in Figure 13-8.
我们希望三角形中心附近的点能收到大量的光,因为L ⃗ 𝐿 →和N ⃗ 𝑁 →大致平行。然而,我们不是在计算三角形中心的照明,而是在其顶点。在那里,光线离表面越近,与法线的夹角就越大,所以它们得到的光照很少。这意味着每个内部像素最终的强度值都是两个小值之间插值的结果,这也是一个低值,如图13-8所示。

Figure 13-8: Interpolating illumination from the vertices, which are dark, results in a dark center, although the normal is parallel to the light vector at that point.

So, what to do? 那么,该怎么做呢?

Phong Shading 芳影

We can overcome the limitations of Gouraud shading, but as usual, there’s a trade-off between quality and resource usage.
我们可以克服Gouraud着色的局限性,但像往常一样,在质量和资源使用之间有一个权衡。

Flat shading involved a single illumination calculation per triangle. Gouraud shading requires three illumination calculations per triangle, plus the interpolation of a single attribute, the illumination, across the triangle. The next step in quality requires us to calculate illumination at every pixel of the triangle.
平坦阴影涉及到每个三角形的单一照度计算。Gouraud着色需要对每个三角形进行三次照度计算,再加上单一属性的插值,即整个三角形的照度。下一步的质量要求我们在三角形的每个像素上计算照度。

This doesn’t sound particularly complex from a theoretical point of view; we’re computing lighting at one or three points already, and we were computing per-pixel lighting for the raytracer after all. What’s tricky here is figuring out where the inputs to the illumination equation come from. Recall that the full illumination equation, with ambient, diffuse, and specular components, is:
从理论上讲,这听起来并不特别复杂;我们已经在计算一个或三个点的照明,而且我们毕竟是在为光线追踪器计算每个像素的照明。这里最棘手的是弄清楚照明方程的输入来自哪里。回想一下,完整的照明方程,包括环境、漫反射和镜面成分,是:。

IP=IA+i=1nIi(N,Li|N||Li|+(R,V|R||V|)s)
I P = I A + ∑ i=1 n I i ⎛ ⎝ ⟨ N ⃗ , L i → ⟩ | N ⃗ || L i → | + ( ⟨ R ⃗ , V ⃗ ⟩ | R ⃗ || V ⃗ | ) s ⎞ ⎠ 𝐼 𝑃 = 𝐼 𝐴 + ∑ ∑ ∑ 𝑖 = 1 𝑛 ℓ 𝐼 𝑖 ( ⟨ 𝑁 → 。𝐿 𝑖 → ⟩ | 𝑁 → | 𝐿 𝑖 → + ( ⟨ 𝑅 → , 𝑉 → ⟩ | 𝑅 → | 𝑉 → ) 𝑠 )

First, we need L. For directional lights, L is given. For point lights, L is defined as the vector from the point in the scene, P, to the position of the light, Q. However, we don’t have P for every pixel of the triangle, but only for the vertices.
首先,我们需要L ⃗ 𝐿 →。对于定向灯,L ⃗ 𝐿 → 已经给出。对于点光源,L ⃗ 𝐿 →被定义为从场景中的点P𝑃到光的位置Q𝑄的矢量。然而,我们对三角形的每个像素都没有P𝑃,而只有顶点有。

What we do have is the projection of P; that is, the x and y we’re about to draw on the canvas! We know that
我们所拥有的是P𝑃的投影;也就是说,我们要在画布上画出的x ′ 𝑥 ′ 和y ′ 𝑦 ′!我们知道

x=xdz

y=ydz

We also happen to have an interpolated but geometrically correct value for 1z as part of the depth-buffering algorithm, so
作为深度缓冲算法的一部分,我们也碰巧有一个内插的但在几何上正确的1 z 1 𝑧的值,所以

x=xd1z
x ′ =xd 1 与𝑥 ′ = 𝑥 𝑑 1 𝑧

y=yd1z

We can recover P from these values:
我们可以从这些数值中恢复P𝑃。

x=xd1z
x= x ′ d 1 z 𝑥 = 𝑑 1 𝑧 1 𝑧

y=yd1z

z=11z

We also need V. This is the vector from the camera (which we know) to P (which we just computed), so V is simply PC.
我们还需要V ⃗ 𝑉 →。这是从相机(我们知道)到P𝑃(我们刚刚计算过)的矢量,所以V⃗ 𝑉 → 只是P-C 𝑃 - 𝐶。

Next, we need N. We only know the normals at the vertices of the triangle. When all you have is a hammer, every problem looks like a nail; our hammer is—you probably guessed it—linear interpolation of attribute values. So let’s take the values of Nx, Ny, and Nz at each vertex and treat each of them as an attribute we can linearly interpolate. Then, at every pixel, we reassemble the interpolated components into a vector, normalize it, and use it as the normal at that pixel.
接下来,我们需要N⃗ 𝑁 →。我们只知道三角形各顶点的法线。当你只有一把锤子时,每一个问题看起来都像钉子;我们的锤子是--你可能猜到了--属性值的线性内插。因此,让我们把每个顶点的N x 𝑁 𝑥、N y 𝑁 𝑦和N z 𝑧的值作为我们可以线性插值的一个属性。然后,在每个像素上,我们将插值的分量重新组合成一个矢量,将其归一化,并将其作为该像素的法线。

This technique is called Phong shading, after Bui Tuong Phong, who invented it in 1973. Figure 13-9 shows the results.
这种技术被称为Phong shading,以1973年发明它的Bui Tuong Phong命名。图13-9显示了这一结果。

Figure 13-9: Phong shading. The surface of the sphere looks smooth and the specular highlight is clearly visible.

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

The sphere looks much better now. Its surface displays the proper curvature, and the specular highlights look well defined. The contour, however, still betrays the fact that we’re rendering an approximation made of triangles. This is not a shortcoming of the shading algorithm, which only determines the color of each pixel of the surface of the triangles but has no control over the shape of the triangles themselves. This sphere approximation uses 420 triangles; we could get a smoother contour by using more triangles, at the cost of worse performance.
球体现在看起来好多了。它的表面显示出适当的曲率,而且镜面高光看起来也很清晰。然而,轮廓仍然暴露了一个事实,即我们正在渲染一个由三角形组成的近似值。这并不是着色算法的缺点,它只决定了三角形表面的每个像素的颜色,但对三角形本身的形状没有控制。这个球体近似使用了420个三角形;我们可以通过使用更多的三角形获得更平滑的轮廓,但代价是性能更差。

Phong shading also solves the problem with the light getting close to a face, now giving the expected results (Figure 13-10).
Phong shading也解决了光线靠近脸部的问题,现在得到了预期的结果(图13-10)。

Figure 13-10: The closer the light is to the surface, the brighter and better defined the specular highlight looks.

At this point, we’ve matched the capabilities of the raytracer developed in Part I, except for shadows and reflections. Using the exact same scene definition, Figure 13-11 shows the output of the rasterizer we’re developing.
在这一点上,我们已经与第一部分中开发的光线追踪器的能力相匹配,除了阴影和反射。使用完全相同的场景定义,图13-11显示了我们正在开发的光栅器的输出。

Figure 13-11: The reference scene, rendered by the rasterizer

For reference, Figure 13-12 shows the raytraced version of the same scene.
作为参考,图13-12显示了同一场景的光线跟踪版本。

Figure 13-12: The reference scene, rendered by the raytracer

The two versions look almost identical, despite using vastly different techniques. This is expected, since the scene definition is identical. The only visible difference can be found in the contour of the spheres: the raytracer renders them as mathematically perfect objects, but we use an approximation made of triangles for the rasterizer.
这两个版本看起来几乎完全一样,尽管使用的技术大不相同。这是意料之中的,因为场景的定义是相同的。唯一可见的区别可以在球体的轮廓上找到:光线追踪器将它们渲染成数学上完美的物体,但我们在光栅器中使用了由三角形组成的近似值。

Another difference is the performance of the two renderers. This is very hardware- and implementation-dependent, but generally speaking, rasterizers can produce full-screen images of complex scenes up to 60 times per second or more, which makes them suitable for interactive applications such as videogames, while raytracers may take multiple seconds to render the same scene once. This difference might tend to disappear in the future; advances in hardware in recent years are making raytracer performance much more competitive with that of rasterizers.
另一个区别是这两种渲染器的性能。这是非常依赖于硬件和实现的,但一般来说,光栅器可以每秒产生复杂场景的全屏图像60次或更多,这使得它们适合于电子游戏等交互式应用,而光线追踪器可能需要多秒才能渲染一次相同的场景。这种差异在未来可能会趋于消失;近年来硬件的进步使光线跟踪器的性能与光栅器相比更具竞争力。

Summary

In this chapter, we added illumination to our rasterizer. The illumination equation we use is exactly the same as the one in Chapter 3 (Light), because we’re using the same lighting model. However, where the raytracer computed the illumination equation at each pixel, our rasterizer can support a variety of different techniques to achieve a specific trade-off between performance and image quality.
在这一章中,我们为光栅器添加了照明。我们使用的照度方程与第三章(光)中的方程完全相同,因为我们使用的是相同的照明模型。然而,在光线跟踪器计算每个像素的照明方程的地方,我们的光栅器可以支持各种不同的技术,以实现性能和图像质量之间的特定权衡。

The fastest shading algorithm, which also produces the least appealing results, is flat shading: we compute the illumination of a single point in a triangle and use it for every pixel in that triangle. This results in a very faceted appearance, especially for objects that approximate curved surfaces such as spheres.
最快的着色算法,同时也产生了最不吸引人的结果,那就是平面着色:我们计算三角形中一个点的照度,并将其用于该三角形中的每个像素。这就产生了一个非常切面的外观,特别是对于像球体这样的近似于曲面的物体。

One step up the quality ladder, we have Gouraud shading: we compute the illumination of the three vertices of a triangle and then interpolate this value across the face of the triangle. This gives objects a smoother appearance, including curved objects. However, this technique fails to capture more subtle lighting effects, such as specular highlights.
在质量阶梯上,我们有Gouraud阴影:我们计算一个三角形的三个顶点的照度,然后将这个值插值到整个三角形的表面。这使物体具有更平滑的外观,包括弯曲的物体。然而,这种技术无法捕捉到更微妙的照明效果,比如镜面高光。

Finally, we studied Phong shading. Much like our raytracer, it computes the illumination equation at every pixel, producing the best results and also the worst performance. The trick in Phong shading is knowing how to compute all the necessary values to evaluate the illumination equation; once again, the answer is linear interpolation—in this case, of normal vectors.
最后,我们研究了Phong着色器。与我们的光线跟踪器一样,它在每个像素上计算光照方程,产生最好的结果,同时也产生最差的性能。Phong着色的诀窍是知道如何计算所有必要的值来评估光照方程;再一次,答案是线性插值--在这种情况下,是法线矢量的插值。

In the next chapter, we’ll add even more detail to the surface of our triangles, using a technique that we haven’t studied for the raytracer: texture mapping.
在下一章中,我们将为我们的三角形表面添加更多的细节,使用一种我们还没有研究过的光线跟踪器的技术:纹理贴图。