Streamingle_URP/Assets/External/Ifacialmocap/UnityRecieve_FACEMOTION3D_and_iFacialMocap.cs

630 lines
16 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; // 좌우 반전 모드 설정
[Header("BlendShape 보정")]
[Tooltip("세로 방향 BlendShape 보정 비율 (입 벌림, 눈 위아래 등)")]
[Range(0.5f, 2.0f)]
public float verticalScale = 1.28f;
[Tooltip("가로 방향 BlendShape 보정 비율 (입 좌우, 볼 등)")]
[Range(0.5f, 2.0f)]
public float horizontalScale = 1.0f;
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;
// 성능 최적화를 위한 캐시
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[] { ',' };
// 세로 방향 BlendShape (위아래 움직임) - verticalScale 적용
private static readonly HashSet<string> VerticalBlendShapes = new HashSet<string>(StringComparer.OrdinalIgnoreCase) {
// 입 세로
"jawopen", "mouthclose", "mouthfunnel", "mouthpucker",
// 눈 위아래
"eyelookupleft", "eyelookupright", "eyelookdownleft", "eyelookdownright",
"eyewideleft", "eyewideright", "eyesquintleft", "eyesquintright",
"eyeblinkleft", "eyeblinkright",
// 눈썹 위아래
"browinnerup", "browdownleft", "browdownright", "browouterupright", "browouterupleft"
};
// 가로 방향 BlendShape (좌우 움직임) - horizontalScale 적용
private static readonly HashSet<string> HorizontalBlendShapes = new HashSet<string>(StringComparer.OrdinalIgnoreCase) {
// 입 좌우
"mouthsmileleft", "mouthsmileright", "mouthfrownleft", "mouthfrownright",
"mouthdimpleleft", "mouthdimpleright", "mouthstretchleft", "mouthstretchright",
"mouthpressleft", "mouthpressright", "mouthlowerdownleft", "mouthlowerdownright",
"mouthupperupleft", "mouthupperupright",
"mouthleft", "mouthright",
// 눈 좌우
"eyelookinleft", "eyelookinright", "eyelookoutleft", "eyelookoutright",
// 볼/코
"cheekpuff", "cheeksquintleft", "cheeksquintright",
"nosesneerleft", "nosesneerright"
};
// 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();
// 세로/가로 방향에 따라 보정 비율 적용
weight = ApplyScaleCorrection(normalizedName, weight);
if (blendShapeCache.TryGetValue(normalizedName, out List<BlendShapeMapping> mappings))
{
// 캐시에서 찾은 모든 매핑에 대해 weight 설정
foreach (var mapping in mappings)
{
if (mapping.renderer != null)
{
mapping.renderer.SetBlendShapeWeight(mapping.index, weight);
}
}
}
}
// 세로/가로 방향에 따른 BlendShape 보정 비율 적용
// 곡선 보정으로 0~100 범위를 유지하면서 민감도 조절
float ApplyScaleCorrection(string normalizedName, float weight)
{
float scale = 1.0f;
if (VerticalBlendShapes.Contains(normalizedName))
{
scale = verticalScale;
}
else if (HorizontalBlendShapes.Contains(normalizedName))
{
scale = horizontalScale;
}
if (Mathf.Approximately(scale, 1.0f))
return weight;
// 0~100 범위 유지하면서 곡선 보정
// scale > 1: 작은 입력에서 더 빠르게 반응 (눌린 화면 보정)
// scale < 1: 작은 입력에서 더 느리게 반응
float normalized = weight / 100f;
float corrected = Mathf.Pow(normalized, 1f / scale);
return corrected * 100f;
}
// 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축 회전 반전
}
switch (strArray2[0])
{
case "head" when headBone != null:
headBone.localRotation = Quaternion.Euler(x, y, -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; // X축 위치 반전
}
headPositionObject.localPosition = new Vector3(posX, posY, posZ);
}
break;
case "rightEye" when rightEyeBone != null:
rightEyeBone.localRotation = Quaternion.Euler(x, y, z);
break;
case "leftEye" when leftEyeBone != null:
leftEyeBone.localRotation = Quaternion.Euler(x, y, 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}");
}
}
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);
}
}
}