Sweep over 48 files (Editor/ excluded): remove comments that restate the code, condense class summaries, fix three stale doc statements (HsvColorPicker alpha bar, PaintHud layoutPrefab, MasterVolume FallbackDefault). Inline two single-use wrappers (IsPointerOverUi, BuildModes), drop an unused using, replace a magic literal with the existing ParallaxBase constant, and convert ~10 UnityEngine.Object null checks to the implicit-bool convention. No serialized fields, signatures, RPC/NetworkVariable names, defaults, or #if blocks touched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
203 lines
8.2 KiB
C#
203 lines
8.2 KiB
C#
using System.Collections.Generic;
|
|
using MeshProcess;
|
|
using UnityEngine;
|
|
#if UNITY_EDITOR
|
|
using UnityEditor;
|
|
using UnityEditor.SceneManagement;
|
|
#endif
|
|
|
|
/// <summary>
|
|
/// Editor tool that runs V-HACD (approximate convex decomposition via the native <see cref="VHACD"/>
|
|
/// plugin) on an environment mesh and bakes the resulting convex hulls into SAVED mesh assets + child
|
|
/// MeshCollider GameObjects — the same output plumbing as <see cref="ConcealmentColliderBaker"/>, only
|
|
/// the decomposition backend differs (V-HACD's voxel solver instead of the surface-clustering
|
|
/// heuristic). Tune the decomposition on the sibling <see cref="VHACD"/> component (its m_parameters),
|
|
/// then run "Bake VHACD Colliders".
|
|
///
|
|
/// The hulls are real child GameObjects referencing hull-mesh assets under
|
|
/// <c>Assets/Generated/VHACDColliders</c>, so they survive domain reload / play mode / scene save
|
|
/// (unlike an in-memory-only bake).
|
|
/// </summary>
|
|
[DisallowMultipleComponent]
|
|
[RequireComponent(typeof(VHACD))]
|
|
public class VHACDColliderBaker : MonoBehaviour
|
|
{
|
|
[Tooltip("Optional. If empty, the shared mesh of a MeshFilter on this object (or a child) is used.")]
|
|
[SerializeField] private Mesh sourceMesh;
|
|
|
|
[Header("Output")]
|
|
[Tooltip("If true, generated colliders inherit the source object's layer. Otherwise 'Collider Layer' is used.")]
|
|
[SerializeField] private bool useSourceLayer = true;
|
|
[SerializeField] private int colliderLayer;
|
|
|
|
[SerializeField, HideInInspector] private List<string> generatedAssetPaths = new List<string>();
|
|
|
|
private const string ContainerName = "VHACDColliders (generated)";
|
|
private const string AssetFolder = "Assets/Generated/VHACDColliders";
|
|
|
|
#if UNITY_EDITOR
|
|
[ContextMenu("Bake VHACD Colliders")]
|
|
public void Bake()
|
|
{
|
|
Mesh mesh = sourceMesh;
|
|
Transform meshSpace = transform;
|
|
if (!mesh)
|
|
{
|
|
MeshFilter mf = GetComponentInChildren<MeshFilter>();
|
|
if (mf && mf.sharedMesh) { mesh = mf.sharedMesh; meshSpace = mf.transform; }
|
|
}
|
|
if (!mesh)
|
|
{
|
|
Debug.LogError($"[VHACDColliderBaker] '{name}': no source mesh (assign one or add a MeshFilter).", this);
|
|
return;
|
|
}
|
|
|
|
VHACD vhacd = GetComponent<VHACD>();
|
|
if (!vhacd) vhacd = gameObject.AddComponent<VHACD>();
|
|
|
|
Clear(); // remove previous container + assets first
|
|
|
|
string baseName = ResolveAssetBaseName();
|
|
|
|
// V-HACD reads mesh.vertices raw => hulls are in the mesh's LOCAL space, so an identity
|
|
// container under meshSpace places them correctly (same convention as ConcealmentColliderBaker).
|
|
List<Mesh> hulls = vhacd.GenerateConvexMeshes(mesh);
|
|
if (hulls == null || hulls.Count == 0)
|
|
{
|
|
Debug.LogWarning($"[VHACDColliderBaker] '{name}': V-HACD produced no convex hulls.", this);
|
|
return;
|
|
}
|
|
|
|
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 i = 0; i < hulls.Count; i++)
|
|
{
|
|
Mesh cm = hulls[i];
|
|
if (!cm || cm.vertexCount < 4) { if (cm) DestroyImmediate(cm); continue; } // convex hull needs >= 4 verts
|
|
|
|
cm.name = $"{baseName}_{made}";
|
|
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<MeshCollider>();
|
|
col.sharedMesh = cm;
|
|
col.convex = true;
|
|
col.isTrigger = false;
|
|
|
|
made++;
|
|
}
|
|
|
|
AssetDatabase.SaveAssets();
|
|
EditorUtility.SetDirty(this);
|
|
EditorSceneManager.MarkSceneDirty(gameObject.scene);
|
|
|
|
Debug.Log($"[VHACDColliderBaker] '{name}': baked {made} convex collider(s) via V-HACD ({hulls.Count} hull(s) returned). 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<Transform>(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())
|
|
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", "VHACDColliders");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Readable, deterministic base name for the generated collider-mesh assets:
|
|
/// - 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.
|
|
/// </summary>
|
|
private string ResolveAssetBaseName()
|
|
{
|
|
string raw;
|
|
PrefabStage stage = PrefabStageUtility.GetPrefabStage(gameObject);
|
|
if (stage)
|
|
{
|
|
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;
|
|
}
|
|
|
|
/// <summary>Slash-joined transform names from <paramref name="t"/> up to (but excluding) <paramref name="stopAt"/> (null = up to the root).</summary>
|
|
private static string GetHierarchyPath(Transform t, Transform stopAt)
|
|
{
|
|
var names = new List<string>();
|
|
for (Transform cur = t; cur && cur != stopAt; cur = cur.parent)
|
|
names.Add(cur.name);
|
|
names.Reverse();
|
|
return string.Join("/", names);
|
|
}
|
|
|
|
/// <summary>Collapses every run of non-[A-Za-z0-9-] characters to a single '_' and trims leading/trailing '_'.</summary>
|
|
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
|
|
}
|