UNPKG

threefiz

Version:

Implementation of a physics engine based on the [Three.js](https://github.com/mrdoob/three.js) library.

271 lines (227 loc) 7.2 kB
import { Box3, Box3Helper, Color, Scene } from "three"; import TinyQueue from "tinyqueue"; import AABBNode from "./AABBNode"; import { calculateEnlargement } from "./utils"; const DEFAULT_MARGIN = 1; const leafNodes: AABBNode[] = []; const freeNodes: AABBNode[] = []; const costResetNodes: AABBNode[] = []; const bestCostCandidates = new TinyQueue<AABBNode>( [], (a, b) => a.cost - b.cost ); let n = 0; let m = 0; const helpers: Box3Helper[] = []; let RESULTS_COUNT = 0; const STACK: AABBNode[] = new Array<AABBNode>(126); let STACK_INDEX = 0; class AABBTree { root: AABBNode | null; nodes: AABBNode[] = []; margin: number; constructor(margin: number = DEFAULT_MARGIN) { this.root = null; this.margin = margin; } insert(aabb: Box3, objectID: number, name?: string): void { const node = new AABBNode(aabb, objectID, name); STACK_INDEX++; node.isLeaf = true; this.nodes.push(node); leafNodes[objectID] = node; this.insertNode(node); } private insertNode(node: AABBNode): void { if (!this.root) { this.root = node; return; } const bestLeaf = this.findBestLeaf(node); let newParent = freeNodes.pop(); if (!newParent) newParent = new AABBNode(new Box3().copy(node.aabb).union(bestLeaf.aabb)); newParent.left = bestLeaf; newParent.right = node; this.nodes.push(newParent); const oldParent = bestLeaf.parent; if (oldParent) { newParent.parent = oldParent; if (oldParent.left === bestLeaf) oldParent.left = newParent; else oldParent.right = newParent; } else this.root = newParent; bestLeaf.parent = newParent; node.parent = newParent; this.refitAncestors(node); } private findBestLeaf(node: AABBNode): AABBNode { let bestCost = Number.MAX_VALUE; let bestNode = this.root!; this.root!.cost = calculateEnlargement(bestNode, node.aabb); bestCostCandidates.push(bestNode); while (bestCostCandidates.length) { const current = this.popBestNode(); const totalCost = current.cost; if (totalCost < bestCost) { if (current.isLeaf) { bestCost = totalCost; bestNode = current; } else { const leftCost = this.calculateCost(current.left!, node); const rightCost = this.calculateCost(current.right!, node); current.left!.cost = leftCost; current.right!.cost = rightCost; if (leftCost < bestCost) bestCostCandidates.push(current.left!); if (rightCost < bestCost) bestCostCandidates.push(current.right!); } } } this.resetCosts(); return bestNode; } private popBestNode(): AABBNode { const node = bestCostCandidates.pop()!; costResetNodes.push(node); return node; } private resetCosts(): void { while (costResetNodes.length) { costResetNodes.pop()!.cost = -1; } } private calculateCost(node1: AABBNode, node2: AABBNode) { return ( calculateEnlargement(node1, node2.aabb) + this.calculateInheritanceCost(node1) ); } private calculateInheritanceCost(node: AABBNode): number { let cost = 0; let child = node; let current = node.parent; let enlargement = 0; while (current) { enlargement = current.rightEnlargement; if (child == current.left) enlargement = current.leftEnlargement; cost += enlargement; child = current; current = current.parent; } return cost; } private refitAncestors(node: AABBNode): void { let current: AABBNode | null = node; let childAABB = current.aabb; while (current !== null) { if (!current.isLeaf) { current.calculateAABB(this.margin); current.calculateEnlargement(childAABB); current.fillInclludedLeafs(); } current.calculateSurfaceArea(); childAABB = current.aabb; current = current.parent; } } query(results: Uint16Array, aabb: Box3, objectID: number): number { RESULTS_COUNT = 0; if (!this.root) return RESULTS_COUNT; STACK[0] = this.root; STACK_INDEX = 1; let node: AABBNode; while (STACK_INDEX !== 0) { node = STACK[--STACK_INDEX]; if (objectID >= node.biggestIncludedObjectId) continue; if (!node.aabb.intersectsBox(aabb)) continue; if (node.isLeaf) { results[RESULTS_COUNT++] = node.objectID!; } else { STACK[STACK_INDEX++] = node.left!; STACK[STACK_INDEX++] = node.right!; } } return RESULTS_COUNT; } update(objectID: number, aabb: Box3): void { const node = leafNodes[objectID]; if (!node) return; if (node.parent!.aabb.containsBox(aabb)) { return; // No need to update if new AABB is contained in the old one } this.removeLeaf(node); this.insertNode(node); this.refitAncestors(node); this.updateHelpers(); } private removeLeaf(node: AABBNode): void { if (node === this.root) { this.root = null; return; } const parent = node.parent!; const grandParent = parent.parent; const sibling = parent.left === node ? parent.right! : parent.left!; this.nodes[this.nodes.indexOf(parent)] = this.nodes[this.nodes.length - 1]; this.nodes.pop(); freeNodes.push(parent); parent.reset(); if (grandParent) { if (grandParent.left === parent) { grandParent.left = sibling; } else { grandParent.right = sibling; } sibling.parent = grandParent; this.refitAncestors(sibling.parent); } else { // parent is root this.root = sibling; sibling.parent = null; } } visualizeToString( node: AABBNode | null = this.root, depth: number = 0 ): string { if (!node) return ""; const indent = " ".repeat(depth); const info = node.isLeaf ? `${node.name}` : node === this.root ? "ROOT" : "B"; const rightSubtree = this.visualizeToString(node.right, depth + 1); const leftSubtree = this.visualizeToString(node.left, depth + 1); return `${rightSubtree}${indent}${info}\n${leftSubtree}`; } visualize( scene: Scene, node: AABBNode | null = this.root, color: Color = new Color("blue") ): void { if (!node) return; const helper = new Box3Helper( node.isLeaf ? node.aabb : new Box3().copy(node.aabb), color ); helper.userData = { isLeaf: node.isLeaf }; scene.add(helper); helpers.push(helper); this.visualize(scene, node.right, color); this.visualize(scene, node.left, color); } private updateHelpers(): void { if (helpers.length) { n = m = 0; const parents = this.nodes.filter((n) => !n.isLeaf); while (n < parents.length) { if (!helpers[m].userData.isLeaf) { helpers[m].box.copy(parents[n].aabb); n++; } m++; } } } } export default AABBTree;