Project_WL/AgentScripts/WL814s_Build.cs

314 lines
17 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// 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<Shader>(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<Material>("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 머티리얼 생성
// ═════════════════════════════════════════════════════════════════════
/// <summary>suffix 세트의 머티리얼 4개를 만든다(텍스처는 texPaths 로 지정 · face 는 Unlit/Transparent 복사본).</summary>
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<Texture2D>(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<Material>(ORIG_MAT[3]);
var fm = AssetDatabase.LoadAssetAtPath<Material>(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<Material>(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<WLCharacterLookSettings>(SO_PATH);
if (so == null)
{
so = ScriptableObject.CreateInstance<WLCharacterLookSettings>();
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<Material>(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<Material>(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<WLCharacterLookSettings>(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;
}
// ═════════════════════════════════════════════════════════════════════
// 프리팹 배선
// ═════════════════════════════════════════════════════════════════════
/// <summary>프리팹 렌더러를 지정 모드의 머티리얼로 바꾸고 WLCharacterLook 을 붙인다.</summary>
public static string PatchPrefab(int mode, bool addComponent)
{
var so = AssetDatabase.LoadAssetAtPath<WLCharacterLookSettings>(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<WLCharacterLook>() == null)
{
root.AddComponent<WLCharacterLook>();
sb.AppendLine("ADD WLCharacterLook on " + root.name);
}
var rs = root.GetComponentsInChildren<Renderer>(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<GameObject>(PREFAB);
if (root == null) return "PREFAB MISS";
sb.AppendLine("components on root: ");
foreach (var c in root.GetComponents<Component>()) sb.AppendLine(" " + c.GetType().Name);
foreach (var r in root.GetComponentsInChildren<Renderer>(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 "<cmd> <a> <b> …"
// ═════════════════════════════════════════════════════════════════════
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 <suffix> <tex0> <tex1> <tex2> <tex3> <faceCopy 0|1>
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; }
}
}