Extending the Rasterizer 扩展光栅器

We’ll conclude this second part of the book the same way we concluded the first one: with a set of possible extensions to the rasterizer we’ve developed in the preceding chapters.
我们将以总结第一部分的方式来结束本书的第二部分:对我们在前几章开发的光栅器进行一系列可能的扩展。

Normal Mapping 正常映射

In Chapter 13 (Shading), we saw how the normal vectors of a surface have a big impact on its appearance. For example, the right choice of normals can make a faceted object look smoothly curved; this is because the right choice of normals changes the way light interacts with the surface, which in turn changes the way our brain guesses the shape of the object. Unfortunately, there’s not much more we can do by interpolating normals beyond making surfaces look smoothly curved.
在第13章(着色)中,我们看到一个表面的法线向量对其外观有很大影响。例如,法线的正确选择可以使一个面状物体看起来有平滑的弧度;这是因为法线的正确选择改变了光线与表面的交互方式,而这又改变了我们的大脑对物体形状的猜测。不幸的是,除了使表面看起来平滑弯曲之外,我们通过插值法线能做的并不多。

In Chapter 14 (Textures), we saw how we could add fake detail to a surface by “painting” on it. This technique, called texture mapping, gives us much finer-grained control over the appearance of a surface. However, texture mapping doesn’t change the shape of the triangles—they’re still flat.
在第14章(纹理)中,我们看到了如何通过在表面上 "绘画 "来增加虚假的细节。这种技术被称为纹理贴图,它给了我们对一个表面的外观更精细的控制。然而,纹理映射并不改变三角形的形状--它们仍然是平面的。

Normal mapping combines both ideas. We can use normals to change the way light interacts with a surface and thus change the apparent shape of the surface; we can use attribute mapping to assign different values of an attribute to different parts of a triangle. By combining the two ideas, normal mapping lets us define surface normals at the pixel level.
法线映射结合了这两种想法。我们可以用法线来改变光线与表面的交互方式,从而改变表面的明显形状;我们可以用属性映射来给三角形的不同部分分配不同的属性值。通过结合这两种想法,法线映射可以让我们在像素层面上定义表面法线。

To do this, we associate a normal map to each triangle. A normal map is similar to a texture map, but its elements are normal vectors instead of colors. At rendering time, instead of computing an interpolated normal like Phong shading does, we use the normal map to get a normal vector for the specific pixel we’re rendering, in the same way that texture mapping gets a color for that specific pixel. Then we use this vector to compute lighting at that pixel.
为了做到这一点,我们给每个三角形关联一个法线图。法线图类似于纹理图,但它的元素是法线向量而不是颜色。在渲染时,我们不是像Phong Shading那样计算一个插值法线,而是使用法线贴图来获取我们正在渲染的特定像素的法线向量,就像纹理贴图获取该特定像素的颜色一样。然后我们用这个向量来计算该像素的光照。

Figure 15-1 shows a flat surface with a texture map applied, and the effects of different light directions when a normal map is also applied.
图15-1显示了一个应用了纹理贴图的平面,以及同时应用法线贴图时不同光线方向的效果。

All three images in Figure 15-1 are renders of a flat square (that is, two triangles) with a texture, as seen in (a). When we add a normal map and the appropriate per-pixel shading, we create the illusion of extra geometrical detail. In (b) and (c), the shading of the diamonds depends on the direction of the incident light, and our brain interprets this as the diamonds having volume.
图15-1中的三张图片都是对一个带有纹理的平面正方形(也就是两个三角形)的渲染,如(a)所示。当我们添加一个法线贴图和适当的每像素阴影时,我们就会创造出额外的几何细节的错觉。在(b)和(c)中,钻石的阴影取决于入射光线的方向,而我们的大脑将此解释为钻石具有体积。

