303 lines
16 KiB
C#
303 lines
16 KiB
C#
// --------------------------------------------------------------------------------------------------------------------
|
|
// <copyright file="UxrStandardAvatarControllerEditor.cs" company="VRMADA">
|
|
// Copyright (c) VRMADA, All rights reserved.
|
|
// </copyright>
|
|
// --------------------------------------------------------------------------------------------------------------------
|
|
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
|
|
{
|
|
/// <summary>
|
|
/// Custom inspector for the <see cref="UxrStandardAvatarController" /> component.
|
|
/// </summary>
|
|
[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
|
|
|
|
/// <summary>
|
|
/// Caches the serialized properties and initializes data.
|
|
/// </summary>
|
|
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);
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called by Unity to draw the inspector for the selected component(s).
|
|
/// </summary>
|
|
public override void OnInspectorGUI()
|
|
{
|
|
serializedObject.Update();
|
|
|
|
UxrAvatar avatar = ((UxrStandardAvatarController)serializedObject.targetObject).GetComponent<UxrAvatar>();
|
|
|
|
// 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<UxrArmIKSolver>().ForEach(s => s.RelaxedElbowAperture = _propArmIKElbowAperture.floatValue);
|
|
}
|
|
|
|
EditorGUI.BeginChangeCheck();
|
|
EditorGUILayout.PropertyField(_propArmIKOverExtendMode, ContentArmIKOverExtendMode);
|
|
if (EditorGUI.EndChangeCheck() && Application.isPlaying)
|
|
{
|
|
UxrIKSolver.GetComponents(avatar, true).OfType<UxrArmIKSolver>().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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called by Unity to draw gizmos. It's used to display some visual aids when the avatar has a
|
|
/// <see cref="UxrAvatarRigType.HalfOrFullBody" /> configuration.
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Creates a reorderable list for the given controller events.
|
|
/// </summary>
|
|
/// <param name="serializedObject">Serialized object representing the selected component(s)</param>
|
|
/// <param name="propertyListControllerEvents">The serialized property with the controller events</param>
|
|
/// <param name="drawCallback">A callback to draw the list</param>
|
|
/// <returns></returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper method that draws an event entry in the inspector.
|
|
/// </summary>
|
|
/// <param name="rect">The rect where to draw the element</param>
|
|
/// <param name="index">The element index in the list</param>
|
|
/// <param name="isActive">Whether the element is active</param>
|
|
/// <param name="isFocused">Whether the element is focused</param>
|
|
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
|
|
}
|
|
} |