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 c15a826..e2858f0 100644 Binary files a/src/Engine.Graphics.Vulkan/Shaders/triangle.frag.spv and b/src/Engine.Graphics.Vulkan/Shaders/triangle.frag.spv differ diff --git a/src/Engine.Graphics.Vulkan/Shaders/triangle.vert b/src/Engine.Graphics.Vulkan/Shaders/triangle.vert index 64e9d7a..2c20c93 100644 --- a/src/Engine.Graphics.Vulkan/Shaders/triangle.vert +++ b/src/Engine.Graphics.Vulkan/Shaders/triangle.vert @@ -7,7 +7,6 @@ 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; @@ -26,5 +25,4 @@ void main() { 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 a1803be..9c4591d 100644 Binary files a/src/Engine.Graphics.Vulkan/Shaders/triangle.vert.spv and b/src/Engine.Graphics.Vulkan/Shaders/triangle.vert.spv differ 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); }