There are a couple of practical considerations to keep in mind. First, the orientations of the vectors in the normal map are relative to the surface of the triangle they apply to. The coordinate system used for this is called tangent space, where two of the axes (usually X and Z) are tangent to (that is, embedded in) the surface and the remaining vector is perpendicular to the surface. At rendering time, the normal vector of the triangle, expressed in camera space, is modified according to the vector in the normal map to obtain a final normal vector that can be used for the illumination equations. This makes a normal map independent of the position and orientation of the object in the scene.
有几个实际的考虑因素需要牢记。首先,法线图中的向量的方向是相对于它们所适用的三角形表面而言的。用于此的坐标系称为切线空间,其中两个轴(通常是X𝑋和Z𝑍)与表面相切(也就是嵌入),其余的矢量则与表面垂直。在渲染时,三角形的法线矢量在摄像机空间中表示,根据法线图中的矢量进行修改,得到最终的法线矢量,可用于照明方程。这使得法线图与场景中物体的位置和方向无关。

Second, a very popular way to encode normal maps is as textures, mapping the values of X, Y, and Z to R, G, and B values. This gives normal maps a very characteristic purple-ish appearance, because purple, a combination of red and blue but no green, encodes flat areas of the surface. Figure 15-2 shows the normal map used in the examples in Figure 15-1.
其次,一种非常流行的法线图编码方式是作为纹理,将X𝑋、Y𝑌和Z𝑍的值映射为R𝑅、G𝐺和B𝐵值。这使法线图呈现出非常有特色的紫色外观,因为紫色是红色和蓝色的组合,但没有绿色,编码的是表面的平坦区域。图15-2是图15-1中的例子所使用的法线图。

Figure 15-2: The normal map used for the examples in Figure 15-1, encoded as a RGB texture

While this technique can drastically improve the perceived complexity of surfaces in a scene, it’s not without limitations. For example, since flat surfaces remain flat, it can’t change the silhouette of an object. For the same reason, the illusion breaks down when a normal-mapped surface is viewed from an extreme angle or up close, or when the features represented by the normal map are too big compared to the size of the surface. This technique is better suited to subtle detail, such as pores on the skin, the pattern on a stucco wall, or the irregular appearance of an orange peel. For this reason, the technique is also known as bump mapping.
虽然这种技术可以极大地改善场景中表面的感知复杂性,但它并非没有局限性。例如,由于平坦的表面仍然是平坦的,它不能改变一个物体的轮廓。出于同样的原因,当从一个极端的角度或近距离观察法线映射的表面时,或者当法线映射所代表的特征与表面的大小相比过大时,这种错觉就会消失。这种技术更适合于微妙的细节,如皮肤上的毛孔、灰泥墙上的图案或橘子皮的不规则外观。由于这个原因,这种技术也被称为凹凸贴图。

Environment Mapping 环境测绘

One of the most striking characteristics of the raytracer we developed is the ability to show objects reflecting one another. It is possible to create a relatively convincing, but somewhat fake, implementation of reflections in our rasterizer.
我们开发的光线追踪器最突出的特点之一是能够显示物体的相互反射。在我们的光栅器中可以创建一个相对有说服力的、但有点假的反射实现。

Imagine we have a scene representing a room in a house, and we want to render a reflective object placed in the middle of the room. For each pixel representing the surface of that object, we know the 3D coordinates of the point it represents, the surface normal at that point, and, since we know the position of the camera, we can also compute the view vector to that point. We could reflect the view vector with respect to the surface normal to obtain a reflection vector, just like we did in Chapter 4 (Shadows and Reflections).
想象一下,我们有一个代表房子里的一个房间的场景,我们想渲染一个放在房间中间的反射物体。对于代表该物体表面的每个像素,我们知道它所代表的点的三维坐标,该点的表面法线,而且,由于我们知道摄像机的位置,我们还可以计算出该点的视图矢量。我们可以将视角矢量相对于表面法线进行反射,得到一个反射矢量,就像我们在第四章(阴影和反射)中做的那样。

At this point, we want to know the color of the light coming from the direction of the reflection vector. If this were a raytracer, we’d just trace a ray in that direction and find out. However, this is not a raytracer. What to do?
在这一点上,我们想知道来自反射矢量方向的光线的颜色。如果这是一个光线追踪器,我们只需沿着这个方向追踪一条光线就可以知道了。然而,这不是一个光线追踪器。该怎么做呢?

