Cycles: Multi-scale Principled Hair Huang Model #116094

Merged
Weizhen Huang merged 4 commits from weizhen/blender:hair-nearfield into main 2024-02-22 18:18:24 +01:00
6 changed files with 97 additions and 13 deletions
Showing only changes of commit e3abd15692 - Show all commits

View File

@ -30,6 +30,9 @@ typedef struct HuangHairExtra {
/* Squared Eccentricity. */
float e2;
/* The projected width of half a pixel at `sd->P` in `h` space. */
float pixel_coverage;
/* Valid integration interval. */
float gamma_m_min, gamma_m_max;
} HuangHairExtra;
@ -181,6 +184,11 @@ ccl_device_inline float arc_length(float e2, float gamma)
return e2 == 0 ? 1.0f : sqrtf(1.0f - e2 * sqr(sinf(gamma)));
}
ccl_device_inline bool is_nearfield(ccl_private const HuangHairBSDF *bsdf)
{
return bsdf->extra->radius > bsdf->extra->pixel_coverage;
}
/** \} */
#ifdef __HAIR__
@ -355,11 +363,12 @@ ccl_device Spectrum bsdf_hair_huang_eval_r(KernelGlobals kg,
}
}
/* Simpson coefficient */
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);
return make_spectrum(bsdf->extra->R * 0.25f * F * integral);
}
/* Approximate components beyond TRT (starting TRRT) by summing up a geometric series. Attenuations
@ -552,8 +561,9 @@ ccl_device Spectrum bsdf_hair_huang_eval_residual(KernelGlobals kg,
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;
const float simpson_coeff = 2.0f / 3.0f * res;
return ((S_tt + S_trt) * sqr(inv_eta) + S_trrt * M * N * M_2_PI_F) * simpson_coeff;
}
ccl_device int bsdf_hair_huang_sample(const KernelGlobals kg,
@ -588,7 +598,9 @@ ccl_device int bsdf_hair_huang_sample(const KernelGlobals kg,
const float b = bsdf->aspect_ratio;
const bool is_circular = (b == 1.0f);
const float h_div_r = sample_h * 2.0f - 1.0f;
/* Sample `h` for farfield model, as the computed intersection might have numerical issues. */
const float h_div_r = is_nearfield(bsdf) ? bsdf->h / bsdf->extra->radius :
(sample_h * 2.0f - 1.0f);
const float gamma_mi = h_to_gamma(h_div_r, b, wi);
/* Macronormal. */
@ -806,20 +818,62 @@ ccl_device Spectrum bsdf_hair_huang_eval(KernelGlobals kg,
/* Early detection of `dot(wi, wmi) < 0`. */
return zero_spectrum();
}
const float r = bsdf->extra->radius;
const float b = bsdf->aspect_ratio;
const float phi_i = (b == 1.0f) ? 0.0f : dir_phi(local_I);
const float gamma_m_min = to_gamma(phi_i - half_span, b);
float gamma_m_min = to_gamma(phi_i - half_span, b);
float gamma_m_max = to_gamma(phi_i + half_span, b);
if (gamma_m_max < gamma_m_min) {
gamma_m_max += M_2PI_F;
}
bsdf->extra->gamma_m_min = gamma_m_min + 1e-3f;
bsdf->extra->gamma_m_max = gamma_m_max - 1e-3f;
/* Prevent numerical issues at the boundary. */
gamma_m_min += 1e-3f;
gamma_m_max -= 1e-3f;
/* Length of the integral interval. */
float dh = 2.0f * r;
if (is_nearfield(bsdf)) {
/* Reduce the integration interval to the subset that's visible to the current pixel.
* Inspired by [An Efficient and Practical Near and Far Field Fur Reflectance Model]
* (https://sites.cs.ucsb.edu/~lingqi/publications/paper_fur2.pdf) by Ling-Qi Yan, Henrik Wann
* Jensen and Ravi Ramamoorthi. */
const float h_max = min(bsdf->h + bsdf->extra->pixel_coverage, r);
const float h_min = max(bsdf->h - bsdf->extra->pixel_coverage, -r);
/* At the boundaries the hair might not cover the whole pixel. */
dh = h_max - h_min;
float nearfield_gamma_min = h_to_gamma(h_max / r, bsdf->aspect_ratio, local_I);
float nearfield_gamma_max = h_to_gamma(h_min / r, bsdf->aspect_ratio, local_I);
if (nearfield_gamma_max < nearfield_gamma_min) {
nearfield_gamma_max += M_2PI_F;
}
/* Wrap range to compute the intersection. */
if ((gamma_m_max - nearfield_gamma_min) > M_2PI_F) {
gamma_m_min -= M_2PI_F;
gamma_m_max -= M_2PI_F;
}
else if ((nearfield_gamma_max - gamma_m_min) > M_2PI_F) {
nearfield_gamma_min -= M_2PI_F;
nearfield_gamma_max -= M_2PI_F;
}
gamma_m_min = fmaxf(gamma_m_min, nearfield_gamma_min);
gamma_m_max = fminf(gamma_m_max, nearfield_gamma_max);
}
bsdf->extra->gamma_m_min = gamma_m_min;
bsdf->extra->gamma_m_max = gamma_m_max;
const float projected_area = cos_theta(local_I) * dh;
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);
projected_area;
}
/* Implements Filter Glossy by capping the effective roughness. */

