Home Reference Source

src/volume/sdf/SuperPrimitive.js

import { Box3, Vector2, Vector3, Vector4 } from "three";
import { SignedDistanceFunction } from "./SignedDistanceFunction";
import { SDFType } from "./SDFType";

/**
 * The super primitive.
 *
 * A function that is able to represent a wide range of conic/rectangular-radial
 * primitives of genus 0 and 1: (round) box, sphere, cylinder, capped cone,
 * torus, capsule, pellet, pipe, etc.
 *
 * Reference:
 *  https://www.shadertoy.com/view/MsVGWG
 */

export class SuperPrimitive extends SignedDistanceFunction {

	/**
	 * Constructs a new super primitive.
	 *
	 * See {@link SuperPrimitivePreset} for a list of default configurations.
	 *
	 * @param {Object} parameters - The parameters.
	 * @param {Array} parameters.s - The size and genus weight [x, y, z, w].
	 * @param {Array} parameters.r - The corner radii [x, y, z].
	 * @param {Number} [material] - A material index.
	 * @example const cube = SuperPrimitive.create(SuperPrimitivePreset.CUBE);
	 */

	constructor(parameters = {}, material) {

		super(SDFType.SUPER_PRIMITIVE, material);

		/**
		 * The base size. The W-component affects the genus of the primitive.
		 *
		 * @type {Vector4}
		 * @private
		 */

		this.s0 = new Vector4(...parameters.s);

		/**
		 * The base corner radii.
		 *
		 * @type {Vector3}
		 * @private
		 */

		this.r0 = new Vector3(...parameters.r);

		/**
		 * The size, adjusted for further calculations.
		 *
		 * @type {Vector4}
		 * @private
		 */

		this.s = new Vector4();

		/**
		 * The corner radii, adjusted for further calculations.
		 *
		 * @type {Vector3}
		 * @private
		 */

		this.r = new Vector3();

		/**
		 * Precomputed corner rounding constants.
		 *
		 * @type {Vector2}
		 * @private
		 */

		this.ba = new Vector2();

		/**
		 * The bottom radius offset.
		 *
		 * @type {Number}
		 * @private
		 */

		this.offset = 0;

		// Calculate constants ahead of time.
		this.precompute();

	}

	/**
	 * Sets the size and genus weight.
	 *
	 * @param {Number} x - X.
	 * @param {Number} y - Y.
	 * @param {Number} z - Z.
	 * @param {Number} w - W.
	 * @return {SuperPrimitive} This instance.
	 */

	setSize(x, y, z, w) {

		this.s0.set(x, y, z, w);

		return this.precompute();

	}

	/**
	 * Sets the corner radii.
	 *
	 * @param {Number} x - X.
	 * @param {Number} y - Y.
	 * @param {Number} z - Z.
	 * @return {SuperPrimitive} This instance.
	 */

	setRadii(x, y, z) {

		this.r0.set(x, y, z);

		return this.precompute();

	}

	/**
	 * Precomputes corner rounding factors.
	 *
	 * @private
	 * @return {SuperPrimitive} This instance.
	 */

	precompute() {

		const s = this.s.copy(this.s0);
		const r = this.r.copy(this.r0);
		const ba = this.ba;

		s.x -= r.x;
		s.y -= r.x;

		r.x -= s.w;
		s.w -= r.y;

		s.z -= r.y;

		this.offset = -2.0 * s.z;

		ba.set(r.z, this.offset);
		const divisor = ba.dot(ba);

		if(divisor === 0.0) {

			// Y must not be 0 to prevent bad values for Z = 0 in the last term (*).
			ba.set(0.0, -1.0);

		} else {

			ba.divideScalar(divisor);

		}

		return this;

	}

	/**
	 * Calculates the bounding box of this SDF.
	 *
	 * @return {Box3} The bounding box.
	 */

	computeBoundingBox() {

		const s = this.s0;
		const boundingBox = new Box3();

		boundingBox.min.x = Math.min(-s.x, -1.0);
		boundingBox.min.y = Math.min(-s.y, -1.0);
		boundingBox.min.z = Math.min(-s.z, -1.0);

		boundingBox.max.x = Math.max(s.x, 1.0);
		boundingBox.max.y = Math.max(s.y, 1.0);
		boundingBox.max.z = Math.max(s.z, 1.0);

		boundingBox.applyMatrix4(this.getTransformation());

		return boundingBox;

	}

	/**
	 * Samples the volume's density at the given point in space.
	 *
	 * @param {Vector3} position - A position.
	 * @return {Number} The euclidean distance to the surface.
	 */

