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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 51 additions & 14 deletions assets/shaders/default_shader.frag
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,18 @@ layout(set = 2, binding = 0) uniform ShadowLightUBO {
float uHeightFogDensity;
float uHeightFogFalloff;
float uHeightFogEnabled;
vec4 uExtraDir[3];
vec4 uExtraColor[3];
uint uNumExtraLights;
mat4 uNdcToShadow;
mat4 uCascadeVP[4];
vec4 uCascadeSplits; // cascade radius around uCameraPos
uint uCascadeCount;
float uCascadeBlend; // blend band, fraction of the radius
};

layout(set = 3, binding = 0) uniform sampler2DShadow shadowMap; // hardware PCF
layout(set = 3, binding = 1) uniform sampler2D shadowMapRaw; // blocker search
layout(set = 3, binding = 2) uniform sampler2DArrayShadow shadowMap; // cascades, PCF
layout(set = 3, binding = 3) uniform sampler2DArray shadowMapRaw; // blocker search

float InterleavedGradientNoise(vec2 screenPos) {
vec3 magic = vec3(0.06711056, 0.00583715, 52.9829189);
Expand All @@ -58,12 +66,13 @@ vec2 VogelDisk(int i, int count, float phi) {
const int BLOCKER_SAMPLES = 16;
const int PCF_SAMPLES = 48;

float FindAvgBlockerDepth(vec2 uv, float zRef, float searchRadius, float phi) {
float FindAvgBlockerDepth(vec2 uv, float layer, float zRef,
float searchRadius, float phi) {
float sum = 0.0;
int count = 0;
for (int i = 0; i < BLOCKER_SAMPLES; ++i) {
vec2 off = VogelDisk(i, BLOCKER_SAMPLES, phi) * searchRadius;
float d = texture(shadowMapRaw, uv + off).r;
float d = texture(shadowMapRaw, vec3(uv + off, layer)).r;
if (d < zRef) {
sum += d;
count++;
Expand All @@ -72,11 +81,12 @@ float FindAvgBlockerDepth(vec2 uv, float zRef, float searchRadius, float phi) {
return (count > 0) ? sum / float(count) : -1.0;
}

float PCFFilter(vec2 uv, float zRef, float filterRadius, float phi) {
float PCFFilter(vec2 uv, float layer, float zRef, float filterRadius,
float phi) {
float shadow = 0.0;
for (int i = 0; i < PCF_SAMPLES; ++i) {
vec2 off = VogelDisk(i, PCF_SAMPLES, phi) * filterRadius;
shadow += texture(shadowMap, vec3(uv + off, zRef));
shadow += texture(shadowMap, vec4(uv + off, layer, zRef));
}
return shadow / float(PCF_SAMPLES);
}
Expand All @@ -87,7 +97,7 @@ float PCFFilter(vec2 uv, float zRef, float filterRadius, float phi) {
// - Penumbra is linear in the blocker-receiver depth gap; no perspective
// divide (that formula only applies to perspective/point lights).
// - uLightSize acts as a softness multiplier — larger → wider penumbra.
float CalcShadow(vec4 sc) {
float CalcShadow(vec4 sc, float layer) {
vec3 projCoords = sc.xyz / sc.w;
projCoords.xy = projCoords.xy * 0.5 + 0.5;

Expand All @@ -96,19 +106,17 @@ float CalcShadow(vec4 sc) {
projCoords.z < 0.0 || projCoords.z > 1.0)
return 1.0;

vec2 fadeCoord = smoothstep(vec2(0.0), vec2(0.05), projCoords.xy)
* smoothstep(vec2(0.0), vec2(0.05), vec2(1.0) - projCoords.xy);
float zFade = smoothstep(0.0, 0.05, projCoords.z)
float edgeFade = smoothstep(0.0, 0.05, projCoords.z)
* smoothstep(0.0, 0.1, 1.0 - projCoords.z);
float edgeFade = fadeCoord.x * fadeCoord.y * zFade;

float zRef = projCoords.z - uShadowBias;
float phi = InterleavedGradientNoise(gl_FragCoord.xy) * 6.283185;

// Fixed 30-texel blocker search: detects blockers ~0.7m above receiver
// on a 28m/4096 map. Decoupled from uLightSize for stability.
float searchRadius = uShadowTexelSize * 30.0;
float avgBlocker = FindAvgBlockerDepth(projCoords.xy, zRef, searchRadius, phi);
float avgBlocker = FindAvgBlockerDepth(projCoords.xy, layer, zRef,
searchRadius, phi);

if (avgBlocker < 0.0) return 1.0;

Expand All @@ -119,10 +127,39 @@ float CalcShadow(vec4 sc) {
uShadowTexelSize * 0.5, // floor: contact-hard
uShadowTexelSize * 20.0); // cap: floating objects

float shadow = PCFFilter(projCoords.xy, zRef, filterRadius, phi);
float shadow = PCFFilter(projCoords.xy, layer, zRef, filterRadius, phi);
return mix(1.0, shadow, uShadowStrength * edgeFade);
}

// Normal offset scales with the cascade radius so it stays the same number
// of texels in every cascade.
vec4 CascadeCoord(uint c, vec3 worldPos, vec3 N) {
float normalBias = uLightDir.w * uCascadeSplits[c] /
uCascadeSplits[uCascadeCount - 1u];
return uCascadeVP[c] * vec4(worldPos + N * normalBias, 1.0);
}

// Picks the cascade by distance from the camera and blends into the next one
// (or out to unshadowed after the last) across the band at its outer edge.
float CalcCascadedShadow(vec3 worldPos, vec3 N) {
if (uCascadeCount == 0u) return 1.0;
float dist = length(worldPos - uCameraPos.xyz);
uint c = 0u;
while (c + 1u < uCascadeCount && dist > uCascadeSplits[c]) ++c;

float outer = uCascadeSplits[c];
if (dist >= outer) return 1.0;
float t = smoothstep(outer * (1.0 - uCascadeBlend), outer, dist);
float shadow = CalcShadow(CascadeCoord(c, worldPos, N), float(c));
if (t > 0.0) {
float next = (c + 1u < uCascadeCount)
? CalcShadow(CascadeCoord(c + 1u, worldPos, N), float(c + 1u))
: 1.0;
shadow = mix(shadow, next, t);
}
return shadow;
}

// ACES film tone mapping (Stephen Hill fit)
vec3 ACESFilm(vec3 x) {
return clamp((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14), vec3(0.0), vec3(1.0));
Expand Down Expand Up @@ -171,7 +208,7 @@ void main() {

// Wrap diffuse: softens shadow terminator, side faces get ~15% sun
float NdotL = clamp(dot(N, -lightDir) * 0.85 + 0.15, 0.0, 1.0);
float shadow = CalcShadow(fragShadowCoord);
float shadow = CalcCascadedShadow(fragWorldPos, N);
vec3 diffuse = lightColor * lightIntens * NdotL * shadow;

// Compose: AO only on ambient (direct light ignores occlusion)
Expand Down
Binary file modified assets/shaders/default_shader.frag.spv
Binary file not shown.
60 changes: 44 additions & 16 deletions assets/shaders/default_shader_gl.frag
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,15 @@ layout(std140, binding = 5) uniform ShadowUBO {
uint PCSSEnabled;
uint ShadowMapEnabled;
float _shadowPad0, _shadowPad1;
mat4 NdcToShadow;
mat4 CascadeVP[4];
vec4 CascadeSplits; // cascade radius around CameraPos
uint CascadeCount;
float CascadeBlend; // blend band, fraction of the radius
};

layout(binding = 11) uniform sampler2DShadow shadowMapSampler;
layout(binding = 12) uniform sampler2D shadowMapDepth;
layout(binding = 17) uniform sampler2DArrayShadow shadowMapSampler;
layout(binding = 18) uniform sampler2DArray shadowMapDepth;

// ---- PCSS Shadow Functions ----

Expand Down Expand Up @@ -196,7 +201,8 @@ const vec2 poissonDisk[32] = vec2[](
vec2(-0.5120, 0.0420), vec2( 0.4210, 0.5120)
);

float FindBlockerDepthGL(vec2 uv, float receiverDepth, float searchRadius) {
float FindBlockerDepthGL(vec2 uv, float layer, float receiverDepth,
float searchRadius) {
float blockerSum = 0.0;
int blockerCount = 0;

Expand All @@ -207,7 +213,7 @@ float FindBlockerDepthGL(vec2 uv, float receiverDepth, float searchRadius) {

for (int i = 0; i < 16; i++) {
vec2 offset = rotation * poissonDisk[i] * searchRadius;
float sampleDepth = texture(shadowMapDepth, uv + offset).r;
float sampleDepth = texture(shadowMapDepth, vec3(uv + offset, layer)).r;
if (sampleDepth < receiverDepth) {
blockerSum += sampleDepth;
blockerCount++;
Expand All @@ -218,7 +224,8 @@ float FindBlockerDepthGL(vec2 uv, float receiverDepth, float searchRadius) {
return blockerSum / float(blockerCount);
}

float PCSSFilterGL(vec2 uv, float receiverDepth, float filterRadius) {
float PCSSFilterGL(vec2 uv, float layer, float receiverDepth,
float filterRadius) {
float angle = InterleavedGradientNoiseGL(gl_FragCoord.xy) * 6.283185;
float sa = sin(angle);
float ca = cos(angle);
Expand All @@ -227,54 +234,75 @@ float PCSSFilterGL(vec2 uv, float receiverDepth, float filterRadius) {
float shadow = 0.0;
for (int i = 0; i < 32; i++) {
vec2 offset = rotation * poissonDisk[i] * filterRadius;
shadow += texture(shadowMapSampler, vec3(uv + offset, receiverDepth));
shadow += texture(shadowMapSampler, vec4(uv + offset, layer, receiverDepth));
}
return shadow / 32.0;
}

float CalcShadowPCSSGL(vec4 sc) {
float CalcShadowPCSSGL(vec4 sc, float layer) {
if (ShadowMapEnabled == 0u) return 1.0;

vec3 proj = sc.xyz / sc.w;
proj.xy = proj.xy * 0.5 + 0.5;
proj.z = proj.z * 0.5 + 0.5;

if (proj.x < 0.0 || proj.x > 1.0 ||
proj.y < 0.0 || proj.y > 1.0 ||
proj.z < 0.0 || proj.z > 1.0)
return 1.0;

vec2 fadeCoord = smoothstep(vec2(0.0), vec2(0.05), proj.xy)
* smoothstep(vec2(0.0), vec2(0.05), vec2(1.0) - proj.xy);
float edgeFade = fadeCoord.x * fadeCoord.y;

float biasedDepth = proj.z - ShadowBias;
float shadow;

if (PCSSEnabled != 0u) {
float searchRadius = ShadowTexelSize * ShadowLightSize * 40.0;
float blockerDepth = FindBlockerDepthGL(proj.xy, biasedDepth, searchRadius);
float blockerDepth = FindBlockerDepthGL(proj.xy, layer, biasedDepth,
searchRadius);

if (blockerDepth < 0.0) return 1.0;

float penumbra = ((biasedDepth - blockerDepth) / blockerDepth) * ShadowLightSize;
float filterRadius = max(penumbra * ShadowTexelSize * 80.0, ShadowTexelSize * 8.0);
filterRadius = min(filterRadius, ShadowTexelSize * ShadowLightSize * 30.0);

shadow = PCSSFilterGL(proj.xy, biasedDepth, filterRadius);
shadow = PCSSFilterGL(proj.xy, layer, biasedDepth, filterRadius);
} else {
// Standard 3x3 PCF
shadow = 0.0;
float radius = ShadowTexelSize * 2.0;
for (int x = -1; x <= 1; x++) {
for (int y = -1; y <= 1; y++) {
vec2 offset = vec2(float(x), float(y)) * radius;
shadow += texture(shadowMapSampler, vec3(proj.xy + offset, biasedDepth));
shadow += texture(shadowMapSampler,
vec4(proj.xy + offset, layer, biasedDepth));
}
}
shadow /= 9.0;
}

return mix(1.0, shadow, ShadowStrength * edgeFade);
return mix(1.0, shadow, ShadowStrength);
}

// Picks the cascade by distance from the camera and blends into the next one
// (or out to unshadowed after the last) across the band at its outer edge.
float CalcCascadedShadowGL(vec3 worldPos) {
if (ShadowMapEnabled == 0u || CascadeCount == 0u) return 1.0;
float dist = length(worldPos - CameraPos);
uint c = 0u;
while (c + 1u < CascadeCount && dist > CascadeSplits[c]) ++c;

float outer = CascadeSplits[c];
if (dist >= outer) return 1.0;
float t = smoothstep(outer * (1.0 - CascadeBlend), outer, dist);
float shadow = CalcShadowPCSSGL(CascadeVP[c] * vec4(worldPos, 1.0), float(c));
if (t > 0.0) {
float next = 1.0;
if (c + 1u < CascadeCount)
next = CalcShadowPCSSGL(CascadeVP[c + 1u] * vec4(worldPos, 1.0),
float(c + 1u));
shadow = mix(shadow, next, t);
}
return shadow;
}

// ---- End PCSS Shadow Functions ----
Expand Down Expand Up @@ -372,7 +400,7 @@ void main() {
L = normalize(-light.Direction);
// Apply PCSS shadow to first directional light
if (i == 0u) {
float shadow = CalcShadowPCSSGL(fragShadowCoord);
float shadow = CalcCascadedShadowGL(fragWorldPos);
attenuation *= shadow;
}
}
Expand Down
62 changes: 42 additions & 20 deletions assets/shaders/lighting_pass.frag
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ layout(set = 0, binding = 0) uniform sampler2D gAlbedoAO; // albedo.rgb +
layout(set = 0, binding = 1) uniform sampler2D gNormalRough; // normal.xyz (encoded) + roughness.a
layout(set = 0, binding = 2) uniform sampler2D gMetalEmit; // metallic.r + emissive.gba
layout(set = 0, binding = 3) uniform sampler2D gDepth; // world pos reconstructed from this
layout(set = 0, binding = 4) uniform sampler2DShadow gShadow; // hardware PCF
layout(set = 0, binding = 4) uniform sampler2DArrayShadow gShadow; // cascades, hardware PCF
layout(set = 0, binding = 5) uniform sampler2D gShadowRaw; // PCSS blocker search
layout(set = 0, binding = 6) uniform sampler2D gSSAO; // screen-space AO (R8, 1=clear, 0=occluded)

Expand Down Expand Up @@ -56,6 +56,10 @@ layout(set = 2, binding = 0) uniform ShadowLightUBO {
// no pad member: std140 aligns the mat4 to 320, matching the C++ struct
// (a vec3 pad here pushed uNdcToShadow to 336 -> garbage shadow matrix)
mat4 uNdcToShadow; // NDC -> shadow clip, CPU-composed (no shimmer)
mat4 uCascadeVP[4];
vec4 uCascadeSplits; // cascade radius around uCameraPos
uint uCascadeCount;
float uCascadeBlend; // blend band, fraction of the radius
};

// -- Set 3: IBL -----------------------------------------------------
Expand Down Expand Up @@ -92,43 +96,61 @@ vec2 VogelDisk(int i, int count, float phi) {
// (gShadow) does a 2x2 lerp per tap, so 12 rotated taps read smooth and crisp.
const int PCF_SAMPLES = 16;

float PCFFilter(vec2 uv, float zRef, float filterRadius, float phi) {
float PCFFilter(vec2 uv, float layer, float zRef, float filterRadius,
float phi) {
float shadow = 0.0;
for (int i = 0; i < PCF_SAMPLES; ++i) {
vec2 off = VogelDisk(i, PCF_SAMPLES, phi) * filterRadius;
shadow += texture(gShadow, vec3(uv + off, zRef));
shadow += texture(gShadow, vec4(uv + off, layer, zRef));
}
return shadow / float(PCF_SAMPLES);
}

// World-space shadow projection — same math as the GL lighting pass and the
// water shader (uLightVP is texel-snapped; the NdcToShadow composition
// shimmered under camera motion from float error at large world coords).
float CalcShadow(vec3 worldPos, vec3 N) {
float normalBias = uLightDir.w;
vec4 sc = uLightVP * vec4(worldPos + N * normalBias, 1.0);
vec3 projCoords = sc.xyz / sc.w;
projCoords.xy = projCoords.xy * 0.5 + 0.5;
// World-space shadow projection into one cascade. The normal offset scales
// with the cascade radius so it stays the same number of texels everywhere.
float CascadeShadow(uint c, vec3 worldPos, vec3 N) {
float normalBias = uLightDir.w * uCascadeSplits[c] /
uCascadeSplits[uCascadeCount - 1u];
vec4 sc = uCascadeVP[c] * vec4(worldPos + N * normalBias, 1.0);
vec3 projCoords = sc.xyz / sc.w;
projCoords.xy = projCoords.xy * 0.5 + 0.5;

if (any(lessThan(projCoords, vec3(0.0))) ||
any(greaterThan(projCoords, vec3(1.0)))) return 1.0;

vec2 fadeCoord = smoothstep(vec2(0.0), vec2(0.05), projCoords.xy)
* smoothstep(vec2(0.0), vec2(0.05), vec2(1.0) - projCoords.xy);
float zFade = smoothstep(0.0, 0.05, projCoords.z)
* smoothstep(0.0, 0.1, 1.0 - projCoords.z);
float edgeFade = fadeCoord.x * fadeCoord.y * zFade;
float zFade = smoothstep(0.0, 0.05, projCoords.z)
* smoothstep(0.0, 0.1, 1.0 - projCoords.z);

float zRef = projCoords.z - uShadowBias;
// Dither in shadow-texel space — world-stable, no boiling under motion
float phi = InterleavedGradientNoise(projCoords.xy / uShadowTexelSize) *
6.283185;

// ~5 texels (≈0.4m at 3072/256m): world-space penumbra wide enough that
// camera motion cannot flicker the edge — the stability-critical knob
// ~5 texels of penumbra keep the edge from flickering under camera
// motion; this is the stability-critical knob
float filterRadius = uShadowTexelSize * 5.0;
float shadow = PCFFilter(projCoords.xy, zRef, filterRadius, phi);
return mix(1.0, shadow, uShadowStrength * edgeFade);
float shadow = PCFFilter(projCoords.xy, float(c), zRef, filterRadius, phi);
return mix(1.0, shadow, uShadowStrength * zFade);
}

// Picks the cascade by distance from the camera and blends into the next one
// (or out to unshadowed after the last) across the band at its outer edge.
float CalcShadow(vec3 worldPos, vec3 N) {
if (uCascadeCount == 0u) return 1.0;
float dist = length(worldPos - uCameraPos.xyz);
uint c = 0u;
while (c + 1u < uCascadeCount && dist > uCascadeSplits[c]) ++c;

float outer = uCascadeSplits[c];
if (dist >= outer) return 1.0;
float t = smoothstep(outer * (1.0 - uCascadeBlend), outer, dist);
float shadow = CascadeShadow(c, worldPos, N);
if (t > 0.0) {
float next = (c + 1u < uCascadeCount)
? CascadeShadow(c + 1u, worldPos, N) : 1.0;
shadow = mix(shadow, next, t);
}
return shadow;
}

// ==================================================================
Expand Down
Binary file modified assets/shaders/lighting_pass.frag.spv
Binary file not shown.
Loading
Loading