using System; using System.Collections.Generic; using System.Reflection; using UnityEngine; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace KindRetargeting.Remote { /// /// 리타게팅 원격 제어 컨트롤러 /// HTTP 서버와 WebSocket 서버를 관리하고 메시지를 처리합니다. /// public class RetargetingRemoteController : MonoBehaviour { [Header("서버 설정")] [SerializeField] private int httpPort = 8080; [SerializeField] private int wsPort = 8081; [SerializeField] private bool autoStart = true; [Header("캐릭터 등록")] [SerializeField] private List registeredCharacters = new List(); private RetargetingHTTPServer httpServer; private RetargetingWebSocketServer wsServer; private Queue mainThreadActions = new Queue(); public bool IsRunning => httpServer?.IsRunning == true && wsServer?.IsRunning == true; public int HttpPort { get => httpPort; set => httpPort = value; } public int WsPort { get => wsPort; set => wsPort = value; } public bool AutoStart { get => autoStart; set => autoStart = value; } // 손 포즈 프리셋 목록 private static readonly string[] handPosePresets = new string[] { "가위", "바위", "보", "브이", "검지", "초기화" }; private void Start() { if (autoStart) { StartServer(); } } private void OnDestroy() { StopServer(); } private void Update() { // 메인 스레드 액션 처리 lock (mainThreadActions) { while (mainThreadActions.Count > 0) { var action = mainThreadActions.Dequeue(); try { action?.Invoke(); } catch (Exception ex) { Debug.LogError($"[RetargetingRemote] 메인 스레드 액션 오류: {ex.Message}"); } } } } public void StartServer() { if (IsRunning) return; httpServer = new RetargetingHTTPServer(httpPort, wsPort); wsServer = new RetargetingWebSocketServer(wsPort); wsServer.OnMessageReceived += OnWebSocketMessage; httpServer.Start(); wsServer.Start(); Debug.Log($"[RetargetingRemote] 서버 시작됨 - HTTP: {httpPort}, WS: {wsPort}"); } public void StopServer() { wsServer?.Stop(); httpServer?.Stop(); if (wsServer != null) { wsServer.OnMessageReceived -= OnWebSocketMessage; } Debug.Log("[RetargetingRemote] 서버 중지됨"); } private void OnWebSocketMessage(string message) { EnqueueMainThread(() => ProcessMessage(message)); } private void EnqueueMainThread(Action action) { lock (mainThreadActions) { mainThreadActions.Enqueue(action); } } private void ProcessMessage(string message) { try { var json = JObject.Parse(message); string action = json["action"]?.ToString(); switch (action) { case "refresh": SendCharacterList(); SendHandPosePresets(); break; case "getCharacterData": { string charId = json["characterId"]?.ToString(); SendCharacterData(charId); } break; case "updateValueRealtime": { string charId = json["characterId"]?.ToString(); string property = json["property"]?.ToString(); float value = json["value"]?.Value() ?? 0f; UpdateValue(charId, property, value); } break; case "setHandPosePreset": { string charId = json["characterId"]?.ToString(); string property = json["property"]?.ToString(); string presetName = json["stringValue"]?.ToString(); ApplyHandPosePreset(charId, property, presetName); } break; case "calibrateIPose": { string charId = json["characterId"]?.ToString(); CalibrateIPose(charId); } break; case "resetCalibration": { string charId = json["characterId"]?.ToString(); ResetCalibration(charId); } break; case "calibrateMingleOpen": { string charId = json["characterId"]?.ToString(); CalibrateMingleOpen(charId); } break; case "calibrateMingleClose": { string charId = json["characterId"]?.ToString(); CalibrateMingleClose(charId); } break; case "calibrateHeadForward": { string charId = json["characterId"]?.ToString(); CalibrateHeadForward(charId); } break; default: Debug.LogWarning($"[RetargetingRemote] 알 수 없는 액션: {action}"); break; } } catch (Exception ex) { Debug.LogError($"[RetargetingRemote] 메시지 처리 오류: {ex.Message}"); } } private void SendCharacterList() { var charIds = new List(); foreach (var script in registeredCharacters) { if (script != null) { charIds.Add(script.gameObject.name); } } var response = new { type = "characterList", characters = charIds }; wsServer?.Broadcast(JsonConvert.SerializeObject(response)); } private void SendHandPosePresets() { var response = new { type = "handPosePresets", presets = handPosePresets }; wsServer?.Broadcast(JsonConvert.SerializeObject(response)); } private void SendCharacterData(string characterId) { var script = FindCharacter(characterId); if (script == null) { SendStatus(false, $"캐릭터를 찾을 수 없습니다: {characterId}"); return; } var limbWeight = script.GetComponent(); var handPose = script.GetComponent(); var data = new Dictionary { // 힙 위치 보정 (로컬) { "hipsVertical", GetPrivateField(script, "hipsOffsetY") }, { "hipsForward", GetPrivateField(script, "hipsOffsetZ") }, { "hipsHorizontal", GetPrivateField(script, "hipsOffsetX") }, // 무릎 위치 조정 { "kneeFrontBackWeight", GetPrivateField(script, "kneeFrontBackWeight") }, { "kneeInOutWeight", GetPrivateField(script, "kneeInOutWeight") }, // 발 IK 위치 조정 { "feetForwardBackward", GetPrivateField(script, "footFrontBackOffset") }, { "feetNarrow", GetPrivateField(script, "footInOutOffset") }, // 바닥 높이 { "floorHeight", script.floorHeight }, // 아바타 크기 { "avatarScale", GetPrivateField(script, "avatarScale") }, // 머리 크기 { "headScale", script.GetHeadScale() }, // 머리 회전 오프셋 { "headRotationOffsetX", script.headRotationOffsetX }, { "headRotationOffsetY", script.headRotationOffsetY }, { "headRotationOffsetZ", script.headRotationOffsetZ }, // 손가락 복제 모드 { "fingerCopyMode", (int)GetPrivateField(script, "fingerCopyMode") }, // 모션 설정 { "useMotionFilter", GetPrivateField(script, "useMotionFilter") }, { "filterBufferSize", GetPrivateField(script, "filterBufferSize") }, { "useBodyRoughMotion", GetPrivateField(script, "useBodyRoughMotion") }, { "bodyRoughness", GetPrivateField(script, "bodyRoughness") }, // 캘리브레이션 상태 { "hasCalibrationData", script.HasCachedSettings() } }; // LimbWeightController 데이터 if (limbWeight != null) { data["limbMinDistance"] = limbWeight.minDistance; data["limbMaxDistance"] = limbWeight.maxDistance; data["weightSmoothSpeed"] = limbWeight.weightSmoothSpeed; data["hipsMinDistance"] = limbWeight.hipsMinDistance; data["hipsMaxDistance"] = limbWeight.hipsMaxDistance; data["groundHipsMinHeight"] = limbWeight.groundHipsMinHeight; data["groundHipsMaxHeight"] = limbWeight.groundHipsMaxHeight; data["footHeightMinThreshold"] = limbWeight.footHeightMinThreshold; data["footHeightMaxThreshold"] = limbWeight.footHeightMaxThreshold; data["chairSeatHeightOffset"] = limbWeight.chairSeatHeightOffset; } // FingerShapedController 데이터 if (handPose != null) { data["handPoseEnabled"] = handPose.enabled; data["leftHandEnabled"] = handPose.leftHandEnabled; data["rightHandEnabled"] = handPose.rightHandEnabled; } else { data["handPoseEnabled"] = false; data["leftHandEnabled"] = false; data["rightHandEnabled"] = false; } var response = new { type = "characterData", characterId = characterId, data = data }; wsServer?.Broadcast(JsonConvert.SerializeObject(response)); } private void UpdateValue(string characterId, string property, float value) { var script = FindCharacter(characterId); if (script == null) return; var limbWeight = script.GetComponent(); var handPose = script.GetComponent(); switch (property) { // 힙 위치 보정 case "hipsVertical": SetPrivateField(script, "hipsOffsetY", value); break; case "hipsForward": SetPrivateField(script, "hipsOffsetZ", value); break; case "hipsHorizontal": SetPrivateField(script, "hipsOffsetX", value); break; // 무릎 위치 조정 case "kneeFrontBackWeight": SetPrivateField(script, "kneeFrontBackWeight", value); break; case "kneeInOutWeight": SetPrivateField(script, "kneeInOutWeight", value); break; // 발 IK 위치 조정 case "feetForwardBackward": SetPrivateField(script, "footFrontBackOffset", value); break; case "feetNarrow": SetPrivateField(script, "footInOutOffset", value); break; // 바닥 높이 case "floorHeight": script.floorHeight = value; break; // 아바타 크기 case "avatarScale": SetPrivateField(script, "avatarScale", value); break; // 머리 크기 case "headScale": script.SetHeadScale(value); break; // 머리 회전 오프셋 case "headRotationOffsetX": script.headRotationOffsetX = value; break; case "headRotationOffsetY": script.headRotationOffsetY = value; break; case "headRotationOffsetZ": script.headRotationOffsetZ = value; break; // 손가락 복제 모드 case "fingerCopyMode": SetPrivateField(script, "fingerCopyMode", (EnumsList.FingerCopyMode)(int)value); break; // 모션 설정 case "useMotionFilter": SetPrivateField(script, "useMotionFilter", value > 0.5f); break; case "filterBufferSize": SetPrivateField(script, "filterBufferSize", (int)value); break; case "useBodyRoughMotion": SetPrivateField(script, "useBodyRoughMotion", value > 0.5f); break; case "bodyRoughness": SetPrivateField(script, "bodyRoughness", value); break; // LimbWeightController 속성 case "limbMinDistance": if (limbWeight != null) limbWeight.minDistance = value; break; case "limbMaxDistance": if (limbWeight != null) limbWeight.maxDistance = value; break; case "weightSmoothSpeed": if (limbWeight != null) limbWeight.weightSmoothSpeed = value; break; case "hipsMinDistance": if (limbWeight != null) limbWeight.hipsMinDistance = value; break; case "hipsMaxDistance": if (limbWeight != null) limbWeight.hipsMaxDistance = value; break; case "groundHipsMinHeight": if (limbWeight != null) limbWeight.groundHipsMinHeight = value; break; case "groundHipsMaxHeight": if (limbWeight != null) limbWeight.groundHipsMaxHeight = value; break; case "footHeightMinThreshold": if (limbWeight != null) limbWeight.footHeightMinThreshold = value; break; case "footHeightMaxThreshold": if (limbWeight != null) limbWeight.footHeightMaxThreshold = value; break; case "chairSeatHeightOffset": if (limbWeight != null) limbWeight.chairSeatHeightOffset = value; break; // FingerShapedController 속성 case "handPoseEnabled": if (handPose != null) handPose.enabled = value > 0.5f; break; case "leftHandEnabled": if (handPose != null) { handPose.leftHandEnabled = value > 0.5f; if (handPose.leftHandEnabled) handPose.enabled = true; } break; case "rightHandEnabled": if (handPose != null) { handPose.rightHandEnabled = value > 0.5f; if (handPose.rightHandEnabled) handPose.enabled = true; } break; default: Debug.LogWarning($"[RetargetingRemote] 알 수 없는 속성: {property}"); break; } } private void ApplyHandPosePreset(string characterId, string property, string presetName) { var script = FindCharacter(characterId); if (script == null) return; var handPose = script.GetComponent(); if (handPose == null) return; // 스크립트 자동 활성화 handPose.enabled = true; // 해당 손 활성화 if (property == "leftHandPreset") handPose.leftHandEnabled = true; else if (property == "rightHandPreset") handPose.rightHandEnabled = true; // 프리셋 적용 float thumb = 0, index = 0, middle = 0, ring = 0, pinky = 0, spread = 0; switch (presetName) { case "가위": thumb = 1f; index = 1f; middle = -1f; ring = -1f; pinky = -1f; spread = 0.3f; break; case "바위": thumb = -1f; index = -1f; middle = -1f; ring = -1f; pinky = -1f; spread = 0f; break; case "보": thumb = 1f; index = 1f; middle = 1f; ring = 1f; pinky = 1f; spread = 1f; break; case "브이": thumb = -1f; index = 1f; middle = 1f; ring = -1f; pinky = -1f; spread = 1f; break; case "검지": thumb = -1f; index = 1f; middle = -1f; ring = -1f; pinky = -1f; spread = 0f; break; case "초기화": thumb = 0.8f; index = 0.8f; middle = 0.8f; ring = 0.8f; pinky = 0.8f; spread = 0.8f; break; } if (property == "leftHandPreset" && handPose.leftHandEnabled) { handPose.leftThumbCurl = thumb; handPose.leftIndexCurl = index; handPose.leftMiddleCurl = middle; handPose.leftRingCurl = ring; handPose.leftPinkyCurl = pinky; handPose.leftSpreadFingers = spread; } else if (property == "rightHandPreset" && handPose.rightHandEnabled) { handPose.rightThumbCurl = thumb; handPose.rightIndexCurl = index; handPose.rightMiddleCurl = middle; handPose.rightRingCurl = ring; handPose.rightPinkyCurl = pinky; handPose.rightSpreadFingers = spread; } SendStatus(true, $"{presetName} 프리셋 적용됨"); } private void CalibrateIPose(string characterId) { var script = FindCharacter(characterId); if (script == null) return; script.I_PoseCalibration(); script.SaveSettings(); // 캘리브레이션 후 설정 저장 SendCharacterData(characterId); SendStatus(true, "I-포즈 캘리브레이션 완료"); } private void ResetCalibration(string characterId) { var script = FindCharacter(characterId); if (script == null) return; script.ResetPoseAndCache(); SendCharacterData(characterId); SendStatus(true, "캘리브레이션 초기화됨"); } private void CalibrateMingleOpen(string characterId) { var script = FindCharacter(characterId); if (script == null) return; script.CalibrateMingleOpen(); script.SaveSettings(); SendCharacterData(characterId); SendStatus(true, "Mingle 펼침 캘리브레이션 완료"); } private void CalibrateMingleClose(string characterId) { var script = FindCharacter(characterId); if (script == null) return; script.CalibrateMingleClose(); script.SaveSettings(); SendCharacterData(characterId); SendStatus(true, "Mingle 모음 캘리브레이션 완료"); } private void CalibrateHeadForward(string characterId) { var script = FindCharacter(characterId); if (script == null) return; script.CalibrateHeadToForward(); script.SaveSettings(); SendCharacterData(characterId); SendStatus(true, "정면 캘리브레이션 완료"); } private CustomRetargetingScript FindCharacter(string characterId) { foreach (var script in registeredCharacters) { if (script != null && script.gameObject.name == characterId) { return script; } } return null; } private void SendStatus(bool success, string message) { var response = new { type = "status", success = success, message = message }; wsServer?.Broadcast(JsonConvert.SerializeObject(response)); } #region Reflection Helpers private T GetPrivateField(object obj, string fieldName) { try { var field = obj.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public); if (field != null) { return (T)field.GetValue(obj); } } catch (Exception) { } return default(T); } private void SetPrivateField(object obj, string fieldName, T value) { try { var field = obj.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public); if (field != null) { field.SetValue(obj, value); } } catch (Exception ex) { Debug.LogError($"[RetargetingRemote] 필드 설정 오류 ({fieldName}): {ex.Message}"); } } #endregion #region Public API /// /// 캐릭터 등록 /// public void RegisterCharacter(CustomRetargetingScript character) { if (!registeredCharacters.Contains(character)) { registeredCharacters.Add(character); } } /// /// 캐릭터 등록 해제 /// public void UnregisterCharacter(CustomRetargetingScript character) { registeredCharacters.Remove(character); } #endregion } }