using System; using System.Collections.Generic; using Unity.Collections; using UnityEngine.XR.ARSubsystems; using Unity.XR.CoreUtils; namespace UnityEngine.XR.ARFoundation { /// /// A [trackable manager](xref:arfoundation-managers#trackables-and-trackable-managers) that detects and tracks /// flat surfaces in the physical environment. Add this component to your XR Origin GameObject to enable plane /// detection in your app. /// /// /// Related information: AR Plane Manager component /// [DefaultExecutionOrder(ARUpdateOrder.k_PlaneManager)] [DisallowMultipleComponent] [RequireComponent(typeof(XROrigin))] [AddComponentMenu("XR/AR Foundation/AR Plane Manager")] [HelpURL("features/plane-detection/arplanemanager")] public sealed class ARPlaneManager : ARTrackableManager< XRPlaneSubsystem, XRPlaneSubsystemDescriptor, XRPlaneSubsystem.Provider, BoundedPlane, ARPlane>, IRaycaster { [SerializeField] [Tooltip( "If not null, this prefab is instantiated for each detected plane. " + "If the prefab does not contain an AR Plane component, ARPlaneManager will add one.")] GameObject m_PlanePrefab; /// /// Get or set the prefab to instantiate for each detected plane. Can be . /// public GameObject planePrefab { get => m_PlanePrefab; set => m_PlanePrefab = value; } [SerializeField, PlaneDetectionModeMask] [Tooltip("The types of planes to detect.")] PlaneDetectionMode m_DetectionMode = (PlaneDetectionMode)(-1); /// /// Get or set the requested plane detection mode. /// public PlaneDetectionMode requestedDetectionMode { get => subsystem?.requestedPlaneDetectionMode ?? m_DetectionMode; set { m_DetectionMode = value; if (enabled && subsystem != null) { subsystem.requestedPlaneDetectionMode = value; } } } /// /// Get the current plane detection mode in use by the subsystem. /// public PlaneDetectionMode currentDetectionMode => subsystem?.currentPlaneDetectionMode ?? PlaneDetectionMode.None; /// /// Invoked when planes have changed (been added, updated, or removed). /// [Obsolete("planesChanged has been deprecated in AR Foundation version 6.0. Use trackablesChanged instead.", false)] public event Action planesChanged; /// /// Attempt to retrieve an existing by . /// /// The of the plane to retrieve. /// The with , or null if it does not exist. public ARPlane GetPlane(TrackableId trackableId) => m_Trackables.TryGetValue(trackableId, out ARPlane plane) ? plane : null; /// /// Performs a raycast against all currently tracked planes. /// /// The ray, in Unity world space, to cast. /// A mask of raycast types to perform. /// The Allocator to use when creating the returned NativeArray. /// /// A new NativeArray of raycast results allocated with . /// The caller owns the memory and is responsible for calling Dispose on the NativeArray. /// /// /// public NativeArray Raycast( Ray ray, TrackableType trackableTypeMask, Allocator allocator) { // No plane types requested; early out. if ((trackableTypeMask & TrackableType.Planes) == TrackableType.None) return new NativeArray(0, allocator); var trackableCollection = trackables; // Allocate a buffer that is at least large enough to contain a hit against every plane var hitBuffer = new NativeArray(trackableCollection.count, Allocator.Temp); try { int count = 0; foreach (var plane in trackableCollection) { TrackableType trackableTypes = TrackableType.None; var t = plane.transform; var normal = t.localRotation * Vector3.up; var infinitePlane = new Plane(normal, t.localPosition); if (!infinitePlane.Raycast(ray, out var distance)) continue; // Pose in session space var pose = new Pose( ray.origin + ray.direction * distance, plane.transform.localRotation); if ((trackableTypeMask & TrackableType.PlaneWithinInfinity) != TrackableType.None) trackableTypes |= TrackableType.PlaneWithinInfinity; // To test the rest, we need the intersection point in plane space var hitPositionPlaneSpace3d = Quaternion.Inverse(plane.transform.localRotation) * (pose.position - plane.transform.localPosition); var hitPositionPlaneSpace = new Vector2(hitPositionPlaneSpace3d.x, hitPositionPlaneSpace3d.z); const TrackableType estimatedOrWithinBounds = TrackableType.PlaneWithinBounds | TrackableType.PlaneEstimated; if ((trackableTypeMask & estimatedOrWithinBounds) != TrackableType.None) { var differenceFromCenter = hitPositionPlaneSpace - plane.centerInPlaneSpace; if (Mathf.Abs(differenceFromCenter.x) <= plane.extents.x && Mathf.Abs(differenceFromCenter.y) <= plane.extents.y) { trackableTypes |= (estimatedOrWithinBounds & trackableTypeMask); } } if ((trackableTypeMask & TrackableType.PlaneWithinPolygon) != TrackableType.None) { if (WindingNumber(hitPositionPlaneSpace, plane.boundary) != 0) trackableTypes |= TrackableType.PlaneWithinPolygon; } if (trackableTypes != TrackableType.None) { hitBuffer[count++] = new XRRaycastHit( plane.trackableId, pose, distance, trackableTypes); } } // Finally, copy to return value var hitResults = new NativeArray(count, allocator); NativeArray.Copy(hitBuffer, hitResults, count); return hitResults; } finally { hitBuffer.Dispose(); } } static float GetCrossDirection(Vector2 a, Vector2 b) { return a.x * b.y - a.y * b.x; } // See http://geomalgorithms.com/a03-_inclusion.html static int WindingNumber( Vector2 positionInPlaneSpace, NativeArray boundaryInPlaneSpace) { int windingNumber = 0; Vector2 point = positionInPlaneSpace; for (int i = 0; i < boundaryInPlaneSpace.Length; ++i) { int j = (i + 1) % boundaryInPlaneSpace.Length; Vector2 vi = boundaryInPlaneSpace[i]; Vector2 vj = boundaryInPlaneSpace[j]; if (vi.y <= point.y) { if (vj.y > point.y) // an upward crossing { if (GetCrossDirection(vj - vi, point - vi) < 0f) // P left of edge ++windingNumber; } // have a valid up intersect } else { // y > P.y (no test needed) if (vj.y <= point.y) // a downward crossing { if (GetCrossDirection(vj - vi, point - vi) > 0f) // P right of edge --windingNumber; } // have a valid down intersect } } return windingNumber; } /// /// Get the prefab to instantiate for each detected plane. Can be . /// /// The Prefab which will be instantiated for each . protected override GameObject GetPrefab() => m_PlanePrefab; /// /// Invoked just before `Start`ing the plane subsystem. Used to set the subsystem's /// `requestedPlaneDetectionMode`. /// protected override void OnBeforeStart() { subsystem.requestedPlaneDetectionMode = m_DetectionMode; } /// /// Invoked just after each is updated. /// /// The being updated. /// The new data associated with the plane. All spatial /// data is is session-relative space. protected override void OnAfterSetSessionRelativeData( ARPlane plane, BoundedPlane sessionRelativeData) { plane.subsumedBy = m_Trackables.GetValueOrDefault(sessionRelativeData.subsumedById); plane.UpdateBoundary(subsystem); } /// /// Invoked when the base class detects trackable changes. /// /// The list of added s. /// The list of updated s. /// The list of removed s. [Obsolete("OnTrackablesChanged() has been deprecated in AR Foundation version 6.0.", false)] protected override void OnTrackablesChanged( List added, List updated, List removed) { if (trackablesChanged != null) { using (new ScopedProfiler("OnPlanesChanged")) planesChanged?.Invoke(new ARPlanesChangedEventArgs(added, updated, removed)); } } /// /// The name to be used for the instantiated GameObject whenever a new plane is detected. /// protected override string gameObjectName => "ARPlane"; /// /// Invoked when Unity enables this `MonoBehaviour`. Used to register with the . /// protected override void OnEnable() { base.OnEnable(); if (subsystem != null) { var raycastManager = GetComponent(); if (raycastManager != null) raycastManager.RegisterRaycaster(this); } } /// /// Invoked when this component is disabled. Used to unregister with the . /// protected override void OnDisable() { base.OnDisable(); var raycastManager = GetComponent(); if (raycastManager != null) raycastManager.UnregisterRaycaster(this); } #if UNITY_EDITOR void OnValidate() { requestedDetectionMode = m_DetectionMode; } #endif // UNITY_EDITOR } }