From 870dee94478972405862078a6a6c70beab46dee7 Mon Sep 17 00:00:00 2001 From: "qsxft258@gmail.com" Date: Tue, 14 Apr 2026 23:25:55 +0900 Subject: [PATCH] =?UTF-8?q?Remove:=20OptitrackSkeletonAnimator=5FMingle=20?= =?UTF-8?q?1=E2=82=AC=20=ED=95=84=ED=84=B0=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FilterStrength 열거형 및 관련 필드/메서드 전체 삭제 - Update() two-pass 구조를 single-pass로 단순화 - 에디터 인스펙터의 필터 강도 UI 제거 - 프레임 보간 및 어깨 증폭 기능은 유지 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Editor/OptitrackSkeletonAnimatorEditor.cs | 45 ---- .../OptitrackSkeletonAnimator_Mingle.cs | 229 ++---------------- 2 files changed, 19 insertions(+), 255 deletions(-) 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 호환 레이어)