#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