Skip to content
Draft
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
22 changes: 16 additions & 6 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1108,12 +1108,20 @@

If the coat is rough, the microfacet BSDF lobes of the underlying base substrate (metal and dielectric) are also effectively roughened. If this is not otherwise accounted for by the light transport, it can instead be reasonably approximated by directly altering the NDF of the base BSDFs.

A formula we recommend for this is obtained by identifying the NDF of each microfacet lobe as corresponding approximately to a Gaussian in slope-space with variance given by $\alpha_t^2 + \alpha_b^2 = r^4$ (in the notation of the [Microfacet model](index.html#model/microfacetmodel) section). Modeling the effect of the roughening as the convolution of these Gaussian NDFs (and double counting the coat variance since the reflection passes through the coat boundary twice), the resulting modified roughness of the base, $r'_\mathrm{B}$, (taking into account the presence weight of the coat, $\mathtt{C}=$ **`coat_weight`**) is given by
We work in GGX $\alpha$-space, where $\alpha = r^2$. Let $\alpha_\mathrm{B} = r_\mathrm{B}^2$ and $\alpha_\mathrm{C} = r_\mathrm{C}^2$ denote the GGX alphas of the base and coat respectively, and let $\eta_\mathrm{ca} = n_c/n_a$ be the IOR of the coat relative to the ambient medium (i.e. **`coat_ior`**). A base lobe with GGX alpha $\alpha_\mathrm{B}$ inside the coat appears broadened to $\eta_\mathrm{ca}\,\alpha_\mathrm{B}$ when viewed from outside, due to Snell's law angular expansion at the exit interface. The coat contributes additional broadening via refraction through its tilted microfacets, with both entry and exit refractions adding variance independently. This motivates an IOR-dependent coat broadening coefficient:
\begin{equation}
r'_\mathrm{B} = \mathrm{lerp}\Bigl( r_\mathrm{B}, \mathrm{min} \bigl(1, r^4_\mathrm{B} + 2 r^4_\mathrm{C} \bigr)^\frac{1}{4}, \mathtt{C} \Bigr)
B(\eta_\mathrm{ca}) = 1.05\;\frac{(\eta_\mathrm{ca}-1)\sqrt{1+\eta_\mathrm{ca}^2}}{\eta_\mathrm{ca}}
\end{equation}
where $r_\mathrm{B}=$ **`specular_roughness`** and $r_\mathrm{C}=$ **`coat_roughness`**.

where the prefactor $1.05$ is fit to Monte Carlo simulations. Adding the base and coat contributions in quadrature gives the raw broadened alpha:
\begin{equation} \label{coat_roughening_heuristic}
\alpha_\mathrm{raw} = \sqrt{\bigl(\eta_\mathrm{ca}\,\alpha_\mathrm{B}\bigr)^2 + \bigl(B(\eta_\mathrm{ca})\,\alpha_\mathrm{C}\bigr)^2}
\end{equation}
At high roughness, total internal reflection at the coat exit prevents steeply angled rays from escaping, imposing an upper limit on the observable lobe width. This is modeled with a smooth $\tanh$ saturation:
\begin{equation}
\alpha_\mathrm{eff} = \alpha_\mathrm{max}\,\tanh\!\left(\frac{\alpha_\mathrm{raw}}{\alpha_\mathrm{max}}\right), \qquad \alpha_\mathrm{max} = 0.85
\end{equation}
The effective roughness is then $r'_\mathrm{B} = \sqrt{\alpha_\mathrm{eff}}$, where $r_\mathrm{B}=$ **`specular_roughness`** and $r_\mathrm{C}=$ **`coat_roughness`**.
The presence weight of the coat ($\mathtt{C}=$ **`coat_weight`**) is accounted for by blending: $\mathrm{lerp}(r_\mathrm{B},\, r'_\mathrm{B},\, \mathtt{C})$.


### Total internal reflection
Expand Down Expand Up @@ -1180,9 +1188,9 @@

The form of this model is the following (with $\mu_i, \mu_o$ the angle cosines to the normal of $\omega_i, \omega_o$):
\begin{equation}
\mu_i \, f_\mathrm{fuzz}(\omega_i, \omega_o) = \mathbf{F} \, E_\mathrm{fuzz}(\mu_o, \alpha) \, D(\mu_i | \mu_o, \alpha)
\mu_i \, f_\mathrm{fuzz}(\omega_i, \omega_o) = \mathbf{F} \, E_\mathrm{fuzz}(\mu_o, r_F) \, D(\mu_i | \mu_o, r_F)
\end{equation}
where $\mathbf{F}$ = **`fuzz_color`**, $E_\mathrm{fuzz}(\mu_o, \alpha)$ (termed $R$ in [#Zeltner2022]) is the reflectance at angle cosine $\mu_o$ given roughness $\alpha$ = **`fuzz_roughness`** $\in [0,1]$, and $D(\mu_i | \mu_o, \alpha)$ is a lobe defined by linear transformations of a cosine lobe (LTCs), where the transformation matrices (and $E_\mathrm{fuzz}$) are tabulated in a grid in the $(\mu_o, \alpha)$ plane, with values fitted to a simulation of the scattering in the volumetric fuzz microflake layer. Since the LTC lobe $D$ is a normalized PDF over the hemisphere, the resulting albedo of $f_\mathrm{fuzz}$ is $\mathbf{F} \, E_\mathrm{fuzz}(\mu_o, \alpha)$.
where $\mathbf{F}$ = **`fuzz_color`**, $E_\mathrm{fuzz}(\mu_o, r_F)$ (termed $R$ in [#Zeltner2022]) is the reflectance at angle cosine $\mu_o$ given roughness $r_F$ = **`fuzz_roughness`** $\in [0,1]$ (termed $\alpha$ in [#Zeltner2022]), and $D(\mu_i | \mu_o, r_F)$ is a lobe defined by linear transformations of a cosine lobe (LTCs), where the transformation matrices (and $E_\mathrm{fuzz}$) are tabulated in a grid in the $(\mu_o, r_F)$ plane, with values fitted to a simulation of the scattering in the volumetric fuzz microflake layer. Since the LTC lobe $D$ is a normalized PDF over the hemisphere, the resulting albedo of $f_\mathrm{fuzz}$ is $\mathbf{F} \, E_\mathrm{fuzz}(\mu_o, r_F)$.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I assume you're using different variables than the Zeltner paper to be consistent with other formulas in the OpenPBR spec. Is that right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, since $\alpha$ means roughness squared in the microfacet case, so it could be a bit confusing to use the same symbol here.


If using the albedo-scaling interpretation of layering, a reasonable approximation of the reflection from the fuzz layer combined with the reflection from the base is to take
\begin{eqnarray}
Expand All @@ -1196,6 +1204,8 @@

The fuzz shading normal is assumed to inherit from that of the substrate layer, the physical picture being that the fuzz volume settles and conforms to the geometry of the substrate. The substrate is generally a mixture of coat and uncoated base. Thus physically the fuzz model should be evaluated with each of the **`geometry_coat_normal`** and **`geometry_normal`** separately (if they differ), and the final result blended according to the **`coat_weight`**. As a practical approximation, it may be more convenient and efficient to instead approximate the fuzz normal by interpolating the coat and base normal according to **`coat_weight`**.

Unlike the coat, the fuzz layer is index-matched with the ambient medium (IOR $n_a$), so refraction does not alter the direction of unscattered rays passing through it. Consequently, *we recommend that the base lobe should not be roughened by the fuzz layer.* The base lobe retains its original angular distribution, attenuated only by the extinction through the fuzz volume. Any apparent broadening of the overall reflected lobe arises from the scattered fuzz contribution sitting alongside the unmodified base lobe, not from widening the base lobe itself.


Fuzz params | Label | Type | Range | Default | Description
---------------------|-----------|----------|:------------:|:-------------:|----------------------------------------------
Expand Down
111 changes: 90 additions & 21 deletions reference/open_pbr_surface.mtlx
Original file line number Diff line number Diff line change
Expand Up @@ -266,31 +266,100 @@
<input name="high" type="float" value="1.0" />
</clamp>

<!-- Roughening due to coat-->
<power name="coat_roughness_to_power_4" type="float">
<!-- Roughening due to coat (physics-based formula, see spec Roughening section) -->
<!-- Convert roughness to GGX alpha: alpha = roughness^2 -->
<multiply name="coat_alpha" type="float">
<input name="in1" type="float" nodename="coat_roughness_clamped" />
<input name="in2" type="float" value="4.0" />
</power>
<multiply name="two_times_coat_roughness_to_power_4" type="float">
<input name="in1" type="float" nodename="coat_roughness_to_power_4" />
<input name="in2" type="float" value="2.0" />
<input name="in2" type="float" nodename="coat_roughness_clamped" />
</multiply>
<power name="specular_roughness_to_power_4" type="float">
<multiply name="specular_alpha" type="float">
<input name="in1" type="float" nodename="specular_roughness_clamped" />
<input name="in2" type="float" value="4.0" />
</power>
<add name="add_coat_and_spec_roughnesses_to_power_4" type="float">
<input name="in1" type="float" nodename="two_times_coat_roughness_to_power_4" />
<input name="in2" type="float" nodename="specular_roughness_to_power_4" />
</add>
<min name="min_1_add_coat_and_spec_roughnesses_to_power_4" type="float">
<input name="in2" type="float" nodename="specular_roughness_clamped" />
</multiply>
<!-- B(eta) = 1.05 * (eta-1) * sqrt(1 + eta^2) / eta -->
<subtract name="roughening_eta_minus_1" type="float">
<input name="in1" type="float" nodename="coat_ior_nonnegative" />
<input name="in2" type="float" value="1.0" />
</subtract>
<multiply name="roughening_eta_sqr" type="float">
<input name="in1" type="float" nodename="coat_ior_nonnegative" />
<input name="in2" type="float" nodename="coat_ior_nonnegative" />
</multiply>
<add name="roughening_one_plus_eta_sqr" type="float">
<input name="in1" type="float" value="1.0" />
<input name="in2" type="float" nodename="add_coat_and_spec_roughnesses_to_power_4" />
</min>
<power name="coat_affected_specular_roughness" type="float">
<input name="in1" type="float" nodename="min_1_add_coat_and_spec_roughnesses_to_power_4" />
<input name="in2" type="float" value="0.25" />
</power>
<input name="in2" type="float" nodename="roughening_eta_sqr" />
</add>
<sqrt name="roughening_sqrt_one_plus_eta_sqr" type="float">
<input name="in" type="float" nodename="roughening_one_plus_eta_sqr" />
</sqrt>
<multiply name="roughening_eta_minus_1_times_sqrt" type="float">
<input name="in1" type="float" nodename="roughening_eta_minus_1" />
<input name="in2" type="float" nodename="roughening_sqrt_one_plus_eta_sqr" />
</multiply>
<divide name="roughening_B_eta_unscaled" type="float">
<input name="in1" type="float" nodename="roughening_eta_minus_1_times_sqrt" />
<input name="in2" type="float" nodename="coat_ior_nonnegative" />
</divide>
<multiply name="roughening_B_eta" type="float">
<input name="in1" type="float" nodename="roughening_B_eta_unscaled" />
<input name="in2" type="float" value="1.05" />
</multiply>
<!-- alpha_raw^2 = (eta * alpha_b)^2 + (B * alpha_c)^2 -->
<multiply name="roughening_eta_alpha_b" type="float">
<input name="in1" type="float" nodename="coat_ior_nonnegative" />
<input name="in2" type="float" nodename="specular_alpha" />
</multiply>
<multiply name="roughening_eta_alpha_b_sqr" type="float">
<input name="in1" type="float" nodename="roughening_eta_alpha_b" />
<input name="in2" type="float" nodename="roughening_eta_alpha_b" />
</multiply>
<multiply name="roughening_B_alpha_c" type="float">
<input name="in1" type="float" nodename="roughening_B_eta" />
<input name="in2" type="float" nodename="coat_alpha" />
</multiply>
<multiply name="roughening_B_alpha_c_sqr" type="float">
<input name="in1" type="float" nodename="roughening_B_alpha_c" />
<input name="in2" type="float" nodename="roughening_B_alpha_c" />
</multiply>
<add name="roughening_alpha_raw_sqr" type="float">
<input name="in1" type="float" nodename="roughening_eta_alpha_b_sqr" />
<input name="in2" type="float" nodename="roughening_B_alpha_c_sqr" />
</add>
<sqrt name="roughening_alpha_raw" type="float">
<input name="in" type="float" nodename="roughening_alpha_raw_sqr" />
</sqrt>
<!-- alpha_eff = 0.85 * tanh(alpha_raw / 0.85), via tanh(x) = (exp(2x)-1)/(exp(2x)+1) -->
<divide name="roughening_x" type="float">
<input name="in1" type="float" nodename="roughening_alpha_raw" />
<input name="in2" type="float" value="0.85" />
</divide>
<multiply name="roughening_two_x" type="float">
<input name="in1" type="float" nodename="roughening_x" />
<input name="in2" type="float" value="2.0" />
</multiply>
<exp name="roughening_e2x" type="float">
<input name="in" type="float" nodename="roughening_two_x" />
</exp>
<subtract name="roughening_e2x_minus_1" type="float">
<input name="in1" type="float" nodename="roughening_e2x" />
<input name="in2" type="float" value="1.0" />
</subtract>
<add name="roughening_e2x_plus_1" type="float">
<input name="in1" type="float" nodename="roughening_e2x" />
<input name="in2" type="float" value="1.0" />
</add>
<divide name="roughening_tanh" type="float">
<input name="in1" type="float" nodename="roughening_e2x_minus_1" />
<input name="in2" type="float" nodename="roughening_e2x_plus_1" />
</divide>
<multiply name="roughening_alpha_eff" type="float">
<input name="in1" type="float" value="0.85" />
<input name="in2" type="float" nodename="roughening_tanh" />
</multiply>
<!-- r_eff = sqrt(alpha_eff) -->
<sqrt name="coat_affected_specular_roughness" type="float">
<input name="in" type="float" nodename="roughening_alpha_eff" />
</sqrt>
<mix name="effective_specular_roughness" type="float">
<input name="fg" type="float" nodename="coat_affected_specular_roughness" />
<input name="bg" type="float" nodename="specular_roughness_clamped" />
Expand Down