Environment mapping provides one possible answer to this question. Suppose that before rendering the objects inside the room, we place a camera in the middle of it and render the scene six times—once for each perpendicular direction (up, down, left, right, front, back). You can imagine the camera is inside an imaginary cube, and each side of the cube is the viewport of one of these renders. We keep these six renders as textures. We call this set of six textures a cube map, which is why this technique is also called cube mapping.
环境映射为这个问题提供了一个可能的答案。假设在渲染房间内的物体之前,我们在房间的中间放置一个摄像机,并对场景进行六次渲染--每个垂直方向(上、下、左、右、前、后)各一次。你可以想象摄像机在一个假想的立方体中,而立方体的每一面都是其中一次渲染的视口。我们把这六个渲染图作为纹理保存起来。我们把这六个纹理的集合称为立方体贴图,这就是为什么这项技术也被称为立方体贴图。

Then we render the reflective object. When we get to the point of needing a reflected color, we can use the direction of the reflected vector to choose one of the textures of the cube map, and then a texel of that texture to get an approximation of the color seen in that direction—all without tracing a single ray!
然后我们渲染反射物体。当我们需要反射颜色时,我们可以使用反射矢量的方向来选择立方体贴图中的一个纹理,然后使用该纹理的一个texel来获得该方向上看到的颜色的近似值--所有这些都不需要追踪一条射线

This technique has some drawbacks. The cube map captures the appearance of a scene from a single point. If the reflective object we’re rendering isn’t located at that point, the position of the reflected objects won’t fully match what we would expect, so it will become clear that this is just an approximation. This would be especially noticeable if the reflective object were to move within the room, because the reflected scene wouldn’t change as the object moves.
这种技术有一些缺点。立体图从一个单一的点上捕捉到了场景的外观。如果我们要渲染的反射物体不在那个点上,那么反射物体的位置就不会与我们所期望的完全一致,所以会很明显地发现这只是一个近似值。如果反射物体在房间里移动,这一点会特别明显,因为反射的场景不会随着物体的移动而改变。

This limitation also suggests the best applications for the technique: if the “room” is big enough and distant enough from the object—that is, if the movement of the object is small with respect to the size of the room—the difference between the true reflection and the pre-rendered environment maps can go unnoticed. For example, this would work very well for a scene representing a reflective spaceship in deep space, since the “room” (the distant stars and galaxies) is infinitely far away for all practical purposes.
这个限制也提示了该技术的最佳应用:如果 "房间 "足够大,而且离物体足够远,也就是说,如果物体的运动相对于房间的大小来说是很小的,那么真实反射和预先渲染的环境图之间的差异就不会被注意到。例如,这对于表现深空中的反射飞船的场景来说非常有效,因为 "房间"(遥远的恒星和星系)在所有实际用途中都是无限远的。

Another drawback is that we’re forced to split the objects in the scene into two categories: static objects that are part of the “room,” which are seen in reflections, and dynamic objects that can be reflective. In some cases, this might be clear (walls and furniture are part of the room; people aren’t), but even then, dynamic objects wouldn’t be reflected on other dynamic objects.
另一个缺点是,我们被迫将场景中的物体分成两类:作为 "房间 "一部分的静态物体,它们在反射中被看到,而动态物体则可以被反射。在某些情况下,这可能很清楚(墙壁和家具是房间的一部分;人不是),但即使如此,动态物体也不会被反射到其他动态物体上。

A final drawback worth mentioning is related to the resolution of the cube maps. Whereas in the raytracer we could trace very precise reflections, in this case we need to make a trade-off between accuracy (higher-resolution cube map textures produce sharper reflections) and memory consumption (higher-resolution cube map textures require more memory). In practice, this means that environment maps won’t produce reflections that are as sharp as true raytraced reflections, especially when looking at reflective objects up close.
最后一个值得一提的缺点是与立方体贴图的分辨率有关。在光线跟踪器中,我们可以跟踪非常精确的反射,而在这种情况下,我们需要在精度(更高分辨率的立方体贴图产生更清晰的反射)和内存消耗(更高分辨率的立方体贴图需要更多的内存)之间做出权衡。在实践中,这意味着环境贴图产生的反射不会像真正的光线追踪反射那样清晰,特别是在近距离观察反射物体时。

