UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

355 lines (257 loc) • 10.3 kB
import { assert } from "../../../assert.js"; import { array_copy } from "../../../collection/array/array_copy.js"; import { binarySearchHighIndex } from "../../../collection/array/binarySearchHighIndex.js"; import { isArrayEqual } from "../../../collection/array/isArrayEqual.js"; import { max2 } from "../../../math/max2.js"; import { min2 } from "../../../math/min2.js"; import { seededRandom } from "../../../math/random/seededRandom.js"; import { number_compare_ascending } from "../../../primitives/numbers/number_compare_ascending.js"; import { v3_dot } from "../../vec3/v3_dot.js"; import { AbstractShape3D } from "./AbstractShape3D.js"; import { compute_signed_distance_gradient_by_sampling } from "./util/compute_signed_distance_gradient_by_sampling.js"; /** * To avoid severe performance overhead, we limit number of possible rejections * @type {number} */ const SAMPLE_REJECTION_ATTEMPT_MAX = 4; /** * Number of samples taken to estimate overlap level * @type {number} */ const VOLUME_OVERLAP_ESTIMATION_TAPS = 1024; const VOLUME_OVERLAP_ESTIMATION_SUB_TAPS = 4; const scratch_array = []; export class UnionShape3D extends AbstractShape3D { constructor() { super(); /** * * @type {AbstractShape3D[]} */ this.children = []; /** * Estimator of how much volume is in overlap, * expressed as fraction of volume occupied by overlapping region of 2 shapes over the sum of volume occupied by each child * @type {number} * @private */ this.__volume_overlap_fraction = -1; this.__volume_sum = -1; this.__volume_sums = []; this.__child_volumes = []; } compute_bounding_box(result) { let x0 = Infinity, y0 = Infinity, z0 = Infinity, x1 = -Infinity, y1 = -Infinity, z1 = -Infinity; const children = this.children; const n = children.length; const tmp_aabb3 = new Float32Array(6); for (let i = 0; i < n; i++) { const child = children[i]; child.compute_bounding_box(tmp_aabb3); x0 = min2(x0, tmp_aabb3[0]); y0 = min2(y0, tmp_aabb3[1]); z0 = min2(z0, tmp_aabb3[2]); x1 = max2(x1, tmp_aabb3[3]); y1 = max2(y1, tmp_aabb3[4]); z1 = max2(z1, tmp_aabb3[5]); } result[0] = x0; result[1] = y0; result[2] = z0; result[3] = x1; result[4] = y1; result[5] = z1; } /** * * @private */ __compute_volume_variables() { this.__volume_sum = 0; const children = this.children; const child_count = children.length; const child_volumes = this.__child_volumes = new Float32Array(child_count); const child_volume_sums = this.__volume_sums = new Float32Array(child_count); for (let i = 0; i < child_count; i++) { const child_volume = children[i].volume; child_volumes[i] = child_volume; this.__volume_sum += child_volume; child_volume_sums[i] = this.__volume_sum; } } /** * Monte-carlo-style volume overlap estimation * @private */ __estimate_volume_overlap_fraction() { if (this.__volume_sum === -1) { this.__compute_volume_variables(); } const children = this.children; const child_count = children.length; const random = seededRandom(1); const child_volume_sums = this.__volume_sums; const child_volume_sum = this.__volume_sum; let i = 0, j = 0, k = 0, tap_overlaps = 0; for (i = 0; i < VOLUME_OVERLAP_ESTIMATION_TAPS; i++) { const target_weight = random() * child_volume_sum; // weighted search for a sample candidate, preferring larger volumes, resulting in more uniform sample distribution const target_child_index = binarySearchHighIndex(child_volume_sums, target_weight, number_compare_ascending, 0, child_count - 1); const child_0 = children[target_child_index]; for (j = 0; j < VOLUME_OVERLAP_ESTIMATION_SUB_TAPS; j++, i++) { child_0.sample_random_point_in_volume(scratch_array, 0, random); for (k = 0; k < child_count; k++) { if (target_child_index === k) { // skip self continue; } const child_1 = children[k]; if (child_1.contains_point(scratch_array)) { tap_overlaps++; } } } } this.__volume_overlap_fraction = tap_overlaps / (VOLUME_OVERLAP_ESTIMATION_TAPS); } /** * * @param {AbstractShape3D[]} children * @returns {UnionShape3D} */ static from(children) { assert.isArray(children, 'children'); const r = new UnionShape3D(); r.children = children; r.__compute_volume_variables(); return r; } get volume() { if (this.__volume_overlap_fraction < 0) { this.__estimate_volume_overlap_fraction(); } let volume_sum = 0; const children = this.children; const child_count = children.length; for (let i = 0; i < child_count; i++) { const child = children[i]; volume_sum += child.volume; } /** * Unbias the volume by taking overlap into account * @type {number} */ const unbiased_volume = volume_sum / (this.__volume_overlap_fraction + 1); return unbiased_volume; } get surface_area() { let r = 0; const children = this.children; const child_count = children.length; for (let i = 0; i < child_count; i++) { const child = children[i]; // TODO this is incorrect math r += child.surface_area; } return r; } signed_distance_gradient_at_point(result, point) { return compute_signed_distance_gradient_by_sampling(result, this, point); } signed_distance_at_point(point) { let r = Infinity; const children = this.children; const child_count = children.length; for (let i = 0; i < child_count; i++) { const child = children[i]; r = min2(child.signed_distance_at_point(point), r); } return r; } contains_point(point) { const children = this.children; const n = children.length; for (let i = 0; i < n; i++) { const child = children[i]; if (child.contains_point(point)) { return true; } } return false; } sample_random_point_in_volume(result, result_offset, random) { const children = this.children; const child_count = children.length; const target_weight = random() * this.__volume_sum; // weighted search for a sample candidate, preferring larger volumes, resulting in more uniform sample distribution const target_child_index = binarySearchHighIndex(this.__volume_sums, target_weight, number_compare_ascending, 0, child_count - 1); const child = children[target_child_index]; let collision_count, i, j; for (i = 0; i < SAMPLE_REJECTION_ATTEMPT_MAX; i++) { // sample shape child.sample_random_point_in_volume(result, result_offset, random); /* because underlying shapes can overlap and we wish to offer a union, just returning the sample would result in over-sampling in areas where shapes overlap to avoid this oversampling, we reject samples in such areas with probability proportional to number of overlapping shapes at the sampled point */ collision_count = 0; for (j = 0; j < child_count; j++) { const child_1 = children[j]; if (child_1 === child) { // exclude sampled child itself continue; } if (child_1.contains_point(result)) { collision_count++; } } if (collision_count > 0) { const rejection_chance = collision_count / (collision_count + 1); const rejection_roll = random(); if (rejection_roll < rejection_chance) { //reject continue; } } // no collisions, or survived rejection break; } } support(result, result_offset, direction_x, direction_y, direction_z) { let best_distance = -Infinity; const children = this.children; const child_count = children.length; for (let i = 0; i < child_count; i++) { const child = children[i]; child.support(scratch_array, 0, direction_x, direction_y, direction_z); const d = v3_dot( scratch_array[0], scratch_array[1], scratch_array[2], direction_x, direction_y, direction_z ); if (d > best_distance) { // best candidate so far, write to result array_copy(scratch_array, 0, result, result_offset, 3); best_distance = d; } } } /** * * @param {UnionShape3D} other * @returns {boolean} */ equals(other) { return super.equals(other) && isArrayEqual(this.children, other.children); } hash() { const children = this.children; const child_count = children.length; let hash = child_count; if (child_count > 0) { hash ^= children[0].hash(); } return hash; } }