OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes / src / shared / blender_curvemapping.h

558 lines · c ref:
#ifndef PROC_NODES_BLENDER_CURVEMAPPING_H
#define PROC_NODES_BLENDER_CURVEMAPPING_H

// BLENDER'S CURVE MAPPING, PORTED FROM BLENDER'S SOURCE.
//
// `curvemap_make_table`, `calchandle_curvemap`, `curvemap_calc_extend`,
// `BKE_curvemap_evaluateF`, `BKE_curvemapping_evaluateF` and
// `BKE_curvemapping_evaluateRGBF` from
// `source/blender/blenkernel/intern/colortools.cc`, with
// `BKE_curve_correct_bezpart` and `BKE_curve_forward_diff_bezier` from
// `intern/curve.cc`. Blender source commit 61a36e08, BLENDER_VERSION 502.
// Constants are CM_* from DNA_color_types.h and HD_* from DNA_curve_enums.h.
//
// This is what Float Curve and RGB Curves do. Ours were PASS-THROUGHS -- the
// control points were not carried across at all, so a curve authored in Blender
// arrived as the identity and every graph using one silently lost its shaping.
//
// A CURVE IS NOT EVALUATED FROM ITS CONTROL POINTS. Blender fits bezier handles,
// samples the bezier at CM_RESOL points per segment, then RESAMPLES that
// polyline into a table of CM_TABLE + 1 = 257 entries evenly spaced in x, and
// every lookup is a linear interpolation between two table entries. So the
// evaluated curve is a piecewise-linear approximation on a fixed grid, and
// matching Blender means reproducing the TABLE, not the mathematical curve.
// Evaluating the bezier directly is smoother and wrong.
//
// FIVE THINGS THAT DO NOT FOLLOW FROM "IT IS A BEZIER":
//
//   * auto handles use a magic 2.5614 in `calchandle_curvemap`. It is not a
//     third, not a half, and not derived from anything in the file.
//   * the FIRST and LAST auto handles are then corrected AGAIN, to point at
//     their neighbour's handle rather than at the neighbouring knot -- and only
//     when there are more than two points.
//   * `correct_bezpart` shortens BOTH handles when they overlap in x, so a
//     steep segment silently changes shape. Without it the resample reads a
//     non-monotonic polyline and the curve folds back on itself.
//   * outside the point range the result is NOT clamped, it is EXTRAPOLATED
//     along a unit-length copy of the end handle -- unless extend is
//     HORIZONTAL. And a zero-length x component there means a slope of
//     10000 rather than a division by zero.
//   * `BKE_curvemapping_evaluateF` clamps to clipr, but `evaluateRGBF` does
//     NOT -- so Float Curve clips its output and RGB Curves does not, from the
//     same struct and the same flag.
//
// WRAPPING IS NOT IMPLEMENTED, deliberately. `CUMA_USE_WRAPPING` is set only by
// Hue Correct in the compositor and the sequencer (verified: nothing else in
// Blender's tree sets it), so neither Float Curve nor RGB Curves can reach it.
// A partial port of it would be a branch nothing here can test.

#include <godot_cpp/variant/utility_functions.hpp>

#include <cmath>

namespace godot {
namespace bcuma {

// DNA_color_types.h
#define BCUMA_RESOL 32 // CM_RESOL -- bezier samples per segment
#define BCUMA_TABLE 256 // CM_TABLE -- table has TABLE + 1 entries
#define BCUMA_TABLEDIV (1.0f / 256.0f)
#define BCUMA_MAX_CURVES 4 // CM_TOT
#define BCUMA_MAX_POINTS 64 // not Blender's; ours, so the table is stack-sized

// CurveMapping::flag
enum {
	CUMA_DO_CLIP = (1 << 0),
	CUMA_EXTEND_EXTRAPOLATE = (1 << 4),
};

// DNA_curve_enums.h. The CurveMapPoint FLAGS that select them are different
// numbers again -- CUMA_HANDLE_VECTOR is (1 << 1) = 2 and CUMA_HANDLE_AUTO_ANIM
// is (1 << 2) = 4 -- and Blender's RNA reports those flag values for
// handle_type (AUTO 0, VECTOR 2, AUTO_CLAMPED 4), measured on 5.2. So the
// bridge sends the FLAG and this converts it.
enum {
	HD_FREE = 0,
	HD_AUTO = 1,
	HD_VECT = 2,
	HD_AUTO_ANIM = 4,
};

// CurveMapPoint::flag
enum {
	CUMA_HANDLE_VECTOR = (1 << 1),
	CUMA_HANDLE_AUTO_ANIM = (1 << 2),
};

static inline int handle_from_point_flag(int p_flag) {
	if (p_flag & CUMA_HANDLE_VECTOR) {
		return HD_VECT;
	}
	if (p_flag & CUMA_HANDLE_AUTO_ANIM) {
		return HD_AUTO_ANIM;
	}
	return HD_AUTO;
}

struct CurvePoint {
	float x = 0.0f, y = 0.0f;
	int handle = HD_AUTO;
};

// Blender's BezTriple, reduced to what a curve map uses: vec[0] is the left
// handle, vec[1] the knot, vec[2] the right handle, each xyz. z stays 0 and is
// carried only because `curvemap_make_table`'s end-handle correction uses the
// 3-component length functions.
struct BezTriple {
	float vec[3][3] = {};
	int h1 = HD_AUTO, h2 = HD_AUTO;
};

struct CurveMap {
	CurvePoint points[BCUMA_MAX_POINTS];
	int totpoint = 0;

