Add clamp to prevent negative metallic Fresnel#256
Add clamp to prevent negative metallic Fresnel#256AdrienHerubel merged 5 commits intoAcademySoftwareFoundation:dev_1.2from
Conversation
|
Thanks for adding this @portsmouth ! Sorry I had forgotten about it. This looks good to me. I had to figure out how you derived this version of the hemispherical albedo formula. Starting with a standard integral of the cosine-weighted F82-tint function over the upper hemisphere in polar coordinates, one can simplify away the phi part and then do a change of variables to convert the integration domain from theta to cos(theta) and ultimately arrive at the form shown in your footnote. I don't know if any of that needs to be included in the text (or maybe it's already clear if considering the context), but I wanted to understand it at least. The method of deriving the final closed-form solution is also not obvious, and I believe it involves Beta integrals, but presumably most people wouldn't be solving this by hand. |
|
Script for the plot, for reference: import os, sys, math
import numpy as np
import matplotlib.pyplot as plt
import colorsys
def Fresnel_Schlick(mu, F0):
return F0 + (1.0 - F0)*(1.0 - mu)**5.0
def get_b(F0, F82tint):
mu_bar = 1.0/7.0
denom = mu_bar * (1.0 - mu_bar)**6.0
b = Fresnel_Schlick(mu_bar, F0) * (1.0 - F82tint) / denom
return b
def Fresnel_F82(mu, F0, F82tint):
b = get_b(F0, F82tint)
return Fresnel_Schlick(mu, F0) - b*mu*(1.0 - mu)**6.0
# Compute F82-tint model albedo numerically
def albedo_numerical(F0, F82tint):
Ntheta = 256
Integrand = np.empty(Ntheta)
mu_array = np.linspace(0.0, 1.0, Ntheta)
# evaluate Integrand for theta_i integral
for n_theta_i in range(Ntheta):
mu_i = mu_array[n_theta_i]
F = Fresnel_F82(mu_i, F0, F82tint)
Integrand[n_theta_i] = 2.0 * F * mu_i
# do integral over theta_i
theta_integral = np.trapz(Integrand, mu_array)
return theta_integral
# Analytical albedo (from Wolfram)
def albedo_analytical(F0, F82tint):
b = get_b(F0, F82tint)
return F0 + (1.0 - F0)/21.0 - b/126.0
N_F0s = 8
F0_values = np.linspace(0.0, 1.0, N_F0s)
N_tints_numerical = 16
F82tint_values_numerical = np.linspace(0.0, 1.0, N_tints_numerical)
albedos_numerical = np.empty(N_tints_numerical)
N_tints_analytical = 64
F82tint_values_analytical = np.linspace(0.0, 1.0, N_tints_analytical)
albedos_analytical = np.empty(N_tints_analytical)
for n_f0 in range(0, N_F0s):
F0 = F0_values[n_f0]
print('Running numerics for F0 = %f' % F0)
for n_t in range(0, N_tints_numerical):
F82tint = F82tint_values_numerical[n_t]
albedos_numerical[n_t] = albedo_numerical(F0, F82tint)
for n_t in range(0, N_tints_analytical):
F82tint = F82tint_values_analytical[n_t]
albedos_analytical[n_t] = albedo_analytical(F0, F82tint)
grayscale = F0 / F0_values[-1]
Clin = colorsys.hsv_to_rgb(1, 0.9, grayscale)
plt.plot(F82tint_values_numerical, albedos_numerical, label='F0 = %f' % F0, color=Clin, marker='.', linestyle='none')
plt.plot(F82tint_values_analytical, albedos_analytical, label='', color=Clin, marker='', linewidth=1.0, linestyle='solid')
plt.xlabel (r'$F_{82}$ tint')
plt.ylabel (r'$E_\mathrm{avg}$, average albedo')
plt.show() |
|
I made the clamp explicit in the spec in e0963af (screenshots at the top updated). |
|
The The MaterialX graph cannot implement this currently though (as noted in #240) , since there is no way to implement the weight other than multiplying it into the BSDF, which cannot be clamped. So we need to discuss how/whether to deal with this Fresnel clamping for MaterialX. EDIT: ah no, we did this already, merged into We just need to rebase this onto |
# Conflicts: # index.html
All we can do for now in the graph, is to clamp the This still wouldn't deal with the negative Fresnel though. |
peterkutz
left a comment
There was a problem hiding this comment.
Nice investigation. The spec changes you've proposed look good to me.
It would be nice if we could avoid the clamping entirely. If I remember correctly, the problem is that the original F82 formula lets you specify the y value of the curve at a particular x value (corresponding to about 82 degrees), but that doesn't mean that the minimum y value coincides with that location. Since the overall curve slopes up from left to right, the minimum y value tends to occur at a lower x value.
Clamping both the curve and the average albedo formula seems like a reasonable solution for handling these edge cases for now.
It might be possible to find an exact closed-form solution for the integral of the clamped function, though it seems possible that it would be be significantly more complex and would start to defeat one of the purposes of using this model in the first place: it's relative simplicity and efficiency. Perhaps we could calculate the x intercepts and then subtract off the integral of the part of the curve between them.
AdrienHerubel
left a comment
There was a problem hiding this comment.
There is a consensus that this change fixes unwanted behaviors. This does not require a change in the MaterialX graph and the negative clamp should already be present in the MaterialX implementation.
b3e14b6
into
AcademySoftwareFoundation:dev_1.2





As discussed in #187, it is useful to quote this formula. (For example, we use it in the multiple scattering compensation for the OpenPBR metal lobe in Arnold).
I put it in a new footnote:
Referenced from the metal section (slightly tweaked to define the$b$ term which appears in the albedo formula, which makes for a clearer presentation anyway closer to the ASM document):