UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

808 lines (634 loc) 24.9 kB
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;