Project_WL/AgentScripts/WL814u_Crawl.cs

404 lines
22 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_Crawl.cs — 「3D 를 유지한 채 픽셀 떨림을 줄이는 대책」 실측 (에디터 전용 · 발주서 WL-814u 범위변경 ⓓ)
// 🔴 씬 저장 0 · 에셋 수정 0 · ProjectSettings 0.
//
// 떨림 수치 정의 (근거를 남긴다)
// 두 연속 프레임의 캐릭터 실루엣을 각자의 bbox 왼쪽아래로 맞춘 뒤(= 이동분을 뺀 뒤) 비교한다.
// shapeFlip % = XOR(실루엣A, 실루엣B) / 두 면적 평균 ← 「모양이 몇 % 바뀌었나」
// colorΔ % = 둘 다 덮인 픽셀 중 색 차이>24 인 비율 ← 「색이 몇 % 흔들렸나」
// 이동분을 빼기 때문에 「캐릭터가 지나간다」는 정상 움직임은 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_Crawl
{
const string SCENE = "Assets/WL/Look/Arena/Scenes/WL_ArenaProto.unity";
const string SHOT = "Screenshots_WL/WL814u/";
const string OUT = "AgentScripts/WL814u_CRAWL.txt";
const int LAYER = 31;
static readonly StringBuilder sb = new StringBuilder();
static void L(string s) { sb.AppendLine(s); Console.WriteLine("[814uX] " + s); }
class Shot { public Color32[] px; public int w, h; }
class Frame { public bool[] m; public Shot c; public int x0, y0, x1, y1, area; }
static GameObject root; static SkinnedMeshRenderer[] smrs; static Camera scam;
static Camera cam, mcam;
static Shot Render(Camera c, int w, int h)
{
var rt = new RenderTexture(w, h, 24, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
rt.antiAliasing = 1; rt.filterMode = FilterMode.Point;
var pA = RenderTexture.active; c.targetTexture = rt; c.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();
c.targetTexture = null; RenderTexture.active = pA;
var s = new Shot { px = t.GetPixels32(), w = w, h = h };
UnityEngine.Object.DestroyImmediate(t); rt.Release(); UnityEngine.Object.DestroyImmediate(rt);
return s;
}
static Frame Grab(int w, int h)
{
mcam.backgroundColor = Color.black; var a = Render(mcam, w, h);
mcam.backgroundColor = Color.white; var b = Render(mcam, w, h);
var f = new Frame { m = new bool[w * h], c = a, x0 = w, y0 = h, x1 = -1, y1 = -1 };
for (int i = 0; i < f.m.Length; i++)
{
int d = Mathf.Max(Mathf.Abs(a.px[i].r - b.px[i].r), Mathf.Max(Mathf.Abs(a.px[i].g - b.px[i].g), Mathf.Abs(a.px[i].b - b.px[i].b)));
if (d < 128) { f.m[i] = true; f.area++; int x = i % w, y = i / w; if (x < f.x0) f.x0 = x; if (x > f.x1) f.x1 = x; if (y < f.y0) f.y0 = y; if (y > f.y1) f.y1 = y; }
}
return f;
}
// bbox 왼쪽아래 기준으로 맞춘 뒤 비교
static void Compare(Frame A, Frame B, int w, int h, out float shapeFlip, out float colorD)
{
int dx = B.x0 - A.x0, dy = B.y0 - A.y0;
int xor = 0, both = 0, chg = 0;
for (int y = A.y0; y <= A.y1; y++)
for (int x = A.x0; x <= A.x1; x++)
{
int bx = x + dx, by = y + dy;
bool ma = A.m[y * w + x];
bool mb = (bx >= 0 && by >= 0 && bx < w && by < h) && B.m[by * w + bx];
if (ma != mb) xor++;
else if (ma)
{
both++;
var ca = A.c.px[y * w + x]; var cb = B.c.px[by * w + bx];
if (Mathf.Abs(ca.r - cb.r) + Mathf.Abs(ca.g - cb.g) + Mathf.Abs(ca.b - cb.b) > 24) chg++;
}
}
shapeFlip = 200f * xor / Mathf.Max(1, A.area + B.area);
colorD = 100f * chg / Mathf.Max(1, both);
}
static bool[] Dilate(bool[] m, int w, int h, int n)
{
var cur = (bool[])m.Clone();
for (int k = 0; k < n; k++)
{
var nx = (bool[])cur.Clone();
for (int y = 0; y < h; y++)
for (int x = 0; x < w; x++)
if (cur[y * w + x])
{
if (x > 0) nx[y * w + x - 1] = true; if (x < w - 1) nx[y * w + x + 1] = true;
if (y > 0) nx[(y - 1) * w + x] = true; if (y < h - 1) nx[(y + 1) * w + x] = true;
}
cur = nx;
}
return cur;
}
static void Setup(float ortho, int w, int h)
{
if (cam != null) UnityEngine.Object.DestroyImmediate(cam.gameObject);
if (mcam != null) UnityEngine.Object.DestroyImmediate(mcam.gameObject);
var b = new Bounds(root.transform.position, Vector3.zero); foreach (var r in smrs) b.Encapsulate(r.bounds);
cam = new GameObject("WL814u_X").AddComponent<Camera>(); cam.CopyFrom(scam);
cam.orthographic = true; cam.orthographicSize = ortho; cam.aspect = (float)w / h; cam.targetTexture = null;
cam.transform.SetPositionAndRotation(b.center - scam.transform.forward * 50f, scam.transform.rotation);
var d1 = cam.GetUniversalAdditionalCameraData(); if (d1 != null) { d1.renderPostProcessing = false; d1.SetRenderer(0); }
mcam = new GameObject("WL814u_XM").AddComponent<Camera>(); mcam.CopyFrom(cam);
mcam.cullingMask = 1 << LAYER; mcam.clearFlags = CameraClearFlags.SolidColor; mcam.aspect = (float)w / h;
var d2 = mcam.GetUniversalAdditionalCameraData(); if (d2 != null) { d2.renderPostProcessing = false; d2.SetRenderer(0); }
for (int i = 0; i < 3; i++) Render(mcam, w, h); // 워밍업(첫 렌더는 텍스처 미로드)
}
// PixelCameraManager.PositionToGrid 와 같은 식: 카메라 축 기준 픽셀 격자에 반올림
static Vector3 ToGrid(Vector3 p, float pixelWorld)
{
var t = cam.transform;
float rx = Vector3.Dot(p, t.right), ry = Vector3.Dot(p, t.up), rz = Vector3.Dot(p, t.forward);
rx = Mathf.Round(rx / pixelWorld) * pixelWorld; ry = Mathf.Round(ry / pixelWorld) * pixelWorld; rz = Mathf.Round(rz / pixelWorld) * pixelWorld;
return rx * t.right + ry * t.up + rz * t.forward;
}
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)); }
}
// ⓓ-2 재측정 — 1차는 8° 만 돌려서 16방향이 경계를 한 번도 안 넘었다(0.00 % 는 측정 구간 탓).
// 이번엔 90° 를 돌려 모든 양자화가 경계를 여러 번 넘게 한다.
public static void Run2()
{
try
{
EditorSceneManager.OpenScene(SCENE, OpenSceneMode.Single);
foreach (var go in UnityEngine.SceneManagement.SceneManager.GetActiveScene().GetRootGameObjects())
if (go.name.Contains("LH_M05")) root = go;
smrs = root.GetComponentsInChildren<SkinnedMeshRenderer>(true).Where(r => r.enabled).ToArray();
scam = Camera.main ?? UnityEngine.Object.FindObjectsByType<Camera>(FindObjectsInactive.Exclude, FindObjectsSortMode.None).First(c => c.orthographic);
var allT = root.GetComponentsInChildren<Transform>(true);
var orig = new Dictionary<Transform, int>(); foreach (var t in allT) { orig[t] = t.gameObject.layer; t.gameObject.layer = LAYER; }
var baseRot = root.transform.rotation;
float yaw0 = scam.transform.eulerAngles.y + 180f;
Setup(3.4f, 135, 240);
const int N = 91; const float STEP = 1.0f; // 1°/프레임 × 90 = 90° 회전
L("");
L("== ⓓ-2(재측정) 회전 계단화 · 90° 회전 (1°/프레임 × 90) · 도트A 240줄 ==");
L(" | 방향 수 | 각도 간격 | 떨림 0 인 프레임 | 모양 변화 평균 %/프레임 | 계단 순간 최대 % | 계단 횟수/90° |");
foreach (int dirs in new[] { 0, 32, 16, 8 })
{
float q = dirs == 0 ? 0f : 360f / dirs;
Frame prev = null; var sf = new List<float>(); int zero = 0, steps = 0; float mx = 0;
for (int i = 0; i < N; i++)
{
float y = yaw0 + i * STEP; if (q > 0f) y = Mathf.Round(y / q) * q;
root.transform.rotation = Quaternion.Euler(0, y, 0);
var f = Grab(135, 240);
if (prev != null)
{
Compare(prev, f, 135, 240, out float s, out float c);
sf.Add(s); if (s < 0.01f) zero++; if (s > 0.5f) steps++; if (s > mx) mx = s;
}
prev = f;
}
L(string.Format(" | {0} | {1} | **{2}/90** | **{3:F2} %** | {4:F1} % | {5} |",
dirs == 0 ? "양자화 없음" : dirs.ToString(), dirs == 0 ? "-" : q.ToString("F1") + "°",
zero, sf.Average(), mx, steps));
}
root.transform.rotation = baseRot;
foreach (var kv in orig) if (kv.Key != null) kv.Key.gameObject.layer = kv.Value;
if (cam != null) UnityEngine.Object.DestroyImmediate(cam.gameObject);
if (mcam != null) UnityEngine.Object.DestroyImmediate(mcam.gameObject);
}
catch (Exception e) { L("EXCEPTION " + e); }
finally { File.AppendAllText(OUT, sb.ToString(), new UTF8Encoding(false)); }
}
static void Body()
{
EditorSceneManager.OpenScene(SCENE, OpenSceneMode.Single);
foreach (var go in UnityEngine.SceneManagement.SceneManager.GetActiveScene().GetRootGameObjects())
if (go.name.Contains("LH_M05")) root = go;
smrs = root.GetComponentsInChildren<SkinnedMeshRenderer>(true).Where(r => r.enabled).ToArray();
scam = Camera.main ?? UnityEngine.Object.FindObjectsByType<Camera>(FindObjectsInactive.Exclude, FindObjectsSortMode.None).First(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; }
float camYaw = scam.transform.eulerAngles.y;
var basePos = root.transform.position;
var baseRot = root.transform.rotation;
root.transform.rotation = Quaternion.Euler(0, camYaw + 180f, 0);
float yaw0 = camYaw + 180f;
const int N = 33; // 33 프레임 = 32 비교
const float STEP = 0.25f; // 프레임당 0.25° 회전 (걷다가 방향 트는 속도 수준)
try
{
// ── 1) 해상도별 회전 떨림 ────────────────────────────────────
L("== ⓓ-1 해상도별 회전 떨림 (캐릭터 yaw 0.25°/프레임 × 32회 · 이동분 제거 후 비교) ==");
L(" | 설정 | 캐릭터 키 | 모양 변화 %/프레임 | 색 변화 %/프레임 | 1° 환산 모양 % |");
var res = new (string name, float ortho, int w, int h)[] {
("도트A 240줄 (ortho 3.4)", 3.4f, 135, 240),
("360줄 (ortho 3.4)", 3.4f, 203, 360),
("480줄 (ortho 3.4)", 3.4f, 270, 480),
("현재 1920줄 (ortho 10)", 10f, 1080, 1920),
};
foreach (var r in res)
{
Setup(r.ortho, r.w, r.h);
var (sf, cd, hgt) = RotSweep(r.w, r.h, yaw0, STEP, N, 0f);
L(string.Format(" | {0} | {1} px | **{2:F2} %** | **{3:F1} %** | {4:F1} % |", r.name, hgt, sf, cd, sf / STEP));
}
// ── 2) 회전 양자화 ──────────────────────────────────────────
L("");
L("== ⓓ-2 회전 계단화 (yaw 를 N방향으로 반올림 · 도트A 240줄) ==");
L(" | 방향 수 | 각도 간격 | 모양 변화 평균 %/프레임 | 그중 최대(계단 순간) % | 계단이 생긴 프레임 수/32 |");
Setup(3.4f, 135, 240);
foreach (int dirs in new[] { 0, 32, 16, 8 })
{
float q = dirs == 0 ? 0f : 360f / dirs;
var (sf, cd, _) = RotSweep(135, 240, yaw0, STEP, N, q, out float mx, out int steps);
L(string.Format(" | {0} | {1} | **{2:F2} %** | {3:F2} % | {4} |",
dirs == 0 ? "양자화 없음(연속)" : dirs.ToString(), dirs == 0 ? "-" : (q.ToString("F1") + "°"), sf, mx, steps));
}
// ── 3) 이동 + 픽셀 격자 스냅 ────────────────────────────────
L("");
L("== ⓓ-3 이동 시 떨림 · 픽셀 격자 스냅(VoxelGridAdjuster 방식) 유/무 (도트A 240줄) ==");
Setup(3.4f, 135, 240);
float pixelWorld = 2f * 3.4f / 240f;
L(string.Format(" 픽셀 1개 = {0:F5} m · 프레임당 이동 0.37 px (일부러 격자에 안 맞는 값)", pixelWorld));
L(" | 스냅 | 모양 변화 평균 %/프레임 | 최대 % |");
foreach (bool snap in new[] { false, true })
{
root.transform.rotation = Quaternion.Euler(0, yaw0, 0);
Frame prev = null; var sfs = new List<float>();
for (int i = 0; i < N; i++)
{
var p = basePos + cam.transform.right * (0.37f * pixelWorld * i);
root.transform.position = snap ? ToGrid(p, pixelWorld) : p;
var f = Grab(135, 240);
if (prev != null) { Compare(prev, f, 135, 240, out float s, out float c); sfs.Add(s); }
prev = f;
}
root.transform.position = basePos;
L(string.Format(" | {0} | **{1:F2} %** | {2:F2} % |", snap ? "켬(격자 스냅)" : "끔", sfs.Average(), sfs.Max()));
}
// ── 4) 외곽선 두께와 떨림 ───────────────────────────────────
L("");
L("== ⓓ-4 외곽선 두께 vs 떨림 (회전 중 · 도트A 240줄 · 외곽선 = 실루엣을 N px 부풀린 띠) ==");
Setup(3.4f, 135, 240);
L(" | 외곽선 두께 | 띠 면적(px) | 띠가 프레임마다 바뀌는 비율 % | 캐릭터 대비 띠 비중 % |");
var frames = new List<Frame>();
for (int i = 0; i < 17; i++) { root.transform.rotation = Quaternion.Euler(0, yaw0 + i * STEP, 0); frames.Add(Grab(135, 240)); }
foreach (int th in new[] { 1, 2, 3 })
{
var bandFlip = new List<float>(); var bandArea = new List<float>(); var ratio = new List<float>();
for (int i = 1; i < frames.Count; i++)
{
var A = frames[i - 1]; var B = frames[i];
var dA = Dilate(A.m, 135, 240, th); var dB = Dilate(B.m, 135, 240, th);
int dx = B.x0 - A.x0, dy = B.y0 - A.y0;
int xor = 0, ar = 0;
for (int y = 1; y < 239; y++)
for (int x = 1; x < 134; x++)
{
bool ba = dA[y * 135 + x] && !A.m[y * 135 + x];
int bxx = x + dx, byy = y + dy;
bool bb = (bxx > 0 && byy > 0 && bxx < 134 && byy < 239) && dB[byy * 135 + bxx] && !B.m[byy * 135 + bxx];
if (ba) ar++;
if (ba != bb) xor++;
}
bandFlip.Add(100f * xor / Mathf.Max(1, ar)); bandArea.Add(ar); ratio.Add(100f * ar / A.area);
}
L(string.Format(" | {0} px | {1:F0} | **{2:F1} %** | {3:F0} % |", th, bandArea.Average(), bandFlip.Average(), ratio.Average()));
}
// ── 5) 연속 프레임 비교 이미지 ──────────────────────────────
L("");
L("== 캡처 ==");
Strip("u_6_crawl_rotation.png", 3.4f, 135, 240, yaw0, STEP, 0f, 16f, "회전 8프레임: 위=양자화 없음 / 아래=16방향 계단화");
StripMove("u_7_crawl_move_snap.png", 3.4f, 135, 240, yaw0, basePos);
}
finally
{
root.transform.rotation = baseRot; root.transform.position = basePos;
foreach (var kv in orig) if (kv.Key != null) kv.Key.gameObject.layer = kv.Value;
if (cam != null) UnityEngine.Object.DestroyImmediate(cam.gameObject);
if (mcam != null) UnityEngine.Object.DestroyImmediate(mcam.gameObject);
}
}
static (float, float, int) RotSweep(int w, int h, float yaw0, float step, int n, float q)
{ return RotSweep(w, h, yaw0, step, n, q, out _, out _); }
static (float, float, int) RotSweep(int w, int h, float yaw0, float step, int n, float q, out float maxFlip, out int stepCount)
{
Frame prev = null; var sf = new List<float>(); var cd = new List<float>(); int hgt = 0;
maxFlip = 0; stepCount = 0;
for (int i = 0; i < n; i++)
{
float y = yaw0 + i * step;
if (q > 0f) y = Mathf.Round(y / q) * q;
root.transform.rotation = Quaternion.Euler(0, y, 0);
var f = Grab(w, h);
hgt = f.y1 - f.y0 + 1;
if (prev != null)
{
Compare(prev, f, w, h, out float s, out float c);
sf.Add(s); cd.Add(c);
if (s > maxFlip) maxFlip = s;
if (s > 0.5f) stepCount++;
}
prev = f;
}
return (sf.Average(), cd.Average(), hgt);
}
// 회전 8프레임 × 2행(양자화 없음 / 양자화)
static void Strip(string file, float ortho, int w, int h, float yaw0, float step, float q1, float dirs2, string note)
{
Setup(ortho, w, h);
int zoom = 6, crop = 48;
var rows = new List<Shot[]>();
foreach (float dirs in new[] { 0f, dirs2 })
{
float q = dirs == 0f ? 0f : 360f / dirs;
var row = new List<Shot>();
for (int i = 0; i < 8; i++)
{
float y = yaw0 + i * step * 4f; // 8프레임에 8° 회전
if (q > 0f) y = Mathf.Round(y / q) * q;
root.transform.rotation = Quaternion.Euler(0, y, 0);
row.Add(Render(cam, w, h));
}
rows.Add(row.ToArray());
}
// 합성
int cw = crop, chh = crop, gap = 4;
int ow = (cw * zoom + gap) * 8 - gap, oh = (chh * zoom + gap) * rows.Count - gap;
var o = new Shot { w = ow, h = oh, px = new Color32[ow * oh] };
for (int i = 0; i < o.px.Length; i++) o.px[i] = new Color32(24, 24, 28, 255);
int cx = w / 2 - crop / 2, cy = h / 2 - crop / 2;
for (int r = 0; r < rows.Count; r++)
for (int k = 0; k < 8; k++)
for (int y = 0; y < chh * zoom; y++)
for (int x = 0; x < cw * zoom; x++)
{
int sx = cx + x / zoom, sy = cy + y / zoom;
if (sx < 0 || sy < 0 || sx >= w || sy >= h) continue;
int dxp = k * (cw * zoom + gap) + x;
int dyp = (rows.Count - 1 - r) * (chh * zoom + gap) + y;
o.px[dyp * ow + dxp] = rows[r][k].px[sy * w + sx];
}
SaveShot(o, file); L(" " + file + " — " + note);
}
static void StripMove(string file, float ortho, int w, int h, float yaw0, Vector3 basePos)
{
Setup(ortho, w, h);
root.transform.rotation = Quaternion.Euler(0, yaw0, 0);
float pixelWorld = 2f * ortho / h;
int zoom = 6, crop = 48, gap = 4;
var rows = new List<Shot[]>();
foreach (bool snap in new[] { false, true })
{
var row = new List<Shot>();
for (int i = 0; i < 8; i++)
{
var p = basePos + cam.transform.right * (0.37f * pixelWorld * i);
root.transform.position = snap ? ToGrid(p, pixelWorld) : p;
row.Add(Render(cam, w, h));
}
rows.Add(row.ToArray());
}
root.transform.position = basePos;
int ow = (crop * zoom + gap) * 8 - gap, oh = (crop * zoom + gap) * 2 - gap;
var o = new Shot { w = ow, h = oh, px = new Color32[ow * oh] };
for (int i = 0; i < o.px.Length; i++) o.px[i] = new Color32(24, 24, 28, 255);
int cx = w / 2 - crop / 2, cy = h / 2 - crop / 2;
for (int r = 0; r < 2; r++)
for (int k = 0; k < 8; k++)
for (int y = 0; y < crop * zoom; y++)
for (int x = 0; x < crop * zoom; x++)
{
int sx = cx + x / zoom, sy = cy + y / zoom;
if (sx < 0 || sy < 0 || sx >= w || sy >= h) continue;
o.px[((1 - r) * (crop * zoom + gap) + y) * ow + k * (crop * zoom + gap) + x] = rows[r][k].px[sy * w + sx];
}
SaveShot(o, file); L(" " + file + " — 이동 8프레임: 위=스냅 없음 / 아래=픽셀 격자 스냅");
}
static void SaveShot(Shot s, string file)
{
var t = new Texture2D(s.w, s.h, TextureFormat.RGBA32, false); t.SetPixels32(s.px); t.Apply();
Directory.CreateDirectory(SHOT); File.WriteAllBytes(SHOT + file, t.EncodeToPNG());
UnityEngine.Object.DestroyImmediate(t);
}
}