user efc0adced8 ADD: 드론 카메라 게임패드 제어 시스템
게임패드(Xbox/PS)를 이용한 6DOF 드론 카메라 자유비행 모드 추가
- GamepadInputHandler: 게임패드 입력 처리 (스틱, 트리거, 버튼, D-pad)
- DroneCameraMode: 관성 기반 드론 물리 시뮬레이션 및 타겟 자동추적
- CameraController: 드론 모드 토글, 프리셋별 드론 상태 저장/복원
- SystemController: 아바타 Head 콜라이더 자동 생성 및 관리
- StreamDeckServerManager: 드론 모드 WebSocket 연동

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 22:57:59 +09:00

1193 lines
36 KiB
C#

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
/// <summary>
/// StreamDeck 단일 기능 버튼들을 통합 관리하는 시스템 컨트롤러
/// 각 기능은 고유 ID로 식별되며, 확장이 용이한 구조
/// </summary>
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<MotionDataRecorder> motionRecorders = new List<MotionDataRecorder>();
[Header("Facial Motion Capture")]
[Tooltip("true면 씬의 모든 페이셜 모션 클라이언트를 자동으로 찾습니다")]
public bool autoFindFacialMotionClients = true;
[Tooltip("수동으로 지정할 페이셜 모션 클라이언트 목록 (autoFindFacialMotionClients가 false일 때 사용)")]
public List<StreamingleFacialReceiver> facialMotionClients = new List<StreamingleFacialReceiver>();
[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<Transform> avatarHeadTargets = new List<Transform>();
// 싱글톤 패턴
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 ? "" : "")}");
}
/// <summary>
/// 씬에서 모든 MotionDataRecorder를 다시 찾습니다
/// </summary>
public void RefreshMotionRecorders()
{
var allRecorders = FindObjectsOfType<MotionDataRecorder>();
motionRecorders = allRecorders.ToList();
Log($"Motion Recorder {motionRecorders.Count}개 발견");
}
#region OptiTrack
/// <summary>
/// OptiTrack 클라이언트 초기화 (씬에서 찾거나 프리펩에서 생성)
/// </summary>
private void InitializeOptitrackClient()
{
// 이미 할당되어 있으면 사용
if (optitrackClient != null)
{
Log("OptiTrack 클라이언트가 이미 할당되어 있습니다.");
return;
}
// 씬에서 찾기
optitrackClient = FindObjectOfType<OptitrackStreamingClient>();
if (optitrackClient != null)
{
Log("씬에서 OptiTrack 클라이언트를 찾았습니다.");
return;
}
// 씬에 없고 자동 생성 옵션이 켜져 있으면 프리펩에서 생성
if (autoSpawnOptitrackClient)
{
SpawnOptitrackClientFromPrefab();
}
else
{
Log("OptiTrack 클라이언트가 없습니다. 자동 생성 옵션이 꺼져 있습니다.");
}
}
/// <summary>
/// OptiTrack 클라이언트 프리펩을 씬에 생성합니다
/// </summary>
public void SpawnOptitrackClientFromPrefab()
{
// 이미 씬에 있는지 확인
var existingClient = FindObjectOfType<OptitrackStreamingClient>();
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<GameObject>(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<OptitrackStreamingClient>();
if (optitrackClient == null)
{
LogError("생성된 프리펩에 OptitrackStreamingClient 컴포넌트가 없습니다!");
Destroy(clientInstance);
}
}
}
#endregion
#region OptiTrack
/// <summary>
/// OptiTrack 마커 표시 토글 (켜기/끄기)
/// </summary>
public void ToggleOptitrackMarkers()
{
if (optitrackClient == null)
{
LogError("OptitrackStreamingClient를 찾을 수 없습니다!");
return;
}
optitrackClient.ToggleDrawMarkers();
Log($"OptiTrack 마커 표시: {optitrackClient.DrawMarkers}");
}
/// <summary>
/// OptiTrack 마커 표시 켜기
/// </summary>
public void ShowOptitrackMarkers()
{
if (optitrackClient == null)
{
LogError("OptitrackStreamingClient를 찾을 수 없습니다!");
return;
}
if (!optitrackClient.DrawMarkers)
{
optitrackClient.ToggleDrawMarkers();
}
Log("OptiTrack 마커 표시 켜짐");
}
/// <summary>
/// OptiTrack 마커 표시 끄기
/// </summary>
public void HideOptitrackMarkers()
{
if (optitrackClient == null)
{
LogError("OptitrackStreamingClient를 찾을 수 없습니다!");
return;
}
if (optitrackClient.DrawMarkers)
{
optitrackClient.ToggleDrawMarkers();
}
Log("OptiTrack 마커 표시 꺼짐");
}
#endregion
#region OptiTrack
/// <summary>
/// OptiTrack 서버에 재접속 시도
/// </summary>
public void ReconnectOptitrack()
{
if (optitrackClient == null)
{
LogError("OptitrackStreamingClient를 찾을 수 없습니다!");
return;
}
Log("OptiTrack 재접속 시도...");
optitrackClient.Reconnect();
Log("OptiTrack 재접속 명령 전송 완료");
}
/// <summary>
/// OptiTrack 연결 상태 확인
/// </summary>
public bool IsOptitrackConnected()
{
if (optitrackClient == null)
{
return false;
}
return optitrackClient.IsConnected();
}
/// <summary>
/// OptiTrack 연결 상태를 문자열로 반환
/// </summary>
public string GetOptitrackConnectionStatus()
{
if (optitrackClient == null)
{
return "OptiTrack 클라이언트 없음";
}
return optitrackClient.GetConnectionStatus();
}
#endregion
#region Facial Motion Capture
/// <summary>
/// 씬에서 모든 Facial Motion 클라이언트를 다시 찾습니다
/// </summary>
public void RefreshFacialMotionClients()
{
var allClients = FindObjectsOfType<StreamingleFacialReceiver>();
facialMotionClients = allClients.ToList();
Log($"Facial Motion 클라이언트 {facialMotionClients.Count}개 발견");
}
/// <summary>
/// 모든 Facial Motion 클라이언트 재접속
/// </summary>
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
/// <summary>
/// 일반 스크린샷 촬영
/// </summary>
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}");
}
}
/// <summary>
/// 알파 채널 포함 스크린샷 촬영
/// NiloToon Prepass 버퍼의 G 채널을 알파로 사용
/// </summary>
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}");
}
}
/// <summary>
/// 스크린샷 저장 폴더 열기
/// </summary>
public void OpenScreenshotFolder()
{
if (Directory.Exists(screenshotSavePath))
{
System.Diagnostics.Process.Start(screenshotSavePath);
Log($"저장 폴더 열기: {screenshotSavePath}");
}
else
{
LogError($"저장 폴더가 존재하지 않습니다: {screenshotSavePath}");
}
}
/// <summary>
/// 파일명 생성
/// </summary>
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)
/// <summary>
/// 모션 녹화 시작 (EasyMotion Recorder + OptiTrack Motive)
/// </summary>
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("녹화를 시작할 수 있는 시스템이 없습니다!");
}
}
/// <summary>
/// 모션 녹화 중지 (EasyMotion Recorder + OptiTrack Motive)
/// </summary>
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("녹화를 중지할 수 있는 시스템이 없습니다!");
}
}
/// <summary>
/// 모션 녹화 토글 (시작/중지)
/// </summary>
public void ToggleMotionRecording()
{
if (isRecording)
{
StopMotionRecording();
}
else
{
StartMotionRecording();
}
}
/// <summary>
/// 현재 녹화 중인지 여부 반환
/// </summary>
public bool IsRecording()
{
return isRecording;
}
#endregion
#region Head
/// <summary>
/// 씬의 모든 아바타 Head 본을 찾아 SphereCollider를 생성합니다.
/// DOF 타겟, 레이캐스트 등 여러 시스템에서 공통으로 활용됩니다.
/// </summary>
public void RefreshAvatarHeadColliders()
{
avatarHeadTargets.Clear();
var retargetingScripts = FindObjectsByType<CustomRetargetingScript>(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<SphereCollider>(out _))
{
var collider = headBone.gameObject.AddComponent<SphereCollider>();
collider.radius = headColliderRadius;
collider.isTrigger = true;
Log($"Head 콜라이더 생성: {headBone.name} ({script.targetAnimator.gameObject.name})");
}
avatarHeadTargets.Add(headBone);
}
Log($"총 {avatarHeadTargets.Count}개의 아바타 Head 타겟 등록 완료");
}
/// <summary>
/// 등록된 아바타 Head 타겟 목록 반환 (다른 시스템에서 활용)
/// </summary>
public List<Transform> GetAvatarHeadTargets()
{
return avatarHeadTargets;
}
#endregion
#region MagicaCloth
#if MAGICACLOTH2
/// <summary>
/// 씬의 모든 MagicaCloth 시뮬레이션을 리셋합니다
/// </summary>
/// <param name="keepPose">true면 현재 포즈를 유지하면서 리셋</param>
public void RefreshAllMagicaCloth(bool keepPose = false)
{
var allMagicaCloths = FindObjectsByType<MagicaCloth>(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}개)");
}
/// <summary>
/// 씬의 모든 MagicaCloth 시뮬레이션을 완전히 초기 상태로 리셋합니다
/// </summary>
public void ResetAllMagicaCloth()
{
RefreshAllMagicaCloth(false);
}
/// <summary>
/// 씬의 모든 MagicaCloth 시뮬레이션을 현재 포즈를 유지하면서 리셋합니다
/// </summary>
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
/// <summary>
/// 리타게팅 웹 리모컨 초기화
/// </summary>
private void InitializeRetargetingRemote()
{
// 이미 할당되어 있으면 사용
if (retargetingRemote == null)
{
// 씬에서 찾기
retargetingRemote = FindObjectOfType<RetargetingRemoteController>();
}
// 없으면 자동 생성
if (retargetingRemote == null && autoStartRetargetingRemote)
{
GameObject remoteObj = new GameObject("RetargetingRemoteController");
retargetingRemote = remoteObj.AddComponent<RetargetingRemoteController>();
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}");
}
}
}
/// <summary>
/// 씬의 모든 CustomRetargetingScript를 자동으로 등록합니다
/// </summary>
public void AutoRegisterRetargetingCharacters()
{
if (retargetingRemote == null)
{
LogError("리타게팅 웹 리모컨 컨트롤러가 없습니다!");
return;
}
var characters = FindObjectsByType<CustomRetargetingScript>(FindObjectsSortMode.None);
foreach (var character in characters)
{
retargetingRemote.RegisterCharacter(character);
}
Log($"리타게팅 캐릭터 {characters.Length}개 등록됨");
}
/// <summary>
/// 리타게팅 웹 리모컨 시작
/// </summary>
public void StartRetargetingRemote()
{
if (retargetingRemote == null)
{
InitializeRetargetingRemote();
}
if (retargetingRemote != null && !retargetingRemote.IsRunning)
{
retargetingRemote.StartServer();
Log($"리타게팅 웹 리모컨 시작됨 - http://localhost:{retargetingHttpPort}");
}
}
/// <summary>
/// 리타게팅 웹 리모컨 중지
/// </summary>
public void StopRetargetingRemote()
{
if (retargetingRemote != null && retargetingRemote.IsRunning)
{
retargetingRemote.StopServer();
Log("리타게팅 웹 리모컨 중지됨");
}
}
/// <summary>
/// 리타게팅 웹 리모컨 토글 (시작/중지)
/// </summary>
public void ToggleRetargetingRemote()
{
if (retargetingRemote == null || !retargetingRemote.IsRunning)
{
StartRetargetingRemote();
}
else
{
StopRetargetingRemote();
}
}
/// <summary>
/// 리타게팅 웹 리모컨 실행 중인지 여부 반환
/// </summary>
public bool IsRetargetingRemoteRunning()
{
return retargetingRemote != null && retargetingRemote.IsRunning;
}
/// <summary>
/// 리타게팅 웹 리모컨 URL 반환
/// </summary>
public string GetRetargetingRemoteUrl()
{
if (retargetingRemote != null && retargetingRemote.IsRunning)
{
return $"http://localhost:{retargetingHttpPort}";
}
return "";
}
#endregion
/// <summary>
/// 명령어 실행 - WebSocket에서 받은 명령을 처리
/// </summary>
public void ExecuteCommand(string command, Dictionary<string, object> 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}");
}
}