Shadows and Reflections 阴影和反射

Our quest to render the scene in progressively more realistic ways continues. In the previous chapter, we modeled the way rays of light interact with surfaces. In this chapter, we’ll model two aspects of the way light interacts with the scene: objects casting shadows and objects reflecting on other objects.
我们继续寻求以渐进式的更真实的方式渲染场景。在上一章中,我们对光线与表面的交互方式进行了建模。在这一章中,我们将对光线与场景交互的方式的两个方面进行建模:物体投射阴影和物体反射到其他物体上。

Shadows

Where there are lights and objects, there are shadows. We have lights and objects. So where are our shadows?
有灯光和物体的地方就有影子。我们有灯光和物体。那么我们的影子在哪里?

Understanding Shadows 了解影子

Let’s begin with a more fundamental question. Why should there be shadows? Shadows happen when there’s a light whose rays can’t reach an object because there’s some other object in the way.
让我们从一个更基本的问题开始。为什么会有影子?当有一束光因为有其他物体的阻挡而无法到达一个物体时,就会出现阴影。

In the previous chapter, we only looked at the very local interactions between a light source and a surface, while ignoring everything else happening in the scene. For shadows to happen, we need to take a more global view and consider the interaction between a light source, a surface we want to draw, and other objects present in the scene.
在上一章中,我们只看了光源和表面之间非常局部的相互作用,而忽略了场景中发生的其他一切。为了让阴影发生,我们需要从一个更全局的角度来考虑光源、我们想要绘制的表面和场景中的其他物体之间的相互作用。

Conceptually, what we’re trying to do is relatively simple. We want to add a little bit of logic that says “if there’s an object between the point and the light, don’t add the illumination coming from this light.”
从概念上讲,我们想做的事情相对简单。我们想添加一点逻辑,说 "如果点和光之间有一个物体,就不要添加来自这个光的照明"。

The two cases we want to distinguish are shown in Figure 4-1.
我们要区分的两种情况如图4-1所示。

Figure 4-1: A shadow is cast over a point whenever there’s an object between the light source and that point.

It turns out we already have all of the tools we need to do this. Let’s start with a directional light. We know P; that’s the point we’re interested in. We know L; that’s part of the definition of the light. Knowing P and L, we can define a ray, namely P+tL, that goes from the point on the surface to the infinitely distant light source. Does this ray intersect any other object? If it doesn’t, there’s nothing between the point and the light, so we compute the illumination from this light as before. If it does, the point is in shadow, so we ignore the illumination from this light.
事实证明,我们已经有了做这件事所需的所有工具。让我们从一个定向光开始。我们知道P𝑃;那是我们感兴趣的点。我们知道L ⃗ 𝐿 →;这是光的定义的一部分。知道了P𝑃和L⃗𝐿→,我们可以定义一条射线,即P+t L⃗𝑃+ 𝑡 𝐿→,从表面上的点到无限远的光源。这条光线是否与其他物体相交?如果没有,那么点和光线之间就没有任何东西,所以我们像以前一样计算这个光线的照度。如果有,那么这个点就在阴影中,所以我们忽略这个光的照度。

We already know how to compute the closest intersection between a ray and a sphere: the TraceRay function we’re using to trace the rays from the camera. We can reuse most of it to compute the closest intersection between the ray of light and the rest of the scene.
我们已经知道如何计算光线与球体的最近交点:我们用来追踪来自摄像机的光线的 TraceRay 函数。我们可以重用它的大部分内容来计算光线和场景的其他部分之间的最近交点。

The parameters for this function are slightly different, though:
不过,这个函数的参数略有不同。

Figure 4-2 shows two points, P0 and P1. When tracing a ray from P0 in the direction of the light, we find no intersections with any objects; this means the light can reach P0, so there’s no shadow over it. In the case of P1, we find two intersections between the ray and the sphere, with t>0 (meaning the intersection is between the surface and the light); therefore, the point is in shadow.
图4-2显示了两个点,P 0 𝑃 0和P 1 𝑃 1。当从P 0 𝑃 0沿着光线的方向追踪光线时,我们发现没有与任何物体相交;这意味着光线可以到达P 0 𝑃 0,所以它上面没有阴影。在P 1 𝑃 1的情况下,我们发现射线和球体之间有两个交点,t>0 𝑡>0(意味着交点在表面和光线之间);因此,该点处于阴影中。

