// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) VRMADA, All rights reserved. // // -------------------------------------------------------------------------------------------------------------------- using System.Collections.Generic; using System.IO; using System.Linq; using UltimateXR.Avatar; using UltimateXR.Core; using UltimateXR.Devices.Visualization; using UltimateXR.Extensions.System.Collections; using UltimateXR.Extensions.System.IO; using UltimateXR.Extensions.Unity; using UltimateXR.Manipulation; using UnityEditor; using UnityEngine; namespace UltimateXR.Editor.Avatar { /// /// Custom editor for . /// [CustomEditor(typeof(UxrHandIntegration))] public partial class UxrHandIntegrationEditor : UnityEditor.Editor { #region Unity /// /// Caches the serialized properties, the different variations and creates the temporal hand gizmo. /// private void OnEnable() { _propertyGizmoHandSize = serializedObject.FindProperty("_gizmoHandSize"); _propertyHandSide = serializedObject.FindProperty("_handSide"); _propertyObjectVariationName = serializedObject.FindProperty("_objectVariationName"); _propertyMaterialVariationName = serializedObject.FindProperty("_materialVariationName"); _propertySelectedRenderPipeline = serializedObject.FindProperty("_selectedRenderPipeline"); _handIntegration = serializedObject.targetObject as UxrHandIntegration; _objectVariationNames = GetCommonObjectVariationNames(); _materialVariationNames = GetCommonMaterialVariationNames(_propertyObjectVariationName.name); GetControllerPipelineRenderers(out _renderers, out _renderPipelineVariations, out _renderPipelineVariationStrings, out _selectedRenderPipelineIndex); serializedObject.Update(); _propertySelectedRenderPipeline.intValue = _selectedRenderPipelineIndex; serializedObject.ApplyModifiedProperties(); UxrGrabber[] grabbers = _handIntegration.GetComponentsInChildren(); foreach (UxrGrabber grabber in grabbers) { if (grabber.HandRenderer == null) { // Do nothing, we just try to force to get the hand renderer reference if it doesn't exist or is inactive } } CreateTemporalObjects(); } /// /// Destroys the temporal hand gizmo. /// private void OnDisable() { DestroyTemporalObjects(); } /// public override void OnInspectorGUI() { serializedObject.Update(); bool variationChanged = false; bool renderPipelineChanged = false; // Gizmo hand size (read-only so that it can only be changed in debug mode). GUI.enabled = false; EditorGUI.BeginChangeCheck(); EditorGUILayout.PropertyField(_propertyGizmoHandSize, ContentGizmoHandSize); if (EditorGUI.EndChangeCheck()) { serializedObject.ApplyModifiedProperties(); RegenerateTemporalObjects(); } GUI.enabled = true; // Hand side EditorGUI.BeginChangeCheck(); EditorGUILayout.PropertyField(_propertyHandSide, ContentHandSide); if (EditorGUI.EndChangeCheck()) { serializedObject.ApplyModifiedProperties(); RegenerateTemporalObjects(); } // Select object variation if (_objectVariationNames.Count > 0) { EditorGUI.BeginChangeCheck(); int handIndex = EditorGUILayout.Popup(ContentObjectVariationName, _objectVariationNames.IndexOf(_propertyObjectVariationName.stringValue), UxrEditorUtils.ToGUIContentArray(_objectVariationNames)); if (EditorGUI.EndChangeCheck()) { variationChanged = true; _propertyObjectVariationName.stringValue = _objectVariationNames[handIndex]; serializedObject.ApplyModifiedProperties(); } _materialVariationNames = GetCommonMaterialVariationNames(_propertyObjectVariationName.stringValue); } // Select material variation if (_materialVariationNames.Count > 0) { EditorGUI.BeginChangeCheck(); int materialIndex = EditorGUILayout.Popup(ContentMaterialVariationName, _materialVariationNames.IndexOf(_propertyMaterialVariationName.stringValue), UxrEditorUtils.ToGUIContentArray(_materialVariationNames)); if (EditorGUI.EndChangeCheck()) { variationChanged = true; _propertyMaterialVariationName.stringValue = _materialVariationNames[materialIndex]; } } // Select Render Pipeline variation if (_renderPipelineVariations.Count > 1) { EditorGUI.BeginChangeCheck(); _propertySelectedRenderPipeline.intValue = EditorGUILayout.Popup(ContentRenderPipeline, _propertySelectedRenderPipeline.intValue, UxrEditorUtils.ToGUIContentArray(_renderPipelineVariationStrings)); if (EditorGUI.EndChangeCheck()) { renderPipelineChanged = true; _selectedRenderPipelineIndex = _propertySelectedRenderPipeline.intValue; } } serializedObject.ApplyModifiedProperties(); // Enable new variation if (variationChanged) { EnableCurrentVariation(_propertyObjectVariationName.stringValue, _propertyMaterialVariationName.stringValue); } if (renderPipelineChanged) { EnableCurrentRenderPipeline(_renderers, _renderPipelineVariations[_selectedRenderPipelineIndex]); } // Alignment to avatar hand EditorGUILayout.Space(); if (UxrEditorUtils.CenteredButton(new GUIContent("Align to avatar"))) { Undo.RegisterCompleteObjectUndo(_handIntegration.transform, "Align hand integration"); if (!_handIntegration.TryToMatchHand()) { EditorUtility.DisplayDialog("Missing required bone references", $"The hand integration could not find an {nameof(UxrAvatar)} component up in the hierarchy or the {nameof(UxrAvatar)} is missing finger bone references in the rig section. The finger bones are required to try to align the hand integration with the avatar's hand.", UxrConstants.Editor.Ok); } } // Check Undo/Redo/Revert: if (_propertyGizmoHandSize.enumValueIndex != (int)_gizmoHandSize) { RegenerateTemporalObjects(); } if (_propertySelectedRenderPipeline.intValue != _selectedRenderPipelineIndex) { _selectedRenderPipelineIndex = _propertySelectedRenderPipeline.intValue; EnableCurrentRenderPipeline(_renderers, _renderPipelineVariations[_selectedRenderPipelineIndex]); } } #endregion #region Event Trigger Methods /// /// Called when there is an undo/redo. /// private void OnUndoRedo() { RegenerateTemporalObjects(); } #endregion #region Private Methods /// /// Gets all the common object variation names among the available components hanging /// from the object. /// /// Common available object variation names private IReadOnlyList GetCommonObjectVariationNames() { List objectVariationNames = new List(); if (_handIntegration) { _controllerHands = _handIntegration.GetComponentsInChildren(true); bool isFirst = true; foreach (UxrControllerHand hand in _controllerHands) { objectVariationNames = isFirst ? hand.Variations.Select(v => v.Name).ToList() : objectVariationNames.Intersect(hand.Variations.Select(v => v.Name)).ToList(); isFirst = false; } } return objectVariationNames; } /// /// Gets all the common material variation names among the available components /// hanging from the object, for a given object variation. /// /// Common available material variation names for the given object variation private IReadOnlyList GetCommonMaterialVariationNames(string objectVariationName) { List materialVariationNames = new List(); if (_handIntegration) { _controllerHands = _handIntegration.GetComponentsInChildren(true); bool isFirst = true; foreach (UxrControllerHand hand in _controllerHands) { foreach (UxrControllerHand.ObjectVariation objectVariation in hand.Variations) { if (objectVariation.Name == objectVariationName) { materialVariationNames = isFirst ? objectVariation.MaterialVariations.Select(v => v.Name).ToList() : materialVariationNames.Intersect(objectVariation.MaterialVariations.Select(v => v.Name)).ToList(); isFirst = false; } } } } return materialVariationNames; } /// /// Retrieves the available renderers that are part of controllers, the list of available material render pipeline /// variations and the currently selected render pipeline. /// /// Returns the renderers that are part of controllers, if any /// Returns the available material render pipeline variations, if any /// /// Returns the UI strings that /// should be mapped with /// /// /// Returns the index in of the /// render pipeline that was found to be currently enabled /// private void GetControllerPipelineRenderers(out List renderers, out List renderPipelineVariations, out List renderPipelineVariationStrings, out int selectedRenderPipelineIndex) { renderers = new List(); renderPipelineVariations = new List(); renderPipelineVariationStrings = new List(); selectedRenderPipelineIndex = -1; if (_handIntegration) { foreach (Renderer renderer in _handIntegration.GetComponentsInChildren(true)) { foreach (Material material in renderer.sharedMaterials.Where(material => material != null)) { if (material.name.Contains(MaterialUrp)) { renderers.Add(renderer); if (!renderPipelineVariations.Contains(RenderPipeline.Urp)) { renderPipelineVariations.Add(RenderPipeline.Urp); renderPipelineVariationStrings.Add(UrpUI); } selectedRenderPipelineIndex = renderPipelineVariations.IndexOf(RenderPipeline.Urp); if (!renderPipelineVariations.Contains(RenderPipeline.Brp) && GetRenderPipelineVariant(material, RenderPipeline.Brp)) { renderPipelineVariations.Add(RenderPipeline.Brp); renderPipelineVariationStrings.Add(BrpUI); } } else if (material.name.Contains(MaterialBrp)) { renderers.Add(renderer); if (!renderPipelineVariations.Contains(RenderPipeline.Brp)) { renderPipelineVariations.Add(RenderPipeline.Brp); renderPipelineVariationStrings.Add(BrpUI); } selectedRenderPipelineIndex = renderPipelineVariations.IndexOf(RenderPipeline.Brp); if (!renderPipelineVariations.Contains(RenderPipeline.Urp) && GetRenderPipelineVariant(material, RenderPipeline.Urp)) { renderPipelineVariations.Add(RenderPipeline.Urp); renderPipelineVariationStrings.Add(UrpUI); } } } } } } /// /// Gets the given material variation if it exists. /// /// Material to get the variation of /// Render pipeline to get the variation for /// The material variation for the given render pipeline if it exists private Material GetRenderPipelineVariant(Material material, RenderPipeline renderPipeline) { string materialName = material.name; string directory = Path.GetDirectoryName(AssetDatabase.GetAssetPath(material)); if (directory == null) { return null; } if (renderPipeline == RenderPipeline.Brp) { if (materialName.Contains(MaterialUrp)) { return AssetDatabase.LoadAssetAtPath(PathExt.Combine(directory, materialName.Replace(MaterialUrp, MaterialBrp)) + ".mat"); } } else if (renderPipeline == RenderPipeline.Urp) { if (materialName.Contains(MaterialBrp)) { return AssetDatabase.LoadAssetAtPath(PathExt.Combine(directory, materialName.Replace(MaterialBrp, MaterialUrp)) + ".mat"); } } return null; } /// /// Enables the current object/material variation. /// /// Object variation name /// Material variation name private void EnableCurrentVariation(string objectVariationName, string materialVariationName) { if (_handIntegration) { foreach (UxrControllerHand hand in _handIntegration.GetComponentsInChildren(true)) { // First pass: Disable all, because the selected variation may share a GameObject with another variation foreach (UxrControllerHand.ObjectVariation objectVariation in hand.Variations) { objectVariation.GameObject.SetActive(false); } // Second pass: Enable selected one and assign material UxrControllerHand.ObjectVariation selectedObjectVariation = hand.Variations.FirstOrDefault(v => v.Name == objectVariationName); if (selectedObjectVariation != null) { selectedObjectVariation.GameObject.SetActive(true); UxrControllerHand.ObjectVariation.MaterialVariation selectedMaterialVariation = selectedObjectVariation.MaterialVariations.FirstOrDefault(v => v.Name == materialVariationName); if (selectedMaterialVariation != null) { foreach (Renderer renderer in selectedObjectVariation.GameObject.GetComponentsInChildren(true)) { renderer.sharedMaterial = selectedMaterialVariation.Material; } } } } } } /// /// Enables the materials for the given render pipeline. /// /// List of renderers whose materials to process /// Render pipeline to enable private void EnableCurrentRenderPipeline(IReadOnlyList renderers, RenderPipeline renderPipeline) { foreach (Renderer renderer in renderers) { Material[] sharedMaterials = renderer.sharedMaterials; for (int i = 0; i < sharedMaterials.Length; ++i) { Material materialVariation = GetRenderPipelineVariant(sharedMaterials[i], renderPipeline); if (materialVariation != null) { sharedMaterials[i] = materialVariation; } } renderer.sharedMaterials = sharedMaterials; } } /// /// Regenerates the hand gizmo. /// private void RegenerateTemporalObjects() { DestroyTemporalObjects(); CreateTemporalObjects(); } /// /// Creates the hand gizmo. /// private void CreateTemporalObjects() { string handAssetGuid = null; if (_propertyGizmoHandSize.enumValueIndex == (int)UxrHandIntegration.GizmoHandSize.Big) { handAssetGuid = _handIntegration.HandSide == UxrHandSide.Left ? LeftBigHandAssetGuid : RightBigHandAssetGuid; _gizmoHandSize = UxrHandIntegration.GizmoHandSize.Big; } else if (_propertyGizmoHandSize.enumValueIndex == (int)UxrHandIntegration.GizmoHandSize.Small) { handAssetGuid = _handIntegration.HandSide == UxrHandSide.Left ? LeftSmallHandAssetGuid : RightSmallHandAssetGuid; _gizmoHandSize = UxrHandIntegration.GizmoHandSize.Small; } if (!string.IsNullOrEmpty(handAssetGuid)) { GameObject handAsset = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(handAssetGuid)); if (handAsset == null) { return; } _handGizmoRoot = Instantiate(handAsset); _handGizmoRoot.hideFlags = HideFlags.HideAndDontSave; _handGizmoRenderer = _handGizmoRoot.GetComponentInChildren(); if (_handGizmoRoot != null) { _handGizmoRoot.transform.SetParent(_handIntegration.transform); _handGizmoRoot.transform.SetPositionAndRotation(_handIntegration.transform); } else { Debug.LogWarning("Hand gizmo could not be loaded"); } } } /// /// Destroys the hand gizmo. /// private void DestroyTemporalObjects() { if (_handGizmoRoot != null) { DestroyImmediate(_handGizmoRoot); } _handGizmoRoot = null; _handGizmoRenderer = null; } #endregion #region Private Types & Data private GUIContent ContentGizmoHandSize => new GUIContent("Gizmo Hand Size", "The hand size "); private GUIContent ContentHandSide => new GUIContent("Hand Side"); private GUIContent ContentObjectVariationName => new GUIContent("Hand", "The type of hand that is shown with the controller"); private GUIContent ContentMaterialVariationName => new GUIContent("Material", "The hand material"); private GUIContent ContentRenderPipeline => new GUIContent("Render Pipeline", "The render pipeline that will be used, so that the correct materials are loaded"); private const string LeftBigHandAssetGuid = "93019d606e943b7429d29c694143ad6e"; private const string RightBigHandAssetGuid = "d45d3edc285cfd64a86fddd771e13f47"; private const string LeftSmallHandAssetGuid = "672e7f52fe868134a80bf396141fe261"; private const string RightSmallHandAssetGuid = "0a1c1014903295a408b755f1fed2863e"; private const string MaterialBrp = "_BRP"; private const string MaterialUrp = "_URP"; private const string BrpUI = "Built-in Render Pipeline"; private const string UrpUI = "Universal Render Pipeline"; private SerializedProperty _propertyGizmoHandSize; private SerializedProperty _propertyHandSide; private SerializedProperty _propertyObjectVariationName; private SerializedProperty _propertyMaterialVariationName; private SerializedProperty _propertySelectedRenderPipeline; private UxrHandIntegration _handIntegration; private UxrHandIntegration.GizmoHandSize _gizmoHandSize; private GameObject _handGizmoRoot; private Renderer _handGizmoRenderer; private IReadOnlyList _controllerHands; private IReadOnlyList _objectVariationNames; private IReadOnlyList _materialVariationNames; private List _renderPipelineVariations; private List _renderPipelineVariationStrings; private List _renderers; private int _selectedRenderPipelineIndex; #endregion } }