// 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
}
}
}