Split concavity-bridging slabs in the collider baker
A slab that spans a concave silhouette (e.g. a table side = two legs plus a top rail, all coplanar and thin) produced one convex hull that filled the open gap between the legs, so a player standing between them counted as embedded. Add a refinement pass: each slab whose in-plane coverage grid shows its convex-hull footprint is less than a configurable fraction (hullFillThreshold, default 0.6) covered by real surface is recursively halved along its larger PCA axis until every piece hugs actual geometry. Solid boards (the shelf) are ~fully covered and pass through untouched. Verified over a 9600-point grid: zero bridging (no collider interior lies >6cm from the source surface), shelf output unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
5c1f31a887
commit
410f9808b5
1 file changed
+175
@@ -44,6 +44,9 @@ public class ConcealmentColliderBaker : MonoBehaviour
|
||||
[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("A slab is recursively split until its convex-hull footprint is at least this fraction covered by real surface (0..1). Guards against one hull bridging open gaps between coplanar members (e.g. a table side = 2 legs + top rail merge into a flat panel whose hull fills the empty gap). Lower => tolerate more concavity/bridging; higher => split into more, tighter surface-hugging pieces. Solid boards (~fully covered) are never split.")]
|
||||
[Range(0f, 1f)] [SerializeField] private float hullFillThreshold = 0.6f;
|
||||
|
||||
[Tooltip("Clusters with fewer triangles than this are discarded as noise.")]
|
||||
[Min(1)] [SerializeField] private int minClusterTriangles = 1;
|
||||
|
||||
@@ -59,6 +62,7 @@ public class ConcealmentColliderBaker : MonoBehaviour
|
||||
|
||||
private const string ContainerName = "ConcealmentColliders (generated)";
|
||||
private const string AssetFolder = "Assets/Generated/ConcealmentColliders";
|
||||
private const int MaxRefineDepth = 8;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[ContextMenu("Bake Convex Colliders")]
|
||||
@@ -162,6 +166,16 @@ public class ConcealmentColliderBaker : MonoBehaviour
|
||||
clusters.Add(cluster);
|
||||
}
|
||||
|
||||
// 3b. Refine: split slabs whose convex hull would bridge open concavities. A table side is
|
||||
// 2 legs + a top rail — all coplanar and thin, so they merge into ONE slab whose hull
|
||||
// fills the empty gap between the legs. Each slab is recursively halved along its larger
|
||||
// in-plane axis until its hull footprint is mostly covered by real surface. Solid boards
|
||||
// (the shelf) are already ~fully covered => returned untouched.
|
||||
var refined = new List<List<int>>(clusters.Count);
|
||||
for (int c = 0; c < clusters.Count; c++)
|
||||
RefineCluster(clusters[c], wv, triA, triB, triC, 0, refined);
|
||||
clusters = refined;
|
||||
|
||||
// 4. Filter noise + cap count (keep largest slabs).
|
||||
clusters.RemoveAll(c => c.Count < minClusterTriangles);
|
||||
clusters.Sort((x, y) => y.Count.CompareTo(x.Count));
|
||||
@@ -415,6 +429,167 @@ public class ConcealmentColliderBaker : MonoBehaviour
|
||||
return mx - mn;
|
||||
}
|
||||
|
||||
// ---- Concavity refinement (split slabs whose hull would bridge open gaps) ----
|
||||
|
||||
/// <summary>
|
||||
/// Recursively halves a slab cluster along its larger in-plane axis until its hull footprint is
|
||||
/// at least <see cref="hullFillThreshold"/> covered by real surface (or it gets too small / too deep).
|
||||
/// Prevents one convex hull from bridging the empty space between disjoint coplanar members (table legs).
|
||||
/// </summary>
|
||||
private void RefineCluster(List<int> cluster, List<Vector3> wv, List<int> triA, List<int> triB, List<int> triC, int depth, List<List<int>> outClusters)
|
||||
{
|
||||
if (cluster.Count <= 2 || depth >= MaxRefineDepth)
|
||||
{
|
||||
outClusters.Add(cluster);
|
||||
return;
|
||||
}
|
||||
|
||||
float fill = ClusterFill(cluster, wv, triA, triB, triC, out Vector3 axis);
|
||||
if (fill >= hullFillThreshold)
|
||||
{
|
||||
outClusters.Add(cluster);
|
||||
return;
|
||||
}
|
||||
|
||||
// Halve at the centroid median along the larger in-plane axis. Recursion + the fill test
|
||||
// converge on surface-hugging pieces; a solid board is >= threshold at once and never splits.
|
||||
int m = cluster.Count;
|
||||
var proj = new float[m];
|
||||
for (int j = 0; j < m; j++)
|
||||
proj[j] = Vector3.Dot((wv[triA[cluster[j]]] + wv[triB[cluster[j]]] + wv[triC[cluster[j]]]) * (1f / 3f), axis);
|
||||
var sorted = (float[])proj.Clone();
|
||||
Array.Sort(sorted);
|
||||
float median = sorted[m / 2];
|
||||
|
||||
var left = new List<int>();
|
||||
var right = new List<int>();
|
||||
for (int j = 0; j < m; j++)
|
||||
{
|
||||
if (proj[j] < median) left.Add(cluster[j]); else right.Add(cluster[j]);
|
||||
}
|
||||
if (left.Count == 0 || right.Count == 0)
|
||||
{
|
||||
outClusters.Add(cluster); // all centroids coincide on the axis — can't halve, keep as-is
|
||||
return;
|
||||
}
|
||||
|
||||
RefineCluster(left, wv, triA, triB, triC, depth + 1, outClusters);
|
||||
RefineCluster(right, wv, triA, triB, triC, depth + 1, outClusters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fraction (0..1) of the cluster's in-plane bounding footprint actually covered by surface, via a
|
||||
/// coverage grid on the PCA plane (union of the projected triangles — single-/double-sided agnostic).
|
||||
/// Low fill => the convex hull would enclose empty space. Outputs the larger in-plane axis (split dir).
|
||||
/// </summary>
|
||||
private static float ClusterFill(List<int> cluster, List<Vector3> wv, List<int> triA, List<int> triB, List<int> triC, out Vector3 splitAxis)
|
||||
{
|
||||
var pts = new List<Vector3>(cluster.Count * 3);
|
||||
var seen = new HashSet<int>();
|
||||
for (int j = 0; j < cluster.Count; j++)
|
||||
{
|
||||
int t = cluster[j];
|
||||
if (seen.Add(triA[t])) pts.Add(wv[triA[t]]);
|
||||
if (seen.Add(triB[t])) pts.Add(wv[triB[t]]);
|
||||
if (seen.Add(triC[t])) pts.Add(wv[triC[t]]);
|
||||
}
|
||||
|
||||
PcaFrame(pts, out Vector3 u, out Vector3 v, out _);
|
||||
|
||||
float umin = float.MaxValue, umax = float.MinValue, vmin = float.MaxValue, vmax = float.MinValue;
|
||||
for (int i = 0; i < pts.Count; i++)
|
||||
{
|
||||
float pu = Vector3.Dot(pts[i], u), pv = Vector3.Dot(pts[i], v);
|
||||
if (pu < umin) umin = pu; if (pu > umax) umax = pu;
|
||||
if (pv < vmin) vmin = pv; if (pv > vmax) vmax = pv;
|
||||
}
|
||||
float eu = Mathf.Max(umax - umin, 1e-5f), ev = Mathf.Max(vmax - vmin, 1e-5f);
|
||||
splitAxis = eu >= ev ? u : v;
|
||||
|
||||
float cell = Mathf.Max(Mathf.Max(eu, ev) / 32f, 1e-4f);
|
||||
int NU = Mathf.Clamp(Mathf.CeilToInt(eu / cell), 1, 40);
|
||||
int NV = Mathf.Clamp(Mathf.CeilToInt(ev / cell), 1, 40);
|
||||
var cov = new bool[NU, NV];
|
||||
for (int j = 0; j < cluster.Count; j++)
|
||||
{
|
||||
int t = cluster[j];
|
||||
RasterTri(cov, NU, NV,
|
||||
ToGrid(wv[triA[t]], u, v, umin, vmin, eu, ev, NU, NV),
|
||||
ToGrid(wv[triB[t]], u, v, umin, vmin, eu, ev, NU, NV),
|
||||
ToGrid(wv[triC[t]], u, v, umin, vmin, eu, ev, NU, NV));
|
||||
}
|
||||
|
||||
int covered = 0;
|
||||
for (int a = 0; a < NU; a++) for (int b = 0; b < NV; b++) if (cov[a, b]) covered++;
|
||||
return (float)covered / (NU * NV);
|
||||
}
|
||||
|
||||
private static Vector2 ToGrid(Vector3 p, Vector3 u, Vector3 v, float umin, float vmin, float eu, float ev, int NU, int NV)
|
||||
=> new Vector2((Vector3.Dot(p, u) - umin) / eu * NU, (Vector3.Dot(p, v) - vmin) / ev * NV);
|
||||
|
||||
/// <summary>Marks every grid cell whose center falls inside the 2D triangle (edge-on triangles paint their thin bbox strip).</summary>
|
||||
private static void RasterTri(bool[,] cov, int NU, int NV, Vector2 a, Vector2 b, Vector2 c)
|
||||
{
|
||||
float minx = Mathf.Min(a.x, Mathf.Min(b.x, c.x)), maxx = Mathf.Max(a.x, Mathf.Max(b.x, c.x));
|
||||
float miny = Mathf.Min(a.y, Mathf.Min(b.y, c.y)), maxy = Mathf.Max(a.y, Mathf.Max(b.y, c.y));
|
||||
int x0 = Mathf.Clamp(Mathf.FloorToInt(minx), 0, NU - 1), x1 = Mathf.Clamp(Mathf.CeilToInt(maxx), 0, NU - 1);
|
||||
int y0 = Mathf.Clamp(Mathf.FloorToInt(miny), 0, NV - 1), y1 = Mathf.Clamp(Mathf.CeilToInt(maxy), 0, NV - 1);
|
||||
|
||||
float d = (b.y - c.y) * (a.x - c.x) + (c.x - b.x) * (a.y - c.y);
|
||||
if (Mathf.Abs(d) < 1e-9f)
|
||||
{
|
||||
for (int x = x0; x <= x1; x++) for (int y = y0; y <= y1; y++) cov[x, y] = true;
|
||||
return;
|
||||
}
|
||||
for (int x = x0; x <= x1; x++)
|
||||
for (int y = y0; y <= y1; y++)
|
||||
{
|
||||
float px = x + 0.5f, py = y + 0.5f;
|
||||
float w0 = ((b.y - c.y) * (px - c.x) + (c.x - b.x) * (py - c.y)) / d;
|
||||
float w1 = ((c.y - a.y) * (px - c.x) + (a.x - c.x) * (py - c.y)) / d;
|
||||
float w2 = 1f - w0 - w1;
|
||||
if (w0 >= -1e-3f && w1 >= -1e-3f && w2 >= -1e-3f) cov[x, y] = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>PCA orthonormal frame of a point cloud: u = major axis, v = mid, n = minor (slab normal). Eigenvectors sorted by variance desc.</summary>
|
||||
private static void PcaFrame(List<Vector3> pts, out Vector3 u, out Vector3 v, out Vector3 n)
|
||||
{
|
||||
u = Vector3.right; v = Vector3.up; n = Vector3.forward;
|
||||
int cnt = pts.Count;
|
||||
if (cnt < 2) return;
|
||||
|
||||
Vector3 mean = Vector3.zero;
|
||||
for (int i = 0; i < cnt; i++) mean += pts[i];
|
||||
mean /= cnt;
|
||||
|
||||
double c00 = 0, c01 = 0, c02 = 0, c11 = 0, c12 = 0, c22 = 0;
|
||||
for (int i = 0; i < cnt; 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 mat = new double[3, 3] { { c00, c01, c02 }, { c01, c11, c12 }, { c02, c12, c22 } };
|
||||
Jacobi3(mat, out double[,] vec, out double[] d);
|
||||
|
||||
int hi = 0, mid = 1, lo = 2;
|
||||
if (d[hi] < d[mid]) { int t = hi; hi = mid; mid = t; }
|
||||
if (d[mid] < d[lo]) { int t = mid; mid = lo; lo = t; }
|
||||
if (d[hi] < d[mid]) { int t = hi; hi = mid; mid = t; }
|
||||
|
||||
u = AxisOr(vec, hi, Vector3.right);
|
||||
v = AxisOr(vec, mid, Vector3.up);
|
||||
n = AxisOr(vec, lo, Vector3.forward);
|
||||
}
|
||||
|
||||
private static Vector3 AxisOr(double[,] vec, int col, Vector3 fallback)
|
||||
{
|
||||
Vector3 a = new Vector3((float)vec[0, col], (float)vec[1, col], (float)vec[2, col]);
|
||||
return a.sqrMagnitude < 1e-12f ? fallback : a.normalized;
|
||||
}
|
||||
|
||||
/// <summary>Cyclic Jacobi eigendecomposition of a symmetric 3x3 matrix (eigenvectors in columns of v).</summary>
|
||||
private static void Jacobi3(double[,] a, out double[,] v, out double[] d)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user