UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

479 lines (396 loc) • 13.2 kB
import { assert } from "../../../core/assert.js"; import { allocate_m4 } from "../../../core/geom/3d/mat4/allocate_m4.js"; import { compose_matrix4_array } from "../../../core/geom/3d/mat4/compose_matrix4_array.js"; import { decompose_matrix_4_array } from "../../../core/geom/3d/mat4/decompose_matrix_4_array.js"; import { M4_IDENTITY } from "../../../core/geom/3d/mat4/M4_IDENTITY.js"; import { m4_multiply } from "../../../core/geom/3d/mat4/m4_multiply.js"; import Quaternion from "../../../core/geom/Quaternion.js"; import Vector3 from "../../../core/geom/Vector3.js"; import { TransformFlags } from "./TransformFlags.js"; /** * * @type {Float32Array} */ const scratch_matrix = new Float32Array(16); /** * Default set of flags for any {@link Transform} * @type {TransformFlags|number} */ const FLAGS_DEFAULT = TransformFlags.AutomaticChangeDetection; /** * A Transform represents the position, rotation, and scale of an object in 3D space. * Think of it like the object's location, orientation, and size. * It has properties for {@link position} (like coordinates), {@link rotation} (how it's oriented), and {@link scale} (how big it is). * * @example * const t = new Transform(); * t.position.set(1,2,3); * * @author Alex Goldring * @copyright Company Named Limited (c) 2025 */ export class Transform { /** * @type {Vector3} * @readonly */ position = new Vector3(0, 0, 0); /** * Orientation represented as a quaternion. * If Euler (XYZ) angles are required - consult {@link Quaternion} for relevant method * @type {Quaternion} * @readonly */ rotation = new Quaternion(0, 0, 0, 1); /** * @type {Vector3} * @readonly */ scale = new Vector3(1, 1, 1); /** * Affine transform matrix, {@link position}, {@link rotation}, and {@link scale} must match, but shear can be different. * Note: this is managed by the {@link Transform} itself unless {@link TransformFlags.AutomaticChangeDetection} is disabled; do not modify the matrix directly. * Generally, if you want to modify it - use {@link fromMatrix} method instead. * * If you're not comfortable with matrices, just leave defaults on and stick to primary attributes i.e. {@link position}, {@link rotation}, and {@link scale}. * @readonly * @type {Float32Array} */ matrix = allocate_m4(); /** * Various bit flags, see {@link TransformFlags} * This should generally be accessed through {@link getFlag}/{@link setFlag} instead of modifying the value directly * @type {number} */ flags = FLAGS_DEFAULT; constructor() { // Watch changes. // This creates a data dependency, which makes GC impossible if we keep references to position/rotation/scale // Please clone those properties if you need to keep them past the expiry of the Transform this.subscribe(this.#handle_component_change, this); } /** * Current "forward" direction * NOTE that this vector is not live, meaning that if you modify transform, previously-obtained result will no longer be valid * @returns {Vector3} */ get forward() { const result = Vector3.forward.clone(); result.applyDirectionMatrix4(this.matrix); return result; } /** * Current "up" direction * @return {Vector3} */ get up() { const result = Vector3.up.clone(); result.applyDirectionMatrix4(this.matrix); return result; } /** * Current "right" direction * @return {Vector3} */ get right() { const result = Vector3.right.clone(); result.applyDirectionMatrix4(this.matrix); return result; } /** * Attach change listener * @param {function} handler * @param {*} [thisArg] */ subscribe(handler, thisArg) { this.position.onChanged.add(handler, thisArg); this.rotation.onChanged.add(handler, thisArg); this.scale.onChanged.add(handler, thisArg); } /** * Disconnect change listener * @param {function} handler * @param {*} [thisArg] */ unsubscribe(handler, thisArg) { this.position.onChanged.remove(handler, thisArg); this.rotation.onChanged.remove(handler, thisArg); this.scale.onChanged.remove(handler, thisArg); } /** * * @private */ #handle_component_change() { if (this.getFlag(TransformFlags.AutomaticChangeDetection)) { this.updateMatrix(); } } /** * * @param {number|TransformFlags} flag * @returns {void} */ setFlag(flag) { this.flags |= flag; } /** * * @param {number|TransformFlags} flag * @returns {void} */ clearFlag(flag) { this.flags &= ~flag; } /** * * @param {number|TransformFlags} flag * @param {boolean} value */ writeFlag(flag, value) { if (value) { this.setFlag(flag); } else { this.clearFlag(flag); } } /** * * @param {number|TransformFlags} flag * @returns {boolean} */ getFlag(flag) { return (this.flags & flag) === flag; } /** * Update {@link matrix} attribute from current {@link position}/{@link rotation}/{@link scale} * * Useful for when {@link TransformFlags.AutomaticChangeDetection} is disabled. */ updateMatrix() { compose_matrix4_array(this.matrix, this.position, this.rotation, this.scale); } /** * * @param {Vector3} target * @param {Vector3} [up] */ lookAt(target, up = Vector3.up) { const position = this.position; const delta_x = target.x - position.x; const delta_y = target.y - position.y; const delta_z = target.z - position.z; if (delta_x === 0 && delta_y === 0 && delta_z === 0) { // Target is at the same location as this transform, no valid rotation, keep whatever we have. // Generally this is a programmer mistake, making any changes to the rotation would be undesirable. // The most valid option is to keep the current state. return; } this.rotation._lookRotation( delta_x, delta_y, delta_z, up.x, up.y, up.z ); } fromJSON(json) { const jp = json.position; if (jp !== undefined) { this.position.fromJSON(jp); } else { this.position.copy(Vector3.zero); } const jr = json.rotation; if (jr !== undefined) { this.rotation.fromJSON(jr); } else { this.rotation.copy(Quaternion.identity); } const js = json.scale; if (js !== undefined) { this.scale.fromJSON(js); } else { this.scale.copy(Vector3.one); } } toJSON() { return { position: this.position.toJSON(), rotation: this.rotation.toJSON(), scale: this.scale.toJSON() }; } /** * Set this transform to match `other`. * Afterward `this.equals(other) === true`. * The `other` is unchanged in the process. * @param {Transform} other */ copy(other) { // prevent the matrix from being overridden this.clearFlag(TransformFlags.AutomaticChangeDetection); this.matrix.set(other.matrix); this.position.copy(other.position); this.rotation.copy(other.rotation); this.scale.copy(other.scale); assert.arrayEqual(this.matrix, other.matrix, 'matrices must be equal after copy'); this.flags = other.flags; } /** * * @returns {Transform} */ clone() { const clone = new Transform(); clone.copy(this); return clone; } /** * Strict equality check * @param {Transform} other * @returns {boolean} */ equals(other) { return this.position.equals(other.position) && this.rotation.equals(other.rotation) && this.scale.equals(other.scale); } /** * * @returns {number} */ hash() { // only use position for hash, for speed, since in most applications position will vary the most return this.position.hash(); } /** * * @param json * @returns {Transform} */ static fromJSON(json) { const result = new Transform(); result.fromJSON(json); return result; } /** * * @param {number[]|Float32Array} mat * @returns {Transform} */ static fromMatrix(mat) { const result = new Transform(); result.fromMatrix(mat); return result; } /** * This is a post-multiplication, `this = this * other`. * The result of the multiplication is written into this transform. * * @param {Transform} other * @returns {this} */ multiply(other) { return this.multiplyTransforms(this, other); } /** * Multiply two transforms, result it written into this one * @param {Transform} a * @param {Transform} b * @returns {this} */ multiplyTransforms(a, b) { assert.defined(a, 'a'); assert.defined(b, 'b'); assert.equal(a.isTransform, true, 'a.isTransform !== true'); assert.equal(b.isTransform, true, 'b.isTransform !== true'); m4_multiply(scratch_matrix, a.matrix, b.matrix); return this.fromMatrix(scratch_matrix); } /** * * @param {mat4|number[]|Float32Array} matrix 4x4 affine matrix * @returns {this} */ fromMatrix(matrix) { assert.isArrayLike(matrix, 'matrix'); assert.greaterThanOrEqual(matrix.length, 16, 'matrix.length must be >= 16'); // we know we are changing the matrix, so we're going to need to disable the flag that sets matrix from position/rotation/scale changes const ad = this.getFlag(TransformFlags.AutomaticChangeDetection); this.clearFlag(TransformFlags.AutomaticChangeDetection); this.matrix.set(matrix); decompose_matrix_4_array(matrix, this.position, this.rotation, this.scale); // restore value of the flag this.writeFlag(TransformFlags.AutomaticChangeDetection, ad); return this; } /** * Write out the current transform to a supplied container * @param {number[]|Float32Array} [result] if not provided, `Float32Array` of size 16 will be created * @returns {number[]} same as the input */ toMatrix(result = new Float32Array(16)) { compose_matrix4_array(result, this.position, this.rotation, this.scale); return result; } /** * reset transform, resulting transform is an identity matrix * - position: [0,0,0] * - rotation: [0,0,0,1] * - scale: [1,1,1] */ makeIdentity() { this.fromMatrix(M4_IDENTITY); } /** * Identity Transform is where position is 0, rotation is 0 (un-rotated) and scale is 1 * @returns {boolean} */ isIdentity() { return this.position.equals(Vector3.zero) && this.rotation.equals(Quaternion.identity) && this.scale.equals(Vector3.one); } toString() { return `Transform{ position: ${this.position}, rotation: ${this.rotation}, scale: ${this.scale} }`; } /** * @deprecated use {@link Quaternion.rotateTowards} instead * @param {Quaternion} sourceQuaternion * @param {Vector3} targetVector * @param {Number} limit */ static adjustRotation( sourceQuaternion, targetVector, limit = Infinity ) { console.warn('deprecated, use Transform.rotation.rotateTowards instead'); const q = new Quaternion(); q.lookRotation(targetVector); sourceQuaternion.rotateTowards(q, limit); } } /** * @readonly * @type {string} */ Transform.typeName = "Transform"; /** * Enables a fast type check, without having to import the class separately. * @readonly * @type {boolean} * @example * if(t.isTransform){ * // t is a Transform, do stuff * } */ Transform.prototype.isTransform = true; /** * @readonly * @deprecated use {@link Transform.prototype.toMatrix} instead */ Transform.prototype.toMatrix4 = Transform.prototype.toMatrix /** * @readonly * @deprecated use {@link Transform.prototype.fromMatrix} instead */ Transform.prototype.fromMatrix4 = Transform.prototype.fromMatrix