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