mmp / pbrt-v3

Source code for pbrt, the renderer described in the third edition of "Physically Based Rendering: From Theory To Implementation", by Matt Pharr, Wenzel Jakob, and Greg Humphreys.
http://pbrt.org
BSD 2-Clause "Simplified" License
4.86k stars 1.18k forks source link

Why is there division by AbsCosTheta(*wi) in the FresnelSpecular bxdf? #300

Closed calvin-godfrey closed 3 years ago

calvin-godfrey commented 3 years ago

I've been working on my own implementation based on pbrt in Rust, and for the most part, it's been smooth (if slow) sailing. But I don't understand any geometric intuition for division by AbsCosTheta(*wi) in the following code:

Spectrum FresnelSpecular::Sample_f(const Vector3f &wo,
        Vector3f *wi, const Point2f &u, Float *pdf,
        BxDFType *sampledType) const {
    Float F = FrDielectric(CosTheta(wo), etaA, etaB);
    if (u[0] < F) {
           *wi = Vector3f(-wo.x, -wo.y, wo.z);

           if (sampledType)
               *sampledType = BxDFType(BSDF_SPECULAR | BSDF_REFLECTION);
           *pdf = F;
           return F * R / AbsCosTheta(*wi); // HERE

    } else {
           bool entering = CosTheta(wo) > 0;
           Float etaI = entering ? etaA : etaB;
           Float etaT = entering ? etaB : etaA;

           if (!Refract(wo, Faceforward(Normal3f(0, 0, 1), wo), etaI / etaT, wi))
               return 0;

           Spectrum ft = T * (1 - F);
           if (mode == TransportMode::Radiance)
               ft *= (etaI * etaI) / (etaT * etaT);

           if (sampledType)
               *sampledType = BxDFType(BSDF_SPECULAR | BSDF_TRANSMISSION);
           *pdf = 1 - F;
           return ft / AbsCosTheta(*wi); // and HERE

    }
}

To check this, I made a glass dragon with urough=vrough=0 and an index of 1 so that its compute scattering functions would add a specularfresnel to its bsdf. I also hardcoded it so that it would only take the specular transmission path, never specular reflection. As far as I'm aware, those parameters should make the glass model completely invisible, but it ended up looking like this, where the glass was much darker than expected and I got those bight pixels even significantly far away from the model.

On the other hand, if I run the exact same code, but simply have return ft at the bottom of the specular transmission branch, the model is invisible, as expected.

To run a more 'normal' render, I changed the index to 1.5 and made it so that specular transmission and reflection were computed, and as far as I can tell, it looks (more or less) exactly as it should:

specular_light_dragon_5.

Now, it's certainly possible that there's something else wrong in my implementation, but it seems like the root cause is that factor, and I couldn't find anything else. Is there a geometric explanation for why that is there, or something else that I am misunderstanding?

mmp commented 3 years ago

The BRDF for perfect specular reflection has a 1/cos theta_i term in it: http://www.pbr-book.org/3ed-2018/Reflection_Models/Specular_Reflection_and_Transmission.html#SpecularReflection. Note that it gets cancelled out later when the cos theta_i factor is incorporated from the reflection equation.

Some renderers return BRDF * cosTheta from their BRDF evaluation methods, which gets rid of this nit.. (So either way's fine, so long as you're consistent..)

calvin-godfrey commented 3 years ago

Okay, thanks for the explanation, that makes sense!