202 lines
6.7 KiB
C#
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();
|
|
}
|