	// Built by make_table.
	float mintable = 0.0f, maxtable = 1.0f;
	float range = 1.0f;
	float ext_in[2] = { 0.0f, 0.0f };
	float ext_out[2] = { 0.0f, 0.0f };
	float table_x[BCUMA_TABLE + 1] = {};
	float table_y[BCUMA_TABLE + 1] = {};
	bool built = false;
};

struct CurveMapping {
	int flag = CUMA_DO_CLIP | CUMA_EXTEND_EXTRAPOLATE;
	float clip_xmin = 0.0f, clip_ymin = 0.0f, clip_xmax = 1.0f, clip_ymax = 1.0f;
	CurveMap cm[BCUMA_MAX_CURVES];
	int totcurve = 1;
};

// --- BKE_curve_correct_bezpart, intern/curve.cc -----------------------------
//
// Shortens both handles proportionally when they cross over in x. Only the x
// components decide, and both handles move.
static inline void correct_bezpart(const float v1[2], float v2[2], float v3[2], const float v4[2]) {
	float h1[2], h2[2];
	h1[0] = v1[0] - v2[0];
	h1[1] = v1[1] - v2[1];
	h2[0] = v4[0] - v3[0];
	h2[1] = v4[1] - v3[1];

	const float len = v4[0] - v1[0];
	const float len1 = fabsf(h1[0]);
	const float len2 = fabsf(h2[0]);

	if ((len1 + len2) == 0.0f) {
		return;
	}

	if ((len1 + len2) > len) {
		const float fac = len / (len1 + len2);
		v2[0] = (v1[0] - fac * h1[0]);
		v2[1] = (v1[1] - fac * h1[1]);
		v3[0] = (v4[0] - fac * h2[0]);
		v3[1] = (v4[1] - fac * h2[1]);
	}
}

// --- BKE_curve_forward_diff_bezier, intern/curve.cc -------------------------
//
// Forward differencing rather than evaluating the cubic per sample: it
// accumulates, so it is NOT the same rounding as a direct evaluation.
static inline void forward_diff_bezier(
		float q0, float q1, float q2, float q3, float *p, int it, int stride_floats) {
	float f = (float)it;
	const float rt0 = q0;
	const float rt1 = 3.0f * (q1 - q0) / f;
	f *= f;
	const float rt2 = 3.0f * (q0 - 2.0f * q1 + q2) / f;
	f *= it;
	const float rt3 = (q3 - q0 + 3.0f * (q1 - q2)) / f;

	q0 = rt0;
	q1 = rt1 + rt2 + rt3;
	q2 = 2.0f * rt2 + 6.0f * rt3;
	q3 = 6.0f * rt3;

	for (int a = 0; a <= it; a++) {
		*p = q0;
		p += stride_floats;
		q0 += q1;
		q1 += q2;
		q2 += q3;
	}
}

// --- calchandle_curvemap, intern/colortools.cc ------------------------------
static inline void calchandle_curvemap(
		BezTriple *bezt, const BezTriple *prev, const BezTriple *next) {
	if (bezt->h1 == 0 && bezt->h2 == 0) {
		return;
	}

	float *p2 = bezt->vec[1];
	float *p2_h1 = bezt->vec[0];
	float *p2_h2 = bezt->vec[2];

	float pt[3] = { 0.0f, 0.0f, 0.0f };
	const float *p1;
	const float *p3;

	if (prev == nullptr) {
		p3 = next->vec[1];
		pt[0] = 2.0f * p2[0] - p3[0];
		pt[1] = 2.0f * p2[1] - p3[1];
		p1 = pt;
	} else {
		p1 = prev->vec[1];
	}

	if (next == nullptr) {
		p1 = prev->vec[1];
		pt[0] = 2.0f * p2[0] - p1[0];
		pt[1] = 2.0f * p2[1] - p1[1];
		p3 = pt;
	} else {
		p3 = next->vec[1];
	}

	float dvec_a[2] = { p2[0] - p1[0], p2[1] - p1[1] };
	float dvec_b[2] = { p3[0] - p2[0], p3[1] - p2[1] };

	float len_a = sqrtf(dvec_a[0] * dvec_a[0] + dvec_a[1] * dvec_a[1]);
	float len_b = sqrtf(dvec_b[0] * dvec_b[0] + dvec_b[1] * dvec_b[1]);
	if (len_a == 0.0f) {
		len_a = 1.0f;
	}
	if (len_b == 0.0f) {
		len_b = 1.0f;
	}

	const bool h1_auto = (bezt->h1 == HD_AUTO || bezt->h1 == HD_AUTO_ANIM);
	const bool h2_auto = (bezt->h2 == HD_AUTO || bezt->h2 == HD_AUTO_ANIM);

	if (h1_auto || h2_auto) {
		float tvec[2];
		tvec[0] = dvec_b[0] / len_b + dvec_a[0] / len_a;
		tvec[1] = dvec_b[1] / len_b + dvec_a[1] / len_a;

		// 2.5614 is Blender's, and is not derived from anything in the file.
		const float len = sqrtf(tvec[0] * tvec[0] + tvec[1] * tvec[1]) * 2.5614f;
		if (len != 0.0f) {
			if (h1_auto) {
				const float la = len_a / len;
				p2_h1[0] = p2[0] + tvec[0] * -la;
				p2_h1[1] = p2[1] + tvec[1] * -la;

				if (bezt->h1 == HD_AUTO_ANIM && next && prev) {
					// Keep the handle horizontal at an extremum, so an
					// auto-clamped curve cannot overshoot past its own points.
					const float ydiff1 = prev->vec[1][1] - bezt->vec[1][1];
					const float ydiff2 = next->vec[1][1] - bezt->vec[1][1];
					if ((ydiff1 <= 0.0f && ydiff2 <= 0.0f) ||
							(ydiff1 >= 0.0f && ydiff2 >= 0.0f)) {
						bezt->vec[0][1] = bezt->vec[1][1];
					} else if (ydiff1 <= 0.0f) {
						bezt->vec[0][1] = MAX(prev->vec[1][1], bezt->vec[0][1]);
					} else {
						bezt->vec[0][1] = MIN(prev->vec[1][1], bezt->vec[0][1]);
					}
				}
			}
			if (h2_auto) {
				const float lb = len_b / len;
				p2_h2[0] = p2[0] + tvec[0] * lb;
				p2_h2[1] = p2[1] + tvec[1] * lb;

				if (bezt->h2 == HD_AUTO_ANIM && next && prev) {
					const float ydiff1 = prev->vec[1][1] - bezt->vec[1][1];
					const float ydiff2 = next->vec[1][1] - bezt->vec[1][1];
					if ((ydiff1 <= 0.0f && ydiff2 <= 0.0f) ||
							(ydiff1 >= 0.0f && ydiff2 >= 0.0f)) {
						bezt->vec[2][1] = bezt->vec[1][1];
					} else if (ydiff1 <= 0.0f) {
						bezt->vec[2][1] = MIN(next->vec[1][1], bezt->vec[2][1]);
					} else {
						bezt->vec[2][1] = MAX(next->vec[1][1], bezt->vec[2][1]);
					}
				}
			}
		}
	}

	if (bezt->h1 == HD_VECT) {
		p2_h1[0] = p2[0] + dvec_a[0] * (-1.0f / 3.0f);
		p2_h1[1] = p2[1] + dvec_a[1] * (-1.0f / 3.0f);
	}
	if (bezt->h2 == HD_VECT) {
		p2_h2[0] = p2[0] + dvec_b[0] * (1.0f / 3.0f);
		p2_h2[1] = p2[1] + dvec_b[1] * (1.0f / 3.0f);
	}
}

// --- curvemap_calc_extend, intern/colortools.cc -----------------------------
//
// `first` and `last` are the two ENDS OF THE SAMPLED POLYLINE, not the control
// points, and ext_in / ext_out are unit-length copies of the end handles.
static inline float curvemap_calc_extend(const CurveMapping &cumap, const CurveMap &cuma,
		float x, const float first[2], const float last[2]) {
	if (x <= first[0]) {
		if ((cumap.flag & CUMA_EXTEND_EXTRAPOLATE) == 0) {
			return first[1]; // horizontal
		}
		if (cuma.ext_in[0] == 0.0f) {
			// A vertical end handle. Blender picks a slope of 10000 rather
			// than dividing by zero.
			return first[1] + cuma.ext_in[1] * 10000.0f;
		}
		return first[1] + cuma.ext_in[1] * (x - first[0]) / cuma.ext_in[0];
	}
	if (x >= last[0]) {
		if ((cumap.flag & CUMA_EXTEND_EXTRAPOLATE) == 0) {
			return last[1];
		}
		if (cuma.ext_out[0] == 0.0f) {
			return last[1] - cuma.ext_out[1] * 10000.0f;
		}
		return last[1] + cuma.ext_out[1] * (x - last[0]) / cuma.ext_out[0];
	}
	return 0.0f;
}

// --- curvemap_make_table, intern/colortools.cc ------------------------------
static inline void curvemap_make_table(const CurveMapping &cumap, CurveMap &cuma) {
	cuma.mintable = cumap.clip_xmin;
	cuma.maxtable = cumap.clip_xmax;

	// Blender has no point limit; this port sizes the bezier buffer on the
	// stack, so it has one. Clamping and saying so beats returning with an
	// unbuilt table, which would evaluate to a silent zero everywhere.
	int bezt_totpoint = MAX(cuma.totpoint, 2);
	if (bezt_totpoint > BCUMA_MAX_POINTS) {
		UtilityFunctions::push_warning(
				"CurveMapping: ", cuma.totpoint, " control points exceeds the ",
				BCUMA_MAX_POINTS, " this port allocates; using the first ",
				BCUMA_MAX_POINTS, " and the curve will differ from Blender's.");
		bezt_totpoint = BCUMA_MAX_POINTS;
	}

	BezTriple bezt[BCUMA_MAX_POINTS];

	if (cuma.totpoint >= 2) {
		for (int a = 0; a < bezt_totpoint; a++) {
			cuma.mintable = MIN(cuma.mintable, cuma.points[a].x);
			cuma.maxtable = MAX(cuma.maxtable, cuma.points[a].x);
			bezt[a].vec[1][0] = cuma.points[a].x;
			bezt[a].vec[1][1] = cuma.points[a].y;
			bezt[a].h1 = bezt[a].h2 = cuma.points[a].handle;
		}
	} else {
		// Blender's fallback when points are missing: a degenerate curve at the
		// origin rather than a refusal.
		cuma.mintable = 0.0f;
		cuma.maxtable = 0.0f;
		bezt[0].h1 = bezt[0].h2 = HD_AUTO;
		bezt[1].h1 = bezt[1].h2 = HD_AUTO;
	}

	const float table_range = cuma.maxtable - cuma.mintable;

	// Handles. Wrapping would give the ends real neighbours; without it the
	// first and last see a null and mirror their one neighbour instead.
	const BezTriple *bezt_prev = nullptr;
	for (int a = 0; a < bezt_totpoint; a++) {
		const BezTriple *bezt_next = (a != bezt_totpoint - 1) ? &bezt[a + 1] : nullptr;
		calchandle_curvemap(&bezt[a], bezt_prev, bezt_next);
		bezt_prev = &bezt[a];
	}

	// THE END HANDLES ARE THEN CORRECTED AGAIN, and only above two points: an
	// auto end handle is re-aimed at its NEIGHBOUR'S HANDLE rather than at the
	// neighbouring knot, keeping its original length.
	if (bezt_totpoint > 2) {
		if (bezt[0].h2 == HD_AUTO) {
			const float dx = bezt[0].vec[2][0] - bezt[0].vec[1][0];
			const float dy = bezt[0].vec[2][1] - bezt[0].vec[1][1];
			const float dz = bezt[0].vec[2][2] - bezt[0].vec[1][2];
			const float hlen = sqrtf(dx * dx + dy * dy + dz * dz);

			float vec[3] = { bezt[1].vec[0][0], bezt[1].vec[0][1], bezt[1].vec[0][2] };
			vec[0] = MAX(vec[0], bezt[0].vec[1][0]);
			vec[0] -= bezt[0].vec[1][0];
			vec[1] -= bezt[0].vec[1][1];
			vec[2] -= bezt[0].vec[1][2];

			const float nlen = sqrtf(vec[0] * vec[0] + vec[1] * vec[1] + vec[2] * vec[2]);
			if (nlen > 1.192092896e-07f) { // FLT_EPSILON
				const float s = hlen / nlen;
				vec[0] *= s;
				vec[1] *= s;
				vec[2] *= s;
				for (int i = 0; i < 3; i++) {
					bezt[0].vec[2][i] = vec[i] + bezt[0].vec[1][i];
					bezt[0].vec[0][i] = bezt[0].vec[1][i] - vec[i];
				}
			}
		}
		const int a = bezt_totpoint - 1;
		if (bezt[a].h2 == HD_AUTO) {
			const float dx = bezt[a].vec[0][0] - bezt[a].vec[1][0];
			const float dy = bezt[a].vec[0][1] - bezt[a].vec[1][1];
			const float dz = bezt[a].vec[0][2] - bezt[a].vec[1][2];
			const float hlen = sqrtf(dx * dx + dy * dy + dz * dz);

			float vec[3] = { bezt[a - 1].vec[2][0], bezt[a - 1].vec[2][1], bezt[a - 1].vec[2][2] };
			vec[0] = MIN(vec[0], bezt[a].vec[1][0]);
			vec[0] -= bezt[a].vec[1][0];
			vec[1] -= bezt[a].vec[1][1];
			vec[2] -= bezt[a].vec[1][2];

			const float nlen = sqrtf(vec[0] * vec[0] + vec[1] * vec[1] + vec[2] * vec[2]);
			if (nlen > 1.192092896e-07f) {
				const float s = hlen / nlen;
				vec[0] *= s;
				vec[1] *= s;
				vec[2] *= s;
				for (int i = 0; i < 3; i++) {
					bezt[a].vec[0][i] = vec[i] + bezt[a].vec[1][i];
					bezt[a].vec[2][i] = bezt[a].vec[1][i] - vec[i];
				}
			}
		}
	}

	// Sample the bezier. Each segment writes BCUMA_RESOL points INCLUDING both
	// ends, so a shared knot appears twice; the resample below walks past the
	// duplicate rather than being confused by it.
	const int totpoint = (bezt_totpoint - 1) * BCUMA_RESOL;
	static const int max_points = (BCUMA_MAX_POINTS - 1) * BCUMA_RESOL;
	float allpoints[max_points * 2];

	float *point = allpoints;
	for (int a = 0; a < bezt_totpoint - 1; a++, point += 2 * BCUMA_RESOL) {
		const int b = a + 1;
		// correct_bezpart MUTATES the two inner handles before sampling.
		correct_bezpart(bezt[a].vec[1], bezt[a].vec[2], bezt[b].vec[0], bezt[b].vec[1]);
		forward_diff_bezier(bezt[a].vec[1][0], bezt[a].vec[2][0], bezt[b].vec[0][0],
				bezt[b].vec[1][0], point, BCUMA_RESOL - 1, 2);
		forward_diff_bezier(bezt[a].vec[1][1], bezt[a].vec[2][1], bezt[b].vec[0][1],
				bezt[b].vec[1][1], point + 1, BCUMA_RESOL - 1, 2);
	}

	// Unit-length copies of the end handles, for extrapolation.
	cuma.ext_in[0] = bezt[0].vec[0][0] - bezt[0].vec[1][0];
	cuma.ext_in[1] = bezt[0].vec[0][1] - bezt[0].vec[1][1];
	float ext_in_range = sqrtf(cuma.ext_in[0] * cuma.ext_in[0] + cuma.ext_in[1] * cuma.ext_in[1]);
	cuma.ext_in[0] /= ext_in_range;
	cuma.ext_in[1] /= ext_in_range;

	const int out_a = bezt_totpoint - 1;
	cuma.ext_out[0] = bezt[out_a].vec[1][0] - bezt[out_a].vec[2][0];
	cuma.ext_out[1] = bezt[out_a].vec[1][1] - bezt[out_a].vec[2][1];
	float ext_out_range = sqrtf(
			cuma.ext_out[0] * cuma.ext_out[0] + cuma.ext_out[1] * cuma.ext_out[1]);
	cuma.ext_out[0] /= ext_out_range;
	cuma.ext_out[1] /= ext_out_range;

	const float range = BCUMA_TABLEDIV * table_range;
	cuma.range = 1.0f / range;

	// Resample the polyline onto CM_TABLE + 1 evenly spaced x.
	const float *firstpoint = allpoints;
	const float *lastpoint = allpoints + 2 * (totpoint - 1);
	point = allpoints;

	for (int a = 0; a <= BCUMA_TABLE; a++) {
		const float cur_x = cuma.mintable + range * (float)a;
		cuma.table_x[a] = cur_x;

		while (cur_x >= point[0] && point != lastpoint) {
			point += 2;
		}

		if (point == firstpoint || (point == lastpoint && cur_x >= point[0])) {
			// compare_ff with 1e-6: ON the end point, take its value directly
			// rather than extrapolating, which an extreme slope would ruin.
			if (fabsf(cur_x - point[0]) <= 1e-6f) {
				cuma.table_y[a] = point[1];
			} else {
				cuma.table_y[a] = curvemap_calc_extend(cumap, cuma, cur_x, firstpoint, lastpoint);
			}
		} else {
			float fac1 = point[0] - point[-2];
			const float fac2 = point[0] - cur_x;
			if (fac1 > 1.192092896e-07f) {
				fac1 = fac2 / fac1;
			} else {
				fac1 = 0.0f;
			}
			cuma.table_y[a] = fac1 * point[-1] + (1.0f - fac1) * point[1];
		}
	}

	cuma.built = true;
}

static inline void curvemapping_init(CurveMapping &cumap) {
	for (int i = 0; i < cumap.totcurve && i < BCUMA_MAX_CURVES; i++) {
		if (!cumap.cm[i].built) {
			curvemap_make_table(cumap, cumap.cm[i]);
		}
	}
}

// --- BKE_curvemap_evaluateF -------------------------------------------------
static inline float curvemap_evaluateF(const CurveMapping &cumap, const CurveMap &cuma, float value) {
	float fi = (value - cuma.mintable) * cuma.range;
	int i = (int)fi;

	if (fi < 0.0f || fi > (float)BCUMA_TABLE) {
		const float first[2] = { cuma.table_x[0], cuma.table_y[0] };
		const float last[2] = { cuma.table_x[BCUMA_TABLE], cuma.table_y[BCUMA_TABLE] };
		return curvemap_calc_extend(cumap, cuma, value, first, last);
	}
	if (i < 0) {
		return cuma.table_y[0];
	}
	if (i >= BCUMA_TABLE) {
		return cuma.table_y[BCUMA_TABLE];
	}
	fi = fi - (float)i;
	return (1.0f - fi) * cuma.table_y[i] + fi * cuma.table_y[i + 1];
}

// --- BKE_curvemapping_evaluateF: the clipped one ----------------------------
//
// Float Curve uses this. RGB Curves does NOT -- see below.
static inline float curvemapping_evaluateF(const CurveMapping &cumap, int cur, float value) {
	float val = curvemap_evaluateF(cumap, cumap.cm[cur], value);
	if (cumap.flag & CUMA_DO_CLIP) {
		if (val < cumap.clip_ymin) {
			val = cumap.clip_ymin;
		} else if (val > cumap.clip_ymax) {
			val = cumap.clip_ymax;
		}
	}
	return val;
}

// --- BKE_curvemapping_evaluateRGBF ------------------------------------------
//
// THE COMBINED CURVE RUNS FIRST. cm[3] is applied to each channel, and its
// result is then fed through that channel's own curve -- not the other way
// round, and not in parallel. And this path uses the UNCLIPPED evaluator, so
// RGB Curves does not clamp where Float Curve does.
static inline void curvemapping_evaluateRGBF(const CurveMapping &cumap, float out[3], const float in[3]) {
	for (int c = 0; c < 3; c++) {
		out[c] = curvemap_evaluateF(cumap, cumap.cm[c],
				curvemap_evaluateF(cumap, cumap.cm[3], in[c]));
	}
}

} // namespace bcuma
} // namespace godot

#endif // PROC_NODES_BLENDER_CURVEMAPPING_H