Figure 4-2: The sphere casts a shadow over P1, but not over P0.

We can treat point lights in a very similar way, with two exceptions. First, L is not constant, but we already know how to compute it from P and the position of the light. Second, we don’t want objects farther away from the light to be able to cast a shadow over P, so in this case we need tmax=1 so that the ray “stops” at the light.
我们可以用非常类似的方式处理点光源,但有两个例外。首先,L ⃗ 𝐿 → 不是常数,但我们已经知道如何从P 𝑃和光的位置计算它。第二,我们不希望离灯较远的物体能够在P𝑃上投下阴影,所以在这种情况下,我们需要t max =1 𝑡 𝑚 𝑎 𝑥 =1,以便射线在灯处 "停止"。

Figure 4-3 shows these situations. When we cast a ray from P0 with direction L0, we find intersections with the small sphere; however, these have t>1, meaning they are not between the light and P0, so we ignore them. Therefore P0 is not in shadow. On the other hand, the ray from P1 with direction L1 intersects the big sphere with 0<t<1, so the sphere casts a shadow over P1.
图4-3显示了这些情况。当我们从P 0 𝑃 0投出一条方向为L 0 𝐿 0的射线时,我们发现与小球的交点;然而,这些交点的t>1 𝑡>1,意味着它们不在光线和P 0 𝑃 0之间,所以我们忽略了它们。因此,P 0 𝑃 0不在阴影中。另一方面,来自P 1 𝑃 1的光线以L 1 𝐿 1的方向与大球体相交,0<t<1 0 < 𝑡 < 1,所以球体对P 1 𝑃 1投下了阴影。

There’s a literal edge case we need to consider. Consider the ray P+tL. If we look for intersections starting from tmin=0, we’ll find one at P itself! We know P is on a sphere, so for t=0, P+0L=P ; in other words, every point would be casting a shadow over itself!
有一个字面上的边缘情况我们需要考虑。考虑射线P+t L ⃗ 𝑃 + 𝑡 𝐿 →。如果我们从t min =0 𝑡 𝑚 𝑖 𝑛 =0开始寻找交点,我们会在P 𝑃本身找到一个!我们知道P𝑃在一个球体上,所以对于t=0 𝑡 = 0,P+0 L ⃗ =P 𝑃 + 0 𝐿 → = 𝑃;换句话说,每个点都会在自己身上投下一个阴影

Figure 4-3: We use the value of t at the intersections to determine whether they cast a shadow over the point.

The simplest workaround is to set tmin to a very small value ϵ instead of 0. Geometrically, we’re saying we want the ray to start just a tiny bit off the surface where P is, rather than exactly at P. So the range will be [ϵ,+] for directional lights and [ϵ,1] for point lights.
最简单的解决方法是将t min𝑡 𝑚 𝑖 𝑛设置为一个非常小的值ϵ 𝜖而不是0 0。从几何学上讲,我们是想让射线从P𝑃所在的表面开始,而不是正好在P𝑃处。因此,定向光的范围将是[ϵ,+∞] [ 𝜖 , + ∞],而点状光的范围是[ϵ,1] [ 𝜖 , 1]。

It might be tempting to fix this by just not computing intersections between the ray and the sphere P belongs to. This would work for spheres, but it would fail for objects with more complex shapes. For example, when you use your hand to protect your eyes from the Sun, your hand is casting a shadow over your face, and both surfaces are part of the same object - your body.
也许有人想通过不计算射线与P𝑃所属球体的交点来解决这个问题。这对球体来说是可行的,但对形状更复杂的物体来说就会失败。例如,当你用手遮挡太阳时,你的手在你的脸上投下了阴影,而这两个表面是同一个物体的一部分--你的身体。

