// PD 지시 #813 · 발주서 WL-811o — 카메라 가림 해소(구도 값 · 가림 소품 숨김 · 이펙트 스케일) 검증 프로브 // (에디트 모드 전용 · Play 0 · 로그인 0 · 에셋 무수정) // // 실행 (CLAUDE.md §1: using 금지 · 네임스페이스 풀어 쓰기) // unity command run_script --file AgentScripts/WL811o_Probe.cs --entry WL811o_Probe.Edit --timeout 1800 // // 무엇을 재는가 // A. 맵 — WL_Nature.prefab 인스턴스화 · 렌더러/콜라이더 수 · 레이어 분포 · 소품 셰이더 큐(숨김 방식 결정 근거) // B. 앵커 — 존1~4 스포너(813001~813004) 월드 좌표 + 아레나 5지점((139,0.8,−20) 중심 + 반경 10 의 4점) · 지면 스냅 // C. 가림률 — 9지점 × 8방향(카메라 yaw) × 9타겟(PC + 반경 4 m 8방향) = 648 선분 // ① 기하 가림(렌더러 바운드 · 콜라이더 없어도 잰다 = 화면에서 실제로 가리는 것) // ② 물리 가림(SphereCast · OccluderFade 가 실제로 잡을 수 있는 것) // 「전(원본 arr_cCamData[0] = 65° · 3 m · 2)」 vs 「후(SO 값)」 // D. 스윕 — cameraHeight × distance 격자의 기하 가림률(값 선택 근거) // E. 숨김 — 가짜 가림(큐브)으로 OccluderFade 숨김/복귀 · 동시 상한 · 제외 규칙(바닥·Actor·스킨드) // F. GC — EvaluateSegment 100회의 GC.GetAllocatedBytesForCurrentThread() 증가분(0 B 목표) // G. C8 — RuntimeDisabled = true → 구도 미적용 · 숨김 전부 원복(원본 카메라·소품 100 %) // H. 이펙트 — WLSkillSpectacleSettings 슬롯 4 본체 스케일 표(811i 튜닝 후보 적용 확인) // // 에디트 모드 주의 // · Time.unscaledTime 이 한 호출 안에서 멈춰 있다 → OccluderFade.TimeOverride 로 시뮬 시계를 민다(811gh·813s 방식). // · 맵 인스턴스는 HideAndDontSave + DestroyImmediate. 어떤 에셋도 쓰지 않는다(씬 dirty 전/후 보고). public static class WL811o_Probe { const string kOut = "AgentScripts/staging/WL811o/EDIT_PROBE.txt"; const string kMap = "Assets/Res_Addr/Map/WL_Nature.prefab"; const string kSpectacle = "Assets/WL/Combat/Settings/Resources/WL/WLSkillSpectacleSettings.asset"; // 원본 실측 (Assets/ResWork/Prefabs/Ingame/Main Camera.prefab · arr_cCamData[0]) const float kOrgHeight = 65f, kOrgDist = 3f, kOrgUp = 2f; const float kLookY = 1.06f; // RealCamera.m_LookAt.y 실측 const float kMapYaw = 233f; // WL_Nature MapZoneData.CamRot_Portal 실측 const float kRadius = 4f; // 발주서 §1-1 「PC 주변 반경 4 m」 const float kTargetY = 1.0f; // 반경 4 m 지점의 검사 높이(가슴 높이) static System.Collections.Generic.List s_log = new System.Collections.Generic.List(); static bool s_hooked; static void OnLog(string cond, string stack, UnityEngine.LogType t) { if (t == UnityEngine.LogType.Error || t == UnityEngine.LogType.Exception || t == UnityEngine.LogType.Assert) s_log.Add(t + " | " + cond); } static void Hook() { if (!s_hooked) { UnityEngine.Application.logMessageReceived += OnLog; s_hooked = true; } } static void Unhook() { if (s_hooked) { UnityEngine.Application.logMessageReceived -= OnLog; s_hooked = false; } } static UnityEngine.GameObject s_map; static System.Collections.Generic.List s_props = new System.Collections.Generic.List(); static System.Collections.Generic.List s_bounds = new System.Collections.Generic.List(); struct Anchor { public string name; public UnityEngine.Vector3 pos; } static System.Collections.Generic.List s_anchors = new System.Collections.Generic.List(); public static string Edit() { var sb = new System.Text.StringBuilder(); sb.Append("# WL-811o 프로브 (에디트 모드 · Play 0 · 로그인 0)\n"); sb.Append("unity=").Append(UnityEngine.Application.unityVersion) .Append(" isPlaying=").Append(UnityEngine.Application.isPlaying) .Append(" utc=").Append(System.DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss")).Append("\n"); bool dirtyBefore = UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene().isDirty; sb.Append("scene=").Append(UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene().name) .Append(" dirtyBefore=").Append(dirtyBefore).Append("\n\n"); // 에셋을 손으로 고친 직후에도 현재 값을 읽게 한다(정적 캐시 + 임포트) UnityEditor.AssetDatabase.Refresh(); WL.CameraFraming.WLCameraFramingSettings.ClearCache(); Hook(); s_log.Clear(); try { SectionA(sb); SectionB(sb); SectionC(sb); SectionD(sb); SectionE(sb); SectionE2(sb); SectionF(sb); SectionG(sb); SectionH(sb); } catch (System.Exception ex) { sb.Append("\n🔴 예외: ").Append(ex.ToString()).Append("\n"); } finally { Cleanup(); Unhook(); } bool dirtyAfter = UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene().isDirty; sb.Append("\n── 로그(Error/Exception) ").Append(s_log.Count).Append("건 ──\n"); for (int i = 0; i < s_log.Count && i < 20; i++) sb.Append(" ").Append(s_log[i]).Append("\n"); sb.Append("scene dirtyAfter=").Append(dirtyAfter).Append(" (before=").Append(dirtyBefore).Append(")\n"); Write(sb.ToString()); return sb.ToString(); } // ───────────────────────────────────────────────────────────────────────── // A. 맵 · 소품 // ───────────────────────────────────────────────────────────────────────── static void SectionA(System.Text.StringBuilder sb) { sb.Append("── A. 맵 · 소품 실측 ──\n"); var asset = UnityEditor.AssetDatabase.LoadAssetAtPath(kMap); if (asset == null) { sb.Append("🔴 ").Append(kMap).Append(" 로드 실패\n"); return; } s_map = UnityEngine.Object.Instantiate(asset); s_map.name = "[WL811o] MapProbe"; s_map.hideFlags = UnityEngine.HideFlags.HideAndDontSave; UnityEngine.Physics.SyncTransforms(); var rends = s_map.GetComponentsInChildren(true); var cols = s_map.GetComponentsInChildren(true); sb.Append("맵 인스턴스: 렌더러 ").Append(rends.Length).Append(" · 콜라이더 ").Append(cols.Length).Append("\n"); // 레이어 분포(콜라이더 기준 — SphereCast 가 볼 수 있는 것) var layerCount = new int[32]; for (int i = 0; i < cols.Length; i++) layerCount[cols[i].gameObject.layer]++; sb.Append("콜라이더 레이어: "); for (int i = 0; i < 32; i++) if (layerCount[i] > 0) sb.Append(i).Append('(').Append(UnityEngine.LayerMask.LayerToName(i)).Append(")=").Append(layerCount[i]).Append(' '); sb.Append("\n"); // 「가릴 수 있는」 소품 후보 = 높이 0.8 m 이상 · 최대 변 30 m 이하 · 이름 제외 규칙 밖 s_props.Clear(); s_bounds.Clear(); int tooLow = 0, tooBig = 0, byName = 0, noMesh = 0; for (int i = 0; i < rends.Length; i++) { var r = rends[i]; if (r == null) continue; if (r is UnityEngine.ParticleSystemRenderer || r is UnityEngine.TrailRenderer || r is UnityEngine.LineRenderer) { noMesh++; continue; } var b = r.bounds; if (b.size.y < 0.8f) { tooLow++; continue; } float side = UnityEngine.Mathf.Max(b.size.x, UnityEngine.Mathf.Max(b.size.y, b.size.z)); if (side > 30f) { tooBig++; continue; } if (Excluded(r.transform.name) || (r.transform.parent != null && Excluded(r.transform.parent.name))) { byName++; continue; } s_props.Add(r); s_bounds.Add(b); } sb.Append("가림 후보 렌더러 ").Append(s_props.Count) .Append(" (제외: 높이<0.8 ").Append(tooLow).Append(" · 최대변>30 ").Append(tooBig) .Append(" · 이름 ").Append(byName).Append(" · 이펙트 ").Append(noMesh).Append(")\n"); // 후보 중 콜라이더를 가진 비율 = OccluderFade(SphereCast)가 실제로 잡을 수 있는 비율 int withCol = 0; for (int i = 0; i < s_props.Count; i++) { var t = s_props[i].transform; if (t.GetComponent() != null || t.GetComponentInParent() != null) withCol++; } sb.Append("후보 중 콜라이더 보유 ").Append(withCol).Append(" / ").Append(s_props.Count) .Append(" (").Append(Pct(withCol, s_props.Count)).Append(")\n"); // 셰이더 큐 — 숨김 방식(Auto) 결정 근거 int opaque = 0, transparent = 0; string sample = ""; for (int i = 0; i < s_props.Count; i++) { var m = s_props[i].sharedMaterial; if (m == null) continue; if (m.renderQueue >= 3000) transparent++; else opaque++; if (sample.Length == 0 && m.shader != null) sample = s_props[i].transform.name + " / " + m.shader.name + " / queue " + m.renderQueue; } sb.Append("소품 머티리얼: 불투명 큐 ").Append(opaque).Append(" · 투명 큐 ").Append(transparent) .Append(" → hideMode Auto 는 ").Append(transparent > opaque ? "MpbAlpha" : "ShadowsOnly").Append(" 로 내려간다\n"); sb.Append("샘플: ").Append(sample.Length > 0 ? sample : "(없음)").Append("\n"); // OccluderFade 규칙(레이어·이름·크기·Actor)을 실제로 통과하는 수 = 런타임 스캔 목록 크기 예상치 var stA = WL.CameraFraming.WLCameraFramingSettings.Instance; if (stA != null) { int elig = 0; for (int i = 0; i < rends.Length; i++) if (rends[i] is UnityEngine.MeshRenderer && WL.CameraFraming.OccluderFade.Eligible(stA, rends[i], true)) elig++; sb.Append("OccluderFade 규칙 통과(런타임 스캔 목록 예상) ").Append(elig).Append(" / MeshRenderer ").Append(rends.Length).Append("\n"); WL.CameraFraming.OccluderFade.ResetDiagnostics(); } sb.Append("\n"); } static bool Excluded(string name) { if (string.IsNullOrEmpty(name)) return false; string[] toks = { "terrain", "ground", "floor", "water", "river", "navmesh", "plane", "road", "path" }; for (int i = 0; i < toks.Length; i++) if (name.IndexOf(toks[i], System.StringComparison.OrdinalIgnoreCase) >= 0) return true; return false; } // ───────────────────────────────────────────────────────────────────────── // B. 앵커 // ───────────────────────────────────────────────────────────────────────── static void SectionB(System.Text.StringBuilder sb) { sb.Append("── B. 앵커(존1~4 + 아레나 5지점) ──\n"); s_anchors.Clear(); if (s_map == null) { sb.Append("🔴 맵 인스턴스 없음\n\n"); return; } // 시작 지점 — 🔴 QA 캡처 01_enter_hud(가림 ≈40 %)가 찍힌 자리 var portal = FindByName(s_map.transform, "Portal_Start"); if (portal != null) Add("시작(Portal_Start)", portal.position); int[] zones = { 813001, 813002, 813003, 813004 }; var mgrs = s_map.GetComponentsInChildren(true); for (int z = 0; z < zones.Length; z++) { for (int i = 0; i < mgrs.Length; i++) { if (mgrs[i].SpawnerID != zones[z]) continue; Add("존" + (z + 1) + "(" + zones[z] + ")", mgrs[i].transform.position); break; } } // 아레나 = WLBossArenaSettings 실측(중심 (139,0.8,−20) · 반경 20) → 중심 + 반경 10 의 4점 var arena = new UnityEngine.Vector3(139f, 0.8f, -20f); Add("아레나-중심", arena); Add("아레나-N", arena + new UnityEngine.Vector3(0f, 0f, 10f)); Add("아레나-E", arena + new UnityEngine.Vector3(10f, 0f, 0f)); Add("아레나-S", arena + new UnityEngine.Vector3(0f, 0f, -10f)); Add("아레나-W", arena + new UnityEngine.Vector3(-10f, 0f, 0f)); for (int i = 0; i < s_anchors.Count; i++) sb.Append(" ").Append(s_anchors[i].name).Append(" = ").Append(V(s_anchors[i].pos)).Append("\n"); sb.Append("앵커 ").Append(s_anchors.Count).Append("개 (지면 스냅: 위 50 m 에서 아래로 레이캐스트 · 실패 시 원좌표)\n\n"); } static UnityEngine.Transform FindByName(UnityEngine.Transform root, string name) { var all = root.GetComponentsInChildren(true); for (int i = 0; i < all.Length; i++) if (all[i].name == name) return all[i]; return null; } /// 지면 스냅 — 소품 꼭대기·메시 뒷면에 얹히지 않게 **원 좌표 y 에 가장 가까운** 히트를 쓴다(±5 m 안에서만). static void Add(string name, UnityEngine.Vector3 p) { var from = new UnityEngine.Vector3(p.x, p.y + 50f, p.z); var hits = UnityEngine.Physics.RaycastAll(from, UnityEngine.Vector3.down, 200f, ~0, UnityEngine.QueryTriggerInteraction.Ignore); float best = 0f, bestD = float.MaxValue; for (int i = 0; i < hits.Length; i++) { float d = UnityEngine.Mathf.Abs(hits[i].point.y - p.y); if (d < bestD) { bestD = d; best = hits[i].point.y; } } if (bestD <= 5f) p.y = best; var a = new Anchor(); a.name = name; a.pos = p; s_anchors.Add(a); } // ───────────────────────────────────────────────────────────────────────── // C. 가림률 전/후 // ───────────────────────────────────────────────────────────────────────── static void SectionC(System.Text.StringBuilder sb) { sb.Append("── C. 가림률 (9지점 × 8방향 × 9타겟 = ").Append(s_anchors.Count * 8 * 9).Append(" 선분) ──\n"); var st = WL.CameraFraming.WLCameraFramingSettings.Instance; if (st == null) { sb.Append("🔴 WLCameraFramingSettings.asset 없음\n\n"); return; } float nh = st.cameraHeight, nd = st.distance, nu = st.distanceUp >= 0f ? st.distanceUp : kOrgUp; sb.Append("전(원본 arr_cCamData[0]) h=").Append(kOrgHeight).Append(" d=").Append(kOrgDist).Append(" up=").Append(kOrgUp) .Append(" · 내림각 ").Append(Down(kOrgHeight, kOrgDist, kOrgUp).ToString("0.0")).Append("°\n"); sb.Append("후(SO 값) h=").Append(nh).Append(" d=").Append(nd).Append(" up=").Append(nu) .Append(" · 내림각 ").Append(Down(nh, nd, nu).ToString("0.0")).Append("°\n\n"); sb.Append("| 지점 | 전(원본 값) | 후(값만) | 후(값+숨김) | 참고: 물리 전/후 |\n|---|---|---|---|---|\n"); int gb = 0, ga = 0, gh = 0, pb = 0, pa = 0, tot = 0; s_maxBlockers = 0; int pcT0 = 0, pcG0 = 0, pcT1 = 0, pcG1 = 0, pcA1 = 0; for (int i = 0; i < s_anchors.Count; i++) { int t1, g1, g2, h1, h2, p1, p2; s_pcTotal = s_pcGeo = s_pcAfter = 0; Measure(s_anchors[i].pos, kOrgHeight, kOrgDist, kOrgUp, out t1, out g1, out h1, out p1); pcT0 += s_pcTotal; pcG0 += s_pcGeo; s_pcTotal = s_pcGeo = s_pcAfter = 0; Measure(s_anchors[i].pos, nh, nd, nu, out t1, out g2, out h2, out p2); pcT1 += s_pcTotal; pcG1 += s_pcGeo; pcA1 += s_pcAfter; gb += g1; ga += g2; gh += h2; pb += p1; pa += p2; tot += t1; sb.Append("| ").Append(s_anchors[i].name) .Append(" | ").Append(Pct(g1, t1)).Append(" | ").Append(Pct(g2, t1)).Append(" | ").Append(Pct(h2, t1)) .Append(" | ").Append(Pct(p1, t1)).Append(" → ").Append(Pct(p2, t1)).Append(" |\n"); } sb.Append("| **합계** | **").Append(Pct(gb, tot)).Append("** | **").Append(Pct(ga, tot)) .Append("** | **").Append(Pct(gh, tot)).Append("** | ").Append(Pct(pb, tot)).Append(" → ").Append(Pct(pa, tot)).Append(" |\n"); sb.Append("가림 = 렌더러 바운드 교차(콜라이더 없어도 화면에서 가린다) · 「값+숨김」 = 막은 소품이 전부 OccluderFade 규칙을 통과하고 동시 상한 이내면 해소된 것으로 센다\n"); sb.Append("물리 = SphereCast(콜라이더만) — 지형 경사면 포함이라 값과 무관하게 오르내린다\n"); sb.Append("🔴 **PC 중심 선분만**(런타임 OccluderFade 가 실제로 쏘는 그 선분) 전 ").Append(Pct(pcG0, pcT0)) .Append(" → 값만 ").Append(Pct(pcG1, pcT1)).Append(" → 값+숨김 ").Append(Pct(pcA1, pcT1)).Append("\n"); sb.Append("한 선분을 막은 소품 최대 수 ").Append(s_maxBlockers).Append("개(동시 상한 ").Append(st.maxConcurrent).Append(")\n"); sb.Append("⚠ 반경 4 m 타겟은 OccluderFade 가 직접 쏘지 않는다 — 같은 소품이 PC 선분도 막을 때만 함께 해소된다(추정)\n\n"); } static int s_pcTotal, s_pcGeo, s_pcAfter, s_maxBlockers; // PC 중심 선분만(= OccluderFade 가 실제로 쏘는 선분) /// 한 지점의 8방향 × 9타겟 선분 중 몇 개가 가리는지. afterHide = OccluderFade 로도 안 풀리는 수. static void Measure(UnityEngine.Vector3 foot, float h, float d, float up, out int total, out int geo, out int afterHide, out int phys) { total = 0; geo = 0; afterHide = 0; phys = 0; var st = WL.CameraFraming.WLCameraFramingSettings.Instance; int mask = st != null ? st.occluderMask.value : ((1 << 0) | (1 << 6)); float radius = st != null ? st.sphereRadius : 0.6f; // 이 지점 주변 후보만 추린다(성능) var near = new System.Collections.Generic.List(); for (int i = 0; i < s_bounds.Count; i++) { var c = s_bounds[i].center; float dx = c.x - foot.x, dz = c.z - foot.z; if (dx * dx + dz * dz < 400f) near.Add(i); // 20 m 이내 } for (int y = 0; y < 8; y++) { float yaw = kMapYaw + y * 45f; var cam = WL.CameraFraming.CameraFramingOverride.CameraPosition(foot, yaw, h, d, up); for (int t = 0; t < 9; t++) { UnityEngine.Vector3 aim; if (t == 0) aim = foot + new UnityEngine.Vector3(0f, kLookY, 0f); else { float a = (t - 1) * 45f * UnityEngine.Mathf.Deg2Rad; aim = foot + new UnityEngine.Vector3(UnityEngine.Mathf.Sin(a) * kRadius, kTargetY, UnityEngine.Mathf.Cos(a) * kRadius); } total++; var dir = aim - cam; float len = dir.magnitude; if (len < 0.001f) continue; dir /= len; var ray = new UnityEngine.Ray(cam, dir); // ① 기하 — 렌더러 바운드 교차 (선분 끝 0.5 m 는 PC 자신이라 제외) int blockers = 0, hideable = 0; for (int k = 0; k < near.Count; k++) { float hitDist; var b = s_bounds[near[k]]; b.Expand(radius * 2f); // 음수 거리 = 카메라가 그 소품 안에 있다 = 확실한 가림(버리지 않는다 · OccluderFade 와 같은 판정) if (!b.IntersectRay(ray, out hitDist) || hitDist >= len - 0.5f) continue; blockers++; if (WL.CameraFraming.OccluderFade.Eligible(st, s_props[near[k]], true)) hideable++; } int capN = st != null ? st.maxConcurrent : 4; bool solvedRay = blockers > 0 && hideable == blockers && blockers <= capN; if (blockers > 0) { geo++; if (!solvedRay) afterHide++; if (blockers > s_maxBlockers) s_maxBlockers = blockers; } if (t == 0) // PC 중심 선분 = 런타임 OccluderFade 가 실제로 쏘는 그 선분 { s_pcTotal++; if (blockers > 0) { s_pcGeo++; if (!solvedRay) s_pcAfter++; } } // ② 물리 — SphereCast(런타임 OccluderFade 와 같은 조건) UnityEngine.RaycastHit rh; if (UnityEngine.Physics.SphereCast(cam + dir * 0.2f, radius, dir, out rh, UnityEngine.Mathf.Max(0f, len - 0.7f), mask, UnityEngine.QueryTriggerInteraction.Ignore)) phys++; } } } // ───────────────────────────────────────────────────────────────────────── // D. 값 스윕 // ───────────────────────────────────────────────────────────────────────── static void SectionD(System.Text.StringBuilder sb) { sb.Append("── D. 값 스윕 (기하 가림률 · 전 지점 합계) ──\n"); float[] hs = { 65f, 70f, 74f, 76f, 78f, 80f }; float[] ds = { 3.0f, 3.4f }; float[] ups = { 2.0f, 2.4f, 2.8f }; sb.Append("| h | d | up 2.0 | up 2.4 | up 2.8 | 내림각(up 2.4) | PC선분만(d·up 최적) |\n|---|---|---|---|---|---|---|\n"); float bestPct = 999f; string bestSet = ""; for (int i = 0; i < hs.Length; i++) { for (int j = 0; j < ds.Length; j++) { sb.Append("| ").Append(hs[i]).Append(" | ").Append(ds[j]); int pcT = 0, pcG = 0; for (int u = 0; u < ups.Length; u++) { int tot = 0, geo = 0, t1, g1, h1, p1; s_pcTotal = s_pcGeo = s_pcAfter = 0; for (int a = 0; a < s_anchors.Count; a++) { Measure(s_anchors[a].pos, hs[i], ds[j], ups[u], out t1, out g1, out h1, out p1); tot += t1; geo += g1; } if (u == 1) { pcT = s_pcTotal; pcG = s_pcGeo; } sb.Append(" | ").Append(Pct(geo, tot)); float pct = tot > 0 ? 100f * geo / tot : 999f; if (pct < bestPct) { bestPct = pct; bestSet = "h " + hs[i] + " · d " + ds[j] + " · up " + ups[u]; } } sb.Append(" | ").Append(Down(hs[i], ds[j], 2.4f).ToString("0.0")).Append("°") .Append(" | ").Append(Pct(pcG, pcT)).Append(" |\n"); } } sb.Append("최소 = ").Append(bestSet).Append(" (").Append(bestPct.ToString("0.0")).Append("%) · 원본은 h 65 · d 3.0 · up 2.0\n\n"); } // ───────────────────────────────────────────────────────────────────────── // E. OccluderFade 가짜 가림 // ───────────────────────────────────────────────────────────────────────── static void SectionE(System.Text.StringBuilder sb) { sb.Append("── E. OccluderFade 가짜 가림 → 숨김/복귀 ──\n"); var st = WL.CameraFraming.WLCameraFramingSettings.Instance; if (st == null) { sb.Append("🔴 SO 없음\n\n"); return; } // 🔴 hideFlags 를 주지 않는다 — FindObjectsByType 이 DontSave 오브젝트를 빼기 때문에 // 바운드 스캔 경로(ⓑ)를 검증하려면 평범한 오브젝트여야 한다. 씬은 저장하지 않는다(끝에 파괴 + dirty 보고). var root = new UnityEngine.GameObject("[WL811o] FakeOcc"); var far = new UnityEngine.Vector3(0f, 0f, 3000f); // 맵에서 멀리 떨어진 빈 공간 var cam = far + new UnityEngine.Vector3(0f, 4.5f, -5.4f); var aim = far + new UnityEngine.Vector3(0f, 1.06f, 0f); try { WL.CameraFraming.WLCameraFramingSettings.RuntimeDisabled = false; WL.CameraFraming.OccluderFade.ResetDiagnostics(); WL.CameraFraming.OccluderFade.TimeOverride = 100f; // 큐브 6개를 선분 위에 나란히 — 동시 상한(기본 4) 검증 var cubes = new UnityEngine.GameObject[6]; for (int i = 0; i < cubes.Length; i++) { var c = UnityEngine.GameObject.CreatePrimitive(UnityEngine.PrimitiveType.Cube); c.name = "FakeRock_" + i; c.transform.SetParent(root.transform, true); c.transform.position = UnityEngine.Vector3.Lerp(cam, aim, 0.25f + i * 0.08f) + new UnityEngine.Vector3(i * 0.03f, 0f, 0f); c.transform.localScale = new UnityEngine.Vector3(1.4f, 2.2f, 1.4f); c.layer = 0; cubes[i] = c; } // 🔴 콜라이더 없는 소품 — WL_Nature 소품의 87.6 % 가 이 상태다. 바운드 스캔(ⓑ)만이 잡을 수 있다. UnityEngine.Object.DestroyImmediate(cubes[0].GetComponent()); // 제외 규칙 검증용 — 바닥 이름 var floor = UnityEngine.GameObject.CreatePrimitive(UnityEngine.PrimitiveType.Cube); floor.name = "Ground_Fake"; floor.transform.SetParent(root.transform, true); floor.transform.position = UnityEngine.Vector3.Lerp(cam, aim, 0.5f); floor.transform.localScale = new UnityEngine.Vector3(2f, 2f, 2f); floor.layer = 0; UnityEngine.Physics.SyncTransforms(); WL.CameraFraming.OccluderFade.ClearScan(); WL.CameraFraming.OccluderFade.RebuildScan(); sb.Append("바운드 스캔 목록 ").Append(WL.CameraFraming.OccluderFade.ScanCount) .Append("개 (FakeRock 6 + Ground_Fake 는 이름 규칙으로 빠진다)\n"); sb.Append("hideMode(SO)=").Append(st.hideMode).Append(" · maxConcurrent=").Append(st.maxConcurrent) .Append(" · sphereRadius=").Append(st.sphereRadius).Append(" · fadeOut=").Append(st.fadeOutSeconds) .Append(" · restoreDelay=").Append(st.restoreDelaySeconds).Append("\n"); // t=100 첫 관측(아직 숨기지 않는다 = 지연) → t=100.25 숨김 WL.CameraFraming.OccluderFade.EvaluateSegment(cam, aim); int hidden0 = WL.CameraFraming.OccluderFade.ActiveHidden; WL.CameraFraming.OccluderFade.TimeOverride = 100f + st.fadeOutSeconds + 0.05f; WL.CameraFraming.OccluderFade.EvaluateSegment(cam, aim); int hidden1 = WL.CameraFraming.OccluderFade.ActiveHidden; sb.Append("첫 관측 직후 숨김 ").Append(hidden0).Append("(기대 0 = ").Append(st.fadeOutSeconds).Append(" s 지연)"); sb.Append(" → 지연 뒤 숨김 ").Append(hidden1).Append("(기대 ").Append(st.maxConcurrent).Append(" = 동시 상한)\n"); sb.Append("숨긴 것: "); for (int i = 0; i < 8; i++) { var n = WL.CameraFraming.OccluderFade.SlotName(i); if (n.Length > 0) sb.Append(n).Append(WL.CameraFraming.OccluderFade.SlotHidden(i) ? "(hidden) " : "(대기) "); } sb.Append("\n"); sb.Append("바닥 이름 제외(Ground_Fake 가 숨김 목록에 없어야 한다): ") .Append(HasSlot("Ground_Fake") ? "🔴 실패" : "PASS").Append("\n"); // 실제 상태 확인 int shadowOnly = 0, off = 0; for (int i = 0; i < cubes.Length; i++) { var r = cubes[i].GetComponent(); if (r == null) continue; if (!r.enabled) off++; else if (r.shadowCastingMode == UnityEngine.Rendering.ShadowCastingMode.ShadowsOnly) shadowOnly++; } sb.Append("렌더러 상태: ShadowsOnly ").Append(shadowOnly).Append(" · enabled=false ").Append(off).Append("\n"); // 가림 해제 → 복귀 WL.CameraFraming.OccluderFade.TimeOverride = 200f; WL.CameraFraming.OccluderFade.EvaluateSegment(cam + new UnityEngine.Vector3(500f, 0f, 0f), aim + new UnityEngine.Vector3(500f, 0f, 0f)); int hidden2 = WL.CameraFraming.OccluderFade.ActiveHidden; int restoredOk = 0; for (int i = 0; i < cubes.Length; i++) { var r = cubes[i].GetComponent(); if (r != null && r.enabled && r.shadowCastingMode == UnityEngine.Rendering.ShadowCastingMode.On) restoredOk++; } sb.Append("가림 해제 후 숨김 ").Append(hidden2).Append("(기대 0) · 원상복귀 렌더러 ") .Append(restoredOk).Append(" / ").Append(cubes.Length).Append("\n"); // 콜라이더 없는 소품 단독 검증 — 물리 경로가 슬롯을 다 채우지 못하게 큐브 1개만 둔다 for (int i = 1; i < cubes.Length; i++) cubes[i].SetActive(false); UnityEngine.GameObject.DestroyImmediate(floor); UnityEngine.Physics.SyncTransforms(); WL.CameraFraming.OccluderFade.ReleaseAll(); WL.CameraFraming.OccluderFade.ClearScan(); WL.CameraFraming.OccluderFade.RebuildScan(); WL.CameraFraming.OccluderFade.TimeOverride = 300f; WL.CameraFraming.OccluderFade.EvaluateSegment(cam, aim); WL.CameraFraming.OccluderFade.TimeOverride = 300.5f; WL.CameraFraming.OccluderFade.EvaluateSegment(cam, aim); var r0 = cubes[0].GetComponent(); bool onlyNoCol = HasSlot("FakeRock_0") && r0.shadowCastingMode == UnityEngine.Rendering.ShadowCastingMode.ShadowsOnly; sb.Append("콜라이더 없는 소품 단독(FakeRock_0 · 콜라이더 ") .Append(cubes[0].GetComponent() == null ? "0" : "1") .Append("): ").Append(onlyNoCol ? "PASS(바운드 스캔이 잡아 숨겼다)" : "🔴 실패") .Append(" · 숨김 ").Append(WL.CameraFraming.OccluderFade.ActiveHidden) .Append(" · shadow=").Append(r0.shadowCastingMode) .Append(" · boundsScanEnabled=").Append(st.boundsScanEnabled) .Append(" · 스캔 ").Append(WL.CameraFraming.OccluderFade.ScanCount) .Append(" · Eligible=").Append(WL.CameraFraming.OccluderFade.Eligible(st, r0, true)).Append("\n"); sb.Append("카운터: polls=").Append(WL.CameraFraming.OccluderFade.Polls) .Append(" blocked=").Append(WL.CameraFraming.OccluderFade.Blocked) .Append(" hidden=").Append(WL.CameraFraming.OccluderFade.HiddenCount) .Append(" restored=").Append(WL.CameraFraming.OccluderFade.RestoredCount) .Append(" capSkip=").Append(WL.CameraFraming.OccluderFade.CapSkips) .Append(" rejected=").Append(WL.CameraFraming.OccluderFade.Rejected).Append("\n\n"); } finally { WL.CameraFraming.OccluderFade.ReleaseAll(); WL.CameraFraming.OccluderFade.TimeOverride = -1f; UnityEngine.Object.DestroyImmediate(root); } } static bool HasSlot(string name) { for (int i = 0; i < 8; i++) if (WL.CameraFraming.OccluderFade.SlotName(i) == name) return true; return false; } // ───────────────────────────────────────────────────────────────────────── // F. GC // ───────────────────────────────────────────────────────────────────────── static void SectionF(System.Text.StringBuilder sb) { sb.Append("── F. GC (EvaluateSegment 100회) ──\n"); var root = new UnityEngine.GameObject("[WL811o] GcOcc"); // 바운드 스캔까지 재려면 평범한 오브젝트여야 한다 var far = new UnityEngine.Vector3(0f, 0f, 4000f); var cam = far + new UnityEngine.Vector3(0f, 4.5f, -5.4f); var aim = far + new UnityEngine.Vector3(0f, 1.06f, 0f); try { for (int i = 0; i < 3; i++) { var c = UnityEngine.GameObject.CreatePrimitive(UnityEngine.PrimitiveType.Cube); c.name = "GcRock_" + i; c.transform.SetParent(root.transform, true); c.transform.position = UnityEngine.Vector3.Lerp(cam, aim, 0.3f + i * 0.1f); c.transform.localScale = new UnityEngine.Vector3(1.4f, 2.2f, 1.4f); c.layer = 0; } UnityEngine.Physics.SyncTransforms(); WL.CameraFraming.OccluderFade.ResetDiagnostics(); WL.CameraFraming.OccluderFade.TimeOverride = 1000f; WL.CameraFraming.OccluderFade.ClearScan(); WL.CameraFraming.OccluderFade.RebuildScan(); // 스캔은 여기서 1회(측정 구간 밖) WL.CameraFraming.OccluderFade.EvaluateSegment(cam, aim); // 워밍업(토큰 분해·MPB 최초 할당) WL.CameraFraming.OccluderFade.TimeOverride = 1001f; WL.CameraFraming.OccluderFade.EvaluateSegment(cam, aim); long before = System.GC.GetAllocatedBytesForCurrentThread(); for (int i = 0; i < 100; i++) { WL.CameraFraming.OccluderFade.TimeOverride = 1001f + i * 0.01f; WL.CameraFraming.OccluderFade.EvaluateSegment(cam, aim); } long after = System.GC.GetAllocatedBytesForCurrentThread(); sb.Append("증가분 ").Append(after - before).Append(" B (100회 · 목표 0) · 스캔 목록 ") .Append(WL.CameraFraming.OccluderFade.ScanCount).Append("개 · 측정 중 재구축 ") .Append(WL.CameraFraming.OccluderFade.ScanBuilds - 1).Append("회(기대 0)\n\n"); } finally { WL.CameraFraming.OccluderFade.ReleaseAll(); WL.CameraFraming.OccluderFade.TimeOverride = -1f; UnityEngine.Object.DestroyImmediate(root); } } // ───────────────────────────────────────────────────────────────────────── // E2. 구도 오버라이드 적용 · 복원 (가짜 RealCamera · 원본 프리팹 무수정) // ───────────────────────────────────────────────────────────────────────── static void SectionE2(System.Text.StringBuilder sb) { sb.Append("── E2. 구도 오버라이드 적용/복원 ──\n"); var st = WL.CameraFraming.WLCameraFramingSettings.Instance; if (st == null) { sb.Append("🔴 SO 없음\n\n"); return; } var go = new UnityEngine.GameObject("[WL811o] FakeRealCamera"); go.hideFlags = UnityEngine.HideFlags.HideAndDontSave; try { // 🔴 에디트 모드에서는 MonoBehaviour Awake 가 돌지 않는다([ExecuteAlways] 아님) → 원본 초기화 부작용 0 var cam = go.AddComponent(); cam.arr_cCamData = new cCamData[2]; cam.arr_cCamData[0] = new cCamData(); // 원본 프리팹 실측값 cam.arr_cCamData[0].minDistance = 3f; cam.arr_cCamData[0].maxDistance = 3f; cam.arr_cCamData[0].DistanceUp = 2f; cam.arr_cCamData[0].cameraHeight = 65f; cam.arr_cCamData[1] = new cCamData(); cam.arr_cCamData[1].minDistance = 2f; cam.arr_cCamData[1].maxDistance = 3f; cam.arr_cCamData[1].DistanceUp = 3.5f; cam.arr_cCamData[1].cameraHeight = 78f; cam.minDistance = 3f; cam.maxDistance = 3f; cam.DistanceUp = 2f; cam.cameraHeight = 65f; WL.CameraFraming.WLCameraFramingSettings.RuntimeDisabled = false; WL.CameraFraming.CameraFramingOverride.ResetDiagnostics(); bool ok = WL.CameraFraming.CameraFramingOverride.ProbeApply(cam); sb.Append("적용 ").Append(ok ? "O" : "🔴 X") .Append(" · arr_cCamData[0] h=").Append(cam.arr_cCamData[0].cameraHeight) .Append(" min=").Append(cam.arr_cCamData[0].minDistance) .Append(" max=").Append(cam.arr_cCamData[0].maxDistance) .Append(" up=").Append(cam.arr_cCamData[0].DistanceUp).Append("\n"); sb.Append("라이브 필드 h=").Append(cam.cameraHeight).Append(" min=").Append(cam.minDistance) .Append(" max=").Append(cam.maxDistance).Append(" up=").Append(cam.DistanceUp) .Append(" · 내림각 ").Append(Down(cam.cameraHeight, cam.maxDistance, cam.DistanceUp).ToString("0.0")).Append("°\n"); sb.Append("보스 프리셋 arr_cCamData[1] 무변경: ") .Append(cam.arr_cCamData[1].cameraHeight == 78f && cam.arr_cCamData[1].maxDistance == 3f ? "PASS" : "🔴 실패").Append("\n"); // C8 — SO 를 끄고 틱 1회 → 원값 복귀 WL.CameraFraming.WLCameraFramingSettings.RuntimeDisabled = true; WL.CameraFraming.CameraFramingOverride.ForceTick(); bool restored = cam.arr_cCamData[0].cameraHeight == 65f && cam.arr_cCamData[0].maxDistance == 3f && cam.arr_cCamData[0].DistanceUp == 2f && cam.cameraHeight == 65f; sb.Append("C8 off → arr_cCamData[0] h=").Append(cam.arr_cCamData[0].cameraHeight) .Append(" max=").Append(cam.arr_cCamData[0].maxDistance).Append(" up=").Append(cam.arr_cCamData[0].DistanceUp) .Append(" · 라이브 h=").Append(cam.cameraHeight) .Append(" → ").Append(restored ? "PASS(원본 100 %)" : "🔴 실패").Append("\n"); sb.Append("카운터: boundaries=").Append(WL.CameraFraming.CameraFramingOverride.Boundaries) .Append(" applied=").Append(WL.CameraFraming.CameraFramingOverride.Applied) .Append(" restored=").Append(WL.CameraFraming.CameraFramingOverride.Restored) .Append(" skippedBusy=").Append(WL.CameraFraming.CameraFramingOverride.SkippedBusy).Append("\n\n"); } finally { WL.CameraFraming.WLCameraFramingSettings.RuntimeDisabled = false; WL.CameraFraming.CameraFramingOverride.ProbeRestore(); UnityEngine.Object.DestroyImmediate(go); } } // ───────────────────────────────────────────────────────────────────────── // G. C8 // ───────────────────────────────────────────────────────────────────────── static void SectionG(System.Text.StringBuilder sb) { sb.Append("── G. C8 롤백 ──\n"); var root = new UnityEngine.GameObject("[WL811o] C8Occ"); root.hideFlags = UnityEngine.HideFlags.HideAndDontSave; var far = new UnityEngine.Vector3(0f, 0f, 5000f); var cam = far + new UnityEngine.Vector3(0f, 4.5f, -5.4f); var aim = far + new UnityEngine.Vector3(0f, 1.06f, 0f); try { var c = UnityEngine.GameObject.CreatePrimitive(UnityEngine.PrimitiveType.Cube); c.name = "C8Rock"; c.transform.SetParent(root.transform, true); c.transform.position = UnityEngine.Vector3.Lerp(cam, aim, 0.4f); c.transform.localScale = new UnityEngine.Vector3(1.4f, 2.2f, 1.4f); c.layer = 0; UnityEngine.Physics.SyncTransforms(); var rnd = c.GetComponent(); WL.CameraFraming.WLCameraFramingSettings.RuntimeDisabled = false; WL.CameraFraming.OccluderFade.ResetDiagnostics(); WL.CameraFraming.OccluderFade.TimeOverride = 2000f; WL.CameraFraming.OccluderFade.EvaluateSegment(cam, aim); WL.CameraFraming.OccluderFade.TimeOverride = 2000.5f; WL.CameraFraming.OccluderFade.EvaluateSegment(cam, aim); sb.Append("on : 숨김 ").Append(WL.CameraFraming.OccluderFade.ActiveHidden) .Append(" · 렌더러 shadow=").Append(rnd.shadowCastingMode).Append(" enabled=").Append(rnd.enabled).Append("\n"); WL.CameraFraming.WLCameraFramingSettings.RuntimeDisabled = true; sb.Append("RuntimeDisabled=true → Active(occ)=").Append(WL.CameraFraming.OccluderFade.Active) .Append(" Active(framing)=").Append(WL.CameraFraming.CameraFramingOverride.Active).Append("\n"); WL.CameraFraming.OccluderFade.ForceTick(); // Active=false 경로 → ReleaseAll WL.CameraFraming.CameraFramingOverride.ForceTick(); // Active=false 경로 → 구도 복원 sb.Append("off: 숨김 ").Append(WL.CameraFraming.OccluderFade.ActiveHidden) .Append(" · 렌더러 shadow=").Append(rnd.shadowCastingMode).Append(" enabled=").Append(rnd.enabled) .Append(" · framing applied=").Append(WL.CameraFraming.CameraFramingOverride.IsApplied).Append("\n"); sb.Append(rnd.enabled && rnd.shadowCastingMode == UnityEngine.Rendering.ShadowCastingMode.On ? "→ PASS (원본 소품 100 %)\n\n" : "→ 🔴 실패\n\n"); } finally { WL.CameraFraming.WLCameraFramingSettings.RuntimeDisabled = false; WL.CameraFraming.OccluderFade.ReleaseAll(); WL.CameraFraming.OccluderFade.TimeOverride = -1f; UnityEngine.Object.DestroyImmediate(root); } } // ───────────────────────────────────────────────────────────────────────── // H. 이펙트 스케일 // ───────────────────────────────────────────────────────────────────────── static void SectionH(System.Text.StringBuilder sb) { sb.Append("── H. 대형 이펙트 스케일(슬롯 4 본체) ──\n"); var so = UnityEditor.AssetDatabase.LoadAssetAtPath(kSpectacle); if (so == null) { sb.Append("🔴 ").Append(kSpectacle).Append(" 로드 실패\n\n"); return; } var sobj = new UnityEditor.SerializedObject(so); var rows = sobj.FindProperty("rows"); if (rows == null || !rows.isArray) { sb.Append("🔴 rows 배열 없음\n\n"); return; } sb.Append("| skillId | slot | body.scale |\n|---|---|---|\n"); for (int i = 0; i < rows.arraySize; i++) { var e = rows.GetArrayElementAtIndex(i); var slot = e.FindPropertyRelative("slot"); if (slot == null || slot.intValue != 4) continue; var id = e.FindPropertyRelative("skillId"); var body = e.FindPropertyRelative("body"); var sc = body != null ? body.FindPropertyRelative("scale") : null; sb.Append("| ").Append(id != null ? id.intValue.ToString() : "?") .Append(" | 4 | ").Append(sc != null ? sc.floatValue.ToString("0.###") : "?").Append(" |\n"); } sb.Append("\n"); } // ───────────────────────────────────────────────────────────────────────── // 공용 // ───────────────────────────────────────────────────────────────────────── static float Down(float h, float d, float up) { return WL.CameraFraming.CameraFramingOverride.DownAngleDeg(h, d, up, kLookY); } static string Pct(int n, int total) { if (total <= 0) return "-"; return (100f * n / total).ToString("0.0") + "% (" + n + "/" + total + ")"; } static string V(UnityEngine.Vector3 p) { return "(" + p.x.ToString("0.0") + ", " + p.y.ToString("0.00") + ", " + p.z.ToString("0.0") + ")"; } static void Cleanup() { if (s_map != null) { UnityEngine.Object.DestroyImmediate(s_map); s_map = null; } s_props.Clear(); s_bounds.Clear(); } static void Write(string text) { try { var full = System.IO.Path.Combine(System.IO.Directory.GetParent(UnityEngine.Application.dataPath).FullName, kOut); System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(full)); System.IO.File.WriteAllText(full, text, new System.Text.UTF8Encoding(true)); } catch (System.Exception) { } } }