|
@@ -0,0 +1,330 @@
|
|
1
|
+/*
|
|
2
|
+ * encantar.js
|
|
3
|
+ * GPU-accelerated Augmented Reality for the web
|
|
4
|
+ * Copyright (C) 2022-2024 Alexandre Martins <alemartf(at)gmail.com>
|
|
5
|
+ *
|
|
6
|
+ * This program is free software: you can redistribute it and/or modify
|
|
7
|
+ * it under the terms of the GNU Lesser General Public License as published
|
|
8
|
+ * by the Free Software Foundation, either version 3 of the License, or
|
|
9
|
+ * (at your option) any later version.
|
|
10
|
+ *
|
|
11
|
+ * This program is distributed in the hope that it will be useful,
|
|
12
|
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+ * GNU Lesser General Public License for more details.
|
|
15
|
+ *
|
|
16
|
+ * You should have received a copy of the GNU Lesser General Public License
|
|
17
|
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
18
|
+ *
|
|
19
|
+ * quaternion.ts
|
|
20
|
+ * Quaternions
|
|
21
|
+ */
|
|
22
|
+
|
|
23
|
+import Speedy from 'speedy-vision';
|
|
24
|
+import { SpeedyMatrix } from 'speedy-vision/types/core/speedy-matrix';
|
|
25
|
+import { IllegalArgumentError } from '../utils/errors';
|
|
26
|
+
|
|
27
|
+/** Small number */
|
|
28
|
+const EPSILON = 1e-6;
|
|
29
|
+
|
|
30
|
+// public / non-internal methods do not change the contents of the quaternion
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+/**
|
|
35
|
+ * Quaternion q = x i + y j + z k + w
|
|
36
|
+ */
|
|
37
|
+export class Quaternion
|
|
38
|
+{
|
|
39
|
+ /** x coordinate (imaginary) */
|
|
40
|
+ public x: number;
|
|
41
|
+
|
|
42
|
+ /** y coordinate (imaginary) */
|
|
43
|
+ public y: number;
|
|
44
|
+
|
|
45
|
+ /** z coordinate (imaginary) */
|
|
46
|
+ public z: number;
|
|
47
|
+
|
|
48
|
+ /** w coordinate (real) */
|
|
49
|
+ public w: number;
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+ /**
|
|
54
|
+ * Constructor
|
|
55
|
+ * @param x x coordinate (imaginary)
|
|
56
|
+ * @param y y coordinate (imaginary)
|
|
57
|
+ * @param z z coordinate (imaginary)
|
|
58
|
+ * @param w w coordinate (real)
|
|
59
|
+ */
|
|
60
|
+ constructor(x: number = 0, y: number = 0, z: number = 0, w: number = 1)
|
|
61
|
+ {
|
|
62
|
+ this.x = +x;
|
|
63
|
+ this.y = +y;
|
|
64
|
+ this.z = +z;
|
|
65
|
+ this.w = +w;
|
|
66
|
+ }
|
|
67
|
+
|
|
68
|
+ /**
|
|
69
|
+ * Instantiate an identity quaternion q = 1
|
|
70
|
+ * @returns a new identity quaternion
|
|
71
|
+ */
|
|
72
|
+ static Identity(): Quaternion
|
|
73
|
+ {
|
|
74
|
+ return new Quaternion(0, 0, 0, 1);
|
|
75
|
+ }
|
|
76
|
+
|
|
77
|
+ /**
|
|
78
|
+ * The length of this quaternion
|
|
79
|
+ * @returns sqrt(x^2 + y^2 + z^2 + w^2)
|
|
80
|
+ */
|
|
81
|
+ length(): number
|
|
82
|
+ {
|
|
83
|
+ const x = this.x;
|
|
84
|
+ const y = this.y;
|
|
85
|
+ const z = this.z;
|
|
86
|
+ const w = this.w;
|
|
87
|
+
|
|
88
|
+ return Math.sqrt(x*x + y*y + z*z + w*w);
|
|
89
|
+ }
|
|
90
|
+
|
|
91
|
+ /**
|
|
92
|
+ * Check if this and q have the same coordinates
|
|
93
|
+ * @param q a quaternion
|
|
94
|
+ * @returns true if this and q have the same coordinates
|
|
95
|
+ */
|
|
96
|
+ equals(q: Quaternion): boolean
|
|
97
|
+ {
|
|
98
|
+ return this.w === q.w && this.x === q.x && this.y === q.y && this.z === q.z;
|
|
99
|
+ }
|
|
100
|
+
|
|
101
|
+ /**
|
|
102
|
+ * Convert to string
|
|
103
|
+ * @returns a string
|
|
104
|
+ */
|
|
105
|
+ toString(): string
|
|
106
|
+ {
|
|
107
|
+ const x = this.x.toFixed(4);
|
|
108
|
+ const y = this.y.toFixed(4);
|
|
109
|
+ const z = this.z.toFixed(4);
|
|
110
|
+ const w = this.w.toFixed(4);
|
|
111
|
+
|
|
112
|
+ return `Quaternion(${x},${y},${z},${w})`;
|
|
113
|
+ }
|
|
114
|
+
|
|
115
|
+ /**
|
|
116
|
+ * Normalize this quaternion
|
|
117
|
+ * @returns this quaternion, normalized
|
|
118
|
+ * @internal
|
|
119
|
+ */
|
|
120
|
+ _normalize(): Quaternion
|
|
121
|
+ {
|
|
122
|
+ const length = this.length();
|
|
123
|
+
|
|
124
|
+ if(length < EPSILON) // zero?
|
|
125
|
+ return this;
|
|
126
|
+
|
|
127
|
+ this.x /= length;
|
|
128
|
+ this.y /= length;
|
|
129
|
+ this.z /= length;
|
|
130
|
+ this.w /= length;
|
|
131
|
+
|
|
132
|
+ return this;
|
|
133
|
+ }
|
|
134
|
+
|
|
135
|
+ /**
|
|
136
|
+ * Copy q to this
|
|
137
|
+ * @param q a quaternion
|
|
138
|
+ * @returns this quaternion
|
|
139
|
+ * @internal
|
|
140
|
+ */
|
|
141
|
+ _copyFrom(q: Quaternion): Quaternion
|
|
142
|
+ {
|
|
143
|
+ this.x = q.x;
|
|
144
|
+ this.y = q.y;
|
|
145
|
+ this.z = q.z;
|
|
146
|
+ this.w = q.w;
|
|
147
|
+
|
|
148
|
+ return this;
|
|
149
|
+ }
|
|
150
|
+
|
|
151
|
+ /**
|
|
152
|
+ * Convert a quaternion to a 3x3 rotation matrix
|
|
153
|
+ * @returns a 3x3 rotation matrix
|
|
154
|
+ * @internal
|
|
155
|
+ */
|
|
156
|
+ _toRotationMatrix(): SpeedyMatrix
|
|
157
|
+ {
|
|
158
|
+ const length = this.length(); // should be ~ 1
|
|
159
|
+
|
|
160
|
+ // sanity check
|
|
161
|
+ if(length < EPSILON)
|
|
162
|
+ return Speedy.Matrix.Eye(3);
|
|
163
|
+
|
|
164
|
+ // let q = (x,y,z,w) be a unit quaternion
|
|
165
|
+ const x = this.x / length;
|
|
166
|
+ const y = this.y / length;
|
|
167
|
+ const z = this.z / length;
|
|
168
|
+ const w = this.w / length;
|
|
169
|
+
|
|
170
|
+ /*
|
|
171
|
+
|
|
172
|
+ Let q = x i + y j + z k + w be a unit quaternion and
|
|
173
|
+ p = x_p i + y_p j + z_p k be a purely imaginary quaternion (w_p = 0)
|
|
174
|
+ representing a vector or point P = (x_p, y_p, z_p) in 3D space.
|
|
175
|
+
|
|
176
|
+ Let's rewrite q as q = v + w, where v = x i + y j + z k, and then
|
|
177
|
+ substitute v by the unit vector u = v / |v|, so that q = |v| u + w.
|
|
178
|
+ Since q is a unit quaternion, it follows that:
|
|
179
|
+
|
|
180
|
+ 1 = |q|^2 = x^2 + y^2 + z^2 + w^2 = |v|^2 + w^2.
|
|
181
|
+
|
|
182
|
+ Given that cos(t~)^2 + sin(t~)^2 = 1 for all real t~, there is a real t
|
|
183
|
+ such that cos(t) = w and sin(t) = |v|. Let's rewrite q as:
|
|
184
|
+
|
|
185
|
+ q = cos(t) + u sin(t)
|
|
186
|
+
|
|
187
|
+ (since 0 <= |v| = sin(t), it follows that 0 <= t <= pi)
|
|
188
|
+
|
|
189
|
+ A rotation of P, of 2t radians around axis u, can be computed as:
|
|
190
|
+
|
|
191
|
+ r_q(p) = q p q*
|
|
192
|
+
|
|
193
|
+ where q* is the conjugate of q. (note: since |q| = 1, q q* = q* q = 1)
|
|
194
|
+
|
|
195
|
+ ---
|
|
196
|
+
|
|
197
|
+ Let h = x_h i + y_h j + z_h k + w_h be a quaternion. The multplication
|
|
198
|
+ q h can be performed by pre-multiplying h, written as a column vector,
|
|
199
|
+ by the following L_q matrix:
|
|
200
|
+
|
|
201
|
+ [ w -z y x ] [ x_h ]
|
|
202
|
+ q h = L_q h = [ z w -x y ] [ y_h ]
|
|
203
|
+ [ -y x w z ] [ z_h ]
|
|
204
|
+ [ -x -y -z w ] [ w_h ]
|
|
205
|
+
|
|
206
|
+ (expand q h = (x i + y j + z k + w) (x_h i + y_h j + z_h k + w_h) to see)
|
|
207
|
+
|
|
208
|
+ Similarly, the product h q* can be expressed by pre-multiplying h by the
|
|
209
|
+ following R_q* matrix:
|
|
210
|
+
|
|
211
|
+ [ w -z y -x ] [ x_h ]
|
|
212
|
+ h q* = R_q* h = [ z w -x -y ] [ y_h ]
|
|
213
|
+ [ -y x w -z ] [ z_h ]
|
|
214
|
+ [ x y z w ] [ w_h ]
|
|
215
|
+
|
|
216
|
+ (expand h q* = (x_h i + y_h j + z_h k + w_h) (-x i - y j - z k + w) to see)
|
|
217
|
+
|
|
218
|
+ ---
|
|
219
|
+
|
|
220
|
+ Although quaternion multiplication is not commutative, it is associative,
|
|
221
|
+ i.e., r_q(p) = (q p)q* = q(p q*). From the matrix equations above, it
|
|
222
|
+ follows that r_q(p) can be expressed as R_q* L_q p = L_q R_q* p. If we
|
|
223
|
+ define M_q = L_q R_q* = R_q* L_q, we can write r_q(p) = M_q p. Matrix M_q
|
|
224
|
+ has the following form:
|
|
225
|
+
|
|
226
|
+ [ w^2 + x^2 - y^2 - z^2 2xy - 2wz 2xz + 2wy 0 ]
|
|
227
|
+ M_q = [ 2xy + 2wz w^2 - x^2 + y^2 - z^2 2yz - 2wx 0 ]
|
|
228
|
+ [ 2xz - 2wy 2yz + 2wx w^2 - x^2 - y^2 + z^2 0 ]
|
|
229
|
+ [ 0 0 0 1 ]
|
|
230
|
+
|
|
231
|
+ Note: the bottom-right entry is x^2 + y^2 + z^2 + w^2 = |q|^2 = 1.
|
|
232
|
+
|
|
233
|
+ Let M be the top-left 3x3 submatrix of M_q. A direct, but boring,
|
|
234
|
+ computation shows that M'M = M M' = I, where M' is the transpose of M.
|
|
235
|
+ In addition, det M = |q|^6 = +1. Therefore, M is a 3x3 rotation matrix.
|
|
236
|
+
|
|
237
|
+ */
|
|
238
|
+
|
|
239
|
+ const x2 = x*x, y2 = y*y, z2 = z*z;//, w2 = w*w;
|
|
240
|
+ const xy = 2*x*y, xz = 2*x*z, yz = 2*y*z;
|
|
241
|
+ const wx = 2*w*x, wy = 2*w*y, wz = 2*w*z;
|
|
242
|
+
|
|
243
|
+ return Speedy.Matrix(3, 3, [
|
|
244
|
+ 1-2*(y2+z2), xy+wz, xz-wy,
|
|
245
|
+ xy-wz, 1-2*(x2+z2), yz+wx,
|
|
246
|
+ xz+wy, yz-wx, 1-2*(x2+y2)
|
|
247
|
+ ]);
|
|
248
|
+ }
|
|
249
|
+
|
|
250
|
+ /**
|
|
251
|
+ * Convert a 3x3 rotation matrix to a unit quaternion
|
|
252
|
+ * @param m a 3x3 rotation matrix. You should ensure that it is a rotation matrix
|
|
253
|
+ * @returns this quaternion
|
|
254
|
+ * @internal
|
|
255
|
+ */
|
|
256
|
+ _fromRotationMatrix(m: SpeedyMatrix): Quaternion
|
|
257
|
+ {
|
|
258
|
+ if(m.rows != 3 || m.columns != 3)
|
|
259
|
+ throw new IllegalArgumentError();
|
|
260
|
+
|
|
261
|
+ /*
|
|
262
|
+
|
|
263
|
+ Let M be the rotation matrix defined above. We're going to find a
|
|
264
|
+ unit quaternion q associated with M.
|
|
265
|
+
|
|
266
|
+ Before we begin, note that q and (-q) encode the same rotation, for
|
|
267
|
+ r_(-q)(p) = (-q)p(-q)* = (-1)q p (-1)q* = (-1)(-1)q p q* = = r_q(p).
|
|
268
|
+ Quaternion multiplication is commutative when a factor is a scalar, i.e.,
|
|
269
|
+ d p = p d for a real d and a quaternion p (check: distributive operation).
|
|
270
|
+
|
|
271
|
+ The trace of M, denoted as tr M, is 3w^2 - x^2 - y^2 - z^2. Since |q| = 1,
|
|
272
|
+ it follows that tr M = 3w^2 - (1 - w^2), which means that 4w^2 = 1 + tr M.
|
|
273
|
+ That being the case, we can write:
|
|
274
|
+
|
|
275
|
+ |w| = sqrt(1 + tr M) / 2
|
|
276
|
+
|
|
277
|
+ We'll arbitrarily pick w >= 0, for q and (-q) encode the same rotation.
|
|
278
|
+
|
|
279
|
+ Let mij denote the element at the i-th row and at the j-th column of M.
|
|
280
|
+ A direct verification shows that m21 - m12 = 4wz. Since w >= 0, it follows
|
|
281
|
+ that sign(z) = sign(m21 - m12). Similarly, sign(y) = sign(m13 - m31) and
|
|
282
|
+ sign(x) = sign(m32 - m23).
|
|
283
|
+
|
|
284
|
+ The quantity m11 + m22 is equal to 2w^2 - 2z^2, which means that 4z^2 =
|
|
285
|
+ 4w^2 - 2(m11 + m22) = (1 + tr M) - 2(m11 + m22). Therefore, let's write:
|
|
286
|
+
|
|
287
|
+ |z| = sqrt((1 + tr M) - 2(m11 + m22)) / 2
|
|
288
|
+
|
|
289
|
+ Of course, z = |z| sign(z). Similarly,
|
|
290
|
+
|
|
291
|
+ |y| = sqrt((1 + tr M) - 2(m11 + m33)) / 2
|
|
292
|
+
|
|
293
|
+ |x| = sqrt((1 + tr M) - 2(m22 + m33)) / 2
|
|
294
|
+
|
|
295
|
+ This gives (x, y, z, w).
|
|
296
|
+
|
|
297
|
+ ---
|
|
298
|
+
|
|
299
|
+ We quickly verify that (1 + tr M) - 2(m11 + m22) >= 0 if M is (really)
|
|
300
|
+ a rotation matrix: (1 + tr M) - 2(m11 + m22) = 1 + tr M - 2(tr M - m33) =
|
|
301
|
+ 1 - tr M + 2 m33 = 1 - (3w^2 - x^2 - y^2 - z^2) + 2(w^2 - x^2 - y^2 + z^2) =
|
|
302
|
+ 1 - w^2 + z^2 = (x^2 + y^2 + z^2 + w^2) - w^2 + z^2 = x^2 + y^2 + 2 z^2 >= 0.
|
|
303
|
+ Similarly, (1 + tr M) - 2(m11 + m33) >= 0 and (1 + tr M) - 2(m22 + m33) >= 0.
|
|
304
|
+
|
|
305
|
+ */
|
|
306
|
+
|
|
307
|
+ const data = m.read();
|
|
308
|
+ const m11 = data[0], m21 = data[1], m31 = data[2],
|
|
309
|
+ m12 = data[3], m22 = data[4], m32 = data[5],
|
|
310
|
+ m13 = data[6], m23 = data[7], m33 = data[8];
|
|
311
|
+
|
|
312
|
+ const tr = 1 + m11 + m22 + m33; // 1 + tr M
|
|
313
|
+ const sx = +(m32 >= m23) - +(m32 < m23); // sign(x)
|
|
314
|
+ const sy = +(m13 >= m31) - +(m13 < m31); // sign(y)
|
|
315
|
+ const sz = +(m21 >= m12) - +(m21 < m12); // sign(z)
|
|
316
|
+
|
|
317
|
+ const w = 0.5 * Math.sqrt(Math.max(0, tr)); // |w| = w
|
|
318
|
+ const x = 0.5 * Math.sqrt(Math.max(0, tr - 2 * (m22 + m33))); // |x|
|
|
319
|
+ const y = 0.5 * Math.sqrt(Math.max(0, tr - 2 * (m11 + m33))); // |y|
|
|
320
|
+ const z = 0.5 * Math.sqrt(Math.max(0, tr - 2 * (m11 + m22))); // |z|
|
|
321
|
+
|
|
322
|
+ const length = Math.sqrt(x*x + y*y + z*z + w*w); // should be ~ 1
|
|
323
|
+ this.x = (x * sx) / length;
|
|
324
|
+ this.y = (y * sy) / length;
|
|
325
|
+ this.z = (z * sz) / length;
|
|
326
|
+ this.w = w / length;
|
|
327
|
+
|
|
328
|
+ return this;
|
|
329
|
+ }
|
|
330
|
+}
|