using UnityEngine; using System.Collections.Generic; using System.Linq; using System.IO; using System; using Entum; #if MAGICACLOTH2 using MagicaCloth2; #endif /// /// StreamDeck 단일 기능 버튼들을 통합 관리하는 시스템 컨트롤러 /// 각 기능은 고유 ID로 식별되며, 확장이 용이한 구조 /// public class SystemController : MonoBehaviour { [Header("OptiTrack 참조")] public OptitrackStreamingClient optitrackClient; [Header("모션 녹화 설정")] [Tooltip("모션 녹화 시 OptiTrack Motive도 함께 녹화할지 여부")] public bool recordOptiTrackWithMotion = true; [Header("EasyMotion Recorder")] [Tooltip("true면 씬의 모든 MotionDataRecorder를 자동으로 찾습니다")] public bool autoFindRecorders = true; [Tooltip("수동으로 지정할 레코더 목록 (autoFindRecorders가 false일 때 사용)")] public List motionRecorders = new List(); [Header("Facial Motion Capture")] [Tooltip("true면 씬의 모든 페이셜 모션 클라이언트를 자동으로 찾습니다")] public bool autoFindFacialMotionClients = true; [Tooltip("수동으로 지정할 페이셜 모션 클라이언트 목록 (autoFindFacialMotionClients가 false일 때 사용)")] public List facialMotionClients = new List(); [Header("Screenshot Settings")] [Tooltip("스크린샷 해상도 (기본: 4K)")] public int screenshotWidth = 3840; [Tooltip("스크린샷 해상도 (기본: 4K)")] public int screenshotHeight = 2160; [Tooltip("스크린샷 저장 경로 (비어있으면 바탕화면)")] public string screenshotSavePath = ""; [Tooltip("파일명 앞에 붙을 접두사")] public string screenshotFilePrefix = "Screenshot"; [Tooltip("알파 채널 추출용 셰이더")] public Shader alphaShader; [Tooltip("NiloToon Prepass 버퍼 텍스처 이름")] public string niloToonPrepassBufferName = "_NiloToonPrepassBufferTex"; [Tooltip("촬영할 카메라 (비어있으면 메인 카메라 사용)")] public Camera screenshotCamera; [Tooltip("알파 채널 블러 반경 (0 = 블러 없음, 1.0 = 약한 블러)")] [Range(0f, 3f)] public float alphaBlurRadius = 1.0f; [Header("디버그")] public bool enableDebugLog = true; private bool isRecording = false; private Material alphaMaterial; // 싱글톤 패턴 public static SystemController Instance { get; private set; } private void Awake() { if (Instance == null) { Instance = this; } else { Destroy(gameObject); } } private void Start() { // OptiTrack 클라이언트 자동 찾기 if (optitrackClient == null) { optitrackClient = FindObjectOfType(); } // Motion Recorder 자동 찾기 if (autoFindRecorders) { RefreshMotionRecorders(); } // Facial Motion 클라이언트 자동 찾기 if (autoFindFacialMotionClients) { RefreshFacialMotionClients(); } // Screenshot 설정 초기화 if (screenshotCamera == null) { screenshotCamera = Camera.main; } if (alphaShader == null) { alphaShader = Shader.Find("Hidden/AlphaFromNiloToon"); if (alphaShader == null) { LogError("알파 셰이더를 찾을 수 없습니다: Hidden/AlphaFromNiloToon"); } } if (string.IsNullOrEmpty(screenshotSavePath)) { screenshotSavePath = Path.Combine(Application.dataPath, "..", "Screenshots"); } // Screenshots 폴더가 없으면 생성 if (!Directory.Exists(screenshotSavePath)) { Directory.CreateDirectory(screenshotSavePath); Log($"Screenshots 폴더 생성됨: {screenshotSavePath}"); } Log("SystemController 초기화 완료"); Log($"Motion Recorder 개수: {motionRecorders.Count}"); Log($"Facial Motion 클라이언트 개수: {facialMotionClients.Count}"); Log($"Screenshot 카메라: {(screenshotCamera != null ? "설정됨" : "없음")}"); } /// /// 씬에서 모든 MotionDataRecorder를 다시 찾습니다 /// public void RefreshMotionRecorders() { var allRecorders = FindObjectsOfType(); motionRecorders = allRecorders.ToList(); Log($"Motion Recorder {motionRecorders.Count}개 발견"); } #region OptiTrack 마커 기능 /// /// OptiTrack 마커 표시 토글 (켜기/끄기) /// public void ToggleOptitrackMarkers() { if (optitrackClient == null) { LogError("OptitrackStreamingClient를 찾을 수 없습니다!"); return; } optitrackClient.ToggleDrawMarkers(); Log($"OptiTrack 마커 표시: {optitrackClient.DrawMarkers}"); } /// /// OptiTrack 마커 표시 켜기 /// public void ShowOptitrackMarkers() { if (optitrackClient == null) { LogError("OptitrackStreamingClient를 찾을 수 없습니다!"); return; } if (!optitrackClient.DrawMarkers) { optitrackClient.ToggleDrawMarkers(); } Log("OptiTrack 마커 표시 켜짐"); } /// /// OptiTrack 마커 표시 끄기 /// public void HideOptitrackMarkers() { if (optitrackClient == null) { LogError("OptitrackStreamingClient를 찾을 수 없습니다!"); return; } if (optitrackClient.DrawMarkers) { optitrackClient.ToggleDrawMarkers(); } Log("OptiTrack 마커 표시 꺼짐"); } #endregion #region OptiTrack 재접속 기능 /// /// OptiTrack 서버에 재접속 시도 /// public void ReconnectOptitrack() { if (optitrackClient == null) { LogError("OptitrackStreamingClient를 찾을 수 없습니다!"); return; } Log("OptiTrack 재접속 시도..."); optitrackClient.Reconnect(); Log("OptiTrack 재접속 명령 전송 완료"); } /// /// OptiTrack 연결 상태 확인 /// public bool IsOptitrackConnected() { if (optitrackClient == null) { return false; } return optitrackClient.IsConnected(); } /// /// OptiTrack 연결 상태를 문자열로 반환 /// public string GetOptitrackConnectionStatus() { if (optitrackClient == null) { return "OptiTrack 클라이언트 없음"; } return optitrackClient.GetConnectionStatus(); } #endregion #region Facial Motion Capture 재접속 기능 /// /// 씬에서 모든 Facial Motion 클라이언트를 다시 찾습니다 /// public void RefreshFacialMotionClients() { var allClients = FindObjectsOfType(); facialMotionClients = allClients.ToList(); Log($"Facial Motion 클라이언트 {facialMotionClients.Count}개 발견"); } /// /// 모든 Facial Motion 클라이언트 재접속 /// public void ReconnectFacialMotion() { if (autoFindFacialMotionClients) { RefreshFacialMotionClients(); } if (facialMotionClients == null || facialMotionClients.Count == 0) { LogError("Facial Motion 클라이언트가 없습니다!"); return; } Log($"Facial Motion 클라이언트 재접속 시도... ({facialMotionClients.Count}개)"); int reconnectedCount = 0; foreach (var client in facialMotionClients) { if (client != null) { try { client.Reconnect(); reconnectedCount++; Log($"클라이언트 재접속 성공: {client.gameObject.name}"); } catch (System.Exception e) { LogError($"클라이언트 재접속 실패 ({client.gameObject.name}): {e.Message}"); } } } if (reconnectedCount > 0) { Log($"=== Facial Motion 재접속 완료 ({reconnectedCount}/{facialMotionClients.Count}개) ==="); } else { LogError("재접속에 성공한 클라이언트가 없습니다!"); } } #endregion #region 스크린샷 기능 /// /// 일반 스크린샷 촬영 /// public void CaptureScreenshot() { if (screenshotCamera == null) { LogError("촬영할 카메라가 설정되지 않았습니다!"); return; } string fileName = GenerateFileName("png"); string fullPath = Path.Combine(screenshotSavePath, fileName); try { // 렌더 텍스처 생성 RenderTexture rt = new RenderTexture(screenshotWidth, screenshotHeight, 24); RenderTexture currentRT = screenshotCamera.targetTexture; // 카메라로 렌더링 screenshotCamera.targetTexture = rt; screenshotCamera.Render(); // 텍스처를 Texture2D로 변환 RenderTexture.active = rt; Texture2D screenshot = new Texture2D(screenshotWidth, screenshotHeight, TextureFormat.RGB24, false); screenshot.ReadPixels(new Rect(0, 0, screenshotWidth, screenshotHeight), 0, 0); screenshot.Apply(); // PNG로 저장 byte[] bytes = screenshot.EncodeToPNG(); File.WriteAllBytes(fullPath, bytes); // 정리 - RenderTexture는 Release() 후 Destroy() 호출 필요 screenshotCamera.targetTexture = currentRT; RenderTexture.active = null; rt.Release(); Destroy(rt); Destroy(screenshot); Log($"스크린샷 저장 완료: {fullPath}"); } catch (Exception e) { LogError($"스크린샷 촬영 실패: {e.Message}"); } } /// /// 알파 채널 포함 스크린샷 촬영 /// NiloToon Prepass 버퍼의 G 채널을 알파로 사용 /// public void CaptureAlphaScreenshot() { if (screenshotCamera == null) { LogError("촬영할 카메라가 설정되지 않았습니다!"); return; } if (alphaShader == null) { LogError("알파 셰이더가 설정되지 않았습니다!"); return; } string fileName = GenerateFileName("png", "_Alpha"); string fullPath = Path.Combine(screenshotSavePath, fileName); try { // 렌더 텍스처 생성 RenderTexture rt = new RenderTexture(screenshotWidth, screenshotHeight, 24); RenderTexture currentRT = screenshotCamera.targetTexture; // 카메라로 렌더링 screenshotCamera.targetTexture = rt; screenshotCamera.Render(); // NiloToon Prepass 버퍼 가져오기 Texture niloToonPrepassBuffer = Shader.GetGlobalTexture(niloToonPrepassBufferName); if (niloToonPrepassBuffer == null) { LogError($"NiloToon Prepass 버퍼를 찾을 수 없습니다: {niloToonPrepassBufferName}"); screenshotCamera.targetTexture = currentRT; Destroy(rt); return; } // 알파 합성용 머티리얼 생성 if (alphaMaterial == null) { alphaMaterial = new Material(alphaShader); } // 알파 채널 합성 RenderTexture alphaRT = new RenderTexture(screenshotWidth, screenshotHeight, 0, RenderTextureFormat.ARGB32); alphaMaterial.SetTexture("_MainTex", rt); alphaMaterial.SetTexture("_AlphaTex", niloToonPrepassBuffer); alphaMaterial.SetFloat("_BlurRadius", alphaBlurRadius); // Blit으로 알파 합성 Graphics.Blit(rt, alphaRT, alphaMaterial); // 텍스처를 Texture2D로 변환 RenderTexture.active = alphaRT; Texture2D screenshot = new Texture2D(screenshotWidth, screenshotHeight, TextureFormat.RGBA32, false); screenshot.ReadPixels(new Rect(0, 0, screenshotWidth, screenshotHeight), 0, 0); screenshot.Apply(); // PNG로 저장 byte[] bytes = screenshot.EncodeToPNG(); File.WriteAllBytes(fullPath, bytes); // 정리 - RenderTexture는 Release() 후 Destroy() 호출 필요 screenshotCamera.targetTexture = currentRT; RenderTexture.active = null; rt.Release(); Destroy(rt); alphaRT.Release(); Destroy(alphaRT); Destroy(screenshot); Log($"알파 스크린샷 저장 완료: {fullPath}"); } catch (Exception e) { LogError($"알파 스크린샷 촬영 실패: {e.Message}"); } } /// /// 스크린샷 저장 폴더 열기 /// public void OpenScreenshotFolder() { if (Directory.Exists(screenshotSavePath)) { System.Diagnostics.Process.Start(screenshotSavePath); Log($"저장 폴더 열기: {screenshotSavePath}"); } else { LogError($"저장 폴더가 존재하지 않습니다: {screenshotSavePath}"); } } /// /// 파일명 생성 /// private string GenerateFileName(string extension, string suffix = "") { string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); return $"{screenshotFilePrefix}{suffix}_{timestamp}.{extension}"; } #endregion #region 모션 녹화 기능 (EasyMotion Recorder + OptiTrack Motive) /// /// 모션 녹화 시작 (EasyMotion Recorder + OptiTrack Motive) /// public void StartMotionRecording() { // OptiTrack 녹화 시작 (옵션이 켜져 있을 때만) bool optitrackStarted = false; if (recordOptiTrackWithMotion) { if (optitrackClient != null) { try { optitrackStarted = optitrackClient.StartRecording(); if (optitrackStarted) { Log("OptiTrack Motive 녹화 시작 성공"); } else { LogError("OptiTrack Motive 녹화 시작 실패"); } } catch (System.Exception e) { LogError($"OptiTrack 녹화 시작 오류: {e.Message}"); } } else { Log("OptiTrack 클라이언트 없음 - OptiTrack 녹화 건너뜀"); } } else { Log("OptiTrack 녹화 옵션 꺼짐 - OptiTrack 녹화 건너뜀"); } // EasyMotion Recorder 녹화 시작 int startedCount = 0; if (motionRecorders != null && motionRecorders.Count > 0) { foreach (var recorder in motionRecorders) { if (recorder != null) { try { // RecordStart 메서드 호출 var method = recorder.GetType().GetMethod("RecordStart"); if (method != null) { method.Invoke(recorder, null); startedCount++; } } catch (System.Exception e) { LogError($"레코더 시작 실패 ({recorder.name}): {e.Message}"); } } } if (startedCount > 0) { Log($"EasyMotion 녹화 시작 ({startedCount}/{motionRecorders.Count}개 레코더)"); } else { LogError("녹화를 시작한 EasyMotion 레코더가 없습니다!"); } } else { Log("EasyMotion Recorder 없음 - EasyMotion 녹화 건너뜀"); } // 하나라도 성공하면 녹화 중 상태로 설정 if (optitrackStarted || startedCount > 0) { isRecording = true; Log($"=== 녹화 시작 완료 ==="); if (recordOptiTrackWithMotion) { Log($"OptiTrack: {(optitrackStarted ? "시작됨" : "시작 안됨")}"); } else { Log($"OptiTrack: 옵션 꺼짐"); } Log($"EasyMotion: {startedCount}개 레코더 시작됨"); } else { LogError("녹화를 시작할 수 있는 시스템이 없습니다!"); } } /// /// 모션 녹화 중지 (EasyMotion Recorder + OptiTrack Motive) /// public void StopMotionRecording() { // OptiTrack 녹화 중지 (옵션이 켜져 있을 때만) bool optitrackStopped = false; if (recordOptiTrackWithMotion) { if (optitrackClient != null) { try { optitrackStopped = optitrackClient.StopRecording(); if (optitrackStopped) { Log("OptiTrack Motive 녹화 중지 성공"); } else { LogError("OptiTrack Motive 녹화 중지 실패"); } } catch (System.Exception e) { LogError($"OptiTrack 녹화 중지 오류: {e.Message}"); } } else { Log("OptiTrack 클라이언트 없음 - OptiTrack 녹화 중지 건너뜀"); } } else { Log("OptiTrack 녹화 옵션 꺼짐 - OptiTrack 녹화 중지 건너뜀"); } // EasyMotion Recorder 녹화 중지 int stoppedCount = 0; if (motionRecorders != null && motionRecorders.Count > 0) { foreach (var recorder in motionRecorders) { if (recorder != null) { try { // RecordEnd 메서드 호출 var method = recorder.GetType().GetMethod("RecordEnd"); if (method != null) { method.Invoke(recorder, null); stoppedCount++; } } catch (System.Exception e) { LogError($"레코더 중지 실패 ({recorder.name}): {e.Message}"); } } } if (stoppedCount > 0) { Log($"EasyMotion 녹화 중지 ({stoppedCount}/{motionRecorders.Count}개 레코더)"); } else { LogError("녹화를 중지한 EasyMotion 레코더가 없습니다!"); } } else { Log("EasyMotion Recorder 없음 - EasyMotion 녹화 중지 건너뜀"); } // 하나라도 성공하면 녹화 중지 상태로 설정 if (optitrackStopped || stoppedCount > 0) { isRecording = false; Log($"=== 녹화 중지 완료 ==="); Log($"OptiTrack: {(optitrackStopped ? "중지됨" : "중지 안됨")}"); Log($"EasyMotion: {stoppedCount}개 레코더 중지됨"); } else { LogError("녹화를 중지할 수 있는 시스템이 없습니다!"); } } /// /// 모션 녹화 토글 (시작/중지) /// public void ToggleMotionRecording() { if (isRecording) { StopMotionRecording(); } else { StartMotionRecording(); } } /// /// 현재 녹화 중인지 여부 반환 /// public bool IsRecording() { return isRecording; } #endregion #region MagicaCloth 시뮬레이션 기능 #if MAGICACLOTH2 /// /// 씬의 모든 MagicaCloth 시뮬레이션을 리셋합니다 /// /// true면 현재 포즈를 유지하면서 리셋 public void RefreshAllMagicaCloth(bool keepPose = false) { var allMagicaCloths = FindObjectsByType(FindObjectsSortMode.None); if (allMagicaCloths == null || allMagicaCloths.Length == 0) { Log("씬에 MagicaCloth 컴포넌트가 없습니다."); return; } int resetCount = 0; foreach (var cloth in allMagicaCloths) { if (cloth != null && cloth.IsValid()) { try { cloth.ResetCloth(keepPose); resetCount++; } catch (System.Exception e) { LogError($"MagicaCloth 리셋 실패 ({cloth.gameObject.name}): {e.Message}"); } } } Log($"MagicaCloth 시뮬레이션 리셋 완료 ({resetCount}/{allMagicaCloths.Length}개)"); } /// /// 씬의 모든 MagicaCloth 시뮬레이션을 완전히 초기 상태로 리셋합니다 /// public void ResetAllMagicaCloth() { RefreshAllMagicaCloth(false); } /// /// 씬의 모든 MagicaCloth 시뮬레이션을 현재 포즈를 유지하면서 리셋합니다 /// public void ResetAllMagicaClothKeepPose() { RefreshAllMagicaCloth(true); } #else public void RefreshAllMagicaCloth(bool keepPose = false) { LogError("MagicaCloth2가 설치되어 있지 않습니다."); } public void ResetAllMagicaCloth() { LogError("MagicaCloth2가 설치되어 있지 않습니다."); } public void ResetAllMagicaClothKeepPose() { LogError("MagicaCloth2가 설치되어 있지 않습니다."); } #endif #endregion /// /// 명령어 실행 - WebSocket에서 받은 명령을 처리 /// public void ExecuteCommand(string command, Dictionary parameters) { Log($"명령어 실행: {command}"); switch (command) { // OptiTrack 마커 case "toggle_optitrack_markers": ToggleOptitrackMarkers(); break; case "show_optitrack_markers": ShowOptitrackMarkers(); break; case "hide_optitrack_markers": HideOptitrackMarkers(); break; // OptiTrack 재접속 case "reconnect_optitrack": ReconnectOptitrack(); break; // Facial Motion 재접속 case "reconnect_facial_motion": ReconnectFacialMotion(); break; case "refresh_facial_motion_clients": RefreshFacialMotionClients(); break; // EasyMotion Recorder case "start_motion_recording": StartMotionRecording(); break; case "stop_motion_recording": StopMotionRecording(); break; case "toggle_motion_recording": ToggleMotionRecording(); break; case "refresh_motion_recorders": RefreshMotionRecorders(); break; // 스크린샷 case "capture_screenshot": CaptureScreenshot(); break; case "capture_alpha_screenshot": CaptureAlphaScreenshot(); break; case "open_screenshot_folder": OpenScreenshotFolder(); break; // MagicaCloth 시뮬레이션 case "refresh_magica_cloth": case "reset_magica_cloth": ResetAllMagicaCloth(); break; case "reset_magica_cloth_keep_pose": ResetAllMagicaClothKeepPose(); break; default: LogError($"알 수 없는 명령어: {command}"); break; } } private void OnDestroy() { if (alphaMaterial != null) { Destroy(alphaMaterial); } } private void Log(string message) { if (enableDebugLog) { Debug.Log($"[SystemController] {message}"); } } private void LogError(string message) { Debug.LogError($"[SystemController] {message}"); } }