001// Copyright (c) FIRST and other WPILib contributors. 002// Open Source Software; you can modify and/or share it under the terms of 003// the WPILib BSD license file in the root directory of this project. 004 005package edu.wpi.first.math.geometry; 006 007import static edu.wpi.first.units.Units.Radians; 008 009import com.fasterxml.jackson.annotation.JsonAutoDetect; 010import com.fasterxml.jackson.annotation.JsonCreator; 011import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 012import com.fasterxml.jackson.annotation.JsonProperty; 013import edu.wpi.first.math.MatBuilder; 014import edu.wpi.first.math.MathSharedStore; 015import edu.wpi.first.math.MathUtil; 016import edu.wpi.first.math.Matrix; 017import edu.wpi.first.math.Nat; 018import edu.wpi.first.math.VecBuilder; 019import edu.wpi.first.math.Vector; 020import edu.wpi.first.math.geometry.proto.Rotation3dProto; 021import edu.wpi.first.math.geometry.struct.Rotation3dStruct; 022import edu.wpi.first.math.interpolation.Interpolatable; 023import edu.wpi.first.math.numbers.N3; 024import edu.wpi.first.units.measure.Angle; 025import edu.wpi.first.util.protobuf.ProtobufSerializable; 026import edu.wpi.first.util.struct.StructSerializable; 027import java.util.Objects; 028 029/** A rotation in a 3D coordinate frame represented by a quaternion. */ 030@JsonIgnoreProperties(ignoreUnknown = true) 031@JsonAutoDetect(getterVisibility = JsonAutoDetect.Visibility.NONE) 032public class Rotation3d 033 implements Interpolatable<Rotation3d>, ProtobufSerializable, StructSerializable { 034 /** 035 * A preallocated Rotation3d representing no rotation. 036 * 037 * <p>This exists to avoid allocations for common rotations. 038 */ 039 public static final Rotation3d kZero = new Rotation3d(); 040 041 private final Quaternion m_q; 042 043 /** Constructs a Rotation3d representing no rotation. */ 044 public Rotation3d() { 045 m_q = new Quaternion(); 046 } 047 048 /** 049 * Constructs a Rotation3d from a quaternion. 050 * 051 * @param q The quaternion. 052 */ 053 @JsonCreator 054 public Rotation3d(@JsonProperty(required = true, value = "quaternion") Quaternion q) { 055 m_q = q.normalize(); 056 } 057 058 /** 059 * Constructs a Rotation3d from extrinsic roll, pitch, and yaw. 060 * 061 * <p>Extrinsic rotations occur in that order around the axes in the fixed global frame rather 062 * than the body frame. 063 * 064 * <p>Angles are measured counterclockwise with the rotation axis pointing "out of the page". If 065 * you point your right thumb along the positive axis direction, your fingers curl in the 066 * direction of positive rotation. 067 * 068 * @param roll The counterclockwise rotation angle around the X axis (roll) in radians. 069 * @param pitch The counterclockwise rotation angle around the Y axis (pitch) in radians. 070 * @param yaw The counterclockwise rotation angle around the Z axis (yaw) in radians. 071 */ 072 public Rotation3d(double roll, double pitch, double yaw) { 073 // https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles#Euler_angles_to_quaternion_conversion 074 double cr = Math.cos(roll * 0.5); 075 double sr = Math.sin(roll * 0.5); 076 077 double cp = Math.cos(pitch * 0.5); 078 double sp = Math.sin(pitch * 0.5); 079 080 double cy = Math.cos(yaw * 0.5); 081 double sy = Math.sin(yaw * 0.5); 082 083 m_q = 084 new Quaternion( 085 cr * cp * cy + sr * sp * sy, 086 sr * cp * cy - cr * sp * sy, 087 cr * sp * cy + sr * cp * sy, 088 cr * cp * sy - sr * sp * cy); 089 } 090 091 /** 092 * Constructs a Rotation3d from extrinsic roll, pitch, and yaw. 093 * 094 * <p>Extrinsic rotations occur in that order around the axes in the fixed global frame rather 095 * than the body frame. 096 * 097 * <p>Angles are measured counterclockwise with the rotation axis pointing "out of the page". If 098 * you point your right thumb along the positive axis direction, your fingers curl in the 099 * direction of positive rotation. 100 * 101 * @param roll The counterclockwise rotation angle around the X axis (roll). 102 * @param pitch The counterclockwise rotation angle around the Y axis (pitch). 103 * @param yaw The counterclockwise rotation angle around the Z axis (yaw). 104 */ 105 public Rotation3d(Angle roll, Angle pitch, Angle yaw) { 106 this(roll.in(Radians), pitch.in(Radians), yaw.in(Radians)); 107 } 108 109 /** 110 * Constructs a Rotation3d with the given rotation vector representation. This representation is 111 * equivalent to axis-angle, where the normalized axis is multiplied by the rotation around the 112 * axis in radians. 113 * 114 * @param rvec The rotation vector. 115 */ 116 public Rotation3d(Vector<N3> rvec) { 117 this(rvec, rvec.norm()); 118 } 119 120 /** 121 * Constructs a Rotation3d with the given axis-angle representation. The axis doesn't have to be 122 * normalized. 123 * 124 * @param axis The rotation axis. 125 * @param angleRadians The rotation around the axis in radians. 126 */ 127 public Rotation3d(Vector<N3> axis, double angleRadians) { 128 double norm = axis.norm(); 129 if (norm == 0.0) { 130 m_q = new Quaternion(); 131 return; 132 } 133 134 // https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles#Definition 135 var v = axis.times(1.0 / norm).times(Math.sin(angleRadians / 2.0)); 136 m_q = new Quaternion(Math.cos(angleRadians / 2.0), v.get(0, 0), v.get(1, 0), v.get(2, 0)); 137 } 138 139 /** 140 * Constructs a Rotation3d with the given axis-angle representation. The axis doesn't have to be 141 * normalized. 142 * 143 * @param axis The rotation axis. 144 * @param angle The rotation around the axis. 145 */ 146 public Rotation3d(Vector<N3> axis, Angle angle) { 147 this(axis, angle.in(Radians)); 148 } 149 150 /** 151 * Constructs a Rotation3d from a rotation matrix. 152 * 153 * @param rotationMatrix The rotation matrix. 154 * @throws IllegalArgumentException if the rotation matrix isn't special orthogonal. 155 */ 156 public Rotation3d(Matrix<N3, N3> rotationMatrix) { 157 final var R = rotationMatrix; 158 159 // Require that the rotation matrix is special orthogonal. This is true if 160 // the matrix is orthogonal (RRᵀ = I) and normalized (determinant is 1). 161 if (R.times(R.transpose()).minus(Matrix.eye(Nat.N3())).normF() > 1e-9) { 162 var msg = "Rotation matrix isn't orthogonal\n\nR =\n" + R.getStorage().toString() + '\n'; 163 MathSharedStore.reportError(msg, Thread.currentThread().getStackTrace()); 164 throw new IllegalArgumentException(msg); 165 } 166 if (Math.abs(R.det() - 1.0) > 1e-9) { 167 var msg = 168 "Rotation matrix is orthogonal but not special orthogonal\n\nR =\n" 169 + R.getStorage().toString() 170 + '\n'; 171 MathSharedStore.reportError(msg, Thread.currentThread().getStackTrace()); 172 throw new IllegalArgumentException(msg); 173 } 174 175 // Turn rotation matrix into a quaternion 176 // https://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToQuaternion/ 177 double trace = R.get(0, 0) + R.get(1, 1) + R.get(2, 2); 178 double w; 179 double x; 180 double y; 181 double z; 182 183 if (trace > 0.0) { 184 double s = 0.5 / Math.sqrt(trace + 1.0); 185 w = 0.25 / s; 186 x = (R.get(2, 1) - R.get(1, 2)) * s; 187 y = (R.get(0, 2) - R.get(2, 0)) * s; 188 z = (R.get(1, 0) - R.get(0, 1)) * s; 189 } else { 190 if (R.get(0, 0) > R.get(1, 1) && R.get(0, 0) > R.get(2, 2)) { 191 double s = 2.0 * Math.sqrt(1.0 + R.get(0, 0) - R.get(1, 1) - R.get(2, 2)); 192 w = (R.get(2, 1) - R.get(1, 2)) / s; 193 x = 0.25 * s; 194 y = (R.get(0, 1) + R.get(1, 0)) / s; 195 z = (R.get(0, 2) + R.get(2, 0)) / s; 196 } else if (R.get(1, 1) > R.get(2, 2)) { 197 double s = 2.0 * Math.sqrt(1.0 + R.get(1, 1) - R.get(0, 0) - R.get(2, 2)); 198 w = (R.get(0, 2) - R.get(2, 0)) / s; 199 x = (R.get(0, 1) + R.get(1, 0)) / s; 200 y = 0.25 * s; 201 z = (R.get(1, 2) + R.get(2, 1)) / s; 202 } else { 203 double s = 2.0 * Math.sqrt(1.0 + R.get(2, 2) - R.get(0, 0) - R.get(1, 1)); 204 w = (R.get(1, 0) - R.get(0, 1)) / s; 205 x = (R.get(0, 2) + R.get(2, 0)) / s; 206 y = (R.get(1, 2) + R.get(2, 1)) / s; 207 z = 0.25 * s; 208 } 209 } 210 211 m_q = new Quaternion(w, x, y, z); 212 } 213 214 /** 215 * Constructs a Rotation3d that rotates the initial vector onto the final vector. 216 * 217 * <p>This is useful for turning a 3D vector (final) into an orientation relative to a coordinate 218 * system vector (initial). 219 * 220 * @param initial The initial vector. 221 * @param last The final vector. 222 */ 223 public Rotation3d(Vector<N3> initial, Vector<N3> last) { 224 double dot = initial.dot(last); 225 double normProduct = initial.norm() * last.norm(); 226 double dotNorm = dot / normProduct; 227 228 if (dotNorm > 1.0 - 1E-9) { 229 // If the dot product is 1, the two vectors point in the same direction so 230 // there's no rotation. The default initialization of m_q will work. 231 m_q = new Quaternion(); 232 } else if (dotNorm < -1.0 + 1E-9) { 233 // If the dot product is -1, the two vectors are antiparallel, so a 180° 234 // rotation is required. Any other vector can be used to generate an 235 // orthogonal one. 236 237 double x = Math.abs(initial.get(0, 0)); 238 double y = Math.abs(initial.get(1, 0)); 239 double z = Math.abs(initial.get(2, 0)); 240 241 // Find vector that is most orthogonal to initial vector 242 Vector<N3> other; 243 if (x < y) { 244 if (x < z) { 245 // Use x-axis 246 other = VecBuilder.fill(1, 0, 0); 247 } else { 248 // Use z-axis 249 other = VecBuilder.fill(0, 0, 1); 250 } 251 } else { 252 if (y < z) { 253 // Use y-axis 254 other = VecBuilder.fill(0, 1, 0); 255 } else { 256 // Use z-axis 257 other = VecBuilder.fill(0, 0, 1); 258 } 259 } 260 261 var axis = Vector.cross(initial, other); 262 263 double axisNorm = axis.norm(); 264 m_q = 265 new Quaternion( 266 0.0, axis.get(0, 0) / axisNorm, axis.get(1, 0) / axisNorm, axis.get(2, 0) / axisNorm); 267 } else { 268 var axis = Vector.cross(initial, last); 269 270 // https://stackoverflow.com/a/11741520 271 m_q = 272 new Quaternion(normProduct + dot, axis.get(0, 0), axis.get(1, 0), axis.get(2, 0)) 273 .normalize(); 274 } 275 } 276 277 /** 278 * Constructs a 3D rotation from a 2D rotation in the X-Y plane. 279 * 280 * @param rotation The 2D rotation. 281 * @see Pose3d#Pose3d(Pose2d) 282 * @see Transform3d#Transform3d(Transform2d) 283 */ 284 public Rotation3d(Rotation2d rotation) { 285 this(0.0, 0.0, rotation.getRadians()); 286 } 287 288 /** 289 * Adds two rotations together. 290 * 291 * @param other The rotation to add. 292 * @return The sum of the two rotations. 293 */ 294 public Rotation3d plus(Rotation3d other) { 295 return rotateBy(other); 296 } 297 298 /** 299 * Subtracts the new rotation from the current rotation and returns the new rotation. 300 * 301 * @param other The rotation to subtract. 302 * @return The difference between the two rotations. 303 */ 304 public Rotation3d minus(Rotation3d other) { 305 return rotateBy(other.unaryMinus()); 306 } 307 308 /** 309 * Takes the inverse of the current rotation. 310 * 311 * @return The inverse of the current rotation. 312 */ 313 public Rotation3d unaryMinus() { 314 return new Rotation3d(m_q.inverse()); 315 } 316 317 /** 318 * Multiplies the current rotation by a scalar. 319 * 320 * @param scalar The scalar. 321 * @return The new scaled Rotation3d. 322 */ 323 public Rotation3d times(double scalar) { 324 // https://en.wikipedia.org/wiki/Slerp#Quaternion_Slerp 325 if (m_q.getW() >= 0.0) { 326 return new Rotation3d( 327 VecBuilder.fill(m_q.getX(), m_q.getY(), m_q.getZ()), 328 2.0 * scalar * Math.acos(m_q.getW())); 329 } else { 330 return new Rotation3d( 331 VecBuilder.fill(-m_q.getX(), -m_q.getY(), -m_q.getZ()), 332 2.0 * scalar * Math.acos(-m_q.getW())); 333 } 334 } 335 336 /** 337 * Divides the current rotation by a scalar. 338 * 339 * @param scalar The scalar. 340 * @return The new scaled Rotation3d. 341 */ 342 public Rotation3d div(double scalar) { 343 return times(1.0 / scalar); 344 } 345 346 /** 347 * Adds the new rotation to the current rotation. The other rotation is applied extrinsically, 348 * which means that it rotates around the global axes. For example, {@code new 349 * Rotation3d(Units.degreesToRadians(90), 0, 0).rotateBy(new Rotation3d(0, 350 * Units.degreesToRadians(45), 0))} rotates by 90 degrees around the +X axis and then by 45 351 * degrees around the global +Y axis. (This is equivalent to {@code new 352 * Rotation3d(Units.degreesToRadians(90), Units.degreesToRadians(45), 0)}) 353 * 354 * @param other The extrinsic rotation to rotate by. 355 * @return The new rotated Rotation3d. 356 */ 357 public Rotation3d rotateBy(Rotation3d other) { 358 return new Rotation3d(other.m_q.times(m_q)); 359 } 360 361 /** 362 * Returns the quaternion representation of the Rotation3d. 363 * 364 * @return The quaternion representation of the Rotation3d. 365 */ 366 @JsonProperty(value = "quaternion") 367 public Quaternion getQuaternion() { 368 return m_q; 369 } 370 371 /** 372 * Returns the counterclockwise rotation angle around the X axis (roll) in radians. 373 * 374 * @return The counterclockwise rotation angle around the X axis (roll) in radians. 375 */ 376 public double getX() { 377 final var w = m_q.getW(); 378 final var x = m_q.getX(); 379 final var y = m_q.getY(); 380 final var z = m_q.getZ(); 381 382 // wpimath/algorithms.md 383 final var cxcy = 1.0 - 2.0 * (x * x + y * y); 384 final var sxcy = 2.0 * (w * x + y * z); 385 final var cy_sq = cxcy * cxcy + sxcy * sxcy; 386 if (cy_sq > 1e-20) { 387 return Math.atan2(sxcy, cxcy); 388 } else { 389 return 0.0; 390 } 391 } 392 393 /** 394 * Returns the counterclockwise rotation angle around the Y axis (pitch) in radians. 395 * 396 * @return The counterclockwise rotation angle around the Y axis (pitch) in radians. 397 */ 398 public double getY() { 399 final var w = m_q.getW(); 400 final var x = m_q.getX(); 401 final var y = m_q.getY(); 402 final var z = m_q.getZ(); 403 404 // https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles#Quaternion_to_Euler_angles_(in_3-2-1_sequence)_conversion 405 double ratio = 2.0 * (w * y - z * x); 406 if (Math.abs(ratio) >= 1.0) { 407 return Math.copySign(Math.PI / 2.0, ratio); 408 } else { 409 return Math.asin(ratio); 410 } 411 } 412 413 /** 414 * Returns the counterclockwise rotation angle around the Z axis (yaw) in radians. 415 * 416 * @return The counterclockwise rotation angle around the Z axis (yaw) in radians. 417 */ 418 public double getZ() { 419 final var w = m_q.getW(); 420 final var x = m_q.getX(); 421 final var y = m_q.getY(); 422 final var z = m_q.getZ(); 423 424 // wpimath/algorithms.md 425 final var cycz = 1.0 - 2.0 * (y * y + z * z); 426 final var cysz = 2.0 * (w * z + x * y); 427 final var cy_sq = cycz * cycz + cysz * cysz; 428 if (cy_sq > 1e-20) { 429 return Math.atan2(cysz, cycz); 430 } else { 431 return Math.atan2(2.0 * w * z, w * w - z * z); 432 } 433 } 434 435 /** 436 * Returns the counterclockwise rotation angle around the X axis (roll) in a measure. 437 * 438 * @return The counterclockwise rotation angle around the x axis (roll) in a measure. 439 */ 440 public Angle getMeasureX() { 441 return Radians.of(getX()); 442 } 443 444 /** 445 * Returns the counterclockwise rotation angle around the Y axis (pitch) in a measure. 446 * 447 * @return The counterclockwise rotation angle around the y axis (pitch) in a measure. 448 */ 449 public Angle getMeasureY() { 450 return Radians.of(getY()); 451 } 452 453 /** 454 * Returns the counterclockwise rotation angle around the Z axis (yaw) in a measure. 455 * 456 * @return The counterclockwise rotation angle around the z axis (yaw) in a measure. 457 */ 458 public Angle getMeasureZ() { 459 return Radians.of(getZ()); 460 } 461 462 /** 463 * Returns the axis in the axis-angle representation of this rotation. 464 * 465 * @return The axis in the axis-angle representation. 466 */ 467 public Vector<N3> getAxis() { 468 double norm = 469 Math.sqrt(m_q.getX() * m_q.getX() + m_q.getY() * m_q.getY() + m_q.getZ() * m_q.getZ()); 470 if (norm == 0.0) { 471 return VecBuilder.fill(0.0, 0.0, 0.0); 472 } else { 473 return VecBuilder.fill(m_q.getX() / norm, m_q.getY() / norm, m_q.getZ() / norm); 474 } 475 } 476 477 /** 478 * Returns the angle in radians in the axis-angle representation of this rotation. 479 * 480 * @return The angle in radians in the axis-angle representation of this rotation. 481 */ 482 public double getAngle() { 483 double norm = 484 Math.sqrt(m_q.getX() * m_q.getX() + m_q.getY() * m_q.getY() + m_q.getZ() * m_q.getZ()); 485 return 2.0 * Math.atan2(norm, m_q.getW()); 486 } 487 488 /** 489 * Returns rotation matrix representation of this rotation. 490 * 491 * @return Rotation matrix representation of this rotation. 492 */ 493 public Matrix<N3, N3> toMatrix() { 494 double w = m_q.getW(); 495 double x = m_q.getX(); 496 double y = m_q.getY(); 497 double z = m_q.getZ(); 498 499 // https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Quaternion-derived_rotation_matrix 500 return MatBuilder.fill( 501 Nat.N3(), 502 Nat.N3(), 503 1.0 - 2.0 * (y * y + z * z), 504 2.0 * (x * y - w * z), 505 2.0 * (x * z + w * y), 506 2.0 * (x * y + w * z), 507 1.0 - 2.0 * (x * x + z * z), 508 2.0 * (y * z - w * x), 509 2.0 * (x * z - w * y), 510 2.0 * (y * z + w * x), 511 1.0 - 2.0 * (x * x + y * y)); 512 } 513 514 /** 515 * Returns rotation vector representation of this rotation. 516 * 517 * @return Rotation vector representation of this rotation. 518 */ 519 public Vector<N3> toVector() { 520 return m_q.toRotationVector(); 521 } 522 523 /** 524 * Returns the angle in a measure in the axis-angle representation of this rotation. 525 * 526 * @return The angle in a measure in the axis-angle representation of this rotation. 527 */ 528 public Angle getMeasureAngle() { 529 return Radians.of(getAngle()); 530 } 531 532 /** 533 * Returns a Rotation2d representing this Rotation3d projected into the X-Y plane. 534 * 535 * @return A Rotation2d representing this Rotation3d projected into the X-Y plane. 536 */ 537 public Rotation2d toRotation2d() { 538 return new Rotation2d(getZ()); 539 } 540 541 @Override 542 public String toString() { 543 return String.format("Rotation3d(%s)", m_q); 544 } 545 546 /** 547 * Checks equality between this Rotation3d and another object. 548 * 549 * @param obj The other object. 550 * @return Whether the two objects are equal or not. 551 */ 552 @Override 553 public boolean equals(Object obj) { 554 return obj instanceof Rotation3d other 555 && Math.abs(Math.abs(m_q.dot(other.m_q)) - m_q.norm() * other.m_q.norm()) < 1e-9; 556 } 557 558 @Override 559 public int hashCode() { 560 return Objects.hash(m_q); 561 } 562 563 @Override 564 public Rotation3d interpolate(Rotation3d endValue, double t) { 565 return plus(endValue.minus(this).times(MathUtil.clamp(t, 0, 1))); 566 } 567 568 /** Rotation3d protobuf for serialization. */ 569 public static final Rotation3dProto proto = new Rotation3dProto(); 570 571 /** Rotation3d struct for serialization. */ 572 public static final Rotation3dStruct struct = new Rotation3dStruct(); 573}