| | | 1 | | //======================================================================= |
| | | 2 | | // FixedQuaternion.Statics.cs |
| | | 3 | | //======================================================================= |
| | | 4 | | // MIT License, Copyright (c) 2024–present David Oravsky (mrdav30) |
| | | 5 | | // See LICENSE file in the project root for full license information. |
| | | 6 | | //======================================================================= |
| | | 7 | | |
| | | 8 | | using System; |
| | | 9 | | using System.Runtime.CompilerServices; |
| | | 10 | | |
| | | 11 | | namespace FixedMathSharp; |
| | | 12 | | |
| | | 13 | | public partial struct FixedQuaternion |
| | | 14 | | { |
| | | 15 | | #region Quaternion Operations |
| | | 16 | | |
| | | 17 | | /// <summary> |
| | | 18 | | /// Checks if this vector has been normalized by checking if the magnitude is close to 1. |
| | | 19 | | /// </summary> |
| | | 20 | | public bool IsNormalized() |
| | | 21 | | { |
| | 3 | 22 | | Fixed64 mag = GetMagnitude(this); |
| | 3 | 23 | | return FixedMath.Abs(mag - Fixed64.One) <= Fixed64.Epsilon; |
| | | 24 | | } |
| | | 25 | | |
| | | 26 | | /// <summary> |
| | | 27 | | /// Calculates the magnitude (or length) of the specified quaternion. |
| | | 28 | | /// </summary> |
| | | 29 | | /// <remarks> |
| | | 30 | | /// If rounding errors cause the computed magnitude to be slightly greater than 1 but within epsilon, the result is |
| | | 31 | | /// This helps maintain numerical stability when working with normalized quaternions.</remarks> |
| | | 32 | | /// <param name="q">The quaternion for which to compute the magnitude.</param> |
| | | 33 | | /// <returns>The magnitude of the quaternion as a Fixed64 value. Returns 0 if the quaternion is the zero quaternion. |
| | | 34 | | public static Fixed64 GetMagnitude(FixedQuaternion q) |
| | | 35 | | { |
| | 107 | 36 | | Fixed64 mag = (q.X * q.X) + (q.Y * q.Y) + (q.Z * q.Z) + (q.W * q.W); |
| | | 37 | | // If rounding error caused the final magnitude to be slightly above 1, clamp it |
| | 107 | 38 | | if (mag > Fixed64.One && mag <= Fixed64.One + Fixed64.Epsilon) |
| | 1 | 39 | | return Fixed64.One; |
| | | 40 | | |
| | 106 | 41 | | return mag != Fixed64.Zero ? FixedMath.Sqrt(mag) : Fixed64.Zero; |
| | | 42 | | } |
| | | 43 | | |
| | | 44 | | /// <summary> |
| | | 45 | | /// Normalizes the quaternion to a unit quaternion. |
| | | 46 | | /// </summary> |
| | | 47 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 48 | | public static FixedQuaternion GetNormalized(FixedQuaternion q) |
| | | 49 | | { |
| | 90 | 50 | | Fixed64 mag = GetMagnitude(q); |
| | | 51 | | |
| | | 52 | | // If magnitude is zero, return identity quaternion (to avoid divide by zero) |
| | 90 | 53 | | if (mag == Fixed64.Zero) |
| | 3 | 54 | | return new FixedQuaternion(Fixed64.Zero, Fixed64.Zero, Fixed64.Zero, Fixed64.One); |
| | | 55 | | |
| | | 56 | | // If already normalized, return as-is |
| | 87 | 57 | | if (FixedMath.Abs(mag - Fixed64.One) <= Fixed64.Epsilon) |
| | 42 | 58 | | return q; |
| | | 59 | | |
| | | 60 | | // Normalize it exactly |
| | 45 | 61 | | return new FixedQuaternion( |
| | 45 | 62 | | q.X / mag, |
| | 45 | 63 | | q.Y / mag, |
| | 45 | 64 | | q.Z / mag, |
| | 45 | 65 | | q.W / mag |
| | 45 | 66 | | ); |
| | | 67 | | } |
| | | 68 | | |
| | | 69 | | /// <summary> |
| | | 70 | | /// Divides one quaternion by another using inverse quaternion multiplication. |
| | | 71 | | /// </summary> |
| | | 72 | | /// <remarks> |
| | | 73 | | /// This is equivalent to <c>dividend * Inverse(divisor)</c>. |
| | | 74 | | /// </remarks> |
| | | 75 | | /// <exception cref="InvalidOperationException"> |
| | | 76 | | /// Thrown when <paramref name="divisor"/> is not invertible. |
| | | 77 | | /// </exception> |
| | | 78 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 79 | | public static FixedQuaternion Divide(FixedQuaternion dividend, FixedQuaternion divisor) |
| | | 80 | | { |
| | 2 | 81 | | Fixed64 divisorMagnitudeSquared = divisor.MagnitudeSquared; |
| | | 82 | | |
| | 2 | 83 | | if (divisorMagnitudeSquared == Fixed64.Zero) |
| | 1 | 84 | | throw new InvalidOperationException("Quaternion divisor is not invertible."); |
| | | 85 | | |
| | 1 | 86 | | Fixed64 invNorm = Fixed64.One / divisorMagnitudeSquared; |
| | 1 | 87 | | FixedQuaternion inverseDivisor = new( |
| | 1 | 88 | | -divisor.X * invNorm, |
| | 1 | 89 | | -divisor.Y * invNorm, |
| | 1 | 90 | | -divisor.Z * invNorm, |
| | 1 | 91 | | divisor.W * invNorm); |
| | | 92 | | |
| | 1 | 93 | | return dividend * inverseDivisor; |
| | | 94 | | } |
| | | 95 | | |
| | | 96 | | /// <summary> |
| | | 97 | | /// Creates a quaternion whose canonical forward direction aligns with the specified direction. |
| | | 98 | | /// </summary> |
| | | 99 | | /// <remarks> |
| | | 100 | | /// The <paramref name="forward"/> and <paramref name="upwards"/> vectors are expressed in |
| | | 101 | | /// FixedMathSharp's canonical basis: <c>+X</c> right, <c>+Y</c> up, and <c>+Z</c> forward. |
| | | 102 | | /// Use <see cref="CoordinateConvention3d"/> or adapter-specific basis conversion before |
| | | 103 | | /// calling this method when external APIs use different semantic axes. |
| | | 104 | | /// </remarks> |
| | | 105 | | /// <param name="forward">The forward direction vector.</param> |
| | | 106 | | /// <param name="upwards">The upwards direction vector (optional, default: Vector3d.Up).</param> |
| | | 107 | | /// <returns>A quaternion representing the rotation from one direction to another.</returns> |
| | | 108 | | public static FixedQuaternion LookRotation(Vector3d forward, Vector3d? upwards = null) |
| | | 109 | | { |
| | 3 | 110 | | Vector3d up = upwards ?? Vector3d.Up; |
| | | 111 | | |
| | 3 | 112 | | Vector3d forwardNormalized = forward.Normalized; |
| | 3 | 113 | | Vector3d right = Vector3d.Cross(up.Normalized, forwardNormalized); |
| | 3 | 114 | | up = Vector3d.Cross(forwardNormalized, right); |
| | | 115 | | |
| | 3 | 116 | | return FromMatrix(new Fixed3x3( |
| | 3 | 117 | | right.X, right.Y, right.Z, |
| | 3 | 118 | | up.X, up.Y, up.Z, |
| | 3 | 119 | | forwardNormalized.X, forwardNormalized.Y, forwardNormalized.Z)); |
| | | 120 | | } |
| | | 121 | | |
| | | 122 | | /// <summary> |
| | | 123 | | /// Converts a rotation matrix into a quaternion representation. |
| | | 124 | | /// </summary> |
| | | 125 | | /// <param name="matrix">The rotation matrix to convert.</param> |
| | | 126 | | /// <returns>A quaternion representing the same rotation as the matrix.</returns> |
| | | 127 | | public static FixedQuaternion FromMatrix(Fixed3x3 matrix) |
| | | 128 | | { |
| | 36 | 129 | | Fixed64 trace = matrix.M11 + matrix.M22 + matrix.M33; |
| | | 130 | | |
| | | 131 | | Fixed64 w, x, y, z; |
| | | 132 | | |
| | 36 | 133 | | if (trace > Fixed64.Zero) |
| | | 134 | | { |
| | 31 | 135 | | Fixed64 s = FixedMath.Sqrt(trace + Fixed64.One); |
| | 31 | 136 | | w = s * Fixed64.Half; |
| | 31 | 137 | | s = Fixed64.Half / s; |
| | 31 | 138 | | x = (matrix.M23 - matrix.M32) * s; |
| | 31 | 139 | | y = (matrix.M31 - matrix.M13) * s; |
| | 31 | 140 | | z = (matrix.M12 - matrix.M21) * s; |
| | | 141 | | } |
| | 5 | 142 | | else if (matrix.M11 > matrix.M22 && matrix.M11 > matrix.M33) |
| | | 143 | | { |
| | 1 | 144 | | Fixed64 s = FixedMath.Sqrt(Fixed64.One + matrix.M11 - matrix.M22 - matrix.M33); |
| | 1 | 145 | | x = s * Fixed64.Half; |
| | 1 | 146 | | s = Fixed64.Half / s; |
| | 1 | 147 | | y = (matrix.M21 + matrix.M12) * s; |
| | 1 | 148 | | z = (matrix.M13 + matrix.M31) * s; |
| | 1 | 149 | | w = (matrix.M23 - matrix.M32) * s; |
| | | 150 | | } |
| | 4 | 151 | | else if (matrix.M22 > matrix.M33) |
| | | 152 | | { |
| | 1 | 153 | | Fixed64 s = FixedMath.Sqrt(Fixed64.One + matrix.M22 - matrix.M11 - matrix.M33); |
| | 1 | 154 | | y = s * Fixed64.Half; |
| | 1 | 155 | | s = Fixed64.Half / s; |
| | 1 | 156 | | z = (matrix.M32 + matrix.M23) * s; |
| | 1 | 157 | | x = (matrix.M21 + matrix.M12) * s; |
| | 1 | 158 | | w = (matrix.M31 - matrix.M13) * s; |
| | | 159 | | } |
| | | 160 | | else |
| | | 161 | | { |
| | 3 | 162 | | Fixed64 s = FixedMath.Sqrt(Fixed64.One + matrix.M33 - matrix.M11 - matrix.M22); |
| | 3 | 163 | | z = s * Fixed64.Half; |
| | 3 | 164 | | s = Fixed64.Half / s; |
| | 3 | 165 | | x = (matrix.M13 + matrix.M31) * s; |
| | 3 | 166 | | y = (matrix.M32 + matrix.M23) * s; |
| | 3 | 167 | | w = (matrix.M12 - matrix.M21) * s; |
| | | 168 | | } |
| | | 169 | | |
| | 36 | 170 | | return new FixedQuaternion(x, y, z, w); |
| | | 171 | | } |
| | | 172 | | |
| | | 173 | | /// <summary> |
| | | 174 | | /// Converts a rotation matrix (upper-left 3x3 part of a 4x4 matrix) into a quaternion representation. |
| | | 175 | | /// </summary> |
| | | 176 | | /// <param name="matrix">The 4x4 matrix containing the rotation component.</param> |
| | | 177 | | /// <remarks>Extracts the upper-left 3x3 rotation part of the 4x4</remarks> |
| | | 178 | | /// <returns>A quaternion representing the same rotation as the matrix.</returns> |
| | | 179 | | public static FixedQuaternion FromMatrix(Fixed4x4 matrix) |
| | | 180 | | { |
| | 29 | 181 | | Fixed3x3 rotationMatrix = new( |
| | 29 | 182 | | matrix.M11, matrix.M12, matrix.M13, |
| | 29 | 183 | | matrix.M21, matrix.M22, matrix.M23, |
| | 29 | 184 | | matrix.M31, matrix.M32, matrix.M33 |
| | 29 | 185 | | ); |
| | | 186 | | |
| | 29 | 187 | | return FromMatrix(rotationMatrix); |
| | | 188 | | } |
| | | 189 | | |
| | | 190 | | /// <summary> |
| | | 191 | | /// Creates a quaternion representing the rotation needed to align canonical <c>+Z</c> forward with the given direct |
| | | 192 | | /// </summary> |
| | | 193 | | /// <remarks> |
| | | 194 | | /// <see cref="Vector3d.Forward"/> returns <see cref="FixedQuaternion.Identity"/>. If an external API names |
| | | 195 | | /// <c>-Z</c> as forward, convert that direction into the canonical convention before calling |
| | | 196 | | /// this method. |
| | | 197 | | /// </remarks> |
| | | 198 | | /// <param name="direction">The target direction vector.</param> |
| | | 199 | | /// <returns>A quaternion representing the rotation to align with the direction.</returns> |
| | | 200 | | public static FixedQuaternion FromDirection(Vector3d direction) |
| | | 201 | | { |
| | 9 | 202 | | Fixed64 directionMagnitudeSquared = direction.MagnitudeSquared; |
| | 9 | 203 | | if (directionMagnitudeSquared == Fixed64.Zero) |
| | 1 | 204 | | return Identity; |
| | | 205 | | |
| | 8 | 206 | | if (FixedMath.Abs(directionMagnitudeSquared - Fixed64.One) > Fixed64.Epsilon) |
| | | 207 | | { |
| | 1 | 208 | | Fixed64 directionMagnitude = FixedMath.Sqrt(directionMagnitudeSquared); |
| | 1 | 209 | | direction = new Vector3d( |
| | 1 | 210 | | direction.X / directionMagnitude, |
| | 1 | 211 | | direction.Y / directionMagnitude, |
| | 1 | 212 | | direction.Z / directionMagnitude); |
| | | 213 | | } |
| | | 214 | | |
| | 8 | 215 | | Fixed64 dot = direction.Z; |
| | 8 | 216 | | if (dot <= -Fixed64.One + Fixed64.Epsilon) |
| | 1 | 217 | | return FromAxisAngle(Vector3d.Up, Fixed64.Pi); |
| | | 218 | | |
| | 7 | 219 | | if (dot >= Fixed64.One - Fixed64.Epsilon) |
| | 2 | 220 | | return Identity; |
| | | 221 | | |
| | 5 | 222 | | if (dot < Fixed64.FromRaw(NearOppositeDirectionDotRaw)) |
| | | 223 | | { |
| | 1 | 224 | | Vector3d axis = new(-direction.Y, direction.X, Fixed64.Zero); |
| | 1 | 225 | | Fixed64 axisMagnitude = axis.Magnitude; |
| | 1 | 226 | | axis = new Vector3d( |
| | 1 | 227 | | axis.X / axisMagnitude, |
| | 1 | 228 | | axis.Y / axisMagnitude, |
| | 1 | 229 | | Fixed64.Zero); |
| | | 230 | | |
| | 1 | 231 | | return FromAxisAngle(axis, FixedMath.Acos(dot)); |
| | | 232 | | } |
| | | 233 | | |
| | 4 | 234 | | Fixed64 scale = FixedMath.Sqrt((Fixed64.One + dot) * Fixed64.Two); |
| | 4 | 235 | | return new FixedQuaternion( |
| | 4 | 236 | | -direction.Y / scale, |
| | 4 | 237 | | direction.X / scale, |
| | 4 | 238 | | Fixed64.Zero, |
| | 4 | 239 | | scale * Fixed64.Half); |
| | | 240 | | } |
| | | 241 | | |
| | | 242 | | /// <summary> |
| | | 243 | | /// Creates a quaternion representing a rotation around a specified axis by a given angle. |
| | | 244 | | /// </summary> |
| | | 245 | | /// <param name="axis">The axis to rotate around (must be normalized).</param> |
| | | 246 | | /// <param name="angle">The rotation angle in radians.</param> |
| | | 247 | | /// <returns>A quaternion representing the rotation.</returns> |
| | | 248 | | public static FixedQuaternion FromAxisAngle(Vector3d axis, Fixed64 angle) |
| | | 249 | | { |
| | | 250 | | // Check if the angle is in a valid range (-pi, pi) |
| | 48 | 251 | | if (angle < -Fixed64.Pi || angle > Fixed64.Pi) |
| | 3 | 252 | | throw new ArgumentOutOfRangeException(nameof(angle), $"Angle must be in the range ({-Fixed64.Pi}, {Fixed64.P |
| | | 253 | | |
| | 45 | 254 | | Fixed64 axisMagnitudeSquared = axis.MagnitudeSquared; |
| | 45 | 255 | | if (axisMagnitudeSquared == Fixed64.Zero) |
| | 1 | 256 | | return Identity; |
| | | 257 | | |
| | 44 | 258 | | if (FixedMath.Abs(axisMagnitudeSquared - Fixed64.One) > Fixed64.Epsilon) |
| | | 259 | | { |
| | 2 | 260 | | Fixed64 axisMagnitude = FixedMath.Sqrt(axisMagnitudeSquared); |
| | 2 | 261 | | axis = new Vector3d( |
| | 2 | 262 | | axis.X / axisMagnitude, |
| | 2 | 263 | | axis.Y / axisMagnitude, |
| | 2 | 264 | | axis.Z / axisMagnitude); |
| | | 265 | | } |
| | | 266 | | |
| | 44 | 267 | | Fixed64 halfAngle = angle / Fixed64.Two; // Half-angle formula |
| | 44 | 268 | | Fixed64 sinHalfAngle = FixedMath.Sin(halfAngle); |
| | 44 | 269 | | Fixed64 cosHalfAngle = FixedMath.Cos(halfAngle); |
| | | 270 | | |
| | 44 | 271 | | return new FixedQuaternion( |
| | 44 | 272 | | axis.X * sinHalfAngle, |
| | 44 | 273 | | axis.Y * sinHalfAngle, |
| | 44 | 274 | | axis.Z * sinHalfAngle, |
| | 44 | 275 | | cosHalfAngle); |
| | | 276 | | } |
| | | 277 | | |
| | | 278 | | /// <summary> |
| | | 279 | | /// Assume the input angles are in degrees and converts them to radians before calling <see cref="FromEulerAngles"/> |
| | | 280 | | /// </summary> |
| | | 281 | | /// <param name="pitch"></param> |
| | | 282 | | /// <param name="yaw"></param> |
| | | 283 | | /// <param name="roll"></param> |
| | | 284 | | /// <returns></returns> |
| | | 285 | | public static FixedQuaternion FromEulerAnglesInDegrees(Fixed64 pitch, Fixed64 yaw, Fixed64 roll) |
| | | 286 | | { |
| | | 287 | | // Convert input angles from degrees to radians |
| | 37 | 288 | | pitch = FixedMath.DegToRad(pitch); |
| | 37 | 289 | | yaw = FixedMath.DegToRad(yaw); |
| | 37 | 290 | | roll = FixedMath.DegToRad(roll); |
| | | 291 | | |
| | | 292 | | // Call the original method that expects angles in radians |
| | 37 | 293 | | return FromEulerAngles(pitch, yaw, roll); |
| | | 294 | | } |
| | | 295 | | |
| | | 296 | | /// <summary> |
| | | 297 | | /// Converts Euler angles (pitch, yaw, roll) to a quaternion and normalizes the result afterwards. |
| | | 298 | | /// Assumes the input angles are in radians. |
| | | 299 | | /// </summary> |
| | | 300 | | /// <remarks> |
| | | 301 | | /// The order of operations is YXZ or yaw-pitch-roll |
| | | 302 | | /// </remarks> |
| | | 303 | | public static FixedQuaternion FromEulerAngles(Fixed64 pitch, Fixed64 yaw, Fixed64 roll) |
| | | 304 | | { |
| | | 305 | | // Check if the angles are in a valid range (-pi, pi) |
| | 52 | 306 | | if (pitch < -Fixed64.Pi || pitch > Fixed64.Pi) |
| | 3 | 307 | | throw new ArgumentOutOfRangeException(nameof(pitch), $"Pitch must be in the range ({-Fixed64.Pi}, {Fixed64.P |
| | | 308 | | |
| | 49 | 309 | | if (yaw < -Fixed64.Pi || yaw > Fixed64.Pi) |
| | 3 | 310 | | throw new ArgumentOutOfRangeException(nameof(yaw), $"Yaw must be in the range ({-Fixed64.Pi}, {Fixed64.Pi}), |
| | | 311 | | |
| | 46 | 312 | | if (roll < -Fixed64.Pi || roll > Fixed64.Pi) |
| | 3 | 313 | | throw new ArgumentOutOfRangeException(nameof(roll), $"Roll must be in the range ({-Fixed64.Pi}, {Fixed64.Pi} |
| | | 314 | | |
| | 43 | 315 | | Fixed64 halfPitch = pitch / Fixed64.Two; |
| | 43 | 316 | | Fixed64 halfYaw = yaw / Fixed64.Two; |
| | 43 | 317 | | Fixed64 halfRoll = roll / Fixed64.Two; |
| | | 318 | | |
| | 43 | 319 | | Fixed64 sx = FixedMath.Sin(halfPitch); |
| | 43 | 320 | | Fixed64 cx = FixedMath.Cos(halfPitch); |
| | 43 | 321 | | Fixed64 sy = FixedMath.Sin(halfYaw); |
| | 43 | 322 | | Fixed64 cy = FixedMath.Cos(halfYaw); |
| | 43 | 323 | | Fixed64 sz = FixedMath.Sin(halfRoll); |
| | 43 | 324 | | Fixed64 cz = FixedMath.Cos(halfRoll); |
| | | 325 | | |
| | | 326 | | // q = qy * qx * qz |
| | 43 | 327 | | Fixed64 x = (cx * sy * sz) + (cy * cz * sx); |
| | 43 | 328 | | Fixed64 y = (cx * cz * sy) - (cy * sx * sz); |
| | 43 | 329 | | Fixed64 z = (cx * cy * sz) - (cz * sx * sy); |
| | 43 | 330 | | Fixed64 w = (cx * cy * cz) + (sx * sy * sz); |
| | | 331 | | |
| | 43 | 332 | | return GetNormalized(new FixedQuaternion(x, y, z, w)); |
| | | 333 | | } |
| | | 334 | | |
| | | 335 | | /// <summary> |
| | | 336 | | /// Computes the logarithm of a quaternion, which represents the rotational displacement. |
| | | 337 | | /// This is useful for interpolation and angular velocity calculations. |
| | | 338 | | /// </summary> |
| | | 339 | | /// <param name="q">The quaternion to compute the logarithm of.</param> |
| | | 340 | | /// <returns>A Vector3d representing the logarithm of the quaternion (axis-angle representation).</returns> |
| | | 341 | | /// <remarks> |
| | | 342 | | /// The logarithm of a unit quaternion is given by: |
| | | 343 | | /// log(q) = (θ * v̀‚), where: |
| | | 344 | | /// - θ = 2 * acos(w) is the rotation angle. |
| | | 345 | | /// - v̀‚ = (x, y, z) / ||(x, y, z)|| is the unit vector representing the axis of rotation. |
| | | 346 | | /// If the quaternion is close to identity, the function returns a zero vector to avoid numerical instability. |
| | | 347 | | /// </remarks> |
| | | 348 | | public static Vector3d QuaternionLog(FixedQuaternion q) |
| | | 349 | | { |
| | | 350 | | // Ensure the quaternion is normalized |
| | 9 | 351 | | q = q.Normalized; |
| | | 352 | | |
| | | 353 | | // Extract vector part |
| | 9 | 354 | | Vector3d v = new(q.X, q.Y, q.Z); |
| | 9 | 355 | | Fixed64 vLength = v.Magnitude; |
| | | 356 | | |
| | | 357 | | // If rotation is very small, avoid division by zero |
| | 9 | 358 | | if (vLength < Fixed64.FromRaw(0x00001000L)) // Small epsilon |
| | 3 | 359 | | return Vector3d.Zero; |
| | | 360 | | |
| | | 361 | | // Compute angle (theta = 2 * acos(w)) |
| | 6 | 362 | | Fixed64 theta = Fixed64.Two * FixedMath.Acos(q.W); |
| | | 363 | | |
| | | 364 | | // Convert to angular velocity |
| | 6 | 365 | | return (v / vLength) * theta; |
| | | 366 | | } |
| | | 367 | | |
| | | 368 | | /// <summary> |
| | | 369 | | /// Computes the angular velocity required to move from `previousRotation` to `currentRotation` over a given time st |
| | | 370 | | /// </summary> |
| | | 371 | | /// <param name="currentRotation">The current orientation as a quaternion.</param> |
| | | 372 | | /// <param name="previousRotation">The previous orientation as a quaternion.</param> |
| | | 373 | | /// <param name="deltaTime">The time step over which the rotation occurs.</param> |
| | | 374 | | /// <returns>A Vector3d representing the angular velocity (in radians per second).</returns> |
| | | 375 | | /// <remarks> |
| | | 376 | | /// This function calculates the change in rotation over `deltaTime` and converts it into angular velocity. |
| | | 377 | | /// - First, it computes the relative rotation: `rotationDelta = currentRotation * previousRotation.Inverse()`. |
| | | 378 | | /// - Then, it applies `QuaternionLog(rotationDelta)` to extract the axis-angle representation. |
| | | 379 | | /// - Finally, it divides by `deltaTime` to compute the angular velocity. |
| | | 380 | | /// </remarks> |
| | | 381 | | public static Vector3d ToAngularVelocity( |
| | | 382 | | FixedQuaternion currentRotation, |
| | | 383 | | FixedQuaternion previousRotation, |
| | | 384 | | Fixed64 deltaTime) |
| | | 385 | | { |
| | 4 | 386 | | FixedQuaternion rotationDelta = currentRotation * previousRotation.Inverse(); |
| | 4 | 387 | | Vector3d angularDisplacement = QuaternionLog(rotationDelta); |
| | | 388 | | |
| | 4 | 389 | | return angularDisplacement / deltaTime; // Convert to angular velocity |
| | | 390 | | } |
| | | 391 | | |
| | | 392 | | /// <summary> |
| | | 393 | | /// Performs a simple linear interpolation between the components of the input quaternions |
| | | 394 | | /// </summary> |
| | | 395 | | public static FixedQuaternion Lerp(FixedQuaternion a, FixedQuaternion b, Fixed64 t) |
| | | 396 | | { |
| | 5 | 397 | | t = FixedMath.Clamp01(t); |
| | | 398 | | |
| | 5 | 399 | | if (Dot(a, b) < Fixed64.Zero) |
| | 1 | 400 | | b = -b; |
| | | 401 | | |
| | | 402 | | FixedQuaternion result; |
| | 5 | 403 | | Fixed64 oneMinusT = Fixed64.One - t; |
| | 5 | 404 | | result.X = a.X * oneMinusT + b.X * t; |
| | 5 | 405 | | result.Y = a.Y * oneMinusT + b.Y * t; |
| | 5 | 406 | | result.Z = a.Z * oneMinusT + b.Z * t; |
| | 5 | 407 | | result.W = a.W * oneMinusT + b.W * t; |
| | | 408 | | |
| | 5 | 409 | | result.NormalizeInPlace(); |
| | | 410 | | |
| | 5 | 411 | | return result; |
| | | 412 | | } |
| | | 413 | | |
| | | 414 | | /// <summary> |
| | | 415 | | /// Calculates the spherical linear interpolation, which results in a smoother and more accurate rotation interpola |
| | | 416 | | /// </summary> |
| | | 417 | | public static FixedQuaternion Slerp(FixedQuaternion a, FixedQuaternion b, Fixed64 t) |
| | | 418 | | { |
| | 4 | 419 | | t = FixedMath.Clamp01(t); |
| | | 420 | | |
| | 4 | 421 | | Fixed64 cosOmega = a.X * b.X + a.Y * b.Y + a.Z * b.Z + a.W * b.W; |
| | | 422 | | |
| | | 423 | | // If the dot product is negative, negate one of the input quaternions. |
| | | 424 | | // This ensures that the interpolation takes the shortest path around the sphere. |
| | 4 | 425 | | if (cosOmega < Fixed64.Zero) |
| | | 426 | | { |
| | 1 | 427 | | b.X = -b.X; |
| | 1 | 428 | | b.Y = -b.Y; |
| | 1 | 429 | | b.Z = -b.Z; |
| | 1 | 430 | | b.W = -b.W; |
| | 1 | 431 | | cosOmega = -cosOmega; |
| | | 432 | | } |
| | | 433 | | |
| | | 434 | | Fixed64 k0, k1; |
| | | 435 | | |
| | | 436 | | // If the quaternions are close, use linear interpolation |
| | 4 | 437 | | if (cosOmega > Fixed64.One - Fixed64.Epsilon) |
| | | 438 | | { |
| | 1 | 439 | | k0 = Fixed64.One - t; |
| | 1 | 440 | | k1 = t; |
| | | 441 | | } |
| | | 442 | | else |
| | | 443 | | { |
| | | 444 | | // Otherwise, use spherical linear interpolation |
| | 3 | 445 | | Fixed64 sinOmega = FixedMath.Sqrt(Fixed64.One - cosOmega * cosOmega); |
| | 3 | 446 | | Fixed64 omega = FixedMath.Atan2(sinOmega, cosOmega); |
| | | 447 | | |
| | 3 | 448 | | k0 = FixedMath.Sin((Fixed64.One - t) * omega) / sinOmega; |
| | 3 | 449 | | k1 = FixedMath.Sin(t * omega) / sinOmega; |
| | | 450 | | } |
| | | 451 | | |
| | | 452 | | FixedQuaternion result; |
| | 4 | 453 | | result.X = a.X * k0 + b.X * k1; |
| | 4 | 454 | | result.Y = a.Y * k0 + b.Y * k1; |
| | 4 | 455 | | result.Z = a.Z * k0 + b.Z * k1; |
| | 4 | 456 | | result.W = a.W * k0 + b.W * k1; |
| | | 457 | | |
| | 4 | 458 | | return result; |
| | | 459 | | } |
| | | 460 | | |
| | | 461 | | /// <summary> |
| | | 462 | | /// Returns the angle in degrees between two rotations a and b. |
| | | 463 | | /// </summary> |
| | | 464 | | /// <param name="a">The first rotation.</param> |
| | | 465 | | /// <param name="b">The second rotation.</param> |
| | | 466 | | /// <returns>The angle in degrees between the two rotations.</returns> |
| | | 467 | | public static Fixed64 Angle(FixedQuaternion a, FixedQuaternion b) |
| | | 468 | | { |
| | | 469 | | // Calculate the dot product of the two quaternions |
| | 3 | 470 | | Fixed64 dot = Dot(a, b); |
| | | 471 | | |
| | | 472 | | // Ensure the dot product is in the range of [-1, 1] to avoid floating-point inaccuracies |
| | 3 | 473 | | dot = FixedMath.Clamp(dot, -Fixed64.One, Fixed64.One); |
| | | 474 | | |
| | | 475 | | // Calculate the angle between the two quaternions using the inverse cosine (arccos) |
| | | 476 | | // arccos(dot(a, b)) gives us the angle in radians, so we convert it to degrees |
| | 3 | 477 | | Fixed64 angleInRadians = FixedMath.Acos(dot); |
| | | 478 | | |
| | | 479 | | // Convert the angle from radians to degrees |
| | 3 | 480 | | Fixed64 angleInDegrees = FixedMath.RadToDeg(angleInRadians); |
| | | 481 | | |
| | 3 | 482 | | return angleInDegrees; |
| | | 483 | | } |
| | | 484 | | |
| | | 485 | | /// <summary> |
| | | 486 | | /// Creates a quaternion from an angle and axis. |
| | | 487 | | /// </summary> |
| | | 488 | | /// <param name="angle">The angle in degrees.</param> |
| | | 489 | | /// <param name="axis">The axis to rotate around (must be normalized).</param> |
| | | 490 | | /// <returns>A quaternion representing the rotation.</returns> |
| | | 491 | | public static FixedQuaternion AngleAxis(Fixed64 angle, Vector3d axis) |
| | | 492 | | { |
| | | 493 | | // Convert the angle to radians |
| | 1 | 494 | | angle = angle.ToRadians(); |
| | | 495 | | |
| | | 496 | | // Normalize the axis |
| | 1 | 497 | | axis = axis.Normalized; |
| | | 498 | | |
| | | 499 | | // Use the half-angle formula (sin(theta / 2), cos(theta / 2)) |
| | 1 | 500 | | Fixed64 halfAngle = angle / Fixed64.Two; |
| | 1 | 501 | | Fixed64 sinHalfAngle = FixedMath.Sin(halfAngle); |
| | 1 | 502 | | Fixed64 cosHalfAngle = FixedMath.Cos(halfAngle); |
| | | 503 | | |
| | 1 | 504 | | return new FixedQuaternion( |
| | 1 | 505 | | axis.X * sinHalfAngle, |
| | 1 | 506 | | axis.Y * sinHalfAngle, |
| | 1 | 507 | | axis.Z * sinHalfAngle, |
| | 1 | 508 | | cosHalfAngle |
| | 1 | 509 | | ); |
| | | 510 | | } |
| | | 511 | | |
| | | 512 | | /// <summary> |
| | | 513 | | /// Calculates the dot product of two quaternions. |
| | | 514 | | /// </summary> |
| | | 515 | | /// <param name="a">The first quaternion.</param> |
| | | 516 | | /// <param name="b">The second quaternion.</param> |
| | | 517 | | /// <returns>The dot product of the two quaternions.</returns> |
| | | 518 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | 15 | 519 | | public static Fixed64 Dot(FixedQuaternion a, FixedQuaternion b) => a.W * b.W + a.X * b.X + a.Y * b.Y + a.Z * b.Z; |
| | | 520 | | |
| | | 521 | | #endregion |
| | | 522 | | } |