Skip to content

Commit 7279710

Browse files
author
Jed Smith
committed
change to parabolic compression function. simplify. add presets for different camera source gamuts to nuke nodes.
1 parent e6b1d7c commit 7279710

File tree

7 files changed

+286
-635
lines changed

7 files changed

+286
-635
lines changed

GamutCompress.blink

Lines changed: 29 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,95 +1,46 @@
1+
/* GamutCompress v0.7
2+
Written by Jed Smith with lots of help from the ACES Gamut Mapping VWG
3+
https://github.com/jedypod
4+
https://community.acescentral.com/t/rgb-saturation-gamut-mapping-approach-and-a-comp-vfx-perspective
5+
https://community.acescentral.com/c/aces-development-acesnext/vwg-aces-gamut-mapping-working-group
6+
*/
17
kernel GamutCompression : public ImageComputationKernel<ePixelWise> {
28
Image<eRead, eAccessPoint, eEdgeClamped> src;
39
Image<eWrite> dst;
410

511
param:
6-
float3 threshold;
7-
float p;
8-
float shd_rolloff;
9-
float cyan;
10-
float magenta;
11-
float yellow;
12+
float3 th;
13+
float3 dl;
1214
bool invert;
1315

1416
local:
15-
float3 thr;
16-
float3 lim;
17+
float3 s;
1718

1819
void init() {
19-
// thr is the percentage of the core gamut to protect: the complement of threshold.
20-
thr = float3(1.0f-threshold.x, 1.0f-threshold.y, 1.0f-threshold.z);
21-
22-
// lim is the distance beyond the gamut boundary that will be compressed to the gamut boundary.
23-
// lim = 0.2 will compress from a distance of 1.2 from achromatic to 1.0 (the gamut boundary).
24-
lim = float3(cyan+1.0f, magenta+1.0f, yellow+1.0f);
25-
}
26-
27-
// calculate hyperbolic tangent
28-
float tanh( float in) {
29-
float f = exp(2.0f*in);
30-
return (f-1.0f)/(f+1.0f);
31-
}
32-
33-
// calculate compressed distance
34-
float compress(float x, float l, float t) {
35-
float cdist;
36-
// power(p) compression function plot https://www.desmos.com/calculator/54aytu7hek
37-
// suggested by James Eggleton https://community.acescentral.com/t/gamut-mapping-compression-curves/3073/10
38-
float s = (l-t)/pow(pow((1.0f-t)/(l-t),-p)-1.0f,1.0f/p); // calc y=1 intersect
39-
if (l < 1.0001) {
40-
return x; // disable compression, avoid nan
41-
}
42-
if (x < t) {
43-
cdist = x;
44-
} else {
45-
if (invert == 0) {
46-
cdist = t+s*((x-t)/s)/(pow(1.0f+pow((x-t)/s,p),1.0f/p)); // compress
47-
} else {
48-
if (x > (t + s)) {
49-
cdist = x; // avoid singularity
50-
}
51-
cdist = t+s*pow(-(pow((x-t)/s,p)/(pow((x-t)/s,p)-1.0f)),1.0f/p); // uncompress
52-
}
53-
}
54-
return cdist;
20+
// Pre-calculate scale so compression function passes through distance limit: (x=dl, y=1)
21+
s = ((float3)(1.0f)-th)/sqrt(max((float3)(1.001f),dl)-(float3)(1.0f));
5522
}
5623

5724
void process() {
58-
// source pixels
59-
SampleType(src) rgba = src();
60-
float3 rgb = float3(rgba.x, rgba.y, rgba.z);
61-
62-
// achromatic axis
63-
float ach = max(rgb.x, max(rgb.y, rgb.z));
64-
65-
// achromatic shadow rolloff
66-
float ach_shd;
67-
if (shd_rolloff < 0.004f) {
68-
// disable shadow rolloff functionality.
69-
// values below 0.004 cause strange behavior, actually increasing distance in some cases.
70-
// if ach < 0.0 and shd_rolloff is disabled, take absolute value. This preserves negative components after compression.
71-
ach_shd = fabs(ach);
25+
float3 rgb = float3(src().x, src().y, src().z);
26+
27+
float ac = max(rgb.x, max(rgb.y, rgb.z)); // Achromatic axis
28+
// Inverse RGB Ratios: distance from achromatic axis
29+
float3 d = ac == 0.0f ? (float3)(0.0f) : (ac-rgb)/fabs(ac);
30+
31+
float3 cd; // Compressed distance
32+
// Parabolic compression function: https://www.desmos.com/calculator/nvhp63hmtj
33+
if (!invert) {
34+
cd.x = d.x<th.x?d.x:s.x*sqrt(d.x-th.x+s.x*s.x/4.0f)-s.x*sqrt(s.x*s.x/4.0f)+th.x;
35+
cd.y = d.y<th.y?d.y:s.y*sqrt(d.y-th.y+s.y*s.y/4.0f)-s.y*sqrt(s.y*s.y/4.0f)+th.y;
36+
cd.z = d.z<th.z?d.z:s.z*sqrt(d.z-th.z+s.z*s.z/4.0f)-s.z*sqrt(s.z*s.z/4.0f)+th.z;
7237
} else {
73-
// lift ach below threshold using a tanh compression function.
74-
// this reduces large distance values in shadow grain, which can cause differences when inverting.
75-
ach_shd = 1.0f-((1.0f-ach)<(1.0f-shd_rolloff)?(1.0f-ach):(1.0f-shd_rolloff)+shd_rolloff*tanh((((1.0f-ach)-(1.0f-shd_rolloff))/shd_rolloff)));
76-
}
77-
78-
// distance from the achromatic axis for each color component aka inverse rgb ratios.
79-
// we normalize the distance by dividing by achromatic, so that 1.0 is at gamut boundary, avoid 0 division errors.
80-
float3 dist = ach_shd == 0.0f ? float3(0.0f, 0.0f, 0.0f) : (ach-rgb)/ach_shd;
81-
82-
// compress distance with parameterized compression function
83-
float3 cdist = float3(
84-
compress(dist.x, lim.x, thr.x),
85-
compress(dist.y, lim.y, thr.y),
86-
compress(dist.z, lim.z, thr.z));
87-
88-
// recalculate rgb from compressed distance and achromatic
89-
// effectively this scales each color component relative to achromatic axis by the compressed distance
90-
float3 crgb = ach-cdist*ach_shd;
38+
cd.x = d.x<th.x?d.x:pow(d.x-th.x+s.x*sqrt(s.x*s.x/4.0f),2.0f)/(s.x*s.x)-s.x*s.x/4.0f+th.x;
39+
cd.y = d.y<th.y?d.y:pow(d.y-th.y+s.y*sqrt(s.y*s.y/4.0f),2.0f)/(s.y*s.y)-s.y*s.y/4.0f+th.y;
40+
cd.z = d.z<th.z?d.z:pow(d.z-th.z+s.z*sqrt(s.z*s.z/4.0f),2.0f)/(s.z*s.z)-s.z*s.z/4.0f+th.z;
41+
}
9142

92-
// write to output
93-
dst() = float4(crgb.x, crgb.y, crgb.z, rgba.w);
43+
rgb = ac-cd*fabs(ac);
44+
dst() = float4(rgb.x, rgb.y, rgb.z, src().w);
9445
}
9546
};

GamutCompress.dctl

Lines changed: 44 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
DEFINE_UI_PARAMS(threshold_c, threshold c, DCTLUI_SLIDER_FLOAT, 0.2f, 0.0f, 0.6f, 0.0f);
2-
DEFINE_UI_PARAMS(threshold_m, threshold m, DCTLUI_SLIDER_FLOAT, 0.2f, 0.0f, 0.6f, 0.0f);
3-
DEFINE_UI_PARAMS(threshold_y, threshold y, DCTLUI_SLIDER_FLOAT, 0.2f, 0.0f, 0.6f, 0.0f);
4-
DEFINE_UI_PARAMS(power, power, DCTLUI_SLIDER_FLOAT, 1.2f, 1.0f, 3.0f, 1.0f);
5-
DEFINE_UI_PARAMS(shd_rolloff, shd rolloff, DCTLUI_SLIDER_FLOAT, 0.0f, 0.0f, 0.03f, 0.0f);
6-
DEFINE_UI_PARAMS(cyan, cyan, DCTLUI_SLIDER_FLOAT, 0.09f, 0.0f, 1.0f, 0.0f);
7-
DEFINE_UI_PARAMS(magenta, magenta, DCTLUI_SLIDER_FLOAT, 0.24f, 0.0f, 1.0f, 0.0f);
8-
DEFINE_UI_PARAMS(yellow, yellow, DCTLUI_SLIDER_FLOAT, 0.12f, 0.0f, 1.0f, 0.0f);
1+
DEFINE_UI_PARAMS(th_c, thr c, DCTLUI_SLIDER_FLOAT, 0.15, 0.0, 0.3, 0.0);
2+
DEFINE_UI_PARAMS(th_m, thr m, DCTLUI_SLIDER_FLOAT, 0.15, 0.0, 0.3, 0.0);
3+
DEFINE_UI_PARAMS(th_y, thr y, DCTLUI_SLIDER_FLOAT, 0.15, 0.0, 0.3, 0.0);
4+
DEFINE_UI_PARAMS(d_c, dist c, DCTLUI_SLIDER_FLOAT, 0.1, 0.0, 0.4, 0.0);
5+
DEFINE_UI_PARAMS(d_m, dist m, DCTLUI_SLIDER_FLOAT, 0.2, 0.0, 0.4, 0.0);
6+
DEFINE_UI_PARAMS(d_y, dist y, DCTLUI_SLIDER_FLOAT, 0.1, 0.0, 0.4, 0.0);
97
DEFINE_UI_PARAMS(working_colorspace, working space, DCTLUI_COMBO_BOX, 0, {acescct, acescc, acescg}, {acescct, acescc, acescg});
108
DEFINE_UI_PARAMS(invert, invert, DCTLUI_CHECK_BOX, 0);
119

