2026-09-10 14:30:35 +00:00
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
// PixelCameraRig.cs — Critter 3D Pixel Camera 를 인게임에서 런타임 조립/복원하는 WL 리그
|
|
|
|
|
|
//
|
|
|
|
|
|
// PD 지시 #814(「Critter 3D Pixel Art Toolkit 으로 도트 게임처럼」) · 발주서 WL-814b §1-3
|
|
|
|
|
|
//
|
|
|
|
|
|
// ■ 왜 이렇게 만들었나 (실측 근거 — 보고서 표 ①)
|
|
|
|
|
|
// · 원본 인게임 카메라는 `Assets/ResWork/Prefabs/Ingame/Main Camera.prefab`(tag MainCamera · fov 60 · 원근)이고
|
|
|
|
|
|
// `Assets/Script/Util/RealCamera.cs` 가 **LateUpdate 에서** 트랜스폼을 쓴다. CamStatus 1/2 는
|
|
|
|
|
|
// `Vector3.Lerp(transform.position, …)` = **현재 위치에서 보간**이라 누가 카메라 트랜스폼에 되쓰면
|
|
|
|
|
|
// 그 값이 다음 프레임의 입력이 된다(피드백 루프). → 리그는 카메라 트랜스폼에 **절대 쓰지 않는다**.
|
|
|
|
|
|
// · `RealCamera.Shaking()` 은 `transform.localPosition` 을 쓴다(811gh 셰이크). 원본 카메라를 **재부모화하면
|
|
|
|
|
|
// 셰이크 기준이 깨진다** → 재부모화 0. 대신 **원본 카메라의 자식**으로 빈 프록시를 하나 달아
|
|
|
|
|
|
// Critter 가 그것을 따라가게 한다(지연 0 · 프레임당 비용 0 · 셰이크도 그대로 미러된다).
|
|
|
|
|
|
// · `MyValue.m_RealCamera` 는 `Camera.main.GetComponent<RealCamera>()` 다 → 원본 카메라가 **계속
|
|
|
|
|
|
// MainCamera 태그를 달고 enabled 여야** 한다(`Camera.main` 은 활성 카메라만 돌려준다).
|
|
|
|
|
|
// · 인게임 UI 캔버스는 **Screen Space - Camera(UICamera)** 이고 `NewGameUI.cs:38` 이 그 UICamera 를
|
|
|
|
|
|
// **`Camera.main` 의 URP 카메라 스택에 Overlay 로** 넣는다 → 원본 카메라를 끄면 UI 가 사라진다.
|
|
|
|
|
|
// 그래서 원본 카메라는 화면 Base 로 남기고, Critter 뷰 카메라를 같은 스택의 **0번 Overlay** 로 끼운다.
|
|
|
|
|
|
// 스택 = [원본 Main Camera(Base · 월드 미렌더)] → [ViewCamera(Overlay · 픽셀 쿼드)] → [UICamera(Overlay · UI)]
|
|
|
|
|
|
// · `HUDBase.cs:28,55`(HP 바 · 데미지 숫자)와 `DSUtil.cs:859`·`InGameTouchInfo.cs:37` 은 전부
|
|
|
|
|
|
// `Camera.main` 의 투영을 쓴다 → 원본 카메라를 **직교로 바꿔** 픽셀 카메라와 같은 투영을 갖게 한다.
|
|
|
|
|
|
//
|
|
|
|
|
|
// ■ 리그 구조 (Assemble 이 만든다 · 디스크 에셋 수정 0)
|
|
|
|
|
|
// WL_PixelCameraRig = PixelCameraSystem.prefab 인스턴스(루트 이름만 바꿈)
|
|
|
|
|
|
// ├─ PixelCameraTransform 프리팹 기본 followed(ChildOfCamera 모드에서는 쓰지 않는다 · 삭제하면
|
|
|
|
|
|
// │ Critter 의 `parent.childCount` 권고에서 벗어나므로 그대로 둔다)
|
|
|
|
|
|
// └─ PixelCamera 직교 카메라 + PixelCameraManager · 저해상도 RT 로 월드를 그린다
|
|
|
|
|
|
// └─ UpscaledDisplay
|
|
|
|
|
|
// ├─ QuadCanvas RT 를 붙인 쿼드(UpscaledCanvas) · rigLayer
|
|
|
|
|
|
// └─ ViewCamera 직교 · CanvasViewCamera(서브픽셀 보정) · rigLayer 만 그린다
|
|
|
|
|
|
// Main Camera (원본 · 트랜스폼 무변경)
|
|
|
|
|
|
// └─ WL_PixelFollow 프록시(빈 오브젝트 · localPos 0 · localRot identity)
|
|
|
|
|
|
//
|
|
|
|
|
|
// ■ C8 롤백
|
|
|
|
|
|
// `WLPixelCameraSettings.enabled = 0`(또는 `RuntimeDisabled`) → 다음 틱에 Teardown → 원본 카메라 값
|
|
|
|
|
|
// (직교/사이즈/컬링/클리어/배경/near·far/fov/depth/포스트/스택)이 **스냅샷 그대로** 복원되고 리그 오브젝트 0 · RT 해제.
|
|
|
|
|
|
//
|
|
|
|
|
|
// 🔴 이 파일은 Debug.LogError / Debug.LogException 을 쓰지 않는다(813z · ErrorLogHookManager 팝업).
|
|
|
|
|
|
// 🔴 Assets/WL/ 에 .asmdef 를 만들지 말 것.
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using UnityEngine.Rendering.Universal;
|
|
|
|
|
|
using PixelCamera;
|
|
|
|
|
|
|
|
|
|
|
|
namespace WL.PixelArt
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>Critter 픽셀 카메라 리그의 조립·복원. 정적 · 전용 러너 1개.</summary>
|
|
|
|
|
|
public static class PixelCameraRig
|
|
|
|
|
|
{
|
|
|
|
|
|
public const string RigName = "WL_PixelCameraRig";
|
|
|
|
|
|
public const string ProxyName = "WL_PixelFollow";
|
|
|
|
|
|
|
|
|
|
|
|
// ───────────────────────────────────────── 진단(프로브가 읽는다)
|
|
|
|
|
|
public static int Boundaries, Assembled, Restored, SkippedLobby, SkippedNoCam, SkippedNoPrefab, Faults;
|
|
|
|
|
|
public static string LastReason = "", LastLog = "";
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>지금 리그가 조립돼 있는가.</summary>
|
|
|
|
|
|
public static bool IsAssembled { get { return s_rigRoot != null; } }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>조립된 리그의 핵심 참조(프로브 전용).</summary>
|
|
|
|
|
|
public static GameObject RigRoot { get { return s_rigRoot; } }
|
|
|
|
|
|
public static Camera SourceCamera { get { return s_src; } }
|
|
|
|
|
|
public static Camera GameCamera { get { return s_gameCam; } }
|
|
|
|
|
|
public static Camera ViewCamera { get { return s_viewCam; } }
|
|
|
|
|
|
public static Transform Proxy { get { return s_proxy; } }
|
|
|
|
|
|
public static PixelCameraManager Manager { get { return s_mgr; } }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>지금 회전 때문에 스냅이 꺼져 있는가(진단).</summary>
|
|
|
|
|
|
public static bool SnapSuspended { get { return s_snapSuspended; } }
|
|
|
|
|
|
|
|
|
|
|
|
// ───────────────────────────────────────── 내부 상태
|
|
|
|
|
|
static Camera s_src;
|
|
|
|
|
|
static GameObject s_rigRoot, s_proxyGo;
|
|
|
|
|
|
static Transform s_proxy;
|
|
|
|
|
|
static Camera s_gameCam, s_viewCam;
|
|
|
|
|
|
static PixelCameraManager s_mgr;
|
|
|
|
|
|
static UpscaledCanvas s_canvas;
|
|
|
|
|
|
static Material s_matInstance;
|
|
|
|
|
|
static MeshRenderer s_quadRenderer;
|
|
|
|
|
|
static Material s_quadSharedMat;
|
|
|
|
|
|
|
|
|
|
|
|
static CamSnapshot s_snap;
|
|
|
|
|
|
static bool s_faulted;
|
|
|
|
|
|
|
|
|
|
|
|
static PCActor s_lastPc;
|
|
|
|
|
|
static int s_lastMapId = int.MinValue;
|
|
|
|
|
|
static float s_nextPoll;
|
|
|
|
|
|
|
|
|
|
|
|
static bool s_snapSuspended;
|
|
|
|
|
|
static float s_snapResumeAt;
|
|
|
|
|
|
static Quaternion s_lastSrcRot = Quaternion.identity;
|
|
|
|
|
|
static bool s_hasLastRot;
|
|
|
|
|
|
|
|
|
|
|
|
static WLPixelCameraSettings Cfg { get { return WLPixelCameraSettings.Instance; } }
|
|
|
|
|
|
static bool Active { get { return WLPixelCameraSettings.Enabled; } }
|
|
|
|
|
|
|
2026-09-10 15:11:37 +00:00
|
|
|
|
// ── WL-814d 런타임 오버라이드 — 룩 토글이 SO `enabled 0`(F-23) 이어도 리그를 강제로 켠다(값도 SO 보다 우선).
|
|
|
|
|
|
// 아무도 부르지 않으면 s_rtOn = false 라 기존 동작과 완전히 같다.
|
|
|
|
|
|
static bool s_rtOn; static float s_rtOrtho = 3.8f; static int s_rtPx = 480;
|
|
|
|
|
|
/// <summary>룩 토글이 지금 리그를 강제로 켜고 있는가(진단).</summary>
|
|
|
|
|
|
public static bool RuntimeOverrideOn { get { return s_rtOn; } }
|
|
|
|
|
|
static float OrthoOf(WLPixelCameraSettings c) { return s_rtOn ? s_rtOrtho : c.orthoSize; }
|
|
|
|
|
|
static int PxOf(WLPixelCameraSettings c) { return s_rtOn ? s_rtPx : c.pixelHeight; }
|
|
|
|
|
|
/// <summary>WL-814d — 룩 토글이 부른다. on 이면 이 값으로 즉시 조립, off 면 해체. 값이 그대로면 무동작.</summary>
|
|
|
|
|
|
public static string SetRuntimeOverride(bool on, float orthoSize, int pixelHeight)
|
|
|
|
|
|
{
|
|
|
|
|
|
int px = Mathf.Max(3, pixelHeight);
|
|
|
|
|
|
if (on == s_rtOn && (!on || (orthoSize == s_rtOrtho && px == s_rtPx))) return "변화 없음(on=" + on + ")";
|
|
|
|
|
|
var cam = s_src != null ? s_src : Camera.main;
|
|
|
|
|
|
if (IsAssembled) Teardown("814d 전환");
|
|
|
|
|
|
s_rtOn = on; s_faulted = false; s_nextPoll = 0f;
|
|
|
|
|
|
if (on) { s_rtOrtho = orthoSize; s_rtPx = px; }
|
|
|
|
|
|
var cfg = Cfg;
|
|
|
|
|
|
if ((!on && !Active) || cfg == null) return "오버라이드 해제(on=" + on + ")";
|
|
|
|
|
|
if (Application.isPlaying) Boot(); // SO enabled 0 이면 러너가 없다 — 여기서 띄운다
|
|
|
|
|
|
if (cam == null) return "Camera.main 없음 — 다음 경계에서 조립";
|
|
|
|
|
|
return Assemble(cfg, cam) ? "조립 ortho=" + OrthoOf(cfg).ToString("0.###") + " px=" + PxOf(cfg) : "조립 실패";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-10 14:30:35 +00:00
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
// 틱 (러너가 매 프레임 부른다 · 에디트 모드에서는 프로브가 직접 부를 수 있다)
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>1프레임. (프로브가 별도 어셈블리에서 부르므로 public)</summary>
|
|
|
|
|
|
public static void Tick()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (s_faulted) return;
|
|
|
|
|
|
|
|
|
|
|
|
// C8 — 기능이 꺼지면 즉시 원상복구.
|
2026-09-10 15:11:37 +00:00
|
|
|
|
if (!Active && !s_rtOn) { if (IsAssembled) Teardown("off"); return; } // WL-814d 오버라이드는 SO 를 넘는다
|
2026-09-10 14:30:35 +00:00
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var cfg = Cfg;
|
|
|
|
|
|
UpdateSnapSuspension(cfg);
|
|
|
|
|
|
|
|
|
|
|
|
float now = Time.unscaledTime;
|
|
|
|
|
|
if (now < s_nextPoll) return;
|
|
|
|
|
|
s_nextPoll = now + Mathf.Max(0.02f, cfg.pollSeconds);
|
|
|
|
|
|
if (cfg.assembleOnMapEnter) PollBoundary(cfg);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (System.Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
s_faulted = true; Faults++;
|
|
|
|
|
|
LastLog = "중단(예외) — " + ex.Message;
|
|
|
|
|
|
Log(LastLog); // 🔴 LogError 금지(813z)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>맵 진입/이탈 경계 — 811q EffectWarmup · 811o CameraFramingOverride 와 같은 규칙.</summary>
|
|
|
|
|
|
static void PollBoundary(WLPixelCameraSettings cfg)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!Application.isPlaying) return; // 에디트 모드 오염 0(프로브는 ProbeAssemble 로 들어온다)
|
|
|
|
|
|
|
|
|
|
|
|
var pc = MyValue.MyPC;
|
|
|
|
|
|
if (DSUtil.CheckNull(pc))
|
|
|
|
|
|
{
|
|
|
|
|
|
// 맵 이탈 — PC 가 사라졌다. 리그를 걷는다.
|
|
|
|
|
|
if (s_lastPc != null && IsAssembled) Teardown("맵 이탈");
|
|
|
|
|
|
s_lastPc = null;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var info = InGameInfo.Ins;
|
|
|
|
|
|
if (DSUtil.CheckNull(info)) return; // 인게임 정보가 아직 없다 — 경계를 소비하지 않는다
|
|
|
|
|
|
|
|
|
|
|
|
int map = MyValue.MyChoiceMapData != null ? MyValue.MyChoiceMapData.n_MapID : int.MinValue;
|
|
|
|
|
|
bool pcChanged = !ReferenceEquals(pc, s_lastPc);
|
|
|
|
|
|
bool mapChanged = map != s_lastMapId;
|
|
|
|
|
|
if (!pcChanged && !mapChanged) return;
|
|
|
|
|
|
|
|
|
|
|
|
bool lobby = info.Get_GameMode() == eGameMode.Lobby;
|
|
|
|
|
|
if (!cfg.assembleInLobby && lobby)
|
|
|
|
|
|
{
|
|
|
|
|
|
s_lastPc = pc; s_lastMapId = map;
|
|
|
|
|
|
Boundaries++; SkippedLobby++;
|
|
|
|
|
|
LastReason = "lobby";
|
|
|
|
|
|
if (IsAssembled) Teardown("로비 진입");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var cam = Camera.main;
|
|
|
|
|
|
if (cam == null) { SkippedNoCam++; return; } // 경계를 소비하지 않는다 — 다음 폴링에서 다시 본다
|
|
|
|
|
|
|
|
|
|
|
|
s_lastPc = pc; s_lastMapId = map;
|
|
|
|
|
|
Boundaries++;
|
|
|
|
|
|
LastReason = pcChanged ? "pc" : ("map " + map);
|
|
|
|
|
|
|
|
|
|
|
|
if (IsAssembled) Teardown("경계 재조립");
|
|
|
|
|
|
Assemble(cfg, cam);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
// 조립
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>리그를 조립한다. 성공 시 true.</summary>
|
|
|
|
|
|
public static bool Assemble(WLPixelCameraSettings cfg, Camera src)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (cfg == null || src == null) return false;
|
|
|
|
|
|
if (IsAssembled) return true;
|
|
|
|
|
|
|
|
|
|
|
|
var prefab = cfg.pixelCameraSystemPrefab;
|
|
|
|
|
|
if (prefab == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
SkippedNoPrefab++;
|
|
|
|
|
|
LastLog = "PixelCameraSystem 프리팹 미설정 — 조립 안 함(URP.unitypackage 해제 확인)";
|
|
|
|
|
|
Log(LastLog);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 이전 잔재(에디트 모드 재실행 등) 제거 — Critter 의 FindAnyObjectByType 가 엉뚱한 걸 잡지 않게.
|
|
|
|
|
|
DestroyStrays();
|
|
|
|
|
|
|
|
|
|
|
|
s_src = src;
|
|
|
|
|
|
s_snap.Capture(src);
|
|
|
|
|
|
|
|
|
|
|
|
// ── 리그 인스턴스. 프리팹이 followed/뷰/캔버스를 이미 배선해 두었으므로 활성 상태로 만들어
|
|
|
|
|
|
// Critter Initialize() 의 LogError 경로(부모 없음 · followed null · 뷰/캔버스 못 찾음)를 전부 피한다.
|
|
|
|
|
|
// 값 보정은 같은 프레임 안에서 끝나므로(LateUpdate 이전) 잘못된 프레임이 그려지지 않는다.
|
|
|
|
|
|
s_rigRoot = Object.Instantiate(prefab);
|
|
|
|
|
|
s_rigRoot.name = RigName;
|
|
|
|
|
|
s_rigRoot.transform.SetPositionAndRotation(src.transform.position, src.transform.rotation);
|
|
|
|
|
|
|
|
|
|
|
|
s_mgr = s_rigRoot.GetComponentInChildren<PixelCameraManager>(true);
|
|
|
|
|
|
var view = s_rigRoot.GetComponentInChildren<CanvasViewCamera>(true);
|
|
|
|
|
|
s_canvas = s_rigRoot.GetComponentInChildren<UpscaledCanvas>(true);
|
|
|
|
|
|
if (s_mgr == null || view == null || s_canvas == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
LastLog = "리그 프리팹 구조가 예상과 다르다 — 조립 취소";
|
|
|
|
|
|
Log(LastLog);
|
|
|
|
|
|
Teardown("구조 불일치");
|
|
|
|
|
|
Faults++;
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
s_gameCam = s_mgr.GetComponent<Camera>();
|
|
|
|
|
|
s_viewCam = view.GetComponent<Camera>();
|
|
|
|
|
|
if (s_gameCam == null || s_viewCam == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
LastLog = "리그 카메라 컴포넌트 없음 — 조립 취소";
|
|
|
|
|
|
Log(LastLog);
|
|
|
|
|
|
Teardown("카메라 없음");
|
|
|
|
|
|
Faults++;
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 🔴 Critter 프리팹의 ViewCamera 에는 AudioListener 가 달려 있다(실측). 원본 Main Camera 의 것과
|
|
|
|
|
|
// 겹치면 Unity 가 "There are 2 audio listeners in the scene" 경고를 매 프레임 낸다 → 리그 것은 끈다.
|
|
|
|
|
|
var listeners = s_rigRoot.GetComponentsInChildren<AudioListener>(true);
|
|
|
|
|
|
for (int i = 0; i < listeners.Length; i++) if (listeners[i] != null) DestroyObj(listeners[i]);
|
|
|
|
|
|
|
|
|
|
|
|
int layer = Mathf.Clamp(cfg.rigLayer, 0, 31);
|
|
|
|
|
|
SetLayerRecursive(s_rigRoot.transform, layer);
|
|
|
|
|
|
s_rigRoot.tag = "Untagged";
|
|
|
|
|
|
s_gameCam.gameObject.tag = "Untagged";
|
|
|
|
|
|
s_viewCam.gameObject.tag = "Untagged";
|
|
|
|
|
|
|
|
|
|
|
|
// ── 쿼드 머티리얼을 런타임 인스턴스로 갈아 끼운다.
|
|
|
|
|
|
// Critter 의 UpscaledCanvas 는 sharedMaterial 에 _LowResTexture 를 직접 SetTexture 한다 →
|
|
|
|
|
|
// 그대로 두면 **디스크의 .mat 에셋이 더러워진다**(에디트 모드에서 특히). 인스턴스로 바꾸고
|
|
|
|
|
|
// OnEnable 을 다시 돌려 캐시를 갱신한다.
|
|
|
|
|
|
s_quadRenderer = s_canvas.GetComponent<MeshRenderer>();
|
|
|
|
|
|
if (s_quadRenderer != null && s_quadRenderer.sharedMaterial != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
s_quadSharedMat = s_quadRenderer.sharedMaterial;
|
|
|
|
|
|
s_matInstance = new Material(s_quadSharedMat);
|
|
|
|
|
|
s_matInstance.name = s_quadSharedMat.name + " (WL rig runtime)";
|
|
|
|
|
|
s_quadRenderer.sharedMaterial = s_matInstance;
|
|
|
|
|
|
s_canvas.enabled = false; s_canvas.enabled = true; // OnEnable 재실행 → 인스턴스 캐시
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── 프록시
|
|
|
|
|
|
if (cfg.proxyMode == PixelProxyMode.ChildOfCamera)
|
|
|
|
|
|
{
|
|
|
|
|
|
s_proxyGo = new GameObject(ProxyName);
|
|
|
|
|
|
s_proxyGo.layer = layer;
|
|
|
|
|
|
s_proxyGo.transform.SetParent(src.transform, false);
|
|
|
|
|
|
s_proxyGo.transform.localPosition = Vector3.zero;
|
|
|
|
|
|
s_proxyGo.transform.localRotation = Quaternion.identity;
|
|
|
|
|
|
s_proxyGo.transform.localScale = Vector3.one;
|
|
|
|
|
|
s_proxy = s_proxyGo.transform;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
s_proxyGo = new GameObject(ProxyName);
|
|
|
|
|
|
s_proxyGo.layer = layer;
|
|
|
|
|
|
s_proxyGo.transform.SetPositionAndRotation(src.transform.position, src.transform.rotation);
|
|
|
|
|
|
var mirror = s_proxyGo.AddComponent<PixelCameraProxyMirror>();
|
|
|
|
|
|
mirror.Source = src.transform;
|
|
|
|
|
|
s_proxy = s_proxyGo.transform;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── 저해상도 게임 카메라 = 원본이 보던 월드를 그대로 본다(리그 레이어만 뺀다)
|
|
|
|
|
|
s_gameCam.orthographic = true;
|
|
|
|
|
|
s_gameCam.nearClipPlane = s_snap.near;
|
|
|
|
|
|
s_gameCam.farClipPlane = s_snap.far;
|
|
|
|
|
|
s_gameCam.cullingMask = s_snap.cullingMask & ~(1 << layer);
|
|
|
|
|
|
s_gameCam.clearFlags = s_snap.clearFlags;
|
|
|
|
|
|
s_gameCam.backgroundColor = s_snap.background;
|
|
|
|
|
|
s_gameCam.allowHDR = s_snap.allowHDR;
|
|
|
|
|
|
s_gameCam.allowMSAA = s_snap.allowMSAA;
|
|
|
|
|
|
s_gameCam.useOcclusionCulling = s_snap.occlusionCulling;
|
|
|
|
|
|
s_gameCam.depth = s_snap.depth - 1f; // 원본(화면 Base)보다 먼저 RT 를 채운다
|
|
|
|
|
|
s_gameCam.targetTexture = null; // Critter 가 만든다
|
|
|
|
|
|
|
|
|
|
|
|
var srcData = s_src.GetUniversalAdditionalCameraData();
|
|
|
|
|
|
var gameData = s_gameCam.GetUniversalAdditionalCameraData();
|
|
|
|
|
|
if (gameData != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
gameData.renderType = CameraRenderType.Base;
|
|
|
|
|
|
if (gameData.cameraStack != null) gameData.cameraStack.Clear();
|
|
|
|
|
|
if (srcData != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
gameData.renderShadows = srcData.renderShadows;
|
|
|
|
|
|
gameData.volumeLayerMask = srcData.volumeLayerMask;
|
|
|
|
|
|
gameData.volumeTrigger = srcData.volumeTrigger;
|
|
|
|
|
|
gameData.renderPostProcessing = cfg.copyPostProcessingToGameCamera && srcData.renderPostProcessing;
|
|
|
|
|
|
gameData.antialiasing = srcData.antialiasing;
|
|
|
|
|
|
gameData.antialiasingQuality = srcData.antialiasingQuality;
|
|
|
|
|
|
gameData.stopNaN = srcData.stopNaN;
|
|
|
|
|
|
gameData.dithering = srcData.dithering;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Critter 값
|
|
|
|
|
|
float aspect = ScreenAspect();
|
2026-09-10 15:11:37 +00:00
|
|
|
|
int px = Mathf.Max(3, PxOf(cfg));
|
2026-09-10 14:30:35 +00:00
|
|
|
|
s_mgr.FollowedTransform = s_proxy;
|
|
|
|
|
|
s_mgr.VoxelGridMovement = cfg.voxelGridMovement;
|
|
|
|
|
|
s_mgr.SubpixelAdjustments = cfg.subpixelAdjustments;
|
|
|
|
|
|
s_mgr.FollowRotation = cfg.followRotation;
|
|
|
|
|
|
s_mgr.resolutionSynchronizationMode = ResolutionSynchronizationMode.SetHeight;
|
|
|
|
|
|
s_mgr.GameResolution = new Vector2Int(Mathf.Max(2, Mathf.RoundToInt(px * aspect)), px);
|
|
|
|
|
|
s_mgr.ControlGameZoom = cfg.controlGameZoom;
|
|
|
|
|
|
s_mgr.GameCameraZoom = GameZoomFor(cfg);
|
|
|
|
|
|
s_mgr.ViewCameraZoom = cfg.viewCameraZoom;
|
|
|
|
|
|
|
|
|
|
|
|
// ── 뷰 카메라 = 픽셀 쿼드만 그린다
|
|
|
|
|
|
s_viewCam.orthographic = true;
|
|
|
|
|
|
s_viewCam.cullingMask = 1 << layer;
|
|
|
|
|
|
s_viewCam.allowHDR = s_snap.allowHDR;
|
|
|
|
|
|
s_viewCam.allowMSAA = false;
|
|
|
|
|
|
s_viewCam.useOcclusionCulling = false;
|
|
|
|
|
|
var viewData = s_viewCam.GetUniversalAdditionalCameraData();
|
|
|
|
|
|
if (viewData != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
viewData.renderPostProcessing = false;
|
|
|
|
|
|
viewData.renderShadows = false;
|
|
|
|
|
|
viewData.renderType = cfg.viewCameraInMainStack ? CameraRenderType.Overlay : CameraRenderType.Base;
|
|
|
|
|
|
if (viewData.cameraStack != null && viewData.renderType == CameraRenderType.Base) viewData.cameraStack.Clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!cfg.viewCameraInMainStack) s_viewCam.depth = s_snap.depth + 1f;
|
|
|
|
|
|
|
|
|
|
|
|
// ── 원본 카메라 = 화면 Base 로 남되 월드는 그리지 않는다(투영만 픽셀 카메라와 맞춘다)
|
|
|
|
|
|
s_src.orthographic = true;
|
2026-09-10 15:11:37 +00:00
|
|
|
|
s_src.orthographicSize = OrthoOf(cfg);
|
2026-09-10 14:30:35 +00:00
|
|
|
|
s_src.cullingMask = cfg.mainCameraCullingMask;
|
|
|
|
|
|
if (cfg.mainCameraSolidBlack)
|
|
|
|
|
|
{
|
|
|
|
|
|
s_src.clearFlags = CameraClearFlags.SolidColor;
|
|
|
|
|
|
s_src.backgroundColor = Color.black;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (srcData != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (cfg.mainCameraPostProcessingOff) srcData.renderPostProcessing = false;
|
|
|
|
|
|
srcData.renderShadows = false;
|
|
|
|
|
|
if (cfg.viewCameraInMainStack && srcData.cameraStack != null)
|
|
|
|
|
|
srcData.cameraStack.Insert(0, s_viewCam); // ViewCamera → UICamera 순서
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Assembled++;
|
|
|
|
|
|
s_hasLastRot = false;
|
|
|
|
|
|
s_snapSuspended = false;
|
2026-09-10 15:11:37 +00:00
|
|
|
|
LastLog = "조립 — ortho " + OrthoOf(cfg).ToString("0.###")
|
2026-09-10 14:30:35 +00:00
|
|
|
|
+ " · RT " + s_mgr.GameResolution.x + "×" + s_mgr.GameResolution.y
|
|
|
|
|
|
+ " · zoom " + s_mgr.GameCameraZoom.ToString("0.####")
|
|
|
|
|
|
+ " · layer " + layer;
|
|
|
|
|
|
Log(LastLog);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>SO 값으로 Critter 게임 카메라 줌(= 캔버스 반높이)을 계산한다.</summary>
|
|
|
|
|
|
public static float GameZoomFor(WLPixelCameraSettings cfg)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (cfg == null) return 5f;
|
2026-09-10 15:11:37 +00:00
|
|
|
|
if (!cfg.compensateViewZoom) return OrthoOf(cfg);
|
|
|
|
|
|
return OrthoOf(cfg) / CanvasOnScreenLimit(PxOf(cfg));
|
2026-09-10 14:30:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>Critter 가 캔버스 가장자리를 넘지 않으려고 쓰는 줌 한계 = 1 − 2/pixelHeight.</summary>
|
|
|
|
|
|
public static float CanvasOnScreenLimit(int pixelHeight)
|
|
|
|
|
|
{
|
|
|
|
|
|
int py = Mathf.Max(3, pixelHeight);
|
|
|
|
|
|
return 1f - (2f / py);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static float ScreenAspect()
|
|
|
|
|
|
{
|
|
|
|
|
|
float w = Screen.width, h = Screen.height;
|
|
|
|
|
|
if (w <= 0f || h <= 0f) return 9f / 16f;
|
|
|
|
|
|
return w / h;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
// 복원
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>리그를 걷고 원본 카메라를 스냅샷 그대로 되돌린다.</summary>
|
|
|
|
|
|
public static void Teardown(string why)
|
|
|
|
|
|
{
|
|
|
|
|
|
bool had = s_rigRoot != null || s_proxyGo != null;
|
|
|
|
|
|
|
2026-09-12 08:18:29 +00:00
|
|
|
|
// 🔴 WL-814f — 무엇을 지우기 전에 Critter 컴포넌트를 **먼저 멈춘다**.
|
|
|
|
|
|
// `Object.Destroy` 는 프레임 끝에 처리되므로, 아래에서 RT·머티리얼 인스턴스를
|
|
|
|
|
|
// 지워도 같은 프레임에 `PixelCameraManager.LateUpdate` 가 한 번 더 돈다.
|
|
|
|
|
|
// 그러면 `targetTexture == null` 을 보고 RT 를 새로 만들어
|
|
|
|
|
|
// `UpscaledCanvas.SetCanvasRenderTexture` → 이미 지운 머티리얼에 `SetTexture` →
|
|
|
|
|
|
// **MissingReferenceException** 이 매 프레임 터진다(모드 전환 때 실측).
|
|
|
|
|
|
if (s_mgr != null) s_mgr.enabled = false;
|
|
|
|
|
|
if (s_canvas != null) s_canvas.enabled = false;
|
|
|
|
|
|
if (s_gameCam != null) s_gameCam.enabled = false;
|
|
|
|
|
|
if (s_viewCam != null) s_viewCam.enabled = false;
|
|
|
|
|
|
|
2026-09-12 08:33:19 +00:00
|
|
|
|
// 🔴 WL-814g — 그리고 리그를 **즉시 비활성**으로 내린다.
|
|
|
|
|
|
// Critter `PixelCameraManager.Initialize()`(PixelCameraManager.cs:123·132)는
|
|
|
|
|
|
// `viewCamera`/`upscaledCanvas` 를 **`FindAnyObjectByType` 로 찾는다**(두 필드는
|
|
|
|
|
|
// `[SerializeField]` 가 아니라 프리팹에 저장되지 않아 인스턴스마다 항상 null → 항상 Find).
|
|
|
|
|
|
// `Object.Destroy` 는 프레임 끝 처리라, 모드 전환에서 새 리그를 `Instantiate` 하는 순간
|
|
|
|
|
|
// **아직 살아 있는 이전 리그**의 뷰 카메라/캔버스를 새 매니저가 잡아 버리고,
|
|
|
|
|
|
// 프레임 끝에 그것들이 파괴되면서 `CanvasViewCamera.get_Aspect` 에서
|
|
|
|
|
|
// `MissingReferenceException` 이 매 프레임 터졌다(PD 실측 200건).
|
|
|
|
|
|
// `FindAnyObjectByType` 는 **비활성 오브젝트를 제외**하므로 여기서 내려두면 충돌이 사라진다.
|
|
|
|
|
|
if (s_rigRoot != null) s_rigRoot.SetActive(false);
|
|
|
|
|
|
|
2026-09-10 14:30:35 +00:00
|
|
|
|
// 원본 카메라 복원 — 스택에서 뷰 카메라부터 뺀다.
|
|
|
|
|
|
if (s_src != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var srcData = s_src.GetUniversalAdditionalCameraData();
|
|
|
|
|
|
if (srcData != null && srcData.cameraStack != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
srcData.cameraStack.Clear();
|
|
|
|
|
|
if (s_snap.stack != null)
|
|
|
|
|
|
for (int i = 0; i < s_snap.stack.Length; i++)
|
|
|
|
|
|
if (s_snap.stack[i] != null) srcData.cameraStack.Add(s_snap.stack[i]);
|
|
|
|
|
|
}
|
|
|
|
|
|
s_snap.Restore(s_src);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// RT 해제 — Critter 가 만든 것을 우리가 반납한다.
|
|
|
|
|
|
if (s_gameCam != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var rt = s_gameCam.targetTexture;
|
|
|
|
|
|
s_gameCam.targetTexture = null;
|
|
|
|
|
|
if (rt != null) { rt.Release(); DestroyObj(rt); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 쿼드 머티리얼 인스턴스 반납 + 원본 sharedMaterial 복귀(디스크 에셋 무변경 보장)
|
|
|
|
|
|
if (s_quadRenderer != null && s_quadSharedMat != null) s_quadRenderer.sharedMaterial = s_quadSharedMat;
|
|
|
|
|
|
if (s_matInstance != null) { DestroyObj(s_matInstance); s_matInstance = null; }
|
|
|
|
|
|
|
|
|
|
|
|
if (s_rigRoot != null) { DestroyObj(s_rigRoot); s_rigRoot = null; }
|
|
|
|
|
|
if (s_proxyGo != null) { DestroyObj(s_proxyGo); s_proxyGo = null; }
|
|
|
|
|
|
DestroyStrays();
|
|
|
|
|
|
|
|
|
|
|
|
s_proxy = null; s_gameCam = null; s_viewCam = null; s_mgr = null; s_canvas = null;
|
|
|
|
|
|
s_quadRenderer = null; s_quadSharedMat = null;
|
|
|
|
|
|
s_snapSuspended = false; s_hasLastRot = false;
|
|
|
|
|
|
s_src = null;
|
|
|
|
|
|
|
|
|
|
|
|
if (had)
|
|
|
|
|
|
{
|
|
|
|
|
|
Restored++;
|
|
|
|
|
|
LastLog = "복원 — " + why;
|
|
|
|
|
|
Log(LastLog);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>이름으로 남은 리그·프록시 잔재를 지운다(도메인 리로드·에디트 모드 재실행 대비).</summary>
|
|
|
|
|
|
static void DestroyStrays()
|
|
|
|
|
|
{
|
|
|
|
|
|
var all = Object.FindObjectsByType<PixelCameraManager>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
|
|
|
|
|
for (int i = 0; i < all.Length; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (all[i] == null) continue;
|
|
|
|
|
|
var root = all[i].transform.parent != null ? all[i].transform.parent.gameObject : all[i].gameObject;
|
|
|
|
|
|
if (root == s_rigRoot) continue;
|
2026-09-12 08:33:19 +00:00
|
|
|
|
// 🔴 WL-814g — 파괴는 프레임 끝이라, 먼저 비활성으로 내려 새 리그의
|
|
|
|
|
|
// `FindAnyObjectByType` 가 이 잔재를 잡지 못하게 한다(위 Teardown 주석 참조).
|
|
|
|
|
|
if (root.name == RigName) { root.SetActive(false); DestroyObj(root); }
|
2026-09-10 14:30:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 프록시(원본 카메라의 자식)도 이름으로 정리
|
|
|
|
|
|
var cam = Camera.main;
|
|
|
|
|
|
if (cam != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
for (int i = cam.transform.childCount - 1; i >= 0; i--)
|
|
|
|
|
|
{
|
|
|
|
|
|
var c = cam.transform.GetChild(i);
|
|
|
|
|
|
if (c != null && c.gameObject != s_proxyGo && c.name == ProxyName) DestroyObj(c.gameObject);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void DestroyObj(Object o)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (o == null) return;
|
|
|
|
|
|
if (Application.isPlaying) Object.Destroy(o);
|
|
|
|
|
|
else Object.DestroyImmediate(o);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void SetLayerRecursive(Transform t, int layer)
|
|
|
|
|
|
{
|
|
|
|
|
|
t.gameObject.layer = layer;
|
|
|
|
|
|
for (int i = 0; i < t.childCount; i++) SetLayerRecursive(t.GetChild(i), layer);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
// 회전 중 스냅 해제 (Critter 문서 권고 · 811o 구도 전환 · 보스 연출)
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
static void UpdateSnapSuspension(WLPixelCameraSettings cfg)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (s_mgr == null || s_src == null) { s_hasLastRot = false; return; }
|
|
|
|
|
|
if (!cfg.disableSnapWhileRotating)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (s_snapSuspended) { s_mgr.VoxelGridMovement = cfg.voxelGridMovement; s_mgr.SubpixelAdjustments = cfg.subpixelAdjustments; s_snapSuspended = false; }
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var rot = s_src.transform.rotation;
|
|
|
|
|
|
float dt = Mathf.Max(1e-4f, Time.unscaledDeltaTime);
|
|
|
|
|
|
if (!s_hasLastRot) { s_lastSrcRot = rot; s_hasLastRot = true; return; }
|
|
|
|
|
|
|
|
|
|
|
|
float degPerSec = Quaternion.Angle(s_lastSrcRot, rot) / dt;
|
|
|
|
|
|
s_lastSrcRot = rot;
|
|
|
|
|
|
|
|
|
|
|
|
float now = Time.unscaledTime;
|
|
|
|
|
|
if (degPerSec >= Mathf.Max(0f, cfg.rotationSnapOffDegPerSec))
|
|
|
|
|
|
{
|
|
|
|
|
|
s_snapResumeAt = now + Mathf.Max(0f, cfg.rotationSnapResumeSeconds);
|
|
|
|
|
|
if (!s_snapSuspended)
|
|
|
|
|
|
{
|
|
|
|
|
|
s_snapSuspended = true;
|
|
|
|
|
|
s_mgr.VoxelGridMovement = false;
|
|
|
|
|
|
s_mgr.SubpixelAdjustments = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (s_snapSuspended && now >= s_snapResumeAt)
|
|
|
|
|
|
{
|
|
|
|
|
|
s_snapSuspended = false;
|
|
|
|
|
|
s_mgr.VoxelGridMovement = cfg.voxelGridMovement;
|
|
|
|
|
|
s_mgr.SubpixelAdjustments = cfg.subpixelAdjustments;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
// 프로브 진입점
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>프로브·에디트 모드: 경계 폴링을 건너뛰고 지금 바로 조립한다.</summary>
|
|
|
|
|
|
public static bool ProbeAssemble(Camera src)
|
|
|
|
|
|
{
|
|
|
|
|
|
var cfg = Cfg;
|
|
|
|
|
|
if (cfg == null) return false;
|
|
|
|
|
|
if (IsAssembled) Teardown("probe 재조립");
|
|
|
|
|
|
return Assemble(cfg, src);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>프로브: 지금 바로 복원한다.</summary>
|
|
|
|
|
|
public static void ProbeTeardown() { Teardown("probe"); }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>프로브: 회전 스냅 판정을 1회 돌린다(에디트 모드).</summary>
|
|
|
|
|
|
public static void ProbeSnapTick() { var c = Cfg; if (c != null) UpdateSnapSuspension(c); }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>프로브: 원본 카메라 스냅샷을 한 줄로.</summary>
|
|
|
|
|
|
public static string SnapshotSummary() { return s_snap.Summary(); }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>프로브: 지금 원본 카메라 상태를 스냅샷과 같은 형식으로.</summary>
|
|
|
|
|
|
public static string CurrentSummary(Camera cam) { var s = new CamSnapshot(); s.Capture(cam); return s.Summary(); }
|
|
|
|
|
|
|
|
|
|
|
|
public static void ResetAll()
|
|
|
|
|
|
{
|
|
|
|
|
|
Teardown("reset");
|
|
|
|
|
|
Boundaries = Assembled = Restored = SkippedLobby = SkippedNoCam = SkippedNoPrefab = Faults = 0;
|
|
|
|
|
|
LastReason = LastLog = "";
|
2026-09-10 15:11:37 +00:00
|
|
|
|
s_faulted = false; s_rtOn = false; // WL-814d 오버라이드도 함께 걷는다
|
2026-09-10 14:30:35 +00:00
|
|
|
|
s_lastPc = null; s_lastMapId = int.MinValue; s_nextPoll = 0f;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void Log(string msg)
|
|
|
|
|
|
{
|
|
|
|
|
|
var cfg = Cfg;
|
|
|
|
|
|
if (cfg != null && cfg.verboseLog) Debug.Log("[PixelCameraRig] " + msg);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
// 러너
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
static GameObject s_runner;
|
|
|
|
|
|
|
|
|
|
|
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
|
|
|
|
|
static void Boot()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (s_runner != null) return;
|
2026-09-10 15:11:37 +00:00
|
|
|
|
if (!Active && !s_rtOn) return; // C8 — 러너 자체가 뜨지 않는다(WL-814d 오버라이드는 예외)
|
2026-09-10 14:30:35 +00:00
|
|
|
|
s_runner = new GameObject("[WL814b] PixelCameraRigRunner");
|
|
|
|
|
|
s_runner.hideFlags = HideFlags.HideAndDontSave;
|
|
|
|
|
|
s_runner.AddComponent<PixelCameraRigRunner>();
|
|
|
|
|
|
Object.DontDestroyOnLoad(s_runner);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
internal static void OnRunnerQuit() { Teardown("quit"); }
|
|
|
|
|
|
|
|
|
|
|
|
#if UNITY_EDITOR
|
|
|
|
|
|
/// <summary>에디터: Play 진입/이탈 경계에서 반드시 복원한다(에디트 모드 프로브 잔재 0).</summary>
|
|
|
|
|
|
[UnityEditor.InitializeOnLoadMethod]
|
|
|
|
|
|
static void EditorHook()
|
|
|
|
|
|
{
|
|
|
|
|
|
UnityEditor.EditorApplication.playModeStateChanged -= OnPlayModeChanged;
|
|
|
|
|
|
UnityEditor.EditorApplication.playModeStateChanged += OnPlayModeChanged;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void OnPlayModeChanged(UnityEditor.PlayModeStateChange s)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (s == UnityEditor.PlayModeStateChange.ExitingEditMode || s == UnityEditor.PlayModeStateChange.ExitingPlayMode)
|
|
|
|
|
|
Teardown("playmode " + s);
|
|
|
|
|
|
}
|
|
|
|
|
|
#endif
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>매 프레임 리그 틱. Critter(기본 실행 순서 0)보다 먼저 돌아야 회전 스냅 해제가 그 프레임에 반영된다.</summary>
|
|
|
|
|
|
[DefaultExecutionOrder(-100)]
|
|
|
|
|
|
internal sealed class PixelCameraRigRunner : MonoBehaviour
|
|
|
|
|
|
{
|
|
|
|
|
|
void Update() { PixelCameraRig.Tick(); }
|
|
|
|
|
|
void OnApplicationQuit() { PixelCameraRig.OnRunnerQuit(); }
|
|
|
|
|
|
void OnDestroy() { PixelCameraRig.OnRunnerQuit(); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>MirrorScript 모드용 프록시. 원본 카메라 트랜스폼을 읽기만 한다(역방향 쓰기 0 = 피드백 루프 0).</summary>
|
|
|
|
|
|
[DefaultExecutionOrder(-100)]
|
|
|
|
|
|
public sealed class PixelCameraProxyMirror : MonoBehaviour
|
|
|
|
|
|
{
|
|
|
|
|
|
public Transform Source;
|
|
|
|
|
|
|
|
|
|
|
|
void LateUpdate()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (Source == null) return;
|
|
|
|
|
|
transform.SetPositionAndRotation(Source.position, Source.rotation);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>프로브: 1회 미러(에디트 모드).</summary>
|
|
|
|
|
|
public void Mirror() { LateUpdate(); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>원본 Camera + URP 데이터의 되돌릴 값 전부.</summary>
|
|
|
|
|
|
internal struct CamSnapshot
|
|
|
|
|
|
{
|
|
|
|
|
|
public bool valid;
|
|
|
|
|
|
public bool ortho;
|
|
|
|
|
|
public float orthoSize, fov, near, far, depth;
|
|
|
|
|
|
public int cullingMask;
|
|
|
|
|
|
public CameraClearFlags clearFlags;
|
|
|
|
|
|
public Color background;
|
|
|
|
|
|
public bool allowHDR, allowMSAA, occlusionCulling;
|
|
|
|
|
|
public RenderTexture target;
|
|
|
|
|
|
|
|
|
|
|
|
public bool hasData;
|
|
|
|
|
|
public CameraRenderType renderType;
|
|
|
|
|
|
public bool renderPost, renderShadows;
|
|
|
|
|
|
public LayerMask volumeMask;
|
|
|
|
|
|
public Transform volumeTrigger;
|
|
|
|
|
|
public AntialiasingMode aa;
|
|
|
|
|
|
public AntialiasingQuality aaQuality;
|
|
|
|
|
|
public Camera[] stack;
|
|
|
|
|
|
|
|
|
|
|
|
public void Capture(Camera c)
|
|
|
|
|
|
{
|
|
|
|
|
|
valid = c != null;
|
|
|
|
|
|
if (!valid) return;
|
|
|
|
|
|
ortho = c.orthographic; orthoSize = c.orthographicSize; fov = c.fieldOfView;
|
|
|
|
|
|
near = c.nearClipPlane; far = c.farClipPlane; depth = c.depth;
|
|
|
|
|
|
cullingMask = c.cullingMask; clearFlags = c.clearFlags; background = c.backgroundColor;
|
|
|
|
|
|
allowHDR = c.allowHDR; allowMSAA = c.allowMSAA; occlusionCulling = c.useOcclusionCulling;
|
|
|
|
|
|
target = c.targetTexture;
|
|
|
|
|
|
|
|
|
|
|
|
var d = c.GetUniversalAdditionalCameraData();
|
|
|
|
|
|
hasData = d != null;
|
|
|
|
|
|
if (!hasData) { stack = null; return; }
|
|
|
|
|
|
renderType = d.renderType;
|
|
|
|
|
|
renderPost = d.renderPostProcessing;
|
|
|
|
|
|
renderShadows = d.renderShadows;
|
|
|
|
|
|
volumeMask = d.volumeLayerMask;
|
|
|
|
|
|
volumeTrigger = d.volumeTrigger;
|
|
|
|
|
|
aa = d.antialiasing;
|
|
|
|
|
|
aaQuality = d.antialiasingQuality;
|
|
|
|
|
|
|
|
|
|
|
|
var st = d.cameraStack;
|
|
|
|
|
|
if (st == null) { stack = null; return; }
|
|
|
|
|
|
stack = new Camera[st.Count];
|
|
|
|
|
|
for (int i = 0; i < st.Count; i++) stack[i] = st[i];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void Restore(Camera c)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!valid || c == null) return;
|
|
|
|
|
|
c.orthographic = ortho; c.orthographicSize = orthoSize; c.fieldOfView = fov;
|
|
|
|
|
|
c.nearClipPlane = near; c.farClipPlane = far; c.depth = depth;
|
|
|
|
|
|
c.cullingMask = cullingMask; c.clearFlags = clearFlags; c.backgroundColor = background;
|
|
|
|
|
|
c.allowHDR = allowHDR; c.allowMSAA = allowMSAA; c.useOcclusionCulling = occlusionCulling;
|
|
|
|
|
|
c.targetTexture = target;
|
|
|
|
|
|
|
|
|
|
|
|
if (!hasData) return;
|
|
|
|
|
|
var d = c.GetUniversalAdditionalCameraData();
|
|
|
|
|
|
if (d == null) return;
|
|
|
|
|
|
d.renderType = renderType;
|
|
|
|
|
|
d.renderPostProcessing = renderPost;
|
|
|
|
|
|
d.renderShadows = renderShadows;
|
|
|
|
|
|
d.volumeLayerMask = volumeMask;
|
|
|
|
|
|
d.volumeTrigger = volumeTrigger;
|
|
|
|
|
|
d.antialiasing = aa;
|
|
|
|
|
|
d.antialiasingQuality = aaQuality;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>비교용 한 줄 요약(프로브가 조립 전/후를 문자열로 대조한다).</summary>
|
|
|
|
|
|
public string Summary()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!valid) return "(none)";
|
|
|
|
|
|
var sb = new System.Text.StringBuilder(160);
|
|
|
|
|
|
sb.Append("ortho=").Append(ortho ? 1 : 0)
|
|
|
|
|
|
.Append(" size=").Append(orthoSize.ToString("0.####"))
|
|
|
|
|
|
.Append(" fov=").Append(fov.ToString("0.###"))
|
|
|
|
|
|
.Append(" near=").Append(near.ToString("0.###"))
|
|
|
|
|
|
.Append(" far=").Append(far.ToString("0.#"))
|
|
|
|
|
|
.Append(" depth=").Append(depth.ToString("0.##"))
|
|
|
|
|
|
.Append(" mask=").Append(cullingMask)
|
|
|
|
|
|
.Append(" clear=").Append((int)clearFlags)
|
|
|
|
|
|
.Append(" bg=").Append(background.r.ToString("0.###")).Append(',').Append(background.g.ToString("0.###")).Append(',').Append(background.b.ToString("0.###")).Append(',').Append(background.a.ToString("0.###"))
|
|
|
|
|
|
.Append(" hdr=").Append(allowHDR ? 1 : 0)
|
|
|
|
|
|
.Append(" msaa=").Append(allowMSAA ? 1 : 0)
|
|
|
|
|
|
.Append(" occ=").Append(occlusionCulling ? 1 : 0)
|
|
|
|
|
|
.Append(" rt=").Append(target == null ? 0 : 1);
|
|
|
|
|
|
if (hasData)
|
|
|
|
|
|
{
|
|
|
|
|
|
sb.Append(" type=").Append((int)renderType)
|
|
|
|
|
|
.Append(" post=").Append(renderPost ? 1 : 0)
|
|
|
|
|
|
.Append(" shadow=").Append(renderShadows ? 1 : 0)
|
|
|
|
|
|
.Append(" vmask=").Append(volumeMask.value)
|
|
|
|
|
|
.Append(" aa=").Append((int)aa)
|
|
|
|
|
|
.Append(" stack=").Append(stack == null ? 0 : stack.Length);
|
|
|
|
|
|
}
|
|
|
|
|
|
return sb.ToString();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|