Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@
- [x] Octree partitioning for 3D space
- [x] Circle collision helpers
- [x] Raycasting utilities
- [ ] Bounding volume hierarchy (BVH) builder
- [x] Bounding volume hierarchy (BVH) builder
**Data structures**
- [x] Binary heap priority queue
- [x] Disjoint set union (union-find)
Expand Down
50 changes: 50 additions & 0 deletions docs/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ export const examples: {
readonly circleAabbCollision: 'examples/circle.ts';
readonly circleSegmentIntersection: 'examples/circle.ts';
readonly sweptAABB: 'examples/sweptAabb.ts';
readonly buildBvh: 'examples/bvh.ts';
readonly queryBvh: 'examples/bvh.ts';
readonly raycastBvh: 'examples/bvh.ts';
};
readonly search: {
readonly fuzzySearch: 'examples/search.ts';
Expand Down Expand Up @@ -793,6 +796,42 @@ export class Octree<T = unknown> {
querySphere(center: Point3D, radius: number): Array<Point3D & { data?: T }>;
}

/**
* Bounding volume hierarchy for accelerating 3D spatial queries.
* Use for: ray picking, collision broad-phase, visibility culling.
* Performance: O(log n) traversal for balanced trees.
* Import: spatial/bvh.ts
*/
export type BvhAxis = 'x' | 'y' | 'z';
export interface BvhEntry<T> { item: T; bounds: Box3 }
export interface BvhLeaf<T> { type: 'leaf'; bounds: Box3; entries: ReadonlyArray<BvhEntry<T>> }
export interface BvhBranch<T> {
type: 'branch';
bounds: Box3;
axis: BvhAxis;
left: BvhNode<T>;
right: BvhNode<T>;
}
export type BvhNode<T> = BvhLeaf<T> | BvhBranch<T>;
export interface BuildBvhOptions<T> {
getBounds(item: T): Box3;
maxLeafSize?: number;
maxDepth?: number;
}
export interface BvhRaycastHit<T> { entry: BvhEntry<T>; distance: number }
export function buildBvh<T>(items: ReadonlyArray<T>, options: BuildBvhOptions<T>): BvhNode<T> | null;
export function queryBvh<T>(
node: BvhNode<T> | null,
query: Box3,
results?: Array<BvhEntry<T>>
): Array<BvhEntry<T>>;
export function raycastBvh<T>(
node: BvhNode<T> | null,
ray: Ray3D,
intersect: (entry: BvhEntry<T>, ray: Ray3D) => number | null,
maxDistance?: number
): BvhRaycastHit<T> | null;

/**
* Axis-aligned bounding box helpers.
* Use for: broad collisions, viewport culling, layout math.
Expand Down Expand Up @@ -3636,6 +3675,12 @@ export interface Vector2D {
y: number;
}

export interface Vector3D {
x: number;
y: number;
z: number;
}

export interface Rect {
x: number;
y: number;
Expand All @@ -3652,6 +3697,11 @@ export interface Box3 {
depth: number;
}

export interface Ray3D {
origin: Point3D;
direction: Vector3D;
}

export interface Graph {
[key: string]: Array<{ node: string; weight?: number }>;
}
Expand Down
66 changes: 66 additions & 0 deletions examples/bvh.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { buildBvh, queryBvh, raycastBvh } from '../src/index.js';

const objects = [
{ id: 'crate', bounds: { x: 0, y: 0, z: 0, width: 2, height: 2, depth: 2 } },
{ id: 'barrel', bounds: { x: 4, y: 0.5, z: 1, width: 1.5, height: 3, depth: 1.5 } },
{ id: 'pillar', bounds: { x: 8, y: 0, z: 0, width: 1, height: 6, depth: 1 } },
];

const bvh = buildBvh(objects, {
getBounds: (item) => item.bounds,
});

const queryResult = queryBvh(bvh, {
x: -1,
y: -1,
z: -1,
width: 5,
height: 5,
depth: 5,
});
console.log('intersecting ids', queryResult.map((entry) => entry.item.id));

const hit = raycastBvh(
bvh,
{ origin: { x: -5, y: 1, z: 1 }, direction: { x: 1, y: 0, z: 0 } },
(entry, ray) => rayAabb(ray, entry.bounds)
);
console.log('first hit', hit?.entry.item.id, 'at distance', hit?.distance);

function rayAabb(
ray: { origin: { x: number; y: number; z: number }; direction: { x: number; y: number; z: number } },
box: { x: number; y: number; z: number; width: number; height: number; depth: number }
): number | null {
const invDirX = 1 / ray.direction.x;
const invDirY = 1 / ray.direction.y;
const invDirZ = 1 / ray.direction.z;

let tMin = ((ray.direction.x >= 0 ? box.x : box.x + box.width) - ray.origin.x) * invDirX;
let tMax = ((ray.direction.x >= 0 ? box.x + box.width : box.x) - ray.origin.x) * invDirX;

const tyMin = ((ray.direction.y >= 0 ? box.y : box.y + box.height) - ray.origin.y) * invDirY;
const tyMax = ((ray.direction.y >= 0 ? box.y + box.height : box.y) - ray.origin.y) * invDirY;

if (tMin > tyMax || tyMin > tMax) {
return null;
}

if (tyMin > tMin) tMin = tyMin;
if (tyMax < tMax) tMax = tyMax;

const tzMin = ((ray.direction.z >= 0 ? box.z : box.z + box.depth) - ray.origin.z) * invDirZ;
const tzMax = ((ray.direction.z >= 0 ? box.z + box.depth : box.z) - ray.origin.z) * invDirZ;

if (tMin > tzMax || tzMin > tMax) {
return null;
}

if (tzMin > tMin) tMin = tzMin;
if (tzMax < tMax) tMax = tzMax;

if (tMax < 0) {
return null;
}

return tMin >= 0 ? tMin : tMax;
}
18 changes: 18 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ export const examples = {
raycastSegment: 'examples/raycast.ts',
raycastAabb: 'examples/raycast.ts',
sweptAABB: 'examples/sweptAabb.ts',
buildBvh: 'examples/bvh.ts',
queryBvh: 'examples/bvh.ts',
raycastBvh: 'examples/bvh.ts',
},
search: {
fuzzySearch: 'examples/search.ts',
Expand Down Expand Up @@ -408,6 +411,21 @@ export { Quadtree } from './spatial/quadtree.js';
*/
export { Octree } from './spatial/octree.js';

/**
* Bounding volume hierarchy builder for accelerating spatial queries.
*
* Example file: examples/bvh.ts
*/
export { buildBvh, queryBvh, raycastBvh } from './spatial/bvh.js';
export type {
BvhNode,
BvhLeaf,
BvhBranch,
BvhEntry,
BvhAxis,
BvhRaycastHit,
} from './spatial/bvh.js';

/**
* Axis-aligned bounding box collision detection helpers.
*
Expand Down
Loading