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; } }