// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "PathTracingMaterialCommon.ush" #define PATH_TRACER_HAIR_SHADING_MODEL 1 // 0: zinke diffuse (a simple, cheap model for performance testing) // 1: chiang principled hair (a sophisticated model with specular reflection/transmission) // Shared routines float3x3 GetHairBasis(float3 T_World, float3 V_World) { // a convenient basis for hair shading calculations: // Z Axis = Hair Tangent // Y Axis = point toward the viewer // X Axis = left/right from viewing direction float3 Tx = normalize(cross(T_World, V_World)); float3 Ty = cross(Tx, T_World); return float3x3(Tx, Ty, T_World); } #if PATH_TRACER_HAIR_SHADING_MODEL == 0 float ZinkeHairDiffuse(float3 L) { // Evaluate the zinke diffuse lobe model. This is the far field average of a diffuse (lambertian) cylinder // This was originally derived in: "Light Scattering from Filaments" - Arno Zinke and Andreas Weber - 2007 // The exact form was mentioned in: "Importance Sampling for Physically-Based Hair Fiber Models" - Eugene d’Eon, Steve Marschner, Johannes Hanika - 2013 // const float CosTheta = sqrt(saturate(1 - L.z * L.z)); #if 0 // exact form - Eq(9) from d'Eon et al. const float CosPhi = clamp(L.y / CosTheta, -1.0, 1.0); const float SinPhi = sqrt(1.0 - CosPhi * CosPhi); const float Phi = acos(CosPhi); return ((PI - Phi) * CosPhi + SinPhi) * CosTheta / (4 * PI); #else // optimized fit to avoid acos // error is around ~0.001 const float CosPhiCosTheta = L.y; return 0.0786133 * CosTheta + CosPhiCosTheta * (0.125 + 0.0463867 * CosPhiCosTheta * rcp(CosTheta + 1e-12)); #endif } FMaterialSample Hair_SampleMaterial( float3 V_World, FPathTracingPayload Payload, float3 RandSample) { float3x3 HairBasis = GetHairBasis(Payload.WorldTangent, V_World); // sample a random position across the width of the fiber const float h = 2 * RandSample.x - 1; // create a normal vector in the plane tangent to the fiber const float2 N = float2(h, sqrt(1 - h * h)); // sample a cosine weighted direction const float4 SampledValue = CosineSampleHemisphere(RandSample.yz); // rotate this sampled direction about the (random) normal vector const float3 L = float3(SampledValue.x * N.y + SampledValue.z * N.x, SampledValue.z * N.y - SampledValue.x * N.x, SampledValue.y); // finally transform into world space const float3 L_World = mul(L, HairBasis); const float Pdf = ZinkeHairDiffuse(L); return CreateMaterialSample(L_World, Payload.GetBaseColor(), Pdf, sign(dot(Payload.WorldGeoNormal, L_World)), 1.0, PATHTRACER_SCATTER_DIFFUSE); } FMaterialEval Hair_EvalMaterial( float3 V_World, float3 L_World, FPathTracingPayload Payload, float2 DiffuseSpecularScale ) { const float3x3 HairBasis = GetHairBasis(Payload.WorldTangent, V_World); const float3 L = mul(HairBasis, L_World); const float Pdf = ZinkeHairDiffuse(L); return CreateMaterialEval(Payload.GetBaseColor() * DiffuseSpecularScale.x, Pdf); } #elif PATH_TRACER_HAIR_SHADING_MODEL == 1 // The implementation below is based on the following paper: // "A Practical and Controllable Hair and Fur Model for Production Path Tracing" // Matt Jen-Yuan Chiang, Benedikt Bitterli, Chuck Tappan, Brent Burley // EGSR 2016 // https://media.disneyanimation.com/uploads/production/publication_asset/152/asset/eurographics2016Fur_Smaller.pdf float LogI0(float x) { // I0 grows too quickly to evaluate robustly in single precision arithmetic // instead evaluate the log which is much smoother // The fits below were found using Mathematica and have error below 10^-6 x = abs(x); if (x < 16.315929055555998) // transition point where the two approximations line up { // rational approximation // 9 fmas + 2 mults + 1 div float num = 0.249915 + x * (0.124919 + x * (0.0486428 + x * (0.011457 + x * 0.000647177))); float den = 1 + x * (0.498089 + x * (0.259956 + x * (0.0741939 + x * (0.0134601 + x * 0.000650605)))); return x * x * (num / den); } else { // beyond this point the curve is mostly linear with a very slight offset // 3 fmas + 1 log // TODO: would it be better to use log2 directly and save one mult? can the compiler do this for us? return x - 0.25 * log(-5.68881 + x * (-19.7156 + 39.4779 * x)); } } float FastASin(float x) { // Maximum error: 10^-3.35 // TODO: consider replacing acosFast/asinFast which have approximation error 10^-2.0 (with one less fma, but also with a slightly sub-optimal constant) float a = saturate(abs(x)); float r = (0.5 * PI) + a * (-0.207034 + 0.0531013 * a); r = (0.5 * PI) - r * sqrt(1 - a); return x >= 0 ? r : -r; } float Mp(float SinThetaI, float SinThetaO, float v) { float rv = rcp(v); float CosThetaO2 = saturate(1 - SinThetaO * SinThetaO); float CosThetaI2 = saturate(1 - SinThetaI * SinThetaI); float CosProduct = sqrt(CosThetaO2 * CosThetaI2); // "Path tracing in Production" - Siggraph 2018 Course // "From Bricks to Bunnies - Adapting a Raytracer to New Requirements" - Luke Emrose // https://jo.dreggn.org/path-tracing-in-production/2018/course-notes.pdf // Simplified expression for better numerical stability - Eq(62) return exp(LogI0(CosProduct * rv) - rv * (SinThetaI * SinThetaO + 1) + log(rv) - log(1 - exp(-2 * rv))); } float TrimmedLogistic(float x, float InvS) { // Logistic distribution, re-normalized to have bounds [-Pi,Pi] // The normalization constant below ensures we integrate to 1.0 float e = exp(-abs(x * InvS)); float a = exp(-PI * InvS); // normalization constant return e * InvS * (1 + a) / ((1 - a) * Pow2(1 + e)); } float SampleTrimmedLogistic(float x, float s) { float a = exp(-PI * rcp(s)); float k = (1 - a) / (1 + a); float r = 2 * x - 1; return s * log((1 + r * k) / (1 - r * k)); } float Phi(int p, float GammaO, float GammaT) { return 2 * p * GammaT - 2 * GammaO + p * PI; } float Np(float phi, int p, float InvS, float GammaO, float GammaT) { // center around chosen angle float dphi = phi - Phi(p, GammaO, GammaT); // wrap to interval (-PI,PI) dphi /= 2 * PI; dphi -= round(dphi); dphi *= 2 * PI; return TrimmedLogistic(dphi, InvS); } // returns sine from cosine (or vice-versa) float TrigInverse(float x) { return sqrt(saturate(1 - x * x)); } // precomputed terms for evaluating/sampling the hair bxdf // TODO: can we somehow do this stuff only once instead of repeating it in eval and sample? struct FHairData { // trig that only depends on viewing direction and offset h float4 SinThetaO; // Viewing angles (adjusted by scale tilt per lobe) float GammaO; float GammaT; float v0; // roughness parameter for Mp float v1; float v2; float v3; float s; // azimuthal roughness parameter for Np float A0; // attenuation term per lobe (first one is always white) float3 A1; float3 A2; float3 A3; float4 LobePdf; float3 LobeCdf; // Normalized sum to help select a lobe }; FHairData PrepareHairData(float3 V, float3 Color, float LongitudinalRoughness, float AzimuthalRoughness, float h, float Specular) { FHairData Result = (FHairData)0; // Longitudinal roughness float Bm = max(LongitudinalRoughness, 0.01); // avoid tiny values float Bm2 = Bm * Bm; float Bm4 = Bm2 * Bm2; float Bm8 = Bm4 * Bm4; float Bm10 = Bm8 * Bm2; float Bm20 = Bm10 * Bm10; // Chiang et al. - Eq(7) Result.v0 = Pow2(0.726 * Bm + 0.812 * Bm2 + 3.7 * Bm20); Result.v1 = 0.25 * Result.v0; Result.v2 = 4.00 * Result.v0; Result.v3 = Result.v2; // Azimuthal roughness const float SqrtPiOver8 = 0.626657069; float Bn = max(AzimuthalRoughness, 0.01); float Bn2 = Bn * Bn; float Bn4 = Bn2 * Bn2; float Bn8 = Bn4 * Bn4; float Bn11 = Bn8 * Bn2 * Bn; float Bn22 = Bn11 * Bn11; // Chiang et al. - Eq(8) + Eq(12) Result.s = SqrtPiOver8 * (0.265 * Bn + 1.194 * Bn2 + 5.372 * Bn22); // Specular to Eta float F0 = Specular * 0.08; float F90 = saturate(Specular * (50.0 * 0.08)); float Eta = (1 + sqrt(F0)) / (1 - sqrt(F0)); // setup main angles float SinThetaO = V.z; float CosThetaO = TrigInverse(SinThetaO); // TODO: should this angle be exposed as a parameter? // Using value from HairBsdf.ush for now float ScaleAngle = 0.035; // about 2 degrees float3 SinScale; // sin(ScaleAngle * 2^k) for k=0,1,2 float3 CosScale; // cos(ScaleAngle * 2^k) for k=0,1,2 SinScale.x = sin(ScaleAngle); CosScale.x = cos(ScaleAngle); SinScale.y = 2 * CosScale.x * SinScale.x; CosScale.y = Pow2(CosScale.x) - Pow2(SinScale.x); SinScale.z = 2 * CosScale.y * SinScale.y; CosScale.z = Pow2(CosScale.y) - Pow2(SinScale.y); // rotate by scale angle (different amount of each lobe) Result.SinThetaO.x = SinThetaO * CosScale.y - CosThetaO * SinScale.y; // R: -2*angle Result.SinThetaO.y = SinThetaO * CosScale.x + CosThetaO * SinScale.x; // TT: +angle Result.SinThetaO.z = SinThetaO * CosScale.z + CosThetaO * SinScale.z; // TRT: +4*angle Result.SinThetaO.w = SinThetaO; // TR*T: 0 angle // Account for refraction through the cylinder float SinGammaT = h * CosThetaO * rsqrt(Pow2(Eta) - Pow2(SinThetaO)); float CosGammaT2 = 1 - SinGammaT * SinGammaT; Result.GammaO = FastASin(h); Result.GammaT = FastASin(SinGammaT); float SinThetaT = SinThetaO / Eta; float CosThetaT2 = 1 - SinThetaT * SinThetaT; // Tinting factors through the hair float CosGammaO = sqrt(saturate(1 - h * h)); float CosTheta = CosThetaO * CosGammaO; float F = lerp(F0, F90, Pow5(1 - CosTheta)); // Schlick approximation // Chiang et al. - Eq(9) float3 SigmaA = Pow2(log(max(Color, 1e-4)) / ((((((0.245 * Bn) + 5.574) * Bn - 10.73) * Bn + 2.532) * Bn - 0.215) * Bn + 5.969)); // Zinke 2007 - Eq(20) float3 T = exp(-SigmaA * (2 * sqrt(CosGammaT2 / CosThetaT2))); // d'Eon et al. - Eq(12) + Eq(13) Result.A0 = F; Result.A1 = Pow2(1 - F) * T; Result.A2 = Result.A1 * F * T; // Chiang et al. - Eq(6) Result.A3 = Result.A2 * F * T * rcp(1.00001 - F * T); // avoid dividing by 0 when F*T=1 Result.LobeCdf = LobeSelectionCdf(Result.A0, Result.A1, Result.A2, Result.A3); Result.LobePdf = LobeSelectionPdf(Result.LobeCdf); return Result; } FMaterialSample Hair_SampleMaterial( float3 V_World, FPathTracingPayload Payload, float3 RandSample) { const float3x3 HairBasis = GetHairBasis(Payload.WorldTangent, V_World); const float3 V = mul(HairBasis, V_World); // TODO: simplify this since HairBasis is already sort of aligned with V ? const float h = 2 * Payload.GetHairPrimitiveUV().y - 1; // remap back to [-1,1] const float LongitudinalRoughness = Payload.GetHairLongitudinalRoughness(); const float AzimuthalRoughness = lerp(1.0, 0.1, Payload.GetHairAzimuthalRoughness()); const FHairData HairData = PrepareHairData(V, Payload.GetBaseColor(), LongitudinalRoughness, AzimuthalRoughness, h, Payload.GetHairSpecular()); // 1) Decide which lobe to sample float u = RandSample.z; int p = 0; float v = 0, SinThetaOp = 0; if (u < HairData.LobeCdf.x) { u = RescaleRandomNumber(u, 0.0, HairData.LobeCdf.x); p = 0; v = HairData.v0; SinThetaOp = HairData.SinThetaO.x; } else if (u < HairData.LobeCdf.y) { u = RescaleRandomNumber(u, HairData.LobeCdf.x, HairData.LobeCdf.y); p = 1; v = HairData.v1; SinThetaOp = HairData.SinThetaO.y; } else if (u < HairData.LobeCdf.z) { u = RescaleRandomNumber(u, HairData.LobeCdf.y, HairData.LobeCdf.z); p = 2; v = HairData.v2; SinThetaOp = HairData.SinThetaO.z; } else { u = RescaleRandomNumber(u, HairData.LobeCdf.z, 1.0); p = 3; v = HairData.v3; SinThetaOp = HairData.SinThetaO.w; } // 2) Sample longitudinal angle const float CosTheta = 1 + v * log(RandSample.x + (1 - RandSample.x) * exp(-2 / v)); const float CosPhi = cos(2 * PI * RandSample.y); const float SinThetaI = -CosTheta * SinThetaOp + sqrt((1 - CosTheta * CosTheta) * (1 - SinThetaOp * SinThetaOp)) * CosPhi; const float CosThetaI = TrigInverse(SinThetaI); // 3) Sample Azimuthal angle const float PhiI = (p < 3) ? Phi(p, HairData.GammaO, HairData.GammaT) + SampleTrimmedLogistic(u, HairData.s) : 2 * PI * u; const float3 L = float3(CosThetaI * sin(PhiI), CosThetaI * cos(PhiI), SinThetaI); // transform direction into world space const float3 L_World = mul(L, HairBasis); FMaterialSample Result = CreateMaterialSample(L_World, 0.0, 0.0, sign(dot(Payload.WorldGeoNormal, L_World)), LongitudinalRoughness, PATHTRACER_SCATTER_SPECULAR); const float InvS = rcp(HairData.s); const float Pdf0 = Mp(SinThetaI, HairData.SinThetaO.x, HairData.v0) * Np(PhiI, 0, InvS, HairData.GammaO, HairData.GammaT); const float Pdf1 = Mp(SinThetaI, HairData.SinThetaO.y, HairData.v1) * Np(PhiI, 1, InvS, HairData.GammaO, HairData.GammaT); const float Pdf2 = Mp(SinThetaI, HairData.SinThetaO.z, HairData.v2) * Np(PhiI, 2, InvS, HairData.GammaO, HairData.GammaT); const float Pdf3 = Mp(SinThetaI, HairData.SinThetaO.w, HairData.v3) * (1 / (2 * PI)); // last lobe is isotropic Result.AddLobeWithMIS(HairData.A0, Pdf0, HairData.LobePdf.x); Result.AddLobeWithMIS(HairData.A1, Pdf1, HairData.LobePdf.y); Result.AddLobeWithMIS(HairData.A2, Pdf2, HairData.LobePdf.z); Result.AddLobeWithMIS(HairData.A3, Pdf3, HairData.LobePdf.w); return Result; } FMaterialEval Hair_EvalMaterial( float3 V_World, float3 L_World, FPathTracingPayload Payload, float2 DiffuseSpecularScale ) { const float3x3 HairBasis = GetHairBasis(Payload.WorldTangent, V_World); const float3 V = mul(HairBasis, V_World); const float3 L = mul(HairBasis, L_World); const float h = 2 * Payload.GetHairPrimitiveUV().y - 1; // remap back to [-1,1] const float LongitudinalRoughness = Payload.GetHairLongitudinalRoughness(); const float AzimuthalRoughness = lerp(1.0, 0.1, Payload.GetHairAzimuthalRoughness()); const FHairData HairData = PrepareHairData(V, Payload.GetBaseColor(), LongitudinalRoughness, AzimuthalRoughness, h, Payload.GetHairSpecular()); // precompute data based of lighting direction const float SinThetaI = L.z; const float CosThetaI = TrigInverse(SinThetaI); const float PhiI = atan2(L.x, L.y); // NOTE: in our coordinate system, PhiO == 0, so we only need to account for L FMaterialEval Result = NullMaterialEval(); const float InvS = rcp(HairData.s); const float Pdf0 = Mp(SinThetaI, HairData.SinThetaO.x, HairData.v0) * Np(PhiI, 0, InvS, HairData.GammaO, HairData.GammaT); const float Pdf1 = Mp(SinThetaI, HairData.SinThetaO.y, HairData.v1) * Np(PhiI, 1, InvS, HairData.GammaO, HairData.GammaT); const float Pdf2 = Mp(SinThetaI, HairData.SinThetaO.z, HairData.v2) * Np(PhiI, 2, InvS, HairData.GammaO, HairData.GammaT); const float Pdf3 = Mp(SinThetaI, HairData.SinThetaO.w, HairData.v3) * (1 / (2 * PI)); // last lobe is isotropic Result.AddLobeWithMIS(HairData.A0, Pdf0, HairData.LobePdf.x); Result.AddLobeWithMIS(HairData.A1, Pdf1, HairData.LobePdf.y); Result.AddLobeWithMIS(HairData.A2, Pdf2, HairData.LobePdf.z); Result.AddLobeWithMIS(HairData.A3, Pdf3, HairData.LobePdf.w); Result.Weight *= DiffuseSpecularScale.y; return Result; } #endif