Home Reference Source

src/textures/lut/TetrahedralUpscaler.js

/**
 * Two temporary vectors.
 *
 * @type {Float32Array[]}
 * @private
 */

const P = [
	new Float32Array(3),
	new Float32Array(3)
];

/**
 * Four temporary RGB color containers.
 *
 * @type {Float32Array[]}
 * @private
 */

const C = [
	new Float32Array(3),
	new Float32Array(3),
	new Float32Array(3),
	new Float32Array(3)
];

/**
 * Six congruent and equal sized tetrahedra, each defined by four vertices.
 *
 * @type {Float32Array[][]}
 * @private
 */

const T = [
	[
		new Float32Array([0, 0, 0]),
		new Float32Array([1, 0, 0]),
		new Float32Array([1, 1, 0]),
		new Float32Array([1, 1, 1])
	], [
		new Float32Array([0, 0, 0]),
		new Float32Array([1, 0, 0]),
		new Float32Array([1, 0, 1]),
		new Float32Array([1, 1, 1])
	], [
		new Float32Array([0, 0, 0]),
		new Float32Array([0, 0, 1]),
		new Float32Array([1, 0, 1]),
		new Float32Array([1, 1, 1])
	], [
		new Float32Array([0, 0, 0]),
		new Float32Array([0, 1, 0]),
		new Float32Array([1, 1, 0]),
		new Float32Array([1, 1, 1])
	], [
		new Float32Array([0, 0, 0]),
		new Float32Array([0, 1, 0]),
		new Float32Array([0, 1, 1]),
		new Float32Array([1, 1, 1])
	], [
		new Float32Array([0, 0, 0]),
		new Float32Array([0, 0, 1]),
		new Float32Array([0, 1, 1]),
		new Float32Array([1, 1, 1])
	]
];

/**
 * Calculates the volume of a given tetrahedron.
 *
 * @private
 * @param {Float32Array} a - A tetrahedron vertex.
 * @param {Float32Array} b - A tetrahedron vertex.
 * @param {Float32Array} c - A tetrahedron vertex.
 * @param {Float32Array} d - A tetrahedron vertex.
 * @return {Number} The volume.
 */

function calculateTetrahedronVolume(a, b, c, d) {

	// Calculate the area of the base triangle.
	const bcX = c[0] - b[0];
	const bcY = c[1] - b[1];
	const bcZ = c[2] - b[2];

	const baX = a[0] - b[0];
	const baY = a[1] - b[1];
	const baZ = a[2] - b[2];

	const crossX = bcY * baZ - bcZ * baY;
	const crossY = bcZ * baX - bcX * baZ;
	const crossZ = bcX * baY - bcY * baX;

	const length = Math.sqrt(crossX * crossX + crossY * crossY + crossZ * crossZ);
	const triangleArea = length * 0.5;

	// Construct the base plane.
	const normalX = crossX / length;
	const normalY = crossY / length;
	const normalZ = crossZ / length;
	const constant = -(a[0] * normalX + a[1] * normalY + a[2] * normalZ);

	// Calculate the height of the tetrahedron.
	const dot = d[0] * normalX + d[1] * normalY + d[2] * normalZ;
	const height = Math.abs(dot + constant);

	return height * triangleArea / 3.0;

}

/**
 * Samples the given data.
 *
 * @private
 * @param {TypedArray} data - The source data (RGBA).
 * @param {Number} size - The size of the source texture.
 * @param {Number} x - The X coordinate.
 * @param {Number} y - The Y coordinate.
 * @param {Number} z - The Z coordinate.
 * @param {Float32Array} color - A container for the sampled color.
 */

function sample(data, size, x, y, z, color) {

	const i4 = (x + y * size + z * size * size) * 4;
	color[0] = data[i4 + 0];
	color[1] = data[i4 + 1];
	color[2] = data[i4 + 2];

}

/**
 * Samples the given data using tetrahedral interpolation.
 *
 * @private
 * @param {TypedArray} data - The source data.
 * @param {Number} size - The size of the source texture.
 * @param {Number} u - The U coordinate.
 * @param {Number} v - The V coordinate.
 * @param {Number} w - The W coordinate.
 * @param {Float32Array} color - A container for the sampled color.
 */

