using UnityEngine; using System.Collections.Generic; using System.Linq; using System.IO; using System; using Entum; using KindRetargeting; using KindRetargeting.Remote; #if MAGICACLOTH2 using MagicaCloth2; #endif /// /// StreamDeck 단일 기능 버튼들을 통합 관리하는 시스템 컨트롤러 /// 각 기능은 고유 ID로 식별되며, 확장이 용이한 구조 /// public class SystemController : MonoBehaviour { [Header("OptiTrack 참조")] public OptitrackStreamingClient optitrackClient; [Tooltip("OptiTrack 클라이언트가 없을 때 자동으로 프리펩에서 생성할지 여부")] public bool autoSpawnOptitrackClient = true; [Tooltip("OptiTrack 클라이언트 프리펩 경로 (Resources 폴더 기준이 아닌 경우 직접 할당 필요)")] public GameObject optitrackClientPrefab; [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("리타게팅 웹 리모컨")] [Tooltip("리타게팅 웹 리모컨 컨트롤러 (없으면 자동 생성)")] public RetargetingRemoteController retargetingRemote; [Tooltip("리타게팅 웹 리모컨 HTTP 포트")] public int retargetingHttpPort = 8080; [Tooltip("리타게팅 웹 리모컨 WebSocket 포트")] public int retargetingWsPort = 8081; [Tooltip("시작 시 리타게팅 웹 리모컨 자동 시작")] public bool autoStartRetargetingRemote = true; [Header("아바타 Head 콜라이더")] [Tooltip("아바타 Head 본에 자동으로 SphereCollider를 생성합니다 (DOF 타겟, 레이캐스트 등에 활용)")] public bool autoCreateHeadColliders = true; [Tooltip("Head 콜라이더 반경")] public float headColliderRadius = 0.1f; [Header("디버그")] public bool enableDebugLog = true; private bool isRecording = false; private Material alphaMaterial; // 글로벌 아바타 Head 타겟 목록 private List avatarHeadTargets = new List(); // 싱글톤 패턴 public static SystemController Instance { get; private set; } private void Awake() { if (Instance == null) { Instance = this; } else { Destroy(gameObject); } } private void Start() { // OptiTrack 클라이언트 자동 찾기 및 생성 InitializeOptitrackClient(); // 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}"); } // 리타게팅 웹 리모컨 초기화 InitializeRetargetingRemote(); // 아바타 Head 콜라이더 자동 생성 if (autoCreateHeadColliders) { RefreshAvatarHeadColliders(); } Log("SystemController 초기화 완료"); Log($"OptiTrack 클라이언트: {(optitrackClient != null ? "설정됨" : "없음")}"); 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 클라이언트 초기화 (씬에서 찾거나 프리펩에서 생성) /// private void InitializeOptitrackClient() { // 이미 할당되어 있으면 사용 if (optitrackClient != null) { Log("OptiTrack 클라이언트가 이미 할당되어 있습니다."); return; } // 씬에서 찾기 optitrackClient = FindObjectOfType(); if (optitrackClient != null) { Log("씬에서 OptiTrack 클라이언트를 찾았습니다."); return; } // 씬에 없고 자동 생성 옵션이 켜져 있으면 프리펩에서 생성 if (autoSpawnOptitrackClient) { SpawnOptitrackClientFromPrefab(); } else { Log("OptiTrack 클라이언트가 없습니다. 자동 생성 옵션이 꺼져 있습니다."); } } /// /// OptiTrack 클라이언트 프리펩을 씬에 생성합니다 /// public void SpawnOptitrackClientFromPrefab() { // 이미 씬에 있는지 확인 var existingClient = FindObjectOfType(); if (existingClient != null) { optitrackClient = existingClient; Log("씬에 이미 OptiTrack 클라이언트가 있습니다."); return; } GameObject clientInstance = null; // 직접 할당된 프리펩이 있으면 사용 if (optitrackClientPrefab != null) { clientInstance = Instantiate(optitrackClientPrefab); clientInstance.name = "Client - OptiTrack"; Log("할당된 프리펩에서 OptiTrack 클라이언트 생성됨"); } else { #if UNITY_EDITOR // 에디터에서는 AssetDatabase를 통해 프리펩 로드 string prefabPath = "Assets/External/OptiTrack Unity Plugin/OptiTrack/Prefabs/Client - OptiTrack.prefab"; var prefab = UnityEditor.AssetDatabase.LoadAssetAtPath(prefabPath); if (prefab != null) { clientInstance = Instantiate(prefab); clientInstance.name = "Client - OptiTrack"; Log($"프리펩에서 OptiTrack 클라이언트 생성됨: {prefabPath}"); } else { LogError($"OptiTrack 클라이언트 프리펩을 찾을 수 없습니다: {prefabPath}"); return; } #else LogError("OptiTrack 클라이언트 프리펩이 할당되지 않았습니다. Inspector에서 프리펩을 할당해주세요."); return; #endif } if (clientInstance != null) { optitrackClient = clientInstance.GetComponent(); if (optitrackClient == null) { LogError("생성된 프리펩에 OptitrackStreamingClient 컴포넌트가 없습니다!"); Destroy(clientInstance); } } } #endregion #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 아바타 Head 콜라이더 관리 /// /// 씬의 모든 아바타 Head 본을 찾아 SphereCollider를 생성합니다. /// DOF 타겟, 레이캐스트 등 여러 시스템에서 공통으로 활용됩니다. /// public void RefreshAvatarHeadColliders() { avatarHeadTargets.Clear(); var retargetingScripts = FindObjectsByType(FindObjectsSortMode.None); Log($"아바타 Head 콜라이더 검색: CustomRetargetingScript {retargetingScripts.Length}개 발견"); foreach (var script in retargetingScripts) { if (script.targetAnimator == null) continue; Transform headBone = script.targetAnimator.GetBoneTransform(HumanBodyBones.Head); if (headBone == null) continue; // 콜라이더가 없으면 추가 if (!headBone.TryGetComponent(out _)) { var collider = headBone.gameObject.AddComponent(); collider.radius = headColliderRadius; collider.isTrigger = true; Log($"Head 콜라이더 생성: {headBone.name} ({script.targetAnimator.gameObject.name})"); } avatarHeadTargets.Add(headBone); } Log($"총 {avatarHeadTargets.Count}개의 아바타 Head 타겟 등록 완료"); } /// /// 등록된 아바타 Head 타겟 목록 반환 (다른 시스템에서 활용) /// public List GetAvatarHeadTargets() { return avatarHeadTargets; } #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 #region 리타게팅 웹 리모컨 기능 /// /// 리타게팅 웹 리모컨 초기화 /// private void InitializeRetargetingRemote() { // 이미 할당되어 있으면 사용 if (retargetingRemote == null) { // 씬에서 찾기 retargetingRemote = FindObjectOfType(); } // 없으면 자동 생성 if (retargetingRemote == null && autoStartRetargetingRemote) { GameObject remoteObj = new GameObject("RetargetingRemoteController"); retargetingRemote = remoteObj.AddComponent(); retargetingRemote.AutoStart = false; // SystemController가 관리 Log("리타게팅 웹 리모컨 컨트롤러 자동 생성됨"); } // 포트 설정 적용 if (retargetingRemote != null) { retargetingRemote.HttpPort = retargetingHttpPort; retargetingRemote.WsPort = retargetingWsPort; } // 캐릭터 자동 등록 if (retargetingRemote != null) { AutoRegisterRetargetingCharacters(); if (autoStartRetargetingRemote && !retargetingRemote.IsRunning) { retargetingRemote.StartServer(); Log($"리타게팅 웹 리모컨 시작됨 - http://localhost:{retargetingHttpPort}"); } } } /// /// 씬의 모든 CustomRetargetingScript를 자동으로 등록합니다 /// public void AutoRegisterRetargetingCharacters() { if (retargetingRemote == null) { LogError("리타게팅 웹 리모컨 컨트롤러가 없습니다!"); return; } var characters = FindObjectsByType(FindObjectsSortMode.None); foreach (var character in characters) { retargetingRemote.RegisterCharacter(character); } Log($"리타게팅 캐릭터 {characters.Length}개 등록됨"); } /// /// 리타게팅 웹 리모컨 시작 /// public void StartRetargetingRemote() { if (retargetingRemote == null) { InitializeRetargetingRemote(); } if (retargetingRemote != null && !retargetingRemote.IsRunning) { retargetingRemote.StartServer(); Log($"리타게팅 웹 리모컨 시작됨 - http://localhost:{retargetingHttpPort}"); } } /// /// 리타게팅 웹 리모컨 중지 /// public void StopRetargetingRemote() { if (retargetingRemote != null && retargetingRemote.IsRunning) { retargetingRemote.StopServer(); Log("리타게팅 웹 리모컨 중지됨"); } } /// /// 리타게팅 웹 리모컨 토글 (시작/중지) /// public void ToggleRetargetingRemote() { if (retargetingRemote == null || !retargetingRemote.IsRunning) { StartRetargetingRemote(); } else { StopRetargetingRemote(); } } /// /// 리타게팅 웹 리모컨 실행 중인지 여부 반환 /// public bool IsRetargetingRemoteRunning() { return retargetingRemote != null && retargetingRemote.IsRunning; } /// /// 리타게팅 웹 리모컨 URL 반환 /// public string GetRetargetingRemoteUrl() { if (retargetingRemote != null && retargetingRemote.IsRunning) { return $"http://localhost:{retargetingHttpPort}"; } return ""; } #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; // OptiTrack 클라이언트 생성 case "spawn_optitrack_client": SpawnOptitrackClientFromPrefab(); 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; // 아바타 Head 콜라이더 case "refresh_avatar_head_colliders": RefreshAvatarHeadColliders(); break; // MagicaCloth 시뮬레이션 case "refresh_magica_cloth": case "reset_magica_cloth": ResetAllMagicaCloth(); break; case "reset_magica_cloth_keep_pose": ResetAllMagicaClothKeepPose(); break; // 리타게팅 웹 리모컨 case "start_retargeting_remote": StartRetargetingRemote(); break; case "stop_retargeting_remote": StopRetargetingRemote(); break; case "toggle_retargeting_remote": ToggleRetargetingRemote(); break; case "refresh_retargeting_characters": AutoRegisterRetargetingCharacters(); 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}"); } }