Shadows

The raytracer we developed featured geometrically correct, very well-defined shadows. These were a very natural extension to the core algorithm. The architecture of a rasterizer makes it slightly more complex to implement shadows, but not impossible.
我们开发的光线跟踪器具有几何上正确的、非常明确的阴影。这是对核心算法的一个非常自然的扩展。光栅器的结构使得阴影的实现稍微复杂一些,但并非不可能。

Let’s start by formalizing the problem we’re trying to solve. In order to render shadows correctly, every time we compute the illumination equation for a pixel and a light, we need to know whether the pixel is actually illuminated by the light or whether it’s in the shadow of an object with respect to that light.
让我们先正式确定我们要解决的问题。为了正确地渲染阴影,每次我们计算一个像素和一盏灯的照度方程时,我们都需要知道这个像素是否真的被灯照亮,或者相对于该灯来说,它是否处于一个物体的阴影中。

With the raytracer, we could answer this question by tracing a ray from the surface to the light; in the rasterizer, we don’t have such a tool, so we’ll have to take a different approach. Let’s explore two different approaches.
在光线追踪器中,我们可以通过追踪一条从表面到光线的光线来回答这个问题;在光栅器中,我们没有这样的工具,所以我们必须采取不同的方法。让我们来探索两种不同的方法。

Stencil Shadows 模板阴影

Stencil shadows is a technique to render shadows with very well-defined edges (imagine the shadows cast by objects on a very sunny day). These are often called hard shadows.
模板阴影是一种渲染具有非常明确边缘的阴影的技术(想象一下在一个非常晴朗的日子里物体投下的阴影)。这些通常被称为硬阴影。

Our rasterizer renders the scene in a single pass; it goes over every triangle in the scene and renders it on the canvas, computing the full illumination equation every time (on a per-triangle, per-vertex, or per-pixel basis, depending on the shading algorithm). At the end of this process, the canvas contains the final render of the scene.
我们的光栅化器以单程方式渲染场景;它翻阅场景中的每个三角形并在画布上进行渲染,每次都计算完整的照明方程(以每个三角形、每个顶点或每个像素为基础,取决于着色算法)。在这个过程的最后,画布上包含了场景的最终渲染。

We’ll start by modifying the rasterizer to render the scene in several passes, one for each light in the scene (including the ambient light). Like before, each pass goes over every triangle, but it computes the illumination equation taking into account only the light associated with that pass.
我们将首先修改光栅化器,使其分几次渲染场景,为场景中的每一束光(包括环境光)渲染一次。像以前一样,每一次都要对每个三角形进行渲染,但它在计算照度方程时只考虑到与该通道相关的光线。

This gives us a set of images of the scene illuminated by each light separately. We can compose them together—that is, add them pixel by pixel—giving us the final render of the scene. This final image is identical to the image produced by the single-pass version. Figure 15-3 shows three light passes and the final composite for our reference scene.
这就给我们提供了一组由每个灯光单独照亮的场景图像。我们可以将它们组合在一起--也就是说,将它们逐个像素地加在一起--得到场景的最终渲染图。这个最终的图像与单通道版本产生的图像是相同的。图15-3显示了三个光程和我们参考场景的最终合成。

This lets us simplify our goal of “rendering a scene with shadows from multiple lights” into “rendering a scene with shadows from a single light, many times.” Now we need to find a way to render a scene illuminated by a single light, while leaving the pixels that are in shadow from that light completely black.
这让我们把 "渲染一个有多个灯的阴影的场景 "的目标简化为 "渲染一个有单个灯的阴影的场景,多次"。现在我们需要找到一种方法来渲染一个由单一灯光照亮的场景,同时让处于阴影中的像素完全变成黑色。

