diff --git a/Assets/Resources/Settings/Streamingle Render Pipeline Asset_Renderer.asset b/Assets/Resources/Settings/Streamingle Render Pipeline Asset_Renderer.asset
index cf9cc387..8f27120e 100644
--- a/Assets/Resources/Settings/Streamingle Render Pipeline Asset_Renderer.asset
+++ b/Assets/Resources/Settings/Streamingle Render Pipeline Asset_Renderer.asset
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3b136cc08ef5d43269c1214953146ede52d9af7d6b6ba9956c0e16e8297e2fb3
-size 18341
+oid sha256:8f7cef64b1251ba5c14b794828bd3d2a93853036c00ad15d5dc460b199104f5b
+size 18958
diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Controllers/CameraController.cs b/Assets/Scripts/Streamingle/StreamingleControl/Controllers/CameraController.cs
index a0941c41..0513f936 100644
--- a/Assets/Scripts/Streamingle/StreamingleControl/Controllers/CameraController.cs
+++ b/Assets/Scripts/Streamingle/StreamingleControl/Controllers/CameraController.cs
@@ -1,4 +1,6 @@
using UnityEngine;
+using UnityEngine.Rendering.Universal;
+using System.Collections;
using System.Collections.Generic;
using UnityRawInput;
using System.Linq;
@@ -190,6 +192,16 @@ public class CameraManager : MonoBehaviour, IController
[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;
+
+ // 블렌드용 렌더 텍스처와 카메라
+ private RenderTexture blendRenderTexture;
+ private Camera blendCamera;
+
private CinemachineCamera currentCamera;
private InputHandler inputHandler;
private CameraPreset currentPreset;
@@ -212,6 +224,10 @@ public class CameraManager : MonoBehaviour, IController
// 스트림덱 연동
private StreamDeckServerManager streamDeckManager;
+ // 블렌드 전환 상태
+ private Coroutine blendCoroutine;
+ private bool isBlending = false;
+
#endregion
#region Properties
@@ -250,6 +266,22 @@ public class CameraManager : MonoBehaviour, IController
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()
@@ -619,22 +651,50 @@ public class CameraManager : MonoBehaviour, IController
#region Camera Management
public void Set(int index)
{
- Debug.Log($"[CameraManager] 카메라 {index}번으로 전환 시작 (총 {cameraPresets?.Count ?? 0}개)");
-
- if (cameraPresets == null)
+ // 블렌드 전환 사용 시
+ if (useBlendTransition)
{
- Debug.LogError("[CameraManager] cameraPresets가 null입니다!");
- return;
- }
-
- if (index < 0 || index >= cameraPresets.Count)
- {
- Debug.LogError($"[CameraManager] 잘못된 인덱스: {index}, 유효 범위: 0-{cameraPresets.Count - 1}");
+ 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);
+ CameraBlendController.EndBlend();
+ }
+
+ blendCoroutine = StartCoroutine(BlendTransitionCoroutine(index, customBlendTime ?? blendTime));
+ }
+
+ ///
+ /// 즉시 카메라 전환 (페이드 없음)
+ ///
+ public void SetImmediate(int index)
+ {
+ Debug.Log($"[CameraManager] 카메라 {index}번으로 전환 시작 (총 {cameraPresets?.Count ?? 0}개)");
+
+ if (!ValidateCameraIndex(index)) return;
+
var newPreset = cameraPresets[index];
-
+
if (!newPreset.IsValid())
{
Debug.LogError($"[CameraManager] 프리셋이 유효하지 않습니다 - 인덱스: {index}");
@@ -643,7 +703,7 @@ public class CameraManager : MonoBehaviour, IController
var oldPreset = currentPreset;
var newCameraName = newPreset.virtualCamera?.gameObject.name ?? "Unknown";
-
+
currentPreset = newPreset;
UpdateCameraPriorities(newPreset.virtualCamera);
@@ -661,6 +721,169 @@ public class CameraManager : MonoBehaviour, IController
Debug.Log($"[CameraManager] 카메라 전환 완료: {newCameraName}");
}
+ 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;
+ }
+
+ // 블렌드용 카메라 생성
+ GameObject blendCamObj = new GameObject("BlendCamera");
+ blendCamObj.transform.SetParent(mainCamera.transform.parent);
+ 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;
+ }
+
+ Debug.Log("[CameraManager] 블렌드 카메라 생성 완료 (URP 설정 포함)");
+ }
+
+ ///
+ /// 블렌드용 렌더 텍스처 생성/갱신
+ ///
+ 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);
+ }
+
+ // 새 렌더 텍스처 생성
+ blendRenderTexture = new RenderTexture(width, height, 24, RenderTextureFormat.ARGB32);
+ blendRenderTexture.name = "CameraBlendRT";
+ blendRenderTexture.Create();
+
+ Debug.Log($"[CameraManager] 블렌드 렌더 텍스처 생성: {width}x{height}");
+ }
+
+ ///
+ /// 크로스 디졸브 블렌드 전환 코루틴
+ ///
+ private IEnumerator BlendTransitionCoroutine(int targetIndex, float duration)
+ {
+ isBlending = true;
+
+ // 블렌드 카메라/텍스처 준비
+ EnsureBlendCamera();
+ EnsureBlendRenderTexture();
+
+ if (blendCamera == null || blendRenderTexture == null)
+ {
+ Debug.LogError("[CameraManager] 블렌드 카메라 또는 텍스처 생성 실패");
+ SetImmediate(targetIndex);
+ isBlending = false;
+ blendCoroutine = null;
+ yield break;
+ }
+
+ // 메인 카메라의 현재 위치/회전 복사 (Cinemachine이 제어하는 실제 카메라)
+ Camera mainCamera = Camera.main;
+ if (mainCamera != null)
+ {
+ blendCamera.transform.position = mainCamera.transform.position;
+ blendCamera.transform.rotation = mainCamera.transform.rotation;
+ blendCamera.fieldOfView = mainCamera.fieldOfView;
+ }
+
+ // 블렌드 카메라로 현재 화면을 렌더 텍스처에 렌더링
+ blendCamera.targetTexture = blendRenderTexture;
+ blendCamera.enabled = true;
+ blendCamera.Render();
+ blendCamera.enabled = false;
+
+ Debug.Log($"[CameraManager] 블렌드 시작 - Duration: {duration}s");
+
+ // 블렌딩 시작 - BlendAmount = 0 (이전 카메라 A만 보임)
+ CameraBlendController.StartBlend(blendRenderTexture);
+
+ // 한 프레임 대기 - 블렌딩이 적용된 상태로 렌더링되도록
+ yield return null;
+
+ // 카메라 전환 (이제 BlendAmount=0이므로 A만 보임)
+ SetImmediate(targetIndex);
+
+ // 블렌드 진행: 0 → 1 (A에서 B로)
+ float elapsed = 0f;
+ while (elapsed < duration)
+ {
+ elapsed += Time.deltaTime;
+ float t = Mathf.Clamp01(elapsed / duration);
+ // 부드러운 이징 적용 (SmoothStep)
+ t = t * t * (3f - 2f * t);
+ CameraBlendController.BlendAmount = t;
+ yield return null;
+ }
+
+ CameraBlendController.BlendAmount = 1f;
+
+ // 블렌딩 종료
+ CameraBlendController.EndBlend();
+
+ Debug.Log("[CameraManager] 블렌드 완료");
+
+ isBlending = false;
+ blendCoroutine = null;
+ }
+
private void UpdateCameraPriorities(CinemachineCamera newCamera)
{
if (newCamera == null)
diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Editor/CameraManagerEditor.cs b/Assets/Scripts/Streamingle/StreamingleControl/Editor/CameraManagerEditor.cs
index d9b0d4cb..93316d06 100644
--- a/Assets/Scripts/Streamingle/StreamingleControl/Editor/CameraManagerEditor.cs
+++ b/Assets/Scripts/Streamingle/StreamingleControl/Editor/CameraManagerEditor.cs
@@ -12,9 +12,50 @@ public class CameraManagerEditor : Editor
private bool isApplicationPlaying;
private bool isListening = false;
+ // Foldout 상태
+ private bool showCameraPresets = true;
+ private bool showControlSettings = true;
+ private bool showSmoothingSettings = true;
+ private bool showZoomSettings = true;
+ private bool showRotationTarget = true;
+ private bool showBlendSettings = true;
+
+ // SerializedProperties
+ private SerializedProperty rotationSensitivityProp;
+ private SerializedProperty panSpeedProp;
+ private SerializedProperty zoomSpeedProp;
+ private SerializedProperty orbitSpeedProp;
+ private SerializedProperty movementSmoothingProp;
+ private SerializedProperty rotationSmoothingProp;
+ private SerializedProperty minZoomDistanceProp;
+ private SerializedProperty maxZoomDistanceProp;
+ private SerializedProperty useAvatarHeadAsTargetProp;
+ private SerializedProperty manualRotationTargetProp;
+ private SerializedProperty useBlendTransitionProp;
+ private SerializedProperty blendTimeProp;
+
+ // 스타일
+ private GUIStyle headerStyle;
+ private GUIStyle sectionBoxStyle;
+ private GUIStyle presetBoxStyle;
+
private void OnEnable()
{
isApplicationPlaying = Application.isPlaying;
+
+ // SerializedProperties 가져오기
+ rotationSensitivityProp = serializedObject.FindProperty("rotationSensitivity");
+ panSpeedProp = serializedObject.FindProperty("panSpeed");
+ zoomSpeedProp = serializedObject.FindProperty("zoomSpeed");
+ orbitSpeedProp = serializedObject.FindProperty("orbitSpeed");
+ movementSmoothingProp = serializedObject.FindProperty("movementSmoothing");
+ rotationSmoothingProp = serializedObject.FindProperty("rotationSmoothing");
+ minZoomDistanceProp = serializedObject.FindProperty("minZoomDistance");
+ maxZoomDistanceProp = serializedObject.FindProperty("maxZoomDistance");
+ useAvatarHeadAsTargetProp = serializedObject.FindProperty("useAvatarHeadAsTarget");
+ manualRotationTargetProp = serializedObject.FindProperty("manualRotationTarget");
+ useBlendTransitionProp = serializedObject.FindProperty("useBlendTransition");
+ blendTimeProp = serializedObject.FindProperty("blendTime");
}
private void OnDisable()
@@ -22,6 +63,36 @@ public class CameraManagerEditor : Editor
StopListening();
}
+ private void InitializeStyles()
+ {
+ if (headerStyle == null)
+ {
+ headerStyle = new GUIStyle(EditorStyles.boldLabel)
+ {
+ fontSize = 12,
+ margin = new RectOffset(0, 0, 5, 5)
+ };
+ }
+
+ if (sectionBoxStyle == null)
+ {
+ sectionBoxStyle = new GUIStyle(EditorStyles.helpBox)
+ {
+ padding = new RectOffset(10, 10, 10, 10),
+ margin = new RectOffset(0, 0, 5, 5)
+ };
+ }
+
+ if (presetBoxStyle == null)
+ {
+ presetBoxStyle = new GUIStyle(GUI.skin.box)
+ {
+ padding = new RectOffset(8, 8, 8, 8),
+ margin = new RectOffset(0, 0, 3, 3)
+ };
+ }
+ }
+
private void StartListening()
{
if (!isApplicationPlaying)
@@ -39,17 +110,176 @@ public class CameraManagerEditor : Editor
public override void OnInspectorGUI()
{
- CameraManager manager = (CameraManager)target;
- EditorGUILayout.Space(10);
+ serializedObject.Update();
+ InitializeStyles();
+
+ CameraManager manager = (CameraManager)target;
- EditorGUILayout.BeginVertical(EditorStyles.helpBox);
- EditorGUILayout.LabelField("카메라 프리셋 관리", EditorStyles.boldLabel);
EditorGUILayout.Space(5);
- // 프리셋 추가 버튼
- if (GUILayout.Button("새 프리셋 추가", GUILayout.Height(30)))
+ // 카메라 컨트롤 설정 섹션
+ DrawControlSettingsSection();
+
+ // 스무딩 설정 섹션
+ DrawSmoothingSection();
+
+ // 줌 제한 섹션
+ DrawZoomLimitsSection();
+
+ // 회전 타겟 섹션
+ DrawRotationTargetSection();
+
+ // 블렌드 전환 섹션
+ DrawBlendTransitionSection();
+
+ EditorGUILayout.Space(10);
+
+ // 카메라 프리셋 섹션
+ DrawCameraPresetsSection(manager);
+
+ // 키 입력 감지 로직
+ HandleKeyListening(manager);
+
+ serializedObject.ApplyModifiedProperties();
+ }
+
+ private void DrawControlSettingsSection()
+ {
+ EditorGUILayout.BeginVertical(sectionBoxStyle);
+
+ showControlSettings = EditorGUILayout.Foldout(showControlSettings, "Camera Control Settings", true, EditorStyles.foldoutHeader);
+
+ if (showControlSettings)
+ {
+ EditorGUILayout.Space(5);
+ EditorGUI.indentLevel++;
+
+ EditorGUILayout.PropertyField(rotationSensitivityProp, new GUIContent("회전 감도", "마우스 회전 감도 (0.5 ~ 10)"));
+ EditorGUILayout.PropertyField(panSpeedProp, new GUIContent("패닝 속도", "휠클릭 패닝 속도 (0.005 ~ 0.1)"));
+ EditorGUILayout.PropertyField(zoomSpeedProp, new GUIContent("줌 속도", "마우스 휠 줌 속도 (0.05 ~ 0.5)"));
+ EditorGUILayout.PropertyField(orbitSpeedProp, new GUIContent("오빗 속도", "궤도 회전 속도 (1 ~ 20)"));
+
+ EditorGUI.indentLevel--;
+ }
+
+ EditorGUILayout.EndVertical();
+ }
+
+ private void DrawSmoothingSection()
+ {
+ EditorGUILayout.BeginVertical(sectionBoxStyle);
+
+ showSmoothingSettings = EditorGUILayout.Foldout(showSmoothingSettings, "Smoothing", true, EditorStyles.foldoutHeader);
+
+ if (showSmoothingSettings)
+ {
+ EditorGUILayout.Space(5);
+ EditorGUI.indentLevel++;
+
+ EditorGUILayout.PropertyField(movementSmoothingProp, new GUIContent("이동 스무딩", "카메라 이동 부드러움 (0 ~ 0.95)"));
+ EditorGUILayout.PropertyField(rotationSmoothingProp, new GUIContent("회전 스무딩", "카메라 회전 부드러움 (0 ~ 0.95)"));
+
+ EditorGUI.indentLevel--;
+ }
+
+ EditorGUILayout.EndVertical();
+ }
+
+ private void DrawZoomLimitsSection()
+ {
+ EditorGUILayout.BeginVertical(sectionBoxStyle);
+
+ showZoomSettings = EditorGUILayout.Foldout(showZoomSettings, "Zoom Limits", true, EditorStyles.foldoutHeader);
+
+ if (showZoomSettings)
+ {
+ EditorGUILayout.Space(5);
+ EditorGUI.indentLevel++;
+
+ EditorGUILayout.PropertyField(minZoomDistanceProp, new GUIContent("최소 줌 거리", "카메라 최소 거리"));
+ EditorGUILayout.PropertyField(maxZoomDistanceProp, new GUIContent("최대 줌 거리", "카메라 최대 거리"));
+
+ // 경고 표시
+ if (minZoomDistanceProp.floatValue >= maxZoomDistanceProp.floatValue)
+ {
+ EditorGUILayout.HelpBox("최소 줌 거리는 최대 줌 거리보다 작아야 합니다.", MessageType.Warning);
+ }
+
+ EditorGUI.indentLevel--;
+ }
+
+ EditorGUILayout.EndVertical();
+ }
+
+ private void DrawRotationTargetSection()
+ {
+ EditorGUILayout.BeginVertical(sectionBoxStyle);
+
+ showRotationTarget = EditorGUILayout.Foldout(showRotationTarget, "Rotation Target", true, EditorStyles.foldoutHeader);
+
+ if (showRotationTarget)
+ {
+ EditorGUILayout.Space(5);
+ EditorGUI.indentLevel++;
+
+ EditorGUILayout.PropertyField(useAvatarHeadAsTargetProp, new GUIContent("아바타 머리 사용", "활성화하면 아바타 머리를 회전 중심으로 사용"));
+
+ EditorGUI.BeginDisabledGroup(useAvatarHeadAsTargetProp.boolValue);
+ EditorGUILayout.PropertyField(manualRotationTargetProp, new GUIContent("수동 회전 타겟", "수동으로 지정하는 회전 중심점"));
+ EditorGUI.EndDisabledGroup();
+
+ if (useAvatarHeadAsTargetProp.boolValue)
+ {
+ EditorGUILayout.HelpBox("런타임에 CustomRetargetingScript를 가진 아바타의 Head 본을 자동으로 찾습니다.", MessageType.Info);
+ }
+
+ EditorGUI.indentLevel--;
+ }
+
+ EditorGUILayout.EndVertical();
+ }
+
+ private void DrawBlendTransitionSection()
+ {
+ EditorGUILayout.BeginVertical(sectionBoxStyle);
+
+ showBlendSettings = EditorGUILayout.Foldout(showBlendSettings, "Camera Blend Transition", true, EditorStyles.foldoutHeader);
+
+ if (showBlendSettings)
+ {
+ EditorGUILayout.Space(5);
+ EditorGUI.indentLevel++;
+
+ EditorGUILayout.PropertyField(useBlendTransitionProp, new GUIContent("블렌드 전환 사용", "카메라 전환 시 크로스 디졸브 효과 사용"));
+
+ EditorGUI.BeginDisabledGroup(!useBlendTransitionProp.boolValue);
+ EditorGUILayout.PropertyField(blendTimeProp, new GUIContent("블렌드 시간", "크로스 디졸브 소요 시간 (초)"));
+ EditorGUI.EndDisabledGroup();
+
+ if (useBlendTransitionProp.boolValue)
+ {
+ EditorGUILayout.HelpBox("URP Renderer에 CameraBlendRendererFeature가 추가되어 있어야 합니다.", MessageType.Info);
+ }
+
+ EditorGUI.indentLevel--;
+ }
+
+ EditorGUILayout.EndVertical();
+ }
+
+ private void DrawCameraPresetsSection(CameraManager manager)
+ {
+ EditorGUILayout.BeginVertical(sectionBoxStyle);
+
+ // 헤더
+ EditorGUILayout.BeginHorizontal();
+ showCameraPresets = EditorGUILayout.Foldout(showCameraPresets, $"Camera Presets ({manager.cameraPresets.Count})", true, EditorStyles.foldoutHeader);
+
+ GUILayout.FlexibleSpace();
+
+ // 프리셋 추가 버튼
+ if (GUILayout.Button("+ 프리셋 추가", GUILayout.Width(100), GUILayout.Height(20)))
{
- // Scene에 있는 CinemachineCamera 컴포넌트를 가져와서 새 프리셋 생성
var newCamera = FindFirstObjectByType();
if (newCamera != null)
{
@@ -61,84 +291,157 @@ public class CameraManagerEditor : Editor
EditorUtility.DisplayDialog("알림", "Scene에 CinemachineCamera가 없습니다.", "확인");
}
}
+ EditorGUILayout.EndHorizontal();
- EditorGUILayout.Space(10);
-
- // 프리셋 리스트
- for (int i = 0; i < manager.cameraPresets.Count; i++)
+ if (showCameraPresets)
{
- var preset = manager.cameraPresets[i];
-
- EditorGUILayout.BeginVertical(GUI.skin.box);
-
- // 프리셋 헤더
- EditorGUILayout.BeginHorizontal();
- EditorGUILayout.LabelField($"프리셋 {i + 1}", EditorStyles.boldLabel, GUILayout.Width(60));
-
- // 프리셋 이름 필드
- preset.presetName = EditorGUILayout.TextField(preset.presetName);
-
- // 삭제 버튼
- if (GUILayout.Button("삭제", GUILayout.Width(50)))
- {
- if (EditorUtility.DisplayDialog("프리셋 삭제",
- $"프리셋 {preset.presetName}을(를) 삭제하시겠습니까?",
- "삭제", "취소"))
- {
- manager.cameraPresets.RemoveAt(i);
- EditorUtility.SetDirty(target);
- continue;
- }
- }
- EditorGUILayout.EndHorizontal();
-
EditorGUILayout.Space(5);
- // 프리셋 설정
- EditorGUI.indentLevel++;
-
- // 가상 카메라 필드
- preset.virtualCamera = (CinemachineCamera)EditorGUILayout.ObjectField(
- "가상 카메라", preset.virtualCamera, typeof(CinemachineCamera), true);
-
- // 핫키 설정 UI
- EditorGUILayout.BeginHorizontal();
- EditorGUILayout.LabelField("핫키", GUILayout.Width(100));
-
- if (preset.hotkey.isRecording)
+ if (manager.cameraPresets.Count == 0)
{
- EditorGUILayout.LabelField("키를 눌렀다 떼면 저장됩니다...", EditorStyles.helpBox);
+ EditorGUILayout.HelpBox("카메라 프리셋이 없습니다. '+ 프리셋 추가' 버튼을 눌러 추가하세요.", MessageType.Info);
}
else
{
- EditorGUILayout.LabelField(preset.hotkey.ToString());
-
- if (GUILayout.Button("레코딩", GUILayout.Width(60)))
+ // 프리셋 리스트
+ for (int i = 0; i < manager.cameraPresets.Count; i++)
{
- foreach (var otherPreset in manager.cameraPresets)
- {
- otherPreset.hotkey.isRecording = false;
- }
- preset.hotkey.isRecording = true;
- preset.hotkey.rawKeys.Clear();
- StartListening();
- }
- if (GUILayout.Button("초기화", GUILayout.Width(60)))
- {
- preset.hotkey.rawKeys.Clear();
- EditorUtility.SetDirty(target);
+ DrawPresetItem(manager, i);
}
}
- EditorGUILayout.EndHorizontal();
-
- EditorGUI.indentLevel--;
- EditorGUILayout.EndVertical();
- EditorGUILayout.Space(5);
}
EditorGUILayout.EndVertical();
+ }
- // 키 입력 감지 로직
+ private void DrawPresetItem(CameraManager manager, int index)
+ {
+ var preset = manager.cameraPresets[index];
+
+ // 활성 프리셋 표시를 위한 배경색
+ bool isActive = Application.isPlaying && manager.CurrentPreset == preset;
+
+ if (isActive)
+ {
+ GUI.backgroundColor = new Color(0.5f, 0.8f, 0.5f);
+ }
+
+ EditorGUILayout.BeginVertical(presetBoxStyle);
+ GUI.backgroundColor = Color.white;
+
+ // 프리셋 헤더
+ EditorGUILayout.BeginHorizontal();
+
+ // 인덱스 번호
+ GUILayout.Label($"{index + 1}", EditorStyles.boldLabel, GUILayout.Width(20));
+
+ // 프리셋 이름 필드
+ EditorGUI.BeginChangeCheck();
+ preset.presetName = EditorGUILayout.TextField(preset.presetName, GUILayout.MinWidth(100));
+ if (EditorGUI.EndChangeCheck())
+ {
+ EditorUtility.SetDirty(target);
+ }
+
+ // 활성 표시
+ if (isActive)
+ {
+ GUILayout.Label("[Active]", EditorStyles.miniLabel, GUILayout.Width(50));
+ }
+
+ GUILayout.FlexibleSpace();
+
+ // 위/아래 버튼
+ EditorGUI.BeginDisabledGroup(index == 0);
+ if (GUILayout.Button("▲", GUILayout.Width(25)))
+ {
+ SwapPresets(manager, index, index - 1);
+ }
+ EditorGUI.EndDisabledGroup();
+
+ EditorGUI.BeginDisabledGroup(index == manager.cameraPresets.Count - 1);
+ if (GUILayout.Button("▼", GUILayout.Width(25)))
+ {
+ SwapPresets(manager, index, index + 1);
+ }
+ EditorGUI.EndDisabledGroup();
+
+ // 삭제 버튼
+ GUI.backgroundColor = new Color(1f, 0.5f, 0.5f);
+ if (GUILayout.Button("X", GUILayout.Width(25)))
+ {
+ if (EditorUtility.DisplayDialog("프리셋 삭제",
+ $"프리셋 '{preset.presetName}'을(를) 삭제하시겠습니까?",
+ "삭제", "취소"))
+ {
+ manager.cameraPresets.RemoveAt(index);
+ EditorUtility.SetDirty(target);
+ return;
+ }
+ }
+ GUI.backgroundColor = Color.white;
+
+ EditorGUILayout.EndHorizontal();
+
+ EditorGUILayout.Space(3);
+
+ // 가상 카메라 필드
+ EditorGUI.BeginChangeCheck();
+ preset.virtualCamera = (CinemachineCamera)EditorGUILayout.ObjectField(
+ "Virtual Camera", preset.virtualCamera, typeof(CinemachineCamera), true);
+ if (EditorGUI.EndChangeCheck())
+ {
+ EditorUtility.SetDirty(target);
+ }
+
+ // 핫키 설정 UI
+ EditorGUILayout.BeginHorizontal();
+ EditorGUILayout.LabelField("Hotkey", GUILayout.Width(70));
+
+ if (preset.hotkey.isRecording)
+ {
+ GUI.backgroundColor = new Color(1f, 0.9f, 0.5f);
+ EditorGUILayout.LabelField("키를 눌렀다 떼면 저장됩니다...", EditorStyles.helpBox);
+ GUI.backgroundColor = Color.white;
+ }
+ else
+ {
+ // 핫키 표시
+ string hotkeyDisplay = preset.hotkey?.ToString() ?? "설정되지 않음";
+ EditorGUILayout.LabelField(hotkeyDisplay, EditorStyles.textField, GUILayout.MinWidth(80));
+
+ if (GUILayout.Button("Record", GUILayout.Width(60)))
+ {
+ foreach (var otherPreset in manager.cameraPresets)
+ {
+ otherPreset.hotkey.isRecording = false;
+ }
+ preset.hotkey.isRecording = true;
+ preset.hotkey.rawKeys.Clear();
+ StartListening();
+ }
+ if (GUILayout.Button("Clear", GUILayout.Width(50)))
+ {
+ preset.hotkey.rawKeys.Clear();
+ EditorUtility.SetDirty(target);
+ }
+ }
+ EditorGUILayout.EndHorizontal();
+
+ EditorGUILayout.EndVertical();
+ EditorGUILayout.Space(2);
+ }
+
+ private void SwapPresets(CameraManager manager, int indexA, int indexB)
+ {
+ var temp = manager.cameraPresets[indexA];
+ manager.cameraPresets[indexA] = manager.cameraPresets[indexB];
+ manager.cameraPresets[indexB] = temp;
+ EditorUtility.SetDirty(target);
+ }
+
+ private void HandleKeyListening(CameraManager manager)
+ {
if (isListening)
{
var e = Event.current;
@@ -150,7 +453,7 @@ public class CameraManagerEditor : Editor
if (e.keyCode != KeyCode.Mouse0 && e.keyCode != KeyCode.Mouse1 && e.keyCode != KeyCode.Mouse2)
{
AddKey(e.keyCode);
- e.Use(); // 이벤트 소비
+ e.Use();
}
}
else if (e.type == EventType.KeyUp && currentKeys.Contains(e.keyCode))
@@ -163,7 +466,7 @@ public class CameraManagerEditor : Editor
StopListening();
Repaint();
}
- e.Use(); // 이벤트 소비
+ e.Use();
}
}
}
diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Input/InputHandler.cs b/Assets/Scripts/Streamingle/StreamingleControl/Input/InputHandler.cs
index 0d76cdec..6c9cedb3 100644
--- a/Assets/Scripts/Streamingle/StreamingleControl/Input/InputHandler.cs
+++ b/Assets/Scripts/Streamingle/StreamingleControl/Input/InputHandler.cs
@@ -13,6 +13,10 @@ public class InputHandler : MonoBehaviour
// 현재 활성화된 입력 모드 (충돌 방지)
private InputMode currentMode = InputMode.None;
+ // 포커스 복귀 시 입력 무시를 위한 변수
+ private int focusIgnoreFrames = 0;
+ private const int FOCUS_IGNORE_FRAME_COUNT = 10; // 포커스 복귀 후 무시할 프레임 수
+
private enum InputMode
{
None,
@@ -36,8 +40,45 @@ public class InputHandler : MonoBehaviour
}
}
+ ///
+ /// Unity 포커스 콜백 - 포커스 복귀 시 입력 무시 프레임 설정
+ ///
+ private void OnApplicationFocus(bool hasFocus)
+ {
+ if (hasFocus)
+ {
+ // 포커스 복귀 시 누적된 스크롤 입력 무시
+ focusIgnoreFrames = FOCUS_IGNORE_FRAME_COUNT;
+ }
+ else
+ {
+ // 포커스 해제 시 상태 리셋
+ currentMode = InputMode.None;
+ isOrbitActive = false;
+ isZoomActive = false;
+ isCtrlRightZoomActive = false;
+ isRightMouseHeld = false;
+ isMiddleMouseHeld = false;
+ lastScrollValue = 0f;
+ }
+ }
+
private void Update()
{
+ // 포커스가 없으면 입력 무시
+ if (!Application.isFocused)
+ {
+ return;
+ }
+
+ // 포커스 복귀 후 무시 프레임 카운트다운
+ if (focusIgnoreFrames > 0)
+ {
+ focusIgnoreFrames--;
+ // 무시 프레임 동안에는 스크롤 값을 계속 소비(버림)
+ _ = Input.mouseScrollDelta;
+ }
+
// 마우스 버튼 Raw 상태
bool leftMouse = Input.GetMouseButton(0);
bool rightMouse = Input.GetMouseButton(1);
@@ -123,6 +164,9 @@ public class InputHandler : MonoBehaviour
public Vector2 GetLookDelta()
{
+ // 포커스가 없으면 0 반환
+ if (!Application.isFocused) return Vector2.zero;
+
return new Vector2(
Input.GetAxis("Mouse X"),
Input.GetAxis("Mouse Y")
@@ -131,6 +175,9 @@ public class InputHandler : MonoBehaviour
public float GetZoomDelta()
{
+ // 포커스가 없거나 포커스 복귀 직후면 0 반환 (누적된 휠 값 무시)
+ if (!Application.isFocused || focusIgnoreFrames > 0) return 0f;
+
return Input.mouseScrollDelta.y;
}
diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Rendering.meta b/Assets/Scripts/Streamingle/StreamingleControl/Rendering.meta
new file mode 100644
index 00000000..fb98b7ed
--- /dev/null
+++ b/Assets/Scripts/Streamingle/StreamingleControl/Rendering.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 634dec0a07bb460468b7e86f02bbc5b9
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Rendering/CameraBlend.mat b/Assets/Scripts/Streamingle/StreamingleControl/Rendering/CameraBlend.mat
new file mode 100644
index 00000000..95cbb56f
--- /dev/null
+++ b/Assets/Scripts/Streamingle/StreamingleControl/Rendering/CameraBlend.mat
@@ -0,0 +1,39 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!21 &2100000
+Material:
+ serializedVersion: 8
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_Name: CameraBlend
+ m_Shader: {fileID: 4800000, guid: 76944497fc2bcdd479fc435a5d4d8447, type: 3}
+ m_Parent: {fileID: 0}
+ m_ModifiedSerializedProperties: 0
+ m_ValidKeywords: []
+ m_InvalidKeywords: []
+ m_LightmapFlags: 4
+ m_EnableInstancingVariants: 0
+ m_DoubleSidedGI: 0
+ m_CustomRenderQueue: -1
+ stringTagMap: {}
+ disabledShaderPasses: []
+ m_LockedProperties:
+ m_SavedProperties:
+ serializedVersion: 3
+ m_TexEnvs:
+ - _MainTex:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _PrevTex:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ m_Ints: []
+ m_Floats:
+ - _BlendAmount: 1
+ m_Colors: []
+ m_BuildTextureStacks: []
+ m_AllowLocking: 1
diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Rendering/CameraBlend.mat.meta b/Assets/Scripts/Streamingle/StreamingleControl/Rendering/CameraBlend.mat.meta
new file mode 100644
index 00000000..c0796a4e
--- /dev/null
+++ b/Assets/Scripts/Streamingle/StreamingleControl/Rendering/CameraBlend.mat.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: f563d7f0c3fc17647ad56388fb936427
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Rendering/CameraBlend.shader b/Assets/Scripts/Streamingle/StreamingleControl/Rendering/CameraBlend.shader
new file mode 100644
index 00000000..21b573cb
--- /dev/null
+++ b/Assets/Scripts/Streamingle/StreamingleControl/Rendering/CameraBlend.shader
@@ -0,0 +1,56 @@
+Shader "Streamingle/CameraBlend"
+{
+ Properties
+ {
+ _PrevTex ("Previous Camera", 2D) = "white" {}
+ _BlendAmount ("Blend Amount", Range(0, 1)) = 0
+ }
+
+ SubShader
+ {
+ Tags
+ {
+ "RenderType" = "Opaque"
+ "RenderPipeline" = "UniversalPipeline"
+ }
+
+ Pass
+ {
+ Name "CameraBlend"
+ ZTest Always
+ ZWrite Off
+ Cull Off
+
+ HLSLPROGRAM
+ #pragma vertex Vert
+ #pragma fragment Frag
+
+ #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
+ #include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"
+
+ TEXTURE2D(_PrevTex);
+ SAMPLER(sampler_PrevTex);
+
+ float _BlendAmount;
+
+ float4 Frag(Varyings input) : SV_Target
+ {
+ float2 uv = input.texcoord;
+
+ // 현재 카메라 (Blitter에서 전달됨)
+ float4 currentColor = SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearClamp, uv);
+
+ // 이전 카메라 (캡처된 텍스처)
+ float4 prevColor = SAMPLE_TEXTURE2D(_PrevTex, sampler_PrevTex, uv);
+
+ // 두 카메라를 블렌딩 (BlendAmount: 0 = 이전, 1 = 현재)
+ float4 finalColor = lerp(prevColor, currentColor, _BlendAmount);
+
+ return finalColor;
+ }
+ ENDHLSL
+ }
+ }
+
+ FallBack Off
+}
diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Rendering/CameraBlend.shader.meta b/Assets/Scripts/Streamingle/StreamingleControl/Rendering/CameraBlend.shader.meta
new file mode 100644
index 00000000..60cb11b3
--- /dev/null
+++ b/Assets/Scripts/Streamingle/StreamingleControl/Rendering/CameraBlend.shader.meta
@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 76944497fc2bcdd479fc435a5d4d8447
+ShaderImporter:
+ externalObjects: {}
+ defaultTextures: []
+ nonModifiableTextures: []
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Rendering/CameraBlendRendererFeature.cs b/Assets/Scripts/Streamingle/StreamingleControl/Rendering/CameraBlendRendererFeature.cs
new file mode 100644
index 00000000..fef6c8b8
--- /dev/null
+++ b/Assets/Scripts/Streamingle/StreamingleControl/Rendering/CameraBlendRendererFeature.cs
@@ -0,0 +1,198 @@
+using UnityEngine;
+using UnityEngine.Rendering;
+using UnityEngine.Rendering.Universal;
+using UnityEngine.Rendering.RenderGraphModule;
+
+///
+/// 카메라 전환 시 크로스 디졸브 블렌딩을 위한 URP Renderer Feature
+/// Unity 6 Render Graph API 사용
+///
+public class CameraBlendRendererFeature : ScriptableRendererFeature
+{
+ [System.Serializable]
+ public class Settings
+ {
+ // BeforeRenderingPostProcessing으로 변경 - 포스트 프로세싱 전에 블렌딩
+ public RenderPassEvent renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;
+ public Material blendMaterial;
+ }
+
+ public Settings settings = new Settings();
+ private CameraBlendRenderPass blendPass;
+
+ public override void Create()
+ {
+ blendPass = new CameraBlendRenderPass(settings);
+ blendPass.renderPassEvent = settings.renderPassEvent;
+ }
+
+ public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
+ {
+ if (settings.blendMaterial == null)
+ return;
+
+ // 게임 카메라만 처리
+ if (renderingData.cameraData.cameraType != CameraType.Game)
+ return;
+
+ // 블렌딩이 활성화된 경우에만 패스 추가
+ if (CameraBlendController.IsBlending && CameraBlendController.BlendTexture != null)
+ {
+ renderer.EnqueuePass(blendPass);
+ }
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ blendPass?.Dispose();
+ }
+}
+
+public class CameraBlendRenderPass : ScriptableRenderPass
+{
+ private CameraBlendRendererFeature.Settings settings;
+ private static readonly int BlendAmountProperty = Shader.PropertyToID("_BlendAmount");
+ private static readonly int PrevTexProperty = Shader.PropertyToID("_PrevTex");
+
+ private class PassData
+ {
+ public Material blendMaterial;
+ public float blendAmount;
+ public RenderTexture blendTexture;
+ public TextureHandle sourceTexture;
+ public TextureHandle destinationTexture;
+ }
+
+ public CameraBlendRenderPass(CameraBlendRendererFeature.Settings settings)
+ {
+ this.settings = settings;
+ profilingSampler = new ProfilingSampler("Camera Blend Pass");
+ ConfigureInput(ScriptableRenderPassInput.Color);
+ }
+
+ public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
+ {
+ if (settings.blendMaterial == null)
+ return;
+
+ if (!CameraBlendController.IsBlending || CameraBlendController.BlendTexture == null)
+ return;
+
+ var resourceData = frameData.Get();
+ var cameraData = frameData.Get();
+
+ // 백버퍼인 경우 스킵 (BeforeRenderingPostProcessing에서는 일반적으로 false)
+ if (resourceData.isActiveTargetBackBuffer)
+ {
+ Debug.LogWarning("[CameraBlend] isActiveTargetBackBuffer - skipping");
+ return;
+ }
+
+ var source = resourceData.activeColorTexture;
+
+ // source가 유효한지 확인
+ if (!source.IsValid())
+ {
+ Debug.LogWarning("[CameraBlend] source texture is not valid");
+ return;
+ }
+
+ // cameraColorDesc 사용 - 카메라 색상 텍스처 설명자
+ var cameraTargetDesc = renderGraph.GetTextureDesc(resourceData.cameraColor);
+ cameraTargetDesc.name = "_CameraBlendDestination";
+ cameraTargetDesc.clearBuffer = false;
+ var destination = renderGraph.CreateTexture(cameraTargetDesc);
+
+ // 블렌딩 패스
+ using (var builder = renderGraph.AddRasterRenderPass("Camera Blend", out var passData, profilingSampler))
+ {
+ passData.blendMaterial = settings.blendMaterial;
+ passData.blendAmount = CameraBlendController.BlendAmount;
+ passData.blendTexture = CameraBlendController.BlendTexture;
+ passData.sourceTexture = source;
+
+ builder.UseTexture(source, AccessFlags.Read);
+ builder.SetRenderAttachment(destination, 0, AccessFlags.Write);
+ builder.AllowPassCulling(false);
+
+ builder.SetRenderFunc((PassData data, RasterGraphContext context) =>
+ {
+ data.blendMaterial.SetFloat(BlendAmountProperty, data.blendAmount);
+ data.blendMaterial.SetTexture(PrevTexProperty, data.blendTexture);
+ Blitter.BlitTexture(context.cmd, data.sourceTexture, new Vector4(1, 1, 0, 0), data.blendMaterial, 0);
+ });
+ }
+
+ // 결과를 source로 복사
+ using (var builder = renderGraph.AddRasterRenderPass("Camera Blend Copy Back", out var copyData, profilingSampler))
+ {
+ copyData.sourceTexture = destination;
+
+ builder.UseTexture(destination, AccessFlags.Read);
+ builder.SetRenderAttachment(source, 0, AccessFlags.Write);
+ builder.AllowPassCulling(false);
+
+ builder.SetRenderFunc((PassData data, RasterGraphContext context) =>
+ {
+ Blitter.BlitTexture(context.cmd, data.sourceTexture, new Vector4(1, 1, 0, 0), 0, false);
+ });
+ }
+ }
+
+ [System.Obsolete("This rendering path is for compatibility mode only (when Render Graph is disabled). Use Render Graph API instead.", false)]
+ public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
+ {
+ // Legacy path
+ }
+
+ public void Dispose()
+ {
+ }
+}
+
+///
+/// 카메라 블렌딩을 제어하는 정적 컨트롤러
+///
+public static class CameraBlendController
+{
+ private static bool isBlending = false;
+ private static float blendAmount = 1f;
+ private static RenderTexture blendTexture;
+
+ public static bool IsBlending
+ {
+ get => isBlending;
+ set => isBlending = value;
+ }
+
+ public static float BlendAmount
+ {
+ get => blendAmount;
+ set => blendAmount = Mathf.Clamp01(value);
+ }
+
+ public static RenderTexture BlendTexture
+ {
+ get => blendTexture;
+ set => blendTexture = value;
+ }
+
+ public static void StartBlend(RenderTexture texture)
+ {
+ blendTexture = texture;
+ blendAmount = 0f;
+ isBlending = true;
+ }
+
+ public static void EndBlend()
+ {
+ isBlending = false;
+ blendAmount = 1f;
+ blendTexture = null;
+ }
+
+ public static void Reset()
+ {
+ EndBlend();
+ }
+}
diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Rendering/CameraBlendRendererFeature.cs.meta b/Assets/Scripts/Streamingle/StreamingleControl/Rendering/CameraBlendRendererFeature.cs.meta
new file mode 100644
index 00000000..c1c2037b
--- /dev/null
+++ b/Assets/Scripts/Streamingle/StreamingleControl/Rendering/CameraBlendRendererFeature.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 14b4e395bb7cf744d87b2c1885014709
\ No newline at end of file