Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 34 additions & 20 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -946,29 +946,33 @@

![Figure [coat_darkening_grid]: Diffuse base with a clear-coat, for coats of different relative IOR $\eta_c$](images/coat_darkening_grid.png width="60%")

However this darkening may not always be desirable artistically, as in some applications it is beneficial for the observed color of the coated color to "match" the input base color (in a sense defined more precisely below in equation [undarkened_coat_albedo]). We allow for this by introducing a **`coat_darkening`** parameter, $\delta$. By default $\delta$ = $1$, in which case the physically correct darkening effect due to internal reflections occurs as normal. In the case $\delta$ = $0$ however, the base albedo is instead _boosted_ uniformly by just enough to counteract the darkening effect. The boost factor is reduced to 1 linearly as $\delta \rightarrow 1$.
However this darkening may not always be desirable artistically, as in some applications it is beneficial for the observed color of the coated base to "match" the input base color (in a sense defined more precisely below in equation [undarkened_coat_albedo]). We allow for this by introducing a **`coat_darkening`** parameter, $\delta$. By default $\delta$ = $1$, in which case the physically correct darkening effect due to internal reflections occurs as normal. In the case $\delta$ = $0$ however, the base albedo is modified to "counteract" the darkening effect. To describe what we mean by "counteract the darkening", we write the (physically darkened) coat albedo at normal incidence in the general form
\begin{equation} \label{general_coat_albedo}
\mathbf{E}_c = F_0 + (1 - F_0) \mathbf{E}^\prime_c \ .
\end{equation}
where $F_0$ is the normal incidence Fresnel factor of the coat, and $\mathbf{E}^\prime_c$ represents the albedo due to transmission into the coat medium (and scattering off the base substrate, potentially multiple internal reflections off the coat interface, and re-transmission back out).
We then require that the *effect* of $\delta$ = **`coat_darkening`** is to multiply $\mathbf{E}^\prime_c$ by an RGB boost factor
\begin{equation} \label{undarkened_coat_albedo_scaling}
\mathbf{B}(\delta) = \mathrm{lerp}\biggl(\frac{\mathbf{T}_\mathrm{coat}^2 \mathbf{E}_b}{\mathbf{E}^\prime_c}, 1, \delta \biggr)
\end{equation}
where $\mathbf{T}_\mathrm{coat}$ is the coat absorption transmittance, and $\mathbf{E}_b$ represents the normal-incidence albedo of the entire base beneath the coat (which can be approximated as the normal-incidence albedos of the individual slabs of the base, blended according to their mix weights).

What we mean by "counteract the darkening" is defined as the albedo of the coated base viewed at normal incidence, $E_c$, being equal to the "un-darkened" albedo
This is a straightforward implementation of the requirement for the albedo of the "un-darkened" coat to be equal to the usual albedo scaling formula of equation [non-reciprocal-albedo-scaling-with-T] (which involves no color shift other than that due to the absorption, and the combination with the Fresnel factor), i.e.
\begin{equation} \label{undarkened_coat_albedo}
E_c = F_0 + E_b (1 - F_0) = \mathrm{lerp}(F_0, 1, E_b)
\end{equation}
where $E_b$ is the base albedo at normal incidence, which is a straightforward interpretation of the requirement for the observed color of the coated base to match the uncoated base, taking into account the presence of the Fresnel effect of the coat. Due to the physical effect of the darkening due to the internal reflections, the base BSDF would generally need to be boosted by some compensating uniform "boost" factor $B_0$ in order for the coated base to achieve this un-darkened albedo. The effect of $\delta$ = **`coat_darkening`** is then defined to be the following modulation of the applied boost factor:
\begin{equation} \label{boost_factor}
B(\delta) = \mathrm{lerp}(B_0, 1, \delta) \ .
\mathbf{E}_c = F_0 + (1 - F_0) \mathbf{E}^\prime_c \mathbf{B}(\delta) \; \rightarrow \; F_0 + \mathbf{T}^2_\mathrm{coat} \, \mathbf{E}_b (1 - F_0)
\end{equation}
While this defines the behavior at a physical level, in practice implementations will need to develop some specific approximation for the coat darkening effect.
While this can be implementation dependent, we suggest here a reasonably simple, efficient scheme which captures the essential behavior.
as $\delta \rightarrow 0$.

