Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

transform.ts 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  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. * transform.ts
  20. * 3D transforms
  21. */
  22. import Speedy from 'speedy-vision';
  23. import { SpeedyMatrix } from 'speedy-vision/types/core/speedy-matrix';
  24. import { Nullable } from '../utils/utils';
  25. import { IllegalArgumentError, IllegalOperationError } from '../utils/errors';
  26. import { Vector3 } from './vector3';
  27. import { Quaternion } from './quaternion';
  28. /** Small number */
  29. const EPSILON = 1e-6;
  30. /**
  31. * A Transform represents a position, a rotation and a scale in 3D space
  32. */
  33. export class Transform
  34. {
  35. /** transformation matrix */
  36. private readonly _matrix: SpeedyMatrix;
  37. /** inverse transform, computed lazily */
  38. private _inverse: Nullable<Transform>;
  39. /** position component, computed lazily */
  40. private _position: Vector3;
  41. /** orientation component, computed lazily */
  42. private _orientation: Quaternion;
  43. /** scale component, computed lazily */
  44. private _scale: Vector3;
  45. /** whether or not this transformation has been decomposed */
  46. private _isDecomposed: boolean;
  47. /** whether or not we have extracted the position from the matrix */
  48. private _isPositionComputed: boolean;
  49. /** unit right vector of the local space, computed lazily */
  50. private _right: Vector3;
  51. /** unit up vector of the local space, computed lazily */
  52. private _up: Vector3;
  53. /** unit forward vector of the local space, computed lazily */
  54. private _forward: Vector3;
  55. /**
  56. * Constructor
  57. * @param matrix a 4x4 transformation matrix. You should ensure that its form is T * R * S (translation * rotation * scale).
  58. */
  59. constructor(matrix: SpeedyMatrix)
  60. {
  61. if(matrix.rows != 4 || matrix.columns != 4)
  62. throw new IllegalArgumentError('A Transform expects a 4x4 transformation matrix');
  63. this._matrix = matrix;
  64. this._inverse = null;
  65. this._position = Vector3.Zero();
  66. this._orientation = Quaternion.Identity();
  67. this._scale = new Vector3(1, 1, 1);
  68. this._isDecomposed = false;
  69. this._isPositionComputed = false;
  70. this._right = Vector3.ZERO;
  71. this._up = Vector3.ZERO;
  72. this._forward = Vector3.ZERO;
  73. }
  74. /**
  75. * The 4x4 transformation matrix
  76. * This matrix is not meant to be changed. Changing it will not update the
  77. * previously computed components of the transform!
  78. */
  79. get matrix(): SpeedyMatrix
  80. {
  81. return this._matrix;
  82. }
  83. /**
  84. * The inverse transform
  85. */
  86. get inverse(): Transform
  87. {
  88. if(this._inverse === null)
  89. this._inverse = new Transform(this._inverseMatrix());
  90. return this._inverse;
  91. }
  92. /**
  93. * The 3D position encoded by the transform
  94. */
  95. get position(): Vector3
  96. {
  97. if(!this._isPositionComputed)
  98. this._computePosition();
  99. return this._position;
  100. }
  101. /**
  102. * A unit quaternion describing the rotational component of the transform
  103. */
  104. get orientation(): Quaternion
  105. {
  106. if(!this._isDecomposed)
  107. this._decompose();
  108. return this._orientation;
  109. }
  110. /**
  111. * The scale encoded by the transform
  112. */
  113. get scale(): Vector3
  114. {
  115. if(!this._isDecomposed)
  116. this._decompose();
  117. return this._scale;
  118. }
  119. /**
  120. * Unit right vector of the local space
  121. */
  122. get right(): Vector3
  123. {
  124. if(this._right === Vector3.ZERO)
  125. this._right = this._scaleAndRotate(new Vector3(1, 0, 0))._normalize();
  126. return this._right;
  127. }
  128. /**
  129. * Unit up vector of the local space
  130. */
  131. get up(): Vector3
  132. {
  133. if(this._up === Vector3.ZERO)
  134. this._up = this._scaleAndRotate(new Vector3(0, 1, 0))._normalize();
  135. return this._up;
  136. }
  137. /**
  138. * Unit forward vector of the local space
  139. */
  140. get forward(): Vector3
  141. {
  142. if(this._forward === Vector3.ZERO) {
  143. // in a right-handed system, the unit forward vector is (0, 0, -1)
  144. // in a left-handed system, it is (0, 0, 1)
  145. this._forward = this._scaleAndRotate(new Vector3(0, 0, -1))._normalize();
  146. }
  147. return this._forward;
  148. }
  149. /**
  150. * Use this transform to scale and rotate a vector
  151. * The translation component of the transform is ignored
  152. * @param v a vector
  153. * @returns input vector v
  154. */
  155. private _scaleAndRotate(v: Vector3): Vector3
  156. {
  157. const m = this._matrix.read();
  158. const h = Math.abs(m[15]) < EPSILON ? Number.NaN : 1 / m[15]; // usually h = 1
  159. const vx = v.x, vy = v.y, vz = v.z;
  160. const x = m[0] * vx + m[4] * vy + m[8] * vz;
  161. const y = m[1] * vx + m[5] * vy + m[9] * vz;
  162. const z = m[2] * vx + m[6] * vy + m[10] * vz;
  163. return v._set(x * h, y * h, z * h);
  164. }
  165. /**
  166. * Decompose this transform
  167. */
  168. private _decompose(): void
  169. {
  170. /*
  171. The shape of a 4x4 transform T * R * S is
  172. [ RS t ]
  173. [ 0' 1 ]
  174. where S is a 3x3 diagonal matrix, R is a 3x3 rotation matrix, t is a
  175. 3x1 translation vector and 0' is a 1x3 zero vector.
  176. How do we decompose it?
  177. 1) Decomposing the translation vector t is trivial
  178. 2) Decomposing matrices R (rotation) and S (scale) can be done by
  179. noticing that (RS)'(RS) = (S'R')(RS) = S'(R'R) S = S'S is diagonal
  180. 3) Since R is a rotation matrix, we have det R = +1. This means that
  181. det RS = det R * det S = det S. If det RS < 0, then we have a change
  182. of handedness (i.e., a negative scale). We may flip the forward axis
  183. (Z) and let the rotation matrix encode the rest of the transformation
  184. 4) Use 2) and 3) to find a suitable S
  185. 5) Compute R = (RS) * S^(-1)
  186. */
  187. const m = this._matrix.read();
  188. const h = Math.abs(m[15]) < EPSILON ? Number.NaN : 1 / m[15]; // usually h = 1
  189. // find t
  190. const tx = m[12] * h;
  191. const ty = m[13] * h;
  192. const tz = m[14] * h;
  193. // find RS
  194. const rs11 = m[0] * h;
  195. const rs21 = m[1] * h;
  196. const rs31 = m[2] * h;
  197. const rs12 = m[4] * h;
  198. const rs22 = m[5] * h;
  199. const rs32 = m[6] * h;
  200. const rs13 = m[8] * h;
  201. const rs23 = m[9] * h;
  202. const rs33 = m[10] * h;
  203. // do we have a change of handedness?
  204. const det = rs13 * (rs21 * rs32 - rs22 * rs31) + rs33 * (rs11 * rs22 - rs12 * rs21) - rs23 * (rs11 * rs32 - rs12 * rs31);
  205. const sign = +(det >= 0) - +(det < 0);
  206. // if det = 0, RS is not invertible!
  207. // find S
  208. const sx = Math.sqrt(rs11 * rs11 + rs12 * rs12 + rs13 * rs13);
  209. const sy = Math.sqrt(rs21 * rs21 + rs22 * rs22 + rs23 * rs23);
  210. const sz = Math.sqrt(rs31 * rs31 + rs32 * rs32 + rs33 * rs33) * sign;
  211. // zero scale?
  212. if(sx < EPSILON || sy < EPSILON || sz * sign < EPSILON) {
  213. this._position._set(tx, ty, tz);
  214. this._scale._set(sx, sy, sz);
  215. this._orientation._copyFrom(Quaternion.Identity());
  216. this._isDecomposed = true;
  217. this._isPositionComputed = true;
  218. return;
  219. }
  220. // find S^(-1)
  221. const zx = 1 / sx;
  222. const zy = 1 / sy;
  223. const zz = 1 / sz;
  224. // find R
  225. const r11 = rs11 * zx;
  226. const r21 = rs21 * zx;
  227. const r31 = rs31 * zx;
  228. const r12 = rs12 * zy;
  229. const r22 = rs22 * zy;
  230. const r32 = rs32 * zy;
  231. const r13 = rs13 * zz;
  232. const r23 = rs23 * zz;
  233. const r33 = rs33 * zz;
  234. // set the components
  235. this._position._set(tx, ty, tz);
  236. this._scale._set(sx, sy, sz);
  237. this._orientation._fromRotationMatrix(Speedy.Matrix(3, 3, [
  238. r11, r21, r31,
  239. r12, r22, r32,
  240. r13, r23, r33
  241. ]));
  242. // done!
  243. this._isDecomposed = true;
  244. this._isPositionComputed = true;
  245. }
  246. /**
  247. * A simpler decomposition routine.
  248. * Sometimes we just need the position.
  249. */
  250. private _computePosition(): void
  251. {
  252. const m = this._matrix.read();
  253. const h = Math.abs(m[15]) < EPSILON ? Number.NaN : 1 / m[15]; // usually h = 1
  254. // find t
  255. this._position._set(m[12] * h, m[13] * h, m[14] * h);
  256. // done!
  257. this._isPositionComputed = true;
  258. }
  259. /**
  260. * Compute the inverse matrix of this transform
  261. * @returns the inverse matrix
  262. */
  263. private _inverseMatrix(): SpeedyMatrix
  264. {
  265. // test
  266. //console.log(Speedy.Matrix(this._matrix.inverse().times(this._matrix)).toString());
  267. // this works, but this inverse is straightforward
  268. return Speedy.Matrix(this._matrix.inverse());
  269. /*
  270. Simple analytic method
  271. ----------------------
  272. The inverse of a 4x4 transform T * R * S
  273. [ RS t ] is [ ZR' -ZR't ]
  274. [ 0' 1 ] [ 0' 1 ]
  275. where S is 3x3, R is 3x3, t is 3x1, 0' is 1x3 and Z is the inverse of S
  276. R is a rotation matrix; S is a diagonal matrix
  277. */
  278. /*
  279. // decompose the transform
  280. if(!this._isDecomposed)
  281. this._decompose();
  282. // find t
  283. const tx = this._position.x;
  284. const ty = this._position.y;
  285. const tz = this._position.z;
  286. // find S (typically 1, but not very accurate)
  287. const sx = this._scale.x;
  288. const sy = this._scale.y;
  289. const sz = this._scale.z;
  290. // sanity check
  291. if(Math.abs(sx) < EPSILON || Math.abs(sy) < EPSILON || Math.abs(sz) < EPSILON) {
  292. //throw new IllegalOperationError('Not an invertible transform: ' + this._matrix.toString());
  293. return Speedy.Matrix(4, 4, new Array(16).fill(Number.NaN)); // more friendly behavior
  294. }
  295. // find R
  296. const r = this._rotation.read();
  297. const r11 = r[0];
  298. const r21 = r[1];
  299. const r31 = r[2];
  300. const r12 = r[3];
  301. const r22 = r[4];
  302. const r32 = r[5];
  303. const r13 = r[6];
  304. const r23 = r[7];
  305. const r33 = r[8];
  306. // find Z = S^(-1)
  307. const zx = 1 / sx;
  308. const zy = 1 / sy;
  309. const zz = 1 / sz;
  310. // compute Z R'
  311. const zr11 = zx * r11;
  312. const zr21 = zy * r12;
  313. const zr31 = zz * r13;
  314. const zr12 = zx * r21;
  315. const zr22 = zy * r22;
  316. const zr32 = zz * r23;
  317. const zr13 = zx * r31;
  318. const zr23 = zy * r32;
  319. const zr33 = zz * r33;
  320. // compute -Z R't
  321. const zrt1 = -(tx * zr11 + ty * zr12 + tz * zr13);
  322. const zrt2 = -(tx * zr21 + ty * zr22 + tz * zr23);
  323. const zrt3 = -(tx * zr31 + ty * zr32 + tz * zr33);
  324. // test
  325. console.log('inverse', Speedy.Matrix(Speedy.Matrix(4, 4, [
  326. zr11, zr21, zr31, 0,
  327. zr12, zr22, zr32, 0,
  328. zr13, zr23, zr33, 0,
  329. zrt1, zrt2, zrt3, 1
  330. ]).times(this._matrix)).toString());
  331. console.log('rotation', Speedy.Matrix(
  332. this._rotation.transpose().times(this._rotation)
  333. ).toString());
  334. console.log('scale', this._scale);
  335. // done!
  336. return Speedy.Matrix(4, 4, [
  337. zr11, zr21, zr31, 0,
  338. zr12, zr22, zr32, 0,
  339. zr13, zr23, zr33, 0,
  340. zrt1, zrt2, zrt3, 1
  341. ]);
  342. */
  343. }
  344. }