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