function tetrahedralSample(data, size, u, v, w, color) {

	const px = u * (size - 1.0);
	const py = v * (size - 1.0);
	const pz = w * (size - 1.0);

	const minX = Math.floor(px);
	const minY = Math.floor(py);
	const minZ = Math.floor(pz);

	const maxX = Math.ceil(px);
	const maxY = Math.ceil(py);
	const maxZ = Math.ceil(pz);

	// Get the UVW coordinates relative to the min and max pixels.
	const su = px - minX;
	const sv = py - minY;
	const sw = pz - minZ;

	if(minX === px && minY === py && minZ === pz) {

		sample(data, size, px, py, pz, color);

	} else {

		let vertices;

		if(su >= sv && sv >= sw) {

			vertices = T[0];

		} else if(su >= sw && sw >= sv) {

			vertices = T[1];

		} else if(sw >= su && su >= sv) {

			vertices = T[2];

		} else if(sv >= su && su >= sw) {

			vertices = T[3];

		} else if(sv >= sw && sw >= su) {

			vertices = T[4];

		} else if(sw >= sv && sv >= su) {

			vertices = T[5];

		}

		const [P0, P1, P2, P3] = vertices;

		const coords = P[0];
		coords[0] = su; coords[1] = sv; coords[2] = sw;

		const tmp = P[1];
		const diffX = maxX - minX;
		const diffY = maxY - minY;
		const diffZ = maxZ - minZ;

		tmp[0] = diffX * P0[0] + minX;
		tmp[1] = diffY * P0[1] + minY;
		tmp[2] = diffZ * P0[2] + minZ;
		sample(data, size, tmp[0], tmp[1], tmp[2], C[0]);

		tmp[0] = diffX * P1[0] + minX;
		tmp[1] = diffY * P1[1] + minY;
		tmp[2] = diffZ * P1[2] + minZ;
		sample(data, size, tmp[0], tmp[1], tmp[2], C[1]);

		tmp[0] = diffX * P2[0] + minX;
		tmp[1] = diffY * P2[1] + minY;
		tmp[2] = diffZ * P2[2] + minZ;
		sample(data, size, tmp[0], tmp[1], tmp[2], C[2]);

		tmp[0] = diffX * P3[0] + minX;
		tmp[1] = diffY * P3[1] + minY;
		tmp[2] = diffZ * P3[2] + minZ;
		sample(data, size, tmp[0], tmp[1], tmp[2], C[3]);

		const V0 = calculateTetrahedronVolume(P1, P2, P3, coords) * 6.0;
		const V1 = calculateTetrahedronVolume(P0, P2, P3, coords) * 6.0;
		const V2 = calculateTetrahedronVolume(P0, P1, P3, coords) * 6.0;
		const V3 = calculateTetrahedronVolume(P0, P1, P2, coords) * 6.0;

		C[0][0] *= V0; C[0][1] *= V0; C[0][2] *= V0;
		C[1][0] *= V1; C[1][1] *= V1; C[1][2] *= V1;
		C[2][0] *= V2; C[2][1] *= V2; C[2][2] *= V2;
		C[3][0] *= V3; C[3][1] *= V3; C[3][2] *= V3;

		color[0] = C[0][0] + C[1][0] + C[2][0] + C[3][0];
		color[1] = C[0][1] + C[1][1] + C[2][1] + C[3][1];
		color[2] = C[0][2] + C[1][2] + C[2][2] + C[3][2];

	}

}

/**
 * A tetrahedral upscaler that can be used to augment 3D LUTs.
 *
 * Based on an implementation by Garrett Johnson:
 * https://github.com/gkjohnson/threejs-sandbox/blob/master/3d-lut/src/TetrahedralUpscaler.js
 */

export class TetrahedralUpscaler {

	/**
	 * Expands the given data to the target size.
	 *
	 * @param {TypedArray} data - The input RGBA data. Assumed to be cubic.
	 * @param {Number} size - The target size.
	 * @return {TypedArray} The new data.
	 */

	static expand(data, size) {

		const originalSize = Math.cbrt(data.length / 4);

		const rgb = new Float32Array(3);
		const array = new data.constructor(size ** 3 * 4);
		const maxValue = (data instanceof Uint8Array) ? 255 : 1.0;
		const sizeSq = size ** 2;
		const s = 1.0 / (size - 1.0);

		for(let z = 0; z < size; ++z) {

			for(let y = 0; y < size; ++y) {

				for(let x = 0; x < size; ++x) {

					const u = x * s;
					const v = y * s;
					const w = z * s;
					const i4 = Math.round(x + y * size + z * sizeSq) * 4;

					tetrahedralSample(data, originalSize, u, v, w, rgb);

					array[i4 + 0] = rgb[0];
					array[i4 + 1] = rgb[1];
					array[i4 + 2] = rgb[2];
					array[i4 + 3] = maxValue;

				}

			}

		}

		return array;

	}

}