Home Reference Source

src/octree/world/WorldOctreeCSG.js

import { Box3, Vector3 } from "three";
import { layout } from "sparse-octree";
import { OperationType } from "../../volume/csg/OperationType";
import { IntermediateWorldOctant } from "./IntermediateWorldOctant";
import { WorldOctant } from "./WorldOctant";

/**
 * A point.
 *
 * @type {Vector3}
 * @private
 */

const p = new Vector3();

/**
 * A vector.
 *
 * @type {Vector3}
 * @private
 */

const v = new Vector3();

/**
 * A box.
 *
 * @type {Box3}
 * @private
 */

const b0 = new Box3();

/**
 * A box.
 *
 * @type {Box3}
 * @private
 */

const b1 = new Box3();

/**
 * A box.
 *
 * @type {Box3}
 * @private
 */

const b2 = new Box3();

/**
 * A list of key coordinate ranges used for octant culling during the recursive
 * octree traversal.
 *
 * @type {Box3[]}
 * @private
 */

const ranges = [];

/**
 * Recursively applies the given SDF to existing octants in the affected region.
 *
 * This is a depth-first approach.
 *
 * @private
 * @param {WorldOctree} world - The world octree.
 * @param {SignedDistanceFunction} sdf - An SDF with a primary Difference CSG type.
 * @param {WorldOctant} octant - The current octant.
 * @param {Number} keyX - The X-coordinate of the current octant's key.
 * @param {Number} keyY - The Y-coordinate of the current octant's key.
 * @param {Number} keyZ - The Z-coordinate of the current octant's key.
 * @param {Number} lod - The current LOD.
 */

function applyDifference(world, sdf, octant, keyX, keyY, keyZ, lod) {

	let grid, keyDesign, children;
	let range, offset, i;

	octant.csg.add(sdf);

	if(lod > 0) {

		// Look at the next lower LOD.
		--lod;

		grid = world.getGrid(lod);
		keyDesign = world.getKeyDesign();
		children = octant.children;
		range = ranges[lod];

		// 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];

				p.set(
					keyX + offset[0],
					keyY + offset[1],
					keyZ + offset[2]
				);

				// Check if the child is affected.
				if(range.containsPoint(p)) {

					// Apply the difference operation to the child.
					applyDifference(world, sdf, grid.get(keyDesign.packKey(p)), p.x, p.y, p.z, lod);

				}

			}

		}

	}

}

/**
 * A world octree CSG operation manager.
 */

export class WorldOctreeCSG {

	/**
	 * Modifies all octants in the specified region with the given SDF.
	 *
	 * Octants that don't exist will be created across all LOD grids.
	 *
	 * @private
	 * @param {WorldOctree} world - A world octree.
	 * @param {Box3} region - The affected region.
	 * @param {SignedDistanceFunction} sdf - An SDF with a primary Union CSG type.
	 */

	static applyUnion(world, region, sdf) {

		const keyDesign = world.getKeyDesign();
		const lodZero = world.lodZero;

		const a = b1.min;
		const b = b1.max;
		const c = b2.min;
		const d = b2.max;
		const range = b2;

		let key, offset;
		let grid, octant;
		let lod, i;

		// Process LOD N to 1.
		for(lod = world.getDepth(); lod > 0; --lod) {

			grid = world.getGrid(lod);

			// Calculate a key coordinate range for this LOD and the next lower LOD.
			world.calculateKeyCoordinates(region.min, lod, a);
			world.calculateKeyCoordinates(region.max, lod, b);
			world.calculateKeyCoordinates(region.min, lod - 1, c);
			world.calculateKeyCoordinates(region.max, lod - 1, d);

			for(key of keyDesign.keyRange(a, b)) {

				if(!grid.has(key)) {

					octant = new IntermediateWorldOctant();
					octant.csg.add(sdf);
					grid.set(key, octant);

					// Translate the key coordinates to the next lower LOD.
					keyDesign.unpackKey(key, v);
					v.x <<= 1; v.y <<= 1; v.z <<= 1;

					// Determine the existence of the child octants.
					for(i = 0; i < 8; ++i) {

						offset = layout[i];

						p.set(
							v.x + offset[0],
							v.y + offset[1],
							v.z + offset[2]
						);

						if(range.containsPoint(p)) {

							// The child exists! store the information in bit i.
							octant.children |= (1 << i);

						}

					}

				}

			}

		}

		// Finally, process LOD zero and add the SDF to the leaf octants.
		world.calculateKeyCoordinates(region.min, 0, a);
		world.calculateKeyCoordinates(region.max, 0, b);

		for(key of keyDesign.keyRange(a, b)) {

			if(!lodZero.has(key)) {

				octant = new WorldOctant();
				lodZero.set(key, octant);

			} else {

				octant = lodZero.get(key);

			}

			octant.csg.add(sdf);

		}

	}

