using Sandbox; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Numerics; using System.Runtime.InteropServices; using System.Text.Json.Serialization; using Sandbox.Interpolation; /// /// Represents a Quaternion rotation. Can be interpreted as a direction unit vector (x,y,z) + rotation around the direction vector (w) which represents the up direction. /// Unlike , this cannot store multiple revolutions around an axis. /// [JsonConverter( typeof( Sandbox.Internal.JsonConvert.RotationConverter ) )] [StructLayout( LayoutKind.Sequential )] public struct Rotation : System.IEquatable, IParsable, IInterpolator { internal System.Numerics.Quaternion _quat; /// /// The X component of this rotation. /// public float x { readonly get => _quat.X; set => _quat.X = value; } /// /// The Y component of this rotation. /// public float y { readonly get => _quat.Y; set => _quat.Y = value; } /// /// The Z component of this rotation. /// public float z { readonly get => _quat.Z; set => _quat.Z = value; } /// /// The W component of this rotation (rotation around the normal defined by X,Y,Z components). /// public float w { readonly get => _quat.W; set => _quat.W = value; } /// /// Initializes this rotation to identity. /// public Rotation() { _quat = Quaternion.Identity; } /// /// Initializes the rotation from given components. /// /// The X component. /// The Y component. /// The Z component. /// The W component. public Rotation( float x, float y, float z, float w ) { _quat = new Quaternion( x, y, z, w ); } /// /// Initializes the rotation from a normal vector + rotation around it. /// /// The normal vector. /// The W component, aka rotation around the normal vector. public Rotation( Vector3 v, float w ) { _quat = new Quaternion( v.x, v.y, v.z, w ); } /// /// The forwards direction of this rotation. /// [ActionGraphInclude( AutoExpand = true )] public readonly Vector3 Forward => Vector3.Forward * this; /// /// The backwards direction of this rotation. /// public readonly Vector3 Backward => Vector3.Backward * this; /// /// The right hand direction of this rotation. /// [ActionGraphInclude( AutoExpand = true )] public readonly Vector3 Right => Vector3.Right * this; /// /// The left hand direction of this rotation. /// public readonly Vector3 Left => Vector3.Left * this; /// /// The upwards direction of this rotation. /// [ActionGraphInclude( AutoExpand = true )] public readonly Vector3 Up => Vector3.Up * this; /// /// The downwards direction of this rotation. /// public readonly Vector3 Down => Vector3.Down * this; /// /// Returns the inverse of this rotation. /// public readonly Rotation Inverse => System.Numerics.Quaternion.Inverse( _quat ); /// /// Divides each component of the rotation by its length, normalizing the rotation. /// public readonly Rotation Normal => System.Numerics.Quaternion.Normalize( _quat ); /// /// Returns conjugate of this rotation, meaning the X Y and Z components are negated. /// public readonly Rotation Conjugate => System.Numerics.Quaternion.Conjugate( _quat ); /// /// Returns a uniformly random rotation. /// [ActionGraphNode( "rotation.random" ), Title( "Random Rotation" ), Group( "Math/Geometry/Rotation" ), Icon( "casino" )] public static Rotation Random => SandboxSystem.Random.Rotation(); /// /// Create from angle and an axis /// /// vector must be normalized before calling this method or the resulting will be incorrect. [ActionGraphNode( "rotation.fromaxis" ), Title( "Rotation From Axis" ), Pure, Group( "Math/Geometry/Rotation" ), Icon( "360" )] public static Rotation FromAxis( Vector3 axis, float degrees ) { return Quaternion.CreateFromAxisAngle( axis, degrees.DegreeToRadian() ); } /// /// Create a Rotation (quaternion) from Angles /// public static Rotation From( Angles angles ) { return From( angles.pitch, angles.yaw, angles.roll ); } /// /// Create a Rotation (quaternion) from pitch yaw roll (degrees) /// [ActionGraphNode( "rotation.from" ), Title( "Rotation From Angles" ), Pure, Group( "Math/Geometry/Rotation" ), Icon( "360" )] public static Rotation From( float pitch, float yaw, float roll ) { Rotation rot = default; pitch = pitch.DegreeToRadian() * 0.5f; yaw = yaw.DegreeToRadian() * 0.5f; roll = roll.DegreeToRadian() * 0.5f; float sp = MathF.Sin( pitch ); float cp = MathF.Cos( pitch ); float sy = MathF.Sin( yaw ); float cy = MathF.Cos( yaw ); float sr = MathF.Sin( roll ); float cr = MathF.Cos( roll ); // NJS: for some reason VC6 wasn't recognizing the common subexpressions: float srXcp = sr * cp, crXsp = cr * sp; rot.x = srXcp * cy - crXsp * sy; // X rot.y = crXsp * cy + srXcp * sy; // Y float crXcp = cr * cp, srXsp = sr * sp; rot.z = crXcp * sy - srXsp * cy; // Z rot.w = crXcp * cy + srXsp * sy; // W (real component) return rot; } /// /// Create a Rotation (quaternion) from pitch (degrees) /// public static Rotation FromPitch( float pitch ) { return From( pitch, 0, 0 ); } /// /// Create a Rotation (quaternion) from yaw (degrees) /// public static Rotation FromYaw( float yaw ) { return From( 0, yaw, 0 ); } /// /// Create a Rotation (quaternion) from roll (degrees) /// public static Rotation FromRoll( float roll ) { return From( 0, 0, roll ); } /// /// Create a Rotation (quaternion) from a forward and up vector /// [ActionGraphNode( "rotation.lookat" ), Pure, Group( "Math/Geometry/Rotation" ), Icon( "visibility" )] public static Rotation LookAt( Vector3 forward, Vector3 up ) { forward = forward.Normal; up = up.Normal; float flRatio = forward.Dot( up ); up = (up - (forward * flRatio)).Normal; var right = forward.Cross( up ).Normal; var vX = forward; var vY = -right; var vZ = up; float flTrace = vX.x + vY.y + vZ.z; Quaternion q; if ( flTrace >= 0.0f ) { q.X = vY.z - vZ.y; q.Y = vZ.x - vX.z; q.Z = vX.y - vY.x; q.W = flTrace + 1.0f; } else { if ( vX.x > vY.y && vX.x > vZ.z ) { q.X = vX.x - vY.y - vZ.z + 1.0f; q.Y = vY.x + vX.y; q.Z = vZ.x + vX.z; q.W = vY.z - vZ.y; } else if ( vY.y > vZ.z ) { q.X = vX.y + vY.x; q.Y = vY.y - vZ.z - vX.x + 1.0f; q.Z = vZ.y + vY.z; q.W = vZ.x - vX.z; } else { q.X = vX.z + vZ.x; q.Y = vY.z + vZ.y; q.Z = vZ.z - vX.x - vY.y + 1.0f; q.W = vX.y - vY.x; } } return Quaternion.Normalize( q ); } /// /// Create a Rotation (quaternion) from a forward vector, using as /// an up vector. This won't give nice results if is very close to straight /// up or down, if that can happen you should use . /// [ActionGraphNode( "rotation.lookat" ), Pure, Group( "Math/Geometry/Rotation" ), Icon( "visibility" )] public static Rotation LookAt( Vector3 forward ) { if ( forward.WithZ( 0f ).IsNearZeroLength ) return LookAt( forward, Vector3.Left ); return LookAt( forward, Vector3.Up ); } /// /// A rotation that represents no rotation. /// public static readonly Rotation Identity = new() { _quat = System.Numerics.Quaternion.Identity }; /// /// Returns the difference between two rotations, as a rotation /// [ActionGraphNode( "rotation.diff" ), Pure, Group( "Math/Geometry/Rotation" ), Icon( "360" )] public static Rotation Difference( Rotation from, Rotation to ) { var fromInv = Quaternion.Conjugate( from._quat ); var diff = Quaternion.Multiply( to._quat, fromInv ); return Quaternion.Normalize( diff ); } /// /// The degree angular distance between this rotation and the target /// public readonly float Distance( Rotation to ) { var diff = Difference( this, to ); return diff.Angle(); } /// /// Returns the turn length of this rotation (from identity) in degrees /// public readonly float Angle() { float d = MathF.Acos( w.Clamp( -1.0f, 1.0f ) ).RadianToDegree() * 2.0f; if ( d > 180 ) d -= 360; return MathF.Abs( d ); } /// /// Return this Rotation as pitch, yaw, roll angles /// public readonly Angles Angles() { // Adapted from https://www.euclideanspace.com/maths/geometry/rotations/conversions/quaternionToEuler/ var m13 = (2.0f * x * z) - (2.0f * w * y); Angles a; a.pitch = MathF.Asin( Math.Clamp( -m13, -1f, 1f ) ).RadianToDegree(); if ( Math.Abs( m13 ).AlmostEqual( 1f ) ) { // North / south pole singularities var m21 = 2f * (w * z - x * y); var m31 = 2f * (x * z + w * y); var sign = -Math.Sign( m13 ); a.pitch = (MathF.PI / 2 * sign).RadianToDegree(); a.yaw = sign * MathF.Atan2( m21 * sign, m31 * sign ).RadianToDegree(); a.roll = 0f; } else { // Normal case var m11 = 2f * (w * w + x * x) - 1f; var m12 = 2f * (x * y + w * z); var m23 = 2f * (y * z + w * x); var m33 = 2f * (w * w + z * z) - 1f; a.yaw = MathF.Atan2( m12, m11 ).RadianToDegree(); a.roll = MathF.Atan2( m23, m33 ).RadianToDegree(); } return a; } /// /// Return this Rotation pitch /// public readonly float Pitch() { float m13 = (2.0f * x * z) - (2.0f * w * y); return MathF.Asin( Math.Clamp( -m13, -1f, 1f ) ).RadianToDegree(); } /// /// Return this Rotation yaw /// public readonly float Yaw() { float m11 = (2.0f * w * w) + (2.0f * x * x) - 1.0f; float m12 = (2.0f * x * y) + (2.0f * w * z); return MathF.Atan2( m12, m11 ).RadianToDegree(); } /// /// Return this Rotation roll /// public readonly float Roll() { float m23 = (2.0f * y * z) + (2.0f * w * x); float m33 = (2.0f * w * w) + (2.0f * z * z) - 1.0f; return MathF.Atan2( m23, m33 ).RadianToDegree(); } /// /// Perform a linear interpolation from a to b by given amount. /// [ActionGraphNode( "geom.lerp" ), Pure, Group( "Math/Geometry" ), Icon( "timeline" )] public static Rotation Lerp( Rotation a, Rotation b, [Range( 0f, 1f )] float frac, bool clamp = true ) { if ( clamp ) frac = frac.Clamp( 0, 1 ); return Quaternion.Lerp( a._quat, b._quat, frac ); } /// /// Perform a spherical interpolation from a to b by given amount. /// [ActionGraphNode( "geom.slerp" ), Pure, Group( "Math/Geometry/Rotation" ), Icon( "360" )] public static Rotation Slerp( Rotation a, Rotation b, float amount, bool clamp = true ) { if ( clamp ) amount = amount.Clamp( 0, 1 ); return Quaternion.Slerp( a._quat, b._quat, amount ); } /// /// Perform a linear interpolation from this rotation to a target rotation by given amount. /// public readonly Rotation LerpTo( Rotation target, float frac, bool clamp = true ) => Lerp( this, target, frac, clamp ); /// /// Perform a spherical interpolation from this rotation to a target rotation by given amount. /// public readonly Rotation SlerpTo( Rotation target, float frac, bool clamp = true ) => Slerp( this, target, frac, clamp ); /// /// Clamp to within degrees of passed rotation /// public readonly Rotation Clamp( Rotation to, float degrees ) => Clamp( to, degrees, out var _ ); /// /// Clamp to within degrees of passed rotation. Also pases out the change in degrees, if any. /// public readonly Rotation Clamp( Rotation to, float degrees, out float change ) { change = 0; // what are you doing if ( degrees <= 0 ) return to; // Get difference var diff = Difference( this, to ); // Get degrees var d = diff.Angle(); // Within range, that's fine if ( d <= degrees ) return this; change = d - degrees; var amount = degrees / d; return Slerp( this, to, 1 - amount ); } /// /// A convenience function that rotates this rotation around a given axis given amount of degrees /// /// vector must be normalized before calling this method or the resulting will be incorrect. public readonly Rotation RotateAroundAxis( Vector3 axis, float degrees ) { return this * Rotation.FromAxis( axis, degrees ); } internal static Rotation Exp( Vector3 V ) { // Exponential map (Grassia) const float kThreshold = 0.018581361f; float Angle = V.Length; if ( Angle < kThreshold ) { // Taylor expansion return new Rotation( (0.5f + Angle * Angle / 48.0f) * V, MathF.Cos( 0.5f * Angle ) ); } else { return Quaternion.CreateFromAxisAngle( V / Angle, Angle ); } } /// /// Smoothly move towards the target rotation /// [ActionGraphNode( "rotation.smoothdamp" ), Pure, Group( "Math/Geometry/Rotation" )] public static Rotation SmoothDamp( Rotation current, in Rotation target, ref Vector3 velocity, float smoothTime, float deltaTime ) { // If smoothing time is zero, directly jump to target (independent of timestep) if ( smoothTime <= 0.0f ) { return target; } // If timestep is zero, stay at current position if ( deltaTime <= 0.0f ) { return current; } // Implicit integration of critically damped spring if ( Quaternion.Dot( current._quat, target._quat ) < 0.0f ) { current = new Rotation( -current.x, -current.y, -current.z, -current.w ); } var delta = Quaternion.Multiply( target._quat - current._quat, 2.0f ) * Quaternion.Conjugate( current._quat ); var omega = MathF.PI * 2.0f / smoothTime; var v = new Vector3( delta.X, delta.Y, delta.Z ); velocity = (velocity + (omega * omega) * deltaTime * v) / ((1.0f + omega * deltaTime) * (1.0f + omega * deltaTime)); return (Exp( velocity * deltaTime ) * current).Normal; } /// /// Will give you the axis most aligned with the given normal /// public readonly Vector3 ClosestAxis( Vector3 normal ) { normal = normal.Normal; var axis = new Vector3[6]; axis[0] = Forward; axis[1] = Left; axis[2] = Up; axis[3] = -axis[0]; axis[4] = -axis[1]; axis[5] = -axis[2]; var bestAxis = Vector3.Zero; var bestDot = -1.0f; for ( var i = 0; i < 6; i++ ) { var dot = normal.Dot( axis[i] ); if ( dot > bestDot ) { bestDot = dot; bestAxis = axis[i]; } } return bestAxis; } /// /// Returns a Rotation that rotates from one direction to another. /// public static Rotation FromToRotation( in Vector3 fromDirection, in Vector3 toDirection ) { Vector3 axis = Vector3.Cross( fromDirection, toDirection ); float angle = Vector3.GetAngle( fromDirection, toDirection ); return FromAxis( axis.Normal, angle ); } /// /// Given a string, try to convert this into a quaternion rotation. The format is "x,y,z,w" /// public static Rotation Parse( string str ) { if ( TryParse( str, null, out var res ) ) return res; return default; } /// public static Rotation Parse( string str, IFormatProvider provider ) { return Parse( str ); } /// public static bool TryParse( string str, out Rotation result ) { return TryParse( str, CultureInfo.InvariantCulture, out result ); } /// public static bool TryParse( [NotNullWhen( true )] string str, IFormatProvider provider, [MaybeNullWhen( false )] out Rotation result ) { result = Identity; if ( string.IsNullOrWhiteSpace( str ) ) return false; str = str.Trim( '[', ']', ' ', '\n', '\r', '\t', '"' ); var components = str.Split( new[] { ' ', ',', ';', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries ); if ( components.Length == 4 ) { // Try to parse as a Rotation (x, y, z, w) if ( float.TryParse( components[0], NumberStyles.Float, provider, out float x ) && float.TryParse( components[1], NumberStyles.Float, provider, out float y ) && float.TryParse( components[2], NumberStyles.Float, provider, out float z ) && float.TryParse( components[3], NumberStyles.Float, provider, out float w ) ) { result = new Rotation( x, y, z, w ); return true; } } else if ( components.Length == 3 ) { // Try to parse as Euler angles (pitch, yaw, roll) if ( float.TryParse( components[0], NumberStyles.Float, provider, out float pitch ) && float.TryParse( components[1], NumberStyles.Float, provider, out float yaw ) && float.TryParse( components[2], NumberStyles.Float, provider, out float roll ) ) { var angles = new Angles( pitch, yaw, roll ); result = angles.ToRotation(); return true; } } return false; } public override readonly string ToString() { return $"{x:0.#####},{y:0.#####},{z:0.#####},{w:0.#####}"; } #region operators public static implicit operator Rotation( in System.Numerics.Quaternion value ) { return new Rotation { _quat = value }; } public static implicit operator Rotation( in Angles value ) => From( value ); public static implicit operator Angles( in Rotation value ) => value.Angles(); public static implicit operator System.Numerics.Quaternion( in Rotation value ) { return new System.Numerics.Quaternion( value.x, value.y, value.z, value.w ); } public static Vector3 operator *( in Rotation f, in Vector3 c1 ) { return System.Numerics.Vector3.Transform( c1._vec, f._quat ); } public static Rotation operator *( Rotation a, Rotation b ) { return Quaternion.Multiply( a._quat, b._quat ); } public static Rotation operator *( Rotation a, float f ) { return Quaternion.Slerp( Quaternion.Identity, a._quat, f ); } public static Rotation operator /( Rotation a, float f ) { return Quaternion.Slerp( Quaternion.Identity, a._quat, 1 / f ); } [Obsolete( "Use the * operator if you want to combine rotations. If you really want to add (+) them use quaternions." )] public static Rotation operator +( Rotation a, Rotation b ) { return Quaternion.Add( a._quat, b._quat ); } [Obsolete( "Use Rotation.Difference if you want to get the delta between rotations. If you really want to subtract (-) them use quaternions." )] public static Rotation operator -( Rotation a, Rotation b ) { return Quaternion.Subtract( a._quat, b._quat ); } #endregion #region equality public static bool operator ==( Rotation left, Rotation right ) => left.Equals( right ); public static bool operator !=( Rotation left, Rotation right ) => !(left == right); public readonly override bool Equals( object obj ) => obj is Rotation o && Equals( o ); public readonly bool Equals( Rotation o ) => _quat.X.AlmostEqual( o._quat.X, 0.000001f ) && _quat.Y.AlmostEqual( o._quat.Y, 0.000001f ) && _quat.Z.AlmostEqual( o._quat.Z, 0.000001f ) && _quat.W.AlmostEqual( o._quat.W, 0.000001f ); public readonly override int GetHashCode() => HashCode.Combine( _quat ); /// /// Returns true if we're nearly equal to the passed rotation. /// /// The value to compare with /// The max difference between component values /// True if nearly equal public readonly bool AlmostEqual( in Rotation r, float delta = 0.0001f ) { return Quaternion.Dot( _quat, r._quat ) > 1.0f - delta; } #endregion Rotation IInterpolator.Interpolate( Rotation a, Rotation b, float delta ) { return a.LerpTo( b, delta ); } }