For this, we introduce the stencil buffer. Like the depth buffer, it has the same dimensions as the canvas, but its elements are integers. We can use it as a stencil for rendering operations, for example, modifying our rendering code to draw a pixel on the canvas only if the corresponding element in the stencil buffer has a value of zero.
为此,我们引入了钢网缓冲区。与深度缓冲区一样,它的尺寸与画布相同,但其元素是整数。我们可以把它作为渲染操作的模版,例如,修改我们的渲染代码,只有当模版缓冲区中的相应元素的值为零时,才在画布上绘制一个像素。

If we can set up the stencil buffer in such a way that illuminated pixels have a value of zero and pixels in shadow have a nonzero value, we can use it to draw only the pixels that are illuminated.
如果我们能把模版缓冲区设置成这样一种方式,使被照亮的像素的值为零,而阴影中的像素的值为非零,我们就可以用它来只画被照亮的像素。

Creating Shadow Volumes 创建影子卷

To set up the stencil buffer, we use something called shadow volumes. A shadow volume is a 3D polygon “wrapped” around the volume of space that’s in shadow from a light.
为了设置模版缓冲区,我们使用了一种叫做阴影体的东西。阴影体是一个三维多边形,它 "包裹 "着被光照到的空间体积。

We construct a shadow volume for each object that might cast a shadow on the scene. First, we determine which edges are part of the silhouette of the object; these are the edges between front-facing and back-facing triangles (we can use the dot product to classify the triangles, like we did for the back-face culling technique in Chapter 12 (Hidden Surface Removal)). Then, for each of these edges, we extrude them away from the direction of the light, all the way to infinity—or, in practice, to a really big distance beyond the scene.
我们为每个可能在场景中投下阴影的物体构建一个阴影体积。首先,我们确定哪些边缘是物体轮廓的一部分;这些是正面和背面三角形之间的边缘(我们可以使用点积来对三角形进行分类,就像我们在第12章(隐藏表面移除)中对背面剔除技术所做的那样)。然后,对于这些边缘,我们把它们从光线的方向挤出,一直挤出到无穷大--或者,在实践中,挤出到场景之外的一个非常大的距离。

This gives us the “sides” of the shadow volume. The “front” of the volume is made by the front-facing triangles of the object itself, and the “back” of the volume can be computed by creating a polygon whose edges are the “far” edges of the extruded sides.
这就为我们提供了阴影体的 "边"。体积的 "正面 "是由物体本身的正面三角形构成的,而体积的 "背面 "可以通过创建一个多边形来计算,这个多边形的边缘是挤出的侧面的 "远 "的边缘。

Figure 15-4 shows the shadow volume created this way for a cube with respect to a point light.
图15-4显示了以这种方式为一个立方体创建的相对于点光源的阴影体积。

Figure 15-4: The shadow volume of a cube with respect to a point light

Next, we’ll see how to use the shadow volumes to determine which pixels in the canvas are in shadow with respect to a light.
接下来,我们将看到如何使用阴影体积来确定画布中哪些像素相对于灯光处于阴影中。

Counting Shadow VolumeRay Intersections
计算影子体积Ray交叉点

Imagine a ray starting from the camera and going into the scene until it hits a surface. Along the way, it might enter and leave any number of shadow volumes.
想象一下,一条射线从摄像机出发,进入场景,直到它碰到一个表面。一路上,它可能会进入和离开任何数量的阴影体。

We can keep track of this with a counter that starts at zero. Every time the ray enters a shadow volume, we increment the counter; every time it leaves, we decrement it. We stop when the ray hits a surface and look at the counter. If it’s zero, it means the ray entered as many shadow volumes as it left, so the point must be illuminated; if it’s not zero, it means the ray is inside at least one shadow volume, so the point must be in shadow. Figure 15-5 shows a few examples of this.
我们可以用一个从零开始的计数器来跟踪这一点。每当射线进入影子体积时,我们就递增该计数器;每当它离开时,我们就递减它。当光线碰到一个表面时,我们就停止,然后看一下计数器。如果计数器为零,说明射线进入的阴影体和离开的阴影体一样多,所以这个点一定是被照亮的;如果计数器不为零,说明射线至少在一个阴影体里面,所以这个点一定是在阴影中。图15-5显示了一些这方面的例子。

