// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; using System.Collections.Generic; using UnityEngine; namespace Microsoft.MixedReality.Toolkit.Physics { public static class ConeCastUtility { public struct ConeCastHit { public ConeCastHit(RaycastHit hit, float distance, float angle) { raycastHit = hit; hitDistance = distance; hitAngle = angle; } public RaycastHit raycastHit; public float hitDistance; public float hitAngle; } static List coneCastHitList = new List(); /// /// Function casts a sphere along a ray and checks if the hitpoint is within the angle of the cone and returns detailed information. /// /// The vertex of the cone and the at the start of the sweep. /// The direction into which to sweep the sphere.. /// The radius of the sweep. /// The max length of the cast. /// The angle used to define the cone. /// A Layer mask that is used to selectively ignore colliders when casting a capsule. /// An array of structs that contain RaycastHit, distance to hit, and the angle of all the objects that were hit. public static ConeCastHit[] ConeCastAll(Vector3 origin, Vector3 direction, float maxRadius, float maxDistance, float coneAngle, LayerMask layerMask) { coneCastHitList.Clear(); RaycastHit[] sphereCastHits = UnityEngine.Physics.SphereCastAll(origin - new Vector3(0, 0, maxRadius), maxRadius, direction, maxDistance, layerMask); if (sphereCastHits.Length == 0) { return Array.Empty(); } for (int i = 0; i < sphereCastHits.Length; i++) { Vector3 hitPoint = sphereCastHits[i].point; Vector3 directionToHit = hitPoint - origin; float angleToHit = Vector3.Angle(direction, directionToHit); if (angleToHit < coneAngle) { coneCastHitList.Add(new ConeCastHit(sphereCastHits[i], directionToHit.magnitude, angleToHit)); } } return coneCastHitList.ToArray(); } private static RaycastHit[] sphereCastHits = null; private static int sphereCastMaxHitCount = 10; private const int sphereCastLimit = 100; /// /// Function casts a sphere along a ray and checks if the hitpoint is within the angle of the cone and returns the best target determined by the weights provided. /// /// The vertex of the cone and the at the start of the sweep. /// The direction into which to sweep the sphere.. /// The radius of the sweep. /// The max length of the cast. /// The angle used to define the cone. /// A Layer mask that is used to selectively ignore colliders when casting a capsule. /// The importance of distance between the hitpoint and the origin in selecting the best target. /// The importance of angle between the hitpoint and the origin in selecting the best target. /// The importance of distance between the hitpoint and the center of the object in selecting the best target. /// The importance of angle between the hitpoint and the center of the object in selecting the best target. /// The RaycastHit of the best object. public static RaycastHit ConeCastBest(Vector3 origin, Vector3 direction, float maxRadius, float maxDistance, float coneAngle, LayerMask layerMask, float distanceWeight, float angleWeight, float distanceToCenterWeight, float angleToCenterWeight) { if (sphereCastHits == null || sphereCastHits.Length < sphereCastMaxHitCount) { sphereCastHits = new RaycastHit[sphereCastMaxHitCount]; } var hitCount = UnityEngine.Physics.SphereCastNonAlloc(origin - (direction * maxRadius), maxRadius, direction, sphereCastHits, maxDistance, layerMask, QueryTriggerInteraction.Ignore); // Algorithm: double the max hit count if there are too many results, up to a certain limit if (hitCount >= sphereCastMaxHitCount && sphereCastMaxHitCount < sphereCastLimit) { // There might be more hits we didn't get, grow the array and try again next time // Note that this frame, the results might be imprecise. sphereCastMaxHitCount = Math.Min(sphereCastLimit, sphereCastMaxHitCount * 2); } RaycastHit hitGameobject = new RaycastHit(); float score = float.MaxValue; for (int i = 0; i < hitCount; i++) { RaycastHit hit = sphereCastHits[i]; Vector3 hitPoint = hit.point; Vector3 directionToHit = hitPoint - origin; float angleToHit = Vector3.Angle(direction, directionToHit); Vector3 hitDistance = hit.collider.transform.position - hitPoint; Vector3 directionToCenter = hit.collider.transform.position - origin; float angleToCenter = Vector3.Angle(direction, directionToCenter); // Additional work to see if there is a better point slightly further ahead on the direction line. This is only allowed if the collider isn't a mesh collider. if (hit.collider.GetType() != typeof(MeshCollider)) { Vector3 pointFurtherAlongGazePath = (maxRadius * 0.5f * direction.normalized) + FindNearestPointOnLine(origin, direction, hitPoint); Vector3 closestPointToPointFurtherAlongGazePath = hit.collider.ClosestPoint(pointFurtherAlongGazePath); Vector3 directionToSecondaryPoint = closestPointToPointFurtherAlongGazePath - origin; float angleToSecondaryPoint = Vector3.Angle(direction, directionToSecondaryPoint); if (angleToSecondaryPoint < angleToHit) { hitPoint = closestPointToPointFurtherAlongGazePath; directionToHit = directionToSecondaryPoint; angleToHit = angleToSecondaryPoint; hitDistance = hit.collider.transform.position - hitPoint; } } if (angleToHit < coneAngle) { float distanceScore = distanceWeight == 0 ? 0.0f : (distanceWeight * directionToHit.magnitude); float angleScore = angleWeight == 0 ? 0.0f : (angleWeight * angleToHit); float centerScore = distanceToCenterWeight == 0 ? 0.0f : (distanceToCenterWeight * hitDistance.magnitude); float centerAngleScore = angleToCenterWeight == 0 ? 0.0f : (angleToCenterWeight * angleToCenter); float newScore = distanceScore + angleScore + centerScore + centerAngleScore; if (newScore < score) { score = newScore; hitGameobject = hit; } } } return hitGameobject; } private static Vector3 FindNearestPointOnLine(Vector3 origin, Vector3 direction, Vector3 point) { direction.Normalize(); Vector3 lhs = point - origin; float dotP = Vector3.Dot(lhs, direction); return origin + direction * dotP; } } }