Fix : 옵티 미러 스켈레톤을 월드 공간 기준으로 재구현
- FillBoneSnapshot의 스켈레톤 미러 로직 제거 (로컬 좌표 + 좌우 본 교체 방식은 부모 본 좌표계 차이로 꼬임 발생) - OptitrackSkeletonAnimator_Mingle에 ApplyWorldSpaceMirror() 추가 - 본 데이터 적용 후 월드 공간에서 전체 포즈 캐시 → 미러 적용 - L/R 본은 상대방 월드 포즈로 교체 + YZ 평면 반사 - 대칭 본은 자기 포즈에 YZ 평면 반사 적용 - 로컬 축 컨벤션 독립적 → 어떤 스켈레톤에서도 정확히 동작 - RigidBody 미러는 기존 GetLatestRigidBodyState() 방식 유지 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1af29e6256
commit
dbf2ac91f3
@ -67,6 +67,11 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
|
||||
// OptiTrack 본 이름 → Transform 빠른 캐시 (GetMappedTransform O(n) → O(1))
|
||||
private Dictionary<string, Transform> m_optiNameTransformCache = new Dictionary<string, Transform>();
|
||||
|
||||
// MirrorMode용: boneId → mirrorBoneId 매핑 (null이면 미구축)
|
||||
private Dictionary<Int32, Int32> m_mirrorBoneIdMap;
|
||||
// MirrorMode용: 월드 포즈 캐시 (매 프레임 재사용, GC 없음)
|
||||
private Dictionary<Int32, (Vector3 pos, Quaternion rot)> m_mirrorWorldPoseCache = new Dictionary<Int32, (Vector3, Quaternion)>();
|
||||
|
||||
// 스파인/넥 체인 Transform 캐시 (GetSpineChainTransforms 매 호출 List 할당 방지)
|
||||
private List<Transform> m_spineChainCache = new List<Transform>();
|
||||
private List<Transform> m_neckChainCache = new List<Transform>();
|
||||
@ -282,6 +287,66 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
|
||||
mapping.cachedTransform.localPosition = finalPos;
|
||||
}
|
||||
}
|
||||
|
||||
// ── MirrorMode: 본 데이터 적용 후 월드 공간 기준 좌우 반전 ─────────────────
|
||||
if (StreamingClient != null && StreamingClient.MirrorMode)
|
||||
ApplyWorldSpaceMirror();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 전체 본을 월드 공간에서 좌우 반전합니다.
|
||||
/// 로컬 축 컨벤션에 독립적이므로 어떤 스켈레톤 구조에서도 올바르게 동작합니다.
|
||||
/// </summary>
|
||||
private void ApplyWorldSpaceMirror()
|
||||
{
|
||||
if (m_mirrorBoneIdMap == null)
|
||||
BuildMirrorBoneIdMapLocal();
|
||||
|
||||
// Step 1: 현재 월드 포즈 전체 캐시 (수정 전 상태 보존)
|
||||
m_mirrorWorldPoseCache.Clear();
|
||||
foreach (var bone in m_skeletonDef.Bones)
|
||||
{
|
||||
if (!m_boneIdToMappingIndex.TryGetValue(bone.Id, out int idx)) continue;
|
||||
var t = boneMappings[idx].cachedTransform;
|
||||
if (t == null) continue;
|
||||
m_mirrorWorldPoseCache[bone.Id] = (t.position, t.rotation);
|
||||
}
|
||||
|
||||
// Step 2: 미러된 월드 포즈 적용
|
||||
foreach (var bone in m_skeletonDef.Bones)
|
||||
{
|
||||
if (!m_boneIdToMappingIndex.TryGetValue(bone.Id, out int idx)) continue;
|
||||
var mapping = boneMappings[idx];
|
||||
if (!mapping.isMapped || mapping.cachedTransform == null) continue;
|
||||
if (!m_mirrorBoneIdMap.TryGetValue(bone.Id, out Int32 mirrorId)) continue;
|
||||
if (!m_mirrorWorldPoseCache.TryGetValue(mirrorId, out var src)) continue;
|
||||
|
||||
// 월드 X 반전 (YZ 평면 반사), 회전은 Y·Z 성분 부호 반전
|
||||
if (mapping.applyPosition)
|
||||
mapping.cachedTransform.position = new Vector3(-src.pos.x, src.pos.y, src.pos.z);
|
||||
if (mapping.applyRotation)
|
||||
mapping.cachedTransform.rotation = new Quaternion(src.rot.x, -src.rot.y, -src.rot.z, src.rot.w);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>본 이름 기반 L/R 미러 ID 맵 구축 (월드 공간 미러용).</summary>
|
||||
private void BuildMirrorBoneIdMapLocal()
|
||||
{
|
||||
m_mirrorBoneIdMap = new Dictionary<Int32, Int32>();
|
||||
var nameToId = new Dictionary<string, Int32>();
|
||||
foreach (var bone in m_skeletonDef.Bones)
|
||||
nameToId[bone.Name] = bone.Id;
|
||||
|
||||
foreach (var bone in m_skeletonDef.Bones)
|
||||
{
|
||||
string n = bone.Name;
|
||||
string mirrorName = null;
|
||||
if (n.Length >= 2 && n[0] == 'L' && char.IsUpper(n[1])) mirrorName = "R" + n.Substring(1);
|
||||
else if (n.Length >= 2 && n[0] == 'R' && char.IsUpper(n[1])) mirrorName = "L" + n.Substring(1);
|
||||
|
||||
m_mirrorBoneIdMap[bone.Id] = (mirrorName != null && nameToId.TryGetValue(mirrorName, out Int32 mid))
|
||||
? mid : bone.Id;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -351,6 +416,7 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
|
||||
m_boneIdToMappingIndex.Clear();
|
||||
m_filterStates.Clear();
|
||||
m_hasLastFrameTimestamp = false;
|
||||
m_mirrorBoneIdMap = null; // 스켈레톤 재구성 시 미러 맵 초기화
|
||||
if (m_skeletonDef == null) return;
|
||||
|
||||
var nameToIdx = new Dictionary<string, int>();
|
||||
|
||||
@ -934,23 +934,11 @@ public class OptitrackStreamingClient : MonoBehaviour
|
||||
posOut.Clear();
|
||||
oriOut.Clear();
|
||||
|
||||
if ( MirrorMode )
|
||||
// 스켈레톤 미러는 OptitrackSkeletonAnimator_Mingle에서 월드 공간 기준으로 처리
|
||||
foreach ( var kvp in state.BonePoses )
|
||||
{
|
||||
Dictionary<Int32, Int32> mirrorMap = GetOrBuildMirrorBoneIdMap( skeletonId );
|
||||
foreach ( var kvp in state.BonePoses )
|
||||
{
|
||||
Int32 targetId = mirrorMap != null && mirrorMap.TryGetValue( kvp.Key, out Int32 mid ) ? mid : kvp.Key;
|
||||
posOut[targetId] = MirrorPosition( kvp.Value.Position );
|
||||
oriOut[targetId] = MirrorOrientation( kvp.Value.Orientation );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach ( var kvp in state.BonePoses )
|
||||
{
|
||||
posOut[kvp.Key] = kvp.Value.Position;
|
||||
oriOut[kvp.Key] = kvp.Value.Orientation;
|
||||
}
|
||||
posOut[kvp.Key] = kvp.Value.Position;
|
||||
oriOut[kvp.Key] = kvp.Value.Orientation;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user