From f4d31b0c24c0d5aedfa3b9b019a9dc5058de76b1 Mon Sep 17 00:00:00 2001 From: MelekTaus Moribundus Date: Wed, 8 Jul 2026 07:23:48 +0200 Subject: [PATCH] ConcealmentMeter: add per-collider AABB broadphase in the voxel loop Cache each environment candidate's world AABB once per evaluation and gate the expensive per-voxel tests (Collider.ClosestPoint, ContainsWorld, concave raycast) behind a cheap bounds.Contains prefilter. Collapses the hot loop from O(voxel x colliders) worth of ClosestPoint/raycast calls to O(voxel x few) for dense collider clusters (VHACD hulls, furniture), removing the framedrop when overlapping many colliders. Conservative AABB test never drops a real hit, so EmbeddedPercent is identical, just cheaper. Co-Authored-By: Claude Opus 4.8 --- .../Concealment/ConcealmentMeter.cs | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/Assets/Scripts/Components/Concealment/ConcealmentMeter.cs b/Assets/Scripts/Components/Concealment/ConcealmentMeter.cs index 3039492..cb66bf0 100644 --- a/Assets/Scripts/Components/Concealment/ConcealmentMeter.cs +++ b/Assets/Scripts/Components/Concealment/ConcealmentMeter.cs @@ -88,6 +88,15 @@ public class ConcealmentMeter : MonoBehaviour private readonly List _concave = new List(); private readonly List _volumes = new List(); + // World AABBs parallel to _convex/_concave/_volumes, fetched ONCE per eval. The per-voxel hot loop + // rejects a candidate with a cheap Bounds.Contains before the expensive ClosestPoint / raycast / + // InverseTransformPoint. A point inside a collider is always inside its world AABB, so this is a + // conservative filter -> never a false negative, only fewer expensive tests. This is what keeps cost + // ~O(voxels) instead of O(voxels x colliders) when the body overlaps many colliders (e.g. VHACD hulls). + private readonly List _convexBounds = new List(); + private readonly List _concaveBounds = new List(); + private readonly List _volumeBounds = new List(); + private readonly List _gizPos = new List(); private readonly List _gizEmbedded = new List(); private float _gizVoxelWorld; @@ -273,6 +282,9 @@ public class ConcealmentMeter : MonoBehaviour _terrains.Clear(); _concave.Clear(); _volumes.Clear(); + _convexBounds.Clear(); + _concaveBounds.Clear(); + _volumeBounds.Clear(); // Baked concealment volumes are authoritative for the (open-shell) furniture they cover; // gathered from the registry independent of colliders so they need no specific collider type. @@ -282,7 +294,8 @@ public class ConcealmentMeter : MonoBehaviour ConcealmentVolume v = vols[i]; if (!v || !v.HasBake) continue; if (characterRoot && (v.transform == characterRoot || v.transform.IsChildOf(characterRoot))) continue; - if (v.WorldBounds.Intersects(wb)) _volumes.Add(v); + Bounds vb = v.WorldBounds; // getter recomputes a matrix transform -> take it once, reuse as the hot-loop filter + if (vb.Intersects(wb)) { _volumes.Add(v); _volumeBounds.Add(vb); } } int n = Physics.OverlapBoxNonAlloc(wb.center, wb.extents, _overlap, Quaternion.identity, @@ -311,11 +324,15 @@ public class ConcealmentMeter : MonoBehaviour // A baked ConcealmentVolume or ConcealmentColliderBaker supersedes raycast parity for this object. if (includeConcaveMeshColliders && !mc.GetComponentInParent() && !mc.GetComponentInParent()) + { _concave.Add(mc); + _concaveBounds.Add(mc.bounds); + } continue; // concave can't use ClosestPoint } _convex.Add(col); // Box / Sphere / Capsule / convex MeshCollider + _convexBounds.Add(col.bounds); } } @@ -323,12 +340,16 @@ public class ConcealmentMeter : MonoBehaviour { for (int i = 0; i < _volumes.Count; i++) { - if (_volumes[i].ContainsWorld(p)) return true; + // AABB reject first: skips the ContainsWorld InverseTransformPoint matrix mult for far voxels. + if (_volumeBounds[i].Contains(p) && _volumes[i].ContainsWorld(p)) return true; } for (int i = 0; i < _convex.Count; i++) { - if ((_convex[i].ClosestPoint(p) - p).sqrMagnitude <= InsideEpsSqr) return true; + // AABB reject first: ClosestPoint (esp. on convex MeshColliders / VHACD hulls) is the dominant + // per-voxel cost; a point outside the collider's world AABB cannot be inside it. + if (_convexBounds[i].Contains(p) && + (_convex[i].ClosestPoint(p) - p).sqrMagnitude <= InsideEpsSqr) return true; } if (includeTerrain) @@ -348,7 +369,8 @@ public class ConcealmentMeter : MonoBehaviour { for (int i = 0; i < _concave.Count; i++) { - if (InsideConcave(_concave[i], p)) return true; + // AABB reject first: InsideConcave fires a raycast loop up to 1000 units per voxel -> gate hard. + if (_concaveBounds[i].Contains(p) && InsideConcave(_concave[i], p)) return true; } }