// ENABLE_VR is not defined on Game Core but the assembly is available with limited features when the XR module is enabled. #if ENABLE_VR || UNITY_GAMECORE #define XR_MODULE_AVAILABLE #endif using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Assertions; using UnityEngine.Serialization; using UnityEngine.XR; namespace Unity.XR.CoreUtils { /// /// The XR Origin represents the session-space origin (0, 0, 0) in an XR scene. /// /// /// The XR Origin component is typically attached to the base object of the XR Origin, /// and stores the that will be manipulated via locomotion. /// It is also used for offsetting the camera. /// [AddComponentMenu("XR/XR Origin")] [DisallowMultipleComponent] [HelpURL("https://docs.unity3d.com/Packages/com.unity.xr.core-utils@2.0/api/Unity.XR.CoreUtils.XROrigin.html")] public class XROrigin : MonoBehaviour { [SerializeField] [Tooltip("The Camera to associate with the XR device.")] Camera m_Camera; /// /// The used to render the scene from the point of view of the XR device. Must be a child of /// the containing this XROrigin component. /// /// /// You can add a component to the /// GameObject to update its position and rotation using tracking data from the XR device. /// You must update the position and rotation using tracking data from the XR device. /// public Camera Camera { get => m_Camera; set => m_Camera = value; } /// /// The parent Transform for all "trackables" (for example, planes and feature points). /// /// /// See [Trackables](xref:arfoundation-managers) for more information. /// public Transform TrackablesParent { get; private set; } /// /// Invoked during /// [Application.onBeforeRender](xref:UnityEngine.Application.onBeforeRender(UnityEngine.Events.UnityAction)) /// whenever the [transform](xref:UnityEngine.Transform) changes. /// public event Action TrackablesParentTransformChanged; /// /// Sets which Tracking Origin Mode to use when initializing the input device. /// /// /// /// public enum TrackingOriginMode { /// /// Uses the default Tracking Origin Mode of the input device. /// /// /// When changing to this value after startup, the Tracking Origin Mode will not be changed. /// NotSpecified, /// /// Sets the Tracking Origin Mode to . /// Input devices will be tracked relative to the first known location. /// /// /// Represents a device-relative tracking origin. A device-relative tracking origin defines a local origin /// at the position of the device in space at some previous point in time, usually at a recenter event, /// power-on, or AR/VR session start. Pose data provided by the device will be in this space relative to /// the local origin. This means that poses returned in this mode will not include the user height (for VR) /// or the device height (for AR) and any camera tracking from the XR device will need to be manually offset accordingly. /// /// Device, /// /// Sets the Tracking Origin Mode to . /// Input devices will be tracked relative to a location on the floor. /// /// /// Represents the tracking origin whereby (0, 0, 0) is on the "floor" or other surface determined by the /// XR device being used. The pose values reported by an XR device in this mode will include the height /// of the XR device above this surface, removing the need to offset the position of the camera tracking /// the XR device by the height of the user (VR) or the height of the device above the floor (AR). /// /// Floor, /// /// Sets the Tracking Origin Mode to . /// Input devices will be tracked relative to a location in the world. /// /// /// Represents a relative tracking origin. A relative tracking origin is defined as a point at an /// arbitrary position and rotation provided by the underlying XR Runtime. Pose data provided by the /// device will be in this space relative to the local origin. /// Unbounded } //This is the average seated height in meters (which equals 44 inches). const float k_DefaultCameraYOffset = 1.1176f; [SerializeField, FormerlySerializedAs("m_RigBaseGameObject")] GameObject m_OriginBaseGameObject; /// /// The "Origin" is used to refer to the base of the XR Origin, by default it is this . /// This is the that will be manipulated via locomotion. /// public GameObject Origin { get => m_OriginBaseGameObject; set => m_OriginBaseGameObject = value; } [SerializeField] GameObject m_CameraFloorOffsetObject; /// /// The to move to desired height off the floor (defaults to this object if none provided). /// This is used to transform the XR device from camera space to XR Origin space. /// public GameObject CameraFloorOffsetObject { get => m_CameraFloorOffsetObject; set { m_CameraFloorOffsetObject = value; MoveOffsetHeight(); } } [SerializeField] TrackingOriginMode m_RequestedTrackingOriginMode = TrackingOriginMode.NotSpecified; /// /// The type of tracking origin to use for this XROrigin. Tracking origins identify where (0, 0, 0) is in the world /// of tracking. Not all devices support all tracking origin modes. /// /// public TrackingOriginMode RequestedTrackingOriginMode { get => m_RequestedTrackingOriginMode; set { m_RequestedTrackingOriginMode = value; TryInitializeCamera(); } } [SerializeField] float m_CameraYOffset = k_DefaultCameraYOffset; /// /// Camera height to be used when in Device Tracking Origin Mode to define the height of the user from the floor. /// This is the amount that the camera is offset from the floor when moving the . /// public float CameraYOffset { get => m_CameraYOffset; set { m_CameraYOffset = value; MoveOffsetHeight(); } } #if XR_MODULE_AVAILABLE || PACKAGE_DOCS_GENERATION /// /// (Read Only) The Tracking Origin Mode of this XR Origin. /// /// public TrackingOriginModeFlags CurrentTrackingOriginMode { get; private set; } #endif /// /// (Read Only) The origin's local position in camera space. /// public Vector3 OriginInCameraSpacePos => m_Camera.transform.InverseTransformPoint(m_OriginBaseGameObject.transform.position); /// /// (Read Only) The camera's local position in origin space. /// public Vector3 CameraInOriginSpacePos => m_OriginBaseGameObject.transform.InverseTransformPoint(m_Camera.transform.position); /// /// (Read Only) The camera's height relative to the origin. /// public float CameraInOriginSpaceHeight => CameraInOriginSpacePos.y; #if XR_MODULE_AVAILABLE /// /// Used to cache the input subsystems without creating additional GC allocations. /// static readonly List s_InputSubsystems = new List(); #endif // Bookkeeping to track lazy initialization of the tracking origin mode type. bool m_CameraInitialized; bool m_CameraInitializing; /// /// Sets the height of the camera based on the current tracking origin mode by updating the . /// void MoveOffsetHeight() { #if XR_MODULE_AVAILABLE if (!Application.isPlaying) return; switch (CurrentTrackingOriginMode) { case TrackingOriginModeFlags.Floor: MoveOffsetHeight(0f); break; case TrackingOriginModeFlags.Device: case TrackingOriginModeFlags.Unbounded: MoveOffsetHeight(m_CameraYOffset); break; default: return; } #endif } /// /// Sets the height of the camera to the given value by updating the . /// /// The local y-position to set. void MoveOffsetHeight(float y) { if (m_CameraFloorOffsetObject != null) { var offsetTransform = m_CameraFloorOffsetObject.transform; var desiredPosition = offsetTransform.localPosition; desiredPosition.y = y; offsetTransform.localPosition = desiredPosition; } } /// /// Repeatedly attempt to initialize the camera. /// void TryInitializeCamera() { if (!Application.isPlaying) return; m_CameraInitialized = SetupCamera(); if (!m_CameraInitialized & !m_CameraInitializing) StartCoroutine(RepeatInitializeCamera()); } /// /// Handles re-centering and off-setting the camera in space depending on which tracking origin mode it is setup in. /// bool SetupCamera() { var initialized = true; #if XR_MODULE_AVAILABLE #if UNITY_2023_2_OR_NEWER SubsystemManager.GetSubsystems(s_InputSubsystems); #else SubsystemManager.GetInstances(s_InputSubsystems); #endif if (s_InputSubsystems.Count > 0) { foreach (var inputSubsystem in s_InputSubsystems) { if (SetupCamera(inputSubsystem)) { // It is possible this could happen more than // once so unregister the callback first just in case. inputSubsystem.trackingOriginUpdated -= OnInputSubsystemTrackingOriginUpdated; inputSubsystem.trackingOriginUpdated += OnInputSubsystemTrackingOriginUpdated; } else { initialized = false; } } } #endif return initialized; } #if XR_MODULE_AVAILABLE bool SetupCamera(XRInputSubsystem inputSubsystem) { if (inputSubsystem == null) return false; var successful = true; switch (m_RequestedTrackingOriginMode) { case TrackingOriginMode.NotSpecified: CurrentTrackingOriginMode = inputSubsystem.GetTrackingOriginMode(); break; case TrackingOriginMode.Device: case TrackingOriginMode.Floor: case TrackingOriginMode.Unbounded: { var supportedModes = inputSubsystem.GetSupportedTrackingOriginModes(); // We need to check for Unknown because we may not be in a state where we can read this data yet. if (supportedModes == TrackingOriginModeFlags.Unknown) return false; // Convert from the request enum to the flags enum that is used by the subsystem var equivalentFlagsMode = ConvertTrackingOriginModeToFlag(m_RequestedTrackingOriginMode); // ReSharper disable once BitwiseOperatorOnEnumWithoutFlags -- Treated like Flags enum when querying supported modes if ((supportedModes & equivalentFlagsMode) == 0) { m_RequestedTrackingOriginMode = TrackingOriginMode.NotSpecified; CurrentTrackingOriginMode = inputSubsystem.GetTrackingOriginMode(); Debug.LogWarning($"Attempting to set the tracking origin mode to {equivalentFlagsMode}, but that is not supported by the SDK." + $" Supported types: {supportedModes:F}. Using the current mode of {CurrentTrackingOriginMode} instead.", this); } else { successful = inputSubsystem.TrySetTrackingOriginMode(equivalentFlagsMode); } } break; default: Debug.LogError($"Unhandled {nameof(TrackingOriginMode)}={m_RequestedTrackingOriginMode}"); return false; } if (successful) MoveOffsetHeight(); if (CurrentTrackingOriginMode == TrackingOriginModeFlags.Device || m_RequestedTrackingOriginMode == TrackingOriginMode.Device || CurrentTrackingOriginMode == TrackingOriginModeFlags.Unbounded || m_RequestedTrackingOriginMode == TrackingOriginMode.Unbounded ) successful = inputSubsystem.TryRecenter(); return successful; } void OnInputSubsystemTrackingOriginUpdated(XRInputSubsystem inputSubsystem) { CurrentTrackingOriginMode = inputSubsystem.GetTrackingOriginMode(); MoveOffsetHeight(); } #endif IEnumerator RepeatInitializeCamera() { m_CameraInitializing = true; while (!m_CameraInitialized) { yield return null; if (!m_CameraInitialized) m_CameraInitialized = SetupCamera(); } m_CameraInitializing = false; } /// /// Rotates the XR origin object around the camera object by the provided . /// This rotation only occurs around the origin's Up vector /// /// The amount of rotation in degrees. /// Returns if the rotation is performed. Otherwise, returns . public bool RotateAroundCameraUsingOriginUp(float angleDegrees) { return RotateAroundCameraPosition(m_OriginBaseGameObject.transform.up, angleDegrees); } /// /// Rotates the XR origin object around the camera object's position in world space using the provided /// as the rotation axis. The XR Origin object is rotated by the amount of degrees provided in . /// /// The axis of the rotation. /// The amount of rotation in degrees. /// Returns if the rotation is performed. Otherwise, returns . public bool RotateAroundCameraPosition(Vector3 vector, float angleDegrees) { if (m_Camera == null || m_OriginBaseGameObject == null) { return false; } // Rotate around the camera position m_OriginBaseGameObject.transform.RotateAround(m_Camera.transform.position, vector, angleDegrees); return true; } /// /// This function will rotate the XR Origin object such that the XR Origin's up vector will match the provided vector. /// /// the vector to which the XR Origin object's up vector will be matched. /// Returns if the rotation is performed or the vectors have already been matched. /// Otherwise, returns . public bool MatchOriginUp(Vector3 destinationUp) { if (m_OriginBaseGameObject == null) { return false; } if (m_OriginBaseGameObject.transform.up == destinationUp) return true; var rigUp = Quaternion.FromToRotation(m_OriginBaseGameObject.transform.up, destinationUp); m_OriginBaseGameObject.transform.rotation = rigUp * transform.rotation; return true; } /// /// This function will rotate the XR Origin object around the camera object using the vector such that: /// /// /// The camera will look at the area in the direction of the /// /// /// The projection of camera's forward vector on the plane with the normal will be in the direction of /// /// /// The up vector of the XR Origin object will match the provided vector (note that the camera's Up vector can not be manipulated) /// /// /// /// The up vector that the origin's up vector will be matched to. /// The forward vector that will be matched to the projection of the camera's forward vector on the plane with the normal . /// Returns if the rotation is performed. Otherwise, returns . public bool MatchOriginUpCameraForward(Vector3 destinationUp, Vector3 destinationForward) { if (m_Camera != null && MatchOriginUp(destinationUp)) { // Project current camera's forward vector on the destination plane, whose normal vector is destinationUp. var projectedCamForward = Vector3.ProjectOnPlane(m_Camera.transform.forward, destinationUp).normalized; // The angle that we want the XROrigin to rotate is the signed angle between projectedCamForward and destinationForward, after the up vectors are matched. var signedAngle = Vector3.SignedAngle(projectedCamForward, destinationForward, destinationUp); RotateAroundCameraPosition(destinationUp, signedAngle); return true; } return false; } /// /// This function will rotate the XR Origin object around the camera object using the vector such that: /// /// /// The forward vector of the XR Origin object, which is the direction the player moves in Unity when walking forward in the physical world, will match the provided vector /// /// /// The up vector of the XR Origin object will match the provided vector /// /// /// /// The up vector that the origin's up vector will be matched to. /// The forward vector that will be matched to the forward vector of the XR Origin object, /// which is the direction the player moves in Unity when walking forward in the physical world. /// Returns if the rotation is performed. Otherwise, returns . public bool MatchOriginUpOriginForward(Vector3 destinationUp, Vector3 destinationForward) { if (m_OriginBaseGameObject != null && MatchOriginUp(destinationUp)) { // The angle that we want the XR Origin to rotate is the signed angle between the origin's forward and destinationForward, after the up vectors are matched. var signedAngle = Vector3.SignedAngle(m_OriginBaseGameObject.transform.forward, destinationForward, destinationUp); RotateAroundCameraPosition(destinationUp, signedAngle); return true; } return false; } /// /// This function moves the camera to the world location provided by . /// It does this by moving the XR Origin object so that the camera's world location matches the desiredWorldLocation /// /// the position in world space that the camera should be moved to /// Returns if the move is performed. Otherwise, returns . public bool MoveCameraToWorldLocation(Vector3 desiredWorldLocation) { if (m_Camera == null) { return false; } var rot = Matrix4x4.Rotate(m_Camera.transform.rotation); var delta = rot.MultiplyPoint3x4(OriginInCameraSpacePos); m_OriginBaseGameObject.transform.position = delta + desiredWorldLocation; return true; } /// /// See . /// protected void Awake() { if (m_CameraFloorOffsetObject == null) { Debug.LogWarning("No Camera Floor Offset GameObject specified for XR Origin, using attached GameObject.", this); m_CameraFloorOffsetObject = gameObject; } if (m_Camera == null) { var mainCamera = Camera.main; if (mainCamera != null) m_Camera = mainCamera; else Debug.LogWarning("No Main Camera is found for XR Origin, please assign the Camera field manually.", this); } // This will be the parent GameObject for any trackables (such as planes) for which // we want a corresponding GameObject. TrackablesParent = new GameObject("Trackables").transform; TrackablesParent.SetParent(transform, false); TrackablesParent.SetLocalPose(Pose.identity); TrackablesParent.localScale = Vector3.one; if (m_Camera) { #if INCLUDE_INPUT_SYSTEM && INCLUDE_LEGACY_INPUT_HELPERS var trackedPoseDriver = m_Camera.GetComponent(); var trackedPoseDriverOld = m_Camera.GetComponent(); if (trackedPoseDriver == null && trackedPoseDriverOld == null) { Debug.LogWarning( $"Camera \"{m_Camera.name}\" does not use a Tracked Pose Driver (Input System), " + "so its transform will not be updated by an XR device. In order for this to be " + "updated, please add a Tracked Pose Driver (Input System) with bindings for position and rotation of the center eye.", this); } #elif !INCLUDE_INPUT_SYSTEM && INCLUDE_LEGACY_INPUT_HELPERS var trackedPoseDriverOld = m_Camera.GetComponent(); if (trackedPoseDriverOld == null) { Debug.LogWarning( $"Camera \"{m_Camera.name}\" does not use a Tracked Pose Driver, and com.unity.xr.legacyinputhelpers is installed. " + "Although the Tracked Pose Driver from Legacy Input Helpers can be used, it is recommended to " + "install com.unity.inputsystem instead and add a Tracked Pose Driver (Input System) with bindings for position and rotation of the center eye.", this); } #elif INCLUDE_INPUT_SYSTEM && !INCLUDE_LEGACY_INPUT_HELPERS var trackedPoseDriver = m_Camera.GetComponent(); if (trackedPoseDriver == null) { Debug.LogWarning( $"Camera \"{m_Camera.name}\" does not use a Tracked Pose Driver (Input System), " + "so its transform will not be updated by an XR device. In order for this to be " + "updated, please add a Tracked Pose Driver (Input System) with bindings for position and rotation of the center eye.", this); } #elif !INCLUDE_INPUT_SYSTEM && !INCLUDE_LEGACY_INPUT_HELPERS Debug.LogWarning( $"Camera \"{m_Camera.name}\" does not use a Tracked Pose Driver and com.unity.inputsystem is not installed, " + "so its transform will not be updated by an XR device. In order for this to be " + "updated, please install com.unity.inputsystem and add a Tracked Pose Driver (Input System) with bindings for position and rotation of the center eye.", this); #endif } } Pose GetCameraOriginPose() { var localOriginPose = Pose.identity; var parent = m_Camera.transform.parent; return parent ? parent.TransformPose(localOriginPose) : localOriginPose; } /// /// See . /// protected void OnEnable() => Application.onBeforeRender += OnBeforeRender; /// /// See . /// protected void OnDisable() => Application.onBeforeRender -= OnBeforeRender; void OnBeforeRender() { if (m_Camera) { var pose = GetCameraOriginPose(); TrackablesParent.position = pose.position; TrackablesParent.rotation = pose.rotation; } if (TrackablesParent.hasChanged) { TrackablesParentTransformChanged?.Invoke( new ARTrackablesParentTransformChangedEventArgs(this, TrackablesParent)); TrackablesParent.hasChanged = false; } } /// /// See . /// protected void OnValidate() { if (m_OriginBaseGameObject == null) m_OriginBaseGameObject = gameObject; if (Application.isPlaying && isActiveAndEnabled) { // Respond to the mode changing by re-initializing the camera, // or just update the offset height in order to avoid recentering. if (IsModeStale()) TryInitializeCamera(); else MoveOffsetHeight(); } bool IsModeStale() { #if XR_MODULE_AVAILABLE if (s_InputSubsystems.Count > 0) { foreach (var inputSubsystem in s_InputSubsystems) { // Convert from the request enum to the flags enum that is used by the subsystem TrackingOriginModeFlags equivalentFlagsMode = ConvertTrackingOriginModeToFlag(m_RequestedTrackingOriginMode); if (equivalentFlagsMode == TrackingOriginModeFlags.Unknown) // Don't need to initialize the camera since we don't set the mode when NotSpecified (we just keep the current value) return false; if (inputSubsystem != null && inputSubsystem.GetTrackingOriginMode() != equivalentFlagsMode) { return true; } } } #endif return false; } } static TrackingOriginModeFlags ConvertTrackingOriginModeToFlag(TrackingOriginMode mode) { switch (mode) { case TrackingOriginMode.NotSpecified: return TrackingOriginModeFlags.Unknown; case TrackingOriginMode.Device: return TrackingOriginModeFlags.Device; case TrackingOriginMode.Floor: return TrackingOriginModeFlags.Floor; case TrackingOriginMode.Unbounded: return TrackingOriginModeFlags.Unbounded; default: Assert.IsTrue(false, $"Unhandled {nameof(TrackingOriginMode)}={mode}"); return TrackingOriginModeFlags.Unknown; } } /// /// See . /// protected void Start() { TryInitializeCamera(); } /// /// See . /// protected void OnDestroy() { #if XR_MODULE_AVAILABLE foreach (var inputSubsystem in s_InputSubsystems) { if (inputSubsystem != null) inputSubsystem.trackingOriginUpdated -= OnInputSubsystemTrackingOriginUpdated; } #endif } } }