Fix : 필터링 시스템 추가 업데이트 패치

This commit is contained in:
MINGLE-RENDER\Mingle-Render 2026-06-13 21:57:13 +09:00
parent 25c3c128e4
commit 9f93d398ad
4 changed files with 106 additions and 86 deletions

Binary file not shown.

View File

@ -15,7 +15,6 @@
<uie:PropertyField binding-path="ServerAddress" label="Live Motive IP"/>
<uie:PropertyField binding-path="EnableReplayPriority" label="Use MMRP Replay Priority"/>
<uie:PropertyField binding-path="ReplayServerAddress" label="MMRP Replay IP"/>
<uie:PropertyField binding-path="DirectSyntheticSkeletonName" label="Fallback Actor Name"/>
<uie:PropertyField binding-path="ConnectionType" label="Connection Type"/>
<uie:PropertyField binding-path="SkeletonCoordinates" label="Skeleton Coordinates"/>
<uie:PropertyField binding-path="TMarkersetCoordinates" label="TMarkerset Coordinates"/>
@ -51,14 +50,6 @@
</ui:Foldout>
</ui:VisualElement>
<!-- Skeleton Frame Filter -->
<ui:VisualElement class="section">
<ui:Foldout text="Skeleton Frame Filter" value="true" class="section-foldout">
<uie:PropertyField binding-path="EnableSkeletonFrameFilter" label="Enable Strict Skeleton Frame Filter"/>
<ui:HelpBox message-type="Info" text="ON: drop the whole skeleton frame if any bone is untracked or invalid. OFF is recommended for live use: valid bones update, invalid bones keep the last pose."/>
</ui:Foldout>
</ui:VisualElement>
<!-- NatNet Version -->
<ui:VisualElement class="section">
<ui:Foldout text="NatNet Version" value="false" class="section-foldout">

View File

