Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
254 changes: 134 additions & 120 deletions features/Hair Specular/Shaders/Hair/Hair.hlsli
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
#ifndef __HAIR_DEPENDENCY_HLSL__
#define __HAIR_DEPENDENCY_HLSL__

#include "Common/BRDF.hlsli"
#include "Common/Math.hlsli"

#define MARSCHNER false
// #define MARSCHNER

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Default lighting model changed from Marschner to Scheuermann.

By making Marschner opt-in with // #define MARSCHNER, the default hair lighting model has been changed to Scheuermann. This is a significant change that will affect the visual appearance of all hair rendering.

Ensure this change is documented in release notes as it may significantly alter the appearance of hair for existing users.

🤖 Prompt for AI Agents
In features/Hair Specular/Shaders/Hair/Hair.hlsli at line 7, the default hair
lighting model was changed from Marschner to Scheuermann by commenting out the
Marschner define. This change affects hair rendering appearance. Update the
release notes to document this change clearly, explaining that Marschner is now
opt-in and the default is Scheuermann, so users are aware of the visual impact.


namespace Hair
{
Expand All @@ -20,19 +21,11 @@ namespace Hair
return dirAtten * norm * pow(sinTH, 0.5 * n);
}

float HairF0()
float3 HairF0()
{
const float n = 1.55;
const float F0 = pow((1 - n) / (1 + n), 2);
return F0;
}

// [Schlick et al. 1998, "An inexpensive brdf model for physically-based rendering."]
// https://doi.org/10.1111/1467-8659.1330233
float Hair_F(float CosTheta)
{
const float F0 = HairF0();
return F0 + (1 - F0) * pow(1 - CosTheta, 5);
return F0.xxx;
}

float3 ShiftTangent(float3 T, float3 N, float shift)
Expand All @@ -42,140 +35,159 @@ namespace Hair

// [Scheuermann 2004, "Hair Rendering and Shading"]
// https://web.engr.oregonstate.edu/~mjb/cs557/Projects/Papers/HairRendering.pdf
void GetHairDirectLightScheuermann(out float3 dirDiffuse, out float3 dirSpecular, float3 T, float3 L, float3 V, float3 N, float3 lightColor, float shininess, float2 uv, float3 baseColor)
void GetHairDirectLightScheuermann(out float3 dirDiffuse, out float3 dirSpecular, float3 T, float3 L, float3 V, float3 N, float3 VN, float3 lightColor, float shininess, float2 uv, float3 baseColor)
{
const float3 H = normalize(L + V);
const float NdotL = saturate(dot(N, L));
const float oNdotL = dot(N, L);
const float NdotL = saturate(oNdotL);
const float NdotV = saturate(dot(N, V));
const float VNdotV = dot(VN, V);
const float VNdotL = dot(VN, L);
const float HdotV = saturate(dot(H, V));
const float HdotL = saturate(dot(H, L));
const float wrapped = 0.5;

dirDiffuse = NdotL * lightColor / Math::PI;
// [Yibing Jiang 2016, "The Process of Creating Volumetric-based Materials in Uncharted 4"]
// https://advances.realtimerendering.com/s2016
dirDiffuse = saturate(oNdotL + wrapped) / (1 + wrapped);
float3 scatterColor = lerp(float3(0.992, 0.808, 0.518), baseColor, 0.5);
dirDiffuse = saturate(scatterColor + NdotL) * dirDiffuse * lightColor;

float3 TshiftPrimary = T;
float3 TshiftSecondary = T;
float3 TshiftPrimary;
float3 TshiftSecondary;

if (SharedData::hairSpecularSettings.EnableTangentShift) {
const float shift = TexTangentShift.SampleLevel(SampColorSampler, uv, SharedData::MipBias).x - 0.5;
TshiftPrimary = ShiftTangent(T, N, shift + SharedData::hairSpecularSettings.PrimaryShift);
TshiftSecondary = ShiftTangent(T, N, shift + SharedData::hairSpecularSettings.SecondaryShift);
const float shift = TexTangentShift.SampleLevel(SampColorSampler, uv, 0).x - 0.5;
TshiftPrimary = ShiftTangent(T, N, shift + SharedData::hairSpecularSettings.PrimaryTangentShift);
TshiftSecondary = ShiftTangent(T, N, shift + SharedData::hairSpecularSettings.SecondaryTangentShift);
} else {
TshiftPrimary = T;
TshiftSecondary = T;
}

const float3 specPrimary = D_KajiyaKay(TshiftPrimary, H, shininess);
const float3 specSecondary = D_KajiyaKay(TshiftSecondary, H, shininess * 0.5);
const float F = Hair_F(saturate(dot(H, V)));
float3 specR = 0.25 * F * (specPrimary + specSecondary) * NdotL * saturate(NdotV * (3.4e+38));
specR = Color::LinearToGamma(specR);
float scatterFresnel1 = pow(saturate(-dot(L, V)), 9) * pow(saturate(1 - NdotV * NdotV), 12);
float scatterFresnel2 = saturate(pow((1 - NdotV), 20));
float3 specT = scatterFresnel1 + scatterFresnel2;
float3 specTerm = specR + specT * baseColor;
const float3 F = BRDF::F_Schlick(HairF0(), HdotL);
float3 specR = 0.25 * F * (specPrimary + specSecondary * scatterColor) * NdotL * saturate(VNdotV * (3.4e+38));
float scatterFresnel1 = pow(saturate(-dot(L, V)), 9) * pow(saturate(1 - VNdotV * VNdotV), 12);
float scatterFresnel2 = saturate(pow((1 - VNdotV), 20));
float3 specT = (scatterFresnel1 + scatterFresnel2 * scatterColor) * SharedData::hairSpecularSettings.Transmission;
float3 specTerm = specR + specT;
// specTerm = Color::LinearToGamma(specTerm);
dirSpecular = specTerm * lightColor;
}

