/* * Copyright (c) Meta Platforms, Inc. and affiliates. * All rights reserved. * * Licensed under the Oculus SDK License Agreement (the "License"); * you may not use the Oculus SDK except in compliance with the License, * which is provided at the time of installation or download, or which * otherwise accompanies this software in either electronic or hard copy form. * * You may obtain a copy of the License at * * https://developer.oculus.com/licenses/oculussdk/ * * Unless required by applicable law or agreed to in writing, the Oculus SDK * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using Oculus.Interaction.HandGrab.Visuals; using Oculus.Interaction.Input; using Oculus.Interaction.Utils; using System.Collections.Generic; using System.IO; using UnityEditor; using UnityEngine; namespace Oculus.Interaction.HandGrab.Editor { public class HandGrabPoseWizard : EditorWindow { /// /// The Hand being used for recording HandGrabPoses /// [SerializeField] private Hand _hand; [SerializeField] private int _handID = 0; public Hand Hand { get { if (_hand == null && _handID != 0) { _hand = EditorUtility.InstanceIDToObject(_handID) as Hand; _handID = 0; } return _hand; } set { _hand = value; if (_hand != null) { _handID = value.GetInstanceID(); } else { _handID = 0; } } } /// /// The Gameobject that the user is recording the HandGrabPose for. e.g. a key /// [SerializeField] private Rigidbody _item; /// /// References the hand prototypes used to represent the HandGrabInteractables. These are the /// static hands placed around the interactable to visualize the different holding hand-poses. /// Not mandatory. /// [SerializeField] private HandGhostProvider _ghostProvider; private HandGhostProvider GhostProvider { #if ISDK_OPENXR_HAND get => _handGhostProvider; #else get => _ghostProvider; #endif } #if ISDK_OPENXR_HAND /// /// References the hand prototypes used to represent the HandGrabInteractables. These are the /// static hands placed around the interactable to visualize the different holding hand-poses. /// Not mandatory. /// [SerializeField] [InspectorName("Ghost Provider")] private HandGhostProvider _handGhostProvider; #endif /// /// This ScriptableObject stores the HandGrabInteractables generated at Play-Mode so it survives /// the Play-Edit cycle. /// Create a collection and assign it even in Play Mode and make sure to store here the /// interactables, then restore it in Edit-Mode to be serialized. /// [SerializeField] private HandGrabInteractableDataCollection _posesCollection; /// /// The keyboard key that can be pressed for recording a hand grab pose /// [SerializeField] private KeyCode _recordKey = KeyCode.Space; private GUIStyle _richTextStyle; private Vector2 _scrollPos = Vector2.zero; [MenuItem("Meta/Interaction/Hand Grab Pose Recorder")] private static void CreateWizard() { HandGrabPoseWizard window = EditorWindow.GetWindow(); window.titleContent = new GUIContent("Hand Grab Pose Recorder"); window.Show(); } private void OnEnable() { _richTextStyle = EditorGUIUtility.GetBuiltinSkin(EditorGUIUtility.isProSkin ? EditorSkin.Scene : EditorSkin.Inspector).label; _richTextStyle.richText = true; _richTextStyle.wordWrap = true; if (GhostProvider == null) { #if ISDK_OPENXR_HAND HandGhostProviderUtils.TryGetDefaultProvider(out _handGhostProvider); #else HandGhostProviderUtils.TryGetDefaultProvider(out _ghostProvider); #endif } } private void OnGUI() { Event e = Event.current; if (e.type == EventType.KeyDown && e.keyCode == _recordKey) { RecordPose(); e.Use(); } GUILayout.Label("Generate HandGrabPoses for grabbing an item using your Hand in Play Mode.\nThen Store and retrieve them in Edit Mode to persist and tweak them.", _richTextStyle); _scrollPos = GUILayout.BeginScrollView(_scrollPos); GUILayout.Space(20); GUILayout.Label("1\nAssign the hand that will be tracked and the item for which you want to record HandGrabPoses", _richTextStyle); GUILayout.Label("Hand used for recording poses:"); Hand = EditorGUILayout.ObjectField(Hand, typeof(Hand), true) as Hand; GUILayout.Label("GameObject to record the hand grab poses for:"); GenerateObjectField(ref _item); GUILayout.Label("Prefabs provider for the hands (ghosts) to visualize the recorded poses:"); #if ISDK_OPENXR_HAND if (HandAnimationUtils.GenerateObjectField(ref _handGhostProvider)) #else if (HandAnimationUtils.GenerateObjectField(ref _ghostProvider)) #endif { HandGhostProviderUtils.SetLastDefaultProvider(GhostProvider); } GUILayout.Space(20); GUILayout.Label("2\nGo to Play Mode and record as many poses as you need.", _richTextStyle); GUILayout.Label($"Press the big Record button with your free hand\nor the {_recordKey} key to record a HandGrabPose (requires focus on this window).", _richTextStyle); _recordKey = (KeyCode)EditorGUILayout.EnumPopup(_recordKey); if (GUILayout.Button("Record HandGrabPose", GUILayout.Height(100))) { RecordPose(); } GUILayout.Space(20); GUILayout.Label("3\nStore your poses before exiting Play Mode.\nIf no collection is provided it will autogenerate one", _richTextStyle); GenerateObjectField(ref _posesCollection); if (GUILayout.Button("Save To Collection")) { SaveToAsset(); } GUILayout.Space(20); GUILayout.Label("4\nNow load the poses from the PosesCollection in Edit Mode to tweak and persist them as gameobjects", _richTextStyle); if (GUILayout.Button("Load From Collection")) { LoadFromAsset(); } GUILayout.EndScrollView(); } private void GenerateObjectField(ref T obj) where T : Object { obj = EditorGUILayout.ObjectField(obj, typeof(T), true) as T; } /// /// Finds the nearest object that can be snapped to and adds a new HandGrabInteractable to /// it with the user hand representation. /// public void RecordPose() { if (!Application.isPlaying) { Debug.LogError("Recording tracked hands only works in Play Mode!", this); return; } if (Hand == null) { Debug.LogError("Missing Hand reference.", this); return; } if (_item == null) { Debug.LogError("Missing recordable item", this); return; } HandPose trackedHandPose = TrackedPose(); if (trackedHandPose == null) { Debug.LogError("Tracked Pose could not be retrieved", this); return; } if (!Hand.GetRootPose(out Pose handRoot)) { Debug.LogError("Hand Root pose could not be retrieved", this); return; } Pose gripPoint = PoseUtils.DeltaScaled(_item.transform, handRoot); HandGrabPose point = AddHandGrabPose(trackedHandPose, gripPoint); AttachGhost(point); } private HandPose TrackedPose() { if (!Hand.GetJointPosesLocal(out ReadOnlyHandJointPoses localJoints)) { return null; } HandPose result = new HandPose(Hand.Handedness); for (int i = 0; i < FingersMetadata.HAND_JOINT_IDS.Length; ++i) { HandJointId jointID = FingersMetadata.HAND_JOINT_IDS[i]; result.JointRotations[i] = localJoints[jointID].rotation; } return result; } private void AttachGhost(HandGrabPose point) { if (GhostProvider == null) { return; } HandGhost ghostPrefab = GhostProvider.GetHand(Hand.Handedness); HandGhost ghost = GameObject.Instantiate(ghostPrefab, point.transform); ghost.SetPose(point); } /// /// Creates a new HandGrabInteractable at the exact pose of a given hand. /// Mostly used with Hand-Tracking at Play-Mode /// /// The user controlled hand pose. /// The user controlled hand pose. /// The generated HandGrabPose. private HandGrabPose AddHandGrabPose(HandPose rawPose, Pose snapPoint) { HandGrabInteractable interactable = HandGrabUtils.CreateHandGrabInteractable(_item.transform); var pointData = new HandGrabUtils.HandGrabPoseData() { handPose = rawPose, scale = Hand.Scale / interactable.RelativeTo.lossyScale.x, gripPose = snapPoint, }; return HandGrabUtils.LoadHandGrabPose(interactable, pointData); } /// /// Creates a new HandGrabInteractable from the stored data. /// Mostly used to restore a HandGrabInteractable that was stored during Play-Mode. /// /// The data of the HandGrabInteractable. /// The generated HandGrabInteractable. private HandGrabInteractable LoadHandGrabInteractable(HandGrabUtils.HandGrabInteractableData data) { HandGrabInteractable interactable = HandGrabUtils.CreateHandGrabInteractable(_item.transform); HandGrabUtils.LoadData(interactable, data); return interactable; } /// /// Stores the interactables to a SerializedObject (the empty object must be /// provided in the inspector or one will be auto-generated). First it translates the HandGrabInteractable to a serialized /// form HandGrabbableData). /// This method is called from a button in the Inspector. /// private void SaveToAsset() { if (_posesCollection == null) { GenerateCollectionAsset(); } var savedPoses = new List(); foreach (HandGrabInteractable snap in _item.GetComponentsInChildren(false)) { savedPoses.Add(HandGrabUtils.SaveData(snap)); } _posesCollection.StoreInteractables(savedPoses); } /// /// Load the HandGrabInteractable from a Collection. /// This method is called from a button in the Inspector and will load the posesCollection. /// private void LoadFromAsset() { if (_posesCollection == null) { return; } foreach (var handPose in _posesCollection.InteractablesData) { LoadHandGrabInteractable(handPose); } } public void GenerateCollectionAsset() { _posesCollection = ScriptableObject.CreateInstance(); string parentDir = Path.Combine("Assets", "HandGrabInteractableDataCollection"); if (!Directory.Exists(parentDir)) { Directory.CreateDirectory(parentDir); } string name = _item != null ? _item.name : "Auto"; AssetDatabase.CreateAsset(_posesCollection, Path.Combine(parentDir, $"{name}_HandGrabCollection.asset")); AssetDatabase.SaveAssets(); } } }