However, this only works if the camera itself is not inside a shadow volume! If a ray starts inside the shadow volume and doesn’t leave it before hitting the surface, our algorithm would incorrectly conclude that it’s illuminated.
然而,这只有在相机本身不在阴影体积内的情况下才有效。如果一条光线从阴影体中开始,在击中表面之前没有离开阴影体,我们的算法就会错误地得出结论说它被照亮了。

Figure 15-5: Counting the intersections between rays and shadow volumes tells us whether a point along the ray is illuminated or in shadow.

We could check for this condition and adjust the counter accordingly, but counting how many shadow volumes a point is inside of is an expensive operation. Fortunately, there’s a way to overcome this limitation that is simpler and cheaper, if somewhat counter-intuitive.
我们可以检查这种情况,并相应地调整计数器,但计算一个点在多少个影子卷中是一个昂贵的操作。幸运的是,有一种方法可以克服这个限制,虽然有点违反直觉,但更简单、更便宜。

Rays are infinite, but shadow volumes aren’t. This means a ray always starts and ends outside a shadow volume. This, in turn, means that a ray always enters a shadow volume as many times as it leaves it; the counter for the entire ray must always be zero.
射线是无限的,但影子体积却不是。这意味着一条射线总是在影子体积之外开始和结束。这反过来又意味着,一条射线进入阴影体的次数和离开阴影体的次数一样多;整个射线的计数器必须始终为零。

Suppose we keep track of the intersections between the ray and the shadow volume after the ray hits the surface. If the counter has a value of zero, then the value must also be zero before the ray hits the surface. If the counter has a nonzero value, it must have the opposite value on the other side of the surface.
假设我们跟踪射线击中表面后,射线与阴影体的交点。如果计数器的值为零,那么在射线击中表面之前,该值也必须为零。如果计数器的值为非零,那么它在表面的另一侧必须有相反的值。

This means counting intersections between the ray and the shadow volume before the ray hits the surface is equivalent to counting the intersections after it—but in this case, we don’t have to worry about the position of the camera! Figure 15-6 shows how this technique always produces correct results.
这意味着在射线击中表面之前计算射线和阴影体之间的交点,等同于计算它之后的交点--但在这种情况下,我们不必担心摄像机的位置图15-6显示了这种技术如何总是产生正确的结果。

Figure 15-6: The counters have a value of zero for points that receive light, and a nonzero value for points that are in shadow, regardless of whether the camera is inside or outside the shadow volume.

Setting up the Stencil Buffer
设置钢网缓冲区

We’re working with a rasterizer, not with a raytracer, so we need to find a way to keep these counters without actually computing any intersections between rays and shadow volumes. We can do this by using the stencil buffer.
我们使用的是光栅器,而不是光线追踪器,所以我们需要找到一种方法来保持这些计数器,而不需要实际计算光线和阴影体之间的任何交叉点。我们可以通过使用模版缓冲区来做到这一点。

First, we render the scene as illuminated only by the ambient light. The ambient light casts no shadows, so we can do this without any changes to the rasterizer. This gives us one of the images we need to compose the final render, but it also gives us depth information for the scene, as seen from the camera, contained in the depth buffer. We need to keep this depth buffer for the next steps.
首先,我们将场景渲染成仅由环境光照亮的状态。环境光没有投射阴影,所以我们可以在不改变光栅器的情况下进行渲染。这给我们提供了我们需要的图像之一,以构成最终的渲染,但它也给我们提供了场景的深度信息,就像从摄像机看到的那样,包含在深度缓冲区中。我们需要为接下来的步骤保留这个深度缓冲区。

