UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

1,260 lines (997 loc) • 39.5 kB
import { assert } from "../../assert.js"; import { UINT32_MAX } from "../../binary/UINT32_MAX.js"; import { array_copy } from "../../collection/array/array_copy.js"; import { array_buffer_copy } from "../../collection/array/typed/array_buffer_copy.js"; import { aabb3_array_combine } from "../../geom/3d/aabb/aabb3_array_combine.js"; import { aabb3_compute_surface_area } from "../../geom/3d/aabb/aabb3_compute_surface_area.js"; import { max2 } from "../../math/max2.js"; import { min2 } from "../../math/min2.js"; export const COLUMN_PARENT = 6; export const COLUMN_CHILD_1 = 7; export const COLUMN_CHILD_2 = 8; export const COLUMN_HEIGHT = 9; /** * A non-leaf node have both CHILD_1 and CHILD_2 set, when CHILD_1 is not set - it's a leaf node * So we can utilize space of CHILD_2 to store USER_DATA, hence there is overlap in schema * @readonly * @type {number} */ export const COLUMN_USER_DATA = COLUMN_CHILD_2; /** * * @type {number} */ export const NULL_NODE = UINT32_MAX; /** * @readonly * @type {number} */ const CAPACITY_GROW_MULTIPLIER = 1.2; /** * @readonly * @type {number} */ const CAPACITY_GROW_MIN_STEP = 64; /** * How many words are used for a single NODE in the tree * One "word" is 4 bytes for the sake of alignment * @readonly * @type {number} */ export const ELEMENT_WORD_COUNT = 10; const ELEMENT_BYTE_COUNT = ELEMENT_WORD_COUNT * 4; /** * How many nodes can be stored in the newly constructed tree before allocation needs to take place * @readonly * @type {number} */ const INITIAL_CAPACITY = 128; /** * Tree can not contain more than this number of nodes * @type {number} */ const NODE_CAPACITY_LIMIT = Math.floor(UINT32_MAX / (ELEMENT_WORD_COUNT * 4)); /** * Bounding Volume Hierarchy. * Stores unsigned integer values at leaves, these are typically IDs or Index values. * Highly optimized both in terms of memory usage and CPU. Most of the code is inlined for speed. * No allocation are performed during usage (except for growing the tree capacity). * RAM usage: 40 bytes per node, compared with V8s per-object allocation size of 80 bytes * @see https://blog.dashlane.com/how-is-data-stored-in-v8-js-engine-memory * @class */ export class BVH { /** * * @type {ArrayBuffer} * @private */ __data_buffer = new ArrayBuffer(INITIAL_CAPACITY * ELEMENT_WORD_COUNT * 4); /** * Access raw data * Useful for serialization * If you intend to modify the data directly - make sure you fully understand the implications of doing so * @returns {ArrayBuffer} */ get data_buffer() { return this.__data_buffer; } /** * * @type {Float32Array} * @private */ __data_float32 = new Float32Array(this.__data_buffer); /** * * @return {Float32Array} */ get data_float32() { return this.__data_float32; } /** * * @type {Uint32Array} * @private */ __data_uint32 = new Uint32Array(this.__data_buffer); /** * How many nodes are currently reserved, this will grow automatically through {@link #allocate_node} method usage * @type {number} * @private */ __capacity = INITIAL_CAPACITY; /** * Number of used nodes. These are either live nodes, or node sitting in the {@link #__free} pool * @type {number} * @private */ __size = 0; /** * Indices of released nodes. Nodes are pulled from here first if available, before the whole tree gets resized * @type {number[]} * @private */ __free = []; /** * Pointer into __free array that's used as a stack, so this pointer represents top of the stack * @type {number} * @private */ __free_pointer = 0; /** * Root node of the hierarchy * @type {number} * @private */ __root = NULL_NODE; /** * * @returns {number} */ get root() { return this.__root; } /** * Make sure you understand what you're doing before using this * @param {number} v */ set root(v) { this.__root = v; } /** * Number of used nodes. Note that this is not the same as number of live nodes * Is the same as number of live nodes if compaction was performed recently * @return {number} */ get size() { return this.__size; } /** * * @returns {number} */ get node_capacity() { return this.__capacity; } /** * * @param {number} v */ set node_capacity(v) { if (this.__size > v) { throw new Error(`Can't shrink capacity to ${v}, because it's below occupancy(${this.__size}).`); } this.__set_capacity(v); } __grow_capacity() { if (this.__capacity >= NODE_CAPACITY_LIMIT) { throw new Error('Can not grow capacity, already at maximum platform limit'); } let new_capacity = Math.ceil(max2( this.__capacity * CAPACITY_GROW_MULTIPLIER, this.__capacity + CAPACITY_GROW_MIN_STEP )); if (new_capacity > NODE_CAPACITY_LIMIT) { // can not grow as much as we'd like, but we can still grow up to the limit new_capacity = NODE_CAPACITY_LIMIT; } this.__set_capacity(new_capacity); } /** * * @param {number} new_capacity in number of nodes * @private */ __set_capacity(new_capacity) { assert.isNonNegativeInteger(new_capacity, 'new_capacity'); if (this.__capacity === new_capacity) { // already at the exact desired capacity return; } const old_data_uint32 = this.__data_uint32; const new_data_buffer = new ArrayBuffer(new_capacity * ELEMENT_BYTE_COUNT); this.__data_buffer = new_data_buffer; this.__data_float32 = new Float32Array(new_data_buffer); this.__data_uint32 = new Uint32Array(new_data_buffer); if (this.__size > 0) { // copy old data into new buffer array_buffer_copy( old_data_uint32.buffer, old_data_uint32.byteOffset, new_data_buffer, 0, Math.min(this.__size * ELEMENT_BYTE_COUNT, new_data_buffer.byteLength) ); } this.__capacity = new_capacity; } /** * Trim allocated memory region to only contain allocated nodes */ trim() { if (this.__capacity > this.__size) { this.__set_capacity(this.__size); } } /** * Allocates specified number of nodes linearly. * Can only be used when the tree is explicitly empty, call {@link release_all} to achieve that. * Nodes from 0 through to count - 1 will be allocated, and you can use them immediately. * * This method is intended for static builds mainly, where we know ahead of time how many nodes we will need, and we want to avoid per-node allocation overhead. * @param {number} node_count */ allocate_linear(node_count) { assert.isNonNegativeInteger(node_count, 'node_count'); assert.equal(this.__size, 0, 'Tree is not empty'); this.node_capacity = node_count; this.__size = node_count; } /** * * @returns {number} */ allocate_node() { let result; const free_stack_top = this.__free_pointer; if (free_stack_top > 0) { // nodes in the free pool const free_index = free_stack_top - 1; result = this.__free[free_index]; this.__free_pointer = free_index; } else { result = this.__size; if (result >= this.__capacity) { // grow capacity // console.log(`#GROW`); this.__grow_capacity(); } this.__size++; } // initialize node const address = ELEMENT_WORD_COUNT * result; const float32 = this.__data_float32; // write AABB float32[address] = Number.POSITIVE_INFINITY; float32[address + 1] = Number.POSITIVE_INFINITY; float32[address + 2] = Number.POSITIVE_INFINITY; float32[address + 3] = Number.NEGATIVE_INFINITY; float32[address + 4] = Number.NEGATIVE_INFINITY; float32[address + 5] = Number.NEGATIVE_INFINITY; const uint32 = this.__data_uint32; uint32[address + COLUMN_PARENT] = NULL_NODE; // parent uint32[address + COLUMN_CHILD_1] = NULL_NODE; // child-1 uint32[address + COLUMN_CHILD_2] = NULL_NODE; // child-2 / user-data uint32[address + COLUMN_HEIGHT] = 0; // height // console.log(`#ALLOCATED: ${result}`); return result; } /** * Release memory used by the node back into the pool * NOTE: Make sure that the node is not "live" (not attached to the hierarchy), otherwise this operation may corrupt the tree * @param {number} id */ release_node(id) { assert.isNonNegativeInteger(id, 'id'); // no reset required as that's handled in the allocation method this.__free[this.__free_pointer++] = id; } /** * * @param {number} id * @returns {boolean} */ node_is_leaf(id) { assert.isNonNegativeInteger(id, 'id'); const child_1 = this.__data_uint32[ELEMENT_WORD_COUNT * id + COLUMN_CHILD_1]; return child_1 === NULL_NODE; } /** * * @param {number} id * @returns {number} */ node_get_user_data(id) { assert.isNonNegativeInteger(id, 'id'); return this.__data_uint32[ELEMENT_WORD_COUNT * id + COLUMN_USER_DATA]; } /** * * @param {number} id * @param {number} value */ node_set_user_data(id, value) { assert.isNonNegativeInteger(id, 'id'); assert.isNonNegativeInteger(value, 'value'); this.__data_uint32[ELEMENT_WORD_COUNT * id + COLUMN_USER_DATA] = value; } /** * * @param {number} id * @returns {number} */ node_get_child1(id) { assert.isNonNegativeInteger(id, 'id'); return this.__data_uint32[ELEMENT_WORD_COUNT * id + COLUMN_CHILD_1]; } /** * * @param {number} node * @param {number} child1 */ node_set_child1(node, child1) { this.__data_uint32[ELEMENT_WORD_COUNT * node + COLUMN_CHILD_1] = child1; } /** * * @param {number} id * @returns {number} */ node_get_child2(id) { assert.isNonNegativeInteger(id, 'id'); return this.__data_uint32[ELEMENT_WORD_COUNT * id + COLUMN_CHILD_2]; } /** * * @param {number} node * @param {number} child2 */ node_set_child2(node, child2) { this.__data_uint32[ELEMENT_WORD_COUNT * node + COLUMN_CHILD_2] = child2; } /** * * @param {number} id * @returns {number} */ node_get_parent(id) { assert.isNonNegativeInteger(id, 'id'); return this.__data_uint32[ELEMENT_WORD_COUNT * id + COLUMN_PARENT]; } /** * * @param {number} node * @param {number} parent */ node_set_parent(node, parent) { this.__data_uint32[ELEMENT_WORD_COUNT * node + COLUMN_PARENT] = parent; } /** * * @param {number} id * @returns {number} */ node_get_height(id) { assert.isNonNegativeInteger(id, 'id'); return this.__data_uint32[ELEMENT_WORD_COUNT * id + COLUMN_HEIGHT]; } /** * * @param {number} id * @param {number} height */ node_set_height(id, height) { assert.isNonNegativeInteger(id, 'id'); this.__data_uint32[ELEMENT_WORD_COUNT * id + COLUMN_HEIGHT] = height; } /** * * @param {number} id * @param {number[]|Float32Array} result */ node_get_aabb(id, result) { assert.isNonNegativeInteger(id, 'id'); const address = ELEMENT_WORD_COUNT * id; const float32 = this.__data_float32; result[0] = float32[address]; result[1] = float32[address + 1]; result[2] = float32[address + 2]; result[3] = float32[address + 3]; result[4] = float32[address + 4]; result[5] = float32[address + 5]; } /** * * @param {number} id * @param {number[]|ArrayLike<number>|AABB3} aabb */ node_set_aabb(id, aabb) { assert.isNonNegativeInteger(id, 'id'); assert.notNaN(aabb[0], 'aabb[0] x0'); assert.notNaN(aabb[1], 'aabb[1] y0'); assert.notNaN(aabb[2], 'aabb[2] z0'); assert.notNaN(aabb[3], 'aabb[3] x1'); assert.notNaN(aabb[4], 'aabb[4] y1'); assert.notNaN(aabb[5], 'aabb[5] z1'); const address = ELEMENT_WORD_COUNT * id; const float32 = this.__data_float32; float32[address] = aabb[0]; float32[address + 1] = aabb[1]; float32[address + 2] = aabb[2]; float32[address + 3] = aabb[3]; float32[address + 4] = aabb[4]; float32[address + 5] = aabb[5]; } /** * * @param {number} id * @param {number[]} aabb */ node_move_aabb(id, aabb) { // TODO only refit for small changes, and re-insert for large changes this.node_set_aabb(id, aabb); const parent = this.__data_uint32[ELEMENT_WORD_COUNT * id + COLUMN_PARENT]; if (parent !== NULL_NODE) { this.bubble_up_refit(parent); } } /** * * @param {number} id * @param {number} x0 * @param {number} y0 * @param {number} z0 * @param {number} x1 * @param {number} y1 * @param {number} z1 */ node_set_aabb_primitive( id, x0, y0, z0, x1, y1, z1 ) { assert.isNonNegativeInteger(id, 'id'); const address = ELEMENT_WORD_COUNT * id; const float32 = this.__data_float32; float32[address] = x0; float32[address + 1] = y0; float32[address + 2] = z0; float32[address + 3] = x1; float32[address + 4] = y1; float32[address + 5] = z1; } /** * * @param {number} id * @returns {number} */ node_get_surface_area(id) { assert.isNonNegativeInteger(id, 'id'); const address = ELEMENT_WORD_COUNT * id; const float32 = this.__data_float32; const x0 = float32[address]; const y0 = float32[address + 1]; const z0 = float32[address + 2]; const x1 = float32[address + 3]; const y1 = float32[address + 4]; const z1 = float32[address + 5]; return aabb3_compute_surface_area( x0, y0, z0, x1, y1, z1 ); } /** * * @param {number} index_a * @param {number} index_b * @returns {number} */ node_get_combined_surface_area(index_a, index_b) { const address_a = ELEMENT_WORD_COUNT * index_a; const address_b = ELEMENT_WORD_COUNT * index_b; const float32 = this.__data_float32; const a_x0 = float32[address_a]; const b_x0 = float32[address_b]; const x0 = min2(a_x0, b_x0); const a_y0 = float32[address_a + 1]; const b_y0 = float32[address_b + 1]; const y0 = min2(a_y0, b_y0); const a_z0 = float32[address_a + 2]; const b_z0 = float32[address_b + 2]; const z0 = min2(a_z0, b_z0); const a_x1 = float32[address_a + 3]; const b_x1 = float32[address_b + 3]; const x1 = max2(a_x1, b_x1); const a_y1 = float32[address_a + 4]; const b_y1 = float32[address_b + 4]; const y1 = max2(a_y1, b_y1); const a_z1 = float32[address_a + 5]; const b_z1 = float32[address_b + 5]; const z1 = max2(a_z1, b_z1); return aabb3_compute_surface_area( x0, y0, z0, x1, y1, z1, ); } /** * * @param {number} destination * @param {number} index_a * @param {number} index_b */ node_set_combined_aabb(destination, index_a, index_b) { assert.isNonNegativeInteger(destination, 'destination'); const address_a = ELEMENT_WORD_COUNT * index_a; const address_b = ELEMENT_WORD_COUNT * index_b; const address_destination = ELEMENT_WORD_COUNT * destination; const float32 = this.__data_float32; aabb3_array_combine( float32, address_destination, float32, address_a, float32, address_b, ); } /** * * @param {number} leaf * @returns {void} */ insert_leaf(leaf) { assert.isNonNegativeInteger(leaf, 'leaf'); assert.equal(this.node_is_leaf(leaf), true, 'not is not a leaf'); let uint32 = this.__data_uint32; if (this.__root === NULL_NODE) { // special case - no root, set this node as a root this.__root = leaf; uint32[leaf * ELEMENT_WORD_COUNT + COLUMN_PARENT] = NULL_NODE; return; } // Find the best sibling for this node let index = this.__root; while (this.node_is_leaf(index) === false) { const node_address = index * ELEMENT_WORD_COUNT; const child1 = uint32[node_address + COLUMN_CHILD_1]; const child2 = uint32[node_address + COLUMN_CHILD_2]; const area = this.node_get_surface_area(index); const combinedArea = this.node_get_combined_surface_area(index, leaf); // 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 let cost1; if (this.node_is_leaf(child1)) { cost1 = this.node_get_combined_surface_area(leaf, child1) + inheritanceCost; } else { const oldArea = this.node_get_surface_area(child1); const newArea = this.node_get_combined_surface_area(leaf, child1); cost1 = (newArea - oldArea) + inheritanceCost; } // Cost of descending into child2 let cost2; if (this.node_is_leaf(child2)) { cost2 = this.node_get_combined_surface_area(leaf, child2) + inheritanceCost; } else { const oldArea = this.node_get_surface_area(child2); const newArea = this.node_get_combined_surface_area(leaf, child2); cost2 = newArea - oldArea + inheritanceCost; } // 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 = uint32[sibling * ELEMENT_WORD_COUNT + COLUMN_PARENT]; const newParent = this.allocate_node(); uint32 = this.__data_uint32; // reference can be invalidated after allocation, re-bind uint32[newParent * ELEMENT_WORD_COUNT + COLUMN_PARENT] = oldParent; this.node_set_combined_aabb(newParent, leaf, sibling); uint32[newParent * ELEMENT_WORD_COUNT + COLUMN_HEIGHT] = uint32[sibling * ELEMENT_WORD_COUNT + COLUMN_HEIGHT] + 1; if (oldParent !== NULL_NODE) { // The sibling was not the root. if (uint32[oldParent * ELEMENT_WORD_COUNT + COLUMN_CHILD_1] === sibling) { uint32[oldParent * ELEMENT_WORD_COUNT + COLUMN_CHILD_1] = newParent; } else { assert.equal(uint32[oldParent * ELEMENT_WORD_COUNT + COLUMN_CHILD_2], sibling); uint32[oldParent * ELEMENT_WORD_COUNT + COLUMN_CHILD_2] = newParent; } } else { // The sibling was the root. this.__root = newParent; } uint32[newParent * ELEMENT_WORD_COUNT + COLUMN_CHILD_1] = sibling; uint32[newParent * ELEMENT_WORD_COUNT + COLUMN_CHILD_2] = leaf; uint32[sibling * ELEMENT_WORD_COUNT + COLUMN_PARENT] = newParent; uint32[leaf * ELEMENT_WORD_COUNT + COLUMN_PARENT] = newParent; // Walk back up the tree fixing heights and AABBs this.bubble_up_update(newParent); } /** * refit and update nodes up the tree. Only updates bounds * NOTE: Does not update "height" * @param {number} parent * @private */ bubble_up_refit(parent) { let index = parent; const uint32 = this.__data_uint32; do { const address = index * ELEMENT_WORD_COUNT; const child1 = uint32[address + COLUMN_CHILD_1]; const child2 = uint32[address + COLUMN_CHILD_2]; assert.notEqual(child1, NULL_NODE, 'child1 is null'); assert.notEqual(child2, NULL_NODE, 'child2 is null'); assert.notEqual(child1, index, 'child1 is equal to parent'); assert.notEqual(child2, index, 'child2 is equal to parent'); this.node_set_combined_aabb(index, child1, child2); index = uint32[address + COLUMN_PARENT]; } while (index !== NULL_NODE) } /** * refit and update nodes up the tree * @param {number} parent * @private */ bubble_up_update(parent) { let index = parent; const uint32 = this.__data_uint32; while (index !== NULL_NODE) { index = this.balance(index); const node_address = index * ELEMENT_WORD_COUNT; const child1 = uint32[node_address + COLUMN_CHILD_1]; const child2 = uint32[node_address + COLUMN_CHILD_2]; assert.notEqual(child1, NULL_NODE, 'child1 is null'); assert.notEqual(child2, NULL_NODE, 'child2 is null'); assert.notEqual(child1, index, 'child1 is equal to parent'); assert.notEqual(child2, index, 'child2 is equal to parent'); uint32[node_address + COLUMN_HEIGHT] = 1 + max2( uint32[child1 * ELEMENT_WORD_COUNT + COLUMN_HEIGHT], uint32[child2 * ELEMENT_WORD_COUNT + COLUMN_HEIGHT], ); this.node_set_combined_aabb(index, child1, child2); index = uint32[node_address + COLUMN_PARENT]; } } /** * NOTE: Leaf node is not released, make sure to call {@link #release_node} separately when you no longer need the leaf node * @param {number} leaf * @returns {void} */ remove_leaf(leaf) { assert.isNonNegativeInteger(leaf, 'leaf'); if (leaf === this.__root) { this.__root = NULL_NODE; return; } const uint32 = this.__data_uint32; const parent = uint32[leaf * ELEMENT_WORD_COUNT + COLUMN_PARENT]; const grandParent = uint32[parent * ELEMENT_WORD_COUNT + COLUMN_PARENT]; let sibling; const parent_child1 = uint32[parent * ELEMENT_WORD_COUNT + COLUMN_CHILD_1]; if (parent_child1 === leaf) { sibling = uint32[parent * ELEMENT_WORD_COUNT + COLUMN_CHILD_2]; } else { sibling = parent_child1; } if (grandParent !== NULL_NODE) { // Destroy parent and connect sibling to grandParent. const grand_parent_child1 = uint32[grandParent * ELEMENT_WORD_COUNT + COLUMN_CHILD_1]; if (grand_parent_child1 === parent) { uint32[grandParent * ELEMENT_WORD_COUNT + COLUMN_CHILD_1] = sibling; } else { uint32[grandParent * ELEMENT_WORD_COUNT + COLUMN_CHILD_2] = sibling; } uint32[sibling * ELEMENT_WORD_COUNT + COLUMN_PARENT] = grandParent; this.release_node(parent); // Adjust ancestor bounds. this.bubble_up_update(grandParent); } else { this.__root = sibling; uint32[sibling * ELEMENT_WORD_COUNT + COLUMN_PARENT] = NULL_NODE; this.release_node(parent); } } /** * Perform a left or right rotation if node A is imbalanced. * Returns the new root index. * @param {number} iA * @returns {number} * @private */ balance(iA) { assert.notEqual(iA, NULL_NODE, 'input is a null node'); //b2TreeNode* A = m_nodes + iA; const uint32 = this.__data_uint32; if (this.node_is_leaf(iA) || uint32[iA * ELEMENT_WORD_COUNT + COLUMN_HEIGHT] < 2) { return iA; } const iB = uint32[iA * ELEMENT_WORD_COUNT + COLUMN_CHILD_1]; const iC = uint32[iA * ELEMENT_WORD_COUNT + COLUMN_CHILD_2]; assert.notEqual(iA, iB, 'child1 equal to parent'); assert.notEqual(iA, iB, 'child2 equal to parent'); assert.greaterThanOrEqual(iB, 0); assert.lessThan(iB, this.node_capacity); assert.greaterThanOrEqual(iC, 0); assert.lessThan(iC, this.node_capacity); //b2TreeNode* B = m_nodes + iB; //b2TreeNode* C = m_nodes + iC; const balance = uint32[iC * ELEMENT_WORD_COUNT + COLUMN_HEIGHT] - uint32[iB * ELEMENT_WORD_COUNT + COLUMN_HEIGHT]; // Rotate C up if (balance > 1) { const iF = uint32[iC * ELEMENT_WORD_COUNT + COLUMN_CHILD_1]; const iG = uint32[iC * ELEMENT_WORD_COUNT + COLUMN_CHILD_2]; assert.notEqual(iC, iF, 'child1 equal to parent'); assert.notEqual(iC, iG, 'child2 equal to parent'); // b2TreeNode* F = m_nodes + iF; // b2TreeNode* G = m_nodes + iG; assert.greaterThanOrEqual(iF, 0); assert.lessThan(iF, this.node_capacity); assert.greaterThanOrEqual(iG, 0); assert.lessThan(iG, this.node_capacity); // Swap A and C uint32[iC * ELEMENT_WORD_COUNT + COLUMN_CHILD_1] = iA; const a_parent = uint32[iA * ELEMENT_WORD_COUNT + COLUMN_PARENT]; uint32[iC * ELEMENT_WORD_COUNT + COLUMN_PARENT] = a_parent; uint32[iA * ELEMENT_WORD_COUNT + COLUMN_PARENT] = iC; // A's old parent should point to C if (a_parent !== NULL_NODE) { assert.notEqual(a_parent, iC); if (uint32[a_parent * ELEMENT_WORD_COUNT + COLUMN_CHILD_1] === iA) { uint32[a_parent * ELEMENT_WORD_COUNT + COLUMN_CHILD_1] = iC; } else { //b2Assert(m_nodes[C->parent].child2 == iA); uint32[a_parent * ELEMENT_WORD_COUNT + COLUMN_CHILD_2] = iC; } } else { this.__root = iC; } // Rotate if (uint32[iF * ELEMENT_WORD_COUNT + COLUMN_HEIGHT] > uint32[iG * ELEMENT_WORD_COUNT + COLUMN_HEIGHT]) { uint32[iC * ELEMENT_WORD_COUNT + COLUMN_CHILD_2] = iF; uint32[iA * ELEMENT_WORD_COUNT + COLUMN_CHILD_2] = iG; uint32[iG * ELEMENT_WORD_COUNT + COLUMN_PARENT] = iA; this.node_set_combined_aabb(iA, iB, iG); this.node_set_combined_aabb(iC, iA, iF); uint32[iA * ELEMENT_WORD_COUNT + COLUMN_HEIGHT] = 1 + max2( uint32[iB * ELEMENT_WORD_COUNT + COLUMN_HEIGHT], uint32[iG * ELEMENT_WORD_COUNT + COLUMN_HEIGHT] ); uint32[iC * ELEMENT_WORD_COUNT + COLUMN_HEIGHT] = 1 + max2( uint32[iA * ELEMENT_WORD_COUNT + COLUMN_HEIGHT], uint32[iF * ELEMENT_WORD_COUNT + COLUMN_HEIGHT] ); } else { uint32[iC * ELEMENT_WORD_COUNT + COLUMN_CHILD_2] = iG; uint32[iA * ELEMENT_WORD_COUNT + COLUMN_CHILD_2] = iF; uint32[iF * ELEMENT_WORD_COUNT + COLUMN_PARENT] = iA; this.node_set_combined_aabb(iA, iB, iF); this.node_set_combined_aabb(iC, iA, iG); uint32[iA * ELEMENT_WORD_COUNT + COLUMN_HEIGHT] = 1 + max2( uint32[iB * ELEMENT_WORD_COUNT + COLUMN_HEIGHT], uint32[iF * ELEMENT_WORD_COUNT + COLUMN_HEIGHT] ); uint32[iC * ELEMENT_WORD_COUNT + COLUMN_HEIGHT] = 1 + max2( uint32[iA * ELEMENT_WORD_COUNT + COLUMN_HEIGHT], uint32[iG * ELEMENT_WORD_COUNT + COLUMN_HEIGHT] ); } assert.notEqual(iC, uint32[iC * ELEMENT_WORD_COUNT + COLUMN_CHILD_1]); assert.notEqual(iC, uint32[iC * ELEMENT_WORD_COUNT + COLUMN_CHILD_2]); return iC; } // Rotate B up if (balance < -1) { const iD = uint32[iB * ELEMENT_WORD_COUNT + COLUMN_CHILD_1]; const iE = uint32[iB * ELEMENT_WORD_COUNT + COLUMN_CHILD_2]; assert.notEqual(iB, iD, 'child1 equal to parent'); assert.notEqual(iB, iE, 'child2 equal to parent'); assert.greaterThanOrEqual(iD, 0); assert.lessThan(iD, this.node_capacity); assert.greaterThanOrEqual(iE, 0); assert.lessThan(iE, this.node_capacity); // Swap A and B uint32[iB * ELEMENT_WORD_COUNT + COLUMN_CHILD_1] = iA; const a_parent = uint32[iA * ELEMENT_WORD_COUNT + COLUMN_PARENT]; uint32[iB * ELEMENT_WORD_COUNT + COLUMN_PARENT] = a_parent; uint32[iA * ELEMENT_WORD_COUNT + COLUMN_PARENT] = iB; // A's old parent should point to B if (a_parent !== NULL_NODE) { if (uint32[a_parent * ELEMENT_WORD_COUNT + COLUMN_CHILD_1] === iA) { uint32[a_parent * ELEMENT_WORD_COUNT + COLUMN_CHILD_1] = iB; } else { //b2Assert(m_nodes[B->parent].child2 == iA); assert.equal(uint32[uint32[iB * ELEMENT_WORD_COUNT + COLUMN_PARENT] * ELEMENT_WORD_COUNT + COLUMN_CHILD_2], iA); uint32[a_parent * ELEMENT_WORD_COUNT + COLUMN_CHILD_2] = iB; } } else { this.__root = iB; } // Rotate if (uint32[iD * ELEMENT_WORD_COUNT + COLUMN_HEIGHT] > uint32[iE * ELEMENT_WORD_COUNT + COLUMN_HEIGHT]) { uint32[iB * ELEMENT_WORD_COUNT + COLUMN_CHILD_2] = iD; uint32[iA * ELEMENT_WORD_COUNT + COLUMN_CHILD_1] = iE; uint32[iE * ELEMENT_WORD_COUNT + COLUMN_PARENT] = iA; this.node_set_combined_aabb(iA, iC, iE); this.node_set_combined_aabb(iB, iA, iD); uint32[iA * ELEMENT_WORD_COUNT + COLUMN_HEIGHT] = 1 + max2( uint32[iC * ELEMENT_WORD_COUNT + COLUMN_HEIGHT], uint32[iE * ELEMENT_WORD_COUNT + COLUMN_HEIGHT] ); uint32[iB * ELEMENT_WORD_COUNT + COLUMN_HEIGHT] = 1 + max2( uint32[iA * ELEMENT_WORD_COUNT + COLUMN_HEIGHT], uint32[iD * ELEMENT_WORD_COUNT + COLUMN_HEIGHT] ); } else { uint32[iB * ELEMENT_WORD_COUNT + COLUMN_CHILD_2] = iE; uint32[iA * ELEMENT_WORD_COUNT + COLUMN_CHILD_1] = iD; uint32[iD * ELEMENT_WORD_COUNT + COLUMN_PARENT] = iA; this.node_set_combined_aabb(iA, iC, iD); this.node_set_combined_aabb(iB, iA, iE); uint32[iA * ELEMENT_WORD_COUNT + COLUMN_HEIGHT] = 1 + max2( uint32[iC * ELEMENT_WORD_COUNT + COLUMN_HEIGHT], uint32[iD * ELEMENT_WORD_COUNT + COLUMN_HEIGHT] ); uint32[iB * ELEMENT_WORD_COUNT + COLUMN_HEIGHT] = 1 + max2( uint32[iA * ELEMENT_WORD_COUNT + COLUMN_HEIGHT], uint32[iE * ELEMENT_WORD_COUNT + COLUMN_HEIGHT] ); } assert.notEqual(iB, uint32[iB * ELEMENT_WORD_COUNT + COLUMN_CHILD_1]); assert.notEqual(iB, uint32[iB * ELEMENT_WORD_COUNT + COLUMN_CHILD_2]); return iB; } assert.notEqual(iA, uint32[iA * ELEMENT_WORD_COUNT + COLUMN_CHILD_1]); assert.notEqual(iA, uint32[iA * ELEMENT_WORD_COUNT + COLUMN_CHILD_2]); // no rotation return iA; } /** * Utility method for assigning both children at once * Children must be valid nodes (non-null) * @param {number} parent * @param {number} child_1 * @param {number} child_2 */ node_assign_children(parent, child_1, child_2) { assert.notEqual(parent, child_1, 'parent is child_1'); assert.notEqual(parent, child_2, 'parent is child_2'); assert.notEqual(child_1, child_2, 'child_1 is child_2'); this.node_set_combined_aabb(parent, child_1, child_2); this.node_assign_children_only(parent, child_1, child_2); } /** * Utility method for assigning both children at once * Children must be valid nodes (non-null) * Does not update bounds. * @param {number} parent * @param {number} child_1 * @param {number} child_2 */ node_assign_children_only(parent, child_1, child_2) { assert.isNonNegativeInteger(parent, 'parent'); assert.isNonNegativeInteger(child_1, 'child_1'); assert.isNonNegativeInteger(child_2, 'child_2'); this.node_set_parent(child_1, parent); this.node_set_parent(child_2, parent); this.node_set_child1(parent, child_1); this.node_set_child2(parent, child_2); this.node_set_height(parent, 1 + Math.max( this.node_get_height(child_1), this.node_get_height(child_2), ) ); } /** * Release all nodes, this essentially resets the tree to empty state * NOTE: For performance reasons, released memory is not reset, this means that attempting to access cleared nodes' memory will yield garbage data */ release_all() { this.__root = NULL_NODE; this.__size = 0; this.__free_pointer = 0; } /** * * @param {function(node:number, tree:BVH):void} callback * @param {*} [ctx] */ traverse(callback, ctx) { let cursor = 0; const stack = []; const root = this.__root; if (root !== NULL_NODE) { stack[cursor++] = root; } const uint32 = this.__data_uint32; while (cursor > 0) { cursor--; const node = stack[cursor]; callback.call(ctx, node, this); const node_address = node * ELEMENT_WORD_COUNT; const child1 = uint32[node_address + COLUMN_CHILD_1]; const child2 = uint32[node_address + COLUMN_CHILD_2]; if (child1 !== NULL_NODE) { stack[cursor++] = child2; stack[cursor++] = child1; } } } /** * * @param {number[]} destination * @param {number} destination_offset * @returns {number} */ collect_nodes_all(destination, destination_offset) { let i = destination_offset; this.traverse(n => { destination[i++] = n; }); return i - destination_offset; } /** * Update parent and child links of a given node to point to a new location, useful for re-locating nodes * @param {number} node node to update * @param {number} destination Where updated links should point to * @private */ __move_node_links(node, destination) { const uint32 = this.__data_uint32; const source_address = node * ELEMENT_WORD_COUNT; // update children of a const child1 = uint32[source_address + COLUMN_CHILD_1]; const child2 = uint32[source_address + COLUMN_CHILD_2]; if (child1 !== NULL_NODE) { uint32[child1 * ELEMENT_WORD_COUNT + COLUMN_PARENT] = destination; uint32[child2 * ELEMENT_WORD_COUNT + COLUMN_PARENT] = destination; } // update parent of a const parent = uint32[source_address + COLUMN_PARENT]; if (parent !== NULL_NODE) { const parent_child1 = uint32[parent * ELEMENT_WORD_COUNT + COLUMN_CHILD_1]; if (parent_child1 === node) { uint32[parent * ELEMENT_WORD_COUNT + COLUMN_CHILD_1] = destination; } else { uint32[parent * ELEMENT_WORD_COUNT + COLUMN_CHILD_2] = destination; } } } /** * Swap two nodes in memory * @param {number} a * @param {number} b * @returns {boolean} */ swap_nodes(a, b) { // console.log(`swap ${a} - ${b}`) const uint32 = this.__data_uint32; const address_a = a * ELEMENT_WORD_COUNT; const address_b = b * ELEMENT_WORD_COUNT; if (uint32[address_a + COLUMN_PARENT] === b) { // attempting to swap direct parent/child, this is unsupported return false; } if (uint32[address_b + COLUMN_PARENT] === a) { // attempting to swap direct parent/child, this is unsupported return false; } this.__move_node_links(a, b); this.__move_node_links(b, a); // copy A to temp buffer array_copy(uint32, address_a, this.__free, this.__free_pointer, ELEMENT_WORD_COUNT); // write data array_copy(uint32, address_b, uint32, address_a, ELEMENT_WORD_COUNT); array_copy(this.__free, this.__free_pointer, uint32, address_b, ELEMENT_WORD_COUNT); // update root as necessary if (this.__root === a) { this.__root = b; } else if (this.__root === b) { this.__root = a; } return true; } } /** * Used for type checking * @readonly * @type {boolean} */ BVH.prototype.isBVH = true;