MayaFlux 0.5.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
17
18namespace P = MayaFlux::Parallel;
19
20namespace MayaFlux::Kinesis {
21
22namespace {
23
24 /// @brief Samples evaluated per chunk.
25 constexpr Eigen::Index k_chunk_samples = 4096;
26
27 /// @brief Below this sample count the chunk loop runs serially on evaluator scratch.
28 constexpr Eigen::Index k_parallel_min_samples = 8192;
29
30 /// @brief Below this point count the arc length pass runs serially.
31 constexpr Eigen::Index k_parallel_min_points = 8192;
32
33 Eigen::Index compute_num_segments(
34 Eigen::Index num_controls,
35 Eigen::Index points_per_segment,
36 Eigen::Index overlap)
37 {
38 return (overlap == 0)
39 ? num_controls / points_per_segment
40 : (num_controls - overlap) / (points_per_segment - overlap);
41 }
42
43 /**
44 * @brief Copy a column-major Eigen basis into a row-major flat buffer.
45 * @param source pps x pps matrix, weight of control q at row q.
46 * @param pps Points per segment.
47 * @param dst Resized to pps * pps, indexed [q * pps + p].
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[q + p * 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} // namespace
277
278// ===========================================================================
279// Single-sample entry points
280// ===========================================================================
281
282Eigen::VectorXd catmull_rom_spline(
283 const Eigen::MatrixXd& control_points,
284 double t,
285 double tension)
286{
287 if (control_points.cols() != 4) {
288 error<std::invalid_argument>(
291 std::source_location::current(),
292 "Catmull-Rom interpolation requires 4 control points, but got {}",
293 control_points.cols());
294 }
295
296 Eigen::Matrix4d basis_matrix = BasisMatrices::catmull_rom_with_tension(tension);
297 Eigen::Vector4d t_vector(t * t * t, t * t, t, 1.0);
298 Eigen::Vector4d coeffs = basis_matrix * t_vector;
299
300 return control_points * coeffs;
301}
302
303Eigen::VectorXd cubic_bezier(
304 const Eigen::MatrixXd& control_points,
305 double t)
306{
307 if (control_points.cols() != 4) {
308 error<std::invalid_argument>(
311 std::source_location::current(),
312 "Cubic Bezier interpolation requires 4 control points, but got {}",
313 control_points.cols());
314 }
315
316 Eigen::Vector4d t_vector(t * t * t, t * t, t, 1.0);
317 Eigen::Vector4d coeffs = BasisMatrices::CUBIC_BEZIER * t_vector;
318
319 return control_points * coeffs;
320}
321
322Eigen::VectorXd quadratic_bezier(
323 const Eigen::MatrixXd& control_points,
324 double t)
325{
326 if (control_points.cols() != 3) {
327 error<std::invalid_argument>(
330 std::source_location::current(),
331 "Quadratic Bezier interpolation requires 3 control points, but got {}",
332 control_points.cols());
333 }
334
335 Eigen::Vector3d t_vector(t * t, t, 1.0);
336 Eigen::Vector3d coeffs = BasisMatrices::QUADRATIC_BEZIER * t_vector;
337
338 return control_points * coeffs;
339}
340
341Eigen::VectorXd cubic_hermite(
342 const Eigen::MatrixXd& endpoints,
343 const Eigen::MatrixXd& tangents,
344 double t)
345{
346 if (endpoints.cols() != 2 || tangents.cols() != 2) {
347 error<std::invalid_argument>(
350 std::source_location::current(),
351 "Cubic Hermite interpolation requires 2 endpoints and 2 tangents, but got {} endpoints and {} tangents",
352 endpoints.cols(), tangents.cols());
353 }
354
355 double t2 = t * t;
356 double t3 = t2 * t;
357
358 double h00 = 2.0 * t3 - 3.0 * t2 + 1.0;
359 double h10 = t3 - 2.0 * t2 + t;
360 double h01 = -2.0 * t3 + 3.0 * t2;
361 double h11 = t3 - t2;
362
363 return h00 * endpoints.col(0) + h10 * tangents.col(0) + h01 * endpoints.col(1) + h11 * tangents.col(1);
364}
365
366Eigen::VectorXd bspline_cubic(
367 const Eigen::MatrixXd& control_points,
368 double t)
369{
370 if (control_points.cols() != 4) {
371 error<std::invalid_argument>(
374 std::source_location::current(),
375 "Cubic B-spline interpolation requires 4 control points, but got {}",
376 control_points.cols());
377 }
378
379 Eigen::Vector4d t_vector(t * t * t, t * t, t, 1.0);
380 Eigen::Vector4d coeffs = BasisMatrices::BSPLINE_CUBIC * t_vector;
381
382 return control_points * coeffs;
383}
384
385Eigen::VectorXd interpolate(
386 const Eigen::MatrixXd& control_points,
387 double t,
389 double tension)
390{
391 switch (mode) {
393 if (control_points.cols() < 2) {
394 error<std::invalid_argument>(
397 std::source_location::current(),
398 "Linear interpolation requires at least 2 points, but got {}",
399 control_points.cols());
400 }
401 return (1.0 - t) * control_points.col(0) + t * control_points.col(1);
402
404 return catmull_rom_spline(control_points, t, tension);
405
407 Eigen::MatrixXd endpoints = control_points.leftCols(2);
408 Eigen::MatrixXd tangents = control_points.rightCols(2);
409 return cubic_hermite(endpoints, tangents, t);
410 }
411
413 return cubic_bezier(control_points, t);
414
416 return quadratic_bezier(control_points, t);
417
419 return bspline_cubic(control_points, t);
420
422 if (control_points.cols() < 2) {
423 error<std::invalid_argument>(
426 std::source_location::current(),
427 "Cosine interpolation requires at least 2 points, but got {}",
428 control_points.cols());
429 }
430 double mu2 = (1.0 - std::cos(t * M_PI)) * 0.5;
431 return (1.0 - mu2) * control_points.col(0) + mu2 * control_points.col(1);
432 }
433
434 default:
435 error<std::invalid_argument>(
438 std::source_location::current(),
439 "Unsupported interpolation mode: {}",
440 static_cast<int>(mode));
441 }
442}
443
444// ===========================================================================
445// CurveEvaluator
446// ===========================================================================
447
449 : m_mode(mode)
450 , m_tension(tension)
451{
453}
454
456{
457 if (mode == m_mode && tension == m_tension) {
458 return;
459 }
460
461 m_mode = mode;
464}
465
467{
468 m_trigonometric = false;
469
470 switch (m_mode) {
473 m_overlap = 1;
474 m_supports_multi = true;
475 m_basis = { -1.0, 1.0, 1.0, 0.0 };
476 break;
477 }
478
481 m_overlap = 1;
482 m_supports_multi = true;
483 m_trigonometric = true;
484 m_basis.clear();
485 break;
486
489 m_overlap = 3;
490 m_supports_multi = true;
491 const Eigen::Matrix4d m = BasisMatrices::catmull_rom_with_tension(m_tension);
492 flatten_basis(m.data(), 4, m_basis);
493 break;
494 }
495
498 m_overlap = 3;
499 m_supports_multi = true;
500 flatten_basis(BasisMatrices::BSPLINE_CUBIC.data(), 4, m_basis);
501 break;
502
505 m_overlap = 1;
506 m_supports_multi = true;
507 flatten_basis(BasisMatrices::CUBIC_BEZIER.data(), 4, m_basis);
508 break;
509
512 m_overlap = 1;
513 m_supports_multi = true;
514 flatten_basis(BasisMatrices::QUADRATIC_BEZIER.data(), 3, m_basis);
515 break;
516
519 m_overlap = 0;
520 m_supports_multi = false;
521 m_basis = {
522 2.0, -3.0, 0.0, 1.0,
523 -2.0, 3.0, 0.0, 0.0,
524 1.0, -2.0, 1.0, 0.0,
525 1.0, -1.0, 0.0, 0.0
526 };
527 break;
528
529 default:
531 m_overlap = 0;
532 m_supports_multi = false;
533 m_basis.clear();
534 break;
535 }
536}
537
539 std::span<const double> control_points,
540 size_t dim,
541 Eigen::Index& count)
542{
543 count = static_cast<Eigen::Index>(control_points.size() / dim);
544
545 const bool pads = (m_mode == InterpolationMode::CATMULL_ROM
548
549 if (!pads) {
550 return control_points.data();
551 }
552
553 const auto n = static_cast<size_t>(count);
554 m_extended.resize((n + 2) * dim);
555
556 const double* src = control_points.data();
557 double* dst = m_extended.data();
558
559 std::copy_n(src, dim, dst);
560 std::copy_n(src, n * dim, dst + dim);
561 std::copy_n(src + (n - 1) * dim, dim, dst + (n + 1) * dim);
562
563 count = static_cast<Eigen::Index>(n + 2);
564 return dst;
565}
566
568 Eigen::Index num_samples,
569 Eigen::Index num_segments,
570 Eigen::Index active_count)
571{
572 const auto segments = static_cast<size_t>(num_segments);
573
574 m_seg_first.assign(segments, -1);
575 m_seg_total.assign(segments, 0);
576
577 for (Eigen::Index i = 0; i < num_samples; ++i) {
578 const double t_global = static_cast<double>(i) / static_cast<double>(num_samples - 1);
579 const double segment_float = t_global * static_cast<double>(num_segments);
580 auto seg_idx = static_cast<Eigen::Index>(std::floor(segment_float));
581
582 if (i == num_samples - 1 || seg_idx >= num_segments) {
583 seg_idx = num_segments - 1;
584 }
585
586 const auto s = static_cast<size_t>(seg_idx);
587 if (m_seg_first[s] < 0) {
588 m_seg_first[s] = i;
589 }
590 ++m_seg_total[s];
591 }
592
593 m_chunks.clear();
594
595 for (Eigen::Index seg_idx = 0; seg_idx < num_segments; ++seg_idx) {
596 const auto s = static_cast<size_t>(seg_idx);
597 if (m_seg_total[s] == 0) {
598 continue;
599 }
600
601 Eigen::Index start_col = seg_idx * (m_points_per_segment - m_overlap);
602 const bool clamp = (start_col + m_points_per_segment > active_count);
603 if (clamp) {
604 start_col = active_count - m_points_per_segment;
605 }
606
607 for (Eigen::Index off = 0; off < m_seg_total[s]; off += k_chunk_samples) {
608 m_chunks.push_back({ .segment = seg_idx,
609 .start_col = start_col,
610 .sample_begin = m_seg_first[s] + off,
611 .sample_count = std::min(k_chunk_samples, m_seg_total[s] - off),
612 .clamp_t_high = clamp });
613 }
614 }
615}
616
618 std::span<const double> control_points,
619 size_t dim,
620 Eigen::Index num_samples,
621 std::vector<double>& out)
622{
623 if (num_samples < 2) {
624 error<std::invalid_argument>(
627 std::source_location::current(),
628 "num_samples must be at least 2, but got {}",
629 num_samples);
630 }
631
632 if (dim == 0) {
633 error<std::invalid_argument>(
636 std::source_location::current(),
637 "dim must be at least 1");
638 }
639
640 const auto control_count = static_cast<Eigen::Index>(control_points.size() / dim);
641
642 if (control_count < 2) {
643 error<std::invalid_argument>(
646 std::source_location::current(),
647 "Need at least 2 control points, but got {}",
648 control_count);
649 }
650
651 if (m_points_per_segment == 0) {
652 error<std::invalid_argument>(
655 std::source_location::current(),
656 "Unsupported interpolation mode: {}",
657 static_cast<int>(m_mode));
658 }
659
660 if (!m_supports_multi && control_count != m_points_per_segment) {
661 error<std::invalid_argument>(
664 std::source_location::current(),
665 "{} interpolation requires exactly {} control points, but got {}",
666 static_cast<int>(m_mode), m_points_per_segment, control_count);
667 }
668
669 Eigen::Index active_count = 0;
670 const double* active = extend(control_points, dim, active_count);
671
672 const Eigen::Index num_segments = compute_num_segments(
673 active_count, m_points_per_segment, m_overlap);
674
675 if (num_segments < 1) {
676 error<std::invalid_argument>(
679 std::source_location::current(),
680 "Need sufficient control points for multi-segment {} interpolation, but got {}",
681 static_cast<int>(m_mode), control_count);
682 }
683
684 const auto stride = static_cast<size_t>(num_samples);
685 out.resize(dim * stride);
686
687 build_chunks(num_samples, num_segments, active_count);
688
689 const Eigen::Index pps = m_points_per_segment;
690 const bool trig = m_trigonometric;
691 const double* basis = m_basis.data();
692 double* out_base = out.data();
693
694 if (m_chunks.size() > 1 && num_samples >= k_parallel_min_samples) {
695 P::for_each(P::par_unseq, m_chunks.begin(), m_chunks.end(),
696 [&](const CurveChunk& chunk) {
697 const auto count = static_cast<size_t>(chunk.sample_count);
698 std::vector<double> params(count);
699 fill_parameters(params.data(), chunk, num_samples, num_segments);
700
701 double* dst = out_base + static_cast<size_t>(chunk.sample_begin);
702
703 if (trig) {
704 evaluate_chunk_cosine(active, chunk.start_col, params.data(),
705 dim, count, dst, stride);
706 return;
707 }
708
709 std::vector<double> folded(dim * static_cast<size_t>(pps));
710 fold_controls(active, chunk.start_col, dim, pps, basis, folded.data());
711
712 evaluate_chunk_polynomial(folded.data(), params.data(),
713 dim, pps, count, dst, stride);
714 });
715
716 return;
717 }
718
719 for (const CurveChunk& chunk : m_chunks) {
720 const auto count = static_cast<size_t>(chunk.sample_count);
721
722 m_tbuf.resize(count);
723 fill_parameters(m_tbuf.data(), chunk, num_samples, num_segments);
724
725 double* dst = out_base + static_cast<size_t>(chunk.sample_begin);
726
727 if (trig) {
728 evaluate_chunk_cosine(active, chunk.start_col, m_tbuf.data(),
729 dim, count, dst, stride);
730 continue;
731 }
732
733 m_folded.resize(dim * static_cast<size_t>(pps));
734 fold_controls(active, chunk.start_col, dim, pps, basis, m_folded.data());
735
736 evaluate_chunk_polynomial(m_folded.data(), m_tbuf.data(),
737 dim, pps, count, dst, stride);
738 }
739}
740
741void CurveEvaluator::reparameterize_planar(
742 std::span<const double> points,
743 size_t dim,
744 Eigen::Index point_count,
745 Eigen::Index num_samples,
746 std::vector<double>& out)
747{
748 const auto n = static_cast<size_t>(point_count);
749 const auto samples = static_cast<size_t>(num_samples);
750
751 if (point_count < 2 || num_samples < 2) {
752 out.assign(points.begin(), points.end());
753 return;
754 }
755
756 const double* src = points.data();
757
758 m_arc.assign(n, 0.0);
759
760 for (size_t d = 0; d < dim; ++d) {
761 const double* row = src + d * n;
762 for (size_t i = 1; i < n; ++i) {
763 const double delta = row[i] - row[i - 1];
764 m_arc[i] += delta * delta;
765 }
766 }
767
768 for (size_t i = 1; i < n; ++i) {
769 m_arc[i] = std::sqrt(m_arc[i]);
770 }
771
772 std::inclusive_scan(m_arc.data() + 1, m_arc.data() + n, m_arc.data() + 1);
773
774 const double total_length = m_arc[n - 1];
775 if (total_length == 0.0) {
776 out.assign(points.begin(), points.end());
777 return;
778 }
779
780 m_lower.resize(samples);
781 m_frac.resize(samples);
782
783 const double step = total_length / static_cast<double>(num_samples - 1);
784 size_t upper = 1;
785
786 for (size_t i = 0; i < samples; ++i) {
787 const double target = static_cast<double>(i) * step;
788
789 while (upper < n - 1 && m_arc[upper] < target) {
790 ++upper;
791 }
792
793 const size_t lower = upper - 1;
794 const double span = m_arc[upper] - m_arc[lower];
795
796 m_lower[i] = lower;
797 m_frac[i] = (span > 0.0) ? ((target - m_arc[lower]) / span) : 0.0;
798 }
799
800 out.resize(dim * samples);
801
802 for (size_t d = 0; d < dim; ++d) {
803 const double* row = src + d * n;
804 double* dst = out.data() + d * samples;
805
806 for (size_t i = 0; i < samples; ++i) {
807 const size_t lower = m_lower[i];
808 const double t = m_frac[i];
809 dst[i] = (1.0 - t) * row[lower] + t * row[lower + 1];
810 }
811 }
812}
813
814void CurveEvaluator::evaluate(
815 const Eigen::MatrixXd& control_points,
816 Eigen::Index num_samples,
817 Eigen::MatrixXd& out)
818{
819 const auto dim = static_cast<size_t>(control_points.rows());
820
821 evaluate_planar(
822 std::span<const double>(control_points.data(),
823 static_cast<size_t>(control_points.size())),
824 dim, num_samples, m_planar);
825
826 out.resize(control_points.rows(), num_samples);
827
828 const auto samples = static_cast<size_t>(num_samples);
829 double* dst = out.data();
830
831 for (size_t d = 0; d < dim; ++d) {
832 const double* row = m_planar.data() + d * samples;
833 for (size_t i = 0; i < samples; ++i) {
834 dst[d + i * dim] = row[i];
835 }
836 }
837}
838
839void CurveEvaluator::reparameterize(
840 const Eigen::MatrixXd& points,
841 Eigen::Index num_samples,
842 Eigen::MatrixXd& out)
843{
844 const auto dim = static_cast<size_t>(points.rows());
845 const auto n = static_cast<size_t>(points.cols());
846
847 m_planar.resize(dim * n);
848 const double* src = points.data();
849
850 for (size_t d = 0; d < dim; ++d) {
851 double* row = m_planar.data() + d * n;
852 for (size_t i = 0; i < n; ++i) {
853 row[i] = src[d + i * dim];
854 }
855 }
856
857 reparameterize_planar(m_planar, dim, points.cols(), num_samples, m_planar_alt);
858
859 const auto samples = m_planar_alt.size() / std::max<size_t>(dim, 1);
860 out.resize(points.rows(), static_cast<Eigen::Index>(samples));
861
862 double* dst = out.data();
863 for (size_t d = 0; d < dim; ++d) {
864 const double* row = m_planar_alt.data() + d * samples;
865 for (size_t i = 0; i < samples; ++i) {
866 dst[d + i * dim] = row[i];
867 }
868 }
869}
870
871// ===========================================================================
872// Free functions
873// ===========================================================================
874
876 const Eigen::MatrixXd& control_points,
877 Eigen::Index num_samples,
879 double tension)
880{
881 CurveEvaluator evaluator(mode, tension);
882 Eigen::MatrixXd result;
883 evaluator.evaluate(control_points, num_samples, result);
884 return result;
885}
886
887double compute_arc_length(const Eigen::MatrixXd& points)
888{
889 if (points.cols() < 2) {
890 return 0.0;
891 }
892
893 double length = 0.0;
894 for (Eigen::Index i = 1; i < points.cols(); ++i) {
895 length += (points.col(i) - points.col(i - 1)).norm();
896 }
897
898 return length;
899}
900
901Eigen::VectorXd compute_arc_length_table(const Eigen::MatrixXd& points)
902{
903 const Eigen::Index n = points.cols();
904
905 Eigen::VectorXd arc_lengths(n);
906 if (n == 0) {
907 return arc_lengths;
908 }
909
910 arc_lengths(0) = 0.0;
911 if (n == 1) {
912 return arc_lengths;
913 }
914
915 if (n >= k_parallel_min_points) {
916 P::for_each(P::par_unseq,
917 std::views::iota(Eigen::Index { 1 }, n).begin(),
918 std::views::iota(Eigen::Index { 1 }, n).end(),
919 [&](Eigen::Index i) {
920 arc_lengths(i) = (points.col(i) - points.col(i - 1)).norm();
921 });
922 } else {
923 for (Eigen::Index i = 1; i < n; ++i) {
924 arc_lengths(i) = (points.col(i) - points.col(i - 1)).norm();
925 }
926 }
927
928 std::inclusive_scan(
929 arc_lengths.data() + 1,
930 arc_lengths.data() + n,
931 arc_lengths.data() + 1);
932
933 return arc_lengths;
934}
935
937 const Eigen::MatrixXd& points,
938 Eigen::Index num_samples)
939{
940 CurveEvaluator evaluator;
941 Eigen::MatrixXd result;
942 evaluator.reparameterize(points, num_samples, result);
943 return result;
944}
945
947 const Kakshya::DataVariant& control_points,
948 Eigen::Index num_samples,
950 double tension)
951{
952 Eigen::MatrixXd control_matrix = Kakshya::to_eigen_matrix(control_points);
953
954 Eigen::MatrixXd interpolated = generate_interpolated_points(
955 control_matrix, num_samples, mode, tension);
956
957 Kakshya::EigenAccess input_access(control_points);
958
959 if (input_access.is_complex()) {
961 }
962
963 if (input_access.is_structured()) {
964 switch (input_access.component_count()) {
965 case 2:
967 case 3:
969 case 4:
971 default:
973 }
974 } else {
976 }
977}
978
979} // namespace MayaFlux::Kinesis
std::vector< glm::vec2 > * points
size_t a
size_t b
double q
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
std::vector< Eigen::Index > m_seg_first
CurveEvaluator(InterpolationMode mode=InterpolationMode::CATMULL_ROM, double tension=0.5)
Construct and resolve the evaluation kernel.
void evaluate_planar(std::span< const double > control_points, size_t dim, Eigen::Index num_samples, std::vector< double > &out)
Evaluate a curve into a coordinate-major buffer.
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:102
@ 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.
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.
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.