using System;
#if USE_LEGACY_INPUT_HELPERS
using UnityEngine.SpatialTracking;
#endif
namespace UnityEngine.XR.ARFoundation
{
///
/// An ARSessionOrigin is the parent for an AR setup. It contains a Camera and
/// any GameObjects created from detected features, such as planes or point clouds.
///
///
/// Session space vs. Unity space
///
/// Because an AR device will be used to drive the Camera's position and rotation,
/// you cannot directly place the Camera at an arbitrary position in the Unity scene.
/// Instead, you should position the ARSessionOrigin. This makes the Camera
/// (and any detected features) relative to that as a result.
///
/// It is important to keep the Camera and detected features in the same space relative to
/// each other (otherwise, detected features like planes won't appear in the correct place relative
/// to the Camera). The space relative to the AR device's starting position is called
/// "session space" or "device space". For example, when the AR session begins, the device might
/// report its position as (0, 0, 0). Detected features, such as planes, will be reported relative
/// to this starting position. The purpose of the ARSessionOrigin is to convert the session space
/// to Unity world space.
///
/// To facilitate this, the ARSessionOrigin creates a new GameObject called "Trackables"
/// as a sibling of its Camera. This should be the parent GameObject for all
/// detected features.
///
/// At runtime, a typical scene graph might look like this:
/// - AR Session Origin
/// - Camera
/// - Trackables
/// - Detected plane 1
/// - Detected plane 2
/// - Point cloud
/// - etc...
///
/// You can access the "trackables" GameObject with .
///
/// Note that the localPosition and localRotation of detected trackables
/// remain in real-world meters relative to the AR device's starting position and rotation.
///
/// Scale
///
/// If you want to scale the content rendered by the ARSessionOrigin you should apply
/// the scale to the ARSessionOrigin's transform. This is preferrable to scaling
/// the content directly, which can have undesirable side effects. Physics and NavMeshes,
/// for example, do not perform well when scaled very small.
///
[DisallowMultipleComponent]
[HelpURL(HelpUrls.ApiWithNamespace + nameof(ARSessionOrigin) + ".html")]
public class ARSessionOrigin : MonoBehaviour
{
[SerializeField]
[Tooltip("The Camera to associate with the AR device.")]
Camera m_Camera;
///
/// The Camera to associate with the AR device. It must be a child of this ARSessionOrigin.
///
///
/// The Camera should update its position and rotation according to the AR device.
/// This is typically accomplished by adding a TrackedPoseDriver component to the
/// Camera.
///
#if UNITY_EDITOR
public new Camera camera
#else
public Camera camera
#endif
{
get { return m_Camera; }
set { m_Camera = value; }
}
///
/// The parent Transform for all "trackables" (for example, planes and feature points).
///
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;
GameObject m_ContentOffsetGameObject;
Transform contentOffsetTransform
{
get
{
if (m_ContentOffsetGameObject == null)
{
// Insert a GameObject directly below the rig
m_ContentOffsetGameObject = new GameObject("Content Placement Offset");
m_ContentOffsetGameObject.transform.SetParent(transform, false);
// Re-parent any children of the ARSessionOrigin
for (var i = 0; i < transform.childCount; ++i)
{
var child = transform.GetChild(i);
if (child != m_ContentOffsetGameObject.transform)
{
child.SetParent(m_ContentOffsetGameObject.transform, true);
--i; // Decrement because childCount is also one less.
}
}
}
return m_ContentOffsetGameObject.transform;
}
}
///
/// Makes appear to be placed at with orientation .
///
/// The Transform of the content you wish to affect.
/// The position you wish the content to appear at. This could be
/// a position on a detected plane, for example.
/// The rotation the content should appear to be in, relative
/// to the Camera.
///
/// This method does not actually change the Transform of content; instead,
/// it updates the ARSessionOrigin's Transform so the content
/// appears to be at the given position and rotation. This is useful for placing AR
/// content onto surfaces when the content itself cannot be moved at runtime.
/// For example, if your content includes terrain or a NavMesh, it cannot
/// be moved or rotated dynamically.
///
public void MakeContentAppearAt(Transform content, Vector3 position, Quaternion rotation)
{
MakeContentAppearAt(content, position);
MakeContentAppearAt(content, rotation);
}
///
/// Makes appear to be placed at .
///
/// The Transform of the content you wish to affect.
/// The position you wish the content to appear at. This could be
/// a position on a detected plane, for example.
///
/// This method does not actually change the Transform of content; instead,
/// it updates the ARSessionOrigin's Transform so the content
/// appears to be at the given position.
///
public void MakeContentAppearAt(Transform content, Vector3 position)
{
if (content == null)
throw new ArgumentNullException("content");
// Adjust the "point of interest" transform to account
// for the actual position we want the content to appear at.
contentOffsetTransform.position += transform.position - position;
// The ARSessionOrigin's position needs to match the content's pivot. This is so
// the entire ARSessionOrigin rotates around the content (so the impression is that
// the content is rotating, not the rig).
transform.position = content.position;
}
///
/// Makes appear to have orientation relative to the Camera.
///
/// The Transform of the content you wish to affect.
/// The rotation the content should appear to be in, relative
/// to the Camera.
///
/// This method does not actually change the Transform of content; instead,
/// it updates the ARSessionOrigin's Transform so that the content
/// appears to be in the requested orientation.
///
public void MakeContentAppearAt(Transform content, Quaternion rotation)
{
if (content == null)
throw new ArgumentNullException("content");
// Since we aren't rotating the content, we need to perform the inverse
// operation on the ARSessionOrigin. For example, if we want the
// content to appear to be rotated 90 degrees on the Y axis, we should
// rotate our rig -90 degrees on the Y axis.
transform.rotation = Quaternion.Inverse(rotation) * content.rotation;
}
void Awake()
{
// 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.localPosition = Vector3.zero;
trackablesParent.localRotation = Quaternion.identity;
trackablesParent.localScale = Vector3.one;
if (camera)
{
var arPoseDriver = camera.GetComponent();
#if USE_LEGACY_INPUT_HELPERS
var trackedPoseDriver = camera.GetComponent();
// Warn if not using a ARPoseDriver or a TrackedPoseDriver
if (arPoseDriver == null && trackedPoseDriver == null)
{
Debug.LogWarning(
$"Camera \"{camera.name}\" does not use a AR Pose Driver or a Tracked Pose Driver, " +
"so its transform will not be updated by an XR device. In order for this to be " +
"updated, please add either an AR Pose Driver or a Tracked Pose Driver.");
}
// If we are using an TrackedPoseDriver, and the user hasn't chosen "make relative"
// then warn if the camera has a non-identity transform (since it will be overwritten).
else if ((trackedPoseDriver != null && !trackedPoseDriver.UseRelativeTransform))
{
var cameraTransform = camera.transform;
if ((cameraTransform.localPosition != Vector3.zero) || (cameraTransform.localRotation != Quaternion.identity))
{
Debug.LogWarning(
$"Camera \"{camera.name}\" has a non-identity transform " +
$"(position = {cameraTransform.localPosition}, rotation = {cameraTransform.localRotation}). " +
"The camera's local position and rotation will be overwritten by the XR device, " +
"so this starting transform will have no effect. Tick the \"Make Relative\" " +
"checkbox on the camera's Tracked Pose Driver to apply this starting transform.");
}
}
// If using ARPoseDriver then it will get overwritten no matter what
else
{
var cameraTransform = camera.transform;
if ((cameraTransform.localPosition != Vector3.zero) || (cameraTransform.localRotation != Quaternion.identity))
{
Debug.LogWarning(
$"Camera \"{camera.name}\" has a non-identity transform " +
$"(position = {cameraTransform.localPosition}, rotation = {cameraTransform.localRotation}). " +
"The camera's local position and rotation will be overwritten by the XR device.");
}
}
#else // !(USE_LEGACY_INPUT_HELPERS
if (arPoseDriver == null)
{
Debug.LogWarning(
$"Camera \"{camera.name}\" does not use a AR Pose Driver, so its transform will not be updated by " +
"an XR device. In order for this to be updated, please add an AR Pose Driver component.");
}
#endif // USE_LEGACY_INPUT_HELPERS
}
}
Pose GetCameraOriginPose()
{
var localOriginPose = Pose.identity;
var parent = camera.transform.parent;
#if USE_LEGACY_INPUT_HELPERS
var trackedPoseDriver = camera.GetComponent();
if (trackedPoseDriver)
{
localOriginPose = trackedPoseDriver.originPose;
}
#endif // USE_LEGACY_INPUT_HELPERS
return parent
? parent.TransformPose(localOriginPose)
: localOriginPose;
}
void OnEnable() => Application.onBeforeRender += OnBeforeRender;
void OnDisable() => Application.onBeforeRender -= OnBeforeRender;
void OnBeforeRender()
{
if (camera)
{
var pose = GetCameraOriginPose();
trackablesParent.position = pose.position;
trackablesParent.rotation = pose.rotation;
}
if (trackablesParent.hasChanged)
{
trackablesParentTransformChanged?.Invoke(
new ARTrackablesParentTransformChangedEventArgs(this, trackablesParent));
trackablesParent.hasChanged = false;
}
}
#if UNITY_EDITOR && USE_LEGACY_INPUT_HELPERS
void OnValidate()
{
if (camera)
{
if ((camera.GetComponent()?.enabled ?? false) &&
(camera.GetComponent()?.enabled ?? false))
{
Debug.LogWarning(
$"Camera \"{camera.name}\" has an AR Pose Driver and a Tracked Pose Driver and both are enabled. " +
"This configuration will cause the camera transform to be updated from both components in a non-deterministic " +
"way. This is likely an unintended error.");
}
}
}
#endif // UNITY_EDITOR && USE_LEGACY_INPUT_HELPERS
}
}