#119: entity bounds from dat vertex data - works for every case, not just multi-part
The1ca412dpart-offset expansion fixed the staircase but still rested on the 5 m promise one level down: a SINGLE part whose mesh extends more than 5 m from its own origin (offset 0 -> box +-5 m) keeps the gaze-dependent vanish. Per the user's mandate ("it must work for every case"), the bound now derives from the dat VERTEX data - the same vertices that get drawn - so no synthetic containment promise remains. Oracle context (read this session): retail has NO whole-entity visibility volume - CPhysicsPart::Draw (0x0050d7a0) viewcone-checks each part's dat-authored CGfxObj.drawing_sphere at the part's own world position (RenderDeviceD3D::DrawMesh 0x005a0860). Retail's bound IS data; ours was a promise. Our per-ENTITY granularity stays (a deliberate batching-era choice, WB-owned per the inventory) but the volume is now data-derived and conservative: visually identical by construction, never culls what retail would draw. - GfxObjBounds: per-GfxObj vertex AABB, cached by id (parts repeat heavily); LocalBoundsAccumulator: union of part-transformed AABB corners (conservative-correct under any affine transform). - WorldEntity.SetLocalBounds + RefreshAabb preferred path: rotate the root-local bounds' 8 corners into world axes + DefaultAabbRadius margin (absorbs animated-pose drift vs the rest-pose bounds; keeps small objects at their historical box size). Offset heuristic stays as the fallback for boundless fixtures. - All four hydration sites wired (outdoor stabs, scenery incl. baked scale, interior cell statics, server live spawns). Tests: tall-single-part coverage (the case1ca412dcould not see), rotation-following, accumulator union. Suites: App 246+1skip / Core 1434+2skip / UI 420 / Net 294. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
1ca412d07b
commit
6a9b529113
4 changed files with 248 additions and 13 deletions
|
|
@ -52,6 +52,67 @@ public class WorldEntityAabbTests
|
|||
Assert.True(entity.AabbMin.Z <= 107f && entity.AabbMax.X >= 305f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Aabb_LocalBounds_TallSinglePart_Covered()
|
||||
{
|
||||
// The every-case fix (#119 follow-up 2): a SINGLE part at identity transform
|
||||
// whose MESH is 12 m tall. The part-offset heuristic cannot see this (offset 0
|
||||
// -> box ±5 m, mesh sticks 7 m out); vertex-derived local bounds must cover it.
|
||||
var entity = new WorldEntity
|
||||
{
|
||||
Id = 1,
|
||||
SourceGfxObjOrSetupId = 0x01000001,
|
||||
Position = new Vector3(100, 100, 50),
|
||||
Rotation = System.Numerics.Quaternion.Identity,
|
||||
MeshRefs = new[] { new MeshRef(0x01000001, Matrix4x4.Identity) },
|
||||
};
|
||||
entity.SetLocalBounds(new Vector3(-1, -1, 0), new Vector3(1, 1, 12));
|
||||
entity.RefreshAabb();
|
||||
|
||||
// Mesh top = position.Z (50) + local 12 = 62; the old ±5 anchor box topped out at 55.
|
||||
Assert.True(entity.AabbMax.Z >= 62f, $"box top {entity.AabbMax.Z} must cover mesh top (z=62)");
|
||||
Assert.True(entity.AabbMin.Z <= 50f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Aabb_LocalBounds_FollowRotation()
|
||||
{
|
||||
// A mesh extending 12 m along +Y, entity rotated 90° about Z -> the extent
|
||||
// now points along -X (or +X depending on sign); the world box must follow.
|
||||
var entity = new WorldEntity
|
||||
{
|
||||
Id = 1,
|
||||
SourceGfxObjOrSetupId = 0x01000001,
|
||||
Position = Vector3.Zero,
|
||||
Rotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, System.MathF.PI / 2f),
|
||||
MeshRefs = new[] { new MeshRef(0x01000001, Matrix4x4.Identity) },
|
||||
};
|
||||
entity.SetLocalBounds(new Vector3(-1, 0, -1), new Vector3(1, 12, 1));
|
||||
entity.RefreshAabb();
|
||||
|
||||
// Rotating +Y by +90° about Z lands on -X: the box must extend ≥12 m on X
|
||||
// (one side), and no longer require 12 m on Y.
|
||||
bool coversX = entity.AabbMin.X <= -12f || entity.AabbMax.X >= 12f;
|
||||
Assert.True(coversX, $"rotated extent must show on X axis (box X [{entity.AabbMin.X},{entity.AabbMax.X}])");
|
||||
Assert.True(entity.AabbMax.Y < 12f, "the unrotated +Y extent must not persist after rotation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Aabb_LocalBoundsAccumulator_UnionsTransformedParts()
|
||||
{
|
||||
var acc = new AcDream.Core.Meshing.LocalBoundsAccumulator();
|
||||
Assert.False(acc.TryGet(out _, out _));
|
||||
|
||||
// Part 1: unit cube at origin. Part 2: unit cube translated to z=15.
|
||||
acc.Add(Matrix4x4.Identity, (new Vector3(-0.5f), new Vector3(0.5f)));
|
||||
acc.Add(Matrix4x4.CreateTranslation(3, 3, 15), (new Vector3(-0.5f), new Vector3(0.5f)));
|
||||
|
||||
Assert.True(acc.TryGet(out var min, out var max));
|
||||
Assert.Equal(-0.5f, min.Z, 3);
|
||||
Assert.Equal(15.5f, max.Z, 3);
|
||||
Assert.Equal(3.5f, max.X, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Aabb_DirtyFlag_SetByMutator_ClearedByRefresh()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue