# Logik Live Gamut Compressor

Hey All

Just watched the Logik Live - thanks Finn, thanks Andy - about the Gamut compressor, I played around with extensively with this and got it to work - if anyone is interested, I can make a little video…?

3 Likes

cool, what was the trick , or was it just my
version of flame? nice!

I read the GitHub a bit and played around until I got it doing stuff. I wouldn’t say I’m a master or that but I can get it to compress. I think.

1 Like

Video or it didn’t happen.

2 Likes

Challenge accepted @randy Mac

1 Like
2 Likes

ah cool!

so I should have just RTFM instead of giving up after trying it for 2 minutes?

feeding it a gradient… like whaaat.

1 Like

I think it needed inputs on the other two and white was giving weird results so I switched it to black. I tend to use gradient node a lot to feed colour into a node because it automatically resizes to the back plate. And you can use it to sample a colour from the back plate more quickly. Lazy I know.

ah, yea I tried feeding it black or white and it wasnt doing anything , you can click black or white in the input option on the matchbox as well but maybe … there is something wrong with how I did it.

What flame version are you on? and can you link me the glsl you downloaded? I tried both the one from jed smith github and the ampas one

uniform sampler2D frontTex, matteTex, selectiveTex;
uniform float power, cyan, magenta, yellow, shd_rolloff, adsk_result_w, adsk_result_h;
uniform int method, working_colorspace;
uniform bool invert, hexagonal;
uniform vec3 threshold;

const float pi = 3.14159265359;

// calc hyperbolic tangent
float tanh( float val) {
float f = exp(2.0*val);
return (f-1.0) / (f+1.0);
}

// calc inverse hyperbolic tangent
float atanh( float val) {
return log((1.0+val)/(1.0-val))/2.0;
}

// Convert acescg to acescct
float lin_to_acescct(float val) {
if (val <= 0.0078125) {
return 10.5402377416545 * val + 0.0729055341958355;
} else {
return (log2(val) + 9.72) / 17.52;
}
}

// Convert acescct to acescg
float acescct_to_lin(float val) {
if (val > 0.155251141552511) {
return pow( 2.0, val*17.52 - 9.72);
} else {
return (val - 0.0729055341958355) / 10.5402377416545;
}
}

// Convert acescg to acescc
float lin_to_acescc(float val) {
if (val <= 0.0) {
return -0.3584474886; // =(log2( pow(2.,-16.))+9.72)/17.52
} else if (val < pow(2.0, -15.0)) {
return (log2(pow(2.0, -16.0)+val*0.5)+9.72)/17.52;
} else { // (val >= pow(2.,-15))
return (log2(val)+9.72)/17.52;
}
}

// Convert acescc to acescg
float acescc_to_lin(float val) {
if (val < -0.3013698630) { // (9.72-15)/17.52
return (pow(2.0, val*17.52-9.72) - pow(2.0, -16.0))2.0;
} else if (val < (log2(65504.0)+9.72)/17.52) {
return pow(2.0, val
17.52-9.72);
} else { // (val >= (log2(HALF_MAX)+9.72)/17.52)
return 65504.0;
}
}

// compression function which gives the y=1 x in tersect at y=0
float f(float x, float k, float thr, int method) {
if (method == 0) {
// natural logarithm compression method
return (exp((1.0-thr+thrlog(1.0-x)-xthrlog(1.0-x))/(thr(1.0-x))))thr+xthr-k;
} else if (method == 1 || method == 2) {
return k;
} else if (method == 3) {
// natural exponent compression method
return -log((-x+1.0)/(thr-x))(-thr+x)+thr-k;
} else if (method == 4) {
// arctangent compression method
return (2.0
tan( (pi*(1.0-thr))/(2.0*(x-thr)))(x-thr))/pi+thr-k;
} else if (method == 5) {
// hyperbolic tangent compression method
return atanh((1.0-thr)/(x-thr))
(x-thr)+thr-k;
}
}

int _sign(float x) {
return x == 0.0 ? 0 : x > 0.0 ? 1 : 0;
}

float bisect(float k, float thr, int method) {
// use a simple bisection algorithm to bruteforce the root of f
// returns an approximation of the value of limit
// such that the compression function intersects y=1 at desired value k
// this allows us to specify the max distance we will compress to the gamut boundary

float a, b, c, y;
float tol = 0.0001; // accuracy of estimate
int nmax = 100; // max iterations

// set up reasonable initial guesses for each method given output ranges of each function
if (method == 0) {
// natural logarithm needs a limit between -inf (linear), and 1 (clip)
a = -15.0;
b = 0.98;
} else if (method == 5) {
// tanh needs more precision
a = 1.000001;
b = 5.0;
} else {
a = 1.0001;
b = 5.0;
}

if (_sign(f(a, k, thr, method)) == _sign(f(b, k, thr, method))) {
// bad estimate. return something close to linear
if ((method == 0) || (method == 3)) {
return -100.0;
} else {
return 1.999999;
}
}
c = (a+b)/2.0;
y = f(c, k, thr, method);
if (abs(y) <= tol) {
return c; // lucky guess
}
int n = 1;
while ((abs(y) > tol) && (n <= nmax)) {
if (_sign(y) == _sign(f(a, k, thr, method))) {
a = c;
} else {
b = c;
}
c = (a+b)/2.0;
y = f(c, k, thr, method);
n += 1;
}
return c;
}

// calculate compressed distance
float compress(float dist, float lim, float thr, bool invert, int method, float power) {
float cdist, s;
if (dist < thr) {
cdist = dist;
} else {
if (method == 0) {
// natural logarithm compression method: Natural Logarithm Compression Function
// inspired by ITU-R BT.2446 http://www.itu.int/pub/R-REP-BT.2446-2019
if (!invert) {
cdist = thrlog(dist/thr-lim)-limthrlog(dist/thr-lim)+thr-thrlog(1.0-lim)+limthrlog(1.0-lim);
} else {
cdist = exp((dist-thr+thrlog(1.0-lim)-limthrlog(1.0-lim))/(thr(1.0-lim)))thr+limthr;
}
} else if (method == 1) {
// simple Reinhard type compression method: Reinhard Compression Function
if (!invert) {
cdist = thr + 1.0/(1.0/(dist - thr) + 1.0/(1.0 - thr) - 1.0/(lim - thr));
} else {
cdist = thr + 1.0/(1.0/(dist - thr) - 1.0/(1.0 - thr) + 1.0/(lim - thr));
}
} else if (method == 2) {
// power§ compression function plot PowerP Compression Function
if (lim < 1.0001) {
return dist; // disable compression, avoid nan
}
s = (lim-thr)/pow(pow((1.0-thr)/(lim-thr),-power)-1.0,1.0/power); // calc y=1 intersect
if (!invert) {
cdist = thr+s*((dist-thr)/s)/(pow(1.0+pow((dist-thr)/s,power),1.0/power)); // compress
} else {
if (dist > (thr + s)) {
cdist = dist; // avoid singularity
}
cdist = thr+spow(-(pow((dist-thr)/s,power)/(pow((dist-thr)/s,power)-1.0)),1.0/power); // uncompress
}
} else if (method == 3) {
// natural exponent compression method: Natural Exponent Compression Function
if (!invert) {
cdist = lim-(lim-thr)exp(-(((dist-thr)((1.0
lim)/(lim-thr))/lim)));
} else {
cdist = -log((dist-lim)/(thr-lim))*(-thr+lim)/1.0+thr;
}
} else if (method == 4) {
// arctangent compression method: plot ArcTan Functions
if (!invert) {
cdist = thr + (lim - thr) * 2.0 / pi * atan(pi/2.0 * (dist - thr)/(lim - thr));
} else {
cdist = thr + (lim - thr) * 2.0 / pi * tan(pi/2.0 * (dist - thr)/(lim - thr));
}
} else if (method == 5) {
// hyperbolic tangent compression method: Hyperbolic Tangent Compression Function
if (!invert) {
cdist = thr + (lim - thr) * tanh( ( (dist - thr)/( lim - thr)));
} else {
cdist = thr + (lim - thr) * atanh( dist/( lim - thr) - thr/( lim - thr));
}
}
}
return cdist;
}

void main() {
vec2 coords = gl_FragCoord.xy / vec2( adsk_result_w, adsk_result_h );
// source pixels
vec3 rgb = texture2D(frontTex, coords).rgb;
float alpha = texture2D(matteTex, coords).g;
float select = texture2D(selectiveTex, coords).g;

if (working_colorspace == 1) {
rgb.x = acescct_to_lin(rgb.x);
rgb.y = acescct_to_lin(rgb.y);
rgb.z = acescct_to_lin(rgb.z);
} else if (working_colorspace == 2) {
rgb.x = acescc_to_lin(rgb.x);
rgb.y = acescc_to_lin(rgb.y);
rgb.z = acescc_to_lin(rgb.z);
}

// thr is the percentage of the core gamut to protect: the complement of threshold.
vec3 thr = vec3(
1.0-max(0.00001, threshold.x),
1.0-max(0.00001, threshold.y),
1.0-max(0.00001, threshold.z));

// lim is the max distance from the gamut boundary that will be compressed
// 0 is a no-op, 1 will compress colors from a distance of 2.0 from achromatic to the gamut boundary
// if method is Reinhard, use the limit as-is
vec3 lim;
if (method == 1 || method == 2) {
lim = vec3(cyan+1.0, magenta+1.0, yellow+1.0);
} else {
// otherwise, we have to bruteforce the value of limit
// such that lim is the value of x where y=1 - also enforce sane ranges to avoid nans

``````// Not sure of a way to pre-calculate a constant using the values from the ui parameters in GLSL...
// This approach might have performance implications
lim = vec3(
bisect(max(0.0001, cyan)+1.0, thr.x, method),
bisect(max(0.0001, magenta)+1.0, thr.y, method),
bisect(max(0.0001, yellow)+1.0, thr.z, method));
``````

}

// achromatic axis
float ach = max(rgb.x, max(rgb.y, rgb.z));

// achromatic shadow rolloff
float ach_shd;
if (shd_rolloff < 0.004) {
// disable shadow rolloff functionality.
// values below 0.004 cause strange behavior, actually increasing distance in some cases.
// if ach < 0.0 and shd_rolloff is disabled, take absolute value. This preserves negative components after compression.
ach_shd = abs(ach);
} else {
// lift ach below threshold using a tanh compression function.
// this reduces large distance values in shadow grain, which can cause differences when inverting.
ach_shd = 1.0-((1.0-ach)<(1.0-shd_rolloff)?(1.0-ach):(1.0-shd_rolloff)+shd_rolloff*tanh((((1.0-ach)-(1.0-shd_rolloff))/shd_rolloff)));
}

// distance from the achromatic axis for each color component aka inverse rgb ratios
vec3 dist;
dist.x = ach_shd == 0.0 ? 0.0 : (ach-rgb.x)/ach_shd;
dist.y = ach_shd == 0.0 ? 0.0 : (ach-rgb.y)/ach_shd;
dist.z = ach_shd == 0.0 ? 0.0 : (ach-rgb.z)/ach_shd;

// compress distance with user controlled parameterized shaper function
float sat;
vec3 csat, cdist;
if (hexagonal) {
// Based on Nick Shaw’s variation on the gamut mapping algorithm
// A variation on Jed's RGB gamut mapper - VWG – Gamut Mapping - Community - ACESCentral
sat = max(dist.x, max(dist.y, dist.z));
csat = vec3(
compress(sat, lim.x, thr.x, invert, method, power),
compress(sat, lim.y, thr.y, invert, method, power),
compress(sat, lim.z, thr.z, invert, method, power));
cdist = sat == 0.0 ? dist : vec3(
dist.x * csat.x / sat,
dist.y * csat.y / sat,
dist.z * csat.z / sat);
} else {
cdist = vec3(
compress(dist.x, lim.x, thr.x, invert, method, power),
compress(dist.y, lim.y, thr.y, invert, method, power),
compress(dist.z, lim.z, thr.z, invert, method, power));
}

// recalculate rgb from compressed distance and achromatic
// effectively this scales each color component relative to achromatic axis by the compressed distance
vec3 crgb = vec3(
ach-cdist.xach_shd,
ach-cdist.y
ach_shd,
ach-cdist.z*ach_shd);

if (working_colorspace == 1) {
crgb.x = lin_to_acescct(crgb.x);
crgb.y = lin_to_acescct(crgb.y);
crgb.z = lin_to_acescct(crgb.z);
} else if (working_colorspace == 2) {
crgb.x = lin_to_acescc(crgb.x);
crgb.y = lin_to_acescc(crgb.y);
crgb.z = lin_to_acescc(crgb.z);
}

crgb = mix(rgb, crgb, select);

gl_FragColor = vec4(crgb, alpha);
}

It was from Jedpod ban’t recall which version, I suspect 0.36 - copied and pasted the code because I wasn’t allowed to upload a glsl file. This has been used on flame 2020.x->2020.3.1

1 Like

GitHub - jedypod/gamut-compress: Tools to compress out of gamut colors back into gamut. I now downloaded that one instead of the other 2 branches I tried (I cant remeber I pulled in the matchbox the day of the live session… shame on me) and this one works , it does look different from your video however but yay it does what I want , thats great.

I am on 2021.0 btw

I just hope AMPAS puts it into ‘acesNEXT’ also hoping for parametric view transforms

2 Likes

For what we use it for (that midnight session where you have to deliver and there isn’t a sober colourist available), I’ve linked the xyz values together so you get an overall compression without changing the hue.

1 Like

ARRRRRGH. GitHub and I do not get along.

How do I download the Matchbox for Flame Gamut Compression.

Maybe I just copy and paste the code and make my own .xml matchbox. Do I need to compile the binaries

1 Like

Help me @johnt Kenobi. You’re my only hope.

Isn’t there one packaged up in the matchboxes from Autodesk?

2 Likes

Indeed.

Looks a little different to what I was expecting but it checks out.

``````ACES 1.3 Reference Gamut Compression
Gamut compression algorithm to bring out-of-gamut scene-referred values into the ACEScg gamut (AP1).

Based on https://github.com/ampas/aces-dev/blob/dev/transforms/ctl/lmt/LMT.Academy.GamutCompress.ctl

ACEStransformIDurn:ampas:aces:transformId:v1.5:LMT.Academy.GamutCompress.a1.3.0/ACEStransformID
ACESuserNameACES 1.3 Look - Gamut Compress/ACESuserName

``````
1 Like

Yeah it doesn’t give you as much control as the one in the link that Finn provided above. It’s relatively simple to build.

If you copy and paste the code from the .glsl file into your own text document named gamutCompress.glsl, then you can build the matchbox on your Flame. Just open a terminal and run

/opt/Autodesk/{flame_version}/bin/shader_builder -m -x gamutCompress.glsl
/opt/Autodesk/{flame_version}/bin/shader_builder -m -p gamutCompress.glsl

That should create a matchbox in the same directory that you can then bring into Flame.

Thanks @Ryland so would you mind just talking me through `-m`, `-p` and `-x`

We are using a shader_builder