- 모든 컨트롤러 에디터를 IMGUI → UI Toolkit(UXML/USS)으로 전환 (Camera, Item, Event, Avatar, System, StreamDeck, OptiTrack, Facial) - StreamingleCommon.uss 공통 테마 + 개별 에디터 USS 스타일시트 - SystemController 서브매니저 분리 (OptiTrack, Facial, Recording, Screenshot 등) - 런타임 컨트롤 패널 (ESC 토글, 좌측 오버레이, 150% 스케일) - 웹 대시보드 서버 (StreamingleDashboardServer) + 리타게팅 통합 - 설정 도구(StreamingleControllerSetupTool) UXML 재작성 + 원클릭 설정 - SimplePoseTransfer UXML 에디터 추가 - 전체 UXML 한글화 + NanumGothic 폰트 적용 - Streamingle.Debug → Streamingle.Debugging 네임스페이스 변경 (Debug.Log 충돌 해결) - 불필요 코드 제거 (rawkey.cs, RetargetingHTTPServer, OptitrackSkeletonAnimator 등) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
189 lines
6.7 KiB
C#
189 lines
6.7 KiB
C#
using UnityEngine;
|
|
using UnityEditor;
|
|
using UnityEngine.UIElements;
|
|
using UnityEditor.UIElements;
|
|
using System.Collections.Generic;
|
|
|
|
[CustomEditor(typeof(StreamingleFacialReceiver))]
|
|
public class StreamingleFacialReceiverEditor : Editor
|
|
{
|
|
private const string UxmlPath = "Assets/External/StreamingleFacial/Editor/UXML/StreamingleFacialReceiverEditor.uxml";
|
|
private const string UssPath = "Assets/External/StreamingleFacial/Editor/UXML/StreamingleFacialReceiverEditor.uss";
|
|
private const string CommonUssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss";
|
|
|
|
private StreamingleFacialReceiver receiver;
|
|
private VisualElement portButtonsContainer;
|
|
private Label activePortValue;
|
|
private VisualElement statusContainer;
|
|
private VisualElement filteringFields;
|
|
|
|
public override VisualElement CreateInspectorGUI()
|
|
{
|
|
receiver = (StreamingleFacialReceiver)target;
|
|
var root = new VisualElement();
|
|
|
|
// Load stylesheets
|
|
var commonUss = AssetDatabase.LoadAssetAtPath<StyleSheet>(CommonUssPath);
|
|
if (commonUss != null) root.styleSheets.Add(commonUss);
|
|
|
|
var uss = AssetDatabase.LoadAssetAtPath<StyleSheet>(UssPath);
|
|
if (uss != null) root.styleSheets.Add(uss);
|
|
|
|
// Load UXML
|
|
var uxml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(UxmlPath);
|
|
if (uxml != null) uxml.CloneTree(root);
|
|
|
|
// Cache references
|
|
statusContainer = root.Q("statusContainer");
|
|
activePortValue = root.Q<Label>("activePortValue");
|
|
portButtonsContainer = root.Q("portButtonsContainer");
|
|
filteringFields = root.Q("filteringFields");
|
|
|
|
// Auto-find button
|
|
var autoFindBtn = root.Q<Button>("autoFindBtn");
|
|
var autoFindResult = root.Q<Label>("autoFindResult");
|
|
if (autoFindBtn != null)
|
|
autoFindBtn.clicked += () => AutoFindARKitMeshes(autoFindResult);
|
|
|
|
// Build dynamic port buttons
|
|
RebuildPortButtons();
|
|
|
|
// Track enableFiltering for conditional visibility
|
|
var enableFilteringProp = serializedObject.FindProperty("enableFiltering");
|
|
UpdateFilteringVisibility(enableFilteringProp.boolValue);
|
|
|
|
root.TrackPropertyValue(enableFilteringProp, prop =>
|
|
{
|
|
UpdateFilteringVisibility(prop.boolValue);
|
|
});
|
|
|
|
// Track availablePorts and activePortIndex changes to rebuild port buttons
|
|
var portsProp = serializedObject.FindProperty("availablePorts");
|
|
root.TrackPropertyValue(portsProp, _ => RebuildPortButtons());
|
|
|
|
var activeIndexProp = serializedObject.FindProperty("activePortIndex");
|
|
root.TrackPropertyValue(activeIndexProp, _ => RebuildPortButtons());
|
|
|
|
// Play mode status polling
|
|
root.schedule.Execute(UpdatePlayModeState).Every(200);
|
|
|
|
return root;
|
|
}
|
|
|
|
private void UpdateFilteringVisibility(bool enabled)
|
|
{
|
|
if (filteringFields == null) return;
|
|
filteringFields.style.display = enabled ? DisplayStyle.Flex : DisplayStyle.None;
|
|
}
|
|
|
|
private void RebuildPortButtons()
|
|
{
|
|
if (portButtonsContainer == null || receiver == null) return;
|
|
portButtonsContainer.Clear();
|
|
|
|
// Update active port label
|
|
if (activePortValue != null)
|
|
activePortValue.text = receiver.LOCAL_PORT.ToString();
|
|
|
|
if (receiver.availablePorts == null || receiver.availablePorts.Length == 0) return;
|
|
|
|
for (int i = 0; i < receiver.availablePorts.Length; i++)
|
|
{
|
|
int idx = i;
|
|
var btn = new Button(() => OnPortButtonClicked(idx))
|
|
{
|
|
text = receiver.availablePorts[i].ToString()
|
|
};
|
|
btn.AddToClassList("facial-port-btn");
|
|
|
|
if (i == receiver.activePortIndex)
|
|
btn.AddToClassList("facial-port-btn--active");
|
|
|
|
portButtonsContainer.Add(btn);
|
|
}
|
|
}
|
|
|
|
private void OnPortButtonClicked(int index)
|
|
{
|
|
if (Application.isPlaying)
|
|
{
|
|
receiver.SwitchToPort(index);
|
|
}
|
|
else
|
|
{
|
|
Undo.RecordObject(target, "Switch Port");
|
|
receiver.activePortIndex = index;
|
|
EditorUtility.SetDirty(target);
|
|
}
|
|
serializedObject.Update();
|
|
RebuildPortButtons();
|
|
}
|
|
|
|
// ARKit 블렌드셰이프 이름 (감지용 최소 세트)
|
|
private static readonly HashSet<string> ARKitNames = new HashSet<string>(System.StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
"EyeBlinkLeft", "EyeBlinkRight", "JawOpen", "MouthClose",
|
|
"MouthSmileLeft", "MouthSmileRight", "BrowDownLeft", "BrowDownRight",
|
|
"EyeWideLeft", "EyeWideRight", "MouthFunnel", "MouthPucker",
|
|
"CheekPuff", "TongueOut", "NoseSneerLeft", "NoseSneerRight",
|
|
// _L/_R 변형
|
|
"eyeBlink_L", "eyeBlink_R", "jawOpen", "mouthClose",
|
|
"mouthSmile_L", "mouthSmile_R", "browDown_L", "browDown_R",
|
|
};
|
|
|
|
private const int MinARKitMatchCount = 5;
|
|
|
|
private void AutoFindARKitMeshes(Label resultLabel)
|
|
{
|
|
if (receiver == null) return;
|
|
|
|
var allSMRs = receiver.GetComponentsInChildren<SkinnedMeshRenderer>(true);
|
|
var found = new List<SkinnedMeshRenderer>();
|
|
|
|
foreach (var smr in allSMRs)
|
|
{
|
|
if (smr == null || smr.sharedMesh == null) continue;
|
|
|
|
int matchCount = 0;
|
|
for (int i = 0; i < smr.sharedMesh.blendShapeCount; i++)
|
|
{
|
|
string name = smr.sharedMesh.GetBlendShapeName(i);
|
|
if (ARKitNames.Contains(name))
|
|
{
|
|
matchCount++;
|
|
if (matchCount >= MinARKitMatchCount) break;
|
|
}
|
|
}
|
|
|
|
if (matchCount >= MinARKitMatchCount)
|
|
found.Add(smr);
|
|
}
|
|
|
|
if (found.Count == 0)
|
|
{
|
|
if (resultLabel != null)
|
|
resultLabel.text = "ARKit 블렌드셰이프를 가진 메쉬를 찾지 못했습니다.";
|
|
return;
|
|
}
|
|
|
|
Undo.RecordObject(target, "Auto Find ARKit Meshes");
|
|
receiver.faceMeshRenderers = found.ToArray();
|
|
EditorUtility.SetDirty(target);
|
|
serializedObject.Update();
|
|
|
|
if (resultLabel != null)
|
|
resultLabel.text = $"{found.Count}개 메쉬 등록 완료";
|
|
}
|
|
|
|
private void UpdatePlayModeState()
|
|
{
|
|
if (statusContainer == null) return;
|
|
|
|
bool isPlaying = Application.isPlaying;
|
|
if (isPlaying && !statusContainer.ClassListContains("facial-status-container--visible"))
|
|
statusContainer.AddToClassList("facial-status-container--visible");
|
|
else if (!isPlaying && statusContainer.ClassListContains("facial-status-container--visible"))
|
|
statusContainer.RemoveFromClassList("facial-status-container--visible");
|
|
}
|
|
}
|