From 835e641b799810b935474d8e5b50a1397eb2e993 Mon Sep 17 00:00:00 2001 From: "qsxft258@gmail.com" Date: Wed, 13 May 2026 01:03:11 +0900 Subject: [PATCH] =?UTF-8?q?Fix=20:=20=ED=8E=98=EC=9D=B4=EC=85=9C=20?= =?UTF-8?q?=EB=B2=84=EA=B7=B8=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StreamingleFacialReceiver.cs | 234 ++++++++++++++++-- ProjectSettings/ProjectSettings.asset | 4 +- Streamingle_URP.slnx | 9 +- 3 files changed, 222 insertions(+), 25 deletions(-) diff --git a/Assets/External/StreamingleFacial/StreamingleFacialReceiver.cs b/Assets/External/StreamingleFacial/StreamingleFacialReceiver.cs index b8b685ec1..35d6a92e3 100644 --- a/Assets/External/StreamingleFacial/StreamingleFacialReceiver.cs +++ b/Assets/External/StreamingleFacial/StreamingleFacialReceiver.cs @@ -61,6 +61,7 @@ public class StreamingleFacialReceiver : MonoBehaviour 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); @@ -409,6 +410,85 @@ public class StreamingleFacialReceiver : MonoBehaviour } } + // Plugin 이 디스크에 쓴 등록 순서 파일을 파싱. Motive 의 description 재정렬 우회용. + // 파일 포맷: + // === CHANNEL DIAG '' tried=N lastIdx=M rejected=K === + // ORDER: [0]name0,[1]name1,... + // (빈 줄) + // Returns null if file missing/unreadable. + private const string kPluginDiagPath = "C:/Users/user/Documents/OptiTrack/iFacialMocap_diag.log"; + private Dictionary> _cachedPluginOrder = null; + private System.DateTime _cachedPluginOrderTime = System.DateTime.MinValue; + Dictionary> LoadPluginChannelOrder() + { + try + { + if (!System.IO.File.Exists(kPluginDiagPath)) return null; + var info = new System.IO.FileInfo(kPluginDiagPath); + if (_cachedPluginOrder != null && info.LastWriteTimeUtc == _cachedPluginOrderTime) + return _cachedPluginOrder; + + var result = new Dictionary>(); + string[] lines = System.IO.File.ReadAllLines(kPluginDiagPath); + string currentDevice = null; + foreach (var raw in lines) + { + string line = raw.Trim(); + if (line.StartsWith("=== CHANNEL DIAG '")) + { + int nameStart = line.IndexOf('\'') + 1; + int nameEnd = line.IndexOf('\'', nameStart); + if (nameEnd > nameStart) currentDevice = line.Substring(nameStart, nameEnd - nameStart); + } + else if (currentDevice != null && line.StartsWith("ORDER:")) + { + string body = line.Substring("ORDER:".Length).Trim(); + var names = new List(64); + // 각 토큰: "[N]name" + foreach (var tok in body.Split(',')) + { + string t = tok.Trim(); + int rb = t.IndexOf(']'); + if (rb > 0 && rb + 1 < t.Length) names.Add(t.Substring(rb + 1)); + } + result[currentDevice] = names; + currentDevice = null; + } + } + _cachedPluginOrder = result; + _cachedPluginOrderTime = info.LastWriteTimeUtc; + Debug.Log($"[Plugin Order] loaded {result.Count} device(s) from diag.log; e.g. primary jawOpen at idx {(result.Values.GetEnumerator().MoveNext() ? FindNameIndex(result, "jawOpen") : -1)}"); + return result; + } + catch (System.Exception e) + { + Debug.LogWarning($"[Plugin Order] read failed: {e.Message}"); + return null; + } + } + static int FindNameIndex(Dictionary> dict, string name) + { + foreach (var kv in dict) for (int i = 0; i < kv.Value.Count; i++) if (kv.Value[i] == name) return i; + return -1; + } + + // "NN_" -> idx=NN, raw=. Returns false if no valid 2-digit prefix. + static bool TryParseIdxPrefix(string s, out int idx, out string raw) + { + if (s != null && s.Length >= 3 && char.IsDigit(s[0]) && char.IsDigit(s[1]) && s[2] == '_') + { + idx = (s[0] - '0') * 10 + (s[1] - '0'); + raw = s.Substring(3); + return true; + } + idx = -1; raw = s; + return false; + } + static bool HasIdxPrefix(string s) + { + return s != null && s.Length >= 3 && char.IsDigit(s[0]) && char.IsDigit(s[1]) && s[2] == '_'; + } + void PollNatNetDevice() { if (natnetStreamingClient == null || natnetDeviceListener == null) return; @@ -423,31 +503,79 @@ public class StreamingleFacialReceiver : MonoBehaviour Time.unscaledTime - natnetLastResolveAttempt > k_NatnetResolveRetrySec) { natnetLastResolveAttempt = Time.unscaledTime; + if (portChanged) natnetDiagLogged = false; natnetLastResolvedFor = baseName; natnetResolvedDevices.Clear(); - var defs = natnetStreamingClient.GetDeviceDefinitions(); - if (defs != null) + // Plugin 이 디스크에 적은 등록 순서를 우선 사용 (Motive 가 description 을 재정렬해서 + // description 으로는 wire 와 정렬할 수 없음). 파일이 있으면 ChannelNames 를 덮어씀. + var pluginOrderByName = LoadPluginChannelOrder(); + + // 2026-05-13: 'gi == wireIndex' 가정이 stale device 환경에서 깨짐 (description 에 + // 남아있는데 wire 에선 빠진 device 가 있으면 인덱스가 어긋남). wire 의 sentinel + // 값 (= port 번호) 으로 직접 매칭하는 fast path 우선 시도. plugin order 파일이 + // 없을 때만 옛 defs 기반 fallback. + if (pluginOrderByName != null && pluginOrderByName.Count > 0) { - // Match base name (primary device) and ANY device whose name starts with - // baseName + "_" (overflow / future suffixes like _A, _B, _overflow). - // Order matters for downstream merging: primary first, then secondaries. - ResolvedDevice? primary = null; - List secondaries = new List(); - for (int gi = 0; gi < defs.Count; gi++) + int port = LOCAL_PORT; + string primaryDevName = $"iFacialMocap_{port}"; + string overflowDevName = $"iFacialMocap_{port}_overflow"; + List primaryNames = pluginOrderByName.TryGetValue(primaryDevName, out var pn) ? pn : null; + List overflowNames = pluginOrderByName.TryGetValue(overflowDevName, out var on) ? on : null; + + int wireCount = natnetDeviceListener.GetWireDeviceCount(); + int primaryWire = -1, overflowWire = -1; + int primaryWireLen = 0, overflowWireLen = 0; + for (int wi = 0; wi < wireCount; wi++) { - var d = defs[gi]; - if (d.Name == baseName) + float[] vals = natnetDeviceListener.GetDeviceChannelsByIndex(wi); + if (vals == null || vals.Length < 1) continue; + int sentinel = Mathf.RoundToInt(vals[0]); + if (sentinel != port) continue; + // 같은 port 의 wire device 중 채널 수가 큰 게 primary, 작은 게 overflow. + // (현 plugin: primary=32 wire-truncated, overflow=24) + if (vals.Length > primaryWireLen) { - primary = new ResolvedDevice { Id = d.Id, WireIndex = gi, ChannelNames = d.ChannelNames }; + // 기존 primary 후보가 있었다면 overflow 로 강등 + if (primaryWire >= 0 && primaryWireLen > overflowWireLen) + { + overflowWire = primaryWire; + overflowWireLen = primaryWireLen; + } + primaryWire = wi; + primaryWireLen = vals.Length; } - else if (d.Name.StartsWith(baseName + "_")) + else if (vals.Length > overflowWireLen) { - secondaries.Add(new ResolvedDevice { Id = d.Id, WireIndex = gi, ChannelNames = d.ChannelNames }); + overflowWire = wi; + overflowWireLen = vals.Length; } } - if (primary.HasValue) natnetResolvedDevices.Add(primary.Value); - natnetResolvedDevices.AddRange(secondaries); + + if (primaryNames != null && primaryWire >= 0) + natnetResolvedDevices.Add(new ResolvedDevice { Id = 0, WireIndex = primaryWire, ChannelNames = primaryNames }); + if (overflowNames != null && overflowWire >= 0) + natnetResolvedDevices.Add(new ResolvedDevice { Id = 0, WireIndex = overflowWire, ChannelNames = overflowNames }); + } + else + { + // Fallback: defs 기반 resolve (plugin order 파일 없거나 비어있을 때) + var defs = natnetStreamingClient.GetDeviceDefinitions(); + if (defs != null) + { + ResolvedDevice? primary = null; + List secondaries = new List(); + for (int gi = 0; gi < defs.Count; gi++) + { + var d = defs[gi]; + if (d.Name == baseName) + primary = new ResolvedDevice { Id = d.Id, WireIndex = gi, ChannelNames = d.ChannelNames }; + else if (d.Name.StartsWith(baseName + "_")) + secondaries.Add(new ResolvedDevice { Id = d.Id, WireIndex = gi, ChannelNames = d.ChannelNames }); + } + if (primary.HasValue) natnetResolvedDevices.Add(primary.Value); + natnetResolvedDevices.AddRange(secondaries); + } } if (natnetLastSeenSeq.Length < natnetResolvedDevices.Count) natnetLastSeenSeq = new ulong[natnetResolvedDevices.Count]; @@ -482,22 +610,84 @@ public class StreamingleFacialReceiver : MonoBehaviour float[] vals = natnetDeviceListener.GetDeviceChannelsByIndex(rd.WireIndex); if (vals == null || rd.ChannelNames == null) continue; - int n = Math.Min(vals.Length, rd.ChannelNames.Count); - for (int c = 0; c < n; c++) + // 2026-05-13: 이름에 "NN_" prefix 가 박혀있으면 그게 wire 슬롯 인덱스. Motive 의 + // description 재정렬에 영향 안 받는 self-describing 방식. prefix 없으면 legacy + // (positional) 페어링 + sentinel-based valOffset fallback. + int valOffset = 0; + bool usePrefixedMode = rd.ChannelNames.Count > 0 && HasIdxPrefix(rd.ChannelNames[0]); + if (!usePrefixedMode) + { + bool namesIncludeSentinel = false; + if (rd.ChannelNames.Count > 0) + { + string n0 = rd.ChannelNames[0]; + if (n0 == "sentinelPort" || n0 == "head_posX") namesIncludeSentinel = true; + } + if (!namesIncludeSentinel && vals.Length > 0) + { + int v0 = Mathf.RoundToInt(vals[0]); + if (v0 >= 40001 && v0 <= 40010) valOffset = 1; + } + } + + 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}] descLen={rd.ChannelNames.Count} valsLen={vals.Length} valOffset={valOffset} firstNames=[{string.Join(",", nameArr)}] firstVals=[{string.Join(",", valDump)}]"); + // 전체 이름 덤프 — plugin 등록순 vs Motive description 정합성 검증용. fallback (Motive desc) + // 일 때만 의미있음. plugin order override 활성 시 자기 자신 vs 자기 자신이라 skip. + var rawDefs = natnetStreamingClient != null ? natnetStreamingClient.GetDeviceDefinitions() : null; + if (rawDefs != null && di < rawDefs.Count) + { + var motiveDescNames = rawDefs[rd.WireIndex].ChannelNames; + if (motiveDescNames != null) + { + var indexed = new string[motiveDescNames.Count]; + for (int k = 0; k < motiveDescNames.Count; k++) indexed[k] = "[" + k + "]" + motiveDescNames[k]; + Debug.Log($"[Motive Desc Full] dev[{di}] WireIdx={rd.WireIndex}: {string.Join(",", indexed)}"); + } + } + } + + int loopEnd = usePrefixedMode ? rd.ChannelNames.Count : Math.Min(vals.Length - valOffset, rd.ChannelNames.Count); + for (int c = 0; c < loopEnd; c++) { string name = rd.ChannelNames[c]; if (string.IsNullOrEmpty(name)) continue; - if (name[0] == 'h' && name.StartsWith("head_")) continue; - if (name[0] == 'l' && name.StartsWith("leftEye_")) continue; - if (name[0] == 'r' && name.StartsWith("rightEye_")) continue; + + int wireSlot; + string rawName; + if (usePrefixedMode && TryParseIdxPrefix(name, out int parsedIdx, out string parsedRaw)) + { + wireSlot = parsedIdx; + rawName = parsedRaw; + } + else + { + wireSlot = c + valOffset; + rawName = name; + } + + if (rawName[0] == 'h' && rawName.StartsWith("head_")) continue; + if (rawName[0] == 'l' && rawName.StartsWith("leftEye_")) continue; + if (rawName[0] == 'r' && rawName.StartsWith("rightEye_")) continue; + if (rawName == "sentinelPort" || rawName == "head_posX") continue; + + if (wireSlot < 0 || wireSlot >= vals.Length) continue; // wire-truncated 채널 if (!first) natnetMsgBuilder.Append('|'); - natnetMsgBuilder.Append(name); + natnetMsgBuilder.Append(rawName); natnetMsgBuilder.Append('-'); - natnetMsgBuilder.Append(vals[c].ToString("0.####", CultureInfo.InvariantCulture)); + natnetMsgBuilder.Append(vals[wireSlot].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"); diff --git a/ProjectSettings/ProjectSettings.asset b/ProjectSettings/ProjectSettings.asset index b99a764b2..a7cf7af89 100644 --- a/ProjectSettings/ProjectSettings.asset +++ b/ProjectSettings/ProjectSettings.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef284afbce129b3f7d9cbfa111112ee95d6a2950fe20ddea27762dd1da5144ae -size 25769 +oid sha256:a944986499a5a09bb44eafc1281dbc22b1c13f650a95150c2e9a8e95a69a5ab5 +size 25934 diff --git a/Streamingle_URP.slnx b/Streamingle_URP.slnx index e1ba5f797..ffb3e8f6f 100644 --- a/Streamingle_URP.slnx +++ b/Streamingle_URP.slnx @@ -1,7 +1,7 @@  + - @@ -14,6 +14,7 @@ + @@ -31,6 +32,7 @@ + @@ -50,6 +52,7 @@ + @@ -58,10 +61,14 @@ + + + +