From 1ca412d07ba7e9eb4c7227565443fe27bf76d498 Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 11 Jun 2026 21:58:17 +0200 Subject: [PATCH] #119: entity bounds must cover the parts - the gaze-dependent staircase vanish User re-gate after 2163308/987313a: run-from-town stairs FIXED, barrel GONE - but the stairs still vanish by VIEWING ANGLE (visible climbing down, gone climbing up; same at the tower top). The gate3 probe data exonerates everything downstream: the entity always draws with correct batches when it reaches the dispatcher (cache hit:119, restZ correct, zero WALK-REJECTs, never clip-culled) - so the vanish lives in the one gaze-dependent gate the probe cannot see: the bounds-based cullers. WorldEntity.RefreshAabb was a fixed +-5 m box around the entity ANCHOR. The staircase's 43 parts spiral 15 m ABOVE the anchor, and BOTH visibility gates derive from the box: the dispatcher's per-entity frustum cull AND RetailPViewRenderer.EntitySphere (the viewcone sphere = this box's bounding sphere). Looking up the spiral put the anchor's neighborhood out of view -> the whole entity culled while 15 m of it stood in front of the camera; looking down kept the anchor in view -> visible. Exactly the reported asymmetry. Fix: expand the box by the largest MeshRef part-translation magnitude (rotation-invariant, so entity.Rotation needs no handling; identity- part entities get offset 0 - behavior unchanged; scenery scale is already baked into the part transforms). Suites: App 246+1skip / Core 1431+2skip / UI 420 / Net 294. Co-Authored-By: Claude Fable 5 --- src/AcDream.Core/World/WorldEntity.cs | 31 +++++++++++++++++-- .../World/WorldEntityAabbTests.cs | 29 +++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/AcDream.Core/World/WorldEntity.cs b/src/AcDream.Core/World/WorldEntity.cs index 6121720a..08160ce9 100644 --- a/src/AcDream.Core/World/WorldEntity.cs +++ b/src/AcDream.Core/World/WorldEntity.cs @@ -116,8 +116,35 @@ public sealed class WorldEntity public void RefreshAabb() { var p = Position; - AabbMin = new Vector3(p.X - DefaultAabbRadius, p.Y - DefaultAabbRadius, p.Z - DefaultAabbRadius); - AabbMax = new Vector3(p.X + DefaultAabbRadius, p.Y + DefaultAabbRadius, p.Z + DefaultAabbRadius); + + // #119 follow-up (2026-06-11): the box must cover the MESH, not just the + // anchor. A multi-part Setup's parts sit at root-relative offsets — the + // AAB3 tower's spiral staircase spans 15 m ABOVE its anchor — and BOTH + // visibility gates derive from this box: the dispatcher's per-entity + // frustum cull (WbDrawDispatcher.WalkEntitiesInto) and the viewcone + // sphere (RetailPViewRenderer.EntitySphere = this box's bounding + // sphere). A fixed ±5 m anchor box dropped the staircase whenever the + // gaze left the anchor's neighborhood: stairs visible looking down the + // spiral (anchor in view), gone looking up (anchor culled) — the + // user-reported direction/angle asymmetry. Expand by the largest part + // offset; using the offset MAGNITUDE keeps the box rotation-invariant, + // so entity.Rotation needs no handling here. Identity-part entities + // (1-part Setups, GfxObjs, scenery) get offset 0 — behavior unchanged. + float radius = DefaultAabbRadius; + var refs = MeshRefs; + if (refs is not null) + { + float maxOffset = 0f; + for (int i = 0; i < refs.Count; i++) + { + float len = refs[i].PartTransform.Translation.Length(); + if (len > maxOffset) maxOffset = len; + } + radius += maxOffset; + } + + AabbMin = new Vector3(p.X - radius, p.Y - radius, p.Z - radius); + AabbMax = new Vector3(p.X + radius, p.Y + radius, p.Z + radius); AabbDirty = false; } diff --git a/tests/AcDream.Core.Tests/World/WorldEntityAabbTests.cs b/tests/AcDream.Core.Tests/World/WorldEntityAabbTests.cs index cafa60e4..2f8e9d68 100644 --- a/tests/AcDream.Core.Tests/World/WorldEntityAabbTests.cs +++ b/tests/AcDream.Core.Tests/World/WorldEntityAabbTests.cs @@ -23,6 +23,35 @@ public class WorldEntityAabbTests Assert.Equal(new Vector3(15, 25, 35), entity.AabbMax); } + [Fact] + public void Aabb_MultiPartOffsets_CoverTheParts() + { + // #119 follow-up: the AAB3 tower staircase — 43 parts spiralling to + // (3, 3, 15.15) root-relative. The box (and the viewcone sphere derived + // from it) must cover the parts, not just the ±5 m anchor neighborhood; + // the anchor-only box made the staircase vanish whenever the gaze left + // the base (visible climbing down, gone climbing up). + var entity = new WorldEntity + { + Id = 1, + SourceGfxObjOrSetupId = 0x020003F2, + Position = new Vector3(300, -132, 112), + Rotation = System.Numerics.Quaternion.Identity, + MeshRefs = new[] + { + new MeshRef(0x01000E2A, Matrix4x4.CreateTranslation(0f, 3f, 1.55f)), + new MeshRef(0x01000E2D, Matrix4x4.CreateTranslation(-1.75f, 3f, 15.15f)), + }, + }; + entity.RefreshAabb(); + + // Top part sits at z = 112 + 15.15 = 127.15 — must be inside the box. + Assert.True(entity.AabbMax.Z >= 127.15f, + $"box top {entity.AabbMax.Z} must cover the highest part (z=127.15)"); + // Conservative symmetric expansion still contains the old ±5 box. + Assert.True(entity.AabbMin.Z <= 107f && entity.AabbMax.X >= 305f); + } + [Fact] public void Aabb_DirtyFlag_SetByMutator_ClearedByRefresh() {