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}