diff --git a/Assets/Scripts/ConcealmentColliderBaker.cs b/Assets/Scripts/ConcealmentColliderBaker.cs new file mode 100644 index 0000000..828eb87 --- /dev/null +++ b/Assets/Scripts/ConcealmentColliderBaker.cs @@ -0,0 +1,608 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +#if UNITY_EDITOR +using UnityEditor; +using UnityEditor.SceneManagement; +#endif + +/// +/// Editor tool that decomposes an environment mesh (typically an open-shell / non-watertight +/// furniture mesh) into a small set of CONVEX MeshColliders via surface clustering, so +/// can treat the furniture as "embeddable" through its +/// convex-collider ClosestPoint path WITHOUT a single convex hull filling the open compartments. +/// +/// Approach (no voxels, no native plugin — "approximate convex decomposition" on the surface): +/// 1. Weld coincident vertices so connectivity is detected across split-normal/UV seams. +/// 2. Grow connected triangle regions into SLABS: a region keeps absorbing edge-adjacent +/// triangles while it stays thinner than along its flattest +/// axis (PCA minor axis). This spans both faces of one board (they are within a board's +/// thickness) but refuses to jump the gap to the next board (that would thicken the region). +/// 3. Convex-hull each slab into its own convex collider. Boards become thin convex slabs; +/// the empty space between boards is covered by no collider, so it stays hollow. +/// +/// The colliders are real child GameObjects referencing SAVED hull-mesh assets, so they survive +/// domain reload / play-mode / scene save (unlike an in-memory-only bake). +/// +/// Runtime-inert: the component exists only so can detect it in a +/// collider's parent and skip the furniture's own concave MeshCollider (exactly like a baked +/// ConcealmentVolume does) — the generated convex slabs are the authoritative embedding shape. +/// +[DisallowMultipleComponent] +public class ConcealmentColliderBaker : MonoBehaviour +{ + [Tooltip("Optional. If empty, the shared mesh of a MeshFilter on this object (or a child) is used.")] + [SerializeField] private Mesh sourceMesh; + + [Header("Clustering")] + [Tooltip("Vertices closer than this (local units) are welded, so shared edges are found across split-normal/UV seams. ~ a hair below the smallest real feature.")] + [Min(1e-4f)] [SerializeField] private float weldEpsilon = 1e-3f; + + [Tooltip("A connected region keeps growing while it stays thinner than this (local units) along its flattest axis. ~ board thickness. Larger => fewer, chunkier colliders (risk of bridging compartments); smaller => more, thinner slabs.")] + [Min(1e-3f)] [SerializeField] private float maxSlabThickness = 0.12f; + + [Tooltip("Flat/thin clusters are inflated to at least this thickness (local units) so the convex collider has usable volume for ClosestPoint tests.")] + [Min(1e-3f)] [SerializeField] private float minThickness = 0.04f; + + [Tooltip("Clusters with fewer triangles than this are discarded as noise.")] + [Min(1)] [SerializeField] private int minClusterTriangles = 1; + + [Tooltip("Hard cap on generated colliders. The meter's broadphase is capped at 64 colliders TOTAL across all environment in range, so keep this modest. Largest clusters win.")] + [Min(1)] [SerializeField] private int maxColliders = 24; + + [Header("Output")] + [Tooltip("If true, generated colliders inherit the source object's layer (so they land on the meter's environment mask). Otherwise 'Collider Layer' is used.")] + [SerializeField] private bool useSourceLayer = true; + [SerializeField] private int colliderLayer; + + [SerializeField, HideInInspector] private List generatedAssetPaths = new List(); + + private const string ContainerName = "ConcealmentColliders (generated)"; + private const string AssetFolder = "Assets/Generated/ConcealmentColliders"; + +#if UNITY_EDITOR + [ContextMenu("Bake Convex Colliders")] + public void Bake() + { + Mesh mesh = sourceMesh; + Transform meshSpace = transform; + if (!mesh) + { + MeshFilter mf = GetComponentInChildren(); + if (mf && mf.sharedMesh) { mesh = mf.sharedMesh; meshSpace = mf.transform; } + } + if (!mesh) + { + Debug.LogError($"[ConcealmentColliderBaker] '{name}': no source mesh (assign one or add a MeshFilter).", this); + return; + } + + Clear(); // remove previous container + assets first + + string baseName = ResolveAssetBaseName(); + + Vector3[] rawVerts = mesh.vertices; + int[] rawTris = mesh.triangles; + + // 1. Weld coincident vertices. + WeldVertices(rawVerts, weldEpsilon, out List wv, out int[] map); + + // Welded triangles (drop degenerate). + var triA = new List(); var triB = new List(); var triC = new List(); + for (int i = 0; i + 2 < rawTris.Length; i += 3) + { + int a = map[rawTris[i]], b = map[rawTris[i + 1]], c = map[rawTris[i + 2]]; + if (a == b || b == c || a == c) continue; + triA.Add(a); triB.Add(b); triC.Add(c); + } + int triCount = triA.Count; + if (triCount == 0) + { + Debug.LogWarning($"[ConcealmentColliderBaker] '{name}': mesh has no valid triangles after welding.", this); + return; + } + + // 2. Edge adjacency (welded edges shared by triangles). + var edgeToTri = new Dictionary(triCount * 3); + var neighbors = new List[triCount]; + for (int t = 0; t < triCount; t++) neighbors[t] = new List(); + for (int t = 0; t < triCount; t++) + { + LinkEdge(edgeToTri, neighbors, triA[t], triB[t], t); + LinkEdge(edgeToTri, neighbors, triB[t], triC[t], t); + LinkEdge(edgeToTri, neighbors, triC[t], triA[t], t); + } + + // 3. Grow connected triangles into thickness-bounded slabs. + // Disconnected boards never link (separate components); connected assemblies split + // wherever a region would exceed maxSlabThickness. + var clusters = new List>(); + bool[] assigned = new bool[triCount]; + var queue = new Queue(); + var vset = new HashSet(); + var vpos = new List(); + var added = new List(); + + for (int s = 0; s < triCount; s++) + { + if (assigned[s]) continue; + + var cluster = new List { s }; + assigned[s] = true; + vset.Clear(); vpos.Clear(); + AddTriVerts(vset, vpos, wv, triA[s], triB[s], triC[s], null); + + queue.Clear(); + for (int k = 0; k < neighbors[s].Count; k++) queue.Enqueue(neighbors[s][k]); + + while (queue.Count > 0) + { + int t = queue.Dequeue(); + if (assigned[t]) continue; + + int before = vpos.Count; + AddTriVerts(vset, vpos, wv, triA[t], triB[t], triC[t], added); + + float th = SlabThickness(vpos, out _); + if (th <= maxSlabThickness) + { + assigned[t] = true; + cluster.Add(t); + for (int k = 0; k < neighbors[t].Count; k++) + if (!assigned[neighbors[t][k]]) queue.Enqueue(neighbors[t][k]); + } + else + { + // Revert the tentatively added verts. + for (int k = 0; k < added.Count; k++) vset.Remove(added[k]); + if (vpos.Count > before) vpos.RemoveRange(before, vpos.Count - before); + } + } + + clusters.Add(cluster); + } + + // 4. Filter noise + cap count (keep largest slabs). + clusters.RemoveAll(c => c.Count < minClusterTriangles); + clusters.Sort((x, y) => y.Count.CompareTo(x.Count)); + int dropped = 0; + if (clusters.Count > maxColliders) + { + dropped = clusters.Count - maxColliders; + clusters.RemoveRange(maxColliders, dropped); + } + + // 5. Build a convex collider per slab. + EnsureFolder(); + Transform container = new GameObject(ContainerName).transform; + container.SetParent(meshSpace, false); + container.localPosition = Vector3.zero; + container.localRotation = Quaternion.identity; + container.localScale = Vector3.one; + int outLayer = useSourceLayer ? gameObject.layer : colliderLayer; + + int made = 0; + for (int ci = 0; ci < clusters.Count; ci++) + { + // Unique verts of this cluster. + var pset = new HashSet(); + var pts = new List(); + List cl = clusters[ci]; + for (int j = 0; j < cl.Count; j++) + { + int t = cl[j]; + if (pset.Add(triA[t])) pts.Add(wv[triA[t]]); + if (pset.Add(triB[t])) pts.Add(wv[triB[t]]); + if (pset.Add(triC[t])) pts.Add(wv[triC[t]]); + } + + float thick = SlabThickness(pts, out Vector3 normal); + List hullPts; + if (thick < minThickness) + { + // Inflate a (near-)flat slab into a thin prism so the hull has volume. + float half = minThickness * 0.5f; + hullPts = new List(pts.Count * 2); + for (int k = 0; k < pts.Count; k++) + { + hullPts.Add(pts[k] + normal * half); + hullPts.Add(pts[k] - normal * half); + } + } + else hullPts = pts; + + if (!ComputeConvexHull(hullPts.ToArray(), out List hv, out List ht) || hv.Count < 4) + continue; + + Mesh cm = new Mesh { name = $"{baseName}_{made}" }; + cm.SetVertices(hv); + cm.SetTriangles(ht, 0); + cm.RecalculateBounds(); + + string path = $"{AssetFolder}/{baseName}_{made}.asset"; + AssetDatabase.CreateAsset(cm, path); + generatedAssetPaths.Add(path); + + GameObject go = new GameObject($"hull_{made}"); + go.transform.SetParent(container, false); + go.layer = outLayer; + MeshCollider col = go.AddComponent(); + col.sharedMesh = cm; + col.convex = true; + col.isTrigger = false; + + made++; + } + + AssetDatabase.SaveAssets(); + EditorUtility.SetDirty(this); + UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(gameObject.scene); + + Debug.Log($"[ConcealmentColliderBaker] '{name}': baked {made} convex collider(s)" + + (dropped > 0 ? $" (+{dropped} smaller slab(s) dropped by Max Colliders={maxColliders})" : "") + + ". Save the scene to persist.", this); + } + + [ContextMenu("Clear Baked Colliders")] + public void Clear() + { + for (int i = 0; i < generatedAssetPaths.Count; i++) + if (!string.IsNullOrEmpty(generatedAssetPaths[i])) + AssetDatabase.DeleteAsset(generatedAssetPaths[i]); + generatedAssetPaths.Clear(); + + var all = GetComponentsInChildren(true); + for (int i = 0; i < all.Length; i++) + if (all[i] && all[i] != transform && all[i].name == ContainerName) + DestroyImmediate(all[i].gameObject); + + EditorUtility.SetDirty(this); + if (gameObject.scene.IsValid()) + UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(gameObject.scene); + } + + private static void EnsureFolder() + { + if (!AssetDatabase.IsValidFolder("Assets/Generated")) + AssetDatabase.CreateFolder("Assets", "Generated"); + if (!AssetDatabase.IsValidFolder(AssetFolder)) + AssetDatabase.CreateFolder("Assets/Generated", "ConcealmentColliders"); + } + + /// + /// Readable, deterministic base name for the generated collider-mesh assets (replaces the old GUID): + /// - Prefab stage: "{assetPath without 'Assets/' and '.prefab'}/{path below the prefab root}" + /// - Scene: "{sceneName}/{full hierarchy path from the scene root}" + /// Sanitized to a filesystem-safe token; on collision with a DIFFERENT baker's assets, "_2", "_3", … is appended. + /// + private string ResolveAssetBaseName() + { + string raw; + PrefabStage stage = PrefabStageUtility.GetPrefabStage(gameObject); + if (stage != null) + { + string ap = stage.assetPath; + if (ap.StartsWith("Assets/")) ap = ap.Substring("Assets/".Length); + if (ap.EndsWith(".prefab")) ap = ap.Substring(0, ap.Length - ".prefab".Length); + // Path below the prefab root — the root's name already lives in the asset path. + string hier = GetHierarchyPath(transform, stage.prefabContentsRoot.transform); + raw = hier.Length == 0 ? ap : ap + "/" + hier; + } + else + { + string scene = string.IsNullOrEmpty(gameObject.scene.name) ? "Untitled" : gameObject.scene.name; + raw = scene + "/" + GetHierarchyPath(transform, null); + } + + string baseName = SanitizeFileName(raw); + if (baseName.Length == 0) baseName = "Bake"; + + // Clear() already removed THIS baker's assets, so a hit here means another baker resolved to the + // same name -> disambiguate. The per-collider "_0" file is the presence marker of a base name. + string candidate = baseName; + for (int dup = 2; System.IO.File.Exists($"{AssetFolder}/{candidate}_0.asset"); dup++) + candidate = $"{baseName}_{dup}"; + return candidate; + } + + /// Slash-joined transform names from up to (but excluding) (null = up to the root). + private static string GetHierarchyPath(Transform t, Transform stopAt) + { + var names = new List(); + for (Transform cur = t; cur && cur != stopAt; cur = cur.parent) + names.Add(cur.name); + names.Reverse(); + return string.Join("/", names); + } + + /// Collapses every run of non-[A-Za-z0-9-] characters to a single '_' and trims leading/trailing '_'. + private static string SanitizeFileName(string s) + { + var sb = new System.Text.StringBuilder(s.Length); + bool pendingSep = false; + for (int i = 0; i < s.Length; i++) + { + char c = s[i]; + bool keep = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-'; + if (keep) + { + if (pendingSep && sb.Length > 0) sb.Append('_'); + pendingSep = false; + sb.Append(c); + } + else pendingSep = true; + } + return sb.ToString(); + } +#endif + + // ---- Welding & clustering helpers (runtime-safe: no editor API) ---- + + private static void WeldVertices(Vector3[] src, float eps, out List welded, out int[] map) + { + welded = new List(src.Length); + map = new int[src.Length]; + float inv = 1f / Mathf.Max(eps, 1e-6f); + var lut = new Dictionary(src.Length); + for (int i = 0; i < src.Length; i++) + { + Vector3 p = src[i]; + int qx = Mathf.RoundToInt(p.x * inv); + int qy = Mathf.RoundToInt(p.y * inv); + int qz = Mathf.RoundToInt(p.z * inv); + long key = ((long)(qx & 0x1FFFFF) << 42) | ((long)(qy & 0x1FFFFF) << 21) | (long)(qz & 0x1FFFFF); + if (lut.TryGetValue(key, out int idx)) { map[i] = idx; } + else { idx = welded.Count; lut[key] = idx; welded.Add(p); map[i] = idx; } + } + } + + private static void LinkEdge(Dictionary edgeToTri, List[] neighbors, int u, int v, int tri) + { + long key = EdgeKey(u, v); + if (edgeToTri.TryGetValue(key, out int other)) + { + neighbors[tri].Add(other); + neighbors[other].Add(tri); + edgeToTri[key] = tri; // chain non-manifold edges (>2 tris) so all get linked + } + else edgeToTri[key] = tri; + } + + private static void AddTriVerts(HashSet set, List pos, List wv, int a, int b, int c, List addedOut) + { + addedOut?.Clear(); + if (set.Add(a)) { pos.Add(wv[a]); addedOut?.Add(a); } + if (set.Add(b)) { pos.Add(wv[b]); addedOut?.Add(b); } + if (set.Add(c)) { pos.Add(wv[c]); addedOut?.Add(c); } + } + + /// Extent of the point cloud along its PCA minor axis (= slab thickness), plus that axis. + private static float SlabThickness(List pts, out Vector3 normal) + { + normal = Vector3.up; + int n = pts.Count; + if (n < 2) return 0f; + + Vector3 mean = Vector3.zero; + for (int i = 0; i < n; i++) mean += pts[i]; + mean /= n; + + double c00 = 0, c01 = 0, c02 = 0, c11 = 0, c12 = 0, c22 = 0; + for (int i = 0; i < n; i++) + { + double x = pts[i].x - mean.x, y = pts[i].y - mean.y, z = pts[i].z - mean.z; + c00 += x * x; c01 += x * y; c02 += x * z; + c11 += y * y; c12 += y * z; c22 += z * z; + } + + var a = new double[3, 3] { { c00, c01, c02 }, { c01, c11, c12 }, { c02, c12, c22 } }; + Jacobi3(a, out double[,] v, out double[] d); + + int mi = 0; + if (d[1] < d[mi]) mi = 1; + if (d[2] < d[mi]) mi = 2; + normal = new Vector3((float)v[0, mi], (float)v[1, mi], (float)v[2, mi]); + if (normal.sqrMagnitude < 1e-12f) { normal = Vector3.up; return 0f; } + normal.Normalize(); + + float mn = float.MaxValue, mx = float.MinValue; + for (int i = 0; i < n; i++) + { + float dp = Vector3.Dot(pts[i], normal); + if (dp < mn) mn = dp; + if (dp > mx) mx = dp; + } + return mx - mn; + } + + /// Cyclic Jacobi eigendecomposition of a symmetric 3x3 matrix (eigenvectors in columns of v). + private static void Jacobi3(double[,] a, out double[,] v, out double[] d) + { + v = new double[3, 3] { { 1, 0, 0 }, { 0, 1, 0 }, { 0, 0, 1 } }; + for (int sweep = 0; sweep < 24; sweep++) + { + double off = Math.Abs(a[0, 1]) + Math.Abs(a[0, 2]) + Math.Abs(a[1, 2]); + if (off < 1e-18) break; + JacobiRotate(a, v, 0, 1); + JacobiRotate(a, v, 0, 2); + JacobiRotate(a, v, 1, 2); + } + d = new double[] { a[0, 0], a[1, 1], a[2, 2] }; + } + + private static void JacobiRotate(double[,] a, double[,] v, int p, int q) + { + double apq = a[p, q]; + if (Math.Abs(apq) < 1e-20) return; + + double app = a[p, p], aqq = a[q, q]; + double phi = 0.5 * Math.Atan2(2.0 * apq, aqq - app); + double c = Math.Cos(phi), s = Math.Sin(phi); + + a[p, p] = c * c * app - 2.0 * s * c * apq + s * s * aqq; + a[q, q] = s * s * app + 2.0 * s * c * apq + c * c * aqq; + a[p, q] = 0; a[q, p] = 0; + + int r = 3 - p - q; + double arp = a[r, p], arq = a[r, q]; + double nrp = c * arp - s * arq; + double nrq = s * arp + c * arq; + a[r, p] = nrp; a[p, r] = nrp; + a[r, q] = nrq; a[q, r] = nrq; + + for (int k = 0; k < 3; k++) + { + double vkp = v[k, p], vkq = v[k, q]; + v[k, p] = c * vkp - s * vkq; + v[k, q] = s * vkp + c * vkq; + } + } + + // ---- Convex hull (incremental; outputs a triangulated hull mesh) ---- + + private struct HFace { public int a, b, c; public Vector3 n; } + + private static bool ComputeConvexHull(Vector3[] pts, out List hullVertsOut, out List hullTrisOut) + { + hullVertsOut = new List(); + hullTrisOut = new List(); + + int n = pts.Length; + if (n < 4) return false; + + Vector3 bmn = pts[0], bmx = pts[0]; + for (int i = 1; i < n; i++) { bmn = Vector3.Min(bmn, pts[i]); bmx = Vector3.Max(bmx, pts[i]); } + float diag = (bmx - bmn).magnitude; + if (diag < 1e-6f) return false; + float eps = diag * 1e-6f; + + int minX = 0, maxX = 0; + for (int i = 1; i < n; i++) { if (pts[i].x < pts[minX].x) minX = i; if (pts[i].x > pts[maxX].x) maxX = i; } + int i0 = minX, i1 = maxX; + if (i0 == i1) return false; + + Vector3 lineDir = pts[i1] - pts[i0]; + float lineLen = lineDir.magnitude; + if (lineLen < eps) return false; + lineDir /= lineLen; + + int i2 = -1; float best = eps; + for (int i = 0; i < n; i++) + { + float dd = Vector3.Cross(pts[i] - pts[i0], lineDir).magnitude; + if (dd > best) { best = dd; i2 = i; } + } + if (i2 < 0) return false; + + Vector3 nrm0 = Vector3.Cross(pts[i1] - pts[i0], pts[i2] - pts[i0]); + float nl = nrm0.magnitude; + if (nl < eps) return false; + nrm0 /= nl; + + int i3 = -1; best = eps; + for (int i = 0; i < n; i++) + { + float dd = Mathf.Abs(Vector3.Dot(pts[i] - pts[i0], nrm0)); + if (dd > best) { best = dd; i3 = i; } + } + if (i3 < 0) return false; + + Vector3 interior = (pts[i0] + pts[i1] + pts[i2] + pts[i3]) * 0.25f; + + var faces = new List(4 + n); + AddHullFace(faces, pts, interior, i0, i1, i2); + AddHullFace(faces, pts, interior, i0, i1, i3); + AddHullFace(faces, pts, interior, i0, i2, i3); + AddHullFace(faces, pts, interior, i1, i2, i3); + + var visible = new List(); + var edgeCount = new Dictionary(); + var horizonA = new List(); + var horizonB = new List(); + + for (int pi = 0; pi < n; pi++) + { + if (pi == i0 || pi == i1 || pi == i2 || pi == i3) continue; + Vector3 p = pts[pi]; + + visible.Clear(); + for (int f = 0; f < faces.Count; f++) + { + HFace hf = faces[f]; + if (Vector3.Dot(hf.n, p - pts[hf.a]) > eps) visible.Add(f); + } + if (visible.Count == 0) continue; + + edgeCount.Clear(); + for (int vi = 0; vi < visible.Count; vi++) + { + HFace hf = faces[visible[vi]]; + AccEdge(edgeCount, hf.a, hf.b); + AccEdge(edgeCount, hf.b, hf.c); + AccEdge(edgeCount, hf.c, hf.a); + } + horizonA.Clear(); horizonB.Clear(); + for (int vi = 0; vi < visible.Count; vi++) + { + HFace hf = faces[visible[vi]]; + if (EdgeIsHorizon(edgeCount, hf.a, hf.b)) { horizonA.Add(hf.a); horizonB.Add(hf.b); } + if (EdgeIsHorizon(edgeCount, hf.b, hf.c)) { horizonA.Add(hf.b); horizonB.Add(hf.c); } + if (EdgeIsHorizon(edgeCount, hf.c, hf.a)) { horizonA.Add(hf.c); horizonB.Add(hf.a); } + } + + visible.Sort(); + for (int vi = visible.Count - 1; vi >= 0; vi--) faces.RemoveAt(visible[vi]); + + for (int h = 0; h < horizonA.Count; h++) + AddHullFace(faces, pts, interior, horizonA[h], horizonB[h], pi); + } + + if (faces.Count < 4) return false; + + var remap = new Dictionary(); + for (int f = 0; f < faces.Count; f++) + { + HFace hf = faces[f]; + hullTrisOut.Add(RemapVert(remap, hullVertsOut, pts, hf.a)); + hullTrisOut.Add(RemapVert(remap, hullVertsOut, pts, hf.b)); + hullTrisOut.Add(RemapVert(remap, hullVertsOut, pts, hf.c)); + } + return true; + } + + private static void AddHullFace(List faces, Vector3[] pts, Vector3 interior, int a, int b, int c) + { + Vector3 nn = Vector3.Cross(pts[b] - pts[a], pts[c] - pts[a]); + float l = nn.magnitude; + if (l < 1e-20f) return; + nn /= l; + if (Vector3.Dot(nn, pts[a] - interior) < 0f) nn = -nn; + faces.Add(new HFace { a = a, b = b, c = c, n = nn }); + } + + private static long EdgeKey(int u, int v) + { + int lo = u < v ? u : v, hi = u < v ? v : u; + return ((long)lo << 32) | (uint)hi; + } + + private static void AccEdge(Dictionary ec, int u, int v) + { + long k = EdgeKey(u, v); + ec.TryGetValue(k, out int c); + ec[k] = c + 1; + } + + private static bool EdgeIsHorizon(Dictionary ec, int u, int v) + { + ec.TryGetValue(EdgeKey(u, v), out int c); + return c == 1; + } + + private static int RemapVert(Dictionary remap, List outV, Vector3[] pts, int idx) + { + if (remap.TryGetValue(idx, out int r)) return r; + r = outV.Count; remap[idx] = r; outV.Add(pts[idx]); + return r; + } +} diff --git a/Assets/Scripts/ConcealmentColliderBaker.cs.meta b/Assets/Scripts/ConcealmentColliderBaker.cs.meta new file mode 100644 index 0000000..d0f74f6 --- /dev/null +++ b/Assets/Scripts/ConcealmentColliderBaker.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 80f683e75cf2f2a41bb32d8e9de0a2f0 \ No newline at end of file diff --git a/Assets/Scripts/ConcealmentMeter.cs b/Assets/Scripts/ConcealmentMeter.cs index 0efd26f..70f53e4 100644 --- a/Assets/Scripts/ConcealmentMeter.cs +++ b/Assets/Scripts/ConcealmentMeter.cs @@ -323,8 +323,9 @@ public class ConcealmentMeter : MonoBehaviour if (col is MeshCollider mc && !mc.convex) { - // A baked volume supersedes raycast parity for this object. - if (includeConcaveMeshColliders && !mc.GetComponentInParent()) + // A baked ConcealmentVolume or ConcealmentColliderBaker supersedes raycast parity for this object. + if (includeConcaveMeshColliders && !mc.GetComponentInParent() + && !mc.GetComponentInParent()) _concave.Add(mc); continue; // concave can't use ClosestPoint }