UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

422 lines (323 loc) • 13.4 kB
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); }