Home Reference Source

src/volume/sdf/SignedDistanceFunction.js

import { Matrix4, Quaternion, Vector3 } from "three";
import { OperationType } from "../csg/OperationType";
import { Union } from "../csg/Union";
import { Difference } from "../csg/Difference";
import { Intersection } from "../csg/Intersection";
import { DensityFunction } from "../csg/DensityFunction";
import { Material } from "../Material";

const m = new Matrix4();

/**
 * An abstract Signed Distance Function.
 *
 * An SDF describes the signed Euclidean distance to the surface of an object,
 * effectively describing its density at every point in 3D space. It yields
 * negative values for points that lie inside the volume and positive values
 * for points outside. The value is zero at the exact boundary of the object.
 *
 * @implements {Serializable}
 * @implements {TransferableContainer}
 */

export class SignedDistanceFunction {

	/**
	 * Constructs a new base SDF.
	 *
	 * @param {SDFType} type - The type of the SDF.
	 * @param {Number} [material=Material.SOLID] - A material index. Must be an integer in the range of 1 to 255.
	 */

	constructor(type, material = Material.SOLID) {

		/**
		 * The type of this SDF.
		 *
		 * @type {SDFType}
		 */

		this.type = type;

		/**
		 * The operation type.
		 *
		 * @type {OperationType}
		 */

		this.operation = null;

		/**
		 * A material index.
		 *
		 * @type {Number}
		 */

		this.material = Math.min(255, Math.max(Material.SOLID, Math.trunc(material)));

		/**
		 * The axis-aligned bounding box of this SDF.
		 *
		 * @type {Box3}
		 * @protected
		 */

		this.boundingBox = null;

		/**
		 * The positional translation.
		 *
		 * Call {@link updateInverseTransformation} after changing this field.
		 *
		 * @type {Vector3}
		 */

		this.position = new Vector3();

		/**
		 * The rotation.
		 *
		 * Call {@link updateInverseTransformation} after changing this field.
		 *
		 * @type {Quaternion}
		 */

		this.quaternion = new Quaternion();

		/**
		 * The scale.
		 *
		 * Call {@link updateInverseTransformation} after changing this field.
		 *
		 * @type {Vector3}
		 */

		this.scale = new Vector3(1, 1, 1);

		/**
		 * The inverted transformation matrix.
		 *
		 * @type {Matrix4}
		 */

		this.inverseTransformation = new Matrix4();

		this.updateInverseTransformation();

		/**
		 * A list of SDFs.
		 *
		 * SDFs can be chained to build CSG expressions.
		 *
		 * @type {SignedDistanceFunction[]}
		 * @private
		 */

		this.children = [];

	}

	/**
	 * Composes a transformation matrix using the translation, rotation and scale
	 * of this SDF.
	 *
	 * The transformation matrix is not needed for most SDF calculations and is
	 * therefore not stored explicitly to save space.
	 *
	 * @param {Matrix4} [matrix] - A matrix to store the transformation in.
	 * @return {Matrix4} The transformation matrix.
	 */

	getTransformation(matrix = m) {

		return matrix.compose(this.position, this.quaternion, this.scale);

	}

	/**
	 * Calculates the AABB of this SDF if it doesn't exist yet and returns it.
	 *
	 * @param {Boolean} [recursive=false] - Whether the child SDFs should be taken into account.
	 * @return {Box3} The bounding box.
	 */

	getBoundingBox(recursive = false) {

		const children = this.children;

		let boundingBox = this.boundingBox;
		let i, l;

		if(boundingBox === null) {

			boundingBox = this.computeBoundingBox();
			this.boundingBox = boundingBox;

		}

		if(recursive) {

			boundingBox = boundingBox.clone();

			for(i = 0, l = children.length; i < l; ++i) {

				boundingBox.union(children[i].getBoundingBox(recursive));

			}

		}

		return boundingBox;

	}

	/**
	 * Sets the material.
	 *
	 * @param {Material} material - The material. Must be an integer in the range of 1 to 255.
	 * @return {SignedDistanceFunction} This SDF.
	 */

	setMaterial(material) {

		this.material = Math.min(255, Math.max(Material.SOLID, Math.trunc(material)));

		return this;

	}

