Files
engine/packages/math/src/Quaternion.ts
ChenMo 63a5662f56 Optimize the writing of function call _onValueChanged (#2497)
* refactor: optimize the writing of function call `_onValueChanged`
2025-01-09 17:40:31 +08:00

821 lines
22 KiB
TypeScript

import { IClone } from "./IClone";
import { ICopy } from "./ICopy";
import { MathUtil } from "./MathUtil";
import { Matrix3x3 } from "./Matrix3x3";
import { Vector3 } from "./Vector3";
/**
* Represents a four dimensional mathematical quaternion.
*/
export class Quaternion implements IClone<Quaternion>, ICopy<QuaternionLike, Quaternion> {
/** @internal */
static readonly _tempVector3 = new Vector3();
/** @internal */
static readonly _tempQuat1 = new Quaternion();
/**
* Determines the sum of two quaternions.
* @param left - The first quaternion to add
* @param right - The second quaternion to add
* @param out - The sum of two quaternions
*/
static add(left: Quaternion, right: Quaternion, out: Quaternion): void {
out._x = left._x + right._x;
out._y = left._y + right._y;
out._z = left._z + right._z;
out._w = left._w + right._w;
out._onValueChanged?.();
}
/**
* Determines the product of two quaternions.
* @param left - The first quaternion to multiply
* @param right - The second quaternion to multiply
* @param out - The product of two quaternions
*/
static multiply(left: Quaternion, right: Quaternion, out: Quaternion): void {
const ax = left._x,
ay = left._y,
az = left._z,
aw = left._w;
const bx = right._x,
by = right._y,
bz = right._z,
bw = right._w;
out._x = ax * bw + aw * bx + ay * bz - az * by;
out._y = ay * bw + aw * by + az * bx - ax * bz;
out._z = az * bw + aw * bz + ax * by - ay * bx;
out._w = aw * bw - ax * bx - ay * by - az * bz;
out._onValueChanged?.();
}
/**
* Calculate quaternion that contains conjugated version of the specified quaternion.
* @param a - The specified quaternion
* @param out - The conjugate version of the specified quaternion
*/
static conjugate(a: Quaternion, out: Quaternion): void {
out._x = -a._x;
out._y = -a._y;
out._z = -a._z;
out._w = a._w;
out._onValueChanged?.();
}
/**
* Determines the dot product of two quaternions.
* @param left - The first quaternion to dot
* @param right - The second quaternion to dot
* @returns The dot product of two quaternions
*/
static dot(left: Quaternion, right: Quaternion): number {
return left._x * right._x + left._y * right._y + left._z * right._z + left._w * right._w;
}
/**
* Determines whether the specified quaternions are equals.
* @param left - The first quaternion to compare
* @param right - The second quaternion to compare
* @returns True if the specified quaternions are equals, false otherwise
*/
static equals(left: Quaternion, right: Quaternion): boolean {
return (
MathUtil.equals(left._x, right._x) &&
MathUtil.equals(left._y, right._y) &&
MathUtil.equals(left._z, right._z) &&
MathUtil.equals(left._w, right._w)
);
}
/**
* Calculate a quaternion rotates around an arbitrary axis.
* @param axis - The axis
* @param rad - The rotation angle in radians
* @param out - The quaternion after rotate
*/
static rotationAxisAngle(axis: Vector3, rad: number, out: Quaternion): void {
const normalAxis = Quaternion._tempVector3;
Vector3.normalize(axis, normalAxis);
rad *= 0.5;
const s = Math.sin(rad);
out._x = normalAxis._x * s;
out._y = normalAxis._y * s;
out._z = normalAxis._z * s;
out._w = Math.cos(rad);
out._onValueChanged?.();
}
/**
* Calculate a quaternion rotates around x, y, z axis (pitch/yaw/roll).
* @param x - The radian of rotation around X (pitch)
* @param y - The radian of rotation around Y (yaw)
* @param z - The radian of rotation around Z (roll)
* @param out - The calculated quaternion
*/
static rotationEuler(x: number, y: number, z: number, out: Quaternion): void {
Quaternion.rotationYawPitchRoll(y, x, z, out);
}
/**
* Calculate a quaternion from the specified yaw, pitch and roll angles.
* @param yaw - Yaw around the y axis in radians
* @param pitch - Pitch around the x axis in radians
* @param roll - Roll around the z axis in radians
* @param out - The calculated quaternion
*/
static rotationYawPitchRoll(yaw: number, pitch: number, roll: number, out: Quaternion): void {
const halfRoll = roll * 0.5;
const halfPitch = pitch * 0.5;
const halfYaw = yaw * 0.5;
const sinRoll = Math.sin(halfRoll);
const cosRoll = Math.cos(halfRoll);
const sinPitch = Math.sin(halfPitch);
const cosPitch = Math.cos(halfPitch);
const sinYaw = Math.sin(halfYaw);
const cosYaw = Math.cos(halfYaw);
const cosYawPitch = cosYaw * cosPitch;
const sinYawPitch = sinYaw * sinPitch;
out._x = cosYaw * sinPitch * cosRoll + sinYaw * cosPitch * sinRoll;
out._y = sinYaw * cosPitch * cosRoll - cosYaw * sinPitch * sinRoll;
out._z = cosYawPitch * sinRoll - sinYawPitch * cosRoll;
out._w = cosYawPitch * cosRoll + sinYawPitch * sinRoll;
out._onValueChanged?.();
}
/**
* Calculate a quaternion from the specified 3x3 matrix.
* @param m - The specified 3x3 matrix
* @param out - The calculated quaternion
*/
static rotationMatrix3x3(m: Matrix3x3, out: Quaternion): void {
const me = m.elements;
const m11 = me[0],
m12 = me[1],
m13 = me[2];
const m21 = me[3],
m22 = me[4],
m23 = me[5];
const m31 = me[6],
m32 = me[7],
m33 = me[8];
const scale = m11 + m22 + m33;
let sqrt, half;
if (scale > 0) {
sqrt = Math.sqrt(scale + 1.0);
out._w = sqrt * 0.5;
sqrt = 0.5 / sqrt;
out._x = (m23 - m32) * sqrt;
out._y = (m31 - m13) * sqrt;
out._z = (m12 - m21) * sqrt;
} else if (m11 >= m22 && m11 >= m33) {
sqrt = Math.sqrt(1.0 + m11 - m22 - m33);
half = 0.5 / sqrt;
out._x = 0.5 * sqrt;
out._y = (m12 + m21) * half;
out._z = (m13 + m31) * half;
out._w = (m23 - m32) * half;
} else if (m22 > m33) {
sqrt = Math.sqrt(1.0 + m22 - m11 - m33);
half = 0.5 / sqrt;
out._x = (m21 + m12) * half;
out._y = 0.5 * sqrt;
out._z = (m32 + m23) * half;
out._w = (m31 - m13) * half;
} else {
sqrt = Math.sqrt(1.0 + m33 - m11 - m22);
half = 0.5 / sqrt;
out._x = (m13 + m31) * half;
out._y = (m23 + m32) * half;
out._z = 0.5 * sqrt;
out._w = (m12 - m21) * half;
}
out._onValueChanged?.();
}
/**
* Calculate the inverse of the specified quaternion.
* @param a - The quaternion whose inverse is to be calculated
* @param out - The inverse of the specified quaternion
*/
static invert(a: Quaternion, out: Quaternion): void {
const { _x: x, _y: y, _z: z, _w: w } = a;
const dot = x * x + y * y + z * z + w * w;
if (dot > MathUtil.zeroTolerance) {
const invDot = 1.0 / dot;
out._x = -x * invDot;
out._y = -y * invDot;
out._z = -z * invDot;
out._w = w * invDot;
out._onValueChanged?.();
}
}
/**
* Performs a linear blend between two quaternions.
* @param start - The first quaternion
* @param end - The second quaternion
* @param t - The blend amount where 0 returns start and 1 end
* @param out - The result of linear blending between two quaternions
*/
static lerp(start: Quaternion, end: Quaternion, t: number, out: Quaternion): void {
const inv = 1.0 - t;
if (Quaternion.dot(start, end) >= 0) {
out._x = start._x * inv + end._x * t;
out._y = start._y * inv + end._y * t;
out._z = start._z * inv + end._z * t;
out._w = start._w * inv + end._w * t;
} else {
out._x = start._x * inv - end._x * t;
out._y = start._y * inv - end._y * t;
out._z = start._z * inv - end._z * t;
out._w = start._w * inv - end._w * t;
}
out.normalize();
}
/**
* Performs a spherical linear blend between two quaternions.
* @param start - The first quaternion
* @param end - The second quaternion
* @param amount - The blend amount where 0 returns start and 1 end
* @param out - The result of spherical linear blending between two quaternions
*/
static slerp(start: Quaternion, end: Quaternion, amount: number, out: Quaternion): void {
let opposite: number;
let inverse: number;
const dot = Quaternion.dot(start, end);
if (Math.abs(dot) > 1.0 - MathUtil.zeroTolerance) {
inverse = 1.0 - amount;
opposite = amount * Math.sign(dot);
} else {
const acos = Math.acos(Math.abs(dot));
const invSin = 1.0 / Math.sin(acos);
inverse = Math.sin((1.0 - amount) * acos) * invSin;
opposite = Math.sin(amount * acos) * invSin * Math.sign(dot);
}
out.x = inverse * start.x + opposite * end.x;
out.y = inverse * start.y + opposite * end.y;
out.z = inverse * start.z + opposite * end.z;
out.w = inverse * start.w + opposite * end.w;
out._onValueChanged?.();
}
/**
* Scales the specified quaternion magnitude to unit length.
* @param a - The specified quaternion
* @param out - The normalized quaternion
*/
static normalize(a: Quaternion, out: Quaternion): void {
const { _x, _y, _z, _w } = a;
let len = Math.sqrt(_x * _x + _y * _y + _z * _z + _w * _w);
if (len > MathUtil.zeroTolerance) {
len = 1 / len;
out._x = _x * len;
out._y = _y * len;
out._z = _z * len;
out._w = _w * len;
out._onValueChanged?.();
}
}
/**
* Calculate a quaternion rotate around X axis.
* @param rad - The rotation angle in radians
* @param out - The calculated quaternion
*/
static rotationX(rad: number, out: Quaternion): void {
rad *= 0.5;
const s = Math.sin(rad);
const c = Math.cos(rad);
out._x = s;
out._y = 0;
out._z = 0;
out._w = c;
out._onValueChanged?.();
}
/**
* Calculate a quaternion rotate around Y axis.
* @param rad - The rotation angle in radians
* @param out - The calculated quaternion
*/
static rotationY(rad: number, out: Quaternion): void {
rad *= 0.5;
const s = Math.sin(rad);
const c = Math.cos(rad);
out._x = 0;
out._y = s;
out._z = 0;
out._w = c;
out._onValueChanged?.();
}
/**
* Calculate a quaternion rotate around Z axis.
* @param rad - The rotation angle in radians
* @param out - The calculated quaternion
*/
static rotationZ(rad: number, out: Quaternion): void {
rad *= 0.5;
const s = Math.sin(rad);
const c = Math.cos(rad);
out._x = 0;
out._y = 0;
out._z = s;
out._w = c;
out._onValueChanged?.();
}
/**
* Calculate a quaternion that the specified quaternion rotate around X axis.
* @param quaternion - The specified quaternion
* @param rad - The rotation angle in radians
* @param out - The calculated quaternion
*/
static rotateX(quaternion: Quaternion, rad: number, out: Quaternion): void {
const { _x, _y, _z, _w } = quaternion;
rad *= 0.5;
const bx = Math.sin(rad);
const bw = Math.cos(rad);
out._x = _x * bw + _w * bx;
out._y = _y * bw + _z * bx;
out._z = _z * bw - _y * bx;
out._w = _w * bw - _x * bx;
out._onValueChanged?.();
}
/**
* Calculate a quaternion that the specified quaternion rotate around Y axis.
* @param quaternion - The specified quaternion
* @param rad - The rotation angle in radians
* @param out - The calculated quaternion
*/
static rotateY(quaternion: Quaternion, rad: number, out: Quaternion): void {
const { _x, _y, _z, _w } = quaternion;
rad *= 0.5;
const by = Math.sin(rad);
const bw = Math.cos(rad);
out._x = _x * bw - _z * by;
out._y = _y * bw + _w * by;
out._z = _z * bw + _x * by;
out._w = _w * bw - _y * by;
out._onValueChanged?.();
}
/**
* Calculate a quaternion that the specified quaternion rotate around Z axis.
* @param quaternion - The specified quaternion
* @param rad - The rotation angle in radians
* @param out - The calculated quaternion
*/
static rotateZ(quaternion: Quaternion, rad: number, out: Quaternion): void {
const { _x, _y, _z, _w } = quaternion;
rad *= 0.5;
const bz = Math.sin(rad);
const bw = Math.cos(rad);
out._x = _x * bw + _y * bz;
out._y = _y * bw - _x * bz;
out._z = _z * bw + _w * bz;
out._w = _w * bw - _z * bz;
out._onValueChanged?.();
}
/**
* Scale a quaternion by a given number.
* @param a - The quaternion
* @param s - The given number
* @param out - The scaled quaternion
*/
static scale(a: Quaternion, s: number, out: Quaternion): void {
out._x = a._x * s;
out._y = a._y * s;
out._z = a._z * s;
out._w = a._w * s;
out._onValueChanged?.();
}
/** @internal */
_x: number;
/** @internal */
_y: number;
/** @internal */
_z: number;
/** @internal */
_w: number;
/** @internal */
_onValueChanged: () => void = null;
/**
* The x component of the quaternion.
*/
public get x(): number {
return this._x;
}
public set x(value: number) {
this._x = value;
this._onValueChanged?.();
}
/**
* The y component of the quaternion.
*/
public get y(): number {
return this._y;
}
public set y(value: number) {
this._y = value;
this._onValueChanged?.();
}
/**
* The z component of the quaternion.
*/
public get z(): number {
return this._z;
}
public set z(value: number) {
this._z = value;
this._onValueChanged?.();
}
/**
* Indicting whether this instance is normalized.
*/
public get normalized(): boolean {
return (
Math.abs(this._x * this._x + this._y * this._y + this._z * this._z + this._w * this._w - 1) <
MathUtil.zeroTolerance
);
}
/**
* The w component of the quaternion.
*/
public get w() {
return this._w;
}
public set w(value: number) {
this._w = value;
this._onValueChanged?.();
}
/**
* Constructor of Quaternion.
* @param x - The x component of the quaternion, default 0
* @param y - The y component of the quaternion, default 0
* @param z - The z component of the quaternion, default 0
* @param w - The w component of the quaternion, default 1
*/
constructor(x: number = 0, y: number = 0, z: number = 0, w: number = 1) {
this._x = x;
this._y = y;
this._z = z;
this._w = w;
}
/**
* Set the value of this quaternion, and return this quaternion.
* @param x - The x component of the quaternion
* @param y - The y component of the quaternion
* @param z - The z component of the quaternion
* @param w - The w component of the quaternion
* @returns This quaternion
*/
set(x: number, y: number, z: number, w: number): Quaternion {
this._x = x;
this._y = y;
this._z = z;
this._w = w;
this._onValueChanged?.();
return this;
}
/**
* Transforms this quaternion into its conjugated version.
* @returns This quaternion
*/
conjugate(): Quaternion {
this._x *= -1;
this._y *= -1;
this._z *= -1;
this._onValueChanged?.();
return this;
}
/**
* Get the rotation axis and rotation angle of the quaternion (unit: radians).
* @param out - The axis as an output parameter
* @returns The rotation angle (unit: radians)
*/
getAxisAngle(out: Vector3): number {
const { _x, _y, _z } = this;
const length = _x * _x + _y * _y + _z * _z;
if (length < MathUtil.zeroTolerance) {
out._x = 1;
out._y = 0;
out._z = 0;
return 0;
} else {
const inv = 1.0 / length;
out._x = this._x * inv;
out._y = this._y * inv;
out._z = this._z * inv;
return Math.acos(this._w) * 2.0;
}
}
/**
* Identity this quaternion.
* @returns This quaternion after identity
*/
identity(): Quaternion {
this._x = 0;
this._y = 0;
this._z = 0;
this._w = 1;
this._onValueChanged?.();
return this;
}
/**
* Calculate the length of this quaternion.
* @returns The length of this quaternion
*/
length(): number {
const { _x, _y, _z, _w } = this;
return Math.sqrt(_x * _x + _y * _y + _z * _z + _w * _w);
}
/**
* Calculates the squared length of this quaternion.
* @returns The squared length of this quaternion
*/
lengthSquared(): number {
const { _x, _y, _z, _w } = this;
return _x * _x + _y * _y + _z * _z + _w * _w;
}
/**
* Converts this quaternion into a unit quaternion.
* @returns This quaternion
*/
normalize(): Quaternion {
Quaternion.normalize(this, this);
return this;
}
/**
* Get the euler of this quaternion.
* @param out - The euler (in radians) as an output parameter
* @returns Euler x->pitch y->yaw z->roll
*/
toEuler(out: Vector3): Vector3 {
this._toYawPitchRoll(out);
const t = out._x;
out._x = out._y;
out._y = t;
out._onValueChanged?.();
return out;
}
/**
* Get the euler of this quaternion.
* @param out - The euler (in radians) as an output parameter
* @returns Euler x->yaw y->pitch z->roll
*/
toYawPitchRoll(out: Vector3): Vector3 {
this._toYawPitchRoll(out);
out._onValueChanged?.();
return out;
}
/**
* Calculate this quaternion rotate around X axis.
* @param rad - The rotation angle in radians
* @returns This quaternion
*/
rotateX(rad: number): Quaternion {
Quaternion.rotateX(this, rad, this);
return this;
}
/**
* Calculate this quaternion rotate around Y axis.
* @param rad - The rotation angle in radians
* @returns This quaternion
*/
rotateY(rad: number): Quaternion {
Quaternion.rotateY(this, rad, this);
return this;
}
/**
* Calculate this quaternion rotate around Z axis.
* @param rad - The rotation angle in radians
* @returns This quaternion
*/
rotateZ(rad: number): Quaternion {
Quaternion.rotateZ(this, rad, this);
return this;
}
/**
* Calculate this quaternion rotates around an arbitrary axis.
* @param axis - The axis
* @param rad - The rotation angle in radians
* @returns This quaternion
*/
rotationAxisAngle(axis: Vector3, rad: number): Quaternion {
Quaternion.rotationAxisAngle(axis, rad, this);
return this;
}
/**
* Determines the product of this quaternion and the specified quaternion.
* @param quat - The specified quaternion
* @returns The product of the two quaternions
*/
multiply(quat: Quaternion): Quaternion {
Quaternion.multiply(this, quat, this);
return this;
}
/**
* Invert this quaternion.
* @returns This quaternion after invert
*/
invert(): Quaternion {
Quaternion.invert(this, this);
return this;
}
/**
* Determines the dot product of this quaternion and the specified quaternion.
* @param quat - The specified quaternion
* @returns The dot product of two quaternions
*/
dot(quat: Quaternion): number {
return Quaternion.dot(this, quat);
}
/**
* Performs a linear blend between this quaternion and the specified quaternion.
* @param quat - The specified quaternion
* @param t - The blend amount where 0 returns this and 1 quat
* @returns - The result of linear blending between two quaternions
*/
lerp(quat: Quaternion, t: number): Quaternion {
Quaternion.lerp(this, quat, t, this);
return this;
}
/**
* Calculate this quaternion rotation around an arbitrary axis.
* @param axis - The axis
* @param rad - The rotation angle in radians
* @returns This quaternion
*/
rotateAxisAngle(axis: Vector3, rad: number): Quaternion {
Quaternion._tempQuat1.rotationAxisAngle(axis, rad);
this.multiply(Quaternion._tempQuat1);
return this;
}
/**
* Creates a clone of this quaternion.
* @returns A clone of this quaternion
*/
clone(): Quaternion {
return new Quaternion(this._x, this._y, this._z, this._w);
}
/**
* Copy this quaternion from the specified quaternion.
* @param source - The specified quaternion
* @returns This quaternion
*/
copyFrom(source: QuaternionLike): Quaternion {
this._x = source.x;
this._y = source.y;
this._z = source.z;
this._w = source.w;
this._onValueChanged?.();
return this;
}
/**
* Copy this quaternion to the specified quaternion.
* @param target - The specified quaternion
* @returns This specified quaternion
*/
copyTo(target: QuaternionLike): QuaternionLike {
target.x = this._x;
target.y = this._y;
target.z = this._z;
target.w = this._w;
return target;
}
/**
* Copy the value of this quaternion from an array.
* @param array - The array
* @param offset - The start offset of the array
* @returns This quaternion
*/
copyFromArray(array: ArrayLike<number>, offset: number = 0): Quaternion {
this._x = array[offset];
this._y = array[offset + 1];
this._z = array[offset + 2];
this._w = array[offset + 3];
this._onValueChanged?.();
return this;
}
/**
* Copy the value of this quaternion to an array.
* @param out - The array
* @param outOffset - The start offset of the array
*/
copyToArray(out: number[] | Float32Array | Float64Array, outOffset: number = 0) {
out[outOffset] = this._x;
out[outOffset + 1] = this._y;
out[outOffset + 2] = this._z;
out[outOffset + 3] = this._w;
}
/**
* Serialize this quaternion to a JSON representation.
* @returns A JSON Object representation of this quaternion
*/
toJSON(): QuaternionLike {
return {
x: this._x,
y: this._y,
z: this._z,
w: this._w
};
}
private _toYawPitchRoll(out: Vector3): void {
// http://www.euclideanspace.com/maths/geometry/rotations/conversions/quaternionToEuler/
const { _x: x, _y: y, _z: z, _w: w } = this;
const xx = x * x;
const yy = y * y;
const zz = z * z;
const ww = w * w;
const unit = xx + yy + zz + ww;
const test = 2 * (x * w - y * z);
if (test > (1 - MathUtil.zeroTolerance) * unit) {
out._x = Math.atan2(2.0 * (w * y - x * z), xx + ww - yy - zz);
out._y = Math.PI / 2;
out._z = 0;
} else if (test < -(1 - MathUtil.zeroTolerance) * unit) {
out._x = Math.atan2(2.0 * (w * y - x * z), xx + ww - yy - zz);
out._y = -Math.PI / 2;
out._z = 0;
} else {
out._x = Math.atan2(2.0 * (z * x + y * w), zz + ww - yy - xx);
out._y = Math.asin(test / unit);
out._z = Math.atan2(2.0 * (x * y + z * w), yy + ww - zz - xx);
}
}
}
interface QuaternionLike {
/** {@inheritDoc Quaternion.x} */
x: number;
/** {@inheritDoc Quaternion.y} */
y: number;
/** {@inheritDoc Quaternion.z} */
z: number;
/** {@inheritDoc Quaternion.w} */
w: number;
}