Fix : 페이셜 이름 업데이트

This commit is contained in:
user 2026-02-09 17:55:33 +09:00
parent efc0adced8
commit 4e5634536a
8 changed files with 489 additions and 220 deletions

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 223415c286f7fd74f85d05754d2da6ad
guid: 07bce16f321d7734488143b6d237cc59
folderAsset: yes
DefaultImporter:
externalObjects: {}

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d38f0b9c853644840a90e334b9404fc1
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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<string, int> nameToIndex;
static BlendShapeIntensityOverrideDrawer()
{
nameToIndex = new Dictionary<string, int>(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();
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 92034514226264240963a52d16489da8

View File

@ -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;
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 49f85d984656bc24583e253320766599

View File

@ -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<string, float> prevBlendShapeValues = new Dictionary<string, float>();
private Dictionary<string, Vector3> prevBoneRotations = new Dictionary<string, Vector3>();
private Vector3 prevHeadPosition = Vector3.zero;
private bool hasFirstFrame = false;
// 연속 스파이크 추적 (같은 방향으로 연속이면 실제 움직임)
private Dictionary<string, int> blendShapeSpikeCount = new Dictionary<string, int>();
private Dictionary<string, float> blendShapeSpikeDirection = new Dictionary<string, float>();
private Dictionary<string, int> boneSpikeCount = new Dictionary<string, int>();
// 빠르게 변하는 BlendShape 목록 (눈 깜빡임, 입 등)
private static readonly HashSet<string> FastBlendShapes = new HashSet<string>(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<BlendShapeIntensityOverride> blendShapeIntensityOverrides = new List<BlendShapeIntensityOverride>();
[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<string, float> intensityOverrideMap = new Dictionary<string, float>(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<string, string> EyeMirrorMap = new Dictionary<string, string>() {
@ -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
}
}
/// <summary>
/// 포트 핫스왑: 지정된 인덱스의 포트로 즉시 전환
/// </summary>
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();
}
/// <summary>
/// 페이셜 모션 캡처 재접속
/// </summary>
@ -666,92 +607,9 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour
return rawValue;
}
/// <summary>
/// 본 회전 필터링: 연속 스파이크 판별 + 프레임독립 EMA
/// </summary>
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;
}
/// <summary>
/// 머리 위치 필터링
/// </summary>
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<GameObject> GetAll(this GameObject obj)
{