// WL814v_Probe.cs — §1-1 부위별 「텍셀 → 화면 픽셀」 밀도 실측 (에디터 전용) // 🔴 씬 저장 0 · 에셋 수정 0 · ProjectSettings 0. 카메라·포즈는 814u 와 동일(ortho 10 · 1080×1920 · 캐릭터를 카메라 정면으로). 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 WL814v_Probe { const string SCENE = "Assets/WL/Look/Arena/Scenes/WL_ArenaProto.unity"; const string OUT = "AgentScripts/WL814v_PROBE.txt"; const int LAYER = 31; const int W = 1080, H = 1920; static readonly StringBuilder sb = new StringBuilder(); static void L(string s) { sb.AppendLine(s); Console.WriteLine("[814vP] " + s); } class Shot { public Color32[] px; public int w, h; } static Shot Render(Camera cam, Color bg) { var rt = new RenderTexture(W, H, 24, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB); rt.antiAliasing = 1; rt.filterMode = FilterMode.Point; 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 = null; 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; } // 검정/흰 배경 2장 차분 = 실루엣 마스크 (반투명·안티앨리어싱 무시) static bool[] Mask(Camera cam, out int x0, out int y0, out int x1, out int y1, out int cnt) { var b = Render(cam, Color.black); var wt = Render(cam, Color.white); 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; } 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)); } } 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(); var scam = Camera.main ?? UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None).First(c => c.orthographic); float camYaw = scam.transform.eulerAngles.y; root.transform.rotation = Quaternion.Euler(0, camYaw + 180f, 0); // 814u 캡처와 동일 포즈 L(string.Format("포즈 = 카메라 정면 yaw {0:F0}° · 카메라 ortho 10 · {1}×{2} (814u 동일)", camYaw + 180f, W, H)); var all = root.GetComponentsInChildren(true); var origLayer = new Dictionary(); foreach (var t in all) { origLayer[t] = t.gameObject.layer; t.gameObject.layer = LAYER; } var b = new Bounds(root.transform.position, Vector3.zero); foreach (var r in smrs) b.Encapsulate(r.bounds); var go2 = new GameObject("WL814v_Cam"); var cam = go2.AddComponent(); cam.CopyFrom(scam); cam.orthographic = true; cam.orthographicSize = 10f; cam.aspect = (float)W / H; cam.targetTexture = null; cam.cullingMask = 1 << LAYER; cam.transform.SetPositionAndRotation(b.center - scam.transform.forward * 50f, scam.transform.rotation); var ucd = cam.GetUniversalAdditionalCameraData(); if (ucd != null) { ucd.renderPostProcessing = false; ucd.SetRenderer(0); } for (int i = 0; i < 3; i++) Render(cam, Color.black); // 배치 워밍업 try { int x0, y0, x1, y1, cntAll; foreach (var r in smrs) r.enabled = true; Mask(cam, out x0, out y0, out x1, out y1, out cntAll); L(string.Format("전체 실루엣: 키 {0} px · 폭 {1} px · 면적 {2} px", y1 - y0 + 1, x1 - x0 + 1, cntAll)); L(""); L("== 부위별 「텍셀 → 화면 픽셀」 밀도 (앞면 삼각형만 · 화면면적 가중) =="); L(" (UV 면적은 실효 텍스처 512² 기준 · 괄호는 원본 2048² 기준)"); foreach (var target in smrs) { // 이 렌더러만 켜서 실제로 그려지는 화면 픽셀 수(가림 반영) foreach (var r in smrs) r.enabled = (r == target); int a0, b0, a1, b1, cnt; Mask(cam, out a0, out b0, out a1, out b1, out cnt); var mesh = new Mesh(); target.BakeMesh(mesh, true); // useScale = true → 스케일 포함 var verts = mesh.vertices; var uvs = mesh.uv; var tris = mesh.triangles; var mNoScale = Matrix4x4.TRS(target.transform.position, target.transform.rotation, Vector3.one); var mFull = target.transform.localToWorldMatrix; // 🔴 cam.WorldToScreenPoint 는 GameView 크기를 쓴다(배치 = 엉뚱한 값) → VP 행렬로 직접 1080×1920 으로 투영 var VP = cam.projectionMatrix * cam.worldToCameraMatrix; System.Func Proj = (wp) => { var c = VP * new Vector4(wp.x, wp.y, wp.z, 1f); if (Mathf.Abs(c.w) > 1e-9f) { c.x /= c.w; c.y /= c.w; c.z /= c.w; } return new Vector3((c.x * 0.5f + 0.5f) * W, (c.y * 0.5f + 0.5f) * H, c.z); }; double bestScore = double.MaxValue; string bestTag = "?"; double dens = 0, sArea = 0, uArea = 0; int nFront = 0; foreach (var mode in new[] { 0, 1 }) { var M = (mode == 0) ? mNoScale : mFull; double sA = 0, uA = 0; int nf = 0, nb = 0; double sAb = 0, uAb = 0; float px0 = 1e9f, py0 = 1e9f, px1 = -1e9f, py1 = -1e9f; for (int i = 0; i < tris.Length; i += 3) { Vector3 p0 = Proj(M.MultiplyPoint3x4(verts[tris[i]])); Vector3 p1 = Proj(M.MultiplyPoint3x4(verts[tris[i + 1]])); Vector3 p2 = Proj(M.MultiplyPoint3x4(verts[tris[i + 2]])); float cross = (p1.x - p0.x) * (p2.y - p0.y) - (p2.x - p0.x) * (p1.y - p0.y); px0 = Mathf.Min(px0, Mathf.Min(p0.x, Mathf.Min(p1.x, p2.x))); px1 = Mathf.Max(px1, Mathf.Max(p0.x, Mathf.Max(p1.x, p2.x))); py0 = Mathf.Min(py0, Mathf.Min(p0.y, Mathf.Min(p1.y, p2.y))); py1 = Mathf.Max(py1, Mathf.Max(p0.y, Mathf.Max(p1.y, p2.y))); Vector2 q0 = uvs[tris[i]], q1 = uvs[tris[i + 1]], q2 = uvs[tris[i + 2]]; float ucross = Mathf.Abs((q1.x - q0.x) * (q2.y - q0.y) - (q2.x - q0.x) * (q1.y - q0.y)); if (cross > 0f) { sA += cross * 0.5; uA += ucross * 0.5 * 512.0 * 512.0; nf++; } else { sAb += -cross * 0.5; uAb += ucross * 0.5 * 512.0 * 512.0; nb++; } } // 앞면 winding 자동 판별: 그려진 화면 px 수에 더 가까운 쪽 if (Math.Abs(sAb - cnt) < Math.Abs(sA - cnt)) { sA = sAb; uA = uAb; nf = nb; } // 검증: 투영 bbox 가 실제 렌더 bbox 와 얼마나 맞나 double score = Math.Abs(px0 - a0) + Math.Abs(px1 - a1) + Math.Abs(py0 - b0) + Math.Abs(py1 - b1); if (score < bestScore) { bestScore = score; bestTag = (mode == 0 ? "TRS(무스케일)" : "localToWorld"); sArea = sA; uArea = uA; nFront = nf; dens = (sA > 0 ? uA / sA : 0); } } UnityEngine.Object.DestroyImmediate(mesh); L(string.Format(" {0,-9}: 그려진 화면 {1,5} px · 앞면삼각 {2,4}개 · 앞면 화면면적 {3,7:F0} px² · UV면적 {4,9:F0} 텍셀²", target.name, cnt, nFront, sArea, uArea)); L(string.Format(" → 화면 1 px = {0:F0} 텍셀(512²) = {1:F0} 텍셀(2048²) · 🔴 격자 1변 = {2:F1} 텍셀(512²) = {3:F1} 텍셀(2048²) · 투영검증 오차 {4:F0} px [{5}]", dens, dens * 16.0, Math.Sqrt(dens), Math.Sqrt(dens) * 4.0, bestScore, bestTag)); // ── U/V 축별 이방성 (면적 가중) ── { var VP2 = cam.projectionMatrix * cam.worldToCameraMatrix; var M2 = (bestTag == "TRS(무스케일)") ? mNoScale : mFull; System.Func P2 = (wp) => { var c = VP2 * new Vector4(wp.x, wp.y, wp.z, 1f); if (Mathf.Abs(c.w) > 1e-9f) { c.x /= c.w; c.y /= c.w; } return new Vector2((c.x * 0.5f + 0.5f) * W, (c.y * 0.5f + 0.5f) * H); }; double wu = 0, wv = 0, wsum = 0; for (int i = 0; i < tris.Length; i += 3) { Vector2 p0 = P2(M2.MultiplyPoint3x4(verts[tris[i]])), p1 = P2(M2.MultiplyPoint3x4(verts[tris[i + 1]])), p2 = P2(M2.MultiplyPoint3x4(verts[tris[i + 2]])); Vector2 q0 = uvs[tris[i]], q1 = uvs[tris[i + 1]], q2 = uvs[tris[i + 2]]; Vector2 e1 = q1 - q0, e2 = q2 - q0; Vector2 f1 = p1 - p0, f2 = p2 - p0; float det = e1.x * e2.y - e2.x * e1.y; if (Mathf.Abs(det) < 1e-12f) continue; Vector2 dPdu = (f1 * e2.y - f2 * e1.y) / det, dPdv = (f2 * e1.x - f1 * e2.x) / det; double ar = Math.Abs(f1.x * f2.y - f2.x * f1.y) * 0.5; wu += ar * dPdu.magnitude / 512.0; wv += ar * dPdv.magnitude / 512.0; wsum += ar; } if (wsum > 0) L(string.Format(" ↳ 이방성: u 1텍셀(512²) = {0:F3} 화면 px · v 1텍셀 = {1:F3} px → 화면 1 px = u {2:F1} × v {3:F1} 텍셀(512²) = u {4:F0} × v {5:F0} 텍셀(2048²)", wu / wsum, wv / wsum, wsum / wu, wsum / wv, 4.0 * wsum / wu, 4.0 * wsum / wv)); } } foreach (var r in smrs) r.enabled = true; } finally { foreach (var kv in origLayer) if (kv.Key != null) kv.Key.gameObject.layer = kv.Value; UnityEngine.Object.DestroyImmediate(go2); } } }