View File

@ -988,6 +988,21 @@ ccl_device void osl_closure_hair_huang_setup(KernelGlobals kg,
bsdf->extra->TT = closure->tt_lobe;
bsdf->extra->TRT = closure->trt_lobe;
bsdf->extra->pixel_coverage = 1.0f;
/* For camera ray, check if the hair covers more than one pixel, in which case a nearfield model
* is needed to prevent ribbon-like appearance. */
if ((path_flag & PATH_RAY_CAMERA) && (sd->type & PRIMITIVE_CURVE)) {
/* Interpolate radius between curve keys. */
const KernelCurve kcurve = kernel_data_fetch(curves, sd->prim);
const int k0 = kcurve.first_key + PRIMITIVE_UNPACK_SEGMENT(sd->type);
const int k1 = k0 + 1;
const float radius = mix(
kernel_data_fetch(curve_keys, k0).w, kernel_data_fetch(curve_keys, k1).w, sd->u);
bsdf->extra->pixel_coverage = 0.5f * sd->dP / radius;
}
sd->flag |= bsdf_hair_huang_setup(sd, bsdf, path_flag);
#endif
}

View File

@ -790,6 +790,21 @@ ccl_device
bsdf->extra->TT = fmaxf(0.0f, TT);
bsdf->extra->TRT = fmaxf(0.0f, TRT);
bsdf->extra->pixel_coverage = 1.0f;
/* For camera ray, check if the hair covers more than one pixel, in which case a
* nearfield model is needed to prevent ribbon-like appearance. */
if ((path_flag & PATH_RAY_CAMERA) && (sd->type & PRIMITIVE_CURVE)) {
/* Interpolate radius between curve keys. */
const KernelCurve kcurve = kernel_data_fetch(curves, sd->prim);
const int k0 = kcurve.first_key + PRIMITIVE_UNPACK_SEGMENT(sd->type);
const int k1 = k0 + 1;
const float radius = mix(
kernel_data_fetch(curve_keys, k0).w, kernel_data_fetch(curve_keys, k1).w, sd->u);
bsdf->extra->pixel_coverage = 0.5f * sd->dP / radius;
}
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. */

View File

@ -844,7 +844,7 @@ class PrincipledHairBsdfNode : public BsdfBaseNode {
NODE_SOCKET_API(float, random)
/* Selected coloring parametrization. */
NODE_SOCKET_API(NodePrincipledHairParametrization, parametrization)
/* Selected scattering model (near-/far-field). */
/* Selected scattering model (chiang/huang). */
NODE_SOCKET_API(NodePrincipledHairModel, model)
virtual int get_feature()

View File

@ -4120,9 +4120,9 @@ static const EnumPropertyItem node_principled_hair_model_items[] = {
"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"},
"Multi-scale hair scattering model by Huang et al. 2022, suitable for viewing both up close "
"and from a distance, supports elliptical cross-sections and has more precise highlight in "
"forward scattering directions"},
{0, nullptr, 0, nullptr, nullptr},
};

@ -1 +1 @@
Subproject commit 74d32aff48a05236adf61a24d8c1b5b4c7c55097
Subproject commit 4f2e16db7ce3bdea79f63c50a9bd5a645f23037f