UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

508 lines (378 loc) • 14.3 kB
import { BVH, COLUMN_CHILD_1, COLUMN_CHILD_2, COLUMN_USER_DATA, ELEMENT_WORD_COUNT, NULL_NODE } from "../../../../core/bvh2/bvh3/BVH.js"; import { ebvh_build_for_geometry_morton } from "../../../../core/bvh2/bvh3/ebvh_build_for_geometry_morton.js"; import { ebvh_optimize_treelet } from "../../../../core/bvh2/bvh3/ebvh_optimize_treelet.js"; import { bvh_query_user_data_ray_segment } from "../../../../core/bvh2/bvh3/query/bvh_query_user_data_ray_segment.js"; import { array_copy } from "../../../../core/collection/array/array_copy.js"; import { SCRATCH_UINT32_TRAVERSAL_STACK } from "../../../../core/collection/SCRATCH_UINT32_TRAVERSAL_STACK.js"; import { AABB3 } from "../../../../core/geom/3d/aabb/AABB3.js"; import { aabb3_from_v3_array } from "../../../../core/geom/3d/aabb/aabb3_from_v3_array.js"; import { aabb3_intersects_ray_segment } from "../../../../core/geom/3d/aabb/aabb3_intersects_ray_segment.js"; import { SurfacePoint3 } from "../../../../core/geom/3d/SurfacePoint3.js"; import { computeTriangleRayIntersectionBarycentricGeometry } from "../../../../core/geom/3d/triangle/computeTriangleRayIntersectionBarycentricGeometry.js"; import Vector4 from "../../../../core/geom/Vector4.js"; import { makeGeometryIndexed } from "../../geometry/buffered/makeGeometryIndexed.js"; import { query_bvh_geometry_nearest } from "../../geometry/buffered/query/query_bvh_geometry_nearest.js"; import { computeBoundingSphereFromVertexData } from "../../geometry/computeBoundingSphereFromVertexData.js"; import { construct_ray_hit_from_geometry } from "./geometry/construct_ray_hit_from_geometry.js"; /** * * @type {number[]|Uint32Array} */ const scratch_uint32_array = new Uint32Array(4096); /** * * @type {number[]} */ const v3_scratch_0 = []; const stack = SCRATCH_UINT32_TRAVERSAL_STACK; export class BufferedGeometryBVH { /** * @type {Uint32Array} */ #morton_codes #bounds = new AABB3() #bounds_sphere = new Float32Array(4); /** * * @type {BVH} * @private */ #bvh = new BVH(); /** * * @param {AABB3|number[]} out */ getBounds(out) { array_copy(this.#bounds, 0, out, 0, 6); } constructor() { /** * * @type {THREE.BufferGeometry|null} * @private */ this.__geometry = null; /** * * @type {number[]|null} * @private */ this.__geometry_index = null; /** * * @type {number[]|null} * @private */ this.__geometry_positions = null; /** * * @type {number} * @private */ this.__triangle_count = 0; } /** * * @param {THREE.BufferGeometry} geo */ build(geo) { makeGeometryIndexed(geo); this.__geometry = geo; const index = geo.getIndex(); const index_array = index.array; const attribute_position = geo.getAttribute('position'); const array_positions = attribute_position.array; this.__geometry_positions = array_positions; this.__geometry_index = index_array; this.__triangle_count = index_array.length / 3; this.#morton_codes = new Uint32Array(this.__triangle_count); const bvh = this.#bvh; aabb3_from_v3_array(this.#bounds, array_positions, array_positions.length); ebvh_build_for_geometry_morton( bvh, index_array, array_positions, this.#morton_codes, this.#bounds, 16 ); ebvh_optimize_treelet(bvh); // // ebvh_build_for_geometry_incremental(bvh, index_array, array_positions, 1); // remove any extra unused space bvh.trim(); const r = new Vector4(); computeBoundingSphereFromVertexData(array_positions, r); r.toArray(this.#bounds_sphere); } /** * Tests ray for occlusion * Returns true if ray hits anything at all * @param {Ray3} ray * @returns {boolean} */ occluded(ray) { const indices = this.__geometry_index; const positions = this.__geometry_positions; const origin_x = ray[0]; const origin_y = ray[1]; const origin_z = ray[2]; const direction_x = ray[3]; const direction_y = ray[4]; const direction_z = ray[5]; const max_distance = ray[6]; const bvh = this.#bvh; const root = bvh.root; if (root === NULL_NODE) { return -1; } /** * Move stack pointer to local variable scope to avoid de-referencing inside the loop * @type {number} */ let pointer = stack.pointer; /** * * @type {number} */ const stack_top = pointer; stack[pointer++] = root; /* For performance, we bind data directly to avoid extra copies required to read out AABB */ const float32 = bvh.__data_float32; const uint32 = bvh.__data_uint32; const inv_direction_x = 1 / direction_x; const inv_direction_y = 1 / direction_y; const inv_direction_z = 1 / direction_z; do { --pointer; /** * * @type {number} */ const node = stack[pointer]; const address = node * ELEMENT_WORD_COUNT; // test node against the ray const intersects = aabb3_intersects_ray_segment( float32[address], float32[address + 1], float32[address + 2], float32[address + 3], float32[address + 4], float32[address + 5], origin_x, origin_y, origin_z, inv_direction_x, inv_direction_y, inv_direction_z, 0, max_distance ); if (!intersects) { continue; } // get fist child to check if this is a leaf node or not const child_1 = uint32[address + COLUMN_CHILD_1]; if (child_1 !== NULL_NODE) { // this is not a leaf node, push children onto traversal stack const child_2 = uint32[address + COLUMN_CHILD_2]; stack[pointer++] = child_2; stack[pointer++] = child_1; } else { // leaf node const triangle_index = uint32[address + COLUMN_USER_DATA]; const intersection_found = computeTriangleRayIntersectionBarycentricGeometry( v3_scratch_0, origin_x, origin_y, origin_z, direction_x, direction_y, direction_z, indices, triangle_index, positions ); if (!intersection_found) { continue; } const t = v3_scratch_0[0]; if (t < max_distance && t > 0) { return true; } } } while (pointer > stack_top); return false; } /** * * @param {SurfacePoint3} result * @param {number} x * @param {number} y * @param {number} z * @returns {boolean} true if result is found, only false when geometry is empty */ nearestSurfacePoint(result, x, y, z) { const indices = this.__geometry_index; const positions = this.__geometry_positions; const bvh = this.#bvh; return query_bvh_geometry_nearest(result, bvh, positions, indices, x, y, z, Infinity); } /** * Code is largely inlined, to avoid extra checks * NOTE: raycast is performed in local coordinate space * @param {number[]} output * @param {number[]|Ray3} ray * @returns {number} distance along the ray, negative if no hit */ raycast2(output, ray) { const indices = this.__geometry_index; const positions = this.__geometry_positions; const origin_x = ray[0]; const origin_y = ray[1]; const origin_z = ray[2]; const direction_x = ray[3]; const direction_y = ray[4]; const direction_z = ray[5]; const max_distance = ray[6]; const bvh = this.#bvh; const root = bvh.root; if (root === NULL_NODE) { return -1; } /** * Move stack pointer to local variable scope to avoid de-referencing inside the loop * @type {number} */ let pointer = stack.pointer; let nearest_hit_distance = max_distance; let best_index = -1; let best_u = 0; let best_v = 0; /** * * @type {number} */ const stack_top = pointer; stack[pointer++] = root; /* For performance, we bind data directly to avoid extra copies required to read out AABB */ const float32 = bvh.__data_float32; const uint32 = bvh.__data_uint32; const inv_direction_x = 1 / direction_x; const inv_direction_y = 1 / direction_y; const inv_direction_z = 1 / direction_z; do { --pointer; /** * * @type {number} */ const node = stack[pointer]; const address = node * ELEMENT_WORD_COUNT; // test node against the ray const intersects = aabb3_intersects_ray_segment( float32[address], float32[address + 1], float32[address + 2], float32[address + 3], float32[address + 4], float32[address + 5], origin_x, origin_y, origin_z, inv_direction_x, inv_direction_y, inv_direction_z, 0, nearest_hit_distance ); if (!intersects) { continue; } // get fist child to check if this is a leaf node or not const child_1 = uint32[address + COLUMN_CHILD_1]; if (child_1 !== NULL_NODE) { // this is not a leaf node, push children onto traversal stack const child_2 = uint32[address + COLUMN_CHILD_2]; stack[pointer++] = child_2; stack[pointer++] = child_1; } else { // leaf node const triangle_index = uint32[address + COLUMN_USER_DATA]; const intersection_found = computeTriangleRayIntersectionBarycentricGeometry( v3_scratch_0, origin_x, origin_y, origin_z, direction_x, direction_y, direction_z, indices, triangle_index, positions ); if (!intersection_found) { continue; } const t = v3_scratch_0[0]; if (t < nearest_hit_distance && t > 0) { nearest_hit_distance = t; best_index = triangle_index; best_u = v3_scratch_0[1]; best_v = v3_scratch_0[2]; } } } while (pointer > stack_top); if (nearest_hit_distance === max_distance) { // no hit return -1; } construct_ray_hit_from_geometry(output, indices, positions, best_index, nearest_hit_distance, best_u, best_v); return nearest_hit_distance; } /** * NOTE: raycast is performed in local coordinate space * @param {number[]} output * @param {number[]|Ray3} ray * @returns {number} distance along the ray, negative if no hit */ raycast(output, ray) { const indices = this.__geometry_index; const positions = this.__geometry_positions; const origin_x = ray[0]; const origin_y = ray[1]; const origin_z = ray[2]; const direction_x = ray[3]; const direction_y = ray[4]; const direction_z = ray[5]; const max_distance = ray[6]; const bvh = this.#bvh; const count = bvh_query_user_data_ray_segment( bvh, bvh.root, scratch_uint32_array, 0, origin_x, origin_y, origin_z, direction_x, direction_y, direction_z, 0, max_distance ); if (count === 0) { // no bvh hit, early exit return -1; } let nearest_hit_distance = max_distance; let best_index = -1; let best_u = 0; let best_v = 0; // check triangles found via BVH for (let i = 0; i < count; i++) { const triangle_index = scratch_uint32_array[i]; const intersection_found = computeTriangleRayIntersectionBarycentricGeometry( v3_scratch_0, origin_x, origin_y, origin_z, direction_x, direction_y, direction_z, indices, triangle_index, positions ); if (!intersection_found) { continue; } const t = v3_scratch_0[0]; if (t < nearest_hit_distance && t > 0) { nearest_hit_distance = t; best_index = triangle_index; best_u = v3_scratch_0[1]; best_v = v3_scratch_0[2]; } } if (nearest_hit_distance === max_distance) { // no hit return -1; } construct_ray_hit_from_geometry(output, indices, positions, best_index, nearest_hit_distance, best_u, best_v); return nearest_hit_distance; } }