feat(D.2b): Slice 2 — wire paperdoll doll RTT pass + re-dress into GameWindow
Drives the doll 3-D pass in a pre-UI hook (after the world passes, before _uiHost.Draw), gated on inventory-open AND doll-view (the viewport widget's Visible, set by the Slots toggle). RefreshPaperdollDoll clones the live player entity (_entitiesByServerGuid[player]) into a DollEntityBuilder doll with COPIED MeshRefs (frozen pose); re-dress is triggered by a dirty flag set in OnLiveAppearanceUpdated (0xF625) — the C# analog of RedressCreature. Correctness fix: the doll is passed in animatedEntityIds so the dispatcher BYPASSES the Tier-1 classification cache (WbDrawDispatcher.cs:1142). Without it, a re-dress (a new WorldEntity with the same fixed DollRenderId) would serve the previous doll's cached batches and the new gear wouldn't appear. Renderer disposed in the GameWindow teardown. Build + full App suite green (594). Static doll (no idle animation yet — next slice). Visual gate next. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3cdecb536b
commit
8b0365e7a9
3 changed files with 103 additions and 2 deletions
|
|
@ -34,6 +34,17 @@ public static class DollEntityBuilder
|
|||
/// </summary>
|
||||
public const uint DollServerGuid = 0xDA11_D011u;
|
||||
|
||||
/// <summary>
|
||||
/// Reserved render-local entity id for the doll. FIXED (the doll is a singleton)
|
||||
/// and high enough never to collide with <c>_liveEntityIdCounter</c> ids. The
|
||||
/// renderer passes this in <c>animatedEntityIds</c> so the dispatcher treats the
|
||||
/// doll as animated — which BYPASSES the Tier-1 classification cache
|
||||
/// (WbDrawDispatcher.cs:1142). That matters because a re-dress builds a NEW
|
||||
/// WorldEntity with this SAME id; without the cache bypass the dispatcher would
|
||||
/// serve the previous doll's cached batches and the new gear wouldn't appear.
|
||||
/// </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);
|
||||
|
|
@ -104,7 +115,7 @@ public static class DollEntityBuilder
|
|||
// replace it before registration.
|
||||
return new WorldEntity
|
||||
{
|
||||
Id = 0u,
|
||||
Id = DollRenderId,
|
||||
ServerGuid = DollServerGuid,
|
||||
SourceGfxObjOrSetupId = setupId,
|
||||
Position = Vector3.Zero,
|
||||
|
|
|
|||
|
|
@ -625,6 +625,12 @@ public sealed class GameWindow : IDisposable
|
|||
private AcDream.App.UI.Layout.InventoryController? _inventoryController;
|
||||
// Phase D.2b Sub-phase C — paperdoll equip-slot controller (wield drag handler).
|
||||
private AcDream.App.UI.Layout.PaperdollController? _paperdollController;
|
||||
// Phase D.2b Sub-phase C Slice 2 — the 3-D doll viewport: an off-screen RTT renderer, the UiViewport
|
||||
// widget that blits it, the inventory frame (for the open-gate), and a dirty flag (re-dress on 0xF625).
|
||||
private AcDream.App.Rendering.PaperdollViewportRenderer? _paperdollViewportRenderer;
|
||||
private AcDream.App.UI.UiViewport? _paperdollViewportWidget;
|
||||
private AcDream.App.UI.UiNineSlicePanel? _inventoryFrame;
|
||||
private bool _paperdollDollDirty = true;
|
||||
// Phase D.2b Task 9 — plugin UI registrations buffered before OnLoad; drained in OnLoad.
|
||||
private readonly AcDream.App.Plugins.BufferedUiRegistry? _uiRegistry;
|
||||
// Phase I.2: ImGui debug panel ViewModel. Lives for as long as
|
||||
|
|
@ -2257,6 +2263,13 @@ public sealed class GameWindow : IDisposable
|
|||
// slot position is seen + usable; the live 3D character (the doll) is Slice 2.
|
||||
emptySlotSprite: contentsEmpty);
|
||||
|
||||
// Slice 2: capture the doll viewport widget (Type 0xD, element 0x100001D5) + the inventory
|
||||
// frame so the per-frame pre-UI hook can render the 3-D doll into the widget's texture only
|
||||
// when the window is open and in doll-view. The renderer itself is built after the
|
||||
// WbDrawDispatcher exists (further down this method).
|
||||
_paperdollViewportWidget = invLayout.FindElement(0x100001D5u) as AcDream.App.UI.UiViewport;
|
||||
_inventoryFrame = inventoryFrame;
|
||||
|
||||
Console.WriteLine("[D.2b-B] retail inventory window from LayoutDesc importer (0x21000023).");
|
||||
}
|
||||
else Console.WriteLine("[D.2b-B] inventory: LayoutDesc 0x21000023 not found.");
|
||||
|
|
@ -2366,6 +2379,16 @@ public sealed class GameWindow : IDisposable
|
|||
// A.5 T22.5: apply A2C gate from quality preset.
|
||||
_wbDrawDispatcher.AlphaToCoverage = _resolvedQuality.AlphaToCoverage;
|
||||
|
||||
// Slice 2: now that the dispatcher exists, build the paperdoll doll RTT renderer and hand it to
|
||||
// the viewport widget. Reuses the dispatcher + the scene-lighting UBO + _gl. The widget only
|
||||
// blits the texture; the pre-UI hook drives the 3-D pass (see OnRender).
|
||||
if (_paperdollViewportWidget is not null && _sceneLightingUbo is not null)
|
||||
{
|
||||
_paperdollViewportRenderer = new AcDream.App.Rendering.PaperdollViewportRenderer(
|
||||
_gl, _wbDrawDispatcher, _sceneLightingUbo);
|
||||
_paperdollViewportWidget.Renderer = _paperdollViewportRenderer;
|
||||
}
|
||||
|
||||
// Phase A8: EnvCellRenderer init. Shares _meshShader (mesh_modern.{vert,frag})
|
||||
// with the dispatcher — both consume the same global mesh buffer (VAO/IBO)
|
||||
// from ObjectMeshManager.GlobalBuffer.
|
||||
|
|
@ -3751,6 +3774,52 @@ public sealed class GameWindow : IDisposable
|
|||
BasePaletteId = md.BasePaletteId,
|
||||
};
|
||||
OnLiveEntitySpawned(newSpawn);
|
||||
|
||||
// Slice 2: a player appearance change (equip / unequip) rebuilt _entitiesByServerGuid[player]
|
||||
// above; flag the paperdoll doll to re-clone from it on the next doll pass (the C# analog of
|
||||
// RedressCreature). Cheap flag — the rebuild is deferred to the pre-UI hook when visible.
|
||||
if (update.Guid == _playerServerGuid)
|
||||
_paperdollDollDirty = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds the paperdoll doll from the live player entity (retail makeObject(player) +
|
||||
/// DoObjDescChangesFromDefault). Clones the player's already-resolved Setup / MeshRefs / palette /
|
||||
/// part overrides into a dedicated <see cref="DollEntityBuilder"/> entity posed at the scene origin
|
||||
/// facing the viewer. The MeshRefs are COPIED so the doll holds a stable pose independent of the
|
||||
/// player's live in-world animation. Returns false (leaving the dirty flag set to retry) when the
|
||||
/// player entity isn't available yet.
|
||||
/// </summary>
|
||||
private bool RefreshPaperdollDoll()
|
||||
{
|
||||
if (_paperdollViewportRenderer is null) return false;
|
||||
if (!_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var pe) || pe.MeshRefs.Count == 0)
|
||||
{
|
||||
_paperdollViewportRenderer.SetDoll(null);
|
||||
return false; // player not ready — retry next frame
|
||||
}
|
||||
|
||||
uint? basePal = null;
|
||||
List<(uint, byte, byte)>? subs = null;
|
||||
if (pe.PaletteOverride is { } po)
|
||||
{
|
||||
basePal = po.BasePaletteId;
|
||||
subs = new List<(uint, byte, byte)>();
|
||||
foreach (var r in po.SubPalettes) subs.Add((r.SubPaletteId, r.Offset, r.Length));
|
||||
}
|
||||
|
||||
List<(byte, uint)>? parts = null;
|
||||
if (pe.PartOverrides.Count > 0)
|
||||
{
|
||||
parts = new List<(byte, uint)>(pe.PartOverrides.Count);
|
||||
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 doll = AcDream.App.Rendering.DollEntityBuilder.Build(
|
||||
pe.SourceGfxObjOrSetupId, meshRefsCopy, basePal, subs, parts);
|
||||
_paperdollViewportRenderer.SetDoll(doll);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -9002,6 +9071,20 @@ public sealed class GameWindow : IDisposable
|
|||
SkipWorldGeometry: ;
|
||||
}
|
||||
|
||||
// Phase D.2b Sub-phase C Slice 2 — paperdoll 3-D doll: render the re-dressed player clone into the
|
||||
// viewport's off-screen texture BEFORE the 2-D UI pass blits it, but only when the inventory window
|
||||
// is open AND the paperdoll is in doll-view (the widget's Visible is driven by the Slots toggle).
|
||||
// The 3-D pass is fully sealed in a GLStateScope so it doesn't disturb the world/UI state.
|
||||
if (_options.RetailUi && _paperdollViewportRenderer is not null
|
||||
&& _paperdollViewportWidget is { Visible: true } dollWidget
|
||||
&& _inventoryFrame is { Visible: true })
|
||||
{
|
||||
if (_paperdollDollDirty && RefreshPaperdollDoll())
|
||||
_paperdollDollDirty = false;
|
||||
dollWidget.TextureHandle = _paperdollViewportRenderer.Render(
|
||||
(int)dollWidget.Width, (int)dollWidget.Height);
|
||||
}
|
||||
|
||||
// Phase D.2b — retail-look UI tree (render-only; input integration deferred).
|
||||
// Self-contained 2D pass: UiHost.Draw → TextRenderer.Flush sets its own
|
||||
// blend/depth state and restores. Drawn before ImGui so the devtools
|
||||
|
|
@ -13081,6 +13164,7 @@ public sealed class GameWindow : IDisposable
|
|||
_terrain?.Dispose();
|
||||
_terrainModernShader?.Dispose();
|
||||
_sceneLightingUbo?.Dispose();
|
||||
_paperdollViewportRenderer?.Dispose(); // Slice 2: the doll's off-screen FBO + textures
|
||||
_particleRenderer?.Dispose();
|
||||
_debugLines?.Dispose();
|
||||
_uiHost?.Dispose();
|
||||
|
|
|
|||
|
|
@ -49,6 +49,12 @@ public sealed unsafe class PaperdollViewportRenderer : IUiViewportRenderer, IDis
|
|||
// neverCullLandblockId the entity is never culled. 0 is unused by live streaming on the doll path.
|
||||
private const uint DollLandblockId = 0u;
|
||||
|
||||
// The doll's render id, passed as "animated" so the dispatcher bypasses the Tier-1 classification
|
||||
// cache (WbDrawDispatcher.cs:1142) and re-classifies from the current MeshRefs every frame. This is
|
||||
// what makes a re-dress (a new WorldEntity built with the same DollRenderId) actually take effect,
|
||||
// AND what lets the idle animation (later slice, TickAnimations rebuilds MeshRefs) show.
|
||||
private static readonly HashSet<uint> _dollAnimatedIds = new() { DollEntityBuilder.DollRenderId };
|
||||
|
||||
public PaperdollViewportRenderer(GL gl, WbDrawDispatcher dispatcher, SceneLightingUboBinding lightUbo)
|
||||
{
|
||||
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
|
||||
|
|
@ -105,7 +111,7 @@ public sealed unsafe class PaperdollViewportRenderer : IUiViewportRenderer, IDis
|
|||
frustum: null,
|
||||
neverCullLandblockId: DollLandblockId,
|
||||
visibleCellIds: null,
|
||||
animatedEntityIds: null);
|
||||
animatedEntityIds: _dollAnimatedIds); // doll treated as animated ⇒ cache-bypass (re-dress correctness)
|
||||
|
||||
return _colorTex;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue