Files
UnrealEngine/Engine/Shaders/Private/PathTracing/Material/PathTracingHair.ush
2025-05-18 13:04:45 +08:00

403 lines
15 KiB
HLSL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 dEon, 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