using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; using System.Collections; using System.Collections.Generic; using UnityRawInput; using System.Linq; using Unity.Cinemachine; using Streamingle; using KindRetargeting; public class CameraManager : MonoBehaviour, IController { #region Classes private static class KeyMapping { private static readonly Dictionary _mapping; static KeyMapping() { _mapping = new Dictionary(RawKeySetup.KeyMapping); } public static bool TryGetRawKey(KeyCode keyCode, out RawKey rawKey) { return _mapping.TryGetValue(keyCode, out rawKey); } public static bool TryGetKeyCode(RawKey rawKey, out KeyCode keyCode) { var pair = _mapping.FirstOrDefault(x => x.Value == rawKey); keyCode = pair.Key; return keyCode != KeyCode.None; } public static bool IsValidRawKey(RawKey key) { return _mapping.ContainsValue(key); } } [System.Serializable] public class HotkeyCommand { public List rawKeys = new List(); [System.NonSerialized] private List unityKeys = new List(); [System.NonSerialized] public bool isRecording = false; [System.NonSerialized] private float recordStartTime; [System.NonSerialized] private const float MAX_RECORD_TIME = 2f; public void StartRecording() { isRecording = true; recordStartTime = Time.time; rawKeys.Clear(); } public void StopRecording() { isRecording = false; InitializeUnityKeys(); } public void UpdateRecording() { if (!isRecording) return; if (Time.time - recordStartTime > MAX_RECORD_TIME) { StopRecording(); return; } foreach (KeyCode keyCode in System.Enum.GetValues(typeof(KeyCode))) { if (Input.GetKeyDown(keyCode) && KeyMapping.TryGetRawKey(keyCode, out RawKey rawKey)) { if (!rawKeys.Contains(rawKey)) { rawKeys.Add(rawKey); } } } bool allKeysReleased = rawKeys.Any() && rawKeys.All(key => !Input.GetKey(KeyMapping.TryGetKeyCode(key, out KeyCode keyCode) ? keyCode : KeyCode.None)); if (allKeysReleased) { StopRecording(); } } public void InitializeUnityKeys() { unityKeys.Clear(); if (rawKeys == null || !rawKeys.Any()) return; foreach (var rawKey in rawKeys) { if (KeyMapping.TryGetKeyCode(rawKey, out KeyCode keyCode) && keyCode != KeyCode.None) { unityKeys.Add(keyCode); } } } public bool IsTriggered() { if (isRecording) return false; if (rawKeys == null || !rawKeys.Any()) return false; bool allRawKeysPressed = rawKeys.All(key => RawInput.IsKeyDown(key)); if (allRawKeysPressed) return true; if (unityKeys.Any()) { return unityKeys.All(key => Input.GetKey(key)); } return false; } public override string ToString() => rawKeys?.Any() == true ? string.Join(" + ", rawKeys) : "설정되지 않음"; } [System.Serializable] public class CameraPreset : ISerializationCallbackReceiver { public string presetName = "New Camera Preset"; public CinemachineCamera virtualCamera; public HotkeyCommand hotkey; [System.NonSerialized] public bool isEditingHotkey = false; // 마우스 조작 허용 여부 [Tooltip("이 카메라에서 마우스 조작(회전, 팬, 줌 등)을 허용할지 여부")] public bool allowMouseControl = true; // 직렬화 버전 체크용 (기존 데이터 마이그레이션) [SerializeField, HideInInspector] private bool _initialized = false; public void OnBeforeSerialize() { } public void OnAfterDeserialize() { // 기존 데이터(버전 업 전)는 _initialized가 false이므로 기본값 적용 if (!_initialized) { allowMouseControl = true; _initialized = true; } } // 프리셋별 초기 상태 저장 (Alt+Q 복원용) [System.NonSerialized] public Vector3 initialPosition; [System.NonSerialized] public Quaternion initialRotation; [System.NonSerialized] public bool hasInitialState = false; // 프리셋별 orbit 상태 저장 (카메라 전환 시 유지용) [System.NonSerialized] public float savedHorizontalAngle; [System.NonSerialized] public float savedVerticalAngle; [System.NonSerialized] public float savedDistance; [System.NonSerialized] public Vector3 savedFocusPoint; [System.NonSerialized] public bool hasOrbitState = false; // 프리셋별 FOV 저장 (Alt+Q 복원용) [System.NonSerialized] public float initialFOV; [System.NonSerialized] public float savedFOV; [System.NonSerialized] public bool hasInitialFOV = false; public CameraPreset(CinemachineCamera camera) { virtualCamera = camera; presetName = camera?.gameObject.name ?? "Unnamed Camera"; hotkey = new HotkeyCommand(); allowMouseControl = true; } public bool IsValid() => virtualCamera != null && hotkey != null; // 초기 상태 저장 (Alt+Q 복원용) public void SaveInitialState() { if (virtualCamera == null) return; initialPosition = virtualCamera.transform.position; initialRotation = virtualCamera.transform.rotation; hasInitialState = true; // FOV 초기값 저장 if (virtualCamera.Lens.FieldOfView > 0) { initialFOV = virtualCamera.Lens.FieldOfView; savedFOV = initialFOV; hasInitialFOV = true; } } // 초기 상태 복원 (Alt+Q) public void RestoreInitialState() { if (!hasInitialState || virtualCamera == null) return; virtualCamera.transform.position = initialPosition; virtualCamera.transform.rotation = initialRotation; // FOV 초기값 복원 if (hasInitialFOV) { var lens = virtualCamera.Lens; lens.FieldOfView = initialFOV; virtualCamera.Lens = lens; savedFOV = initialFOV; } } // orbit 상태 저장 (카메라 전환 시) public void SaveOrbitState(float hAngle, float vAngle, float dist, Vector3 focus, float fov) { savedHorizontalAngle = hAngle; savedVerticalAngle = vAngle; savedDistance = dist; savedFocusPoint = focus; savedFOV = fov; hasOrbitState = true; } // orbit 상태 복원 (카메라 전환 시) public bool TryRestoreOrbitState(out float hAngle, out float vAngle, out float dist, out Vector3 focus, out float fov) { hAngle = savedHorizontalAngle; vAngle = savedVerticalAngle; dist = savedDistance; focus = savedFocusPoint; fov = savedFOV; return hasOrbitState; } } #endregion #region Events public delegate void CameraChangedEventHandler(CameraPreset oldPreset, CameraPreset newPreset); public event CameraChangedEventHandler OnCameraChanged; #endregion #region Fields [SerializeField] public List cameraPresets = new List(); [Header("Camera Control Settings")] [SerializeField, Range(0.5f, 10f)] private float rotationSensitivity = 2f; [SerializeField, Range(0.005f, 0.1f)] private float panSpeed = 0.02f; [SerializeField, Range(0.05f, 0.5f)] private float zoomSpeed = 0.1f; [SerializeField, Range(1f, 20f)] private float orbitSpeed = 10f; [Header("Smoothing")] [SerializeField, Range(0f, 0.95f)] private float movementSmoothing = 0.1f; [SerializeField, Range(0f, 0.95f)] private float rotationSmoothing = 0.1f; [Header("Zoom Limits")] [SerializeField] private float minZoomDistance = 0.5f; [SerializeField] private float maxZoomDistance = 50f; [Header("FOV Settings")] [SerializeField, Range(0.1f, 5f)] private float fovSensitivity = 1f; [SerializeField] private float minFOV = 1f; [SerializeField] private float maxFOV = 90f; [Header("Rotation Target")] [Tooltip("체크하면 아바타 머리를 자동으로 찾아 회전 중심점으로 사용합니다.")] [SerializeField] private bool useAvatarHeadAsTarget = true; [Tooltip("수동으로 회전 중심점을 지정합니다. (useAvatarHeadAsTarget이 false일 때 사용)")] [SerializeField] private Transform manualRotationTarget; [Header("Camera Blend Transition")] [Tooltip("블렌드 전환 사용 여부 (크로스 디졸브)")] [SerializeField] private bool useBlendTransition = false; [Tooltip("블렌드 전환 시간 (초)")] [SerializeField, Range(0.1f, 2f)] private float blendTime = 0.5f; [Tooltip("실시간 블렌딩 (두 카메라 동시 렌더링). 비활성화 시 스냅샷 블렌딩 사용")] [SerializeField] private bool useRealtimeBlend = true; // 블렌드용 렌더 텍스처와 카메라 private RenderTexture prevCameraRenderTexture; // 블렌딩용 이전 카메라 렌더 텍스처 private Camera blendCamera; private CinemachineCamera currentCamera; private InputHandler inputHandler; private CameraPreset currentPreset; // 드론 카메라 모드 private DroneCameraMode droneCameraMode; private GamepadInputHandler gamepadInput; // 프리셋별 드론 상태 저장 private class SavedDroneState { public bool wasActive; public Vector3 position; public Quaternion rotation; public float fov; public Transform target; public int targetIndex; } private Dictionary savedDroneStates = new Dictionary(); // 오빗 카메라 상태 (각도 기반) private float horizontalAngle; private float verticalAngle; private float currentDistance; private Vector3 focusPoint; // 타겟 값 (스무딩용) private float targetHorizontalAngle; private float targetVerticalAngle; private float targetDistance; private Vector3 targetFocusPoint; // 아바타 머리 추적 private Transform avatarHeadTransform; // 스트림덱 연동 private StreamDeckServerManager streamDeckManager; // 블렌드 전환 상태 private Coroutine blendCoroutine; private bool isBlending = false; #endregion #region Properties private bool IsValidSetup => currentCamera != null && inputHandler != null; public CameraPreset CurrentPreset => currentPreset; #endregion #region Unity Messages private void Awake() { InitializeInputHandler(); InitializeRawInput(); InitializeCameraPresets(); // StreamDeckServerManager 찾기 streamDeckManager = FindAnyObjectByType(); if (streamDeckManager == null) { Debug.LogWarning("[CameraManager] StreamDeckServerManager를 찾을 수 없습니다. 스트림덱 연동이 비활성화됩니다."); } } private void Start() { // Start에서 아바타 머리 다시 찾기 (다른 스크립트들이 초기화된 후) if (useAvatarHeadAsTarget && avatarHeadTransform == null) { FindAvatarHead(); } } private void OnDestroy() { if (RawInput.IsRunning) { RawInput.OnKeyDown -= HandleRawKeyDown; RawInput.Stop(); } // 블렌드 리소스 정리 if (prevCameraRenderTexture != null) { prevCameraRenderTexture.Release(); Destroy(prevCameraRenderTexture); prevCameraRenderTexture = null; } if (blendCamera != null) { Destroy(blendCamera.gameObject); blendCamera = null; } } private void Update() { // 게임패드는 IsValidSetup과 무관하게 항상 처리 HandleGamepadButtons(); if (!IsValidSetup) return; UpdateHotkeyRecording(); HandleCameraControls(); HandleHotkeys(); } private void UpdateHotkeyRecording() { foreach (var preset in cameraPresets) { if (preset?.hotkey?.isRecording == true) { preset.hotkey.UpdateRecording(); } } } private void HandleGamepadButtons() { if (gamepadInput == null || !gamepadInput.IsGamepadConnected) return; // A 버튼: 드론 모드 토글 if (gamepadInput.ButtonSouthPressed) { ToggleDroneMode(); } // 드론 모드 전용 버튼 if (droneCameraMode != null && droneCameraMode.IsActive) { // B 버튼: 현재 위치를 홈으로 저장 if (gamepadInput.ButtonEastPressed) { droneCameraMode.SaveCurrentAsHome(); } // Y 버튼: 홈 위치로 복귀 if (gamepadInput.ButtonNorthPressed) { droneCameraMode.ReturnToHome(); } } // D-pad 타겟팅 (드론 모드 관계없이 항상 동작) // Update 실행 순서에 의존하지 않도록 Gamepad.current에서 직접 읽음 if (droneCameraMode != null) { var gamepad = UnityEngine.InputSystem.Gamepad.current; if (gamepad != null) { if (gamepad.dpad.up.wasPressedThisFrame) droneCameraMode.TryTargetFromCamera(); if (gamepad.dpad.down.wasPressedThisFrame) droneCameraMode.ClearTarget(); if (gamepad.dpad.right.wasPressedThisFrame) droneCameraMode.CycleTargetPublic(1); if (gamepad.dpad.left.wasPressedThisFrame) droneCameraMode.CycleTargetPublic(-1); } } // Start 버튼: 다음 카메라 프리셋 if (gamepadInput.StartPressed) { int currentIndex = currentPreset != null ? cameraPresets.IndexOf(currentPreset) : -1; int nextIndex = (currentIndex + 1) % cameraPresets.Count; Set(nextIndex); } } /// /// 드론 모드를 토글합니다. 게임패드가 연결되어 있어야 합니다. /// public void ToggleDroneMode() { if (droneCameraMode == null || currentCamera == null) return; if (droneCameraMode.IsActive) { droneCameraMode.Deactivate(); // orbit 상태 복원 if (currentPreset != null && currentPreset.hasOrbitState) { if (currentPreset.TryRestoreOrbitState(out float hAngle, out float vAngle, out float dist, out Vector3 focus, out float fov)) { targetHorizontalAngle = hAngle; targetVerticalAngle = vAngle; targetDistance = dist; targetFocusPoint = focus; horizontalAngle = hAngle; verticalAngle = vAngle; currentDistance = dist; focusPoint = focus; } } Debug.Log("[CameraManager] 드론 모드 해제"); } else { // 드론 모드 진입 전 orbit 상태 저장 if (currentPreset != null) { float currentFOV = currentPreset.virtualCamera != null ? currentPreset.virtualCamera.Lens.FieldOfView : 60f; currentPreset.SaveOrbitState(targetHorizontalAngle, targetVerticalAngle, targetDistance, targetFocusPoint, currentFOV); } droneCameraMode.Activate(currentCamera); Debug.Log("[CameraManager] 드론 모드 진입"); } // StreamDeck에 상태 변경 알림 NotifyStreamDeckCameraStateChanged(); } public bool IsDroneModeActive => droneCameraMode != null && droneCameraMode.IsActive; private void NotifyStreamDeckCameraStateChanged() { if (streamDeckManager != null) { streamDeckManager.NotifyCameraChanged(); } } #endregion #region Initialization private void InitializeInputHandler() { inputHandler = GetComponent(); if (inputHandler == null) { inputHandler = gameObject.AddComponent(); } // 게임패드 + 드론 모드 초기화 gamepadInput = GetComponent(); if (gamepadInput == null) { gamepadInput = gameObject.AddComponent(); } droneCameraMode = GetComponent(); if (droneCameraMode == null) { droneCameraMode = gameObject.AddComponent(); } } private void InitializeRawInput() { if (!RawInput.IsRunning) { RawInput.Start(); RawInput.WorkInBackground = true; } // 중복 구독 방지를 위해 먼저 해제 후 구독 RawInput.OnKeyDown -= HandleRawKeyDown; RawInput.OnKeyDown += HandleRawKeyDown; } private void InitializeCameraPresets() { if (cameraPresets == null) { cameraPresets = new List(); } if (!cameraPresets.Any()) { return; } foreach (var preset in cameraPresets.Where(p => p?.hotkey != null)) { preset.hotkey.InitializeUnityKeys(); } // 모든 프리셋의 초기 상태 저장 foreach (var preset in cameraPresets.Where(p => p?.IsValid() == true)) { if (!preset.hasInitialState) { preset.SaveInitialState(); } } // 아바타 머리 찾기 FindAvatarHead(); Set(0); } private void FindAvatarHead() { if (!useAvatarHeadAsTarget) return; // CustomRetargetingScript를 가진 아바타 찾기 var retargetingScripts = FindObjectsByType(FindObjectsSortMode.None); foreach (var script in retargetingScripts) { if (script == null || !script.gameObject.activeInHierarchy) continue; // targetAnimator가 설정되어 있으면 사용 Animator animator = script.targetAnimator; // targetAnimator가 null이면 같은 GameObject의 Animator 시도 if (animator == null) { animator = script.GetComponent(); } if (animator != null) { avatarHeadTransform = animator.GetBoneTransform(HumanBodyBones.Head); if (avatarHeadTransform != null) { return; } } } Debug.LogWarning("[CameraManager] 활성화된 아바타의 Head 본을 찾을 수 없습니다. 수동 타겟을 사용하거나 원점을 사용합니다."); } /// /// 현재 회전 중심점을 반환합니다. /// 우선순위: 아바타 머리 > 수동 타겟 > 원점 /// private Vector3 GetRotationCenter() { if (useAvatarHeadAsTarget && avatarHeadTransform != null) { return avatarHeadTransform.position; } else if (manualRotationTarget != null) { return manualRotationTarget.position; } return Vector3.zero; } #endregion #region Input Handling private void HandleRawKeyDown(RawKey key) { if (key == default(RawKey)) return; TryActivatePresetByInput(preset => { if (preset?.hotkey == null) return false; return preset.hotkey.IsTriggered(); }); } private void HandleHotkeys() { if (Input.anyKeyDown) { TryActivatePresetByInput(preset => { if (preset?.hotkey == null) return false; return preset.hotkey.IsTriggered(); }); } } private void TryActivatePresetByInput(System.Func predicate) { var matchingPreset = cameraPresets?.FirstOrDefault(predicate); if (matchingPreset != null) { Set(cameraPresets.IndexOf(matchingPreset)); } } #endregion #region Camera Controls /// /// 현재 카메라 위치에서 각도와 거리를 초기화합니다. /// private void InitializeOrbitState() { if (currentCamera == null) return; focusPoint = GetRotationCenter(); targetFocusPoint = focusPoint; Vector3 direction = currentCamera.transform.position - focusPoint; currentDistance = direction.magnitude; targetDistance = currentDistance; if (currentDistance > 0.01f) { direction.Normalize(); horizontalAngle = Mathf.Atan2(direction.x, direction.z) * Mathf.Rad2Deg; verticalAngle = Mathf.Asin(Mathf.Clamp(direction.y, -1f, 1f)) * Mathf.Rad2Deg; } targetHorizontalAngle = horizontalAngle; targetVerticalAngle = verticalAngle; } private void HandleCameraControls() { if (!IsValidSetup) return; if (currentCamera == null) return; // 드론 모드 활성 시 마우스 제어 스킵 if (droneCameraMode != null && droneCameraMode.IsActive) return; // 현재 프리셋이 마우스 조작을 허용하지 않으면 스킵 if (currentPreset != null && !currentPreset.allowMouseControl) return; // Alt+Q로 초기 위치로 복원 if (Input.GetKey(KeyCode.LeftAlt) && Input.GetKeyDown(KeyCode.Q)) { RestoreInitialCameraState(); return; } HandleInput(); UpdateCameraPosition(); } private void HandleInput() { // 입력 우선순위 처리: Orbit > CtrlRightZoom > Zoom > FOV > Rotation > Panning if (inputHandler.IsOrbitActive()) { HandleOrbiting(); } else if (inputHandler.IsCtrlRightZoomActive()) { HandleCtrlRightZoom(); } else if (inputHandler.IsZoomActive()) { HandleDragZoom(); } else if (inputHandler.IsFOVActive()) { HandleFOV(); } else if (inputHandler.IsRightMouseHeld()) { HandleRotation(); } else if (inputHandler.IsMiddleMouseHeld()) { HandlePanning(); } // 휠 줌은 항상 처리 HandleWheelZoom(); } /// /// 우클릭 드래그: 수평 + 수직 회전 (현재 비활성화 - Alt+우클릭 사용) /// private void HandleRotation() { // 우클릭만으로는 회전하지 않음 - Alt+우클릭(Orbit)으로 대체 } /// /// Alt + 우클릭: 자유 궤도 회전 (X, Y축) /// private void HandleOrbiting() { Vector2 delta = inputHandler.GetLookDelta(); if (delta.sqrMagnitude < float.Epsilon) return; // 회전 속도 targetHorizontalAngle += delta.x * rotationSensitivity; targetVerticalAngle -= delta.y * rotationSensitivity; targetVerticalAngle = Mathf.Clamp(targetVerticalAngle, -80f, 80f); } /// /// 휠클릭 드래그: 패닝 (초점 이동) /// private void HandlePanning() { Vector2 delta = inputHandler.GetLookDelta(); if (delta.sqrMagnitude < float.Epsilon) return; // 거리에 비례하여 패닝 속도 조절 - 0.2배 float speedMultiplier = targetDistance * panSpeed * 0.175f; // 카메라의 로컬 축 기준으로 초점 이동 Vector3 right = currentCamera.transform.right; Vector3 up = currentCamera.transform.up; Vector3 panOffset = (-right * delta.x - up * delta.y) * speedMultiplier; targetFocusPoint += panOffset; } /// /// 마우스 휠: 줌 /// private void HandleWheelZoom() { float scroll = inputHandler.GetZoomDelta(); if (Mathf.Abs(scroll) < 0.01f) return; // 거리에 비례하여 줌 속도 조절 (더 자연스러운 느낌) - 0.4배 float zoomDelta = scroll * zoomSpeed * targetDistance * 0.4f; targetDistance -= zoomDelta; targetDistance = Mathf.Clamp(targetDistance, minZoomDistance, maxZoomDistance); } /// /// Ctrl + 좌클릭 드래그: 줌 /// private void HandleDragZoom() { Vector2 delta = inputHandler.GetLookDelta(); if (delta.sqrMagnitude < float.Epsilon) return; float zoomDelta = delta.y * zoomSpeed * targetDistance * 0.5f; targetDistance -= zoomDelta; targetDistance = Mathf.Clamp(targetDistance, minZoomDistance, maxZoomDistance); } /// /// Ctrl + 우클릭 드래그: 줌 (밀기 = 확대, 당기기 = 축소) /// private void HandleCtrlRightZoom() { Vector2 delta = inputHandler.GetLookDelta(); if (delta.sqrMagnitude < float.Epsilon) return; // 위로 밀면 확대 (거리 감소), 아래로 밀면 축소 (거리 증가) float zoomDelta = delta.y * zoomSpeed * targetDistance * 0.5f; targetDistance -= zoomDelta; targetDistance = Mathf.Clamp(targetDistance, minZoomDistance, maxZoomDistance); } /// /// Shift + 좌클릭/우클릭 드래그: FOV 조절 (위로 밀면 FOV 감소=줌인, 아래로 밀면 FOV 증가=줌아웃) /// private void HandleFOV() { if (currentCamera == null) return; Vector2 delta = inputHandler.GetLookDelta(); if (delta.sqrMagnitude < float.Epsilon) return; // 위로 밀면 FOV 감소 (줌인 효과), 아래로 밀면 FOV 증가 (줌아웃 효과) float fovDelta = -delta.y * fovSensitivity; var lens = currentCamera.Lens; lens.FieldOfView = Mathf.Clamp(lens.FieldOfView + fovDelta, minFOV, maxFOV); currentCamera.Lens = lens; } /// /// 스무딩을 적용하여 카메라 위치 업데이트 /// private void UpdateCameraPosition() { float dt = Time.deltaTime; // 스무딩 적용 if (movementSmoothing > 0.01f) { float smoothSpeed = (1f - movementSmoothing) * 15f; horizontalAngle = Mathf.Lerp(horizontalAngle, targetHorizontalAngle, dt * smoothSpeed); verticalAngle = Mathf.Lerp(verticalAngle, targetVerticalAngle, dt * smoothSpeed); currentDistance = Mathf.Lerp(currentDistance, targetDistance, dt * smoothSpeed); focusPoint = Vector3.Lerp(focusPoint, targetFocusPoint, dt * smoothSpeed); } else { horizontalAngle = targetHorizontalAngle; verticalAngle = targetVerticalAngle; currentDistance = targetDistance; focusPoint = targetFocusPoint; } // 구면 좌표계에서 카메라 위치 계산 float horizontalRad = horizontalAngle * Mathf.Deg2Rad; float verticalRad = verticalAngle * Mathf.Deg2Rad; Vector3 offset = new Vector3( Mathf.Sin(horizontalRad) * Mathf.Cos(verticalRad), Mathf.Sin(verticalRad), Mathf.Cos(horizontalRad) * Mathf.Cos(verticalRad) ) * currentDistance; currentCamera.transform.position = focusPoint + offset; currentCamera.transform.LookAt(focusPoint); } private void RestoreInitialCameraState() { if (currentPreset == null || !currentPreset.hasInitialState) return; currentPreset.RestoreInitialState(); // orbit 상태도 리셋 (초기 위치 기반으로 다시 계산) currentPreset.hasOrbitState = false; InitializeOrbitState(); } #endregion #region Camera Management public void Set(int index) { // 블렌드 전환 사용 시 if (useBlendTransition) { SetWithBlend(index); return; } SetImmediate(index); } /// /// 크로스 디졸브 블렌드와 함께 카메라 전환 /// public void SetWithBlend(int index, float? customBlendTime = null) { if (isBlending) return; if (!ValidateCameraIndex(index)) return; // 같은 카메라로 전환 시도 시 무시 if (currentPreset != null && cameraPresets.IndexOf(currentPreset) == index) return; // 기존 코루틴 중단 if (blendCoroutine != null) { StopCoroutine(blendCoroutine); // UI 오버레이 숨기기 var overlay = CameraBlendOverlay.Instance; if (overlay != null) { overlay.Hide(); } } blendCoroutine = StartCoroutine(BlendTransitionCoroutine(index, customBlendTime ?? blendTime)); } /// /// 즉시 카메라 전환 (페이드 없음) /// public void SetImmediate(int index) { if (!ValidateCameraIndex(index)) return; var newPreset = cameraPresets[index]; if (!newPreset.IsValid()) { Debug.LogError($"[CameraManager] 프리셋이 유효하지 않습니다 - 인덱스: {index}"); return; } var oldPreset = currentPreset; var newCameraName = newPreset.virtualCamera?.gameObject.name ?? "Unknown"; // 드론 모드 활성 중이면 상태 저장 후 해제 if (droneCameraMode != null && droneCameraMode.IsActive && oldPreset != null) { var cam = oldPreset.virtualCamera; savedDroneStates[oldPreset] = new SavedDroneState { wasActive = true, position = cam != null ? cam.transform.position : Vector3.zero, rotation = cam != null ? cam.transform.rotation : Quaternion.identity, fov = cam != null ? cam.Lens.FieldOfView : 60f, target = droneCameraMode.CurrentTarget, targetIndex = -1 }; droneCameraMode.Deactivate(); } else if (droneCameraMode != null && !droneCameraMode.IsActive && oldPreset != null) { // 드론 모드 비활성 상태에서 전환 시 이전 저장 상태 유지 (덮어쓰지 않음) } // 이전 프리셋의 orbit 상태 저장 if (oldPreset != null && oldPreset.allowMouseControl) { float currentFOV = oldPreset.virtualCamera != null ? oldPreset.virtualCamera.Lens.FieldOfView : 60f; oldPreset.SaveOrbitState(targetHorizontalAngle, targetVerticalAngle, targetDistance, targetFocusPoint, currentFOV); } currentPreset = newPreset; UpdateCameraPriorities(newPreset.virtualCamera); // 새 프리셋의 orbit 상태 복원 또는 초기화 if (newPreset.hasOrbitState && newPreset.allowMouseControl) { // 저장된 orbit 상태 복원 if (newPreset.TryRestoreOrbitState(out float hAngle, out float vAngle, out float dist, out Vector3 focus, out float fov)) { targetHorizontalAngle = hAngle; targetVerticalAngle = vAngle; targetDistance = dist; targetFocusPoint = focus; horizontalAngle = hAngle; verticalAngle = vAngle; currentDistance = dist; focusPoint = focus; // FOV 복원 if (newPreset.virtualCamera != null && fov > 0) { var lens = newPreset.virtualCamera.Lens; lens.FieldOfView = fov; newPreset.virtualCamera.Lens = lens; } } } else { // 첫 진입 시 현재 카메라 위치 기반으로 초기화 InitializeOrbitState(); } OnCameraChanged?.Invoke(oldPreset, newPreset); // 이전에 저장된 드론 상태가 있으면 복원 if (droneCameraMode != null && savedDroneStates.TryGetValue(newPreset, out var droneState) && droneState.wasActive) { var cam = newPreset.virtualCamera; if (cam != null) { cam.transform.position = droneState.position; cam.transform.rotation = droneState.rotation; var lens = cam.Lens; lens.FieldOfView = droneState.fov; cam.Lens = lens; } droneCameraMode.Activate(cam); if (droneState.target != null) { droneCameraMode.ClearTarget(); // 타겟 복원은 public 메서드로 droneCameraMode.SetTarget(droneState.target); } } // 스트림덱에 카메라 변경 알림 전송 if (streamDeckManager != null) { streamDeckManager.NotifyCameraChanged(); } } private bool ValidateCameraIndex(int index) { if (cameraPresets == null) { Debug.LogError("[CameraManager] cameraPresets가 null입니다!"); return false; } if (index < 0 || index >= cameraPresets.Count) { Debug.LogError($"[CameraManager] 잘못된 인덱스: {index}, 유효 범위: 0-{cameraPresets.Count - 1}"); return false; } return true; } /// /// 블렌드용 카메라와 렌더 텍스처 생성 /// private void EnsureBlendCamera() { if (blendCamera != null) return; // 메인 카메라 찾기 Camera mainCamera = Camera.main; if (mainCamera == null) { Debug.LogError("[CameraManager] 메인 카메라를 찾을 수 없습니다."); return; } // 블렌드용 카메라 생성 - 독립적인 오브젝트로 (부모 없음) // Cinemachine Brain의 영향을 피하기 위해 부모를 설정하지 않음 GameObject blendCamObj = new GameObject("BlendCamera_Independent"); blendCamObj.transform.SetParent(null); // 부모 없이 루트에 배치 blendCamera = blendCamObj.AddComponent(); // 메인 카메라 설정 복사 blendCamera.CopyFrom(mainCamera); blendCamera.depth = mainCamera.depth - 1; // 메인 카메라보다 먼저 렌더링 blendCamera.enabled = false; // 수동으로 렌더링할 것임 // URP 카메라 데이터 복사 (포스트 프로세싱 등) var mainCameraData = mainCamera.GetUniversalAdditionalCameraData(); var blendCameraData = blendCamera.GetUniversalAdditionalCameraData(); if (mainCameraData != null && blendCameraData != null) { blendCameraData.renderPostProcessing = mainCameraData.renderPostProcessing; blendCameraData.antialiasing = mainCameraData.antialiasing; blendCameraData.antialiasingQuality = mainCameraData.antialiasingQuality; blendCameraData.renderShadows = mainCameraData.renderShadows; blendCameraData.requiresColorOption = mainCameraData.requiresColorOption; blendCameraData.requiresDepthOption = mainCameraData.requiresDepthOption; blendCameraData.dithering = mainCameraData.dithering; blendCameraData.stopNaN = mainCameraData.stopNaN; blendCameraData.volumeLayerMask = mainCameraData.volumeLayerMask; blendCameraData.volumeTrigger = mainCameraData.volumeTrigger; } } /// /// 크로스 디졸브 블렌드 전환 코루틴 /// private IEnumerator BlendTransitionCoroutine(int targetIndex, float duration) { if (useRealtimeBlend) { yield return RealtimeBlendTransitionCoroutine(targetIndex, duration); } else { yield return SnapshotBlendTransitionCoroutine(targetIndex, duration); } } /// /// 스냅샷 블렌딩 (이전 화면 정지) - UI 오버레이 방식 /// private IEnumerator SnapshotBlendTransitionCoroutine(int targetIndex, float duration) { isBlending = true; // 메인 카메라의 현재 위치/회전/FOV를 저장 Camera mainCamera = Camera.main; if (mainCamera == null) { SetImmediate(targetIndex); isBlending = false; blendCoroutine = null; yield break; } Vector3 prevCameraPosition = mainCamera.transform.position; Quaternion prevCameraRotation = mainCamera.transform.rotation; float prevCameraFOV = mainCamera.fieldOfView; // 블렌드 텍스처 준비 EnsurePrevCameraRenderTexture(); if (prevCameraRenderTexture == null) { SetImmediate(targetIndex); isBlending = false; blendCoroutine = null; yield break; } // 블렌드 카메라 준비 EnsureBlendCamera(); if (blendCamera == null) { SetImmediate(targetIndex); isBlending = false; blendCoroutine = null; yield break; } // UI 오버레이 준비 var overlay = CameraBlendOverlay.GetOrCreate(); // 블렌드 카메라를 현재 메인 카메라 위치로 설정하고 렌더링 (스냅샷 캡처) blendCamera.transform.SetPositionAndRotation(prevCameraPosition, prevCameraRotation); blendCamera.fieldOfView = prevCameraFOV; blendCamera.targetTexture = prevCameraRenderTexture; blendCamera.Render(); // UI 오버레이 표시 (알파 1 = 완전 불투명) overlay.Show(prevCameraRenderTexture); overlay.SetAlpha(1f); // 새 카메라로 전환 SetImmediate(targetIndex); // 블렌드 진행: 알파 1 → 0 (스냅샷이 서서히 사라지고 새 카메라가 드러남) float elapsed = 0f; while (elapsed < duration) { // 다음 프레임까지 대기 yield return null; elapsed += Time.deltaTime; float t = Mathf.Clamp01(elapsed / duration); // 부드러운 이징 적용 (SmoothStep) t = t * t * (3f - 2f * t); // 알파를 1에서 0으로: 스냅샷이 서서히 사라짐 overlay.SetAlpha(1f - t); } // 블렌딩 종료 overlay.Hide(); isBlending = false; blendCoroutine = null; } /// /// 실시간 블렌딩 (UI 오버레이 방식) /// 이전 카메라 위치를 저장해두고 그 위치에서 렌더링, UI로 표시하고 서서히 사라지게 함 /// private IEnumerator RealtimeBlendTransitionCoroutine(int targetIndex, float duration) { isBlending = true; // 이전 카메라 프리셋 저장 var prevPreset = currentPreset; if (prevPreset == null || prevPreset.virtualCamera == null) { SetImmediate(targetIndex); isBlending = false; blendCoroutine = null; yield break; } // 메인 카메라의 현재 위치/회전/FOV를 저장 (Cinemachine이 적용한 실제 위치) Camera mainCamera = Camera.main; if (mainCamera == null) { SetImmediate(targetIndex); isBlending = false; blendCoroutine = null; yield break; } Vector3 prevCameraPosition = mainCamera.transform.position; Quaternion prevCameraRotation = mainCamera.transform.rotation; float prevCameraFOV = mainCamera.fieldOfView; // 블렌드 텍스처 준비 EnsurePrevCameraRenderTexture(); if (prevCameraRenderTexture == null) { SetImmediate(targetIndex); isBlending = false; blendCoroutine = null; yield break; } // 블렌드 카메라 준비 EnsureBlendCamera(); if (blendCamera == null) { SetImmediate(targetIndex); isBlending = false; blendCoroutine = null; yield break; } // UI 오버레이 준비 var overlay = CameraBlendOverlay.GetOrCreate(); // 블렌드 카메라를 이전 카메라 위치로 설정하고 첫 프레임 렌더링 blendCamera.transform.SetPositionAndRotation(prevCameraPosition, prevCameraRotation); blendCamera.fieldOfView = prevCameraFOV; blendCamera.targetTexture = prevCameraRenderTexture; blendCamera.Render(); // UI 오버레이 표시 (알파 1 = 완전 불투명) overlay.Show(prevCameraRenderTexture); overlay.SetAlpha(1f); // 새 카메라로 전환 (메인 카메라는 이제 새 위치에서 렌더링됨) SetImmediate(targetIndex); // 블렌드 진행: 알파 1 → 0 (이전 카메라 화면이 서서히 사라짐) float elapsed = 0f; while (elapsed < duration) { // 이전 카메라 시점에서 렌더링 blendCamera.transform.SetPositionAndRotation(prevCameraPosition, prevCameraRotation); blendCamera.fieldOfView = prevCameraFOV; blendCamera.targetTexture = prevCameraRenderTexture; blendCamera.Render(); // 다음 프레임까지 대기 yield return null; elapsed += Time.deltaTime; float t = Mathf.Clamp01(elapsed / duration); // 부드러운 이징 적용 (SmoothStep) t = t * t * (3f - 2f * t); // 알파를 1에서 0으로: 이전 카메라가 서서히 사라짐 overlay.SetAlpha(1f - t); } // 블렌딩 종료 overlay.Hide(); isBlending = false; blendCoroutine = null; } /// /// 실시간 블렌딩용 이전 카메라 렌더 텍스처 생성/갱신 /// private void EnsurePrevCameraRenderTexture() { int width = Screen.width; int height = Screen.height; // 이미 적절한 크기의 텍스처가 있으면 재사용 if (prevCameraRenderTexture != null && prevCameraRenderTexture.width == width && prevCameraRenderTexture.height == height) { return; } // 기존 텍스처 해제 if (prevCameraRenderTexture != null) { prevCameraRenderTexture.Release(); Destroy(prevCameraRenderTexture); } // 새 렌더 텍스처 생성 - HDR 포맷 + depth 포함 (포스트 프로세싱 지원) var descriptor = new RenderTextureDescriptor(width, height, RenderTextureFormat.DefaultHDR, 24); descriptor.sRGB = false; // HDR은 리니어 포맷 prevCameraRenderTexture = new RenderTexture(descriptor); prevCameraRenderTexture.name = "PrevCameraRT"; prevCameraRenderTexture.Create(); } private void UpdateCameraPriorities(CinemachineCamera newCamera) { if (newCamera == null) { Debug.LogError("[CameraManager] 새 카메라가 null입니다!"); return; } if (currentCamera != null) { currentCamera.Priority = 0; } currentCamera = newCamera; currentCamera.Priority = 10; } #endregion #region Hotkey Management public void StartRecordingHotkey(int presetIndex) { if (cameraPresets == null || presetIndex < 0 || presetIndex >= cameraPresets.Count) return; var preset = cameraPresets[presetIndex]; if (!preset.IsValid()) return; foreach (var otherPreset in cameraPresets) { if (otherPreset?.hotkey?.isRecording == true) { otherPreset.hotkey.StopRecording(); } } preset.hotkey.StartRecording(); } public void StopRecordingHotkey(int presetIndex) { if (cameraPresets == null || presetIndex < 0 || presetIndex >= cameraPresets.Count) return; var preset = cameraPresets[presetIndex]; if (preset?.hotkey?.isRecording == true) { preset.hotkey.StopRecording(); } } #endregion #region Camera Data // 카메라 목록 데이터 반환 (HTTP 요청 시 직접 호출됨) public CameraListData GetCameraListData() { var presetList = cameraPresets.Select((preset, index) => new CameraPresetData { index = index, name = preset?.virtualCamera?.gameObject.name ?? $"Camera {index}", isActive = currentPreset == preset, hotkey = preset?.hotkey?.ToString() ?? "설정되지 않음" }).ToArray(); return new CameraListData { camera_count = cameraPresets.Count, presets = presetList, current_index = currentPreset != null ? cameraPresets.IndexOf(currentPreset) : -1 }; } // 현재 카메라 상태 데이터 반환 public CameraStateData GetCurrentCameraState() { if (currentPreset == null) return null; var currentIndex = cameraPresets.IndexOf(currentPreset); return new CameraStateData { current_index = currentIndex, camera_name = currentPreset.virtualCamera?.gameObject.name ?? "Unknown", preset_name = currentPreset.presetName, total_cameras = cameraPresets.Count, is_drone_mode = IsDroneModeActive }; } [System.Serializable] public class CameraPresetData { public int index; public string name; public bool isActive; public string hotkey; } [System.Serializable] public class CameraListData { public int camera_count; public CameraPresetData[] presets; public int current_index; } [System.Serializable] public class CameraStateData { public int current_index; public string camera_name; public string preset_name; public int total_cameras; public bool is_drone_mode; } #endregion #region IController Implementation public string GetControllerId() { return "camera_controller"; } public string GetControllerName() { return "카메라 컨트롤러"; } public object GetControllerData() { return GetCameraListData(); } public void ExecuteAction(string actionId, object parameters) { switch (actionId) { case "switch_camera": if (parameters is int cameraIndex) { Set(cameraIndex); } else if (parameters is System.Dynamic.ExpandoObject expando) { var dict = (IDictionary)expando; if (dict.ContainsKey("camera_index") && dict["camera_index"] is int index) { Set(index); } } break; case "get_camera_list": // 카메라 목록은 GetControllerData()에서 반환됨 break; case "toggle_drone_mode": ToggleDroneMode(); break; case "get_drone_state": // 드론 상태는 GetCurrentCameraState()의 is_drone_mode로 반환됨 break; default: Debug.LogWarning($"[CameraManager] 알 수 없는 액션: {actionId}"); break; } } #endregion }