Fix OptiTrack refresh and UDP facial flow
This commit is contained in:
parent
f05d461d41
commit
c55103a3e5
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.
@ -35,14 +35,21 @@
|
|||||||
<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="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"/>
|
||||||
</ui:Foldout>
|
</ui:Foldout>
|
||||||
</ui:VisualElement>
|
</ui:VisualElement>
|
||||||
|
|
||||||
|
<!-- Definition Refresh -->
|
||||||
|
<ui:VisualElement class="section">
|
||||||
|
<ui:Foldout text="Definition Refresh" value="true" class="section-foldout">
|
||||||
|
<uie:PropertyField binding-path="AutoRefreshDefinitions" label="Auto Refresh Definitions"/>
|
||||||
|
<uie:PropertyField binding-path="DefinitionRefreshInterval" label="Refresh Interval"/>
|
||||||
|
<ui:HelpBox message-type="Info" text="Motive에서 Actor 또는 RigidBody가 추가/삭제되면 DataDescription을 다시 조회하고 등록된 스트림을 재구독합니다."/>
|
||||||
|
</ui:Foldout>
|
||||||
|
</ui:VisualElement>
|
||||||
|
|
||||||
<!-- 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">
|
||||||
|
|||||||
@ -1,229 +0,0 @@
|
|||||||
//======================================================================================================
|
|
||||||
// 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 Dictionary<string, float> _channelSnapshot = new Dictionary<string, float>();
|
|
||||||
private float _lastResolveAttempt = -10f;
|
|
||||||
private const float k_ResolveRetrySeconds = 1.0f;
|
|
||||||
|
|
||||||
void Start()
|
|
||||||
{
|
|
||||||
if (streamingClient == null)
|
|
||||||
streamingClient = FindFirstObjectByType<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)
|
|
||||||
{
|
|
||||||
if (!streamingClient.FillDeviceChannelSnapshot(_resolvedDeviceNames[i], _channelSnapshot))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
foreach (var kv in _channelSnapshot)
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 5ba2193cab25f3f48b3e660ae26e8c3b
|
|
||||||
@ -15,7 +15,6 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
|
||||||
using KindRetargeting;
|
using KindRetargeting;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
@ -38,6 +37,7 @@ public class OptitrackRigidBody : MonoBehaviour
|
|||||||
private OptitrackStreamingClient m_streamingClient;
|
private OptitrackStreamingClient m_streamingClient;
|
||||||
private bool m_isRigidBodyFound = false;
|
private bool m_isRigidBodyFound = false;
|
||||||
private int m_resolvedRigidBodyId = -1;
|
private int m_resolvedRigidBodyId = -1;
|
||||||
|
private float m_nextResolveAttemptTime = 0f;
|
||||||
|
|
||||||
private const float k_RetryInterval = 3.0f; // Motive 재조회 부하 완화 (기존 1초 → 3초)
|
private const float k_RetryInterval = 3.0f; // Motive 재조회 부하 완화 (기존 1초 → 3초)
|
||||||
|
|
||||||
@ -61,54 +61,34 @@ public class OptitrackRigidBody : MonoBehaviour
|
|||||||
}
|
}
|
||||||
|
|
||||||
TryResolveRigidBody();
|
TryResolveRigidBody();
|
||||||
|
|
||||||
// 초기 해결 실패 시 주기적으로 재시도
|
|
||||||
if (m_resolvedRigidBodyId == -1 && !string.IsNullOrEmpty(propName))
|
|
||||||
{
|
|
||||||
StartCoroutine(RetryResolveCoroutine());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TryResolveRigidBody()
|
private bool TryResolveRigidBody()
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(propName))
|
if (!string.IsNullOrEmpty(propName))
|
||||||
{
|
{
|
||||||
int resolved = m_streamingClient.GetRigidBodyIdByName(propName);
|
int resolved = m_streamingClient.GetRigidBodyIdByName(propName);
|
||||||
if (resolved != -1)
|
if (resolved != -1)
|
||||||
{
|
{
|
||||||
|
if (m_resolvedRigidBodyId != -1 && m_resolvedRigidBodyId != resolved)
|
||||||
|
m_streamingClient.UnregisterRigidBody(this, m_resolvedRigidBodyId);
|
||||||
|
|
||||||
m_resolvedRigidBodyId = resolved;
|
m_resolvedRigidBodyId = resolved;
|
||||||
m_streamingClient.RegisterRigidBody(this, m_resolvedRigidBodyId);
|
m_streamingClient.RegisterRigidBody(this, m_resolvedRigidBodyId);
|
||||||
Debug.Log($"OptitrackRigidBody: '{propName}' 해결 완료 (ID: {m_resolvedRigidBodyId})", this);
|
Debug.Log($"OptitrackRigidBody: '{propName}' 해결 완료 (ID: {m_resolvedRigidBodyId})", this);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
m_resolvedRigidBodyId = rigidBodyId;
|
m_resolvedRigidBodyId = rigidBodyId;
|
||||||
m_streamingClient.RegisterRigidBody(this, m_resolvedRigidBodyId);
|
m_streamingClient.RegisterRigidBody(this, m_resolvedRigidBodyId);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerator RetryResolveCoroutine()
|
|
||||||
{
|
|
||||||
var wait = new WaitForSeconds(k_RetryInterval);
|
|
||||||
|
|
||||||
while (m_resolvedRigidBodyId == -1)
|
|
||||||
{
|
|
||||||
yield return wait;
|
|
||||||
|
|
||||||
if (m_streamingClient == null) yield break;
|
|
||||||
|
|
||||||
// 서버에 정의 재조회 요청
|
|
||||||
m_streamingClient.RequestDefinitionRefresh();
|
|
||||||
|
|
||||||
// 다음 프레임까지 대기 (정의 갱신이 Update에서 처리되므로)
|
|
||||||
yield return null;
|
|
||||||
|
|
||||||
TryResolveRigidBody();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#if UNITY_2017_1_OR_NEWER
|
#if UNITY_2017_1_OR_NEWER
|
||||||
void OnEnable()
|
void OnEnable()
|
||||||
{
|
{
|
||||||
@ -131,9 +111,15 @@ public class OptitrackRigidBody : MonoBehaviour
|
|||||||
|
|
||||||
void Update()
|
void Update()
|
||||||
{
|
{
|
||||||
if (m_streamingClient == null || m_resolvedRigidBodyId == -1)
|
if (m_streamingClient == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (m_resolvedRigidBodyId == -1)
|
||||||
|
{
|
||||||
|
RetryResolveIfNeeded();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// NatNet 호출은 Update()에서 1회만 — 캐시하여 OnBeforeRender()에서 재사용
|
// NatNet 호출은 Update()에서 1회만 — 캐시하여 OnBeforeRender()에서 재사용
|
||||||
OptitrackRigidBodyState rbState = m_streamingClient.GetLatestRigidBodyState(m_resolvedRigidBodyId, useNetworkCompensation);
|
OptitrackRigidBodyState rbState = m_streamingClient.GetLatestRigidBodyState(m_resolvedRigidBodyId, useNetworkCompensation);
|
||||||
if (rbState != null)
|
if (rbState != null)
|
||||||
@ -153,6 +139,20 @@ public class OptitrackRigidBody : MonoBehaviour
|
|||||||
m_isRigidBodyFound = false;
|
m_isRigidBodyFound = false;
|
||||||
m_hasCachedPose = false;
|
m_hasCachedPose = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!m_isRigidBodyFound && !string.IsNullOrEmpty(propName))
|
||||||
|
RetryResolveIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void RetryResolveIfNeeded()
|
||||||
|
{
|
||||||
|
if (Time.unscaledTime < m_nextResolveAttemptTime)
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_nextResolveAttemptTime = Time.unscaledTime + k_RetryInterval;
|
||||||
|
m_streamingClient.RequestDefinitionRefresh();
|
||||||
|
TryResolveRigidBody();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
BIN
Assets/External/OptiTrack Unity Plugin/README.md
(Stored with Git LFS)
vendored
BIN
Assets/External/OptiTrack Unity Plugin/README.md
(Stored with Git LFS)
vendored
Binary file not shown.
@ -20,9 +20,6 @@ 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()
|
||||||
{
|
{
|
||||||
@ -49,9 +46,6 @@ 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");
|
||||||
@ -80,16 +74,6 @@ 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());
|
||||||
@ -97,11 +81,6 @@ 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);
|
||||||
|
|
||||||
@ -125,16 +104,6 @@ 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;
|
||||||
@ -144,10 +113,6 @@ 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,26 +21,7 @@
|
|||||||
</ui:Foldout>
|
</ui:Foldout>
|
||||||
</ui:VisualElement>
|
</ui:VisualElement>
|
||||||
|
|
||||||
<!-- Source Mode -->
|
<!-- Port Hot-Swap -->
|
||||||
<ui:VisualElement class="section">
|
|
||||||
<ui:Foldout text="Source Mode" value="true" class="section-foldout">
|
|
||||||
<uie:PropertyField binding-path="sourceMode" label="Source Mode"/>
|
|
||||||
<ui:Label text="DirectUDP = iFacialMocap UDP 직접 수신 NatNetDevice = Motive 거쳐 NatNet으로 수신 (시간동기 + .tak 녹화 통합)" class="facial-info-text"/>
|
|
||||||
|
|
||||||
<!-- NatNet 모드 전용 필드 -->
|
|
||||||
<ui:VisualElement name="natnetFields">
|
|
||||||
<uie:PropertyField binding-path="natnetStreamingClient" label="OptiTrack Streaming Client"/>
|
|
||||||
<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:Foldout>
|
|
||||||
</ui:VisualElement>
|
|
||||||
|
|
||||||
<!-- 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">
|
||||||
@ -52,23 +33,18 @@
|
|||||||
</ui:Foldout>
|
</ui:Foldout>
|
||||||
</ui:VisualElement>
|
</ui:VisualElement>
|
||||||
|
|
||||||
<!-- UDP 전용 섹션들 (DirectUDP 모드일 때만 노출) -->
|
<!-- Shared Port (Master Mode) -->
|
||||||
<ui:VisualElement name="udpSourceFields">
|
<ui:VisualElement class="section">
|
||||||
|
<ui:Foldout text="Shared Port (Master Mode)" value="true" class="section-foldout">
|
||||||
<!-- Shared Port (Master Mode) -->
|
<uie:PropertyField binding-path="useSharedPort" label="Use Shared Port"/>
|
||||||
<ui:VisualElement class="section">
|
<ui:VisualElement name="sharedPortInfo" class="facial-shared-port-info">
|
||||||
<ui:Foldout text="Shared Port (Master Mode)" value="true" class="section-foldout">
|
<ui:Label text="같은 포트에 여러 Receiver를 연결하여 동일한 모캡 데이터를 공유 수신합니다." class="facial-info-text"/>
|
||||||
<uie:PropertyField binding-path="useSharedPort" label="Use Shared Port"/>
|
</ui:VisualElement>
|
||||||
<ui:VisualElement name="sharedPortInfo" class="facial-shared-port-info">
|
<ui:VisualElement name="masterStatusRow" class="facial-master-status-row">
|
||||||
<ui:Label text="같은 포트에 여러 Receiver를 연결하여 동일한 모캡 데이터를 공유 수신합니다." class="facial-info-text"/>
|
<ui:Label text="Master Status" class="facial-port-label"/>
|
||||||
</ui:VisualElement>
|
<ui:Label name="masterStatusValue" text="---" class="facial-master-status-value"/>
|
||||||
<ui:VisualElement name="masterStatusRow" class="facial-master-status-row">
|
</ui:VisualElement>
|
||||||
<ui:Label text="Master Status" class="facial-port-label"/>
|
</ui:Foldout>
|
||||||
<ui:Label name="masterStatusValue" text="---" class="facial-master-status-value"/>
|
|
||||||
</ui:VisualElement>
|
|
||||||
</ui:Foldout>
|
|
||||||
</ui:VisualElement>
|
|
||||||
|
|
||||||
</ui:VisualElement>
|
</ui:VisualElement>
|
||||||
|
|
||||||
<!-- Data Filtering -->
|
<!-- Data Filtering -->
|
||||||
|
|||||||
@ -1,17 +0,0 @@
|
|||||||
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
|
|
||||||
{
|
|
||||||
[Header("Legacy")]
|
|
||||||
[Tooltip("Deprecated. OptitrackStreamingClient now parses NatNet device channels directly.")]
|
|
||||||
public bool disabledByUnifiedOptitrackClient = true;
|
|
||||||
|
|
||||||
[Header("Old Settings (unused)")]
|
|
||||||
public string multicastGroup = "239.255.42.99";
|
|
||||||
public int dataPort = 1511;
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: d700c07a89051f94ebc222b985e79115
|
|
||||||
@ -22,65 +22,6 @@ 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("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;
|
|
||||||
|
|
||||||
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) 캐시.
|
|
||||||
// OptitrackStreamingClient의 description으로부터 1회 lookup.
|
|
||||||
private struct ResolvedDevice
|
|
||||||
{
|
|
||||||
public int Id;
|
|
||||||
// NatNet 프레임 전체에서의 위치 (description 순서 = wire 순서 가정).
|
|
||||||
// listener는 wire 글로벌 인덱스로 키잉되므로 로컬 리스트 인덱스를 넘기면 안 됨
|
|
||||||
// — 다른 포트의 device 데이터를 잘못 가져오게 됨.
|
|
||||||
public string DeviceName;
|
|
||||||
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 bool natnetDiagLogged = false; // [NatNet Align Diag] 1회만 출력
|
|
||||||
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가 공유 수신")]
|
||||||
@ -252,24 +193,7 @@ public class StreamingleFacialReceiver : MonoBehaviour
|
|||||||
// BlendShape 인덱스 캐싱 초기화
|
// BlendShape 인덱스 캐싱 초기화
|
||||||
InitializeBlendShapeCache();
|
InitializeBlendShapeCache();
|
||||||
|
|
||||||
if (sourceMode == FacialSourceMode.NatNetDevice)
|
if (useSharedPort)
|
||||||
{
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
else if (useSharedPort)
|
|
||||||
{
|
{
|
||||||
// 마스터 모드: UdpMaster에 등록하여 공유 수신
|
// 마스터 모드: UdpMaster에 등록하여 공유 수신
|
||||||
StreamingleFacialUdpMaster.Instance.Register(LOCAL_PORT, this);
|
StreamingleFacialUdpMaster.Instance.Register(LOCAL_PORT, this);
|
||||||
@ -401,14 +325,6 @@ 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)
|
||||||
{
|
{
|
||||||
@ -424,219 +340,6 @@ public class StreamingleFacialReceiver : MonoBehaviour
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2026-05-13 PROTOCOL-LEVEL FIX. Plugin 과 양쪽 모두 하드코딩된 canonical
|
|
||||||
// blendshape 순서를 공유. Motive 가 description 을 reorder/strip 해도 wire 데이터
|
|
||||||
// 만큼은 plugin 이 쓴 그대로 도달하므로 alignment 보장. 두 사이드는 이 리스트가
|
|
||||||
// 일치해야 함 — plugin 의 FaceDevice.cpp `kCanonicalBlendshapeNames` 와 동일.
|
|
||||||
private const int kCanonicalPrimaryCount = 31; // primary device 가 담는 first canonical names
|
|
||||||
private static readonly string[] kCanonicalBlendshapeNames = new string[]
|
|
||||||
{
|
|
||||||
"browDown_L", "browDown_R", "browInnerUp", "browOuterUp_L", "browOuterUp_R",
|
|
||||||
"cheekPuff", "cheekSquint_L", "cheekSquint_R",
|
|
||||||
"eyeBlink_L", "eyeBlink_R",
|
|
||||||
"eyeLookDown_L", "eyeLookDown_R", "eyeLookIn_L", "eyeLookIn_R",
|
|
||||||
"eyeLookOut_L", "eyeLookOut_R", "eyeLookUp_L", "eyeLookUp_R",
|
|
||||||
"eyeSquint_L", "eyeSquint_R", "eyeWide_L", "eyeWide_R",
|
|
||||||
"hapihapi",
|
|
||||||
"jawForward", "jawLeft", "jawOpen", "jawRight",
|
|
||||||
"mouthClose", "mouthDimple_L", "mouthDimple_R",
|
|
||||||
"mouthFrown_L", "mouthFrown_R", "mouthFunnel",
|
|
||||||
"mouthLeft", "mouthLowerDown_L", "mouthLowerDown_R",
|
|
||||||
"mouthPress_L", "mouthPress_R", "mouthPucker",
|
|
||||||
"mouthRight", "mouthRollLower", "mouthRollUpper",
|
|
||||||
"mouthShrugLower", "mouthShrugUpper",
|
|
||||||
"mouthSmile_L", "mouthSmile_R",
|
|
||||||
"mouthStretch_L", "mouthStretch_R",
|
|
||||||
"mouthUpperUp_L", "mouthUpperUp_R",
|
|
||||||
"noseSneer_L", "noseSneer_R",
|
|
||||||
"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()
|
|
||||||
{
|
|
||||||
if (natnetStreamingClient == null) return;
|
|
||||||
|
|
||||||
string baseName = EffectiveNatnetDeviceBaseName;
|
|
||||||
var deviceStates = natnetStreamingClient.GetLatestDeviceStates();
|
|
||||||
|
|
||||||
// 포트 변경 또는 정의 미발견 시 재해석. 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;
|
|
||||||
if (portChanged) natnetDiagLogged = false;
|
|
||||||
natnetLastResolvedFor = baseName;
|
|
||||||
natnetResolvedDevices.Clear();
|
|
||||||
|
|
||||||
// 2026-05-13 PROTOCOL-LEVEL FIX. Description 완전 무시. Plugin/Unity 가
|
|
||||||
// 공유하는 kCanonicalBlendshapeNames 가 채널 이름 source of truth.
|
|
||||||
// 1) wire 의 sentinel 값으로 우리 port 의 wire device 식별 (multi-iPhone 환경 OK)
|
|
||||||
// 2) 채널 수 큰 게 primary, 작은 게 overflow
|
|
||||||
// 3) ChannelNames 는 canonical 배열에서 슬라이스로 부여
|
|
||||||
int port = LOCAL_PORT;
|
|
||||||
var wireCandidates = new List<(OptitrackDeviceState state, float[] vals)>();
|
|
||||||
for (int si = 0; si < deviceStates.Count; si++)
|
|
||||||
{
|
|
||||||
float[] vals;
|
|
||||||
if (!TryBuildNatNetDeviceValueArray(deviceStates[si], out vals))
|
|
||||||
continue;
|
|
||||||
if (vals == null || vals.Length < 1) continue;
|
|
||||||
int sentinel = Mathf.RoundToInt(vals[0]);
|
|
||||||
if (sentinel != port) continue;
|
|
||||||
wireCandidates.Add((deviceStates[si], vals));
|
|
||||||
}
|
|
||||||
wireCandidates.Sort((a, b) => b.vals.Length.CompareTo(a.vals.Length));
|
|
||||||
|
|
||||||
// canonical 을 primary slice + overflow slice 로 분리
|
|
||||||
var primarySlice = new List<string>(1 + kCanonicalPrimaryCount);
|
|
||||||
primarySlice.Add("sentinelPort");
|
|
||||||
for (int i = 0; i < kCanonicalPrimaryCount && i < kCanonicalBlendshapeNames.Length; i++)
|
|
||||||
primarySlice.Add(kCanonicalBlendshapeNames[i]);
|
|
||||||
|
|
||||||
var overflowSlice = new List<string>(1 + Math.Max(0, kCanonicalBlendshapeNames.Length - kCanonicalPrimaryCount));
|
|
||||||
overflowSlice.Add("sentinelPort");
|
|
||||||
for (int i = kCanonicalPrimaryCount; i < kCanonicalBlendshapeNames.Length; i++)
|
|
||||||
overflowSlice.Add(kCanonicalBlendshapeNames[i]);
|
|
||||||
|
|
||||||
// 0: primary (큰 채널 수), 1: overflow (작은 채널 수)
|
|
||||||
if (wireCandidates.Count > 0)
|
|
||||||
natnetResolvedDevices.Add(new ResolvedDevice { Id = wireCandidates[0].state.Id, DeviceName = wireCandidates[0].state.Name, ChannelNames = primarySlice });
|
|
||||||
if (wireCandidates.Count > 1)
|
|
||||||
natnetResolvedDevices.Add(new ResolvedDevice { Id = wireCandidates[1].state.Id, DeviceName = wireCandidates[1].state.Name, ChannelNames = overflowSlice });
|
|
||||||
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++)
|
|
||||||
{
|
|
||||||
OptitrackDeviceState state = FindNatNetDeviceState(deviceStates, natnetResolvedDevices[di].Id, natnetResolvedDevices[di].DeviceName);
|
|
||||||
ulong seq = state != null ? (ulong)(uint)state.FrameNumber : 0;
|
|
||||||
if (di < natnetLastSeenSeq.Length && seq != natnetLastSeenSeq[di])
|
|
||||||
{
|
|
||||||
natnetLastSeenSeq[di] = seq;
|
|
||||||
anyNew = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!anyNew) return;
|
|
||||||
|
|
||||||
// iFacialMocap 와이어 포맷 합성: name-value|name-value|...=
|
|
||||||
// 채널 이름은 canonical 리스트(plugin과 공유)에서, 값은 NatnetDeviceListener wire 파싱.
|
|
||||||
natnetMsgBuilder.Length = 0;
|
|
||||||
bool first = true;
|
|
||||||
for (int di = 0; di < natnetResolvedDevices.Count; di++)
|
|
||||||
{
|
|
||||||
var rd = natnetResolvedDevices[di];
|
|
||||||
OptitrackDeviceState state = FindNatNetDeviceState(deviceStates, rd.Id, rd.DeviceName);
|
|
||||||
float[] vals;
|
|
||||||
if (!TryBuildNatNetDeviceValueArray(state, out vals))
|
|
||||||
continue;
|
|
||||||
if (vals == null || rd.ChannelNames == null) continue;
|
|
||||||
|
|
||||||
if (!natnetDiagLogged)
|
|
||||||
{
|
|
||||||
int dumpLen = Math.Min(5, vals.Length);
|
|
||||||
var valDump = new string[dumpLen];
|
|
||||||
for (int k = 0; k < dumpLen; k++) valDump[k] = vals[k].ToString("0.##", CultureInfo.InvariantCulture);
|
|
||||||
int nameDump = Math.Min(5, rd.ChannelNames.Count);
|
|
||||||
var nameArr = new string[nameDump];
|
|
||||||
for (int k = 0; k < nameDump; k++) nameArr[k] = rd.ChannelNames[k];
|
|
||||||
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], ...]
|
|
||||||
// 의 슬라이스. wire vals 는 plugin 이 같은 순서로 채움 (plugin 검증됨).
|
|
||||||
// vals[0] = sentinel (40001), vals[c] = ChannelNames[c]'s blendshape value.
|
|
||||||
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 == "sentinelPort") continue;
|
|
||||||
|
|
||||||
if (!first) natnetMsgBuilder.Append('|');
|
|
||||||
natnetMsgBuilder.Append(name);
|
|
||||||
natnetMsgBuilder.Append('-');
|
|
||||||
natnetMsgBuilder.Append(vals[c].ToString("0.####", CultureInfo.InvariantCulture));
|
|
||||||
first = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!natnetDiagLogged && natnetResolvedDevices.Count > 0) natnetDiagLogged = true;
|
|
||||||
// 파서가 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)
|
||||||
@ -829,12 +532,6 @@ public class StreamingleFacialReceiver : MonoBehaviour
|
|||||||
{
|
{
|
||||||
StartFlag = true;
|
StartFlag = true;
|
||||||
|
|
||||||
if (sourceMode == FacialSourceMode.NatNetDevice)
|
|
||||||
{
|
|
||||||
// NatNet 모드: UDP 자원 없음. OptitrackStreamingClient는 다른 곳이 관리.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (useSharedPort)
|
if (useSharedPort)
|
||||||
{
|
{
|
||||||
// 마스터에서 해제
|
// 마스터에서 해제
|
||||||
@ -878,8 +575,6 @@ public class StreamingleFacialReceiver : MonoBehaviour
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 포트 핫스왑: 지정된 인덱스의 포트로 즉시 전환.
|
/// 포트 핫스왑: 지정된 인덱스의 포트로 즉시 전환.
|
||||||
/// 두 모드 공통: DirectUDP는 UDP listener 재바인드, NatNet은 다음 polling 사이클에서
|
|
||||||
/// 새 포트의 디바이스로 자동 재해석됨.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void SwitchToPort(int portIndex)
|
public void SwitchToPort(int portIndex)
|
||||||
{
|
{
|
||||||
@ -893,15 +588,6 @@ 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)
|
||||||
{
|
{
|
||||||
// 마스터 모드: 포트 전환 요청
|
// 마스터 모드: 포트 전환 요청
|
||||||
@ -929,17 +615,6 @@ 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)
|
||||||
{
|
{
|
||||||
// 마스터 모드: 해제 후 재등록
|
// 마스터 모드: 해제 후 재등록
|
||||||
|
|||||||
BIN
Assets/ResourcesData/Project/260327_모션촬영/260327_모션촬영.unity
(Stored with Git LFS)
BIN
Assets/ResourcesData/Project/260327_모션촬영/260327_모션촬영.unity
(Stored with Git LFS)
Binary file not shown.
BIN
Assets/ResourcesData/Project/260403_모션촬영/260403_모션촬영.unity
(Stored with Git LFS)
BIN
Assets/ResourcesData/Project/260403_모션촬영/260403_모션촬영.unity
(Stored with Git LFS)
Binary file not shown.
BIN
Assets/ResourcesData/Project/260414_레드리허설/260416_레드리허설_레드스테이지.unity
(Stored with Git LFS)
BIN
Assets/ResourcesData/Project/260414_레드리허설/260416_레드리허설_레드스테이지.unity
(Stored with Git LFS)
Binary file not shown.
BIN
Assets/ResourcesData/Project/260419_Rude모션촬영/260419_Rude모션촬영.unity
(Stored with Git LFS)
BIN
Assets/ResourcesData/Project/260419_Rude모션촬영/260419_Rude모션촬영.unity
(Stored with Git LFS)
Binary file not shown.
BIN
Assets/ResourcesData/Project/260429_모션촬영_뮤즈/260429_모션촬영_뮤즈.unity
(Stored with Git LFS)
BIN
Assets/ResourcesData/Project/260429_모션촬영_뮤즈/260429_모션촬영_뮤즈.unity
(Stored with Git LFS)
Binary file not shown.
BIN
Assets/ResourcesData/Project/260501_모션촬영/260501_모션촬영.unity
(Stored with Git LFS)
BIN
Assets/ResourcesData/Project/260501_모션촬영/260501_모션촬영.unity
(Stored with Git LFS)
Binary file not shown.
BIN
Assets/ResourcesData/Project/260504_엠키스코어_은서모션촬영/260504_엠키스코어_은서모션촬영.unity
(Stored with Git LFS)
BIN
Assets/ResourcesData/Project/260504_엠키스코어_은서모션촬영/260504_엠키스코어_은서모션촬영.unity
(Stored with Git LFS)
Binary file not shown.
BIN
Assets/ResourcesData/Project/260513_숙희님방송/260513_숙희님방송.unity
(Stored with Git LFS)
BIN
Assets/ResourcesData/Project/260513_숙희님방송/260513_숙희님방송.unity
(Stored with Git LFS)
Binary file not shown.
BIN
Assets/ResourcesData/Project/260515_모션촬영/260515_모션촬영.unity
(Stored with Git LFS)
BIN
Assets/ResourcesData/Project/260515_모션촬영/260515_모션촬영.unity
(Stored with Git LFS)
Binary file not shown.
BIN
Assets/ResourcesData/Project/260526_모션촬영/260526_모션촬영.unity
(Stored with Git LFS)
BIN
Assets/ResourcesData/Project/260526_모션촬영/260526_모션촬영.unity
(Stored with Git LFS)
Binary file not shown.
BIN
Assets/ResourcesData/Project/260530_텔라님_녹화방송/260530_텔라님_녹화방송.unity
(Stored with Git LFS)
BIN
Assets/ResourcesData/Project/260530_텔라님_녹화방송/260530_텔라님_녹화방송.unity
(Stored with Git LFS)
Binary file not shown.
BIN
Assets/ResourcesData/Project/260530_텔라님_녹화방송/260530_텔라님_녹화방송_콘서트장A.unity
(Stored with Git LFS)
BIN
Assets/ResourcesData/Project/260530_텔라님_녹화방송/260530_텔라님_녹화방송_콘서트장A.unity
(Stored with Git LFS)
Binary file not shown.
BIN
Assets/ResourcesData/Project/260531_이노리님_방송/260531_이노리님_방송_무대A.unity
(Stored with Git LFS)
BIN
Assets/ResourcesData/Project/260531_이노리님_방송/260531_이노리님_방송_무대A.unity
(Stored with Git LFS)
Binary file not shown.
BIN
Assets/ResourcesData/Project/260531_이노리님_방송/260531_이노리님_방송_중세성당.unity
(Stored with Git LFS)
BIN
Assets/ResourcesData/Project/260531_이노리님_방송/260531_이노리님_방송_중세성당.unity
(Stored with Git LFS)
Binary file not shown.
BIN
Assets/ResourcesData/Project/260605_치요님방송/260605_치요님방송_학교옥상.unity
(Stored with Git LFS)
BIN
Assets/ResourcesData/Project/260605_치요님방송/260605_치요님방송_학교옥상.unity
(Stored with Git LFS)
Binary file not shown.
BIN
Assets/ResourcesData/Project/260618_모코님방송/260618_모코님방송.unity
(Stored with Git LFS)
BIN
Assets/ResourcesData/Project/260618_모코님방송/260618_모코님방송.unity
(Stored with Git LFS)
Binary file not shown.
BIN
Assets/ResourcesData/Project/260618_모코님방송/260618_모코님방송_웨딩풍선배경.unity
(Stored with Git LFS)
BIN
Assets/ResourcesData/Project/260618_모코님방송/260618_모코님방송_웨딩풍선배경.unity
(Stored with Git LFS)
Binary file not shown.
BIN
Assets/ResourcesData/Project/260618_모코님방송/260618_모코님방송_웨딩풍선배경2.unity
(Stored with Git LFS)
BIN
Assets/ResourcesData/Project/260618_모코님방송/260618_모코님방송_웨딩풍선배경2.unity
(Stored with Git LFS)
Binary file not shown.
BIN
Assets/ResourcesData/Project/260620_숙희님방송/260620_숙희님방송.unity
(Stored with Git LFS)
BIN
Assets/ResourcesData/Project/260620_숙희님방송/260620_숙희님방송.unity
(Stored with Git LFS)
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user