Add: OptiTrack MirrorMode 좌우 반전 기능 구현
- OptitrackStreamingClient.FillBoneSnapshot: L/R 쌍 본은 위치 유지 + 회전 YZ반사 후 스왑, 중심 본(Hip/척추 등)은 위치 X반전 + 회전 YZ반사 - OptitrackStreamingClient.GetLatestRigidBodyState: 리짓바디 위치·회전 YZ반사 (기존) - GetOrBuildMirrorBoneIdMap: SkeletonName_BoneName 접두사 형식 지원 - 마커/TMarkerset 마커/TMarkerset BonePoses 미러 적용 - OptitrackSkeletonAnimator_Mingle: MirrorMode 토글 시 1€ 필터 상태 자동 리셋 - 월드 공간 미러 코드(ApplyWorldSpaceMirror 등) 제거, 데이터 수신 레벨에서 처리 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
dbf2ac91f3
commit
026eaf094b
@ -67,10 +67,6 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
|
|||||||
// OptiTrack 본 이름 → Transform 빠른 캐시 (GetMappedTransform O(n) → O(1))
|
// OptiTrack 본 이름 → Transform 빠른 캐시 (GetMappedTransform O(n) → O(1))
|
||||||
private Dictionary<string, Transform> m_optiNameTransformCache = new Dictionary<string, Transform>();
|
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 할당 방지)
|
// 스파인/넥 체인 Transform 캐시 (GetSpineChainTransforms 매 호출 List 할당 방지)
|
||||||
private List<Transform> m_spineChainCache = new List<Transform>();
|
private List<Transform> m_spineChainCache = new List<Transform>();
|
||||||
@ -219,6 +215,8 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
|
|||||||
StartCoroutine(CheckSkeletonConnectionPeriodically());
|
StartCoroutine(CheckSkeletonConnectionPeriodically());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool m_lastMirrorMode = false;
|
||||||
|
|
||||||
void Update()
|
void Update()
|
||||||
{
|
{
|
||||||
if (StreamingClient == null)
|
if (StreamingClient == null)
|
||||||
@ -227,6 +225,14 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MirrorMode 변경 감지 → 필터 상태 리셋 (불연속 튐 방지)
|
||||||
|
bool currentMirrorMode = StreamingClient != null && StreamingClient.MirrorMode;
|
||||||
|
if (currentMirrorMode != m_lastMirrorMode)
|
||||||
|
{
|
||||||
|
m_filterStates.Clear();
|
||||||
|
m_lastMirrorMode = currentMirrorMode;
|
||||||
|
}
|
||||||
|
|
||||||
// 스켈레톤 이름 변경 감지
|
// 스켈레톤 이름 변경 감지
|
||||||
if (previousSkeletonName != SkeletonAssetName)
|
if (previousSkeletonName != SkeletonAssetName)
|
||||||
{
|
{
|
||||||
@ -288,65 +294,6 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 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>
|
/// <summary>
|
||||||
@ -416,7 +363,6 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
|
|||||||
m_boneIdToMappingIndex.Clear();
|
m_boneIdToMappingIndex.Clear();
|
||||||
m_filterStates.Clear();
|
m_filterStates.Clear();
|
||||||
m_hasLastFrameTimestamp = false;
|
m_hasLastFrameTimestamp = false;
|
||||||
m_mirrorBoneIdMap = null; // 스켈레톤 재구성 시 미러 맵 초기화
|
|
||||||
if (m_skeletonDef == null) return;
|
if (m_skeletonDef == null) return;
|
||||||
|
|
||||||
var nameToIdx = new Dictionary<string, int>();
|
var nameToIdx = new Dictionary<string, int>();
|
||||||
|
|||||||
@ -934,11 +934,34 @@ public class OptitrackStreamingClient : MonoBehaviour
|
|||||||
posOut.Clear();
|
posOut.Clear();
|
||||||
oriOut.Clear();
|
oriOut.Clear();
|
||||||
|
|
||||||
// 스켈레톤 미러는 OptitrackSkeletonAnimator_Mingle에서 월드 공간 기준으로 처리
|
if ( MirrorMode )
|
||||||
foreach ( var kvp in state.BonePoses )
|
|
||||||
{
|
{
|
||||||
posOut[kvp.Key] = kvp.Value.Position;
|
Dictionary<Int32, Int32> mirrorMap = GetOrBuildMirrorBoneIdMap( skeletonId );
|
||||||
oriOut[kvp.Key] = kvp.Value.Orientation;
|
foreach ( var kvp in state.BonePoses )
|
||||||
|
{
|
||||||
|
Int32 targetId = mirrorMap != null && mirrorMap.TryGetValue( kvp.Key, out Int32 mid ) ? mid : kvp.Key;
|
||||||
|
|
||||||
|
if ( targetId == kvp.Key )
|
||||||
|
{
|
||||||
|
// 대칭 본 (Hip, 척추, 목, 머리 등): YZ 평면 반사 적용
|
||||||
|
posOut[kvp.Key] = MirrorPosition( kvp.Value.Position );
|
||||||
|
oriOut[kvp.Key] = MirrorOrientation( kvp.Value.Orientation );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// L/R 쌍 본: 위치는 자기 자리 유지, 회전은 YZ 반사 후 미러 본으로 스왑
|
||||||
|
posOut[kvp.Key] = 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -976,8 +999,16 @@ public class OptitrackStreamingClient : MonoBehaviour
|
|||||||
var map = new Dictionary<Int32, Int32>( skelDef.Bones.Count );
|
var map = new Dictionary<Int32, Int32>( skelDef.Bones.Count );
|
||||||
foreach ( var bone in skelDef.Bones )
|
foreach ( var bone in skelDef.Bones )
|
||||||
{
|
{
|
||||||
string mirrorName = GetMirrorBoneName( bone.Name );
|
// "SkeletonName_BoneName" 형식 지원: "_" 뒤의 짧은 이름에서 L/R 접두사 처리
|
||||||
if ( mirrorName != null && nameToId.TryGetValue( mirrorName, out Int32 mirrorId ) )
|
string fullName = bone.Name;
|
||||||
|
int sep = fullName.IndexOf( '_' );
|
||||||
|
string prefix = sep >= 0 ? fullName.Substring( 0, sep + 1 ) : ""; // "Skeleton1_"
|
||||||
|
string shortName = sep >= 0 ? fullName.Substring( sep + 1 ) : fullName; // "LUArm"
|
||||||
|
|
||||||
|
string mirrorShort = GetMirrorBoneName( shortName );
|
||||||
|
string mirrorFull = mirrorShort != null ? prefix + mirrorShort : null;
|
||||||
|
|
||||||
|
if ( mirrorFull != null && nameToId.TryGetValue( mirrorFull, out Int32 mirrorId ) )
|
||||||
map[bone.Id] = mirrorId;
|
map[bone.Id] = mirrorId;
|
||||||
else
|
else
|
||||||
map[bone.Id] = bone.Id; // 대칭 본은 자기 자신에 매핑
|
map[bone.Id] = bone.Id; // 대칭 본은 자기 자신에 매핑
|
||||||
@ -1011,6 +1042,22 @@ public class OptitrackStreamingClient : MonoBehaviour
|
|||||||
m_latestTMarkersetStates.TryGetValue(tmarkersetId, out tmarState);
|
m_latestTMarkersetStates.TryGetValue(tmarkersetId, out tmarState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( MirrorMode && tmarState != null && tmarState.BonePoses != null )
|
||||||
|
{
|
||||||
|
var mirrored = new OptitrackTMarkersetState
|
||||||
|
{
|
||||||
|
BonePoses = new Dictionary<Int32, OptitrackPose>( tmarState.BonePoses.Count ),
|
||||||
|
LocalBonePoses = tmarState.LocalBonePoses,
|
||||||
|
};
|
||||||
|
foreach ( var kvp in tmarState.BonePoses )
|
||||||
|
mirrored.BonePoses[kvp.Key] = new OptitrackPose
|
||||||
|
{
|
||||||
|
Position = MirrorPosition( kvp.Value.Position ),
|
||||||
|
Orientation = MirrorOrientation( kvp.Value.Orientation ),
|
||||||
|
};
|
||||||
|
tmarState = mirrored;
|
||||||
|
}
|
||||||
|
|
||||||
return tmarState;
|
return tmarState;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1027,7 +1074,7 @@ public class OptitrackStreamingClient : MonoBehaviour
|
|||||||
{
|
{
|
||||||
OptitrackMarkerState newMarkerState = new OptitrackMarkerState
|
OptitrackMarkerState newMarkerState = new OptitrackMarkerState
|
||||||
{
|
{
|
||||||
Position = markerEntry.Value.Position,
|
Position = MirrorMode ? MirrorPosition( markerEntry.Value.Position ) : markerEntry.Value.Position,
|
||||||
Labeled = markerEntry.Value.Labeled,
|
Labeled = markerEntry.Value.Labeled,
|
||||||
Size = markerEntry.Value.Size,
|
Size = markerEntry.Value.Size,
|
||||||
Id = markerEntry.Value.Id
|
Id = markerEntry.Value.Id
|
||||||
@ -1144,7 +1191,7 @@ public class OptitrackStreamingClient : MonoBehaviour
|
|||||||
{
|
{
|
||||||
OptitrackMarkerState newMarkerState = new OptitrackMarkerState
|
OptitrackMarkerState newMarkerState = new OptitrackMarkerState
|
||||||
{
|
{
|
||||||
Position = markerEntry.Value.Position,
|
Position = MirrorMode ? MirrorPosition( markerEntry.Value.Position ) : markerEntry.Value.Position,
|
||||||
Labeled = markerEntry.Value.Labeled,
|
Labeled = markerEntry.Value.Labeled,
|
||||||
Size = markerEntry.Value.Size,
|
Size = markerEntry.Value.Size,
|
||||||
Id = markerEntry.Value.Id
|
Id = markerEntry.Value.Id
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user