playcanvas
Version:
PlayCanvas WebGL game engine
383 lines (380 loc) • 13.8 kB
JavaScript
import { Vec3 } from '../math/vec3.js';
/**
* @import { BoundingSphere } from './bounding-sphere.js'
* @import { Mat4 } from '../math/mat4.js'
* @import { Ray } from './ray.js'
*/ const tmpVecA = new Vec3();
const tmpVecB = new Vec3();
const tmpVecC = new Vec3();
const tmpVecD = new Vec3();
const tmpVecE = new Vec3();
/**
* Axis-Aligned Bounding Box.
*
* @category Math
*/ class BoundingBox {
/**
* Create a new BoundingBox instance. The bounding box is axis-aligned.
*
* @param {Vec3} [center] - Center of box. The constructor takes a reference of this parameter.
* Defaults to (0, 0, 0).
* @param {Vec3} [halfExtents] - Half the distance across the box in each axis. The constructor
* takes a reference of this parameter. Defaults to (0.5, 0.5, 0.5).
*/ constructor(center, halfExtents){
/**
* Center of box.
*
* @type {Vec3}
* @readonly
*/ this.center = new Vec3();
/**
* Half the distance across the box in each axis.
*
* @type {Vec3}
* @readonly
*/ this.halfExtents = new Vec3(0.5, 0.5, 0.5);
/**
* @type {Vec3}
* @private
*/ this._min = new Vec3();
/**
* @type {Vec3}
* @private
*/ this._max = new Vec3();
if (center) {
this.center.copy(center);
}
if (halfExtents) {
this.halfExtents.copy(halfExtents);
}
}
/**
* Combines two bounding boxes into one, enclosing both.
*
* @param {BoundingBox} other - Bounding box to add.
*/ add(other) {
const tc = this.center;
const tcx = tc.x;
const tcy = tc.y;
const tcz = tc.z;
const th = this.halfExtents;
const thx = th.x;
const thy = th.y;
const thz = th.z;
let tminx = tcx - thx;
let tmaxx = tcx + thx;
let tminy = tcy - thy;
let tmaxy = tcy + thy;
let tminz = tcz - thz;
let tmaxz = tcz + thz;
const oc = other.center;
const ocx = oc.x;
const ocy = oc.y;
const ocz = oc.z;
const oh = other.halfExtents;
const ohx = oh.x;
const ohy = oh.y;
const ohz = oh.z;
const ominx = ocx - ohx;
const omaxx = ocx + ohx;
const ominy = ocy - ohy;
const omaxy = ocy + ohy;
const ominz = ocz - ohz;
const omaxz = ocz + ohz;
if (ominx < tminx) tminx = ominx;
if (omaxx > tmaxx) tmaxx = omaxx;
if (ominy < tminy) tminy = ominy;
if (omaxy > tmaxy) tmaxy = omaxy;
if (ominz < tminz) tminz = ominz;
if (omaxz > tmaxz) tmaxz = omaxz;
tc.x = (tminx + tmaxx) * 0.5;
tc.y = (tminy + tmaxy) * 0.5;
tc.z = (tminz + tmaxz) * 0.5;
th.x = (tmaxx - tminx) * 0.5;
th.y = (tmaxy - tminy) * 0.5;
th.z = (tmaxz - tminz) * 0.5;
}
/**
* Copies the contents of a source AABB.
*
* @param {BoundingBox} src - The AABB to copy from.
*/ copy(src) {
this.center.copy(src.center);
this.halfExtents.copy(src.halfExtents);
}
/**
* Returns a clone of the AABB.
*
* @returns {BoundingBox} A duplicate AABB.
*/ clone() {
return new BoundingBox(this.center, this.halfExtents);
}
/**
* Test whether two axis-aligned bounding boxes intersect.
*
* @param {BoundingBox} other - Bounding box to test against.
* @returns {boolean} True if there is an intersection.
*/ intersects(other) {
const aMax = this.getMax();
const aMin = this.getMin();
const bMax = other.getMax();
const bMin = other.getMin();
return aMin.x <= bMax.x && aMax.x >= bMin.x && aMin.y <= bMax.y && aMax.y >= bMin.y && aMin.z <= bMax.z && aMax.z >= bMin.z;
}
_intersectsRay(ray, point) {
const tMin = tmpVecA.copy(this.getMin()).sub(ray.origin);
const tMax = tmpVecB.copy(this.getMax()).sub(ray.origin);
const dir = ray.direction;
// Ensure that we are not dividing it by zero
if (dir.x === 0) {
tMin.x = tMin.x < 0 ? -Number.MAX_VALUE : Number.MAX_VALUE;
tMax.x = tMax.x < 0 ? -Number.MAX_VALUE : Number.MAX_VALUE;
} else {
tMin.x /= dir.x;
tMax.x /= dir.x;
}
if (dir.y === 0) {
tMin.y = tMin.y < 0 ? -Number.MAX_VALUE : Number.MAX_VALUE;
tMax.y = tMax.y < 0 ? -Number.MAX_VALUE : Number.MAX_VALUE;
} else {
tMin.y /= dir.y;
tMax.y /= dir.y;
}
if (dir.z === 0) {
tMin.z = tMin.z < 0 ? -Number.MAX_VALUE : Number.MAX_VALUE;
tMax.z = tMax.z < 0 ? -Number.MAX_VALUE : Number.MAX_VALUE;
} else {
tMin.z /= dir.z;
tMax.z /= dir.z;
}
const realMin = tmpVecC.set(Math.min(tMin.x, tMax.x), Math.min(tMin.y, tMax.y), Math.min(tMin.z, tMax.z));
const realMax = tmpVecD.set(Math.max(tMin.x, tMax.x), Math.max(tMin.y, tMax.y), Math.max(tMin.z, tMax.z));
const minMax = Math.min(Math.min(realMax.x, realMax.y), realMax.z);
const maxMin = Math.max(Math.max(realMin.x, realMin.y), realMin.z);
const intersects = minMax >= maxMin && maxMin >= 0;
if (intersects) {
point.copy(ray.direction).mulScalar(maxMin).add(ray.origin);
}
return intersects;
}
_fastIntersectsRay(ray) {
const diff = tmpVecA;
const cross = tmpVecB;
const prod = tmpVecC;
const absDiff = tmpVecD;
const absDir = tmpVecE;
const rayDir = ray.direction;
diff.sub2(ray.origin, this.center);
absDiff.set(Math.abs(diff.x), Math.abs(diff.y), Math.abs(diff.z));
prod.mul2(diff, rayDir);
if (absDiff.x > this.halfExtents.x && prod.x >= 0) {
return false;
}
if (absDiff.y > this.halfExtents.y && prod.y >= 0) {
return false;
}
if (absDiff.z > this.halfExtents.z && prod.z >= 0) {
return false;
}
absDir.set(Math.abs(rayDir.x), Math.abs(rayDir.y), Math.abs(rayDir.z));
cross.cross(rayDir, diff);
cross.set(Math.abs(cross.x), Math.abs(cross.y), Math.abs(cross.z));
if (cross.x > this.halfExtents.y * absDir.z + this.halfExtents.z * absDir.y) {
return false;
}
if (cross.y > this.halfExtents.x * absDir.z + this.halfExtents.z * absDir.x) {
return false;
}
if (cross.z > this.halfExtents.x * absDir.y + this.halfExtents.y * absDir.x) {
return false;
}
return true;
}
/**
* Test if a ray intersects with the AABB.
*
* @param {Ray} ray - Ray to test against (direction must be normalized).
* @param {Vec3} [point] - If there is an intersection, the intersection point will be copied
* into here.
* @returns {boolean} True if there is an intersection.
*/ intersectsRay(ray, point) {
if (point) {
return this._intersectsRay(ray, point);
}
return this._fastIntersectsRay(ray);
}
/**
* Sets the minimum and maximum corner of the AABB. Using this function is faster than
* assigning min and max separately.
*
* @param {Vec3} min - The minimum corner of the AABB.
* @param {Vec3} max - The maximum corner of the AABB.
*/ setMinMax(min, max) {
this.center.add2(max, min).mulScalar(0.5);
this.halfExtents.sub2(max, min).mulScalar(0.5);
}
/**
* Return the minimum corner of the AABB.
*
* @returns {Vec3} Minimum corner.
*/ getMin() {
return this._min.copy(this.center).sub(this.halfExtents);
}
/**
* Return the maximum corner of the AABB.
*
* @returns {Vec3} Maximum corner.
*/ getMax() {
return this._max.copy(this.center).add(this.halfExtents);
}
/**
* Test if a point is inside a AABB.
*
* @param {Vec3} point - Point to test.
* @returns {boolean} True if the point is inside the AABB and false otherwise.
*/ containsPoint(point) {
const min = this.getMin();
const max = this.getMax();
if (point.x < min.x || point.x > max.x || point.y < min.y || point.y > max.y || point.z < min.z || point.z > max.z) {
return false;
}
return true;
}
/**
* Set an AABB to enclose the specified AABB if it were to be transformed by the specified 4x4
* matrix.
*
* @param {BoundingBox} aabb - Box to transform and enclose.
* @param {Mat4} m - Transformation matrix to apply to source AABB.
* @param {boolean} ignoreScale - If true is specified, a scale from the matrix is ignored. Defaults to false.
*/ setFromTransformedAabb(aabb, m, ignoreScale = false) {
const ac = aabb.center;
const ar = aabb.halfExtents;
const d = m.data;
let mx0 = d[0];
let mx1 = d[4];
let mx2 = d[8];
let my0 = d[1];
let my1 = d[5];
let my2 = d[9];
let mz0 = d[2];
let mz1 = d[6];
let mz2 = d[10];
// renormalize axis if scale is to be ignored
if (ignoreScale) {
let lengthSq = mx0 * mx0 + mx1 * mx1 + mx2 * mx2;
if (lengthSq > 0) {
const invLength = 1 / Math.sqrt(lengthSq);
mx0 *= invLength;
mx1 *= invLength;
mx2 *= invLength;
}
lengthSq = my0 * my0 + my1 * my1 + my2 * my2;
if (lengthSq > 0) {
const invLength = 1 / Math.sqrt(lengthSq);
my0 *= invLength;
my1 *= invLength;
my2 *= invLength;
}
lengthSq = mz0 * mz0 + mz1 * mz1 + mz2 * mz2;
if (lengthSq > 0) {
const invLength = 1 / Math.sqrt(lengthSq);
mz0 *= invLength;
mz1 *= invLength;
mz2 *= invLength;
}
}
this.center.set(d[12] + mx0 * ac.x + mx1 * ac.y + mx2 * ac.z, d[13] + my0 * ac.x + my1 * ac.y + my2 * ac.z, d[14] + mz0 * ac.x + mz1 * ac.y + mz2 * ac.z);
this.halfExtents.set(Math.abs(mx0) * ar.x + Math.abs(mx1) * ar.y + Math.abs(mx2) * ar.z, Math.abs(my0) * ar.x + Math.abs(my1) * ar.y + Math.abs(my2) * ar.z, Math.abs(mz0) * ar.x + Math.abs(mz1) * ar.y + Math.abs(mz2) * ar.z);
}
/**
* Compute the min and max bounding values to encapsulate all specified vertices.
*
* @param {number[]|Float32Array} vertices - The vertices used to compute the new size for the
* AABB.
* @param {Vec3} min - Stored computed min value.
* @param {Vec3} max - Stored computed max value.
* @param {number} [numVerts] - Number of vertices to use from the beginning of vertices array.
* All vertices are used if not specified.
*/ static computeMinMax(vertices, min, max, numVerts = vertices.length / 3) {
if (numVerts > 0) {
let minx = vertices[0];
let miny = vertices[1];
let minz = vertices[2];
let maxx = minx;
let maxy = miny;
let maxz = minz;
const n = numVerts * 3;
for(let i = 3; i < n; i += 3){
const x = vertices[i];
const y = vertices[i + 1];
const z = vertices[i + 2];
if (x < minx) minx = x;
if (y < miny) miny = y;
if (z < minz) minz = z;
if (x > maxx) maxx = x;
if (y > maxy) maxy = y;
if (z > maxz) maxz = z;
}
min.set(minx, miny, minz);
max.set(maxx, maxy, maxz);
}
}
/**
* Compute the size of the AABB to encapsulate all specified vertices.
*
* @param {number[]|Float32Array} vertices - The vertices used to compute the new size for the
* AABB.
* @param {number} [numVerts] - Number of vertices to use from the beginning of vertices array.
* All vertices are used if not specified.
*/ compute(vertices, numVerts) {
BoundingBox.computeMinMax(vertices, tmpVecA, tmpVecB, numVerts);
this.setMinMax(tmpVecA, tmpVecB);
}
/**
* Test if a Bounding Sphere is overlapping, enveloping, or inside this AABB.
*
* @param {BoundingSphere} sphere - Bounding Sphere to test.
* @returns {boolean} True if the Bounding Sphere is overlapping, enveloping, or inside the
* AABB and false otherwise.
*/ intersectsBoundingSphere(sphere) {
const sq = this._distanceToBoundingSphereSq(sphere);
if (sq <= sphere.radius * sphere.radius) {
return true;
}
return false;
}
_distanceToBoundingSphereSq(sphere) {
const boxMin = this.getMin();
const boxMax = this.getMax();
let sq = 0;
const axis = [
'x',
'y',
'z'
];
for(let i = 0; i < 3; ++i){
let out = 0;
const pn = sphere.center[axis[i]];
const bMin = boxMin[axis[i]];
const bMax = boxMax[axis[i]];
let val = 0;
if (pn < bMin) {
val = bMin - pn;
out += val * val;
}
if (pn > bMax) {
val = pn - bMax;
out += val * val;
}
sq += out;
}
return sq;
}
_expand(expandMin, expandMax) {
tmpVecA.add2(this.getMin(), expandMin);
tmpVecB.add2(this.getMax(), expandMax);
this.setMinMax(tmpVecA, tmpVecB);
}
}
export { BoundingBox };