Project_WL/AgentScripts/WL814u_Probe.cs

298 lines
16 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─────────────────────────────────────────────────────────────────────────────
// WL814u_Probe.cs — 「도트 일러스트급 캐릭터」 한계 실측 (에디터 전용 · 발주서 WL-814u §1)
//
// 🔴 씬 저장 0 · 프리팹/머티리얼/에셋 수정 0 · ProjectSettings 0.
// 씬은 읽기 전용으로 열고 메모리에서만 만진다(레이어 임시 변경 → 원복 · 저장 안 함).
//
// 측정
// ① 화면에서 캐릭터가 차지하는 픽셀 (현재 카메라 ortho 10 · 1080×1920, 그리고 도트A ortho 3.4 · 135×240)
// — 전체 키 / 머리(Neck_M 위) / 얼굴(face02 데칼의 실제 그려진 영역) / 눈 1개
// ② 부위별 마스크 안에 실제로 찍히는 고유 색 수
// ③ face02 · Hair05 의 UV 점유 영역 → 화면 실효 해상도(텍셀당 화면 픽셀)
// ─────────────────────────────────────────────────────────────────────────────
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_Probe
{
const string SCENE = "Assets/WL/Look/Arena/Scenes/WL_ArenaProto.unity";
const string OUT = "AgentScripts/WL814u_PROBE.txt";
const int LAYER = 31;
// face02.png(2048²) 에서 알파>32 덩어리 5개 (파이썬 실측 · png 좌표 · row0 = 위)
// eyeL x[578..854] y[948..1130] / eyeR x[1190..1466] / browL x[663..827] y[817..923] / browR x[1200..1364] / mouth x[991..1057] y[1420..1433]
const int FACE_TEX = 2048;
const int DRAWN_X0 = 578, DRAWN_X1 = 1466, DRAWN_Y0 = 817, DRAWN_Y1 = 1433; // 889 × 617
const int EYE_W = 277, EYE_H = 183, BROW_W = 165, BROW_H = 107, MOUTH_W = 67, MOUTH_H = 14;
static readonly StringBuilder sb = new StringBuilder();
static void L(string s) { sb.AppendLine(s); Console.WriteLine("[814u] " + 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));
Console.WriteLine("[814u] wrote " + OUT);
}
}
// ── 유틸 ────────────────────────────────────────────────────────────────
static GameObject FindRoot()
{
foreach (var go in UnityEngine.SceneManagement.SceneManager.GetActiveScene().GetRootGameObjects())
if (go.name.Contains("LH_M05") || go.name.StartsWith("PC_")) return go;
return null;
}
class Shot
{
public Color32[] px; public int w, h;
public Color32 At(int x, int y) { return px[y * w + x]; }
}
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 prevT = cam.targetTexture; var prevActive = RenderTexture.active;
var prevCF = cam.clearFlags; var prevBG = 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 = prevT; RenderTexture.active = prevActive;
cam.clearFlags = prevCF; cam.backgroundColor = prevBG;
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 count)
{
var b = Render(cam, w, h, Color.black);
var wht = Render(cam, w, h, Color.white);
var m = new bool[w * h];
x0 = w; y0 = h; x1 = -1; y1 = -1; count = 0;
for (int y = 0; y < h; y++)
for (int x = 0; x < w; x++)
{
int i = y * w + x;
int d = Mathf.Max(Mathf.Abs(b.px[i].r - wht.px[i].r), Mathf.Max(Mathf.Abs(b.px[i].g - wht.px[i].g), Mathf.Abs(b.px[i].b - wht.px[i].b)));
if (d < 128) { m[i] = true; count++; if (x < x0) x0 = x; if (x > x1) x1 = x; if (y < y0) y0 = y; if (y > y1) y1 = y; }
}
return m;
}
static int UniqueColors(Shot s, bool[] m)
{
var set = new HashSet<int>();
for (int i = 0; i < m.Length; i++) if (m[i]) set.Add((s.px[i].r << 16) | (s.px[i].g << 8) | s.px[i].b);
return set.Count;
}
// ── 본체 ────────────────────────────────────────────────────────────────
static void Body()
{
EditorSceneManager.OpenScene(SCENE, OpenSceneMode.Single);
L("scene = " + SCENE);
var root = FindRoot();
if (root == null) { L("!! PC root not found"); return; }
L("root = " + root.name + " pos=" + root.transform.position.ToString("F4") + " rot=" + root.transform.eulerAngles.ToString("F2") + " scale=" + root.transform.localScale.ToString("F4"));
var smrs = root.GetComponentsInChildren<SkinnedMeshRenderer>(true).Where(r => r.enabled && r.gameObject.activeInHierarchy).ToArray();
L("");
L("== SMR 전수 ==");
foreach (var r in smrs)
{
var mat = r.sharedMaterial;
var tex = mat != null && mat.HasProperty("_BaseMap") ? mat.GetTexture("_BaseMap") : (mat != null ? mat.mainTexture : null);
L(string.Format(" {0} | sub={1} | mat={2} | shader={3} | tex={4} {5}", r.name, r.sharedMesh != null ? r.sharedMesh.subMeshCount : -1,
mat != null ? mat.name : "null", mat != null ? mat.shader.name : "null",
tex != null ? tex.name : "null", tex != null ? ("(" + tex.width + "x" + tex.height + ")") : ""));
}
// 서브메시 인덱스 = SMR 컴포넌트가 mesh 의 어느 서브메시를 그리는가 (Suriyun 리그는 SMR 1개 = 서브메시 1개)
// face / hair / body / skin 구분
Func<SkinnedMeshRenderer, string> texName = r =>
{
var m = r.sharedMaterial; if (m == null) return "";
var t = m.HasProperty("_BaseMap") ? m.GetTexture("_BaseMap") : m.mainTexture;
return t != null ? t.name.ToLowerInvariant() : m.name.ToLowerInvariant();
};
var faceR = smrs.Where(r => texName(r).Contains("face")).ToArray();
var hairR = smrs.Where(r => texName(r).Contains("hair")).ToArray();
var skinR = smrs.Where(r => texName(r).Contains("skin")).ToArray();
var bodyR = smrs.Where(r => texName(r).Contains("m05")).ToArray();
L(string.Format(" 분류: face={0} hair={1} skin={2} body={3}", faceR.Length, hairR.Length, skinR.Length, bodyR.Length));
// 본
L("");
L("== 본(머리 관련) ==");
var bones = root.GetComponentsInChildren<Transform>(true);
Transform neck = null, head = null;
foreach (var b in bones)
{
if (b.name == "Neck_M" || b.name == "Head_M" || b.name == "HeadEnd_M" || b.name.Contains("Jaw") || b.name.StartsWith("Eye"))
{
L(" " + b.name + " world=" + b.position.ToString("F4"));
if (b.name == "Neck_M") neck = b;
if (b.name == "Head_M") head = b;
}
}
// UV 점유 (변형 없는 sharedMesh 의 UV — UV 는 스키닝으로 안 변한다)
L("");
L("== UV 점유 (텍스처 2048 기준) ==");
foreach (var r in smrs)
{
var mesh = r.sharedMesh; if (mesh == null) continue;
var uv = mesh.uv; if (uv == null || uv.Length == 0) continue;
// 이 SMR 이 그리는 서브메시만
var used = new HashSet<int>();
for (int s = 0; s < mesh.subMeshCount; s++) { }
var tris = mesh.triangles; // 전체 (SMR 이 서브메시 1개만 그려도 mesh 는 공유 · 아래에서 서브메시별로 다시 계산)
for (int s = 0; s < mesh.subMeshCount; s++)
{
var t2 = mesh.GetTriangles(s);
float u0 = 9, u1 = -9, v0 = 9, v1 = -9;
foreach (var i in t2) { 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; }
L(string.Format(" {0} sub{1}: u[{2:F4}..{3:F4}] v[{4:F4}..{5:F4}] → {6:F0} × {7:F0} 텍셀 (tri {8})",
r.name, s, u0, u1, v0, v1, (u1 - u0) * 2048, (v1 - v0) * 2048, t2.Length / 3));
}
break; // 메시는 6 SMR 이 공유 — 한 번만 출력
}
// 카메라
Camera scam = Camera.main;
if (scam == null) scam = UnityEngine.Object.FindObjectsByType<Camera>(FindObjectsInactive.Exclude, FindObjectsSortMode.None).FirstOrDefault(c => c.orthographic);
if (scam == null) { L("!! camera not found"); return; }
L("");
L("== 씬 카메라 == " + scam.name + " ortho=" + scam.orthographic + " size=" + scam.orthographicSize +
" pos=" + scam.transform.position.ToString("F4") + " euler=" + scam.transform.eulerAngles.ToString("F3"));
// 레이어 임시 변경 (씬 저장 안 함)
var origLayer = new Dictionary<Transform, int>();
foreach (var t in bones) { origLayer[t] = t.gameObject.layer; t.gameObject.layer = LAYER; }
try
{
Measure("A. 현재 카메라 (ortho 10 · 1080×1920)", scam, 10f, 1080, 1920, root, smrs, faceR, hairR, skinR, bodyR, neck);
Measure("B. 도트A (ortho 3.4 · 135×240)", scam, 3.4f, 135, 240, root, smrs, faceR, hairR, skinR, bodyR, neck);
}
finally
{
foreach (var kv in origLayer) if (kv.Key != null) kv.Key.gameObject.layer = kv.Value;
}
}
static void Measure(string title, Camera scam, float ortho, int W, int H, GameObject root,
SkinnedMeshRenderer[] all, SkinnedMeshRenderer[] faceR, SkinnedMeshRenderer[] hairR,
SkinnedMeshRenderer[] skinR, SkinnedMeshRenderer[] bodyR, Transform neck)
{
L("");
L("════════ " + title + " ════════");
// 캐릭터만 찍는 임시 카메라
var go = new GameObject("WL814u_Cam");
var cam = go.AddComponent<Camera>();
cam.CopyFrom(scam);
cam.orthographic = true; cam.orthographicSize = ortho;
cam.cullingMask = 1 << LAYER;
cam.targetTexture = null;
cam.transform.SetPositionAndRotation(scam.transform.position, scam.transform.rotation);
var ucd = cam.GetUniversalAdditionalCameraData();
if (ucd != null) { ucd.renderPostProcessing = false; ucd.renderShadows = true; ucd.SetRenderer(0); }
// 캐릭터를 화면 중앙에 (직교라 시선방향 이동은 화면을 안 바꾼다 → 좌우/상하만 맞춘다)
var b = new Bounds(root.transform.position, Vector3.zero);
foreach (var r in all) b.Encapsulate(r.bounds);
var center = b.center;
cam.transform.position = center - cam.transform.forward * 50f;
// 전체 씬(배경 포함) 컬러 — 부위 색 수 세기용
var fullCam = new GameObject("WL814u_FullCam").AddComponent<Camera>();
fullCam.CopyFrom(scam);
fullCam.orthographic = true; fullCam.orthographicSize = ortho;
fullCam.transform.SetPositionAndRotation(cam.transform.position, cam.transform.rotation);
fullCam.targetTexture = null;
var fd = fullCam.GetUniversalAdditionalCameraData(); if (fd != null) { fd.renderPostProcessing = false; fd.SetRenderer(0); }
var full = Render(fullCam, W, H, Color.black);
float pxPerM = H / (2f * ortho);
L(string.Format(" 화면 {0}×{1} · ortho {2} · 1 m = {3:F3} px · 1 px = {4:F5} m", W, H, ortho, pxPerM, 1f / pxPerM));
// 전체 실루엣
int x0, y0, x1, y1, cnt;
SetOnly(all, all);
var mAll = Mask(cam, W, H, out x0, out y0, out x1, out y1, out cnt);
int hAll = y1 - y0 + 1, wAll = x1 - x0 + 1;
L(string.Format(" 🔴 캐릭터 전체 실루엣: 키 {0} px · 폭 {1} px · 면적 {2} px (bbox x[{3}..{4}] y[{5}..{6}])", hAll, wAll, cnt, x0, x1, y0, y1));
L(string.Format(" 화면 세로의 {0:F1} % · 고유 색 {1}", 100f * hAll / H, UniqueColors(full, mAll)));
// 머리 = Neck_M 스크린 y 위쪽
if (neck != null)
{
var sp = cam.WorldToScreenPoint(neck.position);
int neckY = Mathf.RoundToInt(sp.y);
int headPx = y1 - neckY + 1;
// 머리 폭 = 목 위쪽 행들의 최대 폭
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; }
L(string.Format(" 🔴 머리(Neck_M {0} px ~ 정수리 {1} px): 높이 {2} px · 폭 {3} px · 면적 {4} px · 머리:키 = 1:{5:F2}",
neckY, y1, headPx, hx1 - hx0 + 1, hcnt, (float)hAll / Mathf.Max(1, headPx)));
}
// 부위별
foreach (var pair in new[] {
new KeyValuePair<string, SkinnedMeshRenderer[]>("face02(얼굴 데칼)", faceR),
new KeyValuePair<string, SkinnedMeshRenderer[]>("Hair05(머리칼)", hairR),
new KeyValuePair<string, SkinnedMeshRenderer[]>("skin(피부)", skinR),
new KeyValuePair<string, SkinnedMeshRenderer[]>("M05(몸/갑옷)", bodyR) })
{
if (pair.Value.Length == 0) continue;
SetOnly(all, pair.Value);
int fx0, fy0, fx1, fy1, fc;
var m = Mask(cam, W, H, out fx0, out fy0, out fx1, out fy1, out fc);
if (fc == 0) { L(" " + pair.Key + ": 화면에 0 px (가려짐)"); continue; }
L(string.Format(" {0}: {1} × {2} px · 면적 {3} px · 고유 색 {4} (bbox x[{5}..{6}] y[{7}..{8}])",
pair.Key, fx1 - fx0 + 1, fy1 - fy0 + 1, fc, UniqueColors(full, m), fx0, fx1, fy0, fy1));
if (pair.Value == faceR)
{
float sx = (float)(fx1 - fx0 + 1) / (DRAWN_X1 - DRAWN_X0 + 1);
float sy = (float)(fy1 - fy0 + 1) / (DRAWN_Y1 - DRAWN_Y0 + 1);
L(string.Format(" 🔴 얼굴 실효 해상도: 텍셀 1개 = 화면 {0:F4} × {1:F4} px (= 텍스처 {2}×{3} 텍셀이 화면 {4}×{5} px 로 찍힌다)",
sx, sy, DRAWN_X1 - DRAWN_X0 + 1, DRAWN_Y1 - DRAWN_Y0 + 1, fx1 - fx0 + 1, fy1 - fy0 + 1));
L(string.Format(" 🔴 눈 1개 = 화면 {0:F2} × {1:F2} px · 눈썹 1개 = {2:F2} × {3:F2} px · 입 = {4:F2} × {5:F2} px",
EYE_W * sx, EYE_H * sy, BROW_W * sx, BROW_H * sy, MOUTH_W * sx, MOUTH_H * sy));
L(string.Format(" 텍스처 2048 기준 축소율 = 1/{0:F0} (가로) · 1/{1:F0} (세로)", 1f / sx, 1f / sy));
}
}
SetOnly(all, all);
UnityEngine.Object.DestroyImmediate(go);
UnityEngine.Object.DestroyImmediate(fullCam.gameObject);
}
static void SetOnly(SkinnedMeshRenderer[] all, SkinnedMeshRenderer[] on)
{
var set = new HashSet<SkinnedMeshRenderer>(on);
foreach (var r in all) r.enabled = set.Contains(r);
}
}