Rendering with Shadows 用阴影进行渲染

Let’s turn the above discussion into pseudocode.
让我们把上述讨论变成伪代码。

In its previous version, TraceRay computes the closest ray-sphere intersection, and then computes lighting on the intersection. We need to extract the closest intersection code, since we want to reuse it to compute shadows (Listing 4-1).
在其先前的版本中, TraceRay 计算最接近的射线-球体交点,然后在交点上计算照明。我们需要提取最接近的交点的代码,因为我们想重新使用它来计算阴影(清单4-1)。

ClosestIntersection(O, D, t_min, t_max) {
    closest_t = inf
    closest_sphere = NULL
    for sphere in scene.Spheres {
        t1, t2 = IntersectRaySphere(O, D, sphere)
        if t1 in [t_min, t_max] and t1 < closest_t {
            closest_t = t1
            closest_sphere = sphere
        }
        if t2 in [t_min, t_max] and t2 < closest_t {
            closest_t = t2
            closest_sphere = sphere
        }
    }
    return closest_sphere, closest_t
}
Listing 4-1: Computing the closest intersection
清单4-1:计算最近的交叉点

We can rewrite TraceRay to reuse that function, and the resulting version is much simpler (Listing 4-2).
我们可以重写 TraceRay 来重新使用这个函数,这样的版本就简单多了(清单4-2)。

TraceRay(O, D, t_min, t_max) {
    closest_sphere, closest_t = ClosestIntersection(O, D, t_min, t_max)
    if closest_sphere == NULL {
        return BACKGROUND_COLOR
    }
    P = O + closest_t * D
    N = P - closest_sphere.center
    N = N / length(N)
    return closest_sphere.color * ComputeLighting(P, N, -D, closest_sphere.specular)
}
Listing 4-2: A simpler version of TraceRay after factoring out ClosestIntersection
清单4-2:将 ClosestIntersection 因式分解后的 TraceRay 的简单版本

Then, we need to add the shadow check ❶ to ComputeLighting (Listing 4-3).
然后,我们需要将阴影检验❶添加到 ComputeLighting 中(清单4-3)。

ComputeLighting(P, N, V, s) {
    i = 0.0
    for light in scene.Lights {
        if light.type == ambient {
            i += light.intensity
        } else {
            if light.type == point {
                L = light.position - P
                t_max = 1
            } else {
                L = light.direction
                t_max = inf
            }

            // Shadow check
         ❶ shadow_sphere, shadow_t = ClosestIntersection(P, L, 0.001, t_max)
            if shadow_sphere != NULL {
                continue
            }

            // Diffuse
            n_dot_l = dot(N, L)
            if n_dot_l > 0 {
                i += light.intensity * n_dot_l / (length(N) * length(L))
            }

            // Specular
            if s != -1 {
                R = 2 * N * dot(N, L) - L
                r_dot_v = dot(R, V)
                if r_dot_v > 0 {
                    i += light.intensity * pow(r_dot_v / (length(R) * length(V)), s)
                }
            }
        }
    }
    return i
}
Listing 4-3: ComputeLighting with shadow support
清单4-3。 支持阴影的 ComputeLighting

Figure 4-4 shows what the freshly rendered scene looks like.
图4-4显示了刚渲染好的场景的样子。

Figure 4-4: A raytraced scene, now with shadows

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

Now we’re getting somewhere. Objects in the scene interact with each other in a more realistic way, casting shadows over each other. Next we’ll explore more interactions between objects—namely, objects reflecting other objects.
现在我们有了一些进展。场景中的物体以一种更真实的方式相互作用,相互投下阴影。接下来我们将探索物体之间更多的互动--即物体反射其他物体。

Reflections 思考

In the previous chapter, we talked about surfaces that are “mirror-like,” but that only gave them a shiny appearance. Can we have objects that look like true mirrors—that is, can we see other objects reflected on their surface? We can, and in fact doing this in a raytracer is remarkably simple, but it can also be mind-twisting the first time you see how it’s done.
在上一章中,我们谈到了 "像镜子一样 "的表面,但那只是给它们一个闪亮的外观。我们能不能让物体看起来像真正的镜子,也就是说,我们能不能看到其他物体在其表面上的反射?我们可以,事实上,在光线跟踪器中做到这一点非常简单,但当你第一次看到它是如何做到的时候,也可能会觉得很费解。

