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}