	sample(position) {

		position.applyMatrix4(this.inverseTransformation);

		const s = this.s;
		const r = this.r;
		const ba = this.ba;

		const dx = Math.abs(position.x) - s.x;
		const dy = Math.abs(position.y) - s.y;
		const dz = Math.abs(position.z) - s.z;

		const mx0 = Math.max(dx, 0.0);
		const my0 = Math.max(dy, 0.0);
		const l0 = Math.sqrt(mx0 * mx0 + my0 * my0);

		const p = position.z - s.z;
		const q = Math.abs(l0 + Math.min(0.0, Math.max(dx, dy)) - r.x) - s.w;

		const c = Math.min(Math.max(q * ba.x + p * ba.y, 0.0), 1.0);
		const diagX = q - r.z * c;
		const diagY = p - this.offset * c;

		const hx0 = Math.max(q - r.z, 0.0);
		const hy0 = position.z + s.z;
		const hx1 = Math.max(q, 0.0);
		// hy1 = p;

		const diagSq = diagX * diagX + diagY * diagY;
		const h0Sq = hx0 * hx0 + hy0 * hy0;
		const h1Sq = hx1 * hx1 + p * p;
		const paBa = q * -ba.y + p * ba.x;

		const l1 = Math.sqrt(Math.min(diagSq, Math.min(h0Sq, h1Sq)));

		// (*) paBa must not be 0: if dz is also 0, the result will be wrong.
		return l1 * Math.sign(Math.max(paBa, dz)) - r.y;

	}

	/**
	 * 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 = super.serialize();

		result.parameters = {
			s: this.s0.toArray(),
			r: this.r0.toArray()
		};

		return result;

	}

	/**
	 * Creates a new primitive using the specified preset.
	 *
	 * @param {SuperPrimitivePreset} preset - The super primitive preset.
	 */

	static create(preset) {

		const parameters = superPrimitivePresets[preset];

		return new SuperPrimitive({
			s: parameters[0],
			r: parameters[1]
		});

	}

}

/**
 * A list of parameter presets.
 *
 * @type {Array<Float32Array[]>}
 * @private
 */

const superPrimitivePresets = [

	// Cube.
	[
		new Float32Array([1.0, 1.0, 1.0, 1.0]),
		new Float32Array([0.0, 0.0, 0.0])
	],

	// Cylinder.
	[
		new Float32Array([1.0, 1.0, 1.0, 1.0]),
		new Float32Array([1.0, 0.0, 0.0])
	],

	// Cone.
	[
		new Float32Array([0.0, 0.0, 1.0, 1.0]),
		new Float32Array([0.0, 0.0, 1.0])
	],

	// Pill.
	[
		new Float32Array([1.0, 1.0, 2.0, 1.0]),
		new Float32Array([1.0, 1.0, 0.0])
	],

	// Sphere.
	[
		new Float32Array([1.0, 1.0, 1.0, 1.0]),
		new Float32Array([1.0, 1.0, 0.0])
	],

	// Pellet.
	[
		new Float32Array([1.0, 1.0, 0.25, 1.0]),
		new Float32Array([1.0, 0.25, 0.0])
	],

	// Torus.
	[
		new Float32Array([1.0, 1.0, 0.25, 0.25]),
		new Float32Array([1.0, 0.25, 0.0])
	],

	// Pipe.
	[
		new Float32Array([1.0, 1.0, 1.0, 0.25]),
		new Float32Array([1.0, 0.1, 0.0])
	],

	// Corridor.
	[
		new Float32Array([1.0, 1.0, 1.0, 0.25]),
		new Float32Array([0.1, 0.1, 0.0])
	]

];

/**
 * An enumeration of super primitive presets.
 *
 * @type {Object}
 * @property {Number} CUBE - A cube.
 * @property {Number} CYLINDER - A cylinder.
 * @property {Number} CONE - A cone.
 * @property {Number} PILL - A pill.
 * @property {Number} SPHERE - A sphere.
 * @property {Number} PELLET - A pellet.
 * @property {Number} TORUS - A torus.
 * @property {Number} PIPE - A pipe.
 * @property {Number} CORRIDOR - A corridor.
 */

export const SuperPrimitivePreset = {

	CUBE: 0,
	CYLINDER: 1,
	CONE: 2,
	PILL: 3,
	SPHERE: 4,
	PELLET: 5,
	TORUS: 6,
	PIPE: 7,
	CORRIDOR: 8

};