@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
498 lines (398 loc) • 14.2 kB
JavaScript
import { assert } from "../../../assert.js";
import { ceilPowerOfTwo } from "../../../binary/operations/ceilPowerOfTwo.js";
import { array_copy } from "../../../collection/array/array_copy.js";
import { array_swap } from "../../../collection/array/array_swap.js";
import { SCRATCH_UINT32_TRAVERSAL_STACK } from "../../../collection/SCRATCH_UINT32_TRAVERSAL_STACK.js";
import { aabb3_array_combine } from "../../../geom/3d/aabb/aabb3_array_combine.js";
import { aabb3_array_set } from "../../../geom/3d/aabb/aabb3_array_set.js";
import { aabb3_compute_half_surface_area } from "../../../geom/3d/aabb/aabb3_compute_half_surface_area.js";
import { v3_morton_encode_bounded } from "../../../geom/3d/morton/v3_morton_encode_bounded.js";
/**
* @readonly
* @type {number}
*/
export const BVH_BOX_BYTE_SIZE = 6;
/**
* In words (4 byte)
* @readonly
* @type {number}
*/
export const BVH_BINARY_NODE_SIZE = 6;
/**
* @readonly
* @type {number}
*/
export const BVH_LEAF_NODE_SIZE = 7;
/**
*
* @param {Float32Array} data
* @param {number} destination
* @param {number} source
*/
function copy_box_zero_size(data, destination, source) {
assert.isNonNegativeInteger(destination, 'destination');
assert.isNonNegativeInteger(source, 'source');
const x = data[source];
const y = data[source + 1];
const z = data[source + 2];
assert.notNaN(x, 'x');
assert.notNaN(y, 'y');
assert.notNaN(z, 'z');
aabb3_array_set(data, destination, x, y, z, x, y, z);
}
/**
* Assumes data will be normalized to 0...1 value range
* @param {Float32Array} data
* @param {number} address
* @param {number[]} bounds
* @returns {number}
*/
function build_morton(data, address, bounds) {
const x0 = data[address];
const y0 = data[address + 1];
const z0 = data[address + 2];
const x1 = data[address + 3];
const y1 = data[address + 4];
const z1 = data[address + 5];
const cx = (x0 + x1) * 0.5;
const cy = (y0 + y1) * 0.5;
const cz = (z0 + z1) * 0.5;
return v3_morton_encode_bounded(cx, cy, cz, bounds);
}
const stack = SCRATCH_UINT32_TRAVERSAL_STACK;
/**
* Memory-efficient LBVH implementation.
* LBVH is fast to build, is quite fast to query and has good memory usage due to implicit addressing.
* LBVH is static, so it's not suitable for dynamic usecases, if your usecase requires updates to the BVH - use {@link BVH} instead, which is a fully dynamic BVH implementation.
*
* @see https://en.wikipedia.org/wiki/Bounding_volume_hierarchy
* @see BVH
*/
export class BinaryUint32BVH {
/**
*
* @private
* @type {ArrayBuffer}
*/
__data_buffer;
/**
* @readonly
* @type {Float32Array}
* @private
*/
__data_float32;
/**
* @readonly
* @private
* @type {Uint32Array}
*/
__data_uint32;
/**
*
* @type {number}
* @private
*/
__node_count_binary = 0;
/**
*
* @type {number}
* @private
*/
__node_count_leaf = 0;
constructor() {
this.data = new ArrayBuffer(320);
}
/**
* In bytes
* @returns {number}
*/
estimateByteSize() {
return this.data.byteLength + 248;
}
getTotalBoxCount() {
return this.__node_count_binary + this.__node_count_leaf;
}
get binary_node_count() {
return this.__node_count_binary;
}
get leaf_node_count() {
return this.__node_count_leaf;
}
/**
*
* @returns {number}
*/
getLeafBlockAddress() {
return this.__node_count_binary * BVH_BINARY_NODE_SIZE;
}
get float32() {
return this.__data_float32;
}
get uint32() {
return this.__data_uint32;
}
/**
*
* @param {ArrayBuffer} buffer
*/
set data(buffer) {
assert.defined(buffer, 'buffer');
this.__data_buffer = buffer;
this.__data_float32 = new Float32Array(this.__data_buffer);
this.__data_uint32 = new Uint32Array(this.__data_buffer);
}
get data() {
return this.__data_buffer;
}
/**
* Resolve index of the node to address where the node data starts, this is required to know where AABB is stored in memory
* @param {number} node_index
* @returns {number}
*/
getNodeAddress(node_index) {
const binary_node_count = this.__node_count_binary;
const leaf_node_index = node_index - binary_node_count;
if (leaf_node_index < 0) {
// binary node
return node_index * BVH_BINARY_NODE_SIZE;
} else {
// leaf node
return binary_node_count * BVH_BINARY_NODE_SIZE + leaf_node_index * BVH_LEAF_NODE_SIZE;
}
}
initialize_structure() {
// compute memory requirements
const word_count = this.__node_count_binary * BVH_BINARY_NODE_SIZE + this.__node_count_leaf * BVH_LEAF_NODE_SIZE;
const storage_size = word_count * 4;
// possibly resize the storage
if (this.__data_buffer.byteLength < storage_size) {
this.data = new ArrayBuffer(storage_size);
}
}
/**
*
* @param {number} count
*/
setLeafCount(count) {
assert.isNonNegativeInteger(count, 'count');
this.__node_count_leaf = count;
const twoLeafLimit = ceilPowerOfTwo(count);
if (count <= 1) {
// special case
this.__node_count_binary = twoLeafLimit;
} else {
this.__node_count_binary = twoLeafLimit - 1;
}
}
/**
*
* @param {number} index
* @return {number}
*/
getLeafAddress(index) {
assert.isNonNegativeInteger(index, 'index');
assert.lessThan(index, this.__node_count_leaf, 'leaf index overflow');
const leaf_block_address = this.__node_count_binary * BVH_BINARY_NODE_SIZE;
return index * BVH_LEAF_NODE_SIZE + leaf_block_address;
}
/**
*
* @param {number} index
* @param {number} payload
* @param {number} x0
* @param {number} y0
* @param {number} z0
* @param {number} x1
* @param {number} y1
* @param {number} z1
*/
setLeafData(
index, payload,
x0, y0, z0,
x1, y1, z1
) {
assert.isNonNegativeInteger(index, 'index');
assert.lessThan(index, this.__node_count_leaf, 'leaf index overflow');
assert.isNumber(x0, 'x0');
assert.isNumber(y0, 'y0');
assert.isNumber(z0, 'z0');
assert.isNumber(x1, 'x1');
assert.isNumber(y1, 'y1');
assert.isNumber(z1, 'z1');
assert.notNaN(x0, 'x0');
assert.notNaN(y0, 'y0');
assert.notNaN(z0, 'z0');
assert.notNaN(x1, 'x1');
assert.notNaN(y1, 'y1');
assert.notNaN(z1, 'z1');
assert.isNonNegativeInteger(payload, 'payload');
const address = this.getLeafAddress(index);
aabb3_array_set(
this.__data_float32,
address,
x0, y0, z0,
x1, y1, z1
);
this.__data_uint32[address + 6] = payload;
}
/**
* Read bounds of a box at the given address
* @param {number} address where the box is found
* @param {number[]|Float32Array} destination where to write the box coordinates (x0,y0,z0,x1,y1,z1)
* @param {number} destination_offset offset within the destination array where to start writing results
*/
readBounds(address, destination, destination_offset) {
array_copy(this.__data_float32, address, destination, destination_offset, 6);
}
/**
*
* @param {number} leaf_index
* @returns {number}
*/
readLeafPayload(leaf_index) {
const block_address = this.getLeafBlockAddress();
const address = block_address + leaf_index * BVH_LEAF_NODE_SIZE + BVH_BOX_BYTE_SIZE;
return this.__data_uint32[address];
}
compute_total_surface_area() {
let result = 0;
const box = new Float32Array(6);
for (let i = 0; i < this.getTotalBoxCount(); i++) {
let address;
if (i < this.__node_count_binary) {
address = i * BVH_BINARY_NODE_SIZE;
} else {
const li = i - this.__node_count_binary;
address = li * BVH_LEAF_NODE_SIZE + this.getLeafBlockAddress();
}
this.readBounds(address, box, 0);
result += aabb3_compute_half_surface_area(box[0], box[1], box[2], box[3], box[4], box[5]);
}
return result;
}
/**
* Sort leaf nodes according to their morton codes
* @param {number[]} bounds
*/
sort_morton(bounds) {
const leaf_block_address = this.__node_count_binary * BVH_BINARY_NODE_SIZE;
if (this.__node_count_leaf < 2) {
// no swaps available
return;
}
const stack_top = stack.pointer;
let stackPointer = stack_top;
let i, j;
stack[stackPointer++] = 0; // first node
stack[stackPointer++] = this.__node_count_leaf - 1; // last node
const data = this.__data_float32;
while (stackPointer > stack_top) {
stackPointer -= 2;
const right = stack[stackPointer + 1];
const left = stack[stackPointer];
i = left;
j = right;
const pivot_index = (left + right) >> 1;
const pivot_address = pivot_index * BVH_LEAF_NODE_SIZE + leaf_block_address;
const pivot = build_morton(data, pivot_address, bounds);
/* partition */
while (i <= j) {
while (build_morton(data, i * BVH_LEAF_NODE_SIZE + leaf_block_address, bounds) < pivot) {
i++;
}
while (build_morton(data, j * BVH_LEAF_NODE_SIZE + leaf_block_address, bounds) > pivot) {
j--;
}
if (i <= j) {
if (i !== j) {
//do swap
this.__swap_leaves(i, j);
}
i++;
j--;
}
}
/* recursion */
if (left < j) {
stack[stackPointer++] = left;
stack[stackPointer++] = j;
}
if (i < right) {
stack[stackPointer++] = i;
stack[stackPointer++] = right;
}
}
}
/**
* Does not update intermediate node bounds
* @param {number} i
* @param {number} j
* @private
*/
__swap_leaves(i, j) {
const leaf_block_address = this.getLeafBlockAddress();
const a = i * BVH_LEAF_NODE_SIZE + leaf_block_address;
const b = j * BVH_LEAF_NODE_SIZE + leaf_block_address;
array_swap(
this.__data_float32, a,
this.__data_float32, b,
BVH_LEAF_NODE_SIZE
);
}
/**
* Assemble leaf nodes into hierarchy, set binary node bounds iteratively bottom up
*/
build() {
const binary_node_count = this.__node_count_binary;
const leaf_node_block_address = binary_node_count * BVH_BINARY_NODE_SIZE;
let level = Math.floor(Math.log(binary_node_count) / Math.log(2));
let i, offset, level_node_count;
//NOTE: building the first level separately allows us to avoid some switching logic needed to determine what is the type of lower level node
//build one level above leaf nodes
level_node_count = Math.pow(2, level);
offset = (level_node_count - 1) * BVH_BINARY_NODE_SIZE;
let parentIndex;
const node_count_leaf = this.__node_count_leaf;
const float32 = this.__data_float32;
// build bottom-most level, just above the leaves
for (i = 0; i < level_node_count; i++) {
const leaf_index_0 = i << 1;
const leaf_index_1 = leaf_index_0 + 1;
const leaf_offset_0 = leaf_node_block_address + leaf_index_0 * BVH_LEAF_NODE_SIZE;
const leaf_offset_1 = leaf_node_block_address + leaf_index_1 * BVH_LEAF_NODE_SIZE;
if (leaf_index_1 < node_count_leaf) {
// both children nodes are valid
aabb3_array_combine(
float32, offset,
float32, leaf_offset_0,
float32, leaf_offset_1
);
} else if (leaf_index_0 < node_count_leaf) {
// only left child node is valid
array_copy(float32, leaf_offset_0, float32, offset, 6);
} else {
//initialize to 0-size box same position as previous node
copy_box_zero_size(this.__data_float32, offset, (offset - BVH_BINARY_NODE_SIZE));
}
offset += BVH_BINARY_NODE_SIZE;
}
level--;
//build intermediate nodes
for (; level >= 0; level--) {
level_node_count = 1 << level;
parentIndex = (level_node_count - 1);
for (i = 0; i < level_node_count; i++) {
const childIndex0 = (parentIndex << 1) + 1;
const address_parent = parentIndex * BVH_BINARY_NODE_SIZE;
const address_child_0 = childIndex0 * BVH_BINARY_NODE_SIZE;
const address_child_1 = address_child_0 + BVH_BINARY_NODE_SIZE;
aabb3_array_combine(
float32, address_parent,
float32, address_child_0,
float32, address_child_1
);
parentIndex++;
}
}
}
}