1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.server.uwb.correction.math; 17 18 import static com.android.server.uwb.correction.math.MathHelper.F_HALF_PI; 19 import static com.android.server.uwb.correction.math.MathHelper.F_PI; 20 21 import static java.lang.Math.acos; 22 import static java.lang.Math.asin; 23 import static java.lang.Math.atan2; 24 import static java.lang.Math.cos; 25 import static java.lang.Math.sin; 26 import static java.lang.Math.sqrt; 27 28 import androidx.annotation.NonNull; 29 30 import com.android.internal.annotations.Immutable; 31 32 import java.security.InvalidParameterException; 33 import java.util.Locale; 34 35 /** 36 * Represents an orientation in 3D space. 37 * 38 * This uses OpenGL's right-handed coordinate system, where the origin is facing in the 39 * -Z direction. Angle operations such as {@link Quaternion#yawPitchRoll(float, float, float)} 40 * assume these operations relative to a quaternion facing in the -Z direction. 41 * 42 * +Y 43 * | -Z 44 * | / 45 * -X --------- +X 46 * / | 47 * +Z | 48 * -Y 49 * 50 * Yaw, pitch and roll direction can be determined by "grabbing" the axis you're rotating with 51 * your right hand, orienting your thumb to point in the positive direction. Your fingers' curl 52 * direction indicates the rotation created by positive numbers. 53 */ 54 @SuppressWarnings("UnaryPlus") 55 @Immutable 56 public final class Quaternion { 57 public static final Quaternion IDENTITY = new Quaternion(0, 0, 0, 1); 58 private static final float EULER_THRESHOLD = 0.49999994f; 59 private static final float COS_THRESHOLD = 0.9995f; 60 61 public final float x; 62 public final float y; 63 public final float z; 64 public final float w; 65 Quaternion(@onNull float[] v)66 public Quaternion(@NonNull float[] v) { 67 if (v.length != 4) { 68 throw new InvalidParameterException("Array must have 4 elements."); 69 } 70 float x = v[0], y = v[1], z = v[2], w = v[3]; 71 72 float norm = x * x + y * y + z * z + w * w; 73 74 this.x = x * norm; 75 this.y = y * norm; 76 this.z = z * norm; 77 this.w = w * norm; 78 } 79 Quaternion(float x, float y, float z, float w)80 public Quaternion(float x, float y, float z, float w) { 81 float norm = x * x + y * y + z * z + w * w; 82 83 this.x = x * norm; 84 this.y = y * norm; 85 this.z = z * norm; 86 this.w = w * norm; 87 } 88 89 /** Get a new Quaternion using an axis/angle to define the rotation. */ 90 @NonNull axisAngle(@onNull Vector3 axis, float radians)91 public static Quaternion axisAngle(@NonNull Vector3 axis, float radians) { 92 float angle = 0.5f * radians; 93 float sin = (float) sin(angle); 94 float cos = (float) cos(angle); 95 Vector3 a = axis.normalized(); 96 return new Quaternion(sin * a.x, sin * a.y, sin * a.z, cos); 97 } 98 99 /** 100 * Get a new Quaternion using euler angles, applied in YXZ (yaw, pitch, roll) order, to define 101 * the rotation. 102 * <p> 103 * This is consistent with other graphics engines. Note, however, that Unity uses ZXY order 104 * so the same angles used here will produce a different orientation than Unity. 105 * 106 * @param yaw The yaw in radians (rotation about the Y axis). 107 * @param pitch The pitch in radians (rotation about the X axis). 108 * @param roll The roll in radians (rotation about the Z axis). 109 */ 110 @NonNull yawPitchRoll(float yaw, float pitch, float roll)111 public static Quaternion yawPitchRoll(float yaw, float pitch, float roll) { 112 Quaternion qX = axisAngle(new Vector3(0, 1, 0), yaw); 113 Quaternion qY = axisAngle(new Vector3(1, 0, 0), pitch); 114 Quaternion qZ = axisAngle(new Vector3(0, 0, 1), roll); 115 return multiply(multiply(qX, qY), qZ); 116 // return multiply(multiply(qY, qX), qZ); 117 } 118 119 /** Creates a quaternion from the supplied matrix. */ 120 @NonNull fromMatrix(@onNull Matrix matrix)121 public static Quaternion fromMatrix(@NonNull Matrix matrix) { 122 float x, y, z, w; 123 // Use the Graphics Gems code, from 124 // ftp://ftp.cis.upenn.edu/pub/graphics/shoemake/quatut.ps.Z 125 // (also available at http://campar.in.tum.de/twiki/pub/Chair/DwarfTutorial/quatut.pdf) 126 // *NOT* the "Matrix and Quaternions FAQ", which has errors! 127 128 // the trace is the sum of the diagonal elements; see 129 // http://mathworld.wolfram.com/MatrixTrace.html 130 float trace = matrix.data[0] + matrix.data[5] + matrix.data[10]; 131 132 // we protect the division by traceRoot by ensuring that traceRoot>=1 133 if (trace >= 0) { // |w| >= .5 134 float traceRoot = (float) sqrt(trace + 1); // |traceRoot|>=1 ... 135 w = 0.5f * traceRoot; 136 traceRoot = 0.5f / traceRoot; // so this division isn't bad 137 x = (matrix.data[6] - matrix.data[9]) * traceRoot; 138 y = (matrix.data[8] - matrix.data[2]) * traceRoot; 139 z = (matrix.data[1] - matrix.data[4]) * traceRoot; 140 } else if ((matrix.data[0] > matrix.data[5]) && (matrix.data[0] > matrix.data[10])) { 141 // |traceRoot|>=1 142 float traceRoot = 143 (float) 144 sqrt(1.0f + matrix.data[0] - matrix.data[5] - matrix.data[10]); 145 x = traceRoot * 0.5f; // |x| >= .5 146 traceRoot = 0.5f / traceRoot; 147 y = (matrix.data[1] + matrix.data[4]) * traceRoot; 148 z = (matrix.data[8] + matrix.data[2]) * traceRoot; 149 w = (matrix.data[6] - matrix.data[9]) * traceRoot; 150 } else if (matrix.data[5] > matrix.data[10]) { 151 // |traceRoot|>=1 152 float traceRoot = 153 (float) sqrt(1.0f + matrix.data[5] - matrix.data[0] - matrix.data[10]); 154 y = traceRoot * 0.5f; // |y| >= .5 155 traceRoot = 0.5f / traceRoot; 156 x = (matrix.data[1] + matrix.data[4]) * traceRoot; 157 z = (matrix.data[6] + matrix.data[9]) * traceRoot; 158 w = (matrix.data[8] - matrix.data[2]) * traceRoot; 159 } else { 160 // |traceRoot|>=1 161 float traceRoot = 162 (float) sqrt(1.0f + matrix.data[10] - matrix.data[0] - matrix.data[5]); 163 z = traceRoot * 0.5f; // |z| >= .5 164 traceRoot = 0.5f / traceRoot; 165 x = (matrix.data[8] + matrix.data[2]) * traceRoot; 166 y = (matrix.data[6] + matrix.data[9]) * traceRoot; 167 w = (matrix.data[1] - matrix.data[4]) * traceRoot; 168 } 169 170 return new Quaternion(x, y, z, w); 171 } 172 173 /** 174 * Creates a Quaternion that represents the coordinate system defined by three axes. These axes 175 * are assumed to be orthogonal and no error checking is applied. Thus, the user must insure 176 * that the three axes being provided indeed represents a proper right handed coordinate system. 177 */ 178 @NonNull fromAxes( @onNull Vector3 xAxis, @NonNull Vector3 yAxis, @NonNull Vector3 zAxis )179 public static Quaternion fromAxes( 180 @NonNull Vector3 xAxis, 181 @NonNull Vector3 yAxis, 182 @NonNull Vector3 zAxis 183 ) { 184 Vector3 xAxisNormalized = xAxis.normalized(); 185 Vector3 yAxisNormalized = yAxis.normalized(); 186 Vector3 zAxisNormalized = zAxis.normalized(); 187 188 float[] matrix = 189 new float[] { 190 xAxisNormalized.x, 191 xAxisNormalized.y, 192 xAxisNormalized.z, 193 0.0f, 194 yAxisNormalized.x, 195 yAxisNormalized.y, 196 yAxisNormalized.z, 197 0.0f, 198 zAxisNormalized.x, 199 zAxisNormalized.y, 200 zAxisNormalized.z, 201 0.0f, 202 0.0f, 203 0.0f, 204 0.0f, 205 1.0f 206 }; 207 208 return fromMatrix(new Matrix(matrix)); 209 } 210 211 /** Get a Quaternion with the opposite rotation. */ 212 @NonNull inverted()213 public Quaternion inverted() { 214 return new Quaternion(-x, -y, -z, w); 215 } 216 217 /** Flips the sign of the Quaternion, but represents the same rotation. */ negated()218 public Quaternion negated() { 219 return new Quaternion(-x, -y, -z, -w); 220 } 221 222 @NonNull 223 @Override toString()224 public String toString() { 225 return String.format(Locale.getDefault(), "ℍ[% 4.2f,% 4.2f,% 4.2f,% 2.1f]", x, y, z, w); 226 } 227 228 /** Rotates a Vector3 by this Quaternion. */ 229 @NonNull rotateVector(@onNull Vector3 src)230 public Vector3 rotateVector(@NonNull Vector3 src) { 231 // This implements the GLM algorithm which is optimal (15 multiplies and 15 add/subtracts): 232 // google3/third_party/glm/latest/glm/detail/type_quat.inl?l=343&rcl=333309501 233 float vx = src.x; 234 float vy = src.y; 235 float vz = src.z; 236 237 float rx = y * vz - z * vy + w * vx; 238 float ry = z * vx - x * vz + w * vy; 239 float rz = x * vy - y * vx + w * vz; 240 float sx = y * rz - z * ry; 241 float sy = z * rx - x * rz; 242 float sz = x * ry - y * rx; 243 return new Vector3(2 * sx + vx, 2 * sy + vy, 2 * sz + vz); 244 } 245 246 /** 247 * Create a Quaternion by combining two Quaternions multiply(lhs, rhs) is equivalent to 248 * performing the rhs rotation then lhs rotation. Ordering is important for this operation. 249 */ 250 @NonNull multiply(@onNull Quaternion lhs, @NonNull Quaternion rhs)251 public static Quaternion multiply(@NonNull Quaternion lhs, @NonNull Quaternion rhs) { 252 float lx = lhs.x; 253 float ly = lhs.y; 254 float lz = lhs.z; 255 float lw = lhs.w; 256 float rx = rhs.x; 257 float ry = rhs.y; 258 float rz = rhs.z; 259 float rw = rhs.w; 260 261 return new Quaternion( 262 lw * rx + lx * rw + ly * rz - lz * ry, 263 lw * ry - lx * rz + ly * rw + lz * rx, 264 lw * rz + lx * ry - ly * rx + lz * rw, 265 lw * rw - lx * rx - ly * ry - lz * rz); 266 } 267 268 /** 269 * Calculates the dot product of two quaternions. The dot product of two normalized quaternions 270 * is 1 if they represent the same orientation. 271 */ dot(@onNull Quaternion lhs, @NonNull Quaternion rhs)272 public static float dot(@NonNull Quaternion lhs, @NonNull Quaternion rhs) { 273 return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z + lhs.w * rhs.w; 274 } 275 276 @NonNull lerp(@onNull Quaternion q, float t)277 Quaternion lerp(@NonNull Quaternion q, float t) { 278 return new Quaternion( 279 MathHelper.lerp(x, q.x, t), 280 MathHelper.lerp(y, q.y, t), 281 MathHelper.lerp(z, q.z, t), 282 MathHelper.lerp(w, q.w, t) 283 ); 284 } 285 286 /** 287 * Returns the spherical linear interpolation between two given orientations. 288 * 289 * <p>If t is 0 this returns start. As t approaches 1 slerp may approach either +end or -end 290 * (whichever is closest to start). If t is above 1 or below 0 the result will be extrapolated. 291 */ 292 @NonNull slerp(@onNull Quaternion start, @NonNull Quaternion end, float t)293 public static Quaternion slerp(@NonNull Quaternion start, @NonNull Quaternion end, float t) { 294 Quaternion orientation1 = end; 295 296 // cosTheta0 provides the angle between the rotations at t=0 297 float cosTheta0 = dot(start, orientation1); 298 299 // Flip end rotation to get shortest path if needed 300 if (cosTheta0 < 0.0f) { 301 orientation1 = orientation1.negated(); 302 cosTheta0 = -cosTheta0; 303 } 304 305 // Small rotations should just use lerp 306 if (cosTheta0 > COS_THRESHOLD) { 307 return start.lerp(orientation1, t); 308 } 309 310 double sinTheta0 = sqrt(1.0 - cosTheta0 * cosTheta0); 311 double theta0 = acos(cosTheta0); 312 double thetaT = theta0 * t; 313 double sinThetaT = sin(thetaT); 314 double cosThetaT = cos(thetaT); 315 316 float s1 = (float) (sinThetaT / sinTheta0); 317 float s0 = (float) (cosThetaT - cosTheta0 * s1); 318 319 return new Quaternion( 320 start.x * s0 * s1 + orientation1.x, 321 start.y * s0 * s1 + orientation1.y, 322 start.z * s0 * s1 + orientation1.z, 323 start.w * s0 * s1 + orientation1.w 324 ); 325 } 326 327 /** 328 * Subtracts one quaternion from the other, describing the rotation between two rotations. 329 * 330 * @param lhs The left-hand quaternion. 331 * @param rhs The right-hand quaternion. 332 * @return A quaternion describing the difference. 333 */ 334 @NonNull difference(@onNull Quaternion lhs, @NonNull Quaternion rhs)335 public static Quaternion difference(@NonNull Quaternion lhs, @NonNull Quaternion rhs) { 336 return multiply(lhs.inverted(), rhs); 337 } 338 339 /** Get a new Quaternion representing the rotation from one vector to another. */ 340 @NonNull rotationBetweenVectors(@onNull Vector3 start, @NonNull Vector3 end)341 public static Quaternion rotationBetweenVectors(@NonNull Vector3 start, @NonNull Vector3 end) { 342 start = start.normalized(); 343 end = end.normalized(); 344 345 float cosTheta = Vector3.dot(start, end); 346 if (cosTheta < -COS_THRESHOLD) { 347 // Special case when vectors in opposite directions: there is no "ideal" rotation axis. 348 // So guess one; any will do as long as it's perpendicular to start. 349 Vector3 rotationAxis = Vector3.cross(new Vector3(0, 0, 1), start); 350 if (rotationAxis.lengthSquared() < 0.01f) { // bad luck, they were parallel, try again! 351 rotationAxis = Vector3.cross(new Vector3(1, 0, 0), start); 352 } 353 354 return axisAngle(rotationAxis, F_PI); 355 } 356 357 Vector3 rotationAxis = Vector3.cross(start, end); 358 return new Quaternion(rotationAxis.x, rotationAxis.y, rotationAxis.z, 1.0f + cosTheta); 359 } 360 361 /** 362 * Get a new Quaternion representing the rotation created by orthogonal forward and up vectors. 363 */ 364 @NonNull lookRotation(@onNull Vector3 forward, @NonNull Vector3 up)365 public static Quaternion lookRotation(@NonNull Vector3 forward, @NonNull Vector3 up) { 366 Vector3 vector = forward.normalized(); 367 Vector3 vector2 = Vector3.cross(up, vector).normalized(); 368 Vector3 vector3 = Vector3.cross(vector, vector2); 369 float m00 = vector2.x; 370 float m01 = vector2.y; 371 float m02 = vector2.z; 372 float m10 = vector3.x; 373 float m11 = vector3.y; 374 float m12 = vector3.z; 375 float m20 = vector.x; 376 float m21 = vector.y; 377 float m22 = vector.z; 378 379 float num8 = (m00 + m11) + m22; 380 float x, y, z, w; 381 if (num8 > 0f) { 382 float num = (float) sqrt(num8 + 1f); 383 w = num * 0.5f; 384 num = 0.5f / num; 385 x = (m12 - m21) * num; 386 y = (m20 - m02) * num; 387 z = (m01 - m10) * num; 388 return new Quaternion(x, y, z, w); 389 } else if ((m00 >= m11) && (m00 >= m22)) { 390 float num7 = (float) sqrt(((1f + m00) - m11) - m22); 391 float num4 = 0.5f / num7; 392 x = 0.5f * num7; 393 y = (m01 + m10) * num4; 394 z = (m02 + m20) * num4; 395 w = (m12 - m21) * num4; 396 return new Quaternion(x, y, z, w); 397 } else if (m11 > m22) { 398 float num6 = (float) sqrt(((1f + m11) - m00) - m22); 399 float num3 = 0.5f / num6; 400 x = (m10 + m01) * num3; 401 y = 0.5f * num6; 402 z = (m21 + m12) * num3; 403 w = (m20 - m02) * num3; 404 } else { 405 float num5 = (float) sqrt(((1f + m22) - m00) - m11); 406 float num2 = 0.5f / num5; 407 x = (m20 + m02) * num2; 408 y = (m21 + m12) * num2; 409 z = 0.5f * num5; 410 w = (m01 - m10) * num2; 411 } 412 return new Quaternion(x, y, z, w); 413 } 414 415 /** 416 * Get a Vector3 containing the pitch, yaw and roll in degrees, extracted in YXZ (yaw, pitch, 417 * roll) order. Note that this assumes that zero yaw, pitch or roll is a direction 418 * facing into -Z. 419 * 420 * <p>See: {@link #yawPitchRoll}. 421 */ 422 @NonNull toYawPitchRoll()423 public Vector3 toYawPitchRoll() { 424 float test = w * x - y * z; 425 if (test > +EULER_THRESHOLD) { 426 // There is a singularity when the pitch is directly up, so calculate the 427 // angles another way. 428 return new Vector3((float) (+2 * atan2(z, w)), +F_HALF_PI, 0); 429 } 430 if (test < -EULER_THRESHOLD) { 431 // There is a singularity when the pitch is directly down, so calculate the 432 // angles another way. 433 return new Vector3((float) (-2 * atan2(z, w)), -F_HALF_PI, 0); 434 } 435 double pitch = asin(2 * test); 436 double yaw = atan2(2 * (w * y + x * z), 1.0 - 2 * (x * x + y * y)); 437 double roll = atan2(2 * (w * z + x * y), 1.0 - 2 * (x * x + z * z)); 438 return new Vector3( 439 (float) yaw, 440 (float) pitch, 441 (float) roll 442 ); 443 } 444 } 445