From dfe3014539bf54b31a444599c575381b5cec8867 Mon Sep 17 00:00:00 2001 From: doodlum <15017472+doodlum@users.noreply.github.com> Date: Wed, 22 Apr 2026 12:18:26 +0100 Subject: [PATCH 1/5] feat(grass): sss/skylighting improvements Remap skylightingNormal z into [0,1] before cosine-lobe evaluation so backlit grass retains ambient response. Divide out vertex AO from the skylighting term (clamped to prevent blowup), producing a clean min(skylighting, vertexAO) relationship. Replace additive sss/subsurfaceColor split with a saturated albedo lerp toward full-luma (dot(albedo, 1/3)) gated by VertexNormal.w. Drop the manual normal.z clamp in complex grass since the skylightingNormal remap handles it. Requires the Skylighting::Sample API refactor. --- package/Shaders/RunGrass.hlsl | 97 +++++++++++++++++++---------------- 1 file changed, 52 insertions(+), 45 deletions(-) diff --git a/package/Shaders/RunGrass.hlsl b/package/Shaders/RunGrass.hlsl index 338af654d2..5318f59a02 100644 --- a/package/Shaders/RunGrass.hlsl +++ b/package/Shaders/RunGrass.hlsl @@ -521,14 +521,10 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float screenNoise = Random::InterleavedGradientNoise(input.HPosition.xy, SharedData::FrameCount); // Swaps direction of the backfaces otherwise they seem to get lit from the wrong direction. - if (!(Permutation::ExtraShaderDescriptor & Permutation::ExtraFlags::GrassSphereNormal)) { + if (!(Permutation::ExtraShaderDescriptor & Permutation::ExtraFlags::GrassSphereNormal)) if (!frontFace) normal = -normal; - normal.z = max(0.0, normal.z); - normal = normalize(float3(normal.xy, max(0, normal.z))); - } - float3x3 tbn = 0; # if !defined(TRUE_PBR) @@ -648,20 +644,42 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) lightsDiffuseColor += dirLightColor * dirDetailedShadow * saturate(dirLightAngle) * Color::VanillaNormalization(); } - float3 vertexColor = input.VertexColor.xyz; + float3 vertexColor = Color::ColorToLinear(input.VertexColor.xyz); # if defined(SKYLIGHTING) + float skylightingDiffuse = 1.0; float skylightingFadeOutFactor = 1.0; + if (!SharedData::InInterior) { - skylightingFadeOutFactor = Skylighting::getFadeOutFactor(input.WorldPosition.xyz); - vertexColor = lerp(input.VertexColor.xyz * input.VertexMult, vertexColor, skylightingFadeOutFactor); + skylightingFadeOutFactor = Skylighting::GetFadeOutFactor(input.WorldPosition.xyz); + +# if defined(VR) + float3 positionMSSkylight = input.WorldPosition.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz - FrameBuffer::CameraPosAdjust[0].xyz; +# else + float3 positionMSSkylight = input.WorldPosition.xyz; +# endif + + float3 skylightingNormal = normal; + skylightingNormal.z = skylightingNormal.z * 0.5 + 0.5; + skylightingNormal = normalize(skylightingNormal); + + sh2 skylightingSH = Skylighting::Sample(input.HPosition.xy, positionMSSkylight, normal); + skylightingDiffuse = SphericalHarmonics::FuncProductIntegral(skylightingSH, SphericalHarmonics::EvaluateCosineLobe(skylightingNormal)) / Math::PI; + skylightingDiffuse = saturate(skylightingDiffuse); + skylightingDiffuse = lerp(1.0, skylightingDiffuse, skylightingFadeOutFactor); + skylightingDiffuse = Skylighting::MixDiffuse(skylightingDiffuse); + + float vertexAO = max(max(vertexColor.r, vertexColor.g), vertexColor.b); + // Modify skylightingDiffuse such that skylightingDiffuse * vertexAO = min(skylightingDiffuse, vertexAO) + skylightingDiffuse = saturate(skylightingDiffuse / max(vertexAO, 1e-5)); } -# endif +# endif // SKYLIGHTING - float3 albedo = max(0, baseColor.xyz * Color::ColorToLinear(vertexColor)); + float3 albedo = baseColor.xyz * vertexColor; - float3 subsurfaceColor = lerp(dot(albedo, 1.0 / 3.0), albedo, 2.0) * saturate(input.VertexNormal.w * 10.0); - float3 sss = dirLightColor * dirSoftShadow * saturate(-dirLightAngle) * Color::VanillaNormalization(); + float3 subsurfaceAlbedo = lerp(dot(albedo, 1.0 / 3.0), albedo, 2.0) * saturate(input.VertexNormal.w * 10.0) * albedo; + + float3 subsurfaceColor = dirLightColor * dirSoftShadow * saturate(-dirLightAngle) * Color::VanillaNormalization(); if (complex) lightsSpecularColor += dirDetailedShadow * GrassLighting::GetLightSpecularInput(SharedData::DirLightDirection.xyz, viewDirection, normal, dirLightColor, SharedData::grassLightingSettings.Glossiness) * Color::VanillaNormalization(); @@ -730,7 +748,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) lightDiffuseColor = lightColor * saturate(lightAngle); } - sss += lightColor * saturate(-lightAngle); + subsurfaceColor += lightColor * saturate(-lightAngle); lightsDiffuseColor += lightDiffuseColor * Color::VanillaNormalization(); @@ -756,22 +774,6 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float3 directionalAmbientColor = Color::Ambient(max(0, SharedData::GetAmbient(normal))); -# if defined(SKYLIGHTING) - float skylightingDiffuse = 1.0; - if (!SharedData::InInterior) { -# if defined(VR) - float3 positionMSSkylight = input.WorldPosition.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz - FrameBuffer::CameraPosAdjust[0].xyz; -# else - float3 positionMSSkylight = input.WorldPosition.xyz; -# endif - sh2 skylightingSH = Skylighting::sample(SharedData::skylightingSettings, Skylighting::SkylightingProbeArray, Skylighting::stbn_vec3_2Dx1D_128x128x64, input.HPosition.xy, positionMSSkylight, normal); - skylightingDiffuse = SphericalHarmonics::FuncProductIntegral(skylightingSH, SphericalHarmonics::EvaluateCosineLobe(normal)) / Math::PI; - skylightingDiffuse = saturate(skylightingDiffuse); - skylightingDiffuse = lerp(1.0, skylightingDiffuse, skylightingFadeOutFactor); - skylightingDiffuse = Skylighting::mixDiffuse(SharedData::skylightingSettings, skylightingDiffuse); - } -# endif // SKYLIGHTING - # if defined(IBL) if (SharedData::iblSettings.EnableIBL) { # if defined(SKYLIGHTING) @@ -785,7 +787,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) diffuseColor += directionalAmbientColor; diffuseColor *= albedo; - diffuseColor += max(0, sss * subsurfaceColor * SharedData::grassLightingSettings.SubsurfaceScatteringAmount); + diffuseColor += max(0, subsurfaceColor * subsurfaceAlbedo * SharedData::grassLightingSettings.SubsurfaceScatteringAmount); directionalAmbientColor *= albedo; @@ -794,7 +796,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) if (!SharedData::iblSettings.EnableIBL) # endif { - Skylighting::applySkylighting(diffuseColor, directionalAmbientColor, albedo, skylightingDiffuse); + Skylighting::ApplySkylighting(diffuseColor, directionalAmbientColor, albedo, skylightingDiffuse); } # endif @@ -935,34 +937,39 @@ PS_OUTPUT main(PS_INPUT input) normal = normalize(float3(normal.xy, max(0, normal.z))); - float3 vertexColor = input.VertexColor.xyz; + float3 vertexColor = Color::ColorToLinear(input.VertexColor.xyz); # if defined(SKYLIGHTING) + float skylightingDiffuse = 1.0; float skylightingFadeOutFactor = 1.0; - if (!SharedData::InInterior) { - skylightingFadeOutFactor = Skylighting::getFadeOutFactor(input.WorldPosition.xyz); - vertexColor = lerp(input.VertexColor.xyz * input.VertexMult, vertexColor, skylightingFadeOutFactor); - } -# endif - - float3 directionalAmbientColor = Color::Ambient(max(0, SharedData::GetAmbient(normal))); -# if defined(SKYLIGHTING) - float skylightingDiffuse = 1.0; if (!SharedData::InInterior) { + skylightingFadeOutFactor = Skylighting::GetFadeOutFactor(input.WorldPosition.xyz); + # if defined(VR) float3 positionMSSkylight = input.WorldPosition.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz - FrameBuffer::CameraPosAdjust[0].xyz; # else float3 positionMSSkylight = input.WorldPosition.xyz; # endif - sh2 skylightingSH = Skylighting::sample(SharedData::skylightingSettings, Skylighting::SkylightingProbeArray, Skylighting::stbn_vec3_2Dx1D_128x128x64, input.HPosition.xy, positionMSSkylight, normal); - skylightingDiffuse = SphericalHarmonics::FuncProductIntegral(skylightingSH, SphericalHarmonics::EvaluateCosineLobe(normal)) / Math::PI; + + float3 skylightingNormal = normal; + skylightingNormal.z = skylightingNormal.z * 0.5 + 0.5; + skylightingNormal = normalize(skylightingNormal); + + sh2 skylightingSH = Skylighting::Sample(input.HPosition.xy, positionMSSkylight, normal); + skylightingDiffuse = SphericalHarmonics::FuncProductIntegral(skylightingSH, SphericalHarmonics::EvaluateCosineLobe(skylightingNormal)) / Math::PI; skylightingDiffuse = saturate(skylightingDiffuse); skylightingDiffuse = lerp(1.0, skylightingDiffuse, skylightingFadeOutFactor); - skylightingDiffuse = Skylighting::mixDiffuse(SharedData::skylightingSettings, skylightingDiffuse); + skylightingDiffuse = Skylighting::MixDiffuse(skylightingDiffuse); + + float vertexAO = max(max(vertexColor.r, vertexColor.g), vertexColor.b); + // Modify skylightingDiffuse such that skylightingDiffuse * vertexAO = min(skylightingDiffuse, vertexAO) + skylightingDiffuse = saturate(skylightingDiffuse / max(vertexAO, 1e-5)); } # endif // SKYLIGHTING + float3 directionalAmbientColor = Color::Ambient(max(0, SharedData::GetAmbient(normal))); + # if defined(IBL) if (SharedData::iblSettings.EnableIBL) { # if defined(SKYLIGHTING) @@ -985,7 +992,7 @@ PS_OUTPUT main(PS_INPUT input) if (!SharedData::iblSettings.EnableIBL) # endif { - Skylighting::applySkylighting(diffuseColor, directionalAmbientColor, albedo, skylightingDiffuse); + Skylighting::ApplySkylighting(diffuseColor, directionalAmbientColor, albedo, skylightingDiffuse); } # endif From a2dffdecba87598c6a0d2b7ae032d344ce139be8 Mon Sep 17 00:00:00 2001 From: doodlum <15017472+doodlum@users.noreply.github.com> Date: Wed, 22 Apr 2026 12:50:50 +0100 Subject: [PATCH 2/5] feat(grass): linearize vertex color; rework subsurface term Linearize input.VertexColor at the point of use so it composes correctly with the already-linear baseColor. Both main() functions treat vertexColor as linear from here on, which lets the albedo expression drop the redundant outer saturate and double linearization. Re-express subsurface scattering as two clearer quantities: subsurfaceColor accumulates the backlit light-direction terms, and subsurfaceAlbedo bakes the VertexNormal.w weighting into albedo so the final diffuse blend is a simple product. Rename the old sss accumulator accordingly. Drop the post-frontFace normal.z clamp in the !GrassSphereNormal branch since the backface flip and later normalization already produce a valid outward-facing normal. Co-Authored-By: Claude Opus 4.7 --- package/Shaders/RunGrass.hlsl | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/package/Shaders/RunGrass.hlsl b/package/Shaders/RunGrass.hlsl index 338af654d2..04cd092520 100644 --- a/package/Shaders/RunGrass.hlsl +++ b/package/Shaders/RunGrass.hlsl @@ -521,14 +521,10 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float screenNoise = Random::InterleavedGradientNoise(input.HPosition.xy, SharedData::FrameCount); // Swaps direction of the backfaces otherwise they seem to get lit from the wrong direction. - if (!(Permutation::ExtraShaderDescriptor & Permutation::ExtraFlags::GrassSphereNormal)) { + if (!(Permutation::ExtraShaderDescriptor & Permutation::ExtraFlags::GrassSphereNormal)) if (!frontFace) normal = -normal; - normal.z = max(0.0, normal.z); - normal = normalize(float3(normal.xy, max(0, normal.z))); - } - float3x3 tbn = 0; # if !defined(TRUE_PBR) @@ -648,20 +644,21 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) lightsDiffuseColor += dirLightColor * dirDetailedShadow * saturate(dirLightAngle) * Color::VanillaNormalization(); } - float3 vertexColor = input.VertexColor.xyz; + float3 vertexColor = Color::ColorToLinear(input.VertexColor.xyz); # if defined(SKYLIGHTING) float skylightingFadeOutFactor = 1.0; if (!SharedData::InInterior) { skylightingFadeOutFactor = Skylighting::getFadeOutFactor(input.WorldPosition.xyz); - vertexColor = lerp(input.VertexColor.xyz * input.VertexMult, vertexColor, skylightingFadeOutFactor); + vertexColor = lerp(Color::ColorToLinear(input.VertexColor.xyz * input.VertexMult), vertexColor, skylightingFadeOutFactor); } # endif - float3 albedo = max(0, baseColor.xyz * Color::ColorToLinear(vertexColor)); + float3 albedo = baseColor.xyz * vertexColor; + + float3 subsurfaceAlbedo = lerp(dot(albedo, 1.0 / 3.0), albedo, 2.0) * saturate(input.VertexNormal.w * 10.0) * albedo; - float3 subsurfaceColor = lerp(dot(albedo, 1.0 / 3.0), albedo, 2.0) * saturate(input.VertexNormal.w * 10.0); - float3 sss = dirLightColor * dirSoftShadow * saturate(-dirLightAngle) * Color::VanillaNormalization(); + float3 subsurfaceColor = dirLightColor * dirSoftShadow * saturate(-dirLightAngle) * Color::VanillaNormalization(); if (complex) lightsSpecularColor += dirDetailedShadow * GrassLighting::GetLightSpecularInput(SharedData::DirLightDirection.xyz, viewDirection, normal, dirLightColor, SharedData::grassLightingSettings.Glossiness) * Color::VanillaNormalization(); @@ -730,7 +727,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) lightDiffuseColor = lightColor * saturate(lightAngle); } - sss += lightColor * saturate(-lightAngle); + subsurfaceColor += lightColor * saturate(-lightAngle); lightsDiffuseColor += lightDiffuseColor * Color::VanillaNormalization(); @@ -785,7 +782,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) diffuseColor += directionalAmbientColor; diffuseColor *= albedo; - diffuseColor += max(0, sss * subsurfaceColor * SharedData::grassLightingSettings.SubsurfaceScatteringAmount); + diffuseColor += max(0, subsurfaceColor * subsurfaceAlbedo * SharedData::grassLightingSettings.SubsurfaceScatteringAmount); directionalAmbientColor *= albedo; @@ -935,13 +932,13 @@ PS_OUTPUT main(PS_INPUT input) normal = normalize(float3(normal.xy, max(0, normal.z))); - float3 vertexColor = input.VertexColor.xyz; + float3 vertexColor = Color::ColorToLinear(input.VertexColor.xyz); # if defined(SKYLIGHTING) float skylightingFadeOutFactor = 1.0; if (!SharedData::InInterior) { skylightingFadeOutFactor = Skylighting::getFadeOutFactor(input.WorldPosition.xyz); - vertexColor = lerp(input.VertexColor.xyz * input.VertexMult, vertexColor, skylightingFadeOutFactor); + vertexColor = lerp(Color::ColorToLinear(input.VertexColor.xyz * input.VertexMult), vertexColor, skylightingFadeOutFactor); } # endif From b76cec938481dde909a5ed0d3a1ecea3734725f7 Mon Sep 17 00:00:00 2001 From: doodlum <15017472+doodlum@users.noreply.github.com> Date: Wed, 22 Apr 2026 16:18:53 +0100 Subject: [PATCH 3/5] refactor: more cleanup --- package/Shaders/RunGrass.hlsl | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/package/Shaders/RunGrass.hlsl b/package/Shaders/RunGrass.hlsl index 04cd092520..299801e934 100644 --- a/package/Shaders/RunGrass.hlsl +++ b/package/Shaders/RunGrass.hlsl @@ -656,8 +656,6 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float3 albedo = baseColor.xyz * vertexColor; - float3 subsurfaceAlbedo = lerp(dot(albedo, 1.0 / 3.0), albedo, 2.0) * saturate(input.VertexNormal.w * 10.0) * albedo; - float3 subsurfaceColor = dirLightColor * dirSoftShadow * saturate(-dirLightAngle) * Color::VanillaNormalization(); if (complex) @@ -780,9 +778,8 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # endif diffuseColor += directionalAmbientColor; - + diffuseColor += subsurfaceColor * albedo * SharedData::grassLightingSettings.SubsurfaceScatteringAmount; diffuseColor *= albedo; - diffuseColor += max(0, subsurfaceColor * subsurfaceAlbedo * SharedData::grassLightingSettings.SubsurfaceScatteringAmount); directionalAmbientColor *= albedo; @@ -930,8 +927,6 @@ PS_OUTPUT main(PS_INPUT input) float3 ddy = ddy_coarse(input.WorldPosition); float3 normal = -normalize(cross(ddx, ddy)); - normal = normalize(float3(normal.xy, max(0, normal.z))); - float3 vertexColor = Color::ColorToLinear(input.VertexColor.xyz); # if defined(SKYLIGHTING) From d4242294a7276fdb653f454b8f0e237f00fd8dbd Mon Sep 17 00:00:00 2001 From: doodlum <15017472+doodlum@users.noreply.github.com> Date: Wed, 22 Apr 2026 19:36:13 +0100 Subject: [PATCH 4/5] fix: fix missing normalisation --- package/Shaders/RunGrass.hlsl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/Shaders/RunGrass.hlsl b/package/Shaders/RunGrass.hlsl index 299801e934..226212955f 100644 --- a/package/Shaders/RunGrass.hlsl +++ b/package/Shaders/RunGrass.hlsl @@ -725,7 +725,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) lightDiffuseColor = lightColor * saturate(lightAngle); } - subsurfaceColor += lightColor * saturate(-lightAngle); + subsurfaceColor += lightColor * saturate(-lightAngle) * Color::VanillaNormalization(); lightsDiffuseColor += lightDiffuseColor * Color::VanillaNormalization(); From ed6edaaa2a7c12977d67e80665df78aec6842b1c Mon Sep 17 00:00:00 2001 From: doodlum <15017472+doodlum@users.noreply.github.com> Date: Sat, 2 May 2026 21:22:45 +0100 Subject: [PATCH 5/5] Update package/Shaders/RunGrass.hlsl Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- package/Shaders/RunGrass.hlsl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/Shaders/RunGrass.hlsl b/package/Shaders/RunGrass.hlsl index 8bd92cb283..c0e82d5ca8 100644 --- a/package/Shaders/RunGrass.hlsl +++ b/package/Shaders/RunGrass.hlsl @@ -641,7 +641,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float3 albedo = baseColor.xyz * vertexColor; - float3 subsurfaceColor = dirLightColor * dirSoftShadow * saturate(-dirLightAngle) * Color::VanillaNormalization(); + float3 subsurfaceColor = dirLightColor * dirDetailedShadow * saturate(-dirLightAngle) * Color::VanillaNormalization(); if (complex) lightsSpecularColor += dirDetailedShadow * GrassLighting::GetLightSpecularInput(SharedData::DirLightDirection.xyz, viewDirection, normal, dirLightColor, SharedData::grassLightingSettings.Glossiness) * Color::VanillaNormalization();