Fix : 페이셜 버그 패치
This commit is contained in:
parent
835e641b79
commit
edf6896d65
@ -410,84 +410,33 @@ 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()
|
||||
// 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[]
|
||||
{
|
||||
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] == '_';
|
||||
}
|
||||
"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",
|
||||
};
|
||||
|
||||
void PollNatNetDevice()
|
||||
{
|
||||
@ -507,76 +456,40 @@ public class StreamingleFacialReceiver : MonoBehaviour
|
||||
natnetLastResolvedFor = baseName;
|
||||
natnetResolvedDevices.Clear();
|
||||
|
||||
// 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)
|
||||
// 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;
|
||||
int wireCount = natnetDeviceListener.GetWireDeviceCount();
|
||||
var wireCandidates = new List<(int wi, int len)>();
|
||||
for (int wi = 0; wi < wireCount; wi++)
|
||||
{
|
||||
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++)
|
||||
{
|
||||
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 후보가 있었다면 overflow 로 강등
|
||||
if (primaryWire >= 0 && primaryWireLen > overflowWireLen)
|
||||
{
|
||||
overflowWire = primaryWire;
|
||||
overflowWireLen = primaryWireLen;
|
||||
}
|
||||
primaryWire = wi;
|
||||
primaryWireLen = vals.Length;
|
||||
}
|
||||
else if (vals.Length > overflowWireLen)
|
||||
{
|
||||
overflowWire = wi;
|
||||
overflowWireLen = vals.Length;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
float[] vals = natnetDeviceListener.GetDeviceChannelsByIndex(wi);
|
||||
if (vals == null || vals.Length < 1) continue;
|
||||
int sentinel = Mathf.RoundToInt(vals[0]);
|
||||
if (sentinel != port) continue;
|
||||
wireCandidates.Add((wi, vals.Length));
|
||||
}
|
||||
wireCandidates.Sort((a, b) => b.len.CompareTo(a.len));
|
||||
|
||||
// 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 = 0, WireIndex = wireCandidates[0].wi, ChannelNames = primarySlice });
|
||||
if (wireCandidates.Count > 1)
|
||||
natnetResolvedDevices.Add(new ResolvedDevice { Id = 0, WireIndex = wireCandidates[1].wi, ChannelNames = overflowSlice });
|
||||
if (natnetLastSeenSeq.Length < natnetResolvedDevices.Count)
|
||||
natnetLastSeenSeq = new ulong[natnetResolvedDevices.Count];
|
||||
}
|
||||
@ -599,9 +512,7 @@ public class StreamingleFacialReceiver : MonoBehaviour
|
||||
if (!anyNew) return;
|
||||
|
||||
// iFacialMocap 와이어 포맷 합성: name-value|name-value|...=
|
||||
// 채널값은 NatnetDeviceListener (wire 직접 파싱) 에서, 채널 이름은 description에서.
|
||||
// **WIRE INDEX 기반 매칭** — Motive가 모든 plugin device id를 0으로 broadcasting하는
|
||||
// 버그 회피. description 순서 = wire 순서 가정 (Motive registration order).
|
||||
// 채널 이름은 canonical 리스트(plugin과 공유)에서, 값은 NatnetDeviceListener wire 파싱.
|
||||
natnetMsgBuilder.Length = 0;
|
||||
bool first = true;
|
||||
for (int di = 0; di < natnetResolvedDevices.Count; di++)
|
||||
@ -610,26 +521,6 @@ public class StreamingleFacialReceiver : MonoBehaviour
|
||||
float[] vals = natnetDeviceListener.GetDeviceChannelsByIndex(rd.WireIndex);
|
||||
if (vals == null || rd.ChannelNames == null) continue;
|
||||
|
||||
// 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);
|
||||
@ -638,52 +529,23 @@ public class StreamingleFacialReceiver : MonoBehaviour
|
||||
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)}");
|
||||
}
|
||||
}
|
||||
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)}]");
|
||||
}
|
||||
|
||||
int loopEnd = usePrefixedMode ? rd.ChannelNames.Count : Math.Min(vals.Length - valOffset, rd.ChannelNames.Count);
|
||||
for (int c = 0; c < loopEnd; c++)
|
||||
// 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;
|
||||
|
||||
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 (name == "sentinelPort") continue;
|
||||
|
||||
if (!first) natnetMsgBuilder.Append('|');
|
||||
natnetMsgBuilder.Append(rawName);
|
||||
natnetMsgBuilder.Append(name);
|
||||
natnetMsgBuilder.Append('-');
|
||||
natnetMsgBuilder.Append(vals[wireSlot].ToString("0.####", CultureInfo.InvariantCulture));
|
||||
natnetMsgBuilder.Append(vals[c].ToString("0.####", CultureInfo.InvariantCulture));
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user