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
This commit is contained in:
@@ -173,24 +173,43 @@ internal sealed unsafe class VulkanShadowMap : IDisposable
|
|||||||
public static (Matrix4x4 view, Matrix4x4 proj) GetFaceViewProj(Vector3 lightPos, int face)
|
public static (Matrix4x4 view, Matrix4x4 proj) GetFaceViewProj(Vector3 lightPos, int face)
|
||||||
{
|
{
|
||||||
var proj = Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 2f, 1.0f, 0.1f, 60f);
|
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 target = lightPos;
|
||||||
var up = Vector3.UnitY;
|
Vector3 up;
|
||||||
|
|
||||||
target += face switch
|
switch (face)
|
||||||
{
|
{
|
||||||
0 => Vector3.UnitX,
|
case 0: // +X
|
||||||
1 => -Vector3.UnitX,
|
target += Vector3.UnitX;
|
||||||
2 => Vector3.UnitY,
|
up = new Vector3(0, -1, 0);
|
||||||
3 => -Vector3.UnitY,
|
break;
|
||||||
4 => Vector3.UnitZ,
|
case 1: // -X
|
||||||
5 => -Vector3.UnitZ,
|
target += -Vector3.UnitX;
|
||||||
_ => Vector3.UnitZ,
|
up = new Vector3(0, -1, 0);
|
||||||
};
|
break;
|
||||||
|
case 2: // +Y
|
||||||
if (face == 2) up = -Vector3.UnitZ;
|
target += Vector3.UnitY;
|
||||||
else if (face == 3) up = Vector3.UnitZ;
|
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);
|
var view = Matrix4x4.CreateLookAt(lightPos, target, up);
|
||||||
return (view, proj);
|
return (view, proj);
|
||||||
|
|||||||
@@ -48,32 +48,29 @@ public class ShadowMapFaceDirectionTests
|
|||||||
for (int face = 0; face < 6; face++)
|
for (int face = 0; face < 6; face++)
|
||||||
{
|
{
|
||||||
var (_, proj) = ComputeFaceViewProj(Vector3.Zero, face);
|
var (_, proj) = ComputeFaceViewProj(Vector3.Zero, face);
|
||||||
// FOV=90°, aspect=1 → M22 = 1/tan(45°) = 1, then *= -1 → -1... wait
|
// FOV=90°, aspect=1 → M22 = 1/tan(PI/4) = 1 (no M22 flip for shadow cubemap)
|
||||||
// CreatePerspectiveFieldOfView(PI/2, 1, n, f) → M22 = 1/tan(PI/4) = 1
|
Assert.Equal(1f, proj.M22, 0.001f);
|
||||||
// Then M22 *= -1 → M22 = -1
|
|
||||||
Assert.Equal(-1f, proj.M22, 0.001f);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[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 lightPos = new Vector3(0, 5, 0);
|
||||||
var (view, _) = ComputeFaceViewProj(lightPos, 2);
|
var (view, _) = ComputeFaceViewProj(lightPos, 2);
|
||||||
// +Y face: up = -Z
|
// +Y face: up = +Z (Vulkan cubemap convention)
|
||||||
var negZ = Vector3.Transform(new Vector3(0, 0, -1), view);
|
var posZ = Vector3.Transform(new Vector3(0, 0, 1), view);
|
||||||
// Should have positive Y in view space (up direction)
|
Assert.True(posZ.Y > 0, $"Face 2 up should map +Z to +Y view space, got {posZ}");
|
||||||
Assert.True(negZ.Y > 0, $"Face 2 up should map -Z to +Y view space, got {negZ}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[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 lightPos = new Vector3(0, 5, 0);
|
||||||
var (view, _) = ComputeFaceViewProj(lightPos, 3);
|
var (view, _) = ComputeFaceViewProj(lightPos, 3);
|
||||||
// -Y face: up = +Z
|
// -Y face: up = -Z (Vulkan cubemap convention)
|
||||||
var posZ = Vector3.Transform(new Vector3(0, 0, 1), view);
|
var negZ = 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}");
|
Assert.True(negZ.Y > 0, $"Face 3 up should map -Z to +Y view space, got {negZ}");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -82,31 +79,27 @@ public class ShadowMapFaceDirectionTests
|
|||||||
// The shader hardcodes FAR_PLANE = 60.0 and divides by it
|
// The shader hardcodes FAR_PLANE = 60.0 and divides by it
|
||||||
// The projection must use the same far plane
|
// The projection must use the same far plane
|
||||||
var (_, proj) = ComputeFaceViewProj(Vector3.Zero, 0);
|
var (_, proj) = ComputeFaceViewProj(Vector3.Zero, 0);
|
||||||
// M33 for perspective with M22 *= -1: should be negative
|
// M33 for perspective: should be negative (far/near-far)
|
||||||
Assert.True(proj.M33 < 0, $"M33 should be negative for Vulkan projection, got {proj.M33}");
|
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)
|
static (Matrix4x4 view, Matrix4x4 proj) ComputeFaceViewProj(Vector3 lightPos, int face)
|
||||||
{
|
{
|
||||||
var proj = Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 2f, 1.0f, 0.1f, 60f);
|
var proj = Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 2f, 1.0f, 0.1f, 60f);
|
||||||
proj.M22 *= -1;
|
|
||||||
|
|
||||||
var target = lightPos;
|
var target = lightPos;
|
||||||
var up = Vector3.UnitY;
|
Vector3 up;
|
||||||
|
|
||||||
target += face switch
|
switch (face)
|
||||||
{
|
{
|
||||||
0 => Vector3.UnitX,
|
case 0: target += Vector3.UnitX; up = new Vector3(0, -1, 0); break;
|
||||||
1 => -Vector3.UnitX,
|
case 1: target += -Vector3.UnitX; up = new Vector3(0, -1, 0); break;
|
||||||
2 => Vector3.UnitY,
|
case 2: target += Vector3.UnitY; up = new Vector3(0, 0, 1); break;
|
||||||
3 => -Vector3.UnitY,
|
case 3: target += -Vector3.UnitY; up = new Vector3(0, 0, -1); break;
|
||||||
4 => Vector3.UnitZ,
|
case 4: target += Vector3.UnitZ; up = new Vector3(0, -1, 0); break;
|
||||||
5 => -Vector3.UnitZ,
|
case 5: target += -Vector3.UnitZ; up = new Vector3(0, -1, 0); break;
|
||||||
_ => Vector3.UnitZ,
|
default: target += Vector3.UnitZ; up = new Vector3(0, -1, 0); break;
|
||||||
};
|
}
|
||||||
|
|
||||||
if (face == 2) up = -Vector3.UnitZ;
|
|
||||||
else if (face == 3) up = Vector3.UnitZ;
|
|
||||||
|
|
||||||
var view = Matrix4x4.CreateLookAt(lightPos, target, up);
|
var view = Matrix4x4.CreateLookAt(lightPos, target, up);
|
||||||
return (view, proj);
|
return (view, proj);
|
||||||
|
|||||||
Reference in New Issue
Block a user