ADD : 카메라 블렌딩 기능 업데이트

This commit is contained in:
user 2026-01-13 00:16:55 +09:00
parent c1ae4b4379
commit 8298b80dde
11 changed files with 979 additions and 86 deletions

View File

@ -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);
}
/// <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)
{
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;
}
/// <summary>
/// 블렌드용 카메라와 렌더 텍스처 생성
/// </summary>
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<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;
}
Debug.Log("[CameraManager] 블렌드 카메라 생성 완료 (URP 설정 포함)");
}
/// <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);
}
// 새 렌더 텍스처 생성
blendRenderTexture = new RenderTexture(width, height, 24, RenderTextureFormat.ARGB32);
blendRenderTexture.name = "CameraBlendRT";
blendRenderTexture.Create();
Debug.Log($"[CameraManager] 블렌드 렌더 텍스처 생성: {width}x{height}");
}
/// <summary>
/// 크로스 디졸브 블렌드 전환 코루틴
/// </summary>
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)

View File

@ -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<CinemachineCamera>();
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();
}
}
}

View File

@ -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
}
}
/// <summary>
/// Unity 포커스 콜백 - 포커스 복귀 시 입력 무시 프레임 설정
/// </summary>
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;
}

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 634dec0a07bb460468b7e86f02bbc5b9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f563d7f0c3fc17647ad56388fb936427
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 0
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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
}

View File

@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 76944497fc2bcdd479fc435a5d4d8447
ShaderImporter:
externalObjects: {}
defaultTextures: []
nonModifiableTextures: []
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,198 @@
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.Rendering.RenderGraphModule;
/// <summary>
/// 카메라 전환 시 크로스 디졸브 블렌딩을 위한 URP Renderer Feature
/// Unity 6 Render Graph API 사용
/// </summary>
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<UniversalResourceData>();
var cameraData = frameData.Get<UniversalCameraData>();
// 백버퍼인 경우 스킵 (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<PassData>("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<PassData>("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()
{
}
}
/// <summary>
/// 카메라 블렌딩을 제어하는 정적 컨트롤러
/// </summary>
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();
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 14b4e395bb7cf744d87b2c1885014709