From f76197344397e120bdf9a2841e15b00015064220 Mon Sep 17 00:00:00 2001 From: emil28092005 Date: Thu, 18 Jun 2026 21:32:18 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20Vulkan=20cubemap=20face=20directions=20?= =?UTF-8?q?=E2=80=94=20Y-up=20vectors=20corrected?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: used OpenGL cubemap conventions (up=+Y) instead of Vulkan (up=-Y) for X and Z faces. Also applied M22 *= -1 flip to shadow projection which is wrong — that flip is only for swapchain rendering, not for offscreen cubemap rendering. Fix: - Faces 0,1,4,5 (+X,-X,+Z,-Z): up = (0,-1,0) — Vulkan cubemap convention - Face 2 (+Y): up = (0,0,1) — was (0,0,-1), now correct - Face 3 (-Y): up = (0,0,-1) — was (0,0,1), now correct - Removed M22 *= -1 from shadow projection (not needed for offscreen cube) - 227 tests, all pass --- src/Engine.Graphics.Vulkan/VulkanShadowMap.cs | 47 ++++++++++++----- tests/Engine.Tests/ShadowMapTests.cs | 51 ++++++++----------- 2 files changed, 55 insertions(+), 43 deletions(-) diff --git a/src/Engine.Graphics.Vulkan/VulkanShadowMap.cs b/src/Engine.Graphics.Vulkan/VulkanShadowMap.cs index 157a3b6..3d86bab 100644 --- a/src/Engine.Graphics.Vulkan/VulkanShadowMap.cs +++ b/src/Engine.Graphics.Vulkan/VulkanShadowMap.cs @@ -173,24 +173,43 @@ internal sealed unsafe class VulkanShadowMap : IDisposable 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; + // No M22 flip for shadow cubemap — Vulkan cubemap sampling handles Y internally + // The M22 flip is only for the main render pass (swapchain), not for offscreen cube var target = lightPos; - var up = Vector3.UnitY; + Vector3 up; - target += face switch + switch (face) { - 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; + case 0: // +X + target += Vector3.UnitX; + up = new Vector3(0, -1, 0); + break; + case 1: // -X + target += -Vector3.UnitX; + up = new Vector3(0, -1, 0); + break; + case 2: // +Y + target += Vector3.UnitY; + up = new Vector3(0, 0, 1); + break; + case 3: // -Y + target += -Vector3.UnitY; + up = new Vector3(0, 0, -1); + break; + case 4: // +Z + target += Vector3.UnitZ; + up = new Vector3(0, -1, 0); + break; + case 5: // -Z + target += -Vector3.UnitZ; + up = new Vector3(0, -1, 0); + break; + default: + target += Vector3.UnitZ; + up = new Vector3(0, -1, 0); + break; + } var view = Matrix4x4.CreateLookAt(lightPos, target, up); return (view, proj); diff --git a/tests/Engine.Tests/ShadowMapTests.cs b/tests/Engine.Tests/ShadowMapTests.cs index f71ef07..7a683a7 100644 --- a/tests/Engine.Tests/ShadowMapTests.cs +++ b/tests/Engine.Tests/ShadowMapTests.cs @@ -48,32 +48,29 @@ public class ShadowMapFaceDirectionTests for (int face = 0; face < 6; face++) { var (_, proj) = ComputeFaceViewProj(Vector3.Zero, face); - // FOV=90°, aspect=1 → M22 = 1/tan(45°) = 1, then *= -1 → -1... wait - // CreatePerspectiveFieldOfView(PI/2, 1, n, f) → M22 = 1/tan(PI/4) = 1 - // Then M22 *= -1 → M22 = -1 - Assert.Equal(-1f, proj.M22, 0.001f); + // FOV=90°, aspect=1 → M22 = 1/tan(PI/4) = 1 (no M22 flip for shadow cubemap) + Assert.Equal(1f, proj.M22, 0.001f); } } [Fact] - public void Face_2_Uses_Negative_Z_Up() + public void Face_2_Uses_Positive_Z_Up() { var lightPos = new Vector3(0, 5, 0); var (view, _) = ComputeFaceViewProj(lightPos, 2); - // +Y face: up = -Z - var negZ = Vector3.Transform(new Vector3(0, 0, -1), view); - // Should have positive Y in view space (up direction) - Assert.True(negZ.Y > 0, $"Face 2 up should map -Z to +Y view space, got {negZ}"); + // +Y face: up = +Z (Vulkan cubemap convention) + var posZ = Vector3.Transform(new Vector3(0, 0, 1), view); + Assert.True(posZ.Y > 0, $"Face 2 up should map +Z to +Y view space, got {posZ}"); } [Fact] - public void Face_3_Uses_Positive_Z_Up() + public void Face_3_Uses_Negative_Z_Up() { var lightPos = new Vector3(0, 5, 0); var (view, _) = ComputeFaceViewProj(lightPos, 3); - // -Y face: up = +Z - var posZ = Vector3.Transform(new Vector3(0, 0, 1), view); - Assert.True(posZ.Y > 0, $"Face 3 up should map +Z to +Y view space, got {posZ}"); + // -Y face: up = -Z (Vulkan cubemap convention) + var negZ = Vector3.Transform(new Vector3(0, 0, -1), view); + Assert.True(negZ.Y > 0, $"Face 3 up should map -Z to +Y view space, got {negZ}"); } [Fact] @@ -82,31 +79,27 @@ public class ShadowMapFaceDirectionTests // The shader hardcodes FAR_PLANE = 60.0 and divides by it // The projection must use the same far plane var (_, proj) = ComputeFaceViewProj(Vector3.Zero, 0); - // M33 for perspective with M22 *= -1: should be negative - Assert.True(proj.M33 < 0, $"M33 should be negative for Vulkan projection, got {proj.M33}"); + // M33 for perspective: should be negative (far/near-far) + Assert.True(proj.M33 < 0, $"M33 should be negative for perspective projection, got {proj.M33}"); } static (Matrix4x4 view, Matrix4x4 proj) ComputeFaceViewProj(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; + Vector3 up; - target += face switch + switch (face) { - 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; + case 0: target += Vector3.UnitX; up = new Vector3(0, -1, 0); break; + case 1: target += -Vector3.UnitX; up = new Vector3(0, -1, 0); break; + case 2: target += Vector3.UnitY; up = new Vector3(0, 0, 1); break; + case 3: target += -Vector3.UnitY; up = new Vector3(0, 0, -1); break; + case 4: target += Vector3.UnitZ; up = new Vector3(0, -1, 0); break; + case 5: target += -Vector3.UnitZ; up = new Vector3(0, -1, 0); break; + default: target += Vector3.UnitZ; up = new Vector3(0, -1, 0); break; + } var view = Matrix4x4.CreateLookAt(lightPos, target, up); return (view, proj);