// ───────────────────────────────────────────────────────────────────────────── // WL814s_Build.cs — 캐릭터를 배경과 같은 Toon 으로 (에디터 전용 · 발주서 WL-814s §1) // // 🔴 원본(`Assets/Suriyun/**`·`Assets/ThirdParty/**`·`Assets/Script/**`·원본 png) 수정 0. // 전부 `Assets/WL/Look/Character/**` 아래 복사본으로 만든다. // // 배경 실측값(Assets/3DPixelArtEnvironment/Materials/*.mat · 8종 전수) // _Shades 7 (8/8) · _Brightness 0.25 (7/8 · Leaves 1) · _MinimumDarkness 0.2 (7/8 · Leaves 0.25) // _OUTLINESENABLED 1 (7/8 · Leaves 0) · _Outline (0,0,0,0) (7/8 · SandStone 1,1,1,0) · _Cull 2 // Toon 그래프 실측: BaseColor = ToonLighting(AlbedoHighlight = Sample(_BaseMap) * _DiffuseColor, // AlbedoShadow = Sample(_ShadowBaseMap) * _ShadowDiffuseColor, …) // → 텍스처를 그대로 쓰려면 _BaseMap = 텍스처 · _DiffuseColor = 흰색. // → 그림자 색은 배경의 중립 재질 Stone 의 비율 (0.2164, 0.2092, 0.3713) 을 텍스처에 곱한다. // ───────────────────────────────────────────────────────────────────────────── using System; using System.Collections.Generic; using System.IO; using System.Text; using UnityEditor; using UnityEngine; using WL.Look.Character; public static class WL814s_Build { public const string ROOT = "Assets/WL/Look/Character/"; public const string MATS = ROOT + "Materials/"; public const string TEX = ROOT + "Textures/"; public const string RES = ROOT + "Resources/WL/"; public const string SO_PATH = RES + "WLCharacterLookSettings.asset"; public const string PREFAB = "Assets/Res_Addr/PC/LH_M05.prefab"; public const string TOON_SG = "Assets/3DPixelArtEnvironment/Shaders/Toon.shadergraph"; // 인덱스 순서 (SO 4배열 공통) public static readonly string[] KEYS = { "M05", "Skin", "Hair05", "face02" }; public static readonly string[] ORIG_MAT = { "Assets/Suriyun/Characters/RedKnight/Materials/Characters/M05.mat", "Assets/Suriyun/Characters/RedKnight/Materials/Skin&Hair&Face/skin.mat", "Assets/Suriyun/Characters/RedKnight/Materials/Skin&Hair&Face/Hair05.mat", "Assets/Suriyun/Characters/RedKnight/Materials/Skin&Hair&Face/Face/face02.mat", }; public static readonly string[] ORIG_TEX = { "Assets/Suriyun/Characters/RedKnight/Texture/M05/M05.png", "Assets/Suriyun/Characters/RedKnight/Texture/Skin&Hair&Face/Face/Skin.png", "Assets/Suriyun/Characters/RedKnight/Texture/M05/Hair05.png", "Assets/Suriyun/Characters/RedKnight/Texture/Skin&Hair&Face/Face/face02.png", }; // 배경 실측값 public const float SHADES = 7f; public const float BRIGHTNESS = 0.25f; public const float MIN_DARK = 0.2f; public const float OUTLINES = 1f; static readonly Color SHADOW_TINT = new Color(0.2164f, 0.2092f, 0.3713f, 0f); // Stone 의 shadow/diffuse 비율 static readonly Color OUTLINE_COL = new Color(0f, 0f, 0f, 0f); static void Ensure(string folder) { folder = folder.TrimEnd('/'); if (AssetDatabase.IsValidFolder(folder)) return; int i = folder.LastIndexOf('/'); Ensure(folder.Substring(0, i)); AssetDatabase.CreateFolder(folder.Substring(0, i), folder.Substring(i + 1)); } static Shader ToonShader() { var sh = AssetDatabase.LoadAssetAtPath(TOON_SG); return sh; } // ═════════════════════════════════════════════════════════════════════ // 배경 실측 표 // ═════════════════════════════════════════════════════════════════════ public static string DumpEnvMats() { string[] names = { "Leaves", "Trunk", "Stone", "SandStone", "Metal", "Wall", "Roof", "Window" }; var sb = new StringBuilder(); sb.AppendLine("name|shader|Shades|Brightness|MinimumDarkness|OUTLINESENABLED|DiffuseColor|ShadowDiffuseColor|Outline|BaseMap|Cull"); foreach (var n in names) { var m = AssetDatabase.LoadAssetAtPath("Assets/3DPixelArtEnvironment/Materials/" + n + ".mat"); if (m == null) { sb.AppendLine(n + "|MISS"); continue; } sb.AppendLine(string.Format("{0}|{1}|{2}|{3}|{4}|{5}|{6}|{7}|{8}|{9}|{10}", n, m.shader.name, m.HasFloat("_Shades") ? m.GetFloat("_Shades").ToString("0.####") : "-", m.HasFloat("_Brightness") ? m.GetFloat("_Brightness").ToString("0.####") : "-", m.HasFloat("_MinimumDarkness") ? m.GetFloat("_MinimumDarkness").ToString("0.####") : "-", m.IsKeywordEnabled("_OUTLINESENABLED") ? "1" : "0", m.HasColor("_DiffuseColor") ? Fmt(m.GetColor("_DiffuseColor")) : "-", m.HasColor("_ShadowDiffuseColor") ? Fmt(m.GetColor("_ShadowDiffuseColor")) : "-", m.HasColor("_Outline") ? Fmt(m.GetColor("_Outline")) : "-", (m.HasTexture("_BaseMap") && m.GetTexture("_BaseMap") != null) ? m.GetTexture("_BaseMap").name : "none", m.HasFloat("_Cull") ? m.GetFloat("_Cull").ToString("0") : "-")); } return sb.ToString(); } static string Fmt(Color c) { return string.Format("({0:0.####},{1:0.####},{2:0.####},{3:0.####})", c.r, c.g, c.b, c.a); } // ═════════════════════════════════════════════════════════════════════ // Toon 머티리얼 생성 // ═════════════════════════════════════════════════════════════════════ /// suffix 세트의 머티리얼 4개를 만든다(텍스처는 texPaths 로 지정 · face 는 Unlit/Transparent 복사본). public static string MakeSet(string matSuffix, string[] texPaths, bool faceCopy) { var sb = new StringBuilder(); Ensure(MATS); var toon = ToonShader(); if (toon == null) return "TOON SHADER MISS " + TOON_SG; for (int i = 0; i < 4; i++) { string path = MATS + KEYS[i] + matSuffix + ".mat"; var tex = AssetDatabase.LoadAssetAtPath(texPaths[i]); if (tex == null) { sb.AppendLine("TEX MISS " + texPaths[i]); continue; } if (i == 3) { // 얼굴 = Toon 으로 못 옮긴다(알파 미지원) → 원본과 같은 Unlit/Transparent 복사본 if (!faceCopy) { sb.AppendLine("FACE keep original"); continue; } var src = AssetDatabase.LoadAssetAtPath(ORIG_MAT[3]); var fm = AssetDatabase.LoadAssetAtPath(path); if (fm == null) { fm = new Material(src); AssetDatabase.CreateAsset(fm, path); } else { fm.shader = src.shader; fm.CopyPropertiesFromMaterial(src); } fm.SetTexture("_MainTex", tex); EditorUtility.SetDirty(fm); sb.AppendLine("FACE " + path + " shader=" + fm.shader.name + " tex=" + tex.name + " " + tex.width + "x" + tex.height); continue; } var m = AssetDatabase.LoadAssetAtPath(path); if (m == null) { m = new Material(toon); AssetDatabase.CreateAsset(m, path); } m.shader = toon; m.SetTexture("_BaseMap", tex); m.SetTexture("_ShadowBaseMap", tex); m.SetColor("_DiffuseColor", Color.white); m.SetColor("_ShadowDiffuseColor", SHADOW_TINT); m.SetColor("_Outline", OUTLINE_COL); m.SetFloat("_Shades", SHADES); m.SetFloat("_Brightness", BRIGHTNESS); m.SetFloat("_MinimumDarkness", MIN_DARK); m.SetFloat("_Cull", 2f); if (OUTLINES > 0.5f) m.EnableKeyword("_OUTLINESENABLED"); else m.DisableKeyword("_OUTLINESENABLED"); m.SetFloat("_OUTLINESENABLED", OUTLINES); EditorUtility.SetDirty(m); sb.AppendLine("MAT " + path + " tex=" + tex.name + " " + tex.width + "x" + tex.height + " Shades=" + m.GetFloat("_Shades") + " Bright=" + m.GetFloat("_Brightness") + " MinDark=" + m.GetFloat("_MinimumDarkness") + " OUT=" + (m.IsKeywordEnabled("_OUTLINESENABLED") ? 1 : 0)); } AssetDatabase.SaveAssets(); return sb.ToString(); } // ═════════════════════════════════════════════════════════════════════ // SO // ═════════════════════════════════════════════════════════════════════ public static string MakeSO(int defaultMode, string suffix1, string suffix2, string suffix3) { Ensure(RES); var so = AssetDatabase.LoadAssetAtPath(SO_PATH); if (so == null) { so = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(so, SO_PATH); } so.enabled_ = 1; so.mode = defaultMode; so.verboseLog = 0; so.originals = new Material[4]; so.mode1 = new Material[4]; so.mode2 = new Material[4]; so.mode3 = new Material[4]; var sb = new StringBuilder(); for (int i = 0; i < 4; i++) { so.originals[i] = AssetDatabase.LoadAssetAtPath(ORIG_MAT[i]); so.mode1[i] = Pick(MATS + KEYS[i] + suffix1 + ".mat", so.originals[i]); so.mode2[i] = Pick(MATS + KEYS[i] + suffix2 + ".mat", so.originals[i]); so.mode3[i] = Pick(MATS + KEYS[i] + suffix3 + ".mat", so.originals[i]); sb.AppendLine("SO[" + i + "] " + KEYS[i] + " orig=" + Nm(so.originals[i]) + " m1=" + Nm(so.mode1[i]) + " m2=" + Nm(so.mode2[i]) + " m3=" + Nm(so.mode3[i])); } EditorUtility.SetDirty(so); AssetDatabase.SaveAssets(); WLCharacterLookSettings.Invalidate(); sb.AppendLine("SO saved " + SO_PATH + " enabled_=" + so.enabled_ + " mode=" + so.mode); return sb.ToString(); } static Material Pick(string p, Material fallback) { var m = AssetDatabase.LoadAssetAtPath(p); return m != null ? m : fallback; } static string Nm(Material m) { return m == null ? "null" : m.name; } public static string SetSO(int enabled_, int mode) { var so = AssetDatabase.LoadAssetAtPath(SO_PATH); if (so == null) return "SO MISS"; so.enabled_ = enabled_; so.mode = mode; EditorUtility.SetDirty(so); AssetDatabase.SaveAssets(); WLCharacterLookSettings.Invalidate(); return "SO enabled_=" + enabled_ + " mode=" + mode; } // ═════════════════════════════════════════════════════════════════════ // 프리팹 배선 // ═════════════════════════════════════════════════════════════════════ /// 프리팹 렌더러를 지정 모드의 머티리얼로 바꾸고 WLCharacterLook 을 붙인다. public static string PatchPrefab(int mode, bool addComponent) { var so = AssetDatabase.LoadAssetAtPath(SO_PATH); if (so == null) return "SO MISS"; var target = so.SetForMode(mode); var sb = new StringBuilder(); var root = PrefabUtility.LoadPrefabContents(PREFAB); try { if (addComponent && root.GetComponent() == null) { root.AddComponent(); sb.AppendLine("ADD WLCharacterLook on " + root.name); } var rs = root.GetComponentsInChildren(true); foreach (var r in rs) { var mats = r.sharedMaterials; bool ch = false; for (int s = 0; s < mats.Length; s++) { int idx = FindIdx(so, mats[s]); if (idx < 0) { sb.AppendLine("KEEP " + r.name + "[" + s + "] " + Nm(mats[s]) + " (표 밖)"); continue; } if (target[idx] != null && mats[s] != target[idx]) { sb.AppendLine("SWAP " + r.name + "[" + s + "] " + Nm(mats[s]) + " -> " + Nm(target[idx])); mats[s] = target[idx]; ch = true; } else sb.AppendLine("SAME " + r.name + "[" + s + "] " + Nm(mats[s])); } if (ch) r.sharedMaterials = mats; } PrefabUtility.SaveAsPrefabAsset(root, PREFAB); } finally { PrefabUtility.UnloadPrefabContents(root); } AssetDatabase.SaveAssets(); return sb.ToString(); } static int FindIdx(WLCharacterLookSettings c, Material m) { if (m == null) return -1; for (int i = 0; i < 4; i++) { if (c.originals != null && i < c.originals.Length && c.originals[i] == m) return i; if (c.mode1 != null && i < c.mode1.Length && c.mode1[i] == m) return i; if (c.mode2 != null && i < c.mode2.Length && c.mode2[i] == m) return i; if (c.mode3 != null && i < c.mode3.Length && c.mode3[i] == m) return i; } return -1; } public static string DumpPrefab() { var sb = new StringBuilder(); var root = AssetDatabase.LoadAssetAtPath(PREFAB); if (root == null) return "PREFAB MISS"; sb.AppendLine("components on root: "); foreach (var c in root.GetComponents()) sb.AppendLine(" " + c.GetType().Name); foreach (var r in root.GetComponentsInChildren(true)) { var ms = r.sharedMaterials; for (int i = 0; i < ms.Length; i++) sb.AppendLine("REND " + r.name + "[" + i + "] act=" + r.gameObject.activeSelf + " mat=" + Nm(ms[i]) + " shader=" + (ms[i] != null ? ms[i].shader.name : "-")); } return sb.ToString(); } public static void Run() { Debug.Log("[WL814s build] env=\n" + DumpEnvMats()); } // ═════════════════════════════════════════════════════════════════════ // 디스패처 — unity command run_script --entry WL814s_Build.Cmd --args " …" // ═════════════════════════════════════════════════════════════════════ public static string Cmd(string[] args) { if (args == null || args.Length == 0) return DumpEnvMats(); string c = args[0]; try { if (c == "env") return DumpEnvMats(); if (c == "dump") return DumpPrefab(); if (c == "mkset") { // mkset var tex = new string[4] { args[2], args[3], args[4], args[5] }; return MakeSet(args[1], tex, args[6] == "1"); } if (c == "rm") { var sb2 = new StringBuilder(); for (int i = 1; i < args.Length; i++) sb2.AppendLine((AssetDatabase.DeleteAsset(args[i]) ? "DEL " : "MISS ") + args[i]); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); return sb2.ToString(); } if (c == "so") return MakeSO(int.Parse(args[1]), args[2], args[3], args[4]); if (c == "setso") return SetSO(int.Parse(args[1]), int.Parse(args[2])); if (c == "patch") return PatchPrefab(int.Parse(args[1]), args.Length > 2 && args[2] == "1"); return "UNKNOWN " + c; } catch (Exception e) { return "EXCEPTION " + e.GetType().Name + " " + e.Message + "\n" + e.StackTrace; } } }