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:
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
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 > 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
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0a59a347a3128354ebd4f84afc97a0c5
|
||||
Reference in new issue
Block a user