src/textures/lut/LookupTexture.js
import {
Color,
ClampToEdgeWrapping,
DataTexture,
Data3DTexture,
FloatType,
LinearFilter,
LinearSRGBColorSpace,
RGBAFormat,
SRGBColorSpace,
UnsignedByteType,
Vector3
} from "three";
import { LUTOperation } from "../../enums/LUTOperation.js";
import { RawImageData } from "../RawImageData.js";
import workerProgram from "../../../tmp/lut/worker.txt";
const c = /* @__PURE__ */ new Color();
/**
* A 3D lookup texture (LUT).
*
* This texture can be used as-is in a WebGL 2 context. It can also be converted into a 2D texture.
*/
export class LookupTexture extends Data3DTexture {
/**
* Constructs a cubic 3D lookup texture.
*
* @param {TypedArray} data - The pixel data. The default format is RGBA.
* @param {Number} size - The sidelength.
*/
constructor(data, size) {
super(data, size, size, size);
this.type = FloatType;
this.format = RGBAFormat;
this.minFilter = LinearFilter;
this.magFilter = LinearFilter;
this.wrapS = ClampToEdgeWrapping;
this.wrapT = ClampToEdgeWrapping;
this.wrapR = ClampToEdgeWrapping;
this.unpackAlignment = 1;
this.needsUpdate = true;
this.colorSpace = LinearSRGBColorSpace;
/**
* The lower bounds of the input domain.
*
* @type {Vector3}
*/
this.domainMin = new Vector3(0.0, 0.0, 0.0);
/**
* The upper bounds of the input domain.
*
* @type {Vector3}
*/
this.domainMax = new Vector3(1.0, 1.0, 1.0);
}
/**
* Indicates that this is an instance of LookupTexture3D.
*
* @type {Boolean}
* @deprecated
*/
get isLookupTexture3D() {
return true;
}
/**
* Scales this LUT up to a given target size using tetrahedral interpolation.
*
* @param {Number} size - The target sidelength.
* @param {Boolean} [transferData=true] - Extra fast mode. Set to false to keep the original data intact.
* @return {Promise<LookupTexture>} A promise that resolves with a new LUT upon completion.
*/
scaleUp(size, transferData = true) {
const image = this.image;
let promise;
if(size <= image.width) {
promise = Promise.reject(new Error("The target size must be greater than the current size"));
} else {
promise = new Promise((resolve, reject) => {
const workerURL = URL.createObjectURL(new Blob([workerProgram], {
type: "text/javascript"
}));
const worker = new Worker(workerURL);
worker.addEventListener("error", (event) => reject(event.error));
worker.addEventListener("message", (event) => {
const lut = new LookupTexture(event.data, size);
this.colorSpace = lut.colorSpace;
lut.type = this.type;
lut.name = this.name;
URL.revokeObjectURL(workerURL);
resolve(lut);
});
const transferList = transferData ? [image.data.buffer] : [];
worker.postMessage({
operation: LUTOperation.SCALE_UP,
data: image.data,
size
}, transferList);
});
}
return promise;
}
/**
* Applies the given LUT to this one.
*
* @param {LookupTexture} lut - A LUT. Must have the same dimensions, type and format as this LUT.
* @return {LookupTexture} This texture.
*/
applyLUT(lut) {
const img0 = this.image;
const img1 = lut.image;
const size0 = Math.min(img0.width, img0.height, img0.depth);
const size1 = Math.min(img1.width, img1.height, img1.depth);
if(size0 !== size1) {
console.error("Size mismatch");
} else if(lut.type !== FloatType || this.type !== FloatType) {
console.error("Both LUTs must be FloatType textures");
} else if(lut.format !== RGBAFormat || this.format !== RGBAFormat) {
console.error("Both LUTs must be RGBA textures");
} else {
const data0 = img0.data;
const data1 = img1.data;
const size = size0;
const sizeSq = size ** 2;
const s = size - 1;
for(let i = 0, l = size ** 3; i < l; ++i) {
const i4 = i * 4;
const r = data0[i4 + 0] * s;
const g = data0[i4 + 1] * s;
const b = data0[i4 + 2] * s;
const iRGB = Math.round(r + g * size + b * sizeSq) * 4;
data0[i4 + 0] = data1[iRGB + 0];
data0[i4 + 1] = data1[iRGB + 1];
data0[i4 + 2] = data1[iRGB + 2];
}
this.needsUpdate = true;
}
return this;
}
/**
* Converts the LUT data into unsigned byte data.
*
* This is a lossy operation which should only be performed after all other transformations have been applied.
*
* @return {LookupTexture} This texture.
*/
convertToUint8() {
if(this.type === FloatType) {
const floatData = this.image.data;
const uint8Data = new Uint8Array(floatData.length);
for(let i = 0, l = floatData.length; i < l; ++i) {
uint8Data[i] = floatData[i] * 255 + 0.5;
}
this.image.data = uint8Data;
this.type = UnsignedByteType;
this.needsUpdate = true;
}
return this;
}
/**
* Converts the LUT data into float data.
*
* @return {LookupTexture} This texture.
*/
convertToFloat() {
if(this.type === UnsignedByteType) {
const uint8Data = this.image.data;
const floatData = new Float32Array(uint8Data.length);
for(let i = 0, l = uint8Data.length; i < l; ++i) {
floatData[i] = uint8Data[i] / 255;
}
this.image.data = floatData;
this.type = FloatType;
this.needsUpdate = true;
}
return this;
}
/**
* Converts this LUT into RGBA data.
*
* @deprecated LUTs are RGBA by default since three r137.
* @return {LookupTexture} This texture.
*/
convertToRGBA() {
console.warn("LookupTexture", "convertToRGBA() is deprecated, LUTs are now RGBA by default");
return this;
}
/**
* Converts the output of this LUT into sRGB color space.
*
* @return {LookupTexture} This texture.
*/
convertLinearToSRGB() {
const data = this.image.data;
if(this.type === FloatType) {
for(let i = 0, l = data.length; i < l; i += 4) {
c.fromArray(data, i).convertLinearToSRGB().toArray(data, i);
}
this.colorSpace = SRGBColorSpace;
this.needsUpdate = true;
} else {
console.error("Color space conversion requires FloatType data");
}
return this;
}
/**
* Converts the output of this LUT into linear color space.
*
* @return {LookupTexture} This texture.
*/
convertSRGBToLinear() {
const data = this.image.data;
if(this.type === FloatType) {
for(let i = 0, l = data.length; i < l; i += 4) {
c.fromArray(data, i).convertSRGBToLinear().toArray(data, i);
}
this.colorSpace = LinearSRGBColorSpace;
this.needsUpdate = true;
} else {
console.error("Color space conversion requires FloatType data");
}
return this;
}
/**
* Converts this LUT into a 2D data texture.
*
* Please note that custom input domains are not carried over to 2D textures.
*
* @return {DataTexture} The texture.
*/
toDataTexture() {
const width = this.image.width;
const height = this.image.height * this.image.depth;
const texture = new DataTexture(this.image.data, width, height);
texture.name = this.name;
texture.type = this.type;
texture.format = this.format;
texture.minFilter = LinearFilter;
texture.magFilter = LinearFilter;
texture.wrapS = this.wrapS;
texture.wrapT = this.wrapT;
texture.generateMipmaps = false;
texture.needsUpdate = true;
this.colorSpace = texture.colorSpace;
return texture;
}
/**
* Creates a new 3D LUT by copying a given LUT.
*
* Common image-based textures will be converted into 3D data textures.
*
* @param {Texture} texture - The LUT. Assumed to be cubic.
* @return {LookupTexture} A new 3D LUT.
*/
static from(texture) {
const image = texture.image;
const { width, height } = image;
const size = Math.min(width, height);
let data;
if(image instanceof Image) {
// Convert the image into RGBA Uint8 data.
const rawImageData = RawImageData.from(image);
const src = rawImageData.data;
// Horizontal layout?
if(width > height) {
data = new Uint8Array(src.length);
// Slices -> Rows -> Columns.
for(let z = 0; z < size; ++z) {
for(let y = 0; y < size; ++y) {
for(let x = 0; x < size; ++x) {
// Source: horizontal arrangement. Swap Y and Z.
const i4 = (x + z * size + y * size * size) * 4;
// Target: vertical arrangement.
const j4 = (x + y * size + z * size * size) * 4;
data[j4 + 0] = src[i4 + 0];
data[j4 + 1] = src[i4 + 1];
data[j4 + 2] = src[i4 + 2];
data[j4 + 3] = src[i4 + 3];
}
}
}
} else {
// Same layout: convert from Uint8ClampedArray to Uint8Array.
data = new Uint8Array(src.buffer);
}
} else {
data = image.data.slice();
}
const lut = new LookupTexture(data, size);
lut.type = texture.type;
lut.name = texture.name;
texture.colorSpace = lut.colorSpace;
return lut;
}
/**
* Creates a neutral 3D LUT.
*
* @param {Number} size - The sidelength.
* @return {LookupTexture} A neutral 3D LUT.
*/
static createNeutral(size) {
const data = new Float32Array(size ** 3 * 4);
const sizeSq = size ** 2;
const s = 1.0 / (size - 1.0);
for(let r = 0; r < size; ++r) {
for(let g = 0; g < size; ++g) {
for(let b = 0; b < size; ++b) {
const i4 = (r + g * size + b * sizeSq) * 4;
data[i4 + 0] = r * s;
data[i4 + 1] = g * s;
data[i4 + 2] = b * s;
data[i4 + 3] = 1.0;
}
}
}
const lut = new LookupTexture(data, size);
lut.name = "neutral";
return lut;
}
}