Skip to content

Instantly share code, notes, and snippets.

@gstreetmedia
Created May 26, 2026 18:56
Show Gist options
  • Select an option

  • Save gstreetmedia/f9dae7e50fbc3d3eb5b6dbb24e961d1c to your computer and use it in GitHub Desktop.

Select an option

Save gstreetmedia/f9dae7e50fbc3d3eb5b6dbb24e961d1c to your computer and use it in GitHub Desktop.
An Oriented version of the Three.js Box3 class.
// TypeScript
import {Box3, Vector3, Matrix4, Quaternion, Plane, LineCurve3, Euler} from 'three';
/*
const obbA = new OrientedBox3().setFromBox3(boxA);
obbA.center.copy(centerA);
obbA.rotation.setFromEuler(new Euler(0, Math.PI / 4, 0));
obbA.update();
const obbB = new OrientedBox3().setFromBox3(boxB);
obbB.center.copy(centerB);
obbB.rotation.setFromEuler(new Euler(0, 0, 0));
obbB.update();
const overlap = obbA.intersectOrientedBox(obbB);
if (overlap) {
// overlap is an OrientedBox3 aligned with obbA’s orientation
// use overlap.min/max in A’s local frame, or overlap.center/halfSize/axes
}
*/
export const namedCorners = {
rightTopFront: "rightTopFront",
rightTopBack: "rightTopBack",
rightBottomFront: "rightBottomFront",
rightBottomBack: "rightBottomBack",
rightTopCenter: "rightTopCenter",
rightBottomCenter: "rightBottomCenter",
leftTopFront: "leftTopFront",
leftTopBack: "leftTopBack",
leftBottomFront: "leftBottomFront",
leftBottomBack: "leftBottomBack",
leftTopCenter: "leftTopCenter",
leftBottomCenter: "leftBottomCenter",
center: "center",
leftCenterCenter: "leftCenterCenter",
rightCenterCenter: "rightCenterCenter",
centerBottomFront: "centerBottomFront",
centerTopFront: "centerTopFront",
centerTopBack: "centerTopBack",
centerBottomBack: "centerBottomBack",
}
export class OrientedBox3 extends Box3 {
center: Vector3;
rotation: Quaternion;
scale: Vector3;
readonly matrixWorld: Matrix4;
readonly axes: [Vector3, Vector3, Vector3];
readonly halfSize: Vector3;
constructor(min?: Vector3, max?: Vector3) {
super(
min ? min.clone() : new Vector3(+Infinity, +Infinity, +Infinity),
max ? max.clone() : new Vector3(-Infinity, -Infinity, -Infinity)
);
this.center = new Vector3();
this.rotation = new Quaternion();
this.scale = new Vector3(1, 1, 1);
this.matrixWorld = new Matrix4();
this.axes = [
new Vector3(1, 0, 0),
new Vector3(0, 1, 0),
new Vector3(0, 0, 1),
];
this.halfSize = new Vector3();
if (min && max) {
super.getCenter(this.center);
const size = new Vector3();
super.getSize(size);
this.halfSize.set(size.x / 2, size.y / 2, size.z / 2);
this.update();
}
return this;
}
get dimensions() {
let width = this.max.x - this.min.x;
let height = this.max.y - this.min.y;
let length = this.max.z - this.min.z;
return {width, height, length};
}
get hasRotation() {
let {x, y, z} = this.rotation;
return x !== 0 || y !== 0 || z !== 0
}
removeRotation() {
this.rotation.setFromEuler(new Euler(0, 0, 0));
this.update();
}
copy(source: OrientedBox3): this {
this.min.copy(source.min);
this.max.copy(source.max);
this.center.copy(source.center);
this.rotation.copy(source.rotation);
this.scale.copy(source.scale);
this.halfSize.copy(source.halfSize);
// Keep derived data consistent
return this.update();
}
clone(): OrientedBox3 {
return new OrientedBox3().copy(this);
}
setFromCenterAndSize(center: Vector3, size: Vector3): this {
this.center.copy(center);
this.halfSize.set(size.x / 2, size.y / 2, size.z / 2);
this.min.set(
center.x - this.halfSize.x,
center.y - this.halfSize.y,
center.z - this.halfSize.z
);
this.max.set(
center.x + this.halfSize.x,
center.y + this.halfSize.y,
center.z + this.halfSize.z
);
return this.update();
}
setFromBox3(box: Box3): this {
this.min.copy(box.min);
this.max.copy(box.max);
const size = new Vector3();
box.getCenter(this.center);
box.getSize(size);
this.halfSize.set(size.x / 2, size.y / 2, size.z / 2);
this.rotation.identity();
this.scale.set(1, 1, 1);
return this.update();
}
update(): this {
this.matrixWorld.compose(this.center, this.rotation, this.scale);
const e = this.matrixWorld.elements;
this.axes[0].set(e[0], e[1], e[2]).normalize();
this.axes[1].set(e[4], e[5], e[6]).normalize();
this.axes[2].set(e[8], e[9], e[10]).normalize();
return this;
}
/**
* Calculates and returns the corner points of an oriented bounding box.
* The corner points are affected by the center, axes, half sizes, and scale of the box.
*
* @param {Vector3[]} [target] - Optional array to store the resulting corner points. If not provided, a new array is created.
* @return {Vector3[]} An array of 8 corner points represented as `Vector3` objects.
*/
getCorners(target?: Vector3[]): Vector3[] {
const result = target ?? new Array<Vector3>(8);
const hx = this.halfSize.x * Math.abs(this.scale.x);
const hy = this.halfSize.y * Math.abs(this.scale.y);
const hz = this.halfSize.z * Math.abs(this.scale.z);
const ax = this.axes[0], ay = this.axes[1], az = this.axes[2];
const c = this.center;
const dx = ax.clone().multiplyScalar(hx);
const dy = ay.clone().multiplyScalar(hy);
const dz = az.clone().multiplyScalar(hz);
const combos: [number, number, number][] = [
[+1, +1, +1],
[+1, +1, -1],
[+1, -1, +1],
[+1, -1, -1],
[-1, +1, +1],
[-1, +1, -1],
[-1, -1, +1],
[-1, -1, -1],
[-1, 0, 0],
[1, 0, 0],
[0, 0, 0],
[1, 1, 0], //right top center 11
[1, -1, 0], //right bottom center 12
[-1, 1, 0], //left top center 13
[-1, -1, 0] //left bottom center 14
];
for (let i = 0; i < combos.length; i++) {
const [sx, sy, sz] = combos[i];
const v = result[i] ?? new Vector3();
v.copy(c)
.addScaledVector(dx, sx)
.addScaledVector(dy, sy)
.addScaledVector(dz, sz);
result[i] = v;
}
return result;
}
/**
* Retrieves an object containing the named corners and specific calculated points of a structure.
* The method uses internal corner data and additional computations to return a comprehensive set of
* labeled corner points.
*
* @return {Object} An object where each key represents a named corner or calculated point, corresponding
* to its 3D coordinates:
* - rightTopFront: Top-front corner on the right side.
* - rightTopBack: Top-back corner on the right side.
* - rightBottomFront: Bottom-front corner on the right side.
* - rightBottomBack: Bottom-back corner on the right side.
* - leftTopFrom: Top-front corner on the left side.
* - leftTopBack: Top-back corner on the left side.
* - leftBottomFront: Bottom-front corner on the left side.
* - leftBottomBack: Bottom-back corner on the left side.
* - center: The central point of the structure.
* - leftCenterCenter: Center of the left side.
* - rightCenterCenter: Center of the right side.
* - centerBottomFront: Computed center-bottom-front point based on specific coordinates.
* - centerTopBack: Computed center-top-back point based on specific coordinates.
*/
getNamedCorners() {
let corners = this.getCorners();
return {
rightTopFront: corners[0],
rightTopBack: corners[1],
rightBottomFront: corners[2],
rightBottomBack: corners[3],
rightTopCenter: corners[11],
rightBottomCenter: corners[12],
leftTopFront: corners[4],
leftTopBack: corners[5],
leftBottomFront: corners[6],
leftBottomBack: corners[7],
leftTopCenter: corners[13],
leftBottomCenter: corners[14],
center: this.center,
leftCenterCenter: corners[8],
rightCenterCenter: corners[9],
centerBottomFront: new Vector3(this.center.x, corners[2].y, corners[2].z),
centerTopFront: new Vector3(this.center.x, corners[0].y, corners[0].z),
centerTopBack: new Vector3(this.center.x, corners[1].y, corners[1].z),
centerBottomBack: new Vector3(this.center.x, corners[3].y, corners[3].z),
}
}
/**
* Retrieves a specific named corner based on the provided name.
*
* @param {string} name - The name of the corner to retrieve.
* @return {*} The corner object associated with the given name.
*/
getNamedCorner(name) {
return this.getNamedCorners()[name]
}
/**
* Calculates the position of a given vector relative to the center point.
*
* @param {Vector3} absoluteVector3 - The absolute vector in 3D space.
* @return {Vector3} A new vector representing the position relative to the center.
*/
getRelativePosition(absoluteVector3) {
return absoluteVector3.clone().sub(this.center);
}
/**
* Calculates the absolute position by adding the given relative vector to the center position.
*
* @param {Vector3} relativeVector3 - The relative vector represented as a Vector3 object.
* @return {Vector3} The absolute position as a new Vector3 object.
*/
getAbsolutePosition(relativeVector3) {
return relativeVector3.clone().add(this.center);
}
get rightTopFront() {
return this.getNamedCorner("rightTopFront");
}
get rightTopBack() {
return this.getNamedCorner("rightTopBack");
}
get rightBottomFront() {
return this.getNamedCorner("rightBottomFront");
}
get rightBottomBack() {
return this.getNamedCorner("rightBottomBack");
}
get leftTopFront() {
return this.getNamedCorner("leftTopFront");
}
get leftTopBack() {
return this.getNamedCorner("leftTopBack");
}
get leftBottomFront() {
return this.getNamedCorner("leftBottomFront");
}
get leftBottomBack() {
return this.getNamedCorner("leftBottomBack");
}
get leftCenterCenter() {
return this.getNamedCorner("leftCenterCenter");
}
get rightCenterCenter() {
return this.getNamedCorner("rightCenterCenter");
}
get centerBottomFront() {
return this.getNamedCorner("centerBottomFront");
}
get centerTopBack() {
return this.getNamedCorner("centerTopBack");
}
getPlanes(target?: Plane[]): Plane[] {
const planes = target ?? new Array<Plane>(6);
const ax = this.axes[0], ay = this.axes[1], az = this.axes[2];
const c = this.center;
const hx = this.halfSize.x * Math.abs(this.scale.x);
const hy = this.halfSize.y * Math.abs(this.scale.y);
const hz = this.halfSize.z * Math.abs(this.scale.z);
const offsets = [
ax.clone().multiplyScalar(hx),
ax.clone().multiplyScalar(-hx),
ay.clone().multiplyScalar(hy),
ay.clone().multiplyScalar(-hy),
az.clone().multiplyScalar(hz),
az.clone().multiplyScalar(-hz),
];
const normals = [
ax.clone(),
ax.clone().negate(),
ay.clone(),
ay.clone().negate(),
az.clone(),
az.clone().negate(),
];
for (let i = 0; i < 6; i++) {
const plane = planes[i] ?? new Plane();
const p = new Vector3().copy(c).add(offsets[i]);
plane.setFromNormalAndCoplanarPoint(normals[i], p);
planes[i] = plane;
}
return planes;
}
/**
* Boolean test (like Box3.intersectsBox) but for oriented boxes.
*/
intersectsOrientedBox(box: OrientedBox3): boolean {
this.update();
box.update();
return OrientedBox3._obbIntersectsObb(this, box);
}
/**
* Returns the world-space axis-aligned bounding box (AABB) of the intersection
* volume between this OBB and another OBB. Returns null if they do not intersect.
*/
intersectOrientedBoxAABB(box: OrientedBox3, target?: Box3): Box3 | null {
this.update();
box.update();
if (!OrientedBox3._obbIntersectsObb(this, box)) return null;
const points = OrientedBox3._obbIntersectionPoints(this, box);
if (points.length === 0) return null;
const out = target ?? new Box3();
out.setFromPoints(points);
return out;
}
/**
* Box3-like `intersect`: returns the overlap as another OrientedBox3
* expressed in this box's orientation, or null if no overlap.
*/
intersectOrientedBox(
box: OrientedBox3,
target?: OrientedBox3
): OrientedBox3 | null {
this.update();
box.update();
if (!OrientedBox3._obbIntersectsObb(this, box)) return null;
const points = OrientedBox3._obbIntersectionPoints(this, box);
if (points.length === 0) return null;
// Express intersection vertices in this box's local frame
const inv = new Matrix4().copy(this.matrixWorld).invert();
const localPoints = points.map((p) => p.clone().applyMatrix4(inv));
const localBox = new Box3().setFromPoints(localPoints);
const size = new Vector3();
const centerLocal = new Vector3();
localBox.getSize(size);
localBox.getCenter(centerLocal);
// Turn center back into world space (still using this.rotation / this.scale)
const centerWorld = centerLocal.applyMatrix4(this.matrixWorld);
const result = target ?? new OrientedBox3();
result.setFromCenterAndSize(centerWorld, size);
result.rotation.copy(this.rotation);
result.scale.copy(this.scale);
return result.update();
}
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
private static _obbIntersectsObb(a: OrientedBox3, b: OrientedBox3): boolean {
const EPS = 1e-6;
const A = a.axes;
const B = b.axes;
const EA = new Vector3(
a.halfSize.x * Math.abs(a.scale.x),
a.halfSize.y * Math.abs(a.scale.y),
a.halfSize.z * Math.abs(a.scale.z)
);
const EB = new Vector3(
b.halfSize.x * Math.abs(b.scale.x),
b.halfSize.y * Math.abs(b.scale.y),
b.halfSize.z * Math.abs(b.scale.z)
);
const R: number[][] = [
[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
];
const AbsR: number[][] = [
[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
];
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
const r = A[i].dot(B[j]);
R[i][j] = r;
AbsR[i][j] = Math.abs(r) + EPS;
}
}
const tWorld = new Vector3().subVectors(b.center, a.center);
const T = [
tWorld.dot(A[0]),
tWorld.dot(A[1]),
tWorld.dot(A[2]),
];
let ra: number;
let rb: number;
// Axes A0, A1, A2
for (let i = 0; i < 3; i++) {
ra = EA.getComponent(i);
rb = EB.x * AbsR[i][0] + EB.y * AbsR[i][1] + EB.z * AbsR[i][2];
if (Math.abs(T[i]) > ra + rb) return false;
}
// Axes B0, B1, B2
for (let j = 0; j < 3; j++) {
ra = EA.x * AbsR[0][j] + EA.y * AbsR[1][j] + EA.z * AbsR[2][j];
rb = EB.getComponent(j);
const proj = T[0] * R[0][j] + T[1] * R[1][j] + T[2] * R[2][j];
if (Math.abs(proj) > ra + rb) return false;
}
// Cross products Ai x Bj
// A0 x B0
ra = EA.y * AbsR[2][0] + EA.z * AbsR[1][0];
rb = EB.y * AbsR[0][2] + EB.z * AbsR[0][1];
if (Math.abs(T[2] * R[1][0] - T[1] * R[2][0]) > ra + rb) return false;
// A0 x B1
ra = EA.y * AbsR[2][1] + EA.z * AbsR[1][1];
rb = EB.x * AbsR[0][2] + EB.z * AbsR[0][0];
if (Math.abs(T[2] * R[1][1] - T[1] * R[2][1]) > ra + rb) return false;
// A0 x B2
ra = EA.y * AbsR[2][2] + EA.z * AbsR[1][2];
rb = EB.x * AbsR[0][1] + EB.y * AbsR[0][0];
if (Math.abs(T[2] * R[1][2] - T[1] * R[2][2]) > ra + rb) return false;
// A1 x B0
ra = EA.x * AbsR[2][0] + EA.z * AbsR[0][0];
rb = EB.y * AbsR[1][2] + EB.z * AbsR[1][1];
if (Math.abs(T[0] * R[2][0] - T[2] * R[0][0]) > ra + rb) return false;
// A1 x B1
ra = EA.x * AbsR[2][1] + EA.z * AbsR[0][1];
rb = EB.x * AbsR[1][2] + EB.z * AbsR[1][0];
if (Math.abs(T[0] * R[2][1] - T[2] * R[0][1]) > ra + rb) return false;
// A1 x B2
ra = EA.x * AbsR[2][2] + EA.z * AbsR[0][2];
rb = EB.x * AbsR[1][1] + EB.y * AbsR[1][0];
if (Math.abs(T[0] * R[2][2] - T[2] * R[0][2]) > ra + rb) return false;
// A2 x B0
ra = EA.x * AbsR[1][0] + EA.y * AbsR[0][0];
rb = EB.y * AbsR[2][2] + EB.z * AbsR[2][1];
if (Math.abs(T[1] * R[0][0] - T[0] * R[1][0]) > ra + rb) return false;
// A2 x B1
ra = EA.x * AbsR[1][1] + EA.y * AbsR[0][1];
rb = EB.x * AbsR[2][2] + EB.z * AbsR[2][0];
if (Math.abs(T[1] * R[0][1] - T[0] * R[1][1]) > ra + rb) return false;
// A2 x B2
ra = EA.x * AbsR[1][2] + EA.y * AbsR[0][2];
rb = EB.x * AbsR[2][1] + EB.y * AbsR[2][0];
if (Math.abs(T[1] * R[0][2] - T[0] * R[1][2]) > ra + rb) return false;
return true;
}
private static _obbIntersectionPoints(
a: OrientedBox3,
b: OrientedBox3
): Vector3[] {
const result: Vector3[] = [];
const cornersA = a.getCorners();
const cornersB = b.getCorners();
const planesA = a.getPlanes();
const planesB = b.getPlanes();
// 1) Corners of A inside B
for (const p of cornersA) {
if (OrientedBox3._pointInsideOBB(p, b, planesB)) {
result.push(p.clone());
}
}
// 2) Corners of B inside A
for (const p of cornersB) {
if (OrientedBox3._pointInsideOBB(p, a, planesA)) {
result.push(p.clone());
}
}
const edgeIndices: [number, number][] = [
[0, 1], [0, 2], [1, 3], [2, 3],
[4, 5], [4, 6], [5, 7], [6, 7],
[0, 4], [1, 5], [2, 6], [3, 7],
];
const tmp = new Vector3();
// 3) Edges of A vs planes of B
for (const [i0, i1] of edgeIndices) {
const p0 = cornersA[i0];
const p1 = cornersA[i1];
for (const plane of planesB) {
if (
OrientedBox3._segmentIntersectsPlane(p0, p1, plane, tmp) &&
OrientedBox3._pointInsideOBB(tmp, a, planesA) &&
OrientedBox3._pointInsideOBB(tmp, b, planesB)
) {
result.push(tmp.clone());
}
}
}
// 4) Edges of B vs planes of A
for (const [i0, i1] of edgeIndices) {
const p0 = cornersB[i0];
const p1 = cornersB[i1];
for (const plane of planesA) {
if (
OrientedBox3._segmentIntersectsPlane(p0, p1, plane, tmp) &&
OrientedBox3._pointInsideOBB(tmp, a, planesA) &&
OrientedBox3._pointInsideOBB(tmp, b, planesB)
) {
result.push(tmp.clone());
}
}
}
// 5) Deduplicate
const unique: Vector3[] = [];
const EPS = 1e-5;
outer: for (const p of result) {
for (const q of unique) {
if (p.distanceToSquared(q) < EPS * EPS) continue outer;
}
unique.push(p);
}
return unique;
}
private static _pointInsideOBB(
p: Vector3,
box: OrientedBox3,
planes: Plane[]
): boolean {
const EPS = 1e-6;
for (const pl of planes) {
if (pl.distanceToPoint(p) > EPS) return false;
}
return true;
}
private static _segmentIntersectsPlane(
p0: Vector3,
p1: Vector3,
plane: Plane,
target: Vector3
): boolean {
const dir = new Vector3().subVectors(p1, p0);
const denom = plane.normal.dot(dir);
const EPS = 1e-8;
if (Math.abs(denom) < EPS) return false;
const t = -(plane.normal.dot(p0) + plane.constant) / denom;
if (t < 0 || t > 1) return false;
target.copy(p0).addScaledVector(dir, t);
return true;
}
}
export type OrientedBoxFace = 'top' | 'bottom' | 'left' | 'right' | 'front' | 'back';
export class OrientedBox3FaceHelper {
/**
* Returns the world-space center of the requested face.
*
* Face conventions:
* - left / right : -X / +X
* - bottom / top : -Y / +Y
* - back / front : -Z / +Z
*/
static getFaceCenter(box: OrientedBox3, face: OrientedBoxFace, target?: Vector3): Vector3 {
box.update();
const result = target ?? new Vector3();
const ax = box.axes[0]; // local X in world
const ay = box.axes[1]; // local Y in world
const az = box.axes[2]; // local Z in world
const hx = box.halfSize.x * Math.abs(box.scale.x);
const hy = box.halfSize.y * Math.abs(box.scale.y);
const hz = box.halfSize.z * Math.abs(box.scale.z);
result.copy(box.center);
switch (face) {
case 'right': // +X
return result.addScaledVector(ax, hx);
case 'left': // -X
return result.addScaledVector(ax, -hx);
case 'top': // +Y
return result.addScaledVector(ay, hy);
case 'bottom': // -Y
return result.addScaledVector(ay, -hy);
case 'front': // +Z
return result.addScaledVector(az, hz);
case 'back': // -Z
return result.addScaledVector(az, -hz);
}
}
/**
* Returns the opposite (matching) face for a given face.
*
* - top ↔ bottom
* - left ↔ right
* - front ↔ back
*/
static getOppositeFace(face: OrientedBoxFace): OrientedBoxFace {
switch (face) {
case 'top':
return 'bottom';
case 'bottom':
return 'top';
case 'left':
return 'right';
case 'right':
return 'left';
case 'front':
return 'back';
case 'back':
return 'front';
}
}
/**
* Creates a LineCurve3 between two named faces on the same box.
*
* Useful for ExtrudeGeometry.extrudePath, e.g. extrude from "bottom" to "top",
* or from "back" to "front".
*/
static createFaceCenterCurve(
box: OrientedBox3,
fromFace: OrientedBoxFace,
toFace?: OrientedBoxFace
): LineCurve3 {
const faceB = toFace ?? OrientedBox3FaceHelper.getOppositeFace(fromFace);
const p1 = OrientedBox3FaceHelper.getFaceCenter(box, fromFace);
const p2 = OrientedBox3FaceHelper.getFaceCenter(box, faceB);
return new LineCurve3(p1, p2);
}
/**
* Convenience helpers for the three canonical matching pairs:
* - top ↔ bottom
* - left ↔ right
* - front ↔ back
*/
static createTopBottomCurve(box: OrientedBox3, fromTop: boolean = false): LineCurve3 {
const from: OrientedBoxFace = fromTop ? 'top' : 'bottom';
const to: OrientedBoxFace = fromTop ? 'bottom' : 'top';
return OrientedBox3FaceHelper.createFaceCenterCurve(box, from, to);
}
static createLeftRightCurve(box: OrientedBox3, fromLeft: boolean = true): LineCurve3 {
const from: OrientedBoxFace = fromLeft ? 'left' : 'right';
const to: OrientedBoxFace = fromLeft ? 'right' : 'left';
return OrientedBox3FaceHelper.createFaceCenterCurve(box, from, to);
}
static createFrontBackCurve(box: OrientedBox3, fromFront: boolean = true): LineCurve3 {
const from: OrientedBoxFace = fromFront ? 'front' : 'back';
const to: OrientedBoxFace = fromFront ? 'back' : 'front';
return OrientedBox3FaceHelper.createFaceCenterCurve(box, from, to);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment