using Unity.Collections; using UnityEngine.XR.ARSubsystems; namespace UnityEngine.XR.ARFoundation { /// /// Used for raycasting against 3D bounding boxes. /// internal class BoundingBoxRaycaster : IRaycaster { const int k_MaxNumberOfHitFaces = 2; TrackableCollection m_BoundingBoxTrackables; /// /// Constructs a new instance, initializing the collection of trackables to test raycasts against. /// /// The collection of bounding boxes to test raycasts again. public BoundingBoxRaycaster(TrackableCollection boundingBoxTrackables) { m_BoundingBoxTrackables = boundingBoxTrackables; } /// /// Performs a raycast against all 3D bounding boxes passed in during construction. /// /// 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) { // if the bounding box flag is not set for the mask to search for, return early if ((trackableTypeMask & TrackableType.BoundingBox) != TrackableType.BoundingBox) return new NativeArray(0, allocator); var hitBuffer = new NativeArray(m_BoundingBoxTrackables.count, Allocator.Temp); var boundingBoxHitCount = 0; foreach (var boundingBox in m_BoundingBoxTrackables) { var closestHitDistance = float.MaxValue; var numFacesHit = 0; // forward face var wasForwardFaceHit = TryRaycastZAxisFace( boundingBox, ray, 1, hitBuffer, boundingBoxHitCount, ref closestHitDistance); numFacesHit += wasForwardFaceHit ? 1 : 0; // back face if (numFacesHit < k_MaxNumberOfHitFaces) { var wasBackFaceHit = TryRaycastZAxisFace( boundingBox, ray, -1, hitBuffer, boundingBoxHitCount, ref closestHitDistance); numFacesHit += wasBackFaceHit ? 1 : 0; } // right face if (numFacesHit < k_MaxNumberOfHitFaces) { var wasRightFaceHit = TryRaycastXAxisFace( boundingBox, ray, 1, hitBuffer, boundingBoxHitCount, ref closestHitDistance); numFacesHit += wasRightFaceHit ? 1 : 0; } // left face if (numFacesHit < k_MaxNumberOfHitFaces) { var wasLeftFaceHit = TryRaycastXAxisFace( boundingBox, ray, -1, hitBuffer, boundingBoxHitCount, ref closestHitDistance); numFacesHit += wasLeftFaceHit ? 1 : 0; } // top face if (numFacesHit < k_MaxNumberOfHitFaces) { var wasTopFaceHit = TryRaycastYAxisFace( boundingBox, ray, 1, hitBuffer, boundingBoxHitCount, ref closestHitDistance); numFacesHit += wasTopFaceHit ? 1 : 0; } // bottom face if (numFacesHit < k_MaxNumberOfHitFaces) { var wasBottomFaceHit = TryRaycastYAxisFace( boundingBox, ray, -1, hitBuffer, boundingBoxHitCount, ref closestHitDistance); numFacesHit += wasBottomFaceHit ? 1 : 0; } if (numFacesHit > 0) boundingBoxHitCount += 1; } var hitResults = new NativeArray(boundingBoxHitCount, allocator); NativeArray.Copy(hitBuffer, hitResults, boundingBoxHitCount); hitBuffer.Dispose(); return hitResults; } bool TryRaycastZAxisFace( ARBoundingBox boundingBox, Ray ray, int sign, NativeArray hitBuffer, int hitBufferIndex, ref float closestHitDistance) { var boundingBoxTransform = boundingBox.transform; var faceNormal = sign * boundingBoxTransform.forward; var faceOffset = faceNormal * (boundingBox.size.z * 0.5f); var facePosition = boundingBox.pose.position + faceOffset; var infiniteFacePlane = new Plane(faceNormal, facePosition); // return early if the ray does not intersect the infinite plane // of the face if (!infiniteFacePlane.Raycast(ray, out var distance)) return false; // rotate the hit pose so the up vector aligns with the normal of the plane // and the forward vector faces up var hitPose = new Pose( ray.origin + ray.direction * distance, boundingBox.transform.localRotation * Quaternion.Euler(-90, (1 + sign) * 90, 0)); var localHitPosition = boundingBox.transform.InverseTransformPoint(hitPose.position); if (Mathf.Abs(localHitPosition.x) <= boundingBox.size.x * 0.5f && Mathf.Abs(localHitPosition.y) <= boundingBox.size.y * 0.5f) { // we return early because the distance to this face is further than // the closest distance and we only want to update the data in the hitbuffer // for the closest hits. We return true to notify the caller that the face // was hit. if (distance > closestHitDistance) return true; closestHitDistance = distance; hitBuffer[hitBufferIndex] = new( boundingBox.trackableId, hitPose, distance, TrackableType.BoundingBox); return true; } return false; } bool TryRaycastXAxisFace( ARBoundingBox boundingBox, Ray ray, int sign, NativeArray hitBuffer, int hitBufferIndex, ref float closestHitDistance) { var boundingBoxTransform = boundingBox.transform; var faceNormal = sign * boundingBoxTransform.right; var faceOffset = faceNormal * (boundingBox.size.x * 0.5f); var facePosition = boundingBox.pose.position + faceOffset; var infiniteFacePlane = new Plane(faceNormal, facePosition); // return early if the ray does not intersect the infinite plane // of the face if (!infiniteFacePlane.Raycast(ray, out var distance)) return false; // rotate the hit pose so the up vector aligns with the normal of the plane // and the forward vector faces up var hitPose = new Pose( ray.origin + ray.direction * distance, boundingBox.transform.localRotation * Quaternion.Euler(-90, 0, -sign * 90)); var localHitPosition = boundingBox.transform.InverseTransformPoint(hitPose.position); if (Mathf.Abs(localHitPosition.z) <= boundingBox.size.z * 0.5f && Mathf.Abs(localHitPosition.y) <= boundingBox.size.y * 0.5f) { // we return early because the distance to this face is further than // the closest distance and we only want to update the data in the hitbuffer // for the closest hits. We return true to notify the caller that the face // was hit. if (distance > closestHitDistance) return true; closestHitDistance = distance; hitBuffer[hitBufferIndex] = new( boundingBox.trackableId, hitPose, distance, TrackableType.BoundingBox); return true; } return false; } bool TryRaycastYAxisFace( ARBoundingBox boundingBox, Ray ray, int sign, NativeArray hitBuffer, int hitBufferIndex, ref float closestHitDistance) { var boundingBoxTransform = boundingBox.transform; var faceNormal = sign * boundingBoxTransform.up; var faceOffset = faceNormal * (boundingBox.size.y * 0.5f); var facePosition = boundingBox.pose.position + faceOffset; var infiniteFacePlane = new Plane(faceNormal, facePosition); // return early if the ray does not intersect the infinite plane // of the face if (!infiniteFacePlane.Raycast(ray, out var distance)) return false; // rotate the hit pose so the up vector aligns with the normal of the plane // and the forward vector matches the bounding box forward for the top face or // mirrors it for the bottom face. var hitPose = new Pose( ray.origin + ray.direction * distance, boundingBox.transform.localRotation * Quaternion.Euler(0, 0, (1 - sign) * 90)); var localHitPosition = boundingBox.transform.InverseTransformPoint(hitPose.position); if (Mathf.Abs(localHitPosition.x) <= boundingBox.size.x * 0.5f && Mathf.Abs(localHitPosition.z) <= boundingBox.size.z * 0.5f) { // we return early because the distance to this face is further than // the closest distance and we only want to update the data in the hitbuffer // for the closest hits. We return true to notify the caller that the face // was hit. if (distance > closestHitDistance) return true; closestHitDistance = distance; hitBuffer[hitBufferIndex] = new( boundingBox.trackableId, hitPose, distance, TrackableType.BoundingBox); return true; } return false; } } }