777 lines
20 KiB
C#
777 lines
20 KiB
C#
using UnityEngine;
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Collections.Generic;
|
|
using System;
|
|
using System.Collections;
|
|
using System.Globalization;
|
|
|
|
public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour
|
|
{
|
|
// broadcast address
|
|
public bool gameStartWithConnect = true;
|
|
public string iOS_IPAddress = "255.255.255.255";
|
|
public bool mirrorMode = false; // 좌우 반전 모드 설정
|
|
private UdpClient client;
|
|
private bool StartFlag = true;
|
|
|
|
//object references
|
|
public SkinnedMeshRenderer[] faceMeshRenderers;
|
|
public Transform headBone;
|
|
public Transform rightEyeBone;
|
|
public Transform leftEyeBone;
|
|
public Transform headPositionObject;
|
|
|
|
private UdpClient udp;
|
|
private Thread thread;
|
|
private volatile bool isThreadRunning = false; // 스레드 실행 상태 플래그
|
|
private string messageString = "";
|
|
private string lastProcessedMessage = ""; // 이전 메시지 저장용
|
|
public int LOCAL_PORT = 49983;
|
|
|
|
// 데이터 필터링 설정
|
|
[Header("Data Filtering")]
|
|
[Tooltip("데이터 스무딩/필터링 활성화")]
|
|
public bool enableFiltering = true;
|
|
[Tooltip("스무딩 강도 (0=필터없음, 1=최대 스무딩). 프레임레이트 독립적으로 동작")]
|
|
[Range(0f, 0.95f)]
|
|
public float smoothingFactor = 0.5f;
|
|
[Tooltip("프레임 간 최대 허용 변화량 (BlendShape, 0~100 스케일)")]
|
|
[Range(1f, 100f)]
|
|
public float maxBlendShapeDelta = 30f;
|
|
[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>();
|
|
private Dictionary<string, Vector3> prevBoneRotations = new Dictionary<string, Vector3>();
|
|
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[] { '=' };
|
|
private readonly char[] splitPipe = new char[] { '|' };
|
|
private readonly char[] splitAnd = new char[] { '&' };
|
|
private readonly char[] splitDash = new char[] { '-' };
|
|
private readonly char[] splitHash = new char[] { '#' };
|
|
private readonly char[] splitComma = new char[] { ',' };
|
|
|
|
// Mirror mode용 정적 매핑 테이블
|
|
private static readonly Dictionary<string, string> EyeMirrorMap = new Dictionary<string, string>() {
|
|
{"eyelookupleft", "EyeLookUpRight"},
|
|
{"eyelookupright", "EyeLookUpLeft"},
|
|
{"eyelookdownleft", "EyeLookDownRight"},
|
|
{"eyelookdownright", "EyeLookDownLeft"},
|
|
{"eyelookinleft", "EyeLookInRight"},
|
|
{"eyelookinright", "EyeLookInLeft"},
|
|
{"eyelookoutleft", "EyeLookOutRight"},
|
|
{"eyelookoutright", "EyeLookOutLeft"},
|
|
{"eyewideleft", "EyeWideRight"},
|
|
{"eyewideright", "EyeWideLeft"},
|
|
{"eyesquintleft", "EyeSquintRight"},
|
|
{"eyesquintright", "EyeSquintLeft"},
|
|
{"eyeblinkleft", "EyeBlinkRight"},
|
|
{"eyeblinkright", "EyeBlinkLeft"}
|
|
};
|
|
|
|
// BlendShape 매핑 정보를 저장하는 구조체
|
|
private struct BlendShapeMapping
|
|
{
|
|
public SkinnedMeshRenderer renderer;
|
|
public int index;
|
|
|
|
public BlendShapeMapping(SkinnedMeshRenderer r, int i)
|
|
{
|
|
renderer = r;
|
|
index = i;
|
|
}
|
|
}
|
|
|
|
// Start is called
|
|
void StartFunction()
|
|
{
|
|
if (StartFlag == true)
|
|
{
|
|
StartFlag = false;
|
|
|
|
FindGameObjectsInsideUnitySettings();
|
|
|
|
// BlendShape 인덱스 캐싱 초기화
|
|
InitializeBlendShapeCache();
|
|
|
|
//Send to iOS
|
|
if (gameStartWithConnect == true)
|
|
{
|
|
Connect_to_iOS_App();
|
|
}
|
|
|
|
//Recieve udp from iOS
|
|
CreateUdpServer();
|
|
}
|
|
}
|
|
|
|
void Start()
|
|
{
|
|
StartFunction();
|
|
}
|
|
|
|
// BlendShape 인덱스를 미리 캐싱하여 매번 검색하지 않도록 함
|
|
void InitializeBlendShapeCache()
|
|
{
|
|
blendShapeCache = new Dictionary<string, List<BlendShapeMapping>>();
|
|
|
|
if (faceMeshRenderers == null) return;
|
|
|
|
foreach (var meshRenderer in faceMeshRenderers)
|
|
{
|
|
if (meshRenderer == null || meshRenderer.sharedMesh == null) continue;
|
|
|
|
for (int i = 0; i < meshRenderer.sharedMesh.blendShapeCount; i++)
|
|
{
|
|
string shapeName = meshRenderer.sharedMesh.GetBlendShapeName(i);
|
|
string normalizedName = shapeName.ToLowerInvariant();
|
|
|
|
// 여러 변형 이름들을 모두 캐싱
|
|
AddToCache(normalizedName, meshRenderer, i);
|
|
|
|
// _L, _R 변환 버전도 캐싱
|
|
if (shapeName.Contains("_L"))
|
|
{
|
|
string leftVariant = shapeName.Replace("_L", "Left");
|
|
AddToCache(leftVariant.ToLowerInvariant(), meshRenderer, i);
|
|
}
|
|
else if (shapeName.Contains("_R"))
|
|
{
|
|
string rightVariant = shapeName.Replace("_R", "Right");
|
|
AddToCache(rightVariant.ToLowerInvariant(), meshRenderer, i);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void AddToCache(string key, SkinnedMeshRenderer renderer, int index)
|
|
{
|
|
if (!blendShapeCache.ContainsKey(key))
|
|
{
|
|
blendShapeCache[key] = new List<BlendShapeMapping>();
|
|
}
|
|
blendShapeCache[key].Add(new BlendShapeMapping(renderer, index));
|
|
}
|
|
|
|
void CreateUdpServer()
|
|
{
|
|
try
|
|
{
|
|
udp = new UdpClient(LOCAL_PORT);
|
|
udp.Client.ReceiveTimeout = 5;
|
|
|
|
isThreadRunning = true;
|
|
thread = new Thread(new ThreadStart(ThreadMethod));
|
|
thread.IsBackground = true; // 백그라운드 스레드로 설정
|
|
thread.Start();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogError($"[iFacialMocap] UDP 서버 생성 실패: {e.Message}");
|
|
}
|
|
}
|
|
|
|
IEnumerator WaitProcess(float WaitTime)
|
|
{
|
|
yield return new WaitForSeconds(WaitTime);
|
|
}
|
|
|
|
void Connect_to_iOS_App()
|
|
{
|
|
try
|
|
{
|
|
//iFacialMocap
|
|
SendMessage_to_iOSapp("iFacialMocap_sahuasouryya9218sauhuiayeta91555dy3719", 49983);
|
|
|
|
//Facemotion3d
|
|
SendMessage_to_iOSapp("FACEMOTION3D_OtherStreaming", 49993);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogError($"[iFacialMocap] iOS 앱 연결 실패: {e.Message}");
|
|
}
|
|
}
|
|
|
|
void StopStreaming_iOS_App()
|
|
{
|
|
SendMessage_to_iOSapp("StopStreaming_FACEMOTION3D", 49993);
|
|
}
|
|
|
|
//iOSアプリに通信開始のメッセージを送信
|
|
//Send a message to the iOS application to start streaming
|
|
void SendMessage_to_iOSapp(string sendMessage, int send_port)
|
|
{
|
|
try
|
|
{
|
|
client = new UdpClient();
|
|
client.Connect(iOS_IPAddress, send_port);
|
|
byte[] dgram = Encoding.UTF8.GetBytes(sendMessage);
|
|
|
|
// 메시지 전송 시도
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
client.Send(dgram, dgram.Length);
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogError($"[iFacialMocap] 메시지 전송 실패: {e.Message}");
|
|
}
|
|
}
|
|
|
|
// Update is called once per frame
|
|
void Update()
|
|
{
|
|
// 메시지가 변경되었을 때만 처리 (성능 최적화)
|
|
if (!string.IsNullOrEmpty(messageString) && messageString != lastProcessedMessage)
|
|
{
|
|
try
|
|
{
|
|
SetAnimation_inside_Unity_settings();
|
|
lastProcessedMessage = messageString;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogWarning($"[iFacialMocap] Animation 처리 중 오류: {e.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
//BlendShapeの設定
|
|
//set blendshapes (캐시 사용으로 최적화)
|
|
void SetBlendShapeWeightFromStrArray(string[] strArray2)
|
|
{
|
|
if (blendShapeCache == null) return;
|
|
|
|
string shapeName = strArray2[0];
|
|
float weight = float.Parse(strArray2[1], CultureInfo.InvariantCulture);
|
|
|
|
// 정규화된 이름으로 캐시 검색
|
|
string normalizedName = NormalizeBlendShapeName(shapeName).ToLowerInvariant();
|
|
|
|
// 필터링 적용
|
|
if (enableFiltering)
|
|
{
|
|
weight = FilterBlendShapeValue(normalizedName, weight);
|
|
}
|
|
|
|
if (blendShapeCache.TryGetValue(normalizedName, out List<BlendShapeMapping> mappings))
|
|
{
|
|
foreach (var mapping in mappings)
|
|
{
|
|
if (mapping.renderer != null)
|
|
{
|
|
mapping.renderer.SetBlendShapeWeight(mapping.index, weight);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// BlendShape 이름 정규화 함수
|
|
string NormalizeBlendShapeName(string name)
|
|
{
|
|
if (name.EndsWith("_L"))
|
|
name = name.Substring(0, name.Length - 2) + "Left";
|
|
else if (name.EndsWith("_R"))
|
|
name = name.Substring(0, name.Length - 2) + "Right";
|
|
|
|
// 카멜케이스화: 언더스코어 기준 분리 후 각 파트 첫 글자 대문자
|
|
string[] parts = name.Split('_');
|
|
for (int i = 0; i < parts.Length; i++)
|
|
{
|
|
if (parts[i].Length > 0)
|
|
parts[i] = char.ToUpper(parts[i][0]) + parts[i].Substring(1);
|
|
}
|
|
return string.Join("", parts);
|
|
}
|
|
|
|
//BlendShapeとボーンの回転の設定
|
|
//set blendshapes & bone rotation (최적화 버전)
|
|
void SetAnimation_inside_Unity_settings()
|
|
{
|
|
try
|
|
{
|
|
string[] strArray1 = messageString.Split(splitEquals, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
if (strArray1.Length >= 2)
|
|
{
|
|
//blendShapes
|
|
string[] blendShapeMessages = strArray1[0].Split(splitPipe, StringSplitOptions.RemoveEmptyEntries);
|
|
foreach (string message in blendShapeMessages)
|
|
{
|
|
if (string.IsNullOrEmpty(message)) continue;
|
|
|
|
string[] strArray2;
|
|
if (message.Contains("&"))
|
|
{
|
|
strArray2 = message.Split(splitAnd, StringSplitOptions.RemoveEmptyEntries);
|
|
}
|
|
else
|
|
{
|
|
strArray2 = message.Split(splitDash, StringSplitOptions.RemoveEmptyEntries);
|
|
}
|
|
|
|
if (strArray2.Length == 2)
|
|
{
|
|
// 이름 정규화 먼저 적용
|
|
strArray2[0] = NormalizeBlendShapeName(strArray2[0]);
|
|
if (mirrorMode)
|
|
{
|
|
string originalShapeName = strArray2[0];
|
|
string shapeNameLower = originalShapeName.ToLowerInvariant();
|
|
|
|
// 정적 Dictionary 사용
|
|
string mirroredName = originalShapeName;
|
|
if (EyeMirrorMap.TryGetValue(shapeNameLower, out string mappedName))
|
|
{
|
|
mirroredName = mappedName;
|
|
}
|
|
else if (originalShapeName.Contains("Right"))
|
|
{
|
|
mirroredName = originalShapeName.Replace("Right", "Left");
|
|
}
|
|
else if (originalShapeName.Contains("Left"))
|
|
{
|
|
mirroredName = originalShapeName.Replace("Left", "Right");
|
|
}
|
|
strArray2[0] = mirroredName;
|
|
}
|
|
SetBlendShapeWeightFromStrArray(strArray2);
|
|
}
|
|
}
|
|
|
|
// 본 회전 처리
|
|
string[] boneMessages = strArray1[1].Split(splitPipe, StringSplitOptions.RemoveEmptyEntries);
|
|
foreach (string message in boneMessages)
|
|
{
|
|
if (string.IsNullOrEmpty(message)) continue;
|
|
|
|
string[] strArray2 = message.Split(splitHash, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
if (strArray2.Length == 2)
|
|
{
|
|
string[] commaList = strArray2[1].Split(splitComma, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
// 파싱 한 번만 수행
|
|
if (commaList.Length < 3) continue;
|
|
|
|
float x = float.Parse(commaList[0], CultureInfo.InvariantCulture);
|
|
float y = float.Parse(commaList[1], CultureInfo.InvariantCulture);
|
|
float z = float.Parse(commaList[2], CultureInfo.InvariantCulture);
|
|
|
|
if (mirrorMode)
|
|
{
|
|
y = -y; // Y축 회전 반전
|
|
}
|
|
|
|
Vector3 rotation = new Vector3(x, y, z);
|
|
|
|
// 본 회전 필터링 적용
|
|
if (enableFiltering)
|
|
{
|
|
rotation = FilterBoneRotation(strArray2[0], rotation);
|
|
}
|
|
|
|
switch (strArray2[0])
|
|
{
|
|
case "head" when headBone != null:
|
|
headBone.localRotation = Quaternion.Euler(rotation.x, rotation.y, -rotation.z);
|
|
|
|
if (headPositionObject != null && commaList.Length >= 6)
|
|
{
|
|
float posX = -float.Parse(commaList[3], CultureInfo.InvariantCulture);
|
|
float posY = float.Parse(commaList[4], CultureInfo.InvariantCulture);
|
|
float posZ = float.Parse(commaList[5], CultureInfo.InvariantCulture);
|
|
|
|
if (mirrorMode)
|
|
{
|
|
posX = -posX;
|
|
}
|
|
|
|
Vector3 newPos = new Vector3(posX, posY, posZ);
|
|
if (enableFiltering)
|
|
{
|
|
newPos = FilterHeadPosition(newPos);
|
|
}
|
|
headPositionObject.localPosition = newPos;
|
|
}
|
|
break;
|
|
|
|
case "rightEye" when rightEyeBone != null:
|
|
rightEyeBone.localRotation = Quaternion.Euler(rotation.x, rotation.y, rotation.z);
|
|
break;
|
|
|
|
case "leftEye" when leftEyeBone != null:
|
|
leftEyeBone.localRotation = Quaternion.Euler(rotation.x, rotation.y, rotation.z);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogWarning($"[iFacialMocap] Animation 설정 중 오류: {e.Message}");
|
|
}
|
|
}
|
|
|
|
void FindGameObjectsInsideUnitySettings()
|
|
{
|
|
// 모든 Transform 참조는 이미 인스펙터에서 할당되므로 추가 초기화가 필요 없음
|
|
}
|
|
|
|
void ThreadMethod()
|
|
{
|
|
while (isThreadRunning)
|
|
{
|
|
try
|
|
{
|
|
IPEndPoint remoteEP = null;
|
|
byte[] data = udp.Receive(ref remoteEP);
|
|
|
|
// 데이터를 받았을 때만 업데이트
|
|
if (data != null && data.Length > 0)
|
|
{
|
|
messageString = Encoding.ASCII.GetString(data);
|
|
}
|
|
}
|
|
catch (SocketException e)
|
|
{
|
|
// 스레드 종료 중이면 로그 생략
|
|
if (!isThreadRunning) break;
|
|
|
|
if (e.SocketErrorCode != SocketError.TimedOut)
|
|
{
|
|
Debug.LogError($"[iFacialMocap] 데이터 수신 오류: {e.Message}");
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
// 스레드 종료 중이면 로그 생략
|
|
if (!isThreadRunning) break;
|
|
|
|
Debug.LogError($"[iFacialMocap] 예상치 못한 오류: {e.Message}");
|
|
}
|
|
|
|
// CPU를 양보하는 Sleep 사용 (5ms 대기)
|
|
// Busy waiting 대신 Thread.Sleep으로 CPU 사용률 감소
|
|
Thread.Sleep(5);
|
|
}
|
|
}
|
|
|
|
|
|
public string GetMessageString()
|
|
{
|
|
return messageString;
|
|
}
|
|
|
|
void OnEnable()
|
|
{
|
|
StartFunction();
|
|
}
|
|
|
|
void OnDisable()
|
|
{
|
|
try
|
|
{
|
|
OnApplicationQuit();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogWarning($"[iFacialMocap] OnDisable 중 오류: {e.Message}");
|
|
}
|
|
}
|
|
|
|
void OnApplicationQuit()
|
|
{
|
|
if (StartFlag == false)
|
|
{
|
|
StartFlag = true;
|
|
StopUDP();
|
|
}
|
|
}
|
|
|
|
|
|
public void StopUDP()
|
|
{
|
|
if (gameStartWithConnect == true)
|
|
{
|
|
StopStreaming_iOS_App();
|
|
}
|
|
|
|
// 안전한 스레드 종료
|
|
isThreadRunning = false;
|
|
|
|
// UDP 종료
|
|
if (udp != null)
|
|
{
|
|
udp.Close();
|
|
udp.Dispose();
|
|
}
|
|
|
|
// 스레드가 종료될 때까지 대기 (최대 100ms)
|
|
if (thread != null && thread.IsAlive)
|
|
{
|
|
thread.Join(100);
|
|
|
|
// 그래도 종료되지 않으면 강제 종료
|
|
if (thread.IsAlive)
|
|
{
|
|
thread.Abort();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 페이셜 모션 캡처 재접속
|
|
/// </summary>
|
|
public void Reconnect()
|
|
{
|
|
Debug.Log("[iFacialMocap] 재접속 시도 중...");
|
|
|
|
try
|
|
{
|
|
// 기존 연결 종료
|
|
StopUDP();
|
|
|
|
// 잠시 대기
|
|
Thread.Sleep(500);
|
|
|
|
// 플래그 리셋
|
|
StartFlag = true;
|
|
|
|
// 재시작
|
|
StartFunction();
|
|
|
|
Debug.Log("[iFacialMocap] 재접속 완료");
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogError($"[iFacialMocap] 재접속 실패: {e.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 프레임레이트 독립적 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 diff = rawValue - prevValue;
|
|
float delta = Mathf.Abs(diff);
|
|
|
|
// 빠르게 변하는 BlendShape는 임계값을 높여줌
|
|
float threshold = maxBlendShapeDelta;
|
|
if (FastBlendShapes.Contains(name))
|
|
{
|
|
threshold *= fastBlendShapeMultiplier;
|
|
}
|
|
|
|
// 스파이크 감지
|
|
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
|
|
/// </summary>
|
|
Vector3 FilterBoneRotation(string boneName, Vector3 rawRotation)
|
|
{
|
|
if (prevBoneRotations.TryGetValue(boneName, out Vector3 prevRot))
|
|
{
|
|
float delta = Vector3.Distance(rawRotation, prevRot);
|
|
|
|
if (delta > maxRotationDelta)
|
|
{
|
|
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;
|
|
}
|
|
|
|
boneSpikeCount[boneName] = 0;
|
|
|
|
float alpha = GetFrameIndependentSmoothing();
|
|
Vector3 smoothed = Vector3.Lerp(rawRotation, prevRot, alpha);
|
|
prevBoneRotations[boneName] = smoothed;
|
|
return smoothed;
|
|
}
|
|
|
|
prevBoneRotations[boneName] = rawRotation;
|
|
return rawRotation;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 머리 위치 필터링
|
|
/// </summary>
|
|
Vector3 FilterHeadPosition(Vector3 rawPos)
|
|
{
|
|
if (hasFirstFrame)
|
|
{
|
|
float maxPosDelta = maxRotationDelta * 0.01f;
|
|
float delta = Vector3.Distance(rawPos, prevHeadPosition);
|
|
|
|
if (delta > maxPosDelta)
|
|
{
|
|
Vector3 clamped = Vector3.MoveTowards(prevHeadPosition, rawPos, maxPosDelta);
|
|
prevHeadPosition = clamped;
|
|
return clamped;
|
|
}
|
|
|
|
float alpha = GetFrameIndependentSmoothing();
|
|
Vector3 smoothed = Vector3.Lerp(rawPos, prevHeadPosition, alpha);
|
|
prevHeadPosition = smoothed;
|
|
return smoothed;
|
|
}
|
|
|
|
hasFirstFrame = true;
|
|
prevHeadPosition = rawPos;
|
|
return rawPos;
|
|
}
|
|
|
|
private bool HasBlendShapes(SkinnedMeshRenderer skin)
|
|
{
|
|
if (!skin.sharedMesh)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (skin.sharedMesh.blendShapeCount <= 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
}
|
|
public static class FM3D_and_iFacialMocap_GetAllChildren
|
|
{
|
|
public static List<GameObject> GetAll(this GameObject obj)
|
|
{
|
|
List<GameObject> allChildren = new List<GameObject>();
|
|
allChildren.Add(obj);
|
|
GetChildren(obj, ref allChildren);
|
|
return allChildren;
|
|
}
|
|
|
|
public static void GetChildren(GameObject obj, ref List<GameObject> allChildren)
|
|
{
|
|
Transform children = obj.GetComponentInChildren<Transform>();
|
|
if (children.childCount == 0)
|
|
{
|
|
return;
|
|
}
|
|
foreach (Transform ob in children)
|
|
{
|
|
allChildren.Add(ob.gameObject);
|
|
GetChildren(ob.gameObject, ref allChildren);
|
|
}
|
|
}
|
|
} |