From c3cca30bbafb30f1af8cf6aec11bda794c454793 Mon Sep 17 00:00:00 2001 From: emil28092005 Date: Fri, 19 Jun 2026 07:15:29 +0300 Subject: [PATCH] feat: multi-light system with per-light cubemap array shadows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Architecture: - SceneUBO (448B): mat4 vp + numLights + numShadowLights + LightData[8] + shadowParams[4] - LightData: vec4 posAndIntensity + vec4 colorAndRange (32B per light) - Push constants (main): mat4 model only (64B) — all light data in UBO - Push constants (shadow): model(64) + lightViewProj(64) + lightPos(16) + shadowParams(16) = 160B - Shadow: cubemap array (24 layers = 4 lights × 6 faces), samplerCubeArray in shader - Shadow passes: numShadowLights × 6 faces (2 lights = 12 passes) - Fragment shader: loop over lights, calcPBR per light, shadow from corresponding cubemap layer New OBJ files with verified CCW winding: - pyramid.obj, diamond.obj, torus.obj, cone.obj, sphere.obj Scene 'Chaos Architecture': - 28 sphere pyramid (physics) - 30 falling diamonds (physics) - 8 floating torus rings (decorative) - 4 pyramid decorations (static) - 4 cone pillars (static) - Central torus knot (floating) - 2 dynamic point lights (warm + cool), both cast shadows - Both lights orbit in chaotic circles ImGui panel: - Per-light intensity, range, RGB sliders - Shadow bias, sample radius, far plane sliders 227 tests, all pass --- src/CortexEngine.App/Program.cs | 105 ++++--- .../Shaders/shadow.frag | 3 +- .../Shaders/shadow.frag.spv | Bin 1236 -> 1188 bytes .../Shaders/shadow.vert | 3 +- .../Shaders/shadow.vert.spv | Bin 1588 -> 1540 bytes .../Shaders/triangle.frag | 90 +++--- .../Shaders/triangle.frag.spv | Bin 13472 -> 15136 bytes .../Shaders/triangle.vert | 20 +- .../Shaders/triangle.vert.spv | Bin 2316 -> 2568 bytes .../VulkanFrameResources.cs | 2 +- src/Engine.Graphics.Vulkan/VulkanPipeline.cs | 2 +- src/Engine.Graphics.Vulkan/VulkanRenderer.cs | 270 +++++++++--------- src/Engine.Graphics.Vulkan/VulkanShadowMap.cs | 50 ++-- 13 files changed, 294 insertions(+), 251 deletions(-) diff --git a/src/CortexEngine.App/Program.cs b/src/CortexEngine.App/Program.cs index 393a4a6..45b52c0 100644 --- a/src/CortexEngine.App/Program.cs +++ b/src/CortexEngine.App/Program.cs @@ -133,17 +133,29 @@ class Program physicsWorld.Update((float)timing.DeltaTime); physicsWorld.SyncTransforms(world); - // Move light in a chaotic circle - var lightEntity = world.Lookup("MainLight"); - if ((ulong)lightEntity.Id != 0) + // Move lights in chaotic circles + var mainLight = world.Lookup("MainLight"); + if ((ulong)mainLight.Id != 0) { var t = totalTime; var lx = MathF.Sin(t * 0.7f) * 8f + MathF.Cos(t * 0.3f) * 3f; var ly = 12f + MathF.Sin(t * 0.5f) * 5f; var lz = MathF.Cos(t * 0.6f) * 8f + MathF.Sin(t * 0.4f) * 3f; - var lt = lightEntity.Get(); + var lt = mainLight.Get(); lt.Position = new Vector3(lx, ly, lz); - lightEntity.Set(lt); + mainLight.Set(lt); + } + + var secondLight = world.Lookup("SecondLight"); + if ((ulong)secondLight.Id != 0) + { + var t = totalTime + MathF.PI; + var lx = MathF.Cos(t * 0.5f) * 10f + MathF.Sin(t * 0.2f) * 4f; + var ly = 8f + MathF.Cos(t * 0.4f) * 4f; + var lz = MathF.Sin(t * 0.6f) * 10f + MathF.Cos(t * 0.3f) * 3f; + var lt = secondLight.Get(); + lt.Position = new Vector3(lx, ly, lz); + secondLight.Set(lt); } var processed = queue.ProcessPending(); @@ -179,59 +191,60 @@ class Program ImGui.End(); // Shadow parameters panel - ImGui.Begin("Shadow Parameters"); + ImGui.Begin("Shadow & Light Parameters"); - var lightEntity2 = world.Lookup("MainLight"); - if ((ulong)lightEntity2.Id != 0) + // Main light controls + var mainLight2 = world.Lookup("MainLight"); + if ((ulong)mainLight2.Id != 0) { - var lightComp = lightEntity2.Get(); - var intensity = lightComp.Intensity; - var range = lightComp.Range; - var colorR = lightComp.Color.X; - var colorG = lightComp.Color.Y; - var colorB = lightComp.Color.Z; - - ImGui.SliderFloat("Light Intensity", ref intensity, 0.0f, 100.0f, "%.1f"); - ImGui.SliderFloat("Light Range", ref range, 5.0f, 100.0f, "%.1f"); - ImGui.SliderFloat("Light R", ref colorR, 0.0f, 1.0f, "%.2f"); - ImGui.SliderFloat("Light G", ref colorG, 0.0f, 1.0f, "%.2f"); - ImGui.SliderFloat("Light B", ref colorB, 0.0f, 1.0f, "%.2f"); - ImGui.Separator(); - - lightComp.Intensity = intensity; - lightComp.Range = range; - lightComp.Color = new Vector3(colorR, colorG, colorB); - lightEntity2.Set(lightComp); + var lc1 = mainLight2.Get(); + var i1 = lc1.Intensity; var r1 = lc1.Range; + var cr1 = lc1.Color.X; var cg1 = lc1.Color.Y; var cb1 = lc1.Color.Z; + ImGui.Text("Main Light (warm)"); + ImGui.SliderFloat("1 Intensity", ref i1, 0.0f, 50.0f, "%.1f"); + ImGui.SliderFloat("1 Range", ref r1, 5.0f, 100.0f, "%.1f"); + ImGui.SliderFloat("1 R", ref cr1, 0.0f, 1.0f, "%.2f"); + ImGui.SliderFloat("1 G", ref cg1, 0.0f, 1.0f, "%.2f"); + ImGui.SliderFloat("1 B", ref cb1, 0.0f, 1.0f, "%.2f"); + lc1.Intensity = i1; lc1.Range = r1; + lc1.Color = new Vector3(cr1, cg1, cb1); + mainLight2.Set(lc1); } + ImGui.Separator(); + + // Second light controls + var secondLight2 = world.Lookup("SecondLight"); + if ((ulong)secondLight2.Id != 0) + { + var lc2 = secondLight2.Get(); + var i2 = lc2.Intensity; var r2 = lc2.Range; + var cr2 = lc2.Color.X; var cg2 = lc2.Color.Y; var cb2 = lc2.Color.Z; + ImGui.Text("Second Light (cool)"); + ImGui.SliderFloat("2 Intensity", ref i2, 0.0f, 50.0f, "%.1f"); + ImGui.SliderFloat("2 Range", ref r2, 5.0f, 100.0f, "%.1f"); + ImGui.SliderFloat("2 R", ref cr2, 0.0f, 1.0f, "%.2f"); + ImGui.SliderFloat("2 G", ref cg2, 0.0f, 1.0f, "%.2f"); + ImGui.SliderFloat("2 B", ref cb2, 0.0f, 1.0f, "%.2f"); + lc2.Intensity = i2; lc2.Range = r2; + lc2.Color = new Vector3(cr2, cg2, cb2); + secondLight2.Set(lc2); + } + + ImGui.Separator(); + var bias = vkRenderer.ShadowBias; var sampleRadius = vkRenderer.ShadowSampleRadius; var farPlane = vkRenderer.ShadowFarPlane; - ImGui.SliderFloat("Shadow Bias", ref bias, 0.001f, 0.5f, "%.4f"); + ImGui.SliderFloat("Shadow Bias", ref bias, 0.0001f, 0.1f, "%.4f"); ImGui.SliderFloat("Sample Radius", ref sampleRadius, 0.001f, 0.1f, "%.4f"); ImGui.SliderFloat("Far Plane", ref farPlane, 10.0f, 120.0f, "%.1f"); - ImGui.Separator(); vkRenderer.ShadowBias = bias; vkRenderer.ShadowSampleRadius = sampleRadius; vkRenderer.ShadowFarPlane = farPlane; - ImGui.Text($"Current: bias={bias:F4} radius={sampleRadius:F4} far={farPlane:F1}"); - - if (ImGui.Button("Copy Parameters to Clipboard")) - { - var paramsText = $"bias={bias:F4}\nsampleRadius={sampleRadius:F4}\nfarPlane={farPlane:F1}\nshadowMapSize=2048"; - var lightEntity3 = world.Lookup("MainLight"); - if ((ulong)lightEntity3.Id != 0) - { - var lc = lightEntity3.Get(); - paramsText += $"\nlightIntensity={lc.Intensity:F1}\nlightRange={lc.Range:F1}\nlightColor=({lc.Color.X:F2},{lc.Color.Y:F2},{lc.Color.Z:F2})"; - } - ImGui.SetClipboardText(paramsText); - Console.WriteLine("[App] Parameters copied to clipboard:\n" + paramsText); - } - ImGui.End(); renderer.EndImGuiFrame(); @@ -355,11 +368,15 @@ class Program .Set(coneMesh); } - // Light + // Lights world.Entity("MainLight") .Set(new Transform(new Vector3(0, 20, 0), Quaternion.Identity, Vector3.One)) .Set(Light.Point(new Vector3(0, 20, 0), new Vector3(1.0f, 0.95f, 0.85f), intensity: 15.0f, range: 60.0f)); + world.Entity("SecondLight") + .Set(new Transform(new Vector3(-12, 10, 8), Quaternion.Identity, Vector3.One)) + .Set(Light.Point(new Vector3(-12, 10, 8), new Vector3(0.3f, 0.6f, 1.0f), intensity: 10.0f, range: 40.0f)); + var entityCount = 0; world.Each((Entity e, ref Transform _) => entityCount++); Console.WriteLine($"[Scene] {entityCount} entities: sphere pyramid + diamond rain + 8 torus rings + 4 pyramids + 4 cones + floor + grid + light"); diff --git a/src/Engine.Graphics.Vulkan/Shaders/shadow.frag b/src/Engine.Graphics.Vulkan/Shaders/shadow.frag index a3b124d..9a8b290 100644 --- a/src/Engine.Graphics.Vulkan/Shaders/shadow.frag +++ b/src/Engine.Graphics.Vulkan/Shaders/shadow.frag @@ -5,9 +5,8 @@ layout(location = 0) out float outDepth; layout(push_constant) uniform PC { mat4 model; - vec4 lightPos; - vec4 lightColor; mat4 lightViewProj; + vec4 lightPos; vec4 shadowParams; } pc; diff --git a/src/Engine.Graphics.Vulkan/Shaders/shadow.frag.spv b/src/Engine.Graphics.Vulkan/Shaders/shadow.frag.spv index b4fd00eddadfa515bba2b50d77c5cfb965f5cfe5..d396c9c6c83021e9a5f0e3abdb2c5c1661bb0dcb 100644 GIT binary patch delta 161 zcmcb@xrB4VF>7`Peg*~xMj+0~OwT9@%S4%iLwCM%0Rqe j@>j;G!VvTLfP7vk=9@f|$(fOH@?9o>MwZRG%;k&#Lew9x diff --git a/src/Engine.Graphics.Vulkan/Shaders/shadow.vert b/src/Engine.Graphics.Vulkan/Shaders/shadow.vert index 43c43de..a3219c8 100644 --- a/src/Engine.Graphics.Vulkan/Shaders/shadow.vert +++ b/src/Engine.Graphics.Vulkan/Shaders/shadow.vert @@ -4,9 +4,8 @@ layout(location = 0) in vec3 inPosition; layout(push_constant) uniform PC { mat4 model; - vec4 lightPos; - vec4 lightColor; mat4 lightViewProj; + vec4 lightPos; vec4 shadowParams; } pc; diff --git a/src/Engine.Graphics.Vulkan/Shaders/shadow.vert.spv b/src/Engine.Graphics.Vulkan/Shaders/shadow.vert.spv index 3e5b97b1266ad9532b13dfd34cedbb62582ab688..484eb10fc351bed5024582570b63d66b2ca47dcd 100644 GIT binary patch delta 161 zcmdnO)50@hlQla7Hv_e4nLts99_G!g zjFOD9$Y{BG2 dri;Q58@PackX{hx+U&@z&B(~SIhSQRBLFfM8tec7 diff --git a/src/Engine.Graphics.Vulkan/Shaders/triangle.frag b/src/Engine.Graphics.Vulkan/Shaders/triangle.frag index c80b732..81d3ba3 100644 --- a/src/Engine.Graphics.Vulkan/Shaders/triangle.frag +++ b/src/Engine.Graphics.Vulkan/Shaders/triangle.frag @@ -6,19 +6,21 @@ layout(location = 2) in vec3 fragAlbedo; layout(location = 0) out vec4 outColor; -layout(set = 0, binding = 0) uniform CameraUBO { - mat4 vp; +struct LightData { + vec4 posAndIntensity; + vec4 colorAndRange; }; -layout(set = 0, binding = 1) uniform samplerCube shadowCube; +layout(set = 0, binding = 0) uniform SceneUBO { + mat4 vp; + int numLights; + int numShadowLights; + vec2 padding; + LightData lights[8]; + vec4 shadowParams[4]; +} scene; -layout(push_constant) uniform PC { - mat4 model; - vec4 lightPos; - vec4 lightColor; - mat4 lightViewProj; - vec4 shadowParams; -} pc; +layout(set = 0, binding = 1) uniform samplerCubeArray shadowArray; const vec3 AMBIENT = vec3(0.01, 0.01, 0.02); const float PI = 3.14159265359; @@ -92,11 +94,11 @@ mat3 buildTangentBasis(vec3 dir) return mat3(tangent, bitangent, dir); } -float calcShadow(vec3 worldPos, vec3 lightPos, vec3 N, vec3 L) +float calcShadow(vec3 worldPos, vec3 lightPos, vec3 N, vec3 L, int lightIndex) { - float bias = pc.shadowParams.x; - float sampleRadius = pc.shadowParams.y; - float farPlane = pc.shadowParams.z; + float bias = scene.shadowParams[lightIndex].x; + float sampleRadius = scene.shadowParams[lightIndex].y; + float farPlane = scene.shadowParams[lightIndex].z; vec3 dir = worldPos - lightPos; float dist = length(dir); @@ -112,7 +114,7 @@ float calcShadow(vec3 worldPos, vec3 lightPos, vec3 N, vec3 L) for (int i = 0; i < 16; i++) { vec3 sampleDir = normalize(dirNorm + basis * (POISSON_DISK[i] * radius)); - float sampleDepth = texture(shadowCube, sampleDir).r; + float sampleDepth = texture(shadowArray, vec4(sampleDir, float(lightIndex))).r; float sampleMapped = sampleDepth * farPlane; shadow += (dist - slopeBias < sampleMapped) ? 1.0 : 0.0; } @@ -121,28 +123,8 @@ float calcShadow(vec3 worldPos, vec3 lightPos, vec3 N, vec3 L) return shadow; } -void main() +vec3 calcPBR(vec3 N, vec3 V, vec3 L, vec3 radiance, vec3 albedo, float roughness, float metallic) { - vec3 N = normalize(fragNormal); - vec3 V = normalize(-fragWorldPos); - vec3 albedo = fragAlbedo; - float roughness = 0.5; - float metallic = 0.1; - - vec3 lightPos = pc.lightPos.xyz; - float lightIntensity = pc.lightPos.w; - vec3 lightColor = pc.lightColor.xyz; - float lightRange = pc.lightColor.w; - - vec3 toLight = lightPos - fragWorldPos; - float dist = length(toLight); - vec3 L = toLight / max(dist, 0.001); - - float attenuation = pow(clamp(1.0 - dist / max(lightRange, 0.001), 0.0, 1.0), 2.0); - vec3 radiance = lightColor * lightIntensity * attenuation; - - float shadow = calcShadow(fragWorldPos, lightPos, N, L); - vec3 H = normalize(V + L); vec3 F0 = mix(vec3(0.04), albedo, metallic); @@ -158,9 +140,41 @@ void main() vec3 kD = (vec3(1.0) - kS) * (1.0 - metallic); float NdotL = max(dot(N, L), 0.0); - vec3 lighting = (kD * albedo / PI + specular) * radiance * NdotL * shadow; + return (kD * albedo / PI + specular) * radiance * NdotL; +} - vec3 color = AMBIENT * albedo + lighting; +void main() +{ + vec3 N = normalize(fragNormal); + vec3 V = normalize(-fragWorldPos); + vec3 albedo = fragAlbedo; + float roughness = 0.5; + float metallic = 0.1; + + vec3 color = AMBIENT * albedo; + + for (int i = 0; i < scene.numLights; i++) + { + vec3 lightPos = scene.lights[i].posAndIntensity.xyz; + float lightIntensity = scene.lights[i].posAndIntensity.w; + vec3 lightColor = scene.lights[i].colorAndRange.xyz; + float lightRange = scene.lights[i].colorAndRange.w; + + vec3 toLight = lightPos - fragWorldPos; + float dist = length(toLight); + vec3 L = toLight / max(dist, 0.001); + + float attenuation = pow(clamp(1.0 - dist / max(lightRange, 0.001), 0.0, 1.0), 2.0); + vec3 radiance = lightColor * lightIntensity * attenuation; + + float shadow = 1.0; + if (i < scene.numShadowLights) + { + shadow = calcShadow(fragWorldPos, lightPos, N, L, i); + } + + color += calcPBR(N, V, L, radiance, albedo, roughness, metallic) * shadow; + } 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 5751d5be87d3802dabf8eff71a86b949c4793a95..c2b0c65dd1b69c94473c46f428a2cc35f2fd6729 100644 GIT binary patch literal 15136 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 literal 13472 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 J|EStM(O){ZuCf3C literal 2316 zcmZ9M+foxj5QYar08bzy2NiJv@q{8OAc{x;tKfo?Sd@6FD_LQ!O_o}sSYG)GzJbr; zQ+Z>R|2Ml0kx6x?`|rd2(>+P4eWJ^`Hh0c-yH%I3KG!b93G4Lx`sViL^kK6$Jv%o; zM2{=^Kw^5`d9QcqyB@|3i~-5GWLok_@!1@c2#~9aXdR zvuyLh>2N=+rAO5;3+u-`E(kx^g;UwzF>dIvxEZIqby?Yg{Cmm!YLx9nSu^@3Q)rH( zkj(zlMTsRAk6WZ6p zdX$B4SGS~hs@q_!uLpT=Cv(&DXI`JOaPQrJ{V)3&g1=6)dZ?yTo0-N9LvoB66StCl zjA|)ZhpYIwy!q?JEV*l;GF`C`KZo7uM7mBJ>zUs1%3iF-*q z`k=H80um3-;K?DX0;P*etNa z>60*1=C-65?r}*1jt!SV33GX)9Qr$4VA_tcD+LBmi@RE2aH{H5&b=lfpLqPt_owRO z8!BHBwz@OJ8~y-nI(Sf=VR%o`}sr|IDLWRoCJ(zXUt0nM_6`-S8i(ipm@mR_K Lr(yrqX$z8nRWXvj diff --git a/src/Engine.Graphics.Vulkan/VulkanFrameResources.cs b/src/Engine.Graphics.Vulkan/VulkanFrameResources.cs index 698f58b..6afe650 100644 --- a/src/Engine.Graphics.Vulkan/VulkanFrameResources.cs +++ b/src/Engine.Graphics.Vulkan/VulkanFrameResources.cs @@ -5,7 +5,7 @@ namespace Engine.Graphics.Vulkan; internal sealed unsafe class VulkanFrameResources : IDisposable { public const int MaxFramesInFlight = 2; - public const ulong UboSize = 64; + public const ulong UboSize = 448; public VkCommandPool CommandPool; public VkCommandBuffer[] CommandBuffers = new VkCommandBuffer[MaxFramesInFlight]; diff --git a/src/Engine.Graphics.Vulkan/VulkanPipeline.cs b/src/Engine.Graphics.Vulkan/VulkanPipeline.cs index 6807495..6ab6812 100644 --- a/src/Engine.Graphics.Vulkan/VulkanPipeline.cs +++ b/src/Engine.Graphics.Vulkan/VulkanPipeline.cs @@ -154,7 +154,7 @@ internal sealed unsafe class VulkanPipeline : IDisposable { stageFlags = VkShaderStageFlags.Vertex | VkShaderStageFlags.Fragment, offset = 0, - size = 176, + size = 64, }; var descLayout = DescriptorSetLayout; diff --git a/src/Engine.Graphics.Vulkan/VulkanRenderer.cs b/src/Engine.Graphics.Vulkan/VulkanRenderer.cs index bd851ac..f464712 100644 --- a/src/Engine.Graphics.Vulkan/VulkanRenderer.cs +++ b/src/Engine.Graphics.Vulkan/VulkanRenderer.cs @@ -47,7 +47,7 @@ public sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreensh _shadowMap = new VulkanShadowMap(ctx.Device, ctx, _pipeline.DescriptorSetLayout); for (int i = 0; i < VulkanFrameResources.MaxFramesInFlight; i++) - _frameResources.UpdateShadowDescriptor(i, _shadowMap.ShadowSampler, _shadowMap.CubeColorView); + _frameResources.UpdateShadowDescriptor(i, _shadowMap.ShadowSampler, _shadowMap.CubeArrayColorView); _imGui = new VulkanImGui(ctx, _frameResources.CommandPool, swapchain.Format, swapchain.DepthFormat); } @@ -87,40 +87,56 @@ public sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreensh vp = Matrix4x4.CreateLookAt(new Vector3(0, 0, -6), Vector3.Zero, Vector3.UnitY) * proj; } - // Collect point light from ECS — prefer light attached to a moving entity - var lightPos = new Vector4(0, 5, 0, 0); - var lightColor = new Vector4(1, 1, 1, 0); + // Collect all point lights from ECS + var lights = new List<(Vector3 pos, float intensity, Vector3 color, float range)>(); world.Each((Entity e, ref Light l, ref Transform lt) => { - if (l.IsPoint && lightPos.W == 0) - { - lightPos = new Vector4(lt.Position, l.Intensity); - lightColor = new Vector4(l.Color, l.Range); - } + if (l.IsPoint) + lights.Add((lt.Position, l.Intensity, l.Color, l.Range)); }); - if (lightPos.W == 0) + if (lights.Count == 0) { world.Each((Entity e, ref Light l) => { - if (l.IsPoint && lightPos.W == 0) - { - lightPos = new Vector4(l.Position, l.Intensity); - lightColor = new Vector4(l.Color, l.Range); - } + if (l.IsPoint) + lights.Add((l.Position, l.Intensity, l.Color, l.Range)); }); } - // Pack UBO: just mat4 vp (64 bytes) - var uboData = stackalloc byte[64]; + int numLights = Math.Min(lights.Count, 8); + int numShadowLights = Math.Min(numLights, VulkanShadowMap.MaxShadowLights); + + // Pack SceneUBO: mat4 vp(64) + int numLights(4) + int numShadowLights(4) + vec2 pad(8) + LightData[8](256) + vec4 shadowParams[4](64) = 400B → pad to 448 + var uboData = stackalloc byte[448]; var vpCopy = vp; System.Buffer.MemoryCopy(&vpCopy, uboData, 64, 64); + *(int*)(uboData + 64) = numLights; + *(int*)(uboData + 68) = numShadowLights; - // 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; + for (int i = 0; i < numLights; i++) + { + var lp = new Vector4(lights[i].pos, lights[i].intensity); + var lc = new Vector4(lights[i].color, lights[i].range); + var lightOffset = 80 + i * 32; + System.Buffer.MemoryCopy(&lp, uboData + lightOffset, 16, 16); + System.Buffer.MemoryCopy(&lc, uboData + lightOffset + 16, 16, 16); + } + + // Shadow params per shadow light + for (int i = 0; i < numShadowLights; i++) + { + var sp = new Vector4(ShadowBias, ShadowSampleRadius, ShadowFarPlane, 0); + System.Buffer.MemoryCopy(&sp, uboData + 336 + i * 16, 16, 16); + } + + // Compute light view-proj for each shadow light + var lightViewProjs = new Matrix4x4[numShadowLights]; + for (int i = 0; i < numShadowLights; i++) + { + var lightView = Matrix4x4.CreateLookAt(lights[i].pos, Vector3.Zero, Vector3.UnitY); + var lightProj = Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 2f, 1.0f, 0.1f, ShadowFarPlane); + lightViewProjs[i] = lightView * lightProj; + } var drawCalls = new List<(VkBuffer vertexBuf, VkBuffer indexBuf, uint indexCount, Matrix4x4 model, bool castShadow)>(); @@ -142,10 +158,10 @@ public sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreensh drawCalls.Add((entry.vb.Buffer, entry.ib.Buffer, entry.indexCount, t.GetMatrix(), castShadow)); }); - Render(vp, drawCalls, uboData, lightPos, lightColor, lightViewProj); + Render(vp, drawCalls, uboData, lights, lightViewProjs, numShadowLights); } - private void Render(Matrix4x4 vp, List<(VkBuffer vertexBuf, VkBuffer indexBuf, uint indexCount, Matrix4x4 model, bool castShadow)> drawCalls, byte* uboData, Vector4 lightPos, Vector4 lightColor, Matrix4x4 lightViewProj) + private void Render(Matrix4x4 vp, List<(VkBuffer vertexBuf, VkBuffer indexBuf, uint indexCount, Matrix4x4 model, bool castShadow)> drawCalls, byte* uboData, List<(Vector3 pos, float intensity, Vector3 color, float range)> lights, Matrix4x4[] lightViewProjs, int numShadowLights) { _frameResources.WaitFrame(_frameIndex); @@ -157,7 +173,7 @@ public sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreensh { _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, lightViewProj); + Render(vp, drawCalls, uboData, lights, lightViewProjs, numShadowLights); return; } @@ -178,122 +194,123 @@ public sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreensh }; Vk.vkBeginCommandBuffer(cmd, &beginInfo); - // === SHADOW CUBEMAP PASSES (6 faces) === - // Transition color cube: Undefined → ColorAttachmentOptimal + // === SHADOW CUBEMAP ARRAY PASSES (numShadowLights × 6 faces) === + int totalLayers = numShadowLights * 6; TransitionImageLayout(cmd, _shadowMap.ColorImage, VkImageLayout.Undefined, VkImageLayout.ColorAttachmentOptimal, - 0, 0, 0x400, 0x100, 6); - // Transition depth cube: Undefined → DepthStencilAttachmentOptimal + 0, 0, 0x400, 0x100, (uint)totalLayers); TransitionImageLayoutDepth(cmd, _shadowMap.DepthImage, VkImageLayout.Undefined, VkImageLayout.DepthStencilAttachmentOptimal, - 0, 0, 0x100, 0x200, 6); + 0, 0, 0x100, 0x200, (uint)totalLayers); - var lightPosVec = new Vector3(lightPos.X, lightPos.Y, lightPos.Z); - - for (int face = 0; face < 6; face++) + for (int lightIdx = 0; lightIdx < numShadowLights; lightIdx++) { - var (faceView, faceProj) = VulkanShadowMap.GetFaceViewProj(lightPosVec, face); - var faceViewProj = faceView * faceProj; + var lightPosVec = lights[lightIdx].pos; - var colorClear = new VkClearValue + for (int face = 0; face < 6; face++) { - Color = new VkClearColorValue { Float0 = 1.0f, Float1 = 0, Float2 = 0, Float3 = 0 }, - }; + var (faceView, faceProj) = VulkanShadowMap.GetFaceViewProj(lightPosVec, face); + var faceViewProj = faceView * faceProj; + int faceIndex = lightIdx * 6 + face; - var shadowColorAttachment = new VkRenderingAttachmentInfo - { - sType = VkStructureType.RenderingAttachmentInfo, - imageView = _shadowMap.FaceColorViews[face], - imageLayout = VkImageLayout.ColorAttachmentOptimal, - loadOp = VkAttachmentLoadOp.Clear, - storeOp = VkAttachmentStoreOp.Store, - clearValue = colorClear, - }; + var colorClear = new VkClearValue + { + Color = new VkClearColorValue { Float0 = 1.0f, Float1 = 0, Float2 = 0, Float3 = 0 }, + }; - var shadowDepthClear = new VkClearValue - { - DepthStencil = new VkClearDepthStencilValue { Depth = 1.0f, Stencil = 0 }, - }; + var shadowColorAttachment = new VkRenderingAttachmentInfo + { + sType = VkStructureType.RenderingAttachmentInfo, + imageView = _shadowMap.FaceColorViews[faceIndex], + imageLayout = VkImageLayout.ColorAttachmentOptimal, + loadOp = VkAttachmentLoadOp.Clear, + storeOp = VkAttachmentStoreOp.Store, + clearValue = colorClear, + }; - var shadowDepthAttachment = new VkRenderingAttachmentInfo - { - sType = VkStructureType.RenderingAttachmentInfo, - imageView = _shadowMap.FaceDepthViews[face], - imageLayout = VkImageLayout.DepthStencilAttachmentOptimal, - loadOp = VkAttachmentLoadOp.Clear, - storeOp = VkAttachmentStoreOp.Store, - clearValue = shadowDepthClear, - }; + var shadowDepthClear = new VkClearValue + { + DepthStencil = new VkClearDepthStencilValue { Depth = 1.0f, Stencil = 0 }, + }; - var shadowRenderingInfo = new VkRenderingInfo - { - sType = VkStructureType.RenderingInfo, - renderArea = new VkRect2D + var shadowDepthAttachment = new VkRenderingAttachmentInfo + { + sType = VkStructureType.RenderingAttachmentInfo, + imageView = _shadowMap.FaceDepthViews[faceIndex], + 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 = 1, + pColorAttachments = &shadowColorAttachment, + 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 = 1, - pColorAttachments = &shadowColorAttachment, - pDepthAttachment = &shadowDepthAttachment, - }; + }; + Vk.vkCmdSetScissor(cmd, 0, 1, &shadowScissor); + Vk.vkCmdSetDepthBias(cmd, 1.75f, 0.0f, 2.5f); - 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.75f, 0.0f, 2.5f); - - foreach (var dc in drawCalls) - { - if (!dc.castShadow) continue; - - var vertexBuf = dc.vertexBuf; - ulong offset = 0; - Vk.vkCmdBindVertexBuffers(cmd, 0, 1, &vertexBuf, &offset); - Vk.vkCmdBindIndexBuffer(cmd, dc.indexBuf, 0, 1); - - var pcData = stackalloc byte[176]; - 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); + var lpVec4 = new Vector4(lightPosVec, lights[lightIdx].intensity); var sp = new Vector4(ShadowBias, ShadowSampleRadius, ShadowFarPlane, 0); - System.Buffer.MemoryCopy(&sp, pcData + 160, 16, 16); - Vk.vkCmdPushConstants(cmd, _shadowMap.PipelineLayout, VkShaderStageFlags.Vertex | VkShaderStageFlags.Fragment, 0, 176, 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 shadow push constants: model(64) + lightViewProj(64) + lightPos(16) + shadowParams(16) = 160 + var pcData = stackalloc byte[160]; + var modelCopy = dc.model; + System.Buffer.MemoryCopy(&modelCopy, pcData, 64, 64); + var lvpCopy = faceViewProj; + System.Buffer.MemoryCopy(&lvpCopy, pcData + 64, 64, 64); + System.Buffer.MemoryCopy(&lpVec4, pcData + 128, 16, 16); + System.Buffer.MemoryCopy(&sp, pcData + 144, 16, 16); + + 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 color cube: ColorAttachmentOptimal → ShaderReadOnlyOptimal + // Transition shadow color array: ColorAttachmentOptimal → ShaderReadOnlyOptimal TransitionImageLayout(cmd, _shadowMap.ColorImage, VkImageLayout.ColorAttachmentOptimal, VkImageLayout.ShaderReadOnlyOptimal, - 0x400, 0x100, 0x8, 0x20, 6); + 0x400, 0x100, 0x8, 0x20, (uint)totalLayers); // === MAIN PASS === TransitionImageLayout(cmd, _swapchain.Images[imageIndex], @@ -381,20 +398,9 @@ public sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreensh Vk.vkCmdBindVertexBuffers(cmd, 0, 1, &vertexBuf, &offset); Vk.vkCmdBindIndexBuffer(cmd, dc.indexBuf, 0, 1); - // Pack all push constants: model(64) + lightPos(16) + lightColor(16) + lightViewProj(64) = 160 - var pcData = stackalloc byte[176]; - 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); - var sp = new Vector4(ShadowBias, ShadowSampleRadius, ShadowFarPlane, 0); - System.Buffer.MemoryCopy(&sp, pcData + 160, 16, 16); - - Vk.vkCmdPushConstants(cmd, _pipeline.PipelineLayout, VkShaderStageFlags.Vertex | VkShaderStageFlags.Fragment, 0, 176, pcData); + // Main pass push constants: model only (64B) + var model = dc.model; + Vk.vkCmdPushConstants(cmd, _pipeline.PipelineLayout, VkShaderStageFlags.Vertex | VkShaderStageFlags.Fragment, 0, 64, &model); Vk.vkCmdDrawIndexed(cmd, dc.indexCount, 1, 0, 0, 0); } diff --git a/src/Engine.Graphics.Vulkan/VulkanShadowMap.cs b/src/Engine.Graphics.Vulkan/VulkanShadowMap.cs index 66d438b..b066b3d 100644 --- a/src/Engine.Graphics.Vulkan/VulkanShadowMap.cs +++ b/src/Engine.Graphics.Vulkan/VulkanShadowMap.cs @@ -5,17 +5,17 @@ namespace Engine.Graphics.Vulkan; internal sealed unsafe class VulkanShadowMap : IDisposable { - public const uint ShadowMapSize = 2048; - public const float FarPlane = 60.0f; + public const uint ShadowMapSize = 1024; + public const int MaxShadowLights = 4; public VkImage ColorImage; public VkDeviceMemory ColorImageMemory; - public VkImageView CubeColorView; - public VkImageView[] FaceColorViews = new VkImageView[6]; + public VkImageView CubeArrayColorView; + public VkImageView[] FaceColorViews = new VkImageView[MaxShadowLights * 6]; public VkImage DepthImage; public VkDeviceMemory DepthImageMemory; - public VkImageView[] FaceDepthViews = new VkImageView[6]; + public VkImageView[] FaceDepthViews = new VkImageView[MaxShadowLights * 6]; public VkSampler ShadowSampler; public VkPipeline Pipeline; @@ -35,12 +35,14 @@ internal sealed unsafe class VulkanShadowMap : IDisposable CreateShadowSampler(); CreateShadowPipeline(); - Console.WriteLine("[Vulkan] Shadow cubemap created (1024x1024x6, R32_SFLOAT + D32_SFLOAT)"); + Console.WriteLine($"[Vulkan] Shadow cubemap array created ({ShadowMapSize}x{ShadowMapSize}x{MaxShadowLights*6} layers)"); } private void CreateShadowImages() { - // Color cube (R32_SFLOAT) — stores linear distance + int totalLayers = MaxShadowLights * 6; + + // Color cube array (R32_SFLOAT) — stores linear distance var colorInfo = new VkImageCreateInfo { sType = VkStructureType.ImageCreateInfo, @@ -49,7 +51,7 @@ internal sealed unsafe class VulkanShadowMap : IDisposable format = VkFormat.R32Sfloat, extent = new VkExtent3D { Width = ShadowMapSize, Height = ShadowMapSize, Depth = 1 }, mipLevels = 1, - arrayLayers = 6, + arrayLayers = (uint)totalLayers, samples = VkSampleCountFlags.Count1, tiling = VkImageTiling.Optimal, usage = VkImageUsageFlags.ColorAttachment | VkImageUsageFlags.Sampled, @@ -70,22 +72,22 @@ internal sealed unsafe class VulkanShadowMap : IDisposable ColorImageMemory = colorMem; Vk.vkBindImageMemory(_device, ColorImage, ColorImageMemory, 0); - // Cube color view for sampling - var cubeColorViewInfo = new VkImageViewCreateInfo + // Cube array color view for sampling + var cubeArrayViewInfo = new VkImageViewCreateInfo { sType = VkStructureType.ImageViewCreateInfo, image = ColorImage, - viewType = VkImageViewType.TypeCube, + viewType = VkImageViewType.TypeCubeArray, format = VkFormat.R32Sfloat, components = new VkComponentMapping { R = VkComponentSwizzle.Identity, G = VkComponentSwizzle.Identity, B = VkComponentSwizzle.Identity, A = VkComponentSwizzle.Identity }, - subresourceRange = new VkImageSubresourceRange { AspectMask = VkImageAspectFlags.Color, BaseMipLevel = 0, LevelCount = 1, BaseArrayLayer = 0, LayerCount = 6 }, + subresourceRange = new VkImageSubresourceRange { AspectMask = VkImageAspectFlags.Color, BaseMipLevel = 0, LevelCount = 1, BaseArrayLayer = 0, LayerCount = (uint)totalLayers }, }; - var cubeCV = VkImageView.Null; - Vk.vkCreateImageView(_device, &cubeColorViewInfo, 0, &cubeCV); - CubeColorView = cubeCV; + var cubeArrayView = VkImageView.Null; + Vk.vkCreateImageView(_device, &cubeArrayViewInfo, 0, &cubeArrayView); + CubeArrayColorView = cubeArrayView; - // 6 face color views - for (int i = 0; i < 6; i++) + // Face color views for rendering + for (int i = 0; i < totalLayers; i++) { var faceViewInfo = new VkImageViewCreateInfo { @@ -101,7 +103,7 @@ internal sealed unsafe class VulkanShadowMap : IDisposable FaceColorViews[i] = fv; } - // Depth cube (D32_SFLOAT) — for depth testing during shadow render + // Depth cube array (D32_SFLOAT) var depthInfo = new VkImageCreateInfo { sType = VkStructureType.ImageCreateInfo, @@ -110,7 +112,7 @@ internal sealed unsafe class VulkanShadowMap : IDisposable format = VkFormat.D32Sfloat, extent = new VkExtent3D { Width = ShadowMapSize, Height = ShadowMapSize, Depth = 1 }, mipLevels = 1, - arrayLayers = 6, + arrayLayers = (uint)totalLayers, samples = VkSampleCountFlags.Count1, tiling = VkImageTiling.Optimal, usage = VkImageUsageFlags.DepthStencilAttachment, @@ -131,8 +133,8 @@ internal sealed unsafe class VulkanShadowMap : IDisposable DepthImageMemory = depthMem; Vk.vkBindImageMemory(_device, DepthImage, DepthImageMemory, 0); - // 6 face depth views - for (int i = 0; i < 6; i++) + // Face depth views + for (int i = 0; i < totalLayers; i++) { var faceDepthViewInfo = new VkImageViewCreateInfo { @@ -323,7 +325,7 @@ internal sealed unsafe class VulkanShadowMap : IDisposable pDynamicStates = dynamicStates, }; - var pushConstantRange = new VkPushConstantRange { stageFlags = VkShaderStageFlags.Vertex | VkShaderStageFlags.Fragment, offset = 0, size = 176 }; + var pushConstantRange = new VkPushConstantRange { stageFlags = VkShaderStageFlags.Vertex | VkShaderStageFlags.Fragment, offset = 0, size = 160 }; var layoutInfo = new VkPipelineLayoutCreateInfo { @@ -415,8 +417,8 @@ 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 (CubeColorView.Handle != 0) Vk.vkDestroyImageView(_device, CubeColorView, 0); - for (int i = 0; i < 6; i++) + if (CubeArrayColorView.Handle != 0) Vk.vkDestroyImageView(_device, CubeArrayColorView, 0); + for (int i = 0; i < MaxShadowLights * 6; i++) { if (FaceColorViews[i].Handle != 0) Vk.vkDestroyImageView(_device, FaceColorViews[i], 0); if (FaceDepthViews[i].Handle != 0) Vk.vkDestroyImageView(_device, FaceDepthViews[i], 0);