using System; using System.Collections.Generic; using System.ComponentModel; using JetBrains.Annotations; using Unity.XR.CoreUtils; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.InputSystem; using UnityEngine.Rendering; using UnityEngine.UI; #if AR_FOUNDATION_5_0_OR_NEWER using UnityEngine.XR.ARFoundation; #endif using UnityEngine.XR.Interaction.Toolkit; using UnityEngine.XR.Interaction.Toolkit.Attachment; using UnityEngine.XR.Interaction.Toolkit.Inputs; using UnityEngine.XR.Interaction.Toolkit.Interactables; using UnityEngine.XR.Interaction.Toolkit.Interactors; using UnityEngine.XR.Interaction.Toolkit.Interactors.Casters; using UnityEngine.XR.Interaction.Toolkit.Interactors.Visuals; using UnityEngine.XR.Interaction.Toolkit.Locomotion.Teleportation; using UnityEngine.XR.Interaction.Toolkit.Transformers; using UnityEngine.XR.Interaction.Toolkit.UI; using Object = UnityEngine.Object; namespace UnityEditor.XR.Interaction.Toolkit { static class CreateUtils { internal enum HardwareTarget { VR, MobileAR, } const string k_LineMaterial = "Default-Line.mat"; const string k_UILayerName = "UI"; [MenuItem("GameObject/XR/Near-Far Interactor", false, 10), UsedImplicitly] public static void CreateNearFarInteractor(MenuCommand menuCommand) { CreateInteractionManager(); Finalize(CreateNearFarInteractor(menuCommand?.GetContextTransform())); } [MenuItem("GameObject/XR/Ray Interactor", false, 10), UsedImplicitly] public static void CreateRayInteractor(MenuCommand menuCommand) { CreateInteractionManager(); Finalize(CreateRayInteractor(menuCommand?.GetContextTransform())); } [MenuItem("GameObject/XR/Direct Interactor", false, 10), UsedImplicitly] public static void CreateDirectInteractor(MenuCommand menuCommand) { CreateInteractionManager(); Finalize(CreateDirectInteractor(menuCommand?.GetContextTransform())); } [MenuItem("GameObject/XR/Gaze Interactor", false, 10), UsedImplicitly] public static void CreateGazeInteractor(MenuCommand menuCommand) { CreateInteractionManager(); Finalize(CreateGazeInteractor(menuCommand?.GetContextTransform())); } [MenuItem("GameObject/XR/Socket Interactor", false, 10), UsedImplicitly] public static void CreateSocketInteractor(MenuCommand menuCommand) { CreateInteractionManager(); var socketInteractableGO = CreateAndPlaceGameObject("Socket Interactor", menuCommand?.GetContextTransform(), typeof(SphereCollider), typeof(XRSocketInteractor)); var sphereCollider = socketInteractableGO.GetComponent(); sphereCollider.isTrigger = true; sphereCollider.radius = GetScaledRadius(sphereCollider, 0.1f); Finalize(socketInteractableGO); } [MenuItem("GameObject/XR/Grab Interactable", false, 10), UsedImplicitly] public static void CreateGrabInteractable(MenuCommand menuCommand) { CreateInteractionManager(); var grabInteractableGO = CreateAndPlacePrimitive("Grab Interactable", menuCommand?.GetContextTransform(), PrimitiveType.Cube, typeof(XRGrabInteractable), typeof(XRGeneralGrabTransformer)); var transform = grabInteractableGO.transform; var localScale = InverseTransformScale(transform, new Vector3(0.1f, 0.1f, 0.1f)); transform.localScale = Abs(localScale); var boxCollider = grabInteractableGO.GetComponent(); // BoxCollider does not support a negative effective size, // so ensure the size accounts for any negative scaling. boxCollider.size = Vector3.Scale(boxCollider.size, Sign(localScale)); var rigidbody = grabInteractableGO.GetComponent(); // Enable interpolation on the Rigidbody to smooth movement rigidbody.interpolation = RigidbodyInterpolation.Interpolate; // Workaround for an extra "Modified Permitted Displacement Axes" undo operation // requiring two Ctrl+Z undo operations to undo the creation of the grab interactable. // Setting it to -1 instead of the default All = X | Y | Z suppresses that extraneous undo operation // while having no change in the functionality of the transformer. // This seems to be due to the behavior of how serialized [Flags] are handled by Unity. var transformer = grabInteractableGO.GetComponent(); transformer.permittedDisplacementAxes = (XRGeneralGrabTransformer.ManipulationAxes)(-1); Finalize(grabInteractableGO); } [MenuItem("GameObject/XR/Interactable Snap Volume", false, 10), UsedImplicitly] public static void CreateInteractableSnapVolume(MenuCommand menuCommand) { CreateInteractionManager(); var snapVolumeGO = CreateAndPlaceGameObject("Interactable Snap Volume", menuCommand?.GetContextTransform(), typeof(SphereCollider), typeof(XRInteractableSnapVolume)); var sphereCollider = snapVolumeGO.GetComponent(); sphereCollider.isTrigger = true; sphereCollider.radius = GetScaledRadius(sphereCollider, 0.2f); // The Reset method will not find the Interactable up the hierarchy because it runs before being re-parented, // so the initialization of the property is repeated here. var snapVolume = snapVolumeGO.GetComponent(); var interactable = snapVolumeGO.GetComponentInParent(); snapVolume.interactableObject = interactable as Object; if (snapVolume.interactableObject != null) { var col = interactable.transform.GetComponent(); if (col != null && col.enabled && !col.isTrigger) snapVolume.snapToCollider = col; } Finalize(snapVolumeGO); } [MenuItem("GameObject/XR/Interaction Manager", false, 10), UsedImplicitly] public static void CreateInteractionManager(MenuCommand menuCommand) { Finalize(CreateInteractionManager(menuCommand?.GetContextTransform())); } [MenuItem("GameObject/XR/Teleportation Area", false, 10), UsedImplicitly] public static void CreateTeleportationArea(MenuCommand menuCommand) { CreateInteractionManager(); Finalize(CreateAndPlacePrimitive("Teleportation Area", menuCommand?.GetContextTransform(), PrimitiveType.Plane, typeof(TeleportationArea))); } [MenuItem("GameObject/XR/Teleportation Anchor", false, 10), UsedImplicitly] public static void CreateTeleportationAnchor(MenuCommand menuCommand) { CreateInteractionManager(); var anchorGO = CreateAndPlacePrimitive("Teleportation Anchor", menuCommand?.GetContextTransform(), PrimitiveType.Plane, typeof(TeleportationAnchor)); var destinationGO = ObjectFactory.CreateGameObject("Anchor"); Place(destinationGO, anchorGO.transform); var teleportationAnchor = anchorGO.GetComponent(); teleportationAnchor.teleportAnchorTransform = destinationGO.transform; Finalize(anchorGO); } [MenuItem("GameObject/XR/UI Canvas", false, 10), UsedImplicitly] public static void CreateXRUICanvas(MenuCommand menuCommand) { var parentOfNewGameObject = menuCommand?.GetContextTransform(); var currentStage = StageUtility.GetCurrentStageHandle(); var editingPrefabStage = currentStage != StageUtility.GetMainStageHandle(); var canvasGO = CreateAndPlaceGameObject("Canvas", parentOfNewGameObject, typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster), typeof(TrackedDeviceGraphicRaycaster)); // Either inherit the layer of the parent object, or use the same default that GameObject/UI/Canvas uses. if (parentOfNewGameObject == null) canvasGO.layer = LayerMask.NameToLayer(k_UILayerName); var canvas = canvasGO.GetComponent(); canvas.renderMode = RenderMode.WorldSpace; if (!editingPrefabStage) canvas.worldCamera = Camera.main; else Debug.LogWarning("You have just added an XR UI Canvas to a prefab." + " To function properly with an XR Ray Interactor, you must also set the Canvas component's Event Camera in your scene.", canvasGO); // Ensure there is at least one EventSystem setup properly var inputModule = currentStage.FindComponentOfType(); if (inputModule == null || !inputModule.gameObject.scene.IsValid()) { if (!editingPrefabStage) CreateXRUIEventSystemWithParent(parentOfNewGameObject, out _); else Debug.LogWarning("You have just added an XR UI Canvas to a prefab." + " To function properly with an XR Ray Interactor, you must also add an XR UI Event System to your scene.", canvasGO); } Finalize(canvasGO); } [MenuItem("GameObject/XR/UI Event System", false, 10), UsedImplicitly] public static void CreateXRUIEventSystem(MenuCommand menuCommand) { var eventSystemGO = CreateXRUIEventSystemWithParent(menuCommand?.GetContextTransform(), out var changeSelectionOnly); // If there was no serialization change (it already existed), only update the selection. // Passing it to Undo.RegisterCreatedObjectUndo in Finalize would cause the GameObject to be destroyed // upon Undo, which should not happen. This matches the behavior of GameObject > UI > Event System. if (changeSelectionOnly) Selection.activeGameObject = eventSystemGO; else Finalize(eventSystemGO); } [MenuItem("GameObject/XR/XR Origin (VR)", false, 10), UsedImplicitly] public static void CreateXROriginForVR(MenuCommand menuCommand) { CreateInteractionManager(); Finalize(CreateXROriginWithParent(menuCommand?.GetContextTransform(), HardwareTarget.VR)); } #if AR_FOUNDATION_5_0_OR_NEWER [MenuItem("GameObject/XR/XR Origin (Mobile AR)", false, 10), UsedImplicitly] static void CreateXROriginForAR(MenuCommand menuCommand) { CreateInteractionManager(); Finalize(CreateXROriginWithParent(menuCommand?.GetContextTransform(), HardwareTarget.MobileAR)); } #endif /// /// Registers on the Undo stack as the root of a newly created GameObject hierarchy and selects it. /// Components on and its children, if destroyed and recreated via Undo/Redo, will be recreated /// in their state from when this method was called. /// /// The newly created root GameObject. static void Finalize(GameObject gameObject) { Undo.RegisterCreatedObjectUndo(gameObject, $"Create {gameObject.name}"); Selection.activeGameObject = gameObject; } /// /// Create the if necessary. /// /// The parent to use. static GameObject CreateInteractionManager(Transform parent = null) { var currentStage = StageUtility.GetCurrentStageHandle(); var interactionManager = currentStage.FindComponentOfType(); if (interactionManager == null || !interactionManager.gameObject.scene.IsValid()) return CreateAndPlaceGameObject("XR Interaction Manager", parent, typeof(XRInteractionManager)); return interactionManager.gameObject; } static GameObject CreateNearFarInteractor(Transform parent, string name = "Near-Far Interactor") { var nearFarInteractableGO = CreateAndPlaceGameObject(name, parent, typeof(NearFarInteractor), typeof(InteractionAttachController), typeof(SphereInteractionCaster), typeof(CurveInteractionCaster)); var interactor = nearFarInteractableGO.GetComponent(); var attachController = nearFarInteractableGO.GetComponent(); interactor.interactionAttachController = attachController; var nearCaster = nearFarInteractableGO.GetComponent(); interactor.nearInteractionCaster = nearCaster; var farCaster = nearFarInteractableGO.GetComponent(); interactor.farInteractionCaster = farCaster; var lineVisualGO = CreateAndPlaceGameObject("Line Visual", nearFarInteractableGO.transform, typeof(LineRenderer), typeof(CurveVisualController), typeof(SortingGroup)); var lineRenderer = lineVisualGO.GetComponent(); SetupLineRenderer(lineRenderer); var curveVisualController = lineVisualGO.GetComponent(); curveVisualController.lineRenderer = lineRenderer; curveVisualController.curveInteractionDataProvider = interactor; curveVisualController.lineOriginTransform = curveVisualController.transform; // Add a Sorting Group with a custom sorting order to make it render in front of UGUI var sortingGroup = lineVisualGO.GetComponent(); sortingGroup.sortingOrder = 30005; return nearFarInteractableGO; } static GameObject CreateRayInteractor(Transform parent, string name = "Ray Interactor") { var rayInteractableGO = CreateAndPlaceGameObject(name, parent, typeof(XRRayInteractor), typeof(LineRenderer), typeof(XRInteractorLineVisual), typeof(SortingGroup)); SetupLineRenderer(rayInteractableGO.GetComponent()); // Add a Sorting Group with a custom sorting order to make it render in front of UGUI var sortingGroup = rayInteractableGO.GetComponent(); sortingGroup.sortingOrder = 30005; return rayInteractableGO; } static GameObject CreateGazeInteractor(Transform parent, string name = "Gaze Interactor") { var gazeInteractableGO = CreateAndPlaceGameObject(name, parent, typeof(XRGazeInteractor), typeof(SortingGroup)); // Add a Sorting Group with a custom sorting order to make it render in front of UGUI var sortingGroup = gazeInteractableGO.GetComponent(); sortingGroup.sortingOrder = 30005; return gazeInteractableGO; } static void SetupLineRenderer(LineRenderer lineRenderer) { var materials = new Material[1]; materials[0] = AssetDatabase.GetBuiltinExtraResource(k_LineMaterial); lineRenderer.materials = materials; lineRenderer.loop = false; lineRenderer.widthMultiplier = 0.005f; lineRenderer.numCornerVertices = 4; lineRenderer.numCapVertices = 4; lineRenderer.shadowCastingMode = ShadowCastingMode.Off; lineRenderer.receiveShadows = false; lineRenderer.useWorldSpace = true; } static GameObject CreateDirectInteractor(Transform parent, string name = "Direct Interactor") { var directInteractorGO = CreateAndPlaceGameObject(name, parent, typeof(SphereCollider), typeof(XRDirectInteractor)); var sphereCollider = directInteractorGO.GetComponent(); sphereCollider.isTrigger = true; sphereCollider.radius = GetScaledRadius(sphereCollider, 0.1f); return directInteractorGO; } static GameObject CreateXRUIEventSystemWithParent(Transform parent, out bool changeSelectionOnly) { var currentStage = StageUtility.GetCurrentStageHandle(); var inputModule = currentStage.FindComponentOfType(); if (inputModule != null && inputModule.gameObject.scene.IsValid()) { changeSelectionOnly = true; return inputModule.gameObject; } // Ensure there is at least one EventSystem setup properly var eventSystem = currentStage.FindComponentOfType(); GameObject eventSystemGO; if (eventSystem == null || !eventSystem.gameObject.scene.IsValid()) { eventSystemGO = CreateAndPlaceGameObject("EventSystem", parent, typeof(EventSystem), typeof(XRUIInputModule)); } else { eventSystemGO = eventSystem.gameObject; // Remove the Standalone Input Module if already implemented, since it will block the XRUIInputModule var standaloneInputModule = eventSystemGO.GetComponent(); if (standaloneInputModule != null) Undo.DestroyObjectImmediate(standaloneInputModule); Undo.AddComponent(eventSystemGO); } changeSelectionOnly = false; return eventSystemGO; } static GameObject CreateXROriginWithParent(Transform parent, HardwareTarget target) { GameObject originGo; if (target == HardwareTarget.VR) { originGo = CreateAndPlaceGameObject("XR Origin (VR)", parent, typeof(XROrigin), typeof(InputActionManager)); } else if (target == HardwareTarget.MobileAR) { var types = new[] { typeof(XROrigin), typeof(InputActionManager), #if AR_FOUNDATION_5_0_OR_NEWER typeof(ARPlaneManager), typeof(ARRaycastManager), #endif }; originGo = CreateAndPlaceGameObject("XR Origin (Mobile AR)", parent, types); } else { throw new InvalidEnumArgumentException($"Invalid {nameof(HardwareTarget)}: {target}"); } var offsetGo = CreateAndPlaceGameObject("Camera Offset", originGo.transform); var offsetTransform = offsetGo.transform; var xrCamera = XRMainCameraFactory.CreateXRMainCamera(target); Place(xrCamera.gameObject, offsetTransform); var origin = originGo.GetComponent(); origin.CameraFloorOffsetObject = offsetGo; origin.Camera = xrCamera; if (target == HardwareTarget.VR) { // Set the Camera Offset y position based on the default height. // This will make the Scene view of the Camera when not in Play mode more closely match // what the position will be when entering Play mode. In Device mode, it will be this value. // In Floor mode, it will get reset to 0, but will at least be higher than the XROrigin position. offsetTransform.localPosition = new Vector3(0f, origin.CameraYOffset, 0f); } else if (target == HardwareTarget.MobileAR) { offsetTransform.localPosition = Vector3.zero; origin.RequestedTrackingOriginMode = XROrigin.TrackingOriginMode.Device; origin.CameraYOffset = 0f; } var inputActionManager = originGo.GetComponent(); const string assetName = "XRI Default Input Actions"; const string searchFilter = "\"" + assetName + "\" t:InputActionAsset"; foreach (var guid in AssetDatabase.FindAssets(searchFilter)) { var path = AssetDatabase.GUIDToAssetPath(guid); var asset = AssetDatabase.LoadAssetAtPath(path); // The search filter string will return all assets that contains the name, // so ensure an exact match to the expected asset we want to set. if (asset.name.Equals(assetName, StringComparison.OrdinalIgnoreCase)) { inputActionManager.actionAssets = new List { asset }; break; } } return originGo; } /// /// Gets the associated with the . /// /// The object passed to custom menu item functions to operate on. /// Returns the of the object that is the target of a menu command, /// or if there is no context. static Transform GetContextTransform(this MenuCommand menuCommand) { var context = menuCommand.context as GameObject; return context != null ? context.transform : null; } static GameObject CreateAndPlaceGameObject(string name, Transform parent, params Type[] types) { var go = ObjectFactory.CreateGameObject(name, types); Place(go, parent); return go; } static GameObject CreateAndPlacePrimitive(string name, Transform parent, PrimitiveType primitiveType, params Type[] types) { var go = ObjectFactory.CreatePrimitive(primitiveType); go.name = name; go.SetActive(false); foreach (var type in types) ObjectFactory.AddComponent(go, type); go.SetActive(true); Place(go, parent); return go; } static void Place(GameObject go, Transform parent) { var transform = go.transform; if (parent != null) { Undo.SetTransformParent(transform, parent, "Reparenting"); ResetTransform(transform); go.layer = parent.gameObject.layer; } else { // Puts it at the scene pivot, and otherwise world origin if there is no Scene view. var view = SceneView.lastActiveSceneView; if (view != null) view.MoveToView(transform); else transform.position = Vector3.zero; StageUtility.PlaceGameObjectInCurrentStage(go); } // Only at this point do we know the actual parent of the object and can modify its name accordingly. GameObjectUtility.EnsureUniqueNameForSibling(go); } static void ResetTransform(Transform transform) { transform.localPosition = Vector3.zero; transform.localRotation = Quaternion.identity; transform.localScale = Vector3.one; if (transform.parent is RectTransform) { var rectTransform = transform as RectTransform; if (rectTransform != null) { rectTransform.anchorMin = Vector2.zero; rectTransform.anchorMax = Vector2.one; rectTransform.anchoredPosition = Vector2.zero; rectTransform.sizeDelta = Vector2.zero; } } } /// /// Returns the absolute value of each component of the vector. /// /// The vector. /// Returns the absolute value of each component of the vector. /// static Vector3 Abs(Vector3 value) => new Vector3(Mathf.Abs(value.x), Mathf.Abs(value.y), Mathf.Abs(value.z)); /// /// Returns the sign of each component of the vector. /// /// The vector. /// Returns the sign of each component of the vector; 1 when the component is positive or zero, -1 when the component is negative. /// static Vector3 Sign(Vector3 value) => new Vector3(Mathf.Sign(value.x), Mathf.Sign(value.y), Mathf.Sign(value.z)); /// /// Transforms a vector from world space to local space. /// Differs from in that /// this operation is unaffected by rotation. /// /// The the operation is relative to. /// The scale to transform. /// Returns the scale in local space. static Vector3 InverseTransformScale(Transform transform, Vector3 scale) { var lossyScale = transform.lossyScale; return new Vector3( !Mathf.Approximately(lossyScale.x, 0f) ? scale.x / lossyScale.x : scale.x, !Mathf.Approximately(lossyScale.y, 0f) ? scale.y / lossyScale.y : scale.y, !Mathf.Approximately(lossyScale.z, 0f) ? scale.z / lossyScale.z : scale.z); } static float GetRadiusScaleFactor(SphereCollider collider) { // Copied from SphereColliderEditor var result = 0f; var lossyScale = collider.transform.lossyScale; for (var axis = 0; axis < 3; ++axis) result = Mathf.Max(result, Mathf.Abs(lossyScale[axis])); return result; } static float GetScaledRadius(SphereCollider collider, float radius) { var scaleFactor = GetRadiusScaleFactor(collider); return !Mathf.Approximately(scaleFactor, 0f) ? radius / scaleFactor : 0f; } } }