#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 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-11 21:58:17 +02:00
parent 987313aa54
commit 1ca412d07b
2 changed files with 58 additions and 2 deletions

View file

@ -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()
{