Reflection

Look around you. Odds are you’ll find something in your vicinity that is reflective to one degree or another. Maybe it’s your phone’s screen, or a polished table, or a window, or a pair of sunglasses. Whatever it is, that reflection gives you all kinds of clues about what to expect from that surface and helps convince your brain that what you’re seeing is real.

This works in rendered scenes, too. Adding even just a subtle bit of reflection can make your scene bloom with photorealism. Consider the following two images:

images/reflection/reflection-comparison.png

Both depict the same scene, from the same angle and with the same lighting, but the floor on the right is just slightly reflective, making it appear more glossy than the other.

You’ll add this feature to your ray tracer with seven tests:

  1. Add a reflective attribute to your material data structure.
  2. Update prepare_computations to compute the ray’s reflection vector, reflectv.
  3. Handle the case where the ray strikes a nonreflective surface.
  4. Handle the case where the ray strikes a reflective surface.
  5. Make sure shade_hit calls the function for computing reflections.
  6. Make sure your ray tracer can avoid infinite recursion, as when a ray bounces between two parallel mirrors.
  7. Show that your code can set a limit to how deeply recursion is allowed to go.

Note that from here on out, the chapters will be a bit more streamlined. Up to this point, you saw tests introduced with a bit of fanfare and discussion. But now the training wheels are coming off. You know the drill by now. You will see the tests, you will get a bit of explanation, and (where necessary) you will walk through the algorithms and perhaps a smattering of pseudocode. You’ve got this!

Here we go, one test at a time.

Test #1: Add the reflective Material Attribute

Show that your material structure contains a new attribute, called reflective.

When reflective is 0, the surface is completely nonreflective, whereas setting it to 1 produces a perfect mirror. Numbers in between produce partial reflections.

 Scenario​: Reflectivity for the default material
 Given​ m ← material()
 Then​ m.reflective = 0.0

Make sure the new attribute is a floating point value, so that you can implement partial reflection.

Test #2: Compute the reflectv Vector

Show that the prepare_computations function precomputes the reflectv vector.

Create a plane and position a ray above it, slanting downward at a 45° angle. Position the intersection on the plane, and have prepare_computations compute the reflection vector.

1: Scenario​: Precomputing the reflection vector
2: Given​ shape ← plane()
3: And​ r ← ray(point(0, 1, -1), vector(0, -√2/2, √2/2))
4: And​ i ← intersection(√2, shape)
5: When​ comps ← prepare_computations(i, r)
6: Then​ comps.reflectv = vector(0, √2/2, √2/2)

Line 3 creates and orients the ray, and line 4 places the hit 2 units away, courtesy of the Pythagorean theorem. Lastly, line 6 asserts that the reflect vector bounces up from the plane at another 45° angle.

Compute reflectv in prepare_computations by reflecting the ray’s direction vector around the object’s normal vector, like this:

 # after negating the normal, if necessary
 comps.reflectv ← reflect(ray.direction, comps.normalv)

It’s just like you did in your lighting function, in The Phong Reflection Model, when you computed the light’s reflection vector. Here, though, you’re reflecting the ray, and not the light.

Test #3: Strike a Nonreflective Surface

Show that when a ray strikes a nonreflective surface, the reflected_color function returns the color black.

You’re getting to the meat of the reflection algorithm itself, now. This test introduces a new function, reflected_color(world, comps), which will be the core of how your ray tracer computes reflections.

Place a ray inside at the origin of the default world, inside both of the world’s spheres. Bounce the ray off the innermost sphere. By setting the sphere’s ambient property to 1, you can guarantee that any reflection will have something to reflect—but because the innermost sphere is not reflective, reflected_color should simply return black.

 Scenario​: The reflected color for a nonreflective material
 Given​ w ← default_world()
 And​ r ← ray(point(0, 0, 0), vector(0, 0, 1))
 And​ shape ← the second object in w
 And​ shape.material.ambient ← 1
 And​ i ← intersection(1, shape)
 When​ comps ← prepare_computations(i, r)
 And​ color ← reflected_color(w, comps)
 Then​ color = color(0, 0, 0)

