diff --git a/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/Editor/UXML/OptitrackStreamingClientEditor.uxml b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/Editor/UXML/OptitrackStreamingClientEditor.uxml index 7288d631b..de656dfce 100644 --- a/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/Editor/UXML/OptitrackStreamingClientEditor.uxml +++ b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/Editor/UXML/OptitrackStreamingClientEditor.uxml @@ -42,6 +42,14 @@ + + + + + + + + diff --git a/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackStreamingClient.cs b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackStreamingClient.cs index e72d4ec6c..7494971a2 100644 --- a/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackStreamingClient.cs +++ b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackStreamingClient.cs @@ -373,6 +373,12 @@ public class OptitrackStreamingClient : MonoBehaviour [Tooltip("모든 스켈레톤 본과 리지드바디의 좌우를 거울 반전합니다.")] public bool MirrorMode = false; + [Header("Skeleton Frame Filter")] + [Tooltip("스켈레톤 프레임 무결성 필터입니다.\n" + + "ON: 본 하나라도 트래킹 실패·손상되면 그 프레임 전체를 폐기해 떨림/순간 깨짐을 막습니다. 대신 라이브에서 마커 가림이 잦으면 액터가 얼어붙을 수 있습니다.\n" + + "OFF(권장·라이브): 정상 본만 갱신하고 미트래킹·손상 본은 직전 포즈를 유지해 모션이 끊기지 않습니다. (NaN/0-쿼터니언 본은 모드와 무관하게 개별로 건너뜁니다.)")] + public bool EnableSkeletonFrameFilter = false; + #region Private fields //private UInt16 ServerCommandPort = NatNetConstants.DefaultCommandPort; @@ -2074,46 +2080,57 @@ public class OptitrackStreamingClient : MonoBehaviour continue; } - // Motive can occasionally emit an empty or partial skeleton payload. Do not let - // one bad bone overwrite the previous valid pose: stage and validate first. - if (skelRbCount != skelDef.Bones.Count) + // === 스켈레톤 프레임 필터 (EnableSkeletonFrameFilter) === + // Motive가 빈/부분 스켈레톤 페이로드를 내보낼 수 있다. + // ON : 본 개수가 다르면 프레임 전체 폐기 (직전 포즈 유지 → 떨림/부분프레임 방지) + // OFF: 들어온 본 개수만큼 그대로 처리 (모션 끊김 방지) + if (EnableSkeletonFrameFilter && skelRbCount != skelDef.Bones.Count) { continue; } sRigidBodyData[] stagedBones = GetSkeletonFrameScratch(skeletonId, skelRbCount); - bool isValidSkeletonFrame = true; for (int boneIdx = 0; boneIdx < skelRbCount; ++boneIdx) { result = NaturalPoint.NatNetLib.NativeMethods.NatNet_Frame_Skeleton_GetRigidBody( pFrame, skelIdx, boneIdx, out stagedBones[boneIdx] ); NatNetException.ThrowIfNotOK( result, "NatNet_Frame_Skeleton_GetRigidBody failed." ); + } - sRigidBodyData boneData = stagedBones[boneIdx]; - // In the context of frame data (unlike in the definition data), this ID value is a - // packed composite of both the asset/entity (skeleton) ID and member (bone) ID. - Int32 boneSkelId, boneId; - NaturalPoint.NatNetLib.NativeMethods.NatNet_DecodeID( boneData.Id, out boneSkelId, out boneId ); - - if (boneSkelId != skeletonId || - !skelDef.BoneIdToParentIdMap.ContainsKey(boneId) || - !IsValidSkeletonBoneData(boneData)) + // ON 일 때만: 본 하나라도 트래킹 실패/손상이면 프레임 전체 폐기 + if (EnableSkeletonFrameFilter) + { + bool isValidSkeletonFrame = true; + for (int boneIdx = 0; boneIdx < skelRbCount; ++boneIdx) { - isValidSkeletonFrame = false; - break; + sRigidBodyData boneData = stagedBones[boneIdx]; + // In the context of frame data (unlike in the definition data), this ID value is a + // packed composite of both the asset/entity (skeleton) ID and member (bone) ID. + Int32 boneSkelId, boneId; + NaturalPoint.NatNetLib.NativeMethods.NatNet_DecodeID( boneData.Id, out boneSkelId, out boneId ); + + if (boneSkelId != skeletonId || + !skelDef.BoneIdToParentIdMap.ContainsKey(boneId) || + !IsSkeletonBoneTracked(boneData) || + !IsBoneDataUsable(boneData)) + { + isValidSkeletonFrame = false; + break; + } + } + + if (!isValidSkeletonFrame) + { + continue; } } - if (!isValidSkeletonFrame) - { - continue; - } - - // Commit global transforms only after the whole payload has passed validation. + // === 글로벌 트랜스폼 커밋 === + // 필터 OFF 에서도 손상(NaN·0쿼터니언)·미매핑 본은 개별로 건너뛰어 직전 포즈를 보존한다. for (int boneIdx = 0; boneIdx < skelRbCount; ++boneIdx) { sRigidBodyData boneData = stagedBones[boneIdx]; - Int32 boneSkelId, boneId; - NaturalPoint.NatNetLib.NativeMethods.NatNet_DecodeID( boneData.Id, out boneSkelId, out boneId ); + if (!TryGetCommittableBoneId(boneData, skeletonId, skelDef, out int boneId)) + continue; // TODO: Could pre-populate this map when the definitions are retrieved. // Should never allocate after the first frame, at least. @@ -2137,8 +2154,8 @@ public class OptitrackStreamingClient : MonoBehaviour for (int boneIdx = 0; boneIdx < skelRbCount; ++boneIdx) { sRigidBodyData boneData = stagedBones[boneIdx]; - Int32 boneSkelId, boneId; - NaturalPoint.NatNetLib.NativeMethods.NatNet_DecodeID( boneData.Id, out boneSkelId, out boneId ); + if (!TryGetCommittableBoneId(boneData, skeletonId, skelDef, out int boneId)) + continue; Vector3 bonePos = skelState.BonePoses[boneId].Position; Quaternion boneOri = skelState.BonePoses[boneId].Orientation; @@ -2146,10 +2163,10 @@ public class OptitrackStreamingClient : MonoBehaviour Quaternion parentBoneOri = new Quaternion(0,0,0,1); Int32 pId = skelDef.BoneIdToParentIdMap[boneId]; - if (pId != 0) + if (pId != 0 && skelState.BonePoses.TryGetValue(pId, out OptitrackPose parentPose)) { - parentBonePos = skelState.BonePoses[pId].Position; - parentBoneOri = skelState.BonePoses[pId].Orientation; + parentBonePos = parentPose.Position; + parentBoneOri = parentPose.Orientation; } skelState.LocalBonePoses[boneId].Position = bonePos - parentBonePos; skelState.LocalBonePoses[boneId].Orientation = Quaternion.Inverse(parentBoneOri) * boneOri; @@ -2746,11 +2763,18 @@ public class OptitrackStreamingClient : MonoBehaviour return scratch; } - private static bool IsValidSkeletonBoneData(sRigidBodyData boneData) + /// Motive "이 프레임 트래킹됨" 비트(0x01). 엄격 필터(ON)에서만 프레임 폐기 조건으로 쓰인다. + private static bool IsSkeletonBoneTracked(sRigidBodyData boneData) { - if ((boneData.Params & 0x01) == 0) - return false; + return (boneData.Params & 0x01) != 0; + } + /// + /// 본 데이터가 포즈로 커밋해도 안전한지 검사한다(좌표 유한 + 쿼터니언 정상). + /// 트래킹 비트와 무관 — 필터 OFF에서도 손상 본이 직전 포즈를 덮어쓰지 못하게 막는 안전장치. + /// + private static bool IsBoneDataUsable(sRigidBodyData boneData) + { float quaternionMagnitudeSquared = boneData.QX * boneData.QX + boneData.QY * boneData.QY + @@ -2767,6 +2791,20 @@ public class OptitrackStreamingClient : MonoBehaviour quaternionMagnitudeSquared > 0.000001f; } + /// + /// 프레임 본 데이터가 이 스켈레톤에 커밋 가능한지 검사하고 boneId를 출력한다. + /// 스켈레톤 ID 일치 + 정의에 매핑된 본 + 데이터 정상(NaN/0-쿼터니언 아님)일 때만 true. + /// (필터 ON/OFF 공통으로 커밋 단계에서 사용 — OFF에서 손상·미매핑 본 개별 스킵, 2차 패스 KeyNotFound 방지) + /// + private static bool TryGetCommittableBoneId(sRigidBodyData boneData, Int32 skeletonId, OptitrackSkeletonDefinition skelDef, out int boneId) + { + Int32 boneSkelId; + NaturalPoint.NatNetLib.NativeMethods.NatNet_DecodeID( boneData.Id, out boneSkelId, out boneId ); + return boneSkelId == skeletonId + && skelDef.BoneIdToParentIdMap.ContainsKey(boneId) + && IsBoneDataUsable(boneData); + } + private static bool IsFinite(float value) { return !float.IsNaN(value) && !float.IsInfinity(value);