using Sandbox; using System.Runtime.InteropServices; using System.Text.Json.Serialization; /// /// A capsule object, defined by 2 points and a radius. A capsule is a cylinder with round ends (inset half spheres on each end). /// [StructLayout( LayoutKind.Sequential )] public struct Capsule : System.IEquatable { /// /// Position of point A. /// [JsonInclude] public Vector3 CenterA; /// /// Position of point B. /// [JsonInclude] public Vector3 CenterB; /// /// Radius of a capsule. /// [JsonInclude] public float Radius; public Capsule( Vector3 a, Vector3 b, float r ) { CenterA = a; CenterB = b; Radius = r; } /// /// Creates a capsule where Point A is radius units above the ground and Point B is height minus radius units above the ground. /// public static Capsule FromHeightAndRadius( float height, float radius ) { return new Capsule( Vector3.Up * radius, Vector3.Up * (height - radius), radius ); } /// /// Returns a random point within this capsule. /// [JsonIgnore, Hide] public readonly Vector3 RandomPointInside { get { var diff = CenterB - CenterA; var sphereRand = Random.Shared.VectorInSphere( Radius ); if ( diff.IsNearZeroLength ) { // This capsule is just a sphere return sphereRand + CenterA; } var direction = diff.Normal; // Randomly decide whether the point will be in the cylindrical surface or on the hemispherical caps // Common factor is PI * Radius * Radius, so volumes are relative to that var capVolume = 2f / 3f * Radius; // Relative volume of one cap var cylinderVolume = diff.Length; // Relative volume of the cylinder var totalVolume = 2f * capVolume + cylinderVolume; var rand = Random.Shared.Float( 0.0f, totalVolume ); if ( rand < 2 * capVolume ) { // Point in either hemispherical cap var end = Vector3.Dot( direction, sphereRand ) > 0f ? CenterB : CenterA; return end + sphereRand; } else { // Point in the connecting cylinder var t = Random.Shared.Float( 0f, 1f ); var pointOnLine = Vector3.Lerp( CenterA, CenterB, t ); var pointInCircle = Random.Shared.VectorInCircle( Radius ); var norm1 = Vector3.Cross( direction, sphereRand ).Normal; var norm2 = Vector3.Cross( direction, norm1 ); return pointOnLine + norm1 * pointInCircle.x + norm2 * pointInCircle.y; } } } /// /// Returns a random point on the edge of this capsule. /// [JsonIgnore, Hide] public readonly Vector3 RandomPointOnEdge { get { var diff = CenterB - CenterA; var sphereRand = Random.Shared.VectorOnSphere( Radius ); if ( diff.IsNearZeroLength ) { // This capsule is just a sphere return sphereRand + CenterA; } var direction = diff.Normal; // Randomly decide whether the point will be on the cylindrical surface or on the hemispherical caps // Common factor is 2 * PI * Radius, so areas are relative to that var capArea = Radius; // Relative area of one cap var cylinderArea = diff.Length; // Relative area of the cylinder var totalArea = 2 * capArea + cylinderArea; var rand = Random.Shared.Float( 0.0f, totalArea ); if ( rand < 2 * capArea ) { // Point on either hemispherical cap var end = Vector3.Dot( direction, sphereRand ) > 0f ? CenterB : CenterA; return end + sphereRand; } else { // Point on the cylindrical surface var t = Random.Shared.Float( 0f, 1f ); var pointOnLine = Vector3.Lerp( CenterA, CenterB, t ); // Random point on the circle around pointOnLine var randomPerpendicular = Vector3.Cross( direction, sphereRand ).Normal * Radius; return pointOnLine + randomPerpendicular; } } } /// /// Gets the volume of the capsule in cubic units. /// [JsonIgnore, Hide] public readonly float Volume { get { // Calculate the length of the cylindrical part float cylinderLength = (CenterB - CenterA).Length; // Volume of a capsule = volume of cylinder + volume of two hemisphere caps // cylinder = π × radius² × height // two hemispheres = (4/3) × π × radius³ float cylinderVolume = MathF.PI * Radius * Radius * cylinderLength; float sphereVolume = (4.0f / 3.0f) * MathF.PI * Radius * Radius * Radius; return cylinderVolume + sphereVolume; } } /// /// Gets the Bounding Box of the capsule. /// [JsonIgnore, Hide] public readonly BBox Bounds { get { // Create a bounding box that encompasses both sphere ends of the capsule Vector3 radiusVector = new Vector3( Radius ); // Initialize bounds with the first center expanded by radius Vector3 mins = Vector3.Min( CenterA - radiusVector, CenterB - radiusVector ); Vector3 maxs = Vector3.Max( CenterA + radiusVector, CenterB + radiusVector ); return new BBox( mins, maxs ); } } /// /// Calculates the distance from a given point to the edge of the capsule. /// /// Position in the same coordinate space as the capsule public readonly float GetEdgeDistance( Vector3 localPos ) { // Find the closest point on the line segment (CenterA to CenterB) to the position Vector3 lineVec = CenterB - CenterA; float lineLength = lineVec.Length; // Handle degenerate case (capsule is actually a sphere) if ( lineLength < 0.00001f ) { float distanceToCenter = (localPos - CenterA).Length; return MathF.Abs( distanceToCenter - Radius ); } // Calculate normalized direction vector of the capsule axis Vector3 lineDir = lineVec / lineLength; // Calculate projection of point onto line float projection = Vector3.Dot( localPos - CenterA, lineDir ); // Clamp projection to line segment projection = Math.Clamp( projection, 0, lineLength ); // Find closest point on line segment Vector3 closestPoint = CenterA + lineDir * projection; // Calculate distance from point to closest point on line segment float distanceToLine = (localPos - closestPoint).Length; // Return distance to edge (distance to line minus radius) return MathF.Abs( distanceToLine - Radius ); } /// /// Determines if the capsule contains the specified point. /// public readonly bool Contains( Vector3 point ) { // A point is inside the capsule if the distance to the edge is zero or negative return GetEdgeDistance( point ) <= 0; } #region equality public static bool operator ==( Capsule left, Capsule right ) => left.Equals( right ); public static bool operator !=( Capsule left, Capsule right ) => !(left == right); public readonly override bool Equals( object obj ) => obj is Capsule o && Equals( o ); public readonly bool Equals( Capsule o ) => (CenterA, CenterB, Radius) == (o.CenterA, o.CenterB, o.Radius); public readonly override int GetHashCode() => HashCode.Combine( CenterA, CenterB, Radius ); #endregion }