Project_WL/AgentScripts/WL814b_Probe.cs

638 lines
37 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.

// PD 지시 #814 · 발주서 WL-814b — Critter 픽셀 카메라 리그 검증 프로브 + 룩 캡처
// (에디트 모드 전용 · Play 0 · 로그인 0 · 디스크 에셋 무수정)
//
// 실행 (CLAUDE.md §1: using 금지 · 네임스페이스 풀어 쓰기)
// unity command run_script --file AgentScripts/WL814b_Probe.cs --entry WL814b_Probe.MakeAsset --timeout 300
// unity command run_script --file AgentScripts/WL814b_Probe.cs --entry WL814b_Probe.Edit --timeout 1800
//
// 무엇을 재는가
// A. 표 ① 보조 — 원본 Main Camera.prefab 값 · NewGameUI 캔버스 렌더 모드/카메라 · UICamera 스택
// B. 리그 — 임시 씬(추가 로드 · 저장 0)에 맵 + PC + 잡몹 + 원본 카메라 → 조립 → 구조 표
// C. 복원 — 원본 카메라 값 스냅샷 == 복원값(문자열 동일) · 리그 오브젝트 0 · RT 해제
// D. 매핑오차 — Camera.main 투영(WorldToViewportPoint) vs 실제 RT→쿼드→뷰 카메라 화면 좌표(5점 · px)
// E. GC — 프록시 미러 · 회전 스냅 판정 200k 회 할당 증가분(0 B 목표)
// F. C8 — RuntimeDisabled = true → Tick 이 즉시 복원 · 조립 0
// G. 캡처 — 1080×1920 4장(ortho 3.8×480 · 3.8×360 · 4.6×480 · 대조 원근 원본)
public static class WL814b_Probe
{
const string kOutDir = @"E:\NerdNavis\nn_himminji\Screenshots_WL\WL814b";
const string kMap = "Assets/Res_Addr/Map/WL_Nature.prefab";
const string kPc = "Assets/Res_Addr/PC/Ai01.prefab";
const string kCam = "Assets/ResWork/Prefabs/Ingame/Main Camera.prefab";
const string kRig = "Assets/3DPixelCamera/URP/PixelCameraSystem.prefab";
const string kSo = "Assets/WL/Combat/Settings/Resources/WL/WLPixelCameraSettings.asset";
const string kSnap = "AgentScripts/staging/WL814b/EDIT_PROBE.txt";
const int W = 1080, H = 1920;
static System.Text.StringBuilder sb;
static int pass, fail;
static UnityEngine.Vector3 s_lookAtWorld;
static void L(string s) { sb.Append(s).Append('\n'); }
static void Chk(bool ok, string label, string detail)
{
if (ok) pass++; else fail++;
sb.Append(ok ? " PASS " : " FAIL ").Append(label);
if (!string.IsNullOrEmpty(detail)) sb.Append(" ").Append(detail);
sb.Append('\n');
}
// ─────────────────────────────────────────────────────────────────────────
// SO 에셋 생성 (1회) — 손으로 YAML 쓰지 않고 에디터가 만들게 한다
// ─────────────────────────────────────────────────────────────────────────
public static string MakeAsset()
{
sb = new System.Text.StringBuilder();
var dir = System.IO.Path.GetDirectoryName(kSo).Replace('\\', '/');
if (!System.IO.Directory.Exists(dir)) { L("FAIL 폴더 없음 " + dir); return sb.ToString(); }
var so = UnityEditor.AssetDatabase.LoadAssetAtPath<WL.PixelArt.WLPixelCameraSettings>(kSo);
bool created = false;
if (so == null)
{
so = UnityEngine.ScriptableObject.CreateInstance<WL.PixelArt.WLPixelCameraSettings>();
UnityEditor.AssetDatabase.CreateAsset(so, kSo);
created = true;
}
var rig = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(kRig);
so.pixelCameraSystemPrefab = rig;
UnityEditor.EditorUtility.SetDirty(so);
UnityEditor.AssetDatabase.SaveAssets();
UnityEditor.AssetDatabase.Refresh();
L((created ? "생성" : "갱신") + " " + kSo);
L(" prefab=" + (rig == null ? "null(패키지 해제 확인)" : UnityEditor.AssetDatabase.GetAssetPath(rig)));
L(" guid=" + UnityEditor.AssetDatabase.AssetPathToGUID(kSo));
L(" enabled=" + so.enabled + " orthoSize=" + so.orthoSize + " pixelHeight=" + so.pixelHeight
+ " rigLayer=" + so.rigLayer + " proxyMode=" + so.proxyMode
+ " compensateViewZoom=" + so.compensateViewZoom);
return sb.ToString();
}
// ─────────────────────────────────────────────────────────────────────────
// 본 프로브
// ─────────────────────────────────────────────────────────────────────────
public static string Edit()
{
sb = new System.Text.StringBuilder();
pass = fail = 0;
System.IO.Directory.CreateDirectory(kOutDir);
L("# WL-814b 프로브 — " + System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
L("unity=" + UnityEngine.Application.unityVersion + " · isPlaying=" + UnityEngine.Application.isPlaying);
SectionA();
// 배치모드 에디터의 현재(무제목·빈) 씬에 직접 짓는다 — 추가 씬 생성은 무제목 씬이 열려 있으면 금지된다.
// 모든 인스턴스는 HideAndDontSave + DestroyImmediate 이므로 씬에 남지도, 저장되지도 않는다(811o 방식).
var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
L("현재 씬 = '" + scene.name + "' path='" + scene.path + "' dirty(before)=" + scene.isDirty
+ " rootCount=" + scene.rootCount);
UnityEngine.GameObject mapGo = null, pcGo = null, camGo = null, lightGo = null;
var mobs = new System.Collections.Generic.List<UnityEngine.GameObject>();
try
{
BuildScene(ref mapGo, ref pcGo, ref camGo, ref lightGo, mobs);
if (camGo == null) { L("FAIL 카메라 인스턴스 실패 — 중단"); }
else
{
var cam = camGo.GetComponent<UnityEngine.Camera>();
SectionBCD(cam, pcGo);
SectionE(cam);
SectionF(cam);
SectionG(cam);
}
}
catch (System.Exception ex) { L("EXCEPTION " + ex.GetType().Name + " " + ex.Message); L(ex.StackTrace); fail++; }
finally
{
WL.PixelArt.PixelCameraRig.Teardown("probe end");
WL.PixelArt.WLPixelCameraSettings.RuntimeDisabled = false;
for (int i = 0; i < mobs.Count; i++) if (mobs[i] != null) UnityEngine.Object.DestroyImmediate(mobs[i]);
if (pcGo != null) UnityEngine.Object.DestroyImmediate(pcGo);
if (camGo != null) UnityEngine.Object.DestroyImmediate(camGo);
if (mapGo != null) UnityEngine.Object.DestroyImmediate(mapGo);
if (lightGo != null) UnityEngine.Object.DestroyImmediate(lightGo);
var after = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
L("씬 정리 후: dirty=" + after.isDirty + " rootCount=" + after.rootCount + " (저장 0)");
}
L("");
L("RESULT " + (fail == 0 ? "PASS" : "FAIL") + " " + pass + "/" + (pass + fail));
var outPath = kSnap;
System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(outPath));
System.IO.File.WriteAllText(outPath, sb.ToString(), new System.Text.UTF8Encoding(false));
return sb.ToString();
}
// ── A. 표 ① 보조 실측 ────────────────────────────────────────────────────
static void SectionA()
{
L("");
L("## A. 원본 카메라 · UI 캔버스 실측");
var camPf = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(kCam);
if (camPf == null) { L("FAIL Main Camera.prefab 로드 실패"); fail++; return; }
var c = camPf.GetComponent<UnityEngine.Camera>();
var rc = camPf.GetComponent("RealCamera");
var al = camPf.GetComponent<UnityEngine.AudioListener>();
var mrv = camPf.GetComponent("MeshRendererVisibility");
var ucd = camPf.GetComponent<UnityEngine.Rendering.Universal.UniversalAdditionalCameraData>();
L("Main Camera.prefab: tag=" + camPf.tag + " layer=" + camPf.layer + " parent=" + (camPf.transform.parent == null ? "none" : camPf.transform.parent.name)
+ " children=" + camPf.transform.childCount);
L(" Camera: ortho=" + c.orthographic + " fov=" + c.fieldOfView + " size=" + c.orthographicSize
+ " near=" + c.nearClipPlane + " far=" + c.farClipPlane + " depth=" + c.depth
+ " mask=" + c.cullingMask + " clear=" + c.clearFlags + " hdr=" + c.allowHDR + " msaa=" + c.allowMSAA
+ " occlusionCulling=" + c.useOcclusionCulling);
if (ucd != null)
L(" URP: type=" + ucd.renderType + " post=" + ucd.renderPostProcessing + " shadows=" + ucd.renderShadows
+ " volumeMask=" + ucd.volumeLayerMask.value + " aa=" + ucd.antialiasing
+ " stack=" + (ucd.cameraStack == null ? -1 : ucd.cameraStack.Count));
L(" 컴포넌트: RealCamera=" + (rc != null) + " AudioListener=" + (al != null) + " MeshRendererVisibility=" + (mrv != null));
Chk(rc != null && al != null, "원본 카메라 = RealCamera + AudioListener 보유(재부모화·비활성 금지 근거)", "");
var uiPf = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>("Assets/Res_Addr/MainUI/NewGameUI.prefab");
if (uiPf != null)
{
var canvases = uiPf.GetComponentsInChildren<UnityEngine.Canvas>(true);
L("NewGameUI.prefab 캔버스 " + canvases.Length + "개");
for (int i = 0; i < canvases.Length; i++)
{
var cv = canvases[i];
L(" [" + i + "] " + cv.name + " renderMode=" + cv.renderMode
+ " worldCamera=" + (cv.worldCamera == null ? "null" : cv.worldCamera.name)
+ " planeDistance=" + cv.planeDistance + " sortingOrder=" + cv.sortingOrder + " layer=" + cv.gameObject.layer);
}
var cams = uiPf.GetComponentsInChildren<UnityEngine.Camera>(true);
for (int i = 0; i < cams.Length; i++)
L(" UI camera: " + cams[i].name + " ortho=" + cams[i].orthographic + " depth=" + cams[i].depth
+ " mask=" + cams[i].cullingMask + " clear=" + cams[i].clearFlags);
bool ssc = false;
for (int i = 0; i < canvases.Length; i++) if (canvases[i].renderMode == UnityEngine.RenderMode.ScreenSpaceCamera) ssc = true;
Chk(ssc, "인게임 UI 캔버스 = Screen Space - Camera(UICamera) → 원본 카메라를 끄면 UI 가 사라진다", "");
}
else { L("FAIL NewGameUI.prefab 로드 실패"); fail++; }
}
// ── 씬 조립 ──────────────────────────────────────────────────────────────
static void BuildScene(ref UnityEngine.GameObject mapGo, ref UnityEngine.GameObject pcGo,
ref UnityEngine.GameObject camGo, ref UnityEngine.GameObject lightGo,
System.Collections.Generic.List<UnityEngine.GameObject> mobs)
{
L("");
L("## B. 임시 씬(추가 로드 · 저장 0)");
var mapPf = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(kMap);
if (mapPf != null) { mapGo = UnityEngine.Object.Instantiate(mapPf); mapGo.name = "__WL814b_Map"; mapGo.hideFlags = UnityEngine.HideFlags.HideAndDontSave; }
L("맵 " + (mapGo == null ? "실패" : mapGo.name + " renderers=" + mapGo.GetComponentsInChildren<UnityEngine.Renderer>(true).Length));
// 지면 y 찾기 + 소품이 가장 많은 앵커 고르기(룩 캡처라 배경이 비어 있으면 안 된다)
UnityEngine.Physics.SyncTransforms();
var rends = mapGo == null ? new UnityEngine.Renderer[0] : mapGo.GetComponentsInChildren<UnityEngine.Renderer>(true);
// 소품이 실제로 모여 있는 곳을 찾는다 — 소품 중심들 중 반경 10 m 이웃이 가장 많은 지점
var cands = new System.Collections.Generic.List<UnityEngine.Vector3>();
for (int r = 0; r < rends.Length; r++)
{
if (rends[r] == null) continue;
var b = rends[r].bounds;
if (b.size.y < 1.0f || b.size.y > 20f) continue; // 지형/바닥/거대 오브젝트 제외
if (b.center.y > 6f) continue; // 언덕 위·공중 제외
cands.Add(b.center);
}
UnityEngine.Vector3 pcPos = new UnityEngine.Vector3(139f, 0.8f, -20f);
int bestCount = -1;
for (int i = 0; i < cands.Count; i++)
{
int n = 0;
for (int j = 0; j < cands.Count; j++)
if ((cands[j] - cands[i]).sqrMagnitude < 100f) n++; // 반경 10 m
if (n > bestCount) { bestCount = n; pcPos = cands[i]; }
}
string bestHit = "미검출";
UnityEngine.RaycastHit gh;
if (UnityEngine.Physics.Raycast(new UnityEngine.Vector3(pcPos.x, pcPos.y + 100f, pcPos.z), UnityEngine.Vector3.down, out gh, 400f))
{ pcPos.y = gh.point.y; bestHit = gh.collider.name; }
L(" 소품 후보 " + cands.Count + "개 · 가장 밀집한 지점 반경 10 m 이웃 " + bestCount + "개");
L("PC 위치 " + pcPos.ToString("F2") + " · 반경 10 m 소품 " + bestCount + "개 (지면 " + bestHit + ")");
var pcPf = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(kPc);
if (pcPf != null)
{
pcGo = UnityEngine.Object.Instantiate(pcPf);
pcGo.name = "__WL814b_PC";
pcGo.hideFlags = UnityEngine.HideFlags.HideAndDontSave;
pcGo.transform.position = pcPos;
pcGo.transform.rotation = UnityEngine.Quaternion.Euler(0f, 200f, 0f);
StripBehaviours(pcGo);
L("PC 포즈: " + PoseIdle(pcGo));
}
L("PC " + (pcGo == null ? "실패" : "ok"));
// 잡몹 3
string[] mobPaths = {
"Assets/Res_Addr/Mobs/Mob/Anto_A.prefab",
"Assets/Res_Addr/Mobs/Mob/Anto_B.prefab",
"Assets/Res_Addr/Mobs/Mob/Angez_A.prefab",
};
UnityEngine.Vector3[] mobOff = {
new UnityEngine.Vector3(2.2f, 0f, 1.6f),
new UnityEngine.Vector3(-2.4f, 0f, 0.8f),
new UnityEngine.Vector3(0.2f, 0f, 3.0f),
};
for (int i = 0; i < mobPaths.Length; i++)
{
var pf = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(mobPaths[i]);
if (pf == null) continue;
var g = UnityEngine.Object.Instantiate(pf);
g.name = "__WL814b_Mob" + i;
g.hideFlags = UnityEngine.HideFlags.HideAndDontSave;
var p = pcPos + mobOff[i];
UnityEngine.RaycastHit mh;
if (UnityEngine.Physics.Raycast(new UnityEngine.Vector3(p.x, 200f, p.z), UnityEngine.Vector3.down, out mh, 500f)) p.y = mh.point.y;
g.transform.position = p;
g.transform.rotation = UnityEngine.Quaternion.Euler(0f, 20f + i * 90f, 0f);
StripBehaviours(g);
PoseIdle(g);
mobs.Add(g);
}
L("잡몹 " + mobs.Count + "체");
lightGo = new UnityEngine.GameObject("__WL814b_Sun");
lightGo.hideFlags = UnityEngine.HideFlags.HideAndDontSave;
var lt = lightGo.AddComponent<UnityEngine.Light>();
lt.type = UnityEngine.LightType.Directional;
lt.intensity = 1.1f;
lt.color = new UnityEngine.Color(1f, 0.97f, 0.92f);
lightGo.transform.rotation = UnityEngine.Quaternion.Euler(48f, 35f, 0f);
// 원본 카메라 인스턴스 — 811o 구도(h 76 · d 3.0 · up 2.4)를 RealCamera 수식으로 직접 계산
var camPf = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(kCam);
if (camPf == null) return;
camGo = UnityEngine.Object.Instantiate(camPf);
camGo.name = "__WL814b_MainCamera";
camGo.hideFlags = UnityEngine.HideFlags.HideAndDontSave;
StripBehaviours(camGo); // RealCamera·MeshRendererVisibility 정지(트랜스폼은 우리가 잡는다)
var cam = camGo.GetComponent<UnityEngine.Camera>();
cam.aspect = (float)W / H;
const float camHeight = 76f, rotateAround = 347.21f, distanceAway = 3.0f, distanceUp = 2.4f;
var lookAt = new UnityEngine.Vector3(0f, 1.06f, 0f);
var rotv = UnityEngine.Quaternion.Euler(camHeight, rotateAround, 0f) * UnityEngine.Vector3.one;
var camPosition = pcPos + UnityEngine.Vector3.up * distanceUp - rotv * distanceAway;
camGo.transform.position = camPosition;
s_lookAtWorld = pcPos + lookAt;
camGo.transform.LookAt(s_lookAtWorld);
float dist = UnityEngine.Vector3.Distance(camPosition, pcPos + lookAt);
L("카메라(811o 구도) pos=" + camPosition.ToString("F3") + " euler=" + camGo.transform.eulerAngles.ToString("F2"));
L(" PC 주시점까지 거리 = " + dist.ToString("F3") + " m → 원근 fov 60 의 반높이 = " + (dist * UnityEngine.Mathf.Tan(30f * UnityEngine.Mathf.Deg2Rad)).ToString("F3") + " m");
}
/// <summary>에디트 모드에서는 Animator 가 돌지 않아 T 포즈가 된다 → idle 클립을 1프레임 샘플링한다.</summary>
static string PoseIdle(UnityEngine.GameObject go)
{
var anim = go.GetComponentInChildren<UnityEngine.Animator>(true);
if (anim == null || anim.runtimeAnimatorController == null) return "Animator 없음(T 포즈)";
var clips = anim.runtimeAnimatorController.animationClips;
if (clips == null || clips.Length == 0) return "클립 0(T 포즈)";
UnityEngine.AnimationClip pick = null;
for (int i = 0; i < clips.Length; i++)
{
if (clips[i] == null) continue;
var n = clips[i].name.ToLower();
if (n.Contains("idle") || n.Contains("wait") || n.Contains("stand")) { pick = clips[i]; break; }
}
if (pick == null) pick = clips[0];
pick.SampleAnimation(anim.gameObject, 0.4f);
return pick.name + " @0.4s (" + clips.Length + "클립)";
}
/// <summary>임시 씬에서 원본 로직이 돌지 않게 MonoBehaviour 를 전부 끈다(에셋 무변경 · 인스턴스만).</summary>
static void StripBehaviours(UnityEngine.GameObject go)
{
var all = go.GetComponentsInChildren<UnityEngine.MonoBehaviour>(true);
for (int i = 0; i < all.Length; i++)
{
if (all[i] == null) continue;
if (all[i] is UnityEngine.Rendering.Universal.UniversalAdditionalCameraData) continue;
try { all[i].enabled = false; } catch { }
}
}
// ── B/C/D. 조립 · 복원 · 매핑 오차 ──────────────────────────────────────
static void SectionBCD(UnityEngine.Camera cam, UnityEngine.GameObject pcGo)
{
L("");
L("## B. 리그 조립");
var cfg = MakeCfg(3.8f, 480, false);
if (cfg == null) { L("FAIL SO/프리팹 없음 — 중단"); fail++; return; }
string before = WL.PixelArt.PixelCameraRig.CurrentSummary(cam);
int origMask = cam.cullingMask;
L("조립 전 원본 카메라: " + before);
bool ok = WL.PixelArt.PixelCameraRig.Assemble(cfg, cam);
Chk(ok && WL.PixelArt.PixelCameraRig.IsAssembled, "조립 성공", WL.PixelArt.PixelCameraRig.LastLog);
if (!ok) return;
var rig = WL.PixelArt.PixelCameraRig.RigRoot;
var gameCam = WL.PixelArt.PixelCameraRig.GameCamera;
var viewCam = WL.PixelArt.PixelCameraRig.ViewCamera;
var mgr = WL.PixelArt.PixelCameraRig.Manager;
var proxy = WL.PixelArt.PixelCameraRig.Proxy;
L("리그 구조:");
Dump(rig.transform, 1);
L("프록시: " + (proxy == null ? "null" : proxy.name + " parent=" + (proxy.parent == null ? "none" : proxy.parent.name)
+ " worldPos=" + proxy.position.ToString("F4") + " camPos=" + cam.transform.position.ToString("F4")));
Chk(proxy != null && (proxy.position - cam.transform.position).sqrMagnitude < 1e-8f,
"프록시 월드 위치 == 원본 카메라 위치(지연 0)", "");
Chk(proxy != null && UnityEngine.Quaternion.Angle(proxy.rotation, cam.transform.rotation) < 1e-3f,
"프록시 회전 == 원본 카메라 회전", "");
string after = WL.PixelArt.PixelCameraRig.CurrentSummary(cam);
L("조립 후 원본 카메라: " + after);
Chk(cam.orthographic, "원본 카메라 직교 전환", "size=" + cam.orthographicSize);
Chk(cam.cullingMask == 0, "원본 카메라 월드 미렌더(cullingMask 0)", "mask=" + cam.cullingMask);
Chk(cam.tag == "MainCamera" && cam.enabled, "MainCamera 태그·enabled 유지(Camera.main 보존)", "tag=" + cam.tag);
// Critter 를 1프레임 강제로 돌린다(에디트 모드에는 LateUpdate 가 안 올 수 있다)
viewCam.aspect = (float)W / H;
mgr.SendMessage("LateUpdate", UnityEngine.SendMessageOptions.DontRequireReceiver);
var rt = gameCam.targetTexture;
L("게임 카메라: ortho=" + gameCam.orthographic + " size=" + gameCam.orthographicSize.ToString("F4")
+ " mask=" + gameCam.cullingMask + " depth=" + gameCam.depth
+ " RT=" + (rt == null ? "null" : rt.width + "×" + rt.height + " filter=" + rt.filterMode + " fmt=" + rt.format));
L("뷰 카메라: ortho=" + viewCam.orthographic + " size=" + viewCam.orthographicSize.ToString("F4")
+ " mask=" + viewCam.cullingMask + " near=" + viewCam.nearClipPlane + " far=" + viewCam.farClipPlane
+ " localPos=" + viewCam.transform.localPosition.ToString("F5"));
L("Critter: GameResolution=" + mgr.GameResolution + " zoom=" + mgr.GameCameraZoom.ToString("F5")
+ " viewZoom=" + mgr.ViewCameraZoom + " grid=" + mgr.VoxelGridMovement + " sub=" + mgr.SubpixelAdjustments
+ " followRot=" + mgr.FollowRotation);
Chk(rt != null && rt.height == 480 && rt.filterMode == UnityEngine.FilterMode.Point,
"저해상도 RT 480줄 · 포인트 필터", rt == null ? "null" : rt.width + "×" + rt.height);
Chk(gameCam.cullingMask == (origMask & ~(1 << cfg.rigLayer)),
"게임 카메라 = 원본 cullingMask 승계", "orig=" + origMask + " game=" + gameCam.cullingMask);
Chk((gameCam.cullingMask & (1 << cfg.rigLayer)) == 0, "게임 카메라가 픽셀 쿼드를 다시 그리지 않는다(리그 레이어 제외)", "");
Chk(viewCam.cullingMask == (1 << cfg.rigLayer), "뷰 카메라는 픽셀 쿼드만 그린다", "mask=" + viewCam.cullingMask);
// ── D. 매핑 오차
L("");
L("## D. 매핑 오차 — Camera.main 투영 vs 실제 화면(1080×1920 · px)");
MappingTable(cam, gameCam, viewCam, mgr, pcGo);
// ── C. 복원
L("");
L("## C. 복원");
var rtRef = gameCam.targetTexture;
WL.PixelArt.PixelCameraRig.Teardown("probe C");
string restored = WL.PixelArt.PixelCameraRig.CurrentSummary(cam);
L("복원 후 원본 카메라: " + restored);
Chk(restored == before, "원본 카메라 값 스냅샷 == 복원값(문자열 완전 일치)", restored == before ? "" : "\n before=" + before + "\n after =" + restored);
Chk(!WL.PixelArt.PixelCameraRig.IsAssembled, "리그 오브젝트 0", "");
Chk(rtRef == null, "RT 참조 해제(Release + Destroy)", "rt=" + (rtRef == null ? "null" : "alive"));
int strays = 0;
var left = UnityEngine.Object.FindObjectsByType<PixelCamera.PixelCameraManager>(UnityEngine.FindObjectsInactive.Include, UnityEngine.FindObjectsSortMode.None);
strays = left == null ? 0 : left.Length;
Chk(strays == 0, "씬에 PixelCameraManager 잔재 0", "found=" + strays);
int proxyLeft = 0;
for (int i = 0; i < cam.transform.childCount; i++) if (cam.transform.GetChild(i).name == WL.PixelArt.PixelCameraRig.ProxyName) proxyLeft++;
Chk(proxyLeft == 0, "원본 카메라 자식 프록시 0", "found=" + proxyLeft);
var mat = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.Material>("Assets/3DPixelCamera/URP/UpscaleMaterialURP.mat");
bool matClean = mat == null || !mat.HasProperty("_LowResTexture") || mat.GetTexture("_LowResTexture") == null;
Chk(matClean, "디스크 UpscaleMaterialURP.mat 의 _LowResTexture 오염 0(런타임 인스턴스 사용)",
mat == null ? "mat=null" : ("tex=" + (mat.GetTexture("_LowResTexture") == null ? "null" : mat.GetTexture("_LowResTexture").name)));
}
static void MappingTable(UnityEngine.Camera src, UnityEngine.Camera gameCam, UnityEngine.Camera viewCam,
PixelCamera.PixelCameraManager mgr, UnityEngine.GameObject pcGo)
{
var quad = FindQuad();
if (quad == null) { L("FAIL 픽셀 쿼드를 못 찾음"); fail++; return; }
var rt = gameCam.targetTexture;
if (rt == null) { L("FAIL RT 없음"); fail++; return; }
var basePos = pcGo != null ? pcGo.transform.position : src.transform.position + src.transform.forward * 6f;
var pts = new UnityEngine.Vector3[]
{
basePos,
basePos + new UnityEngine.Vector3(0f, 1.7f, 0f),
basePos + new UnityEngine.Vector3(1.8f, 0.2f, 1.4f),
basePos + new UnityEngine.Vector3(-2.1f, 1.0f, 1.0f),
basePos + new UnityEngine.Vector3(0.4f, 0.6f, 2.6f),
};
L(" # | world | Camera.main px | 실제 화면 px | 오차 px | RT양자화 포함 오차 px");
float maxErr = 0f, maxErrQ = 0f, sumErr = 0f, sumErrQ = 0f;
for (int i = 0; i < pts.Length; i++)
{
var p = pts[i];
var vSrc = src.WorldToViewportPoint(p);
var expect = new UnityEngine.Vector2(vSrc.x * W, vSrc.y * H);
var vGame = gameCam.WorldToViewportPoint(p);
var actual = ViewportToScreen(vGame.x, vGame.y, quad, viewCam);
float qx = (UnityEngine.Mathf.Floor(vGame.x * rt.width) + 0.5f) / rt.width;
float qy = (UnityEngine.Mathf.Floor(vGame.y * rt.height) + 0.5f) / rt.height;
var actualQ = ViewportToScreen(qx, qy, quad, viewCam);
float e = UnityEngine.Vector2.Distance(expect, actual);
float eq = UnityEngine.Vector2.Distance(expect, actualQ);
maxErr = UnityEngine.Mathf.Max(maxErr, e); maxErrQ = UnityEngine.Mathf.Max(maxErrQ, eq);
sumErr += e; sumErrQ += eq;
L(" " + (i + 1) + " | " + p.ToString("F2")
+ " | (" + expect.x.ToString("F1") + "," + expect.y.ToString("F1") + ")"
+ " | (" + actual.x.ToString("F1") + "," + actual.y.ToString("F1") + ")"
+ " | " + e.ToString("F2")
+ " | " + eq.ToString("F2"));
}
L(" 최대 " + maxErr.ToString("F2") + " px · 평균 " + (sumErr / pts.Length).ToString("F2")
+ " px (연속) / 최대 " + maxErrQ.ToString("F2") + " px · 평균 " + (sumErrQ / pts.Length).ToString("F2") + " px (RT 양자화 포함)");
L(" 참고: 1 RT 픽셀 = 화면 " + ((float)H / rt.height).ToString("F2") + " px(세로) · " + ((float)W / rt.width).ToString("F2") + " px(가로)");
Chk(maxErr <= 2.0f, "연속 매핑 오차 ≤ 2 px (compensateViewZoom 보정 확인)", "max=" + maxErr.ToString("F2") + " px");
Chk(maxErrQ <= (float)H / rt.height, "RT 양자화 포함 오차 ≤ 업스케일 1픽셀", "max=" + maxErrQ.ToString("F2") + " px");
}
static UnityEngine.Vector2 ViewportToScreen(float vx, float vy, UnityEngine.Transform quad, UnityEngine.Camera viewCam)
{
var quadLocal = new UnityEngine.Vector3(vx - 0.5f, vy - 0.5f, 0f);
var quadWorld = quad.TransformPoint(quadLocal);
var vView = viewCam.WorldToViewportPoint(quadWorld);
return new UnityEngine.Vector2(vView.x * W, vView.y * H);
}
static UnityEngine.Transform FindQuad()
{
var rig = WL.PixelArt.PixelCameraRig.RigRoot;
if (rig == null) return null;
var uc = rig.GetComponentInChildren<PixelCamera.UpscaledCanvas>(true);
return uc == null ? null : uc.transform;
}
static void Dump(UnityEngine.Transform t, int depth)
{
var line = new System.Text.StringBuilder();
line.Append(' ', depth * 2).Append("- ").Append(t.name).Append(" layer=").Append(t.gameObject.layer);
var comps = t.GetComponents<UnityEngine.Component>();
line.Append(" [");
for (int i = 0; i < comps.Length; i++) if (comps[i] != null) line.Append(comps[i].GetType().Name).Append(' ');
line.Append("]");
line.Append(" lp=").Append(t.localPosition.ToString("F4")).Append(" ls=").Append(t.localScale.ToString("F4"));
L(line.ToString());
for (int i = 0; i < t.childCount; i++) Dump(t.GetChild(i), depth + 1);
}
// ── E. GC ────────────────────────────────────────────────────────────────
static void SectionE(UnityEngine.Camera cam)
{
L("");
L("## E. GC (200k 회)");
var probeGo = new UnityEngine.GameObject("__WL814b_MirrorProbe");
probeGo.hideFlags = UnityEngine.HideFlags.HideAndDontSave;
var mirror = probeGo.AddComponent<WL.PixelArt.PixelCameraProxyMirror>();
mirror.Source = cam.transform;
System.GC.Collect();
long m0 = System.GC.GetAllocatedBytesForCurrentThread();
long t0 = System.GC.GetTotalMemory(false);
int g0 = System.GC.CollectionCount(0);
for (int i = 0; i < 200000; i++) mirror.Mirror();
long m1 = System.GC.GetAllocatedBytesForCurrentThread();
long t1 = System.GC.GetTotalMemory(false);
int g1 = System.GC.CollectionCount(0);
L("프록시 미러 ×200k: thread Δ" + (m1 - m0) + " B · total Δ" + (t1 - t0) + " B · gen0 " + (g1 - g0));
Chk(m1 - m0 == 0, "프록시 미러 GC 0 B", "Δ=" + (m1 - m0));
UnityEngine.Object.DestroyImmediate(probeGo);
var cfg = MakeCfg(3.8f, 480, false);
WL.PixelArt.PixelCameraRig.Assemble(cfg, cam);
System.GC.Collect();
long s0 = System.GC.GetAllocatedBytesForCurrentThread();
for (int i = 0; i < 200000; i++) WL.PixelArt.PixelCameraRig.ProbeSnapTick();
long s1 = System.GC.GetAllocatedBytesForCurrentThread();
L("회전 스냅 판정 ×200k: thread Δ" + (s1 - s0) + " B");
Chk(s1 - s0 == 0, "회전 스냅 판정 GC 0 B", "Δ=" + (s1 - s0));
WL.PixelArt.PixelCameraRig.Teardown("probe E");
}
// ── F. C8 ────────────────────────────────────────────────────────────────
static void SectionF(UnityEngine.Camera cam)
{
L("");
L("## F. C8 롤백");
var so = WL.PixelArt.WLPixelCameraSettings.Instance;
Chk(so != null, "SO 로드(Resources/WL/WLPixelCameraSettings)", so == null ? "null" : "enabled=" + so.enabled);
string before = WL.PixelArt.PixelCameraRig.CurrentSummary(cam);
var cfg = MakeCfg(3.8f, 480, false);
WL.PixelArt.PixelCameraRig.Assemble(cfg, cam);
Chk(WL.PixelArt.PixelCameraRig.IsAssembled, "조립됨(C8 전)", "");
WL.PixelArt.WLPixelCameraSettings.RuntimeDisabled = true;
Chk(!WL.PixelArt.WLPixelCameraSettings.Enabled, "RuntimeDisabled → Enabled=false", "");
WL.PixelArt.PixelCameraRig.Tick(); // Active=false → 즉시 Teardown
Chk(!WL.PixelArt.PixelCameraRig.IsAssembled, "Tick 1회에 리그 해체", "");
string after = WL.PixelArt.PixelCameraRig.CurrentSummary(cam);
Chk(after == before, "C8 복원값 == 원값", after == before ? "" : "\n before=" + before + "\n after =" + after);
WL.PixelArt.PixelCameraRig.Tick(); // 재호출해도 조립 0
Chk(!WL.PixelArt.PixelCameraRig.IsAssembled, "C8 상태에서 재조립 0", "");
WL.PixelArt.WLPixelCameraSettings.RuntimeDisabled = false;
}
// ── G. 캡처 ──────────────────────────────────────────────────────────────
static void SectionG(UnityEngine.Camera cam)
{
L("");
L("## G. 룩 캡처 1080×1920 → " + kOutDir);
L(CaptureControl(cam, "look_00_persp_original"));
L(CaptureRig(cam, 3.8f, 480, "look_01_ortho38_px480"));
L(CaptureRig(cam, 3.8f, 360, "look_02_ortho38_px360"));
L(CaptureRig(cam, 4.6f, 480, "look_03_ortho46_px480"));
// 보너스 — 811o 구도의 실측 거리(카메라→주시점)에서 원근 fov 60 과 PC 평면 크기가 정확히 같아지는 ortho
float matched = UnityEngine.Vector3.Distance(cam.transform.position, s_lookAtWorld) * UnityEngine.Mathf.Tan(30f * UnityEngine.Mathf.Deg2Rad);
L(CaptureRig(cam, matched, 480, "look_04_orthoMatched_px480") + " ← 실측 정합값 ortho " + matched.ToString("F3"));
}
static string CaptureControl(UnityEngine.Camera cam, string tag)
{
var rt = new UnityEngine.RenderTexture(W, H, 24, UnityEngine.RenderTextureFormat.ARGB32);
var prevTarget = cam.targetTexture;
float prevAspect = cam.aspect;
cam.aspect = (float)W / H;
cam.targetTexture = rt;
cam.Render();
cam.targetTexture = prevTarget;
cam.aspect = prevAspect;
string path = WritePng(rt, tag);
rt.Release(); UnityEngine.Object.DestroyImmediate(rt);
return " 대조(원근 fov " + cam.fieldOfView + ") → " + path;
}
static string CaptureRig(UnityEngine.Camera cam, float orthoSize, int pixelHeight, string tag)
{
var cfg = MakeCfg(orthoSize, pixelHeight, false); // viewCameraInMainStack=false → 뷰 카메라를 Base 로 직접 렌더
if (cfg == null) return " FAIL cfg";
if (!WL.PixelArt.PixelCameraRig.Assemble(cfg, cam)) return " FAIL assemble " + tag;
var gameCam = WL.PixelArt.PixelCameraRig.GameCamera;
var viewCam = WL.PixelArt.PixelCameraRig.ViewCamera;
var mgr = WL.PixelArt.PixelCameraRig.Manager;
var cap = new UnityEngine.RenderTexture(W, H, 24, UnityEngine.RenderTextureFormat.ARGB32);
viewCam.aspect = (float)W / H;
viewCam.targetTexture = cap;
mgr.SendMessage("LateUpdate", UnityEngine.SendMessageOptions.DontRequireReceiver);
mgr.SendMessage("LateUpdate", UnityEngine.SendMessageOptions.DontRequireReceiver); // RT 생성 후 위치·줌 확정
var lowRt = gameCam.targetTexture;
gameCam.Render(); // 저해상도 RT 를 채운다
viewCam.Render(); // 쿼드를 1080×1920 으로 업스케일
string path = WritePng(cap, tag);
string info = " ortho " + orthoSize.ToString("0.##") + " × px " + pixelHeight
+ " → RT " + (lowRt == null ? "null" : lowRt.width + "×" + lowRt.height)
+ " · gameZoom " + mgr.GameCameraZoom.ToString("F4")
+ " · viewSize " + viewCam.orthographicSize.ToString("F4")
+ " → " + path;
viewCam.targetTexture = null;
cap.Release(); UnityEngine.Object.DestroyImmediate(cap);
WL.PixelArt.PixelCameraRig.Teardown("capture " + tag);
return info;
}
static string WritePng(UnityEngine.RenderTexture rt, string tag)
{
var prev = UnityEngine.RenderTexture.active;
UnityEngine.RenderTexture.active = rt;
var tex = new UnityEngine.Texture2D(rt.width, rt.height, UnityEngine.TextureFormat.RGB24, false);
tex.ReadPixels(new UnityEngine.Rect(0, 0, rt.width, rt.height), 0, 0);
tex.Apply();
UnityEngine.RenderTexture.active = prev;
var path = System.IO.Path.Combine(kOutDir, tag + ".png");
System.IO.File.WriteAllBytes(path, UnityEngine.ImageConversion.EncodeToPNG(tex));
UnityEngine.Object.DestroyImmediate(tex);
return path;
}
// ── 임시 설정(디스크 SO 를 절대 건드리지 않는다) ─────────────────────────
static WL.PixelArt.WLPixelCameraSettings MakeCfg(float orthoSize, int pixelHeight, bool inStack)
{
var rig = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(kRig);
if (rig == null) return null;
var cfg = UnityEngine.ScriptableObject.CreateInstance<WL.PixelArt.WLPixelCameraSettings>();
cfg.hideFlags = UnityEngine.HideFlags.HideAndDontSave;
cfg.pixelCameraSystemPrefab = rig;
cfg.orthoSize = orthoSize;
cfg.pixelHeight = pixelHeight;
cfg.viewCameraInMainStack = inStack;
cfg.verboseLog = false;
return cfg;
}
}