488 lines
25 KiB
C#
488 lines
25 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// OccluderFade.cs — 카메라와 PC 사이를 막은 지형 소품을 잠시 숨긴다
|
||
//
|
||
// PD 지시 #813 · 발주서 WL-811o §1-2 (2026-09-09) · 근거 = qa/W2-4b_재채점_v2.md D-8 · 채점 7-3 = 0
|
||
// 「지형 바위가 PC 를 완전히 가림(Screenshots_WL/Q4b/01_enter_hud.png ≈40 %)」
|
||
//
|
||
// ■ 이 파일이 하는 것 — 원본 훅 0 · RealCamera 무수정 · 새 셰이더/머티리얼 0 · 폴링만
|
||
// 0.1 s 마다 카메라 → PC 주시점 선분을 검사해서 걸린 **지형 소품 렌더러**를 fadeOutSeconds 뒤 숨기고,
|
||
// 가림이 풀리면 restoreDelaySeconds 뒤 원래대로 되돌린다. 동시 상한 4(SO).
|
||
//
|
||
// ■ 검사 2단 (발주서는 SphereCast 만 지정했지만 실측이 그것으로 부족하다고 말한다)
|
||
// ⓐ SphereCast(반경 0.6 m · SO) — 콜라이더가 있는 소품·지형
|
||
// ⓑ 바운드 스캔 — 🔴 **WL_Nature 실측: 가림 후보 소품 979개 중 콜라이더 보유는 121개(12.4 %)**.
|
||
// 나무·덤불·바위 대부분에 콜라이더가 없어 ⓐ 만으로는 화면을 실제로 가리는 것의 12 % 밖에 못 잡는다.
|
||
// 그래서 맵 진입(PC 교체·씬 변경) 때 소품 렌더러 목록을 한 번 훑어 두고, 폴링에서는 그 목록의
|
||
// **렌더러 바운드**와 선분을 교차 검사한다(콜라이더 불요 · 폴링 중 할당 0). SO 로 끌 수 있다.
|
||
//
|
||
// ■ 숨김 방식 (실측 후 선택 · SO hideMode)
|
||
// 🔴 실측: 가림 후보 979개의 머티리얼이 **전부 불투명 큐**(URP/Lit queue 2000 · 투명 0).
|
||
// 불투명 셰이더에서는 MaterialPropertyBlock 알파가 화면에 아무 효과가 없고, 새 셰이더·머티리얼 생성은
|
||
// 발주서 §2 금지다. → Auto 는 **ShadowsOnly**(렌더러는 안 보이고 그림자는 남는다 · 머티리얼 무변경 · GC 0)로 내려간다.
|
||
// 이 경로에서 fadeOutSeconds 는 「스쳐 지나가는 소품이 한 프레임 깜빡이는 것」을 막는 지연으로 쓴다.
|
||
// (투명 큐 소품이 있는 맵에서는 Auto 가 알파 페이드를 고른다 — 코드는 두 경로를 다 갖고 있다.)
|
||
//
|
||
// ■ 안전장치 (맵이 통째로 사라지는 사고 방지)
|
||
// · 레이어 마스크(기본 Default + Map) — Actor(PC·몹)·UI·Water·GrassFX 레이어는 애초에 안 본다
|
||
// · Actor 컴포넌트 보유 · SkinnedMeshRenderer · ParticleSystem/Trail/Line 렌더러(이펙트)는 절대 제외
|
||
// · 이름 규칙(terrain/ground/floor/water/…) · 바운드 높이 하한 · 바운드 최대 변 상한(지형 통짜 메시 보호)
|
||
// · 동시 상한(기본 4) · 러너 파괴·씬 전환·SO off 에 **전부 원복**(C8)
|
||
//
|
||
// ■ GC 0
|
||
// Physics.SphereCastNonAlloc(사전 할당 RaycastHit[16]) · 슬롯 배열 사전 할당 · 스캔 배열 사전 할당 ·
|
||
// MaterialPropertyBlock 1개 재사용 · 이름 비교는 IndexOf(OrdinalIgnoreCase)(ToLower 할당 없음) ·
|
||
// verboseLog 를 켤 때만 문자열 생성. **할당은 스캔 재구축(맵 진입 1회) 에만 있다.**
|
||
//
|
||
// 🔴 어셈블리 주의: Assets/WL/Camera/ 에 .asmdef 를 만들지 말 것(RealCamera 가 Assembly-CSharp 에 있다).
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
using System;
|
||
using UnityEngine;
|
||
using UnityEngine.Rendering;
|
||
|
||
namespace WL.CameraFraming
|
||
{
|
||
/// <summary>카메라~PC 가림 소품 임시 숨김. 정적 · 새 Manager/Singleton 0(시간 축은 CameraFramingRunner 공용).</summary>
|
||
public static class OccluderFade
|
||
{
|
||
const int SlotMax = 8; // SO maxConcurrent(1~8)의 물리 상한
|
||
const int HitMax = 16; // 한 번의 SphereCast 에서 볼 최대 히트 수
|
||
|
||
static WLCameraFramingSettings Cfg { get { return WLCameraFramingSettings.Instance; } }
|
||
|
||
/// <summary>기능이 살아 있는가. 에셋이 없거나 플래그가 꺼져 있으면 false = 소품 100 % 표시.</summary>
|
||
public static bool Active
|
||
{
|
||
get { var c = Cfg; return c != null && c.enabled && c.occluderFadeEnabled && !WLCameraFramingSettings.RuntimeDisabled; }
|
||
}
|
||
|
||
// ───────────────────────────────────────── 진단 카운터 (프로브가 읽는다)
|
||
public static int Polls, Blocked, HiddenCount, RestoredCount, CapSkips, Rejected, ScanBuilds;
|
||
public static int ActiveHidden; // 지금 실제로 숨겨 둔 수
|
||
public static string LastLog = "";
|
||
public static string LastHiddenName = "";
|
||
public static OccluderHideMode LastHideMode = OccluderHideMode.Auto;
|
||
|
||
// ───────────────────────────────────────── 내부 상태 (전부 사전 할당)
|
||
sealed class Slot
|
||
{
|
||
public Renderer r;
|
||
public Transform tf;
|
||
public ShadowCastingMode originShadow;
|
||
public bool originEnabled;
|
||
public bool hidden;
|
||
public OccluderHideMode mode;
|
||
public int colorProp;
|
||
public Color originColor;
|
||
public float firstSeen, lastSeen;
|
||
}
|
||
|
||
static readonly Slot[] s_slots = NewSlots();
|
||
static readonly RaycastHit[] s_hits = new RaycastHit[HitMax];
|
||
static MaterialPropertyBlock s_mpb;
|
||
static string[] s_excludeTokens = new string[0];
|
||
static string s_excludeSource;
|
||
static float s_nextPoll;
|
||
static bool s_faulted;
|
||
|
||
// ── 바운드 스캔(콜라이더 없는 소품) 상태
|
||
static Renderer[] s_scan = new Renderer[0];
|
||
static int s_scanCount;
|
||
static float s_nextScan;
|
||
static PCActor s_scanPc;
|
||
static int s_scanScene = -1;
|
||
|
||
/// <summary>스캔 목록에 들어간 소품 수(진단).</summary>
|
||
public static int ScanCount { get { return s_scanCount; } }
|
||
|
||
static readonly int PropBaseColor = Shader.PropertyToID("_BaseColor");
|
||
static readonly int PropColor = Shader.PropertyToID("_Color");
|
||
|
||
/// <summary>
|
||
/// 프로브 전용 시간 소스(초). 0 미만이면 Time.unscaledTime 을 쓴다.
|
||
/// 🔴 에디트 모드에서는 Time.unscaledTime 이 흐르지 않아 페이드/복귀를 검증할 수 없다 —
|
||
/// 프로브가 이 값을 밀어 가짜 시간으로 상태 전이를 확인한다. 런타임에서는 항상 −1.
|
||
/// </summary>
|
||
public static float TimeOverride = -1f;
|
||
|
||
static float Now { get { return TimeOverride >= 0f ? TimeOverride : Time.unscaledTime; } }
|
||
|
||
static Slot[] NewSlots()
|
||
{
|
||
var a = new Slot[SlotMax];
|
||
for (int i = 0; i < SlotMax; i++) a[i] = new Slot();
|
||
return a;
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// 틱
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>러너가 매 프레임 부른다. 내부에서 pollSeconds 로 스로틀한다.</summary>
|
||
internal static void Tick()
|
||
{
|
||
if (s_faulted) return;
|
||
|
||
if (!Active) { if (ActiveHidden > 0) ReleaseAll(); return; }
|
||
|
||
var cfg = Cfg;
|
||
float now = Now;
|
||
if (now < s_nextPoll) return;
|
||
s_nextPoll = now + Mathf.Max(0.02f, cfg.pollSeconds);
|
||
|
||
// 🔴 Debug.LogException 을 쓰지 않는다 — ErrorLogHookManager 가 그것을 플레이어 팝업으로 띄운다(813x §D-1).
|
||
try
|
||
{
|
||
var cam = MyValue.m_RealCamera;
|
||
var pc = MyValue.MyPC;
|
||
if (DSUtil.CheckNull(cam) || DSUtil.CheckNull(pc)) { UpdateSlots(cfg, now); return; }
|
||
|
||
Vector3 camPos = cam.transform.position;
|
||
Vector3 aim = pc.transform.position + cam.m_LookAt; // RealCamera.transform.LookAt 과 같은 점
|
||
EvaluateSegment(camPos, aim);
|
||
}
|
||
catch (Exception ex) { s_faulted = true; Log("중단(예외) — " + ex.Message); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 카메라 위치 → 주시점 선분을 검사하고 슬롯을 갱신한다.
|
||
/// 프로브가 에디트 모드에서 「가짜 가림」을 만들 때 직접 부른다(러너·PC 없이도 동작).
|
||
/// </summary>
|
||
public static void EvaluateSegment(Vector3 camPos, Vector3 aim)
|
||
{
|
||
var cfg = Cfg;
|
||
if (cfg == null) return;
|
||
float now = Now;
|
||
Polls++;
|
||
|
||
Vector3 dir = aim - camPos;
|
||
float dist = dir.magnitude;
|
||
if (dist > 0.0001f)
|
||
{
|
||
dir /= dist;
|
||
// 카메라 근평면 안쪽에서 시작하면 겹친 콜라이더가 distance 0 으로 보고된다 — 조금 앞에서 쏜다.
|
||
float skip = Mathf.Min(0.2f, dist * 0.1f);
|
||
float segLen = Mathf.Max(0f, dist - skip - 0.5f); // 끝 0.5 m 는 PC 자신
|
||
float radius = Mathf.Max(0f, cfg.sphereRadius);
|
||
|
||
// ⓐ 콜라이더가 있는 소품·지형
|
||
int n = radius > 0.0001f
|
||
? Physics.SphereCastNonAlloc(camPos + dir * skip, radius, dir, s_hits, segLen,
|
||
cfg.occluderMask, QueryTriggerInteraction.Ignore)
|
||
: Physics.RaycastNonAlloc(camPos + dir * skip, dir, s_hits, segLen,
|
||
cfg.occluderMask, QueryTriggerInteraction.Ignore);
|
||
if (n > HitMax) n = HitMax;
|
||
if (n > 0) Blocked++;
|
||
|
||
for (int i = 0; i < n; i++)
|
||
{
|
||
var col = s_hits[i].collider;
|
||
if (col == null) continue;
|
||
var rnd = Resolve(cfg, col);
|
||
if (rnd == null) continue;
|
||
Touch(cfg, rnd, now);
|
||
}
|
||
|
||
// ⓑ 콜라이더가 없는 소품 — 사전 스캔 목록의 바운드로 판정(폴링 중 할당 0)
|
||
EnsureScan(cfg, now);
|
||
if (s_scanCount > 0)
|
||
{
|
||
float br = cfg.boundsScanRadius > 0.0001f ? cfg.boundsScanRadius : radius;
|
||
var ray = new Ray(camPos + dir * skip, dir);
|
||
for (int i = 0; i < s_scanCount; i++)
|
||
{
|
||
var rnd = s_scan[i];
|
||
if (rnd == null) continue;
|
||
var b = rnd.bounds;
|
||
b.Expand(br * 2f); // 선분 반경만큼 부풀린다(SphereCast 와 같은 뜻)
|
||
float hitD;
|
||
if (!b.IntersectRay(ray, out hitD)) continue;
|
||
// 🔴 Bounds.IntersectRay 는 광선 시작점이 바운드 **안**에 있으면 음수 거리를 준다
|
||
// (= 카메라가 그 소품 속에 있다 = 확실한 가림). 음수를 버리면 가장 심한 가림을 놓친다.
|
||
if (hitD > segLen) continue;
|
||
Touch(cfg, rnd, now);
|
||
}
|
||
}
|
||
}
|
||
|
||
UpdateSlots(cfg, now);
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// 후보 판정
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>콜라이더 → 숨겨도 되는 렌더러. 자격이 없으면 null.</summary>
|
||
static Renderer Resolve(WLCameraFramingSettings cfg, Collider col)
|
||
{
|
||
// PC·몹 (레이어로 이미 걸러지지만 프리팹이 레이어를 안 맞춰 놓은 경우까지 막는다)
|
||
if (cfg.skipActors && col.GetComponentInParent<Actor>() != null) { Rejected++; return null; }
|
||
|
||
var rnd = col.GetComponent<Renderer>();
|
||
if (rnd == null) rnd = col.GetComponentInParent<Renderer>();
|
||
if (rnd == null) { Rejected++; return null; }
|
||
return Eligible(cfg, rnd, false) ? rnd : null;
|
||
}
|
||
|
||
/// <summary>이 렌더러를 숨겨도 되는가 — 콜라이더 경로와 바운드 스캔 경로 공용 규칙.</summary>
|
||
public static bool Eligible(WLCameraFramingSettings cfg, Renderer rnd, bool checkActor)
|
||
{
|
||
if (cfg == null || rnd == null) return false;
|
||
if (rnd is ParticleSystemRenderer || rnd is TrailRenderer || rnd is LineRenderer) { Rejected++; return false; } // 이펙트
|
||
if (cfg.skipSkinnedRenderers && rnd is SkinnedMeshRenderer) { Rejected++; return false; }
|
||
|
||
if (((1 << rnd.gameObject.layer) & cfg.occluderMask.value) == 0) { Rejected++; return false; } // 마스크 밖(Actor·UI·Water…)
|
||
if (checkActor && cfg.skipActors && rnd.GetComponentInParent<Actor>() != null) { Rejected++; return false; }
|
||
|
||
// 바닥·지형·물 — 이름 규칙(자기 이름 + 부모 이름)
|
||
var tf = rnd.transform;
|
||
if (NameExcluded(cfg, tf.name) || (tf.parent != null && NameExcluded(cfg, tf.parent.name))) { Rejected++; return false; }
|
||
|
||
var b = rnd.bounds;
|
||
if (b.size.y < cfg.minOccluderHeight) { Rejected++; return false; } // 잔디·자갈은 가리지 않는다
|
||
float maxSide = Mathf.Max(b.size.x, Mathf.Max(b.size.y, b.size.z));
|
||
if (maxSide > cfg.maxOccluderSize) { Rejected++; return false; } // 지형 통짜 메시·배경 산
|
||
|
||
return true;
|
||
}
|
||
|
||
static bool NameExcluded(WLCameraFramingSettings cfg, string name)
|
||
{
|
||
if (string.IsNullOrEmpty(name)) return false;
|
||
EnsureTokens(cfg);
|
||
for (int i = 0; i < s_excludeTokens.Length; i++)
|
||
{
|
||
var t = s_excludeTokens[i];
|
||
if (t.Length > 0 && name.IndexOf(t, StringComparison.OrdinalIgnoreCase) >= 0) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
static void EnsureTokens(WLCameraFramingSettings cfg)
|
||
{
|
||
string src = cfg.excludeNameContains;
|
||
if (ReferenceEquals(src, s_excludeSource)) return; // 문자열이 그대로면 재분해 없음 = GC 0
|
||
s_excludeSource = src;
|
||
s_excludeTokens = string.IsNullOrEmpty(src) ? new string[0] : src.Split(',');
|
||
for (int i = 0; i < s_excludeTokens.Length; i++) s_excludeTokens[i] = s_excludeTokens[i].Trim();
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// 바운드 스캔 — 맵 진입 1회 목록 구축(여기서만 할당)
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
|
||
static void EnsureScan(WLCameraFramingSettings cfg, float now)
|
||
{
|
||
if (!cfg.boundsScanEnabled) { s_scanCount = 0; return; }
|
||
|
||
var pc = MyValue.MyPC;
|
||
int scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene().handle;
|
||
bool boundary = s_scanCount == 0 || !ReferenceEquals(pc, s_scanPc) || scene != s_scanScene;
|
||
bool timed = cfg.scanRebuildSeconds > 0.0001f && now >= s_nextScan;
|
||
if (!boundary && !timed) return;
|
||
|
||
s_scanPc = pc; s_scanScene = scene;
|
||
s_nextScan = now + Mathf.Max(1f, cfg.scanRebuildSeconds);
|
||
BuildScan(cfg);
|
||
}
|
||
|
||
/// <summary>맵 소품 목록 재구축(프로브·맵 교체용). 이 함수 안에서만 할당이 있다.</summary>
|
||
public static void RebuildScan()
|
||
{
|
||
var cfg = Cfg;
|
||
if (cfg == null) return;
|
||
s_scanPc = MyValue.MyPC;
|
||
s_scanScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene().handle;
|
||
s_nextScan = Now + Mathf.Max(1f, cfg.scanRebuildSeconds);
|
||
BuildScan(cfg);
|
||
}
|
||
|
||
static void BuildScan(WLCameraFramingSettings cfg)
|
||
{
|
||
var all = UnityEngine.Object.FindObjectsByType<MeshRenderer>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
|
||
int cap = Mathf.Clamp(cfg.scanMax, 16, 8192);
|
||
if (s_scan.Length < cap) s_scan = new Renderer[cap];
|
||
|
||
int n = 0;
|
||
for (int i = 0; i < all.Length && n < cap; i++)
|
||
{
|
||
var r = all[i];
|
||
if (r == null) continue;
|
||
if (!Eligible(cfg, r, true)) continue;
|
||
s_scan[n++] = r;
|
||
}
|
||
for (int i = n; i < s_scan.Length; i++) s_scan[i] = null;
|
||
s_scanCount = n;
|
||
ScanBuilds++;
|
||
Log("스캔 재구축 " + n + " / 렌더러 " + all.Length);
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// 슬롯
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>이번 폴링에서 이 렌더러가 가림으로 잡혔다 — 슬롯을 잡거나 시간을 갱신한다.</summary>
|
||
static void Touch(WLCameraFramingSettings cfg, Renderer rnd, float now)
|
||
{
|
||
int cap = Mathf.Clamp(cfg.maxConcurrent, 1, SlotMax);
|
||
int free = -1, used = 0;
|
||
for (int i = 0; i < SlotMax; i++)
|
||
{
|
||
var s = s_slots[i];
|
||
if (s.r == null) { if (free < 0) free = i; continue; }
|
||
used++;
|
||
if (ReferenceEquals(s.r, rnd)) { s.lastSeen = now; return; }
|
||
}
|
||
if (used >= cap || free < 0) { CapSkips++; return; }
|
||
|
||
var slot = s_slots[free];
|
||
slot.r = rnd;
|
||
slot.tf = rnd.transform;
|
||
slot.originShadow = rnd.shadowCastingMode;
|
||
slot.originEnabled = rnd.enabled;
|
||
slot.hidden = false;
|
||
slot.firstSeen = now;
|
||
slot.lastSeen = now;
|
||
slot.mode = ResolveMode(cfg, rnd, ref slot.colorProp, ref slot.originColor);
|
||
}
|
||
|
||
static OccluderHideMode ResolveMode(WLCameraFramingSettings cfg, Renderer rnd, ref int colorProp, ref Color originColor)
|
||
{
|
||
colorProp = 0;
|
||
originColor = Color.white;
|
||
var mode = cfg.hideMode;
|
||
if (mode == OccluderHideMode.MpbAlpha || mode == OccluderHideMode.Auto)
|
||
{
|
||
var mat = rnd.sharedMaterial;
|
||
if (mat != null)
|
||
{
|
||
int p = mat.HasProperty(PropBaseColor) ? PropBaseColor : (mat.HasProperty(PropColor) ? PropColor : 0);
|
||
// 투명 큐가 아니면 알파를 낮춰도 화면에 안 나타난다 → Auto 는 ShadowsOnly 로 내려간다.
|
||
if (p != 0 && (mode == OccluderHideMode.MpbAlpha || mat.renderQueue >= 3000))
|
||
{
|
||
colorProp = p;
|
||
originColor = mat.GetColor(p);
|
||
return OccluderHideMode.MpbAlpha;
|
||
}
|
||
}
|
||
return OccluderHideMode.ShadowsOnly;
|
||
}
|
||
return mode;
|
||
}
|
||
|
||
/// <summary>슬롯 시간 진행 — 숨김/복귀/원복.</summary>
|
||
static void UpdateSlots(WLCameraFramingSettings cfg, float now)
|
||
{
|
||
float fade = Mathf.Max(0f, cfg.fadeOutSeconds);
|
||
float back = Mathf.Max(0f, cfg.restoreDelaySeconds);
|
||
int active = 0;
|
||
|
||
for (int i = 0; i < SlotMax; i++)
|
||
{
|
||
var s = s_slots[i];
|
||
if (s.r == null) continue;
|
||
if (s.tf == null) { Clear(s, false); continue; } // 파괴됨(Unity fake-null)
|
||
|
||
if ((now - s.lastSeen) >= back) { Clear(s, true); continue; } // 가림이 풀렸다 → 복귀
|
||
|
||
if (s.mode == OccluderHideMode.MpbAlpha)
|
||
{
|
||
float t = fade > 0.0001f ? Mathf.Clamp01((now - s.firstSeen) / fade) : 1f;
|
||
SetAlpha(s, Mathf.Lerp(1f, 0f, t));
|
||
if (!s.hidden && t >= 1f) { s.hidden = true; HiddenCount++; Mark(s); }
|
||
}
|
||
else if (!s.hidden && (now - s.firstSeen) >= fade)
|
||
{
|
||
if (s.mode == OccluderHideMode.RendererOff) s.r.enabled = false;
|
||
else s.r.shadowCastingMode = ShadowCastingMode.ShadowsOnly;
|
||
s.hidden = true; HiddenCount++; Mark(s);
|
||
}
|
||
if (s.hidden) active++;
|
||
}
|
||
ActiveHidden = active;
|
||
}
|
||
|
||
static void Mark(Slot s)
|
||
{
|
||
LastHideMode = s.mode;
|
||
var c = Cfg;
|
||
if (c != null && c.verboseLog)
|
||
{
|
||
LastHiddenName = s.tf != null ? s.tf.name : "?";
|
||
Log("숨김 " + LastHiddenName + " (" + s.mode + ")");
|
||
}
|
||
}
|
||
|
||
static void SetAlpha(Slot s, float a)
|
||
{
|
||
if (s.colorProp == 0) return;
|
||
if (s_mpb == null) s_mpb = new MaterialPropertyBlock();
|
||
s.r.GetPropertyBlock(s_mpb);
|
||
var c = s.originColor; c.a = s.originColor.a * a;
|
||
s_mpb.SetColor(s.colorProp, c);
|
||
s.r.SetPropertyBlock(s_mpb);
|
||
}
|
||
|
||
/// <summary>슬롯 비우기 — restore = true 면 원래 상태로 되돌린다.</summary>
|
||
static void Clear(Slot s, bool restore)
|
||
{
|
||
if (restore && s.r != null)
|
||
{
|
||
if (s.mode == OccluderHideMode.MpbAlpha) s.r.SetPropertyBlock(null);
|
||
else if (s.mode == OccluderHideMode.RendererOff) s.r.enabled = s.originEnabled;
|
||
else s.r.shadowCastingMode = s.originShadow;
|
||
if (s.hidden) RestoredCount++;
|
||
}
|
||
s.r = null; s.tf = null; s.hidden = false; s.colorProp = 0;
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// 진단
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>숨겨 둔 것을 전부 원복한다(씬 전환·러너 파괴·앱 종료·SO off).</summary>
|
||
public static void ReleaseAll()
|
||
{
|
||
for (int i = 0; i < SlotMax; i++) Clear(s_slots[i], true);
|
||
ActiveHidden = 0;
|
||
}
|
||
|
||
/// <summary>프로브용: 카운터 초기화(먼저 전부 원복한다).</summary>
|
||
public static void ResetDiagnostics()
|
||
{
|
||
ReleaseAll();
|
||
Polls = Blocked = HiddenCount = RestoredCount = CapSkips = Rejected = ScanBuilds = 0;
|
||
LastLog = LastHiddenName = "";
|
||
s_nextPoll = 0f; s_faulted = false;
|
||
}
|
||
|
||
/// <summary>프로브용: 스캔 목록을 버린다(다음 폴링에서 다시 훑는다).</summary>
|
||
public static void ClearScan() { s_scanCount = 0; s_scanPc = null; s_scanScene = -1; }
|
||
|
||
/// <summary>프로브용: 러너 없이 1회 폴링(강제 틱).</summary>
|
||
public static void ForceTick() { s_nextPoll = 0f; Tick(); }
|
||
|
||
/// <summary>프로브용: 지금 슬롯에 잡힌 렌더러 이름(없으면 빈 문자열). i = 0..7</summary>
|
||
public static string SlotName(int i)
|
||
{
|
||
if (i < 0 || i >= SlotMax) return "";
|
||
var s = s_slots[i];
|
||
return (s.r != null && s.tf != null) ? s.tf.name : "";
|
||
}
|
||
|
||
/// <summary>프로브용: 슬롯이 실제로 숨겨진 상태인지.</summary>
|
||
public static bool SlotHidden(int i)
|
||
{
|
||
if (i < 0 || i >= SlotMax) return false;
|
||
return s_slots[i].r != null && s_slots[i].hidden;
|
||
}
|
||
|
||
static void Log(string msg)
|
||
{
|
||
LastLog = msg;
|
||
var c = Cfg;
|
||
if (c != null && c.verboseLog) Debug.Log("[OccluderFade] " + msg);
|
||
}
|
||
}
|
||
}
|