@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
355 lines (257 loc) • 10.3 kB
JavaScript
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;
}
}