From 09e013b7bd446d0fe69420cbfdf192a6b0a29f14 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 1 May 2026 16:05:33 +0200 Subject: [PATCH] docs(issues): #37 humanoid coat doesn't extend up to neck (env-var diagnostics committed) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Filed as #37 after a ~3 hr investigation that ruled out animation source, backface culling/winding, palette overlay, and head-GfxObj polygons. Confirmed: - Stub is from part 9 (upper torso/coat) post-AnimPartChange (gfx 0x0100120D) - Part 9's both surfaces ARE matched by our 2 TextureChanges - Server data complete; composition formula matches ACME + retail decomp Untested hypothesis space (next session): - Texture decode chain (compare our SurfaceDecoder vs ACME TextureHelpers) - Polygon-to-surface index off-by-one on part 9 - Multi-layer texture composition AC may do - UV mapping bug Diagnostic env vars committed to source for next-session reuse: - ACDREAM_HIDE_PART=N — hide specific humanoid part to localize bugs - ACDREAM_NO_CULL=1 — disable backface culling - ACDREAM_DUMP_CLOTHING=1 — dump APC + TC + per-part Surface chain coverage Bug was originally reported as "head/neck protrudes forward"; the apparent forward shift turned out to be an optical illusion from the missing coat collar. Math + cdb-ground-truth + ACME comparison confirmed the head placement is correct retail-faithful — see #37 for the long write-up. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/ISSUES.md | 90 +++++++++++++++++++ src/AcDream.App/Rendering/GameWindow.cs | 58 ++++++++++++ .../Rendering/InstancedMeshRenderer.cs | 21 ++++- 3 files changed, 166 insertions(+), 3 deletions(-) diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 87b1db7..7f9f11c 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -46,6 +46,96 @@ Copy this block when adding a new issue: # Active issues +## #37 — Humanoid coat doesn't extend up to neck (visible "skin stub" between hair and coat) + +**Status:** OPEN +**Severity:** LOW (cosmetic; doesn't affect gameplay) +**Filed:** 2026-05-01 +**Component:** rendering / clothing / textures + +**Description:** Every humanoid character (player + NPCs) wearing a coat +shows a visible skin-colored region at the top of the coat where retail +shows continuous coat fabric. From the back view: hair → skin stub → +coat top. In retail: hair → coat collar (no exposed skin). This was +originally reported as "head/neck protruding forward" — the apparent +forward shift is an optical illusion caused by the missing coat collar. + +**Investigation 2026-05-01 (~3 hr session, conclusively ruled out +many hypotheses):** + +What we ruled out: + +- **Animation source.** `ACDREAM_USE_PLACEMENT_BASE=1` (force chars to + `Setup.PlacementFrames[Resting]` instead of `Animation.PartFrames[0]`) + → stub still visible. +- **Backface culling / mesh winding.** `ACDREAM_NO_CULL=1` (disable + `glCullFace` entirely) → stub still visible. +- **Palette overlay (SubPalettes).** `ACDREAM_NO_PALETTE_OVERLAY=1` + (skip `ComposePalette`) → stub still visible (other colors broke + as expected — confirms overlay was firing). Bug is NOT a body-skin + SubPalette being mis-applied to coat fabric. +- **Bug source = part 16 (head).** `ACDREAM_HIDE_PART=16` → head goes + away, stub remains UNCHANGED (clean coat top with same shape). + Stub is NOT from head GfxObj polygons. +- **Per-part placement frame Origin.** `ACDREAM_NUDGE_Y=-0.1` confirmed + `+Y = forward` in body-local; head Origin (0, 0.013, 1.587) places + head correctly relative to spine. Math checks out. + +What we confirmed (data is correct): + +- Player Setup `0x02000001` (Aluvian Male), 34 parts. +- Server (ACE) sends `animParts=34 texChanges=12 subPalettes=10`. +- Part 9 (upper torso/coat) has gfx `0x0100120D` after AnimPartChange. +- Part 9 has 2 surfaces, BOTH covered by 2 TextureChanges + (`oldTex=0x050003D5→0x05001AFE`, `oldTex=0x050003D4→0x05001AFC`). +- Stub IS from part 9: `ACDREAM_HIDE_PART=9` → entire torso (including + stub region) disappears. +- Per-part composition formula (`Scale × Rotation × Translation`) + matches ACME's `StaticObjectManager.cs:256-258` and retail decomp's + `Frame::combine` at `0x00518FD0`. + +**Remaining hypothesis space (untested):** + +1. **Texture decode produces skin pixels** for `0x05001AFE/0x05001AFC` + where ACME / retail produces coat pixels. Compare our SurfaceDecoder + against ACME's `TextureHelpers.cs` for INDEX16 / palette-indexed + chains. +2. **Polygon-to-surface mapping off-by-one.** Specific polygons of + part 9 reference an unintended surface. Add a dump: for each polygon + in gfx 0x0100120D, print `PosSurface` index + the resolved Surface id. +3. **Multi-layer texture composition retail does and we skip.** AC's + "ApplyCloth" or similar layered texture step. Grep + `acclient_2013_pseudo_c.txt` for `BlendBaseLayer`, `LayerSurfaces`, + any composition method that combines multiple textures into one. +4. **UV mapping bug.** Part 9's polygon UVs map to a skin region of + the coat texture. Dump per-vertex UV vs vertex Z; if a high-Z vertex + has UV.v near a skin region, that's the source. + +**Files (diagnostic env vars committed for next-session reuse):** + +- `src/AcDream.App/Rendering/InstancedMeshRenderer.cs:210-275` + — `ACDREAM_NO_CULL` env var +- `src/AcDream.App/Rendering/GameWindow.cs` — `ACDREAM_HIDE_PART=N` + hides specific humanoid part; `ACDREAM_DUMP_CLOTHING=1` dumps + AnimPartChanges + TextureChanges + per-part Surface chain coverage. +- `src/AcDream.App/Rendering/TextureCache.cs:159-204` — `DecodeFromDats` + is the texture decode entry. Compare against + `references/WorldBuilder-ACME-Edition/.../TextureHelpers.cs`. + +**Reproduction:** + +```powershell +$env:ACDREAM_LIVE = "1"; $env:ACDREAM_DEVTOOLS = "1" +# normal launch — visible from chase camera looking at +Acdream's back +``` + +Stub is visible on +Acdream and on every NPC humanoid (Pathwarden, +Town Crier, Shopkeeper Renald, etc.). + +**Acceptance:** Side-by-side retail + acdream rendering of +Acdream +shows coat extending up to chin level on both. No exposed skin +between hair and coat. + ## #L.1 — Hotbar UI panel **Status:** OPEN diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 771c37d..34e2df9 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -168,6 +168,10 @@ public sealed class GameWindow : IDisposable // Keep the experimental path available for DAT archaeology only. private readonly bool _enableSkyPesDebug = string.Equals(Environment.GetEnvironmentVariable("ACDREAM_ENABLE_SKY_PES"), "1", StringComparison.Ordinal); + + // 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; private readonly HashSet _activeSkyPes = new(); private readonly HashSet _missingSkyPes = new(); @@ -1912,6 +1916,17 @@ public sealed class GameWindow : IDisposable // then proceed with the normal upload loop. var parts = new List(flat); var animPartChanges = spawn.AnimPartChanges ?? Array.Empty(); + // Diagnostic: dump AnimPartChanges + TextureChanges for humanoid setups + // gated on ACDREAM_DUMP_CLOTHING=1. Used to verify whether the server is + // sending coverage for the neck (part 9 for Aluvian Male) etc. + bool dumpClothing = string.Equals(Environment.GetEnvironmentVariable("ACDREAM_DUMP_CLOTHING"), "1", StringComparison.Ordinal) + && setup.Parts.Count >= 10; + if (dumpClothing) + { + Console.WriteLine($"\n=== DUMP_CLOTHING: guid=0x{spawn.Guid:X8} name='{spawn.Name}' setup=0x{setup.Id:X8} APC={animPartChanges.Count} ==="); + foreach (var c in animPartChanges) + Console.WriteLine($" APC part={c.PartIndex:D2} -> gfx=0x{c.NewModelId:X8}"); + } foreach (var change in animPartChanges) { if (change.PartIndex < parts.Count) @@ -1932,6 +1947,45 @@ public sealed class GameWindow : IDisposable // to get a texture decoded with the replacement SurfaceTexture // substituted inside the Surface's decode chain. var textureChanges = spawn.TextureChanges ?? Array.Empty(); + if (dumpClothing) + { + Console.WriteLine($" TextureChanges count={textureChanges.Count}"); + foreach (var tc in textureChanges) + Console.WriteLine($" TC part={tc.PartIndex:D2} oldTex=0x{tc.OldTexture:X8} -> newTex=0x{tc.NewTexture:X8}"); + + // For each part (post-AnimPartChange), dump its Surface chain so we + // can see which OrigTextureIds the part references and check which + // are covered by our TextureChanges. + var tcByPart = new Dictionary>(); + foreach (var tc in textureChanges) + { + if (!tcByPart.TryGetValue(tc.PartIndex, out var set)) { set = new HashSet(); tcByPart[tc.PartIndex] = set; } + set.Add(tc.OldTexture); + } + for (int pi = 0; pi < parts.Count; pi++) + { + var pgfx = _dats.Get(parts[pi].GfxObjId); + if (pgfx is null) continue; + if (pgfx.Surfaces.Count == 0) continue; + tcByPart.TryGetValue(pi, out var coveredOldTex); + int matched = 0; + int unmatched = 0; + var unmatchedList = new List(); + foreach (var surfQid in pgfx.Surfaces) + { + uint surfId = (uint)surfQid; + var surf = _dats.Get(surfId); + if (surf is null) continue; + uint origTex = (uint)surf.OrigTextureId; + if (coveredOldTex is not null && coveredOldTex.Contains(origTex)) matched++; + else { unmatched++; unmatchedList.Add($"surf=0x{surfId:X8} origTex=0x{origTex:X8}"); } + } + if (pgfx.Surfaces.Count > 0) + Console.WriteLine($" part[{pi:D2}] gfx=0x{parts[pi].GfxObjId:X8} surfaces={pgfx.Surfaces.Count} matched={matched} unmatched={unmatched}"); + foreach (var s in unmatchedList) + Console.WriteLine($" UNMATCHED {s}"); + } + } Dictionary>? resolvedOverridesByPart = null; if (textureChanges.Count > 0) { @@ -6036,6 +6090,10 @@ public sealed class GameWindow : IDisposable partTransform = partTransform * scaleMat; var template = ae.PartTemplate[i]; + if (s_hidePartIndex >= 0 && i == s_hidePartIndex && partCount >= 10) + { + continue; + } newMeshRefs.Add(new AcDream.Core.World.MeshRef(template.GfxObjId, partTransform) { SurfaceOverrides = template.SurfaceOverrides, diff --git a/src/AcDream.App/Rendering/InstancedMeshRenderer.cs b/src/AcDream.App/Rendering/InstancedMeshRenderer.cs index 7f4ce29..92e8f5c 100644 --- a/src/AcDream.App/Rendering/InstancedMeshRenderer.cs +++ b/src/AcDream.App/Rendering/InstancedMeshRenderer.cs @@ -207,6 +207,11 @@ public sealed unsafe class InstancedMeshRenderer : IDisposable } // ── Pass 1: Opaque + ClipMap ────────────────────────────────────────── + // Diagnostic: ACDREAM_NO_CULL=1 disables backface culling entirely. + if (string.Equals(Environment.GetEnvironmentVariable("ACDREAM_NO_CULL"), "1", StringComparison.Ordinal)) + { + _gl.Disable(EnableCap.CullFace); + } foreach (var (key, grp) in _groups) { if (!_gpuByGfxObj.TryGetValue(key.GfxObjId, out var subMeshes)) @@ -268,9 +273,19 @@ public sealed unsafe class InstancedMeshRenderer : IDisposable // ── Pass 2: Translucent (AlphaBlend, Additive, InvAlpha) ───────────── _gl.Enable(EnableCap.Blend); _gl.DepthMask(false); - _gl.Enable(EnableCap.CullFace); - _gl.CullFace(TriangleFace.Back); - _gl.FrontFace(FrontFaceDirection.Ccw); + // Diagnostic: ACDREAM_NO_CULL=1 disables backface culling (used 2026-05-01 + // to test if our mesh winding (0,i,i+1) vs ACME's (i+1,i,0) is causing + // visible polygons to be culled, especially around the neck/coat seam). + if (string.Equals(Environment.GetEnvironmentVariable("ACDREAM_NO_CULL"), "1", StringComparison.Ordinal)) + { + _gl.Disable(EnableCap.CullFace); + } + else + { + _gl.Enable(EnableCap.CullFace); + _gl.CullFace(TriangleFace.Back); + _gl.FrontFace(FrontFaceDirection.Ccw); + } foreach (var (key, grp) in _groups) {