Fix : 페이셜 고도화

This commit is contained in:
qsxft258@gmail.com 2026-02-01 12:23:15 +09:00
parent 76906a6380
commit e1d9e2f88c

View File

@ -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<string, float> prevBlendShapeValues = new Dictionary<string, float>();
@ -51,6 +57,26 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour
private Vector3 prevHeadPosition = Vector3.zero;
private bool hasFirstFrame = false;
// 연속 스파이크 추적 (같은 방향으로 연속이면 실제 움직임)
private Dictionary<string, int> blendShapeSpikeCount = new Dictionary<string, int>();
private Dictionary<string, float> blendShapeSpikeDirection = new Dictionary<string, float>();
private Dictionary<string, int> boneSpikeCount = new Dictionary<string, int>();
// 빠르게 변하는 BlendShape 목록 (눈 깜빡임, 입 등)
private static readonly HashSet<string> FastBlendShapes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"eyeblinkleft", "eyeblinkright",
"eyesquintleft", "eyesquintright",
"eyewideleft", "eyewideright",
"jawopen", "mouthclose",
"mouthfunnel", "mouthpucker",
"mouthsmileright", "mouthsmileleft",
"mouthfrownright", "mouthfrownleft",
};
// 프레임레이트 독립 스무딩을 위한 기준 FPS
private const float ReferenceFPS = 60f;
// 성능 최적화를 위한 캐시
private Dictionary<string, List<BlendShapeMapping>> blendShapeCache;
private readonly char[] splitEquals = new char[] { '=' };
@ -561,33 +587,87 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour
}
/// <summary>
/// BlendShape 값 필터링: 스파이크 제거 + EMA 스무딩
/// 프레임레이트 독립적 EMA 계수 계산
/// </summary>
float GetFrameIndependentSmoothing()
{
float dt = Time.deltaTime;
if (dt <= 0f) return smoothingFactor;
// 기준 60fps에서의 smoothingFactor를 현재 dt에 맞게 보정
return 1f - Mathf.Pow(1f - smoothingFactor, dt * ReferenceFPS);
}
/// <summary>
/// BlendShape 값 필터링: 연속 스파이크 판별 + 카테고리별 임계값 + 프레임독립 EMA
/// </summary>
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;
}
/// <summary>
/// 본 회전 필터링: 스파이크 제거 + EMA 스무딩
/// 본 회전 필터링: 연속 스파이크 판별 + 프레임독립 EMA
/// </summary>
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;
}
// EMA 스무딩
Vector3 smoothed = Vector3.Lerp(rawRotation, prevRot, smoothingFactor);
Vector3 clamped = Vector3.MoveTowards(prevRot, rawRotation, maxRotationDelta);
prevBoneRotations[boneName] = clamped;
return clamped;
}
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;
}