scimesh 0.3.4
Headless CPU-only 3D software renderer for scientific mesh visualization
Loading...
Searching...
No Matches
spline.h
Go to the documentation of this file.
1
31
32#pragma once
33
34#include <scimesh/types.h>
35#include <glm/geometric.hpp>
36
37#include <algorithm>
38#include <cmath>
39#include <vector>
40
41namespace scimesh {
42
43namespace detail {
44
49constexpr size_t kMinPathPoints = 2u;
50
53constexpr double kMinKnotInterval = 1e-6;
54
57constexpr double kArcLengthEpsilon = 1e-9;
58
76inline Vec3 hermite_segment(const Vec3 &p0, const Vec3 &p1, const Vec3 &m0,
77 const Vec3 &m1, double h, double s) {
78 const double s2 = s * s;
79 const double s3 = s2 * s;
80
81 const double w00 = 2.0 * s3 - 3.0 * s2 + 1.0;
82 const double w10 = s3 - 2.0 * s2 + s;
83 const double w01 = -2.0 * s3 + 3.0 * s2;
84 const double w11 = s3 - s2;
85
86 const Vec3 ends = p0 * static_cast<float>(w00) + p1 * static_cast<float>(w01);
87 const Vec3 tangents = m0 * static_cast<float>(w10 * h) +
88 m1 * static_cast<float>(w11 * h);
89 return ends + tangents;
90}
91
96inline size_t segment_count(size_t num_points, bool closed) {
98 return 0u;
99 }
100 return closed ? num_points : num_points - 1u;
101}
102
104inline size_t next_index(size_t i, size_t num_points, bool closed) {
105 const size_t n = i + 1u;
106 if (n < num_points) {
107 return n;
108 }
109 return closed ? 0u : num_points - 1u;
110}
111
114 return std::max(1, samples_per_segment);
115}
116
117} // namespace detail
118
119// ---------------------------------------------------------------------------
120// Path queries
121// ---------------------------------------------------------------------------
122
138inline float path_length(const std::vector<Vec3> &path, bool closed = false) {
139 const size_t num_segments = detail::segment_count(path.size(), closed);
140 double total = 0.0;
141 for (size_t i = 0; i < num_segments; ++i) {
142 total += glm::length(path[detail::next_index(i, path.size(), closed)] -
143 path[i]);
144 }
145 return static_cast<float>(total);
146}
147
160inline std::vector<Vec3> remove_duplicate_points(const std::vector<Vec3> &path,
161 float epsilon = 1e-6f) {
162 std::vector<Vec3> cleaned;
163 cleaned.reserve(path.size());
164 for (const Vec3 &p : path) {
165 if (cleaned.empty() || glm::length(p - cleaned.back()) > epsilon) {
166 cleaned.push_back(p);
167 }
168 }
169 return cleaned;
170}
171
185inline std::vector<Vec3> path_tangents(const std::vector<Vec3> &path,
186 bool closed = false) {
187 std::vector<Vec3> tangents;
188 const size_t n = path.size();
189 if (static_cast<int>(n) < static_cast<int>(detail::kMinPathPoints)) {
190 return tangents;
191 }
192 tangents.resize(n);
193
194 for (size_t i = 0; i < n; ++i) {
195 Vec3 dir;
196 if (closed) {
197 const Vec3 prev = path[(i + n - 1u) % n];
198 const Vec3 next = path[(i + 1u) % n];
199 dir = next - prev;
200 } else if (i == 0u) {
201 dir = path[1] - path[0];
202 } else if (i + 1u == n) {
203 dir = path[n - 1u] - path[n - 2u];
204 } else {
205 dir = path[i + 1u] - path[i - 1u];
206 }
207
208 const float len = glm::length(dir);
209 if (len > 1e-8f) {
210 tangents[i] = dir / len;
211 } else if (i > 0u) {
212 tangents[i] = tangents[i - 1u];
213 } else {
214 tangents[i] = Vec3(0.0f, 0.0f, 1.0f);
215 }
216 }
217 return tangents;
218}
219
253inline std::vector<float> path_curvature(const std::vector<Vec3> &path,
254 bool closed = false) {
255 const size_t n = path.size();
256 if (n < 3u) {
257 return std::vector<float>();
258 }
259
260 std::vector<float> curvature(n, 0.0f);
261 for (size_t i = 0; i < n; ++i) {
262 if (!closed && (i == 0u || i + 1u == n)) {
263 continue; // filled in below from the neighbouring interior point.
264 }
265 const Vec3 prev = path[closed ? (i + n - 1u) % n : i - 1u];
266 const Vec3 next = path[closed ? (i + 1u) % n : i + 1u];
267
268 const Vec3 v = next - prev; // ~ x' / (2h)
269 const Vec3 a = next - 2.0f * path[i] + prev; // ~ x'' / h^2
270 const float v_len = glm::length(v);
271 if (v_len < 1e-8f) {
272 continue;
273 }
274 const float cross_len = glm::length(glm::cross(v, a));
275 curvature[i] = (4.0f * cross_len) /
276 (v_len * v_len * v_len);
277 }
278
279 if (!closed && n >= 3u) {
280 curvature[0] = curvature[1];
281 curvature[n - 1u] = curvature[n - 2u];
282 }
283 return curvature;
284}
285
286// ---------------------------------------------------------------------------
287// Resampling
288// ---------------------------------------------------------------------------
289
333inline std::vector<Vec3> resample_by_arclength(const std::vector<Vec3> &path,
334 float step,
335 bool closed = false) {
336 const size_t n = path.size();
337 if (step <= 0.0f || n < 2u) {
338 return path;
339 }
340
341 const double total = path_length(path, closed);
342 if (total <= 0.0) {
343 return std::vector<Vec3>(1u, path[0]);
344 }
345
346 // A closed loop is resampled at the step that covers it in a whole number
347 // of steps, so that the seam is a regular step like every other one.
348 double step_used = static_cast<double>(step);
349 if (closed) {
350 const double steps = std::max(
351 3.0, std::round(total / static_cast<double>(step)));
353 }
354
355 std::vector<Vec3> out;
356 out.reserve(static_cast<size_t>(std::ceil(total / step_used)) + 2u);
357 out.push_back(path[0]);
358
359 const double kStep = step_used;
360 const double eps = detail::kArcLengthEpsilon * total;
362
363 double travelled = 0.0; // arc length consumed before the current segment
364 double next_at = kStep; // arc length of the next sample
365 for (size_t i = 0; i < num_segments; ++i) {
366 const Vec3 a = path[i];
367 const Vec3 b = path[detail::next_index(i, n, closed)];
368 const double seg_len = glm::length(b - a);
369
370 if (seg_len > 0.0) {
371 while (next_at <= travelled + seg_len && next_at < total - eps) {
372 const double u = (next_at - travelled) / seg_len;
373 out.push_back(a + static_cast<float>(u) * (b - a));
374 next_at += kStep;
375 }
376 }
378 }
379
380 // The end of an open path is part of the shape, even when it does not fall
381 // on a sample boundary; a closed path closes on its starting point.
382 out.push_back(closed ? path[0] : path[n - 1u]);
383 return out;
384}
385
386// ---------------------------------------------------------------------------
387// Curve sampling
388// ---------------------------------------------------------------------------
389
423inline std::vector<Vec3> hermite_path(const std::vector<Vec3> &points,
424 const std::vector<Vec3> &tangents,
425 int samples_per_segment = 8,
426 bool closed = false) {
427 std::vector<Vec3> path;
428 const std::vector<Vec3> pts = remove_duplicate_points(points);
429 if (pts.size() != points.size() || tangents.size() != points.size()) {
430 return path; // caller passed degenerate points or mismatched sizes.
431 }
432 if (pts.size() < detail::kMinPathPoints ||
433 (closed && pts.size() < 3u)) {
434 return path;
435 }
436
438 const size_t num_segments = detail::segment_count(pts.size(), closed);
439 path.reserve(num_segments * static_cast<size_t>(samples) + 1u);
440
441 // Each segment spans one unit of the curve parameter, so h == 1 and the
442 // caller's tangents are used as-is.
443 for (size_t i = 0; i < num_segments; ++i) {
444 const size_t j = detail::next_index(i, pts.size(), closed);
445 for (int k = 0; k < samples; ++k) {
446 const double s = static_cast<double>(k) / static_cast<double>(samples);
448 tangents[j], 1.0, s));
449 }
450 }
451 if (closed) {
452 path.push_back(pts[0]); // the loop closes on the first point.
453 } else {
454 path.push_back(pts.back());
455 }
456 return path;
457}
458
499inline std::vector<Vec3> catmull_rom_path(const std::vector<Vec3> &points,
500 int samples_per_segment = 8,
501 bool closed = false,
502 float alpha = 0.5f) {
503 std::vector<Vec3> path;
504 const std::vector<Vec3> pts = remove_duplicate_points(points);
505 const size_t n = pts.size();
506 if (n < detail::kMinPathPoints || (closed && n < 3u)) {
507 return path;
508 }
509
510 // Knot intervals: the parameter distance between two neighbouring points.
511 // For alpha = 0 every interval is 1 (the uniform case), otherwise it grows
512 // with the chord length to the power of alpha.
513 std::vector<double> interval(n, 1.0);
514 for (size_t i = 0; i < n; ++i) {
515 const size_t j = detail::next_index(i, n, closed);
516 if (!closed && j == i) {
517 break; // open curve: the last point has no outgoing segment.
518 }
519 const double chord = glm::length(pts[j] - pts[i]);
520 interval[i] = std::max(
521 (alpha == 0.0f) ? 1.0 : std::pow(chord, static_cast<double>(alpha)),
523 }
524
525 // Cumulative knot values along the curve.
526 std::vector<double> knot(n, 0.0);
527 for (size_t i = 1; i < n; ++i) {
528 knot[i] = knot[i - 1u] + interval[i - 1u];
529 }
530
531 // Tangent at each control point: the central difference of its neighbours,
532 // divided by their knot distance (which is what makes the formula work for
533 // the non-uniform parameterizations).
534 std::vector<Vec3> tangents(n);
535 for (size_t i = 0; i < n; ++i) {
536 Vec3 prev, next;
537 double t_prev, t_next;
538 if (closed) {
539 prev = pts[(i + n - 1u) % n];
540 next = pts[(i + 1u) % n];
541 t_prev = knot[i] - interval[(i + n - 1u) % n];
542 t_next = knot[i] + interval[i];
543 } else if (i == 0u) {
544 // Reflected phantom point: keeps the curve starting at pts[0] with
545 // the tangent direction of the first segment.
546 prev = 2.0f * pts[0] - pts[1];
547 next = pts[1];
548 t_prev = knot[0] - interval[0];
549 t_next = knot[1];
550 } else if (i + 1u == n) {
551 next = 2.0f * pts[n - 1u] - pts[n - 2u];
552 prev = pts[n - 2u];
553 t_prev = knot[n - 2u];
554 t_next = knot[n - 1u] + interval[n - 2u];
555 } else {
556 prev = pts[i - 1u];
557 next = pts[i + 1u];
558 t_prev = knot[i - 1u];
559 t_next = knot[i + 1u];
560 }
561
562 const double span = t_next - t_prev;
564 tangents[i] = (next - prev) / static_cast<float>(span);
565 }
566 }
567
570 path.reserve(num_segments * static_cast<size_t>(samples) + 1u);
571
572 for (size_t i = 0; i < num_segments; ++i) {
573 const size_t j = detail::next_index(i, n, closed);
574 const double h = (j == 0u) ? (interval[n - 1u]) : (knot[j] - knot[i]);
575 for (int k = 0; k < samples; ++k) {
576 const double s = static_cast<double>(k) / static_cast<double>(samples);
578 tangents[j], h, s));
579 }
580 }
581 path.push_back(closed ? pts[0] : pts[n - 1u]);
582 return path;
583}
584
614inline std::vector<Vec3> bspline_path(const std::vector<Vec3> &points,
615 int samples_per_segment = 8,
616 bool closed = false) {
617 std::vector<Vec3> path;
618 const std::vector<Vec3> pts = remove_duplicate_points(points);
619 const size_t n = pts.size();
620 if (n < 4u) {
621 return path; // a cubic B-spline needs four control points per piece.
622 }
623
625
626 // Uniform cubic B-spline basis, as a function of the local parameter s in
627 // [0, 1] within one knot span.
628 const auto basis = [](double s, double &b0, double &b1, double &b2, double &b3) {
629 const double s2 = s * s;
630 const double s3 = s2 * s;
631 b0 = (1.0 - 3.0 * s + 3.0 * s2 - s3) / 6.0;
632 b1 = (4.0 - 6.0 * s2 + 3.0 * s3) / 6.0;
633 b2 = (1.0 + 3.0 * s + 3.0 * s2 - 3.0 * s3) / 6.0;
634 b3 = s3 / 6.0;
635 };
636
637 // Segment `i` is controlled by the points i-3 .. i (wrapped for a closed
638 // curve): a uniform cubic B-spline is defined on the knot span [3, n] of
639 // the uniform knot vector, which is n - 3 segments for an open curve and n
640 // for a closed one. For the open curve, segment i therefore uses the
641 // points i .. i+3 directly.
642 const size_t num_segments = closed ? n : (n - 3u);
643 path.reserve(num_segments * static_cast<size_t>(samples) + 1u);
644
645 for (size_t i = 0; i < num_segments; ++i) {
646 const Vec3 &p0 = pts[closed ? (i + n - 3u) % n : i];
647 const Vec3 &p1 = pts[closed ? (i + n - 2u) % n : i + 1u];
648 const Vec3 &p2 = pts[closed ? (i + n - 1u) % n : i + 2u];
649 const Vec3 &p3 = pts[closed ? i : i + 3u];
650
651 // The last segment of an open curve also emits its endpoint (s = 1),
652 // so that the path ends where the curve does.
653 const int emit = (!closed && i + 1u == num_segments) ? samples + 1 : samples;
654 for (int k = 0; k < emit; ++k) {
655 const double s = static_cast<double>(k) / static_cast<double>(samples);
656 double b0, b1, b2, b3;
657 basis(s, b0, b1, b2, b3);
658 path.push_back(static_cast<float>(b0) * p0 +
659 static_cast<float>(b1) * p1 +
660 static_cast<float>(b2) * p2 +
661 static_cast<float>(b3) * p3);
662 }
663 }
664
665 // A periodic B-spline returns to its starting point after n knot spans, so
666 // the loop closes on the first sample.
667 if (closed) {
668 path.push_back(path[0]);
669 }
670 return path;
671}
672
697inline std::vector<Vec3> bezier_path(const std::vector<Vec3> &control_points,
698 int samples = 64) {
699 std::vector<Vec3> path;
700 const size_t n = control_points.size();
701 if (n < 2u) {
702 return path;
703 }
704
705 const int count = std::max(2, samples);
706 path.reserve(static_cast<size_t>(count));
707
708 std::vector<Vec3> work(n);
709 for (int k = 0; k < count; ++k) {
710 // De Casteljau: repeated linear interpolation between neighbouring
711 // points converges to the curve point for the current parameter.
712 const float u = static_cast<float>(k) / static_cast<float>(count - 1);
714 for (size_t level = n - 1u; level > 0u; --level) {
715 for (size_t i = 0; i < level; ++i) {
716 work[i] = (1.0f - u) * work[i] + u * work[i + 1u];
717 }
718 }
719 path.push_back(work[0]);
720 }
721 return path;
722}
723
724} // namespace scimesh
int sample_count(int samples_per_segment)
Clamp a requested sample count to at least one.
Definition spline.h:113
constexpr double kMinKnotInterval
Smallest knot interval, to keep a degenerate point pair from producing a zero-length interval (and a ...
Definition spline.h:53
constexpr double kArcLengthEpsilon
Tolerance used when deciding whether an arc-length sample coincides with the end of a path,...
Definition spline.h:57
size_t next_index(size_t i, size_t num_points, bool closed)
Index of the point after i, wrapping around for closed curves.
Definition spline.h:104
constexpr size_t kMinPathPoints
Minimum number of points that can describe a path.
Definition spline.h:49
Vec3 hermite_segment(const Vec3 &p0, const Vec3 &p1, const Vec3 &m0, const Vec3 &m1, double h, double s)
Cubic Hermite basis evaluation of a single curve segment.
Definition spline.h:76
size_t segment_count(size_t num_points, bool closed)
Number of segments a sampling function has to emit.
Definition spline.h:96
std::vector< Vec3 > bspline_path(const std::vector< Vec3 > &points, int samples_per_segment=8, bool closed=false)
Sample a uniform cubic B-spline through the given points.
Definition spline.h:614
std::vector< Vec3 > catmull_rom_path(const std::vector< Vec3 > &points, int samples_per_segment=8, bool closed=false, float alpha=0.5f)
Sample a non-uniform Catmull-Rom curve through the given points.
Definition spline.h:499
std::vector< Vec3 > path_tangents(const std::vector< Vec3 > &path, bool closed=false)
Unit tangent direction at every point of a path.
Definition spline.h:185
float path_length(const std::vector< Vec3 > &path, bool closed=false)
Total length of a polyline path.
Definition spline.h:138
std::vector< float > path_curvature(const std::vector< Vec3 > &path, bool closed=false)
Discrete curvature at every point of a path.
Definition spline.h:253
std::vector< Vec3 > resample_by_arclength(const std::vector< Vec3 > &path, float step, bool closed=false)
Resample a path at a fixed arc-length step.
Definition spline.h:333
glm::vec3 Vec3
3-component floating-point vector (xyz).
Definition types.h:46
std::vector< Vec3 > bezier_path(const std::vector< Vec3 > &control_points, int samples=64)
Sample a Bezier curve from a control polygon.
Definition spline.h:697
std::vector< Vec3 > hermite_path(const std::vector< Vec3 > &points, const std::vector< Vec3 > &tangents, int samples_per_segment=8, bool closed=false)
Sample a cubic Hermite curve with caller-supplied tangents.
Definition spline.h:423
std::vector< Vec3 > remove_duplicate_points(const std::vector< Vec3 > &path, float epsilon=1e-6f)
Remove points that repeat their predecessor.
Definition spline.h:160
int x
Left edge of the bitmap, in image pixels.
Definition text.cpp:223
Fundamental types used throughout the scimesh rendering engine.