	/**
	 * Sets the CSG operation type of this SDF.
	 *
	 * @param {OperationType} operation - The CSG operation type.
	 * @return {SignedDistanceFunction} This SDF.
	 */

	setOperationType(operation) {

		this.operation = operation;

		return this;

	}

	/**
	 * Updates the inverse transformation matrix.
	 *
	 * This method should be called after the position, quaternion or scale has
	 * changed. The bounding box will be updated automatically.
	 *
	 * @return {SignedDistanceFunction} This SDF.
	 */

	updateInverseTransformation() {

		this.getTransformation(this.inverseTransformation).invert();
		this.boundingBox = null;

		return this;

	}

	/**
	 * Adds the given SDF to this one.
	 *
	 * @param {SignedDistanceFunction} sdf - An SDF.
	 * @return {SignedDistanceFunction} This SDF.
	 */

	union(sdf) {

		this.children.push(sdf.setOperationType(OperationType.UNION));

		return this;

	}

	/**
	 * Subtracts the given SDF from this one.
	 *
	 * @param {SignedDistanceFunction} sdf - An SDF.
	 * @return {SignedDistanceFunction} This SDF.
	 */

	subtract(sdf) {

		this.children.push(sdf.setOperationType(OperationType.DIFFERENCE));

		return this;

	}

	/**
	 * Intersects the given SDF with this one.
	 *
	 * @param {SignedDistanceFunction} sdf - An SDF.
	 * @return {SignedDistanceFunction} This SDF.
	 */

	intersect(sdf) {

		this.children.push(sdf.setOperationType(OperationType.INTERSECTION));

		return this;

	}

	/**
	 * Translates this SDF into a CSG expression.
	 *
	 * @return {Operation} A CSG operation.
	 * @example a.union(b.intersect(c)).union(d).subtract(e) => Difference(Union(a, Intersection(b, c), d), e)
	 */

	toCSG() {

		const children = this.children;

		let operation = new DensityFunction(this);
		let operationType;
		let child;
		let i, l;

		for(i = 0, l = children.length; i < l; ++i) {

			child = children[i];

			if(operationType !== child.operation) {

				operationType = child.operation;

				switch(operationType) {

					case OperationType.UNION:
						operation = new Union(operation);
						break;

					case OperationType.DIFFERENCE:
						operation = new Difference(operation);
						break;

					case OperationType.INTERSECTION:
						operation = new Intersection(operation);
						break;

				}

			}

			operation.children.push(child.toCSG());

		}

		return operation;

	}

	/**
	 * Serialises this SDF.
	 *
	 * @param {Boolean} [deflate=false] - Whether the data should be compressed if possible.
	 * @return {Object} The serialised data.
	 */

	serialize(deflate = false) {

		const result = {
			type: this.type,
			operation: this.operation,
			material: this.material,
			position: this.position.toArray(),
			quaternion: this.quaternion.toArray(),
			scale: this.scale.toArray(),
			parameters: null,
			children: []
		};

		let i, l;

		for(i = 0, l = this.children.length; i < l; ++i) {

			result.children.push(this.children[i].serialize(deflate));

		}

		return result;

	}

	/**
	 * Creates a list of transferable items.
	 *
	 * @param {Array} [transferList] - An optional target list. The transferable items will be added to this list.
	 * @return {Transferable[]} The transfer list.
	 */

	createTransferList(transferList = []) {

		return transferList;

	}

	/**
	 * Returns a plain object that describes this SDF.
	 *
	 * @return {Object} A simple description of this SDF.
	 */

	toJSON() {

		return this.serialize(true);

	}

	/**
	 * Calculates the bounding box of this SDF.
	 *
	 * @protected
	 * @throws {Error} An error is thrown if the method is not overridden.
	 * @return {Box3} The bounding box.
	 */

	computeBoundingBox() {

		throw new Error("SignedDistanceFunction#computeBoundingBox method not implemented!");

	}

	/**
	 * Samples the volume's density at the given point in space.
	 *
	 * @throws {Error} An error is thrown if the method is not overridden.
	 * @param {Vector3} position - A position.
	 * @return {Number} The Euclidean distance to the surface.
	 */

	sample(position) {

		throw new Error("SignedDistanceFunction#sample method not implemented!");

	}

}