- BackgroundSceneLoaderWindow: OnGUI → CreateGUI (Toolbar + ToolbarSearchField) - PropBrowserWindow: OnGUI → CreateGUI (Toolbar + ToolbarSearchField) - StreamingleCommon.uss: 브라우저 공통 스타일 추가 (그리드/리스트/뷰토글/액션바/상태바) - excludeFromWeb 상태 새로고침 시 보존 수정 - 삭제된 배경 리소스 정리 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
304 lines
12 KiB
C#
304 lines
12 KiB
C#
using UnityEngine;
|
|
using UnityEditor;
|
|
using UnityEngine.UIElements;
|
|
using UnityEditor.UIElements;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
|
|
public class MaterialAndTextureTool : EditorWindow
|
|
{
|
|
private const string CommonUssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss";
|
|
|
|
private ObjectField prefabField;
|
|
private TextField materialPathField;
|
|
private TextField texturePathField;
|
|
private VisualElement resultContainer;
|
|
|
|
private GameObject targetPrefab;
|
|
private Dictionary<string, List<Material>> duplicateMaterialMap = new Dictionary<string, List<Material>>();
|
|
private Dictionary<string, List<Texture>> duplicateTextureMap = new Dictionary<string, List<Texture>>();
|
|
private Dictionary<Material, Material> materialCopies = new Dictionary<Material, Material>();
|
|
private Dictionary<Texture, Texture> textureCopies = new Dictionary<Texture, Texture>();
|
|
private HashSet<Material> collectedMaterials = new HashSet<Material>();
|
|
private HashSet<Texture> collectedTextures = new HashSet<Texture>();
|
|
|
|
[MenuItem("Tools/Utilities/머티리얼 & 텍스처 도구")]
|
|
public static void ShowWindow()
|
|
{
|
|
var window = GetWindow<MaterialAndTextureTool>("머티리얼 & 텍스처 도구");
|
|
window.minSize = new Vector2(600, 500);
|
|
}
|
|
|
|
public void CreateGUI()
|
|
{
|
|
var root = rootVisualElement;
|
|
root.AddToClassList("tool-root");
|
|
|
|
var commonUss = AssetDatabase.LoadAssetAtPath<StyleSheet>(CommonUssPath);
|
|
if (commonUss != null) root.styleSheets.Add(commonUss);
|
|
|
|
root.Add(new Label("머티리얼 & 텍스처 유틸리티") { name = "title" });
|
|
root.Q<Label>("title").AddToClassList("tool-title");
|
|
|
|
prefabField = new ObjectField("타겟 프리팹") { objectType = typeof(GameObject), allowSceneObjects = true };
|
|
prefabField.RegisterValueChangedCallback(evt => targetPrefab = evt.newValue as GameObject);
|
|
root.Add(prefabField);
|
|
|
|
materialPathField = new TextField("머티리얼 저장 경로") { value = "Assets/DuplicatedMaterials" };
|
|
root.Add(materialPathField);
|
|
|
|
texturePathField = new TextField("텍스처 저장 경로") { value = "Assets/DuplicatedTextures" };
|
|
root.Add(texturePathField);
|
|
|
|
var btnRow = new VisualElement { style = { flexDirection = FlexDirection.Row, marginTop = 8 } };
|
|
|
|
var checkBtn = new Button(CollectDuplicates) { text = "중복 이름 검사" };
|
|
checkBtn.style.flexGrow = 1;
|
|
checkBtn.style.marginRight = 4;
|
|
btnRow.Add(checkBtn);
|
|
|
|
var renameBtn = new Button(RenameDuplicateAssets) { text = "중복 이름 변경" };
|
|
renameBtn.style.flexGrow = 1;
|
|
renameBtn.style.marginRight = 4;
|
|
btnRow.Add(renameBtn);
|
|
|
|
var copyBtn = new Button(DuplicateMaterialsAndTextures) { text = "머티리얼/텍스처 복사" };
|
|
copyBtn.style.flexGrow = 1;
|
|
copyBtn.AddToClassList("btn-primary");
|
|
btnRow.Add(copyBtn);
|
|
|
|
root.Add(btnRow);
|
|
|
|
// 결과 영역
|
|
var resultScroll = new ScrollView { style = { flexGrow = 1, marginTop = 8 } };
|
|
resultContainer = new VisualElement();
|
|
resultScroll.Add(resultContainer);
|
|
root.Add(resultScroll);
|
|
}
|
|
|
|
private void RebuildResultUI()
|
|
{
|
|
if (resultContainer == null) return;
|
|
resultContainer.Clear();
|
|
|
|
// 중복 머티리얼
|
|
AddDuplicateSection("중복된 이름의 머티리얼", duplicateMaterialMap);
|
|
AddDuplicateSection("중복된 이름의 텍스처", duplicateTextureMap);
|
|
|
|
// 참조된 모든 머티리얼
|
|
if (collectedMaterials.Count > 0)
|
|
{
|
|
resultContainer.Add(new Label($"참조된 모든 머티리얼 ({collectedMaterials.Count})") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginTop = 8 } });
|
|
foreach (var mat in collectedMaterials)
|
|
{
|
|
var row = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center, marginBottom = 1 } };
|
|
var nameBtn = new Button(() => EditorGUIUtility.PingObject(mat)) { text = mat.name };
|
|
nameBtn.style.width = 200;
|
|
row.Add(nameBtn);
|
|
var objField = new ObjectField { value = mat, objectType = typeof(Material) };
|
|
objField.SetEnabled(false);
|
|
objField.style.flexGrow = 1;
|
|
row.Add(objField);
|
|
resultContainer.Add(row);
|
|
}
|
|
}
|
|
|
|
// 참조된 모든 텍스처
|
|
if (collectedTextures.Count > 0)
|
|
{
|
|
resultContainer.Add(new Label($"참조된 모든 텍스처 ({collectedTextures.Count})") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginTop = 8 } });
|
|
foreach (var tex in collectedTextures)
|
|
{
|
|
var row = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center, marginBottom = 1 } };
|
|
var nameBtn = new Button(() => EditorGUIUtility.PingObject(tex)) { text = tex.name };
|
|
nameBtn.style.width = 200;
|
|
row.Add(nameBtn);
|
|
var objField = new ObjectField { value = tex, objectType = typeof(Texture) };
|
|
objField.SetEnabled(false);
|
|
objField.style.flexGrow = 1;
|
|
row.Add(objField);
|
|
resultContainer.Add(row);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void AddDuplicateSection<T>(string title, Dictionary<string, List<T>> map) where T : Object
|
|
{
|
|
bool hasDuplicates = false;
|
|
foreach (var pair in map)
|
|
if (pair.Value.Count >= 2) { hasDuplicates = true; break; }
|
|
|
|
if (!hasDuplicates) return;
|
|
|
|
resultContainer.Add(new Label(title) { style = { unityFontStyleAndWeight = FontStyle.Bold, marginTop = 8, color = new Color(1f, 0.4f, 0.4f) } });
|
|
|
|
foreach (var pair in map)
|
|
{
|
|
if (pair.Value.Count < 2) continue;
|
|
resultContainer.Add(new Label($"\u26A0 {pair.Key} ({pair.Value.Count}개)") { style = { color = new Color(1f, 0.4f, 0.4f) } });
|
|
foreach (var obj in pair.Value)
|
|
{
|
|
var objField = new ObjectField { value = obj, objectType = typeof(T) };
|
|
objField.SetEnabled(false);
|
|
resultContainer.Add(objField);
|
|
}
|
|
}
|
|
}
|
|
|
|
void CollectDuplicates()
|
|
{
|
|
duplicateMaterialMap.Clear();
|
|
duplicateTextureMap.Clear();
|
|
collectedMaterials.Clear();
|
|
collectedTextures.Clear();
|
|
|
|
if (targetPrefab == null) return;
|
|
|
|
HashSet<Material> seenMaterials = new HashSet<Material>();
|
|
HashSet<Texture> seenTextures = new HashSet<Texture>();
|
|
|
|
var renderers = targetPrefab.GetComponentsInChildren<Renderer>(true);
|
|
|
|
foreach (var renderer in renderers)
|
|
{
|
|
foreach (var mat in renderer.sharedMaterials)
|
|
{
|
|
if (mat == null || seenMaterials.Contains(mat)) continue;
|
|
seenMaterials.Add(mat);
|
|
collectedMaterials.Add(mat);
|
|
|
|
if (!duplicateMaterialMap.ContainsKey(mat.name))
|
|
duplicateMaterialMap[mat.name] = new List<Material>();
|
|
duplicateMaterialMap[mat.name].Add(mat);
|
|
|
|
Shader shader = mat.shader;
|
|
int count = ShaderUtil.GetPropertyCount(shader);
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
string propName = ShaderUtil.GetPropertyName(shader, i);
|
|
Texture tex = mat.GetTexture(propName);
|
|
if (tex == null || seenTextures.Contains(tex)) continue;
|
|
seenTextures.Add(tex);
|
|
collectedTextures.Add(tex);
|
|
|
|
if (!duplicateTextureMap.ContainsKey(tex.name))
|
|
duplicateTextureMap[tex.name] = new List<Texture>();
|
|
duplicateTextureMap[tex.name].Add(tex);
|
|
}
|
|
}
|
|
}
|
|
|
|
RebuildResultUI();
|
|
}
|
|
|
|
void RenameDuplicateAssets()
|
|
{
|
|
Dictionary<string, int> renameCount = new Dictionary<string, int>();
|
|
RenameAssetGroup(duplicateMaterialMap, renameCount);
|
|
RenameAssetGroup(duplicateTextureMap, renameCount);
|
|
AssetDatabase.SaveAssets();
|
|
AssetDatabase.Refresh();
|
|
CollectDuplicates();
|
|
}
|
|
|
|
void RenameAssetGroup<T>(Dictionary<string, List<T>> map, Dictionary<string, int> counter) where T : Object
|
|
{
|
|
foreach (var pair in map)
|
|
{
|
|
if (pair.Value.Count < 2) continue;
|
|
foreach (var obj in pair.Value)
|
|
{
|
|
string path = AssetDatabase.GetAssetPath(obj);
|
|
if (string.IsNullOrEmpty(path)) continue;
|
|
|
|
if (!counter.ContainsKey(pair.Key)) counter[pair.Key] = 1;
|
|
else counter[pair.Key] += 1;
|
|
|
|
string newName = pair.Key + "_" + counter[pair.Key];
|
|
string result = AssetDatabase.RenameAsset(path, newName);
|
|
if (result != "")
|
|
Debug.LogWarning("리네이밍 실패: " + result);
|
|
}
|
|
}
|
|
}
|
|
|
|
void DuplicateMaterialsAndTextures()
|
|
{
|
|
if (targetPrefab == null) return;
|
|
|
|
string materialOutputPath = materialPathField.value;
|
|
string textureOutputPath = texturePathField.value;
|
|
|
|
materialCopies.Clear();
|
|
textureCopies.Clear();
|
|
|
|
if (!AssetDatabase.IsValidFolder(materialOutputPath))
|
|
AssetDatabase.CreateFolder("Assets", "DuplicatedMaterials");
|
|
if (!AssetDatabase.IsValidFolder(textureOutputPath))
|
|
AssetDatabase.CreateFolder("Assets", "DuplicatedTextures");
|
|
|
|
Renderer[] renderers = targetPrefab.GetComponentsInChildren<Renderer>(true);
|
|
|
|
foreach (var renderer in renderers)
|
|
{
|
|
Material[] newMats = new Material[renderer.sharedMaterials.Length];
|
|
|
|
for (int i = 0; i < newMats.Length; i++)
|
|
{
|
|
Material orig = renderer.sharedMaterials[i];
|
|
if (orig == null) continue;
|
|
|
|
if (!materialCopies.ContainsKey(orig))
|
|
{
|
|
Material newMat = new Material(orig);
|
|
string matPath = AssetDatabase.GenerateUniqueAssetPath(materialOutputPath + "/" + orig.name + "_Copy.mat");
|
|
AssetDatabase.CreateAsset(newMat, matPath);
|
|
materialCopies[orig] = newMat;
|
|
CopyTextures(orig, newMat, textureOutputPath);
|
|
EditorUtility.SetDirty(newMat);
|
|
}
|
|
newMats[i] = materialCopies[orig];
|
|
}
|
|
|
|
Undo.RecordObject(renderer, "Apply Copied Materials");
|
|
renderer.sharedMaterials = newMats;
|
|
}
|
|
|
|
AssetDatabase.SaveAssets();
|
|
AssetDatabase.Refresh();
|
|
}
|
|
|
|
void CopyTextures(Material original, Material copy, string textureOutputPath)
|
|
{
|
|
Shader shader = original.shader;
|
|
int count = ShaderUtil.GetPropertyCount(shader);
|
|
|
|
string[] maskProps = { "_MaskMap", "_OcclusionMap", "_DetailMask", "_RoughnessMap", "_MetallicGlossMap" };
|
|
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
string prop = ShaderUtil.GetPropertyName(shader, i);
|
|
Texture tex = original.GetTexture(prop);
|
|
if (tex == null) continue;
|
|
|
|
bool isTexEnv = ShaderUtil.GetPropertyType(shader, i) == ShaderUtil.ShaderPropertyType.TexEnv;
|
|
bool isMask = System.Array.IndexOf(maskProps, prop) >= 0;
|
|
|
|
if (isTexEnv || isMask)
|
|
{
|
|
if (!textureCopies.ContainsKey(tex))
|
|
{
|
|
string path = AssetDatabase.GetAssetPath(tex);
|
|
string newPath = AssetDatabase.GenerateUniqueAssetPath(textureOutputPath + "/" + tex.name + "_Copy" + Path.GetExtension(path));
|
|
AssetDatabase.CopyAsset(path, newPath);
|
|
Texture newTex = AssetDatabase.LoadAssetAtPath<Texture>(newPath);
|
|
textureCopies[tex] = newTex;
|
|
}
|
|
|
|
copy.SetTexture(prop, textureCopies[tex]);
|
|
EditorUtility.SetDirty(copy);
|
|
}
|
|
}
|
|
}
|
|
}
|