ADD : 모티브 페이셜 녹화 시스템 추가
This commit is contained in:
parent
fb242ccb9a
commit
2e8ef9d0f9
@ -255,6 +255,7 @@ namespace NaturalPoint.NatNetLib
|
|||||||
public List<sAssetDescription> AssetDescriptions; // trained markerset added
|
public List<sAssetDescription> AssetDescriptions; // trained markerset added
|
||||||
public List<sForcePlateDescription> ForcePlateDescriptions;
|
public List<sForcePlateDescription> ForcePlateDescriptions;
|
||||||
public List<sCameraDescription> CameraDescriptions;
|
public List<sCameraDescription> CameraDescriptions;
|
||||||
|
public List<sDeviceDescription> DeviceDescriptions; // generic analog devices (e.g. iFacialMocap)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -476,6 +477,7 @@ namespace NaturalPoint.NatNetLib
|
|||||||
Int32 numAssetDescs = 0; // trained markerset added
|
Int32 numAssetDescs = 0; // trained markerset added
|
||||||
Int32 numForcePlateDescs = 0;
|
Int32 numForcePlateDescs = 0;
|
||||||
Int32 numCameraDescs = 0;
|
Int32 numCameraDescs = 0;
|
||||||
|
Int32 numDeviceDescs = 0;
|
||||||
|
|
||||||
for ( Int32 i = 0; i < dataDescriptions.DataDescriptionCount; ++i )
|
for ( Int32 i = 0; i < dataDescriptions.DataDescriptionCount; ++i )
|
||||||
{
|
{
|
||||||
@ -501,6 +503,9 @@ namespace NaturalPoint.NatNetLib
|
|||||||
case (Int32)NatNetDataDescriptionType.NatNetDataDescriptionType_Camera:
|
case (Int32)NatNetDataDescriptionType.NatNetDataDescriptionType_Camera:
|
||||||
++numCameraDescs;
|
++numCameraDescs;
|
||||||
break;
|
break;
|
||||||
|
case (Int32)NatNetDataDescriptionType.NatNetDataDescriptionType_Device:
|
||||||
|
++numDeviceDescs;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -512,6 +517,7 @@ namespace NaturalPoint.NatNetLib
|
|||||||
AssetDescriptions = new List<sAssetDescription>(numAssetDescs), // trained markerset added
|
AssetDescriptions = new List<sAssetDescription>(numAssetDescs), // trained markerset added
|
||||||
ForcePlateDescriptions = new List<sForcePlateDescription>( numForcePlateDescs ),
|
ForcePlateDescriptions = new List<sForcePlateDescription>( numForcePlateDescs ),
|
||||||
CameraDescriptions = new List<sCameraDescription>( numCameraDescs ),
|
CameraDescriptions = new List<sCameraDescription>( numCameraDescs ),
|
||||||
|
DeviceDescriptions = new List<sDeviceDescription>( numDeviceDescs ),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Now populate the lists.
|
// Now populate the lists.
|
||||||
@ -545,6 +551,10 @@ namespace NaturalPoint.NatNetLib
|
|||||||
sCameraDescription cameraDesc = (sCameraDescription)Marshal.PtrToStructure(desc.Description, typeof(sCameraDescription));
|
sCameraDescription cameraDesc = (sCameraDescription)Marshal.PtrToStructure(desc.Description, typeof(sCameraDescription));
|
||||||
retDescriptions.CameraDescriptions.Add(cameraDesc);
|
retDescriptions.CameraDescriptions.Add(cameraDesc);
|
||||||
break;
|
break;
|
||||||
|
case (Int32)NatNetDataDescriptionType.NatNetDataDescriptionType_Device:
|
||||||
|
sDeviceDescription deviceDesc = (sDeviceDescription)Marshal.PtrToStructure( desc.Description, typeof( sDeviceDescription ) );
|
||||||
|
retDescriptions.DeviceDescriptions.Add( deviceDesc );
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -28,6 +28,8 @@
|
|||||||
<uie:PropertyField binding-path="DrawTMarkersetMarkers" label="Draw TMarkerset Markers"/>
|
<uie:PropertyField binding-path="DrawTMarkersetMarkers" label="Draw TMarkerset Markers"/>
|
||||||
<uie:PropertyField binding-path="DrawCameras" label="Draw Cameras"/>
|
<uie:PropertyField binding-path="DrawCameras" label="Draw Cameras"/>
|
||||||
<uie:PropertyField binding-path="DrawForcePlates" label="Draw Force Plates"/>
|
<uie:PropertyField binding-path="DrawForcePlates" label="Draw Force Plates"/>
|
||||||
|
<uie:PropertyField binding-path="ReceiveDevices" label="Receive Devices"/>
|
||||||
|
<ui:HelpBox message-type="Info" text="iFacialMocap 등 analog device 데이터 수신. 매 프레임 sFrameOfMocapData 마샬링 발생(~200KB) — 페이셜 안 쓰면 OFF."/>
|
||||||
<uie:PropertyField binding-path="RecordOnPlay" label="Record On Play"/>
|
<uie:PropertyField binding-path="RecordOnPlay" label="Record On Play"/>
|
||||||
<uie:PropertyField binding-path="SkipDataDescriptions" label="Skip Data Descriptions"/>
|
<uie:PropertyField binding-path="SkipDataDescriptions" label="Skip Data Descriptions"/>
|
||||||
<uie:PropertyField binding-path="AutoReconnect" label="Auto Reconnect"/>
|
<uie:PropertyField binding-path="AutoReconnect" label="Auto Reconnect"/>
|
||||||
|
|||||||
234
Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackFaceDevice.cs
vendored
Normal file
234
Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackFaceDevice.cs
vendored
Normal file
@ -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:
|
||||||
|
// - <baseName> (e.g. "iFacialMocap_40001") - single device, <= 32 channels
|
||||||
|
// - <baseName>_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<string, List<BlendShapeMapping>> _blendshapeCache;
|
||||||
|
private bool _cacheBuilt;
|
||||||
|
|
||||||
|
// Resolved device names (1 or 2: base, base_A+_B)
|
||||||
|
private List<string> _resolvedDeviceNames = new List<string>(2);
|
||||||
|
private float _lastResolveAttempt = -10f;
|
||||||
|
private const float k_ResolveRetrySeconds = 1.0f;
|
||||||
|
|
||||||
|
void Start()
|
||||||
|
{
|
||||||
|
if (streamingClient == null)
|
||||||
|
streamingClient = FindObjectOfType<OptitrackStreamingClient>();
|
||||||
|
|
||||||
|
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<string, List<BlendShapeMapping>>(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<BlendShapeMapping>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackFaceDevice.cs.meta
vendored
Normal file
2
Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackFaceDevice.cs.meta
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5ba2193cab25f3f48b3e660ae26e8c3b
|
||||||
@ -240,6 +240,31 @@ public class OptitrackCameraDefinition
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>Definition of a generic analog device (e.g. iFacialMocap face stream).</summary>
|
||||||
|
public class OptitrackDeviceDefinition
|
||||||
|
{
|
||||||
|
public Int32 Id;
|
||||||
|
public string Name;
|
||||||
|
public string SerialNumber;
|
||||||
|
public Int32 DeviceType;
|
||||||
|
public Int32 ChannelDataType;
|
||||||
|
public Int32 ChannelCount;
|
||||||
|
public List<string> ChannelNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>Latest sampled frame for a generic analog device.</summary>
|
||||||
|
public class OptitrackDeviceState
|
||||||
|
{
|
||||||
|
public Int32 Id;
|
||||||
|
public string Name;
|
||||||
|
/// <summary>Channel values keyed by channel name (matches ChannelNames in definition).</summary>
|
||||||
|
public Dictionary<string, float> ChannelValues;
|
||||||
|
public Int32 FrameNumber;
|
||||||
|
public OptitrackHiResTimer.Timestamp DeliveryTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public static class OptitrackHiResTimer
|
public static class OptitrackHiResTimer
|
||||||
{
|
{
|
||||||
public struct Timestamp
|
public struct Timestamp
|
||||||
@ -315,6 +340,9 @@ public class OptitrackStreamingClient : MonoBehaviour
|
|||||||
[Tooltip("Draws force plate visuals in the viewport for debugging and other uses.")]
|
[Tooltip("Draws force plate visuals in the viewport for debugging and other uses.")]
|
||||||
public bool DrawForcePlates = false;
|
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.")]
|
[Tooltip("Motive will record when the Unity project is played.")]
|
||||||
public bool RecordOnPlay = false;
|
public bool RecordOnPlay = false;
|
||||||
|
|
||||||
@ -360,6 +388,12 @@ public class OptitrackStreamingClient : MonoBehaviour
|
|||||||
private List<OptitrackMarkersDefinition> m_markersDefinitions = new List<OptitrackMarkersDefinition>();
|
private List<OptitrackMarkersDefinition> m_markersDefinitions = new List<OptitrackMarkersDefinition>();
|
||||||
private List<OptitrackCameraDefinition> m_cameraDefinitions = new List<OptitrackCameraDefinition>();
|
private List<OptitrackCameraDefinition> m_cameraDefinitions = new List<OptitrackCameraDefinition>();
|
||||||
private List<OptitrackForcePlateDefinition> m_forcePlateDefinitions = new List<OptitrackForcePlateDefinition>();
|
private List<OptitrackForcePlateDefinition> m_forcePlateDefinitions = new List<OptitrackForcePlateDefinition>();
|
||||||
|
private List<OptitrackDeviceDefinition> m_deviceDefinitions = new List<OptitrackDeviceDefinition>();
|
||||||
|
|
||||||
|
/// <summary>Maps from a streamed device's ID to its most recent channel values.</summary>
|
||||||
|
private Dictionary<Int32, OptitrackDeviceState> m_latestDeviceStates = new Dictionary<Int32, OptitrackDeviceState>();
|
||||||
|
/// <summary>Name -> Id lookup for devices (built from descriptions).</summary>
|
||||||
|
private Dictionary<string, Int32> m_deviceNameToId = new Dictionary<string, Int32>();
|
||||||
|
|
||||||
/// <summary>Maps from a streamed rigid body's ID to its most recent available pose data.</summary>
|
/// <summary>Maps from a streamed rigid body's ID to its most recent available pose data.</summary>
|
||||||
private Dictionary<Int32, OptitrackRigidBodyState> m_latestRigidBodyStates = new Dictionary<Int32, OptitrackRigidBodyState>();
|
private Dictionary<Int32, OptitrackRigidBodyState> m_latestRigidBodyStates = new Dictionary<Int32, OptitrackRigidBodyState>();
|
||||||
@ -1260,6 +1294,10 @@ public class OptitrackStreamingClient : MonoBehaviour
|
|||||||
{
|
{
|
||||||
descriptionTypeMask |= (1 << (int)NatNetDataDescriptionType.NatNetDataDescriptionType_ForcePlate);
|
descriptionTypeMask |= (1 << (int)NatNetDataDescriptionType.NatNetDataDescriptionType_ForcePlate);
|
||||||
}
|
}
|
||||||
|
if( ReceiveDevices )
|
||||||
|
{
|
||||||
|
descriptionTypeMask |= (1 << (int)NatNetDataDescriptionType.NatNetDataDescriptionType_Device);
|
||||||
|
}
|
||||||
m_dataDescs = m_client.GetDataDescriptions(descriptionTypeMask);
|
m_dataDescs = m_client.GetDataDescriptions(descriptionTypeMask);
|
||||||
|
|
||||||
// 정의를 성공적으로 받았으므로 자동 재조회 카운터 리셋
|
// 정의를 성공적으로 받았으므로 자동 재조회 카운터 리셋
|
||||||
@ -1269,6 +1307,13 @@ public class OptitrackStreamingClient : MonoBehaviour
|
|||||||
m_skeletonDefinitions.Clear();
|
m_skeletonDefinitions.Clear();
|
||||||
m_tmarkersetDefinitions.Clear();
|
m_tmarkersetDefinitions.Clear();
|
||||||
m_mirrorBoneIdMaps.Clear(); // 스켈레톤 정의 변경 시 mirror map 캐시 무효화
|
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 스레드가 접근하는 캐시는 락으로 보호하여 레이스 방지
|
// NatNet 스레드가 접근하는 캐시는 락으로 보호하여 레이스 방지
|
||||||
lock (m_frameDataUpdateLock)
|
lock (m_frameDataUpdateLock)
|
||||||
@ -1501,6 +1546,46 @@ public class OptitrackStreamingClient : MonoBehaviour
|
|||||||
m_forcePlateDefinitions.Add(forcePlateDef);
|
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<string>(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;
|
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)
|
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<string, float>(),
|
||||||
|
};
|
||||||
|
m_latestDeviceStates[id] = state;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>Get the latest channel values for a device by ID.</summary>
|
||||||
|
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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>Get the latest channel values for a device by Motive name (e.g. "iFacialMocap_40001").</summary>
|
||||||
|
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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>Returns a snapshot of currently registered device definitions.</summary>
|
||||||
|
public List<OptitrackDeviceDefinition> GetDeviceDefinitions()
|
||||||
|
{
|
||||||
|
Monitor.Enter( m_frameDataUpdateLock );
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return new List<OptitrackDeviceDefinition>(m_deviceDefinitions);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Monitor.Exit( m_frameDataUpdateLock );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private string GetMarkerName( sMarker marker )
|
private string GetMarkerName( sMarker marker )
|
||||||
{
|
{
|
||||||
int assetID = marker.Id >> 16; // high word = Asset ID Number
|
int assetID = marker.Id >> 16; // high word = Asset ID Number
|
||||||
|
|||||||
@ -20,6 +20,9 @@ public class StreamingleFacialReceiverEditor : Editor
|
|||||||
private VisualElement medianEuroFields;
|
private VisualElement medianEuroFields;
|
||||||
private VisualElement sharedPortInfo;
|
private VisualElement sharedPortInfo;
|
||||||
private Label masterStatusValue;
|
private Label masterStatusValue;
|
||||||
|
private VisualElement udpSourceFields;
|
||||||
|
private VisualElement natnetFields;
|
||||||
|
private Label natnetEffectiveNameValue;
|
||||||
|
|
||||||
public override VisualElement CreateInspectorGUI()
|
public override VisualElement CreateInspectorGUI()
|
||||||
{
|
{
|
||||||
@ -46,6 +49,9 @@ public class StreamingleFacialReceiverEditor : Editor
|
|||||||
medianEuroFields = root.Q("medianEuroFields");
|
medianEuroFields = root.Q("medianEuroFields");
|
||||||
sharedPortInfo = root.Q("sharedPortInfo");
|
sharedPortInfo = root.Q("sharedPortInfo");
|
||||||
masterStatusValue = root.Q<Label>("masterStatusValue");
|
masterStatusValue = root.Q<Label>("masterStatusValue");
|
||||||
|
udpSourceFields = root.Q("udpSourceFields");
|
||||||
|
natnetFields = root.Q("natnetFields");
|
||||||
|
natnetEffectiveNameValue = root.Q<Label>("natnetEffectiveNameValue");
|
||||||
|
|
||||||
// Auto-find button
|
// Auto-find button
|
||||||
var autoFindBtn = root.Q<Button>("autoFindBtn");
|
var autoFindBtn = root.Q<Button>("autoFindBtn");
|
||||||
@ -74,6 +80,16 @@ public class StreamingleFacialReceiverEditor : Editor
|
|||||||
UpdateSharedPortVisibility(prop.boolValue);
|
UpdateSharedPortVisibility(prop.boolValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Track sourceMode: toggle UDP vs NatNet field sets.
|
||||||
|
// FacialSourceMode: 0=DirectUDP, 1=NatNetDevice
|
||||||
|
var sourceModeProp = serializedObject.FindProperty("sourceMode");
|
||||||
|
UpdateSourceModeVisibility(sourceModeProp.enumValueIndex);
|
||||||
|
|
||||||
|
root.TrackPropertyValue(sourceModeProp, prop =>
|
||||||
|
{
|
||||||
|
UpdateSourceModeVisibility(prop.enumValueIndex);
|
||||||
|
});
|
||||||
|
|
||||||
// Track availablePorts and activePortIndex changes to rebuild port buttons
|
// Track availablePorts and activePortIndex changes to rebuild port buttons
|
||||||
var portsProp = serializedObject.FindProperty("availablePorts");
|
var portsProp = serializedObject.FindProperty("availablePorts");
|
||||||
root.TrackPropertyValue(portsProp, _ => RebuildPortButtons());
|
root.TrackPropertyValue(portsProp, _ => RebuildPortButtons());
|
||||||
@ -81,6 +97,11 @@ public class StreamingleFacialReceiverEditor : Editor
|
|||||||
var activeIndexProp = serializedObject.FindProperty("activePortIndex");
|
var activeIndexProp = serializedObject.FindProperty("activePortIndex");
|
||||||
root.TrackPropertyValue(activeIndexProp, _ => RebuildPortButtons());
|
root.TrackPropertyValue(activeIndexProp, _ => RebuildPortButtons());
|
||||||
|
|
||||||
|
// NatNet 모드의 effective name = prefix + active port. prefix 변경 시 라벨 갱신.
|
||||||
|
var prefixProp = serializedObject.FindProperty("natnetDeviceNamePrefix");
|
||||||
|
if (prefixProp != null)
|
||||||
|
root.TrackPropertyValue(prefixProp, _ => RebuildPortButtons());
|
||||||
|
|
||||||
// Play mode status polling
|
// Play mode status polling
|
||||||
root.schedule.Execute(UpdatePlayModeState).Every(200);
|
root.schedule.Execute(UpdatePlayModeState).Every(200);
|
||||||
|
|
||||||
@ -104,6 +125,16 @@ public class StreamingleFacialReceiverEditor : Editor
|
|||||||
sharedPortInfo.style.display = useShared ? DisplayStyle.Flex : DisplayStyle.None;
|
sharedPortInfo.style.display = useShared ? DisplayStyle.Flex : DisplayStyle.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UpdateSourceModeVisibility(int modeIndex)
|
||||||
|
{
|
||||||
|
// FacialSourceMode: 0=DirectUDP, 1=NatNetDevice
|
||||||
|
bool isDirectUdp = (modeIndex == 0);
|
||||||
|
if (udpSourceFields != null)
|
||||||
|
udpSourceFields.style.display = isDirectUdp ? DisplayStyle.Flex : DisplayStyle.None;
|
||||||
|
if (natnetFields != null)
|
||||||
|
natnetFields.style.display = isDirectUdp ? DisplayStyle.None : DisplayStyle.Flex;
|
||||||
|
}
|
||||||
|
|
||||||
private void RebuildPortButtons()
|
private void RebuildPortButtons()
|
||||||
{
|
{
|
||||||
if (portButtonsContainer == null || receiver == null) return;
|
if (portButtonsContainer == null || receiver == null) return;
|
||||||
@ -113,6 +144,10 @@ public class StreamingleFacialReceiverEditor : Editor
|
|||||||
if (activePortValue != null)
|
if (activePortValue != null)
|
||||||
activePortValue.text = receiver.LOCAL_PORT.ToString();
|
activePortValue.text = receiver.LOCAL_PORT.ToString();
|
||||||
|
|
||||||
|
// Update NatNet effective name label (prefix + active port)
|
||||||
|
if (natnetEffectiveNameValue != null)
|
||||||
|
natnetEffectiveNameValue.text = receiver.EffectiveNatnetDeviceBaseName;
|
||||||
|
|
||||||
if (receiver.availablePorts == null || receiver.availablePorts.Length == 0) return;
|
if (receiver.availablePorts == null || receiver.availablePorts.Length == 0) return;
|
||||||
|
|
||||||
for (int i = 0; i < receiver.availablePorts.Length; i++)
|
for (int i = 0; i < receiver.availablePorts.Length; i++)
|
||||||
|
|||||||
@ -21,21 +21,26 @@
|
|||||||
</ui:Foldout>
|
</ui:Foldout>
|
||||||
</ui:VisualElement>
|
</ui:VisualElement>
|
||||||
|
|
||||||
<!-- Shared Port (Master Mode) -->
|
<!-- Source Mode -->
|
||||||
<ui:VisualElement class="section">
|
<ui:VisualElement class="section">
|
||||||
<ui:Foldout text="Shared Port (Master Mode)" value="true" class="section-foldout">
|
<ui:Foldout text="Source Mode" value="true" class="section-foldout">
|
||||||
<uie:PropertyField binding-path="useSharedPort" label="Use Shared Port"/>
|
<uie:PropertyField binding-path="sourceMode" label="Source Mode"/>
|
||||||
<ui:VisualElement name="sharedPortInfo" class="facial-shared-port-info">
|
<ui:Label text="DirectUDP = iFacialMocap UDP 직접 수신 NatNetDevice = Motive 거쳐 NatNet으로 수신 (시간동기 + .tak 녹화 통합)" class="facial-info-text"/>
|
||||||
<ui:Label text="같은 포트에 여러 Receiver를 연결하여 동일한 모캡 데이터를 공유 수신합니다." class="facial-info-text"/>
|
|
||||||
</ui:VisualElement>
|
<!-- NatNet 모드 전용 필드 -->
|
||||||
<ui:VisualElement name="masterStatusRow" class="facial-master-status-row">
|
<ui:VisualElement name="natnetFields">
|
||||||
<ui:Label text="Master Status" class="facial-port-label"/>
|
<uie:PropertyField binding-path="natnetStreamingClient" label="OptiTrack Streaming Client"/>
|
||||||
<ui:Label name="masterStatusValue" text="---" class="facial-master-status-value"/>
|
<uie:PropertyField binding-path="natnetDeviceNamePrefix" label="Device Name Prefix"/>
|
||||||
|
<ui:VisualElement name="natnetEffectiveNameRow" class="facial-active-port-row">
|
||||||
|
<ui:Label text="Effective Base Name" class="facial-port-label"/>
|
||||||
|
<ui:Label name="natnetEffectiveNameValue" text="---" class="facial-port-value"/>
|
||||||
|
</ui:VisualElement>
|
||||||
|
<ui:Label text="비워두면 씬에서 자동 검색. 최종 디바이스 이름 = Prefix + 활성 포트. _A/_B 분할 자동 감지." class="facial-info-text"/>
|
||||||
</ui:VisualElement>
|
</ui:VisualElement>
|
||||||
</ui:Foldout>
|
</ui:Foldout>
|
||||||
</ui:VisualElement>
|
</ui:VisualElement>
|
||||||
|
|
||||||
<!-- Port Hot-Swap (port buttons built dynamically in C#) -->
|
<!-- Port Hot-Swap (두 모드 공통: DirectUDP 바인드 포트 / NatNet 디바이스 포트 접미사) -->
|
||||||
<ui:VisualElement class="section">
|
<ui:VisualElement class="section">
|
||||||
<ui:Foldout text="Port Hot-Swap" value="true" class="section-foldout">
|
<ui:Foldout text="Port Hot-Swap" value="true" class="section-foldout">
|
||||||
<ui:VisualElement name="activePortRow" class="facial-active-port-row">
|
<ui:VisualElement name="activePortRow" class="facial-active-port-row">
|
||||||
@ -47,6 +52,25 @@
|
|||||||
</ui:Foldout>
|
</ui:Foldout>
|
||||||
</ui:VisualElement>
|
</ui:VisualElement>
|
||||||
|
|
||||||
|
<!-- UDP 전용 섹션들 (DirectUDP 모드일 때만 노출) -->
|
||||||
|
<ui:VisualElement name="udpSourceFields">
|
||||||
|
|
||||||
|
<!-- Shared Port (Master Mode) -->
|
||||||
|
<ui:VisualElement class="section">
|
||||||
|
<ui:Foldout text="Shared Port (Master Mode)" value="true" class="section-foldout">
|
||||||
|
<uie:PropertyField binding-path="useSharedPort" label="Use Shared Port"/>
|
||||||
|
<ui:VisualElement name="sharedPortInfo" class="facial-shared-port-info">
|
||||||
|
<ui:Label text="같은 포트에 여러 Receiver를 연결하여 동일한 모캡 데이터를 공유 수신합니다." class="facial-info-text"/>
|
||||||
|
</ui:VisualElement>
|
||||||
|
<ui:VisualElement name="masterStatusRow" class="facial-master-status-row">
|
||||||
|
<ui:Label text="Master Status" class="facial-port-label"/>
|
||||||
|
<ui:Label name="masterStatusValue" text="---" class="facial-master-status-value"/>
|
||||||
|
</ui:VisualElement>
|
||||||
|
</ui:Foldout>
|
||||||
|
</ui:VisualElement>
|
||||||
|
|
||||||
|
</ui:VisualElement>
|
||||||
|
|
||||||
<!-- Data Filtering -->
|
<!-- Data Filtering -->
|
||||||
<ui:VisualElement class="section">
|
<ui:VisualElement class="section">
|
||||||
<ui:Foldout text="Data Filtering" value="true" class="section-foldout">
|
<ui:Foldout text="Data Filtering" value="true" class="section-foldout">
|
||||||
|
|||||||
281
Assets/External/StreamingleFacial/NatnetDeviceListener.cs
vendored
Normal file
281
Assets/External/StreamingleFacial/NatnetDeviceListener.cs
vendored
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
//======================================================================================================
|
||||||
|
// NatnetDeviceListener
|
||||||
|
//
|
||||||
|
// Joins NatNet's multicast data stream and parses the WIRE format directly to extract
|
||||||
|
// analog device frame data. Bypasses OptitrackStreamingClient's wrapper struct (which
|
||||||
|
// can't read device data correctly because the C# struct layout doesn't match what
|
||||||
|
// the native NatNet DLL fills into the pFrame buffer for plugin-registered devices).
|
||||||
|
//
|
||||||
|
// Verified: Motive DOES broadcast plugin device data via NatNet (both live and playback);
|
||||||
|
// the existing wrapper just can't read it. This listener confirms by parsing wire bytes
|
||||||
|
// directly.
|
||||||
|
//
|
||||||
|
// Wire format (NatNet 4.x) per section: int32 count, int32 sectionSize, then `sectionSize`
|
||||||
|
// bytes of section payload. We walk past markerSets, otherMarkers, rigidBodies, skeletons,
|
||||||
|
// assets, labeledMarkers, forcePlates, then read devices.
|
||||||
|
//======================================================================================================
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Threading;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
public class NatnetDeviceListener : MonoBehaviour
|
||||||
|
{
|
||||||
|
// ----------------------- Singleton -----------------------
|
||||||
|
// Only one NatnetDeviceListener is needed per scene (multicast — single subscription
|
||||||
|
// serves all consumers). Use NatnetDeviceListener.Instance to access; auto-creates
|
||||||
|
// on first access if not already in scene.
|
||||||
|
|
||||||
|
private static NatnetDeviceListener _instance;
|
||||||
|
|
||||||
|
public static NatnetDeviceListener Instance
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_instance != null) return _instance;
|
||||||
|
_instance = FindObjectOfType<NatnetDeviceListener>();
|
||||||
|
if (_instance == null)
|
||||||
|
{
|
||||||
|
var go = new GameObject("NatnetDeviceListener");
|
||||||
|
_instance = go.AddComponent<NatnetDeviceListener>();
|
||||||
|
}
|
||||||
|
return _instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Awake()
|
||||||
|
{
|
||||||
|
if (_instance != null && _instance != this)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[NatnetDeviceListener] Duplicate instance on '{gameObject.name}' — destroying. Use NatnetDeviceListener.Instance.", gameObject);
|
||||||
|
Destroy(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Header("NatNet Multicast Settings")]
|
||||||
|
[Tooltip("Default Motive multicast group = 239.255.42.99")]
|
||||||
|
public string multicastGroup = "239.255.42.99";
|
||||||
|
|
||||||
|
[Tooltip("Default Motive data port = 1511")]
|
||||||
|
public int dataPort = 1511;
|
||||||
|
|
||||||
|
[Header("Diagnostics (runtime-only)")]
|
||||||
|
[SerializeField] private int _packetsReceived;
|
||||||
|
[SerializeField] private int _framesWithDevices;
|
||||||
|
[SerializeField] private int _activeDeviceCount;
|
||||||
|
[SerializeField] private int _lastFrameNumber;
|
||||||
|
[SerializeField] private string _lastError = "";
|
||||||
|
|
||||||
|
// Per-device latest channel values, keyed by WIRE INDEX (0, 1, 2, ...) NOT
|
||||||
|
// device ID. Motive empirically assigns ID=0 to all plugin devices, so ID-based
|
||||||
|
// keying causes collision. Wire index = position in NatNet frame's devices array.
|
||||||
|
// Description order assumed to match wire order (Motive sends in registration order).
|
||||||
|
private readonly Dictionary<int, float[]> _deviceChannelsByIndex = new Dictionary<int, float[]>();
|
||||||
|
private readonly Dictionary<int, ulong> _deviceFrameSeqByIndex = new Dictionary<int, ulong>();
|
||||||
|
// Also track id for diagnostics.
|
||||||
|
private readonly Dictionary<int, int> _deviceIdByIndex = new Dictionary<int, int>();
|
||||||
|
private int _wireDeviceCount = 0;
|
||||||
|
private readonly object _stateLock = new object();
|
||||||
|
|
||||||
|
private UdpClient _udp;
|
||||||
|
private Thread _thread;
|
||||||
|
private volatile bool _running;
|
||||||
|
|
||||||
|
void Start() { StartListener(); }
|
||||||
|
void OnDisable() { StopListener(); }
|
||||||
|
void OnApplicationQuit() { StopListener(); }
|
||||||
|
void OnDestroy()
|
||||||
|
{
|
||||||
|
if (_instance == this) _instance = null;
|
||||||
|
StopListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
void StartListener()
|
||||||
|
{
|
||||||
|
if (_running) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_udp = new UdpClient();
|
||||||
|
_udp.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
||||||
|
_udp.Client.Bind(new IPEndPoint(IPAddress.Any, dataPort));
|
||||||
|
// INADDR_ANY interface — let OS pick. Matches the working Python sniffer pattern.
|
||||||
|
_udp.JoinMulticastGroup(IPAddress.Parse(multicastGroup));
|
||||||
|
_udp.Client.ReceiveTimeout = 1000;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_lastError = $"bind/join failed: {e.Message}";
|
||||||
|
Debug.LogError($"[NatnetDeviceListener] {_lastError}", this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_running = true;
|
||||||
|
_thread = new Thread(ThreadLoop) { IsBackground = true, Name = "NatnetDeviceListener" };
|
||||||
|
_thread.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void StopListener()
|
||||||
|
{
|
||||||
|
_running = false;
|
||||||
|
try { _udp?.Close(); } catch { }
|
||||||
|
_udp = null;
|
||||||
|
if (_thread != null && _thread.IsAlive)
|
||||||
|
{
|
||||||
|
_thread.Join(500);
|
||||||
|
_thread = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThreadLoop()
|
||||||
|
{
|
||||||
|
var ep = new IPEndPoint(IPAddress.Any, 0);
|
||||||
|
while (_running)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
byte[] data = _udp.Receive(ref ep);
|
||||||
|
Interlocked.Increment(ref _packetsReceived);
|
||||||
|
ParsePacket(data);
|
||||||
|
}
|
||||||
|
catch (SocketException se)
|
||||||
|
{
|
||||||
|
if (!_running) break;
|
||||||
|
if (se.SocketErrorCode == SocketError.TimedOut) continue;
|
||||||
|
_lastError = $"recv: {se.Message}";
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException) { break; }
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_lastError = $"loop: {e.Message}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walks NatNet 4.x frame data sections (each prefixed with count + sectionSize),
|
||||||
|
// jumps past unknown sections by raw byte count, extracts device data.
|
||||||
|
void ParsePacket(byte[] data)
|
||||||
|
{
|
||||||
|
if (data.Length < 12) return;
|
||||||
|
ushort msgId = BitConverter.ToUInt16(data, 0);
|
||||||
|
if (msgId != 7) return; // NAT_FRAMEOFDATA
|
||||||
|
|
||||||
|
int o = 4;
|
||||||
|
int frameNumber = BitConverter.ToInt32(data, o); o += 4;
|
||||||
|
|
||||||
|
// 7 variable-length sections before Devices, each as [int32 count][int32 sectionSize][bytes...]
|
||||||
|
// Order: markerSets, otherMarkers, rigidBodies, skeletons, assets, labeledMarkers, forcePlates.
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (int s = 0; s < 7; s++)
|
||||||
|
{
|
||||||
|
if (o + 8 > data.Length) return;
|
||||||
|
/* count */ BitConverter.ToInt32(data, o); o += 4;
|
||||||
|
int sz = BitConverter.ToInt32(data, o); o += 4;
|
||||||
|
o += sz;
|
||||||
|
if (o > data.Length) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Devices section
|
||||||
|
if (o + 8 > data.Length) return;
|
||||||
|
int devCnt = BitConverter.ToInt32(data, o); o += 4;
|
||||||
|
int devSz = BitConverter.ToInt32(data, o); o += 4;
|
||||||
|
int devEnd = o + devSz;
|
||||||
|
if (devEnd > data.Length || devCnt < 0 || devCnt > 64) return;
|
||||||
|
|
||||||
|
// Parse each device — store by WIRE INDEX, not device ID.
|
||||||
|
// GC OPT: reuse pre-allocated float[] per wire index. Only realloc when
|
||||||
|
// channel count changes (rare — only on device re-registration).
|
||||||
|
int activeCount = 0;
|
||||||
|
for (int d = 0; d < devCnt; d++)
|
||||||
|
{
|
||||||
|
if (o + 8 > devEnd) break;
|
||||||
|
int devId = BitConverter.ToInt32(data, o); o += 4;
|
||||||
|
int nCh = BitConverter.ToInt32(data, o); o += 4;
|
||||||
|
if (nCh < 0 || nCh > 1024) break;
|
||||||
|
|
||||||
|
// Reuse existing buffer if size matches; else allocate new (GC opt).
|
||||||
|
// Stale detection moved to plugin-side (Motive Devices Panel state).
|
||||||
|
float[] vals;
|
||||||
|
_deviceChannelsByIndex.TryGetValue(d, out vals);
|
||||||
|
if (vals == null || vals.Length != nCh)
|
||||||
|
vals = new float[nCh];
|
||||||
|
|
||||||
|
bool ok = true;
|
||||||
|
for (int c = 0; c < nCh; c++)
|
||||||
|
{
|
||||||
|
if (o + 4 > devEnd) { ok = false; break; }
|
||||||
|
int nFr = BitConverter.ToInt32(data, o); o += 4;
|
||||||
|
vals[c] = (nFr > 0 && o + 4 <= devEnd) ? BitConverter.ToSingle(data, o) : 0f;
|
||||||
|
o += nFr * 4;
|
||||||
|
if (o > devEnd) { ok = false; break; }
|
||||||
|
}
|
||||||
|
if (!ok) break;
|
||||||
|
|
||||||
|
lock (_stateLock)
|
||||||
|
{
|
||||||
|
_deviceChannelsByIndex[d] = vals;
|
||||||
|
_deviceIdByIndex[d] = devId;
|
||||||
|
_deviceFrameSeqByIndex[d] = _deviceFrameSeqByIndex.TryGetValue(d, out var s2) ? s2 + 1 : 1;
|
||||||
|
}
|
||||||
|
activeCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateLock) { _wireDeviceCount = activeCount; }
|
||||||
|
_lastFrameNumber = frameNumber;
|
||||||
|
_activeDeviceCount = activeCount;
|
||||||
|
if (activeCount > 0) Interlocked.Increment(ref _framesWithDevices);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_lastError = $"parse: {e.Message}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------- Public API -----------------------
|
||||||
|
|
||||||
|
/// <summary>Returns the latest channel values for a device by WIRE INDEX (0,1,2,...).
|
||||||
|
/// Wire index = position in NatNet frame's devices array, in registration/broadcast order.
|
||||||
|
/// Returns the internal array DIRECTLY (no Clone, no GC alloc). Caller must finish reading
|
||||||
|
/// in one Update tick — values may be overwritten by the NatNet thread otherwise. Acceptable
|
||||||
|
/// for our use (worst case = 1-frame torn read).</summary>
|
||||||
|
public float[] GetDeviceChannelsByIndex(int wireIndex)
|
||||||
|
{
|
||||||
|
lock (_stateLock)
|
||||||
|
{
|
||||||
|
return _deviceChannelsByIndex.TryGetValue(wireIndex, out var arr) ? arr : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Snapshot version: copies values into caller-provided buffer under lock.
|
||||||
|
/// Returns number of values copied (or 0 if device not found).
|
||||||
|
/// Use this if you need exact frame consistency without race window.</summary>
|
||||||
|
public int CopyDeviceChannelsByIndex(int wireIndex, float[] dest)
|
||||||
|
{
|
||||||
|
if (dest == null) return 0;
|
||||||
|
lock (_stateLock)
|
||||||
|
{
|
||||||
|
if (!_deviceChannelsByIndex.TryGetValue(wireIndex, out var arr) || arr == null) return 0;
|
||||||
|
int n = Math.Min(arr.Length, dest.Length);
|
||||||
|
Array.Copy(arr, dest, n);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ulong GetDeviceFrameSeqByIndex(int wireIndex)
|
||||||
|
{
|
||||||
|
lock (_stateLock)
|
||||||
|
{
|
||||||
|
return _deviceFrameSeqByIndex.TryGetValue(wireIndex, out var n) ? n : 0UL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetWireDeviceCount()
|
||||||
|
{
|
||||||
|
lock (_stateLock) { return _wireDeviceCount; }
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/External/StreamingleFacial/NatnetDeviceListener.cs.meta
vendored
Normal file
2
Assets/External/StreamingleFacial/NatnetDeviceListener.cs.meta
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d700c07a89051f94ebc222b985e79115
|
||||||
@ -22,6 +22,48 @@ public class StreamingleFacialReceiver : MonoBehaviour
|
|||||||
private string messageString = "";
|
private string messageString = "";
|
||||||
private string lastProcessedMessage = ""; // 이전 메시지 저장용
|
private string lastProcessedMessage = ""; // 이전 메시지 저장용
|
||||||
|
|
||||||
|
// ── 데이터 소스 모드 ──
|
||||||
|
public enum FacialSourceMode
|
||||||
|
{
|
||||||
|
DirectUDP, // 기존: iFacialMocap 앱에서 직접 UDP 수신
|
||||||
|
NatNetDevice // 신규: Motive NatNet의 device 채널 데이터에서 수신
|
||||||
|
}
|
||||||
|
|
||||||
|
[Header("Source Mode")]
|
||||||
|
[Tooltip("DirectUDP=기존 iFacialMocap UDP 수신 / NatNetDevice=Motive 거쳐서 NatNet으로 받기")]
|
||||||
|
public FacialSourceMode sourceMode = FacialSourceMode.DirectUDP;
|
||||||
|
|
||||||
|
[Header("NatNet Mode (sourceMode=NatNetDevice일 때)")]
|
||||||
|
[Tooltip("OptitrackStreamingClient. 비워두면 Start에서 자동 검색. (디바이스 이름→ID, 채널 이름 메타데이터용)")]
|
||||||
|
public OptitrackStreamingClient natnetStreamingClient;
|
||||||
|
[Tooltip("NatnetDeviceListener. 비워두면 Start에서 자동 검색하고, 없으면 자동 생성. (실제 채널값을 NatNet wire 직접 파싱으로 가져옴 — wrapper 우회)")]
|
||||||
|
public NatnetDeviceListener natnetDeviceListener;
|
||||||
|
[Tooltip("Motive 디바이스 이름 prefix. 활성 포트가 붙어 최종 base name = prefix + activePort (예: \"iFacialMocap_\" + 40001 = \"iFacialMocap_40001\"). 32채널 초과로 분할되었으면 _A/_B 접미사 자동 감지.")]
|
||||||
|
public string natnetDeviceNamePrefix = "iFacialMocap_";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 현재 활성 포트로부터 합성된 디바이스 base name. NatNet 모드에서 사용.
|
||||||
|
/// </summary>
|
||||||
|
public string EffectiveNatnetDeviceBaseName => natnetDeviceNamePrefix + LOCAL_PORT;
|
||||||
|
|
||||||
|
// NatNet 모드: device name → (device id, channel names) 캐시.
|
||||||
|
// OptitrackStreamingClient의 description으로부터 1회 lookup.
|
||||||
|
private struct ResolvedDevice
|
||||||
|
{
|
||||||
|
public int Id;
|
||||||
|
// NatNet 프레임 전체에서의 위치 (description 순서 = wire 순서 가정).
|
||||||
|
// listener는 wire 글로벌 인덱스로 키잉되므로 로컬 리스트 인덱스를 넘기면 안 됨
|
||||||
|
// — 다른 포트의 device 데이터를 잘못 가져오게 됨.
|
||||||
|
public int WireIndex;
|
||||||
|
public List<string> ChannelNames;
|
||||||
|
}
|
||||||
|
private List<ResolvedDevice> natnetResolvedDevices = new List<ResolvedDevice>(2);
|
||||||
|
private float natnetLastResolveAttempt = -10f;
|
||||||
|
private string natnetLastResolvedFor = ""; // 포트 변경 감지용
|
||||||
|
private ulong[] natnetLastSeenSeq = new ulong[2]; // resolved 디바이스별 frame seq 추적
|
||||||
|
private const float k_NatnetResolveRetrySec = 1.0f;
|
||||||
|
private System.Text.StringBuilder natnetMsgBuilder = new System.Text.StringBuilder(2048);
|
||||||
|
|
||||||
// ── 공유 포트 (마스터 모드) ──
|
// ── 공유 포트 (마스터 모드) ──
|
||||||
[Header("Shared Port (Master Mode)")]
|
[Header("Shared Port (Master Mode)")]
|
||||||
[Tooltip("활성화 시 UdpMaster를 통해 같은 포트의 데이터를 여러 Receiver가 공유 수신")]
|
[Tooltip("활성화 시 UdpMaster를 통해 같은 포트의 데이터를 여러 Receiver가 공유 수신")]
|
||||||
@ -193,7 +235,26 @@ public class StreamingleFacialReceiver : MonoBehaviour
|
|||||||
// BlendShape 인덱스 캐싱 초기화
|
// BlendShape 인덱스 캐싱 초기화
|
||||||
InitializeBlendShapeCache();
|
InitializeBlendShapeCache();
|
||||||
|
|
||||||
if (useSharedPort)
|
if (sourceMode == FacialSourceMode.NatNetDevice)
|
||||||
|
{
|
||||||
|
// NatNet 모드: 자체 UDP 서버 안 띄움. 두 컴포넌트에 의존:
|
||||||
|
// - OptitrackStreamingClient: device 이름→ID + 채널 이름 메타데이터
|
||||||
|
// - NatnetDeviceListener: 실제 채널값 (wire 직접 파싱)
|
||||||
|
if (natnetStreamingClient == null)
|
||||||
|
natnetStreamingClient = FindObjectOfType<OptitrackStreamingClient>();
|
||||||
|
if (natnetStreamingClient == null)
|
||||||
|
{
|
||||||
|
Debug.LogError("[Streamingle] NatNet 모드인데 OptitrackStreamingClient를 못 찾음.", this);
|
||||||
|
}
|
||||||
|
else if (!natnetStreamingClient.ReceiveDevices)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[Streamingle] OptitrackStreamingClient.ReceiveDevices 가 꺼져있음 — 켜야 device description이 들어옴.", this);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (natnetDeviceListener == null)
|
||||||
|
natnetDeviceListener = NatnetDeviceListener.Instance;
|
||||||
|
}
|
||||||
|
else if (useSharedPort)
|
||||||
{
|
{
|
||||||
// 마스터 모드: UdpMaster에 등록하여 공유 수신
|
// 마스터 모드: UdpMaster에 등록하여 공유 수신
|
||||||
StreamingleFacialUdpMaster.Instance.Register(LOCAL_PORT, this);
|
StreamingleFacialUdpMaster.Instance.Register(LOCAL_PORT, this);
|
||||||
@ -325,6 +386,14 @@ public class StreamingleFacialReceiver : MonoBehaviour
|
|||||||
RebuildIntensityOverrideMap();
|
RebuildIntensityOverrideMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NatNet 모드: device 채널을 폴링해 iFacialMocap 와이어 포맷의 messageString을
|
||||||
|
// 만든 뒤 기존 SetAnimation 파이프라인을 그대로 태운다. (이렇게 하면 필터/미러/
|
||||||
|
// intensity 코드 한 줄도 안 고치고 NatNet 경로 추가됨)
|
||||||
|
if (sourceMode == FacialSourceMode.NatNetDevice)
|
||||||
|
{
|
||||||
|
PollNatNetDevice();
|
||||||
|
}
|
||||||
|
|
||||||
// 메시지가 변경되었을 때만 처리 (성능 최적화)
|
// 메시지가 변경되었을 때만 처리 (성능 최적화)
|
||||||
if (!string.IsNullOrEmpty(messageString) && messageString != lastProcessedMessage)
|
if (!string.IsNullOrEmpty(messageString) && messageString != lastProcessedMessage)
|
||||||
{
|
{
|
||||||
@ -340,6 +409,102 @@ public class StreamingleFacialReceiver : MonoBehaviour
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PollNatNetDevice()
|
||||||
|
{
|
||||||
|
if (natnetStreamingClient == null || natnetDeviceListener == null) return;
|
||||||
|
|
||||||
|
string baseName = EffectiveNatnetDeviceBaseName;
|
||||||
|
|
||||||
|
// 포트 변경 또는 정의 미발견 시 재해석. OptitrackStreamingClient의 device descriptions
|
||||||
|
// 에서 우리 base name과 매칭되는 device id + channel names를 캐시.
|
||||||
|
bool portChanged = natnetLastResolvedFor != baseName;
|
||||||
|
if (portChanged ||
|
||||||
|
natnetResolvedDevices.Count == 0 ||
|
||||||
|
Time.unscaledTime - natnetLastResolveAttempt > k_NatnetResolveRetrySec)
|
||||||
|
{
|
||||||
|
natnetLastResolveAttempt = Time.unscaledTime;
|
||||||
|
natnetLastResolvedFor = baseName;
|
||||||
|
natnetResolvedDevices.Clear();
|
||||||
|
|
||||||
|
var defs = natnetStreamingClient.GetDeviceDefinitions();
|
||||||
|
if (defs != null)
|
||||||
|
{
|
||||||
|
// Match base name (primary device) and ANY device whose name starts with
|
||||||
|
// baseName + "_" (overflow / future suffixes like _A, _B, _overflow).
|
||||||
|
// Order matters for downstream merging: primary first, then secondaries.
|
||||||
|
ResolvedDevice? primary = null;
|
||||||
|
List<ResolvedDevice> secondaries = new List<ResolvedDevice>();
|
||||||
|
for (int gi = 0; gi < defs.Count; gi++)
|
||||||
|
{
|
||||||
|
var d = defs[gi];
|
||||||
|
if (d.Name == baseName)
|
||||||
|
{
|
||||||
|
primary = new ResolvedDevice { Id = d.Id, WireIndex = gi, ChannelNames = d.ChannelNames };
|
||||||
|
}
|
||||||
|
else if (d.Name.StartsWith(baseName + "_"))
|
||||||
|
{
|
||||||
|
secondaries.Add(new ResolvedDevice { Id = d.Id, WireIndex = gi, ChannelNames = d.ChannelNames });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (primary.HasValue) natnetResolvedDevices.Add(primary.Value);
|
||||||
|
natnetResolvedDevices.AddRange(secondaries);
|
||||||
|
}
|
||||||
|
if (natnetLastSeenSeq.Length < natnetResolvedDevices.Count)
|
||||||
|
natnetLastSeenSeq = new ulong[natnetResolvedDevices.Count];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (natnetResolvedDevices.Count == 0) return;
|
||||||
|
|
||||||
|
// GC OPT: skip rebuild if no listener has new wire frame since last poll.
|
||||||
|
// (Unity Update tick can run faster than NatNet broadcast rate.)
|
||||||
|
bool anyNew = false;
|
||||||
|
for (int di = 0; di < natnetResolvedDevices.Count; di++)
|
||||||
|
{
|
||||||
|
int wi = natnetResolvedDevices[di].WireIndex;
|
||||||
|
ulong seq = natnetDeviceListener.GetDeviceFrameSeqByIndex(wi);
|
||||||
|
if (di < natnetLastSeenSeq.Length && seq != natnetLastSeenSeq[di])
|
||||||
|
{
|
||||||
|
natnetLastSeenSeq[di] = seq;
|
||||||
|
anyNew = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!anyNew) return;
|
||||||
|
|
||||||
|
// iFacialMocap 와이어 포맷 합성: name-value|name-value|...=
|
||||||
|
// 채널값은 NatnetDeviceListener (wire 직접 파싱) 에서, 채널 이름은 description에서.
|
||||||
|
// **WIRE INDEX 기반 매칭** — Motive가 모든 plugin device id를 0으로 broadcasting하는
|
||||||
|
// 버그 회피. description 순서 = wire 순서 가정 (Motive registration order).
|
||||||
|
natnetMsgBuilder.Length = 0;
|
||||||
|
bool first = true;
|
||||||
|
for (int di = 0; di < natnetResolvedDevices.Count; di++)
|
||||||
|
{
|
||||||
|
var rd = natnetResolvedDevices[di];
|
||||||
|
float[] vals = natnetDeviceListener.GetDeviceChannelsByIndex(rd.WireIndex);
|
||||||
|
if (vals == null || rd.ChannelNames == null) continue;
|
||||||
|
|
||||||
|
int n = Math.Min(vals.Length, rd.ChannelNames.Count);
|
||||||
|
for (int c = 0; c < n; c++)
|
||||||
|
{
|
||||||
|
string name = rd.ChannelNames[c];
|
||||||
|
if (string.IsNullOrEmpty(name)) continue;
|
||||||
|
if (name[0] == 'h' && name.StartsWith("head_")) continue;
|
||||||
|
if (name[0] == 'l' && name.StartsWith("leftEye_")) continue;
|
||||||
|
if (name[0] == 'r' && name.StartsWith("rightEye_")) continue;
|
||||||
|
|
||||||
|
if (!first) natnetMsgBuilder.Append('|');
|
||||||
|
natnetMsgBuilder.Append(name);
|
||||||
|
natnetMsgBuilder.Append('-');
|
||||||
|
natnetMsgBuilder.Append(vals[c].ToString("0.####", CultureInfo.InvariantCulture));
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 파서가 split('=', RemoveEmptyEntries) 길이 >= 2를 요구함.
|
||||||
|
// 빈 transforms 섹션은 RemoveEmptyEntries에 의해 제거되므로 dummy 채워넣음.
|
||||||
|
natnetMsgBuilder.Append("=head#0,0,0,0,0,0|leftEye#0,0,0|rightEye#0,0,0");
|
||||||
|
|
||||||
|
messageString = natnetMsgBuilder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
//BlendShapeの設定
|
//BlendShapeの設定
|
||||||
//set blendshapes (캐시 사용으로 최적화)
|
//set blendshapes (캐시 사용으로 최적화)
|
||||||
void SetBlendShapeWeightFromStrArray(string[] strArray2)
|
void SetBlendShapeWeightFromStrArray(string[] strArray2)
|
||||||
@ -532,6 +697,12 @@ public class StreamingleFacialReceiver : MonoBehaviour
|
|||||||
{
|
{
|
||||||
StartFlag = true;
|
StartFlag = true;
|
||||||
|
|
||||||
|
if (sourceMode == FacialSourceMode.NatNetDevice)
|
||||||
|
{
|
||||||
|
// NatNet 모드: UDP 자원 없음. OptitrackStreamingClient는 다른 곳이 관리.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (useSharedPort)
|
if (useSharedPort)
|
||||||
{
|
{
|
||||||
// 마스터에서 해제
|
// 마스터에서 해제
|
||||||
@ -574,7 +745,9 @@ public class StreamingleFacialReceiver : MonoBehaviour
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 포트 핫스왑: 지정된 인덱스의 포트로 즉시 전환
|
/// 포트 핫스왑: 지정된 인덱스의 포트로 즉시 전환.
|
||||||
|
/// 두 모드 공통: DirectUDP는 UDP listener 재바인드, NatNet은 다음 polling 사이클에서
|
||||||
|
/// 새 포트의 디바이스로 자동 재해석됨.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void SwitchToPort(int portIndex)
|
public void SwitchToPort(int portIndex)
|
||||||
{
|
{
|
||||||
@ -588,6 +761,15 @@ public class StreamingleFacialReceiver : MonoBehaviour
|
|||||||
activePortIndex = portIndex;
|
activePortIndex = portIndex;
|
||||||
Debug.Log($"[iFacialMocap] 포트 전환: {availablePorts[portIndex]}");
|
Debug.Log($"[iFacialMocap] 포트 전환: {availablePorts[portIndex]}");
|
||||||
|
|
||||||
|
if (sourceMode == FacialSourceMode.NatNetDevice)
|
||||||
|
{
|
||||||
|
// NatNet 모드: 다음 PollNatNetDevice 호출에서 새 base name으로 자동 재해석.
|
||||||
|
// 즉시 강제하려면 캐시 무효화.
|
||||||
|
natnetResolvedDevices.Clear();
|
||||||
|
natnetLastResolveAttempt = -10f;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (useSharedPort)
|
if (useSharedPort)
|
||||||
{
|
{
|
||||||
// 마스터 모드: 포트 전환 요청
|
// 마스터 모드: 포트 전환 요청
|
||||||
@ -615,6 +797,17 @@ public class StreamingleFacialReceiver : MonoBehaviour
|
|||||||
{
|
{
|
||||||
Debug.Log("[iFacialMocap] 재접속 시도 중...");
|
Debug.Log("[iFacialMocap] 재접속 시도 중...");
|
||||||
|
|
||||||
|
if (sourceMode == FacialSourceMode.NatNetDevice)
|
||||||
|
{
|
||||||
|
// NatNet 모드: 디바이스 재해석 강제 (다음 PollNatNetDevice 호출에서 다시 검색).
|
||||||
|
natnetResolvedDevices.Clear();
|
||||||
|
natnetLastResolveAttempt = -10f;
|
||||||
|
yield return null;
|
||||||
|
reconnectCoroutine = null;
|
||||||
|
Debug.Log("[iFacialMocap] NatNet device 재해석 강제 완료");
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
if (useSharedPort)
|
if (useSharedPort)
|
||||||
{
|
{
|
||||||
// 마스터 모드: 해제 후 재등록
|
// 마스터 모드: 해제 후 재등록
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user