Home Reference Source

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;

	}

}