Fix : 페이셜 이름 업데이트
This commit is contained in:
parent
efc0adced8
commit
4e5634536a
@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 223415c286f7fd74f85d05754d2da6ad
|
guid: 07bce16f321d7734488143b6d237cc59
|
||||||
folderAsset: yes
|
folderAsset: yes
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
8
Assets/External/StreamingleFacial/Editor.meta
vendored
Normal file
8
Assets/External/StreamingleFacial/Editor.meta
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d38f0b9c853644840a90e334b9404fc1
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
136
Assets/External/StreamingleFacial/Editor/BlendShapeIntensityOverrideDrawer.cs
vendored
Normal file
136
Assets/External/StreamingleFacial/Editor/BlendShapeIntensityOverrideDrawer.cs
vendored
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/External/StreamingleFacial/Editor/BlendShapeIntensityOverrideDrawer.cs.meta
vendored
Normal file
2
Assets/External/StreamingleFacial/Editor/BlendShapeIntensityOverrideDrawer.cs.meta
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 92034514226264240963a52d16489da8
|
||||||
263
Assets/External/StreamingleFacial/Editor/StreamingleFacialReceiverEditor.cs
vendored
Normal file
263
Assets/External/StreamingleFacial/Editor/StreamingleFacialReceiverEditor.cs
vendored
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/External/StreamingleFacial/Editor/StreamingleFacialReceiverEditor.cs.meta
vendored
Normal file
2
Assets/External/StreamingleFacial/Editor/StreamingleFacialReceiverEditor.cs.meta
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 49f85d984656bc24583e253320766599
|
||||||
@ -5,31 +5,30 @@ using System.Text;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
||||||
public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour
|
public class StreamingleFacialReceiver : MonoBehaviour
|
||||||
{
|
{
|
||||||
// broadcast address
|
public bool mirrorMode = true; // 좌우 반전 모드 설정
|
||||||
public bool gameStartWithConnect = true;
|
|
||||||
public string iOS_IPAddress = "255.255.255.255";
|
|
||||||
public bool mirrorMode = false; // 좌우 반전 모드 설정
|
|
||||||
private UdpClient client;
|
|
||||||
private bool StartFlag = true;
|
private bool StartFlag = true;
|
||||||
|
|
||||||
//object references
|
//object references
|
||||||
public SkinnedMeshRenderer[] faceMeshRenderers;
|
public SkinnedMeshRenderer[] faceMeshRenderers;
|
||||||
public Transform headBone;
|
|
||||||
public Transform rightEyeBone;
|
|
||||||
public Transform leftEyeBone;
|
|
||||||
public Transform headPositionObject;
|
|
||||||
|
|
||||||
private UdpClient udp;
|
private UdpClient udp;
|
||||||
private Thread thread;
|
private Thread thread;
|
||||||
private volatile bool isThreadRunning = false; // 스레드 실행 상태 플래그
|
private volatile bool isThreadRunning = false; // 스레드 실행 상태 플래그
|
||||||
private string messageString = "";
|
private string messageString = "";
|
||||||
private string lastProcessedMessage = ""; // 이전 메시지 저장용
|
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")]
|
[Header("Data Filtering")]
|
||||||
@ -37,7 +36,7 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour
|
|||||||
public bool enableFiltering = true;
|
public bool enableFiltering = true;
|
||||||
[Tooltip("스무딩 강도 (0=필터없음, 1=최대 스무딩). 프레임레이트 독립적으로 동작")]
|
[Tooltip("스무딩 강도 (0=필터없음, 1=최대 스무딩). 프레임레이트 독립적으로 동작")]
|
||||||
[Range(0f, 0.95f)]
|
[Range(0f, 0.95f)]
|
||||||
public float smoothingFactor = 0.5f;
|
public float smoothingFactor = 0.1f;
|
||||||
[Tooltip("프레임 간 최대 허용 변화량 (BlendShape, 0~100 스케일)")]
|
[Tooltip("프레임 간 최대 허용 변화량 (BlendShape, 0~100 스케일)")]
|
||||||
[Range(1f, 100f)]
|
[Range(1f, 100f)]
|
||||||
public float maxBlendShapeDelta = 30f;
|
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, 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, int> blendShapeSpikeCount = new Dictionary<string, int>();
|
||||||
private Dictionary<string, float> blendShapeSpikeDirection = new Dictionary<string, float>();
|
private Dictionary<string, float> blendShapeSpikeDirection = new Dictionary<string, float>();
|
||||||
private Dictionary<string, int> boneSpikeCount = new Dictionary<string, int>();
|
|
||||||
|
|
||||||
// 빠르게 변하는 BlendShape 목록 (눈 깜빡임, 입 등)
|
// 빠르게 변하는 BlendShape 목록 (눈 깜빡임, 입 등)
|
||||||
private static readonly HashSet<string> FastBlendShapes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
private static readonly HashSet<string> FastBlendShapes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||||
@ -74,6 +69,28 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour
|
|||||||
"mouthfrownright", "mouthfrownleft",
|
"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
|
// 프레임레이트 독립 스무딩을 위한 기준 FPS
|
||||||
private const float ReferenceFPS = 60f;
|
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[] splitPipe = new char[] { '|' };
|
||||||
private readonly char[] splitAnd = new char[] { '&' };
|
private readonly char[] splitAnd = new char[] { '&' };
|
||||||
private readonly char[] splitDash = new char[] { '-' };
|
private readonly char[] splitDash = new char[] { '-' };
|
||||||
private readonly char[] splitHash = new char[] { '#' };
|
|
||||||
private readonly char[] splitComma = new char[] { ',' };
|
|
||||||
|
|
||||||
// Mirror mode용 정적 매핑 테이블
|
// Mirror mode용 정적 매핑 테이블
|
||||||
private static readonly Dictionary<string, string> EyeMirrorMap = new Dictionary<string, string>() {
|
private static readonly Dictionary<string, string> EyeMirrorMap = new Dictionary<string, string>() {
|
||||||
@ -129,12 +144,6 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour
|
|||||||
// BlendShape 인덱스 캐싱 초기화
|
// BlendShape 인덱스 캐싱 초기화
|
||||||
InitializeBlendShapeCache();
|
InitializeBlendShapeCache();
|
||||||
|
|
||||||
//Send to iOS
|
|
||||||
if (gameStartWithConnect == true)
|
|
||||||
{
|
|
||||||
Connect_to_iOS_App();
|
|
||||||
}
|
|
||||||
|
|
||||||
//Recieve udp from iOS
|
//Recieve udp from iOS
|
||||||
CreateUdpServer();
|
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
|
if (!string.IsNullOrEmpty(entry.blendShapeName))
|
||||||
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);
|
intensityOverrideMap[entry.blendShapeName] = entry.intensity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
intensityMapDirty = false;
|
||||||
{
|
}
|
||||||
Debug.LogError($"[iFacialMocap] 메시지 전송 실패: {e.Message}");
|
|
||||||
}
|
float GetBlendShapeIntensity(string normalizedName)
|
||||||
|
{
|
||||||
|
if (intensityOverrideMap.TryGetValue(normalizedName, out float val))
|
||||||
|
return val * globalIntensity;
|
||||||
|
return globalIntensity;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update is called once per frame
|
// Update is called once per frame
|
||||||
void Update()
|
void Update()
|
||||||
{
|
{
|
||||||
|
// 강도 오버라이드 맵 갱신 (인스펙터 변경 반영)
|
||||||
|
if (intensityMapDirty)
|
||||||
|
{
|
||||||
|
RebuildIntensityOverrideMap();
|
||||||
|
}
|
||||||
|
|
||||||
// 메시지가 변경되었을 때만 처리 (성능 최적화)
|
// 메시지가 변경되었을 때만 처리 (성능 최적화)
|
||||||
if (!string.IsNullOrEmpty(messageString) && messageString != lastProcessedMessage)
|
if (!string.IsNullOrEmpty(messageString) && messageString != lastProcessedMessage)
|
||||||
{
|
{
|
||||||
@ -284,6 +277,10 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour
|
|||||||
// 정규화된 이름으로 캐시 검색
|
// 정규화된 이름으로 캐시 검색
|
||||||
string normalizedName = NormalizeBlendShapeName(shapeName).ToLowerInvariant();
|
string normalizedName = NormalizeBlendShapeName(shapeName).ToLowerInvariant();
|
||||||
|
|
||||||
|
// 강도 배율 적용
|
||||||
|
weight *= GetBlendShapeIntensity(normalizedName);
|
||||||
|
weight = Mathf.Clamp(weight, 0f, 100f);
|
||||||
|
|
||||||
// 필터링 적용
|
// 필터링 적용
|
||||||
if (enableFiltering)
|
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)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@ -529,11 +459,6 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour
|
|||||||
|
|
||||||
public void StopUDP()
|
public void StopUDP()
|
||||||
{
|
{
|
||||||
if (gameStartWithConnect == true)
|
|
||||||
{
|
|
||||||
StopStreaming_iOS_App();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 안전한 스레드 종료
|
// 안전한 스레드 종료
|
||||||
isThreadRunning = false;
|
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>
|
||||||
/// 페이셜 모션 캡처 재접속
|
/// 페이셜 모션 캡처 재접속
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -666,92 +607,9 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour
|
|||||||
return rawValue;
|
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)
|
public static List<GameObject> GetAll(this GameObject obj)
|
||||||
{
|
{
|
||||||
Loading…
x
Reference in New Issue
Block a user