diff --git a/Assets/External/Ifacialmocap/UnityRecieve_FACEMOTION3D_and_iFacialMocap.cs b/Assets/External/Ifacialmocap/UnityRecieve_FACEMOTION3D_and_iFacialMocap.cs index 006e6e7f..2a21cfd2 100644 --- a/Assets/External/Ifacialmocap/UnityRecieve_FACEMOTION3D_and_iFacialMocap.cs +++ b/Assets/External/Ifacialmocap/UnityRecieve_FACEMOTION3D_and_iFacialMocap.cs @@ -26,9 +26,51 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour private UdpClient udp; private Thread thread; + private volatile bool isThreadRunning = false; // 스레드 실행 상태 플래그 private string messageString = ""; + private string lastProcessedMessage = ""; // 이전 메시지 저장용 public int LOCAL_PORT = 49983; + // 성능 최적화를 위한 캐시 + 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() { @@ -38,6 +80,9 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour FindGameObjectsInsideUnitySettings(); + // BlendShape 인덱스 캐싱 초기화 + InitializeBlendShapeCache(); + //Send to iOS if (gameStartWithConnect == true) { @@ -54,6 +99,49 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour 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 @@ -61,7 +149,9 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour 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) @@ -121,37 +211,41 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour // Update is called once per frame void Update() { - try + // 메시지가 변경되었을 때만 처리 (성능 최적화) + if (!string.IsNullOrEmpty(messageString) && messageString != lastProcessedMessage) { - SetAnimation_inside_Unity_settings(); + try + { + SetAnimation_inside_Unity_settings(); + lastProcessedMessage = messageString; + } + catch (Exception e) + { + Debug.LogWarning($"[iFacialMocap] Animation 처리 중 오류: {e.Message}"); + } } - catch - { } } //BlendShapeの設定 - //set blendshapes + //set blendshapes (캐시 사용으로 최적화) void SetBlendShapeWeightFromStrArray(string[] strArray2) { - string mappedShapeName = strArray2[0].Replace("_R", "Right").Replace("_L", "Left"); + if (blendShapeCache == null) return; + + string shapeName = strArray2[0]; float weight = float.Parse(strArray2[1], CultureInfo.InvariantCulture); - if (faceMeshRenderers != null) + // 정규화된 이름으로 캐시 검색 + string normalizedName = NormalizeBlendShapeName(shapeName).ToLowerInvariant(); + + if (blendShapeCache.TryGetValue(normalizedName, out List mappings)) { - foreach (var meshRenderer in faceMeshRenderers) + // 캐시에서 찾은 모든 매핑에 대해 weight 설정 + foreach (var mapping in mappings) { - if (meshRenderer != null && meshRenderer.sharedMesh != null) + if (mapping.renderer != null) { - // 대소문자 구분 없이 블렌드쉐입 찾기 - for (int i = 0; i < meshRenderer.sharedMesh.blendShapeCount; i++) - { - string blendShapeName = meshRenderer.sharedMesh.GetBlendShapeName(i); - if (string.Equals(blendShapeName, mappedShapeName, StringComparison.OrdinalIgnoreCase)) - { - meshRenderer.SetBlendShapeWeight(i, weight); - break; - } - } + mapping.renderer.SetBlendShapeWeight(mapping.index, weight); } } } @@ -176,26 +270,29 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour } //BlendShapeとボーンの回転の設定 - //set blendshapes & bone rotation + //set blendshapes & bone rotation (최적화 버전) void SetAnimation_inside_Unity_settings() { try { - string[] strArray1 = messageString.Split('='); + string[] strArray1 = messageString.Split(splitEquals, StringSplitOptions.RemoveEmptyEntries); if (strArray1.Length >= 2) { //blendShapes - foreach (string message in strArray1[0].Split('|')) + string[] blendShapeMessages = strArray1[0].Split(splitPipe, StringSplitOptions.RemoveEmptyEntries); + foreach (string message in blendShapeMessages) { - string[] strArray2 = new string[3]; + if (string.IsNullOrEmpty(message)) continue; + + string[] strArray2; if (message.Contains("&")) { - strArray2 = message.Split('&'); + strArray2 = message.Split(splitAnd, StringSplitOptions.RemoveEmptyEntries); } else { - strArray2 = message.Split('-'); + strArray2 = message.Split(splitDash, StringSplitOptions.RemoveEmptyEntries); } if (strArray2.Length == 2) @@ -206,27 +303,12 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour { string originalShapeName = strArray2[0]; string shapeNameLower = originalShapeName.ToLowerInvariant(); - // eye 블렌드쉐입 매핑 (key는 소문자, value는 Unity 카멜케이스) - 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"} - }; + + // 정적 Dictionary 사용 string mirroredName = originalShapeName; - if (eyeMirrorMap.ContainsKey(shapeNameLower)) + if (EyeMirrorMap.TryGetValue(shapeNameLower, out string mappedName)) { - mirroredName = eyeMirrorMap[shapeNameLower]; + mirroredName = mappedName; } else if (originalShapeName.Contains("Right")) { @@ -242,72 +324,65 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour } } - foreach (string message in strArray1[1].Split('|')) + // 본 회전 처리 + string[] boneMessages = strArray1[1].Split(splitPipe, StringSplitOptions.RemoveEmptyEntries); + foreach (string message in boneMessages) { - string[] strArray2 = message.Split('#'); + if (string.IsNullOrEmpty(message)) continue; + + string[] strArray2 = message.Split(splitHash, StringSplitOptions.RemoveEmptyEntries); if (strArray2.Length == 2) { - string[] commaList = strArray2[1].Split(','); - if (strArray2[0] == "head" && headBone != null) + 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) { - 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축 회전 반전 - } - - headBone.localRotation = Quaternion.Euler(x, y, -z); - - if (headPositionObject != null) - { - 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) + y = -y; // Y축 회전 반전 + } + + switch (strArray2[0]) + { + case "head" when headBone != null: + headBone.localRotation = Quaternion.Euler(x, y, -z); + + if (headPositionObject != null && commaList.Length >= 6) { - posX = -posX; // X축 위치 반전 + 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; // X축 위치 반전 + } + + headPositionObject.localPosition = new Vector3(posX, posY, posZ); } - - headPositionObject.localPosition = new Vector3(posX, posY, posZ); - } - } - else if (strArray2[0] == "rightEye" && rightEyeBone != null) - { - 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축 회전 반전 - } - - rightEyeBone.localRotation = Quaternion.Euler(x, y, z); - } - else if (strArray2[0] == "leftEye" && leftEyeBone != null) - { - 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축 회전 반전 - } - - leftEyeBone.localRotation = Quaternion.Euler(x, y, z); + break; + + case "rightEye" when rightEyeBone != null: + rightEyeBone.localRotation = Quaternion.Euler(x, y, z); + break; + + case "leftEye" when leftEyeBone != null: + leftEyeBone.localRotation = Quaternion.Euler(x, y, z); + break; } } } } } - catch + catch (Exception e) { + Debug.LogWarning($"[iFacialMocap] Animation 설정 중 오류: {e.Message}"); } } @@ -318,20 +393,24 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour void ThreadMethod() { - //Process once every 5ms - long next = DateTime.Now.Ticks + 50000; - long now; - - while (true) + while (isThreadRunning) { try { IPEndPoint remoteEP = null; byte[] data = udp.Receive(ref remoteEP); - messageString = Encoding.ASCII.GetString(data); + + // 데이터를 받았을 때만 업데이트 + 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}"); @@ -339,15 +418,15 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour } catch (Exception e) { + // 스레드 종료 중이면 로그 생략 + if (!isThreadRunning) break; + Debug.LogError($"[iFacialMocap] 예상치 못한 오류: {e.Message}"); } - do - { - now = DateTime.Now.Ticks; - } - while (now < next); - next += 50000; + // CPU를 양보하는 Sleep 사용 (5ms 대기) + // Busy waiting 대신 Thread.Sleep으로 CPU 사용률 감소 + Thread.Sleep(5); } } @@ -368,8 +447,9 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour { OnApplicationQuit(); } - catch + catch (Exception e) { + Debug.LogWarning($"[iFacialMocap] OnDisable 중 오류: {e.Message}"); } } @@ -389,8 +469,28 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour { StopStreaming_iOS_App(); } - udp.Dispose(); - thread.Abort(); + + // 안전한 스레드 종료 + isThreadRunning = false; + + // UDP 종료 + if (udp != null) + { + udp.Close(); + udp.Dispose(); + } + + // 스레드가 종료될 때까지 대기 (최대 100ms) + if (thread != null && thread.IsAlive) + { + thread.Join(100); + + // 그래도 종료되지 않으면 강제 종료 + if (thread.IsAlive) + { + thread.Abort(); + } + } } private bool HasBlendShapes(SkinnedMeshRenderer skin)