617 lines
32 KiB
C#
617 lines
32 KiB
C#
|
|
// WL-814c — Critter Environment 룩 패스: ① 머티리얼 전수 실측 ② Toon 머티리얼 생성 ③ 렌더러 피처 추가
|
||
|
|
// survey : unity command run_script --file AgentScripts/WL814c_Convert.cs --entry WL814c_Convert.Survey
|
||
|
|
// convert: unity command run_script --file AgentScripts/WL814c_Convert.cs --entry WL814c_Convert.Convert
|
||
|
|
// feature: unity command run_script --file AgentScripts/WL814c_Convert.cs --entry WL814c_Convert.AddOutlineFeature
|
||
|
|
// 🔴 원본 .mat / 프리팹 / 씬 수정 0 · Assets/3DPixelArtEnvironment 수정 0 · 새 파일은 Assets/WL/Look/Env/ 아래만.
|
||
|
|
// 🔴 `Environment` 는 Critter 가 쓰는 전역 네임스페이스라 System.Environment 와 충돌한다 → global:: 로 명시.
|
||
|
|
|
||
|
|
using System;
|
||
|
|
using System.Collections.Generic;
|
||
|
|
using System.IO;
|
||
|
|
using System.Text;
|
||
|
|
using UnityEditor;
|
||
|
|
using UnityEngine;
|
||
|
|
using UnityEngine.Rendering;
|
||
|
|
using UnityEngine.Rendering.Universal;
|
||
|
|
|
||
|
|
public static class WL814c_Convert
|
||
|
|
{
|
||
|
|
const string kSurveyTxt = "AgentScripts/WL814c_SURVEY.txt";
|
||
|
|
const string kConvertTxt = "AgentScripts/WL814c_CONVERT.txt";
|
||
|
|
const string kMatDir = "Assets/WL/Look/Env/Materials";
|
||
|
|
const string kToonShader = "Assets/3DPixelArtEnvironment/Shaders/Toon.shadergraph";
|
||
|
|
const string kMapPrefab = "Assets/Res_Addr/Map/WL_Nature.prefab";
|
||
|
|
|
||
|
|
static StringBuilder s_sb;
|
||
|
|
static int s_pass, s_fail;
|
||
|
|
static void L(string s) { s_sb.AppendLine(s); Debug.Log("[WL814c] " + s); }
|
||
|
|
static void Chk(string n, bool ok, string d) { if (ok) s_pass++; else s_fail++; L((ok ? " PASS " : " FAIL ") + n + " — " + d); }
|
||
|
|
static void Write(string p) { File.WriteAllText(p, s_sb.ToString(), new UTF8Encoding(false)); Debug.Log("[WL814c] → " + p); }
|
||
|
|
|
||
|
|
// ═════════════════════════════════════════════════════════════════════════
|
||
|
|
// ① 실측 표 — 머티리얼 전수
|
||
|
|
// ═════════════════════════════════════════════════════════════════════════
|
||
|
|
|
||
|
|
sealed class MatInfo
|
||
|
|
{
|
||
|
|
public Material mat;
|
||
|
|
public string path;
|
||
|
|
public string shader;
|
||
|
|
public int rendererSlots;
|
||
|
|
public bool hasBaseMap, hasMainTex;
|
||
|
|
public string renderType;
|
||
|
|
public int renderQueue;
|
||
|
|
public bool isParticle;
|
||
|
|
public bool anyMeshHasVertexColor;
|
||
|
|
public string sampleRenderer;
|
||
|
|
}
|
||
|
|
|
||
|
|
public static void Survey()
|
||
|
|
{
|
||
|
|
s_sb = new StringBuilder(); s_pass = 0; s_fail = 0;
|
||
|
|
L("WL-814c 머티리얼 전수 실측 · " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " · Unity " + Application.unityVersion);
|
||
|
|
try
|
||
|
|
{
|
||
|
|
// ── URP 렌더러 에셋
|
||
|
|
var urp = GraphicsSettings.currentRenderPipeline as UniversalRenderPipelineAsset;
|
||
|
|
L("");
|
||
|
|
L("## A. 활성 URP 에셋 · 렌더러 에셋");
|
||
|
|
L(" currentRenderPipeline = " + (urp != null ? AssetDatabase.GetAssetPath(urp) : "null"));
|
||
|
|
var rdPath = RendererDataPath(urp);
|
||
|
|
L(" m_RendererDataList[0] = " + rdPath);
|
||
|
|
var rd = string.IsNullOrEmpty(rdPath) ? null : AssetDatabase.LoadAssetAtPath<ScriptableRendererData>(rdPath);
|
||
|
|
if (rd != null)
|
||
|
|
{
|
||
|
|
L(" 현재 피처 " + rd.rendererFeatures.Count + "개:");
|
||
|
|
foreach (var f in rd.rendererFeatures)
|
||
|
|
L(" - " + (f == null ? "(null)" : f.name + " [" + f.GetType().FullName + "] active=" + f.isActive));
|
||
|
|
}
|
||
|
|
Chk("A 렌더러 에셋", rd != null, rdPath);
|
||
|
|
|
||
|
|
// ── 맵 머티리얼
|
||
|
|
var map = AssetDatabase.LoadAssetAtPath<GameObject>(kMapPrefab);
|
||
|
|
Chk("B 맵 프리팹", map != null, kMapPrefab);
|
||
|
|
if (map == null) return;
|
||
|
|
|
||
|
|
var table = new Dictionary<Material, MatInfo>();
|
||
|
|
int rendererCount = 0, nullSlots = 0;
|
||
|
|
CollectFromPrefab(map, table, ref rendererCount, ref nullSlots);
|
||
|
|
|
||
|
|
L("");
|
||
|
|
L("## B. WL_Nature.prefab 머티리얼 전수 (렌더러 " + rendererCount + " · 빈 슬롯 " + nullSlots + ")");
|
||
|
|
DumpTable(table);
|
||
|
|
|
||
|
|
// ── 지면 후보 · 물 후보
|
||
|
|
L("");
|
||
|
|
L("## C. 지면(Ground) 후보 · 물(Water) 후보 · 메시 읽기 가능 여부");
|
||
|
|
L(" | 오브젝트 | 메시 | 면적(m²) | isReadable | 바운드 | 머티리얼 |");
|
||
|
|
L(" |---|---|---|---|---|---|");
|
||
|
|
var groundRows = new List<string>();
|
||
|
|
foreach (var mf in map.GetComponentsInChildren<MeshFilter>(true))
|
||
|
|
{
|
||
|
|
var mr = mf.GetComponent<MeshRenderer>();
|
||
|
|
if (mr == null || mf.sharedMesh == null) continue;
|
||
|
|
var b = mr.bounds;
|
||
|
|
float footprint = b.size.x * b.size.z;
|
||
|
|
if (footprint < 100f) continue; // 지면 후보 = 수평 투영 100 m² 이상
|
||
|
|
float area = -1f;
|
||
|
|
try { if (mf.sharedMesh.isReadable) area = global::Environment.Utilities.MeshUtilities.GetMeshArea(mf.sharedMesh); }
|
||
|
|
catch { area = -2f; }
|
||
|
|
string mats = "";
|
||
|
|
foreach (var m in mr.sharedMaterials) mats += (m != null ? m.name : "(null)") + " ";
|
||
|
|
groundRows.Add(" | " + Path(mf.transform) + " | " + mf.sharedMesh.name + " | " +
|
||
|
|
(area >= 0 ? area.ToString("0") : (area == -2f ? "예외" : "읽기불가")) + " | " +
|
||
|
|
mf.sharedMesh.isReadable + " | " + b.size.ToString("0.0") + " | " + mats.Trim() + " |");
|
||
|
|
}
|
||
|
|
groundRows.Sort();
|
||
|
|
foreach (var r in groundRows) L(r);
|
||
|
|
L(" 지면 후보 " + groundRows.Count + "개");
|
||
|
|
|
||
|
|
L("");
|
||
|
|
L(" 물 후보(이름 또는 셰이더에 water):");
|
||
|
|
int water = 0;
|
||
|
|
foreach (var r in map.GetComponentsInChildren<Renderer>(true))
|
||
|
|
{
|
||
|
|
bool nameHit = r.name.ToLowerInvariant().Contains("water");
|
||
|
|
bool shaderHit = false;
|
||
|
|
foreach (var m in r.sharedMaterials)
|
||
|
|
if (m != null && m.shader != null && m.shader.name.ToLowerInvariant().Contains("water")) shaderHit = true;
|
||
|
|
if (nameHit || shaderHit) { water++; L(" - " + Path(r.transform) + " · " + (r.sharedMaterial != null ? r.sharedMaterial.name : "-")); }
|
||
|
|
}
|
||
|
|
L(" 합계 " + water + "개");
|
||
|
|
|
||
|
|
var terrains = map.GetComponentsInChildren<Terrain>(true);
|
||
|
|
L(" Unity Terrain 컴포넌트: " + terrains.Length + "개");
|
||
|
|
|
||
|
|
// ── 캐릭터
|
||
|
|
L("");
|
||
|
|
L("## D. 캐릭터 프리팹 머티리얼");
|
||
|
|
var charPaths = new List<string>();
|
||
|
|
charPaths.Add("Assets/Res_Addr/PC/Ai01.prefab");
|
||
|
|
foreach (var g in AssetDatabase.FindAssets("t:Prefab", new[] { "Assets/Res_Addr/Mobs" }))
|
||
|
|
{
|
||
|
|
var p = AssetDatabase.GUIDToAssetPath(g);
|
||
|
|
if (p.EndsWith(".prefab")) charPaths.Add(p);
|
||
|
|
}
|
||
|
|
var charTable = new Dictionary<Material, MatInfo>();
|
||
|
|
int cr = 0, cn = 0;
|
||
|
|
int charPrefabs = 0;
|
||
|
|
foreach (var p in charPaths)
|
||
|
|
{
|
||
|
|
var go = AssetDatabase.LoadAssetAtPath<GameObject>(p);
|
||
|
|
if (go == null) continue;
|
||
|
|
charPrefabs++;
|
||
|
|
CollectFromPrefab(go, charTable, ref cr, ref cn);
|
||
|
|
}
|
||
|
|
L(" 캐릭터 프리팹 " + charPrefabs + "개 · 렌더러 " + cr + " · 빈 슬롯 " + cn);
|
||
|
|
DumpTable(charTable);
|
||
|
|
}
|
||
|
|
catch (Exception ex) { s_fail++; L("EXCEPTION " + ex); }
|
||
|
|
L("");
|
||
|
|
L("RESULT " + (s_fail == 0 ? "PASS" : "FAIL") + " · " + s_pass + "/" + (s_pass + s_fail) + " · FAIL " + s_fail);
|
||
|
|
Write(kSurveyTxt);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void DumpTable(Dictionary<Material, MatInfo> table)
|
||
|
|
{
|
||
|
|
var list = new List<MatInfo>(table.Values);
|
||
|
|
list.Sort((a, b) => string.CompareOrdinal(a.shader + a.mat.name, b.shader + b.mat.name));
|
||
|
|
L(" | 머티리얼 | 셰이더 | 슬롯 | RenderType | queue | BaseMap | MainTex | 정점색메시 | 경로 |");
|
||
|
|
L(" |---|---|---|---|---|---|---|---|---|");
|
||
|
|
foreach (var i in list)
|
||
|
|
L(" | " + i.mat.name + " | " + i.shader + " | " + i.rendererSlots + " | " + i.renderType + " | " + i.renderQueue +
|
||
|
|
" | " + (i.hasBaseMap ? "O" : "-") + " | " + (i.hasMainTex ? "O" : "-") + " | " + (i.anyMeshHasVertexColor ? "O" : "-") +
|
||
|
|
" | " + i.path + " |");
|
||
|
|
var byShader = new Dictionary<string, int>();
|
||
|
|
foreach (var i in list) { int c; byShader.TryGetValue(i.shader, out c); byShader[i.shader] = c + 1; }
|
||
|
|
L(" 셰이더별 개수:");
|
||
|
|
foreach (var kv in byShader) L(" " + kv.Key + " = " + kv.Value);
|
||
|
|
L(" 머티리얼 합계 " + list.Count + "종");
|
||
|
|
}
|
||
|
|
|
||
|
|
static void CollectFromPrefab(GameObject root, Dictionary<Material, MatInfo> table, ref int rendererCount, ref int nullSlots)
|
||
|
|
{
|
||
|
|
foreach (var r in root.GetComponentsInChildren<Renderer>(true))
|
||
|
|
{
|
||
|
|
rendererCount++;
|
||
|
|
bool vtxColor = false;
|
||
|
|
var mf = r.GetComponent<MeshFilter>();
|
||
|
|
if (mf != null && mf.sharedMesh != null && mf.sharedMesh.isReadable)
|
||
|
|
{ try { vtxColor = mf.sharedMesh.colors32 != null && mf.sharedMesh.colors32.Length > 0; } catch { } }
|
||
|
|
var smr = r as SkinnedMeshRenderer;
|
||
|
|
if (smr != null && smr.sharedMesh != null && smr.sharedMesh.isReadable)
|
||
|
|
{ try { vtxColor = smr.sharedMesh.colors32 != null && smr.sharedMesh.colors32.Length > 0; } catch { } }
|
||
|
|
|
||
|
|
foreach (var m in r.sharedMaterials)
|
||
|
|
{
|
||
|
|
if (m == null) { nullSlots++; continue; }
|
||
|
|
MatInfo info;
|
||
|
|
if (!table.TryGetValue(m, out info))
|
||
|
|
{
|
||
|
|
info = new MatInfo
|
||
|
|
{
|
||
|
|
mat = m,
|
||
|
|
path = AssetDatabase.GetAssetPath(m),
|
||
|
|
shader = m.shader != null ? m.shader.name : "(null)",
|
||
|
|
renderType = m.GetTag("RenderType", false, "(none)"),
|
||
|
|
renderQueue = m.renderQueue,
|
||
|
|
hasBaseMap = m.HasProperty("_BaseMap") && m.GetTexture("_BaseMap") != null,
|
||
|
|
hasMainTex = m.HasProperty("_MainTex") && m.GetTexture("_MainTex") != null,
|
||
|
|
sampleRenderer = Path(r.transform)
|
||
|
|
};
|
||
|
|
info.isParticle = r is ParticleSystemRenderer || info.shader.ToLowerInvariant().Contains("particle");
|
||
|
|
table[m] = info;
|
||
|
|
}
|
||
|
|
info.rendererSlots++;
|
||
|
|
if (vtxColor) info.anyMeshHasVertexColor = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
static string Path(Transform t)
|
||
|
|
{
|
||
|
|
var sb = new StringBuilder(t.name);
|
||
|
|
var p = t.parent;
|
||
|
|
int guard = 0;
|
||
|
|
while (p != null && guard++ < 12) { sb.Insert(0, p.name + "/"); p = p.parent; }
|
||
|
|
return sb.ToString();
|
||
|
|
}
|
||
|
|
|
||
|
|
static string RendererDataPath(UniversalRenderPipelineAsset urp)
|
||
|
|
{
|
||
|
|
if (urp == null) return "";
|
||
|
|
var so = new SerializedObject(urp);
|
||
|
|
var list = so.FindProperty("m_RendererDataList");
|
||
|
|
if (list == null || list.arraySize == 0) return "";
|
||
|
|
int idx = 0;
|
||
|
|
var di = so.FindProperty("m_DefaultRendererIndex");
|
||
|
|
if (di != null) idx = Mathf.Clamp(di.intValue, 0, list.arraySize - 1);
|
||
|
|
var el = list.GetArrayElementAtIndex(idx).objectReferenceValue;
|
||
|
|
return el != null ? AssetDatabase.GetAssetPath(el) : "";
|
||
|
|
}
|
||
|
|
|
||
|
|
// ═════════════════════════════════════════════════════════════════════════
|
||
|
|
// ② Toon 머티리얼 생성
|
||
|
|
// ═════════════════════════════════════════════════════════════════════════
|
||
|
|
|
||
|
|
public static void Convert()
|
||
|
|
{
|
||
|
|
s_sb = new StringBuilder(); s_pass = 0; s_fail = 0;
|
||
|
|
L("WL-814c Toon 머티리얼 생성 · " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
|
||
|
|
try
|
||
|
|
{
|
||
|
|
var toon = AssetDatabase.LoadAssetAtPath<Shader>(kToonShader);
|
||
|
|
Chk("S 셰이더", toon != null, kToonShader);
|
||
|
|
if (toon == null) return;
|
||
|
|
|
||
|
|
var cfg = LoadSettings();
|
||
|
|
Chk("S SO", cfg != null, cfg != null ? AssetDatabase.GetAssetPath(cfg) : "없음");
|
||
|
|
if (cfg == null) return;
|
||
|
|
|
||
|
|
Directory.CreateDirectory(kMatDir);
|
||
|
|
|
||
|
|
// 원본 후보 수집: 맵 + (SO convertCharacters 면) 캐릭터
|
||
|
|
var table = new Dictionary<Material, MatInfo>();
|
||
|
|
int rc = 0, ns = 0;
|
||
|
|
var map = AssetDatabase.LoadAssetAtPath<GameObject>(kMapPrefab);
|
||
|
|
if (map != null) CollectFromPrefab(map, table, ref rc, ref ns);
|
||
|
|
int mapMats = table.Count;
|
||
|
|
|
||
|
|
var charPaths = new List<string>();
|
||
|
|
charPaths.Add("Assets/Res_Addr/PC/Ai01.prefab");
|
||
|
|
foreach (var g in AssetDatabase.FindAssets("t:Prefab", new[] { "Assets/Res_Addr/Mobs" }))
|
||
|
|
{
|
||
|
|
var p = AssetDatabase.GUIDToAssetPath(g);
|
||
|
|
if (p.EndsWith(".prefab")) charPaths.Add(p);
|
||
|
|
}
|
||
|
|
var charTable = new Dictionary<Material, MatInfo>();
|
||
|
|
foreach (var p in charPaths)
|
||
|
|
{
|
||
|
|
var go = AssetDatabase.LoadAssetAtPath<GameObject>(p);
|
||
|
|
if (go != null) CollectFromPrefab(go, charTable, ref rc, ref ns);
|
||
|
|
}
|
||
|
|
foreach (var kv in charTable) if (!table.ContainsKey(kv.Key)) table[kv.Key] = kv.Value;
|
||
|
|
|
||
|
|
L(" 후보 = 맵 " + mapMats + "종 + 캐릭터 " + charTable.Count + "종 → 중복 제거 " + table.Count + "종");
|
||
|
|
|
||
|
|
var origs = new List<Material>();
|
||
|
|
var toons = new List<Material>();
|
||
|
|
var skipped = new List<string>();
|
||
|
|
var rows = new List<string>();
|
||
|
|
|
||
|
|
foreach (var kv in table)
|
||
|
|
{
|
||
|
|
var src = kv.Key; var info = kv.Value;
|
||
|
|
string why;
|
||
|
|
if (!IsConvertible(info, out why)) { skipped.Add(" | " + src.name + " | " + info.shader + " | " + why + " |"); continue; }
|
||
|
|
bool isChar = charTable.ContainsKey(src) && !IsInMap(src, map);
|
||
|
|
|
||
|
|
string dst = kMatDir + "/" + Sanitize(src.name) + "_Toon.mat";
|
||
|
|
var m = AssetDatabase.LoadAssetAtPath<Material>(dst);
|
||
|
|
bool created = false;
|
||
|
|
if (m == null) { m = new Material(toon); AssetDatabase.CreateAsset(m, dst); created = true; }
|
||
|
|
if (m.shader != toon) m.shader = toon;
|
||
|
|
|
||
|
|
ApplyToonValues(m, src, cfg);
|
||
|
|
EditorUtility.SetDirty(m);
|
||
|
|
|
||
|
|
origs.Add(src); toons.Add(m);
|
||
|
|
var bm = m.GetTexture("_BaseMap");
|
||
|
|
rows.Add(" | " + src.name + " | " + info.shader + " | " + m.name + " | " +
|
||
|
|
(bm != null ? bm.name : "(없음)") + " | " + ColorStr(m.GetColor("_DiffuseColor")) + " | " +
|
||
|
|
m.GetFloat("_Shades").ToString("0") + " | " + (isChar ? "캐릭터" : "맵") + " | " + (created ? "신규" : "갱신") + " |");
|
||
|
|
}
|
||
|
|
|
||
|
|
AssetDatabase.SaveAssets();
|
||
|
|
AssetDatabase.Refresh();
|
||
|
|
|
||
|
|
// SO 테이블 채우기
|
||
|
|
var soo = new SerializedObject(cfg);
|
||
|
|
var pO = soo.FindProperty("originals");
|
||
|
|
var pT = soo.FindProperty("toons");
|
||
|
|
pO.arraySize = origs.Count; pT.arraySize = toons.Count;
|
||
|
|
for (int i = 0; i < origs.Count; i++)
|
||
|
|
{
|
||
|
|
pO.GetArrayElementAtIndex(i).objectReferenceValue = origs[i];
|
||
|
|
pT.GetArrayElementAtIndex(i).objectReferenceValue = toons[i];
|
||
|
|
}
|
||
|
|
soo.ApplyModifiedPropertiesWithoutUndo();
|
||
|
|
EditorUtility.SetDirty(cfg);
|
||
|
|
AssetDatabase.SaveAssets();
|
||
|
|
|
||
|
|
L("");
|
||
|
|
L("## 생성 머티리얼 표 (" + toons.Count + "종)");
|
||
|
|
L(" | 원본 | 원본 셰이더 | 생성 | BaseMap | DiffuseColor | Shades | 구분 | 상태 |");
|
||
|
|
L(" |---|---|---|---|---|---|---|---|");
|
||
|
|
rows.Sort();
|
||
|
|
foreach (var r in rows) L(r);
|
||
|
|
|
||
|
|
L("");
|
||
|
|
L("## 제외 (" + skipped.Count + "종)");
|
||
|
|
L(" | 머티리얼 | 셰이더 | 이유 |");
|
||
|
|
L(" |---|---|---|");
|
||
|
|
skipped.Sort();
|
||
|
|
foreach (var r in skipped) L(r);
|
||
|
|
|
||
|
|
Chk("C 생성", toons.Count > 0, toons.Count + "종 → " + kMatDir);
|
||
|
|
Chk("C SO 테이블", cfg.originals != null && cfg.originals.Length == toons.Count, cfg.originals != null ? cfg.originals.Length + "쌍" : "null");
|
||
|
|
}
|
||
|
|
catch (Exception ex) { s_fail++; L("EXCEPTION " + ex); }
|
||
|
|
L("");
|
||
|
|
L("RESULT " + (s_fail == 0 ? "PASS" : "FAIL") + " · " + s_pass + "/" + (s_pass + s_fail) + " · FAIL " + s_fail);
|
||
|
|
Write(kConvertTxt);
|
||
|
|
}
|
||
|
|
|
||
|
|
static bool IsInMap(Material m, GameObject map)
|
||
|
|
{
|
||
|
|
if (map == null) return false;
|
||
|
|
foreach (var r in map.GetComponentsInChildren<Renderer>(true))
|
||
|
|
foreach (var x in r.sharedMaterials) if (x == m) return true;
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>불투명 서피스 머티리얼만 변환. 파티클·투명·이펙트·UI·스카이박스·물은 제외(룩 유지).</summary>
|
||
|
|
static bool IsConvertible(MatInfo i, out string why)
|
||
|
|
{
|
||
|
|
why = "";
|
||
|
|
string sh = i.shader.ToLowerInvariant();
|
||
|
|
string nm = i.mat.name.ToLowerInvariant();
|
||
|
|
if (i.isParticle) { why = "파티클"; return false; }
|
||
|
|
if (sh.Contains("particle") || sh.Contains("ui/") || sh.Contains("skybox") || sh.Contains("sprite") ||
|
||
|
|
sh.Contains("textmeshpro") || sh.Contains("unlit") || sh.Contains("universal render pipeline/2d"))
|
||
|
|
{ why = "파티클/UI/폰트/Unlit"; return false; }
|
||
|
|
if (sh.Contains("water") || nm.Contains("water")) { why = "물(별도 Critter Water 스왑)"; return false; }
|
||
|
|
if (i.renderType == "Transparent" || i.renderType == "TransparentCutout") { why = "투명(" + i.renderType + ")"; return false; }
|
||
|
|
if (i.renderQueue >= 2450) { why = "queue " + i.renderQueue + "(투명)"; return false; }
|
||
|
|
if (sh.Contains("shader graphs/toon") && !sh.Contains("water")) { why = "이미 Critter Toon"; return false; }
|
||
|
|
bool surface = i.mat.HasProperty("_BaseMap") || i.mat.HasProperty("_MainTex") ||
|
||
|
|
i.mat.HasProperty("_BaseColor") || i.mat.HasProperty("_Color");
|
||
|
|
if (!surface) { why = "서피스 머티리얼 아님(_BaseMap/_MainTex/_Color 0)"; return false; }
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ═════════════════════════════════════════════════════════════════════════
|
||
|
|
// ⓪ 설정 SO 생성 + Critter 에셋 참조 채우기
|
||
|
|
// ═════════════════════════════════════════════════════════════════════════
|
||
|
|
|
||
|
|
public static void CreateSettings()
|
||
|
|
{
|
||
|
|
s_sb = new StringBuilder(); s_pass = 0; s_fail = 0;
|
||
|
|
L("WL-814c 설정 SO 생성 · " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
|
||
|
|
try
|
||
|
|
{
|
||
|
|
const string dir = "Assets/WL/Look/Env/Resources/WL";
|
||
|
|
const string path = dir + "/WLEnvLookSettings.asset";
|
||
|
|
Directory.CreateDirectory(dir);
|
||
|
|
var cfg = AssetDatabase.LoadAssetAtPath<WL.Look.Env.WLEnvLookSettings>(path);
|
||
|
|
bool created = false;
|
||
|
|
if (cfg == null)
|
||
|
|
{
|
||
|
|
cfg = ScriptableObject.CreateInstance<WL.Look.Env.WLEnvLookSettings>();
|
||
|
|
AssetDatabase.CreateAsset(cfg, path);
|
||
|
|
created = true;
|
||
|
|
}
|
||
|
|
cfg.grassMesh = LoadSub<Mesh>("Assets/3DPixelArtEnvironment/Meshes/Instanced/Grass_Leaf.fbx");
|
||
|
|
cfg.flowerMesh = LoadSub<Mesh>("Assets/3DPixelArtEnvironment/Meshes/Instanced/Flower_Leaf.fbx");
|
||
|
|
cfg.grassMaterial = AssetDatabase.LoadAssetAtPath<Material>("Assets/3DPixelArtEnvironment/Materials/Instanced_Grass.mat");
|
||
|
|
cfg.flowerMaterial = AssetDatabase.LoadAssetAtPath<Material>("Assets/3DPixelArtEnvironment/Materials/Instanced_Flower.mat");
|
||
|
|
cfg.waterMaterial = AssetDatabase.LoadAssetAtPath<Material>("Assets/3DPixelArtEnvironment/Materials/Water.mat");
|
||
|
|
EditorUtility.SetDirty(cfg);
|
||
|
|
AssetDatabase.SaveAssets();
|
||
|
|
AssetDatabase.Refresh();
|
||
|
|
|
||
|
|
Chk("S0 SO", cfg != null, (created ? "신규 " : "갱신 ") + path);
|
||
|
|
Chk("S0 잔디 메시", cfg.grassMesh != null, cfg.grassMesh != null ? cfg.grassMesh.name : "null");
|
||
|
|
Chk("S0 잔디 머티리얼", cfg.grassMaterial != null, cfg.grassMaterial != null ? cfg.grassMaterial.name : "null");
|
||
|
|
Chk("S0 꽃 메시", cfg.flowerMesh != null, cfg.flowerMesh != null ? cfg.flowerMesh.name : "null");
|
||
|
|
Chk("S0 꽃 머티리얼", cfg.flowerMaterial != null, cfg.flowerMaterial != null ? cfg.flowerMaterial.name : "null");
|
||
|
|
Chk("S0 물 머티리얼", cfg.waterMaterial != null, cfg.waterMaterial != null ? cfg.waterMaterial.name : "null");
|
||
|
|
}
|
||
|
|
catch (Exception ex) { s_fail++; L("EXCEPTION " + ex); }
|
||
|
|
L("");
|
||
|
|
L("RESULT " + (s_fail == 0 ? "PASS" : "FAIL") + " · " + s_pass + "/" + (s_pass + s_fail) + " · FAIL " + s_fail);
|
||
|
|
Write("AgentScripts/WL814c_SETTINGS.txt");
|
||
|
|
}
|
||
|
|
|
||
|
|
static T LoadSub<T>(string path) where T : UnityEngine.Object
|
||
|
|
{
|
||
|
|
var all = AssetDatabase.LoadAllAssetsAtPath(path);
|
||
|
|
foreach (var a in all) { var t = a as T; if (t != null) return t; }
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
static void ApplyToonValues(Material m, Material src, WL.Look.Env.WLEnvLookSettings c)
|
||
|
|
{
|
||
|
|
// BaseMap / DiffuseColor 이관 — 원본 _BaseMap → _MainTex 순
|
||
|
|
Texture tex = null;
|
||
|
|
if (src.HasProperty("_BaseMap")) tex = src.GetTexture("_BaseMap");
|
||
|
|
if (tex == null && src.HasProperty("_MainTex")) tex = src.GetTexture("_MainTex");
|
||
|
|
Vector2 sc = Vector2.one, of = Vector2.zero;
|
||
|
|
if (src.HasProperty("_BaseMap")) { sc = src.GetTextureScale("_BaseMap"); of = src.GetTextureOffset("_BaseMap"); }
|
||
|
|
else if (src.HasProperty("_MainTex")) { sc = src.GetTextureScale("_MainTex"); of = src.GetTextureOffset("_MainTex"); }
|
||
|
|
|
||
|
|
Color baseCol = Color.white;
|
||
|
|
if (src.HasProperty("_BaseColor")) baseCol = src.GetColor("_BaseColor");
|
||
|
|
else if (src.HasProperty("_Color")) baseCol = src.GetColor("_Color");
|
||
|
|
|
||
|
|
m.SetTexture("_BaseMap", tex);
|
||
|
|
m.SetTextureScale("_BaseMap", sc);
|
||
|
|
m.SetTextureOffset("_BaseMap", of);
|
||
|
|
m.SetTexture("_ShadowBaseMap", tex);
|
||
|
|
m.SetTextureScale("_ShadowBaseMap", sc);
|
||
|
|
m.SetTextureOffset("_ShadowBaseMap", of);
|
||
|
|
|
||
|
|
m.SetColor("_DiffuseColor", baseCol);
|
||
|
|
// 그림자 색 = 원본 색을 어둡게 + 살짝 차갑게(데모 머티리얼의 관계를 따름)
|
||
|
|
Color sh = new Color(baseCol.r * c.shadowTint.r, baseCol.g * c.shadowTint.g, baseCol.b * c.shadowTint.b, baseCol.a);
|
||
|
|
m.SetColor("_ShadowDiffuseColor", sh);
|
||
|
|
|
||
|
|
// 툰 램프
|
||
|
|
m.SetFloat("_Shades", c.shades);
|
||
|
|
m.SetFloat("_Brightness", c.brightness);
|
||
|
|
m.SetFloat("_MinimumDarkness", c.minimumDarkness);
|
||
|
|
if (m.HasProperty("_AmbientStrength")) m.SetFloat("_AmbientStrength", c.ambientStrength);
|
||
|
|
|
||
|
|
// 외곽선
|
||
|
|
m.SetFloat("_DepthThreshold", c.depthThreshold);
|
||
|
|
m.SetFloat("_NormalThreshold", c.normalThreshold);
|
||
|
|
m.SetVector("_NormalBias", c.normalBias);
|
||
|
|
m.SetFloat("_DepthEdgeStrength", c.depthEdgeStrength);
|
||
|
|
m.SetFloat("_NormalEdgeStrength", c.normalEdgeStrength);
|
||
|
|
if (m.HasProperty("_Outline")) m.SetColor("_Outline", c.outlineColor);
|
||
|
|
SetKw(m, "_OUTLINESENABLED", c.outlinesEnabled);
|
||
|
|
|
||
|
|
// 구름 그림자
|
||
|
|
// 🔴 실측: 이 셰이더에 `_CLOUDSENABLED` 키워드는 **없다**(keywordSpace 36개에 없음 · 데모 .mat 의
|
||
|
|
// `_CLOUDSENABLED` 는 구버전 잔재라 m_InvalidKeywords 로 들어가 있다). 구름 on/off 는 `_Cloud_Strength` 로 한다.
|
||
|
|
m.SetFloat("_Cloud_Density", c.cloudDensity);
|
||
|
|
m.SetVector("_Cloud_Movement", c.cloudMovement);
|
||
|
|
m.SetFloat("_Cloud_Strength", c.cloudShadows ? c.cloudStrength : 0f);
|
||
|
|
m.SetFloat("_Cloud_Cover", c.cloudCover);
|
||
|
|
m.SetFloat("_Cloud_Change", c.cloudChange);
|
||
|
|
m.SetVector("_Cloud_Step", c.cloudStep);
|
||
|
|
m.DisableKeyword("_CLOUDSENABLED"); // 구버전 잔재 키워드가 m_InvalidKeywords 로 남지 않게
|
||
|
|
|
||
|
|
m.enableInstancing = src.enableInstancing;
|
||
|
|
m.doubleSidedGI = src.doubleSidedGI;
|
||
|
|
if (src.HasProperty("_Cull") && m.HasProperty("_Cull")) m.SetFloat("_Cull", src.GetFloat("_Cull"));
|
||
|
|
m.renderQueue = -1;
|
||
|
|
}
|
||
|
|
|
||
|
|
static void SetKw(Material m, string kw, bool on)
|
||
|
|
{
|
||
|
|
if (on) m.EnableKeyword(kw); else m.DisableKeyword(kw);
|
||
|
|
if (m.HasProperty(kw)) m.SetFloat(kw, on ? 1f : 0f);
|
||
|
|
}
|
||
|
|
|
||
|
|
static string ColorStr(Color c) { return "(" + c.r.ToString("0.00") + "," + c.g.ToString("0.00") + "," + c.b.ToString("0.00") + ")"; }
|
||
|
|
static string Sanitize(string n)
|
||
|
|
{
|
||
|
|
var sb = new StringBuilder();
|
||
|
|
foreach (var ch in n) sb.Append(char.IsLetterOrDigit(ch) || ch == '_' || ch == '-' || ch == '.' ? ch : '_');
|
||
|
|
return sb.ToString();
|
||
|
|
}
|
||
|
|
|
||
|
|
static WL.Look.Env.WLEnvLookSettings LoadSettings()
|
||
|
|
{
|
||
|
|
var all = AssetDatabase.FindAssets("t:WLEnvLookSettings");
|
||
|
|
foreach (var g in all)
|
||
|
|
{
|
||
|
|
var p = AssetDatabase.GUIDToAssetPath(g);
|
||
|
|
var a = AssetDatabase.LoadAssetAtPath<WL.Look.Env.WLEnvLookSettings>(p);
|
||
|
|
if (a != null) return a;
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ═════════════════════════════════════════════════════════════════════════
|
||
|
|
// ③ 렌더러 피처 추가 (🔴 URP 에셋 디스크 수정은 이 1건만 허용)
|
||
|
|
// ═════════════════════════════════════════════════════════════════════════
|
||
|
|
|
||
|
|
public static void AddOutlineFeature()
|
||
|
|
{
|
||
|
|
s_sb = new StringBuilder(); s_pass = 0; s_fail = 0;
|
||
|
|
L("WL-814c 픽셀 외곽선 렌더러 피처 추가 · " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
|
||
|
|
try
|
||
|
|
{
|
||
|
|
var urp = GraphicsSettings.currentRenderPipeline as UniversalRenderPipelineAsset;
|
||
|
|
string rdPath = RendererDataPath(urp);
|
||
|
|
var rd = AssetDatabase.LoadAssetAtPath<ScriptableRendererData>(rdPath);
|
||
|
|
Chk("F 렌더러 에셋", rd != null, rdPath);
|
||
|
|
if (rd == null) return;
|
||
|
|
|
||
|
|
var type = typeof(global::Environment.PixelOutlineSetupFeature);
|
||
|
|
foreach (var f in rd.rendererFeatures)
|
||
|
|
if (f != null && f.GetType() == type)
|
||
|
|
{ Chk("F 이미 있음(추가 0)", true, f.name); Write(kConvertTxt.Replace("CONVERT", "FEATURE")); return; }
|
||
|
|
|
||
|
|
var feature = (ScriptableRendererFeature)ScriptableObject.CreateInstance(type);
|
||
|
|
feature.name = "PixelOutlineSetup";
|
||
|
|
feature.hideFlags = HideFlags.HideInHierarchy;
|
||
|
|
feature.SetActive(false); // 🔴 디스크 기본값 = off → 타이틀/로비 비용 0 · 런타임이 인게임에서만 켠다
|
||
|
|
AssetDatabase.AddObjectToAsset(feature, rd);
|
||
|
|
|
||
|
|
var so = new SerializedObject(rd);
|
||
|
|
var feats = so.FindProperty("m_RendererFeatures");
|
||
|
|
var map = so.FindProperty("m_RendererFeatureMap");
|
||
|
|
feats.arraySize++;
|
||
|
|
feats.GetArrayElementAtIndex(feats.arraySize - 1).objectReferenceValue = feature;
|
||
|
|
|
||
|
|
// m_RendererFeatureMap = 각 피처 localFileId 를 8바이트 리틀엔디언으로 이어붙인 hex 문자열
|
||
|
|
long id;
|
||
|
|
string guid;
|
||
|
|
AssetDatabase.TryGetGUIDAndLocalFileIdentifier(feature, out guid, out id);
|
||
|
|
map.stringValue = map.stringValue + Hex8(id);
|
||
|
|
so.ApplyModifiedPropertiesWithoutUndo();
|
||
|
|
|
||
|
|
EditorUtility.SetDirty(rd);
|
||
|
|
AssetDatabase.SaveAssets();
|
||
|
|
AssetDatabase.ImportAsset(rdPath, ImportAssetOptions.ForceUpdate);
|
||
|
|
|
||
|
|
var reload = AssetDatabase.LoadAssetAtPath<ScriptableRendererData>(rdPath);
|
||
|
|
int n = 0;
|
||
|
|
foreach (var f in reload.rendererFeatures) if (f != null && f.GetType() == type) n++;
|
||
|
|
Chk("F 추가 확인", n == 1, "피처 " + reload.rendererFeatures.Count + "개 · PixelOutlineSetup " + n + "개 · map=" + new SerializedObject(reload).FindProperty("m_RendererFeatureMap").stringValue);
|
||
|
|
foreach (var f in reload.rendererFeatures) L(" - " + (f == null ? "(null)" : f.name + " [" + f.GetType().Name + "] active=" + f.isActive));
|
||
|
|
}
|
||
|
|
catch (Exception ex) { s_fail++; L("EXCEPTION " + ex); }
|
||
|
|
L("");
|
||
|
|
L("RESULT " + (s_fail == 0 ? "PASS" : "FAIL") + " · " + s_pass + "/" + (s_pass + s_fail) + " · FAIL " + s_fail);
|
||
|
|
Write("AgentScripts/WL814c_FEATURE.txt");
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>m_RendererFeatureMap(List<long>) 을 현재 피처 목록의 localFileId 로 다시 만든다.</summary>
|
||
|
|
public static void FixFeatureMap()
|
||
|
|
{
|
||
|
|
s_sb = new StringBuilder(); s_pass = 0; s_fail = 0;
|
||
|
|
L("WL-814c 렌더러 피처 맵 재작성 · " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
|
||
|
|
try
|
||
|
|
{
|
||
|
|
var urp = GraphicsSettings.currentRenderPipeline as UniversalRenderPipelineAsset;
|
||
|
|
string rdPath = RendererDataPath(urp);
|
||
|
|
var rd = AssetDatabase.LoadAssetAtPath<ScriptableRendererData>(rdPath);
|
||
|
|
Chk("M 렌더러 에셋", rd != null, rdPath);
|
||
|
|
if (rd == null) return;
|
||
|
|
|
||
|
|
var so = new SerializedObject(rd);
|
||
|
|
var map = so.FindProperty("m_RendererFeatureMap");
|
||
|
|
Chk("M 맵 프로퍼티", map != null && map.isArray, map != null ? "isArray=" + map.isArray : "null");
|
||
|
|
if (map == null || !map.isArray) return;
|
||
|
|
|
||
|
|
map.arraySize = rd.rendererFeatures.Count;
|
||
|
|
for (int i = 0; i < rd.rendererFeatures.Count; i++)
|
||
|
|
{
|
||
|
|
long id = 0; string guid;
|
||
|
|
if (rd.rendererFeatures[i] != null)
|
||
|
|
AssetDatabase.TryGetGUIDAndLocalFileIdentifier(rd.rendererFeatures[i], out guid, out id);
|
||
|
|
map.GetArrayElementAtIndex(i).longValue = id;
|
||
|
|
L(" [" + i + "] " + (rd.rendererFeatures[i] != null ? rd.rendererFeatures[i].name : "(null)") + " localId=" + id);
|
||
|
|
}
|
||
|
|
so.ApplyModifiedPropertiesWithoutUndo();
|
||
|
|
EditorUtility.SetDirty(rd);
|
||
|
|
AssetDatabase.SaveAssets();
|
||
|
|
AssetDatabase.ImportAsset(rdPath, ImportAssetOptions.ForceUpdate);
|
||
|
|
|
||
|
|
var re = AssetDatabase.LoadAssetAtPath<ScriptableRendererData>(rdPath);
|
||
|
|
var so2 = new SerializedObject(re);
|
||
|
|
var m2 = so2.FindProperty("m_RendererFeatureMap");
|
||
|
|
Chk("M 맵 길이", m2.arraySize == re.rendererFeatures.Count, m2.arraySize + " / 피처 " + re.rendererFeatures.Count);
|
||
|
|
bool nulls = false;
|
||
|
|
foreach (var f in re.rendererFeatures) if (f == null) nulls = true;
|
||
|
|
Chk("M null 피처 0", !nulls, nulls ? "있음" : "없음");
|
||
|
|
}
|
||
|
|
catch (Exception ex) { s_fail++; L("EXCEPTION " + ex); }
|
||
|
|
L("");
|
||
|
|
L("RESULT " + (s_fail == 0 ? "PASS" : "FAIL") + " · " + s_pass + "/" + (s_pass + s_fail) + " · FAIL " + s_fail);
|
||
|
|
Write("AgentScripts/WL814c_FEATUREMAP.txt");
|
||
|
|
}
|
||
|
|
|
||
|
|
static string Hex8(long id)
|
||
|
|
{
|
||
|
|
var b = BitConverter.GetBytes(id);
|
||
|
|
var sb = new StringBuilder(16);
|
||
|
|
for (int i = 0; i < 8; i++) sb.Append(b[i].ToString("x2"));
|
||
|
|
return sb.ToString();
|
||
|
|
}
|
||
|
|
}
|