Cycles: Add thin film iridescence to Principled BSDF #118477

Merged
Lukas Stockner merged 18 commits from LukasStockner/blender:iridescence into main 2024-05-02 14:28:57 +02:00
15 changed files with 316 additions and 62 deletions

View File

@ -166,13 +166,13 @@ ccl_device_inline int bsdf_sample(KernelGlobals kg,
case CLOSURE_BSDF_MICROFACET_GGX_REFRACTION_ID:
case CLOSURE_BSDF_MICROFACET_GGX_GLASS_ID:
label = bsdf_microfacet_ggx_sample(
sc, Ng, sd->wi, rand, eval, wo, pdf, sampled_roughness, eta);
kg, sc, Ng, sd->wi, rand, eval, wo, pdf, sampled_roughness, eta);
break;
case CLOSURE_BSDF_MICROFACET_BECKMANN_ID:
case CLOSURE_BSDF_MICROFACET_BECKMANN_REFRACTION_ID:
case CLOSURE_BSDF_MICROFACET_BECKMANN_GLASS_ID:
label = bsdf_microfacet_beckmann_sample(
sc, Ng, sd->wi, rand, eval, wo, pdf, sampled_roughness, eta);
kg, sc, Ng, sd->wi, rand, eval, wo, pdf, sampled_roughness, eta);
break;
case CLOSURE_BSDF_ASHIKHMIN_SHIRLEY_ID:
label = bsdf_ashikhmin_shirley_sample(
@ -506,12 +506,12 @@ ccl_device_inline
/* For consistency with eval() this should be using sd->Ng, but that causes
* artifacts (see shadow_terminator_metal test). Needs deeper investigation
* for how to solve this. */
eval = bsdf_microfacet_ggx_eval(sc, sd->N, sd->wi, wo, pdf);
eval = bsdf_microfacet_ggx_eval(kg, sc, sd->N, sd->wi, wo, pdf);
break;
case CLOSURE_BSDF_MICROFACET_BECKMANN_ID:
case CLOSURE_BSDF_MICROFACET_BECKMANN_REFRACTION_ID:
case CLOSURE_BSDF_MICROFACET_BECKMANN_GLASS_ID:
eval = bsdf_microfacet_beckmann_eval(sc, sd->N, sd->wi, wo, pdf);
eval = bsdf_microfacet_beckmann_eval(kg, sc, sd->N, sd->wi, wo, pdf);
break;
case CLOSURE_BSDF_ASHIKHMIN_SHIRLEY_ID:
eval = bsdf_ashikhmin_shirley_eval(sc, sd->N, sd->wi, wo, pdf);

View File

@ -29,7 +29,14 @@ enum MicrofacetFresnel {
F82_TINT,
};
typedef struct FresnelThinFilm {
float thickness;
float ior;
} FresnelThinFilm;
typedef struct FresnelDielectricTint {
FresnelThinFilm thin_film;
Spectrum reflection_tint;
Spectrum transmission_tint;
} FresnelDielectricTint;
@ -39,6 +46,8 @@ typedef struct FresnelConductor {
} FresnelConductor;
typedef struct FresnelGeneralizedSchlick {
FresnelThinFilm thin_film;
Spectrum reflection_tint;
Spectrum transmission_tint;
/* Reflectivity at perpendicular (F0) and glancing (F90) angles. */
@ -217,7 +226,8 @@ ccl_device_forceinline float3 microfacet_ggx_sample_vndf(const float3 wi,
* the incoming angle `cos_theta_i`.
* Also returns the cosine of the angle between the normal and the refracted ray as `r_cos_theta_t`
* if provided. */
ccl_device_forceinline void microfacet_fresnel(ccl_private const MicrofacetBsdf *bsdf,
ccl_device_forceinline void microfacet_fresnel(KernelGlobals kg,
ccl_private const MicrofacetBsdf *bsdf,
const float cos_theta_i,
ccl_private float *r_cos_theta_t,
ccl_private Spectrum *r_reflectance,
@ -258,13 +268,41 @@ ccl_device_forceinline void microfacet_fresnel(ccl_private const MicrofacetBsdf
else if (bsdf->fresnel_type == MicrofacetFresnel::GENERALIZED_SCHLICK) {
ccl_private FresnelGeneralizedSchlick *fresnel = (ccl_private FresnelGeneralizedSchlick *)
bsdf->fresnel;
float s;
if (fresnel->exponent < 0.0f) {
Spectrum F;
if (fresnel->thin_film.thickness > 0.1f) {
/* Iridescence doesn't combine well with the general case. We only expose it through the
* Principled BSDF for now, so it's fine to not support custom exponents and F90. */
kernel_assert(fresnel->exponent < 0.0f);
kernel_assert(fresnel->f90 == one_spectrum());
F = fresnel_iridescence(kg,
1.0f,
fresnel->thin_film.ior,
bsdf->ior,
cos_theta_i,
fresnel->thin_film.thickness,
r_cos_theta_t);
/* Apply F0 scaling (here per-channel, since iridescence produces colored output).
* Note that the usual approach (as used below) cannot be used here, since F may be below
* F0_real. Therefore, use a different approach: Scale the result by (F0 / F0_real), with
* the strength of the scaling depending on how close F is to F0_real.
* There isn't one single "correct" way to do this, it's just for artistic control anyways.
*/
const float F0_real = F0_from_ior(bsdf->ior);
if (F0_real > 1e-5f && !isequal(F, one_spectrum())) {
FOREACH_SPECTRUM_CHANNEL (i) {
const float s = saturatef(inverse_lerp(1.0f, F0_real, GET_SPECTRUM_CHANNEL(F, i)));
const float factor = GET_SPECTRUM_CHANNEL(fresnel->f0, i) / F0_real;
GET_SPECTRUM_CHANNEL(F, i) *= mix(1.0f, factor, s);
}
}
}
else if (fresnel->exponent < 0.0f) {
/* Special case: Use real Fresnel curve to determine the interpolation between F0 and F90.
* Used by Principled v1. */
* Used by Principled BSDF. */
const float F_real = fresnel_dielectric(cos_theta_i, bsdf->ior, r_cos_theta_t);
const float F0_real = F0_from_ior(bsdf->ior);
s = saturatef(inverse_lerp(F0_real, 1.0f, F_real));
const float s = saturatef(inverse_lerp(F0_real, 1.0f, F_real));
F = mix(fresnel->f0, fresnel->f90, s);
}
else {
/* Regular case: Generalized Schlick term. */
@ -282,9 +320,10 @@ ccl_device_forceinline void microfacet_fresnel(ccl_private const MicrofacetBsdf
/* TODO(lukas): Is a special case for exponent==5 worth it? */
/* When going from a higher to a lower IOR, we must use the transmitted angle. */
s = powf(1.0f - ((bsdf->ior < 1.0f) ? cos_theta_t : cos_theta_i), fresnel->exponent);
const float fresnel_angle = ((bsdf->ior < 1.0f) ? cos_theta_t : cos_theta_i);
const float s = powf(1.0f - fresnel_angle, fresnel->exponent);
F = mix(fresnel->f0, fresnel->f90, s);
}
const Spectrum F = mix(fresnel->f0, fresnel->f90, s);
*r_reflectance = F * fresnel->reflection_tint;
*r_transmittance = (one_spectrum() - F) * fresnel->transmission_tint;
}
@ -374,7 +413,7 @@ ccl_device Spectrum bsdf_microfacet_estimate_albedo(KernelGlobals kg,
{
const float cos_NI = dot(sd->wi, bsdf->N);
Spectrum reflectance, transmittance;
microfacet_fresnel(bsdf, cos_NI, nullptr, &reflectance, &transmittance);
microfacet_fresnel(kg, bsdf, cos_NI, nullptr, &reflectance, &transmittance);
reflectance *= (float)eval_reflection;
transmittance *= (float)eval_transmission;
@ -384,19 +423,25 @@ ccl_device Spectrum bsdf_microfacet_estimate_albedo(KernelGlobals kg,
ccl_private FresnelGeneralizedSchlick *fresnel = (ccl_private FresnelGeneralizedSchlick *)
bsdf->fresnel;
weizhen marked this conversation as resolved Outdated

For cognitive ease I would prefer to add an empty if (fresnel->thin_film.thickness > 0.1f) branch and put the comment there, the following block is then put in the else branch

For cognitive ease I would prefer to add an empty `if (fresnel->thin_film.thickness > 0.1f)` branch and put the comment there, the following block is then put in the `else` branch
float rough = sqrtf(sqrtf(bsdf->alpha_x * bsdf->alpha_y));
float s;
if (fresnel->exponent < 0.0f) {
float z = sqrtf(fabsf((bsdf->ior - 1.0f) / (bsdf->ior + 1.0f)));
s = lookup_table_read_3D(
kg, rough, cos_NI, z, kernel_data.tables.ggx_gen_schlick_ior_s, 16, 16, 16);
if (fresnel->thin_film.thickness > 0.1f) {
/* Precomputing LUTs for thin-film iridescence isn't viable, so fall back to the specular
* reflection approximation from the microfacet_fresnel call above in that case. */
}
else {
float z = 1.0f / (0.2f * fresnel->exponent + 1.0f);
s = lookup_table_read_3D(
kg, rough, cos_NI, z, kernel_data.tables.ggx_gen_schlick_s, 16, 16, 16);
float rough = sqrtf(sqrtf(bsdf->alpha_x * bsdf->alpha_y));
float s;
if (fresnel->exponent < 0.0f) {
float z = sqrtf(fabsf((bsdf->ior - 1.0f) / (bsdf->ior + 1.0f)));
s = lookup_table_read_3D(
kg, rough, cos_NI, z, kernel_data.tables.ggx_gen_schlick_ior_s, 16, 16, 16);
}
else {
float z = 1.0f / (0.2f * fresnel->exponent + 1.0f);
s = lookup_table_read_3D(
kg, rough, cos_NI, z, kernel_data.tables.ggx_gen_schlick_s, 16, 16, 16);
}
reflectance = mix(fresnel->f0, fresnel->f90, s) * fresnel->reflection_tint;
}
reflectance = mix(fresnel->f0, fresnel->f90, s) * fresnel->reflection_tint;
}
else if (bsdf->fresnel_type == MicrofacetFresnel::F82_TINT) {
ccl_private FresnelF82Tint *fresnel = (ccl_private FresnelF82Tint *)bsdf->fresnel;
@ -498,7 +543,8 @@ ccl_device_forceinline int bsdf_microfacet_eval_flag(const ccl_private Microface
}
weizhen marked this conversation as resolved Outdated

This does not compile on Metal. Add ccl_private.

This does not compile on Metal. Add `ccl_private`.
template<MicrofacetType m_type>
ccl_device Spectrum bsdf_microfacet_eval(ccl_private const ShaderClosure *sc,
ccl_device Spectrum bsdf_microfacet_eval(KernelGlobals kg,
ccl_private const ShaderClosure *sc,
const float3 Ng,
const float3 wi,
const float3 wo,
@ -544,7 +590,7 @@ ccl_device Spectrum bsdf_microfacet_eval(ccl_private const ShaderClosure *sc,
/* Compute Fresnel coefficients. */
const float cos_HI = dot(H, wi);
Spectrum reflectance, transmittance;
microfacet_fresnel(bsdf, cos_HI, nullptr, &reflectance, &transmittance);
microfacet_fresnel(kg, bsdf, cos_HI, nullptr, &reflectance, &transmittance);
if (is_zero(reflectance) && is_zero(transmittance)) {
return zero_spectrum();
@ -587,7 +633,8 @@ ccl_device Spectrum bsdf_microfacet_eval(ccl_private const ShaderClosure *sc,
}
template<MicrofacetType m_type>
ccl_device int bsdf_microfacet_sample(ccl_private const ShaderClosure *sc,
ccl_device int bsdf_microfacet_sample(KernelGlobals kg,
ccl_private const ShaderClosure *sc,
float3 Ng,
float3 wi,
const float3 rand,
@ -647,7 +694,7 @@ ccl_device int bsdf_microfacet_sample(ccl_private const ShaderClosure *sc,
float cos_HO;
/* Compute Fresnel coefficients. */
Spectrum reflectance, transmittance;
microfacet_fresnel(bsdf, cos_HI, &cos_HO, &reflectance, &transmittance);
microfacet_fresnel(kg, bsdf, cos_HI, &cos_HO, &reflectance, &transmittance);
if (is_zero(reflectance) && is_zero(transmittance)) {
return LABEL_NONE;
@ -920,17 +967,19 @@ ccl_device void bsdf_microfacet_blur(ccl_private ShaderClosure *sc, float roughn
bsdf->alpha_y = fmaxf(roughness, bsdf->alpha_y);
}
ccl_device Spectrum bsdf_microfacet_ggx_eval(ccl_private const ShaderClosure *sc,
ccl_device Spectrum bsdf_microfacet_ggx_eval(KernelGlobals kg,
ccl_private const ShaderClosure *sc,
const float3 Ng,
const float3 wi,
const float3 wo,
ccl_private float *pdf)
{
ccl_private const MicrofacetBsdf *bsdf = (ccl_private const MicrofacetBsdf *)sc;
return bsdf->energy_scale * bsdf_microfacet_eval<MicrofacetType::GGX>(sc, Ng, wi, wo, pdf);
return bsdf->energy_scale * bsdf_microfacet_eval<MicrofacetType::GGX>(kg, sc, Ng, wi, wo, pdf);
}
ccl_device int bsdf_microfacet_ggx_sample(ccl_private const ShaderClosure *sc,
ccl_device int bsdf_microfacet_ggx_sample(KernelGlobals kg,
ccl_private const ShaderClosure *sc,
float3 Ng,
float3 wi,
const float3 rand,
@ -942,7 +991,7 @@ ccl_device int bsdf_microfacet_ggx_sample(ccl_private const ShaderClosure *sc,
{
int label = bsdf_microfacet_sample<MicrofacetType::GGX>(
sc, Ng, wi, rand, eval, wo, pdf, sampled_roughness, eta);
kg, sc, Ng, wi, rand, eval, wo, pdf, sampled_roughness, eta);
*eval *= ((ccl_private const MicrofacetBsdf *)sc)->energy_scale;
return label;
}
@ -985,16 +1034,18 @@ ccl_device int bsdf_microfacet_beckmann_glass_setup(ccl_private MicrofacetBsdf *
return SD_BSDF | SD_BSDF_HAS_TRANSMISSION | bsdf_microfacet_eval_flag(bsdf);
}
ccl_device Spectrum bsdf_microfacet_beckmann_eval(ccl_private const ShaderClosure *sc,
ccl_device Spectrum bsdf_microfacet_beckmann_eval(KernelGlobals kg,
ccl_private const ShaderClosure *sc,
const float3 Ng,
const float3 wi,
const float3 wo,
ccl_private float *pdf)
{
return bsdf_microfacet_eval<MicrofacetType::BECKMANN>(sc, Ng, wi, wo, pdf);
return bsdf_microfacet_eval<MicrofacetType::BECKMANN>(kg, sc, Ng, wi, wo, pdf);
}
ccl_device int bsdf_microfacet_beckmann_sample(ccl_private const ShaderClosure *sc,
ccl_device int bsdf_microfacet_beckmann_sample(KernelGlobals kg,
ccl_private const ShaderClosure *sc,
float3 Ng,
float3 wi,
const float3 rand,
@ -1005,7 +1056,7 @@ ccl_device int bsdf_microfacet_beckmann_sample(ccl_private const ShaderClosure *
ccl_private float *eta)
{
return bsdf_microfacet_sample<MicrofacetType::BECKMANN>(
sc, Ng, wi, rand, eval, wo, pdf, sampled_roughness, eta);
kg, sc, Ng, wi, rand, eval, wo, pdf, sampled_roughness, eta);
}
CCL_NAMESPACE_END

View File

@ -9,9 +9,14 @@
CCL_NAMESPACE_BEGIN
/* Compute fresnel reflectance. Also return the dot product of the refracted ray and the normal as
* `cos_theta_t`, as it is used when computing the direction of the refracted ray. */
ccl_device float fresnel_dielectric(float cos_theta_i, float eta, ccl_private float *r_cos_theta_t)
/* Compute fresnel reflectance for perpendicular (aka S-) and parallel (aka P-) polarized light.
* If requested by the caller, r_phi is set to the phase shift on reflection.
* Also returns the dot product of the refracted ray and the normal as `cos_theta_t`, as it is
* used when computing the direction of the refracted ray. */
ccl_device float2 fresnel_dielectric_polarized(float cos_theta_i,
float eta,
ccl_private float *r_cos_theta_t,
ccl_private float2 *r_phi)
{
kernel_assert(!isnan_safe(cos_theta_i));
@ -20,7 +25,18 @@ ccl_device float fresnel_dielectric(float cos_theta_i, float eta, ccl_private fl
const float eta_cos_theta_t_sq = sqr(eta) - (1.0f - sqr(cos_theta_i));
if (eta_cos_theta_t_sq <= 0) {
/* Total internal reflection. */
return 1.0f;
if (r_phi) {
/* The following code would compute the proper phase shift on TIR.
* However, for the current user of this computation (the iridescence code),
* this doesn't actually affect the result, so don't bother with the computation for now.
*
* const float fac = sqrtf(1.0f - sqr(cosThetaI) - sqr(eta));
* r_phi->x = -2.0f * atanf(fac / cosThetaI);
* r_phi->y = -2.0f * atanf(fac / (cosThetaI * sqr(eta)));
*/
*r_phi = zero_float2();
}
return one_float2();
}
cos_theta_i = fabsf(cos_theta_i);
@ -33,9 +49,22 @@ ccl_device float fresnel_dielectric(float cos_theta_i, float eta, ccl_private fl
/* Amplitudes of reflected waves. */
const float r_s = (cos_theta_i + eta * cos_theta_t) / (cos_theta_i - eta * cos_theta_t);
const float r_p = (cos_theta_t + eta * cos_theta_i) / (cos_theta_t - eta * cos_theta_i);
const float r_p = (cos_theta_t + eta * cos_theta_i) / (eta * cos_theta_i - cos_theta_t);
return 0.5f * (sqr(r_s) + sqr(r_p));
if (r_phi) {
*r_phi = make_float2(r_s < 0.0f, r_p < 0.0f) * M_PI_F;
}
/* Return squared amplitude to get the fraction of reflected energy. */
return make_float2(sqr(r_s), sqr(r_p));
weizhen marked this conversation as resolved Outdated

The newly added sqr() is not used, I assume you wanted to use it here?

The newly added `sqr()` is not used, I assume you wanted to use it here?

At first I thought that this needs to return un-squared coefficients, so I used average(sqr(fresnel_dielectric_polarized(...))) in fresnel_dielectric. But with the code as it is now, I don't think sqr(make_float2(r_s, r_p)) is much nicer than make_float2(sqr(r_s), sqr(r_p)), so we might as well not add it at all I guess.

At first I thought that this needs to return un-squared coefficients, so I used `average(sqr(fresnel_dielectric_polarized(...)))` in `fresnel_dielectric`. But with the code as it is now, I don't think `sqr(make_float2(r_s, r_p))` is much nicer than `make_float2(sqr(r_s), sqr(r_p))`, so we might as well not add it at all I guess.
}
/* Compute fresnel reflectance for unpolarized light. */
ccl_device_forceinline float fresnel_dielectric(float cos_theta_i,
float eta,
ccl_private float *r_cos_theta_t)
{
return average(fresnel_dielectric_polarized(cos_theta_i, eta, r_cos_theta_t, nullptr));
weizhen marked this conversation as resolved Outdated

We use nullptr now.
Also, average() for float2 doesn't seem to be defined on metal 😕

We use `nullptr` now. Also, `average()` for `float2` doesn't seem to be defined on metal 😕

Ah, okay, I wasn't sure if that's supported on GPU but looks like it is.

Ah, okay, I wasn't sure if that's supported on GPU but looks like it is.

I think the kernel is all C++ now, but not feature complete on some GPUs, not sure what exactly is supported but if buildbot doesn't complain it should be fine.

I think the kernel is all C++ now, but not feature complete on some GPUs, not sure what exactly is supported but if buildbot doesn't complain it should be fine.
}
/* Refract the incident ray, given the cosine of the refraction angle and the relative refractive
@ -235,4 +264,123 @@ ccl_device_inline Spectrum closure_layering_weight(const Spectrum layer_albedo,
return weight * saturatef(1.0f - reduce_max(safe_divide_color(layer_albedo, weight)));
}
/* ******** Thin-film iridescence implementation ********
*
* Based on "A Practical Extension to Microfacet Theory for the Modeling of Varying Iridescence"
* by Laurent Belcour and Pascal Barla.
* https://belcour.github.io/blog/research/publication/2017/05/01/brdf-thin-film.html.
*/
/* Evaluate the sensitivity functions for the Fourier-space spectral integration.
* The code here uses the Gaussian fit for the CIE XYZ curves that is provided
weizhen marked this conversation as resolved Outdated

Why not sqrtf() here?

Why not `sqrtf()` here?

Thanks, fixed.

Thanks, fixed.
* in the reference implementation.
* For details on what this actually represents, see the paper.
* In theory we should pre-compute the sensitivity functions for the working RGB
* colorspace, remap them to be functions of (light) frequency, take their Fourier
* transform and store them as a LUT that gets looked up here.
* In practise, using the XYZ fit and converting the result from XYZ to RGB is easier.
*/
ccl_device_inline Spectrum iridescence_lookup_sensitivity(float OPD, float shift)
{
float phase = M_2PI_F * OPD * 1e-9f;
float3 val = make_float3(5.4856e-13f, 4.4201e-13f, 5.2481e-13f);
float3 pos = make_float3(1.6810e+06f, 1.7953e+06f, 2.2084e+06f);
float3 var = make_float3(4.3278e+09f, 9.3046e+09f, 6.6121e+09f);
float3 xyz = val * sqrt(M_2PI_F * var) * cos(pos * phase + shift) * exp(-sqr(phase) * var);
xyz.x += 1.64408e-8f * cosf(2.2399e+06f * phase + shift) * expf(-4.5282e+09f * sqr(phase));
weizhen marked this conversation as resolved Outdated

This function is almost identical as fresnel_dielectric() except for the phase computation, would it be practical to reuse that function? The phase computation could then be:

phi->x = float(r_s < 0.0f) * M_PI_F;
phi->y = float(r_p < 0.0f) * M_PI_F;

The existing function also returns cos_theta_t, which is needed for computing the OPD.

If it turns out to be impractical, I would then prefer that these two functions have the same structure, notation and comment, eg. snake_case, a comment indicating total internal reflection, mentioning s and p polarization (or compute r_s and r_p first, then square the result).

This function is almost identical as `fresnel_dielectric()` except for the phase computation, would it be practical to reuse that function? The phase computation could then be: ``` phi->x = float(r_s < 0.0f) * M_PI_F; phi->y = float(r_p < 0.0f) * M_PI_F; ``` The existing function also returns `cos_theta_t`, which is needed for computing the OPD. If it turns out to be impractical, I would then prefer that these two functions have the same structure, notation and comment, eg. `snake_case`, a comment indicating total internal reflection, mentioning s and p polarization (or compute `r_s` and `r_p` first, then square the result).

Good point, thanks, those are indeed very similar. I'll merge them together.

Comparing the two, I think I found a sign error in the current code. So far it wouldn't have mattered because we only use squared values, but it does matter for the phase.

Good point, thanks, those are indeed very similar. I'll merge them together. Comparing the two, I think I found a sign error in the current code. So far it wouldn't have mattered because we only use squared values, but it does matter for the phase.

Do you mean the sign of cos_theta_t? In fresnel_dielectric() it is the angle between the surface normal and the refracted ray, hence negative, and the sign is used for refract_angle(). The result should be the same as your fresnel_dielectric_polarized(), i.e. cos_theta_i + eta * cos_theta_t < 0 is equivalent to eta1 * cosThetaI < eta2 * cosThetaT.

Do you mean the sign of `cos_theta_t`? In `fresnel_dielectric()` it is the angle between the surface normal and the refracted ray, hence negative, and the sign is used for `refract_angle()`. The result should be the same as your `fresnel_dielectric_polarized()`, i.e. `cos_theta_i + eta * cos_theta_t < 0` is equivalent to `eta1 * cosThetaI < eta2 * cosThetaT`.

Yes, I've noticed that and updated the code accordingly.
What I meant is that the current code does

const float r_p = (cos_theta_t + eta * cos_theta_i) / (cos_theta_t - eta * cos_theta_i);

but I think it should be

const float r_p = (cos_theta_t + eta * cos_theta_i) / (eta * cos_theta_i - cos_theta_t);

(note the sign swap in the denominator). This only sign-swaps r_p, which doesn't matter currently since the code only returns sqr(r_p).

Yes, I've noticed that and updated the code accordingly. What I meant is that the current code does ``` const float r_p = (cos_theta_t + eta * cos_theta_i) / (cos_theta_t - eta * cos_theta_i); ``` but I think it should be ``` const float r_p = (cos_theta_t + eta * cos_theta_i) / (eta * cos_theta_i - cos_theta_t); ``` (note the sign swap in the denominator). This only sign-swaps `r_p`, which doesn't matter currently since the code only returns `sqr(r_p)`.

Yes, you are right, I was not paying attention to the denominator. I remember switching the order was only to make the two lines have the same structure because the sign didn't matter. Sorry for the confusion 😄

Yes, you are right, I was not paying attention to the denominator. I remember switching the order was only to make the two lines have the same structure because the sign didn't matter. Sorry for the confusion 😄
return xyz / 1.0685e-7f;
}
ccl_device_inline float3
iridescence_airy_summation(float T121, float R12, float R23, float OPD, float phi)
{
if (R23 == 1.0f) {
/* Shortcut for TIR on the bottom interface. */
return one_float3();
}
float R123 = R12 * R23;
float r123 = sqrtf(R123);
float Rs = sqr(T121) * R23 / (1.0f - R123);
/* Perform summation over path order differences (equation 10). */
float3 R = make_float3(R12 + Rs); /* C0 */
float Cm = (Rs - T121);
/* Truncate after m=3, higher differences have barely any impact. */
for (int m = 1; m < 4; m++) {
Cm *= r123;
R += Cm * 2.0f * iridescence_lookup_sensitivity(m * OPD, m * phi);
}
return R;
}
ccl_device Spectrum fresnel_iridescence(KernelGlobals kg,
float eta1,
float eta2,
float eta3,
float cos_theta_1,
float thickness,
ccl_private float *r_cos_theta_3)
{
/* For films below 30nm, the wave-optic-based Airy summation approach no longer applies,
* so blend towards the case without coating. */
if (thickness < 30.0f) {
eta2 = mix(eta1, eta2, smoothstep(0.0f, 30.0f, thickness));
}
float cos_theta_2;
float2 phi12, phi23;
weizhen marked this conversation as resolved Outdated

I believe we should trust fresnel_dielectric_polarized() instead of checking TIR before calling the function.

I believe we should trust `fresnel_dielectric_polarized()` instead of checking TIR before calling the function.
/* Compute reflection at the top interface (ambient to film). */
float2 R12 = fresnel_dielectric_polarized(cos_theta_1, eta2 / eta1, &cos_theta_2, &phi12);
if (isequal(R12, one_float2())) {
/* TIR at the top interface. */
return one_spectrum();
}
/* Compute optical path difference inside the thin film. */
float OPD = -2.0f * eta2 * thickness * cos_theta_2;
/* Compute reflection at the bottom interface (film to medium). */
float2 R23 = fresnel_dielectric_polarized(-cos_theta_2, eta3 / eta2, r_cos_theta_3, &phi23);
if (isequal(R23, one_float2())) {
/* TIR at the bottom interface.
* All the Airy summation math still simplifies to 1.0 in this case. */
return one_spectrum();
}
/* Compute helper parameters. */
float2 T121 = one_float2() - R12;
float2 phi = make_float2(M_PI_F, M_PI_F) - phi12 + phi23;
/* Perform Airy summation and average the polarizations. */
float3 R = mix(iridescence_airy_summation(T121.x, R12.x, R23.x, OPD, phi.x),
iridescence_airy_summation(T121.y, R12.y, R23.y, OPD, phi.y),
0.5f);
/* Color space conversion here is tricky.
* In theory, the correct thing would be to compute the spectral color matching functions
* for the RGB channels, take their Fourier transform in wavelength parametrization, and
* then use that in iridescence_lookup_sensitivity().
* To avoid this complexity, the code here instead uses the reference implementation's
* Gaussian fit of the CIE XYZ curves. However, this means that at this point, R is in
* XYZ values, not RGB.
* Additionally, since I is a reflectivity, not a luminance, the spectral color matching
* functions should be multiplied by the reference illuminant. Since the fit is based on
* the "raw" CIE XYZ curves, the reference illuminant implicitly is a constant spectrum,
* meaning Illuminant E.
* Therefore, we can't just use the regular XYZ->RGB conversion here, we need to include
* a chromatic adaption from E to whatever the white point of the working color space is.
* The proper way to do this would be a Von Kries-style transform, but to keep it simple,
* we just multiply by the white point here.
*
* Note: The reference implementation sidesteps all this by just hardcoding a XYZ->CIE RGB
* matrix. Since CIE RGB uses E as its white point, this sidesteps the chromatic adaption
* topic, but the primary colors don't match (unless you happen to actually work in CIE RGB.)
*/
R *= float4_to_float3(kernel_data.film.white_xyz);
return saturate(xyz_to_rgb(kg, R));
}
CCL_NAMESPACE_END

View File

@ -69,6 +69,7 @@ KERNEL_STRUCT_MEMBER(film, float4, xyz_to_r)
KERNEL_STRUCT_MEMBER(film, float4, xyz_to_g)
KERNEL_STRUCT_MEMBER(film, float4, xyz_to_b)
KERNEL_STRUCT_MEMBER(film, float4, rgb_to_y)
KERNEL_STRUCT_MEMBER(film, float4, white_xyz)
/* Rec709 to rendering color space. */
KERNEL_STRUCT_MEMBER(film, float4, rec709_to_r)
KERNEL_STRUCT_MEMBER(film, float4, rec709_to_g)

View File

@ -598,7 +598,8 @@ mnee_sample_bsdf_dh(ClosureType type, float alpha_x, float alpha_y, float sample
* We assume here that the pdf (in half-vector measure) is the same as
* the one calculation when sampling the microfacet normals from the
* specular chain above: this allows us to simplify the bsdf weight */
ccl_device_forceinline Spectrum mnee_eval_bsdf_contribution(ccl_private ShaderClosure *closure,
ccl_device_forceinline Spectrum mnee_eval_bsdf_contribution(KernelGlobals kg,
ccl_private ShaderClosure *closure,
float3 wi,
float3 wo)
{
@ -623,7 +624,7 @@ ccl_device_forceinline Spectrum mnee_eval_bsdf_contribution(ccl_private ShaderCl
}
Spectrum reflectance, transmittance;
microfacet_fresnel(bsdf, cosHI, nullptr, &reflectance, &transmittance);
microfacet_fresnel(kg, bsdf, cosHI, nullptr, &reflectance, &transmittance);
/*
* bsdf_do = (1 - F) * D_do * G * |h.wi| / (n.wi * n.wo)
@ -895,7 +896,7 @@ ccl_device_forceinline bool mnee_path_contribution(KernelGlobals kg,
/* Evaluate product term inside eq.6 at solution interface. vi
* divided by corresponding sampled pdf:
* fr(vi)_do / pdf_dh(vi) x |do/dh| x |n.wo / n.h| */
Spectrum bsdf_contribution = mnee_eval_bsdf_contribution(v.bsdf, wi, wo);
Spectrum bsdf_contribution = mnee_eval_bsdf_contribution(kg, v.bsdf, wi, wo);
bsdf_eval_mul(throughput, bsdf_contribution);
}

View File

@ -274,6 +274,8 @@ ccl_device void osl_closure_dielectric_bsdf_setup(KernelGlobals kg,
fresnel->reflection_tint = rgb_to_spectrum(closure->reflection_tint);
fresnel->transmission_tint = rgb_to_spectrum(closure->transmission_tint);
fresnel->thin_film.thickness = 0.0f;
fresnel->thin_film.ior = 0.0f;
bsdf_microfacet_setup_fresnel_dielectric_tint(kg, bsdf, sd, fresnel, preserve_energy);
if (layer_albedo != NULL) {
@ -423,6 +425,8 @@ ccl_device void osl_closure_generalized_schlick_bsdf_setup(
fresnel->f0 = rgb_to_spectrum(closure->f0);
fresnel->f90 = rgb_to_spectrum(closure->f90);
fresnel->exponent = closure->exponent;
fresnel->thin_film.thickness = 0.0f;
fresnel->thin_film.ior = 0.0f;
bsdf_microfacet_setup_fresnel_generalized_schlick(kg, bsdf, sd, fresnel, preserve_energy);
if (layer_albedo != NULL) {

View File

@ -83,8 +83,8 @@ ccl_device
uint specular_ior_level_offset, roughness_offset, specular_tint_offset, anisotropic_offset,
sheen_weight_offset, sheen_tint_offset, sheen_roughness_offset, coat_weight_offset,
coat_roughness_offset, coat_ior_offset, eta_offset, transmission_weight_offset,
anisotropic_rotation_offset, coat_tint_offset, coat_normal_offset, dummy, alpha_offset,
emission_strength_offset, emission_offset, unused;
anisotropic_rotation_offset, coat_tint_offset, coat_normal_offset, alpha_offset,
emission_strength_offset, emission_offset, thinfilm_thickness_offset, unused;
uint4 data_node2 = read_node(kg, &offset);
float3 T = stack_load_float3(stack, data_node.y);
@ -147,21 +147,24 @@ ccl_device
// get the subsurface scattering data
uint4 data_subsurf = read_node(kg, &offset);
uint4 data_alpha_emission = read_node(kg, &offset);
svm_unpack_node_uchar4(data_alpha_emission.x,
uint4 data_alpha_emission_thin = read_node(kg, &offset);
svm_unpack_node_uchar4(data_alpha_emission_thin.x,
&alpha_offset,
&emission_strength_offset,
&emission_offset,
&dummy);
&thinfilm_thickness_offset);
float alpha = stack_valid(alpha_offset) ? stack_load_float(stack, alpha_offset) :
__uint_as_float(data_alpha_emission.y);
__uint_as_float(data_alpha_emission_thin.y);
alpha = saturatef(alpha);
float emission_strength = stack_valid(emission_strength_offset) ?
stack_load_float(stack, emission_strength_offset) :
__uint_as_float(data_alpha_emission.z);
__uint_as_float(data_alpha_emission_thin.z);
float3 emission = stack_load_float3(stack, emission_offset) * emission_strength;
float thinfilm_thickness = fmaxf(stack_load_float(stack, thinfilm_thickness_offset), 1e-5f);
float thinfilm_ior = fmaxf(stack_load_float(stack, data_alpha_emission_thin.w), 1e-5f);
Spectrum weight = closure_weight * mix_weight;
float alpha_x = sqr(roughness), alpha_y = sqr(roughness);
@ -323,6 +326,9 @@ ccl_device
fresnel->transmission_tint = refractive_caustics ?
sqrt(rgb_to_spectrum(clamped_base_color)) :
zero_spectrum();
fresnel->thin_film.thickness = thinfilm_thickness;
fresnel->thin_film.ior = (sd->flag & SD_BACKFACING) ? thinfilm_ior / ior :
thinfilm_ior;
/* setup bsdf */
sd->flag |= bsdf_microfacet_ggx_glass_setup(bsdf);
@ -346,7 +352,7 @@ ccl_device
}
/* Specular component */
if (reflective_caustics && eta != 1.0f) {
if (reflective_caustics && (eta != 1.0f || thinfilm_thickness > 0.1f)) {
ccl_private MicrofacetBsdf *bsdf = (ccl_private MicrofacetBsdf *)bsdf_alloc(
sd, sizeof(MicrofacetBsdf), weight);
ccl_private FresnelGeneralizedSchlick *fresnel =
@ -366,6 +372,8 @@ ccl_device
fresnel->exponent = -eta;
fresnel->reflection_tint = one_spectrum();
fresnel->transmission_tint = zero_spectrum();
fresnel->thin_film.thickness = thinfilm_thickness;
fresnel->thin_film.ior = thinfilm_ior;
/* setup bsdf */
sd->flag |= bsdf_microfacet_ggx_setup(bsdf);
@ -601,6 +609,8 @@ ccl_device
fresnel->reflection_tint = reflective_caustics ? rgb_to_spectrum(color) : zero_spectrum();
fresnel->transmission_tint = refractive_caustics ? rgb_to_spectrum(color) :
zero_spectrum();
fresnel->thin_film.thickness = 0.0f;
fresnel->thin_film.ior = 0.0f;
/* setup bsdf */
if (type == CLOSURE_BSDF_MICROFACET_BECKMANN_GLASS_ID) {

View File

@ -642,6 +642,7 @@ void ShaderManager::device_update_common(Device * /*device*/,
kfilm->xyz_to_g = float3_to_float4(xyz_to_g);
kfilm->xyz_to_b = float3_to_float4(xyz_to_b);
kfilm->rgb_to_y = float3_to_float4(rgb_to_y);
kfilm->white_xyz = float3_to_float4(white_xyz);
kfilm->rec709_to_r = float3_to_float4(rec709_to_r);
kfilm->rec709_to_g = float3_to_float4(rec709_to_g);
kfilm->rec709_to_b = float3_to_float4(rec709_to_b);
@ -894,6 +895,7 @@ void ShaderManager::init_xyz_transforms()
xyz_to_g = float4_to_float3(xyz_to_rec709.y);
xyz_to_b = float4_to_float3(xyz_to_rec709.z);
rgb_to_y = make_float3(0.2126729f, 0.7151522f, 0.0721750f);
white_xyz = make_float3(0.95047f, 1.0f, 1.08883f);
rec709_to_r = make_float3(1.0f, 0.0f, 0.0f);
rec709_to_g = make_float3(0.0f, 1.0f, 0.0f);
@ -950,6 +952,7 @@ void ShaderManager::init_xyz_transforms()
const Transform rgb_to_xyz = transform_inverse(xyz_to_rgb);
rgb_to_y = float4_to_float3(rgb_to_xyz.y);
white_xyz = transform_direction(&rgb_to_xyz, one_float3());
const Transform rec709_to_rgb = xyz_to_rgb * transform_inverse(xyz_to_rec709);
rec709_to_r = float4_to_float3(rec709_to_rgb.x);

View File

@ -250,6 +250,7 @@ class ShaderManager {
float3 xyz_to_g;
float3 xyz_to_b;
float3 rgb_to_y;
float3 white_xyz;
float3 rec709_to_r;
float3 rec709_to_g;
float3 rec709_to_b;

View File

@ -2661,6 +2661,9 @@ NODE_DEFINE(PrincipledBsdfNode)
SOCKET_IN_COLOR(emission_color, "Emission Color", one_float3());
SOCKET_IN_FLOAT(emission_strength, "Emission Strength", 0.0f);
SOCKET_IN_FLOAT(thin_film_thickness, "Thin Film Thickness", 0.0f);
SOCKET_IN_FLOAT(thin_film_ior, "Thin Film IOR", 1.3f);
SOCKET_IN_FLOAT(surface_mix_weight, "SurfaceMixWeight", 0.0f, SocketType::SVM_INTERNAL);
SOCKET_OUT_CLOSURE(BSDF, "BSDF");
@ -2762,6 +2765,8 @@ void PrincipledBsdfNode::compile(SVMCompiler &compiler)
int alpha_offset = compiler.stack_assign_if_linked(alpha_in);
int emission_strength_offset = compiler.stack_assign_if_linked(emission_strength_in);
int emission_color_offset = compiler.stack_assign(input("Emission Color"));
int thin_film_thickness_offset = compiler.stack_assign(input("Thin Film Thickness"));
int thin_film_ior_offset = compiler.stack_assign(input("Thin Film IOR"));
compiler.add_node(
NODE_CLOSURE_BSDF,
@ -2800,12 +2805,13 @@ void PrincipledBsdfNode::compile(SVMCompiler &compiler)
subsurface_scale_offset,
subsurface_anisotropy_offset);
compiler.add_node(
compiler.encode_uchar4(
alpha_offset, emission_strength_offset, emission_color_offset, SVM_STACK_INVALID),
__float_as_int(get_float(alpha_in->socket_type)),
__float_as_int(get_float(emission_strength_in->socket_type)),
SVM_STACK_INVALID);
compiler.add_node(compiler.encode_uchar4(alpha_offset,
emission_strength_offset,
emission_color_offset,
thin_film_thickness_offset),
__float_as_int(get_float(alpha_in->socket_type)),
__float_as_int(get_float(emission_strength_in->socket_type)),
thin_film_ior_offset);
}
void PrincipledBsdfNode::compile(OSLCompiler &compiler)

View File

@ -536,6 +536,8 @@ class PrincipledBsdfNode : public BsdfBaseNode {
NODE_SOCKET_API(float3, emission_color)
NODE_SOCKET_API(float, emission_strength)
NODE_SOCKET_API(float, surface_mix_weight)
NODE_SOCKET_API(float, thin_film_thickness)
NODE_SOCKET_API(float, thin_film_ior)
public:
void attributes(Shader *shader, AttributeRequestSet *attributes);

View File

@ -119,17 +119,26 @@ ccl_device_inline bool is_zero(const float2 a)
return (a.x == 0.0f && a.y == 0.0f);
}
ccl_device_inline float average(const float2 a)
{
return (a.x + a.y) * (1.0f / 2.0f);
}
ccl_device_inline float dot(const float2 a, const float2 b)
{
return a.x * b.x + a.y * b.y;
}
#endif
ccl_device_inline float average(const float2 a)
{
return (a.x + a.y) * (1.0f / 2.0f);
}
ccl_device_inline bool isequal(const float2 a, const float2 b)
{
#if defined(__KERNEL_METAL__)
return all(a == b);
#else
return a == b;
#endif
}
ccl_device_inline float len(const float2 a)
{
return sqrtf(dot(a, a));

View File

@ -371,6 +371,11 @@ ccl_device_inline float3 log(float3 v)
return make_float3(logf(v.x), logf(v.y), logf(v.z));
}
ccl_device_inline float3 cos(float3 v)
{
return make_float3(cosf(v.x), cosf(v.y), cosf(v.z));
}
ccl_device_inline float3 reflect(const float3 incident, const float3 normal)
{
float3 unit_normal = normalize(normal);

View File

@ -55,6 +55,8 @@ void node_bsdf_principled(vec4 base_color,
vec4 sheen_tint,
vec4 emission,
float emission_strength,
float thin_film_thickness,
float thin_film_ior,
const float do_diffuse,
const float do_coat,
const float do_refraction,

View File

@ -258,6 +258,17 @@ static void node_declare(NodeDeclarationBuilder &b)
"Strength of the emitted light. A value of 1.0 ensures "
"that the object in the image has the exact same color as the Emission Color");
#define SOCK_EMISSION_STRENGTH_ID 27
/* Panel for Thin Film settings. */
PanelDeclarationBuilder &film = b.add_panel("Thin Film").default_closed(true);
film.add_input<decl::Float>("Thin Film Thickness")
weizhen marked this conversation as resolved Outdated

Specify the unit (nm).

Specify the unit (nm).

Good point. If we're already handling units, might as well do it properly: I've created #120900 for this, depending on what PR gets merged first I'll just update the other to tag this input correctly.

Good point. If we're already handling units, might as well do it properly: I've created #120900 for this, depending on what PR gets merged first I'll just update the other to tag this input correctly.

Default value for float socket is 0.0f by default, no reason to make this explicit.
Max value also does looks like meaningless (what for such soft max?)

Default value for float socket is `0.0f` by default, no reason to make this explicit. _Max value also does looks like meaningless (what for such soft max?)_

I think it's easier to understand the code with the default value explicit.

For soft max indeed FLT_MAX should generally be used if there is no real upper bound. But this can sometimes cause numerical precision problems, so not sure it works here.

I think it's easier to understand the code with the default value explicit. For soft max indeed `FLT_MAX` should generally be used if there is no real upper bound. But this can sometimes cause numerical precision problems, so not sure it works here.

I mean, at some point such declaration can be cleaned due to the fact this is not really necessary.

I mean, at some point such declaration can be cleaned due to the fact this is not really necessary.
.default_value(0.0)
.min(0.0f)
weizhen marked this conversation as resolved Outdated

I think we could default to the IOR of water, which is 1.33.

I think we could default to the IOR of water, which is 1.33.
.max(100000.0f)
.subtype(PROP_WAVELENGTH);
#define SOCK_THIN_FILM_THICKNESS_ID 28
film.add_input<decl::Float>("Thin Film IOR").default_value(1.33f).min(1.0f).max(1000.0f);
#define SOCK_THIN_FILM_IOR_ID 29
}
static void node_shader_init_principled(bNodeTree * /*ntree*/, bNode *node)