// ───────────────────────────────────────────────────────────────────────────── // WL814s_Capture.cs — WL_ArenaProto 씬에서 같은 카메라·같은 포즈로 단계별 캡처 (발주 WL-814s §4) // // Play 중에 호출한다. 4개 상태(원본 · ①셰이더 · ①+③축소 · ①+③+②평탄화)를 // **같은 프레임·같은 포즈**에서 연속으로 렌더한다 → 비교가 공정하다. // 🔴 씬·프리팹·머티리얼 파일에 아무 것도 쓰지 않는다(런타임 sharedMaterials 참조만 바꾼다). // ───────────────────────────────────────────────────────────────────────────── using System; using System.Collections.Generic; using System.IO; using System.Text; using UnityEngine; using WL.Look.Character; public static class WL814s_Capture { public const int W = 1080, H = 1920; static string OutDir() { string d = Path.GetFullPath(Path.Combine(Application.dataPath, "../Screenshots_WL/WL814s")); Directory.CreateDirectory(d); return d; } static GameObject FindPc() { var all = UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None); foreach (var a in all) { if (a.GetComponentInChildren(true) != null) return a.gameObject; } return null; } static void FreezePose(GameObject pc) { var an = pc != null ? pc.GetComponent() : null; if (an == null) return; an.fireEvents = false; an.speed = 0f; an.Play("idle", 0, 0.25f); an.Update(0f); } static void Shot(Camera cam, string file) { var rt = new RenderTexture(W, H, 24, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB); rt.antiAliasing = 1; var prevT = cam.targetTexture; var prevActive = RenderTexture.active; cam.targetTexture = rt; cam.Render(); RenderTexture.active = rt; var tex = new Texture2D(W, H, TextureFormat.RGB24, false); tex.ReadPixels(new Rect(0, 0, W, H), 0, 0); tex.Apply(false); File.WriteAllBytes(file, tex.EncodeToPNG()); cam.targetTexture = prevT; RenderTexture.active = prevActive; UnityEngine.Object.DestroyImmediate(tex); rt.Release(); UnityEngine.Object.DestroyImmediate(rt); } /// args = ["set", "", "", "", …] · 각 모드를 같은 포즈로 찍는다. public static string Cmd(string[] args) { try { var sb = new StringBuilder(); var cam = Camera.main; if (cam == null) { var cams = UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None); if (cams.Length > 0) cam = cams[0]; } if (cam == null) return "NO CAMERA"; var pc = FindPc(); if (pc == null) return "NO PC"; FreezePose(pc); string dir = OutDir(); sb.AppendLine("cam=" + cam.name + " ortho=" + cam.orthographic + " size=" + cam.orthographicSize + " pos=" + cam.transform.position.ToString("F4") + " euler=" + cam.transform.eulerAngles.ToString("F3")); sb.AppendLine("pc=" + pc.name + " pos=" + pc.transform.position.ToString("F4")); string cmd = args.Length > 0 ? args[0] : "set"; if (cmd == "set") { string prefix = args[1]; for (int i = 2; i < args.Length; i++) { int mode = int.Parse(args[i]); WLCharacterLook.ApplyMode(pc, mode, null); FreezePose(pc); string f = Path.Combine(dir, prefix + "_m" + mode + ".png"); Shot(cam, f); sb.AppendLine("SHOT mode=" + mode + " -> " + f + " · " + WLCharacterLook.LastLog + " · " + DumpMats(pc)); } } else if (cmd == "face") { // 얼굴 확대 — 같은 각도, 머리 중심, 작은 ortho size string prefix = args[1]; // 얼굴이 카메라를 보게 돌린다(이 샷 전용 · Play 중이라 파일에 남지 않는다) var fwd = -cam.transform.forward; fwd.y = 0f; if (fwd.sqrMagnitude > 0.0001f) pc.transform.rotation = Quaternion.LookRotation(fwd.normalized, Vector3.up); FreezePose(pc); var head = FindHead(pc); var go = new GameObject("WL814s_FaceCam"); var fc = go.AddComponent(); fc.CopyFrom(cam); fc.orthographic = true; fc.orthographicSize = float.Parse(args[2]); go.transform.rotation = cam.transform.rotation; go.transform.position = head - cam.transform.forward * 20f; fc.targetTexture = null; for (int i = 3; i < args.Length; i++) { int mode = int.Parse(args[i]); WLCharacterLook.ApplyMode(pc, mode, null); FreezePose(pc); string f = Path.Combine(dir, prefix + "_m" + mode + ".png"); Shot(fc, f); sb.AppendLine("FACE mode=" + mode + " -> " + f); } sb.AppendLine("head=" + head.ToString("F4") + " size=" + fc.orthographicSize); UnityEngine.Object.DestroyImmediate(go); } else if (cmd == "suffix") { // suffix … · Materials/.mat 을 직접 물린다 // (같은 Play 세션 안에서 찍으므로 구름 그림자·잔디까지 같은 조건 = 공정한 비교) string prefix = args[1]; for (int i = 2; i < args.Length; i++) { ApplySuffix(pc, args[i]); FreezePose(pc); string f = Path.Combine(dir, prefix + "_" + args[i].Replace("_", "") + ".png"); Shot(cam, f); sb.AppendLine("SHOT " + args[i] + " -> " + f + " · " + DumpMats(pc)); } } else if (cmd == "suffixface") { string prefix = args[1]; var fwd2 = -cam.transform.forward; fwd2.y = 0f; if (fwd2.sqrMagnitude > 0.0001f) pc.transform.rotation = Quaternion.LookRotation(fwd2.normalized, Vector3.up); FreezePose(pc); var head2 = FindHead(pc); var go2 = new GameObject("WL814s_FaceCam2"); var fc2 = go2.AddComponent(); fc2.CopyFrom(cam); fc2.orthographic = true; fc2.orthographicSize = float.Parse(args[2]); go2.transform.rotation = cam.transform.rotation; go2.transform.position = head2 - cam.transform.forward * 20f; for (int i = 3; i < args.Length; i++) { ApplySuffix(pc, args[i]); FreezePose(pc); Shot(fc2, Path.Combine(dir, prefix + "_" + args[i].Replace("_", "") + ".png")); sb.AppendLine("FACE " + args[i]); } UnityEngine.Object.DestroyImmediate(go2); } else if (cmd == "dumponly") { // 아무 것도 적용하지 않고 지금 상태만 읽는다 (Awake 가 한 일을 그대로 본다) var c0 = WLCharacterLookSettings.Instance; sb.AppendLine("SO enabled_=" + (c0 != null ? c0.enabled_ : -1) + " mode=" + (c0 != null ? c0.mode : -1) + " ActiveMode=" + WLCharacterLookSettings.ActiveMode + " · Awake 적용내역=" + WLCharacterLook.LastLog); sb.AppendLine("RESULT " + DumpMats(pc)); } else if (cmd == "probe") { for (int i = 1; i < args.Length; i++) { int mode = int.Parse(args[i]); WLCharacterLook.ApplyMode(pc, mode, null); sb.AppendLine("MODE " + mode + " · " + DumpMats(pc)); } } else if (cmd == "switch") { // SO 가 지시하는 대로 (enabled_ 실측용) WLCharacterLookSettings.Invalidate(); WLCharacterLook.Apply(pc); sb.AppendLine("SO enabled_=" + (WLCharacterLookSettings.Instance != null ? WLCharacterLookSettings.Instance.enabled_ : -1) + " mode=" + (WLCharacterLookSettings.Instance != null ? WLCharacterLookSettings.Instance.mode : -1) + " ActiveMode=" + WLCharacterLookSettings.ActiveMode); sb.AppendLine("RESULT " + DumpMats(pc)); } return sb.ToString(); } catch (Exception e) { return "EXCEPTION " + e.GetType().Name + " " + e.Message + "\n" + e.StackTrace; } } // 비교 전용 — Materials/.mat 을 렌더러에 직접 물린다("_orig" 면 원본으로) static readonly string[] KEYS = { "M05", "Skin", "Hair05", "face02" }; static void ApplySuffix(GameObject pc, string suffix) { var cfg = WLCharacterLookSettings.Instance; if (cfg == null) return; if (suffix == "_orig") { WLCharacterLook.ApplyMode(pc, 0, cfg); return; } var want = new Material[4]; for (int i = 0; i < 4; i++) { var m = UnityEditor.AssetDatabase.LoadAssetAtPath( "Assets/WL/Look/Character/Materials/" + KEYS[i] + suffix + ".mat"); want[i] = m != null ? m : cfg.originals[i]; } foreach (var r in pc.GetComponentsInChildren(true)) { var ms = r.sharedMaterials; bool ch = false; for (int s = 0; s < ms.Length; s++) { int idx = -1; for (int i = 0; i < 4 && idx < 0; i++) { if (cfg.originals[i] == ms[s] || cfg.mode1[i] == ms[s] || cfg.mode2[i] == ms[s] || cfg.mode3[i] == ms[s]) idx = i; else if (ms[s] != null && ms[s].name.StartsWith(KEYS[i])) idx = i; } if (idx < 0 || want[idx] == null || ms[s] == want[idx]) continue; ms[s] = want[idx]; ch = true; } if (ch) r.sharedMaterials = ms; } } static Vector3 FindHead(GameObject pc) { // 얼굴 머티리얼을 쓰는 렌더러의 바운즈 중심 var rs = pc.GetComponentsInChildren(true); foreach (var r in rs) { var ms = r.sharedMaterials; foreach (var m in ms) if (m != null && m.name.StartsWith("face")) return r.bounds.center; } var b = new Bounds(pc.transform.position, Vector3.zero); foreach (var r in rs) if (r.enabled) b.Encapsulate(r.bounds); return new Vector3(b.center.x, b.max.y - b.size.y * 0.12f, b.center.z); } static string DumpMats(GameObject pc) { var sb = new StringBuilder(); var rs = pc.GetComponentsInChildren(true); foreach (var r in rs) { var ms = r.sharedMaterials; for (int i = 0; i < ms.Length; i++) sb.Append(r.name + "[" + i + "]=" + (ms[i] == null ? "null" : ms[i].name + "/" + ms[i].shader.name) + "; "); } return sb.ToString(); } }