You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

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