// WL814u_Probe2.cs — §1 보정 실측 + UV 점유 + 픽셀 떨림(pixel crawl) 실험 (에디터 전용) // 🔴 씬 저장 0 · 에셋 수정 0. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.Rendering.Universal; public static class WL814u_Probe2 { const string SCENE = "Assets/WL/Look/Arena/Scenes/WL_ArenaProto.unity"; const string OUT = "AgentScripts/WL814u_PROBE2.txt"; const int LAYER = 31; static readonly StringBuilder sb = new StringBuilder(); static void L(string s) { sb.AppendLine(s); Console.WriteLine("[814u2] " + s); } public static void Run() { try { Body(); } catch (Exception e) { L("EXCEPTION " + e); } finally { Directory.CreateDirectory(Path.GetDirectoryName(OUT)); File.WriteAllText(OUT, sb.ToString(), new UTF8Encoding(true)); } } class Shot { public Color32[] px; public int w, h; } static Shot Render(Camera cam, int w, int h, Color bg) { var rt = new RenderTexture(w, h, 24, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB); rt.antiAliasing = 1; rt.filterMode = FilterMode.Point; var pT = cam.targetTexture; var pA = RenderTexture.active; var pC = cam.clearFlags; var pB = cam.backgroundColor; cam.clearFlags = CameraClearFlags.SolidColor; cam.backgroundColor = bg; cam.targetTexture = rt; cam.Render(); RenderTexture.active = rt; var t = new Texture2D(w, h, TextureFormat.RGBA32, false); t.ReadPixels(new Rect(0, 0, w, h), 0, 0); t.Apply(); cam.targetTexture = pT; RenderTexture.active = pA; cam.clearFlags = pC; cam.backgroundColor = pB; var s = new Shot { px = t.GetPixels32(), w = w, h = h }; UnityEngine.Object.DestroyImmediate(t); rt.Release(); UnityEngine.Object.DestroyImmediate(rt); return s; } static bool[] Mask(Camera cam, int w, int h, out int x0, out int y0, out int x1, out int y1, out int cnt, out Shot color) { var b = Render(cam, w, h, Color.black); var wt = Render(cam, w, h, Color.white); color = b; var m = new bool[w * h]; x0 = w; y0 = h; x1 = -1; y1 = -1; cnt = 0; for (int i = 0; i < m.Length; i++) { int d = Mathf.Max(Mathf.Abs(b.px[i].r - wt.px[i].r), Mathf.Max(Mathf.Abs(b.px[i].g - wt.px[i].g), Mathf.Abs(b.px[i].b - wt.px[i].b))); if (d < 128) { m[i] = true; cnt++; int x = i % w, y = i / w; if (x < x0) x0 = x; if (x > x1) x1 = x; if (y < y0) y0 = y; if (y > y1) y1 = y; } } return m; } static void Body() { EditorSceneManager.OpenScene(SCENE, OpenSceneMode.Single); GameObject root = null; foreach (var go in UnityEngine.SceneManagement.SceneManager.GetActiveScene().GetRootGameObjects()) if (go.name.Contains("LH_M05")) { root = go; break; } if (root == null) { L("!! root"); return; } var smrs = root.GetComponentsInChildren(true).Where(r => r.enabled && r.gameObject.activeInHierarchy).ToArray(); // ── UV 점유 (렌더러별 · 자기 메시) ───────────────────────────── L("== UV 점유 (렌더러별 · 텍스처 2048 기준) =="); foreach (var r in smrs) { var mesh = r.sharedMesh; if (mesh == null) continue; var uv = mesh.uv; var tri = mesh.triangles; float u0 = 9, u1 = -9, v0 = 9, v1 = -9; var cells = new HashSet(); // 64텍셀 격자 점유 칸 수 foreach (var i in tri) { var q = uv[i]; if (q.x < u0) u0 = q.x; if (q.x > u1) u1 = q.x; if (q.y < v0) v0 = q.y; if (q.y > v1) v1 = q.y; cells.Add(Mathf.Clamp((int)(q.x * 32), 0, 31) * 32 + Mathf.Clamp((int)(q.y * 32), 0, 31)); } L(string.Format(" {0}: u[{1:F4}..{2:F4}] v[{3:F4}..{4:F4}] → {5:F0}×{6:F0} 텍셀 · 64격자 점유 {7}/1024칸 · verts {8} tri {9}", r.name, u0, u1, v0, v1, (u1 - u0) * 2048, (v1 - v0) * 2048, cells.Count, mesh.vertexCount, tri.Length / 3)); } Transform neck = root.GetComponentsInChildren(true).FirstOrDefault(t => t.name == "Neck_M"); Camera scam = Camera.main ?? UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None).FirstOrDefault(c => c.orthographic); var all = root.GetComponentsInChildren(true); var orig = new Dictionary(); foreach (var t in all) { orig[t] = t.gameObject.layer; t.gameObject.layer = LAYER; } var faceR = smrs.Where(r => r.name == "Face").ToArray(); try { Do("A. 현재 (ortho 10 · 1080×1920)", scam, 10f, 1080, 1920, root, smrs, faceR, neck); Do("B. 도트A (ortho 3.4 · 135×240)", scam, 3.4f, 135, 240, root, smrs, faceR, neck); } finally { foreach (var kv in orig) if (kv.Key != null) kv.Key.gameObject.layer = kv.Value; } } static void Do(string title, Camera scam, float ortho, int W, int H, GameObject root, SkinnedMeshRenderer[] smrs, SkinnedMeshRenderer[] faceR, Transform neck) { L(""); L("════════ " + title + " ════════"); var go = new GameObject("WL814u_Cam2"); var cam = go.AddComponent(); cam.CopyFrom(scam); cam.orthographic = true; cam.orthographicSize = ortho; cam.cullingMask = 1 << LAYER; cam.targetTexture = null; cam.aspect = (float)W / H; // 🔴 해상도 고정 (1차 probe 의 머리 측정 버그 원인) cam.transform.SetPositionAndRotation(scam.transform.position, scam.transform.rotation); var ucd = cam.GetUniversalAdditionalCameraData(); if (ucd != null) { ucd.renderPostProcessing = false; ucd.SetRenderer(0); } var b = new Bounds(root.transform.position, Vector3.zero); foreach (var r in smrs) b.Encapsulate(r.bounds); cam.transform.position = b.center - cam.transform.forward * 50f; int x0, y0, x1, y1, cnt; Shot col; foreach (var r in smrs) r.enabled = true; var mAll = Mask(cam, W, H, out x0, out y0, out x1, out y1, out cnt, out col); L(string.Format(" 전체: 키 {0} px · 폭 {1} px · 면적 {2} px (y[{3}..{4}])", y1 - y0 + 1, x1 - x0 + 1, cnt, y0, y1)); // 머리 = Neck_M 위 (뷰포트 기준 · 해상도 무관) var vp = cam.WorldToViewportPoint(neck.position); int neckY = Mathf.RoundToInt(vp.y * H); int hx0 = W, hx1 = -1, hcnt = 0; for (int y = Mathf.Max(0, neckY); y <= y1; y++) for (int x = 0; x < W; x++) if (mAll[y * W + x]) { hcnt++; if (x < hx0) hx0 = x; if (x > hx1) hx1 = x; } int headPx = y1 - neckY + 1; L(string.Format(" 🔴 머리(Neck_M y={0} → 정수리 y={1}): 높이 {2} px · 폭 {3} px · 면적 {4} px · 머리:키 = 1:{5:F2}", neckY, y1, headPx, hx1 - hx0 + 1, hcnt, (float)(y1 - y0 + 1) / Mathf.Max(1, headPx))); // 얼굴 데칼 foreach (var r in smrs) r.enabled = faceR.Contains(r); int fx0, fy0, fx1, fy1, fc; Shot fcol; var mF = Mask(cam, W, H, out fx0, out fy0, out fx1, out fy1, out fc, out fcol); if (fc > 0) { L(string.Format(" 🔴 얼굴 데칼(이목구비 전부): {0} × {1} px · 면적 {2} px", fx1 - fx0 + 1, fy1 - fy0 + 1, fc)); L(string.Format(" 🔴 눈~턱(얼굴 데칼 위 {0} → 턱=Neck_M {1}): {2} px", fy1, neckY, fy1 - neckY + 1)); float sx = (fx1 - fx0 + 1) / 889f, sy = (fy1 - fy0 + 1) / 617f; L(string.Format(" 눈 1개 = {0:F2} × {1:F2} px · 눈썹 {2:F2} × {3:F2} px · 입 {4:F2} × {5:F2} px · 텍셀 1개 = {6:F4} px", 277 * sx, 183 * sy, 165 * sx, 107 * sy, 67 * sx, 14 * sy, sx)); } else L(" 얼굴 데칼: 0 px"); foreach (var r in smrs) r.enabled = true; // ── 픽셀 떨림 실험: 캐릭터를 0.5° 씩 돌리며 실루엣/색이 몇 % 바뀌나 ── L(" ── 픽셀 떨림(pixel crawl) 실험 · 캐릭터 yaw 를 0.5° 씩 16회 ──"); var y0rot = root.transform.eulerAngles.y; bool[] prevM = null; Shot prevC = null; var flips = new List(); var colch = new List(); for (int i = 0; i <= 16; i++) { root.transform.rotation = Quaternion.Euler(0, y0rot + i * 0.5f, 0); int a0, a1, a2, a3, ac; Shot c; var m = Mask(cam, W, H, out a0, out a2, out a1, out a3, out ac, out c); if (prevM != null) { int flip = 0, chg = 0, both = 0; for (int k = 0; k < m.Length; k++) { if (m[k] != prevM[k]) flip++; if (m[k] && prevM[k]) { both++; if (Mathf.Abs(c.px[k].r - prevC.px[k].r) + Mathf.Abs(c.px[k].g - prevC.px[k].g) + Mathf.Abs(c.px[k].b - prevC.px[k].b) > 24) chg++; } } flips.Add(100f * flip / Mathf.Max(1, ac)); colch.Add(100f * chg / Mathf.Max(1, both)); } prevM = m; prevC = c; } root.transform.rotation = Quaternion.Euler(0, y0rot, 0); L(string.Format(" 0.5° 회전마다 실루엣 픽셀의 {0:F1} % 가 켜지거나 꺼진다 (최소 {1:F1} · 최대 {2:F1})", flips.Average(), flips.Min(), flips.Max())); L(string.Format(" 0.5° 회전마다 남아 있는 픽셀의 {0:F1} % 가 색이 바뀐다(Δ>24) (최소 {1:F1} · 최대 {2:F1})", colch.Average(), colch.Min(), colch.Max())); L(" → 1° 당 실루엣 " + (flips.Average() * 2).ToString("F1") + " % · 색 " + (colch.Average() * 2).ToString("F1") + " %"); UnityEngine.Object.DestroyImmediate(go); } }