float Hair_g(float B, float Theta)
{
const float DenominatorB = max(B, 0.01f);
return exp(-0.5 * pow(Theta, 2) / (B * B)) / (sqrt(2 * Math::PI) * DenominatorB);
return exp(-0.5 * Theta * Theta / (B * B)) / (sqrt(Math::TAU) * B);
}

// [Marschner et al. 2003, "Light reflection from human hair fibers."]
// https://graphics.stanford.edu/papers/hair/hair-sg03final.pdf
float3 D_Marschner(float3 L, float3 V, float3 N, float roughness, float3 baseColor, float Area, float Backlit)
float3 D_Marschner(float3 L, float3 V, float3 N, float roughness, float3 baseColor, float area, float backlit)
{
const float VoL = dot(V, L);
const float SinThetaL = dot(N, L);
const float SinThetaV = dot(N, V);
float CosThetaD = cos(0.5 * abs(asin(SinThetaV) - asin(SinThetaL)));
const float NdotL = dot(N, L);
const float NdotV = dot(N, V);
const float VdotL = dot(V, L);

float cosThetaL = sqrt(max(0, 1 - NdotL * NdotL));
float cosThetaV = sqrt(max(0, 1 - NdotV * NdotV));
float cosThetaD = sqrt((1 + cosThetaL * cosThetaV + NdotV * NdotL) / 2.0);

const float3 Lp = L - SinThetaL * N;
const float3 Vp = V - SinThetaV * N;
const float CosPhi = dot(Lp, Vp) * rsqrt(dot(Lp, Lp) * dot(Vp, Vp) + 1e-4);
const float CosHalfPhi = sqrt(saturate(0.5 + 0.5 * CosPhi));
const float3 Lp = L - NdotL * N;
const float3 Vp = V - NdotL * N;
const float cosPhi = dot(Lp, Vp) * rsqrt(dot(Lp, Lp) * dot(Vp, Vp) + 1e-4);
const float cosHalfPhi = sqrt(saturate(0.5 + 0.5 * cosPhi));

float n = 1.55;
float n_prime = 1.19 / CosThetaD + 0.36 * CosThetaD;
float n_prime = 1.19 / cosThetaD + 0.36 * cosThetaD;

float Shift = 0.035;
float Alpha[] = {
const float Shift = 0.0499f;
const float Alpha[] = {
-Shift * 2,
Shift,
Shift * 4,
Shift * 4
};

float B[] = {
Area + pow(roughness, 2),
Area + pow(roughness, 2) / 2,
Area + pow(roughness, 2) * 2,
area + roughness,
area + roughness / 2,
area + roughness * 2
};

float3 R, TT, TRT = 0;

{
const float sa = sin(Alpha[0]);
const float ca = cos(Alpha[0]);
float Shift = 2 * sa * (ca * CosHalfPhi * sqrt(1 - SinThetaV * SinThetaV) + sa * SinThetaV);
float hairIOR = 1.55;
float3 specularColor = HairF0();

float3 Tp;
float Mp, Np, Fp, a, h, f;
float ThetaH = NdotL + NdotV;

float3 R, TT, TRT;

// R
Mp = Hair_g(B[0], ThetaH - Alpha[0]);
Np = 0.25 * cosHalfPhi;
Fp = BRDF::F_Schlick(specularColor, sqrt(saturate(0.5 + 0.5 * VdotL))).x;
R = (Mp * Np) * (Fp * lerp(1, backlit, saturate(-VdotL)));

// TT
Mp = Hair_g(B[1], ThetaH - Alpha[1]);
a = (1.55f / hairIOR) * rcp(n_prime);
h = cosHalfPhi * (1 + a * (0.6 - 0.8 * cosPhi));
f = BRDF::F_Schlick(specularColor, cosThetaD * sqrt(saturate(1 - h * h))).x;
Fp = (1 - f) * (1 - f);
Tp = pow(abs(baseColor), 0.5 * sqrt(1 - (h * a) * (h * a)) / cosThetaD);
Np = exp(-3.65 * cosPhi - 3.98);
TT = (Mp * Np) * (Fp * Tp) * backlit;

// TRT
Mp = Hair_g(B[2], ThetaH - Alpha[2]);
f = BRDF::F_Schlick(specularColor, cosThetaD * 0.5f).x;
Fp = (1 - f) * (1 - f) * f;
Tp = pow(abs(baseColor), 0.8 / cosThetaD);
Np = exp(17 * cosPhi - 16.78);
TRT = (Mp * Np) * (Fp * Tp);

float Mp = Hair_g(B[0] * sqrt(2.0) * CosHalfPhi, SinThetaL + SinThetaV - Shift);
float Np = 0.25 * CosHalfPhi;
float Fp = Hair_F(sqrt(saturate(0.5 + 0.5 * VoL)));
R = Mp * Np * Fp * 2 * lerp(1, Backlit, saturate(-VoL));
}

{
float Mp = Hair_g(B[1], SinThetaL + SinThetaV - Alpha[1]);
float a = 1 / n_prime;
float h = CosHalfPhi * (1 + a * (0.6 - 0.8 * CosPhi));
float f = Hair_F(CosThetaD * sqrt(saturate(1 - h * h)));

float Fp = pow(1 - f, 2);
float3 Tp = pow(baseColor, 0.5 * sqrt(1 - pow(h * a, 2)) / CosThetaD);
float Np = exp(-3.65 * CosPhi - 3.98);

TT = Mp * Np * Fp * Tp * Backlit;
}

{
float Mp = Hair_g(B[2], SinThetaL + SinThetaV - Alpha[2]);
return R + TT + TRT;
}

float f = Hair_F(CosThetaD * 0.5);
float Fp = pow(1 - f, 2) * f;
float3 Tp = pow(baseColor, 0.8 / CosThetaD);
float3 GetHairDiffuseAttenuationKajiyaKay(float3 N, float3 V, float3 L, float shadow, float3 baseColor)
{
float NdotL = dot(N, L);
float NdotV = dot(N, V);
float3 S = 0;

float Np = exp(17 * CosPhi - 16.78);
float diffuseKajiya = 1 - abs(NdotL);

TRT = Mp * Np * Fp * Tp;
}
float3 fakeN = normalize(V - N * NdotV);
const float wrap = 1;
float wrappedNdotL = saturate((dot(fakeN, L) + wrap) / ((1 + wrap) * (1 + wrap)));
float diffuseScatter = (1 / Math::PI) * lerp(wrappedNdotL, diffuseKajiya, 0.33);
float luma = Color::RGBToLuminance(baseColor);
float3 scatterTint = pow(abs(baseColor / luma), 1 - shadow);
S += sqrt(baseColor) * diffuseScatter * scatterTint;

return R + TT + TRT;
return S;
}

void GetHairDirectLightMarschner(out float3 dirDiffuse, out float3 dirSpecular, float3 T, float3 L, float3 V, float3 N, float3 lightColor, float shininess, float2 uv, float3 baseColor)
{
lightColor *= Math::PI;
dirDiffuse = 0;
dirSpecular = 0;
const float roughness = 1 - 0.01 * shininess;

dirSpecular += D_Marschner(L, V, N, roughness, baseColor, 0, 1) * lightColor;
}
const float roughness = pow(abs(2.0 / (shininess + 2.0)), 0.25);

void GetHairDirectLight(out float3 dirDiffuse, out float3 dirSpecular, float3 T, float3 L, float3 V, float3 N, float3 lightColor, float shininess, float2 uv, float3 baseColor)
{
if (!MARSCHNER)
GetHairDirectLightScheuermann(dirDiffuse, dirSpecular, T, L, V, N, lightColor, shininess, uv, baseColor);
else
GetHairDirectLightMarschner(dirDiffuse, dirSpecular, T, L, V, N, lightColor, shininess, uv, baseColor);
dirSpecular += D_Marschner(L, V, N, roughness, baseColor, 0, SharedData::hairSpecularSettings.Transmission) * lightColor;
dirDiffuse += GetHairDiffuseAttenuationKajiyaKay(N, V, L, 0, baseColor) * lightColor;
dirSpecular = max(dirSpecular, 0);
dirDiffuse = max(dirDiffuse, 0);
}

// [Lazarov 2013, "Getting More Physical in Call of Duty: Black Ops II"]
// https://blog.selfshadow.com/publications/s2013-shading-course/lazarov/s2013_pbs_black_ops_2_slides_v2.pdf
float2 GetEnvBRDFApproxLazarov(float roughness, float NdotV)
void GetHairDirectLight(out float3 dirDiffuse, out float3 dirSpecular, float3 T, float3 L, float3 V, float3 N, float3 VN, float3 lightColor, float shininess, float2 uv, float3 baseColor)
{
const float4 c0 = { -1, -0.0275, -0.572, 0.022 };
const float4 c1 = { 1, 0.0425, 1.04, -0.04 };
float4 r = roughness * c0 + c1;
float a004 = min(r.x * r.x, exp2(-9.28 * NdotV)) * r.x + r.y;
float2 AB = float2(-1.04, 1.04) * a004 + r.zw;
return AB;
#ifndef MARSCHNER
GetHairDirectLightScheuermann(dirDiffuse, dirSpecular, T, L, V, N, VN, lightColor, shininess, uv, baseColor);
#else
GetHairDirectLightMarschner(dirDiffuse, dirSpecular, T, L, V, N, lightColor, shininess, uv, baseColor);
#endif
}

float3 ShiftNormal(float3 T, float3 N, float shift)
Expand All @@ -187,43 +199,44 @@ namespace Hair

float3 ShiftWorldNormal(float3 T, float3 N, float n, float2 uv)
{
const float shift = TexTangentShift.SampleLevel(SampColorSampler, uv, SharedData::MipBias).x - 0.5;
const float shift = TexTangentShift.SampleLevel(SampColorSampler, uv, 0).x - 0.5;
float3 T_shifted = ShiftTangent(T, N, shift + n);
float3 N_shifted = normalize(cross(T_shifted, cross(N, T_shifted)));
return N_shifted;
}

void GetHairIndirectSpecularLobeWeights(out float3 diffuseLobeWeight, out float3 specularLobeWeightPrimary, out float3 specularLobeWeightSecondary, float3 T, float3 N, float3 V, float3 VN, float shininess, float2 uv, float3 baseColor)
{
const float roughnessPrimary = 1 - 0.01 * shininess;
const float roughnessSecondary = 1 - 0.005 * shininess;
const float roughnessPrimary = pow(abs(2.0 / (shininess + 2.0)), 0.25);
const float roughnessSecondary = pow(abs(2.0 / (shininess * 0.5 + 2.0)), 0.25);
const float NdotV = saturate(dot(N, V));

if (MARSCHNER) {
specularLobeWeightPrimary = 0;
float3 L = normalize(V - N * dot(V, N));
float NdotL = dot(N, L);
float VdotL = dot(V, L);
#ifdef MARSCHNER
specularLobeWeightPrimary = 0;
specularLobeWeightSecondary = 0;
float3 L = normalize(V - N * dot(V, N));
float NdotL = dot(N, L);
float VdotL = dot(V, L);

diffuseLobeWeight = D_Marschner(L, V, N, roughnessPrimary, baseColor * Math::PI, 0.2, 0);
return;
}
diffuseLobeWeight = D_Marschner(L, V, N, roughnessPrimary, baseColor * Math::PI, 0.2, 0);
return;
#else

float NdotVshifted = NdotV;
float NdotVshifted2 = NdotV;

if (SharedData::hairSpecularSettings.EnableTangentShift) {
const float shift = TexTangentShift.SampleBias(SampColorSampler, uv, SharedData::MipBias).x - 0.5;
NdotVshifted = saturate(dot(ShiftNormal(T, N, shift + SharedData::hairSpecularSettings.PrimaryShift), V));
NdotVshifted2 = saturate(dot(ShiftNormal(T, N, shift + SharedData::hairSpecularSettings.SecondaryShift), V));
const float shift = TexTangentShift.SampleLevel(SampColorSampler, uv, 0).x - 0.5;
NdotVshifted = saturate(dot(ShiftNormal(T, N, shift + SharedData::hairSpecularSettings.PrimaryTangentShift), V));
NdotVshifted2 = saturate(dot(ShiftNormal(T, N, shift + SharedData::hairSpecularSettings.SecondaryTangentShift), V));
}

diffuseLobeWeight = baseColor;
specularLobeWeightPrimary = 0;
specularLobeWeightSecondary = 0;

const float2 specularBRDFPrimary = GetEnvBRDFApproxLazarov(roughnessPrimary, NdotVshifted);
const float2 specularBRDFSecondary = GetEnvBRDFApproxLazarov(roughnessSecondary, NdotVshifted2);
const float2 specularBRDFPrimary = BRDF::EnvBRDFApproxLazarov(roughnessPrimary, NdotVshifted);
const float2 specularBRDFSecondary = BRDF::EnvBRDFApproxLazarov(roughnessSecondary, NdotVshifted2);

const float3 F0 = HairF0();
specularLobeWeightPrimary = F0 * specularBRDFPrimary.x + specularBRDFPrimary.y;
Expand All @@ -240,6 +253,7 @@ namespace Hair
horizon = horizon * horizon;
specularLobeWeightPrimary *= horizon;
specularLobeWeightSecondary *= horizon;
#endif
}

float3 Saturation(float3 color, float saturation)
Expand All @@ -259,13 +273,13 @@ namespace Hair
float3 N1 = N;
float3 N2 = N;

const float roughnessPrimary = 1 - 0.01 * glossiness;
const float roughnessSecondary = 1 - 0.005 * glossiness;
const float roughnessPrimary = pow(abs(2.0 / (glossiness + 2.0)), 0.25);
const float roughnessSecondary = pow(abs(2.0 / (glossiness * 0.5 + 2.0)), 0.25);

if (SharedData::hairSpecularSettings.EnableTangentShift) {
const float shift = TexTangentShift.SampleLevel(SampColorSampler, uv, SharedData::MipBias).x - 0.5;
N1 = ShiftNormal(T, N, shift + SharedData::hairSpecularSettings.PrimaryShift);
N2 = ShiftNormal(T, N, shift + SharedData::hairSpecularSettings.SecondaryShift);
const float shift = TexTangentShift.SampleLevel(SampColorSampler, uv, 0).x - 0.5;
N1 = ShiftNormal(T, N, shift + SharedData::hairSpecularSettings.PrimaryTangentShift);
N2 = ShiftNormal(T, N, shift + SharedData::hairSpecularSettings.SecondaryTangentShift);
}

# if defined(SKYLIGHTING)
Expand Down
10 changes: 5 additions & 5 deletions package/Shaders/Common/SharedData.hlsli
Original file line number Diff line number Diff line change
Expand Up @@ -149,17 +149,17 @@ namespace SharedData
struct HairSpecularSettings
{
uint Enabled;
float Glossiness;
float HairGlossiness;
float SpecularMult;
float DiffuseMult;
uint EnableTangentShift;
float PrimaryShift;
float SecondaryShift;
float Saturation;
float PrimaryTangentShift;
float SecondaryTangentShift;
float HairSaturation;
float SpecularIndirectMult;
float DiffuseIndirectMult;
float BaseColorMult;
float pad;
float Transmission;
};

struct TerrainVariationSettings
Expand Down
Loading