Mirrors and Reflection 镜子和反射

Let’s look at how mirrors work. When you look at a mirror, what you’re seeing are the rays of light that bounce off the mirror. Rays of light are reflected symmetrically with respect to the surface normal, as you can see in Figure 4-5.
让我们来看看镜子是如何工作的。当你看镜子时,你所看到的是在镜子上反弹的光线。正如你在图4-5中看到的那样,光线是相对于表面法线对称地反射的。

Figure 4-5: A ray of light bounces off a mirror in a direction symmetrical to the mirror’s normal.

Suppose we’re tracing a ray, and the closest intersection happens to be with a mirror. What color is this ray of light? It’s not the color of the mirror itself, because we’re looking at reflected light. So we need to figure out where this light is coming from and what color it is. So all we have to do is compute the direction of the reflected ray and figure out the color of the light coming from that direction.
假设我们在追踪一条光线,而最接近的交点恰好是一面镜子。这条光线是什么颜色?这不是镜子本身的颜色,因为我们看的是反射光。所以我们需要弄清楚这束光是从哪里来的,它是什么颜色。因此,我们要做的就是计算出反射光线的方向,然后算出从那个方向来的光线的颜色。

If only we had a function that, given a ray, returned the color of the light coming from its direction . . .
如果我们有一个函数,给定一条光线,返回来自其方向的光线的颜色,那就好了。..

Oh, wait! We do have one, and it’s called TraceRay!
哦,等等!我们确实有一个,它叫做 TraceRay !

At the main loop, for each pixel, we create a ray from the camera to the scene and we call TraceRay to figure out what color the camera “sees” in that direction. If TraceRay determines that the camera is seeing a mirror, it just needs to compute the direction of the reflected ray and to figure out the color of the light coming from that direction; it must call . . . itself.
在主循环中,对于每个像素,我们创建一条从摄像机到场景的光线,并调用 TraceRay 来计算摄像机在这个方向上 "看到 "什么颜色。如果 TraceRay 确定摄像机看到的是一面镜子,它只需要计算出反射光线的方向,并计算出来自该方向的光线的颜色;它必须调用 ..本身。

At this point, I suggest you read the last couple of paragraphs again until you get it. If this is the first time you’ve read about recursive raytracing, it may take a couple of reads and some head scratching until you really get it.
在这一点上,我建议你再次阅读最后几段,直到你明白为止。如果这是你第一次读到递归光线追踪,可能需要读几遍并挠头,直到你真正理解为止。

Go on, I’ll wait—and once the euphoria of this beautiful aha! moment has started to wane, let’s formalize this a bit.
继续,我等着--一旦这个美丽的 "啊哈 "时刻的兴奋感开始减弱,让我们把它正式化一点。

When we design a recursive algorithm (one that calls itself), we need to ensure we don’t cause an infinite loop (also known as “This program has stopped responding. Do you want to terminate it?”). This algorithm has two natural exit conditions: when the ray hits a non-reflective object and when it doesn’t hit anything. But there’s a simple case where we could get trapped in an infinite loop: the infinite hall effect. This is what happens when you put a mirror in front of another and look into it—infinite copies of yourself!
当我们设计一个递归算法(一个会调用自己的算法)时,我们需要确保不会造成无限循环(也被称为 "这个程序已经停止响应")。你想终止它吗?")。这个算法有两个自然的退出条件:当射线击中一个不反光的物体时,以及当它没有击中任何东西时。但是有一种简单的情况,我们可能会陷入一个无限循环:无限霍尔效应。这就是当你把一面镜子放在另一面镜子前面并向它看去时发生的情况--无限的自己的副本

