Project_WL/AgentScripts/WL814c_Convert.cs

618 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, isChar);
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, bool isChar)
{
// 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);
// 🔴 캐릭터(스킨드 메시)는 폴리곤이 촘촘해 270px 에서 노멀 외곽선이 노이즈가 된다 → SO 로 따로 끈다.
SetKw(m, "_OUTLINESENABLED", c.outlinesEnabled && (!isChar || c.characterOutlines));
// 구름 그림자
// 🔴 실측: 이 셰이더에 `_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&lt;long&gt;) 을 현재 피처 목록의 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();
}
}