/* * encantar.js * GPU-accelerated Augmented Reality for the web * Copyright (C) 2022-2024 Alexandre Martins * * 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 . * * 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; /** 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 ]); */ } }