// 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(kSo); bool created = false; if (so == null) { so = UnityEngine.ScriptableObject.CreateInstance(); UnityEditor.AssetDatabase.CreateAsset(so, kSo); created = true; } var rig = UnityEditor.AssetDatabase.LoadAssetAtPath(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(); try { BuildScene(ref mapGo, ref pcGo, ref camGo, ref lightGo, mobs); if (camGo == null) { L("FAIL 카메라 인스턴스 실패 — 중단"); } else { var cam = camGo.GetComponent(); 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(kCam); if (camPf == null) { L("FAIL Main Camera.prefab 로드 실패"); fail++; return; } var c = camPf.GetComponent(); var rc = camPf.GetComponent("RealCamera"); var al = camPf.GetComponent(); var mrv = camPf.GetComponent("MeshRendererVisibility"); var ucd = camPf.GetComponent(); 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("Assets/Res_Addr/MainUI/NewGameUI.prefab"); if (uiPf != null) { var canvases = uiPf.GetComponentsInChildren(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(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 mobs) { L(""); L("## B. 임시 씬(추가 로드 · 저장 0)"); var mapPf = UnityEditor.AssetDatabase.LoadAssetAtPath(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(true).Length)); // 지면 y 찾기 + 소품이 가장 많은 앵커 고르기(룩 캡처라 배경이 비어 있으면 안 된다) UnityEngine.Physics.SyncTransforms(); var rends = mapGo == null ? new UnityEngine.Renderer[0] : mapGo.GetComponentsInChildren(true); // 소품이 실제로 모여 있는 곳을 찾는다 — 소품 중심들 중 반경 10 m 이웃이 가장 많은 지점 var cands = new System.Collections.Generic.List(); 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(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(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(); 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(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(); 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"); } /// 에디트 모드에서는 Animator 가 돌지 않아 T 포즈가 된다 → idle 클립을 1프레임 샘플링한다. static string PoseIdle(UnityEngine.GameObject go) { var anim = go.GetComponentInChildren(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 + "클립)"; } /// 임시 씬에서 원본 로직이 돌지 않게 MonoBehaviour 를 전부 끈다(에셋 무변경 · 인스턴스만). static void StripBehaviours(UnityEngine.GameObject go) { var all = go.GetComponentsInChildren(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(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("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(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(); 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(); 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(kRig); if (rig == null) return null; var cfg = UnityEngine.ScriptableObject.CreateInstance(); cfg.hideFlags = UnityEngine.HideFlags.HideAndDontSave; cfg.pixelCameraSystemPrefab = rig; cfg.orthoSize = orthoSize; cfg.pixelHeight = pixelHeight; cfg.viewCameraInMainStack = inStack; cfg.verboseLog = false; return cfg; } }