// WL-816b — 🔴 **Play 모드에서만** 재는 무기 측정/촬영 하니스 (814x2~x4 방식 그대로) // 편집 모드 렌더는 스킨 메시가 갱신되지 않아 무의미하다(814x 실측). // run_script --file AgentScripts/WL816b_Play.cs --entry WL816b.Probe (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 WL816b { public const string RAW = "Screenshots_WL/WL816b/raw/"; const int W = 1080, H = 1920; public const float ORTHO = 10f; // 🔴 게임 크기(814x~x4 와 동일 축) const string WDIR = "Assets/Res_Addr/Weapon/"; static readonly StringBuilder sb = new StringBuilder(); static void L(string s) { sb.AppendLine(s); } static GameObject s_root; 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 Transform[] s_sockets; static readonly List s_spawned = new List(); static float s_charHeight; // ─────────────────────────────────────────────────────────── 표: 무기 → 소켓 index (ClassConfig 실측) public static readonly string[][] WEAPONS = new string[][] { // 이름 소켓idx 클래스 new[]{ "PP_Theme_10_Mace_003", "1", "10501 기본" }, new[]{ "PP_Theme_10_Shield_003", "0", "10501 기본" }, new[]{ "Elven_Sword_01", "2", "10101/10102" }, new[]{ "PP_Theme_08_Mace_003", "1", "10502" }, new[]{ "PP_Theme_08_Shield_003", "0", "10502" }, new[]{ "PP_Theme_05_Bow_003", "6", "10402" }, new[]{ "PP_Theme_05_Bow_004", "6", "10401" }, new[]{ "PP_Theme_03_Scepter_001","1", "10303" }, new[]{ "PP_Theme_03_Wand_001", "1", "10302" }, new[]{ "Dagger1", "3", "10201/10202" }, new[]{ "Dagger2", "4", "10201/10202" }, new[]{ "Orb", "5", "10301" }, }; 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; // 🔴 키 = 소켓 아래(무기)를 뺀 몸 렌더러만. 814p `MeasureBodyLocalHeight` 와 같은 정의. var body = s_root.GetComponentsInChildren(true).Where(r => r.enabled && r.gameObject.activeInHierarchy).ToArray(); var b = new Bounds(s_root.transform.position, Vector3.zero); foreach (var r in body) b.Encapsulate(r.bounds); s_center = b.center; s_charHeight = b.size.y; // 소켓 — 이름으로 찾는다(LH_M05 본 계층) string[] names = { "w_shield", "w_righthand", "w_onehand", "w_bladedancer_l", "w_bladedancer_r", "w_orb", "w_archer_l" }; s_sockets = new Transform[names.Length]; foreach (var t in s_root.GetComponentsInChildren(true)) for (int i = 0; i < names.Length; i++) if (s_sockets[i] == null && t.name == names[i]) s_sockets[i] = t; L("root=" + s_root.name + " 키 " + s_charHeight.ToString("F4") + " m · center " + s_center.ToString("F2") + " · rootLossy " + s_root.transform.lossyScale.ToString("F4")); for (int i = 0; i < names.Length; i++) L(" socket[" + i + "] " + names[i] + " = " + (s_sockets[i] == null ? "없음" : s_sockets[i].name + " lossy " + s_sockets[i].lossyScale.ToString("F4"))); L(" renderers " + string.Join(",", body.Select(r => r.name))); var pc = WLCharacterPaletteSettings.Instance; L(" palette SO enabled=" + (pc == null ? "SO없음" : pc.enabled_.ToString()) + " rows=" + (pc == null || pc.rendererNames == null ? 0 : pc.rendererNames.Length)); } static void Teardown() { Clear(); s_cam.transform.SetPositionAndRotation(s_camPos, s_camRot); s_cam.orthographicSize = s_camSize; Time.timeScale = s_ts; } // ────────────────────────────────────────────────────────── 무기 부착 (814p 규칙: 키의 60 % · clamp 0.3~1.0) public static GameObject Spawn(string weapon, int socketIdx, out float ratioBefore, out float mult) { ratioBefore = 0f; mult = 1f; var pf = UnityEditor.AssetDatabase.LoadAssetAtPath(WDIR + weapon + ".prefab"); if (pf == null || s_sockets[socketIdx] == null) return null; var go = UnityEngine.Object.Instantiate(pf, s_sockets[socketIdx]); go.name = weapon + "(Clone)"; // 🔴 실제 게임과 같은 이름(DSUtil.Get_Clone) — 팔레트 표 매칭이 이름 기반이다 go.transform.localPosition = Vector3.zero; go.transform.localRotation = Quaternion.identity; go.transform.localScale = Vector3.one; s_spawned.Add(go); // 길이 = 무기 루트 로컬 공간의 렌더러 localBounds 합 × lossyScale (814p 정의 · 포즈 무관) var rs = go.GetComponentsInChildren(true).Where(r => !(r is ParticleSystemRenderer) && !(r is TrailRenderer) && !(r is LineRenderer)).ToArray(); if (rs.Length == 0) return go; // Orb = 파티클만 → 미보정(814p 미확인 ②) var wb = new Bounds(); bool first = true; foreach (var r in rs) { var lb = r.localBounds; var m = go.transform.worldToLocalMatrix * r.transform.localToWorldMatrix; for (int c = 0; c < 8; c++) { var p = m.MultiplyPoint3x4(lb.center + Vector3.Scale(lb.extents, new Vector3((c & 1) == 0 ? -1 : 1, (c & 2) == 0 ? -1 : 1, (c & 4) == 0 ? -1 : 1))); if (first) { wb = new Bounds(p, Vector3.zero); first = false; } else wb.Encapsulate(p); } } var ls = go.transform.lossyScale; float len = Mathf.Max(wb.size.x * Mathf.Abs(ls.x), Mathf.Max(wb.size.y * Mathf.Abs(ls.y), wb.size.z * Mathf.Abs(ls.z))); ratioBefore = len / Mathf.Max(s_charHeight, 1e-6f); mult = Mathf.Clamp(0.60f / Mathf.Max(ratioBefore, 1e-6f), 0.3f, 1.0f); go.transform.localScale = Vector3.one * mult; return go; } public static void Clear() { foreach (var g in s_spawned) if (g != null) UnityEngine.Object.DestroyImmediate(g); s_spawned.Clear(); } // ────────────────────────────────────────────────────────── 렌더 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) { Directory.CreateDirectory(Path.GetDirectoryName(RAW + name)); var t = new Texture2D(W, H, TextureFormat.RGBA32, false); t.SetPixels32(s.px); t.Apply(); File.WriteAllBytes(RAW + name, t.EncodeToPNG()); UnityEngine.Object.Destroy(t); } static void Shoot(string name, float ortho) { Render(ortho); Save(Render(ortho), name); } /// 지정한 렌더러만 남긴 흑백 마스크(배경·포스트 off · 레이어 31 단독). static bool[] Mask(Renderer[] on, float ortho, string savePng) { var all = UnityEngine.Object.FindObjectsByType(FindObjectsSortMode.None); var was = all.Select(r => r.enabled).ToArray(); var ucd = s_cam.GetComponent(); bool post = ucd != null && ucd.renderPostProcessing; if (ucd != null) ucd.renderPostProcessing = false; int cull = s_cam.cullingMask; var clr = s_cam.clearFlags; var bg = s_cam.backgroundColor; var lay = on.Select(r => r.gameObject.layer).ToArray(); for (int i = 0; i < all.Length; i++) all[i].enabled = on.Contains(all[i]); foreach (var r in on) r.gameObject.layer = 31; s_cam.cullingMask = 1 << 31; s_cam.clearFlags = CameraClearFlags.SolidColor; s_cam.backgroundColor = Color.black; Render(ortho); // 🔴 갓 Instantiate 한 렌더러는 첫 렌더에 안 잡힌다(814x §6-7 과 같은 증상) — 1장 버린다 var sh = Render(ortho); for (int i = 0; i < on.Length; i++) on[i].gameObject.layer = lay[i]; s_cam.cullingMask = cull; s_cam.clearFlags = clr; s_cam.backgroundColor = bg; for (int i = 0; i < all.Length; i++) if (all[i] != null) all[i].enabled = was[i]; if (ucd != null) ucd.renderPostProcessing = post; var m = new bool[sh.px.Length]; for (int i = 0; i < m.Length; i++) m[i] = sh.px[i].r + sh.px[i].g + sh.px[i].b > 24; if (!string.IsNullOrEmpty(savePng)) { var q = new Shot { px = new Color32[m.Length] }; for (int i = 0; i < m.Length; i++) q.px[i] = m[i] ? new Color32(255, 255, 255, 255) : new Color32(0, 0, 0, 255); Save(q, savePng); } return m; } public struct Met { public int px, uniq, steps; public float ratio; public Vector3 mean; } /// 포스트 포함 게임 렌더에서 마스크 영역의 고유색/비율/단계를 잰다. static Met Measure(Shot game, bool[] mask) { var set = new HashSet(); int n = 0; long r = 0, g = 0, b = 0; var cols = new List(); for (int i = 0; i < mask.Length; i++) { if (!mask[i]) continue; var p = game.px[i]; n++; r += p.r; g += p.g; b += p.b; set.Add((p.r << 16) | (p.g << 8) | p.b); cols.Add(p); } var m = new Met { px = n, uniq = set.Count, ratio = n > 0 ? set.Count * 100f / n : 0f }; if (n > 0) m.mean = new Vector3(r / (float)n, g / (float)n, b / (float)n); m.steps = Steps(cols); return m; } /// 단계 = RGB 거리 16 이내로 묶었을 때 면적 90 % 를 덮는 군집 수(814x 정의). static int Steps(List cols) { if (cols.Count == 0) return 0; var cent = new List(); var cnt = new List(); foreach (var c in cols) { var v = new Vector3(c.r, c.g, c.b); int bj = -1; float bd = 16f; for (int j = 0; j < cent.Count; j++) { float d = (cent[j] / cnt[j] - v).magnitude; if (d < bd) { bd = d; bj = j; } } if (bj < 0) { cent.Add(v); cnt.Add(1); } else { cent[bj] += v; cnt[bj]++; } } var order = Enumerable.Range(0, cnt.Count).OrderByDescending(i => cnt[i]).ToArray(); int tot = cols.Count, acc = 0, k = 0; foreach (var i in order) { acc += cnt[i]; k++; if (acc >= tot * 0.90f) break; } return k; } // ────────────────────────────────────────────────────────── 팔레트 적용 (런타임 · 에셋 저장 0) public static Material s_baseMat; static readonly List s_tmp = new List(); /// 구운 결과를 스폰된 무기에 끼운다. 에셋을 만들지 않는다(스윕용). public static void ApplyBaked(GameObject go, Vector3[] pal, Mesh mesh) { if (s_baseMat == null) s_baseMat = UnityEditor.AssetDatabase.LoadAssetAtPath("Assets/WL/Look/Character/Materials/M05_ToonP.mat"); int N = pal.Length, G = Mathf.NextPowerOfTwo(Mathf.CeilToInt(Mathf.Sqrt(N))), CELL = 8, S = G * CELL; var tex = new Texture2D(S, S, TextureFormat.RGBA32, false); // mip 없음 tex.filterMode = FilterMode.Point; tex.wrapMode = TextureWrapMode.Clamp; var px = new Color32[S * S]; for (int i = 0; i < px.Length; i++) px[i] = new Color32(255, 0, 255, 255); for (int j = 0; j < N; j++) { int gx = j % G, gy = j / G; var c = new Color32((byte)Mathf.RoundToInt(Mathf.Clamp01(pal[j].x) * 255f), (byte)Mathf.RoundToInt(Mathf.Clamp01(pal[j].y) * 255f), (byte)Mathf.RoundToInt(Mathf.Clamp01(pal[j].z) * 255f), 255); for (int y = 0; y < CELL; y++) for (int x = 0; x < CELL; x++) px[(gy * CELL + y) * S + gx * CELL + x] = c; } tex.SetPixels32(px); tex.Apply(false, false); var mat = new Material(s_baseMat) { name = "W_ToonP_tmp" }; if (mat.HasProperty("_BaseMap")) mat.SetTexture("_BaseMap", tex); if (mat.HasProperty("_MainTex")) mat.SetTexture("_MainTex", tex); s_tmp.Add(tex); s_tmp.Add(mat); foreach (var mf in go.GetComponentsInChildren(true)) mf.sharedMesh = mesh; foreach (var r in go.GetComponentsInChildren(true)) { if (r is ParticleSystemRenderer) continue; var ms = r.sharedMaterials; for (int i = 0; i < ms.Length; i++) ms[i] = mat; r.sharedMaterials = ms; } } public static void FreeTmp() { foreach (var o in s_tmp) if (o != null) UnityEngine.Object.DestroyImmediate(o); s_tmp.Clear(); } /// 씬의 참조 룩(외곽선·대비)을 새 렌더러까지 포함해 다시 얹는다(멱등). /// 참조 룩 장식을 벗긴다. 🔴 메시·머티리얼을 갈아끼우기 **전에** 반드시 부른다 — /// 안 그러면 다음 Apply 의 Restore 가 예전 머티리얼을 도로 덮어써서 교체가 사라진다. public static void UnLook() { WLReferenceLook.Restore(); foreach (var g in UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None)) if (g != null && g.name == "WL814t_ReferenceLook_Volume") UnityEngine.Object.DestroyImmediate(g); } public static void ReLook() { var cfg = WLReferenceLookSettings.Instance; if (cfg == null || cfg.enabled_ == 0) return; // 🔴 Play 에서 `Destroy` 는 프레임 끝에 실행된다. run_script 는 한 콜백 안에서 끝나므로 // Apply 를 반복하면 임시 Volume 이 겹겹이 살아남아 포스트가 누적된다(캐릭터 수치가 회차마다 나빠진다). // → Restore 로 참조를 끊은 뒤 남은 Volume 을 즉시 지우고 새로 얹는다. WLReferenceLook.Restore(); foreach (var g in UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None)) if (g != null && g.name == "WL814t_ReferenceLook_Volume") UnityEngine.Object.DestroyImmediate(g); WLReferenceLook.Apply(WLReferenceLook.Opt.FromSettings(cfg), cfg); } /// 🔴 「부위(팔레트 칸)당 색 단계」 — 814x 정의. 칸마다 고유색을 찍은 지도(Unlit·빛 0)로 칸을 가른다. public static string CellSteps(GameObject go, Vector3[] pal, out int maxStep, out float avgStep) { maxStep = 0; avgStep = 0f; int N = pal.Length, G = Mathf.NextPowerOfTwo(Mathf.CeilToInt(Mathf.Sqrt(N))), CELL = 8, S = G * CELL; var idTex = new Texture2D(S, S, TextureFormat.RGBA32, false) { filterMode = FilterMode.Point, wrapMode = TextureWrapMode.Clamp }; var px = new Color32[S * S]; var idCol = new Color32[N]; for (int i = 0; i < px.Length; i++) px[i] = new Color32(0, 0, 0, 255); for (int j = 0; j < N; j++) { idCol[j] = new Color32((byte)(20 + j * 13 % 236), (byte)(250 - j * 29 % 200), (byte)(40 + j * 47 % 200), 255); int gx = j % G, gy = j / G; for (int y = 0; y < CELL; y++) for (int x = 0; x < CELL; x++) px[(gy * CELL + y) * S + gx * CELL + x] = idCol[j]; } idTex.SetPixels32(px); idTex.Apply(false, false); var rends = go.GetComponentsInChildren(true).Where(r => !(r is ParticleSystemRenderer)).ToArray(); var keep = rends.Select(r => r.sharedMaterials).ToArray(); var idMat = new Material(Shader.Find("Unlit/Texture")); idMat.mainTexture = idTex; foreach (var r in rends) { var ms = new Material[r.sharedMaterials.Length]; for (int i = 0; i < ms.Length; i++) ms[i] = idMat; r.sharedMaterials = ms; } var idShot = RenderIsolated(rends); for (int i = 0; i < rends.Length; i++) rends[i].sharedMaterials = keep[i]; UnityEngine.Object.DestroyImmediate(idMat); UnityEngine.Object.DestroyImmediate(idTex); var game = Render(ORTHO); var sbb = new StringBuilder(); int sum = 0, cnt = 0; for (int j = 0; j < N; j++) { var cols = new List(); for (int i = 0; i < idShot.px.Length; i++) { var p = idShot.px[i]; if (Mathf.Abs(p.r - idCol[j].r) <= 10 && Mathf.Abs(p.g - idCol[j].g) <= 10 && Mathf.Abs(p.b - idCol[j].b) <= 10) cols.Add(game.px[i]); } if (cols.Count < 3) continue; // 3 px 미만 칸은 통계 의미 없음 int st = Steps(cols); sbb.Append(j + ":" + cols.Count + "px/" + st + "단 "); if (st > maxStep) maxStep = st; sum += st; cnt++; } avgStep = cnt > 0 ? sum / (float)cnt : 0f; return "칸 " + cnt + "개 측정 · 평균 " + avgStep.ToString("F2") + " · 최대 " + maxStep + " · " + sbb; } static Shot RenderIsolated(Renderer[] on) { var all = UnityEngine.Object.FindObjectsByType(FindObjectsSortMode.None); var was = all.Select(r => r.enabled).ToArray(); var ucd = s_cam.GetComponent(); bool post = ucd != null && ucd.renderPostProcessing; if (ucd != null) ucd.renderPostProcessing = false; int cull = s_cam.cullingMask; var clr = s_cam.clearFlags; var bg = s_cam.backgroundColor; var lay = on.Select(r => r.gameObject.layer).ToArray(); for (int i = 0; i < all.Length; i++) all[i].enabled = on.Contains(all[i]); foreach (var r in on) r.gameObject.layer = 31; s_cam.cullingMask = 1 << 31; s_cam.clearFlags = CameraClearFlags.SolidColor; s_cam.backgroundColor = Color.black; Render(ortho: ORTHO); var sh = Render(ORTHO); for (int i = 0; i < on.Length; i++) on[i].gameObject.layer = lay[i]; s_cam.cullingMask = cull; s_cam.clearFlags = clr; s_cam.backgroundColor = bg; for (int i = 0; i < all.Length; i++) if (all[i] != null) all[i].enabled = was[i]; if (ucd != null) ucd.renderPostProcessing = post; return sh; } public static Met MeasureNow(GameObject go, string maskName, string shotName) { var wr = go.GetComponentsInChildren(true).Where(r => r.enabled && r.gameObject.activeInHierarchy && !(r is ParticleSystemRenderer)).ToArray(); var mk = Mask(wr, ORTHO, maskName); var sh = Render(ORTHO); Save(sh, shotName); return Measure(sh, mk); } public static Met MeasureChar(string maskName, string shotName) { var br = s_root.GetComponentsInChildren(true).Where(r => r.enabled && r.gameObject.activeInHierarchy && !r.transform.IsChildOf(s_sockets[0]) && !r.transform.IsChildOf(s_sockets[1]) && !r.transform.IsChildOf(s_sockets[2]) && !r.transform.IsChildOf(s_sockets[3]) && !r.transform.IsChildOf(s_sockets[4]) && !r.transform.IsChildOf(s_sockets[5]) && !r.transform.IsChildOf(s_sockets[6])).ToArray(); var mk = Mask(br, ORTHO, maskName); var sh = Render(ORTHO); if (!string.IsNullOrEmpty(shotName)) Save(sh, shotName); return Measure(sh, mk); } public static void PubSetup() { Setup(); } public static void PubTeardown() { Teardown(); } public static void Log(string s) { L(s); } public static void Flush(string f) { Directory.CreateDirectory("AgentScripts"); File.WriteAllText("AgentScripts/" + f, sb.ToString(), new UTF8Encoding(true)); } public static void Reset() { sb.Length = 0; } public static void ShootGame(string name) { Shoot(name, ORTHO); } // ── 디버그: 마스크가 0 px 로 나온 무기를 캔다 public static void Dbg() { sb.Length = 0; Setup(); foreach (var wn in new[] { "PP_Theme_10_Mace_003", "PP_Theme_08_Mace_003" }) { Clear(); float rb, mult; var go = Spawn(wn, 1, out rb, out mult); var r = go.GetComponentsInChildren(true)[0]; L(wn + " scale=" + go.transform.localScale.ToString("F4") + " lossy=" + go.transform.lossyScale.ToString("F4") + " worldBounds=" + r.bounds.ToString("F3") + " vis=" + r.isVisible + " mat=" + r.sharedMaterial.name + " shader=" + r.sharedMaterial.shader.name + " layer=" + r.gameObject.layer + " pos=" + go.transform.position.ToString("F3")); var mk = Mask(new[] { r }, ORTHO, "dbg_" + wn + ".png"); int n = 0; for (int i = 0; i < mk.Length; i++) if (mk[i]) n++; L(" mask px=" + n); } Clear(); Teardown(); File.WriteAllText("AgentScripts/WL816b_DBG.txt", sb.ToString(), new UTF8Encoding(true)); } // ────────────────────────────────────────────────────────── ① 우선순위 프로브 public static void Probe() { sb.Length = 0; Setup(); L(""); L("=== 무기별 화면 실측 (인게임 Play · 포스트 포함 · 1080x1920 · ortho " + ORTHO.ToString("F0") + ") ==="); var bodyR = s_root.GetComponentsInChildren(true).Where(r => r.enabled && r.gameObject.activeInHierarchy).ToArray(); var mBody = Mask(bodyR, ORTHO, "mask_char.png"); var gm = Render(ORTHO); Save(gm, "a_before_none.png"); var mc = Measure(gm, mBody); L(string.Format("캐릭터(무기 없음) : {0} px · 고유색 {1} · 비율 {2:F2} % · 단계 {3}", mc.px, mc.uniq, mc.ratio, mc.steps)); L(""); L("| 무기 | 소켓 | 클래스 | 배수 | 화면 px | 고유색 | 서로다른색 % | 단계 |"); L("|---|---|---|---|---|---|---|---|"); foreach (var w in WEAPONS) { Clear(); float rb, mult; var go = Spawn(w[0], int.Parse(w[1]), out rb, out mult); if (go == null) { L("| " + w[0] + " | " + w[1] + " | " + w[2] + " | — | 부착실패 | | | |"); continue; } var wr = go.GetComponentsInChildren(true).Where(r => r.enabled && r.gameObject.activeInHierarchy).ToArray(); if (wr.Length == 0) { L("| " + w[0] + " | " + w[1] + " | " + w[2] + " | — | 렌더러 0 | | | |"); continue; } var mk = Mask(wr, ORTHO, "mask_" + w[0] + ".png"); var sh = Render(ORTHO); Save(sh, "a_before_" + w[0] + ".png"); var mm = Measure(sh, mk); L(string.Format("| {0} | {1} | {2} | ×{3:F4} | **{4}** | {5} | {6:F2} | {7} |", w[0], w[1], w[2], mult, mm.px, mm.uniq, mm.ratio, mm.steps)); } Clear(); Teardown(); Directory.CreateDirectory("AgentScripts"); File.WriteAllText("AgentScripts/WL816b_PLAY_PROBE.txt", sb.ToString(), new UTF8Encoding(true)); Debug.Log("[WL816b] play probe done"); } }