Project_WL/AgentScripts/WL814u_Probe2.cs

173 lines
9.8 KiB
C#
Raw Permalink Normal View History

// 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<SkinnedMeshRenderer>(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<int>(); // 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<Transform>(true).FirstOrDefault(t => t.name == "Neck_M");
Camera scam = Camera.main ?? UnityEngine.Object.FindObjectsByType<Camera>(FindObjectsInactive.Exclude, FindObjectsSortMode.None).FirstOrDefault(c => c.orthographic);
var all = root.GetComponentsInChildren<Transform>(true);
var orig = new Dictionary<Transform, int>();
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<Camera>();
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<float>(); var colch = new List<float>();
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);
}
}