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}");
}
}