There are many ways to prevent an infinite recursion. We’ll just introduce a recursion limit to the algorithm; this will control how “deep” it can go. Let’s call it r. When r=0, we see objects but no reflections. When r=1, we see objects and the reflections of some objects on them (Figure 4-6).
有很多方法可以防止无限递归的发生。我们只是给算法引入一个递归限制;这将控制它能走多 "深"。我们把它叫做r𝑟。当r=0𝑟=0时,我们看到物体但没有反射。当r=1𝑟=1时,我们看到物体和一些物体在其上的反射(图4-6)。

Figure 4-6: Reflections limited to one recursive call (r = 1). We see spheres reflected on spheres, but the reflected spheres don’t look reflective themselves.

When r=2, we see objects, the reflections of some objects, and the reflections of the reflections of some objects (and so on for greater values of r). Figure 4-7 shows the result of r=3. In general, it doesn’t make much sense to go deeper than three levels, since the differences are barely noticeable at that point.
当r=2 𝑟=2时,我们看到的是物体,一些物体的反射,以及一些物体的反射的反射(以此类推,r 𝑟的值越大越好)。图4-7显示了r=3 𝑟=3的结果。一般来说,深入到三层以上就没有什么意义了,因为在这一点上几乎看不出差别。

Figure 4-7: Reflections limited to three recursive calls (r = 3). Now we can see the reflections of the reflections of the reflections of the spheres.

We’ll make another distinction. “Reflectiveness” doesn’t have to be an all-or-nothing proposition; objects may be only partially reflective. We’ll assign a number between 0 and 1 to every surface, specifying how reflective it is. Then we’ll compute the weighted average of the locally illuminated color and the reflected color using that number as the weight.
我们再做一个区分。"反射性 "不一定是一个全有或全无的命题;物体可能只是部分地反射。我们将给每个表面分配一个介于0 0和11 1之间的数字,指定它的反射程度。然后,我们将用这个数字作为权重,计算出局部照明颜色和反射颜色的加权平均值。

Finally, what are the parameters for the recursive call to TraceRay?
最后,对 TraceRay 的递归调用的参数是什么?

Now we’re ready to turn this into actual pseudocode.
现在我们准备把这个变成实际的伪代码。

Rendering with Reflections 用反射进行渲染

Let’s add reflections to our raytracer. First, we modify the scene definition by adding a reflective property to each surface, describing how reflective it is, from 0.0 (not reflective at all) to 1.0 (a perfect mirror):
让我们把反射添加到我们的光线跟踪器中。首先,我们修改场景定义,给每个表面添加一个 reflective 属性,描述它的反射程度,从0.0(完全不反射)到1.0(一个完美的镜子)。

sphere {
    center = (0, -1, 3)
    radius = 1
    color = (255, 0, 0)  # Red
    specular = 500  # Shiny
    reflective = 0.2  # A bit reflective
}
sphere {
    center = (-2, 0, 4)
    radius = 1
    color = (0, 0, 255)  # Blue
    specular = 500  # Shiny
    reflective = 0.3  # A bit more reflective
}
sphere {
    center = (2, 0, 4)
    radius = 1
    color = (0, 255, 0)  # Green
    specular = 10  # Somewhat shiny
    reflective = 0.4  # Even more reflective
}
sphere {
    color = (255, 255, 0)  # Yellow
    center = (0, -5001, 0)
    radius = 5000
    specular = 1000  # Very shiny
    reflective = 0.5  # Half reflective
}

We already use the “reflect ray” formula during the computation of specular reflections, so we can factor it out. It takes a ray R and a normal N and returns R reflected with respect to N.
在计算镜面反射时,我们已经使用了 "反射光线 "公式,所以我们可以把它算出来。它需要一条光线R⃗ 𝑅 →和一个法线N⃗ 𝑁 →并返回R⃗ 𝑅 →相对于N⃗ 𝑁 →的反射。

ReflectRay(R, N) {
    return 2 * N * dot(N, R) - R;
}

The only change we need to make to ComputeLighting is replacing the reflection equation with a call to this new ReflectRay.
我们需要对 ComputeLighting 做出的唯一改变是用对这个新的 ReflectRay 的调用来取代反射方程。

