src/octree/world/WorldOctree.js
- import { Vector3 } from "three";
- import { layout } from "sparse-octree";
- import { KeyDesign } from "./KeyDesign";
- import { WorldOctantIterator } from "./WorldOctantIterator";
- import { WorldOctantWrapper } from "./WorldOctantWrapper";
- import { WorldOctreeCSG } from "./WorldOctreeCSG";
- import { WorldOctreeRaycaster } from "./WorldOctreeRaycaster";
-
- /**
- * A vector.
- *
- * @type {Vector3}
- * @private
- */
-
- const v = new Vector3();
-
- /**
- * Recursively deletes octant children.
- *
- * @param {WorldOctree} world - A world octree.
- * @param {WorldOctant} octant - The current octant.
- * @param {Number} keyX - The X-coordinate of the current octant key.
- * @param {Number} keyY - The Y-coordinate of the current octant key.
- * @param {Number} keyZ - The Z-coordinate of the current octant key.
- * @param {Number} lod - The current LOD value.
- */
-
- function removeChildren(world, octant, keyX, keyY, keyZ, lod) {
-
- let grid, keyDesign;
- let children, child;
- let offset, key, i;
-
- // The octants from LOD zero have no children.
- if(lod > 0) {
-
- // Look at the next lower LOD.
- --lod;
-
- grid = world.getGrid(lod);
- keyDesign = world.getKeyDesign();
- children = octant.children;
-
- // Translate the key coordinates to the next lower LOD.
- keyX <<= 1; keyY <<= 1; keyZ <<= 1;
-
- for(i = 0; i < 8; ++i) {
-
- // Check if the child exists.
- if((children & (1 << i)) !== 0) {
-
- offset = layout[i];
-
- v.set(
- keyX + offset[0],
- keyY + offset[1],
- keyZ + offset[2]
- );
-
- key = keyDesign.packKey(v);
-
- // Fetch the child and remove it from the grid.
- child = grid.get(key);
- grid.delete(key);
-
- removeChildren(world, child, v.x, v.y, v.z, lod);
-
- }
-
- }
-
- octant.children = 0;
-
- }
-
- }
-
- /**
- * Recursively removes empty parent nodes.
- *
- * @param {WorldOctree} world - A world octree.
- * @param {Number} keyX - The X-coordinate of the deleted octant's key.
- * @param {Number} keyY - The Y-coordinate of the deleted octant's key.
- * @param {Number} keyZ - The Z-coordinate of the deleted octant's key.
- * @param {Number} lod - The current LOD value.
- */
-
- function prune(world, keyX, keyY, keyZ, lod) {
-
- let grid, i, key, parent;
-
- if(++lod < world.levels) {
-
- // Look at the next higher LOD grid.
- grid = world.getGrid(lod);
-
- // Determine the position of the deleted octant relative to its parent.
- i = WorldOctree.calculateOffsetIndex(keyX, keyY, keyZ);
-
- // Translate the key coordinates to the next higher LOD.
- v.set(keyX >>> 1, keyY >>> 1, keyZ >>> 1);
-
- // The resulting coordinates identify the parent octant.
- key = world.getKeyDesign().packKey(v);
- parent = grid.get(key);
-
- // Unset the existence flag of the deleted child.
- parent.children &= ~(1 << i);
-
- // Check if there are any children left.
- if(parent.children === 0) {
-
- // Remove the empty parent and recur.
- grid.delete(key);
- prune(world, v.x, v.y, v.z, lod);
-
- }
-
- }
-
- }
-
- /**
- * An octree that subdivides space for fast spatial searches.
- *
- * The purpose of this linear octree is to efficiently organise volume data. It
- * allows direct access to different LOD layers, octant neighbours and parents.
- *
- * The world octree is axis-aligned and cannot be rotated.
- */
-
- export class WorldOctree {
-
- /**
- * Constructs a new world octree.
- *
- * Each octant can be uniquely identified by a 3D coordinate and a LOD value.
- * The individual values for X, Y and Z are combined into a 53-bit key.
- *
- * @param {Number} [cellSize=20] - The size of the smallest octants in LOD zero. Must be an integer i such that 0 < i < 2 ** (33 - levels).
- * @param {Number} [levels=8] - The amount of detail levels. Must be an integer i such that 0 < i < 33.
- * @param {KeyDesign} [keyDesign] - The bit allotments for the octant coordinates.
- */
-
- constructor(cellSize = 20, levels = 8, keyDesign = new KeyDesign()) {
-
- levels = Math.max(Math.min(Math.trunc(levels), 32), 1);
-
- /**
- * The LOD zero cell size.
- *
- * @type {Number}
- * @private
- */
-
- this.cellSize = Math.max(Math.min(Math.trunc(cellSize), Math.pow(2, 33 - levels) - 1), 1);
-
- /**
- * The octant key design.
- *
- * @type {KeyDesign}
- * @private
- */
-
- this.keyDesign = keyDesign;
-
- /**
- * The octant LOD grids.
- *
- * @type {Map[]}
- * @private
- */
-
- this.grids = [];
-
- while(this.grids.length < levels) {
-
- this.grids.push(new Map());
-
- }
-
- /**
- * An empty octant wrapper that merely holds the bounds of this world.
- *
- * @type {WorldOctantWrapper}
- * @private
- */
-
- this.bounds = new WorldOctantWrapper();
-
- this.bounds.min.copy(this.keyDesign.halfRange).multiplyScalar(-this.cellSize);
- this.bounds.max.copy(this.keyDesign.halfRange).multiplyScalar(this.cellSize);
-
- }
-
- /**
- * The lower bounds of this world.
- *
- * @type {Vector3}
- */
-
- get min() {
-
- return this.bounds.min;
-
- }
-
- /**
- * The upper bounds of this world.
- *
- * @type {Vector3}
- */
-
- get max() {
-
- return this.bounds.max;
-
- }
-
- /**
- * The amount of detail levels. This value can not be changed.
- *
- * @type {Number}
- */
-
- get levels() {
-
- return this.grids.length;
-
- }
-
- /**
- * The LOD zero octant grid.
- *
- * @type {Number}
- */
-
- get lodZero() {
-
- return this.grids[0];
-
- }
-
- /**
- * Returns the key design.
- *
- * @return {KeyDesign} The key design.
- */
-
- getKeyDesign() {
-
- return this.keyDesign;
-
- }
-
- /**
- * Returns the size of the cells in the specified LOD grid.
- *
- * @param {Number} [lod=0] - The LOD. Must be an integer; fractions will be truncated.
- * @return {Number} The cell size.
- */
-
- getCellSize(lod = 0) {
-
- return (this.cellSize << lod) >>> 0;
-
- }
-
- /**
- * Computes the center of this world.
- *
- * @param {Vector3} [target] - A target vector. If none is provided, a new one will be created.
- * @return {Vector3} A vector that describes the center of this world.
- */
-
- getCenter(target) {
-
- return this.bounds.getCenter(target);
-
- }
-
- /**
- * Sets the center of this world.
- *
- * Keeping the center at (0, 0, 0) is recommended because a large offset can
- * lead to floating point coordinate imprecisions.
- *
- * @param {Vector3} center - The new center.
- */
-
- setCenter(center) {
-
- this.min.copy(this.keyDesign.halfRange).multiplyScalar(-this.cellSize).add(center);
- this.max.copy(this.keyDesign.halfRange).multiplyScalar(this.cellSize).add(center);
-
- }
-
- /**
- * Computes the size of this world.
- *
- * @param {Vector3} [target] - A target vector. If none is provided, a new one will be created.
- * @return {Vector3} A vector that describes the size of this world.
- */
-
- getDimensions(target) {
-
- return this.bounds.getDimensions(target);
-
- }
-
- /**
- * The world octree depth is constant and corresponds to the amount of detail
- * levels.
- *
- * @return {Number} The octree depth.
- */
-
- getDepth() {
-
- return this.grids.length - 1;
-
- }
-
- /**
- * Returns a specific LOD grid.
- *
- * @param {Number} lod - The LOD of the grid.
- * @return {Map} The requested LOD grid or undefined if the given LOD is out of bounds.
- */
-
- getGrid(lod) {
-
- return (lod >= 0 && lod < this.grids.length) ? this.grids[lod] : undefined;
-
- }
-
- /**
- * Removes all octants.
- */
-
- clear() {
-
- let i, l;
-
- for(i = 0, l = this.grids.length; i < l; ++i) {
-
- this.grids[i].clear();
-
- }
-
- }
-
- /**
- * Checks if the given point lies inside this octree's boundaries.
- *
- * @param {Vector3} point - A point.
- * @return {Boolean} Whether the given point lies inside this octree.
- */
-
- containsPoint(point) {
-
- return this.bounds.containsPoint(point);
-
- }
-
- /**
- * Fetches all octants of the specified LOD.
- *
- * @param {Number} level - The LOD.
- * @return {Iterable} A collection that contains the octants of the specified LOD.
- */
-
- findOctantsByLevel(level) {
-
- return this.octants(level);
-
- }
-
- /**
- * Calculates key coordinates based on a given position and LOD.
- *
- * @param {Vector3} position - A position.
- * @param {Number} lod - The target LOD.
- * @param {Vector3} [target] - A vector to store the result in. If none is provided, a new one will be created.
- * @return {Vector3} The key coordinates.
- */
-
- calculateKeyCoordinates(position, lod, target = new Vector3()) {
-
- const cellSize = this.cellSize << lod;
-
- // Translate to the origin (zero-based unsigned coordinates).
- v.subVectors(position, this.min);
-
- target.set(
- Math.trunc(v.x / cellSize),
- Math.trunc(v.y / cellSize),
- Math.trunc(v.z / cellSize)
- );
-
- return target;
-
- }
-
- /**
- * Retrieves the octant of a specific LOD that contains the given point.
- *
- * @param {Vector3} point - A point.
- * @param {Number} [lod=0] - A LOD value.
- * @return {WorldOctant} The octant that contains the point or undefined if it doesn't exist.
- */
-
- getOctantByPoint(point, lod = 0) {
-
- const keyDesign = this.keyDesign;
- const grid = this.getGrid(lod);
-
- let result;
-
- if(grid !== undefined) {
-
- if(this.containsPoint(point)) {
-
- this.calculateKeyCoordinates(point, lod, v);
- result = grid.get(keyDesign.packKey(v));
-
- } else {
-
- console.error("Position out of range", point);
-
- }
-
- } else {
-
- console.error("Invalid LOD", lod);
-
- }
-
- return result;
-
- }
-
- /**
- * Removes a specific octant by a given key.
- *
- * Children and empty parent nodes will be removed as well.
- *
- * @param {Number} key - The key of the octant that should be removed.
- * @param {Number} [lod=0] - The LOD of the octant.
- */
-
- removeOctant(key, lod = 0) {
-
- const keyDesign = this.keyDesign;
- const grid = this.getGrid(lod);
-
- let keyX, keyY, keyZ;
-
- if(grid !== undefined) {
-
- if(grid.has(key)) {
-
- // Note: v will be modified by removeChildren and prune.
- keyDesign.unpackKey(key, v);
- keyX = v.x; keyY = v.y; keyZ = v.z;
-
- // Recursively delete all children in the lower LOD grids.
- removeChildren(this, grid.get(key), keyX, keyY, keyZ, lod);
-
- // Remove the octant.
- grid.delete(key);
-
- // Recursively delete empty parent nodes.
- prune(this, keyX, keyY, keyZ, lod);
-
- } else {
-
- console.error("No octant found", key);
-
- }
-
- } else {
-
- console.error("Invalid LOD", lod);
-
- }
-
- }
-
- /**
- * Applies the given SDF to the affected octants.
- *
- * @param {SignedDistanceFunction} sdf - An SDF.
- */
-
- applyCSG(sdf) {
-
- WorldOctreeCSG.applyCSG(this, sdf);
-
- }
-
- /**
- * Finds the octants that intersect with the given ray. The intersecting
- * octants are sorted by distance, closest first. Empty octants will not be
- * included in the result.
- *
- * @param {Ray} ray - A ray.
- * @param {Array} [intersects] - An optional target list to be filled with the intersecting octants.
- * @return {WorldOctant[]} The intersecting octants.
- */
-
- raycast(ray, intersects = []) {
-
- return WorldOctreeRaycaster.intersectWorldOctree(this, ray, intersects);
-
- }
-
- /**
- * Returns a new world octant iterator.
- *
- * The octants returned by this iterator are augmented with explicit
- * positional information. See {@link WorldOctantWrapper} for more details.
- *
- * @param {Number} [lod=0] - The LOD grid to consider.
- * @return {WorldOctantIterator} An iterator.
- */
-
- octants(lod = 0) {
-
- return new WorldOctantIterator(this, lod);
-
- }
-
- /**
- * Calculates an offset index from octant key coordinates.
- *
- * The index identifies the octant's positional offset relative to its parent:
- *
- * ```text
- * 0: [0, 0, 0]
- * 1: [0, 0, 1]
- * 2: [0, 1, 0]
- * 3: [0, 1, 1]
- * 4: [1, 0, 0]
- * 5: [1, 0, 1]
- * 6: [1, 1, 0]
- * 7: [1, 1, 1]
- * ```
- *
- * Note: This binary pattern is defined by the external sparse-octree module.
- *
- * For more information on fast bitwise modulo with power of two divisors see:
- * https://graphics.stanford.edu/~seander/bithacks.html#ModulusDivisionEasy
- *
- * @param {Number} x - The X-coordinate of the octant key.
- * @param {Number} y - The Y-coordinate of the octant key.
- * @param {Number} z - The Z-coordinate of the octant key.
- * @return {Number} The index of the relative position offset. Range: [0, 7].
- */
-
- static calculateOffsetIndex(x, y, z) {
-
- // Bitwise modulo: n % (1 << s) = n & ((1 << s) - 1) for positive integers.
- const offsetX = x & 1;
- const offsetY = y & 1;
- const offsetZ = z & 1;
-
- // Use a reversed packing order for correct indexing (X * 4 + Y * 2 + Z).
- return (offsetX << 2) + (offsetY << 1) + offsetZ;
-
- }
-
- }