1.3.77
 
Loading...
Searching...
No Matches
CollisionDetection.cpp
Go to the documentation of this file.
1
16#include "CollisionDetection.h"
17#include <atomic>
18#include <functional>
19#include <limits>
20#include <queue>
21
22#ifdef _OPENMP
23#include <omp.h>
24#endif
25
26// SIMD headers
27#ifdef __AVX2__
28#include <immintrin.h>
29#elif defined(__SSE4_1__)
30#include <smmintrin.h>
31#elif defined(__SSE2__)
32#include <emmintrin.h>
33#endif
34
35#ifdef HELIOS_CUDA_AVAILABLE
36#include <cuda_runtime.h>
37#endif
38
39using namespace helios;
40
41#ifdef HELIOS_CUDA_AVAILABLE
42// GPU BVH node structure (must match the one in .cu file)
43struct GPUBVHNode {
44 float3 aabb_min, aabb_max;
45 unsigned int left_child, right_child;
46 unsigned int primitive_start, primitive_count;
47 unsigned int is_leaf, padding;
48};
49
50// External CUDA functions
51extern "C" {
52void launchBVHTraversal(void *h_nodes, int node_count, unsigned int *h_primitive_indices, int primitive_count, float *h_primitive_aabb_min, float *h_primitive_aabb_max, float *h_query_aabb_min, float *h_query_aabb_max, int num_queries,
53 unsigned int *h_results, unsigned int *h_result_counts, int max_results_per_query);
54bool launchVoxelRayPathLengths(int num_rays, float *h_ray_origins, float *h_ray_directions, float grid_center_x, float grid_center_y, float grid_center_z, float grid_size_x, float grid_size_y, float grid_size_z, int grid_divisions_x,
55 int grid_divisions_y, int grid_divisions_z, int primitive_count, int *h_voxel_ray_counts, float *h_voxel_path_lengths, int *h_voxel_transmitted, int *h_voxel_hit_before, int *h_voxel_hit_after, int *h_voxel_hit_inside);
56// Warp-efficient GPU kernels
57void launchWarpEfficientBVH(void *h_bvh_soa_gpu, unsigned int *h_primitive_indices, int primitive_count, float *h_primitive_aabb_min, float *h_primitive_aabb_max, float *h_ray_origins, float *h_ray_directions, float *h_ray_max_distances,
58 int num_rays, unsigned int *h_results, unsigned int *h_result_counts, int max_results_per_ray);
59}
60
61// Helper function to convert helios::vec3 to float3
62inline float3 heliosVecToFloat3(const helios::vec3 &v) {
63 return make_float3(v.x, v.y, v.z);
64}
65#endif
66
68
69 if (a_context == nullptr) {
70 helios_runtime_error("ERROR (CollisionDetection::CollisionDetection): Context is null");
71 }
72
73 context = a_context;
74 printmessages = true;
75
76#ifdef HELIOS_CUDA_AVAILABLE
77 gpu_acceleration_enabled = isGPUAvailable();
78#else
79 gpu_acceleration_enabled = false;
80#endif
81
82 // Initialize GPU memory pointers
83 d_bvh_nodes = nullptr;
84 d_primitive_indices = nullptr;
85 d_primitive_types = nullptr;
86 d_primitive_vertices = nullptr;
87 d_vertex_offsets = nullptr;
88 d_mask_data = nullptr;
89 d_mask_offsets = nullptr;
90 d_mask_sizes = nullptr;
91 d_mask_IDs = nullptr;
92 d_uv_data = nullptr;
93 d_uv_IDs = nullptr;
94 d_gpu_has_masks = false;
95 d_gpu_node_count = 0;
96 d_gpu_primitive_count = 0;
97 d_gpu_total_vertex_count = 0;
98 gpu_memory_allocated = false;
99
100 // Initialize BVH caching variables
101 bvh_dirty = true;
102 soa_dirty = true; // SoA needs initial build
103 automatic_bvh_rebuilds = true; // Default: allow automatic rebuilds
104
105 // Initialize hierarchical BVH variables
106 hierarchical_bvh_enabled = false;
107 static_bvh_valid = false;
108
109 // Initialize tree-based BVH variables
110 tree_based_bvh_enabled = false;
111 tree_isolation_distance = 5.0f; // Default 5 meter isolation distance
112 obstacle_spatial_grid_initialized = false;
113
114// Issue warning if OpenMP not available
115#ifndef _OPENMP
116 static bool openmp_warning_issued = false;
117 if (printmessages && !openmp_warning_issued) {
118 std::cout << "WARNING (CollisionDetection): OpenMP not available. Using serial CPU implementation. "
119 << "Performance will be significantly slower. Consider installing OpenMP for parallel execution." << std::endl;
120 openmp_warning_issued = true;
121 }
122#endif
123
124 // Initialize grid parameters
125 grid_center = make_vec3(0, 0, 0);
126 grid_size = make_vec3(1, 1, 1);
127 grid_divisions = helios::make_int3(1, 1, 1);
128
129 // Initialize voxel data parameters
130 voxel_data_initialized = false;
131 voxel_grid_center = make_vec3(0, 0, 0);
132 voxel_grid_size = make_vec3(1, 1, 1);
133 voxel_grid_divisions = helios::make_int3(1, 1, 1);
134 use_flat_arrays = false; // Default to false, enabled during initialization
135
136 // Initialize spatial optimization parameters
137 max_collision_distance = 10.0f; // Default 10 meter maximum distance
138}
139
141#ifdef HELIOS_CUDA_AVAILABLE
142 freeGPUMemory();
143#endif
144}
145
146std::vector<uint> CollisionDetection::findCollisions(uint UUID, bool allow_spatial_culling) {
147 return findCollisions(std::vector<uint>{UUID}, allow_spatial_culling);
148}
149
150std::vector<uint> CollisionDetection::findCollisions(const std::vector<uint> &UUIDs, bool allow_spatial_culling) {
151
153 warnings.setEnabled(printmessages);
154
155 if (UUIDs.empty()) {
156 warnings.addWarning("no_uuids_provided", "No UUIDs provided");
157 warnings.report(std::cerr);
158 return {};
159 }
160
161 // Validate UUIDs - throw exception if any are invalid
162 std::vector<uint> valid_UUIDs;
163 for (uint uuid: UUIDs) {
164 if (context->doesPrimitiveExist(uuid)) {
165 valid_UUIDs.push_back(uuid);
166 } else {
167 helios_runtime_error("ERROR (CollisionDetection::findCollisions): Invalid UUID " + std::to_string(uuid) + " provided");
168 }
169 }
170
171 // Automatically rebuild BVH if geometry has changed or BVH is empty
172 ensureBVHCurrent();
173
174 std::vector<uint> all_collisions;
175
176 for (uint UUID: valid_UUIDs) {
177
178 // Get bounding box for query primitive
179 if (!context->doesPrimitiveExist(UUID)) {
180 continue; // Skip invalid primitive
181 }
182 vec3 aabb_min, aabb_max;
183 context->getPrimitiveBoundingBox(UUID, aabb_min, aabb_max);
184
185 std::vector<uint> collisions;
186
187#ifdef HELIOS_CUDA_AVAILABLE
188 if (gpu_acceleration_enabled && gpu_memory_allocated) {
189 collisions = traverseBVH_GPU(aabb_min, aabb_max);
190 } else {
191 collisions = traverseBVH_CPU(aabb_min, aabb_max);
192 }
193#else
194 collisions = traverseBVH_CPU(aabb_min, aabb_max);
195#endif
196
197 // Remove the query UUID from results
198 collisions.erase(std::remove(collisions.begin(), collisions.end(), UUID), collisions.end());
199
200 // Add to overall results
201 all_collisions.insert(all_collisions.end(), collisions.begin(), collisions.end());
202 }
203
204 // Remove duplicates
205 std::sort(all_collisions.begin(), all_collisions.end());
206 all_collisions.erase(std::unique(all_collisions.begin(), all_collisions.end()), all_collisions.end());
207
208 return all_collisions;
209}
210
211std::vector<uint> CollisionDetection::findCollisions(const std::vector<uint> &primitive_UUIDs, const std::vector<uint> &object_IDs, bool allow_spatial_culling) {
212
214 warnings.setEnabled(printmessages);
215
216 if (primitive_UUIDs.empty() && object_IDs.empty()) {
217 warnings.addWarning("no_inputs_provided", "No UUIDs or object IDs provided");
218 warnings.report(std::cerr);
219 return {};
220 }
221
222 // Expand object IDs to their constituent primitive UUIDs
223 std::vector<uint> all_test_UUIDs = primitive_UUIDs;
224
225 for (uint ObjID: object_IDs) {
226 if (!context->doesObjectExist(ObjID)) {
227 helios_runtime_error("ERROR (CollisionDetection::findCollisions): Object ID " + std::to_string(ObjID) + " does not exist");
228 }
229
230 std::vector<uint> object_UUIDs = context->getObjectPrimitiveUUIDs(ObjID);
231 all_test_UUIDs.insert(all_test_UUIDs.end(), object_UUIDs.begin(), object_UUIDs.end());
232 }
233
234 return findCollisions(all_test_UUIDs, allow_spatial_culling);
235}
236
237std::vector<uint> CollisionDetection::findCollisions(const std::vector<uint> &query_UUIDs, const std::vector<uint> &query_object_IDs, const std::vector<uint> &target_UUIDs, const std::vector<uint> &target_object_IDs, bool allow_spatial_culling) {
238
240 warnings.setEnabled(printmessages);
241
242 if (query_UUIDs.empty() && query_object_IDs.empty()) {
243 warnings.addWarning("no_query_inputs", "No query UUIDs or object IDs provided");
244 warnings.report(std::cerr);
245 return {};
246 }
247
248 // Expand query objects to their constituent primitive UUIDs
249 std::vector<uint> all_query_UUIDs = query_UUIDs;
250
251 for (uint ObjID: query_object_IDs) {
252 if (!context->doesObjectExist(ObjID)) {
253 helios_runtime_error("ERROR (CollisionDetection::findCollisions): Query object ID " + std::to_string(ObjID) + " does not exist");
254 }
255
256 std::vector<uint> object_UUIDs = context->getObjectPrimitiveUUIDs(ObjID);
257 all_query_UUIDs.insert(all_query_UUIDs.end(), object_UUIDs.begin(), object_UUIDs.end());
258 }
259
260 // Validate query UUIDs
261 if (!validateUUIDs(all_query_UUIDs)) {
262 helios_runtime_error("ERROR (CollisionDetection::findCollisions): One or more invalid query UUIDs provided");
263 }
264
265 // Build restricted BVH if target geometry is specified
266
267 if (target_UUIDs.empty() && target_object_IDs.empty()) {
268 // Use all geometry in context as target
269 ensureBVHCurrent();
270 } else {
271 // Build restricted BVH with only target geometry
272 std::vector<uint> all_target_UUIDs = target_UUIDs;
273
274 for (uint ObjID: target_object_IDs) {
275 if (!context->doesObjectExist(ObjID)) {
276 helios_runtime_error("ERROR (CollisionDetection::findCollisions): Target object ID " + std::to_string(ObjID) + " does not exist");
277 }
278
279 std::vector<uint> object_UUIDs = context->getObjectPrimitiveUUIDs(ObjID);
280 all_target_UUIDs.insert(all_target_UUIDs.end(), object_UUIDs.begin(), object_UUIDs.end());
281 }
282
283 // OPTIMIZATION: Use per-tree BVH if enabled for better scaling
284 if (tree_based_bvh_enabled && allow_spatial_culling && !all_query_UUIDs.empty()) {
285 // Get tree-relevant geometry instead of using all targets
286 helios::vec3 query_center = helios::vec3(0, 0, 0);
287 if (!all_query_UUIDs.empty()) {
288 // Calculate approximate query center from first query primitive
289 helios::vec3 min_corner, max_corner;
290 context->getPrimitiveBoundingBox(all_query_UUIDs[0], min_corner, max_corner);
291 query_center = (min_corner + max_corner) * 0.5f;
292 }
293
294 // Use the configured tree isolation distance (collision cone height)
295 std::vector<uint> effective_targets = getRelevantGeometryForTree(query_center, all_query_UUIDs, tree_isolation_distance);
296
297 all_target_UUIDs = effective_targets;
298 }
299
300 // Validate target UUIDs
301 if (!all_target_UUIDs.empty() && !validateUUIDs(all_target_UUIDs)) {
302 helios_runtime_error("ERROR (CollisionDetection::findCollisions): One or more invalid target UUIDs provided");
303 }
304
305 // Build BVH with only the target geometry (with caching)
306 if (!all_target_UUIDs.empty()) {
307 updateBVH(all_target_UUIDs, false); // Use caching logic instead of direct rebuild
308 }
309 }
310
311 // Perform collision detection using the same logic as the standard findCollisions
312 std::vector<uint> all_collisions;
313
314 for (uint UUID: all_query_UUIDs) {
315
316 // Get bounding box for query primitive
317 if (!context->doesPrimitiveExist(UUID)) {
318 continue; // Skip invalid primitive
319 }
320 vec3 aabb_min, aabb_max;
321 context->getPrimitiveBoundingBox(UUID, aabb_min, aabb_max);
322
323 std::vector<uint> collisions;
324
325#ifdef HELIOS_CUDA_AVAILABLE
326 if (gpu_acceleration_enabled && gpu_memory_allocated) {
327 collisions = traverseBVH_GPU(aabb_min, aabb_max);
328 } else {
329 collisions = traverseBVH_CPU(aabb_min, aabb_max);
330 }
331#else
332 collisions = traverseBVH_CPU(aabb_min, aabb_max);
333#endif
334
335 // Remove the query UUID from results
336 collisions.erase(std::remove(collisions.begin(), collisions.end(), UUID), collisions.end());
337
338 // Add to overall results
339 all_collisions.insert(all_collisions.end(), collisions.begin(), collisions.end());
340 }
341
342 // Remove duplicates
343 std::sort(all_collisions.begin(), all_collisions.end());
344 all_collisions.erase(std::unique(all_collisions.begin(), all_collisions.end()), all_collisions.end());
345
346 warnings.report(std::cerr);
347 return all_collisions;
348}
349
350void CollisionDetection::buildBVH(const std::vector<uint> &UUIDs) {
351
352 // Create warning aggregator
354 warnings.setEnabled(printmessages);
355
356 std::vector<uint> primitives_to_include;
357
358 if (UUIDs.empty()) {
359 // Include all primitives in context
360 primitives_to_include = context->getAllUUIDs();
361 } else {
362 primitives_to_include = UUIDs;
363 }
364
365
366 if (primitives_to_include.empty()) {
367 warnings.addWarning("no_primitives_for_bvh", "No primitives found to build BVH");
368 warnings.report(std::cerr);
369 return;
370 }
371
372 // Validate UUIDs - throw exception if any are invalid (only when specific UUIDs are provided)
373 std::vector<uint> valid_primitives;
374 if (!UUIDs.empty()) {
375 // When specific UUIDs are provided, they must all be valid
376 for (uint uuid: primitives_to_include) {
377 if (context->doesPrimitiveExist(uuid)) {
378 valid_primitives.push_back(uuid);
379 } else {
380 helios_runtime_error("ERROR (CollisionDetection::buildBVH): Invalid UUID " + std::to_string(uuid) + " provided");
381 }
382 }
383 } else {
384 // When no specific UUIDs provided (use all), filter out invalid ones
385 for (uint uuid: primitives_to_include) {
386 if (context->doesPrimitiveExist(uuid)) {
387 valid_primitives.push_back(uuid);
388 } else {
389 warnings.addWarning("invalid_uuid_skipped", "Skipping invalid UUID " + std::to_string(uuid));
390 }
391 }
392
393 if (valid_primitives.empty()) {
394 warnings.addWarning("no_valid_primitives_after_filtering", "No valid primitives found after filtering");
395 warnings.report(std::cerr);
396 return;
397 }
398 }
399
400 primitives_to_include = valid_primitives;
401
402 // Check if the primitive set has actually changed before clearing cache
403 std::set<uint> new_primitive_set(primitives_to_include.begin(), primitives_to_include.end());
404 std::set<uint> old_primitive_set(primitive_indices.begin(), primitive_indices.end());
405
406 bool primitive_set_changed = (new_primitive_set != old_primitive_set);
407
408 if (primitive_set_changed) {
409 // Clear primitive cache only when primitive set changes - CRITICAL for performance
410 primitive_cache.clear();
411 }
412
413 // Clear existing BVH
414 bvh_nodes.clear();
415 primitive_indices.clear();
416
417 // Copy primitives to indices array
418 primitive_indices = primitives_to_include;
419
420 // Pre-allocate BVH nodes to avoid excessive resizing
421 // For N primitives, we need at most 2*N-1 nodes for a complete binary tree
422 size_t max_nodes = std::max(size_t(1), 2 * primitives_to_include.size());
423 bvh_nodes.clear();
424 bvh_nodes.resize(max_nodes); // Pre-allocate ALL nodes at once to avoid any resizing
425 next_available_node_index = 1; // Track next available node (0 is root)
426
427 // OPTIMIZATION: Pre-cache bounding boxes with dirty flagging to avoid repeated expensive calculations
428 // Only clear cache for primitives that no longer exist or are dirty
429 std::unordered_set<uint> current_primitives(primitives_to_include.begin(), primitives_to_include.end());
430
431 // Remove cached entries for primitives that no longer exist
432 auto cache_it = primitive_aabbs_cache.begin();
433 while (cache_it != primitive_aabbs_cache.end()) {
434 if (current_primitives.find(cache_it->first) == current_primitives.end()) {
435 cache_it = primitive_aabbs_cache.erase(cache_it);
436 } else {
437 ++cache_it;
438 }
439 }
440
441 // Update only dirty or missing cache entries
442 for (uint UUID: primitives_to_include) {
443 if (!context->doesPrimitiveExist(UUID)) {
444 continue; // Skip invalid primitive
445 }
446
447 // Only update if not cached or marked as dirty
448 bool needs_update = (primitive_aabbs_cache.find(UUID) == primitive_aabbs_cache.end()) || (dirty_primitive_cache.find(UUID) != dirty_primitive_cache.end());
449
450 if (needs_update) {
451 vec3 aabb_min, aabb_max;
452 context->getPrimitiveBoundingBox(UUID, aabb_min, aabb_max);
453 primitive_aabbs_cache[UUID] = {aabb_min, aabb_max};
454 dirty_primitive_cache.erase(UUID); // Mark as clean
455 }
456 }
457
458 // Build BVH recursively starting from root
459 buildBVHRecursive(0, 0, primitive_indices.size(), 0);
460
461 // Resize to actual nodes used (much more efficient than shrink_to_fit)
462 bvh_nodes.resize(next_available_node_index);
463
464 // Clear any stale optimized structures since we just rebuilt the BVH
465 // They will be rebuilt on demand by ensureOptimizedBVH() when needed
466 bvh_nodes_soa.clear();
467 bvh_nodes_soa.node_count = 0;
468
469 // The dense, BVH-leaf-ordered primitive cache is keyed by primitive_indices order, which buildBVHRecursive
470 // just reordered (even if the primitive set is unchanged and primitive_cache was kept). Drop it so the next
471 // ray cast rebuilds it in the new order via ensurePrimitiveCacheCurrent().
472 primitive_cache_dense.clear();
473
474 // Transfer to GPU if acceleration is enabled
475#ifdef HELIOS_CUDA_AVAILABLE
476 if (gpu_acceleration_enabled) {
477 transferBVHToGPU();
478 }
479#endif
480
481 // Update internal tracking - record what we have processed
482 // This allows us to avoid redundant rebuilds until user marks geometry clean
483 std::vector<uint> context_deleted_uuids = context->getDeletedUUIDs();
484
485 // Track ALL primitives that are now in the BVH, not just dirty ones
486 // This ensures isBVHValid() works correctly when new geometry is added
487 last_processed_uuids.clear();
488 last_processed_uuids.insert(primitives_to_include.begin(), primitives_to_include.end());
489
490 last_processed_deleted_uuids.clear();
491 last_processed_deleted_uuids.insert(context_deleted_uuids.begin(), context_deleted_uuids.end());
492
493 // Update BVH geometry tracking for caching
494 last_bvh_geometry.clear();
495 last_bvh_geometry.insert(primitives_to_include.begin(), primitives_to_include.end());
496 bvh_dirty = false;
497 soa_dirty = true; // SoA needs rebuild after BVH change
498
499 // Report aggregated warnings
500 warnings.report(std::cerr);
501}
502
504 markBVHDirty();
505 buildBVH();
506}
507
509 automatic_bvh_rebuilds = false;
510}
511
513 automatic_bvh_rebuilds = true;
514}
515
517 hierarchical_bvh_enabled = true;
518 static_bvh_valid = false; // Force rebuild of static BVH
519}
520
522 hierarchical_bvh_enabled = false;
523 // Clear static BVH data
524 static_bvh_nodes.clear();
525 static_bvh_primitives.clear();
526 static_bvh_valid = false;
527 last_static_bvh_geometry.clear();
528}
529
530void CollisionDetection::updateHierarchicalBVH(const std::set<uint> &requested_geometry, bool force_rebuild) {
531
532 // Step 1: Build/update static BVH if needed
533 if (!static_bvh_valid || force_rebuild || static_geometry_cache != last_static_bvh_geometry) {
535 }
536
537 // Step 2: Separate dynamic geometry (not in static cache)
538 std::vector<uint> dynamic_geometry;
539 for (uint uuid: requested_geometry) {
540 if (static_geometry_cache.find(uuid) == static_geometry_cache.end()) {
541 dynamic_geometry.push_back(uuid);
542 }
543 }
544
545
546 // Step 3: Build dynamic BVH with remaining geometry (this is smaller and faster)
547 if (!dynamic_geometry.empty()) {
548 buildBVH(dynamic_geometry); // Use existing buildBVH for dynamic part
549 } else {
550 // No dynamic geometry - just clear the dynamic BVH
551 bvh_nodes.clear();
552 primitive_indices.clear();
553 }
554
555 // Update cache
556 last_bvh_geometry = requested_geometry;
557 bvh_dirty = false;
558 soa_dirty = true; // SoA needs rebuild after BVH change
559}
560
562 if (static_geometry_cache.empty()) {
563 static_bvh_nodes.clear();
564 static_bvh_primitives.clear();
565 static_bvh_valid = false;
566 return;
567 }
568
569 std::vector<uint> static_primitives(static_geometry_cache.begin(), static_geometry_cache.end());
570
571
572 // Build BVH for static geometry (reuse existing GPU BVH building logic)
573 // For now, we'll store it in the static BVH structures but use same building method
574 std::vector<BVHNode> temp_nodes;
575 std::vector<uint> temp_primitives;
576
577 // Swap in static storage for building
578 std::swap(bvh_nodes, temp_nodes);
579 std::swap(primitive_indices, temp_primitives);
580
581 // Build BVH using existing method
582 buildBVH(static_primitives);
583
584 // Store result in static BVH and restore dynamic BVH
585 static_bvh_nodes = bvh_nodes;
586 static_bvh_primitives = primitive_indices;
587 std::swap(bvh_nodes, temp_nodes);
588 std::swap(primitive_indices, temp_primitives);
589
590 static_bvh_valid = true;
591 last_static_bvh_geometry = static_geometry_cache;
592}
593
594void CollisionDetection::updateBVH(const std::vector<uint> &UUIDs, bool force_rebuild) {
595 // Convert input to set for efficient comparison
596 std::set<uint> requested_geometry(UUIDs.begin(), UUIDs.end());
597
598 // Check if geometry has changed significantly
599 bool geometry_changed = (requested_geometry != last_bvh_geometry) || bvh_dirty;
600
601 if (!geometry_changed && !force_rebuild) {
602 return;
603 }
604
605 // Use hierarchical BVH approach if enabled
606 if (hierarchical_bvh_enabled) {
607 updateHierarchicalBVH(requested_geometry, force_rebuild);
608 return;
609 }
610
611 // Determine if we need a full rebuild or can do incremental update
612 if (force_rebuild || bvh_nodes.empty()) {
613 // Full rebuild required
614 buildBVH(UUIDs);
615 } else {
616 // Check how much geometry has changed
617 std::set<uint> added_geometry, removed_geometry;
618
619 // Find added geometry (in requested but not in last_bvh_geometry)
620 std::set_difference(requested_geometry.begin(), requested_geometry.end(), last_bvh_geometry.begin(), last_bvh_geometry.end(), std::inserter(added_geometry, added_geometry.begin()));
621
622 // Find removed geometry (in last_bvh_geometry but not in requested)
623 std::set_difference(last_bvh_geometry.begin(), last_bvh_geometry.end(), requested_geometry.begin(), requested_geometry.end(), std::inserter(removed_geometry, removed_geometry.begin()));
624
625 // If more than 20% of geometry changed, do full rebuild, otherwise incremental
626 size_t total_change = added_geometry.size() + removed_geometry.size();
627 size_t current_size = std::max(last_bvh_geometry.size(), requested_geometry.size());
628
629 // For plant growth, we want to favor incremental updates since they're mostly additions
630 // Use a more aggressive threshold that considers the type of changes
631 bool mostly_additions = (removed_geometry.size() < added_geometry.size() * 0.1f); // <10% removals
632 float change_threshold = mostly_additions ? 0.5f : 0.2f; // Higher threshold for growth scenarios
633
634 if (current_size == 0 || (float(total_change) / float(current_size)) > change_threshold) {
635 buildBVH(UUIDs);
636 } else {
637 // Implement incremental update by selective insertion/removal
638 incrementalUpdateBVH(added_geometry, removed_geometry, requested_geometry);
639 }
640 }
641
642 // Update tracking
643 last_bvh_geometry = requested_geometry;
644 bvh_dirty = false;
645 soa_dirty = true; // SoA needs rebuild after BVH change
646}
647
648void CollisionDetection::setStaticGeometry(const std::vector<uint> &UUIDs) {
649 static_geometry_cache.clear();
650 static_geometry_cache.insert(UUIDs.begin(), UUIDs.end());
651}
652
653void CollisionDetection::ensureBVHCurrent() {
654 // If automatic rebuilds are disabled, skip all automatic updates
655 if (!automatic_bvh_rebuilds) {
656 return;
657 }
658
659 // If BVH is completely empty, build it with all geometry
660 if (bvh_nodes.empty()) {
661 if (printmessages) {
662 std::cout << "Building initial BVH..." << std::endl;
663 }
664 buildBVH();
665 return;
666 }
667
668 // Use two-level dirty tracking pattern (similar to Visualizer plugin)
669 // Get dirty UUIDs from Context (but don't clear them - that's for the user)
670 std::vector<uint> context_dirty_uuids = context->getDirtyUUIDs(false); // Don't include deleted
671 std::vector<uint> context_deleted_uuids = context->getDeletedUUIDs();
672
673 // Convert to sets for efficient comparison
674 std::set<uint> current_dirty(context_dirty_uuids.begin(), context_dirty_uuids.end());
675 std::set<uint> current_deleted(context_deleted_uuids.begin(), context_deleted_uuids.end());
676
677 // Check if there are new changes since our last processing
678 bool has_new_dirty = false;
679 bool has_new_deleted = false;
680
681 // Check for new dirty UUIDs we haven't processed
682 for (uint uuid: current_dirty) {
683 if (last_processed_uuids.find(uuid) == last_processed_uuids.end()) {
684 has_new_dirty = true;
685 break;
686 }
687 }
688
689 // Check for new deleted UUIDs we haven't processed
690 for (uint uuid: current_deleted) {
691 if (last_processed_deleted_uuids.find(uuid) == last_processed_deleted_uuids.end()) {
692 has_new_deleted = true;
693 break;
694 }
695 }
696
697 // Only rebuild if there are genuinely new changes since our last processing
698 if (has_new_dirty || has_new_deleted) {
699 if (printmessages) {
700 std::cout << "Geometry has changed since last BVH build, rebuilding..." << std::endl;
701 }
702
703 buildBVH(); // This will update our internal tracking
704 }
705
706 // Note: We do NOT call context->markGeometryClean() here
707 // That should only be done by the user after all plugins have processed the changes
708}
709
711 if (bvh_nodes.empty()) {
712 return false;
713 }
714
715 // Check if there are new primitives in the context that aren't in our BVH
716 std::vector<uint> all_context_uuids = context->getAllUUIDs();
717 for (uint uuid: all_context_uuids) {
718 if (last_processed_uuids.find(uuid) == last_processed_uuids.end()) {
719 return false; // Found a primitive that's not in our BVH
720 }
721 }
722
723 // Check for deleted UUIDs we haven't processed
724 std::vector<uint> context_deleted_uuids = context->getDeletedUUIDs();
725 for (uint uuid: context_deleted_uuids) {
726 if (last_processed_deleted_uuids.find(uuid) == last_processed_deleted_uuids.end()) {
727 return false; // Found new deleted UUID we haven't processed
728 }
729 }
730
731 // Check if any primitives in our BVH have been deleted
732 for (uint uuid: primitive_indices) {
733 if (!context->doesPrimitiveExist(uuid)) {
734 return false; // A primitive in our BVH no longer exists
735 }
736 }
737
738 return true; // BVH is current with respect to geometry changes
739}
740
742#ifdef HELIOS_CUDA_AVAILABLE
743 if (!isGPUAvailable()) {
744 if (printmessages) {
745 std::cerr << "WARNING: GPU acceleration requested but no usable GPU is available (no CUDA device or HELIOS_NO_GPU is set). Using CPU-only mode." << std::endl;
746 }
747 gpu_acceleration_enabled = false;
748 return;
749 }
750 gpu_acceleration_enabled = true;
751 if (!bvh_nodes.empty()) {
752 transferBVHToGPU();
753 }
754#else
755 if (printmessages) {
756 std::cerr << "WARNING: GPU acceleration requested but CUDA not available. Ignoring request." << std::endl;
757 }
758#endif
759}
760
762 gpu_acceleration_enabled = false;
763#ifdef HELIOS_CUDA_AVAILABLE
764 freeGPUMemory();
765#endif
766}
767
769 return gpu_acceleration_enabled;
770}
771
773 static bool checked = false;
774 static bool available = false;
775 if (checked) {
776 return available;
777 }
778 checked = true;
779
780#ifdef HELIOS_CUDA_AVAILABLE
781 // Allow forcing the CPU path for headless/CI simulation, mirroring the radiation plugin.
782 const char *no_gpu = std::getenv("HELIOS_NO_GPU");
783 if (no_gpu && std::string(no_gpu) != "0") {
784 available = false;
785 return available;
786 }
787 int deviceCount = 0;
788 cudaError_t err = cudaGetDeviceCount(&deviceCount);
789 available = (err == cudaSuccess && deviceCount > 0);
790#else
791 available = false;
792#endif
793 return available;
794}
795
797 printmessages = false;
798}
799
801 printmessages = true;
802}
803
804void CollisionDetection::setCancelFlag(volatile int *flag) {
805 cancel_flag = flag;
806}
807
809 return primitive_indices.size();
810}
811
812void CollisionDetection::getBVHStatistics(size_t &node_count, size_t &leaf_count, size_t &max_depth) const {
813
814 node_count = bvh_nodes.size();
815 leaf_count = 0;
816 max_depth = 0;
817
818 // Simple traversal to count leaves and find max depth
819 std::function<void(uint, size_t)> traverse = [&](uint node_idx, size_t depth) {
820 if (node_idx >= bvh_nodes.size())
821 return;
822
823 const BVHNode &node = bvh_nodes[node_idx];
824 max_depth = std::max(max_depth, depth);
825
826 if (node.is_leaf) {
827 leaf_count++;
828 } else {
829 if (node.left_child != 0xFFFFFFFF) {
830 traverse(node.left_child, depth + 1);
831 }
832 if (node.right_child != 0xFFFFFFFF) {
833 traverse(node.right_child, depth + 1);
834 }
835 }
836 };
837
838 if (!bvh_nodes.empty()) {
839 traverse(0, 0);
840 }
841}
842
843void CollisionDetection::calculateAABB(const std::vector<uint> &primitives, vec3 &aabb_min, vec3 &aabb_max) const {
844
845 if (primitives.empty()) {
846 aabb_min = make_vec3(0, 0, 0);
847 aabb_max = make_vec3(0, 0, 0);
848 return;
849 }
850
851 // Find first valid primitive for initialization
852 size_t first_valid = 0;
853 while (first_valid < primitives.size() && !context->doesPrimitiveExist(primitives[first_valid])) {
854 first_valid++;
855 }
856 if (first_valid >= primitives.size()) {
857 // No valid primitives found
858 aabb_min = make_vec3(0, 0, 0);
859 aabb_max = make_vec3(0, 0, 0);
860 return;
861 }
862
863 // Initialize with first valid primitive's bounding box
864 context->getPrimitiveBoundingBox(primitives[first_valid], aabb_min, aabb_max);
865
866 // Expand to include all remaining valid primitives
867 for (size_t i = first_valid + 1; i < primitives.size(); i++) {
868 if (!context->doesPrimitiveExist(primitives[i])) {
869 continue; // Skip invalid primitive
870 }
871 vec3 prim_min, prim_max;
872 context->getPrimitiveBoundingBox(primitives[i], prim_min, prim_max);
873
874 aabb_min.x = std::min(aabb_min.x, prim_min.x);
875 aabb_min.y = std::min(aabb_min.y, prim_min.y);
876 aabb_min.z = std::min(aabb_min.z, prim_min.z);
877
878 aabb_max.x = std::max(aabb_max.x, prim_max.x);
879 aabb_max.y = std::max(aabb_max.y, prim_max.y);
880 aabb_max.z = std::max(aabb_max.z, prim_max.z);
881 }
882}
883
884void CollisionDetection::buildBVHRecursive(uint node_index, size_t primitive_start, size_t primitive_count, int depth) {
885
886 // Node should already be pre-allocated
887 if (node_index >= bvh_nodes.size()) {
888 throw std::runtime_error("CollisionDetection: BVH recursive access exceeded pre-allocated capacity");
889 }
890
891 // Bounds check for primitive_indices access
892 if (primitive_start + primitive_count > primitive_indices.size()) {
893 throw std::runtime_error("CollisionDetection: BVH primitive bounds check failed - primitive_start(" + std::to_string(primitive_start) + ") + primitive_count(" + std::to_string(primitive_count) + ") > primitive_indices.size(" +
894 std::to_string(primitive_indices.size()) + ")");
895 }
896
897 BVHNode &node = bvh_nodes[node_index];
898
899 // Calculate bounding box for this node using cached AABBs
900 if (primitive_count == 0) {
901 node.aabb_min = make_vec3(0, 0, 0);
902 node.aabb_max = make_vec3(0, 0, 0);
903 } else {
904 // Initialize with first primitive's cached AABB
905 uint first_uuid = primitive_indices[primitive_start];
906 auto it = primitive_aabbs_cache.find(first_uuid);
907 if (it == primitive_aabbs_cache.end()) {
908 // Handle missing primitive - use zero bounds
909 node.aabb_min = make_vec3(0, 0, 0);
910 node.aabb_max = make_vec3(0, 0, 0);
911 return;
912 }
913 const auto &first_cached_aabb = it->second;
914 node.aabb_min = first_cached_aabb.first;
915 node.aabb_max = first_cached_aabb.second;
916
917 // Expand to include all primitives in this node
918 for (size_t i = 1; i < primitive_count; i++) {
919 uint uuid = primitive_indices[primitive_start + i];
920 auto it = primitive_aabbs_cache.find(uuid);
921 if (it == primitive_aabbs_cache.end()) {
922 continue; // Skip missing primitive
923 }
924 const auto &cached_aabb = it->second;
925 node.aabb_min.x = std::min(node.aabb_min.x, cached_aabb.first.x);
926 node.aabb_min.y = std::min(node.aabb_min.y, cached_aabb.first.y);
927 node.aabb_min.z = std::min(node.aabb_min.z, cached_aabb.first.z);
928
929 node.aabb_max.x = std::max(node.aabb_max.x, cached_aabb.second.x);
930 node.aabb_max.y = std::max(node.aabb_max.y, cached_aabb.second.y);
931 node.aabb_max.z = std::max(node.aabb_max.z, cached_aabb.second.z);
932 }
933 }
934
935 // Stopping criteria - make this a leaf. The leaf-size threshold is the primary stop; the depth cap is only a
936 // backstop against pathological inputs. A binned-SAH builder produces well-balanced trees, so the previous
937 // very-shallow large-scene cap (depth 6 -> up to thousands of primitives per leaf) is no longer needed.
938 const int MAX_PRIMITIVES_PER_LEAF = 8;
939 const int MAX_DEPTH = 64; // backstop only; SAH + leaf threshold normally stop far sooner
940
941 if (primitive_count <= static_cast<size_t>(MAX_PRIMITIVES_PER_LEAF) || depth >= MAX_DEPTH) {
942 // Make leaf node
943 node.is_leaf = true;
944 node.primitive_start = primitive_start;
945 node.primitive_count = primitive_count;
946 node.left_child = 0xFFFFFFFF;
947 node.right_child = 0xFFFFFFFF;
948 return;
949 }
950
951 // ---- Binned Surface Area Heuristic (SAH) split ----
952 // Standard approach (PBRT): for each axis, bin primitive centroids over the node's centroid bounds, accumulate
953 // per-bin counts and AABBs, then evaluate the SAH cost C(split) = SA(left)*N_left + SA(right)*N_right at each of
954 // the (NUM_BINS-1) bin boundaries. Pick the axis+boundary with the lowest cost. Falls back to a median split when
955 // the centroid bounds are degenerate (all centroids coincident on every axis).
956 constexpr int NUM_BINS = 16;
957
958 auto centroid_of = [&](uint uuid, vec3 &out) -> bool {
959 auto it = primitive_aabbs_cache.find(uuid);
960 if (it == primitive_aabbs_cache.end()) {
961 return false;
962 }
963 out = (it->second.first + it->second.second) * 0.5f;
964 return true;
965 };
966
967 // Centroid bounds of this node's primitives.
968 vec3 centroid_min = make_vec3(std::numeric_limits<float>::max(), std::numeric_limits<float>::max(), std::numeric_limits<float>::max());
969 vec3 centroid_max = make_vec3(-std::numeric_limits<float>::max(), -std::numeric_limits<float>::max(), -std::numeric_limits<float>::max());
970 for (size_t i = 0; i < primitive_count; i++) {
971 vec3 c;
972 if (!centroid_of(primitive_indices[primitive_start + i], c)) {
973 continue;
974 }
975 centroid_min.x = std::min(centroid_min.x, c.x);
976 centroid_min.y = std::min(centroid_min.y, c.y);
977 centroid_min.z = std::min(centroid_min.z, c.z);
978 centroid_max.x = std::max(centroid_max.x, c.x);
979 centroid_max.y = std::max(centroid_max.y, c.y);
980 centroid_max.z = std::max(centroid_max.z, c.z);
981 }
982 const vec3 centroid_extent = centroid_max - centroid_min;
983
984 auto surface_area = [](const vec3 &mn, const vec3 &mx) -> float {
985 const vec3 d = mx - mn;
986 if (d.x < 0.f || d.y < 0.f || d.z < 0.f) {
987 return 0.f; // empty
988 }
989 return 2.0f * (d.x * d.y + d.y * d.z + d.z * d.x);
990 };
991
992 int split_axis = -1;
993 float best_cost = std::numeric_limits<float>::max();
994 int best_bin_boundary = -1; // primitives with bin < boundary go left
995
996 // Evaluate each axis whose centroid extent is non-degenerate.
997 for (int axis = 0; axis < 3; axis++) {
998 const float axis_extent = (axis == 0) ? centroid_extent.x : (axis == 1) ? centroid_extent.y : centroid_extent.z;
999 if (axis_extent <= 0.f) {
1000 continue; // all centroids share this coordinate; no useful split on this axis
1001 }
1002 const float axis_min = (axis == 0) ? centroid_min.x : (axis == 1) ? centroid_min.y : centroid_min.z;
1003 const float scale = float(NUM_BINS) / axis_extent;
1004
1005 int bin_counts[NUM_BINS] = {0};
1006 vec3 bin_min[NUM_BINS];
1007 vec3 bin_max[NUM_BINS];
1008 for (int b = 0; b < NUM_BINS; b++) {
1009 bin_min[b] = make_vec3(std::numeric_limits<float>::max(), std::numeric_limits<float>::max(), std::numeric_limits<float>::max());
1010 bin_max[b] = make_vec3(-std::numeric_limits<float>::max(), -std::numeric_limits<float>::max(), -std::numeric_limits<float>::max());
1011 }
1012
1013 // Bin every primitive by its centroid coordinate on this axis.
1014 for (size_t i = 0; i < primitive_count; i++) {
1015 const uint uuid = primitive_indices[primitive_start + i];
1016 auto it = primitive_aabbs_cache.find(uuid);
1017 if (it == primitive_aabbs_cache.end()) {
1018 continue;
1019 }
1020 const vec3 c = (it->second.first + it->second.second) * 0.5f;
1021 const float c_axis = (axis == 0) ? c.x : (axis == 1) ? c.y : c.z;
1022 int bin = int((c_axis - axis_min) * scale);
1023 if (bin < 0)
1024 bin = 0;
1025 if (bin >= NUM_BINS)
1026 bin = NUM_BINS - 1;
1027 bin_counts[bin]++;
1028 const vec3 &pmin = it->second.first;
1029 const vec3 &pmax = it->second.second;
1030 bin_min[bin].x = std::min(bin_min[bin].x, pmin.x);
1031 bin_min[bin].y = std::min(bin_min[bin].y, pmin.y);
1032 bin_min[bin].z = std::min(bin_min[bin].z, pmin.z);
1033 bin_max[bin].x = std::max(bin_max[bin].x, pmax.x);
1034 bin_max[bin].y = std::max(bin_max[bin].y, pmax.y);
1035 bin_max[bin].z = std::max(bin_max[bin].z, pmax.z);
1036 }
1037
1038 // Prefix (left) and suffix (right) sweeps to get count + AABB on each side of every boundary.
1039 int left_count[NUM_BINS];
1040 float left_area[NUM_BINS];
1041 vec3 acc_min = make_vec3(std::numeric_limits<float>::max(), std::numeric_limits<float>::max(), std::numeric_limits<float>::max());
1042 vec3 acc_max = make_vec3(-std::numeric_limits<float>::max(), -std::numeric_limits<float>::max(), -std::numeric_limits<float>::max());
1043 int running = 0;
1044 for (int b = 0; b < NUM_BINS; b++) {
1045 if (bin_counts[b] > 0) {
1046 acc_min.x = std::min(acc_min.x, bin_min[b].x);
1047 acc_min.y = std::min(acc_min.y, bin_min[b].y);
1048 acc_min.z = std::min(acc_min.z, bin_min[b].z);
1049 acc_max.x = std::max(acc_max.x, bin_max[b].x);
1050 acc_max.y = std::max(acc_max.y, bin_max[b].y);
1051 acc_max.z = std::max(acc_max.z, bin_max[b].z);
1052 }
1053 running += bin_counts[b];
1054 left_count[b] = running;
1055 left_area[b] = (running > 0) ? surface_area(acc_min, acc_max) : 0.f;
1056 }
1057
1058 acc_min = make_vec3(std::numeric_limits<float>::max(), std::numeric_limits<float>::max(), std::numeric_limits<float>::max());
1059 acc_max = make_vec3(-std::numeric_limits<float>::max(), -std::numeric_limits<float>::max(), -std::numeric_limits<float>::max());
1060 running = 0;
1061 // Boundary b separates bins [0..b] (left) from [b+1..NUM_BINS-1] (right), b in [0, NUM_BINS-2].
1062 for (int b = NUM_BINS - 1; b >= 1; b--) {
1063 if (bin_counts[b] > 0) {
1064 acc_min.x = std::min(acc_min.x, bin_min[b].x);
1065 acc_min.y = std::min(acc_min.y, bin_min[b].y);
1066 acc_min.z = std::min(acc_min.z, bin_min[b].z);
1067 acc_max.x = std::max(acc_max.x, bin_max[b].x);
1068 acc_max.y = std::max(acc_max.y, bin_max[b].y);
1069 acc_max.z = std::max(acc_max.z, bin_max[b].z);
1070 }
1071 running += bin_counts[b];
1072 const int right_count = running;
1073 const float right_area = (right_count > 0) ? surface_area(acc_min, acc_max) : 0.f;
1074 const int lc = left_count[b - 1];
1075 if (lc == 0 || right_count == 0) {
1076 continue; // degenerate (all on one side); not a valid split
1077 }
1078 const float cost = left_area[b - 1] * float(lc) + right_area * float(right_count);
1079 if (cost < best_cost) {
1080 best_cost = cost;
1081 split_axis = axis;
1082 best_bin_boundary = b - 1; // primitives in bins [0..best_bin_boundary] go left
1083 }
1084 }
1085 }
1086
1087 size_t split_index;
1088 if (split_axis < 0) {
1089 // No valid SAH split found (e.g. all centroids coincident). Fall back to a median split along the longest
1090 // spatial extent so construction still makes progress deterministically.
1091 vec3 extent = node.aabb_max - node.aabb_min;
1092 int median_axis = 0;
1093 if (extent.y > extent.x)
1094 median_axis = 1;
1095 if (extent.z > (median_axis == 0 ? extent.x : extent.y))
1096 median_axis = 2;
1097 std::sort(primitive_indices.begin() + primitive_start, primitive_indices.begin() + primitive_start + primitive_count, [&](uint a, uint b) {
1098 vec3 ca, cb;
1099 const bool oka = centroid_of(a, ca);
1100 const bool okb = centroid_of(b, cb);
1101 if (!oka || !okb) {
1102 return a < b;
1103 }
1104 const float va = (median_axis == 0) ? ca.x : (median_axis == 1) ? ca.y : ca.z;
1105 const float vb = (median_axis == 0) ? cb.x : (median_axis == 1) ? cb.y : cb.z;
1106 if (va == vb) {
1107 return a < b; // stable tiebreaker -> deterministic build
1108 }
1109 return va < vb;
1110 });
1111 split_index = primitive_count / 2;
1112 } else {
1113 // Partition primitives by the chosen axis+boundary. std::partition keeps construction O(n) per node (no full
1114 // sort). A stable tiebreaker is not needed for partition determinism, but the bin assignment is a pure
1115 // function of geometry so the partition is deterministic for a fixed primitive set.
1116 const float axis_min = (split_axis == 0) ? centroid_min.x : (split_axis == 1) ? centroid_min.y : centroid_min.z;
1117 const float axis_extent = (split_axis == 0) ? centroid_extent.x : (split_axis == 1) ? centroid_extent.y : centroid_extent.z;
1118 const float scale = float(NUM_BINS) / axis_extent;
1119 const int boundary = best_bin_boundary;
1120 auto mid = std::partition(primitive_indices.begin() + primitive_start, primitive_indices.begin() + primitive_start + primitive_count, [&](uint uuid) {
1121 vec3 c;
1122 if (!centroid_of(uuid, c)) {
1123 return true; // keep missing-AABB primitives on the left
1124 }
1125 const float c_axis = (split_axis == 0) ? c.x : (split_axis == 1) ? c.y : c.z;
1126 int bin = int((c_axis - axis_min) * scale);
1127 if (bin < 0)
1128 bin = 0;
1129 if (bin >= NUM_BINS)
1130 bin = NUM_BINS - 1;
1131 return bin <= boundary;
1132 });
1133 split_index = size_t(std::distance(primitive_indices.begin() + primitive_start, mid));
1134 // Guard against a partition that put everything on one side (floating-point edge case): fall back to median.
1135 if (split_index == 0 || split_index == primitive_count) {
1136 split_index = primitive_count / 2;
1137 }
1138 }
1139
1140 // Allocate child nodes from pre-allocated array (no resizing needed)
1141 uint left_child_index = next_available_node_index++;
1142 uint right_child_index = next_available_node_index++;
1143
1144 // Ensure we don't exceed pre-allocated capacity
1145 if (right_child_index >= bvh_nodes.size()) {
1146 throw std::runtime_error("CollisionDetection: BVH node allocation exceeded pre-calculated capacity");
1147 }
1148
1149 // Re-get the node reference after potential reallocation
1150 BVHNode &updated_node = bvh_nodes[node_index];
1151 updated_node.left_child = left_child_index;
1152 updated_node.right_child = right_child_index;
1153 updated_node.is_leaf = false;
1154 updated_node.primitive_start = 0;
1155 updated_node.primitive_count = 0;
1156
1157 // Recursively build child nodes
1158 buildBVHRecursive(left_child_index, primitive_start, split_index, depth + 1);
1159 buildBVHRecursive(right_child_index, primitive_start + split_index, primitive_count - split_index, depth + 1);
1160}
1161
1162std::vector<uint> CollisionDetection::traverseBVH_CPU(const vec3 &query_aabb_min, const vec3 &query_aabb_max) {
1163
1164 std::vector<uint> results;
1165
1166 if (bvh_nodes.empty()) {
1167 return results;
1168 }
1169
1170 // Stack-based traversal to avoid recursion
1171 std::vector<uint> node_stack;
1172 node_stack.push_back(0); // Start with root node
1173
1174 while (!node_stack.empty()) {
1175 uint node_idx = node_stack.back();
1176 node_stack.pop_back();
1177
1178 if (node_idx >= bvh_nodes.size())
1179 continue;
1180
1181 const BVHNode &node = bvh_nodes[node_idx];
1182
1183 // Test if query AABB intersects node AABB
1184 if (!aabbIntersect(query_aabb_min, query_aabb_max, node.aabb_min, node.aabb_max)) {
1185 continue;
1186 }
1187
1188 if (node.is_leaf) {
1189 // Check each primitive in this leaf for intersection
1190 for (uint i = 0; i < node.primitive_count; i++) {
1191 uint primitive_id = primitive_indices[node.primitive_start + i];
1192
1193 // Get this primitive's AABB
1194 if (!context->doesPrimitiveExist(primitive_id)) {
1195 continue; // Skip invalid primitive
1196 }
1197 vec3 prim_min, prim_max;
1198 context->getPrimitiveBoundingBox(primitive_id, prim_min, prim_max);
1199
1200 // Only add to results if AABBs actually intersect
1201 if (aabbIntersect(query_aabb_min, query_aabb_max, prim_min, prim_max)) {
1202 results.push_back(primitive_id);
1203 }
1204 }
1205 } else {
1206 // Add child nodes to stack for processing
1207 if (node.left_child != 0xFFFFFFFF) {
1208 node_stack.push_back(node.left_child);
1209 }
1210 if (node.right_child != 0xFFFFFFFF) {
1211 node_stack.push_back(node.right_child);
1212 }
1213 }
1214 }
1215
1216 return results;
1217}
1218
1219#ifdef HELIOS_CUDA_AVAILABLE
1220std::vector<uint> CollisionDetection::traverseBVH_GPU(const vec3 &query_aabb_min, const vec3 &query_aabb_max) {
1221 if (!gpu_memory_allocated) {
1222 helios_runtime_error("ERROR: GPU traversal requested but GPU memory is not allocated. Call buildBVH() or transferBVHToGPU() first.");
1223 }
1224
1225 // Prepare single query
1226 float query_min_array[3] = {query_aabb_min.x, query_aabb_min.y, query_aabb_min.z};
1227 float query_max_array[3] = {query_aabb_max.x, query_aabb_max.y, query_aabb_max.z};
1228
1229 // Prepare primitive AABB arrays
1230 std::vector<float> primitive_min_array(primitive_indices.size() * 3);
1231 std::vector<float> primitive_max_array(primitive_indices.size() * 3);
1232
1233 for (size_t i = 0; i < primitive_indices.size(); i++) {
1234 uint uuid = primitive_indices[i];
1235 auto it = primitive_aabbs_cache.find(uuid);
1236 if (it == primitive_aabbs_cache.end()) {
1237 continue; // Skip missing primitive
1238 }
1239 const auto &cached_aabb = it->second;
1240
1241 primitive_min_array[i * 3] = cached_aabb.first.x;
1242 primitive_min_array[i * 3 + 1] = cached_aabb.first.y;
1243 primitive_min_array[i * 3 + 2] = cached_aabb.first.z;
1244
1245 primitive_max_array[i * 3] = cached_aabb.second.x;
1246 primitive_max_array[i * 3 + 1] = cached_aabb.second.y;
1247 primitive_max_array[i * 3 + 2] = cached_aabb.second.z;
1248 }
1249
1250 const int max_results = 1000; // Reasonable limit
1251 std::vector<unsigned int> results(max_results);
1252 unsigned int result_count = 0;
1253
1254 // Call CUDA kernel wrapper
1255 launchBVHTraversal(d_bvh_nodes, bvh_nodes.size(), d_primitive_indices, primitive_indices.size(), primitive_min_array.data(), primitive_max_array.data(), query_min_array, query_max_array, 1, results.data(), &result_count, max_results);
1256
1257 // Convert to return format
1258 std::vector<uint> final_results;
1259 for (unsigned int i = 0; i < result_count; i++) {
1260 final_results.push_back(results[i]);
1261 }
1262
1263 return final_results;
1264}
1265#endif
1266
1267bool CollisionDetection::aabbIntersect(const vec3 &min1, const vec3 &max1, const vec3 &min2, const vec3 &max2) {
1268 return (min1.x <= max2.x && max1.x >= min2.x) && (min1.y <= max2.y && max1.y >= min2.y) && (min1.z <= max2.z && max1.z >= min2.z);
1269}
1270
1271bool CollisionDetection::rayAABBIntersect(const vec3 &origin, const vec3 &direction, const vec3 &aabb_min, const vec3 &aabb_max, float &t_min, float &t_max) const {
1272
1273 t_min = 0.0f;
1274 t_max = std::numeric_limits<float>::max();
1275
1276 // Check intersection with each pair of parallel planes (X, Y, Z)
1277 for (int i = 0; i < 3; i++) {
1278 float dir_component = (i == 0) ? direction.x : (i == 1) ? direction.y : direction.z;
1279 float orig_component = (i == 0) ? origin.x : (i == 1) ? origin.y : origin.z;
1280 float min_component = (i == 0) ? aabb_min.x : (i == 1) ? aabb_min.y : aabb_min.z;
1281 float max_component = (i == 0) ? aabb_max.x : (i == 1) ? aabb_max.y : aabb_max.z;
1282
1283 if (std::abs(dir_component) < 1e-9f) {
1284 // Ray is parallel to the slab
1285 if (orig_component < min_component || orig_component > max_component) {
1286 return false; // Ray is outside the slab and parallel to it
1287 }
1288 } else {
1289 // Compute intersection t values with the two planes
1290 float t1 = (min_component - orig_component) / dir_component;
1291 float t2 = (max_component - orig_component) / dir_component;
1292
1293 // Make sure t1 is the near intersection and t2 is the far intersection
1294 if (t1 > t2) {
1295 std::swap(t1, t2);
1296 }
1297
1298 // Update the overall near and far intersection parameters
1299 t_min = std::max(t_min, t1);
1300 t_max = std::min(t_max, t2);
1301
1302 // If the near intersection is farther than the far intersection, no intersection
1303 if (t_min > t_max) {
1304 return false;
1305 }
1306 }
1307 }
1308
1309 // Ray intersects AABB if t_min <= t_max and the intersection is in front of ray origin
1310 return t_max >= 0.0f;
1311}
1312
1313bool CollisionDetection::coneAABBIntersect(const Cone &cone, const vec3 &aabb_min, const vec3 &aabb_max) {
1314 // Check if apex is inside AABB
1315 if (cone.apex.x >= aabb_min.x && cone.apex.x <= aabb_max.x && cone.apex.y >= aabb_min.y && cone.apex.y <= aabb_max.y && cone.apex.z >= aabb_min.z && cone.apex.z <= aabb_max.z) {
1316 return true; // Apex is inside AABB, definite intersection
1317 }
1318
1319 // Early rejection using bounding sphere around AABB
1320 vec3 box_center = 0.5f * (aabb_min + aabb_max);
1321 vec3 box_half_extents = 0.5f * (aabb_max - aabb_min);
1322 float box_radius = box_half_extents.magnitude();
1323
1324 // For infinite cone, check if any AABB corner is inside the cone
1325 if (cone.height <= 0.0f) {
1326 // Check all 8 corners of the AABB
1327 for (int i = 0; i < 8; i++) {
1328 vec3 corner = make_vec3((i & 1) ? aabb_max.x : aabb_min.x, (i & 2) ? aabb_max.y : aabb_min.y, (i & 4) ? aabb_max.z : aabb_min.z);
1329
1330 // Vector from apex to corner
1331 vec3 apex_to_corner = corner - cone.apex;
1332 float distance_along_axis = apex_to_corner * cone.axis;
1333
1334 // Point must be in front of apex
1335 if (distance_along_axis > 0) {
1336 // Check if point is within cone angle
1337 float cos_angle = distance_along_axis / apex_to_corner.magnitude();
1338 if (cos_angle >= cosf(cone.half_angle)) {
1339 return true; // This corner is inside the cone
1340 }
1341 }
1342 }
1343
1344 // Check if cone axis intersects AABB
1345 // Use ray-AABB intersection with cone axis as ray
1346 float t_min = 0.0f;
1347 float t_max = std::numeric_limits<float>::max();
1348
1349 for (int i = 0; i < 3; i++) {
1350 float axis_component = (i == 0) ? cone.axis.x : (i == 1) ? cone.axis.y : cone.axis.z;
1351 float apex_component = (i == 0) ? cone.apex.x : (i == 1) ? cone.apex.y : cone.apex.z;
1352 float min_component = (i == 0) ? aabb_min.x : (i == 1) ? aabb_min.y : aabb_min.z;
1353 float max_component = (i == 0) ? aabb_max.x : (i == 1) ? aabb_max.y : aabb_max.z;
1354
1355 if (std::abs(axis_component) < 1e-6f) {
1356 // Ray is parallel to slab
1357 if (apex_component < min_component || apex_component > max_component) {
1358 return false; // Ray is outside slab
1359 }
1360 } else {
1361 // Compute intersection t values
1362 float t1 = (min_component - apex_component) / axis_component;
1363 float t2 = (max_component - apex_component) / axis_component;
1364
1365 if (t1 > t2)
1366 std::swap(t1, t2);
1367
1368 t_min = std::max(t_min, t1);
1369 t_max = std::min(t_max, t2);
1370
1371 if (t_min > t_max) {
1372 return false; // No intersection
1373 }
1374 }
1375 }
1376
1377 // If we get here, the cone axis intersects the AABB
1378 // But for narrow cones, we need to check if the AABB is within the cone angle
1379 if (t_min >= 0 && t_max >= 0) {
1380 // Check if the intersection region on the axis is within the cone angle
1381 // Find the closest point on the axis within the intersection region
1382 float t_check = std::max(0.0f, t_min);
1383 vec3 axis_point = cone.apex + cone.axis * t_check;
1384
1385 // Find the closest point in the AABB to this axis point
1386 vec3 closest_in_box = make_vec3(std::max(aabb_min.x, std::min(axis_point.x, aabb_max.x)), std::max(aabb_min.y, std::min(axis_point.y, aabb_max.y)), std::max(aabb_min.z, std::min(axis_point.z, aabb_max.z)));
1387
1388 // Check if this closest point is within the cone
1389 vec3 apex_to_point = closest_in_box - cone.apex;
1390 float distance_along_axis = apex_to_point * cone.axis;
1391
1392 if (distance_along_axis > 0) {
1393 float distance_to_point = apex_to_point.magnitude();
1394 if (distance_to_point > 0) {
1395 float cos_angle = distance_along_axis / distance_to_point;
1396 if (cos_angle >= cosf(cone.half_angle)) {
1397 return true;
1398 }
1399 }
1400 }
1401 }
1402 } else {
1403 // Finite cone case
1404 // Check if any AABB corner is inside the finite cone
1405 for (int i = 0; i < 8; i++) {
1406 vec3 corner = make_vec3((i & 1) ? aabb_max.x : aabb_min.x, (i & 2) ? aabb_max.y : aabb_min.y, (i & 4) ? aabb_max.z : aabb_min.z);
1407
1408 // Vector from apex to corner
1409 vec3 apex_to_corner = corner - cone.apex;
1410 float distance_along_axis = apex_to_corner * cone.axis;
1411
1412 // Check if point is within cone height and in front of apex
1413 if (distance_along_axis > 0 && distance_along_axis <= cone.height) {
1414 // Check if point is within cone angle
1415 float cos_angle = distance_along_axis / apex_to_corner.magnitude();
1416 if (cos_angle >= cosf(cone.half_angle)) {
1417 return true; // This corner is inside the cone
1418 }
1419 }
1420 }
1421
1422 // Also need to check if cone base intersects AABB
1423 vec3 base_center = cone.apex + cone.axis * cone.height;
1424 float base_radius = cone.height * tanf(cone.half_angle);
1425
1426 // Simple sphere-AABB check for cone base
1427 vec3 closest_point = make_vec3(std::max(aabb_min.x, std::min(base_center.x, aabb_max.x)), std::max(aabb_min.y, std::min(base_center.y, aabb_max.y)), std::max(aabb_min.z, std::min(base_center.z, aabb_max.z)));
1428
1429 float dist_sq = (closest_point - base_center).magnitude();
1430 if (dist_sq <= base_radius) {
1431 return true; // Base circle intersects AABB
1432 }
1433 }
1434
1435 // More sophisticated tests could be added here for edge cases
1436 // For now, return false if no intersection found
1437 return false;
1438}
1439
1440bool CollisionDetection::coneAABBIntersectFast(const Cone &cone, const vec3 &aabb_min, const vec3 &aabb_max) {
1441 // Fast-path cone-AABB intersection optimized for high-throughput filtering
1442 // Uses aggressive early rejection to eliminate 99%+ of geometry with minimal computation
1443
1444 // Fast rejection test 1: Check if apex is inside AABB (very common case, worth checking first)
1445 if (cone.apex.x >= aabb_min.x && cone.apex.x <= aabb_max.x && cone.apex.y >= aabb_min.y && cone.apex.y <= aabb_max.y && cone.apex.z >= aabb_min.z && cone.apex.z <= aabb_max.z) {
1446 return true; // Apex inside AABB - definite intersection
1447 }
1448
1449 // Fast rejection test 2: Behind-apex test (eliminates geometry behind cone)
1450 vec3 box_center = 0.5f * (aabb_min + aabb_max);
1451 vec3 apex_to_center = box_center - cone.apex;
1452 float distance_along_axis = apex_to_center * cone.axis;
1453
1454 if (distance_along_axis <= 0.0f) {
1455 return false; // AABB is completely behind cone apex
1456 }
1457
1458 // Fast rejection test 3: Height test for finite cones
1459 if (cone.height > 0.0f && distance_along_axis > cone.height) {
1460 // Box center is beyond cone height, but we need to check if any part of box is within height
1461 vec3 box_half_extents = 0.5f * (aabb_max - aabb_min);
1462 float box_radius = box_half_extents.magnitude();
1463 if (distance_along_axis - box_radius > cone.height) {
1464 return false; // Entire AABB is beyond cone height
1465 }
1466 }
1467
1468 // Fast rejection test 4: Cone angle bounding sphere test (fast approximation)
1469 float max_distance = (cone.height > 0.0f) ? cone.height : distance_along_axis;
1470 float max_radius_at_distance = max_distance * tanf(cone.half_angle);
1471
1472 // Find closest point on cone axis to box center
1473 vec3 axis_point = cone.apex + cone.axis * distance_along_axis;
1474 float distance_from_axis = (box_center - axis_point).magnitude();
1475
1476 // Conservative bounding sphere test
1477 vec3 box_half_extents = 0.5f * (aabb_max - aabb_min);
1478 float box_radius = box_half_extents.magnitude();
1479
1480 if (distance_from_axis > max_radius_at_distance + box_radius) {
1481 return false; // AABB is completely outside cone's maximum radius
1482 }
1483
1484 // If we get here, AABB might intersect cone - fall back to precise test
1485 // This should only happen for a small percentage of AABBs
1486 return coneAABBIntersect(cone, aabb_min, aabb_max);
1487}
1488
1489// -------- RASTERIZATION-BASED COLLISION DETECTION IMPLEMENTATION --------
1490
1491int CollisionDetection::calculateOptimalBinCount(float cone_half_angle, int geometry_count) {
1492 // Target: ~1 degree angular resolution, scaled by cone size
1493 float base_resolution = M_PI / 180.0f; // 1 degree in radians
1494 float cone_solid_angle = 2.0f * M_PI * (1.0f - cosf(cone_half_angle));
1495 int optimal_bins = (int) (cone_solid_angle / (base_resolution * base_resolution));
1496
1497 // Clamp based on geometry complexity and performance
1498 int min_bins = 64; // Always enough resolution for gap detection
1499 int max_bins = std::min(1024, geometry_count * 4); // Don't exceed geometry complexity
1500
1501 return std::clamp(optimal_bins, min_bins, max_bins);
1502}
1503
1504std::vector<uint> CollisionDetection::filterPrimitivesParallel(const Cone &cone, const std::vector<uint> &primitive_uuids) {
1505 std::vector<uint> filtered_uuids;
1506
1507 if (primitive_uuids.empty()) {
1508 return filtered_uuids;
1509 }
1510
1511 // Reserve space for result vector
1512 filtered_uuids.reserve(primitive_uuids.size() / 10); // Estimate ~10% will pass filter
1513
1514#ifdef _OPENMP
1515 // Use thread-local vectors to avoid synchronization overhead
1516 const int num_threads = omp_get_max_threads();
1517 std::vector<std::vector<uint>> thread_results(num_threads);
1518
1519 // Pre-allocate thread-local storage
1520 for (int i = 0; i < num_threads; i++) {
1521 thread_results[i].reserve(primitive_uuids.size() / (num_threads * 10));
1522 }
1523
1524// Always parallelize AABB filtering (high value, low overhead)
1525#pragma omp parallel
1526 {
1527 int thread_id = omp_get_thread_num();
1528 std::vector<uint> &local_results = thread_results[thread_id];
1529
1530#pragma omp for nowait
1531 for (int i = 0; i < static_cast<int>(primitive_uuids.size()); i++) {
1532 uint uuid = primitive_uuids[i];
1533
1534 // Get primitive vertices and calculate AABB
1535 if (context->doesPrimitiveExist(uuid)) {
1536 std::vector<vec3> vertices = context->getPrimitiveVertices(uuid);
1537 if (!vertices.empty()) {
1538 // Calculate AABB from vertices
1539 vec3 aabb_min = vertices[0];
1540 vec3 aabb_max = vertices[0];
1541 for (const vec3 &vertex: vertices) {
1542 aabb_min = make_vec3(std::min(aabb_min.x, vertex.x), std::min(aabb_min.y, vertex.y), std::min(aabb_min.z, vertex.z));
1543 aabb_max = make_vec3(std::max(aabb_max.x, vertex.x), std::max(aabb_max.y, vertex.y), std::max(aabb_max.z, vertex.z));
1544 }
1545
1546 // Use fast cone-AABB intersection test
1547 if (coneAABBIntersectFast(cone, aabb_min, aabb_max)) {
1548 local_results.push_back(uuid);
1549 }
1550 }
1551 }
1552 }
1553 }
1554
1555 // Merge results from all threads
1556 size_t total_count = 0;
1557 for (const auto &thread_result: thread_results) {
1558 total_count += thread_result.size();
1559 }
1560
1561 filtered_uuids.reserve(total_count);
1562 for (const auto &thread_result: thread_results) {
1563 filtered_uuids.insert(filtered_uuids.end(), thread_result.begin(), thread_result.end());
1564 }
1565#else
1566 // Serial fallback when OpenMP is not available
1567 for (size_t i = 0; i < primitive_uuids.size(); i++) {
1568 uint uuid = primitive_uuids[i];
1569
1570 // Get primitive vertices and calculate AABB
1571 if (context->doesPrimitiveExist(uuid)) {
1572 std::vector<vec3> vertices = context->getPrimitiveVertices(uuid);
1573 if (!vertices.empty()) {
1574 // Calculate AABB from vertices
1575 vec3 aabb_min = vertices[0];
1576 vec3 aabb_max = vertices[0];
1577 for (const vec3 &vertex: vertices) {
1578 aabb_min = make_vec3(std::min(aabb_min.x, vertex.x), std::min(aabb_min.y, vertex.y), std::min(aabb_min.z, vertex.z));
1579 aabb_max = make_vec3(std::max(aabb_max.x, vertex.x), std::max(aabb_max.y, vertex.y), std::max(aabb_max.z, vertex.z));
1580 }
1581
1582 // Use fast cone-AABB intersection test
1583 if (coneAABBIntersectFast(cone, aabb_min, aabb_max)) {
1584 filtered_uuids.push_back(uuid);
1585 }
1586 }
1587 }
1588 }
1589#endif
1590
1591 return filtered_uuids;
1592}
1593
1594float CollisionDetection::cartesianToSphericalCone(const vec3 &vector, const vec3 &cone_axis, float &theta, float &phi) {
1595 float distance = vector.magnitude();
1596 if (distance < 1e-6f) {
1597 theta = 0.0f;
1598 phi = 0.0f;
1599 return 0.0f;
1600 }
1601
1602 vec3 normalized_vector = vector / distance;
1603
1604 // Calculate phi (polar angle from cone axis)
1605 float cos_phi = normalized_vector * cone_axis;
1606 phi = acosf(std::clamp(cos_phi, -1.0f, 1.0f));
1607
1608 // Calculate theta (azimuthal angle around cone axis)
1609 // Create orthonormal basis from cone axis
1610 vec3 up = (abs(cone_axis.z) < 0.999f) ? make_vec3(0, 0, 1) : make_vec3(1, 0, 0);
1611 vec3 right = cross(cone_axis, up);
1612 right.normalize();
1613 vec3 forward = cross(right, cone_axis);
1614
1615 // Project vector onto perpendicular plane
1616 vec3 projected = normalized_vector - cone_axis * cos_phi;
1617 if (projected.magnitude() > 1e-6f) {
1618 projected.normalize();
1619 float cos_theta = projected * right;
1620 float sin_theta = projected * forward;
1621 theta = atan2f(sin_theta, cos_theta);
1622 if (theta < 0.0f)
1623 theta += 2.0f * M_PI; // Ensure [0, 2π]
1624 } else {
1625 theta = 0.0f; // Vector is along cone axis
1626 }
1627
1628 return distance;
1629}
1630
1631bool CollisionDetection::sphericalCoordsToBinIndices(float theta, float phi, const AngularBins &bins, int &theta_bin, int &phi_bin) {
1632 // Check if angles are within valid cone range
1633 if (phi < 0.0f || theta < 0.0f || theta >= 2.0f * M_PI) {
1634 return false;
1635 }
1636
1637 // Map to bin indices
1638 theta_bin = (int) (theta * bins.theta_divisions / (2.0f * M_PI));
1639 phi_bin = (int) (phi * bins.phi_divisions / M_PI); // Assume phi range is [0, PI] for full sphere
1640
1641 // Clamp to valid ranges
1642 theta_bin = std::clamp(theta_bin, 0, bins.theta_divisions - 1);
1643 phi_bin = std::clamp(phi_bin, 0, bins.phi_divisions - 1);
1644
1645 return true;
1646}
1647
1648void CollisionDetection::projectGeometryToBins(const Cone &cone, const std::vector<uint> &filtered_uuids, AngularBins &bins) {
1649 bins.clear();
1650
1651 const int PARALLEL_THRESHOLD = 500; // Empirically determined threshold
1652
1653 if (filtered_uuids.size() > PARALLEL_THRESHOLD) {
1654// Conditional parallel projection for large geometry sets
1655#ifdef _OPENMP
1656 const int num_threads = omp_get_max_threads();
1657 std::vector<AngularBins> thread_bins(num_threads, AngularBins(bins.theta_divisions, bins.phi_divisions));
1658
1659#pragma omp parallel
1660 {
1661 int thread_id = omp_get_thread_num();
1662 AngularBins &local_bins = thread_bins[thread_id];
1663
1664#pragma omp for nowait
1665 for (int i = 0; i < static_cast<int>(filtered_uuids.size()); i++) {
1666 uint uuid = filtered_uuids[i];
1667
1668 if (context->doesPrimitiveExist(uuid)) {
1669 std::vector<vec3> vertices = context->getPrimitiveVertices(uuid);
1670
1671 // Project each vertex to spherical coordinates
1672 for (const vec3 &vertex: vertices) {
1673 vec3 apex_to_vertex = vertex - cone.apex;
1674 float distance = apex_to_vertex.magnitude();
1675
1676 if (distance > 1e-6f) {
1677 float theta, phi;
1678 cartesianToSphericalCone(apex_to_vertex, cone.axis, theta, phi);
1679
1680 // Skip if outside cone angle
1681 if (phi <= cone.half_angle) {
1682 int theta_bin, phi_bin;
1683 if (sphericalCoordsToBinIndices(theta, phi, local_bins, theta_bin, phi_bin)) {
1684 local_bins.setCovered(theta_bin, phi_bin, distance);
1685 }
1686 }
1687 }
1688 }
1689 }
1690 }
1691 }
1692
1693 // Merge thread-local bins into global bins
1694 for (const auto &thread_bin: thread_bins) {
1695 for (int theta = 0; theta < bins.theta_divisions; theta++) {
1696 for (int phi = 0; phi < bins.phi_divisions; phi++) {
1697 if (thread_bin.isCovered(theta, phi)) {
1698 int index = theta * bins.phi_divisions + phi;
1699 float thread_depth = thread_bin.depth_values[index];
1700 bins.setCovered(theta, phi, thread_depth);
1701 }
1702 }
1703 }
1704 }
1705#else
1706 // Fallback to serial if OpenMP not available
1707 projectGeometryToBinsSerial(cone, filtered_uuids, bins);
1708#endif
1709 } else {
1710 // Serial projection for small geometry sets
1711 projectGeometryToBinsSerial(cone, filtered_uuids, bins);
1712 }
1713}
1714
1715void CollisionDetection::projectGeometryToBinsSerial(const Cone &cone, const std::vector<uint> &filtered_uuids, AngularBins &bins) {
1716 for (uint uuid: filtered_uuids) {
1717 if (context->doesPrimitiveExist(uuid)) {
1718 std::vector<vec3> vertices = context->getPrimitiveVertices(uuid);
1719
1720 // Project each vertex to spherical coordinates
1721 for (const vec3 &vertex: vertices) {
1722 vec3 apex_to_vertex = vertex - cone.apex;
1723 float distance = apex_to_vertex.magnitude();
1724
1725 if (distance > 1e-6f) {
1726 float theta, phi;
1727 cartesianToSphericalCone(apex_to_vertex, cone.axis, theta, phi);
1728
1729 // Skip if outside cone angle
1730 if (phi <= cone.half_angle) {
1731 int theta_bin, phi_bin;
1732 if (sphericalCoordsToBinIndices(theta, phi, bins, theta_bin, phi_bin)) {
1733 bins.setCovered(theta_bin, phi_bin, distance);
1734 }
1735 }
1736 }
1737 }
1738 }
1739 }
1740}
1741
1742std::vector<CollisionDetection::Gap> CollisionDetection::findGapsInCoverageMap(const AngularBins &bins, const Cone &cone) {
1743 std::vector<Gap> gaps;
1744
1745 // Connected component analysis to find contiguous free regions
1746 std::vector<std::vector<bool>> visited(bins.theta_divisions, std::vector<bool>(bins.phi_divisions, false));
1747
1748 for (int theta = 0; theta < bins.theta_divisions; theta++) {
1749 for (int phi = 0; phi < bins.phi_divisions; phi++) {
1750 if (!bins.isCovered(theta, phi) && !visited[theta][phi]) {
1751 // Found start of new gap - flood fill to find full extent
1752 Gap gap = floodFillGap(bins, theta, phi, visited, cone);
1753
1754 // Only keep gaps meeting minimum size thresholds
1755 const float MIN_GAP_SIZE_STERADIANS = 0.01f; // Minimum gap size
1756 if (gap.angular_size > MIN_GAP_SIZE_STERADIANS) {
1757 gaps.push_back(gap);
1758 }
1759 }
1760 }
1761 }
1762
1763 // Sort gaps by angular size (largest first)
1764 std::sort(gaps.begin(), gaps.end(), [](const Gap &a, const Gap &b) { return a.angular_size > b.angular_size; });
1765
1766 return gaps;
1767}
1768
1769CollisionDetection::Gap CollisionDetection::floodFillGap(const AngularBins &bins, int start_theta, int start_phi, std::vector<std::vector<bool>> &visited, const Cone &cone) {
1770 Gap gap;
1771 std::queue<std::pair<int, int>> queue;
1772 queue.push({start_theta, start_phi});
1773
1774 float total_solid_angle = 0;
1775 vec3 weighted_center(0, 0, 0);
1776
1777 while (!queue.empty()) {
1778 auto [theta, phi] = queue.front();
1779 queue.pop();
1780
1781 if (visited[theta][phi] || bins.isCovered(theta, phi))
1782 continue;
1783 visited[theta][phi] = true;
1784
1785 // Calculate solid angle contribution of this bin
1786 float bin_solid_angle = calculateBinSolidAngle(theta, phi, bins, cone.half_angle);
1787 total_solid_angle += bin_solid_angle;
1788
1789 // Accumulate weighted center direction
1790 vec3 bin_direction = binIndicesToCartesian(theta, phi, bins, cone);
1791 weighted_center = weighted_center + bin_direction * bin_solid_angle;
1792
1793 // Add unoccupied neighbors to queue
1794 addUnoccupiedNeighbors(theta, phi, bins, visited, queue);
1795 }
1796
1797 gap.angular_size = total_solid_angle;
1798 gap.center_direction = weighted_center.magnitude() > 1e-6f ? weighted_center.normalize() : cone.axis;
1799
1800 return gap;
1801}
1802
1803float CollisionDetection::calculateBinSolidAngle(int theta_bin, int phi_bin, const AngularBins &bins, float cone_half_angle) {
1804 // Calculate solid angle of a single bin
1805 float theta_step = 2.0f * M_PI / bins.theta_divisions;
1806 float phi_step = cone_half_angle / bins.phi_divisions;
1807
1808 // For small angles, solid angle ≈ θ_step × φ_step × sin(φ)
1809 float phi = (phi_bin + 0.5f) * phi_step;
1810 return theta_step * phi_step * sinf(phi);
1811}
1812
1813vec3 CollisionDetection::binIndicesToCartesian(int theta_bin, int phi_bin, const AngularBins &bins, const Cone &cone) {
1814 // Convert bin indices back to cartesian direction
1815 float theta = (theta_bin + 0.5f) * 2.0f * M_PI / bins.theta_divisions;
1816 float phi = (phi_bin + 0.5f) * cone.half_angle / bins.phi_divisions;
1817
1818 // Create orthonormal basis from cone axis (same as in cartesianToSphericalCone)
1819 vec3 up = (abs(cone.axis.z) < 0.999f) ? make_vec3(0, 0, 1) : make_vec3(1, 0, 0);
1820 vec3 right = cross(cone.axis, up);
1821 right.normalize();
1822 vec3 forward = cross(right, cone.axis);
1823
1824 // Convert spherical to cartesian
1825 float sin_phi = sinf(phi);
1826 float cos_phi = cosf(phi);
1827 float sin_theta = sinf(theta);
1828 float cos_theta = cosf(theta);
1829
1830 vec3 direction = cone.axis * cos_phi + (right * cos_theta + forward * sin_theta) * sin_phi;
1831 return direction.normalize();
1832}
1833
1834void CollisionDetection::addUnoccupiedNeighbors(int theta, int phi, const AngularBins &bins, std::vector<std::vector<bool>> &visited, std::queue<std::pair<int, int>> &queue) {
1835 // Add 4-connected neighbors (up, down, left, right in bin space)
1836 const int neighbors[4][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
1837
1838 for (int i = 0; i < 4; i++) {
1839 int new_theta = theta + neighbors[i][0];
1840 int new_phi = phi + neighbors[i][1];
1841
1842 // Handle theta wraparound (circular)
1843 if (new_theta < 0)
1844 new_theta += bins.theta_divisions;
1845 if (new_theta >= bins.theta_divisions)
1846 new_theta -= bins.theta_divisions;
1847
1848 // Check phi bounds (no wraparound)
1849 if (new_phi >= 0 && new_phi < bins.phi_divisions) {
1850 if (!visited[new_theta][new_phi] && !bins.isCovered(new_theta, new_phi)) {
1851 queue.push({new_theta, new_phi});
1852 }
1853 }
1854 }
1855}
1856
1857// -------- MAIN RASTERIZED FINDOPTIMALCONEPATH IMPLEMENTATION --------
1858
1859CollisionDetection::OptimalPathResult CollisionDetection::findOptimalConePath(const vec3 &apex, const vec3 &centralAxis, float half_angle, float height, int initialSamples) {
1860
1861 OptimalPathResult result;
1862 result.direction = centralAxis;
1863 result.direction.normalize();
1864 result.collisionCount = 0;
1865 result.confidence = 0.0f;
1866
1867 // Validate input parameters
1868 if (initialSamples <= 0 || half_angle <= 0.0f || half_angle > M_PI) {
1869 if (printmessages) {
1870 std::cerr << "WARNING: Invalid parameters for findOptimalConePath" << std::endl;
1871 }
1872 return result;
1873 }
1874
1875 if (bvh_nodes.empty()) {
1876 // No geometry to collide with, central axis is optimal
1877 result.confidence = 1.0f;
1878 return result;
1879 }
1880
1881 // Use original fish-eye camera gap detection algorithm
1882 std::vector<Gap> detected_gaps = detectGapsInCone(apex, centralAxis, half_angle, height, initialSamples);
1883
1884 if (detected_gaps.empty()) {
1885 // No gaps found, fall back to central axis
1886 // if (printmessages) {
1887 // std::cerr << "WARNING: No gaps detected in cone, using central axis" << std::endl;
1888 // }
1889 result.confidence = 0.1f;
1890 return result;
1891 }
1892
1893 // Score gaps using fish-eye metric
1894 scoreGapsByFishEyeMetric(detected_gaps, centralAxis);
1895
1896 // Find optimal direction toward highest-scoring gap
1897 result.direction = findOptimalGapDirection(detected_gaps, centralAxis);
1898
1899 // Count collisions along optimal direction for reporting using modern ray-tracing
1900 float max_distance = (height > 0.0f) ? height : -1.0f;
1901 HitResult direction_hit = castRay(apex, result.direction, max_distance);
1902 result.collisionCount = direction_hit.hit ? 1 : 0;
1903
1904 // Calculate confidence based on gap quality
1905 if (!detected_gaps.empty()) {
1906 // Higher confidence for larger, well-defined gaps
1907 const Gap &best_gap = detected_gaps[0]; // Assuming first is best after sorting
1908 result.confidence = std::min(1.0f, best_gap.angular_size * 10.0f); // Scale angular size to confidence
1909 }
1910
1911 return result;
1912}
1913
1914#ifdef HELIOS_CUDA_AVAILABLE
1915void CollisionDetection::allocateGPUMemory() {
1916 if (gpu_memory_allocated) {
1917 freeGPUMemory(); // Clean up existing allocation
1918 }
1919
1920 if (bvh_nodes.empty() || primitive_indices.empty()) {
1921 return; // Nothing to allocate
1922 }
1923
1924 // Initialize pointers to nullptr for safety
1925 d_bvh_nodes = nullptr;
1926 d_primitive_indices = nullptr;
1927
1928 // Check if a usable GPU is actually available at runtime (also honors HELIOS_NO_GPU)
1929 if (!isGPUAvailable()) {
1930 // No usable GPU - disable GPU acceleration and fall back to CPU
1931 if (printmessages) {
1932 std::cout << "WARNING (CollisionDetection::allocateGPUMemory): No usable GPU available. Falling back to CPU-only mode." << std::endl;
1933 }
1934 gpu_acceleration_enabled = false;
1935 return;
1936 }
1937
1938 // Calculate sizes
1939 size_t bvh_size = bvh_nodes.size() * sizeof(GPUBVHNode);
1940 size_t indices_size = primitive_indices.size() * sizeof(uint);
1941
1942 // Validate sizes are reasonable
1943 if (bvh_size == 0 || indices_size == 0) {
1944 helios_runtime_error("ERROR: Invalid BVH or primitive data sizes for GPU allocation");
1945 }
1946
1947 // Allocate BVH nodes
1948 cudaError_t err = cudaMalloc(&d_bvh_nodes, bvh_size);
1949 if (err != cudaSuccess) {
1950 // GPU allocation failed - fall back to CPU instead of crashing
1951 if (printmessages) {
1952 std::cout << "WARNING (CollisionDetection::allocateGPUMemory): Failed to allocate GPU memory (" << cudaGetErrorString(err) << "). Falling back to CPU-only mode." << std::endl;
1953 }
1954 gpu_acceleration_enabled = false;
1955 return;
1956 }
1957
1958 // Allocate primitive indices
1959 err = cudaMalloc((void **) &d_primitive_indices, indices_size);
1960 if (err != cudaSuccess) {
1961 // GPU allocation failed - clean up and fall back to CPU
1962 cudaFree(d_bvh_nodes);
1963 d_bvh_nodes = nullptr;
1964 if (printmessages) {
1965 std::cout << "WARNING (CollisionDetection::allocateGPUMemory): Failed to allocate primitive indices (" << cudaGetErrorString(err) << "). Falling back to CPU-only mode." << std::endl;
1966 }
1967 gpu_acceleration_enabled = false;
1968 return;
1969 }
1970
1971 // Mark as allocated only after both allocations succeeded
1972 gpu_memory_allocated = true;
1973}
1974#endif
1975
1976#ifdef HELIOS_CUDA_AVAILABLE
1977void CollisionDetection::freeGPUMemory() {
1978 if (!gpu_memory_allocated)
1979 return;
1980
1981 if (d_bvh_nodes) {
1982 cudaFree(d_bvh_nodes);
1983 d_bvh_nodes = nullptr;
1984 }
1985
1986 if (d_primitive_indices) {
1987 cudaFree(d_primitive_indices);
1988 d_primitive_indices = nullptr;
1989 }
1990
1991 if (d_primitive_types) {
1992 cudaFree(d_primitive_types);
1993 d_primitive_types = nullptr;
1994 }
1995
1996 if (d_primitive_vertices) {
1997 cudaFree(d_primitive_vertices);
1998 d_primitive_vertices = nullptr;
1999 }
2000
2001 if (d_vertex_offsets) {
2002 cudaFree(d_vertex_offsets);
2003 d_vertex_offsets = nullptr;
2004 }
2005
2006 if (d_mask_data) {
2007 cudaFree(d_mask_data);
2008 d_mask_data = nullptr;
2009 }
2010 if (d_mask_offsets) {
2011 cudaFree(d_mask_offsets);
2012 d_mask_offsets = nullptr;
2013 }
2014 if (d_mask_sizes) {
2015 cudaFree(d_mask_sizes);
2016 d_mask_sizes = nullptr;
2017 }
2018 if (d_mask_IDs) {
2019 cudaFree(d_mask_IDs);
2020 d_mask_IDs = nullptr;
2021 }
2022 if (d_uv_data) {
2023 cudaFree(d_uv_data);
2024 d_uv_data = nullptr;
2025 }
2026 if (d_uv_IDs) {
2027 cudaFree(d_uv_IDs);
2028 d_uv_IDs = nullptr;
2029 }
2030 d_gpu_has_masks = false;
2031
2032 d_gpu_node_count = 0;
2033 d_gpu_primitive_count = 0;
2034 d_gpu_total_vertex_count = 0;
2035
2036 gpu_memory_allocated = false;
2037}
2038#endif
2039
2040#ifdef HELIOS_CUDA_AVAILABLE
2041void CollisionDetection::transferBVHToGPU() {
2042 if (!gpu_acceleration_enabled || bvh_nodes.empty()) {
2043 return;
2044 }
2045
2046 // Always reallocate GPU memory to handle size changes
2047 if (gpu_memory_allocated) {
2048 freeGPUMemory();
2049 }
2050 allocateGPUMemory();
2051
2052 // Re-check if GPU acceleration is still enabled after allocation attempt
2053 // allocateGPUMemory() may have disabled it due to lack of GPU hardware
2054 if (!gpu_acceleration_enabled) {
2055 return; // Gracefully fall back to CPU mode
2056 }
2057
2058 // Verify allocation succeeded
2059 if (!gpu_memory_allocated || d_bvh_nodes == nullptr || d_primitive_indices == nullptr) {
2060 helios_runtime_error("ERROR: Failed to allocate GPU memory for BVH transfer");
2061 }
2062
2063 // Convert CPU BVH to GPU format
2064 std::vector<GPUBVHNode> gpu_nodes(bvh_nodes.size());
2065 for (size_t i = 0; i < bvh_nodes.size(); i++) {
2066 const BVHNode &cpu_node = bvh_nodes[i];
2067 GPUBVHNode &gpu_node = gpu_nodes[i];
2068
2069 gpu_node.aabb_min = heliosVecToFloat3(cpu_node.aabb_min);
2070 gpu_node.aabb_max = heliosVecToFloat3(cpu_node.aabb_max);
2071 gpu_node.left_child = cpu_node.left_child;
2072 gpu_node.right_child = cpu_node.right_child;
2073 gpu_node.primitive_start = cpu_node.primitive_start;
2074 gpu_node.primitive_count = cpu_node.primitive_count;
2075 gpu_node.is_leaf = cpu_node.is_leaf ? 1 : 0;
2076 gpu_node.padding = 0;
2077 }
2078
2079 // Transfer to GPU
2080 cudaError_t err = cudaMemcpy(d_bvh_nodes, gpu_nodes.data(), gpu_nodes.size() * sizeof(GPUBVHNode), cudaMemcpyHostToDevice);
2081 if (err != cudaSuccess) {
2082 helios_runtime_error("CUDA error transferring BVH nodes: " + std::string(cudaGetErrorString(err)));
2083 }
2084
2085 err = cudaMemcpy(d_primitive_indices, primitive_indices.data(), primitive_indices.size() * sizeof(uint), cudaMemcpyHostToDevice);
2086 if (err != cudaSuccess) {
2087 helios_runtime_error("CUDA error transferring primitive indices: " + std::string(cudaGetErrorString(err)));
2088 }
2089
2090 // Upload the full scene geometry (primitive types, packed vertices, vertex offsets) so it stays resident on the
2091 // device across many chunked ray casts. Previously every GPU ray batch re-extracted and re-uploaded all of this;
2092 // making it resident is what keeps chunked synthetic scans from re-uploading the scene once per chunk.
2093 std::vector<int> primitive_types;
2094 std::vector<float> primitive_vertices_xyz; // flat xyz, 4 vertices (12 floats) per primitive
2095 std::vector<unsigned int> vertex_offsets;
2096 std::vector<unsigned char> mask_data;
2097 std::vector<unsigned int> mask_offsets;
2098 std::vector<int> mask_sizes;
2099 std::vector<int> mask_IDs;
2100 std::vector<float> uv_data;
2101 std::vector<int> uv_IDs;
2102 buildGPUGeometrySoA(primitive_types, primitive_vertices_xyz, vertex_offsets, mask_data, mask_offsets, mask_sizes, mask_IDs, uv_data, uv_IDs);
2103
2104 d_gpu_node_count = static_cast<int>(bvh_nodes.size());
2105 d_gpu_primitive_count = static_cast<int>(primitive_indices.size());
2106 d_gpu_total_vertex_count = static_cast<int>(primitive_vertices_xyz.size() / 3);
2107
2108 const size_t types_size = primitive_types.size() * sizeof(int);
2109 const size_t vertices_size = primitive_vertices_xyz.size() * sizeof(float); // == total_vertex_count * sizeof(float3)
2110 const size_t offsets_size = vertex_offsets.size() * sizeof(unsigned int);
2111
2112 if ((err = cudaMalloc(&d_primitive_types, types_size)) != cudaSuccess) {
2113 helios_runtime_error("CUDA error allocating GPU primitive types: " + std::string(cudaGetErrorString(err)));
2114 }
2115 if ((err = cudaMalloc(&d_primitive_vertices, vertices_size)) != cudaSuccess) {
2116 helios_runtime_error("CUDA error allocating GPU primitive vertices: " + std::string(cudaGetErrorString(err)));
2117 }
2118 if ((err = cudaMalloc(&d_vertex_offsets, offsets_size)) != cudaSuccess) {
2119 helios_runtime_error("CUDA error allocating GPU vertex offsets: " + std::string(cudaGetErrorString(err)));
2120 }
2121
2122 if ((err = cudaMemcpy(d_primitive_types, primitive_types.data(), types_size, cudaMemcpyHostToDevice)) != cudaSuccess) {
2123 helios_runtime_error("CUDA error transferring primitive types: " + std::string(cudaGetErrorString(err)));
2124 }
2125 if ((err = cudaMemcpy(d_primitive_vertices, primitive_vertices_xyz.data(), vertices_size, cudaMemcpyHostToDevice)) != cudaSuccess) {
2126 helios_runtime_error("CUDA error transferring primitive vertices: " + std::string(cudaGetErrorString(err)));
2127 }
2128 if ((err = cudaMemcpy(d_vertex_offsets, vertex_offsets.data(), offsets_size, cudaMemcpyHostToDevice)) != cudaSuccess) {
2129 helios_runtime_error("CUDA error transferring vertex offsets: " + std::string(cudaGetErrorString(err)));
2130 }
2131
2132 // Upload the texture-transparency SoA so the kernel can reject hits on transparent texels (parity with the CPU
2133 // isHitTexelOpaque path). The per-primitive mask_IDs/uv arrays always upload (one int per primitive, cheap); the
2134 // potentially-large mask pixel data and per-mask metadata only upload when at least one primitive has a mask.
2135 d_gpu_has_masks = !mask_offsets.empty();
2136
2137 const size_t mask_IDs_size = mask_IDs.size() * sizeof(int);
2138 const size_t uv_data_size = uv_data.size() * sizeof(float);
2139 const size_t uv_IDs_size = uv_IDs.size() * sizeof(int);
2140 if ((err = cudaMalloc(&d_mask_IDs, mask_IDs_size)) != cudaSuccess) {
2141 helios_runtime_error("CUDA error allocating GPU mask IDs: " + std::string(cudaGetErrorString(err)));
2142 }
2143 if ((err = cudaMalloc(&d_uv_data, uv_data_size)) != cudaSuccess) {
2144 helios_runtime_error("CUDA error allocating GPU UV data: " + std::string(cudaGetErrorString(err)));
2145 }
2146 if ((err = cudaMalloc(&d_uv_IDs, uv_IDs_size)) != cudaSuccess) {
2147 helios_runtime_error("CUDA error allocating GPU UV IDs: " + std::string(cudaGetErrorString(err)));
2148 }
2149 if ((err = cudaMemcpy(d_mask_IDs, mask_IDs.data(), mask_IDs_size, cudaMemcpyHostToDevice)) != cudaSuccess) {
2150 helios_runtime_error("CUDA error transferring mask IDs: " + std::string(cudaGetErrorString(err)));
2151 }
2152 if ((err = cudaMemcpy(d_uv_data, uv_data.data(), uv_data_size, cudaMemcpyHostToDevice)) != cudaSuccess) {
2153 helios_runtime_error("CUDA error transferring UV data: " + std::string(cudaGetErrorString(err)));
2154 }
2155 if ((err = cudaMemcpy(d_uv_IDs, uv_IDs.data(), uv_IDs_size, cudaMemcpyHostToDevice)) != cudaSuccess) {
2156 helios_runtime_error("CUDA error transferring UV IDs: " + std::string(cudaGetErrorString(err)));
2157 }
2158
2159 if (d_gpu_has_masks) {
2160 const size_t mask_data_size = mask_data.size() * sizeof(unsigned char);
2161 const size_t mask_offsets_size = mask_offsets.size() * sizeof(unsigned int);
2162 const size_t mask_sizes_size = mask_sizes.size() * sizeof(int);
2163 if ((err = cudaMalloc(&d_mask_data, mask_data_size)) != cudaSuccess) {
2164 helios_runtime_error("CUDA error allocating GPU mask data: " + std::string(cudaGetErrorString(err)));
2165 }
2166 if ((err = cudaMalloc(&d_mask_offsets, mask_offsets_size)) != cudaSuccess) {
2167 helios_runtime_error("CUDA error allocating GPU mask offsets: " + std::string(cudaGetErrorString(err)));
2168 }
2169 if ((err = cudaMalloc(&d_mask_sizes, mask_sizes_size)) != cudaSuccess) {
2170 helios_runtime_error("CUDA error allocating GPU mask sizes: " + std::string(cudaGetErrorString(err)));
2171 }
2172 if ((err = cudaMemcpy(d_mask_data, mask_data.data(), mask_data_size, cudaMemcpyHostToDevice)) != cudaSuccess) {
2173 helios_runtime_error("CUDA error transferring mask data: " + std::string(cudaGetErrorString(err)));
2174 }
2175 if ((err = cudaMemcpy(d_mask_offsets, mask_offsets.data(), mask_offsets_size, cudaMemcpyHostToDevice)) != cudaSuccess) {
2176 helios_runtime_error("CUDA error transferring mask offsets: " + std::string(cudaGetErrorString(err)));
2177 }
2178 if ((err = cudaMemcpy(d_mask_sizes, mask_sizes.data(), mask_sizes_size, cudaMemcpyHostToDevice)) != cudaSuccess) {
2179 helios_runtime_error("CUDA error transferring mask sizes: " + std::string(cudaGetErrorString(err)));
2180 }
2181 }
2182}
2183
2184void CollisionDetection::buildGPUGeometrySoA(std::vector<int> &primitive_types, std::vector<float> &primitive_vertices_xyz, std::vector<unsigned int> &vertex_offsets, std::vector<unsigned char> &mask_data, std::vector<unsigned int> &mask_offsets,
2185 std::vector<int> &mask_sizes, std::vector<int> &mask_IDs, std::vector<float> &uv_data, std::vector<int> &uv_IDs) {
2186 // Pack each primitive (in primitive_indices/BVH order) into exactly 4 vertices so vertex_offset == i*4. Triangles
2187 // use 3 vertices + 1 pad, patches use 4, voxels store [min, max, pad, pad]. This mirrors the packing the kernel
2188 // expects (CollisionDetection.cu rayPrimitiveBVHKernel) and the convention castRaysGPU previously built inline.
2189 const size_t nprim = primitive_indices.size();
2190 primitive_types.assign(nprim, 0);
2191 vertex_offsets.assign(nprim, 0);
2192 primitive_vertices_xyz.clear();
2193 primitive_vertices_xyz.reserve(nprim * 4 * 3);
2194
2195 // Texture-transparency SoA, mirroring the CPU isHitTexelOpaque() path so a GPU ray passes through transparent texels.
2196 // Masks are de-duplicated by texture file (many leaf patches share one PNG), so mask_offsets/mask_sizes index a small
2197 // set of distinct masks while mask_IDs is per-primitive. uv_data carries 4 vec2 per primitive (xy floats, padded);
2198 // uv_IDs[i] >= 0 marks a primitive that has explicit per-vertex UVs (else parametric for patches, opaque for triangles).
2199 mask_data.clear();
2200 mask_offsets.clear();
2201 mask_sizes.clear();
2202 mask_IDs.assign(nprim, -1);
2203 uv_data.assign(nprim * 4 * 2, 0.f);
2204 uv_IDs.assign(nprim, -1);
2205 std::map<std::string, int> texture_to_mask_idx; // texture file -> index into mask_offsets/mask_sizes
2206
2207 helios::WarningAggregator primitive_vertex_warnings;
2208 primitive_vertex_warnings.setEnabled(printmessages);
2209
2210 auto push_vertex = [&](const helios::vec3 &v) {
2211 primitive_vertices_xyz.push_back(v.x);
2212 primitive_vertices_xyz.push_back(v.y);
2213 primitive_vertices_xyz.push_back(v.z);
2214 };
2215 auto push_zero = [&]() { push_vertex(make_vec3(0, 0, 0)); };
2216
2217 unsigned int vertex_index = 0;
2218 for (size_t i = 0; i < nprim; i++) {
2219 vertex_offsets[i] = vertex_index;
2220
2221 const uint UUID = primitive_indices[i];
2222 PrimitiveType ptype = context->getPrimitiveType(UUID);
2223 primitive_types[i] = static_cast<int>(ptype);
2224
2225 // Texture transparency: only patches/triangles carry a mask. De-duplicate the flattened mask by texture file.
2226 if ((ptype == PRIMITIVE_TYPE_PATCH || ptype == PRIMITIVE_TYPE_TRIANGLE) && context->primitiveTextureHasTransparencyChannel(UUID)) {
2227 const std::string texfile = context->getPrimitiveTextureFile(UUID);
2228 auto cached = texture_to_mask_idx.find(texfile);
2229 int mask_idx;
2230 if (cached != texture_to_mask_idx.end()) {
2231 mask_idx = cached->second;
2232 } else {
2233 const std::vector<std::vector<bool>> *trans = context->getPrimitiveTextureTransparencyData(UUID);
2234 helios::int2 tex_size = context->getPrimitiveTextureSize(UUID);
2235 mask_idx = static_cast<int>(mask_offsets.size());
2236 mask_offsets.push_back(static_cast<unsigned int>(mask_data.size()));
2237 mask_sizes.push_back(tex_size.x);
2238 mask_sizes.push_back(tex_size.y);
2239 // Flatten row-major [y][x] (row 0 = top) into bytes, matching the kernel's offset + y*width + x lookup.
2240 for (int y = 0; y < tex_size.y; y++) {
2241 for (int x = 0; x < tex_size.x; x++) {
2242 mask_data.push_back((trans != nullptr && y < static_cast<int>(trans->size()) && x < static_cast<int>((*trans)[y].size()) && (*trans)[y][x]) ? 1u : 0u);
2243 }
2244 }
2245 texture_to_mask_idx[texfile] = mask_idx;
2246 }
2247 mask_IDs[i] = mask_idx;
2248
2249 // Store per-vertex UVs when present (patch needs 4, triangle needs 3) so the kernel reproduces the CPU
2250 // interpolation. Missing/short UV sets leave uv_IDs[i] = -1 (patch falls back to parametric, triangle to solid).
2251 std::vector<vec2> uv = context->getPrimitiveTextureUV(UUID);
2252 const size_t need = (ptype == PRIMITIVE_TYPE_PATCH) ? 4 : 3;
2253 if (uv.size() >= need) {
2254 for (size_t v = 0; v < need; v++) {
2255 uv_data[i * 8 + v * 2 + 0] = uv[v].x;
2256 uv_data[i * 8 + v * 2 + 1] = uv[v].y;
2257 }
2258 uv_IDs[i] = 1;
2259 }
2260 }
2261
2262 std::vector<vec3> vertices = context->getPrimitiveVertices(UUID);
2263
2264 if (ptype == PRIMITIVE_TYPE_TRIANGLE) {
2265 if (vertices.size() >= 3) {
2266 for (int v = 0; v < 3; v++) {
2267 push_vertex(vertices[v]);
2268 }
2269 push_zero(); // pad to 4
2270 } else {
2271 primitive_vertex_warnings.addWarning("triangle_wrong_vertex_count", "Triangle primitive " + std::to_string(primitive_indices[i]) + " has " + std::to_string(vertices.size()) + " vertices");
2272 for (int v = 0; v < 4; v++) {
2273 push_zero();
2274 }
2275 }
2276 } else if (ptype == PRIMITIVE_TYPE_PATCH) {
2277 if (vertices.size() >= 4) {
2278 for (int v = 0; v < 4; v++) {
2279 push_vertex(vertices[v]);
2280 }
2281 } else {
2282 primitive_vertex_warnings.addWarning("patch_wrong_vertex_count", "Patch primitive " + std::to_string(primitive_indices[i]) + " has " + std::to_string(vertices.size()) + " vertices");
2283 for (int v = 0; v < 4; v++) {
2284 push_zero();
2285 }
2286 }
2287 } else if (ptype == PRIMITIVE_TYPE_VOXEL) {
2288 vec3 voxel_min = vertices.empty() ? make_vec3(0, 0, 0) : vertices[0];
2289 vec3 voxel_max = voxel_min;
2290 for (const auto &vertex: vertices) {
2291 voxel_min.x = std::min(voxel_min.x, vertex.x);
2292 voxel_min.y = std::min(voxel_min.y, vertex.y);
2293 voxel_min.z = std::min(voxel_min.z, vertex.z);
2294 voxel_max.x = std::max(voxel_max.x, vertex.x);
2295 voxel_max.y = std::max(voxel_max.y, vertex.y);
2296 voxel_max.z = std::max(voxel_max.z, vertex.z);
2297 }
2298 push_vertex(voxel_min); // v0 = min
2299 push_vertex(voxel_max); // v1 = max
2300 push_zero();
2301 push_zero();
2302 } else {
2303 for (int v = 0; v < 4; v++) {
2304 push_zero();
2305 }
2306 }
2307 vertex_index += 4;
2308 }
2309
2310 primitive_vertex_warnings.report(std::cerr);
2311}
2312#endif
2313
2314void CollisionDetection::markBVHDirty() {
2315 // Clear internal tracking so BVH will be rebuilt on next access
2316 last_processed_uuids.clear();
2317 last_processed_deleted_uuids.clear();
2318 last_bvh_geometry.clear();
2319 bvh_dirty = true;
2320
2321 // Note: Don't clear primitive_cache here - it will be cleared only when
2322 // buildBVH() detects actual primitive set changes, not just geometry updates
2323
2324 // Free GPU memory since BVH will be rebuilt
2325#ifdef HELIOS_CUDA_AVAILABLE
2326 freeGPUMemory();
2327#endif
2328}
2329
2330void CollisionDetection::incrementalUpdateBVH(const std::set<uint> &added_geometry, const std::set<uint> &removed_geometry, const std::set<uint> &final_geometry) {
2331
2332 // Validate new geometries exist first
2333 for (uint uuid: added_geometry) {
2334 if (!context->doesPrimitiveExist(uuid)) {
2335 if (printmessages) {
2336 std::cerr << "Warning: Added primitive " << uuid << " does not exist, falling back to full rebuild" << std::endl;
2337 }
2338 std::vector<uint> final_primitives(final_geometry.begin(), final_geometry.end());
2339 buildBVH(final_primitives);
2340 return;
2341 }
2342 }
2343
2344 // For plant growth scenarios, most changes are additions (new leaves/branches)
2345 // We can optimize for this by caching primitive AABBs and selective reconstruction
2346
2347 // Update primitive AABB cache for new primitives
2348 for (uint uuid: added_geometry) {
2349 updatePrimitiveAABBCache(uuid);
2350 }
2351
2352 // Remove old primitives from cache
2353 for (uint uuid: removed_geometry) {
2354 primitive_aabbs_cache.erase(uuid);
2355 }
2356
2357 // For incremental updates, we use a two-stage approach:
2358 // 1. If the number of changes is small relative to tree size, do targeted insertion
2359 // 2. Otherwise, do optimized rebuild with cached AABBs
2360
2361 size_t total_changes = added_geometry.size() + removed_geometry.size();
2362 size_t current_size = final_geometry.size();
2363
2364 // If changes are very small (<5% of tree), try targeted insertion
2365 if (current_size > 0 && !bvh_nodes.empty() && (float(total_changes) / float(current_size)) < 0.05f) {
2366 // For very small changes, targeted insertion can be beneficial
2367 bool insertion_successful = true;
2368
2369 // Remove primitives from primitive_indices
2370 if (!removed_geometry.empty()) {
2371 primitive_indices.erase(std::remove_if(primitive_indices.begin(), primitive_indices.end(), [&removed_geometry](uint uuid) { return removed_geometry.find(uuid) != removed_geometry.end(); }), primitive_indices.end());
2372 }
2373
2374 // Add new primitives to primitive_indices
2375 for (uint uuid: added_geometry) {
2376 primitive_indices.push_back(uuid);
2377 }
2378
2379 // For small changes, rebuild only affected subtrees by invalidating nodes
2380 // This is more efficient than full rebuild for tiny changes
2381 if (insertion_successful && total_changes < 50) {
2382 if (printmessages) {
2383 std::cout << "Using targeted tree update for " << total_changes << " changes" << std::endl;
2384 }
2385
2386 // Mark BVH as needing rebalance but keep existing structure where possible
2387 // For now, we do a fast rebuild since true incremental tree rebalancing
2388 // requires complex algorithms that may not be worth the complexity
2389 optimizedRebuildBVH(final_geometry);
2390 return;
2391 }
2392 }
2393
2394 // Fall back to optimized full rebuild using cached AABBs
2395 std::vector<uint> final_primitives(final_geometry.begin(), final_geometry.end());
2396 buildBVH(final_primitives);
2397
2398 // Update tracking
2399 last_bvh_geometry = final_geometry;
2400 bvh_dirty = false;
2401 soa_dirty = true; // SoA needs rebuild after BVH change
2402}
2403
2404bool CollisionDetection::validateUUIDs(const std::vector<uint> &UUIDs) const {
2406 warnings.setEnabled(printmessages);
2407
2408 bool all_valid = true;
2409 for (uint UUID: UUIDs) {
2410 if (!context->doesPrimitiveExist(UUID)) {
2411 warnings.addWarning("primitive_uuid_not_exist", "Primitive UUID " + std::to_string(UUID) + " does not exist - skipping");
2412 all_valid = false;
2413 }
2414 }
2415
2416 warnings.report(std::cerr);
2417 return all_valid;
2418}
2419
2420bool CollisionDetection::rayPrimitiveIntersection(const vec3 &origin, const vec3 &direction, uint primitive_UUID, float &distance) const {
2421 // Check if primitive exists first
2422 if (!context->doesPrimitiveExist(primitive_UUID)) {
2423 return false;
2424 }
2425
2426 try {
2427 // Get primitive type and vertices
2428 PrimitiveType type = context->getPrimitiveType(primitive_UUID);
2429 std::vector<vec3> vertices = context->getPrimitiveVertices(primitive_UUID);
2430
2431 if (vertices.empty()) {
2432 return false;
2433 }
2434
2435
2436 bool hit = false;
2437 float min_distance = std::numeric_limits<float>::max();
2438
2439 if (type == PRIMITIVE_TYPE_TRIANGLE) {
2440 // Triangle intersection using radiation model algorithm (proven to work)
2441 if (vertices.size() >= 3) {
2442 const vec3 &v0 = vertices[0];
2443 const vec3 &v1 = vertices[1];
2444 const vec3 &v2 = vertices[2];
2445
2446 // Use the same algorithm as radiation model's triangle_intersect
2447 float a = v0.x - v1.x, b = v0.x - v2.x, c = direction.x, d = v0.x - origin.x;
2448 float e = v0.y - v1.y, f = v0.y - v2.y, g = direction.y, h = v0.y - origin.y;
2449 float i = v0.z - v1.z, j = v0.z - v2.z, k = direction.z, l = v0.z - origin.z;
2450
2451 float m = f * k - g * j, n = h * k - g * l, p = f * l - h * j;
2452 float q = g * i - e * k, s = e * j - f * i;
2453
2454 float denom = a * m + b * q + c * s;
2455 if (std::abs(denom) < 1e-8f) {
2456 return false; // Ray is parallel to triangle
2457 }
2458
2459 float inv_denom = 1.0f / denom;
2460
2461 float e1 = d * m - b * n - c * p;
2462 float beta = e1 * inv_denom;
2463
2464 if (beta >= 0.0f) {
2465 float r = e * l - h * i;
2466 float e2 = a * n + d * q + c * r;
2467 float gamma = e2 * inv_denom;
2468
2469 if (gamma >= 0.0f && beta + gamma <= 1.0f) {
2470 float e3 = a * p - b * r + d * s;
2471 float t = e3 * inv_denom;
2472
2473 if (t > 1e-8f && t < min_distance) {
2474 min_distance = t;
2475 hit = true;
2476 }
2477 }
2478 }
2479 }
2480 } else if (type == PRIMITIVE_TYPE_PATCH) {
2481 // Patch (quadrilateral) intersection using radiation model algorithm
2482 if (vertices.size() >= 4) {
2483 const vec3 &v0 = vertices[0];
2484 const vec3 &v1 = vertices[1];
2485 const vec3 &v2 = vertices[2];
2486 const vec3 &v3 = vertices[3];
2487
2488 // Calculate patch vectors and normal (same as radiation model)
2489 vec3 anchor = v0;
2490 vec3 normal = cross(v1 - v0, v2 - v0);
2491 normal.normalize();
2492
2493 vec3 a = v1 - v0; // First edge vector
2494 vec3 b = v3 - v0; // Second edge vector
2495
2496 // Ray-plane intersection
2497 float denom = direction * normal;
2498 if (std::abs(denom) > 1e-8f) { // Not parallel to plane
2499 float t = (anchor - origin) * normal / denom;
2500
2501 if (t > 1e-8f && t < 1e8f) { // Valid intersection distance
2502 // Find intersection point
2503 vec3 p = origin + direction * t;
2504 vec3 d = p - anchor;
2505
2506 // Project onto patch coordinate system
2507 float ddota = d * a;
2508 float ddotb = d * b;
2509
2510 // Check if point is within patch bounds
2511 if (ddota >= 0.0f && ddota <= (a * a) && ddotb >= 0.0f && ddotb <= (b * b)) {
2512
2513 if (t < min_distance) {
2514 min_distance = t;
2515 hit = true;
2516 }
2517 }
2518 }
2519 }
2520 }
2521 } else if (type == PRIMITIVE_TYPE_VOXEL) {
2522 // Voxel (AABB) intersection using slab method
2523 if (vertices.size() == 8) {
2524 // Calculate AABB from 8 vertices
2525 vec3 aabb_min = vertices[0];
2526 vec3 aabb_max = vertices[0];
2527
2528 for (int i = 1; i < 8; i++) {
2529 aabb_min.x = std::min(aabb_min.x, vertices[i].x);
2530 aabb_min.y = std::min(aabb_min.y, vertices[i].y);
2531 aabb_min.z = std::min(aabb_min.z, vertices[i].z);
2532 aabb_max.x = std::max(aabb_max.x, vertices[i].x);
2533 aabb_max.y = std::max(aabb_max.y, vertices[i].y);
2534 aabb_max.z = std::max(aabb_max.z, vertices[i].z);
2535 }
2536
2537 // Ray-AABB intersection using slab method
2538 float t_near = -std::numeric_limits<float>::max();
2539 float t_far = std::numeric_limits<float>::max();
2540
2541 // Check intersection with each slab (X, Y, Z)
2542 for (int i = 0; i < 3; i++) {
2543 float ray_dir_component = (i == 0) ? direction.x : (i == 1) ? direction.y : direction.z;
2544 float ray_orig_component = (i == 0) ? origin.x : (i == 1) ? origin.y : origin.z;
2545 float aabb_min_component = (i == 0) ? aabb_min.x : (i == 1) ? aabb_min.y : aabb_min.z;
2546 float aabb_max_component = (i == 0) ? aabb_max.x : (i == 1) ? aabb_max.y : aabb_max.z;
2547
2548 if (std::abs(ray_dir_component) < 1e-8f) {
2549 // Ray is parallel to slab
2550 if (ray_orig_component < aabb_min_component || ray_orig_component > aabb_max_component) {
2551 return false; // Ray is outside slab and parallel - no intersection
2552 }
2553 } else {
2554 // Calculate intersection distances for this slab
2555 float t1 = (aabb_min_component - ray_orig_component) / ray_dir_component;
2556 float t2 = (aabb_max_component - ray_orig_component) / ray_dir_component;
2557
2558 // Ensure t1 <= t2
2559 if (t1 > t2) {
2560 std::swap(t1, t2);
2561 }
2562
2563 // Update near and far intersection distances
2564 t_near = std::max(t_near, t1);
2565 t_far = std::min(t_far, t2);
2566
2567 // Early exit if no intersection possible
2568 if (t_near > t_far) {
2569 return false;
2570 }
2571 }
2572 }
2573
2574 // Check if intersection is in front of ray origin
2575 if (t_far >= 0.0f && t_near < min_distance) {
2576 // Use t_near if it's positive (ray starts outside box), otherwise t_far (ray starts inside box)
2577 float intersection_distance = (t_near >= 1e-8f) ? t_near : t_far;
2578 if (intersection_distance >= 1e-8f) {
2579 min_distance = intersection_distance;
2580 hit = true;
2581 }
2582 }
2583 }
2584 }
2585
2586 if (hit) {
2587 distance = min_distance;
2588 return true;
2589 }
2590
2591 return false;
2592 } catch (const std::exception &e) {
2593 // Primitive no longer exists or can't be accessed
2594 return false;
2595 }
2596}
2597
2598void CollisionDetection::calculateGridIntersection(const vec3 &grid_center, const vec3 &grid_size, const helios::int3 &grid_divisions, const std::vector<uint> &UUIDs) {
2599 // Use slicePrimitivesUsingGrid to populate grid_cells
2600 std::vector<uint> uuids_to_process = UUIDs.empty() ? context->getAllUUIDs() : UUIDs;
2601
2602 // Filter to only non-voxel primitives
2603 std::vector<uint> planar_primitives;
2604 for (uint uuid: uuids_to_process) {
2605 if (context->getPrimitiveType(uuid) != PRIMITIVE_TYPE_VOXEL) {
2606 planar_primitives.push_back(uuid);
2607 }
2608 }
2609
2610 // This will populate grid_cells
2611 slicePrimitivesUsingGrid(planar_primitives, grid_center, grid_size, grid_divisions);
2612}
2613
2614std::vector<std::vector<std::vector<std::vector<uint>>>> CollisionDetection::getGridCells() {
2615 return grid_cells;
2616}
2617
2618std::vector<uint> CollisionDetection::getGridIntersections(int i, int j, int k) {
2619 if (i < 0 || i >= static_cast<int>(grid_cells.size()) || j < 0 || j >= static_cast<int>(grid_cells[i].size()) || k < 0 || k >= static_cast<int>(grid_cells[i][j].size())) {
2620 helios_runtime_error("ERROR (CollisionDetection::getGridIntersections): Grid indices out of bounds");
2621 }
2622 return grid_cells[i][j][k];
2623}
2624
2625int CollisionDetection::optimizeLayout(const std::vector<uint> &UUIDs, float learning_rate, int max_iterations) {
2626 if (printmessages) {
2627 std::cerr << "WARNING: optimizeLayout not yet implemented" << std::endl;
2628 }
2629 return 0;
2630}
2631
2632int CollisionDetection::countRayIntersections(const vec3 &origin, const vec3 &direction, float max_distance) {
2633
2634 int intersection_count = 0;
2635
2636 if (bvh_nodes.empty()) {
2637 return intersection_count;
2638 }
2639
2640 // OPTIMIZATION: Minimum distance threshold to avoid self-intersection with nearby geometry
2641 // This prevents plant's own geometry (shoot tips, etc.) from occluding the entire cone view
2642 float min_distance = 0.05f; // 5cm minimum distance - ignore intersections closer than this
2643
2644 // Ensure the BVH is current before traversal
2645 const_cast<CollisionDetection *>(this)->ensureBVHCurrent();
2646
2647 // Stack-based traversal to avoid recursion
2648 std::vector<uint> node_stack;
2649 node_stack.push_back(0); // Start with root node
2650
2651 while (!node_stack.empty()) {
2652 uint node_idx = node_stack.back();
2653 node_stack.pop_back();
2654
2655 if (node_idx >= bvh_nodes.size())
2656 continue;
2657
2658 const BVHNode &node = bvh_nodes[node_idx];
2659
2660 // Test if ray intersects node AABB
2661 float t_min, t_max;
2662 if (!rayAABBIntersect(origin, direction, node.aabb_min, node.aabb_max, t_min, t_max)) {
2663 continue;
2664 }
2665
2666 // Check if intersection is within distance range (both min and max)
2667 if (t_max < min_distance) {
2668 continue; // Entire AABB is too close - skip
2669 }
2670 if (max_distance > 0.0f && t_min > max_distance) {
2671 continue; // Entire AABB is too far - skip
2672 }
2673
2674 if (node.is_leaf) {
2675 // Check each primitive in this leaf for ray intersection
2676 for (uint i = 0; i < node.primitive_count; i++) {
2677 uint primitive_id = primitive_indices[node.primitive_start + i];
2678
2679 // Get this primitive's AABB
2680 if (!context->doesPrimitiveExist(primitive_id)) {
2681 continue; // Skip invalid primitive
2682 }
2683 vec3 prim_min, prim_max;
2684 context->getPrimitiveBoundingBox(primitive_id, prim_min, prim_max);
2685
2686 // Test ray against primitive AABB
2687 float prim_t_min, prim_t_max;
2688 if (rayAABBIntersect(origin, direction, prim_min, prim_max, prim_t_min, prim_t_max)) {
2689 // Check distance constraints (both min and max)
2690 bool within_min_distance = prim_t_min >= min_distance;
2691 bool within_max_distance = (max_distance <= 0.0f) || (prim_t_min <= max_distance);
2692
2693 if (within_min_distance && within_max_distance) {
2694 intersection_count++;
2695 }
2696 }
2697 }
2698 } else {
2699 // Add child nodes to stack for further traversal
2700 if (node.left_child != 0xFFFFFFFF) {
2701 node_stack.push_back(node.left_child);
2702 }
2703 if (node.right_child != 0xFFFFFFFF) {
2704 node_stack.push_back(node.right_child);
2705 }
2706 }
2707 }
2708
2709 return intersection_count;
2710}
2711
2712bool CollisionDetection::findNearestRayIntersection(const vec3 &origin, const vec3 &direction, const std::set<uint> &candidate_UUIDs, float &nearest_distance, float max_distance) {
2713
2714 nearest_distance = std::numeric_limits<float>::max();
2715 bool found_intersection = false;
2716
2717 // Check if we need to traverse both static and dynamic BVHs
2718 bool check_static_bvh = hierarchical_bvh_enabled && static_bvh_valid && !static_bvh_nodes.empty();
2719 bool check_dynamic_bvh = !bvh_nodes.empty();
2720
2721 if (!check_static_bvh && !check_dynamic_bvh) {
2722 return false;
2723 }
2724
2725
2726 // Ensure the BVH is current before traversal
2727 const_cast<CollisionDetection *>(this)->ensureBVHCurrent();
2728
2729 // Lambda function to traverse a BVH and find ray intersections
2730 auto traverseBVH = [&](const std::vector<BVHNode> &nodes, const std::vector<uint> &primitives, const char *bvh_name) {
2731 if (nodes.empty())
2732 return;
2733
2734 // Stack-based traversal to avoid recursion
2735 std::vector<uint> node_stack;
2736 node_stack.push_back(0); // Start with root node
2737
2738 while (!node_stack.empty()) {
2739 uint node_idx = node_stack.back();
2740 node_stack.pop_back();
2741
2742 if (node_idx >= nodes.size()) {
2743 continue;
2744 }
2745
2746 const BVHNode &node = nodes[node_idx];
2747
2748 // Test if ray intersects node AABB
2749 float t_min, t_max;
2750 if (!rayAABBIntersect(origin, direction, node.aabb_min, node.aabb_max, t_min, t_max)) {
2751 continue;
2752 }
2753
2754 // Check if intersection is within distance range
2755 if (max_distance > 0.0f && t_min > max_distance) {
2756 continue; // Entire AABB is too far - skip
2757 }
2758
2759 // If we've already found a closer intersection than this AABB, skip it
2760 if (t_min > nearest_distance) {
2761 continue;
2762 }
2763
2764 if (node.is_leaf) {
2765 // Check each primitive in this leaf for ray intersection
2766 for (uint i = 0; i < node.primitive_count; i++) {
2767 uint primitive_id = primitives[node.primitive_start + i];
2768
2769
2770 // Skip if this primitive is not in the candidate set (unless candidate set is empty)
2771 if (!candidate_UUIDs.empty() && candidate_UUIDs.find(primitive_id) == candidate_UUIDs.end()) {
2772 continue;
2773 }
2774
2775
2776 // Get this primitive's AABB
2777 if (!context->doesPrimitiveExist(primitive_id)) {
2778 continue; // Skip invalid primitive
2779 }
2780
2781 vec3 prim_min, prim_max;
2782 context->getPrimitiveBoundingBox(primitive_id, prim_min, prim_max);
2783
2784 // Test ray against primitive AABB
2785 float prim_t_min, prim_t_max;
2786 if (rayAABBIntersect(origin, direction, prim_min, prim_max, prim_t_min, prim_t_max)) {
2787 // Check distance constraints
2788 bool within_max_distance = (max_distance <= 0.0f) || (prim_t_min <= max_distance);
2789
2790 if (within_max_distance && prim_t_min > 0.0f && prim_t_min < nearest_distance) {
2791 // For now, we use AABB intersection distance as an approximation
2792 // A more accurate implementation would perform exact ray-primitive intersection
2793 nearest_distance = prim_t_min;
2794 found_intersection = true;
2795 }
2796 }
2797 }
2798 } else {
2799 // Add child nodes to stack for further traversal
2800 if (node.left_child != 0xFFFFFFFF) {
2801 node_stack.push_back(node.left_child);
2802 }
2803 if (node.right_child != 0xFFFFFFFF) {
2804 node_stack.push_back(node.right_child);
2805 }
2806 }
2807 } // End of while loop
2808 }; // End of lambda
2809
2810 // First, traverse the static BVH if hierarchical BVH is enabled
2811 if (check_static_bvh) {
2812 traverseBVH(static_bvh_nodes, static_bvh_primitives, "static");
2813 }
2814
2815 // Then, traverse the dynamic BVH
2816 if (check_dynamic_bvh) {
2817 traverseBVH(bvh_nodes, primitive_indices, "dynamic");
2818 }
2819
2820 return found_intersection;
2821}
2822
2823bool CollisionDetection::findNearestPrimitiveDistance(const vec3 &origin, const vec3 &direction, const std::vector<uint> &candidate_UUIDs, float &distance, vec3 &obstacle_direction) {
2824
2826 warnings.setEnabled(printmessages);
2827
2828 if (candidate_UUIDs.empty()) {
2829 warnings.addWarning("no_candidate_uuids", "No candidate UUIDs provided");
2830 warnings.report(std::cerr);
2831 return false;
2832 }
2833
2834 // Validate that direction is normalized
2835 float dir_magnitude = direction.magnitude();
2836 if (std::abs(dir_magnitude - 1.0f) > 1e-6f) {
2837 warnings.addWarning("direction_not_normalized", "Direction vector is not normalized (magnitude = " + std::to_string(dir_magnitude) + ")");
2838 warnings.report(std::cerr);
2839 return false;
2840 }
2841
2842
2843 // Filter out invalid UUIDs
2844 std::vector<uint> valid_candidates;
2845 for (uint uuid: candidate_UUIDs) {
2846 if (context->doesPrimitiveExist(uuid)) {
2847 valid_candidates.push_back(uuid);
2848 } else {
2849 warnings.addWarning("invalid_candidate_uuid", "Skipping invalid UUID " + std::to_string(uuid));
2850 }
2851 }
2852
2853
2854 if (valid_candidates.empty()) {
2855 warnings.addWarning("no_valid_candidates", "No valid candidate UUIDs after filtering");
2856 warnings.report(std::cerr);
2857 return false;
2858 }
2859
2860 float nearest_distance_found = std::numeric_limits<float>::max();
2861 vec3 nearest_obstacle_direction;
2862 bool found_forward_surface = false;
2863
2864 // Check each candidate primitive to find the nearest "forward-facing" surface
2865 for (uint primitive_id: valid_candidates) {
2866 // Get primitive normal and a point on the surface using Context methods
2867 vec3 surface_normal = context->getPrimitiveNormal(primitive_id);
2868 std::vector<vec3> vertices = context->getPrimitiveVertices(primitive_id);
2869
2870 if (vertices.empty()) {
2871 continue; // Skip if no vertices
2872 }
2873
2874 // Use first vertex as a point on the plane
2875 vec3 point_on_plane = vertices[0];
2876
2877 // Calculate distance from origin to the plane
2878 vec3 to_origin = origin - point_on_plane;
2879 float distance_to_plane = to_origin * surface_normal;
2880
2881 // Distance is the absolute value
2882 float surface_distance = std::abs(distance_to_plane);
2883
2884 // The direction from origin to closest point on surface
2885 vec3 surface_direction;
2886 if (distance_to_plane > 0) {
2887 // Origin is on the positive side of the normal - direction to surface is -normal
2888 surface_direction = -surface_normal;
2889 } else {
2890 // Origin is on the negative side of the normal - direction to surface is +normal
2891 surface_direction = surface_normal;
2892 }
2893
2894 // Check if this surface is "in front" using dot product
2895 float dot_product = surface_direction * direction;
2896
2897 if (dot_product > 0.0f) { // Surface is in front of origin
2898 if (surface_distance < nearest_distance_found) {
2899 nearest_distance_found = surface_distance;
2900 nearest_obstacle_direction = surface_direction;
2901 found_forward_surface = true;
2902 }
2903 }
2904 }
2905
2906 if (found_forward_surface) {
2907 distance = nearest_distance_found;
2908 obstacle_direction = nearest_obstacle_direction;
2909 warnings.report(std::cerr);
2910 return true;
2911 }
2912
2913 warnings.report(std::cerr);
2914 return false;
2915}
2916
2917bool CollisionDetection::findNearestSolidObstacleInCone(const vec3 &apex, const vec3 &axis, float half_angle, float height, const std::vector<uint> &candidate_UUIDs, float &distance, vec3 &obstacle_direction, int num_rays) {
2918
2920 warnings.setEnabled(printmessages);
2921
2922 // OPTIMIZATION: Use per-tree BVH if enabled for better scaling
2923 std::vector<uint> effective_candidates;
2924 if (tree_based_bvh_enabled) {
2925 // Get tree-relevant geometry - use proportional distance for spatial filtering
2926 float spatial_filter_distance = height * 1.25f; // 25% buffer beyond cone height
2927 effective_candidates = getRelevantGeometryForTree(apex, candidate_UUIDs, spatial_filter_distance);
2928
2929 } else {
2930 effective_candidates = candidate_UUIDs;
2931 }
2932
2933 if (effective_candidates.empty()) {
2934 return false; // No obstacles within detection range - normal for outer branches
2935 }
2936
2937 // Validate input parameters
2938 if (half_angle <= 0.0f || half_angle > M_PI / 2.0f) {
2939 warnings.addWarning("invalid_half_angle", "Invalid half_angle " + std::to_string(half_angle));
2940 warnings.report(std::cerr);
2941 return false;
2942 }
2943
2944 if (height <= 0.0f) {
2945 warnings.addWarning("invalid_height", "Invalid height " + std::to_string(height));
2946 warnings.report(std::cerr);
2947 return false;
2948 }
2949
2950 // Ensure BVH is current
2951 ensureBVHCurrent();
2952
2953 // Check if BVH is empty
2954 if (bvh_nodes.empty()) {
2955 return false; // No geometry to collide with
2956 }
2957
2958 // Convert effective candidate UUIDs to set for efficient lookup
2959 std::set<uint> candidate_set(effective_candidates.begin(), effective_candidates.end());
2960
2961 // Generate ray directions within the cone
2962 std::vector<vec3> ray_directions = sampleDirectionsInCone(apex, axis, half_angle, num_rays);
2963
2964 float nearest_distance = std::numeric_limits<float>::max();
2965 vec3 nearest_direction;
2966 bool found_obstacle = false;
2967
2968 // Use modern batch ray-casting for better performance
2969 std::vector<RayQuery> ray_queries;
2970 ray_queries.reserve(ray_directions.size());
2971
2972 for (const vec3 &ray_dir: ray_directions) {
2973 ray_queries.emplace_back(apex, ray_dir, height, candidate_UUIDs);
2974 }
2975
2976 // Cast all rays in batch - automatically selects CPU/GPU based on count
2977 RayTracingStats ray_stats;
2978 std::vector<HitResult> hit_results = castRays(ray_queries, &ray_stats);
2979
2980 // Find the nearest obstacle from all ray results
2981 for (size_t i = 0; i < hit_results.size(); ++i) {
2982 const HitResult &result = hit_results[i];
2983
2984 if (result.hit && result.distance < nearest_distance) {
2985 nearest_distance = result.distance;
2986 nearest_direction = ray_directions[i];
2987 found_obstacle = true;
2988 }
2989 }
2990
2991 if (found_obstacle) {
2992 distance = nearest_distance;
2993 obstacle_direction = nearest_direction;
2994 warnings.report(std::cerr);
2995 return true;
2996 }
2997
2998 warnings.report(std::cerr);
2999 return false;
3000}
3001
3002bool CollisionDetection::findNearestSolidObstacleInCone(const vec3 &apex, const vec3 &axis, float half_angle, float height, const std::vector<uint> &candidate_UUIDs, const std::vector<uint> &plant_primitives, float &distance,
3003 vec3 &obstacle_direction, int num_rays) {
3004
3006 warnings.setEnabled(printmessages);
3007
3008 // OPTIMIZATION: Use per-tree BVH with plant primitive identification for better scaling
3009 std::vector<uint> effective_candidates;
3010 if (tree_based_bvh_enabled) {
3011 // Get tree-relevant geometry using plant primitives to identify the querying tree
3012 effective_candidates = getRelevantGeometryForTree(apex, plant_primitives, height);
3013
3014 if (printmessages && !effective_candidates.empty()) {
3015 std::cout << "Per-tree findNearestSolidObstacleInCone: Using " << effective_candidates.size() << " relevant targets instead of " << candidate_UUIDs.size() << " total targets" << std::endl;
3016 }
3017 } else {
3018 effective_candidates = candidate_UUIDs;
3019 }
3020
3021 if (effective_candidates.empty()) {
3022 return false; // No obstacles within detection range - normal for outer branches
3023 }
3024
3025 // Validate input parameters
3026 if (half_angle <= 0.0f || half_angle > M_PI / 2.0f) {
3027 warnings.addWarning("invalid_half_angle", "Invalid half_angle " + std::to_string(half_angle));
3028 warnings.report(std::cerr);
3029 return false;
3030 }
3031
3032 if (height <= 0.0f) {
3033 warnings.addWarning("invalid_height", "Invalid height " + std::to_string(height));
3034 warnings.report(std::cerr);
3035 return false;
3036 }
3037
3038 // Ensure BVH is current
3039 ensureBVHCurrent();
3040
3041 // Check if BVH is empty
3042 if (bvh_nodes.empty()) {
3043 return false; // No geometry to collide with
3044 }
3045
3046 // Convert effective candidate UUIDs to set for efficient lookup
3047 std::set<uint> candidate_set(effective_candidates.begin(), effective_candidates.end());
3048
3049 // Generate ray directions within the cone
3050 std::vector<vec3> ray_directions = sampleDirectionsInCone(apex, axis, half_angle, num_rays);
3051
3052 float nearest_distance = std::numeric_limits<float>::max();
3053 vec3 nearest_direction;
3054 bool found_obstacle = false;
3055
3056 // Use modern batch ray-casting for better performance
3057 std::vector<RayQuery> ray_queries;
3058 ray_queries.reserve(ray_directions.size());
3059
3060 for (const vec3 &ray_dir: ray_directions) {
3061 ray_queries.emplace_back(apex, ray_dir, height, effective_candidates);
3062 }
3063
3064 // Cast all rays in batch - automatically selects CPU/GPU based on count
3065 RayTracingStats ray_stats;
3066 std::vector<HitResult> hit_results = castRays(ray_queries, &ray_stats);
3067
3068 // Find the nearest obstacle from all ray results
3069 for (size_t i = 0; i < hit_results.size(); ++i) {
3070 const HitResult &result = hit_results[i];
3071
3072 if (result.hit && result.distance < nearest_distance) {
3073 nearest_distance = result.distance;
3074 nearest_direction = ray_directions[i];
3075 found_obstacle = true;
3076 }
3077 }
3078
3079 if (found_obstacle) {
3080 distance = nearest_distance;
3081 obstacle_direction = nearest_direction;
3082 warnings.report(std::cerr);
3083 return true;
3084 }
3085
3086 warnings.report(std::cerr);
3087 return false;
3088}
3089
3090
3091std::vector<helios::vec3> CollisionDetection::sampleDirectionsInCone(const vec3 &apex, const vec3 &central_axis, float half_angle, int num_samples) {
3092
3093 std::vector<vec3> directions;
3094 directions.reserve(num_samples);
3095
3096 if (num_samples <= 0 || half_angle <= 0.0f) {
3097 return directions;
3098 }
3099
3100 // Normalize the central axis
3101 vec3 axis = central_axis;
3102 axis.normalize();
3103
3104 // Create an orthonormal basis with the central axis as the primary axis
3105 vec3 u, v;
3106 if (std::abs(axis.z) < 0.9f) {
3107 u = cross(axis, make_vec3(0, 0, 1));
3108 } else {
3109 u = cross(axis, make_vec3(1, 0, 0));
3110 }
3111 u.normalize();
3112 v = cross(axis, u);
3113 v.normalize();
3114
3115 // Generate uniform samples within the cone using rejection sampling on hemisphere
3116 std::random_device rd;
3117 std::mt19937 gen(rd());
3118 std::uniform_real_distribution<float> uniform_dist(0.0f, 1.0f);
3119
3120 int samples_generated = 0;
3121 int max_attempts = num_samples * 10; // Limit attempts to prevent infinite loops
3122 int attempts = 0;
3123
3124 while (samples_generated < num_samples && attempts < max_attempts) {
3125 attempts++;
3126
3127 // Generate uniform sample on unit hemisphere using spherical coordinates
3128 float u1 = uniform_dist(gen);
3129 float u2 = uniform_dist(gen);
3130
3131 // Use stratified sampling for better distribution
3132 if (samples_generated > 0) {
3133 float stratum_u1 = (float) samples_generated / (float) num_samples;
3134 float stratum_u2 = uniform_dist(gen);
3135 u1 = (stratum_u1 + u1 / (float) num_samples);
3136 if (u1 > 1.0f)
3137 u1 -= 1.0f;
3138 }
3139
3140 // Convert to spherical coordinates
3141 // For uniform sampling within cone, we need:
3142 // cos(theta) uniformly distributed between cos(half_angle) and 1
3143 float cos_half_angle = cosf(half_angle);
3144 float cos_theta = cos_half_angle + u1 * (1.0f - cos_half_angle);
3145 float sin_theta = sqrtf(1.0f - cos_theta * cos_theta);
3146 float phi = 2.0f * M_PI * u2;
3147
3148 // Convert to Cartesian coordinates in local coordinate system
3149 float x = sin_theta * cosf(phi);
3150 float y = sin_theta * sinf(phi);
3151 float z = cos_theta;
3152
3153 // Transform from local coordinates to world coordinates
3154 vec3 local_direction = make_vec3(x, y, z);
3155 vec3 world_direction = u * local_direction.x + v * local_direction.y + axis * local_direction.z;
3156 world_direction.normalize();
3157
3158 // Verify the direction is within the cone (numerical precision check)
3159 float dot_product = world_direction * axis;
3160 if (dot_product >= cos_half_angle - 1e-6f) {
3161 directions.push_back(world_direction);
3162 samples_generated++;
3163 }
3164 }
3165
3166 // If we couldn't generate enough samples, fill with the central axis
3167 while (directions.size() < (size_t) num_samples) {
3168 directions.push_back(axis);
3169 }
3170
3171 return directions;
3172}
3173
3174
3175// -------- SPATIAL GRID OPTIMIZATION --------
3176
3177std::vector<uint> CollisionDetection::getCandidatesUsingSpatialGrid(const Cone &cone, const vec3 &apex, const vec3 &central_axis, float half_angle, float height) {
3178 // Get cone AABB for grid cell determination
3179 vec3 cone_base = apex + central_axis * height;
3180 float cone_base_radius = height * tan(half_angle);
3181
3182 vec3 cone_aabb_min = vec3(std::min(apex.x, cone_base.x - cone_base_radius), std::min(apex.y, cone_base.y - cone_base_radius), std::min(apex.z, cone_base.z - cone_base_radius));
3183 vec3 cone_aabb_max = vec3(std::max(apex.x, cone_base.x + cone_base_radius), std::max(apex.y, cone_base.y + cone_base_radius), std::max(apex.z, cone_base.z + cone_base_radius));
3184
3185 // Use BVH primitive indices approach for better performance
3186 std::vector<uint> all_primitives;
3187 if (!primitive_indices.empty()) {
3188 all_primitives = primitive_indices; // Use BVH primitive indices
3189 } else {
3190 // Fallback: get all primitives from context (slower)
3191 all_primitives = context->getAllUUIDs();
3192 }
3193
3194 // Use existing filterPrimitivesParallel method with cone object
3195 return filterPrimitivesParallel(cone, all_primitives);
3196}
3197
3198// -------- HYBRID BVH + RASTERIZATION METHODS --------
3199
3200std::vector<uint> CollisionDetection::getCandidatePrimitivesInCone(const vec3 &apex, const vec3 &central_axis, float half_angle, float height) {
3201 std::vector<uint> candidates;
3202
3203 // Create cone object for filtering
3204 Cone cone{apex, central_axis, half_angle, height};
3205
3206 // OPTIMIZATION: Use spatial grid to avoid O(N) scaling with tree count
3207 candidates = getCandidatesUsingSpatialGrid(cone, apex, central_axis, half_angle, height);
3208
3209 return candidates;
3210}
3211
3212
3213// -------- GAP DETECTION IMPLEMENTATION --------
3214
3215std::vector<CollisionDetection::Gap> CollisionDetection::detectGapsInCone(const vec3 &apex, const vec3 &central_axis, float half_angle, float height, int num_samples) {
3216
3217 std::vector<Gap> gaps;
3218
3219 // Generate dense ray samples within the cone
3220 std::vector<vec3> sample_directions = sampleDirectionsInCone(apex, central_axis, half_angle, num_samples);
3221
3222 if (sample_directions.empty()) {
3223 return gaps;
3224 }
3225
3226 // Build ray sample data using modern ray-tracing API
3227 std::vector<RaySample> ray_samples;
3228 ray_samples.reserve(sample_directions.size());
3229
3230 float max_distance = (height > 0.0f) ? height : -1.0f;
3231
3232 // Use batch ray casting for better performance
3233 std::vector<RayQuery> ray_queries;
3234 ray_queries.reserve(sample_directions.size());
3235
3236 for (const vec3 &direction: sample_directions) {
3237 ray_queries.emplace_back(apex, direction, max_distance);
3238 }
3239
3240 // Cast all rays in batch - automatically selects CPU/GPU based on count
3241 RayTracingStats ray_stats;
3242 std::vector<HitResult> hit_results = castRays(ray_queries, &ray_stats);
3243
3244 // Process results to build ray samples
3245 for (size_t i = 0; i < hit_results.size(); ++i) {
3246 RaySample sample;
3247 sample.direction = sample_directions[i];
3248
3249 if (hit_results[i].hit) {
3250 sample.distance = hit_results[i].distance;
3251 sample.is_free = false;
3252 } else {
3253 sample.distance = (max_distance > 0.0f) ? max_distance : 1000.0f;
3254 sample.is_free = true;
3255 }
3256
3257 ray_samples.push_back(sample);
3258 }
3259
3260
3261 // Use a more sophisticated gap detection approach based on contiguous free regions
3262 // First, sort samples by angular position relative to central axis to identify contiguous regions
3263 std::vector<std::pair<float, size_t>> angular_positions;
3264 for (size_t i = 0; i < ray_samples.size(); ++i) {
3265 if (ray_samples[i].is_free) {
3266 // Calculate angular position in cone-relative coordinates
3267 float dot_product = ray_samples[i].direction * central_axis;
3268 dot_product = std::max(-1.0f, std::min(1.0f, dot_product));
3269 float angular_from_center = acosf(dot_product);
3270 angular_positions.push_back({angular_from_center, i});
3271 }
3272 }
3273
3274 if (angular_positions.empty()) {
3275 return gaps; // No free samples
3276 }
3277
3278 // Sort by angular position
3279 std::sort(angular_positions.begin(), angular_positions.end());
3280
3281 // Find contiguous free regions (gaps) with minimum size threshold
3282 std::vector<bool> processed(ray_samples.size(), false);
3283 float min_gap_angular_size = half_angle * 0.05f; // 5% of cone angle minimum gap size
3284
3285 for (size_t start = 0; start < angular_positions.size(); ++start) {
3286 size_t start_idx = angular_positions[start].second;
3287 if (processed[start_idx])
3288 continue;
3289
3290 Gap new_gap;
3291 new_gap.sample_indices.push_back(start_idx);
3292 processed[start_idx] = true;
3293
3294 // Extend gap by finding nearby free samples using k-nearest neighbor approach
3295 std::vector<float> distances_to_start;
3296 for (size_t j = 0; j < ray_samples.size(); ++j) {
3297 if (j != start_idx && ray_samples[j].is_free && !processed[j]) {
3298 float dot_product = ray_samples[start_idx].direction * ray_samples[j].direction;
3299 dot_product = std::max(-1.0f, std::min(1.0f, dot_product));
3300 float angular_distance = acosf(dot_product);
3301 distances_to_start.push_back(angular_distance);
3302 } else {
3303 distances_to_start.push_back(999.0f); // Large value for excluded samples
3304 }
3305 }
3306
3307 // Add nearby samples to gap using adaptive threshold
3308 float sample_density = 2.0f * half_angle / sqrtf((float) num_samples);
3309 float adaptive_threshold = sample_density * 3.0f; // 3x sample spacing for connection
3310
3311 for (size_t j = 0; j < ray_samples.size(); ++j) {
3312 if (j != start_idx && ray_samples[j].is_free && !processed[j] && distances_to_start[j] < adaptive_threshold) {
3313 new_gap.sample_indices.push_back(j);
3314 processed[j] = true;
3315 }
3316 }
3317
3318 // Only keep gaps that meet minimum size requirements
3319 if (new_gap.sample_indices.size() >= 5) { // Require at least 5 samples for a valid gap
3320 gaps.push_back(new_gap);
3321 }
3322 }
3323
3324 // Calculate gap properties
3325 for (Gap &gap: gaps) {
3326 // Calculate gap center direction (average of constituent directions)
3327 vec3 center(0, 0, 0);
3328 for (int idx: gap.sample_indices) {
3329 center = center + ray_samples[idx].direction;
3330 }
3331 center = center / (float) gap.sample_indices.size();
3332 center.normalize();
3333 gap.center_direction = center;
3334
3335 // Calculate angular size
3336 std::vector<RaySample> gap_samples;
3337 for (int idx: gap.sample_indices) {
3338 gap_samples.push_back(ray_samples[idx]);
3339 }
3340 gap.angular_size = calculateGapAngularSize(gap_samples, central_axis);
3341
3342 // Calculate angular distance from central axis
3343 float dot_product = gap.center_direction * central_axis;
3344 dot_product = std::max(-1.0f, std::min(1.0f, dot_product));
3345 gap.angular_distance = acosf(dot_product);
3346 }
3347
3348 // OPTIMIZATION: Spatial filtering - remove gaps that are too far from central axis
3349 // This reduces the number of gaps passed to the expensive scoring function
3350 if (gaps.size() > 10) {
3351 float max_angular_distance = half_angle * 0.8f; // Only consider gaps within 80% of cone angle
3352
3353 auto it = std::remove_if(gaps.begin(), gaps.end(), [max_angular_distance](const Gap &gap) { return gap.angular_distance > max_angular_distance; });
3354
3355 gaps.erase(it, gaps.end());
3356
3357 // If filtering removed too many gaps, keep at least the closest ones
3358 if (gaps.size() < 3 && gaps.size() > 0) {
3359 // Sort by angular distance and keep the closest ones
3360 std::partial_sort(gaps.begin(), gaps.begin() + std::min(size_t(3), gaps.size()), gaps.end(), [](const Gap &a, const Gap &b) { return a.angular_distance < b.angular_distance; });
3361 }
3362 }
3363
3364 return gaps;
3365}
3366
3367
3368float CollisionDetection::calculateGapAngularSize(const std::vector<RaySample> &gap_samples, const vec3 &central_axis) {
3369
3370 if (gap_samples.empty()) {
3371 return 0.0f;
3372 }
3373
3374 // Find the angular extent of the gap by finding min/max angles
3375 float min_angle = M_PI;
3376 float max_angle = 0.0f;
3377
3378 for (const RaySample &sample: gap_samples) {
3379 float dot_product = sample.direction * central_axis;
3380 dot_product = std::max(-1.0f, std::min(1.0f, dot_product));
3381 float angle = acosf(dot_product);
3382
3383 min_angle = std::min(min_angle, angle);
3384 max_angle = std::max(max_angle, angle);
3385 }
3386
3387 // Simple approximation: angular size as solid angle
3388 float angular_width = max_angle - min_angle;
3389
3390 // Convert to approximate solid angle (steradians)
3391 // This is a rough approximation: solid_angle ≈ π * (angular_width)^2
3392 float solid_angle = M_PI * angular_width * angular_width;
3393
3394 return solid_angle;
3395}
3396
3397void CollisionDetection::scoreGapsByFishEyeMetric(std::vector<Gap> &gaps, const vec3 &central_axis) {
3398
3399 // Early exit for small gap counts - full sort is fine
3400 if (gaps.size() <= 10) {
3401 for (Gap &gap: gaps) {
3402 // Fish-eye metric: prefer larger gaps closer to central axis
3403 // Gap size component (logarithmic scaling for larger gaps)
3404 float size_score = log(1.0f + gap.angular_size * 100.0f); // Scale up angular size
3405 // Distance penalty (exponential penalty for gaps far from center)
3406 float distance_penalty = exp(gap.angular_distance * 2.0f);
3407 // Combined score (higher is better)
3408 gap.score = size_score / distance_penalty;
3409 }
3410 // Sort gaps by score (highest first)
3411 std::sort(gaps.begin(), gaps.end(), [](const Gap &a, const Gap &b) { return a.score > b.score; });
3412 return;
3413 }
3414
3415 // OPTIMIZATION: For large gap counts, use partial sorting
3416 // We only need the top 3-5 gaps for collision avoidance
3417 const size_t max_gaps_needed = std::min(size_t(5), gaps.size());
3418
3419 // Calculate scores for all gaps
3420 for (Gap &gap: gaps) {
3421 // Fish-eye metric: prefer larger gaps closer to central axis
3422
3423 // Gap size component (logarithmic scaling for larger gaps)
3424 float size_score = log(1.0f + gap.angular_size * 100.0f); // Scale up angular size
3425
3426 // Distance penalty (exponential penalty for gaps far from center)
3427 float distance_penalty = exp(gap.angular_distance * 2.0f);
3428
3429 // Combined score (higher is better)
3430 gap.score = size_score / distance_penalty;
3431 }
3432
3433 // Use partial_sort to get only the top N gaps - O(n log k) instead of O(n log n)
3434 std::partial_sort(gaps.begin(), gaps.begin() + max_gaps_needed, gaps.end(), [](const Gap &a, const Gap &b) { return a.score > b.score; });
3435
3436 // Resize to keep only the top gaps to avoid processing unnecessary gaps later
3437 gaps.resize(max_gaps_needed);
3438}
3439
3440helios::vec3 CollisionDetection::findOptimalGapDirection(const std::vector<Gap> &gaps, const vec3 &central_axis) {
3441
3442 if (gaps.empty()) {
3443 // No gaps found, return central axis
3444 vec3 result = central_axis;
3445 result.normalize();
3446 return result;
3447 }
3448
3449 // Return direction toward the highest-scoring gap
3450 const Gap &best_gap = gaps[0];
3451 return best_gap.center_direction;
3452}
3453
3454// -------- SPATIAL OPTIMIZATION METHODS --------
3455
3456std::vector<std::pair<uint, uint>> CollisionDetection::findCollisionsWithinDistance(const std::vector<uint> &query_UUIDs, const std::vector<uint> &target_UUIDs, float max_distance) {
3457
3458 std::vector<std::pair<uint, uint>> collision_pairs;
3459
3460
3461 // Update primitive centroids cache for target geometry
3462 for (uint target_id: target_UUIDs) {
3463 if (primitive_centroids_cache.find(target_id) == primitive_centroids_cache.end()) {
3464 // Calculate and cache centroid for this primitive
3465 std::vector<vec3> vertices = context->getPrimitiveVertices(target_id);
3466 if (!vertices.empty()) {
3467 vec3 centroid = make_vec3(0, 0, 0);
3468 for (const vec3 &vertex: vertices) {
3469 centroid = centroid + vertex;
3470 }
3471 centroid = centroid / float(vertices.size());
3472 primitive_centroids_cache[target_id] = centroid;
3473 }
3474 }
3475 }
3476
3477 // For each query primitive, find nearby targets within distance
3478 for (uint query_id: query_UUIDs) {
3479 // Get query centroid
3480 std::vector<vec3> query_vertices = context->getPrimitiveVertices(query_id);
3481 if (query_vertices.empty())
3482 continue;
3483
3484 vec3 query_centroid = make_vec3(0, 0, 0);
3485 for (const vec3 &vertex: query_vertices) {
3486 query_centroid = query_centroid + vertex;
3487 }
3488 query_centroid = query_centroid / float(query_vertices.size());
3489
3490 // Check distance to each target
3491 for (uint target_id: target_UUIDs) {
3492 if (query_id == target_id)
3493 continue; // Skip self-collision
3494
3495 auto target_centroid_it = primitive_centroids_cache.find(target_id);
3496 if (target_centroid_it != primitive_centroids_cache.end()) {
3497 vec3 target_centroid = target_centroid_it->second;
3498 float distance = (query_centroid - target_centroid).magnitude();
3499
3500 if (distance <= max_distance) {
3501 // Within distance threshold, check for actual collision
3502 std::vector<uint> single_query = {query_id};
3503 std::vector<uint> single_target = {target_id};
3504 std::vector<uint> empty_objects;
3505
3506 std::vector<uint> collisions = findCollisions(single_query, empty_objects, single_target, empty_objects);
3507 if (!collisions.empty()) {
3508 collision_pairs.push_back(std::make_pair(query_id, target_id));
3509 }
3510 }
3511 }
3512 }
3513 }
3514
3515 return collision_pairs;
3516}
3517
3519 if (distance <= 0.0f) {
3520 helios_runtime_error("ERROR (CollisionDetection::setMaxCollisionDistance): Distance must be positive");
3521 }
3522
3523 max_collision_distance = distance;
3524}
3525
3527 return max_collision_distance;
3528}
3529
3530std::vector<uint> CollisionDetection::filterGeometryByDistance(const helios::vec3 &query_center, float max_radius, const std::vector<uint> &candidate_UUIDs) {
3531
3532 std::vector<uint> filtered_UUIDs;
3533
3534 // Get list of candidates (either provided or all primitives)
3535 std::vector<uint> candidates;
3536 if (candidate_UUIDs.empty()) {
3537 candidates = context->getAllUUIDs();
3538 } else {
3539 candidates = candidate_UUIDs;
3540 }
3541
3542 // Filter candidates by distance
3543 for (uint candidate_id: candidates) {
3544 // Skip if primitive doesn't exist
3545 if (!context->doesPrimitiveExist(candidate_id)) {
3546 continue;
3547 }
3548
3549 // Get primitive centroid (calculate if not cached)
3550 vec3 centroid;
3551 auto cache_it = primitive_centroids_cache.find(candidate_id);
3552 if (cache_it != primitive_centroids_cache.end()) {
3553 centroid = cache_it->second;
3554 } else {
3555 // Calculate and cache centroid
3556 std::vector<vec3> vertices = context->getPrimitiveVertices(candidate_id);
3557 if (vertices.empty())
3558 continue;
3559
3560 centroid = make_vec3(0, 0, 0);
3561 for (const vec3 &vertex: vertices) {
3562 centroid = centroid + vertex;
3563 }
3564 centroid = centroid / float(vertices.size());
3565 primitive_centroids_cache[candidate_id] = centroid;
3566 }
3567
3568 // Check distance from query center
3569 float distance = (query_center - centroid).magnitude();
3570 if (distance <= max_radius) {
3571 filtered_UUIDs.push_back(candidate_id);
3572 }
3573 }
3574
3575 return filtered_UUIDs;
3576}
3577
3578// -------- VOXEL RAY PATH LENGTH CALCULATIONS --------
3579
3580void CollisionDetection::calculateVoxelRayPathLengths(const vec3 &grid_center, const vec3 &grid_size, const helios::int3 &grid_divisions, const std::vector<vec3> &ray_origins, const std::vector<vec3> &ray_directions) {
3581
3583 warnings.setEnabled(printmessages);
3584
3585 if (ray_origins.size() != ray_directions.size()) {
3586 helios_runtime_error("ERROR (CollisionDetection::calculateVoxelRayPathLengths): ray_origins and ray_directions vectors must have same size");
3587 }
3588
3589 if (ray_origins.empty()) {
3590 warnings.addWarning("no_rays_provided", "No rays provided");
3591 warnings.report(std::cerr);
3592 return;
3593 }
3594
3595 // Initialize voxel data structures for the given grid
3596 initializeVoxelData(grid_center, grid_size, grid_divisions);
3597
3598 // Ensure BVH and primitive cache are built before parallel section
3599 // This prevents thread-safety issues when multiple threads try to build them
3600 ensureBVHCurrent();
3601 ensurePrimitiveCacheCurrent();
3602
3603 // Choose GPU or CPU implementation based on acceleration setting
3604#ifdef HELIOS_CUDA_AVAILABLE
3606 // Try GPU first, but fall back to CPU if GPU fails (no hardware, out of memory, etc.)
3607 bool gpu_success = calculateVoxelRayPathLengths_GPU(ray_origins, ray_directions);
3608 if (!gpu_success) {
3609 // GPU failed - fall back to CPU and disable GPU for future calls
3610 if (printmessages) {
3611 warnings.addWarning("gpu_voxel_fallback", "GPU voxel calculation failed, falling back to CPU");
3612 }
3613 gpu_acceleration_enabled = false;
3614 calculateVoxelRayPathLengths_CPU(ray_origins, ray_directions);
3615 }
3616 } else {
3617 calculateVoxelRayPathLengths_CPU(ray_origins, ray_directions);
3618 }
3619#else
3620 calculateVoxelRayPathLengths_CPU(ray_origins, ray_directions);
3621#endif
3622
3623 warnings.report(std::cerr);
3624}
3625
3626void CollisionDetection::setVoxelTransmissionProbability(int P_denom, int P_trans, const helios::int3 &ijk) {
3627 if (!validateVoxelIndices(ijk)) {
3628 helios_runtime_error("ERROR (CollisionDetection::setVoxelTransmissionProbability): Invalid voxel indices");
3629 }
3630
3631 if (!voxel_data_initialized) {
3632 helios_runtime_error("ERROR (CollisionDetection::setVoxelTransmissionProbability): Voxel data not initialized. Call calculateVoxelRayPathLengths first.");
3633 }
3634
3635 if (use_flat_arrays) {
3636 size_t flat_idx = flatIndex(ijk);
3637 voxel_ray_counts_flat[flat_idx] = P_denom;
3638 voxel_transmitted_flat[flat_idx] = P_trans;
3639 } else {
3640 voxel_ray_counts[ijk.x][ijk.y][ijk.z] = P_denom;
3641 voxel_transmitted[ijk.x][ijk.y][ijk.z] = P_trans;
3642 }
3643}
3644
3645void CollisionDetection::getVoxelTransmissionProbability(const helios::int3 &ijk, int &P_denom, int &P_trans) const {
3646 if (!validateVoxelIndices(ijk)) {
3647 helios_runtime_error("ERROR (CollisionDetection::getVoxelTransmissionProbability): Invalid voxel indices");
3648 }
3649
3650 if (!voxel_data_initialized) {
3651 P_denom = 0;
3652 P_trans = 0;
3653 return;
3654 }
3655
3656 if (use_flat_arrays) {
3657 size_t flat_idx = flatIndex(ijk);
3658 P_denom = voxel_ray_counts_flat[flat_idx];
3659 P_trans = voxel_transmitted_flat[flat_idx];
3660 } else {
3661 P_denom = voxel_ray_counts[ijk.x][ijk.y][ijk.z];
3662 P_trans = voxel_transmitted[ijk.x][ijk.y][ijk.z];
3663 }
3664}
3665
3666void CollisionDetection::setVoxelRbar(float r_bar, const helios::int3 &ijk) {
3667 if (!validateVoxelIndices(ijk)) {
3668 helios_runtime_error("ERROR (CollisionDetection::setVoxelRbar): Invalid voxel indices");
3669 }
3670
3671 if (!voxel_data_initialized) {
3672 helios_runtime_error("ERROR (CollisionDetection::setVoxelRbar): Voxel data not initialized. Call calculateVoxelRayPathLengths first.");
3673 }
3674
3675 if (use_flat_arrays) {
3676 size_t flat_idx = flatIndex(ijk);
3677 // Store r_bar * ray_count so that getVoxelRbar returns the correct value when it divides
3678 int ray_count = voxel_ray_counts_flat[flat_idx];
3679 if (ray_count == 0) {
3680 ray_count = 1; // Set to 1 to avoid division by zero
3681 voxel_ray_counts_flat[flat_idx] = 1;
3682 }
3683 voxel_path_lengths_flat[flat_idx] = r_bar * static_cast<float>(ray_count);
3684 } else {
3685 // Store r_bar * ray_count so that getVoxelRbar returns the correct value when it divides
3686 int ray_count = voxel_ray_counts[ijk.x][ijk.y][ijk.z];
3687 if (ray_count == 0) {
3688 ray_count = 1; // Set to 1 to avoid division by zero
3689 voxel_ray_counts[ijk.x][ijk.y][ijk.z] = 1;
3690 }
3691 voxel_path_lengths[ijk.x][ijk.y][ijk.z] = r_bar * static_cast<float>(ray_count);
3692 }
3693}
3694
3696 if (!validateVoxelIndices(ijk)) {
3697 helios_runtime_error("ERROR (CollisionDetection::getVoxelRbar): Invalid voxel indices");
3698 }
3699
3700 if (!voxel_data_initialized) {
3701 return 0.0f;
3702 }
3703
3704 if (use_flat_arrays) {
3705 size_t flat_idx = flatIndex(ijk);
3706 int ray_count = voxel_ray_counts_flat[flat_idx];
3707 if (ray_count == 0) {
3708 return 0.0f;
3709 }
3710 // If this was set directly via setVoxelRbar, return it as-is
3711 // If this accumulated from ray calculations, compute the average
3712 return voxel_path_lengths_flat[flat_idx] / static_cast<float>(ray_count);
3713 } else {
3714 int ray_count = voxel_ray_counts[ijk.x][ijk.y][ijk.z];
3715 if (ray_count == 0) {
3716 return 0.0f;
3717 }
3718 // If this was set directly via setVoxelRbar, return it as-is
3719 // If this accumulated from ray calculations, compute the average
3720 return voxel_path_lengths[ijk.x][ijk.y][ijk.z] / static_cast<float>(ray_count);
3721 }
3722}
3723
3724void CollisionDetection::getVoxelRayHitCounts(const helios::int3 &ijk, int &hit_before, int &hit_after, int &hit_inside) const {
3725 if (!validateVoxelIndices(ijk)) {
3726 helios_runtime_error("ERROR (CollisionDetection::getVoxelRayHitCounts): Invalid voxel indices");
3727 }
3728
3729 if (!voxel_data_initialized) {
3730 hit_before = 0;
3731 hit_after = 0;
3732 hit_inside = 0;
3733 return;
3734 }
3735
3736 if (use_flat_arrays) {
3737 size_t flat_idx = flatIndex(ijk);
3738 hit_before = voxel_hit_before_flat[flat_idx];
3739 hit_after = voxel_hit_after_flat[flat_idx];
3740 hit_inside = voxel_hit_inside_flat[flat_idx];
3741 } else {
3742 hit_before = voxel_hit_before[ijk.x][ijk.y][ijk.z];
3743 hit_after = voxel_hit_after[ijk.x][ijk.y][ijk.z];
3744 hit_inside = voxel_hit_inside[ijk.x][ijk.y][ijk.z];
3745 }
3746}
3747
3748std::vector<float> CollisionDetection::getVoxelRayPathLengths(const helios::int3 &ijk) const {
3749 if (!validateVoxelIndices(ijk)) {
3750 helios_runtime_error("ERROR (CollisionDetection::getVoxelRayPathLengths): Invalid voxel indices");
3751 }
3752
3753 if (!voxel_data_initialized) {
3754 return std::vector<float>();
3755 }
3756
3757 if (use_flat_arrays) {
3758 // Use flat array structure with offsets
3759 size_t flat_idx = flatIndex(ijk);
3760
3761 // Bounds checking for flat array access
3762 if (flat_idx >= voxel_individual_path_offsets.size() || flat_idx >= voxel_individual_path_counts.size()) {
3763 return std::vector<float>();
3764 }
3765
3766 size_t offset = voxel_individual_path_offsets[flat_idx];
3767 size_t count = voxel_individual_path_counts[flat_idx];
3768
3769 // Additional bounds checking for the data array
3770 if (count == 0 || offset + count > voxel_individual_path_lengths_flat.size()) {
3771 return std::vector<float>();
3772 }
3773
3774 std::vector<float> result;
3775 result.reserve(count);
3776
3777 for (size_t i = 0; i < count; ++i) {
3778 result.push_back(voxel_individual_path_lengths_flat[offset + i]);
3779 }
3780
3781 return result;
3782 } else {
3783 return voxel_individual_path_lengths[ijk.x][ijk.y][ijk.z];
3784 }
3785}
3786
3788 voxel_ray_counts.clear();
3789 voxel_transmitted.clear();
3790 voxel_path_lengths.clear();
3791 voxel_hit_before.clear();
3792 voxel_hit_after.clear();
3793 voxel_hit_inside.clear();
3794 voxel_individual_path_lengths.clear();
3795 voxel_data_initialized = false;
3796
3797 if (printmessages) {
3798 std::cout << "Voxel data cleared." << std::endl;
3799 }
3800}
3801
3802// -------- VOXEL RAY PATH LENGTH HELPER METHODS --------
3803
3804void CollisionDetection::initializeVoxelData(const vec3 &grid_center, const vec3 &grid_size, const helios::int3 &grid_divisions) {
3805
3806 // Check if we need to reinitialize (grid parameters changed)
3807 bool need_reinit = !voxel_data_initialized || (grid_center - voxel_grid_center).magnitude() > 1e-6 || (grid_size - voxel_grid_size).magnitude() > 1e-6 || grid_divisions.x != voxel_grid_divisions.x || grid_divisions.y != voxel_grid_divisions.y ||
3808 grid_divisions.z != voxel_grid_divisions.z;
3809
3810 if (!need_reinit) {
3811 // Just clear existing data but keep structure
3812 if (use_flat_arrays) {
3813 // Clear flat arrays
3814 size_t total_voxels = static_cast<size_t>(grid_divisions.x) * grid_divisions.y * grid_divisions.z;
3815 std::fill(voxel_ray_counts_flat.begin(), voxel_ray_counts_flat.end(), 0);
3816 std::fill(voxel_transmitted_flat.begin(), voxel_transmitted_flat.end(), 0);
3817 std::fill(voxel_path_lengths_flat.begin(), voxel_path_lengths_flat.end(), 0.0f);
3818 std::fill(voxel_hit_before_flat.begin(), voxel_hit_before_flat.end(), 0);
3819 std::fill(voxel_hit_after_flat.begin(), voxel_hit_after_flat.end(), 0);
3820 std::fill(voxel_hit_inside_flat.begin(), voxel_hit_inside_flat.end(), 0);
3821
3822 // Clear individual path lengths
3823 voxel_individual_path_lengths_flat.clear();
3824 std::fill(voxel_individual_path_offsets.begin(), voxel_individual_path_offsets.end(), 0);
3825 std::fill(voxel_individual_path_counts.begin(), voxel_individual_path_counts.end(), 0);
3826 } else {
3827 // Clear nested vectors
3828 for (int i = 0; i < grid_divisions.x; i++) {
3829 for (int j = 0; j < grid_divisions.y; j++) {
3830 for (int k = 0; k < grid_divisions.z; k++) {
3831 voxel_ray_counts[i][j][k] = 0;
3832 voxel_transmitted[i][j][k] = 0;
3833 voxel_path_lengths[i][j][k] = 0.0f;
3834 voxel_hit_before[i][j][k] = 0;
3835 voxel_hit_after[i][j][k] = 0;
3836 voxel_hit_inside[i][j][k] = 0;
3837 voxel_individual_path_lengths[i][j][k].clear();
3838 }
3839 }
3840 }
3841 }
3842 return;
3843 }
3844
3845 // Store grid parameters
3846 voxel_grid_center = grid_center;
3847 voxel_grid_size = grid_size;
3848 voxel_grid_divisions = grid_divisions;
3849
3850 // Enable flat arrays for better performance
3851 use_flat_arrays = true;
3852
3853 if (use_flat_arrays) {
3854 // Initialize optimized flat arrays (Structure-of-Arrays)
3855 size_t total_voxels = static_cast<size_t>(grid_divisions.x) * grid_divisions.y * grid_divisions.z;
3856
3857 voxel_ray_counts_flat.assign(total_voxels, 0);
3858 voxel_transmitted_flat.assign(total_voxels, 0);
3859 voxel_path_lengths_flat.assign(total_voxels, 0.0f);
3860 voxel_hit_before_flat.assign(total_voxels, 0);
3861 voxel_hit_after_flat.assign(total_voxels, 0);
3862 voxel_hit_inside_flat.assign(total_voxels, 0);
3863
3864 // Individual path lengths with dynamic storage
3865 voxel_individual_path_lengths_flat.clear();
3866 voxel_individual_path_offsets.assign(total_voxels, 0);
3867 voxel_individual_path_counts.assign(total_voxels, 0);
3868
3869 // Reserve reasonable initial capacity for individual paths
3870 voxel_individual_path_lengths_flat.reserve(total_voxels * 10); // Average 10 paths per voxel
3871
3872 } else {
3873 // Fallback to nested vectors for compatibility
3874 voxel_ray_counts.resize(grid_divisions.x);
3875 voxel_transmitted.resize(grid_divisions.x);
3876 voxel_path_lengths.resize(grid_divisions.x);
3877 voxel_hit_before.resize(grid_divisions.x);
3878 voxel_hit_after.resize(grid_divisions.x);
3879 voxel_hit_inside.resize(grid_divisions.x);
3880 voxel_individual_path_lengths.resize(grid_divisions.x);
3881
3882 for (int i = 0; i < grid_divisions.x; i++) {
3883 voxel_ray_counts[i].resize(grid_divisions.y);
3884 voxel_transmitted[i].resize(grid_divisions.y);
3885 voxel_path_lengths[i].resize(grid_divisions.y);
3886 voxel_hit_before[i].resize(grid_divisions.y);
3887 voxel_hit_after[i].resize(grid_divisions.y);
3888 voxel_hit_inside[i].resize(grid_divisions.y);
3889 voxel_individual_path_lengths[i].resize(grid_divisions.y);
3890
3891 for (int j = 0; j < grid_divisions.y; j++) {
3892 voxel_ray_counts[i][j].resize(grid_divisions.z, 0);
3893 voxel_transmitted[i][j].resize(grid_divisions.z, 0);
3894 voxel_path_lengths[i][j].resize(grid_divisions.z, 0.0f);
3895 voxel_hit_before[i][j].resize(grid_divisions.z, 0);
3896 voxel_hit_after[i][j].resize(grid_divisions.z, 0);
3897 voxel_hit_inside[i][j].resize(grid_divisions.z, 0);
3898 voxel_individual_path_lengths[i][j].resize(grid_divisions.z);
3899 }
3900 }
3901 }
3902
3903 voxel_data_initialized = true;
3904}
3905
3906bool CollisionDetection::validateVoxelIndices(const helios::int3 &ijk) const {
3907 return (ijk.x >= 0 && ijk.x < voxel_grid_divisions.x && ijk.y >= 0 && ijk.y < voxel_grid_divisions.y && ijk.z >= 0 && ijk.z < voxel_grid_divisions.z);
3908}
3909
3910void CollisionDetection::calculateVoxelAABB(const helios::int3 &ijk, vec3 &voxel_min, vec3 &voxel_max) const {
3911 vec3 voxel_size = make_vec3(voxel_grid_size.x / static_cast<float>(voxel_grid_divisions.x), voxel_grid_size.y / static_cast<float>(voxel_grid_divisions.y), voxel_grid_size.z / static_cast<float>(voxel_grid_divisions.z));
3912
3913 vec3 grid_min = voxel_grid_center - 0.5f * voxel_grid_size;
3914
3915 voxel_min = grid_min + make_vec3(static_cast<float>(ijk.x) * voxel_size.x, static_cast<float>(ijk.y) * voxel_size.y, static_cast<float>(ijk.z) * voxel_size.z);
3916
3917 voxel_max = voxel_min + voxel_size;
3918}
3919
3920std::vector<std::pair<helios::int3, float>> CollisionDetection::traverseVoxelGrid(const vec3 &ray_origin, const vec3 &ray_direction) const {
3921 std::vector<std::pair<helios::int3, float>> traversed_voxels;
3922
3923 // Grid bounds
3924 vec3 grid_min = voxel_grid_center - 0.5f * voxel_grid_size;
3925 vec3 grid_max = voxel_grid_center + 0.5f * voxel_grid_size;
3926 vec3 voxel_size = make_vec3(voxel_grid_size.x / static_cast<float>(voxel_grid_divisions.x), voxel_grid_size.y / static_cast<float>(voxel_grid_divisions.y), voxel_grid_size.z / static_cast<float>(voxel_grid_divisions.z));
3927
3928 // Test if ray intersects grid at all
3929 float t_grid_min, t_grid_max;
3930 if (!rayAABBIntersect(ray_origin, ray_direction, grid_min, grid_max, t_grid_min, t_grid_max)) {
3931 return traversed_voxels; // Empty - ray doesn't hit grid
3932 }
3933
3934 // Ensure intersection is in forward direction
3935 if (t_grid_max <= 1e-6) {
3936 return traversed_voxels; // Grid is behind ray
3937 }
3938
3939 // Clamp t_grid_min to 0 if ray starts inside grid
3940 t_grid_min = std::max(0.0f, t_grid_min);
3941
3942 // Fast path for single voxel grids
3943 if (voxel_grid_divisions.x == 1 && voxel_grid_divisions.y == 1 && voxel_grid_divisions.z == 1) {
3944 float path_length = t_grid_max - t_grid_min;
3945 if (path_length > 1e-6f) {
3946 traversed_voxels.emplace_back(helios::make_int3(0, 0, 0), path_length);
3947 }
3948 return traversed_voxels;
3949 }
3950
3951 // Starting position in grid space
3952 vec3 start_pos = ray_origin + t_grid_min * ray_direction;
3953
3954 // Convert to voxel indices (clamped to grid bounds)
3955 helios::int3 current_voxel;
3956 current_voxel.x = static_cast<int>(std::floor((start_pos.x - grid_min.x) / voxel_size.x));
3957 current_voxel.y = static_cast<int>(std::floor((start_pos.y - grid_min.y) / voxel_size.y));
3958 current_voxel.z = static_cast<int>(std::floor((start_pos.z - grid_min.z) / voxel_size.z));
3959
3960 // Clamp to grid bounds
3961 current_voxel.x = std::max(0, std::min(current_voxel.x, voxel_grid_divisions.x - 1));
3962 current_voxel.y = std::max(0, std::min(current_voxel.y, voxel_grid_divisions.y - 1));
3963 current_voxel.z = std::max(0, std::min(current_voxel.z, voxel_grid_divisions.z - 1));
3964
3965 // DDA algorithm parameters
3966 helios::int3 step;
3967 vec3 t_delta, t_max;
3968
3969 // Set up stepping direction and delta t values
3970 for (int i = 0; i < 3; i++) {
3971 float dir_comp = (i == 0) ? ray_direction.x : (i == 1) ? ray_direction.y : ray_direction.z;
3972 float size_comp = (i == 0) ? voxel_size.x : (i == 1) ? voxel_size.y : voxel_size.z;
3973 float grid_min_comp = (i == 0) ? grid_min.x : (i == 1) ? grid_min.y : grid_min.z;
3974 float start_comp = (i == 0) ? start_pos.x : (i == 1) ? start_pos.y : start_pos.z;
3975 int current_comp = (i == 0) ? current_voxel.x : (i == 1) ? current_voxel.y : current_voxel.z;
3976 int max_comp = (i == 0) ? voxel_grid_divisions.x : (i == 1) ? voxel_grid_divisions.y : voxel_grid_divisions.z;
3977
3978 if (std::abs(dir_comp) < 1e-8f) {
3979 // Ray is parallel to this axis
3980 if (i == 0) {
3981 step.x = 0;
3982 t_delta.x = 1e30f; // Large value
3983 t_max.x = 1e30f;
3984 } else if (i == 1) {
3985 step.y = 0;
3986 t_delta.y = 1e30f;
3987 t_max.y = 1e30f;
3988 } else {
3989 step.z = 0;
3990 t_delta.z = 1e30f;
3991 t_max.z = 1e30f;
3992 }
3993 } else {
3994 // Calculate step direction and delta t
3995 if (i == 0) {
3996 step.x = (dir_comp > 0) ? 1 : -1;
3997 t_delta.x = std::abs(size_comp / dir_comp);
3998
3999 if (step.x > 0) {
4000 t_max.x = t_grid_min + (grid_min_comp + (current_comp + 1) * size_comp - start_comp) / dir_comp;
4001 } else {
4002 t_max.x = t_grid_min + (grid_min_comp + current_comp * size_comp - start_comp) / dir_comp;
4003 }
4004 } else if (i == 1) {
4005 step.y = (dir_comp > 0) ? 1 : -1;
4006 t_delta.y = std::abs(size_comp / dir_comp);
4007
4008 if (step.y > 0) {
4009 t_max.y = t_grid_min + (grid_min_comp + (current_comp + 1) * size_comp - start_comp) / dir_comp;
4010 } else {
4011 t_max.y = t_grid_min + (grid_min_comp + current_comp * size_comp - start_comp) / dir_comp;
4012 }
4013 } else {
4014 step.z = (dir_comp > 0) ? 1 : -1;
4015 t_delta.z = std::abs(size_comp / dir_comp);
4016
4017 if (step.z > 0) {
4018 t_max.z = t_grid_min + (grid_min_comp + (current_comp + 1) * size_comp - start_comp) / dir_comp;
4019 } else {
4020 t_max.z = t_grid_min + (grid_min_comp + current_comp * size_comp - start_comp) / dir_comp;
4021 }
4022 }
4023 }
4024 }
4025
4026 // Traverse the grid
4027 float current_t = t_grid_min;
4028
4029 while (validateVoxelIndices(current_voxel) && current_t < t_grid_max) {
4030 // Calculate path length through this voxel
4031 float next_t = std::min({t_max.x, t_max.y, t_max.z, t_grid_max});
4032 float path_length = next_t - current_t;
4033
4034 if (path_length > 1e-6f) {
4035 traversed_voxels.emplace_back(current_voxel, path_length);
4036 }
4037
4038 // Move to next voxel
4039 if (next_t >= t_grid_max) {
4040 break; // Reached end of grid
4041 }
4042
4043 // Determine which axis to step along
4044 if (t_max.x <= t_max.y && t_max.x <= t_max.z) {
4045 current_voxel.x += step.x;
4046 t_max.x += t_delta.x;
4047 } else if (t_max.y <= t_max.z) {
4048 current_voxel.y += step.y;
4049 t_max.y += t_delta.y;
4050 } else {
4051 current_voxel.z += step.z;
4052 t_max.z += t_delta.z;
4053 }
4054
4055 current_t = next_t;
4056 }
4057
4058 return traversed_voxels;
4059}
4060
4061void CollisionDetection::calculateVoxelRayPathLengths_CPU(const std::vector<vec3> &ray_origins, const std::vector<vec3> &ray_directions) {
4062
4063
4064 auto start_time = std::chrono::high_resolution_clock::now();
4065
4066 // Performance profiling variables
4067 std::atomic<long long> total_raycast_time(0);
4068 std::atomic<int> raycast_count(0);
4069
4070 // NOTE: BVH currency should be ensured by caller before calling this function
4071 // to avoid rebuilding BVH on every batch of rays
4072
4073 const int num_rays = static_cast<int>(ray_origins.size());
4074
4075 // PERFORMANCE OPTIMIZATION: Use thread-local storage to eliminate atomic operations
4076 const int total_voxels = voxel_grid_divisions.x * voxel_grid_divisions.y * voxel_grid_divisions.z;
4077
4078 // Pre-compute grid bounds for early culling
4079 vec3 grid_min = voxel_grid_center - 0.5f * voxel_grid_size;
4080 vec3 grid_max = voxel_grid_center + 0.5f * voxel_grid_size;
4081
4082#ifdef _OPENMP
4083 const int num_threads = omp_get_max_threads();
4084
4085 // Thread-local accumulation arrays to eliminate atomic operations
4086 std::vector<std::vector<int>> thread_ray_counts(num_threads, std::vector<int>(total_voxels, 0));
4087 std::vector<std::vector<float>> thread_path_lengths(num_threads, std::vector<float>(total_voxels, 0.0f));
4088 std::vector<std::vector<int>> thread_hit_before(num_threads, std::vector<int>(total_voxels, 0));
4089 std::vector<std::vector<int>> thread_hit_after(num_threads, std::vector<int>(total_voxels, 0));
4090 std::vector<std::vector<int>> thread_hit_inside(num_threads, std::vector<int>(total_voxels, 0));
4091 std::vector<std::vector<int>> thread_transmitted(num_threads, std::vector<int>(total_voxels, 0));
4092 std::vector<std::vector<std::vector<float>>> thread_individual_paths(num_threads, std::vector<std::vector<float>>(total_voxels));
4093
4094// Use OpenMP for parallel processing with thread-local accumulation
4095#pragma omp parallel for schedule(dynamic)
4096 for (int ray_idx = 0; ray_idx < num_rays; ray_idx++) {
4097 const int thread_id = omp_get_thread_num();
4098 const vec3 &ray_origin = ray_origins[ray_idx];
4099 const vec3 &ray_direction = ray_directions[ray_idx];
4100
4101 // Early culling: Skip rays that don't intersect the grid at all
4102 float t_grid_min, t_grid_max;
4103 if (!rayAABBIntersect(ray_origin, ray_direction, grid_min, grid_max, t_grid_min, t_grid_max) || t_grid_max <= 1e-6) {
4104 continue; // Skip this ray entirely
4105 }
4106
4107 // Use DDA traversal to get only intersected voxels
4108 auto traversed_voxels = traverseVoxelGrid(ray_origin, ray_direction);
4109
4110 // Only perform expensive ray classification for rays that actually intersect the grid
4111 if (traversed_voxels.empty()) {
4112 continue; // Skip rays that don't traverse any voxels
4113 }
4114
4115 // Perform ray classification once per ray (outside voxel loop for efficiency)
4116 auto raycast_start = std::chrono::high_resolution_clock::now();
4117 RayQuery query(ray_origin, ray_direction, -1.0f, {});
4118 HitResult hit = castRay(query);
4119 auto raycast_end = std::chrono::high_resolution_clock::now();
4120
4121 // Profile raycast performance
4122 total_raycast_time += std::chrono::duration_cast<std::chrono::microseconds>(raycast_end - raycast_start).count();
4123 raycast_count++;
4124
4125 float hit_distance = hit.hit ? hit.distance : 1e30f; // Large value if no hit
4126
4127 // Process each intersected voxel
4128 for (const auto &voxel_data: traversed_voxels) {
4129 const helios::int3 &voxel_idx = voxel_data.first;
4130 float path_length = voxel_data.second;
4131
4132 // Calculate voxel bounds for ray classification
4133 vec3 voxel_min, voxel_max;
4134 calculateVoxelAABB(voxel_idx, voxel_min, voxel_max);
4135
4136 // Get t_min and t_max for this voxel
4137 float t_min, t_max;
4138 rayAABBIntersect(ray_origin, ray_direction, voxel_min, voxel_max, t_min, t_max);
4139
4140 // Ensure valid intersection
4141 if (t_min < 0)
4142 t_min = 0;
4143
4144 // Perform ray classification for Beer's law calculations
4145 bool hit_before = false;
4146 bool hit_after = false;
4147 bool hit_inside = false;
4148
4149 if (hit.hit) {
4150 // Classify the hit based on where it occurs relative to voxel
4151 if (hit_distance < t_min) {
4152 // Hit occurs before entering voxel
4153 hit_before = true;
4154 } else if (hit_distance >= t_min && hit_distance <= t_max) {
4155 // Hit occurs inside voxel
4156 hit_inside = true;
4157 hit_after = true; // Also count as hit_after since it's after entering
4158 } else {
4159 // Hit occurs after exiting voxel
4160 hit_after = true;
4161 }
4162 } else {
4163 // No geometry hit - ray is transmitted through entire scene
4164 hit_after = true; // Consider this as reaching the voxel
4165 }
4166
4167 // PERFORMANCE OPTIMIZATION: Use thread-local accumulation (no synchronization!)
4168 size_t flat_idx = flatIndex(voxel_idx);
4169
4170 // All operations are now thread-local - no atomic operations needed!
4171 thread_ray_counts[thread_id][flat_idx]++;
4172 thread_path_lengths[thread_id][flat_idx] += path_length;
4173
4174 if (hit_before) {
4175 thread_hit_before[thread_id][flat_idx]++;
4176 }
4177 if (hit_after) {
4178 thread_hit_after[thread_id][flat_idx]++;
4179 }
4180 if (hit_inside) {
4181 thread_hit_inside[thread_id][flat_idx]++;
4182 } else {
4183 thread_transmitted[thread_id][flat_idx]++;
4184 }
4185
4186 // Store individual path lengths in thread-local storage (no critical section!)
4187 thread_individual_paths[thread_id][flat_idx].push_back(path_length);
4188 }
4189 }
4190
4191 // PERFORMANCE OPTIMIZATION: Reduction phase - combine thread-local results
4192 // This single reduction eliminates hundreds of thousands of atomic operations!
4193 for (int thread_id = 0; thread_id < num_threads; ++thread_id) {
4194 for (int voxel_idx = 0; voxel_idx < total_voxels; ++voxel_idx) {
4195 voxel_ray_counts_flat[voxel_idx] += thread_ray_counts[thread_id][voxel_idx];
4196 voxel_path_lengths_flat[voxel_idx] += thread_path_lengths[thread_id][voxel_idx];
4197 voxel_hit_before_flat[voxel_idx] += thread_hit_before[thread_id][voxel_idx];
4198 voxel_hit_after_flat[voxel_idx] += thread_hit_after[thread_id][voxel_idx];
4199 voxel_hit_inside_flat[voxel_idx] += thread_hit_inside[thread_id][voxel_idx];
4200 voxel_transmitted_flat[voxel_idx] += thread_transmitted[thread_id][voxel_idx];
4201 }
4202 }
4203
4204 // Post-process for flat array storage - aggregate individual paths from thread-local storage
4205 if (use_flat_arrays) {
4206 // Consolidate individual path lengths into flat storage with proper offsets
4207 voxel_individual_path_lengths_flat.clear();
4208
4209 // Calculate total size needed from all threads
4210 size_t total_paths = 0;
4211 for (int thread_id = 0; thread_id < num_threads; ++thread_id) {
4212 for (int voxel_idx = 0; voxel_idx < total_voxels; ++voxel_idx) {
4213 total_paths += thread_individual_paths[thread_id][voxel_idx].size();
4214 }
4215 }
4216 voxel_individual_path_lengths_flat.reserve(total_paths);
4217
4218 // Build flat array and offsets by aggregating from all threads
4219 size_t current_offset = 0;
4220 for (int voxel_idx = 0; voxel_idx < total_voxels; ++voxel_idx) {
4221 voxel_individual_path_offsets[voxel_idx] = current_offset;
4222 size_t voxel_path_count = 0;
4223
4224 // Aggregate paths from all threads for this voxel
4225 for (int thread_id = 0; thread_id < num_threads; ++thread_id) {
4226 for (float path_length: thread_individual_paths[thread_id][voxel_idx]) {
4227 voxel_individual_path_lengths_flat.push_back(path_length);
4228 voxel_path_count++;
4229 }
4230 }
4231
4232 voxel_individual_path_counts[voxel_idx] = voxel_path_count;
4233 current_offset += voxel_path_count;
4234 }
4235 }
4236#else
4237 // Serial fallback when OpenMP is not available
4238 const int num_threads = 1;
4239 const int thread_id = 0;
4240
4241 // Direct accumulation arrays for serial processing
4242 std::vector<int> serial_ray_counts(total_voxels, 0);
4243 std::vector<float> serial_path_lengths(total_voxels, 0.0f);
4244 std::vector<int> serial_hit_before(total_voxels, 0);
4245 std::vector<int> serial_hit_after(total_voxels, 0);
4246 std::vector<int> serial_hit_inside(total_voxels, 0);
4247 std::vector<int> serial_transmitted(total_voxels, 0);
4248 std::vector<std::vector<float>> serial_individual_paths(total_voxels);
4249
4250 // Serial processing without OpenMP pragmas
4251 for (int ray_idx = 0; ray_idx < num_rays; ray_idx++) {
4252 const vec3 &ray_origin = ray_origins[ray_idx];
4253 const vec3 &ray_direction = ray_directions[ray_idx];
4254
4255 // Early culling: Skip rays that don't intersect the grid at all
4256 float t_grid_min, t_grid_max;
4257 if (!rayAABBIntersect(ray_origin, ray_direction, grid_min, grid_max, t_grid_min, t_grid_max) || t_grid_max <= 1e-6) {
4258 continue; // Skip this ray entirely
4259 }
4260
4261 // Use DDA traversal to get only intersected voxels
4262 auto traversed_voxels = traverseVoxelGrid(ray_origin, ray_direction);
4263
4264 // Only perform expensive ray classification for rays that actually intersect the grid
4265 if (traversed_voxels.empty()) {
4266 continue; // Skip rays that don't traverse any voxels
4267 }
4268
4269 // Perform ray classification once per ray (outside voxel loop for efficiency)
4270 auto raycast_start = std::chrono::high_resolution_clock::now();
4271 RayQuery query(ray_origin, ray_direction, -1.0f, {});
4272 HitResult hit = castRay(query);
4273 auto raycast_end = std::chrono::high_resolution_clock::now();
4274
4275 // Profile raycast performance
4276 total_raycast_time += std::chrono::duration_cast<std::chrono::microseconds>(raycast_end - raycast_start).count();
4277 raycast_count++;
4278
4279 float hit_distance = hit.hit ? hit.distance : 1e30f; // Large value if no hit
4280
4281 // Process each intersected voxel
4282 for (const auto &voxel_data: traversed_voxels) {
4283 const helios::int3 &voxel_idx = voxel_data.first;
4284 float path_length = voxel_data.second;
4285
4286 // Calculate voxel bounds for ray classification
4287 vec3 voxel_min, voxel_max;
4288 calculateVoxelAABB(voxel_idx, voxel_min, voxel_max);
4289
4290 // Get t_min and t_max for this voxel
4291 float t_min, t_max;
4292 rayAABBIntersect(ray_origin, ray_direction, voxel_min, voxel_max, t_min, t_max);
4293
4294 // Ensure valid intersection
4295 if (t_min < 0)
4296 t_min = 0;
4297
4298 // Perform ray classification for Beer's law calculations
4299 bool hit_before = false;
4300 bool hit_after = false;
4301 bool hit_inside = false;
4302
4303 if (hit.hit) {
4304 // Classify the hit based on where it occurs relative to voxel
4305 if (hit_distance < t_min) {
4306 // Hit occurs before entering voxel
4307 hit_before = true;
4308 } else if (hit_distance >= t_min && hit_distance <= t_max) {
4309 // Hit occurs inside voxel
4310 hit_inside = true;
4311 hit_after = true; // Also count as hit_after since it's after entering
4312 } else {
4313 // Hit occurs after exiting voxel
4314 hit_after = true;
4315 }
4316 } else {
4317 // No geometry hit - ray is transmitted through entire scene
4318 hit_after = true; // Consider this as reaching the voxel
4319 }
4320
4321 // Direct accumulation for serial processing
4322 size_t flat_idx = flatIndex(voxel_idx);
4323
4324 serial_ray_counts[flat_idx]++;
4325 serial_path_lengths[flat_idx] += path_length;
4326
4327 if (hit_before) {
4328 serial_hit_before[flat_idx]++;
4329 }
4330 if (hit_after) {
4331 serial_hit_after[flat_idx]++;
4332 }
4333 if (hit_inside) {
4334 serial_hit_inside[flat_idx]++;
4335 } else {
4336 serial_transmitted[flat_idx]++;
4337 }
4338
4339 // Store individual path lengths for serial processing
4340 serial_individual_paths[flat_idx].push_back(path_length);
4341 }
4342 }
4343
4344 // Direct assignment from serial arrays to final storage
4345 for (int voxel_idx = 0; voxel_idx < total_voxels; ++voxel_idx) {
4346 voxel_ray_counts_flat[voxel_idx] += serial_ray_counts[voxel_idx];
4347 voxel_path_lengths_flat[voxel_idx] += serial_path_lengths[voxel_idx];
4348 voxel_hit_before_flat[voxel_idx] += serial_hit_before[voxel_idx];
4349 voxel_hit_after_flat[voxel_idx] += serial_hit_after[voxel_idx];
4350 voxel_hit_inside_flat[voxel_idx] += serial_hit_inside[voxel_idx];
4351 voxel_transmitted_flat[voxel_idx] += serial_transmitted[voxel_idx];
4352 }
4353
4354 // Post-process for flat array storage - aggregate individual paths from serial storage
4355 if (use_flat_arrays) {
4356 // Consolidate individual path lengths into flat storage with proper offsets
4357 voxel_individual_path_lengths_flat.clear();
4358
4359 // Calculate total size needed
4360 size_t total_paths = 0;
4361 for (int voxel_idx = 0; voxel_idx < total_voxels; ++voxel_idx) {
4362 total_paths += serial_individual_paths[voxel_idx].size();
4363 }
4364 voxel_individual_path_lengths_flat.reserve(total_paths);
4365
4366 // Build flat array and offsets
4367 size_t current_offset = 0;
4368 for (int voxel_idx = 0; voxel_idx < total_voxels; ++voxel_idx) {
4369 voxel_individual_path_offsets[voxel_idx] = current_offset;
4370 size_t voxel_path_count = serial_individual_paths[voxel_idx].size();
4371
4372 // Copy paths to flat array
4373 for (float path_length: serial_individual_paths[voxel_idx]) {
4374 voxel_individual_path_lengths_flat.push_back(path_length);
4375 }
4376
4377 voxel_individual_path_counts[voxel_idx] = voxel_path_count;
4378 current_offset += voxel_path_count;
4379 }
4380 }
4381#endif
4382
4383 auto end_time = std::chrono::high_resolution_clock::now();
4384 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
4385
4386 // Performance profiling output
4387 long long avg_raycast_time = raycast_count > 0 ? total_raycast_time.load() / raycast_count.load() : 0;
4388 //
4389 // if (printmessages) {
4390 // // Report some statistics
4391 // int total_ray_voxel_intersections = 0;
4392 // if (use_flat_arrays) {
4393 // // Sum from flat arrays
4394 // for (size_t i = 0; i < voxel_ray_counts_flat.size(); i++) {
4395 // total_ray_voxel_intersections += voxel_ray_counts_flat[i];
4396 // }
4397 // } else {
4398 // // Sum from nested vectors
4399 // for (int i = 0; i < voxel_grid_divisions.x; i++) {
4400 // for (int j = 0; j < voxel_grid_divisions.y; j++) {
4401 // for (int k = 0; k < voxel_grid_divisions.z; k++) {
4402 // total_ray_voxel_intersections += voxel_ray_counts[i][j][k];
4403 // }
4404 // }
4405 // }
4406 // }
4407 // }
4408}
4409
4410#ifdef HELIOS_CUDA_AVAILABLE
4411bool CollisionDetection::calculateVoxelRayPathLengths_GPU(const std::vector<vec3> &ray_origins, const std::vector<vec3> &ray_directions) {
4412 // Check if a usable GPU is actually available at runtime (also honors HELIOS_NO_GPU)
4413 if (!isGPUAvailable()) {
4414 // No usable GPU - return false to trigger CPU fallback
4415 return false;
4416 }
4417
4418 if (printmessages) {
4419 }
4420
4421 auto start_time = std::chrono::high_resolution_clock::now();
4422
4423 const int num_rays = static_cast<int>(ray_origins.size());
4424 const int total_voxels = voxel_grid_divisions.x * voxel_grid_divisions.y * voxel_grid_divisions.z;
4425
4426 // Prepare data for GPU kernel
4427 std::vector<float> h_ray_origins(num_rays * 3);
4428 std::vector<float> h_ray_directions(num_rays * 3);
4429 std::vector<int> h_voxel_ray_counts(total_voxels, 0);
4430 std::vector<float> h_voxel_path_lengths(total_voxels, 0.0f);
4431 std::vector<int> h_voxel_transmitted(total_voxels, 0);
4432 std::vector<int> h_voxel_hit_before(total_voxels, 0);
4433 std::vector<int> h_voxel_hit_after(total_voxels, 0);
4434 std::vector<int> h_voxel_hit_inside(total_voxels, 0);
4435
4436 // Convert ray data to flat arrays
4437 for (int i = 0; i < num_rays; i++) {
4438 h_ray_origins[i * 3 + 0] = ray_origins[i].x;
4439 h_ray_origins[i * 3 + 1] = ray_origins[i].y;
4440 h_ray_origins[i * 3 + 2] = ray_origins[i].z;
4441
4442 h_ray_directions[i * 3 + 0] = ray_directions[i].x;
4443 h_ray_directions[i * 3 + 1] = ray_directions[i].y;
4444 h_ray_directions[i * 3 + 2] = ray_directions[i].z;
4445 }
4446
4447 // Check if there are any primitives in the scene for geometry detection
4448 int primitive_count = static_cast<int>(primitive_cache.size());
4449
4450 // Launch CUDA kernel
4451 bool gpu_success = launchVoxelRayPathLengths(num_rays, h_ray_origins.data(), h_ray_directions.data(), voxel_grid_center.x, voxel_grid_center.y, voxel_grid_center.z, voxel_grid_size.x, voxel_grid_size.y, voxel_grid_size.z, voxel_grid_divisions.x,
4452 voxel_grid_divisions.y, voxel_grid_divisions.z, primitive_count, h_voxel_ray_counts.data(), h_voxel_path_lengths.data(), h_voxel_transmitted.data(), h_voxel_hit_before.data(), h_voxel_hit_after.data(),
4453 h_voxel_hit_inside.data());
4454
4455 if (!gpu_success) {
4456 // GPU kernel failed - return false to trigger CPU fallback
4457 return false;
4458 }
4459
4460 // Copy results back to class data structures
4461 if (use_flat_arrays) {
4462 // Copy directly to flat arrays
4463 voxel_ray_counts_flat = h_voxel_ray_counts;
4464 voxel_path_lengths_flat = h_voxel_path_lengths;
4465 voxel_transmitted_flat = h_voxel_transmitted;
4466
4467 // Copy hit classification data from GPU kernel
4468 voxel_hit_before_flat = h_voxel_hit_before;
4469 voxel_hit_after_flat = h_voxel_hit_after;
4470 voxel_hit_inside_flat = h_voxel_hit_inside;
4471
4472 // Initialize individual path length data structures
4473 // Note: GPU implementation currently provides aggregate data only
4474 // For now, initialize empty individual path data to prevent crashes
4475 voxel_individual_path_lengths_flat.clear();
4476 std::fill(voxel_individual_path_offsets.begin(), voxel_individual_path_offsets.end(), 0);
4477 std::fill(voxel_individual_path_counts.begin(), voxel_individual_path_counts.end(), 0);
4478
4479 // TODO: Implement proper individual path length collection in GPU kernel
4480 // For now, estimate individual path lengths based on ray geometry
4481 // This is a workaround until the GPU kernel can provide individual path data
4482 for (int voxel_idx = 0; voxel_idx < total_voxels; ++voxel_idx) {
4483 voxel_individual_path_offsets[voxel_idx] = voxel_individual_path_lengths_flat.size();
4484
4485 if (h_voxel_ray_counts[voxel_idx] > 0) {
4486 // Calculate individual path lengths for each ray by simulating ray-voxel intersection
4487 // This is an approximation but more accurate than using just averages
4488 std::vector<float> estimated_paths;
4489
4490 // Convert flat voxel index back to 3D coordinates
4491 int voxel_z = voxel_idx % voxel_grid_divisions.z;
4492 int voxel_y = (voxel_idx / voxel_grid_divisions.z) % voxel_grid_divisions.y;
4493 int voxel_x = voxel_idx / (voxel_grid_divisions.y * voxel_grid_divisions.z);
4494
4495 // Calculate voxel bounds
4496 vec3 voxel_size = voxel_grid_size;
4497 voxel_size.x /= voxel_grid_divisions.x;
4498 voxel_size.y /= voxel_grid_divisions.y;
4499 voxel_size.z /= voxel_grid_divisions.z;
4500
4501 vec3 voxel_min = voxel_grid_center - voxel_grid_size * 0.5f;
4502 voxel_min.x += voxel_x * voxel_size.x;
4503 voxel_min.y += voxel_y * voxel_size.y;
4504 voxel_min.z += voxel_z * voxel_size.z;
4505
4506 vec3 voxel_max = voxel_min + voxel_size;
4507
4508 // Check each ray to see if it intersects this voxel and calculate path length
4509 for (int ray_idx = 0; ray_idx < num_rays; ++ray_idx) {
4510 vec3 ray_origin = ray_origins[ray_idx];
4511 vec3 ray_dir = ray_directions[ray_idx];
4512
4513 // Ray-box intersection algorithm
4514 float t_min = 0.0f;
4515 float t_max = std::numeric_limits<float>::max();
4516
4517 // Check intersection with each axis-aligned slab
4518 for (int axis = 0; axis < 3; ++axis) {
4519 float origin_comp = (axis == 0) ? ray_origin.x : (axis == 1) ? ray_origin.y : ray_origin.z;
4520 float dir_comp = (axis == 0) ? ray_dir.x : (axis == 1) ? ray_dir.y : ray_dir.z;
4521 float min_comp = (axis == 0) ? voxel_min.x : (axis == 1) ? voxel_min.y : voxel_min.z;
4522 float max_comp = (axis == 0) ? voxel_max.x : (axis == 1) ? voxel_max.y : voxel_max.z;
4523
4524 if (std::abs(dir_comp) < 1e-9f) {
4525 // Ray is parallel to slab
4526 if (origin_comp < min_comp || origin_comp > max_comp) {
4527 t_max = -1.0f; // No intersection
4528 break;
4529 }
4530 } else {
4531 float t1 = (min_comp - origin_comp) / dir_comp;
4532 float t2 = (max_comp - origin_comp) / dir_comp;
4533
4534 if (t1 > t2)
4535 std::swap(t1, t2);
4536
4537 t_min = std::max(t_min, t1);
4538 t_max = std::min(t_max, t2);
4539
4540 if (t_min > t_max)
4541 break; // No intersection
4542 }
4543 }
4544
4545 // If there's a valid intersection, calculate path length
4546 if (t_max > t_min && t_max > 0.0f) {
4547 float entry_t = std::max(0.0f, t_min);
4548 float exit_t = t_max;
4549 float path_length = exit_t - entry_t;
4550
4551 if (path_length > 1e-6f) {
4552 estimated_paths.push_back(path_length);
4553 }
4554 }
4555 }
4556
4557 // Store the estimated paths
4558 for (float path_length: estimated_paths) {
4559 voxel_individual_path_lengths_flat.push_back(path_length);
4560 }
4561 voxel_individual_path_counts[voxel_idx] = estimated_paths.size();
4562 } else {
4563 voxel_individual_path_counts[voxel_idx] = 0;
4564 }
4565 }
4566
4567 } else {
4568 // Copy to nested vectors
4569 for (int i = 0; i < voxel_grid_divisions.x; i++) {
4570 for (int j = 0; j < voxel_grid_divisions.y; j++) {
4571 for (int k = 0; k < voxel_grid_divisions.z; k++) {
4572 int flat_idx = i * voxel_grid_divisions.y * voxel_grid_divisions.z + j * voxel_grid_divisions.z + k;
4573 voxel_ray_counts[i][j][k] = h_voxel_ray_counts[flat_idx];
4574 voxel_path_lengths[i][j][k] = h_voxel_path_lengths[flat_idx];
4575 voxel_transmitted[i][j][k] = h_voxel_transmitted[flat_idx];
4576
4577 // Copy hit classification data from GPU kernel
4578 voxel_hit_before[i][j][k] = h_voxel_hit_before[flat_idx];
4579 voxel_hit_after[i][j][k] = h_voxel_hit_after[flat_idx];
4580 voxel_hit_inside[i][j][k] = h_voxel_hit_inside[flat_idx];
4581
4582 // Initialize individual path lengths (approximation)
4583 voxel_individual_path_lengths[i][j][k].clear();
4584 if (h_voxel_ray_counts[flat_idx] > 0) {
4585 float avg_path_length = h_voxel_path_lengths[flat_idx] / h_voxel_ray_counts[flat_idx];
4586 for (int ray = 0; ray < h_voxel_ray_counts[flat_idx]; ++ray) {
4587 voxel_individual_path_lengths[i][j][k].push_back(avg_path_length);
4588 }
4589 }
4590 }
4591 }
4592 }
4593 }
4594
4595 auto end_time = std::chrono::high_resolution_clock::now();
4596 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
4597
4598 if (printmessages) {
4599
4600 // Report some statistics
4601 int total_ray_voxel_intersections = 0;
4602 for (const auto &count: h_voxel_ray_counts) {
4603 total_ray_voxel_intersections += count;
4604 }
4605 }
4606
4607 return true; // GPU execution succeeded
4608}
4609#endif
4610
4611void CollisionDetection::ensureOptimizedBVH() {
4612 // Convert standard BVH to Structure-of-Arrays format for optimal cache performance
4613 // Only rebuild if BVH structure has actually changed OR SoA is explicitly dirty
4614 if (soa_dirty || bvh_nodes_soa.node_count != bvh_nodes.size() || bvh_nodes_soa.aabb_mins.empty()) {
4615
4616 if (bvh_nodes.empty()) {
4617 bvh_nodes_soa.clear();
4618 bvh_nodes_soa.node_count = 0;
4619 return;
4620 }
4621
4622 size_t node_count = bvh_nodes.size();
4623
4624 // OPTIMIZATION 1: Pre-allocate all arrays to exact size (no reserve+push_back)
4625 bvh_nodes_soa.node_count = node_count;
4626 bvh_nodes_soa.aabb_mins.resize(node_count);
4627 bvh_nodes_soa.aabb_maxs.resize(node_count);
4628 bvh_nodes_soa.left_children.resize(node_count);
4629 bvh_nodes_soa.right_children.resize(node_count);
4630 bvh_nodes_soa.primitive_starts.resize(node_count);
4631 bvh_nodes_soa.primitive_counts.resize(node_count);
4632 bvh_nodes_soa.is_leaf_flags.resize(node_count);
4633
4634 // OPTIMIZATION 2: Direct assignment instead of push_back (much faster)
4635 for (size_t i = 0; i < node_count; ++i) {
4636 const BVHNode &node = bvh_nodes[i];
4637
4638 // Hot data: frequently accessed during traversal
4639 bvh_nodes_soa.aabb_mins[i] = node.aabb_min;
4640 bvh_nodes_soa.aabb_maxs[i] = node.aabb_max;
4641 bvh_nodes_soa.left_children[i] = node.left_child;
4642 bvh_nodes_soa.right_children[i] = node.right_child;
4643
4644 // Cold data: accessed less frequently
4645 bvh_nodes_soa.primitive_starts[i] = node.primitive_start;
4646 bvh_nodes_soa.primitive_counts[i] = node.primitive_count;
4647 bvh_nodes_soa.is_leaf_flags[i] = node.is_leaf ? 1 : 0;
4648 }
4649
4650 // Mark SoA as clean after successful conversion
4651 soa_dirty = false;
4652 }
4653}
4654
4655void CollisionDetection::updatePrimitiveAABBCache(uint uuid) {
4656 // Check if primitive exists
4657 if (!context->doesPrimitiveExist(uuid)) {
4658 return;
4659 }
4660
4661 try {
4662 // Get primitive vertices to compute AABB
4663 std::vector<vec3> vertices = context->getPrimitiveVertices(uuid);
4664 if (vertices.empty()) {
4665 return;
4666 }
4667
4668 // Compute AABB from vertices
4669 vec3 aabb_min = vertices[0];
4670 vec3 aabb_max = vertices[0];
4671
4672 for (const vec3 &vertex: vertices) {
4673 aabb_min.x = std::min(aabb_min.x, vertex.x);
4674 aabb_min.y = std::min(aabb_min.y, vertex.y);
4675 aabb_min.z = std::min(aabb_min.z, vertex.z);
4676
4677 aabb_max.x = std::max(aabb_max.x, vertex.x);
4678 aabb_max.y = std::max(aabb_max.y, vertex.y);
4679 aabb_max.z = std::max(aabb_max.z, vertex.z);
4680 }
4681
4682 // Store in cache
4683 primitive_aabbs_cache[uuid] = std::make_pair(aabb_min, aabb_max);
4684
4685 } catch (const std::exception &e) {
4686 if (printmessages) {
4687 std::cerr << "Warning: Failed to cache AABB for primitive " << uuid << ": " << e.what() << std::endl;
4688 }
4689 }
4690}
4691
4692void CollisionDetection::optimizedRebuildBVH(const std::set<uint> &final_geometry) {
4693 // Convert to vector for buildBVH compatibility
4694 std::vector<uint> final_primitives(final_geometry.begin(), final_geometry.end());
4695
4696 // Ensure all primitive AABBs are cached
4697 for (uint uuid: final_geometry) {
4698 if (primitive_aabbs_cache.find(uuid) == primitive_aabbs_cache.end()) {
4699 updatePrimitiveAABBCache(uuid);
4700 }
4701 }
4702
4703 if (printmessages) {
4704 std::cout << "Optimized rebuild with " << final_primitives.size() << " primitives (using cached AABBs)" << std::endl;
4705 }
4706
4707 // Use regular buildBVH but with pre-cached AABBs for efficiency
4708 buildBVH(final_primitives);
4709
4710 // Update tracking
4711 last_bvh_geometry = final_geometry;
4712 bvh_dirty = false;
4713 soa_dirty = true; // SoA needs rebuild after BVH change
4714}
4715
4716// -------- TREE-BASED BVH IMPLEMENTATION --------
4717
4718void CollisionDetection::enableTreeBasedBVH(float isolation_distance) {
4719 tree_based_bvh_enabled = true;
4720 tree_isolation_distance = isolation_distance;
4721}
4722
4724 tree_based_bvh_enabled = false;
4725 tree_bvh_map.clear();
4726 object_to_tree_map.clear();
4727}
4728
4730 return tree_based_bvh_enabled;
4731}
4732
4734 if (static_obstacle_primitives.empty() || obstacle_spatial_grid_initialized) {
4735 return;
4736 }
4737
4738 // Set cell size to be roughly the static obstacle distance for optimal performance
4739 obstacle_spatial_grid.cell_size = 20.0f; // Slightly larger than MAX_STATIC_OBSTACLE_DISTANCE
4740 obstacle_spatial_grid.grid_cells.clear();
4741
4742 // Insert each static obstacle into the spatial grid
4743 for (uint obstacle_prim: static_obstacle_primitives) {
4744 if (context->doesPrimitiveExist(obstacle_prim)) {
4745 helios::vec3 min_corner, max_corner;
4746 context->getPrimitiveBoundingBox(obstacle_prim, min_corner, max_corner);
4747 helios::vec3 prim_center = (min_corner + max_corner) * 0.5f;
4748
4749 int64_t grid_key = obstacle_spatial_grid.getGridKey(prim_center.x, prim_center.y);
4750 obstacle_spatial_grid.grid_cells[grid_key].push_back(obstacle_prim);
4751 }
4752 }
4753
4754 obstacle_spatial_grid_initialized = true;
4755}
4756
4757std::vector<uint> CollisionDetection::ObstacleSpatialGrid::getRelevantObstacles(const helios::vec3 &position, float radius) const {
4758 std::vector<uint> relevant_obstacles;
4759
4760 // Calculate the range of grid cells to search
4761 int32_t min_grid_x = static_cast<int32_t>(std::floor((position.x - radius) / cell_size));
4762 int32_t max_grid_x = static_cast<int32_t>(std::floor((position.x + radius) / cell_size));
4763 int32_t min_grid_y = static_cast<int32_t>(std::floor((position.y - radius) / cell_size));
4764 int32_t max_grid_y = static_cast<int32_t>(std::floor((position.y + radius) / cell_size));
4765
4766 // Search all relevant grid cells
4767 for (int32_t grid_x = min_grid_x; grid_x <= max_grid_x; ++grid_x) {
4768 for (int32_t grid_y = min_grid_y; grid_y <= max_grid_y; ++grid_y) {
4769 int64_t grid_key = (static_cast<int64_t>(grid_x) << 32) | static_cast<uint32_t>(grid_y);
4770
4771 auto cell_it = grid_cells.find(grid_key);
4772 if (cell_it != grid_cells.end()) {
4773 // Add all obstacles from this cell (could add distance filtering here if needed)
4774 relevant_obstacles.insert(relevant_obstacles.end(), cell_it->second.begin(), cell_it->second.end());
4775 }
4776 }
4777 }
4778
4779 return relevant_obstacles;
4780}
4781
4782void CollisionDetection::registerTree(uint tree_object_id, const std::vector<uint> &tree_primitives) {
4783 if (!tree_based_bvh_enabled) {
4784 if (printmessages) {
4785 std::cout << "WARNING: Tree registration ignored - tree-based BVH not enabled" << std::endl;
4786 }
4787 return;
4788 }
4789
4790 // Calculate tree spatial bounds
4791 vec3 tree_center(0, 0, 0);
4792 vec3 aabb_min(1e30f, 1e30f, 1e30f);
4793 vec3 aabb_max(-1e30f, -1e30f, -1e30f);
4794
4795 for (uint prim_uuid: tree_primitives) {
4796 if (context->doesPrimitiveExist(prim_uuid)) {
4797 vec3 prim_min, prim_max;
4798 context->getPrimitiveBoundingBox(prim_uuid, prim_min, prim_max);
4799
4800 aabb_min.x = std::min(aabb_min.x, prim_min.x);
4801 aabb_min.y = std::min(aabb_min.y, prim_min.y);
4802 aabb_min.z = std::min(aabb_min.z, prim_min.z);
4803
4804 aabb_max.x = std::max(aabb_max.x, prim_max.x);
4805 aabb_max.y = std::max(aabb_max.y, prim_max.y);
4806 aabb_max.z = std::max(aabb_max.z, prim_max.z);
4807 }
4808 }
4809
4810 tree_center = (aabb_min + aabb_max) * 0.5f;
4811 float tree_radius = (aabb_max - aabb_min).magnitude() * 0.5f;
4812
4813 // Create or update tree BVH entry
4814 TreeBVH &tree_bvh = tree_bvh_map[tree_object_id];
4815 tree_bvh.tree_object_id = tree_object_id;
4816 tree_bvh.tree_center = tree_center;
4817 tree_bvh.tree_radius = tree_radius;
4818
4819 // Update object-to-tree mapping
4820 for (uint prim_uuid: tree_primitives) {
4821 object_to_tree_map[prim_uuid] = tree_object_id;
4822 }
4823}
4824
4825void CollisionDetection::setStaticObstacles(const std::vector<uint> &obstacle_primitives) {
4826 static_obstacle_primitives.clear();
4827
4828 // Validate and store static obstacles
4829 for (uint prim_uuid: obstacle_primitives) {
4830 if (context->doesPrimitiveExist(prim_uuid)) {
4831 static_obstacle_primitives.push_back(prim_uuid);
4832 }
4833 }
4834
4835 // Initialize spatial grid for fast obstacle lookup
4836 obstacle_spatial_grid_initialized = false;
4838}
4839
4840std::vector<uint> CollisionDetection::getRelevantGeometryForTree(const helios::vec3 &query_position, const std::vector<uint> &query_primitives, float max_distance) {
4841 std::vector<uint> relevant_geometry;
4842
4843 if (!tree_based_bvh_enabled) {
4844 // If tree-based BVH is disabled, return empty to signal using all geometry
4845 return relevant_geometry;
4846 }
4847
4848 // Use spatial grid for fast static obstacle lookup if available
4849 const float MAX_STATIC_OBSTACLE_DISTANCE = max_distance; // Use collision detection distance for static obstacles
4850
4851 if (obstacle_spatial_grid_initialized && !static_obstacle_primitives.empty()) {
4852 // Fast spatial grid lookup - O(1) instead of O(N)
4853 std::vector<uint> candidate_obstacles = obstacle_spatial_grid.getRelevantObstacles(query_position, MAX_STATIC_OBSTACLE_DISTANCE);
4854
4855
4856 // Still need distance check for precise filtering within grid cells
4857 for (uint static_prim: candidate_obstacles) {
4858 if (context->doesPrimitiveExist(static_prim)) {
4859 helios::vec3 min_corner, max_corner;
4860 context->getPrimitiveBoundingBox(static_prim, min_corner, max_corner);
4861 helios::vec3 prim_center = (min_corner + max_corner) * 0.5f;
4862 float distance = (query_position - prim_center).magnitude();
4863 if (distance < MAX_STATIC_OBSTACLE_DISTANCE) {
4864 relevant_geometry.push_back(static_prim);
4865 }
4866 }
4867 }
4868
4869 } else {
4870 // Fallback to linear search if spatial grid not available
4871 for (uint static_prim: static_obstacle_primitives) {
4872 if (context->doesPrimitiveExist(static_prim)) {
4873 helios::vec3 min_corner, max_corner;
4874 context->getPrimitiveBoundingBox(static_prim, min_corner, max_corner);
4875 helios::vec3 prim_center = (min_corner + max_corner) * 0.5f;
4876 float distance = (query_position - prim_center).magnitude();
4877 if (distance < MAX_STATIC_OBSTACLE_DISTANCE) {
4878 relevant_geometry.push_back(static_prim);
4879 }
4880 }
4881 }
4882 }
4883
4884 // Find which tree this query belongs to
4885 uint source_tree_id = 0;
4886 if (!query_primitives.empty()) {
4887 // Use the first query primitive to identify source tree
4888 auto it = object_to_tree_map.find(query_primitives[0]);
4889 if (it != object_to_tree_map.end()) {
4890 source_tree_id = it->second;
4891 }
4892 }
4893
4894 // If we couldn't identify the source tree, find the closest tree to query position
4895 if (source_tree_id == 0) {
4896 float min_distance = 1e30f;
4897 for (const auto &tree_pair: tree_bvh_map) {
4898 const TreeBVH &tree = tree_pair.second;
4899 float distance = (query_position - tree.tree_center).magnitude();
4900 if (distance < min_distance) {
4901 min_distance = distance;
4902 source_tree_id = tree.tree_object_id;
4903 }
4904 }
4905 }
4906
4907 // Add geometry from source tree and nearby trees within interaction distance
4908 for (const auto &tree_pair: tree_bvh_map) {
4909 const TreeBVH &tree = tree_pair.second;
4910 uint tree_id = tree_pair.first;
4911
4912 if (tree_id == source_tree_id) {
4913 // Always include source tree's own geometry
4914 for (uint prim_uuid: tree.primitive_indices) {
4915 if (context->doesPrimitiveExist(prim_uuid)) {
4916 relevant_geometry.push_back(prim_uuid);
4917 }
4918 }
4919 } else {
4920 // Check if other trees are within interaction distance
4921 float distance = (query_position - tree.tree_center).magnitude();
4922 float interaction_threshold = tree_isolation_distance + tree.tree_radius;
4923
4924 if (distance < interaction_threshold) {
4925 // Include nearby tree's geometry
4926 for (uint prim_uuid: tree.primitive_indices) {
4927 if (context->doesPrimitiveExist(prim_uuid)) {
4928 relevant_geometry.push_back(prim_uuid);
4929 }
4930 }
4931 }
4932 }
4933 }
4934
4935 return relevant_geometry;
4936}