Next, for each light, we follow these steps:
接下来,对于每一盏灯,我们遵循以下步骤。

  1. “Render” the back faces of the shadow volumes to the stencil buffer, incrementing its value whenever the pixel fails the depth buffer test. This counts the number of times the ray leaves a shadow volume after hitting the closest surface.
    "渲染 "阴影体的背面到模版缓冲区,每当像素没有通过深度缓冲区的测试时,就会增加它的值。这将计算射线在碰到最近的表面后离开阴影体的次数。

  2. “Render” the front faces of the shadow volumes to the stencil buffer, decrementing its value whenever the pixel fails the depth buffer test. This counts the number of times the ray enters a shadow volume after hitting the closest surface.
    "渲染 "阴影体的正面到模版缓冲区,每当像素没有通过深度缓冲区的测试时,就递减其值。这将计算射线在碰到最近的表面后进入阴影体的次数。

Note that during the “rendering” steps, we’re only interested in modifying the stencil buffer; there’s no need to write pixels to the canvas, and therefore no need to calculate illumination or texturing. We also don’t write to the depth buffer, because the sides of the shadow volumes aren’t actually physical objects in the scene. Instead, we use the depth buffer we computed during the ambient lighting pass.
请注意,在 "渲染 "步骤中,我们只对修改模版缓冲区感兴趣;不需要向画布写入像素,因此也不需要计算照明或纹理。我们也不写到深度缓冲区,因为阴影体的两侧实际上不是场景中的物理对象。相反,我们使用我们在环境光照过程中计算的深度缓冲区。

After doing this, the stencil buffer has zeros for the pixels that are illuminated and other values for the pixels that are in shadow. So we render the scene normally, illuminated by the single light corresponding to this pass, calling PutPixel only on the pixels where the stencil buffer has a value of zero.
这样做之后,钢网缓冲区中被照亮的像素为零,而阴影中的像素为其他值。因此,我们正常渲染场景,由这个通道对应的单一灯光照射,只在模版缓冲区的值为0的像素上调用 PutPixel

Repeating this process for every light, we end up with a set of images corresponding to the scene illuminated by each of the lights, with shadows correctly taken into account. The final step is to compose all the images into a final render of the scene by adding them together pixel by pixel.
对每一盏灯重复这一过程,我们最终会得到一组对应于每一盏灯所照亮的场景的图像,并正确考虑到阴影。最后一步是将所有的图像按像素加在一起,组成场景的最终渲染。

The idea of using the stencil buffer to render shadows dates back to the early 1990s, but the first implementations had several drawbacks. The depth-fail variant described here was independently discovered several times during 1999 and 2000, most notably by John Carmack while working on Doom 3, which is why this variant is also known as Carmack’s Reverse.
使用模版缓冲器渲染阴影的想法可以追溯到20世纪90年代初,但最初的实现有几个缺点。这里描述的深度失败的变体在1999年和2000年期间被独立地发现了几次,最引人注目的是约翰-卡马克在开发《毁灭战士3》时发现的,这就是为什么这个变体也被称为卡马克的反转。

Shadow Mapping 阴影映射

The other well-known technique to render shadows in a rasterizer is called shadow mapping. This renders shadows with less defined edges (imagine the shadows cast by objects on a cloudy day). These are often called soft shadows.
在光栅器中渲染阴影的另一个著名技术叫做阴影映射。这可以渲染出边缘不太明确的阴影(想象一下阴天时物体投下的阴影)。这些通常被称为软阴影。

To reiterate, the question we’re trying to answer is, given a point on a surface and a light, does the point receive illumination from that light? This is equivalent to determining whether there’s an object between the light and the point.
重申一下,我们要回答的问题是,给定表面上的一个点和一束光,这个点是否接受该光的照耀?这相当于确定在光和点之间是否有一个物体。

With the raytracer, we traced a ray from the point to the light. In some sense, we’re asking whether the point can “see” the light, or, equivalently, whether the light can “see” the point.
通过光线追踪器,我们追踪了一条从点到光的光线。在某种意义上,我们在问点是否能 "看到 "光,或者说,光是否能 "看到 "点。

