From fa937ecd948547227266e745327aa4e66c342ec0 Mon Sep 17 00:00:00 2001 From: emil28092005 Date: Thu, 18 Jun 2026 20:35:01 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20cubemap=20shadow=20mapping=20=E2=80=94?= =?UTF-8?q?=20true=20omnidirectional=20point=20light=20shadows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - VulkanShadowMap: 1024x1024x6 layer D32_SFLOAT cube image - CubeCompatible flag, cube view for sampling, 6 face views for rendering - GetFaceViewProj: 6 directions (+X, -X, +Y, -Y, +Z, -Z) with correct up vectors - 6 shadow render passes per frame (one per cube face) - Fragment shader: samplerCube instead of sampler2D - Shadow: direction from light to fragment, distance comparison - No more perspective frustum limitation — omnidirectional shadows - shadow.vert: same push constant block (160B), uses lightViewProj per face - triangle.vert: removed fragLightSpacePos (not needed for cubemap) - Reduced shadow map size to 1024 (6x memory vs single 2048) - Floor/Grid excluded from shadow casting --- .../Shaders/triangle.frag | 39 ++--- .../Shaders/triangle.frag.spv | Bin 11936 -> 10404 bytes .../Shaders/triangle.vert | 2 - .../Shaders/triangle.vert.spv | Bin 2428 -> 2264 bytes src/Engine.Graphics.Vulkan/VulkanEnums.cs | 6 + src/Engine.Graphics.Vulkan/VulkanRenderer.cs | 139 +++++++++--------- src/Engine.Graphics.Vulkan/VulkanShadowMap.cs | 82 +++++++++-- 7 files changed, 158 insertions(+), 110 deletions(-) diff --git a/src/Engine.Graphics.Vulkan/Shaders/triangle.frag b/src/Engine.Graphics.Vulkan/Shaders/triangle.frag index a604fd7..39794f0 100644 --- a/src/Engine.Graphics.Vulkan/Shaders/triangle.frag +++ b/src/Engine.Graphics.Vulkan/Shaders/triangle.frag @@ -3,7 +3,6 @@ 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; @@ -11,7 +10,7 @@ layout(set = 0, binding = 0) uniform CameraUBO { mat4 vp; }; -layout(set = 0, binding = 1) uniform sampler2D shadowMap; +layout(set = 0, binding = 1) uniform samplerCube shadowCube; layout(push_constant) uniform PC { mat4 model; @@ -64,31 +63,19 @@ 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) +float calcShadow(vec3 worldPos, vec3 lightPos, vec3 N, vec3 L) { - vec3 projCoords = lightSpacePos.xyz / lightSpacePos.w; - projCoords = projCoords * 0.5 + 0.5; + vec3 dir = worldPos - lightPos; + float dist = length(dir); + vec3 dirNorm = dir / max(dist, 0.001); - 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; + // Sample cubemap: direction from light to fragment + float closestDepth = texture(shadowCube, dirNorm).r; + // closestDepth is in [0,1], map to world distance + float mappedDepth = closestDepth * 60.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; + float bias = max(0.05 * (1.0 - dot(N, L)), 0.005); + return dist - bias < mappedDepth ? 1.0 : 0.0; } void main() @@ -111,8 +98,8 @@ 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); + // Shadow from cubemap + float shadow = calcShadow(fragWorldPos, lightPos, N, L); vec3 H = normalize(V + L); vec3 F0 = mix(vec3(0.04), albedo, metallic); diff --git a/src/Engine.Graphics.Vulkan/Shaders/triangle.frag.spv b/src/Engine.Graphics.Vulkan/Shaders/triangle.frag.spv index c15a826b869f5df3a735068214bed1df5c39ede4..e2858f0334d157b3741cd56818fe434f4f179626 100644 GIT binary patch literal 10404 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 OZeKl9o(nb4#s34*1G~Qf literal 11936 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 diff --git a/src/Engine.Graphics.Vulkan/VulkanEnums.cs b/src/Engine.Graphics.Vulkan/VulkanEnums.cs index bfa2dd9..6717849 100644 --- a/src/Engine.Graphics.Vulkan/VulkanEnums.cs +++ b/src/Engine.Graphics.Vulkan/VulkanEnums.cs @@ -340,6 +340,12 @@ public enum VkAccessFlags2 : ulong ShaderWrite = 0x200000000, } +public enum VkImageCreateFlags : uint +{ + None = 0, + CubeCompatible = 0x00000010, +} + public enum VkDynamicState : int { Viewport = 0, diff --git a/src/Engine.Graphics.Vulkan/VulkanRenderer.cs b/src/Engine.Graphics.Vulkan/VulkanRenderer.cs index 6632983..759d38b 100644 --- a/src/Engine.Graphics.Vulkan/VulkanRenderer.cs +++ b/src/Engine.Graphics.Vulkan/VulkanRenderer.cs @@ -43,7 +43,7 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen _shadowMap = new VulkanShadowMap(ctx.Device, ctx, _pipeline.DescriptorSetLayout); for (int i = 0; i < VulkanFrameResources.MaxFramesInFlight; i++) - _frameResources.UpdateShadowDescriptor(i, _shadowMap.ShadowSampler, _shadowMap.DepthImageView); + _frameResources.UpdateShadowDescriptor(i, _shadowMap.ShadowSampler, _shadowMap.CubeImageView); _imGui = new VulkanImGui(ctx, _frameResources.CommandPool, swapchain.Format, swapchain.DepthFormat); } @@ -174,90 +174,97 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen }; Vk.vkBeginCommandBuffer(cmd, &beginInfo); - // === SHADOW PASS === + // === SHADOW CUBEMAP PASSES (6 faces) === 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 lightPosVec = new Vector3(lightPos.X, lightPos.Y, lightPos.Z); - var shadowDepthAttachment = new VkRenderingAttachmentInfo + for (int face = 0; face < 6; face++) { - sType = VkStructureType.RenderingAttachmentInfo, - imageView = _shadowMap.DepthImageView, - imageLayout = VkImageLayout.DepthStencilAttachmentOptimal, - loadOp = VkAttachmentLoadOp.Clear, - storeOp = VkAttachmentStoreOp.Store, - clearValue = shadowDepthClear, - }; + var (faceView, faceProj) = VulkanShadowMap.GetFaceViewProj(lightPosVec, face); + var faceViewProj = faceView * faceProj; - var shadowRenderingInfo = new VkRenderingInfo - { - sType = VkStructureType.RenderingInfo, - renderArea = new VkRect2D + var shadowDepthClear = new VkClearValue + { + DepthStencil = new VkClearDepthStencilValue { Depth = 1.0f, Stencil = 0 }, + }; + + var shadowDepthAttachment = new VkRenderingAttachmentInfo + { + sType = VkStructureType.RenderingAttachmentInfo, + imageView = _shadowMap.FaceImageViews[face], + 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 }, - }, - layerCount = 1, - colorAttachmentCount = 0, - pColorAttachments = null, - pDepthAttachment = &shadowDepthAttachment, - }; + }; + Vk.vkCmdSetScissor(cmd, 0, 1, &shadowScissor); - Vk.vkCmdBeginRendering(cmd, &shadowRenderingInfo); + Vk.vkCmdSetDepthBias(cmd, 1.25f, 0.0f, 1.75f); - Vk.vkCmdBindPipeline(cmd, VkPipelineBindPoint.Graphics, _shadowMap.Pipeline); + foreach (var dc in drawCalls) + { + if (!dc.castShadow) continue; - 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 vertexBuf = dc.vertexBuf; + ulong offset = 0; + Vk.vkCmdBindVertexBuffers(cmd, 0, 1, &vertexBuf, &offset); + Vk.vkCmdBindIndexBuffer(cmd, dc.indexBuf, 0, 1); - 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); + var pcData = stackalloc byte[160]; + var modelCopy = dc.model; + System.Buffer.MemoryCopy(&modelCopy, pcData, 64, 64); + var lpCopy = lightPos; + System.Buffer.MemoryCopy(&lpCopy, pcData + 64, 16, 16); + var lcCopy = lightColor; + System.Buffer.MemoryCopy(&lcCopy, pcData + 80, 16, 16); + var lvpCopy = faceViewProj; + System.Buffer.MemoryCopy(&lvpCopy, pcData + 96, 64, 64); - Vk.vkCmdSetDepthBias(cmd, 1.25f, 0.0f, 1.75f); + Vk.vkCmdPushConstants(cmd, _shadowMap.PipelineLayout, VkShaderStageFlags.Vertex | VkShaderStageFlags.Fragment, 0, 160, pcData); - foreach (var dc in drawCalls) - { - if (!dc.castShadow) continue; + Vk.vkCmdDrawIndexed(cmd, dc.indexCount, 1, 0, 0, 0); + } - var vertexBuf = dc.vertexBuf; - ulong offset = 0; - Vk.vkCmdBindVertexBuffers(cmd, 0, 1, &vertexBuf, &offset); - Vk.vkCmdBindIndexBuffer(cmd, dc.indexBuf, 0, 1); - - // Pack all push constants for shadow pass - var pcData = stackalloc byte[160]; - var modelCopy = dc.model; - System.Buffer.MemoryCopy(&modelCopy, pcData, 64, 64); - var lpCopy = lightPos; - System.Buffer.MemoryCopy(&lpCopy, pcData + 64, 16, 16); - var lcCopy = lightColor; - System.Buffer.MemoryCopy(&lcCopy, pcData + 80, 16, 16); - var lvpCopy = lightViewProj; - System.Buffer.MemoryCopy(&lvpCopy, pcData + 96, 64, 64); - - Vk.vkCmdPushConstants(cmd, _shadowMap.PipelineLayout, VkShaderStageFlags.Vertex | VkShaderStageFlags.Fragment, 0, 160, pcData); - - Vk.vkCmdDrawIndexed(cmd, dc.indexCount, 1, 0, 0, 0); + Vk.vkCmdEndRendering(cmd); } - Vk.vkCmdEndRendering(cmd); - - // Transition shadow map: depth attachment → shader read + // Transition shadow cubemap: depth attachment → shader read TransitionImageLayoutDepth(cmd, _shadowMap.DepthImage, VkImageLayout.DepthStencilAttachmentOptimal, VkImageLayout.ShaderReadOnlyOptimal, 0x100, 0x200, 0x8, 0x20); diff --git a/src/Engine.Graphics.Vulkan/VulkanShadowMap.cs b/src/Engine.Graphics.Vulkan/VulkanShadowMap.cs index a42f55e..17c0197 100644 --- a/src/Engine.Graphics.Vulkan/VulkanShadowMap.cs +++ b/src/Engine.Graphics.Vulkan/VulkanShadowMap.cs @@ -1,14 +1,16 @@ using System.Runtime.InteropServices; +using System.Numerics; namespace Engine.Graphics.Vulkan; internal sealed unsafe class VulkanShadowMap : IDisposable { - public const uint ShadowMapSize = 2048; + public const uint ShadowMapSize = 1024; public VkImage DepthImage; public VkDeviceMemory DepthImageMemory; - public VkImageView DepthImageView; + public VkImageView CubeImageView; + public VkImageView[] FaceImageViews = new VkImageView[6]; public VkSampler ShadowSampler; public VkPipeline Pipeline; public VkPipelineLayout PipelineLayout; @@ -23,23 +25,24 @@ internal sealed unsafe class VulkanShadowMap : IDisposable _device = device; _ctx = ctx; - CreateShadowImage(); + CreateShadowCubeImage(); CreateShadowSampler(); - CreateShadowPipeline(descLayout); + CreateShadowPipeline(); - Console.WriteLine("[Vulkan] Shadow map created (2048x2048 D32_SFLOAT)"); + Console.WriteLine("[Vulkan] Shadow cubemap created (1024x1024x6 D32_SFLOAT)"); } - private void CreateShadowImage() + private void CreateShadowCubeImage() { var imageInfo = new VkImageCreateInfo { sType = VkStructureType.ImageCreateInfo, + flags = (uint)VkImageCreateFlags.CubeCompatible, imageType = VkImageType.Type2D, format = VkFormat.D32Sfloat, extent = new VkExtent3D { Width = ShadowMapSize, Height = ShadowMapSize, Depth = 1 }, mipLevels = 1, - arrayLayers = 1, + arrayLayers = 6, samples = VkSampleCountFlags.Count1, tiling = VkImageTiling.Optimal, usage = VkImageUsageFlags.DepthStencilAttachment | VkImageUsageFlags.Sampled, @@ -67,19 +70,38 @@ internal sealed unsafe class VulkanShadowMap : IDisposable DepthImageMemory = mem; Vk.vkBindImageMemory(_device, DepthImage, DepthImageMemory, 0); - var viewInfo = new VkImageViewCreateInfo + // Cube view for sampling + var cubeViewInfo = new VkImageViewCreateInfo { sType = VkStructureType.ImageViewCreateInfo, image = DepthImage, - viewType = VkImageViewType.Type2D, + viewType = VkImageViewType.TypeCube, 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 }, + subresourceRange = new VkImageSubresourceRange { AspectMask = VkImageAspectFlags.Depth, BaseMipLevel = 0, LevelCount = 1, BaseArrayLayer = 0, LayerCount = 6 }, }; - var view = VkImageView.Null; - Vk.vkCreateImageView(_device, &viewInfo, 0, &view); - DepthImageView = view; + var cubeView = VkImageView.Null; + Vk.vkCreateImageView(_device, &cubeViewInfo, 0, &cubeView); + CubeImageView = cubeView; + + // 6 face views for rendering + for (int i = 0; i < 6; i++) + { + var faceViewInfo = 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 = (uint)i, LayerCount = 1 }, + }; + + var faceView = VkImageView.Null; + Vk.vkCreateImageView(_device, &faceViewInfo, 0, &faceView); + FaceImageViews[i] = faceView; + } } private void CreateShadowSampler() @@ -95,7 +117,7 @@ internal sealed unsafe class VulkanShadowMap : IDisposable addressModeW = VkSamplerAddressMode.ClampToBorder, minLod = 0, maxLod = 1, - borderColor = 1, // VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE + borderColor = 1, // FLOAT_OPAQUE_WHITE }; var samp = VkSampler.Null; @@ -103,7 +125,33 @@ internal sealed unsafe class VulkanShadowMap : IDisposable ShadowSampler = samp; } - private void CreateShadowPipeline(VkDescriptorSetLayout descLayout) + public static (Matrix4x4 view, Matrix4x4 proj) GetFaceViewProj(Vector3 lightPos, int face) + { + var proj = Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 2f, 1.0f, 0.1f, 60f); + proj.M22 *= -1; + + var target = lightPos; + var up = Vector3.UnitY; + + target += face switch + { + 0 => Vector3.UnitX, + 1 => -Vector3.UnitX, + 2 => Vector3.UnitY, + 3 => -Vector3.UnitY, + 4 => Vector3.UnitZ, + 5 => -Vector3.UnitZ, + _ => Vector3.UnitZ, + }; + + if (face == 2) up = -Vector3.UnitZ; + else if (face == 3) up = Vector3.UnitZ; + + var view = Matrix4x4.CreateLookAt(lightPos, target, up); + return (view, proj); + } + + private void CreateShadowPipeline() { var vertSpv = LoadShader("Shaders/shadow.vert.spv"); var fragSpv = LoadShader("Shaders/shadow.frag.spv"); @@ -296,7 +344,9 @@ internal sealed unsafe class VulkanShadowMap : IDisposable 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 (CubeImageView.Handle != 0) Vk.vkDestroyImageView(_device, CubeImageView, 0); + for (int i = 0; i < 6; i++) + if (FaceImageViews[i].Handle != 0) Vk.vkDestroyImageView(_device, FaceImageViews[i], 0); if (DepthImage.Handle != 0) Vk.vkDestroyImage(_device, DepthImage, 0); if (DepthImageMemory.Handle != 0) Vk.vkFreeMemory(_device, DepthImageMemory, 0); }