// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) VRMADA, All rights reserved. // // -------------------------------------------------------------------------------------------------------------------- using System.Linq; using UltimateXR.Animation.IK; using UltimateXR.Avatar; using UltimateXR.Avatar.Controllers; using UltimateXR.Avatar.Rig; using UltimateXR.Editor.Animation.IK; using UltimateXR.Extensions.System.Collections; using UltimateXR.Extensions.Unity; using UltimateXR.Manipulation.HandPoses; using UnityEditor; using UnityEditorInternal; using UnityEngine; namespace UltimateXR.Editor.Avatar.Controllers { /// /// Custom inspector for the component. /// [CustomEditor(typeof(UxrStandardAvatarController))] public sealed class UxrStandardAvatarControllerEditor : UnityEditor.Editor { #region Public Types & Data public const string PropAllowHandTracking = "_allowHandTracking"; public const string PropUseArmIK = "_useArmIK"; public const string PropArmIKElbowAperture = "_armIKElbowAperture"; public const string PropArmIKOverExtendMode = "_armIKOverExtendMode"; public const string PropUseBodyIK = "_useBodyIK"; public const string PropBodyIKSettings = "_bodyIKSettings"; //public const string PropUseLegIK = "_useLegIK"; public const string PropListControllerEvents = "_listControllerEvents"; #endregion #region Unity /// /// Caches the serialized properties and initializes data. /// private void OnEnable() { UxrStandardAvatarController selectedController = (UxrStandardAvatarController)serializedObject.targetObject; UxrAvatar avatar = selectedController.Avatar; _propAllowHandTracking = serializedObject.FindProperty(PropAllowHandTracking); _propUseArmIK = serializedObject.FindProperty(PropUseArmIK); _propArmIKElbowAperture = serializedObject.FindProperty(PropArmIKElbowAperture); _propArmIKOverExtendMode = serializedObject.FindProperty(PropArmIKOverExtendMode); _propUseBodyIK = serializedObject.FindProperty(PropUseBodyIK); _propBodyIKSettings = serializedObject.FindProperty(PropBodyIKSettings); //_propUseLegIK = serializedObject.FindProperty(PropUseLegIK); _propListControllerEvents = serializedObject.FindProperty(PropListControllerEvents); if (_propListControllerEvents != null) { _reorderableEventList = CreateReorderableList(serializedObject, _propListControllerEvents, DrawControllerEventCallback); } // Get avatar info _reorderableEventList.elementHeightCallback = index => { if (index >= selectedController.ControllerEvents.Count) { return 0; } UxrHandPoseAsset handPoseAsset = avatar.GetHandPose(selectedController.ControllerEvents[index].PoseName); return EditorGUIUtility.singleLineHeight * 3.5f + (handPoseAsset && handPoseAsset.PoseType == UxrHandPoseType.Blend ? EditorGUIUtility.singleLineHeight : 0.0f); }; } /// /// Called by Unity to draw the inspector for the selected component(s). /// public override void OnInspectorGUI() { serializedObject.Update(); UxrAvatar avatar = ((UxrStandardAvatarController)serializedObject.targetObject).GetComponent(); // Handle UI EditorGUILayout.Space(); EditorGUILayout.Space(); _foldoutGeneral = UxrEditorUtils.FoldoutStylish("General", _foldoutGeneral); if (_foldoutGeneral) { EditorGUILayout.PropertyField(_propAllowHandTracking, ContentAllowHandTracking); } if (avatar.AvatarRigType == UxrAvatarRigType.HalfOrFullBody) { _foldoutIK = UxrEditorUtils.FoldoutStylish("Inverse Kinematics", _foldoutIK); if (_foldoutIK) { EditorGUILayout.PropertyField(_propUseArmIK, ContentUseArmIK); if (!avatar.AvatarRig.HasArmData()) { EditorGUILayout.HelpBox($"To use arm IK, the {nameof(UxrAvatar)} component needs arm references in the Avatar Rig section", MessageType.Warning); } else { if (_propUseArmIK.boolValue) { EditorGUI.indentLevel++; EditorGUI.BeginChangeCheck(); EditorGUILayout.Slider(_propArmIKElbowAperture, 0.0f, 1.0f, ContentArmIKElbowAperture); if (EditorGUI.EndChangeCheck() && Application.isPlaying) { UxrIKSolver.GetComponents(avatar, true).OfType().ForEach(s => s.RelaxedElbowAperture = _propArmIKElbowAperture.floatValue); } EditorGUI.BeginChangeCheck(); EditorGUILayout.PropertyField(_propArmIKOverExtendMode, ContentArmIKOverExtendMode); if (EditorGUI.EndChangeCheck() && Application.isPlaying) { UxrIKSolver.GetComponents(avatar, true).OfType().ForEach(s => s.OverExtendMode = (UxrArmOverExtendMode)_propArmIKOverExtendMode.enumValueIndex); } EditorGUI.indentLevel--; } } bool hasUpperBodyReferences = avatar.AvatarRig.HasAnyUpperBodyIKReference(); EditorGUILayout.PropertyField(_propUseBodyIK, ContentUseBodyIK); if (!hasUpperBodyReferences) { EditorGUILayout.HelpBox($"To use body IK, the {nameof(UxrAvatar)} component needs upper body references in the Avatar Rig section", MessageType.Warning); } GUI.enabled = hasUpperBodyReferences; if (_propUseBodyIK.boolValue && hasUpperBodyReferences) { EditorGUI.indentLevel++; EditorGUILayout.PropertyField(_propBodyIKSettings, ContentBodyIKSettings); EditorGUI.indentLevel--; } GUI.enabled = false; //EditorGUILayout.PropertyField(_propUseLegIK); EditorGUILayout.Toggle(ContentUseLegIK, false); GUI.enabled = true; } } _foldoutHandEvents = UxrEditorUtils.FoldoutStylish("Special Hand Pose Events", _foldoutHandEvents); if (_foldoutHandEvents) { EditorGUILayout.LabelField("Hand poses based on controller input events:", EditorStyles.boldLabel); if (string.IsNullOrEmpty(avatar.PrefabGuid) || !avatar.GetAllHandPoses().Any()) { EditorGUILayout.HelpBox($"To start using this functionality add hand poses to the {nameof(UxrAvatar)} component first.", MessageType.Info); } else { _reorderableEventList?.DoLayoutList(); } } serializedObject.ApplyModifiedProperties(); } /// /// Called by Unity to draw gizmos. It's used to display some visual aids when the avatar has a /// configuration. /// private void OnSceneGUI() { UxrStandardAvatarController standardAvatarController = (UxrStandardAvatarController)serializedObject.targetObject; UxrAvatar avatar = standardAvatarController.Avatar; if (standardAvatarController && standardAvatarController.UseBodyIK && avatar && avatar.AvatarRigType == UxrAvatarRigType.HalfOrFullBody) { Transform avatarTransform = avatar.transform; float neckBaseHeight = _propBodyIKSettings.FindPropertyRelative(UxrIKBodySettingsDrawer.PropertyNeckBaseHeight).floatValue; float neckForwardOffset = _propBodyIKSettings.FindPropertyRelative(UxrIKBodySettingsDrawer.PropertyNeckForwardOffset).floatValue; float eyesBaseHeight = _propBodyIKSettings.FindPropertyRelative(UxrIKBodySettingsDrawer.PropertyEyesBaseHeight).floatValue; float eyesForwardOffset = _propBodyIKSettings.FindPropertyRelative(UxrIKBodySettingsDrawer.PropertyEyesForwardOffset).floatValue; float neckRadius = 0.08f; float eyesSeparation = 0.065f; float eyesRadius = 0.01f; if (avatar.AvatarRig.Head.Neck == null) { Handles.DrawSolidDisc(avatarTransform.position + avatarTransform.GetScaledVector(0.0f, neckBaseHeight, neckForwardOffset), avatarTransform.up, neckRadius); } Handles.DrawSolidDisc(avatarTransform.position + avatarTransform.GetScaledVector(-eyesSeparation * 0.5f, eyesBaseHeight, eyesForwardOffset), avatarTransform.forward, eyesRadius); Handles.DrawSolidDisc(avatarTransform.position + avatarTransform.GetScaledVector(eyesSeparation * 0.5f, eyesBaseHeight, eyesForwardOffset), avatarTransform.forward, eyesRadius); } } #endregion #region Private Methods /// /// Creates a reorderable list for the given controller events. /// /// Serialized object representing the selected component(s) /// The serialized property with the controller events /// A callback to draw the list /// private static ReorderableList CreateReorderableList(SerializedObject serializedObject, SerializedProperty propertyListControllerEvents, ReorderableList.ElementCallbackDelegate drawCallback) { ReorderableList reorderableEventList = new ReorderableList(serializedObject, propertyListControllerEvents, true, true, true, true); reorderableEventList.drawHeaderCallback = rect => { EditorGUI.LabelField(rect, "Drag elements to reorder them by priority from top to bottom"); }; reorderableEventList.drawElementCallback = drawCallback; return reorderableEventList; } /// /// Helper method that draws an event entry in the inspector. /// /// The rect where to draw the element /// The element index in the list /// Whether the element is active /// Whether the element is focused private void DrawControllerEventCallback(Rect rect, int index, bool isActive, bool isFocused) { UxrStandardAvatarController selectedController = (UxrStandardAvatarController)serializedObject.targetObject; UxrAvatar avatar = selectedController.Avatar; SerializedProperty element = _reorderableEventList.serializedProperty.GetArrayElementAtIndex(index); int nLineIndex = 0; Rect GetCurrentRect() { return new Rect(rect.x, rect.y + EditorGUIUtility.singleLineHeight * nLineIndex, rect.width, EditorGUIUtility.singleLineHeight); } // Animation type EditorGUI.PropertyField(GetCurrentRect(), element.FindPropertyRelative("_animationType"), new GUIContent("Animation type")); nLineIndex++; // Controller button mask int buttons = EditorGUI.MaskField(GetCurrentRect(), new GUIContent("Controller button(s)"), element.FindPropertyRelative("_buttons").intValue, UxrEditorUtils.GetControllerButtonNames().ToArray()); element.FindPropertyRelative("_buttons").intValue = buttons; nLineIndex++; // List animator parameters UxrEditorUtils.HandPoseDropdown(GetCurrentRect(), new GUIContent("Hand Pose"), avatar, element.FindPropertyRelative("_handPose"), out UxrHandPoseAsset selectedHandPose); nLineIndex++; if (selectedHandPose && selectedHandPose.PoseType == UxrHandPoseType.Blend) { element.FindPropertyRelative("_poseBlendValue").floatValue = EditorGUI.Slider(GetCurrentRect(), new GUIContent("Pose Blend"), element.FindPropertyRelative("_poseBlendValue").floatValue, 0.0f, 1.0f); } } #endregion #region Private Types & Data private GUIContent ContentAllowHandTracking { get; } = new GUIContent("Allow Hand Tracking", "Switches to hand-tracking to update the avatar hands when available"); private GUIContent ContentUseArmIK { get; } = new GUIContent("Use Arm IK", "Whether to try to naturally orient the arms using the position of the hands"); private GUIContent ContentArmIKElbowAperture { get; } = new GUIContent("Elbow Neutral Aperture", "Controls how close the elbows will be to the body when arms are computed using inverse kinematics"); private GUIContent ContentArmIKOverExtendMode { get; } = new GUIContent("Arm Over-Extend", "Controls what to do when the user extends the hands over the avatar's arm reach"); private GUIContent ContentUseBodyIK { get; } = new GUIContent("Use Body IK", "Whether to try to naturally orient the avatar body using the positions of the head and hand"); private GUIContent ContentBodyIKSettings { get; } = new GUIContent("Body IK Settings"); private GUIContent ContentUseLegIK { get; } = new GUIContent("Use Leg IK (TBD)", ""); private SerializedProperty _propAllowHandTracking; private SerializedProperty _propUseArmIK; private SerializedProperty _propArmIKElbowAperture; private SerializedProperty _propArmIKOverExtendMode; private SerializedProperty _propUseBodyIK; private SerializedProperty _propBodyIKSettings; //private SerializedProperty _propUseLegIK; private SerializedProperty _propListControllerEvents; private bool _foldoutGeneral = true; private bool _foldoutIK = true; private bool _foldoutHandEvents = true; private ReorderableList _reorderableEventList; #endregion } }