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