Skip to content

Clamp metallic Fresnel after specular_weight scaling via F0 and F82 - #316

Open
jamportz wants to merge 1 commit into
AcademySoftwareFoundation:dev_1.2from
jamportz:metal_specular_weight_clamp
Open

jamportz wants to merge 1 commit into
AcademySoftwareFoundation:dev_1.2from
jamportz:metal_specular_weight_clamp

Conversation

@jamportz

@jamportz jamportz commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #314.

image

Summary

In v1.2 (#238) the metallic Fresnel is $\mathrm{clamp}(\xi_s \mathbf{F}_{82}(\mu), 0, 1)$, where $\xi_s$ = specular_weight. The MaterialX graph can't express this clamp, because the Fresnel is evaluated inside generalized_schlick_bsdf. So the reference graph just sets the lobe weight to $\xi_s$, and for $\xi_s > 1$ it reflects more light than it receives (see the furnace test below).

As discussed in the meeting, this PR instead clamps the two colors that parametrize the F82-tint curve:

$$\mathbf{F}_{\mathrm{metal}}(\mu) = \min(\xi_s, 1) \, \max\bigl(\mathbf{F}_{82}(\mu), 0\bigr)$$

where $\mathbf{F}_{82}(\mu)$ is the existing F82-tint curve, built from the colors

$$\mathbf{F}_0 = \min(k \, \mathtt{base\_weight} \, \mathtt{base\_color}, 1) \ , \quad \mathbf{C}_s = \min(k \, \mathtt{specular\_color}, 1) \ , \quad k = \max(\xi_s, 1) \ .$$
  • $\xi_s \le 1$: identical to before ($k = 1$). The whole metallic lobe is scaled by $\xi_s$, so there is no look change in the normal range. The $\xi_s = 1$ renders below are pixel-identical to the old graph.
  • $\xi_s > 1$: $\mathbf{F}_0$ and the F82 edge tint $\mathbf{C}_s$ are both boosted by $\xi_s$, each clamped to $[0, 1]$. The result is always a valid F82-tint curve, so it is bounded by 1 with no upper clamp. As $\xi_s \to \infty$ it tends to a perfect white mirror, the same limit as the previous clamp.
  • Expressible in MaterialX with standard nodes, via the weight, color0 and color82 inputs of generalized_schlick_bsdf.

A footnote records an equivalent form for generalized-Schlick implementations: $\mathbf{F}_0 = \min(\xi_s \mathtt{base\_weight} \mathtt{base\_color}, 1)$ and $\mathbf{F}_{90} = \min(\xi_s, 1)$, with no lobe weight. This works because $\mathbf{C}_s$ is relative to the Schlick curve, so scaling the whole curve by $\xi_s \le 1$ is the same as scaling $\mathbf{F}_0$ and $\mathbf{F}_{90}$.

Changes

  • Spec (index.html): $\mathbf{F}_{\mathrm{metal}}$ is now stated in terms of the existing $\mathbf{F}_{82}$ curve, with $\mathbf{F}_0$ and $\mathbf{C}_s$ defined as the boosted, clamped colors. Added the $\mathbf{F}_{90}$ footnote and updated the specular_weight description in the metal parameter table.
  • MaterialX (reference/open_pbr_surface.mtlx): added metal_weight $= \min(\xi_s, 1)$, metal_boost $= \max(\xi_s, 1)$, metal_F0 and metal_F82, and wired them into all four metal generalized_schlick_bsdf nodes (base, thin-film, haze, haze + thin-film).
    • The coat-darkening albedo estimate Emetal is now metal_weight * metal_F0 $= \min(\xi_s \mathtt{base\_weight} \mathtt{base\_color}, 1)$. Note: this also brings base_weight into Emetal; previously it used base_color alone.
  • CHANGELOG.md: added an entry.

Furnace test

A smooth ($\mathtt{specular\_roughness} = 0$) metal sphere in a uniform white environment of radiance 1. Each pixel is then exactly $\mathbf{F}_{\mathrm{metal}}(\mu)$, so anything brighter than the background creates energy. The images are shown one stop down, so the background is mid-grey. "max" is the largest pixel value in the image; the background is 1, so a physical result has max = 1. All renders use the MaterialX graph compiled to OSL and rendered in Arnold.

Rows: v1.2 graph is the current graph (unclamped); A boosts $\mathbf{F}_0$ only; B (this PR) boosts $\mathbf{F}_0$ and $\mathbf{C}_s$; C boosts $\mathbf{F}_0$ and the absolute 82° reflectivity (see "Alternatives considered").

Gold: $\mathtt{base\_color} = (1, 0.72, 0.315)$, $\mathtt{specular\_color} = (1, 0.973, 0.597)$

$\xi_s = 1$ $\xi_s = 2$ $\xi_s = 4$ $\xi_s = 8$
v1.2
graph

max 1.00

max 2.00

max 4.00

max 8.00
A
max 1.00

max 1.00

max 1.00

max 1.00
B
(this PR)

max 1.00

max 1.00

max 1.00

max 1.00
C
max 1.00

max 1.00

max 1.00

max 1.00

Tinted metal: $\mathtt{base\_color} = (0.5, 0.5, 0.5)$, $\mathtt{specular\_color} = (0.3, 0.55, 0.9)$

$\xi_s = 1$ $\xi_s = 2$ $\xi_s = 4$ $\xi_s = 8$
v1.2
graph

max 1.00

max 1.60

max 3.18

max 6.35
A
max 1.00

max 1.00

max 1.00

max 1.00
B
(this PR)

max 1.00

max 1.00

max 1.00

max 1.00
C
max 1.00

max 1.00

max 1.00

max 1.00

The old graph glows once $\xi_s > 1$: the sphere becomes brighter than the environment illuminating it, with pixel values up to about $\xi_s$. A, B and C never exceed the background. As $\xi_s$ grows they tend to a perfect white mirror, which is correctly invisible in a furnace. The differences between A, B and C are confined to a thin band at the silhouette (82° maps to the outer ~1% of the sphere's radius), so they are compared using the Fresnel curves under "Alternatives considered".

Shader ball (gold, roughness 0.2)

Same rows as above.

$\xi_s = 1$ $\xi_s = 2$ $\xi_s = 4$ $\xi_s = 8$
v1.2
graph
A
B
(this PR)
C

Alternatives considered

All three options below are identical for $\xi_s \le 1$; they differ only in the edge tint used for $\xi_s > 1$.

Edge tint for $\xi_s > 1$ Behaviour
A $\mathbf{C}_s = \mathtt{specular\_color}$ (boost $\mathbf{F}_0$ only) Keeps the edge dip, but boosting never brightens the edges. Hits a ceiling below a white mirror for tinted metals. Darker than the previous behaviour, by up to 5–8% in albedo.
B (this PR) $\mathbf{C}_s = \min(k \mathtt{specular\_color}, 1)$ Stays within ~1–3% albedo of the previous behaviour and has the same $\xi_s \to \infty$ limit. The edge tint washes out quickly: it has no effect once $\xi_s \mathtt{specular\_color} \ge 1$. It is somewhat brighter than a pure curve scaling around 82°, because the tint boost compounds with the boosted Schlick curve.
C boost the absolute 82° reflectivity $\mathbf{C}_s \mathbf{F}_\mathrm{Schlick}(\bar\mu)$ by $k$, clamped Most faithful to "scale the curve", and always lies between A and B. It needs extra graph nodes and is nearly identical to B for realistic metals.

Analytic Fresnel curves for single channels, compared with the old $\xi_s \mathbf{F}_{82}$ graph and the v1.2 spec clamp:

🤖 Generated with Claude Code

…ftwareFoundation#314)

Replace clamp(specular_weight * F82, 0, 1) with a formulation expressible
in the MaterialX graph: specular_weight <= 1 scales the whole metal lobe
(unchanged), while specular_weight > 1 boosts the F0 and F82 edge-tint
colors, each clamped to [0, 1]. The result is always a valid F82-tint
Fresnel curve bounded in [0, 1].

The MaterialX graph drives the generalized_schlick_bsdf weight, color0 and
color82 inputs accordingly, and the Emetal coat-darkening estimate now uses
the boosted F0 (which also includes base_weight).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
jamportz added a commit to jamportz/OpenPBR that referenced this pull request Sep 23, 2026
…amp)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@jamportz
jamportz marked this pull request as ready for review September 23, 2026 20:47

@fpsunflower fpsunflower left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@jstone-lucasfilm jstone-lucasfilm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great to me, @jamportz, and clamping before the BSDF evaluation should work well in MaterialX/OSL/MDL.

One minor, non-blocking suggestion for the specification text: the statement that the curve "tends to a perfect (white) mirror as specular_weight -> inf" holds only where the colors are non-zero. Since the boost is multiplicative, a zero channel in specular_color would stay at zero for any specular_weight, so the edge reflectance in that channel would remain pinned at zero.

Perhaps this could be qualified as "tends to a perfect (white) mirror as specular_weight -> inf, for non-zero colors"?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants