From 153aa3b932a76b532caec3581dbd68740e6c9a24 Mon Sep 17 00:00:00 2001 From: Sascha Willems Date: Mon, 28 Apr 2025 21:03:37 +0200 Subject: [PATCH] Add slang shader for shadow mapping and shadow mapping cascades sample --- .../shadowmappingcascade.cpp | 14 +- shaders/slang/shadowmapping/offscreen.slang | 23 +++ shaders/slang/shadowmapping/quad.slang | 48 +++++ shaders/slang/shadowmapping/scene.slang | 116 ++++++++++++ .../shadowmappingcascade/debugshadowmap.slang | 29 +++ .../shadowmappingcascade/depthpass.slang | 46 +++++ .../slang/shadowmappingcascade/scene.slang | 168 ++++++++++++++++++ 7 files changed, 436 insertions(+), 8 deletions(-) create mode 100644 shaders/slang/shadowmapping/offscreen.slang create mode 100644 shaders/slang/shadowmapping/quad.slang create mode 100644 shaders/slang/shadowmapping/scene.slang create mode 100644 shaders/slang/shadowmappingcascade/debugshadowmap.slang create mode 100644 shaders/slang/shadowmappingcascade/depthpass.slang create mode 100644 shaders/slang/shadowmappingcascade/scene.slang diff --git a/examples/shadowmappingcascade/shadowmappingcascade.cpp b/examples/shadowmappingcascade/shadowmappingcascade.cpp index 9133a03c..55c6328b 100644 --- a/examples/shadowmappingcascade/shadowmappingcascade.cpp +++ b/examples/shadowmappingcascade/shadowmappingcascade.cpp @@ -1,10 +1,8 @@ /* Vulkan Example - Cascaded shadow mapping for directional light sources - Copyright by Sascha Willems - www.saschawillems.de + Copyright (c) 2016-2025 by Sascha Willems - www.saschawillems.de This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT) -*/ -/* This example implements projective cascaded shadow mapping. This technique splits up the camera frustum into multiple frustums with each getting its own full-res shadow map, implemented as a layered depth-only image. The shader then selects the proper shadow map layer depending on what split of the frustum the depth value @@ -175,7 +173,7 @@ public: vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSet, 0, nullptr); // Floor - vkCmdPushConstants(commandBuffer, pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(PushConstBlock), &pushConstBlock); + vkCmdPushConstants(commandBuffer, pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(PushConstBlock), &pushConstBlock); models.terrain.draw(commandBuffer, vkglTF::RenderFlags::BindImages, pipelineLayout); // Trees @@ -189,7 +187,7 @@ public: for (auto& position : positions) { pushConstBlock.position = glm::vec4(position, 0.0f); - vkCmdPushConstants(commandBuffer, pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(PushConstBlock), &pushConstBlock); + vkCmdPushConstants(commandBuffer, pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(PushConstBlock), &pushConstBlock); // This will also bind the texture images to set 1 models.tree.draw(commandBuffer, vkglTF::RenderFlags::BindImages, pipelineLayout); } @@ -413,7 +411,7 @@ public: vkCmdBindPipeline(drawCmdBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelines.debugShadowMap); PushConstBlock pushConstBlock = {}; pushConstBlock.cascadeIndex = displayDepthMapCascadeIndex; - vkCmdPushConstants(drawCmdBuffers[i], pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(PushConstBlock), &pushConstBlock); + vkCmdPushConstants(drawCmdBuffers[i], pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(PushConstBlock), &pushConstBlock); vkCmdDraw(drawCmdBuffers[i], 3, 1, 0, 0); } @@ -490,7 +488,7 @@ public: // Shared pipeline layout (scene and depth map debug display) { - VkPushConstantRange pushConstantRange = vks::initializers::pushConstantRange(VK_SHADER_STAGE_VERTEX_BIT, sizeof(PushConstBlock), 0); + VkPushConstantRange pushConstantRange = vks::initializers::pushConstantRange(VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, sizeof(PushConstBlock), 0); std::array setLayouts = { descriptorSetLayout, vkglTF::descriptorSetLayoutImage }; VkPipelineLayoutCreateInfo pipelineLayoutCreateInfo = vks::initializers::pipelineLayoutCreateInfo(setLayouts.data(), static_cast(setLayouts.size())); pipelineLayoutCreateInfo.pushConstantRangeCount = 1; @@ -500,7 +498,7 @@ public: // Depth pass pipeline layout { - VkPushConstantRange pushConstantRange = vks::initializers::pushConstantRange(VK_SHADER_STAGE_VERTEX_BIT, sizeof(PushConstBlock), 0); + VkPushConstantRange pushConstantRange = vks::initializers::pushConstantRange(VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, sizeof(PushConstBlock), 0); std::array setLayouts = { descriptorSetLayout, vkglTF::descriptorSetLayoutImage }; VkPipelineLayoutCreateInfo pipelineLayoutCreateInfo = vks::initializers::pipelineLayoutCreateInfo(setLayouts.data(), static_cast(setLayouts.size())); pipelineLayoutCreateInfo.pushConstantRangeCount = 1; diff --git a/shaders/slang/shadowmapping/offscreen.slang b/shaders/slang/shadowmapping/offscreen.slang new file mode 100644 index 00000000..decb80c3 --- /dev/null +++ b/shaders/slang/shadowmapping/offscreen.slang @@ -0,0 +1,23 @@ +/* Copyright (c) 2025, Sascha Willems + * + * SPDX-License-Identifier: MIT + * + */ + +struct UBO +{ + float4x4 depthMVP; +}; +ConstantBuffer ubo; + +[shader("vertex")] +float4 vertexMain(float3 Pos) : SV_POSITION +{ + return mul(ubo.depthMVP, float4(Pos, 1.0)); +} + +[shader("fragment")] +float4 fragmentMain() +{ + return float4(1.0, 0.0, 0.0, 1.0); +} \ No newline at end of file diff --git a/shaders/slang/shadowmapping/quad.slang b/shaders/slang/shadowmapping/quad.slang new file mode 100644 index 00000000..c6237b1d --- /dev/null +++ b/shaders/slang/shadowmapping/quad.slang @@ -0,0 +1,48 @@ +/* Copyright (c) 2025, Sascha Willems + * + * SPDX-License-Identifier: MIT + * + */ + +struct UBO +{ + float4x4 projection; + float4x4 view; + float4x4 model; + float4x4 lightSpace; + float4 lightPos; + float zNear; + float zFar; +}; +ConstantBuffer ubo; +Sampler2D samplerColor; + +float LinearizeDepth(float depth) +{ + float n = ubo.zNear; + float f = ubo.zFar; + float z = depth; + return (2.0 * n) / (f + n - z * (f - n)); +} + +struct VSOutput +{ + float4 Pos : SV_POSITION; + float2 UV; +}; + +[shader("vertex")] +VSOutput vertexMain(uint VertexIndex: SV_VertexID) +{ + VSOutput output; + output.UV = float2((VertexIndex << 1) & 2, VertexIndex & 2); + output.Pos = float4(output.UV * 2.0f - 1.0f, 0.0f, 1.0f); + return output; +} + +[shader("fragment")] +float4 fragmentMain(VSOutput input) +{ + float depth = samplerColor.Sample(input.UV).r; + return float4((1.0 - LinearizeDepth(depth)).xxx, 1.0); +} diff --git a/shaders/slang/shadowmapping/scene.slang b/shaders/slang/shadowmapping/scene.slang new file mode 100644 index 00000000..2f9395a6 --- /dev/null +++ b/shaders/slang/shadowmapping/scene.slang @@ -0,0 +1,116 @@ +/* Copyright (c) 2025, Sascha Willems + * + * SPDX-License-Identifier: MIT + * + */ + +struct VSInput +{ + float3 Pos; + float2 UV; + float3 Color; + float3 Normal; +}; + +struct VSOutput +{ + float4 Pos : SV_POSITION; + float3 Normal; + float3 Color; + float3 ViewVec; + float3 LightVec; + float4 ShadowCoord; +}; + +struct UBO +{ + float4x4 projection; + float4x4 view; + float4x4 model; + float4x4 lightSpace; + float4 lightPos; + float zNear; + float zFar; +}; +ConstantBuffer ubo; +Sampler2D shadowMapSampler; + +[SpecializationConstant] const int enablePCF = 0; + +#define ambient 0.1 + +float textureProj(float4 shadowCoord, float2 off) +{ + float shadow = 1.0; + if ( shadowCoord.z > -1.0 && shadowCoord.z < 1.0 ) + { + float dist = shadowMapSampler.Sample(shadowCoord.xy + off).r; + if ( shadowCoord.w > 0.0 && dist < shadowCoord.z ) + { + shadow = ambient; + } + } + return shadow; +} + +float filterPCF(float4 sc) +{ + int2 texDim; + shadowMapSampler.GetDimensions(texDim.x, texDim.y); + float scale = 1.5; + float dx = scale * 1.0 / float(texDim.x); + float dy = scale * 1.0 / float(texDim.y); + + float shadowFactor = 0.0; + int count = 0; + int range = 1; + + for (int x = -range; x <= range; x++) + { + for (int y = -range; y <= range; y++) + { + shadowFactor += textureProj(sc, float2(dx*x, dy*y)); + count++; + } + + } + return shadowFactor / count; +} + +static const float4x4 biasMat = float4x4( + 0.5, 0.0, 0.0, 0.5, + 0.0, 0.5, 0.0, 0.5, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0); + +[shader("vertex")] +VSOutput vertexMain(VSInput input) +{ + VSOutput output = (VSOutput)0; + output.Color = input.Color; + output.Normal = input.Normal; + + output.Pos = mul(ubo.projection, mul(ubo.view, mul(ubo.model, float4(input.Pos.xyz, 1.0)))); + + float4 pos = mul(ubo.model, float4(input.Pos, 1.0)); + output.Normal = mul((float3x3)ubo.model, input.Normal); + output.LightVec = normalize(ubo.lightPos.xyz - input.Pos); + output.ViewVec = -pos.xyz; + + output.ShadowCoord = mul(biasMat, mul(ubo.lightSpace, mul(ubo.model, float4(input.Pos, 1.0)))); + return output; +} + +[shader("fragment")] +float4 fragmentMain(VSOutput input) +{ + float shadow = (enablePCF == 1) ? filterPCF(input.ShadowCoord / input.ShadowCoord.w) : textureProj(input.ShadowCoord / input.ShadowCoord.w, float2(0.0, 0.0)); + + float3 N = normalize(input.Normal); + float3 L = normalize(input.LightVec); + float3 V = normalize(input.ViewVec); + float3 R = normalize(-reflect(L, N)); + float3 diffuse = max(dot(N, L), ambient) * input.Color; + + return float4(diffuse * shadow, 1.0); +} diff --git a/shaders/slang/shadowmappingcascade/debugshadowmap.slang b/shaders/slang/shadowmappingcascade/debugshadowmap.slang new file mode 100644 index 00000000..925b7d51 --- /dev/null +++ b/shaders/slang/shadowmappingcascade/debugshadowmap.slang @@ -0,0 +1,29 @@ +/* Copyright (c) 2025, Sascha Willems + * + * SPDX-License-Identifier: MIT + * + */ + +[[vk::binding(1,0)]] Sampler2DArray shadowMapSampler; + +struct VSOutput +{ + float4 Pos : SV_POSITION; + float2 UV; +}; + +[shader("vertex")] +VSOutput vertexMain(uint VertexIndex: SV_VertexID) +{ + VSOutput output; + output.UV = float2((VertexIndex << 1) & 2, VertexIndex & 2); + output.Pos = float4(output.UV * 2.0f - 1.0f, 0.0f, 1.0f); + return output; +} + +[shader("fragment")] +float4 fragmentMain(VSOutput input, uniform float4 meshPosition, uniform uint cascadeIndex) +{ + float depth = shadowMapSampler.Sample(float3(input.UV, float(cascadeIndex))).r; + return float4(depth.xxx, 1.0); +} \ No newline at end of file diff --git a/shaders/slang/shadowmappingcascade/depthpass.slang b/shaders/slang/shadowmappingcascade/depthpass.slang new file mode 100644 index 00000000..287fe6c8 --- /dev/null +++ b/shaders/slang/shadowmappingcascade/depthpass.slang @@ -0,0 +1,46 @@ +/* Copyright (c) 2025, Sascha Willems + * + * SPDX-License-Identifier: MIT + * + */ + +struct VSInput +{ + float3 Pos; + float2 UV; +}; + +struct VSOutput +{ + float4 Pos : SV_POSITION; + float2 UV; +}; + +// todo: pass via specialization constant +#define SHADOW_MAP_CASCADE_COUNT 4 + +struct UBO { + float4x4 cascadeViewProjMat[SHADOW_MAP_CASCADE_COUNT]; +}; +[[vk::binding(3, 0)]] ConstantBuffer ubo; + +[[vk::binding(0, 1)]] Sampler2D colorMapSampler : register(s0, space1); + +[shader("vertex")] +VSOutput vertexMain(VSInput input, uniform float4 meshPosition, uniform uint cascadeIndex) +{ + VSOutput output; + output.UV = input.UV; + float3 pos = input.Pos + meshPosition.xyz; + output.Pos = mul(ubo.cascadeViewProjMat[cascadeIndex], float4(pos, 1.0)); + return output; +} + +[shader("fragment")] +void fragmentMain(VSOutput input) +{ + float alpha = colorMapSampler.Sample(input.UV).a; + if (alpha < 0.5) { + clip(-1); + } +} \ No newline at end of file diff --git a/shaders/slang/shadowmappingcascade/scene.slang b/shaders/slang/shadowmappingcascade/scene.slang new file mode 100644 index 00000000..8e4bdd56 --- /dev/null +++ b/shaders/slang/shadowmappingcascade/scene.slang @@ -0,0 +1,168 @@ +/* Copyright (c) 2025, Sascha Willems + * + * SPDX-License-Identifier: MIT + * + */ + +#define SHADOW_MAP_CASCADE_COUNT 4 + +struct VSInput +{ + float3 Pos; + float2 UV; + float3 Color; + float3 Normal; +}; + +struct VSOutput +{ + float4 Pos : SV_POSITION; + float3 Normal; + float3 Color; + float3 ViewPos; + float3 WorldPos; + float2 UV; +}; + +[SpecializationConstant] const int enablePCF = 0; + +#define ambient 0.3 + +struct UBOScene { + float4x4 projection; + float4x4 view; + float4x4 model; +}; +ConstantBuffer uboScene; + +[[vk::binding(1, 0)]] Sampler2DArray shadowMapSampler; + +struct UBOCascades { + float4 cascadeSplits; + float4x4 inverseViewMat; + float3 lightDir; + float _pad; + int colorCascades; +}; +[[vk::binding(2, 0)]] ConstantBuffer uboCascades; + +struct CVPM { + float4x4 matrices[SHADOW_MAP_CASCADE_COUNT]; +}; +[[vk::binding(3, 0)]] ConstantBuffer cascadeViewProjMatrices; + +[[vk::binding(0, 1)]] Sampler2D colorMapSampler; + +static const float4x4 biasMat = float4x4( + 0.5, 0.0, 0.0, 0.5, + 0.0, 0.5, 0.0, 0.5, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0 +); + +float textureProj(float4 shadowCoord, float2 offset, uint cascadeIndex) +{ + float shadow = 1.0; + float bias = 0.005; + + if (shadowCoord.z > -1.0 && shadowCoord.z < 1.0) { + float dist = shadowMapSampler.Sample(float3(shadowCoord.xy + offset, cascadeIndex)).r; + if (shadowCoord.w > 0 && dist < shadowCoord.z - bias) { + shadow = ambient; + } + } + return shadow; +} + +float filterPCF(float4 sc, uint cascadeIndex) +{ + int3 texDim; + shadowMapSampler.GetDimensions(texDim.x, texDim.y, texDim.z); + float scale = 0.75; + float dx = scale * 1.0 / float(texDim.x); + float dy = scale * 1.0 / float(texDim.y); + + float shadowFactor = 0.0; + int count = 0; + int range = 1; + + for (int x = -range; x <= range; x++) { + for (int y = -range; y <= range; y++) { + shadowFactor += textureProj(sc, float2(dx*x, dy*y), cascadeIndex); + count++; + } + } + return shadowFactor / count; +} + +[shader("vertex")] +VSOutput vertexMain(VSInput input, uniform float4 meshPosition) +{ + VSOutput output; + output.Color = input.Color; + output.Normal = input.Normal; + output.UV = input.UV; + float3 pos = input.Pos + meshPosition.xyz; + output.WorldPos = pos; + output.ViewPos = mul(uboScene.view, float4(pos.xyz, 1.0)).xyz; + output.Pos = mul(uboScene.projection, mul(uboScene.view, mul(uboScene.model, float4(pos.xyz, 1.0)))); + return output; +} + +[shader("fragment")] +float4 fragmentMain(VSOutput input) +{ + float4 outFragColor; + float4 color = colorMapSampler.Sample(input.UV); + if (color.a < 0.5) { + clip(-1); + } + + // Get cascade index for the current fragment's view position + uint cascadeIndex = 0; + for (uint i = 0; i < SHADOW_MAP_CASCADE_COUNT - 1; ++i) { + if (input.ViewPos.z < uboCascades.cascadeSplits[i]) { + cascadeIndex = i + 1; + } + } + + // Depth compare for shadowing + float4 shadowCoord = mul(biasMat, mul(cascadeViewProjMatrices.matrices[cascadeIndex], float4(input.WorldPos, 1.0))); + + float shadow = 0; + if (enablePCF == 1) { + shadow = filterPCF(shadowCoord / shadowCoord.w, cascadeIndex); + } else { + shadow = textureProj(shadowCoord / shadowCoord.w, float2(0.0, 0.0), cascadeIndex); + } + + // Directional light + float3 N = normalize(input.Normal); + float3 L = normalize(-uboCascades.lightDir); + float3 H = normalize(L + input.ViewPos); + float diffuse = max(dot(N, L), ambient); + float3 lightColor = float3(1.0, 1.0, 1.0); + outFragColor.rgb = max(lightColor * (diffuse * color.rgb), float3(0.0, 0.0, 0.0)); + outFragColor.rgb *= shadow; + outFragColor.a = color.a; + + // Color cascades (if enabled) + if (uboCascades.colorCascades == 1) { + switch(cascadeIndex) { + case 0 : + outFragColor.rgb *= float3(1.0f, 0.25f, 0.25f); + break; + case 1 : + outFragColor.rgb *= float3(0.25f, 1.0f, 0.25f); + break; + case 2 : + outFragColor.rgb *= float3(0.25f, 0.25f, 1.0f); + break; + case 3 : + outFragColor.rgb *= float3(1.0f, 1.0f, 0.25f); + break; + } + } + + return outFragColor; +}