194 lines
6.9 KiB
C#
194 lines
6.9 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 emaFields;
|
|
private VisualElement euroFields;
|
|
|
|
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");
|
|
emaFields = root.Q("emaFields");
|
|
euroFields = root.Q("euroFields");
|
|
|
|
// 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 filterMode for conditional visibility of EMA/Euro fields
|
|
var filterModeProp = serializedObject.FindProperty("filterMode");
|
|
UpdateFilterModeVisibility(filterModeProp.enumValueIndex);
|
|
|
|
root.TrackPropertyValue(filterModeProp, prop =>
|
|
{
|
|
UpdateFilterModeVisibility(prop.enumValueIndex);
|
|
});
|
|
|
|
// 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 UpdateFilterModeVisibility(int modeIndex)
|
|
{
|
|
// FilterMode: 0=None, 1=EMA, 2=OneEuro
|
|
if (emaFields != null)
|
|
emaFields.style.display = modeIndex == 1 ? DisplayStyle.Flex : DisplayStyle.None;
|
|
if (euroFields != null)
|
|
euroFields.style.display = modeIndex == 2 ? 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");
|
|
}
|
|
}
|