fix(render): #113 root cause #2 - GfxObj meshes draw only DrawingBSP-referenced polys (the REAL phantom staircase)

The user gate + bisect overturned the coincident-cell attribution: the
phantom staircase persists in the PRE-session build (bisect screenshot at
the hall wall) and is drawn by the ENTITY pipeline, untouched by any clip.

Root cause (dat-proven, DumpHallModel_PolyFlagHistogram): retail renders a
GfxObj by TRAVERSING its drawing BSP (D3DPolyRender); polygons present in
the Polygons dictionary but referenced by NO DrawingBSP node are never
drawn - they are physics/no-draw geometry. The Holtburg meeting hall
(0x010014C3) keeps its exterior stair-ramp as dictionary polys 0+1: in
the PhysicsBSP (ACE walks The Sentry on it at z 117-118; invisible-but-
walkable in retail) but orphaned from the draw tree (true at ALL degrade
levels - the LOD theory is dead, Degrades[0] IS the base model). The hill
cottage (0x01000827) carries 8 such orphans. Our extraction iterated the
dictionary -> drew the collision skeleton: the wall staircase up close,
the flying stairs over the cottage roofline from afar (orphan ramp spans
world 221-232 at z 116-124.5; visible over the cottage roof from the west).

Fix: PrepareGfxObjMeshData filters to CollectDrawingBspPolygonIds(gfxObj)
when a drawing BSP exists; models without one draw everything (unchanged).
Physics untouched (collision keeps the full physics set - retail parity).
CellStruct extraction not touched (different conventions; no orphan
evidence there yet).

Dat-backed pins: Issue113DrawingBspFilterTests (hall orphans == 0+1,
cottage orphans == 0..7). Suites: App 226 / Core 1392 + the 4
pre-existing #99-era failures / UI 420 / Net 294.

Note: the earlier shell-clip enable (927fd8f, scoped 9ce335e) remains
correct and orthogonal - it crops interior CELL geometry to apertures
outdoors; this commit removes the phantom SHELL geometry at its source.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-10 20:52:52 +02:00
parent 6c9bbce433
commit e46d3d9273
3 changed files with 202 additions and 1 deletions

View file

@ -0,0 +1,73 @@
using System;
using System.IO;
using System.Linq;
using AcDream.App.Rendering.Wb;
using DatReaderWriter;
using DatReaderWriter.Options;
using Xunit;
using Xunit.Abstractions;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// #113 root cause #2 (2026-06-11): retail renders a GfxObj by traversing its
/// DRAWING BSP — dictionary polygons referenced by no BSP node are physics/
/// no-draw geometry and are never drawn. The Holtburg meeting hall
/// (0x010014C3) keeps its walkable exterior stair-ramp as dictionary polys
/// {0,1} (present in the PhysicsBSP — The Sentry patrols it; absent from
/// every DrawingBSP node — retail shows a plain wall). Our extraction
/// iterated the dictionary and drew the "phantom staircase". These pins hold
/// ObjectMeshManager.CollectDrawingBspPolygonIds to the dat facts.
/// </summary>
public class Issue113DrawingBspFilterTests
{
private readonly ITestOutputHelper _out;
public Issue113DrawingBspFilterTests(ITestOutputHelper output) => _out = output;
private static string? ResolveDatDir()
{
var fromEnv = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR");
if (!string.IsNullOrWhiteSpace(fromEnv) && Directory.Exists(fromEnv))
return fromEnv;
var def = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Documents", "Asheron's Call");
return Directory.Exists(def) ? def : null;
}
[Fact]
public void MeetingHall_OrphanStairPolys_AreExcludedFromDrawSet()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var hall = dats.Get<DatReaderWriter.DBObjs.GfxObj>(0x010014C3u)!;
var drawn = ObjectMeshManager.CollectDrawingBspPolygonIds(hall);
Assert.NotNull(drawn);
// The two orphans (the invisible walkable stair-ramp) must be excluded;
// everything else referenced.
Assert.DoesNotContain((ushort)0, drawn!);
Assert.DoesNotContain((ushort)1, drawn!);
var orphans = hall.Polygons.Keys.Where(k => !drawn.Contains(k)).OrderBy(k => k).ToArray();
_out.WriteLine($"hall orphans: [{string.Join(",", orphans)}]");
Assert.Equal(new ushort[] { 0, 1 }, orphans);
}
[Fact]
public void HillCottage_OrphanPolys_AreExcludedFromDrawSet()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cottage = dats.Get<DatReaderWriter.DBObjs.GfxObj>(0x01000827u)!;
var drawn = ObjectMeshManager.CollectDrawingBspPolygonIds(cottage);
Assert.NotNull(drawn);
var orphans = cottage.Polygons.Keys.Where(k => !drawn!.Contains(k)).OrderBy(k => k).ToArray();
_out.WriteLine($"cottage orphans: [{string.Join(",", orphans)}]");
Assert.Equal(new ushort[] { 0, 1, 2, 3, 4, 5, 6, 7 }, orphans);
}
}