From 5aa805e16a9d4483d3e3f3b79e9796c886c0b79a Mon Sep 17 00:00:00 2001 From: "qsxft258@gmail.com" Date: Sun, 19 Apr 2026 19:03:02 +0900 Subject: [PATCH] =?UTF-8?q?Optimize:=20OptiTrack=20=ED=94=8C=EB=9F=AC?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20Motive=20=EB=B6=80=ED=95=98=20=EC=B5=9C?= =?UTF-8?q?=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SetProperty 원격 명령을 최초 연결에서만 전송, 재연결 시 스킵 - 정의 재조회(UpdateDefinitions) 쿨다운 5초→15초, 최대 10회 제한 - DrawMarkers/DrawTMarkersetMarkers/DrawCameras/DrawForcePlates 락 범위 축소 - RecordOnPlay 재연결 루프에서 녹화 시작/종료 스킵, 성공 후에만 재시작 - SubscribeMarkers 실패 시 10초 쿨다운 (매 프레임 재시도 방지) - OnNatNetFrameReceived 내 GetSkeletonDefinitionById를 본 루프 밖으로 이동 - GetMarkerName assetID→이름 캐시 도입 (3중 선형 탐색 제거) - OptitrackRigidBody Update+OnBeforeRender 이중 NatNet 호출 제거 (캐싱) - OptitrackSkeletonAnimator_Mingle 스켈레톤 체크 주기 0.1초→1초+지수 백오프 - ToggleRecording 녹화 상태 추적으로 2중 명령 제거 - ResetStreamingSubscriptions 중복 명령 축소 - GetLatestTMarkMarkerStates 디버그 로그 잔재 제거 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../OptiTrack/Scripts/OptitrackRigidBody.cs | 31 +- .../OptitrackSkeletonAnimator_Mingle.cs | 35 +- .../Scripts/OptitrackStreamingClient.cs | 468 +++++++++--------- 3 files changed, 295 insertions(+), 239 deletions(-) diff --git a/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackRigidBody.cs b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackRigidBody.cs index 1e6040909..cd403c6d1 100644 --- a/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackRigidBody.cs +++ b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackRigidBody.cs @@ -39,7 +39,12 @@ public class OptitrackRigidBody : MonoBehaviour private bool m_isRigidBodyFound = false; private int m_resolvedRigidBodyId = -1; - private const float k_RetryInterval = 1.0f; + private const float k_RetryInterval = 3.0f; // Motive 재조회 부하 완화 (기존 1초 → 3초) + + // Update()에서 조회한 포즈를 캐시 — OnBeforeRender()에서 재사용하여 이중 NatNet 호출 방지 + private Vector3 m_cachedPosition; + private Quaternion m_cachedRotation; + private bool m_hasCachedPose = false; public bool isRigidBodyFound { @@ -129,37 +134,35 @@ public class OptitrackRigidBody : MonoBehaviour if (m_streamingClient == null || m_resolvedRigidBodyId == -1) return; + // NatNet 호출은 Update()에서 1회만 — 캐시하여 OnBeforeRender()에서 재사용 OptitrackRigidBodyState rbState = m_streamingClient.GetLatestRigidBodyState(m_resolvedRigidBodyId, useNetworkCompensation); if (rbState != null) { m_isRigidBodyFound = rbState.IsTracked; if (m_isRigidBodyFound) { - transform.localPosition = rbState.Pose.Position; - transform.localRotation = rbState.Pose.Orientation; + m_cachedPosition = rbState.Pose.Position; + m_cachedRotation = rbState.Pose.Orientation; + m_hasCachedPose = true; + transform.localPosition = m_cachedPosition; + transform.localRotation = m_cachedRotation; } } else { m_isRigidBodyFound = false; + m_hasCachedPose = false; } } void UpdatePose() { - if (m_streamingClient == null || m_resolvedRigidBodyId == -1) - return; - - OptitrackRigidBodyState rbState = m_streamingClient.GetLatestRigidBodyState(m_resolvedRigidBodyId, useNetworkCompensation); - if (rbState != null) + // OnBeforeRender용: NatNet 재호출 없이 캐시된 포즈 적용 (렌더링 직전 최신 적용) + if (m_hasCachedPose && m_isRigidBodyFound) { - m_isRigidBodyFound = rbState.IsTracked; - if (m_isRigidBodyFound) - { - transform.localPosition = rbState.Pose.Position; - transform.localRotation = rbState.Pose.Orientation; - } + transform.localPosition = m_cachedPosition; + transform.localRotation = m_cachedRotation; } } } diff --git a/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackSkeletonAnimator_Mingle.cs b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackSkeletonAnimator_Mingle.cs index 854546b74..b7d0893b7 100644 --- a/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackSkeletonAnimator_Mingle.cs +++ b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackSkeletonAnimator_Mingle.cs @@ -52,7 +52,12 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour [HideInInspector] public bool isSkeletonFound = false; - private const float k_SkeletonCheckInterval = 0.1f; + // 스켈레톤 연결 체크 주기: 발견 전 1초(+백오프), 발견 후 2초 (기존 0.1초에서 완화) + private const float k_SkeletonCheckIntervalDefault = 1.0f; + private const float k_SkeletonCheckIntervalConnected = 2.0f; + private const float k_SkeletonCheckIntervalMax = 10.0f; // 백오프 최대치 + private float m_currentCheckInterval = k_SkeletonCheckIntervalDefault; + private int m_definitionRefreshRequests = 0; // 연속 재조회 요청 횟수 (백오프 계산용) private Coroutine m_checkCoroutine; private bool m_initialized = false; @@ -518,16 +523,38 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour RebuildBoneIdMapping(); Debug.Log($"[OptiTrack] 스켈레톤 '{SkeletonAssetName}' 연결 성공"); } + + // 발견됨 → 체크 주기 완화, 백오프 리셋 + if (isSkeletonFound) + { + m_currentCheckInterval = k_SkeletonCheckIntervalConnected; + m_definitionRefreshRequests = 0; + } } else { - // 스켈레톤 정의를 찾지 못함 → 서버에 정의 재조회 요청 + // 스켈레톤 정의를 찾지 못함 → 지수 백오프로 재조회 요청 빈도 제한 isSkeletonFound = false; - StreamingClient.RequestDefinitionRefresh(); + m_definitionRefreshRequests++; + + // 처음 3회까지는 즉시 요청, 이후 백오프 적용 + if (m_definitionRefreshRequests <= 3) + { + StreamingClient.RequestDefinitionRefresh(); + m_currentCheckInterval = k_SkeletonCheckIntervalDefault; + } + else + { + // 3회 초과 시 점진적 백오프 (2초 → 4초 → 8초 → 10초 상한) + m_currentCheckInterval = Mathf.Min( + k_SkeletonCheckIntervalDefault * Mathf.Pow(2f, m_definitionRefreshRequests - 3), + k_SkeletonCheckIntervalMax); + StreamingClient.RequestDefinitionRefresh(); + } } } - yield return new WaitForSeconds(k_SkeletonCheckInterval); + yield return new WaitForSeconds(m_currentCheckInterval); } } diff --git a/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackStreamingClient.cs b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackStreamingClient.cs index d38617e22..d80092596 100644 --- a/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackStreamingClient.cs +++ b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackStreamingClient.cs @@ -345,6 +345,8 @@ public class OptitrackStreamingClient : MonoBehaviour private bool m_hasDrawnCameras = false; private bool m_hasDrawnForcePlates = false; private bool m_subscribedToMarkers = false; + private float m_markerSubscribeRetryCooldown = 0f; // 마커 구독 실패 시 재시도 쿨다운 + private const float k_MarkerSubscribeRetryInterval = 10f; // 10초 간격 재시도 private OptitrackHiResTimer.Timestamp m_lastFrameDeliveryTimestamp; private Coroutine m_connectionHealthCoroutine = null; @@ -398,12 +400,25 @@ public class OptitrackStreamingClient : MonoBehaviour /// private object m_frameDataUpdateLock = new object(); + /// assetID → assetName 캐시 (GetMarkerName 내 3중 선형 탐색 제거용). + private Dictionary m_assetIdToNameCache = new Dictionary(); + // 중간에 새 스켈레톤이 생성된 경우 정의 재조회 플래그 (NatNet 스레드에서도 씀 → volatile) private volatile bool m_pendingDefinitionRefresh = false; private float m_definitionRefreshCooldown = 0f; // 자동 재연결 진행 중 여부 (중복 재연결 방지) private bool m_isReconnecting = false; + + // 녹화 상태 추적 (ToggleRecording에서 불필요한 이중 명령 방지) + private bool m_isRecording = false; + + // SetProperty 원격 명령을 최초 연결에서만 전송 (재연결 시 Motive 설정 반복 변경 방지) + private bool m_hasAppliedServerSettings = false; + + // 정의 재조회 횟수 제한 (Motive 부하 방지: 최대 횟수 초과 시 수동 트리거 필요) + private int m_definitionRefreshCount = 0; + private const int k_MaxAutoDefinitionRefreshes = 10; #endregion Private fields @@ -421,58 +436,69 @@ public class OptitrackStreamingClient : MonoBehaviour { if (DrawMarkers) { + // 마커 구독 실패 시 매 프레임 재시도 방지 — 쿨다운 적용 if (m_client != null && ConnectionType == ClientConnectionType.Unicast && !m_subscribedToMarkers) { - SubscribeMarkers(); + if (m_markerSubscribeRetryCooldown <= 0f) + { + SubscribeMarkers(); + if (!m_subscribedToMarkers) + m_markerSubscribeRetryCooldown = k_MarkerSubscribeRetryInterval; + } + else + { + m_markerSubscribeRetryCooldown -= Time.deltaTime; + } } - List markerIds = new List(); - //Debug.Log("markers: " + m_latestMarkerStates.Count); + // 락 범위 최소화: 데이터만 복사 → 락 해제 후 GameObject 생성/파괴 + var markerSnapshot = new Dictionary(); lock (m_frameDataUpdateLock) { - // Move existing spheres and create new ones if necessary - foreach (KeyValuePair markerEntry in m_latestMarkerStates) + foreach (var markerEntry in m_latestMarkerStates) { - if (m_latestMarkerSpheres.ContainsKey( markerEntry.Key )) - { - m_latestMarkerSpheres[markerEntry.Key].transform.position = markerEntry.Value.Position; - } - else - { - var sphere = GameObject.CreatePrimitive( PrimitiveType.Cube ); - sphere.transform.parent = this.transform; - sphere.transform.localScale = new Vector3( markerEntry.Value.Size, markerEntry.Value.Size, markerEntry.Value.Size ); - sphere.transform.position = markerEntry.Value.Position; - sphere.name = markerEntry.Value.Name; - if (markerEntry.Value.IsActive) - { - // Make active markers cyan colored - sphere.GetComponent().material.SetColor("_Color", Color.cyan); - } - m_latestMarkerSpheres[markerEntry.Key] = sphere; - } - markerIds.Add( markerEntry.Key ); + markerSnapshot[markerEntry.Key] = ( + markerEntry.Value.Position, + markerEntry.Value.Size, + markerEntry.Value.Name, + markerEntry.Value.IsActive + ); } - // find spheres to remove that weren't in the previous frame - List markerSphereIdsToDelete = new List(); - foreach (KeyValuePair markerSphereEntry in m_latestMarkerSpheres) + } + + // 락 밖에서 GameObject 업데이트/생성 + var activeIds = new HashSet(markerSnapshot.Count); + foreach (var kvp in markerSnapshot) + { + activeIds.Add(kvp.Key); + if (m_latestMarkerSpheres.ContainsKey(kvp.Key)) { - if (!markerIds.Contains( markerSphereEntry.Key )) - { - // stale marker, tag for removal - markerSphereIdsToDelete.Add( markerSphereEntry.Key ); - } + m_latestMarkerSpheres[kvp.Key].transform.position = kvp.Value.pos; } - // remove stale spheres - foreach(Int32 markerId in markerSphereIdsToDelete) + else { - if(m_latestMarkerSpheres.ContainsKey(markerId)) - { - Destroy( m_latestMarkerSpheres[markerId] ); - m_latestMarkerSpheres.Remove( markerId ); - } + var sphere = GameObject.CreatePrimitive(PrimitiveType.Cube); + sphere.transform.parent = this.transform; + sphere.transform.localScale = new Vector3(kvp.Value.size, kvp.Value.size, kvp.Value.size); + sphere.transform.position = kvp.Value.pos; + sphere.name = kvp.Value.name; + if (kvp.Value.isActive) + sphere.GetComponent().material.SetColor("_Color", Color.cyan); + m_latestMarkerSpheres[kvp.Key] = sphere; } } + // 락 밖에서 stale 오브젝트 제거 + var staleIds = new List(); + foreach (var sphereEntry in m_latestMarkerSpheres) + { + if (!activeIds.Contains(sphereEntry.Key)) + staleIds.Add(sphereEntry.Key); + } + foreach (var id in staleIds) + { + Destroy(m_latestMarkerSpheres[id]); + m_latestMarkerSpheres.Remove(id); + } } else { @@ -485,27 +511,22 @@ public class OptitrackStreamingClient : MonoBehaviour } - //Draw the camera positions once on startup. + // 락 밖에서 카메라 지오메트리 생성 (1회성 — 데이터는 m_cameraDefinitions에 이미 복사됨) if (DrawCameras && !m_hasDrawnCameras ) { if (m_client.ServerAppVersion >= new Version(3, 0, 0)) { - lock (m_frameDataUpdateLock) + // m_cameraDefinitions는 메인 스레드의 UpdateDefinitions()에서 채워짐 — 락 불필요 + var cameraGroup = new GameObject("Cameras"); + foreach (OptitrackCameraDefinition camera in m_cameraDefinitions) { - var cameraGroup = new GameObject("Cameras"); - //cameraGroup.transform.parent = this.transform; //Adds the camera group as a child of the streaming client - - // Create the geometry for cameras. - foreach (OptitrackCameraDefinition camera in m_cameraDefinitions) - { - var geometry = GameObject.CreatePrimitive(PrimitiveType.Cube); - geometry.transform.parent = cameraGroup.transform; - geometry.transform.localScale = new Vector3(0.1f, 0.1f, 0.1f); - geometry.transform.position = camera.Position; - geometry.transform.rotation = camera.Orientation; - geometry.name = camera.Name; - geometry.GetComponent().material.SetColor("_Color", Color.black); - } + var geometry = GameObject.CreatePrimitive(PrimitiveType.Cube); + geometry.transform.parent = cameraGroup.transform; + geometry.transform.localScale = new Vector3(0.1f, 0.1f, 0.1f); + geometry.transform.position = camera.Position; + geometry.transform.rotation = camera.Orientation; + geometry.name = camera.Name; + geometry.GetComponent().material.SetColor("_Color", Color.black); } } else @@ -513,43 +534,31 @@ public class OptitrackStreamingClient : MonoBehaviour Debug.LogWarning("Drawing cameras is only supported in Motive 3.0+."); } - m_hasDrawnCameras = true; } - - //Draw the camera positions once on startup. + // 락 밖에서 포스 플레이트 지오메트리 생성 (1회성) if (DrawForcePlates && !m_hasDrawnForcePlates) { - lock (m_frameDataUpdateLock) + var forcePlateGroup = new GameObject("Force Plates"); + foreach (OptitrackForcePlateDefinition plate in m_forcePlateDefinitions) { - var cameraGroup = new GameObject("Force Plates"); - //cameraGroup.transform.parent = this.transform; //Adds the camera group as a child of the streaming client + Vector3 p0 = new Vector3(-plate.Corners[0], plate.Corners[1], plate.Corners[2]); + Vector3 p1 = new Vector3(-plate.Corners[3], plate.Corners[4], plate.Corners[5]); + Vector3 p2 = new Vector3(-plate.Corners[6], plate.Corners[7], plate.Corners[8]); + Vector3 p3 = new Vector3(-plate.Corners[9], plate.Corners[10], plate.Corners[11]); + Vector3 pAverage = (p0 + p1 + p2 + p3) / 4; - // Create the geometry for cameras. - foreach (OptitrackForcePlateDefinition plate in m_forcePlateDefinitions) - { - // Corner Locations (Adjusted for Unity world space) - Vector3 p0 = new Vector3(-plate.Corners[0], plate.Corners[1], plate.Corners[2]); - Vector3 p1 = new Vector3(-plate.Corners[3], plate.Corners[4], plate.Corners[5]); - Vector3 p2 = new Vector3(-plate.Corners[6], plate.Corners[7], plate.Corners[8]); - Vector3 p3 = new Vector3(-plate.Corners[9], plate.Corners[10], plate.Corners[11]); - Vector3 pAverage = (p0 + p1 + p2 + p3) / 4; - - var geometry = GameObject.CreatePrimitive(PrimitiveType.Cube); - geometry.transform.parent = cameraGroup.transform; - geometry.transform.localScale = new Vector3(plate.Length * 0.0254f, 0.03f, plate.Width * 0.0254f); // inches to meters - geometry.transform.position = pAverage; // Corner of the plate - geometry.transform.rotation = Quaternion.LookRotation(p2 - p1); //Quaternion.identity; - geometry.name = plate.SerialNumber; - geometry.GetComponent().material.SetColor("_Color", Color.blue); - - } - - m_hasDrawnForcePlates = true; + var geometry = GameObject.CreatePrimitive(PrimitiveType.Cube); + geometry.transform.parent = forcePlateGroup.transform; + geometry.transform.localScale = new Vector3(plate.Length * 0.0254f, 0.03f, plate.Width * 0.0254f); + geometry.transform.position = pAverage; + geometry.transform.rotation = Quaternion.LookRotation(p2 - p1); + geometry.name = plate.SerialNumber; + geometry.GetComponent().material.SetColor("_Color", Color.blue); } - + m_hasDrawnForcePlates = true; } //if (TimecodeProvider) @@ -560,58 +569,54 @@ public class OptitrackStreamingClient : MonoBehaviour // Trained Markerset Markers if requested to draw // trained markerset added if (DrawTMarkersetMarkers) { - //if (m_client != null && ConnectionType == ClientConnectionType.Unicast && !m_subscribedToTMarkMarkers) - //{ - // SubscribeTMarkMarkers(); - //} - - List tmarkmarkerIds = new List(); - //Debug.Log("tmark states: " + m_latestTMarkMarkerStates.Count); + // 락 범위 최소화: 데이터만 복사 → 락 해제 후 GameObject 생성/파괴 + var tmarkSnapshot = new Dictionary(); lock (m_frameDataUpdateLock) { - // Move existing spheres and create new ones if necessary - foreach (KeyValuePair markerEntry in m_latestTMarkMarkerStates) + foreach (var markerEntry in m_latestTMarkMarkerStates) { - if (m_latestTMarkMarkerSpheres.ContainsKey(markerEntry.Key)) - { - m_latestTMarkMarkerSpheres[markerEntry.Key].transform.position = markerEntry.Value.Position; - } - else - { - var cube = GameObject.CreatePrimitive(PrimitiveType.Cube); - cube.transform.parent = this.transform; - cube.transform.localScale = new Vector3(markerEntry.Value.Size, markerEntry.Value.Size, markerEntry.Value.Size); - cube.transform.position = markerEntry.Value.Position; - cube.name = markerEntry.Value.Name; - if (markerEntry.Value.IsActive) - { - // Make active markers cyan colored - cube.GetComponent().material.SetColor("_Color", Color.cyan); - } - m_latestTMarkMarkerSpheres[markerEntry.Key] = cube; - } - tmarkmarkerIds.Add(markerEntry.Key); + tmarkSnapshot[markerEntry.Key] = ( + markerEntry.Value.Position, + markerEntry.Value.Size, + markerEntry.Value.Name, + markerEntry.Value.IsActive + ); } - // find spheres to remove that weren't in the previous frame - List markerCubeIdsToDelete = new List(); - foreach (KeyValuePair markerCubeEntry in m_latestTMarkMarkerSpheres) + } + + // 락 밖에서 GameObject 업데이트/생성 + var activeTMarkIds = new HashSet(tmarkSnapshot.Count); + foreach (var kvp in tmarkSnapshot) + { + activeTMarkIds.Add(kvp.Key); + if (m_latestTMarkMarkerSpheres.ContainsKey(kvp.Key)) { - if (!tmarkmarkerIds.Contains(markerCubeEntry.Key)) - { - // stale marker, tag for removal - markerCubeIdsToDelete.Add(markerCubeEntry.Key); - } + m_latestTMarkMarkerSpheres[kvp.Key].transform.position = kvp.Value.pos; } - // remove stale spheres - foreach (Int32 markerId in markerCubeIdsToDelete) + else { - if (m_latestTMarkMarkerSpheres.ContainsKey(markerId)) - { - Destroy(m_latestTMarkMarkerSpheres[markerId]); - m_latestTMarkMarkerSpheres.Remove(markerId); - } + var cube = GameObject.CreatePrimitive(PrimitiveType.Cube); + cube.transform.parent = this.transform; + cube.transform.localScale = new Vector3(kvp.Value.size, kvp.Value.size, kvp.Value.size); + cube.transform.position = kvp.Value.pos; + cube.name = kvp.Value.name; + if (kvp.Value.isActive) + cube.GetComponent().material.SetColor("_Color", Color.cyan); + m_latestTMarkMarkerSpheres[kvp.Key] = cube; } } + // 락 밖에서 stale 오브젝트 제거 + var staleTMarkIds = new List(); + foreach (var cubeEntry in m_latestTMarkMarkerSpheres) + { + if (!activeTMarkIds.Contains(cubeEntry.Key)) + staleTMarkIds.Add(cubeEntry.Key); + } + foreach (var id in staleTMarkIds) + { + Destroy(m_latestTMarkMarkerSpheres[id]); + m_latestTMarkMarkerSpheres.Remove(id); + } } else { @@ -624,14 +629,27 @@ public class OptitrackStreamingClient : MonoBehaviour } // 미등록 스켈레톤이 프레임에 등장했을 때 정의 자동 재조회 (Motive 중간 생성 대응) + // 쿨다운 15초 + 최대 횟수 제한으로 Motive DataDescription 요청 폭풍 방지 if (m_pendingDefinitionRefresh && m_definitionRefreshCooldown <= 0f && m_client != null && !SkipDataDescriptions) { m_pendingDefinitionRefresh = false; - m_definitionRefreshCooldown = 5f; - try { UpdateDefinitions(); } - catch (Exception ex) { Debug.LogException(ex, this); } + + if (m_definitionRefreshCount < k_MaxAutoDefinitionRefreshes) + { + m_definitionRefreshCooldown = 15f; + m_definitionRefreshCount++; + try { UpdateDefinitions(); } + catch (Exception ex) { Debug.LogException(ex, this); } + } + else + { + // 최대 횟수 초과 — 경고 로그 출력 후 자동 재조회 중단 + m_definitionRefreshCooldown = 60f; // 1분 후 다시 시도 허용 + Debug.LogWarning(GetType().FullName + ": 자동 정의 재조회 최대 횟수(" + k_MaxAutoDefinitionRefreshes + "회) 초과. Motive에서 에셋을 확인하세요.", this); + } } - m_definitionRefreshCooldown -= Time.deltaTime; + if (m_definitionRefreshCooldown > 0f) + m_definitionRefreshCooldown -= Time.deltaTime; } @@ -666,7 +684,9 @@ public class OptitrackStreamingClient : MonoBehaviour { if(m_client != null) { - return m_client.RequestCommand("StartRecording"); + bool result = m_client.RequestCommand("StartRecording"); + if (result) m_isRecording = true; + return result; } return false; } @@ -679,7 +699,9 @@ public class OptitrackStreamingClient : MonoBehaviour { if (m_client != null) { - return m_client.RequestCommand("StopRecording"); + bool result = m_client.RequestCommand("StopRecording"); + if (result) m_isRecording = false; + return result; } return false; } @@ -730,6 +752,9 @@ public class OptitrackStreamingClient : MonoBehaviour if (m_receivedFrameSinceConnect) { Debug.Log(GetType().FullName + ": 재연결 성공.", this); + // 재연결 성공 후에만 녹화 재시작 + if (RecordOnPlay) + StartRecording(); m_isReconnecting = false; yield break; } @@ -744,7 +769,7 @@ public class OptitrackStreamingClient : MonoBehaviour } if (m_client != null) { - if (RecordOnPlay) StopRecording(); + // 재연결 루프 중에는 녹화 중지 명령 생략 — Motive 디스크 I/O 반복 방지 try { m_client.NativeFrameReceived -= OnNatNetFrameReceived; } catch (System.Exception) { } try { m_client.Disconnect(); } catch (System.Exception) { } try { m_client.Dispose(); } catch (System.Exception) { } @@ -833,21 +858,20 @@ public class OptitrackStreamingClient : MonoBehaviour { if (m_client != null) { - // Note: There's no direct way to check if recording is active, - // so we'll try to start recording first, and if it fails, try to stop - bool startResult = StartRecording(); - if (!startResult) + // 상태 추적 기반 토글 — 기존: "일단 Start → 실패하면 Stop" 이중 명령 → 1회 명령으로 개선 + if (m_isRecording) { - // If start failed, try to stop (might already be recording) - bool stopResult = StopRecording(); - if (stopResult) - { + if (StopRecording()) Debug.Log("OptiTrack: 레코딩을 중지했습니다."); - } + else + Debug.LogWarning("OptiTrack: 레코딩 중지에 실패했습니다."); } else { - Debug.Log("OptiTrack: 레코딩을 시작했습니다."); + if (StartRecording()) + Debug.Log("OptiTrack: 레코딩을 시작했습니다."); + else + Debug.LogWarning("OptiTrack: 레코딩 시작에 실패했습니다."); } } else @@ -1193,7 +1217,6 @@ public class OptitrackStreamingClient : MonoBehaviour public List GetLatestTMarkMarkerStates() // trained markerset added { List tmarkmarkerStates = new List(); - Debug.Log("GetLatestTMarkMarker: " + m_latestTMarkMarkerStates.Count); lock (m_frameDataUpdateLock) { @@ -1239,10 +1262,14 @@ public class OptitrackStreamingClient : MonoBehaviour } m_dataDescs = m_client.GetDataDescriptions(descriptionTypeMask); + // 정의를 성공적으로 받았으므로 자동 재조회 카운터 리셋 + m_definitionRefreshCount = 0; + m_rigidBodyDefinitions.Clear(); m_skeletonDefinitions.Clear(); m_tmarkersetDefinitions.Clear(); m_mirrorBoneIdMaps.Clear(); // 스켈레톤 정의 변경 시 mirror map 캐시 무효화 + m_assetIdToNameCache.Clear(); // assetID→이름 캐시 무효화 // ---------------------------------- // - Translate Rigid Body Definitions @@ -1546,19 +1573,29 @@ public class OptitrackStreamingClient : MonoBehaviour m_client = new NatNetClient(); m_client.Connect( connType, localAddr, serverAddr ); - // Remotely change the Skeleton Coordinate property to Global/Local - if (SkeletonCoordinates == StreamingCoordinatesValues.Global) - m_client.RequestCommand("SetProperty,,Skeleton Coordinates,false"); - else - m_client.RequestCommand("SetProperty,,Skeleton Coordinates,true"); + // SetProperty는 최초 연결에서만 전송 — 재연결 시 Motive 글로벌 설정 반복 변경 방지 + if ( !m_hasAppliedServerSettings ) + { + // Remotely change the Skeleton Coordinate property to Global/Local + if (SkeletonCoordinates == StreamingCoordinatesValues.Global) + m_client.RequestCommand("SetProperty,,Skeleton Coordinates,false"); + else + m_client.RequestCommand("SetProperty,,Skeleton Coordinates,true"); - // Remotely change the Bone Naming Convention to Motive/FBX/BVH - if (BoneNamingConvention == OptitrackBoneNameConvention.Motive) - m_client.RequestCommand("SetProperty,,Bone Naming Convention,0"); - else if (BoneNamingConvention == OptitrackBoneNameConvention.FBX) - m_client.RequestCommand("SetProperty,,Bone Naming Convention,1"); - else if (BoneNamingConvention == OptitrackBoneNameConvention.BVH) - m_client.RequestCommand("SetProperty,,Bone Naming Convention,2"); + // Remotely change the Bone Naming Convention to Motive/FBX/BVH + if (BoneNamingConvention == OptitrackBoneNameConvention.Motive) + m_client.RequestCommand("SetProperty,,Bone Naming Convention,0"); + else if (BoneNamingConvention == OptitrackBoneNameConvention.FBX) + m_client.RequestCommand("SetProperty,,Bone Naming Convention,1"); + else if (BoneNamingConvention == OptitrackBoneNameConvention.BVH) + m_client.RequestCommand("SetProperty,,Bone Naming Convention,2"); + + m_hasAppliedServerSettings = true; + } + else + { + Debug.Log(GetType().FullName + ": 재연결 — SetProperty 명령 스킵 (최초 연결에서 이미 적용됨).", this); + } } catch ( Exception ex ) { @@ -1568,8 +1605,9 @@ public class OptitrackStreamingClient : MonoBehaviour yield break; } - // SetProperty 명령이 서버에 적용될 때까지 대기 (메인 스레드 블락 없이) - yield return new UnityEngine.WaitForSeconds( 0.1f ); + // SetProperty 명령이 서버에 적용될 때까지 대기 (재연결 시 SetProperty 스킵했으면 대기 불필요) + if (!m_isReconnecting) + yield return new UnityEngine.WaitForSeconds( 0.1f ); try { @@ -1592,7 +1630,8 @@ public class OptitrackStreamingClient : MonoBehaviour SubscribeTMarkerset(tmark.Value, tmark.Key); } - if (RecordOnPlay) + // 재연결 중에는 녹화 시작 스킵 — Motive의 Take 파일 반복 열기/닫기 방지 + if (RecordOnPlay && !m_isReconnecting) StartRecording(); byte[] NatNetVersion = m_client.ServerDescription.NatNetVersion; @@ -1783,6 +1822,19 @@ public class OptitrackStreamingClient : MonoBehaviour result = NaturalPoint.NatNetLib.NativeMethods.NatNet_Frame_Skeleton_GetRigidBodyCount( pFrame, skelIdx, out skelRbCount ); NatNetException.ThrowIfNotOK( result, "NatNet_Frame_Skeleton_GetRigidBodyCount failed." ); + // 스켈레톤 정의 검색을 본 루프 밖에서 1회만 수행 (기존: 매 본마다 선형 탐색) + OptitrackSkeletonDefinition skelDef = GetSkeletonDefinitionById(skeletonId); + if (skelDef == null) + { + // Motive에서 중간에 스켈레톤이 생성된 경우 — 메인 스레드에서 정의 재조회 예약 (중복 로그 방지) + if (!m_pendingDefinitionRefresh && m_definitionRefreshCount < k_MaxAutoDefinitionRefreshes) + { + Debug.LogWarning(GetType().FullName + ": 알 수 없는 스켈레톤 ID " + skeletonId + " — 정의 재조회 예약됨.", this); + m_pendingDefinitionRefresh = true; + } + continue; + } + for (int boneIdx = 0; boneIdx < skelRbCount; ++boneIdx) { sRigidBodyData boneData = new sRigidBodyData(); @@ -1814,16 +1866,6 @@ public class OptitrackStreamingClient : MonoBehaviour Vector3 parentBonePos = new Vector3(0,0,0); Quaternion parentBoneOri = new Quaternion(0,0,0,1); - OptitrackSkeletonDefinition skelDef = GetSkeletonDefinitionById(skeletonId); - if (skelDef == null) - { - // Motive에서 중간에 스켈레톤이 생성된 경우 — 메인 스레드에서 정의 재조회 예약 - if (!m_pendingDefinitionRefresh) - Debug.LogWarning(GetType().FullName + ": 알 수 없는 스켈레톤 ID " + skeletonId + " — 정의 재조회 예약됨.", this); - m_pendingDefinitionRefresh = true; - continue; - } - Int32 pId = skelDef.BoneIdToParentIdMap[boneId]; if (pId != 0) { @@ -1849,8 +1891,14 @@ public class OptitrackStreamingClient : MonoBehaviour // Ensure we have a state corresponding to this tmarkerset ID. OptitrackTMarkersetState tmarkState = GetOrCreateTMarkersetState(tmarkersetId); - // Enumerate this tmarkerset's bone rigid bodies. + // TMarkerset 정의 검색을 본 루프 밖에서 1회만 수행 (기존: 매 본마다 선형 탐색) Int32 tmarkRbCount = m_dataDescs.AssetDescriptions[tmarkIdx].RigidBodyCount; + OptitrackTMarkersetDefinition tmarkDef = GetTMarkersetDefinitionById(tmarkersetId); + if (tmarkDef == null) + { + Debug.LogError(GetType().FullName + ": OnNatNetFrameReceived, no corresponding tmarkerset definition for received tmarkerset frame data.", this); + continue; + } for (int boneIdx = 0; boneIdx < tmarkRbCount; ++boneIdx) { @@ -1858,13 +1906,9 @@ public class OptitrackStreamingClient : MonoBehaviour result = NaturalPoint.NatNetLib.NativeMethods.NatNet_Frame_TMarkerset_GetRigidBody(pFrame, tmarkIdx, boneIdx, out boneData); NatNetException.ThrowIfNotOK(result, "NatNet_Frame_TMarkerset_GetRigidBody failed."); - // In the context of frame data (unlike in the definition data), this ID value is a - // packed composite of both the asset/entity (tmarkerset) ID and member (bone) ID. Int32 boneTMarkId, boneId; NaturalPoint.NatNetLib.NativeMethods.NatNet_DecodeID(boneData.Id, out boneTMarkId, out boneId); - // TODO: Could pre-populate this map when the definitions are retrieved. - // Should never allocate after the first frame, at least. if (tmarkState.BonePoses.ContainsKey(boneId) == false) { tmarkState.BonePoses[boneId] = new OptitrackPose(); @@ -1883,13 +1927,6 @@ public class OptitrackStreamingClient : MonoBehaviour Vector3 parentBonePos = new Vector3(0, 0, 0); Quaternion parentBoneOri = new Quaternion(0, 0, 0, 1); - OptitrackTMarkersetDefinition tmarkDef = GetTMarkersetDefinitionById(tmarkersetId); - if (tmarkDef == null) - { - Debug.LogError(GetType().FullName + ": OnNatNetFrameReceived, no corresponding tmarkerset definition for received tmarkerset frame data.", this); - continue; - } - Int32 pId = tmarkDef.BoneIdToParentIdMap[boneId]; if (pId != -1) { @@ -1970,54 +2007,43 @@ public class OptitrackStreamingClient : MonoBehaviour private string GetMarkerName( sMarker marker ) { - int hashKey = marker.Id.GetHashCode(); - int assetID = marker.Id.GetHashCode() >> 16; // high word = Asset ID Number - int memberID = marker.Id.GetHashCode() & 0x00ffff; // low word = Member ID Number (constraint number) + int assetID = marker.Id >> 16; // high word = Asset ID Number + int memberID = marker.Id & 0x00ffff; // low word = Member ID Number (constraint number) - // Figure out the asset name if it exists. - string assetName = ""; - OptitrackRigidBodyDefinition rigidBodyDef = GetRigidBodyDefinitionById( assetID ); - OptitrackSkeletonDefinition skeletonDef = GetSkeletonDefinitionById( assetID ); - OptitrackTMarkersetDefinition tmarkersetDef = GetTMarkersetDefinitionById( assetID ); - - if (rigidBodyDef != null) + // assetID→이름 캐시 사용 (기존: 매 마커마다 3개 리스트 선형 탐색) + string assetName; + if (!m_assetIdToNameCache.TryGetValue(assetID, out assetName)) { - assetName = rigidBodyDef.Name; - } - else if (skeletonDef != null) - { - assetName = skeletonDef.Name; - } - else if (tmarkersetDef != null) - { - assetName = tmarkersetDef.Name; + assetName = ""; + OptitrackRigidBodyDefinition rigidBodyDef = GetRigidBodyDefinitionById( assetID ); + if (rigidBodyDef != null) + assetName = rigidBodyDef.Name; + else + { + OptitrackSkeletonDefinition skeletonDef = GetSkeletonDefinitionById( assetID ); + if (skeletonDef != null) + assetName = skeletonDef.Name; + else + { + OptitrackTMarkersetDefinition tmarkersetDef = GetTMarkersetDefinitionById( assetID ); + if (tmarkersetDef != null) + assetName = tmarkersetDef.Name; + } + } + m_assetIdToNameCache[assetID] = assetName; } - // Figure out if the marker is labeled or active bool IsLabeled = (marker.Params & 0x10) == 0; bool IsActive = (marker.Params & 0x20) != 0; - string name = ""; - // Go through the possible naming conventions for the markers - // Check different Active/Passive Labeled/Unlabeled Configurations. if (IsActive && !IsLabeled) - { - name = "Active " + marker.Id.ToString(); - } + return "Active " + marker.Id.ToString(); else if (IsActive && IsLabeled) - { - name = "Active " + marker.Id.ToString() +" (" + assetName + " Member ID: " + memberID + " )"; - } + return "Active " + marker.Id.ToString() + " (" + assetName + " Member ID: " + memberID + " )"; else if (!IsActive && !IsLabeled) - { - name = "Passive " + marker.Id.ToString(); - } - else if (!IsActive && IsLabeled) - { - name = "Passive (" + assetName + " Member ID: " + memberID + ")"; - } - - return name; + return "Passive " + marker.Id.ToString(); + else + return "Passive (" + assetName + " Member ID: " + memberID + ")"; } private void RigidBodyDataToState(sRigidBodyData rbData, OptitrackHiResTimer.Timestamp timestamp, OptitrackRigidBodyState rbState) @@ -2033,8 +2059,8 @@ public class OptitrackStreamingClient : MonoBehaviour private void ResetStreamingSubscriptions() { - m_client.RequestCommand( "SubscribeToData" ); // Clear all filters - m_client.RequestCommand( "SubscribeToData,AllTypes,None" ); // Unsubscribe from all data by default + // 1개 명령으로 통합: "SubscribeToData"는 모든 필터를 클리어하고 기본 상태(구독 없음)로 리셋 + m_client.RequestCommand( "SubscribeToData" ); }