@@ -50,37 +48,12 @@ __DEVICE__ float acescc_to_lin(float in) {
5048
}
5149
}
5250

53-
// calculate compressed distance
54-
__DEVICE__ float compress(float x, float l, float t, float p, bool invert) {
55-
float cdist;
56-
// power(p) compression function plot https://www.desmos.com/calculator/54aytu7hek
57-
float s = (l-t)/_powf(_powf((1.0f-t)/(l-t),-p)-1.0f,1.0f/p); // calc y=1 intersect
58-
if (l < 1.0001f) {
59-
return x; // disable compression, avoid nan
60-
}
61-
if (x < t) {
62-
cdist = x;
63-
}
64-
else {
65-
if (invert == 0) {
66-
cdist = t+s*((x-t)/s)/(_powf(1.0f+_powf((x-t)/s,p),1.0f/p)); // compress
67-
} else {
68-
if (x > (t + s)) {
69-
cdist = x; // avoid singularity
70-
}
71-
cdist = t+s*_powf(-(_powf((x-t)/s,p)/(_powf((x-t)/s,p)-1.0f)),1.0f/p); // uncompress
72-
}
73-
}
74-
return cdist;
75-
}
76-
7751
__DEVICE__ float3 transform(int p_Width, int p_Height, int p_X, int p_Y, float p_R, float p_G, float p_B)
7852
{
7953

80-
// source pixels
8154
float3 rgb = make_float3(p_R, p_G, p_B);
8255

83-
// working colorspace to linear
56+
// Linearize working colorspace
8457
if (working_colorspace == acescct) {
8558
rgb.x = acescct_to_lin(rgb.x);
8659
rgb.y = acescct_to_lin(rgb.y);
@@ -91,69 +64,50 @@ __DEVICE__ float3 transform(int p_Width, int p_Height, int p_X, int p_Y, float p
9164
rgb.z = acescc_to_lin(rgb.z);
9265
}
9366

94-
// thr is the percentage of the core gamut to protect: the complement of threshold.
95-
float3 thr = make_float3(
96-
1.0f-_fmaxf(0.00001, threshold_c),
97-
1.0f-_fmaxf(0.00001, threshold_m),
98-
1.0f-_fmaxf(0.00001, threshold_y));
99-
100-
// lim is the distance beyond the gamut boundary that will be compressed to the gamut boundary.
101-
// lim = 0.2 will compress from a distance of 1.2 from achromatic to 1.0 (the gamut boundary).
102-
float3 lim;
103-
lim = make_float3(cyan+1.0f, magenta+1.0f, yellow+1.0f);
104-
105-
// achromatic axis
106-
float ach = _fmaxf(rgb.x, _fmaxf(rgb.y, rgb.z));
107-
108-
// achromatic shadow rolloff
109-
float ach_shd;
110-
if (shd_rolloff < 0.004f) {
111-
// disable shadow rolloff functionality.
112-
// values below 0.004 cause strange behavior, actually increasing distance in some cases.
113-
// if ach < 0.0 and shd_rolloff is disabled, take absolute value. This preserves negative components after compression.
114-
ach_shd = _fabs(ach);
115-
} else {
116-
// lift ach below threshold using a tanh compression function.
117-
// this reduces large distance values in shadow grain, which can cause differences when inverting.
118-
ach_shd = 1.0f-((1.0f-ach)<(1.0f-shd_rolloff)?(1.0f-ach):(1.0f-shd_rolloff)+shd_rolloff*_tanhf((((1.0f-ach)-(1.0f-shd_rolloff))/shd_rolloff)));
119-
}
67+
// Amount of outer gamut to affect
68+
float3 th = 1.0f-make_float3(th_c, th_m, th_y);
69+
70+
// Distance limit: How far beyond the gamut boundary to compress
71+
float3 dl = 1.0f+make_float3(d_c, d_m, d_y);
12072

121-
// distance from the achromatic axis for each color component aka inverse rgb ratios
122-
// distance is normalized by achromatic, so that 1.0f is at gamut boundary. avoid 0 div
123-
float3 dist;
124-
dist.x = ach_shd == 0.0f ? 0.0f : (ach-rgb.x)/ach_shd;
125-
dist.y = ach_shd == 0.0f ? 0.0f : (ach-rgb.y)/ach_shd;
126-
dist.z = ach_shd == 0.0f ? 0.0f : (ach-rgb.z)/ach_shd;
73+
// Calculate scale so compression function passes through distance limit: (x=dl, y=1)
74+
float3 s;
75+
s.x = (1.0f-th.x)/_sqrtf(_fmaxf(1.001f, dl.x)-1.0f);
76+
s.y = (1.0f-th.y)/_sqrtf(_fmaxf(1.001f, dl.y)-1.0f);
77+
s.z = (1.0f-th.z)/_sqrtf(_fmaxf(1.001f, dl.z)-1.0f);
78+
79+
// Achromatic axis
80+
float ac = _fmaxf(rgb.x, _fmaxf(rgb.y, rgb.z));
12781

128-
// compress distance with user controlled parameterized shaper function
129-
float3 cdist = make_float3(
130-
compress(dist.x, lim.x, thr.x, power, invert),
131-
compress(dist.y, lim.y, thr.y, power, invert),
132-
compress(dist.z, lim.z, thr.z, power, invert));
82+
// Inverse RGB Ratios: distance from achromatic axis
83+
float3 d = ac == 0.0f ? make_float3(0.0f) : (ac-rgb)/_fabs(ac);
13384

134-
// recalculate rgb from compressed distance and achromatic
135-
// effectively this scales each color component relative to achromatic axis by the compressed distance
136-
float3 crgb = make_float3(
137-
ach-cdist.x*ach_shd,
138-
ach-cdist.y*ach_shd,
139-
ach-cdist.z*ach_shd);
85+
float3 cd; // Compressed distance
86+
// Parabolic compression function: https://www.desmos.com/calculator/nvhp63hmtj
87+
if (invert == 0) {
88+
cd.x = d.x<th.x?d.x:s.x*_sqrtf(d.x-th.x+s.x*s.x/4.0f)-s.x*_sqrtf(s.x*s.x/4.0f)+th.x;
89+
cd.y = d.y<th.y?d.y:s.y*_sqrtf(d.y-th.y+s.y*s.y/4.0f)-s.y*_sqrtf(s.y*s.y/4.0f)+th.y;
90+
cd.z = d.z<th.z?d.z:s.z*_sqrtf(d.z-th.z+s.z*s.z/4.0f)-s.z*_sqrtf(s.z*s.z/4.0f)+th.z;
91+
} else {
92+
cd.x = d.x<th.x?d.x:_powf(d.x-th.x+s.x*_sqrtf(s.x*s.x/4.0f),2.0f)/(s.x*s.x)-s.x*s.x/4.0f+th.x;
93+
cd.y = d.y<th.y?d.y:_powf(d.y-th.y+s.y*_sqrtf(s.y*s.y/4.0f),2.0f)/(s.y*s.y)-s.y*s.y/4.0f+th.y;
94+
cd.z = d.z<th.z?d.z:_powf(d.z-th.z+s.z*_sqrtf(s.z*s.z/4.0f),2.0f)/(s.z*s.z)-s.z*s.z/4.0f+th.z;
95+
}
14096

141-
// catch nans just in case
142-
crgb.x = isnan(crgb.x) ? 0.0f : crgb.x;
143-
crgb.y = isnan(crgb.y) ? 0.0f : crgb.y;
144-
crgb.z = isnan(crgb.z) ? 0.0f : crgb.z;
97+
// Inverse RGB Ratios to RGB
98+
rgb = ac-cd*_fabs(ac);
14599

146-
// linear to working colorspace
100+
// Linear to working colorspace
147101
if (working_colorspace == acescct) {
148-
crgb.x = lin_to_acescct(crgb.x);
149-
crgb.y = lin_to_acescct(crgb.y);
150-
crgb.z = lin_to_acescct(crgb.z);
102+
rgb.x = lin_to_acescct(rgb.x);
103+
rgb.y = lin_to_acescct(rgb.y);
104+
rgb.z = lin_to_acescct(rgb.z);
151105
} else if (working_colorspace == acescc) {
152-
crgb.x = lin_to_acescc(crgb.x);
153-
crgb.y = lin_to_acescc(crgb.y);
154-
crgb.z = lin_to_acescc(crgb.z);
106+
rgb.x = lin_to_acescc(rgb.x);
107+
rgb.y = lin_to_acescc(rgb.y);
108+
rgb.z = lin_to_acescc(rgb.z);
155109
}
156110

157-
// write output
158-
return crgb;
111+
// Return output RGB
112+
return rgb;
159113
}

0 commit comments

Comments
 (0)