From b5d2ffe77d8ddc25a4838acdbe03fdf4ec2fba1b Mon Sep 17 00:00:00 2001 From: Jiaye Date: Thu, 26 Feb 2026 23:18:01 +0800 Subject: [PATCH 01/20] fix(hair): marschner with weird coloring under volumetric shadows --- .../Hair Specular/Shaders/Hair/Hair.hlsli | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/features/Hair Specular/Shaders/Hair/Hair.hlsli b/features/Hair Specular/Shaders/Hair/Hair.hlsli index 4821237173..0eeac98528 100644 --- a/features/Hair Specular/Shaders/Hair/Hair.hlsli +++ b/features/Hair Specular/Shaders/Hair/Hair.hlsli @@ -57,9 +57,8 @@ 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, out float3 dirTransmission, float3 T, float3 L, float3 V, float3 N, float3 VN, float3 lightColor, float shininess, float selfShadow, float2 uv, float3 baseColor) + void GetHairDirectLightScheuermann(out float3 dirDiffuse, out float3 dirSpecular, out float3 dirTransmission, float3 T, float3 L, float3 V, float3 N, float3 VN, DirectContext context, float shininess, float2 uv, float3 baseColor) { - lightColor *= selfShadow; const float3 H = normalize(L + V); const float oNdotL = dot(N, L); const float NdotL = saturate(oNdotL); @@ -71,6 +70,9 @@ namespace Hair const float HdotL = saturate(dot(H, L)); const float wrapped = 0.5; + float3 lightColor = context.lightColor * context.detailedShadow; + float3 softColor = context.lightColor * context.softShadow * context.hairShadow; + // [Yibing Jiang 2016, "The Process of Creating Volumetric-based Materials in Uncharted 4"] // https://advances.realtimerendering.com/s2016 dirDiffuse = saturate(oNdotL + wrapped) / (1 + wrapped); @@ -97,7 +99,7 @@ namespace Hair float scatterFresnel2 = saturate(pow(abs(1 - VNdotV), 20)); float3 specT = (scatterFresnel1 + scatterFresnel2 * scatterColor) * SharedData::hairSpecularSettings.Transmission; dirSpecular = specR * lightColor * SharedData::hairSpecularSettings.SpecularMult; - dirTransmission = specT * lightColor * SharedData::hairSpecularSettings.SpecularMult; + dirTransmission = specT * softColor * SharedData::hairSpecularSettings.SpecularMult; } float Hair_g(float B, float Theta) @@ -192,9 +194,9 @@ namespace Hair return max(S, 0); } - void GetHairDirectLightMarschner(out float3 dirDiffuse, out float3 dirSpecular, out float3 dirTransmission, float3 T, float3 L, float3 V, float3 N, float3 VN, float3 lightColor, float shininess, float selfShadow, float2 uv, float3 baseColor) + void GetHairDirectLightMarschner(out float3 dirDiffuse, out float3 dirSpecular, out float3 dirTransmission, float3 T, float3 L, float3 V, float3 N, float3 VN, DirectContext context, float shininess, float2 uv, float3 baseColor) { - lightColor *= Color::PBRLightingCompensation; + float3 lightColor = context.lightColor * Color::PBRLightingCompensation; dirDiffuse = 0; dirSpecular = 0; dirTransmission = 0; @@ -205,10 +207,10 @@ namespace Hair T = ShiftTangent(T, N, shift); } - float backlit = SharedData::hairSpecularSettings.Transmission * selfShadow; + float transmission = SharedData::hairSpecularSettings.Transmission * context.hairShadow * context.detailedShadow; - dirTransmission += D_Marschner(L, V, T, roughness, baseColor, 0, backlit) * lightColor * SharedData::hairSpecularSettings.SpecularMult; - dirTransmission += GetHairDiffuseAttenuationKajiyaKay(T, V, L, selfShadow, baseColor) * lightColor * SharedData::hairSpecularSettings.DiffuseMult; + dirTransmission += D_Marschner(L, V, T, roughness, baseColor, 0, 1) * lightColor * transmission * SharedData::hairSpecularSettings.SpecularMult; + dirTransmission += GetHairDiffuseAttenuationKajiyaKay(T, V, L, context.detailedShadow, baseColor) * lightColor * transmission * SharedData::hairSpecularSettings.DiffuseMult; } void GetHairDirectLight(out DirectLightingOutput lightingOutput, DirectContext context, MaterialProperties material, float3x3 tbnTr, float2 uv) @@ -219,13 +221,13 @@ namespace Hair const float3 VN = normalize(tbnTr[2]); const float3 L = normalize(context.lightDir); - float3 lightColor = context.lightColor * context.detailedShadow; + float3 lightColor = context.lightColor; float selfShadow = context.hairShadow * context.softShadow; if (SharedData::hairSpecularSettings.HairMode == 0) { - GetHairDirectLightScheuermann(lightingOutput.diffuse, lightingOutput.specular, lightingOutput.transmission, T, L, V, N, VN, lightColor, material.Shininess, selfShadow, uv, material.BaseColor); + GetHairDirectLightScheuermann(lightingOutput.diffuse, lightingOutput.specular, lightingOutput.transmission, T, L, V, N, VN, context, material.Shininess, uv, material.BaseColor); } else { - GetHairDirectLightMarschner(lightingOutput.diffuse, lightingOutput.specular, lightingOutput.transmission, T, L, V, N, VN, lightColor, material.Shininess, selfShadow, uv, material.BaseColor); + GetHairDirectLightMarschner(lightingOutput.diffuse, lightingOutput.specular, lightingOutput.transmission, T, L, V, N, VN, context, material.Shininess, uv, material.BaseColor); } } From e22f3b2da8be6b6c8ab850bd59f7f065b9f37f78 Mon Sep 17 00:00:00 2001 From: jiayev Date: Fri, 27 Feb 2026 02:15:12 +0800 Subject: [PATCH 02/20] Update features/Hair Specular/Shaders/Hair/Hair.hlsli Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- features/Hair Specular/Shaders/Hair/Hair.hlsli | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/features/Hair Specular/Shaders/Hair/Hair.hlsli b/features/Hair Specular/Shaders/Hair/Hair.hlsli index 0eeac98528..baa2c1c03b 100644 --- a/features/Hair Specular/Shaders/Hair/Hair.hlsli +++ b/features/Hair Specular/Shaders/Hair/Hair.hlsli @@ -221,8 +221,15 @@ namespace Hair const float3 VN = normalize(tbnTr[2]); const float3 L = normalize(context.lightDir); - float3 lightColor = context.lightColor; - float selfShadow = context.hairShadow * context.softShadow; + void GetHairDirectLight(out DirectLightingOutput lightingOutput, DirectContext context, MaterialProperties material, float3x3 tbnTr, float2 uv) + { + const float3 T = normalize(context.worldNormal); + const float3 V = normalize(context.viewDir); + const float3 N = normalize(context.vertexNormal); + const float3 VN = normalize(tbnTr[2]); + const float3 L = normalize(context.lightDir); + + if (SharedData::hairSpecularSettings.HairMode == 0) { if (SharedData::hairSpecularSettings.HairMode == 0) { GetHairDirectLightScheuermann(lightingOutput.diffuse, lightingOutput.specular, lightingOutput.transmission, T, L, V, N, VN, context, material.Shininess, uv, material.BaseColor); From 6c6ad7b8e615c749e2a475bf70d3b97aa7aca819 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 18:15:41 +0000 Subject: [PATCH 03/20] =?UTF-8?q?style:=20=F0=9F=8E=A8=20apply=20pre-commi?= =?UTF-8?q?t.ci=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated formatting by clang-format, prettier, and other hooks. See https://pre-commit.ci for details. --- .../Hair Specular/Shaders/Hair/Hair.hlsli | 161 +++++++++--------- 1 file changed, 80 insertions(+), 81 deletions(-) diff --git a/features/Hair Specular/Shaders/Hair/Hair.hlsli b/features/Hair Specular/Shaders/Hair/Hair.hlsli index baa2c1c03b..8f51c1593f 100644 --- a/features/Hair Specular/Shaders/Hair/Hair.hlsli +++ b/features/Hair Specular/Shaders/Hair/Hair.hlsli @@ -221,91 +221,90 @@ namespace Hair const float3 VN = normalize(tbnTr[2]); const float3 L = normalize(context.lightDir); - void GetHairDirectLight(out DirectLightingOutput lightingOutput, DirectContext context, MaterialProperties material, float3x3 tbnTr, float2 uv) - { - const float3 T = normalize(context.worldNormal); - const float3 V = normalize(context.viewDir); - const float3 N = normalize(context.vertexNormal); - const float3 VN = normalize(tbnTr[2]); - const float3 L = normalize(context.lightDir); - - if (SharedData::hairSpecularSettings.HairMode == 0) { - - if (SharedData::hairSpecularSettings.HairMode == 0) { - GetHairDirectLightScheuermann(lightingOutput.diffuse, lightingOutput.specular, lightingOutput.transmission, T, L, V, N, VN, context, material.Shininess, uv, material.BaseColor); - } else { - GetHairDirectLightMarschner(lightingOutput.diffuse, lightingOutput.specular, lightingOutput.transmission, T, L, V, N, VN, context, material.Shininess, uv, material.BaseColor); - } - } - - void GetHairIndirectLobeWeights(out IndirectLobeWeights lobeWeights, IndirectContext context, MaterialProperties material, float2 uv) - { - lobeWeights = (IndirectLobeWeights)0; - - float3 T = normalize(context.worldNormal); - const float3 V = normalize(context.viewDir); - const float3 N = normalize(context.vertexNormal); - - if (SharedData::hairSpecularSettings.HairMode == 1) { - if (SharedData::hairSpecularSettings.EnableTangentShift) { - const float shift = TexTangentShift.SampleLevel(SampColorSampler, uv, 0).x - 0.5; - T = ShiftTangent(T, N, shift); + void GetHairDirectLight(out DirectLightingOutput lightingOutput, DirectContext context, MaterialProperties material, float3x3 tbnTr, float2 uv) + { + const float3 T = normalize(context.worldNormal); + const float3 V = normalize(context.viewDir); + const float3 N = normalize(context.vertexNormal); + const float3 VN = normalize(tbnTr[2]); + const float3 L = normalize(context.lightDir); + + if (SharedData::hairSpecularSettings.HairMode == 0) { + if (SharedData::hairSpecularSettings.HairMode == 0) { + GetHairDirectLightScheuermann(lightingOutput.diffuse, lightingOutput.specular, lightingOutput.transmission, T, L, V, N, VN, context, material.Shininess, uv, material.BaseColor); + } else { + GetHairDirectLightMarschner(lightingOutput.diffuse, lightingOutput.specular, lightingOutput.transmission, T, L, V, N, VN, context, material.Shininess, uv, material.BaseColor); + } } - float3 L = normalize(V - T * dot(V, T)); - - lobeWeights.diffuse = D_Marschner(L, V, T, 1 - saturate(material.Shininess * 0.01), material.BaseColor, 0.2, 0) * Math::PI * SharedData::hairSpecularSettings.SpecularIndirectMult; - lobeWeights.diffuse += GetHairDiffuseAttenuationKajiyaKay(T, V, L, 1, material.BaseColor) * Math::PI * SharedData::hairSpecularSettings.DiffuseIndirectMult; - return; - } else { - lobeWeights.diffuse = saturate(material.BaseColor * SharedData::hairSpecularSettings.DiffuseIndirectMult); - float2 hairBRDF = BRDF::EnvBRDF(material.Roughness, saturate(dot(N, V))); - float3 hairSpecularLobe = material.F0 * hairBRDF.x + hairBRDF.y; - lobeWeights.diffuse *= (1 - hairSpecularLobe); - lobeWeights.specular = saturate(hairSpecularLobe * SharedData::hairSpecularSettings.SpecularIndirectMult); - } - } - - float3 Saturation(float3 color, float saturation) - { - float luminance = Color::RGBToLuminance(color); - return saturate(lerp(float3(luminance, luminance, luminance), color, saturation)); - } - - float HairSelfShadow(float3 positionWS, float3 lightDirWS, float noise, uint eyeIndex) - { - if (!SharedData::hairSpecularSettings.EnableSelfShadow) { - return 1.0; - } - - // Simple raymarch - const int stepCount = 4; - float3 positionVS = FrameBuffer::WorldToView(positionWS, true, eyeIndex); - float3 lightDirVS = FrameBuffer::WorldToView(lightDirWS, false, eyeIndex); - lightDirVS *= max(SharedData::hairSpecularSettings.SelfShadowScale * GAME_UNIT_TO_CM, 0.05); - float stepSize = 1.0 / stepCount; - - float3 ray = positionVS + lightDirVS * (noise - 0.5) * 2 * stepSize; - float shadow = 1.0; - int hitCount = 0; + void GetHairIndirectLobeWeights(out IndirectLobeWeights lobeWeights, IndirectContext context, MaterialProperties material, float2 uv) + { + lobeWeights = (IndirectLobeWeights)0; + + float3 T = normalize(context.worldNormal); + const float3 V = normalize(context.viewDir); + const float3 N = normalize(context.vertexNormal); + + if (SharedData::hairSpecularSettings.HairMode == 1) { + if (SharedData::hairSpecularSettings.EnableTangentShift) { + const float shift = TexTangentShift.SampleLevel(SampColorSampler, uv, 0).x - 0.5; + T = ShiftTangent(T, N, shift); + } + float3 L = normalize(V - T * dot(V, T)); + + lobeWeights.diffuse = D_Marschner(L, V, T, 1 - saturate(material.Shininess * 0.01), material.BaseColor, 0.2, 0) * Math::PI * SharedData::hairSpecularSettings.SpecularIndirectMult; + lobeWeights.diffuse += GetHairDiffuseAttenuationKajiyaKay(T, V, L, 1, material.BaseColor) * Math::PI * SharedData::hairSpecularSettings.DiffuseIndirectMult; + return; + } else { + lobeWeights.diffuse = saturate(material.BaseColor * SharedData::hairSpecularSettings.DiffuseIndirectMult); + float2 hairBRDF = BRDF::EnvBRDF(material.Roughness, saturate(dot(N, V))); + float3 hairSpecularLobe = material.F0 * hairBRDF.x + hairBRDF.y; + lobeWeights.diffuse *= (1 - hairSpecularLobe); + lobeWeights.specular = saturate(hairSpecularLobe * SharedData::hairSpecularSettings.SpecularIndirectMult); + } + } - [unroll(stepCount)] for (int i = 0; i < stepCount; ++i) - { - ray += lightDirVS * stepSize; - float2 rayUV = FrameBuffer::ViewToUV(ray, true, eyeIndex); - if (FrameBuffer::IsOutsideFrame(rayUV)) - continue; - float rayDepth = ray.z; - float sampleDepth = SharedData::GetScreenDepth(rayUV, eyeIndex); - if (sampleDepth < rayDepth) { - hitCount++; + float3 Saturation(float3 color, float saturation) + { + float luminance = Color::RGBToLuminance(color); + return saturate(lerp(float3(luminance, luminance, luminance), color, saturation)); } - } - if (hitCount > 0) { - shadow -= pow(abs((float)hitCount / (float)stepCount), SharedData::hairSpecularSettings.SelfShadowExponent); + float HairSelfShadow(float3 positionWS, float3 lightDirWS, float noise, uint eyeIndex) + { + if (!SharedData::hairSpecularSettings.EnableSelfShadow) { + return 1.0; + } + + // Simple raymarch + const int stepCount = 4; + + float3 positionVS = FrameBuffer::WorldToView(positionWS, true, eyeIndex); + float3 lightDirVS = FrameBuffer::WorldToView(lightDirWS, false, eyeIndex); + lightDirVS *= max(SharedData::hairSpecularSettings.SelfShadowScale * GAME_UNIT_TO_CM, 0.05); + float stepSize = 1.0 / stepCount; + + float3 ray = positionVS + lightDirVS * (noise - 0.5) * 2 * stepSize; + float shadow = 1.0; + int hitCount = 0; + + [unroll(stepCount)] for (int i = 0; i < stepCount; ++i) + { + ray += lightDirVS * stepSize; + float2 rayUV = FrameBuffer::ViewToUV(ray, true, eyeIndex); + if (FrameBuffer::IsOutsideFrame(rayUV)) + continue; + float rayDepth = ray.z; + float sampleDepth = SharedData::GetScreenDepth(rayUV, eyeIndex); + if (sampleDepth < rayDepth) { + hitCount++; + } + } + + if (hitCount > 0) { + shadow -= pow(abs((float)hitCount / (float)stepCount), SharedData::hairSpecularSettings.SelfShadowExponent); + } + return lerp(1.0, shadow, SharedData::hairSpecularSettings.SelfShadowStrength); + } } - return lerp(1.0, shadow, SharedData::hairSpecularSettings.SelfShadowStrength); - } -} #endif //__HAIR_DEPENDENCY_HLSL__ \ No newline at end of file From 7bff545510a1e819a7432ea397afb553317e38d8 Mon Sep 17 00:00:00 2001 From: jiayev Date: Fri, 27 Feb 2026 02:17:09 +0800 Subject: [PATCH 04/20] fix ai error --- features/Hair Specular/Shaders/Hair/Hair.hlsli | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/features/Hair Specular/Shaders/Hair/Hair.hlsli b/features/Hair Specular/Shaders/Hair/Hair.hlsli index 8f51c1593f..82dbeb6685 100644 --- a/features/Hair Specular/Shaders/Hair/Hair.hlsli +++ b/features/Hair Specular/Shaders/Hair/Hair.hlsli @@ -213,14 +213,6 @@ namespace Hair dirTransmission += GetHairDiffuseAttenuationKajiyaKay(T, V, L, context.detailedShadow, baseColor) * lightColor * transmission * SharedData::hairSpecularSettings.DiffuseMult; } - void GetHairDirectLight(out DirectLightingOutput lightingOutput, DirectContext context, MaterialProperties material, float3x3 tbnTr, float2 uv) - { - const float3 T = normalize(context.worldNormal); - const float3 V = normalize(context.viewDir); - const float3 N = normalize(context.vertexNormal); - const float3 VN = normalize(tbnTr[2]); - const float3 L = normalize(context.lightDir); - void GetHairDirectLight(out DirectLightingOutput lightingOutput, DirectContext context, MaterialProperties material, float3x3 tbnTr, float2 uv) { const float3 T = normalize(context.worldNormal); @@ -307,4 +299,4 @@ namespace Hair return lerp(1.0, shadow, SharedData::hairSpecularSettings.SelfShadowStrength); } } -#endif //__HAIR_DEPENDENCY_HLSL__ \ No newline at end of file +#endif //__HAIR_DEPENDENCY_HLSL__ From ea92f697901dedf6ceee5c9cc7df8788ebb4aeae Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 18:17:37 +0000 Subject: [PATCH 05/20] =?UTF-8?q?style:=20=F0=9F=8E=A8=20apply=20pre-commi?= =?UTF-8?q?t.ci=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated formatting by clang-format, prettier, and other hooks. See https://pre-commit.ci for details. --- .../Hair Specular/Shaders/Hair/Hair.hlsli | 142 +++++++++--------- 1 file changed, 71 insertions(+), 71 deletions(-) diff --git a/features/Hair Specular/Shaders/Hair/Hair.hlsli b/features/Hair Specular/Shaders/Hair/Hair.hlsli index 82dbeb6685..8c73355c6b 100644 --- a/features/Hair Specular/Shaders/Hair/Hair.hlsli +++ b/features/Hair Specular/Shaders/Hair/Hair.hlsli @@ -213,90 +213,90 @@ namespace Hair dirTransmission += GetHairDiffuseAttenuationKajiyaKay(T, V, L, context.detailedShadow, baseColor) * lightColor * transmission * SharedData::hairSpecularSettings.DiffuseMult; } - void GetHairDirectLight(out DirectLightingOutput lightingOutput, DirectContext context, MaterialProperties material, float3x3 tbnTr, float2 uv) + void GetHairDirectLight(out DirectLightingOutput lightingOutput, DirectContext context, MaterialProperties material, float3x3 tbnTr, float2 uv) + { + const float3 T = normalize(context.worldNormal); + const float3 V = normalize(context.viewDir); + const float3 N = normalize(context.vertexNormal); + const float3 VN = normalize(tbnTr[2]); + const float3 L = normalize(context.lightDir); + + if (SharedData::hairSpecularSettings.HairMode == 0) { + if (SharedData::hairSpecularSettings.HairMode == 0) { + GetHairDirectLightScheuermann(lightingOutput.diffuse, lightingOutput.specular, lightingOutput.transmission, T, L, V, N, VN, context, material.Shininess, uv, material.BaseColor); + } else { + GetHairDirectLightMarschner(lightingOutput.diffuse, lightingOutput.specular, lightingOutput.transmission, T, L, V, N, VN, context, material.Shininess, uv, material.BaseColor); + } + } + + void GetHairIndirectLobeWeights(out IndirectLobeWeights lobeWeights, IndirectContext context, MaterialProperties material, float2 uv) { - const float3 T = normalize(context.worldNormal); + lobeWeights = (IndirectLobeWeights)0; + + float3 T = normalize(context.worldNormal); const float3 V = normalize(context.viewDir); const float3 N = normalize(context.vertexNormal); - const float3 VN = normalize(tbnTr[2]); - const float3 L = normalize(context.lightDir); - if (SharedData::hairSpecularSettings.HairMode == 0) { - if (SharedData::hairSpecularSettings.HairMode == 0) { - GetHairDirectLightScheuermann(lightingOutput.diffuse, lightingOutput.specular, lightingOutput.transmission, T, L, V, N, VN, context, material.Shininess, uv, material.BaseColor); - } else { - GetHairDirectLightMarschner(lightingOutput.diffuse, lightingOutput.specular, lightingOutput.transmission, T, L, V, N, VN, context, material.Shininess, uv, material.BaseColor); + if (SharedData::hairSpecularSettings.HairMode == 1) { + if (SharedData::hairSpecularSettings.EnableTangentShift) { + const float shift = TexTangentShift.SampleLevel(SampColorSampler, uv, 0).x - 0.5; + T = ShiftTangent(T, N, shift); } + float3 L = normalize(V - T * dot(V, T)); + + lobeWeights.diffuse = D_Marschner(L, V, T, 1 - saturate(material.Shininess * 0.01), material.BaseColor, 0.2, 0) * Math::PI * SharedData::hairSpecularSettings.SpecularIndirectMult; + lobeWeights.diffuse += GetHairDiffuseAttenuationKajiyaKay(T, V, L, 1, material.BaseColor) * Math::PI * SharedData::hairSpecularSettings.DiffuseIndirectMult; + return; + } else { + lobeWeights.diffuse = saturate(material.BaseColor * SharedData::hairSpecularSettings.DiffuseIndirectMult); + float2 hairBRDF = BRDF::EnvBRDF(material.Roughness, saturate(dot(N, V))); + float3 hairSpecularLobe = material.F0 * hairBRDF.x + hairBRDF.y; + lobeWeights.diffuse *= (1 - hairSpecularLobe); + lobeWeights.specular = saturate(hairSpecularLobe * SharedData::hairSpecularSettings.SpecularIndirectMult); } + } - void GetHairIndirectLobeWeights(out IndirectLobeWeights lobeWeights, IndirectContext context, MaterialProperties material, float2 uv) - { - lobeWeights = (IndirectLobeWeights)0; - - float3 T = normalize(context.worldNormal); - const float3 V = normalize(context.viewDir); - const float3 N = normalize(context.vertexNormal); - - if (SharedData::hairSpecularSettings.HairMode == 1) { - if (SharedData::hairSpecularSettings.EnableTangentShift) { - const float shift = TexTangentShift.SampleLevel(SampColorSampler, uv, 0).x - 0.5; - T = ShiftTangent(T, N, shift); - } - float3 L = normalize(V - T * dot(V, T)); - - lobeWeights.diffuse = D_Marschner(L, V, T, 1 - saturate(material.Shininess * 0.01), material.BaseColor, 0.2, 0) * Math::PI * SharedData::hairSpecularSettings.SpecularIndirectMult; - lobeWeights.diffuse += GetHairDiffuseAttenuationKajiyaKay(T, V, L, 1, material.BaseColor) * Math::PI * SharedData::hairSpecularSettings.DiffuseIndirectMult; - return; - } else { - lobeWeights.diffuse = saturate(material.BaseColor * SharedData::hairSpecularSettings.DiffuseIndirectMult); - float2 hairBRDF = BRDF::EnvBRDF(material.Roughness, saturate(dot(N, V))); - float3 hairSpecularLobe = material.F0 * hairBRDF.x + hairBRDF.y; - lobeWeights.diffuse *= (1 - hairSpecularLobe); - lobeWeights.specular = saturate(hairSpecularLobe * SharedData::hairSpecularSettings.SpecularIndirectMult); - } - } + float3 Saturation(float3 color, float saturation) + { + float luminance = Color::RGBToLuminance(color); + return saturate(lerp(float3(luminance, luminance, luminance), color, saturation)); + } - float3 Saturation(float3 color, float saturation) - { - float luminance = Color::RGBToLuminance(color); - return saturate(lerp(float3(luminance, luminance, luminance), color, saturation)); + float HairSelfShadow(float3 positionWS, float3 lightDirWS, float noise, uint eyeIndex) + { + if (!SharedData::hairSpecularSettings.EnableSelfShadow) { + return 1.0; } - float HairSelfShadow(float3 positionWS, float3 lightDirWS, float noise, uint eyeIndex) - { - if (!SharedData::hairSpecularSettings.EnableSelfShadow) { - return 1.0; - } + // Simple raymarch + const int stepCount = 4; - // Simple raymarch - const int stepCount = 4; - - float3 positionVS = FrameBuffer::WorldToView(positionWS, true, eyeIndex); - float3 lightDirVS = FrameBuffer::WorldToView(lightDirWS, false, eyeIndex); - lightDirVS *= max(SharedData::hairSpecularSettings.SelfShadowScale * GAME_UNIT_TO_CM, 0.05); - float stepSize = 1.0 / stepCount; - - float3 ray = positionVS + lightDirVS * (noise - 0.5) * 2 * stepSize; - float shadow = 1.0; - int hitCount = 0; - - [unroll(stepCount)] for (int i = 0; i < stepCount; ++i) - { - ray += lightDirVS * stepSize; - float2 rayUV = FrameBuffer::ViewToUV(ray, true, eyeIndex); - if (FrameBuffer::IsOutsideFrame(rayUV)) - continue; - float rayDepth = ray.z; - float sampleDepth = SharedData::GetScreenDepth(rayUV, eyeIndex); - if (sampleDepth < rayDepth) { - hitCount++; - } - } + float3 positionVS = FrameBuffer::WorldToView(positionWS, true, eyeIndex); + float3 lightDirVS = FrameBuffer::WorldToView(lightDirWS, false, eyeIndex); + lightDirVS *= max(SharedData::hairSpecularSettings.SelfShadowScale * GAME_UNIT_TO_CM, 0.05); + float stepSize = 1.0 / stepCount; + + float3 ray = positionVS + lightDirVS * (noise - 0.5) * 2 * stepSize; + float shadow = 1.0; + int hitCount = 0; - if (hitCount > 0) { - shadow -= pow(abs((float)hitCount / (float)stepCount), SharedData::hairSpecularSettings.SelfShadowExponent); + [unroll(stepCount)] for (int i = 0; i < stepCount; ++i) + { + ray += lightDirVS * stepSize; + float2 rayUV = FrameBuffer::ViewToUV(ray, true, eyeIndex); + if (FrameBuffer::IsOutsideFrame(rayUV)) + continue; + float rayDepth = ray.z; + float sampleDepth = SharedData::GetScreenDepth(rayUV, eyeIndex); + if (sampleDepth < rayDepth) { + hitCount++; } - return lerp(1.0, shadow, SharedData::hairSpecularSettings.SelfShadowStrength); } + + if (hitCount > 0) { + shadow -= pow(abs((float)hitCount / (float)stepCount), SharedData::hairSpecularSettings.SelfShadowExponent); + } + return lerp(1.0, shadow, SharedData::hairSpecularSettings.SelfShadowStrength); } + } #endif //__HAIR_DEPENDENCY_HLSL__ From 0ad300b118391afa1f19ef559f7023cd437c20fa Mon Sep 17 00:00:00 2001 From: jiayev Date: Fri, 27 Feb 2026 02:19:15 +0800 Subject: [PATCH 06/20] ughhh ai --- features/Hair Specular/Shaders/Hair/Hair.hlsli | 1 - 1 file changed, 1 deletion(-) diff --git a/features/Hair Specular/Shaders/Hair/Hair.hlsli b/features/Hair Specular/Shaders/Hair/Hair.hlsli index 8c73355c6b..f1c2d74e5f 100644 --- a/features/Hair Specular/Shaders/Hair/Hair.hlsli +++ b/features/Hair Specular/Shaders/Hair/Hair.hlsli @@ -221,7 +221,6 @@ namespace Hair const float3 VN = normalize(tbnTr[2]); const float3 L = normalize(context.lightDir); - if (SharedData::hairSpecularSettings.HairMode == 0) { if (SharedData::hairSpecularSettings.HairMode == 0) { GetHairDirectLightScheuermann(lightingOutput.diffuse, lightingOutput.specular, lightingOutput.transmission, T, L, V, N, VN, context, material.Shininess, uv, material.BaseColor); } else { From 0e72e08c8728a91a48ae6eb99493b95f6e1a61f2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 18:19:42 +0000 Subject: [PATCH 07/20] =?UTF-8?q?style:=20=F0=9F=8E=A8=20apply=20pre-commi?= =?UTF-8?q?t.ci=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated formatting by clang-format, prettier, and other hooks. See https://pre-commit.ci for details. --- .../Hair Specular/Shaders/Hair/Hair.hlsli | 130 +++++++++--------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/features/Hair Specular/Shaders/Hair/Hair.hlsli b/features/Hair Specular/Shaders/Hair/Hair.hlsli index f1c2d74e5f..52f2ef1326 100644 --- a/features/Hair Specular/Shaders/Hair/Hair.hlsli +++ b/features/Hair Specular/Shaders/Hair/Hair.hlsli @@ -221,81 +221,81 @@ namespace Hair const float3 VN = normalize(tbnTr[2]); const float3 L = normalize(context.lightDir); - if (SharedData::hairSpecularSettings.HairMode == 0) { - GetHairDirectLightScheuermann(lightingOutput.diffuse, lightingOutput.specular, lightingOutput.transmission, T, L, V, N, VN, context, material.Shininess, uv, material.BaseColor); - } else { - GetHairDirectLightMarschner(lightingOutput.diffuse, lightingOutput.specular, lightingOutput.transmission, T, L, V, N, VN, context, material.Shininess, uv, material.BaseColor); - } + if (SharedData::hairSpecularSettings.HairMode == 0) { + GetHairDirectLightScheuermann(lightingOutput.diffuse, lightingOutput.specular, lightingOutput.transmission, T, L, V, N, VN, context, material.Shininess, uv, material.BaseColor); + } else { + GetHairDirectLightMarschner(lightingOutput.diffuse, lightingOutput.specular, lightingOutput.transmission, T, L, V, N, VN, context, material.Shininess, uv, material.BaseColor); } + } - void GetHairIndirectLobeWeights(out IndirectLobeWeights lobeWeights, IndirectContext context, MaterialProperties material, float2 uv) - { - lobeWeights = (IndirectLobeWeights)0; - - float3 T = normalize(context.worldNormal); - const float3 V = normalize(context.viewDir); - const float3 N = normalize(context.vertexNormal); - - if (SharedData::hairSpecularSettings.HairMode == 1) { - if (SharedData::hairSpecularSettings.EnableTangentShift) { - const float shift = TexTangentShift.SampleLevel(SampColorSampler, uv, 0).x - 0.5; - T = ShiftTangent(T, N, shift); - } - float3 L = normalize(V - T * dot(V, T)); - - lobeWeights.diffuse = D_Marschner(L, V, T, 1 - saturate(material.Shininess * 0.01), material.BaseColor, 0.2, 0) * Math::PI * SharedData::hairSpecularSettings.SpecularIndirectMult; - lobeWeights.diffuse += GetHairDiffuseAttenuationKajiyaKay(T, V, L, 1, material.BaseColor) * Math::PI * SharedData::hairSpecularSettings.DiffuseIndirectMult; - return; - } else { - lobeWeights.diffuse = saturate(material.BaseColor * SharedData::hairSpecularSettings.DiffuseIndirectMult); - float2 hairBRDF = BRDF::EnvBRDF(material.Roughness, saturate(dot(N, V))); - float3 hairSpecularLobe = material.F0 * hairBRDF.x + hairBRDF.y; - lobeWeights.diffuse *= (1 - hairSpecularLobe); - lobeWeights.specular = saturate(hairSpecularLobe * SharedData::hairSpecularSettings.SpecularIndirectMult); + void GetHairIndirectLobeWeights(out IndirectLobeWeights lobeWeights, IndirectContext context, MaterialProperties material, float2 uv) + { + lobeWeights = (IndirectLobeWeights)0; + + float3 T = normalize(context.worldNormal); + const float3 V = normalize(context.viewDir); + const float3 N = normalize(context.vertexNormal); + + if (SharedData::hairSpecularSettings.HairMode == 1) { + if (SharedData::hairSpecularSettings.EnableTangentShift) { + const float shift = TexTangentShift.SampleLevel(SampColorSampler, uv, 0).x - 0.5; + T = ShiftTangent(T, N, shift); } + float3 L = normalize(V - T * dot(V, T)); + + lobeWeights.diffuse = D_Marschner(L, V, T, 1 - saturate(material.Shininess * 0.01), material.BaseColor, 0.2, 0) * Math::PI * SharedData::hairSpecularSettings.SpecularIndirectMult; + lobeWeights.diffuse += GetHairDiffuseAttenuationKajiyaKay(T, V, L, 1, material.BaseColor) * Math::PI * SharedData::hairSpecularSettings.DiffuseIndirectMult; + return; + } else { + lobeWeights.diffuse = saturate(material.BaseColor * SharedData::hairSpecularSettings.DiffuseIndirectMult); + float2 hairBRDF = BRDF::EnvBRDF(material.Roughness, saturate(dot(N, V))); + float3 hairSpecularLobe = material.F0 * hairBRDF.x + hairBRDF.y; + lobeWeights.diffuse *= (1 - hairSpecularLobe); + lobeWeights.specular = saturate(hairSpecularLobe * SharedData::hairSpecularSettings.SpecularIndirectMult); } + } - float3 Saturation(float3 color, float saturation) - { - float luminance = Color::RGBToLuminance(color); - return saturate(lerp(float3(luminance, luminance, luminance), color, saturation)); + float3 Saturation(float3 color, float saturation) + { + float luminance = Color::RGBToLuminance(color); + return saturate(lerp(float3(luminance, luminance, luminance), color, saturation)); + } + + float HairSelfShadow(float3 positionWS, float3 lightDirWS, float noise, uint eyeIndex) + { + if (!SharedData::hairSpecularSettings.EnableSelfShadow) { + return 1.0; } - float HairSelfShadow(float3 positionWS, float3 lightDirWS, float noise, uint eyeIndex) - { - if (!SharedData::hairSpecularSettings.EnableSelfShadow) { - return 1.0; - } + // Simple raymarch + const int stepCount = 4; - // Simple raymarch - const int stepCount = 4; - - float3 positionVS = FrameBuffer::WorldToView(positionWS, true, eyeIndex); - float3 lightDirVS = FrameBuffer::WorldToView(lightDirWS, false, eyeIndex); - lightDirVS *= max(SharedData::hairSpecularSettings.SelfShadowScale * GAME_UNIT_TO_CM, 0.05); - float stepSize = 1.0 / stepCount; - - float3 ray = positionVS + lightDirVS * (noise - 0.5) * 2 * stepSize; - float shadow = 1.0; - int hitCount = 0; - - [unroll(stepCount)] for (int i = 0; i < stepCount; ++i) - { - ray += lightDirVS * stepSize; - float2 rayUV = FrameBuffer::ViewToUV(ray, true, eyeIndex); - if (FrameBuffer::IsOutsideFrame(rayUV)) - continue; - float rayDepth = ray.z; - float sampleDepth = SharedData::GetScreenDepth(rayUV, eyeIndex); - if (sampleDepth < rayDepth) { - hitCount++; - } - } + float3 positionVS = FrameBuffer::WorldToView(positionWS, true, eyeIndex); + float3 lightDirVS = FrameBuffer::WorldToView(lightDirWS, false, eyeIndex); + lightDirVS *= max(SharedData::hairSpecularSettings.SelfShadowScale * GAME_UNIT_TO_CM, 0.05); + float stepSize = 1.0 / stepCount; + + float3 ray = positionVS + lightDirVS * (noise - 0.5) * 2 * stepSize; + float shadow = 1.0; + int hitCount = 0; - if (hitCount > 0) { - shadow -= pow(abs((float)hitCount / (float)stepCount), SharedData::hairSpecularSettings.SelfShadowExponent); + [unroll(stepCount)] for (int i = 0; i < stepCount; ++i) + { + ray += lightDirVS * stepSize; + float2 rayUV = FrameBuffer::ViewToUV(ray, true, eyeIndex); + if (FrameBuffer::IsOutsideFrame(rayUV)) + continue; + float rayDepth = ray.z; + float sampleDepth = SharedData::GetScreenDepth(rayUV, eyeIndex); + if (sampleDepth < rayDepth) { + hitCount++; } - return lerp(1.0, shadow, SharedData::hairSpecularSettings.SelfShadowStrength); } + + if (hitCount > 0) { + shadow -= pow(abs((float)hitCount / (float)stepCount), SharedData::hairSpecularSettings.SelfShadowExponent); + } + return lerp(1.0, shadow, SharedData::hairSpecularSettings.SelfShadowStrength); } +} #endif //__HAIR_DEPENDENCY_HLSL__ From 70f0d8f61f26a32d65b547dba359fce174276a72 Mon Sep 17 00:00:00 2001 From: Jiaye Date: Fri, 27 Feb 2026 10:05:52 +0800 Subject: [PATCH 08/20] fix(hair): update shadow calculations for hair transmission and diffuse attenuation --- features/Hair Specular/Shaders/Hair/Hair.hlsli | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/features/Hair Specular/Shaders/Hair/Hair.hlsli b/features/Hair Specular/Shaders/Hair/Hair.hlsli index 52f2ef1326..eb64135c94 100644 --- a/features/Hair Specular/Shaders/Hair/Hair.hlsli +++ b/features/Hair Specular/Shaders/Hair/Hair.hlsli @@ -207,10 +207,10 @@ namespace Hair T = ShiftTangent(T, N, shift); } - float transmission = SharedData::hairSpecularSettings.Transmission * context.hairShadow * context.detailedShadow; + float shadow = context.hairShadow * context.detailedShadow; - dirTransmission += D_Marschner(L, V, T, roughness, baseColor, 0, 1) * lightColor * transmission * SharedData::hairSpecularSettings.SpecularMult; - dirTransmission += GetHairDiffuseAttenuationKajiyaKay(T, V, L, context.detailedShadow, baseColor) * lightColor * transmission * SharedData::hairSpecularSettings.DiffuseMult; + dirTransmission += D_Marschner(L, V, T, roughness, baseColor, 0, SharedData::hairSpecularSettings.Transmission) * lightColor * shadow * SharedData::hairSpecularSettings.SpecularMult; + dirTransmission += GetHairDiffuseAttenuationKajiyaKay(T, V, L, shadow, baseColor) * lightColor * shadow * SharedData::hairSpecularSettings.DiffuseMult; } void GetHairDirectLight(out DirectLightingOutput lightingOutput, DirectContext context, MaterialProperties material, float3x3 tbnTr, float2 uv) From bdd9d88083bc24d53d41e41171c21dcfbabac687 Mon Sep 17 00:00:00 2001 From: Jiaye Date: Fri, 27 Feb 2026 10:58:42 +0800 Subject: [PATCH 09/20] test(hair): add HLSL unit tests for hair specular features --- package/Shaders/Tests/TestHair.hlsl | 619 ++++++++++++++++++++++++++++ 1 file changed, 619 insertions(+) create mode 100644 package/Shaders/Tests/TestHair.hlsl diff --git a/package/Shaders/Tests/TestHair.hlsl b/package/Shaders/Tests/TestHair.hlsl new file mode 100644 index 0000000000..91c1298400 --- /dev/null +++ b/package/Shaders/Tests/TestHair.hlsl @@ -0,0 +1,619 @@ +// HLSL Unit Tests for Hair/Hair.hlsli (Hair Specular feature) + +// Stubs for dependencies + +// Stub samplers +SamplerState SampColorSampler : register(s0); + +// Stub for BRDF.hlsli dependencies - stub F0/EnvBRDF usage +#include "/Shaders/Common/Math.hlsli" +#include "/Shaders/Common/Color.hlsli" + +// Stub SharedData hairSpecularSettings +namespace SharedData +{ + struct HairSpecularSettings + { + uint Enabled; + float HairGlossiness; + float SpecularMult; + float DiffuseMult; + uint EnableTangentShift; + float PrimaryTangentShift; + float SecondaryTangentShift; + float HairSaturation; + float SpecularIndirectMult; + float DiffuseIndirectMult; + float BaseColorMult; + float Transmission; + uint EnableSelfShadow; + float SelfShadowStrength; + float SelfShadowExponent; + float SelfShadowScale; + uint HairMode; + uint3 pad; + }; + + // Default test settings + static HairSpecularSettings hairSpecularSettings = { + 1, // Enabled + 0.5f, // HairGlossiness + 1.0f, // SpecularMult + 1.0f, // DiffuseMult + 0, // EnableTangentShift (disabled for simpler tests) + 0.0f, // PrimaryTangentShift + 0.5f, // SecondaryTangentShift + 1.0f, // HairSaturation + 1.0f, // SpecularIndirectMult + 1.0f, // DiffuseIndirectMult + 1.0f, // BaseColorMult + 0.5f, // Transmission + 0, // EnableSelfShadow (disabled) + 0.5f, // SelfShadowStrength + 2.0f, // SelfShadowExponent + 1.0f, // SelfShadowScale + 0, // HairMode (Scheuermann) + uint3(0, 0, 0) + }; +} + +// Stub Game.hlsli constants +#define GAME_UNIT_TO_CM 1.428f + +// Stub for BRDF.hlsli +namespace BRDF +{ + float3 F_Schlick(float3 F0, float VdotH) + { + float Fc = pow(1 - VdotH, 5); + return F0 + (1 - F0) * Fc; + } + + float2 EnvBRDF(float roughness, float NdotV) + { + // Simplified approximation + float a = roughness * roughness; + float x = 1 - NdotV; + float x2 = x * x; + return float2(saturate(1 - a + a * x2), saturate(x2 * 0.5)); + } +} + +// Stub for Color.hlsli if needed +namespace Color +{ + static const float PBRLightingCompensation = 1.0f; + + float RGBToLuminance(float3 color) + { + return dot(color, float3(0.2126f, 0.7152f, 0.0722f)); + } +} + +// Stub texture - returns 0.5 (neutral shift) +namespace Hair +{ + class StubTangentShiftTexture + { + float SampleLevel(SamplerState s, float2 uv, float mip) + { + return 0.5f; // Neutral (shift = 0 after -0.5) + } + }; + + static StubTangentShiftTexture TexTangentShift; +} + +// Stub DirectContext and MaterialProperties +struct DirectContext +{ + float3 lightColor; + float3 lightDir; + float3 viewDir; + float3 worldNormal; + float3 vertexNormal; + float detailedShadow; + float softShadow; + float hairShadow; +}; + +struct MaterialProperties +{ + float3 BaseColor; + float Roughness; + float Shininess; + float3 F0; +}; + +struct DirectLightingOutput +{ + float3 diffuse; + float3 specular; + float3 transmission; +}; + +struct IndirectLobeWeights +{ + float3 diffuse; + float3 specular; +}; + +struct IndirectContext +{ + float3 viewDir; + float3 worldNormal; + float3 vertexNormal; +}; + +// Minimal epsilon +#define EPSILON_DIVISION 1e-6f + +#include "/Test/STF/ShaderTestFramework.hlsli" + +// ============================================================================ +// HAIR HELPER FUNCTIONS (inline from Hair.hlsli without dependencies) +// ============================================================================ + +namespace HairTest +{ + float3 ReorientTangent(float3 T, float3 N) + { + float3 T_reoriented = normalize(T - N * dot(T, N)); + return T_reoriented; + } + + // [Kajiya et al. 1989] + float3 D_KajiyaKay(float3 T, float3 H, float n) + { + float TH = dot(T, H); + float sinTH = saturate(1 - TH * TH); + float dirAtten = saturate(TH + 1); + float norm = (n + 2) / (2 * Math::PI); + return dirAtten * norm * pow(sinTH, 0.5 * n); + } + + float3 HairF0() + { + const float n = 1.55; + const float F0 = pow((1 - n) / (1 + n), 2); + return F0.xxx; + } + + float3 ShiftTangent(float3 T, float3 N, float shift) + { + return normalize(T + N * shift); + } + + float3 ShiftNormal(float3 T, float3 N, float shift) + { + float3 T_shifted = ShiftTangent(T, N, shift); + float3 N_shifted = normalize(cross(T_shifted, cross(N, T_shifted))); + return N_shifted; + } + + float Hair_g(float B, float Theta) + { + return exp(-0.5 * Theta * Theta / (B * B)) / (sqrt(Math::TAU) * B); + } + + 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 diffuseKajiya = 1 - abs(NdotL); + + 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 = max(Color::RGBToLuminance(baseColor), 1e-4); + float3 scatterTint = shadow < 1 ? pow(abs(baseColor / luma), 1 - shadow) : 1; + S += sqrt(baseColor) * diffuseScatter * scatterTint; + + return max(S, 0); + } + + float3 Saturation(float3 color, float saturation) + { + float luminance = Color::RGBToLuminance(color); + return saturate(lerp(float3(luminance, luminance, luminance), color, saturation)); + } +} + +// Test tolerance constants +namespace TestConstants +{ + static const float FLOAT16_EPSILON = 0.001f; + static const float APPROX_TOLERANCE = 0.01f; + static const float EXACT_TOLERANCE = 0.0001f; + static const float NEAR_ZERO = 0.0001f; +} + +// ============================================================================ +// TANGENT REORIENTATION TESTS +// ============================================================================ + +/// @tags hair, tangent +[numthreads(1, 1, 1)] void TestReorientTangent() +{ + // Test: Tangent perpendicular to normal should remain unchanged + float3 N = float3(0, 0, 1); + float3 T = float3(1, 0, 0); // Already perpendicular to N + + float3 T_reoriented = HairTest::ReorientTangent(T, N); + + ASSERT(IsTrue, abs(dot(T_reoriented, N)) < TestConstants::EXACT_TOLERANCE); + ASSERT(IsTrue, abs(length(T_reoriented) - 1.0f) < TestConstants::EXACT_TOLERANCE); + + // Test: Non-perpendicular tangent should be projected + float3 T_tilted = normalize(float3(1, 0, 0.5)); + float3 T_tilted_reoriented = HairTest::ReorientTangent(T_tilted, N); + + ASSERT(IsTrue, abs(dot(T_tilted_reoriented, N)) < TestConstants::EXACT_TOLERANCE); + ASSERT(IsTrue, abs(length(T_tilted_reoriented) - 1.0f) < TestConstants::EXACT_TOLERANCE); +} + +/// @tags hair, tangent +[numthreads(1, 1, 1)] void TestShiftTangent() +{ + float3 T = float3(1, 0, 0); + float3 N = float3(0, 0, 1); + + // Zero shift should return original tangent (normalized) + float3 T_noshift = HairTest::ShiftTangent(T, N, 0.0f); + ASSERT(IsTrue, abs(T_noshift.x - 1.0f) < TestConstants::EXACT_TOLERANCE); + ASSERT(IsTrue, abs(T_noshift.z) < TestConstants::EXACT_TOLERANCE); + + // Positive shift should tilt toward normal + float3 T_shifted = HairTest::ShiftTangent(T, N, 0.5f); + ASSERT(IsTrue, T_shifted.z > 0.0f); // Should have some z component + ASSERT(IsTrue, abs(length(T_shifted) - 1.0f) < TestConstants::EXACT_TOLERANCE); + + // Negative shift should tilt away from normal + float3 T_shifted_neg = HairTest::ShiftTangent(T, N, -0.5f); + ASSERT(IsTrue, T_shifted_neg.z < 0.0f); +} + +// ============================================================================ +// KAJIYA-KAY SPECULAR TESTS +// ============================================================================ + +/// @tags hair, specular, kajiya-kay +[numthreads(1, 1, 1)] void TestKajiyaKayBasic() +{ + float3 T = float3(1, 0, 0); // Tangent along X + float3 H = float3(0, 1, 0); // Half vector along Y (perpendicular to T) + float shininess = 50.0f; + + float3 spec = HairTest::D_KajiyaKay(T, H, shininess); + + // Perpendicular H to T should give maximum specular (sinTH = 1) + ASSERT(IsTrue, spec.x > 0.0f); + ASSERT(IsTrue, !isnan(spec.x) && !isinf(spec.x)); + + // All channels should be equal (grayscale output) + ASSERT(IsTrue, abs(spec.x - spec.y) < TestConstants::EXACT_TOLERANCE); + ASSERT(IsTrue, abs(spec.y - spec.z) < TestConstants::EXACT_TOLERANCE); +} + +/// @tags hair, specular, kajiya-kay +[numthreads(1, 1, 1)] void TestKajiyaKayShininessEffect() +{ + float3 T = float3(1, 0, 0); + float3 H = normalize(float3(0, 1, 0.5)); + + // Higher shininess should give sharper highlights + float3 spec_low = HairTest::D_KajiyaKay(T, H, 10.0f); + float3 spec_high = HairTest::D_KajiyaKay(T, H, 100.0f); + + // Both should be positive + ASSERT(IsTrue, spec_low.x > 0.0f); + ASSERT(IsTrue, spec_high.x > 0.0f); + + // With same angle, higher shininess concentrates energy + // At non-peak angle, high shininess should be lower + ASSERT(IsTrue, spec_low.x != spec_high.x); +} + +/// @tags hair, specular, kajiya-kay +[numthreads(1, 1, 1)] void TestKajiyaKayDirectionalAttenuation() +{ + float3 T = float3(1, 0, 0); + float shininess = 50.0f; + + // H parallel to T (TH = 1) -> dirAtten = saturate(1+1) = 1, but sinTH = 0 + float3 H_parallel = T; + float3 spec_parallel = HairTest::D_KajiyaKay(T, H_parallel, shininess); + ASSERT(IsTrue, spec_parallel.x < TestConstants::EXACT_TOLERANCE); // sinTH = 0 + + // H anti-parallel to T (TH = -1) -> dirAtten = saturate(-1+1) = 0 + float3 H_anti = -T; + float3 spec_anti = HairTest::D_KajiyaKay(T, H_anti, shininess); + ASSERT(IsTrue, spec_anti.x < TestConstants::EXACT_TOLERANCE); +} + +// ============================================================================ +// HAIR F0 TESTS +// ============================================================================ + +/// @tags hair, fresnel +[numthreads(1, 1, 1)] void TestHairF0() +{ + float3 F0 = HairTest::HairF0(); + + // Hair has IOR of 1.55, F0 = ((1-n)/(1+n))^2 ≈ 0.046 + float expected = pow((1.0f - 1.55f) / (1.0f + 1.55f), 2); + + ASSERT(IsTrue, abs(F0.x - expected) < TestConstants::EXACT_TOLERANCE); + ASSERT(IsTrue, abs(F0.y - expected) < TestConstants::EXACT_TOLERANCE); + ASSERT(IsTrue, abs(F0.z - expected) < TestConstants::EXACT_TOLERANCE); + + // Should be around 0.046 + ASSERT(IsTrue, F0.x > 0.04f && F0.x < 0.05f); +} + +// ============================================================================ +// GAUSSIAN DISTRIBUTION TESTS +// ============================================================================ + +/// @tags hair, marschner, gaussian +[numthreads(1, 1, 1)] void TestHairGaussian() +{ + float B = 0.3f; // Beta (roughness) + + // At theta = 0, should be maximum + float g_0 = HairTest::Hair_g(B, 0.0f); + + // At theta = B, should be exp(-0.5) * 1/(sqrt(2*PI)*B) ≈ 0.606 * peak + float g_B = HairTest::Hair_g(B, B); + + ASSERT(IsTrue, g_0 > g_B); + ASSERT(IsTrue, g_0 > 0.0f); + ASSERT(IsTrue, g_B > 0.0f); + + // Should be symmetric + float g_pos = HairTest::Hair_g(B, 0.2f); + float g_neg = HairTest::Hair_g(B, -0.2f); + ASSERT(IsTrue, abs(g_pos - g_neg) < TestConstants::EXACT_TOLERANCE); + + // Higher B (rougher) should have lower peak + float g_rough = HairTest::Hair_g(0.5f, 0.0f); + float g_smooth = HairTest::Hair_g(0.1f, 0.0f); + ASSERT(IsTrue, g_smooth > g_rough); +} + +// ============================================================================ +// DIFFUSE ATTENUATION TESTS +// ============================================================================ + +/// @tags hair, diffuse, kajiya-kay +[numthreads(1, 1, 1)] void TestHairDiffuseAttenuation() +{ + float3 N = float3(0, 0, 1); + float3 V = normalize(float3(0, 0.5, 1)); + float3 L = normalize(float3(0, 0.5, 1)); + float3 baseColor = float3(0.5, 0.3, 0.2); // Brown hair + + float3 diffuse = HairTest::GetHairDiffuseAttenuationKajiyaKay(N, V, L, 1.0f, baseColor); + + // Should be positive + ASSERT(IsTrue, all(diffuse >= 0.0f)); + + // Should be finite + ASSERT(IsTrue, all(!isnan(diffuse))); + ASSERT(IsTrue, all(!isinf(diffuse))); + + // With shadow < 1, should include scatter tint + float3 diffuse_shadowed = HairTest::GetHairDiffuseAttenuationKajiyaKay(N, V, L, 0.5f, baseColor); + ASSERT(IsTrue, all(diffuse_shadowed >= 0.0f)); +} + +/// @tags hair, diffuse, kajiya-kay +[numthreads(1, 1, 1)] void TestHairDiffuseBaseColorEffect() +{ + float3 N = float3(0, 0, 1); + float3 V = normalize(float3(0, 0.5, 1)); + float3 L = normalize(float3(0, 0.5, 1)); + + float3 darkHair = float3(0.1, 0.08, 0.05); + float3 lightHair = float3(0.8, 0.7, 0.5); + + float3 diffuse_dark = HairTest::GetHairDiffuseAttenuationKajiyaKay(N, V, L, 1.0f, darkHair); + float3 diffuse_light = HairTest::GetHairDiffuseAttenuationKajiyaKay(N, V, L, 1.0f, lightHair); + + // Lighter hair should have more diffuse scattering (sqrt of baseColor) + ASSERT(IsTrue, diffuse_light.x > diffuse_dark.x); +} + +// ============================================================================ +// SATURATION TESTS +// ============================================================================ + +/// @tags hair, saturation, color +[numthreads(1, 1, 1)] void TestHairSaturation() +{ + float3 color = float3(0.8, 0.4, 0.2); + + // Saturation = 1 should return original color + float3 result_1 = HairTest::Saturation(color, 1.0f); + ASSERT(IsTrue, abs(result_1.x - color.x) < TestConstants::EXACT_TOLERANCE); + ASSERT(IsTrue, abs(result_1.y - color.y) < TestConstants::EXACT_TOLERANCE); + ASSERT(IsTrue, abs(result_1.z - color.z) < TestConstants::EXACT_TOLERANCE); + + // Saturation = 0 should return grayscale (luminance) + float3 result_0 = HairTest::Saturation(color, 0.0f); + float luma = Color::RGBToLuminance(color); + ASSERT(IsTrue, abs(result_0.x - luma) < TestConstants::EXACT_TOLERANCE); + ASSERT(IsTrue, abs(result_0.y - luma) < TestConstants::EXACT_TOLERANCE); + ASSERT(IsTrue, abs(result_0.z - luma) < TestConstants::EXACT_TOLERANCE); + + // Saturation = 0.5 should be halfway + float3 result_half = HairTest::Saturation(color, 0.5f); + float3 expected_half = lerp(float3(luma, luma, luma), color, 0.5f); + ASSERT(IsTrue, abs(result_half.x - expected_half.x) < TestConstants::EXACT_TOLERANCE); +} + +/// @tags hair, saturation, color +[numthreads(1, 1, 1)] void TestHairSaturationGrayscale() +{ + // Grayscale input should be unaffected by saturation changes + float3 gray = float3(0.5, 0.5, 0.5); + + float3 result_0 = HairTest::Saturation(gray, 0.0f); + float3 result_1 = HairTest::Saturation(gray, 1.0f); + float3 result_2 = HairTest::Saturation(gray, 2.0f); + + ASSERT(IsTrue, abs(result_0.x - 0.5f) < TestConstants::EXACT_TOLERANCE); + ASSERT(IsTrue, abs(result_1.x - 0.5f) < TestConstants::EXACT_TOLERANCE); + // Note: result_2 may be clamped by saturate() +} + +// ============================================================================ +// SHIFT NORMAL TESTS +// ============================================================================ + +/// @tags hair, normal, shift +[numthreads(1, 1, 1)] void TestShiftNormal() +{ + float3 T = float3(1, 0, 0); + float3 N = float3(0, 0, 1); + + // Zero shift should return original normal direction + float3 N_noshift = HairTest::ShiftNormal(T, N, 0.0f); + ASSERT(IsTrue, abs(length(N_noshift) - 1.0f) < TestConstants::EXACT_TOLERANCE); + + // Shifted normal should still be unit length + float3 N_shifted = HairTest::ShiftNormal(T, N, 0.3f); + ASSERT(IsTrue, abs(length(N_shifted) - 1.0f) < TestConstants::EXACT_TOLERANCE); + + // Shifted normal should be different from original when shift != 0 + float3 N_shifted2 = HairTest::ShiftNormal(T, N, 0.5f); + float dotNN = dot(N, N_shifted2); + ASSERT(IsTrue, dotNN < 1.0f - TestConstants::EXACT_TOLERANCE); +} + +// ============================================================================ +// EDGE CASE TESTS +// ============================================================================ + +/// @tags hair, edge-cases, robustness +[numthreads(1, 1, 1)] void TestKajiyaKayEdgeCases() +{ + float3 T = float3(1, 0, 0); + + // Very high shininess + float3 H = float3(0, 1, 0); + float3 spec_high = HairTest::D_KajiyaKay(T, H, 1000.0f); + ASSERT(IsTrue, !isnan(spec_high.x) && !isinf(spec_high.x)); + ASSERT(IsTrue, spec_high.x >= 0.0f); + + // Very low shininess + float3 spec_low = HairTest::D_KajiyaKay(T, H, 1.0f); + ASSERT(IsTrue, !isnan(spec_low.x) && !isinf(spec_low.x)); + ASSERT(IsTrue, spec_low.x >= 0.0f); + + // Normalized vectors (edge case: exactly parallel) + float3 spec_para = HairTest::D_KajiyaKay(T, T, 50.0f); + ASSERT(IsTrue, !isnan(spec_para.x)); +} + +/// @tags hair, edge-cases, robustness +[numthreads(1, 1, 1)] void TestGaussianEdgeCases() +{ + // Very small B (very smooth hair) + float g_smooth = HairTest::Hair_g(0.01f, 0.0f); + ASSERT(IsTrue, !isnan(g_smooth) && !isinf(g_smooth)); + ASSERT(IsTrue, g_smooth > 0.0f); + + // Large theta + float g_large = HairTest::Hair_g(0.3f, 2.0f); + ASSERT(IsTrue, !isnan(g_large)); + ASSERT(IsTrue, g_large >= 0.0f); + ASSERT(IsTrue, g_large < 1.0f); // Should be very small +} + +/// @tags hair, edge-cases, robustness +[numthreads(1, 1, 1)] void TestDiffuseAttenuationEdgeCases() +{ + float3 baseColor = float3(0.5, 0.3, 0.2); + + // Colinear V and N + float3 N = float3(0, 0, 1); + float3 V = float3(0, 0, 1); + float3 L = float3(0, 0, 1); + + float3 diffuse = HairTest::GetHairDiffuseAttenuationKajiyaKay(N, V, L, 1.0f, baseColor); + ASSERT(IsTrue, all(!isnan(diffuse))); + + // Very dark hair (near black) + float3 darkHair = float3(0.01, 0.01, 0.01); + float3 diffuse_dark = HairTest::GetHairDiffuseAttenuationKajiyaKay(N, V, L, 0.5f, darkHair); + ASSERT(IsTrue, all(!isnan(diffuse_dark))); + ASSERT(IsTrue, all(!isinf(diffuse_dark))); +} + +/// @tags hair, saturation, edge-cases +[numthreads(1, 1, 1)] void TestSaturationEdgeCases() +{ + // Very saturated color + float3 saturatedColor = float3(1.0, 0.0, 0.0); + float3 result = HairTest::Saturation(saturatedColor, 1.5f); + ASSERT(IsTrue, all(!isnan(result))); + ASSERT(IsTrue, all(result >= 0.0f)); + ASSERT(IsTrue, all(result <= 1.0f)); // Should be clamped by saturate + + // Near-black color + float3 darkColor = float3(0.001, 0.001, 0.001); + float3 result_dark = HairTest::Saturation(darkColor, 1.0f); + ASSERT(IsTrue, all(!isnan(result_dark))); +} + +// ============================================================================ +// PHYSICAL PROPERTY TESTS +// ============================================================================ + +/// @tags hair, fresnel, physical +[numthreads(1, 1, 1)] void TestHairFresnelBehavior() +{ + float3 F0 = HairTest::HairF0(); + + // At normal incidence, Fresnel should equal F0 + float3 F_normal = BRDF::F_Schlick(F0, 1.0f); + ASSERT(IsTrue, abs(F_normal.x - F0.x) < TestConstants::EXACT_TOLERANCE); + + // At grazing angle, should approach 1.0 + float3 F_grazing = BRDF::F_Schlick(F0, 0.0f); + ASSERT(IsTrue, abs(F_grazing.x - 1.0f) < TestConstants::EXACT_TOLERANCE); + + // Monotonically increasing as angle increases (VdotH decreases) + float3 F_30 = BRDF::F_Schlick(F0, 0.866f); // cos(30°) + float3 F_60 = BRDF::F_Schlick(F0, 0.5f); // cos(60°) + ASSERT(IsTrue, F_60.x > F_30.x); + ASSERT(IsTrue, F_30.x > F_normal.x); +} + +/// @tags hair, specular, energy-conservation +[numthreads(1, 1, 1)] void TestKajiyaKayEnergyConservation() +{ + float3 T = float3(1, 0, 0); + float shininess = 50.0f; + + // Sample multiple angles and verify specular doesn't exceed reasonable bounds + float3 angles[4] = { + float3(0, 1, 0), + float3(0, 0, 1), + normalize(float3(0, 1, 1)), + normalize(float3(1, 1, 1)) + }; + + for (int i = 0; i < 4; i++) { + float3 H = angles[i]; + float3 spec = HairTest::D_KajiyaKay(T, H, shininess); + + // Should be bounded (NDF normalized) + ASSERT(IsTrue, spec.x >= 0.0f); + ASSERT(IsTrue, spec.x < 100.0f); // Reasonable upper bound + } +} From f5227e2de3a99b0fb3436c9ca22f1c765d1cc4fa Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 02:59:15 +0000 Subject: [PATCH 10/20] =?UTF-8?q?style:=20=F0=9F=8E=A8=20apply=20pre-commi?= =?UTF-8?q?t.ci=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated formatting by clang-format, prettier, and other hooks. See https://pre-commit.ci for details. --- package/Shaders/Tests/TestHair.hlsl | 89 +++++++++++++---------------- 1 file changed, 40 insertions(+), 49 deletions(-) diff --git a/package/Shaders/Tests/TestHair.hlsl b/package/Shaders/Tests/TestHair.hlsl index 91c1298400..e9eb34eaeb 100644 --- a/package/Shaders/Tests/TestHair.hlsl +++ b/package/Shaders/Tests/TestHair.hlsl @@ -6,8 +6,8 @@ SamplerState SampColorSampler : register(s0); // Stub for BRDF.hlsli dependencies - stub F0/EnvBRDF usage -#include "/Shaders/Common/Math.hlsli" #include "/Shaders/Common/Color.hlsli" +#include "/Shaders/Common/Math.hlsli" // Stub SharedData hairSpecularSettings namespace SharedData @@ -236,8 +236,7 @@ namespace TestConstants // ============================================================================ /// @tags hair, tangent -[numthreads(1, 1, 1)] void TestReorientTangent() -{ +[numthreads(1, 1, 1)] void TestReorientTangent() { // Test: Tangent perpendicular to normal should remain unchanged float3 N = float3(0, 0, 1); float3 T = float3(1, 0, 0); // Already perpendicular to N @@ -255,8 +254,8 @@ namespace TestConstants ASSERT(IsTrue, abs(length(T_tilted_reoriented) - 1.0f) < TestConstants::EXACT_TOLERANCE); } -/// @tags hair, tangent -[numthreads(1, 1, 1)] void TestShiftTangent() + /// @tags hair, tangent + [numthreads(1, 1, 1)] void TestShiftTangent() { float3 T = float3(1, 0, 0); float3 N = float3(0, 0, 1); @@ -281,8 +280,7 @@ namespace TestConstants // ============================================================================ /// @tags hair, specular, kajiya-kay -[numthreads(1, 1, 1)] void TestKajiyaKayBasic() -{ +[numthreads(1, 1, 1)] void TestKajiyaKayBasic() { float3 T = float3(1, 0, 0); // Tangent along X float3 H = float3(0, 1, 0); // Half vector along Y (perpendicular to T) float shininess = 50.0f; @@ -298,8 +296,8 @@ namespace TestConstants ASSERT(IsTrue, abs(spec.y - spec.z) < TestConstants::EXACT_TOLERANCE); } -/// @tags hair, specular, kajiya-kay -[numthreads(1, 1, 1)] void TestKajiyaKayShininessEffect() + /// @tags hair, specular, kajiya-kay + [numthreads(1, 1, 1)] void TestKajiyaKayShininessEffect() { float3 T = float3(1, 0, 0); float3 H = normalize(float3(0, 1, 0.5)); @@ -318,8 +316,7 @@ namespace TestConstants } /// @tags hair, specular, kajiya-kay -[numthreads(1, 1, 1)] void TestKajiyaKayDirectionalAttenuation() -{ +[numthreads(1, 1, 1)] void TestKajiyaKayDirectionalAttenuation() { float3 T = float3(1, 0, 0); float shininess = 50.0f; @@ -334,12 +331,12 @@ namespace TestConstants ASSERT(IsTrue, spec_anti.x < TestConstants::EXACT_TOLERANCE); } -// ============================================================================ -// HAIR F0 TESTS -// ============================================================================ + // ============================================================================ + // HAIR F0 TESTS + // ============================================================================ -/// @tags hair, fresnel -[numthreads(1, 1, 1)] void TestHairF0() + /// @tags hair, fresnel + [numthreads(1, 1, 1)] void TestHairF0() { float3 F0 = HairTest::HairF0(); @@ -359,8 +356,7 @@ namespace TestConstants // ============================================================================ /// @tags hair, marschner, gaussian -[numthreads(1, 1, 1)] void TestHairGaussian() -{ +[numthreads(1, 1, 1)] void TestHairGaussian() { float B = 0.3f; // Beta (roughness) // At theta = 0, should be maximum @@ -384,12 +380,12 @@ namespace TestConstants ASSERT(IsTrue, g_smooth > g_rough); } -// ============================================================================ -// DIFFUSE ATTENUATION TESTS -// ============================================================================ + // ============================================================================ + // DIFFUSE ATTENUATION TESTS + // ============================================================================ -/// @tags hair, diffuse, kajiya-kay -[numthreads(1, 1, 1)] void TestHairDiffuseAttenuation() + /// @tags hair, diffuse, kajiya-kay + [numthreads(1, 1, 1)] void TestHairDiffuseAttenuation() { float3 N = float3(0, 0, 1); float3 V = normalize(float3(0, 0.5, 1)); @@ -411,8 +407,7 @@ namespace TestConstants } /// @tags hair, diffuse, kajiya-kay -[numthreads(1, 1, 1)] void TestHairDiffuseBaseColorEffect() -{ +[numthreads(1, 1, 1)] void TestHairDiffuseBaseColorEffect() { float3 N = float3(0, 0, 1); float3 V = normalize(float3(0, 0.5, 1)); float3 L = normalize(float3(0, 0.5, 1)); @@ -427,12 +422,12 @@ namespace TestConstants ASSERT(IsTrue, diffuse_light.x > diffuse_dark.x); } -// ============================================================================ -// SATURATION TESTS -// ============================================================================ + // ============================================================================ + // SATURATION TESTS + // ============================================================================ -/// @tags hair, saturation, color -[numthreads(1, 1, 1)] void TestHairSaturation() + /// @tags hair, saturation, color + [numthreads(1, 1, 1)] void TestHairSaturation() { float3 color = float3(0.8, 0.4, 0.2); @@ -456,8 +451,7 @@ namespace TestConstants } /// @tags hair, saturation, color -[numthreads(1, 1, 1)] void TestHairSaturationGrayscale() -{ +[numthreads(1, 1, 1)] void TestHairSaturationGrayscale() { // Grayscale input should be unaffected by saturation changes float3 gray = float3(0.5, 0.5, 0.5); @@ -470,12 +464,12 @@ namespace TestConstants // Note: result_2 may be clamped by saturate() } -// ============================================================================ -// SHIFT NORMAL TESTS -// ============================================================================ + // ============================================================================ + // SHIFT NORMAL TESTS + // ============================================================================ -/// @tags hair, normal, shift -[numthreads(1, 1, 1)] void TestShiftNormal() + /// @tags hair, normal, shift + [numthreads(1, 1, 1)] void TestShiftNormal() { float3 T = float3(1, 0, 0); float3 N = float3(0, 0, 1); @@ -499,8 +493,7 @@ namespace TestConstants // ============================================================================ /// @tags hair, edge-cases, robustness -[numthreads(1, 1, 1)] void TestKajiyaKayEdgeCases() -{ +[numthreads(1, 1, 1)] void TestKajiyaKayEdgeCases() { float3 T = float3(1, 0, 0); // Very high shininess @@ -519,8 +512,8 @@ namespace TestConstants ASSERT(IsTrue, !isnan(spec_para.x)); } -/// @tags hair, edge-cases, robustness -[numthreads(1, 1, 1)] void TestGaussianEdgeCases() + /// @tags hair, edge-cases, robustness + [numthreads(1, 1, 1)] void TestGaussianEdgeCases() { // Very small B (very smooth hair) float g_smooth = HairTest::Hair_g(0.01f, 0.0f); @@ -535,8 +528,7 @@ namespace TestConstants } /// @tags hair, edge-cases, robustness -[numthreads(1, 1, 1)] void TestDiffuseAttenuationEdgeCases() -{ +[numthreads(1, 1, 1)] void TestDiffuseAttenuationEdgeCases() { float3 baseColor = float3(0.5, 0.3, 0.2); // Colinear V and N @@ -554,8 +546,8 @@ namespace TestConstants ASSERT(IsTrue, all(!isinf(diffuse_dark))); } -/// @tags hair, saturation, edge-cases -[numthreads(1, 1, 1)] void TestSaturationEdgeCases() + /// @tags hair, saturation, edge-cases + [numthreads(1, 1, 1)] void TestSaturationEdgeCases() { // Very saturated color float3 saturatedColor = float3(1.0, 0.0, 0.0); @@ -575,8 +567,7 @@ namespace TestConstants // ============================================================================ /// @tags hair, fresnel, physical -[numthreads(1, 1, 1)] void TestHairFresnelBehavior() -{ +[numthreads(1, 1, 1)] void TestHairFresnelBehavior() { float3 F0 = HairTest::HairF0(); // At normal incidence, Fresnel should equal F0 @@ -594,8 +585,8 @@ namespace TestConstants ASSERT(IsTrue, F_30.x > F_normal.x); } -/// @tags hair, specular, energy-conservation -[numthreads(1, 1, 1)] void TestKajiyaKayEnergyConservation() + /// @tags hair, specular, energy-conservation + [numthreads(1, 1, 1)] void TestKajiyaKayEnergyConservation() { float3 T = float3(1, 0, 0); float shininess = 50.0f; From 08c54d4f107116f585759112c075a00f58e46f54 Mon Sep 17 00:00:00 2001 From: Jiaye Date: Fri, 27 Feb 2026 11:06:33 +0800 Subject: [PATCH 11/20] prevent redefinition --- package/Shaders/Tests/TestHair.hlsl | 2 -- 1 file changed, 2 deletions(-) diff --git a/package/Shaders/Tests/TestHair.hlsl b/package/Shaders/Tests/TestHair.hlsl index e9eb34eaeb..bb316d22c4 100644 --- a/package/Shaders/Tests/TestHair.hlsl +++ b/package/Shaders/Tests/TestHair.hlsl @@ -5,8 +5,6 @@ // Stub samplers SamplerState SampColorSampler : register(s0); -// Stub for BRDF.hlsli dependencies - stub F0/EnvBRDF usage -#include "/Shaders/Common/Color.hlsli" #include "/Shaders/Common/Math.hlsli" // Stub SharedData hairSpecularSettings From 5cc79f9a0708f02dcea97dcc18ddcf0336a2476b Mon Sep 17 00:00:00 2001 From: Jiaye Date: Fri, 27 Feb 2026 11:33:50 +0800 Subject: [PATCH 12/20] fix(hair): enhance HLSL unit tests for hair specular features and improve dependency stubs --- package/Shaders/Tests/TestHair.hlsl | 405 ++++++++++------------------ 1 file changed, 142 insertions(+), 263 deletions(-) diff --git a/package/Shaders/Tests/TestHair.hlsl b/package/Shaders/Tests/TestHair.hlsl index bb316d22c4..72557af222 100644 --- a/package/Shaders/Tests/TestHair.hlsl +++ b/package/Shaders/Tests/TestHair.hlsl @@ -1,13 +1,41 @@ // HLSL Unit Tests for Hair/Hair.hlsli (Hair Specular feature) +// +// This test file tests the actual Hair.hlsli implementation by: +// 1. Defining necessary stubs for external dependencies (SharedData, textures, etc.) +// 2. Including the real Hair.hlsli file +// 3. Testing Hair namespace functions directly -// Stubs for dependencies +// ============================================================================ +// STUBS FOR EXTERNAL DEPENDENCIES +// ============================================================================ -// Stub samplers +// Stub sampler required by Hair.hlsli SamplerState SampColorSampler : register(s0); +// Include common dependencies that Hair.hlsli needs #include "/Shaders/Common/Math.hlsli" +#include "/Shaders/Common/Color.hlsli" +#include "/Shaders/Common/LightingCommon.hlsli" -// Stub SharedData hairSpecularSettings +// Stub BRDF functions used by Hair.hlsli +namespace BRDF +{ + float3 F_Schlick(float3 F0, float VdotH) + { + float Fc = pow(1 - VdotH, 5); + return F0 + (1 - F0) * Fc; + } + + float2 EnvBRDF(float roughness, float NdotV) + { + float a = roughness * roughness; + float x = 1 - NdotV; + float x2 = x * x; + return float2(saturate(1 - a + a * x2), saturate(x2 * 0.5)); + } +} + +// Stub SharedData namespace with hairSpecularSettings namespace SharedData { struct HairSpecularSettings @@ -32,193 +60,35 @@ namespace SharedData uint3 pad; }; - // Default test settings + // Test configuration with typical values static HairSpecularSettings hairSpecularSettings = { - 1, // Enabled - 0.5f, // HairGlossiness - 1.0f, // SpecularMult - 1.0f, // DiffuseMult - 0, // EnableTangentShift (disabled for simpler tests) - 0.0f, // PrimaryTangentShift - 0.5f, // SecondaryTangentShift - 1.0f, // HairSaturation - 1.0f, // SpecularIndirectMult - 1.0f, // DiffuseIndirectMult - 1.0f, // BaseColorMult - 0.5f, // Transmission - 0, // EnableSelfShadow (disabled) - 0.5f, // SelfShadowStrength - 2.0f, // SelfShadowExponent - 1.0f, // SelfShadowScale - 0, // HairMode (Scheuermann) + 1, // Enabled + 0.5f, // HairGlossiness + 1.0f, // SpecularMult + 1.0f, // DiffuseMult + 0, // EnableTangentShift (disabled for predictable tests) + 0.0f, // PrimaryTangentShift + 0.5f, // SecondaryTangentShift + 1.0f, // HairSaturation + 1.0f, // SpecularIndirectMult + 1.0f, // DiffuseIndirectMult + 1.0f, // BaseColorMult + 0.5f, // Transmission + 0, // EnableSelfShadow (disabled) + 0.5f, // SelfShadowStrength + 2.0f, // SelfShadowExponent + 1.0f, // SelfShadowScale + 0, // HairMode (0 = Scheuermann) uint3(0, 0, 0) }; } -// Stub Game.hlsli constants -#define GAME_UNIT_TO_CM 1.428f - -// Stub for BRDF.hlsli -namespace BRDF -{ - float3 F_Schlick(float3 F0, float VdotH) - { - float Fc = pow(1 - VdotH, 5); - return F0 + (1 - F0) * Fc; - } - - float2 EnvBRDF(float roughness, float NdotV) - { - // Simplified approximation - float a = roughness * roughness; - float x = 1 - NdotV; - float x2 = x * x; - return float2(saturate(1 - a + a * x2), saturate(x2 * 0.5)); - } -} - -// Stub for Color.hlsli if needed -namespace Color -{ - static const float PBRLightingCompensation = 1.0f; - - float RGBToLuminance(float3 color) - { - return dot(color, float3(0.2126f, 0.7152f, 0.0722f)); - } -} - -// Stub texture - returns 0.5 (neutral shift) -namespace Hair -{ - class StubTangentShiftTexture - { - float SampleLevel(SamplerState s, float2 uv, float mip) - { - return 0.5f; // Neutral (shift = 0 after -0.5) - } - }; - - static StubTangentShiftTexture TexTangentShift; -} - -// Stub DirectContext and MaterialProperties -struct DirectContext -{ - float3 lightColor; - float3 lightDir; - float3 viewDir; - float3 worldNormal; - float3 vertexNormal; - float detailedShadow; - float softShadow; - float hairShadow; -}; - -struct MaterialProperties -{ - float3 BaseColor; - float Roughness; - float Shininess; - float3 F0; -}; - -struct DirectLightingOutput -{ - float3 diffuse; - float3 specular; - float3 transmission; -}; - -struct IndirectLobeWeights -{ - float3 diffuse; - float3 specular; -}; - -struct IndirectContext -{ - float3 viewDir; - float3 worldNormal; - float3 vertexNormal; -}; - -// Minimal epsilon -#define EPSILON_DIVISION 1e-6f - -#include "/Test/STF/ShaderTestFramework.hlsli" - // ============================================================================ -// HAIR HELPER FUNCTIONS (inline from Hair.hlsli without dependencies) +// INCLUDE THE REAL HAIR.HLSLI // ============================================================================ +#include "/Shaders/Hair/Hair.hlsli" -namespace HairTest -{ - float3 ReorientTangent(float3 T, float3 N) - { - float3 T_reoriented = normalize(T - N * dot(T, N)); - return T_reoriented; - } - - // [Kajiya et al. 1989] - float3 D_KajiyaKay(float3 T, float3 H, float n) - { - float TH = dot(T, H); - float sinTH = saturate(1 - TH * TH); - float dirAtten = saturate(TH + 1); - float norm = (n + 2) / (2 * Math::PI); - return dirAtten * norm * pow(sinTH, 0.5 * n); - } - - float3 HairF0() - { - const float n = 1.55; - const float F0 = pow((1 - n) / (1 + n), 2); - return F0.xxx; - } - - float3 ShiftTangent(float3 T, float3 N, float shift) - { - return normalize(T + N * shift); - } - - float3 ShiftNormal(float3 T, float3 N, float shift) - { - float3 T_shifted = ShiftTangent(T, N, shift); - float3 N_shifted = normalize(cross(T_shifted, cross(N, T_shifted))); - return N_shifted; - } - - float Hair_g(float B, float Theta) - { - return exp(-0.5 * Theta * Theta / (B * B)) / (sqrt(Math::TAU) * B); - } - - 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 diffuseKajiya = 1 - abs(NdotL); - - 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 = max(Color::RGBToLuminance(baseColor), 1e-4); - float3 scatterTint = shadow < 1 ? pow(abs(baseColor / luma), 1 - shadow) : 1; - S += sqrt(baseColor) * diffuseScatter * scatterTint; - - return max(S, 0); - } - - float3 Saturation(float3 color, float saturation) - { - float luminance = Color::RGBToLuminance(color); - return saturate(lerp(float3(luminance, luminance, luminance), color, saturation)); - } -} +#include "/Test/STF/ShaderTestFramework.hlsli" // Test tolerance constants namespace TestConstants @@ -234,42 +104,43 @@ namespace TestConstants // ============================================================================ /// @tags hair, tangent -[numthreads(1, 1, 1)] void TestReorientTangent() { +[numthreads(1, 1, 1)] void TestReorientTangent() +{ // Test: Tangent perpendicular to normal should remain unchanged float3 N = float3(0, 0, 1); float3 T = float3(1, 0, 0); // Already perpendicular to N - float3 T_reoriented = HairTest::ReorientTangent(T, N); + float3 T_reoriented = Hair::ReorientTangent(T, N); ASSERT(IsTrue, abs(dot(T_reoriented, N)) < TestConstants::EXACT_TOLERANCE); ASSERT(IsTrue, abs(length(T_reoriented) - 1.0f) < TestConstants::EXACT_TOLERANCE); // Test: Non-perpendicular tangent should be projected float3 T_tilted = normalize(float3(1, 0, 0.5)); - float3 T_tilted_reoriented = HairTest::ReorientTangent(T_tilted, N); + float3 T_tilted_reoriented = Hair::ReorientTangent(T_tilted, N); ASSERT(IsTrue, abs(dot(T_tilted_reoriented, N)) < TestConstants::EXACT_TOLERANCE); ASSERT(IsTrue, abs(length(T_tilted_reoriented) - 1.0f) < TestConstants::EXACT_TOLERANCE); } - /// @tags hair, tangent - [numthreads(1, 1, 1)] void TestShiftTangent() +/// @tags hair, tangent +[numthreads(1, 1, 1)] void TestShiftTangent() { float3 T = float3(1, 0, 0); float3 N = float3(0, 0, 1); // Zero shift should return original tangent (normalized) - float3 T_noshift = HairTest::ShiftTangent(T, N, 0.0f); + float3 T_noshift = Hair::ShiftTangent(T, N, 0.0f); ASSERT(IsTrue, abs(T_noshift.x - 1.0f) < TestConstants::EXACT_TOLERANCE); ASSERT(IsTrue, abs(T_noshift.z) < TestConstants::EXACT_TOLERANCE); // Positive shift should tilt toward normal - float3 T_shifted = HairTest::ShiftTangent(T, N, 0.5f); + float3 T_shifted = Hair::ShiftTangent(T, N, 0.5f); ASSERT(IsTrue, T_shifted.z > 0.0f); // Should have some z component ASSERT(IsTrue, abs(length(T_shifted) - 1.0f) < TestConstants::EXACT_TOLERANCE); // Negative shift should tilt away from normal - float3 T_shifted_neg = HairTest::ShiftTangent(T, N, -0.5f); + float3 T_shifted_neg = Hair::ShiftTangent(T, N, -0.5f); ASSERT(IsTrue, T_shifted_neg.z < 0.0f); } @@ -278,12 +149,13 @@ namespace TestConstants // ============================================================================ /// @tags hair, specular, kajiya-kay -[numthreads(1, 1, 1)] void TestKajiyaKayBasic() { +[numthreads(1, 1, 1)] void TestKajiyaKayBasic() +{ float3 T = float3(1, 0, 0); // Tangent along X float3 H = float3(0, 1, 0); // Half vector along Y (perpendicular to T) float shininess = 50.0f; - float3 spec = HairTest::D_KajiyaKay(T, H, shininess); + float3 spec = Hair::D_KajiyaKay(T, H, shininess); // Perpendicular H to T should give maximum specular (sinTH = 1) ASSERT(IsTrue, spec.x > 0.0f); @@ -294,15 +166,15 @@ namespace TestConstants ASSERT(IsTrue, abs(spec.y - spec.z) < TestConstants::EXACT_TOLERANCE); } - /// @tags hair, specular, kajiya-kay - [numthreads(1, 1, 1)] void TestKajiyaKayShininessEffect() +/// @tags hair, specular, kajiya-kay +[numthreads(1, 1, 1)] void TestKajiyaKayShininessEffect() { float3 T = float3(1, 0, 0); float3 H = normalize(float3(0, 1, 0.5)); // Higher shininess should give sharper highlights - float3 spec_low = HairTest::D_KajiyaKay(T, H, 10.0f); - float3 spec_high = HairTest::D_KajiyaKay(T, H, 100.0f); + float3 spec_low = Hair::D_KajiyaKay(T, H, 10.0f); + float3 spec_high = Hair::D_KajiyaKay(T, H, 100.0f); // Both should be positive ASSERT(IsTrue, spec_low.x > 0.0f); @@ -314,29 +186,30 @@ namespace TestConstants } /// @tags hair, specular, kajiya-kay -[numthreads(1, 1, 1)] void TestKajiyaKayDirectionalAttenuation() { +[numthreads(1, 1, 1)] void TestKajiyaKayDirectionalAttenuation() +{ float3 T = float3(1, 0, 0); float shininess = 50.0f; // H parallel to T (TH = 1) -> dirAtten = saturate(1+1) = 1, but sinTH = 0 float3 H_parallel = T; - float3 spec_parallel = HairTest::D_KajiyaKay(T, H_parallel, shininess); + float3 spec_parallel = Hair::D_KajiyaKay(T, H_parallel, shininess); ASSERT(IsTrue, spec_parallel.x < TestConstants::EXACT_TOLERANCE); // sinTH = 0 // H anti-parallel to T (TH = -1) -> dirAtten = saturate(-1+1) = 0 float3 H_anti = -T; - float3 spec_anti = HairTest::D_KajiyaKay(T, H_anti, shininess); + float3 spec_anti = Hair::D_KajiyaKay(T, H_anti, shininess); ASSERT(IsTrue, spec_anti.x < TestConstants::EXACT_TOLERANCE); } - // ============================================================================ - // HAIR F0 TESTS - // ============================================================================ +// ============================================================================ +// HAIR F0 TESTS +// ============================================================================ - /// @tags hair, fresnel - [numthreads(1, 1, 1)] void TestHairF0() +/// @tags hair, fresnel +[numthreads(1, 1, 1)] void TestHairF0() { - float3 F0 = HairTest::HairF0(); + float3 F0 = Hair::HairF0(); // Hair has IOR of 1.55, F0 = ((1-n)/(1+n))^2 ≈ 0.046 float expected = pow((1.0f - 1.55f) / (1.0f + 1.55f), 2); @@ -354,43 +227,44 @@ namespace TestConstants // ============================================================================ /// @tags hair, marschner, gaussian -[numthreads(1, 1, 1)] void TestHairGaussian() { +[numthreads(1, 1, 1)] void TestHairGaussian() +{ float B = 0.3f; // Beta (roughness) // At theta = 0, should be maximum - float g_0 = HairTest::Hair_g(B, 0.0f); + float g_0 = Hair::Hair_g(B, 0.0f); // At theta = B, should be exp(-0.5) * 1/(sqrt(2*PI)*B) ≈ 0.606 * peak - float g_B = HairTest::Hair_g(B, B); + float g_B = Hair::Hair_g(B, B); ASSERT(IsTrue, g_0 > g_B); ASSERT(IsTrue, g_0 > 0.0f); ASSERT(IsTrue, g_B > 0.0f); // Should be symmetric - float g_pos = HairTest::Hair_g(B, 0.2f); - float g_neg = HairTest::Hair_g(B, -0.2f); + float g_pos = Hair::Hair_g(B, 0.2f); + float g_neg = Hair::Hair_g(B, -0.2f); ASSERT(IsTrue, abs(g_pos - g_neg) < TestConstants::EXACT_TOLERANCE); // Higher B (rougher) should have lower peak - float g_rough = HairTest::Hair_g(0.5f, 0.0f); - float g_smooth = HairTest::Hair_g(0.1f, 0.0f); + float g_rough = Hair::Hair_g(0.5f, 0.0f); + float g_smooth = Hair::Hair_g(0.1f, 0.0f); ASSERT(IsTrue, g_smooth > g_rough); } - // ============================================================================ - // DIFFUSE ATTENUATION TESTS - // ============================================================================ +// ============================================================================ +// DIFFUSE ATTENUATION TESTS +// ============================================================================ - /// @tags hair, diffuse, kajiya-kay - [numthreads(1, 1, 1)] void TestHairDiffuseAttenuation() +/// @tags hair, diffuse, kajiya-kay +[numthreads(1, 1, 1)] void TestHairDiffuseAttenuation() { float3 N = float3(0, 0, 1); float3 V = normalize(float3(0, 0.5, 1)); float3 L = normalize(float3(0, 0.5, 1)); float3 baseColor = float3(0.5, 0.3, 0.2); // Brown hair - float3 diffuse = HairTest::GetHairDiffuseAttenuationKajiyaKay(N, V, L, 1.0f, baseColor); + float3 diffuse = Hair::GetHairDiffuseAttenuationKajiyaKay(N, V, L, 1.0f, baseColor); // Should be positive ASSERT(IsTrue, all(diffuse >= 0.0f)); @@ -400,12 +274,13 @@ namespace TestConstants ASSERT(IsTrue, all(!isinf(diffuse))); // With shadow < 1, should include scatter tint - float3 diffuse_shadowed = HairTest::GetHairDiffuseAttenuationKajiyaKay(N, V, L, 0.5f, baseColor); + float3 diffuse_shadowed = Hair::GetHairDiffuseAttenuationKajiyaKay(N, V, L, 0.5f, baseColor); ASSERT(IsTrue, all(diffuse_shadowed >= 0.0f)); } /// @tags hair, diffuse, kajiya-kay -[numthreads(1, 1, 1)] void TestHairDiffuseBaseColorEffect() { +[numthreads(1, 1, 1)] void TestHairDiffuseBaseColorEffect() +{ float3 N = float3(0, 0, 1); float3 V = normalize(float3(0, 0.5, 1)); float3 L = normalize(float3(0, 0.5, 1)); @@ -413,75 +288,76 @@ namespace TestConstants float3 darkHair = float3(0.1, 0.08, 0.05); float3 lightHair = float3(0.8, 0.7, 0.5); - float3 diffuse_dark = HairTest::GetHairDiffuseAttenuationKajiyaKay(N, V, L, 1.0f, darkHair); - float3 diffuse_light = HairTest::GetHairDiffuseAttenuationKajiyaKay(N, V, L, 1.0f, lightHair); + float3 diffuse_dark = Hair::GetHairDiffuseAttenuationKajiyaKay(N, V, L, 1.0f, darkHair); + float3 diffuse_light = Hair::GetHairDiffuseAttenuationKajiyaKay(N, V, L, 1.0f, lightHair); // Lighter hair should have more diffuse scattering (sqrt of baseColor) ASSERT(IsTrue, diffuse_light.x > diffuse_dark.x); } - // ============================================================================ - // SATURATION TESTS - // ============================================================================ +// ============================================================================ +// SATURATION TESTS +// ============================================================================ - /// @tags hair, saturation, color - [numthreads(1, 1, 1)] void TestHairSaturation() +/// @tags hair, saturation, color +[numthreads(1, 1, 1)] void TestHairSaturation() { float3 color = float3(0.8, 0.4, 0.2); // Saturation = 1 should return original color - float3 result_1 = HairTest::Saturation(color, 1.0f); + float3 result_1 = Hair::Saturation(color, 1.0f); ASSERT(IsTrue, abs(result_1.x - color.x) < TestConstants::EXACT_TOLERANCE); ASSERT(IsTrue, abs(result_1.y - color.y) < TestConstants::EXACT_TOLERANCE); ASSERT(IsTrue, abs(result_1.z - color.z) < TestConstants::EXACT_TOLERANCE); // Saturation = 0 should return grayscale (luminance) - float3 result_0 = HairTest::Saturation(color, 0.0f); + float3 result_0 = Hair::Saturation(color, 0.0f); float luma = Color::RGBToLuminance(color); ASSERT(IsTrue, abs(result_0.x - luma) < TestConstants::EXACT_TOLERANCE); ASSERT(IsTrue, abs(result_0.y - luma) < TestConstants::EXACT_TOLERANCE); ASSERT(IsTrue, abs(result_0.z - luma) < TestConstants::EXACT_TOLERANCE); // Saturation = 0.5 should be halfway - float3 result_half = HairTest::Saturation(color, 0.5f); + float3 result_half = Hair::Saturation(color, 0.5f); float3 expected_half = lerp(float3(luma, luma, luma), color, 0.5f); ASSERT(IsTrue, abs(result_half.x - expected_half.x) < TestConstants::EXACT_TOLERANCE); } /// @tags hair, saturation, color -[numthreads(1, 1, 1)] void TestHairSaturationGrayscale() { +[numthreads(1, 1, 1)] void TestHairSaturationGrayscale() +{ // Grayscale input should be unaffected by saturation changes float3 gray = float3(0.5, 0.5, 0.5); - float3 result_0 = HairTest::Saturation(gray, 0.0f); - float3 result_1 = HairTest::Saturation(gray, 1.0f); - float3 result_2 = HairTest::Saturation(gray, 2.0f); + float3 result_0 = Hair::Saturation(gray, 0.0f); + float3 result_1 = Hair::Saturation(gray, 1.0f); + float3 result_2 = Hair::Saturation(gray, 2.0f); ASSERT(IsTrue, abs(result_0.x - 0.5f) < TestConstants::EXACT_TOLERANCE); ASSERT(IsTrue, abs(result_1.x - 0.5f) < TestConstants::EXACT_TOLERANCE); // Note: result_2 may be clamped by saturate() } - // ============================================================================ - // SHIFT NORMAL TESTS - // ============================================================================ +// ============================================================================ +// SHIFT NORMAL TESTS +// ============================================================================ - /// @tags hair, normal, shift - [numthreads(1, 1, 1)] void TestShiftNormal() +/// @tags hair, normal, shift +[numthreads(1, 1, 1)] void TestShiftNormal() { float3 T = float3(1, 0, 0); float3 N = float3(0, 0, 1); // Zero shift should return original normal direction - float3 N_noshift = HairTest::ShiftNormal(T, N, 0.0f); + float3 N_noshift = Hair::ShiftNormal(T, N, 0.0f); ASSERT(IsTrue, abs(length(N_noshift) - 1.0f) < TestConstants::EXACT_TOLERANCE); // Shifted normal should still be unit length - float3 N_shifted = HairTest::ShiftNormal(T, N, 0.3f); + float3 N_shifted = Hair::ShiftNormal(T, N, 0.3f); ASSERT(IsTrue, abs(length(N_shifted) - 1.0f) < TestConstants::EXACT_TOLERANCE); // Shifted normal should be different from original when shift != 0 - float3 N_shifted2 = HairTest::ShiftNormal(T, N, 0.5f); + float3 N_shifted2 = Hair::ShiftNormal(T, N, 0.5f); float dotNN = dot(N, N_shifted2); ASSERT(IsTrue, dotNN < 1.0f - TestConstants::EXACT_TOLERANCE); } @@ -491,42 +367,44 @@ namespace TestConstants // ============================================================================ /// @tags hair, edge-cases, robustness -[numthreads(1, 1, 1)] void TestKajiyaKayEdgeCases() { +[numthreads(1, 1, 1)] void TestKajiyaKayEdgeCases() +{ float3 T = float3(1, 0, 0); // Very high shininess float3 H = float3(0, 1, 0); - float3 spec_high = HairTest::D_KajiyaKay(T, H, 1000.0f); + float3 spec_high = Hair::D_KajiyaKay(T, H, 1000.0f); ASSERT(IsTrue, !isnan(spec_high.x) && !isinf(spec_high.x)); ASSERT(IsTrue, spec_high.x >= 0.0f); // Very low shininess - float3 spec_low = HairTest::D_KajiyaKay(T, H, 1.0f); + float3 spec_low = Hair::D_KajiyaKay(T, H, 1.0f); ASSERT(IsTrue, !isnan(spec_low.x) && !isinf(spec_low.x)); ASSERT(IsTrue, spec_low.x >= 0.0f); // Normalized vectors (edge case: exactly parallel) - float3 spec_para = HairTest::D_KajiyaKay(T, T, 50.0f); + float3 spec_para = Hair::D_KajiyaKay(T, T, 50.0f); ASSERT(IsTrue, !isnan(spec_para.x)); } - /// @tags hair, edge-cases, robustness - [numthreads(1, 1, 1)] void TestGaussianEdgeCases() +/// @tags hair, edge-cases, robustness +[numthreads(1, 1, 1)] void TestGaussianEdgeCases() { // Very small B (very smooth hair) - float g_smooth = HairTest::Hair_g(0.01f, 0.0f); + float g_smooth = Hair::Hair_g(0.01f, 0.0f); ASSERT(IsTrue, !isnan(g_smooth) && !isinf(g_smooth)); ASSERT(IsTrue, g_smooth > 0.0f); // Large theta - float g_large = HairTest::Hair_g(0.3f, 2.0f); + float g_large = Hair::Hair_g(0.3f, 2.0f); ASSERT(IsTrue, !isnan(g_large)); ASSERT(IsTrue, g_large >= 0.0f); ASSERT(IsTrue, g_large < 1.0f); // Should be very small } /// @tags hair, edge-cases, robustness -[numthreads(1, 1, 1)] void TestDiffuseAttenuationEdgeCases() { +[numthreads(1, 1, 1)] void TestDiffuseAttenuationEdgeCases() +{ float3 baseColor = float3(0.5, 0.3, 0.2); // Colinear V and N @@ -534,29 +412,29 @@ namespace TestConstants float3 V = float3(0, 0, 1); float3 L = float3(0, 0, 1); - float3 diffuse = HairTest::GetHairDiffuseAttenuationKajiyaKay(N, V, L, 1.0f, baseColor); + float3 diffuse = Hair::GetHairDiffuseAttenuationKajiyaKay(N, V, L, 1.0f, baseColor); ASSERT(IsTrue, all(!isnan(diffuse))); // Very dark hair (near black) float3 darkHair = float3(0.01, 0.01, 0.01); - float3 diffuse_dark = HairTest::GetHairDiffuseAttenuationKajiyaKay(N, V, L, 0.5f, darkHair); + float3 diffuse_dark = Hair::GetHairDiffuseAttenuationKajiyaKay(N, V, L, 0.5f, darkHair); ASSERT(IsTrue, all(!isnan(diffuse_dark))); ASSERT(IsTrue, all(!isinf(diffuse_dark))); } - /// @tags hair, saturation, edge-cases - [numthreads(1, 1, 1)] void TestSaturationEdgeCases() +/// @tags hair, saturation, edge-cases +[numthreads(1, 1, 1)] void TestSaturationEdgeCases() { // Very saturated color float3 saturatedColor = float3(1.0, 0.0, 0.0); - float3 result = HairTest::Saturation(saturatedColor, 1.5f); + float3 result = Hair::Saturation(saturatedColor, 1.5f); ASSERT(IsTrue, all(!isnan(result))); ASSERT(IsTrue, all(result >= 0.0f)); ASSERT(IsTrue, all(result <= 1.0f)); // Should be clamped by saturate // Near-black color float3 darkColor = float3(0.001, 0.001, 0.001); - float3 result_dark = HairTest::Saturation(darkColor, 1.0f); + float3 result_dark = Hair::Saturation(darkColor, 1.0f); ASSERT(IsTrue, all(!isnan(result_dark))); } @@ -565,8 +443,9 @@ namespace TestConstants // ============================================================================ /// @tags hair, fresnel, physical -[numthreads(1, 1, 1)] void TestHairFresnelBehavior() { - float3 F0 = HairTest::HairF0(); +[numthreads(1, 1, 1)] void TestHairFresnelBehavior() +{ + float3 F0 = Hair::HairF0(); // At normal incidence, Fresnel should equal F0 float3 F_normal = BRDF::F_Schlick(F0, 1.0f); @@ -583,8 +462,8 @@ namespace TestConstants ASSERT(IsTrue, F_30.x > F_normal.x); } - /// @tags hair, specular, energy-conservation - [numthreads(1, 1, 1)] void TestKajiyaKayEnergyConservation() +/// @tags hair, specular, energy-conservation +[numthreads(1, 1, 1)] void TestKajiyaKayEnergyConservation() { float3 T = float3(1, 0, 0); float shininess = 50.0f; @@ -599,10 +478,10 @@ namespace TestConstants for (int i = 0; i < 4; i++) { float3 H = angles[i]; - float3 spec = HairTest::D_KajiyaKay(T, H, shininess); + float3 spec = Hair::D_KajiyaKay(T, H, shininess); // Should be bounded (NDF normalized) ASSERT(IsTrue, spec.x >= 0.0f); ASSERT(IsTrue, spec.x < 100.0f); // Reasonable upper bound } -} +} \ No newline at end of file From c35b54adfedbb6385f999e00d308382106a0b583 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 03:34:25 +0000 Subject: [PATCH 13/20] =?UTF-8?q?style:=20=F0=9F=8E=A8=20apply=20pre-commi?= =?UTF-8?q?t.ci=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated formatting by clang-format, prettier, and other hooks. See https://pre-commit.ci for details. --- package/Shaders/Tests/TestHair.hlsl | 123 +++++++++++++--------------- 1 file changed, 57 insertions(+), 66 deletions(-) diff --git a/package/Shaders/Tests/TestHair.hlsl b/package/Shaders/Tests/TestHair.hlsl index 72557af222..6a969ceef1 100644 --- a/package/Shaders/Tests/TestHair.hlsl +++ b/package/Shaders/Tests/TestHair.hlsl @@ -13,9 +13,9 @@ SamplerState SampColorSampler : register(s0); // Include common dependencies that Hair.hlsli needs -#include "/Shaders/Common/Math.hlsli" #include "/Shaders/Common/Color.hlsli" #include "/Shaders/Common/LightingCommon.hlsli" +#include "/Shaders/Common/Math.hlsli" // Stub BRDF functions used by Hair.hlsli namespace BRDF @@ -62,23 +62,23 @@ namespace SharedData // Test configuration with typical values static HairSpecularSettings hairSpecularSettings = { - 1, // Enabled - 0.5f, // HairGlossiness - 1.0f, // SpecularMult - 1.0f, // DiffuseMult - 0, // EnableTangentShift (disabled for predictable tests) - 0.0f, // PrimaryTangentShift - 0.5f, // SecondaryTangentShift - 1.0f, // HairSaturation - 1.0f, // SpecularIndirectMult - 1.0f, // DiffuseIndirectMult - 1.0f, // BaseColorMult - 0.5f, // Transmission - 0, // EnableSelfShadow (disabled) - 0.5f, // SelfShadowStrength - 2.0f, // SelfShadowExponent - 1.0f, // SelfShadowScale - 0, // HairMode (0 = Scheuermann) + 1, // Enabled + 0.5f, // HairGlossiness + 1.0f, // SpecularMult + 1.0f, // DiffuseMult + 0, // EnableTangentShift (disabled for predictable tests) + 0.0f, // PrimaryTangentShift + 0.5f, // SecondaryTangentShift + 1.0f, // HairSaturation + 1.0f, // SpecularIndirectMult + 1.0f, // DiffuseIndirectMult + 1.0f, // BaseColorMult + 0.5f, // Transmission + 0, // EnableSelfShadow (disabled) + 0.5f, // SelfShadowStrength + 2.0f, // SelfShadowExponent + 1.0f, // SelfShadowScale + 0, // HairMode (0 = Scheuermann) uint3(0, 0, 0) }; } @@ -104,8 +104,7 @@ namespace TestConstants // ============================================================================ /// @tags hair, tangent -[numthreads(1, 1, 1)] void TestReorientTangent() -{ +[numthreads(1, 1, 1)] void TestReorientTangent() { // Test: Tangent perpendicular to normal should remain unchanged float3 N = float3(0, 0, 1); float3 T = float3(1, 0, 0); // Already perpendicular to N @@ -123,8 +122,8 @@ namespace TestConstants ASSERT(IsTrue, abs(length(T_tilted_reoriented) - 1.0f) < TestConstants::EXACT_TOLERANCE); } -/// @tags hair, tangent -[numthreads(1, 1, 1)] void TestShiftTangent() + /// @tags hair, tangent + [numthreads(1, 1, 1)] void TestShiftTangent() { float3 T = float3(1, 0, 0); float3 N = float3(0, 0, 1); @@ -149,8 +148,7 @@ namespace TestConstants // ============================================================================ /// @tags hair, specular, kajiya-kay -[numthreads(1, 1, 1)] void TestKajiyaKayBasic() -{ +[numthreads(1, 1, 1)] void TestKajiyaKayBasic() { float3 T = float3(1, 0, 0); // Tangent along X float3 H = float3(0, 1, 0); // Half vector along Y (perpendicular to T) float shininess = 50.0f; @@ -166,8 +164,8 @@ namespace TestConstants ASSERT(IsTrue, abs(spec.y - spec.z) < TestConstants::EXACT_TOLERANCE); } -/// @tags hair, specular, kajiya-kay -[numthreads(1, 1, 1)] void TestKajiyaKayShininessEffect() + /// @tags hair, specular, kajiya-kay + [numthreads(1, 1, 1)] void TestKajiyaKayShininessEffect() { float3 T = float3(1, 0, 0); float3 H = normalize(float3(0, 1, 0.5)); @@ -186,8 +184,7 @@ namespace TestConstants } /// @tags hair, specular, kajiya-kay -[numthreads(1, 1, 1)] void TestKajiyaKayDirectionalAttenuation() -{ +[numthreads(1, 1, 1)] void TestKajiyaKayDirectionalAttenuation() { float3 T = float3(1, 0, 0); float shininess = 50.0f; @@ -202,12 +199,12 @@ namespace TestConstants ASSERT(IsTrue, spec_anti.x < TestConstants::EXACT_TOLERANCE); } -// ============================================================================ -// HAIR F0 TESTS -// ============================================================================ + // ============================================================================ + // HAIR F0 TESTS + // ============================================================================ -/// @tags hair, fresnel -[numthreads(1, 1, 1)] void TestHairF0() + /// @tags hair, fresnel + [numthreads(1, 1, 1)] void TestHairF0() { float3 F0 = Hair::HairF0(); @@ -227,8 +224,7 @@ namespace TestConstants // ============================================================================ /// @tags hair, marschner, gaussian -[numthreads(1, 1, 1)] void TestHairGaussian() -{ +[numthreads(1, 1, 1)] void TestHairGaussian() { float B = 0.3f; // Beta (roughness) // At theta = 0, should be maximum @@ -252,12 +248,12 @@ namespace TestConstants ASSERT(IsTrue, g_smooth > g_rough); } -// ============================================================================ -// DIFFUSE ATTENUATION TESTS -// ============================================================================ + // ============================================================================ + // DIFFUSE ATTENUATION TESTS + // ============================================================================ -/// @tags hair, diffuse, kajiya-kay -[numthreads(1, 1, 1)] void TestHairDiffuseAttenuation() + /// @tags hair, diffuse, kajiya-kay + [numthreads(1, 1, 1)] void TestHairDiffuseAttenuation() { float3 N = float3(0, 0, 1); float3 V = normalize(float3(0, 0.5, 1)); @@ -279,8 +275,7 @@ namespace TestConstants } /// @tags hair, diffuse, kajiya-kay -[numthreads(1, 1, 1)] void TestHairDiffuseBaseColorEffect() -{ +[numthreads(1, 1, 1)] void TestHairDiffuseBaseColorEffect() { float3 N = float3(0, 0, 1); float3 V = normalize(float3(0, 0.5, 1)); float3 L = normalize(float3(0, 0.5, 1)); @@ -295,12 +290,12 @@ namespace TestConstants ASSERT(IsTrue, diffuse_light.x > diffuse_dark.x); } -// ============================================================================ -// SATURATION TESTS -// ============================================================================ + // ============================================================================ + // SATURATION TESTS + // ============================================================================ -/// @tags hair, saturation, color -[numthreads(1, 1, 1)] void TestHairSaturation() + /// @tags hair, saturation, color + [numthreads(1, 1, 1)] void TestHairSaturation() { float3 color = float3(0.8, 0.4, 0.2); @@ -324,8 +319,7 @@ namespace TestConstants } /// @tags hair, saturation, color -[numthreads(1, 1, 1)] void TestHairSaturationGrayscale() -{ +[numthreads(1, 1, 1)] void TestHairSaturationGrayscale() { // Grayscale input should be unaffected by saturation changes float3 gray = float3(0.5, 0.5, 0.5); @@ -338,12 +332,12 @@ namespace TestConstants // Note: result_2 may be clamped by saturate() } -// ============================================================================ -// SHIFT NORMAL TESTS -// ============================================================================ + // ============================================================================ + // SHIFT NORMAL TESTS + // ============================================================================ -/// @tags hair, normal, shift -[numthreads(1, 1, 1)] void TestShiftNormal() + /// @tags hair, normal, shift + [numthreads(1, 1, 1)] void TestShiftNormal() { float3 T = float3(1, 0, 0); float3 N = float3(0, 0, 1); @@ -367,8 +361,7 @@ namespace TestConstants // ============================================================================ /// @tags hair, edge-cases, robustness -[numthreads(1, 1, 1)] void TestKajiyaKayEdgeCases() -{ +[numthreads(1, 1, 1)] void TestKajiyaKayEdgeCases() { float3 T = float3(1, 0, 0); // Very high shininess @@ -387,8 +380,8 @@ namespace TestConstants ASSERT(IsTrue, !isnan(spec_para.x)); } -/// @tags hair, edge-cases, robustness -[numthreads(1, 1, 1)] void TestGaussianEdgeCases() + /// @tags hair, edge-cases, robustness + [numthreads(1, 1, 1)] void TestGaussianEdgeCases() { // Very small B (very smooth hair) float g_smooth = Hair::Hair_g(0.01f, 0.0f); @@ -403,8 +396,7 @@ namespace TestConstants } /// @tags hair, edge-cases, robustness -[numthreads(1, 1, 1)] void TestDiffuseAttenuationEdgeCases() -{ +[numthreads(1, 1, 1)] void TestDiffuseAttenuationEdgeCases() { float3 baseColor = float3(0.5, 0.3, 0.2); // Colinear V and N @@ -422,8 +414,8 @@ namespace TestConstants ASSERT(IsTrue, all(!isinf(diffuse_dark))); } -/// @tags hair, saturation, edge-cases -[numthreads(1, 1, 1)] void TestSaturationEdgeCases() + /// @tags hair, saturation, edge-cases + [numthreads(1, 1, 1)] void TestSaturationEdgeCases() { // Very saturated color float3 saturatedColor = float3(1.0, 0.0, 0.0); @@ -443,8 +435,7 @@ namespace TestConstants // ============================================================================ /// @tags hair, fresnel, physical -[numthreads(1, 1, 1)] void TestHairFresnelBehavior() -{ +[numthreads(1, 1, 1)] void TestHairFresnelBehavior() { float3 F0 = Hair::HairF0(); // At normal incidence, Fresnel should equal F0 @@ -462,8 +453,8 @@ namespace TestConstants ASSERT(IsTrue, F_30.x > F_normal.x); } -/// @tags hair, specular, energy-conservation -[numthreads(1, 1, 1)] void TestKajiyaKayEnergyConservation() + /// @tags hair, specular, energy-conservation + [numthreads(1, 1, 1)] void TestKajiyaKayEnergyConservation() { float3 T = float3(1, 0, 0); float shininess = 50.0f; From e0bd064bdcebff066dffc41817ac1852df7a6462 Mon Sep 17 00:00:00 2001 From: Jiaye Date: Fri, 27 Feb 2026 11:50:34 +0800 Subject: [PATCH 14/20] fix(tests): update shader test dependencies and include feature shader paths --- package/Shaders/Tests/TestHair.hlsl | 39 ++++++++--------------------- tests/shaders/CMakeLists.txt | 15 +++++++++++ 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/package/Shaders/Tests/TestHair.hlsl b/package/Shaders/Tests/TestHair.hlsl index 6a969ceef1..2b0a9d3065 100644 --- a/package/Shaders/Tests/TestHair.hlsl +++ b/package/Shaders/Tests/TestHair.hlsl @@ -6,36 +6,14 @@ // 3. Testing Hair namespace functions directly // ============================================================================ -// STUBS FOR EXTERNAL DEPENDENCIES +// STUBS FOR EXTERNAL DEPENDENCIES (must be defined BEFORE including Hair.hlsli) // ============================================================================ // Stub sampler required by Hair.hlsli SamplerState SampColorSampler : register(s0); -// Include common dependencies that Hair.hlsli needs -#include "/Shaders/Common/Color.hlsli" -#include "/Shaders/Common/LightingCommon.hlsli" -#include "/Shaders/Common/Math.hlsli" - -// Stub BRDF functions used by Hair.hlsli -namespace BRDF -{ - float3 F_Schlick(float3 F0, float VdotH) - { - float Fc = pow(1 - VdotH, 5); - return F0 + (1 - F0) * Fc; - } - - float2 EnvBRDF(float roughness, float NdotV) - { - float a = roughness * roughness; - float x = 1 - NdotV; - float x2 = x * x; - return float2(saturate(1 - a + a * x2), saturate(x2 * 0.5)); - } -} - // Stub SharedData namespace with hairSpecularSettings +// This must be defined before Hair.hlsli is included since it uses SharedData::hairSpecularSettings namespace SharedData { struct HairSpecularSettings @@ -84,10 +62,15 @@ namespace SharedData } // ============================================================================ -// INCLUDE THE REAL HAIR.HLSLI +// INCLUDE THE REAL HAIR.HLSLI AND ITS DEPENDENCIES // ============================================================================ +// Hair.hlsli includes: Common/BRDF.hlsli, Common/Color.hlsli, Common/Game.hlsli, Common/Math.hlsli +// These are all real files from the codebase that will be included automatically #include "/Shaders/Hair/Hair.hlsli" +// Include common dependencies needed for tests (LightingCommon provides struct definitions) +#include "/Shaders/Common/LightingCommon.hlsli" + #include "/Test/STF/ShaderTestFramework.hlsli" // Test tolerance constants @@ -308,9 +291,9 @@ namespace TestConstants // Saturation = 0 should return grayscale (luminance) float3 result_0 = Hair::Saturation(color, 0.0f); float luma = Color::RGBToLuminance(color); - ASSERT(IsTrue, abs(result_0.x - luma) < TestConstants::EXACT_TOLERANCE); - ASSERT(IsTrue, abs(result_0.y - luma) < TestConstants::EXACT_TOLERANCE); - ASSERT(IsTrue, abs(result_0.z - luma) < TestConstants::EXACT_TOLERANCE); +ASSERT(IsTrue, abs(result_0.x - luma) < TestConstants::EXACT_TOLERANCE); +ASSERT(IsTrue, abs(result_0.y - luma) < TestConstants::EXACT_TOLERANCE); +ASSERT(IsTrue, abs(result_0.z - luma) < TestConstants::EXACT_TOLERANCE); // Saturation = 0.5 should be halfway float3 result_half = Hair::Saturation(color, 0.5f); diff --git a/tests/shaders/CMakeLists.txt b/tests/shaders/CMakeLists.txt index 0df3773cc2..a691a849c3 100644 --- a/tests/shaders/CMakeLists.txt +++ b/tests/shaders/CMakeLists.txt @@ -42,6 +42,9 @@ include(DetectGraphicsTools) set(SHADER_SOURCE_DIR ${CMAKE_SOURCE_DIR}/package/Shaders) set(SHADER_SOURCE_REL_DIR "Shaders") +# Set feature shader source paths (for Hair Specular, etc.) +set(FEATURES_DIR ${CMAKE_SOURCE_DIR}/features) + # ============================================================================ # RUNTIME DISCOVERY: No code generation needed! # ============================================================================ @@ -74,6 +77,18 @@ asset_dependency_init(shader_tests) # Add the shader source directory to be copied relative to the exe target_add_asset_directory(shader_tests ${SHADER_SOURCE_DIR} "/${SHADER_SOURCE_REL_DIR}") +# Add feature shader directories (Hair Specular, etc.) to virtual shader paths +# These are mapped so that #include "/Shaders/Hair/Hair.hlsli" works in tests +file(GLOB FEATURE_SHADER_DIRS + LIST_DIRECTORIES true + "${FEATURES_DIR}/*/Shaders" +) +foreach(FEATURE_SHADER_DIR ${FEATURE_SHADER_DIRS}) + if(IS_DIRECTORY ${FEATURE_SHADER_DIR}) + target_add_asset_directory(shader_tests ${FEATURE_SHADER_DIR} "/${SHADER_SOURCE_REL_DIR}") + endif() +endforeach() + # Optional: Make shader tests depend on HLSL test files for automatic rebuild triggers # This ensures that changing HLSL test files triggers a relink (to update copied assets) file(GLOB_RECURSE HLSL_TEST_FILES "${SHADER_SOURCE_DIR}/Tests/Test*.hlsl") From 4e7b31bb034f8a68495cb4e2cf12930a653d7c33 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 03:51:06 +0000 Subject: [PATCH 15/20] =?UTF-8?q?style:=20=F0=9F=8E=A8=20apply=20pre-commi?= =?UTF-8?q?t.ci=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated formatting by clang-format, prettier, and other hooks. See https://pre-commit.ci for details. --- package/Shaders/Tests/TestHair.hlsl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package/Shaders/Tests/TestHair.hlsl b/package/Shaders/Tests/TestHair.hlsl index 2b0a9d3065..e5ab14f5aa 100644 --- a/package/Shaders/Tests/TestHair.hlsl +++ b/package/Shaders/Tests/TestHair.hlsl @@ -291,9 +291,9 @@ namespace TestConstants // Saturation = 0 should return grayscale (luminance) float3 result_0 = Hair::Saturation(color, 0.0f); float luma = Color::RGBToLuminance(color); -ASSERT(IsTrue, abs(result_0.x - luma) < TestConstants::EXACT_TOLERANCE); -ASSERT(IsTrue, abs(result_0.y - luma) < TestConstants::EXACT_TOLERANCE); -ASSERT(IsTrue, abs(result_0.z - luma) < TestConstants::EXACT_TOLERANCE); + ASSERT(IsTrue, abs(result_0.x - luma) < TestConstants::EXACT_TOLERANCE); + ASSERT(IsTrue, abs(result_0.y - luma) < TestConstants::EXACT_TOLERANCE); + ASSERT(IsTrue, abs(result_0.z - luma) < TestConstants::EXACT_TOLERANCE); // Saturation = 0.5 should be halfway float3 result_half = Hair::Saturation(color, 0.5f); From f51dda71f8ce1b0a9823e7c0045ce2960d87f134 Mon Sep 17 00:00:00 2001 From: Jiaye Date: Fri, 27 Feb 2026 12:48:23 +0800 Subject: [PATCH 16/20] feat(hair): add PBR lighting constants and include common dependencies for tests --- package/Shaders/Common/Color.hlsli | 4 ++++ package/Shaders/Tests/TestHair.hlsl | 2 ++ 2 files changed, 6 insertions(+) diff --git a/package/Shaders/Common/Color.hlsli b/package/Shaders/Common/Color.hlsli index 023a72b70e..14478daae8 100644 --- a/package/Shaders/Common/Color.hlsli +++ b/package/Shaders/Common/Color.hlsli @@ -319,6 +319,10 @@ namespace Color return ENABLE_LL ? SharedData::linearLightingSettings.vanillaDiffuseColorMult : 1.0f; } #else + const static float PBRLightingScale = 1.0; + const static float ReflectionNormalisationScale = 1.0; + const static float PBRLightingCompensation = Math::PI; + float3 Diffuse(float3 color) { # if defined(TRUE_PBR) diff --git a/package/Shaders/Tests/TestHair.hlsl b/package/Shaders/Tests/TestHair.hlsl index e5ab14f5aa..95d04b3ced 100644 --- a/package/Shaders/Tests/TestHair.hlsl +++ b/package/Shaders/Tests/TestHair.hlsl @@ -64,6 +64,8 @@ namespace SharedData // ============================================================================ // INCLUDE THE REAL HAIR.HLSLI AND ITS DEPENDENCIES // ============================================================================ +// Include common dependencies needed for tests (LightingCommon provides struct definitions) +#include "/Shaders/Common/LightingCommon.hlsli" // Hair.hlsli includes: Common/BRDF.hlsli, Common/Color.hlsli, Common/Game.hlsli, Common/Math.hlsli // These are all real files from the codebase that will be included automatically #include "/Shaders/Hair/Hair.hlsli" From 138ebc936dd3a9baba3bdb46537fe59cec3bdea8 Mon Sep 17 00:00:00 2001 From: Jiaye Date: Fri, 27 Feb 2026 12:50:21 +0800 Subject: [PATCH 17/20] fix(hair): add stub function for screen depth in SharedData namespace --- package/Shaders/Tests/TestHair.hlsl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package/Shaders/Tests/TestHair.hlsl b/package/Shaders/Tests/TestHair.hlsl index 95d04b3ced..ebd41909e3 100644 --- a/package/Shaders/Tests/TestHair.hlsl +++ b/package/Shaders/Tests/TestHair.hlsl @@ -59,6 +59,8 @@ namespace SharedData 0, // HairMode (0 = Scheuermann) uint3(0, 0, 0) }; + + float GetScreenDepth(float2 uv, uint index = 0) { return 1.0f; } // Stub function for screen depth, if needed by Hair.hlsli } // ============================================================================ From 4d8149bac91f6ff7f71c7d1d3facd1ff73808b63 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 04:51:20 +0000 Subject: [PATCH 18/20] =?UTF-8?q?style:=20=F0=9F=8E=A8=20apply=20pre-commi?= =?UTF-8?q?t.ci=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated formatting by clang-format, prettier, and other hooks. See https://pre-commit.ci for details. --- package/Shaders/Tests/TestHair.hlsl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/Shaders/Tests/TestHair.hlsl b/package/Shaders/Tests/TestHair.hlsl index ebd41909e3..2117e9465b 100644 --- a/package/Shaders/Tests/TestHair.hlsl +++ b/package/Shaders/Tests/TestHair.hlsl @@ -60,7 +60,7 @@ namespace SharedData uint3(0, 0, 0) }; - float GetScreenDepth(float2 uv, uint index = 0) { return 1.0f; } // Stub function for screen depth, if needed by Hair.hlsli + float GetScreenDepth(float2 uv, uint index = 0) { return 1.0f; } // Stub function for screen depth, if needed by Hair.hlsli } // ============================================================================ From 091fdda8adc397f74f128991fbffc51d85b3cbe5 Mon Sep 17 00:00:00 2001 From: Jiaye Date: Fri, 27 Feb 2026 13:22:29 +0800 Subject: [PATCH 19/20] add hair define --- package/Shaders/Tests/TestHair.hlsl | 1 + 1 file changed, 1 insertion(+) diff --git a/package/Shaders/Tests/TestHair.hlsl b/package/Shaders/Tests/TestHair.hlsl index 2117e9465b..be82a46328 100644 --- a/package/Shaders/Tests/TestHair.hlsl +++ b/package/Shaders/Tests/TestHair.hlsl @@ -4,6 +4,7 @@ // 1. Defining necessary stubs for external dependencies (SharedData, textures, etc.) // 2. Including the real Hair.hlsli file // 3. Testing Hair namespace functions directly +#define CS_HAIR // ============================================================================ // STUBS FOR EXTERNAL DEPENDENCIES (must be defined BEFORE including Hair.hlsli) From 26d3d139d3e0256e185b53c37dc19e44e4474efa Mon Sep 17 00:00:00 2001 From: Jiaye Date: Fri, 27 Feb 2026 13:41:50 +0800 Subject: [PATCH 20/20] add HAIR define to test --- package/Shaders/Tests/TestHair.hlsl | 1 + 1 file changed, 1 insertion(+) diff --git a/package/Shaders/Tests/TestHair.hlsl b/package/Shaders/Tests/TestHair.hlsl index be82a46328..a51e9d527c 100644 --- a/package/Shaders/Tests/TestHair.hlsl +++ b/package/Shaders/Tests/TestHair.hlsl @@ -5,6 +5,7 @@ // 2. Including the real Hair.hlsli file // 3. Testing Hair namespace functions directly #define CS_HAIR +#define HAIR // ============================================================================ // STUBS FOR EXTERNAL DEPENDENCIES (must be defined BEFORE including Hair.hlsli)