Project_WL/AgentScripts/WL811o_Probe.cs

764 lines
46 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 지시 #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<string> s_log = new System.Collections.Generic.List<string>();
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<UnityEngine.Renderer> s_props = new System.Collections.Generic.List<UnityEngine.Renderer>();
static System.Collections.Generic.List<UnityEngine.Bounds> s_bounds = new System.Collections.Generic.List<UnityEngine.Bounds>();
struct Anchor { public string name; public UnityEngine.Vector3 pos; }
static System.Collections.Generic.List<Anchor> s_anchors = new System.Collections.Generic.List<Anchor>();
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<UnityEngine.GameObject>(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<UnityEngine.Renderer>(true);
var cols = s_map.GetComponentsInChildren<UnityEngine.Collider>(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<UnityEngine.Collider>() != null || t.GetComponentInParent<UnityEngine.Collider>() != 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<MobControlMgr>(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<UnityEngine.Transform>(true);
for (int i = 0; i < all.Length; i++) if (all[i].name == name) return all[i];
return null;
}
/// <summary>지면 스냅 — 소품 꼭대기·메시 뒷면에 얹히지 않게 **원 좌표 y 에 가장 가까운** 히트를 쓴다(±5 m 안에서만).</summary>
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 가 실제로 쏘는 선분)
/// <summary>한 지점의 8방향 × 9타겟 선분 중 몇 개가 가리는지. afterHide = OccluderFade 로도 안 풀리는 수.</summary>
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<int>();
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<UnityEngine.Collider>());
// 제외 규칙 검증용 — 바닥 이름
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<UnityEngine.Renderer>();
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<UnityEngine.Renderer>();
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<UnityEngine.Renderer>();
bool onlyNoCol = HasSlot("FakeRock_0") &&
r0.shadowCastingMode == UnityEngine.Rendering.ShadowCastingMode.ShadowsOnly;
sb.Append("콜라이더 없는 소품 단독(FakeRock_0 · 콜라이더 ")
.Append(cubes[0].GetComponent<UnityEngine.Collider>() == 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<RealCamera>();
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<UnityEngine.Renderer>();
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<UnityEngine.ScriptableObject>(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) { }
}
}