MiniGames/Assets/SmartMaterialCloner.cs
2025-09-06 00:08:22 +05:00

202 lines
6.7 KiB
C#

using System.Collections.Generic;
using System.Linq;
using UnityEngine;
[AddComponentMenu("Tools/Smart Material Cloner")]
[ExecuteAlways] // lets you run it in edit mode
public class SmartMaterialCloner : MonoBehaviour
{
[Header("Copy from this hierarchy (GOOD materials)")]
public Transform SourceRoot;
[Header("Matching Options")]
public bool IncludeInactive = true;
public bool MatchByMeshFirst = true;
public bool NormalizeNames = true;
public bool PreferSameSlotCount = true;
public bool BreakTiesByNearest = true;
[Header("Output")]
public bool VerboseLog = true;
// —— Context menu actions you can run from the component's header ——
[ContextMenu("Preview Matches")]
public void PreviewMatches() => ApplyInternal(previewOnly: true);
[ContextMenu("Apply From Source")]
public void ApplyFromSource() => ApplyInternal(previewOnly: false);
// ————————————————————————————————————————————————————————————————
private class SrcEntry
{
public Renderer renderer;
public string nameKey;
public string meshKey;
public int slotCount;
public Vector3 worldPos;
public Material[] mats;
}
private void ApplyInternal(bool previewOnly)
{
if (SourceRoot == null)
{
Debug.LogError("[SmartMaterialCloner] Please assign SourceRoot (the good hierarchy).");
return;
}
var sourceRenderers = SourceRoot.GetComponentsInChildren<Renderer>(IncludeInactive);
if (sourceRenderers.Length == 0)
{
Debug.LogWarning("[SmartMaterialCloner] No renderers found under SourceRoot.");
return;
}
// Build indices
var srcList = new List<SrcEntry>(sourceRenderers.Length);
foreach (var r in sourceRenderers)
{
srcList.Add(new SrcEntry
{
renderer = r,
nameKey = NormalizeNames ? Normalize(r.gameObject.name) : r.gameObject.name,
meshKey = GetMeshKey(r),
slotCount = GetSlotCount(r),
worldPos = r.transform.position,
mats = CloneArray(r.sharedMaterials)
});
}
var byMesh = srcList.Where(e => !string.IsNullOrEmpty(e.meshKey))
.GroupBy(e => e.meshKey)
.ToDictionary(g => g.Key, g => g.ToList());
var byName = srcList.GroupBy(e => e.nameKey)
.ToDictionary(g => g.Key, g => g.ToList());
var targets = GetComponentsInChildren<Renderer>(IncludeInactive);
int applied = 0, missed = 0, previewed = 0;
foreach (var t in targets)
{
var tNameKey = NormalizeNames ? Normalize(t.gameObject.name) : t.gameObject.name;
var tMeshKey = GetMeshKey(t);
int tSlots = GetSlotCount(t);
Vector3 tPos = t.transform.position;
var picked = PickBestCandidate(tMeshKey, tNameKey, tSlots, tPos, byMesh, byName);
if (picked == null)
{
missed++;
if (VerboseLog)
Debug.Log($"[SmartMaterialCloner] Missed: {FullPath(t.transform)} (mesh='{tMeshKey ?? "none"}')");
continue;
}
if (previewOnly)
{
previewed++;
if (VerboseLog)
Debug.Log($"[SmartMaterialCloner][PREVIEW] {FullPath(t.transform)} <= {FullPath(picked.renderer.transform)} ({picked.mats.Length} mats)");
continue;
}
t.sharedMaterials = picked.mats; // no Editor API; works in edit or play mode
applied++;
if (VerboseLog)
Debug.Log($"[SmartMaterialCloner] Applied to {FullPath(t.transform)} from {FullPath(picked.renderer.transform)}");
}
Debug.Log($"[SmartMaterialCloner] {(previewOnly ? $"Previewed {previewed}" : $"Applied {applied}")}, Missed {missed}");
}
private SrcEntry PickBestCandidate(
string tMeshKey, string tNameKey, int tSlots, Vector3 tPos,
Dictionary<string, List<SrcEntry>> byMesh,
Dictionary<string, List<SrcEntry>> byName)
{
List<SrcEntry> candidates = null;
if (MatchByMeshFirst && !string.IsNullOrEmpty(tMeshKey) && byMesh.TryGetValue(tMeshKey, out candidates))
return PickBest(candidates, tSlots, tPos);
if (byName.TryGetValue(tNameKey, out candidates))
return PickBest(candidates, tSlots, tPos);
return null;
}
private SrcEntry PickBest(List<SrcEntry> candidates, int targetSlots, Vector3 targetPos)
{
if (candidates == null || candidates.Count == 0) return null;
IEnumerable<SrcEntry> filtered = candidates;
if (PreferSameSlotCount)
{
var slotMatches = candidates.Where(c => c.slotCount == targetSlots).ToList();
if (slotMatches.Count > 0) filtered = slotMatches;
}
if (BreakTiesByNearest)
{
float bestDist = float.MaxValue;
SrcEntry best = null;
foreach (var c in filtered)
{
float d = (c.worldPos - targetPos).sqrMagnitude;
if (d < bestDist) { bestDist = d; best = c; }
}
return best;
}
return filtered.First();
}
private static int GetSlotCount(Renderer r) => r && r.sharedMaterials != null ? r.sharedMaterials.Length : 0;
private static string GetMeshKey(Renderer r)
{
if (!r) return null;
Mesh mesh = null;
if (r is SkinnedMeshRenderer sk) mesh = sk.sharedMesh;
else
{
var mf = r.GetComponent<MeshFilter>();
if (mf) mesh = mf.sharedMesh;
}
if (!mesh) return null;
// Runtime-safe key (no AssetDatabase)
return $"{mesh.name}::submeshes={mesh.subMeshCount}";
}
private static string Normalize(string name)
{
if (string.IsNullOrEmpty(name)) return name;
string n = name;
if (n.EndsWith(" (Clone)")) n = n.Substring(0, n.Length - " (Clone)".Length);
int idxOpen = n.LastIndexOf(" (");
if (idxOpen >= 0 && n.EndsWith(")"))
{
string inside = n.Substring(idxOpen + 2, n.Length - idxOpen - 3);
if (int.TryParse(inside, out _)) n = n.Substring(0, idxOpen);
}
return n.Trim();
}
private static string FullPath(Transform t)
{
var stack = new System.Collections.Generic.Stack<string>();
while (t != null) { stack.Push(t.name); t = t.parent; }
return string.Join("/", stack);
}
private static Material[] CloneArray(Material[] src) => src == null ? new Material[0] : (Material[])src.Clone();
}