UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

987 lines (868 loc) • 22.5 kB
import { assert } from "../../../assert.js"; import Vector3 from "../../Vector3.js"; import { aabb3_build_corners } from "./aabb3_build_corners.js"; import { aabb3_compute_distance_above_plane_max } from "./aabb3_compute_distance_above_plane_max.js"; import { aabb3_compute_plane_side } from "./aabb3_compute_plane_side.js"; import { aabb3_compute_surface_area } from "./aabb3_compute_surface_area.js"; import { aabb3_intersects_clipping_volume_array } from "./aabb3_intersects_clipping_volume_array.js"; import { aabb3_intersects_frustum_degree } from "./aabb3_intersects_frustum_degree.js"; import { aabb3_intersects_line_segment } from "./aabb3_intersects_line_segment.js"; import { aabb3_intersects_ray } from "./aabb3_intersects_ray.js"; import { aabb3_matrix4_project } from "./aabb3_matrix4_project.js"; import { aabb3_signed_distance_sqr_to_point } from "./aabb3_signed_distance_sqr_to_point.js"; import { aabb3_signed_distance_to_aabb3 } from "./aabb3_signed_distance_to_aabb3.js"; /** * Axis-Aligned bounding box in 3D * NOTE: In cases where all you want is raw performance - prefer to use typed arrays instead along with `aabb3_` functions */ export class AABB3 { /** * * @param {number} [x0] * @param {number} [y0] * @param {number} [z0] * @param {number} [x1] * @param {number} [y1] * @param {number} [z1] * @constructor */ constructor( x0 = 0, y0 = 0, z0 = 0, x1 = 0, y1 = 0, z1 = 0 ) { this.setBounds(x0, y0, z0, x1, y1, z1); } * [Symbol.iterator]() { yield this.x0; yield this.y0; yield this.z0; yield this.x1; yield this.y1; yield this.z1; } /** * * @returns {number} */ get 0() { return this.x0; } /** * * @returns {number} */ get 1() { return this.y0; } /** * * @returns {number} */ get 2() { return this.z0; } /** * * @returns {number} */ get 3() { return this.x1; } /** * * @returns {number} */ get 4() { return this.y1; } /** * * @returns {number} */ get 5() { return this.z1; } /** * * @param {number} v */ set 0(v) { this.x0 = v; } /** * * @param {number} v */ set 1(v) { this.y0 = v; } /** * * @param {number} v */ set 2(v) { this.z0 = v; } /** * * @param {number} v */ set 3(v) { this.x1 = v; } /** * * @param {number} v */ set 4(v) { this.y1 = v; } /** * * @param {number} v */ set 5(v) { this.z1 = v; } /** * * @param {number} x * @param {number} y * @param {number} z * @param {number} tolerance * @returns {boolean} */ containsPointWithTolerance(x, y, z, tolerance) { return !((x + tolerance) < this.x0 || (x - tolerance) > this.x1 || (y + tolerance) < this.y0 || (y - tolerance) > this.y1 || (z + tolerance) < this.z0 || (z - tolerance) > this.z1); } /** * * @returns {number} */ computeSurfaceArea() { return aabb3_compute_surface_area(this.x0, this.y0, this.z0, this.x1, this.y1, this.z1); } /** * * @returns {number} */ getSurfaceArea() { return this.computeSurfaceArea(); } /** * * @returns {number} */ computeVolume() { return this.getExtentsX() * this.getExtentsY() * this.getExtentsZ(); } /** * * @param {AABB3} other */ copy(other) { this.setBounds(other.x0, other.y0, other.z0, other.x1, other.y1, other.z1); } /** * * @param {Number} x0 * @param {Number} y0 * @param {Number} z0 * @param {Number} x1 * @param {Number} y1 * @param {Number} z1 */ setBounds(x0, y0, z0, x1, y1, z1) { assert.notNaN(x0, 'x0'); assert.notNaN(y0, 'y0'); assert.notNaN(z0, 'z0'); assert.notNaN(x1, 'x1'); assert.notNaN(y1, 'y1'); assert.notNaN(z1, 'z1'); /** * * @type {number} */ this.x0 = x0; /** * * @type {number} */ this.y0 = y0; /** * * @type {number} */ this.z0 = z0; /** * * @type {number} */ this.x1 = x1; /** * * @type {number} */ this.y1 = y1; /** * * @type {number} */ this.z1 = z1; } /** * * @param {AABB3} other * @returns {boolean} */ equals(other) { return this._equals(other.x0, other.y0, other.z0, other.x1, other.y1, other.z1); } /** * * @param {Number} x0 * @param {Number} y0 * @param {Number} z0 * @param {Number} x1 * @param {Number} y1 * @param {Number} z1 * @returns {boolean} */ _equals(x0, y0, z0, x1, y1, z1) { return this.x0 === x0 && this.y0 === y0 && this.z0 === z0 && this.x1 === x1 && this.y1 === y1 && this.z1 === z1; } /** * Same as setBounds, but does not require component pairs to be ordered (e.g. x0 <= x1). Method will enforce the correct order and invoke setBounds internally * @param {Number} x0 * @param {Number} y0 * @param {Number} z0 * @param {Number} x1 * @param {Number} y1 * @param {Number} z1 */ setBoundsUnordered(x0, y0, z0, x1, y1, z1) { // sort bound coordinates let _x0, _y0, _z0, _x1, _y1, _z1; if (x0 < x1) { _x0 = x0; _x1 = x1; } else { _x0 = x1; _x1 = x0; } if (y0 < y1) { _y0 = y0; _y1 = y1; } else { _y0 = y1; _y1 = y0; } if (z0 < z1) { _z0 = z0; _z1 = z1; } else { _z0 = z1; _z1 = z0; } // write sorted this.setBounds(_x0, _y0, _z0, _x1, _y1, _z1); } setNegativelyInfiniteBounds() { this.setBounds(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY); } setInfiniteBounds() { this.setBounds(Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY); } /** * * @param {number} x * @param {number} y * @param {number} z */ _translate(x, y, z) { this.setBounds( this.x0 + x, this.y0 + y, this.z0 + z, this.x1 + x, this.y1 + y, this.z1 + z ) } /** * * @param {number} x * @param {number} y * @param {number} z * @returns {number} Squared distance to point, value is negative if the point is inside the box */ distanceToPoint2(x, y, z) { return aabb3_signed_distance_sqr_to_point( this.x0, this.y0, this.z0, this.x1, this.y1, this.z1, x, y, z ); } /** * * @param {AABB3} box * @returns {number} */ distanceToBox(box) { return this._distanceToBox(box.x0, box.y0, box.z0, box.x1, box.y1, box.z1); } /** * Computes separation distance between two boxes. * If poxes penetrate or one is inside another - the result will be negative. * @param {number} x0 * @param {number} y0 * @param {number} z0 * @param {number} x1 * @param {number} y1 * @param {number} z1 * @returns {number} */ _distanceToBox(x0, y0, z0, x1, y1, z1) { const _x0 = this.x0; const _y0 = this.y0; const _z0 = this.z0; const _x1 = this.x1; const _y1 = this.y1; const _z1 = this.z1; return aabb3_signed_distance_to_aabb3( _x0, _y0, _z0, _x1, _y1, _z1, x0, y0, z0, x1, y1, z1 ); } /** * * @param {AABB3} other * @returns {number} */ costForInclusion(other) { return this._costForInclusion(other.x0, other.y0, other.z0, other.x1, other.y1, other.z1); } /** * Surface area delta when including a given AABB * 0 means that including a given AABB would not cause any change to total surface area * @param {number} x0 * @param {number} y0 * @param {number} z0 * @param {number} x1 * @param {number} y1 * @param {number} z1 * @returns {number} */ _costForInclusion(x0, y0, z0, x1, y1, z1) { let x = 0; let y = 0; let z = 0; // const _x0 = this.x0; const _y0 = this.y0; const _z0 = this.z0; const _x1 = this.x1; const _y1 = this.y1; const _z1 = this.z1; // if (_x0 > x0) { x += _x0 - x0; } if (_x1 < x1) { x += x1 - _x1; } if (_y0 > y0) { y += _y0 - y0; } if (_y1 < y1) { y += y1 - _y1; } if (_z0 > z0) { z += _z0 - z0; } if (_z1 < z1) { z += z1 - _z1; } const dx = _x1 - _x0; const dy = _y1 - _y0; const dz = _z1 - _z0; return (x * (dy + dz) + y * (dx + dz) + z * (dx + dy)); } /** * * @param {number} x * @param {number} y * @param {number} z * @returns {boolean} */ _expandToFitPoint(x, y, z) { let expanded = false; if (x < this.x0) { this.x0 = x; expanded = true; } if (y < this.y0) { this.y0 = y; expanded = true; } if (z < this.z0) { this.z0 = z; expanded = true; } if (x > this.x1) { this.x1 = x; expanded = true; } if (y > this.y1) { this.y1 = y; expanded = true; } if (z > this.z1) { this.z1 = z; expanded = true; } return expanded; } /** * * @param {AABB3} box * @returns {boolean} */ expandToFit(box) { return this._expandToFit(box.x0, box.y0, box.z0, box.x1, box.y1, box.z1); } /** * * @param {number} x0 * @param {number} y0 * @param {number} z0 * @param {number} x1 * @param {number} y1 * @param {number} z1 * @returns {boolean} */ _expandToFit(x0, y0, z0, x1, y1, z1) { let expanded = false; if (x0 < this.x0) { this.x0 = x0; expanded = true; } if (y0 < this.y0) { this.y0 = y0; expanded = true; } if (z0 < this.z0) { this.z0 = z0; expanded = true; } if (x1 > this.x1) { this.x1 = x1; expanded = true; } if (y1 > this.y1) { this.y1 = y1; expanded = true; } if (z1 > this.z1) { this.z1 = z1; expanded = true; } return expanded; } /** * * @param {number} x0 * @param {number} y0 * @param {number} z0 * @param {number} x1 * @param {number} y1 * @param {number} z1 * @returns {boolean} */ _containsBox(x0, y0, z0, x1, y1, z1) { return x0 >= this.x0 && y0 >= this.y0 && z0 >= this.z0 && x1 <= this.x1 && y1 <= this.y1 && z1 <= this.z1; } /** * * @param {AABB3} box * @returns {boolean} */ containsBox(box) { return this._containsBox(box.x0, box.y0, box.z0, box.x1, box.y1, box.z1); } /** * * @returns {number} */ getExtentsX() { return (this.x1 - this.x0); } get width() { return this.getExtentsX(); } /** * * @returns {number} */ getExtentsY() { return (this.y1 - this.y0); } get height() { return this.getExtentsY(); } /** * * @returns {number} */ getExtentsZ() { return (this.z1 - this.z0); } get depth() { return this.getExtentsZ(); } /** * half-width in X axis * @returns {number} */ getHalfExtentsX() { return this.getExtentsX() / 2; } /** * half-width in Y axis * @returns {number} */ getHalfExtentsY() { return this.getExtentsY() / 2; } /** * half-width in Z axis * @returns {number} */ getHalfExtentsZ() { return this.getExtentsZ() / 2; } /** * * @param {Vector3} target */ getExtents(target) { target.set( this.width, this.height, this.height, ); } /** * * @returns {number} */ getCenterX() { return (this.x0 + this.x1) * 0.5; } get centerX() { return this.getCenterX(); } /** * * @returns {number} */ getCenterY() { return (this.y0 + this.y1) * 0.5; } get centerY() { return this.getCenterY(); } /** * * @returns {number} */ getCenterZ() { return (this.z0 + this.z1) * 0.5; } get centerZ() { return this.getCenterZ(); } /** * Get center position of the box * @param {Vector3} [target] where to write result */ getCenter(target = new Vector3()) { const x = this.getCenterX(); const y = this.getCenterY(); const z = this.getCenterZ(); target.set( x, y, z ); return target; } /** * Accepts ray description, first set of coordinates is origin (oX,oY,oZ) and second is direction (dX,dY,dZ). Algorithm from GraphicsGems by Andrew Woo * @param oX * @param oY * @param oZ * @param dX * @param dY * @param dZ */ intersectRay(oX, oY, oZ, dX, dY, dZ) { return aabb3_intersects_ray(this.x0, this.y0, this.z0, this.x1, this.y1, this.z1, oX, oY, oZ, dX, dY, dZ); } intersectSegment(startX, startY, startZ, endX, endY, endZ) { return aabb3_intersects_line_segment(this.x0, this.y0, this.z0, this.x1, this.y1, this.z1, startX, startY, startZ, endX, endY, endZ); } /** * * @param {THREE.Box} box * @returns {boolean} */ threeContainsBox(box) { const min = box.min; const max = box.max; return this._containsBox(min.x, min.y, min.z, max.x, max.y, max.z); } /** * * @param {function(x:number, y:number, z:number)} callback * @param {*} [thisArg] */ traverseCorners(callback, thisArg) { const _x0 = this.x0; const _y0 = this.y0; const _z0 = this.z0; const _x1 = this.x1; const _y1 = this.y1; const _z1 = this.z1; callback.call(thisArg, _x0, _y0, _z0); callback.call(thisArg, _x0, _y0, _z1); callback.call(thisArg, _x0, _y1, _z0); callback.call(thisArg, _x0, _y1, _z1); callback.call(thisArg, _x1, _y0, _z0); callback.call(thisArg, _x1, _y0, _z1); callback.call(thisArg, _x1, _y1, _z0); callback.call(thisArg, _x1, _y1, _z1); } /** * * @param {number[]|Float64Array|Float32Array} result */ getCorners(result) { aabb3_build_corners( result, 0, this.x0, this.y0, this.z0, this.x1, this.y1, this.z1 ); } /** * * @param {number[]|Float32Array|Float64Array} result * @param {number} offset */ writeToArray(result = [], offset = 0) { assert.isNonNegativeInteger(offset, 'offset'); result[offset] = this.x0; result[offset + 1] = this.y0; result[offset + 2] = this.z0; result[offset + 3] = this.x1; result[offset + 4] = this.y1; result[offset + 5] = this.z1; return result; } /** * * @param {number[]|Float32Array|Float64Array} source * @param {number} offset */ readFromArray(source, offset = 0) { assert.isNonNegativeInteger(offset, 'offset'); const _x0 = source[offset]; const _y0 = source[offset + 1]; const _z0 = source[offset + 2]; const _x1 = source[offset + 3]; const _y1 = source[offset + 4]; const _z1 = source[offset + 5]; this.setBounds(_x0, _y0, _z0, _x1, _y1, _z1); } /** * @param {THREE.Plane} plane * @returns {int} 2,0,or -2; 2: above, -2 : below, 0 : intersects plane */ computePlaneSide(plane) { const normal = plane.normal; return aabb3_compute_plane_side( normal.x, normal.y, normal.z, plane.constant, this.x0, this.y0, this.z0, this.x1, this.y1, this.z1 ); } /** * * @param {number} normal_x * @param {number} normal_y * @param {number} normal_z * @param {number} offset * @returns {number} */ computeDistanceAbovePlane(normal_x, normal_y, normal_z, offset) { return aabb3_compute_distance_above_plane_max( normal_x, normal_y, normal_z, offset, this.x0, this.y0, this.z0, this.x1, this.y1, this.z1 ); } /** * * @param {number} normal_x * @param {number} normal_y * @param {number} normal_z * @param {number} offset * @returns {boolean} */ _isBelowPlane(normal_x, normal_y, normal_z, offset) { return this.computeDistanceAbovePlane(normal_x, normal_y, normal_z, offset) < 0; } /** * * @param {Plane} plane * @return {boolean} */ isBelowPlane(plane) { const normal = plane.normal; return this._isBelowPlane(normal.x, normal.y, normal.z, plane.constant); } /** * @param {Plane[]} clippingPlanes * @returns {boolean} */ intersectSpace(clippingPlanes) { let i = 0; const l = clippingPlanes.length; for (; i < l; i++) { const plane = clippingPlanes[i]; if (this.isBelowPlane(plane)) { return false; } } return true; } /** * * @param {Frustum} frustum * @returns {number} */ intersectFrustumDegree(frustum) { const planes = frustum.planes; let i = 0; let result = 2; for (; i < 6; i++) { const plane = planes[i]; const planeSide = this.computePlaneSide(plane); if (planeSide < 0) { return 0; } else if (planeSide === 0) { result = 1; } } return result; } /** * * @param {number[]} frustum * @returns {number} */ intersectFrustumDegree_array(frustum) { return aabb3_intersects_frustum_degree( this.x0, this.y0, this.z0, this.x1, this.y1, this.z1, frustum ); } /** * * @param {{planes:Array}}frustum * @returns {boolean} */ intersectFrustum(frustum) { const planes = frustum.planes; for (let i = 0; i < 6; i++) { const plane = planes[i]; if (this.isBelowPlane(plane)) { return false; } } return true; } /** * * @param {ArrayLike<number>|number[]|Float32Array}frustum * @returns {boolean} */ intersectFrustum_array(frustum) { const x0 = this.x0; const y0 = this.y0; const z0 = this.z0; const x1 = this.x1; const y1 = this.y1; const z1 = this.z1; return aabb3_intersects_clipping_volume_array(x0, y0, z0, x1, y1, z1, frustum, 0, 6); } /** * * @param {number[]|ArrayLike<number>|Float32Array} matrix */ applyMatrix4(matrix) { const a = []; const b = []; this.writeToArray(a, 0); aabb3_matrix4_project(b, a, matrix); this.readFromArray(b, 0); } /** * Expands the box in all directions by the given amount * @param {number} extra */ grow(extra) { this.x0 -= extra; this.y0 -= extra; this.z0 -= extra; this.x1 += extra; this.y1 += extra; this.z1 += extra; } /** * * @returns {AABB3} */ clone() { const clone = new AABB3(); clone.copy(this); return clone; } fromJSON({ x0, y0, z0, x1, y1, z1 }) { this.setBounds(x0, y0, z0, x1, y1, z1); } } /** * @readonly * @type {boolean} */ AABB3.prototype.isAABB3 = true; AABB3.prototype.toArray = AABB3.prototype.writeToArray; AABB3.prototype.fromArray = AABB3.prototype.readFromArray; /** * Pretending to be an array * @readonly * @type {number} */ AABB3.prototype.length = 6