using UnityEngine; using UnityEditor; using UnityEngine.UIElements; using UnityEditor.UIElements; using System.Collections.Generic; [CustomPropertyDrawer(typeof(StreamingleFacialReceiver.BlendShapeIntensityOverride))] public class BlendShapeIntensityOverrideDrawer : PropertyDrawer { private static readonly string[] ARKitBlendShapeNames = new string[] { // Eye "EyeBlinkLeft", "EyeBlinkRight", "EyeLookDownLeft", "EyeLookDownRight", "EyeLookInLeft", "EyeLookInRight", "EyeLookOutLeft", "EyeLookOutRight", "EyeLookUpLeft", "EyeLookUpRight", "EyeSquintLeft", "EyeSquintRight", "EyeWideLeft", "EyeWideRight", // Jaw "JawForward", "JawLeft", "JawRight", "JawOpen", // Mouth "MouthClose", "MouthFunnel", "MouthPucker", "MouthLeft", "MouthRight", "MouthSmileLeft", "MouthSmileRight", "MouthFrownLeft", "MouthFrownRight", "MouthDimpleLeft", "MouthDimpleRight", "MouthStretchLeft", "MouthStretchRight", "MouthRollLower", "MouthRollUpper", "MouthShrugLower", "MouthShrugUpper", "MouthPressLeft", "MouthPressRight", "MouthLowerDownLeft", "MouthLowerDownRight", "MouthUpperUpLeft", "MouthUpperUpRight", // Brow "BrowDownLeft", "BrowDownRight", "BrowInnerUp", "BrowOuterUpLeft", "BrowOuterUpRight", // Cheek/Nose "CheekPuff", "CheekSquintLeft", "CheekSquintRight", "NoseSneerLeft", "NoseSneerRight", // Tongue "TongueOut", }; private static readonly List DisplayNames; private static readonly Dictionary NameToIndex; static BlendShapeIntensityOverrideDrawer() { NameToIndex = new Dictionary(System.StringComparer.OrdinalIgnoreCase); DisplayNames = new List(ARKitBlendShapeNames.Length); for (int i = 0; i < ARKitBlendShapeNames.Length; i++) { string name = ARKitBlendShapeNames[i]; string category; if (name.StartsWith("Eye")) category = "Eye"; else if (name.StartsWith("Jaw")) category = "Jaw"; else if (name.StartsWith("Mouth")) category = "Mouth"; else if (name.StartsWith("Brow")) category = "Brow"; else if (name.StartsWith("Cheek") || name.StartsWith("Nose")) category = "Cheek-Nose"; else if (name.StartsWith("Tongue")) category = "Tongue"; else category = ""; DisplayNames.Add(category + "/" + name); NameToIndex[name] = i; } } private const string UssPath = "Assets/External/StreamingleFacial/Editor/UXML/StreamingleFacialReceiverEditor.uss"; public override VisualElement CreatePropertyGUI(SerializedProperty property) { var row = new VisualElement(); row.AddToClassList("blendshape-override-row"); var uss = AssetDatabase.LoadAssetAtPath(UssPath); if (uss != null) row.styleSheets.Add(uss); var nameProp = property.FindPropertyRelative("blendShapeName"); var intensityProp = property.FindPropertyRelative("intensity"); // Default value for new entries if (intensityProp.floatValue == 0f && string.IsNullOrEmpty(nameProp.stringValue)) { intensityProp.floatValue = 1.0f; nameProp.stringValue = ARKitBlendShapeNames[0]; property.serializedObject.ApplyModifiedPropertiesWithoutUndo(); } // Dropdown for ARKit blendshape selection int currentIndex = 0; if (!string.IsNullOrEmpty(nameProp.stringValue) && NameToIndex.TryGetValue(nameProp.stringValue, out int idx)) currentIndex = idx; var dropdown = new PopupField(DisplayNames, currentIndex); dropdown.AddToClassList("blendshape-override-dropdown"); dropdown.RegisterValueChangedCallback(evt => { int newIdx = DisplayNames.IndexOf(evt.newValue); if (newIdx >= 0 && newIdx < ARKitBlendShapeNames.Length) { nameProp.stringValue = ARKitBlendShapeNames[newIdx]; nameProp.serializedObject.ApplyModifiedProperties(); } }); row.Add(dropdown); // Slider for intensity var slider = new Slider(0f, 3f); slider.value = intensityProp.floatValue; slider.AddToClassList("blendshape-override-slider"); row.Add(slider); // Value label with color coding var valueLabel = new Label($"x{intensityProp.floatValue:F1}"); valueLabel.AddToClassList("blendshape-override-value"); UpdateValueLabelStyle(valueLabel, intensityProp.floatValue); row.Add(valueLabel); // Bind slider <-> property slider.RegisterValueChangedCallback(evt => { intensityProp.floatValue = evt.newValue; intensityProp.serializedObject.ApplyModifiedProperties(); valueLabel.text = $"x{evt.newValue:F1}"; UpdateValueLabelStyle(valueLabel, evt.newValue); }); // Track external changes to intensity row.TrackPropertyValue(intensityProp, prop => { slider.SetValueWithoutNotify(prop.floatValue); valueLabel.text = $"x{prop.floatValue:F1}"; UpdateValueLabelStyle(valueLabel, prop.floatValue); }); // Track external changes to blendShapeName row.TrackPropertyValue(nameProp, prop => { if (!string.IsNullOrEmpty(prop.stringValue) && NameToIndex.TryGetValue(prop.stringValue, out int newIdx)) { dropdown.SetValueWithoutNotify(DisplayNames[newIdx]); } }); return row; } private void UpdateValueLabelStyle(Label label, float value) { label.RemoveFromClassList("blendshape-value--low"); label.RemoveFromClassList("blendshape-value--normal"); label.RemoveFromClassList("blendshape-value--high"); if (value < 0.5f) label.AddToClassList("blendshape-value--low"); else if (value > 1.5f) label.AddToClassList("blendshape-value--high"); else label.AddToClassList("blendshape-value--normal"); } }