diff --git a/Assets/External/OptiTrack Unity Plugin/OptiTrack/Editor/OptitrackSkeletonAnimatorEditor.cs b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Editor/OptitrackSkeletonAnimatorEditor.cs
index 6ff4f96a6..1d3b7fd3f 100644
--- a/Assets/External/OptiTrack Unity Plugin/OptiTrack/Editor/OptitrackSkeletonAnimatorEditor.cs
+++ b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Editor/OptitrackSkeletonAnimatorEditor.cs
@@ -4,12 +4,57 @@ using UnityEngine;
[CustomEditor(typeof(OptitrackSkeletonAnimator_Mingle))]
public class OptitrackSkeletonAnimatorEditor : Editor
{
+ private static readonly string[] k_FilterLabels = { "Off", "Low", "Medium", "High", "Custom" };
+
public override void OnInspectorGUI()
{
DrawDefaultInspector();
var anim = (OptitrackSkeletonAnimator_Mingle)target;
+ // 필터 강도 버튼 나열
+ EditorGUILayout.Space(4);
+ EditorGUILayout.LabelField("필터 강도", EditorStyles.boldLabel);
+
+ EditorGUILayout.BeginHorizontal();
+ for (int i = 0; i < k_FilterLabels.Length; i++)
+ {
+ var strength = (OptitrackSkeletonAnimator_Mingle.FilterStrength)i;
+ bool isSelected = anim.filterStrength == strength;
+
+ var prevBgBtn = GUI.backgroundColor;
+ GUI.backgroundColor = isSelected ? new Color(0.4f, 0.7f, 1f) : Color.white;
+
+ if (GUILayout.Button(k_FilterLabels[i], GUILayout.Height(24)))
+ {
+ Undo.RecordObject(anim, "Change Filter Strength");
+ anim.SetFilterStrength(strength);
+ EditorUtility.SetDirty(anim);
+ }
+
+ GUI.backgroundColor = prevBgBtn;
+ }
+ EditorGUILayout.EndHorizontal();
+
+ // Custom일 때 슬라이더 표시
+ if (anim.filterStrength == OptitrackSkeletonAnimator_Mingle.FilterStrength.Custom)
+ {
+ EditorGUI.indentLevel++;
+ EditorGUI.BeginChangeCheck();
+ float minCutoff = EditorGUILayout.Slider("Min Cutoff (Hz)", anim.filterMinCutoff, 0.1f, 10f);
+ float beta = EditorGUILayout.Slider("Beta", anim.filterBeta, 0f, 5f);
+ float maxCutoff = EditorGUILayout.Slider("Max Cutoff (Hz)", anim.filterMaxCutoff, 5f, 120f);
+ if (EditorGUI.EndChangeCheck())
+ {
+ Undo.RecordObject(anim, "Change Filter Custom Values");
+ anim.filterMinCutoff = minCutoff;
+ anim.filterBeta = beta;
+ anim.filterMaxCutoff = maxCutoff;
+ EditorUtility.SetDirty(anim);
+ }
+ EditorGUI.indentLevel--;
+ }
+
// 연결 상태 배지
EditorGUILayout.Space(8);
var prevBg = GUI.backgroundColor;
diff --git a/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackSkeletonAnimator_Mingle.cs b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackSkeletonAnimator_Mingle.cs
index 9a99205dd..3a7cba5bf 100644
--- a/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackSkeletonAnimator_Mingle.cs
+++ b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackSkeletonAnimator_Mingle.cs
@@ -33,19 +33,53 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
[Tooltip("에디터에서 캡처한 T-포즈 데이터. 비어 있으면 런타임 CacheRestPose()를 사용합니다.")]
public OptitrackRestPoseData RestPoseAsset;
+ public enum FilterStrength { Off, Low, Medium, High, Custom }
+
[Header("본 1€ 필터 (속도 적응형 저역통과)")]
- [Tooltip("활성화 시 빠른 움직임은 그대로, 정지/느린 움직임의 노이즈를 제거합니다.\n단순 EMA보다 모션 보존이 훨씬 우수합니다.")]
- public bool enableBoneFilter = true;
- [Tooltip("최소 차단 주파수 (Hz). 정지 시 노이즈 제거 강도. 낮을수록 강함. 권장: 2~4 Hz")]
- [Range(0.1f, 10f)]
- public float filterMinCutoff = 3.0f;
- [Tooltip("속도 계수. 빠른 동작에서 cutoff 상승 속도. 높을수록 지연 감소. 권장: 0.5~2.0")]
- [Range(0f, 5f)]
- public float filterBeta = 1.5f;
- [Tooltip("최대 차단 주파수 상한 (Hz). 빠른 동작에서도 이 이상 주파수는 항상 제거됩니다.\n" +
- "MagicaCloth2 지터가 빠른 동작에서 발생하면 낮추세요. 권장: 10~20 Hz")]
- [Range(5f, 120f)]
- public float filterMaxCutoff = 15.0f;
+ [HideInInspector]
+ public FilterStrength filterStrength = FilterStrength.Medium;
+
+ [HideInInspector] public float filterMinCutoff = 3.0f;
+ [HideInInspector] public float filterBeta = 1.5f;
+ [HideInInspector] public float filterMaxCutoff = 15.0f;
+
+ // 프리셋별 파라미터 (minCutoff, beta, maxCutoff)
+ private static readonly (float minCutoff, float beta, float maxCutoff)[] k_FilterPresets =
+ {
+ (0f, 0f, 0f), // Off (사용 안 함)
+ (5.0f, 2.0f, 25.0f), // Low
+ (3.0f, 1.5f, 15.0f), // Medium
+ (1.5f, 0.8f, 10.0f), // High
+ (0f, 0f, 0f), // Custom (프리셋 적용 안 함)
+ };
+
+ [HideInInspector] public bool enableBoneFilter = true;
+
+ ///
+ /// 런타임에서 필터 강도를 변경합니다. StreamDeck/핫키 등에서 호출.
+ ///
+ public void SetFilterStrength(FilterStrength strength)
+ {
+ filterStrength = strength;
+ if (strength != FilterStrength.Off && strength != FilterStrength.Custom)
+ {
+ var preset = k_FilterPresets[(int)strength];
+ filterMinCutoff = preset.minCutoff;
+ filterBeta = preset.beta;
+ filterMaxCutoff = preset.maxCutoff;
+ }
+ // 프리셋 전환 시 필터 상태 리셋 (이전 값과 불연속 방지)
+ m_filterStates.Clear();
+ }
+
+ ///
+ /// 현재 프리셋에서 다음 프리셋으로 순환. 버튼 하나로 Off→Low→Medium→High→Custom→Off...
+ ///
+ public void CycleFilterStrength()
+ {
+ int next = ((int)filterStrength + 1) % System.Enum.GetValues(typeof(FilterStrength)).Length;
+ SetFilterStrength((FilterStrength)next);
+ }
private OptitrackSkeletonDefinition m_skeletonDef;
private string previousSkeletonName;
@@ -239,6 +273,9 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
return;
}
+ // 필터 활성화 상태 동기화 (프리셋 값은 SetFilterStrength()에서만 적용)
+ enableBoneFilter = filterStrength != FilterStrength.Off;
+
// MirrorMode 변경 감지 → 필터 상태 리셋 (불연속 튐 방지)
bool currentMirrorMode = StreamingClient != null && StreamingClient.MirrorMode;
if (currentMirrorMode != m_lastMirrorMode)
diff --git a/Assets/External/StreamingleFacial/Editor/StreamingleFacialReceiverEditor.cs b/Assets/External/StreamingleFacial/Editor/StreamingleFacialReceiverEditor.cs
index c64595b8f..fdb3844cf 100644
--- a/Assets/External/StreamingleFacial/Editor/StreamingleFacialReceiverEditor.cs
+++ b/Assets/External/StreamingleFacial/Editor/StreamingleFacialReceiverEditor.cs
@@ -17,6 +17,9 @@ public class StreamingleFacialReceiverEditor : Editor
private VisualElement statusContainer;
private VisualElement emaFields;
private VisualElement euroFields;
+ private VisualElement medianEuroFields;
+ private VisualElement sharedPortInfo;
+ private Label masterStatusValue;
public override VisualElement CreateInspectorGUI()
{
@@ -40,6 +43,9 @@ public class StreamingleFacialReceiverEditor : Editor
portButtonsContainer = root.Q("portButtonsContainer");
emaFields = root.Q("emaFields");
euroFields = root.Q("euroFields");
+ medianEuroFields = root.Q("medianEuroFields");
+ sharedPortInfo = root.Q("sharedPortInfo");
+ masterStatusValue = root.Q