From e6e305dfa98ab7024c1e34c1178e8441973f578f Mon Sep 17 00:00:00 2001 From: emil28092005 Date: Thu, 18 Jun 2026 20:09:32 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20shadow=20mapping=20=E2=80=94=20depth-on?= =?UTF-8?q?ly=20render=20pass=20from=20light=20POV=20+=20PCF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - VulkanShadowMap.cs: 2048x2048 D32_SFLOAT image, sampler (clamp-to-border), separate shadow pipeline (depth-only, no color, depth bias enabled, CULL_NONE) - Shadow shaders: shadow.vert (lightViewProj * model * pos), shadow.frag (empty) - Main shaders updated: fragLightSpacePos output from vertex, PCF 3x3 in fragment - Push constants expanded to 160B: model(64) + lightPos(16) + lightColor(16) + lightViewProj(64) - 3 push constant ranges: Vertex(model), Fragment(light), Vertex|Fragment(lightViewProj) - Descriptor set: 2 bindings — UBO(vp) + CombinedImageSampler(shadowMap) - Shadow pass: before main pass, renders scene depth from light position - Image transitions: shadow map UNDEFINED→DEPTH→SHADER_READ_ONLY each frame - vkCmdSetDepthBias(1.25, 0, 1.75) for acne prevention - Light VP: CreateLookAt(lightPos, origin, up) * Perspective(60°, 1.0, 0.1, 60) - 0 validation errors on build --- .../Shaders/shadow.frag | 4 + .../Shaders/shadow.frag.spv | Bin 0 -> 180 bytes .../Shaders/shadow.vert | 14 + .../Shaders/shadow.vert.spv | Bin 0 -> 1328 bytes .../Shaders/triangle.frag | 40 ++- .../Shaders/triangle.frag.spv | Bin 8700 -> 11936 bytes .../Shaders/triangle.vert | 4 +- .../Shaders/triangle.vert.spv | Bin 2368 -> 2428 bytes src/Engine.Graphics.Vulkan/Vk.cs | 3 + .../VulkanFrameResources.cs | 35 +- src/Engine.Graphics.Vulkan/VulkanPipeline.cs | 24 +- src/Engine.Graphics.Vulkan/VulkanRenderer.cs | 104 +++++- src/Engine.Graphics.Vulkan/VulkanShadowMap.cs | 305 ++++++++++++++++++ 13 files changed, 518 insertions(+), 15 deletions(-) create mode 100644 src/Engine.Graphics.Vulkan/Shaders/shadow.frag create mode 100644 src/Engine.Graphics.Vulkan/Shaders/shadow.frag.spv create mode 100644 src/Engine.Graphics.Vulkan/Shaders/shadow.vert create mode 100644 src/Engine.Graphics.Vulkan/Shaders/shadow.vert.spv create mode 100644 src/Engine.Graphics.Vulkan/VulkanShadowMap.cs diff --git a/src/Engine.Graphics.Vulkan/Shaders/shadow.frag b/src/Engine.Graphics.Vulkan/Shaders/shadow.frag new file mode 100644 index 0000000..f0bcb49 --- /dev/null +++ b/src/Engine.Graphics.Vulkan/Shaders/shadow.frag @@ -0,0 +1,4 @@ +#version 450 + +void main() { +} diff --git a/src/Engine.Graphics.Vulkan/Shaders/shadow.frag.spv b/src/Engine.Graphics.Vulkan/Shaders/shadow.frag.spv new file mode 100644 index 0000000000000000000000000000000000000000..757dd51bdd669a9f3efd078466cb07f5b583bc95 GIT binary patch literal 180 zcmZQ(Qf6mhU}WHC;9y{5fB-=TCZGrdP>c;syZZ$D=oOcw=$V)rfK>1?FoP9>)bKN~ zGOz$?5YA1^%mYaX0Odh^pus>EGte{!hC@KbAeG4Kgn=?3IYqE41~A(Us0t(oQU?Mc Rn|=UASb_9kpld9E7y!nA3km=L literal 0 HcmV?d00001 diff --git a/src/Engine.Graphics.Vulkan/Shaders/shadow.vert b/src/Engine.Graphics.Vulkan/Shaders/shadow.vert new file mode 100644 index 0000000..1f7ca77 --- /dev/null +++ b/src/Engine.Graphics.Vulkan/Shaders/shadow.vert @@ -0,0 +1,14 @@ +#version 450 + +layout(location = 0) in vec3 inPosition; + +layout(push_constant) uniform PC { + mat4 model; + vec4 lightPos; + vec4 lightColor; + mat4 lightViewProj; +} pc; + +void main() { + gl_Position = pc.lightViewProj * pc.model * vec4(inPosition, 1.0); +} diff --git a/src/Engine.Graphics.Vulkan/Shaders/shadow.vert.spv b/src/Engine.Graphics.Vulkan/Shaders/shadow.vert.spv new file mode 100644 index 0000000000000000000000000000000000000000..15e1809cd1c7cdc40f99314eda2571b5fba5ffc2 GIT binary patch literal 1328 zcmZ9K%Wl(95QYy;-4+ThrSt-n;u02R~B*aZY3X3j+Ds@Y%)+$DBTqQ;zR=ff{ z5>LejiSM(0qKFesX6B#YGyfU8Gt-?9;X+sp3*ljC&Ptd831D-z@9*vJ?T)LH-6u~U z%UBAXnkZ*E@Hl4{J3GpU##gZ0Scl&X{x`$~#xy6P_ae2^;^QD29b}^_`$h`d;KtU< zabD%+5OlugJF~{~VYQ!s&*a6Lv#9lo{QO-$u8xMs*`%goYhQ|DQcKXMR*&%te$caD zQpe(FtIxVCU@2<{`}ZDsdtqR zCsz%9>h8v#@2MMmqK}wb#PkvK88PQZ%vj9*+gn{t@7c*c6EOMKJigVd*fiF)z=@c1 z#okO|_7xk{SkvG6)BVl=?=R+ahgct_em~ooXBD;f-9fuN<8`7re2Q(rx1YIpT;qQM zpJJXzZGMqh&0FM`@bbmBiQG@@VNFZnUB)}p`inOAn-O!)5m2iC%(F@E4(+Qh z=eXa9#T_{38n{}lJICB_L@f5bPV8LyYi-S5-v1Wf`^5Sdv6{r~XYNj;PPuom5PrP2 z-+dyVRns%xmOECnNBj?n@9-`E&R)mVr>2)M8yyWEIh!rVeMHU;V)0+}@tK?01|}{? zy}oy~et@IS6pPZCUO}tANOELM@z4K4{TDyliUyhpH!2`_qtL9hS M++5yYHuoI+2TOBOQ2+n{ literal 0 HcmV?d00001 diff --git a/src/Engine.Graphics.Vulkan/Shaders/triangle.frag b/src/Engine.Graphics.Vulkan/Shaders/triangle.frag index 164e00c..a604fd7 100644 --- a/src/Engine.Graphics.Vulkan/Shaders/triangle.frag +++ b/src/Engine.Graphics.Vulkan/Shaders/triangle.frag @@ -3,6 +3,7 @@ layout(location = 0) in vec3 fragWorldPos; layout(location = 1) in vec3 fragNormal; layout(location = 2) in vec3 fragAlbedo; +layout(location = 3) in vec4 fragLightSpacePos; layout(location = 0) out vec4 outColor; @@ -10,10 +11,13 @@ layout(set = 0, binding = 0) uniform CameraUBO { mat4 vp; }; +layout(set = 0, binding = 1) uniform sampler2D shadowMap; + layout(push_constant) uniform PC { mat4 model; vec4 lightPos; vec4 lightColor; + mat4 lightViewProj; } pc; const vec3 AMBIENT = vec3(0.01, 0.01, 0.02); @@ -60,6 +64,33 @@ vec3 acesTonemap(vec3 color) return clamp((color * (a * color + b)) / (color * (c * color + d) + e), 0.0, 1.0); } +float calcShadow(vec4 lightSpacePos, vec3 N, vec3 L) +{ + vec3 projCoords = lightSpacePos.xyz / lightSpacePos.w; + projCoords = projCoords * 0.5 + 0.5; + + if (projCoords.x < 0.0 || projCoords.x > 1.0) return 1.0; + if (projCoords.y < 0.0 || projCoords.y > 1.0) return 1.0; + if (projCoords.z > 1.0) return 1.0; + + float bias = max(0.005 * (1.0 - dot(N, L)), 0.0005); + float currentDepth = projCoords.z; + + // PCF 3x3 + float shadow = 0.0; + vec2 texelSize = 1.0 / vec2(2048.0, 2048.0); + for (int x = -1; x <= 1; x++) + { + for (int y = -1; y <= 1; y++) + { + float closestDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r; + shadow += currentDepth - bias > closestDepth ? 0.0 : 1.0; + } + } + shadow /= 9.0; + return shadow; +} + void main() { vec3 N = normalize(fragNormal); @@ -68,8 +99,6 @@ void main() float roughness = 0.5; float metallic = 0.1; - vec3 color = AMBIENT * albedo; - vec3 lightPos = pc.lightPos.xyz; float lightIntensity = pc.lightPos.w; vec3 lightColor = pc.lightColor.xyz; @@ -82,6 +111,9 @@ void main() float attenuation = pow(clamp(1.0 - dist / max(lightRange, 0.001), 0.0, 1.0), 2.0); vec3 radiance = lightColor * lightIntensity * attenuation; + // Shadow + float shadow = calcShadow(fragLightSpacePos, N, L); + vec3 H = normalize(V + L); vec3 F0 = mix(vec3(0.04), albedo, metallic); @@ -97,7 +129,9 @@ void main() vec3 kD = (vec3(1.0) - kS) * (1.0 - metallic); float NdotL = max(dot(N, L), 0.0); - color += (kD * albedo / PI + specular) * radiance * NdotL; + vec3 lighting = (kD * albedo / PI + specular) * radiance * NdotL * shadow; + + vec3 color = AMBIENT * albedo + lighting; color = acesTonemap(color); color = pow(color, vec3(1.0 / 2.2)); diff --git a/src/Engine.Graphics.Vulkan/Shaders/triangle.frag.spv b/src/Engine.Graphics.Vulkan/Shaders/triangle.frag.spv index bd86092540bc3a183b6a1c22dd9bc320ef274ed4..c15a826b869f5df3a735068214bed1df5c39ede4 100644 GIT binary patch literal 11936 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 diff --git a/src/Engine.Graphics.Vulkan/Shaders/triangle.vert b/src/Engine.Graphics.Vulkan/Shaders/triangle.vert index 0182991..64e9d7a 100644 --- a/src/Engine.Graphics.Vulkan/Shaders/triangle.vert +++ b/src/Engine.Graphics.Vulkan/Shaders/triangle.vert @@ -7,6 +7,7 @@ layout(location = 2) in vec3 inNormal; layout(location = 0) out vec3 fragWorldPos; layout(location = 1) out vec3 fragNormal; layout(location = 2) out vec3 fragAlbedo; +layout(location = 3) out vec4 fragLightSpacePos; layout(set = 0, binding = 0) uniform CameraUBO { mat4 vp; @@ -16,13 +17,14 @@ layout(push_constant) uniform PC { mat4 model; vec4 lightPos; vec4 lightColor; + mat4 lightViewProj; } pc; void main() { vec4 worldPos = pc.model * vec4(inPosition, 1.0); - gl_Position = pc.model * vec4(inPosition, 1.0); gl_Position = vp * worldPos; fragWorldPos = worldPos.xyz; fragNormal = mat3(pc.model) * inNormal; fragAlbedo = inColor; + fragLightSpacePos = pc.lightViewProj * worldPos; } diff --git a/src/Engine.Graphics.Vulkan/Shaders/triangle.vert.spv b/src/Engine.Graphics.Vulkan/Shaders/triangle.vert.spv index f3fc4bdda7b07fc7a7d43dfe786c0d60fe82629a..a1803be692600d872dfca30971cd763b1cc40a12 100644 GIT binary patch literal 2428 zcmZ9M+foxj5QYcB#xtm>;0c%D`G9yrJRl)K;eygwlzOQVR$ObzQXz`vm9O9{`BdIm z<^Ro2!^)&O)BX2h{^_2i(m%1!xjuKu?RQJASjSwy5GSna_r>Lv<;kty+T{HQQ$!59 ziVq}a$Q|+efW9}{X$Rw^q$Zh^+>tz$%t`8!hNPlTzxLlM5V-+;c5@>k2V`&O-E6Iu zZ%GwW5f|rLO(#Z?*TFaPwItJC)dxR(vUL4(FXS1{SsriZS>7dnpO5Dj9)FuA+pTW? zMK)(~qs>+H$loRHs9@SlbKNddb}avT_P&*L-zME&@?ECq0M5c^37i!RgPX_GPH!ds zk;wEsV(svgubHKr3+Yy`-C0d`d~n3~HOqEX$!6xe_ZjfB#)dQb%o!UV*vzU}wa1b^$&FwS zNoSGIniWp`BcC>SOa3tRAr=66b!-ao%}pF{T#hVOw9%$-W?)95C(` zYqwtDLpXO_d%>YN7rPd)F?lAWTMsTur^jG$#Ae=L)VdsWi@zb8+MJ93RmPqoiH!B z&kJRK$0W?0xy~wvdmfj7W5e~7gt@&|4*eeun6@44bilZW#hnQloLV}SbI(f1CmuiZ z|E0S4&Ph0%m}g=SS1`CThbt0hQ|5X#;BckJH3@lE^SX3mz^Oz3Zc1tr>TnJ=aaQBD zbZWd($GHEL1PozuNA5%}v&L`zyeoZgKko?xr!R1PAOU098Pn3i5tf}{IS+-A18zAp z(!mkd-ke9m$bqZnJeCfQuq>yg$?`UzNZ>+kV(ksgO6Q%@*J16?B*cQ#2i`cC#m`Hx sd#vLB|FEq`3)1Nk@0NJp+;a)I&ESRf@*EdK3^?aCw1fTC4Q3?&0R6<8OaK4? literal 2368 zcmZXU+foxj5QYbm4R`=V2OLF?_)c#A!p3Cd6o#zQTCHrgC3~QC> zNUD&WcsN%nIkE9#9lRdZ{7`#AfB4}N1_z&7X`bQKit$nuM$P2B9$|<3=w8eacBC+g5y2yrE1-8R^P1eN-wBuV5~2P$(`;eX3U?ye{5C{-u=@@ zFP;VXtEgG8s(GBvY=VX%3C665TMIwrO)RG~;a*$v(SMdF+0w7sdRXq< z32ag}%i`!8*lGsjT(F9ytn+zKV-gm%i`tXgUaW&#P0q#7x%Ms$BL|G08-@yd^{IyIFrvzu;GEtJhC?5k;M-vinwuc z=Pu|&(hAy(l40e;7n?g4_1mo-3>lGqI@R%yrSmh47;^Z^`8Mg*VruuK*r4oQ=|xG0 zWGc1$q_fE99gxqta2n7KCphE3pq*TB&c|l%b{?_A66b!bQ16PgBBmDgu&pPfvaiY} z2W(7QQM>g4A41)6?FomZE;h3=UQ?+~?wC;K%^k>{)=s~{;Ec`e!SK(dy2a1S=3KiY zHg5rpd~E(-Vl$T|?Zld&p71t0Bo@!x$ohF3_~$i8cra5IF~5cImdWJ}Fc0pvCY1T~ zOPDEhTT%?X&r86u;W8v)F0YkCe@9YG+c9=A#o%Famr@K)6;-9~WeNGj<7d7AsdWkylzSd%W}PyVp*=Yg^^D@{w&u!vZ+lB z{CG2WB@+^Q34d(j@bfMnNO&vRUO!B6_|t<&67Fq1nUziqxYe1Ho~?sToSm^Coilbd zapw0#0*0`-H}`}iZxO%scv1Sv9zPQXPXFMzECFL#jTPzO2+L|%&Z;nSz%6G@`f4iL z{>xbxMh;vprz9O5VOgDuUMAnph6FC0O{~3(=h8PN^p&2JCB%Z$2fjfti+>?~JN5Cs KVgFIJ1<7BD4Uh-` diff --git a/src/Engine.Graphics.Vulkan/Vk.cs b/src/Engine.Graphics.Vulkan/Vk.cs index 3e893a1..2882df2 100644 --- a/src/Engine.Graphics.Vulkan/Vk.cs +++ b/src/Engine.Graphics.Vulkan/Vk.cs @@ -64,6 +64,7 @@ internal static unsafe class Vk public delegate void VkCmdDraw(VkCommandBuffer commandBuffer, uint vertexCount, uint instanceCount, uint firstVertex, uint firstInstance); public delegate void VkCmdDrawIndexed(VkCommandBuffer commandBuffer, uint indexCount, uint instanceCount, uint firstIndex, int vertexOffset, uint firstInstance); public delegate void VkCmdBindIndexBuffer(VkCommandBuffer commandBuffer, VkBuffer buffer, ulong offset, int indexType); + public delegate void VkCmdSetDepthBias(VkCommandBuffer commandBuffer, float depthBiasConstantFactor, float depthBiasClamp, float depthBiasSlopeFactor); public delegate void VkCmdBeginRendering(VkCommandBuffer commandBuffer, VkRenderingInfo* pRenderingInfo); public delegate void VkCmdEndRendering(VkCommandBuffer commandBuffer); public delegate void VkCmdPipelineBarrier2(VkCommandBuffer commandBuffer, VkDependencyInfo* pDependencyInfo); @@ -148,6 +149,7 @@ internal static unsafe class Vk public static VkCmdDraw vkCmdDraw; public static VkCmdDrawIndexed vkCmdDrawIndexed; public static VkCmdBindIndexBuffer vkCmdBindIndexBuffer; + public static VkCmdSetDepthBias vkCmdSetDepthBias; public static VkCmdBeginRendering vkCmdBeginRendering; public static VkCmdEndRendering vkCmdEndRendering; public static VkCmdPipelineBarrier2 vkCmdPipelineBarrier2; @@ -239,6 +241,7 @@ internal static unsafe class Vk vkCmdDraw = LoadDev(p, "vkCmdDraw"); vkCmdDrawIndexed = LoadDev(p, "vkCmdDrawIndexed"); vkCmdBindIndexBuffer = LoadDev(p, "vkCmdBindIndexBuffer"); + vkCmdSetDepthBias = LoadDev(p, "vkCmdSetDepthBias"); vkCmdBeginRendering = LoadDev(p, "vkCmdBeginRendering"); vkCmdEndRendering = LoadDev(p, "vkCmdEndRendering"); vkCmdPipelineBarrier2 = LoadDev(p, "vkCmdPipelineBarrier2"); diff --git a/src/Engine.Graphics.Vulkan/VulkanFrameResources.cs b/src/Engine.Graphics.Vulkan/VulkanFrameResources.cs index 229f830..698f58b 100644 --- a/src/Engine.Graphics.Vulkan/VulkanFrameResources.cs +++ b/src/Engine.Graphics.Vulkan/VulkanFrameResources.cs @@ -140,19 +140,25 @@ internal sealed unsafe class VulkanFrameResources : IDisposable private void CreateDescriptorPoolAndSets(VkDescriptorSetLayout layout) { - var poolSize = new VkDescriptorPoolSize + var poolSizes = stackalloc VkDescriptorPoolSize[2]; + poolSizes[0] = new VkDescriptorPoolSize { type = VkDescriptorType.UniformBuffer, descriptorCount = MaxFramesInFlight, }; + poolSizes[1] = new VkDescriptorPoolSize + { + type = VkDescriptorType.CombinedImageSampler, + descriptorCount = MaxFramesInFlight, + }; var poolInfo = new VkDescriptorPoolCreateInfo { sType = VkStructureType.DescriptorPoolCreateInfo, flags = VkDescriptorPoolCreateFlags.FreeDescriptorSet, maxSets = MaxFramesInFlight, - poolSizeCount = 1, - pPoolSizes = &poolSize, + poolSizeCount = 2, + pPoolSizes = poolSizes, }; var descPool = VkDescriptorPool.Null; @@ -204,6 +210,29 @@ internal sealed unsafe class VulkanFrameResources : IDisposable } } + public void UpdateShadowDescriptor(int frameIndex, VkSampler sampler, VkImageView shadowView) + { + var imageInfo = new VkDescriptorImageInfo + { + sampler = sampler, + imageView = shadowView, + imageLayout = VkImageLayout.ShaderReadOnlyOptimal, + }; + + var write = new VkWriteDescriptorSet + { + sType = VkStructureType.WriteDescriptorSet, + dstSet = DescriptorSets[frameIndex], + dstBinding = 1, + dstArrayElement = 0, + descriptorCount = 1, + descriptorType = VkDescriptorType.CombinedImageSampler, + pImageInfo = (nint)(&imageInfo), + }; + + Vk.vkUpdateDescriptorSets(_device, 1, &write, 0, 0); + } + public void UpdateUbo(int frameIndex, void* data, ulong size) { void* pData = null; diff --git a/src/Engine.Graphics.Vulkan/VulkanPipeline.cs b/src/Engine.Graphics.Vulkan/VulkanPipeline.cs index 6cfe5af..5de566b 100644 --- a/src/Engine.Graphics.Vulkan/VulkanPipeline.cs +++ b/src/Engine.Graphics.Vulkan/VulkanPipeline.cs @@ -150,7 +150,7 @@ internal sealed unsafe class VulkanPipeline : IDisposable pDynamicStates = dynamicStates, }; - var pushConstantRanges = stackalloc VkPushConstantRange[2]; + var pushConstantRanges = stackalloc VkPushConstantRange[3]; pushConstantRanges[0] = new VkPushConstantRange { stageFlags = VkShaderStageFlags.Vertex, @@ -163,6 +163,12 @@ internal sealed unsafe class VulkanPipeline : IDisposable offset = 64, size = 32, }; + pushConstantRanges[2] = new VkPushConstantRange + { + stageFlags = VkShaderStageFlags.Vertex | VkShaderStageFlags.Fragment, + offset = 96, + size = 64, + }; var descLayout = DescriptorSetLayout; var layoutInfo = new VkPipelineLayoutCreateInfo @@ -170,7 +176,7 @@ internal sealed unsafe class VulkanPipeline : IDisposable sType = VkStructureType.PipelineLayoutCreateInfo, setLayoutCount = 1, pSetLayouts = &descLayout, - pushConstantRangeCount = 2, + pushConstantRangeCount = 3, pPushConstantRanges = (nint)pushConstantRanges, }; @@ -221,19 +227,27 @@ internal sealed unsafe class VulkanPipeline : IDisposable private void CreateDescriptorSetLayout() { - var binding = new VkDescriptorSetLayoutBinding + var bindings = stackalloc VkDescriptorSetLayoutBinding[2]; + bindings[0] = new VkDescriptorSetLayoutBinding { binding = 0, descriptorType = VkDescriptorType.UniformBuffer, descriptorCount = 1, stageFlags = VkShaderStageFlags.Vertex, }; + bindings[1] = new VkDescriptorSetLayoutBinding + { + binding = 1, + descriptorType = VkDescriptorType.CombinedImageSampler, + descriptorCount = 1, + stageFlags = VkShaderStageFlags.Fragment, + }; var info = new VkDescriptorSetLayoutCreateInfo { sType = VkStructureType.DescriptorSetLayoutCreateInfo, - bindingCount = 1, - pBindings = &binding, + bindingCount = 2, + pBindings = bindings, }; var descLayout = VkDescriptorSetLayout.Null; diff --git a/src/Engine.Graphics.Vulkan/VulkanRenderer.cs b/src/Engine.Graphics.Vulkan/VulkanRenderer.cs index 659bee2..cffad2f 100644 --- a/src/Engine.Graphics.Vulkan/VulkanRenderer.cs +++ b/src/Engine.Graphics.Vulkan/VulkanRenderer.cs @@ -13,6 +13,7 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen private readonly VulkanSwapchain _swapchain; private readonly VulkanPipeline _pipeline; private readonly VulkanFrameResources _frameResources; + private readonly VulkanShadowMap _shadowMap; internal readonly VulkanImGui? _imGui; private readonly Dictionary _meshCache = new(); @@ -39,6 +40,11 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen _frameResources = new VulkanFrameResources(ctx.Device, ctx.GraphicsQueueFamilyIndex, swapchain.ImageCount, ctx, _pipeline.DescriptorSetLayout); + _shadowMap = new VulkanShadowMap(ctx.Device, ctx, _pipeline.DescriptorSetLayout); + + for (int i = 0; i < VulkanFrameResources.MaxFramesInFlight; i++) + _frameResources.UpdateShadowDescriptor(i, _shadowMap.ShadowSampler, _shadowMap.DepthImageView); + _imGui = new VulkanImGui(ctx, _frameResources.CommandPool, swapchain.Format, swapchain.DepthFormat); } @@ -105,6 +111,13 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen var vpCopy = vp; System.Buffer.MemoryCopy(&vpCopy, uboData, 64, 64); + // Compute light view-projection matrix for shadow mapping + var lightPosVec = new Vector3(lightPos.X, lightPos.Y, lightPos.Z); + var lightView = Matrix4x4.CreateLookAt(lightPosVec, Vector3.Zero, Vector3.UnitY); + var lightProj = Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 3f, 1.0f, 0.1f, 60f); + lightProj.M22 *= -1; + var lightViewProj = lightView * lightProj; + var drawCalls = new List<(VkBuffer vertexBuf, VkBuffer indexBuf, uint indexCount, Matrix4x4 model)>(); world.Each((Entity e, ref Transform t, ref Mesh m) => @@ -123,10 +136,10 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen drawCalls.Add((entry.vb.Buffer, entry.ib.Buffer, entry.indexCount, t.GetMatrix())); }); - Render(vp, drawCalls, uboData, lightPos, lightColor); + Render(vp, drawCalls, uboData, lightPos, lightColor, lightViewProj); } - private void Render(Matrix4x4 vp, List<(VkBuffer vertexBuf, VkBuffer indexBuf, uint indexCount, Matrix4x4 model)> drawCalls, byte* uboData, Vector4 lightPos, Vector4 lightColor) + private void Render(Matrix4x4 vp, List<(VkBuffer vertexBuf, VkBuffer indexBuf, uint indexCount, Matrix4x4 model)> drawCalls, byte* uboData, Vector4 lightPos, Vector4 lightColor, Matrix4x4 lightViewProj) { _frameResources.WaitFrame(_frameIndex); @@ -138,7 +151,7 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen { _swapchain.Recreate(_ctx.SurfaceExtent.Width == 0 ? 1280 : (int)_ctx.SurfaceExtent.Width, _ctx.SurfaceExtent.Height == 0 ? 720 : (int)_ctx.SurfaceExtent.Height); - Render(vp, drawCalls, uboData, lightPos, lightColor); + Render(vp, drawCalls, uboData, lightPos, lightColor, lightViewProj); return; } @@ -159,6 +172,86 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen }; Vk.vkBeginCommandBuffer(cmd, &beginInfo); + // === SHADOW PASS === + TransitionImageLayoutDepth(cmd, _shadowMap.DepthImage, + VkImageLayout.Undefined, VkImageLayout.DepthStencilAttachmentOptimal, + 0, 0, 0x100, 0x200); + + var shadowDepthClear = new VkClearValue + { + DepthStencil = new VkClearDepthStencilValue { Depth = 1.0f, Stencil = 0 }, + }; + + var shadowDepthAttachment = new VkRenderingAttachmentInfo + { + sType = VkStructureType.RenderingAttachmentInfo, + imageView = _shadowMap.DepthImageView, + imageLayout = VkImageLayout.DepthStencilAttachmentOptimal, + loadOp = VkAttachmentLoadOp.Clear, + storeOp = VkAttachmentStoreOp.Store, + clearValue = shadowDepthClear, + }; + + var shadowRenderingInfo = new VkRenderingInfo + { + sType = VkStructureType.RenderingInfo, + renderArea = new VkRect2D + { + Offset = new VkOffset2D { X = 0, Y = 0 }, + Extent = new VkExtent2D { Width = VulkanShadowMap.ShadowMapSize, Height = VulkanShadowMap.ShadowMapSize }, + }, + layerCount = 1, + colorAttachmentCount = 0, + pColorAttachments = null, + pDepthAttachment = &shadowDepthAttachment, + }; + + Vk.vkCmdBeginRendering(cmd, &shadowRenderingInfo); + + Vk.vkCmdBindPipeline(cmd, VkPipelineBindPoint.Graphics, _shadowMap.Pipeline); + + var shadowViewport = new VkViewport + { + X = 0, Y = 0, + Width = VulkanShadowMap.ShadowMapSize, + Height = VulkanShadowMap.ShadowMapSize, + MinDepth = 0, MaxDepth = 1, + }; + Vk.vkCmdSetViewport(cmd, 0, 1, &shadowViewport); + + var shadowScissor = new VkRect2D + { + Offset = new VkOffset2D { X = 0, Y = 0 }, + Extent = new VkExtent2D { Width = VulkanShadowMap.ShadowMapSize, Height = VulkanShadowMap.ShadowMapSize }, + }; + Vk.vkCmdSetScissor(cmd, 0, 1, &shadowScissor); + + Vk.vkCmdSetDepthBias(cmd, 1.25f, 0.0f, 1.75f); + + foreach (var dc in drawCalls) + { + var vertexBuf = dc.vertexBuf; + ulong offset = 0; + Vk.vkCmdBindVertexBuffers(cmd, 0, 1, &vertexBuf, &offset); + Vk.vkCmdBindIndexBuffer(cmd, dc.indexBuf, 0, 1); + + var model = dc.model; + Vk.vkCmdPushConstants(cmd, _shadowMap.PipelineLayout, VkShaderStageFlags.Vertex, 0, 64, &model); + + var lvpCopy = lightViewProj; + Vk.vkCmdPushConstants(cmd, _shadowMap.PipelineLayout, VkShaderStageFlags.Vertex | VkShaderStageFlags.Fragment, 96, 64, &lvpCopy); + + Vk.vkCmdDrawIndexed(cmd, dc.indexCount, 1, 0, 0, 0); + } + + Vk.vkCmdEndRendering(cmd); + + // Transition shadow map: depth attachment → shader read + TransitionImageLayoutDepth(cmd, _shadowMap.DepthImage, + VkImageLayout.DepthStencilAttachmentOptimal, VkImageLayout.ShaderReadOnlyOptimal, + 0x100, 0x200, 0x8, 0x20); + + // === MAIN PASS === TransitionImageLayout(cmd, _swapchain.Images[imageIndex], VkImageLayout.Undefined, VkImageLayout.ColorAttachmentOptimal, 0, 0, @@ -253,6 +346,10 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen var lcCopy = lightColor; Vk.vkCmdPushConstants(cmd, _pipeline.PipelineLayout, VkShaderStageFlags.Fragment, 80, 16, &lcCopy); + // Light view-proj at offset 96 (vertex + fragment) + var lvpCopy = lightViewProj; + Vk.vkCmdPushConstants(cmd, _pipeline.PipelineLayout, VkShaderStageFlags.Vertex | VkShaderStageFlags.Fragment, 96, 64, &lvpCopy); + Vk.vkCmdDrawIndexed(cmd, dc.indexCount, 1, 0, 0, 0); } @@ -442,6 +539,7 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen } _meshCache.Clear(); + _shadowMap?.Dispose(); _imGui?.Dispose(); _frameResources?.Dispose(); _pipeline?.Dispose(); diff --git a/src/Engine.Graphics.Vulkan/VulkanShadowMap.cs b/src/Engine.Graphics.Vulkan/VulkanShadowMap.cs new file mode 100644 index 0000000..a87c40e --- /dev/null +++ b/src/Engine.Graphics.Vulkan/VulkanShadowMap.cs @@ -0,0 +1,305 @@ +using System.Runtime.InteropServices; + +namespace Engine.Graphics.Vulkan; + +internal sealed unsafe class VulkanShadowMap : IDisposable +{ + public const uint ShadowMapSize = 2048; + + public VkImage DepthImage; + public VkDeviceMemory DepthImageMemory; + public VkImageView DepthImageView; + public VkSampler ShadowSampler; + public VkPipeline Pipeline; + public VkPipelineLayout PipelineLayout; + public VkShaderModule VertModule; + + private readonly VkDevice _device; + private readonly VulkanContext _ctx; + private bool _disposed; + + public VulkanShadowMap(VkDevice device, VulkanContext ctx, VkDescriptorSetLayout descLayout) + { + _device = device; + _ctx = ctx; + + CreateShadowImage(); + CreateShadowSampler(); + CreateShadowPipeline(descLayout); + + Console.WriteLine("[Vulkan] Shadow map created (2048x2048 D32_SFLOAT)"); + } + + private void CreateShadowImage() + { + var imageInfo = new VkImageCreateInfo + { + sType = VkStructureType.ImageCreateInfo, + imageType = VkImageType.Type2D, + format = VkFormat.D32Sfloat, + extent = new VkExtent3D { Width = ShadowMapSize, Height = ShadowMapSize, Depth = 1 }, + mipLevels = 1, + arrayLayers = 1, + samples = VkSampleCountFlags.Count1, + tiling = VkImageTiling.Optimal, + usage = VkImageUsageFlags.DepthStencilAttachment | VkImageUsageFlags.Sampled, + sharingMode = VkSharingMode.Exclusive, + initialLayout = VkImageLayout.Undefined, + }; + + var img = VkImage.Null; + Vk.vkCreateImage(_device, &imageInfo, 0, &img); + DepthImage = img; + + var reqs = new VkMemoryRequirements(); + Vk.vkGetImageMemoryRequirements(_device, DepthImage, &reqs); + var memTypeIndex = _ctx.FindMemoryType(reqs.memoryTypeBits, VkMemoryPropertyFlags.DeviceLocal); + + var allocInfo = new VkMemoryAllocateInfo + { + sType = VkStructureType.MemoryAllocateInfo, + allocationSize = reqs.size, + memoryTypeIndex = memTypeIndex, + }; + + var mem = VkDeviceMemory.Null; + Vk.vkAllocateMemory(_device, &allocInfo, 0, &mem); + DepthImageMemory = mem; + Vk.vkBindImageMemory(_device, DepthImage, DepthImageMemory, 0); + + var viewInfo = new VkImageViewCreateInfo + { + sType = VkStructureType.ImageViewCreateInfo, + image = DepthImage, + viewType = VkImageViewType.Type2D, + format = VkFormat.D32Sfloat, + components = new VkComponentMapping { R = VkComponentSwizzle.Identity, G = VkComponentSwizzle.Identity, B = VkComponentSwizzle.Identity, A = VkComponentSwizzle.Identity }, + subresourceRange = new VkImageSubresourceRange { AspectMask = VkImageAspectFlags.Depth, BaseMipLevel = 0, LevelCount = 1, BaseArrayLayer = 0, LayerCount = 1 }, + }; + + var view = VkImageView.Null; + Vk.vkCreateImageView(_device, &viewInfo, 0, &view); + DepthImageView = view; + } + + private void CreateShadowSampler() + { + var samplerInfo = new VkSamplerCreateInfo + { + sType = VkStructureType.SamplerCreateInfo, + magFilter = VkFilter.Linear, + minFilter = VkFilter.Linear, + mipmapMode = VkSamplerMipmapMode.Linear, + addressModeU = VkSamplerAddressMode.ClampToBorder, + addressModeV = VkSamplerAddressMode.ClampToBorder, + addressModeW = VkSamplerAddressMode.ClampToBorder, + minLod = 0, + maxLod = 1, + borderColor = 1, // VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE + }; + + var samp = VkSampler.Null; + Vk.vkCreateSampler(_device, &samplerInfo, 0, &samp); + ShadowSampler = samp; + } + + private void CreateShadowPipeline(VkDescriptorSetLayout descLayout) + { + var vertSpv = LoadShader("Shaders/shadow.vert.spv"); + var fragSpv = LoadShader("Shaders/shadow.frag.spv"); + VertModule = CreateShaderModule(vertSpv); + var fragModule = CreateShaderModule(fragSpv); + + fixed (byte* pName = "main\0"u8) + { + var stages = stackalloc VkPipelineShaderStageCreateInfo[2]; + stages[0] = new VkPipelineShaderStageCreateInfo + { + sType = VkStructureType.PipelineShaderStageCreateInfo, + stage = VkShaderStageFlags.Vertex, + module = VertModule, + pName = pName, + }; + stages[1] = new VkPipelineShaderStageCreateInfo + { + sType = VkStructureType.PipelineShaderStageCreateInfo, + stage = VkShaderStageFlags.Fragment, + module = fragModule, + pName = pName, + }; + + var bindings = stackalloc VkVertexInputBindingDescription[1]; + bindings[0] = new VkVertexInputBindingDescription { binding = 0, stride = (uint)sizeof(Engine.Core.Vertex), inputRate = VkVertexInputRate.Vertex }; + + var attributes = stackalloc VkVertexInputAttributeDescription[1]; + attributes[0] = new VkVertexInputAttributeDescription { location = 0, binding = 0, format = VkFormat.R32G32B32Sfloat, offset = 0 }; + + var vertexInputState = new VkPipelineVertexInputStateCreateInfo + { + sType = VkStructureType.PipelineVertexInputStateCreateInfo, + vertexBindingDescriptionCount = 1, + pVertexBindingDescriptions = bindings, + vertexAttributeDescriptionCount = 1, + pVertexAttributeDescriptions = attributes, + }; + + var inputAssemblyState = new VkPipelineInputAssemblyStateCreateInfo + { + sType = VkStructureType.PipelineInputAssemblyStateCreateInfo, + topology = VkPrimitiveTopology.TriangleList, + primitiveRestartEnable = VkBool32.False, + }; + + var viewportState = new VkPipelineViewportStateCreateInfo + { + sType = VkStructureType.PipelineViewportStateCreateInfo, + viewportCount = 1, + scissorCount = 1, + }; + + var rasterizationState = new VkPipelineRasterizationStateCreateInfo + { + sType = VkStructureType.PipelineRasterizationStateCreateInfo, + depthClampEnable = VkBool32.False, + rasterizerDiscardEnable = VkBool32.False, + polygonMode = VkPolygonMode.Fill, + cullMode = VkCullModeFlags.None, + frontFace = VkFrontFace.CounterClockwise, + depthBiasEnable = VkBool32.True, + lineWidth = 1.0f, + }; + + var multisampleState = new VkPipelineMultisampleStateCreateInfo + { + sType = VkStructureType.PipelineMultisampleStateCreateInfo, + rasterizationSamples = VkSampleCountFlags.Count1, + }; + + var depthStencilState = new VkPipelineDepthStencilStateCreateInfo + { + sType = VkStructureType.PipelineDepthStencilStateCreateInfo, + depthTestEnable = VkBool32.True, + depthWriteEnable = VkBool32.True, + depthCompareOp = VkCompareOp.LessOrEqual, + depthBoundsTestEnable = VkBool32.False, + stencilTestEnable = VkBool32.False, + }; + + var colorBlendState = new VkPipelineColorBlendStateCreateInfo + { + sType = VkStructureType.PipelineColorBlendStateCreateInfo, + logicOpEnable = VkBool32.False, + attachmentCount = 0, + pAttachments = null, + }; + + var dynamicStates = stackalloc VkDynamicState[3]; + dynamicStates[0] = VkDynamicState.Viewport; + dynamicStates[1] = VkDynamicState.Scissor; + dynamicStates[2] = VkDynamicState.DepthBias; + + var dynamicState = new VkPipelineDynamicStateCreateInfo + { + sType = VkStructureType.PipelineDynamicStateCreateInfo, + dynamicStateCount = 3, + pDynamicStates = dynamicStates, + }; + + var pushConstantRanges = stackalloc VkPushConstantRange[2]; + pushConstantRanges[0] = new VkPushConstantRange { stageFlags = VkShaderStageFlags.Vertex, offset = 0, size = 64 }; + pushConstantRanges[1] = new VkPushConstantRange { stageFlags = VkShaderStageFlags.Fragment, offset = 64, size = 96 }; + + var layoutInfo = new VkPipelineLayoutCreateInfo + { + sType = VkStructureType.PipelineLayoutCreateInfo, + setLayoutCount = 0, + pushConstantRangeCount = 2, + pPushConstantRanges = (nint)pushConstantRanges, + }; + + var pl = VkPipelineLayout.Null; + Vk.vkCreatePipelineLayout(_device, &layoutInfo, 0, &pl); + PipelineLayout = pl; + + var depthFormat = VkFormat.D32Sfloat; + var renderingInfo = new VkPipelineRenderingCreateInfo + { + sType = VkStructureType.PipelineRenderingCreateInfo, + colorAttachmentCount = 0, + pColorAttachmentFormats = null, + depthAttachmentFormat = depthFormat, + }; + + var pipelineInfo = new VkGraphicsPipelineCreateInfo + { + sType = VkStructureType.GraphicsPipelineCreateInfo, + pNext = (nint)(&renderingInfo), + stageCount = 2, + pStages = stages, + pVertexInputState = &vertexInputState, + pInputAssemblyState = &inputAssemblyState, + pViewportState = &viewportState, + pRasterizationState = &rasterizationState, + pMultisampleState = &multisampleState, + pDepthStencilState = &depthStencilState, + pColorBlendState = &colorBlendState, + pDynamicState = &dynamicState, + layout = PipelineLayout, + renderPass = new VkRenderPass { Handle = 0 }, + subpass = 0, + }; + + var pp = VkPipeline.Null; + Vk.vkCreateGraphicsPipelines(_device, 0, 1, &pipelineInfo, 0, &pp); + Pipeline = pp; + + Vk.vkDestroyShaderModule(_device, fragModule, 0); + } + } + + private VkShaderModule CreateShaderModule(byte[] spv) + { + fixed (byte* pCode = spv) + { + var info = new VkShaderModuleCreateInfo + { + sType = VkStructureType.ShaderModuleCreateInfo, + codeSize = (nuint)spv.Length, + pCode = (uint*)pCode, + }; + var module = VkShaderModule.Null; + Vk.vkCreateShaderModule(_device, &info, 0, &module); + return module; + } + } + + private static byte[] LoadShader(string path) + { + if (!File.Exists(path)) + { + var altPath = Path.Combine(AppContext.BaseDirectory, path); + if (!File.Exists(altPath)) + { + altPath = Path.Combine(AppContext.BaseDirectory, "Shaders", Path.GetFileName(path)); + if (!File.Exists(altPath)) + throw new FileNotFoundException($"Shader file not found: {path}"); + } + return File.ReadAllBytes(altPath); + } + return File.ReadAllBytes(path); + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + if (Pipeline.Handle != 0) Vk.vkDestroyPipeline(_device, Pipeline, 0); + if (PipelineLayout.Handle != 0) Vk.vkDestroyPipelineLayout(_device, PipelineLayout, 0); + if (VertModule.Handle != 0) Vk.vkDestroyShaderModule(_device, VertModule, 0); + if (ShadowSampler.Handle != 0) Vk.vkDestroySampler(_device, ShadowSampler, 0); + if (DepthImageView.Handle != 0) Vk.vkDestroyImageView(_device, DepthImageView, 0); + if (DepthImage.Handle != 0) Vk.vkDestroyImage(_device, DepthImage, 0); + if (DepthImageMemory.Handle != 0) Vk.vkFreeMemory(_device, DepthImageMemory, 0); + } +}