OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes / src / core / proc_curves.cpp

246 lines · cpp ref:
#include "proc_curves.h"

#include <cmath>

namespace godot {

void ProcCurves::_bind_methods() {
	ClassDB::bind_method(D_METHOD("get_point_count"), &ProcCurves::point_count);
	ClassDB::bind_method(D_METHOD("get_curve_count"), &ProcCurves::curve_count);
	ClassDB::bind_method(D_METHOD("evaluate_position", "curve", "t"), &ProcCurves::evaluate_position);
	ClassDB::bind_method(D_METHOD("evaluate_tangent", "curve", "t"), &ProcCurves::evaluate_tangent);
	ClassDB::bind_method(D_METHOD("approximate_length", "curve", "samples"), &ProcCurves::approximate_length, DEFVAL(32));
}

int ProcCurves::curve_count() const {
	if (curve_offsets.size() > 1) {
		return curve_offsets.size() - 1;
	}
	return 0;
}

int ProcCurves::domain_size(AttrDomain p_domain) const {
	switch (p_domain) {
		case AttrDomain::Point: return point_count();
		case AttrDomain::Curve: return curve_count();
		default: return 0;
	}
}

Ref<ProcCurves> ProcCurves::duplicate_curves() const {
	Ref<ProcCurves> dup;
	dup.instantiate();
	dup->positions = positions;
	dup->radii = radii;
	dup->tilts = tilts;
	dup->curve_offsets = curve_offsets;
	dup->curve_types = curve_types;
	dup->point_attrs.propagate_from(point_attrs);
	dup->curve_attrs.propagate_from(curve_attrs);
	return dup;
}

// Points spaced evenly along the curve BY LENGTH, which is what Blender's
// Resample Curve and Curve to Points do. Parameter-uniform sampling bunches up
// wherever the control points are close together; on a bezier that is a visible
// difference and on a line it is none at all, which is how it went unnoticed.
//
// The length table is built by dense parameter sampling rather than
// analytically: a bezier's arc length has no closed form, and Blender's own
// resampler walks an evaluated polyline too.
PackedVector3Array ProcCurves::sample_uniform(int p_curve, int p_count) const {
	PackedVector3Array out;
	if (p_count <= 0 || p_curve < 0 || p_curve >= curve_count()) {
		return out;
	}
	if (p_count == 1) {
		out.push_back(evaluate_position(p_curve, 0.0f));
		return out;
	}

	const int start = curve_offsets[p_curve];
	const int n = curve_offsets[p_curve + 1] - start;
	// Enough samples that the polyline approximates the curve; for a Poly curve
	// the segments are exact at any resolution above its own point count.
	const int steps = MAX(n * 64, 4096);
	const bool cyclic = is_cyclic(p_curve);

	// A CYCLIC curve's polyline closes back to its first point, and its samples
	// span [0, total) rather than [0, total] -- N points around a loop, with no
	// repeat of the start.
	PackedVector3Array pts;
	PackedFloat32Array cumulative;
	pts.resize(steps + 1);
	cumulative.resize(steps + 1);
	float total = 0.0f;
	for (int i = 0; i <= steps; i++) {
		if (cyclic && i == steps) {
			pts.ptrw()[i] = pts[0];
		} else {
			pts.ptrw()[i] = cyclic
					? evaluate_position_cyclic(p_curve, (float)i / (float)steps)
					: evaluate_position(p_curve, (float)i / (float)steps);
		}
		if (i > 0) {
			total += pts[i].distance_to(pts[i - 1]);
		}
		cumulative.ptrw()[i] = total;
	}

	if (total <= 0.0f) {
		for (int i = 0; i < p_count; i++) {
			out.push_back(pts[0]);
		}
		return out;
	}

	int walk = 0;
	for (int i = 0; i < p_count; i++) {
		const float target = cyclic ? (total * (float)i / (float)p_count)
									: (total * (float)i / (float)(p_count - 1));
		while (walk < steps && cumulative[walk + 1] < target) {
			walk++;
		}
		const float seg_len = cumulative[walk + 1] - cumulative[walk];
		const float f = seg_len > 0.0f ? (target - cumulative[walk]) / seg_len : 0.0f;
		out.push_back(pts[walk].lerp(pts[MIN(walk + 1, steps)], f));
	}
	return out;
}

Vector3 ProcCurves::evaluate_position(int p_curve, float p_t) const {
	if (p_curve < 0 || p_curve >= curve_count()) {
		return Vector3();
	}

	int start = curve_offsets[p_curve];
	int end = curve_offsets[p_curve + 1];
	int n = end - start;

	if (n == 0) {
		return Vector3();
	}
	if (n == 1) {
		return positions[start];
	}

	p_t = CLAMP(p_t, 0.0f, 1.0f);

	// Determine curve type (default to Poly if out of range)
	CurveType type = CurveType::Poly;
	if (p_curve < curve_types.size()) {
		type = static_cast<CurveType>(curve_types[p_curve]);
	}

	if (type == CurveType::Bezier) {
		// Bezier: every 4 consecutive points form a segment [p0, h0, h1, p1, ...]
		int n_segs = MAX((n - 1) / 3, 1);
		float seg_f = p_t * n_segs;
		int seg = CLAMP((int)seg_f, 0, n_segs - 1);
		float lt = seg_f - seg;

		int base = seg * 3;
		Vector3 p0 = positions[start + base];
		Vector3 p1 = positions[start + MIN(base + 1, n - 1)];
		Vector3 p2 = positions[start + MIN(base + 2, n - 1)];
		Vector3 p3 = positions[start + MIN(base + 3, n - 1)];

		float u = 1.0f - lt;
		return u * u * u * p0 + 3.0f * u * u * lt * p1 + 3.0f * u * lt * lt * p2 + lt * lt * lt * p3;
	}

	// For Poly and CatmullRom: map t to segment.
	//
	// CLAMP THE SEGMENT BEFORE TAKING THE LOCAL t, not after. At t = 1 the
	// segment index lands one past the last, and the old order computed
	// local_t against the UNCLAMPED index -- giving 0 -- and then clamped. On a
	// two-point line that returned lerp(start, end, 0), which is the START:
	// asking for the end of a curve gave you its beginning. Every curve
	// sampled inclusively at t = 1 was wrong, and the last point of every
	// Curve to Points was the first.
	float seg_f = p_t * (n - 1);
	int seg = CLAMP((int)seg_f, 0, n - 2);
	float local_t = seg_f - seg;

	if (type == CurveType::CatmullRom) {
		int i0 = MAX(seg - 1, 0);
		int i1 = seg;
		int i2 = MIN(seg + 1, n - 1);
		int i3 = MIN(seg + 2, n - 1);

		Vector3 a = positions[start + i0];
		Vector3 b = positions[start + i1];
		Vector3 c = positions[start + i2];
		Vector3 d = positions[start + i3];

		float t2 = local_t * local_t;
		float t3 = t2 * local_t;

		return 0.5f * (a * (-t3 + 2.0f * t2 - local_t) +
				b * (3.0f * t3 - 5.0f * t2 + 2.0f) +
				c * (-3.0f * t3 + 4.0f * t2 + local_t) +
				d * (t3 - t2));
	}

	// Poly (linear interpolation)
	Vector3 p0 = positions[start + seg];
	Vector3 p1 = positions[start + seg + 1];
	return p0.lerp(p1, local_t);
}

Vector3 ProcCurves::evaluate_position_cyclic(int p_curve, float p_t) const {
	// A cyclic poly curve has n segments rather than n - 1, the last running
	// from the final point back to the first.
	const int start = curve_offsets[p_curve];
	const int n = curve_offsets[p_curve + 1] - start;
	if (n < 2) {
		return evaluate_position(p_curve, p_t);
	}
	const float seg_f = CLAMP(p_t, 0.0f, 1.0f) * (float)n;
	int seg = CLAMP((int)seg_f, 0, n - 1);
	const float local_t = seg_f - (float)seg;
	const Vector3 &p0 = positions[start + seg];
	const Vector3 &p1 = positions[start + ((seg + 1) % n)];
	return p0.lerp(p1, local_t);
}

Vector3 ProcCurves::evaluate_tangent(int p_curve, float p_t) const {
	const float h = 0.001f;
	float t0 = MAX(p_t - h, 0.0f);
	float t1 = MIN(p_t + h, 1.0f);

	if (t0 == t1) {
		return Vector3(1, 0, 0); // Degenerate case
	}

	Vector3 p0 = evaluate_position(p_curve, t0);
	Vector3 p1 = evaluate_position(p_curve, t1);
	Vector3 diff = p1 - p0;

	if (diff.length_squared() < 1e-12f) {
		return Vector3(1, 0, 0);
	}

	return diff.normalized();
}

float ProcCurves::approximate_length(int p_curve, int p_samples) const {
	if (p_curve < 0 || p_curve >= curve_count() || p_samples < 2) {
		return 0.0f;
	}

	float total = 0.0f;
	Vector3 prev = evaluate_position(p_curve, 0.0f);

	for (int i = 1; i <= p_samples; i++) {
		float t = static_cast<float>(i) / p_samples;
		Vector3 curr = evaluate_position(p_curve, t);
		total += prev.distance_to(curr);
		prev = curr;
	}

	return total;
}

} // namespace godot