// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) VRMADA, All rights reserved. // // -------------------------------------------------------------------------------------------------------------------- using System.Collections.Generic; using System.Linq; using UltimateXR.Avatar; using UltimateXR.Core; using UltimateXR.Editor.Avatar; using UltimateXR.Editor.Manipulation.HandPoses; using UltimateXR.Extensions.Unity; using UltimateXR.Manipulation; using UltimateXR.Manipulation.HandPoses; using UnityEditor; using UnityEngine; namespace UltimateXR.Editor.Manipulation { /// /// Custom editor for . /// [CustomEditor(typeof(UxrGrabbableObjectSnapTransform))] public class UxrGrabbableObjectSnapTransformEditor : UnityEditor.Editor { #region Public Methods /// /// Refreshes the preview poses for a specific avatar and hand pose. Designed to be called from the Hand Pose Editor to /// keep the preview meshes updated. Preview hand pose meshes that share the same prefab will be updated accordingly. /// /// The avatar to update the poses for /// The pose asset to look for and update public static void RefreshGrabPoseMeshes(UxrAvatar avatar, UxrHandPoseAsset handPoseAsset) { foreach (KeyValuePair editorPair in s_openEditors) { editorPair.Value.RefreshGrabPoseMeshesInternal(avatar.GetAvatarPrefab(), handPoseAsset); } } /// /// Creates a hand pose preview object. /// /// Parent that will be assigned to the new object /// The preview mesh object /// Mesh assigned to the object /// Shared materials to be assigned to the mesh /// New preview GameObject public static GameObject CreateAndSetupPreviewMeshObject(Transform parent, UxrPreviewHandGripMesh previewMesh, Mesh mesh, Material[] sharedMaterials) { // Create child that the proxy will have to follow GameObject grabPose = EditorUtility.CreateGameObjectWithHideFlags("GrabPose", HideFlags.HideAndDontSave, typeof(UxrGrabbableObjectPreviewMesh)); grabPose.transform.position = parent.position; grabPose.transform.rotation = parent.rotation; grabPose.transform.SetParent(parent); // Create proxy at root level that avoids non-uniform scaling problems GameObject grabPoseProxy = EditorUtility.CreateGameObjectWithHideFlags("GrabPoseProxy", HideFlags.HideAndDontSave, typeof(UxrGrabbableObjectPreviewMeshProxy), typeof(MeshFilter), typeof(MeshRenderer)); grabPoseProxy.transform.position = parent.position; grabPoseProxy.transform.rotation = parent.rotation; grabPoseProxy.GetComponent().sharedMesh = mesh; grabPoseProxy.GetComponent().sharedMaterials = sharedMaterials; grabPoseProxy.GetComponent().PreviewMesh = previewMesh; grabPoseProxy.GetComponent().PreviewMeshComponent = grabPose.GetComponent(); grabPose.GetComponent().PreviewMeshProxy = grabPoseProxy.GetComponent(); return grabPose; } #endregion #region Unity /// /// Creates temporal grab pose editor objects. /// private void OnEnable() { for (int i = 0; i < serializedObject.targetObjects.Length; ++i) { UxrGrabbableObjectSnapTransform snapTransform = serializedObject.targetObject as UxrGrabbableObjectSnapTransform; snapTransform.hideFlags = HideFlags.DontSaveInBuild; } Undo.undoRedoPerformed += OnUndoRedo; CreateGrabPoseTempEditorObjects(); // Add a static reference so that we can update the grip preview meshes from the Hand Pose Editor if (s_openEditors.ContainsKey(serializedObject) == false) { s_openEditors.Add(serializedObject, this); } // Destroy objects that don't belong to any UxrGrabbableObject (duplicated in editor or moved from hierarchy) foreach (Object targetObject in serializedObject.targetObjects) { CheckUnusedTransform(targetObject as UxrGrabbableObjectSnapTransform); } } /// /// Destroys temporal grab pose editor objects. /// private void OnDisable() { DestroyGrabPoseTempEditorObjects(); Undo.undoRedoPerformed -= OnUndoRedo; if (s_openEditors.ContainsKey(serializedObject)) { s_openEditors.Remove(serializedObject); } } /// /// Draws the custom inspector. /// public override void OnInspectorGUI() { if (serializedObject.targetObjects.Length == 1) { UxrGrabbableObjectSnapTransform snapTransform = serializedObject.targetObject as UxrGrabbableObjectSnapTransform; UxrGrabbableObject grabbableObject = snapTransform.SafeGetComponentInParent(); if (grabbableObject == null) { return; } UxrAvatar avatarForGrip = UxrAvatarEditorExt.GetFromGuid(grabbableObject.Editor_GetGrabAlignTransformAvatar(snapTransform.transform)); if (avatarForGrip) { IEnumerable grabPointIndices = grabbableObject.Editor_GetGrabPointsForGrabAlignTransform(avatarForGrip, snapTransform.transform); if (grabPointIndices.Count() > 1) { EditorGUILayout.HelpBox($"This transform is being used in more than one grab point of {grabbableObject.name}", MessageType.Warning); } else if (grabPointIndices.Count() == 1) { UxrGrabPointInfo grabPoint = grabbableObject.GetGrabPoint(grabPointIndices.First()); UxrGripPoseInfo gripPose = grabPoint.GetGripPoseInfo(avatarForGrip); UxrHandPoseAsset gripPoseAsset = gripPose?.HandPose; if (gripPoseAsset != null && gripPoseAsset.PoseType == UxrHandPoseType.Blend) { EditorGUILayout.HelpBox($"This slider will affect both the left and right hand of pose {gripPoseAsset.name}. Grab points in {nameof(UxrGrabbableObject)} use the same slider for both hands.", MessageType.Info); Undo.RecordObject(grabbableObject, "Change Pose Blend"); EditorGUILayout.Space(); float oldBlend = gripPose.PoseBlendValue; float newBlend = EditorGUILayout.Slider($"Pose Blend ({gripPoseAsset.name})", oldBlend, 0.0f, 1.0f); gripPose.PoseBlendValue = newBlend; if (!Mathf.Approximately(oldBlend, newBlend)) { _previewMeshLeft?.Refresh(grabbableObject, _cachedSingleObjectAvatar, grabPointIndices.First(), UxrHandSide.Left); _previewMeshRight?.Refresh(grabbableObject, _cachedSingleObjectAvatar, grabPointIndices.First(), UxrHandSide.Right); } EditorGUILayout.Space(); } } UxrGrabber[] grabbers = avatarForGrip.GetComponentsInChildren(); UxrGrabber leftGrabber = grabbers.FirstOrDefault(g => g.Side == UxrHandSide.Left); UxrGrabber rightGrabber = grabbers.FirstOrDefault(g => g.Side == UxrHandSide.Right); UxrGrabber grabber = _previewMeshLeft != null ? leftGrabber : _previewMeshRight != null ? rightGrabber : null; if (grabber) { if (UxrEditorUtils.CenteredButton(new GUIContent("Mirror Up/Down", ""))) { Undo.RegisterCompleteObjectUndo(snapTransform.transform, "Mirror transform"); snapTransform.transform.ApplyMirroring(snapTransform.transform.position, snapTransform.transform.TransformDirection(-grabber.LocalFingerDirection), grabber.RequiredMirrorType); } if (UxrEditorUtils.CenteredButton(new GUIContent("Mirror Front/Back", ""))) { Undo.RegisterCompleteObjectUndo(snapTransform.transform, "Mirror transform"); snapTransform.transform.ApplyMirroring(snapTransform.transform.position, snapTransform.transform.TransformDirection(-grabber.LocalPalmOutDirection), grabber.RequiredMirrorType); } if (grabPointIndices.Count() == 1) { UxrGrabPointInfo grabPoint = grabbableObject.GetGrabPoint(grabPointIndices.First()); UxrGripPoseInfo gripPose = grabPoint.GetGripPoseInfo(avatarForGrip); if (gripPose.GripAlignTransformHandLeft != null && gripPose.GripAlignTransformHandRight != null) { if (UxrEditorUtils.CenteredButton(new GUIContent("Copy From Other Hand", ""))) { Undo.RegisterCompleteObjectUndo(snapTransform.transform, "Set transform"); snapTransform.transform.SetPositionAndRotation(snapTransform.transform == gripPose.GripAlignTransformHandLeft ? gripPose.GripAlignTransformHandRight : gripPose.GripAlignTransformHandLeft); } } } } } } } #endregion #region Event Trigger Methods /// /// Recomputes the preview poses. /// private void OnUndoRedo() { ComputeGrabPoseMeshes(); } #endregion #region Private Methods /// /// Tries to obtain the left and right grab pose meshes of an align transform if they are available. /// /// The registered avatar prefab to get the pose meshes for /// The transform that is potentially an align transform for a UxrGrabbableObject grab point /// Will contain the left grab pose mesh if it was found /// Will contain the right grab pose mesh if it was found private static void TryGetGrabPoseMeshes(UxrAvatar avatarPrefab, Transform transform, out UxrPreviewHandGripMesh previewMeshLeft, out UxrPreviewHandGripMesh previewMeshRight) { previewMeshLeft = null; previewMeshRight = null; UxrGrabbableObject[] grabbableObjects = transform.GetComponentsInParent(); UxrAvatar avatar = UxrGrabbableObjectEditor.TryGetSceneAvatar(avatarPrefab.gameObject); foreach (UxrGrabbableObject grabbableObject in grabbableObjects) { for (int i = 0; i < grabbableObject.GrabPointCount; ++i) { UxrGrabPointInfo grabPoint = grabbableObject.GetGrabPoint(i); if (grabPoint.HideHandGrabberRenderer == false && grabPoint.SnapMode == UxrSnapToHandMode.PositionAndRotation) { if (grabbableObject.Editor_GetGrabPointGrabAlignTransform(avatarPrefab, i, UxrHandSide.Left) == transform && (grabPoint.HandSide == UxrHandSide.Left || grabPoint.BothHandsCompatible)) { previewMeshLeft = UxrPreviewHandGripMesh.Build(grabbableObject, avatar, i, UxrHandSide.Left); if (previewMeshLeft != null) { grabPoint.GetGripPoseInfo(avatarPrefab).GrabPoseMeshLeft = previewMeshLeft.UnityMesh; } } if (grabbableObject.Editor_GetGrabPointGrabAlignTransform(avatarPrefab, i, UxrHandSide.Right) == transform && (grabPoint.HandSide == UxrHandSide.Right || grabPoint.BothHandsCompatible)) { previewMeshRight = UxrPreviewHandGripMesh.Build(grabbableObject, avatar, i, UxrHandSide.Right); if (previewMeshRight != null) { grabPoint.GetGripPoseInfo(avatarPrefab).GrabPoseMeshRight = previewMeshRight.UnityMesh; } } } } } } /// /// Regenerates the grab pose meshes /// private void ComputeGrabPoseMeshes() { DestroyGrabPoseTempEditorObjects(); CreateGrabPoseTempEditorObjects(); } /// /// Creates the temporal grab pose GameObjects that are used to quickly preview/edit grabbing mechanics /// in the editor /// private void CreateGrabPoseTempEditorObjects() { // Create temporal grab pose objects _previewMeshLeft = null; _previewMeshRight = null; foreach (Object targetObject in serializedObject.targetObjects) { UxrGrabbableObjectSnapTransform snapTransform = targetObject as UxrGrabbableObjectSnapTransform; UxrGrabbableObject grabbableObject = snapTransform.SafeGetComponentInParent(); if (grabbableObject == null) { continue; } // Get the avatar prefab UxrAvatar avatarPrefab = UxrAvatarEditorExt.GetFromGuid(grabbableObject.Editor_GetGrabAlignTransformAvatar(snapTransform.transform)); if (avatarPrefab == null) { return; } // Try to get a compatible instance of the prefab in the scene UxrAvatar avatar = UxrGrabbableObjectEditor.TryGetSceneAvatar(avatarPrefab.gameObject); if (avatar == null) { continue; } _cachedSingleObjectAvatar = avatar; // Get grabber renderers which will be used to know which materials to use when rendering grab poses UxrGrabber[] grabbers = avatar.GetComponentsInChildren(false); Renderer leftGrabberRenderer = grabbers.FirstOrDefault(g => g.Side == UxrHandSide.Left && g.HandRenderer != null)?.HandRenderer; Renderer rightGrabberRenderer = grabbers.FirstOrDefault(g => g.Side == UxrHandSide.Right && g.HandRenderer != null)?.HandRenderer; if (snapTransform && avatar) { TryGetGrabPoseMeshes(avatarPrefab, snapTransform.transform, out _previewMeshLeft, out _previewMeshRight); if (_previewMeshLeft != null && leftGrabberRenderer) { GameObject grabPose = CreateAndSetupPreviewMeshObject(snapTransform.transform, _previewMeshLeft, _previewMeshLeft.UnityMesh, leftGrabberRenderer.sharedMaterials); } if (_previewMeshRight != null && rightGrabberRenderer) { GameObject grabPose = CreateAndSetupPreviewMeshObject(snapTransform.transform, _previewMeshRight, _previewMeshRight.UnityMesh, rightGrabberRenderer.sharedMaterials); } } } } /// /// Refreshes the preview poses for a specific avatar and hand pose. Designed to be called /// from the Hand Pose Editor to keep the preview meshes updated. /// /// The registered avatar to look for /// The pose asset to look for and update private void RefreshGrabPoseMeshesInternal(UxrAvatar avatarPrefab, UxrHandPoseAsset handPoseAsset) { foreach (Object targetObject in serializedObject.targetObjects) { UxrGrabbableObjectSnapTransform snapTransform = targetObject as UxrGrabbableObjectSnapTransform; UxrGrabbableObject grabbableObject = snapTransform.SafeGetComponentInParent(); UxrAvatar selectedAvatar = UxrAvatarEditorExt.GetFromGuid(grabbableObject.Editor_GetGrabAlignTransformAvatar(snapTransform.transform)); if (selectedAvatar == null || grabbableObject == null || !selectedAvatar.IsPrefabCompatibleWith(avatarPrefab)) { continue; } UxrAvatar avatar = UxrGrabbableObjectEditor.TryGetSceneAvatar(avatarPrefab.gameObject); if (avatar == null) { continue; } for (int i = 0; i < grabbableObject.GrabPointCount; ++i) { UxrGrabPointInfo grabPoint = grabbableObject.GetGrabPoint(i); UxrGripPoseInfo gripPose = grabPoint.GetGripPoseInfo(avatar); UxrHandPoseAsset gripPoseAsset = gripPose?.HandPose; if (gripPoseAsset == null) { continue; } // Does this grab point use the pose? if ((grabbableObject.Editor_GetGrabPointGrabAlignTransform(avatar, i, UxrHandSide.Left) == snapTransform.transform || grabbableObject.Editor_GetGrabPointGrabAlignTransform(avatar, i, UxrHandSide.Right) == snapTransform.transform) && gripPoseAsset.name == handPoseAsset.name) { _previewMeshLeft?.Refresh(grabbableObject, _cachedSingleObjectAvatar, i, UxrHandSide.Left, true); _previewMeshRight?.Refresh(grabbableObject, _cachedSingleObjectAvatar, i, UxrHandSide.Right, true); } } } } /// /// Checks if a given UxrGrabbableObjectSnapTransform's transform is referenced by /// an UxrGrabbableObject. Components that are not being referenced are usually the /// result of duplicating manually in the editor a GameObject that is used as a /// snap transform for an UxrGrabbableObject. It also happens if a transform is /// moved out of the UxrGrabbableObject hierarchy. /// In case of not being referenced, the preview meshes hanging from it should be /// deleted. The method will also remove the UxrGrabbableObjectSnapTransform /// component itself. /// /// The transform to check private void CheckUnusedTransform(UxrGrabbableObjectSnapTransform snapTransform) { if (snapTransform == null) { return; } UxrGrabbableObject grabbableObject = snapTransform.SafeGetComponentInParent(); if (grabbableObject) { // Check if it's still being referenced by grabbableObject bool referenced = false; for (int i = 0; i < grabbableObject.GrabPointCount; ++i) { if (grabbableObject.Editor_HasGrabPointWithGrabAlignTransform(snapTransform.transform)) { referenced = true; break; } } if (!referenced) { // Hanging from UxrGrabbableObject but not referenced DestroyGrabPoseTempEditorObjects(snapTransform); DestroyImmediate(snapTransform); } } else { // Moved out of UxrGrabbableObject hierarchy DestroyGrabPoseTempEditorObjects(snapTransform); DestroyImmediate(snapTransform); } } /// /// Destroys the temporal grab pose objects created by CreateGrabPoseTempEditorObjects() /// private void DestroyGrabPoseTempEditorObjects() { foreach (Object targetObject in serializedObject.targetObjects) { UxrGrabbableObjectSnapTransform snapTransform = targetObject as UxrGrabbableObjectSnapTransform; DestroyGrabPoseTempEditorObjects(snapTransform); } } /// /// Destroys all temporal editor objects belonging to the snap transform /// /// Snap transform to delete temporal objects for private void DestroyGrabPoseTempEditorObjects(UxrGrabbableObjectSnapTransform snapTransform) { if (snapTransform) { UxrGrabbableObjectPreviewMesh[] previewMeshComponents = snapTransform.GetComponentsInChildren(); foreach (UxrGrabbableObjectPreviewMesh previewMeshComponent in previewMeshComponents) { DestroyImmediate(previewMeshComponent.gameObject); } } } #endregion #region Private Types & Data private static readonly Dictionary s_openEditors = new Dictionary(); private UxrAvatar _cachedSingleObjectAvatar; private UxrPreviewHandGripMesh _previewMeshLeft; private UxrPreviewHandGripMesh _previewMeshRight; #endregion } }