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 com.fasterxml.jackson.annotation.JsonAutoDetect;
008import com.fasterxml.jackson.annotation.JsonCreator;
009import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
010import com.fasterxml.jackson.annotation.JsonProperty;
011import edu.wpi.first.math.MathSharedStore;
012import edu.wpi.first.math.MathUtil;
013import edu.wpi.first.math.Matrix;
014import edu.wpi.first.math.Nat;
015import edu.wpi.first.math.VecBuilder;
016import edu.wpi.first.math.Vector;
017import edu.wpi.first.math.geometry.proto.Rotation3dProto;
018import edu.wpi.first.math.geometry.struct.Rotation3dStruct;
019import edu.wpi.first.math.interpolation.Interpolatable;
020import edu.wpi.first.math.numbers.N3;
021import edu.wpi.first.util.protobuf.ProtobufSerializable;
022import edu.wpi.first.util.struct.StructSerializable;
023import java.util.Objects;
024import org.ejml.dense.row.factory.DecompositionFactory_DDRM;
025
026/** A rotation in a 3D coordinate frame represented by a quaternion. */
027@JsonIgnoreProperties(ignoreUnknown = true)
028@JsonAutoDetect(getterVisibility = JsonAutoDetect.Visibility.NONE)
029public class Rotation3d
030    implements Interpolatable<Rotation3d>, ProtobufSerializable, StructSerializable {
031  private final Quaternion m_q;
032
033  /** Constructs a Rotation3d with a default angle of 0 degrees. */
034  public Rotation3d() {
035    m_q = new Quaternion();
036  }
037
038  /**
039   * Constructs a Rotation3d from a quaternion.
040   *
041   * @param q The quaternion.
042   */
043  @JsonCreator
044  public Rotation3d(@JsonProperty(required = true, value = "quaternion") Quaternion q) {
045    m_q = q.normalize();
046  }
047
048  /**
049   * Constructs a Rotation3d from extrinsic roll, pitch, and yaw.
050   *
051   * <p>Extrinsic rotations occur in that order around the axes in the fixed global frame rather
052   * than the body frame.
053   *
054   * <p>Angles are measured counterclockwise with the rotation axis pointing "out of the page". If
055   * you point your right thumb along the positive axis direction, your fingers curl in the
056   * direction of positive rotation.
057   *
058   * @param roll The counterclockwise rotation angle around the X axis (roll) in radians.
059   * @param pitch The counterclockwise rotation angle around the Y axis (pitch) in radians.
060   * @param yaw The counterclockwise rotation angle around the Z axis (yaw) in radians.
061   */
062  public Rotation3d(double roll, double pitch, double yaw) {
063    // https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles#Euler_angles_to_quaternion_conversion
064    double cr = Math.cos(roll * 0.5);
065    double sr = Math.sin(roll * 0.5);
066
067    double cp = Math.cos(pitch * 0.5);
068    double sp = Math.sin(pitch * 0.5);
069
070    double cy = Math.cos(yaw * 0.5);
071    double sy = Math.sin(yaw * 0.5);
072
073    m_q =
074        new Quaternion(
075            cr * cp * cy + sr * sp * sy,
076            sr * cp * cy - cr * sp * sy,
077            cr * sp * cy + sr * cp * sy,
078            cr * cp * sy - sr * sp * cy);
079  }
080
081  /**
082   * Constructs a Rotation3d with the given rotation vector representation. This representation is
083   * equivalent to axis-angle, where the normalized axis is multiplied by the rotation around the
084   * axis in radians.
085   *
086   * @param rvec The rotation vector.
087   */
088  public Rotation3d(Vector<N3> rvec) {
089    this(rvec, rvec.norm());
090  }
091
092  /**
093   * Constructs a Rotation3d with the given axis-angle representation. The axis doesn't have to be
094   * normalized.
095   *
096   * @param axis The rotation axis.
097   * @param angleRadians The rotation around the axis in radians.
098   */
099  public Rotation3d(Vector<N3> axis, double angleRadians) {
100    double norm = axis.norm();
101    if (norm == 0.0) {
102      m_q = new Quaternion();
103      return;
104    }
105
106    // https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles#Definition
107    var v = axis.times(1.0 / norm).times(Math.sin(angleRadians / 2.0));
108    m_q = new Quaternion(Math.cos(angleRadians / 2.0), v.get(0, 0), v.get(1, 0), v.get(2, 0));
109  }
110
111  /**
112   * Constructs a Rotation3d from a rotation matrix.
113   *
114   * @param rotationMatrix The rotation matrix.
115   * @throws IllegalArgumentException if the rotation matrix isn't special orthogonal.
116   */
117  public Rotation3d(Matrix<N3, N3> rotationMatrix) {
118    final var R = rotationMatrix;
119
120    // Require that the rotation matrix is special orthogonal. This is true if
121    // the matrix is orthogonal (RRᵀ = I) and normalized (determinant is 1).
122    if (R.times(R.transpose()).minus(Matrix.eye(Nat.N3())).normF() > 1e-9) {
123      var msg = "Rotation matrix isn't orthogonal\n\nR =\n" + R.getStorage().toString() + '\n';
124      MathSharedStore.reportError(msg, Thread.currentThread().getStackTrace());
125      throw new IllegalArgumentException(msg);
126    }
127    if (Math.abs(R.det() - 1.0) > 1e-9) {
128      var msg =
129          "Rotation matrix is orthogonal but not special orthogonal\n\nR =\n"
130              + R.getStorage().toString()
131              + '\n';
132      MathSharedStore.reportError(msg, Thread.currentThread().getStackTrace());
133      throw new IllegalArgumentException(msg);
134    }
135
136    // Turn rotation matrix into a quaternion
137    // https://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToQuaternion/
138    double trace = R.get(0, 0) + R.get(1, 1) + R.get(2, 2);
139    double w;
140    double x;
141    double y;
142    double z;
143
144    if (trace > 0.0) {
145      double s = 0.5 / Math.sqrt(trace + 1.0);
146      w = 0.25 / s;
147      x = (R.get(2, 1) - R.get(1, 2)) * s;
148      y = (R.get(0, 2) - R.get(2, 0)) * s;
149      z = (R.get(1, 0) - R.get(0, 1)) * s;
150    } else {
151      if (R.get(0, 0) > R.get(1, 1) && R.get(0, 0) > R.get(2, 2)) {
152        double s = 2.0 * Math.sqrt(1.0 + R.get(0, 0) - R.get(1, 1) - R.get(2, 2));
153        w = (R.get(2, 1) - R.get(1, 2)) / s;
154        x = 0.25 * s;
155        y = (R.get(0, 1) + R.get(1, 0)) / s;
156        z = (R.get(0, 2) + R.get(2, 0)) / s;
157      } else if (R.get(1, 1) > R.get(2, 2)) {
158        double s = 2.0 * Math.sqrt(1.0 + R.get(1, 1) - R.get(0, 0) - R.get(2, 2));
159        w = (R.get(0, 2) - R.get(2, 0)) / s;
160        x = (R.get(0, 1) + R.get(1, 0)) / s;
161        y = 0.25 * s;
162        z = (R.get(1, 2) + R.get(2, 1)) / s;
163      } else {
164        double s = 2.0 * Math.sqrt(1.0 + R.get(2, 2) - R.get(0, 0) - R.get(1, 1));
165        w = (R.get(1, 0) - R.get(0, 1)) / s;
166        x = (R.get(0, 2) + R.get(2, 0)) / s;
167        y = (R.get(1, 2) + R.get(2, 1)) / s;
168        z = 0.25 * s;
169      }
170    }
171
172    m_q = new Quaternion(w, x, y, z);
173  }
174
175  /**
176   * Constructs a Rotation3d that rotates the initial vector onto the final vector.
177   *
178   * <p>This is useful for turning a 3D vector (final) into an orientation relative to a coordinate
179   * system vector (initial).
180   *
181   * @param initial The initial vector.
182   * @param last The final vector.
183   */
184  public Rotation3d(Vector<N3> initial, Vector<N3> last) {
185    double dot = initial.dot(last);
186    double normProduct = initial.norm() * last.norm();
187    double dotNorm = dot / normProduct;
188
189    if (dotNorm > 1.0 - 1E-9) {
190      // If the dot product is 1, the two vectors point in the same direction so
191      // there's no rotation. The default initialization of m_q will work.
192      m_q = new Quaternion();
193    } else if (dotNorm < -1.0 + 1E-9) {
194      // If the dot product is -1, the two vectors point in opposite directions
195      // so a 180 degree rotation is required. Any orthogonal vector can be used
196      // for it. Q in the QR decomposition is an orthonormal basis, so it
197      // contains orthogonal unit vectors.
198      var X = VecBuilder.fill(initial.get(0, 0), initial.get(1, 0), initial.get(2, 0));
199      final var qr = DecompositionFactory_DDRM.qr(3, 1);
200      qr.decompose(X.getStorage().getMatrix());
201      final var Q = qr.getQ(null, false);
202
203      // w = cos(θ/2) = cos(90°) = 0
204      //
205      // For x, y, and z, we use the second column of Q because the first is
206      // parallel instead of orthogonal. The third column would also work.
207      m_q = new Quaternion(0.0, Q.get(0, 1), Q.get(1, 1), Q.get(2, 1));
208    } else {
209      // initial x last
210      var axis =
211          VecBuilder.fill(
212              initial.get(1, 0) * last.get(2, 0) - last.get(1, 0) * initial.get(2, 0),
213              last.get(0, 0) * initial.get(2, 0) - initial.get(0, 0) * last.get(2, 0),
214              initial.get(0, 0) * last.get(1, 0) - last.get(0, 0) * initial.get(1, 0));
215
216      // https://stackoverflow.com/a/11741520
217      m_q =
218          new Quaternion(normProduct + dot, axis.get(0, 0), axis.get(1, 0), axis.get(2, 0))
219              .normalize();
220    }
221  }
222
223  /**
224   * Adds two rotations together.
225   *
226   * @param other The rotation to add.
227   * @return The sum of the two rotations.
228   */
229  public Rotation3d plus(Rotation3d other) {
230    return rotateBy(other);
231  }
232
233  /**
234   * Subtracts the new rotation from the current rotation and returns the new rotation.
235   *
236   * @param other The rotation to subtract.
237   * @return The difference between the two rotations.
238   */
239  public Rotation3d minus(Rotation3d other) {
240    return rotateBy(other.unaryMinus());
241  }
242
243  /**
244   * Takes the inverse of the current rotation.
245   *
246   * @return The inverse of the current rotation.
247   */
248  public Rotation3d unaryMinus() {
249    return new Rotation3d(m_q.inverse());
250  }
251
252  /**
253   * Multiplies the current rotation by a scalar.
254   *
255   * @param scalar The scalar.
256   * @return The new scaled Rotation3d.
257   */
258  public Rotation3d times(double scalar) {
259    // https://en.wikipedia.org/wiki/Slerp#Quaternion_Slerp
260    if (m_q.getW() >= 0.0) {
261      return new Rotation3d(
262          VecBuilder.fill(m_q.getX(), m_q.getY(), m_q.getZ()),
263          2.0 * scalar * Math.acos(m_q.getW()));
264    } else {
265      return new Rotation3d(
266          VecBuilder.fill(-m_q.getX(), -m_q.getY(), -m_q.getZ()),
267          2.0 * scalar * Math.acos(-m_q.getW()));
268    }
269  }
270
271  /**
272   * Divides the current rotation by a scalar.
273   *
274   * @param scalar The scalar.
275   * @return The new scaled Rotation3d.
276   */
277  public Rotation3d div(double scalar) {
278    return times(1.0 / scalar);
279  }
280
281  /**
282   * Adds the new rotation to the current rotation. The other rotation is applied extrinsically,
283   * which means that it rotates around the global axes. For example, {@code new
284   * Rotation3d(Units.degreesToRadians(90), 0, 0).rotateBy(new Rotation3d(0,
285   * Units.degreesToRadians(45), 0))} rotates by 90 degrees around the +X axis and then by 45
286   * degrees around the global +Y axis. (This is equivalent to {@code new
287   * Rotation3d(Units.degreesToRadians(90), Units.degreesToRadians(45), 0)})
288   *
289   * @param other The extrinsic rotation to rotate by.
290   * @return The new rotated Rotation3d.
291   */
292  public Rotation3d rotateBy(Rotation3d other) {
293    return new Rotation3d(other.m_q.times(m_q));
294  }
295
296  /**
297   * Returns the quaternion representation of the Rotation3d.
298   *
299   * @return The quaternion representation of the Rotation3d.
300   */
301  @JsonProperty(value = "quaternion")
302  public Quaternion getQuaternion() {
303    return m_q;
304  }
305
306  /**
307   * Returns the counterclockwise rotation angle around the X axis (roll) in radians.
308   *
309   * @return The counterclockwise rotation angle around the X axis (roll) in radians.
310   */
311  public double getX() {
312    final var w = m_q.getW();
313    final var x = m_q.getX();
314    final var y = m_q.getY();
315    final var z = m_q.getZ();
316
317    // wpimath/algorithms.md
318    final var cxcy = 1.0 - 2.0 * (x * x + y * y);
319    final var sxcy = 2.0 * (w * x + y * z);
320    final var cy_sq = cxcy * cxcy + sxcy * sxcy;
321    if (cy_sq > 1e-20) {
322      return Math.atan2(sxcy, cxcy);
323    } else {
324      return 0.0;
325    }
326  }
327
328  /**
329   * Returns the counterclockwise rotation angle around the Y axis (pitch) in radians.
330   *
331   * @return The counterclockwise rotation angle around the Y axis (pitch) in radians.
332   */
333  public double getY() {
334    final var w = m_q.getW();
335    final var x = m_q.getX();
336    final var y = m_q.getY();
337    final var z = m_q.getZ();
338
339    // https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles#Quaternion_to_Euler_angles_(in_3-2-1_sequence)_conversion
340    double ratio = 2.0 * (w * y - z * x);
341    if (Math.abs(ratio) >= 1.0) {
342      return Math.copySign(Math.PI / 2.0, ratio);
343    } else {
344      return Math.asin(ratio);
345    }
346  }
347
348  /**
349   * Returns the counterclockwise rotation angle around the Z axis (yaw) in radians.
350   *
351   * @return The counterclockwise rotation angle around the Z axis (yaw) in radians.
352   */
353  public double getZ() {
354    final var w = m_q.getW();
355    final var x = m_q.getX();
356    final var y = m_q.getY();
357    final var z = m_q.getZ();
358
359    // wpimath/algorithms.md
360    final var cycz = 1.0 - 2.0 * (y * y + z * z);
361    final var cysz = 2.0 * (w * z + x * y);
362    final var cy_sq = cycz * cycz + cysz * cysz;
363    if (cy_sq > 1e-20) {
364      return Math.atan2(cysz, cycz);
365    } else {
366      return Math.atan2(2.0 * w * z, w * w - z * z);
367    }
368  }
369
370  /**
371   * Returns the axis in the axis-angle representation of this rotation.
372   *
373   * @return The axis in the axis-angle representation.
374   */
375  public Vector<N3> getAxis() {
376    double norm =
377        Math.sqrt(m_q.getX() * m_q.getX() + m_q.getY() * m_q.getY() + m_q.getZ() * m_q.getZ());
378    if (norm == 0.0) {
379      return VecBuilder.fill(0.0, 0.0, 0.0);
380    } else {
381      return VecBuilder.fill(m_q.getX() / norm, m_q.getY() / norm, m_q.getZ() / norm);
382    }
383  }
384
385  /**
386   * Returns the angle in radians in the axis-angle representation of this rotation.
387   *
388   * @return The angle in radians in the axis-angle representation of this rotation.
389   */
390  public double getAngle() {
391    double norm =
392        Math.sqrt(m_q.getX() * m_q.getX() + m_q.getY() * m_q.getY() + m_q.getZ() * m_q.getZ());
393    return 2.0 * Math.atan2(norm, m_q.getW());
394  }
395
396  /**
397   * Returns a Rotation2d representing this Rotation3d projected into the X-Y plane.
398   *
399   * @return A Rotation2d representing this Rotation3d projected into the X-Y plane.
400   */
401  public Rotation2d toRotation2d() {
402    return new Rotation2d(getZ());
403  }
404
405  @Override
406  public String toString() {
407    return String.format("Rotation3d(%s)", m_q);
408  }
409
410  /**
411   * Checks equality between this Rotation3d and another object.
412   *
413   * @param obj The other object.
414   * @return Whether the two objects are equal or not.
415   */
416  @Override
417  public boolean equals(Object obj) {
418    if (obj instanceof Rotation3d) {
419      var other = (Rotation3d) obj;
420      return Math.abs(Math.abs(m_q.dot(other.m_q)) - m_q.norm() * other.m_q.norm()) < 1e-9;
421    }
422    return false;
423  }
424
425  @Override
426  public int hashCode() {
427    return Objects.hash(m_q);
428  }
429
430  @Override
431  public Rotation3d interpolate(Rotation3d endValue, double t) {
432    return plus(endValue.minus(this).times(MathUtil.clamp(t, 0, 1)));
433  }
434
435  /** Rotation3d protobuf for serialization. */
436  public static final Rotation3dProto proto = new Rotation3dProto();
437
438  /** Rotation3d struct for serialization. */
439  public static final Rotation3dStruct struct = new Rotation3dStruct();
440}