Fix : 페이셜 버그 업데이트
This commit is contained in:
parent
f5b6690aee
commit
835e641b79
@ -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 '<deviceName>' 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<string, List<string>> _cachedPluginOrder = null;
|
||||
private System.DateTime _cachedPluginOrderTime = System.DateTime.MinValue;
|
||||
Dictionary<string, List<string>> 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, List<string>>();
|
||||
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<string>(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<string, List<string>> 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_<name>" -> idx=NN, raw=<name>. 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<ResolvedDevice> secondaries = new List<ResolvedDevice>();
|
||||
for (int gi = 0; gi < defs.Count; gi++)
|
||||
int port = LOCAL_PORT;
|
||||
string primaryDevName = $"iFacialMocap_{port}";
|
||||
string overflowDevName = $"iFacialMocap_{port}_overflow";
|
||||
List<string> primaryNames = pluginOrderByName.TryGetValue(primaryDevName, out var pn) ? pn : null;
|
||||
List<string> 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<ResolvedDevice> secondaries = new List<ResolvedDevice>();
|
||||
for (int gi = 0; gi < defs.Count; gi++)
|
||||
{
|
||||
var d = defs[gi];
|
||||
if (d.Name == baseName)
|
||||
primary = new ResolvedDevice { Id = d.Id, WireIndex = gi, ChannelNames = d.ChannelNames };
|
||||
else if (d.Name.StartsWith(baseName + "_"))
|
||||
secondaries.Add(new ResolvedDevice { Id = d.Id, WireIndex = gi, ChannelNames = d.ChannelNames });
|
||||
}
|
||||
if (primary.HasValue) natnetResolvedDevices.Add(primary.Value);
|
||||
natnetResolvedDevices.AddRange(secondaries);
|
||||
}
|
||||
}
|
||||
if (natnetLastSeenSeq.Length < natnetResolvedDevices.Count)
|
||||
natnetLastSeenSeq = new ulong[natnetResolvedDevices.Count];
|
||||
@ -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");
|
||||
|
||||
BIN
ProjectSettings/ProjectSettings.asset
(Stored with Git LFS)
BIN
ProjectSettings/ProjectSettings.asset
(Stored with Git LFS)
Binary file not shown.
@ -1,7 +1,7 @@
|
||||
<Solution>
|
||||
<Project Path="AmplifyShaderEditor.csproj" />
|
||||
<Project Path="Assembly-CSharp-Editor.csproj" />
|
||||
<Project Path="Assembly-CSharp.csproj" />
|
||||
<Project Path="AmplifyShaderEditor.csproj" />
|
||||
<Project Path="lilToon.Editor.csproj" />
|
||||
<Project Path="MagicaClothV2.csproj" />
|
||||
<Project Path="Unity.InternalAPIEditorBridge.020.csproj" />
|
||||
@ -14,6 +14,7 @@
|
||||
<Project Path="NiloToon.NiloToonURP.Runtime.csproj" />
|
||||
<Project Path="MagicaCloth2Example.csproj" />
|
||||
<Project Path="UniGLTF.Editor.csproj" />
|
||||
<Project Path="CFXRDemo.csproj" />
|
||||
<Project Path="FullscreenEditor.csproj" />
|
||||
<Project Path="VRMShaders.VRM.IO.Runtime.csproj" />
|
||||
<Project Path="UniVRM.Editor.csproj" />
|
||||
@ -31,6 +32,7 @@
|
||||
<Project Path="UniGLTF.Tests.csproj" />
|
||||
<Project Path="uOSC.Runtime.csproj" />
|
||||
<Project Path="PlanarReflections5.csproj" />
|
||||
<Project Path="CFXRRuntime.csproj" />
|
||||
<Project Path="NiloToon.NiloToonURP.Shaders.csproj" />
|
||||
<Project Path="MK.Glow.URP.Editor.csproj" />
|
||||
<Project Path="VRMShaders.GLTF.IO.Editor.csproj" />
|
||||
@ -50,6 +52,7 @@
|
||||
<Project Path="HBAO.Editor.csproj" />
|
||||
<Project Path="PlanarReflections5_Editor.csproj" />
|
||||
<Project Path="HBAO.Runtime.csproj" />
|
||||
<Project Path="ScreenSpaceReflections.csproj" />
|
||||
<Project Path="VRMShaders.GLTF.IO.Tests.csproj" />
|
||||
<Project Path="UniHumanoid.Editor.Tests.csproj" />
|
||||
<Project Path="LWGUI.Runtime.csproj" />
|
||||
@ -58,10 +61,14 @@
|
||||
<Project Path="NiloToon.NiloToonURP.ShaderLibrary.csproj" />
|
||||
<Project Path="VRMShaders.VRM10.Format.Runtime.csproj" />
|
||||
<Project Path="Klak.Hap.csproj" />
|
||||
<Project Path="CFXREditor.csproj" />
|
||||
<Project Path="KinoBloom.Runtime.csproj" />
|
||||
<Project Path="VRM.Samples.Editor.Tests.csproj" />
|
||||
<Project Path="HBAO.Universal.Editor.csproj" />
|
||||
<Project Path="CFXR.WelcomeScreen.csproj" />
|
||||
<Project Path="lilToon.Editor.External.csproj" />
|
||||
<Project Path="LWGUI.Timeline.csproj" />
|
||||
<Project Path="ToonyColorsPro.Demo.Editor.csproj" />
|
||||
<Project Path="UniGLTF.Samples.ScreenSpace.csproj" />
|
||||
<Project Path="LWGUI.Timeline.Editor.csproj" />
|
||||
<Project Path="Klak.Hap.Editor.csproj" />
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user