// 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);
}
}
}