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" );
}