#ifndef PROC_NODES_BLENDER_COLORBAND_H
#define PROC_NODES_BLENDER_COLORBAND_H
// BLENDER'S COLOUR BAND, PORTED FROM BLENDER'S SOURCE.
//
// `BKE_colorband_evaluate` and `colorband_hue_interp` from
// `source/blender/blenkernel/intern/colorband.cc`, and
// `key_curve_position_weights` from `source/blender/blenkernel/intern/key.cc`,
// Blender source commit 61a36e08, BLENDER_VERSION 502. The enums are
// COLBAND_* from DNA_colorband_types.h.
//
// This is what the Color Ramp node does. Ours returned Color(f, f, f, 1) --
// a grey ramp -- because the stops were not carried across at all, and a grey
// gradient is a plausible-looking wrong answer rather than a visible failure.
//
// FIVE THINGS IN HERE THAT DO NOT FOLLOW FROM WHAT A RAMP IS:
//
// * `fac` is measured from the RIGHT stop. cbd1 is the first stop with
// pos > in, fac = (in - cbd1->pos) / (cbd2->pos - cbd1->pos), and the
// result is mfac * cbd1 + fac * cbd2. That is ordinary interpolation
// written backwards; writing it forwards gives a MIRRORED ramp that still
// looks like a ramp.
// * LINEAR, EASE and CONSTANT return the end stop outside the stop range.
// B_SPLINE and CARDINAL do not, because their interpolation begins before
// the first stop and ends after the last; instead the end stop is COPIED
// with its position forced to 1.0 (and the first to 0.0) so there is
// something to interpolate against.
// * the two spline modes CLAMP their result to [0,1]. The linear ones do not.
// * coincident stop positions divide by zero, and the fallback is fac = 0
// except past the last stop, where it is 1. The asymmetry is a fix for
// Blender #26732, so it is load-bearing rather than arbitrary.
// * HSV and HSL FORCE the interpolation to LINEAR. The dropdown still shows
// whatever it was set to and is silently ignored.
//
// Blender's ipotype and color_mode live in one struct but are not independent:
// read `ipotype` through the mode, never directly.
#include "blender_ramp_blend.h" // rgb_to_hsv / hsl and their inverses
#include <cmath>
namespace godot {
namespace bcoba {
// DNA_colorband_types.h
enum {
COLBAND_BLEND_RGB = 0,
COLBAND_BLEND_HSV = 1,
COLBAND_BLEND_HSL = 2,
};
enum {
COLBAND_INTERP_LINEAR = 0,
COLBAND_INTERP_EASE = 1,
COLBAND_INTERP_B_SPLINE = 2,
COLBAND_INTERP_CARDINAL = 3,
COLBAND_INTERP_CONSTANT = 4,
};
enum {
COLBAND_HUE_NEAR = 0,
COLBAND_HUE_FAR = 1,
COLBAND_HUE_CW = 2,
COLBAND_HUE_CCW = 3,
};
// MAXCOLORBAND, DNA_colorband_types.h. Blender's own hard limit on stops.
#define BCOBA_MAX_STOPS 32
struct Stop {
float pos = 0.0f;
float r = 0.0f, g = 0.0f, b = 0.0f, a = 1.0f;
};
struct ColorBand {
int tot = 0;
int ipotype = COLBAND_INTERP_LINEAR;
int ipotype_hue = COLBAND_HUE_NEAR;
int color_mode = COLBAND_BLEND_RGB;
Stop data[BCOBA_MAX_STOPS];
};
// DNA_key_types.h. Only the two a colour band can reach are implemented.
//
// KEY_LINEAR and KEY_CATMULL_ROM are also in Blender's switch and are NOT
// reachable from here. CATMULL_ROM is worth naming because it is the SAME
// polynomial as CARDINAL with fc = 0.5 -- so reaching for the textbook
// Catmull-Rom tension silently gives a different curve from Blender's
// CARDINAL, whose fc is 0.71.
enum {
KEY_CARDINAL = 1,
KEY_BSPLINE = 2,
};
static inline void key_curve_position_weights(float t, float data[4], int type) {
if (type == KEY_CARDINAL) {
const float t2 = t * t;
const float t3 = t2 * t;
const float fc = 0.71f;
data[0] = -fc * t3 + 2.0f * fc * t2 - fc * t;
data[1] = (2.0f - fc) * t3 + (fc - 3.0f) * t2 + 1.0f;
data[2] = (fc - 2.0f) * t3 + (3.0f - 2.0f * fc) * t2 + fc * t;
data[3] = fc * t3 - fc * t2;
} else { // KEY_BSPLINE
const float t2 = t * t;
const float t3 = t2 * t;
data[0] = -0.16666666f * t3 + 0.5f * t2 - 0.5f * t + 0.16666666f;
data[1] = 0.5f * t3 - t2 + 0.66666666f;
data[2] = -0.5f * t3 + 0.5f * t2 + 0.5f * t + 0.16666666f;
data[3] = 0.16666666f * t3;
}
}
// Four winding rules over the hue circle. `mfac` and `fac` are the weights of
// h1 and h2 respectively, already computed by the caller.
static inline float colorband_hue_interp(
const int ipotype_hue, const float mfac, const float fac, float h1, float h2) {
#define HUE_INTERP(h_a, h_b) ((mfac * (h_a)) + (fac * (h_b)))
#define HUE_MOD(h) (((h) < 1.0f) ? (h) : (h) - 1.0f)
h1 = HUE_MOD(h1);
h2 = HUE_MOD(h2);
int mode = 0;
switch (ipotype_hue) {
case COLBAND_HUE_NEAR: {
if ((h1 < h2) && (h2 - h1) > +0.5f) {
mode = 1;
} else if ((h1 > h2) && (h2 - h1) < -0.5f) {
mode = 2;
} else {
mode = 0;
}
break;
}
case COLBAND_HUE_FAR: {
// Full loop in hue space when both stops are the same hue.
if (h1 == h2) {
mode = 1;
} else if ((h1 < h2) && (h2 - h1) < +0.5f) {
mode = 1;
} else if ((h1 > h2) && (h2 - h1) > -0.5f) {
mode = 2;
} else {
mode = 0;
}
break;
}
case COLBAND_HUE_CCW: {
mode = (h1 > h2) ? 2 : 0;
break;
}
case COLBAND_HUE_CW: {
mode = (h1 < h2) ? 1 : 0;
break;
}
}
float h_interp = 0.0f;
switch (mode) {
case 0:
h_interp = HUE_INTERP(h1, h2);
break;
case 1:
h_interp = HUE_INTERP(h1 + 1.0f, h2);
h_interp = HUE_MOD(h_interp);
break;
case 2:
h_interp = HUE_INTERP(h1, h2 + 1.0f);
h_interp = HUE_MOD(h_interp);
break;
}
#undef HUE_INTERP
#undef HUE_MOD
return h_interp;
}
// `out` is r, g, b, a. Returns false only for an empty band, exactly as
// Blender's does -- the caller decides what an empty band means.
//
// Written with indices rather than Blender's walking pointers, because two of
// its branches point cbd1 one PAST the last stop and cbd2 at a stack copy, and
// the neighbour lookups cbd1 + 1 / cbd2 - 1 are in bounds only because of the
// `a >= tot - 1` and `a < 2` guards. Same reads, spelled so that is checkable.
static inline bool colorband_evaluate(const ColorBand &coba, float in, float out[4]) {
if (coba.tot == 0) {
return false;
}
// HSV and HSL ignore the interpolation setting.
const int ipotype = (coba.color_mode == COLBAND_BLEND_RGB)
? coba.ipotype
: COLBAND_INTERP_LINEAR;
const bool ipo_is_linearish = (ipotype == COLBAND_INTERP_LINEAR ||
ipotype == COLBAND_INTERP_EASE || ipotype == COLBAND_INTERP_CONSTANT);
auto emit = [&out](const Stop &s) {
out[0] = s.r;
out[1] = s.g;
out[2] = s.b;
out[3] = s.a;
};
if (coba.tot == 1) {
emit(coba.data[0]);
return true;
}
if (in <= coba.data[0].pos && ipo_is_linearish) {
// Before the first stop.
emit(coba.data[0]);
return true;
}
// First stop with pos > in, or tot if there is none.
int a = 0;
while (a < coba.tot && !(coba.data[a].pos > in)) {
a++;
}
Stop right, left;
const Stop *cbd1; // Blender's cbd1: the stop at index a
const Stop *cbd2; // Blender's cbd2: the stop at index a - 1
if (a == coba.tot) {
cbd2 = &coba.data[a - 1];
right = *cbd2;
right.pos = 1.0f;
cbd1 = &right;
} else if (a == 0) {
cbd1 = &coba.data[0];
left = *cbd1;
left.pos = 0.0f;
cbd2 = &left;
} else {
cbd1 = &coba.data[a];
cbd2 = &coba.data[a - 1];
}
if (a == coba.tot && ipo_is_linearish) {
// After the last stop.
emit(*cbd2);
return true;
}
if (ipotype == COLBAND_INTERP_CONSTANT) {
emit(*cbd2);
return true;
}
float fac;
if (cbd2->pos != cbd1->pos) {
fac = (in - cbd1->pos) / (cbd2->pos - cbd1->pos);
} else {
// Blender #26732: 0 everywhere except past the last stop.
fac = (a != coba.tot) ? 0.0f : 1.0f;
}
if (ipotype == COLBAND_INTERP_B_SPLINE || ipotype == COLBAND_INTERP_CARDINAL) {
// Interpolate from right to left: 3 2 1 0.
const Stop *cbd0 = (a >= coba.tot - 1) ? cbd1 : &coba.data[a + 1];
const Stop *cbd3 = (a < 2) ? cbd2 : &coba.data[a - 2];
fac = CLAMP(fac, 0.0f, 1.0f);
float t[4];
key_curve_position_weights(
fac, t, (ipotype == COLBAND_INTERP_CARDINAL) ? KEY_CARDINAL : KEY_BSPLINE);
out[0] = t[3] * cbd3->r + t[2] * cbd2->r + t[1] * cbd1->r + t[0] * cbd0->r;
out[1] = t[3] * cbd3->g + t[2] * cbd2->g + t[1] * cbd1->g + t[0] * cbd0->g;
out[2] = t[3] * cbd3->b + t[2] * cbd2->b + t[1] * cbd1->b + t[0] * cbd0->b;
out[3] = t[3] * cbd3->a + t[2] * cbd2->a + t[1] * cbd1->a + t[0] * cbd0->a;
for (int i = 0; i < 4; i++) {
out[i] = CLAMP(out[i], 0.0f, 1.0f);
}
return true;
}
if (ipotype == COLBAND_INTERP_EASE) {
const float fac2 = fac * fac;
fac = 3.0f * fac2 - 2.0f * fac2 * fac;
}
const float mfac = 1.0f - fac;
if (coba.color_mode == COLBAND_BLEND_HSV) {
float col1[3], col2[3];
bramp::rgb_to_hsv(cbd1->r, cbd1->g, cbd1->b, col1[0], col1[1], col1[2]);
bramp::rgb_to_hsv(cbd2->r, cbd2->g, cbd2->b, col2[0], col2[1], col2[2]);
out[0] = colorband_hue_interp(coba.ipotype_hue, mfac, fac, col1[0], col2[0]);
out[1] = mfac * col1[1] + fac * col2[1];
out[2] = mfac * col1[2] + fac * col2[2];
out[3] = mfac * cbd1->a + fac * cbd2->a;
// Blender's hsv_to_rgb_v(out, out). h, s and v are taken by value, so
// writing r, g, b over them is safe -- and the alpha set above is not
// touched.
bramp::hsv_to_rgb(out[0], out[1], out[2], out[0], out[1], out[2]);
} else if (coba.color_mode == COLBAND_BLEND_HSL) {
float col1[3], col2[3];
bramp::rgb_to_hsl(cbd1->r, cbd1->g, cbd1->b, col1[0], col1[1], col1[2]);
bramp::rgb_to_hsl(cbd2->r, cbd2->g, cbd2->b, col2[0], col2[1], col2[2]);
out[0] = colorband_hue_interp(coba.ipotype_hue, mfac, fac, col1[0], col2[0]);
out[1] = mfac * col1[1] + fac * col2[1];
out[2] = mfac * col1[2] + fac * col2[2];
out[3] = mfac * cbd1->a + fac * cbd2->a;
bramp::hsl_to_rgb(out[0], out[1], out[2], out[0], out[1], out[2]);
} else { // COLBAND_BLEND_RGB
out[0] = mfac * cbd1->r + fac * cbd2->r;
out[1] = mfac * cbd1->g + fac * cbd2->g;
out[2] = mfac * cbd1->b + fac * cbd2->b;
out[3] = mfac * cbd1->a + fac * cbd2->a;
}
return true;
}
} // namespace bcoba
} // namespace godot
#endif // PROC_NODES_BLENDER_COLORBAND_H