knightcrawler25 / GLSL-PathTracer

A toy physically based GPU path tracer (C++/OpenGL/GLSL)
MIT License
1.87k stars 176 forks source link

Reflection term evaluates into incorrect contribution for smooth surfaces #96

Closed gamer9xxx closed 4 months ago

gamer9xxx commented 4 months ago

For the perfectly smooth materials, DisneyEval returns a full light contribution, even for angles that are far from the perfect reflection. DisneySample never generates such samples, so usually this is not a problem, but once the samples are generated any other way (e.g. NEE), this results in a wrong contribution. To better demonstrate what I mean, I wrote a simple unit test, where the debug_color.b should be always 0, yet it evaluates to the same full light contribution as a perfect reflection.

    State state = (State)0;
    state.eta = 1.0f;
    state.mat.baseColor = float3(1.0f, 1.0f, 1.0f);
    state.mat.metallic = 1.0f;
    state.mat.ior = 1.0f;

    float aspect = sqrt(1.0 - state.mat.anisotropic * 0.9);
    state.mat.ax = max(0.001, state.mat.roughness / aspect);
    state.mat.ay = max(0.001, state.mat.roughness * aspect);

    float3 normal = float3(0.0f, 1.0f, 0.0f);
    float3 N = float3(0.0f, 1.0f, 0.0f);
    float3 L = float3(0.0f, 1.0f, 0.0f);

    float3 V_0 = float3(0.0f, 1.0f, 0.0f); // perfect reflection angle
    float pdf0 = 0.0f;
    float3 bsdf0 = DisneyEval(state, V_0, N, L, pdf0);

    float3 V_1 = normalize(float3(0.0f, 1.0f, 1.0f)); // far from perfect reflection angle
    float pdf1 = 0.0f;
    float3 bsdf1 = DisneyEval(state, V_1, N, L, pdf1);

    debug_color.r = pdf0;
    debug_color.g = pdf1;

    if (0.0f != pdf0 && 0.0f != pdf1)
        debug_color.b = all((bsdf0 / pdf0) == (bsdf1 / pdf1)); // Both reflections evaluate to the same contribution
    else
        debug_color.b = 0.0f;
knightcrawler25 commented 4 months ago

Hey,

In your code, for NEE, you are evaluating the BSDF but are not taking into account the light pdf and instead using the BSDF pdf (without MIS) which isn't quite right.

You can have a look at the light sampling and BSDF sampling code from the simple path tracer example from PBRTv4:

Light Sampling: https://github.com/mmp/pbrt-v4/blob/39e01e61f8de07b99859df04b271a02a53d9aeb2/src/pbrt/cpu/integrators.cpp#L441

BSDF Sampling: https://github.com/mmp/pbrt-v4/blob/39e01e61f8de07b99859df04b271a02a53d9aeb2/src/pbrt/cpu/integrators.cpp#L453

Documentation: https://www.pbr-book.org/4ed/Light_Transport_I_Surface_Reflection/A_Simple_Path_Tracer

Here are some examples from this project (16 spp):

Only light sampling (essentially similar to your unit test): img_16_light

Only BSDF sampling: img_16_bsdf

MIS: img_16_mis

Another example with light sampling only and a directional light (zero area and pdf = 1). The sphere on the top left is a smooth white metal with 0.001 roughness. Here only the highlight is visible and the remaining surface has no contribution which is the expected output from the BSDF evaluation for the light: img_16

gamer9xxx commented 4 months ago

Hi, I see, thanks for the explanation, you are correct!

I couldn't get my head around the fact that BSDF.eval / BSDF.pdf returns full contribution regardless of the direction, but that's still valid because BSDF.sample never returns such direction and in case we get samples from other domain such as NEE.sample, we must apply proper MIS to get the correct contribution, so it actually all works as expected.

Thanks for the examples, esp. the last example with pdf = 1 was an interesting exercise! And big thanks for this great library!