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

View File

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

View File

@ -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
/// </summary>
private object m_frameDataUpdateLock = new object();
/// <summary>assetID → assetName 캐시 (GetMarkerName 내 3중 선형 탐색 제거용).</summary>
private Dictionary<Int32, string> m_assetIdToNameCache = new Dictionary<Int32, string>();
// 중간에 새 스켈레톤이 생성된 경우 정의 재조회 플래그 (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<Int32> markerIds = new List<Int32>();
//Debug.Log("markers: " + m_latestMarkerStates.Count);
// 락 범위 최소화: 데이터만 복사 → 락 해제 후 GameObject 생성/파괴
var markerSnapshot = new Dictionary<Int32, (Vector3 pos, float size, string name, bool isActive)>();
lock (m_frameDataUpdateLock)
{
// Move existing spheres and create new ones if necessary
foreach (KeyValuePair<Int32, OptitrackMarkerState> 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<Renderer>().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<Int32> markerSphereIdsToDelete = new List<Int32>();
foreach (KeyValuePair<Int32, GameObject> markerSphereEntry in m_latestMarkerSpheres)
}
// 락 밖에서 GameObject 업데이트/생성
var activeIds = new HashSet<Int32>(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<Renderer>().material.SetColor("_Color", Color.cyan);
m_latestMarkerSpheres[kvp.Key] = sphere;
}
}
// 락 밖에서 stale 오브젝트 제거
var staleIds = new List<Int32>();
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<Renderer>().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<Renderer>().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<Renderer>().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<Renderer>().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<Int32> tmarkmarkerIds = new List<Int32>();
//Debug.Log("tmark states: " + m_latestTMarkMarkerStates.Count);
// 락 범위 최소화: 데이터만 복사 → 락 해제 후 GameObject 생성/파괴
var tmarkSnapshot = new Dictionary<Int32, (Vector3 pos, float size, string name, bool isActive)>();
lock (m_frameDataUpdateLock)
{
// Move existing spheres and create new ones if necessary
foreach (KeyValuePair<Int32, OptitrackMarkerState> 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<Renderer>().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<Int32> markerCubeIdsToDelete = new List<Int32>();
foreach (KeyValuePair<Int32, GameObject> markerCubeEntry in m_latestTMarkMarkerSpheres)
}
// 락 밖에서 GameObject 업데이트/생성
var activeTMarkIds = new HashSet<Int32>(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<Renderer>().material.SetColor("_Color", Color.cyan);
m_latestTMarkMarkerSpheres[kvp.Key] = cube;
}
}
// 락 밖에서 stale 오브젝트 제거
var staleTMarkIds = new List<Int32>();
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<OptitrackMarkerState> GetLatestTMarkMarkerStates() // trained markerset added
{
List<OptitrackMarkerState> tmarkmarkerStates = new List<OptitrackMarkerState>();
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" );
}