556 lines
20 KiB
C#
556 lines
20 KiB
C#
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<OptiTrackBoneMapping> boneMappings = new List<OptiTrackBoneMapping>();
|
|
|
|
[Header("디버그")]
|
|
public bool logUnmappedBones = false;
|
|
|
|
private OptitrackSkeletonDefinition m_skeletonDef;
|
|
private string previousSkeletonName;
|
|
|
|
[HideInInspector]
|
|
public bool isSkeletonFound = false;
|
|
|
|
// 에디터 디버그용 — 런타임에 Motive에서 실제로 수신된 본 목록
|
|
[HideInInspector]
|
|
public List<string> debugReceivedBoneNames = new List<string>();
|
|
[HideInInspector]
|
|
public int debugReceivedBoneCount = 0;
|
|
|
|
private float updateInterval = 0.1f;
|
|
|
|
// 본 ID → 매핑 인덱스 (빠른 룩업)
|
|
private Dictionary<Int32, int> m_boneIdToMappingIndex = new Dictionary<Int32, int>();
|
|
// 본 이름 → Transform 캐시 (전체 하이어라키)
|
|
private Dictionary<string, Transform> m_allTransforms = new Dictionary<string, Transform>();
|
|
|
|
// torn read 방지용 스냅샷 버퍼
|
|
private Dictionary<Int32, Vector3> m_snapshotPositions = new Dictionary<Int32, Vector3>();
|
|
private Dictionary<Int32, Quaternion> m_snapshotOrientations = new Dictionary<Int32, Quaternion>();
|
|
|
|
// OptiTrack 본 이름 → FBX 노드 접미사 기본 매핑
|
|
public static readonly Dictionary<string, string> DefaultOptiToFbxSuffix = new Dictionary<string, string>
|
|
{
|
|
// 몸통 (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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 하이어라키의 모든 Transform을 이름으로 캐싱
|
|
/// </summary>
|
|
private void BuildTransformCache()
|
|
{
|
|
m_allTransforms.Clear();
|
|
var allChildren = GetComponentsInChildren<Transform>(true);
|
|
foreach (var t in allChildren)
|
|
{
|
|
if (!m_allTransforms.ContainsKey(t.name))
|
|
{
|
|
m_allTransforms[t.name] = t;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 기존 매핑의 Transform 캐시만 갱신
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// OptiTrack 본 ID → boneMappings 인덱스 매핑 구축
|
|
/// </summary>
|
|
private void RebuildBoneIdMapping()
|
|
{
|
|
m_boneIdToMappingIndex.Clear();
|
|
debugReceivedBoneNames.Clear();
|
|
if (m_skeletonDef == null) return;
|
|
|
|
debugReceivedBoneCount = m_skeletonDef.Bones.Count;
|
|
|
|
var nameToIdx = new Dictionary<string, int>();
|
|
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<OptitrackStreamingClient>();
|
|
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<HumanBodyBones, string> HumanBoneToOptiName = new Dictionary<HumanBodyBones, string>
|
|
{
|
|
{ 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" },
|
|
};
|
|
|
|
/// <summary>
|
|
/// HumanBodyBones enum으로 OptiTrack 매핑된 Transform 반환
|
|
/// (Humanoid 호환 레이어 — sourceAnimator.GetBoneTransform() 대체)
|
|
/// </summary>
|
|
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" };
|
|
|
|
/// <summary>
|
|
/// OptiTrack 본 이름으로 매핑된 Transform 반환
|
|
/// </summary>
|
|
public Transform GetMappedTransform(string optiTrackBoneName)
|
|
{
|
|
foreach (var mapping in boneMappings)
|
|
{
|
|
if (mapping.optiTrackBoneName == optiTrackBoneName && mapping.isMapped)
|
|
return mapping.cachedTransform;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 스파인 체인의 모든 Transform을 순서대로 반환 (Ab → Spine2 → Spine3 → Spine4 → Chest)
|
|
/// </summary>
|
|
public List<Transform> GetSpineChainTransforms()
|
|
{
|
|
var chain = new List<Transform>();
|
|
foreach (var name in SpineChainOptiNames)
|
|
{
|
|
Transform t = GetMappedTransform(name);
|
|
if (t != null)
|
|
chain.Add(t);
|
|
}
|
|
return chain;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 넥 체인의 모든 Transform을 순서대로 반환
|
|
/// </summary>
|
|
public List<Transform> GetNeckChainTransforms()
|
|
{
|
|
var chain = new List<Transform>();
|
|
foreach (var name in NeckChainOptiNames)
|
|
{
|
|
Transform t = GetMappedTransform(name);
|
|
if (t != null)
|
|
chain.Add(t);
|
|
}
|
|
return chain;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 체인의 누적 로컬 회전을 계산 (각 본의 localRotation을 순서대로 곱함)
|
|
/// </summary>
|
|
public static Quaternion ComputeChainRotation(List<Transform> chain)
|
|
{
|
|
Quaternion total = Quaternion.identity;
|
|
foreach (var t in chain)
|
|
{
|
|
total *= t.localRotation;
|
|
}
|
|
return total;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 초기 포즈(T-포즈) 캐싱 — Humanoid가 아니므로 직접 캐싱
|
|
/// </summary>
|
|
[HideInInspector] public Dictionary<string, Quaternion> restLocalRotations = new Dictionary<string, Quaternion>();
|
|
[HideInInspector] public Dictionary<string, Vector3> restLocalPositions = new Dictionary<string, Vector3>();
|
|
[HideInInspector] public bool isRestPoseCached = false;
|
|
|
|
/// <summary>
|
|
/// 현재 포즈를 기준 포즈(T-포즈)로 캐싱
|
|
/// </summary>
|
|
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}개 본");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 캐싱된 기준 포즈로 복원 (T-포즈 복원)
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 특정 본의 기준 포즈 로컬 회전 반환
|
|
/// </summary>
|
|
public Quaternion GetRestLocalRotation(string optiTrackBoneName)
|
|
{
|
|
if (restLocalRotations.TryGetValue(optiTrackBoneName, out Quaternion rot))
|
|
return rot;
|
|
return Quaternion.identity;
|
|
}
|
|
|
|
#endregion
|
|
}
|