using UnityEngine; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Collections.Generic; using System; using System.Collections; using System.Globalization; public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour { // broadcast address public bool gameStartWithConnect = true; public string iOS_IPAddress = "255.255.255.255"; public bool mirrorMode = false; // 좌우 반전 모드 설정 private UdpClient client; private bool StartFlag = true; //object references public SkinnedMeshRenderer[] faceMeshRenderers; public Transform headBone; public Transform rightEyeBone; public Transform leftEyeBone; public Transform headPositionObject; private UdpClient udp; private Thread thread; private volatile bool isThreadRunning = false; // 스레드 실행 상태 플래그 private string messageString = ""; private string lastProcessedMessage = ""; // 이전 메시지 저장용 public int LOCAL_PORT = 49983; // 데이터 필터링 설정 [Header("Data Filtering")] [Tooltip("데이터 스무딩/필터링 활성화")] public bool enableFiltering = true; [Tooltip("스무딩 강도 (0=필터없음, 1=최대 스무딩)")] [Range(0f, 0.95f)] public float smoothingFactor = 0.5f; [Tooltip("프레임 간 최대 허용 변화량 (BlendShape, 0~100 스케일)")] [Range(1f, 100f)] public float maxBlendShapeDelta = 30f; [Tooltip("프레임 간 최대 허용 회전 변화량 (도)")] [Range(1f, 90f)] public float maxRotationDelta = 25f; // 필터링용 이전 값 저장 private Dictionary prevBlendShapeValues = new Dictionary(); private Dictionary prevBoneRotations = new Dictionary(); private Vector3 prevHeadPosition = Vector3.zero; private bool hasFirstFrame = false; // 성능 최적화를 위한 캐시 private Dictionary> blendShapeCache; private readonly char[] splitEquals = new char[] { '=' }; private readonly char[] splitPipe = new char[] { '|' }; private readonly char[] splitAnd = new char[] { '&' }; private readonly char[] splitDash = new char[] { '-' }; private readonly char[] splitHash = new char[] { '#' }; private readonly char[] splitComma = new char[] { ',' }; // Mirror mode용 정적 매핑 테이블 private static readonly Dictionary EyeMirrorMap = new Dictionary() { {"eyelookupleft", "EyeLookUpRight"}, {"eyelookupright", "EyeLookUpLeft"}, {"eyelookdownleft", "EyeLookDownRight"}, {"eyelookdownright", "EyeLookDownLeft"}, {"eyelookinleft", "EyeLookInRight"}, {"eyelookinright", "EyeLookInLeft"}, {"eyelookoutleft", "EyeLookOutRight"}, {"eyelookoutright", "EyeLookOutLeft"}, {"eyewideleft", "EyeWideRight"}, {"eyewideright", "EyeWideLeft"}, {"eyesquintleft", "EyeSquintRight"}, {"eyesquintright", "EyeSquintLeft"}, {"eyeblinkleft", "EyeBlinkRight"}, {"eyeblinkright", "EyeBlinkLeft"} }; // BlendShape 매핑 정보를 저장하는 구조체 private struct BlendShapeMapping { public SkinnedMeshRenderer renderer; public int index; public BlendShapeMapping(SkinnedMeshRenderer r, int i) { renderer = r; index = i; } } // Start is called void StartFunction() { if (StartFlag == true) { StartFlag = false; FindGameObjectsInsideUnitySettings(); // BlendShape 인덱스 캐싱 초기화 InitializeBlendShapeCache(); //Send to iOS if (gameStartWithConnect == true) { Connect_to_iOS_App(); } //Recieve udp from iOS CreateUdpServer(); } } void Start() { StartFunction(); } // BlendShape 인덱스를 미리 캐싱하여 매번 검색하지 않도록 함 void InitializeBlendShapeCache() { blendShapeCache = new Dictionary>(); if (faceMeshRenderers == null) return; foreach (var meshRenderer in faceMeshRenderers) { if (meshRenderer == null || meshRenderer.sharedMesh == null) continue; for (int i = 0; i < meshRenderer.sharedMesh.blendShapeCount; i++) { string shapeName = meshRenderer.sharedMesh.GetBlendShapeName(i); string normalizedName = shapeName.ToLowerInvariant(); // 여러 변형 이름들을 모두 캐싱 AddToCache(normalizedName, meshRenderer, i); // _L, _R 변환 버전도 캐싱 if (shapeName.Contains("_L")) { string leftVariant = shapeName.Replace("_L", "Left"); AddToCache(leftVariant.ToLowerInvariant(), meshRenderer, i); } else if (shapeName.Contains("_R")) { string rightVariant = shapeName.Replace("_R", "Right"); AddToCache(rightVariant.ToLowerInvariant(), meshRenderer, i); } } } } void AddToCache(string key, SkinnedMeshRenderer renderer, int index) { if (!blendShapeCache.ContainsKey(key)) { blendShapeCache[key] = new List(); } blendShapeCache[key].Add(new BlendShapeMapping(renderer, index)); } void CreateUdpServer() { try { udp = new UdpClient(LOCAL_PORT); udp.Client.ReceiveTimeout = 5; isThreadRunning = true; thread = new Thread(new ThreadStart(ThreadMethod)); thread.IsBackground = true; // 백그라운드 스레드로 설정 thread.Start(); } catch (Exception e) { Debug.LogError($"[iFacialMocap] UDP 서버 생성 실패: {e.Message}"); } } IEnumerator WaitProcess(float WaitTime) { yield return new WaitForSeconds(WaitTime); } void Connect_to_iOS_App() { try { //iFacialMocap SendMessage_to_iOSapp("iFacialMocap_sahuasouryya9218sauhuiayeta91555dy3719", 49983); //Facemotion3d SendMessage_to_iOSapp("FACEMOTION3D_OtherStreaming", 49993); } catch (Exception e) { Debug.LogError($"[iFacialMocap] iOS 앱 연결 실패: {e.Message}"); } } void StopStreaming_iOS_App() { SendMessage_to_iOSapp("StopStreaming_FACEMOTION3D", 49993); } //iOSアプリに通信開始のメッセージを送信 //Send a message to the iOS application to start streaming void SendMessage_to_iOSapp(string sendMessage, int send_port) { try { client = new UdpClient(); client.Connect(iOS_IPAddress, send_port); byte[] dgram = Encoding.UTF8.GetBytes(sendMessage); // 메시지 전송 시도 for (int i = 0; i < 5; i++) { client.Send(dgram, dgram.Length); } } catch (Exception e) { Debug.LogError($"[iFacialMocap] 메시지 전송 실패: {e.Message}"); } } // Update is called once per frame void Update() { // 메시지가 변경되었을 때만 처리 (성능 최적화) if (!string.IsNullOrEmpty(messageString) && messageString != lastProcessedMessage) { try { SetAnimation_inside_Unity_settings(); lastProcessedMessage = messageString; } catch (Exception e) { Debug.LogWarning($"[iFacialMocap] Animation 처리 중 오류: {e.Message}"); } } } //BlendShapeの設定 //set blendshapes (캐시 사용으로 최적화) void SetBlendShapeWeightFromStrArray(string[] strArray2) { if (blendShapeCache == null) return; string shapeName = strArray2[0]; float weight = float.Parse(strArray2[1], CultureInfo.InvariantCulture); // 정규화된 이름으로 캐시 검색 string normalizedName = NormalizeBlendShapeName(shapeName).ToLowerInvariant(); // 필터링 적용 if (enableFiltering) { weight = FilterBlendShapeValue(normalizedName, weight); } if (blendShapeCache.TryGetValue(normalizedName, out List mappings)) { foreach (var mapping in mappings) { if (mapping.renderer != null) { mapping.renderer.SetBlendShapeWeight(mapping.index, weight); } } } } // BlendShape 이름 정규화 함수 string NormalizeBlendShapeName(string name) { if (name.EndsWith("_L")) name = name.Substring(0, name.Length - 2) + "Left"; else if (name.EndsWith("_R")) name = name.Substring(0, name.Length - 2) + "Right"; // 카멜케이스화: 언더스코어 기준 분리 후 각 파트 첫 글자 대문자 string[] parts = name.Split('_'); for (int i = 0; i < parts.Length; i++) { if (parts[i].Length > 0) parts[i] = char.ToUpper(parts[i][0]) + parts[i].Substring(1); } return string.Join("", parts); } //BlendShapeとボーンの回転の設定 //set blendshapes & bone rotation (최적화 버전) void SetAnimation_inside_Unity_settings() { try { string[] strArray1 = messageString.Split(splitEquals, StringSplitOptions.RemoveEmptyEntries); if (strArray1.Length >= 2) { //blendShapes string[] blendShapeMessages = strArray1[0].Split(splitPipe, StringSplitOptions.RemoveEmptyEntries); foreach (string message in blendShapeMessages) { if (string.IsNullOrEmpty(message)) continue; string[] strArray2; if (message.Contains("&")) { strArray2 = message.Split(splitAnd, StringSplitOptions.RemoveEmptyEntries); } else { strArray2 = message.Split(splitDash, StringSplitOptions.RemoveEmptyEntries); } if (strArray2.Length == 2) { // 이름 정규화 먼저 적용 strArray2[0] = NormalizeBlendShapeName(strArray2[0]); if (mirrorMode) { string originalShapeName = strArray2[0]; string shapeNameLower = originalShapeName.ToLowerInvariant(); // 정적 Dictionary 사용 string mirroredName = originalShapeName; if (EyeMirrorMap.TryGetValue(shapeNameLower, out string mappedName)) { mirroredName = mappedName; } else if (originalShapeName.Contains("Right")) { mirroredName = originalShapeName.Replace("Right", "Left"); } else if (originalShapeName.Contains("Left")) { mirroredName = originalShapeName.Replace("Left", "Right"); } strArray2[0] = mirroredName; } SetBlendShapeWeightFromStrArray(strArray2); } } // 본 회전 처리 string[] boneMessages = strArray1[1].Split(splitPipe, StringSplitOptions.RemoveEmptyEntries); foreach (string message in boneMessages) { if (string.IsNullOrEmpty(message)) continue; string[] strArray2 = message.Split(splitHash, StringSplitOptions.RemoveEmptyEntries); if (strArray2.Length == 2) { string[] commaList = strArray2[1].Split(splitComma, StringSplitOptions.RemoveEmptyEntries); // 파싱 한 번만 수행 if (commaList.Length < 3) continue; float x = float.Parse(commaList[0], CultureInfo.InvariantCulture); float y = float.Parse(commaList[1], CultureInfo.InvariantCulture); float z = float.Parse(commaList[2], CultureInfo.InvariantCulture); if (mirrorMode) { y = -y; // Y축 회전 반전 } Vector3 rotation = new Vector3(x, y, z); // 본 회전 필터링 적용 if (enableFiltering) { rotation = FilterBoneRotation(strArray2[0], rotation); } switch (strArray2[0]) { case "head" when headBone != null: headBone.localRotation = Quaternion.Euler(rotation.x, rotation.y, -rotation.z); if (headPositionObject != null && commaList.Length >= 6) { float posX = -float.Parse(commaList[3], CultureInfo.InvariantCulture); float posY = float.Parse(commaList[4], CultureInfo.InvariantCulture); float posZ = float.Parse(commaList[5], CultureInfo.InvariantCulture); if (mirrorMode) { posX = -posX; } Vector3 newPos = new Vector3(posX, posY, posZ); if (enableFiltering) { newPos = FilterHeadPosition(newPos); } headPositionObject.localPosition = newPos; } break; case "rightEye" when rightEyeBone != null: rightEyeBone.localRotation = Quaternion.Euler(rotation.x, rotation.y, rotation.z); break; case "leftEye" when leftEyeBone != null: leftEyeBone.localRotation = Quaternion.Euler(rotation.x, rotation.y, rotation.z); break; } } } } } catch (Exception e) { Debug.LogWarning($"[iFacialMocap] Animation 설정 중 오류: {e.Message}"); } } void FindGameObjectsInsideUnitySettings() { // 모든 Transform 참조는 이미 인스펙터에서 할당되므로 추가 초기화가 필요 없음 } void ThreadMethod() { while (isThreadRunning) { try { IPEndPoint remoteEP = null; byte[] data = udp.Receive(ref remoteEP); // 데이터를 받았을 때만 업데이트 if (data != null && data.Length > 0) { messageString = Encoding.ASCII.GetString(data); } } catch (SocketException e) { // 스레드 종료 중이면 로그 생략 if (!isThreadRunning) break; if (e.SocketErrorCode != SocketError.TimedOut) { Debug.LogError($"[iFacialMocap] 데이터 수신 오류: {e.Message}"); } } catch (Exception e) { // 스레드 종료 중이면 로그 생략 if (!isThreadRunning) break; Debug.LogError($"[iFacialMocap] 예상치 못한 오류: {e.Message}"); } // CPU를 양보하는 Sleep 사용 (5ms 대기) // Busy waiting 대신 Thread.Sleep으로 CPU 사용률 감소 Thread.Sleep(5); } } public string GetMessageString() { return messageString; } void OnEnable() { StartFunction(); } void OnDisable() { try { OnApplicationQuit(); } catch (Exception e) { Debug.LogWarning($"[iFacialMocap] OnDisable 중 오류: {e.Message}"); } } void OnApplicationQuit() { if (StartFlag == false) { StartFlag = true; StopUDP(); } } public void StopUDP() { if (gameStartWithConnect == true) { StopStreaming_iOS_App(); } // 안전한 스레드 종료 isThreadRunning = false; // UDP 종료 if (udp != null) { udp.Close(); udp.Dispose(); } // 스레드가 종료될 때까지 대기 (최대 100ms) if (thread != null && thread.IsAlive) { thread.Join(100); // 그래도 종료되지 않으면 강제 종료 if (thread.IsAlive) { thread.Abort(); } } } /// /// 페이셜 모션 캡처 재접속 /// public void Reconnect() { Debug.Log("[iFacialMocap] 재접속 시도 중..."); try { // 기존 연결 종료 StopUDP(); // 잠시 대기 Thread.Sleep(500); // 플래그 리셋 StartFlag = true; // 재시작 StartFunction(); Debug.Log("[iFacialMocap] 재접속 완료"); } catch (Exception e) { Debug.LogError($"[iFacialMocap] 재접속 실패: {e.Message}"); } } /// /// BlendShape 값 필터링: 스파이크 제거 + EMA 스무딩 /// float FilterBlendShapeValue(string name, float rawValue) { if (prevBlendShapeValues.TryGetValue(name, out float prevValue)) { float delta = Mathf.Abs(rawValue - prevValue); // 스파이크 감지: 변화량이 임계값 초과 시 이전 값 유지 if (delta > maxBlendShapeDelta) { return prevValue; } // EMA 스무딩 적용 float smoothed = Mathf.Lerp(rawValue, prevValue, smoothingFactor); prevBlendShapeValues[name] = smoothed; return smoothed; } // 첫 프레임은 그대로 저장 prevBlendShapeValues[name] = rawValue; return rawValue; } /// /// 본 회전 필터링: 스파이크 제거 + EMA 스무딩 /// Vector3 FilterBoneRotation(string boneName, Vector3 rawRotation) { if (prevBoneRotations.TryGetValue(boneName, out Vector3 prevRot)) { float delta = Vector3.Distance(rawRotation, prevRot); // 스파이크 감지 if (delta > maxRotationDelta) { return prevRot; } // EMA 스무딩 Vector3 smoothed = Vector3.Lerp(rawRotation, prevRot, smoothingFactor); prevBoneRotations[boneName] = smoothed; return smoothed; } prevBoneRotations[boneName] = rawRotation; return rawRotation; } /// /// 머리 위치 필터링 /// Vector3 FilterHeadPosition(Vector3 rawPos) { if (hasFirstFrame) { float delta = Vector3.Distance(rawPos, prevHeadPosition); // 위치 스파이크 감지 (0.1 단위 기준) if (delta > maxRotationDelta * 0.01f) { return prevHeadPosition; } Vector3 smoothed = Vector3.Lerp(rawPos, prevHeadPosition, smoothingFactor); prevHeadPosition = smoothed; return smoothed; } hasFirstFrame = true; prevHeadPosition = rawPos; return rawPos; } private bool HasBlendShapes(SkinnedMeshRenderer skin) { if (!skin.sharedMesh) { return false; } if (skin.sharedMesh.blendShapeCount <= 0) { return false; } return true; } } public static class FM3D_and_iFacialMocap_GetAllChildren { public static List GetAll(this GameObject obj) { List allChildren = new List(); allChildren.Add(obj); GetChildren(obj, ref allChildren); return allChildren; } public static void GetChildren(GameObject obj, ref List allChildren) { Transform children = obj.GetComponentInChildren(); if (children.childCount == 0) { return; } foreach (Transform ob in children) { allChildren.Add(ob.gameObject); GetChildren(ob.gameObject, ref allChildren); } } }