UNPKG

planck-js

Version:

2D JavaScript/TypeScript physics engine for cross-platform HTML5 game development

901 lines (749 loc) 23.2 kB
/* * Planck.js * * Copyright (c) Erin Catto, Ali Shakiba * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import { SettingsInternal as Settings } from "../Settings"; import { Pool } from "../util/Pool"; import { Vec2, Vec2Value } from "../common/Vec2"; import { AABB, AABBValue, RayCastCallback, RayCastInput } from "./AABB"; /** @internal */ const _ASSERT = typeof ASSERT === "undefined" ? false : ASSERT; /** @internal */ const math_abs = Math.abs; /** @internal */ const math_max = Math.max; export type DynamicTreeQueryCallback = (nodeId: number) => boolean; /** * A node in the dynamic tree. The client does not interact with this directly. */ export class TreeNode<T> { id: number; /** Enlarged AABB */ aabb: AABB = new AABB(); userData: T = null; parent: TreeNode<T> = null; child1: TreeNode<T> = null; child2: TreeNode<T> = null; /** 0: leaf, -1: free node */ height: number = -1; constructor(id?: number) { this.id = id; } /** @internal */ toString(): string { return this.id + ": " + this.userData; } isLeaf(): boolean { return this.child1 == null; } } /** @internal */ const poolTreeNode = new Pool<TreeNode<any>>({ create(): TreeNode<any> { return new TreeNode(); }, release(node: TreeNode<any>) { node.userData = null; node.parent = null; node.child1 = null; node.child2 = null; node.height = -1; node.id = undefined; } }); /** * A dynamic AABB tree broad-phase, inspired by Nathanael Presson's btDbvt. A * dynamic tree arranges data in a binary tree to accelerate queries such as * volume queries and ray casts. Leafs are proxies with an AABB. In the tree we * expand the proxy AABB by `aabbExtension` so that the proxy AABB is bigger * than the client object. This allows the client object to move by small * amounts without triggering a tree update. * * Nodes are pooled and relocatable, so we use node indices rather than * pointers. */ export class DynamicTree<T> { m_root: TreeNode<T>; m_lastProxyId: number; m_nodes: { [id: number]: TreeNode<T> }; constructor() { this.m_root = null; this.m_nodes = {}; this.m_lastProxyId = 0; } /** * Get proxy user data. * * @return the proxy user data or 0 if the id is invalid. */ getUserData(id: number): T { const node = this.m_nodes[id]; if (_ASSERT) console.assert(!!node); return node.userData; } /** * Get the fat AABB for a node id. * * @return the proxy user data or 0 if the id is invalid. */ getFatAABB(id: number): AABB { const node = this.m_nodes[id]; if (_ASSERT) console.assert(!!node); return node.aabb; } allocateNode(): TreeNode<T> { const node = poolTreeNode.allocate(); node.id = ++this.m_lastProxyId; this.m_nodes[node.id] = node; return node; } freeNode(node: TreeNode<T>): void { // tslint:disable-next-line:no-dynamic-delete delete this.m_nodes[node.id]; poolTreeNode.release(node); } /** * Create a proxy in the tree as a leaf node. We return the index of the node * instead of a pointer so that we can grow the node pool. * * Create a proxy. Provide a tight fitting AABB and a userData pointer. */ createProxy(aabb: AABBValue, userData: T): number { if (_ASSERT) console.assert(AABB.isValid(aabb)); const node = this.allocateNode(); node.aabb.set(aabb); // Fatten the aabb. AABB.extend(node.aabb, Settings.aabbExtension); node.userData = userData; node.height = 0; this.insertLeaf(node); return node.id; } /** * Destroy a proxy. This asserts if the id is invalid. */ destroyProxy(id: number): void { const node = this.m_nodes[id]; if (_ASSERT) console.assert(!!node); if (_ASSERT) console.assert(node.isLeaf()); this.removeLeaf(node); this.freeNode(node); } /** * Move a proxy with a swepted AABB. If the proxy has moved outside of its * fattened AABB, then the proxy is removed from the tree and re-inserted. * Otherwise the function returns immediately. * * @param d Displacement * * @return true if the proxy was re-inserted. */ moveProxy(id: number, aabb: AABBValue, d: Vec2Value): boolean { if (_ASSERT) console.assert(AABB.isValid(aabb)); if (_ASSERT) console.assert(!d || Vec2.isValid(d)); const node = this.m_nodes[id]; if (_ASSERT) console.assert(!!node); if (_ASSERT) console.assert(node.isLeaf()); if (node.aabb.contains(aabb)) { return false; } this.removeLeaf(node); node.aabb.set(aabb); // Extend AABB. aabb = node.aabb; AABB.extend(aabb, Settings.aabbExtension); // Predict AABB displacement. // const d = Vec2.mul(Settings.aabbMultiplier, displacement); if (d.x < 0.0) { aabb.lowerBound.x += d.x * Settings.aabbMultiplier; } else { aabb.upperBound.x += d.x * Settings.aabbMultiplier; } if (d.y < 0.0) { aabb.lowerBound.y += d.y * Settings.aabbMultiplier; } else { aabb.upperBound.y += d.y * Settings.aabbMultiplier; } this.insertLeaf(node); return true; } insertLeaf(leaf: TreeNode<T>): void { if (_ASSERT) console.assert(AABB.isValid(leaf.aabb)); if (this.m_root == null) { this.m_root = leaf; this.m_root.parent = null; return; } // Find the best sibling for this node const leafAABB = leaf.aabb; let index = this.m_root; while (!index.isLeaf()) { const child1 = index.child1; const child2 = index.child2; const area = index.aabb.getPerimeter(); const combinedArea = AABB.combinedPerimeter(index.aabb, leafAABB); // Cost of creating a new parent for this node and the new leaf const cost = 2.0 * combinedArea; // Minimum cost of pushing the leaf further down the tree const inheritanceCost = 2.0 * (combinedArea - area); // Cost of descending into child1 const newArea1 = AABB.combinedPerimeter(leafAABB, child1.aabb); let cost1 = newArea1 + inheritanceCost; if (!child1.isLeaf()) { const oldArea = child1.aabb.getPerimeter(); cost1 -= oldArea; } // Cost of descending into child2 const newArea2 = AABB.combinedPerimeter(leafAABB, child2.aabb); let cost2 = newArea2 + inheritanceCost; if (!child2.isLeaf()) { const oldArea = child2.aabb.getPerimeter(); cost2 -= oldArea; } // Descend according to the minimum cost. if (cost < cost1 && cost < cost2) { break; } // Descend if (cost1 < cost2) { index = child1; } else { index = child2; } } const sibling = index; // Create a new parent. const oldParent = sibling.parent; const newParent = this.allocateNode(); newParent.parent = oldParent; newParent.userData = null; newParent.aabb.combine(leafAABB, sibling.aabb); newParent.height = sibling.height + 1; if (oldParent != null) { // The sibling was not the root. if (oldParent.child1 === sibling) { oldParent.child1 = newParent; } else { oldParent.child2 = newParent; } newParent.child1 = sibling; newParent.child2 = leaf; sibling.parent = newParent; leaf.parent = newParent; } else { // The sibling was the root. newParent.child1 = sibling; newParent.child2 = leaf; sibling.parent = newParent; leaf.parent = newParent; this.m_root = newParent; } // Walk back up the tree fixing heights and AABBs index = leaf.parent; while (index != null) { index = this.balance(index); const child1 = index.child1; const child2 = index.child2; if (_ASSERT) console.assert(child1 != null); if (_ASSERT) console.assert(child2 != null); index.height = 1 + math_max(child1.height, child2.height); index.aabb.combine(child1.aabb, child2.aabb); index = index.parent; } // validate(); } removeLeaf(leaf: TreeNode<T>): void { if (leaf === this.m_root) { this.m_root = null; return; } const parent = leaf.parent; const grandParent = parent.parent; let sibling; if (parent.child1 === leaf) { sibling = parent.child2; } else { sibling = parent.child1; } if (grandParent != null) { // Destroy parent and connect sibling to grandParent. if (grandParent.child1 === parent) { grandParent.child1 = sibling; } else { grandParent.child2 = sibling; } sibling.parent = grandParent; this.freeNode(parent); // Adjust ancestor bounds. let index = grandParent; while (index != null) { index = this.balance(index); const child1 = index.child1; const child2 = index.child2; index.aabb.combine(child1.aabb, child2.aabb); index.height = 1 + math_max(child1.height, child2.height); index = index.parent; } } else { this.m_root = sibling; sibling.parent = null; this.freeNode(parent); } // validate(); } /** * Perform a left or right rotation if node A is imbalanced. Returns the new * root index. */ balance(iA: TreeNode<T>): TreeNode<T> { if (_ASSERT) console.assert(iA != null); const A = iA; if (A.isLeaf() || A.height < 2) { return iA; } const B = A.child1; const C = A.child2; const balance = C.height - B.height; // Rotate C up if (balance > 1) { const F = C.child1; const G = C.child2; // Swap A and C C.child1 = A; C.parent = A.parent; A.parent = C; // A's old parent should point to C if (C.parent != null) { if (C.parent.child1 === iA) { C.parent.child1 = C; } else { C.parent.child2 = C; } } else { this.m_root = C; } // Rotate if (F.height > G.height) { C.child2 = F; A.child2 = G; G.parent = A; A.aabb.combine(B.aabb, G.aabb); C.aabb.combine(A.aabb, F.aabb); A.height = 1 + math_max(B.height, G.height); C.height = 1 + math_max(A.height, F.height); } else { C.child2 = G; A.child2 = F; F.parent = A; A.aabb.combine(B.aabb, F.aabb); C.aabb.combine(A.aabb, G.aabb); A.height = 1 + math_max(B.height, F.height); C.height = 1 + math_max(A.height, G.height); } return C; } // Rotate B up if (balance < -1) { const D = B.child1; const E = B.child2; // Swap A and B B.child1 = A; B.parent = A.parent; A.parent = B; // A's old parent should point to B if (B.parent != null) { if (B.parent.child1 === A) { B.parent.child1 = B; } else { B.parent.child2 = B; } } else { this.m_root = B; } // Rotate if (D.height > E.height) { B.child2 = D; A.child1 = E; E.parent = A; A.aabb.combine(C.aabb, E.aabb); B.aabb.combine(A.aabb, D.aabb); A.height = 1 + math_max(C.height, E.height); B.height = 1 + math_max(A.height, D.height); } else { B.child2 = E; A.child1 = D; D.parent = A; A.aabb.combine(C.aabb, D.aabb); B.aabb.combine(A.aabb, E.aabb); A.height = 1 + math_max(C.height, D.height); B.height = 1 + math_max(A.height, E.height); } return B; } return A; } /** * Compute the height of the binary tree in O(N) time. Should not be called * often. */ getHeight(): number { if (this.m_root == null) { return 0; } return this.m_root.height; } /** * Get the ratio of the sum of the node areas to the root area. */ getAreaRatio(): number { if (this.m_root == null) { return 0.0; } const root = this.m_root; const rootArea = root.aabb.getPerimeter(); let totalArea = 0.0; let node; const it = this.iteratorPool.allocate().preorder(this.m_root); while (node = it.next()) { if (node.height < 0) { // Free node in pool continue; } totalArea += node.aabb.getPerimeter(); } this.iteratorPool.release(it); return totalArea / rootArea; } /** * Compute the height of a sub-tree. */ computeHeight(id?: number): number { let node; if (typeof id !== "undefined") { node = this.m_nodes[id]; } else { node = this.m_root; } // if (_ASSERT) console.assert(0 <= id && id < this.m_nodeCapacity); if (node.isLeaf()) { return 0; } const height1 = this.computeHeight(node.child1.id); const height2 = this.computeHeight(node.child2.id); return 1 + math_max(height1, height2); } validateStructure(node: TreeNode<T>): void { if (node == null) { return; } if (node === this.m_root) { if (_ASSERT) console.assert(node.parent == null); } const child1 = node.child1; const child2 = node.child2; if (node.isLeaf()) { if (_ASSERT) console.assert(child1 == null); if (_ASSERT) console.assert(child2 == null); if (_ASSERT) console.assert(node.height === 0); return; } // if (_ASSERT) console.assert(0 <= child1 && child1 < this.m_nodeCapacity); // if (_ASSERT) console.assert(0 <= child2 && child2 < this.m_nodeCapacity); if (_ASSERT) console.assert(child1.parent === node); if (_ASSERT) console.assert(child2.parent === node); this.validateStructure(child1); this.validateStructure(child2); } validateMetrics(node: TreeNode<T>): void { if (node == null) { return; } const child1 = node.child1; const child2 = node.child2; if (node.isLeaf()) { if (_ASSERT) console.assert(child1 == null); if (_ASSERT) console.assert(child2 == null); if (_ASSERT) console.assert(node.height === 0); return; } // if (_ASSERT) console.assert(0 <= child1 && child1 < this.m_nodeCapacity); // if (_ASSERT) console.assert(0 <= child2 && child2 < this.m_nodeCapacity); const height1 = child1.height; const height2 = child2.height; const height = 1 + math_max(height1, height2); if (_ASSERT) console.assert(node.height === height); const aabb = new AABB(); aabb.combine(child1.aabb, child2.aabb); if (_ASSERT) console.assert(AABB.areEqual(aabb, node.aabb)); this.validateMetrics(child1); this.validateMetrics(child2); } /** * Validate this tree. For testing. */ validate(): void { if (!_ASSERT) return; this.validateStructure(this.m_root); this.validateMetrics(this.m_root); console.assert(this.getHeight() === this.computeHeight()); } /** * Get the maximum balance of an node in the tree. The balance is the difference * in height of the two children of a node. */ getMaxBalance(): number { let maxBalance = 0; let node; const it = this.iteratorPool.allocate().preorder(this.m_root); while (node = it.next()) { if (node.height <= 1) { continue; } if (_ASSERT) console.assert(!node.isLeaf()); const balance = math_abs(node.child2.height - node.child1.height); maxBalance = math_max(maxBalance, balance); } this.iteratorPool.release(it); return maxBalance; } /** * Build an optimal tree. Very expensive. For testing. */ rebuildBottomUp(): void { const nodes = []; let count = 0; // Build array of leaves. Free the rest. let node; const it = this.iteratorPool.allocate().preorder(this.m_root); while (node = it.next()) { if (node.height < 0) { // free node in pool continue; } if (node.isLeaf()) { node.parent = null; nodes[count] = node; ++count; } else { this.freeNode(node); } } this.iteratorPool.release(it); while (count > 1) { let minCost = Infinity; let iMin = -1; let jMin = -1; for (let i = 0; i < count; ++i) { const aabbi = nodes[i].aabb; for (let j = i + 1; j < count; ++j) { const aabbj = nodes[j].aabb; const cost = AABB.combinedPerimeter(aabbi, aabbj); if (cost < minCost) { iMin = i; jMin = j; minCost = cost; } } } const child1 = nodes[iMin]; const child2 = nodes[jMin]; const parent = this.allocateNode(); parent.child1 = child1; parent.child2 = child2; parent.height = 1 + math_max(child1.height, child2.height); parent.aabb.combine(child1.aabb, child2.aabb); parent.parent = null; child1.parent = parent; child2.parent = parent; nodes[jMin] = nodes[count - 1]; nodes[iMin] = parent; --count; } this.m_root = nodes[0]; if (_ASSERT) this.validate(); } /** * Shift the world origin. Useful for large worlds. The shift formula is: * position -= newOrigin * * @param newOrigin The new origin with respect to the old origin */ shiftOrigin(newOrigin: Vec2Value): void { // Build array of leaves. Free the rest. let node; const it = this.iteratorPool.allocate().preorder(this.m_root); while (node = it.next()) { const aabb = node.aabb; aabb.lowerBound.x -= newOrigin.x; aabb.lowerBound.y -= newOrigin.y; aabb.upperBound.x -= newOrigin.x; aabb.upperBound.y -= newOrigin.y; } this.iteratorPool.release(it); } /** * Query an AABB for overlapping proxies. The callback class is called for each * proxy that overlaps the supplied AABB. */ query(aabb: AABBValue, queryCallback: DynamicTreeQueryCallback): void { if (_ASSERT) console.assert(typeof queryCallback === "function"); const stack = this.stackPool.allocate(); stack.push(this.m_root); while (stack.length > 0) { const node = stack.pop(); if (node == null) { continue; } if (AABB.testOverlap(node.aabb, aabb)) { if (node.isLeaf()) { const proceed = queryCallback(node.id); if (proceed === false) { return; } } else { stack.push(node.child1); stack.push(node.child2); } } } this.stackPool.release(stack); } /** * Ray-cast against the proxies in the tree. This relies on the callback to * perform a exact ray-cast in the case were the proxy contains a shape. The * callback also performs the any collision filtering. This has performance * roughly equal to k * log(n), where k is the number of collisions and n is the * number of proxies in the tree. * * @param input The ray-cast input data. The ray extends from `p1` to `p1 + maxFraction * (p2 - p1)`. * @param rayCastCallback A function that is called for each proxy that is hit by the ray. If the return value is a positive number it will update the maxFraction of the ray cast input, and if it is zero it will terminate they ray cast. */ rayCast(input: RayCastInput, rayCastCallback: RayCastCallback): void { // TODO: GC if (_ASSERT) console.assert(typeof rayCastCallback === "function"); const p1 = input.p1; const p2 = input.p2; const r = Vec2.sub(p2, p1); if (_ASSERT) console.assert(r.lengthSquared() > 0.0); r.normalize(); // v is perpendicular to the segment. const v = Vec2.crossNumVec2(1.0, r); const abs_v = Vec2.abs(v); // Separating axis for segment (Gino, p80). // |dot(v, p1 - c)| > dot(|v|, h) let maxFraction = input.maxFraction; // Build a bounding box for the segment. const segmentAABB = new AABB(); let t = Vec2.combine((1 - maxFraction), p1, maxFraction, p2); segmentAABB.combinePoints(p1, t); const stack = this.stackPool.allocate(); const subInput = this.inputPool.allocate(); stack.push(this.m_root); while (stack.length > 0) { const node = stack.pop(); if (node == null) { continue; } if (AABB.testOverlap(node.aabb, segmentAABB) === false) { continue; } // Separating axis for segment (Gino, p80). // |dot(v, p1 - c)| > dot(|v|, h) const c = node.aabb.getCenter(); const h = node.aabb.getExtents(); const separation = math_abs(Vec2.dot(v, Vec2.sub(p1, c))) - Vec2.dot(abs_v, h); if (separation > 0.0) { continue; } if (node.isLeaf()) { subInput.p1 = Vec2.clone(input.p1); subInput.p2 = Vec2.clone(input.p2); subInput.maxFraction = maxFraction; const value = rayCastCallback(subInput, node.id); if (value === 0.0) { // The client has terminated the ray cast. break; } else if (value > 0.0) { // update segment bounding box. maxFraction = value; t = Vec2.combine((1 - maxFraction), p1, maxFraction, p2); segmentAABB.combinePoints(p1, t); } } else { stack.push(node.child1); stack.push(node.child2); } } this.stackPool.release(stack); this.inputPool.release(subInput); } private inputPool: Pool<RayCastInput> = new Pool<RayCastInput>({ create(): RayCastInput { // tslint:disable-next-line:no-object-literal-type-assertion return {} as RayCastInput; }, release(stack: RayCastInput): void { } }); private stackPool: Pool<Array<TreeNode<T>>> = new Pool<Array<TreeNode<T>>>({ create(): Array<TreeNode<T>> { return []; }, release(stack: Array<TreeNode<T>>): void { stack.length = 0; } }); private iteratorPool: Pool<Iterator<T>> = new Pool<Iterator<T>>({ create(): Iterator<T> { return new Iterator(); }, release(iterator: Iterator<T>): void { iterator.close(); } }); } /** @internal */ class Iterator<T> { parents: Array<TreeNode<T>> = []; states: number[] = []; preorder(root: TreeNode<T>): Iterator<T> { this.parents.length = 0; this.parents.push(root); this.states.length = 0; this.states.push(0); return this; } next(): TreeNode<T> { while (this.parents.length > 0) { const i = this.parents.length - 1; const node = this.parents[i]; if (this.states[i] === 0) { this.states[i] = 1; return node; } if (this.states[i] === 1) { this.states[i] = 2; if (node.child1) { this.parents.push(node.child1); this.states.push(1); return node.child1; } } if (this.states[i] === 2) { this.states[i] = 3; if (node.child2) { this.parents.push(node.child2); this.states.push(1); return node.child2; } } this.parents.pop(); this.states.pop(); } } close(): void { this.parents.length = 0; } }