diff --git a/assets/shaders/default_shader.frag b/assets/shaders/default_shader.frag index b3a6bb8e..11b81999 100644 --- a/assets/shaders/default_shader.frag +++ b/assets/shaders/default_shader.frag @@ -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); @@ -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++; @@ -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); } @@ -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; @@ -96,11 +106,8 @@ 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; @@ -108,7 +115,8 @@ float CalcShadow(vec4 sc) { // 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; @@ -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)); @@ -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) diff --git a/assets/shaders/default_shader.frag.spv b/assets/shaders/default_shader.frag.spv index 4773f1df..b1de49d4 100644 Binary files a/assets/shaders/default_shader.frag.spv and b/assets/shaders/default_shader.frag.spv differ diff --git a/assets/shaders/default_shader_gl.frag b/assets/shaders/default_shader_gl.frag index 9a28745b..d70a18ef 100644 --- a/assets/shaders/default_shader_gl.frag +++ b/assets/shaders/default_shader_gl.frag @@ -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 ---- @@ -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; @@ -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++; @@ -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); @@ -227,32 +234,30 @@ 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; @@ -260,7 +265,7 @@ float CalcShadowPCSSGL(vec4 sc) { 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; @@ -268,13 +273,36 @@ float CalcShadowPCSSGL(vec4 sc) { 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 ---- @@ -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; } } diff --git a/assets/shaders/lighting_pass.frag b/assets/shaders/lighting_pass.frag index 06da48bb..4a1e9ba6 100644 --- a/assets/shaders/lighting_pass.frag +++ b/assets/shaders/lighting_pass.frag @@ -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) @@ -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 ----------------------------------------------------- @@ -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; } // ================================================================== diff --git a/assets/shaders/lighting_pass.frag.spv b/assets/shaders/lighting_pass.frag.spv index 8ed6ac24..89fcbd76 100644 Binary files a/assets/shaders/lighting_pass.frag.spv and b/assets/shaders/lighting_pass.frag.spv differ diff --git a/assets/shaders/skinned_shader.frag b/assets/shaders/skinned_shader.frag index ca9a7021..a230317e 100644 --- a/assets/shaders/skinned_shader.frag +++ b/assets/shaders/skinned_shader.frag @@ -31,10 +31,23 @@ layout(set = 2, binding = 0) uniform ShadowLightUBO { float uFogStart; float uFogEnd; vec2 _fogPad; + vec4 uFogZenithColor; + float uHeightFogTop; + 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 // ------------------------------------------------------------ // Sampling helpers @@ -68,12 +81,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++; @@ -82,11 +96,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); } @@ -98,7 +113,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; @@ -107,11 +122,8 @@ float CalcShadow(vec4 sc) { projCoords.z < 0.0 || projCoords.z > 1.0) return 1.0; - vec2 fadeXY = smoothstep(vec2(0.0), vec2(0.04), projCoords.xy) - * smoothstep(vec2(0.0), vec2(0.04), vec2(1.0) - projCoords.xy); - float fadeZ = smoothstep(0.0, 0.05, projCoords.z) - * smoothstep(0.0, 0.08, 1.0 - projCoords.z); - float edgeFade = fadeXY.x * fadeXY.y * fadeZ; + float edgeFade = smoothstep(0.0, 0.05, projCoords.z) + * smoothstep(0.0, 0.08, 1.0 - projCoords.z); float zRef = projCoords.z - uShadowBias; float phi = InterleavedGradientNoise(gl_FragCoord.xy) * 6.283185; @@ -120,7 +132,8 @@ float CalcShadow(vec4 sc) { // above receiver on a 28m/4096 map — decoupled from uLightSize so the // blocker search stays reliable at all softness settings. 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; @@ -133,10 +146,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; +} + void main() { vec4 texColor = texture(diffuseTexture, fragUV); vec4 baseColor = texColor * fragColor; @@ -162,7 +204,7 @@ void main() { float spec = normFactor * pow(max(dot(N, halfDir), 0.0), shininess); vec3 specular = lightColor * spec * 0.5 * max(NdotL, 0.0); - float shadow = CalcShadow(fragShadowCoord); + float shadow = CalcCascadedShadow(fragWorldPos, N); // Output linear HDR — tonemapping (ACES + gamma) is applied in the // bloom composite pass. Doing Reinhard here would cause double-tonemapping. diff --git a/assets/shaders/skinned_shader.frag.spv b/assets/shaders/skinned_shader.frag.spv index 639554d1..0f900d8e 100644 Binary files a/assets/shaders/skinned_shader.frag.spv and b/assets/shaders/skinned_shader.frag.spv differ diff --git a/include/private/Graphics/Common/ConstantBuffer.hpp b/include/private/Graphics/Common/ConstantBuffer.hpp index d96c6ebb..66d99136 100644 --- a/include/private/Graphics/Common/ConstantBuffer.hpp +++ b/include/private/Graphics/Common/ConstantBuffer.hpp @@ -1,8 +1,9 @@ #ifndef _CONSTANTBUFFER_H_ #define _CONSTANTBUFFER_H_ -#include #include +#include +#include #include namespace Sleak { @@ -43,6 +44,9 @@ namespace Sleak { // Lights[]) still read a valid prefix. Don't reorder. static constexpr uint32_t MAX_LIGHTS = 16; + /// Upper bound on directional shadow cascades (array layers). + static constexpr uint32_t MAX_SHADOW_CASCADES = 4; + /// Full per-frame lighting UBO: camera, ambient, fog, and the packed light array. struct alignas(16) LightCBData { // Header Row 0: CameraPos.xyz + NumActiveLights @@ -204,7 +208,18 @@ namespace Sleak { // Shadow coords must use this — a per-fragment round trip // through reconstructed world position shimmers under rotation. float NdcToShadow[16]; + + // Cascades. LightVP above is the last cascade so shaders that + // only know the single map keep working. + float CascadeVP[MAX_SHADOW_CASCADES][16]; + float CascadeSplits[MAX_SHADOW_CASCADES]; // radius around camera + uint32_t CascadeCount; + float CascadeBlend; // blend band, fraction of the cascade radius + float _cascadePad[2]; }; + static_assert(offsetof(ShadowLightUBO, CascadeVP) == 384 && + offsetof(ShadowLightUBO, CascadeCount) == 656, + "ShadowLightUBO must match the std140 shader block"); // GPU-aligned POD struct for post-process settings constant buffer. // Total: 16 bytes (1 x 16-byte row). @@ -263,7 +278,17 @@ namespace Sleak { // Row 6-9: NDC -> shadow clip (see ShadowLightUBO::NdcToShadow) float NdcToShadow[16]; + + // Rows 10-27: cascades (see ShadowLightUBO) + float CascadeVP[MAX_SHADOW_CASCADES][16]; + float CascadeSplits[MAX_SHADOW_CASCADES]; + uint32_t CascadeCount; + float CascadeBlend; + float _cascadePad[2]; }; + static_assert(offsetof(PCSSShadowGPUData, CascadeVP) == 160 && + offsetof(PCSSShadowGPUData, CascadeCount) == 432, + "PCSSShadowGPUData must match the std140 shader block"); // GPU-aligned POD struct for SSAO composite settings (used in PBR shader). // Total: 16 bytes (1 x 16-byte row). diff --git a/include/private/Graphics/Common/Renderer.hpp b/include/private/Graphics/Common/Renderer.hpp index 56bb17f9..e4248696 100644 --- a/include/private/Graphics/Common/Renderer.hpp +++ b/include/private/Graphics/Common/Renderer.hpp @@ -1,10 +1,11 @@ #pragma once - -#include "RenderContext.hpp" #include #include +#include "ConstantBuffer.hpp" +#include "RenderContext.hpp" + namespace Sleak { namespace RenderEngine { @@ -134,6 +135,11 @@ class ENGINE_API Renderer { // Shadow mapping support (overridden by VulkanRenderer) virtual void UpdateShadowLightUBO(const void* data, uint32_t size) { (void)data; (void)size; } virtual void SetLightVP(const float* lightVP) { (void)lightVP; } + /// Stages one light view-projection per cascade for the next shadow pass. + virtual void SetShadowCascades(const float* viewProj, uint32_t count) { + (void)viewProj; + (void)count; + } void SetShadowPassEnabled(bool enabled) { m_shadowPassEnabled = enabled; } bool IsShadowPassEnabled() const { return m_shadowPassEnabled; } @@ -193,15 +199,31 @@ class ENGINE_API Renderer { /// Requests a new shadow map size, clamped to [256, 8192] and queued if resources already exist. void SetShadowMapResolution(uint32_t res) { if (res < 256 || res > 8192) return; - if (res == m_shadowMapResolution) return; if (m_shadowResourcesCreated) { + QueueShadowResourceChange(); m_pendingShadowMapResolution = res; - m_shadowResChangeRequested = true; return; } m_shadowMapResolution = res; } uint32_t GetShadowMapResolution() const { return m_shadowMapResolution; } + + /// Requests a shadow cascade (array layer) count, queued like the size. + void SetShadowCascadeCount(uint32_t count) { + if (count < 1 || count > MAX_SHADOW_CASCADES) return; + if (m_shadowResourcesCreated) { + QueueShadowResourceChange(); + m_pendingShadowCascadeCount = count; + return; + } + m_shadowCascadeCount = count; + } + uint32_t GetShadowCascadeCount() const { return m_shadowCascadeCount; } + + /// Scales each backend's tuned shadow-pass raster depth bias. + void SetShadowDepthBiasScale(float scale) { + m_shadowDepthBiasScale = scale > 0.0f ? scale : 0.0f; + } virtual void ApplyShadowResolutionChange() {} // Deferred rendering mode (default: enabled) @@ -307,11 +329,22 @@ class ENGINE_API Renderer { // PCSS bool m_pcssEnabled = true; - // Shadow map resolution + // Shadow map resolution and cascade (layer) count uint32_t m_shadowMapResolution = 2048; uint32_t m_pendingShadowMapResolution = 2048; + uint32_t m_shadowCascadeCount = 1; + uint32_t m_pendingShadowCascadeCount = 1; bool m_shadowResourcesCreated = false; bool m_shadowResChangeRequested = false; + float m_shadowDepthBiasScale = 1.0f; + + /// Seeds the pending shadow settings from the live ones. + void QueueShadowResourceChange() { + if (m_shadowResChangeRequested) return; + m_pendingShadowMapResolution = m_shadowMapResolution; + m_pendingShadowCascadeCount = m_shadowCascadeCount; + m_shadowResChangeRequested = true; + } // Deferred rendering bool m_deferredEnabled = true; diff --git a/include/private/Graphics/OpenGL/OpenGLRenderer.hpp b/include/private/Graphics/OpenGL/OpenGLRenderer.hpp index f58c8631..a7610b32 100644 --- a/include/private/Graphics/OpenGL/OpenGLRenderer.hpp +++ b/include/private/Graphics/OpenGL/OpenGLRenderer.hpp @@ -107,9 +107,17 @@ class ENGINE_API OpenGLRenderer : public Renderer, public RenderContext { /// Configures the shared VAO's vertex attribute layout for the standard mesh format. void SetupVertexLayout(); - // Shadow mapping + // Shadow mapping: one depth array layer per cascade, plus a 2D view of + // the last cascade for shaders that only know the single map. GLuint m_shadowFBO = 0; GLuint m_shadowDepthTex = 0; + GLuint m_shadowLegacyView = 0; + GLuint m_shadowCompareSampler = 0; + GLuint m_shadowRawSampler = 0; + float m_cascadeVP[MAX_SHADOW_CASCADES][16] = {}; + float m_pendingCascadeVP[MAX_SHADOW_CASCADES][16] = {}; + uint32_t m_cascadeCount = 0; + uint32_t m_pendingCascadeCount = 0; GLuint m_shadowUBO = 0; bool m_shadowMapCreated = false; bool m_shadowUBOCreated = false; @@ -119,9 +127,17 @@ class ENGINE_API OpenGLRenderer : public Renderer, public RenderContext { bool m_inShadowPass = false; GLuint m_shadowTransformUBO = 0; void SetLightVP(const float* mat) override; + /// Stages the per-cascade light view-projections for the next BeginRender. + void SetShadowCascades(const float* viewProj, uint32_t count) override; void UpdateShadowLightUBO(const void* data, uint32_t size) override; - /// Allocates the shadow-pass depth FBO and texture at the configured resolution. + /// Drops the shadow map so it is rebuilt with the queued settings. + void ApplyShadowResolutionChange() override; + /// Allocates the shadow-pass depth FBO and cascade array texture. bool CreateShadowMapResources(); + /// Deletes the shadow array texture, its view, samplers, and FBO. + void DestroyShadowMapResources(); + /// Binds the last cascade at unit 3 and the whole array at 17/18. + void BindShadowMapTextures(); /// Allocates the UBO backing ShadowLightUBO. bool CreateShadowUBO(); /// Replays cached shadow-caster draws into the depth-only shadow FBO. diff --git a/include/private/Graphics/Vulkan/VulkanRenderer.hpp b/include/private/Graphics/Vulkan/VulkanRenderer.hpp index 053adb78..7941745e 100644 --- a/include/private/Graphics/Vulkan/VulkanRenderer.hpp +++ b/include/private/Graphics/Vulkan/VulkanRenderer.hpp @@ -182,6 +182,8 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { void UpdateShadowLightUBO(const void* data, uint32_t size) override; /// Stages the light view-projection matrix for commit at the next BeginRender. void SetLightVP(const float* lightVP) override; + /// Stages the per-cascade light view-projections for the next BeginRender. + void SetShadowCascades(const float* viewProj, uint32_t count) override; // Deferred rendering overrides virtual bool IsDeferredEnabled() const override { return m_deferredEnabled && m_gbufferResourcesCreated; } @@ -206,7 +208,7 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { void ApplyMSAAChange() override; /// Recreates the swapchain to apply a queued VSync toggle. void ApplyVSyncChange() override; - /// Recreates the extent-dependent shadow map objects at the queued resolution. + /// Recreates the shadow map objects with the queued size and cascades. void ApplyShadowResolutionChange() override; private: @@ -261,6 +263,16 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { bool CreateShadowLightUBOResources(); /// Destroys the shadow map image, pipeline, render pass, and light UBO resources. void CleanupShadowResources(); + /// Creates the layered shadow image and its array and per-layer views. + bool CreateShadowMapImage(); + /// Creates one framebuffer per cascade layer for the shadow render pass. + bool CreateShadowFramebuffers(); + /// Destroys what CreateShadowMapImage made. + void DestroyShadowMapImage(); + /// Points the set-3 shadow descriptors at the current shadow views. + void WriteShadowSamplerDescriptors(); + /// Renders the cached shadow casters into every cascade layer. + void RecordShadowPass(); /// Creates the Vulkan instance with validation layers when available. bool InitVulkan(); /// Creates the SDL-backed Vulkan presentation surface. @@ -481,11 +493,13 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { // Shadow mapping resources VkImage m_shadowImage = VK_NULL_HANDLE; VkDeviceMemory m_shadowImageMemory = VK_NULL_HANDLE; - VkImageView m_shadowImageView = VK_NULL_HANDLE; + VkImageView m_shadowImageView = VK_NULL_HANDLE; // last cascade, 2D + VkImageView m_shadowArrayView = VK_NULL_HANDLE; // all cascades + std::array m_shadowLayerViews = {}; VkSampler m_shadowSampler = VK_NULL_HANDLE; // compare sampler (hardware PCF) VkSampler m_shadowRawSampler = VK_NULL_HANDLE; // non-compare sampler (PCSS blocker search) VkRenderPass m_shadowRenderPass = VK_NULL_HANDLE; - VkFramebuffer m_shadowFramebuffer = VK_NULL_HANDLE; + std::array m_shadowFramebuffers = {}; VkPipeline m_shadowPipeline = VK_NULL_HANDLE; VulkanShader* m_shadowShader = nullptr; bool m_shadowPassActive = false; @@ -507,6 +521,12 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { float m_pendingLightVP[16] = {}; bool m_hasPendingLightVP = false; + // Per-cascade light VPs, staged and committed the same way as m_lightVP + float m_cascadeVP[MAX_SHADOW_CASCADES][16] = {}; + float m_pendingCascadeVP[MAX_SHADOW_CASCADES][16] = {}; + uint32_t m_cascadeCount = 0; + uint32_t m_pendingCascadeCount = 0; + // Light/Shadow UBO (set 2, binding 0) VkDescriptorSetLayout m_lightUBODescriptorSetLayout = VK_NULL_HANDLE; VkDescriptorPool m_lightUBODescriptorPool = VK_NULL_HANDLE; diff --git a/include/public/Core/GraphicsConfig.hpp b/include/public/Core/GraphicsConfig.hpp index 3cb65314..d7129f7e 100644 --- a/include/public/Core/GraphicsConfig.hpp +++ b/include/public/Core/GraphicsConfig.hpp @@ -16,6 +16,9 @@ enum class GraphicsQuality { Off, Low, Medium, High, Ultra }; /// Application::ApplyGraphicsConfig(). Game owns and persists this. /// @ingroup core struct ENGINE_API GraphicsConfig { + /// shadowBias value the backends' raster depth bias is tuned against. + static constexpr float kDefaultShadowBias = 0.002f; + // Post-FX (deferred path) bool ssaoEnabled = false; float ssaoRadius = 0.5f; @@ -36,6 +39,11 @@ struct ENGINE_API GraphicsConfig { float shadowBias = 0.002f; float shadowStrength = 1.0f; + // Shadow cascades + uint32_t shadowCascadeCount = 3; + float shadowSplitLambda = 0.6f; // 0 uniform, 1 logarithmic + float shadowMaxDistance = 128.0f; // radius the cascades cover + // Culling bool frustumCullingEnabled = true; bool occlusionCullingEnabled = true; diff --git a/include/public/Lighting/DirectionalLight.hpp b/include/public/Lighting/DirectionalLight.hpp index 33908b1b..43726bf8 100644 --- a/include/public/Lighting/DirectionalLight.hpp +++ b/include/public/Lighting/DirectionalLight.hpp @@ -23,6 +23,7 @@ namespace Sleak { /// player can actually see multiplies your effective texel density. /// The default covers a very large area, which is right for open /// terrain and far too coarse for a single figure on a small floor. + /// /// @code{.cpp} /// auto* key = new Sleak::DirectionalLight("KeyLight"); @@ -62,6 +63,22 @@ namespace Sleak { } Math::Vector3D GetDirection() const { return m_direction; } + /// Number of shadow cascades, clamped to 1-4 when the shadow is built. + void SetShadowCascadeCount(uint32_t count) { + m_shadowCascadeCount = count; + } + uint32_t GetShadowCascadeCount() const { return m_shadowCascadeCount; } + + /// Split scheme blend: 0 is uniform, 1 is logarithmic. + void SetShadowSplitLambda(float lambda) { + m_shadowSplitLambda = lambda; + } + float GetShadowSplitLambda() const { return m_shadowSplitLambda; } + + /// Radius the cascades cover around the camera; 0 uses frustum size. + void SetShadowMaxDistance(float dist) { m_shadowMaxDistance = dist; } + float GetShadowMaxDistance() const { return m_shadowMaxDistance; } + // Shadow frustum configuration void SetShadowFrustumSize(float size) { m_shadowFrustumSize = size; } float GetShadowFrustumSize() const { return m_shadowFrustumSize; } @@ -81,6 +98,9 @@ namespace Sleak { float m_shadowDistance = 160.0f; float m_shadowNear = 0.1f; float m_shadowFar = 500.0f; + uint32_t m_shadowCascadeCount = 1; + float m_shadowSplitLambda = 0.6f; + float m_shadowMaxDistance = 0.0f; }; } // namespace Sleak diff --git a/src/Core/Application.cpp b/src/Core/Application.cpp index 83c0eae5..76808da3 100644 --- a/src/Core/Application.cpp +++ b/src/Core/Application.cpp @@ -422,6 +422,8 @@ namespace Sleak { renderer->SetIBLIntensity(cfg.iblIntensity); renderer->SetTAAEnabled(cfg.taaEnabled); renderer->SetShadowMapResolution(cfg.shadowMapResolution); + renderer->SetShadowDepthBiasScale(cfg.shadowBias / + GraphicsConfig::kDefaultShadowBias); renderer->SetMSAASampleCount(cfg.msaaSamples); renderer->SetTonemappingEnabled(cfg.tonemapEnabled); renderer->SetExposure(cfg.exposure); diff --git a/src/Graphics/Common/GraphicsConfig.cpp b/src/Graphics/Common/GraphicsConfig.cpp index 89007578..b14e61b9 100644 --- a/src/Graphics/Common/GraphicsConfig.cpp +++ b/src/Graphics/Common/GraphicsConfig.cpp @@ -17,6 +17,7 @@ GraphicsConfig GraphicsConfig::Preset(GraphicsQuality q) { cfg.lightShaftEnabled = false; cfg.shadowEnabled = false; cfg.pcssEnabled = false; + cfg.shadowCascadeCount = 1; cfg.occlusionCullingEnabled = false; cfg.occlusionBufferWidth = 160; cfg.occlusionBufferHeight = 90; @@ -34,6 +35,9 @@ GraphicsConfig GraphicsConfig::Preset(GraphicsQuality q) { cfg.pcssEnabled = false; cfg.shadowMapResolution = 1024; cfg.shadowFrustumSize = 96.0f; + cfg.shadowCascadeCount = 2; + cfg.shadowSplitLambda = 0.5f; + cfg.shadowMaxDistance = 96.0f; cfg.shadowCasterDistance = 64.0f; cfg.occlusionCullingEnabled = false; cfg.occlusionBufferWidth = 160; @@ -50,6 +54,9 @@ GraphicsConfig GraphicsConfig::Preset(GraphicsQuality q) { cfg.lightShaftEnabled = false; cfg.shadowEnabled = true; cfg.pcssEnabled = true; + cfg.shadowCascadeCount = 3; + cfg.shadowSplitLambda = 0.6f; + cfg.shadowMaxDistance = 128.0f; cfg.occlusionCullingEnabled = true; cfg.occlusionBufferWidth = 256; cfg.occlusionBufferHeight = 144; @@ -65,13 +72,16 @@ GraphicsConfig GraphicsConfig::Preset(GraphicsQuality q) { cfg.lightShaftEnabled = true; cfg.shadowEnabled = true; cfg.pcssEnabled = true; + cfg.shadowCascadeCount = 4; + cfg.shadowSplitLambda = 0.7f; + cfg.shadowMaxDistance = 160.0f; cfg.occlusionCullingEnabled = true; cfg.occlusionBufferWidth = 256; cfg.occlusionBufferHeight = 144; break; } case GraphicsQuality::Ultra: { - // High + larger shadow range + TAA. + // High + more shadow range in the same cascades + TAA. cfg.ssaoEnabled = true; cfg.ssrEnabled = true; cfg.bloomEnabled = true; @@ -82,8 +92,10 @@ GraphicsConfig GraphicsConfig::Preset(GraphicsQuality q) { cfg.pcssEnabled = true; cfg.shadowMapResolution = 3072; cfg.shadowDistance = 256.0f; - cfg.shadowFrustumSize = 160.0f; - cfg.shadowCasterDistance = 160.0f; + cfg.shadowCascadeCount = 4; + cfg.shadowSplitLambda = 0.75f; + cfg.shadowMaxDistance = 256.0f; + cfg.shadowCasterDistance = 256.0f; cfg.occlusionCullingEnabled = true; cfg.occlusionBufferWidth = 320; cfg.occlusionBufferHeight = 180; @@ -101,6 +113,9 @@ void GraphicsConfig::ApplyShadows(Sleak::DirectionalLight& light) const { light.SetShadowDistance(shadowDistance); light.SetShadowBias(shadowBias); light.SetShadowStrength(shadowStrength); + light.SetShadowCascadeCount(shadowCascadeCount); + light.SetShadowSplitLambda(shadowSplitLambda); + light.SetShadowMaxDistance(shadowMaxDistance); } } // namespace Sleak diff --git a/src/Graphics/OpenGL/OpenGLRenderer.cpp b/src/Graphics/OpenGL/OpenGLRenderer.cpp index 58da1d67..65fa6334 100644 --- a/src/Graphics/OpenGL/OpenGLRenderer.cpp +++ b/src/Graphics/OpenGL/OpenGLRenderer.cpp @@ -121,11 +121,14 @@ void OpenGLRenderer::BeginRender() { ApplyVSyncChange(); if (m_msaaChangeRequested) ApplyMSAAChange(); + if (m_shadowResChangeRequested) ApplyShadowResolutionChange(); // Commit staged lightVP before shadow pass so shadow + main agree. if (m_hasPendingLightVP) { memcpy(m_lightVP, m_pendingLightVP, sizeof(m_lightVP)); } + memcpy(m_cascadeVP, m_pendingCascadeVP, sizeof(m_cascadeVP)); + m_cascadeCount = m_pendingCascadeCount; // Shadow pass before main rendering if (m_shadowPassEnabled) { @@ -192,9 +195,7 @@ void OpenGLRenderer::Cleanup() { CleanupGBufferResources(); if (m_shadowTransformUBO) { glDeleteBuffers(1, &m_shadowTransformUBO); m_shadowTransformUBO = 0; } if (m_shadowUBO) { glDeleteBuffers(1, &m_shadowUBO); m_shadowUBO = 0; } - if (m_shadowDepthTex) { glDeleteTextures(1, &m_shadowDepthTex); m_shadowDepthTex = 0; } - if (m_shadowFBO) { glDeleteFramebuffers(1, &m_shadowFBO); m_shadowFBO = 0; } - m_shadowMapCreated = false; + DestroyShadowMapResources(); m_shadowUBOCreated = false; CleanupMSAAFramebuffer(); if (bImInitialized) { @@ -652,6 +653,23 @@ void OpenGLRenderer::SetLightVP(const float* mat) { } } +void OpenGLRenderer::SetShadowCascades(const float* viewProj, uint32_t count) { + count = std::min(count, MAX_SHADOW_CASCADES); + if (count > 0 && viewProj) + memcpy(m_pendingCascadeVP, viewProj, sizeof(float) * 16 * count); + m_pendingCascadeCount = count; +} + +void OpenGLRenderer::ApplyShadowResolutionChange() { + m_shadowResChangeRequested = false; + if (m_pendingShadowMapResolution == m_shadowMapResolution && + m_pendingShadowCascadeCount == m_shadowCascadeCount) + return; + DestroyShadowMapResources(); + m_shadowMapResolution = m_pendingShadowMapResolution; + m_shadowCascadeCount = m_pendingShadowCascadeCount; +} + bool OpenGLRenderer::CreateShadowUBO() { if (m_shadowUBOCreated) return true; glGenBuffers(1, &m_shadowUBO); @@ -677,6 +695,11 @@ void OpenGLRenderer::UpdateShadowLightUBO(const void* data, uint32_t size) { shadowData.ShadowLightSize = ubo->LightSize; shadowData.PCSSEnabled = m_pcssEnabled ? 1 : 0; shadowData.ShadowMapEnabled = m_shadowPassEnabled ? 1 : 0; + memcpy(shadowData.CascadeVP, ubo->CascadeVP, sizeof(shadowData.CascadeVP)); + memcpy(shadowData.CascadeSplits, ubo->CascadeSplits, + sizeof(shadowData.CascadeSplits)); + shadowData.CascadeCount = ubo->CascadeCount; + shadowData.CascadeBlend = ubo->CascadeBlend; glBindBuffer(GL_UNIFORM_BUFFER, m_shadowUBO); glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(shadowData), &shadowData); @@ -689,41 +712,99 @@ void OpenGLRenderer::UpdateShadowLightUBO(const void* data, uint32_t size) { bool OpenGLRenderer::CreateShadowMapResources() { if (m_shadowMapCreated) return true; - // Create depth texture + const GLsizei layers = static_cast(m_shadowCascadeCount); + const float borderColor[] = {1.0f, 1.0f, 1.0f, 1.0f}; + glGenTextures(1, &m_shadowDepthTex); - glBindTexture(GL_TEXTURE_2D, m_shadowDepthTex); - glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, - m_shadowMapResolution, m_shadowMapResolution, 0, - GL_DEPTH_COMPONENT, GL_FLOAT, nullptr); + glBindTexture(GL_TEXTURE_2D_ARRAY, m_shadowDepthTex); + glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_DEPTH_COMPONENT32F, + m_shadowMapResolution, m_shadowMapResolution, layers); + glBindTexture(GL_TEXTURE_2D_ARRAY, 0); + + // 2D view of the last cascade, compared in hardware, for shaders that + // still declare a single sampler2DShadow at unit 3. + glGenTextures(1, &m_shadowLegacyView); + glTextureView(m_shadowLegacyView, GL_TEXTURE_2D, m_shadowDepthTex, + GL_DEPTH_COMPONENT32F, 0, 1, layers - 1, 1); + glBindTexture(GL_TEXTURE_2D, m_shadowLegacyView); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); - float borderColor[] = {1.0f, 1.0f, 1.0f, 1.0f}; glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL); glBindTexture(GL_TEXTURE_2D, 0); - // Create FBO + // The array is read through sampler objects: one compares (PCF), the + // other returns raw depth for the PCSS blocker search. + glGenSamplers(1, &m_shadowCompareSampler); + glGenSamplers(1, &m_shadowRawSampler); + for (GLuint sampler : {m_shadowCompareSampler, m_shadowRawSampler}) { + glSamplerParameteri(sampler, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); + glSamplerParameteri(sampler, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); + glSamplerParameterfv(sampler, GL_TEXTURE_BORDER_COLOR, borderColor); + } + const GLuint cmp = m_shadowCompareSampler; + glSamplerParameteri(cmp, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glSamplerParameteri(cmp, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glSamplerParameteri(cmp, GL_TEXTURE_COMPARE_MODE, + GL_COMPARE_REF_TO_TEXTURE); + glSamplerParameteri(cmp, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL); + const GLuint raw = m_shadowRawSampler; + glSamplerParameteri(raw, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glSamplerParameteri(raw, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glSamplerParameteri(raw, GL_TEXTURE_COMPARE_MODE, GL_NONE); + glGenFramebuffers(1, &m_shadowFBO); glBindFramebuffer(GL_FRAMEBUFFER, m_shadowFBO); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, m_shadowDepthTex, 0); + glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, + m_shadowDepthTex, 0, 0); glDrawBuffer(GL_NONE); glReadBuffer(GL_NONE); if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { SLEAK_ERROR("OpenGL shadow FBO is not complete!"); glBindFramebuffer(GL_FRAMEBUFFER, 0); + DestroyShadowMapResources(); return false; } glBindFramebuffer(GL_FRAMEBUFFER, 0); m_shadowMapCreated = true; m_shadowResourcesCreated = true; - SLEAK_INFO("OpenGL shadow map resources created ({}x{})", m_shadowMapResolution, m_shadowMapResolution); + SLEAK_INFO("OpenGL shadow map resources created ({}x{}, {} cascades)", + m_shadowMapResolution, m_shadowMapResolution, layers); return true; } +void OpenGLRenderer::DestroyShadowMapResources() { + for (GLuint* tex : {&m_shadowLegacyView, &m_shadowDepthTex}) { + if (*tex) glDeleteTextures(1, tex); + *tex = 0; + } + for (GLuint* sampler : {&m_shadowCompareSampler, &m_shadowRawSampler}) { + if (*sampler) glDeleteSamplers(1, sampler); + *sampler = 0; + } + if (m_shadowFBO) glDeleteFramebuffers(1, &m_shadowFBO); + m_shadowFBO = 0; + m_shadowMapCreated = false; + m_shadowResourcesCreated = false; +} + +void OpenGLRenderer::BindShadowMapTextures() { + if (!m_shadowMapCreated) return; + glActiveTexture(GL_TEXTURE3); + glBindTexture(GL_TEXTURE_2D, m_shadowLegacyView); + glActiveTexture(GL_TEXTURE17); + glBindTexture(GL_TEXTURE_2D_ARRAY, m_shadowDepthTex); + glBindSampler(17, m_shadowCompareSampler); + glActiveTexture(GL_TEXTURE18); + glBindTexture(GL_TEXTURE_2D_ARRAY, m_shadowDepthTex); + glBindSampler(18, m_shadowRawSampler); + glActiveTexture(GL_TEXTURE0); +} + void OpenGLRenderer::RenderShadowPass() { // Skip if no cached draws — preserve previous frame's shadow map auto* queue = RenderCommandQueue::GetInstance(); @@ -737,14 +818,13 @@ void OpenGLRenderer::RenderShadowPass() { GLint savedViewport[4]; glGetIntegerv(GL_VIEWPORT, savedViewport); - // Bind shadow FBO glBindFramebuffer(GL_FRAMEBUFFER, m_shadowFBO); glViewport(0, 0, m_shadowMapResolution, m_shadowMapResolution); - glClear(GL_DEPTH_BUFFER_BIT); // Enable polygon offset for slope-scale depth bias glEnable(GL_POLYGON_OFFSET_FILL); - glPolygonOffset(2.0f, 4.0f); + glPolygonOffset(2.0f * m_shadowDepthBiasScale, + 4.0f * m_shadowDepthBiasScale); // Back-face cull: closed solid volumes always have the light-facing // surface win — same depth result, half the raster. @@ -755,9 +835,15 @@ void OpenGLRenderer::RenderShadowPass() { // Ensure VAO is bound for shadow pass draws glBindVertexArray(m_VAO); - // Execute shadow draw commands + // One layer per cascade; BindConstantBuffer builds the caster transform + // from m_lightVP, so it carries the cascade being drawn. + const uint32_t cascades = std::min(m_cascadeCount, m_shadowCascadeCount); m_inShadowPass = true; - if (queue) { + for (uint32_t c = 0; c < cascades; ++c) { + glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, + m_shadowDepthTex, 0, static_cast(c)); + glClear(GL_DEPTH_BUFFER_BIT); + memcpy(m_lightVP, m_cascadeVP[c], sizeof(m_lightVP)); queue->ExecuteShadowPass(this); } m_inShadowPass = false; @@ -775,10 +861,7 @@ void OpenGLRenderer::RenderShadowPass() { glViewport(savedViewport[0], savedViewport[1], savedViewport[2], savedViewport[3]); - // Bind shadow map texture at unit 3 - glActiveTexture(GL_TEXTURE3); - glBindTexture(GL_TEXTURE_2D, m_shadowDepthTex); - glActiveTexture(GL_TEXTURE0); + BindShadowMapTextures(); } // ============================================================ @@ -1087,11 +1170,7 @@ void OpenGLRenderer::ExecuteDeferredLightingPass() { // Rebind shadow map at unit 3 — BindGBufferShader cleared it to avoid // sampler2DShadow/sampler2D conflict during the geometry pass. - if (m_shadowMapCreated) { - glActiveTexture(GL_TEXTURE3); - glBindTexture(GL_TEXTURE_2D, m_shadowDepthTex); - glActiveTexture(GL_TEXTURE0); - } + BindShadowMapTextures(); // IBL — bind irradiance/prefilter/BRDF at units 14/15/16 and upload the // settings UBO at slot 10. Disabled if precompute hasn't completed. diff --git a/src/Graphics/Vulkan/VulkanDeferred.cpp b/src/Graphics/Vulkan/VulkanDeferred.cpp index 201676ce..fed7e7ed 100644 --- a/src/Graphics/Vulkan/VulkanDeferred.cpp +++ b/src/Graphics/Vulkan/VulkanDeferred.cpp @@ -775,9 +775,9 @@ void VulkanRenderer::UpdateGBufferDescriptors() { // Depth images must use DEPTH_STENCIL_READ_ONLY_OPTIMAL (not SHADER_READ_ONLY_OPTIMAL) // when accessed as a sampler; using the wrong layout causes VK_ERROR_DEVICE_LOST. // Fall back to the default 1x1 white texture when no shadow map is available. - if (m_shadowImageView) { + if (m_shadowArrayView) { imageInfos[4].imageLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL; - imageInfos[4].imageView = m_shadowImageView; + imageInfos[4].imageView = m_shadowArrayView; imageInfos[4].sampler = m_shadowSampler ? m_shadowSampler : (m_defaultTexture ? m_defaultTexture->GetSampler() : VK_NULL_HANDLE); } else if (m_defaultTexture) { diff --git a/src/Graphics/Vulkan/VulkanDescriptors.cpp b/src/Graphics/Vulkan/VulkanDescriptors.cpp index 5094c00c..997a857a 100644 --- a/src/Graphics/Vulkan/VulkanDescriptors.cpp +++ b/src/Graphics/Vulkan/VulkanDescriptors.cpp @@ -66,20 +66,18 @@ bool VulkanRenderer::CreateDescriptorSetLayout() { &m_lightUBODescriptorSetLayout) != VK_SUCCESS) SLEAK_RETURN_ERR("Failed to create light UBO descriptor set layout!"); - // Set 3: shadow map samplers — binding 0 = compare sampler (PCF), - // binding 1 = raw sampler (PCSS blocker search) - std::array shadowSamplerBindings{}; - shadowSamplerBindings[0].binding = 0; - shadowSamplerBindings[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - shadowSamplerBindings[0].descriptorCount = 1; - shadowSamplerBindings[0].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; - shadowSamplerBindings[0].pImmutableSamplers = nullptr; - - shadowSamplerBindings[1].binding = 1; - shadowSamplerBindings[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - shadowSamplerBindings[1].descriptorCount = 1; - shadowSamplerBindings[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; - shadowSamplerBindings[1].pImmutableSamplers = nullptr; + // Set 3: shadow map samplers. Bindings 0/1 = last cascade as a 2D map + // (compare for PCF, raw for the PCSS blocker search), 2/3 = all cascades + // as an array with the same two samplers. + std::array shadowSamplerBindings{}; + for (uint32_t b = 0; b < shadowSamplerBindings.size(); ++b) { + shadowSamplerBindings[b].binding = b; + shadowSamplerBindings[b].descriptorType = + VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + shadowSamplerBindings[b].descriptorCount = 1; + shadowSamplerBindings[b].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + shadowSamplerBindings[b].pImmutableSamplers = nullptr; + } VkDescriptorSetLayoutCreateInfo shadowSamplerLayoutInfo{}; shadowSamplerLayoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; diff --git a/src/Graphics/Vulkan/VulkanPipelines.cpp b/src/Graphics/Vulkan/VulkanPipelines.cpp index 01c3e353..6b367bd7 100644 --- a/src/Graphics/Vulkan/VulkanPipelines.cpp +++ b/src/Graphics/Vulkan/VulkanPipelines.cpp @@ -897,6 +897,16 @@ bool VulkanRenderer::CreateCustomFormatPipelines(VertexFormatHandle format) { } else { VkPipelineShaderStageCreateInfo shaderStage = shadowShader->GetVertexInfo(); + const std::array shadowDynamicStates = { + VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR, + VK_DYNAMIC_STATE_DEPTH_BIAS}; + VkPipelineDynamicStateCreateInfo shadowDynamicState{}; + shadowDynamicState.sType = + VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; + shadowDynamicState.dynamicStateCount = + static_cast(shadowDynamicStates.size()); + shadowDynamicState.pDynamicStates = shadowDynamicStates.data(); + VkPipelineInputAssemblyStateCreateInfo inputAssembly{}; inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; @@ -915,10 +925,7 @@ bool VulkanRenderer::CreateCustomFormatPipelines(VertexFormatHandle format) { rasterizer.lineWidth = 1.0f; rasterizer.cullMode = VK_CULL_MODE_BACK_BIT; rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; - rasterizer.depthBiasEnable = VK_TRUE; - rasterizer.depthBiasConstantFactor = 1.25f; - rasterizer.depthBiasSlopeFactor = 1.75f; - rasterizer.depthBiasClamp = 0.0f; + rasterizer.depthBiasEnable = VK_TRUE; // factors set per pass VkPipelineMultisampleStateCreateInfo msaa{}; msaa.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; @@ -949,7 +956,7 @@ bool VulkanRenderer::CreateCustomFormatPipelines(VertexFormatHandle format) { pipelineInfo.pMultisampleState = &msaa; pipelineInfo.pDepthStencilState = &depthStencil; pipelineInfo.pColorBlendState = &colorBlendInfo; - pipelineInfo.pDynamicState = &dynamicState; + pipelineInfo.pDynamicState = &shadowDynamicState; pipelineInfo.layout = pipelineLay; pipelineInfo.renderPass = m_shadowRenderPass; pipelineInfo.subpass = 0; diff --git a/src/Graphics/Vulkan/VulkanRenderer.cpp b/src/Graphics/Vulkan/VulkanRenderer.cpp index 1b3cfe12..717e32bc 100644 --- a/src/Graphics/Vulkan/VulkanRenderer.cpp +++ b/src/Graphics/Vulkan/VulkanRenderer.cpp @@ -175,6 +175,8 @@ void VulkanRenderer::BeginRender() { if (m_hasPendingLightVP) { memcpy(m_lightVP, m_pendingLightVP, sizeof(m_lightVP)); } + memcpy(m_cascadeVP, m_pendingCascadeVP, sizeof(m_cascadeVP)); + m_cascadeCount = m_pendingCascadeCount; // Apply pending changes between frames if (m_vsyncChangeRequested) @@ -260,60 +262,8 @@ void VulkanRenderer::BeginRender() { bFrameStarted = true; m_pbrMaterialSlot[currentFrame] = 0; // reset PBR material ring for this frame - // Skip shadow pass if no cached draws — preserve previous frame's shadow map - auto* shadowQueue = RenderCommandQueue::GetInstance(); - bool hasShadowDraws = shadowQueue && shadowQueue->HasCachedShadowDraws(); - - if (m_shadowResourcesCreated && m_shadowPassEnabled && hasShadowDraws) { - VkClearValue shadowClear{}; - shadowClear.depthStencil = {1.0f, 0}; - - VkRenderPassBeginInfo shadowPassInfo{}; - shadowPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; - shadowPassInfo.renderPass = m_shadowRenderPass; - shadowPassInfo.framebuffer = m_shadowFramebuffer; - shadowPassInfo.renderArea.offset = {0, 0}; - shadowPassInfo.renderArea.extent = {m_shadowMapResolution, m_shadowMapResolution}; - shadowPassInfo.clearValueCount = 1; - shadowPassInfo.pClearValues = &shadowClear; - - vkCmdBeginRenderPass(command, &shadowPassInfo, VK_SUBPASS_CONTENTS_INLINE); - vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, m_shadowPipeline); - - // shadow_depth.vert statically declares `layout(set = 1, binding = 0) uniform BoneUBO` - // (skinning conditioned on boneWeights). The shader must have set 1 bound even for - // non-skinned casters, otherwise vkCmdDrawIndexed fires VUID-vkCmdDrawIndexed-None-08600. - // Bind the bone UBO once at pass start so all shadow draws (skinned or static) are legal. - if (m_boneUBOCreated) { - vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, - pipelineLay, 1, 1, - &boneDescriptorSets[currentFrame], 0, nullptr); - } - - VkViewport shadowViewport{}; - shadowViewport.x = 0.0f; - shadowViewport.y = 0.0f; - shadowViewport.width = static_cast(m_shadowMapResolution); - shadowViewport.height = static_cast(m_shadowMapResolution); - shadowViewport.minDepth = 0.0f; - shadowViewport.maxDepth = 1.0f; - vkCmdSetViewport(command, 0, 1, &shadowViewport); - - VkRect2D shadowScissor{}; - shadowScissor.offset = {0, 0}; - shadowScissor.extent = {m_shadowMapResolution, m_shadowMapResolution}; - vkCmdSetScissor(command, 0, 1, &shadowScissor); - - m_shadowPassActive = true; - m_shadowPCCacheValid = false; - auto* queue = RenderCommandQueue::GetInstance(); - if (queue) { - queue->ExecuteShadowPass(this); - } - m_shadowPassActive = false; - - vkCmdEndRenderPass(command); - } + // No cached draws keeps the previous frame's shadow map + RecordShadowPass(); // ---- Deferred path: begin GBuffer render pass ---- if (m_gbufferResourcesCreated && m_deferredEnabled) { @@ -1494,190 +1444,32 @@ bool VulkanRenderer::CreateSyncObjects() { return true; } -/// Recreates the extent-dependent shadow map objects (image, view, -/// framebuffer) at the queued resolution. Keep in sync with -/// CreateShadowResources; samplers, render pass, and pipeline are extent-independent. +/// Recreates the shadow image, views, and framebuffers at the queued +/// resolution and cascade count; samplers, render pass, and pipeline stay. void VulkanRenderer::ApplyShadowResolutionChange() { if (!m_shadowResChangeRequested) return; m_shadowResChangeRequested = false; if (!m_shadowResourcesCreated) return; - if (m_pendingShadowMapResolution == m_shadowMapResolution) return; + if (m_pendingShadowMapResolution == m_shadowMapResolution && + m_pendingShadowCascadeCount == m_shadowCascadeCount) + return; vkDeviceWaitIdle(device); - - vkDestroyFramebuffer(device, m_shadowFramebuffer, nullptr); - m_shadowFramebuffer = VK_NULL_HANDLE; - vkDestroyImageView(device, m_shadowImageView, nullptr); - m_shadowImageView = VK_NULL_HANDLE; - vkDestroyImage(device, m_shadowImage, nullptr); - m_shadowImage = VK_NULL_HANDLE; - vkFreeMemory(device, m_shadowImageMemory, nullptr); - m_shadowImageMemory = VK_NULL_HANDLE; + DestroyShadowMapImage(); m_shadowMapResolution = m_pendingShadowMapResolution; - - VkImageCreateInfo imageInfo{}; - imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; - imageInfo.imageType = VK_IMAGE_TYPE_2D; - imageInfo.extent = {m_shadowMapResolution, m_shadowMapResolution, 1}; - imageInfo.mipLevels = 1; - imageInfo.arrayLayers = 1; - imageInfo.format = VK_FORMAT_D32_SFLOAT; - imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; - imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - imageInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | - VK_IMAGE_USAGE_SAMPLED_BIT; - imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; - imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; - - if (vkCreateImage(device, &imageInfo, nullptr, &m_shadowImage) != - VK_SUCCESS) { - SLEAK_ERROR("Shadow resolution change: image creation failed"); - m_shadowResourcesCreated = false; - return; - } - - VkMemoryRequirements memReqs; - vkGetImageMemoryRequirements(device, m_shadowImage, &memReqs); - - VkMemoryAllocateInfo allocInfo{}; - allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; - allocInfo.allocationSize = memReqs.size; - allocInfo.memoryTypeIndex = FindMemoryType( - memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - - if (vkAllocateMemory(device, &allocInfo, nullptr, &m_shadowImageMemory) != - VK_SUCCESS) { - SLEAK_ERROR("Shadow resolution change: memory allocation failed"); - m_shadowResourcesCreated = false; - return; - } - vkBindImageMemory(device, m_shadowImage, m_shadowImageMemory, 0); - - VkImageViewCreateInfo viewInfo{}; - viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; - viewInfo.image = m_shadowImage; - viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; - viewInfo.format = VK_FORMAT_D32_SFLOAT; - viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; - viewInfo.subresourceRange.baseMipLevel = 0; - viewInfo.subresourceRange.levelCount = 1; - viewInfo.subresourceRange.baseArrayLayer = 0; - viewInfo.subresourceRange.layerCount = 1; - - if (vkCreateImageView(device, &viewInfo, nullptr, &m_shadowImageView) != - VK_SUCCESS) { - SLEAK_ERROR("Shadow resolution change: image view creation failed"); + m_shadowCascadeCount = m_pendingShadowCascadeCount; + if (!CreateShadowMapImage() || !CreateShadowFramebuffers()) { + SLEAK_ERROR("Shadow map recreation failed"); + DestroyShadowMapImage(); m_shadowResourcesCreated = false; return; } + WriteShadowSamplerDescriptors(); - VkFramebufferCreateInfo fbInfo{}; - fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; - fbInfo.renderPass = m_shadowRenderPass; - fbInfo.attachmentCount = 1; - fbInfo.pAttachments = &m_shadowImageView; - fbInfo.width = m_shadowMapResolution; - fbInfo.height = m_shadowMapResolution; - fbInfo.layers = 1; - - if (vkCreateFramebuffer(device, &fbInfo, nullptr, &m_shadowFramebuffer) != - VK_SUCCESS) { - SLEAK_ERROR("Shadow resolution change: framebuffer creation failed"); - m_shadowResourcesCreated = false; - return; - } - - // Initial layout transition — descriptor must be valid pre-first-pass - { - VkCommandBufferAllocateInfo cmdAllocInfo{}; - cmdAllocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; - cmdAllocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; - cmdAllocInfo.commandPool = commands; - cmdAllocInfo.commandBufferCount = 1; - - VkCommandBuffer cmdBuf; - vkAllocateCommandBuffers(device, &cmdAllocInfo, &cmdBuf); - - VkCommandBufferBeginInfo cmdBeginInfo{}; - cmdBeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - cmdBeginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(cmdBuf, &cmdBeginInfo); - - VkImageMemoryBarrier barrier{}; - barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; - barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; - barrier.newLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL; - barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier.image = m_shadowImage; - barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; - barrier.subresourceRange.baseMipLevel = 0; - barrier.subresourceRange.levelCount = 1; - barrier.subresourceRange.baseArrayLayer = 0; - barrier.subresourceRange.layerCount = 1; - barrier.srcAccessMask = 0; - barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; - - vkCmdPipelineBarrier(cmdBuf, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, - VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, - nullptr, 0, nullptr, 1, &barrier); - - vkEndCommandBuffer(cmdBuf); - - VkSubmitInfo layoutSubmit{}; - layoutSubmit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - layoutSubmit.commandBufferCount = 1; - layoutSubmit.pCommandBuffers = &cmdBuf; - - VkFenceCreateInfo layoutFenceInfo{}; - layoutFenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; - VkFence layoutFence; - vkCreateFence(device, &layoutFenceInfo, nullptr, &layoutFence); - vkQueueSubmit(graphicsQueue, 1, &layoutSubmit, layoutFence); - vkWaitForFences(device, 1, &layoutFence, VK_TRUE, UINT64_MAX); - vkDestroyFence(device, layoutFence, nullptr); - vkFreeCommandBuffers(device, commands, 1, &cmdBuf); - } - - // Point set-3 descriptors at the new image view - if (m_lightUBOCreated && m_shadowImageView && m_shadowSampler && - m_shadowRawSampler) { - for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) { - VkDescriptorImageInfo compareInfo{}; - compareInfo.imageLayout = - VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL; - compareInfo.imageView = m_shadowImageView; - compareInfo.sampler = m_shadowSampler; - - VkDescriptorImageInfo rawInfo{}; - rawInfo.imageLayout = - VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL; - rawInfo.imageView = m_shadowImageView; - rawInfo.sampler = m_shadowRawSampler; - - std::array writes{}; - writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - writes[0].dstSet = m_shadowSamplerDescriptorSets[i]; - writes[0].dstBinding = 0; - writes[0].dstArrayElement = 0; - writes[0].descriptorType = - VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - writes[0].descriptorCount = 1; - writes[0].pImageInfo = &compareInfo; - - writes[1] = writes[0]; - writes[1].dstBinding = 1; - writes[1].pImageInfo = &rawInfo; - - vkUpdateDescriptorSets(device, - static_cast(writes.size()), - writes.data(), 0, nullptr); - } - } - - SLEAK_INFO("VulkanRenderer: Shadow map resized to {}x{}", - m_shadowMapResolution, m_shadowMapResolution); + SLEAK_INFO("VulkanRenderer: Shadow map resized to {}x{}, {} cascades", + m_shadowMapResolution, m_shadowMapResolution, + m_shadowCascadeCount); } /// Top-level orchestrator for deferred rendering: creates the GBuffer color diff --git a/src/Graphics/Vulkan/VulkanShadow.cpp b/src/Graphics/Vulkan/VulkanShadow.cpp index 3ff9a4d6..0d4cbcec 100644 --- a/src/Graphics/Vulkan/VulkanShadow.cpp +++ b/src/Graphics/Vulkan/VulkanShadow.cpp @@ -14,57 +14,7 @@ namespace Sleak { /// Creates the shadow depth image, sampler, render pass, and framebuffer. bool VulkanRenderer::CreateShadowResources() { // 1. Create shadow depth image (2048x2048, D32_SFLOAT) - VkImageCreateInfo imageInfo{}; - imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; - imageInfo.imageType = VK_IMAGE_TYPE_2D; - imageInfo.extent = {m_shadowMapResolution, m_shadowMapResolution, 1}; - imageInfo.mipLevels = 1; - imageInfo.arrayLayers = 1; - imageInfo.format = VK_FORMAT_D32_SFLOAT; - imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; - imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - imageInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | - VK_IMAGE_USAGE_SAMPLED_BIT; - imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; - imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; - - if (vkCreateImage(device, &imageInfo, nullptr, &m_shadowImage) != VK_SUCCESS) { - SLEAK_ERROR("Failed to create shadow map image!"); - return false; - } - - VkMemoryRequirements memReqs; - vkGetImageMemoryRequirements(device, m_shadowImage, &memReqs); - - VkMemoryAllocateInfo allocInfo{}; - allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; - allocInfo.allocationSize = memReqs.size; - allocInfo.memoryTypeIndex = FindMemoryType(memReqs.memoryTypeBits, - VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - - if (vkAllocateMemory(device, &allocInfo, nullptr, &m_shadowImageMemory) != VK_SUCCESS) { - SLEAK_ERROR("Failed to allocate shadow map memory!"); - return false; - } - - vkBindImageMemory(device, m_shadowImage, m_shadowImageMemory, 0); - - // 2. Create image view (DEPTH aspect) - VkImageViewCreateInfo viewInfo{}; - viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; - viewInfo.image = m_shadowImage; - viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; - viewInfo.format = VK_FORMAT_D32_SFLOAT; - viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; - viewInfo.subresourceRange.baseMipLevel = 0; - viewInfo.subresourceRange.levelCount = 1; - viewInfo.subresourceRange.baseArrayLayer = 0; - viewInfo.subresourceRange.layerCount = 1; - - if (vkCreateImageView(device, &viewInfo, nullptr, &m_shadowImageView) != VK_SUCCESS) { - SLEAK_ERROR("Failed to create shadow map image view!"); - return false; - } + if (!CreateShadowMapImage()) return false; // 3. Create comparison sampler with bilinear filtering for smooth PCF VkSamplerCreateInfo samplerInfo{}; @@ -153,76 +103,7 @@ bool VulkanRenderer::CreateShadowResources() { return false; } - // 5. Create framebuffer - VkFramebufferCreateInfo fbInfo{}; - fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; - fbInfo.renderPass = m_shadowRenderPass; - fbInfo.attachmentCount = 1; - fbInfo.pAttachments = &m_shadowImageView; - fbInfo.width = m_shadowMapResolution; - fbInfo.height = m_shadowMapResolution; - fbInfo.layers = 1; - - if (vkCreateFramebuffer(device, &fbInfo, nullptr, &m_shadowFramebuffer) != VK_SUCCESS) { - SLEAK_ERROR("Failed to create shadow framebuffer!"); - return false; - } - - // 6. Transition shadow image to DEPTH_STENCIL_READ_ONLY_OPTIMAL so the - // descriptor is valid even before the first shadow pass runs. - // Depth images must use this layout (not SHADER_READ_ONLY_OPTIMAL) - // for sampler access; the wrong layout causes VK_ERROR_DEVICE_LOST. - { - VkCommandBufferAllocateInfo cmdAllocInfo{}; - cmdAllocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; - cmdAllocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; - cmdAllocInfo.commandPool = commands; - cmdAllocInfo.commandBufferCount = 1; - - VkCommandBuffer cmdBuf; - vkAllocateCommandBuffers(device, &cmdAllocInfo, &cmdBuf); - - VkCommandBufferBeginInfo cmdBeginInfo{}; - cmdBeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - cmdBeginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(cmdBuf, &cmdBeginInfo); - - VkImageMemoryBarrier barrier{}; - barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; - barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; - barrier.newLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL; - barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier.image = m_shadowImage; - barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; - barrier.subresourceRange.baseMipLevel = 0; - barrier.subresourceRange.levelCount = 1; - barrier.subresourceRange.baseArrayLayer = 0; - barrier.subresourceRange.layerCount = 1; - barrier.srcAccessMask = 0; - barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; - - vkCmdPipelineBarrier(cmdBuf, - VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, - VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, - 0, 0, nullptr, 0, nullptr, 1, &barrier); - - vkEndCommandBuffer(cmdBuf); - - VkSubmitInfo layoutSubmit{}; - layoutSubmit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - layoutSubmit.commandBufferCount = 1; - layoutSubmit.pCommandBuffers = &cmdBuf; - - VkFenceCreateInfo layoutFenceInfo{}; - layoutFenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; - VkFence layoutFence; - vkCreateFence(device, &layoutFenceInfo, nullptr, &layoutFence); - vkQueueSubmit(graphicsQueue, 1, &layoutSubmit, layoutFence); - vkWaitForFences(device, 1, &layoutFence, VK_TRUE, UINT64_MAX); - vkDestroyFence(device, layoutFence, nullptr); - vkFreeCommandBuffers(device, commands, 1, &cmdBuf); - } + if (!CreateShadowFramebuffers()) return false; // 7. Create shadow pipeline if (!CreateShadowPipeline()) { @@ -230,43 +111,12 @@ bool VulkanRenderer::CreateShadowResources() { return false; } - // Write shadow sampler to set 3 descriptors (UBO resources already created). - // Binding 0 uses the compare sampler for hardware PCF; binding 1 uses the - // raw sampler so the PCSS blocker search can read un-compared depth values. - if (m_lightUBOCreated && m_shadowImageView && m_shadowSampler && m_shadowRawSampler) { - for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) { - // Depth images must use DEPTH_STENCIL_READ_ONLY_OPTIMAL for sampler access. - VkDescriptorImageInfo compareInfo{}; - compareInfo.imageLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL; - compareInfo.imageView = m_shadowImageView; - compareInfo.sampler = m_shadowSampler; - - VkDescriptorImageInfo rawInfo{}; - rawInfo.imageLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL; - rawInfo.imageView = m_shadowImageView; - rawInfo.sampler = m_shadowRawSampler; - - std::array writes{}; - writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - writes[0].dstSet = m_shadowSamplerDescriptorSets[i]; - writes[0].dstBinding = 0; - writes[0].dstArrayElement = 0; - writes[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - writes[0].descriptorCount = 1; - writes[0].pImageInfo = &compareInfo; - - writes[1] = writes[0]; - writes[1].dstBinding = 1; - writes[1].pImageInfo = &rawInfo; - - vkUpdateDescriptorSets(device, static_cast(writes.size()), - writes.data(), 0, nullptr); - } - } + WriteShadowSamplerDescriptors(); m_shadowResourcesCreated = true; - SLEAK_INFO("VulkanRenderer: Shadow mapping resources created ({}x{} shadow map)", - m_shadowMapResolution, m_shadowMapResolution); + SLEAK_INFO( + "VulkanRenderer: Shadow mapping resources created ({}x{}, {} cascades)", + m_shadowMapResolution, m_shadowMapResolution, m_shadowCascadeCount); return true; } @@ -283,8 +133,9 @@ bool VulkanRenderer::CreateShadowPipeline() { // Vertex-only pipeline (no fragment shader) VkPipelineShaderStageCreateInfo shaderStage = m_shadowShader->GetVertexInfo(); - std::vector dynamicStates = { - VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; + std::vector dynamicStates = {VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR, + VK_DYNAMIC_STATE_DEPTH_BIAS}; VkPipelineDynamicStateCreateInfo dynamicState{}; dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; @@ -332,10 +183,7 @@ bool VulkanRenderer::CreateShadowPipeline() { rasterizer.lineWidth = 1.0f; rasterizer.cullMode = VK_CULL_MODE_BACK_BIT; // same winding as main pass rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; - rasterizer.depthBiasEnable = VK_TRUE; - rasterizer.depthBiasConstantFactor = 1.25f; - rasterizer.depthBiasSlopeFactor = 1.75f; - rasterizer.depthBiasClamp = 0.0f; + rasterizer.depthBiasEnable = VK_TRUE; // factors set per pass VkPipelineMultisampleStateCreateInfo msaa{}; msaa.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; @@ -421,13 +269,13 @@ bool VulkanRenderer::CreateShadowLightUBOResources() { memset(m_lightUBOMapped[i], 0, uboSize); } - // Create descriptor pool for light UBO + shadow samplers. - // Two shadow samplers per frame now (compare + raw for PCSS blocker search). + // Create descriptor pool for light UBO + shadow samplers: compare + raw + // for the last cascade (2D) and for all cascades (array). std::array poolSizes{}; poolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; poolSizes[0].descriptorCount = MAX_FRAMES_IN_FLIGHT; poolSizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - poolSizes[1].descriptorCount = MAX_FRAMES_IN_FLIGHT * 2; + poolSizes[1].descriptorCount = MAX_FRAMES_IN_FLIGHT * 4; VkDescriptorPoolCreateInfo poolInfo{}; poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; @@ -489,10 +337,9 @@ bool VulkanRenderer::CreateShadowLightUBOResources() { return false; } - // Write default shadow sampler descriptors (using default texture as placeholder) - // These will be overwritten with actual shadow map when shadow resources are created. - // Both binding=0 (compare) and binding=1 (raw) must be populated or the layout - // is incomplete and first-frame sampling reads undefined memory. + // Placeholder descriptors (the default texture) until the shadow map + // exists. Every binding must be populated or the layout is incomplete and + // first-frame sampling reads undefined memory. if (m_defaultTexture) { for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) { VkDescriptorImageInfo imageInfo{}; @@ -500,17 +347,16 @@ bool VulkanRenderer::CreateShadowLightUBOResources() { imageInfo.imageView = m_defaultTexture->GetImageView(); imageInfo.sampler = m_defaultTexture->GetSampler(); - std::array writes{}; - writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - writes[0].dstSet = m_shadowSamplerDescriptorSets[i]; - writes[0].dstBinding = 0; - writes[0].dstArrayElement = 0; - writes[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - writes[0].descriptorCount = 1; - writes[0].pImageInfo = &imageInfo; - - writes[1] = writes[0]; - writes[1].dstBinding = 1; + std::array writes{}; + for (uint32_t b = 0; b < writes.size(); ++b) { + writes[b].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[b].dstSet = m_shadowSamplerDescriptorSets[i]; + writes[b].dstBinding = b; + writes[b].descriptorType = + VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + writes[b].descriptorCount = 1; + writes[b].pImageInfo = &imageInfo; + } vkUpdateDescriptorSets(device, static_cast(writes.size()), writes.data(), 0, nullptr); @@ -531,10 +377,6 @@ void VulkanRenderer::CleanupShadowResources() { delete m_shadowShader; m_shadowShader = nullptr; - if (m_shadowFramebuffer) { - vkDestroyFramebuffer(device, m_shadowFramebuffer, nullptr); - m_shadowFramebuffer = VK_NULL_HANDLE; - } if (m_shadowRenderPass) { vkDestroyRenderPass(device, m_shadowRenderPass, nullptr); m_shadowRenderPass = VK_NULL_HANDLE; @@ -547,18 +389,7 @@ void VulkanRenderer::CleanupShadowResources() { vkDestroySampler(device, m_shadowRawSampler, nullptr); m_shadowRawSampler = VK_NULL_HANDLE; } - if (m_shadowImageView) { - vkDestroyImageView(device, m_shadowImageView, nullptr); - m_shadowImageView = VK_NULL_HANDLE; - } - if (m_shadowImage) { - vkDestroyImage(device, m_shadowImage, nullptr); - m_shadowImage = VK_NULL_HANDLE; - } - if (m_shadowImageMemory) { - vkFreeMemory(device, m_shadowImageMemory, nullptr); - m_shadowImageMemory = VK_NULL_HANDLE; - } + DestroyShadowMapImage(); // Cleanup light UBO resources if (m_lightUBOCreated) { @@ -618,5 +449,279 @@ void VulkanRenderer::SetLightVP(const float* lightVP) { } } +/// Stages the per-cascade light view-projections; BeginRender commits them. +void VulkanRenderer::SetShadowCascades(const float* viewProj, uint32_t count) { + count = std::min(count, MAX_SHADOW_CASCADES); + if (count > 0 && viewProj) + memcpy(m_pendingCascadeVP, viewProj, sizeof(float) * 16 * count); + m_pendingCascadeCount = count; +} + +/// Creates the layered shadow image and its array and per-layer views. +bool VulkanRenderer::CreateShadowMapImage() { + const uint32_t layers = m_shadowCascadeCount; + + VkImageCreateInfo imageInfo{}; + imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + imageInfo.imageType = VK_IMAGE_TYPE_2D; + imageInfo.extent = {m_shadowMapResolution, m_shadowMapResolution, 1}; + imageInfo.mipLevels = 1; + imageInfo.arrayLayers = layers; + imageInfo.format = VK_FORMAT_D32_SFLOAT; + imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + imageInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | + VK_IMAGE_USAGE_SAMPLED_BIT; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + + if (vkCreateImage(device, &imageInfo, nullptr, &m_shadowImage) != + VK_SUCCESS) { + SLEAK_ERROR("Failed to create shadow map image!"); + return false; + } + + VkMemoryRequirements memReqs; + vkGetImageMemoryRequirements(device, m_shadowImage, &memReqs); + + VkMemoryAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + allocInfo.allocationSize = memReqs.size; + allocInfo.memoryTypeIndex = FindMemoryType( + memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + + if (vkAllocateMemory(device, &allocInfo, nullptr, &m_shadowImageMemory) != + VK_SUCCESS) { + SLEAK_ERROR("Failed to allocate shadow map memory!"); + return false; + } + vkBindImageMemory(device, m_shadowImage, m_shadowImageMemory, 0); + + VkImageViewCreateInfo viewInfo{}; + viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + viewInfo.image = m_shadowImage; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY; + viewInfo.format = VK_FORMAT_D32_SFLOAT; + viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; + viewInfo.subresourceRange.baseMipLevel = 0; + viewInfo.subresourceRange.levelCount = 1; + viewInfo.subresourceRange.baseArrayLayer = 0; + viewInfo.subresourceRange.layerCount = layers; + + if (vkCreateImageView(device, &viewInfo, nullptr, &m_shadowArrayView) != + VK_SUCCESS) { + SLEAK_ERROR("Failed to create shadow map array view!"); + return false; + } + + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.subresourceRange.layerCount = 1; + for (uint32_t i = 0; i < layers; ++i) { + viewInfo.subresourceRange.baseArrayLayer = i; + if (vkCreateImageView(device, &viewInfo, nullptr, + &m_shadowLayerViews[i]) != VK_SUCCESS) { + SLEAK_ERROR("Failed to create shadow map layer view!"); + return false; + } + } + // Shaders that only know one shadow map sample the last cascade. + m_shadowImageView = m_shadowLayerViews[layers - 1]; + + // Transition every layer to DEPTH_STENCIL_READ_ONLY_OPTIMAL so the + // descriptors are valid before the first shadow pass runs. Depth images + // must use this layout (not SHADER_READ_ONLY_OPTIMAL) for sampler access. + { + VkCommandBufferAllocateInfo cmdAllocInfo{}; + cmdAllocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + cmdAllocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + cmdAllocInfo.commandPool = commands; + cmdAllocInfo.commandBufferCount = 1; + + VkCommandBuffer cmdBuf; + vkAllocateCommandBuffers(device, &cmdAllocInfo, &cmdBuf); + + VkCommandBufferBeginInfo cmdBeginInfo{}; + cmdBeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + cmdBeginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + vkBeginCommandBuffer(cmdBuf, &cmdBeginInfo); + + VkImageMemoryBarrier barrier{}; + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barrier.newLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = m_shadowImage; + barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; + barrier.subresourceRange.baseMipLevel = 0; + barrier.subresourceRange.levelCount = 1; + barrier.subresourceRange.baseArrayLayer = 0; + barrier.subresourceRange.layerCount = layers; + barrier.srcAccessMask = 0; + barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + + vkCmdPipelineBarrier(cmdBuf, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, + nullptr, 0, nullptr, 1, &barrier); + + vkEndCommandBuffer(cmdBuf); + + VkSubmitInfo layoutSubmit{}; + layoutSubmit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + layoutSubmit.commandBufferCount = 1; + layoutSubmit.pCommandBuffers = &cmdBuf; + + VkFenceCreateInfo layoutFenceInfo{}; + layoutFenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; + VkFence layoutFence; + vkCreateFence(device, &layoutFenceInfo, nullptr, &layoutFence); + vkQueueSubmit(graphicsQueue, 1, &layoutSubmit, layoutFence); + vkWaitForFences(device, 1, &layoutFence, VK_TRUE, UINT64_MAX); + vkDestroyFence(device, layoutFence, nullptr); + vkFreeCommandBuffers(device, commands, 1, &cmdBuf); + } + + return true; +} + +/// Creates one framebuffer per cascade layer for the shadow render pass. +bool VulkanRenderer::CreateShadowFramebuffers() { + VkFramebufferCreateInfo fbInfo{}; + fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + fbInfo.renderPass = m_shadowRenderPass; + fbInfo.attachmentCount = 1; + fbInfo.width = m_shadowMapResolution; + fbInfo.height = m_shadowMapResolution; + fbInfo.layers = 1; + + for (uint32_t i = 0; i < m_shadowCascadeCount; ++i) { + fbInfo.pAttachments = &m_shadowLayerViews[i]; + if (vkCreateFramebuffer(device, &fbInfo, nullptr, + &m_shadowFramebuffers[i]) != VK_SUCCESS) { + SLEAK_ERROR("Failed to create shadow framebuffer!"); + return false; + } + } + return true; +} + +/// Destroys the shadow image, its views, and the per-cascade framebuffers. +void VulkanRenderer::DestroyShadowMapImage() { + for (auto& fb : m_shadowFramebuffers) { + if (fb) vkDestroyFramebuffer(device, fb, nullptr); + fb = VK_NULL_HANDLE; + } + for (auto& view : m_shadowLayerViews) { + if (view) vkDestroyImageView(device, view, nullptr); + view = VK_NULL_HANDLE; + } + m_shadowImageView = VK_NULL_HANDLE; + if (m_shadowArrayView) { + vkDestroyImageView(device, m_shadowArrayView, nullptr); + m_shadowArrayView = VK_NULL_HANDLE; + } + if (m_shadowImage) { + vkDestroyImage(device, m_shadowImage, nullptr); + m_shadowImage = VK_NULL_HANDLE; + } + if (m_shadowImageMemory) { + vkFreeMemory(device, m_shadowImageMemory, nullptr); + m_shadowImageMemory = VK_NULL_HANDLE; + } +} + +/// Points set 3 at the shadow views: bindings 0/1 see the last cascade as a +/// plain 2D map, bindings 2/3 see every cascade as an array. +void VulkanRenderer::WriteShadowSamplerDescriptors() { + if (!m_lightUBOCreated || !m_shadowImageView || !m_shadowArrayView || + !m_shadowSampler || !m_shadowRawSampler) + return; + + for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) { + std::array infos{}; + for (auto& info : infos) + info.imageLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL; + infos[0].imageView = m_shadowImageView; + infos[0].sampler = m_shadowSampler; + infos[1].imageView = m_shadowImageView; + infos[1].sampler = m_shadowRawSampler; + infos[2].imageView = m_shadowArrayView; + infos[2].sampler = m_shadowSampler; + infos[3].imageView = m_shadowArrayView; + infos[3].sampler = m_shadowRawSampler; + + std::array writes{}; + for (uint32_t b = 0; b < writes.size(); ++b) { + writes[b].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[b].dstSet = m_shadowSamplerDescriptorSets[i]; + writes[b].dstBinding = b; + writes[b].descriptorType = + VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + writes[b].descriptorCount = 1; + writes[b].pImageInfo = &infos[b]; + } + vkUpdateDescriptorSets(device, static_cast(writes.size()), + writes.data(), 0, nullptr); + } +} + +/// Renders the cached shadow casters once per cascade, each into its own +/// layer with that cascade's light VP behind the shadow push constants. +void VulkanRenderer::RecordShadowPass() { + auto* queue = RenderCommandQueue::GetInstance(); + if (!m_shadowResourcesCreated || !m_shadowPassEnabled || !queue || + !queue->HasCachedShadowDraws()) + return; + + const uint32_t cascades = std::min(m_cascadeCount, m_shadowCascadeCount); + + VkClearValue shadowClear{}; + shadowClear.depthStencil = {1.0f, 0}; + + VkViewport viewport{}; + viewport.width = static_cast(m_shadowMapResolution); + viewport.height = static_cast(m_shadowMapResolution); + viewport.maxDepth = 1.0f; + VkRect2D scissor{}; + scissor.extent = {m_shadowMapResolution, m_shadowMapResolution}; + + for (uint32_t c = 0; c < cascades; ++c) { + VkRenderPassBeginInfo passInfo{}; + passInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + passInfo.renderPass = m_shadowRenderPass; + passInfo.framebuffer = m_shadowFramebuffers[c]; + passInfo.renderArea.extent = scissor.extent; + passInfo.clearValueCount = 1; + passInfo.pClearValues = &shadowClear; + + vkCmdBeginRenderPass(command, &passInfo, VK_SUBPASS_CONTENTS_INLINE); + vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, + m_shadowPipeline); + m_activeCustomFormat = 0; + m_customFormatUnbound = false; + + // shadow_depth.vert statically declares the set-1 BoneUBO, so it must + // be bound even for static casters (VUID-vkCmdDrawIndexed-None-08600). + if (m_boneUBOCreated) { + vkCmdBindDescriptorSets( + command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLay, 1, 1, + &boneDescriptorSets[currentFrame], 0, nullptr); + } + + vkCmdSetViewport(command, 0, 1, &viewport); + vkCmdSetScissor(command, 0, 1, &scissor); + vkCmdSetDepthBias(command, 1.25f * m_shadowDepthBiasScale, 0.0f, + 1.75f * m_shadowDepthBiasScale); + + memcpy(m_lightVP, m_cascadeVP[c], sizeof(m_lightVP)); + m_shadowPassActive = true; + m_shadowPCCacheValid = false; + queue->ExecuteShadowPass(this); + m_shadowPassActive = false; + + vkCmdEndRenderPass(command); + } +} + } // namespace RenderEngine } // namespace Sleak diff --git a/src/Lighting/LightManager.cpp b/src/Lighting/LightManager.cpp index b4517184..70de20ff 100644 --- a/src/Lighting/LightManager.cpp +++ b/src/Lighting/LightManager.cpp @@ -51,6 +51,84 @@ static bool Invert4x4(const float m[16], float inv[16]) { return true; } + +constexpr float kCascadeBlend = 0.1f; +constexpr float kCascadePadTexels = 8.0f; + +/// Near plane of the main camera, read back from its projection matrix. +float MainCameraNear() { + const auto& P = Sleak::Camera::GetMainProjectionMatrix(); + if (P(2, 3) == 0.0f || P(2, 2) == 0.0f) return 0.1f; + float n = -P(3, 2) / P(2, 2); + return n > 0.0f ? n : 0.1f; +} + +/// Fits one sphere per cascade around the camera (practical split radii) and +/// builds a texel-snapped orthographic light view-projection for each. +void BuildShadowCascades(const Sleak::DirectionalLight& light, + const Sleak::Math::Vector3D& camPos, uint32_t count, + uint32_t resolution, float outVP[][16], + float outSplits[]) { + using namespace Sleak; + float maxDist = light.GetShadowMaxDistance(); + if (maxDist <= 0.0f) maxDist = light.GetShadowFrustumSize(); + const float nearDist = std::clamp(MainCameraNear(), 0.01f, maxDist * 0.5f); + const float lambda = std::clamp(light.GetShadowSplitLambda(), 0.0f, 1.0f); + + // Fixed light orientation with no translation: the cascades only move + // in light space, where they are snapped to whole texels. + const auto dir = light.GetDirection(); + Math::Vector ld({dir.GetX(), dir.GetY(), dir.GetZ()}); + Math::Vector up = + (std::fabs(dir.GetY()) > 0.999f) + ? Math::Vector({0.0f, 0.0f, 1.0f}) + : Math::Vector({0.0f, 1.0f, 0.0f}); + Math::Matrix4 lightView = Math::Matrix4::LookTo( + Math::Vector({0.0f, 0.0f, 0.0f}), ld, up); + + auto toLight = [&](int axis) { + return camPos.GetX() * lightView(0, axis) + + camPos.GetY() * lightView(1, axis) + + camPos.GetZ() * lightView(2, axis); + }; + const float cx = toLight(0), cy = toLight(1), cz = toLight(2); + + const float res = static_cast(resolution); + const float pad = res / (res - 2.0f * kCascadePadTexels); + + for (uint32_t i = 0; i < count; ++i) { + const float p = static_cast(i + 1) / static_cast(count); + const float logSplit = nearDist * std::pow(maxDist / nearDist, p); + const float uniSplit = nearDist + (maxDist - nearDist) * p; + const float split = + (i + 1 == count) ? maxDist + : lambda * logSplit + (1.0f - lambda) * uniSplit; + outSplits[i] = split; + + const float halfExtent = split * pad; + const float texel = 2.0f * halfExtent / res; + const float x = std::round(cx / texel) * texel; + const float y = std::round(cy / texel) * texel; + + const float pull = std::max(light.GetShadowDistance(), halfExtent); + const float zNear = cz - pull + light.GetShadowNearPlane(); + const float zFar = + cz - pull + std::max(light.GetShadowFarPlane(), pull + halfExtent); + + // Vulkan-style [0,1] depth; translations live in row 3 because the + // engine is row-major and GLSL reads the matrix transposed. + Math::Matrix4 proj = Math::Matrix4::Identity(); + proj(0, 0) = 1.0f / halfExtent; + proj(1, 1) = 1.0f / halfExtent; + proj(2, 2) = 1.0f / (zFar - zNear); + proj(3, 0) = -x / halfExtent; + proj(3, 1) = -y / halfExtent; + proj(3, 2) = -zNear / (zFar - zNear); + + Math::Matrix4 vp = lightView * proj; + std::memcpy(outVP[i], &vp(0, 0), sizeof(float) * 16); + } +} } // namespace namespace Sleak { @@ -212,107 +290,44 @@ void LightManager::UpdateShadowData() { float intensity = activeLight->GetIntensity(); Math::Matrix4 lightVP = Math::Matrix4::Identity(); + float cascadeVP[RenderEngine::MAX_SHADOW_CASCADES][16] = {}; + float cascadeSplits[RenderEngine::MAX_SHADOW_CASCADES] = {}; + uint32_t cascadeCount = 0; if (shadowLight) { - // Compute light view-projection matrix from shadow configuration - float frustumSize = shadowLight->GetShadowFrustumSize(); - float shadowDist = shadowLight->GetShadowDistance(); - float nearP = shadowLight->GetShadowNearPlane(); - float farP = shadowLight->GetShadowFarPlane(); - - // Light position: follow camera XZ but fix Y at world origin. - // Anchoring Y prevents the shadow frustum from shifting vertically - // when the player jumps/flies, which causes hard Z-plane cutoff flicker. - const auto& camPos = Camera::GetMainCameraPosition(); - Math::Vector3D lightPos = Math::Vector3D(camPos.GetX(), 0.0f, camPos.GetZ()) - + dir * (-shadowDist); - - // Convert to Vector for Matrix methods - Math::Vector lp({lightPos.GetX(), lightPos.GetY(), lightPos.GetZ()}); - Math::Vector ld({dir.GetX(), dir.GetY(), dir.GetZ()}); - - // Avoid degenerate LookTo when light direction is nearly vertical - // (cross product with (0,1,0) would be zero → NaN matrix) - Math::Vector up = (fabsf(dir.GetY()) > 0.999f) - ? Math::Vector({0.0f, 0.0f, 1.0f}) - : Math::Vector({0.0f, 1.0f, 0.0f}); - - Math::Matrix4 lightView = Math::Matrix4::LookTo(lp, ld, up); - - // Build Vulkan-compatible orthographic projection (LH, [0,1] depth range) - // Engine stores row-major, GLSL reads column-major (transposed) — - // translations go in ROW 3 so they end up in GLSL column 3. - float left = -frustumSize, right = frustumSize; - float bottom = -frustumSize, top = frustumSize; - Math::Matrix4 lightProj = Math::Matrix4::Identity(); - lightProj(0, 0) = 2.0f / (right - left); - lightProj(1, 1) = 2.0f / (top - bottom); - lightProj(2, 2) = 1.0f / (farP - nearP); - lightProj(3, 0) = -(right + left) / (right - left); - lightProj(3, 1) = -(top + bottom) / (top - bottom); - lightProj(3, 2) = -nearP / (farP - nearP); - - // ---- Texel snap (DirectX SDK standard technique) ---- - // Project world origin through the raw lightVP, measure its XY in - // shadow-map texel space, snap to nearest texel, apply the delta - // back to the projection matrix. This guarantees the sampling grid - // is aligned to world-space texel cells so sub-texel camera motion - // never shifts which texel a world point lands on → no shimmer. - // - // Using round() (not floor) — floor flips by a full texel when - // the fractional part crosses 0 due to float noise. - const float shadowMapSize = - static_cast(renderer->GetShadowMapResolution()); - const float halfShadow = shadowMapSize * 0.5f; - - Math::Matrix4 lightVP_raw = lightView * lightProj; - // Anchor the snap on the WORLD ORIGIN (fixed point). The camera is a - // constant offset from the light frustum (lightPos follows camXZ), so - // projecting the camera yields the SAME texel coords every frame — - // constant delta, snap no-ops, world texels crawl while moving. The - // origin's projected texel position drifts as the frustum follows the - // camera; rounding it quantizes frustum motion to whole texels. - // Row-vector convention: (0,0,0,1) * M = row 3. - float clipX = lightVP_raw(3, 0); - float clipY = lightVP_raw(3, 1); - float clipW = lightVP_raw(3, 3); - if (clipW != 0.0f) { - float ndcX = clipX / clipW; - float ndcY = clipY / clipW; - float texX = ndcX * halfShadow; - float texY = ndcY * halfShadow; - float roundedX = std::round(texX); - float roundedY = std::round(texY); - float offsetNdcX = (roundedX - texX) / halfShadow; - float offsetNdcY = (roundedY - texY) / halfShadow; - lightProj(3, 0) += offsetNdcX; - lightProj(3, 1) += offsetNdcY; - } - - // LightVP = View * Projection (row-major convention) - lightVP = lightView * lightProj; + cascadeCount = std::clamp(shadowLight->GetShadowCascadeCount(), 1u, + RenderEngine::MAX_SHADOW_CASCADES); + renderer->SetShadowCascadeCount(cascadeCount); + BuildShadowCascades(*shadowLight, Camera::GetMainCameraPosition(), + cascadeCount, renderer->GetShadowMapResolution(), + cascadeVP, cascadeSplits); } - // DIAG --shadowfreeze: latch the first lightVP forever. If shadows still + // DIAG --shadowfreeze: latch the first cascades forever. If shadows still // shimmer with a frozen frustum, the cause is screen-space, not the // frustum-follow chain. { static const bool s_freeze = CommandLine::HasFlag("--shadowfreeze"); static bool s_latched = false; - static float s_frozenVP[16]; + static float s_frozenVP[RenderEngine::MAX_SHADOW_CASCADES][16]; if (s_freeze && shadowLight) { if (!s_latched) { - std::memcpy(s_frozenVP, &lightVP(0, 0), sizeof(s_frozenVP)); + std::memcpy(s_frozenVP, cascadeVP, sizeof(s_frozenVP)); s_latched = true; SLEAK_WARN("shadowfreeze: light frustum latched"); } else { - std::memcpy(&lightVP(0, 0), s_frozenVP, sizeof(s_frozenVP)); + std::memcpy(cascadeVP, s_frozenVP, sizeof(s_frozenVP)); } } } - // Set the light VP matrix on the renderer + // The last cascade doubles as the single map for older shaders. + if (cascadeCount > 0) { + std::memcpy(&lightVP(0, 0), cascadeVP[cascadeCount - 1], + sizeof(float) * 16); + } renderer->SetLightVP(&lightVP(0, 0)); + renderer->SetShadowCascades(&cascadeVP[0][0], cascadeCount); // Build shadow light UBO const auto& camPos = Camera::GetMainCameraPosition(); @@ -376,6 +391,11 @@ void LightManager::UpdateShadowData() { 1.0f / static_cast(renderer->GetShadowMapResolution()); ubo.LightSize = shadowLight ? shadowLight->GetLightSize() : 0.0f; + std::memcpy(ubo.CascadeVP, cascadeVP, sizeof(ubo.CascadeVP)); + std::memcpy(ubo.CascadeSplits, cascadeSplits, sizeof(ubo.CascadeSplits)); + ubo.CascadeCount = cascadeCount; + ubo.CascadeBlend = kCascadeBlend; + // Fog — distance gradient (horizon + zenith) and exponential height fog if (m_fogEnabled) { ubo.FogColor[0] = m_fogR;