OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes / src / shared / blender_colorband.h

328 lines · c ref:
#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