There’s a small change in the main method—we need to pass a recursion limit to the top-level TraceRay call:
在main方法中有一个小变化--我们需要向顶层的 TraceRay 调用传递一个递归限制。

color = TraceRay(O, D, 1, inf, recursion_depth)

We can set the initial value of recursion_depth to a sensible value such as 3, as discussed previously.
我们可以将 recursion_depth 的初始值设置为一个合理的值,如3,如前所述。

The only significant changes happen near the end of TraceRay, where we compute the reflections recursively. You can see the changes in Listing 4-4.
唯一显著的变化发生在 TraceRay 的结尾处,我们在那里递归地计算反射。你可以在清单4-4中看到这些变化。

TraceRay(O, D, t_min, t_max, recursion_depth) {
    closest_sphere, closest_t = ClosestIntersection(O, D, t_min, t_max)

    if closest_sphere == NULL {
        return BACKGROUND_COLOR
    }

    // Compute local color
    P = O + closest_t * D
    N = P - closest_sphere.center
    N = N / length(N)
    local_color = closest_sphere.color * ComputeLighting(P, N, -D, closest_sphere.specular)

    // If we hit the recursion limit or the object is not reflective, we're done
 ❶ r = closest_sphere.reflective
    if recursion_depth <= 0 or r <= 0 {
        return local_color
    }

    // Compute the reflected color
    R = ReflectRay(-D, N)
 ❷ reflected_color = TraceRay(P, R, 0.001, inf, recursion_depth - 1)

   ❸return local_color * (1 - r) + reflected_color * r
}
Listing 4-4: The raytracer pseudocode, now with reflections
清单4-4:光线跟踪器的伪代码,现在有了反射。

The changes to the code are surprisingly simple. First, we check whether we need to compute reflections at all ❶. If the sphere is not reflective or we hit the recursion limit, we’re done, and we can just return the sphere’s own color.
对代码的修改出乎意料地简单。首先,我们检查我们是否需要计算所有❶处的反射。如果球体没有反射或者我们遇到递归极限,我们就完成了,我们可以直接返回球体自己的颜色。

The most interesting change is the recursive call ❷; TraceRay calls itself, with the appropriate parameters for reflection and, importantly, decrementing the recursion depth counter; this, combined with the check ❶, prevents an infinite loop.
最有趣的变化是递归调用❷; TraceRay 调用自己,用适当的参数进行反射,重要的是,递减递归深度计数器;这与检查❶相结合,防止了无限循环的发生。

Finally, once we have the sphere’s local color and the reflected color, we blend them together ❸, using “how reflective this sphere is” as the blending weight.
最后,一旦我们有了球体的局部颜色和反射颜色,我们将它们混合在一起❸,使用 "这个球体的反射程度 "作为混合权重。

I’ll let the results speak for themselves. Check out Figure 4-8.
我将让结果自己说话。请看图4-8。

Figure 4-8: The raytraced scene, now with reflections

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

Summary

In the previous chapters, we developed a basic framework to render a 3D scene on a 2D canvas, modeling the way a ray of light interacts with the surface of an object. This gave us a simple initial representation of the scene.
在前几章中,我们开发了一个基本框架,在二维画布上渲染三维场景,对光线与物体表面的交互方式进行建模。这给了我们一个简单的场景的初始表示。

In this chapter, we extended this framework to model how different objects in the scene interact not only with rays of light, but with each other—by casting shadows over each other and by reflecting each other. As a result, the rendered scene looks significantly more realistic.
在这一章中,我们扩展了这个框架,以模拟场景中的不同物体如何不仅与光线互动,而且相互之间的互动--在对方身上投下阴影和相互反射。因此,渲染后的场景看起来明显更加逼真。

In the next chapter, we’ll briefly discuss different ways to extend this work, from representing objects other than spheres to practical considerations such as rendering performance.
在下一章中,我们将简要地讨论扩展这项工作的不同方法,从表现球体以外的物体到渲染性能等实际考虑。