UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

435 lines (357 loc) • 11 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_multiply } from "../../../core/geom/3d/mat4/m4_multiply.js"; import { MATRIX_4_IDENTITY } from "../../../core/geom/3d/mat4/MATRIX_4_IDENTITY.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 position (like coordinates), rotation (how it's turned), and scale (how big it is). * * It also uses a "matrix" (a table of numbers) internally to efficiently store and calculate transformations, but you usually interact with the position, rotation, and scale directly. * @class */ 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); /** * transform matrix, position, rotation and scale must match, but shear can be different * @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.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 position/rotation/scale */ 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 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() }; } /** * * @param {Transform} other */ copy(other) { // prevent matrix from being overriden 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; } /** * * @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.fromMatrix4(mat); return result; } /** * * @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'); m4_multiply(scratch_matrix, a.matrix, b.matrix); return this.fromMatrix4(scratch_matrix); } /** * * @param {mat4|number[]|Float32Array} matrix * @returns {this} */ fromMatrix4(matrix) { assert.isArrayLike(matrix, 'matrix'); // 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 */ toMatrix4(result) { compose_matrix4_array(result, this.position, this.rotation, this.scale); } /** * reset transform, resulting transform is an identity matrix * - position: [0,0,0] * - rotation: [0,0,0,1] * - scale: [1,1,1] */ makeIdentity() { this.fromMatrix4(MATRIX_4_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 `{ 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 ) { const q = new Quaternion(); q.lookRotation(targetVector); sourceQuaternion.rotateTowards(q, limit); } } /** * @readonly * @type {string} */ Transform.typeName = "Transform"; /** * @readonly * @type {boolean} */ Transform.prototype.isTransform = true;