178 lines
11 KiB
C#
178 lines
11 KiB
C#
|
|
// 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<SkinnedMeshRenderer>(true).Where(r => r.enabled && r.gameObject.activeInHierarchy).ToArray();
|
|||
|
|
var scam = Camera.main ?? UnityEngine.Object.FindObjectsByType<Camera>(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<Transform>(true);
|
|||
|
|
var origLayer = new Dictionary<Transform, int>();
|
|||
|
|
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<Camera>(); 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<Vector3, Vector3> 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<Vector3, Vector2> 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);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|