Monday, January 31, 2022

Direct Light 2: integrating shadow rays into a path tracer

Part 1 of 3

In my last post, I talked about using shadow rays to sample a light source directly.  Here is our old lovely but noisy path tracer:

vec3 ray_color(ray r)
if (r hits an object)
r = ray(hitpoint, generate a scattered  direction from the surface)
return emitted(hitpoint) + reflectance*ray_color(r)
else
return background_color(r)

This works great provided the light emitting objects are big, but otherwise we get a lot of noise.  We also are crossing our fingers we don't get an infinite recursion.

Let's assume we have a magic function direct_light(vec3 p, reflectance_info ref).   Can we just do this to the return line above:

return emitted(hitpoint) + direct(hitpont, ref_info) +reflectance*ray_color(r)

NOPE!  That would double count direct because that sacttered ray r might hit the light and get the emitted part.  So this is better:

return direct(hitpont, ref_info) +reflectance*ray_color(r)

BUT, if you see the light source in the picture, it will be black!

And before we fix that, another problem is that if the surface is a perfect mirror, the direct lighting itself will be too noisy because only one point on the light matters.

So we need something like a "sees the light" flag on a ray.

vec3 ray_color(ray r)
vec3 color = (0, 0, 0)
if (r hits an object)
if (r.should_see_lights)
color +=  emitted(hitpoint)
r = ray(hitpoint, generate a scattered  direction from the surface, should_r_see_lights_flag)
if (r.should_see_light)
color += reflectance*ray_color(r)
else
color += direct(hitpoint, ref_info) +reflectance*ray_color(r)
else
color += background_color(r)
return color

Man that is pretty ugly!  But I don't know a much better way.  Be sure to set the viewing ray flags to "should_see_light = true".

Are we done?  No.  For perfect mirrors, should_see_lights scattered rays should be set true.  For diffuse reflectors, false.  For glossy objects, it depends on the size of the light source.  Here, a lovely technique from Eric Veach is often used.   I would go with the "balance heuristic"-- it is easiest.  There is a wonderful figure from the paper that shows why this technique is needed:

So are we done?   No, there is one more issue.  Does the background "emit" light?  Isn't it a light source.  The answer is you can do it either way.  But if it is a light source, and you can hit it, we can get rid of the   if (r hits an object)  branch-- the background is always hit!  But if it has infinite radius where is the hitpoint?  I would say these design decisions are not obvious and I am on the fence even after trying all of them.

Next time: what goes in here: direct(hitpoint, ref_info)

Brian said...

Don't some path tracers get rid of the should_see_lights flag with this complexity by separating the "light emitting objects" from the other geometry. The only thing is if you have to SEE the lights then you have to double the light geometry with a simple function that returns the constant, non scattering light color on camera rays. I suppose it's a bit doing the same thing, So you'd have something like:

vec3 ray_color(ray r)
vec3 color = (0, 0, 0)
if (r hits an non_light_object)
color += emitted(hitpoint, &do_scatter) // do_scatter returns false if this is a "light geometry" and first bounce
if (do_scatter)
r = ray(hitpoint, generate a scattered direction from the surface)
color += reflectance*ray_color(r) + direct(hitpoint, ref_info) // direct lighting against light_objects
else
color += background_color(r)
return color

This is much cleaner than the sees the light flag IMO though maybe doing basically the same thing. The other nice part is that you can then completely separate the lights used for direct lighting into their own data structure from the scene geometry.

Peter Shirley said...

Thanks Brian I like it!