@ -91,6 +91,23 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
HumanBodyBones.RightToes,
};
private static readonly string[] s_fingerBoneNameTokens =
{
"Thumb",
"Index",
"Middle",
"Ring",
"Pinky",
"Little",
"Finger",
};
private const bool k_EnableResetPoseFrameFilter = true;
private const bool k_ResetPoseComparePositions = false;
private const float k_ResetPosePositionTolerance = 0.025f;
private const float k_ResetPoseRotationToleranceDegrees = 5f;
private const int k_ResetPoseMinimumBodyBones = 8;
// 스파인/넥 체인 Transform 캐시 (GetSpineChainTransforms 매 호출 List 할당 방지)
private List<Transform> m_spineChainCache = new List<Transform>();
@ -341,6 +358,9 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
return;
// ── NatNet 실제 프레임 간격 계산 (하드웨어 타이머 — 렌더 프레임 등락과 완전 독립) ──
if (ShouldSkipResetPoseFrame())
return;
if (m_hasLastFrameTimestamp && frameTs.m_ticks != m_lastFrameTimestamp.m_ticks)
{
float measuredDt = frameTs.SecondsSince(m_lastFrameTimestamp);
@ -647,6 +667,66 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
/// 두 OptiTrack 프레임 사이를 시간 기반으로 보간합니다.
/// m_snapshotPositions/Orientations를 보간된 결과로 덮어씁니다.
/// </summary>
private bool ShouldSkipResetPoseFrame()
{
if (!k_EnableResetPoseFrameFilter || !m_isRestPoseCached || m_skeletonDef == null)
return false;
int testedBodyBoneCount = 0;
float positionTolerance = Mathf.Max(0.0f, k_ResetPosePositionTolerance);
float rotationTolerance = Mathf.Max(0.0f, k_ResetPoseRotationToleranceDegrees);
for (int i = 0; i < m_skeletonDef.Bones.Count; ++i)
{
OptitrackSkeletonDefinition.BoneDefinition bone = m_skeletonDef.Bones[i];
if (!m_boneIdToMappingIndex.TryGetValue(bone.Id, out int mappingIndex))
continue;
OptiTrackBoneMapping mapping = boneMappings[mappingIndex];
if (!mapping.isMapped)
continue;
if (IsFingerBoneName(mapping.optiTrackBoneName))
continue;
if (!m_restLocalRotations.TryGetValue(mapping.optiTrackBoneName, out Quaternion restRot) ||
!m_snapshotOrientations.TryGetValue(bone.Id, out Quaternion frameRot))
{
return false;
}
if (Quaternion.Angle(restRot, frameRot) > rotationTolerance)
return false;
if (k_ResetPoseComparePositions &&
mapping.applyPosition &&
m_restLocalPositions.TryGetValue(mapping.optiTrackBoneName, out Vector3 restPos) &&
m_snapshotPositions.TryGetValue(bone.Id, out Vector3 framePos) &&
Vector3.Distance(restPos, framePos) > positionTolerance)
{
return false;
}
++testedBodyBoneCount;
}
return testedBodyBoneCount >= k_ResetPoseMinimumBodyBones;
}
private static bool IsFingerBoneName(string boneName)
{
if (string.IsNullOrEmpty(boneName))
return false;
for (int i = 0; i < s_fingerBoneNameTokens.Length; ++i)
{
if (boneName.IndexOf(s_fingerBoneNameTokens[i], StringComparison.InvariantCultureIgnoreCase) >= 0)
return true;
}
return false;
}
private void InterpolateSnapshots(OptitrackHiResTimer.Timestamp frameTs)
{
// 새 프레임 감지 (타임스탬프가 변경되었으면 새 OptiTrack 프레임이 도착한 것)

View File

@ -341,9 +341,6 @@ public class OptitrackStreamingClient : MonoBehaviour
[Range(0.05f, 5.0f)]
public float ReplayFreshnessSeconds = 0.35f;
[Tooltip("Actor/skeleton name to use when the NatNet command channel is unavailable and names cannot be read from MODELDEF. Must match OptitrackSkeletonAnimator_Mingle.SkeletonAssetName.")]
public string DirectSyntheticSkeletonName = "002";
[Tooltip("Controls whether skeleton data is streamed with local or global coordinates.")]
public StreamingCoordinatesValues SkeletonCoordinates = StreamingCoordinatesValues.Local;
@ -390,10 +387,6 @@ public class OptitrackStreamingClient : MonoBehaviour
[Tooltip("Mirrors streamed skeleton bones and retargeted avatar motion left/right.")]
public bool MirrorMode = false;
[Header("Skeleton Frame Filter")]
[Tooltip("Strict skeleton frame filter.\nON: drops the whole skeleton frame when any bone is untracked or invalid.\nOFF: valid bones update and invalid bones keep the previous pose. Recommended for live use.")]
public bool EnableSkeletonFrameFilter = false;
#region Private fields
//private UInt16 ServerCommandPort = NatNetConstants.DefaultCommandPort;
//private UInt16 ServerDataPort = NatNetConstants.DefaultDataPort;
@ -821,6 +814,12 @@ public class OptitrackStreamingClient : MonoBehaviour
return;
}
if (m_directNatNetConnected && ConnectionType == ClientConnectionType.Multicast)
{
Debug.Log(GetType().FullName + ": direct UDP receiver is already active; reconnect request skipped.", this);
return;
}
Debug.Log("OptiTrack: reconnect requested.");
// 湲곗〈 ?곌껐 ?뺣━ (StopAllCoroutines ?ы븿)
@ -1919,6 +1918,15 @@ public class OptitrackStreamingClient : MonoBehaviour
m_lastReplayFrameDeliveryTimestamp.AgeSeconds <= ReplayFreshnessSeconds;
}
private bool HasRecentStreamingFrame(float thresholdSeconds)
{
OptitrackHiResTimer.Timestamp liveTimestamp;
liveTimestamp.m_ticks = Interlocked.Read(ref m_lastFrameDeliveryTimestamp.m_ticks);
bool liveFresh = m_receivedFrameSinceConnect && liveTimestamp.AgeSeconds < thresholdSeconds;
bool replayFresh = IsReplayFrameFresh();
return liveFresh || replayFresh;
}
private void StartDirectFrameReceiver(string localAddress, string multicastAddress, UInt16 dataPort)
{
StopDirectFrameReceiver();
@ -2046,6 +2054,8 @@ public class OptitrackStreamingClient : MonoBehaviour
{
m_replayReceivedFrameSinceConnect = true;
Interlocked.Exchange(ref m_lastReplayFrameDeliveryTimestamp.m_ticks, frameTimestamp.m_ticks);
m_receivedFrameSinceConnect = true;
Interlocked.Exchange(ref m_lastFrameDeliveryTimestamp.m_ticks, frameTimestamp.m_ticks);
}
else
{
@ -2212,27 +2222,8 @@ public class OptitrackStreamingClient : MonoBehaviour
return;
}
if (EnableSkeletonFrameFilter && boneCount != skelDef.Bones.Count)
return;
OptitrackSkeletonState skelState = GetOrCreateSkeletonState(skeletonId);
if (EnableSkeletonFrameFilter)
{
for (int b = 0; b < boneCount; b++)
{
int boneSkelId, boneId;
DirectDecodeId(stagedBones[b].Id, out boneSkelId, out boneId);
if (boneSkelId != skeletonId ||
!skelDef.BoneIdToParentIdMap.ContainsKey(boneId) ||
!IsSkeletonBoneTracked(stagedBones[b]) ||
!IsBoneDataUsable(stagedBones[b]))
{
return;
}
}
}
for (int b = 0; b < boneCount; b++)
{
sRigidBodyData boneData = stagedBones[b];
@ -2332,8 +2323,6 @@ public class OptitrackStreamingClient : MonoBehaviour
private string ChooseSyntheticSkeletonName(int skeletonId)
{
if (!string.IsNullOrWhiteSpace(DirectSyntheticSkeletonName))
return DirectSyntheticSkeletonName.Trim();
return "Skeleton" + skeletonId;
}
@ -3035,7 +3024,6 @@ public class OptitrackStreamingClient : MonoBehaviour
// The coroutine is stopped on disconnect and restarted on connect.
YieldInstruction checkIntervalYield = new WaitForSeconds( kHealthCheckIntervalSeconds );
OptitrackHiResTimer.Timestamp connectionInitiatedTimestamp = OptitrackHiResTimer.Now();
OptitrackHiResTimer.Timestamp lastFrameReceivedTimestamp;
bool wasReceivingFrames = false;
bool warnedPendingFirstFrame = false;
@ -3043,7 +3031,7 @@ public class OptitrackStreamingClient : MonoBehaviour
{
yield return checkIntervalYield;
if ( m_receivedFrameSinceConnect == false )
if ( m_receivedFrameSinceConnect == false && !IsReplayFrameFresh() )
{
// Still waiting for first frame. Warn exactly once if this takes too long.
if ( connectionInitiatedTimestamp.AgeSeconds > kRecentFrameThresholdSeconds )
@ -3063,8 +3051,7 @@ public class OptitrackStreamingClient : MonoBehaviour
else
{
// We've received at least one frame, do ongoing checks for changes in connection health.
lastFrameReceivedTimestamp.m_ticks = Interlocked.Read( ref m_lastFrameDeliveryTimestamp.m_ticks );
bool receivedRecentFrame = lastFrameReceivedTimestamp.AgeSeconds < kRecentFrameThresholdSeconds;
bool receivedRecentFrame = HasRecentStreamingFrame(kRecentFrameThresholdSeconds);
if ( wasReceivingFrames == false && receivedRecentFrame == true )
{
@ -3078,6 +3065,11 @@ public class OptitrackStreamingClient : MonoBehaviour
// Transition: Good health -> bad health.
wasReceivingFrames = false;
Debug.LogWarning( GetType().FullName + ": No streaming frames received from the server recently.", this );
if (m_directNatNetConnected)
{
Debug.LogWarning(GetType().FullName + ": direct UDP receiver stays active; automatic reconnect is skipped to avoid interrupting live/replay multicast.", this);
continue;
}
if ( AutoReconnect )
{
Debug.Log( GetType().FullName + ": starting automatic reconnect.", this );
@ -3199,15 +3191,6 @@ public class OptitrackStreamingClient : MonoBehaviour
continue;
}
// === ?ㅼ펷?덊넠 ?꾨젅???꾪꽣 (EnableSkeletonFrameFilter) ===
// Motive媛€ 鍮?遺€遺??ㅼ펷?덊넠 ?섏씠濡쒕뱶瑜??대낫?????덈떎.
// ON : 蹂?媛쒖닔媛€ ?ㅻⅤ硫??꾨젅???꾩껜 ?먭린 (吏곸쟾 ?ъ쫰 ?좎? ???⑤┝/遺€遺꾪봽?덉엫 諛⑹?)
// OFF: ?ㅼ뼱??蹂?媛쒖닔留뚰겮 洹몃?濡?泥섎━ (紐⑥뀡 ?딄? 諛⑹?)
if (EnableSkeletonFrameFilter && skelRbCount != skelDef.Bones.Count)
{
continue;
}
sRigidBodyData[] stagedBones = GetSkeletonFrameScratch(skeletonId, skelRbCount);
for (int boneIdx = 0; boneIdx < skelRbCount; ++boneIdx)
{
@ -3215,34 +3198,6 @@ public class OptitrackStreamingClient : MonoBehaviour
NatNetException.ThrowIfNotOK( result, "NatNet_Frame_Skeleton_GetRigidBody failed." );
}
// ON ???뚮쭔: 蹂??섎굹?쇰룄 ?몃옒???ㅽ뙣/?먯긽?대㈃ ?꾨젅???꾩껜 ?먭린
if (EnableSkeletonFrameFilter)
{
bool isValidSkeletonFrame = true;
for (int boneIdx = 0; boneIdx < skelRbCount; ++boneIdx)
{
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;
}
}
// === 湲€濡쒕쾶 ?몃옖?ㅽ뤌 而ㅻ컠 ===
// ?꾪꽣 OFF ?먯꽌???먯긽(NaN쨌0荑쇳꽣?덉뼵)쨌誘몃ℓ??蹂몄? 媛쒕퀎濡?嫄대꼫?곗뼱 吏곸쟾 ?ъ쫰瑜?蹂댁〈?쒕떎.
for (int boneIdx = 0; boneIdx < skelRbCount; ++boneIdx)
@ -3890,12 +3845,6 @@ public class OptitrackStreamingClient : MonoBehaviour
return scratch;
}
/// <summary>Motive "???꾨젅???몃옒?밸맖" 鍮꾪듃(0x01). ?꾧꺽 ?꾪꽣(ON)?먯꽌留??꾨젅???먭린 議곌굔?쇰줈 ?곗씤??</summary>
private static bool IsSkeletonBoneTracked(sRigidBodyData boneData)
{
return (boneData.Params & 0x01) != 0;
}
/// <summary>
/// 蹂??곗씠?곌? ?ъ쫰濡?而ㅻ컠?대룄 ?덉쟾?쒖? 寃€?ы븳??醫뚰몴 ?좏븳 + 荑쇳꽣?덉뼵 ?뺤긽).
/// ?몃옒??鍮꾪듃?€ 臾닿? ???꾪꽣 OFF?먯꽌???먯긽 蹂몄씠 吏곸쟾 ?ъ쫰瑜???뼱?곗? 紐삵븯寃?留됰뒗 ?덉쟾?μ튂.