Add baked voxel concealment volumes for concave furniture

Replace unreliable raycast parity on open-shell meshes with editor-baked
voxel grids. ConcealmentVolume bakes an inside-mask per object using the
union of a generalized-winding-number test (solid material, robust to open
shells) and an exterior flood-fill (enclosed cavities). ConcealmentMeter
now prefers baked volumes via an O(1) world-space lookup through a registry,
falling back to the old concave parity only for un-baked colliders.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 4.8 committed 2026-06-27 09:10:47 +02:00
1 parent a3ef7055b4
commit 13de058819
5 files changed
+763 -452

No files matched your search

File diff suppressed because it is too large. Load diff
+55 -1
View File
@@ -3366,6 +3366,9 @@ PrefabInstance:
- targetCorrespondingSourceObject: {fileID: 156146, guid: 62a7d5dd45994494182ff5c1d8da0d3c, type: 3}
insertIndex: -1
addedObject: {fileID: 1666568580}
- targetCorrespondingSourceObject: {fileID: 156146, guid: 62a7d5dd45994494182ff5c1d8da0d3c, type: 3}
insertIndex: -1
addedObject: {fileID: 1666568584}
m_SourcePrefab: {fileID: 100100000, guid: 62a7d5dd45994494182ff5c1d8da0d3c, type: 3}
--- !u!4 &753428817 stripped
Transform:
@@ -4678,7 +4681,7 @@ MonoBehaviour:
serializedVersion: 2
m_Bits: 4294967295
includeConcaveMeshColliders: 0
includeTerrain: 1
includeTerrain: 0
evaluateInterval: 0.1
evaluateInPlayMode: 1
drawGizmos: 0
@@ -5006,6 +5009,30 @@ MeshCollider:
m_Convex: 0
m_CookingOptions: 30
m_Mesh: {fileID: 4300000, guid: 6bb1e412df9cd4efd8941377ebabb1be, type: 3}
--- !u!114 &1260370193
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1260370184}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 0a59a347a3128354ebd4f84afc97a0c5, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::ConcealmentVolume
resolution: 48
sourceMesh: {fileID: 0}
method: 0
drawBakedGizmos: 0
gizmoColor: {r: 0.2, g: 0.6, b: 1, a: 0.35}
origin: {x: -1.102527, y: -0.6764439, z: -0.04410102}
voxelSize: 0.044101078
nx: 50
ny: 31
nz: 15
bits: 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e00000000e0790000000080e701000000009e0700000000780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000780000000080e701000000009e0700000000781e00000000e0010000000000000000000000800700000000781e00000000e0790000000080e701000000001e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e00000000e0790000000080e701000000009e070000000078000000000000000000000000e001000000009e0700000000781e00000000e07900000000800700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800700000000781e00000000e0790000000080e701000000001e000000000000000000000000780000000080e701000000009e0700000000781e00000000e00100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e001000000009e0700000000781e00000000e0790000000080070000000000000000000000001e00000000e0790000000080e701000000009e0700000000780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000780000000080e701000000009e0700000000781e00000000e0010000000000000000000000800700000000781e00000000e0790000000080e701000000001e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e00000000e0790000000080e701000000009e070000000078000000000000000000000000e001000000009e0700000000781e00000000e07900000000800700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800700000000781e00000000e0790000000080e701000000001e000000000000000000000000780000000080e701000000009e0700000000781e00000000e00100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e001000000009e0700000000781e00000000e0790000000080070000000000000000000000001e00000000e0f9ffffffffffe701000000009e0700000000780400000000801000000000004200000000000801000000002004000000008010000000000042000000000008010000000020040000000080100000000000420000000000080100000000200400000000801000000000004200000000000801000000002004000000008010000000000042000000000008010000000020040000000080780000000080e7ffffffffff9fffffffffff7f1e00000000e001000000000000000000000080070000000078feffffffffff790000000080e701000000001e01000000002004000000008010000000000042000000000008010000000020040000000080100000000000420000000000080100000000200400000000801000000000004200000000000801000000002004000000008010000000000042000000000008010000000020040000000080100000000000420000000000080100000000201e00000000e0f9ffffffffffe7ffffffffff9f070000000078000000000000000000000000e001000000009effffffffff7f1e00000000e0790000000080470000000000080100000000200400000000801000000000004200000000000801000000002004000000008010000000000042000000000008010000000020040000000080100000000000420000000000080100000000200400000000801000000000004200000000000801000000002004000000008010000000000042000000000088070000000078fefffffffffff9ffffffffffe701000000001e000000000000000000000000780000000080e7ffffffffff9f0700000000781e00000000e01100000000004200000000000801000000002004000000008010000000000042000000000008010000000020040000000080100000000000420000000000080100000000200400000000801000000000004200000000000801000000002004000000008010000000000042000000000008010000000020040000000080100000000000e201000000009efffLine truncated
insideCount: 2891
--- !u!1 &1282734303
GameObject:
m_ObjectHideFlags: 0
@@ -6414,6 +6441,30 @@ MeshCollider:
m_Convex: 0
m_CookingOptions: 30
m_Mesh: {fileID: 4300000, guid: 0727ba69b779d42a0978a362226fad3e, type: 3}
--- !u!114 &1666568584
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1666568576}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 0a59a347a3128354ebd4f84afc97a0c5, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::ConcealmentVolume
resolution: 48
sourceMesh: {fileID: 0}
method: 0
drawBakedGizmos: 0
gizmoColor: {r: 0.2, g: 0.6, b: 1, a: 0.35}
origin: {x: -1.3557315, y: -0.70876616, z: -0.054229498}
voxelSize: 0.05422926
nx: 50
ny: 26
nz: 27
bits: 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000c010000000030000000000000000000000000000000000000000000000000000c00000000c030000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c00000000c030000000000003000000000000000000000000000000000000000000000000c000000000000c03000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000c030000000030000000000000000000000000000000000000000000000000000c00000000c030000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c00000000c030000000000003000000000000000000000000000000000000f8ffffffffffef0300000000bf0f00000000fc3e00000000f0fb00000000c0ef0300000000bf0f00000000fc3e00000000f0fb00000000c0ef0300000000bf0f00000000fc3e00000000f0fb00000000c0ef0300000000bf0f00000000fc3e00000000f0fb00000000c0ef0300000000bf0f00000000fc3e00000000f0fbffffffffffefffffffffffbffffffffffffffeffffffffff03000000000000000000000080fffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffff3f000000000000000000000000f8ffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffeffffffffff03000000000000000000000080fffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffff3f000000000000000000000000f8ffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffeffffffffff03000000000000000000000080fffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffff3f000000000000000000000000f8ffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffeffffffffff03000000000000000000000080fffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffff3f000000000000000000000000f8ffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffeffffffffff030000000000000000000000800700000000f8fefffffcfffffbfffff3ffffefffffcfffffbfffff3ffffffffefffffcfffffbfffff3ffffefffffcfffffbfffff3ffffffffefffffcfffffbfffff3ffffefffffdfffffbfffff7ffffffffefffffdfffffbfffff7ffffefffffdfffffbfffff7ffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffff3f000000000000000000000000780000000080ef0100000000be0700000000f81eff1fe0ffe37bfc7f80ff8feff1ff01fe3fbec7ff07f8fff81eff1fe0ffe37bfc7f80ff8feff1ff01fe3fbec7ff07f8fff81eff1fe0ffe37bfc7f80ff8feff1ff01fe3fbec7ff07f8fff81eff1fe0ffe3fbffffffffffefffffffffffbffffffffffffffefffffffffffbffffffffffefffffffffffbffffffffffffffeffffffffff030000000000000000000000800700000000f81e00000000e07b0000000080ef0100000000be0700000000f81e00000000e07b0000000080ef0100000000be070Line truncated
insideCount: 15957
--- !u!134 &1671101580
PhysicsMaterial:
m_ObjectHideFlags: 0
@@ -7200,6 +7251,9 @@ PrefabInstance:
- targetCorrespondingSourceObject: {fileID: 133438, guid: 6f95ff39b42c3414d9a7ced2eea58274, type: 3}
insertIndex: -1
addedObject: {fileID: 1260370189}
- targetCorrespondingSourceObject: {fileID: 133438, guid: 6f95ff39b42c3414d9a7ced2eea58274, type: 3}
insertIndex: -1
addedObject: {fileID: 1260370193}
m_SourcePrefab: {fileID: 100100000, guid: 6f95ff39b42c3414d9a7ced2eea58274, type: 3}
--- !u!4 &1860487068 stripped
Transform:
+22 -1
View File
@@ -83,6 +83,7 @@ public class ConcealmentMeter : MonoBehaviour
private readonly List<Collider> _convex = new List<Collider>();
private readonly List<Terrain> _terrains = new List<Terrain>();
private readonly List<MeshCollider> _concave = new List<MeshCollider>();
private readonly List<ConcealmentVolume> _volumes = new List<ConcealmentVolume>();
// Gizmo cache.
private readonly List<Vector3> _gizPos = new List<Vector3>();
@@ -145,6 +146,7 @@ public class ConcealmentMeter : MonoBehaviour
BuildEnvironmentCandidates(worldBounds);
bool anyEnv = _convex.Count > 0 ||
_volumes.Count > 0 ||
(includeTerrain && _terrains.Count > 0) ||
(includeConcaveMeshColliders && _concave.Count > 0);
@@ -257,6 +259,18 @@ public class ConcealmentMeter : MonoBehaviour
_convex.Clear();
_terrains.Clear();
_concave.Clear();
_volumes.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.
IReadOnlyList<ConcealmentVolume> vols = ConcealmentVolume.All;
for (int i = 0; i < vols.Count; i++)
{
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);
}
int n = Physics.OverlapBoxNonAlloc(wb.center, wb.extents, _overlap, Quaternion.identity,
environmentMask, QueryTriggerInteraction.Ignore);
@@ -277,7 +291,9 @@ public class ConcealmentMeter : MonoBehaviour
if (col is MeshCollider mc && !mc.convex)
{
if (includeConcaveMeshColliders) _concave.Add(mc);
// A baked volume supersedes raycast parity for this object.
if (includeConcaveMeshColliders && !mc.GetComponentInParent<ConcealmentVolume>())
_concave.Add(mc);
continue; // concave can't use ClosestPoint
}
@@ -287,6 +303,11 @@ public class ConcealmentMeter : MonoBehaviour
private bool IsInsideAnyEnvironment(Vector3 p)
{
for (int i = 0; i < _volumes.Count; i++)
{
if (_volumes[i].ContainsWorld(p)) return true;
}
for (int i = 0; i < _convex.Count; i++)
{
if ((_convex[i].ClosestPoint(p) - p).sqrMagnitude <= InsideEpsSqr) return true;
+276
View File
@@ -0,0 +1,276 @@
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Editor-baked solid/enclosed voxel volume for a single (typically non-convex, non-watertight)
/// environment mesh. Runtime lookup is an O(1) grid test, so <see cref="ConcealmentMeter"/> can
/// treat furniture as "embeddable" without per-voxel raycast parity (which is unreliable on the
/// open-shell furniture meshes of asset kits).
///
/// Bake (editor only): voxelizes the source mesh in LOCAL space and marks a voxel as INSIDE when
/// either classifier fires:
/// * Generalized Winding Number &gt; 0.5 -> robustly captures SOLID material even for open shells.
/// * Exterior flood-fill cannot reach it -> captures fully ENCLOSED air cavities.
/// The union is conservative in the safe direction: an unreachable solid OR an enclosed pocket both
/// count as concealment; open regions (seat surface, under a thin table) correctly read as exposed.
///
/// Query is done via <see cref="ContainsWorld"/>; bake and query share the SAME mesh-local space
/// (verts at bake time, transform.InverseTransformPoint at query time), so position/rotation/scale
/// are handled uniformly.
/// </summary>
[DisallowMultipleComponent]
public class ConcealmentVolume : MonoBehaviour
{
public enum BakeMethod { WindingNumberOrFloodFill, WindingNumberOnly, FloodFillOnly }
[Header("Bake settings")]
[Min(2)]
[Tooltip("Voxels along the longest local-bounds axis. Higher = sharper & bigger bake. ~32 is a good start.")]
[SerializeField] private int resolution = 32;
[Tooltip("Source mesh. Auto = MeshCollider.sharedMesh, else MeshFilter.sharedMesh on this object.")]
[SerializeField] private Mesh sourceMesh;
[SerializeField] private BakeMethod method = BakeMethod.WindingNumberOrFloodFill;
[Header("Debug")]
[SerializeField] private bool drawBakedGizmos;
[SerializeField] private Color gizmoColor = new Color(0.2f, 0.6f, 1f, 0.35f);
// --- Baked data (local space) ---
[SerializeField, HideInInspector] private Vector3 origin; // local min corner of the grid
[SerializeField, HideInInspector] private float voxelSize;
[SerializeField, HideInInspector] private int nx, ny, nz;
[SerializeField, HideInInspector] private uint[] bits; // packed inside-flags, idx = ix + nx*(iy + ny*iz)
[SerializeField, HideInInspector] private int insideCount;
public bool HasBake => bits != null && bits.Length > 0 && nx > 0;
public int InsideVoxelCount => insideCount;
private static readonly List<ConcealmentVolume> Active = new List<ConcealmentVolume>();
public static IReadOnlyList<ConcealmentVolume> All => Active;
private void OnEnable() { if (!Active.Contains(this)) Active.Add(this); }
private void OnDisable() { Active.Remove(this); }
/// <summary>World-space AABB of the baked grid (for broadphase rejection).</summary>
public Bounds WorldBounds
{
get
{
Vector3 mn = origin;
Vector3 mx = origin + new Vector3(nx * voxelSize, ny * voxelSize, nz * voxelSize);
Matrix4x4 m = transform.localToWorldMatrix;
Bounds b = new Bounds(m.MultiplyPoint3x4(mn), Vector3.zero);
b.Encapsulate(m.MultiplyPoint3x4(new Vector3(mx.x, mn.y, mn.z)));
b.Encapsulate(m.MultiplyPoint3x4(new Vector3(mn.x, mx.y, mn.z)));
b.Encapsulate(m.MultiplyPoint3x4(new Vector3(mn.x, mn.y, mx.z)));
b.Encapsulate(m.MultiplyPoint3x4(new Vector3(mx.x, mx.y, mn.z)));
b.Encapsulate(m.MultiplyPoint3x4(new Vector3(mx.x, mn.y, mx.z)));
b.Encapsulate(m.MultiplyPoint3x4(new Vector3(mn.x, mx.y, mx.z)));
b.Encapsulate(m.MultiplyPoint3x4(mx));
return b;
}
}
/// <summary>O(1) test: is the world point inside a baked-as-solid/enclosed voxel?</summary>
public bool ContainsWorld(Vector3 world)
{
if (!HasBake) return false;
Vector3 lp = transform.InverseTransformPoint(world);
int ix = Mathf.FloorToInt((lp.x - origin.x) / voxelSize);
if (ix < 0 || ix >= nx) return false;
int iy = Mathf.FloorToInt((lp.y - origin.y) / voxelSize);
if (iy < 0 || iy >= ny) return false;
int iz = Mathf.FloorToInt((lp.z - origin.z) / voxelSize);
if (iz < 0 || iz >= nz) return false;
int idx = ix + nx * (iy + ny * iz);
return (bits[idx >> 5] & (1u << (idx & 31))) != 0;
}
#if UNITY_EDITOR
[ContextMenu("Bake Concealment Volume")]
public void Bake()
{
Mesh mesh = sourceMesh;
if (!mesh)
{
var mc = GetComponent<MeshCollider>();
if (mc) mesh = mc.sharedMesh;
}
if (!mesh)
{
var mf = GetComponent<MeshFilter>();
if (mf) mesh = mf.sharedMesh;
}
if (!mesh) { Debug.LogError($"[ConcealmentVolume] {name}: no source mesh.", this); return; }
Vector3[] verts = mesh.vertices;
int[] tris = mesh.triangles;
if (verts.Length == 0 || tris.Length < 3) { Debug.LogError($"[ConcealmentVolume] {name}: empty mesh.", this); return; }
Bounds lb = mesh.bounds;
float maxExtent = Mathf.Max(lb.size.x, Mathf.Max(lb.size.y, lb.size.z));
if (maxExtent <= 1e-6f) { Debug.LogError($"[ConcealmentVolume] {name}: degenerate bounds.", this); return; }
float vs = maxExtent / resolution;
const int pad = 1; // empty shell so exterior flood always has a seed
nx = Mathf.CeilToInt(lb.size.x / vs) + 2 * pad;
ny = Mathf.CeilToInt(lb.size.y / vs) + 2 * pad;
nz = Mathf.CeilToInt(lb.size.z / vs) + 2 * pad;
voxelSize = vs;
origin = lb.min - new Vector3(vs, vs, vs) * pad;
int count = nx * ny * nz;
bool[] wall = new bool[count];
// --- 1) Rasterize triangles into wall voxels (barycentric supersampling, step = vs/2) ---
for (int t = 0; t < tris.Length; t += 3)
{
Vector3 a = verts[tris[t]], b = verts[tris[t + 1]], c = verts[tris[t + 2]];
float eMax = Mathf.Max((b - a).magnitude, (c - a).magnitude);
int steps = Mathf.Max(1, Mathf.CeilToInt(eMax / (vs * 0.5f)));
float inv = 1f / steps;
for (int u = 0; u <= steps; u++)
{
for (int v = 0; v <= steps - u; v++)
{
Vector3 p = a + (b - a) * (u * inv) + (c - a) * (v * inv);
int ix = Mathf.Clamp(Mathf.FloorToInt((p.x - origin.x) / vs), 0, nx - 1);
int iy = Mathf.Clamp(Mathf.FloorToInt((p.y - origin.y) / vs), 0, ny - 1);
int iz = Mathf.Clamp(Mathf.FloorToInt((p.z - origin.z) / vs), 0, nz - 1);
wall[ix + nx * (iy + ny * iz)] = true;
}
}
}
// --- 2) Exterior flood-fill (6-connected) through non-wall voxels ---
bool[] outside = new bool[count];
var stack = new Stack<int>(count / 4 + 16);
for (int iz = 0; iz < nz; iz++)
for (int iy = 0; iy < ny; iy++)
for (int ix = 0; ix < nx; ix++)
{
if (ix != 0 && ix != nx - 1 && iy != 0 && iy != ny - 1 && iz != 0 && iz != nz - 1) continue;
int idx = ix + nx * (iy + ny * iz);
if (!wall[idx] && !outside[idx]) { outside[idx] = true; stack.Push(idx); }
}
while (stack.Count > 0)
{
int idx = stack.Pop();
int ix = idx % nx;
int iy = (idx / nx) % ny;
int iz = idx / (nx * ny);
PushNeighbor(ix - 1, iy, iz, wall, outside, stack);
PushNeighbor(ix + 1, iy, iz, wall, outside, stack);
PushNeighbor(ix, iy - 1, iz, wall, outside, stack);
PushNeighbor(ix, iy + 1, iz, wall, outside, stack);
PushNeighbor(ix, iy, iz - 1, wall, outside, stack);
PushNeighbor(ix, iy, iz + 1, wall, outside, stack);
}
// --- 3) Classify each voxel; pack into bits ---
bits = new uint[(count + 31) >> 5];
insideCount = 0;
int floodOnly = 0, windingOnly = 0;
bool useWinding = method != BakeMethod.FloodFillOnly;
bool useFlood = method != BakeMethod.WindingNumberOnly;
for (int iz = 0; iz < nz; iz++)
for (int iy = 0; iy < ny; iy++)
for (int ix = 0; ix < nx; ix++)
{
int idx = ix + nx * (iy + ny * iz);
bool enclosed = useFlood && !outside[idx];
bool solid = false;
if (useWinding)
{
Vector3 cWorldLocal = new Vector3(
origin.x + (ix + 0.5f) * vs,
origin.y + (iy + 0.5f) * vs,
origin.z + (iz + 0.5f) * vs);
solid = WindingNumber(cWorldLocal, verts, tris) > 0.5;
}
bool inside = enclosed || solid;
if (!inside) continue;
bits[idx >> 5] |= 1u << (idx & 31);
insideCount++;
if (enclosed && !solid) floodOnly++;
if (solid && !enclosed) windingOnly++;
}
UnityEditor.Undo.RecordObject(this, "Bake Concealment Volume");
UnityEditor.EditorUtility.SetDirty(this);
if (!Application.isPlaying)
UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(gameObject.scene);
Debug.Log($"[ConcealmentVolume] {name}: baked {nx}x{ny}x{nz} (vs={vs:F3}) -> inside={insideCount}/{count} " +
$"(winding-only={windingOnly}, flood-only={floodOnly}), bits={bits.Length * 4}B", this);
}
[ContextMenu("Clear Bake")]
public void ClearBake()
{
bits = null; nx = ny = nz = 0; insideCount = 0;
UnityEditor.EditorUtility.SetDirty(this);
}
private void PushNeighbor(int ix, int iy, int iz, bool[] wall, bool[] outside, Stack<int> stack)
{
if (ix < 0 || ix >= nx || iy < 0 || iy >= ny || iz < 0 || iz >= nz) return;
int idx = ix + nx * (iy + ny * iz);
if (wall[idx] || outside[idx]) return;
outside[idx] = true;
stack.Push(idx);
}
/// <summary>Generalized winding number at p (mesh-local). ~1 inside a solid, ~0 outside; robust for open shells.</summary>
private static double WindingNumber(Vector3 p, Vector3[] verts, int[] tris)
{
double sum = 0.0;
double px = p.x, py = p.y, pz = p.z;
for (int t = 0; t < tris.Length; t += 3)
{
Vector3 A = verts[tris[t]], B = verts[tris[t + 1]], C = verts[tris[t + 2]];
double ax = A.x - px, ay = A.y - py, az = A.z - pz;
double bx = B.x - px, by = B.y - py, bz = B.z - pz;
double cx = C.x - px, cy = C.y - py, cz = C.z - pz;
double la = System.Math.Sqrt(ax * ax + ay * ay + az * az);
double lb = System.Math.Sqrt(bx * bx + by * by + bz * bz);
double lc = System.Math.Sqrt(cx * cx + cy * cy + cz * cz);
if (la < 1e-12 || lb < 1e-12 || lc < 1e-12) continue;
// triple product a . (b x c)
double crx = by * cz - bz * cy;
double cry = bz * cx - bx * cz;
double crz = bx * cy - by * cx;
double numer = ax * crx + ay * cry + az * crz;
double denom = la * lb * lc
+ (ax * bx + ay * by + az * bz) * lc
+ (bx * cx + by * cy + bz * cz) * la
+ (cx * ax + cy * ay + cz * az) * lb;
sum += System.Math.Atan2(numer, denom) * 2.0;
}
return sum / (4.0 * System.Math.PI);
}
private void OnDrawGizmosSelected()
{
if (!drawBakedGizmos || !HasBake) return;
Gizmos.color = gizmoColor;
Gizmos.matrix = transform.localToWorldMatrix;
Vector3 cube = Vector3.one * (voxelSize * 0.9f);
for (int iz = 0; iz < nz; iz++)
for (int iy = 0; iy < ny; iy++)
for (int ix = 0; ix < nx; ix++)
{
int idx = ix + nx * (iy + ny * iz);
if ((bits[idx >> 5] & (1u << (idx & 31))) == 0) continue;
Vector3 c = new Vector3(
origin.x + (ix + 0.5f) * voxelSize,
origin.y + (iy + 0.5f) * voxelSize,
origin.z + (iz + 0.5f) * voxelSize);
Gizmos.DrawCube(c, cube);
}
}
#endif
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0a59a347a3128354ebd4f84afc97a0c5