Project_WL/AgentScripts/WL811o_Probe.cs

764 lines
46 KiB
C#
Raw Normal View History

// 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) { }
}
}