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:
parent
594942f127
commit
8fa66c23d5
8 changed files with 136 additions and 37 deletions
|
|
@ -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 175521–175527: 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 0x004a5a39–0x004a5a69). 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 0x004a5a51–0x004a5a61).
|
||||
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/>
|
||||
|
|
|
|||
|
|
@ -45,9 +45,13 @@ public static class DollEntityBuilder
|
|||
/// </summary>
|
||||
public const uint DollRenderId = 0xDA11_D012u;
|
||||
|
||||
// retail set_heading: 191.367905° about +Z, expressed as a Quaternion
|
||||
// (axis-angle, axis = (0,0,1), θ = 191.367905° in radians).
|
||||
private static readonly float _headingRad = 191.367905f * (MathF.PI / 180f);
|
||||
// retail RedressCreature: CPhysicsObj::set_heading(191.367905°) (decomp 0x004a3c0a). Frame::set_heading(h)
|
||||
// (decomp 0x00535e40) builds the facing vector as (sin h°, cos h°, 0): at h=0 the creature faces +Y (AC
|
||||
// North), the heading increasing CLOCKWISE toward +X. We rotate the doll's default +Y-forward body about
|
||||
// +Z; System.Numerics rotates +Y → (−sin θ, cos θ), so to land on retail's (sin h, cos h) the angle is
|
||||
// NEGATED (θ = −h). Using +h mirrors the X-lean (~22° off → the face reads as turned away from the viewer).
|
||||
private const float _headingDegrees = 191.367905f;
|
||||
private static readonly float _headingRad = -_headingDegrees * (MathF.PI / 180f);
|
||||
private static readonly Quaternion _dollRotation =
|
||||
Quaternion.CreateFromAxisAngle(new Vector3(0f, 0f, 1f), _headingRad);
|
||||
|
||||
|
|
|
|||
|
|
@ -3816,13 +3816,82 @@ public sealed class GameWindow : IDisposable
|
|||
foreach (var p in pe.PartOverrides) parts.Add((p.PartIndex, p.GfxObjId));
|
||||
}
|
||||
|
||||
var meshRefsCopy = new List<AcDream.Core.World.MeshRef>(pe.MeshRefs); // frozen-pose snapshot
|
||||
var meshRefsCopy = new List<AcDream.Core.World.MeshRef>(pe.MeshRefs); // dressed parts (player's live frame)
|
||||
var doll = AcDream.App.Rendering.DollEntityBuilder.Build(
|
||||
pe.SourceGfxObjOrSetupId, meshRefsCopy, basePal, subs, parts);
|
||||
ApplyPaperdollPose(doll, pe.SourceGfxObjOrSetupId); // re-pose into the paperdoll "posing" stance
|
||||
_paperdollViewportRenderer.SetDoll(doll);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve the paperdoll "posing" stance DID (the model pose — left leg back) exactly as retail:
|
||||
/// <c>gmPaperDollUI</c> ctor (decomp 174243) sets <c>m_didAnimation = DBObj::GetDIDByEnum(0x10000005, 7)</c>,
|
||||
/// which is <c>DBCache::GetDIDFromEnumStatic</c> (decomp 20380) = <c>master[MasterMapId][0x10000005]</c>
|
||||
/// → submap <c>0x25000009</c> → key <c>7</c>. (Icon effects use keys 1-6 of the same submap; key 7 is the
|
||||
/// paperdoll pose.) Per-race <c>UpdateForRace</c> override deferred — the ctor default applies to all
|
||||
/// body-types for now. Returns 0 if the chain can't resolve.
|
||||
/// </summary>
|
||||
private uint ResolvePaperdollPoseDid()
|
||||
{
|
||||
var dats = _dats;
|
||||
if (dats is null) return 0u;
|
||||
uint masterDid = (uint)dats.Portal.Header.MasterMapId;
|
||||
if (masterDid == 0) return 0u;
|
||||
if (!dats.Portal.TryGet<DatReaderWriter.DBObjs.EnumIDMap>(masterDid, out var master)) return 0u;
|
||||
// DBCache::GetDIDFromEnum (decomp 20380) resolves master[arg4] → submap, then submap[arg3].
|
||||
// GetDIDFromEnumStatic(0x10000005, 7) ⇒ arg3=0x10000005, arg4=7 ⇒ master[7] → submap, submap[0x10000005].
|
||||
if (!master.ClientEnumToID.TryGetValue(7u, out var subDid)) return 0u; // master-level key = 7
|
||||
if (!dats.Portal.TryGet<DatReaderWriter.DBObjs.EnumIDMap>(subDid, out var sub)) return 0u;
|
||||
return sub.ClientEnumToID.TryGetValue(0x10000005u, out var did) ? did : 0u; // submap index = 0x10000005
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-pose the doll's already-dressed parts into the paperdoll "posing" stance — the C# analog of
|
||||
/// retail <c>set_sequence_animation(doll, m_didAnimation, …)</c>. Keeps each cloned part's GfxObjId +
|
||||
/// surface overrides (the dressed appearance), but replaces its transform with the pose animation's
|
||||
/// frame-0 per-part transform, using the same Scale·Rotate·Translate composition as the per-frame
|
||||
/// animation tick (<see cref="TickAnimations"/>, GameWindow.cs ~9999). Static (the pose is fixed). If the
|
||||
/// DID does not resolve to an <c>Animation</c>, the doll keeps its cloned pose (no regression) — the log
|
||||
/// line surfaces what resolved so the pose is verified, not guessed.
|
||||
/// </summary>
|
||||
private void ApplyPaperdollPose(AcDream.Core.World.WorldEntity doll, uint setupId)
|
||||
{
|
||||
var dats = _dats;
|
||||
if (dats is null) return;
|
||||
// Retail's paperdoll doll is STATIC — it holds the pose animation's frame, it does NOT loop.
|
||||
_animatedEntities.Remove(doll.Id);
|
||||
// poseDID is cdb-CONFIRMED = 0x030003C0 (== retail gmPaperDollUI m_didAnimation for Horan; UpdateForRace
|
||||
// did not override). Crash guard: only load a 0x03xxxxxx (Animation) DID (a non-animation id parses a
|
||||
// garbage frame count → OOM).
|
||||
uint poseDid = ResolvePaperdollPoseDid();
|
||||
if ((poseDid >> 24) != 0x03u) return;
|
||||
var anim = dats.Get<DatReaderWriter.DBObjs.Animation>(poseDid);
|
||||
var setup = dats.Get<DatReaderWriter.DBObjs.Setup>(setupId);
|
||||
if (anim is null || setup is null || anim.PartFrames.Count == 0) return;
|
||||
|
||||
// Retail plays the pose animation once and HOLDS the last frame (set_sequence_animation framerate=0,
|
||||
// RedressCreature decomp 0x004a3c22). For 0x030003C0 the animation has only two distinct keyframes —
|
||||
// frame 0 (a transitional, raised/bent arm) and frames 1..N (all byte-identical: the settled stance,
|
||||
// arms down + one leg back) — verified by dumping the dat. The held pose is therefore the last frame.
|
||||
int frameIdx = anim.PartFrames.Count - 1;
|
||||
var frame = anim.PartFrames[frameIdx];
|
||||
var src = doll.MeshRefs;
|
||||
var reposed = new List<AcDream.Core.World.MeshRef>(src.Count);
|
||||
for (int i = 0; i < src.Count; i++)
|
||||
{
|
||||
var scale = i < setup.DefaultScale.Count ? setup.DefaultScale[i] : System.Numerics.Vector3.One;
|
||||
System.Numerics.Vector3 origin = System.Numerics.Vector3.Zero;
|
||||
System.Numerics.Quaternion orient = System.Numerics.Quaternion.Identity;
|
||||
if (i < frame.Frames.Count) { origin = frame.Frames[i].Origin; orient = frame.Frames[i].Orientation; }
|
||||
var transform = System.Numerics.Matrix4x4.CreateScale(scale)
|
||||
* System.Numerics.Matrix4x4.CreateFromQuaternion(orient)
|
||||
* System.Numerics.Matrix4x4.CreateTranslation(origin);
|
||||
reposed.Add(new AcDream.Core.World.MeshRef(src[i].GfxObjId, transform) { SurfaceOverrides = src[i].SurfaceOverrides });
|
||||
}
|
||||
doll.MeshRefs = reposed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Commit B 2026-04-29 — register a live (server-spawned) entity into
|
||||
/// the <see cref="ShadowObjectRegistry"/> as a single collision body.
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ public sealed unsafe class PaperdollViewportRenderer : IUiViewportRenderer, IDis
|
|||
ColorAndIntensity = new Vector4(1f, 1f, 1f, 2.0f), // white @ retail intensity 2.0
|
||||
ConeAngleEtc = Vector4.Zero,
|
||||
},
|
||||
CellAmbient = new Vector4(0.40f, 0.40f, 0.40f, 1f), // 1 active light; ambient tunable at the gate
|
||||
CellAmbient = new Vector4(0.30f, 0.30f, 0.30f, 1f), // retail CreatureMode default ambient (ctor 0x004543cf = 0.3,0.3,0.3)
|
||||
FogParams = new Vector4(1e9f, 1e9f, 0f, 0f), // fog pushed to infinity = no fog on the doll
|
||||
FogColor = Vector4.Zero,
|
||||
CameraAndTime = new Vector4(0.12f, -2.4f, 0.88f, 0f), // camera world pos (matches DollCamera eye)
|
||||
|
|
|
|||
|
|
@ -132,14 +132,6 @@ public sealed class PaperdollController : IItemListDragHandler
|
|||
}
|
||||
}
|
||||
|
||||
// Slice 2 diagnostic (temporary): confirm the importer surfaced the toggle button + doll
|
||||
// viewport, and WHERE the button sits. Stripped once the toggle is visually confirmed.
|
||||
Console.WriteLine($"[Slice2-paperdoll] armorSlots={_armorSlots.Count}/9 " +
|
||||
$"viewport={_dollViewport?.GetType().Name ?? "MISSING"} " +
|
||||
$"slotsButton={slotsBtnEl?.GetType().Name ?? "MISSING"} " +
|
||||
$"btnRect=({slotsBtnEl?.Left ?? -1},{slotsBtnEl?.Top ?? -1},{slotsBtnEl?.Width ?? -1},{slotsBtnEl?.Height ?? -1}) " +
|
||||
$"btnVisible={slotsBtnEl?.Visible.ToString() ?? "n/a"}");
|
||||
|
||||
ApplyView(); // initial state = doll-view (armor slots hidden)
|
||||
|
||||
Populate();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue