import { Broadphase } from '../collision/Broadphase' import type { AABB } from '../collision/AABB' import type { Body } from '../objects/Body' import type { World } from '../world/World' /** * Sweep and prune broadphase along one axis. */ export class SAPBroadphase extends Broadphase { /** * List of bodies currently in the broadphase. */ axisList: Body[] /** * The world to search in. */ world: World | null /** * Axis to sort the bodies along. * Set to 0 for x axis, and 1 for y axis. * For best performance, pick the axis where bodies are most distributed. */ axisIndex: 0 | 1 | 2 private _addBodyHandler: (event: { body: Body }) => void private _removeBodyHandler: (event: { body: Body }) => void /** * Check if the bounds of two bodies overlap, along the given SAP axis. */ static checkBounds(bi: Body, bj: Body, axisIndex: 0 | 1 | 2): boolean { let biPos: number let bjPos: number if (axisIndex === 0) { biPos = bi.position.x bjPos = bj.position.x } else if (axisIndex === 1) { biPos = bi.position.y bjPos = bj.position.y } else if (axisIndex === 2) { biPos = bi.position.z bjPos = bj.position.z } const ri = bi.boundingRadius, rj = bj.boundingRadius, boundA2 = biPos! + ri, boundB1 = bjPos! - rj return boundB1 < boundA2 } // Note: these are identical, save for x/y/z lowerbound /** * insertionSortX */ static insertionSortX(a: Body[]): Body[] { for (let i = 1, l = a.length; i < l; i++) { const v = a[i] let j: number for (j = i - 1; j >= 0; j--) { if (a[j].aabb.lowerBound.x <= v.aabb.lowerBound.x) { break } a[j + 1] = a[j] } a[j + 1] = v } return a } /** * insertionSortY */ static insertionSortY(a: Body[]): Body[] { for (let i = 1, l = a.length; i < l; i++) { const v = a[i] let j: number for (j = i - 1; j >= 0; j--) { if (a[j].aabb.lowerBound.y <= v.aabb.lowerBound.y) { break } a[j + 1] = a[j] } a[j + 1] = v } return a } /** * insertionSortZ */ static insertionSortZ(a: Body[]): Body[] { for (let i = 1, l = a.length; i < l; i++) { const v = a[i] let j: number for (j = i - 1; j >= 0; j--) { if (a[j].aabb.lowerBound.z <= v.aabb.lowerBound.z) { break } a[j + 1] = a[j] } a[j + 1] = v } return a } constructor(world: World) { super() this.axisList = [] this.world = null this.axisIndex = 0 const axisList = this.axisList this._addBodyHandler = (event: { body: Body }) => { axisList.push(event.body) } this._removeBodyHandler = (event: { body: Body }) => { const idx = axisList.indexOf(event.body) if (idx !== -1) { axisList.splice(idx, 1) } } if (world) { this.setWorld(world) } } /** * Change the world */ setWorld(world: World): void { // Clear the old axis array this.axisList.length = 0 // Add all bodies from the new world for (let i = 0; i < world.bodies.length; i++) { this.axisList.push(world.bodies[i]) } // Remove old handlers, if any world.removeEventListener('addBody', this._addBodyHandler) world.removeEventListener('removeBody', this._removeBodyHandler) // Add handlers to update the list of bodies. world.addEventListener('addBody', this._addBodyHandler) world.addEventListener('removeBody', this._removeBodyHandler) this.world = world this.dirty = true } /** * Collect all collision pairs */ collisionPairs(world: World, p1: Body[], p2: Body[]): void { const bodies = this.axisList const N = bodies.length const axisIndex = this.axisIndex let i let j if (this.dirty) { this.sortList() this.dirty = false } // Look through the list for (i = 0; i !== N; i++) { const bi = bodies[i] for (j = i + 1; j < N; j++) { const bj = bodies[j] if (!this.needBroadphaseCollision(bi, bj)) { continue } if (!SAPBroadphase.checkBounds(bi, bj, axisIndex)) { break } this.intersectionTest(bi, bj, p1, p2) } } } sortList(): void { const axisList = this.axisList const axisIndex = this.axisIndex const N = axisList.length // Update AABBs for (let i = 0; i !== N; i++) { const bi = axisList[i] if (bi.aabbNeedsUpdate) { bi.updateAABB() } } // Sort the list if (axisIndex === 0) { SAPBroadphase.insertionSortX(axisList) } else if (axisIndex === 1) { SAPBroadphase.insertionSortY(axisList) } else if (axisIndex === 2) { SAPBroadphase.insertionSortZ(axisList) } } /** * Computes the variance of the body positions and estimates the best axis to use. * Will automatically set property `axisIndex`. */ autoDetectAxis(): void { let sumX = 0 let sumX2 = 0 let sumY = 0 let sumY2 = 0 let sumZ = 0 let sumZ2 = 0 const bodies = this.axisList const N = bodies.length const invN = 1 / N for (let i = 0; i !== N; i++) { const b = bodies[i] const centerX = b.position.x sumX += centerX sumX2 += centerX * centerX const centerY = b.position.y sumY += centerY sumY2 += centerY * centerY const centerZ = b.position.z sumZ += centerZ sumZ2 += centerZ * centerZ } const varianceX = sumX2 - sumX * sumX * invN const varianceY = sumY2 - sumY * sumY * invN const varianceZ = sumZ2 - sumZ * sumZ * invN if (varianceX > varianceY) { if (varianceX > varianceZ) { this.axisIndex = 0 } else { this.axisIndex = 2 } } else if (varianceY > varianceZ) { this.axisIndex = 1 } else { this.axisIndex = 2 } } /** * Returns all the bodies within an AABB. * @param result An array to store resulting bodies in. */ aabbQuery(world: World, aabb: AABB, result: Body[] = []): Body[] { if (this.dirty) { this.sortList() this.dirty = false } const axisIndex = this.axisIndex let axis: 'x' | 'y' | 'z' = 'x' if (axisIndex === 1) { axis = 'y' } if (axisIndex === 2) { axis = 'z' } const axisList = this.axisList const lower = aabb.lowerBound[axis] const upper = aabb.upperBound[axis] for (let i = 0; i < axisList.length; i++) { const b = axisList[i] if (b.aabbNeedsUpdate) { b.updateAABB() } if (b.aabb.overlaps(aabb)) { result.push(b) } } return result } }