1
$\begingroup$

I've been working through PBRT, implementing my own version of it in Rust. Right now I'm working on shadow rays, and it looks like pbrt doesn't do anything to account for specular behavior, and treats materials like perfect glass as opaque with respect to shadows. I decided to try to implement a version that accounts for this behavior, but I don't see a difference visually. Ignoring the bug that causes some of my threads to get stuck, the top one is my attempt at accounting for specular behavior and the bottom one is the 'normal' implementation that casts a single ray to see if it hits the light or not.

mine

normal

I can't see any difference in how the ground looks, and I don't believe that's how it should look for a perfectly specular glass model. My implementation is as follows (it's in Rust, so I've commented to get the general idea across):

pub fn occlusion_intersects(&self, ray: &Ray, tmin: f64, tmax: f64, depth: i32) -> Option<HitRecord> {
    // find intersection with scene
    let ans = self.intersects(ray, tmin, tmax);
    // if no intersection was found, return None
    if ans.is_none() {
        return None
    }
    let mut ans = ans.unwrap(); // rust stuff, ignore this
    // compute the bsdf for the material
    Material::compute_scattering_default(&mut ans);
    // we only want to specular transmission or reflection
    let bsdf_type = BSDF_TRANSMISSION | BSDF_REFLECTION | BSDF_SPECULAR;
    // sample_f returns a new outgoing direction according to a bsdf that matches
    // one of the types above, or (0, 0, 0) if no matching bsdfs.
    let (wi, .. ) = ans.bsdf.sample_f(&ans.wo, bsdf_type);
    // if there was no specular bsdf or we've recursed too many times:
    if (wi == util::zero_vector() || depth == MAX_DEPTH {
        // then return the current intersection to check if it
        // hit the expected light object
        return Some(ans);
    }
    // otherwise make a new ray going in the new specular direction
    let new_ray = ans.spawn_ray(&wi);
    // and return that answer
    return self.occlusion_intersects(&new_ray, tmin, tmax, depth + 1);
}

And for reference, the default implementation would be:

pub fn occlusion_intersects(&self, ray: &Ray, tmin: f64, tmax: f64, depth: i32) -> Option<HitRecord> {
    // return the first intersection, ignore all specular behavior
    return self.intersects(ray, tmin, tmax);
}

Is there something wrong with my implementation? I kind of guessed as to how it would work, but the logic written in the comments makes sense to me.

$\endgroup$
2
  • $\begingroup$ What do you mean by "pbrt doesn't do anything to account for specular behavior, and treats materials like perfect glass as opaque with respect to shadow"? How else should it be treated? $\endgroup$
    – Hubble
    Commented Dec 14, 2020 at 20:53
  • $\begingroup$ @Hubble my thinking is that for perfectly specular transmission/reflection, you can continue tracing the shadow ray because there's only one possible outgoing vector for a given incoming vector. $\endgroup$ Commented Dec 14, 2020 at 20:59

1 Answer 1

1
$\begingroup$

Shadow rays that hit a glass surface or similar are stopped because there's no direct path from the light source to the shading point—there's a piece of glass in the way.

for perfectly specular transmission/reflection, you can continue tracing the shadow ray because there's only one possible outgoing vector for a given incoming vector

That won't work, because the transmission/reflection will change the direction of the ray. The shadow ray was presumably created by pointing it at a light source, but after transmission/reflection, it's most likely not going to hit the light source anymore.

The idea of firing rays directly at light sources can be seen as a sampling strategy designed to exploit knowledge of which directions light is most likely to come from, and put your rays into those directions. If you're receiving light via a specular interaction rather than directly from the source, you don't know anymore which directions you should sample in.

I imagine for simple cases like a glass sphere, etc, you could algebraically solve for a light path that would refract toward a chosen point on a light source. Then you could trace shadow rays along that path. But when you get into geometry as complex as the dragon, there's no realistic way to calculate which directions the light is going to come in from.

When specular surfaces like glass or metal focus light onto a diffuse surface, that produces a caustic. Caustics are a notorious pain point for vanilla path tracing due to this problem—there is no way to know where to concentrate your samples to make your rays hit light sources, so the best you can do is fire rays in all directions (or sample from the BRDF) and hope that enough of them will find their way to a light source. Technically, path tracing can render caustics correctly, but it takes an extremely large number of samples to do it.

More practically, this issue can be solved by using photon mapping (which was invented precisely to deal with caustics), or some kind of path guiding methods like Metropolis and friends that can "discover" the caustics and dynamically concentrate samples around them.

$\endgroup$
2
  • $\begingroup$ I appreciate the write up, I see where my misunderstanding was! I just have one follow up question, is my idea right, at least in theory? That if I follow the path of a shadow ray through specular materials and it ends up hitting the light, then the original point should be considered unoccluded with respect to the light? $\endgroup$ Commented Dec 14, 2020 at 23:47
  • 1
    $\begingroup$ I don't think so. The problem is, then you'll be double-counting the light, because you'll also accumulate it when you hit it through your regular recursive path tracing. When you explicitly sample lights using shadow rays, you have to ignore them if you hit them through recursive path tracing (with no intervening bounces). Basically you need to make sure each type of light path is handled by just one sampling strategy, you don't want the same path to be able to be generated through multiple of them. $\endgroup$ Commented Dec 15, 2020 at 0:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.