Skip to content

Clamp PBR surface opacity before shader construction - #3019

Open
tdavidovicNV wants to merge 2 commits into
AcademySoftwareFoundation:mainfrom
tdavidovicNV:spec/clamp-surface-opacity
Open

Clamp PBR surface opacity before shader construction#3019
tdavidovicNV wants to merge 2 commits into
AcademySoftwareFoundation:mainfrom
tdavidovicNV:spec/clamp-surface-opacity

Conversation

@tdavidovicNV

@tdavidovicNV tdavidovicNV commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

The PBR specification defines surface cutout opacity (O) over the range [0, 1], but does not specify how values outside that range are handled.

Consider this shader:

<?xml version="1.0"?>
<materialx version="1.39">
  <oren_nayar_diffuse_bsdf name="diffuse" type="BSDF" />

  <surface name="background" type="surfaceshader">
    <input name="bsdf" type="BSDF" nodename="diffuse" />
    <input name="opacity" type="float" value="0.2" />
  </surface>

  <surface name="foreground" type="surfaceshader">
    <input name="bsdf" type="BSDF" nodename="diffuse" />
    <input name="opacity" type="float" value="2.0" />
  </surface>

  <mix name="surface_mix" type="surfaceshader">
    <input name="bg" type="surfaceshader" nodename="background" />
    <input name="fg" type="surfaceshader" nodename="foreground" />
    <input name="mix" type="float" value="0.25" />
  </mix>

  <surfacematerial name="material" type="material">
    <input
      name="surfaceshader"
      type="surfaceshader"
      nodename="surface_mix"
    />
  </surfacematerial>
</materialx>

OSL clamps opacity at each leaf surface, giving:

mix(clamp(0.2, 0, 1), clamp(2.0, 0, 1), 0.25) = 0.4

GLSL and MDL effectively clamp only the final result, giving:

clamp(mix(0.2, 2.0, 0.25), 0, 1) = 0.65

Clamping at each surface node is preferable because opacity represents coverage in [0, 1]. A surface with opacity 2.0 has no meaningful coverage interpretation, and allowing that invalid value to participate in later surface operations can produce valid-looking but unintuitive downstream results.

This PR:

  • Defines the cutout opacity as O = clamp(opacity, 0, 1) before the surface shader is constructed.
  • Adds matching [0, 1] UI bounds and documentation to ND_surface.
  • Applies the clamp in the common hardware implementation used by GLSL, MSL, and Slang.
  • Applies the same behavior to the MDL implementation, aligning it with the existing OSL behavior.

If clamping is not the intended policy, the specification should instead explicitly declare out-of-range opacity inputs undefined. In either case, the current disagreement between backends should be resolved or documented.

@tdavidovicNV
tdavidovicNV marked this pull request as draft July 27, 2026 17:17
@tdavidovicNV
tdavidovicNV marked this pull request as ready for review July 27, 2026 18:20
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