@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
808 lines (634 loc) • 24.9 kB
JavaScript
import { orient3d } from "robust-predicates";
import { assert } from "../../../assert.js";
import { Base64 } from "../../../binary/base64/Base64.js";
import { BinaryBuffer } from "../../../binary/BinaryBuffer.js";
import { EndianType } from "../../../binary/EndianType.js";
import { array_copy } from "../../../collection/array/array_copy.js";
import { array_quick_sort_by_comparator } from "../../../collection/array/array_quick_sort_by_comparator.js";
import { typed_array_copy } from "../../../collection/array/typed/typed_array_copy.js";
import { max3 } from "../../../math/max3.js";
import { number_compare_descending } from "../../../primitives/numbers/number_compare_descending.js";
/**
* @readonly
* @type {number}
*/
const LAYOUT_TETRA_WORD_COUNT = 8;
/**
* Size in bytes of a single tetrahedron record
* @readonly
* @type {number}
*/
export const LAYOUT_TETRA_BYTE_SIZE = LAYOUT_TETRA_WORD_COUNT * 4;
/**
* @readonly
* @type {number}
*/
export const INVALID_NEIGHBOUR = 0xFFFFFFFF;
/**
* @readonly
* @type {number}
*/
export const MAX_TET_INDEX = 0xFFFFFFFC;
/**
* @readonly
* @type {number}
*/
const DEFAULT_INITIAL_SIZE = 128;
/**
* @readonly
* @type {number}
*/
const CAPACITY_GROW_MULTIPLIER = 1.2;
/**
* @readonly
* @type {number}
*/
const CAPACITY_GROW_MIN_STEP = 32;
/**
* Only keeps track of tetrahedra, actual point coordinates are stored outside.
* For most useful operations point coordinates are passed in as an extra argument.
*
* Binary Layout:
* vertex_id_a :: uint32
* vertex_id_b :: uint32
* vertex_id_c :: uint32
* vertex_id_d :: uint32
* neighbour_a :: uint32 - neighbour tetrahedron, opposite to vertex A
* neighbour_b :: uint32 - neighbour tetrahedron, opposite to vertex B
* neighbour_c :: uint32 - neighbour tetrahedron, opposite to vertex C
* neighbour_d :: uint32 - neighbour tetrahedron, opposite to vertex D
* Layout is similar to [1], but is interleaved for better cache locality.
* Also note that sub-determinants are not included, these are only needed for building the mesh, we excluded them to keep structure clean and more compact.
*
* Neighbours are encoded in the following manner:
* MSB -> [tet_id:30bit][opposite_corner_index:2bit] <- LSB
* Code to get tet index: encoded >> 2
* Code to get corner index: encoded & 3
*
* @see [1] 2018 "One machine, one minute, three billion tetrahedra" by Célestin Marot, Jeanne Pellerin and Jean-François Remacle
* @see https://git.immc.ucl.ac.be/hextreme/hxt_seqdel (C source code for [1])
*/
export class TetrahedralMesh {
/**
*
* @param {number} [initial_size]
*/
constructor(initial_size = DEFAULT_INITIAL_SIZE) {
assert.isNonNegativeInteger(initial_size, 'initial_size');
/**
*
* @type {ArrayBuffer}
* @private
*/
this.__buffer = new ArrayBuffer(initial_size * LAYOUT_TETRA_BYTE_SIZE);
/**
*
* @type {Uint32Array}
* @private
*/
this.__data_uint32 = new Uint32Array(this.__buffer);
/**
*
* @type {DataView}
* @private
*/
this.__view = new DataView(this.__buffer);
/**
*
* @type {number}
* @private
*/
this.__capacity = initial_size;
/**
*
* @type {number}
* @private
*/
this.__used_end = 0;
/**
* Unused slots
* @type {number[]}
* @private
*/
this.__free = [];
/**
*
* @type {number}
* @private
*/
this.__free_pointer = 0;
}
/**
* Access raw data
* Useful for serialization
* If you intend to modify the data directly - make sure you fully understand the implications of doing so
* @returns {ArrayBuffer}
*/
get data_buffer() {
return this.__buffer;
}
/**
* Exposes internal state, when this is false there are hole in the allocated memory
* Useful mainly for serialization and debugging.
* When serializing, you would want to get rid of any holes first by calling {@link compact}
* @return {boolean}
*/
get isCompacted() {
return this.__free_pointer === 0;
}
/**
* Traverse live tetrahedrons
* @param { function( tet_id:number, mesh:TetrahedralMesh ):* } visitor
* @param {*} [thisArg]
*/
forEach(visitor, thisArg) {
assert.isFunction(visitor, 'visitor');
for (let i = 0; i < this.__used_end; i++) {
if (!this.exists(i)) {
continue;
}
visitor.call(thisArg, i, this);
}
}
/**
* Produces a list of live tetrahedrons
* Allocates.
* @return {number[]}
*/
getLive() {
/**
*
* @type {number[]}
*/
const r = [];
this.forEach((tet) => r.push(tet));
return r;
}
/**
* Clears all data from the mesh, making it contain 0 tetrahedrons
* Ensures that consequent allocation requests will be sequential
*/
clear() {
// clear data
this.__data_uint32.fill(0, 0, this.__used_end);
// reset metadata
this.__used_end = 0;
this.__free_pointer = 0;
this.__free.splice(0, this.__free.length);
}
/**
*
* @param {number} capacity
*/
setCapacity(capacity) {
assert.isNonNegativeInteger(capacity, 'capacity');
if (capacity === this.__capacity) {
// do nothing
return;
}
if (capacity < this.__capacity && capacity < this.__used_end) {
throw new Error('Reducing capacity would result in dropping information. This is an illegal operation. If you need to reduce capacity - either drop data or compact the layout first.');
}
// allocate new buffer
const new_buffer = new ArrayBuffer(capacity * LAYOUT_TETRA_BYTE_SIZE);
// move data across from old buffer to the new one
const destination_uint8 = new Uint8Array(new_buffer);
const source_uint8 = new Uint8Array(this.__buffer);
typed_array_copy(source_uint8, destination_uint8);
// set new buffer
this.__buffer = new_buffer;
this.__view = new DataView(new_buffer);
this.__data_uint32 = new Uint32Array(new_buffer);
// write new capacity
this.__capacity = capacity;
}
/**
*
* @return {number}
*/
getCapacity() {
return this.__capacity;
}
/**
* How many tetrahedrons are contained in the mesh, includes any unallocated tetrahedrons
* @deprecated use {@link count} instead
* @return {number}
*/
size() {
console.warn('Deprecated, use .count instead');
return this.__used_end;
}
/**
* Number of currently live tetrahedrons.
* Excludes unallocated tetrahedrons.
* @return {number}
*/
get count() {
return this.__used_end - this.__free_pointer;
}
/**
* Grow capacity to at least the specified size
* @private
* @param {number} capacity minimum
*/
growCapacity(capacity) {
assert.isNonNegativeInteger(capacity, 'capacity');
const existing_capacity = this.__capacity;
const new_capacity = max3(
capacity,
Math.ceil(existing_capacity * CAPACITY_GROW_MULTIPLIER),
existing_capacity + CAPACITY_GROW_MIN_STEP
);
this.setCapacity(new_capacity);
}
/**
* Make sure that capacity is large enough to contain a certain total number of tetrahedrons
* @param {number} capacity
*/
ensureCapacity(capacity) {
assert.isNonNegativeInteger(capacity, 'capacity');
if (this.__capacity >= capacity) {
// big enough
return;
}
this.growCapacity(capacity);
}
/**
* NOTE: this method can be quite slow in cases of sparse allocation, please prefer not to use it
* @param {number} tet
* @return {boolean}
*/
exists(tet) {
if (tet < 0 || tet >= this.__used_end) {
return false;
}
for (let i = 0; i < this.__free_pointer; i++) {
const free = this.__free[i];
if (tet === free) {
return false;
}
}
return true;
}
/**
* NOTE: the neighbour value must be encoded, see format specification for details
* @param {number} tetra_index
* @param {number} neighbour_index
* @returns {number} index of the neighbour encoded with the opposite corner
*/
getNeighbour(tetra_index, neighbour_index) {
assert.isNonNegativeInteger(tetra_index, 'tetra_index');
assert.ok(this.exists(tetra_index), 'tetrahedron does not exist');
assert.isNonNegativeInteger(neighbour_index, 'neighbour_index');
assert.lessThan(neighbour_index, 4, 'neighbour_index');
const tetra_address = LAYOUT_TETRA_BYTE_SIZE * tetra_index;
return this.__view.getUint32(tetra_address + (4 + neighbour_index) * 4);
}
/**
* NOTE: the neighbour value must be encoded, see format specification for details
* @param {number} tetra_index
* @param {number} neighbour_index which neighbour to set (00..11)
* @param {number} neighbour index of the neighbour encoded with the opposite corner
*/
setNeighbour(tetra_index, neighbour_index, neighbour) {
assert.isNonNegativeInteger(tetra_index, 'tetra_index');
assert.ok(this.exists(tetra_index), 'tetrahedron does not exist');
assert.isNonNegativeInteger(neighbour_index, 'neighbour_index');
assert.isNonNegativeInteger(neighbour, 'neighbour');
assert.lessThan(neighbour_index, 4, 'neighbour_index');
const tetra_address = LAYOUT_TETRA_BYTE_SIZE * tetra_index;
return this.__view.setUint32(tetra_address + (4 + neighbour_index) * 4, neighbour);
}
/**
*
* @param {number} tet_index
* @param {number} point_index should be an integer between 0 and 3
* @returns {number}
*/
getVertexIndex(tet_index, point_index) {
assert.isNonNegativeInteger(tet_index, 'tet_index');
assert.lessThanOrEqual(tet_index, MAX_TET_INDEX, 'max index exceeded');
//assert.ok(this.exists(tet_index), 'tetrahedron does not exist');
assert.isNonNegativeInteger(point_index, 'point_index');
assert.lessThan(point_index, 4, 'point_index must be less than 4');
return this.__view.getUint32(tet_index * LAYOUT_TETRA_BYTE_SIZE + point_index * 4);
}
/**
*
* @param {number} tet_index
* @param {number} point_index
* @param {number} vertex
*/
setVertexIndex(tet_index, point_index, vertex) {
assert.isNonNegativeInteger(tet_index, 'tet_index');
assert.lessThanOrEqual(tet_index, MAX_TET_INDEX, 'max index exceeded');
//assert.ok(this.exists(tet_index), 'tetrahedron does not exist');
assert.isNonNegativeInteger(point_index, 'point_index');
assert.lessThan(point_index, 4, 'point_index must be less than 4');
assert.isNonNegativeInteger(vertex, 'vertex');
return this.__view.setUint32(
tet_index * LAYOUT_TETRA_BYTE_SIZE + point_index * 4,
vertex
);
}
/**
* Whether a given tetrahedron contains vertex with a given index
* @param {number} tet
* @param {number} vertex
* @return {boolean}
*/
tetContainsVertex(tet, vertex) {
for (let i = 0; i < 4; i++) {
if (this.getVertexIndex(tet, i) === vertex) {
return true;
}
}
return false;
}
/**
* Allocate empty tet
* NOTE: the tet memory might be dirty, please make sure you set/clear it as necessary
* @return {number} index of allocated tetrahedron
*/
allocate() {
if (this.__free_pointer > 0) {
this.__free_pointer--;
return this.__free[this.__free_pointer];
}
const tetra_index = this.__used_end;
this.__used_end++;
if (tetra_index >= this.__capacity) {
// needs to be increased in size
this.growCapacity(tetra_index);
}
// initialize neighbours
for (let i = 0; i < 4; i++) {
this.setNeighbour(tetra_index, i, INVALID_NEIGHBOUR);
}
//assert(this.validateLength());
// assert(this.validateLength());
return tetra_index;
}
/**
*
* @param {number} a
* @param {number} b
* @param {number} c
* @param {number} d
* @returns {number} index of the new tetrahedron
*/
append(a, b, c, d) {
const tetra_index = this.allocate();
const address = tetra_index * LAYOUT_TETRA_BYTE_SIZE;
const view = this.__view;
view.setUint32(address, a);
view.setUint32(address + 4, b);
view.setUint32(address + 8, c);
view.setUint32(address + 12, d);
// set neighbours
view.setUint32(address + 16, INVALID_NEIGHBOUR);
view.setUint32(address + 20, INVALID_NEIGHBOUR);
view.setUint32(address + 24, INVALID_NEIGHBOUR);
view.setUint32(address + 28, INVALID_NEIGHBOUR);
return tetra_index;
}
/**
* Sets back-links on neighbours to this tet to INVALID_NEIGHBOUR basically making them into mesh surface
* This is a useful method for when you want to completely remove a given tet from the mesh to make sure that no dangling references will remain
* @param {number} tetra_index
*/
disconnect(tetra_index) {
// find neighbours and remove reference to self
for (let i = 0; i < 4; i++) {
const neighbour_encoded = this.getNeighbour(tetra_index, i);
if (neighbour_encoded === INVALID_NEIGHBOUR) {
// no neighbour
continue;
}
// get tetrahedra index and point index
const neighbour_index = neighbour_encoded >> 2;
const neighbour_point = neighbour_encoded & 3;
// clear reference to self
this.setNeighbour(neighbour_index, neighbour_point, INVALID_NEIGHBOUR);
}
}
/**
* Remove tetrahedron, de-allocating memory
* Please note that if there are any dangling references in the mesh neighbourhood - you will need to take care of that separately
* @param {number} tetra_index
*/
delete(tetra_index) {
assert.isNonNegativeInteger(tetra_index, 'tera_index');
assert.lessThan(tetra_index, this.__used_end, 'attempting to remove tet outside of valid region');
// assert.equal(this.__occupancy.get(tetra_index), true, 'tetrahedron does not exist');
if (tetra_index === this.__used_end - 1) {
// tet was at the end of the allocated space
this.__used_end--;
} else {
// mark as dead
this.__free[this.__free_pointer++] = tetra_index;
}
//assert(this.validateLength());
//assert(this.validateLength());
}
/**
* Used mainly to remove tetrahedrons whos points touch the "super-tetrahedron's" points that was inserted originally
* These points are identified by an offset + count parameters
* @param {number} range_start
* @param {number} range_end
*/
removeTetrasConnectedToPoints(range_start, range_end) {
for (let i = this.__used_end - 1; i >= 0; i--) {
for (let j = 0; j < 4; j++) {
const point_index = this.getVertexIndex(i, j);
if (point_index >= range_start && point_index <= range_end) {
if (!this.exists(i)) {
// tet doesn't actually exist (deallocated)
break;
}
// point index is in range, tetra should be removed
this.disconnect(i);
this.delete(i);
break;
}
}
}
}
/**
* Note that this method does not guarantee to find the containing tet in case of concave mesh, that is - if there is a gap between the starting tet and the countaining tet
* @param {number} x
* @param {number} y
* @param {number} z
* @param {number[]} points Positions of vertices of tetrahedrons
* @param {number} [start_tetrahedron]
* @returns {number} index of tetra or -1 if no containing tetra found
*/
walkToTetraContainingPoint(x, y, z, points, start_tetrahedron = 0) {
let entering_face = 4;
let cur_tet = start_tetrahedron;
let i;
for (let steps_remaining = this.count + 1; steps_remaining > 0; steps_remaining--) {
for (i = 0; i < 4; i++) {
// we walk whenever the volume is positive
const a_i = (i + 1) & 3;
const b_i = (i & 2) ^ 3;
const c_i = (i + 3) & 2;
const a_index = this.getVertexIndex(cur_tet, a_i);
const b_index = this.getVertexIndex(cur_tet, b_i);
const c_index = this.getVertexIndex(cur_tet, c_i);
const a3 = a_index * 3;
const b3 = b_index * 3;
const c3 = c_index * 3;
const ax = points[a3];
const ay = points[a3 + 1];
const az = points[a3 + 2];
const bx = points[b3];
const by = points[b3 + 1];
const bz = points[b3 + 2];
const cx = points[c3];
const cy = points[c3 + 1];
const cz = points[c3 + 2];
if (i !== entering_face && orient3d(ax, ay, az, bx, by, bz, cx, cy, cz, x, y, z) < 0.0) {
// point is outside the tet on the neighbour's side, move in that direction
const neighbour = this.getNeighbour(cur_tet, i);
if (neighbour === INVALID_NEIGHBOUR) {
// walked outside the mesh, point is not contained within
return -1;
}
// assert.notEqual(neighbour, INVALID_NEIGHBOUR, 'walked outside of the mesh');
cur_tet = neighbour >>> 2;
entering_face = neighbour & 3;
break;
}
}
if (i === 4) {
// point is inside the tet
return cur_tet;
}
}
throw new Error(`Failed to find tet, likely mesh is corrupted or non-convex`);
}
/**
* Relocate tetrahedron in memory, patches neighbourhood links as well
* NOTE: The destination slot will be overwritten. This is a dangerous method that can break the topology, make sure you fully understand what you are doing when using it
* @param {number} source_index index of source tetrahedron
* @param {number} destination_index new index, where the source tetrahedron is to be moved
*/
relocate(source_index, destination_index) {
assert.isNonNegativeInteger(source_index, 'source_index');
assert.isNonNegativeInteger(destination_index, 'destination_index');
if (source_index === destination_index) {
// avoid unnecessary work
return;
}
// validate_tetrahedron_neighbourhood(this, source_index, console.error);
// patch neighbours
for (let i = 0; i < 4; i++) {
const encoded_neighbour = this.getNeighbour(source_index, i);
if (encoded_neighbour === INVALID_NEIGHBOUR) {
// no neighbour
continue;
}
const neighbour_index = encoded_neighbour >> 2;
const neighbour_vertex = encoded_neighbour & 3;
const encoded_tet = (destination_index << 2) | (i & 3);
assert.equal(this.getNeighbour(neighbour_index, neighbour_vertex), (source_index << 2) | (i & 3), 'invalid source state');
this.setNeighbour(neighbour_index, neighbour_vertex, encoded_tet);
}
const layout_word_size = LAYOUT_TETRA_BYTE_SIZE >> 2;
array_copy(
this.__data_uint32, source_index * layout_word_size,
this.__data_uint32, destination_index * layout_word_size,
layout_word_size
);
// validate_tetrahedron_neighbourhood(this, destination_index, console.error);
}
/**
* Perform compaction, removing unused memory slots
* NOTE: existing tetrahedron indices can become invalidated as tets are moved into free slots
* @returns {number} number of relocated elements
*/
compact() {
// sort free
array_quick_sort_by_comparator(this.__free, number_compare_descending, null, 0, this.__free_pointer - 1);
let relocation_count = 0;
let free_head_pointer = 0;
while (this.__free_pointer > free_head_pointer) {
const last_used = this.__used_end - 1;
if (this.__free[free_head_pointer] >= last_used) {
/*
The slot is actually free.
As we have sorted the free slots, it's expected to be at the start of the list
Adjust pointers and continue onto the next slot
*/
free_head_pointer++
this.__used_end = last_used;
continue;
}
const free_slot = this.__free[this.__free_pointer - 1];
this.__free_pointer--;
if (last_used <= free_slot) {
continue;
}
this.relocate(last_used, free_slot);
relocation_count++;
this.__used_end--;
}
this.__free.splice(0, this.__free.length); // release memory
this.__free_pointer = 0;
return relocation_count;
}
/**
*
* @param {BinaryBuffer} buffer
*/
serialize(buffer) {
buffer.writeUint32(1); // format version
buffer.writeUintVar(this.__used_end);
buffer.writeUintVar(this.__free_pointer);
// record main buffer
buffer.writeUint32Array(this.__data_uint32, 0, this.__used_end * LAYOUT_TETRA_WORD_COUNT);
buffer.writeUint32Array(this.__free, 0, this.__free_pointer);
}
/**
*
* @param {BinaryBuffer} buffer
*/
deserialize(buffer) {
const version_number = buffer.readUint32();
if (version_number !== 1) {
throw new Error(`Unsupported version number, expected ${1}, instead got ${version_number}`);
}
this.__used_end = buffer.readUintVar();
this.__free_pointer = buffer.readUintVar();
this.ensureCapacity(this.__used_end);
buffer.readUint32Array(this.__data_uint32, 0, this.__used_end * LAYOUT_TETRA_WORD_COUNT);
buffer.readUint32Array(this.__free, 0, this.__free_pointer);
}
/**
* Turns data into a base64 encoded string
* @return {string}
*/
serialize_base64() {
const buffer = new BinaryBuffer();
buffer.endianness = EndianType.LittleEndian;
this.serialize(buffer);
buffer.trim();
return Base64.encode(buffer.data);
}
/**
* Dual of serialization method, decodes a base64 representation
* @param {string} str
*/
deserialize_base64(str) {
const array_buffer = Base64.decode(str);
const buffer = BinaryBuffer.fromArrayBuffer(array_buffer);
buffer.endianness = EndianType.LittleEndian;
this.deserialize(buffer);
}
}
/**
* @readonly
* @type {boolean}
*/
TetrahedralMesh.prototype.isTetrahedralMesh = true;