using UnityEngine; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Collections.Generic; using System; using System.Globalization; public class StreamingleFacialReceiver : MonoBehaviour { public bool mirrorMode = true; // 좌우 반전 모드 설정 private bool StartFlag = true; //object references public SkinnedMeshRenderer[] faceMeshRenderers; private UdpClient udp; private Thread thread; private volatile bool isThreadRunning = false; // 스레드 실행 상태 플래그 private string messageString = ""; private string lastProcessedMessage = ""; // 이전 메시지 저장용 // 포트 핫스왑 시스템 [Header("Port Hot-Swap")] [Tooltip("사용 가능한 아이폰 포트 목록")] public int[] availablePorts = new int[] { 40001, 40002, 40003, 40004, 40005 }; [Tooltip("현재 활성화된 포트 인덱스 (0~4)")] [Range(0, 4)] public int activePortIndex = 0; public int LOCAL_PORT => availablePorts != null && activePortIndex < availablePorts.Length ? availablePorts[activePortIndex] : 49983; // 데이터 필터링 설정 [Header("Data Filtering")] [Tooltip("데이터 스무딩/필터링 활성화")] public bool enableFiltering = true; [Tooltip("스무딩 강도 (0=필터없음, 1=최대 스무딩). 프레임레이트 독립적으로 동작")] [Range(0f, 0.95f)] public float smoothingFactor = 0.1f; [Tooltip("프레임 간 최대 허용 변화량 (BlendShape, 0~100 스케일)")] [Range(1f, 100f)] public float maxBlendShapeDelta = 30f; [Tooltip("프레임 간 최대 허용 회전 변화량 (도)")] [Range(1f, 90f)] public float maxRotationDelta = 25f; [Tooltip("눈 깜빡임 등 빠른 BlendShape의 임계값 배수")] [Range(1f, 3f)] public float fastBlendShapeMultiplier = 2.0f; [Tooltip("스파이크 판정 전 허용할 연속 프레임 수 (연속이면 실제 움직임으로 판단)")] [Range(1, 5)] public int spikeToleranceFrames = 2; // 필터링용 이전 값 저장 private Dictionary prevBlendShapeValues = new Dictionary(); // 연속 스파이크 추적 (같은 방향으로 연속이면 실제 움직임) private Dictionary blendShapeSpikeCount = new Dictionary(); private Dictionary blendShapeSpikeDirection = new Dictionary(); // 빠르게 변하는 BlendShape 목록 (눈 깜빡임, 입 등) private static readonly HashSet FastBlendShapes = new HashSet(StringComparer.OrdinalIgnoreCase) { "eyeblinkleft", "eyeblinkright", "eyesquintleft", "eyesquintright", "eyewideleft", "eyewideright", "jawopen", "mouthclose", "mouthfunnel", "mouthpucker", "mouthsmileright", "mouthsmileleft", "mouthfrownright", "mouthfrownleft", }; // 페이셜 개별 강도 조절 [Header("Facial Intensity")] [Tooltip("전체 BlendShape 강도 배율 (1.0 = 원본)")] [Range(0f, 3f)] public float globalIntensity = 1.0f; [Tooltip("개별 BlendShape 강도 오버라이드 (이름, 배율). 여기에 없는 항목은 globalIntensity 적용")] public List blendShapeIntensityOverrides = new List(); [System.Serializable] public class BlendShapeIntensityOverride { [Tooltip("ARKit BlendShape 이름 (예: EyeBlinkLeft, JawOpen, MouthSmileLeft 등)")] public string blendShapeName; [Range(0f, 3f)] public float intensity = 1.0f; } // 런타임 빠른 조회용 Dictionary private Dictionary intensityOverrideMap = new Dictionary(StringComparer.OrdinalIgnoreCase); private bool intensityMapDirty = true; // 프레임레이트 독립 스무딩을 위한 기준 FPS private const float ReferenceFPS = 60f; // 성능 최적화를 위한 캐시 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[] { '-' }; // 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(); //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}"); } } void OnValidate() { intensityMapDirty = true; } void RebuildIntensityOverrideMap() { intensityOverrideMap.Clear(); foreach (var entry in blendShapeIntensityOverrides) { if (!string.IsNullOrEmpty(entry.blendShapeName)) { intensityOverrideMap[entry.blendShapeName] = entry.intensity; } } intensityMapDirty = false; } float GetBlendShapeIntensity(string normalizedName) { if (intensityOverrideMap.TryGetValue(normalizedName, out float val)) return val * globalIntensity; return globalIntensity; } // Update is called once per frame void Update() { // 강도 오버라이드 맵 갱신 (인스펙터 변경 반영) if (intensityMapDirty) { RebuildIntensityOverrideMap(); } // 메시지가 변경되었을 때만 처리 (성능 최적화) 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(); // 강도 배율 적용 weight *= GetBlendShapeIntensity(normalizedName); weight = Mathf.Clamp(weight, 0f, 100f); // 필터링 적용 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); } } } } 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() { // 안전한 스레드 종료 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 SwitchToPort(int portIndex) { if (availablePorts == null || portIndex < 0 || portIndex >= availablePorts.Length) { Debug.LogError($"[iFacialMocap] 잘못된 포트 인덱스: {portIndex}"); return; } activePortIndex = portIndex; Debug.Log($"[iFacialMocap] 포트 전환: {availablePorts[portIndex]}"); Reconnect(); } /// /// 페이셜 모션 캡처 재접속 /// 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}"); } } /// /// 프레임레이트 독립적 EMA 계수 계산 /// float GetFrameIndependentSmoothing() { float dt = Time.deltaTime; if (dt <= 0f) return smoothingFactor; // 기준 60fps에서의 smoothingFactor를 현재 dt에 맞게 보정 return 1f - Mathf.Pow(1f - smoothingFactor, dt * ReferenceFPS); } /// /// BlendShape 값 필터링: 연속 스파이크 판별 + 카테고리별 임계값 + 프레임독립 EMA /// float FilterBlendShapeValue(string name, float rawValue) { if (prevBlendShapeValues.TryGetValue(name, out float prevValue)) { float diff = rawValue - prevValue; float delta = Mathf.Abs(diff); // 빠르게 변하는 BlendShape는 임계값을 높여줌 float threshold = maxBlendShapeDelta; if (FastBlendShapes.Contains(name)) { threshold *= fastBlendShapeMultiplier; } // 스파이크 감지 if (delta > threshold) { // 연속 스파이크 추적: 같은 방향이면 카운트 증가 float prevDir = 0f; blendShapeSpikeDirection.TryGetValue(name, out prevDir); bool sameDirection = (diff > 0 && prevDir > 0) || (diff < 0 && prevDir < 0); int count = 0; blendShapeSpikeCount.TryGetValue(name, out count); if (sameDirection) { count++; } else { count = 1; } blendShapeSpikeCount[name] = count; blendShapeSpikeDirection[name] = diff; // 연속 프레임 이상 같은 방향이면 실제 움직임으로 판단 → 통과 if (count >= spikeToleranceFrames) { blendShapeSpikeCount[name] = 0; prevBlendShapeValues[name] = rawValue; return rawValue; } // 단발성 스파이크 → 허용량만큼만 이동 float clamped = prevValue + Mathf.Clamp(diff, -threshold, threshold); prevBlendShapeValues[name] = clamped; return clamped; } // 정상 범위 → 스파이크 카운터 리셋 blendShapeSpikeCount[name] = 0; // 프레임레이트 독립 EMA 스무딩 float alpha = GetFrameIndependentSmoothing(); float smoothed = Mathf.Lerp(rawValue, prevValue, alpha); prevBlendShapeValues[name] = smoothed; return smoothed; } // 첫 프레임 prevBlendShapeValues[name] = rawValue; return rawValue; } } public static class StreamingleFacialReceiverExtensions { 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); } } }