fix(rendering): #47 — walk DIDDegrade for retail close-detail meshes
Humanoid bodies (Setup 0x02000001 + heritage variants) rendered visibly flat / bulky vs retail because we drew the base GfxObj id from Setup / AnimPartChange directly. Retail's CPhysicsPart::LoadGfxObjArray (0x0050DCF0) treats that base id as the entry point to a DIDDegrade table; close/player rendering uses Degrades[0].Id, which is the higher-detail mesh that carries bicep / deltoid / shoulder geometry. ACViewer also has this bug — it was the key signal it isn't acdream- specific. Both clients drew the LOD-3 base mesh (e.g. 14 verts / 17 polys for Aluvian Male upper arm 0x01000055), missing the close- detail variant (0x01001795: 32 verts / 60 polys). Adds GfxObjDegradeResolver that walks the table with safe fallbacks at every step. Wired in GameWindow after AnimPartChange application and before texture-change resolution so texture overrides match the resolved mesh's surfaces. Gated by ACDREAM_RETAIL_CLOSE_DEGRADES=1 and scoped to humanoid setups (34 parts with >=8 null-sentinel attachment slots) while the fix bakes — the change is harmless on non-humanoid setups (resolver falls back to base when no degrade table) but we hold the broader sweep until LOD distance plumbing lands. User confirmed visually 2026-05-06: bicep, deltoid, and back-muscle definition match retail. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8d7cad5b14
commit
0bd9b9693b
5 changed files with 648 additions and 2 deletions
|
|
@ -172,6 +172,38 @@ public sealed class GameWindow : IDisposable
|
|||
// Diagnostic: hide a specific humanoid part (>=10 parts) at render.
|
||||
private static readonly int s_hidePartIndex =
|
||||
int.TryParse(Environment.GetEnvironmentVariable("ACDREAM_HIDE_PART"), out var hp) ? hp : -1;
|
||||
|
||||
// Issue #47 — opt in to retail's close-detail GfxObj selection on
|
||||
// humanoid setups. When enabled, every per-part GfxObj id (after
|
||||
// server AnimPartChanges are applied) is replaced with Degrades[0]
|
||||
// from its DIDDegrade table when present. See GfxObjDegradeResolver
|
||||
// for the full retail-decomp citation. Off by default while the fix
|
||||
// bakes; flip to default-on once we've confirmed no scenery/setup
|
||||
// regressions.
|
||||
private static readonly bool s_retailCloseDegrades =
|
||||
string.Equals(Environment.GetEnvironmentVariable("ACDREAM_RETAIL_CLOSE_DEGRADES"), "1", StringComparison.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Issue #47 humanoid-setup detector. Matches Aluvian Male
|
||||
/// (<c>0x02000001</c>) and the 34-part heritage sibling setups
|
||||
/// (Aluvian Female, Sho M/F, Gharu M/F, Viamont/Empyrean, etc.)
|
||||
/// by structure rather than id list: a humanoid setup has exactly
|
||||
/// 34 parts, and the trailing attachment slots (parts 17–33) are
|
||||
/// the AC null-part sentinel <c>0x010001EC</c>. Non-humanoid
|
||||
/// 34-part setups (rare) won't have the sentinel pattern.
|
||||
/// </summary>
|
||||
private static bool IsIssue47HumanoidSetup(DatReaderWriter.DBObjs.Setup setup)
|
||||
{
|
||||
if (setup.Parts.Count != 34) return false;
|
||||
const uint NullPartGfx = 0x010001ECu;
|
||||
int nullSlots = 0;
|
||||
for (int i = 17; i < setup.Parts.Count; i++)
|
||||
if ((uint)setup.Parts[i] == NullPartGfx) nullSlots++;
|
||||
// At least half of slots 17–33 wired to the null sentinel — enough
|
||||
// to distinguish humanoids from any future 34-part creature setup.
|
||||
return nullSlots >= 8;
|
||||
}
|
||||
|
||||
private readonly HashSet<SkyPesKey> _activeSkyPes = new();
|
||||
private readonly HashSet<SkyPesKey> _missingSkyPes = new();
|
||||
|
||||
|
|
@ -2076,6 +2108,41 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
// Issue #47 — retail's close/player rendering path resolves each
|
||||
// part's base GfxObj through its DIDDegrade table to the close-
|
||||
// detail mesh in slot 0. Without this, humanoid arms/torso draw
|
||||
// the LOW-detail base GfxObj (e.g. 0x01000055, 14 verts / 17
|
||||
// polys) instead of the close mesh (0x01001795, 32 verts / 60
|
||||
// polys), losing all bicep/shoulder/back geometry. See
|
||||
// <see cref="GfxObjDegradeResolver"/> for the named-retail
|
||||
// citation (CPhysicsPart::LoadGfxObjArray at 0x0050DCF0,
|
||||
// ::UpdateViewerDistance at 0x0050E030, ::Draw at 0x0050D7A0).
|
||||
//
|
||||
// Order matters: the swap happens AFTER AnimPartChanges have
|
||||
// installed the server's body/clothing/head ids, BEFORE texture
|
||||
// changes resolve (which match against the resolved mesh's
|
||||
// surfaces) and BEFORE the GfxObjMesh.Build / texture upload
|
||||
// path consumes the part list.
|
||||
if (s_retailCloseDegrades && IsIssue47HumanoidSetup(setup))
|
||||
{
|
||||
for (int partIdx = 0; partIdx < parts.Count; partIdx++)
|
||||
{
|
||||
var part = parts[partIdx];
|
||||
if (!AcDream.Core.Meshing.GfxObjDegradeResolver.TryResolveCloseGfxObj(
|
||||
_dats, part.GfxObjId,
|
||||
out uint resolvedId, out _))
|
||||
continue;
|
||||
if (resolvedId == part.GfxObjId)
|
||||
continue;
|
||||
|
||||
parts[partIdx] = new AcDream.Core.World.MeshRef(
|
||||
resolvedId, part.PartTransform);
|
||||
|
||||
if (dumpClothing)
|
||||
Console.WriteLine($" DEGRADE part={partIdx:D2} gfx=0x{part.GfxObjId:X8} -> close=0x{resolvedId:X8}");
|
||||
}
|
||||
}
|
||||
|
||||
// Build per-part texture overrides. The server sends TextureChanges as
|
||||
// (partIdx, oldSurfaceTextureId, newSurfaceTextureId) where both ids
|
||||
// are in the SurfaceTexture (0x05) range. Our sub-meshes are keyed
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue