using UnityEngine; using UnityEditor; [CustomEditor(typeof(StreamingleFacialReceiver))] public class StreamingleFacialReceiverEditor : Editor { private SerializedProperty mirrorMode; private SerializedProperty faceMeshRenderers; private SerializedProperty availablePorts; private SerializedProperty enableFiltering; private SerializedProperty smoothingFactor; private SerializedProperty maxBlendShapeDelta; private SerializedProperty maxRotationDelta; private SerializedProperty fastBlendShapeMultiplier; private SerializedProperty spikeToleranceFrames; private SerializedProperty globalIntensity; private SerializedProperty blendShapeIntensityOverrides; private bool showConnection = false; private bool showFiltering = false; private bool showIntensity = false; private static readonly Color HeaderColor = new Color(0.18f, 0.18f, 0.18f, 1f); private static readonly Color ActivePortColor = new Color(0.2f, 0.8f, 0.4f, 1f); private static readonly Color InactivePortColor = new Color(0.35f, 0.35f, 0.35f, 1f); private static readonly Color AccentColor = new Color(0.4f, 0.7f, 1f, 1f); private GUIStyle _headerStyle; private GUIStyle _sectionBoxStyle; private GUIStyle _portButtonStyle; private GUIStyle _portButtonActiveStyle; private GUIStyle _statusLabelStyle; void OnEnable() { mirrorMode = serializedObject.FindProperty("mirrorMode"); faceMeshRenderers = serializedObject.FindProperty("faceMeshRenderers"); availablePorts = serializedObject.FindProperty("availablePorts"); enableFiltering = serializedObject.FindProperty("enableFiltering"); smoothingFactor = serializedObject.FindProperty("smoothingFactor"); maxBlendShapeDelta = serializedObject.FindProperty("maxBlendShapeDelta"); maxRotationDelta = serializedObject.FindProperty("maxRotationDelta"); fastBlendShapeMultiplier = serializedObject.FindProperty("fastBlendShapeMultiplier"); spikeToleranceFrames = serializedObject.FindProperty("spikeToleranceFrames"); globalIntensity = serializedObject.FindProperty("globalIntensity"); blendShapeIntensityOverrides = serializedObject.FindProperty("blendShapeIntensityOverrides"); } void InitStyles() { if (_headerStyle != null) return; _headerStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 13, alignment = TextAnchor.MiddleLeft, padding = new RectOffset(8, 0, 4, 4), }; _headerStyle.normal.textColor = AccentColor; _sectionBoxStyle = new GUIStyle("HelpBox") { padding = new RectOffset(10, 10, 8, 8), margin = new RectOffset(0, 0, 4, 8), }; _portButtonStyle = new GUIStyle(GUI.skin.button) { fontSize = 12, fontStyle = FontStyle.Bold, fixedHeight = 36, alignment = TextAnchor.MiddleCenter, }; _portButtonActiveStyle = new GUIStyle(_portButtonStyle); _portButtonActiveStyle.normal.textColor = Color.white; _statusLabelStyle = new GUIStyle(EditorStyles.miniLabel) { alignment = TextAnchor.MiddleCenter, fontSize = 10, }; } public override void OnInspectorGUI() { serializedObject.Update(); InitStyles(); var mocap = (StreamingleFacialReceiver)target; // 타이틀 EditorGUILayout.Space(4); DrawTitle(); EditorGUILayout.Space(4); // 기본 설정 DrawBasicSettings(); // 포트 핫스왑 DrawPortSection(mocap); // 필터링 DrawFilteringSection(); // 페이셜 강도 DrawIntensitySection(); serializedObject.ApplyModifiedProperties(); } void DrawTitle() { var rect = EditorGUILayout.GetControlRect(false, 32); EditorGUI.DrawRect(rect, HeaderColor); var titleStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 15, alignment = TextAnchor.MiddleLeft, }; titleStyle.normal.textColor = Color.white; EditorGUI.LabelField(rect, " Streamingle Facial Receiver", titleStyle); // 상태 표시 if (Application.isPlaying) { var statusRect = new Rect(rect.xMax - 80, rect.y, 75, rect.height); var dotRect = new Rect(statusRect.x - 12, rect.y + rect.height / 2 - 4, 8, 8); EditorGUI.DrawRect(dotRect, ActivePortColor); EditorGUI.LabelField(statusRect, "LIVE", _statusLabelStyle); } } void DrawBasicSettings() { EditorGUILayout.BeginVertical(_sectionBoxStyle); EditorGUILayout.PropertyField(faceMeshRenderers, new GUIContent("Face Mesh Renderers")); EditorGUILayout.Space(2); EditorGUILayout.PropertyField(mirrorMode, new GUIContent("Mirror Mode (L/R Flip)")); EditorGUILayout.EndVertical(); } void DrawPortSection(StreamingleFacialReceiver mocap) { showConnection = DrawSectionHeader("Port Hot-Swap", showConnection); if (!showConnection) return; EditorGUILayout.BeginVertical(_sectionBoxStyle); // 현재 포트 표시 EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Active Port", EditorStyles.miniLabel, GUILayout.Width(70)); var portLabel = new GUIStyle(EditorStyles.boldLabel); portLabel.normal.textColor = ActivePortColor; EditorGUILayout.LabelField(mocap.LOCAL_PORT.ToString(), portLabel); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(4); // 포트 버튼 그리드 if (mocap.availablePorts != null && mocap.availablePorts.Length > 0) { EditorGUILayout.BeginHorizontal(); for (int i = 0; i < mocap.availablePorts.Length; i++) { bool isActive = i == mocap.activePortIndex; var prevBg = GUI.backgroundColor; GUI.backgroundColor = isActive ? ActivePortColor : InactivePortColor; var style = isActive ? _portButtonActiveStyle : _portButtonStyle; string label = mocap.availablePorts[i].ToString(); if (GUILayout.Button(label, style)) { if (Application.isPlaying) { mocap.SwitchToPort(i); } else { Undo.RecordObject(mocap, "Switch Port"); mocap.activePortIndex = i; EditorUtility.SetDirty(mocap); } } GUI.backgroundColor = prevBg; } EditorGUILayout.EndHorizontal(); } EditorGUILayout.Space(2); EditorGUILayout.PropertyField(availablePorts, new GUIContent("Port List"), true); EditorGUILayout.EndVertical(); } void DrawFilteringSection() { showFiltering = DrawSectionHeader("Data Filtering", showFiltering); if (!showFiltering) return; EditorGUILayout.BeginVertical(_sectionBoxStyle); EditorGUILayout.PropertyField(enableFiltering, new GUIContent("Enable")); if (enableFiltering.boolValue) { EditorGUI.indentLevel++; EditorGUILayout.Space(2); EditorGUILayout.PropertyField(smoothingFactor, new GUIContent("Smoothing")); EditorGUILayout.PropertyField(maxBlendShapeDelta, new GUIContent("Max BlendShape Delta")); EditorGUILayout.PropertyField(maxRotationDelta, new GUIContent("Max Rotation Delta")); EditorGUILayout.PropertyField(fastBlendShapeMultiplier, new GUIContent("Fast BS Multiplier")); EditorGUILayout.PropertyField(spikeToleranceFrames, new GUIContent("Spike Tolerance")); EditorGUI.indentLevel--; } EditorGUILayout.EndVertical(); } void DrawIntensitySection() { showIntensity = DrawSectionHeader("Facial Intensity", showIntensity); if (!showIntensity) return; EditorGUILayout.BeginVertical(_sectionBoxStyle); // Global intensity - 크게 표시 EditorGUILayout.LabelField("Global", EditorStyles.miniLabel); EditorGUILayout.PropertyField(globalIntensity, GUIContent.none); EditorGUILayout.Space(6); // 구분선 var lineRect = EditorGUILayout.GetControlRect(false, 1); EditorGUI.DrawRect(lineRect, new Color(0.4f, 0.4f, 0.4f, 0.5f)); EditorGUILayout.Space(4); EditorGUILayout.LabelField("Per-BlendShape Overrides", EditorStyles.miniLabel); EditorGUILayout.PropertyField(blendShapeIntensityOverrides, GUIContent.none, true); EditorGUILayout.EndVertical(); } bool DrawSectionHeader(string title, bool expanded) { EditorGUILayout.Space(2); var rect = EditorGUILayout.GetControlRect(false, 24); EditorGUI.DrawRect(rect, HeaderColor); // 폴드 화살표 + 타이틀 var foldRect = new Rect(rect.x + 4, rect.y, rect.width - 4, rect.height); bool result = EditorGUI.Foldout(foldRect, expanded, " " + title, true, _headerStyle); return result; } }