For this test, make your reflected_color function return black when the material’s reflective attribute is 0. The next test will flesh that function out a bit more.

Test #4: Strike a Reflective Surface

Show that reflected_color returns the color via reflection when the struck surface is reflective.

Add a reflective plane to the default scene, just below the spheres, and orient a ray so it strikes the plane, reflects upward, and hits the outermost sphere.

1: Scenario​: The reflected color for a reflective material
Given​ w ← default_world()
And​ shape ← plane() with:
| material.reflective | 0.5 |
5:  | transform | translation(0, -1, 0) |
And​ shape is added to w
And​ r ← ray(point(0, 0, -3), vector(0, -√2/2, √2/2))
And​ i ← intersection(√2, shape)
When​ comps ← prepare_computations(i, r)
10: And​ color ← reflected_color(w, comps)
Then​ color = color(0.19032, 0.2379, 0.14274)

Lines 3–5 configure the (semi)reflective plane and position it at y = -1. After preparing the hit, the reflected color will be a darker version of the sphere’s shade of green, because the plane will only reflect half of the light from the sphere.

Implement reflected_color by creating a new ray, originating at the hit’s location and pointing in the direction of reflectv. Find the color of the new ray via color_at, and then multiply the result by the reflective value. If reflective is set to something between 0 and 1, this will give you partial reflection.

In pseudocode, it goes like this:

 function​ reflected_color(world, comps)
 if​ comps.object.material.reflective = 0
 return​ color(0, 0, 0)
 end​ ​if
 
  reflect_ray ← ray(comps.over_point, comps.reflectv)
  color ← color_at(world, reflect_ray)
 
 return​ color * comps.object.material.reflective
 end​ ​function

Spawning these secondary rays is how ray tracers can produce such realistic reflections. Just make sure to use the comps.over_point attribute (and not comps.point) when constructing the new ray. Otherwise, floating point rounding errors will make some rays originate just below the surface, causing them to intersect the same surface they should be reflecting from.

Test #5: Update the shade_hit Function

Show that shade_hit incorporates the reflected color into the final color.

Recycle the previous test, but this time call shade_hit instead of calling reflected_color directly. The resulting color should combine the white of the plane with the reflected green of the sphere.

 Scenario​: shade_hit() with a reflective material
 Given​ w ← default_world()
 And​ shape ← plane() with:
  | material.reflective | 0.5 |
  | transform | translation(0, -1, 0) |
 And​ shape is added to w
 And​ r ← ray(point(0, 0, -3), vector(0, -√2/2, √2/2))
 And​ i ← intersection(√2, shape)
 When​ comps ← prepare_computations(i, r)
 And​ color ← shade_hit(w, comps)
 Then​ color = color(0.87677, 0.92436, 0.82918)

Implement this by making the shade_hit function call reflected_color, and adding the color it returns to the surface color. In pseudocode:

 function​ shade_hit(world, comps)
  shadowed ← is_shadowed(world, comps.over_point)
 
  surface ← lighting(comps.object.material,
  comps.object,
  world.light,
  comps.over_point, comps.eyev, comps.normalv,
  shadowed)
 
» reflected ← reflected_color(world, comps)
»
»return​ surface + reflected
 end​ ​function

By adding the reflected color to the surface color, the two blend together and produce a believable reflection. However, there’s a gotcha hiding here. The shade_hit function now calls reflected_color, which calls color_at, which calls shade_hit… That’s a recursive loop, with the potential to cause some problems. Let’s address that next.

Test #6: Avoid Infinite Recursion

Show that your code safely handles infinite recursion caused by two objects that mutually reflect rays between themselves.

Create two parallel mirrors by positioning one plane above another and making them both reflective. Orient a ray so that it strikes one plane and bounces to the other. What will happen?

 Scenario​: color_at() with mutually reflective surfaces
 Given​ w ← world()
 And​ w.light ← point_light(point(0, 0, 0), color(1, 1, 1))
 And​ lower ← plane() with:
  | material.reflective | 1 |
  | transform | translation(0, -1, 0) |
 And​ lower is added to w
 And​ upper ← plane() with:
  | material.reflective | 1 |
  | transform | translation(0, 1, 0) |
 And​ upper is added to w
 And​ r ← ray(point(0, 0, 0), vector(0, 1, 0))
 Then​ color_at(w, r) should terminate successfully

