MayaFlux 0.4.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
GeometryPrimitives.cpp
Go to the documentation of this file.
2
3#include <glm/gtc/matrix_transform.hpp>
4
5namespace MayaFlux::Kinesis {
6
7namespace {
8 constexpr std::array<Nodes::TextureQuadVertex, 4> k_unit_quad = { {
9 { .position = { -1.0F, -1.0F, 0.0F }, .texcoord = { 0.0F, 1.0F } },
10 { .position = { 1.0F, -1.0F, 0.0F }, .texcoord = { 1.0F, 1.0F } },
11 { .position = { -1.0F, 1.0F, 0.0F }, .texcoord = { 0.0F, 0.0F } },
12 { .position = { 1.0F, 1.0F, 0.0F }, .texcoord = { 1.0F, 0.0F } },
13 } };
14
15} // namespace
16
17//==============================================================================
18// Primitive Generation
19//==============================================================================
20
21std::vector<glm::vec3> generate_circle(
22 const glm::vec3& center,
23 float radius,
24 size_t segments,
25 const glm::vec3& normal)
26{
27 if (segments < 3) {
28 segments = 3;
29 }
30
31 std::vector<glm::vec3> vertices;
32 vertices.reserve(segments + 1);
33
34 glm::vec3 n = glm::normalize(normal);
35 glm::vec3 u;
36
37 if (std::abs(n.z) < 0.9F) {
38 u = glm::normalize(glm::cross(n, glm::vec3(0, 0, 1)));
39 } else {
40 u = glm::normalize(glm::cross(n, glm::vec3(1, 0, 0)));
41 }
42
43 glm::vec3 v = glm::cross(n, u);
44
45 float angle_step = glm::two_pi<float>() / static_cast<float>(segments);
46
47 for (size_t i = 0; i <= segments; ++i) {
48 float angle = static_cast<float>(i) * angle_step;
49 float cos_a = std::cos(angle);
50 float sin_a = std::sin(angle);
51
52 glm::vec3 position = center + radius * (cos_a * u + sin_a * v);
53
54 vertices.push_back(position);
55 }
56
57 return vertices;
58}
59
60std::vector<glm::vec3> generate_ellipse(
61 const glm::vec3& center,
62 float semi_major,
63 float semi_minor,
64 size_t segments,
65 const glm::vec3& normal)
66{
67 if (segments < 3) {
68 segments = 3;
69 }
70
71 std::vector<glm::vec3> vertices;
72 vertices.reserve(segments + 1);
73
74 glm::vec3 n = glm::normalize(normal);
75 glm::vec3 u;
76
77 if (std::abs(n.z) < 0.9F) {
78 u = glm::normalize(glm::cross(n, glm::vec3(0, 0, 1)));
79 } else {
80 u = glm::normalize(glm::cross(n, glm::vec3(1, 0, 0)));
81 }
82
83 glm::vec3 v = glm::cross(n, u);
84
85 float angle_step = glm::two_pi<float>() / static_cast<float>(segments);
86
87 for (size_t i = 0; i <= segments; ++i) {
88 float angle = static_cast<float>(i) * angle_step;
89 float cos_a = std::cos(angle);
90 float sin_a = std::sin(angle);
91
92 glm::vec3 position = center + semi_major * cos_a * u + semi_minor * sin_a * v;
93
94 vertices.push_back(position);
95 }
96
97 return vertices;
98}
99
100std::vector<glm::vec3> generate_rectangle(
101 const glm::vec3& center,
102 float width,
103 float height,
104 const glm::vec3& normal)
105{
106 glm::vec3 n = glm::normalize(normal);
107 glm::vec3 u;
108
109 if (std::abs(n.z) < 0.9F) {
110 u = glm::normalize(glm::cross(n, glm::vec3(0, 0, 1)));
111 } else {
112 u = glm::normalize(glm::cross(n, glm::vec3(1, 0, 0)));
113 }
114
115 glm::vec3 v = glm::cross(n, u);
116
117 float half_width = width * 0.5F;
118 float half_height = height * 0.5F;
119
120 std::vector<glm::vec3> vertices;
121 vertices.reserve(5);
122
123 vertices.push_back(center - half_width * u - half_height * v);
124
125 vertices.push_back(center + half_width * u - half_height * v);
126
127 vertices.push_back(center + half_width * u + half_height * v);
128
129 vertices.push_back(center - half_width * u + half_height * v);
130
131 vertices.push_back(vertices[0]);
132
133 return vertices;
134}
135
136std::vector<glm::vec3> generate_regular_polygon(
137 const glm::vec3& center,
138 float radius,
139 size_t sides,
140 const glm::vec3& normal,
141 float phase_offset)
142{
143 if (sides < 3) {
144 sides = 3;
145 }
146
147 std::vector<glm::vec3> vertices;
148 vertices.reserve(sides + 1);
149
150 glm::vec3 n = glm::normalize(normal);
151 glm::vec3 u;
152
153 if (std::abs(n.z) < 0.9F) {
154 u = glm::normalize(glm::cross(n, glm::vec3(0, 0, 1)));
155 } else {
156 u = glm::normalize(glm::cross(n, glm::vec3(1, 0, 0)));
157 }
158
159 glm::vec3 v = glm::cross(n, u);
160
161 float angle_step = glm::two_pi<float>() / static_cast<float>(sides);
162
163 for (size_t i = 0; i <= sides; ++i) {
164 float angle = static_cast<float>(i) * angle_step + phase_offset;
165 float cos_a = std::cos(angle);
166 float sin_a = std::sin(angle);
167
168 glm::vec3 position = center + radius * (cos_a * u + sin_a * v);
169
170 vertices.push_back(position);
171 }
172
173 return vertices;
174}
175
176//==============================================================================
177// Transformations
178//==============================================================================
179
181 std::vector<glm::vec3>& vertices,
182 const glm::mat4& transform)
183{
184 for (auto& vertex : vertices) {
185 glm::vec4 pos(vertex, 1.0F);
186 pos = transform * pos;
187 vertex = glm::vec3(pos);
188 }
189}
190
192 std::vector<glm::vec3>& vertices,
193 const glm::vec3& axis,
194 float angle,
195 const glm::vec3& origin)
196{
197 glm::vec3 normalized_axis = glm::normalize(axis);
198
199 glm::mat4 transform = glm::translate(glm::mat4(1.0F), origin);
200 transform = glm::rotate(transform, angle, normalized_axis);
201 transform = glm::translate(transform, -origin);
202
203 apply_transform(vertices, transform);
204}
205
207 std::vector<glm::vec3>& vertices,
208 const glm::vec3& displacement)
209{
210 for (auto& vertex : vertices) {
211 vertex += displacement;
212 }
213}
214
216 std::vector<glm::vec3>& vertices,
217 float scale,
218 const glm::vec3& origin)
219{
220 if (scale <= 0.0F) {
221 return;
222 }
223
224 for (auto& vertex : vertices) {
225 glm::vec3 offset = vertex - origin;
226 vertex = origin + offset * scale;
227 }
228}
229
231 std::vector<glm::vec3>& vertices,
232 const glm::vec3& scale,
233 const glm::vec3& origin)
234{
235 for (auto& vertex : vertices) {
236 glm::vec3 offset = vertex - origin;
237 vertex = origin + offset * scale;
238 }
239}
240
241//==============================================================================
242// Differential Geometry
243//==============================================================================
244
245std::vector<Nodes::LineVertex> compute_path_normals(
246 const std::vector<Nodes::LineVertex>& path_vertices,
247 float normal_length,
248 size_t stride)
249{
250 if (path_vertices.size() < 2 || stride == 0) {
251 return {};
252 }
253
254 std::vector<Nodes::LineVertex> normals;
255 normals.reserve((path_vertices.size() - 1) / stride * 2);
256
257 for (size_t i = 0; i < path_vertices.size() - 1; i += stride) {
258 glm::vec3 p0 = path_vertices[i].position;
259 glm::vec3 p1 = path_vertices[i + 1].position;
260
261 glm::vec3 tangent = p1 - p0;
262 float length = glm::length(tangent);
263
264 if (length < 1e-6F) {
265 continue;
266 }
267
268 tangent /= length;
269
270 // Normal (perpendicular in XY plane)
271 // For 2D: rotate tangent 90° counter-clockwise
272 glm::vec3 normal(-tangent.y, tangent.x, 0.0F);
273 normal = glm::normalize(normal) * normal_length;
274
275 glm::vec3 midpoint = (p0 + p1) * 0.5F;
276
277 glm::vec3 color = path_vertices[i].color;
278 float thickness = path_vertices[i].thickness;
279
280 normals.push_back({ .position = midpoint - normal * 0.5F,
281 .color = color,
282 .thickness = thickness });
283
284 normals.push_back({ .position = midpoint + normal * 0.5F,
285 .color = color,
286 .thickness = thickness });
287 }
288
289 return normals;
290}
291
292std::vector<Nodes::LineVertex> compute_path_tangents(
293 const std::vector<Nodes::LineVertex>& path_vertices,
294 float tangent_length,
295 size_t stride)
296{
297 if (path_vertices.size() < 2 || stride == 0) {
298 return {};
299 }
300
301 std::vector<Nodes::LineVertex> tangents;
302 tangents.reserve((path_vertices.size() - 1) / stride * 2);
303
304 for (size_t i = 0; i < path_vertices.size() - 1; i += stride) {
305 glm::vec3 p0 = path_vertices[i].position;
306 glm::vec3 p1 = path_vertices[i + 1].position;
307
308 glm::vec3 tangent = p1 - p0;
309 float length = glm::length(tangent);
310
311 if (length < 1e-6F) {
312 continue;
313 }
314
315 tangent = glm::normalize(tangent) * tangent_length;
316
317 glm::vec3 color = path_vertices[i].color;
318 float thickness = path_vertices[i].thickness;
319
320 tangents.push_back({ .position = p0 - tangent * 0.5F,
321 .color = color,
322 .thickness = thickness });
323
324 tangents.push_back({ .position = p0 + tangent * 0.5F,
325 .color = color,
326 .thickness = thickness });
327 }
328
329 return tangents;
330}
331
332std::vector<Nodes::LineVertex> compute_path_curvature(
333 const std::vector<Nodes::LineVertex>& path_vertices,
334 float curvature_scale,
335 size_t stride)
336{
337 if (path_vertices.size() < 3 || stride == 0) {
338 return {};
339 }
340
341 std::vector<Nodes::LineVertex> curvatures;
342 curvatures.reserve((path_vertices.size() - 2) / stride * 2);
343
344 for (size_t i = 1; i < path_vertices.size() - 1; i += stride) {
345 glm::vec3 p_prev = path_vertices[i - 1].position;
346 glm::vec3 p_curr = path_vertices[i].position;
347 glm::vec3 p_next = path_vertices[i + 1].position;
348
349 glm::vec3 curvature = (p_next - 2.0F * p_curr + p_prev) * curvature_scale;
350
351 glm::vec3 color = path_vertices[i].color;
352 float thickness = path_vertices[i].thickness;
353
354 curvatures.push_back({ .position = p_curr,
355 .color = color,
356 .thickness = thickness });
357
358 curvatures.push_back({ .position = p_curr + curvature,
359 .color = color,
360 .thickness = thickness });
361 }
362
363 return curvatures;
364}
365
366//==============================================================================
367// Parametric Curves
368//==============================================================================
369
370std::vector<glm::vec3> sample_parametric_curve(
371 const std::function<glm::vec3(float)>& curve,
372 size_t samples)
373{
374 if (samples < 2) {
375 samples = 2;
376 }
377
378 std::vector<glm::vec3> vertices;
379 vertices.reserve(samples);
380
381 for (size_t i = 0; i < samples; ++i) {
382 float t = static_cast<float>(i) / static_cast<float>(samples - 1);
383 glm::vec3 position = curve(t);
384
385 vertices.push_back(position);
386 }
387
388 return vertices;
389}
390
391std::vector<Nodes::LineVertex> reparameterize_by_arc_length(
392 const std::vector<Nodes::LineVertex>& path_vertices,
393 size_t num_samples)
394{
395 if (path_vertices.size() < 2 || num_samples < 2) {
396 return path_vertices;
397 }
398
399 std::vector<float> arc_lengths;
400 arc_lengths.reserve(path_vertices.size());
401 arc_lengths.push_back(0.0F);
402
403 float total_length = 0.0F;
404 for (size_t i = 1; i < path_vertices.size(); ++i) {
405 float segment_length = glm::distance(
406 path_vertices[i].position,
407 path_vertices[i - 1].position);
408 total_length += segment_length;
409 arc_lengths.push_back(total_length);
410 }
411
412 if (total_length < 1e-6F) {
413 return path_vertices;
414 }
415
416 std::vector<Nodes::LineVertex> resampled;
417 resampled.reserve(num_samples);
418
419 for (size_t i = 0; i < num_samples; ++i) {
420 float target_length = (static_cast<float>(i) / static_cast<float>(num_samples - 1)) * total_length;
421
422 auto it = std::ranges::lower_bound(arc_lengths, target_length);
423 size_t idx = std::distance(arc_lengths.begin(), it);
424
425 if (idx == 0) {
426 resampled.push_back(path_vertices[0]);
427 } else if (idx >= path_vertices.size()) {
428 resampled.push_back(path_vertices.back());
429 } else {
430 float s0 = arc_lengths[idx - 1];
431 float s1 = arc_lengths[idx];
432 float t = (target_length - s0) / (s1 - s0);
433
434 glm::vec3 p0 = path_vertices[idx - 1].position;
435 glm::vec3 p1 = path_vertices[idx].position;
436 glm::vec3 position = glm::mix(p0, p1, t);
437
438 glm::vec3 c0 = path_vertices[idx - 1].color;
439 glm::vec3 c1 = path_vertices[idx].color;
440 glm::vec3 color = glm::mix(c0, c1, t);
441
442 resampled.push_back({ .position = position,
443 .color = color,
444 .thickness = 1.0F });
445 }
446 }
447
448 return resampled;
449}
450
451//==============================================================================
452// Geometric Operations
453//==============================================================================
454
456 std::vector<glm::vec3>& vertices,
457 const glm::vec3& plane_point,
458 const glm::vec3& plane_normal)
459{
460 glm::vec3 n = glm::normalize(plane_normal);
461
462 for (auto& vertex : vertices) {
463 glm::vec3 offset = vertex - plane_point;
464 float distance = glm::dot(offset, n);
465 vertex = vertex - distance * n;
466 }
467}
468
469std::vector<glm::vec3> compute_convex_hull_2d(
470 const std::vector<glm::vec3>& vertices,
471 const glm::vec3& projection_normal)
472{
473 if (vertices.size() < 3) {
474 return vertices;
475 }
476
477 glm::vec3 n = glm::normalize(projection_normal);
478 glm::vec3 u;
479
480 if (std::abs(n.z) < 0.9F) {
481 u = glm::normalize(glm::cross(n, glm::vec3(0, 0, 1)));
482 } else {
483 u = glm::normalize(glm::cross(n, glm::vec3(1, 0, 0)));
484 }
485
486 glm::vec3 v = glm::cross(n, u);
487
488 struct Point2D {
489 glm::vec2 pos;
490 size_t index;
491 };
492
493 std::vector<Point2D> points;
494 points.reserve(vertices.size());
495
496 for (size_t i = 0; i < vertices.size(); ++i) {
497 glm::vec3 offset = vertices[i];
498 float x = glm::dot(offset, u);
499 float y = glm::dot(offset, v);
500 points.push_back({ glm::vec2(x, y), i });
501 }
502
503 auto pivot_it = std::ranges::min_element(points,
504 [](const Point2D& a, const Point2D& b) {
505 if (std::abs(a.pos.y - b.pos.y) < 1e-6F) {
506 return a.pos.x < b.pos.x;
507 }
508 return a.pos.y < b.pos.y;
509 });
510
511 std::swap(points[0], *pivot_it);
512 Point2D pivot = points[0];
513
514 std::sort(points.begin() + 1, points.end(),
515 [&pivot](const Point2D& a, const Point2D& b) {
516 glm::vec2 va = a.pos - pivot.pos;
517 glm::vec2 vb = b.pos - pivot.pos;
518
519 float cross = va.x * vb.y - va.y * vb.x;
520 if (std::abs(cross) < 1e-6F) {
521 return glm::length(va) < glm::length(vb);
522 }
523 return cross > 0.0F;
524 });
525
526 // Graham scan
527 std::vector<size_t> hull;
528 hull.push_back(points[0].index);
529 hull.push_back(points[1].index);
530
531 auto ccw = [](const glm::vec2& a, const glm::vec2& b, const glm::vec2& c) {
532 return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x) > 0.0F;
533 };
534
535 for (size_t i = 2; i < points.size(); ++i) {
536 while (hull.size() >= 2) {
537 size_t top = hull.back();
538 size_t second = hull[hull.size() - 2];
539
540 glm::vec2 p1 = points[second].pos;
541 glm::vec2 p2 = points[top].pos;
542 glm::vec2 p3 = points[i].pos;
543
544 if (ccw(p1, p2, p3)) {
545 break;
546 }
547 hull.pop_back();
548 }
549 hull.push_back(points[i].index);
550 }
551
552 std::vector<glm::vec3> result;
553 result.reserve(hull.size() + 1);
554
555 for (size_t idx : hull) {
556 result.push_back(vertices[idx]);
557 }
558
559 result.push_back(vertices[hull[0]]);
560
561 return result;
562}
563
564//==============================================================================
565// Color Utilities
566//==============================================================================
567
568std::vector<Nodes::LineVertex> apply_color_gradient(
569 const std::vector<glm::vec3>& positions,
570 const std::vector<glm::vec3>& colors,
571 const std::vector<float>& color_positions,
572 float default_thickness)
573{
574 if (positions.empty() || colors.empty()) {
575 return {};
576 }
577
578 std::vector<Nodes::LineVertex> vertices;
579 vertices.reserve(positions.size());
580
581 if (colors.size() == 1) {
582 for (const auto& pos : positions) {
583 vertices.push_back({ .position = pos,
584 .color = colors[0],
585 .thickness = default_thickness });
586 }
587 return vertices;
588 }
589
590 std::vector<float> stops = color_positions;
591 if (stops.empty()) {
592 stops.reserve(colors.size());
593 for (size_t i = 0; i < colors.size(); ++i) {
594 stops.push_back(static_cast<float>(i) / static_cast<float>(colors.size() - 1));
595 }
596 }
597
598 for (size_t i = 0; i < positions.size(); ++i) {
599 float t = static_cast<float>(i) / static_cast<float>(positions.size() - 1);
600
601 glm::vec3 color;
602 if (t <= stops[0]) {
603 color = colors[0];
604 } else if (t >= stops.back()) {
605 color = colors.back();
606 } else {
607 size_t idx = 0;
608 for (size_t j = 1; j < stops.size(); ++j) {
609 if (t <= stops[j]) {
610 idx = j - 1;
611 break;
612 }
613 }
614
615 float local_t = (t - stops[idx]) / (stops[idx + 1] - stops[idx]);
616 color = glm::mix(colors[idx], colors[idx + 1], local_t);
617 }
618
619 vertices.push_back({ .position = positions[i],
620 .color = color,
621 .thickness = default_thickness });
622 }
623
624 return vertices;
625}
626
627std::vector<Nodes::LineVertex> apply_uniform_color(
628 const std::vector<glm::vec3>& positions,
629 const glm::vec3& color,
630 float default_thickness)
631{
632 std::vector<Nodes::LineVertex> vertices;
633 vertices.reserve(positions.size());
634
635 for (const auto& pos : positions) {
636 vertices.push_back({ .position = pos,
637 .color = color,
638 .thickness = default_thickness });
639 }
640
641 return vertices;
642}
643
644std::vector<Nodes::LineVertex> apply_vertex_colors(
645 const std::vector<glm::vec3>& positions,
646 const std::vector<glm::vec3>& colors,
647 float default_thickness)
648{
649 if (positions.size() != colors.size()) {
650 return {};
651 }
652
653 std::vector<Nodes::LineVertex> vertices;
654 vertices.reserve(positions.size());
655
656 for (size_t i = 0; i < positions.size(); ++i) {
657 vertices.push_back({ .position = positions[i],
658 .color = colors[i],
659 .thickness = default_thickness });
660 }
661
662 return vertices;
663}
664
665QuadGeometry generate_quad(glm::vec2 position, glm::vec2 scale, float rotation)
666{
667 const float cos_r = std::cos(rotation);
668 const float sin_r = std::sin(rotation);
669
670 QuadGeometry out { .layout = Kakshya::VertexLayout::for_textured_quad() };
671
672 for (size_t i = 0; i < 4; ++i) {
673 const float x = k_unit_quad[i].position.x * scale.x;
674 const float y = k_unit_quad[i].position.y * scale.y;
675
676 out.vertices[i].position = {
677 x * cos_r - y * sin_r + position.x,
678 x * sin_r + y * cos_r + position.y,
679 0.0F,
680 };
681 out.vertices[i].texcoord = k_unit_quad[i].texcoord;
682 }
683
684 return out;
685}
686
687} // namespace MayaFlux::Kinesis
size_t a
size_t b
uint32_t width
Range radius
glm::vec3 position
std::optional< glm::vec3 > color
std::vector< Nodes::LineVertex > compute_path_normals(const std::vector< Nodes::LineVertex > &path_vertices, float normal_length, size_t stride)
Compute normal vectors along a piecewise-linear path.
std::vector< Nodes::LineVertex > reparameterize_by_arc_length(const std::vector< Nodes::LineVertex > &path_vertices, size_t num_samples)
Resample path vertices for arc-length parameterization.
void apply_transform(std::vector< glm::vec3 > &vertices, const glm::mat4 &transform)
Apply rigid transformation to vertex set.
void apply_translation(std::vector< glm::vec3 > &vertices, const glm::vec3 &displacement)
Apply translation to vertex set.
std::vector< Nodes::LineVertex > apply_color_gradient(const std::vector< glm::vec3 > &positions, const std::vector< glm::vec3 > &colors, const std::vector< float > &color_positions, float default_thickness)
Apply color interpolation to position vertices.
std::vector< Nodes::LineVertex > compute_path_tangents(const std::vector< Nodes::LineVertex > &path_vertices, float tangent_length, size_t stride)
Compute tangent vectors along a piecewise-linear path.
std::vector< Nodes::LineVertex > apply_vertex_colors(const std::vector< glm::vec3 > &positions, const std::vector< glm::vec3 > &colors, float default_thickness)
Convert positions to LineVertex with per-vertex colors.
std::vector< Nodes::LineVertex > apply_uniform_color(const std::vector< glm::vec3 > &positions, const glm::vec3 &color, float default_thickness)
Apply uniform color to position vertices.
std::vector< glm::vec3 > compute_convex_hull_2d(const std::vector< glm::vec3 > &vertices, const glm::vec3 &projection_normal)
Compute convex hull of vertex set (2D projection)
void project_onto_plane(std::vector< glm::vec3 > &vertices, const glm::vec3 &plane_point, const glm::vec3 &plane_normal)
Project vertices onto plane defined by normal.
std::vector< glm::vec3 > generate_ellipse(const glm::vec3 &center, float semi_major, float semi_minor, size_t segments, const glm::vec3 &normal)
Generate vertices along an elliptical path.
std::vector< glm::vec3 > sample_parametric_curve(const std::function< glm::vec3(float)> &curve, size_t samples)
Sample parametric curve uniformly in parameter space.
QuadGeometry generate_quad(glm::vec2 position, glm::vec2 scale, float rotation)
Generate a textured quad centred on the origin.
SpatialField distance(const glm::vec3 &anchor, float radius, DistanceMetric metric=DistanceMetric::EUCLIDEAN)
Normalized distance from an anchor point using the specified metric.
std::vector< glm::vec3 > generate_circle(const glm::vec3 &center, float radius, size_t segments, const glm::vec3 &normal)
Generate vertices along a circular path.
std::vector< glm::vec3 > generate_rectangle(const glm::vec3 &center, float width, float height, const glm::vec3 &normal)
Generate vertices of an axis-aligned rectangular path.
void apply_scale(std::vector< glm::vec3 > &vertices, const glm::vec3 &scale, const glm::vec3 &origin)
Apply non-uniform scaling to vertex set.
std::vector< glm::vec3 > generate_regular_polygon(const glm::vec3 &center, float radius, size_t sides, const glm::vec3 &normal, float phase_offset)
Generate vertices of a regular n-gon.
Tendency< D, float > scale(const Tendency< D, float > &t, float factor)
Uniform scaling of a scalar-output tendency.
Definition Tendency.hpp:97
void apply_rotation(std::vector< glm::vec3 > &vertices, const glm::vec3 &axis, float angle, const glm::vec3 &origin)
Apply rotation to vertex set around arbitrary axis.
void apply_uniform_scale(std::vector< glm::vec3 > &vertices, float scale, const glm::vec3 &origin)
Apply uniform scaling to vertex set.
std::vector< Nodes::LineVertex > compute_path_curvature(const std::vector< Nodes::LineVertex > &path_vertices, float curvature_scale, size_t stride)
Compute curvature vectors along a path (2nd derivative approximation)
Textured quad vertex data together with its semantic layout descriptor.