Files
emil28092005 f761973443 fix: Vulkan cubemap face directions — Y-up vectors corrected
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
2026-06-18 21:32:18 +03:00

108 lines
3.8 KiB
C#

using System.Numerics;
namespace Engine.Tests;
public class ShadowMapFaceDirectionTests
{
[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
[InlineData(5)]
public void All_Six_Faces_Produce_Valid_View_Matrices(int face)
{
// Test the face direction logic directly (without VulkanShadowMap class)
var lightPos = new Vector3(3, 7, -2);
var (view, proj) = ComputeFaceViewProj(lightPos, face);
Assert.True(!float.IsNaN(view.M11));
Assert.True(!float.IsNaN(proj.M11));
Assert.True(!float.IsInfinity(view.M11));
Assert.True(!float.IsInfinity(proj.M11));
}
[Theory]
[InlineData(0, 1, 0, 0)]
[InlineData(1, -1, 0, 0)]
[InlineData(2, 0, 1, 0)]
[InlineData(3, 0, -1, 0)]
[InlineData(4, 0, 0, 1)]
[InlineData(5, 0, 0, -1)]
public void Face_Target_Is_LightPos_Plus_Direction(int face, float dx, float dy, float dz)
{
var lightPos = new Vector3(5, 10, 3);
var (view, _) = ComputeFaceViewProj(lightPos, face);
// View matrix transforms lightPos to origin
var origin = Vector3.Transform(lightPos, view);
Assert.Equal(0f, origin.X, 0.001f);
Assert.Equal(0f, origin.Y, 0.001f);
Assert.Equal(0f, origin.Z, 0.001f);
}
[Fact]
public void All_Faces_Have_90_Degrees_FOV()
{
for (int face = 0; face < 6; face++)
{
var (_, proj) = ComputeFaceViewProj(Vector3.Zero, face);
// 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_Positive_Z_Up()
{
var lightPos = new Vector3(0, 5, 0);
var (view, _) = ComputeFaceViewProj(lightPos, 2);
// +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_Negative_Z_Up()
{
var lightPos = new Vector3(0, 5, 0);
var (view, _) = ComputeFaceViewProj(lightPos, 3);
// -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]
public void FarPlane_Matches_Between_Projection_And_Shader()
{
// 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: 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);
var target = lightPos;
Vector3 up;
switch (face)
{
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);
}
}