diff --git a/Assets/External/OptiTrack Unity Plugin/OptiTrack/Editor/OptitrackSkeletonAnimatorEditor.cs b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Editor/OptitrackSkeletonAnimatorEditor.cs
index 1d3b7fd3f..6ff4f96a6 100644
--- a/Assets/External/OptiTrack Unity Plugin/OptiTrack/Editor/OptitrackSkeletonAnimatorEditor.cs
+++ b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Editor/OptitrackSkeletonAnimatorEditor.cs
@@ -4,57 +4,12 @@ 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 21ae18214..854546b74 100644
--- a/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackSkeletonAnimator_Mingle.cs
+++ b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackSkeletonAnimator_Mingle.cs
@@ -33,33 +33,11 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
[Tooltip("에디터에서 캡처한 T-포즈 데이터. 비어 있으면 런타임 CacheRestPose()를 사용합니다.")]
public OptitrackRestPoseData RestPoseAsset;
- public enum FilterStrength { Off, Low, Medium, High, Custom }
-
- [Header("본 1€ 필터 (속도 적응형 저역통과)")]
- [HideInInspector]
- public FilterStrength filterStrength = FilterStrength.Off;
-
[Header("어깨 증폭")]
[Tooltip("어깨 회전을 증폭합니다. 1 = 원본, 2 = 2배. 하위 체인(상완)은 자동 역보정되어 손 위치가 유지됩니다.")]
[Range(0f, 10f)]
public float shoulderAmplify = 2f;
- [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;
-
[Header("프레임 보간")]
[Tooltip("OptiTrack 프레임 사이를 보간하여 Unity 가변 프레임에서도 부드러운 모션을 생성합니다. 약 1프레임(~8ms @120fps) 지연이 추가됩니다.")]
public bool enableInterpolation = true;
@@ -68,32 +46,6 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
[Range(0f, 0.05f)]
public float interpolationDelay = 0f;
- ///
- /// 런타임에서 필터 강도를 변경합니다. 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;
@@ -145,17 +97,6 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
private OptitrackHiResTimer.Timestamp m_lastFrameTimestamp;
private bool m_hasLastFrameTimestamp = false;
- // 1€ 필터 상태 (본 ID → 이전 프레임 필터 상태)
- private struct BoneFilterState
- {
- public Quaternion prevOri;
- public float dOriMag; // 필터된 각속도 크기 (rad/s)
- public Vector3 prevPos;
- public float dPosMag; // 필터된 선속도 크기 (m/s)
- public bool initialized;
- }
- private Dictionary m_filterStates = new Dictionary();
-
// 프레임 보간용 이중 버퍼 (prev/curr OptiTrack 프레임)
private Dictionary m_interpPrevPos = new Dictionary();
private Dictionary m_interpPrevOri = new Dictionary();
@@ -334,14 +275,10 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
return;
}
- // 필터 활성화 상태 동기화 (프리셋 값은 SetFilterStrength()에서만 적용)
- enableBoneFilter = filterStrength != FilterStrength.Off;
-
- // MirrorMode 변경 감지 → 필터/보간 상태 리셋 (불연속 튐 방지)
+ // MirrorMode 변경 감지 → 보간 상태 리셋 (불연속 튐 방지)
bool currentMirrorMode = StreamingClient != null && StreamingClient.MirrorMode;
if (currentMirrorMode != m_lastMirrorMode)
{
- m_filterStates.Clear();
ClearInterpolationBuffers();
m_lastMirrorMode = currentMirrorMode;
}
@@ -379,83 +316,29 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
if (enableInterpolation)
InterpolateSnapshots(frameTs);
- // ── Pass 1: Raw 데이터 적용 → IK 포인트 월드 위치 캡처 ──────────────────
- // 필터가 활성화되어 있을 때만 two-pass, 비활성이면 single-pass
- if (enableBoneFilter)
+ // 본 Transform에 스냅샷 적용
+ foreach (var bone in m_skeletonDef.Bones)
{
- // Raw 데이터로 모든 본 업데이트 (필터 없이)
- foreach (var bone in m_skeletonDef.Bones)
- {
- if (!m_boneIdToMappingIndex.TryGetValue(bone.Id, out int mappingIdx))
- continue;
- var mapping = boneMappings[mappingIdx];
- if (!mapping.isMapped || mapping.cachedTransform == null)
- continue;
+ if (!m_boneIdToMappingIndex.TryGetValue(bone.Id, out int mappingIdx))
+ continue;
+ var mapping = boneMappings[mappingIdx];
+ if (!mapping.isMapped || mapping.cachedTransform == null)
+ continue;
- if (mapping.applyRotation && m_snapshotOrientations.TryGetValue(bone.Id, out Quaternion rawOri))
- mapping.cachedTransform.localRotation = rawOri;
- if (mapping.applyPosition && m_snapshotPositions.TryGetValue(bone.Id, out Vector3 rawPos))
- mapping.cachedTransform.localPosition = rawPos;
- }
-
- // Raw 상태에서 IK 포인트 월드 위치/회전 캡처
- foreach (var ikBone in k_IKPointBones)
- {
- Transform t = GetBoneTransform(ikBone);
- if (t != null)
- {
- m_rawWorldPositions[ikBone] = t.position;
- m_rawWorldRotations[ikBone] = t.rotation;
- }
- }
-
- // Pass 2: 필터 적용된 데이터로 덮어쓰기
- foreach (var bone in m_skeletonDef.Bones)
- {
- if (!m_boneIdToMappingIndex.TryGetValue(bone.Id, out int mappingIdx))
- continue;
- var mapping = boneMappings[mappingIdx];
- if (!mapping.isMapped || mapping.cachedTransform == null)
- continue;
-
- if (mapping.applyRotation && m_snapshotOrientations.TryGetValue(bone.Id, out Quaternion finalOri))
- {
- finalOri = ApplyOneEuroOri(bone.Id, finalOri, m_natNetDt);
- mapping.cachedTransform.localRotation = finalOri;
- }
- if (mapping.applyPosition && m_snapshotPositions.TryGetValue(bone.Id, out Vector3 finalPos))
- {
- finalPos = ApplyOneEuroPos(bone.Id, finalPos, m_natNetDt);
- mapping.cachedTransform.localPosition = finalPos;
- }
- }
+ if (mapping.applyRotation && m_snapshotOrientations.TryGetValue(bone.Id, out Quaternion finalOri))
+ mapping.cachedTransform.localRotation = finalOri;
+ if (mapping.applyPosition && m_snapshotPositions.TryGetValue(bone.Id, out Vector3 finalPos))
+ mapping.cachedTransform.localPosition = finalPos;
}
- else
+
+ // IK 포인트 월드 위치/회전 캡처
+ foreach (var ikBone in k_IKPointBones)
{
- // 필터 비활성: single-pass, raw = filtered
- foreach (var bone in m_skeletonDef.Bones)
+ Transform t = GetBoneTransform(ikBone);
+ if (t != null)
{
- if (!m_boneIdToMappingIndex.TryGetValue(bone.Id, out int mappingIdx))
- continue;
- var mapping = boneMappings[mappingIdx];
- if (!mapping.isMapped || mapping.cachedTransform == null)
- continue;
-
- if (mapping.applyRotation && m_snapshotOrientations.TryGetValue(bone.Id, out Quaternion finalOri))
- mapping.cachedTransform.localRotation = finalOri;
- if (mapping.applyPosition && m_snapshotPositions.TryGetValue(bone.Id, out Vector3 finalPos))
- mapping.cachedTransform.localPosition = finalPos;
- }
-
- // 필터 없으면 현재 Transform 위치/회전이 곧 raw
- foreach (var ikBone in k_IKPointBones)
- {
- Transform t = GetBoneTransform(ikBone);
- if (t != null)
- {
- m_rawWorldPositions[ikBone] = t.position;
- m_rawWorldRotations[ikBone] = t.rotation;
- }
+ m_rawWorldPositions[ikBone] = t.position;
+ m_rawWorldRotations[ikBone] = t.rotation;
}
}
@@ -577,7 +460,6 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
private void RebuildBoneIdMapping()
{
m_boneIdToMappingIndex.Clear();
- m_filterStates.Clear();
m_hasLastFrameTimestamp = false;
if (m_skeletonDef == null) return;
@@ -724,79 +606,6 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
}
}
- // ── 1€ Filter (One Euro Filter) ──────────────────────────────────────────
- // 참고: Géry Casiez et al., "1€ Filter: A Simple Speed-based Low-pass Filter", CHI 2012
- // 속도가 빠를수록 cutoff 상승 → 지연 감소, 속도가 느릴수록 cutoff = minCutoff → 노이즈 제거
-
- // dt초 간격에서의 1차 LP 필터 alpha
- private static float OE_Alpha(float cutoffHz, float dt)
- {
- float r = 2f * Mathf.PI * cutoffHz * dt;
- return r / (r + 1f);
- }
-
- private Quaternion ApplyOneEuroOri(int boneId, Quaternion raw, float dt)
- {
- const float k_DCutoff = 1f; // 도함수 필터의 고정 cutoff (Hz)
-
- if (!m_filterStates.TryGetValue(boneId, out BoneFilterState s) || !s.initialized)
- {
- m_filterStates[boneId] = new BoneFilterState
- {
- prevOri = raw, dOriMag = 0f,
- prevPos = Vector3.zero, dPosMag = 0f,
- initialized = true
- };
- return raw;
- }
-
- // 각속도(rad/s) 추정
- float angleDeg = Quaternion.Angle(s.prevOri, raw);
- float speed = angleDeg * Mathf.Deg2Rad / Mathf.Max(dt, 1e-4f);
-
- // 도함수를 별도 LP로 스무딩
- float dAlpha = OE_Alpha(k_DCutoff, dt);
- float filtDeriv = s.dOriMag + dAlpha * (speed - s.dOriMag);
-
- // 적응 cutoff: 빠르면 cutoff 상승, 단 maxCutoff 이상은 항상 제거
- float cutoff = Mathf.Min(filterMinCutoff + filterBeta * filtDeriv, filterMaxCutoff);
- float alpha = OE_Alpha(cutoff, dt);
- Quaternion filtered = Quaternion.Slerp(s.prevOri, raw, alpha);
-
- s.prevOri = filtered;
- s.dOriMag = filtDeriv;
- m_filterStates[boneId] = s;
- return filtered;
- }
-
- private Vector3 ApplyOneEuroPos(int boneId, Vector3 raw, float dt)
- {
- const float k_DCutoff = 1f;
-
- if (!m_filterStates.TryGetValue(boneId, out BoneFilterState s) || !s.initialized)
- {
- // 회전 필터가 이미 초기화했을 수 있으므로 위치만 패치
- s.prevPos = raw;
- s.dPosMag = 0f;
- s.initialized = true;
- m_filterStates[boneId] = s;
- return raw;
- }
-
- float speed = (raw - s.prevPos).magnitude / Mathf.Max(dt, 1e-4f);
- float dAlpha = OE_Alpha(k_DCutoff, dt);
- float filtDeriv = s.dPosMag + dAlpha * (speed - s.dPosMag);
-
- float cutoff = Mathf.Min(filterMinCutoff + filterBeta * filtDeriv, filterMaxCutoff);
- float alpha = OE_Alpha(cutoff, dt);
- Vector3 filtered = Vector3.Lerp(s.prevPos, raw, alpha);
-
- s.prevPos = filtered;
- s.dPosMag = filtDeriv;
- m_filterStates[boneId] = s;
- return filtered;
- }
-
#region 외부 접근용 헬퍼
// HumanBodyBones → OptiTrack 본 이름 매핑 (Humanoid 호환 레이어)