@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
987 lines (868 loc) • 22.5 kB
JavaScript
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