diff --git a/intern/cycles/blender/curves.cpp b/intern/cycles/blender/curves.cpp index e69663077af..34f8667ea9b 100644 --- a/intern/cycles/blender/curves.cpp +++ b/intern/cycles/blender/curves.cpp @@ -4,6 +4,7 @@ #include +#include "BKE_curves.hh" #include "blender/sync.h" #include "blender/util.h" @@ -275,10 +276,13 @@ static void ExportCurveSegments(Scene *scene, Hair *hair, ParticleCurveData *CDa if (hair->num_curves()) return; + Attribute *attr_normal = NULL; Attribute *attr_intercept = NULL; Attribute *attr_length = NULL; Attribute *attr_random = NULL; + if (hair->need_attribute(scene, ATTR_STD_VERTEX_NORMAL)) + attr_normal = hair->attributes.add(ATTR_STD_VERTEX_NORMAL); if (hair->need_attribute(scene, ATTR_STD_CURVE_INTERCEPT)) attr_intercept = hair->attributes.add(ATTR_STD_CURVE_INTERCEPT); if (hair->need_attribute(scene, ATTR_STD_CURVE_LENGTH)) @@ -329,6 +333,12 @@ static void ExportCurveSegments(Scene *scene, Hair *hair, ParticleCurveData *CDa if (attr_intercept) attr_intercept->add(time); + if (attr_normal) { + /* NOTE: the geometry normals are not computed for legacy particle hairs. This hair + * system is expected to be discarded. */ + attr_normal->add(make_float3(1.0f, 0.0f, 0.0f)); + } + num_curve_keys++; } @@ -925,6 +935,14 @@ static void export_hair_curves(Scene *scene, float *attr_intercept = NULL; float *attr_length = NULL; + if (hair->need_attribute(scene, ATTR_STD_VERTEX_NORMAL)) { + /* Get geometry normals. */ + float3 *attr_normal = hair->attributes.add(ATTR_STD_VERTEX_NORMAL)->data_float3(); + int i = 0; + for (BL::FloatVectorValueReadOnly &normal : b_curves.normals) { + attr_normal[i++] = get_float3(normal.vector()); + } + } if (hair->need_attribute(scene, ATTR_STD_CURVE_INTERCEPT)) { attr_intercept = hair->attributes.add(ATTR_STD_CURVE_INTERCEPT)->data_float(); } diff --git a/intern/cycles/blender/shader.cpp b/intern/cycles/blender/shader.cpp index 5f9af906469..28f88276077 100644 --- a/intern/cycles/blender/shader.cpp +++ b/intern/cycles/blender/shader.cpp @@ -624,10 +624,14 @@ static ShaderNode *add_node(Scene *scene, else if (b_node.is_a(&RNA_ShaderNodeBsdfHairPrincipled)) { BL::ShaderNodeBsdfHairPrincipled b_principled_hair_node(b_node); PrincipledHairBsdfNode *principled_hair = graph->create_node(); + principled_hair->set_model((NodePrincipledHairModel)get_enum(b_principled_hair_node.ptr, + "model", + NODE_PRINCIPLED_HAIR_MODEL_NUM, + NODE_PRINCIPLED_HAIR_HUANG)); principled_hair->set_parametrization( (NodePrincipledHairParametrization)get_enum(b_principled_hair_node.ptr, "parametrization", - NODE_PRINCIPLED_HAIR_NUM, + NODE_PRINCIPLED_HAIR_PARAMETRIZATION_NUM, NODE_PRINCIPLED_HAIR_REFLECTANCE)); node = principled_hair; } diff --git a/intern/cycles/kernel/CMakeLists.txt b/intern/cycles/kernel/CMakeLists.txt index 898d780c614..86ea761b679 100644 --- a/intern/cycles/kernel/CMakeLists.txt +++ b/intern/cycles/kernel/CMakeLists.txt @@ -139,7 +139,8 @@ set(SRC_KERNEL_CLOSURE_HEADERS closure/bssrdf.h closure/emissive.h closure/volume.h - closure/bsdf_hair_principled.h + closure/bsdf_principled_hair_chiang.h + closure/bsdf_principled_hair_huang.h ) set(SRC_KERNEL_SVM_HEADERS diff --git a/intern/cycles/kernel/closure/bsdf.h b/intern/cycles/kernel/closure/bsdf.h index 4c906ddbab0..455624db919 100644 --- a/intern/cycles/kernel/closure/bsdf.h +++ b/intern/cycles/kernel/closure/bsdf.h @@ -16,7 +16,8 @@ #include "kernel/closure/bsdf_ashikhmin_shirley.h" #include "kernel/closure/bsdf_toon.h" #include "kernel/closure/bsdf_hair.h" -#include "kernel/closure/bsdf_hair_principled.h" +#include "kernel/closure/bsdf_principled_hair_chiang.h" +#include "kernel/closure/bsdf_principled_hair_huang.h" #include "kernel/closure/bssrdf.h" #include "kernel/closure/volume.h" // clang-format on @@ -195,8 +196,11 @@ ccl_device_inline int bsdf_sample(KernelGlobals kg, sc, Ng, sd->wi, rand_xy, eval, wo, pdf, sampled_roughness); *eta = 1.0f; break; - case CLOSURE_BSDF_HAIR_PRINCIPLED_ID: - label = bsdf_principled_hair_sample(kg, sc, sd, rand, eval, wo, pdf, sampled_roughness, eta); + case CLOSURE_BSDF_HAIR_CHIANG_ID: + label = bsdf_hair_chiang_sample(kg, sc, sd, rand, eval, wo, pdf, sampled_roughness, eta); + break; + case CLOSURE_BSDF_HAIR_HUANG_ID: + label = bsdf_hair_huang_sample(kg, sc, sd, rand, eval, wo, pdf, sampled_roughness, eta); break; case CLOSURE_BSDF_SHEEN_ID: label = bsdf_sheen_sample(sc, Ng, sd->wi, rand_xy, eval, wo, pdf); @@ -325,10 +329,15 @@ ccl_device_inline void bsdf_roughness_eta(const KernelGlobals kg, ((ccl_private HairBsdf *)sc)->roughness2); *eta = 1.0f; break; - case CLOSURE_BSDF_HAIR_PRINCIPLED_ID: - alpha = ((ccl_private PrincipledHairBSDF *)sc)->m0_roughness; + case CLOSURE_BSDF_HAIR_CHIANG_ID: + alpha = ((ccl_private ChiangHairBSDF *)sc)->m0_roughness; *roughness = make_float2(alpha, alpha); - *eta = ((ccl_private PrincipledHairBSDF *)sc)->eta; + *eta = ((ccl_private ChiangHairBSDF *)sc)->eta; + break; + case CLOSURE_BSDF_HAIR_HUANG_ID: + alpha = ((ccl_private HuangHairBSDF *)sc)->roughness; + *roughness = make_float2(alpha, alpha); + *eta = ((ccl_private HuangHairBSDF *)sc)->eta; break; case CLOSURE_BSDF_SHEEN_ID: alpha = ((ccl_private SheenBsdf *)sc)->roughness; @@ -405,12 +414,15 @@ ccl_device_inline int bsdf_label(const KernelGlobals kg, case CLOSURE_BSDF_HAIR_TRANSMISSION_ID: label = LABEL_TRANSMIT | LABEL_GLOSSY; break; - case CLOSURE_BSDF_HAIR_PRINCIPLED_ID: + case CLOSURE_BSDF_HAIR_CHIANG_ID: if (bsdf_is_transmission(sc, wo)) label = LABEL_TRANSMIT | LABEL_GLOSSY; else label = LABEL_REFLECT | LABEL_GLOSSY; break; + case CLOSURE_BSDF_HAIR_HUANG_ID: + label = LABEL_REFLECT | LABEL_GLOSSY; + break; case CLOSURE_BSDF_SHEEN_ID: label = LABEL_REFLECT | LABEL_DIFFUSE; break; @@ -493,8 +505,11 @@ ccl_device_inline case CLOSURE_BSDF_GLOSSY_TOON_ID: eval = bsdf_glossy_toon_eval(sc, sd->wi, wo, pdf); break; - case CLOSURE_BSDF_HAIR_PRINCIPLED_ID: - eval = bsdf_principled_hair_eval(kg, sd, sc, wo, pdf); + case CLOSURE_BSDF_HAIR_CHIANG_ID: + eval = bsdf_hair_chiang_eval(kg, sd, sc, wo, pdf); + break; + case CLOSURE_BSDF_HAIR_HUANG_ID: + eval = bsdf_hair_huang_eval(kg, sd, sc, wo, pdf); break; case CLOSURE_BSDF_HAIR_REFLECTION_ID: eval = bsdf_hair_reflection_eval(sc, sd->wi, wo, pdf); @@ -551,8 +566,11 @@ ccl_device void bsdf_blur(KernelGlobals kg, ccl_private ShaderClosure *sc, float case CLOSURE_BSDF_ASHIKHMIN_SHIRLEY_ID: bsdf_ashikhmin_shirley_blur(sc, roughness); break; - case CLOSURE_BSDF_HAIR_PRINCIPLED_ID: - bsdf_principled_hair_blur(sc, roughness); + case CLOSURE_BSDF_HAIR_CHIANG_ID: + bsdf_hair_chiang_blur(sc, roughness); + break; + case CLOSURE_BSDF_HAIR_HUANG_ID: + bsdf_hair_huang_blur(sc, roughness); break; default: break; @@ -581,10 +599,13 @@ ccl_device_inline Spectrum bsdf_albedo(KernelGlobals kg, albedo *= bsdf_microfacet_estimate_albedo( kg, sd, (ccl_private const MicrofacetBsdf *)sc, reflection, transmission); } - else if (sc->type == CLOSURE_BSDF_HAIR_PRINCIPLED_ID) { + else if (sc->type == CLOSURE_BSDF_HAIR_CHIANG_ID) { /* TODO(lukas): Principled Hair could also be split into a glossy and a transmission component, * similar to Glass BSDFs. */ - albedo *= bsdf_principled_hair_albedo(sd, sc); + albedo *= bsdf_hair_chiang_albedo(sd, sc); + } + else if (sc->type == CLOSURE_BSDF_HAIR_HUANG_ID) { + albedo *= bsdf_hair_huang_albedo(sd, sc); } #endif return albedo; diff --git a/intern/cycles/kernel/closure/bsdf_microfacet.h b/intern/cycles/kernel/closure/bsdf_microfacet.h index aa415764344..a1165454088 100644 --- a/intern/cycles/kernel/closure/bsdf_microfacet.h +++ b/intern/cycles/kernel/closure/bsdf_microfacet.h @@ -423,6 +423,12 @@ ccl_device_inline float bsdf_aniso_lambda(float alpha_x, float alpha_y, float3 V return bsdf_lambda_from_sqr_alpha_tan_n(sqr_alpha_tan_n); } +/* Monodirectional shadowing-masking term. */ +template ccl_device_inline float bsdf_G(float alpha2, float cos_N) +{ + return 1.0f / (1.0f + bsdf_lambda(alpha2, cos_N)); +} + /* Combined shadowing-masking term. */ template ccl_device_inline float bsdf_G(float alpha2, float cos_NI, float cos_NO) diff --git a/intern/cycles/kernel/closure/bsdf_hair_principled.h b/intern/cycles/kernel/closure/bsdf_principled_hair_chiang.h similarity index 82% rename from intern/cycles/kernel/closure/bsdf_hair_principled.h rename to intern/cycles/kernel/closure/bsdf_principled_hair_chiang.h index 7b6420b3160..7060f5bc032 100644 --- a/intern/cycles/kernel/closure/bsdf_hair_principled.h +++ b/intern/cycles/kernel/closure/bsdf_principled_hair_chiang.h @@ -1,6 +1,9 @@ /* SPDX-FileCopyrightText: 2018-2022 Blender Foundation * - * SPDX-License-Identifier: Apache-2.0 */ + * SPDX-License-Identifier: Apache-2.0 + * + * This code implements the paper [A practical and controllable hair and fur model for production + * path tracing](https://doi.org/10.1145/2775280.2792559) by Chiang, Matt Jen-Yuan, et al. */ #pragma once @@ -12,12 +15,12 @@ CCL_NAMESPACE_BEGIN -typedef struct PrincipledHairExtra { +typedef struct ChiangHairExtra { /* Geometry data. */ float4 geom; -} PrincipledHairExtra; +} ChiangHairExtra; -typedef struct PrincipledHairBSDF { +typedef struct ChiangHairBSDF { SHADER_CLOSURE_BASE; /* Absorption coefficient. */ @@ -34,13 +37,11 @@ typedef struct PrincipledHairBSDF { float m0_roughness; /* Extra closure. */ - ccl_private PrincipledHairExtra *extra; -} PrincipledHairBSDF; + ccl_private ChiangHairExtra *extra; +} ChiangHairBSDF; -static_assert(sizeof(ShaderClosure) >= sizeof(PrincipledHairBSDF), - "PrincipledHairBSDF is too large!"); -static_assert(sizeof(ShaderClosure) >= sizeof(PrincipledHairExtra), - "PrincipledHairExtra is too large!"); +static_assert(sizeof(ShaderClosure) >= sizeof(ChiangHairBSDF), "ChiangHairBSDF is too large!"); +static_assert(sizeof(ShaderClosure) >= sizeof(ChiangHairExtra), "ChiangHairExtra is too large!"); /* Gives the change in direction in the normal plane for the given angles and p-th-order * scattering. */ @@ -158,10 +159,9 @@ ccl_device_inline float longitudinal_scattering( #ifdef __HAIR__ /* Set up the hair closure. */ -ccl_device int bsdf_principled_hair_setup(ccl_private ShaderData *sd, - ccl_private PrincipledHairBSDF *bsdf) +ccl_device int bsdf_hair_chiang_setup(ccl_private ShaderData *sd, ccl_private ChiangHairBSDF *bsdf) { - bsdf->type = CLOSURE_BSDF_HAIR_PRINCIPLED_ID; + bsdf->type = CLOSURE_BSDF_HAIR_CHIANG_ID; bsdf->v = clamp(bsdf->v, 0.001f, 1.0f); bsdf->s = clamp(bsdf->s, 0.001f, 1.0f); /* Apply Primary Reflection Roughness modifier. */ @@ -252,15 +252,15 @@ ccl_device_inline void hair_alpha_angles(float sin_theta_i, } /* Evaluation function for our shader. */ -ccl_device Spectrum bsdf_principled_hair_eval(KernelGlobals kg, - ccl_private const ShaderData *sd, - ccl_private const ShaderClosure *sc, - const float3 wo, - ccl_private float *pdf) +ccl_device Spectrum bsdf_hair_chiang_eval(KernelGlobals kg, + ccl_private const ShaderData *sd, + ccl_private const ShaderClosure *sc, + const float3 wo, + ccl_private float *pdf) { kernel_assert(isfinite_safe(sd->P) && isfinite_safe(sd->ray_length)); - ccl_private const PrincipledHairBSDF *bsdf = (ccl_private const PrincipledHairBSDF *)sc; + ccl_private const ChiangHairBSDF *bsdf = (ccl_private const ChiangHairBSDF *)sc; const float3 Y = float4_to_float3(bsdf->extra->geom); const float3 X = safe_normalize(sd->dPdu); @@ -334,17 +334,17 @@ ccl_device Spectrum bsdf_principled_hair_eval(KernelGlobals kg, } /* Sampling function for the hair shader. */ -ccl_device int bsdf_principled_hair_sample(KernelGlobals kg, - ccl_private const ShaderClosure *sc, - ccl_private ShaderData *sd, - float3 rand, - ccl_private Spectrum *eval, - ccl_private float3 *wo, - ccl_private float *pdf, - ccl_private float2 *sampled_roughness, - ccl_private float *eta) +ccl_device int bsdf_hair_chiang_sample(KernelGlobals kg, + ccl_private const ShaderClosure *sc, + ccl_private ShaderData *sd, + float3 rand, + ccl_private Spectrum *eval, + ccl_private float3 *wo, + ccl_private float *pdf, + ccl_private float2 *sampled_roughness, + ccl_private float *eta) { - ccl_private PrincipledHairBSDF *bsdf = (ccl_private PrincipledHairBSDF *)sc; + ccl_private ChiangHairBSDF *bsdf = (ccl_private ChiangHairBSDF *)sc; *sampled_roughness = make_float2(bsdf->m0_roughness, bsdf->m0_roughness); *eta = bsdf->eta; @@ -456,28 +456,20 @@ ccl_device int bsdf_principled_hair_sample(KernelGlobals kg, } /* Implements Filter Glossy by capping the effective roughness. */ -ccl_device void bsdf_principled_hair_blur(ccl_private ShaderClosure *sc, float roughness) +ccl_device void bsdf_hair_chiang_blur(ccl_private ShaderClosure *sc, float roughness) { - ccl_private PrincipledHairBSDF *bsdf = (ccl_private PrincipledHairBSDF *)sc; + ccl_private ChiangHairBSDF *bsdf = (ccl_private ChiangHairBSDF *)sc; bsdf->v = fmaxf(roughness, bsdf->v); bsdf->s = fmaxf(roughness, bsdf->s); bsdf->m0_roughness = fmaxf(roughness, bsdf->m0_roughness); } -/* Hair Albedo */ - -ccl_device_inline float bsdf_principled_hair_albedo_roughness_scale( - const float azimuthal_roughness) +/* Hair Albedo. */ +ccl_device Spectrum bsdf_hair_chiang_albedo(ccl_private const ShaderData *sd, + ccl_private const ShaderClosure *sc) { - const float x = azimuthal_roughness; - return (((((0.245f * x) + 5.574f) * x - 10.73f) * x + 2.532f) * x - 0.215f) * x + 5.969f; -} - -ccl_device Spectrum bsdf_principled_hair_albedo(ccl_private const ShaderData *sd, - ccl_private const ShaderClosure *sc) -{ - ccl_private PrincipledHairBSDF *bsdf = (ccl_private PrincipledHairBSDF *)sc; + ccl_private ChiangHairBSDF *bsdf = (ccl_private ChiangHairBSDF *)sc; const float cos_theta_o = cos_from_sin(dot(sd->wi, safe_normalize(sd->dPdu))); const float cos_gamma_o = cos_from_sin(bsdf->extra->geom.w); @@ -488,22 +480,4 @@ ccl_device Spectrum bsdf_principled_hair_albedo(ccl_private const ShaderData *sd return exp(-sqrt(bsdf->sigma) * roughness_scale) + make_spectrum(f); } -ccl_device_inline Spectrum -bsdf_principled_hair_sigma_from_reflectance(const Spectrum color, const float azimuthal_roughness) -{ - const Spectrum sigma = log(color) / - bsdf_principled_hair_albedo_roughness_scale(azimuthal_roughness); - return sigma * sigma; -} - -ccl_device_inline Spectrum bsdf_principled_hair_sigma_from_concentration(const float eumelanin, - const float pheomelanin) -{ - const float3 eumelanin_color = make_float3(0.506f, 0.841f, 1.653f); - const float3 pheomelanin_color = make_float3(0.343f, 0.733f, 1.924f); - - return eumelanin * rgb_to_spectrum(eumelanin_color) + - pheomelanin * rgb_to_spectrum(pheomelanin_color); -} - CCL_NAMESPACE_END diff --git a/intern/cycles/kernel/closure/bsdf_principled_hair_huang.h b/intern/cycles/kernel/closure/bsdf_principled_hair_huang.h new file mode 100644 index 00000000000..d37f6e99ffc --- /dev/null +++ b/intern/cycles/kernel/closure/bsdf_principled_hair_huang.h @@ -0,0 +1,882 @@ +/* SPDX-FileCopyrightText: 2023 Blender Foundation + * + * SPDX-License-Identifier: Apache-2.0 + + * This code implements the paper [A Microfacet-based Hair Scattering + * Model](https://onlinelibrary.wiley.com/doi/full/10.1111/cgf.14588) by Weizhen Huang, Matthias B. + * Hullin and Johannes Hanika. */ + +#pragma once + +#include "kernel/closure/bsdf_util.h" +#include "kernel/sample/lcg.h" +#include "kernel/util/color.h" + +CCL_NAMESPACE_BEGIN + +typedef struct HuangHairExtra { + /* Optional modulation factors. */ + float R, TT, TRT; + + /* Local coordinate system. X is stored as `bsdf->N`.*/ + float3 Y, Z; + + /* Incident direction in local coordinate system. */ + float3 wi; + + /* Projected radius from the view direction. */ + float radius; + + /* Squared Eccentricity. */ + float e2; +} HuangHairExtra; + +typedef struct HuangHairBSDF { + SHADER_CLOSURE_BASE; + + /* Absorption coefficient. */ + Spectrum sigma; + + /* Microfacet distribution roughness. */ + float roughness; + + /* Cuticle tilt angle. */ + float tilt; + + /* Index of refraction. */ + float eta; + + /* The ratio of the minor axis to the major axis. */ + float aspect_ratio; + + /* Azimuthal offset. */ + float h; + + /* Extra closure for optional modulation factors and local coordinate system. */ + ccl_private HuangHairExtra *extra; +} HuangHairBSDF; + +static_assert(sizeof(ShaderClosure) >= sizeof(HuangHairBSDF), "HuangHairBSDF is too large!"); +static_assert(sizeof(ShaderClosure) >= sizeof(HuangHairExtra), "HuangHairExtra is too large!"); + +/* -------------------------------------------------------------------- */ +/** \name Hair coordinate system utils. + * \{ */ + +/* Returns `sin(theta)` of the given direction. */ +ccl_device_inline float sin_theta(const float3 w) +{ + return w.y; +} + +/* Returns `cos(theta)` of the given direction. */ +ccl_device_inline float cos_theta(const float3 w) +{ + return safe_sqrtf(sqr(w.x) + sqr(w.z)); +} + +/* Returns `tan(theta)` of the given direction. */ +ccl_device_inline float tan_theta(const float3 w) +{ + return sin_theta(w) / cos_theta(w); +} + +/* Returns `sin(phi)` and `cos(phi)` of the given direction. */ +ccl_device float sin_phi(const float3 w) +{ + return w.x / cos_theta(w); +} + +ccl_device float2 sincos_phi(const float3 w) +{ + float c = cos_theta(w); + return make_float2(w.x / c, w.z / c); +} + +/* Extract the theta coordinate from the given direction. + * -pi < theta < pi */ +ccl_device_inline float dir_theta(const float3 w) +{ + return atan2f(sin_theta(w), cos_theta(w)); +} + +/* Extract the phi coordinate from the given direction, assuming `phi(wi) == 0`. + * -pi < phi < pi */ +ccl_device_inline float dir_phi(const float3 w) +{ + return atan2f(w.x, w.z); +} + +/* Extract theta and phi coordinates from the given direction, assuming `phi(wi) == 0`. + * -pi/2 < theta < pi/2, -pi < phi < pi */ +ccl_device_inline float2 dir_sph(const float3 w) +{ + return make_float2(dir_theta(w), dir_phi(w)); +} + +/* Conversion between `gamma` and `phi`. Notations see Figure 5 in the paper. */ +ccl_device_inline float to_phi(float gamma, float b) +{ + if (b == 1.0f) { + return gamma; + } + float sin_gamma, cos_gamma; + fast_sincosf(gamma, &sin_gamma, &cos_gamma); + return atan2f(b * sin_gamma, cos_gamma); +} + +ccl_device_inline float to_gamma(float phi, float b) +{ + if (b == 1.0f) { + return phi; + } + float sin_phi, cos_phi; + fast_sincosf(phi, &sin_phi, &cos_phi); + return atan2f(sin_phi, b * cos_phi); +} + +/* Compute the coordinate on the ellipse, given `gamma` and the aspect ratio between the minor axis + * and the major axis. */ +ccl_device_inline float2 to_point(float gamma, float b) +{ + float sin_gamma, cos_gamma; + fast_sincosf(gamma, &sin_gamma, &cos_gamma); + return make_float2(sin_gamma, b * cos_gamma); +} + +/* Compute the vector direction given by `theta` and `gamma`. */ +ccl_device_inline float3 sphg_dir(float theta, float gamma, float b) +{ + float sin_theta, cos_theta, sin_gamma, cos_gamma, sin_phi, cos_phi; + + fast_sincosf(theta, &sin_theta, &cos_theta); + fast_sincosf(gamma, &sin_gamma, &cos_gamma); + + if (b == 1.0f) { + sin_phi = sin_gamma; + cos_phi = cos_gamma; + } + else { + float tan_gamma = sin_gamma / cos_gamma; + float tan_phi = b * tan_gamma; + cos_phi = signf(cos_gamma) * inversesqrtf(sqr(tan_phi) + 1.0f); + sin_phi = cos_phi * tan_phi; + } + return make_float3(sin_phi * cos_theta, sin_theta, cos_phi * cos_theta); +} + +ccl_device_inline float arc_length(float e2, float gamma) +{ + return e2 == 0 ? 1.0f : sqrtf(1.0f - e2 * sqr(sinf(gamma))); +} + +/** \} */ + +#ifdef __HAIR__ +/* Set up the hair closure. */ +ccl_device int bsdf_hair_huang_setup(ccl_private ShaderData *sd, + ccl_private HuangHairBSDF *bsdf, + uint32_t path_flag) +{ + bsdf->type = CLOSURE_BSDF_HAIR_HUANG_ID; + + bsdf->roughness = clamp(bsdf->roughness, 0.001f, 1.0f); + + /* Negate to keep it consistent with principled hair BSDF. */ + bsdf->tilt = -bsdf->tilt; + + /* Compute local frame. The Y axis is aligned with the curve tangent; the X axis is perpendicular + to the ray direction for circular cross-sections, or aligned with the major axis for elliptical + cross-sections. */ + const float3 Y = safe_normalize(sd->dPdu); + const float3 X = safe_normalize(cross(Y, sd->wi)); + + /* h from -1..0..1 means the rays goes from grazing the hair, to hitting it at the center, to + * grazing the other edge. This is the cosine of the angle between `sd->N` and `X`. */ + bsdf->h = (sd->type & PRIMITIVE_CURVE_RIBBON) ? -sd->v : -dot(X, sd->N); + + kernel_assert(fabsf(bsdf->h) < 1.0f + 1e-4f); + kernel_assert(isfinite_safe(bsdf->h)); + + if (bsdf->aspect_ratio != 1.0f && (sd->type & PRIMITIVE_CURVE)) { + /* Align local frame with the curve normal. */ + if (bsdf->aspect_ratio > 1.0f) { + /* Switch major and minor axis. */ + bsdf->aspect_ratio = 1.0f / bsdf->aspect_ratio; + const float3 minor_axis = safe_normalize(cross(sd->dPdu, bsdf->N)); + bsdf->N = safe_normalize(cross(minor_axis, sd->dPdu)); + } + } + else { + /* Align local frame with the ray direction so that `phi_i == 0`. */ + bsdf->N = X; + } + kernel_assert(!is_zero(bsdf->N) && isfinite_safe(bsdf->N)); + + /* Fill extra closure. */ + bsdf->extra->Z = safe_normalize(cross(bsdf->N, sd->dPdu)); + bsdf->extra->Y = safe_normalize(cross(bsdf->extra->Z, bsdf->N)); + + const float3 I = make_float3( + dot(sd->wi, bsdf->N), dot(sd->wi, bsdf->extra->Y), dot(sd->wi, bsdf->extra->Z)); + bsdf->extra->wi = I; + bsdf->extra->e2 = 1.0f - sqr(bsdf->aspect_ratio); + bsdf->extra->radius = bsdf->extra->e2 == 0 ? + 1.0f : + sqrtf(1.0f - bsdf->extra->e2 * sqr(I.x) / (sqr(I.x) + sqr(I.z))); + + /* Treat as transparent material if intersection lies outside of the projected radius. */ + if (fabsf(bsdf->h) >= bsdf->extra->radius) { + /* Remove allocated closures. */ + sd->num_closure--; + sd->num_closure_left += 2; + /* Allocate transparent closure. */ + bsdf_transparent_setup(sd, bsdf->weight, path_flag); + return 0; + } + + return SD_BSDF | SD_BSDF_HAS_EVAL | SD_BSDF_NEEDS_LCG | SD_BSDF_HAS_TRANSMISSION; +} + +#endif /* __HAIR__ */ + +/* Albedo correction, treat as glass. `rough` has already applied square root. */ +ccl_device_forceinline float bsdf_hair_huang_energy_scale(KernelGlobals kg, + float mu, + float rough, + float ior) +{ + const bool inv_table = (ior < 1.0f); + const int ofs = inv_table ? kernel_data.tables.ggx_glass_inv_E : kernel_data.tables.ggx_glass_E; + const float z = sqrtf(fabsf((ior - 1.0f) / (ior + 1.0f))); + return 1.0f / lookup_table_read_3D(kg, rough, mu, z, ofs, 16, 16, 16); +} + +/* Sample microfacets from a tilted mesonormal. */ +ccl_device_inline float3 sample_wh( + KernelGlobals kg, const float roughness, const float3 wi, const float3 wm, const float2 rand) +{ + /* Coordinate transformation for microfacet sampling. */ + float3 s, t; + make_orthonormals(wm, &s, &t); + + const float3 wi_wm = make_float3(dot(wi, s), dot(wi, t), dot(wi, wm)); + + const float3 wh_wm = microfacet_ggx_sample_vndf(wi_wm, roughness, roughness, rand); + + const float3 wh = wh_wm.x * s + wh_wm.y * t + wh_wm.z * wm; + return wh; +} + +/* Check micronormal/mesonormal direct visiblity from direction `v`. */ +ccl_device_inline bool microfacet_visible(const float3 v, const float3 m, const float3 h) +{ + return (dot(v, h) > 0.0f && dot(v, m) > 0.0f); +} + +/* Check micronormal/mesonormal direct visiblity from directions `wi` and `wo`. */ +ccl_device_inline bool microfacet_visible(const float3 wi, + const float3 wo, + const float3 m, + const float3 h) +{ + return microfacet_visible(wi, m, h) && microfacet_visible(wo, m, h); +} + +/* Combined shadowing-masking term divided by the shadowing-masking in the incoming direction. */ +ccl_device_inline float bsdf_Go(float alpha2, float cos_NI, float cos_NO) +{ + const float lambdaI = bsdf_lambda(alpha2, cos_NI); + const float lambdaO = bsdf_lambda(alpha2, cos_NO); + return (1.0f + lambdaI) / (1.0f + lambdaI + lambdaO); +} + +ccl_device Spectrum bsdf_hair_huang_eval_r(KernelGlobals kg, + ccl_private const ShaderClosure *sc, + const float3 wi, + const float3 wo) +{ + ccl_private HuangHairBSDF *bsdf = (ccl_private HuangHairBSDF *)sc; + + if (bsdf->extra->R <= 0.0f) { + return zero_float3(); + } + + /* Get minor axis, assuming major axis is 1. */ + const float b = bsdf->aspect_ratio; + const bool is_circular = (b == 1.0f); + + const float phi_i = is_circular ? 0.0f : dir_phi(wi); + const float phi_o = dir_phi(wo); + + /* Compute visible azimuthal range from incoming and outgoing directions. */ + /* `dot(wi, wmi) > 0` */ + const float tan_tilt = tanf(bsdf->tilt); + float phi_m_max1 = acosf(fmaxf(-tan_tilt * tan_theta(wi), 0.0f)) + phi_i; + if (isnan_safe(phi_m_max1)) { + return zero_spectrum(); + } + float phi_m_min1 = -phi_m_max1 + 2.0f * phi_i; + + /* `dot(wo, wmi) > 0` */ + float phi_m_max2 = acosf(fmaxf(-tan_tilt * tan_theta(wo), 0.0f)) + phi_o; + if (isnan_safe(phi_m_max2)) { + return zero_spectrum(); + } + float phi_m_min2 = -phi_m_max2 + 2.0f * phi_o; + + if (!is_circular) { + /* Try to wrap range. */ + if ((phi_m_max2 - phi_m_min1) > M_2PI_F) { + phi_m_min2 -= M_2PI_F; + phi_m_max2 -= M_2PI_F; + } + if ((phi_m_max1 - phi_m_min2) > M_2PI_F) { + phi_m_min1 -= M_2PI_F; + phi_m_max1 -= M_2PI_F; + } + } + + const float phi_m_min = fmaxf(phi_m_min1, phi_m_min2) + 1e-3f; + const float phi_m_max = fminf(phi_m_max1, phi_m_max2) - 1e-3f; + if (phi_m_min > phi_m_max) { + return zero_spectrum(); + } + + const float gamma_m_min = to_gamma(phi_m_min, b); + float gamma_m_max = to_gamma(phi_m_max, b); + if (gamma_m_max < gamma_m_min) { + gamma_m_max += M_2PI_F; + } + + const float3 wh = normalize(wi + wo); + + const float roughness = bsdf->roughness; + const float roughness2 = sqr(roughness); + + /* Maximal sample resolution. */ + float res = roughness * 0.7f; + /* Number of intervals should be even. */ + const size_t intervals = 2 * (size_t)ceilf((gamma_m_max - gamma_m_min) / res * 0.5f); + + /* Modified resolution based on numbers of intervals. */ + res = (gamma_m_max - gamma_m_min) / float(intervals); + + /* Integrate using Composite Simpson's 1/3 rule. */ + float integral = 0.0f; + for (size_t i = 0; i <= intervals; i++) { + const float gamma_m = gamma_m_min + i * res; + const float3 wm = sphg_dir(bsdf->tilt, gamma_m, b); + + if (microfacet_visible(wi, wo, make_float3(wm.x, 0.0f, wm.z), wh)) { + const float weight = (i == 0 || i == intervals) ? 0.5f : (i % 2 + 1); + const float cos_mi = dot(wm, wi); + const float G = bsdf_G(roughness2, cos_mi, dot(wm, wo)); + integral += weight * bsdf_D(roughness2, dot(wm, wh)) * G * + arc_length(bsdf->extra->e2, gamma_m) * + bsdf_hair_huang_energy_scale(kg, cos_mi, sqrtf(roughness), bsdf->eta); + } + } + + integral *= (2.0f / 3.0f * res); + + const float F = fresnel_dielectric_cos(dot(wi, wh), bsdf->eta); + + return make_spectrum(bsdf->extra->R * 0.125f * F * integral / bsdf->extra->radius); +} + +/* Approximate components beyond TRT (starting TRRT) by summing up a geometric series. Attenuations + * are approximated from previous interactions. */ +ccl_device Spectrum bsdf_hair_huang_eval_trrt(const float T, const float R, const Spectrum A) +{ + /* `T` could be zero due to total internal reflection. Clamp to avoid numerical issues. */ + const float T_avg = max(1.0f - R, 1e-5f); + const Spectrum TRRT_avg = T * sqr(R) * T_avg * A * A * A; + return TRRT_avg / (one_spectrum() - A * (1.0f - T_avg)); +} + +/* Evaluate components beyond R using numerical integration. TT and TRT are computed via combined + * Monte Carlo-Simpson integration; components beyond TRRT are integrated via Simpson's method. */ +ccl_device Spectrum bsdf_hair_huang_eval_residual(KernelGlobals kg, + ccl_private const ShaderClosure *sc, + const float3 wi, + const float3 wo, + uint rng_quadrature) +{ + ccl_private HuangHairBSDF *bsdf = (ccl_private HuangHairBSDF *)sc; + + if (bsdf->extra->TT <= 0.0f && bsdf->extra->TRT <= 0.0f) { + return zero_spectrum(); + } + + /* Get minor axis, assuming major axis is 1. */ + const float b = bsdf->aspect_ratio; + const bool is_circular = (b == 1.0f); + + const float phi_i = is_circular ? 0.0f : dir_phi(wi); + + /* Compute visible azimuthal range from the incoming direction. */ + const float tan_tilt = tanf(bsdf->tilt); + const float phi_m_max = acosf(fmaxf(-tan_tilt * tan_theta(wi), 0.0f)) + phi_i; + if (isnan_safe(phi_m_max)) { + /* Early detection of `dot(wi, wmi) < 0`. */ + return zero_spectrum(); + } + const float phi_m_min = -phi_m_max + 2.0f * phi_i; + + if (tan_tilt * tan_theta(wo) < -1.0f) { + /* Early detection of `dot(wo, wmo) < 0`. */ + return zero_spectrum(); + } + + const Spectrum mu_a = bsdf->sigma; + const float eta = bsdf->eta; + const float inv_eta = 1.0f / eta; + + const float gamma_m_min = to_gamma(phi_m_min, b) + 1e-3f; + float gamma_m_max = to_gamma(phi_m_max, b) - 1e-3f; + if (gamma_m_max < gamma_m_min) { + gamma_m_max += M_2PI_F; + } + + const float roughness = bsdf->roughness; + const float roughness2 = sqr(roughness); + const float sqrt_roughness = sqrtf(roughness); + + float res = roughness * 0.8f; + const size_t intervals = 2 * (size_t)ceilf((gamma_m_max - gamma_m_min) / res * 0.5f); + res = (gamma_m_max - gamma_m_min) / intervals; + + Spectrum S_tt = zero_spectrum(); + Spectrum S_trt = zero_spectrum(); + Spectrum S_trrt = zero_spectrum(); + for (size_t i = 0; i <= intervals; i++) { + + const float gamma_mi = gamma_m_min + i * res; + + const float3 wmi = sphg_dir(bsdf->tilt, gamma_mi, b); + const float3 wmi_ = sphg_dir(0.0f, gamma_mi, b); + + /* Sample `wh1`. */ + const float2 sample1 = make_float2(lcg_step_float(&rng_quadrature), + lcg_step_float(&rng_quadrature)); + + const float3 wh1 = sample_wh(kg, roughness, wi, wmi, sample1); + const float cos_hi1 = dot(wi, wh1); + if (!(cos_hi1 > 0)) { + continue; + } + + const float cos_mi1 = dot(wi, wmi); + float cos_theta_t1; + const float T1 = 1.0f - fresnel_dielectric(cos_hi1, eta, &cos_theta_t1); + const float scale1 = bsdf_hair_huang_energy_scale(kg, cos_mi1, sqrt_roughness, eta); + + /* Refraction at the first interface. */ + const float3 wt = refract_angle(wi, wh1, cos_theta_t1, inv_eta); + const float phi_t = dir_phi(wt); + const float gamma_mt = 2.0f * to_phi(phi_t, b) - gamma_mi; + const float3 wmt = sphg_dir(-bsdf->tilt, gamma_mt, b); + const float3 wmt_ = sphg_dir(0.0f, gamma_mt, b); + + const float cos_mo1 = dot(-wt, wmi); + const float cos_mi2 = dot(-wt, wmt); + const float G1o = bsdf_Go(roughness2, cos_mi1, cos_mo1); + if (!microfacet_visible(wi, -wt, wmi, wh1) || !microfacet_visible(wi, -wt, wmi_, wh1)) { + continue; + } + + const float weight = (i == 0 || i == intervals) ? 0.5f : (i % 2 + 1); + + const Spectrum A_t = exp(mu_a / cos_theta(wt) * + (is_circular ? + 2.0f * cosf(gamma_mi - phi_t) : + -len(to_point(gamma_mi, b) - to_point(gamma_mt + M_PI_F, b)))); + + const float scale2 = bsdf_hair_huang_energy_scale(kg, cos_mi2, sqrt_roughness, inv_eta); + + /* TT */ + if (bsdf->extra->TT > 0.0f) { + if (dot(wo, wt) >= inv_eta - 1e-5f) { /* Total internal reflection otherwise. */ + float3 wh2 = -wt + inv_eta * wo; + const float rcp_norm_wh2 = 1.0f / len(wh2); + wh2 *= rcp_norm_wh2; + const float cos_mh2 = dot(wmt, wh2); + if (cos_mh2 >= 0.0f) { /* Microfacet visiblity from macronormal. */ + const float cos_hi2 = dot(-wt, wh2); + const float cos_ho2 = dot(-wo, wh2); + const float cos_mo2 = dot(-wo, wmt); + + const float T2 = (1.0f - fresnel_dielectric_cos(cos_hi2, inv_eta)) * scale2; + const float D2 = bsdf_D(roughness2, cos_mh2); + const float G2 = bsdf_G(roughness2, cos_mi2, cos_mo2); + + const Spectrum result = weight * T1 * scale1 * T2 * D2 * G1o * G2 * A_t / cos_mo1 * + cos_mi1 * cos_hi2 * cos_ho2 * sqr(rcp_norm_wh2); + + if (isfinite_safe(result)) { + S_tt += bsdf->extra->TT * result * arc_length(bsdf->extra->e2, gamma_mt); + } + } + } + } + + /* TRT and beyond. */ + if (bsdf->extra->TRT > 0.0f) { + /* Sample `wh2`. */ + const float2 sample2 = make_float2(lcg_step_float(&rng_quadrature), + lcg_step_float(&rng_quadrature)); + const float3 wh2 = sample_wh(kg, roughness, -wt, wmt, sample2); + const float cos_hi2 = dot(-wt, wh2); + if (!(cos_hi2 > 0)) { + continue; + } + const float R2 = fresnel_dielectric_cos(cos_hi2, inv_eta); + + const float3 wtr = -reflect(wt, wh2); + if (dot(-wtr, wo) < inv_eta - 1e-5f) { + /* Total internal reflection. */ + S_trrt += weight * bsdf_hair_huang_eval_trrt(T1, R2, A_t); + continue; + } + + if (!microfacet_visible(-wt, -wtr, wmt, wh2) || !microfacet_visible(-wt, -wtr, wmt_, wh2)) { + continue; + } + + const float phi_tr = dir_phi(wtr); + const float gamma_mtr = gamma_mi - 2.0f * (to_phi(phi_t, b) - to_phi(phi_tr, b)) + M_PI_F; + const float3 wmtr = sphg_dir(-bsdf->tilt, gamma_mtr, b); + const float3 wmtr_ = sphg_dir(0.0f, gamma_mtr, b); + + float3 wh3 = wtr + inv_eta * wo; + const float rcp_norm_wh3 = 1.0f / len(wh3); + wh3 *= rcp_norm_wh3; + const float cos_mh3 = dot(wmtr, wh3); + if (cos_mh3 < 0.0f || !microfacet_visible(wtr, -wo, wmtr, wh3) || + !microfacet_visible(wtr, -wo, wmtr_, wh3)) + { + S_trrt += weight * bsdf_hair_huang_eval_trrt(T1, R2, A_t); + continue; + } + + const float cos_hi3 = dot(wh3, wtr); + const float cos_ho3 = dot(wh3, -wo); + const float cos_mi3 = dot(wmtr, wtr); + + const float T3 = (1.0f - fresnel_dielectric_cos(cos_hi3, inv_eta)) * + bsdf_hair_huang_energy_scale(kg, cos_mi3, sqrt_roughness, inv_eta); + const float D3 = bsdf_D(roughness2, cos_mh3); + + const Spectrum A_tr = exp(mu_a / cos_theta(wtr) * + -(is_circular ? + 2.0f * fabsf(cosf(phi_tr - gamma_mt)) : + len(to_point(gamma_mtr, b) - to_point(gamma_mt, b)))); + + const float cos_mo2 = dot(wmt, -wtr); + const float G2o = bsdf_Go(roughness2, cos_mi2, cos_mo2); + const float G3 = bsdf_G(roughness2, cos_mi3, dot(wmtr, -wo)); + + const Spectrum result = weight * T1 * scale1 * R2 * scale2 * T3 * D3 * G1o * G2o * G3 * A_t * + A_tr / (cos_mo1 * cos_mo2) * cos_mi1 * cos_mi2 * cos_hi3 * cos_ho3 * + sqr(rcp_norm_wh3); + + if (isfinite_safe(result)) { + S_trt += bsdf->extra->TRT * result * arc_length(bsdf->extra->e2, gamma_mtr); + } + + S_trrt += weight * bsdf_hair_huang_eval_trrt(T1, R2, A_t); + } + } + + /* TRRT+ terms, following the approach in [A practical and controllable hair and fur model for + * production path tracing](https://doi.org/10.1145/2775280.2792559) by Chiang, Matt Jen-Yuan, et + * al. */ + const float M = longitudinal_scattering( + sin_theta(wi), cos_theta(wi), sin_theta(wo), cos_theta(wo), 4.0f * bsdf->roughness); + const float N = M_1_2PI_F; + + return ((S_tt + S_trt) * sqr(inv_eta) / bsdf->extra->radius + S_trrt * M * N * M_2_PI_F) * res / + 3.0f; +} + +ccl_device int bsdf_hair_huang_sample(const KernelGlobals kg, + ccl_private const ShaderClosure *sc, + ccl_private ShaderData *sd, + float3 rand, + ccl_private Spectrum *eval, + ccl_private float3 *wo, + ccl_private float *pdf, + ccl_private float2 *sampled_roughness, + ccl_private float *eta) +{ + ccl_private HuangHairBSDF *bsdf = (ccl_private HuangHairBSDF *)sc; + + const float roughness = bsdf->roughness; + *sampled_roughness = make_float2(roughness, roughness); + *eta = bsdf->eta; + + kernel_assert(fabsf(bsdf->h) < bsdf->extra->radius); + + /* Generate samples. */ + float sample_lobe = rand.x; + const float sample_h = rand.y; + const float2 sample_h1 = make_float2(rand.z, lcg_step_float(&sd->lcg_state)); + const float2 sample_h2 = make_float2(lcg_step_float(&sd->lcg_state), + lcg_step_float(&sd->lcg_state)); + const float2 sample_h3 = make_float2(lcg_step_float(&sd->lcg_state), + lcg_step_float(&sd->lcg_state)); + + /* Get `wi` in local coordinate. */ + const float3 wi = bsdf->extra->wi; + + const float2 sincos_phi_i = sincos_phi(wi); + const float sin_phi_i = sincos_phi_i.x; + const float cos_phi_i = sincos_phi_i.y; + + /* Get minor axis, assuming major axis is 1. */ + const float b = bsdf->aspect_ratio; + const bool is_circular = (b == 1.0f); + + const float h = sample_h * 2.0f - 1.0f; + const float gamma_mi = is_circular ? + asinf(h) : + atan2f(cos_phi_i, -b * sin_phi_i) - + acosf(h * bsdf->extra->radius * + inversesqrtf(sqr(cos_phi_i) + sqr(b * sin_phi_i))); + + /* Macronormal. */ + const float3 wmi_ = sphg_dir(0, gamma_mi, b); + + /* Mesonormal. */ + float st, ct; + fast_sincosf(bsdf->tilt, &st, &ct); + const float3 wmi = make_float3(wmi_.x * ct, st, wmi_.z * ct); + const float cos_mi1 = dot(wmi, wi); + + if (cos_mi1 < 0.0f || dot(wmi_, wi) < 0.0f) { + /* Macro/mesonormal invisible. */ + *pdf = 0.0f; + return LABEL_NONE; + } + + /* Sample R lobe. */ + const float roughness2 = sqr(roughness); + const float sqrt_roughness = sqrtf(roughness); + const float3 wh1 = sample_wh(kg, roughness, wi, wmi, sample_h1); + const float3 wr = -reflect(wi, wh1); + + /* Ensure that this is a valid sample. */ + if (!microfacet_visible(wi, wmi_, wh1)) { + *pdf = 0.0f; + return LABEL_NONE; + } + + float cos_theta_t1; + const float R1 = fresnel_dielectric(dot(wi, wh1), *eta, &cos_theta_t1); + const float scale1 = bsdf_hair_huang_energy_scale(kg, cos_mi1, sqrt_roughness, bsdf->eta); + const float R = bsdf->extra->R * R1 * scale1 * microfacet_visible(wr, wmi_, wh1) * + bsdf_Go(roughness2, cos_mi1, dot(wmi, wr)); + + /* Sample TT lobe. */ + const float inv_eta = 1.0f / bsdf->eta; + const float3 wt = refract_angle(wi, wh1, cos_theta_t1, inv_eta); + const float phi_t = dir_phi(wt); + + const float gamma_mt = 2.0f * to_phi(phi_t, b) - gamma_mi; + const float3 wmt = sphg_dir(-bsdf->tilt, gamma_mt, b); + const float3 wmt_ = sphg_dir(0.0f, gamma_mt, b); + + const float3 wh2 = sample_wh(kg, roughness, -wt, wmt, sample_h2); + + const float3 wtr = -reflect(wt, wh2); + + float3 wh3, wtt, wtrt, wmtr, wtrrt; + Spectrum TT = zero_spectrum(); + Spectrum TRT = zero_spectrum(); + Spectrum TRRT = zero_spectrum(); + const float cos_mi2 = dot(-wt, wmt); + + if (cos_mi2 > 0.0f && microfacet_visible(-wt, wmi_, wh1) && microfacet_visible(-wt, wmt_, wh2)) { + const Spectrum mu_a = bsdf->sigma; + const Spectrum A_t = exp(mu_a / cos_theta(wt) * + (is_circular ? + 2.0f * cosf(phi_t - gamma_mi) : + -len(to_point(gamma_mi, b) - to_point(gamma_mt + M_PI_F, b)))); + + float cos_theta_t2; + const float R2 = fresnel_dielectric(dot(-wt, wh2), inv_eta, &cos_theta_t2); + const float T1 = (1.0f - R1) * scale1 * bsdf_Go(roughness2, cos_mi1, dot(wmi, -wt)); + const float T2 = 1.0f - R2; + const float scale2 = bsdf_hair_huang_energy_scale(kg, cos_mi2, sqrt_roughness, inv_eta); + + wtt = refract_angle(-wt, wh2, cos_theta_t2, *eta); + + if (dot(wmt, -wtt) > 0.0f && cos_theta_t2 != 0.0f && microfacet_visible(-wtt, wmt_, wh2)) { + TT = bsdf->extra->TT * T1 * A_t * T2 * scale2 * bsdf_Go(roughness2, cos_mi2, dot(wmt, -wtt)); + } + + /* Sample TRT lobe. */ + const float phi_tr = dir_phi(wtr); + const float gamma_mtr = gamma_mi - 2.0f * (to_phi(phi_t, b) - to_phi(phi_tr, b)) + M_PI_F; + wmtr = sphg_dir(-bsdf->tilt, gamma_mtr, b); + + wh3 = sample_wh(kg, roughness, wtr, wmtr, sample_h3); + + float cos_theta_t3; + const float R3 = fresnel_dielectric(dot(wtr, wh3), inv_eta, &cos_theta_t3); + + wtrt = refract_angle(wtr, wh3, cos_theta_t3, *eta); + + const float cos_mi3 = dot(wmtr, wtr); + if (cos_mi3 > 0.0f) { + const Spectrum A_tr = exp(mu_a / cos_theta(wtr) * + -(is_circular ? + 2.0f * fabsf(cos(phi_tr - gamma_mt)) : + len(to_point(gamma_mt, b) - to_point(gamma_mtr, b)))); + + const Spectrum TR = T1 * R2 * scale2 * A_t * A_tr * + bsdf_hair_huang_energy_scale(kg, cos_mi3, sqrt_roughness, inv_eta) * + bsdf_Go(roughness2, cos_mi2, dot(wmt, -wtr)); + + const float T3 = 1.0f - R3; + + if (cos_theta_t3 != 0.0f && + microfacet_visible(wtr, -wtrt, make_float3(wmtr.x, 0.0f, wmtr.z), wh3)) + { + TRT = bsdf->extra->TRT * TR * make_spectrum(T3) * + bsdf_Go(roughness2, cos_mi3, dot(wmtr, -wtrt)); + } + + /* Sample TRRT+ terms, following the approach in [A practical and controllable hair and fur + * model for production path tracing](https://doi.org/10.1145/2775280.2792559) by Chiang, + * Matt Jen-Yuan, et al. */ + + /* Sample `theta_o`. */ + const float rand_theta = max(lcg_step_float(&sd->lcg_state), 1e-5f); + const float fac = 1.0f + + bsdf->roughness * + logf(rand_theta + (1.0f - rand_theta) * expf(-2.0f / bsdf->roughness)); + const float sin_theta_o = -fac * sin_theta(wi) + + cos_from_sin(fac) * + cosf(M_2PI_F * lcg_step_float(&sd->lcg_state)) * cos_theta(wi); + const float cos_theta_o = cos_from_sin(sin_theta_o); + + /* Sample `phi_o`. */ + const float phi_o = M_2PI_F * lcg_step_float(&sd->lcg_state); + float sin_phi_o, cos_phi_o; + fast_sincosf(phi_o, &sin_phi_o, &cos_phi_o); + + /* Compute outgoing direction. */ + wtrrt = make_float3(sin_phi_o * cos_theta_o, sin_theta_o, cos_phi_o * cos_theta_o); + + /* Compute residual term by summing up the geometric series `A * T + A^2 * R * T + ...`. + * Attenuations are approximated from previous interactions. */ + const Spectrum A_avg = sqrt(A_t * A_tr); + /* `T` could be zero due to total internal reflection. Clamp to avoid numerical issues. */ + const float T_avg = max(0.5f * (T2 + T3), 1e-5f); + const Spectrum A_res = A_avg * T_avg / (one_spectrum() - A_avg * (1.0f - T_avg)); + + TRRT = TR * R3 * A_res * bsdf_Go(roughness2, cos_mi3, dot(wmtr, -reflect(wtr, wh3))); + } + } + + /* Select lobe based on energy. */ + const float r = R; + const float tt = average(TT); + const float trt = average(TRT); + const float trrt = average(TRRT); + const float total_energy = r + tt + trt + trrt; + + if (total_energy == 0.0f) { + *pdf = 0.0f; + return LABEL_NONE; + } + + float3 local_O; + + sample_lobe *= total_energy; + if (sample_lobe < r) { + local_O = wr; + *eval = make_spectrum(total_energy); + } + else if (sample_lobe < (r + tt)) { + local_O = wtt; + *eval = TT / tt * total_energy; + } + else if (sample_lobe < (r + tt + trt)) { + local_O = wtrt; + *eval = TRT / trt * total_energy; + } + else { + local_O = wtrrt; + *eval = TRRT / trrt * make_spectrum(total_energy); + } + + /* Get local coordinate system. */ + const float3 X = bsdf->N; + const float3 Y = bsdf->extra->Y; + const float3 Z = bsdf->extra->Z; + + /* Transform `wo` to global coordinate system. */ + *wo = local_O.x * X + local_O.y * Y + local_O.z * Z; + + /* Ensure the same pdf is returned for BSDF and emitter sampling. The importance sampling pdf is + * already factored in the value so this value is only used for MIS. */ + *pdf = 1.0f; + + return LABEL_GLOSSY | LABEL_REFLECT; +} + +ccl_device Spectrum bsdf_hair_huang_eval(KernelGlobals kg, + ccl_private const ShaderData *sd, + ccl_private const ShaderClosure *sc, + const float3 wo, + ccl_private float *pdf) +{ + ccl_private HuangHairBSDF *bsdf = (ccl_private HuangHairBSDF *)sc; + + kernel_assert(fabsf(bsdf->h) < bsdf->extra->radius); + + /* Get local coordinate system. */ + const float3 X = bsdf->N; + const float3 Y = bsdf->extra->Y; + const float3 Z = bsdf->extra->Z; + + /* Transform `wi`/`wo` from global coordinate system to local. */ + const float3 local_I = bsdf->extra->wi; + const float3 local_O = make_float3(dot(wo, X), dot(wo, Y), dot(wo, Z)); + + /* TODO: better estimation of the pdf */ + *pdf = 1.0f; + + return (bsdf_hair_huang_eval_r(kg, sc, local_I, local_O) + + bsdf_hair_huang_eval_residual(kg, sc, local_I, local_O, sd->lcg_state)) / + cos_theta(local_I); +} + +/* Implements Filter Glossy by capping the effective roughness. */ +ccl_device void bsdf_hair_huang_blur(ccl_private ShaderClosure *sc, float roughness) +{ + ccl_private HuangHairBSDF *bsdf = (ccl_private HuangHairBSDF *)sc; + + bsdf->roughness = fmaxf(roughness, bsdf->roughness); +} + +/* Hair Albedo. Computed by summing up geometric series, assuming circular cross-section and + * specular reflection. */ +ccl_device Spectrum bsdf_hair_huang_albedo(ccl_private const ShaderData *sd, + ccl_private const ShaderClosure *sc) +{ + ccl_private HuangHairBSDF *bsdf = (ccl_private HuangHairBSDF *)sc; + + const float3 wmi = make_float3(bsdf->h, 0.0f, cos_from_sin(bsdf->h)); + float cos_t; + const float f = fresnel_dielectric(dot(wmi, bsdf->extra->wi), bsdf->eta, &cos_t); + const float3 wt = refract_angle(bsdf->extra->wi, wmi, cos_t, 1.0f / bsdf->eta); + const Spectrum A = exp(2.0f * bsdf->sigma * cos_t / (1.0f - sqr(wt.y))); + + return safe_divide(A - 2.0f * f * A + f, one_spectrum() - f * A); +} + +CCL_NAMESPACE_END diff --git a/intern/cycles/kernel/closure/bsdf_util.h b/intern/cycles/kernel/closure/bsdf_util.h index 7a11f56db03..d26c1f0d6a1 100644 --- a/intern/cycles/kernel/closure/bsdf_util.h +++ b/intern/cycles/kernel/closure/bsdf_util.h @@ -9,6 +9,45 @@ 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) +{ + kernel_assert(!isnan_safe(cos_theta_i)); + + /* Using Snell's law, calculate the squared cosine of the angle between the surface normal and + * the transmitted ray. */ + const float cos_theta_t_sq = 1.0f - (1.0f - sqr(cos_theta_i)) / sqr(eta); + if (cos_theta_t_sq <= 0) { + /* Total internal reflection. */ + return 1.0f; + } + + cos_theta_i = fabsf(cos_theta_i); + /* Relative to the surface normal. */ + const float cos_theta_t = -safe_sqrtf(cos_theta_t_sq); + + if (r_cos_theta_t) { + *r_cos_theta_t = cos_theta_t; + } + + /* 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); + + return 0.5f * (sqr(r_s) + sqr(r_p)); +} + +/* Refract the incident ray, given the cosine of the refraction angle and the relative refractive + * index of the incoming medium w.r.t. the outgoing medium. */ +ccl_device_inline float3 refract_angle(const float3 incident, + const float3 normal, + const float cos_theta_t, + const float inv_eta) +{ + return (inv_eta * dot(normal, incident) + cos_theta_t) * normal - inv_eta * incident; +} + ccl_device float fresnel_dielectric( float eta, const float3 N, const float3 I, ccl_private float3 *T, ccl_private bool *is_inside) { @@ -197,4 +236,30 @@ ccl_device float3 maybe_ensure_valid_specular_reflection(ccl_private ShaderData return ensure_valid_specular_reflection(sd->Ng, sd->wi, N); } +/* Principled Hair albedo and absorption coefficients. */ +ccl_device_inline float bsdf_principled_hair_albedo_roughness_scale( + const float azimuthal_roughness) +{ + const float x = azimuthal_roughness; + return (((((0.245f * x) + 5.574f) * x - 10.73f) * x + 2.532f) * x - 0.215f) * x + 5.969f; +} + +ccl_device_inline Spectrum +bsdf_principled_hair_sigma_from_reflectance(const Spectrum color, const float azimuthal_roughness) +{ + const Spectrum sigma = log(color) / + bsdf_principled_hair_albedo_roughness_scale(azimuthal_roughness); + return sigma * sigma; +} + +ccl_device_inline Spectrum bsdf_principled_hair_sigma_from_concentration(const float eumelanin, + const float pheomelanin) +{ + const float3 eumelanin_color = make_float3(0.506f, 0.841f, 1.653f); + const float3 pheomelanin_color = make_float3(0.343f, 0.733f, 1.924f); + + return eumelanin * rgb_to_spectrum(eumelanin_color) + + pheomelanin * rgb_to_spectrum(pheomelanin_color); +} + CCL_NAMESPACE_END diff --git a/intern/cycles/kernel/film/denoising_passes.h b/intern/cycles/kernel/film/denoising_passes.h index 33a42247522..43afc49ca83 100644 --- a/intern/cycles/kernel/film/denoising_passes.h +++ b/intern/cycles/kernel/film/denoising_passes.h @@ -56,11 +56,16 @@ ccl_device_forceinline void film_write_denoising_features_surface(KernelGlobals } /* All closures contribute to the normal feature, but only diffuse-like ones to the albedo. */ - normal += sc->N * sc->sample_weight; + /* If far-field hair, use fiber tangent as feature instead of normal. */ + normal += (sc->type == CLOSURE_BSDF_HAIR_HUANG_ID ? safe_normalize(sd->dPdu) : sc->N) * + sc->sample_weight; sum_weight += sc->sample_weight; Spectrum closure_albedo = bsdf_albedo(kg, sd, sc, true, true); - if (bsdf_get_specular_roughness_squared(sc) > sqr(0.075f)) { + if (bsdf_get_specular_roughness_squared(sc) > sqr(0.075f) || + sc->type == CLOSURE_BSDF_HAIR_HUANG_ID) + { + /* Far-field hair models "count" as diffuse. */ diffuse_albedo += closure_albedo; sum_nonspecular_weight += sc->sample_weight; } diff --git a/intern/cycles/kernel/osl/closures_setup.h b/intern/cycles/kernel/osl/closures_setup.h index a8cdac2ddec..96514855d49 100644 --- a/intern/cycles/kernel/osl/closures_setup.h +++ b/intern/cycles/kernel/osl/closures_setup.h @@ -818,26 +818,26 @@ ccl_device void osl_closure_hair_transmission_setup( sd->flag |= bsdf_hair_transmission_setup(bsdf); } -ccl_device void osl_closure_principled_hair_setup(KernelGlobals kg, - ccl_private ShaderData *sd, - uint32_t path_flag, - float3 weight, - ccl_private const PrincipledHairClosure *closure, - float3 *layer_albedo) +ccl_device void osl_closure_hair_chiang_setup(KernelGlobals kg, + ccl_private ShaderData *sd, + uint32_t path_flag, + float3 weight, + ccl_private const ChiangHairClosure *closure, + float3 *layer_albedo) { #ifdef __HAIR__ if (osl_closure_skip(kg, sd, path_flag, LABEL_GLOSSY)) { return; } - ccl_private PrincipledHairBSDF *bsdf = (ccl_private PrincipledHairBSDF *)bsdf_alloc( - sd, sizeof(PrincipledHairBSDF), rgb_to_spectrum(weight)); + ccl_private ChiangHairBSDF *bsdf = (ccl_private ChiangHairBSDF *)bsdf_alloc( + sd, sizeof(ChiangHairBSDF), rgb_to_spectrum(weight)); if (!bsdf) { return; } - ccl_private PrincipledHairExtra *extra = (ccl_private PrincipledHairExtra *)closure_alloc_extra( - sd, sizeof(PrincipledHairExtra)); + ccl_private ChiangHairExtra *extra = (ccl_private ChiangHairExtra *)closure_alloc_extra( + sd, sizeof(ChiangHairExtra)); if (!extra) { return; } @@ -852,7 +852,51 @@ ccl_device void osl_closure_principled_hair_setup(KernelGlobals kg, bsdf->extra = extra; - sd->flag |= bsdf_principled_hair_setup(sd, bsdf); + sd->flag |= bsdf_hair_chiang_setup(sd, bsdf); +#endif +} + +ccl_device void osl_closure_hair_huang_setup(KernelGlobals kg, + ccl_private ShaderData *sd, + uint32_t path_flag, + float3 weight, + ccl_private const HuangHairClosure *closure, + float3 *layer_albedo) +{ +#ifdef __HAIR__ + if (osl_closure_skip(kg, sd, path_flag, LABEL_GLOSSY)) { + return; + } + + if (closure->r_lobe <= 0.0f && closure->tt_lobe <= 0.0f && closure->trt_lobe <= 0.0f) { + return; + } + + ccl_private HuangHairBSDF *bsdf = (ccl_private HuangHairBSDF *)bsdf_alloc( + sd, sizeof(HuangHairBSDF), rgb_to_spectrum(weight)); + if (!bsdf) { + return; + } + + ccl_private HuangHairExtra *extra = (ccl_private HuangHairExtra *)closure_alloc_extra( + sd, sizeof(HuangHairExtra)); + if (!extra) { + return; + } + + bsdf->N = closure->N; + bsdf->sigma = closure->sigma; + bsdf->roughness = closure->roughness; + bsdf->tilt = closure->tilt; + bsdf->eta = closure->eta; + bsdf->aspect_ratio = closure->aspect_ratio; + + bsdf->extra = extra; + bsdf->extra->R = closure->r_lobe; + bsdf->extra->TT = closure->tt_lobe; + bsdf->extra->TRT = closure->trt_lobe; + + sd->flag |= bsdf_hair_huang_setup(sd, bsdf, path_flag); #endif } diff --git a/intern/cycles/kernel/osl/closures_template.h b/intern/cycles/kernel/osl/closures_template.h index 099e64ae2b8..589b5a5d374 100644 --- a/intern/cycles/kernel/osl/closures_template.h +++ b/intern/cycles/kernel/osl/closures_template.h @@ -167,15 +167,27 @@ OSL_CLOSURE_STRUCT_BEGIN(HairTransmission, hair_transmission) OSL_CLOSURE_STRUCT_MEMBER(HairReflection, FLOAT, float, offset, NULL) OSL_CLOSURE_STRUCT_END(HairTransmission, hair_transmission) -OSL_CLOSURE_STRUCT_BEGIN(PrincipledHair, principled_hair) - OSL_CLOSURE_STRUCT_MEMBER(PrincipledHair, VECTOR, packed_float3, N, NULL) - OSL_CLOSURE_STRUCT_MEMBER(PrincipledHair, VECTOR, packed_float3, sigma, NULL) - OSL_CLOSURE_STRUCT_MEMBER(PrincipledHair, FLOAT, float, v, NULL) - OSL_CLOSURE_STRUCT_MEMBER(PrincipledHair, FLOAT, float, s, NULL) - OSL_CLOSURE_STRUCT_MEMBER(PrincipledHair, FLOAT, float, m0_roughness, NULL) - OSL_CLOSURE_STRUCT_MEMBER(PrincipledHair, FLOAT, float, alpha, NULL) - OSL_CLOSURE_STRUCT_MEMBER(PrincipledHair, FLOAT, float, eta, NULL) -OSL_CLOSURE_STRUCT_END(PrincipledHair, principled_hair) +OSL_CLOSURE_STRUCT_BEGIN(ChiangHair, hair_chiang) + OSL_CLOSURE_STRUCT_MEMBER(ChiangHair, VECTOR, packed_float3, N, NULL) + OSL_CLOSURE_STRUCT_MEMBER(ChiangHair, VECTOR, packed_float3, sigma, NULL) + OSL_CLOSURE_STRUCT_MEMBER(ChiangHair, FLOAT, float, v, NULL) + OSL_CLOSURE_STRUCT_MEMBER(ChiangHair, FLOAT, float, s, NULL) + OSL_CLOSURE_STRUCT_MEMBER(ChiangHair, FLOAT, float, m0_roughness, NULL) + OSL_CLOSURE_STRUCT_MEMBER(ChiangHair, FLOAT, float, alpha, NULL) + OSL_CLOSURE_STRUCT_MEMBER(ChiangHair, FLOAT, float, eta, NULL) +OSL_CLOSURE_STRUCT_END(ChiangHair, hair_chiang) + +OSL_CLOSURE_STRUCT_BEGIN(HuangHair, hair_huang) + OSL_CLOSURE_STRUCT_MEMBER(HuangHair, VECTOR, packed_float3, N, NULL) + OSL_CLOSURE_STRUCT_MEMBER(HuangHair, VECTOR, packed_float3, sigma, NULL) + OSL_CLOSURE_STRUCT_MEMBER(HuangHair, FLOAT, float, roughness, NULL) + OSL_CLOSURE_STRUCT_MEMBER(HuangHair, FLOAT, float, tilt, NULL) + OSL_CLOSURE_STRUCT_MEMBER(HuangHair, FLOAT, float, eta, NULL) + OSL_CLOSURE_STRUCT_MEMBER(HuangHair, FLOAT, float, aspect_ratio, NULL) + OSL_CLOSURE_STRUCT_MEMBER(HuangHair, FLOAT, float, r_lobe, NULL) + OSL_CLOSURE_STRUCT_MEMBER(HuangHair, FLOAT, float, tt_lobe, NULL) + OSL_CLOSURE_STRUCT_MEMBER(HuangHair, FLOAT, float, trt_lobe, NULL) +OSL_CLOSURE_STRUCT_END(HuangHair, hair_huang) OSL_CLOSURE_STRUCT_BEGIN(VolumeAbsorption, absorption) OSL_CLOSURE_STRUCT_END(VolumeAbsorption, absorption) diff --git a/intern/cycles/kernel/osl/shaders/node_principled_hair_bsdf.osl b/intern/cycles/kernel/osl/shaders/node_principled_hair_bsdf.osl index e9eac0099db..bd7229ddcc6 100644 --- a/intern/cycles/kernel/osl/shaders/node_principled_hair_bsdf.osl +++ b/intern/cycles/kernel/osl/shaders/node_principled_hair_bsdf.osl @@ -30,7 +30,8 @@ shader node_principled_hair_bsdf(color Color = color(0.017513, 0.005763, 0.00205 color Tint = 1.0, color AbsorptionCoefficient = color(0.245531, 0.52, 1.365), normal Normal = Ng, - string parametrization = "Absorption coefficient", + string model = "Huang", + string parametrization = "Direct Coloring", float Offset = radians(2), float Roughness = 0.3, float RadialRoughness = 0.3, @@ -39,6 +40,10 @@ shader node_principled_hair_bsdf(color Color = color(0.017513, 0.005763, 0.00205 float IOR = 1.55, string AttrRandom = "geom:curve_random", float Random = 0.0, + float AspectRatio = 0.85, + float Rlobe = 1.0, + float TTlobe = 1.0, + float TRTlobe = 1.0, output closure color BSDF = 0) { @@ -89,5 +94,15 @@ shader node_principled_hair_bsdf(color Color = color(0.017513, 0.005763, 0.00205 sigma = sigma_from_concentration(0.0, 0.8054375); } - BSDF = principled_hair(Normal, sigma, roughness, radial_roughness, m0_roughness, Offset, IOR); + if (model == "Huang") { + normal major_axis = Normal; + if (AspectRatio != 1.0) { + getattribute("geom:N", major_axis); + } + BSDF = hair_huang( + major_axis, sigma, roughness, Offset, IOR, AspectRatio, Rlobe, TTlobe, TRTlobe); + } + else { + BSDF = hair_chiang(Normal, sigma, roughness, radial_roughness, m0_roughness, Offset, IOR); + } } diff --git a/intern/cycles/kernel/osl/shaders/stdcycles.h b/intern/cycles/kernel/osl/shaders/stdcycles.h index 0556cceca60..ebaf6de8758 100644 --- a/intern/cycles/kernel/osl/shaders/stdcycles.h +++ b/intern/cycles/kernel/osl/shaders/stdcycles.h @@ -39,13 +39,22 @@ closure color hair_reflection(normal N, float roughnessu, float roughnessv, vector T, float offset) BUILTIN; closure color hair_transmission(normal N, float roughnessu, float roughnessv, vector T, float offset) BUILTIN; -closure color principled_hair(normal N, - color sigma, - float roughnessu, - float roughnessv, - float coat, - float alpha, - float eta) BUILTIN; +closure color hair_chiang(normal N, + color sigma, + float roughnessu, + float roughnessv, + float coat, + float alpha, + float eta) BUILTIN; +closure color hair_huang(normal N, + color sigma, + float roughness, + float tilt, + float eta, + float aspect_ratio, + float r_lobe, + float tt_lobe, + float trt_lobe) BUILTIN; // Volume closure color henyey_greenstein(float g) BUILTIN; diff --git a/intern/cycles/kernel/svm/closure.h b/intern/cycles/kernel/svm/closure.h index 68f849ab608..6ba19575ea8 100644 --- a/intern/cycles/kernel/svm/closure.h +++ b/intern/cycles/kernel/svm/closure.h @@ -566,7 +566,8 @@ ccl_device_noinline int svm_node_closure_bsdf(KernelGlobals kg, break; } #ifdef __HAIR__ - case CLOSURE_BSDF_HAIR_PRINCIPLED_ID: { + case CLOSURE_BSDF_HAIR_CHIANG_ID: + case CLOSURE_BSDF_HAIR_HUANG_ID: { uint4 data_node2 = read_node(kg, &offset); uint4 data_node3 = read_node(kg, &offset); uint4 data_node4 = read_node(kg, &offset); @@ -578,18 +579,18 @@ ccl_device_noinline int svm_node_closure_bsdf(KernelGlobals kg, float alpha = stack_load_float_default(stack, offset_ofs, data_node.z); float ior = stack_load_float_default(stack, ior_ofs, data_node.w); - uint coat_ofs, melanin_ofs, melanin_redness_ofs, absorption_coefficient_ofs; + uint tint_ofs, melanin_ofs, melanin_redness_ofs, absorption_coefficient_ofs; svm_unpack_node_uchar4(data_node2.x, - &coat_ofs, + &tint_ofs, &melanin_ofs, &melanin_redness_ofs, &absorption_coefficient_ofs); - uint tint_ofs, random_ofs, random_color_ofs, random_roughness_ofs; + uint shared_ofs1, random_ofs, random_color_ofs, shared_ofs2; svm_unpack_node_uchar4( - data_node3.x, &tint_ofs, &random_ofs, &random_color_ofs, &random_roughness_ofs); + data_node3.x, &shared_ofs1, &random_ofs, &random_color_ofs, &shared_ofs2); - const AttributeDescriptor attr_descr_random = find_attribute(kg, sd, data_node4.y); + const AttributeDescriptor attr_descr_random = find_attribute(kg, sd, data_node2.y); float random = 0.0f; if (attr_descr_random.offset != ATTR_STD_NOT_FOUND) { random = primitive_surface_attribute_float(kg, sd, attr_descr_random, NULL, NULL); @@ -598,83 +599,130 @@ ccl_device_noinline int svm_node_closure_bsdf(KernelGlobals kg, random = stack_load_float_default(stack, random_ofs, data_node3.y); } - ccl_private PrincipledHairBSDF *bsdf = (ccl_private PrincipledHairBSDF *)bsdf_alloc( - sd, sizeof(PrincipledHairBSDF), weight); - if (bsdf) { - ccl_private PrincipledHairExtra *extra = (ccl_private PrincipledHairExtra *) - closure_alloc_extra(sd, sizeof(PrincipledHairExtra)); + /* Random factors range: [-randomization/2, +randomization/2]. */ + float random_roughness = param2; + float factor_random_roughness = 1.0f + 2.0f * (random - 0.5f) * random_roughness; + float roughness = param1 * factor_random_roughness; + float radial_roughness = (type == CLOSURE_BSDF_HAIR_CHIANG_ID) ? + stack_load_float_default(stack, shared_ofs2, data_node4.y) * + factor_random_roughness : + roughness; - if (!extra) + Spectrum sigma; + switch (parametrization) { + case NODE_PRINCIPLED_HAIR_DIRECT_ABSORPTION: { + float3 absorption_coefficient = stack_load_float3(stack, absorption_coefficient_ofs); + sigma = rgb_to_spectrum(absorption_coefficient); break; + } + case NODE_PRINCIPLED_HAIR_PIGMENT_CONCENTRATION: { + float melanin = stack_load_float_default(stack, melanin_ofs, data_node2.z); + float melanin_redness = stack_load_float_default( + stack, melanin_redness_ofs, data_node2.w); - /* Random factors range: [-randomization/2, +randomization/2]. */ - float random_roughness = stack_load_float_default( - stack, random_roughness_ofs, data_node3.w); - float factor_random_roughness = 1.0f + 2.0f * (random - 0.5f) * random_roughness; - float roughness = param1 * factor_random_roughness; - float radial_roughness = param2 * factor_random_roughness; + /* Randomize melanin. */ + float random_color = stack_load_float_default(stack, random_color_ofs, data_node3.z); + random_color = clamp(random_color, 0.0f, 1.0f); + float factor_random_color = 1.0f + 2.0f * (random - 0.5f) * random_color; + melanin *= factor_random_color; - /* Remap Coat value to [0, 100]% of Roughness. */ - float coat = stack_load_float_default(stack, coat_ofs, data_node2.y); - float m0_roughness = 1.0f - clamp(coat, 0.0f, 1.0f); + /* Map melanin 0..inf from more perceptually linear 0..1. */ + melanin = -logf(fmaxf(1.0f - melanin, 0.0001f)); - bsdf->N = maybe_ensure_valid_specular_reflection(sd, N); - bsdf->v = roughness; - bsdf->s = radial_roughness; - bsdf->m0_roughness = m0_roughness; - bsdf->alpha = alpha; - bsdf->eta = ior; - bsdf->extra = extra; + /* Benedikt Bitterli's melanin ratio remapping. */ + float eumelanin = melanin * (1.0f - melanin_redness); + float pheomelanin = melanin * melanin_redness; + Spectrum melanin_sigma = bsdf_principled_hair_sigma_from_concentration(eumelanin, + pheomelanin); - switch (parametrization) { - case NODE_PRINCIPLED_HAIR_DIRECT_ABSORPTION: { - float3 absorption_coefficient = stack_load_float3(stack, absorption_coefficient_ofs); - bsdf->sigma = rgb_to_spectrum(absorption_coefficient); + /* Optional tint. */ + float3 tint = stack_load_float3(stack, tint_ofs); + Spectrum tint_sigma = bsdf_principled_hair_sigma_from_reflectance(rgb_to_spectrum(tint), + radial_roughness); + + sigma = melanin_sigma + tint_sigma; + break; + } + case NODE_PRINCIPLED_HAIR_REFLECTANCE: { + float3 color = stack_load_float3(stack, color_ofs); + sigma = bsdf_principled_hair_sigma_from_reflectance(rgb_to_spectrum(color), + radial_roughness); + break; + } + default: { + /* Fallback to brownish hair, same as defaults for melanin. */ + kernel_assert(!"Invalid Hair parametrization!"); + sigma = bsdf_principled_hair_sigma_from_concentration(0.0f, 0.8054375f); + break; + } + } + + if (type == CLOSURE_BSDF_HAIR_CHIANG_ID) { + ccl_private ChiangHairBSDF *bsdf = (ccl_private ChiangHairBSDF *)bsdf_alloc( + sd, sizeof(ChiangHairBSDF), weight); + if (bsdf) { + ccl_private ChiangHairExtra *extra = (ccl_private ChiangHairExtra *)closure_alloc_extra( + sd, sizeof(ChiangHairExtra)); + + if (!extra) { break; } - case NODE_PRINCIPLED_HAIR_PIGMENT_CONCENTRATION: { - float melanin = stack_load_float_default(stack, melanin_ofs, data_node2.z); - float melanin_redness = stack_load_float_default( - stack, melanin_redness_ofs, data_node2.w); - /* Randomize melanin. */ - float random_color = stack_load_float_default(stack, random_color_ofs, data_node3.z); - random_color = clamp(random_color, 0.0f, 1.0f); - float factor_random_color = 1.0f + 2.0f * (random - 0.5f) * random_color; - melanin *= factor_random_color; + /* Remap Coat value to [0, 100]% of Roughness. */ + float coat = stack_load_float_default(stack, shared_ofs1, data_node3.w); + float m0_roughness = 1.0f - clamp(coat, 0.0f, 1.0f); - /* Map melanin 0..inf from more perceptually linear 0..1. */ - melanin = -logf(fmaxf(1.0f - melanin, 0.0001f)); + bsdf->v = roughness; + bsdf->s = radial_roughness; + bsdf->m0_roughness = m0_roughness; + bsdf->alpha = alpha; + bsdf->eta = ior; + bsdf->extra = extra; + bsdf->sigma = sigma; - /* Benedikt Bitterli's melanin ratio remapping. */ - float eumelanin = melanin * (1.0f - melanin_redness); - float pheomelanin = melanin * melanin_redness; - Spectrum melanin_sigma = bsdf_principled_hair_sigma_from_concentration(eumelanin, - pheomelanin); - - /* Optional tint. */ - float3 tint = stack_load_float3(stack, tint_ofs); - Spectrum tint_sigma = bsdf_principled_hair_sigma_from_reflectance( - rgb_to_spectrum(tint), radial_roughness); - - bsdf->sigma = melanin_sigma + tint_sigma; - break; - } - case NODE_PRINCIPLED_HAIR_REFLECTANCE: { - float3 color = stack_load_float3(stack, color_ofs); - bsdf->sigma = bsdf_principled_hair_sigma_from_reflectance(rgb_to_spectrum(color), - radial_roughness); - break; - } - default: { - /* Fallback to brownish hair, same as defaults for melanin. */ - kernel_assert(!"Invalid Principled Hair parametrization!"); - bsdf->sigma = bsdf_principled_hair_sigma_from_concentration(0.0f, 0.8054375f); - break; - } + sd->flag |= bsdf_hair_chiang_setup(sd, bsdf); + } + } + else { + kernel_assert(type == CLOSURE_BSDF_HAIR_HUANG_ID); + uint R_ofs, TT_ofs, TRT_ofs, unused; + svm_unpack_node_uchar4(data_node4.x, &R_ofs, &TT_ofs, &TRT_ofs, &unused); + float R = stack_load_float_default(stack, R_ofs, data_node4.y); + float TT = stack_load_float_default(stack, TT_ofs, data_node4.z); + float TRT = stack_load_float_default(stack, TRT_ofs, data_node4.w); + if (R <= 0.0f && TT <= 0.0f && TRT <= 0.0f) { + break; } - sd->flag |= bsdf_principled_hair_setup(sd, bsdf); + ccl_private HuangHairBSDF *bsdf = (ccl_private HuangHairBSDF *)bsdf_alloc( + sd, sizeof(HuangHairBSDF), weight); + if (bsdf) { + ccl_private HuangHairExtra *extra = (ccl_private HuangHairExtra *)closure_alloc_extra( + sd, sizeof(HuangHairExtra)); + + if (!extra) { + break; + } + + bsdf->extra = extra; + bsdf->extra->R = fmaxf(0.0f, R); + bsdf->extra->TT = fmaxf(0.0f, TT); + bsdf->extra->TRT = fmaxf(0.0f, TRT); + + bsdf->aspect_ratio = stack_load_float_default(stack, shared_ofs1, data_node3.w); + if (bsdf->aspect_ratio != 1.0f) { + /* Align ellipse major axis with the curve normal direction. */ + const AttributeDescriptor attr_descr_normal = find_attribute(kg, sd, shared_ofs2); + bsdf->N = curve_attribute_float3(kg, sd, attr_descr_normal, NULL, NULL); + } + + bsdf->roughness = roughness; + bsdf->tilt = alpha; + bsdf->eta = ior; + bsdf->sigma = sigma; + + sd->flag |= bsdf_hair_huang_setup(sd, bsdf, path_flag); + } } break; } diff --git a/intern/cycles/kernel/svm/types.h b/intern/cycles/kernel/svm/types.h index bab608fc920..9def5a19fcb 100644 --- a/intern/cycles/kernel/svm/types.h +++ b/intern/cycles/kernel/svm/types.h @@ -390,11 +390,17 @@ typedef enum ShaderType { SHADER_TYPE_BUMP, } ShaderType; +typedef enum NodePrincipledHairModel { + NODE_PRINCIPLED_HAIR_CHIANG = 0, + NODE_PRINCIPLED_HAIR_HUANG = 1, + NODE_PRINCIPLED_HAIR_MODEL_NUM, +} NodePrincipledHairModel; + typedef enum NodePrincipledHairParametrization { NODE_PRINCIPLED_HAIR_REFLECTANCE = 0, NODE_PRINCIPLED_HAIR_PIGMENT_CONCENTRATION = 1, NODE_PRINCIPLED_HAIR_DIRECT_ABSORPTION = 2, - NODE_PRINCIPLED_HAIR_NUM, + NODE_PRINCIPLED_HAIR_PARAMETRIZATION_NUM, } NodePrincipledHairParametrization; typedef enum NodeCombSepColorType { @@ -433,13 +439,14 @@ typedef enum ClosureType { /* Transmission */ CLOSURE_BSDF_MICROFACET_BECKMANN_REFRACTION_ID, CLOSURE_BSDF_MICROFACET_GGX_REFRACTION_ID, - CLOSURE_BSDF_HAIR_PRINCIPLED_ID, CLOSURE_BSDF_HAIR_TRANSMISSION_ID, /* Glass */ CLOSURE_BSDF_MICROFACET_BECKMANN_GLASS_ID, CLOSURE_BSDF_MICROFACET_GGX_GLASS_ID, CLOSURE_BSDF_MICROFACET_MULTI_GGX_GLASS_ID, /* virtual closure */ + CLOSURE_BSDF_HAIR_CHIANG_ID, + CLOSURE_BSDF_HAIR_HUANG_ID, /* Special cases */ CLOSURE_BSDF_TRANSPARENT_ID, @@ -463,12 +470,12 @@ typedef enum ClosureType { } ClosureType; /* watch this, being lazy with memory usage */ -#define CLOSURE_IS_BSDF(type) (type <= CLOSURE_BSDF_TRANSPARENT_ID) +#define CLOSURE_IS_BSDF(type) (type != CLOSURE_NONE_ID && type <= CLOSURE_BSDF_TRANSPARENT_ID) #define CLOSURE_IS_BSDF_DIFFUSE(type) \ (type >= CLOSURE_BSDF_DIFFUSE_ID && type <= CLOSURE_BSDF_TRANSLUCENT_ID) #define CLOSURE_IS_BSDF_GLOSSY(type) \ ((type >= CLOSURE_BSDF_MICROFACET_GGX_ID && type <= CLOSURE_BSDF_HAIR_REFLECTION_ID) || \ - (type == CLOSURE_BSDF_HAIR_PRINCIPLED_ID)) + (type == CLOSURE_BSDF_HAIR_CHIANG_ID) || (type == CLOSURE_BSDF_HAIR_HUANG_ID)) #define CLOSURE_IS_BSDF_TRANSMISSION(type) \ (type >= CLOSURE_BSDF_MICROFACET_BECKMANN_REFRACTION_ID && \ type <= CLOSURE_BSDF_HAIR_TRANSMISSION_ID) @@ -483,7 +490,8 @@ typedef enum ClosureType { type <= CLOSURE_BSDF_MICROFACET_GGX_REFRACTION_ID) || \ (type >= CLOSURE_BSDF_MICROFACET_BECKMANN_GLASS_ID && \ type <= CLOSURE_BSDF_MICROFACET_MULTI_GGX_GLASS_ID)) -#define CLOSURE_IS_BSDF_OR_BSSRDF(type) (type <= CLOSURE_BSSRDF_RANDOM_WALK_FIXED_RADIUS_ID) +#define CLOSURE_IS_BSDF_OR_BSSRDF(type) \ + (type != CLOSURE_NONE_ID && type <= CLOSURE_BSSRDF_RANDOM_WALK_FIXED_RADIUS_ID) #define CLOSURE_IS_BSSRDF(type) \ (type >= CLOSURE_BSSRDF_BURLEY_ID && type <= CLOSURE_BSSRDF_RANDOM_WALK_FIXED_RADIUS_ID) #define CLOSURE_IS_VOLUME(type) \ diff --git a/intern/cycles/scene/attribute.cpp b/intern/cycles/scene/attribute.cpp index 5f4802681d7..231a89bd53a 100644 --- a/intern/cycles/scene/attribute.cpp +++ b/intern/cycles/scene/attribute.cpp @@ -619,6 +619,9 @@ Attribute *AttributeSet::add(AttributeStandard std, ustring name) } else if (geometry->geometry_type == Geometry::HAIR) { switch (std) { + case ATTR_STD_VERTEX_NORMAL: + attr = add(name, TypeDesc::TypeNormal, ATTR_ELEMENT_CURVE_KEY); + break; case ATTR_STD_UV: attr = add(name, TypeFloat2, ATTR_ELEMENT_CURVE); break; diff --git a/intern/cycles/scene/shader_graph.cpp b/intern/cycles/scene/shader_graph.cpp index 0ed1af82b4a..c0d6e626070 100644 --- a/intern/cycles/scene/shader_graph.cpp +++ b/intern/cycles/scene/shader_graph.cpp @@ -1136,8 +1136,10 @@ int ShaderGraph::get_num_closures() * for the volume steps. */ num_closures += MAX_VOLUME_STACK_SIZE; } - else if (closure_type == CLOSURE_BSDF_HAIR_PRINCIPLED_ID) { - num_closures += 4; + else if (closure_type == CLOSURE_BSDF_HAIR_CHIANG_ID || + closure_type == CLOSURE_BSDF_HAIR_HUANG_ID) + { + num_closures += 2; } else { ++num_closures; diff --git a/intern/cycles/scene/shader_nodes.cpp b/intern/cycles/scene/shader_nodes.cpp index 5b3bbf95b5d..6856f9bfec5 100644 --- a/intern/cycles/scene/shader_nodes.cpp +++ b/intern/cycles/scene/shader_nodes.cpp @@ -3402,6 +3402,12 @@ NODE_DEFINE(PrincipledHairBsdfNode) { NodeType *type = NodeType::add("principled_hair_bsdf", create, NodeType::SHADER); + /* Scattering models. */ + static NodeEnum model_enum; + model_enum.insert("Chiang", NODE_PRINCIPLED_HAIR_CHIANG); + model_enum.insert("Huang", NODE_PRINCIPLED_HAIR_HUANG); + SOCKET_ENUM(model, "Model", model_enum, NODE_PRINCIPLED_HAIR_HUANG); + /* Color parametrization specified as enum. */ static NodeEnum parametrization_enum; parametrization_enum.insert("Direct coloring", NODE_PRINCIPLED_HAIR_REFLECTANCE); @@ -3418,6 +3424,8 @@ NODE_DEFINE(PrincipledHairBsdfNode) SOCKET_IN_VECTOR( absorption_coefficient, "Absorption Coefficient", make_float3(0.245531f, 0.52f, 1.365f)); + SOCKET_IN_FLOAT(aspect_ratio, "Aspect Ratio", 0.85f); + SOCKET_IN_FLOAT(offset, "Offset", 2.f * M_PI_F / 180.f); SOCKET_IN_FLOAT(roughness, "Roughness", 0.3f); SOCKET_IN_FLOAT(radial_roughness, "Radial Roughness", 0.3f); @@ -3428,7 +3436,10 @@ NODE_DEFINE(PrincipledHairBsdfNode) SOCKET_IN_FLOAT(random_color, "Random Color", 0.0f); SOCKET_IN_FLOAT(random, "Random", 0.0f); - SOCKET_IN_NORMAL(normal, "Normal", zero_float3(), SocketType::LINK_NORMAL); + SOCKET_IN_FLOAT(R, "R lobe", 1.0f); + SOCKET_IN_FLOAT(TT, "TT lobe", 1.0f); + SOCKET_IN_FLOAT(TRT, "TRT lobe", 1.0f); + SOCKET_IN_FLOAT(surface_mix_weight, "SurfaceMixWeight", 0.0f, SocketType::SVM_INTERNAL); SOCKET_OUT_CLOSURE(BSDF, "BSDF"); @@ -3438,13 +3449,20 @@ NODE_DEFINE(PrincipledHairBsdfNode) PrincipledHairBsdfNode::PrincipledHairBsdfNode() : BsdfBaseNode(get_node_type()) { - closure = CLOSURE_BSDF_HAIR_PRINCIPLED_ID; + closure = CLOSURE_BSDF_HAIR_HUANG_ID; } -/* Enable retrieving Hair Info -> Random if Random isn't linked. */ void PrincipledHairBsdfNode::attributes(Shader *shader, AttributeRequestSet *attributes) { + if (model == NODE_PRINCIPLED_HAIR_HUANG) { + /* Make sure we have the normal for elliptical cross section tracking. */ + if (aspect_ratio != 1.0f || input("Aspect Ratio")->link) { + attributes->add(ATTR_STD_VERTEX_NORMAL); + } + } + if (!input("Random")->link) { + /* Enable retrieving Hair Info -> Random if Random isn't linked. */ attributes->add(ATTR_STD_CURVE_RANDOM); } ShaderNode::attributes(shader, attributes); @@ -3453,6 +3471,9 @@ void PrincipledHairBsdfNode::attributes(Shader *shader, AttributeRequestSet *att /* Prepares the input data for the SVM shader. */ void PrincipledHairBsdfNode::compile(SVMCompiler &compiler) { + closure = (model == NODE_PRINCIPLED_HAIR_HUANG) ? CLOSURE_BSDF_HAIR_HUANG_ID : + CLOSURE_BSDF_HAIR_CHIANG_ID; + compiler.add_node(NODE_CLOSURE_SET_WEIGHT, one_float3()); ShaderInput *roughness_in = input("Roughness"); @@ -3461,10 +3482,17 @@ void PrincipledHairBsdfNode::compile(SVMCompiler &compiler) ShaderInput *offset_in = input("Offset"); ShaderInput *coat_in = input("Coat"); ShaderInput *ior_in = input("IOR"); + ShaderInput *melanin_in = input("Melanin"); ShaderInput *melanin_redness_in = input("Melanin Redness"); ShaderInput *random_color_in = input("Random Color"); + ShaderInput *R_in = input("R lobe"); + ShaderInput *TT_in = input("TT lobe"); + ShaderInput *TRT_in = input("TRT lobe"); + + ShaderInput *aspect_ratio_in = input("Aspect Ratio"); + int color_ofs = compiler.stack_assign(input("Color")); int tint_ofs = compiler.stack_assign(input("Tint")); int absorption_coefficient_ofs = compiler.stack_assign(input("Absorption Coefficient")); @@ -3472,7 +3500,6 @@ void PrincipledHairBsdfNode::compile(SVMCompiler &compiler) int roughness_ofs = compiler.stack_assign_if_linked(roughness_in); int radial_roughness_ofs = compiler.stack_assign_if_linked(radial_roughness_in); - int normal_ofs = compiler.stack_assign_if_linked(input("Normal")); int offset_ofs = compiler.stack_assign_if_linked(offset_in); int ior_ofs = compiler.stack_assign_if_linked(ior_in); @@ -3493,41 +3520,56 @@ void PrincipledHairBsdfNode::compile(SVMCompiler &compiler) NODE_CLOSURE_BSDF, /* Socket IDs can be packed 4 at a time into a single data packet */ compiler.encode_uchar4( - closure, roughness_ofs, radial_roughness_ofs, compiler.closure_mix_weight_offset()), + closure, roughness_ofs, random_roughness_ofs, compiler.closure_mix_weight_offset()), /* The rest are stored as unsigned integers */ __float_as_uint(roughness), - __float_as_uint(radial_roughness)); + __float_as_uint(random_roughness)); + /* data node */ - compiler.add_node(normal_ofs, + compiler.add_node(SVM_STACK_INVALID, compiler.encode_uchar4(offset_ofs, ior_ofs, color_ofs, parametrization), __float_as_uint(offset), __float_as_uint(ior)); + /* data node 2 */ compiler.add_node(compiler.encode_uchar4( - coat_ofs, melanin_ofs, melanin_redness_ofs, absorption_coefficient_ofs), - __float_as_uint(coat), + tint_ofs, melanin_ofs, melanin_redness_ofs, absorption_coefficient_ofs), + attr_random, __float_as_uint(melanin), __float_as_uint(melanin_redness)); /* data node 3 */ - compiler.add_node( - compiler.encode_uchar4(tint_ofs, random_in_ofs, random_color_ofs, random_roughness_ofs), - __float_as_uint(random), - __float_as_uint(random_color), - __float_as_uint(random_roughness)); + if (model == NODE_PRINCIPLED_HAIR_HUANG) { + compiler.add_node(compiler.encode_uchar4(compiler.stack_assign_if_linked(aspect_ratio_in), + random_in_ofs, + random_color_ofs, + compiler.attribute(ATTR_STD_VERTEX_NORMAL)), + __float_as_uint(random), + __float_as_uint(random_color), + __float_as_uint(aspect_ratio)); + } + else { + compiler.add_node( + compiler.encode_uchar4(coat_ofs, random_in_ofs, random_color_ofs, radial_roughness_ofs), + __float_as_uint(random), + __float_as_uint(random_color), + __float_as_uint(coat)); + } /* data node 4 */ - compiler.add_node( - compiler.encode_uchar4( - SVM_STACK_INVALID, SVM_STACK_INVALID, SVM_STACK_INVALID, SVM_STACK_INVALID), - attr_random, - SVM_STACK_INVALID, - SVM_STACK_INVALID); + compiler.add_node(compiler.encode_uchar4(compiler.stack_assign_if_linked(R_in), + compiler.stack_assign_if_linked(TT_in), + compiler.stack_assign_if_linked(TRT_in), + SVM_STACK_INVALID), + __float_as_uint(model == NODE_PRINCIPLED_HAIR_HUANG ? R : radial_roughness), + __float_as_uint(TT), + __float_as_uint(TRT)); } /* Prepares the input data for the OSL shader. */ void PrincipledHairBsdfNode::compile(OSLCompiler &compiler) { + compiler.parameter(this, "model"); compiler.parameter(this, "parametrization"); compiler.add(this, "node_principled_hair_bsdf"); } diff --git a/intern/cycles/scene/shader_nodes.h b/intern/cycles/scene/shader_nodes.h index 12ed1ad657f..61e4f7d5df3 100644 --- a/intern/cycles/scene/shader_nodes.h +++ b/intern/cycles/scene/shader_nodes.h @@ -857,12 +857,22 @@ class PrincipledHairBsdfNode : public BsdfBaseNode { /* Absorption coefficient (unfiltered). */ NODE_SOCKET_API(float3, absorption_coefficient) - NODE_SOCKET_API(float3, normal) + /* Aspect Ratio. */ + NODE_SOCKET_API(float, aspect_ratio) + + /* Optional modulation factors for the lobes. */ + NODE_SOCKET_API(float, R) + NODE_SOCKET_API(float, TT) + NODE_SOCKET_API(float, TRT) + + /* Weight for mix shader. */ NODE_SOCKET_API(float, surface_mix_weight) /* If linked, here will be the given random number. */ NODE_SOCKET_API(float, random) /* Selected coloring parametrization. */ NODE_SOCKET_API(NodePrincipledHairParametrization, parametrization) + /* Selected scattering model (near-/far-field). */ + NODE_SOCKET_API(NodePrincipledHairModel, model) }; class HairBsdfNode : public BsdfNode { diff --git a/source/blender/blenkernel/intern/node.cc b/source/blender/blenkernel/intern/node.cc index 838483ce2b5..67ce4138167 100644 --- a/source/blender/blenkernel/intern/node.cc +++ b/source/blender/blenkernel/intern/node.cc @@ -556,6 +556,14 @@ void ntreeBlendWrite(BlendWriter *writer, bNodeTree *ntree) BKE_id_blend_write(writer, &ntree->id); for (bNode *node : ntree->all_nodes()) { + if (ntree->type == NTREE_SHADER && node->type == SH_NODE_BSDF_HAIR_PRINCIPLED) { + /* For Principeld Hair BSDF, also write to `node->custom1` for forward compatibility, because + * prior to 4.0 `node->custom1` was used for color parametrization instead of + * `node->storage->parametrization`. */ + NodeShaderHairPrincipled *data = static_cast(node->storage); + node->custom1 = data->parametrization; + } + BLO_write_struct(writer, bNode, node); if (node->prop) { diff --git a/source/blender/blenloader/intern/versioning_400.cc b/source/blender/blenloader/intern/versioning_400.cc index 378837b5712..a386ab6401e 100644 --- a/source/blender/blenloader/intern/versioning_400.cc +++ b/source/blender/blenloader/intern/versioning_400.cc @@ -404,6 +404,21 @@ static void version_principled_bsdf_sheen(bNodeTree *ntree) } } +/* Replace old Principled Hair BSDF as a variant in the new Principled Hair BSDF. */ +static void version_replace_principled_hair_model(bNodeTree *ntree) +{ + LISTBASE_FOREACH (bNode *, node, &ntree->nodes) { + if (node->type != SH_NODE_BSDF_HAIR_PRINCIPLED) { + continue; + } + NodeShaderHairPrincipled *data = MEM_cnew(__func__); + data->model = SHD_PRINCIPLED_HAIR_CHIANG; + data->parametrization = node->custom1; + + node->storage = data; + } +} + void blo_do_versions_400(FileData *fd, Library * /*lib*/, Main *bmain) { if (!MAIN_VERSION_FILE_ATLEAST(bmain, 400, 1)) { @@ -673,6 +688,15 @@ void blo_do_versions_400(FileData *fd, Library * /*lib*/, Main *bmain) { /* Keep this block, even when empty. */ + if (!DNA_struct_find(fd->filesdna, "NodeShaderHairPrincipled")) { + FOREACH_NODETREE_BEGIN (bmain, ntree, id) { + if (ntree->type == NTREE_SHADER) { + version_replace_principled_hair_model(ntree); + } + } + FOREACH_NODETREE_END; + } + if (!DNA_struct_elem_find(fd->filesdna, "LightProbe", "float", "grid_flag")) { LISTBASE_FOREACH (LightProbe *, lightprobe, &bmain->lightprobes) { /* Keep old behavior of baking the whole lighting. */ diff --git a/source/blender/gpu/shaders/material/gpu_shader_material_hair.glsl b/source/blender/gpu/shaders/material/gpu_shader_material_hair.glsl index 1e7d8124700..c02de23df67 100644 --- a/source/blender/gpu/shaders/material/gpu_shader_material_hair.glsl +++ b/source/blender/gpu/shaders/material/gpu_shader_material_hair.glsl @@ -38,6 +38,10 @@ void node_bsdf_hair_principled(vec4 color, float coat, float ior, float offset, + float aspect_ratio, + float R, + float TT, + float TRT, float random_color, float random_roughness, float random, diff --git a/source/blender/makesdna/DNA_node_types.h b/source/blender/makesdna/DNA_node_types.h index 4d16ea1bc7c..4dc8fdff619 100644 --- a/source/blender/makesdna/DNA_node_types.h +++ b/source/blender/makesdna/DNA_node_types.h @@ -1286,6 +1286,12 @@ typedef struct NodeShaderPrincipled { char _pad[3]; } NodeShaderPrincipled; +typedef struct NodeShaderHairPrincipled { + short model; + short parametrization; + char _pad[4]; +} NodeShaderHairPrincipled; + /** TEX_output. */ typedef struct TexNodeOutput { char name[64]; @@ -1917,7 +1923,13 @@ enum { SHD_HAIR_TRANSMISSION = 1, }; -/* principled hair parametrization */ +/* principled hair models */ +enum { + SHD_PRINCIPLED_HAIR_CHIANG = 0, + SHD_PRINCIPLED_HAIR_HUANG = 1, +}; + +/* principled hair color parametrization */ enum { SHD_PRINCIPLED_HAIR_REFLECTANCE = 0, SHD_PRINCIPLED_HAIR_PIGMENT_CONCENTRATION = 1, diff --git a/source/blender/makesrna/intern/rna_nodetree.cc b/source/blender/makesrna/intern/rna_nodetree.cc index ab3f6b96c91..56d9c6da11a 100644 --- a/source/blender/makesrna/intern/rna_nodetree.cc +++ b/source/blender/makesrna/intern/rna_nodetree.cc @@ -4280,7 +4280,24 @@ static const EnumPropertyItem node_hair_items[] = { {0, nullptr, 0, nullptr, nullptr}, }; -static const EnumPropertyItem node_principled_hair_items[] = { +static const EnumPropertyItem node_principled_hair_model_items[] = { + {SHD_PRINCIPLED_HAIR_CHIANG, + "CHIANG", + 0, + "Chiang", + "Near-field hair scattering model by Chiang et. al 2016, suitable for close-up looks, but is " + "more noisy when viewing from a distance"}, + {SHD_PRINCIPLED_HAIR_HUANG, + "HUANG", + 0, + "Huang", + "Far-field hair scattering model by Huang et. al 2022, suitable for viewing from a distance, " + "supports elliptical cross-sections and has more precise highlight in forward scattering " + "directions"}, + {0, nullptr, 0, nullptr, nullptr}, +}; + +static const EnumPropertyItem node_principled_hair_parametrization_items[] = { {SHD_PRINCIPLED_HAIR_DIRECT_ABSORPTION, "ABSORPTION", 0, @@ -4291,8 +4308,8 @@ static const EnumPropertyItem node_principled_hair_items[] = { "MELANIN", 0, "Melanin Concentration", - "Define the melanin concentrations below to get the most realistic-looking hair " - "(you can get the concentrations for different types of hair online)"}, + "Define the melanin concentrations below to get the most realistic-looking hair (you can get " + "the concentrations for different types of hair online)"}, {SHD_PRINCIPLED_HAIR_REFLECTANCE, "COLOR", 0, @@ -5735,20 +5752,29 @@ static void def_hair(StructRNA *srna) RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_update"); } -/* RNA initialization for the custom property. */ +/* RNA initialization for the custom properties. */ static void def_hair_principled(StructRNA *srna) { PropertyRNA *prop; - prop = RNA_def_property(srna, "parametrization", PROP_ENUM, PROP_NONE); - RNA_def_property_enum_sdna(prop, nullptr, "custom1"); - RNA_def_property_ui_text( - prop, "Color Parametrization", "Select the shader's color parametrization"); - RNA_def_property_enum_items(prop, node_principled_hair_items); - RNA_def_property_enum_default(prop, SHD_PRINCIPLED_HAIR_REFLECTANCE); + RNA_def_struct_sdna_from(srna, "NodeShaderHairPrincipled", "storage"); + + prop = RNA_def_property(srna, "model", PROP_ENUM, PROP_NONE); + RNA_def_property_enum_sdna(prop, nullptr, "model"); + RNA_def_property_ui_text(prop, "Scattering model", "Select from Chiang or Huang model"); + RNA_def_property_enum_items(prop, node_principled_hair_model_items); + RNA_def_property_enum_default(prop, SHD_PRINCIPLED_HAIR_HUANG); /* Upon editing, update both the node data AND the UI representation */ /* (This effectively shows/hides the relevant sockets) */ RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_ShaderNode_socket_update"); + + prop = RNA_def_property(srna, "parametrization", PROP_ENUM, PROP_NONE); + RNA_def_property_enum_sdna(prop, nullptr, "parametrization"); + RNA_def_property_ui_text( + prop, "Color Parametrization", "Select the shader's color parametrization"); + RNA_def_property_enum_items(prop, node_principled_hair_parametrization_items); + RNA_def_property_enum_default(prop, SHD_PRINCIPLED_HAIR_REFLECTANCE); + RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_ShaderNode_socket_update"); } static void def_sh_uvmap(StructRNA *srna) diff --git a/source/blender/nodes/shader/nodes/node_shader_bsdf_hair_principled.cc b/source/blender/nodes/shader/nodes/node_shader_bsdf_hair_principled.cc index 17e8d9aa0de..01bce462f44 100644 --- a/source/blender/nodes/shader/nodes/node_shader_bsdf_hair_principled.cc +++ b/source/blender/nodes/shader/nodes/node_shader_bsdf_hair_principled.cc @@ -3,6 +3,7 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ #include "node_shader_util.hh" +#include "node_util.hh" #include "UI_interface.hh" #include "UI_resources.hh" @@ -12,65 +13,142 @@ namespace blender::nodes::node_shader_bsdf_hair_principled_cc { /* Color, melanin and absorption coefficient default to approximately same brownish hair. */ static void node_declare(NodeDeclarationBuilder &b) { - b.add_input("Color").default_value({0.017513f, 0.005763f, 0.002059f, 1.0f}); - b.add_input("Melanin").default_value(0.8f).min(0.0f).max(1.0f).subtype(PROP_FACTOR); + b.add_input("Color") + .default_value({0.017513f, 0.005763f, 0.002059f, 1.0f}) + .description("The RGB color of the strand. Only used in Direct Coloring"); + b.add_input("Melanin") + .default_value(0.8f) + .min(0.0f) + .max(1.0f) + .subtype(PROP_FACTOR) + .description("Hair pigment. Specify its absolute quantity between 0 and 1"); b.add_input("Melanin Redness") .default_value(1.0f) .min(0.0f) .max(1.0f) - .subtype(PROP_FACTOR); - b.add_input("Tint").default_value({1.0f, 1.0f, 1.0f, 1.0f}); + .subtype(PROP_FACTOR) + .description( + "Fraction of pheomelanin in melanin, gives yellowish to reddish color, as opposed to " + "the brownish to black color of eumelanin"); + b.add_input("Tint") + .default_value({1.0f, 1.0f, 1.0f, 1.0f}) + .description("Additional color used for dyeing the hair."); b.add_input("Absorption Coefficient") .default_value({0.245531f, 0.52f, 1.365f}) .min(0.0f) - .max(1000.0f); + .max(1000.0f) + .description( + "Specifies energy absorption per unit length as light passes through the hair. A higher " + "value leads to a darker color."); + b.add_input("Aspect Ratio") + .default_value(0.85f) + .min(0.0f) + .max(1.0f) + .subtype(PROP_FACTOR) + .description( + "For elliptical hair cross-section, the aspect ratio is the ratio of the minor axis to " + "the major axis (the major axis is aligned with the curve normal). Recommended values " + "are 0.8~1 for Asian hair, 0.65~0.9 for Caucasian hair, 0.5~0.65 for African hair. Set " + "this to 1 for circular cross-section"); b.add_input("Roughness") .default_value(0.3f) .min(0.0f) .max(1.0f) - .subtype(PROP_FACTOR); + .subtype(PROP_FACTOR) + .description("Hair roughness. A low value leads to a metallic look."); b.add_input("Radial Roughness") .default_value(0.3f) .min(0.0f) .max(1.0f) .subtype(PROP_FACTOR); - b.add_input("Coat").default_value(0.0f).min(0.0f).max(1.0f).subtype(PROP_FACTOR); - b.add_input("IOR").default_value(1.55f).min(0.0f).max(1000.0f); + b.add_input("Coat") + .default_value(0.0f) + .min(0.0f) + .max(1.0f) + .subtype(PROP_FACTOR) + .description( + "Simulate a shiny coat by reducing the roughness to the given factor only for the first " + "light bounce (diffuse). Range [0, 1] is equivalent to a reduction of [0%, 100%] of the " + "original roughness."); + b.add_input("IOR").default_value(1.55f).min(0.0f).max(1000.0f).description( + "Index of refraction determines how much the ray is bent. At 1.0 rays pass straight through " + "like in a transparent material; higher values cause larger deflection in angle. Default " + "value is 1.55 (the IOR of keratin)"); b.add_input("Offset") .default_value(2.0f * float(M_PI) / 180.0f) .min(-M_PI_2) .max(M_PI_2) - .subtype(PROP_ANGLE); + .subtype(PROP_ANGLE) + .description( + "The tilt angle of the cuticle scales (the outermost part of the hair). They are always " + "tilted towards the hair root. The value is usually between 2 and 4 for human hair"); b.add_input("Random Color") .default_value(0.0f) .min(0.0f) .max(1.0f) - .subtype(PROP_FACTOR); + .subtype(PROP_FACTOR) + .description("Vary the melanin concentration for each strand"); b.add_input("Random Roughness") .default_value(0.0f) .min(0.0f) .max(1.0f) - .subtype(PROP_FACTOR); + .subtype(PROP_FACTOR) + .description("Vary roughness values for each strand"); b.add_input("Random").hide_value(); b.add_input("Weight").unavailable(); + b.add_input("Reflection", "R lobe") + .default_value(1.0f) + .min(0.0f) + .max(1.0f) + .subtype(PROP_FACTOR) + .description( + "Optional factor for modulating the first light bounce off the hair surface. The color " + "of this component is always white. Keep this 1.0 for physical correctness"); + b.add_input("Transmission", "TT lobe") + .default_value(1.0f) + .min(0.0f) + .max(1.0f) + .subtype(PROP_FACTOR) + .description( + "Optional factor for modulating the transmission component. Picks up the color of the " + "pigment inside the hair. Keep this 1.0 for physical correctness"); + b.add_input("Secondary Reflection", "TRT lobe") + .default_value(1.0f) + .min(0.0f) + .max(1.0f) + .subtype(PROP_FACTOR) + .description( + "Optional factor for modulating the component which is transmitted into the hair, " + "reflected off the backside of the hair and then transmitted out of the hair. This " + "component is oriented approximately around the incoming direction, and picks up the " + "color of the pigment inside the hair. Keep this 1.0 for physical correctness"); b.add_output("BSDF"); } static void node_shader_buts_principled_hair(uiLayout *layout, bContext * /*C*/, PointerRNA *ptr) { + uiItemR(layout, ptr, "model", UI_ITEM_R_SPLIT_EMPTY_NAME, "", ICON_NONE); uiItemR(layout, ptr, "parametrization", UI_ITEM_R_SPLIT_EMPTY_NAME, "", ICON_NONE); } -/* Initialize the custom Parametrization property to Color. */ +/* Initialize custom properties. */ static void node_shader_init_hair_principled(bNodeTree * /*ntree*/, bNode *node) { - node->custom1 = SHD_PRINCIPLED_HAIR_REFLECTANCE; + NodeShaderHairPrincipled *data = MEM_cnew(__func__); + + data->model = SHD_PRINCIPLED_HAIR_HUANG; + data->parametrization = SHD_PRINCIPLED_HAIR_REFLECTANCE; + + node->storage = data; } -/* Triggers (in)visibility of some sockets when changing Parametrization. */ +/* Triggers (in)visibility of some sockets when changing the parametrization or the model. */ static void node_shader_update_hair_principled(bNodeTree *ntree, bNode *node) { - int parametrization = node->custom1; + NodeShaderHairPrincipled *data = static_cast(node->storage); + + int parametrization = data->parametrization; + int model = data->model; LISTBASE_FOREACH (bNodeSocket *, sock, &node->inputs) { if (STREQ(sock->name, "Color")) { @@ -97,6 +175,20 @@ static void node_shader_update_hair_principled(bNodeTree *ntree, bNode *node) bke::nodeSetSocketAvailability( ntree, sock, parametrization == SHD_PRINCIPLED_HAIR_PIGMENT_CONCENTRATION); } + else if (STREQ(sock->name, "Radial Roughness")) { + bke::nodeSetSocketAvailability(ntree, sock, model == SHD_PRINCIPLED_HAIR_CHIANG); + } + else if (STREQ(sock->name, "Coat")) { + bke::nodeSetSocketAvailability(ntree, sock, model == SHD_PRINCIPLED_HAIR_CHIANG); + } + else if (STREQ(sock->name, "Aspect Ratio")) { + bke::nodeSetSocketAvailability(ntree, sock, model == SHD_PRINCIPLED_HAIR_HUANG); + } + else if (STREQ(sock->name, "Reflection") || STREQ(sock->name, "Transmission") || + STREQ(sock->name, "Secondary Reflection")) + { + bke::nodeSetSocketAvailability(ntree, sock, model == SHD_PRINCIPLED_HAIR_HUANG); + } } } @@ -127,6 +219,8 @@ void register_node_type_sh_bsdf_hair_principled() ntype.initfunc = file_ns::node_shader_init_hair_principled; ntype.updatefunc = file_ns::node_shader_update_hair_principled; ntype.gpu_fn = file_ns::node_shader_gpu_hair_principled; + node_type_storage( + &ntype, "NodeShaderHairPrincipled", node_free_standard_storage, node_copy_standard_storage); nodeRegisterType(&ntype); }