From 8298b80dde8e8151baa474e07b3eedc8056ad413 Mon Sep 17 00:00:00 2001 From: user Date: Tue, 13 Jan 2026 00:16:55 +0900 Subject: [PATCH] =?UTF-8?q?ADD=20:=20=EC=B9=B4=EB=A9=94=EB=9D=BC=20?= =?UTF-8?q?=EB=B8=94=EB=A0=8C=EB=94=A9=20=EA=B8=B0=EB=8A=A5=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ingle Render Pipeline Asset_Renderer.asset | 4 +- .../Controllers/CameraController.cs | 247 +++++++++- .../Editor/CameraManagerEditor.cs | 447 +++++++++++++++--- .../StreamingleControl/Input/InputHandler.cs | 47 ++ .../StreamingleControl/Rendering.meta | 8 + .../Rendering/CameraBlend.mat | 39 ++ .../Rendering/CameraBlend.mat.meta | 8 + .../Rendering/CameraBlend.shader | 56 +++ .../Rendering/CameraBlend.shader.meta | 9 + .../Rendering/CameraBlendRendererFeature.cs | 198 ++++++++ .../CameraBlendRendererFeature.cs.meta | 2 + 11 files changed, 979 insertions(+), 86 deletions(-) create mode 100644 Assets/Scripts/Streamingle/StreamingleControl/Rendering.meta create mode 100644 Assets/Scripts/Streamingle/StreamingleControl/Rendering/CameraBlend.mat create mode 100644 Assets/Scripts/Streamingle/StreamingleControl/Rendering/CameraBlend.mat.meta create mode 100644 Assets/Scripts/Streamingle/StreamingleControl/Rendering/CameraBlend.shader create mode 100644 Assets/Scripts/Streamingle/StreamingleControl/Rendering/CameraBlend.shader.meta create mode 100644 Assets/Scripts/Streamingle/StreamingleControl/Rendering/CameraBlendRendererFeature.cs create mode 100644 Assets/Scripts/Streamingle/StreamingleControl/Rendering/CameraBlendRendererFeature.cs.meta 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