Skip to content

Fix view-dependent albedo AOV for glossy-only BSDFs - #1896

Open
RyuDongHo wants to merge 4 commits into
mitsuba-renderer:masterfrom
RyuDongHo:fix-edr-glossy-only
Open

RyuDongHo wants to merge 4 commits into
mitsuba-renderer:masterfrom
RyuDongHo:fix-edr-glossy-only

Conversation

@RyuDongHo

@RyuDongHo RyuDongHo commented Jun 10, 2026 •

Copy link
Copy Markdown

Description

While generating albedo G-buffers with Mitsuba for Monte Carlo denoising
research, I noticed the albedo AOV was contaminated on glossy-only surfaces:
a bright, camera-dependent blob appears wherever a rough-metal or rough-glass
surface faces the camera, with values far above 1 (up to ~96 in the attached
scene). An albedo AOV is expected to be a view-independent reflectance in
[0, 1] — this silently corrupts every denoising workflow built on it (e.g.
the OptiX denoiser integration from #283).

The cause is the base-class fallback of BSDF::eval_diffuse_reflectance()
(eval at wo = (0, 0, 1) times π): exact for a Lambertian lobe, but for
BSDFs without a diffuse lobe it evaluates the glossy microfacet lobe, which
spikes at the specular peak when the view direction approaches the normal.
Related precedent: #862 (missing override in NormalMap).

This PR overrides eval_diffuse_reflectance() in the two microfacet-only
plugins, following the Open Image Denoise albedo recommendations:

  • roughconductor → the normal-incidence Fresnel reflectance (F0),
    modulated by specular_reflectance when present (OIDN: "the reflectivity
    at normal incidence"
    for metallic surfaces).
  • roughdielectric → 1, tinted by specular_transmittance when present
    (OIDN: an albedo of 1 for non-delta dielectric surfaces such as glass).

The doxygen documentation in include/mitsuba/render/bsdf.h is updated
accordingly. If a stricter reading of the "diffuse reflectance" contract is
preferred (return 0, since neither plugin has a diffuse lobe), the first
commit of this branch implements exactly that and the follow-up commits can
be dropped — happy to adjust.

Fixes # (issue)

Testing

  • New regression tests, passing for scalar_rgb (and the full files pass
    including the existing chi2/eval_pdf tests, 20/20):
    • test_rough_conductor.py::test07_eval_diffuse_reflectance — value equals
      F0 computed from η, k (η=0.2, k=3 → 0.9234) at five view angles
      (view-independence + correctness).
    • test_rough_dielectric.py::test14_eval_diffuse_reflectance — value equals
      1 for plain rough glass and equals the tint for
      specular_transmittance = (0.2, 0.4, 0.6), at five view angles.
  • Standalone repro script (attached to the issue): before → roughconductor
    23.83 / roughdielectric 0.97 at cos θ = 0.999, view-dependent; after →
    flat F0 / 1.0 at all angles. diffuse control unchanged at its reflectance.
  • Scene-level check (cuda_ad_rgb, aov integrator albedo:albedo wrapping
    path): Tungsten "spaceship" close-up of a rough-glass canopy and rough
    metal. Before: albedo max 96.27 with a camera-tracking blob. After: glass
    dome flat 1.0, metal flat F0·specular_reflectance ≈ 0.537, no
    view-dependent content, max value 1.0.

Checklist

  • My code follows the style guidelines of this project
  • My changes generate no new warnings
  • My code also compiles for cuda_* and llvm_* variants. If you can't test this, please leave below
  • I have commented my code
  • I have made corresponding changes to the documentation
  • I have added tests that prove my fix is effective or that my feature works
  • I cleaned the commit history and removed any "Merge" commits
  • I give permission that the Mitsuba 3 project may redistribute my contributions under the terms of its license

…ctric)

The base-class fallback of eval_diffuse_reflectance() evaluates the BSDF
at wo = (0, 0, 1) and multiplies by pi. This is exact for a Lambertian
lobe, but for microfacet-only BSDFs it evaluates the glossy lobe instead:
when the view direction approaches the surface normal, the half-vector
aligns with the specular peak and the returned value spikes (with
alpha = 0.1, roughdielectric returns ~0.97 and roughconductor ~24 at
cos(theta) ~ 1, falling to 0 within a few degrees).

In renders this paints a bright, view-dependent blob into the albedo AOV
wherever a glossy surface faces the camera, violating the contract of a
view-independent reflectance in [0, 1] that denoisers rely on.

Override eval_diffuse_reflectance() to return 0 in roughconductor and
roughdielectric (no diffuse lobe), document the behaviour in bsdf.h, and
add regression tests covering several view angles.
Returning 0 makes the albedo AOV uninformative on rough metals: denoisers
lose the texture and color edges of the material. Return the
normal-incidence Fresnel reflectance (F0), modulated by
specular_reflectance when present, instead. This is the value denoisers
such as OIDN recommend as the albedo of a metal: view-independent,
bounded in [0, 1], and carrying the base color of colored metals
(gold, copper).

roughdielectric keeps returning 0, since a transmissive microfacet
surface has no meaningful base reflectance.
A rough dielectric reflects or transmits essentially all incident
energy, so 1 -- tinted by specular_transmittance when present -- is the
appropriate view-independent albedo for denoising AOVs (the convention
denoisers such as OIDN recommend for transmissive surfaces). Returning 0
made the albedo feature claim the surface reflects nothing, which is
the opposite of its actual throughput.
…ents

The F0 / 1 fallback values for roughconductor and roughdielectric follow
the Open Image Denoise documentation for metallic and dielectric
surfaces; say so where they are implemented.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant