Optimize: OptiTrack 플러그인 Motive 부하 최적화

- 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) <noreply@anthropic.com>
This commit is contained in:
qsxft258@gmail.com 2026-04-19 19:03:02 +09:00
parent f0b6a55649
commit 5aa805e16a
3 changed files with 295 additions and 239 deletions

View File

@ -39,7 +39,12 @@ public class OptitrackRigidBody : MonoBehaviour
private bool m_isRigidBodyFound = false; private bool m_isRigidBodyFound = false;
private int m_resolvedRigidBodyId = -1; 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 public bool isRigidBodyFound
{ {
@ -129,37 +134,35 @@ public class OptitrackRigidBody : MonoBehaviour
if (m_streamingClient == null || m_resolvedRigidBodyId == -1) if (m_streamingClient == null || m_resolvedRigidBodyId == -1)
return; return;
// NatNet 호출은 Update()에서 1회만 — 캐시하여 OnBeforeRender()에서 재사용
OptitrackRigidBodyState rbState = m_streamingClient.GetLatestRigidBodyState(m_resolvedRigidBodyId, useNetworkCompensation); OptitrackRigidBodyState rbState = m_streamingClient.GetLatestRigidBodyState(m_resolvedRigidBodyId, useNetworkCompensation);
if (rbState != null) if (rbState != null)
{ {
m_isRigidBodyFound = rbState.IsTracked; m_isRigidBodyFound = rbState.IsTracked;
if (m_isRigidBodyFound) if (m_isRigidBodyFound)
{ {
transform.localPosition = rbState.Pose.Position; m_cachedPosition = rbState.Pose.Position;
transform.localRotation = rbState.Pose.Orientation; m_cachedRotation = rbState.Pose.Orientation;
m_hasCachedPose = true;
transform.localPosition = m_cachedPosition;
transform.localRotation = m_cachedRotation;
} }
} }
else else
{ {
m_isRigidBodyFound = false; m_isRigidBodyFound = false;
m_hasCachedPose = false;
} }
} }
void UpdatePose() void UpdatePose()
{ {
if (m_streamingClient == null || m_resolvedRigidBodyId == -1) // OnBeforeRender용: NatNet 재호출 없이 캐시된 포즈 적용 (렌더링 직전 최신 적용)
return; if (m_hasCachedPose && m_isRigidBodyFound)
OptitrackRigidBodyState rbState = m_streamingClient.GetLatestRigidBodyState(m_resolvedRigidBodyId, useNetworkCompensation);
if (rbState != null)
{ {
m_isRigidBodyFound = rbState.IsTracked; transform.localPosition = m_cachedPosition;
if (m_isRigidBodyFound) transform.localRotation = m_cachedRotation;
{
transform.localPosition = rbState.Pose.Position;
transform.localRotation = rbState.Pose.Orientation;
}
} }
} }
} }

View File

@ -52,7 +52,12 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
[HideInInspector] [HideInInspector]
public bool isSkeletonFound = false; 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 Coroutine m_checkCoroutine;
private bool m_initialized = false; private bool m_initialized = false;
@ -518,16 +523,38 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
RebuildBoneIdMapping(); RebuildBoneIdMapping();
Debug.Log($"[OptiTrack] 스켈레톤 '{SkeletonAssetName}' 연결 성공"); Debug.Log($"[OptiTrack] 스켈레톤 '{SkeletonAssetName}' 연결 성공");
} }
// 발견됨 → 체크 주기 완화, 백오프 리셋
if (isSkeletonFound)
{
m_currentCheckInterval = k_SkeletonCheckIntervalConnected;
m_definitionRefreshRequests = 0;
}
} }
else else
{ {
// 스켈레톤 정의를 찾지 못함 → 서버에 정의 재조회 요청 // 스켈레톤 정의를 찾지 못함 → 지수 백오프로 재조회 요청 빈도 제한
isSkeletonFound = false; isSkeletonFound = false;
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(); StreamingClient.RequestDefinitionRefresh();
} }
} }
}
yield return new WaitForSeconds(k_SkeletonCheckInterval); yield return new WaitForSeconds(m_currentCheckInterval);
} }
} }

View File

@ -345,6 +345,8 @@ public class OptitrackStreamingClient : MonoBehaviour
private bool m_hasDrawnCameras = false; private bool m_hasDrawnCameras = false;
private bool m_hasDrawnForcePlates = false; private bool m_hasDrawnForcePlates = false;
private bool m_subscribedToMarkers = false; private bool m_subscribedToMarkers = false;
private float m_markerSubscribeRetryCooldown = 0f; // 마커 구독 실패 시 재시도 쿨다운
private const float k_MarkerSubscribeRetryInterval = 10f; // 10초 간격 재시도
private OptitrackHiResTimer.Timestamp m_lastFrameDeliveryTimestamp; private OptitrackHiResTimer.Timestamp m_lastFrameDeliveryTimestamp;
private Coroutine m_connectionHealthCoroutine = null; private Coroutine m_connectionHealthCoroutine = null;
@ -398,12 +400,25 @@ public class OptitrackStreamingClient : MonoBehaviour
/// </summary> /// </summary>
private object m_frameDataUpdateLock = new object(); private object m_frameDataUpdateLock = new object();
/// <summary>assetID → assetName 캐시 (GetMarkerName 내 3중 선형 탐색 제거용).</summary>
private Dictionary<Int32, string> m_assetIdToNameCache = new Dictionary<Int32, string>();
// 중간에 새 스켈레톤이 생성된 경우 정의 재조회 플래그 (NatNet 스레드에서도 씀 → volatile) // 중간에 새 스켈레톤이 생성된 경우 정의 재조회 플래그 (NatNet 스레드에서도 씀 → volatile)
private volatile bool m_pendingDefinitionRefresh = false; private volatile bool m_pendingDefinitionRefresh = false;
private float m_definitionRefreshCooldown = 0f; private float m_definitionRefreshCooldown = 0f;
// 자동 재연결 진행 중 여부 (중복 재연결 방지) // 자동 재연결 진행 중 여부 (중복 재연결 방지)
private bool m_isReconnecting = false; 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 #endregion Private fields
@ -421,57 +436,68 @@ public class OptitrackStreamingClient : MonoBehaviour
{ {
if (DrawMarkers) if (DrawMarkers)
{ {
// 마커 구독 실패 시 매 프레임 재시도 방지 — 쿨다운 적용
if (m_client != null && ConnectionType == ClientConnectionType.Unicast && !m_subscribedToMarkers) if (m_client != null && ConnectionType == ClientConnectionType.Unicast && !m_subscribedToMarkers)
{
if (m_markerSubscribeRetryCooldown <= 0f)
{ {
SubscribeMarkers(); SubscribeMarkers();
} if (!m_subscribedToMarkers)
m_markerSubscribeRetryCooldown = k_MarkerSubscribeRetryInterval;
List<Int32> markerIds = new List<Int32>();
//Debug.Log("markers: " + m_latestMarkerStates.Count);
lock (m_frameDataUpdateLock)
{
// Move existing spheres and create new ones if necessary
foreach (KeyValuePair<Int32, OptitrackMarkerState> markerEntry in m_latestMarkerStates)
{
if (m_latestMarkerSpheres.ContainsKey( markerEntry.Key ))
{
m_latestMarkerSpheres[markerEntry.Key].transform.position = markerEntry.Value.Position;
} }
else else
{ {
var sphere = GameObject.CreatePrimitive( PrimitiveType.Cube ); m_markerSubscribeRetryCooldown -= Time.deltaTime;
}
}
// 락 범위 최소화: 데이터만 복사 → 락 해제 후 GameObject 생성/파괴
var markerSnapshot = new Dictionary<Int32, (Vector3 pos, float size, string name, bool isActive)>();
lock (m_frameDataUpdateLock)
{
foreach (var markerEntry in m_latestMarkerStates)
{
markerSnapshot[markerEntry.Key] = (
markerEntry.Value.Position,
markerEntry.Value.Size,
markerEntry.Value.Name,
markerEntry.Value.IsActive
);
}
}
// 락 밖에서 GameObject 업데이트/생성
var activeIds = new HashSet<Int32>(markerSnapshot.Count);
foreach (var kvp in markerSnapshot)
{
activeIds.Add(kvp.Key);
if (m_latestMarkerSpheres.ContainsKey(kvp.Key))
{
m_latestMarkerSpheres[kvp.Key].transform.position = kvp.Value.pos;
}
else
{
var sphere = GameObject.CreatePrimitive(PrimitiveType.Cube);
sphere.transform.parent = this.transform; sphere.transform.parent = this.transform;
sphere.transform.localScale = new Vector3( markerEntry.Value.Size, markerEntry.Value.Size, markerEntry.Value.Size ); sphere.transform.localScale = new Vector3(kvp.Value.size, kvp.Value.size, kvp.Value.size);
sphere.transform.position = markerEntry.Value.Position; sphere.transform.position = kvp.Value.pos;
sphere.name = markerEntry.Value.Name; sphere.name = kvp.Value.name;
if (markerEntry.Value.IsActive) if (kvp.Value.isActive)
{
// Make active markers cyan colored
sphere.GetComponent<Renderer>().material.SetColor("_Color", Color.cyan); sphere.GetComponent<Renderer>().material.SetColor("_Color", Color.cyan);
m_latestMarkerSpheres[kvp.Key] = sphere;
} }
m_latestMarkerSpheres[markerEntry.Key] = sphere;
} }
markerIds.Add( markerEntry.Key ); // 락 밖에서 stale 오브젝트 제거
} var staleIds = new List<Int32>();
// find spheres to remove that weren't in the previous frame foreach (var sphereEntry in m_latestMarkerSpheres)
List<Int32> markerSphereIdsToDelete = new List<Int32>();
foreach (KeyValuePair<Int32, GameObject> markerSphereEntry in m_latestMarkerSpheres)
{ {
if (!markerIds.Contains( markerSphereEntry.Key )) if (!activeIds.Contains(sphereEntry.Key))
staleIds.Add(sphereEntry.Key);
}
foreach (var id in staleIds)
{ {
// stale marker, tag for removal Destroy(m_latestMarkerSpheres[id]);
markerSphereIdsToDelete.Add( markerSphereEntry.Key ); m_latestMarkerSpheres.Remove(id);
}
}
// remove stale spheres
foreach(Int32 markerId in markerSphereIdsToDelete)
{
if(m_latestMarkerSpheres.ContainsKey(markerId))
{
Destroy( m_latestMarkerSpheres[markerId] );
m_latestMarkerSpheres.Remove( markerId );
}
}
} }
} }
else else
@ -485,17 +511,13 @@ public class OptitrackStreamingClient : MonoBehaviour
} }
//Draw the camera positions once on startup. // 락 밖에서 카메라 지오메트리 생성 (1회성 — 데이터는 m_cameraDefinitions에 이미 복사됨)
if (DrawCameras && !m_hasDrawnCameras ) if (DrawCameras && !m_hasDrawnCameras )
{ {
if (m_client.ServerAppVersion >= new Version(3, 0, 0)) if (m_client.ServerAppVersion >= new Version(3, 0, 0))
{ {
lock (m_frameDataUpdateLock) // m_cameraDefinitions는 메인 스레드의 UpdateDefinitions()에서 채워짐 — 락 불필요
{
var cameraGroup = new GameObject("Cameras"); 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) foreach (OptitrackCameraDefinition camera in m_cameraDefinitions)
{ {
var geometry = GameObject.CreatePrimitive(PrimitiveType.Cube); var geometry = GameObject.CreatePrimitive(PrimitiveType.Cube);
@ -507,29 +529,20 @@ public class OptitrackStreamingClient : MonoBehaviour
geometry.GetComponent<Renderer>().material.SetColor("_Color", Color.black); geometry.GetComponent<Renderer>().material.SetColor("_Color", Color.black);
} }
} }
}
else else
{ {
Debug.LogWarning("Drawing cameras is only supported in Motive 3.0+."); Debug.LogWarning("Drawing cameras is only supported in Motive 3.0+.");
} }
m_hasDrawnCameras = true; m_hasDrawnCameras = true;
} }
// 락 밖에서 포스 플레이트 지오메트리 생성 (1회성)
//Draw the camera positions once on startup.
if (DrawForcePlates && !m_hasDrawnForcePlates) if (DrawForcePlates && !m_hasDrawnForcePlates)
{ {
lock (m_frameDataUpdateLock) var forcePlateGroup = new GameObject("Force Plates");
{
var cameraGroup = new GameObject("Force Plates");
//cameraGroup.transform.parent = this.transform; //Adds the camera group as a child of the streaming client
// Create the geometry for cameras.
foreach (OptitrackForcePlateDefinition plate in m_forcePlateDefinitions) 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 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 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 p2 = new Vector3(-plate.Corners[6], plate.Corners[7], plate.Corners[8]);
@ -537,21 +550,17 @@ public class OptitrackStreamingClient : MonoBehaviour
Vector3 pAverage = (p0 + p1 + p2 + p3) / 4; Vector3 pAverage = (p0 + p1 + p2 + p3) / 4;
var geometry = GameObject.CreatePrimitive(PrimitiveType.Cube); var geometry = GameObject.CreatePrimitive(PrimitiveType.Cube);
geometry.transform.parent = cameraGroup.transform; geometry.transform.parent = forcePlateGroup.transform;
geometry.transform.localScale = new Vector3(plate.Length * 0.0254f, 0.03f, plate.Width * 0.0254f); // inches to meters geometry.transform.localScale = new Vector3(plate.Length * 0.0254f, 0.03f, plate.Width * 0.0254f);
geometry.transform.position = pAverage; // Corner of the plate geometry.transform.position = pAverage;
geometry.transform.rotation = Quaternion.LookRotation(p2 - p1); //Quaternion.identity; geometry.transform.rotation = Quaternion.LookRotation(p2 - p1);
geometry.name = plate.SerialNumber; geometry.name = plate.SerialNumber;
geometry.GetComponent<Renderer>().material.SetColor("_Color", Color.blue); geometry.GetComponent<Renderer>().material.SetColor("_Color", Color.blue);
} }
m_hasDrawnForcePlates = true; m_hasDrawnForcePlates = true;
} }
}
//if (TimecodeProvider) //if (TimecodeProvider)
//{ //{
// Debug.Log(""); // Debug.Log("");
@ -560,57 +569,53 @@ public class OptitrackStreamingClient : MonoBehaviour
// Trained Markerset Markers if requested to draw // trained markerset added // Trained Markerset Markers if requested to draw // trained markerset added
if (DrawTMarkersetMarkers) if (DrawTMarkersetMarkers)
{ {
//if (m_client != null && ConnectionType == ClientConnectionType.Unicast && !m_subscribedToTMarkMarkers) // 락 범위 최소화: 데이터만 복사 → 락 해제 후 GameObject 생성/파괴
//{ var tmarkSnapshot = new Dictionary<Int32, (Vector3 pos, float size, string name, bool isActive)>();
// SubscribeTMarkMarkers();
//}
List<Int32> tmarkmarkerIds = new List<Int32>();
//Debug.Log("tmark states: " + m_latestTMarkMarkerStates.Count);
lock (m_frameDataUpdateLock) lock (m_frameDataUpdateLock)
{ {
// Move existing spheres and create new ones if necessary foreach (var markerEntry in m_latestTMarkMarkerStates)
foreach (KeyValuePair<Int32, OptitrackMarkerState> markerEntry in m_latestTMarkMarkerStates)
{ {
if (m_latestTMarkMarkerSpheres.ContainsKey(markerEntry.Key)) tmarkSnapshot[markerEntry.Key] = (
markerEntry.Value.Position,
markerEntry.Value.Size,
markerEntry.Value.Name,
markerEntry.Value.IsActive
);
}
}
// 락 밖에서 GameObject 업데이트/생성
var activeTMarkIds = new HashSet<Int32>(tmarkSnapshot.Count);
foreach (var kvp in tmarkSnapshot)
{ {
m_latestTMarkMarkerSpheres[markerEntry.Key].transform.position = markerEntry.Value.Position; activeTMarkIds.Add(kvp.Key);
if (m_latestTMarkMarkerSpheres.ContainsKey(kvp.Key))
{
m_latestTMarkMarkerSpheres[kvp.Key].transform.position = kvp.Value.pos;
} }
else else
{ {
var cube = GameObject.CreatePrimitive(PrimitiveType.Cube); var cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
cube.transform.parent = this.transform; cube.transform.parent = this.transform;
cube.transform.localScale = new Vector3(markerEntry.Value.Size, markerEntry.Value.Size, markerEntry.Value.Size); cube.transform.localScale = new Vector3(kvp.Value.size, kvp.Value.size, kvp.Value.size);
cube.transform.position = markerEntry.Value.Position; cube.transform.position = kvp.Value.pos;
cube.name = markerEntry.Value.Name; cube.name = kvp.Value.name;
if (markerEntry.Value.IsActive) if (kvp.Value.isActive)
{
// Make active markers cyan colored
cube.GetComponent<Renderer>().material.SetColor("_Color", Color.cyan); cube.GetComponent<Renderer>().material.SetColor("_Color", Color.cyan);
m_latestTMarkMarkerSpheres[kvp.Key] = cube;
} }
m_latestTMarkMarkerSpheres[markerEntry.Key] = cube;
} }
tmarkmarkerIds.Add(markerEntry.Key); // 락 밖에서 stale 오브젝트 제거
} var staleTMarkIds = new List<Int32>();
// find spheres to remove that weren't in the previous frame foreach (var cubeEntry in m_latestTMarkMarkerSpheres)
List<Int32> markerCubeIdsToDelete = new List<Int32>();
foreach (KeyValuePair<Int32, GameObject> markerCubeEntry in m_latestTMarkMarkerSpheres)
{ {
if (!tmarkmarkerIds.Contains(markerCubeEntry.Key)) if (!activeTMarkIds.Contains(cubeEntry.Key))
staleTMarkIds.Add(cubeEntry.Key);
}
foreach (var id in staleTMarkIds)
{ {
// stale marker, tag for removal Destroy(m_latestTMarkMarkerSpheres[id]);
markerCubeIdsToDelete.Add(markerCubeEntry.Key); m_latestTMarkMarkerSpheres.Remove(id);
}
}
// remove stale spheres
foreach (Int32 markerId in markerCubeIdsToDelete)
{
if (m_latestTMarkMarkerSpheres.ContainsKey(markerId))
{
Destroy(m_latestTMarkMarkerSpheres[markerId]);
m_latestTMarkMarkerSpheres.Remove(markerId);
}
}
} }
} }
else else
@ -624,13 +629,26 @@ public class OptitrackStreamingClient : MonoBehaviour
} }
// 미등록 스켈레톤이 프레임에 등장했을 때 정의 자동 재조회 (Motive 중간 생성 대응) // 미등록 스켈레톤이 프레임에 등장했을 때 정의 자동 재조회 (Motive 중간 생성 대응)
// 쿨다운 15초 + 최대 횟수 제한으로 Motive DataDescription 요청 폭풍 방지
if (m_pendingDefinitionRefresh && m_definitionRefreshCooldown <= 0f && m_client != null && !SkipDataDescriptions) if (m_pendingDefinitionRefresh && m_definitionRefreshCooldown <= 0f && m_client != null && !SkipDataDescriptions)
{ {
m_pendingDefinitionRefresh = false; m_pendingDefinitionRefresh = false;
m_definitionRefreshCooldown = 5f;
if (m_definitionRefreshCount < k_MaxAutoDefinitionRefreshes)
{
m_definitionRefreshCooldown = 15f;
m_definitionRefreshCount++;
try { UpdateDefinitions(); } try { UpdateDefinitions(); }
catch (Exception ex) { Debug.LogException(ex, this); } catch (Exception ex) { Debug.LogException(ex, this); }
} }
else
{
// 최대 횟수 초과 — 경고 로그 출력 후 자동 재조회 중단
m_definitionRefreshCooldown = 60f; // 1분 후 다시 시도 허용
Debug.LogWarning(GetType().FullName + ": 자동 정의 재조회 최대 횟수(" + k_MaxAutoDefinitionRefreshes + "회) 초과. Motive에서 에셋을 확인하세요.", this);
}
}
if (m_definitionRefreshCooldown > 0f)
m_definitionRefreshCooldown -= Time.deltaTime; m_definitionRefreshCooldown -= Time.deltaTime;
} }
@ -666,7 +684,9 @@ public class OptitrackStreamingClient : MonoBehaviour
{ {
if(m_client != null) if(m_client != null)
{ {
return m_client.RequestCommand("StartRecording"); bool result = m_client.RequestCommand("StartRecording");
if (result) m_isRecording = true;
return result;
} }
return false; return false;
} }
@ -679,7 +699,9 @@ public class OptitrackStreamingClient : MonoBehaviour
{ {
if (m_client != null) if (m_client != null)
{ {
return m_client.RequestCommand("StopRecording"); bool result = m_client.RequestCommand("StopRecording");
if (result) m_isRecording = false;
return result;
} }
return false; return false;
} }
@ -730,6 +752,9 @@ public class OptitrackStreamingClient : MonoBehaviour
if (m_receivedFrameSinceConnect) if (m_receivedFrameSinceConnect)
{ {
Debug.Log(GetType().FullName + ": 재연결 성공.", this); Debug.Log(GetType().FullName + ": 재연결 성공.", this);
// 재연결 성공 후에만 녹화 재시작
if (RecordOnPlay)
StartRecording();
m_isReconnecting = false; m_isReconnecting = false;
yield break; yield break;
} }
@ -744,7 +769,7 @@ public class OptitrackStreamingClient : MonoBehaviour
} }
if (m_client != null) if (m_client != null)
{ {
if (RecordOnPlay) StopRecording(); // 재연결 루프 중에는 녹화 중지 명령 생략 — Motive 디스크 I/O 반복 방지
try { m_client.NativeFrameReceived -= OnNatNetFrameReceived; } catch (System.Exception) { } try { m_client.NativeFrameReceived -= OnNatNetFrameReceived; } catch (System.Exception) { }
try { m_client.Disconnect(); } catch (System.Exception) { } try { m_client.Disconnect(); } catch (System.Exception) { }
try { m_client.Dispose(); } catch (System.Exception) { } try { m_client.Dispose(); } catch (System.Exception) { }
@ -833,21 +858,20 @@ public class OptitrackStreamingClient : MonoBehaviour
{ {
if (m_client != null) if (m_client != null)
{ {
// Note: There's no direct way to check if recording is active, // 상태 추적 기반 토글 — 기존: "일단 Start → 실패하면 Stop" 이중 명령 → 1회 명령으로 개선
// so we'll try to start recording first, and if it fails, try to stop if (m_isRecording)
bool startResult = StartRecording();
if (!startResult)
{
// If start failed, try to stop (might already be recording)
bool stopResult = StopRecording();
if (stopResult)
{ {
if (StopRecording())
Debug.Log("OptiTrack: 레코딩을 중지했습니다."); Debug.Log("OptiTrack: 레코딩을 중지했습니다.");
} else
Debug.LogWarning("OptiTrack: 레코딩 중지에 실패했습니다.");
} }
else else
{ {
if (StartRecording())
Debug.Log("OptiTrack: 레코딩을 시작했습니다."); Debug.Log("OptiTrack: 레코딩을 시작했습니다.");
else
Debug.LogWarning("OptiTrack: 레코딩 시작에 실패했습니다.");
} }
} }
else else
@ -1193,7 +1217,6 @@ public class OptitrackStreamingClient : MonoBehaviour
public List<OptitrackMarkerState> GetLatestTMarkMarkerStates() // trained markerset added public List<OptitrackMarkerState> GetLatestTMarkMarkerStates() // trained markerset added
{ {
List<OptitrackMarkerState> tmarkmarkerStates = new List<OptitrackMarkerState>(); List<OptitrackMarkerState> tmarkmarkerStates = new List<OptitrackMarkerState>();
Debug.Log("GetLatestTMarkMarker: " + m_latestTMarkMarkerStates.Count);
lock (m_frameDataUpdateLock) lock (m_frameDataUpdateLock)
{ {
@ -1239,10 +1262,14 @@ public class OptitrackStreamingClient : MonoBehaviour
} }
m_dataDescs = m_client.GetDataDescriptions(descriptionTypeMask); m_dataDescs = m_client.GetDataDescriptions(descriptionTypeMask);
// 정의를 성공적으로 받았으므로 자동 재조회 카운터 리셋
m_definitionRefreshCount = 0;
m_rigidBodyDefinitions.Clear(); m_rigidBodyDefinitions.Clear();
m_skeletonDefinitions.Clear(); m_skeletonDefinitions.Clear();
m_tmarkersetDefinitions.Clear(); m_tmarkersetDefinitions.Clear();
m_mirrorBoneIdMaps.Clear(); // 스켈레톤 정의 변경 시 mirror map 캐시 무효화 m_mirrorBoneIdMaps.Clear(); // 스켈레톤 정의 변경 시 mirror map 캐시 무효화
m_assetIdToNameCache.Clear(); // assetID→이름 캐시 무효화
// ---------------------------------- // ----------------------------------
// - Translate Rigid Body Definitions // - Translate Rigid Body Definitions
@ -1546,6 +1573,9 @@ public class OptitrackStreamingClient : MonoBehaviour
m_client = new NatNetClient(); m_client = new NatNetClient();
m_client.Connect( connType, localAddr, serverAddr ); m_client.Connect( connType, localAddr, serverAddr );
// SetProperty는 최초 연결에서만 전송 — 재연결 시 Motive 글로벌 설정 반복 변경 방지
if ( !m_hasAppliedServerSettings )
{
// Remotely change the Skeleton Coordinate property to Global/Local // Remotely change the Skeleton Coordinate property to Global/Local
if (SkeletonCoordinates == StreamingCoordinatesValues.Global) if (SkeletonCoordinates == StreamingCoordinatesValues.Global)
m_client.RequestCommand("SetProperty,,Skeleton Coordinates,false"); m_client.RequestCommand("SetProperty,,Skeleton Coordinates,false");
@ -1559,6 +1589,13 @@ public class OptitrackStreamingClient : MonoBehaviour
m_client.RequestCommand("SetProperty,,Bone Naming Convention,1"); m_client.RequestCommand("SetProperty,,Bone Naming Convention,1");
else if (BoneNamingConvention == OptitrackBoneNameConvention.BVH) else if (BoneNamingConvention == OptitrackBoneNameConvention.BVH)
m_client.RequestCommand("SetProperty,,Bone Naming Convention,2"); m_client.RequestCommand("SetProperty,,Bone Naming Convention,2");
m_hasAppliedServerSettings = true;
}
else
{
Debug.Log(GetType().FullName + ": 재연결 — SetProperty 명령 스킵 (최초 연결에서 이미 적용됨).", this);
}
} }
catch ( Exception ex ) catch ( Exception ex )
{ {
@ -1568,7 +1605,8 @@ public class OptitrackStreamingClient : MonoBehaviour
yield break; yield break;
} }
// SetProperty 명령이 서버에 적용될 때까지 대기 (메인 스레드 블락 없이) // SetProperty 명령이 서버에 적용될 때까지 대기 (재연결 시 SetProperty 스킵했으면 대기 불필요)
if (!m_isReconnecting)
yield return new UnityEngine.WaitForSeconds( 0.1f ); yield return new UnityEngine.WaitForSeconds( 0.1f );
try try
@ -1592,7 +1630,8 @@ public class OptitrackStreamingClient : MonoBehaviour
SubscribeTMarkerset(tmark.Value, tmark.Key); SubscribeTMarkerset(tmark.Value, tmark.Key);
} }
if (RecordOnPlay) // 재연결 중에는 녹화 시작 스킵 — Motive의 Take 파일 반복 열기/닫기 방지
if (RecordOnPlay && !m_isReconnecting)
StartRecording(); StartRecording();
byte[] NatNetVersion = m_client.ServerDescription.NatNetVersion; 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 ); result = NaturalPoint.NatNetLib.NativeMethods.NatNet_Frame_Skeleton_GetRigidBodyCount( pFrame, skelIdx, out skelRbCount );
NatNetException.ThrowIfNotOK( result, "NatNet_Frame_Skeleton_GetRigidBodyCount failed." ); 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) for (int boneIdx = 0; boneIdx < skelRbCount; ++boneIdx)
{ {
sRigidBodyData boneData = new sRigidBodyData(); sRigidBodyData boneData = new sRigidBodyData();
@ -1814,16 +1866,6 @@ public class OptitrackStreamingClient : MonoBehaviour
Vector3 parentBonePos = new Vector3(0,0,0); Vector3 parentBonePos = new Vector3(0,0,0);
Quaternion parentBoneOri = new Quaternion(0,0,0,1); 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]; Int32 pId = skelDef.BoneIdToParentIdMap[boneId];
if (pId != 0) if (pId != 0)
{ {
@ -1849,8 +1891,14 @@ public class OptitrackStreamingClient : MonoBehaviour
// Ensure we have a state corresponding to this tmarkerset ID. // Ensure we have a state corresponding to this tmarkerset ID.
OptitrackTMarkersetState tmarkState = GetOrCreateTMarkersetState(tmarkersetId); OptitrackTMarkersetState tmarkState = GetOrCreateTMarkersetState(tmarkersetId);
// Enumerate this tmarkerset's bone rigid bodies. // TMarkerset 정의 검색을 본 루프 밖에서 1회만 수행 (기존: 매 본마다 선형 탐색)
Int32 tmarkRbCount = m_dataDescs.AssetDescriptions[tmarkIdx].RigidBodyCount; 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) 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); result = NaturalPoint.NatNetLib.NativeMethods.NatNet_Frame_TMarkerset_GetRigidBody(pFrame, tmarkIdx, boneIdx, out boneData);
NatNetException.ThrowIfNotOK(result, "NatNet_Frame_TMarkerset_GetRigidBody failed."); 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; Int32 boneTMarkId, boneId;
NaturalPoint.NatNetLib.NativeMethods.NatNet_DecodeID(boneData.Id, out boneTMarkId, out 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) if (tmarkState.BonePoses.ContainsKey(boneId) == false)
{ {
tmarkState.BonePoses[boneId] = new OptitrackPose(); tmarkState.BonePoses[boneId] = new OptitrackPose();
@ -1883,13 +1927,6 @@ public class OptitrackStreamingClient : MonoBehaviour
Vector3 parentBonePos = new Vector3(0, 0, 0); Vector3 parentBonePos = new Vector3(0, 0, 0);
Quaternion parentBoneOri = new Quaternion(0, 0, 0, 1); 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]; Int32 pId = tmarkDef.BoneIdToParentIdMap[boneId];
if (pId != -1) if (pId != -1)
{ {
@ -1970,54 +2007,43 @@ public class OptitrackStreamingClient : MonoBehaviour
private string GetMarkerName( sMarker marker ) private string GetMarkerName( sMarker marker )
{ {
int hashKey = marker.Id.GetHashCode(); int assetID = marker.Id >> 16; // high word = Asset ID Number
int assetID = marker.Id.GetHashCode() >> 16; // high word = Asset ID Number int memberID = marker.Id & 0x00ffff; // low word = Member ID Number (constraint number)
int memberID = marker.Id.GetHashCode() & 0x00ffff; // low word = Member ID Number (constraint number)
// Figure out the asset name if it exists. // assetID→이름 캐시 사용 (기존: 매 마커마다 3개 리스트 선형 탐색)
string assetName = ""; string assetName;
if (!m_assetIdToNameCache.TryGetValue(assetID, out assetName))
{
assetName = "";
OptitrackRigidBodyDefinition rigidBodyDef = GetRigidBodyDefinitionById( assetID ); OptitrackRigidBodyDefinition rigidBodyDef = GetRigidBodyDefinitionById( assetID );
OptitrackSkeletonDefinition skeletonDef = GetSkeletonDefinitionById( assetID );
OptitrackTMarkersetDefinition tmarkersetDef = GetTMarkersetDefinitionById( assetID );
if (rigidBodyDef != null) if (rigidBodyDef != null)
{
assetName = rigidBodyDef.Name; assetName = rigidBodyDef.Name;
} else
else if (skeletonDef != null)
{ {
OptitrackSkeletonDefinition skeletonDef = GetSkeletonDefinitionById( assetID );
if (skeletonDef != null)
assetName = skeletonDef.Name; assetName = skeletonDef.Name;
} else
else if (tmarkersetDef != null)
{ {
OptitrackTMarkersetDefinition tmarkersetDef = GetTMarkersetDefinitionById( assetID );
if (tmarkersetDef != null)
assetName = tmarkersetDef.Name; assetName = tmarkersetDef.Name;
} }
}
m_assetIdToNameCache[assetID] = assetName;
}
// Figure out if the marker is labeled or active
bool IsLabeled = (marker.Params & 0x10) == 0; bool IsLabeled = (marker.Params & 0x10) == 0;
bool IsActive = (marker.Params & 0x20) != 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) if (IsActive && !IsLabeled)
{ return "Active " + marker.Id.ToString();
name = "Active " + marker.Id.ToString();
}
else if (IsActive && IsLabeled) else if (IsActive && IsLabeled)
{ return "Active " + marker.Id.ToString() + " (" + assetName + " Member ID: " + memberID + " )";
name = "Active " + marker.Id.ToString() +" (" + assetName + " Member ID: " + memberID + " )";
}
else if (!IsActive && !IsLabeled) else if (!IsActive && !IsLabeled)
{ return "Passive " + marker.Id.ToString();
name = "Passive " + marker.Id.ToString(); else
} return "Passive (" + assetName + " Member ID: " + memberID + ")";
else if (!IsActive && IsLabeled)
{
name = "Passive (" + assetName + " Member ID: " + memberID + ")";
}
return name;
} }
private void RigidBodyDataToState(sRigidBodyData rbData, OptitrackHiResTimer.Timestamp timestamp, OptitrackRigidBodyState rbState) private void RigidBodyDataToState(sRigidBodyData rbData, OptitrackHiResTimer.Timestamp timestamp, OptitrackRigidBodyState rbState)
@ -2033,8 +2059,8 @@ public class OptitrackStreamingClient : MonoBehaviour
private void ResetStreamingSubscriptions() private void ResetStreamingSubscriptions()
{ {
m_client.RequestCommand( "SubscribeToData" ); // Clear all filters // 1개 명령으로 통합: "SubscribeToData"는 모든 필터를 클리어하고 기본 상태(구독 없음)로 리셋
m_client.RequestCommand( "SubscribeToData,AllTypes,None" ); // Unsubscribe from all data by default m_client.RequestCommand( "SubscribeToData" );
} }