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 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 4.8 committed 2026-07-08 07:23:48 +02:00
1 parent 50781e32d2
commit f4d31b0c24
1 file changed
+26 -4
@@ -88,6 +88,15 @@ public class ConcealmentMeter : MonoBehaviour
private readonly List<MeshCollider> _concave = new List<MeshCollider>();
private readonly List<ConcealmentVolume> _volumes = new List<ConcealmentVolume>();
// 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<Bounds> _convexBounds = new List<Bounds>();
private readonly List<Bounds> _concaveBounds = new List<Bounds>();
private readonly List<Bounds> _volumeBounds = new List<Bounds>();
private readonly List<Vector3> _gizPos = new List<Vector3>();
private readonly List<bool> _gizEmbedded = new List<bool>();
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<ConcealmentVolume>()
&& !mc.GetComponentInParent<ConcealmentColliderBaker>())
{
_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;
}
}