OpenPBR icon indicating copy to clipboard operation
OpenPBR copied to clipboard

Clarify the coat absorption behaviour for partial coverage

Open virtualzavie opened this issue 9 months ago • 3 comments

While working on an implementation of OpenPBR, I’ve noticed an ambiguity in how coat_weight affects coat absorption.

In summary, the question is whether coat_weight should be treated as partial coverage of a full-thickness coat (applying full coat absorption first then interpolating, i.e. “exponent → lerp”) or as a variable thickness applied uniformly (interpolating the coat_color first then applying the exponential absorption, i.e. “lerp → exponent”).

This distinction affects the resulting colour, in particular hue, especially for mid-range values of coat_weight that are neither fully off (0) nor fully on (1). The current specification does not clarify the exact behaviour (an explicit equation would be welcome), and each interpretation has valid arguments in its favor. The wording ("Coverage weight of coat slab", "presence weight of the coat") suggests “exponent → lerp”, but both Adobe and Autodesk implementations I’ve tested give results consistent with “lerp → exponent” with a distinctive hue shift.

Fraction of coverage (exponent → lerp)

This interpretation treats coat_weight as a fraction of a fully dense, full-thickness coat layer. The model applies the absorption exponent to the coat_color first, then interpolates based on coat_weight between that absorption and the absence of absorption (i.e. white).

From an artist's perspective:

  • Because the absorption is fully computed first, then simply blended, it is easier to anticipate how adjusting coat_weight will affect the final colour.
  • There's a more direct link between the input coat_color and the result: the resulting hue remains closely tied to the given coat_color when using partial weights, making it easier to obtain a target result.
  • Since both reflection intensity and absorption scale consistently with coat_weight, the relationship between them can feel more intuitive.
  • It is not the behaviour they are used to in Subtance and Arnold.

Image

Fraction of thickness (lerp → exponent)

This interpretation treats coat_weight as partial thickness of the coat layer. The model interpolates based on coat_weight between an absence of absorption (i.e. white) and the given coat_color first, then applies the exponential absorption of that colour.

Partial weights lead to a “physically expected” hue shift. As you reduce thickness, the hue changes due to less absorption. For example the resulting appearance of a coat with an orange coat_color will shift toward pink or an eggshell tint at intermediate weights. This aligns with the behaviour I have observed in Substance and Arnold.

From an artist's perspective:

  • This hue shift can be unintuitive, and achieving a desired result can be more difficult since there is no direct link between the coat_color and the obtained result.
  • This hue shift can be desirable if the intent is to represent wear or brush strokes, as it more closely physically models such cases.
  • The coat reflection intensity decreases linearly with the coat_weight, but the absorption color shift does not.
  • In fact, if we treated the coat_weight as a shorthand for thickness, we should do so for coat reflectivity as well, which means it could only be "on" or "off".

Image

Current behaviour in existing implementations

Here are some comparisons. Note how despite the differences in the models, all three show this egg-shell hue shift with partial coverage. Also note how the tinting effect drops more rapidly than the reflectivity when coat_weight reduces.

Coating effect in Substance with Adobe Standard Material: Image

Coating effect in Arnold with Standard Surface: Image

Coating effect in Arnold with OpenPBR: Image

Conclusion

The fact that both Substance and Arnold implement a similar behaviour is a strong argument in favour of keeping and formalising that behaviour. However, what matters for artists is predictability and direct control, and it might be easier for them to manually add a missing hue shift than to correct an unexpected or unwanted one. Eitherway, the specification should be explicit about the expected effect.

virtualzavie avatar Mar 19 '25 18:03 virtualzavie

In summary, the question is whether coat_weight should be treated as partial coverage of a full-thickness coat (applying full coat absorption first then interpolating, i.e. “exponent → lerp”) or as a variable thickness applied uniformly (interpolating the coat_color first then applying the exponential absorption, i.e. “lerp → exponent”).

To clarify, it should certainly be the former, i.e. partial coverage of the full coat, as we say explicitly that coat_weight is such a partial presence weight.

The coat is applied on top of the base substrate, with a coverage weight C coat_weight as follows:

It seems unambiguous (the spec defines what a coverage weight means earlier, as a statistical mix of covered and non-covered). So I disagree with the statement that the spec "does not clarify the exact behaviour".

Arnold implements it as this partial coverage, and generates the "eggshell" shading you showed. Is it not just a gradual mixing of the coat color with white (with any apparent non-linearity due to perception)?

portsmouth avatar May 12 '25 20:05 portsmouth

It's possible that I misunderstood something. At this point maybe it's simpler to discuss over a piece of code. I'll try to put together an minimal example.

virtualzavie avatar May 15 '25 18:05 virtualzavie

Maybe it's helpful to point out what the spec says explicitly.

We say that the coated base has the structure of a layer operation (of coat slab on base) with a "presence weight":

M_\textrm{coated-base}   = \mathrm{\mathbf{layer}}(M_\textrm{base-substrate} , S_\textrm{coat},              \mathtt{C})      

where the presence weight of the coat slab is $\mathtt{C}$=coat_weight. The weight has nothing to do with the absorption properties of the coat, it only functions as this presence weight.

Earlier, we defined explicitly that the meaning of this presence weight is:

\mathrm{\mathbf{layer}}(S_\mathrm{sub}, S_\mathrm{coat}, w_\mathrm{coat}) = \mathrm{\mathbf{mix}}(S_\mathrm{sub}, \mathrm{\mathbf{layer}}(S_\mathrm{sub}, S_\mathrm{coat}), w_\mathrm{coat}) \ .

i.e. it means a (statistical) mix of the structures where the coat slab is either present or not present. There is no room for ambiguity about the physical meaning of the coat_weight then, unless you think this description/formalism itself is somehow incoherent.

So in principle what one does is compute the entire BSDF with the coat, and the entire BSDF without the coat, and blend the two with the coat_weight.

In practice, in albedo-scaling approximation, the effect of the coat boils down to a formula like this (which -- the formula boxed in red -- is exactly what Standard Surface does for example) -- basically blending the BSDFs of the situation with and without the coat:

Image

A somewhat crude approximation, since the BSDF of a coated base is not in reality simply a weighted blend of the separate base and coat BSDFS, as we know (e.g. the coat modifies the effective base roughness, there are effects due to internal reflections, etc.), but a useful one.

portsmouth avatar May 15 '25 19:05 portsmouth