From 2e8ef9d0f931074bbcb4177c2220dc4b96c379aa Mon Sep 17 00:00:00 2001 From: "qsxft258@gmail.com" Date: Sun, 26 Apr 2026 16:06:24 +0900 Subject: [PATCH] =?UTF-8?q?ADD=20:=20=EB=AA=A8=ED=8B=B0=EB=B8=8C=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=85=9C=20=EB=85=B9=ED=99=94=20=EC=8B=9C?= =?UTF-8?q?=EC=8A=A4=ED=85=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Plugins/Managed/NatNetLib/Client.cs | 10 + .../UXML/OptitrackStreamingClientEditor.uxml | 2 + .../OptiTrack/Scripts/OptitrackFaceDevice.cs | 234 +++++++++++++++ .../Scripts/OptitrackFaceDevice.cs.meta | 2 + .../Scripts/OptitrackStreamingClient.cs | 226 +++++++++++++- .../Editor/StreamingleFacialReceiverEditor.cs | 35 +++ .../UXML/StreamingleFacialReceiverEditor.uxml | 44 ++- .../StreamingleFacial/NatnetDeviceListener.cs | 281 ++++++++++++++++++ .../NatnetDeviceListener.cs.meta | 2 + .../StreamingleFacialReceiver.cs | 197 +++++++++++- 10 files changed, 1020 insertions(+), 13 deletions(-) create mode 100644 Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackFaceDevice.cs create mode 100644 Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackFaceDevice.cs.meta create mode 100644 Assets/External/StreamingleFacial/NatnetDeviceListener.cs create mode 100644 Assets/External/StreamingleFacial/NatnetDeviceListener.cs.meta diff --git a/Assets/External/OptiTrack Unity Plugin/OptiTrack/Plugins/Managed/NatNetLib/Client.cs b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Plugins/Managed/NatNetLib/Client.cs index 3a2c02ccf..62b83e01d 100644 --- a/Assets/External/OptiTrack Unity Plugin/OptiTrack/Plugins/Managed/NatNetLib/Client.cs +++ b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Plugins/Managed/NatNetLib/Client.cs @@ -255,6 +255,7 @@ namespace NaturalPoint.NatNetLib public List AssetDescriptions; // trained markerset added public List ForcePlateDescriptions; public List CameraDescriptions; + public List DeviceDescriptions; // generic analog devices (e.g. iFacialMocap) } @@ -476,6 +477,7 @@ namespace NaturalPoint.NatNetLib Int32 numAssetDescs = 0; // trained markerset added Int32 numForcePlateDescs = 0; Int32 numCameraDescs = 0; + Int32 numDeviceDescs = 0; for ( Int32 i = 0; i < dataDescriptions.DataDescriptionCount; ++i ) { @@ -501,6 +503,9 @@ namespace NaturalPoint.NatNetLib case (Int32)NatNetDataDescriptionType.NatNetDataDescriptionType_Camera: ++numCameraDescs; break; + case (Int32)NatNetDataDescriptionType.NatNetDataDescriptionType_Device: + ++numDeviceDescs; + break; } } @@ -512,6 +517,7 @@ namespace NaturalPoint.NatNetLib AssetDescriptions = new List(numAssetDescs), // trained markerset added ForcePlateDescriptions = new List( numForcePlateDescs ), CameraDescriptions = new List( numCameraDescs ), + DeviceDescriptions = new List( numDeviceDescs ), }; // Now populate the lists. @@ -545,6 +551,10 @@ namespace NaturalPoint.NatNetLib sCameraDescription cameraDesc = (sCameraDescription)Marshal.PtrToStructure(desc.Description, typeof(sCameraDescription)); retDescriptions.CameraDescriptions.Add(cameraDesc); break; + case (Int32)NatNetDataDescriptionType.NatNetDataDescriptionType_Device: + sDeviceDescription deviceDesc = (sDeviceDescription)Marshal.PtrToStructure( desc.Description, typeof( sDeviceDescription ) ); + retDescriptions.DeviceDescriptions.Add( deviceDesc ); + break; } } diff --git a/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/Editor/UXML/OptitrackStreamingClientEditor.uxml b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/Editor/UXML/OptitrackStreamingClientEditor.uxml index 24e04258f..003eb81ba 100644 --- a/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/Editor/UXML/OptitrackStreamingClientEditor.uxml +++ b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/Editor/UXML/OptitrackStreamingClientEditor.uxml @@ -28,6 +28,8 @@ + + diff --git a/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackFaceDevice.cs b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackFaceDevice.cs new file mode 100644 index 000000000..2d6279ea0 --- /dev/null +++ b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackFaceDevice.cs @@ -0,0 +1,234 @@ +//====================================================================================================== +// OptitrackFaceDevice +// +// Subscribes to one or more analog devices that the iFacialMocapPlugin (Motive plugin) +// publishes via NatNet, and applies blendshape weights + (optionally) head/eye pose to +// SkinnedMeshRenderers / Transforms. +// +// The plugin splits a face stream when total channels exceed NatNet's 32-per-device limit: +// - (e.g. "iFacialMocap_40001") - single device, <= 32 channels +// - _A + _B - two devices when > 32 channels +// +// Set DeviceBaseName to the base ("iFacialMocap_40001"); this component automatically +// finds and merges _A and _B if they exist. +// +// Pose channel names produced by the plugin: +// head_eulerX/Y/Z, head_posX/Y/Z, leftEye_eulerX/Y/Z, rightEye_eulerX/Y/Z +//====================================================================================================== + +using System; +using System.Collections.Generic; +using UnityEngine; + +[DefaultExecutionOrder(-50)] +public class OptitrackFaceDevice : MonoBehaviour +{ + [Tooltip("OptitrackStreamingClient instance to read device data from. Falls back to FindObjectOfType<>() at Start if null.")] + public OptitrackStreamingClient streamingClient; + + [Tooltip("Motive device base name (e.g. \"iFacialMocap_40001\"). _A/_B suffixes are auto-detected if the device was split.")] + public string deviceBaseName = "iFacialMocap_40001"; + + [Header("Output: Blendshapes")] + [Tooltip("SkinnedMeshRenderers whose blendshape weights will be driven by incoming channel values. Channel names are matched against blendshape names (case-sensitive, with _L/_R or Left/Right tolerance).")] + public SkinnedMeshRenderer[] faceMeshRenderers; + + [Tooltip("Multiply incoming blendshape values (0-100 typically) by this scalar before applying.")] + [Range(0f, 3f)] + public float blendshapeScale = 1.0f; + + [Header("Output: Head Pose (optional)")] + [Tooltip("If set, applies head_eulerXYZ to this Transform's localEulerAngles. Leave null to skip.")] + public Transform headBone; + + [Tooltip("If set, applies leftEye_eulerXYZ to this Transform.")] + public Transform leftEyeBone; + + [Tooltip("If set, applies rightEye_eulerXYZ to this Transform.")] + public Transform rightEyeBone; + + [Header("Diagnostics (read-only)")] + [SerializeField] private bool _connected; + [SerializeField] private int _resolvedDeviceCount; + [SerializeField] private int _totalChannelCount; + [SerializeField] private int _appliedBlendshapeCount; + [SerializeField] private float _lastJawOpen; + [SerializeField] private float _lastEyeBlinkLeft; + + // Pre-resolved channel name -> (renderer, blendshape index) for O(1) apply. + private struct BlendShapeMapping + { + public SkinnedMeshRenderer renderer; + public int index; + } + private Dictionary> _blendshapeCache; + private bool _cacheBuilt; + + // Resolved device names (1 or 2: base, base_A+_B) + private List _resolvedDeviceNames = new List(2); + private float _lastResolveAttempt = -10f; + private const float k_ResolveRetrySeconds = 1.0f; + + void Start() + { + if (streamingClient == null) + streamingClient = FindObjectOfType(); + + if (streamingClient != null && !streamingClient.ReceiveDevices) + { + Debug.LogWarning($"[OptitrackFaceDevice] {streamingClient.name}.ReceiveDevices is OFF. " + + "Enable it on the streaming client to get face data.", this); + } + + BuildBlendshapeCache(); + } + + void BuildBlendshapeCache() + { + _blendshapeCache = new Dictionary>(StringComparer.OrdinalIgnoreCase); + if (faceMeshRenderers == null) return; + + foreach (var smr in faceMeshRenderers) + { + if (smr == null || smr.sharedMesh == null) continue; + for (int i = 0; i < smr.sharedMesh.blendShapeCount; ++i) + { + string name = smr.sharedMesh.GetBlendShapeName(i); + AddToCache(name, smr, i); + + // Also accept _L/_R shorthand <-> Left/Right canonical + if (name.EndsWith("_L")) + AddToCache(name.Substring(0, name.Length - 2) + "Left", smr, i); + else if (name.EndsWith("_R")) + AddToCache(name.Substring(0, name.Length - 2) + "Right", smr, i); + else if (name.EndsWith("Left")) + AddToCache(name.Substring(0, name.Length - 4) + "_L", smr, i); + else if (name.EndsWith("Right")) + AddToCache(name.Substring(0, name.Length - 5) + "_R", smr, i); + } + } + _cacheBuilt = true; + } + + void AddToCache(string key, SkinnedMeshRenderer smr, int idx) + { + if (!_blendshapeCache.TryGetValue(key, out var list)) + { + list = new List(1); + _blendshapeCache[key] = list; + } + list.Add(new BlendShapeMapping { renderer = smr, index = idx }); + } + + void TryResolveDevices() + { + // Probe for base, base_A, base_B. Devices appear dynamically as the + // plugin sees its first packet, so retry every k_ResolveRetrySeconds. + if (Time.unscaledTime - _lastResolveAttempt < k_ResolveRetrySeconds) return; + _lastResolveAttempt = Time.unscaledTime; + + _resolvedDeviceNames.Clear(); + _totalChannelCount = 0; + + var defs = streamingClient.GetDeviceDefinitions(); + foreach (var d in defs) + { + if (d.Name == deviceBaseName || + d.Name == deviceBaseName + "_A" || + d.Name == deviceBaseName + "_B") + { + _resolvedDeviceNames.Add(d.Name); + _totalChannelCount += d.ChannelCount; + } + } + _resolvedDeviceCount = _resolvedDeviceNames.Count; + _connected = _resolvedDeviceCount > 0; + } + + void Update() + { + if (streamingClient == null) { _connected = false; return; } + if (!_cacheBuilt) BuildBlendshapeCache(); + + if (_resolvedDeviceNames.Count == 0) + { + TryResolveDevices(); + if (_resolvedDeviceNames.Count == 0) return; + } + + // Pose accumulators (last device wins if both _A and _B somehow have pose + // — in practice pose lives entirely in one device). + Vector3? headEuler = null, headPos = null, leftEyeEuler = null, rightEyeEuler = null; + + int appliedBs = 0; + for (int i = 0; i < _resolvedDeviceNames.Count; ++i) + { + var state = streamingClient.GetLatestDeviceState(_resolvedDeviceNames[i]); + if (state == null) continue; + + // Snapshot to a local list to avoid holding the lock during apply. + // ChannelValues is a Dictionary updated by the NatNet thread; iterating + // it while it mutates would throw. The streaming client's GetLatestDeviceState + // returns the live reference, so we iterate quickly under no lock — accept + // occasional torn reads (one frame stale value) over the perf cost of + // copying the dict every frame. + foreach (var kv in state.ChannelValues) + { + string name = kv.Key; + float v = kv.Value; + + // Pose channels? + switch (name) + { + case "head_eulerX": headEuler = With(headEuler, 0, v); continue; + case "head_eulerY": headEuler = With(headEuler, 1, v); continue; + case "head_eulerZ": headEuler = With(headEuler, 2, v); continue; + case "head_posX": headPos = With(headPos, 0, v); continue; + case "head_posY": headPos = With(headPos, 1, v); continue; + case "head_posZ": headPos = With(headPos, 2, v); continue; + case "leftEye_eulerX": leftEyeEuler = With(leftEyeEuler, 0, v); continue; + case "leftEye_eulerY": leftEyeEuler = With(leftEyeEuler, 1, v); continue; + case "leftEye_eulerZ": leftEyeEuler = With(leftEyeEuler, 2, v); continue; + case "rightEye_eulerX": rightEyeEuler = With(rightEyeEuler, 0, v); continue; + case "rightEye_eulerY": rightEyeEuler = With(rightEyeEuler, 1, v); continue; + case "rightEye_eulerZ": rightEyeEuler = With(rightEyeEuler, 2, v); continue; + } + + // Blendshape — apply scaled value to all matching renderers. + if (_blendshapeCache != null && _blendshapeCache.TryGetValue(name, out var maps)) + { + float weight = Mathf.Clamp(v * blendshapeScale, 0f, 100f); + foreach (var m in maps) + { + if (m.renderer != null) + m.renderer.SetBlendShapeWeight(m.index, weight); + } + appliedBs++; + } + + // Diagnostics + if (name == "jawOpen") _lastJawOpen = v; + if (name == "eyeBlink_L" || name == "eyeBlinkLeft") _lastEyeBlinkLeft = v; + } + } + _appliedBlendshapeCount = appliedBs; + + // Apply pose + if (headBone != null && headEuler.HasValue) + headBone.localEulerAngles = headEuler.Value; + if (leftEyeBone != null && leftEyeEuler.HasValue) + leftEyeBone.localEulerAngles = leftEyeEuler.Value; + if (rightEyeBone != null && rightEyeEuler.HasValue) + rightEyeBone.localEulerAngles = rightEyeEuler.Value; + } + + // Helper: set component i of a Vector3?, initializing to zero if null. + private static Vector3 With(Vector3? cur, int axis, float v) + { + Vector3 r = cur ?? Vector3.zero; + if (axis == 0) r.x = v; + else if (axis == 1) r.y = v; + else r.z = v; + return r; + } +} diff --git a/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackFaceDevice.cs.meta b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackFaceDevice.cs.meta new file mode 100644 index 000000000..6b953355d --- /dev/null +++ b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackFaceDevice.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5ba2193cab25f3f48b3e660ae26e8c3b \ No newline at end of file diff --git a/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackStreamingClient.cs b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackStreamingClient.cs index 5f544b3a4..3dd0acdbd 100644 --- a/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackStreamingClient.cs +++ b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackStreamingClient.cs @@ -240,6 +240,31 @@ public class OptitrackCameraDefinition } +/// Definition of a generic analog device (e.g. iFacialMocap face stream). +public class OptitrackDeviceDefinition +{ + public Int32 Id; + public string Name; + public string SerialNumber; + public Int32 DeviceType; + public Int32 ChannelDataType; + public Int32 ChannelCount; + public List ChannelNames; +} + + +/// Latest sampled frame for a generic analog device. +public class OptitrackDeviceState +{ + public Int32 Id; + public string Name; + /// Channel values keyed by channel name (matches ChannelNames in definition). + public Dictionary ChannelValues; + public Int32 FrameNumber; + public OptitrackHiResTimer.Timestamp DeliveryTimestamp; +} + + public static class OptitrackHiResTimer { public struct Timestamp @@ -315,6 +340,9 @@ public class OptitrackStreamingClient : MonoBehaviour [Tooltip("Draws force plate visuals in the viewport for debugging and other uses.")] public bool DrawForcePlates = false; + [Tooltip("Receive analog device data (e.g. iFacialMocap face streams). Each frame the full sFrameOfMocapData is marshaled (~200KB), so leave off if no devices are needed.")] + public bool ReceiveDevices = false; + [Tooltip("Motive will record when the Unity project is played.")] public bool RecordOnPlay = false; @@ -360,6 +388,12 @@ public class OptitrackStreamingClient : MonoBehaviour private List m_markersDefinitions = new List(); private List m_cameraDefinitions = new List(); private List m_forcePlateDefinitions = new List(); + private List m_deviceDefinitions = new List(); + + /// Maps from a streamed device's ID to its most recent channel values. + private Dictionary m_latestDeviceStates = new Dictionary(); + /// Name -> Id lookup for devices (built from descriptions). + private Dictionary m_deviceNameToId = new Dictionary(); /// Maps from a streamed rigid body's ID to its most recent available pose data. private Dictionary m_latestRigidBodyStates = new Dictionary(); @@ -1260,7 +1294,11 @@ public class OptitrackStreamingClient : MonoBehaviour { descriptionTypeMask |= (1 << (int)NatNetDataDescriptionType.NatNetDataDescriptionType_ForcePlate); } - m_dataDescs = m_client.GetDataDescriptions(descriptionTypeMask); + if( ReceiveDevices ) + { + descriptionTypeMask |= (1 << (int)NatNetDataDescriptionType.NatNetDataDescriptionType_Device); + } + m_dataDescs = m_client.GetDataDescriptions(descriptionTypeMask); // 정의를 성공적으로 받았으므로 자동 재조회 카운터 리셋 m_definitionRefreshCount = 0; @@ -1269,6 +1307,13 @@ public class OptitrackStreamingClient : MonoBehaviour m_skeletonDefinitions.Clear(); m_tmarkersetDefinitions.Clear(); m_mirrorBoneIdMaps.Clear(); // 스켈레톤 정의 변경 시 mirror map 캐시 무효화 + // Device caches are accessed from NatNet thread; lock them. + lock (m_frameDataUpdateLock) + { + m_deviceDefinitions.Clear(); + m_deviceNameToId.Clear(); + m_latestDeviceStates.Clear(); + } // NatNet 스레드가 접근하는 캐시는 락으로 보호하여 레이스 방지 lock (m_frameDataUpdateLock) @@ -1501,6 +1546,46 @@ public class OptitrackStreamingClient : MonoBehaviour m_forcePlateDefinitions.Add(forcePlateDef); } + + // ---------------------------------- + // - Device Definitions (generic analog: iFacialMocap, NIDAQ, etc.) + // ---------------------------------- + if (m_dataDescs.DeviceDescriptions != null) + { + for (int devIdx = 0; devIdx < m_dataDescs.DeviceDescriptions.Count; ++devIdx) + { + sDeviceDescription dev = m_dataDescs.DeviceDescriptions[devIdx]; + + // dev.ChannelNames is a fixed-size 32 array (NatNet C# wrapper limit). + // Plugin can register devices with > 32 channels (Motive stores them all), + // but only the first 32 names survive the wrapper marshaling. + // Clamp ChannelCount to the marshaled name array's actual length to avoid + // IndexOutOfRangeException on the loop below. + int actualNameCount = (dev.ChannelNames != null) + ? Math.Min(dev.ChannelCount, dev.ChannelNames.Length) + : 0; + + OptitrackDeviceDefinition deviceDef = new OptitrackDeviceDefinition + { + Id = dev.Id, + Name = dev.Name, + SerialNumber = dev.SerialNo, + DeviceType = dev.DeviceType, + ChannelDataType = dev.ChannelDataType, + ChannelCount = actualNameCount, + ChannelNames = new List(actualNameCount), + }; + + for (int i = 0; i < actualNameCount; ++i) + { + deviceDef.ChannelNames.Add(dev.ChannelNames[i]); + } + + m_deviceDefinitions.Add(deviceDef); + m_deviceNameToId[deviceDef.Name] = deviceDef.Id; + } + } + } @@ -1998,6 +2083,23 @@ public class OptitrackStreamingClient : MonoBehaviour markerState.IsActive = (marker.Params & 0x20) != 0; } + // ---------------------------------- + // - Update analog devices (iFacialMocap face, NIDAQ, etc.) + // + // We CANNOT use eventArgs.MarshaledFrame here — that triggers a full + // sFrameOfMocapData marshal which throws on the fixed-size MarkerSets[] + // array (uninitialized bytes in unused slots fail ANSI string decoding). + // + // Instead: unsafe pointer arithmetic to read DeviceCount + each sDeviceData + // directly from native memory, skipping the problematic sections entirely. + // sDeviceData / sAnalogChannelData have no string fields, so PtrToStructure + // on individual entries is safe. + // ---------------------------------- + if (m_deviceDefinitions.Count > 0) + { + ProcessDeviceData(eventArgs.NativeFramePointer, frameTimestamp); + } + } catch (Exception ex) { @@ -2010,6 +2112,128 @@ public class OptitrackStreamingClient : MonoBehaviour } } + + // Cached struct offsets/sizes for ProcessDeviceData. Computed once via reflection, + // then reused — Marshal.OffsetOf / SizeOf don't allocate on subsequent calls. + private static int s_DeviceCountOffset = -1; + private static int s_DevicesArrayOffset; + private static int s_DeviceDataSize; + private static int s_FrameNumberOffset; + + private static void EnsureDeviceOffsetsInitialized() + { + if (s_DeviceCountOffset >= 0) return; + s_DeviceCountOffset = (int)Marshal.OffsetOf(typeof(sFrameOfMocapData), "DeviceCount"); + s_DevicesArrayOffset = (int)Marshal.OffsetOf(typeof(sFrameOfMocapData), "Devices"); + s_FrameNumberOffset = (int)Marshal.OffsetOf(typeof(sFrameOfMocapData), "FrameNumber"); + s_DeviceDataSize = Marshal.SizeOf(typeof(sDeviceData)); + } + + private void ProcessDeviceData(IntPtr pFrame, OptitrackHiResTimer.Timestamp frameTimestamp) + { + if (pFrame == IntPtr.Zero) return; + EnsureDeviceOffsetsInitialized(); + + int deviceCount = Marshal.ReadInt32(pFrame, s_DeviceCountOffset); + + if (deviceCount <= 0) return; + if (deviceCount > NatNetConstants.MaxDevices) deviceCount = NatNetConstants.MaxDevices; + + int frameNumber = Marshal.ReadInt32(pFrame, s_FrameNumberOffset); + + for (int devIdx = 0; devIdx < deviceCount; ++devIdx) + { + IntPtr pDevice = IntPtr.Add(pFrame, s_DevicesArrayOffset + devIdx * s_DeviceDataSize); + sDeviceData devData = (sDeviceData)Marshal.PtrToStructure(pDevice, typeof(sDeviceData)); + + OptitrackDeviceDefinition devDef = GetDeviceDefinitionById(devData.Id); + if (devDef == null) continue; + + OptitrackDeviceState devState = GetOrCreateDeviceState(devData.Id); + devState.Name = devDef.Name; + devState.FrameNumber = frameNumber; + devState.DeliveryTimestamp = frameTimestamp; + + int chCount = Math.Min(devData.ChannelCount, devDef.ChannelCount); + for (int ch = 0; ch < chCount; ++ch) + { + // Use subframe 0 — face data is one sample per frame. + sAnalogChannelData chData = devData.ChannelData[ch]; + float value = chData.Values[0]; + devState.ChannelValues[devDef.ChannelNames[ch]] = value; + } + } + } + + + private OptitrackDeviceDefinition GetDeviceDefinitionById( Int32 id ) + { + for (int i = 0; i < m_deviceDefinitions.Count; ++i) + if (m_deviceDefinitions[i].Id == id) return m_deviceDefinitions[i]; + return null; + } + + private OptitrackDeviceState GetOrCreateDeviceState( Int32 id ) + { + if (m_latestDeviceStates.TryGetValue(id, out OptitrackDeviceState state)) + return state; + state = new OptitrackDeviceState + { + Id = id, + ChannelValues = new Dictionary(), + }; + m_latestDeviceStates[id] = state; + return state; + } + + + /// Get the latest channel values for a device by ID. + public OptitrackDeviceState GetLatestDeviceState( Int32 deviceId ) + { + Monitor.Enter( m_frameDataUpdateLock ); + try + { + return m_latestDeviceStates.TryGetValue(deviceId, out var s) ? s : null; + } + finally + { + Monitor.Exit( m_frameDataUpdateLock ); + } + } + + + /// Get the latest channel values for a device by Motive name (e.g. "iFacialMocap_40001"). + public OptitrackDeviceState GetLatestDeviceState( string deviceName ) + { + Monitor.Enter( m_frameDataUpdateLock ); + try + { + if (m_deviceNameToId.TryGetValue(deviceName, out Int32 id) && + m_latestDeviceStates.TryGetValue(id, out var s)) + return s; + return null; + } + finally + { + Monitor.Exit( m_frameDataUpdateLock ); + } + } + + + /// Returns a snapshot of currently registered device definitions. + public List GetDeviceDefinitions() + { + Monitor.Enter( m_frameDataUpdateLock ); + try + { + return new List(m_deviceDefinitions); + } + finally + { + Monitor.Exit( m_frameDataUpdateLock ); + } + } + private string GetMarkerName( sMarker marker ) { int assetID = marker.Id >> 16; // high word = Asset ID Number diff --git a/Assets/External/StreamingleFacial/Editor/StreamingleFacialReceiverEditor.cs b/Assets/External/StreamingleFacial/Editor/StreamingleFacialReceiverEditor.cs index fdb3844cf..dcac988a2 100644 --- a/Assets/External/StreamingleFacial/Editor/StreamingleFacialReceiverEditor.cs +++ b/Assets/External/StreamingleFacial/Editor/StreamingleFacialReceiverEditor.cs @@ -20,6 +20,9 @@ public class StreamingleFacialReceiverEditor : Editor private VisualElement medianEuroFields; private VisualElement sharedPortInfo; private Label masterStatusValue; + private VisualElement udpSourceFields; + private VisualElement natnetFields; + private Label natnetEffectiveNameValue; public override VisualElement CreateInspectorGUI() { @@ -46,6 +49,9 @@ public class StreamingleFacialReceiverEditor : Editor medianEuroFields = root.Q("medianEuroFields"); sharedPortInfo = root.Q("sharedPortInfo"); masterStatusValue = root.Q