Home Reference Source

src/volume/Edge.js

import { Vector3 } from "three";

/**
 * An isovalue bias for the Zero Crossing approximation.
 *
 * @type {Number}
 * @private
 */

const ISOVALUE_BIAS = 1e-4;

/**
 * An error threshold for the Zero Crossing approximation.
 *
 * @type {Number}
 * @private
 */

const INTERVAL_THRESHOLD = 1e-6;

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

const ab = new Vector3();

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

const p = new Vector3();

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

const v = new Vector3();

/**
 * An edge between two material grid points.
 */

export class Edge {

	/**
	 * Constructs a new edge.
	 *
	 * @param {Vector3} [a] - A starting point. If none is provided, a new vector will be created.
	 * @param {Vector3} [b] - An ending point. If none is provided, a new vector will be created.
	 */

	constructor(a = new Vector3(), b = new Vector3()) {

		/**
		 * The starting point of the edge.
		 *
		 * @type {Vector3}
		 */

		this.a = a;

		/**
		 * The ending point of the edge.
		 *
		 * @type {Vector3}
		 */

		this.b = b;

		/**
		 * The index of the starting material grid point.
		 *
		 * @type {Number}
		 */

		this.index = -1;

		/**
		 * The local grid coordinates of the starting point.
		 *
		 * @type {Vector3}
		 */

		this.coordinates = new Vector3();

		/**
		 * The Zero Crossing interpolation value.
		 *
		 * @type {Number}
		 */

		this.t = 0.0;

		/**
		 * The surface normal at the Zero Crossing position.
		 *
		 * @type {Vector3}
		 */

		this.n = new Vector3();

	}

	/**
	 * Approximates the smallest density along the edge.
	 *
	 * @param {SignedDistanceFunction} sdf - A density field.
	 * @param {Number} [steps=8] - The maximum number of interpolation steps. Cannot be smaller than 2.
	 */

	approximateZeroCrossing(sdf, steps = 8) {

		const s = Math.max(1, steps - 1);

		let a = 0.0;
		let b = 1.0;
		let c = 0.0;
		let i = 0;

		let densityA, densityC;

		// Compute the vector from a to b.
		ab.subVectors(this.b, this.a);

		// Use bisection to find the root of the SDF.
		while(i <= s) {

			c = (a + b) / 2;

			p.addVectors(this.a, v.copy(ab).multiplyScalar(c));
			densityC = sdf.sample(p);

			if(Math.abs(densityC) <= ISOVALUE_BIAS || (b - a) / 2 <= INTERVAL_THRESHOLD) {

				// Solution found.
				break;

			} else {

				p.addVectors(this.a, v.copy(ab).multiplyScalar(a));
				densityA = sdf.sample(p);

				if(Math.sign(densityC) === Math.sign(densityA)) {

					a = c;

				} else {

					b = c;

				}

			}

			++i;

		}

		this.t = c;

	}

	/**
	 * Calculates the Zero Crossing position.
	 *
	 * @param {Vector3} target - A target for the Zero Crossing position. If none is provided, a new vector will be created.
	 * @return {Vector3} The Zero Crossing position.
	 */

	computeZeroCrossingPosition(target = new Vector3()) {

		return target.subVectors(this.b, this.a).multiplyScalar(this.t).add(this.a);

	}

	/**
	 * Computes the normal of the surface at the edge intersection.
	 *
	 * @param {SignedDistanceFunction} sdf - A density field.
	 * @return {Vector3} The normal.
	 * @todo Use analytical derivation instead of finite differences.
	 */

	computeSurfaceNormal(sdf) {

		const position = this.computeZeroCrossingPosition(ab);
		const E = 1e-3;

		const dx = sdf.sample(p.addVectors(position, v.set(E, 0, 0))) - sdf.sample(p.subVectors(position, v.set(E, 0, 0)));
		const dy = sdf.sample(p.addVectors(position, v.set(0, E, 0))) - sdf.sample(p.subVectors(position, v.set(0, E, 0)));
		const dz = sdf.sample(p.addVectors(position, v.set(0, 0, E))) - sdf.sample(p.subVectors(position, v.set(0, 0, E)));

		this.n.set(dx, dy, dz).normalize();

	}

}