	/**
	 * Modifies existing octants in the specified region with the given SDF.
	 *
	 * Calculating an entry LOD depending on the longest side of the affected
	 * region could improve performance, but by skipping higher LOD grid some
	 * intermediate octants won't be affected by the SDF.
	 *
	 * ```js
	 * const min = region.min;
	 * const max = region.max;
	 *
	 * const s = Math.max(
	 * 	Math.max(Math.max(Math.abs(min.x), Math.abs(min.y)), Math.abs(min.z)),
	 * 	Math.max(Math.max(Math.abs(max.x), Math.abs(max.y)), Math.abs(max.z))
	 * );
	 *
	 * const quotientCeiled = Math.ceil(s / world.getCellSize());
	 * const doublingsCeiled = Math.ceil(Math.log2(quotientCeiled));
	 * const lod = Math.min(doublingsCeiled, world.getDepth());
	 * ```
	 *
	 * @private
	 * @param {WorldOctree} world - A world octree.
	 * @param {Box3} region - The affected region.
	 * @param {SignedDistanceFunction} sdf - An SDF with a primary Difference CSG type.
	 */

	static applyDifference(world, region, sdf) {

		const lod = world.getDepth();
		const keyDesign = world.getKeyDesign();
		const grid = world.getGrid(lod);

		// Consider all octants of the entry LOD grid that lie in the given region.
		const a = world.calculateKeyCoordinates(region.min, lod, b1.min);
		const b = world.calculateKeyCoordinates(region.max, lod, b1.max);

		let i, l;
		let range;
		let key;

		// Precompute key coordinate ranges for the lower LOD grids.
		for(i = 0, l = lod - 1; i < l; ++i) {

			if(i < ranges.length) {

				// Reuse a cached box.
				range = ranges[i];

				world.calculateKeyCoordinates(region.min, i, range.min);
				world.calculateKeyCoordinates(region.max, i, range.max);

			} else {

				// Create a new box for this LOD and cache it.
				ranges.push(new Box3(
					world.calculateKeyCoordinates(region.min, i),
					world.calculateKeyCoordinates(region.max, i)
				));

			}

		}

		// Delve into the octant structures.
		for(key of keyDesign.keyRange(a, b)) {

			if(grid.has(key)) {

				keyDesign.unpackKey(key, v);

				// Recursively modify affected LOD zero cells.
				applyDifference(world, sdf, grid.get(key), v.x, v.y, v.z, lod);

			}

		}

	}

	/**
	 * Modifies all existing octants.
	 *
	 * Warning: This CSG operation is highly destructive and expensive when used
	 * as a primary operation. It should rather be used in CSG composites where it
	 * can only affect local data.
	 *
	 * @private
	 * @param {WorldOctree} world - A world octree.
	 * @param {SignedDistanceFunction} sdf - An SDF with a primary Intersection CSG type.
	 */

	static applyIntersection(world, sdf) {

		let lod, octant;

		for(lod = world.getDepth(); lod >= 0; --lod) {

			for(octant of world.getGrid(lod).values()) {

				octant.csg.add(sdf);

			}

		}

	}

	/**
	 * Applies the given SDF to the affected octants.
	 *
	 * @param {WorldOctree} world - A world octree.
	 * @param {SignedDistanceFunction} sdf - An SDF.
	 */

	static applyCSG(world, sdf) {

		// Calculate the area of effect.
		const region = b0.copy(sdf.getBoundingBox(true));

		// Limit the affected region to the world boundaries.
		region.min.max(world.min);
		region.max.min(world.max);

		switch(sdf.operation) {

			case OperationType.UNION:
				this.applyUnion(world, region, sdf);
				break;

			case OperationType.DIFFERENCE:
				this.applyDifference(world, region, sdf);
				break;

			case OperationType.INTERSECTION:
				this.applyIntersection(world, sdf);
				break;

			default:
				console.error("No CSG operation type specified", sdf);
				break;

		}

	}

}