Remove: OptitrackSkeletonAnimator_Mingle 1€ 필터 제거
- FilterStrength 열거형 및 관련 필드/메서드 전체 삭제 - Update() two-pass 구조를 single-pass로 단순화 - 에디터 인스펙터의 필터 강도 UI 제거 - 프레임 보간 및 어깨 증폭 기능은 유지 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7d32cc8b8c
commit
870dee9447
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 런타임에서 필터 강도를 변경합니다. StreamDeck/핫키 등에서 호출.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 프리셋에서 다음 프리셋으로 순환. 버튼 하나로 Off→Low→Medium→High→Custom→Off...
|
||||
/// </summary>
|
||||
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<Int32, BoneFilterState> m_filterStates = new Dictionary<Int32, BoneFilterState>();
|
||||
|
||||
// 프레임 보간용 이중 버퍼 (prev/curr OptiTrack 프레임)
|
||||
private Dictionary<Int32, Vector3> m_interpPrevPos = new Dictionary<Int32, Vector3>();
|
||||
private Dictionary<Int32, Quaternion> m_interpPrevOri = new Dictionary<Int32, Quaternion>();
|
||||
@ -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 호환 레이어)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user