using System; using System.Collections.Generic; using UnityEngine; using System.Collections; [System.Serializable] public class OptiTrackBoneMapping { public string optiTrackBoneName; // OptiTrack 본 이름 (예: "Hip", "Ab") public string fbxNodeName; // FBX 노드 이름 (예: "001_Hips", "001_Spine") [HideInInspector] public Transform cachedTransform; // 런타임 캐시 public bool applyPosition = true; public bool applyRotation = true; public bool isMapped = false; // 매핑 성공 여부 } [DefaultExecutionOrder(-100)] public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour { [Header("OptiTrack 설정")] [Tooltip("OptiTrackStreamingClient가 포함된 오브젝트")] public OptitrackStreamingClient StreamingClient; [Tooltip("Motive의 스켈레톤 에셋 이름")] public string SkeletonAssetName = "Skeleton1"; [Header("본 매핑 (OptiTrack → FBX 노드)")] [Tooltip("자동 매핑 후 수동 조정 가능")] public List boneMappings = new List(); [Header("디버그")] public bool logUnmappedBones = false; private OptitrackSkeletonDefinition m_skeletonDef; private string previousSkeletonName; [HideInInspector] public bool isSkeletonFound = false; // 에디터 디버그용 — 런타임에 Motive에서 실제로 수신된 본 목록 [HideInInspector] public List debugReceivedBoneNames = new List(); [HideInInspector] public int debugReceivedBoneCount = 0; private float updateInterval = 0.1f; // 본 ID → 매핑 인덱스 (빠른 룩업) private Dictionary m_boneIdToMappingIndex = new Dictionary(); // 본 이름 → Transform 캐시 (전체 하이어라키) private Dictionary m_allTransforms = new Dictionary(); // torn read 방지용 스냅샷 버퍼 private Dictionary m_snapshotPositions = new Dictionary(); private Dictionary m_snapshotOrientations = new Dictionary(); // OptiTrack 본 이름 → FBX 노드 접미사 기본 매핑 public static readonly Dictionary DefaultOptiToFbxSuffix = new Dictionary { // 몸통 (Motive 5본 스파인 체인) // Motive: Hip → Ab → Spine2 → Spine3 → Spine4 → Chest → Neck → Neck2 → Head // FBX: Hips → Spine → Spine1 → Spine2 → Spine3 → Spine4 → Neck → Neck1 → Head {"Hip", "Hips"}, {"Ab", "Spine"}, {"Spine2", "Spine1"}, {"Spine3", "Spine2"}, {"Spine4", "Spine3"}, {"Chest", "Spine4"}, {"Neck", "Neck"}, {"Neck2", "Neck1"}, {"Head", "Head"}, // 왼쪽 팔 {"LShoulder", "LeftShoulder"}, {"LUArm", "LeftArm"}, {"LFArm", "LeftForeArm"}, {"LHand", "LeftHand"}, // 오른쪽 팔 {"RShoulder", "RightShoulder"}, {"RUArm", "RightArm"}, {"RFArm", "RightForeArm"}, {"RHand", "RightHand"}, // 왼쪽 다리 {"LThigh", "LeftUpLeg"}, {"LShin", "LeftLeg"}, {"LFoot", "LeftFoot"}, {"LToe", "LeftToeBase"}, // 오른쪽 다리 {"RThigh", "RightUpLeg"}, {"RShin", "RightLeg"}, {"RFoot", "RightFoot"}, {"RToe", "RightToeBase"}, // 왼쪽 손가락 {"LThumb1", "LeftHandThumb1"}, {"LThumb2", "LeftHandThumb2"}, {"LThumb3", "LeftHandThumb3"}, {"LIndex1", "LeftHandIndex1"}, {"LIndex2", "LeftHandIndex2"}, {"LIndex3", "LeftHandIndex3"}, {"LMiddle1", "LeftHandMiddle1"}, {"LMiddle2", "LeftHandMiddle2"}, {"LMiddle3", "LeftHandMiddle3"}, {"LRing1", "LeftHandRing1"}, {"LRing2", "LeftHandRing2"}, {"LRing3", "LeftHandRing3"}, {"LPinky1", "LeftHandPinky1"}, {"LPinky2", "LeftHandPinky2"}, {"LPinky3", "LeftHandPinky3"}, // 오른쪽 손가락 {"RThumb1", "RightHandThumb1"}, {"RThumb2", "RightHandThumb2"}, {"RThumb3", "RightHandThumb3"}, {"RIndex1", "RightHandIndex1"}, {"RIndex2", "RightHandIndex2"}, {"RIndex3", "RightHandIndex3"}, {"RMiddle1", "RightHandMiddle1"}, {"RMiddle2", "RightHandMiddle2"}, {"RMiddle3", "RightHandMiddle3"}, {"RRing1", "RightHandRing1"}, {"RRing2", "RightHandRing2"}, {"RRing3", "RightHandRing3"}, {"RPinky1", "RightHandPinky1"}, {"RPinky2", "RightHandPinky2"}, {"RPinky3", "RightHandPinky3"}, }; void Start() { BuildTransformCache(); InitializeStreamingClient(); if (StreamingClient != null) { StreamingClient.RegisterSkeleton(this, this.SkeletonAssetName); } // 에디터에서 세팅한 매핑의 Transform 캐시 갱신 if (boneMappings.Count > 0) { RefreshTransformCache(); } else { Debug.LogWarning("[OptiTrack] 본 매핑이 비어있습니다. Inspector에서 'FBX 분석 → 자동 매핑 생성' 버튼을 눌러주세요.", this); } StartCoroutine(CheckSkeletonConnectionPeriodically()); } void Update() { if (StreamingClient == null) { InitializeStreamingClient(); return; } // 스켈레톤 이름 변경 감지 if (previousSkeletonName != SkeletonAssetName) { StreamingClient.RegisterSkeleton(this, SkeletonAssetName); m_skeletonDef = StreamingClient.GetSkeletonDefinitionByName(SkeletonAssetName); previousSkeletonName = SkeletonAssetName; if (m_skeletonDef != null) RebuildBoneIdMapping(); return; } if (m_skeletonDef == null) { m_skeletonDef = StreamingClient.GetSkeletonDefinitionByName(SkeletonAssetName); if (m_skeletonDef == null) return; RebuildBoneIdMapping(); } // 최신 스켈레톤 상태 OptitrackSkeletonState skelState = StreamingClient.GetLatestSkeletonState(m_skeletonDef.Id); if (skelState == null) return; // torn read 방지 — 스냅샷 복사 m_snapshotPositions.Clear(); m_snapshotOrientations.Clear(); foreach (var kvp in skelState.BonePoses) { m_snapshotPositions[kvp.Key] = kvp.Value.Position; m_snapshotOrientations[kvp.Key] = kvp.Value.Orientation; } // 각 본 업데이트 — Transform 직접 적용 foreach (var bone in m_skeletonDef.Bones) { if (!m_boneIdToMappingIndex.TryGetValue(bone.Id, out int mappingIdx)) continue; var mapping = boneMappings[mappingIdx]; if (!mapping.isMapped || mapping.cachedTransform == null) continue; if (m_snapshotOrientations.TryGetValue(bone.Id, out Quaternion ori)) { if (mapping.applyPosition && m_snapshotPositions.TryGetValue(bone.Id, out Vector3 pos)) { mapping.cachedTransform.localPosition = SnapPosition(pos); } if (mapping.applyRotation) { mapping.cachedTransform.localRotation = SnapQuaternion(ori); } } } } /// /// 하이어라키의 모든 Transform을 이름으로 캐싱 /// private void BuildTransformCache() { m_allTransforms.Clear(); var allChildren = GetComponentsInChildren(true); foreach (var t in allChildren) { if (!m_allTransforms.ContainsKey(t.name)) { m_allTransforms[t.name] = t; } } } /// /// 기존 매핑의 Transform 캐시만 갱신 /// private void RefreshTransformCache() { foreach (var mapping in boneMappings) { if (!string.IsNullOrEmpty(mapping.fbxNodeName) && m_allTransforms.TryGetValue(mapping.fbxNodeName, out Transform t)) { mapping.cachedTransform = t; mapping.isMapped = true; } } } /// /// OptiTrack 본 ID → boneMappings 인덱스 매핑 구축 /// private void RebuildBoneIdMapping() { m_boneIdToMappingIndex.Clear(); debugReceivedBoneNames.Clear(); if (m_skeletonDef == null) return; debugReceivedBoneCount = m_skeletonDef.Bones.Count; var nameToIdx = new Dictionary(); for (int i = 0; i < boneMappings.Count; i++) { if (!nameToIdx.ContainsKey(boneMappings[i].optiTrackBoneName)) nameToIdx[boneMappings[i].optiTrackBoneName] = i; } int matchCount = 0; foreach (var bone in m_skeletonDef.Bones) { string boneName = bone.Name; string optiName = boneName.Contains("_") ? boneName.Substring(boneName.IndexOf('_') + 1) : boneName; // 디버그: Motive에서 수신된 모든 본 기록 string parentInfo = m_skeletonDef.BoneIdToParentIdMap.TryGetValue(bone.Id, out Int32 parentId) ? $"parent={parentId}" : "root"; debugReceivedBoneNames.Add($"[ID:{bone.Id}] {boneName} (suffix: {optiName}, {parentInfo})"); if (nameToIdx.TryGetValue(optiName, out int idx)) { m_boneIdToMappingIndex[bone.Id] = idx; matchCount++; } else if (logUnmappedBones) { Debug.LogWarning($"[OptiTrack] 매핑되지 않은 OptiTrack 본: {boneName} (suffix: {optiName})"); } } Debug.Log($"[OptiTrack] 본 ID 매핑 완료: {matchCount}/{m_skeletonDef.Bones.Count} 매칭"); } private void InitializeStreamingClient() { if (StreamingClient == null) { StreamingClient = FindAnyObjectByType(); if (StreamingClient != null) Debug.Log("OptiTrack Streaming Client를 찾았습니다.", this); } } private IEnumerator CheckSkeletonConnectionPeriodically() { while (true) { if (StreamingClient != null) { m_skeletonDef = StreamingClient.GetSkeletonDefinitionByName(SkeletonAssetName); if (m_skeletonDef != null) { OptitrackSkeletonState skelState = StreamingClient.GetLatestSkeletonState(m_skeletonDef.Id); bool wasFound = isSkeletonFound; isSkeletonFound = (skelState != null); if (isSkeletonFound && !wasFound) { StreamingClient.RegisterSkeleton(this, SkeletonAssetName); previousSkeletonName = SkeletonAssetName; RebuildBoneIdMapping(); Debug.Log($"[OptiTrack] 스켈레톤 '{SkeletonAssetName}' 연결 성공"); } } else { isSkeletonFound = false; } } yield return new WaitForSeconds(updateInterval); } } // 소수점 4자리 이하 제거 (0.0001m = 0.1mm 미만 노이즈 제거) private const float POS_PRECISION = 10000f; private static Vector3 SnapPosition(Vector3 v) { return new Vector3( Mathf.Round(v.x * POS_PRECISION) / POS_PRECISION, Mathf.Round(v.y * POS_PRECISION) / POS_PRECISION, Mathf.Round(v.z * POS_PRECISION) / POS_PRECISION ); } // 쿼터니언 소수점 5자리 이하 제거 + 정규화 private const float ROT_PRECISION = 100000f; private static Quaternion SnapQuaternion(Quaternion q) { return new Quaternion( Mathf.Round(q.x * ROT_PRECISION) / ROT_PRECISION, Mathf.Round(q.y * ROT_PRECISION) / ROT_PRECISION, Mathf.Round(q.z * ROT_PRECISION) / ROT_PRECISION, Mathf.Round(q.w * ROT_PRECISION) / ROT_PRECISION ).normalized; } #region 외부 접근용 헬퍼 // HumanBodyBones → OptiTrack 본 이름 매핑 (Humanoid 호환 레이어) public static readonly Dictionary HumanBoneToOptiName = new Dictionary { { HumanBodyBones.Hips, "Hip" }, { HumanBodyBones.Head, "Head" }, // 스파인/넥은 분배 처리되므로 1:1 매핑은 참고용 { HumanBodyBones.Spine, "Ab" }, { HumanBodyBones.Chest, "Chest" }, { HumanBodyBones.Neck, "Neck" }, // 왼쪽 팔 { HumanBodyBones.LeftShoulder, "LShoulder" }, { HumanBodyBones.LeftUpperArm, "LUArm" }, { HumanBodyBones.LeftLowerArm, "LFArm" }, { HumanBodyBones.LeftHand, "LHand" }, // 오른쪽 팔 { HumanBodyBones.RightShoulder, "RShoulder" }, { HumanBodyBones.RightUpperArm, "RUArm" }, { HumanBodyBones.RightLowerArm, "RFArm" }, { HumanBodyBones.RightHand, "RHand" }, // 왼쪽 다리 { HumanBodyBones.LeftUpperLeg, "LThigh" }, { HumanBodyBones.LeftLowerLeg, "LShin" }, { HumanBodyBones.LeftFoot, "LFoot" }, { HumanBodyBones.LeftToes, "LToe" }, // 오른쪽 다리 { HumanBodyBones.RightUpperLeg, "RThigh" }, { HumanBodyBones.RightLowerLeg, "RShin" }, { HumanBodyBones.RightFoot, "RFoot" }, { HumanBodyBones.RightToes, "RToe" }, // 왼쪽 손가락 { HumanBodyBones.LeftThumbProximal, "LThumb1" }, { HumanBodyBones.LeftThumbIntermediate, "LThumb2" }, { HumanBodyBones.LeftThumbDistal, "LThumb3" }, { HumanBodyBones.LeftIndexProximal, "LIndex1" }, { HumanBodyBones.LeftIndexIntermediate, "LIndex2" }, { HumanBodyBones.LeftIndexDistal, "LIndex3" }, { HumanBodyBones.LeftMiddleProximal, "LMiddle1" }, { HumanBodyBones.LeftMiddleIntermediate, "LMiddle2" }, { HumanBodyBones.LeftMiddleDistal, "LMiddle3" }, { HumanBodyBones.LeftRingProximal, "LRing1" }, { HumanBodyBones.LeftRingIntermediate, "LRing2" }, { HumanBodyBones.LeftRingDistal, "LRing3" }, { HumanBodyBones.LeftLittleProximal, "LPinky1" }, { HumanBodyBones.LeftLittleIntermediate, "LPinky2" }, { HumanBodyBones.LeftLittleDistal, "LPinky3" }, // 오른쪽 손가락 { HumanBodyBones.RightThumbProximal, "RThumb1" }, { HumanBodyBones.RightThumbIntermediate, "RThumb2" }, { HumanBodyBones.RightThumbDistal, "RThumb3" }, { HumanBodyBones.RightIndexProximal, "RIndex1" }, { HumanBodyBones.RightIndexIntermediate, "RIndex2" }, { HumanBodyBones.RightIndexDistal, "RIndex3" }, { HumanBodyBones.RightMiddleProximal, "RMiddle1" }, { HumanBodyBones.RightMiddleIntermediate, "RMiddle2" }, { HumanBodyBones.RightMiddleDistal, "RMiddle3" }, { HumanBodyBones.RightRingProximal, "RRing1" }, { HumanBodyBones.RightRingIntermediate, "RRing2" }, { HumanBodyBones.RightRingDistal, "RRing3" }, { HumanBodyBones.RightLittleProximal, "RPinky1" }, { HumanBodyBones.RightLittleIntermediate, "RPinky2" }, { HumanBodyBones.RightLittleDistal, "RPinky3" }, }; /// /// HumanBodyBones enum으로 OptiTrack 매핑된 Transform 반환 /// (Humanoid 호환 레이어 — sourceAnimator.GetBoneTransform() 대체) /// public Transform GetBoneTransform(HumanBodyBones bone) { if (HumanBoneToOptiName.TryGetValue(bone, out string optiName)) return GetMappedTransform(optiName); return null; } // 스파인 체인 OptiTrack 이름 (Hips 제외, 순서대로) public static readonly string[] SpineChainOptiNames = { "Ab", "Spine2", "Spine3", "Spine4", "Chest" }; // 넥 체인 public static readonly string[] NeckChainOptiNames = { "Neck", "Neck2" }; /// /// OptiTrack 본 이름으로 매핑된 Transform 반환 /// public Transform GetMappedTransform(string optiTrackBoneName) { foreach (var mapping in boneMappings) { if (mapping.optiTrackBoneName == optiTrackBoneName && mapping.isMapped) return mapping.cachedTransform; } return null; } /// /// 스파인 체인의 모든 Transform을 순서대로 반환 (Ab → Spine2 → Spine3 → Spine4 → Chest) /// public List GetSpineChainTransforms() { var chain = new List(); foreach (var name in SpineChainOptiNames) { Transform t = GetMappedTransform(name); if (t != null) chain.Add(t); } return chain; } /// /// 넥 체인의 모든 Transform을 순서대로 반환 /// public List GetNeckChainTransforms() { var chain = new List(); foreach (var name in NeckChainOptiNames) { Transform t = GetMappedTransform(name); if (t != null) chain.Add(t); } return chain; } /// /// 체인의 누적 로컬 회전을 계산 (각 본의 localRotation을 순서대로 곱함) /// public static Quaternion ComputeChainRotation(List chain) { Quaternion total = Quaternion.identity; foreach (var t in chain) { total *= t.localRotation; } return total; } /// /// 초기 포즈(T-포즈) 캐싱 — Humanoid가 아니므로 직접 캐싱 /// [HideInInspector] public Dictionary restLocalRotations = new Dictionary(); [HideInInspector] public Dictionary restLocalPositions = new Dictionary(); [HideInInspector] public bool isRestPoseCached = false; /// /// 현재 포즈를 기준 포즈(T-포즈)로 캐싱 /// public void CacheRestPose() { restLocalRotations.Clear(); restLocalPositions.Clear(); foreach (var mapping in boneMappings) { if (mapping.isMapped && mapping.cachedTransform != null) { restLocalRotations[mapping.optiTrackBoneName] = mapping.cachedTransform.localRotation; restLocalPositions[mapping.optiTrackBoneName] = mapping.cachedTransform.localPosition; } } isRestPoseCached = true; Debug.Log($"[OptiTrack] 기준 포즈 캐싱 완료: {restLocalRotations.Count}개 본"); } /// /// 캐싱된 기준 포즈로 복원 (T-포즈 복원) /// public void RestoreRestPose() { if (!isRestPoseCached) return; foreach (var mapping in boneMappings) { if (mapping.isMapped && mapping.cachedTransform != null) { if (restLocalRotations.TryGetValue(mapping.optiTrackBoneName, out Quaternion rot)) mapping.cachedTransform.localRotation = rot; if (restLocalPositions.TryGetValue(mapping.optiTrackBoneName, out Vector3 pos)) mapping.cachedTransform.localPosition = pos; } } } /// /// 특정 본의 기준 포즈 로컬 회전 반환 /// public Quaternion GetRestLocalRotation(string optiTrackBoneName) { if (restLocalRotations.TryGetValue(optiTrackBoneName, out Quaternion rot)) return rot; return Quaternion.identity; } #endregion }