MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
MotionCurves.cpp
Go to the documentation of this file.
1#include "MotionCurves.hpp"
2
3#ifdef MAYAFLUX_ARCH_X64
4#include <immintrin.h>
5#endif
6#ifdef MAYAFLUX_ARCH_ARM64
7#include <arm_neon.h>
8#endif
9
12
13#include "BasisMatrices.hpp"
14
16#include <execution>
17
18namespace MayaFlux::Kinesis {
19
20namespace {
21
22 /// @brief Samples evaluated per chunk.
23 constexpr Eigen::Index k_chunk_samples = 4096;
24
25 /// @brief Below this sample count the chunk loop runs serially on evaluator scratch.
26 constexpr Eigen::Index k_parallel_min_samples = 8192;
27
28 /// @brief Below this point count the arc length pass runs serially.
29 constexpr Eigen::Index k_parallel_min_points = 8192;
30
31 Eigen::Index compute_num_segments(
32 Eigen::Index num_controls,
33 Eigen::Index points_per_segment,
34 Eigen::Index overlap)
35 {
36 return (overlap == 0)
37 ? num_controls / points_per_segment
38 : (num_controls - overlap) / (points_per_segment - overlap);
39 }
40
41 /**
42 * @brief Copy a column-major Eigen basis into a control-major flat buffer.
43 * @param source pps x pps matrix with one row per power of t, highest first,
44 * and one column per control, as BasisMatrices stores them.
45 * @param pps Points per segment.
46 * @param dst Resized to pps * pps, indexed [q * pps + p], the coefficient of
47 * power p contributed by control q.
48 */
49 void flatten_basis(const double* source, Eigen::Index pps, std::vector<double>& dst)
50 {
51 dst.resize(static_cast<size_t>(pps * pps));
52 for (Eigen::Index q = 0; q < pps; ++q) {
53 for (Eigen::Index p = 0; p < pps; ++p) {
54 dst[static_cast<size_t>(q * pps + p)] = source[p + q * pps];
55 }
56 }
57 }
58
59 /**
60 * @brief Fold the basis into a segment's control block.
61 * @param ctrl Point-major control storage.
62 * @param start_col First control point of the segment.
63 * @param dim Coordinate count.
64 * @param pps Points per segment.
65 * @param basis Row-major pps * pps basis.
66 * @param folded Output, dim * pps, indexed [d * pps + p].
67 *
68 * folded(d,p) = sum over q of control(d,q) * basis(q,p), so the per-sample
69 * cost collapses to one Horner evaluation per coordinate.
70 */
71 void fold_controls(
72 const double* ctrl,
73 Eigen::Index start_col,
74 size_t dim,
75 Eigen::Index pps,
76 const double* basis,
77 double* folded)
78 {
79 const double* block = ctrl + static_cast<size_t>(start_col) * dim;
80
81 for (size_t d = 0; d < dim; ++d) {
82 for (Eigen::Index p = 0; p < pps; ++p) {
83 double sum = 0.0;
84 for (Eigen::Index q = 0; q < pps; ++q) {
85 sum += block[static_cast<size_t>(q) * dim + d]
86 * basis[static_cast<size_t>(q * pps + p)];
87 }
88 folded[d * static_cast<size_t>(pps) + static_cast<size_t>(p)] = sum;
89 }
90 }
91 }
92
93 /**
94 * @brief Fill the local curve parameter for every sample in a chunk.
95 * @param dst Buffer of chunk.sample_count doubles.
96 *
97 * Every sample in a chunk shares the same segment by construction, so the
98 * floor is loop invariant and the parameter is affine in the sample index.
99 * Division by the sample span is kept rather than folded into a reciprocal
100 * multiply so the values match the scalar formulation exactly.
101 */
102 void fill_parameters(
103 double* dst,
104 const CurveChunk& chunk,
105 Eigen::Index num_samples,
106 Eigen::Index num_segments)
107 {
108 const auto count = static_cast<size_t>(chunk.sample_count);
109
110 if (chunk.clamp_t_high) {
111 std::fill_n(dst, count, 1.0);
112 return;
113 }
114
115 const double delta = static_cast<double>(num_segments)
116 / static_cast<double>(num_samples - 1);
117 const double origin = -static_cast<double>(chunk.segment);
118 const auto begin = static_cast<double>(chunk.sample_begin);
119
120 size_t j = 0;
121
122#ifdef MAYAFLUX_ARCH_X64
123 const __m256d v_delta = _mm256_set1_pd(delta);
124 const __m256d v_origin = _mm256_set1_pd(origin);
125 const __m256d v_step = _mm256_set1_pd(4.0);
126 __m256d idx = _mm256_set_pd(begin + 3.0, begin + 2.0, begin + 1.0, begin);
127
128 for (; j + 4 <= count; j += 4) {
129 _mm256_storeu_pd(dst + j, _mm256_fmadd_pd(idx, v_delta, v_origin));
130 idx = _mm256_add_pd(idx, v_step);
131 }
132#elif defined(MAYAFLUX_ARCH_ARM64)
133 const float64x2_t v_delta = vdupq_n_f64(delta);
134 const float64x2_t v_step = vdupq_n_f64(2.0);
135 float64x2_t idx = { begin, begin + 1.0 };
136
137 for (; j + 2 <= count; j += 2) {
138 vst1q_f64(dst + j, vfmaq_f64(vdupq_n_f64(origin), idx, v_delta));
139 idx = vaddq_f64(idx, v_step);
140 }
141#endif
142
143 for (; j < count; ++j) {
144 dst[j] = (begin + static_cast<double>(j)) * delta + origin;
145 }
146
147 if (chunk.sample_begin + chunk.sample_count == num_samples) {
148 dst[count - 1] = 1.0;
149 }
150 }
151
152 /**
153 * @brief Horner evaluation of one chunk for every coordinate.
154 * @tparam Pps Points per segment, giving the polynomial degree Pps - 1.
155 * @param folded dim * Pps folded control block.
156 * @param params chunk parameter buffer.
157 * @param dim Coordinate count.
158 * @param count Sample count.
159 * @param dst First output sample of coordinate 0.
160 * @param stride Distance between coordinate rows in the output.
161 *
162 * The parameter occupies the SIMD lanes and the coefficients broadcast,
163 * so four consecutive samples of one coordinate are produced per vector
164 * and stored contiguously.
165 */
166 template <size_t Pps>
167 void evaluate_rows(
168 const double* folded,
169 const double* params,
170 size_t dim,
171 size_t count,
172 double* dst,
173 size_t stride)
174 {
175 for (size_t d = 0; d < dim; ++d) {
176 const double* co = folded + d * Pps;
177 double* row = dst + d * stride;
178
179 size_t j = 0;
180
181#ifdef MAYAFLUX_ARCH_X64
182 __m256d cv[Pps];
183 for (size_t p = 0; p < Pps; ++p) {
184 cv[p] = _mm256_set1_pd(co[p]);
185 }
186
187 for (; j + 4 <= count; j += 4) {
188 const __m256d tv = _mm256_loadu_pd(params + j);
189 __m256d acc = cv[0];
190 for (size_t p = 1; p < Pps; ++p) {
191 acc = _mm256_fmadd_pd(acc, tv, cv[p]);
192 }
193 _mm256_storeu_pd(row + j, acc);
194 }
195#elif defined(MAYAFLUX_ARCH_ARM64)
196 float64x2_t cv[Pps];
197 for (size_t p = 0; p < Pps; ++p) {
198 cv[p] = vdupq_n_f64(co[p]);
199 }
200
201 for (; j + 2 <= count; j += 2) {
202 const float64x2_t tv = vld1q_f64(params + j);
203 float64x2_t acc = cv[0];
204 for (size_t p = 1; p < Pps; ++p) {
205 acc = vfmaq_f64(cv[p], acc, tv);
206 }
207 vst1q_f64(row + j, acc);
208 }
209#endif
210
211 for (; j < count; ++j) {
212 double acc = co[0];
213 for (size_t p = 1; p < Pps; ++p) {
214 acc = acc * params[j] + co[p];
215 }
216 row[j] = acc;
217 }
218 }
219 }
220
221 void evaluate_chunk_polynomial(
222 const double* folded,
223 const double* params,
224 size_t dim,
225 Eigen::Index pps,
226 size_t count,
227 double* dst,
228 size_t stride)
229 {
230 switch (pps) {
231 case 2:
232 evaluate_rows<2>(folded, params, dim, count, dst, stride);
233 break;
234 case 3:
235 evaluate_rows<3>(folded, params, dim, count, dst, stride);
236 break;
237 case 4:
238 evaluate_rows<4>(folded, params, dim, count, dst, stride);
239 break;
240 default:
241 break;
242 }
243 }
244
245 /**
246 * @brief Cosine blend of a two point segment.
247 *
248 * Not polynomial in the parameter, so it takes the scalar path. AVX2
249 * carries no transcendental, matching the treatment of the trigonometric
250 * maps in Discrete/Transform.
251 */
252 void evaluate_chunk_cosine(
253 const double* ctrl,
254 Eigen::Index start_col,
255 const double* params,
256 size_t dim,
257 size_t count,
258 double* dst,
259 size_t stride)
260 {
261 const double* c0 = ctrl + static_cast<size_t>(start_col) * dim;
262 const double* c1 = c0 + dim;
263
264 for (size_t d = 0; d < dim; ++d) {
265 double* row = dst + d * stride;
266 const double a = c0[d];
267 const double b = c1[d];
268
269 for (size_t j = 0; j < count; ++j) {
270 const double mu = (1.0 - std::cos(params[j] * M_PI)) * 0.5;
271 row[j] = (1.0 - mu) * a + mu * b;
272 }
273 }
274 }
275
276 /**
277 * @brief Derivative of the cosine blend with respect to the parameter.
278 *
279 * mu(t) = (1 - cos(pi t)) / 2, so d/dt of (1 - mu) a + mu b is
280 * (b - a) * pi * sin(pi t) / 2.
281 */
282 void evaluate_chunk_cosine_derivative(
283 const double* ctrl,
284 Eigen::Index start_col,
285 const double* params,
286 size_t dim,
287 size_t count,
288 double* dst,
289 size_t stride)
290 {
291 const double* c0 = ctrl + static_cast<size_t>(start_col) * dim;
292 const double* c1 = c0 + dim;
293
294 for (size_t d = 0; d < dim; ++d) {
295 double* row = dst + d * stride;
296 const double delta = c1[d] - c0[d];
297
298 for (size_t j = 0; j < count; ++j) {
299 row[j] = delta * M_PI * std::sin(params[j] * M_PI) * 0.5;
300 }
301 }
302 }
303
304 /**
305 * @brief Differentiate a folded coefficient block in place of a new one.
306 * @param folded dim * pps coefficients in descending power order.
307 * @param dim Coordinate count.
308 * @param pps Points per segment, giving degree pps - 1.
309 * @param dst dim * (pps - 1) output coefficients, descending power order.
310 *
311 * Horner stores the highest power first, so coefficient p carries power
312 * pps - 1 - p and differentiating is a scale by that power with the
313 * constant term dropped. The result feeds the same evaluate_rows kernel
314 * one degree lower.
315 */
316 void fold_derivative(
317 const double* folded,
318 size_t dim,
319 Eigen::Index pps,
320 double* dst)
321 {
322 const auto n = static_cast<size_t>(pps);
323 for (size_t d = 0; d < dim; ++d) {
324 const double* co = folded + d * n;
325 double* dco = dst + d * (n - 1);
326 for (size_t p = 0; p + 1 < n; ++p) {
327 dco[p] = co[p] * static_cast<double>(n - 1 - p);
328 }
329 }
330 }
331
332} // namespace
333
335 std::span<const double> points,
336 size_t dim,
337 size_t count,
338 std::vector<double>& tangents)
339{
340 if (dim == 0 || count < 2 || points.size() < dim * count) {
341 tangents.assign(dim * count, 0.0);
342 return;
343 }
344
345 tangents.resize(dim * count);
346
347 for (size_t d = 0; d < dim; ++d) {
348 const double* row = points.data() + d * count;
349 double* out = tangents.data() + d * count;
350
351 for (size_t i = 0; i < count; ++i) {
352 const size_t a = i == 0 ? 0 : i - 1;
353 const size_t b = i + 1 < count ? i + 1 : count - 1;
354 out[i] = row[b] - row[a];
355 }
356 }
357}
358
359// ===========================================================================
360// Single-sample entry points
361// ===========================================================================
362
363Eigen::VectorXd catmull_rom_spline(
364 const Eigen::MatrixXd& control_points,
365 double t,
366 double tension)
367{
368 if (control_points.cols() != 4) {
369 error<std::invalid_argument>(
372 std::source_location::current(),
373 "Catmull-Rom interpolation requires 4 control points, but got {}",
374 control_points.cols());
375 }
376
377 Eigen::Matrix4d basis_matrix = BasisMatrices::catmull_rom_with_tension(tension);
378 Eigen::Vector4d t_vector(t * t * t, t * t, t, 1.0);
379 Eigen::Vector4d coeffs = basis_matrix.transpose() * t_vector;
380
381 return control_points * coeffs;
382}
383
384Eigen::VectorXd radial_fan_cubic(
385 const Eigen::MatrixXd& control_points,
386 double t,
387 double tension)
388{
389 if (control_points.cols() != 4) {
390 error<std::invalid_argument>(
393 std::source_location::current(),
394 "Radial fan cubic requires 4 control points, but got {}",
395 control_points.cols());
396 }
397
398 const Eigen::Matrix4d basis_matrix = BasisMatrices::catmull_rom_with_tension(tension);
399 const Eigen::Vector4d powers(t * t * t, t * t, t, 1.0);
400 return control_points * (basis_matrix * powers);
401}
402
403Eigen::VectorXd cubic_bezier(
404 const Eigen::MatrixXd& control_points,
405 double t)
406{
407 if (control_points.cols() != 4) {
408 error<std::invalid_argument>(
411 std::source_location::current(),
412 "Cubic Bezier interpolation requires 4 control points, but got {}",
413 control_points.cols());
414 }
415
416 Eigen::Vector4d t_vector(t * t * t, t * t, t, 1.0);
417 Eigen::Vector4d coeffs = BasisMatrices::CUBIC_BEZIER * t_vector;
418
419 return control_points * coeffs;
420}
421
422Eigen::VectorXd quadratic_bezier(
423 const Eigen::MatrixXd& control_points,
424 double t)
425{
426 if (control_points.cols() != 3) {
427 error<std::invalid_argument>(
430 std::source_location::current(),
431 "Quadratic Bezier interpolation requires 3 control points, but got {}",
432 control_points.cols());
433 }
434
435 Eigen::Vector3d t_vector(t * t, t, 1.0);
436 Eigen::Vector3d coeffs = BasisMatrices::QUADRATIC_BEZIER * t_vector;
437
438 return control_points * coeffs;
439}
440
441Eigen::VectorXd cubic_hermite(
442 const Eigen::MatrixXd& endpoints,
443 const Eigen::MatrixXd& tangents,
444 double t)
445{
446 if (endpoints.cols() != 2 || tangents.cols() != 2) {
447 error<std::invalid_argument>(
450 std::source_location::current(),
451 "Cubic Hermite interpolation requires 2 endpoints and 2 tangents, but got {} endpoints and {} tangents",
452 endpoints.cols(), tangents.cols());
453 }
454
455 double t2 = t * t;
456 double t3 = t2 * t;
457
458 double h00 = 2.0 * t3 - 3.0 * t2 + 1.0;
459 double h10 = t3 - 2.0 * t2 + t;
460 double h01 = -2.0 * t3 + 3.0 * t2;
461 double h11 = t3 - t2;
462
463 return h00 * endpoints.col(0) + h10 * tangents.col(0) + h01 * endpoints.col(1) + h11 * tangents.col(1);
464}
465
466Eigen::VectorXd bspline_cubic(
467 const Eigen::MatrixXd& control_points,
468 double t)
469{
470 if (control_points.cols() != 4) {
471 error<std::invalid_argument>(
474 std::source_location::current(),
475 "Cubic B-spline interpolation requires 4 control points, but got {}",
476 control_points.cols());
477 }
478
479 Eigen::Vector4d t_vector(t * t * t, t * t, t, 1.0);
480 Eigen::Vector4d coeffs = BasisMatrices::BSPLINE_CUBIC.transpose() * t_vector;
481
482 return control_points * coeffs;
483}
484
485Eigen::VectorXd interpolate(
486 const Eigen::MatrixXd& control_points,
487 double t,
489 double tension)
490{
491 switch (mode) {
493 if (control_points.cols() < 2) {
494 error<std::invalid_argument>(
497 std::source_location::current(),
498 "Linear interpolation requires at least 2 points, but got {}",
499 control_points.cols());
500 }
501 return (1.0 - t) * control_points.col(0) + t * control_points.col(1);
502
504 return catmull_rom_spline(control_points, t, tension);
505
507 return radial_fan_cubic(control_points, t, tension);
508
510 Eigen::MatrixXd endpoints = control_points.leftCols(2);
511 Eigen::MatrixXd tangents = control_points.rightCols(2);
512 return cubic_hermite(endpoints, tangents, t);
513 }
514
516 return cubic_bezier(control_points, t);
517
519 return quadratic_bezier(control_points, t);
520
522 return bspline_cubic(control_points, t);
523
525 if (control_points.cols() < 2) {
526 error<std::invalid_argument>(
529 std::source_location::current(),
530 "Cosine interpolation requires at least 2 points, but got {}",
531 control_points.cols());
532 }
533 double mu2 = (1.0 - std::cos(t * M_PI)) * 0.5;
534 return (1.0 - mu2) * control_points.col(0) + mu2 * control_points.col(1);
535 }
536
537 default:
538 error<std::invalid_argument>(
541 std::source_location::current(),
542 "Unsupported interpolation mode: {}",
543 static_cast<int>(mode));
544 }
545}
546
547// ===========================================================================
548// CurveEvaluator
549// ===========================================================================
550
552 : m_mode(mode)
553 , m_tension(tension)
554{
556}
557
559{
560 if (mode == m_mode && tension == m_tension) {
561 return;
562 }
563
564 m_mode = mode;
567}
568
570{
571 m_trigonometric = false;
572
573 switch (m_mode) {
576 m_overlap = 1;
577 m_supports_multi = true;
578 m_basis = { -1.0, 1.0, 1.0, 0.0 };
579 break;
580 }
581
584 m_overlap = 1;
585 m_supports_multi = true;
586 m_trigonometric = true;
587 m_basis.clear();
588 break;
589
592 m_overlap = 3;
593 m_supports_multi = true;
594 const Eigen::Matrix4d m = BasisMatrices::catmull_rom_with_tension(m_tension);
595 flatten_basis(m.data(), 4, m_basis);
596 break;
597 }
598
601 m_overlap = 3;
602 m_supports_multi = true;
603 const Eigen::Matrix4d m = BasisMatrices::catmull_rom_with_tension(m_tension);
604 m_basis.resize(16);
605 for (Eigen::Index q = 0; q < 4; ++q) {
606 for (Eigen::Index p = 0; p < 4; ++p) {
607 m_basis[static_cast<size_t>(q * 4 + p)] = m(q, p);
608 }
609 }
610 break;
611 }
612
615 m_overlap = 3;
616 m_supports_multi = true;
617 flatten_basis(BasisMatrices::BSPLINE_CUBIC.data(), 4, m_basis);
618 break;
619
622 m_overlap = 1;
623 m_supports_multi = true;
624 flatten_basis(BasisMatrices::CUBIC_BEZIER.data(), 4, m_basis);
625 break;
626
629 m_overlap = 1;
630 m_supports_multi = true;
631 flatten_basis(BasisMatrices::QUADRATIC_BEZIER.data(), 3, m_basis);
632 break;
633
636 m_overlap = 0;
637 m_supports_multi = false;
638 m_basis = {
639 2.0, -3.0, 0.0, 1.0,
640 -2.0, 3.0, 0.0, 0.0,
641 1.0, -2.0, 1.0, 0.0,
642 1.0, -1.0, 0.0, 0.0
643 };
644 break;
645
646 default:
648 m_overlap = 0;
649 m_supports_multi = false;
650 m_basis.clear();
651 break;
652 }
653}
654
656 std::span<const double> control_points,
657 size_t dim,
658 Eigen::Index& count)
659{
660 count = static_cast<Eigen::Index>(control_points.size() / dim);
661
662 const bool pads = (m_mode == InterpolationMode::CATMULL_ROM
665
666 if (!pads) {
667 return control_points.data();
668 }
669
670 const auto n = static_cast<size_t>(count);
671 m_extended.resize((n + 2) * dim);
672
673 const double* src = control_points.data();
674 double* dst = m_extended.data();
675
676 std::copy_n(src, dim, dst);
677 std::copy_n(src, n * dim, dst + dim);
678 std::copy_n(src + (n - 1) * dim, dim, dst + (n + 1) * dim);
679
680 count = static_cast<Eigen::Index>(n + 2);
681 return dst;
682}
683
685 Eigen::Index num_samples,
686 Eigen::Index num_segments,
687 Eigen::Index active_count)
688{
689 const auto segments = static_cast<size_t>(num_segments);
690
691 m_seg_first.assign(segments, -1);
692 m_seg_total.assign(segments, 0);
693
694 for (Eigen::Index i = 0; i < num_samples; ++i) {
695 const double t_global = static_cast<double>(i) / static_cast<double>(num_samples - 1);
696 const double segment_float = t_global * static_cast<double>(num_segments);
697 auto seg_idx = static_cast<Eigen::Index>(std::floor(segment_float));
698
699 if (i == num_samples - 1 || seg_idx >= num_segments) {
700 seg_idx = num_segments - 1;
701 }
702
703 const auto s = static_cast<size_t>(seg_idx);
704 if (m_seg_first[s] < 0) {
705 m_seg_first[s] = i;
706 }
707 ++m_seg_total[s];
708 }
709
710 m_chunks.clear();
711
712 for (Eigen::Index seg_idx = 0; seg_idx < num_segments; ++seg_idx) {
713 const auto s = static_cast<size_t>(seg_idx);
714 if (m_seg_total[s] == 0) {
715 continue;
716 }
717
718 Eigen::Index start_col = seg_idx * (m_points_per_segment - m_overlap);
719 const bool clamp = (start_col + m_points_per_segment > active_count);
720 if (clamp) {
721 start_col = active_count - m_points_per_segment;
722 }
723
724 for (Eigen::Index off = 0; off < m_seg_total[s]; off += k_chunk_samples) {
725 m_chunks.push_back({ .segment = seg_idx,
726 .start_col = start_col,
727 .sample_begin = m_seg_first[s] + off,
728 .sample_count = std::min(k_chunk_samples, m_seg_total[s] - off),
729 .clamp_t_high = clamp });
730 }
731 }
732}
733
735 std::span<const double> control_points,
736 size_t dim,
737 Eigen::Index num_samples,
738 std::vector<double>& out,
739 std::vector<double>& tangents)
740{
741 if (num_samples < 2) {
742 error<std::invalid_argument>(
745 std::source_location::current(),
746 "num_samples must be at least 2, but got {}",
747 num_samples);
748 }
749
750 if (dim == 0) {
751 error<std::invalid_argument>(
754 std::source_location::current(),
755 "dim must be at least 1");
756 }
757
758 const auto control_count = static_cast<Eigen::Index>(control_points.size() / dim);
759
760 if (control_count < 2) {
761 error<std::invalid_argument>(
764 std::source_location::current(),
765 "Need at least 2 control points, but got {}",
766 control_count);
767 }
768
769 if (m_points_per_segment == 0) {
770 error<std::invalid_argument>(
773 std::source_location::current(),
774 "Unsupported interpolation mode: {}",
775 static_cast<int>(m_mode));
776 }
777
778 if (!m_supports_multi && control_count != m_points_per_segment) {
779 error<std::invalid_argument>(
782 std::source_location::current(),
783 "{} interpolation requires exactly {} control points, but got {}",
784 static_cast<int>(m_mode), m_points_per_segment, control_count);
785 }
786
787 Eigen::Index active_count = 0;
788 const double* active = extend(control_points, dim, active_count);
789
790 const Eigen::Index num_segments = compute_num_segments(
791 active_count, m_points_per_segment, m_overlap);
792
793 if (num_segments < 1) {
794 error<std::invalid_argument>(
797 std::source_location::current(),
798 "Need sufficient control points for multi-segment {} interpolation, but got {}",
799 static_cast<int>(m_mode), control_count);
800 }
801
802 const auto stride = static_cast<size_t>(num_samples);
803 out.resize(dim * stride);
804 tangents.resize(dim * stride);
805
806 build_chunks(num_samples, num_segments, active_count);
807
808 const Eigen::Index pps = m_points_per_segment;
809 const bool trig = m_trigonometric;
810 const double* basis = m_basis.data();
811 double* out_base = out.data();
812 double* tan_base = tangents.data();
813
814 if (m_chunks.size() > 1 && num_samples >= k_parallel_min_samples) {
815 std::for_each(std::execution::par_unseq, m_chunks.begin(), m_chunks.end(),
816 [&](const CurveChunk& chunk) {
817 const auto count = static_cast<size_t>(chunk.sample_count);
818 std::vector<double> params(count);
819 fill_parameters(params.data(), chunk, num_samples, num_segments);
820
821 const auto offset = static_cast<size_t>(chunk.sample_begin);
822 double* dst = out_base + offset;
823 double* tan = tan_base + offset;
824
825 if (trig) {
826 evaluate_chunk_cosine(active, chunk.start_col, params.data(),
827 dim, count, dst, stride);
828 evaluate_chunk_cosine_derivative(active, chunk.start_col, params.data(),
829 dim, count, tan, stride);
830 return;
831 }
832
833 std::vector<double> folded(dim * static_cast<size_t>(pps));
834 fold_controls(active, chunk.start_col, dim, pps, basis, folded.data());
835
836 evaluate_chunk_polynomial(folded.data(), params.data(),
837 dim, pps, count, dst, stride);
838
839 std::vector<double> dfolded(dim * static_cast<size_t>(pps - 1));
840 fold_derivative(folded.data(), dim, pps, dfolded.data());
841
842 evaluate_chunk_polynomial(dfolded.data(), params.data(),
843 dim, pps - 1, count, tan, stride);
844 });
845
846 return;
847 }
848
849 for (const CurveChunk& chunk : m_chunks) {
850 const auto count = static_cast<size_t>(chunk.sample_count);
851
852 m_tbuf.resize(count);
853 fill_parameters(m_tbuf.data(), chunk, num_samples, num_segments);
854
855 const auto offset = static_cast<size_t>(chunk.sample_begin);
856 double* dst = out_base + offset;
857 double* tan = tan_base + offset;
858
859 if (trig) {
860 evaluate_chunk_cosine(active, chunk.start_col, m_tbuf.data(),
861 dim, count, dst, stride);
862 evaluate_chunk_cosine_derivative(active, chunk.start_col, m_tbuf.data(),
863 dim, count, tan, stride);
864 continue;
865 }
866
867 m_folded.resize(dim * static_cast<size_t>(pps));
868 fold_controls(active, chunk.start_col, dim, pps, basis, m_folded.data());
869
870 evaluate_chunk_polynomial(m_folded.data(), m_tbuf.data(),
871 dim, pps, count, dst, stride);
872
873 m_dfolded.resize(dim * static_cast<size_t>(pps - 1));
874 fold_derivative(m_folded.data(), dim, pps, m_dfolded.data());
875
876 evaluate_chunk_polynomial(m_dfolded.data(), m_tbuf.data(),
877 dim, pps - 1, count, tan, stride);
878 }
879}
880
881void CurveEvaluator::reparameterize_planar(
882 std::span<const double> points,
883 size_t dim,
884 Eigen::Index point_count,
885 Eigen::Index num_samples,
886 std::vector<double>& out)
887{
888 const auto n = static_cast<size_t>(point_count);
889 const auto samples = static_cast<size_t>(num_samples);
890
891 if (point_count < 2 || num_samples < 2) {
892 out.assign(points.begin(), points.end());
893 return;
894 }
895
896 const double* src = points.data();
897
898 m_arc.assign(n, 0.0);
899
900 for (size_t d = 0; d < dim; ++d) {
901 const double* row = src + d * n;
902 for (size_t i = 1; i < n; ++i) {
903 const double delta = row[i] - row[i - 1];
904 m_arc[i] += delta * delta;
905 }
906 }
907
908 for (size_t i = 1; i < n; ++i) {
909 m_arc[i] = std::sqrt(m_arc[i]);
910 }
911
912 std::inclusive_scan(m_arc.data() + 1, m_arc.data() + n, m_arc.data() + 1);
913
914 const double total_length = m_arc[n - 1];
915 if (total_length == 0.0) {
916 out.assign(points.begin(), points.end());
917 return;
918 }
919
920 m_lower.resize(samples);
921 m_frac.resize(samples);
922
923 const double step = total_length / static_cast<double>(num_samples - 1);
924 size_t upper = 1;
925
926 for (size_t i = 0; i < samples; ++i) {
927 const double target = static_cast<double>(i) * step;
928
929 while (upper < n - 1 && m_arc[upper] < target) {
930 ++upper;
931 }
932
933 const size_t lower = upper - 1;
934 const double span = m_arc[upper] - m_arc[lower];
935
936 m_lower[i] = lower;
937 m_frac[i] = (span > 0.0) ? ((target - m_arc[lower]) / span) : 0.0;
938 }
939
940 out.resize(dim * samples);
941
942 for (size_t d = 0; d < dim; ++d) {
943 const double* row = src + d * n;
944 double* dst = out.data() + d * samples;
945
946 for (size_t i = 0; i < samples; ++i) {
947 const size_t lower = m_lower[i];
948 const double t = m_frac[i];
949 dst[i] = (1.0 - t) * row[lower] + t * row[lower + 1];
950 }
951 }
952}
953
954void CurveEvaluator::evaluate(
955 const Eigen::MatrixXd& control_points,
956 Eigen::Index num_samples,
957 Eigen::MatrixXd& out)
958{
959 const auto dim = static_cast<size_t>(control_points.rows());
960
961 evaluate_planar(
962 std::span<const double>(control_points.data(),
963 static_cast<size_t>(control_points.size())),
964 dim, num_samples, m_planar, m_planar_tangents);
965
966 out.resize(control_points.rows(), num_samples);
967
968 const auto samples = static_cast<size_t>(num_samples);
969 double* dst = out.data();
970
971 for (size_t d = 0; d < dim; ++d) {
972 const double* row = m_planar.data() + d * samples;
973 for (size_t i = 0; i < samples; ++i) {
974 dst[d + i * dim] = row[i];
975 }
976 }
977}
978
979void CurveEvaluator::reparameterize(
980 const Eigen::MatrixXd& points,
981 Eigen::Index num_samples,
982 Eigen::MatrixXd& out)
983{
984 const auto dim = static_cast<size_t>(points.rows());
985 const auto n = static_cast<size_t>(points.cols());
986
987 m_planar.resize(dim * n);
988 const double* src = points.data();
989
990 for (size_t d = 0; d < dim; ++d) {
991 double* row = m_planar.data() + d * n;
992 for (size_t i = 0; i < n; ++i) {
993 row[i] = src[d + i * dim];
994 }
995 }
996
997 reparameterize_planar(m_planar, dim, points.cols(), num_samples, m_planar_alt);
998
999 const auto samples = m_planar_alt.size() / std::max<size_t>(dim, 1);
1000 out.resize(points.rows(), static_cast<Eigen::Index>(samples));
1001
1002 double* dst = out.data();
1003 for (size_t d = 0; d < dim; ++d) {
1004 const double* row = m_planar_alt.data() + d * samples;
1005 for (size_t i = 0; i < samples; ++i) {
1006 dst[d + i * dim] = row[i];
1007 }
1008 }
1009}
1010
1011// ===========================================================================
1012// Free functions
1013// ===========================================================================
1014
1016 const Eigen::MatrixXd& control_points,
1017 Eigen::Index num_samples,
1019 double tension)
1020{
1021 CurveEvaluator evaluator(mode, tension);
1022 Eigen::MatrixXd result;
1023 evaluator.evaluate(control_points, num_samples, result);
1024 return result;
1025}
1026
1027double compute_arc_length(const Eigen::MatrixXd& points)
1028{
1029 if (points.cols() < 2) {
1030 return 0.0;
1031 }
1032
1033 double length = 0.0;
1034 for (Eigen::Index i = 1; i < points.cols(); ++i) {
1035 length += (points.col(i) - points.col(i - 1)).norm();
1036 }
1037
1038 return length;
1039}
1040
1041Eigen::VectorXd compute_arc_length_table(const Eigen::MatrixXd& points)
1042{
1043 const Eigen::Index n = points.cols();
1044
1045 Eigen::VectorXd arc_lengths(n);
1046 if (n == 0) {
1047 return arc_lengths;
1048 }
1049
1050 arc_lengths(0) = 0.0;
1051 if (n == 1) {
1052 return arc_lengths;
1053 }
1054
1055 if (n >= k_parallel_min_points) {
1056 std::for_each(std::execution::par_unseq,
1057 std::views::iota(Eigen::Index { 1 }, n).begin(),
1058 std::views::iota(Eigen::Index { 1 }, n).end(),
1059 [&](Eigen::Index i) {
1060 arc_lengths(i) = (points.col(i) - points.col(i - 1)).norm();
1061 });
1062 } else {
1063 for (Eigen::Index i = 1; i < n; ++i) {
1064 arc_lengths(i) = (points.col(i) - points.col(i - 1)).norm();
1065 }
1066 }
1067
1068 std::inclusive_scan(
1069 arc_lengths.data() + 1,
1070 arc_lengths.data() + n,
1071 arc_lengths.data() + 1);
1072
1073 return arc_lengths;
1074}
1075
1077 const Eigen::MatrixXd& points,
1078 Eigen::Index num_samples)
1079{
1080 CurveEvaluator evaluator;
1081 Eigen::MatrixXd result;
1082 evaluator.reparameterize(points, num_samples, result);
1083 return result;
1084}
1085
1087 const Kakshya::DataVariant& control_points,
1088 Eigen::Index num_samples,
1090 double tension)
1091{
1092 Eigen::MatrixXd control_matrix = Kakshya::to_eigen_matrix(control_points);
1093
1094 Eigen::MatrixXd interpolated = generate_interpolated_points(
1095 control_matrix, num_samples, mode, tension);
1096
1097 Kakshya::EigenAccess input_access(control_points);
1098
1099 if (input_access.is_complex()) {
1101 }
1102
1103 if (input_access.is_structured()) {
1104 switch (input_access.component_count()) {
1105 case 2:
1107 case 3:
1109 case 4:
1111 default:
1113 }
1114 } else {
1116 }
1117}
1118
1119} // namespace MayaFlux::Kinesis
uint64_t length
Definition Composite.cpp:11
uint64_t offset
Definition Composite.cpp:10
std::vector< glm::vec2 > * points
uint32_t * dst
const uint32_t * src
uint32_t mode
size_t a
size_t b
double q
std::vector< float > * out
size_t count
bool is_complex() const
Check if data contains complex numbers.
bool is_structured() const
Check if data contains GLM types.
size_t component_count() const
Get number of components per element (rows in matrix representation)
Type-erased accessor for converting DataVariant to Eigen types.
void evaluate(const Eigen::MatrixXd &control_points, Eigen::Index num_samples, Eigen::MatrixXd &out)
Evaluate a curve into a caller-owned matrix.
InterpolationMode mode() const
Currently configured mode.
std::vector< Eigen::Index > m_seg_total
double tension() const
Currently configured tension.
const double * extend(std::span< const double > control_points, size_t dim, Eigen::Index &count)
void reparameterize(const Eigen::MatrixXd &points, Eigen::Index num_samples, Eigen::MatrixXd &out)
Resample a polyline to uniform arc length into a caller-owned matrix.
void build_chunks(Eigen::Index num_samples, Eigen::Index num_segments, Eigen::Index active_count)
std::vector< CurveChunk > m_chunks
void evaluate_planar(std::span< const double > control_points, size_t dim, Eigen::Index num_samples, std::vector< double > &out, std::vector< double > &tangents)
Evaluate a curve into coordinate-major position and tangent buffers.
std::vector< Eigen::Index > m_seg_first
CurveEvaluator(InterpolationMode mode=InterpolationMode::CATMULL_ROM, double tension=0.5)
Construct and resolve the evaluation kernel.
void configure(InterpolationMode mode, double tension)
Re-resolve the kernel.
Reusable interpolation state for callers evaluating many curves.
@ Runtime
General runtime operations (default fallback)
@ Kinesis
General mathematical and physics algorithns.
Eigen::MatrixXd to_eigen_matrix(const Kakshya::DataVariant &variant)
Convenience function for direct conversion.
Kakshya::DataVariant from_eigen_matrix(const Eigen::MatrixXd &matrix, MatrixInterpretation interpretation=MatrixInterpretation::AUTO)
Convenience function for direct conversion.
std::variant< std::vector< double >, std::vector< float >, std::vector< uint8_t >, std::vector< uint16_t >, std::vector< uint32_t >, std::vector< std::complex< float > >, std::vector< std::complex< double > >, std::vector< glm::vec2 >, std::vector< glm::vec3 >, std::vector< glm::vec4 >, std::vector< glm::mat4 > > DataVariant
Multi-type data storage for different precision needs.
Definition NDData.hpp:70
@ SCALAR
Single row → scalar values.
@ COMPLEX
2 rows → complex (row 0 = real, row 1 = imag)
std::vector< double > sum(std::span< const double > data, size_t n_windows, uint32_t hop_size, uint32_t window_size)
Sum per window.
Definition Analysis.cpp:469
double compute_arc_length(const Eigen::MatrixXd &points)
Compute arc length of curve using trapezoidal rule.
InterpolationMode
Mathematical interpolation methods.
Eigen::VectorXd bspline_cubic(const Eigen::MatrixXd &control_points, double t)
Uniform B-spline interpolation using Eigen matrices.
Eigen::VectorXd cubic_bezier(const Eigen::MatrixXd &control_points, double t)
Cubic Bezier interpolation using Eigen matrices.
Eigen::VectorXd quadratic_bezier(const Eigen::MatrixXd &control_points, double t)
Quadratic Bezier interpolation using Eigen matrices.
void central_difference_tangents(std::span< const double > points, size_t dim, size_t count, std::vector< double > &tangents)
Tangents of a sampled curve by finite difference, coordinate-major.
Eigen::VectorXd catmull_rom_spline(const Eigen::MatrixXd &control_points, double t, double tension)
Catmull-Rom spline interpolation using Eigen matrices.
Eigen::MatrixXd generate_interpolated_points(const Eigen::MatrixXd &control_points, Eigen::Index num_samples, InterpolationMode mode, double tension)
Generate interpolated points from control points.
Eigen::VectorXd compute_arc_length_table(const Eigen::MatrixXd &points)
Compute arc length parameterization table.
Kakshya::DataVariant interpolate_nddata(const Kakshya::DataVariant &control_points, Eigen::Index num_samples, InterpolationMode mode, double tension)
Process DataVariant through interpolation.
std::vector< Kakshya::LineVertex > reparameterize_by_arc_length(const std::vector< Kakshya::LineVertex > &path_vertices, size_t num_samples)
Resample path vertices for arc-length parameterization.
Eigen::VectorXd cubic_hermite(const Eigen::MatrixXd &endpoints, const Eigen::MatrixXd &tangents, double t)
Cubic Hermite interpolation using Eigen matrices.
Eigen::VectorXd interpolate(const Eigen::MatrixXd &control_points, double t, InterpolationMode mode, double tension)
Generic interpolation dispatcher.
Eigen::VectorXd radial_fan_cubic(const Eigen::MatrixXd &control_points, double t, double tension)
Origin-oriented cubic sweep from tension * (p0 - p1) to p3.
static const Eigen::Matrix4d BSPLINE_CUBIC
static const Eigen::Matrix3d QUADRATIC_BEZIER
static Eigen::Matrix4d catmull_rom_with_tension(double tension)
static const Eigen::Matrix4d CUBIC_BEZIER
Eigen::Index start_col
First control point of the owning segment.
A contiguous run of samples evaluated in one pass.