This leads us to the core idea of shadow mapping. We render the scene from the point of view of the light, preserving the depth buffer. Similar to how we created the environment maps we described above, we render the scene six times and end up with six depth buffers. These depth buffers, which we call shadow maps, let us determine the distance to the closest surface the light can “see” in any given direction.
这把我们引向了阴影贴图的核心思想。我们从光线的角度渲染场景,保留深度缓冲区。与我们创建上述环境地图的方式类似,我们对场景进行六次渲染,最后得到六个深度缓冲区。这些深度缓冲区,我们称之为阴影贴图,让我们确定光线在任何给定方向上可以 "看到 "的最近的表面的距离。

The situation is slightly more complicated for directional lights, because these have no position to render from. Instead, we need to render the scene from a direction. This requires using an orthographic projection instead of our usual perspective projection. With perspective projection and point lights, every ray starts from a point; with orthographic projection and directional lights, every ray is parallel to each other, sharing the same direction.
对于定向灯来说,情况要稍微复杂一些,因为这些灯没有位置可以渲染。相反,我们需要从一个方向来渲染场景。这就需要使用正射投影,而不是我们常用的透视投影。使用透视投影和点光源,每条光线都从一个点开始;使用正射投影和定向光源,每条光线都是相互平行的,共享同一个方向。

When we want to determine whether a point is in shadow or not, we compute the distance and the direction from the light to the point. We use the direction to look up the corresponding entry in the shadow map. If this depth value is smaller than the distance from the point to the light, it means there’s a surface that is closer to the light than the point we’re illuminating, and therefore the point is in the shadow of that surface; otherwise, the light can “see” the point unobstructed, so the point is illuminated by the light.
当我们想确定一个点是否在阴影中时,我们要计算从光到这个点的距离和方向。我们使用方向来查找阴影图中的相应条目。如果这个深度值小于点到光的距离,就意味着有一个表面比我们要照亮的点更靠近光,因此这个点处于这个表面的阴影中;否则,光可以无障碍地 "看到 "这个点,所以这个点被光照亮了。

Note that the shadow maps have a limited resolution, usually lower than the canvas. Depending on the distance and the relative orientation of the point and the light, this might cause the shadows to look blocky. To avoid this, we can sample the depth of the surrounding depth entries as well and determine whether the point lies on the edge of a shadow (as evidenced by a depth discontinuity in the surrounding entries). If this is the case, we can use a technique similar to bilinear filtering, as we did in Chapter 14 (Textures), to come up with a value between 0.0 and 1.0 representing how much the point is visible from the light and multiply it by the illumination of the light; this gives the shadows created by shadow mapping their characteristic blurry appearance. Other ways to avoid the blocky appearance involve sampling the shadow map in different ways—look into percentage closer filtering, for example.
请注意,阴影图的分辨率是有限的,通常低于画布。根据点和光的距离和相对方向,这可能会导致阴影看起来很块。为了避免这种情况,我们可以对周围的深度条目进行采样,并确定该点是否位于阴影的边缘(如周围条目的深度不连续所证明的)。如果是这样的话,我们可以使用类似于双线性滤波的技术,就像我们在第14章(纹理)中所做的那样,得出一个介于0.0和1.0之间的值,代表该点在光线下的可见程度,并将其乘以光线的照度;这使得阴影贴图所产生的阴影具有其特有的模糊外观。其他避免块状外观的方法包括以不同的方式对阴影贴图进行采样--例如,查看更接近百分比的过滤。

Summary

Like in Chapter 5 (Extending the Raytracer), this chapter briefly introduced several ideas you can explore by yourself. These extend the rasterizer developed over the previous chapters to bring its features closer to those of the raytracer, while retaining its speed advantage. There’s always a trade-off, and in this case it comes in the form of less accurate results or increased memory consumption, depending on the algorithm.
和第5章(扩展光线跟踪器)一样,本章简要介绍了几个你可以自己探索的想法。这些扩展了前几章开发的光栅器,使其功能更接近于光线跟踪器的功能,同时保留其速度优势。总有一种权衡,在这种情况下,它以不太准确的结果或增加内存消耗的形式出现,这取决于算法。