Your ray tracer will probably not handle this well. Because of that recursive loop you made for the previous test, your reflections will bounce back and forth between those two mirrors, right up until your stack explodes.

Infinite recursion is the pits.

Joe asks:
Joe asks:
How can I test “should terminate successfully”?

Testing “should terminate successfully” can be tricky. Rather than trying to determine whether your program will actually terminate (because good luck with that[17]), it might be better to check for the opposite. Look for what happens when the program doesn’t terminate. Mostly likely, under infinite recursion, your program will eventually run out of memory. Does your environment raise an exception when this happens? Test for that, if you can. Or, if that’s not an option, you might instead assert that the function terminates in some finite amount of time.

Still, the tests must pass. One way to accomplish this is to limit how deeply the recursion is allowed to go. After all, if a ray can only bounce four or five times, it is unlikely to blow up your call stack. You can implement this constraint by declaring some threshold and then requiring the reflected_color function to return immediately if the recursion goes deeper than that.

For now, allow this test to fail. The next test will point you in the right direction and will help you get them both passing.

Test #7: Limit Recursion

Show that reflected_color returns without effect when invoked at the limit of its recursive threshold.

Duplicate the scenario in Test #5: Update the shade_hit Function. The difference, though, is that here you’ll invoke reflected_color(world, comps, remaining) with a new, additional parameter—remaining—which tells the function how many more recursive calls it is allowed to make.

1: Scenario​: The reflected color at the maximum recursive depth
Given​ w ← default_world()
And​ shape ← plane() with:
| material.reflective | 0.5 |
5:  | transform | translation(0, -1, 0) |
And​ shape is added to w
And​ r ← ray(point(0, 0, -3), vector(0, -√2/2, √2/2))
And​ i ← intersection(√2, shape)
When​ comps ← prepare_computations(i, r)
10: And​ color ← reflected_color(w, comps, 0)
Then​ color = color(0, 0, 0)

Line 10 sets the remaining parameter to 0, telling the function that it is not allowed to make any more recursive calls. It should return black instead.

Make this pass by adding another condition to the top of your reflected_color function. It should return black if remaining is less than 1.

To make this useful, though, you next need to pass that number back and forth between color_at, shade_hit, and reflected_color. Perform the following refactoring:

  1. Add a third parameter to color_at(world, ray, remaining).
  2. Add a third parameter to shade_hit(world, hit, remaining).
  3. Make color_at pass the remaining value to shade_hit.
  4. Make it so that when reflected_color calls color_at, it decrements the remaining value before passing it on.

In other words, use something like this:

 function​ color_at(world, ray, remaining)
 # ...
  color ← shade_hit(world, comps, remaining)
 # ...
 end​ ​function
 
 function​ shade_hit(world, comps, remaining)
 # ...
  reflected ← reflected_color(world, comps, remaining)
 # ...
 end​ ​function
 
 function​ reflected_color(world, comps, remaining)
 if​ remaining <= 0
 return​ color(0, 0, 0)
 end​ ​if
 
 # ...
  color ← color_at(world, reflect_ray, remaining - 1)
 # ...
 end​ ​function

In this way, your code keeps track of how deep the recursion is allowed to go and avoids nastiness when things get a little carried away.

Be sure to change all existing calls of color_at and shade_hit to pass in the maximum recursive depth via the new remaining parameter. If your programming language supports default parameter values, this is a great place for it. Setting remaining’s default value to 4 or 5 is empirically pretty safe. Larger numbers will slow down your renderer on scenes with lots of reflective objects.

Once that’s done, you should find that all your previous tests are now passing again, including Test #6: Avoid Infinite Recursion.

Whew!

Take a moment and celebrate with a simple scene. Populate it with spheres, and make some of them reflective. See how the color of a surface affects reflection. Do some colors work better than others? What happens when you vary the ambient, diffuse, and specular parameters on a reflective surface?

When you’ve got that working to your satisfaction, read on. It’s time to talk about transparency and refraction.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.174.239