@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
422 lines (323 loc) • 13.4 kB
JavaScript
import { assert } from "../../assert.js";
import { lsb_32 } from "../../binary/lsb_32.js";
import { bitCount } from "../../binary/operations/bitCount.js";
import { ELEMENT_WORD_COUNT, NULL_NODE } from "./BVH.js";
// Common continuous memory region for better cache performance
const scratch_memory = new ArrayBuffer((256 * 3 + 16) * 4 + 256 * 6 * 4);
const scratch_bounds = new Float32Array(scratch_memory, 0, 256 * 6);
const scratch_areas = new Float32Array(scratch_memory, 6144, 256);
const scratch_cost = new Float32Array(scratch_memory, 7168, 256);
const scratch_partitions = new Uint32Array(scratch_memory, 8192, 256);
const scratch_treelet = new Uint32Array(scratch_memory, 9216, 16);
/**
*
* @param {BVH} bvh
* @param {number[]} leaves
* @param {number} mask
*/
function assemble_mask(bvh, leaves, mask) {
const leaf_count = bitCount(mask);
if (leaf_count === 1) {
const leaf_index = lsb_32(mask);
return leaves[leaf_index];
}
let partitioning_left = scratch_partitions[mask];
let partition_right = mask & ~(partitioning_left);
const parent = bvh.allocate_node();
const left_node = assemble_mask(bvh, leaves, partitioning_left);
const right_node = assemble_mask(bvh, leaves, partition_right);
bvh.node_assign_children(parent, left_node, right_node);
return parent;
}
/**
*
* @param {BVH} bvh
* @param {number} root
* @param {number[]} leaves
* @param {number} mask
*/
function rebuild_treelet(bvh, root, leaves, mask) {
let partitioning_left = scratch_partitions[mask];
let partition_right = mask & ~(partitioning_left);
bvh.node_assign_children(root,
assemble_mask(bvh, leaves, partitioning_left),
assemble_mask(bvh, leaves, partition_right),
);
}
//cost of traversing an intermediate node
const SAH_COST_INTERMEDIATE = 1.2;
// cost of performing ray/triangle test
const SAH_COST_TRIANGLE = 1;
/**
*
* @param {BVH} bvh
* @param {number} root
* @param {number[]|Uint32Array} leaves
* @param {number} leaf_count
*/
function optimize_treelet(bvh, root, leaves, leaf_count) {
// console.log(`TREELET(${leaf_count}): ${leaves.slice(0, leaf_count).join(', ')}`); // DEBUG
// see "Fast Parallel Construction of High-Quality Bounding Volume Hierarchies", Algorithm 2
const bvh_float32 = bvh.data_float32;
// Calculate surface area for each subset
const mask_limit = (1 << leaf_count) - 1;
for (let s = 1; s <= mask_limit; s++) {
let min_x, min_y, min_z, max_x, max_y, max_z;
// Check if 's' is a power of 2 (i.e., a single leaf)
// (s & (s-1)) === 0 is the standard trick for this
const is_a_leaf = (s & (s - 1)) === 0;
if (is_a_leaf) {
// --- CASE A: LEAF ---
// Map the mask bit back to a linear index (0..7)
const leaf_index = lsb_32(s);
// Read from your compact linear array
const node = leaves[leaf_index];
const source_offset = node * ELEMENT_WORD_COUNT;
min_x = bvh_float32[source_offset];
min_y = bvh_float32[source_offset + 1];
min_z = bvh_float32[source_offset + 2];
max_x = bvh_float32[source_offset + 3];
max_y = bvh_float32[source_offset + 4];
max_z = bvh_float32[source_offset + 5];
// Initialize base cost for leaf (SAH_COST_TRIANGLE * Area)
// (Calculated below)
} else {
// --- CASE B: SUBSET ---
// Split s into: LSB (Leaf) + Rest (Subset)
// Both of these are < s, so they are guaranteed to be initialized.
const mask_lsb = s & -s; // Extract lowest set bit
const mask_rest = s ^ mask_lsb; // The rest of the bits
const i_lsb = mask_lsb * 6;
const i_rest = mask_rest * 6;
// Merge bounds
min_x = Math.min(scratch_bounds[i_lsb], scratch_bounds[i_rest]);
min_y = Math.min(scratch_bounds[i_lsb + 1], scratch_bounds[i_rest + 1]);
min_z = Math.min(scratch_bounds[i_lsb + 2], scratch_bounds[i_rest + 2]);
max_x = Math.max(scratch_bounds[i_lsb + 3], scratch_bounds[i_rest + 3]);
max_y = Math.max(scratch_bounds[i_lsb + 4], scratch_bounds[i_rest + 4]);
max_z = Math.max(scratch_bounds[i_lsb + 5], scratch_bounds[i_rest + 5]);
}
// --- STORE ---
const dest_offset = s * 6;
scratch_bounds[dest_offset] = min_x;
scratch_bounds[dest_offset + 1] = min_y;
scratch_bounds[dest_offset + 2] = min_z;
scratch_bounds[dest_offset + 3] = max_x;
scratch_bounds[dest_offset + 4] = max_y;
scratch_bounds[dest_offset + 5] = max_z;
// Calculate Surface Area
const dx = max_x - min_x;
const dy = max_y - min_y;
const dz = max_z - min_z;
const area = 2.0 * (dx * dy + dy * dz + dz * dx);
scratch_areas[s] = area;
if(is_a_leaf){
// Initialize costs of individual leaves
scratch_cost[s] = SAH_COST_TRIANGLE * area;
}
}
// Optimize every subset of leaves
for (let k = 2; k <= leaf_count; k++) {
for (let s = 1; s <= mask_limit; s++) {
if (bitCount(s) !== k) {
continue;
}
// Try each way of partitioning the leaves
let best_cost = Infinity;
let best_partition = 0;
const delta = (s - 1) & s;
let partition = (-delta) & s;
do {
const cost = scratch_cost[partition] + scratch_cost[s ^ partition];
if (cost < best_cost) {
best_cost = cost;
best_partition = partition;
}
partition = (partition - delta) & s;
} while (partition !== 0)
// Calculate final SAH cost (Equation 2)
const sah_heuristic = SAH_COST_INTERMEDIATE * scratch_areas[s] + best_cost
scratch_cost[s] = sah_heuristic;
scratch_partitions[s] = best_partition;
}
}
// build tree
rebuild_treelet(bvh, root, leaves, (1 << leaf_count) - 1);
}
/**
*
* @param {BVH} bvh
* @param {number} root
* @param {number} treelet_max_size
*/
function optimize_at_node(bvh, root, treelet_max_size) {
if (root === NULL_NODE) {
return;
}
if (bvh.node_is_leaf(root)) {
return;
}
const left = bvh.node_get_child1(root);
const right = bvh.node_get_child2(root);
let treelet_size = 0;
if (left !== NULL_NODE) {
scratch_treelet[treelet_size++] = left;
}
if (right !== NULL_NODE) {
scratch_treelet[treelet_size++] = right;
}
if (treelet_size === 0) {
return;
}
while (treelet_size < treelet_max_size) {
// pick node with the largest surface area
let best_node_index = -1;
let best_area = 0; //note that this is deliberate, expanding node with 0 area is pointless
for (let i = 0; i < treelet_size; i++) {
const n = scratch_treelet[i];
if (bvh.node_is_leaf(n)) {
// can't expand
continue;
}
// TODO maintain list of areas instead of computing them every time
const area = bvh.node_get_surface_area(n);
if (area > best_area) {
best_area = area;
best_node_index = i;
}
}
if (best_node_index === -1) {
break;
}
// expand candidate
const victim = scratch_treelet[best_node_index];
const c0 = bvh.node_get_child1(victim);
const c1 = bvh.node_get_child2(victim);
scratch_treelet[best_node_index] = c0;
scratch_treelet[treelet_size++] = c1;
bvh.release_node(victim);
}
if (treelet_size <= 2) {
// nothing to do
return;
}
optimize_treelet(bvh, root, scratch_treelet, treelet_size);
}
const CAME_FROM_TYPE_PARENT = 0;
const CAME_FROM_TYPE_CHILD_1 = 1;
const CAME_FROM_TYPE_CHILD_2 = 2;
/**
* Perform linear optimization across the tree, similar to rotations, but we support somewhat arbitrary depth and all possible permutations
* Note that the root does not move, but everything below the root is subject to change
* @see "Fast Parallel Construction of High-Quality Bounding Volume Hierarchies" by Kerras, NVIDIA 2013
* @param {BVH} bvh
* @param {number} [root] where to start optimization, only nodes below this one will be considered
* @param {number} [treelet_size] the larger the size - the more optimization opportunities, but it gets exponentially slower
*/
export function ebvh_optimize_treelet(
bvh,
root = bvh.root,
treelet_size = 7
) {
assert.isNonNegativeInteger(treelet_size, 'treelet_size');
assert.lessThanOrEqual(treelet_size, 8, 'limit is 8');
if (root === NULL_NODE) {
// special case
return;
}
if (bvh.node_is_leaf(root)) {
// special case
return;
}
// we traverse depth-first using stack-less scheme
let came_from_node = root;
let came_from_type = CAME_FROM_TYPE_PARENT;
let node = bvh.node_get_child1(came_from_node);
if (node === NULL_NODE) {
node = bvh.node_get_child2(came_from_node);
}
if (node === NULL_NODE) {
// no children, done
return;
}
const min_height = Math.ceil(Math.log2(treelet_size));
// do a stackless traversal
while (true) {
if (
came_from_type === CAME_FROM_TYPE_CHILD_2
&& bvh.node_get_height(node) >= min_height
) {
// only form treelets when moving up
optimize_at_node(bvh, node, treelet_size);
}
// only add to treelet when traversing up, not down
if (bvh.node_is_leaf(node)) {
assert.equal(came_from_type, CAME_FROM_TYPE_PARENT);
const parent = came_from_node;
const parent_child_1 = bvh.node_get_child1(parent);
if (parent_child_1 === node) {
came_from_type = CAME_FROM_TYPE_CHILD_1;
} else {
came_from_type = CAME_FROM_TYPE_CHILD_2;
}
came_from_node = node;
node = parent;
} else if (came_from_type === CAME_FROM_TYPE_CHILD_2) {
// finishing traversal of this branch
came_from_node = node;
const parent = bvh.node_get_parent(node);
if (bvh.node_get_child1(parent) === node) {
came_from_type = CAME_FROM_TYPE_CHILD_1;
} else {
came_from_type = CAME_FROM_TYPE_CHILD_2;
}
if (node === root) {
// traversed all the way back up
break;
}
node = parent;
} else if (came_from_type === CAME_FROM_TYPE_CHILD_1) {
// traversing up from left child
const child2 = bvh.node_get_child2(node);
came_from_node = node;
if (child2 === NULL_NODE) {
// no right child, finish this branch
came_from_type = CAME_FROM_TYPE_CHILD_2; // indicate end
node = bvh.node_get_parent(node);
} else {
came_from_type = CAME_FROM_TYPE_PARENT;
node = child2;
}
} else if (came_from_type === CAME_FROM_TYPE_PARENT) {
const child1 = bvh.node_get_child1(node);
const child2 = bvh.node_get_child2(node);
if (child1 !== NULL_NODE) {
came_from_type = CAME_FROM_TYPE_PARENT;
came_from_node = node;
node = child1;
} else if (child2 !== NULL_NODE) {
came_from_type = CAME_FROM_TYPE_PARENT;
came_from_node = node;
node = child2;
} else {
// this should not happen, as this would mean that node is empty and completely pointless
// we remove the node and traverse up
const parent_child1 = bvh.node_get_child1(came_from_node);
bvh.release_node(node);
if (parent_child1 === node) {
// we are left child
bvh.node_set_child1(came_from_node, NULL_NODE);
came_from_node = NULL_NODE;
came_from_type = CAME_FROM_TYPE_CHILD_1;
} else {
bvh.node_set_child2(came_from_node, NULL_NODE);
came_from_node = NULL_NODE;
came_from_type = CAME_FROM_TYPE_CHILD_2;
}
node = came_from_node;
}
}
}
// optimize last collected treelet
optimize_at_node(bvh, root, treelet_size);
}