// Copyright (c) Craftwork Games. All rights reserved. // Licensed under the MIT license. // See LICENSE file in the project root for full license information. using System; using System.Diagnostics; using System.Runtime.Serialization; using Microsoft.Xna.Framework; namespace MonoGame.Extended { /// /// Represents a semi-infinite ray in 2D space starting at an origin point and extending in a direction. /// [DataContract] [DebuggerDisplay("{DebugDisplayString,nq}")] public struct Ray2D : IEquatable { #region Public Fields /// /// The direction vector defining which way this ray extends from its origin. /// Should be normalized for accurate distance calculations. /// [DataMember] public Vector2 Direction; /// /// The starting point of this ray in 2D space. /// [DataMember] public Vector2 Origin; #endregion #region Internal Properties internal readonly string DebugDisplayString { get { return string.Concat( "Origin( ", Origin.ToString(), " ) \r\n", "Direction( ", Direction.ToString(), " )" ); } } #endregion #region Public Constructors /// /// Creates a new with the specified origin and direction. /// /// The starting point of the ray in 2D space. /// /// The direction vector defining which way the ray extends. Should be normalized for accurate distance calculations. /// public Ray2D(Vector2 origin, Vector2 direction) { Origin = origin; Direction = direction; } #endregion #region Public Methods #endregion /// /// Creates a from a starting point toward a target point. /// /// The starting point of the ray in 2D space. /// A target point the ray should pass through. /// /// A new starting at the specified point with a unit direction vector /// pointing toward the target point. /// /// /// Thrown when the points are too close to define a valid direction. /// public static Ray2D CreateFromPoints(Vector2 start, Vector2 through) { Vector2 direction = through - start; float lengthSq = direction.LengthSquared(); if (lengthSq < Collision2D.Epsilon * Collision2D.Epsilon) { throw new ArgumentException("Points must be distinct to define a ray direction"); } return new Ray2D(start, Vector2.Normalize(direction)); } /// /// Computes a point along this ray at the specified parametric distance. /// /// The parametric distance along the ray from the origin. /// /// The point at position Origin + distanceAlongRay * Direction. /// /// /// Negative values of return points behind the ray's origin. /// public readonly Vector2 GetPoint(float distanceAlongRay) { return Origin + distanceAlongRay * Direction; } /// /// Computes the closest point on this ray to a specified point. /// /// The point in 2D space to find the closest point to. /// /// When this method returns, contains the parametric distance along the ray to the closest point. /// Will be clamped to zero or greater, as rays only extend forward from the origin. /// /// /// The closest point on the ray. If the point projects behind the ray's origin, returns the origin itself. /// public readonly Vector2 ClosestPoint(Vector2 point, out float distanceAlongRay) { Vector2 ab = Direction; float denom = Vector2.Dot(ab, ab); if (denom <= Collision2D.Epsilon) { // Direction of ray is effectively zero, so it's just a point. distanceAlongRay = 0.0f; return Origin; } // Project point ab, but deferring divide by Dot(ab, ab) distanceAlongRay = Vector2.Dot(point - Origin, ab); if (distanceAlongRay <= 0.0f) { // point projects before the ray origin, clamp to origin distanceAlongRay = 0.0f; return Origin; } // Point projects after the ray origin, must do deferred divide distanceAlongRay /= denom; return Origin + distanceAlongRay * ab; } /// /// Computes the squared distance from a point to the closest point on this ray. /// /// The point in 2D space to measure from. /// /// The squared distance. Returns the distance to the origin if the point /// projects behind the ray's starting point, or the perpendicular distance if the point /// projects onto the ray. /// /// /// This method returns squared distance to avoid the expensive square root operation, /// making it efficient for distance comparisons. /// public readonly float DistanceSquaredToPoint(Vector2 point) { Vector2 closestPoint = ClosestPoint(point, out _); return Vector2.DistanceSquared(point, closestPoint); } /// /// Computes the distance from a point to the closest point on this ray. /// /// The point in 2D space to measure from. /// /// The distance. Returns the distance to the origin if the point /// projects behind the ray's starting point, or the perpendicular distance if the point /// projects onto the ray. /// /// /// This method performs a square root operation. For distance comparisons, /// use instead to avoid the computational cost. /// public readonly float DistanceToPoint(Vector2 point) { float distSq = DistanceSquaredToPoint(point); return MathF.Sqrt(distSq); } /// /// Returns a normalized representation of the specified ray with a unit direction vector. /// /// The ray to normalize. /// /// When this method returns, contains a with the same origin but a unit direction vector. /// public static void Normalize(ref Ray2D value, out Ray2D result) { result = new Ray2D(value.Origin, Vector2.Normalize(value.Direction)); } /// /// Returns a normalized representation of the specified ray with a unit direction vector. /// /// The ray to normalize. /// /// A new with the same origin but a unit direction vector. /// public static Ray2D Normalize(Ray2D value) { Ray2D result; Normalize(ref value, out result); return result; } /// /// Normalizes this ray's direction vector to unit length. /// /// /// After normalization, the will be a unit vector while /// remains unchanged. /// public void Normalize() { Direction.Normalize(); } /// /// Tests if this ray intersects with a line. /// /// The line to test against. /// /// When this method returns , contains the parametric distance along this ray /// to the intersection point. /// When this method returns , contains . /// /// /// When this method returns , contains the point where the ray and line intersect. /// When this method returns , contains . /// /// /// if the ray and line intersect in the ray's forward direction; /// otherwise, if they are parallel or the intersection point is behind the ray's origin. /// public readonly bool Intersects(Line2D line, out float? distanceAlongRay, out Vector2? point) { return line.Intersects(this, out distanceAlongRay, out point); } /// /// Tests if this ray intersects with a line. /// /// The line to test against. /// /// if the ray and line intersect in the ray's forward direction; /// otherwise, if they are parallel or the intersection point is behind the ray's origin. /// public readonly bool Intersects(Line2D line) { return line.Intersects(this); } /// /// Tests if this ray intersects with another ray. /// /// The other ray to test against. /// /// When this method returns , contains the parametric distance along this ray /// to the intersection point. /// When this method returns , contains . /// /// /// When this method returns , contains the parametric distance along the other ray /// to the intersection point. /// When this method returns , contains . /// /// /// When this method returns , contains the point where the rays intersect. /// When this method returns , contains . /// /// /// if both rays intersect in their forward directions; /// otherwise, if they are parallel or if the intersection point is behind either ray's origin. /// public readonly bool Intersects(Ray2D other, out float? distanceAlongRay1, out float? distanceAlongRay2, out Vector2? point) { // C. Ericson, Real-Time Collision Detection, Morgan Kaufmann, 2005 // Parametric intersection of two rays using 2D cross products // Derived from Section 5.1.9.1 "2D Segment Intersection" (line–line formulation) if (!Collision2D.SolveParametricIntersection2D(Origin, Direction, other.Origin, other.Direction, out float t1, out float t2)) { distanceAlongRay1 = distanceAlongRay2 = null; point = null; return false; } // Only intersections in forward direction. Negative direction indicates // intersection would have happened behind origin. if (t1 < 0.0f || t2 < 0.0f) { distanceAlongRay1 = distanceAlongRay2 = null; point = null; return false; } distanceAlongRay1 = t1; distanceAlongRay2 = t2; point = Origin + t1 * Direction; return true; } /// /// Tests if this ray intersects with another ray. /// /// The other ray to test against. /// /// if both rays intersect in their forward directions; /// otherwise, if they are parallel or if the intersection point is behind either ray's origin. /// public readonly bool Intersects(Ray2D other) { return Intersects(other, out _, out _, out _); } /// /// Tests if this ray intersects with a line segment. /// /// The line segment to test against. /// /// When this method returns , contains the parametric distance along this ray /// to the intersection point. /// When this method returns , contains . /// /// /// When this method returns , contains the parametric distance along the segment /// to the intersection point, in the range [0, 1] where 0 represents the start and 1 represents the end. /// When this method returns , contains . /// /// /// When this method returns , contains the point where the ray and segment intersect. /// When this method returns , contains . /// /// /// if the ray intersects the segment in its forward direction and within the segment's bounds; /// otherwise, if they are parallel, the intersection is outside the segment, or behind the ray's origin. /// public readonly bool Intersects(LineSegment2D segment, out float? distanceAlongRay, out float? distanceAlongSegment, out Vector2? point) { // C. Ericson, Real-Time Collision Detection, Morgan Kaufmann, 2005 // Parametric intersection of a ray with a line segment using 2D cross products // Derived from Section 5.1.9.1 "2D Segment Intersection" (line–line formulation) Vector2 segmentDir = segment.End - segment.Start; if (!Collision2D.SolveParametricIntersection2D(Origin, Direction, segment.Start, segmentDir, out float tRay, out float tSeg)) { distanceAlongRay = distanceAlongSegment = null; point = null; return false; } // Intersection only if within segments bounds [0,1] and // only in forward direction of ray. Negative direction of ray indicates // intersection would have happened behind ray origin if (tSeg < 0.0f || tSeg > 1.0f || tRay < 0.0f) { distanceAlongRay = distanceAlongSegment = null; point = null; return false; } distanceAlongRay = tRay; distanceAlongSegment = tSeg; point = Origin + tRay * Direction; return true; } /// Tests if this intersects a . /// The to test against. /// /// if this ray and intersect; otherwise, . /// public readonly bool Intersects(LineSegment2D segment) { return Intersects(segment, out _, out _, out _); } /// /// Tests if this ray intersects with an axis-aligned bounding box and computes the parametric distances to the intersection points. /// /// The bounding box to test against. /// /// When this method returns , contains the parametric distance along this ray /// to the entry intersection point, where the intersection point equals Origin + tRayMin * Direction. /// If the ray origin is inside the bounding box, this will be 0. /// When this method returns , contains . /// /// /// When this method returns , contains the parametric distance along this ray /// to the exit intersection point, where the intersection point equals Origin + tRayMax * Direction. /// This is always greater than or equal to . /// When this method returns , contains . /// /// /// if the ray intersects the bounding box in its forward direction; /// otherwise, . /// public readonly bool Intersects(BoundingBox2D box, out float? tRayMin, out float? tRayMax) { if (!Collision2D.ClipLineToAabb(Origin, Direction, box.Min, box.Max, 0.0f, float.MaxValue, out float tEnter, out float tExit)) { tRayMin = tRayMax = null; return false; } tRayMin = tEnter; tRayMax = tExit; return true; } /// /// Tests if this ray intersects with an axis-aligned bounding box. /// /// The bounding box to test against. /// /// if the ray intersects the bounding box in its forward direction; /// otherwise, . /// public readonly bool Intersects(BoundingBox2D box) { return Intersects(box, out _, out _); } /// /// Tests if this ray intersects with a circle and computes the parametric distances to the intersection points. /// /// The circle to test against. /// /// When this method returns , contains the parametric distance along this ray /// to the first intersection point, where the intersection point equals Origin + tRayMin * Direction. /// If the ray origin is inside the circle, this will be 0. /// When this method returns , contains . /// /// /// When this method returns , contains the parametric distance along this ray /// to the second intersection point, where the intersection point equals Origin + tRayMax * Direction. /// This is always greater than or equal to . /// When this method returns , contains . /// /// /// if the ray intersects the circle in its forward direction; /// otherwise, . /// public readonly bool Intersects(BoundingCircle2D circle, out float? tRayMin, out float? tRayMax) { if (!Collision2D.RayCircleIntersectionInterval(Origin, Direction, circle.Center, circle.Radius, out float tMin, out float tMax)) { tRayMin = tRayMax = null; return false; } if (!Collision2D.ClipInterval(tMin, tMax, 0.0f, float.MaxValue, out float entry, out float exit)) { tRayMin = tRayMax = null; return false; } tRayMin = entry; tRayMax = exit; return true; } /// /// Tests if this ray intersects with a circle. /// /// The circle to test against. /// /// if the ray intersects the circle in its forward direction; /// otherwise, . /// public readonly bool Intersects(BoundingCircle2D circle) { return Intersects(circle, out _, out _); } /// /// Tests if this ray intersects with a capsule and computes the parametric distances to the intersection points. /// /// The capsule to test against. /// /// When this method returns , contains the parametric distance along this ray /// to the entry intersection point, where the intersection point equals Origin + tRayMin * Direction. /// If the ray origin is inside the capsule, this will be 0. /// When this method returns , contains . /// /// /// When this method returns , contains the parametric distance along this ray /// to the exit intersection point, where the intersection point equals Origin + tRayMax * Direction. /// This is always greater than or equal to . /// When this method returns , contains . /// /// /// if the ray intersects the capsule in its forward direction; /// otherwise, . /// public readonly bool Intersects(BoundingCapsule2D capsule, out float? tRayMin, out float? tRayMax) { if (!Collision2D.RayCapsuleIntersectionInterval(Origin, Direction, capsule.PointA, capsule.PointB, capsule.Radius, out float tMin, out float tMax)) { tRayMin = tRayMax = null; return false; } if (!Collision2D.ClipInterval(tMin, tMax, 0.0f, float.MaxValue, out float entry, out float exit)) { tRayMin = tRayMax = null; return false; } tRayMin = entry; tRayMax = exit; return true; } /// /// Tests if this ray intersects with a capsule. /// /// The capsule to test against. /// /// if the ray intersects the capsule in its forward direction; /// otherwise, . /// public readonly bool Intersects(BoundingCapsule2D capsule) { return Intersects(capsule, out _, out _); } /// /// Tests if this ray intersects with an oriented bounding box and computes the parametric distances to the intersection points. /// /// The oriented bounding box to test against. /// /// When this method returns , contains the parametric distance along this ray /// to the entry intersection point, where the intersection point equals Origin + tRayMin * Direction. /// If the ray origin is inside the box, this will be 0. /// When this method returns , contains . /// /// /// When this method returns , contains the parametric distance along this ray /// to the exit intersection point, where the intersection point equals Origin + tRayMax * Direction. /// This is always greater than or equal to . /// When this method returns , contains . /// /// /// if the ray intersects the box in its forward direction; /// otherwise, . /// public readonly bool Intersects(OrientedBoundingBox2D obb, out float? tRayMin, out float? tRayMax) { // C. Ericson, Real-Time Collision Detection, Morgan Kaufmann, 2005 // Parametric intersection of a ray with an oriented bounding rectangle (2D reduction) // Derived from Section 5.3.3 "Intersecting Ray or Segment Against Box" // Project the ray origin and direction onto the OBBs local axes // to transform the ray from world space to OBB local space. Vector2 diff = Origin - obb.Center; Vector2 localOrigin = new Vector2( Vector2.Dot(diff, obb.AxisX), Vector2.Dot(diff, obb.AxisY) ); Vector2 localDirection = new Vector2( Vector2.Dot(Direction, obb.AxisX), Vector2.Dot(Direction, obb.AxisY) ); if (!Collision2D.ClipLineToAabb(localOrigin, localDirection, -obb.HalfExtents, obb.HalfExtents, 0.0f, float.MaxValue, out float tEnter, out float tExit)) { tRayMin = tRayMax = null; return false; } tRayMin = tEnter; tRayMax = tExit; return true; } /// /// Tests if this ray intersects with an oriented bounding box. /// /// The oriented bounding box to test against. /// /// if the ray intersects the box in its forward direction; /// otherwise, . /// public readonly bool Intersects(OrientedBoundingBox2D obb) { return Intersects(obb, out _, out _); } /// /// Tests if this ray intersects with a polygon and computes the parametric distances to the intersection points. /// /// The polygon to test against. /// /// When this method returns , contains the parametric distance along this ray /// to the entry intersection point, where the intersection point equals Origin + tRayMin * Direction. /// If the ray origin is inside the polygon, this will be 0. /// When this method returns , contains . /// /// /// When this method returns , contains the parametric distance along this ray /// to the exit intersection point, where the intersection point equals Origin + tRayMax * Direction. /// This is always greater than or equal to . /// When this method returns , contains . /// /// /// When this method returns , contains the position of the entry intersection point /// corresponding to . /// When this method returns , contains . /// /// /// if the ray intersects the polygon in its forward direction; /// otherwise, . /// public readonly bool Intersects(BoundingPolygon2D polygon, out float? tRayMin, out float? tRayMax, out Vector2? point) { // Clip ray's parametric line against the polygon if (!Collision2D.ClipLineToConvexPolygon(Origin, Direction, polygon.Vertices, polygon.Normals, 0.0f, float.MaxValue, out float t0, out float t1)) { tRayMin = tRayMax = null; point = null; return false; } if (!Collision2D.ClipInterval(t0, t1, 0.0f, float.MaxValue, out float entry, out float exit)) { tRayMin = tRayMax = null; point = null; return false; } tRayMin = entry; tRayMax = exit; point = Origin + Direction * entry; return true; } /// /// Tests if this ray intersects with a polygon. /// /// The polygon to test against. /// /// if the ray intersects the polygon in its forward direction; /// otherwise, . /// public readonly bool Intersects(BoundingPolygon2D polygon) { return Intersects(polygon, out _, out _, out _); } /// /// Deconstructs this ray into its component values. /// /// /// When this method returns, contains the starting point of this ray in 2D space. /// /// /// When this method returns, contains the direction vector defining which way this ray extends. /// public readonly void Deconstruct(out Vector2 origin, out Vector2 direction) { origin = Origin; direction = Direction; } /// public readonly bool Equals(Ray2D other) { return Origin.Equals(other.Origin) && Direction.Equals(other.Direction); } /// public override readonly bool Equals(object obj) { return (obj is Ray2D other) && Equals(other); } /// public override readonly int GetHashCode() { return Origin.GetHashCode() ^ Direction.GetHashCode(); } /// public override readonly string ToString() { return "{{Origin:" + Origin.ToString() + " Direction:" + Direction.ToString() + "}}"; } /// public static bool operator ==(Ray2D left, Ray2D right) { return left.Equals(right); } /// public static bool operator !=(Ray2D left, Ray2D right) { return !left.Equals(right); } } }