/* Copyright (c) 2016 NaturalPoint Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ using System; using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Runtime.InteropServices; using System.Text; using System.Threading; using UnityEngine; using NaturalPoint; using NaturalPoint.NatNetLib; /// Skeleton naming conventions supported by OptiTrack Motive. public enum OptitrackBoneNameConvention { Motive, FBX, BVH, } public enum StreamingCoordinatesValues { Local, Global } /// Describes the position and orientation of a streamed tracked object. public class OptitrackPose { public Vector3 Position; public Quaternion Orientation; } /// Represents the state of a streamed marker. public class OptitrackMarkerState { public string Name; public Vector3 Position; public float Size; public bool Labeled; public Int32 Id; public bool IsActive; } /// Represents the state of a streamed rigid body at an instant in time. public class OptitrackRigidBodyState { public OptitrackHiResTimer.Timestamp DeliveryTimestamp; public OptitrackPose Pose; public bool IsTracked; } /// Represents the state of a streamed skeleton at an instant in time. public class OptitrackSkeletonState { /// Maps from OptiTrack bone IDs to their corresponding bone poses. public Dictionary BonePoses; public Dictionary LocalBonePoses; /// Timestamp for received NatNet frame data. public OptitrackHiResTimer.Timestamp DeliveryTimestamp; } /// Represents the state of a streamed trained markerset at an instant in time. public class OptitrackTMarkersetState // trained markerset added { /// Maps from OptiTrack bone IDs to their corresponding bone poses. public Dictionary BonePoses; public Dictionary LocalBonePoses; } public class OptitrackRigidBodyDefinition { public class MarkerDefinition { public Vector3 Position; public Int32 RequiredLabel; } public Int32 Id; public string Name; public List Markers; } /// Describes the hierarchy and neutral pose of a streamed skeleton. public class OptitrackSkeletonDefinition { public class BoneDefinition { /// The ID of this bone within this skeleton. public Int32 Id; /// The ID of this bone's parent bone. A value of 0 means that this is the root bone. public Int32 ParentId; /// The name of this bone. public string Name; /// /// This bone's position offset from its parent in the skeleton's neutral pose. /// (The neutral orientation is always .) /// public Vector3 Offset; } /// Skeleton ID. Used as an argument to . public Int32 Id; /// Skeleton asset name. public string Name; /// True when the definition was inferred from frame packets and does not contain a Motive asset name. public bool IsSynthetic; /// Bone names, hierarchy, and neutral pose position information. public List Bones; /// Bone hierarchy information public Dictionary BoneIdToParentIdMap; } public class OptitrackTMarkersetDefinition // trained markerset added // check where this is coming from { public class BoneDefinition { /// The ID of this bone within this trained markerset. public Int32 Id; /// The ID of this bone's parent bone. A value of 0 means that this is the root bone. public Int32 ParentId; /// The name of this bone. public string Name; /// /// This bone's position offset from its parent in the skeleton's neutral pose. /// (The neutral orientation is always .) /// public Vector3 Offset; } public class MarkerDefinition { /// The name of this marker. public string Name; public Vector3 Position; public Int32 Id; //public float Size; //public bool Labeled; //public bool IsActive; } /// Asset ID. Used as an argument to . public Int32 Id; /// Skeleton asset name. public string Name; /// Bone names, hierarchy, and neutral pose position information. public List Bones; /// Bone hierarchy information public Dictionary BoneIdToParentIdMap; public List Markers; } public class OptitrackMarkersDefinition { /// The name of this bone. public string Name; } public class OptitrackForcePlateDefinition { /// The ID of this force plate. public Int32 Id; /// The serial number of this force plate. public string SerialNumber; /// The width of the force plate. public float Width; /// The length of the force plate. public float Length; /// The electrical offset of this force plate. public Vector3 ElectricalOffset; /// The calibration matrix of this force plate. public List CalibrationMatrix; /// The corner locations of this force plate. public List Corners; /// The force plate type. public Int32 PlateType; /// The force plate channel data type. public Int32 ChannelDataType; /// The force plate channel count. public Int32 ChannelCount; /// The channel names for the force plate. public List ChannelNames; } public class OptitrackCameraDefinition { /// The name of this camera. public string Name; /// The name of this camera. public Vector3 Position; /// The name of this camera. public Quaternion Orientation; } public static class OptitrackHiResTimer { public struct Timestamp { internal Int64 m_ticks; public float AgeSeconds { get { return Now().SecondsSince( this ); } } public float SecondsSince( Timestamp reference ) { Int64 deltaTicks = m_ticks - reference.m_ticks; return deltaTicks / (float)System.Diagnostics.Stopwatch.Frequency; } } public static Timestamp Now() { return new Timestamp { m_ticks = System.Diagnostics.Stopwatch.GetTimestamp() }; } } /// /// Connects to a NatNet streaming server and makes the data available in lightweight Unity-friendly representations. /// public class OptitrackStreamingClient : MonoBehaviour { private const int k_DefaultNatNetCommandPort = 1510; private const int k_DefaultNatNetDataPort = 1511; public enum ClientConnectionType { Multicast, Unicast } [Header("Connection Settings")] [Tooltip("The Streaming IP (Local Interface) in Motive")] public string ServerAddress = "127.0.0.1"; [Tooltip("Must be on the same network as the Streaming IP (Local Interface) in Motive.")] [HideInInspector] public string LocalAddress = "127.0.0.1"; [Tooltip("Automatically selects the local IPv4 interface that routes to ServerAddress. LocalAddress is used as a fallback if detection fails.")] [HideInInspector] public bool AutoDetectLocalAddress = true; [Tooltip("The local IPv4 address selected for the active connection.")] [HideInInspector] public string ResolvedLocalAddress = ""; [Tooltip("Unicast performs subscription reducing your overall data set in some applications.")] public ClientConnectionType ConnectionType; [Header("NatNet Endpoint Override")] [Tooltip("NatNet command port. Motive/MMRP default = 1510.")] [Range(1, 65535)] public int CommandPort = k_DefaultNatNetCommandPort; [Tooltip("NatNet data port. Motive/MMRP default = 1511.")] [Range(1, 65535)] public int DataPort = k_DefaultNatNetDataPort; [Tooltip("Optional multicast group override. Empty uses the NatNet default group (usually 239.255.42.99). Use a separate group for replay, e.g. 239.255.42.100.")] public string MulticastAddress = ""; [Header("Replay Priority Quick Setup")] [Tooltip("Enable Live Motive + MMRP Replay priority switching from this Streaming Client inspector.")] public bool EnableReplayPriority = false; [Tooltip("MMRP / recording PC IP address. Replay frames from this IP override live frames while fresh.")] public string ReplayServerAddress = "127.0.0.1"; [Tooltip("Replay frames are preferred while newer than this many seconds.")] [Range(0.05f, 5.0f)] public float ReplayFreshnessSeconds = 0.35f; [Tooltip("Controls whether skeleton data is streamed with local or global coordinates.")] public StreamingCoordinatesValues SkeletonCoordinates = StreamingCoordinatesValues.Local; [Tooltip("Controls whether tmarkerset data is streamed with local or global coordinates.")] public StreamingCoordinatesValues TMarkersetCoordinates = StreamingCoordinatesValues.Local; [Tooltip("Controls the Bone Naming Convention in the streamed data.")] public OptitrackBoneNameConvention BoneNamingConvention = OptitrackBoneNameConvention.Motive; [Header("Extra Features")] [Tooltip("Draws marker visuals in the viewport for debugging and other uses. Using this will increase the data rate in Unicast mode.")] public bool DrawMarkers = false; [Tooltip("Draws trained markerset marker visuals in the viewport for debugging and other uses. Using this will increase the data rate in Unicast mode.")] public bool DrawTMarkersetMarkers = false; // trained markerset added [Tooltip("Draws camera visuals in the viewport for debugging and other uses. Motive 3.0+ only.")] public bool DrawCameras = false; [Tooltip("Draws force plate visuals in the viewport for debugging and other uses.")] public bool DrawForcePlates = false; [Tooltip("Motive will record when the Unity project is played.")] public bool RecordOnPlay = false; [Tooltip("Skips getting data descriptions. Skeletons will not work with this feature turned on, but it will reduce network usage with a large number of rigid bodies.")] public bool SkipDataDescriptions = false; [Tooltip("Automatically retries the initial connection and reconnects when streaming frames stop.")] public bool AutoReconnect = true; [Header("Connection Health")] [Tooltip("How often connection health is checked. This only reads local timestamps and does not contact Motive.")] [Min(0.1f)] public float ConnectionHealthCheckIntervalSeconds = 0.25f; [Tooltip("Seconds without live/replay frames before the stream is considered stale.")] [Min(0.5f)] public float StreamingFrameTimeoutSeconds = 1.0f; [Tooltip("Delay before the first full reconnect attempt after a stale stream is detected.")] [Min(0f)] public float FullReconnectInitialDelaySeconds = 0.0f; [Tooltip("Seconds to wait for frames after a full reconnect attempt before retrying.")] [Min(0.25f)] public float FullReconnectFrameWaitSeconds = 1.5f; [Tooltip("Delay before subsequent full reconnect attempts.")] [Min(0.5f)] public float FullReconnectRetryDelaySeconds = 2.0f; [Tooltip("Timeout for the direct NatNet server-info command during connect/reconnect, in milliseconds.")] [Min(100)] public int DirectServerInfoTimeoutMs = 500; [Tooltip("Timeout for the direct NatNet MODELDEF command during connect/reconnect, in milliseconds.")] [Min(100)] public int DirectModelDefTimeoutMs = 1000; [Tooltip("Changes to the version of Natnet used by the server")] public string ServerNatNetVersion = ""; public string ClientNatNetVersion = ""; //[Tooltip("Timecode Provider")] //public bool TimecodeProvider = false; [Header("Mirror Mode")] [Tooltip("Mirrors streamed skeleton bones and retargeted avatar motion left/right.")] public bool MirrorMode = false; #region Private fields //private UInt16 ServerCommandPort = NatNetConstants.DefaultCommandPort; //private UInt16 ServerDataPort = NatNetConstants.DefaultDataPort; private bool m_doneSubscriptionNotice = false; private bool m_receivedFrameSinceConnect = false; 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; private OptitrackHiResTimer.Timestamp m_lastFrameDeliveryTimestamp; private Coroutine m_connectionHealthCoroutine = null; private Coroutine m_replayConnectCoroutine = null; private UdpClient m_directFrameUdp = null; private Thread m_directFrameThread = null; private volatile bool m_directFrameRunning = false; private bool m_directFrameLogged = false; private volatile bool m_directFrameLogPending = false; private volatile bool m_directTimeoutLogPending = false; private volatile bool m_directSocketErrorLogPending = false; private string m_directSocketErrorMessage = ""; private int m_directTimeoutCount = 0; private bool m_directNatNetConnected = false; private bool m_directNoModelDefWarned = false; private IPAddress m_directServerAddress = null; private IPAddress m_directLocalAddress = null; private IPAddress m_directMulticastAddress = null; private UInt16 m_directCommandPort = 0; private UInt16 m_directDataPort = 0; private enum DirectDefinitionSource { None, Live, Replay, } private DirectDefinitionSource m_directDefinitionSource = DirectDefinitionSource.None; private NatNetClient m_client; private NatNetClient m_replayClient; private bool m_replayReceivedFrameSinceConnect = false; private bool m_replayConnectWarned = false; private OptitrackHiResTimer.Timestamp m_lastReplayFrameDeliveryTimestamp; private NatNetClient.DataDescriptions m_dataDescs; private List m_rigidBodyDefinitions = new List(); private List m_skeletonDefinitions = new List(); private List m_tmarkersetDefinitions = new List(); // trained markerset added private List m_tmarkmarkersDefinitions = new List(); // trained markerset added private List m_markersDefinitions = new List(); private List m_cameraDefinitions = new List(); private List m_forcePlateDefinitions = new List(); /// Maps from a streamed rigid body's ID to its most recent available pose data. private Dictionary m_latestRigidBodyStates = new Dictionary(); /// Maps from a streamed skeleton's ID to its most recent available pose data. private Dictionary m_latestSkeletonStates = new Dictionary(); /// Reusable staging buffers used to validate a complete skeleton frame before committing it. private Dictionary m_skeletonFrameScratch = new Dictionary(); /// Cache for MirrorMode skeleton bone ID mappings. private Dictionary> m_mirrorBoneIdMaps = new Dictionary>(); /// Maps from a streamed trained markerset's ID to its most recent available pose data. private Dictionary m_latestTMarkersetStates = new Dictionary(); // trained markerset added /// Maps from a streamed marker's ID to its most recent available position. private Dictionary m_latestMarkerStates = new Dictionary(); /// Maps from a streamed trained markerset marker's ID to its most recent available position. private Dictionary m_latestTMarkMarkerStates = new Dictionary(); // trained markerset added /// Maps from a streamed rigid body's ID to its component. private Dictionary m_rigidBodies = new Dictionary(); /// Maps from a streamed skeleton names to its component. private Dictionary m_skeletons = new Dictionary(); private HashSet m_registeredSkeletonComponents = new HashSet(); /// Maps from a streamed trained markerset names to its component. private Dictionary m_tmarkersets = new Dictionary(); // trained markerset added /// Maps from a streamed marker's ID to its sphere game object. Used for drawing markers. private Dictionary m_latestMarkerSpheres = new Dictionary(); /// Maps from a streamed trained markerset marker's ID to its sphere game object. Used for drawing markers. private Dictionary m_latestTMarkMarkerSpheres = new Dictionary(); // trained markerset added /// /// Lock held during access to fields which are potentially modified by (which /// executes on a separate thread). Note while the lock is held, any frame updates received are simply dropped. /// private object m_frameDataUpdateLock = new object(); /// Cache from streamed asset ID to asset name. private Dictionary m_assetIdToNameCache = new Dictionary(); private HashSet m_latestFrameRigidBodyIds = new HashSet(); private HashSet m_currentFrameRigidBodyIds = new HashSet(); private HashSet m_latestFrameSkeletonIds = new HashSet(); private HashSet m_currentFrameSkeletonIds = new HashSet(); private bool m_hasFrameRigidBodyTopologySnapshot = false; private bool m_hasFrameSkeletonTopologySnapshot = false; private volatile bool m_pendingDefinitionRefresh = false; private volatile bool m_forceDefinitionRefreshSoon = false; private volatile bool m_pendingReplayDefinitionRefresh = false; private volatile bool m_forceReplayDefinitionRefreshSoon = false; private volatile bool m_pendingSkeletonDefinitionNotify = false; private float m_definitionRefreshCooldown = 0f; private bool m_isReconnecting = false; private bool m_isRecording = false; private bool m_hasAppliedServerSettings = false; [Header("Definition Refresh")] [Tooltip("Motive에서 Actor/RigidBody가 추가되거나 삭제될 때 Unity 재시작 없이 DataDescription을 주기적으로 다시 조회합니다.")] public bool AutoRefreshDefinitions = true; [Tooltip("DataDescription 자동 재조회 간격(초). 값이 낮을수록 변경 반영이 빠르지만 Motive 명령 요청이 늘어납니다.")] [Min(1f)] public float DefinitionRefreshInterval = 5f; private int m_definitionRefreshFailureCount = 0; private float m_nextAutoDefinitionRefreshTime = 0f; #endregion Private fields /// /// public void RequestDefinitionRefresh() { m_pendingDefinitionRefresh = true; } private bool CanRefreshDefinitions() { return !SkipDataDescriptions && (m_client != null || (ConnectionType == ClientConnectionType.Multicast && m_directNatNetConnected)); } private bool CanRefreshReplayDefinitions() { return !SkipDataDescriptions && EnableReplayPriority && ConnectionType == ClientConnectionType.Multicast && m_directNatNetConnected && !string.IsNullOrWhiteSpace(ReplayServerAddress); } private void RefreshDefinitionsForActiveTransport() { if (m_client != null) { UpdateDefinitions(); ResubscribeRegisteredAssets(); return; } if (ConnectionType == ClientConnectionType.Multicast && m_directNatNetConnected) { UpdateDirectDefinitions(); ResubscribeRegisteredAssets(); return; } throw new InvalidOperationException("No active OptiTrack transport is available for DataDescription refresh."); } private void RefreshReplayDefinitionsForActiveTransport() { if (ConnectionType == ClientConnectionType.Multicast && m_directNatNetConnected) { UpdateDirectReplayDefinitions(); ResubscribeRegisteredAssets(); return; } throw new InvalidOperationException("No active MMRP replay transport is available for DataDescription refresh."); } private void Update() { if (m_pendingSkeletonDefinitionNotify) { m_pendingSkeletonDefinitionNotify = false; NotifyRegisteredSkeletonDefinitionsChanged(); } if (m_directFrameLogPending) { m_directFrameLogPending = false; Debug.Log(GetType().FullName + ": receiving NatNet frames through direct UDP fallback.", this); } if (m_directTimeoutLogPending) { m_directTimeoutLogPending = false; Debug.LogWarning(GetType().FullName + ": direct NatNet receiver is joined but has not received multicast packets yet. If Python receives frames on this PC, check Unity Editor firewall permission or multicast routing.", this); } if (m_directSocketErrorLogPending) { m_directSocketErrorLogPending = false; Debug.LogWarning(GetType().FullName + ": direct NatNet receiver socket warning: " + m_directSocketErrorMessage, this); } if (EnableReplayPriority && m_replayConnectCoroutine == null && ConnectionType != ClientConnectionType.Multicast) m_replayConnectCoroutine = StartCoroutine(ReplayConnectLoop()); else if (!EnableReplayPriority && m_replayConnectCoroutine != null) { StopCoroutine(m_replayConnectCoroutine); m_replayConnectCoroutine = null; CleanupReplayClient(); } if (DrawMarkers) { if (m_client != null && ConnectionType == ClientConnectionType.Unicast && !m_subscribedToMarkers) { if (m_markerSubscribeRetryCooldown <= 0f) { SubscribeMarkers(); if (!m_subscribedToMarkers) m_markerSubscribeRetryCooldown = k_MarkerSubscribeRetryInterval; } else { m_markerSubscribeRetryCooldown -= Time.deltaTime; } } var markerSnapshot = new Dictionary(); lock (m_frameDataUpdateLock) { foreach (var markerEntry in m_latestMarkerStates) { markerSnapshot[markerEntry.Key] = ( markerEntry.Value.Position, markerEntry.Value.Size, markerEntry.Value.Name, markerEntry.Value.IsActive ); } } var activeIds = new HashSet(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.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; } } 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 { // not drawing markers, remove all marker spheres foreach (KeyValuePair markerSphereEntry in m_latestMarkerSpheres) { Destroy( m_latestMarkerSpheres[markerSphereEntry.Key] ); } m_latestMarkerSpheres.Clear(); } if (DrawCameras && !m_hasDrawnCameras ) { if (m_client.ServerAppVersion >= new Version(3, 0, 0)) { var cameraGroup = new GameObject("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); } } else { Debug.LogWarning("Drawing cameras is only supported in Motive 3.0+."); } m_hasDrawnCameras = true; } if (DrawForcePlates && !m_hasDrawnForcePlates) { var forcePlateGroup = new GameObject("Force Plates"); foreach (OptitrackForcePlateDefinition plate in m_forcePlateDefinitions) { 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 = 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) //{ // Debug.Log(""); //} // Trained Markerset Markers if requested to draw // trained markerset added if (DrawTMarkersetMarkers) { var tmarkSnapshot = new Dictionary(); lock (m_frameDataUpdateLock) { foreach (var markerEntry in m_latestTMarkMarkerStates) { tmarkSnapshot[markerEntry.Key] = ( markerEntry.Value.Position, markerEntry.Value.Size, markerEntry.Value.Name, markerEntry.Value.IsActive ); } } var activeTMarkIds = new HashSet(tmarkSnapshot.Count); foreach (var kvp in tmarkSnapshot) { activeTMarkIds.Add(kvp.Key); if (m_latestTMarkMarkerSpheres.ContainsKey(kvp.Key)) { m_latestTMarkMarkerSpheres[kvp.Key].transform.position = kvp.Value.pos; } else { 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; } } 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 { // not drawing markers, remove all marker spheres foreach (KeyValuePair markerCubeEntry in m_latestTMarkMarkerSpheres) { Destroy(m_latestTMarkMarkerSpheres[markerCubeEntry.Key]); } m_latestTMarkMarkerSpheres.Clear(); } if (m_forceDefinitionRefreshSoon) { m_forceDefinitionRefreshSoon = false; m_pendingDefinitionRefresh = true; m_definitionRefreshCooldown = 0f; } if (m_forceReplayDefinitionRefreshSoon) { m_forceReplayDefinitionRefreshSoon = false; m_pendingReplayDefinitionRefresh = true; m_definitionRefreshCooldown = 0f; } // Refresh streamed asset definitions when Motive actors or rigid bodies are added/removed. if (m_pendingReplayDefinitionRefresh && m_definitionRefreshCooldown <= 0f && CanRefreshReplayDefinitions()) { m_pendingReplayDefinitionRefresh = false; try { RefreshReplayDefinitionsForActiveTransport(); m_definitionRefreshCooldown = Mathf.Max(DefinitionRefreshInterval, 1f); m_nextAutoDefinitionRefreshTime = Time.unscaledTime + Mathf.Max(DefinitionRefreshInterval, 1f); } catch (Exception ex) { m_definitionRefreshFailureCount++; m_definitionRefreshCooldown = Mathf.Min(60f, 2f * Mathf.Pow(2f, Mathf.Min(m_definitionRefreshFailureCount, 5))); m_pendingReplayDefinitionRefresh = true; Debug.LogWarning(GetType().FullName + ": MMRP DataDescription refresh failed. Retrying in " + m_definitionRefreshCooldown + " seconds.", this); Debug.LogException(ex, this); } } if (m_pendingDefinitionRefresh && m_definitionRefreshCooldown <= 0f && CanRefreshDefinitions()) { m_pendingDefinitionRefresh = false; try { RefreshDefinitionsForActiveTransport(); m_definitionRefreshCooldown = Mathf.Max(DefinitionRefreshInterval, 1f); m_nextAutoDefinitionRefreshTime = Time.unscaledTime + Mathf.Max(DefinitionRefreshInterval, 1f); } catch (Exception ex) { m_definitionRefreshFailureCount++; m_definitionRefreshCooldown = Mathf.Min(60f, 2f * Mathf.Pow(2f, Mathf.Min(m_definitionRefreshFailureCount, 5))); m_pendingDefinitionRefresh = true; Debug.LogWarning(GetType().FullName + ": DataDescription refresh failed. Retrying in " + m_definitionRefreshCooldown + " seconds.", this); Debug.LogException(ex, this); } } if (m_definitionRefreshCooldown > 0f) m_definitionRefreshCooldown -= Time.deltaTime; if (AutoRefreshDefinitions && CanRefreshDefinitions() && m_definitionRefreshCooldown <= 0f && Time.unscaledTime >= m_nextAutoDefinitionRefreshTime) { m_nextAutoDefinitionRefreshTime = Time.unscaledTime + Mathf.Max(DefinitionRefreshInterval, 1f); RequestDefinitionRefresh(); } } /// /// Returns the first component located in the scene. /// Provides a convenient, sensible default in the common case where only a single client exists. /// Issues a warning if more than one such component is found. /// /// An arbitrary OptitrackClient from the scene, or null if none are found. public static OptitrackStreamingClient FindDefaultClient() { OptitrackStreamingClient[] allClients = FindObjectsByType(FindObjectsSortMode.None); if ( allClients.Length == 0 ) { Debug.LogError( "Unable to locate any " + typeof( OptitrackStreamingClient ).FullName + " components." ); return null; } else if ( allClients.Length > 1 ) { Debug.LogWarning( "Multiple " + typeof( OptitrackStreamingClient ).FullName + " components found in scene; defaulting to first available." ); } return allClients[0]; } /// /// Sends a message to Motive to start recording /// /// A boolean indicating if message was successful. public bool StartRecording() { if(m_client != null) { bool result = m_client.RequestCommand("StartRecording"); if (result) m_isRecording = true; return result; } return false; } /// /// Sends a message to Motive to stop recording /// /// A boolean indicating if message was successful. public bool StopRecording() { if (m_client != null) { bool result = m_client.RequestCommand("StopRecording"); if (result) m_isRecording = false; return result; } return false; } /// /// Reconnects to the OptiTrack streaming server by disconnecting and reconnecting. /// public void Reconnect() { if (m_isReconnecting) { Debug.LogWarning(GetType().FullName + ": Reconnect is already in progress.", this); return; } Debug.Log("OptiTrack: full reconnect requested."); OnDisable(); StartCoroutine(ReconnectCoroutine()); } /// /// Coroutine for handling the reconnection process with a retry loop. /// Attempts up to 5 times using the configurable full reconnect timing values. /// Last known pose is preserved during reconnect (m_latestSkeletonStates not cleared). /// private System.Collections.IEnumerator ReconnectCoroutine() { m_isReconnecting = true; const int maxAttempts = 5; for (int attempt = 1; attempt <= maxAttempts; attempt++) { m_receivedFrameSinceConnect = false; float delay = (attempt == 1) ? Mathf.Max(FullReconnectInitialDelaySeconds, 0f) : Mathf.Max(FullReconnectRetryDelaySeconds, 0.5f); Debug.Log(string.Format("{0}: reconnect attempt {1}/{2} in {3} seconds...", GetType().FullName, attempt, maxAttempts, delay), this); if (delay > 0f) yield return new WaitForSeconds(delay); yield return StartCoroutine(ConnectCoroutine()); float deadline = Time.realtimeSinceStartup + Mathf.Max(FullReconnectFrameWaitSeconds, 0.25f); while (!m_receivedFrameSinceConnect && Time.realtimeSinceStartup < deadline) yield return null; if (m_receivedFrameSinceConnect) { Debug.Log(GetType().FullName + ": reconnect succeeded.", this); if (RecordOnPlay) StartRecording(); m_isReconnecting = false; yield break; } Debug.LogWarning(string.Format("{0}: reconnect attempt {1}/{2} failed; no streaming frames received.", GetType().FullName, attempt, maxAttempts), this); if (m_connectionHealthCoroutine != null) { StopCoroutine(m_connectionHealthCoroutine); m_connectionHealthCoroutine = null; } if (m_client != null || m_directNatNetConnected) CleanupClient(); } Debug.LogError(string.Format("{0}: all {1} reconnect attempts failed. Reconnect manually from the Inspector.", GetType().FullName, maxAttempts), this); m_isReconnecting = false; } /// /// Gets the current connection status of the client. /// /// True if connected and receiving data, false otherwise. public bool IsConnected() { return ((m_client != null || m_directNatNetConnected) && m_receivedFrameSinceConnect) || IsReplayFrameFresh(); } /// /// Gets the connection status as a readable string. /// /// Connection status description. public string GetConnectionStatus() { if (IsReplayFrameFresh()) { return "Replay active (MMRP)"; } if (m_client == null && !m_directNatNetConnected) { return "Disconnected"; } else if (!m_receivedFrameSinceConnect) { return "Connected, waiting for data"; } else { float lastFrameAge = m_lastFrameDeliveryTimestamp.AgeSeconds; if (lastFrameAge < 5.0f) { return "Connected, receiving data"; } else { return "Connected, data interrupted"; } } } #region Recording Control Functions /// /// Starts recording with enhanced logging. /// public void StartRecordingWithLog() { bool result = StartRecording(); if (result) { Debug.Log("OptiTrack: recording started."); } else { Debug.LogWarning("OptiTrack: failed to start recording."); } } /// /// Stops recording with enhanced logging. /// public void StopRecordingWithLog() { bool result = StopRecording(); if (result) { Debug.Log("OptiTrack: recording stopped."); } else { Debug.LogWarning("OptiTrack: failed to stop recording."); } } /// /// Toggles recording state on this client. /// public void ToggleRecording() { if (m_client != null) { if (m_isRecording) { if (StopRecording()) Debug.Log("OptiTrack: recording stopped."); else Debug.LogWarning("OptiTrack: failed to stop recording."); } else { if (StartRecording()) Debug.Log("OptiTrack: recording started."); else Debug.LogWarning("OptiTrack: failed to start recording."); } } else { Debug.LogError("OptiTrack: client is not connected."); } } public void ToggleDrawMarkers() { DrawMarkers = !DrawMarkers; Debug.Log($"OptiTrack: DrawMarkers = {DrawMarkers}"); } #endregion /// Get the most recently received state for the specified rigid body. /// Corresponds to the "User ID" field in Motive. /// The most recent available state, or null if none available. public OptitrackRigidBodyState GetLatestRigidBodyState( Int32 rigidBodyId, bool networkCompensation = true ) { OptitrackRigidBodyState rbState; if ( ! networkCompensation || m_client == null ) { lock ( m_frameDataUpdateLock ) { m_latestRigidBodyStates.TryGetValue( rigidBodyId, out rbState ); } } else { sRigidBodyData rbData; m_client.GetPredictedRigidBodyPose( rigidBodyId, out rbData, 0.0 ); rbState = new OptitrackRigidBodyState(); RigidBodyDataToState( rbData, OptitrackHiResTimer.Now(), rbState ); } if ( MirrorMode && rbState != null ) { rbState = new OptitrackRigidBodyState { DeliveryTimestamp = rbState.DeliveryTimestamp, IsTracked = rbState.IsTracked, Pose = new OptitrackPose { Position = MirrorPosition( rbState.Pose.Position ), Orientation = MirrorOrientation( rbState.Pose.Orientation ), } }; } return rbState; } /// Get the most recently received state for the specified skeleton. /// /// Taken from the corresponding field. /// To find the appropriate skeleton definition, use . /// /// The most recent available state, or null if none available. public OptitrackSkeletonState GetLatestSkeletonState( Int32 skeletonId ) { OptitrackSkeletonState skelState; lock ( m_frameDataUpdateLock ) { m_latestSkeletonStates.TryGetValue( skeletonId, out skelState ); } return skelState; } /// /// /// True when the requested data exists; otherwise false. public bool FillBoneSnapshot( Int32 skeletonId, Dictionary posOut, Dictionary oriOut, out OptitrackHiResTimer.Timestamp deliveryTimestamp ) { deliveryTimestamp = default( OptitrackHiResTimer.Timestamp ); lock ( m_frameDataUpdateLock ) { OptitrackSkeletonState state; if ( !m_latestSkeletonStates.TryGetValue( skeletonId, out state ) || state == null ) return false; deliveryTimestamp = state.DeliveryTimestamp; posOut.Clear(); oriOut.Clear(); if ( MirrorMode ) { Dictionary mirrorMap = GetOrBuildMirrorBoneIdMap( skeletonId ); foreach ( var kvp in state.BonePoses ) { Int32 targetId = mirrorMap != null && mirrorMap.TryGetValue( kvp.Key, out Int32 mid ) ? mid : kvp.Key; if ( targetId == kvp.Key ) { posOut[kvp.Key] = MirrorPosition( kvp.Value.Position ); oriOut[kvp.Key] = MirrorOrientation( kvp.Value.Orientation ); } else { posOut[kvp.Key] = kvp.Value.Position; oriOut[targetId] = MirrorOrientation( kvp.Value.Orientation ); } } } else { foreach ( var kvp in state.BonePoses ) { posOut[kvp.Key] = kvp.Value.Position; oriOut[kvp.Key] = kvp.Value.Orientation; } } return true; } } /// /// Returns the left/right counterpart for a Motive bone name, or null for center bones. /// For example, LUArm maps to RUArm, RShin maps to LShin, and Hip maps to null. /// private static string GetMirrorBoneName( string name ) { if ( string.IsNullOrEmpty( name ) || name.Length < 2 ) return null; if ( name[0] == 'L' && char.IsUpper( name[1] ) ) return "R" + name.Substring( 1 ); if ( name[0] == 'R' && char.IsUpper( name[1] ) ) return "L" + name.Substring( 1 ); return null; } /// /// private Dictionary GetOrBuildMirrorBoneIdMap( Int32 skeletonId ) { if ( m_mirrorBoneIdMaps.TryGetValue( skeletonId, out var cached ) ) return cached; OptitrackSkeletonDefinition skelDef = GetSkeletonDefinitionById( skeletonId ); if ( skelDef == null ) return null; var nameToId = new Dictionary( skelDef.Bones.Count ); foreach ( var bone in skelDef.Bones ) nameToId[bone.Name] = bone.Id; var map = new Dictionary( skelDef.Bones.Count ); foreach ( var bone in skelDef.Bones ) { string fullName = bone.Name; int sep = fullName.IndexOf( '_' ); string prefix = sep >= 0 ? fullName.Substring( 0, sep + 1 ) : ""; // "Skeleton1_" string shortName = sep >= 0 ? fullName.Substring( sep + 1 ) : fullName; // "LUArm" string mirrorShort = GetMirrorBoneName( shortName ); string mirrorFull = mirrorShort != null ? prefix + mirrorShort : null; if ( mirrorFull != null && nameToId.TryGetValue( mirrorFull, out Int32 mirrorId ) ) map[bone.Id] = mirrorId; else map[bone.Id] = bone.Id; } m_mirrorBoneIdMaps[skeletonId] = map; return map; } /// Mirror helper for YZ-plane coordinate conversion. private static Quaternion MirrorOrientation( Quaternion q ) => new Quaternion( q.x, -q.y, -q.z, q.w ); /// Mirror helper for YZ-plane coordinate conversion. private static Vector3 MirrorPosition( Vector3 pos ) => new Vector3( -pos.x, pos.y, pos.z ); /// Get the most recently received state for the specified trained markerset. /// /// Taken from the corresponding field. /// To find the appropriate skeleton definition, use . /// /// The most recent available state, or null if none available. public OptitrackTMarkersetState GetLatestTMarkersetState(Int32 tmarkersetId) { lock (m_frameDataUpdateLock) { if (!m_latestTMarkersetStates.TryGetValue(tmarkersetId, out var source) || source == null) return null; var snapshot = new OptitrackTMarkersetState { BonePoses = CopyPoseDictionary(source.BonePoses, MirrorMode), LocalBonePoses = CopyPoseDictionary(source.LocalBonePoses, MirrorMode), }; return snapshot; } } private static Dictionary CopyPoseDictionary( Dictionary source, bool mirror) { var snapshot = new Dictionary(source != null ? source.Count : 0); if (source == null) return snapshot; foreach (var kvp in source) { snapshot[kvp.Key] = new OptitrackPose { Position = mirror ? MirrorPosition(kvp.Value.Position) : kvp.Value.Position, Orientation = mirror ? MirrorOrientation(kvp.Value.Orientation) : kvp.Value.Orientation, }; } return snapshot; } /// /// Copies trained markerset poses while holding the NatNet frame lock. /// public bool FillTMarkersetSnapshot(Int32 tmarkersetId, bool useLocalBonePoses, Dictionary posOut, Dictionary oriOut) { lock (m_frameDataUpdateLock) { if (!m_latestTMarkersetStates.TryGetValue(tmarkersetId, out var state) || state == null) return false; var source = useLocalBonePoses ? state.LocalBonePoses : state.BonePoses; if (source == null) return false; posOut.Clear(); oriOut.Clear(); foreach (var kvp in source) { posOut[kvp.Key] = MirrorMode ? MirrorPosition(kvp.Value.Position) : kvp.Value.Position; oriOut[kvp.Key] = MirrorMode ? MirrorOrientation(kvp.Value.Orientation) : kvp.Value.Orientation; } return true; } } /// Get the most recently received state for streamed markers. /// The most recent available marker states, or null if none available. public List GetLatestMarkerStates() { List markerStates = new List(); lock (m_frameDataUpdateLock) { foreach (KeyValuePair markerEntry in m_latestMarkerStates) { OptitrackMarkerState newMarkerState = new OptitrackMarkerState { Position = MirrorMode ? MirrorPosition( markerEntry.Value.Position ) : markerEntry.Value.Position, Labeled = markerEntry.Value.Labeled, Size = markerEntry.Value.Size, Id = markerEntry.Value.Id }; markerStates.Add( newMarkerState ); } } return markerStates; } /// Retrieves the definition of the rigid body with the specified streaming ID. /// /// The specified rigid body definition, or null if not found. public OptitrackRigidBodyDefinition GetRigidBodyDefinitionById( Int32 rigidBodyId ) { return m_rigidBodyDefinitions.Find( def => def.Id == rigidBodyId ); } /// /// Gets the rigid body ID by its name. /// /// The name of the rigid body to find. /// The ID of the rigid body if found, -1 otherwise. public int GetRigidBodyIdByName(string rigidBodyName) { var definition = m_rigidBodyDefinitions.Find(def => def.Name == rigidBodyName); return definition != null ? definition.Id : -1; } /// Retrieves the definition of the skeleton with the specified asset name. /// The name of the skeleton for which to retrieve the definition. /// The specified skeleton definition, or null if not found. public OptitrackSkeletonDefinition GetSkeletonDefinitionByName( string skeletonAssetName ) { for ( int i = 0; i < m_skeletonDefinitions.Count; ++i ) { OptitrackSkeletonDefinition skelDef = m_skeletonDefinitions[i]; if (skelDef.IsSynthetic) continue; if ( skelDef.Name.Equals( skeletonAssetName, StringComparison.InvariantCultureIgnoreCase ) ) { return skelDef; } } return null; } /// Retrieves the definition of the skeleton with the specified skeleton id. /// The id of the skeleton for which to retrieve the definition. /// The specified skeleton definition, or null if not found. public OptitrackSkeletonDefinition GetSkeletonDefinitionById( Int32 skeletonId ) { for (int i = 0; i < m_skeletonDefinitions.Count; ++i) { OptitrackSkeletonDefinition skelDef = m_skeletonDefinitions[i]; if (skelDef.Id == skeletonId) { return skelDef; } } return null; } /// Retrieves the definition of the tmarkerset with the specified asset name. /// The name of the tmarkerset for which to retrieve the definition. /// The specified tmarkerset definition, or null if not found. // trained markerset added public OptitrackTMarkersetDefinition GetTMarkersetDefinitionByName(string TMarkersetName) { for (int i = 0; i < m_tmarkersetDefinitions.Count; i++) { OptitrackTMarkersetDefinition tmarDef = m_tmarkersetDefinitions[i]; if (tmarDef.Name.Equals(TMarkersetName, StringComparison.InvariantCultureIgnoreCase)) { return tmarDef; } } return null; } /// Retrieves the definition of the tmarkerset with the specified tmarkerset id. /// The id of the tmarkerset for which to retrieve the definition. /// The specified tmarkerset definition, or null if not found. // trained markerset added public OptitrackTMarkersetDefinition GetTMarkersetDefinitionById(Int32 tmarkersetId) { for (int i = 0; i < m_tmarkersetDefinitions.Count; ++i) { OptitrackTMarkersetDefinition tmarDef = m_tmarkersetDefinitions[i]; if (tmarDef.Id == tmarkersetId) { return tmarDef; } } return null; } /// Get the most recently received state for streamed trained markerset markers. /// The most recent available trained markerset marker states, or null if none available. public List GetLatestTMarkMarkerStates() // trained markerset added { List tmarkmarkerStates = new List(); lock (m_frameDataUpdateLock) { foreach (KeyValuePair markerEntry in m_latestTMarkMarkerStates) { OptitrackMarkerState newMarkerState = new OptitrackMarkerState { Position = MirrorMode ? MirrorPosition( markerEntry.Value.Position ) : markerEntry.Value.Position, Labeled = markerEntry.Value.Labeled, Size = markerEntry.Value.Size, Id = markerEntry.Value.Id }; tmarkmarkerStates.Add(newMarkerState); } } return tmarkmarkerStates; } /// Request data descriptions from the host, then update our definitions. /// /// Thrown by if the request to the server fails. /// public void UpdateDefinitions() { // This may throw an exception if the server request times out or otherwise fails. UInt32 descriptionTypeMask = 0; descriptionTypeMask |= (1 << (int)NatNetDataDescriptionType.NatNetDataDescriptionType_RigidBody); descriptionTypeMask |= (1 << (int)NatNetDataDescriptionType.NatNetDataDescriptionType_Skeleton); descriptionTypeMask |= (1 << (int)NatNetDataDescriptionType.NatNetDataDescriptionType_Asset); // trained markerset added if (DrawMarkers) { descriptionTypeMask |= (1 << (int)NatNetDataDescriptionType.NatNetDataDescriptionType_MarkerSet); } if (DrawCameras) { descriptionTypeMask |= (1 << (int)NatNetDataDescriptionType.NatNetDataDescriptionType_Camera); } if( DrawForcePlates) { descriptionTypeMask |= (1 << (int)NatNetDataDescriptionType.NatNetDataDescriptionType_ForcePlate); } m_dataDescs = m_client.GetDataDescriptions(descriptionTypeMask); m_definitionRefreshFailureCount = 0; m_rigidBodyDefinitions.Clear(); m_skeletonDefinitions.Clear(); m_tmarkersetDefinitions.Clear(); m_mirrorBoneIdMaps.Clear(); lock (m_frameDataUpdateLock) { m_assetIdToNameCache.Clear(); } // ---------------------------------- // - Translate Rigid Body Definitions // ---------------------------------- for ( int nativeRbDescIdx = 0; nativeRbDescIdx < m_dataDescs.RigidBodyDescriptions.Count; ++nativeRbDescIdx ) { sRigidBodyDescription nativeRb = m_dataDescs.RigidBodyDescriptions[nativeRbDescIdx]; OptitrackRigidBodyDefinition rbDef = new OptitrackRigidBodyDefinition { Id = nativeRb.Id, Name = nativeRb.Name, Markers = new List( nativeRb.MarkerCount ), }; // Populate nested marker definitions. for ( int nativeMarkerIdx = 0; nativeMarkerIdx < nativeRb.MarkerCount; ++nativeMarkerIdx ) { int positionOffset = nativeMarkerIdx * Marshal.SizeOf( typeof( MarkerDataVector ) ); IntPtr positionPtr = new IntPtr( nativeRb.MarkerPositions.ToInt64() + positionOffset ); int labelOffset = nativeMarkerIdx * Marshal.SizeOf( typeof( Int32 ) ); IntPtr labelPtr = new IntPtr( nativeRb.MarkerRequiredLabels.ToInt64() + labelOffset ); MarkerDataVector nativePos = (MarkerDataVector)Marshal.PtrToStructure( positionPtr, typeof( MarkerDataVector ) ); Int32 nativeLabel = Marshal.ReadInt32( labelPtr ); OptitrackRigidBodyDefinition.MarkerDefinition markerDef = new OptitrackRigidBodyDefinition.MarkerDefinition { Position = new Vector3( -nativePos.Values[0], nativePos.Values[1], nativePos.Values[2] ), RequiredLabel = nativeLabel, }; rbDef.Markers.Add( markerDef ); } m_rigidBodyDefinitions.Add( rbDef ); } // ---------------------------------- // - Translate Skeleton Definitions // ---------------------------------- for ( int nativeSkelDescIdx = 0; nativeSkelDescIdx < m_dataDescs.SkeletonDescriptions.Count; ++nativeSkelDescIdx ) { sSkeletonDescription nativeSkel = m_dataDescs.SkeletonDescriptions[nativeSkelDescIdx]; OptitrackSkeletonDefinition skelDef = new OptitrackSkeletonDefinition { Id = nativeSkel.Id, Name = nativeSkel.Name, Bones = new List(nativeSkel.RigidBodyCount), BoneIdToParentIdMap = new Dictionary(), }; // Populate nested bone definitions. for ( int nativeBoneIdx = 0; nativeBoneIdx < nativeSkel.RigidBodyCount; ++nativeBoneIdx ) { sRigidBodyDescription nativeBone = nativeSkel.RigidBodies[nativeBoneIdx]; OptitrackSkeletonDefinition.BoneDefinition boneDef = new OptitrackSkeletonDefinition.BoneDefinition { Id = nativeBone.Id, ParentId = nativeBone.ParentId, Name = nativeBone.Name, Offset = new Vector3( -nativeBone.OffsetX, nativeBone.OffsetY, nativeBone.OffsetZ ), }; skelDef.Bones.Add( boneDef ); skelDef.BoneIdToParentIdMap[boneDef.Id] = boneDef.ParentId; } m_skeletonDefinitions.Add( skelDef ); } // -------------------------------------------------------------------- // - Translate Trained Markerset Definitions // trained markerset added // -------------------------------------------------------------------- for (int nativeTmarkDescIdx = 0; nativeTmarkDescIdx < m_dataDescs.AssetDescriptions.Count; ++nativeTmarkDescIdx) { sAssetDescription nativeTmark = m_dataDescs.AssetDescriptions[nativeTmarkDescIdx]; // Debug.Log("#rbs: " + nativeTmark.RigidBodyCount); // correct OptitrackTMarkersetDefinition tmarkDef = new OptitrackTMarkersetDefinition { Id = nativeTmark.AssetID, Name = nativeTmark.Name, Bones = new List(nativeTmark.RigidBodyCount), BoneIdToParentIdMap = new Dictionary(), Markers = new List(nativeTmark.MarkerCount), }; // Populate nested bone definitions. for (int nativeBoneIdx = 0; nativeBoneIdx < nativeTmark.RigidBodyCount; ++nativeBoneIdx) { sRigidBodyDescription nativeBone = nativeTmark.RigidBodies[nativeBoneIdx]; OptitrackTMarkersetDefinition.BoneDefinition boneDef = new OptitrackTMarkersetDefinition.BoneDefinition { Id = nativeBone.Id, ParentId = nativeBone.ParentId, Name = nativeBone.Name, Offset = new Vector3(-nativeBone.OffsetX, nativeBone.OffsetY, nativeBone.OffsetZ), }; tmarkDef.Bones.Add(boneDef); tmarkDef.BoneIdToParentIdMap[boneDef.Id] = boneDef.ParentId; } // Populate nested marker definitions for (int nativeMarkerIdx = 0; nativeMarkerIdx < nativeTmark.MarkerCount; ++nativeMarkerIdx) { sMarkerDescription nativeMarker = nativeTmark.Markers[nativeMarkerIdx]; //Debug.Log("TMarkerset (X, Y, Z): " + nativeMarker.X + " " + nativeMarker.Y + " " + nativeMarker.Z); //Debug.Log(nativeMarker.Id + " " + nativeMarker.Name); OptitrackTMarkersetDefinition.MarkerDefinition markerDef = new OptitrackTMarkersetDefinition.MarkerDefinition { Name = nativeMarker.Name, Id = nativeMarker.Id, Position = new Vector3(-nativeMarker.X, nativeMarker.Y, nativeMarker.Z), }; tmarkDef.Markers.Add(markerDef); } //foreach (KeyValuePair kvp in tmarkDef.BoneIdToParentIdMap) //{ // Debug.Log("Key: " + kvp.Key + "Value: " + kvp.Value); //} // correct m_tmarkersetDefinitions.Add(tmarkDef); } // ---------------------------------- // - Get Marker Definitions (ToDo) // ---------------------------------- //for (int markersetNumber = 0; markersetNumber < m_dataDescs.MarkerSetDescriptions.Count; ++markersetNumber) //{ // sMarkerSetDescription markerDescription = m_dataDescs.MarkerSetDescriptions[markersetNumber]; // if(markerDescription.Name == "all") // { // Int32 nMarkers = markerDescription.MarkerCount; // for( int i = 0; i < nMarkers; ++i) // { // int nameOffset = i * NatNetConstants.MaxNameLength; //Marshal.SizeOf(typeof(Char**)); // IntPtr namePtr = new IntPtr(markerDescription.MarkerNames.ToInt64() + nameOffset); // // FIXME: Need to de-construct the char array of names to use for marker naming later. // // This throws an exception, thus doesn't work. // // MarkerNames is a char** of size [MarkerCount][MaxNameLength] // //string nativeLabel = Marshal.PtrToStringAnsi(namePtr); // } // OptitrackMarkersDefinition markersDef = new OptitrackMarkersDefinition // { // Name = markerDescription.Name // }; // m_markersDefinitions.Add(markersDef); // } //} // ---------------------------------- // - Camera Definitions // ---------------------------------- for (int cameraIndex = 0; cameraIndex < m_dataDescs.CameraDescriptions.Count; ++cameraIndex) { sCameraDescription camera = m_dataDescs.CameraDescriptions[cameraIndex]; OptitrackCameraDefinition cameraDef = new OptitrackCameraDefinition { Name = camera.Name, //Unity and motive use opposite positive-x directions, so positionX needs to be set to negative Position = new Vector3(-camera.PositionX, camera.PositionY, camera.PositionZ), Orientation = new Quaternion(camera.RotationX, camera.RotationY, camera.RotationZ, camera.RotationW), }; m_cameraDefinitions.Add(cameraDef); } // ---------------------------------- // - Force Plate Definitions // ---------------------------------- for (int plateIndex = 0; plateIndex < m_dataDescs.ForcePlateDescriptions.Count; ++plateIndex) { sForcePlateDescription plate = m_dataDescs.ForcePlateDescriptions[plateIndex]; OptitrackForcePlateDefinition forcePlateDef = new OptitrackForcePlateDefinition { Id = plate.Id, SerialNumber = plate.SerialNo, Width = plate.Width, // in inches Length = plate.Length, // in inches ElectricalOffset = new Vector3(plate.OriginX, plate.OriginY, plate.OriginZ), CalibrationMatrix = new List(12 * 12), Corners = new List(4 * 3), PlateType = plate.PlateType, ChannelDataType = plate.ChannelDataType, ChannelCount = plate.ChannelCount, ChannelNames = new List(plate.ChannelCount), }; // Populate corner locations for( int i = 0; i < 12; ++i ) { forcePlateDef.Corners.Add( plate.Corners[i] ); } //Populate Channel Names for (int i = 0; i < forcePlateDef.ChannelCount; ++i) { forcePlateDef.ChannelNames.Add(plate.ChannelNames[i]); } m_forcePlateDefinitions.Add(forcePlateDef); } PruneStatesMissingFromDefinitions(); NotifyRegisteredSkeletonDefinitionsChanged(); } private void PruneStatesMissingFromDefinitions() { var validRigidBodyIds = new HashSet(); foreach (var def in m_rigidBodyDefinitions) validRigidBodyIds.Add(def.Id); var validSkeletonIds = new HashSet(); foreach (var def in m_skeletonDefinitions) validSkeletonIds.Add(def.Id); var validTMarkersetIds = new HashSet(); foreach (var def in m_tmarkersetDefinitions) validTMarkersetIds.Add(def.Id); lock (m_frameDataUpdateLock) { RemoveMissingKeys(m_latestRigidBodyStates, validRigidBodyIds); RemoveMissingKeys(m_latestSkeletonStates, validSkeletonIds); RemoveMissingKeys(m_skeletonFrameScratch, validSkeletonIds); RemoveMissingKeys(m_latestTMarkersetStates, validTMarkersetIds); } } private static void RemoveMissingKeys(Dictionary dictionary, HashSet validKeys) { if (dictionary.Count == 0) return; var staleKeys = new List(); foreach (var kvp in dictionary) { if (!validKeys.Contains(kvp.Key)) staleKeys.Add(kvp.Key); } foreach (var key in staleKeys) dictionary.Remove(key); } private void ResubscribeRegisteredAssets() { if (m_client == null || ConnectionType != ClientConnectionType.Unicast) return; m_doneSubscriptionNotice = false; ResetStreamingSubscriptions(); foreach (KeyValuePair rb in m_rigidBodies) SubscribeRigidBody(rb.Value, rb.Key); foreach (KeyValuePair skel in m_skeletons) SubscribeSkeleton(skel.Value, skel.Key); foreach (KeyValuePair tmark in m_tmarkersets) SubscribeTMarkerset(tmark.Value, tmark.Key); if (DrawTMarkersetMarkers) SubscribeTMarkMarkers(); if (DrawMarkers) SubscribeMarkers(); } private void NotifyRegisteredSkeletonDefinitionsChanged() { if (m_registeredSkeletonComponents.Count == 0) return; var registeredSkeletons = new List(m_registeredSkeletonComponents); foreach (MonoBehaviour component in registeredSkeletons) { if (component == null) { m_registeredSkeletonComponents.Remove(component); continue; } if (component is OptitrackSkeletonAnimator_Mingle animator && animator.isActiveAndEnabled) animator.RefreshSkeletonDefinitionBinding(); } } public void RegisterRigidBody( MonoBehaviour component, Int32 rigidBodyId ) { if ( m_rigidBodies.TryGetValue( rigidBodyId, out MonoBehaviour existingComponent ) ) { if (existingComponent == component) { SubscribeRigidBody(component, rigidBodyId); return; } #if false MonoBehaviour existingRb = m_rigidBodies[rigidBodyId]; Debug.LogError( GetType().FullName + ": " + rb.GetType().FullName + " has duplicate rigid body ID " + rigidBodyId, component ); Debug.LogError( GetType().FullName + ": (Existing " + existingRb.GetType().FullName + " was already registered with that ID)", existingRb ); rb.enabled = false; #endif } m_rigidBodies[rigidBodyId] = component; SubscribeRigidBody(component, rigidBodyId); } public void UnregisterRigidBody( MonoBehaviour component, Int32 rigidBodyId ) { if (m_rigidBodies.TryGetValue(rigidBodyId, out MonoBehaviour existingComponent) && existingComponent == component) { m_rigidBodies.Remove(rigidBodyId); } } public void RegisterSkeleton(MonoBehaviour component, string name) { m_registeredSkeletonComponents.Add(component); if (m_skeletons.TryGetValue(name, out MonoBehaviour existingComponent)) { if (existingComponent == component) { SubscribeSkeleton(component, name); return; } #if false MonoBehaviour existingSkel = m_skeletons[rigidBodyId]; Debug.LogError( "Duplicate skeleton detected, " + GetType().FullName + ": (Existing " + existingRb.GetType().FullName + " was already registered with that ID)", existingRb ); #endif } m_skeletons[name] = component; SubscribeSkeleton(component, name); } public void RegisterTMarkerset(MonoBehaviour component, string name) // trained markerset added { if (m_tmarkersets.TryGetValue(name, out MonoBehaviour existingComponent)) { if (existingComponent == component) { SubscribeTMarkerset(component, name); return; } } m_tmarkersets[name] = component; SubscribeTMarkerset(component, name); } /// /// (Re)initializes and connects to the configured streaming server. /// void OnEnable() { m_receivedFrameSinceConnect = false; if (AutoReconnect) StartCoroutine( InitialConnectWithRetry() ); else StartCoroutine( ConnectCoroutine() ); if (EnableReplayPriority && m_replayConnectCoroutine == null && ConnectionType != ClientConnectionType.Multicast) m_replayConnectCoroutine = StartCoroutine( ReplayConnectLoop() ); } /// /// private System.Collections.IEnumerator InitialConnectWithRetry() { const int maxAttempts = 10; float[] retryDelays = { 1f, 2f, 3f, 5f, 10f }; for (int attempt = 1; attempt <= maxAttempts; attempt++) { yield return StartCoroutine(ConnectCoroutine()); bool transportConnected = m_client != null || m_directNatNetConnected; if (transportConnected) { if (m_directNatNetConnected && m_client == null) { if (attempt > 1) Debug.Log(string.Format("{0}: initial direct NatNet receiver started (attempt {1}/{2}).", GetType().FullName, attempt, maxAttempts), this); yield break; } float deadline = Time.realtimeSinceStartup + 5.0f; while (!m_receivedFrameSinceConnect && Time.realtimeSinceStartup < deadline) yield return null; if (m_receivedFrameSinceConnect) { if (attempt > 1) Debug.Log(string.Format("{0}: initial connection succeeded (attempt {1}/{2}).", GetType().FullName, attempt, maxAttempts), this); yield break; } Debug.LogWarning(string.Format("{0}: initial connection attempt {1}/{2} connected to {3} but received no frames for 5 seconds. ServerNatNet={4}, ClientNatNet={5}, Local={6}, Connection={7}. Check Motive streaming mode, multicast interface, or NatNet SDK compatibility.", GetType().FullName, attempt, maxAttempts, ServerAddress, ServerNatNetVersion, ClientNatNetVersion, ResolvedLocalAddress, ConnectionType), this); CleanupClient(); } if (attempt == maxAttempts) { Debug.LogError(string.Format("{0}: all {1} initial connection attempts failed. Reconnect manually from the Inspector.", GetType().FullName, maxAttempts), this); yield break; } float delay = retryDelays[Mathf.Min(attempt - 1, retryDelays.Length - 1)]; Debug.Log(string.Format("{0}: Initial connection attempt {1}/{2} failed; retrying in {3} seconds.", GetType().FullName, attempt, maxAttempts, delay), this); yield return new WaitForSeconds(delay); } } /// /// private void CleanupClient() { StopDirectFrameReceiver(); m_directNatNetConnected = false; if (m_connectionHealthCoroutine != null) { StopCoroutine(m_connectionHealthCoroutine); m_connectionHealthCoroutine = null; } if (m_client != null) { try { m_client.NativeFrameReceived -= OnNatNetFrameReceived; } catch (Exception) { } try { m_client.Disconnect(); } catch (Exception) { } try { m_client.Dispose(); } catch (Exception) { } m_client = null; } } private System.Collections.IEnumerator ReplayConnectLoop() { var retryDelay = new WaitForSeconds(2.0f); while (EnableReplayPriority) { if (m_replayClient == null && !string.IsNullOrWhiteSpace(ReplayServerAddress)) yield return StartCoroutine(ConnectReplayCoroutine()); else if (m_replayClient != null && m_replayReceivedFrameSinceConnect && m_lastReplayFrameDeliveryTimestamp.AgeSeconds > 5.0f) { CleanupReplayClient(); } yield return retryDelay; } CleanupReplayClient(); m_replayConnectCoroutine = null; } private System.Collections.IEnumerator ConnectReplayCoroutine() { IPAddress serverAddr; IPAddress localAddr; try { serverAddr = IPAddress.Parse(ReplayServerAddress.Trim()); localAddr = ResolveLocalAddress(serverAddr); } catch (Exception ex) { Debug.LogException(ex, this); yield break; } NatNetClient connectedClient = null; Exception connectError = null; System.Threading.Tasks.Task connectTask = System.Threading.Tasks.Task.Run(() => { NatNetClient c = null; try { c = new NatNetClient(); c.Connect( NatNetConnectionType.NatNetConnectionType_Multicast, localAddr, serverAddr, k_DefaultNatNetCommandPort, k_DefaultNatNetDataPort, IPAddress.Parse("239.255.42.100")); connectedClient = c; } catch (Exception ex) { connectError = ex; if (c != null) { try { c.Dispose(); } catch { } } } }); while (!connectTask.IsCompleted) yield return null; if (connectError != null || connectedClient == null) { if (!m_replayConnectWarned) { Debug.LogWarning(GetType().FullName + ": Replay NatNet is not available yet. Server=" + ReplayServerAddress + ", local=" + localAddr + ", command=1510, data=1511, multicast=239.255.42.100. This is normal while MMRP is not replaying.", this); m_replayConnectWarned = true; } yield break; } CleanupReplayClient(); m_replayClient = connectedClient; m_replayReceivedFrameSinceConnect = false; m_replayConnectWarned = false; m_replayClient.NativeFrameReceived += OnNatNetFrameReceived; Debug.Log(GetType().FullName + ": Replay NatNet connected to " + ReplayServerAddress + " (239.255.42.100).", this); } private void CleanupReplayClient() { if (m_replayClient != null) { try { m_replayClient.NativeFrameReceived -= OnNatNetFrameReceived; } catch { } try { m_replayClient.Disconnect(); } catch { } try { m_replayClient.Dispose(); } catch { } m_replayClient = null; } m_replayReceivedFrameSinceConnect = false; } private bool IsReplayFrameFresh() { return EnableReplayPriority && m_replayReceivedFrameSinceConnect && m_lastReplayFrameDeliveryTimestamp.AgeSeconds <= ReplayFreshnessSeconds; } private bool HasRecentStreamingFrame(float thresholdSeconds) { OptitrackHiResTimer.Timestamp liveTimestamp; liveTimestamp.m_ticks = Interlocked.Read(ref m_lastFrameDeliveryTimestamp.m_ticks); bool liveFresh = m_receivedFrameSinceConnect && liveTimestamp.AgeSeconds < thresholdSeconds; bool replayFresh = IsReplayFrameFresh(); return liveFresh || replayFresh; } private bool StartDirectFrameReceiver(string localAddress, string multicastAddress, UInt16 dataPort) { StopDirectFrameReceiver(); try { IPAddress group = IPAddress.Parse(multicastAddress); IPAddress local = IPAddress.Parse(localAddress); m_directFrameUdp = new UdpClient(AddressFamily.InterNetwork); m_directFrameUdp.ExclusiveAddressUse = false; m_directFrameUdp.Client.ReceiveBufferSize = 1024 * 1024; m_directFrameUdp.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); m_directFrameUdp.Client.Bind(new IPEndPoint(IPAddress.Any, dataPort)); m_directFrameUdp.Client.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(group, local)); if (EnableReplayPriority) { try { IPAddress replayGroup = IPAddress.Parse("239.255.42.100"); IPAddress replayLocal = local; if (!string.IsNullOrWhiteSpace(ReplayServerAddress)) { IPAddress replayServer = IPAddress.Parse(ReplayServerAddress.Trim()); replayLocal = ResolveLocalAddress(replayServer); } if (!replayGroup.Equals(group) || !replayLocal.Equals(local)) m_directFrameUdp.Client.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(replayGroup, replayLocal)); } catch (Exception ex) { Debug.LogWarning(GetType().FullName + ": direct NatNet replay multicast join failed: " + ex.Message, this); } } m_directFrameUdp.Client.ReceiveTimeout = 1000; m_directFrameRunning = true; m_directNatNetConnected = true; m_directFrameLogged = false; m_directTimeoutCount = 0; m_directTimeoutLogPending = false; m_directSocketErrorLogPending = false; m_directFrameThread = new Thread(DirectFrameThreadLoop) { IsBackground = true, Name = "OptitrackStreamingClient.DirectNatNet" }; m_directFrameThread.Start(); Debug.Log(GetType().FullName + ": direct NatNet frame receiver joined " + multicastAddress + ":" + dataPort + " on " + localAddress + (EnableReplayPriority ? " with replay priority group 239.255.42.100." : "."), this); return true; } catch (Exception ex) { StopDirectFrameReceiver(); Debug.LogWarning(GetType().FullName + ": direct NatNet frame receiver failed to start: " + ex.Message, this); return false; } } private void StopDirectFrameReceiver() { m_directFrameRunning = false; m_directFrameLogPending = false; m_directTimeoutLogPending = false; m_directSocketErrorLogPending = false; m_directNatNetConnected = false; try { m_directFrameUdp?.Close(); } catch { } m_directFrameUdp = null; if (m_directFrameThread != null && m_directFrameThread.IsAlive) m_directFrameThread.Join(500); m_directFrameThread = null; } private void DirectFrameThreadLoop() { IPEndPoint ep = new IPEndPoint(IPAddress.Any, 0); while (m_directFrameRunning) { try { byte[] data = m_directFrameUdp.Receive(ref ep); m_directTimeoutCount = 0; ParseDirectFramePacket(data, ep.Address); } catch (SocketException se) { if (!m_directFrameRunning) break; if (se.SocketErrorCode == SocketError.TimedOut) { int timeoutCount = Interlocked.Increment(ref m_directTimeoutCount); if (timeoutCount == 5 || timeoutCount == 15) m_directTimeoutLogPending = true; continue; } m_directSocketErrorMessage = se.SocketErrorCode + " / " + se.Message; m_directSocketErrorLogPending = true; } catch (ObjectDisposedException) { break; } catch (Exception ex) { m_directSocketErrorMessage = ex.GetType().Name + " / " + ex.Message; m_directSocketErrorLogPending = true; } } } private void ParseDirectFramePacket(byte[] data, IPAddress sourceAddress) { if (data == null || data.Length < 12) return; if (BitConverter.ToUInt16(data, 0) != 7) return; // NAT_FRAMEOFDATA int o = 4; int frameNumber = BitConverter.ToInt32(data, o); o += 4; OptitrackHiResTimer.Timestamp frameTimestamp = OptitrackHiResTimer.Now(); if (!m_directFrameLogged) { m_directFrameLogged = true; m_directFrameLogPending = true; } bool isReplayFrame = EnableReplayPriority && !string.IsNullOrWhiteSpace(ReplayServerAddress) && sourceAddress != null && sourceAddress.ToString() == ReplayServerAddress.Trim(); if (!isReplayFrame && IsReplayFrameFresh()) return; QueueDefinitionRefreshForFrameSource(isReplayFrame); if (isReplayFrame) { m_replayReceivedFrameSinceConnect = true; Interlocked.Exchange(ref m_lastReplayFrameDeliveryTimestamp.m_ticks, frameTimestamp.m_ticks); m_receivedFrameSinceConnect = true; Interlocked.Exchange(ref m_lastFrameDeliveryTimestamp.m_ticks, frameTimestamp.m_ticks); } else { m_receivedFrameSinceConnect = true; Interlocked.Exchange(ref m_lastFrameDeliveryTimestamp.m_ticks, frameTimestamp.m_ticks); } if (!Monitor.TryEnter(m_frameDataUpdateLock, 1)) return; try { int rbCount, rbStart, rbEnd; if (!ReadDirectSection(data, ref o, out rbCount, out rbStart, out rbEnd)) return; // markerSets if (!ReadDirectSection(data, ref o, out rbCount, out rbStart, out rbEnd)) return; // unlabeled if (!ReadDirectSection(data, ref o, out rbCount, out rbStart, out rbEnd)) return; // rigidBodies ParseDirectRigidBodies(data, rbCount, rbStart, rbEnd, frameTimestamp, isReplayFrame); int skelCount, skelStart, skelEnd; if (!ReadDirectSection(data, ref o, out skelCount, out skelStart, out skelEnd)) return; // skeletons ParseDirectSkeletons(data, skelCount, skelStart, skelEnd, frameTimestamp, isReplayFrame); int skipCount, skipStart, skipEnd; if (!ReadDirectSection(data, ref o, out skipCount, out skipStart, out skipEnd)) return; // assets if (!ReadDirectSection(data, ref o, out skipCount, out skipStart, out skipEnd)) return; // labeled markers if (!ReadDirectSection(data, ref o, out skipCount, out skipStart, out skipEnd)) return; // force plates if (!ReadDirectSection(data, ref o, out skipCount, out skipStart, out skipEnd)) return; // devices } finally { Monitor.Exit(m_frameDataUpdateLock); } } private static bool ReadDirectSection(byte[] data, ref int o, out int count, out int start, out int end) { count = 0; start = end = o; if (o + 8 > data.Length) return false; count = BitConverter.ToInt32(data, o); o += 4; int size = BitConverter.ToInt32(data, o); o += 4; if (count < 0 || size < 0 || o + size > data.Length) return false; start = o; end = o + size; o = end; return true; } private static bool ReadDirectRigidBody(byte[] data, ref int o, int end, out sRigidBodyData rb) { rb = new sRigidBodyData(); if (o + 38 > end || o + 38 > data.Length) return false; rb.Id = BitConverter.ToInt32(data, o); o += 4; rb.X = BitConverter.ToSingle(data, o); o += 4; rb.Y = BitConverter.ToSingle(data, o); o += 4; rb.Z = BitConverter.ToSingle(data, o); o += 4; rb.QX = BitConverter.ToSingle(data, o); o += 4; rb.QY = BitConverter.ToSingle(data, o); o += 4; rb.QZ = BitConverter.ToSingle(data, o); o += 4; rb.QW = BitConverter.ToSingle(data, o); o += 4; rb.MeanError = BitConverter.ToSingle(data, o); o += 4; rb.Params = BitConverter.ToInt16(data, o); o += 2; return true; } private void QueueStreamedTopologyDefinitionRefresh(bool preferReplayDefinitions) { if (!AutoRefreshDefinitions || SkipDataDescriptions) return; if (preferReplayDefinitions && EnableReplayPriority && !string.IsNullOrWhiteSpace(ReplayServerAddress)) { if (m_pendingReplayDefinitionRefresh || m_forceReplayDefinitionRefreshSoon) return; m_pendingReplayDefinitionRefresh = true; m_forceReplayDefinitionRefreshSoon = true; return; } if (m_pendingDefinitionRefresh || m_forceDefinitionRefreshSoon) return; m_pendingDefinitionRefresh = true; m_forceDefinitionRefreshSoon = true; } private void QueueDefinitionRefreshForFrameSource(bool isReplayFrame) { if (isReplayFrame) { if (m_directDefinitionSource != DirectDefinitionSource.Replay) QueueStreamedTopologyDefinitionRefresh(true); } else if (m_directDefinitionSource == DirectDefinitionSource.Replay) { QueueStreamedTopologyDefinitionRefresh(false); } } private static bool UpdateFrameTopologyIds(HashSet latestIds, HashSet currentIds, ref bool hasSnapshot) { if (!hasSnapshot) { latestIds.Clear(); foreach (Int32 id in currentIds) latestIds.Add(id); hasSnapshot = true; return false; } if (latestIds.SetEquals(currentIds)) return false; latestIds.Clear(); foreach (Int32 id in currentIds) latestIds.Add(id); return true; } private void ParseDirectRigidBodies(byte[] data, int count, int start, int end, OptitrackHiResTimer.Timestamp frameTimestamp, bool isReplayFrame) { int o = start; m_currentFrameRigidBodyIds.Clear(); for (int i = 0; i < count; i++) { sRigidBodyData rbData; if (!ReadDirectRigidBody(data, ref o, end, out rbData)) return; m_currentFrameRigidBodyIds.Add(rbData.Id); OptitrackRigidBodyState rbState = GetOrCreateRigidBodyState(rbData.Id); RigidBodyDataToState(rbData, frameTimestamp, rbState); } if (UpdateFrameTopologyIds(m_latestFrameRigidBodyIds, m_currentFrameRigidBodyIds, ref m_hasFrameRigidBodyTopologySnapshot)) QueueStreamedTopologyDefinitionRefresh(isReplayFrame); } private void ParseDirectSkeletons(byte[] data, int count, int start, int end, OptitrackHiResTimer.Timestamp frameTimestamp, bool isReplayFrame) { int o = start; m_currentFrameSkeletonIds.Clear(); for (int i = 0; i < count; i++) { if (o + 8 > end) return; int skeletonId = BitConverter.ToInt32(data, o); o += 4; int boneCount = BitConverter.ToInt32(data, o); o += 4; if (boneCount < 0 || boneCount > 4096 || o + boneCount * 38 > end) return; m_currentFrameSkeletonIds.Add(skeletonId); sRigidBodyData[] stagedBones = GetSkeletonFrameScratch(skeletonId, boneCount); for (int b = 0; b < boneCount; b++) { if (!ReadDirectRigidBody(data, ref o, end, out stagedBones[b])) return; } CommitDirectSkeletonFrame(skeletonId, stagedBones, boneCount, frameTimestamp, isReplayFrame); } if (UpdateFrameTopologyIds(m_latestFrameSkeletonIds, m_currentFrameSkeletonIds, ref m_hasFrameSkeletonTopologySnapshot)) QueueStreamedTopologyDefinitionRefresh(isReplayFrame); } private void CommitDirectSkeletonFrame(int skeletonId, sRigidBodyData[] stagedBones, int boneCount, OptitrackHiResTimer.Timestamp frameTimestamp, bool isReplayFrame) { OptitrackSkeletonDefinition skelDef = GetSkeletonDefinitionById(skeletonId); if (skelDef == null) { skelDef = EnsureDirectSyntheticSkeletonDefinition(skeletonId, stagedBones, boneCount); } if (skelDef == null) { QueueStreamedTopologyDefinitionRefresh(isReplayFrame); return; } OptitrackSkeletonState skelState = GetOrCreateSkeletonState(skeletonId); for (int b = 0; b < boneCount; b++) { sRigidBodyData boneData = stagedBones[b]; if (!TryGetDirectCommittableBoneId(boneData, skeletonId, skelDef, out int boneId)) continue; if (!skelState.BonePoses.ContainsKey(boneId)) skelState.BonePoses[boneId] = new OptitrackPose(); if (!skelState.LocalBonePoses.ContainsKey(boneId)) skelState.LocalBonePoses[boneId] = new OptitrackPose(); skelState.BonePoses[boneId].Position = new Vector3(-boneData.X, boneData.Y, boneData.Z); skelState.BonePoses[boneId].Orientation = new Quaternion(-boneData.QX, boneData.QY, boneData.QZ, -boneData.QW); } for (int b = 0; b < boneCount; b++) { sRigidBodyData boneData = stagedBones[b]; if (!TryGetDirectCommittableBoneId(boneData, skeletonId, skelDef, out int boneId)) continue; Vector3 bonePos = skelState.BonePoses[boneId].Position; Quaternion boneOri = skelState.BonePoses[boneId].Orientation; Vector3 parentBonePos = Vector3.zero; Quaternion parentBoneOri = Quaternion.identity; Int32 parentId = skelDef.BoneIdToParentIdMap[boneId]; if (parentId != 0 && skelState.BonePoses.TryGetValue(parentId, out OptitrackPose parentPose)) { parentBonePos = parentPose.Position; parentBoneOri = parentPose.Orientation; } skelState.LocalBonePoses[boneId].Position = bonePos - parentBonePos; skelState.LocalBonePoses[boneId].Orientation = Quaternion.Inverse(parentBoneOri) * boneOri; } skelState.DeliveryTimestamp = frameTimestamp; } private static void DirectDecodeId(int encodedId, out int assetId, out int memberId) { assetId = (encodedId >> 16) & 0x7FFF; memberId = encodedId & 0xFFFF; } private static bool TryGetDirectCommittableBoneId(sRigidBodyData boneData, Int32 skeletonId, OptitrackSkeletonDefinition skelDef, out int boneId) { int boneSkelId; DirectDecodeId(boneData.Id, out boneSkelId, out boneId); return boneSkelId == skeletonId && skelDef.BoneIdToParentIdMap.ContainsKey(boneId) && IsBoneDataUsable(boneData); } private OptitrackSkeletonDefinition EnsureDirectSyntheticSkeletonDefinition(int skeletonId, sRigidBodyData[] stagedBones, int boneCount) { OptitrackSkeletonDefinition existing = GetSkeletonDefinitionById(skeletonId); if (existing != null) return existing; string skeletonName = ChooseSyntheticSkeletonName(skeletonId); OptitrackSkeletonDefinition skelDef = new OptitrackSkeletonDefinition { Id = skeletonId, Name = skeletonName, IsSynthetic = true, Bones = new List(boneCount), BoneIdToParentIdMap = new Dictionary(), }; for (int i = 0; i < boneCount; i++) { int boneSkelId, boneId; DirectDecodeId(stagedBones[i].Id, out boneSkelId, out boneId); if (boneSkelId != skeletonId) continue; string shortName = DirectMotiveBoneName(boneId); int parentId = DirectMotiveParentId(boneId); OptitrackSkeletonDefinition.BoneDefinition boneDef = new OptitrackSkeletonDefinition.BoneDefinition { Id = boneId, ParentId = parentId, Name = skeletonName + "_" + shortName, Offset = Vector3.zero, }; skelDef.Bones.Add(boneDef); skelDef.BoneIdToParentIdMap[boneDef.Id] = boneDef.ParentId; } if (skelDef.Bones.Count == 0) return null; m_skeletonDefinitions.Add(skelDef); m_mirrorBoneIdMaps.Clear(); m_pendingSkeletonDefinitionNotify = true; return skelDef; } private string ChooseSyntheticSkeletonName(int skeletonId) { return "Skeleton" + skeletonId; } private static string DirectMotiveBoneName(int boneId) { switch (boneId) { case 1: return "Hip"; case 2: return "Ab"; case 52: return "Spine2"; case 53: return "Spine3"; case 54: return "Spine4"; case 3: return "Chest"; case 4: return "Neck"; case 58: return "Neck2"; case 5: return "Head"; case 6: return "LShoulder"; case 7: return "LUArm"; case 8: return "LFArm"; case 9: return "LHand"; case 22: return "LThumb1"; case 23: return "LThumb2"; case 24: return "LThumb3"; case 25: return "LIndex1"; case 26: return "LIndex2"; case 27: return "LIndex3"; case 28: return "LMiddle1"; case 29: return "LMiddle2"; case 30: return "LMiddle3"; case 31: return "LRing1"; case 32: return "LRing2"; case 33: return "LRing3"; case 34: return "LPinky1"; case 35: return "LPinky2"; case 36: return "LPinky3"; case 10: return "RShoulder"; case 11: return "RUArm"; case 12: return "RFArm"; case 13: return "RHand"; case 37: return "RThumb1"; case 38: return "RThumb2"; case 39: return "RThumb3"; case 40: return "RIndex1"; case 41: return "RIndex2"; case 42: return "RIndex3"; case 43: return "RMiddle1"; case 44: return "RMiddle2"; case 45: return "RMiddle3"; case 46: return "RRing1"; case 47: return "RRing2"; case 48: return "RRing3"; case 49: return "RPinky1"; case 50: return "RPinky2"; case 51: return "RPinky3"; case 14: return "LThigh"; case 15: return "LShin"; case 16: return "LFoot"; case 17: return "LToe"; case 18: return "RThigh"; case 19: return "RShin"; case 20: return "RFoot"; case 21: return "RToe"; default: return "Bone" + boneId; } } private static int DirectMotiveParentId(int boneId) { switch (boneId) { case 1: return 0; case 2: return 1; case 52: return 2; case 53: return 52; case 54: return 53; case 3: return 54; case 4: return 3; case 58: return 4; case 5: return 58; case 6: return 3; case 7: return 6; case 8: return 7; case 9: return 8; case 22: return 9; case 23: return 22; case 24: return 23; case 25: return 9; case 26: return 25; case 27: return 26; case 28: return 9; case 29: return 28; case 30: return 29; case 31: return 9; case 32: return 31; case 33: return 32; case 34: return 9; case 35: return 34; case 36: return 35; case 10: return 3; case 11: return 10; case 12: return 11; case 13: return 12; case 37: return 13; case 38: return 37; case 39: return 38; case 40: return 13; case 41: return 40; case 42: return 41; case 43: return 13; case 44: return 43; case 45: return 44; case 46: return 13; case 47: return 46; case 48: return 47; case 49: return 13; case 50: return 49; case 51: return 50; case 14: return 1; case 15: return 14; case 16: return 15; case 17: return 16; case 18: return 1; case 19: return 18; case 20: return 19; case 21: return 20; default: return 0; } } private bool ConnectDirectNatNet(IPAddress serverAddr, IPAddress localAddr, UInt16 commandPort, UInt16 dataPort, IPAddress multicastAddr) { StopDirectFrameReceiver(); string hostName; byte[] appVersion; byte[] natNetVersion; byte[] modelDefPacket; UInt16 negotiatedDataPort = dataPort; IPAddress negotiatedMulticast = multicastAddr; bool hasServerInfo = DirectRequestServerInfo(serverAddr, localAddr, commandPort, out hostName, out appVersion, out natNetVersion, out negotiatedDataPort, out negotiatedMulticast); if (!hasServerInfo) { hostName = "NatNet multicast source"; appVersion = new byte[4]; natNetVersion = new byte[] { 4, 2, 0, 0 }; negotiatedDataPort = dataPort; negotiatedMulticast = multicastAddr ?? IPAddress.Parse("239.255.42.99"); if (!m_directNoModelDefWarned) { Debug.LogWarning(GetType().FullName + ": NatNet command channel did not respond on " + serverAddr + ":" + commandPort + ". Continuing with multicast frames and synthetic skeleton definitions.", this); m_directNoModelDefWarned = true; } } if (negotiatedDataPort == 0) negotiatedDataPort = dataPort; if (negotiatedMulticast == null) negotiatedMulticast = multicastAddr ?? IPAddress.Parse("239.255.42.99"); if (hasServerInfo && DirectRequestModelDef(serverAddr, localAddr, commandPort, out modelDefPacket)) { if (!SkipDataDescriptions) { if (DirectUpdateDefinitions(modelDefPacket)) m_directDefinitionSource = DirectDefinitionSource.Live; else Debug.LogWarning(GetType().FullName + ": direct NatNet MODELDEF parse failed. Falling back to synthetic skeleton definitions from incoming frames.", this); } } else if (hasServerInfo && !m_directNoModelDefWarned) { Debug.LogWarning(GetType().FullName + ": direct NatNet MODELDEF request failed. Falling back to synthetic skeleton definitions from incoming frames.", this); m_directNoModelDefWarned = true; } ServerNatNetVersion = natNetVersion[0] + "." + natNetVersion[1] + "." + natNetVersion[2] + "." + natNetVersion[3]; ClientNatNetVersion = "Direct UDP"; if (!StartDirectFrameReceiver(localAddr.ToString(), negotiatedMulticast.ToString(), negotiatedDataPort)) return false; StoreDirectEndpoint(serverAddr, localAddr, commandPort, negotiatedDataPort, negotiatedMulticast); m_nextAutoDefinitionRefreshTime = Time.unscaledTime + Mathf.Max(DefinitionRefreshInterval, 1f); Debug.Log(GetType().FullName + ": Connected to direct NatNet server. Server=" + serverAddr + ", host=" + hostName + ", local=" + localAddr + ", serverNatNet=" + ServerNatNetVersion + ", multicast=" + negotiatedMulticast + ":" + negotiatedDataPort + ".", this); return true; } private void StoreDirectEndpoint(IPAddress serverAddr, IPAddress localAddr, UInt16 commandPort, UInt16 dataPort, IPAddress multicastAddr) { m_directServerAddress = serverAddr; m_directLocalAddress = localAddr; m_directCommandPort = commandPort; m_directDataPort = dataPort; m_directMulticastAddress = multicastAddr; } private void UpdateDirectDefinitions() { IPAddress serverAddr; IPAddress localAddr; IPAddress multicastAddr = null; UInt16 commandPort; UInt16 dataPort; try { serverAddr = IPAddress.Parse(ServerAddress); commandPort = (UInt16)Mathf.Clamp(CommandPort, 1, 65535); dataPort = (UInt16)Mathf.Clamp(DataPort, 1, 65535); if (!string.IsNullOrWhiteSpace(MulticastAddress)) multicastAddr = IPAddress.Parse(MulticastAddress.Trim()); localAddr = ResolveLocalAddress(serverAddr); ResolvedLocalAddress = localAddr.ToString(); } catch (Exception ex) { throw new InvalidOperationException("Error parsing direct NatNet refresh settings.", ex); } string hostName; byte[] appVersion; byte[] natNetVersion; byte[] modelDefPacket; UInt16 negotiatedDataPort = dataPort; IPAddress negotiatedMulticast = multicastAddr ?? IPAddress.Parse("239.255.42.99"); if (DirectRequestServerInfo(serverAddr, localAddr, commandPort, out hostName, out appVersion, out natNetVersion, out negotiatedDataPort, out negotiatedMulticast)) { if (negotiatedDataPort == 0) negotiatedDataPort = dataPort; if (negotiatedMulticast == null) negotiatedMulticast = multicastAddr ?? IPAddress.Parse("239.255.42.99"); ServerNatNetVersion = natNetVersion[0] + "." + natNetVersion[1] + "." + natNetVersion[2] + "." + natNetVersion[3]; } else { negotiatedDataPort = dataPort; negotiatedMulticast = multicastAddr ?? IPAddress.Parse("239.255.42.99"); } if (!DirectRequestModelDef(serverAddr, localAddr, commandPort, out modelDefPacket)) throw new InvalidOperationException("Direct NatNet MODELDEF request failed."); if (!DirectUpdateDefinitions(modelDefPacket)) throw new InvalidOperationException("Direct NatNet MODELDEF parse failed."); m_directDefinitionSource = DirectDefinitionSource.Live; bool dataEndpointChanged = m_directServerAddress == null || m_directLocalAddress == null || m_directMulticastAddress == null || !m_directServerAddress.Equals(serverAddr) || !m_directLocalAddress.Equals(localAddr) || !m_directMulticastAddress.Equals(negotiatedMulticast) || m_directCommandPort != commandPort || m_directDataPort != negotiatedDataPort; if (dataEndpointChanged) { if (!StartDirectFrameReceiver(localAddr.ToString(), negotiatedMulticast.ToString(), negotiatedDataPort)) throw new InvalidOperationException("Direct NatNet frame receiver failed to restart after endpoint change."); Debug.Log(GetType().FullName + ": direct NatNet frame endpoint changed during refresh; receiver restarted on " + negotiatedMulticast + ":" + negotiatedDataPort + ".", this); } StoreDirectEndpoint(serverAddr, localAddr, commandPort, negotiatedDataPort, negotiatedMulticast); ClientNatNetVersion = "Direct UDP"; } private void UpdateDirectReplayDefinitions() { IPAddress serverAddr; IPAddress localAddr; UInt16 commandPort; try { serverAddr = IPAddress.Parse(ReplayServerAddress.Trim()); commandPort = (UInt16)Mathf.Clamp(CommandPort, 1, 65535); localAddr = ResolveLocalAddress(serverAddr); } catch (Exception ex) { throw new InvalidOperationException("Error parsing MMRP replay NatNet refresh settings.", ex); } string hostName; byte[] appVersion; byte[] natNetVersion; byte[] modelDefPacket; UInt16 negotiatedDataPort; IPAddress negotiatedMulticast; DirectRequestServerInfo(serverAddr, localAddr, commandPort, out hostName, out appVersion, out natNetVersion, out negotiatedDataPort, out negotiatedMulticast); if (!DirectRequestModelDef(serverAddr, localAddr, commandPort, out modelDefPacket)) throw new InvalidOperationException("MMRP replay NatNet MODELDEF request failed."); if (!DirectUpdateDefinitions(modelDefPacket)) throw new InvalidOperationException("MMRP replay NatNet MODELDEF parse failed."); m_directDefinitionSource = DirectDefinitionSource.Replay; ClientNatNetVersion = "Direct UDP + MMRP"; } private bool DirectRequestServerInfo(IPAddress serverAddr, IPAddress localAddr, UInt16 commandPort, out string hostName, out byte[] appVersion, out byte[] natNetVersion, out UInt16 dataPort, out IPAddress multicastAddress) { hostName = ""; appVersion = new byte[4]; natNetVersion = new byte[4]; dataPort = 0; multicastAddress = null; byte[] response; byte[] pingPayload = DirectBuildPingPayload(); if (!DirectCommandRequest(serverAddr, localAddr, commandPort, 0, pingPayload, Mathf.Max(DirectServerInfoTimeoutMs, 100), out response)) // NAT_PING return false; if (response.Length < 4 || BitConverter.ToUInt16(response, 0) != 1) // NAT_PINGRESPONSE / server info return false; int payloadLen = BitConverter.ToUInt16(response, 2); if (payloadLen < 268 || response.Length < 4 + payloadLen) return false; int o = 4; hostName = DirectReadFixedString(response, o, 256); o += 256; Buffer.BlockCopy(response, o, appVersion, 0, 4); o += 4; Buffer.BlockCopy(response, o, natNetVersion, 0, 4); o += 4; // NatNet 4.x server info may append clock frequency, data port, multicast flag and multicast address. if (response.Length >= o + 8) o += 8; if (response.Length >= o + 2) { dataPort = BitConverter.ToUInt16(response, o); o += 2; } if (response.Length >= o + 1) o += 1; // multicast flag if (response.Length >= o + 4) { byte a = response[o], b = response[o + 1], c = response[o + 2], d = response[o + 3]; if (a != 0 || b != 0 || c != 0 || d != 0) multicastAddress = new IPAddress(new byte[] { a, b, c, d }); } return true; } private bool DirectRequestModelDef(IPAddress serverAddr, IPAddress localAddr, UInt16 commandPort, out byte[] modelDefPacket) { modelDefPacket = null; byte[] response; if (!DirectCommandRequest(serverAddr, localAddr, commandPort, 4, null, Mathf.Max(DirectModelDefTimeoutMs, 100), out response)) // NAT_REQUEST_MODELDEF return false; if (response.Length < 8 || BitConverter.ToUInt16(response, 0) != 5) // NAT_MODELDEF return false; modelDefPacket = response; return true; } private bool DirectCommandRequest(IPAddress serverAddr, IPAddress localAddr, UInt16 commandPort, UInt16 messageId, byte[] payload, int timeoutMs, out byte[] response) { response = null; try { using (UdpClient udp = new UdpClient()) { udp.Client.ReceiveTimeout = timeoutMs; udp.Client.Bind(new IPEndPoint(localAddr, 0)); byte[] packet = DirectBuildPacket(messageId, payload); udp.Send(packet, packet.Length, new IPEndPoint(serverAddr, commandPort)); IPEndPoint remote = new IPEndPoint(IPAddress.Any, 0); response = udp.Receive(ref remote); return response != null && response.Length >= 4; } } catch (Exception ex) { Debug.LogWarning(GetType().FullName + ": direct NatNet command " + messageId + " failed against " + serverAddr + ":" + commandPort + " (" + ex.Message + ").", this); return false; } } private static byte[] DirectBuildPacket(UInt16 messageId, byte[] payload) { int payloadLen = payload != null ? payload.Length : 0; byte[] packet = new byte[4 + payloadLen]; Array.Copy(BitConverter.GetBytes(messageId), 0, packet, 0, 2); Array.Copy(BitConverter.GetBytes((UInt16)payloadLen), 0, packet, 2, 2); if (payloadLen > 0) Buffer.BlockCopy(payload, 0, packet, 4, payloadLen); return packet; } private static byte[] DirectBuildPingPayload() { byte[] payload = new byte[264]; byte[] name = Encoding.ASCII.GetBytes("UnityDirectNatNet"); Buffer.BlockCopy(name, 0, payload, 0, Math.Min(name.Length, 255)); payload[256] = 4; payload[257] = 0; payload[258] = 0; payload[259] = 0; payload[260] = 4; payload[261] = 2; payload[262] = 0; payload[263] = 0; return payload; } private bool DirectUpdateDefinitions(byte[] modelDefPacket) { if (modelDefPacket == null || modelDefPacket.Length < 8) return false; int payloadLen = BitConverter.ToUInt16(modelDefPacket, 2); int end = Math.Min(modelDefPacket.Length, 4 + payloadLen); int o = 4; if (o + 4 > end) return false; List rigidBodies = new List(); List skeletons = new List(); int dataSetCount = BitConverter.ToInt32(modelDefPacket, o); o += 4; for (int i = 0; i < dataSetCount && o + 8 <= end; i++) { int dataSetType = BitConverter.ToInt32(modelDefPacket, o); o += 4; int dataSetSize = BitConverter.ToInt32(modelDefPacket, o); o += 4; if (dataSetSize < 0 || o + dataSetSize > end) return false; int dataSetEnd = o + dataSetSize; try { if (dataSetType == 1) { rigidBodies.Add(DirectReadRigidBodyDefinition(modelDefPacket, ref o, dataSetEnd)); } else if (dataSetType == 2) { skeletons.Add(DirectReadSkeletonDefinition(modelDefPacket, ref o, dataSetEnd)); } } catch { return false; } o = dataSetEnd; } m_definitionRefreshFailureCount = 0; m_rigidBodyDefinitions.Clear(); m_skeletonDefinitions.Clear(); m_tmarkersetDefinitions.Clear(); m_mirrorBoneIdMaps.Clear(); m_rigidBodyDefinitions.AddRange(rigidBodies); m_skeletonDefinitions.AddRange(skeletons); lock (m_frameDataUpdateLock) { m_assetIdToNameCache.Clear(); } PruneStatesMissingFromDefinitions(); NotifyRegisteredSkeletonDefinitionsChanged(); return true; } private static OptitrackRigidBodyDefinition DirectReadRigidBodyDefinition(byte[] data, ref int o, int end) { string name; int id, parentId, markerCount; Vector3 offset; DirectReadRigidBodyDesc(data, ref o, end, out name, out id, out parentId, out offset, out markerCount); OptitrackRigidBodyDefinition rbDef = new OptitrackRigidBodyDefinition { Id = id, Name = name, Markers = new List(Math.Max(0, markerCount)), }; for (int i = 0; i < markerCount; i++) rbDef.Markers.Add(new OptitrackRigidBodyDefinition.MarkerDefinition()); return rbDef; } private static OptitrackSkeletonDefinition DirectReadSkeletonDefinition(byte[] data, ref int o, int end) { string name = DirectReadCString(data, ref o, end); int id = DirectReadInt32(data, ref o, end); int boneCount = DirectReadInt32(data, ref o, end); OptitrackSkeletonDefinition skelDef = new OptitrackSkeletonDefinition { Id = id, Name = name, Bones = new List(Math.Max(0, boneCount)), BoneIdToParentIdMap = new Dictionary(), }; for (int i = 0; i < boneCount; i++) { string boneName; int boneId, parentId, markerCount; Vector3 offset; DirectReadRigidBodyDesc(data, ref o, end, out boneName, out boneId, out parentId, out offset, out markerCount); OptitrackSkeletonDefinition.BoneDefinition boneDef = new OptitrackSkeletonDefinition.BoneDefinition { Id = boneId, ParentId = parentId, Name = boneName, Offset = offset, }; skelDef.Bones.Add(boneDef); skelDef.BoneIdToParentIdMap[boneDef.Id] = boneDef.ParentId; } return skelDef; } private static void DirectReadRigidBodyDesc(byte[] data, ref int o, int end, out string name, out int id, out int parentId, out Vector3 offset, out int markerCount) { name = DirectReadCString(data, ref o, end); id = DirectReadInt32(data, ref o, end); parentId = DirectReadInt32(data, ref o, end); float x = DirectReadFloat(data, ref o, end); float y = DirectReadFloat(data, ref o, end); float z = DirectReadFloat(data, ref o, end); offset = new Vector3(-x, y, z); // Orientation offset is present in NatNet 4.x descriptions. DirectReadFloat(data, ref o, end); DirectReadFloat(data, ref o, end); DirectReadFloat(data, ref o, end); DirectReadFloat(data, ref o, end); markerCount = DirectReadInt32(data, ref o, end); int markerBytes = markerCount * (12 + 4); if (markerCount < 0 || o + markerBytes > end) throw new ArgumentOutOfRangeException("markerCount"); o += markerBytes; for (int i = 0; i < markerCount; i++) DirectReadCString(data, ref o, end); } private static int DirectReadInt32(byte[] data, ref int o, int end) { if (o + 4 > end) throw new ArgumentOutOfRangeException("o"); int v = BitConverter.ToInt32(data, o); o += 4; return v; } private static float DirectReadFloat(byte[] data, ref int o, int end) { if (o + 4 > end) throw new ArgumentOutOfRangeException("o"); float v = BitConverter.ToSingle(data, o); o += 4; return v; } private static string DirectReadCString(byte[] data, ref int o, int end) { if (o >= end) return ""; int start = o; while (o < end && data[o] != 0) o++; string value = Encoding.UTF8.GetString(data, start, o - start); if (o < end) o++; return value; } private static string DirectReadFixedString(byte[] data, int o, int maxLen) { int len = 0; while (len < maxLen && o + len < data.Length && data[o + len] != 0) len++; return Encoding.UTF8.GetString(data, o, len); } private System.Collections.IEnumerator ConnectCoroutine() { m_receivedFrameSinceConnect = false; IPAddress serverAddr; IPAddress localAddr; IPAddress multicastAddr = null; UInt16 commandPort; UInt16 dataPort; NatNetConnectionType connType; try { serverAddr = IPAddress.Parse( ServerAddress ); commandPort = (UInt16)Mathf.Clamp( CommandPort, 1, 65535 ); dataPort = (UInt16)Mathf.Clamp( DataPort, 1, 65535 ); if ( !string.IsNullOrWhiteSpace( MulticastAddress ) ) multicastAddr = IPAddress.Parse( MulticastAddress.Trim() ); localAddr = ResolveLocalAddress( serverAddr ); ResolvedLocalAddress = localAddr.ToString(); connType = ConnectionType == ClientConnectionType.Unicast ? NatNetConnectionType.NatNetConnectionType_Unicast : NatNetConnectionType.NatNetConnectionType_Multicast; } catch ( Exception ex ) { Debug.LogException( ex, this ); Debug.LogError( GetType().FullName + ": Error parsing connection settings. Server=" + ServerAddress + ", command=" + CommandPort + ", data=" + DataPort + ", multicast=" + MulticastAddress + ".", this ); yield break; } if (ConnectionType == ClientConnectionType.Multicast) { IPAddress directMulticastAddr = multicastAddr ?? IPAddress.Parse("239.255.42.99"); if (!ConnectDirectNatNet(serverAddr, localAddr, commandPort, dataPort, directMulticastAddr)) { Debug.LogError(GetType().FullName + ": Error connecting to direct NatNet server. Server=" + ServerAddress + ", local=" + localAddr + ", command=" + commandPort + ", data=" + dataPort + ", multicast=" + directMulticastAddr + ". Check Motive streaming, firewall, and network interface.", this); yield break; } if (m_replayConnectCoroutine != null) { StopCoroutine(m_replayConnectCoroutine); m_replayConnectCoroutine = null; CleanupReplayClient(); } if (RecordOnPlay && !m_isReconnecting) Debug.LogWarning(GetType().FullName + ": RecordOnPlay requires the NatNet SDK command path and is skipped in direct multicast mode.", this); m_connectionHealthCoroutine = StartCoroutine(CheckConnectionHealth()); yield break; } Debug.LogError(GetType().FullName + ": Unicast/NatNet SDK connection is disabled. This project now uses the direct UDP multicast NatNet path only.", this); yield break; #pragma warning disable 162 bool applyServerSettings = !m_hasAppliedServerSettings; NatNetClient connectedClient = null; Exception connectError = null; System.Threading.Tasks.Task connectTask = System.Threading.Tasks.Task.Run( () => { NatNetClient c = null; try { c = new NatNetClient(); if (commandPort == k_DefaultNatNetCommandPort && dataPort == k_DefaultNatNetDataPort && multicastAddr == null) { c.Connect( connType, localAddr, serverAddr ); } else { c.Connect( connType, localAddr, serverAddr, commandPort, dataPort, multicastAddr ); } if ( applyServerSettings ) { // Remotely change the Skeleton Coordinate property to Global/Local if (SkeletonCoordinates == StreamingCoordinatesValues.Global) c.RequestCommand("SetProperty,,Skeleton Coordinates,false"); else c.RequestCommand("SetProperty,,Skeleton Coordinates,true"); // Remotely change the Bone Naming Convention to Motive/FBX/BVH if (BoneNamingConvention == OptitrackBoneNameConvention.Motive) c.RequestCommand("SetProperty,,Bone Naming Convention,0"); else if (BoneNamingConvention == OptitrackBoneNameConvention.FBX) c.RequestCommand("SetProperty,,Bone Naming Convention,1"); else if (BoneNamingConvention == OptitrackBoneNameConvention.BVH) c.RequestCommand("SetProperty,,Bone Naming Convention,2"); } connectedClient = c; } catch ( Exception ex ) { connectError = ex; if ( c != null ) { try { c.Dispose(); } catch { } } } } ); while ( !connectTask.IsCompleted ) yield return null; if ( connectError != null || connectedClient == null ) { if ( connectError != null ) Debug.LogException( connectError, this ); Debug.LogError( GetType().FullName + ": Error connecting to NatNet server. Server=" + ServerAddress + ", local=" + localAddr + ", command=" + commandPort + ", data=" + dataPort + ", multicast=" + (multicastAddr != null ? multicastAddr.ToString() : "default") + ". Check Motive streaming, firewall, and network interface.", this ); yield break; } m_client = connectedClient; if ( applyServerSettings ) m_hasAppliedServerSettings = true; else Debug.Log(GetType().FullName + ": reconnect skipped SetProperty commands; they were already applied on the first connection.", this); if (!m_isReconnecting) yield return new UnityEngine.WaitForSeconds( 0.1f ); try { if (!SkipDataDescriptions) { UpdateDefinitions(); m_nextAutoDefinitionRefreshTime = Time.unscaledTime + Mathf.Max(DefinitionRefreshInterval, 1f); } } catch (Exception ex) { Debug.LogException(ex, this); } ResubscribeRegisteredAssets(); if (RecordOnPlay && !m_isReconnecting) StartRecording(); byte[] NatNetVersion = m_client.ServerDescription.NatNetVersion; ServerNatNetVersion = NatNetVersion[0] + "." + NatNetVersion[1] + "." + NatNetVersion[2] + "." + NatNetVersion[3]; ClientNatNetVersion = "" + NatNetClient.NatNetLibVersion; Debug.Log(GetType().FullName + ": Connected to NatNet server. Server=" + ServerAddress + ", local=" + ResolvedLocalAddress + ", serverNatNet=" + ServerNatNetVersion + ", clientNatNet=" + ClientNatNetVersion + ", connection=" + ConnectionType + ".", this); m_client.NativeFrameReceived += OnNatNetFrameReceived; m_connectionHealthCoroutine = StartCoroutine( CheckConnectionHealth() ); #pragma warning restore 162 } /// /// Disconnects from the streaming server and cleans up . /// void OnDisable() { StopAllCoroutines(); StopDirectFrameReceiver(); m_connectionHealthCoroutine = null; m_replayConnectCoroutine = null; m_isReconnecting = false; CleanupReplayClient(); if (m_client != null) { if (RecordOnPlay) StopRecording(); m_client.NativeFrameReceived -= OnNatNetFrameReceived; try { m_client.Disconnect(); } catch { } m_client.Dispose(); m_client = null; } } System.Collections.IEnumerator CheckConnectionHealth() { float healthCheckIntervalSeconds = Mathf.Max(ConnectionHealthCheckIntervalSeconds, 0.1f); float recentFrameThresholdSeconds = Mathf.Max(StreamingFrameTimeoutSeconds, healthCheckIntervalSeconds); // The lifespan of these variables is tied to the lifespan of a single connection session. // The coroutine is stopped on disconnect and restarted on connect. YieldInstruction checkIntervalYield = new WaitForSeconds( healthCheckIntervalSeconds ); OptitrackHiResTimer.Timestamp connectionInitiatedTimestamp = OptitrackHiResTimer.Now(); bool wasReceivingFrames = false; bool warnedPendingFirstFrame = false; while ( true ) { yield return checkIntervalYield; if ( m_receivedFrameSinceConnect == false && !IsReplayFrameFresh() ) { // Still waiting for first frame. Warn exactly once if this takes too long. if ( connectionInitiatedTimestamp.AgeSeconds > recentFrameThresholdSeconds ) { if ( warnedPendingFirstFrame == false ) { if (m_directNatNetConnected) Debug.LogWarning( GetType().FullName + ": Direct NatNet receiver is running but no frames have arrived in Unity yet. Check Unity Editor firewall permission or multicast routing.", this ); else Debug.LogWarning( GetType().FullName + ": No frames received from the server yet. Verify your connection settings are correct and that the server is streaming.", this ); warnedPendingFirstFrame = true; } continue; } } else { // We've received at least one frame, do ongoing checks for changes in connection health. bool receivedRecentFrame = HasRecentStreamingFrame(recentFrameThresholdSeconds); if ( wasReceivingFrames == false && receivedRecentFrame == true ) { // Transition: Bad health -> good health. wasReceivingFrames = true; Debug.Log( GetType().FullName + ": Receiving streaming data from the server.", this ); continue; } else if ( wasReceivingFrames == true && receivedRecentFrame == false ) { // Transition: Good health -> bad health. wasReceivingFrames = false; Debug.LogWarning( GetType().FullName + ": No streaming frames received from the server recently.", this ); if ( AutoReconnect ) { Debug.Log( GetType().FullName + ": starting automatic reconnect.", this ); Reconnect(); } continue; } } } } #region Private methods /// /// Event handler for NatNet frame delivery. Updates our simplified state representations. /// NOTE: This executes in the context of the NatNetLib network service thread! /// /// /// Because the type is expensive to marshal, we instead utilize the /// , treating it as as opaque, and /// passing it to some helper "accessor" functions to retrieve the subset of data we care about, using only /// blittable types which do not cause any garbage to be allocated. /// /// /// private void OnNatNetFrameReceived( object sender, NatNetClient.NativeFrameReceivedEventArgs eventArgs ) { bool isReplayFrame = ReferenceEquals(sender, m_replayClient); if (!isReplayFrame && IsReplayFrameFresh()) { return; } if ( ! Monitor.TryEnter( m_frameDataUpdateLock, 1 ) ) { return; } try { // Update health markers. OptitrackHiResTimer.Timestamp frameTimestamp = OptitrackHiResTimer.Now(); if (isReplayFrame) { m_replayReceivedFrameSinceConnect = true; Interlocked.Exchange(ref m_lastReplayFrameDeliveryTimestamp.m_ticks, frameTimestamp.m_ticks); } m_receivedFrameSinceConnect = true; Interlocked.Exchange( ref m_lastFrameDeliveryTimestamp.m_ticks, frameTimestamp.m_ticks ); // Process received frame. IntPtr pFrame = eventArgs.NativeFramePointer; NatNetError result = NatNetError.NatNetError_OK; // get timestamp UInt64 transmitTimestamp; result = NaturalPoint.NatNetLib.NativeMethods.NatNet_Frame_GetTransmitTimestamp(pFrame, out transmitTimestamp); // get and decode timecode (if available) UInt32 timecode; UInt32 timecodeSubframe; result = NaturalPoint.NatNetLib.NativeMethods.NatNet_Frame_GetTimecode(pFrame, out timecode, out timecodeSubframe); Int32 hour, minute, second, frameNumber, subframeNumber; NaturalPoint.NatNetLib.NativeMethods.NatNet_DecodeTimecode(timecode, timecodeSubframe, out hour, out minute, out second, out frameNumber, out subframeNumber); //Debug.Log(hour + "......" + minute + second + frameNumber + subframeNumber); // ---------------------- // - Update rigid bodies // ---------------------- Int32 frameRbCount; result = NaturalPoint.NatNetLib.NativeMethods.NatNet_Frame_GetRigidBodyCount( pFrame, out frameRbCount ); NatNetException.ThrowIfNotOK( result, "NatNet_Frame_GetRigidBodyCount failed." ); m_currentFrameRigidBodyIds.Clear(); for (int rbIdx = 0; rbIdx < frameRbCount; ++rbIdx) { sRigidBodyData rbData = new sRigidBodyData(); result = NaturalPoint.NatNetLib.NativeMethods.NatNet_Frame_GetRigidBody( pFrame, rbIdx, out rbData ); NatNetException.ThrowIfNotOK( result, "NatNet_Frame_GetRigidBody failed." ); m_currentFrameRigidBodyIds.Add(rbData.Id); // Ensure we have a state corresponding to this rigid body ID. OptitrackRigidBodyState rbState = GetOrCreateRigidBodyState( rbData.Id ); RigidBodyDataToState(rbData, OptitrackHiResTimer.Now(), rbState); } if (UpdateFrameTopologyIds(m_latestFrameRigidBodyIds, m_currentFrameRigidBodyIds, ref m_hasFrameRigidBodyTopologySnapshot)) QueueStreamedTopologyDefinitionRefresh(isReplayFrame); // ---------------------- // - Update skeletons // ---------------------- Int32 frameSkeletonCount; result = NaturalPoint.NatNetLib.NativeMethods.NatNet_Frame_GetSkeletonCount( pFrame, out frameSkeletonCount ); NatNetException.ThrowIfNotOK( result, "NatNet_Frame_GetSkeletonCount failed." ); m_currentFrameSkeletonIds.Clear(); for (int skelIdx = 0; skelIdx < frameSkeletonCount; ++skelIdx) { Int32 skeletonId; result = NaturalPoint.NatNetLib.NativeMethods.NatNet_Frame_Skeleton_GetId( pFrame, skelIdx, out skeletonId ); NatNetException.ThrowIfNotOK( result, "NatNet_Frame_Skeleton_GetId failed." ); m_currentFrameSkeletonIds.Add(skeletonId); // Ensure we have a state corresponding to this skeleton ID. OptitrackSkeletonState skelState = GetOrCreateSkeletonState( skeletonId ); // Enumerate this skeleton's bone rigid bodies. Int32 skelRbCount; result = NaturalPoint.NatNetLib.NativeMethods.NatNet_Frame_Skeleton_GetRigidBodyCount( pFrame, skelIdx, out skelRbCount ); NatNetException.ThrowIfNotOK( result, "NatNet_Frame_Skeleton_GetRigidBodyCount failed." ); OptitrackSkeletonDefinition skelDef = GetSkeletonDefinitionById(skeletonId); if (skelDef == null) { if (!m_pendingDefinitionRefresh) { Debug.LogWarning(GetType().FullName + ": missing skeleton definition for streamed skeleton ID " + skeletonId + "; scheduling definition refresh.", this); } QueueStreamedTopologyDefinitionRefresh(isReplayFrame); continue; } sRigidBodyData[] stagedBones = GetSkeletonFrameScratch(skeletonId, skelRbCount); for (int boneIdx = 0; boneIdx < skelRbCount; ++boneIdx) { result = NaturalPoint.NatNetLib.NativeMethods.NatNet_Frame_Skeleton_GetRigidBody( pFrame, skelIdx, boneIdx, out stagedBones[boneIdx] ); NatNetException.ThrowIfNotOK( result, "NatNet_Frame_Skeleton_GetRigidBody failed." ); } for (int boneIdx = 0; boneIdx < skelRbCount; ++boneIdx) { sRigidBodyData boneData = stagedBones[boneIdx]; if (!TryGetCommittableBoneId(boneData, skeletonId, skelDef, out int boneId)) continue; // TODO: Could pre-populate this map when the definitions are retrieved. // Should never allocate after the first frame, at least. if (skelState.BonePoses.ContainsKey( boneId ) == false) { skelState.BonePoses[boneId] = new OptitrackPose(); } if (skelState.LocalBonePoses.ContainsKey( boneId ) == false) { skelState.LocalBonePoses[boneId] = new OptitrackPose(); } // Flip coordinate handedness from right to left by inverting X and W. Vector3 bonePos = new Vector3(-boneData.X, boneData.Y, boneData.Z); Quaternion boneOri = new Quaternion(-boneData.QX, boneData.QY, boneData.QZ, -boneData.QW); skelState.BonePoses[boneId].Position = bonePos; skelState.BonePoses[boneId].Orientation = boneOri; } // Derive locals in a second pass so parent order in the payload does not matter. for (int boneIdx = 0; boneIdx < skelRbCount; ++boneIdx) { sRigidBodyData boneData = stagedBones[boneIdx]; if (!TryGetCommittableBoneId(boneData, skeletonId, skelDef, out int boneId)) continue; Vector3 bonePos = skelState.BonePoses[boneId].Position; Quaternion boneOri = skelState.BonePoses[boneId].Orientation; Vector3 parentBonePos = new Vector3(0,0,0); Quaternion parentBoneOri = new Quaternion(0,0,0,1); Int32 pId = skelDef.BoneIdToParentIdMap[boneId]; if (pId != 0 && skelState.BonePoses.TryGetValue(pId, out OptitrackPose parentPose)) { parentBonePos = parentPose.Position; parentBoneOri = parentPose.Orientation; } skelState.LocalBonePoses[boneId].Position = bonePos - parentBonePos; skelState.LocalBonePoses[boneId].Orientation = Quaternion.Inverse(parentBoneOri) * boneOri; } skelState.DeliveryTimestamp = frameTimestamp; } if (UpdateFrameTopologyIds(m_latestFrameSkeletonIds, m_currentFrameSkeletonIds, ref m_hasFrameSkeletonTopologySnapshot)) QueueStreamedTopologyDefinitionRefresh(isReplayFrame); // ----------------------------------------------------- // - Update trained markerset // trained markerset added // ---------------------------------------------------- var dataDescsSnapshot = m_dataDescs; m_latestTMarkMarkerStates.Clear(); if (dataDescsSnapshot != null && dataDescsSnapshot.AssetDescriptions != null) for (int tmarkIdx = 0; tmarkIdx < dataDescsSnapshot.AssetDescriptions.Count; ++tmarkIdx) { Int32 tmarkersetId = dataDescsSnapshot.AssetDescriptions[tmarkIdx].AssetID; // Ensure we have a state corresponding to this tmarkerset ID. OptitrackTMarkersetState tmarkState = GetOrCreateTMarkersetState(tmarkersetId); Int32 tmarkRbCount = dataDescsSnapshot.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) { sRigidBodyData boneData = new sRigidBodyData(); result = NaturalPoint.NatNetLib.NativeMethods.NatNet_Frame_TMarkerset_GetRigidBody(pFrame, tmarkIdx, boneIdx, out boneData); NatNetException.ThrowIfNotOK(result, "NatNet_Frame_TMarkerset_GetRigidBody failed."); Int32 boneTMarkId, boneId; NaturalPoint.NatNetLib.NativeMethods.NatNet_DecodeID(boneData.Id, out boneTMarkId, out boneId); if (tmarkState.BonePoses.ContainsKey(boneId) == false) { tmarkState.BonePoses[boneId] = new OptitrackPose(); } if (tmarkState.LocalBonePoses.ContainsKey(boneId) == false) { tmarkState.LocalBonePoses[boneId] = new OptitrackPose(); } // Flip coordinate handedness from right to left by inverting X and W. Vector3 bonePos = new Vector3(-boneData.X, boneData.Y, boneData.Z); Quaternion boneOri = new Quaternion(-boneData.QX, boneData.QY, boneData.QZ, -boneData.QW); tmarkState.BonePoses[boneId].Position = bonePos; tmarkState.BonePoses[boneId].Orientation = boneOri; Vector3 parentBonePos = new Vector3(0, 0, 0); Quaternion parentBoneOri = new Quaternion(0, 0, 0, 1); Int32 pId = tmarkDef.BoneIdToParentIdMap[boneId]; if (pId != -1) { parentBonePos = tmarkState.BonePoses[pId].Position; parentBoneOri = tmarkState.BonePoses[pId].Orientation; } tmarkState.LocalBonePoses[boneId].Position = bonePos - parentBonePos; tmarkState.LocalBonePoses[boneId].Orientation = Quaternion.Inverse(parentBoneOri) * boneOri; } // -------------------------------------------- // - Update trained markerset markers // -------------------------------------------- Int32 tmarkMarkerCount = dataDescsSnapshot.AssetDescriptions[tmarkIdx].MarkerCount; /*result = NaturalPoint.NatNetLib.NativeMethods.NatNet_Frame_TMarkerset_GetMarkerCount(pFrame, tmarkIdx, out tmarkMarkerCount); NatNetException.ThrowIfNotOK(result, "NatNet_Frame_TMarkerset_GetMarkerCount failed.");*/ //Debug.Log("tmark marker count: " + tmarkMarkerCount); // working finally // Update Trained Markerset Marker data for (int markerIdx = 0; markerIdx < tmarkMarkerCount; ++markerIdx) { sMarker tmarker = new sMarker(); // markerData result = NaturalPoint.NatNetLib.NativeMethods.NatNet_Frame_TMarkerset_GetMarker(pFrame, tmarkIdx, markerIdx, out tmarker); NatNetException.ThrowIfNotOK(result, "NatNet_Frame_TMarkerset_GetMarker failed."); //Debug.Log("result: " + result); // Flip coordinate handedness OptitrackMarkerState tmarkerState = GetOrCreateTMarkMarkerState(tmarker.Id); tmarkerState.Name = GetMarkerName(tmarker); tmarkerState.Position = new Vector3(-tmarker.X, tmarker.Y, tmarker.Z); tmarkerState.Size = tmarker.Size; tmarkerState.Labeled = (tmarker.Params & 0x10) == 0; tmarkerState.Id = tmarker.Id; tmarkerState.IsActive = (tmarker.Params & 0x20) != 0; } } // ---------------------- // - Update markers // ---------------------- Int32 MarkerCount; result = NaturalPoint.NatNetLib.NativeMethods.NatNet_Frame_GetLabeledMarkerCount( pFrame, out MarkerCount ); NatNetException.ThrowIfNotOK( result, "NatNet_Frame_GetLabeledMarkerCount failed."); m_latestMarkerStates.Clear(); //Debug.Log("marker count: " + MarkerCount); for (int markerIdx = 0; markerIdx < MarkerCount; ++markerIdx) { sMarker marker = new sMarker(); result = NaturalPoint.NatNetLib.NativeMethods.NatNet_Frame_GetLabeledMarker( pFrame, markerIdx, out marker ); NatNetException.ThrowIfNotOK( result, "NatNet_Frame_GetLabeledMarker failed." ); // Flip coordinate handedness OptitrackMarkerState markerState = GetOrCreateMarkerState( marker.Id ); markerState.Name = GetMarkerName( marker ); markerState.Position = new Vector3( -marker.X, marker.Y, marker.Z ); markerState.Size = marker.Size; markerState.Labeled = (marker.Params & 0x10) == 0; markerState.Id = marker.Id; markerState.IsActive = (marker.Params & 0x20) != 0; } } catch (Exception ex) { Debug.LogError( GetType().FullName + ": OnNatNetFrameReceived encountered an exception.", this ); Debug.LogException( ex, this ); } finally { Monitor.Exit( m_frameDataUpdateLock ); } } private string GetMarkerName( sMarker marker ) { int assetID = marker.Id >> 16; // high word = Asset ID Number int memberID = marker.Id & 0x00ffff; // low word = Member ID Number (constraint number) string assetName; if (!m_assetIdToNameCache.TryGetValue(assetID, out assetName)) { 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; } bool IsLabeled = (marker.Params & 0x10) == 0; bool IsActive = (marker.Params & 0x20) != 0; if (IsActive && !IsLabeled) return "Active " + marker.Id.ToString(); else if (IsActive && IsLabeled) return "Active " + marker.Id.ToString() + " (" + assetName + " Member ID: " + memberID + " )"; else if (!IsActive && !IsLabeled) return "Passive " + marker.Id.ToString(); else return "Passive (" + assetName + " Member ID: " + memberID + ")"; } private void RigidBodyDataToState(sRigidBodyData rbData, OptitrackHiResTimer.Timestamp timestamp, OptitrackRigidBodyState rbState) { rbState.DeliveryTimestamp = timestamp; rbState.Pose = new OptitrackPose { Position = new Vector3(-rbData.X, rbData.Y, rbData.Z), Orientation = new Quaternion(-rbData.QX, rbData.QY, rbData.QZ, -rbData.QW), }; rbState.IsTracked = (rbData.Params & 0x01) != 0; } private void ResetStreamingSubscriptions() { m_client.RequestCommand( "SubscribeToData" ); } private void SubscribeRigidBody( MonoBehaviour component, Int32 rigidBodyId ) { if ( m_client != null && ConnectionType == ClientConnectionType.Unicast ) { // Try subscribing up to 3 times with a 2000 ms timeout before giving up. bool subscribeSucceeded = m_client.RequestCommand( "SubscribeByID,RigidBody," + rigidBodyId, 2000, 3 ); // Log a warning on the first failure. if ( ! subscribeSucceeded && ! m_doneSubscriptionNotice ) { if ( m_client.ServerDescription.HostApp == "Motive" ) { // Host app is Motive: If new enough to support subscription, failure is an error. // Otherwise, warn them that they may want to update Motive to reduce bandwidth consumption. if ( m_client.ServerAppVersion >= new Version(2, 2, 0) ) { Debug.LogError( "Failed to subscribe to rigid body streaming data for component", component); } else { Debug.LogWarning("Your version of Motive is too old to support NatNet rigid body data subscription; streaming bandwidth consumption may be higher than necessary. This feature works in Motive 2.2.0+."); } } else { // Not Motive, we don't know whether it "should" support this. Warning instead of error. Debug.LogWarning( "Failed to subscribe to rigid body streaming data for component", component ); } m_doneSubscriptionNotice = true; } } } private void SubscribeSkeleton(MonoBehaviour component, string name ) { if (m_client != null && ConnectionType == ClientConnectionType.Unicast) { if (m_client.ServerAppVersion >= new Version(2, 2, 1)) { // Try subscribing up to 3 times with a 2000 ms timeout before giving up. bool subscribeSucceeded = m_client.RequestCommand("SubscribeToData,Skeleton," + name, 2000, 3); // Log a warning on the first failure. if (!subscribeSucceeded && !m_doneSubscriptionNotice) { Debug.LogError("Failed to subscribe to skeleton streaming data for component", component); m_doneSubscriptionNotice = true; } } else if (m_client.ServerAppVersion == new Version(2, 2, 0, 0)) { // Motive 2.2.0 has a bug were Motive says it subscribes successfully, but doesn't. // Subscribing to all skeletons still works, so for this version that is done instead. // Try subscribing up to 3 times with a 2000 ms timeout before giving up. bool subscribeSucceeded = m_client.RequestCommand("SubscribeToData,Skeleton,All" + name, 2000, 3); if (!subscribeSucceeded && !m_doneSubscriptionNotice) { Debug.LogError("Failed to subscribe to all skeletons streaming data some unknown reason.", component); m_doneSubscriptionNotice = true; } } else { Debug.LogWarning("Your version of Motive is too old to support NatNet skeleton data subscription; streaming bandwidth consumption may be higher than necessary. This feature works in Motive 2.2.1+."); m_doneSubscriptionNotice = true; } } } private void SubscribeTMarkerset(MonoBehaviour component, string name) // check the version numbers // trained markerset added { if (m_client != null && ConnectionType == ClientConnectionType.Unicast) { if (m_client.ServerAppVersion >= new Version(2, 2, 1)) { // Try subscribing up to 3 times with a 2000 ms timeout before giving up. bool subscribeSucceeded = m_client.RequestCommand("SubscribeToData,TrainedMarkersets," + name, 2000, 3); // Log a warning on the first failure. if (!subscribeSucceeded && !m_doneSubscriptionNotice) { Debug.LogError("Failed to subscribe to trained markerset streaming data for component", component); m_doneSubscriptionNotice = true; } } else if (m_client.ServerAppVersion == new Version(2, 2, 0, 0)) { // Motive 2.2.0 has a bug were Motive says it subscribes successfully, but doesn't. // Subscribing to all skeletons still works, so for this version that is done instead. // Try subscribing up to 3 times with a 2000 ms timeout before giving up. bool subscribeSucceeded = m_client.RequestCommand("SubscribeToData,TrainedMarkersets,All" + name, 2000, 3); if (!subscribeSucceeded && !m_doneSubscriptionNotice) { Debug.LogError("Failed to subscribe to all trained markersets streaming data some unknown reason.", component); m_doneSubscriptionNotice = true; } } else { Debug.LogWarning("Your version of Motive is too old to support NatNet skeleton data subscription; streaming bandwidth consumption may be higher than necessary. This feature works in Motive 2.2.1+."); m_doneSubscriptionNotice = true; } } } private void SubscribeTMarkMarkers() { if (m_client != null && ConnectionType == ClientConnectionType.Unicast) { bool subscribeSucceeded4 = m_client.RequestCommand("SubscribeToData,TrainedMarkersetMarkers,All", 2000, 3); //Debug.Log("TMMarkers: " + subscribeSucceeded4); // Log a warning on the first failure. if (!subscribeSucceeded4 && !m_doneSubscriptionNotice) { if (m_client.ServerDescription.HostApp == "Motive") { // Host app is Motive: If new enough to support subscription, failure is an error. // Otherwise, warn them that they may want to update Motive to reduce bandwidth consumption. if (m_client.ServerAppVersion >= new Version(2, 2, 0)) { Debug.LogError("Failed to subscribe to tmark marker streaming data"); } else { Debug.LogWarning("Your version of Motive is too old to support NatNet tmark marker data subscription; streaming bandwidth consumption may be higher than necessary. This feature works in Motive 2.2.0+."); } } else { // Not Motive, we don't know whether it "should" support this. Warning instead of error. Debug.LogWarning("Failed to subscribe to tmark marker streaming data"); } m_doneSubscriptionNotice = true; } } } private void SubscribeMarkers( ) { if (m_client != null && ConnectionType == ClientConnectionType.Unicast) { // Try subscribing up to 3 times with a 2000 ms timeout before giving up. bool subscribeSucceeded = m_client.RequestCommand("SubscribeToData,MarkerSetMarkers,All", 2000, 3); bool subscribeSucceeded2 = m_client.RequestCommand("SubscribeToData,LabeledMarkers,All", 2000, 3); bool subscribeSucceeded3 = m_client.RequestCommand("SubscribeToData,LegacyUnlabeledMarkers,All", 2000, 3); //bool subscribeSucceeded4 = m_client.RequestCommand("SubscribeToData, TrainedMarkersetMarkers,All", 2000, 3); //bool allSubscribeSucceeded = subscribeSucceeded4; bool allSubscribeSucceeded = subscribeSucceeded && subscribeSucceeded2 && subscribeSucceeded3; m_subscribedToMarkers = allSubscribeSucceeded; // Log a warning on the first failure. if (!allSubscribeSucceeded && !m_doneSubscriptionNotice) { if (m_client.ServerDescription.HostApp == "Motive") { // Host app is Motive: If new enough to support subscription, failure is an error. // Otherwise, warn them that they may want to update Motive to reduce bandwidth consumption. if (m_client.ServerAppVersion >= new Version(2, 2, 0)) { Debug.LogError("Failed to subscribe to marker streaming data"); } else { Debug.LogWarning("Your version of Motive is too old to support NatNet rigid body data subscription; streaming bandwidth consumption may be higher than necessary. This feature works in Motive 2.2.0+."); } } else { // Not Motive, we don't know whether it "should" support this. Warning instead of error. Debug.LogWarning("Failed to subscribe to marker streaming data"); } m_doneSubscriptionNotice = true; } } } private IPAddress ResolveLocalAddress(IPAddress serverAddress) { if (!AutoDetectLocalAddress) return IPAddress.Parse(LocalAddress); if (IPAddress.IsLoopback(serverAddress)) return IPAddress.Loopback; try { using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)) { socket.Connect(new IPEndPoint(serverAddress, Mathf.Clamp(CommandPort, 1, 65535))); if (socket.LocalEndPoint is IPEndPoint endpoint && endpoint.Address.AddressFamily == AddressFamily.InterNetwork && !IPAddress.Any.Equals(endpoint.Address)) { LocalAddress = endpoint.Address.ToString(); return endpoint.Address; } } } catch (Exception ex) { Debug.LogWarning(GetType().FullName + ": failed to auto-detect local address. Falling back to LocalAddress. " + ex.Message, this); } return IPAddress.Parse(LocalAddress); } private sRigidBodyData[] GetSkeletonFrameScratch(Int32 skeletonId, Int32 boneCount) { if (!m_skeletonFrameScratch.TryGetValue(skeletonId, out var scratch) || scratch.Length != boneCount) { scratch = new sRigidBodyData[boneCount]; m_skeletonFrameScratch[skeletonId] = scratch; } return scratch; } /// /// private static bool IsBoneDataUsable(sRigidBodyData boneData) { float quaternionMagnitudeSquared = boneData.QX * boneData.QX + boneData.QY * boneData.QY + boneData.QZ * boneData.QZ + boneData.QW * boneData.QW; return IsFinite(boneData.X) && IsFinite(boneData.Y) && IsFinite(boneData.Z) && IsFinite(boneData.QX) && IsFinite(boneData.QY) && IsFinite(boneData.QZ) && IsFinite(boneData.QW) && quaternionMagnitudeSquared > 0.000001f; } /// /// private static bool TryGetCommittableBoneId(sRigidBodyData boneData, Int32 skeletonId, OptitrackSkeletonDefinition skelDef, out int boneId) { Int32 boneSkelId; NaturalPoint.NatNetLib.NativeMethods.NatNet_DecodeID( boneData.Id, out boneSkelId, out boneId ); return boneSkelId == skeletonId && skelDef.BoneIdToParentIdMap.ContainsKey(boneId) && IsBoneDataUsable(boneData); } private static bool IsFinite(float value) { return !float.IsNaN(value) && !float.IsInfinity(value); } /// /// Returns the corresponding to the provided . /// If the requested state object does not exist yet, it will initialize and return a newly-created one. /// /// Makes the assumption that the lock on is already held. /// The ID of the rigid body for which to retrieve the corresponding state. /// The existing state object, or a newly created one if necessary. private OptitrackRigidBodyState GetOrCreateRigidBodyState( Int32 rigidBodyId ) { OptitrackRigidBodyState returnedState = null; if ( m_latestRigidBodyStates.ContainsKey( rigidBodyId ) ) { returnedState = m_latestRigidBodyStates[rigidBodyId]; } else { OptitrackRigidBodyState newRbState = new OptitrackRigidBodyState { Pose = new OptitrackPose(), }; m_latestRigidBodyStates[rigidBodyId] = newRbState; returnedState = newRbState; } return returnedState; } /// /// Returns the corresponding to the provided . /// If the requested state object does not exist yet, it will initialize and return a newly-created one. /// /// Makes the assumption that the lock on is already held. /// The ID of the skeleton for which to retrieve the corresponding state. /// The existing state object, or a newly created one if necessary. private OptitrackSkeletonState GetOrCreateSkeletonState( Int32 skeletonId ) { OptitrackSkeletonState returnedState = null; if ( m_latestSkeletonStates.ContainsKey( skeletonId ) ) { returnedState = m_latestSkeletonStates[skeletonId]; } else { OptitrackSkeletonState newSkeletonState = new OptitrackSkeletonState { BonePoses = new Dictionary(), LocalBonePoses = new Dictionary(), }; m_latestSkeletonStates[skeletonId] = newSkeletonState; returnedState = newSkeletonState; } return returnedState; } private OptitrackTMarkersetState GetOrCreateTMarkersetState(Int32 tmarkersetId) { OptitrackTMarkersetState returnedState = null; if (m_latestTMarkersetStates.ContainsKey(tmarkersetId)) { returnedState = m_latestTMarkersetStates[tmarkersetId]; } else { OptitrackTMarkersetState newTMarkersetState = new OptitrackTMarkersetState { BonePoses = new Dictionary(), LocalBonePoses = new Dictionary(), }; m_latestTMarkersetStates[tmarkersetId] = newTMarkersetState; returnedState = newTMarkersetState; } return returnedState; } /// /// Returns the corresponding to the provided . /// If the requested state object does not exist yet, it will initialize and return a newly-created one. /// /// Makes the assumption that the lock on is already held. /// The ID of the bone in trained markerset for which to retrieve the corresponding state. /// The existing state object, or a newly created one if necessary. private OptitrackMarkerState GetOrCreateTMarkMarkerState(Int32 markerId) { OptitrackMarkerState returnedState = null; if (m_latestTMarkMarkerStates.ContainsKey(markerId)) { returnedState = m_latestTMarkMarkerStates[markerId]; } else { OptitrackMarkerState newMarkerState = new OptitrackMarkerState { Position = new Vector3(), }; m_latestTMarkMarkerStates[markerId] = newMarkerState; returnedState = newMarkerState; } //Debug.Log(returnedState); return returnedState; } /// /// Returns the corresponding to the provided . /// If the requested state object does not exist yet, it will initialize and return a newly-created one. /// /// Makes the assumption that the lock on is already held. /// The ID of the rigid body for which to retrieve the corresponding state. /// The existing state object, or a newly created one if necessary. private OptitrackMarkerState GetOrCreateMarkerState(Int32 markerId) { OptitrackMarkerState returnedState = null; if (m_latestMarkerStates.ContainsKey( markerId )) { returnedState = m_latestMarkerStates[markerId]; } else { OptitrackMarkerState newMarkerState = new OptitrackMarkerState { Position = new Vector3(), }; m_latestMarkerStates[markerId] = newMarkerState; returnedState = newMarkerState; } return returnedState; } /// /// [System.Obsolete("Use FillBoneSnapshot() instead of direct lock manipulation.")] internal void _EnterFrameDataUpdateLock() { Monitor.Enter( m_frameDataUpdateLock ); } /// /// [System.Obsolete("Use FillBoneSnapshot() instead of direct lock manipulation.")] internal void _ExitFrameDataUpdateLock() { Monitor.Exit( m_frameDataUpdateLock ); } #endregion Private methods }