fix(D.2b): Slice 2 — retail-exact doll pose, camera + heading (visual gate)

The paperdoll doll now matches retail: correct held pose, framing, and
facing. Three decomp-sourced fixes closed the visual gate.

Pose: cdb-confirmed m_didAnimation = 0x030003C0 (gmPaperDollUI), played
once + HELD (set_sequence_animation framerate=0, RedressCreature
0x004a3c22). Dumping the dat showed the 29-frame anim has only two
distinct keyframes — frame 0 (transitional, bent arm) and frames 1..28
(byte-identical: the settled stance, arms down + leg back) — so
ApplyPaperdollPose applies the LAST frame statically (no looping).

Camera: ported verbatim from UIElement_Viewport::SetCamera (decomp
0x004a5a39). position (0.12,-2.4,0.88); direction (0,0,0) => IDENTITY
view frame => look straight down +Y, ZERO yaw; FOV pi/4 (CreatureMode
ctor default 0x004543cf); ambient 0.3. The prior hand-tune aimed the
camera at mid-body, adding a ~2deg yaw that turned the doll's face away
— full-body framing comes from eye-height + FOV, not aiming.

Heading: retail Frame::set_heading(h) (0x00535e40) builds facing
(sin h, cos h); System.Numerics CreateFromAxisAngle(+Z, +h) rotates the
body's default +Y forward to (-sin h, cos h) — the X-lean was MIRRORED
(~22deg), the real cause of the turned-away face. Negate the angle to
land on retail's facing.

Wrap-up: stripped the temporary O/P pose-frame stepper + Slice2
diagnostics; divergence register AP-66 reworded, AP-67 (RTT doll render
vs in-cell CreatureMode::Render) + AP-68 (per-race UpdateForRace
unimpl) added; DollCameraTests pinned to the retail values + a zero-yaw
guard. tools/cdb/paperdoll-pose.cdb = the pose-DID capture script.
Build + full suite green (Core 1579 / Core.Net 343 / App 597 / UI 425).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-25 13:16:27 +02:00
parent 594942f127
commit 8fa66c23d5
8 changed files with 136 additions and 37 deletions

View file

@ -1,35 +1,46 @@
using System;
using System.Numerics;
namespace AcDream.App.Rendering;
/// <summary>
/// Fixed camera for the paperdoll mini-scene. Derived from <c>gmPaperDollUI::PostInit</c>
/// (decomp lines 175521175527: eye offset ~(0.12, -2.4, 0.88) about the origin), but with the
/// look-at raised to mid-body and the camera pulled back after the first visual gate: the doll's
/// model origin is at the FEET (z=0), so aiming at the origin framed only the lower legs. We aim
/// at ~0.95 m (a human's vertical centre) and stand back ~3.7 m so the whole ~1.9 m figure fits.
/// AC up-axis = +Z.
/// Fixed camera for the paperdoll mini-scene — retail-exact, ported from the gmPaperDollUI viewport
/// setup (decomp 0x004a5a390x004a5a69). The viewport (element <c>0x100001d5</c>) is configured by
/// <c>UIElement_Viewport::SetCamera(position, direction)</c> with:
/// <list type="bullet">
/// <item>position = (0.12, 2.4, 0.88) [hex 0x3df5c28f, 0xc019999a, 0x3f6147ae]</item>
/// <item>direction = (0, 0, 0) ⇒ <c>CreatureMode::SetCameraDirection</c> resets the view frame to
/// IDENTITY (<c>euler_set_rotate(0,0,0)</c> then <c>rotate(0,0,0)</c>), so the camera looks
/// straight down +Y with +Z up — NO yaw, NO pitch.</item>
/// </list>
/// The doll is framed purely by camera POSITION + FOV, NOT by aiming at the body: at distance 2.4 with
/// a π/4 (45°) vertical FOV the visible vertical band is z≈[0.11, 1.87], which covers the whole ~1.6 m
/// figure even though the model origin sits at the FEET (z=0). FOV π/4 is <c>CreatureMode</c>'s default
/// <c>m_fFOVRadians</c> (ctor 0x004543cf, hex 0x3f490fdb); default ambient is (0.3,0.3,0.3); the
/// paperdoll uses <c>UseSharpMode</c> (not SmartboxFOV) so <c>Render::SetFOVRad(m_fFOVRadians)</c> applies.
///
/// Uses the same <see cref="Matrix4x4.CreateLookAt"/> /
/// <see cref="Matrix4x4.CreatePerspectiveFieldOfView"/> convention as
/// <see cref="ChaseCamera"/> so the doll's triangle winding and back-face
/// culling match the world render pass.
/// <para>An earlier hand-tune aimed the camera at mid-body to "fit the figure", which introduced a
/// spurious yaw (Target.x ≠ Eye.x) that rotated the view and turned the doll's face away from the
/// viewer. This restores retail's zero-yaw frame, where full-body framing comes from the eye height +
/// FOV, not from aiming.</para>
///
/// Exact per-race camera framing (<c>UpdateForRace</c>) is deferred polish.
/// Uses the same <see cref="Matrix4x4.CreateLookAt"/> / <see cref="Matrix4x4.CreatePerspectiveFieldOfView"/>
/// convention as <see cref="ChaseCamera"/> so the doll's triangle winding + back-face culling match the
/// world render pass. AC up-axis = +Z.
/// </summary>
public sealed class DollCamera : ICamera
{
// Body centre height (metres) the camera aims at — model origin is at the feet (z=0).
private const float BodyCentreZ = 0.95f;
private static readonly Vector3 Eye = new(0.12f, -3.7f, BodyCentreZ);
private static readonly Vector3 Target = new(0f, 0f, BodyCentreZ);
// Retail paperdoll camera origin (decomp 0x004a5a510x004a5a61).
private static readonly Vector3 Eye = new(0.12f, -2.4f, 0.88f);
// Identity view orientation ⇒ look straight down +Y (no yaw/pitch). Target = Eye + (0,1,0).
private static readonly Vector3 Target = new(0.12f, -1.4f, 0.88f);
private static readonly Vector3 Up = Vector3.UnitZ; // AC up-axis = +Z, same as ChaseCamera
/// <summary>Vertical field of view in radians. ~34° default; tune at the visual gate.</summary>
public float FovRadians { get; set; } = 0.6f;
/// <summary>Vertical field of view — retail <c>CreatureMode</c> default <c>m_fFOVRadians</c> = π/4 (45°).</summary>
public float FovRadians { get; set; } = MathF.PI / 4f;
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 Far { get; set; } = 50f; // doll scene is small; 50 m is ample
public float Aspect { get; set; } = 1f;
/// <inheritdoc/>