scimesh 0.3.4
Headless CPU-only 3D software renderer for scientific mesh visualization
Loading...
Searching...
No Matches
primitives.cpp
Go to the documentation of this file.
2#include <scimesh/normals.h>
4#include <glm/glm.hpp>
5#include <glm/gtc/constants.hpp>
6#include <cmath>
7#include <algorithm>
8// <array> and <utility> must be included explicitly: this file uses
9// std::array (pyramid/tetrahedron faces) and std::swap. libstdc++ (GCC, also
10// used by R on Windows) pulls both in transitively, but libc++ (clang on
11// macOS) does not, which made the package fail to compile there. Note that
12// libc++ forward-declares std::array in <__tuple> (for tuple_size), so
13// omitting <array> yields the confusing error "implicit instantiation of
14// undefined template 'std::array<...>'" instead of "no member named array".
15#include <array>
16#include <utility>
17
18namespace scimesh {
19
20static inline void make_basis(const Vec3 &dir, Vec3 &u, Vec3 &v) {
21 Vec3 arbitrary = (std::abs(glm::dot(dir, Vec3(0.0f, 1.0f, 0.0f))) < 0.999f)
22 ? Vec3(0.0f, 1.0f, 0.0f)
23 : Vec3(1.0f, 0.0f, 0.0f);
24 u = glm::normalize(glm::cross(arbitrary, dir));
25 v = glm::cross(dir, u);
26}
27
35static inline float batch_radius(const std::vector<float> &radii, size_t i) {
36 if (radii.empty()) {
37 return 1.0f;
38 }
39 return (i < radii.size()) ? radii[i] : radii[0];
40}
41
45static inline Color batch_color(const std::vector<Color> &colors, size_t i) {
46 if (colors.empty()) {
47 return Color(1.0f, 1.0f, 1.0f, 1.0f);
48 }
49 return (i < colors.size()) ? colors[i] : colors[0];
50}
51
60static inline void sphere_geometry_counts(int segments, size_t &num_verts,
61 size_t &num_tris) {
62 const size_t s = static_cast<size_t>(std::max(3, segments));
63 // North pole + (s - 1) body rings of s vertices + south pole.
64 num_verts = s * (s - 1u) + 2u;
65 // North cap (s) + (s - 2) body rings (2 * s each) + south cap (s).
66 num_tris = 2u * s + (s - 2u) * 2u * s;
67}
68
75static inline void cylinder_geometry_counts(int segments, bool caps,
76 size_t &num_verts,
77 size_t &num_tris) {
78 const size_t s = static_cast<size_t>(std::max(3, segments));
79 num_verts = 2u * s + (caps ? 2u * (s + 1u) : 0u);
80 num_tris = 2u * s + (caps ? 2u * s : 0u);
81}
82
83
84Mesh generate_sphere(const Vec3 &center, float radius, int segments,
85 const Color &color) {
86 segments = std::max(3, segments);
87 Mesh m;
88
89 if (segments < 2) {
90 return m;
91 }
92
93 int n_rings = std::max(2, segments);
95
97 m.vertices.push_back(center + Vec3(0.0f, radius, 0.0f));
98 m.colors.push_back(color);
99
100 for (int ring = 1; ring < n_rings; ++ring) {
101 float phi = glm::pi<float>() * static_cast<float>(ring) /
102 static_cast<float>(segments);
103 float y = radius * std::cos(phi);
104 float r = radius * std::sin(phi);
105
106 float step = glm::two_pi<float>() / static_cast<float>(verts_per_ring);
107 for (int j = 0; j < verts_per_ring; ++j) {
108 float theta = step * static_cast<float>(j);
109 m.vertices.push_back(
110 center + Vec3(r * std::cos(theta), y, r * std::sin(theta)));
111 m.colors.push_back(color);
112 }
113 }
114
115 uint32_t south_idx = static_cast<uint32_t>(m.vertices.size());
116 m.vertices.push_back(center + Vec3(0.0f, -radius, 0.0f));
117 m.colors.push_back(color);
118
119 m.normals.resize(m.vertices.size());
120 for (size_t i = 0; i < m.vertices.size(); ++i) {
121 m.normals[i] = glm::normalize(m.vertices[i] - center);
122 }
123
124 // North cap — outward-facing triangles (CCW from above)
125 for (int j = 0; j < verts_per_ring; ++j) {
126 uint32_t j_next = 1 + static_cast<uint32_t>((j + 1) % verts_per_ring);
127 m.triangles.push_back({north_idx, j_next, 1 + static_cast<uint32_t>(j)});
128 }
129
130 // Body rings
131 for (int ring = 0; ring < n_rings - 2; ++ring) {
132 uint32_t ring_start = 1 + static_cast<uint32_t>(ring) * verts_per_ring;
134 1 + static_cast<uint32_t>(ring + 1) * verts_per_ring;
135
136 for (int j = 0; j < verts_per_ring; ++j) {
137 uint32_t a = ring_start + j;
138 uint32_t b = ring_start + (j + 1) % verts_per_ring;
141
142 m.triangles.push_back({a, b, c});
143 m.triangles.push_back({a, c, d});
144 }
145 }
146
147 // South cap — outward-facing triangles (CCW from below)
149 1 + static_cast<uint32_t>(n_rings - 2) * verts_per_ring;
150 for (int j = 0; j < verts_per_ring; ++j) {
152 m.triangles.push_back({south_idx, last_start + j, j_next});
153 }
154
155 return m;
156}
157
158
160 int segments, const Color &color, bool caps) {
161 segments = std::max(3, segments);
162 Mesh m;
163
164 size_t expected_verts = 0, expected_tris = 0;
165 cylinder_geometry_counts(segments, caps, expected_verts, expected_tris);
166 m.vertices.reserve(expected_verts);
167 m.normals.reserve(expected_verts);
168 m.colors.reserve(expected_verts);
169 m.triangles.reserve(expected_tris);
170
171 Vec3 dir = glm::normalize(end - start);
172 Vec3 uu, vv;
173 make_basis(dir, uu, vv);
174
175 // Pre-calculate the radial vectors and positions for the rings
176 std::vector<Vec3> radials(segments);
177 std::vector<Vec3> bottom_ring(segments);
178 std::vector<Vec3> top_ring(segments);
179
180 float step = glm::two_pi<float>() / static_cast<float>(segments);
181 for (int i = 0; i < segments; ++i) {
182 float a = step * static_cast<float>(i);
183 radials[i] = std::cos(a) * uu + std::sin(a) * vv;
185 top_ring[i] = end + radius * radials[i];
186 }
187
188 // --- 1. THE BODY (Smooth shading radially) ---
189 for (int i = 0; i < segments; ++i) {
190 m.vertices.push_back(bottom_ring[i]);
191 m.normals.push_back(radials[i]);
192 m.colors.push_back(color);
193 }
194 for (int i = 0; i < segments; ++i) {
195 m.vertices.push_back(top_ring[i]);
196 m.normals.push_back(radials[i]);
197 m.colors.push_back(color);
198 }
199
200 for (int i = 0; i < segments; ++i) {
201 uint32_t a = i;
202 uint32_t b = (i + 1) % segments;
203 uint32_t c = segments + (i + 1) % segments;
204 uint32_t d = segments + i;
205
206 // FIXED: Swapped from {a, c, b} to {a, b, c} for CCW outward facing
207 m.triangles.push_back({a, b, c});
208 m.triangles.push_back({a, c, d});
209 }
210
211 if (caps) {
212 // --- 2. BOTTOM CAP (Flat shading, normal = -dir) ---
213 uint32_t bottom_cap_offset = static_cast<uint32_t>(m.vertices.size());
214
215 // Add the bottom center vertex
216 m.vertices.push_back(start);
217 m.normals.push_back(-dir);
218 m.colors.push_back(color);
219
220 // Add dedicated edge vertices for the bottom cap
221 for (int i = 0; i < segments; ++i) {
222 m.vertices.push_back(bottom_ring[i]);
223 m.normals.push_back(-dir); // Shared flat normal
224 m.colors.push_back(color);
225 }
226
227 for (int i = 0; i < segments; ++i) {
231
232 // Reversed CCW winding to face outward from the bottom
233 m.triangles.push_back({center_idx, next_edge_idx, edge_idx});
234 }
235
236 // --- 3. TOP CAP (Flat shading, normal = +dir) ---
237 uint32_t top_cap_offset = static_cast<uint32_t>(m.vertices.size());
238
239 // Add the top center vertex
240 m.vertices.push_back(end);
241 m.normals.push_back(dir);
242 m.colors.push_back(color);
243
244 // Add dedicated edge vertices for the top cap
245 for (int i = 0; i < segments; ++i) {
246 m.vertices.push_back(top_ring[i]);
247 m.normals.push_back(dir); // Shared flat normal
248 m.colors.push_back(color);
249 }
250
251 for (int i = 0; i < segments; ++i) {
255
256 // Standard CCW winding to face outward from the top
257 m.triangles.push_back({center_idx, edge_idx, next_edge_idx});
258 }
259 }
260
261 return m;
262}
263
264
265Mesh generate_cone(const Vec3 &base, const Vec3 &tip, float radius,
266 int segments, const Color &color) {
267 segments = std::max(3, segments);
268 Mesh m;
269
270 Vec3 dir_vec = tip - base;
271 float height = glm::length(dir_vec);
272
273 // Prevent division by zero if base and tip are identical
274 Vec3 dir = (height > 1e-8f) ? (dir_vec / height) : Vec3(0, 1, 0);
275
276 Vec3 uu, vv;
277 make_basis(dir, uu, vv);
278
279 float step = glm::two_pi<float>() / static_cast<float>(segments);
280
281 // --- 1. THE BODY (Smooth shading radially, duplicated tip) ---
283 for (int i = 0; i < segments; ++i) {
284 float a = step * static_cast<float>(i);
285 Vec3 radial = std::cos(a) * uu + std::sin(a) * vv;
286
287 // Calculate the mathematically perfect sloped normal for the cone surface
288 Vec3 slope_normal = glm::normalize(radial * height + dir * radius);
289
290 // Base ring vertex
291 m.vertices.push_back(base + radius * radial);
292 m.normals.push_back(slope_normal);
293 m.colors.push_back(color);
294
295 // Tip vertex (Duplicated for this specific slice to maintain the sloped normal)
296 m.vertices.push_back(tip);
297 m.normals.push_back(slope_normal);
298 m.colors.push_back(color);
299 }
300
301 for (int i = 0; i < segments; ++i) {
303 uint32_t tip_idx = body_offset + i * 2 + 1;
304 uint32_t next_base_idx = body_offset + ((i + 1) % segments) * 2;
305
306 // Outward facing CCW
307 m.triangles.push_back({base_idx, next_base_idx, tip_idx});
308 }
309
310 // --- 2. BOTTOM CAP (Flat shading, normal = -dir) ---
311 uint32_t cap_offset = static_cast<uint32_t>(m.vertices.size());
312
313 // Add the bottom center vertex
314 m.vertices.push_back(base);
315 m.normals.push_back(-dir);
316 m.colors.push_back(color);
317
318 // Add dedicated edge vertices for the bottom cap
319 for (int i = 0; i < segments; ++i) {
320 float a = step * static_cast<float>(i);
321 Vec3 radial = std::cos(a) * uu + std::sin(a) * vv;
322
323 m.vertices.push_back(base + radius * radial);
324 m.normals.push_back(-dir); // Shared flat normal pointing down
325 m.colors.push_back(color);
326 }
327
328 for (int i = 0; i < segments; ++i) {
331 uint32_t next_edge_idx = cap_offset + 1 + ((i + 1) % segments);
332
333 // Reversed CCW winding to face outward from the bottom
334 m.triangles.push_back({center_idx, next_edge_idx, edge_idx});
335 }
336
337 return m;
338}
339
340
341Mesh generate_arrow(const Vec3 &from, const Vec3 &to, float shaft_radius,
342 float head_radius, float head_length, int segments,
343 const Color &color) {
344 Vec3 dir_vec = to - from;
345 float total_len = glm::length(dir_vec);
346 if (total_len < 1e-8f)
347 return Mesh();
348
350 float hl = std::min(head_length, total_len * 0.8f);
351
352 Vec3 head_base = to - hl * dir;
353
356
358 return shaft;
359}
360
361void merge_mesh(Mesh &dst, const Mesh &src) {
362 uint32_t offset = static_cast<uint32_t>(dst.vertices.size());
363 dst.vertices.insert(dst.vertices.end(), src.vertices.begin(),
364 src.vertices.end());
365 dst.colors.insert(dst.colors.end(), src.colors.begin(), src.colors.end());
366 dst.normals.insert(dst.normals.end(), src.normals.begin(), src.normals.end());
367
368 for (const auto &tri : src.triangles) {
369 dst.triangles.push_back(
370 {tri.v0 + offset, tri.v1 + offset, tri.v2 + offset});
371 }
372}
373
374Mesh generate_multi_spheres(const std::vector<Vec3> &centers,
375 const std::vector<float> &radii,
376 const std::vector<Color> &colors,
377 int segments) {
378 Mesh result;
379 const size_t n = centers.size();
380 if (n == 0) {
381 return result;
382 }
383
384 // Reserve the exact final size up front: every sphere has the same
385 // geometry (same `segments`), so the total is n times the per-sphere count.
386 size_t verts_per_sphere = 0, tris_per_sphere = 0;
387 sphere_geometry_counts(segments, verts_per_sphere, tris_per_sphere);
388 result.vertices.reserve(n * verts_per_sphere);
389 result.normals.reserve(n * verts_per_sphere);
390 result.colors.reserve(n * verts_per_sphere);
391 result.triangles.reserve(n * tris_per_sphere);
392
393 for (size_t i = 0; i < n; ++i) {
394 float r = batch_radius(radii, i);
395 Color c = batch_color(colors, i);
398 }
399 return result;
400}
401
402Mesh generate_multi_cylinders(const std::vector<Vec3> &starts,
403 const std::vector<Vec3> &ends,
404 const std::vector<float> &radii,
405 const std::vector<Color> &colors,
406 int segments, bool caps) {
407 Mesh result;
408 const size_t n = starts.size();
409 if (n == 0) {
410 return result;
411 }
412
413 // Reserve the exact final size up front: every cylinder has the same
414 // geometry (same `segments`, same `caps`), so the total is n times the
415 // per-cylinder count.
416 size_t verts_per_cyl = 0, tris_per_cyl = 0;
417 cylinder_geometry_counts(segments, caps, verts_per_cyl, tris_per_cyl);
418 result.vertices.reserve(n * verts_per_cyl);
419 result.normals.reserve(n * verts_per_cyl);
420 result.colors.reserve(n * verts_per_cyl);
421 result.triangles.reserve(n * tris_per_cyl);
422
423 for (size_t i = 0; i < n; ++i) {
424 float r = batch_radius(radii, i);
425 Color c = batch_color(colors, i);
428 }
429 return result;
430}
431
437static inline void tube_geometry_counts(size_t num_path_points, int segments,
438 bool cap_start, bool cap_end,
439 size_t &num_verts, size_t &num_tris) {
440 const size_t s = static_cast<size_t>(std::max(3, segments));
441 if (num_path_points < 2u) {
442 num_verts = 0u;
443 num_tris = 0u;
444 return;
445 }
446 const size_t rings = num_path_points;
447 const size_t sides = rings - 1u;
448 num_verts = rings * s;
449 num_tris = sides * 2u * s;
450 if (cap_start) {
451 num_verts += 1u + s;
452 num_tris += s;
453 }
454 if (cap_end) {
455 num_verts += 1u + s;
456 num_tris += s;
457 }
458}
459
466static std::vector<Vec3> clean_tube_path(const std::vector<Vec3> &path,
467 float epsilon = 1e-6f) {
468 std::vector<Vec3> cleaned;
469 cleaned.reserve(path.size());
470 for (const Vec3 &p : path) {
471 if (cleaned.empty() || glm::length(p - cleaned.back()) > epsilon) {
472 cleaned.push_back(p);
473 }
474 }
475 return cleaned;
476}
477
483static inline Vec3 rotate_between(const Vec3 &v, const Vec3 &from_dir,
484 const Vec3 &to_dir) {
485 const Vec3 axis = glm::cross(from_dir, to_dir);
486 const float axis_len = glm::length(axis);
487 const float cos_angle = glm::dot(from_dir, to_dir);
488
489 if (axis_len < 1e-6f) {
490 if (cos_angle > 0.0f) {
491 return v; // parallel: nothing to do.
492 }
493 // Anti-parallel (180 degrees). The rotation axis is arbitrary but has
494 // to be perpendicular to the direction; Rodrigues reduces to a mirror.
495 const Vec3 helper = (std::abs(from_dir.x) < 0.9f) ? Vec3(1.0f, 0.0f, 0.0f)
496 : Vec3(0.0f, 1.0f, 0.0f);
497 const Vec3 perp = glm::normalize(glm::cross(from_dir, helper));
498 return -v + 2.0f * glm::dot(v, perp) * perp;
499 }
500
501 // Rodrigues' rotation formula, with sin = |cross| and cos = dot for unit
502 // input vectors.
503 const Vec3 k = axis / axis_len;
504 return v * cos_angle + glm::cross(k, v) * axis_len +
505 k * (glm::dot(k, v) * (1.0f - cos_angle));
506}
507
512static std::vector<Vec3> tube_path_tangents(const std::vector<Vec3> &pts) {
513 const size_t n = pts.size();
514 std::vector<Vec3> tangents(n);
515 for (size_t i = 0; i < n; ++i) {
516 Vec3 dir;
517 if (i == 0) {
518 dir = pts[1] - pts[0];
519 } else if (i + 1 == n) {
520 dir = pts[n - 1] - pts[n - 2];
521 } else {
522 dir = pts[i + 1] - pts[i - 1];
523 }
524 const float len = glm::length(dir);
525 // Note: glm::normalize() rather than dir / len, so that the geometry of
526 // a 2-point tube is bit-identical to the one of generate_cylinder().
527 tangents[i] = (len > 1e-8f) ? glm::normalize(dir) : tangents[i - 1];
528 }
529 return tangents;
530}
531
532Mesh generate_tube(const std::vector<Vec3> &path, float radius, int segments,
533 const Color &color, bool cap_start, bool cap_end) {
534 segments = std::max(3, segments);
535 Mesh m;
536
537 const std::vector<Vec3> pts = clean_tube_path(path);
538 if (pts.size() < 2u) {
539 return m; // a single point (or none) has no direction to sweep along.
540 }
541
542 const size_t num_rings = pts.size();
543 const size_t s = static_cast<size_t>(segments);
544
545 size_t expected_verts = 0, expected_tris = 0;
546 tube_geometry_counts(num_rings, segments, cap_start, cap_end,
548 m.vertices.reserve(expected_verts);
549 m.normals.reserve(expected_verts);
550 m.colors.reserve(expected_verts);
551 m.triangles.reserve(expected_tris);
552
553 const std::vector<Vec3> tangents = tube_path_tangents(pts);
554 const float step = glm::two_pi<float>() / static_cast<float>(segments);
555
556 // Build one ring per path point. The first frame comes from an arbitrary
557 // but stable basis; all following frames are transported from the previous
558 // one, which avoids the twisting that a per-point basis would introduce.
559 Vec3 u, v;
560 make_basis(tangents[0], u, v);
561
562 std::vector<Vec3> ring_basis_u(num_rings), ring_basis_v(num_rings);
563 ring_basis_u[0] = u;
564 ring_basis_v[0] = v;
565 for (size_t i = 1; i < num_rings; ++i) {
566 Vec3 transported = rotate_between(ring_basis_u[i - 1], tangents[i - 1],
567 tangents[i]);
568 // Re-orthogonalize against the new tangent to fight float drift.
570 const float len = glm::length(transported);
571 if (len < 1e-6f) {
572 // The transported vector collapsed (can happen for very sharp
573 // turns). Fall back to a fresh basis for this ring.
574 make_basis(tangents[i], ring_basis_u[i], ring_basis_v[i]);
575 continue;
576 }
578 ring_basis_v[i] = glm::cross(tangents[i], ring_basis_u[i]);
579 }
580
581 for (size_t i = 0; i < num_rings; ++i) {
582 for (int j = 0; j < segments; ++j) {
583 const float a = step * static_cast<float>(j);
584 const Vec3 radial = std::cos(a) * ring_basis_u[i] +
585 std::sin(a) * ring_basis_v[i];
586 m.vertices.push_back(pts[i] + radius * radial);
587 m.normals.push_back(radial);
588 m.colors.push_back(color);
589 }
590 }
591
592 // Side quads (same winding as the cylinder body of generate_cylinder()).
593 for (size_t i = 0; i + 1u < num_rings; ++i) {
594 for (int j = 0; j < segments; ++j) {
595 const uint32_t a = static_cast<uint32_t>(i * s + static_cast<size_t>(j));
596 const uint32_t b = static_cast<uint32_t>(i * s + static_cast<size_t>((j + 1) % segments));
597 const uint32_t c = static_cast<uint32_t>((i + 1u) * s + static_cast<size_t>((j + 1) % segments));
598 const uint32_t d = static_cast<uint32_t>((i + 1u) * s + static_cast<size_t>(j));
599 m.triangles.push_back({a, b, c});
600 m.triangles.push_back({a, c, d});
601 }
602 }
603
604 // Start cap: outward-facing normal is -tangent of the first ring.
605 if (cap_start) {
606 const uint32_t cap_offset = static_cast<uint32_t>(m.vertices.size());
607 const Vec3 cap_normal = -tangents[0];
608 m.vertices.push_back(pts[0]);
609 m.normals.push_back(cap_normal);
610 m.colors.push_back(color);
611 for (int j = 0; j < segments; ++j) {
612 const float a = step * static_cast<float>(j);
613 const Vec3 radial = std::cos(a) * ring_basis_u[0] +
614 std::sin(a) * ring_basis_v[0];
615 m.vertices.push_back(pts[0] + radius * radial);
616 m.normals.push_back(cap_normal);
617 m.colors.push_back(color);
618 }
619 for (int j = 0; j < segments; ++j) {
621 const uint32_t edge_idx = cap_offset + 1u + static_cast<uint32_t>(j);
622 const uint32_t next_edge_idx =
623 cap_offset + 1u + static_cast<uint32_t>((j + 1) % segments);
624 m.triangles.push_back({center_idx, next_edge_idx, edge_idx});
625 }
626 }
627
628 // End cap: outward-facing normal is +tangent of the last ring.
629 if (cap_end) {
630 const size_t last = num_rings - 1u;
631 const uint32_t cap_offset = static_cast<uint32_t>(m.vertices.size());
632 const Vec3 cap_normal = tangents[last];
633 m.vertices.push_back(pts[last]);
634 m.normals.push_back(cap_normal);
635 m.colors.push_back(color);
636 for (int j = 0; j < segments; ++j) {
637 const float a = step * static_cast<float>(j);
638 const Vec3 radial = std::cos(a) * ring_basis_u[last] +
639 std::sin(a) * ring_basis_v[last];
640 m.vertices.push_back(pts[last] + radius * radial);
641 m.normals.push_back(cap_normal);
642 m.colors.push_back(color);
643 }
644 for (int j = 0; j < segments; ++j) {
646 const uint32_t edge_idx = cap_offset + 1u + static_cast<uint32_t>(j);
647 const uint32_t next_edge_idx =
648 cap_offset + 1u + static_cast<uint32_t>((j + 1) % segments);
649 m.triangles.push_back({center_idx, edge_idx, next_edge_idx});
650 }
651 }
652
653 return m;
654}
655
656Mesh generate_multi_tubes(const std::vector<std::vector<Vec3>> &paths,
657 const std::vector<float> &radii,
658 const std::vector<Color> &colors,
659 int segments, bool caps) {
660 Mesh result;
661 const size_t n = paths.size();
662 if (n == 0) {
663 return result;
664 }
665
666 // Unlike spheres/cylinders, tubes can differ in length, so the exact total
667 // has to be summed over the paths. Cleaning the paths here duplicates a
668 // little work in generate_tube(), but keeps the reservation exact.
669 size_t total_verts = 0, total_tris = 0;
670 for (const auto &path : paths) {
671 size_t path_verts = 0, path_tris = 0;
672 tube_geometry_counts(clean_tube_path(path).size(), segments, caps, caps,
676 }
677 result.vertices.reserve(total_verts);
678 result.normals.reserve(total_verts);
679 result.colors.reserve(total_verts);
680 result.triangles.reserve(total_tris);
681
682 for (size_t i = 0; i < n; ++i) {
683 const float r = batch_radius(radii, i);
684 const Color c = batch_color(colors, i);
686 }
687 return result;
688}
689
690Mesh generate_cuboid(const Vec3 &center, const Vec3 &half,
691 const Color &color) {
692 Mesh m;
693
694 // 6 faces * 4 vertices = 24 distinct vertices
695 m.vertices.reserve(24);
696 m.normals.reserve(24);
697 m.colors.assign(24, color);
698 m.triangles.reserve(12);
699
700 float x = half.x, y = half.y, z = half.z;
701
702 // The 8 unique spatial positions (kept exactly as you had them)
703 Vec3 v[8] = {
704 center + Vec3(-x, -y, -z), // 0: Bottom-Left-Back
705 center + Vec3( x, -y, -z), // 1: Bottom-Right-Back
706 center + Vec3( x, y, -z), // 2: Top-Right-Back
707 center + Vec3(-x, y, -z), // 3: Top-Left-Back
708 center + Vec3(-x, -y, z), // 4: Bottom-Left-Front
709 center + Vec3( x, -y, z), // 5: Bottom-Right-Front
710 center + Vec3( x, y, z), // 6: Top-Right-Front
711 center + Vec3(-x, y, z), // 7: Top-Left-Front
712 };
713
714 // Define the 6 faces using the 8 positions, plus the perfect normal for that face
715 struct Face {
716 int v0, v1, v2, v3;
717 Vec3 normal;
718 };
719
720 Face faces[6] = {
721 {4, 5, 6, 7, Vec3( 0, 0, 1)}, // Front
722 {1, 0, 3, 2, Vec3( 0, 0, -1)}, // Back
723 {0, 1, 5, 4, Vec3( 0, -1, 0)}, // Bottom
724 {7, 6, 2, 3, Vec3( 0, 1, 0)}, // Top
725 {1, 2, 6, 5, Vec3( 1, 0, 0)}, // Right
726 {0, 4, 7, 3, Vec3(-1, 0, 0)} // Left
727 };
728
729 uint32_t index = 0;
730 for (int i = 0; i < 6; i++) {
731 // Push the 4 distinct vertices for this face
732 m.vertices.push_back(v[faces[i].v0]);
733 m.vertices.push_back(v[faces[i].v1]);
734 m.vertices.push_back(v[faces[i].v2]);
735 m.vertices.push_back(v[faces[i].v3]);
736
737 // Push the perfectly flat normal 4 times
738 for (int j = 0; j < 4; j++) {
739 m.normals.push_back(faces[i].normal);
740 }
741
742 // Create the two triangles for this quad using our new unrolled indices
743 m.triangles.push_back({index, index + 1, index + 2});
744 m.triangles.push_back({index, index + 2, index + 3});
745
746 index += 4;
747 }
748
749 return m;
750}
751
752
754 float hw, const Color &color) {
755 Mesh m;
756
757 // 4 sides (3 verts each) + 1 square base (4 verts) = 16 distinct vertices
758 m.vertices.reserve(16);
759 m.normals.reserve(16);
760 m.colors.assign(16, color);
761 m.triangles.reserve(6);
762
763 // Define the base corner positions
764 Vec3 p0 = base_center + Vec3(-hw, 0, -hw); // Back-Left
765 Vec3 p1 = base_center + Vec3( hw, 0, -hw); // Back-Right
766 Vec3 p2 = base_center + Vec3( hw, 0, hw); // Front-Right
767 Vec3 p3 = base_center + Vec3(-hw, 0, hw); // Front-Left
768
769 // 1. GENERATE THE 4 SIDE FACES (Unrolled for sharp edges)
770 std::vector<std::array<Vec3, 3>> side_faces = {
771 {p1, p0, apex}, // Back side
772 {p2, p1, apex}, // Right side
773 {p3, p2, apex}, // Front side
774 {p0, p3, apex} // Left side
775 };
776
777 uint32_t index = 0;
778 for (const auto& face : side_faces) {
779 Vec3 A = face[0], B = face[1], C = face[2];
780
781 // Calculate exact perpendicular normal for this side
782 Vec3 normal = glm::normalize(glm::cross(B - A, C - A));
783
784 // Push 3 dedicated vertices for this face
785 m.vertices.push_back(A);
786 m.vertices.push_back(B);
787 m.vertices.push_back(C);
788
789 m.normals.push_back(normal);
790 m.normals.push_back(normal);
791 m.normals.push_back(normal);
792
793 m.triangles.push_back({index, index + 1, index + 2});
794 index += 3;
795 }
796
797 // 2. GENERATE THE BASE QUAD (4 shared vertices, identical straight-down normal)
798 Vec3 base_normal = Vec3(0.0f, -1.0f, 0.0f);
799
800 m.vertices.push_back(p0); // index 12
801 m.vertices.push_back(p1); // index 13
802 m.vertices.push_back(p2); // index 14
803 m.vertices.push_back(p3); // index 15
804
805 for (int i = 0; i < 4; i++) {
806 m.normals.push_back(base_normal);
807 }
808
809 // Two CCW triangles forming the square base (viewed from below)
810 m.triangles.push_back({index, index + 2, index + 3}); // {p0, p2, p3}
811 m.triangles.push_back({index, index + 1, index + 2}); // {p0, p1, p2}
812
813 return m;
814}
815
817 const Vec3 &p2, const Vec3 &p3,
818 const Color &color) {
819 Mesh m;
820
821 // 4 faces * 3 vertices per face = 12 distinct vertices
822 m.vertices.reserve(12);
823 m.normals.reserve(12);
824 m.colors.assign(12, color);
825 m.triangles.reserve(4);
826
827 Vec3 centroid = (p0 + p1 + p2 + p3) * 0.25f;
828
829 std::vector<std::array<Vec3, 3>> faces = {
830 {p0, p1, p2}, {p0, p2, p3}, {p0, p3, p1}, {p1, p3, p2}
831 };
832
833 uint32_t index = 0;
834 for (const auto& face : faces) {
835 Vec3 A = face[0], B = face[1], C = face[2];
836 Vec3 normal = glm::normalize(glm::cross(B - A, C - A));
837
838 // Flip normal and vertex order if facing inward
839 if (glm::dot(normal, A - centroid) < 0.0f) {
840 normal = -normal;
841 std::swap(B, C);
842 }
843
844 // Push 3 distinct vertices for this specific face
845 m.vertices.push_back(A);
846 m.vertices.push_back(B);
847 m.vertices.push_back(C);
848
849 // All 3 vertices share the exact same perpendicular face normal
850 m.normals.push_back(normal);
851 m.normals.push_back(normal);
852 m.normals.push_back(normal);
853
854 m.triangles.push_back({index, index + 1, index + 2});
855 index += 3;
856 }
857
858 return m;
859}
860
861
862Mesh generate_torus(const Vec3 &center, float R, float r,
863 int seg_major, int seg_minor, const Color &color) {
864 Mesh m;
865
866 // We can pre-allocate memory for slight performance gains
868 m.vertices.reserve(total_vertices);
869 m.normals.reserve(total_vertices);
870 m.colors.reserve(total_vertices);
871 m.triangles.reserve(total_vertices * 2);
872
873 // 1. Generate Vertices, Colors, and Normals
874 for (int i = 0; i < seg_major; i++) {
875 float phi = glm::two_pi<float>() * float(i) / float(seg_major);
876 float cp = std::cos(phi), sp = std::sin(phi);
877
878 // Calculate the center of the current tube ring (minor center)
879 Vec3 minor_center = center + Vec3(R * cp, 0.0f, R * sp);
880
881 for (int j = 0; j < seg_minor; j++) {
882 float theta = glm::two_pi<float>() * float(j) / float(seg_minor);
883 float ct = std::cos(theta), st = std::sin(theta);
884
885 float px = (R + r * ct) * cp;
886 float py = r * st;
887 float pz = (R + r * ct) * sp;
888
889 Vec3 pos = center + Vec3(px, py, pz);
890 m.vertices.push_back(pos);
891 m.colors.push_back(color);
892
893 // The normal is simply the direction from the tube's center to the vertex
894 m.normals.push_back(glm::normalize(pos - minor_center));
895 }
896 }
897
898 // 2. Generate Triangles (with fixed, outward-facing winding order)
899 for (int i = 0; i < seg_major; i++) {
900 int ni = (i + 1) % seg_major;
901
902 for (int j = 0; j < seg_minor; j++) {
903 int nj = (j + 1) % seg_minor;
904
905 uint32_t a = i * seg_minor + j; // Current ring, current slice
906 uint32_t b = ni * seg_minor + j; // Next ring, current slice
907 uint32_t c = ni * seg_minor + nj; // Next ring, next slice
908 uint32_t d = i * seg_minor + nj; // Current ring, next slice
909
910 // FIXED: Flipped winding order so the torus renders from the outside
911 m.triangles.push_back({a, d, c});
912 m.triangles.push_back({a, c, b});
913 }
914 }
915
916 return m;
917}
918
919Mesh generate_plane(const Vec3 &center, const Vec3 &normal,
920 float hx, float hy, const Color &color) {
921 Mesh m;
922 Vec3 n = glm::length(normal) > 1e-9f ? glm::normalize(normal) : Vec3(0,0,1);
923 Vec3 u, v;
924 if (std::abs(n.x) < 0.9f) u = glm::normalize(glm::cross(n, Vec3(1,0,0)));
925 else u = glm::normalize(glm::cross(n, Vec3(0,1,0)));
926 v = glm::cross(n, u);
927
928 // Calculate the 4 corner positions
929 Vec3 p0 = center - u * hx - v * hy; // Bottom-Left
930 Vec3 p1 = center + u * hx - v * hy; // Bottom-Right
931 Vec3 p2 = center + u * hx + v * hy; // Top-Right
932 Vec3 p3 = center - u * hx + v * hy; // Top-Left
933
934 m.vertices.reserve(8);
935 m.normals.reserve(8);
936 m.colors.reserve(8);
937 m.triangles.reserve(4);
938
939 // --- 1. FRONT FACE (Normal = +n) ---
940 m.vertices.insert(m.vertices.end(), {p0, p1, p2, p3});
941 for (int i = 0; i < 4; i++) {
942 m.normals.push_back(n);
943 m.colors.push_back(color);
944 }
945 // Standard CCW winding for the front
946 m.triangles.push_back({0, 1, 2});
947 m.triangles.push_back({0, 2, 3});
948
949 // --- 2. BACK FACE (Normal = -n) ---
950 m.vertices.insert(m.vertices.end(), {p0, p1, p2, p3});
951 for (int i = 0; i < 4; i++) {
952 m.normals.push_back(-n);
953 m.colors.push_back(color);
954 }
955 // Reversed CCW winding so the triangles face outward from the back
956 m.triangles.push_back({4, 6, 5});
957 m.triangles.push_back({4, 7, 6});
958
959 return m;
960}
961
962} // namespace scimesh
Low-level math utilities for the rendering pipeline.
Mesh generate_multi_tubes(const std::vector< std::vector< Vec3 > > &paths, const std::vector< float > &radii, const std::vector< Color > &colors, int segments, bool caps)
Generate multiple tubes in a single mesh (efficient batching).
Mesh generate_pyramid(const Vec3 &base_center, const Vec3 &apex, float hw, const Color &color)
Generate a square-based pyramid.
Mesh generate_tetrahedron(const Vec3 &p0, const Vec3 &p1, const Vec3 &p2, const Vec3 &p3, const Color &color)
Generate a tetrahedron (triangular pyramid) from four points.
Mesh generate_sphere(const Vec3 &center, float radius, int segments, const Color &color)
Generate a UV-sphere (latitude/longitude tessellation).
glm::vec3 Vec3
3-component floating-point vector (xyz).
Definition types.h:46
Mesh generate_cuboid(const Vec3 &center, const Vec3 &half, const Color &color)
Generate an axis-aligned cuboid (rectangular box).
void merge_mesh(Mesh &dst, const Mesh &src)
Merge one mesh into another (append geometry).
Mesh generate_arrow(const Vec3 &from, const Vec3 &to, float shaft_radius, float head_radius, float head_length, int segments, const Color &color)
Generate a 3D arrow from from to to.
Mesh generate_torus(const Vec3 &center, float R, float r, int seg_major, int seg_minor, const Color &color)
Generate a torus (donut shape).
Mesh generate_multi_spheres(const std::vector< Vec3 > &centers, const std::vector< float > &radii, const std::vector< Color > &colors, int segments)
Generate multiple spheres in a single mesh (efficient batching).
Mesh generate_cylinder(const Vec3 &start, const Vec3 &end, float radius, int segments, const Color &color, bool caps)
Generate a cylinder between two endpoints.
Mesh generate_multi_cylinders(const std::vector< Vec3 > &starts, const std::vector< Vec3 > &ends, const std::vector< float > &radii, const std::vector< Color > &colors, int segments, bool caps)
Generate multiple cylinders in a single mesh (efficient batching).
Mesh generate_plane(const Vec3 &center, const Vec3 &normal, float hx, float hy, const Color &color)
Generate a flat rectangular plane.
Mesh generate_cone(const Vec3 &base, const Vec3 &tip, float radius, int segments, const Color &color)
Generate a cone from a base circle to a tip point.
Mesh generate_tube(const std::vector< Vec3 > &path, float radius, int segments, const Color &color, bool cap_start, bool cap_end)
Generate a tube (generalized cylinder) along a polyline path.
Compute per-vertex surface normals for lighting.
Procedural geometry generators.
An RGBA color with floating-point components.
Definition types.h:88
A 3D triangle mesh using an indexed face set representation.
Definition mesh.h:76
std::vector< Vec3 > vertices
3D vertex positions.
Definition mesh.h:86
int y
Top edge of the bitmap, in image pixels.
Definition text.cpp:224
int x
Left edge of the bitmap, in image pixels.
Definition text.cpp:223