148 lines
3.7 KiB
JavaScript
148 lines
3.7 KiB
JavaScript
/* @license
|
|
* Copyright 2024 Google Inc. All Rights Reserved.
|
|
* Licensed under the Apache License, Version 2.0 (the 'License');
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an 'AS IS' BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
// Running:
|
|
// node lut-writer.mjs
|
|
// produces: pbrNeutral.cube
|
|
|
|
import * as fs from 'fs';
|
|
|
|
const size = 57;
|
|
// must match the lg2 vars in config.ocio
|
|
const log2Min = -9;
|
|
const log2Max = 10;
|
|
|
|
const text = [
|
|
`TITLE "PBR Neutral sRGB"`,
|
|
`# PBR Neutral sRGB LUT`,
|
|
`DOMAIN_MIN 0 0 0`,
|
|
`DOMAIN_MAX 1 1 1`,
|
|
`LUT_3D_SIZE ${size}`,
|
|
];
|
|
|
|
function clamp(x) {
|
|
return Math.min(Math.max(x, 0), 1);
|
|
}
|
|
|
|
function round(x) {
|
|
return clamp(x).toFixed(7).replace(/\.?0*$/, '');
|
|
}
|
|
|
|
function inverseLog(x) {
|
|
const log2 = (x / (size - 1)) * (log2Max - log2Min) + log2Min;
|
|
return Math.pow(2, log2);
|
|
}
|
|
|
|
function rgb2string(rgb) {
|
|
return `${round(rgb.R)} ${round(rgb.G)} ${round(rgb.B)}`;
|
|
}
|
|
|
|
function pbrNeutral(rgb) {
|
|
const startCompression = 0.8 - 0.04;
|
|
const desaturation = 0.15;
|
|
|
|
let {R, G, B} = rgb;
|
|
|
|
const x = Math.min(R, G, B);
|
|
const offset = x < 0.08 ? x - 6.25 * x * x : 0.04;
|
|
R -= offset;
|
|
G -= offset;
|
|
B -= offset;
|
|
|
|
const peak = Math.max(R, G, B);
|
|
if (peak < startCompression) {
|
|
return {R, G, B};
|
|
}
|
|
|
|
const d = 1 - startCompression;
|
|
const newPeak = 1 - d * d / (peak + d - startCompression);
|
|
const scale = newPeak / peak;
|
|
R *= scale;
|
|
G *= scale;
|
|
B *= scale;
|
|
|
|
const f = 1 / (desaturation * (peak - newPeak) + 1);
|
|
R = f * R + (1 - f) * newPeak;
|
|
G = f * G + (1 - f) * newPeak;
|
|
B = f * B + (1 - f) * newPeak;
|
|
|
|
return {R, G, B};
|
|
}
|
|
|
|
function inverseNeutral(rgb) {
|
|
const startCompression = 0.8 - 0.04;
|
|
const desaturation = 0.15;
|
|
|
|
let {R, G, B} = rgb;
|
|
|
|
const peak = Math.max(R, G, B);
|
|
if (peak > startCompression) {
|
|
const d = 1 - startCompression;
|
|
const oldPeak = d * d / (1 - peak) - d + startCompression;
|
|
const fInv = desaturation * (oldPeak - peak) + 1;
|
|
const f = 1 / fInv;
|
|
R = (R + (f - 1) * peak) * fInv;
|
|
G = (G + (f - 1) * peak) * fInv;
|
|
B = (B + (f - 1) * peak) * fInv;
|
|
const scale = oldPeak / peak;
|
|
R *= scale;
|
|
G *= scale;
|
|
B *= scale;
|
|
}
|
|
|
|
const y = Math.min(R, G, B);
|
|
let offset = 0.04;
|
|
if (y < 0.04) {
|
|
const x = Math.sqrt(y / 6.25);
|
|
offset = x - 6.25 * x * x;
|
|
}
|
|
R += offset;
|
|
G += offset;
|
|
B += offset;
|
|
|
|
return {R, G, B};
|
|
}
|
|
|
|
function relativeError(rgbBase, rgbCheck) {
|
|
const {R, G, B} = rgbBase;
|
|
const dR = rgbCheck.R - R;
|
|
const dG = rgbCheck.G - G;
|
|
const dB = rgbCheck.B - B;
|
|
const dMag = Math.sqrt(dR * dR + dG * dG + dB * dB);
|
|
const dBase = Math.max(Math.sqrt(R * R + G * G + B * B), 1e-20);
|
|
return dMag / dBase;
|
|
}
|
|
|
|
let maxError = 0;
|
|
for (let b = 0; b < size; ++b) {
|
|
for (let g = 0; g < size; ++g) {
|
|
for (let r = 0; r < size; ++r) {
|
|
// invert the lg2 transform in the OCIO config - used to more evenly-space
|
|
// the LUT points
|
|
const rgbIn = {R: inverseLog(r), G: inverseLog(g), B: inverseLog(b)};
|
|
const rgbOut = pbrNeutral(rgbIn);
|
|
text.push(rgb2string(rgbOut));
|
|
// verify inverse
|
|
const rgbIn2 = inverseNeutral(rgbOut);
|
|
const error = relativeError(rgbIn, rgbIn2);
|
|
maxError = Math.max(maxError, error);
|
|
}
|
|
}
|
|
}
|
|
text.push('');
|
|
|
|
console.log('Maximum relative error of inverse = ', maxError);
|
|
|
|
fs.writeFileSync('pbrNeutral.cube', text.join('\n')); |