1318 lines
42 KiB
C#

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<KeyCode, RawKey> _mapping;
static KeyMapping()
{
_mapping = new Dictionary<KeyCode, RawKey>(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<RawKey> rawKeys = new List<RawKey>();
[System.NonSerialized] private List<KeyCode> unityKeys = new List<KeyCode>();
[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<CameraPreset> cameraPresets = new List<CameraPreset>();
[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 blendRenderTexture;
private RenderTexture prevCameraRenderTexture; // 실시간 블렌딩용 이전 카메라 렌더 텍스처
private Camera blendCamera;
private CinemachineCamera currentCamera;
private InputHandler inputHandler;
private CameraPreset currentPreset;
// 오빗 카메라 상태 (각도 기반)
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<StreamDeckServerManager>();
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();
}
// 블렌드 리소스 정리
CameraBlendController.Reset();
if (blendRenderTexture != null)
{
blendRenderTexture.Release();
Destroy(blendRenderTexture);
blendRenderTexture = null;
}
if (blendCamera != null)
{
Destroy(blendCamera.gameObject);
blendCamera = null;
}
}
private void Update()
{
if (!IsValidSetup) return;
UpdateHotkeyRecording();
HandleCameraControls();
HandleHotkeys();
}
private void UpdateHotkeyRecording()
{
foreach (var preset in cameraPresets)
{
if (preset?.hotkey?.isRecording == true)
{
preset.hotkey.UpdateRecording();
}
}
}
#endregion
#region Initialization
private void InitializeInputHandler()
{
inputHandler = GetComponent<InputHandler>();
if (inputHandler == null)
{
inputHandler = gameObject.AddComponent<InputHandler>();
}
}
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<CameraPreset>();
}
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<CustomRetargetingScript>(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<Animator>();
}
if (animator != null)
{
avatarHeadTransform = animator.GetBoneTransform(HumanBodyBones.Head);
if (avatarHeadTransform != null)
{
return;
}
}
}
Debug.LogWarning("[CameraManager] 활성화된 아바타의 Head 본을 찾을 수 없습니다. 수동 타겟을 사용하거나 원점을 사용합니다.");
}
/// <summary>
/// 현재 회전 중심점을 반환합니다.
/// 우선순위: 아바타 머리 > 수동 타겟 > 원점
/// </summary>
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<CameraPreset, bool> predicate)
{
var matchingPreset = cameraPresets?.FirstOrDefault(predicate);
if (matchingPreset != null)
{
Set(cameraPresets.IndexOf(matchingPreset));
}
}
#endregion
#region Camera Controls
/// <summary>
/// 현재 카메라 위치에서 각도와 거리를 초기화합니다.
/// </summary>
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 (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();
}
/// <summary>
/// 우클릭 드래그: 수평 + 수직 회전 (현재 비활성화 - Alt+우클릭 사용)
/// </summary>
private void HandleRotation()
{
// 우클릭만으로는 회전하지 않음 - Alt+우클릭(Orbit)으로 대체
}
/// <summary>
/// Alt + 우클릭: 자유 궤도 회전 (X, Y축)
/// </summary>
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);
}
/// <summary>
/// 휠클릭 드래그: 패닝 (초점 이동)
/// </summary>
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;
}
/// <summary>
/// 마우스 휠: 줌
/// </summary>
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);
}
/// <summary>
/// Ctrl + 좌클릭 드래그: 줌
/// </summary>
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);
}
/// <summary>
/// Ctrl + 우클릭 드래그: 줌 (밀기 = 확대, 당기기 = 축소)
/// </summary>
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);
}
/// <summary>
/// Shift + 좌클릭/우클릭 드래그: FOV 조절 (위로 밀면 FOV 감소=줌인, 아래로 밀면 FOV 증가=줌아웃)
/// </summary>
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;
}
/// <summary>
/// 스무딩을 적용하여 카메라 위치 업데이트
/// </summary>
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);
}
/// <summary>
/// 크로스 디졸브 블렌드와 함께 카메라 전환
/// </summary>
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);
CameraBlendController.EndBlend();
}
blendCoroutine = StartCoroutine(BlendTransitionCoroutine(index, customBlendTime ?? blendTime));
}
/// <summary>
/// 즉시 카메라 전환 (페이드 없음)
/// </summary>
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";
// 이전 프리셋의 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 (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;
}
/// <summary>
/// 블렌드용 카메라와 렌더 텍스처 생성
/// </summary>
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<Camera>();
// 메인 카메라 설정 복사
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;
}
}
/// <summary>
/// 블렌드용 렌더 텍스처 생성/갱신
/// </summary>
private void EnsureBlendRenderTexture()
{
int width = Screen.width;
int height = Screen.height;
// 이미 적절한 크기의 텍스처가 있으면 재사용
if (blendRenderTexture != null &&
blendRenderTexture.width == width &&
blendRenderTexture.height == height)
{
return;
}
// 기존 텍스처 해제
if (blendRenderTexture != null)
{
blendRenderTexture.Release();
Destroy(blendRenderTexture);
}
// 새 렌더 텍스처 생성 - 색상 전용 (depth 없음), 리니어 색공간에서 올바른 블렌딩
var descriptor = new RenderTextureDescriptor(width, height, RenderTextureFormat.ARGB32, 0); // depth = 0
descriptor.sRGB = true; // sRGB 텍스처로 설정하여 리니어 파이프라인과 일치
blendRenderTexture = new RenderTexture(descriptor);
blendRenderTexture.name = "CameraBlendRT";
blendRenderTexture.Create();
}
/// <summary>
/// 크로스 디졸브 블렌드 전환 코루틴
/// </summary>
private IEnumerator BlendTransitionCoroutine(int targetIndex, float duration)
{
if (useRealtimeBlend)
{
yield return RealtimeBlendTransitionCoroutine(targetIndex, duration);
}
else
{
yield return SnapshotBlendTransitionCoroutine(targetIndex, duration);
}
}
/// <summary>
/// 스냅샷 블렌딩 (이전 화면 정지)
/// </summary>
private IEnumerator SnapshotBlendTransitionCoroutine(int targetIndex, float duration)
{
isBlending = true;
// 블렌드 텍스처 준비
EnsureBlendRenderTexture();
if (blendRenderTexture == null)
{
SetImmediate(targetIndex);
isBlending = false;
blendCoroutine = null;
yield break;
}
// 렌더 패스에서 현재 프레임(포스트 프로세싱 적용 후)을 캡처하도록 요청
CameraBlendController.RequestCapture(blendRenderTexture);
// 캡처가 완료될 때까지 대기 (다음 프레임 렌더링 후)
yield return new WaitForEndOfFrame();
yield return null; // 렌더 패스가 실행될 때까지 한 프레임 더 대기
// 캡처 완료 확인
if (!CameraBlendController.CaptureReady)
{
CameraBlendController.EndBlend();
SetImmediate(targetIndex);
isBlending = false;
blendCoroutine = null;
yield break;
}
// 블렌딩 시작 - BlendAmount = 1 (이전 카메라만 보임)
CameraBlendController.StartBlendAfterCapture();
CameraBlendController.BlendAmount = 1f;
// 카메라 전환 (이제 BlendAmount=1이므로 캡처된 이전 화면만 보임)
SetImmediate(targetIndex);
// 블렌드 진행: 1 → 0 (이전 카메라가 서서히 사라지고 새 카메라가 드러남)
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / duration);
// 부드러운 이징 적용 (SmoothStep)
t = t * t * (3f - 2f * t);
// 1에서 0으로: 이전 카메라가 서서히 사라짐
CameraBlendController.BlendAmount = 1f - t;
yield return null;
}
CameraBlendController.BlendAmount = 0f;
// 블렌딩 종료
CameraBlendController.EndBlend();
isBlending = false;
blendCoroutine = null;
}
/// <summary>
/// 실시간 블렌딩 (두 카메라 동시 렌더링)
/// 이전 카메라 위치를 저장해두고 그 위치에서 렌더링, 서서히 사라지면서 새 카메라가 드러남
/// </summary>
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;
}
// SetImmediate 호출 전에 이전 카메라의 위치/회전/FOV를 저장
Vector3 prevCameraPosition = prevPreset.virtualCamera.transform.position;
Quaternion prevCameraRotation = prevPreset.virtualCamera.transform.rotation;
float prevCameraFOV = prevPreset.virtualCamera.Lens.FieldOfView;
// 블렌드 텍스처 준비
EnsureBlendRenderTexture();
EnsurePrevCameraRenderTexture();
if (blendRenderTexture == null || prevCameraRenderTexture == null)
{
SetImmediate(targetIndex);
isBlending = false;
blendCoroutine = null;
yield break;
}
// 블렌드 카메라 준비
EnsureBlendCamera();
if (blendCamera == null)
{
SetImmediate(targetIndex);
isBlending = false;
blendCoroutine = null;
yield break;
}
// 블렌드 카메라를 이전 카메라 위치로 설정하고 첫 프레임 렌더링
blendCamera.transform.SetPositionAndRotation(prevCameraPosition, prevCameraRotation);
blendCamera.fieldOfView = prevCameraFOV;
blendCamera.targetTexture = prevCameraRenderTexture;
// Camera.Render()로 렌더링 (URP에서도 포스트 프로세싱 적용됨)
blendCamera.Render();
Graphics.Blit(prevCameraRenderTexture, blendRenderTexture);
// 실시간 블렌딩 시작 (BlendAmount = 1에서 시작, 이전 카메라가 100% 보임)
CameraBlendController.StartRealtimeBlend(blendRenderTexture);
CameraBlendController.BlendAmount = 1f;
// 새 카메라로 전환 (메인 카메라는 이제 새 위치에서 렌더링됨)
SetImmediate(targetIndex);
// 블렌드 진행: 1 → 0 (이전 카메라 화면이 서서히 사라지고 새 카메라가 드러남)
float elapsed = 0f;
while (elapsed < duration)
{
// 이전 카메라 시점에서 먼저 렌더링 (메인 카메라 렌더링 전에)
blendCamera.transform.SetPositionAndRotation(prevCameraPosition, prevCameraRotation);
blendCamera.fieldOfView = prevCameraFOV;
blendCamera.targetTexture = prevCameraRenderTexture;
// Camera.Render()로 렌더링 (URP에서도 포스트 프로세싱 적용됨)
blendCamera.Render();
Graphics.Blit(prevCameraRenderTexture, blendRenderTexture);
// 다음 프레임까지 대기
yield return null;
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / duration);
// 부드러운 이징 적용 (SmoothStep)
t = t * t * (3f - 2f * t);
// 1에서 0으로: 이전 카메라가 서서히 사라짐
CameraBlendController.BlendAmount = 1f - t;
}
CameraBlendController.BlendAmount = 0f;
// 블렌딩 종료
CameraBlendController.EndBlend();
isBlending = false;
blendCoroutine = null;
}
/// <summary>
/// 실시간 블렌딩용 이전 카메라 렌더 텍스처 생성/갱신
/// </summary>
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
};
}
[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;
}
#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<string, object>)expando;
if (dict.ContainsKey("camera_index") && dict["camera_index"] is int index)
{
Set(index);
}
}
break;
case "get_camera_list":
// 카메라 목록은 GetControllerData()에서 반환됨
break;
default:
Debug.LogWarning($"[CameraManager] 알 수 없는 액션: {actionId}");
break;
}
}
#endregion
}