From 4e5634536aed84195373b531c8bbf3324e10e1a5 Mon Sep 17 00:00:00 2001 From: user Date: Mon, 9 Feb 2026 17:55:33 +0900 Subject: [PATCH] =?UTF-8?q?Fix=20:=20=ED=8E=98=EC=9D=B4=EC=85=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...acialmocap.meta => StreamingleFacial.meta} | 2 +- Assets/External/StreamingleFacial/Editor.meta | 8 + .../BlendShapeIntensityOverrideDrawer.cs | 136 ++++++++ .../BlendShapeIntensityOverrideDrawer.cs.meta | 2 + .../Editor/StreamingleFacialReceiverEditor.cs | 263 ++++++++++++++++ .../StreamingleFacialReceiverEditor.cs.meta | 2 + .../StreamingleFacialReceiver.cs | 296 +++++------------- .../StreamingleFacialReceiver.cs.meta | 0 8 files changed, 489 insertions(+), 220 deletions(-) rename Assets/External/{Ifacialmocap.meta => StreamingleFacial.meta} (77%) create mode 100644 Assets/External/StreamingleFacial/Editor.meta create mode 100644 Assets/External/StreamingleFacial/Editor/BlendShapeIntensityOverrideDrawer.cs create mode 100644 Assets/External/StreamingleFacial/Editor/BlendShapeIntensityOverrideDrawer.cs.meta create mode 100644 Assets/External/StreamingleFacial/Editor/StreamingleFacialReceiverEditor.cs create mode 100644 Assets/External/StreamingleFacial/Editor/StreamingleFacialReceiverEditor.cs.meta rename Assets/External/{Ifacialmocap => StreamingleFacial}/StreamingleFacialReceiver.cs (70%) rename Assets/External/{Ifacialmocap => StreamingleFacial}/StreamingleFacialReceiver.cs.meta (100%) diff --git a/Assets/External/Ifacialmocap.meta b/Assets/External/StreamingleFacial.meta similarity index 77% rename from Assets/External/Ifacialmocap.meta rename to Assets/External/StreamingleFacial.meta index e6f9153c..84e87174 100644 --- a/Assets/External/Ifacialmocap.meta +++ b/Assets/External/StreamingleFacial.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 223415c286f7fd74f85d05754d2da6ad +guid: 07bce16f321d7734488143b6d237cc59 folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Assets/External/StreamingleFacial/Editor.meta b/Assets/External/StreamingleFacial/Editor.meta new file mode 100644 index 00000000..069200fc --- /dev/null +++ b/Assets/External/StreamingleFacial/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d38f0b9c853644840a90e334b9404fc1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/External/StreamingleFacial/Editor/BlendShapeIntensityOverrideDrawer.cs b/Assets/External/StreamingleFacial/Editor/BlendShapeIntensityOverrideDrawer.cs new file mode 100644 index 00000000..2ce4adb4 --- /dev/null +++ b/Assets/External/StreamingleFacial/Editor/BlendShapeIntensityOverrideDrawer.cs @@ -0,0 +1,136 @@ +using UnityEngine; +using UnityEditor; +using System.Collections.Generic; + +[CustomPropertyDrawer(typeof(StreamingleFacialReceiver.BlendShapeIntensityOverride))] +public class BlendShapeIntensityOverrideDrawer : PropertyDrawer +{ + // 카테고리별로 구분된 ARKit BlendShape 이름 (Popup에 구분선 역할) + private static readonly string[] ARKitBlendShapeNames = new string[] + { + // Eye (0-13) + "EyeBlinkLeft", "EyeBlinkRight", + "EyeLookDownLeft", "EyeLookDownRight", + "EyeLookInLeft", "EyeLookInRight", + "EyeLookOutLeft", "EyeLookOutRight", + "EyeLookUpLeft", "EyeLookUpRight", + "EyeSquintLeft", "EyeSquintRight", + "EyeWideLeft", "EyeWideRight", + // Jaw (14-17) + "JawForward", "JawLeft", "JawRight", "JawOpen", + // Mouth (18-37) + "MouthClose", "MouthFunnel", "MouthPucker", + "MouthLeft", "MouthRight", + "MouthSmileLeft", "MouthSmileRight", + "MouthFrownLeft", "MouthFrownRight", + "MouthDimpleLeft", "MouthDimpleRight", + "MouthStretchLeft", "MouthStretchRight", + "MouthRollLower", "MouthRollUpper", + "MouthShrugLower", "MouthShrugUpper", + "MouthPressLeft", "MouthPressRight", + "MouthLowerDownLeft", "MouthLowerDownRight", + "MouthUpperUpLeft", "MouthUpperUpRight", + // Brow (38-42) + "BrowDownLeft", "BrowDownRight", + "BrowInnerUp", + "BrowOuterUpLeft", "BrowOuterUpRight", + // Cheek/Nose (43-47) + "CheekPuff", "CheekSquintLeft", "CheekSquintRight", + "NoseSneerLeft", "NoseSneerRight", + // Tongue (48) + "TongueOut", + }; + + // 카테고리 구분 표시용 + private static readonly string[] DisplayNames; + private static readonly Dictionary nameToIndex; + + static BlendShapeIntensityOverrideDrawer() + { + nameToIndex = new Dictionary(System.StringComparer.OrdinalIgnoreCase); + + // 카테고리 프리픽스 부여 + DisplayNames = new string[ARKitBlendShapeNames.Length]; + for (int i = 0; i < ARKitBlendShapeNames.Length; i++) + { + string name = ARKitBlendShapeNames[i]; + string category; + + if (name.StartsWith("Eye")) category = "Eye"; + else if (name.StartsWith("Jaw")) category = "Jaw"; + else if (name.StartsWith("Mouth")) category = "Mouth"; + else if (name.StartsWith("Brow")) category = "Brow"; + else if (name.StartsWith("Cheek") || name.StartsWith("Nose")) category = "Cheek-Nose"; + else if (name.StartsWith("Tongue")) category = "Tongue"; + else category = ""; + + DisplayNames[i] = category + "/" + name; + nameToIndex[name] = i; + } + } + + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) + { + return EditorGUIUtility.singleLineHeight + 2f; + } + + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + EditorGUI.BeginProperty(position, label, property); + + position.y += 1f; + position.height -= 2f; + + var nameProp = property.FindPropertyRelative("blendShapeName"); + var intensityProp = property.FindPropertyRelative("intensity"); + + // 새로 추가된 항목의 기본값 보정 + if (intensityProp.floatValue == 0f && string.IsNullOrEmpty(nameProp.stringValue)) + { + intensityProp.floatValue = 1.0f; + } + + // 레이아웃: [드롭다운 55%] [슬라이더 35%] [값 라벨 10%] + float dropW = position.width * 0.55f; + float sliderW = position.width * 0.35f; + float valW = position.width * 0.10f - 6f; + + Rect dropRect = new Rect(position.x, position.y, dropW - 2f, position.height); + Rect sliderRect = new Rect(position.x + dropW + 2f, position.y, sliderW - 2f, position.height); + Rect valRect = new Rect(position.x + dropW + sliderW + 4f, position.y, valW, position.height); + + // 현재 인덱스 + int currentIndex = 0; + if (!string.IsNullOrEmpty(nameProp.stringValue) && nameToIndex.TryGetValue(nameProp.stringValue, out int idx)) + { + currentIndex = idx; + } + + // 드롭다운 (카테고리 구분) + int newIndex = EditorGUI.Popup(dropRect, currentIndex, DisplayNames); + if (newIndex != currentIndex || string.IsNullOrEmpty(nameProp.stringValue)) + { + nameProp.stringValue = ARKitBlendShapeNames[newIndex]; + } + + // 슬라이더 + intensityProp.floatValue = GUI.HorizontalSlider(sliderRect, intensityProp.floatValue, 0f, 3f); + + // 값 표시 (색상으로 강약 표현) + float val = intensityProp.floatValue; + Color valColor; + if (val < 0.5f) valColor = new Color(1f, 0.4f, 0.4f); // 약함 = 빨강 + else if (val > 1.5f) valColor = new Color(0.4f, 0.8f, 1f); // 강함 = 파랑 + else valColor = new Color(0.7f, 0.7f, 0.7f); // 보통 = 회색 + + var valStyle = new GUIStyle(EditorStyles.miniLabel) + { + alignment = TextAnchor.MiddleRight, + fontStyle = FontStyle.Bold, + }; + valStyle.normal.textColor = valColor; + EditorGUI.LabelField(valRect, $"x{val:F1}", valStyle); + + EditorGUI.EndProperty(); + } +} diff --git a/Assets/External/StreamingleFacial/Editor/BlendShapeIntensityOverrideDrawer.cs.meta b/Assets/External/StreamingleFacial/Editor/BlendShapeIntensityOverrideDrawer.cs.meta new file mode 100644 index 00000000..ea728542 --- /dev/null +++ b/Assets/External/StreamingleFacial/Editor/BlendShapeIntensityOverrideDrawer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 92034514226264240963a52d16489da8 \ No newline at end of file diff --git a/Assets/External/StreamingleFacial/Editor/StreamingleFacialReceiverEditor.cs b/Assets/External/StreamingleFacial/Editor/StreamingleFacialReceiverEditor.cs new file mode 100644 index 00000000..5ef0ca2c --- /dev/null +++ b/Assets/External/StreamingleFacial/Editor/StreamingleFacialReceiverEditor.cs @@ -0,0 +1,263 @@ +using UnityEngine; +using UnityEditor; +[CustomEditor(typeof(StreamingleFacialReceiver))] +public class StreamingleFacialReceiverEditor : Editor +{ + private SerializedProperty mirrorMode; + private SerializedProperty faceMeshRenderers; + private SerializedProperty availablePorts; + + private SerializedProperty enableFiltering; + private SerializedProperty smoothingFactor; + private SerializedProperty maxBlendShapeDelta; + private SerializedProperty maxRotationDelta; + private SerializedProperty fastBlendShapeMultiplier; + private SerializedProperty spikeToleranceFrames; + private SerializedProperty globalIntensity; + private SerializedProperty blendShapeIntensityOverrides; + + private bool showConnection = false; + private bool showFiltering = false; + private bool showIntensity = false; + + private static readonly Color HeaderColor = new Color(0.18f, 0.18f, 0.18f, 1f); + private static readonly Color ActivePortColor = new Color(0.2f, 0.8f, 0.4f, 1f); + private static readonly Color InactivePortColor = new Color(0.35f, 0.35f, 0.35f, 1f); + private static readonly Color AccentColor = new Color(0.4f, 0.7f, 1f, 1f); + + private GUIStyle _headerStyle; + private GUIStyle _sectionBoxStyle; + private GUIStyle _portButtonStyle; + private GUIStyle _portButtonActiveStyle; + private GUIStyle _statusLabelStyle; + + void OnEnable() + { + mirrorMode = serializedObject.FindProperty("mirrorMode"); + faceMeshRenderers = serializedObject.FindProperty("faceMeshRenderers"); + availablePorts = serializedObject.FindProperty("availablePorts"); + enableFiltering = serializedObject.FindProperty("enableFiltering"); + smoothingFactor = serializedObject.FindProperty("smoothingFactor"); + maxBlendShapeDelta = serializedObject.FindProperty("maxBlendShapeDelta"); + maxRotationDelta = serializedObject.FindProperty("maxRotationDelta"); + fastBlendShapeMultiplier = serializedObject.FindProperty("fastBlendShapeMultiplier"); + spikeToleranceFrames = serializedObject.FindProperty("spikeToleranceFrames"); + globalIntensity = serializedObject.FindProperty("globalIntensity"); + blendShapeIntensityOverrides = serializedObject.FindProperty("blendShapeIntensityOverrides"); + } + + void InitStyles() + { + if (_headerStyle != null) return; + + _headerStyle = new GUIStyle(EditorStyles.boldLabel) + { + fontSize = 13, + alignment = TextAnchor.MiddleLeft, + padding = new RectOffset(8, 0, 4, 4), + }; + _headerStyle.normal.textColor = AccentColor; + + _sectionBoxStyle = new GUIStyle("HelpBox") + { + padding = new RectOffset(10, 10, 8, 8), + margin = new RectOffset(0, 0, 4, 8), + }; + + _portButtonStyle = new GUIStyle(GUI.skin.button) + { + fontSize = 12, + fontStyle = FontStyle.Bold, + fixedHeight = 36, + alignment = TextAnchor.MiddleCenter, + }; + + _portButtonActiveStyle = new GUIStyle(_portButtonStyle); + _portButtonActiveStyle.normal.textColor = Color.white; + + _statusLabelStyle = new GUIStyle(EditorStyles.miniLabel) + { + alignment = TextAnchor.MiddleCenter, + fontSize = 10, + }; + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + InitStyles(); + + var mocap = (StreamingleFacialReceiver)target; + + // 타이틀 + EditorGUILayout.Space(4); + DrawTitle(); + EditorGUILayout.Space(4); + + // 기본 설정 + DrawBasicSettings(); + + // 포트 핫스왑 + DrawPortSection(mocap); + + // 필터링 + DrawFilteringSection(); + + // 페이셜 강도 + DrawIntensitySection(); + + serializedObject.ApplyModifiedProperties(); + } + + void DrawTitle() + { + var rect = EditorGUILayout.GetControlRect(false, 32); + EditorGUI.DrawRect(rect, HeaderColor); + + var titleStyle = new GUIStyle(EditorStyles.boldLabel) + { + fontSize = 15, + alignment = TextAnchor.MiddleLeft, + }; + titleStyle.normal.textColor = Color.white; + + EditorGUI.LabelField(rect, " Streamingle Facial Receiver", titleStyle); + + // 상태 표시 + if (Application.isPlaying) + { + var statusRect = new Rect(rect.xMax - 80, rect.y, 75, rect.height); + var dotRect = new Rect(statusRect.x - 12, rect.y + rect.height / 2 - 4, 8, 8); + EditorGUI.DrawRect(dotRect, ActivePortColor); + EditorGUI.LabelField(statusRect, "LIVE", _statusLabelStyle); + } + } + + void DrawBasicSettings() + { + EditorGUILayout.BeginVertical(_sectionBoxStyle); + + EditorGUILayout.PropertyField(faceMeshRenderers, new GUIContent("Face Mesh Renderers")); + EditorGUILayout.Space(2); + EditorGUILayout.PropertyField(mirrorMode, new GUIContent("Mirror Mode (L/R Flip)")); + + EditorGUILayout.EndVertical(); + } + + void DrawPortSection(StreamingleFacialReceiver mocap) + { + showConnection = DrawSectionHeader("Port Hot-Swap", showConnection); + if (!showConnection) return; + + EditorGUILayout.BeginVertical(_sectionBoxStyle); + + // 현재 포트 표시 + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Active Port", EditorStyles.miniLabel, GUILayout.Width(70)); + var portLabel = new GUIStyle(EditorStyles.boldLabel); + portLabel.normal.textColor = ActivePortColor; + EditorGUILayout.LabelField(mocap.LOCAL_PORT.ToString(), portLabel); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(4); + + // 포트 버튼 그리드 + if (mocap.availablePorts != null && mocap.availablePorts.Length > 0) + { + EditorGUILayout.BeginHorizontal(); + for (int i = 0; i < mocap.availablePorts.Length; i++) + { + bool isActive = i == mocap.activePortIndex; + + var prevBg = GUI.backgroundColor; + GUI.backgroundColor = isActive ? ActivePortColor : InactivePortColor; + + var style = isActive ? _portButtonActiveStyle : _portButtonStyle; + string label = mocap.availablePorts[i].ToString(); + + if (GUILayout.Button(label, style)) + { + if (Application.isPlaying) + { + mocap.SwitchToPort(i); + } + else + { + Undo.RecordObject(mocap, "Switch Port"); + mocap.activePortIndex = i; + EditorUtility.SetDirty(mocap); + } + } + + GUI.backgroundColor = prevBg; + } + EditorGUILayout.EndHorizontal(); + } + + EditorGUILayout.Space(2); + EditorGUILayout.PropertyField(availablePorts, new GUIContent("Port List"), true); + + EditorGUILayout.EndVertical(); + } + + void DrawFilteringSection() + { + showFiltering = DrawSectionHeader("Data Filtering", showFiltering); + if (!showFiltering) return; + + EditorGUILayout.BeginVertical(_sectionBoxStyle); + + EditorGUILayout.PropertyField(enableFiltering, new GUIContent("Enable")); + + if (enableFiltering.boolValue) + { + EditorGUI.indentLevel++; + EditorGUILayout.Space(2); + EditorGUILayout.PropertyField(smoothingFactor, new GUIContent("Smoothing")); + EditorGUILayout.PropertyField(maxBlendShapeDelta, new GUIContent("Max BlendShape Delta")); + EditorGUILayout.PropertyField(maxRotationDelta, new GUIContent("Max Rotation Delta")); + EditorGUILayout.PropertyField(fastBlendShapeMultiplier, new GUIContent("Fast BS Multiplier")); + EditorGUILayout.PropertyField(spikeToleranceFrames, new GUIContent("Spike Tolerance")); + EditorGUI.indentLevel--; + } + + EditorGUILayout.EndVertical(); + } + + void DrawIntensitySection() + { + showIntensity = DrawSectionHeader("Facial Intensity", showIntensity); + if (!showIntensity) return; + + EditorGUILayout.BeginVertical(_sectionBoxStyle); + + // Global intensity - 크게 표시 + EditorGUILayout.LabelField("Global", EditorStyles.miniLabel); + EditorGUILayout.PropertyField(globalIntensity, GUIContent.none); + + EditorGUILayout.Space(6); + + // 구분선 + var lineRect = EditorGUILayout.GetControlRect(false, 1); + EditorGUI.DrawRect(lineRect, new Color(0.4f, 0.4f, 0.4f, 0.5f)); + + EditorGUILayout.Space(4); + EditorGUILayout.LabelField("Per-BlendShape Overrides", EditorStyles.miniLabel); + EditorGUILayout.PropertyField(blendShapeIntensityOverrides, GUIContent.none, true); + + EditorGUILayout.EndVertical(); + } + + bool DrawSectionHeader(string title, bool expanded) + { + EditorGUILayout.Space(2); + var rect = EditorGUILayout.GetControlRect(false, 24); + EditorGUI.DrawRect(rect, HeaderColor); + + // 폴드 화살표 + 타이틀 + var foldRect = new Rect(rect.x + 4, rect.y, rect.width - 4, rect.height); + bool result = EditorGUI.Foldout(foldRect, expanded, " " + title, true, _headerStyle); + + return result; + } +} diff --git a/Assets/External/StreamingleFacial/Editor/StreamingleFacialReceiverEditor.cs.meta b/Assets/External/StreamingleFacial/Editor/StreamingleFacialReceiverEditor.cs.meta new file mode 100644 index 00000000..60132fb1 --- /dev/null +++ b/Assets/External/StreamingleFacial/Editor/StreamingleFacialReceiverEditor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 49f85d984656bc24583e253320766599 \ No newline at end of file diff --git a/Assets/External/Ifacialmocap/StreamingleFacialReceiver.cs b/Assets/External/StreamingleFacial/StreamingleFacialReceiver.cs similarity index 70% rename from Assets/External/Ifacialmocap/StreamingleFacialReceiver.cs rename to Assets/External/StreamingleFacial/StreamingleFacialReceiver.cs index 1ce7b18e..d801c3e8 100644 --- a/Assets/External/Ifacialmocap/StreamingleFacialReceiver.cs +++ b/Assets/External/StreamingleFacial/StreamingleFacialReceiver.cs @@ -5,31 +5,30 @@ 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 +public class StreamingleFacialReceiver : MonoBehaviour { - // broadcast address - public bool gameStartWithConnect = true; - public string iOS_IPAddress = "255.255.255.255"; - public bool mirrorMode = false; // 좌우 반전 모드 설정 - private UdpClient client; + public bool mirrorMode = true; // 좌우 반전 모드 설정 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("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")] @@ -37,7 +36,7 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour public bool enableFiltering = true; [Tooltip("스무딩 강도 (0=필터없음, 1=최대 스무딩). 프레임레이트 독립적으로 동작")] [Range(0f, 0.95f)] - public float smoothingFactor = 0.5f; + public float smoothingFactor = 0.1f; [Tooltip("프레임 간 최대 허용 변화량 (BlendShape, 0~100 스케일)")] [Range(1f, 100f)] public float maxBlendShapeDelta = 30f; @@ -53,14 +52,10 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour // 필터링용 이전 값 저장 private Dictionary prevBlendShapeValues = new Dictionary(); - private Dictionary prevBoneRotations = new Dictionary(); - private Vector3 prevHeadPosition = Vector3.zero; - private bool hasFirstFrame = false; // 연속 스파이크 추적 (같은 방향으로 연속이면 실제 움직임) private Dictionary blendShapeSpikeCount = new Dictionary(); private Dictionary blendShapeSpikeDirection = new Dictionary(); - private Dictionary boneSpikeCount = new Dictionary(); // 빠르게 변하는 BlendShape 목록 (눈 깜빡임, 입 등) private static readonly HashSet FastBlendShapes = new HashSet(StringComparer.OrdinalIgnoreCase) @@ -74,6 +69,28 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour "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; @@ -83,8 +100,6 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour 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() { @@ -129,12 +144,6 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour // BlendShape 인덱스 캐싱 초기화 InitializeBlendShapeCache(); - //Send to iOS - if (gameStartWithConnect == true) - { - Connect_to_iOS_App(); - } - //Recieve udp from iOS CreateUdpServer(); } @@ -206,57 +215,41 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour } } - IEnumerator WaitProcess(float WaitTime) + + void OnValidate() { - yield return new WaitForSeconds(WaitTime); + intensityMapDirty = true; } - void Connect_to_iOS_App() + void RebuildIntensityOverrideMap() { - try + intensityOverrideMap.Clear(); + foreach (var entry in blendShapeIntensityOverrides) { - //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++) + if (!string.IsNullOrEmpty(entry.blendShapeName)) { - client.Send(dgram, dgram.Length); + intensityOverrideMap[entry.blendShapeName] = entry.intensity; } } - catch (Exception e) - { - Debug.LogError($"[iFacialMocap] 메시지 전송 실패: {e.Message}"); - } + 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) { @@ -284,6 +277,10 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour // 정규화된 이름으로 캐시 검색 string normalizedName = NormalizeBlendShapeName(shapeName).ToLowerInvariant(); + // 강도 배율 적용 + weight *= GetBlendShapeIntensity(normalizedName); + weight = Mathf.Clamp(weight, 0f, 100f); + // 필터링 적용 if (enableFiltering) { @@ -375,74 +372,7 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour } } - // 본 회전 처리 - 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) { @@ -529,11 +459,6 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour public void StopUDP() { - if (gameStartWithConnect == true) - { - StopStreaming_iOS_App(); - } - // 안전한 스레드 종료 isThreadRunning = false; @@ -557,6 +482,22 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour } } + /// + /// 포트 핫스왑: 지정된 인덱스의 포트로 즉시 전환 + /// + 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(); + } + /// /// 페이셜 모션 캡처 재접속 /// @@ -666,92 +607,9 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour 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) - { - int count = 0; - boneSpikeCount.TryGetValue(boneName, out count); - count++; - boneSpikeCount[boneName] = count; - - // 연속이면 실제 움직임 - if (count >= spikeToleranceFrames) - { - boneSpikeCount[boneName] = 0; - prevBoneRotations[boneName] = rawRotation; - return rawRotation; - } - - Vector3 clamped = Vector3.MoveTowards(prevRot, rawRotation, maxRotationDelta); - prevBoneRotations[boneName] = clamped; - return clamped; - } - - boneSpikeCount[boneName] = 0; - - float alpha = GetFrameIndependentSmoothing(); - Vector3 smoothed = Vector3.Lerp(rawRotation, prevRot, alpha); - prevBoneRotations[boneName] = smoothed; - return smoothed; - } - - prevBoneRotations[boneName] = rawRotation; - return rawRotation; - } - - /// - /// 머리 위치 필터링 - /// - Vector3 FilterHeadPosition(Vector3 rawPos) - { - if (hasFirstFrame) - { - float maxPosDelta = maxRotationDelta * 0.01f; - float delta = Vector3.Distance(rawPos, prevHeadPosition); - - if (delta > maxPosDelta) - { - Vector3 clamped = Vector3.MoveTowards(prevHeadPosition, rawPos, maxPosDelta); - prevHeadPosition = clamped; - return clamped; - } - - float alpha = GetFrameIndependentSmoothing(); - Vector3 smoothed = Vector3.Lerp(rawPos, prevHeadPosition, alpha); - 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 class StreamingleFacialReceiverExtensions { public static List GetAll(this GameObject obj) { diff --git a/Assets/External/Ifacialmocap/StreamingleFacialReceiver.cs.meta b/Assets/External/StreamingleFacial/StreamingleFacialReceiver.cs.meta similarity index 100% rename from Assets/External/Ifacialmocap/StreamingleFacialReceiver.cs.meta rename to Assets/External/StreamingleFacial/StreamingleFacialReceiver.cs.meta