How can I account for specular interactions in shadow rays? 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.


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(&amp;self, ray: &amp;Ray, tmin: f64, tmax: f64, depth: i32) -&gt; Option&lt;HitRecord&gt; {
    // 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(&amp;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(&amp;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(&amp;wi);
    // and return that answer
    return self.occlusion_intersects(&amp;new_ray, tmin, tmax, depth + 1);
}

And for reference, the default implementation would be:
pub fn occlusion_intersects(&amp;self, ray: &amp;Ray, tmin: f64, tmax: f64, depth: i32) -&gt; Option&lt;HitRecord&gt; {
    // 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.
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 &quot;discover&quot; the caustics and dynamically concentrate samples around them.