diff --git a/Assets/External/Ifacialmocap/UnityRecieve_FACEMOTION3D_and_iFacialMocap.cs b/Assets/External/Ifacialmocap/UnityRecieve_FACEMOTION3D_and_iFacialMocap.cs index a4221fca..1ce7b18e 100644 --- a/Assets/External/Ifacialmocap/UnityRecieve_FACEMOTION3D_and_iFacialMocap.cs +++ b/Assets/External/Ifacialmocap/UnityRecieve_FACEMOTION3D_and_iFacialMocap.cs @@ -35,7 +35,7 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour [Header("Data Filtering")] [Tooltip("데이터 스무딩/필터링 활성화")] public bool enableFiltering = true; - [Tooltip("스무딩 강도 (0=필터없음, 1=최대 스무딩)")] + [Tooltip("스무딩 강도 (0=필터없음, 1=최대 스무딩). 프레임레이트 독립적으로 동작")] [Range(0f, 0.95f)] public float smoothingFactor = 0.5f; [Tooltip("프레임 간 최대 허용 변화량 (BlendShape, 0~100 스케일)")] @@ -44,6 +44,12 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour [Tooltip("프레임 간 최대 허용 회전 변화량 (도)")] [Range(1f, 90f)] public float maxRotationDelta = 25f; + [Tooltip("눈 깜빡임 등 빠른 BlendShape의 임계값 배수")] + [Range(1f, 3f)] + public float fastBlendShapeMultiplier = 2.0f; + [Tooltip("스파이크 판정 전 허용할 연속 프레임 수 (연속이면 실제 움직임으로 판단)")] + [Range(1, 5)] + public int spikeToleranceFrames = 2; // 필터링용 이전 값 저장 private Dictionary prevBlendShapeValues = new Dictionary(); @@ -51,6 +57,26 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour private Vector3 prevHeadPosition = Vector3.zero; private bool hasFirstFrame = false; + // 연속 스파이크 추적 (같은 방향으로 연속이면 실제 움직임) + private Dictionary blendShapeSpikeCount = new Dictionary(); + private Dictionary blendShapeSpikeDirection = new Dictionary(); + private Dictionary boneSpikeCount = new Dictionary(); + + // 빠르게 변하는 BlendShape 목록 (눈 깜빡임, 입 등) + private static readonly HashSet FastBlendShapes = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "eyeblinkleft", "eyeblinkright", + "eyesquintleft", "eyesquintright", + "eyewideleft", "eyewideright", + "jawopen", "mouthclose", + "mouthfunnel", "mouthpucker", + "mouthsmileright", "mouthsmileleft", + "mouthfrownright", "mouthfrownleft", + }; + + // 프레임레이트 독립 스무딩을 위한 기준 FPS + private const float ReferenceFPS = 60f; + // 성능 최적화를 위한 캐시 private Dictionary> blendShapeCache; private readonly char[] splitEquals = new char[] { '=' }; @@ -561,33 +587,87 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour } /// - /// BlendShape 값 필터링: 스파이크 제거 + EMA 스무딩 + /// 프레임레이트 독립적 EMA 계수 계산 + /// + float GetFrameIndependentSmoothing() + { + float dt = Time.deltaTime; + if (dt <= 0f) return smoothingFactor; + // 기준 60fps에서의 smoothingFactor를 현재 dt에 맞게 보정 + return 1f - Mathf.Pow(1f - smoothingFactor, dt * ReferenceFPS); + } + + /// + /// BlendShape 값 필터링: 연속 스파이크 판별 + 카테고리별 임계값 + 프레임독립 EMA /// float FilterBlendShapeValue(string name, float rawValue) { if (prevBlendShapeValues.TryGetValue(name, out float prevValue)) { - float delta = Mathf.Abs(rawValue - prevValue); + float diff = rawValue - prevValue; + float delta = Mathf.Abs(diff); - // 스파이크 감지: 변화량이 임계값 초과 시 이전 값 유지 - if (delta > maxBlendShapeDelta) + // 빠르게 변하는 BlendShape는 임계값을 높여줌 + float threshold = maxBlendShapeDelta; + if (FastBlendShapes.Contains(name)) { - return prevValue; + threshold *= fastBlendShapeMultiplier; } - // EMA 스무딩 적용 - float smoothed = Mathf.Lerp(rawValue, prevValue, smoothingFactor); + // 스파이크 감지 + if (delta > threshold) + { + // 연속 스파이크 추적: 같은 방향이면 카운트 증가 + float prevDir = 0f; + blendShapeSpikeDirection.TryGetValue(name, out prevDir); + bool sameDirection = (diff > 0 && prevDir > 0) || (diff < 0 && prevDir < 0); + + int count = 0; + blendShapeSpikeCount.TryGetValue(name, out count); + + if (sameDirection) + { + count++; + } + else + { + count = 1; + } + + blendShapeSpikeCount[name] = count; + blendShapeSpikeDirection[name] = diff; + + // 연속 프레임 이상 같은 방향이면 실제 움직임으로 판단 → 통과 + if (count >= spikeToleranceFrames) + { + blendShapeSpikeCount[name] = 0; + prevBlendShapeValues[name] = rawValue; + return rawValue; + } + + // 단발성 스파이크 → 허용량만큼만 이동 + float clamped = prevValue + Mathf.Clamp(diff, -threshold, threshold); + prevBlendShapeValues[name] = clamped; + return clamped; + } + + // 정상 범위 → 스파이크 카운터 리셋 + blendShapeSpikeCount[name] = 0; + + // 프레임레이트 독립 EMA 스무딩 + float alpha = GetFrameIndependentSmoothing(); + float smoothed = Mathf.Lerp(rawValue, prevValue, alpha); prevBlendShapeValues[name] = smoothed; return smoothed; } - // 첫 프레임은 그대로 저장 + // 첫 프레임 prevBlendShapeValues[name] = rawValue; return rawValue; } /// - /// 본 회전 필터링: 스파이크 제거 + EMA 스무딩 + /// 본 회전 필터링: 연속 스파이크 판별 + 프레임독립 EMA /// Vector3 FilterBoneRotation(string boneName, Vector3 rawRotation) { @@ -595,14 +675,30 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour { float delta = Vector3.Distance(rawRotation, prevRot); - // 스파이크 감지 if (delta > maxRotationDelta) { - return prevRot; + 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; } - // EMA 스무딩 - Vector3 smoothed = Vector3.Lerp(rawRotation, prevRot, smoothingFactor); + boneSpikeCount[boneName] = 0; + + float alpha = GetFrameIndependentSmoothing(); + Vector3 smoothed = Vector3.Lerp(rawRotation, prevRot, alpha); prevBoneRotations[boneName] = smoothed; return smoothed; } @@ -618,15 +714,18 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour { if (hasFirstFrame) { + float maxPosDelta = maxRotationDelta * 0.01f; float delta = Vector3.Distance(rawPos, prevHeadPosition); - // 위치 스파이크 감지 (0.1 단위 기준) - if (delta > maxRotationDelta * 0.01f) + if (delta > maxPosDelta) { - return prevHeadPosition; + Vector3 clamped = Vector3.MoveTowards(prevHeadPosition, rawPos, maxPosDelta); + prevHeadPosition = clamped; + return clamped; } - Vector3 smoothed = Vector3.Lerp(rawPos, prevHeadPosition, smoothingFactor); + float alpha = GetFrameIndependentSmoothing(); + Vector3 smoothed = Vector3.Lerp(rawPos, prevHeadPosition, alpha); prevHeadPosition = smoothed; return smoothed; }