Streamingle_URP/Assets/External/StreamingleFacial/StreamingleFacialReceiver.cs

835 lines
23 KiB
C#

using UnityEngine;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Collections;
using System.Collections.Generic;
using System;
using System.Globalization;
public class StreamingleFacialReceiver : MonoBehaviour
{
public bool mirrorMode = true; // 좌우 반전 모드 설정
private bool StartFlag = true;
//object references
public SkinnedMeshRenderer[] faceMeshRenderers;
private UdpClient udp;
private Thread thread;
private volatile bool isThreadRunning = false; // 스레드 실행 상태 플래그
private string messageString = "";
private string lastProcessedMessage = ""; // 이전 메시지 저장용
// ── 공유 포트 (마스터 모드) ──
[Header("Shared Port (Master Mode)")]
[Tooltip("활성화 시 UdpMaster를 통해 같은 포트의 데이터를 여러 Receiver가 공유 수신")]
public bool useSharedPort = true;
// 포트 핫스왑 시스템
[Header("Port Hot-Swap")]
[Tooltip("사용 가능한 아이폰 포트 목록")]
public int[] availablePorts = new int[] { 40001, 40002, 40003, 40004, 40005 };
[Tooltip("현재 활성화된 포트 인덱스 (0~4)")]
[Range(0, 4)]
public int activePortIndex = 0;
public int LOCAL_PORT => availablePorts != null && activePortIndex < availablePorts.Length ? availablePorts[activePortIndex] : 49983;
// 필터 모드 설정
public enum FilterMode { None, EMA, OneEuro, MedianOneEuro }
[Header("Data Filtering")]
[Tooltip("필터링 모드: None=필터없음, EMA=스무딩+스파이크, OneEuro=1€ 적응형, Kalman=칼만필터, MedianOneEuro=메디안+1€ 복합")]
public FilterMode filterMode = FilterMode.MedianOneEuro;
// EMA 필터 설정 (기존 호환)
[Header("EMA Filter Settings")]
[Tooltip("스무딩 강도 (0=필터없음, 1=최대 스무딩). 프레임레이트 독립적으로 동작")]
[Range(0f, 0.95f)]
public float smoothingFactor = 0.1f;
[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;
// 1€ Euro Filter 설정
[Header("1€ Euro Filter Settings")]
[Tooltip("최소 cutoff 주파수 (Hz). 낮을수록 정지 시 스무딩 강함, 높을수록 반응 빠름")]
[Range(0.01f, 10f)]
public float euroMinCutoff = 1.0f;
[Tooltip("속도 계수. 높을수록 빠른 움직임에 즉시 반응")]
[Range(0f, 20f)]
public float euroBeta = 0.5f;
[Tooltip("미분 cutoff 주파수 (Hz). 속도 추정의 스무딩. 보통 1.0 유지")]
[Range(0.1f, 5f)]
public float euroDCutoff = 1.0f;
// Median+OneEuro 복합 필터 설정
[Header("Median+Euro Filter Settings")]
[Tooltip("Median 윈도우 크기 (홀수). 스파이크 제거용. 3=최소 지연, 5=일반, 7+=강한 제거")]
[Range(3, 11)]
public int medianWindowSize = 5;
// ── 필터 인스턴스 ──
// Euro 필터
private Dictionary<string, OneEuroFilter> euroFilters = new Dictionary<string, OneEuroFilter>();
private float euroMinCutoffPrev, euroBetaPrev, euroDCutoffPrev;
// EMA 필터링용 이전 값
private Dictionary<string, float> prevBlendShapeValues = new Dictionary<string, float>();
// 연속 스파이크 추적
private Dictionary<string, int> blendShapeSpikeCount = new Dictionary<string, int>();
private Dictionary<string, float> blendShapeSpikeDirection = new Dictionary<string, float>();
// Median 필터 (MedianOneEuro용)
private Dictionary<string, MedianFilter> medianFilters = new Dictionary<string, MedianFilter>();
// MedianOneEuro용 Euro 필터 (별도 인스턴스)
private Dictionary<string, OneEuroFilter> medianEuroFilters = new Dictionary<string, OneEuroFilter>();
private int medianWindowSizePrev;
// 빠르게 변하는 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",
};
// 페이셜 개별 강도 조절
[Header("Facial Intensity")]
[Tooltip("전체 BlendShape 강도 배율 (1.0 = 원본)")]
[Range(0f, 3f)]
public float globalIntensity = 1.0f;
[Tooltip("개별 BlendShape 강도 오버라이드 (이름, 배율). 여기에 없는 항목은 globalIntensity 적용")]
public List<BlendShapeIntensityOverride> blendShapeIntensityOverrides = new List<BlendShapeIntensityOverride>();
[System.Serializable]
public class BlendShapeIntensityOverride
{
[Tooltip("ARKit BlendShape 이름 (예: EyeBlinkLeft, JawOpen, MouthSmileLeft 등)")]
public string blendShapeName;
[Range(0f, 3f)]
public float intensity = 1.0f;
}
// 런타임 빠른 조회용 Dictionary
private Dictionary<string, float> intensityOverrideMap = new Dictionary<string, float>(StringComparer.OrdinalIgnoreCase);
private bool intensityMapDirty = true;
// 프레임레이트 독립 스무딩을 위한 기준 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[] { '-' };
// 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;
}
}
// 재접속 코루틴 중복 방지
private Coroutine reconnectCoroutine;
// ── 마스터 모드에서 호출되는 메서드 ──
/// <summary>
/// UdpMaster가 메시지를 분배할 때 호출. 직접 호출하지 마세요.
/// </summary>
public void SetMessageFromMaster(string msg)
{
messageString = msg;
}
// Start is called
void StartFunction()
{
if (StartFlag == true)
{
StartFlag = false;
FindGameObjectsInsideUnitySettings();
// BlendShape 인덱스 캐싱 초기화
InitializeBlendShapeCache();
if (useSharedPort)
{
// 마스터 모드: UdpMaster에 등록하여 공유 수신
StreamingleFacialUdpMaster.Instance.Register(LOCAL_PORT, this);
}
else
{
// 독립 모드: 자체 UDP 서버 생성
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}");
}
}
void OnValidate()
{
intensityMapDirty = true;
SyncEuroParams();
}
void SyncEuroParams()
{
if (euroFilters != null)
{
foreach (var filter in euroFilters.Values)
filter.UpdateParams(euroMinCutoff, euroBeta, euroDCutoff);
}
if (medianEuroFilters != null)
{
foreach (var filter in medianEuroFilters.Values)
filter.UpdateParams(euroMinCutoff, euroBeta, euroDCutoff);
}
euroMinCutoffPrev = euroMinCutoff;
euroBetaPrev = euroBeta;
euroDCutoffPrev = euroDCutoff;
}
void RebuildIntensityOverrideMap()
{
intensityOverrideMap.Clear();
foreach (var entry in blendShapeIntensityOverrides)
{
if (!string.IsNullOrEmpty(entry.blendShapeName))
{
intensityOverrideMap[entry.blendShapeName] = entry.intensity;
}
}
intensityMapDirty = false;
}
float GetBlendShapeIntensity(string normalizedName)
{
if (intensityOverrideMap.TryGetValue(normalizedName, out float val))
return val * globalIntensity;
return globalIntensity;
}
// Update is called once per frame
void Update()
{
// 강도 오버라이드 맵 갱신 (인스펙터 변경 반영)
if (intensityMapDirty)
{
RebuildIntensityOverrideMap();
}
// 메시지가 변경되었을 때만 처리 (성능 최적화)
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();
// 강도 배율 적용
weight *= GetBlendShapeIntensity(normalizedName);
weight = Mathf.Clamp(weight, 0f, 100f);
// 필터링 적용
if (filterMode != FilterMode.None)
{
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);
}
}
}
}
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 (ObjectDisposedException)
{
// UDP 소켓이 닫힌 경우 (정상 종료 시 발생)
break;
}
catch (Exception e)
{
// 스레드 종료 중이면 로그 생략
if (!isThreadRunning) break;
Debug.LogError($"[iFacialMocap] 예상치 못한 오류: {e.Message}");
}
// CPU를 양보하는 Sleep 사용 (5ms 대기)
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;
if (useSharedPort)
{
// 마스터에서 해제
if (StreamingleFacialUdpMaster.Instance != null)
{
StreamingleFacialUdpMaster.Instance.Unregister(LOCAL_PORT, this);
}
}
else
{
StopUDP();
}
}
}
public void StopUDP()
{
// 안전한 스레드 종료
isThreadRunning = false;
// UDP 종료
if (udp != null)
{
try
{
udp.Close();
udp.Dispose();
}
catch (Exception) { }
udp = null;
}
// 스레드가 종료될 때까지 대기 (최대 300ms)
if (thread != null && thread.IsAlive)
{
thread.Join(300);
thread = null;
}
}
/// <summary>
/// 포트 핫스왑: 지정된 인덱스의 포트로 즉시 전환
/// </summary>
public void SwitchToPort(int portIndex)
{
if (availablePorts == null || portIndex < 0 || portIndex >= availablePorts.Length)
{
Debug.LogError($"[iFacialMocap] 잘못된 포트 인덱스: {portIndex}");
return;
}
int oldPort = LOCAL_PORT;
activePortIndex = portIndex;
Debug.Log($"[iFacialMocap] 포트 전환: {availablePorts[portIndex]}");
if (useSharedPort)
{
// 마스터 모드: 포트 전환 요청
StreamingleFacialUdpMaster.Instance.SwitchPort(oldPort, LOCAL_PORT, this);
}
else
{
Reconnect();
}
}
/// <summary>
/// 페이셜 모션 캡처 재접속 (프리징 없는 코루틴 방식)
/// </summary>
public void Reconnect()
{
if (reconnectCoroutine != null)
{
StopCoroutine(reconnectCoroutine);
}
reconnectCoroutine = StartCoroutine(ReconnectCoroutine());
}
private IEnumerator ReconnectCoroutine()
{
Debug.Log("[iFacialMocap] 재접속 시도 중...");
if (useSharedPort)
{
// 마스터 모드: 해제 후 재등록
StreamingleFacialUdpMaster.Instance.Unregister(LOCAL_PORT, this);
yield return null; // 1프레임 대기
StreamingleFacialUdpMaster.Instance.Register(LOCAL_PORT, this);
}
else
{
// 독립 모드: UDP 종료 후 코루틴으로 대기 (프리징 없음)
StopUDP();
// OS가 포트를 해제할 시간을 코루틴으로 대기 (메인 스레드 블로킹 없음)
yield return new WaitForSeconds(0.5f);
// 플래그 리셋
StartFlag = true;
// 재시작
StartFunction();
}
reconnectCoroutine = null;
Debug.Log("[iFacialMocap] 재접속 완료");
}
/// <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 값 필터링: filterMode에 따라 적절한 필터 적용
/// </summary>
float FilterBlendShapeValue(string name, float rawValue)
{
switch (filterMode)
{
case FilterMode.OneEuro:
return FilterOneEuro(name, rawValue);
case FilterMode.MedianOneEuro:
return FilterMedianOneEuro(name, rawValue);
case FilterMode.EMA:
return FilterEMA(name, rawValue);
default:
return rawValue;
}
}
/// <summary>
/// 1€ Euro Filter: 속도 기반 적응형 cutoff
/// </summary>
float FilterOneEuro(string name, float rawValue)
{
float t = Time.time;
if (!euroFilters.TryGetValue(name, out var filter))
{
filter = new OneEuroFilter(60f, euroMinCutoff, euroBeta, euroDCutoff);
euroFilters[name] = filter;
// 첫 샘플은 필터 초기화용 → 그대로 반환
filter.Filter(rawValue, t);
return rawValue;
}
// 파라미터 변경 감지 → 모든 필터 업데이트
if (euroMinCutoffPrev != euroMinCutoff || euroBetaPrev != euroBeta || euroDCutoffPrev != euroDCutoff)
{
SyncEuroParams();
}
return Mathf.Clamp(filter.Filter(rawValue, t), 0f, 100f);
}
/// <summary>
/// Median + OneEuro 복합 필터:
/// 1단계: Median 필터로 스파이크/이상치 제거
/// 2단계: OneEuro 필터로 적응형 스무딩
/// → 스파이크에 강하면서도 빠른 움직임에 반응하는 최고 품질 필터링
/// </summary>
float FilterMedianOneEuro(string name, float rawValue)
{
// Median 윈도우 크기 변경 감지 → 필터 재생성
if (medianWindowSizePrev != medianWindowSize)
{
medianFilters.Clear();
medianWindowSizePrev = medianWindowSize;
}
// 1단계: Median 필터
if (!medianFilters.TryGetValue(name, out var medianFilter))
{
medianFilter = new MedianFilter(medianWindowSize);
medianFilters[name] = medianFilter;
}
float medianValue = medianFilter.Filter(rawValue);
// 2단계: OneEuro 필터
float t = Time.time;
if (!medianEuroFilters.TryGetValue(name, out var euroFilter))
{
euroFilter = new OneEuroFilter(60f, euroMinCutoff, euroBeta, euroDCutoff);
medianEuroFilters[name] = euroFilter;
euroFilter.Filter(medianValue, t);
return medianValue;
}
// 파라미터 변경 감지
if (euroMinCutoffPrev != euroMinCutoff || euroBetaPrev != euroBeta || euroDCutoffPrev != euroDCutoff)
{
SyncEuroParams();
}
return Mathf.Clamp(euroFilter.Filter(medianValue, t), 0f, 100f);
}
/// <summary>
/// EMA 필터: 연속 스파이크 판별 + 카테고리별 임계값 + 프레임독립 EMA (기존 로직)
/// </summary>
float FilterEMA(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;
}
}
public static class StreamingleFacialReceiverExtensions
{
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);
}
}
}