1317 lines
46 KiB
JavaScript
1317 lines
46 KiB
JavaScript
/**
|
|
* @monogrid/gainmap-js v3.4.0
|
|
* With ❤️, by MONOGRID <gainmap@monogrid.com>
|
|
*/
|
|
|
|
(function (global, factory) {
|
|
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('three')) :
|
|
typeof define === 'function' && define.amd ? define(['exports', 'three'], factory) :
|
|
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.encode = {}, global.three));
|
|
})(this, (function (exports, three) { 'use strict';
|
|
|
|
/**
|
|
* Used internally
|
|
*
|
|
* @internal
|
|
* @param canvas
|
|
* @param mimeType
|
|
* @param quality
|
|
* @returns
|
|
*/
|
|
const canvasToBlob = async (canvas, mimeType, quality) => {
|
|
if (typeof OffscreenCanvas !== 'undefined' && canvas instanceof OffscreenCanvas) {
|
|
return canvas.convertToBlob({ type: mimeType, quality: quality });
|
|
}
|
|
else if (canvas instanceof HTMLCanvasElement) {
|
|
return new Promise((resolve, reject) => {
|
|
canvas.toBlob((res) => {
|
|
if (res)
|
|
resolve(res);
|
|
else
|
|
reject(new Error('Failed to convert canvas to blob'));
|
|
}, mimeType, quality);
|
|
});
|
|
}
|
|
/* istanbul ignore next
|
|
as long as this function is not exported this is only here
|
|
to satisfy TS strict mode internally
|
|
*/
|
|
throw new Error('Unsupported canvas element');
|
|
};
|
|
/**
|
|
* Converts a RAW RGBA image buffer into the provided `mimeType` using the provided `quality`
|
|
*
|
|
* @category Compression
|
|
* @group Compression
|
|
* @param params
|
|
* @throws {Error} if the browser does not support [createImageBitmap](https://caniuse.com/createimagebitmap)
|
|
* @throws {Error} if the provided source image cannot be decoded
|
|
* @throws {Error} if the function fails to create a canvas context
|
|
*/
|
|
const compress = async (params) => {
|
|
if (typeof createImageBitmap === 'undefined')
|
|
throw new Error('createImageBitmap() not supported.');
|
|
const { source, mimeType, quality, flipY } = params;
|
|
let imageBitmapSource;
|
|
if ((source instanceof Uint8Array || source instanceof Uint8ClampedArray) && 'sourceMimeType' in params) {
|
|
imageBitmapSource = new Blob([source], { type: params.sourceMimeType });
|
|
}
|
|
else if (source instanceof ImageData) {
|
|
imageBitmapSource = source;
|
|
}
|
|
else {
|
|
throw new Error('Invalid source image');
|
|
}
|
|
const img = await createImageBitmap(imageBitmapSource);
|
|
const width = img.width;
|
|
const height = img.height;
|
|
let canvas;
|
|
if (typeof OffscreenCanvas !== 'undefined') {
|
|
canvas = new OffscreenCanvas(width, height);
|
|
}
|
|
else {
|
|
canvas = document.createElement('canvas');
|
|
canvas.width = width;
|
|
canvas.height = height;
|
|
}
|
|
// apparently we do need to assert the type here for npx tsc to be happy
|
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx)
|
|
throw new Error('Failed to create canvas Context');
|
|
// flip Y
|
|
if (flipY === true) {
|
|
ctx.translate(0, height);
|
|
ctx.scale(1, -1);
|
|
}
|
|
ctx.drawImage(img, 0, 0, width, height);
|
|
const blob = await canvasToBlob(canvas, mimeType, quality || 0.9);
|
|
const data = new Uint8Array(await blob.arrayBuffer());
|
|
return {
|
|
data,
|
|
mimeType,
|
|
width,
|
|
height
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Utility function to obtain a `DataTexture` from various input formats
|
|
*
|
|
* @category Utility
|
|
* @group Utility
|
|
*
|
|
* @param image
|
|
* @returns
|
|
*/
|
|
const getDataTexture = (image) => {
|
|
let dataTexture;
|
|
if (image instanceof three.DataTexture) {
|
|
if (!(image.image.data instanceof Uint16Array) && !(image.image.data instanceof Float32Array)) {
|
|
throw new Error('Provided image is not HDR');
|
|
}
|
|
dataTexture = image;
|
|
}
|
|
else {
|
|
dataTexture = new three.DataTexture(image.data, image.width, image.height, 'format' in image ? image.format : three.RGBAFormat, image.type, three.UVMapping, three.RepeatWrapping, three.RepeatWrapping, three.LinearFilter, three.LinearFilter, 1, 'colorSpace' in image && image.colorSpace === 'srgb' ? image.colorSpace : three.LinearSRGBColorSpace);
|
|
// TODO: This tries to detect a raw RGBE and applies flipY
|
|
// see if there's a better way to detect it?
|
|
if ('header' in image && 'gamma' in image) {
|
|
dataTexture.flipY = true;
|
|
}
|
|
dataTexture.needsUpdate = true;
|
|
}
|
|
return dataTexture;
|
|
};
|
|
|
|
const getBufferForType = (type, width, height) => {
|
|
let out;
|
|
switch (type) {
|
|
case three.UnsignedByteType:
|
|
out = new Uint8ClampedArray(width * height * 4);
|
|
break;
|
|
case three.HalfFloatType:
|
|
out = new Uint16Array(width * height * 4);
|
|
break;
|
|
case three.UnsignedIntType:
|
|
out = new Uint32Array(width * height * 4);
|
|
break;
|
|
case three.ByteType:
|
|
out = new Int8Array(width * height * 4);
|
|
break;
|
|
case three.ShortType:
|
|
out = new Int16Array(width * height * 4);
|
|
break;
|
|
case three.IntType:
|
|
out = new Int32Array(width * height * 4);
|
|
break;
|
|
case three.FloatType:
|
|
out = new Float32Array(width * height * 4);
|
|
break;
|
|
default:
|
|
throw new Error('Unsupported data type');
|
|
}
|
|
return out;
|
|
};
|
|
let _canReadPixelsResult;
|
|
/**
|
|
* Test if this browser implementation can correctly read pixels from the specified
|
|
* Render target type.
|
|
*
|
|
* Runs only once
|
|
*
|
|
* @param type
|
|
* @param renderer
|
|
* @param camera
|
|
* @param renderTargetOptions
|
|
* @returns
|
|
*/
|
|
const canReadPixels = (type, renderer, camera, renderTargetOptions) => {
|
|
if (_canReadPixelsResult !== undefined)
|
|
return _canReadPixelsResult;
|
|
const testRT = new three.WebGLRenderTarget(1, 1, renderTargetOptions);
|
|
renderer.setRenderTarget(testRT);
|
|
const mesh = new three.Mesh(new three.PlaneGeometry(), new three.MeshBasicMaterial({ color: 0xffffff }));
|
|
renderer.render(mesh, camera);
|
|
renderer.setRenderTarget(null);
|
|
const out = getBufferForType(type, testRT.width, testRT.height);
|
|
renderer.readRenderTargetPixels(testRT, 0, 0, testRT.width, testRT.height, out);
|
|
testRT.dispose();
|
|
mesh.geometry.dispose();
|
|
mesh.material.dispose();
|
|
_canReadPixelsResult = out[0] !== 0;
|
|
return _canReadPixelsResult;
|
|
};
|
|
/**
|
|
* Utility class used for rendering a texture with a material
|
|
*
|
|
* @category Core
|
|
* @group Core
|
|
*/
|
|
class QuadRenderer {
|
|
_renderer;
|
|
_rendererIsDisposable = false;
|
|
_material;
|
|
_scene;
|
|
_camera;
|
|
_quad;
|
|
_renderTarget;
|
|
_width;
|
|
_height;
|
|
_type;
|
|
_colorSpace;
|
|
_supportsReadPixels = true;
|
|
/**
|
|
* Constructs a new QuadRenderer
|
|
*
|
|
* @param options Parameters for this QuadRenderer
|
|
*/
|
|
constructor(options) {
|
|
this._width = options.width;
|
|
this._height = options.height;
|
|
this._type = options.type;
|
|
this._colorSpace = options.colorSpace;
|
|
const rtOptions = {
|
|
// fixed options
|
|
format: three.RGBAFormat,
|
|
depthBuffer: false,
|
|
stencilBuffer: false,
|
|
// user options
|
|
type: this._type, // set in class property
|
|
colorSpace: this._colorSpace, // set in class property
|
|
anisotropy: options.renderTargetOptions?.anisotropy !== undefined ? options.renderTargetOptions?.anisotropy : 1,
|
|
generateMipmaps: options.renderTargetOptions?.generateMipmaps !== undefined ? options.renderTargetOptions?.generateMipmaps : false,
|
|
magFilter: options.renderTargetOptions?.magFilter !== undefined ? options.renderTargetOptions?.magFilter : three.LinearFilter,
|
|
minFilter: options.renderTargetOptions?.minFilter !== undefined ? options.renderTargetOptions?.minFilter : three.LinearFilter,
|
|
samples: options.renderTargetOptions?.samples !== undefined ? options.renderTargetOptions?.samples : undefined,
|
|
wrapS: options.renderTargetOptions?.wrapS !== undefined ? options.renderTargetOptions?.wrapS : three.ClampToEdgeWrapping,
|
|
wrapT: options.renderTargetOptions?.wrapT !== undefined ? options.renderTargetOptions?.wrapT : three.ClampToEdgeWrapping
|
|
};
|
|
this._material = options.material;
|
|
if (options.renderer) {
|
|
this._renderer = options.renderer;
|
|
}
|
|
else {
|
|
this._renderer = QuadRenderer.instantiateRenderer();
|
|
this._rendererIsDisposable = true;
|
|
}
|
|
this._scene = new three.Scene();
|
|
this._camera = new three.OrthographicCamera();
|
|
this._camera.position.set(0, 0, 10);
|
|
this._camera.left = -0.5;
|
|
this._camera.right = 0.5;
|
|
this._camera.top = 0.5;
|
|
this._camera.bottom = -0.5;
|
|
this._camera.updateProjectionMatrix();
|
|
if (!canReadPixels(this._type, this._renderer, this._camera, rtOptions)) {
|
|
let alternativeType;
|
|
switch (this._type) {
|
|
case three.HalfFloatType:
|
|
alternativeType = this._renderer.extensions.has('EXT_color_buffer_float') ? three.FloatType : undefined;
|
|
break;
|
|
}
|
|
if (alternativeType !== undefined) {
|
|
console.warn(`This browser does not support reading pixels from ${this._type} RenderTargets, switching to ${three.FloatType}`);
|
|
this._type = alternativeType;
|
|
}
|
|
else {
|
|
this._supportsReadPixels = false;
|
|
console.warn('This browser dos not support toArray or toDataTexture, calls to those methods will result in an error thrown');
|
|
}
|
|
}
|
|
this._quad = new three.Mesh(new three.PlaneGeometry(), this._material);
|
|
this._quad.geometry.computeBoundingBox();
|
|
this._scene.add(this._quad);
|
|
this._renderTarget = new three.WebGLRenderTarget(this.width, this.height, rtOptions);
|
|
this._renderTarget.texture.mapping = options.renderTargetOptions?.mapping !== undefined ? options.renderTargetOptions?.mapping : three.UVMapping;
|
|
}
|
|
/**
|
|
* Instantiates a temporary renderer
|
|
*
|
|
* @returns
|
|
*/
|
|
static instantiateRenderer() {
|
|
const renderer = new three.WebGLRenderer();
|
|
renderer.setSize(128, 128);
|
|
// renderer.outputColorSpace = SRGBColorSpace
|
|
// renderer.toneMapping = LinearToneMapping
|
|
// renderer.debug.checkShaderErrors = false
|
|
// this._rendererIsDisposable = true
|
|
return renderer;
|
|
}
|
|
/**
|
|
* Renders the input texture using the specified material
|
|
*/
|
|
render = () => {
|
|
this._renderer.setRenderTarget(this._renderTarget);
|
|
try {
|
|
this._renderer.render(this._scene, this._camera);
|
|
}
|
|
catch (e) {
|
|
this._renderer.setRenderTarget(null);
|
|
throw e;
|
|
}
|
|
this._renderer.setRenderTarget(null);
|
|
};
|
|
/**
|
|
* Obtains a Buffer containing the rendered texture.
|
|
*
|
|
* @throws Error if the browser cannot read pixels from this RenderTarget type.
|
|
* @returns a TypedArray containing RGBA values from this renderer
|
|
*/
|
|
toArray() {
|
|
if (!this._supportsReadPixels)
|
|
throw new Error('Can\'t read pixels in this browser');
|
|
const out = getBufferForType(this._type, this._width, this._height);
|
|
this._renderer.readRenderTargetPixels(this._renderTarget, 0, 0, this._width, this._height, out);
|
|
return out;
|
|
}
|
|
/**
|
|
* Performs a readPixel operation in the renderTarget
|
|
* and returns a DataTexture containing the read data
|
|
*
|
|
* @param options options
|
|
* @returns
|
|
*/
|
|
toDataTexture(options) {
|
|
const returnValue = new three.DataTexture(
|
|
// fixed values
|
|
this.toArray(), this.width, this.height, three.RGBAFormat, this._type,
|
|
// user values
|
|
options?.mapping || three.UVMapping, options?.wrapS || three.ClampToEdgeWrapping, options?.wrapT || three.ClampToEdgeWrapping, options?.magFilter || three.LinearFilter, options?.minFilter || three.LinearFilter, options?.anisotropy || 1,
|
|
// fixed value
|
|
three.LinearSRGBColorSpace);
|
|
// set this afterwards, we can't set it in constructor
|
|
returnValue.generateMipmaps = options?.generateMipmaps !== undefined ? options?.generateMipmaps : false;
|
|
return returnValue;
|
|
}
|
|
/**
|
|
* If using a disposable renderer, it will dispose it.
|
|
*/
|
|
disposeOnDemandRenderer() {
|
|
this._renderer.setRenderTarget(null);
|
|
if (this._rendererIsDisposable) {
|
|
this._renderer.dispose();
|
|
this._renderer.forceContextLoss();
|
|
}
|
|
}
|
|
/**
|
|
* Will dispose of **all** assets used by this renderer.
|
|
*
|
|
*
|
|
* @param disposeRenderTarget will dispose of the renderTarget which will not be usable later
|
|
* set this to true if you passed the `renderTarget.texture` to a `PMREMGenerator`
|
|
* or are otherwise done with it.
|
|
*
|
|
* @example
|
|
* ```js
|
|
* const loader = new HDRJPGLoader(renderer)
|
|
* const result = await loader.loadAsync('gainmap.jpeg')
|
|
* const mesh = new Mesh(geometry, new MeshBasicMaterial({ map: result.renderTarget.texture }) )
|
|
* // DO NOT dispose the renderTarget here,
|
|
* // it is used directly in the material
|
|
* result.dispose()
|
|
* ```
|
|
*
|
|
* @example
|
|
* ```js
|
|
* const loader = new HDRJPGLoader(renderer)
|
|
* const pmremGenerator = new PMREMGenerator( renderer );
|
|
* const result = await loader.loadAsync('gainmap.jpeg')
|
|
* const envMap = pmremGenerator.fromEquirectangular(result.renderTarget.texture)
|
|
* const mesh = new Mesh(geometry, new MeshStandardMaterial({ envMap }) )
|
|
* // renderTarget can be disposed here
|
|
* // because it was used to generate a PMREM texture
|
|
* result.dispose(true)
|
|
* ```
|
|
*/
|
|
dispose(disposeRenderTarget) {
|
|
this.disposeOnDemandRenderer();
|
|
if (disposeRenderTarget) {
|
|
this.renderTarget.dispose();
|
|
}
|
|
// dispose shader material texture uniforms
|
|
if (this.material instanceof three.ShaderMaterial) {
|
|
Object.values(this.material.uniforms).forEach(v => {
|
|
if (v.value instanceof three.Texture)
|
|
v.value.dispose();
|
|
});
|
|
}
|
|
// dispose other material properties
|
|
Object.values(this.material).forEach(value => {
|
|
if (value instanceof three.Texture)
|
|
value.dispose();
|
|
});
|
|
this.material.dispose();
|
|
this._quad.geometry.dispose();
|
|
}
|
|
/**
|
|
* Width of the texture
|
|
*/
|
|
get width() { return this._width; }
|
|
set width(value) {
|
|
this._width = value;
|
|
this._renderTarget.setSize(this._width, this._height);
|
|
}
|
|
/**
|
|
* Height of the texture
|
|
*/
|
|
get height() { return this._height; }
|
|
set height(value) {
|
|
this._height = value;
|
|
this._renderTarget.setSize(this._width, this._height);
|
|
}
|
|
/**
|
|
* The renderer used
|
|
*/
|
|
get renderer() { return this._renderer; }
|
|
/**
|
|
* The `WebGLRenderTarget` used.
|
|
*/
|
|
get renderTarget() { return this._renderTarget; }
|
|
set renderTarget(value) {
|
|
this._renderTarget = value;
|
|
this._width = value.width;
|
|
this._height = value.height;
|
|
// this._type = value.texture.type
|
|
}
|
|
/**
|
|
* The `Material` used.
|
|
*/
|
|
get material() { return this._material; }
|
|
/**
|
|
*
|
|
*/
|
|
get type() { return this._type; }
|
|
get colorSpace() { return this._colorSpace; }
|
|
}
|
|
|
|
const vertexShader$2 = /* glsl */ `
|
|
varying vec2 vUv;
|
|
|
|
void main() {
|
|
vUv = uv;
|
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
}
|
|
`;
|
|
const fragmentShader$2 = /* glsl */ `
|
|
#ifndef saturate
|
|
#define saturate( a ) clamp( a, 0.0, 1.0 )
|
|
#endif
|
|
uniform sampler2D sdr;
|
|
uniform sampler2D hdr;
|
|
uniform vec3 gamma;
|
|
uniform vec3 offsetSdr;
|
|
uniform vec3 offsetHdr;
|
|
uniform float minLog2;
|
|
uniform float maxLog2;
|
|
|
|
varying vec2 vUv;
|
|
|
|
void main() {
|
|
vec3 sdrColor = texture2D(sdr, vUv).rgb;
|
|
vec3 hdrColor = texture2D(hdr, vUv).rgb;
|
|
|
|
vec3 pixelGain = (hdrColor + offsetHdr) / (sdrColor + offsetSdr);
|
|
vec3 logRecovery = (log2(pixelGain) - minLog2) / (maxLog2 - minLog2);
|
|
vec3 clampedRecovery = saturate(logRecovery);
|
|
gl_FragColor = vec4(pow(clampedRecovery, gamma), 1.0);
|
|
}
|
|
`;
|
|
/**
|
|
* A Material which is able to encode a gainmap
|
|
*
|
|
* @category Materials
|
|
* @group Materials
|
|
*/
|
|
class GainMapEncoderMaterial extends three.ShaderMaterial {
|
|
_minContentBoost;
|
|
_maxContentBoost;
|
|
_offsetSdr;
|
|
_offsetHdr;
|
|
_gamma;
|
|
/**
|
|
*
|
|
* @param params
|
|
*/
|
|
constructor({ sdr, hdr, offsetSdr, offsetHdr, maxContentBoost, minContentBoost, gamma }) {
|
|
if (!maxContentBoost)
|
|
throw new Error('maxContentBoost is required');
|
|
if (!sdr)
|
|
throw new Error('sdr is required');
|
|
if (!hdr)
|
|
throw new Error('hdr is required');
|
|
const _gamma = gamma || [1, 1, 1];
|
|
const _offsetSdr = offsetSdr || [1 / 64, 1 / 64, 1 / 64];
|
|
const _offsetHdr = offsetHdr || [1 / 64, 1 / 64, 1 / 64];
|
|
const _minContentBoost = minContentBoost || 1;
|
|
const _maxContentBoost = Math.max(maxContentBoost, 1.0001);
|
|
super({
|
|
name: 'GainMapEncoderMaterial',
|
|
vertexShader: vertexShader$2,
|
|
fragmentShader: fragmentShader$2,
|
|
uniforms: {
|
|
sdr: { value: sdr },
|
|
hdr: { value: hdr },
|
|
gamma: { value: new three.Vector3().fromArray(_gamma) },
|
|
offsetSdr: { value: new three.Vector3().fromArray(_offsetSdr) },
|
|
offsetHdr: { value: new three.Vector3().fromArray(_offsetHdr) },
|
|
minLog2: { value: Math.log2(_minContentBoost) },
|
|
maxLog2: { value: Math.log2(_maxContentBoost) }
|
|
},
|
|
blending: three.NoBlending,
|
|
depthTest: false,
|
|
depthWrite: false
|
|
});
|
|
this._minContentBoost = _minContentBoost;
|
|
this._maxContentBoost = _maxContentBoost;
|
|
this._offsetSdr = _offsetSdr;
|
|
this._offsetHdr = _offsetHdr;
|
|
this._gamma = _gamma;
|
|
this.needsUpdate = true;
|
|
this.uniformsNeedUpdate = true;
|
|
}
|
|
/**
|
|
* @see {@link GainmapEncodingParameters.gamma}
|
|
*/
|
|
get gamma() { return this._gamma; }
|
|
set gamma(value) {
|
|
this._gamma = value;
|
|
this.uniforms.gamma.value = new three.Vector3().fromArray(value);
|
|
}
|
|
/**
|
|
* @see {@link GainmapEncodingParameters.offsetHdr}
|
|
*/
|
|
get offsetHdr() { return this._offsetHdr; }
|
|
set offsetHdr(value) {
|
|
this._offsetHdr = value;
|
|
this.uniforms.offsetHdr.value = new three.Vector3().fromArray(value);
|
|
}
|
|
/**
|
|
* @see {@link GainmapEncodingParameters.offsetSdr}
|
|
*/
|
|
get offsetSdr() { return this._offsetSdr; }
|
|
set offsetSdr(value) {
|
|
this._offsetSdr = value;
|
|
this.uniforms.offsetSdr.value = new three.Vector3().fromArray(value);
|
|
}
|
|
/**
|
|
* @see {@link GainmapEncodingParameters.minContentBoost}
|
|
* @remarks Non logarithmic space
|
|
*/
|
|
get minContentBoost() { return this._minContentBoost; }
|
|
set minContentBoost(value) {
|
|
this._minContentBoost = value;
|
|
this.uniforms.minLog2.value = Math.log2(value);
|
|
}
|
|
/**
|
|
* @see {@link GainmapEncodingParameters.maxContentBoost}
|
|
* @remarks Non logarithmic space
|
|
*/
|
|
get maxContentBoost() { return this._maxContentBoost; }
|
|
set maxContentBoost(value) {
|
|
this._maxContentBoost = value;
|
|
this.uniforms.maxLog2.value = Math.log2(value);
|
|
}
|
|
/**
|
|
* @see {@link GainMapMetadata.gainMapMin}
|
|
* @remarks Logarithmic space
|
|
*/
|
|
get gainMapMin() { return [Math.log2(this._minContentBoost), Math.log2(this._minContentBoost), Math.log2(this._minContentBoost)]; }
|
|
/**
|
|
* @see {@link GainMapMetadata.gainMapMax}
|
|
* @remarks Logarithmic space
|
|
*/
|
|
get gainMapMax() { return [Math.log2(this._maxContentBoost), Math.log2(this._maxContentBoost), Math.log2(this._maxContentBoost)]; }
|
|
/**
|
|
* @see {@link GainMapMetadata.hdrCapacityMin}
|
|
* @remarks Logarithmic space
|
|
*/
|
|
get hdrCapacityMin() { return Math.min(Math.max(0, this.gainMapMin[0]), Math.max(0, this.gainMapMin[1]), Math.max(0, this.gainMapMin[2])); }
|
|
/**
|
|
* @see {@link GainMapMetadata.hdrCapacityMax}
|
|
* @remarks Logarithmic space
|
|
*/
|
|
get hdrCapacityMax() { return Math.max(Math.max(0, this.gainMapMax[0]), Math.max(0, this.gainMapMax[1]), Math.max(0, this.gainMapMax[2])); }
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param params
|
|
* @returns
|
|
* @category Encoding Functions
|
|
* @group Encoding Functions
|
|
*/
|
|
const getGainMap = (params) => {
|
|
const { image, sdr, renderer } = params;
|
|
const dataTexture = getDataTexture(image);
|
|
const material = new GainMapEncoderMaterial({
|
|
...params,
|
|
sdr: sdr.renderTarget.texture,
|
|
hdr: dataTexture
|
|
});
|
|
const quadRenderer = new QuadRenderer({
|
|
width: dataTexture.image.width,
|
|
height: dataTexture.image.height,
|
|
type: three.UnsignedByteType,
|
|
colorSpace: three.LinearSRGBColorSpace,
|
|
material,
|
|
renderer,
|
|
renderTargetOptions: params.renderTargetOptions
|
|
});
|
|
try {
|
|
quadRenderer.render();
|
|
}
|
|
catch (e) {
|
|
quadRenderer.disposeOnDemandRenderer();
|
|
throw e;
|
|
}
|
|
return quadRenderer;
|
|
};
|
|
|
|
const vertexShader$1 = /* glsl */ `
|
|
varying vec2 vUv;
|
|
|
|
void main() {
|
|
vUv = uv;
|
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
}
|
|
`;
|
|
const fragmentShader$1 = /* glsl */ `
|
|
#ifndef saturate
|
|
#define saturate( a ) clamp( a, 0.0, 1.0 )
|
|
#endif
|
|
|
|
uniform sampler2D map;
|
|
uniform float brightness;
|
|
uniform float contrast;
|
|
uniform float saturation;
|
|
uniform float exposure;
|
|
|
|
varying vec2 vUv;
|
|
|
|
mat4 brightnessMatrix( float brightness ) {
|
|
return mat4(
|
|
1, 0, 0, 0,
|
|
0, 1, 0, 0,
|
|
0, 0, 1, 0,
|
|
brightness, brightness, brightness, 1
|
|
);
|
|
}
|
|
|
|
mat4 contrastMatrix( float contrast ) {
|
|
float t = ( 1.0 - contrast ) / 2.0;
|
|
return mat4(
|
|
contrast, 0, 0, 0,
|
|
0, contrast, 0, 0,
|
|
0, 0, contrast, 0,
|
|
t, t, t, 1
|
|
);
|
|
}
|
|
|
|
mat4 saturationMatrix( float saturation ) {
|
|
vec3 luminance = vec3( 0.3086, 0.6094, 0.0820 );
|
|
float oneMinusSat = 1.0 - saturation;
|
|
vec3 red = vec3( luminance.x * oneMinusSat );
|
|
red+= vec3( saturation, 0, 0 );
|
|
vec3 green = vec3( luminance.y * oneMinusSat );
|
|
green += vec3( 0, saturation, 0 );
|
|
vec3 blue = vec3( luminance.z * oneMinusSat );
|
|
blue += vec3( 0, 0, saturation );
|
|
return mat4(
|
|
red, 0,
|
|
green, 0,
|
|
blue, 0,
|
|
0, 0, 0, 1
|
|
);
|
|
}
|
|
|
|
vec3 RRTAndODTFit( vec3 v ) {
|
|
vec3 a = v * ( v + 0.0245786 ) - 0.000090537;
|
|
vec3 b = v * ( 0.983729 * v + 0.4329510 ) + 0.238081;
|
|
return a / b;
|
|
}
|
|
|
|
vec3 ACESFilmicToneMapping( vec3 color ) {
|
|
// sRGB => XYZ => D65_2_D60 => AP1 => RRT_SAT
|
|
const mat3 ACESInputMat = mat3(
|
|
vec3( 0.59719, 0.07600, 0.02840 ), // transposed from source
|
|
vec3( 0.35458, 0.90834, 0.13383 ),
|
|
vec3( 0.04823, 0.01566, 0.83777 )
|
|
);
|
|
// ODT_SAT => XYZ => D60_2_D65 => sRGB
|
|
const mat3 ACESOutputMat = mat3(
|
|
vec3( 1.60475, -0.10208, -0.00327 ), // transposed from source
|
|
vec3( -0.53108, 1.10813, -0.07276 ),
|
|
vec3( -0.07367, -0.00605, 1.07602 )
|
|
);
|
|
color = ACESInputMat * color;
|
|
// Apply RRT and ODT
|
|
color = RRTAndODTFit( color );
|
|
color = ACESOutputMat * color;
|
|
// Clamp to [0, 1]
|
|
return saturate( color );
|
|
}
|
|
|
|
// source: https://www.cs.utah.edu/docs/techreports/2002/pdf/UUCS-02-001.pdf
|
|
vec3 ReinhardToneMapping( vec3 color ) {
|
|
return saturate( color / ( vec3( 1.0 ) + color ) );
|
|
}
|
|
|
|
// source: http://filmicworlds.com/blog/filmic-tonemapping-operators/
|
|
vec3 CineonToneMapping( vec3 color ) {
|
|
// optimized filmic operator by Jim Hejl and Richard Burgess-Dawson
|
|
color = max( vec3( 0.0 ), color - 0.004 );
|
|
return pow( ( color * ( 6.2 * color + 0.5 ) ) / ( color * ( 6.2 * color + 1.7 ) + 0.06 ), vec3( 2.2 ) );
|
|
}
|
|
|
|
// nothing
|
|
vec3 LinearToneMapping ( vec3 color ) {
|
|
return color;
|
|
}
|
|
|
|
// Matrices for rec 2020 <> rec 709 color space conversion
|
|
// matrix provided in row-major order so it has been transposed
|
|
// https://www.itu.int/pub/R-REP-BT.2407-2017
|
|
const mat3 LINEAR_REC2020_TO_LINEAR_SRGB = mat3(
|
|
vec3( 1.6605, - 0.1246, - 0.0182 ),
|
|
vec3( - 0.5876, 1.1329, - 0.1006 ),
|
|
vec3( - 0.0728, - 0.0083, 1.1187 )
|
|
);
|
|
|
|
const mat3 LINEAR_SRGB_TO_LINEAR_REC2020 = mat3(
|
|
vec3( 0.6274, 0.0691, 0.0164 ),
|
|
vec3( 0.3293, 0.9195, 0.0880 ),
|
|
vec3( 0.0433, 0.0113, 0.8956 )
|
|
);
|
|
|
|
// https://iolite-engine.com/blog_posts/minimal_agx_implementation
|
|
// Mean error^2: 3.6705141e-06
|
|
vec3 agxDefaultContrastApprox( vec3 x ) {
|
|
vec3 x2 = x * x;
|
|
vec3 x4 = x2 * x2;
|
|
return + 15.5 * x4 * x2
|
|
- 40.14 * x4 * x
|
|
+ 31.96 * x4
|
|
- 6.868 * x2 * x
|
|
+ 0.4298 * x2
|
|
+ 0.1191 * x
|
|
- 0.00232;
|
|
}
|
|
|
|
// AgX Tone Mapping implementation based on Filament, which in turn is based
|
|
// on Blender's implementation using rec 2020 primaries
|
|
// https://github.com/google/filament/pull/7236
|
|
// Inputs and outputs are encoded as Linear-sRGB.
|
|
|
|
vec3 AgXToneMapping( vec3 color ) {
|
|
|
|
// AgX constants
|
|
const mat3 AgXInsetMatrix = mat3(
|
|
vec3( 0.856627153315983, 0.137318972929847, 0.11189821299995 ),
|
|
vec3( 0.0951212405381588, 0.761241990602591, 0.0767994186031903 ),
|
|
vec3( 0.0482516061458583, 0.101439036467562, 0.811302368396859 )
|
|
);
|
|
|
|
// explicit AgXOutsetMatrix generated from Filaments AgXOutsetMatrixInv
|
|
const mat3 AgXOutsetMatrix = mat3(
|
|
vec3( 1.1271005818144368, - 0.1413297634984383, - 0.14132976349843826 ),
|
|
vec3( - 0.11060664309660323, 1.157823702216272, - 0.11060664309660294 ),
|
|
vec3( - 0.016493938717834573, - 0.016493938717834257, 1.2519364065950405 )
|
|
);
|
|
|
|
// LOG2_MIN = -10.0
|
|
// LOG2_MAX = +6.5
|
|
// MIDDLE_GRAY = 0.18
|
|
const float AgxMinEv = - 12.47393; // log2( pow( 2, LOG2_MIN ) * MIDDLE_GRAY )
|
|
const float AgxMaxEv = 4.026069; // log2( pow( 2, LOG2_MAX ) * MIDDLE_GRAY )
|
|
|
|
color = LINEAR_SRGB_TO_LINEAR_REC2020 * color;
|
|
|
|
color = AgXInsetMatrix * color;
|
|
|
|
// Log2 encoding
|
|
color = max( color, 1e-10 ); // avoid 0 or negative numbers for log2
|
|
color = log2( color );
|
|
color = ( color - AgxMinEv ) / ( AgxMaxEv - AgxMinEv );
|
|
|
|
color = clamp( color, 0.0, 1.0 );
|
|
|
|
// Apply sigmoid
|
|
color = agxDefaultContrastApprox( color );
|
|
|
|
// Apply AgX look
|
|
// v = agxLook(v, look);
|
|
|
|
color = AgXOutsetMatrix * color;
|
|
|
|
// Linearize
|
|
color = pow( max( vec3( 0.0 ), color ), vec3( 2.2 ) );
|
|
|
|
color = LINEAR_REC2020_TO_LINEAR_SRGB * color;
|
|
|
|
// Gamut mapping. Simple clamp for now.
|
|
color = clamp( color, 0.0, 1.0 );
|
|
|
|
return color;
|
|
|
|
}
|
|
|
|
// https://modelviewer.dev/examples/tone-mapping
|
|
|
|
vec3 NeutralToneMapping( vec3 color ) {
|
|
|
|
const float StartCompression = 0.8 - 0.04;
|
|
const float Desaturation = 0.15;
|
|
|
|
float x = min( color.r, min( color.g, color.b ) );
|
|
|
|
float offset = x < 0.08 ? x - 6.25 * x * x : 0.04;
|
|
|
|
color -= offset;
|
|
|
|
float peak = max( color.r, max( color.g, color.b ) );
|
|
|
|
if ( peak < StartCompression ) return color;
|
|
|
|
float d = 1. - StartCompression;
|
|
|
|
float newPeak = 1. - d * d / ( peak + d - StartCompression );
|
|
|
|
color *= newPeak / peak;
|
|
|
|
float g = 1. - 1. / ( Desaturation * ( peak - newPeak ) + 1. );
|
|
|
|
return mix( color, vec3( newPeak ), g );
|
|
|
|
}
|
|
|
|
|
|
|
|
void main() {
|
|
vec4 color = texture2D(map, vUv);
|
|
|
|
vec4 exposed = vec4(exposure * color.rgb, color.a);
|
|
|
|
vec4 tonemapped = vec4(TONEMAPPING_FUNCTION(exposed.rgb), color.a);
|
|
|
|
vec4 adjusted =
|
|
brightnessMatrix( brightness ) *
|
|
contrastMatrix( contrast ) *
|
|
saturationMatrix( saturation ) *
|
|
tonemapped;
|
|
|
|
gl_FragColor = adjusted;
|
|
}
|
|
`;
|
|
/**
|
|
* A Material used to adjust the SDR representation of an HDR image
|
|
*
|
|
* @category Materials
|
|
* @group Materials
|
|
*/
|
|
class SDRMaterial extends three.ShaderMaterial {
|
|
_brightness = 0;
|
|
_contrast = 1;
|
|
_saturation = 1;
|
|
_exposure = 1;
|
|
_toneMapping;
|
|
_map;
|
|
/**
|
|
*
|
|
* @param params
|
|
*/
|
|
constructor({ map, toneMapping }) {
|
|
super({
|
|
name: 'SDRMaterial',
|
|
vertexShader: vertexShader$1,
|
|
fragmentShader: fragmentShader$1,
|
|
uniforms: {
|
|
map: { value: map },
|
|
brightness: { value: 0 },
|
|
contrast: { value: 1 },
|
|
saturation: { value: 1 },
|
|
exposure: { value: 1 }
|
|
},
|
|
blending: three.NoBlending,
|
|
depthTest: false,
|
|
depthWrite: false
|
|
});
|
|
this._map = map;
|
|
this.toneMapping = this._toneMapping = toneMapping || three.ACESFilmicToneMapping;
|
|
this.needsUpdate = true;
|
|
this.uniformsNeedUpdate = true;
|
|
}
|
|
get toneMapping() { return this._toneMapping; }
|
|
set toneMapping(value) {
|
|
let valid = false;
|
|
switch (value) {
|
|
case three.ACESFilmicToneMapping:
|
|
this.defines.TONEMAPPING_FUNCTION = 'ACESFilmicToneMapping';
|
|
valid = true;
|
|
break;
|
|
case three.ReinhardToneMapping:
|
|
this.defines.TONEMAPPING_FUNCTION = 'ReinhardToneMapping';
|
|
valid = true;
|
|
break;
|
|
case three.CineonToneMapping:
|
|
this.defines.TONEMAPPING_FUNCTION = 'CineonToneMapping';
|
|
valid = true;
|
|
break;
|
|
case three.LinearToneMapping:
|
|
this.defines.TONEMAPPING_FUNCTION = 'LinearToneMapping';
|
|
valid = true;
|
|
break;
|
|
case three.AgXToneMapping:
|
|
this.defines.TONEMAPPING_FUNCTION = 'AgXToneMapping';
|
|
valid = true;
|
|
break;
|
|
case three.NeutralToneMapping:
|
|
this.defines.TONEMAPPING_FUNCTION = 'NeutralToneMapping';
|
|
valid = true;
|
|
break;
|
|
default:
|
|
console.error(`Unsupported toneMapping: ${value}. Using LinearToneMapping.`);
|
|
this.defines.TONEMAPPING_FUNCTION = 'LinearToneMapping';
|
|
this._toneMapping = three.LinearToneMapping;
|
|
}
|
|
if (valid) {
|
|
this._toneMapping = value;
|
|
}
|
|
this.needsUpdate = true;
|
|
}
|
|
get brightness() { return this._brightness; }
|
|
set brightness(value) {
|
|
this._brightness = value;
|
|
this.uniforms.brightness.value = value;
|
|
}
|
|
get contrast() { return this._contrast; }
|
|
set contrast(value) {
|
|
this._contrast = value;
|
|
this.uniforms.contrast.value = value;
|
|
}
|
|
get saturation() { return this._saturation; }
|
|
set saturation(value) {
|
|
this._saturation = value;
|
|
this.uniforms.saturation.value = value;
|
|
}
|
|
get exposure() { return this._exposure; }
|
|
set exposure(value) {
|
|
this._exposure = value;
|
|
this.uniforms.exposure.value = value;
|
|
}
|
|
get map() { return this._map; }
|
|
set map(value) {
|
|
this._map = value;
|
|
this.uniforms.map.value = value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Renders an SDR Representation of an HDR Image
|
|
*
|
|
* @category Encoding Functions
|
|
* @group Encoding Functions
|
|
*
|
|
* @param hdrTexture The HDR image to be rendered
|
|
* @param renderer (optional) WebGLRenderer to use during the rendering, a disposable renderer will be create and destroyed if this is not provided.
|
|
* @param toneMapping (optional) Tone mapping to be applied to the SDR Rendition
|
|
* @param renderTargetOptions (optional) Options to use when creating the output renderTarget
|
|
* @throws {Error} if the WebGLRenderer fails to render the SDR image
|
|
*/
|
|
const getSDRRendition = (hdrTexture, renderer, toneMapping, renderTargetOptions) => {
|
|
hdrTexture.needsUpdate = true;
|
|
const quadRenderer = new QuadRenderer({
|
|
width: hdrTexture.image.width,
|
|
height: hdrTexture.image.height,
|
|
type: three.UnsignedByteType,
|
|
colorSpace: three.SRGBColorSpace,
|
|
material: new SDRMaterial({ map: hdrTexture, toneMapping }),
|
|
renderer,
|
|
renderTargetOptions
|
|
});
|
|
try {
|
|
quadRenderer.render();
|
|
}
|
|
catch (e) {
|
|
quadRenderer.disposeOnDemandRenderer();
|
|
throw e;
|
|
}
|
|
return quadRenderer;
|
|
};
|
|
|
|
/**
|
|
* Encodes a Gainmap starting from an HDR file.
|
|
*
|
|
* @remarks
|
|
* if you do not pass a `renderer` parameter
|
|
* you must manually dispose the result
|
|
* ```js
|
|
* const encodingResult = await encode({ ... })
|
|
* // do something with the buffers
|
|
* const sdr = encodingResult.sdr.getArray()
|
|
* const gainMap = encodingResult.gainMap.getArray()
|
|
* // after that
|
|
* encodingResult.sdr.dispose()
|
|
* encodingResult.gainMap.dispose()
|
|
* ```
|
|
*
|
|
* @category Encoding Functions
|
|
* @group Encoding Functions
|
|
*
|
|
* @example
|
|
* import { encode, findTextureMinMax } from '@monogrid/gainmap-js'
|
|
* import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js'
|
|
*
|
|
* // load an HDR file
|
|
* const loader = new EXRLoader()
|
|
* const image = await loader.loadAsync('image.exr')
|
|
*
|
|
* // find RAW RGB Max value of a texture
|
|
* const textureMax = findTextureMinMax(image)
|
|
*
|
|
* // Encode the gainmap
|
|
* const encodingResult = encode({
|
|
* image,
|
|
* // this will encode the full HDR range
|
|
* maxContentBoost: Math.max.apply(this, textureMax)
|
|
* })
|
|
* // can be re-encoded after changing parameters
|
|
* encodingResult.sdr.material.exposure = 0.9
|
|
* encodingResult.sdr.render()
|
|
* // or
|
|
* encodingResult.gainMap.material.gamma = [1.1, 1.1, 1.1]
|
|
* encodingResult.gainMap.render()
|
|
*
|
|
* // do something with encodingResult.gainMap.toArray()
|
|
* // and encodingResult.sdr.toArray()
|
|
*
|
|
* // renderers must be manually disposed
|
|
* encodingResult.sdr.dispose()
|
|
* encodingResult.gainMap.dispose()
|
|
*
|
|
* @param params Encoding Parameters
|
|
* @returns
|
|
*/
|
|
const encode = (params) => {
|
|
const { image, renderer } = params;
|
|
const dataTexture = getDataTexture(image);
|
|
const sdr = getSDRRendition(dataTexture, renderer, params.toneMapping, params.renderTargetOptions);
|
|
const gainMapRenderer = getGainMap({
|
|
...params,
|
|
image: dataTexture,
|
|
sdr,
|
|
renderer: sdr.renderer // reuse the same (maybe disposable?) renderer
|
|
});
|
|
return {
|
|
sdr,
|
|
gainMap: gainMapRenderer,
|
|
hdr: dataTexture,
|
|
getMetadata: () => {
|
|
return {
|
|
gainMapMax: gainMapRenderer.material.gainMapMax,
|
|
gainMapMin: gainMapRenderer.material.gainMapMin,
|
|
gamma: gainMapRenderer.material.gamma,
|
|
hdrCapacityMax: gainMapRenderer.material.hdrCapacityMax,
|
|
hdrCapacityMin: gainMapRenderer.material.hdrCapacityMin,
|
|
offsetHdr: gainMapRenderer.material.offsetHdr,
|
|
offsetSdr: gainMapRenderer.material.offsetSdr
|
|
};
|
|
}
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Encodes a Gainmap starting from an HDR file into compressed file formats (`image/jpeg`, `image/webp` or `image/png`).
|
|
*
|
|
* Uses {@link encode} internally, then pipes the results to {@link compress}.
|
|
*
|
|
* @remarks
|
|
* if a `renderer` parameter is not provided
|
|
* This function will automatically dispose its "disposable"
|
|
* renderer, no need to dispose it manually later
|
|
*
|
|
* @category Encoding Functions
|
|
* @group Encoding Functions
|
|
* @example
|
|
* import { encodeAndCompress, findTextureMinMax } from '@monogrid/gainmap-js'
|
|
* import { encodeJPEGMetadata } from '@monogrid/gainmap-js/libultrahdr'
|
|
* import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js'
|
|
*
|
|
* // load an HDR file
|
|
* const loader = new EXRLoader()
|
|
* const image = await loader.loadAsync('image.exr')
|
|
*
|
|
* // find RAW RGB Max value of a texture
|
|
* const textureMax = findTextureMinMax(image)
|
|
*
|
|
* // Encode the gainmap
|
|
* const encodingResult = await encodeAndCompress({
|
|
* image,
|
|
* maxContentBoost: Math.max.apply(this, textureMax),
|
|
* mimeType: 'image/jpeg'
|
|
* })
|
|
*
|
|
* // embed the compressed images + metadata into a single
|
|
* // JPEG file
|
|
* const jpeg = encodeJPEGMetadata({
|
|
* ...encodingResult,
|
|
* sdr: encodingResult.sdr,
|
|
* gainMap: encodingResult.gainMap
|
|
* })
|
|
*
|
|
* // `jpeg` will be an `Uint8Array` which can be saved somewhere
|
|
*
|
|
*
|
|
* @param params Encoding Parameters
|
|
* @throws {Error} if the browser does not support [createImageBitmap](https://caniuse.com/createimagebitmap)
|
|
*/
|
|
const encodeAndCompress = async (params) => {
|
|
const encodingResult = encode(params);
|
|
const { mimeType, quality, flipY, withWorker } = params;
|
|
let compressResult;
|
|
let rawSDR;
|
|
let rawGainMap;
|
|
const sdrImageData = new ImageData(encodingResult.sdr.toArray(), encodingResult.sdr.width, encodingResult.sdr.height);
|
|
const gainMapImageData = new ImageData(encodingResult.gainMap.toArray(), encodingResult.gainMap.width, encodingResult.gainMap.height);
|
|
if (withWorker) {
|
|
const workerResult = await Promise.all([
|
|
withWorker.compress({
|
|
source: sdrImageData,
|
|
mimeType,
|
|
quality,
|
|
flipY
|
|
}),
|
|
withWorker.compress({
|
|
source: gainMapImageData,
|
|
mimeType,
|
|
quality,
|
|
flipY
|
|
})
|
|
]);
|
|
compressResult = workerResult;
|
|
rawSDR = workerResult[0].source;
|
|
rawGainMap = workerResult[1].source;
|
|
}
|
|
else {
|
|
compressResult = await Promise.all([
|
|
compress({
|
|
source: sdrImageData,
|
|
mimeType,
|
|
quality,
|
|
flipY
|
|
}),
|
|
compress({
|
|
source: gainMapImageData,
|
|
mimeType,
|
|
quality,
|
|
flipY
|
|
})
|
|
]);
|
|
rawSDR = sdrImageData.data;
|
|
rawGainMap = gainMapImageData.data;
|
|
}
|
|
encodingResult.sdr.dispose();
|
|
encodingResult.gainMap.dispose();
|
|
return {
|
|
...encodingResult,
|
|
...encodingResult.getMetadata(),
|
|
sdr: compressResult[0],
|
|
gainMap: compressResult[1],
|
|
rawSDR,
|
|
rawGainMap
|
|
};
|
|
};
|
|
|
|
const vertexShader = /* glsl */ `
|
|
varying vec2 vUv;
|
|
void main() {
|
|
vUv = uv;
|
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
}
|
|
`;
|
|
const fragmentShader = /* glsl */ `
|
|
precision mediump float;
|
|
|
|
#ifndef CELL_SIZE
|
|
#define CELL_SIZE 2
|
|
#endif
|
|
|
|
#ifndef COMPARE_FUNCTION
|
|
#define COMPARE_FUNCTION max
|
|
#endif
|
|
|
|
#ifndef INITIAL_VALUE
|
|
#define INITIAL_VALUE 0
|
|
#endif
|
|
|
|
uniform sampler2D map;
|
|
uniform vec2 u_srcResolution;
|
|
|
|
varying vec2 vUv;
|
|
|
|
void main() {
|
|
// compute the first pixel the source cell
|
|
vec2 srcPixel = floor(gl_FragCoord.xy) * float(CELL_SIZE);
|
|
|
|
// one pixel in source
|
|
vec2 onePixel = vec2(1) / u_srcResolution;
|
|
|
|
// uv for first pixel in cell. +0.5 for center of pixel
|
|
vec2 uv = (srcPixel + 0.5) * onePixel;
|
|
|
|
vec4 resultColor = vec4(INITIAL_VALUE);
|
|
|
|
for (int y = 0; y < CELL_SIZE; ++y) {
|
|
for (int x = 0; x < CELL_SIZE; ++x) {
|
|
resultColor = COMPARE_FUNCTION(resultColor, texture2D(map, uv + vec2(x, y) * onePixel));
|
|
}
|
|
}
|
|
|
|
gl_FragColor = resultColor;
|
|
}
|
|
`;
|
|
/**
|
|
*
|
|
* @category Utility
|
|
* @group Utility
|
|
*
|
|
* @param image
|
|
* @param mode
|
|
* @param renderer
|
|
* @returns
|
|
*/
|
|
const findTextureMinMax = (image, mode = 'max', renderer) => {
|
|
const srcTex = getDataTexture(image);
|
|
// check if texture has a valid format
|
|
if (srcTex.format !== three.AlphaFormat &&
|
|
srcTex.format !== three.RGBFormat &&
|
|
srcTex.format !== three.RGBAFormat &&
|
|
srcTex.format !== three.DepthFormat &&
|
|
srcTex.format !== three.DepthStencilFormat &&
|
|
srcTex.format !== three.RedFormat &&
|
|
srcTex.format !== three.RedIntegerFormat &&
|
|
srcTex.format !== three.RGFormat &&
|
|
srcTex.format !== three.RGIntegerFormat &&
|
|
srcTex.format !== three.RGBIntegerFormat &&
|
|
srcTex.format !== three.RGBAIntegerFormat) {
|
|
throw new Error('Unsupported texture format');
|
|
}
|
|
const cellSize = 2;
|
|
const mat = new three.ShaderMaterial({
|
|
vertexShader,
|
|
fragmentShader,
|
|
uniforms: {
|
|
u_srcResolution: { value: new three.Vector2(srcTex.image.width, srcTex.image.height) },
|
|
map: { value: srcTex }
|
|
},
|
|
defines: {
|
|
CELL_SIZE: cellSize,
|
|
COMPARE_FUNCTION: mode,
|
|
INITIAL_VALUE: mode === 'max' ? 0 : 65504 // max half float value
|
|
}
|
|
});
|
|
srcTex.needsUpdate = true;
|
|
mat.needsUpdate = true;
|
|
let w = srcTex.image.width;
|
|
let h = srcTex.image.height;
|
|
const quadRenderer = new QuadRenderer({
|
|
width: w,
|
|
height: h,
|
|
type: srcTex.type,
|
|
colorSpace: srcTex.colorSpace,
|
|
material: mat,
|
|
renderer
|
|
});
|
|
const frameBuffers = [];
|
|
while (w > 1 || h > 1) {
|
|
w = Math.max(1, (w + cellSize - 1) / cellSize | 0);
|
|
h = Math.max(1, (h + cellSize - 1) / cellSize | 0);
|
|
const fb = new three.WebGLRenderTarget(w, h, {
|
|
type: quadRenderer.type,
|
|
format: srcTex.format,
|
|
colorSpace: quadRenderer.colorSpace,
|
|
minFilter: three.NearestFilter,
|
|
magFilter: three.NearestFilter,
|
|
wrapS: three.ClampToEdgeWrapping,
|
|
wrapT: three.ClampToEdgeWrapping,
|
|
generateMipmaps: false,
|
|
depthBuffer: false,
|
|
stencilBuffer: false
|
|
});
|
|
frameBuffers.push(fb);
|
|
}
|
|
w = srcTex.image.width;
|
|
h = srcTex.image.height;
|
|
frameBuffers.forEach((fbi) => {
|
|
w = Math.max(1, (w + cellSize - 1) / cellSize | 0);
|
|
h = Math.max(1, (h + cellSize - 1) / cellSize | 0);
|
|
quadRenderer.renderTarget = fbi;
|
|
quadRenderer.render();
|
|
mat.uniforms.map.value = fbi.texture;
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
mat.uniforms.u_srcResolution.value.x = w;
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
mat.uniforms.u_srcResolution.value.y = h;
|
|
});
|
|
const out = quadRenderer.toArray();
|
|
quadRenderer.dispose();
|
|
frameBuffers.forEach(fb => fb.dispose());
|
|
return [
|
|
quadRenderer.type === three.FloatType ? out[0] : three.DataUtils.fromHalfFloat(out[0]),
|
|
quadRenderer.type === three.FloatType ? out[1] : three.DataUtils.fromHalfFloat(out[1]),
|
|
quadRenderer.type === three.FloatType ? out[2] : three.DataUtils.fromHalfFloat(out[2])
|
|
];
|
|
};
|
|
|
|
exports.GainMapEncoderMaterial = GainMapEncoderMaterial;
|
|
exports.SDRMaterial = SDRMaterial;
|
|
exports.compress = compress;
|
|
exports.encode = encode;
|
|
exports.encodeAndCompress = encodeAndCompress;
|
|
exports.findTextureMinMax = findTextureMinMax;
|
|
exports.getGainMap = getGainMap;
|
|
exports.getSDRRendition = getSDRRendition;
|
|
|
|
}));
|