Skip to content

Instantly share code, notes, and snippets.

@Sam-Izdat
Created June 19, 2024 03:55
Show Gist options
  • Save Sam-Izdat/c1f7ea42a778a10754f1aa167ebf7f35 to your computer and use it in GitHub Desktop.
Save Sam-Izdat/c1f7ea42a778a10754f1aa167ebf7f35 to your computer and use it in GitHub Desktop.
import {g, tm} from './util';
import {ColorTransform, TransferFunction} from './colorspace';
let ready = false;
let setup = () => ti.addToKernelScope({
tm,
ColorTransform, TransferFunction,
ToneMapping,
tone_map,
tone_map_hable, _hable_partial,
tone_map_agx, _agx_default_contrast_approx, _agx, _agx_eotf, _agx_look,
tone_map_aces_fitted_to_srgb
});
export class ToneMapping {
static apply = (mapper, v) => tone_map(mapper, v); // [ToneMapping, vec3] -> vec3
static Variant = class {
static NONE = 1<<0;
static CLAMP = 1<<1;
static AGX = 1<<2;
static AGX_PUNCHY = 1<<3;
static AGX_GOLDEN = 1<<4;
static HABLE = 1<<5;
static REINHARD = 1<<6;
static ACESCG = 1<<7;
static AGX_ANY = this.AGX | this.AGX_PUNCHY | this.AGX_GOLDEN;
static SUPPORTED = this.NONE | this.CLAMP | this.AGX_ANY | this.HABLE | this.ACESCG;
};
constructor(variant=ToneMapping.Variant.HABLE) {
return (async () => {
// Dummy kernel for ti.Static for now.
this.init = ti.classKernel(this, () => {});
this.init();
if (!ready) { setup(); ready=true; }
g.assert_bitwise_and(variant, ToneMapping.Variant.SUPPORTED, 'unsupported tone mapping');
this.variant = variant;
return this;
})();
}
}
export var tone_map = (tone_mapper, v) => {
if (ti.Static(tone_mapper.variant == ToneMapping.Variant.NONE)) {
return v;
} else if (ti.Static(tone_mapper.variant == ToneMapping.Variant.CLAMP)) {
return TransferFunction.srgb_oetf(tm.saturate(v));
} else if (ti.Static(tone_mapper.variant & ToneMapping.Variant.AGX_ANY)) {
return tone_map_agx(tone_mapper, v);
} else if (ti.Static(tone_mapper.variant == ToneMapping.Variant.HABLE)) {
return tone_map_hable(v);
} else if (ti.Static(tone_mapper.variant == ToneMapping.Variant.ACESCG)) {
return tone_map_aces_fitted_to_srgb(v);
} else {
return [1.,0.,0.];
}
}
var _hable_partial = (v) => {
let A = 0.15;
let B = 0.50;
let C = 0.10;
let D = 0.20;
let E = 0.02;
let F = 0.30;
return ((v*(A*v+C*B)+D*E)/(v*(A*v+B)+D*F))-E/F;
}
// HDR scene-linear sRGB in -> LDR sRGB out (display-ready w/ gamma)
export var tone_map_hable = (v) => {
let exposure_bias = 2.;
let curr = _hable_partial(v * exposure_bias);
let w = 11.2;
let white_scale = 1. / _hable_partial(w);
return TransferFunction.srgb_oetf(tm.saturate(curr * white_scale));
}
// HDR scene-linear ACEScg in -> LDR sRGB out (display-ready w/ gamma)
export var tone_map_aces_fitted_to_srgb = (v) => {
let out = tm.mm(ColorTransform.mat_aces_rrt_sat, v);
let a = out * (out + 0.0245786) - 0.000090537;
let b = out * (0.983729 * out + 0.4329510) + 0.238081;
// ODT_SAT -> CIE_XYZ -> D60_2_D65 -> sRGB
out = tm.mm(ColorTransform.mat_ap1_to_srgb_tm, a / b);
return TransferFunction.srgb_oetf(tm.saturate(out));
}
// Can be used if output should have AP1 primaries:
// HDR scene-linear ACEScg in -> LDR ACEScg out
// export var tone_map_aces_fitted = (v) => {
// let out = tm.mm(ColorTransform.mat_aces_rrt_sat, v);
// let a = out * (out + 0.0245786) - 0.000090537;
// let b = out * (0.983729 * out + 0.4329510) + 0.238081;
// out = tm.mm(ColorTransform.mat_aces_odt_sat, a / b);
// return tm.clamp(out, 0., 1.);
// }
// MIT License
//
// Copyright (c) 2024 Missing Deadlines (Benjamin Wrensch)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
// All values used to derive this implementation are sourced from Troy’s initial AgX implementation/OCIO config file available here:
// https://github.com/sobotka/AgX
// https://iolite-engine.com/blog_posts/minimal_agx_implementation
// Cheaper...
var _agx_default_contrast_approx = (x) => { // vec3 -> vec3
let x2 = x * x;
let 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;
}
// More expensive - 7th order
// var _agx_default_contrast_approx = (x) => { // vec3 -> vec3
// let x2 = x * x;
// let x4 = x2 * x2;
// let x6 = x4 * x2;
// return - 17.86 * x6 * x
// + 78.01 * x6
// - 126.7 * x4 * x
// + 92.06 * x4
// - 28.72 * x2 * x
// + 4.361 * x2
// - 0.1718 * x
// + 0.002857;
// }
var _agx = (v) => { // vec3 -> vec3
let agx_mat = [
[0.842479062253094 , 0.0784335999999992, 0.0792237451477643],
[0.0423282422610123, 0.878468636469772 , 0.0791661274605434],
[0.0423756549057051, 0.0784336 , 0.879142973793104 ]];
// Transpose of:
// let agx_mat = [
// [ 0.842479062253094, 0.0423282422610123, 0.0423756549057051],
// [ 0.0784335999999992, 0.878468636469772, 0.0784336],
// [ 0.0792237451477643, 0.0791661274605434, 0.879142973793104]];
let min_ev = -12.47393;
let max_ev = 4.026069;
// Input transform (inset)
let val = tm.mm(agx_mat, v);
// Log2 space encoding
val = tm.clamp(tm.log2(val), min_ev, max_ev);
val = (val - min_ev) / (max_ev - min_ev);
// Apply sigmoid function approximation
val = _agx_default_contrast_approx(val);
return val;
}
var _agx_eotf = (v) => { // vec3 -> vec3
let agx_mat_inv = [
[ 1.19687900512017 , -0.0980208811401368, -0.0990297440797205],
[-0.0528968517574562, 1.15190312990417 , -0.0989611768448433],
[-0.0529716355144438, -0.0980434501171241, 1.15107367264116 ]];
// Transpose of:
// let agx_mat_inv = [
// [1.19687900512017, -0.0528968517574562, -0.0529716355144438],
// [-0.0980208811401368, 1.15190312990417, -0.0980434501171241],
// [-0.0990297440797205, -0.0989611768448433, 1.15107367264116]];
// Inverse input transform (outset)
let val = tm.mm(agx_mat_inv, v);
// sRGB IEC 61966-2-1 2.2 Exponent Reference EOTF Display
// NOTE: We're linearizing the output here. Comment/adjust when
// *not* using a sRGB render target\
// val = ti.pow(val, [2.2, 2.2, 2.2]);
// More accurate EOTF - uncomment to linearize:
// val = TransferFunction.srgb_eotf(val);
// ----------------------------------------
// TODO: See what's Blender's doing.
//
// Three.js, apparently following Blender's lead, somehow goes via Rec. 2020 w/ sRGB linear i/o.
// https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderChunk/tonemapping_pars_fragment.glsl.js
//
// The changed matrices are (untransposed):
//
// let agx_mat_inv = [
// [ 1.1271005818144368, -0.1413297634984383, - 0.14132976349843826],
// [-0.11060664309660323, 1.157823702216272, - 0.11060664309660294],
// [-0.016493938717834573, - 0.016493938717834257, 1.2519364065950405]];
// let agx_mat = [
// [ 0.856627153315983, 0.137318972929847, 0.11189821299995],
// [ 0.0951212405381588, 0.761241990602591, 0.0767994186031903],
// [ 0.0482516061458583, 0.101439036467562, 0.811302368396859]];
//
// It's not totally clear what's going at the end in that threejs shader. The pow 2.2 approximates sRGB's EOTF,
// but then immediately there's a linear transform from Rec. 2020 to sRGB? And no OETF after?
// Something is a little fishy.
return tm.saturate(val);
}
var _agx_look = (v, slope, power, sat) => { // [vec3, vec3, vec3, float] -> vec3
let lw = [0.2126, 0.7152, 0.0722];
let luma = ti.dot(v, lw);
let offset = [0.0, 0.0, 0.0];
// ASC CDL
let val = ti.pow(v * slope + offset, power);
return luma + sat * (val - luma);
}
// HDR scene-linear sRGB in -> LDR sRGB out (display-ready w/ gamma)
export var tone_map_agx = (tone_mapper, v) => { // [vec3, int] -> vec3
let value = _agx(v);
let slope = [1.0, 1.0, 1.0];
let power = [1.0, 1.0, 1.0];
let sat = 1.0;
if (ti.Static(tone_mapper.variant == ToneMapping.Variant.AGX_GOLDEN)) {
slope = [1.0, 0.9, 0.5];
power = [0.8, 0.8, 0.8];
sat = 0.8;
} else if (ti.Static(tone_mapper.variant == ToneMapping.Variant.AGX_PUNCHY)) {
// To match Sobotka:
slope = [1.0, 1.0, 1.0];
power = [1.35, 1.35, 1.35];
sat = 1.4;
// To match MrLixm:
// slope = [1.0, 1.0, 1.0];
// power = [1.3, 1.3, 1.3];
// sat = 1.2;
}
value = _agx_look(value, slope, power, sat);
value = _agx_eotf(value);
return value;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment