123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428 |
- /*
- * encantar.js
- * GPU-accelerated Augmented Reality for the web
- * Copyright (C) 2022-2024 Alexandre Martins <alemartf(at)gmail.com>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published
- * by the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- *
- * transform.ts
- * 3D transforms
- */
-
- import Speedy from 'speedy-vision';
- import { SpeedyMatrix } from 'speedy-vision/types/core/speedy-matrix';
- import { Nullable } from '../utils/utils';
- import { IllegalArgumentError, IllegalOperationError } from '../utils/errors';
- import { Vector3 } from './vector3';
- import { Quaternion } from './quaternion';
-
- /** Small number */
- const EPSILON = 1e-6;
-
-
-
- /**
- * A Transform represents a position, a rotation and a scale in 3D space
- */
- export class Transform
- {
- /** transformation matrix */
- private readonly _matrix: SpeedyMatrix;
-
- /** inverse transform, computed lazily */
- private _inverse: Nullable<Transform>;
-
- /** position component, computed lazily */
- private _position: Vector3;
-
- /** orientation component, computed lazily */
- private _orientation: Quaternion;
-
- /** scale component, computed lazily */
- private _scale: Vector3;
-
- /** whether or not this transformation has been decomposed */
- private _isDecomposed: boolean;
-
- /** whether or not we have extracted the position from the matrix */
- private _isPositionComputed: boolean;
-
- /** unit right vector of the local space, computed lazily */
- private _right: Vector3;
-
- /** unit up vector of the local space, computed lazily */
- private _up: Vector3;
-
- /** unit forward vector of the local space, computed lazily */
- private _forward: Vector3;
-
-
-
- /**
- * Constructor
- * @param matrix a 4x4 transformation matrix. You should ensure that its form is T * R * S (translation * rotation * scale).
- */
- constructor(matrix: SpeedyMatrix)
- {
- if(matrix.rows != 4 || matrix.columns != 4)
- throw new IllegalArgumentError('A Transform expects a 4x4 transformation matrix');
-
- this._matrix = matrix;
- this._inverse = null;
-
- this._position = Vector3.Zero();
- this._orientation = Quaternion.Identity();
- this._scale = new Vector3(1, 1, 1);
-
- this._isDecomposed = false;
- this._isPositionComputed = false;
-
- this._right = Vector3.ZERO;
- this._up = Vector3.ZERO;
- this._forward = Vector3.ZERO;
- }
-
- /**
- * The 4x4 transformation matrix
- * This matrix is not meant to be changed. Changing it will not update the
- * previously computed components of the transform!
- */
- get matrix(): SpeedyMatrix
- {
- return this._matrix;
- }
-
- /**
- * The inverse transform
- */
- get inverse(): Transform
- {
- if(this._inverse === null)
- this._inverse = new Transform(this._inverseMatrix());
-
- return this._inverse;
- }
-
- /**
- * The 3D position encoded by the transform
- */
- get position(): Vector3
- {
- if(!this._isPositionComputed)
- this._computePosition();
-
- return this._position;
- }
-
- /**
- * A unit quaternion describing the rotational component of the transform
- */
- get orientation(): Quaternion
- {
- if(!this._isDecomposed)
- this._decompose();
-
- return this._orientation;
- }
-
- /**
- * The scale encoded by the transform
- */
- get scale(): Vector3
- {
- if(!this._isDecomposed)
- this._decompose();
-
- return this._scale;
- }
-
- /**
- * Unit right vector of the local space
- */
- get right(): Vector3
- {
- if(this._right === Vector3.ZERO)
- this._right = this._scaleAndRotate(new Vector3(1, 0, 0))._normalize();
-
- return this._right;
- }
-
- /**
- * Unit up vector of the local space
- */
- get up(): Vector3
- {
- if(this._up === Vector3.ZERO)
- this._up = this._scaleAndRotate(new Vector3(0, 1, 0))._normalize();
-
- return this._up;
- }
-
- /**
- * Unit forward vector of the local space
- */
- get forward(): Vector3
- {
- if(this._forward === Vector3.ZERO) {
- // in a right-handed system, the unit forward vector is (0, 0, -1)
- // in a left-handed system, it is (0, 0, 1)
- this._forward = this._scaleAndRotate(new Vector3(0, 0, -1))._normalize();
- }
-
- return this._forward;
- }
-
- /**
- * Use this transform to scale and rotate a vector
- * The translation component of the transform is ignored
- * @param v a vector
- * @returns input vector v
- */
- private _scaleAndRotate(v: Vector3): Vector3
- {
- const m = this._matrix.read();
- const h = Math.abs(m[15]) < EPSILON ? Number.NaN : 1 / m[15]; // usually h = 1
- const vx = v.x, vy = v.y, vz = v.z;
-
- const x = m[0] * vx + m[4] * vy + m[8] * vz;
- const y = m[1] * vx + m[5] * vy + m[9] * vz;
- const z = m[2] * vx + m[6] * vy + m[10] * vz;
-
- return v._set(x * h, y * h, z * h);
- }
-
- /**
- * Decompose this transform
- */
- private _decompose(): void
- {
- /*
-
- The shape of a 4x4 transform T * R * S is
-
- [ RS t ]
- [ 0' 1 ]
-
- where S is a 3x3 diagonal matrix, R is a 3x3 rotation matrix, t is a
- 3x1 translation vector and 0' is a 1x3 zero vector.
-
- How do we decompose it?
-
- 1) Decomposing the translation vector t is trivial
-
- 2) Decomposing matrices R (rotation) and S (scale) can be done by
- noticing that (RS)'(RS) = (S'R')(RS) = S'(R'R) S = S'S is diagonal
-
- 3) Since R is a rotation matrix, we have det R = +1. This means that
- det RS = det R * det S = det S. If det RS < 0, then we have a change
- of handedness (i.e., a negative scale). We may flip the forward axis
- (Z) and let the rotation matrix encode the rest of the transformation
-
- 4) Use 2) and 3) to find a suitable S
-
- 5) Compute R = (RS) * S^(-1)
-
- */
- const m = this._matrix.read();
- const h = Math.abs(m[15]) < EPSILON ? Number.NaN : 1 / m[15]; // usually h = 1
-
- // find t
- const tx = m[12] * h;
- const ty = m[13] * h;
- const tz = m[14] * h;
-
- // find RS
- const rs11 = m[0] * h;
- const rs21 = m[1] * h;
- const rs31 = m[2] * h;
- const rs12 = m[4] * h;
- const rs22 = m[5] * h;
- const rs32 = m[6] * h;
- const rs13 = m[8] * h;
- const rs23 = m[9] * h;
- const rs33 = m[10] * h;
-
- // do we have a change of handedness?
- const det = rs13 * (rs21 * rs32 - rs22 * rs31) + rs33 * (rs11 * rs22 - rs12 * rs21) - rs23 * (rs11 * rs32 - rs12 * rs31);
- const sign = +(det >= 0) - +(det < 0);
-
- // if det = 0, RS is not invertible!
-
- // find S
- const sx = Math.sqrt(rs11 * rs11 + rs12 * rs12 + rs13 * rs13);
- const sy = Math.sqrt(rs21 * rs21 + rs22 * rs22 + rs23 * rs23);
- const sz = Math.sqrt(rs31 * rs31 + rs32 * rs32 + rs33 * rs33) * sign;
-
- // zero scale?
- if(sx < EPSILON || sy < EPSILON || sz * sign < EPSILON) {
- this._position._set(tx, ty, tz);
- this._scale._set(sx, sy, sz);
- this._orientation._copyFrom(Quaternion.Identity());
- this._isDecomposed = true;
- this._isPositionComputed = true;
- return;
- }
-
- // find S^(-1)
- const zx = 1 / sx;
- const zy = 1 / sy;
- const zz = 1 / sz;
-
- // find R
- const r11 = rs11 * zx;
- const r21 = rs21 * zx;
- const r31 = rs31 * zx;
- const r12 = rs12 * zy;
- const r22 = rs22 * zy;
- const r32 = rs32 * zy;
- const r13 = rs13 * zz;
- const r23 = rs23 * zz;
- const r33 = rs33 * zz;
-
- // set the components
- this._position._set(tx, ty, tz);
- this._scale._set(sx, sy, sz);
- this._orientation._fromRotationMatrix(Speedy.Matrix(3, 3, [
- r11, r21, r31,
- r12, r22, r32,
- r13, r23, r33
- ]));
-
- // done!
- this._isDecomposed = true;
- this._isPositionComputed = true;
- }
-
- /**
- * A simpler decomposition routine.
- * Sometimes we just need the position.
- */
- private _computePosition(): void
- {
- const m = this._matrix.read();
- const h = Math.abs(m[15]) < EPSILON ? Number.NaN : 1 / m[15]; // usually h = 1
-
- // find t
- this._position._set(m[12] * h, m[13] * h, m[14] * h);
-
- // done!
- this._isPositionComputed = true;
- }
-
- /**
- * Compute the inverse matrix of this transform
- * @returns the inverse matrix
- */
- private _inverseMatrix(): SpeedyMatrix
- {
- // test
- //console.log(Speedy.Matrix(this._matrix.inverse().times(this._matrix)).toString());
-
- // this works, but this inverse is straightforward
- return Speedy.Matrix(this._matrix.inverse());
-
- /*
-
- Simple analytic method
- ----------------------
-
- The inverse of a 4x4 transform T * R * S
-
- [ RS t ] is [ ZR' -ZR't ]
- [ 0' 1 ] [ 0' 1 ]
-
- where S is 3x3, R is 3x3, t is 3x1, 0' is 1x3 and Z is the inverse of S
-
- R is a rotation matrix; S is a diagonal matrix
-
- */
-
- /*
- // decompose the transform
- if(!this._isDecomposed)
- this._decompose();
-
- // find t
- const tx = this._position.x;
- const ty = this._position.y;
- const tz = this._position.z;
-
- // find S (typically 1, but not very accurate)
- const sx = this._scale.x;
- const sy = this._scale.y;
- const sz = this._scale.z;
-
- // sanity check
- if(Math.abs(sx) < EPSILON || Math.abs(sy) < EPSILON || Math.abs(sz) < EPSILON) {
- //throw new IllegalOperationError('Not an invertible transform: ' + this._matrix.toString());
- return Speedy.Matrix(4, 4, new Array(16).fill(Number.NaN)); // more friendly behavior
- }
-
- // find R
- const r = this._rotation.read();
- const r11 = r[0];
- const r21 = r[1];
- const r31 = r[2];
- const r12 = r[3];
- const r22 = r[4];
- const r32 = r[5];
- const r13 = r[6];
- const r23 = r[7];
- const r33 = r[8];
-
- // find Z = S^(-1)
- const zx = 1 / sx;
- const zy = 1 / sy;
- const zz = 1 / sz;
-
- // compute Z R'
- const zr11 = zx * r11;
- const zr21 = zy * r12;
- const zr31 = zz * r13;
- const zr12 = zx * r21;
- const zr22 = zy * r22;
- const zr32 = zz * r23;
- const zr13 = zx * r31;
- const zr23 = zy * r32;
- const zr33 = zz * r33;
-
- // compute -Z R't
- const zrt1 = -(tx * zr11 + ty * zr12 + tz * zr13);
- const zrt2 = -(tx * zr21 + ty * zr22 + tz * zr23);
- const zrt3 = -(tx * zr31 + ty * zr32 + tz * zr33);
-
- // test
- console.log('inverse', Speedy.Matrix(Speedy.Matrix(4, 4, [
- zr11, zr21, zr31, 0,
- zr12, zr22, zr32, 0,
- zr13, zr23, zr33, 0,
- zrt1, zrt2, zrt3, 1
- ]).times(this._matrix)).toString());
-
- console.log('rotation', Speedy.Matrix(
- this._rotation.transpose().times(this._rotation)
- ).toString());
-
- console.log('scale', this._scale);
-
- // done!
- return Speedy.Matrix(4, 4, [
- zr11, zr21, zr31, 0,
- zr12, zr22, zr32, 0,
- zr13, zr23, zr33, 0,
- zrt1, zrt2, zrt3, 1
- ]);
- */
- }
- }
|