Explorar el Código

Introduce Quaternions

customisations
alemart hace 10 meses
padre
commit
2c130b640d
Se han modificado 4 ficheros con 426 adiciones y 27 borrados
  1. 67
    0
      docs/api/quaternion.md
  2. 1
    0
      mkdocs.yml
  3. 330
    0
      src/geometry/quaternion.ts
  4. 28
    27
      src/main.ts

+ 67
- 0
docs/api/quaternion.md Ver fichero

@@ -0,0 +1,67 @@
1
+# Quaternion
2
+
3
+A number system used in encantar.js to represent rotations in 3D space.
4
+
5
+*Since:* 0.4.0
6
+
7
+## Properties
8
+
9
+### x
10
+
11
+`quaternion.x: number`
12
+
13
+The x coordinate of the quaternion (imaginary).
14
+
15
+### y
16
+
17
+`quaternion.y: number`
18
+
19
+The y coordinate of the quaternion (imaginary).
20
+
21
+### z
22
+
23
+`quaternion.z: number`
24
+
25
+The z coordinate of the quaternion (imaginary).
26
+
27
+### w
28
+
29
+`quaternion.w: number`
30
+
31
+The w coordinate of the quaternion (real).
32
+
33
+## Methods
34
+
35
+### length
36
+
37
+`quaternion.length(): number`
38
+
39
+Compute the length of the quaternion: `sqrt(x^2 + y^2 + z^2 + w^2)`.
40
+
41
+**Returns**
42
+
43
+The length of the quaternion.
44
+
45
+### equals
46
+
47
+`quaternion.equals(q: Quaternion): boolean`
48
+
49
+Check if `this` and `q` have the same coordinates.
50
+
51
+**Arguments**
52
+
53
+* `q: Quaternion`. A quaternion.
54
+
55
+**Returns**
56
+
57
+`true` if `this` and `q` have the same coordinates.
58
+
59
+### toString
60
+
61
+`quaternion.toString(): string`
62
+
63
+Generate a string representation of the quaternion.
64
+
65
+**Returns**
66
+
67
+A string representation of the quaternion.

+ 1
- 0
mkdocs.yml Ver fichero

@@ -95,6 +95,7 @@ nav:
95 95
       - 'View': 'api/view.md'
96 96
       - 'PerspectiveView': 'api/perspective-view.md'
97 97
       - 'RigidTransform': 'api/rigid-transform.md'
98
+      - 'Quaternion': 'api/quaternion.md'
98 99
     - 'Visualization':
99 100
       - 'Viewport': 'api/viewport.md'
100 101
       - 'HUD': 'api/hud.md'

+ 330
- 0
src/geometry/quaternion.ts Ver fichero

@@ -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
+}

+ 28
- 27
src/main.ts Ver fichero

@@ -28,6 +28,7 @@ import { TrackerFactory } from './trackers/tracker-factory';
28 28
 import { SourceFactory } from './sources/source-factory';
29 29
 import { Viewport, ViewportSettings } from './core/viewport';
30 30
 import { Utils } from './utils/utils';
31
+
31 32
 declare const __AR_VERSION__: string;
32 33
 declare const __AR_DEVELOPMENT_MODE__: string;
33 34
 declare const __AR_WEBSITE__: string;
@@ -48,65 +49,65 @@ export default class AR
48 49
     }
49 50
 
50 51
     /**
51
-     * Trackers
52
+     * Checks if the engine can be run in the browser the client is using
53
+     * @returns true if the engine is compatible with the browser
52 54
      */
53
-    static get Tracker(): typeof TrackerFactory
55
+    static isSupported(): boolean
54 56
     {
55
-        return TrackerFactory;
57
+        return Session.isSupported();
56 58
     }
57 59
 
58 60
     /**
59
-     * Sources of data
61
+     * Engine version
60 62
      */
61
-    static get Source(): typeof SourceFactory
63
+    static get version(): string
62 64
     {
63
-        return SourceFactory;
65
+        if(__AR_DEVELOPMENT_MODE__)
66
+            return __AR_VERSION__ + '-dev';
67
+        else
68
+            return __AR_VERSION__;
64 69
     }
65 70
 
66 71
     /**
67
-     * Create a viewport
68
-     * @param settings
69
-     * @returns a new viewport with the specified settings
72
+     * Speedy Vision
70 73
      */
71
-    static Viewport(settings: ViewportSettings): Viewport
74
+    static get Speedy(): typeof Speedy
72 75
     {
73
-        return new Viewport(settings);
76
+        return Speedy;
74 77
     }
75 78
 
76 79
     /**
77
-     * Global Settings
80
+     * Trackers
78 81
      */
79
-    static get Settings(): typeof Settings
82
+    static get Tracker(): typeof TrackerFactory
80 83
     {
81
-        return Settings;
84
+        return TrackerFactory;
82 85
     }
83 86
 
84 87
     /**
85
-     * Engine version
88
+     * Sources of data
86 89
      */
87
-    static get version(): string
90
+    static get Source(): typeof SourceFactory
88 91
     {
89
-        if(__AR_DEVELOPMENT_MODE__)
90
-            return __AR_VERSION__ + '-dev';
91
-        else
92
-            return __AR_VERSION__;
92
+        return SourceFactory;
93 93
     }
94 94
 
95 95
     /**
96
-     * Speedy Vision
96
+     * Create a viewport
97
+     * @param settings
98
+     * @returns a new viewport with the specified settings
97 99
      */
98
-    static get Speedy(): typeof Speedy
100
+    static Viewport(settings: ViewportSettings): Viewport
99 101
     {
100
-        return Speedy;
102
+        return new Viewport(settings);
101 103
     }
102 104
 
103 105
     /**
104
-     * Checks if the engine can be run in the browser the client is using
105
-     * @returns true if the engine is compatible with the browser
106
+     * Global Settings
106 107
      */
107
-    static isSupported(): boolean
108
+    static get Settings(): typeof Settings
108 109
     {
109
-        return Session.isSupported();
110
+        return Settings;
110 111
     }
111 112
 }
112 113
 

Loading…
Cancelar
Guardar