It can be shown that in the case of a Lambertian base with (constant) albedo $E_b$ and a perfectly smooth dielectric clear-coat -- the so-called _interfaced Lambertian_ model ([#Elias2001], [#d'Eon2021]) -- that the exact directional albedo of the coated base has the albedo-scaling-like form (where $\eta_c$ is the ratio of the coat IOR to the ambient IOR, and $F$ is the corresponding coat Fresnel factor):
Note that we have not specified the detailed physical mechanism by which the boosting of $\mathbf{E}^\prime_c$ occurs, instead we have only defined what the resultant effective albedo modification has to be as **`coat_darkening`** is varied. While this defines the behavior at the level of the required albedos, in practice implementations will need to develop some specific approximation for the coat darkening effect consistent with their physical approximations. Due to the physical effect of the darkening due to the internal reflections, the base BSDF would generally need to be altered in order for the coated base to achieve the un-darkened albedo. While this can be implementation dependent, we suggest here a reasonably simple, efficient scheme which captures the essential behavior.

Physically, it can be shown that in the case of a Lambertian base with (constant) albedo $E_b$ and a perfectly smooth dielectric clear-coat -- the so-called _interfaced Lambertian_ model ([#Elias2001], [#d'Eon2021]) -- that the exact directional albedo of the coated base has the albedo-scaling-like form (where $\eta_c$ is the ratio of the coat IOR to the ambient IOR, and $F$ is the corresponding coat Fresnel factor):
\begin{equation} \label{interfaced_lambertian_albedo}
E_c(\omega_o) = F(\omega_o, \eta_c) + E_b \Bigl(1 - F(\omega_o, \eta_c)\Bigr) \Delta(E_b, \eta_c)
\mathbf{E}_c(\omega_o) = F(\omega_o, \eta_c) + \mathbf{E}_b \Bigl(1 - F(\omega_o, \eta_c)\Bigr) \mathbf{\Delta}(\mathbf{E}_b, \eta_c)
\end{equation}
where $\Delta(E_b, \eta_c)$ is a darkening factor given by
where $\mathbf{\Delta}(\mathbf{E}_b, \eta_c)$ is a darkening factor given by
\begin{equation} \label{general_darkening_formula}
\Delta(E_b, \eta_c) = \frac{1 - K}{1 - E_b K} \ .
\mathbf{\Delta}(\mathbf{E}_b, \eta_c) = \frac{1 - K}{1 - K \mathbf{E}_b} \ .
\end{equation}
We can thus approximate the required boost factor as the reciprocal of this, i.e. $B_0 \approx \Delta^{-1}$. Note that as $E_b \rightarrow 1$, the darkening factor $\Delta \rightarrow 1$, as required since physically no energy is lost if the base reflects all incident energy (in the clear-coat case). If the base albedo $E_b$ is boosted by $B(\delta)$, then clearly at $\delta=0$ the darkening factor cancels and equation [undarkened_coat_albedo] is satisfied as required.

Here $K \in [0,1]$ is the _internal diffuse reflection coefficient_, which corresponds to the fraction of the energy leaving the base which returns
to the base due to internal reflection from the coat. Higher values of $K$ lead to more internal reflections, thus more darkening and saturation. In the case of a Lambertian base (which should be a reasonable approximation to the rough metal, dielectric, or diffuse cases), $K = K_r$ where, with relative coat IOR $\eta_c$:
\begin{equation} \label{internal_diffuse_reflection_coefficient_for_rough_base}
Expand All @@ -979,7 +983,6 @@
K_s = F(\omega_o, \eta_c) \ ,
\end{equation}
which becomes exact in the limit of a perfectly smooth metallic or dielectric base. There is significantly less darkening in the case of a smooth base, as the specular reflection from the base is less likely to generate total internal reflection at the coat boundary.

We thus recommend in the general case that the darkening factor of equation [general_darkening_formula] be applied, but taking
\begin{equation} \label{internal_diffuse_reflection_coefficient_for_general_base}
K = \mathrm{lerp}(K_s, K_r, r_b)
Expand All @@ -994,13 +997,24 @@
\end{equation}
while the metallic roughness can be taken to be $r_m = r$. (Note that in this formula for $r_d$, a clamp must be applied to ensure that $\xi_s F_s \in [0, 1]$).

Given the general formula equation [general_darkening_formula] for the darkening, a reasonable approximate scheme -- assuming no other compensation is made to approximate the effect -- is to multiply the base BSDF by the uniform _modulated darkening factor_ (taking into account the presence weight $\mathtt{C}$ = **`coat_weight`** and the darkening parameter $\delta$ = **`coat_darkening`**):
Incorporating the absorption coefficient is more complicated, but a rough approximation at normal incidence is to replace
\begin{equation} \label{general_darkening_formula_with_absorption}
\mathbf{\Delta}(\mathbf{E}_b, \eta_c) \approx \frac{1 - K}{1 - K \mathbf{E}_b \mathbf{T}^2_\mathrm{coat}}
\end{equation}
and equation [interfaced_lambertian_albedo] is modified with an additional $\mathbf{T}^2_\mathrm{coat}$ factor in the second term.
We can thus approximate $\mathbf{E}^\prime_c \approx \mathbf{E}_b \mathbf{T}^2_\mathrm{coat} \mathbf{\Delta}$. The boost factor of equation [undarkened_coat_albedo_scaling] then reduces to:
\begin{equation} \label{B_approx}
\mathbf{B}(\delta) = \mathrm{lerp}\biggl(\frac{1}{\mathbf{\Delta}}, 1, \delta\biggr) \ .
\end{equation}

Given the general formula equation [general_darkening_formula_with_absorption] for the darkening, a reasonable approximate scheme -- assuming no other compensation is made to approximate the effect -- is to multiply the base BSDF by the uniform _modulated darkening factor_ (depending on the darkening parameter $\delta$ = **`coat_darkening`**):
\begin{equation} \label{modulated_darkening_factor}
(1-\mathtt{C}) + \mathtt{C} \, B(\delta) \,\Delta = \mathrm{lerp}(1, \Delta, \mathtt{C}\,\delta) \ ,
\mathbf{B}(\delta) \,\mathbf{\Delta} \mathbf{T}^2_\mathrm{coat} = \mathrm{lerp}\left(1, \mathbf{\Delta}, \delta\right) \,\mathbf{T}^2_\mathrm{coat}
\end{equation}
with $\Delta$ evaluated via equation [general_darkening_formula] with internal diffuse reflection coefficient, accounting for base roughness, given by equation [internal_diffuse_reflection_coefficient_for_general_base].
with $\mathbf{\Delta}$ evaluated via equation [general_darkening_formula_with_absorption] with internal diffuse reflection coefficient, accounting for base roughness, given by equation [internal_diffuse_reflection_coefficient_for_general_base].

In the absense of the coat the physical darkening does not occur, which may be modelled by having the base darkening factor be $\mathrm{lerp}(1, \mathbf{B}(\delta) \,\mathbf{\Delta} \mathbf{T}^2_\mathrm{coat}, \mathtt{C})$ where $\mathtt{C}$ = **`coat_weight`** is the presence weight of the coat.

The base albedo $E_b$ which appears in equation [general_darkening_formula] represents the normal-incidence albedo of the entire base beneath the coat. This albedo can be approximated as the normal-incidence albedos of the individual slabs of the base, blended according to their mix weights.


### View-dependent absorption
Expand Down
58 changes: 33 additions & 25 deletions reference/open_pbr_surface.mtlx
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,9 @@
<input name="mix" type="float" interfacename="base_metalness" />
</mix>

<!-- Coat darkening calculation -->
<!-- Coat Layer -->

<!-- Coat inter-reflection darkening darkening calculation -->
<!-- approximate Kcoat, "internal diffuse reflection coefficient" of coat -->
<subtract name="one_minus_coat_F0" type="float">
<input name="in1" type="float" value="1.0" />
Expand Down Expand Up @@ -494,69 +496,75 @@
<input name="bg" type="color3" nodename="Edielectric" />
<input name="mix" type="float" interfacename="base_metalness" />
</mix>
<!-- final base darkening factor due to coat: base_darkening = (1 - Kcoat) / (1 - Ebase*Kcoat) -->
<!-- final base inter-reflection darkening darkening factor due to coat,
where Tcoat2 = coat_color:
base_darkening = (1 - Kcoat) / (1 - Ebase*Kcoat*Tcoat2) -->
<multiply name="Ebase_Kcoat" type="color3">
<input name="in1" type="color3" nodename="Ebase" />
<input name="in2" type="float" nodename="Kcoat" />
</multiply>
<multiply name="Ebase_Kcoat_Tcoat2" type="color3">
<input name="in1" type="color3" nodename="Ebase_Kcoat" />
<input name="in2" type="color3" interfacename="coat_color" />
</multiply>
<subtract name="one_minus_Kcoat" type="float">
<input name="in1" type="float" value="1.0" />
<input name="in2" type="float" nodename="Kcoat" />
</subtract>
<subtract name="one_minus_Ebase_Kcoat" type="color3">
<subtract name="one_minus_Ebase_Kcoat_Tcoat2" type="color3">
<input name="in1" type="color3" value="1.0, 1.0, 1.0" />
<input name="in2" type="color3" nodename="Ebase_Kcoat" />
<input name="in2" type="color3" nodename="Ebase_Kcoat_Tcoat2" />
</subtract>
<convert name="one_minus_Kcoat_color" type="color3">
<input name="in" type="float" nodename="one_minus_Kcoat" />
</convert>
<divide name="base_darkening" type="color3">
<input name="in1" type="color3" nodename="one_minus_Kcoat_color" />
<input name="in2" type="color3" nodename="one_minus_Ebase_Kcoat" />
<input name="in2" type="color3" nodename="one_minus_Ebase_Kcoat_Tcoat2" />
</divide>
<multiply name="coat_weight_times_coat_darkening" type="float">
<input name="in1" type="float" interfacename="coat_weight" />
<input name="in2" type="float" interfacename="coat_darkening" />
</multiply>

<!-- inter-reflection darkening, modulated by coat_darkening -->
<mix name="modulated_base_darkening" type="color3">
<input name="fg" type="color3" nodename="base_darkening" />
<input name="bg" type="color3" value="1.0, 1.0, 1.0" />
<input name="mix" type="float" nodename="coat_weight_times_coat_darkening" />
<input name="mix" type="float" interfacename="coat_darkening" />
</mix>
<multiply name="darkened_base_substrate" type="BSDF">
<input name="in1" type="BSDF" nodename="base_substrate" />
<input name="in2" type="color3" nodename="modulated_base_darkening" />
</multiply>

<!-- Coat Layer -->
<mix name="coat_attenuation" type="color3">
<input name="fg" type="color3" interfacename="coat_color" />
<input name="bg" type="color3" value="1.0, 1.0, 1.0" />
<input name="mix" type="float" interfacename="coat_weight" />
</mix>
<multiply name="coat_substrate_attenuated" type="BSDF">
<!-- base_substrate, attenuated due to absorption color, and (optional) inter-reflection darkening -->
<multiply name="base_substrate_attenuated" type="BSDF">
<input name="in1" type="BSDF" nodename="darkened_base_substrate" />
<input name="in2" type="color3" nodename="coat_attenuation" />
<input name="in2" type="color3" nodename="coat_color" />
</multiply>

<!-- coat BSDF -->
<open_pbr_anisotropy name="coat_roughness_vector" type="vector2">
<input name="roughness" type="float" interfacename="coat_roughness" />
<input name="anisotropy" type="float" interfacename="coat_roughness_anisotropy" />
</open_pbr_anisotropy>
<dielectric_bsdf name="coat_bsdf" type="BSDF">
<input name="weight" type="float" interfacename="coat_weight" />
<input name="weight" type="float" value="1.0" />
<input name="ior" type="float" interfacename="coat_ior" />
<input name="roughness" type="vector2" nodename="coat_roughness_vector" />
<input name="normal" type="vector3" interfacename="geometry_coat_normal" />
<input name="tangent" type="vector3" interfacename="geometry_coat_tangent" />
<input name="scatter_mode" type="string" value="R" />
</dielectric_bsdf>
<layer name="coat_layer" type="BSDF">
<!-- coated base -->
<layer name="coated_base_substrate" type="BSDF">
<input name="top" type="BSDF" nodename="coat_bsdf" />
<input name="base" type="BSDF" nodename="coat_substrate_attenuated" />
<input name="base" type="BSDF" nodename="base_substrate_attenuated" />
</layer>
<!-- statistical mix of coated and uncoated base -->
<mix name="partially_coated_base" type="color3">
<input name="fg" type="BSDF" nodename="coated_base_substrate" />
<input name="bg" type="BSDF" nodename="base_substrate" />
<input name="mix" type="float" interfacename="coat_weight" />
</mix>


<!-- Fuzz Layer -->
<!-- Fuzz Layer -->
<sheen_bsdf name="fuzz_bsdf" type="BSDF">
<input name="weight" type="float" interfacename="fuzz_weight" />
<input name="color" type="color3" interfacename="fuzz_color" />
Expand All @@ -566,7 +574,7 @@
</sheen_bsdf>
<layer name="fuzz_layer" type="BSDF">
<input name="top" type="BSDF" nodename="fuzz_bsdf" />
<input name="base" type="BSDF" nodename="coat_layer" />
<input name="base" type="BSDF" nodename="partially_coated_base" />
</layer>

<!-- Emission Layer -->
Expand Down