From 10cb31223f8382b0d7325385e9e252469c5f2356 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 23 Jun 2026 09:00:27 +0200 Subject: [PATCH] =?UTF-8?q?feat(D.2b):=20Slice=202=20=E2=80=94=20DollCamer?= =?UTF-8?q?a=20(fixed=20paperdoll=20ICamera)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- src/AcDream.App/Rendering/DollCamera.cs | 37 +++++++++++++++++++ .../Rendering/DollCameraTests.cs | 27 ++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 src/AcDream.App/Rendering/DollCamera.cs create mode 100644 tests/AcDream.App.Tests/Rendering/DollCameraTests.cs diff --git a/src/AcDream.App/Rendering/DollCamera.cs b/src/AcDream.App/Rendering/DollCamera.cs new file mode 100644 index 00000000..b2ba8ff4 --- /dev/null +++ b/src/AcDream.App/Rendering/DollCamera.cs @@ -0,0 +1,37 @@ +using System.Numerics; + +namespace AcDream.App.Rendering; + +/// +/// Fixed camera for the paperdoll mini-scene. Eye and look-at from +/// gmPaperDollUI::PostInit (decomp lines 175521–175527): +/// eye (0.12, -2.4, 0.88) looking at the origin, AC up-axis = +Z. +/// +/// Uses the same / +/// convention as +/// so the doll's triangle winding and back-face +/// culling match the world render pass. +/// +/// Per-race camera distance (UpdateForRace) is deferred polish. +/// +public sealed class DollCamera : ICamera +{ + private static readonly Vector3 Eye = new(0.12f, -2.4f, 0.88f); + private static readonly Vector3 Target = Vector3.Zero; + private static readonly Vector3 Up = Vector3.UnitZ; // AC up-axis = +Z, same as ChaseCamera + + /// Vertical field of view in radians. ~34° default; tune at the visual gate. + public float FovRadians { get; set; } = 0.6f; + + public float Near { get; set; } = 0.1f; // same near plane as ChaseCamera / retail znear + public float Far { get; set; } = 50f; // doll scene is small; 50 m is ample + public float Aspect { get; set; } = 1f; + + /// + public Matrix4x4 View => + Matrix4x4.CreateLookAt(Eye, Target, Up); + + /// + public Matrix4x4 Projection => + Matrix4x4.CreatePerspectiveFieldOfView(FovRadians, Aspect <= 0f ? 1f : Aspect, Near, Far); +} diff --git a/tests/AcDream.App.Tests/Rendering/DollCameraTests.cs b/tests/AcDream.App.Tests/Rendering/DollCameraTests.cs new file mode 100644 index 00000000..c65afdfb --- /dev/null +++ b/tests/AcDream.App.Tests/Rendering/DollCameraTests.cs @@ -0,0 +1,27 @@ +using System.Numerics; +using AcDream.App.Rendering; +using Xunit; + +namespace AcDream.App.Tests.Rendering; + +public class DollCameraTests +{ + [Fact] + public void Eye_position_is_the_retail_camera_offset() + { + var cam = new DollCamera { Aspect = 100f / 214f }; + Assert.True(Matrix4x4.Invert(cam.View, out var inv)); + var eye = inv.Translation; + Assert.Equal(0.12f, eye.X, 3); + Assert.Equal(-2.4f, eye.Y, 3); + Assert.Equal(0.88f, eye.Z, 3); + } + + [Fact] + public void Projection_is_finite_and_uses_aspect() + { + var cam = new DollCamera { Aspect = 1.5f }; + Assert.True(float.IsFinite(cam.Projection.M11)); + Assert.NotEqual(0f, cam.Projection.M34); // perspective w = -z term + } +}