Skip to content

[Bug] IBL diffuse term uses multi-bounce-compensated reflectance instead of single-bounce #1296

Description

@untoldengine

Context

Follow-up from PR #1292 (material shading fixes) — see review comment: #1292 (comment)

Issue

In computeIBLContribution (Sources/UntoldEngine/Shaders/LightShader.metal), the diffuse term is attenuated by the full multi-bounce-compensated reflected value returned by environmentReflectance (which includes the Kulla/Conty laterBounces energy-compensation factor), rather than the raw single-bounce reflectance (reflectedInOneBounce).

Physically, the energy "taken from" diffuse should be the single-bounce specular share at that viewing angle. The multi-bounce compensation exists to recover energy lost by the specular term's own approximation (accounting for light bouncing between a rough surface's microfacets before escaping) — it isn't a measure of how much the surface diverts away from diffuse scattering in the first place. Using the compensated value for both purposes conflates the two.

Why it hasn't caused a visible bug yet

  • For non-metals, F0 = 0.04 keeps laterBounces close to 1, so the error is small.
  • For metals, (1 - metallic) = 0 zeroes out the diffuse term entirely, so the discrepancy never shows up.

None of the new render tests in #1292 caught this because they don't target the specific case where it would matter: a non-metal with high roughness where laterBounces deviates furthest from 1.

Suggested fix

Use reflectedInOneBounce (already computed inside environmentReflectance) for the diffuse-energy-conservation term, and keep the fully compensated reflected value only for the specular contribution. This likely means exposing reflectedInOneBounce from environmentReflectance (e.g. as an additional return value or a second small helper) rather than just the final compensated color.

Acceptance criteria

  • Diffuse attenuation in computeIBLContribution uses single-bounce reflectance, not the multi-bounce-compensated one.
  • Existing EnvironmentReflectionShadingTests (especially testAWhiteSurfaceGivesBackAllTheLightWhateverItsFinish and testARoughColoredMetalStaysWithinItsColor) still pass.
  • Consider adding a test for a rough, high-F0-adjacent non-metal (e.g. roughness 1.0, base color gray) to pin down the expected diffuse/specular split precisely enough to catch regressions here.

Activity

  1. changed the title [-]IBL diffuse term uses multi-bounce-compensated reflectance instead of single-bounce[/-] [+][Bug] IBL diffuse term uses multi-bounce-compensated reflectance instead of single-bounce[/+] on Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions