314 lines
17 KiB
C#
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; }
|
|
}
|
|
}
|