// WL-814x2 — 🔴 **Play 모드에서만** 재는 색 측정/촬영 하니스 // 편집 모드 렌더는 스킨 메시가 갱신되지 않아 무의미하다(814x 에서 실측). // run_script --file AgentScripts/WL814x2_Play.cs --entry WL814x2.Run (Play 중에 부른다) using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using UnityEngine; using WL.Look.Arena; using WL.Look.Character; public static class WL814x2 { public const string RAW = "Screenshots_WL/WL814x/2nd/raw/"; const int W = 1080, H = 1920; static readonly StringBuilder sb = new StringBuilder(); static void L(string s) { sb.AppendLine(s); } static GameObject s_root; static Renderer[] s_rends; static Material[][] s_orig; static Camera s_cam; static Vector3 s_camPos, s_center, s_pos; static Quaternion s_rot, s_camRot; static Animator s_an; static float s_camSize, s_ts; static void Setup() { s_root = GameObject.Find("PC_LH_M05"); if (s_root == null) foreach (var g in UnityEngine.Object.FindObjectsByType(FindObjectsSortMode.None)) if (g.name.Contains("LH_M05")) { s_root = g.gameObject; break; } // 🔴 아레나에는 캐릭터를 걷게 하는 리그가 있다 — 샷마다 위치/포즈가 달라지면 색 비교가 무의미하다. s_ts = Time.timeScale; Time.timeScale = 0f; s_an = s_root.GetComponentInChildren(); if (s_an != null) { s_an.speed = 0f; try { s_an.Play("idle", 0, 0.25f); s_an.Update(0f); } catch { } } s_cam = Camera.main ?? UnityEngine.Object.FindObjectsByType(FindObjectsSortMode.None).First(c => c.orthographic); s_camPos = s_cam.transform.position; s_camSize = s_cam.orthographicSize; s_camRot = s_cam.transform.rotation; s_root.transform.rotation = Quaternion.Euler(0, s_cam.transform.eulerAngles.y + 180f, 0); s_pos = s_root.transform.position; s_rot = s_root.transform.rotation; s_rends = s_root.GetComponentsInChildren(true).Where(r => r.enabled && r.gameObject.activeInHierarchy).ToArray(); s_orig = s_rends.Select(r => r.sharedMaterials).ToArray(); var b = new Bounds(s_root.transform.position, Vector3.zero); foreach (var r in s_rends) b.Encapsulate(r.bounds); s_center = b.center; L("renderers " + string.Join(",", s_rends.Select(r => r.name)) + " · center " + s_center.ToString("F2") + " · h " + b.size.y.ToString("F3")); foreach (var r in s_rends) { var m = r.sharedMaterial; L(" " + r.name + " mat=" + m.name + " shader=" + m.shader.name + " base=" + (m.HasProperty("_BaseMap") && m.GetTexture("_BaseMap") ? m.GetTexture("_BaseMap").name : "-") + " diff=" + (m.HasProperty("_DiffuseColor") ? m.GetColor("_DiffuseColor").ToString("F3") : "-") + " shadow=" + (m.HasProperty("_ShadowDiffuseColor") ? m.GetColor("_ShadowDiffuseColor").ToString("F4") : "-") + " shades=" + (m.HasProperty("_Shades") ? m.GetFloat("_Shades").ToString("F0") : "-") + " minDark=" + (m.HasProperty("_MinimumDarkness") ? m.GetFloat("_MinimumDarkness").ToString("F2") : "-")); } var rc = WLReferenceLookSettings.Instance; L("ReferenceLook: rimOn=" + rc.rimOn + " moodOn=" + rc.moodOn + " mood=" + rc.moodStrength.ToString("F2") + " contrast=" + rc.contrastOn + " minDark=" + rc.minimumDarkness.ToString("F2") + " shadowScale=" + rc.shadowColorScale.ToString("F2") + " shadowTintAmt=" + rc.shadowTintAmount.ToString("F2") + " shadowTint=" + rc.shadowTint.ToString("F3") + " charShades=" + rc.charShades + " key=" + rc.keyLightColor.ToString("F2") + "×" + rc.keyLightIntensity.ToString("F2") + " amb=" + rc.ambientColor.ToString("F2")); } static void Teardown() { for (int i = 0; i < s_rends.Length; i++) { s_rends[i].sharedMaterials = s_orig[i]; s_rends[i].enabled = true; } s_cam.transform.SetPositionAndRotation(s_camPos, s_camRot); s_cam.orthographicSize = s_camSize; Time.timeScale = s_ts; } static int[] s_layers; static int s_cull; static CameraClearFlags s_clear; static Color s_bg; static bool s_post, s_postSaved; /// 캐릭터만 남기고 배경·그림자·포스트를 모두 끈 렌더 상태(마스크·덩어리지도 전용). static void Isolate(bool on) { var ts = s_root.GetComponentsInChildren(true); var ucd = s_cam.GetComponent(); if (on) { s_layers = ts.Select(t => t.gameObject.layer).ToArray(); foreach (var t in ts) t.gameObject.layer = 31; s_cull = s_cam.cullingMask; s_clear = s_cam.clearFlags; s_bg = s_cam.backgroundColor; s_cam.cullingMask = 1 << 31; s_cam.clearFlags = CameraClearFlags.SolidColor; s_cam.backgroundColor = Color.black; if (ucd != null) { s_post = ucd.renderPostProcessing; s_postSaved = true; ucd.renderPostProcessing = false; } } else { for (int i = 0; i < ts.Length && i < s_layers.Length; i++) ts[i].gameObject.layer = s_layers[i]; s_cam.cullingMask = s_cull; s_cam.clearFlags = s_clear; s_cam.backgroundColor = s_bg; if (ucd != null && s_postSaved) { ucd.renderPostProcessing = s_post; s_postSaved = false; } } } class Shot { public Color32[] px; } static Shot Render(float ortho) { // 매 렌더마다 같은 위치·같은 포즈로 고정한다 s_root.transform.SetPositionAndRotation(s_pos, s_rot); if (s_an != null) { s_an.speed = 0f; try { s_an.Play("idle", 0, 0.25f); s_an.Update(0f); } catch { } } // 🔴 카메라도 매 프레임 따라다니는 스크립트가 있다 — 회전까지 고정해야 프레이밍이 같다. s_cam.orthographicSize = ortho; s_cam.transform.SetPositionAndRotation(s_center - (s_camRot * Vector3.forward) * 30f, s_camRot); var rt = new RenderTexture(W, H, 24, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB); rt.antiAliasing = 1; rt.filterMode = FilterMode.Point; rt.Create(); var prev = s_cam.targetTexture; s_cam.targetTexture = rt; s_cam.Render(); s_cam.targetTexture = prev; var pa = RenderTexture.active; RenderTexture.active = rt; var t = new Texture2D(W, H, TextureFormat.RGBA32, false); t.ReadPixels(new Rect(0, 0, W, H), 0, 0); t.Apply(); RenderTexture.active = pa; var s = new Shot { px = t.GetPixels32() }; UnityEngine.Object.Destroy(t); rt.Release(); UnityEngine.Object.Destroy(rt); return s; } static void Save(Shot s, string name) { var t = new Texture2D(W, H, TextureFormat.RGBA32, false); t.SetPixels32(s.px); t.Apply(); Directory.CreateDirectory(RAW); File.WriteAllBytes(RAW + name + ".png", t.EncodeToPNG()); UnityEngine.Object.Destroy(t); } static void Shoot(string name, float ortho) { Render(ortho); Save(Render(ortho), name); } static void SaveMask(string name, float ortho, params Renderer[] on) { Isolate(true); foreach (var r in s_rends) r.enabled = on.Contains(r); var a = Render(ortho); foreach (var r in s_rends) r.enabled = true; Isolate(false); var t = new Texture2D(W, H, TextureFormat.RGBA32, false); var px = new Color32[W * H]; int n = 0; for (int i = 0; i < px.Length; i++) { int d = a.px[i].r + a.px[i].g + a.px[i].b; bool hit = d > 12; if (hit) n++; px[i] = hit ? new Color32(255, 255, 255, 255) : new Color32(0, 0, 0, 255); } t.SetPixels32(px); t.Apply(); Directory.CreateDirectory(RAW); File.WriteAllBytes(RAW + "mask_" + name + ".png", t.EncodeToPNG()); UnityEngine.Object.Destroy(t); L("mask " + name + " @" + ortho.ToString("F2") + " = " + n + " px"); } /// 덩어리(팔레트 칸) 지도 — 빛 0. 부위별 색을 재기 위한 마스크. static void ShootIdMap(string name, float ortho) { var pal = UnityEditor.AssetDatabase.LoadAssetAtPath("Assets/WL/Look/Character/Textures/WLPalette_FINAL.png"); int S = pal.width, G = S / 8; var t = new Texture2D(S, S, TextureFormat.RGBA32, false) { filterMode = FilterMode.Point, wrapMode = TextureWrapMode.Clamp }; var px = new Color32[S * S]; for (int i = 0; i < px.Length; i++) px[i] = new Color32(0, 0, 0, 255); for (int j = 0; j < G * G; j++) { int gx = j % G, gy = j / G; var c = new Color32((byte)(9 + j * 15), (byte)(240 - j * 11), (byte)(40 + j * 7), 255); for (int y = 0; y < 8; y++) for (int x = 0; x < 8; x++) px[(gy * 8 + y) * S + gx * 8 + x] = c; } t.SetPixels32(px); t.Apply(); Isolate(true); var sh = Shader.Find("Unlit/Texture"); var keep = new List(); for (int i = 0; i < s_rends.Length; i++) { if (s_rends[i].name == "Face") { s_rends[i].enabled = false; keep.Add(s_rends[i]); continue; } var m = new Material(sh); m.SetTexture("_MainTex", t); s_rends[i].sharedMaterials = Enumerable.Repeat(m, s_orig[i].Length).ToArray(); } Shoot(name, ortho); foreach (var r in keep) r.enabled = true; for (int i = 0; i < s_rends.Length; i++) s_rends[i].sharedMaterials = s_orig[i]; Isolate(false); UnityEngine.Object.Destroy(t); // 팔레트 칸 색도 적어 둔다(인덱스 → 팔레트 RGB) var cols = pal.GetPixels32(); for (int j = 0; j < G * G; j++) { int gx = j % G, gy = j / G; var c = cols[(gy * 8 + 4) * S + gx * 8 + 4]; L(string.Format("PAL {0,2} id={1},{2},{3} rgb={4},{5},{6}", j, 9 + j * 15, 240 - j * 11, 40 + j * 7, c.r, c.g, c.b)); } } /// ToonP 머티리얼의 그림자색만 바꾼 임시 복제본으로 교체. static void SetShadow(Color shadow, float shades) { for (int i = 0; i < s_rends.Length; i++) { var mats = (Material[])s_orig[i].Clone(); for (int m = 0; m < mats.Length; m++) { if (mats[m] == null || mats[m].shader == null || mats[m].shader.name != "Shader Graphs/Toon") continue; var c = new Material(mats[m]) { name = "TMP_" + mats[m].name }; c.SetColor("_ShadowDiffuseColor", shadow); if (shades > 0) c.SetFloat("_Shades", shades); mats[m] = c; } s_rends[i].sharedMaterials = mats; } } static void ResetMats() { for (int i = 0; i < s_rends.Length; i++) s_rends[i].sharedMaterials = s_orig[i]; } static void Look(float mood, float shadowTintAmount, int rim) { var cfg = WLReferenceLookSettings.Instance; var o = WLReferenceLook.Opt.FromSettings(cfg); o.mood = mood > 0f; o.moodStrength = mood; o.shadowTintAmount = shadowTintAmount; o.rim = rim != 0; WLReferenceLook.Apply(o, cfg); } public static string Run() { sb.Length = 0; try { Setup(); SaveMask("all", 0.95f, s_rends); foreach (var r in s_rends) SaveMask(r.name, 0.95f, r); SaveMask("all10", 10f, s_rends); foreach (var r in s_rends) SaveMask(r.name + "10", 10f, r); ShootIdMap("idmap", 0.95f); ShootIdMap("idmap10", 10f); var cfg = WLReferenceLookSettings.Instance; float mood0 = cfg.moodStrength, tint0 = cfg.shadowTintAmount; Action V = (name, shadow, shades, mood, tintAmt, rim) => { ResetMats(); WLReferenceLook.Restore(); if (shadow.a >= 0f && shadow != Color.clear) SetShadow(shadow, shades); Look(mood, tintAmt, rim); Shoot(name, 0.95f); Shoot(name + "_g", 10f); L("shot " + name + " shadow=" + shadow.ToString("F3") + " shades=" + shades + " mood=" + mood.ToString("F2") + " tintAmt=" + tintAmt.ToString("F2") + " · " + WLReferenceLook.LastLog); }; var purple = new Color(0.2164f, 0.2092f, 0.3713f, 0f); V("a_now", purple, 3f, mood0, tint0, 0); V("b_neutral", new Color(0.45f, 0.45f, 0.45f, 0f), 3f, mood0, tint0, 0); V("c_neutral_notint", new Color(0.45f, 0.45f, 0.45f, 0f), 3f, mood0, 0f, 0); V("d_neutral_notint_mood0", new Color(0.45f, 0.45f, 0.45f, 0f), 3f, 0f, 0f, 0); V("e_warm_notint", new Color(0.52f, 0.45f, 0.40f, 0f), 3f, mood0, 0f, 0); V("f_neutral_notint_s7", new Color(0.45f, 0.45f, 0.45f, 0f), 7f, mood0, 0f, 0); V("g_dark_notint", new Color(0.32f, 0.32f, 0.34f, 0f), 3f, mood0, 0f, 0); ResetMats(); WLReferenceLook.Restore(); Look(mood0, tint0, 0); L("DONE"); } catch (Exception e) { L("EXCEPTION " + e); } finally { try { Teardown(); } catch { } Directory.CreateDirectory("AgentScripts"); File.WriteAllText("AgentScripts/WL814x2_PLAY.txt", sb.ToString(), new UTF8Encoding(true)); } return sb.ToString(); } /// 아레나 씬을 연다(Play 진입 전 · 편집 모드에서 1회). public static string Open() { UnityEditor.SceneManagement.EditorSceneManager.OpenScene("Assets/WL/Look/Arena/Scenes/WL_ArenaProto.unity", UnityEditor.SceneManagement.OpenSceneMode.Single); return "opened " + UnityEngine.SceneManagement.SceneManager.GetActiveScene().name; } /// 무드(포스트 그레이딩·앰비언트·키라이트)가 화면 색을 얼마나 틀어놓는지 — 에셋 수정 0(런타임 프로파일 인스턴스). public static string Mood() { sb.Length = 0; try { Setup(); SaveMask("all", 0.95f, s_rends); foreach (var r in s_rends) SaveMask(r.name, 0.95f, r); SaveMask("all10", 10f, s_rends); ShootIdMap("idmap", 0.95f); var cfg = WLReferenceLookSettings.Instance; Action V = (name, mood, killWB, killSMH) => { WLReferenceLook.Restore(); var o = WLReferenceLook.Opt.FromSettings(cfg); o.rim = false; o.mood = mood > 0f; o.moodStrength = mood; WLReferenceLook.Apply(o, cfg); var vgo = GameObject.Find("WL814t_ReferenceLook_Volume"); // killWB = 포스트 그레이딩 Volume 의 weight 만 0 으로 (조명 무드는 그대로 둔다) if (vgo != null && killWB) vgo.GetComponent().weight = 0f; Shoot(name, 0.95f); Shoot(name + "_g", 10f); L("mood shot " + name + " mood=" + mood.ToString("F2") + " killWB=" + killWB + " killSMH=" + killSMH); }; V("m30", 0.30f, false, false); V("m15", 0.15f, false, false); V("m00", 0.00f, false, false); V("n_novol30", 0.30f, true, false); V("n_novol15", 0.15f, true, false); WLReferenceLook.Restore(); var o2 = WLReferenceLook.Opt.FromSettings(cfg); o2.rim = false; WLReferenceLook.Apply(o2, cfg); L("DONE"); } catch (Exception e) { L("EXCEPTION " + e); } finally { try { Teardown(); } catch { } File.WriteAllText("AgentScripts/WL814x2_MOOD.txt", sb.ToString(), new UTF8Encoding(true)); } return sb.ToString(); } /// 🔴 캐릭터 전용 광원 색조 상쇄 — _DiffuseColor 에 휘도보존 역이득을 넣는다. /// 씬·조명·814t 설정·ProjectSettings 는 하나도 안 건드린다. public static string SetTint() { var g = new Color(0.994f, 1.024f, 0.894f, 1f); var sb2 = new StringBuilder(); foreach (var n in new[] { "M05_ToonP", "Skin_ToonP", "Hair05_ToonP" }) { var p = "Assets/WL/Look/Character/Materials/" + n + ".mat"; var m = UnityEditor.AssetDatabase.LoadAssetAtPath(p); if (m == null) { sb2.AppendLine("!! " + p); continue; } m.SetColor("_DiffuseColor", g); UnityEditor.EditorUtility.SetDirty(m); sb2.AppendLine(n + " _DiffuseColor = " + g.ToString("F3")); } UnityEditor.AssetDatabase.SaveAssets(); return sb2.ToString(); } }