게임패드(Xbox/PS)를 이용한 6DOF 드론 카메라 자유비행 모드 추가 - GamepadInputHandler: 게임패드 입력 처리 (스틱, 트리거, 버튼, D-pad) - DroneCameraMode: 관성 기반 드론 물리 시뮬레이션 및 타겟 자동추적 - CameraController: 드론 모드 토글, 프리셋별 드론 상태 저장/복원 - SystemController: 아바타 Head 콜라이더 자동 생성 및 관리 - StreamDeckServerManager: 드론 모드 WebSocket 연동 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1126 lines
40 KiB
C#
1126 lines
40 KiB
C#
using UnityEngine;
|
|
using Unity.Cinemachine;
|
|
using UnityRawInput;
|
|
using UnityEngine.Rendering;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
|
|
[DefaultExecutionOrder(2)]
|
|
public class CameraControlSystem : MonoBehaviour
|
|
{
|
|
[Header("FOV Physics Settings")]
|
|
[SerializeField] private float fovForce = 400f; // 포스 강도 (고정값) - 2배 증가
|
|
[SerializeField] private float fovDamping = 6f; // 감쇠력
|
|
[SerializeField] private float fovMaxVelocity = 200f; // 최대 속도 - 2배 증가
|
|
[SerializeField] private float minFOV = 0.1f;
|
|
[SerializeField] private float maxFOV = 60f;
|
|
|
|
[Header("DOF Physics Settings")]
|
|
[SerializeField] private float dofStep = 0.01f; // DOF Focal Length 증가/감소 단위
|
|
[SerializeField] private float minDOF = 0.01f; // Focal Length 최소값
|
|
[SerializeField] private float maxDOF = 1f; // Focal Length 최대값
|
|
|
|
[Header("Screenshot Settings")]
|
|
[SerializeField] private string screenshotPath = "Screenshots";
|
|
[SerializeField] private int screenshotResolutionMultiplier = 1;
|
|
|
|
|
|
private CameraManager cameraManager;
|
|
private CinemachineCamera currentVirtualCamera;
|
|
private CameraInfoUI cameraInfoUI;
|
|
private GameObject cameraInfoUIGameObject; // UI GameObject 참조 저장 (정리용)
|
|
|
|
// FOV 물리 시스템 변수들
|
|
private float fovVelocity = 0f; // 현재 FOV 변화 속도
|
|
private float targetFOV = 60f; // 목표 FOV
|
|
private bool isApplyingForce = false; // 현재 포스가 적용 중인지
|
|
|
|
// DOF 값 변수
|
|
private float currentDOFValue = 0.3f; // 현재 DOF Focal Length 값
|
|
|
|
// Beautify Volume Override 참조
|
|
private object beautifyOverride;
|
|
private bool isDOFEnabled = false; // DOF 활성화 상태 (기본값: 꺼짐)
|
|
|
|
// F13/F14 제어 모드 전환 (true: FOV, false: DOF)
|
|
private bool isFOVMode = true;
|
|
|
|
// F18 더블클릭 감지용
|
|
private float lastF18ClickTime = 0f;
|
|
private const float doubleClickThreshold = 0.3f;
|
|
|
|
// DOF 타겟팅용
|
|
private Transform currentDOFTarget = null;
|
|
private List<Transform> availableDOFTargets = new List<Transform>();
|
|
private int currentTargetIndex = 0;
|
|
|
|
private void Awake()
|
|
{
|
|
// 스크린샷 폴더 생성
|
|
if (!System.IO.Directory.Exists(screenshotPath))
|
|
{
|
|
System.IO.Directory.CreateDirectory(screenshotPath);
|
|
}
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
cameraManager = FindObjectOfType<CameraManager>();
|
|
if (cameraManager != null)
|
|
{
|
|
cameraManager.OnCameraChanged += OnCameraChanged;
|
|
UpdateCurrentCamera();
|
|
}
|
|
|
|
// CameraInfoUI 생성 (정리를 위해 참조 저장)
|
|
cameraInfoUIGameObject = new GameObject("CameraInfoUI");
|
|
cameraInfoUI = cameraInfoUIGameObject.AddComponent<CameraInfoUI>();
|
|
|
|
// Beautify 컴포넌트 찾기
|
|
Debug.Log("[CameraControlSystem] Beautify 초기화 시작");
|
|
InitializeBeautify();
|
|
|
|
// DOF 타겟 찾기
|
|
FindDOFTargets();
|
|
|
|
InitializeRawInput();
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
// RawInput 정리
|
|
if (RawInput.IsRunning)
|
|
{
|
|
try
|
|
{
|
|
RawInput.OnKeyDown -= HandleRawKeyDown;
|
|
// 다른 컴포넌트가 사용 중일 수 있으므로 Stop은 호출하지 않음
|
|
}
|
|
catch (System.Exception ex)
|
|
{
|
|
// RawInput 정리 실패 무시
|
|
}
|
|
}
|
|
|
|
// CameraManager 이벤트 해제
|
|
if (cameraManager != null)
|
|
{
|
|
cameraManager.OnCameraChanged -= OnCameraChanged;
|
|
}
|
|
|
|
// CameraInfoUI GameObject 정리
|
|
if (cameraInfoUIGameObject != null)
|
|
{
|
|
Destroy(cameraInfoUIGameObject);
|
|
cameraInfoUIGameObject = null;
|
|
cameraInfoUI = null;
|
|
}
|
|
}
|
|
|
|
private void InitializeRawInput()
|
|
{
|
|
try
|
|
{
|
|
if (!RawInput.IsRunning)
|
|
{
|
|
RawInput.Start();
|
|
RawInput.WorkInBackground = true; // 백그라운드에서도 키 입력 감지
|
|
RawInput.InterceptMessages = false; // 다른 앱으로 키 메시지 전달
|
|
}
|
|
|
|
// 이벤트 중복 등록 방지
|
|
RawInput.OnKeyDown -= HandleRawKeyDown;
|
|
RawInput.OnKeyDown += HandleRawKeyDown;
|
|
}
|
|
catch (System.Exception ex)
|
|
{
|
|
// RawInput 실패 시 Unity Input으로 대체
|
|
}
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
HandleFOVControl();
|
|
UpdateFOVPhysics();
|
|
UpdateDOFPhysics();
|
|
UpdateDOFTargetTracking();
|
|
}
|
|
|
|
private void HandleRawKeyDown(RawKey key)
|
|
{
|
|
switch (key)
|
|
{
|
|
case RawKey.F13:
|
|
if (isFOVMode) IncreaseFOV();
|
|
else IncreaseDOF();
|
|
break;
|
|
case RawKey.F14:
|
|
if (isFOVMode) DecreaseFOV();
|
|
else DecreaseDOF();
|
|
break;
|
|
case RawKey.F15:
|
|
ToggleControlMode();
|
|
break;
|
|
case RawKey.F16:
|
|
TakeScreenshot();
|
|
break;
|
|
case RawKey.F17:
|
|
ToggleCameraUI();
|
|
break;
|
|
case RawKey.F18:
|
|
HandleF18Click();
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void HandleFOVControl()
|
|
{
|
|
// Unity Input으로도 처리 (RawInput과 병행하여 안정성 확보)
|
|
if (Input.GetKeyDown(KeyCode.F13))
|
|
{
|
|
if (isFOVMode) IncreaseFOV();
|
|
else IncreaseDOF();
|
|
}
|
|
else if (Input.GetKeyDown(KeyCode.F14))
|
|
{
|
|
if (isFOVMode) DecreaseFOV();
|
|
else DecreaseDOF();
|
|
}
|
|
else if (Input.GetKeyDown(KeyCode.F15))
|
|
{
|
|
ToggleControlMode();
|
|
}
|
|
else if (Input.GetKeyDown(KeyCode.F16))
|
|
{
|
|
TakeScreenshot();
|
|
}
|
|
else if (Input.GetKeyDown(KeyCode.F17))
|
|
{
|
|
ToggleCameraUI();
|
|
}
|
|
else if (Input.GetKeyDown(KeyCode.F18))
|
|
{
|
|
HandleF18Click();
|
|
}
|
|
}
|
|
|
|
private void OnCameraChanged(CameraManager.CameraPreset oldPreset, CameraManager.CameraPreset newPreset)
|
|
{
|
|
UpdateCurrentCamera();
|
|
}
|
|
|
|
private void UpdateCurrentCamera()
|
|
{
|
|
if (cameraManager?.CurrentPreset?.virtualCamera != null)
|
|
{
|
|
currentVirtualCamera = cameraManager.CurrentPreset.virtualCamera;
|
|
}
|
|
}
|
|
|
|
private void IncreaseFOV()
|
|
{
|
|
if (currentVirtualCamera == null) return;
|
|
|
|
// 현재 FOV를 targetFOV로 초기화 (처음 호출 시)
|
|
if (!isApplyingForce)
|
|
{
|
|
targetFOV = currentVirtualCamera.Lens.FieldOfView;
|
|
isApplyingForce = true;
|
|
}
|
|
|
|
ApplyFOVForce(fovForce); // 양의 포스 적용
|
|
}
|
|
|
|
private void DecreaseFOV()
|
|
{
|
|
if (currentVirtualCamera == null) return;
|
|
|
|
// 현재 FOV를 targetFOV로 초기화 (처음 호출 시)
|
|
if (!isApplyingForce)
|
|
{
|
|
targetFOV = currentVirtualCamera.Lens.FieldOfView;
|
|
isApplyingForce = true;
|
|
}
|
|
|
|
ApplyFOVForce(-fovForce); // 음의 포스 적용
|
|
}
|
|
|
|
private void ApplyFOVForce(float force)
|
|
{
|
|
// 포스를 속도에 더함 (가속도 = 포스, 질량 = 1로 가정)
|
|
fovVelocity += force * Time.deltaTime;
|
|
|
|
// 최대 속도 제한
|
|
fovVelocity = Mathf.Clamp(fovVelocity, -fovMaxVelocity, fovMaxVelocity);
|
|
}
|
|
|
|
private void UpdateFOVPhysics()
|
|
{
|
|
if (currentVirtualCamera == null) return;
|
|
|
|
var lens = currentVirtualCamera.Lens;
|
|
float currentFOV = lens.FieldOfView;
|
|
|
|
// 더 부드러운 감쇠력 적용 (지수적 감쇠)
|
|
float dampingFactor = Mathf.Exp(-fovDamping * Time.deltaTime);
|
|
fovVelocity *= dampingFactor;
|
|
|
|
// 속도가 거의 0에 가까우면 완전히 정지 (더 작은 임계값)
|
|
if (Mathf.Abs(fovVelocity) < 0.05f)
|
|
{
|
|
fovVelocity = 0f;
|
|
isApplyingForce = false;
|
|
}
|
|
|
|
// 속도가 있을 때만 FOV 업데이트
|
|
if (Mathf.Abs(fovVelocity) > 0.001f)
|
|
{
|
|
float newFOV = currentFOV + (fovVelocity * Time.deltaTime);
|
|
|
|
// FOV 범위 제한 및 경계에서 부드러운 반발
|
|
if (newFOV <= minFOV)
|
|
{
|
|
newFOV = minFOV;
|
|
fovVelocity = Mathf.Max(0f, fovVelocity * 0.3f); // 부드러운 반발
|
|
}
|
|
else if (newFOV >= maxFOV)
|
|
{
|
|
newFOV = maxFOV;
|
|
fovVelocity = Mathf.Min(0f, fovVelocity * 0.3f); // 부드러운 반발
|
|
}
|
|
|
|
lens.FieldOfView = newFOV;
|
|
currentVirtualCamera.Lens = lens;
|
|
|
|
targetFOV = newFOV;
|
|
}
|
|
}
|
|
|
|
private void InitializeBeautify()
|
|
{
|
|
Debug.Log("[CameraControlSystem] Volume 생성 또는 찾기 시작...");
|
|
|
|
// 같은 게임오브젝트에서 Volume 컴포넌트 찾기 또는 생성
|
|
Volume volume = GetComponent<Volume>();
|
|
if (volume == null)
|
|
{
|
|
Debug.Log("[CameraControlSystem] Volume이 없어서 새로 생성합니다");
|
|
volume = gameObject.AddComponent<Volume>();
|
|
volume.priority = 10f; // 우선순위 설정
|
|
volume.isGlobal = true; // 전역 볼륨으로 설정
|
|
|
|
// VolumeProfile 생성
|
|
volume.sharedProfile = ScriptableObject.CreateInstance<VolumeProfile>();
|
|
volume.sharedProfile.name = "CameraControl_BeautifyProfile";
|
|
|
|
Debug.Log("[CameraControlSystem] Volume 생성 완료 (우선순위: 10)");
|
|
}
|
|
else
|
|
{
|
|
// 기존 Volume의 우선순위 확인/설정
|
|
if (volume.priority != 10f)
|
|
{
|
|
volume.priority = 10f;
|
|
Debug.Log("[CameraControlSystem] 기존 Volume의 우선순위를 10으로 설정");
|
|
}
|
|
}
|
|
|
|
if (volume != null)
|
|
{
|
|
Debug.Log($"[CameraControlSystem] Volume 발견: {volume.name}");
|
|
if (volume.sharedProfile != null)
|
|
{
|
|
Debug.Log($"[CameraControlSystem] Profile: {volume.sharedProfile.name}");
|
|
|
|
// 정확한 어셈블리명으로 Beautify 타입 찾기
|
|
System.Type beautifyType = System.Type.GetType("Beautify.Universal.Beautify, Unity.RenderPipelines.Universal.Runtime");
|
|
|
|
if (beautifyType != null)
|
|
{
|
|
Debug.Log($"[CameraControlSystem] Beautify 타입 발견: {beautifyType.FullName}");
|
|
}
|
|
else
|
|
{
|
|
Debug.LogError("[CameraControlSystem] Beautify 타입을 찾을 수 없음 (어셈블리명 포함)");
|
|
}
|
|
|
|
if (beautifyType != null)
|
|
{
|
|
try
|
|
{
|
|
// VolumeProfile.TryGet을 올바른 시그니처로 호출
|
|
Debug.Log("[CameraControlSystem] VolumeProfile.TryGet 직접 호출");
|
|
|
|
// out 파라미터를 위한 배열
|
|
object[] parameters = { null };
|
|
|
|
// TryGet<T>(out T component) 메서드 찾기
|
|
var methods = volume.sharedProfile.GetType().GetMethods();
|
|
System.Reflection.MethodInfo tryGetMethod = null;
|
|
|
|
foreach (var method in methods)
|
|
{
|
|
if (method.Name == "TryGet" && method.IsGenericMethodDefinition)
|
|
{
|
|
var paramTypes = method.GetParameters();
|
|
if (paramTypes.Length == 1 && paramTypes[0].ParameterType.IsByRef)
|
|
{
|
|
tryGetMethod = method;
|
|
Debug.Log("[CameraControlSystem] TryGet 메서드 발견");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (tryGetMethod != null)
|
|
{
|
|
var genericMethod = tryGetMethod.MakeGenericMethod(beautifyType);
|
|
|
|
// Invoke 호출
|
|
object[] args = new object[1];
|
|
bool result = (bool)genericMethod.Invoke(volume.sharedProfile, args);
|
|
|
|
if (result && args[0] != null)
|
|
{
|
|
beautifyOverride = args[0];
|
|
Debug.Log($"[CameraControlSystem] Beautify Override 발견! {beautifyOverride.GetType().Name}");
|
|
SetupDOFField(beautifyType);
|
|
return; // 성공!
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning("[CameraControlSystem] Profile에 Beautify가 없음, 새로 생성합니다");
|
|
CreateBeautifyOverride(volume, beautifyType);
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Debug.LogError("[CameraControlSystem] 올바른 TryGet 메서드를 찾을 수 없음");
|
|
}
|
|
}
|
|
catch (System.Exception ex)
|
|
{
|
|
Debug.LogError($"[CameraControlSystem] TryGet 호출 실패: {ex.Message}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Debug.LogError("[CameraControlSystem] Beautify 타입을 찾을 수 없음");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Debug.LogError("[CameraControlSystem] Volume에 Profile이 없음");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Debug.LogError("[CameraControlSystem] 같은 게임오브젝트에 Volume이 없음");
|
|
}
|
|
|
|
// 실패한 경우 BeautifySettings 방식 시도 (백업)
|
|
try
|
|
{
|
|
var beautifySettingsType = System.Type.GetType("Beautify.Universal.BeautifySettings, Beautify");
|
|
if (beautifySettingsType != null)
|
|
{
|
|
Debug.Log("[CameraControlSystem] BeautifySettings 타입 발견");
|
|
|
|
var sharedSettingsProperty = beautifySettingsType.GetProperty("sharedSettings", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public);
|
|
if (sharedSettingsProperty != null)
|
|
{
|
|
beautifyOverride = sharedSettingsProperty.GetValue(null);
|
|
|
|
if (beautifyOverride != null)
|
|
{
|
|
Debug.Log($"[CameraControlSystem] Beautify Override 발견: {beautifyOverride.GetType().Name}");
|
|
|
|
// depthOfFieldDistance 필드 확인
|
|
var dofDistanceField = beautifyOverride.GetType().GetField("depthOfFieldDistance");
|
|
if (dofDistanceField != null)
|
|
{
|
|
Debug.Log($"[CameraControlSystem] depthOfFieldDistance 필드 발견: {dofDistanceField.FieldType}");
|
|
|
|
var fieldValue = dofDistanceField.GetValue(beautifyOverride);
|
|
if (fieldValue != null)
|
|
{
|
|
Debug.Log($"[CameraControlSystem] 필드 값 타입: {fieldValue.GetType()}");
|
|
|
|
// FloatParameter 타입의 value 속성 가져오기
|
|
var valueProperty = fieldValue.GetType().GetProperty("value");
|
|
if (valueProperty != null && valueProperty.PropertyType == typeof(float))
|
|
{
|
|
currentDOFValue = (float)valueProperty.GetValue(fieldValue);
|
|
Debug.Log($"[CameraControlSystem] 초기 DOF 거리: {currentDOFValue}");
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning("[CameraControlSystem] value 속성을 찾을 수 없음");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning("[CameraControlSystem] depthOfFieldDistance 필드 값이 null");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning("[CameraControlSystem] depthOfFieldDistance 필드를 찾을 수 없음");
|
|
|
|
// 모든 필드 출력해보기
|
|
var allFields = beautifyOverride.GetType().GetFields();
|
|
Debug.Log($"[CameraControlSystem] 사용 가능한 모든 필드 ({allFields.Length}개):");
|
|
foreach (var field in allFields)
|
|
{
|
|
if (field.Name.ToLower().Contains("depth") || field.Name.ToLower().Contains("dof") || field.Name.ToLower().Contains("focus"))
|
|
{
|
|
var fieldValue = field.GetValue(beautifyOverride);
|
|
if (fieldValue != null)
|
|
{
|
|
var valueProperty = fieldValue.GetType().GetProperty("value");
|
|
var overrideProperty = fieldValue.GetType().GetProperty("overrideState");
|
|
if (valueProperty != null && overrideProperty != null)
|
|
{
|
|
var currentValue = valueProperty.GetValue(fieldValue);
|
|
var isOverridden = overrideProperty.GetValue(fieldValue);
|
|
Debug.Log($" - {field.Name} ({field.FieldType}) = {currentValue}, Override: {isOverridden}");
|
|
}
|
|
else
|
|
{
|
|
Debug.Log($" - {field.Name} ({field.FieldType}) = {fieldValue}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Debug.Log($" - {field.Name} ({field.FieldType}) = null");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning("[CameraControlSystem] BeautifySettings.sharedSettings가 null");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning("[CameraControlSystem] sharedSettings 속성을 찾을 수 없음");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning("[CameraControlSystem] BeautifySettings 타입을 찾을 수 없음");
|
|
}
|
|
}
|
|
catch (System.Exception ex)
|
|
{
|
|
Debug.LogError($"[CameraControlSystem] Beautify 초기화 실패: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
// DOF 제어 메서드들
|
|
private void IncreaseDOF()
|
|
{
|
|
if (beautifyOverride == null || !isDOFEnabled) return;
|
|
|
|
currentDOFValue = Mathf.Clamp(currentDOFValue + dofStep, minDOF, maxDOF);
|
|
SetDOFValue(currentDOFValue);
|
|
Debug.Log($"[CameraControlSystem] DOF 증가: {currentDOFValue:F3}");
|
|
}
|
|
|
|
private void DecreaseDOF()
|
|
{
|
|
if (beautifyOverride == null || !isDOFEnabled) return;
|
|
|
|
currentDOFValue = Mathf.Clamp(currentDOFValue - dofStep, minDOF, maxDOF);
|
|
SetDOFValue(currentDOFValue);
|
|
Debug.Log($"[CameraControlSystem] DOF 감소: {currentDOFValue:F3}");
|
|
}
|
|
|
|
private void UpdateDOFPhysics()
|
|
{
|
|
// 더 이상 물리 시스템 사용 안 함
|
|
}
|
|
|
|
// DOF Focal Length 값을 가져오는 헬퍼 메서드
|
|
private float? GetCurrentDOFValue()
|
|
{
|
|
if (beautifyOverride == null) return null;
|
|
|
|
string[] possibleFieldNames = {
|
|
"depthOfFieldFocalLength",
|
|
"focalLength",
|
|
"dofFocalLength"
|
|
};
|
|
|
|
var overrideType = beautifyOverride.GetType();
|
|
|
|
foreach (string fieldName in possibleFieldNames)
|
|
{
|
|
var dofField = overrideType.GetField(fieldName);
|
|
if (dofField != null)
|
|
{
|
|
var fieldValue = dofField.GetValue(beautifyOverride);
|
|
if (fieldValue != null)
|
|
{
|
|
// VolumeParameter<float> 타입
|
|
var valueProperty = fieldValue.GetType().GetProperty("value");
|
|
if (valueProperty != null && valueProperty.PropertyType == typeof(float))
|
|
{
|
|
return (float)valueProperty.GetValue(fieldValue);
|
|
}
|
|
// 직접 float 타입
|
|
else if (fieldValue is float)
|
|
{
|
|
return (float)fieldValue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// DOF Focal Length 값을 설정하는 헬퍼 메서드
|
|
private void SetDOFValue(float newValue)
|
|
{
|
|
if (beautifyOverride == null)
|
|
{
|
|
Debug.LogWarning("[CameraControlSystem] beautifyOverride가 null입니다");
|
|
return;
|
|
}
|
|
|
|
string[] possibleFieldNames = {
|
|
"depthOfFieldFocalLength",
|
|
"focalLength",
|
|
"dofFocalLength"
|
|
};
|
|
|
|
var overrideType = beautifyOverride.GetType();
|
|
|
|
foreach (string fieldName in possibleFieldNames)
|
|
{
|
|
var dofField = overrideType.GetField(fieldName);
|
|
if (dofField != null)
|
|
{
|
|
var fieldValue = dofField.GetValue(beautifyOverride);
|
|
if (fieldValue != null)
|
|
{
|
|
// VolumeParameter<float> 타입
|
|
var valueProperty = fieldValue.GetType().GetProperty("value");
|
|
if (valueProperty != null && valueProperty.PropertyType == typeof(float))
|
|
{
|
|
if (valueProperty.CanWrite)
|
|
{
|
|
// Override 상태를 먼저 활성화
|
|
var overrideState = fieldValue.GetType().GetProperty("overrideState");
|
|
if (overrideState != null && overrideState.CanWrite)
|
|
{
|
|
overrideState.SetValue(fieldValue, true);
|
|
}
|
|
|
|
valueProperty.SetValue(fieldValue, newValue);
|
|
return;
|
|
}
|
|
}
|
|
// 직접 float 타입
|
|
else if (fieldValue is float && dofField.FieldType == typeof(float))
|
|
{
|
|
dofField.SetValue(beautifyOverride, newValue);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Debug.LogError("[CameraControlSystem] DOF Focal Length 필드를 찾을 수 없거나 설정할 수 없습니다");
|
|
}
|
|
|
|
private void CreateBeautifyOverride(Volume volume, System.Type beautifyType)
|
|
{
|
|
try
|
|
{
|
|
Debug.Log("[CameraControlSystem] Beautify Override 생성 중...");
|
|
|
|
// Beautify 인스턴스를 ScriptableObject.CreateInstance로 생성
|
|
beautifyOverride = ScriptableObject.CreateInstance(beautifyType);
|
|
|
|
if (beautifyOverride != null)
|
|
{
|
|
Debug.Log("[CameraControlSystem] Beautify Override 인스턴스 생성 성공 (ScriptableObject)");
|
|
|
|
// VolumeProfile에 추가하는 다양한 방법 시도
|
|
bool addSuccess = false;
|
|
|
|
// 방법 1: Add<T>() 메서드들 확인
|
|
var methods = volume.sharedProfile.GetType().GetMethods();
|
|
foreach (var method in methods)
|
|
{
|
|
if (method.Name == "Add")
|
|
{
|
|
Debug.Log($"[CameraControlSystem] Add 메서드 발견: {method}");
|
|
var parameters = method.GetParameters();
|
|
foreach (var param in parameters)
|
|
{
|
|
Debug.Log($" - 파라미터: {param.ParameterType} {param.Name}");
|
|
}
|
|
|
|
// T Add<T>(bool overrides = false) where T : VolumeComponent, new() 시그니처 찾기
|
|
if (method.IsGenericMethodDefinition && parameters.Length <= 1)
|
|
{
|
|
try
|
|
{
|
|
var genericMethod = method.MakeGenericMethod(beautifyType);
|
|
object[] args = parameters.Length == 0 ? new object[0] : new object[] { false };
|
|
|
|
var addedComponent = genericMethod.Invoke(volume.sharedProfile, args);
|
|
|
|
if (addedComponent != null)
|
|
{
|
|
beautifyOverride = addedComponent;
|
|
Debug.Log("[CameraControlSystem] Beautify Override가 Profile에 추가됨 (Add<T>)");
|
|
addSuccess = true;
|
|
break;
|
|
}
|
|
}
|
|
catch (System.Exception ex)
|
|
{
|
|
Debug.Log($"[CameraControlSystem] Add<T> 시도 실패: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 방법 2: components 리스트에 직접 추가
|
|
if (!addSuccess)
|
|
{
|
|
try
|
|
{
|
|
var componentsField = volume.sharedProfile.GetType().GetField("components", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
|
if (componentsField != null)
|
|
{
|
|
var components = (System.Collections.IList)componentsField.GetValue(volume.sharedProfile);
|
|
components.Add(beautifyOverride);
|
|
|
|
Debug.Log("[CameraControlSystem] Beautify Override가 Profile에 추가됨 (직접 추가)");
|
|
addSuccess = true;
|
|
}
|
|
}
|
|
catch (System.Exception ex)
|
|
{
|
|
Debug.LogError($"[CameraControlSystem] 직접 추가 실패: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
if (addSuccess)
|
|
{
|
|
SetupDOFField(beautifyType);
|
|
}
|
|
else
|
|
{
|
|
Debug.LogError("[CameraControlSystem] Beautify Override 추가 실패");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Debug.LogError("[CameraControlSystem] Beautify Override 인스턴스 생성 실패");
|
|
}
|
|
}
|
|
catch (System.Exception ex)
|
|
{
|
|
Debug.LogError($"[CameraControlSystem] Beautify Override 생성 실패: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private void SetupDOFField(System.Type beautifyType)
|
|
{
|
|
// 모든 DOF 관련 설정들
|
|
string[] dofFieldsToEnable = {
|
|
"depthOfField", // DOF 메인 활성화
|
|
"depthOfFieldDistance", // DOF 거리
|
|
"depthOfFieldBokeh", // 보케 효과
|
|
"depthOfFieldForegroundBlur", // 전경 블러
|
|
"depthOfFieldMaxSamples", // 최대 샘플
|
|
"depthOfFieldDownsampling", // 다운샘플링
|
|
"depthOfFieldMaxBrightness", // 최대 밝기
|
|
"depthOfFieldFocalLength" // 포커스 렌즈 길이
|
|
};
|
|
|
|
foreach (string fieldName in dofFieldsToEnable)
|
|
{
|
|
var field = beautifyType.GetField(fieldName);
|
|
if (field != null)
|
|
{
|
|
var fieldValue = field.GetValue(beautifyOverride);
|
|
if (fieldValue != null)
|
|
{
|
|
var valueProperty = fieldValue.GetType().GetProperty("value");
|
|
var overrideProperty = fieldValue.GetType().GetProperty("overrideState");
|
|
|
|
if (valueProperty != null && overrideProperty != null)
|
|
{
|
|
// Override 상태 활성화
|
|
overrideProperty.SetValue(fieldValue, true);
|
|
|
|
// 필드별 기본값 설정
|
|
switch (fieldName)
|
|
{
|
|
case "depthOfField":
|
|
valueProperty.SetValue(fieldValue, false); // 기본값: 비활성화
|
|
break;
|
|
|
|
case "depthOfFieldDistance":
|
|
valueProperty.SetValue(fieldValue, 1f);
|
|
currentDOFValue = 1f;
|
|
break;
|
|
|
|
case "depthOfFieldBokeh":
|
|
valueProperty.SetValue(fieldValue, true);
|
|
break;
|
|
|
|
case "depthOfFieldForegroundBlur":
|
|
valueProperty.SetValue(fieldValue, true);
|
|
break;
|
|
|
|
case "depthOfFieldMaxSamples":
|
|
if (valueProperty.PropertyType == typeof(int))
|
|
{
|
|
valueProperty.SetValue(fieldValue, 8);
|
|
}
|
|
break;
|
|
|
|
case "depthOfFieldDownsampling":
|
|
if (valueProperty.PropertyType == typeof(int))
|
|
{
|
|
valueProperty.SetValue(fieldValue, 2);
|
|
}
|
|
break;
|
|
|
|
case "depthOfFieldMaxBrightness":
|
|
if (valueProperty.PropertyType == typeof(float))
|
|
{
|
|
valueProperty.SetValue(fieldValue, 1000f);
|
|
}
|
|
break;
|
|
|
|
case "depthOfFieldFocalLength":
|
|
if (valueProperty.PropertyType == typeof(float))
|
|
{
|
|
valueProperty.SetValue(fieldValue, 0.3f); // 포커스 렌즈 길이 0.3
|
|
currentDOFValue = 0.3f;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void TakeScreenshot()
|
|
{
|
|
StartCoroutine(TakeScreenshotCoroutine());
|
|
}
|
|
|
|
private System.Collections.IEnumerator TakeScreenshotCoroutine()
|
|
{
|
|
// UI 임시 숨기기
|
|
bool wasUIVisible = cameraInfoUI?.IsUIVisible ?? false;
|
|
if (wasUIVisible && cameraInfoUI != null)
|
|
{
|
|
cameraInfoUI.ToggleUI();
|
|
}
|
|
|
|
// 한 프레임 대기 (UI 숨김 적용)
|
|
yield return new WaitForEndOfFrame();
|
|
|
|
string timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
|
|
string filename = $"Screenshot_{timestamp}.png";
|
|
string fullPath = System.IO.Path.Combine(screenshotPath, filename);
|
|
|
|
// 고해상도 스크린샷 촬영 (플래시 효과 전에!)
|
|
ScreenCapture.CaptureScreenshot(fullPath, screenshotResolutionMultiplier);
|
|
|
|
// 스크린샷 촬영 후 플래시 효과 시작
|
|
if (cameraInfoUI != null)
|
|
{
|
|
cameraInfoUI.TriggerScreenshotFlash();
|
|
}
|
|
|
|
// 0.1초 후 UI 복원 (스크린샷 완료 보장)
|
|
yield return new WaitForSeconds(0.1f);
|
|
|
|
if (wasUIVisible && cameraInfoUI != null)
|
|
{
|
|
cameraInfoUI.ToggleUI();
|
|
}
|
|
}
|
|
|
|
private void ToggleCameraUI()
|
|
{
|
|
if (cameraInfoUI != null)
|
|
{
|
|
cameraInfoUI.ToggleUI();
|
|
}
|
|
}
|
|
|
|
private void ToggleDOF()
|
|
{
|
|
if (beautifyOverride == null) return;
|
|
|
|
isDOFEnabled = !isDOFEnabled;
|
|
|
|
var overrideType = beautifyOverride.GetType();
|
|
var dofField = overrideType.GetField("depthOfField");
|
|
|
|
if (dofField != null)
|
|
{
|
|
var fieldValue = dofField.GetValue(beautifyOverride);
|
|
if (fieldValue != null)
|
|
{
|
|
var valueProperty = fieldValue.GetType().GetProperty("value");
|
|
var overrideProperty = fieldValue.GetType().GetProperty("overrideState");
|
|
|
|
if (valueProperty != null && overrideProperty != null)
|
|
{
|
|
overrideProperty.SetValue(fieldValue, true);
|
|
valueProperty.SetValue(fieldValue, isDOFEnabled);
|
|
|
|
Debug.Log($"[CameraControlSystem] DOF {(isDOFEnabled ? "활성화" : "비활성화")}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ToggleControlMode()
|
|
{
|
|
isFOVMode = !isFOVMode;
|
|
Debug.Log($"[CameraControlSystem] 제어 모드 전환: {(isFOVMode ? "FOV" : "DOF")} 모드");
|
|
}
|
|
|
|
private void HandleF18Click()
|
|
{
|
|
float currentTime = Time.time;
|
|
float timeSinceLastClick = currentTime - lastF18ClickTime;
|
|
|
|
if (timeSinceLastClick <= doubleClickThreshold)
|
|
{
|
|
// 더블클릭: DOF ON/OFF
|
|
ToggleDOF();
|
|
}
|
|
else
|
|
{
|
|
// 싱글클릭: DOF 타겟 전환
|
|
CycleDOFTarget();
|
|
}
|
|
|
|
lastF18ClickTime = currentTime;
|
|
}
|
|
|
|
private void FindDOFTargets()
|
|
{
|
|
availableDOFTargets.Clear();
|
|
|
|
// SystemController에서 글로벌하게 관리하는 Head 타겟 목록 가져오기
|
|
var systemController = SystemController.Instance;
|
|
if (systemController != null)
|
|
{
|
|
var headTargets = systemController.GetAvatarHeadTargets();
|
|
if (headTargets != null && headTargets.Count > 0)
|
|
{
|
|
availableDOFTargets.AddRange(headTargets);
|
|
Debug.Log($"[CameraControlSystem] SystemController에서 DOF 타겟 {availableDOFTargets.Count}개 가져옴");
|
|
return;
|
|
}
|
|
}
|
|
|
|
// SystemController가 없거나 타겟이 비어있으면 직접 검색 (폴백)
|
|
var retargetingScripts = FindObjectsByType<KindRetargeting.CustomRetargetingScript>(FindObjectsSortMode.None);
|
|
foreach (var script in retargetingScripts)
|
|
{
|
|
if (script.targetAnimator == null) continue;
|
|
|
|
Transform headBone = script.targetAnimator.GetBoneTransform(HumanBodyBones.Head);
|
|
if (headBone == null) continue;
|
|
|
|
if (!headBone.TryGetComponent<SphereCollider>(out _))
|
|
{
|
|
var collider = headBone.gameObject.AddComponent<SphereCollider>();
|
|
collider.radius = 0.1f;
|
|
collider.isTrigger = true;
|
|
}
|
|
|
|
availableDOFTargets.Add(headBone);
|
|
}
|
|
|
|
Debug.Log($"[CameraControlSystem] DOF 타겟 {availableDOFTargets.Count}개 발견 (폴백)");
|
|
}
|
|
|
|
private void CycleDOFTarget()
|
|
{
|
|
if (currentVirtualCamera == null)
|
|
{
|
|
Debug.LogWarning("[CameraControlSystem] 가상 카메라가 없습니다.");
|
|
return;
|
|
}
|
|
|
|
// 카메라 중앙에서 레이 발사
|
|
Camera mainCamera = Camera.main;
|
|
if (mainCamera == null)
|
|
{
|
|
Debug.LogWarning("[CameraControlSystem] 메인 카메라를 찾을 수 없습니다.");
|
|
return;
|
|
}
|
|
|
|
Ray ray = mainCamera.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0));
|
|
RaycastHit[] hits = Physics.RaycastAll(ray, 100f);
|
|
|
|
Debug.Log($"[CameraControlSystem] 레이캐스트: {hits.Length}개의 오브젝트 감지");
|
|
|
|
// SphereCollider가 있는 Head 본만 필터링
|
|
List<RaycastHit> validHits = new List<RaycastHit>();
|
|
foreach (var hit in hits)
|
|
{
|
|
if (hit.collider is SphereCollider && hit.transform.name.Contains("Head"))
|
|
{
|
|
validHits.Add(hit);
|
|
Debug.Log($"[CameraControlSystem] 유효한 타겟 발견: {hit.transform.name} (거리: {hit.distance:F2}m)");
|
|
}
|
|
}
|
|
|
|
if (validHits.Count == 0)
|
|
{
|
|
Debug.LogWarning("[CameraControlSystem] 레이에 맞은 Head 타겟이 없습니다.");
|
|
return;
|
|
}
|
|
|
|
// 거리순으로 정렬
|
|
validHits.Sort((a, b) => a.distance.CompareTo(b.distance));
|
|
|
|
// 현재 타겟 찾기
|
|
int nextIndex = 0;
|
|
if (currentDOFTarget != null)
|
|
{
|
|
for (int i = 0; i < validHits.Count; i++)
|
|
{
|
|
if (validHits[i].transform == currentDOFTarget)
|
|
{
|
|
nextIndex = (i + 1) % validHits.Count;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 다음 타겟 설정
|
|
currentDOFTarget = validHits[nextIndex].transform;
|
|
float distance = validHits[nextIndex].distance;
|
|
|
|
// depthOfFieldDistance 설정
|
|
SetDOFDistance(distance);
|
|
|
|
Debug.Log($"[CameraControlSystem] DOF 타겟 설정: {currentDOFTarget.name} (거리: {distance:F2}m)");
|
|
}
|
|
|
|
private void SetDOFDistance(float distance)
|
|
{
|
|
if (beautifyOverride == null)
|
|
{
|
|
Debug.LogWarning("[CameraControlSystem] beautifyOverride가 null입니다");
|
|
return;
|
|
}
|
|
|
|
var overrideType = beautifyOverride.GetType();
|
|
var dofDistanceField = overrideType.GetField("depthOfFieldDistance");
|
|
|
|
if (dofDistanceField != null)
|
|
{
|
|
var fieldValue = dofDistanceField.GetValue(beautifyOverride);
|
|
if (fieldValue != null)
|
|
{
|
|
var valueProperty = fieldValue.GetType().GetProperty("value");
|
|
var overrideProperty = fieldValue.GetType().GetProperty("overrideState");
|
|
|
|
if (valueProperty != null && overrideProperty != null)
|
|
{
|
|
overrideProperty.SetValue(fieldValue, true);
|
|
valueProperty.SetValue(fieldValue, distance);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void UpdateDOFTargetTracking()
|
|
{
|
|
// 현재 타겟이 설정되어 있고, DOF가 활성화되어 있으면 지속적으로 거리 업데이트
|
|
if (currentDOFTarget != null && isDOFEnabled && currentVirtualCamera != null)
|
|
{
|
|
float distance = Vector3.Distance(currentVirtualCamera.transform.position, currentDOFTarget.position);
|
|
SetDOFDistance(distance);
|
|
}
|
|
}
|
|
|
|
// Public 메서드들 (외부에서 호출 가능)
|
|
public void SetFOV(float fov)
|
|
{
|
|
if (currentVirtualCamera == null) return;
|
|
|
|
var lens = currentVirtualCamera.Lens;
|
|
lens.FieldOfView = Mathf.Clamp(fov, minFOV, maxFOV);
|
|
currentVirtualCamera.Lens = lens;
|
|
}
|
|
|
|
public float GetCurrentFOV()
|
|
{
|
|
return currentVirtualCamera?.Lens.FieldOfView ?? 0f;
|
|
}
|
|
|
|
public void SetFOVLimits(float min, float max)
|
|
{
|
|
minFOV = min;
|
|
maxFOV = max;
|
|
}
|
|
|
|
public void SetScreenshotPath(string path)
|
|
{
|
|
screenshotPath = path;
|
|
if (!System.IO.Directory.Exists(screenshotPath))
|
|
{
|
|
System.IO.Directory.CreateDirectory(screenshotPath);
|
|
}
|
|
}
|
|
|
|
// UI에서 사용할 Public 메서드들
|
|
public float GetCurrentForce()
|
|
{
|
|
return fovForce;
|
|
}
|
|
|
|
public float GetCurrentVelocity()
|
|
{
|
|
return fovVelocity;
|
|
}
|
|
|
|
// DOF UI에서 사용할 Public 메서드들
|
|
public float GetCurrentDOF()
|
|
{
|
|
var dofValue = GetCurrentDOFValue();
|
|
return dofValue.HasValue ? dofValue.Value : 0f;
|
|
}
|
|
|
|
public float GetCurrentDofForce()
|
|
{
|
|
return dofStep;
|
|
}
|
|
|
|
public float GetCurrentDofVelocity()
|
|
{
|
|
return 0f; // 더 이상 velocity 사용 안 함
|
|
}
|
|
|
|
public bool IsFOVMode()
|
|
{
|
|
return isFOVMode;
|
|
}
|
|
} |