Fix : 옵티트랙 시스템 업데이트 DLL구조 분리
This commit is contained in:
parent
1d53db7cab
commit
25c3c128e4
BIN
Assets/External/OptiTrack Unity Plugin/OptiTrack/Plugins/x86/NatNetLib.dll
(Stored with Git LFS)
vendored
BIN
Assets/External/OptiTrack Unity Plugin/OptiTrack/Plugins/x86/NatNetLib.dll
(Stored with Git LFS)
vendored
Binary file not shown.
@ -1,52 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 86f370a71a6ec2a4da168a46e45eef86
|
|
||||||
PluginImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
iconMap: {}
|
|
||||||
executionOrder: {}
|
|
||||||
defineConstraints: []
|
|
||||||
isPreloaded: 0
|
|
||||||
isOverridable: 0
|
|
||||||
isExplicitlyReferenced: 0
|
|
||||||
validateReferences: 1
|
|
||||||
platformData:
|
|
||||||
- first:
|
|
||||||
Any:
|
|
||||||
second:
|
|
||||||
enabled: 1
|
|
||||||
settings: {}
|
|
||||||
- first:
|
|
||||||
Editor: Editor
|
|
||||||
second:
|
|
||||||
enabled: 0
|
|
||||||
settings:
|
|
||||||
CPU: x86
|
|
||||||
DefaultValueInitialized: true
|
|
||||||
- first:
|
|
||||||
Standalone: Linux64
|
|
||||||
second:
|
|
||||||
enabled: 0
|
|
||||||
settings:
|
|
||||||
CPU: None
|
|
||||||
- first:
|
|
||||||
Standalone: OSXUniversal
|
|
||||||
second:
|
|
||||||
enabled: 0
|
|
||||||
settings:
|
|
||||||
CPU: x86
|
|
||||||
- first:
|
|
||||||
Standalone: Win
|
|
||||||
second:
|
|
||||||
enabled: 1
|
|
||||||
settings:
|
|
||||||
CPU: x86
|
|
||||||
- first:
|
|
||||||
Standalone: Win64
|
|
||||||
second:
|
|
||||||
enabled: 0
|
|
||||||
settings:
|
|
||||||
CPU: None
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
BIN
Assets/External/OptiTrack Unity Plugin/OptiTrack/Plugins/x86_64/NatNetLib.dll
(Stored with Git LFS)
vendored
BIN
Assets/External/OptiTrack Unity Plugin/OptiTrack/Plugins/x86_64/NatNetLib.dll
(Stored with Git LFS)
vendored
Binary file not shown.
@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 90a37dc4ae5b35a45876059d687031bc
|
|
||||||
BIN
Assets/External/OptiTrack Unity Plugin/OptiTrack/Prefabs/Client - OptiTrack.prefab
(Stored with Git LFS)
vendored
BIN
Assets/External/OptiTrack Unity Plugin/OptiTrack/Prefabs/Client - OptiTrack.prefab
(Stored with Git LFS)
vendored
Binary file not shown.
@ -22,30 +22,25 @@ public class OptitrackStreamingClientEditor : Editor
|
|||||||
client = (OptitrackStreamingClient)target;
|
client = (OptitrackStreamingClient)target;
|
||||||
var root = new VisualElement();
|
var root = new VisualElement();
|
||||||
|
|
||||||
// Load stylesheets
|
|
||||||
var commonUss = AssetDatabase.LoadAssetAtPath<StyleSheet>(CommonUssPath);
|
var commonUss = AssetDatabase.LoadAssetAtPath<StyleSheet>(CommonUssPath);
|
||||||
if (commonUss != null) root.styleSheets.Add(commonUss);
|
if (commonUss != null) root.styleSheets.Add(commonUss);
|
||||||
|
|
||||||
var uss = AssetDatabase.LoadAssetAtPath<StyleSheet>(UssPath);
|
var uss = AssetDatabase.LoadAssetAtPath<StyleSheet>(UssPath);
|
||||||
if (uss != null) root.styleSheets.Add(uss);
|
if (uss != null) root.styleSheets.Add(uss);
|
||||||
|
|
||||||
// Load UXML
|
|
||||||
var uxml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(UxmlPath);
|
var uxml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(UxmlPath);
|
||||||
if (uxml != null) uxml.CloneTree(root);
|
if (uxml != null) uxml.CloneTree(root);
|
||||||
|
|
||||||
// Cache references
|
|
||||||
statusDot = root.Q("statusDot");
|
statusDot = root.Q("statusDot");
|
||||||
statusText = root.Q<Label>("statusText");
|
statusText = root.Q<Label>("statusText");
|
||||||
runtimeOffline = root.Q("runtimeOffline");
|
runtimeOffline = root.Q("runtimeOffline");
|
||||||
runtimeOnline = root.Q("runtimeOnline");
|
runtimeOnline = root.Q("runtimeOnline");
|
||||||
runtimeInfo = root.Q("runtimeInfo");
|
runtimeInfo = root.Q("runtimeInfo");
|
||||||
|
|
||||||
// Reconnect button
|
|
||||||
var reconnectBtn = root.Q<Button>("reconnectBtn");
|
var reconnectBtn = root.Q<Button>("reconnectBtn");
|
||||||
if (reconnectBtn != null)
|
if (reconnectBtn != null)
|
||||||
reconnectBtn.clicked += () => { if (Application.isPlaying) client.Reconnect(); };
|
reconnectBtn.clicked += () => { if (Application.isPlaying) client.Reconnect(); };
|
||||||
|
|
||||||
// Play mode polling
|
|
||||||
root.schedule.Execute(UpdatePlayModeState).Every(300);
|
root.schedule.Execute(UpdatePlayModeState).Every(300);
|
||||||
|
|
||||||
return root;
|
return root;
|
||||||
@ -57,7 +52,6 @@ public class OptitrackStreamingClientEditor : Editor
|
|||||||
|
|
||||||
bool isPlaying = Application.isPlaying;
|
bool isPlaying = Application.isPlaying;
|
||||||
|
|
||||||
// Toggle runtime sections
|
|
||||||
if (isPlaying)
|
if (isPlaying)
|
||||||
{
|
{
|
||||||
if (!runtimeOnline.ClassListContains("opti-runtime-online--visible"))
|
if (!runtimeOnline.ClassListContains("opti-runtime-online--visible"))
|
||||||
@ -73,7 +67,6 @@ public class OptitrackStreamingClientEditor : Editor
|
|||||||
runtimeOffline.RemoveFromClassList("opti-runtime-offline--hidden");
|
runtimeOffline.RemoveFromClassList("opti-runtime-offline--hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update status badge
|
|
||||||
if (isPlaying)
|
if (isPlaying)
|
||||||
{
|
{
|
||||||
bool connected = client.IsConnected();
|
bool connected = client.IsConnected();
|
||||||
@ -97,7 +90,6 @@ public class OptitrackStreamingClientEditor : Editor
|
|||||||
{
|
{
|
||||||
if (statusDot == null || statusText == null) return;
|
if (statusDot == null || statusText == null) return;
|
||||||
|
|
||||||
// Clear all states
|
|
||||||
statusDot.RemoveFromClassList("opti-status-dot--connected");
|
statusDot.RemoveFromClassList("opti-status-dot--connected");
|
||||||
statusDot.RemoveFromClassList("opti-status-dot--disconnected");
|
statusDot.RemoveFromClassList("opti-status-dot--disconnected");
|
||||||
statusText.RemoveFromClassList("opti-status-text--connected");
|
statusText.RemoveFromClassList("opti-status-text--connected");
|
||||||
@ -125,6 +117,8 @@ public class OptitrackStreamingClientEditor : Editor
|
|||||||
? client.LocalAddress
|
? client.LocalAddress
|
||||||
: client.ResolvedLocalAddress);
|
: client.ResolvedLocalAddress);
|
||||||
AddInfoRow(runtimeInfo, "Connection Type", client.ConnectionType.ToString());
|
AddInfoRow(runtimeInfo, "Connection Type", client.ConnectionType.ToString());
|
||||||
|
if (client.EnableReplayPriority)
|
||||||
|
AddInfoRow(runtimeInfo, "Replay / MMRP", client.ReplayServerAddress);
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(client.ServerNatNetVersion))
|
if (!string.IsNullOrEmpty(client.ServerNatNetVersion))
|
||||||
AddInfoRow(runtimeInfo, "Server NatNet", client.ServerNatNetVersion);
|
AddInfoRow(runtimeInfo, "Server NatNet", client.ServerNatNetVersion);
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements">
|
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements">
|
||||||
|
|
||||||
<!-- Title Bar -->
|
<!-- Title Bar -->
|
||||||
<ui:VisualElement name="titleBar" class="opti-title-bar">
|
<ui:VisualElement name="titleBar" class="opti-title-bar">
|
||||||
@ -12,11 +12,20 @@
|
|||||||
<!-- Connection Settings -->
|
<!-- Connection Settings -->
|
||||||
<ui:VisualElement class="section">
|
<ui:VisualElement class="section">
|
||||||
<ui:Foldout text="Connection Settings" value="true" class="section-foldout">
|
<ui:Foldout text="Connection Settings" value="true" class="section-foldout">
|
||||||
<uie:PropertyField binding-path="ServerAddress" label="Server Address"/>
|
<uie:PropertyField binding-path="ServerAddress" label="Live Motive IP"/>
|
||||||
|
<uie:PropertyField binding-path="EnableReplayPriority" label="Use MMRP Replay Priority"/>
|
||||||
|
<uie:PropertyField binding-path="ReplayServerAddress" label="MMRP Replay IP"/>
|
||||||
|
<uie:PropertyField binding-path="DirectSyntheticSkeletonName" label="Fallback Actor Name"/>
|
||||||
<uie:PropertyField binding-path="ConnectionType" label="Connection Type"/>
|
<uie:PropertyField binding-path="ConnectionType" label="Connection Type"/>
|
||||||
<uie:PropertyField binding-path="SkeletonCoordinates" label="Skeleton Coordinates"/>
|
<uie:PropertyField binding-path="SkeletonCoordinates" label="Skeleton Coordinates"/>
|
||||||
<uie:PropertyField binding-path="TMarkersetCoordinates" label="TMarkerset Coordinates"/>
|
<uie:PropertyField binding-path="TMarkersetCoordinates" label="TMarkerset Coordinates"/>
|
||||||
<uie:PropertyField binding-path="BoneNamingConvention" label="Bone Naming Convention"/>
|
<uie:PropertyField binding-path="BoneNamingConvention" label="Bone Naming Convention"/>
|
||||||
|
<ui:Foldout text="Advanced Endpoint Override" value="false" class="section-foldout">
|
||||||
|
<uie:PropertyField binding-path="CommandPort" label="Command Port"/>
|
||||||
|
<uie:PropertyField binding-path="DataPort" label="Data Port"/>
|
||||||
|
<uie:PropertyField binding-path="MulticastAddress" label="Multicast Address"/>
|
||||||
|
<uie:PropertyField binding-path="ReplayFreshnessSeconds" label="Replay Freshness Seconds"/>
|
||||||
|
</ui:Foldout>
|
||||||
</ui:Foldout>
|
</ui:Foldout>
|
||||||
</ui:VisualElement>
|
</ui:VisualElement>
|
||||||
|
|
||||||
@ -28,7 +37,7 @@
|
|||||||
<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"/>
|
<uie:PropertyField binding-path="ReceiveDevices" label="Receive Devices"/>
|
||||||
<ui:HelpBox message-type="Info" text="iFacialMocap 등 analog device 데이터 수신. 매 프레임 sFrameOfMocapData 마샬링 발생(~200KB) — 페이셜 안 쓰면 OFF."/>
|
<ui:HelpBox message-type="Info" text="Receives analog device data such as iFacialMocap face streams. Turn this off if the scene does not use NatNet device data."/>
|
||||||
<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"/>
|
||||||
@ -38,15 +47,15 @@
|
|||||||
<!-- Mirror Mode -->
|
<!-- Mirror Mode -->
|
||||||
<ui:VisualElement class="section">
|
<ui:VisualElement class="section">
|
||||||
<ui:Foldout text="Mirror Mode" value="true" class="section-foldout">
|
<ui:Foldout text="Mirror Mode" value="true" class="section-foldout">
|
||||||
<uie:PropertyField binding-path="MirrorMode" label="Mirror Mode (좌우 반전)"/>
|
<uie:PropertyField binding-path="MirrorMode" label="Mirror Mode"/>
|
||||||
</ui:Foldout>
|
</ui:Foldout>
|
||||||
</ui:VisualElement>
|
</ui:VisualElement>
|
||||||
|
|
||||||
<!-- Skeleton Frame Filter -->
|
<!-- Skeleton Frame Filter -->
|
||||||
<ui:VisualElement class="section">
|
<ui:VisualElement class="section">
|
||||||
<ui:Foldout text="Skeleton Frame Filter" value="true" class="section-foldout">
|
<ui:Foldout text="Skeleton Frame Filter" value="true" class="section-foldout">
|
||||||
<uie:PropertyField binding-path="EnableSkeletonFrameFilter" label="Enable Filter (엄격)"/>
|
<uie:PropertyField binding-path="EnableSkeletonFrameFilter" label="Enable Strict Skeleton Frame Filter"/>
|
||||||
<ui:HelpBox message-type="Info" text="ON: 본 하나라도 트래킹 실패/손상되면 그 프레임 전체를 폐기 → 떨림은 줄지만 라이브에서 마커 가림 시 액터가 얼어붙을 수 있음. OFF(권장·라이브): 정상 본만 갱신, 미트래킹·손상 본은 직전 포즈 유지 → 모션이 끊기지 않음."/>
|
<ui:HelpBox message-type="Info" text="ON: drop the whole skeleton frame if any bone is untracked or invalid. OFF is recommended for live use: valid bones update, invalid bones keep the last pose."/>
|
||||||
</ui:Foldout>
|
</ui:Foldout>
|
||||||
</ui:VisualElement>
|
</ui:VisualElement>
|
||||||
|
|
||||||
@ -62,7 +71,7 @@
|
|||||||
<ui:VisualElement name="runtimeSection" class="section">
|
<ui:VisualElement name="runtimeSection" class="section">
|
||||||
<ui:Foldout text="Runtime Controls" value="true" class="section-foldout">
|
<ui:Foldout text="Runtime Controls" value="true" class="section-foldout">
|
||||||
<ui:VisualElement name="runtimeOffline" class="opti-runtime-offline">
|
<ui:VisualElement name="runtimeOffline" class="opti-runtime-offline">
|
||||||
<ui:HelpBox message-type="Info" text="재접속 기능은 플레이 모드에서만 사용할 수 있습니다."/>
|
<ui:HelpBox message-type="Info" text="Runtime controls are available in Play Mode."/>
|
||||||
</ui:VisualElement>
|
</ui:VisualElement>
|
||||||
<ui:VisualElement name="runtimeOnline" class="opti-runtime-online">
|
<ui:VisualElement name="runtimeOnline" class="opti-runtime-online">
|
||||||
<ui:Button name="reconnectBtn" text="OptiTrack Reconnect" class="opti-reconnect-btn"/>
|
<ui:Button name="reconnectBtn" text="OptiTrack Reconnect" class="opti-reconnect-btn"/>
|
||||||
|
|||||||
@ -49,6 +49,8 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
|
|||||||
private OptitrackSkeletonDefinition m_skeletonDef;
|
private OptitrackSkeletonDefinition m_skeletonDef;
|
||||||
private string previousSkeletonName;
|
private string previousSkeletonName;
|
||||||
|
|
||||||
|
private OptitrackStreamingClient m_boundStreamingClient;
|
||||||
|
|
||||||
[HideInInspector]
|
[HideInInspector]
|
||||||
public bool isSkeletonFound = false;
|
public bool isSkeletonFound = false;
|
||||||
|
|
||||||
@ -261,15 +263,45 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
|
|||||||
{
|
{
|
||||||
if (StreamingClient != null)
|
if (StreamingClient != null)
|
||||||
{
|
{
|
||||||
StreamingClient.RegisterSkeleton(this, this.SkeletonAssetName);
|
RebindStreamingClient();
|
||||||
previousSkeletonName = SkeletonAssetName;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 코루틴이 이미 돌고 있지 않으면 시작
|
|
||||||
if (m_checkCoroutine == null)
|
if (m_checkCoroutine == null)
|
||||||
m_checkCoroutine = StartCoroutine(CheckSkeletonConnectionPeriodically());
|
m_checkCoroutine = StartCoroutine(CheckSkeletonConnectionPeriodically());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetStreamingClient(OptitrackStreamingClient client)
|
||||||
|
{
|
||||||
|
if (StreamingClient == client && m_boundStreamingClient == client)
|
||||||
|
return;
|
||||||
|
StreamingClient = client;
|
||||||
|
RebindStreamingClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RebindStreamingClient()
|
||||||
|
{
|
||||||
|
m_boundStreamingClient = StreamingClient;
|
||||||
|
m_skeletonDef = null;
|
||||||
|
m_boneIdToMappingIndex.Clear();
|
||||||
|
m_snapshotPositions.Clear();
|
||||||
|
m_snapshotOrientations.Clear();
|
||||||
|
ClearInterpolationBuffers();
|
||||||
|
m_hasLastFrameTimestamp = false;
|
||||||
|
isSkeletonFound = false;
|
||||||
|
|
||||||
|
if (StreamingClient == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
StreamingClient.RegisterSkeleton(this, SkeletonAssetName);
|
||||||
|
previousSkeletonName = SkeletonAssetName;
|
||||||
|
m_skeletonDef = StreamingClient.GetSkeletonDefinitionByName(SkeletonAssetName);
|
||||||
|
if (m_skeletonDef != null)
|
||||||
|
{
|
||||||
|
RebuildBoneIdMapping();
|
||||||
|
isSkeletonFound = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private bool m_lastMirrorMode = false;
|
private bool m_lastMirrorMode = false;
|
||||||
|
|
||||||
void Update()
|
void Update()
|
||||||
@ -280,6 +312,12 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (StreamingClient != m_boundStreamingClient)
|
||||||
|
{
|
||||||
|
RebindStreamingClient();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// MirrorMode 변경 감지 → 보간 상태 리셋 (불연속 튐 방지)
|
// MirrorMode 변경 감지 → 보간 상태 리셋 (불연속 튐 방지)
|
||||||
bool currentMirrorMode = StreamingClient != null && StreamingClient.MirrorMode;
|
bool currentMirrorMode = StreamingClient != null && StreamingClient.MirrorMode;
|
||||||
if (currentMirrorMode != m_lastMirrorMode)
|
if (currentMirrorMode != m_lastMirrorMode)
|
||||||
@ -291,12 +329,7 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
|
|||||||
// 스켈레톤 이름 변경 감지
|
// 스켈레톤 이름 변경 감지
|
||||||
if (previousSkeletonName != SkeletonAssetName)
|
if (previousSkeletonName != SkeletonAssetName)
|
||||||
{
|
{
|
||||||
StreamingClient.RegisterSkeleton(this, SkeletonAssetName);
|
RebindStreamingClient();
|
||||||
m_skeletonDef = StreamingClient.GetSkeletonDefinitionByName(SkeletonAssetName);
|
|
||||||
previousSkeletonName = SkeletonAssetName;
|
|
||||||
if (m_skeletonDef != null)
|
|
||||||
RebuildBoneIdMapping();
|
|
||||||
ClearInterpolationBuffers();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -479,8 +512,7 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
|
|||||||
m_hipBoneId = -1;
|
m_hipBoneId = -1;
|
||||||
foreach (var bone in m_skeletonDef.Bones)
|
foreach (var bone in m_skeletonDef.Bones)
|
||||||
{
|
{
|
||||||
string boneName = bone.Name;
|
string optiName = ExtractOptiTrackBoneName(bone.Name, nameToIdx);
|
||||||
string optiName = boneName.Contains("_") ? boneName.Substring(boneName.IndexOf('_') + 1) : boneName;
|
|
||||||
if (nameToIdx.TryGetValue(optiName, out int idx))
|
if (nameToIdx.TryGetValue(optiName, out int idx))
|
||||||
{
|
{
|
||||||
m_boneIdToMappingIndex[bone.Id] = idx;
|
m_boneIdToMappingIndex[bone.Id] = idx;
|
||||||
@ -492,6 +524,41 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
|
|||||||
Debug.Log($"[OptiTrack] 본 ID 매핑 완료: {matchCount}/{m_skeletonDef.Bones.Count} 매칭");
|
Debug.Log($"[OptiTrack] 본 ID 매핑 완료: {matchCount}/{m_skeletonDef.Bones.Count} 매칭");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string ExtractOptiTrackBoneName(string streamedBoneName, Dictionary<string, int> knownOptiNames)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(streamedBoneName))
|
||||||
|
return streamedBoneName;
|
||||||
|
|
||||||
|
if (knownOptiNames.ContainsKey(streamedBoneName))
|
||||||
|
return streamedBoneName;
|
||||||
|
|
||||||
|
int firstSep = streamedBoneName.IndexOf('_');
|
||||||
|
if (firstSep >= 0)
|
||||||
|
{
|
||||||
|
string afterFirst = streamedBoneName.Substring(firstSep + 1);
|
||||||
|
if (knownOptiNames.ContainsKey(afterFirst))
|
||||||
|
return afterFirst;
|
||||||
|
|
||||||
|
string beforeFirst = streamedBoneName.Substring(0, firstSep);
|
||||||
|
if (knownOptiNames.ContainsKey(beforeFirst))
|
||||||
|
return beforeFirst;
|
||||||
|
}
|
||||||
|
|
||||||
|
int lastSep = streamedBoneName.LastIndexOf('_');
|
||||||
|
if (lastSep > 0 && lastSep != firstSep)
|
||||||
|
{
|
||||||
|
string beforeLast = streamedBoneName.Substring(0, lastSep);
|
||||||
|
if (knownOptiNames.ContainsKey(beforeLast))
|
||||||
|
return beforeLast;
|
||||||
|
|
||||||
|
string afterLast = streamedBoneName.Substring(lastSep + 1);
|
||||||
|
if (knownOptiNames.ContainsKey(afterLast))
|
||||||
|
return afterLast;
|
||||||
|
}
|
||||||
|
|
||||||
|
return streamedBoneName;
|
||||||
|
}
|
||||||
|
|
||||||
private void InitializeStreamingClient()
|
private void InitializeStreamingClient()
|
||||||
{
|
{
|
||||||
if (StreamingClient == null)
|
if (StreamingClient == null)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,281 +1,17 @@
|
|||||||
//======================================================================================================
|
|
||||||
// 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;
|
using UnityEngine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Legacy compatibility component.
|
||||||
|
/// NatNet device channels are now parsed directly by OptitrackStreamingClient.
|
||||||
|
/// This stub keeps old scenes/prefabs from producing missing-script references.
|
||||||
|
/// </summary>
|
||||||
public class NatnetDeviceListener : MonoBehaviour
|
public class NatnetDeviceListener : MonoBehaviour
|
||||||
{
|
{
|
||||||
// ----------------------- Singleton -----------------------
|
[Header("Legacy")]
|
||||||
// Only one NatnetDeviceListener is needed per scene (multicast — single subscription
|
[Tooltip("Deprecated. OptitrackStreamingClient now parses NatNet device channels directly.")]
|
||||||
// serves all consumers). Use NatnetDeviceListener.Instance to access; auto-creates
|
public bool disabledByUnifiedOptitrackClient = true;
|
||||||
// on first access if not already in scene.
|
|
||||||
|
|
||||||
private static NatnetDeviceListener _instance;
|
[Header("Old Settings (unused)")]
|
||||||
|
|
||||||
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";
|
public string multicastGroup = "239.255.42.99";
|
||||||
|
|
||||||
[Tooltip("Default Motive data port = 1511")]
|
|
||||||
public int dataPort = 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; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,8 +36,6 @@ public class StreamingleFacialReceiver : MonoBehaviour
|
|||||||
[Header("NatNet Mode (sourceMode=NatNetDevice일 때)")]
|
[Header("NatNet Mode (sourceMode=NatNetDevice일 때)")]
|
||||||
[Tooltip("OptitrackStreamingClient. 비워두면 Start에서 자동 검색. (디바이스 이름→ID, 채널 이름 메타데이터용)")]
|
[Tooltip("OptitrackStreamingClient. 비워두면 Start에서 자동 검색. (디바이스 이름→ID, 채널 이름 메타데이터용)")]
|
||||||
public OptitrackStreamingClient natnetStreamingClient;
|
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 접미사 자동 감지.")]
|
[Tooltip("Motive 디바이스 이름 prefix. 활성 포트가 붙어 최종 base name = prefix + activePort (예: \"iFacialMocap_\" + 40001 = \"iFacialMocap_40001\"). 32채널 초과로 분할되었으면 _A/_B 접미사 자동 감지.")]
|
||||||
public string natnetDeviceNamePrefix = "iFacialMocap_";
|
public string natnetDeviceNamePrefix = "iFacialMocap_";
|
||||||
|
|
||||||
@ -46,6 +44,24 @@ public class StreamingleFacialReceiver : MonoBehaviour
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string EffectiveNatnetDeviceBaseName => natnetDeviceNamePrefix + LOCAL_PORT;
|
public string EffectiveNatnetDeviceBaseName => natnetDeviceNamePrefix + LOCAL_PORT;
|
||||||
|
|
||||||
|
public void SetNatNetSource(OptitrackStreamingClient client)
|
||||||
|
{
|
||||||
|
if (natnetStreamingClient == client) return;
|
||||||
|
natnetStreamingClient = client;
|
||||||
|
ResetNatNetDeviceResolution();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetNatNetDeviceResolution()
|
||||||
|
{
|
||||||
|
natnetResolvedDevices.Clear();
|
||||||
|
natnetLastResolveAttempt = -10f;
|
||||||
|
natnetLastResolvedFor = "";
|
||||||
|
Array.Clear(natnetLastSeenSeq, 0, natnetLastSeenSeq.Length);
|
||||||
|
natnetDiagLogged = false;
|
||||||
|
messageString = "";
|
||||||
|
lastProcessedMessage = "";
|
||||||
|
}
|
||||||
|
|
||||||
// NatNet 모드: device name → (device id, channel names) 캐시.
|
// NatNet 모드: device name → (device id, channel names) 캐시.
|
||||||
// OptitrackStreamingClient의 description으로부터 1회 lookup.
|
// OptitrackStreamingClient의 description으로부터 1회 lookup.
|
||||||
private struct ResolvedDevice
|
private struct ResolvedDevice
|
||||||
@ -54,7 +70,7 @@ public class StreamingleFacialReceiver : MonoBehaviour
|
|||||||
// NatNet 프레임 전체에서의 위치 (description 순서 = wire 순서 가정).
|
// NatNet 프레임 전체에서의 위치 (description 순서 = wire 순서 가정).
|
||||||
// listener는 wire 글로벌 인덱스로 키잉되므로 로컬 리스트 인덱스를 넘기면 안 됨
|
// listener는 wire 글로벌 인덱스로 키잉되므로 로컬 리스트 인덱스를 넘기면 안 됨
|
||||||
// — 다른 포트의 device 데이터를 잘못 가져오게 됨.
|
// — 다른 포트의 device 데이터를 잘못 가져오게 됨.
|
||||||
public int WireIndex;
|
public string DeviceName;
|
||||||
public List<string> ChannelNames;
|
public List<string> ChannelNames;
|
||||||
}
|
}
|
||||||
private List<ResolvedDevice> natnetResolvedDevices = new List<ResolvedDevice>(2);
|
private List<ResolvedDevice> natnetResolvedDevices = new List<ResolvedDevice>(2);
|
||||||
@ -252,8 +268,6 @@ public class StreamingleFacialReceiver : MonoBehaviour
|
|||||||
Debug.LogWarning("[Streamingle] OptitrackStreamingClient.ReceiveDevices 가 꺼져있음 — 켜야 device description이 들어옴.", this);
|
Debug.LogWarning("[Streamingle] OptitrackStreamingClient.ReceiveDevices 가 꺼져있음 — 켜야 device description이 들어옴.", this);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (natnetDeviceListener == null)
|
|
||||||
natnetDeviceListener = NatnetDeviceListener.Instance;
|
|
||||||
}
|
}
|
||||||
else if (useSharedPort)
|
else if (useSharedPort)
|
||||||
{
|
{
|
||||||
@ -438,11 +452,73 @@ public class StreamingleFacialReceiver : MonoBehaviour
|
|||||||
"tongueOut", "trackingStatus",
|
"tongueOut", "trackingStatus",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static OptitrackDeviceState FindNatNetDeviceState(List<OptitrackDeviceState> states, int id, string name)
|
||||||
|
{
|
||||||
|
if (states == null) return null;
|
||||||
|
for (int i = 0; i < states.Count; i++)
|
||||||
|
{
|
||||||
|
var state = states[i];
|
||||||
|
if (state == null) continue;
|
||||||
|
if (state.Id == id) return state;
|
||||||
|
if (!string.IsNullOrEmpty(name) && state.Name == name) return state;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryBuildNatNetDeviceValueArray(OptitrackDeviceState state, out float[] values)
|
||||||
|
{
|
||||||
|
values = null;
|
||||||
|
if (state == null || state.ChannelValues == null || state.ChannelValues.Count == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
int maxChannelIndex = -1;
|
||||||
|
foreach (var key in state.ChannelValues.Keys)
|
||||||
|
{
|
||||||
|
int index;
|
||||||
|
if (TryParseNatNetChannelIndex(key, out index))
|
||||||
|
maxChannelIndex = Mathf.Max(maxChannelIndex, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxChannelIndex >= 0)
|
||||||
|
{
|
||||||
|
values = new float[maxChannelIndex + 1];
|
||||||
|
foreach (var kvp in state.ChannelValues)
|
||||||
|
{
|
||||||
|
int index;
|
||||||
|
if (TryParseNatNetChannelIndex(kvp.Key, out index) && index >= 0 && index < values.Length)
|
||||||
|
values[index] = kvp.Value;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.ChannelValues.TryGetValue("sentinelPort", out float sentinel))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
values = new float[1 + kCanonicalBlendshapeNames.Length];
|
||||||
|
values[0] = sentinel;
|
||||||
|
for (int i = 0; i < kCanonicalBlendshapeNames.Length; i++)
|
||||||
|
{
|
||||||
|
float value;
|
||||||
|
if (state.ChannelValues.TryGetValue(kCanonicalBlendshapeNames[i], out value))
|
||||||
|
values[i + 1] = value;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseNatNetChannelIndex(string key, out int index)
|
||||||
|
{
|
||||||
|
index = -1;
|
||||||
|
if (string.IsNullOrEmpty(key) || !key.StartsWith("Channel", StringComparison.Ordinal))
|
||||||
|
return false;
|
||||||
|
return int.TryParse(key.Substring("Channel".Length), NumberStyles.Integer, CultureInfo.InvariantCulture, out index);
|
||||||
|
}
|
||||||
|
|
||||||
void PollNatNetDevice()
|
void PollNatNetDevice()
|
||||||
{
|
{
|
||||||
if (natnetStreamingClient == null || natnetDeviceListener == null) return;
|
if (natnetStreamingClient == null) return;
|
||||||
|
|
||||||
string baseName = EffectiveNatnetDeviceBaseName;
|
string baseName = EffectiveNatnetDeviceBaseName;
|
||||||
|
var deviceStates = natnetStreamingClient.GetLatestDeviceStates();
|
||||||
|
|
||||||
// 포트 변경 또는 정의 미발견 시 재해석. OptitrackStreamingClient의 device descriptions
|
// 포트 변경 또는 정의 미발견 시 재해석. OptitrackStreamingClient의 device descriptions
|
||||||
// 에서 우리 base name과 매칭되는 device id + channel names를 캐시.
|
// 에서 우리 base name과 매칭되는 device id + channel names를 캐시.
|
||||||
@ -462,17 +538,18 @@ public class StreamingleFacialReceiver : MonoBehaviour
|
|||||||
// 2) 채널 수 큰 게 primary, 작은 게 overflow
|
// 2) 채널 수 큰 게 primary, 작은 게 overflow
|
||||||
// 3) ChannelNames 는 canonical 배열에서 슬라이스로 부여
|
// 3) ChannelNames 는 canonical 배열에서 슬라이스로 부여
|
||||||
int port = LOCAL_PORT;
|
int port = LOCAL_PORT;
|
||||||
int wireCount = natnetDeviceListener.GetWireDeviceCount();
|
var wireCandidates = new List<(OptitrackDeviceState state, float[] vals)>();
|
||||||
var wireCandidates = new List<(int wi, int len)>();
|
for (int si = 0; si < deviceStates.Count; si++)
|
||||||
for (int wi = 0; wi < wireCount; wi++)
|
|
||||||
{
|
{
|
||||||
float[] vals = natnetDeviceListener.GetDeviceChannelsByIndex(wi);
|
float[] vals;
|
||||||
|
if (!TryBuildNatNetDeviceValueArray(deviceStates[si], out vals))
|
||||||
|
continue;
|
||||||
if (vals == null || vals.Length < 1) continue;
|
if (vals == null || vals.Length < 1) continue;
|
||||||
int sentinel = Mathf.RoundToInt(vals[0]);
|
int sentinel = Mathf.RoundToInt(vals[0]);
|
||||||
if (sentinel != port) continue;
|
if (sentinel != port) continue;
|
||||||
wireCandidates.Add((wi, vals.Length));
|
wireCandidates.Add((deviceStates[si], vals));
|
||||||
}
|
}
|
||||||
wireCandidates.Sort((a, b) => b.len.CompareTo(a.len));
|
wireCandidates.Sort((a, b) => b.vals.Length.CompareTo(a.vals.Length));
|
||||||
|
|
||||||
// canonical 을 primary slice + overflow slice 로 분리
|
// canonical 을 primary slice + overflow slice 로 분리
|
||||||
var primarySlice = new List<string>(1 + kCanonicalPrimaryCount);
|
var primarySlice = new List<string>(1 + kCanonicalPrimaryCount);
|
||||||
@ -487,9 +564,9 @@ public class StreamingleFacialReceiver : MonoBehaviour
|
|||||||
|
|
||||||
// 0: primary (큰 채널 수), 1: overflow (작은 채널 수)
|
// 0: primary (큰 채널 수), 1: overflow (작은 채널 수)
|
||||||
if (wireCandidates.Count > 0)
|
if (wireCandidates.Count > 0)
|
||||||
natnetResolvedDevices.Add(new ResolvedDevice { Id = 0, WireIndex = wireCandidates[0].wi, ChannelNames = primarySlice });
|
natnetResolvedDevices.Add(new ResolvedDevice { Id = wireCandidates[0].state.Id, DeviceName = wireCandidates[0].state.Name, ChannelNames = primarySlice });
|
||||||
if (wireCandidates.Count > 1)
|
if (wireCandidates.Count > 1)
|
||||||
natnetResolvedDevices.Add(new ResolvedDevice { Id = 0, WireIndex = wireCandidates[1].wi, ChannelNames = overflowSlice });
|
natnetResolvedDevices.Add(new ResolvedDevice { Id = wireCandidates[1].state.Id, DeviceName = wireCandidates[1].state.Name, ChannelNames = overflowSlice });
|
||||||
if (natnetLastSeenSeq.Length < natnetResolvedDevices.Count)
|
if (natnetLastSeenSeq.Length < natnetResolvedDevices.Count)
|
||||||
natnetLastSeenSeq = new ulong[natnetResolvedDevices.Count];
|
natnetLastSeenSeq = new ulong[natnetResolvedDevices.Count];
|
||||||
}
|
}
|
||||||
@ -501,8 +578,8 @@ public class StreamingleFacialReceiver : MonoBehaviour
|
|||||||
bool anyNew = false;
|
bool anyNew = false;
|
||||||
for (int di = 0; di < natnetResolvedDevices.Count; di++)
|
for (int di = 0; di < natnetResolvedDevices.Count; di++)
|
||||||
{
|
{
|
||||||
int wi = natnetResolvedDevices[di].WireIndex;
|
OptitrackDeviceState state = FindNatNetDeviceState(deviceStates, natnetResolvedDevices[di].Id, natnetResolvedDevices[di].DeviceName);
|
||||||
ulong seq = natnetDeviceListener.GetDeviceFrameSeqByIndex(wi);
|
ulong seq = state != null ? (ulong)(uint)state.FrameNumber : 0;
|
||||||
if (di < natnetLastSeenSeq.Length && seq != natnetLastSeenSeq[di])
|
if (di < natnetLastSeenSeq.Length && seq != natnetLastSeenSeq[di])
|
||||||
{
|
{
|
||||||
natnetLastSeenSeq[di] = seq;
|
natnetLastSeenSeq[di] = seq;
|
||||||
@ -518,7 +595,10 @@ public class StreamingleFacialReceiver : MonoBehaviour
|
|||||||
for (int di = 0; di < natnetResolvedDevices.Count; di++)
|
for (int di = 0; di < natnetResolvedDevices.Count; di++)
|
||||||
{
|
{
|
||||||
var rd = natnetResolvedDevices[di];
|
var rd = natnetResolvedDevices[di];
|
||||||
float[] vals = natnetDeviceListener.GetDeviceChannelsByIndex(rd.WireIndex);
|
OptitrackDeviceState state = FindNatNetDeviceState(deviceStates, rd.Id, rd.DeviceName);
|
||||||
|
float[] vals;
|
||||||
|
if (!TryBuildNatNetDeviceValueArray(state, out vals))
|
||||||
|
continue;
|
||||||
if (vals == null || rd.ChannelNames == null) continue;
|
if (vals == null || rd.ChannelNames == null) continue;
|
||||||
|
|
||||||
if (!natnetDiagLogged)
|
if (!natnetDiagLogged)
|
||||||
@ -529,7 +609,7 @@ public class StreamingleFacialReceiver : MonoBehaviour
|
|||||||
int nameDump = Math.Min(5, rd.ChannelNames.Count);
|
int nameDump = Math.Min(5, rd.ChannelNames.Count);
|
||||||
var nameArr = new string[nameDump];
|
var nameArr = new string[nameDump];
|
||||||
for (int k = 0; k < nameDump; k++) nameArr[k] = rd.ChannelNames[k];
|
for (int k = 0; k < nameDump; k++) nameArr[k] = rd.ChannelNames[k];
|
||||||
Debug.Log($"[NatNet Align Diag] dev[{di}] WireIdx={rd.WireIndex} canonicalLen={rd.ChannelNames.Count} wireLen={vals.Length} firstNames=[{string.Join(",", nameArr)}] firstVals=[{string.Join(",", valDump)}]");
|
Debug.Log($"[NatNet Align Diag] dev[{di}] Device={rd.DeviceName} Id={rd.Id} canonicalLen={rd.ChannelNames.Count} wireLen={vals.Length} firstNames=[{string.Join(",", nameArr)}] firstVals=[{string.Join(",", valDump)}]");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Canonical 매핑: rd.ChannelNames 는 [sentinelPort, canonical[a], canonical[a+1], ...]
|
// Canonical 매핑: rd.ChannelNames 는 [sentinelPort, canonical[a], canonical[a+1], ...]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user