1.3.64
 
Loading...
Searching...
No Matches
selfTest.cpp
Go to the documentation of this file.
1
16#include "CollisionDetection.h"
17#define DOCTEST_CONFIG_IMPLEMENT
18#include <chrono>
19#include <cmath>
20#include <doctest.h>
21#include <iostream>
22#include "doctest_utils.h"
23#include "global.h"
24
25using namespace helios;
26
27int CollisionDetection::selfTest(int argc, char **argv) {
28 return helios::runDoctestWithValidation(argc, argv);
29}
30
31namespace CollisionTests {
32
36 std::vector<uint> generateSeparatedTriangles(Context *context, int count, float separation = 5.0f) {
37 std::vector<uint> uuids;
38 for (int i = 0; i < count; i++) {
39 float x = i * separation;
40 uint uuid = context->addTriangle(make_vec3(x, -1, 0), make_vec3(x + 1, -1, 0), make_vec3(x + 0.5f, 1, 0));
41 uuids.push_back(uuid);
42 }
43 return uuids;
44 }
45
49 std::vector<uint> generateOverlappingCluster(Context *context, int count, vec3 center = make_vec3(0, 0, 0)) {
50 std::vector<uint> uuids;
51 for (int i = 0; i < count; i++) {
52 float angle = (2.0f * M_PI * i) / count;
53 float radius = 0.5f; // Overlapping radius
54 float x = center.x + radius * cos(angle);
55 float y = center.y + radius * sin(angle);
56
57 uint uuid = context->addTriangle(make_vec3(x - 0.5f, y - 0.5f, center.z), make_vec3(x + 0.5f, y - 0.5f, center.z), make_vec3(x, y + 0.5f, center.z));
58 uuids.push_back(uuid);
59 }
60 return uuids;
61 }
62
72 std::vector<uint> createParallelWallsWithGap(Context *context, vec3 gap_center, float gap_width, float wall_height = 2.0f, float wall_distance = 3.0f) {
73 std::vector<uint> uuids;
74 float half_gap = gap_width * 0.5f;
75 float half_height = wall_height * 0.5f;
76
77 // Left wall (before gap)
78 float left_wall_end = gap_center.x - half_gap;
79 if (left_wall_end > -5.0f) {
80 uint uuid1 = context->addTriangle(make_vec3(-5.0f, gap_center.y - half_height, wall_distance), make_vec3(left_wall_end, gap_center.y - half_height, wall_distance), make_vec3(-5.0f, gap_center.y + half_height, wall_distance));
81 uint uuid2 = context->addTriangle(make_vec3(left_wall_end, gap_center.y - half_height, wall_distance), make_vec3(left_wall_end, gap_center.y + half_height, wall_distance), make_vec3(-5.0f, gap_center.y + half_height, wall_distance));
82 uuids.push_back(uuid1);
83 uuids.push_back(uuid2);
84 }
85
86 // Right wall (after gap)
87 float right_wall_start = gap_center.x + half_gap;
88 if (right_wall_start < 5.0f) {
89 uint uuid3 = context->addTriangle(make_vec3(right_wall_start, gap_center.y - half_height, wall_distance), make_vec3(5.0f, gap_center.y - half_height, wall_distance), make_vec3(right_wall_start, gap_center.y + half_height, wall_distance));
90 uint uuid4 = context->addTriangle(make_vec3(5.0f, gap_center.y - half_height, wall_distance), make_vec3(5.0f, gap_center.y + half_height, wall_distance), make_vec3(right_wall_start, gap_center.y + half_height, wall_distance));
91 uuids.push_back(uuid3);
92 uuids.push_back(uuid4);
93 }
94
95 return uuids;
96 }
97
105 vec3 direction = gap_center - apex;
106 return direction.normalize();
107 }
108
115 float measureAngularError(vec3 actual, vec3 expected) {
116 // Ensure both vectors are normalized
117 actual = actual.normalize();
118 expected = expected.normalize();
119
120 // Calculate dot product and clamp to valid range for acos
121 float dot = std::max(-1.0f, std::min(1.0f, actual * expected));
122 return acosf(dot);
123 }
124
133 std::vector<uint> createSymmetricTwinGaps(Context *context, float gap_separation, float gap_width, float wall_distance = 3.0f) {
134 std::vector<uint> uuids;
135 float half_separation = gap_separation * 0.5f;
136
137 // Create left gap
138 auto left_wall = createParallelWallsWithGap(context, make_vec3(-half_separation, 0, 0), gap_width, 2.0f, wall_distance);
139 uuids.insert(uuids.end(), left_wall.begin(), left_wall.end());
140
141 // Create right gap
142 auto right_wall = createParallelWallsWithGap(context, make_vec3(half_separation, 0, 0), gap_width, 2.0f, wall_distance);
143 uuids.insert(uuids.end(), right_wall.begin(), right_wall.end());
144
145 return uuids;
146 }
147
148} // namespace CollisionTests
149
150DOCTEST_TEST_CASE("CollisionDetection Plugin Initialization") {
151 Context context;
152 CollisionDetection collision(&context);
153 collision.disableMessages();
154
155 // Test basic initialization
156 DOCTEST_CHECK_NOTHROW(collision.disableMessages());
157 DOCTEST_CHECK_NOTHROW(collision.enableMessages());
158 collision.disableMessages(); // Re-disable to suppress subsequent output
159
160 // Test GPU acceleration capabilities based on actual hardware availability
161 try {
162 // Try to enable GPU acceleration and see if it actually works (capture any warnings)
163 bool gpu_enabled;
164 {
165 helios::capture_cerr capture;
166 collision.enableGPUAcceleration();
167 gpu_enabled = collision.isGPUAccelerationEnabled();
168 } // Capture destroyed before assertions
169
170 // Create minimal test geometry to verify GPU functionality
171 uint test_uuid = context.addTriangle(make_vec3(-1, -1, 0), make_vec3(1, -1, 0), make_vec3(0, 1, 0));
172 collision.buildBVH();
173
174 // If we reach here without exception, GPU may be available
175 DOCTEST_INFO("GPU acceleration capability test - actual hardware dependent");
176 if (gpu_enabled) {
177 DOCTEST_WARN("GPU acceleration is available and enabled on this system");
178 } else {
179 DOCTEST_WARN("GPU acceleration requested but not available - using CPU fallback");
180 }
181
182 // Test that we can successfully disable GPU acceleration
183 DOCTEST_CHECK_NOTHROW(collision.disableGPUAcceleration());
184 DOCTEST_CHECK(collision.isGPUAccelerationEnabled() == false);
185
186 } catch (std::exception &e) {
187 // GPU initialization failure is acceptable - GPU may not be available
188 DOCTEST_WARN((std::string("GPU acceleration test failed (expected on non-NVIDIA systems): ") + e.what()).c_str());
189
190 // Ensure CPU mode works regardless
191 DOCTEST_CHECK_NOTHROW(collision.disableGPUAcceleration());
192 DOCTEST_CHECK(collision.isGPUAccelerationEnabled() == false);
193 }
194}
195
196
197DOCTEST_TEST_CASE("CollisionDetection BVH Construction") {
198 Context context;
199
200 // Suppress all initialization and BVH building messages
201 CollisionDetection collision(&context);
202 collision.disableMessages();
203
204 // Create some simple triangles
205 uint UUID1 = context.addTriangle(make_vec3(-1, -1, 0), make_vec3(1, -1, 0), make_vec3(0, 1, 0));
206 uint UUID2 = context.addTriangle(make_vec3(2, -1, 0), make_vec3(4, -1, 0), make_vec3(3, 1, 0));
207 uint UUID3 = context.addTriangle(make_vec3(-0.5, -0.5, 1), make_vec3(1.5, -0.5, 1), make_vec3(0.5, 1.5, 1));
208
209 // Build BVH
210 collision.buildBVH();
211
212 DOCTEST_CHECK(collision.isBVHValid() == true);
213 DOCTEST_CHECK(collision.getPrimitiveCount() == 3);
214}
215
216
217DOCTEST_TEST_CASE("CollisionDetection Basic Collision Detection") {
218 Context context;
219
220 // Suppress all initialization and BVH building messages
221 CollisionDetection collision(&context);
222 collision.disableMessages();
223 collision.disableGPUAcceleration(); // Use CPU for deterministic results
224
225 // Create overlapping triangles
226 uint UUID1 = context.addTriangle(make_vec3(-1, -1, 0), make_vec3(1, -1, 0), make_vec3(0, 1, 0));
227 uint UUID2 = context.addTriangle(make_vec3(-0.5, -0.5, 0), make_vec3(1.5, -0.5, 0), make_vec3(0.5, 1.5, 0));
228
229 // Create non-overlapping triangle
230 uint UUID3 = context.addTriangle(make_vec3(10, -1, 0), make_vec3(11, -1, 0), make_vec3(10.5, 1, 0));
231
232 collision.buildBVH();
233
234 // Test collision between overlapping triangles
235 std::vector<uint> collisions1 = collision.findCollisions(UUID1);
236
237 // UUID1 should collide with UUID2 but not UUID3
238 bool found_UUID2 = std::find(collisions1.begin(), collisions1.end(), UUID2) != collisions1.end();
239 bool found_UUID3 = std::find(collisions1.begin(), collisions1.end(), UUID3) != collisions1.end();
240
241 DOCTEST_CHECK(found_UUID2 == true);
242 DOCTEST_CHECK(found_UUID3 == false);
243}
244
245
246DOCTEST_TEST_CASE("CollisionDetection BVH Statistics") {
247 Context context;
248
249 // Suppress all initialization and BVH building messages
250 CollisionDetection collision(&context);
251 collision.disableMessages();
252
253 // Create a larger set of primitives
254 for (int i = 0; i < 20; i++) {
255 context.addTriangle(make_vec3(i, -1, 0), make_vec3(i + 1, -1, 0), make_vec3(i + 0.5f, 1, 0));
256 }
257
258 collision.buildBVH();
259
260 size_t node_count, leaf_count, max_depth;
261 collision.getBVHStatistics(node_count, leaf_count, max_depth);
262
263 DOCTEST_CHECK(node_count > 0);
264 DOCTEST_CHECK(leaf_count > 0);
265 // Small geometry sets may result in single leaf (depth 0) which is valid optimization
266 DOCTEST_CHECK(max_depth >= 0);
267}
268
269
270DOCTEST_TEST_CASE("CollisionDetection Empty Geometry Handling") {
271 Context context;
272
273 // Suppress all initialization and BVH building messages
274 CollisionDetection collision(&context);
275 collision.disableMessages();
276
277 // Try to build BVH with no primitives
278 collision.buildBVH();
279
280 // Should handle gracefully
281 DOCTEST_CHECK(collision.getPrimitiveCount() == 0);
282
283 // Try collision detection with empty BVH
284 std::vector<uint> collisions = collision.findCollisions(std::vector<uint>{});
285
286 DOCTEST_CHECK(collisions.empty() == true);
287}
288
289
290DOCTEST_TEST_CASE("CollisionDetection Invalid UUID Handling") {
291 Context context;
292
293 // Suppress all initialization and BVH building messages
294 CollisionDetection collision(&context);
295 collision.disableMessages();
296
297 uint UUID1 = context.addTriangle(make_vec3(-1, -1, 0), make_vec3(1, -1, 0), make_vec3(0, 1, 0));
298
299 // Try collision detection with invalid UUID - should throw std::runtime_error
300 DOCTEST_CHECK_THROWS_AS(collision.findCollisions(999999), std::runtime_error);
301
302 // Also verify the exception message contains relevant information
303 try {
304 collision.findCollisions(999999);
305 DOCTEST_FAIL("Expected exception was not thrown");
306 } catch (const std::runtime_error &e) {
307 std::string error_msg = e.what();
308 bool has_relevant_content = error_msg.find("UUID") != std::string::npos || error_msg.find("invalid") != std::string::npos;
309 DOCTEST_CHECK(has_relevant_content);
310 }
311}
312
313
314DOCTEST_TEST_CASE("CollisionDetection GPU/CPU Mode Switching") {
315 Context context;
316
317 // Suppress all initialization and BVH building messages
318 CollisionDetection collision(&context);
319 collision.disableMessages();
320
321 uint UUID1 = context.addTriangle(make_vec3(-1, -1, 0), make_vec3(1, -1, 0), make_vec3(0, 1, 0));
322 uint UUID2 = context.addTriangle(make_vec3(-0.5, -0.5, 0), make_vec3(1.5, -0.5, 0), make_vec3(0.5, 1.5, 0));
323
324 collision.buildBVH();
325
326 // Test with GPU enabled (capture any warnings)
327 std::vector<uint> gpu_results;
328 {
329 helios::capture_cerr capture;
330 collision.enableGPUAcceleration();
331 gpu_results = collision.findCollisions(UUID1);
332 }
333
334 // Test with GPU disabled
335 collision.disableGPUAcceleration();
336 std::vector<uint> cpu_results = collision.findCollisions(UUID1);
337
338 // Results should be equivalent (though may be in different order)
339 std::sort(gpu_results.begin(), gpu_results.end());
340 std::sort(cpu_results.begin(), cpu_results.end());
341
342 DOCTEST_CHECK(gpu_results == cpu_results);
343}
344
345
346DOCTEST_TEST_CASE("CollisionDetection Null Context Error Handling") {
347 // Should throw std::runtime_error when context is null
348 DOCTEST_CHECK_THROWS_AS(CollisionDetection collision(nullptr), std::runtime_error);
349
350 // Also verify the exception message contains relevant information
351 try {
352 CollisionDetection collision(nullptr);
353 DOCTEST_FAIL("Expected exception was not thrown");
354 } catch (const std::runtime_error &e) {
355 std::string error_msg = e.what();
356 bool has_relevant_content = error_msg.find("context") != std::string::npos || error_msg.find("Context") != std::string::npos || error_msg.find("null") != std::string::npos;
357 DOCTEST_CHECK(has_relevant_content);
358 }
359}
360
361
362DOCTEST_TEST_CASE("CollisionDetection Invalid UUIDs in BuildBVH") {
363 Context context;
364
365 // Suppress all initialization messages
366 CollisionDetection collision(&context);
367 collision.disableMessages();
368
369 // Try to build BVH with invalid UUIDs - should throw std::runtime_error
370 std::vector<uint> invalid_UUIDs = {999999, 888888};
371
372 DOCTEST_CHECK_THROWS_AS(collision.buildBVH(invalid_UUIDs), std::runtime_error);
373
374 // Also verify the exception message contains relevant information
375 try {
376 collision.buildBVH(invalid_UUIDs);
377 DOCTEST_FAIL("Expected exception was not thrown");
378 } catch (const std::runtime_error &e) {
379 std::string error_msg = e.what();
380 bool has_relevant_content = error_msg.find("UUID") != std::string::npos || error_msg.find("invalid") != std::string::npos;
381 DOCTEST_CHECK(has_relevant_content);
382 }
383}
384
385
386DOCTEST_TEST_CASE("CollisionDetection Primitive/Object Collision Detection") {
387 Context context;
388
389 // Suppress all initialization and BVH building messages
390 CollisionDetection collision(&context);
391 collision.disableMessages();
392 collision.disableGPUAcceleration();
393
394 // Create some primitives
395 uint UUID1 = context.addTriangle(make_vec3(-1, -1, 0), make_vec3(1, -1, 0), make_vec3(0, 1, 0));
396 uint UUID2 = context.addTriangle(make_vec3(-0.5, -0.5, 0), make_vec3(1.5, -0.5, 0), make_vec3(0.5, 1.5, 0));
397
398 // Create a compound object
399 uint objID = context.addTileObject(make_vec3(0, 0, 1), make_vec2(2, 2), make_SphericalCoord(0, 0), make_int2(1, 1));
400
401 collision.buildBVH();
402
403 // Test mixed primitive/object collision detection
404 std::vector<uint> primitive_UUIDs = {UUID1};
405 std::vector<uint> object_IDs = {objID};
406
407 DOCTEST_CHECK_NOTHROW(collision.findCollisions(primitive_UUIDs, object_IDs));
408}
409
410
411DOCTEST_TEST_CASE("CollisionDetection Empty Input Handling") {
412 Context context;
413
414 // Suppress all initialization messages
415 CollisionDetection collision(&context);
416 collision.disableMessages();
417
418 // Test empty primitive vector
419 std::vector<uint> empty_primitives;
420 std::vector<uint> collisions1 = collision.findCollisions(empty_primitives);
421
422 // Test empty mixed input
423 std::vector<uint> empty_objects;
424 std::vector<uint> collisions2 = collision.findCollisions(empty_primitives, empty_objects);
425
426 DOCTEST_CHECK(collisions1.empty() == true);
427 DOCTEST_CHECK(collisions2.empty() == true);
428}
429
430
431DOCTEST_TEST_CASE("CollisionDetection Invalid Object ID Error Handling") {
432 Context context;
433
434 // Suppress all initialization messages
435 CollisionDetection collision(&context);
436 collision.disableMessages();
437
438 // Try collision detection with invalid object ID - should throw std::runtime_error
439 std::vector<uint> empty_primitives;
440 std::vector<uint> invalid_objects = {999999};
441
442 DOCTEST_CHECK_THROWS_AS(collision.findCollisions(empty_primitives, invalid_objects), std::runtime_error);
443
444 // Also verify the exception message contains relevant information
445 try {
446 collision.findCollisions(empty_primitives, invalid_objects);
447 DOCTEST_FAIL("Expected exception was not thrown");
448 } catch (const std::runtime_error &e) {
449 std::string error_msg = e.what();
450 bool has_relevant_content = error_msg.find("object") != std::string::npos || error_msg.find("Object") != std::string::npos || error_msg.find("999999") != std::string::npos || error_msg.find("exist") != std::string::npos ||
451 error_msg.find("invalid") != std::string::npos;
452 DOCTEST_CHECK(has_relevant_content);
453 }
454}
455
456
457DOCTEST_TEST_CASE("CollisionDetection Manual BVH Rebuild") {
458 Context context;
459
460 // Suppress all initialization and BVH building messages
461 CollisionDetection collision(&context);
462 collision.disableMessages();
463
464 // Create initial geometry
465 uint UUID1 = context.addTriangle(make_vec3(-1, -1, 0), make_vec3(1, -1, 0), make_vec3(0, 1, 0));
466 collision.buildBVH();
467
468 size_t initial_count = collision.getPrimitiveCount();
469
470 // Add more geometry
471 uint UUID2 = context.addTriangle(make_vec3(2, -1, 0), make_vec3(4, -1, 0), make_vec3(3, 1, 0));
472
473 // Force rebuild
474 collision.rebuildBVH();
475
476 size_t final_count = collision.getPrimitiveCount();
477
478 DOCTEST_CHECK(final_count == 2);
479}
480
481
482DOCTEST_TEST_CASE("CollisionDetection Message Control") {
483 Context context;
484
485 // Suppress initialization messages (we're testing message control itself)
486 CollisionDetection collision(&context);
487 collision.disableMessages();
488
489 // Test message disabling/enabling
490 DOCTEST_CHECK_NOTHROW(collision.disableMessages());
491 DOCTEST_CHECK_NOTHROW(collision.enableMessages());
492 collision.disableMessages(); // Re-disable to suppress subsequent output
493}
494
495
496DOCTEST_TEST_CASE("CollisionDetection Large Geometry Handling") {
497 Context context;
498
499 // Suppress all initialization and BVH building messages
500 CollisionDetection collision(&context);
501 collision.disableMessages();
502 collision.disableGPUAcceleration();
503
504 // Create many primitives to stress test BVH
505 for (int i = 0; i < 50; i++) {
506 context.addTriangle(make_vec3(i, -1, 0), make_vec3(i + 1, -1, 0), make_vec3(i + 0.5f, 1, 0));
507 }
508
509 collision.buildBVH();
510
511 // Test collision detection with large BVH
512 uint UUID = context.getAllUUIDs()[0];
513 std::vector<uint> collisions = collision.findCollisions(UUID);
514
515 // Verify BVH statistics make sense
516 size_t node_count, leaf_count, max_depth;
517 collision.getBVHStatistics(node_count, leaf_count, max_depth);
518
519 DOCTEST_CHECK(collision.getPrimitiveCount() == 50);
520 DOCTEST_CHECK(node_count > 0);
521 // Small geometry sets may result in single leaf (depth 0) which is valid optimization
522 DOCTEST_CHECK(max_depth >= 0);
523}
524
525
526DOCTEST_TEST_CASE("CollisionDetection Single Primitive Edge Case") {
527 Context context;
528
529 // Suppress all initialization and BVH building messages
530 CollisionDetection collision(&context);
531 collision.disableMessages();
532 collision.disableGPUAcceleration();
533
534 // Create single primitive
535 uint UUID1 = context.addTriangle(make_vec3(-1, -1, 0), make_vec3(1, -1, 0), make_vec3(0, 1, 0));
536
537 collision.buildBVH();
538
539 // Test collision with itself (should return empty since self is removed)
540 std::vector<uint> collisions = collision.findCollisions(UUID1);
541
542 // Verify BVH handles single primitive correctly
543 size_t node_count, leaf_count, max_depth;
544 collision.getBVHStatistics(node_count, leaf_count, max_depth);
545
546 DOCTEST_CHECK(collision.getPrimitiveCount() == 1);
547 DOCTEST_CHECK(collision.isBVHValid() == true);
548}
549
550
551DOCTEST_TEST_CASE("CollisionDetection Overlapping AABB Primitives") {
552 Context context;
553
554 // Suppress all initialization and BVH building messages
555 CollisionDetection collision(&context);
556 collision.disableMessages();
557 collision.disableGPUAcceleration();
558
559 // Create primitives with overlapping AABBs
560 // These triangles are tilted so their AABBs will overlap in Z
561 uint UUID1 = context.addTriangle(make_vec3(-1, -1, 0), make_vec3(1, -1, 0), make_vec3(0, 1, 0.2f));
562 uint UUID2 = context.addTriangle(make_vec3(-1, -1, 0.1f), make_vec3(1, -1, 0.1f), make_vec3(0, 1, -0.1f));
563
564 collision.buildBVH();
565
566 // These should collide (overlapping AABBs)
567 std::vector<uint> collisions = collision.findCollisions(UUID1);
568
569 bool found_collision = std::find(collisions.begin(), collisions.end(), UUID2) != collisions.end();
570
571 DOCTEST_CHECK(found_collision == true);
572}
573
574
575DOCTEST_TEST_CASE("CollisionDetection BVH Validity Persistence") {
576 Context context;
577
578 // Suppress all initialization and BVH building messages
579 CollisionDetection collision(&context);
580 collision.disableMessages();
581
582 // Initially invalid (no BVH built)
583 DOCTEST_CHECK(collision.isBVHValid() == false);
584
585 // Create geometry and build
586 uint UUID1 = context.addTriangle(make_vec3(-1, -1, 0), make_vec3(1, -1, 0), make_vec3(0, 1, 0));
587 collision.buildBVH();
588
589 // Should be valid after building
590 DOCTEST_CHECK(collision.isBVHValid() == true);
591}
592
593
594DOCTEST_TEST_CASE("CollisionDetection Soft/Hard Detection Integration - BVH Sharing") {
595 Context context;
596
597 CollisionDetection collision(&context);
598 collision.disableMessages();
599 collision.disableGPUAcceleration();
600
601 // Create test geometry - obstacle and some primitives for "soft" collision testing
602 uint obstacle = context.addPatch(make_vec3(0, 0, 1), make_vec2(2, 2)); // Obstacle at height 1m
603 uint soft_prim1 = context.addTriangle(make_vec3(-0.5, -0.5, 0.5), make_vec3(0.5, -0.5, 0.5), make_vec3(0, 0.5, 0.5)); // Below obstacle
604 uint soft_prim2 = context.addTriangle(make_vec3(-1.5, -1.5, 1.5), make_vec3(-0.5, -1.5, 1.5), make_vec3(-1, -0.5, 1.5)); // Above obstacle
605
606 // Build BVH with all geometry for initial soft collision detection
607 std::vector<uint> all_geometry = {obstacle, soft_prim1, soft_prim2};
608 collision.buildBVH(all_geometry);
609
610 // Verify initial BVH state
611 DOCTEST_CHECK(collision.isBVHValid() == true);
612 size_t initial_node_count, initial_leaf_count, initial_max_depth;
613 collision.getBVHStatistics(initial_node_count, initial_leaf_count, initial_max_depth);
614
615 // Simulate soft collision detection (standard findCollisions)
616 std::vector<uint> soft_collisions = collision.findCollisions({soft_prim1, soft_prim2});
617 bool soft_detection_completed = true; // Mark that soft detection has run
618
619 // Now test hard detection using the same BVH
620 vec3 test_origin = make_vec3(0, 0, 0.5);
621 vec3 test_direction = make_vec3(0, 0, 1); // Pointing up toward obstacle
622 float distance;
623 vec3 obstacle_direction;
624
625 // This should use the SAME BVH that was built for soft detection
626 bool hard_hit = collision.findNearestSolidObstacleInCone(test_origin, test_direction, 0.52f, 1.0f, {obstacle}, distance, obstacle_direction);
627
628 // Verify both detections work correctly despite sharing BVH
629 DOCTEST_CHECK(soft_detection_completed == true);
630 DOCTEST_CHECK(hard_hit == true);
631 DOCTEST_CHECK(distance < 1.0f); // Should detect obstacle before 1m
632
633 // Verify BVH state wasn't corrupted by interleaved usage
634 size_t final_node_count, final_leaf_count, final_max_depth;
635 collision.getBVHStatistics(final_node_count, final_leaf_count, final_max_depth);
636
637 DOCTEST_CHECK(initial_node_count == final_node_count);
638 DOCTEST_CHECK(initial_leaf_count == final_leaf_count);
639 DOCTEST_CHECK(collision.isBVHValid() == true);
640}
641
642
643DOCTEST_TEST_CASE("CollisionDetection Soft/Hard Detection Integration - Sequential Calls") {
644 Context context;
645
646 CollisionDetection collision(&context);
647 collision.disableMessages(); // Suppress console output during testing
648 collision.disableGPUAcceleration();
649
650 // Create a more complex scene
651 uint ground = context.addPatch(make_vec3(0, 0, 0), make_vec2(4, 4));
652 uint wall = context.addPatch(make_vec3(1, 0, 0.5), make_vec2(0.1, 2), make_SphericalCoord(0.5 * M_PI, 0.5 * M_PI)); // Vertical wall
653 uint plant_stem = context.addTriangle(make_vec3(-0.02, 0, 0), make_vec3(0.02, 0, 0), make_vec3(0, 0, 0.8));
654
655 std::vector<uint> all_obstacles = {ground, wall};
656 std::vector<uint> plant_parts = {plant_stem};
657
658 collision.buildBVH(all_obstacles);
659
660 // Test 1: Soft collision detection between plant and obstacles
661 std::vector<uint> soft_collisions = collision.findCollisions(plant_parts, {}, all_obstacles, {});
662
663 // Test 2: Hard detection for plant growth (cone-based)
664 vec3 growth_tip = make_vec3(0, 0, 0.8);
665 vec3 growth_direction = make_vec3(0.5, 0, 0.2); // Angled toward wall
666 growth_direction.normalize();
667
668
669 float distance;
670 vec3 obstacle_direction;
671 bool hard_hit = collision.findNearestSolidObstacleInCone(growth_tip, growth_direction, 0.35f, 2.0f, // Increased height to 2.0m
672 all_obstacles, distance, obstacle_direction);
673
674 // Test 3: Repeat soft detection to ensure no state corruption
675 std::vector<uint> soft_collisions_2 = collision.findCollisions(plant_parts, {}, all_obstacles, {});
676
677 // Test 4: Repeat hard detection with different parameters
678 vec3 growth_direction_2 = make_vec3(-0.3, 0, 0.3);
679 growth_direction_2.normalize();
680
681 bool hard_hit_2 = collision.findNearestSolidObstacleInCone(growth_tip, growth_direction_2, 0.35f, 0.5f, all_obstacles, distance, obstacle_direction);
682
683 // Verify consistency - repeated calls should give same results
684 DOCTEST_CHECK(soft_collisions.size() == soft_collisions_2.size());
685 DOCTEST_CHECK(hard_hit == true); // Should detect wall
686 DOCTEST_CHECK(collision.isBVHValid() == true);
687}
688
689
690DOCTEST_TEST_CASE("CollisionDetection Soft/Hard Detection Integration - Different Geometry Sets") {
691 Context context;
692
693 CollisionDetection collision(&context);
694 collision.disableMessages();
695 collision.disableGPUAcceleration();
696
697 // Create separate geometry sets for soft and hard detection
698 uint hard_obstacle_1 = context.addPatch(make_vec3(1, 0, 1), make_vec2(1, 1)); // For hard detection only
699 uint hard_obstacle_2 = context.addPatch(make_vec3(-1, 0, 1), make_vec2(1, 1)); // For hard detection only
700
701 uint soft_object_1 = context.addTriangle(make_vec3(0, 1, 1.0), make_vec3(0.5, 1.5, 1.0), make_vec3(-0.5, 1.5, 1.0)); // For soft detection only
702 uint soft_object_2 = context.addTriangle(make_vec3(0, -1, 0.5), make_vec3(0.5, -1.5, 0.5), make_vec3(-0.5, -1.5, 0.5)); // For soft detection only
703
704 uint shared_object = context.addPatch(make_vec3(0, 0, 2), make_vec2(0.5, 0.5)); // Used by both detection types
705
706 // Build BVH with ALL geometry (this is typical in plant architecture)
707 std::vector<uint> all_geometry = {hard_obstacle_1, hard_obstacle_2, soft_object_1, soft_object_2, shared_object};
708 collision.buildBVH(all_geometry);
709
710 // Test hard detection using only hard obstacles
711 vec3 test_origin = make_vec3(0, 0, 0.5);
712 vec3 test_direction = make_vec3(1, 0, 0.3); // Toward hard_obstacle_1
713 test_direction.normalize();
714
715 std::vector<uint> hard_only = {hard_obstacle_1, hard_obstacle_2, shared_object};
716 float distance;
717 vec3 obstacle_direction;
718
719 bool hard_hit = collision.findNearestSolidObstacleInCone(test_origin, test_direction, 0.4f, 2.0f, hard_only, distance, obstacle_direction);
720
721 // Test soft detection using only soft objects
722 std::vector<uint> soft_only = {soft_object_1, soft_object_2, shared_object};
723 std::vector<uint> soft_collisions = collision.findCollisions(soft_only);
724
725 // Test with mixed queries to ensure BVH handles subset filtering correctly
726 vec3 test_direction_2 = make_vec3(0, 1, 0.3); // Toward soft_object_1
727 test_direction_2.normalize();
728
729 bool hard_hit_2 = collision.findNearestSolidObstacleInCone(test_origin, test_direction_2, 0.4f, 10.0f, // Very generous height for detection
730 soft_only, distance, obstacle_direction); // Using soft objects for hard detection
731
732 // Verify that detection works correctly with different geometry subsets
733 DOCTEST_CHECK(hard_hit == true); // Should detect hard obstacle
734 DOCTEST_CHECK(hard_hit_2 == true); // Should also detect soft object when used as hard obstacle
735 DOCTEST_CHECK(collision.isBVHValid() == true);
736
737 // Verify BVH efficiency - primitive count should match total geometry
738 DOCTEST_CHECK(collision.getPrimitiveCount() == all_geometry.size());
739}
740
741
742DOCTEST_TEST_CASE("CollisionDetection Soft/Hard Detection Integration - BVH Rebuild Behavior") {
743 Context context;
744
745 CollisionDetection collision(&context);
746 collision.disableMessages();
747 collision.disableGPUAcceleration();
748
749 // Initial geometry
750 uint obstacle1 = context.addPatch(make_vec3(0, 0, 1), make_vec2(1, 1));
751 std::vector<uint> initial_geometry = {obstacle1};
752
753 collision.buildBVH(initial_geometry);
754
755 // Test initial state
756 vec3 test_origin = make_vec3(0, 0, 0.5);
757 vec3 test_direction = make_vec3(0, 0, 1);
758 float distance;
759 vec3 obstacle_direction;
760
761 bool hit_initial = collision.findNearestSolidObstacleInCone(test_origin, test_direction, 0.3f, 1.0f, initial_geometry, distance, obstacle_direction);
762
763 // Add more geometry (this might trigger BVH rebuild internally)
764 uint obstacle2 = context.addPatch(make_vec3(1, 1, 1), make_vec2(1, 1));
765 uint obstacle3 = context.addPatch(make_vec3(-1, -1, 1), make_vec2(1, 1));
766
767 std::vector<uint> expanded_geometry = {obstacle1, obstacle2, obstacle3};
768
769 // This should trigger a BVH rebuild
770 collision.buildBVH(expanded_geometry);
771
772 // Test that both detection methods still work after rebuild
773 std::vector<uint> soft_collisions = collision.findCollisions(expanded_geometry);
774
775 bool hit_after_rebuild = collision.findNearestSolidObstacleInCone(test_origin, test_direction, 0.3f, 1.0f, expanded_geometry, distance, obstacle_direction);
776
777 // Test detection with different geometry subset after rebuild
778 vec3 test_direction_2 = make_vec3(1, 1, 0.2);
779 test_direction_2.normalize();
780
781 bool hit_subset = collision.findNearestSolidObstacleInCone(test_origin, test_direction_2, 0.5f, 2.0f, {obstacle2}, distance, obstacle_direction); // Only test against obstacle2
782
783 // Verify all detection methods work correctly after BVH rebuild
784 DOCTEST_CHECK(hit_initial == true);
785 DOCTEST_CHECK(hit_after_rebuild == true);
786 DOCTEST_CHECK(hit_subset == true);
787 DOCTEST_CHECK(collision.isBVHValid() == true);
788 DOCTEST_CHECK(collision.getPrimitiveCount() == expanded_geometry.size());
789}
790
791
792DOCTEST_TEST_CASE("CollisionDetection GPU Acceleration") {
793 Context context;
794
795 // Suppress all initialization and BVH building messages
796 CollisionDetection collision(&context);
797 collision.disableMessages();
798
799 // Create test geometry
800 for (int i = 0; i < 5; i++) {
801 context.addTriangle(make_vec3(i, -1, 0), make_vec3(i + 1, -1, 0), make_vec3(i + 0.5f, 1, 0));
802 }
803
804 // Test GPU vs CPU equivalence (if GPU available)
805 try {
806 collision.disableGPUAcceleration();
807 collision.buildBVH();
808 uint UUID = context.getAllUUIDs()[0];
809 std::vector<uint> cpu_results = collision.findCollisions(UUID);
810
811 std::vector<uint> gpu_results;
812 {
813 helios::capture_cerr capture;
814 collision.enableGPUAcceleration();
815 collision.buildBVH(); // This should transfer to GPU
816 gpu_results = collision.findCollisions(UUID);
817 } // Capture destroyed before assertions
818
819 // Compare results (allowing for different orders)
820 std::sort(cpu_results.begin(), cpu_results.end());
821 std::sort(gpu_results.begin(), gpu_results.end());
822
823 // Check if results match (GPU may not be available, so we use INFO instead of CHECK)
824 DOCTEST_INFO("GPU/CPU result comparison - GPU may not be available on this system");
825 if (cpu_results.size() == gpu_results.size()) {
826 bool results_match = true;
827 for (size_t i = 0; i < cpu_results.size(); i++) {
828 if (cpu_results[i] != gpu_results[i]) {
829 results_match = false;
830 break;
831 }
832 }
833 // Only warn if results don't match - this is acceptable if no GPU
834 if (!results_match) {
835 DOCTEST_WARN("GPU/CPU results differ - may be expected if no CUDA device");
836 }
837 }
838 } catch (std::exception &e) {
839 // GPU test failure is acceptable - GPU may not be available
840 DOCTEST_WARN((std::string("GPU test failed (may be expected): ") + e.what()).c_str());
841 }
842}
843
844
845DOCTEST_TEST_CASE("CollisionDetection GPU/CPU Message Display") {
846 Context context;
847
848 // Suppress initialization message, then create collision detection object and test geometry
849 CollisionDetection collision(&context);
850 collision.disableMessages();
851
852 uint UUID1 = context.addTriangle(make_vec3(-1, -1, 0), make_vec3(1, -1, 0), make_vec3(0, 1, 0));
853 uint UUID2 = context.addTriangle(make_vec3(-0.5, -0.5, 0), make_vec3(1.5, -0.5, 0), make_vec3(0.5, 1.5, 0));
854 collision.buildBVH(); // Build once to avoid BVH construction messages later
855
856 // Test that messages are displayed when enabled
857 collision.enableMessages();
858 collision.disableGPUAcceleration();
859
860 // Capture traversal messages using capture_cout (these are the specific messages we want to test)
861 std::string cpu_output;
862 std::vector<uint> cpu_results;
863 {
864 helios::capture_cout capture_cpu;
865 cpu_results = collision.findCollisions(UUID1);
866 cpu_output = capture_cpu.get_captured_output();
867 } // capture destroyed here
868
869 // Check that some CPU message was displayed (the exact message may vary)
870 // DOCTEST_INFO("CPU output: " << cpu_output);
871 // For now, just check that collision detection worked (test is mainly about message suppression)
872 DOCTEST_CHECK(true); // Placeholder - the main goal is testing message suppression below
873
874 // Test GPU message (if available)
875 std::string gpu_output;
876 std::vector<uint> gpu_results;
877 {
878 helios::capture_cerr capture_err;
879 helios::capture_cout capture_gpu;
880 collision.enableGPUAcceleration();
881 gpu_results = collision.findCollisions(UUID1);
882 gpu_output = capture_gpu.get_captured_output();
883 } // Capture destroyed before assertions
884
885 // Should contain either GPU or CPU message depending on availability
886 // For now, just check basic functionality (the main test is message suppression below)
887 DOCTEST_CHECK(true); // Placeholder - the main goal is testing message suppression below
888
889 // Test message suppression
890 collision.disableMessages();
891
892 std::string silent_output;
893 std::vector<uint> silent_results;
894 {
895 helios::capture_cout capture_silent;
896 silent_results = collision.findCollisions(UUID1);
897 silent_output = capture_silent.get_captured_output();
898 } // capture destroyed here
899
900 // Should not contain traversal messages when disabled
901 DOCTEST_CHECK(silent_output.find("Using GPU acceleration") == std::string::npos);
902 DOCTEST_CHECK(silent_output.find("Using CPU traversal") == std::string::npos);
903}
904
905
906DOCTEST_TEST_CASE("CollisionDetection Automatic BVH Building") {
907 Context context;
908
909 // Suppress all initialization and automatic BVH building messages
910 CollisionDetection collision(&context);
911 collision.disableMessages();
912 collision.disableGPUAcceleration(); // Use CPU for predictable behavior
913
914 // Create initial geometry
915 uint UUID1 = context.addTriangle(make_vec3(-1, -1, 0), make_vec3(1, -1, 0), make_vec3(0, 1, 0));
916
917 // BVH should be invalid initially (not built)
918 DOCTEST_CHECK(collision.isBVHValid() == false);
919
920 // Calling findCollisions should automatically build BVH
921 std::vector<uint> results = collision.findCollisions(UUID1);
922
923 // BVH should now be valid
924 DOCTEST_CHECK(collision.isBVHValid() == true);
925 DOCTEST_CHECK(collision.getPrimitiveCount() == 1);
926
927 // Add more geometry and mark it dirty in context
928 uint UUID2 = context.addTriangle(make_vec3(2, -1, 0), make_vec3(4, -1, 0), make_vec3(3, 1, 0));
929
930 // BVH should now be invalid because there's a new primitive not in the BVH
931 DOCTEST_CHECK(collision.isBVHValid() == false);
932
933 // But calling findCollisions should detect the new geometry and rebuild
934 results = collision.findCollisions(UUID1);
935
936 // BVH should now include both primitives
937 DOCTEST_CHECK(collision.getPrimitiveCount() == 2);
938 DOCTEST_CHECK(collision.isBVHValid() == true);
939
940 // Test that repeated calls don't unnecessarily rebuild
941 size_t count_before = collision.getPrimitiveCount();
942 results = collision.findCollisions(UUID1);
943 size_t count_after = collision.getPrimitiveCount();
944
945 // Should be the same (no unnecessary rebuild)
946 DOCTEST_CHECK(count_before == count_after);
947}
948
949
950DOCTEST_TEST_CASE("CollisionDetection Restricted Geometry - UUIDs Only") {
951 Context context;
952
953 // Suppress all initialization and BVH building messages
954 CollisionDetection collision(&context);
955 collision.disableMessages();
956 collision.disableGPUAcceleration();
957
958 // Create test geometry: 3 overlapping triangles
959 uint UUID1 = context.addTriangle(make_vec3(-1, -1, 0), make_vec3(1, -1, 0), make_vec3(0, 1, 0));
960 uint UUID2 = context.addTriangle(make_vec3(-0.5, -0.5, 0), make_vec3(1.5, -0.5, 0), make_vec3(0.5, 1.5, 0));
961 // UUID3 overlaps in z-dimension to ensure AABB collision
962 uint UUID3 = context.addTriangle(make_vec3(-0.5, -0.5, -0.1f), make_vec3(1.5, -0.5, -0.1f), make_vec3(0.5, 1.5, 0.1f));
963
964 // Test unrestricted collision detection (should find all collisions)
965 std::vector<uint> all_results = collision.findCollisions(UUID1);
966
967 // UUID1 should collide with both UUID2 and UUID3
968 bool found_UUID2_all = std::find(all_results.begin(), all_results.end(), UUID2) != all_results.end();
969 bool found_UUID3_all = std::find(all_results.begin(), all_results.end(), UUID3) != all_results.end();
970
971 DOCTEST_CHECK(found_UUID2_all == true);
972 DOCTEST_CHECK(found_UUID3_all == true);
973
974 // Test restricted collision detection - only target UUID2
975 std::vector<uint> query_UUIDs = {UUID1};
976 std::vector<uint> query_objects = {};
977 std::vector<uint> target_UUIDs = {UUID2}; // Only target UUID2
978 std::vector<uint> target_objects = {};
979
980 std::vector<uint> restricted_results = collision.findCollisions(query_UUIDs, query_objects, target_UUIDs, target_objects);
981
982 // Should only find collision with UUID2, not UUID3
983 bool found_UUID2_restricted = std::find(restricted_results.begin(), restricted_results.end(), UUID2) != restricted_results.end();
984 bool found_UUID3_restricted = std::find(restricted_results.begin(), restricted_results.end(), UUID3) != restricted_results.end();
985
986 DOCTEST_CHECK(found_UUID2_restricted == true);
987 DOCTEST_CHECK(found_UUID3_restricted == false);
988}
989
990
991DOCTEST_TEST_CASE("CollisionDetection Restricted Geometry - Object IDs") {
992 Context context;
993
994 // Suppress all initialization and BVH building messages
995 CollisionDetection collision(&context);
996 collision.disableMessages();
997 collision.disableGPUAcceleration();
998
999 // Create individual primitives
1000 uint UUID1 = context.addTriangle(make_vec3(-1, -1, 0), make_vec3(1, -1, 0), make_vec3(0, 1, 0));
1001
1002 // Create compound objects
1003 uint objID1 = context.addTileObject(make_vec3(0, 0, 0.5f), make_vec2(2, 2), make_SphericalCoord(0, 0), make_int2(1, 1));
1004 uint objID2 = context.addTileObject(make_vec3(10, 0, 0), make_vec2(2, 2), make_SphericalCoord(0, 0), make_int2(1, 1));
1005
1006 // Test collision detection restricted to specific object
1007 std::vector<uint> query_UUIDs = {UUID1};
1008 std::vector<uint> query_objects = {};
1009 std::vector<uint> target_UUIDs = {};
1010 std::vector<uint> target_objects = {objID1}; // Only target objID1
1011
1012 DOCTEST_CHECK_NOTHROW(collision.findCollisions(query_UUIDs, query_objects, target_UUIDs, target_objects));
1013
1014 // Test mixed UUID/Object ID restriction
1015 std::vector<uint> mixed_target_UUIDs = {UUID1};
1016 std::vector<uint> mixed_target_objects = {objID1};
1017
1018 DOCTEST_CHECK_NOTHROW(collision.findCollisions(query_UUIDs, query_objects, mixed_target_UUIDs, mixed_target_objects));
1019}
1020
1021
1022DOCTEST_TEST_CASE("CollisionDetection Restricted Geometry - Error Handling") {
1023 Context context;
1024
1025 // Suppress all initialization and BVH building messages
1026 CollisionDetection collision(&context);
1027 collision.disableMessages();
1028
1029 // Create valid geometry
1030 uint UUID1 = context.addTriangle(make_vec3(-1, -1, 0), make_vec3(1, -1, 0), make_vec3(0, 1, 0));
1031
1032 // Test error handling for invalid query UUIDs - should throw std::runtime_error
1033 std::vector<uint> invalid_query_UUIDs = {999999};
1034 std::vector<uint> query_objects = {};
1035 std::vector<uint> valid_target_UUIDs = {UUID1};
1036 std::vector<uint> target_objects = {};
1037
1038 DOCTEST_CHECK_THROWS_AS(collision.findCollisions(invalid_query_UUIDs, query_objects, valid_target_UUIDs, target_objects), std::runtime_error);
1039
1040 // Test error handling for invalid target UUIDs - should throw std::runtime_error
1041 std::vector<uint> valid_query_UUIDs = {UUID1};
1042 std::vector<uint> invalid_target_UUIDs = {999999};
1043
1044 DOCTEST_CHECK_THROWS_AS(collision.findCollisions(valid_query_UUIDs, query_objects, invalid_target_UUIDs, target_objects), std::runtime_error);
1045
1046 // Test error handling for invalid query object IDs - should throw std::runtime_error
1047 std::vector<uint> invalid_query_objects = {999999};
1048
1049 DOCTEST_CHECK_THROWS_AS(collision.findCollisions(valid_query_UUIDs, invalid_query_objects, valid_target_UUIDs, target_objects), std::runtime_error);
1050
1051 // Test error handling for invalid target object IDs - should throw std::runtime_error
1052 std::vector<uint> invalid_target_objects = {999999};
1053
1054 DOCTEST_CHECK_THROWS_AS(collision.findCollisions(valid_query_UUIDs, query_objects, valid_target_UUIDs, invalid_target_objects), std::runtime_error);
1055
1056 // Additional verification - test that the exception messages are meaningful
1057 try {
1058 collision.findCollisions(invalid_query_UUIDs, query_objects, valid_target_UUIDs, target_objects);
1059 DOCTEST_FAIL("Expected exception was not thrown");
1060 } catch (const std::runtime_error &e) {
1061 std::string error_msg = e.what();
1062 bool has_relevant_content = error_msg.find("UUID") != std::string::npos || error_msg.find("invalid") != std::string::npos;
1063 DOCTEST_CHECK(has_relevant_content);
1064 }
1065}
1066
1067DOCTEST_TEST_CASE("CollisionDetection findOptimalConePath Basic Functionality") {
1068 Context context;
1069
1070 // Suppress all initialization and BVH building messages
1071 CollisionDetection collision(&context);
1072 collision.disableMessages();
1073
1074 // Create a simple test scene with obstacles
1075 // Add a few triangles that will create obstacles in certain directions
1076 uint triangle1 = context.addTriangle(make_vec3(2, -1, -1), make_vec3(2, 1, -1), make_vec3(2, 0, 1));
1077 uint triangle2 = context.addTriangle(make_vec3(-2, -1, -1), make_vec3(-2, 1, -1), make_vec3(-2, 0, 1));
1078
1079 // Test with cone pointing towards obstacles
1080 vec3 apex = make_vec3(0, 0, -5);
1081 vec3 central_axis = make_vec3(0, 0, 1); // Pointing toward obstacles
1082 float half_angle = M_PI / 4.0f; // 45 degrees
1083
1084 // Test 1: Basic functionality with default parameters
1085 CollisionDetection::OptimalPathResult result = collision.findOptimalConePath(apex, central_axis, half_angle);
1086
1087 DOCTEST_CHECK(result.direction.magnitude() > 0.9f); // Should be normalized
1088 DOCTEST_CHECK(result.direction.magnitude() < 1.1f);
1089 DOCTEST_CHECK(result.confidence >= 0.0f);
1090 DOCTEST_CHECK(result.confidence <= 1.0f);
1091 DOCTEST_CHECK(result.collisionCount >= 0);
1092}
1093
1094DOCTEST_TEST_CASE("CollisionDetection findOptimalConePath Gap Detection") {
1095 Context context;
1096
1097 // Suppress all initialization and BVH building messages
1098 CollisionDetection collision(&context);
1099 collision.disableMessages();
1100
1101 // Create test geometry with obstacles in a specific pattern
1102 // Add obstacle directly in front (along central axis)
1103 context.addTriangle(make_vec3(-0.5f, -0.5f, 0), make_vec3(0.5f, -0.5f, 0), make_vec3(0, 0.5f, 0));
1104 // Add fewer obstacles to the side (at an angle from central axis)
1105 context.addTriangle(make_vec3(3, -0.2f, 0), make_vec3(3, 0.2f, 0), make_vec3(3, 0, 0.5f));
1106
1107 vec3 apex = make_vec3(0, 0, -2);
1108 vec3 central_axis = make_vec3(0, 0, 1); // Pointing toward main obstacle
1109 float half_angle = M_PI / 3.0f; // 60 degrees - wide cone
1110
1111 // Test 1: Default behavior - should find optimal gap-based path
1112 CollisionDetection::OptimalPathResult gap_result = collision.findOptimalConePath(apex, central_axis, half_angle, 0.0f, 64);
1113
1114 // Test 2: Higher sample count for better gap detection
1115 CollisionDetection::OptimalPathResult dense_result = collision.findOptimalConePath(apex, central_axis, half_angle, 0.0f, 256);
1116
1117 // Test 3: Lower sample count
1118 CollisionDetection::OptimalPathResult sparse_result = collision.findOptimalConePath(apex, central_axis, half_angle, 0.0f, 32);
1119
1120 // Verify results are reasonable
1121 DOCTEST_CHECK(gap_result.direction.magnitude() > 0.9f);
1122 DOCTEST_CHECK(dense_result.direction.magnitude() > 0.9f);
1123 DOCTEST_CHECK(sparse_result.direction.magnitude() > 0.9f);
1124
1125 // All should have valid confidence scores
1126 DOCTEST_CHECK(gap_result.confidence >= 0.0f);
1127 DOCTEST_CHECK(gap_result.confidence <= 1.0f);
1128 DOCTEST_CHECK(dense_result.confidence >= 0.0f);
1129 DOCTEST_CHECK(dense_result.confidence <= 1.0f);
1130 DOCTEST_CHECK(sparse_result.confidence >= 0.0f);
1131 DOCTEST_CHECK(sparse_result.confidence <= 1.0f);
1132
1133 // Results should be valid directions
1134 float gap_deviation = acosf(std::max(-1.0f, std::min(1.0f, gap_result.direction * central_axis)));
1135 float dense_deviation = acosf(std::max(-1.0f, std::min(1.0f, dense_result.direction * central_axis)));
1136
1137 DOCTEST_CHECK(gap_deviation >= 0.0f); // Should be valid angle
1138 DOCTEST_CHECK(dense_deviation >= 0.0f); // Should be valid angle
1139}
1140
1141DOCTEST_TEST_CASE("CollisionDetection findOptimalConePath Edge Cases") {
1142 Context context;
1143
1144 // Suppress all initialization and BVH building messages
1145 CollisionDetection collision(&context);
1146 collision.disableMessages();
1147
1148 vec3 apex = make_vec3(0, 0, 0);
1149 vec3 central_axis = make_vec3(0, 0, 1);
1150
1151 // Test 1: Invalid parameters
1152 {
1153 // Zero samples
1154 CollisionDetection::OptimalPathResult result = collision.findOptimalConePath(apex, central_axis, M_PI / 4.0f, 0.0f, 0);
1155 DOCTEST_CHECK(result.direction * central_axis > 0.9f); // Should default to central axis
1156 }
1157
1158 // Test 2: Zero half-angle
1159 {
1160 CollisionDetection::OptimalPathResult result = collision.findOptimalConePath(apex, central_axis, 0.0f, 0.0f, 16);
1161 DOCTEST_CHECK(result.direction * central_axis > 0.9f); // Should default to central axis
1162 }
1163
1164 // Test 3: Empty scene (no geometry)
1165 {
1166 CollisionDetection::OptimalPathResult result = collision.findOptimalConePath(apex, central_axis, M_PI / 4.0f, 0.0f, 16);
1167 DOCTEST_CHECK(result.direction * central_axis > 0.9f); // Should prefer central axis
1168 DOCTEST_CHECK(result.collisionCount == 0); // No collisions in empty scene
1169 DOCTEST_CHECK(result.confidence == 1.0f); // High confidence with no obstacles
1170 }
1171
1172 // Test 4: Single sample
1173 {
1174 // Add some geometry
1175 context.addTriangle(make_vec3(-1, -1, 1), make_vec3(1, -1, 1), make_vec3(0, 1, 1));
1176
1177 CollisionDetection::OptimalPathResult result = collision.findOptimalConePath(apex, central_axis, M_PI / 4.0f, 0.0f, 1);
1178 DOCTEST_CHECK(result.direction.magnitude() > 0.9f); // Should be normalized
1179 DOCTEST_CHECK(result.confidence == 1.0f); // Perfect confidence with single sample
1180 }
1181}
1182
1183DOCTEST_TEST_CASE("CollisionDetection Finite Cone Height") {
1184 Context context;
1185
1186 // Suppress all initialization and BVH building messages
1187 CollisionDetection collision(&context);
1188 collision.disableMessages();
1189
1190 // Create obstacles at different distances
1191 // Close obstacle
1192 context.addTriangle(make_vec3(-0.5f, -0.5f, 1), make_vec3(0.5f, -0.5f, 1), make_vec3(0, 0.5f, 1));
1193 // Far obstacle
1194 context.addTriangle(make_vec3(-0.5f, -0.5f, 5), make_vec3(0.5f, -0.5f, 5), make_vec3(0, 0.5f, 5));
1195
1196 vec3 apex = make_vec3(0, 0, 0);
1197 vec3 central_axis = make_vec3(0, 0, 1);
1198 float half_angle = M_PI / 6.0f; // 30 degrees
1199
1200 // Test 1: Short cone - should only see close obstacle
1201 CollisionDetection::OptimalPathResult short_result = collision.findOptimalConePath(apex, central_axis, half_angle, 2.0f, 16);
1202
1203 // Test 2: Long cone - should see both obstacles
1204 CollisionDetection::OptimalPathResult long_result = collision.findOptimalConePath(apex, central_axis, half_angle, 10.0f, 16);
1205
1206 // Both should produce valid results
1207 DOCTEST_CHECK(short_result.direction.magnitude() > 0.9f);
1208 DOCTEST_CHECK(long_result.direction.magnitude() > 0.9f);
1209 DOCTEST_CHECK(short_result.collisionCount >= 0);
1210 DOCTEST_CHECK(long_result.collisionCount >= 0);
1211}
1212
1213// ================================================================
1214// ACCURACY TEST CASES FOR findOptimalConePath
1215// ================================================================
1216
1217DOCTEST_TEST_CASE("CollisionDetection findOptimalConePath Accuracy - Single Gap Tests") {
1218 Context context;
1219 CollisionDetection collision(&context);
1220 collision.disableMessages();
1221
1222 vec3 apex = make_vec3(0, 0, -3);
1223 vec3 central_axis = make_vec3(0, 0, 1);
1224 float half_angle = M_PI / 4.0f; // 45 degrees
1225
1226 // Test 1: Center gap - optimal path should point straight ahead
1227 {
1228 CollisionTests::createParallelWallsWithGap(&context, make_vec3(0, 0, 0), 1.0f, 2.0f, 3.0f);
1229
1230 CollisionDetection::OptimalPathResult result = collision.findOptimalConePath(apex, central_axis, half_angle, 0.0f, 128);
1231 vec3 expected_direction = CollisionTests::calculateGapCenterDirection(apex, make_vec3(0, 0, 3.0f));
1232 float angular_error = CollisionTests::measureAngularError(result.direction, expected_direction);
1233
1234 DOCTEST_CHECK(result.direction.magnitude() > 0.9f);
1235 DOCTEST_CHECK(result.direction.magnitude() < 1.1f);
1236 DOCTEST_CHECK(angular_error < 0.1f); // Less than ~5.7 degrees
1237 DOCTEST_CHECK(result.confidence > 0.5f); // Should have reasonable confidence
1238 }
1239
1240 // Test 2: Off-center gap - should point toward gap center
1241 {
1242 Context context_reset;
1243 CollisionDetection collision_reset(&context_reset);
1244 collision_reset.disableMessages();
1245
1246 vec3 gap_center = make_vec3(0.5f, 0, 0);
1247 CollisionTests::createParallelWallsWithGap(&context_reset, gap_center, 0.8f, 2.0f, 3.0f);
1248
1249 CollisionDetection::OptimalPathResult result = collision_reset.findOptimalConePath(apex, central_axis, half_angle, 0.0f, 128);
1250 vec3 expected_direction = CollisionTests::calculateGapCenterDirection(apex, make_vec3(gap_center.x, gap_center.y, 3.0f));
1251 float angular_error = CollisionTests::measureAngularError(result.direction, expected_direction);
1252
1253 DOCTEST_CHECK(angular_error < 0.15f); // Less than ~8.6 degrees
1254 DOCTEST_CHECK(result.confidence > 0.3f);
1255 }
1256
1257 // Test 3: Narrow gap - should still find it but with lower confidence
1258 {
1259 Context context_reset;
1260 CollisionDetection collision_reset(&context_reset);
1261 collision_reset.disableMessages();
1262
1263 CollisionTests::createParallelWallsWithGap(&context_reset, make_vec3(0, 0, 0), 0.3f, 2.0f, 3.0f);
1264
1265 CollisionDetection::OptimalPathResult result = collision_reset.findOptimalConePath(apex, central_axis, half_angle, 0.0f, 256);
1266 vec3 expected_direction = CollisionTests::calculateGapCenterDirection(apex, make_vec3(0, 0, 3.0f));
1267 float angular_error = CollisionTests::measureAngularError(result.direction, expected_direction);
1268
1269 DOCTEST_CHECK(angular_error < 0.2f); // Less than ~11.5 degrees (more tolerance for narrow gap)
1270 DOCTEST_CHECK(result.confidence >= 0.0f); // Valid confidence
1271 }
1272}
1273
1274DOCTEST_TEST_CASE("CollisionDetection findOptimalConePath Accuracy - Symmetric Twin Gaps") {
1275 Context context;
1276 CollisionDetection collision(&context);
1277 collision.disableMessages();
1278
1279 vec3 apex = make_vec3(0, 0, -3);
1280 vec3 central_axis = make_vec3(0, 0, 1);
1281 float half_angle = M_PI / 3.0f; // 60 degrees - wide enough to see both gaps
1282
1283 // Test 1: Identical gaps equidistant from center - should prefer center alignment
1284 {
1285 CollisionTests::createSymmetricTwinGaps(&context, 2.0f, 0.8f, 3.0f);
1286
1287 CollisionDetection::OptimalPathResult result = collision.findOptimalConePath(apex, central_axis, half_angle, 0.0f, 256);
1288
1289 // Should prefer the more centrally aligned solution
1290 float deviation_from_center = fabsf(acosf(std::max(-1.0f, std::min(1.0f, result.direction * central_axis))));
1291 DOCTEST_CHECK(deviation_from_center < M_PI / 6.0f); // Less than 30 degrees from center
1292 DOCTEST_CHECK(result.confidence > 0.4f);
1293 }
1294
1295 // Test 2: Different sized gaps - should prefer larger gap
1296 {
1297 Context context_reset;
1298 CollisionDetection collision_reset(&context_reset);
1299 collision_reset.disableMessages();
1300
1301 // Create small left gap and large right gap
1302 CollisionTests::createParallelWallsWithGap(&context_reset, make_vec3(-1.0f, 0, 0), 0.5f, 2.0f, 3.0f);
1303 CollisionTests::createParallelWallsWithGap(&context_reset, make_vec3(1.0f, 0, 0), 1.5f, 2.0f, 3.0f);
1304
1305 CollisionDetection::OptimalPathResult result = collision_reset.findOptimalConePath(apex, central_axis, half_angle, 0.0f, 256);
1306
1307 // Should prefer the larger gap (right side)
1308 vec3 expected_direction = CollisionTests::calculateGapCenterDirection(apex, make_vec3(1.0f, 0, 3.0f));
1309 float angular_error = CollisionTests::measureAngularError(result.direction, expected_direction);
1310
1311 DOCTEST_CHECK(angular_error < 0.3f); // Should point roughly toward larger gap
1312 DOCTEST_CHECK(result.confidence > 0.3f);
1313 }
1314}
1315
1316DOCTEST_TEST_CASE("CollisionDetection findOptimalConePath Accuracy - Angular Precision") {
1317 Context context;
1318 CollisionDetection collision(&context);
1319 collision.disableMessages();
1320
1321 vec3 apex = make_vec3(0, 0, -2);
1322 vec3 central_axis = make_vec3(0, 0, 1);
1323 float half_angle = M_PI / 4.0f; // 45 degrees
1324
1325 // Test different gap positions at known angles
1326 struct TestCase {
1327 vec3 gap_position;
1328 float expected_angle_from_center; // in radians
1329 std::string description;
1330 };
1331
1332 std::vector<TestCase> test_cases = {
1333 {make_vec3(0, 0, 0), 0.0f, "Center gap"},
1334 {make_vec3(0.5f, 0, 0), 0.245f, "15 degree gap"}, // atan(0.5/2) ≈ 0.245 rad
1335 {make_vec3(-0.7f, 0, 0), -0.334f, "Negative 19 degree gap"}, // atan(-0.7/2) ≈ -0.334 rad
1336 {make_vec3(0, 0.8f, 0), 0.381f, "Vertical 22 degree gap"} // atan(0.8/2) ≈ 0.381 rad
1337 };
1338
1339 for (const auto &test_case: test_cases) {
1340 // Reset context for each test
1341 Context context_fresh;
1342 CollisionDetection collision_fresh(&context_fresh);
1343 collision_fresh.disableMessages();
1344
1345 // Create gap at specific position
1346 CollisionTests::createParallelWallsWithGap(&context_fresh, test_case.gap_position, 0.6f, 2.0f, 2.0f);
1347
1348 CollisionDetection::OptimalPathResult result = collision_fresh.findOptimalConePath(apex, central_axis, half_angle, 0.0f, 512);
1349
1350 // Calculate expected direction
1351 vec3 gap_center_3d = make_vec3(test_case.gap_position.x, test_case.gap_position.y, 2.0f);
1352 vec3 expected_direction = CollisionTests::calculateGapCenterDirection(apex, gap_center_3d);
1353 float angular_error = CollisionTests::measureAngularError(result.direction, expected_direction);
1354
1355 // Verify angular accuracy
1356 DOCTEST_CHECK_MESSAGE(angular_error < 0.35f, test_case.description.c_str()); // Less than 20 degrees (realistic tolerance)
1357 DOCTEST_CHECK_MESSAGE(result.confidence > 0.1f, test_case.description.c_str());
1358 }
1359}
1360
1361DOCTEST_TEST_CASE("CollisionDetection findOptimalConePath Accuracy - Distance-Based Priority") {
1362 Context context;
1363 CollisionDetection collision(&context);
1364 collision.disableMessages();
1365
1366 vec3 apex = make_vec3(0, 0, -4);
1367 vec3 central_axis = make_vec3(0, 0, 1);
1368 float half_angle = M_PI / 3.0f; // 60 degrees
1369
1370 // Test 1: Large far gap vs small near gap - fish-eye metric should prefer larger angular gap
1371 {
1372 // Near small gap (at distance 2.0)
1373 CollisionTests::createParallelWallsWithGap(&context, make_vec3(-1.0f, 0, 0), 0.4f, 2.0f, 2.0f);
1374
1375 // Far large gap (at distance 6.0)
1376 CollisionTests::createParallelWallsWithGap(&context, make_vec3(1.0f, 0, 0), 2.0f, 2.0f, 6.0f);
1377
1378 CollisionDetection::OptimalPathResult result = collision.findOptimalConePath(apex, central_axis, half_angle, 0.0f, 256);
1379
1380 // Fish-eye metric should make closer objects appear larger
1381 // The closer small gap should have larger angular size than the far large gap
1382 vec3 near_direction = CollisionTests::calculateGapCenterDirection(apex, make_vec3(-1.0f, 0, 2.0f));
1383 float angular_error_near = CollisionTests::measureAngularError(result.direction, near_direction);
1384
1385 DOCTEST_CHECK(angular_error_near < 0.5f); // Should be closer to near gap
1386 DOCTEST_CHECK(result.confidence > 0.2f);
1387 }
1388
1389 // Test 2: Same-sized gaps at different distances
1390 {
1391 Context context_reset;
1392 CollisionDetection collision_reset(&context_reset);
1393 collision_reset.disableMessages();
1394
1395 // Near gap at distance 2.0
1396 CollisionTests::createParallelWallsWithGap(&context_reset, make_vec3(-0.8f, 0, 0), 0.8f, 2.0f, 2.0f);
1397
1398 // Far gap at distance 5.0
1399 CollisionTests::createParallelWallsWithGap(&context_reset, make_vec3(0.8f, 0, 0), 0.8f, 2.0f, 5.0f);
1400
1401 CollisionDetection::OptimalPathResult result = collision_reset.findOptimalConePath(apex, central_axis, half_angle, 0.0f, 256);
1402
1403 // Should prefer the closer gap due to larger angular size
1404 vec3 near_direction = CollisionTests::calculateGapCenterDirection(apex, make_vec3(-0.8f, 0, 2.0f));
1405 float angular_error_near = CollisionTests::measureAngularError(result.direction, near_direction);
1406
1407 DOCTEST_CHECK(angular_error_near < 0.4f); // Should prefer closer gap
1408 DOCTEST_CHECK(result.confidence > 0.2f);
1409 }
1410
1411 // Test 3: Gap size vs distance trade-off verification
1412 {
1413 Context context_reset;
1414 CollisionDetection collision_reset(&context_reset);
1415 collision_reset.disableMessages();
1416
1417 // Create scenario where angular sizes are roughly similar
1418 // Near small gap (angular size ≈ gap_width / distance)
1419 CollisionTests::createParallelWallsWithGap(&context_reset, make_vec3(-0.5f, 0, 0), 0.5f, 2.0f, 2.5f); // Angular size ≈ 0.2 rad
1420
1421 // Far medium gap
1422 CollisionTests::createParallelWallsWithGap(&context_reset, make_vec3(0.5f, 0, 0), 1.0f, 2.0f, 5.0f); // Angular size ≈ 0.2 rad
1423
1424 CollisionDetection::OptimalPathResult result = collision_reset.findOptimalConePath(apex, central_axis, half_angle, 0.0f, 512);
1425
1426 // Both gaps should have similar scoring - result should be reasonable for either
1427 DOCTEST_CHECK(result.direction.magnitude() > 0.9f);
1428 DOCTEST_CHECK(result.direction.magnitude() < 1.1f);
1429 DOCTEST_CHECK(result.confidence > 0.1f);
1430
1431 // Direction should point toward one of the gaps (not somewhere random)
1432 vec3 near_dir = CollisionTests::calculateGapCenterDirection(apex, make_vec3(-0.5f, 0, 2.5f));
1433 vec3 far_dir = CollisionTests::calculateGapCenterDirection(apex, make_vec3(0.5f, 0, 5.0f));
1434 float error_near = CollisionTests::measureAngularError(result.direction, near_dir);
1435 float error_far = CollisionTests::measureAngularError(result.direction, far_dir);
1436
1437 DOCTEST_CHECK((error_near < 0.3f || error_far < 0.3f)); // Should point toward one of the gaps
1438 }
1439}
1440
1441DOCTEST_TEST_CASE("CollisionDetection findOptimalConePath Accuracy - Edge Case Geometry") {
1442 Context context;
1443 CollisionDetection collision(&context);
1444 collision.disableMessages();
1445
1446 vec3 apex = make_vec3(0, 0, -2);
1447 vec3 central_axis = make_vec3(0, 0, 1);
1448
1449 // Test 1: Very narrow gap - algorithm should handle gracefully
1450 {
1451 float half_angle = M_PI / 6.0f; // 30 degrees
1452 CollisionTests::createParallelWallsWithGap(&context, make_vec3(0, 0, 0), 0.1f, 2.0f, 2.0f); // Very narrow gap
1453
1454 CollisionDetection::OptimalPathResult result = collision.findOptimalConePath(apex, central_axis, half_angle, 0.0f, 512);
1455
1456 DOCTEST_CHECK(result.direction.magnitude() > 0.9f);
1457 DOCTEST_CHECK(result.direction.magnitude() < 1.1f);
1458 DOCTEST_CHECK(result.confidence >= 0.0f);
1459 DOCTEST_CHECK(result.confidence <= 1.0f);
1460
1461 // Should still point roughly toward the gap
1462 vec3 expected_direction = CollisionTests::calculateGapCenterDirection(apex, make_vec3(0, 0, 2.0f));
1463 float angular_error = CollisionTests::measureAngularError(result.direction, expected_direction);
1464 DOCTEST_CHECK(angular_error < 0.5f); // Reasonable accuracy even for narrow gap
1465 }
1466
1467 // Test 2: Gap at edge of cone - testing cone boundary conditions
1468 {
1469 Context context_reset;
1470 CollisionDetection collision_reset(&context_reset);
1471 collision_reset.disableMessages();
1472
1473 float half_angle = M_PI / 4.0f; // 45 degrees
1474 // Place gap near the edge of the cone
1475 float edge_angle = half_angle * 0.8f; // 80% toward cone edge
1476 float gap_x = 2.0f * tan(edge_angle); // Distance * tan(angle) gives x position
1477
1478 CollisionTests::createParallelWallsWithGap(&context_reset, make_vec3(gap_x, 0, 0), 0.6f, 2.0f, 2.0f);
1479
1480 CollisionDetection::OptimalPathResult result = collision_reset.findOptimalConePath(apex, central_axis, half_angle, 0.0f, 256);
1481
1482 DOCTEST_CHECK(result.direction.magnitude() > 0.9f);
1483 DOCTEST_CHECK(result.direction.magnitude() < 1.1f);
1484
1485 // Should find the gap at the cone edge
1486 vec3 expected_direction = CollisionTests::calculateGapCenterDirection(apex, make_vec3(gap_x, 0, 2.0f));
1487 float angular_error = CollisionTests::measureAngularError(result.direction, expected_direction);
1488 DOCTEST_CHECK(angular_error < 0.4f); // More tolerance for edge case (less than 23 degrees)
1489 }
1490
1491 // Test 3: Partially occluded gap - realistic scenario
1492 {
1493 Context context_reset;
1494 CollisionDetection collision_reset(&context_reset);
1495 collision_reset.disableMessages();
1496
1497 float half_angle = M_PI / 3.0f; // 60 degrees
1498
1499 // Create main gap
1500 CollisionTests::createParallelWallsWithGap(&context_reset, make_vec3(0, 0, 0), 1.2f, 2.0f, 3.0f);
1501
1502 // Add partial obstruction in front of the gap
1503 context_reset.addTriangle(make_vec3(-0.3f, -0.4f, 1.5f), make_vec3(0.3f, -0.4f, 1.5f), make_vec3(0, 0.2f, 1.5f));
1504
1505 CollisionDetection::OptimalPathResult result = collision_reset.findOptimalConePath(apex, central_axis, half_angle, 0.0f, 512);
1506
1507 // Should still find a reasonable path despite partial occlusion
1508 DOCTEST_CHECK(result.direction.magnitude() > 0.9f);
1509 DOCTEST_CHECK(result.direction.magnitude() < 1.1f);
1510 DOCTEST_CHECK(result.confidence >= 0.0f); // Valid confidence
1511
1512 // Should point roughly toward gap region (allowing for avoidance of occlusion)
1513 vec3 gap_direction = CollisionTests::calculateGapCenterDirection(apex, make_vec3(0, 0, 3.0f));
1514 float angular_error = CollisionTests::measureAngularError(result.direction, gap_direction);
1515 DOCTEST_CHECK(angular_error < 0.6f); // More tolerance due to occlusion
1516 }
1517
1518 // Test 4: Multiple competing gaps - stress test for decision making
1519 {
1520 Context context_reset;
1521 CollisionDetection collision_reset(&context_reset);
1522 collision_reset.disableMessages();
1523
1524 float half_angle = M_PI / 2.5f; // 72 degrees - very wide cone
1525
1526 // Create multiple gaps at various positions
1527 CollisionTests::createParallelWallsWithGap(&context_reset, make_vec3(-1.5f, 0, 0), 0.7f, 2.0f, 4.0f);
1528 CollisionTests::createParallelWallsWithGap(&context_reset, make_vec3(0, 0, 0), 0.5f, 2.0f, 3.0f);
1529 CollisionTests::createParallelWallsWithGap(&context_reset, make_vec3(1.2f, 0, 0), 0.8f, 2.0f, 3.5f);
1530 CollisionTests::createParallelWallsWithGap(&context_reset, make_vec3(0, 1.0f, 0), 0.6f, 2.0f, 3.2f);
1531
1532 CollisionDetection::OptimalPathResult result = collision_reset.findOptimalConePath(apex, central_axis, half_angle, 0.0f, 512);
1533
1534 // Should make a reasonable choice among the gaps
1535 DOCTEST_CHECK(result.direction.magnitude() > 0.9f);
1536 DOCTEST_CHECK(result.direction.magnitude() < 1.1f);
1537 DOCTEST_CHECK(result.confidence > 0.1f); // Should have some confidence in the choice
1538
1539 // Direction should be within the cone
1540 float deviation_from_center = acosf(std::max(-1.0f, std::min(1.0f, result.direction * central_axis)));
1541 DOCTEST_CHECK(deviation_from_center <= half_angle + 0.01f); // Within cone bounds (small tolerance)
1542 }
1543}
1544
1545DOCTEST_TEST_CASE("CollisionDetection Scale Test - 1000 Primitives") {
1546 Context context;
1547
1548 // Suppress all initialization and BVH building messages
1549 CollisionDetection collision(&context);
1550 collision.disableMessages();
1551
1552 // Generate 1000 separated triangles
1553 auto uuids = CollisionTests::generateSeparatedTriangles(&context, 1000);
1554
1555 // Should not crash during BVH construction
1556 DOCTEST_CHECK_NOTHROW(collision.buildBVH());
1557
1558 DOCTEST_CHECK(collision.isBVHValid() == true);
1559 DOCTEST_CHECK(collision.getPrimitiveCount() == 1000);
1560}
1561
1562DOCTEST_TEST_CASE("CollisionDetection Scale Test - 10000 Primitives") {
1563 Context context;
1564
1565 // Suppress all initialization and BVH building messages
1566 CollisionDetection collision(&context);
1567 collision.disableMessages();
1568
1569 // Generate 10,000 separated triangles - should stress memory allocation
1570 auto uuids = CollisionTests::generateSeparatedTriangles(&context, 10000);
1571
1572 // This would have caught the original memory allocation bug
1573 DOCTEST_CHECK_NOTHROW(collision.buildBVH());
1574
1575 DOCTEST_CHECK(collision.isBVHValid() == true);
1576 DOCTEST_CHECK(collision.getPrimitiveCount() == 10000);
1577}
1578
1579// =================== CPU vs GPU VALIDATION ===================
1580
1581DOCTEST_TEST_CASE("CollisionDetection CPU vs GPU Consistency - Small Scale") {
1582 Context context;
1583
1584 // Suppress all initialization and BVH building messages
1585
1586 // Create test scenario - overlapping and non-overlapping primitives
1587 auto overlapping = CollisionTests::generateOverlappingCluster(&context, 5, make_vec3(0, 0, 0));
1588 auto separated = CollisionTests::generateSeparatedTriangles(&context, 5, 10.0f);
1589
1590 // Test with CPU
1591 CollisionDetection cpu_collision(&context);
1592 cpu_collision.disableMessages();
1593 cpu_collision.disableGPUAcceleration();
1594 cpu_collision.buildBVH();
1595
1596 // Test with GPU
1597 CollisionDetection gpu_collision(&context);
1598 gpu_collision.disableMessages();
1599 gpu_collision.enableGPUAcceleration();
1600 gpu_collision.buildBVH();
1601
1602 // Compare results for each primitive
1603 for (uint uuid: overlapping) {
1604 auto cpu_results = cpu_collision.findCollisions(uuid);
1605 auto gpu_results = gpu_collision.findCollisions(uuid);
1606
1607 // Sort for comparison
1608 std::sort(cpu_results.begin(), cpu_results.end());
1609 std::sort(gpu_results.begin(), gpu_results.end());
1610
1611 DOCTEST_CHECK(cpu_results == gpu_results);
1612 }
1613}
1614
1615DOCTEST_TEST_CASE("CollisionDetection CPU vs GPU Consistency - Large Scale") {
1616 Context context;
1617
1618 // Suppress all initialization and BVH building messages
1619 CollisionDetection collision(&context);
1620 collision.disableMessages();
1621
1622 // Generate 1000 primitives with known collision pattern
1623 auto cluster1 = CollisionTests::generateOverlappingCluster(&context, 10, make_vec3(0, 0, 0));
1624 auto cluster2 = CollisionTests::generateOverlappingCluster(&context, 10, make_vec3(20, 0, 0));
1625 auto separated = CollisionTests::generateSeparatedTriangles(&context, 980, 2.0f);
1626
1627 // Test CPU results
1628 collision.disableGPUAcceleration();
1629 collision.buildBVH();
1630 auto cpu_results = collision.findCollisions(cluster1[0]);
1631
1632 // Test GPU results
1633 std::vector<uint> gpu_results;
1634 {
1635 helios::capture_cerr capture;
1636 collision.enableGPUAcceleration();
1637 collision.buildBVH();
1638 gpu_results = collision.findCollisions(cluster1[0]);
1639 } // Capture destroyed before assertions
1640
1641 // Sort for comparison
1642 std::sort(cpu_results.begin(), cpu_results.end());
1643 std::sort(gpu_results.begin(), gpu_results.end());
1644
1645 // This would have caught the original GPU false positive bug
1646 DOCTEST_CHECK(cpu_results == gpu_results);
1647}
1648
1649// =================== NEGATIVE TESTING ===================
1650
1651DOCTEST_TEST_CASE("CollisionDetection Negative Test - Well Separated Primitives") {
1652 Context context;
1653
1654 // Suppress all initialization and BVH building messages
1655 CollisionDetection collision(&context);
1656 collision.disableMessages();
1657
1658 // Create primitives that definitely should not intersect
1659 uint triangle1 = context.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0.5f, 1, 0));
1660 uint triangle2 = context.addTriangle(make_vec3(10, 10, 10), make_vec3(11, 10, 10), make_vec3(10.5f, 11, 10));
1661 uint patch = context.addPatch(make_vec3(20, 20, 20), make_vec2(1, 1));
1662
1663 collision.buildBVH();
1664
1665 // Test CPU
1666 collision.disableGPUAcceleration();
1667 auto cpu_collisions = collision.findCollisions(triangle1);
1668
1669 // Test GPU
1670 std::vector<uint> gpu_collisions;
1671 {
1672 helios::capture_cerr capture;
1673 collision.enableGPUAcceleration();
1674 gpu_collisions = collision.findCollisions(triangle1);
1675 } // Capture destroyed before assertions
1676
1677 // Should find 0 collisions (not counting self)
1678 DOCTEST_CHECK(cpu_collisions.size() == 0);
1679 DOCTEST_CHECK(gpu_collisions.size() == 0);
1680}
1681
1682DOCTEST_TEST_CASE("CollisionDetection Negative Test - Patch vs Distant Model") {
1683 Context context;
1684
1685 // Suppress all initialization and BVH building messages
1686 CollisionDetection collision(&context);
1687 collision.disableMessages();
1688
1689 // Create a cluster of triangles far from origin
1690 auto distant_triangles = CollisionTests::generateOverlappingCluster(&context, 20, make_vec3(50, 50, 50));
1691
1692 // Create patch at origin
1693 uint patch = context.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1));
1694
1695 collision.buildBVH();
1696
1697 // Test both CPU and GPU
1698 collision.disableGPUAcceleration();
1699 auto cpu_collisions = collision.findCollisions(patch);
1700
1701 std::vector<uint> gpu_collisions;
1702 {
1703 helios::capture_cerr capture;
1704 collision.enableGPUAcceleration();
1705 gpu_collisions = collision.findCollisions(patch);
1706 } // Capture destroyed before assertions
1707
1708 // Should find 0 collisions
1709 DOCTEST_CHECK(cpu_collisions.size() == 0);
1710 DOCTEST_CHECK(gpu_collisions.size() == 0);
1711 DOCTEST_CHECK(cpu_collisions == gpu_collisions);
1712}
1713
1714// =================== EDGE CASE TESTING ===================
1715
1716DOCTEST_TEST_CASE("CollisionDetection Edge Case - Boundary Touching") {
1717 Context context;
1718
1719 // Suppress all initialization and BVH building messages
1720 CollisionDetection collision(&context);
1721 collision.disableMessages();
1722
1723 // Create primitives that exactly touch at boundaries
1724 uint triangle1 = context.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0.5f, 1, 0));
1725 uint triangle2 = context.addTriangle(make_vec3(1, 0, 0), make_vec3(2, 0, 0), make_vec3(1.5f, 1, 0)); // Shares edge
1726
1727 collision.buildBVH();
1728
1729 // Test CPU vs GPU consistency
1730 collision.disableGPUAcceleration();
1731 auto cpu_results = collision.findCollisions(triangle1);
1732
1733 std::vector<uint> gpu_results;
1734 {
1735 helios::capture_cerr capture;
1736 collision.enableGPUAcceleration();
1737 gpu_results = collision.findCollisions(triangle1);
1738 } // Capture destroyed before assertions
1739
1740 std::sort(cpu_results.begin(), cpu_results.end());
1741 std::sort(gpu_results.begin(), gpu_results.end());
1742
1743 DOCTEST_CHECK(cpu_results == gpu_results);
1744}
1745
1746DOCTEST_TEST_CASE("CollisionDetection Edge Case - Very Small Overlaps") {
1747 Context context;
1748
1749 // Suppress all initialization and BVH building messages
1750 CollisionDetection collision(&context);
1751 collision.disableMessages();
1752
1753 // Create primitives with tiny overlaps (precision testing)
1754 uint triangle1 = context.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0.5f, 1, 0));
1755 uint triangle2 = context.addTriangle(make_vec3(0.99f, 0, 0), make_vec3(1.99f, 0, 0), make_vec3(1.49f, 1, 0));
1756
1757 collision.buildBVH();
1758
1759 // Test CPU vs GPU consistency for precision
1760 collision.disableGPUAcceleration();
1761 auto cpu_results = collision.findCollisions(triangle1);
1762
1763 std::vector<uint> gpu_results;
1764 {
1765 helios::capture_cerr capture;
1766 collision.enableGPUAcceleration();
1767 gpu_results = collision.findCollisions(triangle1);
1768 } // Capture destroyed before assertions
1769
1770 std::sort(cpu_results.begin(), cpu_results.end());
1771 std::sort(gpu_results.begin(), gpu_results.end());
1772
1773 DOCTEST_CHECK(cpu_results == gpu_results);
1774}
1775
1776// =================== REAL GEOMETRY TESTING ===================
1777
1778DOCTEST_TEST_CASE("CollisionDetection Real Geometry - PLY File Loading") {
1779 Context context;
1780
1781 // Suppress all initialization and BVH building messages
1782 CollisionDetection collision(&context);
1783 collision.disableMessages();
1784
1785 // This would test with actual PLY files if available
1786 // For now, simulate complex real geometry
1787 std::vector<uint> complex_model;
1788
1789 // Generate a "complex model" with varying triangle sizes and orientations
1790 for (int i = 0; i < 1000; i++) {
1791 float scale = 0.1f + (i % 10) * 0.05f; // Varying sizes
1792 float angle = (i * 0.1f);
1793 float x = cos(angle) * (i * 0.01f);
1794 float y = sin(angle) * (i * 0.01f);
1795 float z = (i % 100) * 0.001f; // Varying heights
1796
1797 uint uuid = context.addTriangle(make_vec3(x - scale, y - scale, z), make_vec3(x + scale, y - scale, z), make_vec3(x, y + scale, z));
1798 complex_model.push_back(uuid);
1799 }
1800
1801 // Test patch at various positions
1802 uint patch_intersecting = context.addPatch(make_vec3(0, 0, 0.05f), make_vec2(0.5f, 0.5f));
1803 uint patch_non_intersecting = context.addPatch(make_vec3(100, 100, 100), make_vec2(1, 1));
1804
1805 collision.buildBVH();
1806
1807 // Test CPU vs GPU with complex geometry
1808 collision.disableGPUAcceleration();
1809 auto cpu_intersecting = collision.findCollisions(patch_intersecting);
1810 auto cpu_non_intersecting = collision.findCollisions(patch_non_intersecting);
1811
1812 std::vector<uint> gpu_intersecting, gpu_non_intersecting;
1813 {
1814 helios::capture_cerr capture;
1815 collision.enableGPUAcceleration();
1816 gpu_intersecting = collision.findCollisions(patch_intersecting);
1817 gpu_non_intersecting = collision.findCollisions(patch_non_intersecting);
1818 } // Capture destroyed before assertions
1819
1820 // Sort for comparison
1821 std::sort(cpu_intersecting.begin(), cpu_intersecting.end());
1822 std::sort(gpu_intersecting.begin(), gpu_intersecting.end());
1823 std::sort(cpu_non_intersecting.begin(), cpu_non_intersecting.end());
1824 std::sort(gpu_non_intersecting.begin(), gpu_non_intersecting.end());
1825
1826 DOCTEST_CHECK(cpu_intersecting == gpu_intersecting);
1827 DOCTEST_CHECK(cpu_non_intersecting == gpu_non_intersecting);
1828 DOCTEST_CHECK(cpu_non_intersecting.size() == 0); // Should be empty
1829}
1830
1831// =================== PERFORMANCE REGRESSION TESTING ===================
1832
1833DOCTEST_TEST_CASE("CollisionDetection Performance - BVH Construction Time") {
1834 Context context;
1835
1836 // Suppress all initialization messages - but we want to time BVH construction itself
1837 CollisionDetection collision(&context);
1838 collision.disableMessages();
1839
1840 // Generate substantial geometry
1841 auto uuids = CollisionTests::generateSeparatedTriangles(&context, 5000);
1842
1843 // End suppression for timing the actual BVH construction (but keep it silent)
1844 // Time BVH construction
1845 auto start = std::chrono::high_resolution_clock::now();
1846 collision.buildBVH();
1847 auto end = std::chrono::high_resolution_clock::now();
1848
1849 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
1850
1851 // Should complete within reasonable time (adjust threshold as needed)
1852 DOCTEST_CHECK(duration.count() < 5000); // 5 seconds max
1853 DOCTEST_CHECK(collision.isBVHValid() == true);
1854}
1855
1856// =================== MEMORY STRESS TESTING ===================
1857
1858DOCTEST_TEST_CASE("CollisionDetection Memory Stress - Progressive Loading") {
1859 Context context;
1860
1861 // Suppress all initialization and BVH building messages
1862 CollisionDetection collision(&context);
1863 collision.disableMessages();
1864
1865 // Progressively add more primitives to test memory allocation patterns
1866 std::vector<uint> all_uuids;
1867
1868 for (int batch = 0; batch < 10; batch++) {
1869 // Add 1000 more primitives each batch
1870 auto batch_uuids = CollisionTests::generateSeparatedTriangles(&context, 1000, 5.0f + batch * 50.0f);
1871 all_uuids.insert(all_uuids.end(), batch_uuids.begin(), batch_uuids.end());
1872
1873 // Rebuild BVH each time (stress test memory management)
1874 DOCTEST_CHECK_NOTHROW(collision.buildBVH());
1875 DOCTEST_CHECK(collision.isBVHValid() == true);
1876 DOCTEST_CHECK(collision.getPrimitiveCount() == all_uuids.size());
1877 }
1878}
1879
1880// =================== RAY DISTANCE TESTING ===================
1881
1882DOCTEST_TEST_CASE("CollisionDetection findNearestPrimitiveDistance - Basic Functionality") {
1883 Context context;
1884 CollisionDetection collision(&context);
1885 collision.disableMessages();
1886
1887 // Create a simple scene with triangles at known distances
1888 uint triangle1 = context.addTriangle(make_vec3(5, -1, -1), make_vec3(5, 1, -1), make_vec3(5, 0, 1));
1889 uint triangle2 = context.addTriangle(make_vec3(10, -1, -1), make_vec3(10, 1, -1), make_vec3(10, 0, 1));
1890 uint triangle3 = context.addTriangle(make_vec3(15, -1, -1), make_vec3(15, 1, -1), make_vec3(15, 0, 1));
1891
1892 // Test 1: Ray hitting the nearest triangle
1893 vec3 origin = make_vec3(0, 0, 0);
1894 vec3 direction = make_vec3(1, 0, 0); // Pointing along +X axis
1895 std::vector<uint> candidate_UUIDs = {triangle1, triangle2, triangle3};
1896 float distance;
1897 vec3 obstacle_direction;
1898
1899 bool result = collision.findNearestPrimitiveDistance(origin, direction, candidate_UUIDs, distance, obstacle_direction);
1900 DOCTEST_CHECK(result == true);
1901 DOCTEST_CHECK(distance >= 4.0f); // Should be approximately 5.0, but AABB might be slightly smaller
1902 DOCTEST_CHECK(distance <= 6.0f);
1903
1904 // Test 2: Ray missing all triangles
1905 vec3 direction_miss = make_vec3(0, 1, 0); // Pointing along +Y axis
1906 bool result_miss = collision.findNearestPrimitiveDistance(origin, direction_miss, candidate_UUIDs, distance, obstacle_direction);
1907 DOCTEST_CHECK(result_miss == false);
1908
1909 // Test 3: Ray with subset of candidates
1910 std::vector<uint> subset_UUIDs = {triangle2, triangle3}; // Exclude nearest triangle
1911 bool result_subset = collision.findNearestPrimitiveDistance(origin, direction, subset_UUIDs, distance, obstacle_direction);
1912 DOCTEST_CHECK(result_subset == true);
1913 DOCTEST_CHECK(distance >= 9.0f); // Should be approximately 10.0
1914 DOCTEST_CHECK(distance <= 11.0f);
1915}
1916
1917DOCTEST_TEST_CASE("CollisionDetection findNearestPrimitiveDistance - Edge Cases") {
1918 Context context;
1919 CollisionDetection collision(&context);
1920 collision.disableMessages();
1921
1922 // Create test geometry
1923 uint triangle1 = context.addTriangle(make_vec3(5, -1, -1), make_vec3(5, 1, -1), make_vec3(5, 0, 1));
1924 vec3 origin = make_vec3(0, 0, 0);
1925 vec3 direction = make_vec3(1, 0, 0);
1926 float distance;
1927
1928 // Test 1: Empty candidate list
1929 std::vector<uint> empty_UUIDs;
1930 vec3 obstacle_direction_unused;
1931 bool result_empty = collision.findNearestPrimitiveDistance(origin, direction, empty_UUIDs, distance, obstacle_direction_unused);
1932 DOCTEST_CHECK(result_empty == false);
1933
1934 // Test 2: Non-normalized direction vector (should return false with warning)
1935 vec3 non_normalized_dir = make_vec3(2, 0, 0); // Magnitude = 2
1936 std::vector<uint> valid_UUIDs = {triangle1};
1937 bool result_non_norm = collision.findNearestPrimitiveDistance(origin, non_normalized_dir, valid_UUIDs, distance, obstacle_direction_unused);
1938 DOCTEST_CHECK(result_non_norm == false);
1939
1940 // Test 3: Invalid UUID in candidate list
1941 std::vector<uint> invalid_UUIDs = {999999}; // Non-existent UUID
1942 bool result_invalid = collision.findNearestPrimitiveDistance(origin, direction, invalid_UUIDs, distance, obstacle_direction_unused);
1943 DOCTEST_CHECK(result_invalid == false);
1944
1945 // Test 4: Mixed valid and invalid UUIDs
1946 std::vector<uint> mixed_UUIDs = {triangle1, 999999};
1947 bool result_mixed = collision.findNearestPrimitiveDistance(origin, direction, mixed_UUIDs, distance, obstacle_direction_unused);
1948 DOCTEST_CHECK(result_mixed == true); // Should still find the valid triangle
1949 DOCTEST_CHECK(distance >= 4.0f);
1950 DOCTEST_CHECK(distance <= 6.0f);
1951}
1952
1953DOCTEST_TEST_CASE("CollisionDetection findNearestPrimitiveDistance - Complex Scenarios") {
1954 Context context;
1955 CollisionDetection collision(&context);
1956 collision.disableMessages();
1957
1958 // Create overlapping geometry
1959 auto cluster = CollisionTests::generateOverlappingCluster(&context, 10, make_vec3(5, 0, 0));
1960
1961 // Test 1: Ray through dense cluster
1962 vec3 origin = make_vec3(0, 0, 0);
1963 vec3 direction = make_vec3(1, 0, 0);
1964 float distance;
1965 vec3 obstacle_direction_unused;
1966
1967 bool result = collision.findNearestPrimitiveDistance(origin, direction, cluster, distance, obstacle_direction_unused);
1968 // Updated expectation: ray traveling in +X direction should NOT hit triangles in XY plane (parallel)
1969 DOCTEST_CHECK(result == false);
1970
1971 // Test 2: Ray from near the cluster edge
1972 // Note: Ray parallel to triangles should not detect intersection
1973 vec3 origin_near = make_vec3(3.5, 0, 0); // Just outside the cluster
1974 vec3 direction_out = make_vec3(1, 0, 0);
1975 bool result_near = collision.findNearestPrimitiveDistance(origin_near, direction_out, cluster, distance, obstacle_direction_unused);
1976 DOCTEST_CHECK(result_near == false); // Ray parallel to triangles - no hit expected
1977
1978 // Test 3: Test with ray that can actually hit the triangles (perpendicular approach)
1979 vec3 origin_above = make_vec3(5, 0, 1); // Above the cluster center
1980 vec3 direction_down = make_vec3(0, 0, -1); // Growing downward toward triangles
1981 bool result_perpendicular = collision.findNearestPrimitiveDistance(origin_above, direction_down, cluster, distance, obstacle_direction_unused);
1982 DOCTEST_CHECK(result_perpendicular == true); // Should hit triangles when approaching perpendicularly
1983 DOCTEST_CHECK(distance >= 0.9f); // Distance from z=1 to z=0 (approximately)
1984 DOCTEST_CHECK(distance <= 1.1f);
1985}
1986
1987DOCTEST_TEST_CASE("CollisionDetection findNearestPrimitiveDistance - Directional Testing") {
1988 Context context;
1989 CollisionDetection collision(&context);
1990 collision.disableMessages();
1991
1992 // Create triangles in different directions
1993 uint triangle_x = context.addTriangle(make_vec3(5, -1, -1), make_vec3(5, 1, -1), make_vec3(5, 0, 1));
1994 uint triangle_y = context.addTriangle(make_vec3(-1, 5, -1), make_vec3(1, 5, -1), make_vec3(0, 5, 1));
1995 uint triangle_z = context.addTriangle(make_vec3(-1, -1, 5), make_vec3(1, -1, 5), make_vec3(0, 1, 5));
1996 uint triangle_neg_x = context.addTriangle(make_vec3(-5, -1, -1), make_vec3(-5, 1, -1), make_vec3(-5, 0, 1));
1997
1998 std::vector<uint> all_triangles = {triangle_x, triangle_y, triangle_z, triangle_neg_x};
1999 vec3 origin = make_vec3(0, 0, 0);
2000 float distance;
2001
2002 // Test different ray directions
2003 struct DirectionTest {
2004 vec3 direction;
2005 float expected_min;
2006 float expected_max;
2007 bool should_hit;
2008 };
2009
2010 std::vector<DirectionTest> tests = {
2011 {make_vec3(1, 0, 0), 4.0f, 6.0f, true}, // +X direction
2012 {make_vec3(0, 1, 0), 4.0f, 6.0f, true}, // +Y direction
2013 {make_vec3(0, 0, 1), 4.0f, 6.0f, true}, // +Z direction
2014 {make_vec3(-1, 0, 0), 4.0f, 6.0f, true}, // -X direction
2015 {make_vec3(0.707f, 0.707f, 0), 6.0f, 8.0f, false}, // Diagonal XY (should miss)
2016 };
2017
2018 vec3 obstacle_direction_unused;
2019 for (const auto &test: tests) {
2020 bool result = collision.findNearestPrimitiveDistance(origin, test.direction, all_triangles, distance, obstacle_direction_unused);
2021 DOCTEST_CHECK(result == test.should_hit);
2022 if (result && test.should_hit) {
2023 DOCTEST_CHECK(distance >= test.expected_min);
2024 DOCTEST_CHECK(distance <= test.expected_max);
2025 }
2026 }
2027}
2028
2029DOCTEST_TEST_CASE("CollisionDetection - findNearestPrimitiveDistance front/back face detection") {
2030 helios::Context context;
2031 CollisionDetection collision(&context);
2032 collision.disableMessages();
2033
2034 // Create a horizontal patch at z=1.0 (normal pointing up in +Z direction)
2035 vec3 patch_center = make_vec3(0, 0, 1);
2036 vec2 patch_size = make_vec2(2, 2);
2037 uint horizontal_patch = context.addPatch(patch_center, patch_size);
2038
2039 std::vector<uint> candidates = {horizontal_patch};
2040 float distance;
2041 vec3 obstacle_direction;
2042
2043 // Test 1: Approaching from below (should get +Z direction - toward surface)
2044 vec3 origin_below = make_vec3(0, 0, 0.5f);
2045 vec3 direction_up = make_vec3(0, 0, 1); // Growing upward
2046
2047 bool found_below = collision.findNearestPrimitiveDistance(origin_below, direction_up, candidates, distance, obstacle_direction);
2048 DOCTEST_CHECK(found_below == true);
2049 DOCTEST_CHECK(distance >= 0.49f);
2050 DOCTEST_CHECK(distance <= 0.51f);
2051 // When approaching from below, obstacle_direction should point upward (+Z)
2052 DOCTEST_CHECK(obstacle_direction.z > 0.9f);
2053 DOCTEST_CHECK(std::abs(obstacle_direction.x) < 0.1f);
2054 DOCTEST_CHECK(std::abs(obstacle_direction.y) < 0.1f);
2055
2056 // Test 2: Approaching from above (should get -Z direction - toward surface)
2057 vec3 origin_above = make_vec3(0, 0, 1.5f);
2058 vec3 direction_down = make_vec3(0, 0, -1); // Growing downward
2059
2060 bool found_above = collision.findNearestPrimitiveDistance(origin_above, direction_down, candidates, distance, obstacle_direction);
2061 DOCTEST_CHECK(found_above == true);
2062 DOCTEST_CHECK(distance >= 0.49f);
2063 DOCTEST_CHECK(distance <= 0.51f);
2064 // When approaching from above, obstacle_direction should point downward (-Z)
2065 DOCTEST_CHECK(obstacle_direction.z < -0.9f);
2066 DOCTEST_CHECK(std::abs(obstacle_direction.x) < 0.1f);
2067 DOCTEST_CHECK(std::abs(obstacle_direction.y) < 0.1f);
2068
2069 // Test 3: Growing away from surface (should not detect obstacle)
2070 vec3 origin_below2 = make_vec3(0, 0, 0.5f);
2071 vec3 direction_away = make_vec3(0, 0, -1); // Growing away from obstacle
2072
2073 bool found_away = collision.findNearestPrimitiveDistance(origin_below2, direction_away, candidates, distance, obstacle_direction);
2074 DOCTEST_CHECK(found_away == false); // Should not detect surface behind growth direction
2075}
2076
2077// =================== CONE-BASED OBSTACLE DETECTION TESTS ===================
2078
2079DOCTEST_TEST_CASE("CollisionDetection Cone-Based Obstacle Detection - Basic Functionality") {
2080 Context context;
2081 CollisionDetection collision(&context);
2082 collision.disableMessages();
2083
2084 // Create a horizontal patch obstacle at z=1.0
2085 uint obstacle_uuid = context.addPatch(make_vec3(0, 0, 1), make_vec2(2, 2));
2086 std::vector<uint> obstacles = {obstacle_uuid};
2087 collision.buildBVH(obstacles);
2088
2089 // Test 1: Ray from below, directly toward obstacle
2090 vec3 apex = make_vec3(0, 0, 0.5f);
2091 vec3 axis = make_vec3(0, 0, 1); // Straight up
2092 float half_angle = deg2rad(30.0f); // 30 degree half-angle
2093 float height = 1.0f; // 1 meter detection range
2094
2095 float distance;
2096 vec3 obstacle_direction;
2097
2098 bool found = collision.findNearestSolidObstacleInCone(apex, axis, half_angle, height, obstacles, distance, obstacle_direction);
2099
2100 DOCTEST_CHECK(found == true);
2101 DOCTEST_CHECK(distance >= 0.49f);
2102 DOCTEST_CHECK(distance <= 0.51f); // Should be ~0.5m from apex to obstacle
2103 DOCTEST_CHECK(obstacle_direction.z > 0.9f); // Direction should be mostly upward
2104
2105 // Test 2: Ray from far away - should not detect
2106 vec3 apex_far = make_vec3(0, 0, -2.0f);
2107 bool found_far = collision.findNearestSolidObstacleInCone(apex_far, axis, half_angle, height, obstacles, distance, obstacle_direction);
2108 DOCTEST_CHECK(found_far == false); // Too far away
2109
2110 // Test 3: Narrow cone that misses obstacle
2111 float narrow_angle = deg2rad(5.0f); // Very narrow cone
2112 vec3 axis_offset = make_vec3(3.0f, 0, 1); // Aimed well to the side to clearly miss the 2x2 patch
2113 axis_offset.normalize();
2114
2115 bool found_narrow = collision.findNearestSolidObstacleInCone(apex, axis_offset, narrow_angle, height, obstacles, distance, obstacle_direction);
2116 DOCTEST_CHECK(found_narrow == false); // Should miss the obstacle
2117}
2118
2119DOCTEST_TEST_CASE("CollisionDetection Cone-Based vs Legacy Method Comparison") {
2120 Context context;
2121 CollisionDetection collision(&context);
2122 collision.disableMessages();
2123
2124 // Create test obstacle
2125 uint obstacle_uuid = context.addPatch(make_vec3(0, 0, 1), make_vec2(1, 1));
2126 std::vector<uint> obstacles = {obstacle_uuid};
2127 collision.buildBVH(obstacles);
2128
2129 // Test parameters
2130 vec3 origin = make_vec3(0, 0, 0.2f);
2131 vec3 direction = make_vec3(0, 0, 1);
2132
2133 // Legacy method
2134 float legacy_distance;
2135 vec3 legacy_obstacle_direction;
2136 bool legacy_found = collision.findNearestPrimitiveDistance(origin, direction, obstacles, legacy_distance, legacy_obstacle_direction);
2137
2138 // New cone method
2139 float cone_distance;
2140 vec3 cone_obstacle_direction;
2141 float half_angle = deg2rad(30.0f);
2142 float height = 2.0f;
2143 bool cone_found = collision.findNearestSolidObstacleInCone(origin, direction, half_angle, height, obstacles, cone_distance, cone_obstacle_direction);
2144
2145 // Both should find the obstacle
2146 DOCTEST_CHECK(legacy_found == true);
2147 DOCTEST_CHECK(cone_found == true);
2148
2149 // Both methods should produce reasonable distance measurements (within 5% of expected 0.8m)
2150 DOCTEST_CHECK(std::abs(cone_distance - 0.8f) < 0.04f); // Within 4cm of expected distance
2151 DOCTEST_CHECK(std::abs(legacy_distance - 0.8f) < 0.04f); // Within 4cm of expected distance
2152
2153 // Both should have reasonable direction vectors
2154 DOCTEST_CHECK(legacy_obstacle_direction.magnitude() > 0.9f);
2155 DOCTEST_CHECK(cone_obstacle_direction.magnitude() > 0.9f);
2156}
2157
2158DOCTEST_TEST_CASE("CollisionDetection Cone-Based Triangle vs Patch Intersection") {
2159 Context context;
2160 CollisionDetection collision(&context);
2161 collision.disableMessages();
2162
2163 // Test triangle intersection
2164 uint triangle_uuid = context.addTriangle(make_vec3(-0.5f, -0.5f, 1.0f), make_vec3(0.5f, -0.5f, 1.0f), make_vec3(0, 0.5f, 1.0f));
2165
2166 // Test patch intersection
2167 uint patch_uuid = context.addPatch(make_vec3(2, 0, 1), make_vec2(1, 1));
2168
2169 std::vector<uint> triangle_obstacles = {triangle_uuid};
2170 std::vector<uint> patch_obstacles = {patch_uuid};
2171
2172 collision.buildBVH({triangle_uuid, patch_uuid});
2173
2174 vec3 apex = make_vec3(0, 0, 0.5f);
2175 vec3 axis = make_vec3(0, 0, 1);
2176 float half_angle = deg2rad(30.0f);
2177 float height = 1.0f;
2178 float distance;
2179 vec3 obstacle_direction;
2180
2181 // Test triangle intersection
2182 bool triangle_found = collision.findNearestSolidObstacleInCone(apex, axis, half_angle, height, triangle_obstacles, distance, obstacle_direction);
2183 DOCTEST_CHECK(triangle_found == true);
2184 DOCTEST_CHECK(distance > 0.4f);
2185 DOCTEST_CHECK(distance < 0.6f);
2186
2187 // Test patch intersection
2188 vec3 apex_patch = make_vec3(2, 0, 0.5f);
2189 bool patch_found = collision.findNearestSolidObstacleInCone(apex_patch, axis, half_angle, height, patch_obstacles, distance, obstacle_direction);
2190 DOCTEST_CHECK(patch_found == true);
2191 DOCTEST_CHECK(distance > 0.4f);
2192 DOCTEST_CHECK(distance < 0.6f);
2193}
2194
2195DOCTEST_TEST_CASE("CollisionDetection Cone-Based Parameter Validation") {
2196 Context context;
2197 CollisionDetection collision(&context);
2198 collision.disableMessages();
2199
2200 uint obstacle_uuid = context.addPatch(make_vec3(0, 0, 1), make_vec2(1, 1));
2201 std::vector<uint> obstacles = {obstacle_uuid};
2202 collision.buildBVH(obstacles);
2203
2204 vec3 apex = make_vec3(0, 0, 0.5f);
2205 vec3 axis = make_vec3(0, 0, 1);
2206 float distance;
2207 vec3 obstacle_direction;
2208
2209 // Test invalid parameters
2210 bool result1 = collision.findNearestSolidObstacleInCone(apex, axis, -0.1f, 1.0f, obstacles, distance, obstacle_direction); // Negative angle
2211 DOCTEST_CHECK(result1 == false);
2212
2213 bool result2 = collision.findNearestSolidObstacleInCone(apex, axis, M_PI, 1.0f, obstacles, distance, obstacle_direction); // Too large angle
2214 DOCTEST_CHECK(result2 == false);
2215
2216 bool result3 = collision.findNearestSolidObstacleInCone(apex, axis, deg2rad(30.0f), -1.0f, obstacles, distance, obstacle_direction); // Negative height
2217 DOCTEST_CHECK(result3 == false);
2218
2219 // Test empty candidate list
2220 std::vector<uint> empty_obstacles;
2221 bool result4 = collision.findNearestSolidObstacleInCone(apex, axis, deg2rad(30.0f), 1.0f, empty_obstacles, distance, obstacle_direction);
2222 DOCTEST_CHECK(result4 == false);
2223
2224 // Test valid parameters - should work
2225 bool result5 = collision.findNearestSolidObstacleInCone(apex, axis, deg2rad(30.0f), 1.0f, obstacles, distance, obstacle_direction);
2226 DOCTEST_CHECK(result5 == true);
2227}
2228
2229
2230DOCTEST_TEST_CASE("CollisionDetection Voxel Ray Path Length - Basic Functionality") {
2231 Context context;
2232 CollisionDetection collision(&context);
2233 collision.disableMessages();
2234
2235 // Set up simple voxel grid
2236 vec3 grid_center(0, 0, 0);
2237 vec3 grid_size(10, 10, 10);
2238 int3 grid_divisions(2, 2, 2);
2239
2240 // Create simple rays through the grid
2241 std::vector<vec3> ray_origins;
2242 std::vector<vec3> ray_directions;
2243
2244 // Ray going straight through the grid center
2245 ray_origins.push_back(make_vec3(-10, 0, 0));
2246 ray_directions.push_back(make_vec3(1, 0, 0));
2247
2248 // Ray going diagonally through multiple voxels
2249 ray_origins.push_back(make_vec3(-10, -10, -10));
2250 ray_directions.push_back(normalize(make_vec3(1, 1, 1)));
2251
2252 // Calculate voxel ray path lengths
2253 collision.calculateVoxelRayPathLengths(grid_center, grid_size, grid_divisions, ray_origins, ray_directions);
2254
2255 // Test transmission probability access
2256 int P_denom, P_trans;
2257 collision.getVoxelTransmissionProbability(make_int3(0, 0, 0), P_denom, P_trans);
2258 DOCTEST_CHECK(P_denom >= 0);
2259 DOCTEST_CHECK(P_trans >= 0);
2260 DOCTEST_CHECK(P_trans <= P_denom);
2261
2262 // Test r_bar access
2263 float r_bar = collision.getVoxelRbar(make_int3(0, 0, 0));
2264 DOCTEST_CHECK(r_bar >= 0.0f);
2265
2266 // Clear data should work without error
2267 collision.clearVoxelData();
2268}
2269
2270DOCTEST_TEST_CASE("CollisionDetection Voxel Ray Path Length - Edge Cases") {
2271 Context context;
2272 CollisionDetection collision(&context);
2273 collision.disableMessages();
2274
2275 // Test with empty ray vectors
2276 vec3 grid_center(0, 0, 0);
2277 vec3 grid_size(5, 5, 5);
2278 int3 grid_divisions(1, 1, 1);
2279
2280 std::vector<vec3> empty_origins;
2281 std::vector<vec3> empty_directions;
2282
2283 // Should handle empty input gracefully
2284 collision.calculateVoxelRayPathLengths(grid_center, grid_size, grid_divisions, empty_origins, empty_directions);
2285
2286 // Test invalid voxel indices
2287 int P_denom, P_trans;
2288
2289 // Test boundary cases - should handle gracefully or throw appropriate error
2290 {
2291 capture_cerr capture;
2292 try {
2293 collision.getVoxelTransmissionProbability(make_int3(-1, 0, 0), P_denom, P_trans);
2294 } catch (const std::exception &e) {
2295 // Expected behavior - invalid indices should be handled
2296 }
2297
2298 try {
2299 collision.getVoxelTransmissionProbability(make_int3(1, 0, 0), P_denom, P_trans);
2300 } catch (const std::exception &e) {
2301 // Expected behavior - out of bounds indices
2302 }
2303 } // capture destroyed here
2304
2305 // Assertions after capture is destroyed
2306 DOCTEST_CHECK(true);
2307}
2308
2309DOCTEST_TEST_CASE("CollisionDetection Voxel Ray Path Length - Data Consistency") {
2310 Context context;
2311 CollisionDetection collision(&context);
2312 collision.disableMessages();
2313
2314 vec3 grid_center(0, 0, 0);
2315 vec3 grid_size(6, 6, 6);
2316 int3 grid_divisions(3, 3, 3);
2317
2318 // Create systematic ray pattern
2319 std::vector<vec3> ray_origins;
2320 std::vector<vec3> ray_directions;
2321
2322 // Grid of parallel rays
2323 for (int i = -1; i <= 1; i++) {
2324 for (int j = -1; j <= 1; j++) {
2325 ray_origins.push_back(make_vec3(i * 1.5f, j * 1.5f, -10));
2326 ray_directions.push_back(make_vec3(0, 0, 1));
2327 }
2328 }
2329
2330 // Calculate path lengths
2331 collision.calculateVoxelRayPathLengths(grid_center, grid_size, grid_divisions, ray_origins, ray_directions);
2332
2333 // Verify data consistency
2334 bool found_data = false;
2335 for (int i = 0; i < grid_divisions.x; i++) {
2336 for (int j = 0; j < grid_divisions.y; j++) {
2337 for (int k = 0; k < grid_divisions.z; k++) {
2338 int P_denom, P_trans;
2339 collision.getVoxelTransmissionProbability(make_int3(i, j, k), P_denom, P_trans);
2340
2341 if (P_denom > 0) {
2342 found_data = true;
2343 // Transmission count should never exceed total count
2344 DOCTEST_CHECK(P_trans <= P_denom);
2345
2346 // R_bar should be positive when rays are present
2347 float r_bar = collision.getVoxelRbar(make_int3(i, j, k));
2348 DOCTEST_CHECK(r_bar > 0.0f);
2349 }
2350 }
2351 }
2352 }
2353
2354 DOCTEST_CHECK(found_data); // Should have found at least some ray intersections
2355}
2356
2357DOCTEST_TEST_CASE("CollisionDetection Voxel Ray Path Length - Manual Data Setting") {
2358 Context context;
2359 CollisionDetection collision(&context);
2360 collision.disableMessages();
2361
2362 vec3 grid_center(0, 0, 0);
2363 vec3 grid_size(4, 4, 4);
2364 int3 grid_divisions(2, 2, 2);
2365
2366 // Initialize with minimal calculation to set up data structures
2367 std::vector<vec3> init_origins;
2368 std::vector<vec3> init_directions;
2369 init_origins.push_back(make_vec3(0, 0, -10)); // Outside the grid
2370 init_directions.push_back(make_vec3(0, 0, 1));
2371 collision.calculateVoxelRayPathLengths(grid_center, grid_size, grid_divisions, init_origins, init_directions);
2372
2373 // Test manual data setting
2374 int3 test_voxel(0, 0, 0);
2375 collision.setVoxelTransmissionProbability(100, 75, test_voxel);
2376 collision.setVoxelRbar(2.5f, test_voxel);
2377
2378 // Verify the data was set correctly
2379 int P_denom, P_trans;
2380 collision.getVoxelTransmissionProbability(test_voxel, P_denom, P_trans);
2381 DOCTEST_CHECK(P_denom == 100);
2382 DOCTEST_CHECK(P_trans == 75);
2383
2384 float r_bar = collision.getVoxelRbar(test_voxel);
2385 DOCTEST_CHECK(std::abs(r_bar - 2.5f) < 1e-6f);
2386
2387 // Test another voxel
2388 int3 test_voxel2(1, 1, 1);
2389 collision.setVoxelTransmissionProbability(200, 150, test_voxel2);
2390 collision.setVoxelRbar(3.7f, test_voxel2);
2391
2392 collision.getVoxelTransmissionProbability(test_voxel2, P_denom, P_trans);
2393 DOCTEST_CHECK(P_denom == 200);
2394 DOCTEST_CHECK(P_trans == 150);
2395
2396 r_bar = collision.getVoxelRbar(test_voxel2);
2397 DOCTEST_CHECK(std::abs(r_bar - 3.7f) < 1e-6f);
2398
2399 // Verify first voxel data is still intact
2400 collision.getVoxelTransmissionProbability(test_voxel, P_denom, P_trans);
2401 DOCTEST_CHECK(P_denom == 100);
2402 DOCTEST_CHECK(P_trans == 75);
2403}
2404
2405DOCTEST_TEST_CASE("CollisionDetection Voxel Ray Path Length - Different Grid Sizes") {
2406 Context context;
2407 CollisionDetection collision(&context);
2408 collision.disableMessages();
2409
2410 // Test various grid sizes
2411 std::vector<int3> test_grids = {
2412 make_int3(1, 1, 1), // Single voxel
2413 make_int3(2, 1, 1), // Linear arrangement
2414 make_int3(4, 4, 4), // Cubic arrangement
2415 make_int3(5, 3, 2) // Asymmetric arrangement
2416 };
2417
2418 for (const auto &grid_div: test_grids) {
2419 vec3 grid_center(0, 0, 0);
2420 vec3 grid_size(10, 10, 10);
2421
2422 std::vector<vec3> ray_origins;
2423 std::vector<vec3> ray_directions;
2424
2425 // Single test ray
2426 ray_origins.push_back(make_vec3(0, 0, -10));
2427 ray_directions.push_back(make_vec3(0, 0, 1));
2428
2429 collision.calculateVoxelRayPathLengths(grid_center, grid_size, grid_div, ray_origins, ray_directions);
2430
2431 // Verify that valid indices work
2432 bool found_valid_voxel = false;
2433 for (int i = 0; i < grid_div.x; i++) {
2434 for (int j = 0; j < grid_div.y; j++) {
2435 for (int k = 0; k < grid_div.z; k++) {
2436 int P_denom, P_trans;
2437 collision.getVoxelTransmissionProbability(make_int3(i, j, k), P_denom, P_trans);
2438 float r_bar = collision.getVoxelRbar(make_int3(i, j, k));
2439
2440 // Should not crash and should give reasonable values
2441 DOCTEST_CHECK(P_denom >= 0);
2442 DOCTEST_CHECK(P_trans >= 0);
2443 DOCTEST_CHECK(r_bar >= 0.0f);
2444 found_valid_voxel = true;
2445 }
2446 }
2447 }
2448 DOCTEST_CHECK(found_valid_voxel);
2449
2450 collision.clearVoxelData();
2451 }
2452}
2453
2454DOCTEST_TEST_CASE("CollisionDetection Voxel Ray Path Length - Ray Direction Variations") {
2455 Context context;
2456 CollisionDetection collision(&context);
2457 collision.disableMessages();
2458
2459 vec3 grid_center(0, 0, 0);
2460 vec3 grid_size(8, 8, 8);
2461 int3 grid_divisions(2, 2, 2);
2462
2463 // Test rays with different directions
2464 std::vector<vec3> test_directions = {
2465 make_vec3(1, 0, 0), // X-axis
2466 make_vec3(0, 1, 0), // Y-axis
2467 make_vec3(0, 0, 1), // Z-axis
2468 normalize(make_vec3(1, 1, 1)), // Diagonal
2469 normalize(make_vec3(1, -1, 0)), // Diagonal in XY plane
2470 normalize(make_vec3(-1, 0, 1)) // Negative X, positive Z
2471 };
2472
2473 for (const auto &direction: test_directions) {
2474 std::vector<vec3> ray_origins;
2475 std::vector<vec3> ray_directions;
2476
2477 // Start ray from outside grid
2478 vec3 start_point = grid_center - direction * 10.0f;
2479 ray_origins.push_back(start_point);
2480 ray_directions.push_back(direction);
2481
2482 collision.calculateVoxelRayPathLengths(grid_center, grid_size, grid_divisions, ray_origins, ray_directions);
2483
2484 // Should find intersections for most directions through the center
2485 bool found_intersections = false;
2486 for (int i = 0; i < grid_divisions.x; i++) {
2487 for (int j = 0; j < grid_divisions.y; j++) {
2488 for (int k = 0; k < grid_divisions.z; k++) {
2489 int P_denom, P_trans;
2490 collision.getVoxelTransmissionProbability(make_int3(i, j, k), P_denom, P_trans);
2491 if (P_denom > 0) {
2492 found_intersections = true;
2493 float r_bar = collision.getVoxelRbar(make_int3(i, j, k));
2494 DOCTEST_CHECK(r_bar > 0.0f);
2495 }
2496 }
2497 }
2498 }
2499
2500 DOCTEST_CHECK(found_intersections);
2501 collision.clearVoxelData();
2502 }
2503}
2504
2505DOCTEST_TEST_CASE("CollisionDetection Voxel Ray Path Length - GPU/CPU Consistency") {
2506 Context context;
2507 CollisionDetection collision(&context);
2508 collision.disableMessages();
2509
2510 vec3 grid_center(0, 0, 0);
2511 vec3 grid_size(6, 6, 6);
2512 int3 grid_divisions(3, 3, 3);
2513
2514 // Create test rays
2515 std::vector<vec3> ray_origins;
2516 std::vector<vec3> ray_directions;
2517
2518 for (int i = 0; i < 5; i++) {
2519 ray_origins.push_back(make_vec3(i - 2.0f, 0, -10));
2520 ray_directions.push_back(make_vec3(0, 0, 1));
2521 }
2522
2523 // Test CPU implementation first
2524 collision.disableGPUAcceleration();
2525 collision.calculateVoxelRayPathLengths(grid_center, grid_size, grid_divisions, ray_origins, ray_directions);
2526
2527 // Store CPU results
2528 std::vector<std::vector<std::vector<std::pair<int, float>>>> cpu_results(grid_divisions.x);
2529 for (int i = 0; i < grid_divisions.x; i++) {
2530 cpu_results[i].resize(grid_divisions.y);
2531 for (int j = 0; j < grid_divisions.y; j++) {
2532 cpu_results[i][j].resize(grid_divisions.z);
2533 for (int k = 0; k < grid_divisions.z; k++) {
2534 int P_denom, P_trans;
2535 collision.getVoxelTransmissionProbability(make_int3(i, j, k), P_denom, P_trans);
2536 float r_bar = collision.getVoxelRbar(make_int3(i, j, k));
2537 cpu_results[i][j][k] = std::make_pair(P_denom, r_bar);
2538 }
2539 }
2540 }
2541
2542 // Test GPU implementation
2543 {
2544 helios::capture_cerr capture;
2545 collision.enableGPUAcceleration();
2546 collision.clearVoxelData();
2547 collision.calculateVoxelRayPathLengths(grid_center, grid_size, grid_divisions, ray_origins, ray_directions);
2548 } // Capture destroyed before assertions
2549
2550 // Compare GPU results with CPU results
2551 for (int i = 0; i < grid_divisions.x; i++) {
2552 for (int j = 0; j < grid_divisions.y; j++) {
2553 for (int k = 0; k < grid_divisions.z; k++) {
2554 int P_denom_gpu, P_trans_gpu;
2555 collision.getVoxelTransmissionProbability(make_int3(i, j, k), P_denom_gpu, P_trans_gpu);
2556 float r_bar_gpu = collision.getVoxelRbar(make_int3(i, j, k));
2557
2558 int P_denom_cpu = cpu_results[i][j][k].first;
2559 float r_bar_cpu = cpu_results[i][j][k].second;
2560
2561 // Results should match within reasonable tolerance
2562 // Allow some tolerance for different algorithms (GPU brute-force vs CPU DDA)
2563 DOCTEST_CHECK(abs(P_denom_gpu - P_denom_cpu) <= 1);
2564 if (r_bar_cpu > 0 && r_bar_gpu > 0) {
2565 DOCTEST_CHECK(std::abs(r_bar_gpu - r_bar_cpu) < 1e-4f);
2566 }
2567 }
2568 }
2569 }
2570}
2571
2572DOCTEST_TEST_CASE("CollisionDetection Voxel Ray Path Length - Parameter Validation") {
2573 Context context;
2574 CollisionDetection collision(&context);
2575 collision.disableMessages();
2576
2577 // Test 1: Negative grid size
2578 vec3 grid_center(0, 0, 0);
2579 vec3 negative_size(-5, 5, 5);
2580 int3 grid_divisions(2, 2, 2);
2581 std::vector<vec3> ray_origins = {make_vec3(0, 0, -10)};
2582 std::vector<vec3> ray_directions = {make_vec3(0, 0, 1)};
2583
2584 // Should handle gracefully without crashing
2585 try {
2586 collision.calculateVoxelRayPathLengths(grid_center, negative_size, grid_divisions, ray_origins, ray_directions);
2587 // If it doesn't throw, verify voxel data is still accessible
2588 int P_denom, P_trans;
2589 collision.getVoxelTransmissionProbability(make_int3(0, 0, 0), P_denom, P_trans);
2590 DOCTEST_CHECK(P_denom >= 0);
2591 DOCTEST_CHECK(P_trans >= 0);
2592 } catch (const std::exception &e) {
2593 // Exception is acceptable for invalid parameters
2594 DOCTEST_CHECK(true);
2595 }
2596
2597 // Test 2: Zero grid divisions
2598 int3 zero_divisions(0, 2, 2);
2599 try {
2600 collision.calculateVoxelRayPathLengths(grid_center, make_vec3(5, 5, 5), zero_divisions, ray_origins, ray_directions);
2601 // If no exception, verify it handles gracefully by checking voxel access behavior
2602 int P_denom_zero, P_trans_zero;
2603 try {
2604 collision.getVoxelTransmissionProbability(make_int3(0, 0, 0), P_denom_zero, P_trans_zero);
2605 // Either returns valid data or the getter throws - both are acceptable
2606 DOCTEST_CHECK(P_denom_zero >= 0);
2607 } catch (const std::exception &inner_e) {
2608 // Exception on voxel access is acceptable for zero divisions
2609 DOCTEST_CHECK(true);
2610 }
2611 } catch (const std::exception &e) {
2612 DOCTEST_CHECK(true); // Exception is also expected behavior
2613 }
2614
2615 // Test 3: Mismatched ray vector sizes
2616 std::vector<vec3> mismatched_directions = {make_vec3(0, 0, 1), make_vec3(1, 0, 0)};
2617 try {
2618 collision.calculateVoxelRayPathLengths(grid_center, make_vec3(5, 5, 5), make_int3(2, 2, 2), ray_origins, mismatched_directions);
2619 DOCTEST_CHECK(false); // Should throw an exception
2620 } catch (const std::exception &e) {
2621 DOCTEST_CHECK(true); // Expected behavior
2622 }
2623
2624 // Test 4: Invalid P_trans > P_denom
2625 vec3 valid_grid_size(4, 4, 4);
2626 int3 valid_divisions(2, 2, 2);
2627 collision.calculateVoxelRayPathLengths(grid_center, valid_grid_size, valid_divisions, ray_origins, ray_directions);
2628
2629 // This should be allowed - implementation may handle it gracefully
2630 collision.setVoxelTransmissionProbability(10, 15, make_int3(0, 0, 0)); // P_trans > P_denom
2631 int test_P_denom, test_P_trans;
2632 collision.getVoxelTransmissionProbability(make_int3(0, 0, 0), test_P_denom, test_P_trans);
2633 DOCTEST_CHECK(test_P_denom == 10);
2634 DOCTEST_CHECK(test_P_trans == 15);
2635
2636 // Test 5: Negative values for transmission probability
2637 collision.setVoxelTransmissionProbability(-5, -3, make_int3(0, 0, 0));
2638 collision.getVoxelTransmissionProbability(make_int3(0, 0, 0), test_P_denom, test_P_trans);
2639 DOCTEST_CHECK(test_P_denom == -5);
2640 DOCTEST_CHECK(test_P_trans == -3);
2641}
2642
2643DOCTEST_TEST_CASE("CollisionDetection Voxel Ray Path Length - Mathematical Validation") {
2644 Context context;
2645 CollisionDetection collision(&context);
2646 collision.disableMessages();
2647
2648 // Test 1: Single ray through single voxel - analytical solution
2649 vec3 grid_center(0, 0, 0);
2650 vec3 grid_size(2, 2, 2); // 2x2x2 cube
2651 int3 grid_divisions(1, 1, 1); // Single voxel
2652
2653 // Ray passes straight through center
2654 std::vector<vec3> ray_origins = {make_vec3(0, 0, -5)};
2655 std::vector<vec3> ray_directions = {make_vec3(0, 0, 1)};
2656
2657 collision.calculateVoxelRayPathLengths(grid_center, grid_size, grid_divisions, ray_origins, ray_directions);
2658
2659 float r_bar = collision.getVoxelRbar(make_int3(0, 0, 0));
2660
2661 // Expected path length through 2x2x2 cube should be 2.0 (cube height)
2662 DOCTEST_CHECK(std::abs(r_bar - 2.0f) < 0.1f);
2663
2664 // Test 2: Diagonal ray through cubic voxel
2665 collision.clearVoxelData();
2666 std::vector<vec3> diagonal_origins = {make_vec3(-2, -2, -2)};
2667 std::vector<vec3> diagonal_directions = {normalize(make_vec3(1, 1, 1))};
2668
2669 collision.calculateVoxelRayPathLengths(grid_center, grid_size, grid_divisions, diagonal_origins, diagonal_directions);
2670
2671 float diagonal_r_bar = collision.getVoxelRbar(make_int3(0, 0, 0));
2672
2673 // Expected diagonal through 2x2x2 cube should be sqrt(3)*2 = ~3.46
2674 float expected_diagonal = std::sqrt(3.0f) * 2.0f;
2675 DOCTEST_CHECK(std::abs(diagonal_r_bar - expected_diagonal) < 0.2f);
2676
2677 // Test 3: Multiple rays, verify statistical consistency
2678 collision.clearVoxelData();
2679 std::vector<vec3> multi_origins;
2680 std::vector<vec3> multi_directions;
2681
2682 // Create 4 parallel rays through same voxel
2683 for (int i = 0; i < 4; i++) {
2684 multi_origins.push_back(make_vec3(0.5f * i - 0.75f, 0, -5));
2685 multi_directions.push_back(make_vec3(0, 0, 1));
2686 }
2687
2688 collision.calculateVoxelRayPathLengths(grid_center, grid_size, grid_divisions, multi_origins, multi_directions);
2689
2690 int P_denom, P_trans;
2691 collision.getVoxelTransmissionProbability(make_int3(0, 0, 0), P_denom, P_trans);
2692 float multi_r_bar = collision.getVoxelRbar(make_int3(0, 0, 0));
2693
2694 // Should have 4 rays hitting the voxel
2695 DOCTEST_CHECK(P_denom == 4);
2696 // All rays should pass through (assuming no geometry), so P_trans should equal P_denom
2697 DOCTEST_CHECK(P_trans == P_denom);
2698 // Average path length should still be ~2.0
2699 DOCTEST_CHECK(std::abs(multi_r_bar - 2.0f) < 0.1f);
2700}
2701
2702DOCTEST_TEST_CASE("CollisionDetection Voxel Ray Path Length - Numerical Precision") {
2703 Context context;
2704 CollisionDetection collision(&context);
2705 collision.disableMessages();
2706
2707 // Test 1: Very small grid (precision test)
2708 vec3 tiny_center(0, 0, 0);
2709 vec3 tiny_size(0.001f, 0.001f, 0.001f);
2710 int3 tiny_divisions(1, 1, 1);
2711
2712 std::vector<vec3> tiny_origins = {make_vec3(0, 0, -0.01f)};
2713 std::vector<vec3> tiny_directions = {make_vec3(0, 0, 1)};
2714
2715 collision.calculateVoxelRayPathLengths(tiny_center, tiny_size, tiny_divisions, tiny_origins, tiny_directions);
2716 float tiny_r_bar = collision.getVoxelRbar(make_int3(0, 0, 0));
2717
2718 // Should handle small values without underflow
2719 DOCTEST_CHECK(tiny_r_bar > 0.0f);
2720 DOCTEST_CHECK(tiny_r_bar < 0.1f); // Should be on order of tiny_size
2721
2722 // Test 2: Large grid (overflow test)
2723 vec3 large_center(0, 0, 0);
2724 vec3 large_size(1000.0f, 1000.0f, 1000.0f);
2725 int3 large_divisions(2, 2, 2);
2726
2727 std::vector<vec3> large_origins = {make_vec3(0, 0, -2000)};
2728 std::vector<vec3> large_directions = {make_vec3(0, 0, 1)};
2729
2730 collision.calculateVoxelRayPathLengths(large_center, large_size, large_divisions, large_origins, large_directions);
2731 float large_r_bar = collision.getVoxelRbar(make_int3(1, 1, 1)); // Center voxel
2732
2733 // Should handle large values without overflow
2734 DOCTEST_CHECK(large_r_bar > 0.0f);
2735 DOCTEST_CHECK(large_r_bar < 2000.0f); // Should be reasonable
2736
2737 // Test 3: CPU/GPU precision comparison with tight tolerance
2738 collision.clearVoxelData();
2739 vec3 precision_center(0, 0, 0);
2740 vec3 precision_size(10, 10, 10);
2741 int3 precision_divisions(5, 5, 5);
2742
2743 std::vector<vec3> precision_origins;
2744 std::vector<vec3> precision_directions;
2745 for (int i = 0; i < 10; i++) {
2746 precision_origins.push_back(make_vec3(i - 5.0f, 0, -20));
2747 precision_directions.push_back(make_vec3(0, 0, 1));
2748 }
2749
2750 // CPU calculation
2751 collision.disableGPUAcceleration();
2752 collision.calculateVoxelRayPathLengths(precision_center, precision_size, precision_divisions, precision_origins, precision_directions);
2753
2754 // Store CPU results with higher precision
2755 std::vector<float> cpu_rbars;
2756 for (int i = 0; i < precision_divisions.x; i++) {
2757 for (int j = 0; j < precision_divisions.y; j++) {
2758 for (int k = 0; k < precision_divisions.z; k++) {
2759 cpu_rbars.push_back(collision.getVoxelRbar(make_int3(i, j, k)));
2760 }
2761 }
2762 }
2763
2764 // GPU calculation
2765 {
2766 helios::capture_cerr capture;
2767 collision.enableGPUAcceleration();
2768 collision.clearVoxelData();
2769 collision.calculateVoxelRayPathLengths(precision_center, precision_size, precision_divisions, precision_origins, precision_directions);
2770 } // Capture destroyed before assertions
2771
2772 // Compare with tighter tolerance than existing test
2773 int idx = 0;
2774 for (int i = 0; i < precision_divisions.x; i++) {
2775 for (int j = 0; j < precision_divisions.y; j++) {
2776 for (int k = 0; k < precision_divisions.z; k++) {
2777 float gpu_rbar = collision.getVoxelRbar(make_int3(i, j, k));
2778 float cpu_rbar = cpu_rbars[idx++];
2779
2780 if (cpu_rbar > 0 && gpu_rbar > 0) {
2781 float relative_error = std::abs(gpu_rbar - cpu_rbar) / std::max(cpu_rbar, gpu_rbar);
2782 DOCTEST_CHECK(relative_error < 1e-5f); // Tighter precision requirement
2783 }
2784 }
2785 }
2786 }
2787}
2788
2789DOCTEST_TEST_CASE("CollisionDetection Voxel Ray Path Length - Error Recovery and State Management") {
2790 Context context;
2791 CollisionDetection collision(&context);
2792 collision.disableMessages();
2793
2794 // Test 1: API calls before initialization should handle gracefully
2795 try {
2796 int P_denom, P_trans;
2797 collision.getVoxelTransmissionProbability(make_int3(0, 0, 0), P_denom, P_trans);
2798 // Should return zeros for uninitialized data
2799 DOCTEST_CHECK(P_denom == 0);
2800 DOCTEST_CHECK(P_trans == 0);
2801 } catch (const std::exception &e) {
2802 // Exception is also acceptable
2803 DOCTEST_CHECK(true);
2804 }
2805
2806 try {
2807 float r_bar = collision.getVoxelRbar(make_int3(0, 0, 0));
2808 // Should return zero for uninitialized data
2809 DOCTEST_CHECK(r_bar == 0.0f);
2810 } catch (const std::exception &e) {
2811 // Exception is also acceptable
2812 DOCTEST_CHECK(true);
2813 }
2814
2815 // Test 2: Multiple initialization cycles
2816 vec3 grid_center(0, 0, 0);
2817 vec3 grid_size(4, 4, 4);
2818 int3 grid_divisions(2, 2, 2);
2819 std::vector<vec3> ray_origins = {make_vec3(0, 0, -10)};
2820 std::vector<vec3> ray_directions = {make_vec3(0, 0, 1)};
2821
2822 // Initialize multiple times - should handle gracefully
2823 for (int cycle = 0; cycle < 3; cycle++) {
2824 collision.calculateVoxelRayPathLengths(grid_center, grid_size, grid_divisions, ray_origins, ray_directions);
2825
2826 // Verify data is accessible
2827 int P_denom, P_trans;
2828 collision.getVoxelTransmissionProbability(make_int3(0, 0, 0), P_denom, P_trans);
2829 DOCTEST_CHECK(P_denom >= 0);
2830
2831 float r_bar = collision.getVoxelRbar(make_int3(0, 0, 0));
2832 DOCTEST_CHECK(r_bar >= 0);
2833
2834 // Clear and reinitialize
2835 collision.clearVoxelData();
2836 }
2837
2838 // Test 3: State consistency after errors
2839 try {
2840 // Attempt invalid operation
2841 collision.setVoxelTransmissionProbability(10, 5, make_int3(-1, 0, 0)); // Invalid index
2842 DOCTEST_CHECK(false); // Should throw
2843 } catch (const std::exception &e) {
2844 // After error, system should still be usable
2845 collision.calculateVoxelRayPathLengths(grid_center, grid_size, grid_divisions, ray_origins, ray_directions);
2846
2847 int P_denom, P_trans;
2848 collision.getVoxelTransmissionProbability(make_int3(0, 0, 0), P_denom, P_trans);
2849 DOCTEST_CHECK(P_denom >= 0);
2850 }
2851}
2852
2853DOCTEST_TEST_CASE("CollisionDetection Voxel Ray Path Length - Memory and Performance Stress") {
2854 Context context;
2855 CollisionDetection collision(&context);
2856 collision.disableMessages();
2857
2858 // Test 1: Moderately large grid (memory test)
2859 vec3 stress_center(0, 0, 0);
2860 vec3 stress_size(50, 50, 50);
2861 int3 stress_divisions(10, 10, 10); // 1000 voxels
2862
2863 // Create many rays
2864 std::vector<vec3> stress_origins;
2865 std::vector<vec3> stress_directions;
2866 for (int i = 0; i < 100; i++) {
2867 for (int j = 0; j < 10; j++) {
2868 stress_origins.push_back(make_vec3(i - 50.0f, j - 5.0f, -100));
2869 stress_directions.push_back(make_vec3(0, 0, 1));
2870 }
2871 }
2872
2873 // Should handle 1000 rays x 1000 voxels without issues
2874 auto start_time = std::chrono::high_resolution_clock::now();
2875 collision.calculateVoxelRayPathLengths(stress_center, stress_size, stress_divisions, stress_origins, stress_directions);
2876 auto end_time = std::chrono::high_resolution_clock::now();
2877 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
2878
2879 // Verify computation completed successfully
2880 bool found_data = false;
2881 for (int i = 0; i < stress_divisions.x && !found_data; i++) {
2882 for (int j = 0; j < stress_divisions.y && !found_data; j++) {
2883 for (int k = 0; k < stress_divisions.z && !found_data; k++) {
2884 int P_denom, P_trans;
2885 collision.getVoxelTransmissionProbability(make_int3(i, j, k), P_denom, P_trans);
2886 if (P_denom > 0) {
2887 found_data = true;
2888 DOCTEST_CHECK(P_trans <= P_denom);
2889 float r_bar = collision.getVoxelRbar(make_int3(i, j, k));
2890 DOCTEST_CHECK(r_bar > 0.0f);
2891 }
2892 }
2893 }
2894 }
2895 DOCTEST_CHECK(found_data);
2896
2897 // Test 2: Memory cleanup validation
2898 collision.clearVoxelData();
2899
2900 // After clear, should return default values
2901 int P_denom, P_trans;
2902 collision.getVoxelTransmissionProbability(make_int3(0, 0, 0), P_denom, P_trans);
2903 DOCTEST_CHECK(P_denom == 0);
2904 DOCTEST_CHECK(P_trans == 0);
2905
2906 float r_bar = collision.getVoxelRbar(make_int3(0, 0, 0));
2907 DOCTEST_CHECK(r_bar == 0.0f);
2908
2909 // Test 3: Repeated allocation/deallocation cycles
2910 for (int cycle = 0; cycle < 5; cycle++) {
2911 vec3 cycle_size(8 + cycle * 2, 8 + cycle * 2, 8 + cycle * 2);
2912 int3 cycle_divisions(2 + cycle, 2 + cycle, 2 + cycle);
2913
2914 std::vector<vec3> cycle_origins = {make_vec3(0, 0, -20)};
2915 std::vector<vec3> cycle_directions = {make_vec3(0, 0, 1)};
2916
2917 collision.calculateVoxelRayPathLengths(stress_center, cycle_size, cycle_divisions, cycle_origins, cycle_directions);
2918
2919 // Verify some data exists
2920 collision.getVoxelTransmissionProbability(make_int3(0, 0, 0), P_denom, P_trans);
2921 DOCTEST_CHECK(P_denom >= 0);
2922
2923 collision.clearVoxelData();
2924 }
2925}
2926
2927DOCTEST_TEST_CASE("CollisionDetection Voxel Ray Path Length - Integration with BVH") {
2928 Context context;
2929 CollisionDetection collision(&context);
2930 collision.disableMessages();
2931
2932 // Create some geometry to ensure BVH is built
2933 std::vector<uint> sphere_UUIDs = context.addSphere(10, make_vec3(0, 0, 0), 1.0f);
2934
2935 // Build BVH before calling voxel calculations to avoid thread safety issues
2936 collision.buildBVH(sphere_UUIDs);
2937
2938 // Test 1: Voxel calculations with existing geometry
2939 vec3 grid_center(0, 0, 0);
2940 vec3 grid_size(10, 10, 10);
2941 int3 grid_divisions(5, 5, 5);
2942
2943 std::vector<vec3> ray_origins;
2944 std::vector<vec3> ray_directions;
2945 for (int i = 0; i < 8; i++) {
2946 ray_origins.push_back(make_vec3(i - 4.0f, 0, -15));
2947 ray_directions.push_back(make_vec3(0, 0, 1));
2948 }
2949
2950 // Should work normally even with geometry in context
2951 collision.calculateVoxelRayPathLengths(grid_center, grid_size, grid_divisions, ray_origins, ray_directions);
2952
2953 // Verify data is accessible
2954 bool found_voxel_data = false;
2955 for (int i = 0; i < grid_divisions.x; i++) {
2956 for (int j = 0; j < grid_divisions.y; j++) {
2957 for (int k = 0; k < grid_divisions.z; k++) {
2958 int P_denom, P_trans;
2959 collision.getVoxelTransmissionProbability(make_int3(i, j, k), P_denom, P_trans);
2960 if (P_denom > 0) {
2961 found_voxel_data = true;
2962 DOCTEST_CHECK(P_trans >= 0);
2963 DOCTEST_CHECK(P_trans <= P_denom);
2964
2965 float r_bar = collision.getVoxelRbar(make_int3(i, j, k));
2966 DOCTEST_CHECK(r_bar >= 0.0f);
2967 }
2968 }
2969 }
2970 }
2971 DOCTEST_CHECK(found_voxel_data);
2972
2973 // Test 2: Verify collision detection still works after voxel calculations
2974 std::vector<uint> collision_results = collision.findCollisions(sphere_UUIDs[0]);
2975 DOCTEST_CHECK(collision_results.size() >= 0); // Should execute without error
2976
2977 // Test 3: Interleaved operations
2978 collision.clearVoxelData();
2979
2980 // Add more geometry
2981 std::vector<uint> triangle_UUIDs;
2982 triangle_UUIDs.push_back(context.addTriangle(make_vec3(5, 0, 0), make_vec3(6, 1, 0), make_vec3(6, 0, 1)));
2983
2984 // Recalculate voxels
2985 collision.calculateVoxelRayPathLengths(grid_center, grid_size, grid_divisions, ray_origins, ray_directions);
2986
2987 // Find collisions with new geometry
2988 std::vector<uint> new_collisions = collision.findCollisions(triangle_UUIDs);
2989 DOCTEST_CHECK(new_collisions.size() >= 0);
2990
2991 // Verify voxel data is still valid
2992 int final_P_denom, final_P_trans;
2993 collision.getVoxelTransmissionProbability(make_int3(2, 2, 2), final_P_denom, final_P_trans);
2994 DOCTEST_CHECK(final_P_denom >= 0);
2995 DOCTEST_CHECK(final_P_trans >= 0);
2996}
2997
2998DOCTEST_TEST_CASE("CollisionDetection Voxel Ray Path Length - Edge Case Ray Geometries") {
2999 Context context;
3000 CollisionDetection collision(&context);
3001 collision.disableMessages();
3002
3003 vec3 grid_center(0, 0, 0);
3004 vec3 grid_size(4, 4, 4);
3005 int3 grid_divisions(2, 2, 2);
3006
3007 // Test 1: Ray parallel to voxel face (grazing)
3008 std::vector<vec3> grazing_origins = {make_vec3(-3, 2.0f, 0)}; // At grid boundary
3009 std::vector<vec3> grazing_directions = {make_vec3(1, 0, 0)}; // Parallel to face
3010
3011 collision.calculateVoxelRayPathLengths(grid_center, grid_size, grid_divisions, grazing_origins, grazing_directions);
3012
3013 // Should handle gracefully - may or may not intersect
3014 bool found_grazing_intersection = false;
3015 for (int i = 0; i < grid_divisions.x; i++) {
3016 for (int j = 0; j < grid_divisions.y; j++) {
3017 for (int k = 0; k < grid_divisions.z; k++) {
3018 int P_denom, P_trans;
3019 collision.getVoxelTransmissionProbability(make_int3(i, j, k), P_denom, P_trans);
3020 if (P_denom > 0) {
3021 found_grazing_intersection = true;
3022 float r_bar = collision.getVoxelRbar(make_int3(i, j, k));
3023 DOCTEST_CHECK(r_bar >= 0.0f);
3024 }
3025 }
3026 }
3027 }
3028
3029 // Test 2: Ray touching corner/edge
3030 collision.clearVoxelData();
3031 std::vector<vec3> corner_origins = {make_vec3(-3, -3, -3)};
3032 std::vector<vec3> corner_directions = {normalize(make_vec3(1, 1, 1))};
3033
3034 collision.calculateVoxelRayPathLengths(grid_center, grid_size, grid_divisions, corner_origins, corner_directions);
3035
3036 bool found_corner_intersection = false;
3037 for (int i = 0; i < grid_divisions.x; i++) {
3038 for (int j = 0; j < grid_divisions.y; j++) {
3039 for (int k = 0; k < grid_divisions.z; k++) {
3040 int P_denom, P_trans;
3041 collision.getVoxelTransmissionProbability(make_int3(i, j, k), P_denom, P_trans);
3042 if (P_denom > 0) {
3043 found_corner_intersection = true;
3044 float r_bar = collision.getVoxelRbar(make_int3(i, j, k));
3045 DOCTEST_CHECK(r_bar >= 0.0f);
3046 }
3047 }
3048 }
3049 }
3050
3051 // Test 3: Rays with very small direction components (near-zero)
3052 collision.clearVoxelData();
3053 std::vector<vec3> near_zero_origins = {make_vec3(0, 0, -5)};
3054 std::vector<vec3> near_zero_directions = {normalize(make_vec3(1e-6f, 1e-6f, 1.0f))};
3055
3056 collision.calculateVoxelRayPathLengths(grid_center, grid_size, grid_divisions, near_zero_origins, near_zero_directions);
3057
3058 // Should handle without numerical issues
3059 bool found_near_zero_intersection = false;
3060 for (int i = 0; i < grid_divisions.x; i++) {
3061 for (int j = 0; j < grid_divisions.y; j++) {
3062 for (int k = 0; k < grid_divisions.z; k++) {
3063 int P_denom, P_trans;
3064 collision.getVoxelTransmissionProbability(make_int3(i, j, k), P_denom, P_trans);
3065 if (P_denom > 0) {
3066 found_near_zero_intersection = true;
3067 float r_bar = collision.getVoxelRbar(make_int3(i, j, k));
3068 DOCTEST_CHECK(r_bar >= 0.0f);
3069 DOCTEST_CHECK(std::isfinite(r_bar)); // Check for NaN/inf
3070 }
3071 }
3072 }
3073 }
3074}
3075
3076// -------- GENERIC RAY-TRACING TESTS --------
3077
3078DOCTEST_TEST_CASE("CollisionDetection Generic Ray Casting - Basic Functionality") {
3079 Context context;
3080 CollisionDetection collision(&context);
3081 collision.disableMessages();
3082
3083 // Create simple test geometry - triangle
3084 vec3 v0 = make_vec3(0, 0, 0);
3085 vec3 v1 = make_vec3(1, 0, 0);
3086 vec3 v2 = make_vec3(0.5f, 0, 1);
3087 uint triangle_uuid = context.addTriangle(v0, v1, v2);
3088
3089 // Test 1: Hit test - ray intersecting triangle
3090 vec3 ray_origin = make_vec3(0.5f, -1, 0.5f);
3091 vec3 ray_direction = normalize(make_vec3(0, 1, 0));
3092
3093 CollisionDetection::CollisionDetection::HitResult result = collision.castRay(ray_origin, ray_direction);
3094
3095 DOCTEST_CHECK(result.hit == true);
3096 DOCTEST_CHECK(result.primitive_UUID == triangle_uuid);
3097 DOCTEST_CHECK(result.distance > 0.9f);
3098 DOCTEST_CHECK(result.distance < 1.1f);
3099
3100 // Check intersection point is reasonable
3101 DOCTEST_CHECK(result.intersection_point.x > 0.4f);
3102 DOCTEST_CHECK(result.intersection_point.x < 0.6f);
3103 DOCTEST_CHECK(std::abs(result.intersection_point.y) < 1e-5f);
3104 DOCTEST_CHECK(result.intersection_point.z > 0.4f);
3105 DOCTEST_CHECK(result.intersection_point.z < 0.6f);
3106
3107 // Test 2: Miss test - ray not intersecting triangle
3108 vec3 miss_origin = make_vec3(2, -1, 0.5f);
3109 vec3 miss_direction = normalize(make_vec3(0, 1, 0));
3110
3111 CollisionDetection::HitResult miss_result = collision.castRay(miss_origin, miss_direction);
3112 DOCTEST_CHECK(miss_result.hit == false);
3113 DOCTEST_CHECK(miss_result.distance < 0);
3114
3115 // Test 3: Max distance constraint
3116 vec3 limited_origin = make_vec3(0.5f, -2, 0.5f);
3117 vec3 limited_direction = normalize(make_vec3(0, 1, 0));
3118 float max_distance = 1.5f; // Ray would need ~2 units to reach triangle
3119
3120 CollisionDetection::HitResult limited_result = collision.castRay(limited_origin, limited_direction, max_distance);
3121 DOCTEST_CHECK(limited_result.hit == false);
3122}
3123
3124DOCTEST_TEST_CASE("CollisionDetection Generic Ray Casting - CollisionDetection::RayQuery Structure") {
3125 Context context;
3126 CollisionDetection collision(&context);
3127 collision.disableMessages();
3128
3129 // Create test geometry
3130 uint triangle1 = context.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0.5f, 0, 1));
3131 uint triangle2 = context.addTriangle(make_vec3(2, 0, 0), make_vec3(3, 0, 0), make_vec3(2.5f, 0, 1));
3132
3133 // Test 1: CollisionDetection::RayQuery with default constructor
3135 query1.origin = make_vec3(0.5f, -1, 0.5f);
3136 query1.direction = normalize(make_vec3(0, 1, 0));
3137
3138 CollisionDetection::HitResult result1 = collision.castRay(query1);
3139 DOCTEST_CHECK(result1.hit == true);
3140
3141 // Test 2: CollisionDetection::RayQuery with full constructor
3142 CollisionDetection::RayQuery query2(make_vec3(2.5f, -1, 0.5f), normalize(make_vec3(0, 1, 0)), -1.0f, {triangle2});
3143
3144 CollisionDetection::HitResult result2 = collision.castRay(query2);
3145 DOCTEST_CHECK(result2.hit == true);
3146 DOCTEST_CHECK(result2.primitive_UUID == triangle2);
3147
3148 // Test 3: Target UUID filtering - should only hit triangle1
3149 CollisionDetection::RayQuery query3(make_vec3(0.5f, -1, 0.5f), normalize(make_vec3(0, 1, 0)), -1.0f, {triangle1});
3150
3151 CollisionDetection::HitResult result3 = collision.castRay(query3);
3152 DOCTEST_CHECK(result3.hit == true);
3153 DOCTEST_CHECK(result3.primitive_UUID == triangle1);
3154
3155 // Test 4: Target UUID filtering with non-intersecting primitive
3156 CollisionDetection::RayQuery query4(make_vec3(0.5f, -1, 0.5f), normalize(make_vec3(0, 1, 0)), -1.0f, {triangle2});
3157
3158 CollisionDetection::HitResult result4 = collision.castRay(query4);
3159 DOCTEST_CHECK(result4.hit == false);
3160}
3161
3162DOCTEST_TEST_CASE("CollisionDetection Batch Ray Casting") {
3163 Context context;
3164 CollisionDetection collision(&context);
3165 collision.disableMessages();
3166
3167 // Create test geometry
3168 uint triangle = context.addTriangle(make_vec3(-1, 0, -1), make_vec3(1, 0, -1), make_vec3(0, 0, 1));
3169
3170
3171 // Create multiple ray queries
3172 std::vector<CollisionDetection::RayQuery> queries;
3173 queries.push_back(CollisionDetection::RayQuery(make_vec3(0, -1, 0), normalize(make_vec3(0, 1, 0)))); // Hit
3174 queries.push_back(CollisionDetection::RayQuery(make_vec3(2, -1, 0), normalize(make_vec3(0, 1, 0)))); // Miss
3175 queries.push_back(CollisionDetection::RayQuery(make_vec3(-0.5f, -1, 0), normalize(make_vec3(0, 1, 0)))); // Hit
3176 queries.push_back(CollisionDetection::RayQuery(make_vec3(0.5f, -1, 0), normalize(make_vec3(0, 1, 0)))); // Hit
3177
3178 // Test batch casting with statistics
3180 std::vector<CollisionDetection::HitResult> results = collision.castRays(queries, &stats);
3181
3182
3183 DOCTEST_CHECK(results.size() == 4);
3184 DOCTEST_CHECK(stats.total_rays_cast == 4);
3185 DOCTEST_CHECK(stats.total_hits == 3);
3186 DOCTEST_CHECK(stats.average_ray_distance > 0);
3187
3188 // Verify individual results
3189 DOCTEST_CHECK(results[0].hit == true); // Center hit
3190 DOCTEST_CHECK(results[1].hit == false); // Miss
3191 DOCTEST_CHECK(results[2].hit == true); // Left hit
3192 DOCTEST_CHECK(results[3].hit == true); // Right hit
3193
3194 // All hits should be on the same triangle
3195 for (const auto &result: results) {
3196 if (result.hit) {
3197 DOCTEST_CHECK(result.primitive_UUID == triangle);
3198 DOCTEST_CHECK(result.distance > 0);
3199 }
3200 }
3201}
3202
3203DOCTEST_TEST_CASE("CollisionDetection Grid Ray Intersection") {
3204 Context context;
3205 CollisionDetection collision(&context);
3206 collision.disableMessages();
3207
3208 // Create test geometry - triangle in center
3209 uint triangle = context.addTriangle(make_vec3(-0.5f, 0, -0.5f), make_vec3(0.5f, 0, -0.5f), make_vec3(0, 0, 0.5f));
3210
3211 // Set up grid parameters
3212 vec3 grid_center = make_vec3(0, 0, 0);
3213 vec3 grid_size = make_vec3(4, 4, 4);
3214 int3 grid_divisions = make_int3(2, 2, 2);
3215
3216 // Create rays hitting different voxels
3217 std::vector<CollisionDetection::RayQuery> rays;
3218 rays.push_back(CollisionDetection::RayQuery(make_vec3(0, -2, 0), normalize(make_vec3(0, 1, 0)))); // Center voxel
3219 rays.push_back(CollisionDetection::RayQuery(make_vec3(-0.8f, -2, 0), normalize(make_vec3(0, 1, 0)))); // Different voxel
3220 rays.push_back(CollisionDetection::RayQuery(make_vec3(2, -2, 0), normalize(make_vec3(0, 1, 0)))); // Miss entirely
3221
3222 auto grid_results = collision.performGridRayIntersection(grid_center, grid_size, grid_divisions, rays);
3223
3224 DOCTEST_CHECK(grid_results.size() == 2); // x-divisions
3225 DOCTEST_CHECK(grid_results[0].size() == 2); // y-divisions
3226 DOCTEST_CHECK(grid_results[0][0].size() == 2); // z-divisions
3227
3228 // Count total hits across all voxels
3229 int total_hits = 0;
3230 for (int i = 0; i < 2; i++) {
3231 for (int j = 0; j < 2; j++) {
3232 for (int k = 0; k < 2; k++) {
3233 total_hits += grid_results[i][j][k].size();
3234 }
3235 }
3236 }
3237 DOCTEST_CHECK(total_hits >= 1); // At least one hit should be recorded
3238}
3239
3240DOCTEST_TEST_CASE("CollisionDetection Ray Path Lengths Detailed") {
3241 Context context;
3242 CollisionDetection collision(&context);
3243 collision.disableMessages();
3244
3245 // Create test geometry
3246 uint triangle = context.addTriangle(make_vec3(-1, 0, -1), make_vec3(1, 0, -1), make_vec3(0, 0, 1));
3247
3248 // Set up test parameters
3249 vec3 grid_center = make_vec3(0, 0, 0);
3250 vec3 grid_size = make_vec3(4, 4, 4);
3251 int3 grid_divisions = make_int3(2, 2, 2);
3252
3253 std::vector<vec3> ray_origins = {
3254 make_vec3(0, -2, 0), make_vec3(0.5f, -2, 0), make_vec3(2, -2, 0) // This should miss
3255 };
3256
3257 std::vector<vec3> ray_directions = {normalize(make_vec3(0, 1, 0)), normalize(make_vec3(0, 1, 0)), normalize(make_vec3(0, 1, 0))};
3258
3259 std::vector<CollisionDetection::HitResult> hit_results;
3260 collision.calculateRayPathLengthsDetailed(grid_center, grid_size, grid_divisions, ray_origins, ray_directions, hit_results);
3261
3262 DOCTEST_CHECK(hit_results.size() == 3);
3263
3264 // First two rays should hit
3265 DOCTEST_CHECK(hit_results[0].hit == true);
3266 DOCTEST_CHECK(hit_results[1].hit == true);
3267 DOCTEST_CHECK(hit_results[2].hit == false);
3268
3269 // Hit distances should be reasonable
3270 DOCTEST_CHECK(hit_results[0].distance > 1.5f);
3271 DOCTEST_CHECK(hit_results[0].distance < 2.5f);
3272 DOCTEST_CHECK(hit_results[1].distance > 1.5f);
3273 DOCTEST_CHECK(hit_results[1].distance < 2.5f);
3274
3275 // Verify that existing voxel data is also updated
3276 int P_denom, P_trans;
3277 collision.getVoxelTransmissionProbability(make_int3(0, 0, 0), P_denom, P_trans);
3278 DOCTEST_CHECK(P_denom >= 0);
3279}
3280
3281DOCTEST_TEST_CASE("CollisionDetection Ray Casting - Normal Calculation") {
3282 Context context;
3283 CollisionDetection collision(&context);
3284 collision.disableMessages();
3285
3286 // Test 1: Triangle normal calculation
3287 vec3 v0 = make_vec3(0, 0, 0);
3288 vec3 v1 = make_vec3(1, 0, 0);
3289 vec3 v2 = make_vec3(0, 1, 0);
3290 uint triangle = context.addTriangle(v0, v1, v2);
3291
3292 vec3 ray_origin = make_vec3(0.3f, 0.3f, -1);
3293 vec3 ray_direction = normalize(make_vec3(0, 0, 1));
3294
3295 CollisionDetection::CollisionDetection::HitResult result = collision.castRay(ray_origin, ray_direction);
3296
3297 DOCTEST_CHECK(result.hit == true);
3298
3299 // Normal should point in +Z direction (calculated from cross product)
3300 vec3 expected_normal = normalize(make_vec3(0, 0, 1));
3301 float dot_product = result.normal.x * expected_normal.x + result.normal.y * expected_normal.y + result.normal.z * expected_normal.z;
3302 DOCTEST_CHECK(std::abs(dot_product) > 0.9f); // Should be nearly parallel
3303
3304 // Test 2: Patch normal calculation
3305 uint patch = context.addPatch(make_vec3(0, 0, 2), make_vec2(1, 1));
3306
3307 vec3 patch_ray_origin = make_vec3(0, 0, 1);
3308 vec3 patch_ray_direction = normalize(make_vec3(0, 0, 1));
3309
3310 CollisionDetection::HitResult patch_result = collision.castRay(patch_ray_origin, patch_ray_direction);
3311
3312 DOCTEST_CHECK(patch_result.hit == true);
3313 DOCTEST_CHECK(patch_result.primitive_UUID == patch);
3314
3315 // Patch normal should also be reasonable
3316 DOCTEST_CHECK(patch_result.normal.magnitude() > 0.9f);
3317 DOCTEST_CHECK(patch_result.normal.magnitude() < 1.1f);
3318}
3319
3320DOCTEST_TEST_CASE("CollisionDetection Ray Casting - Edge Cases and Error Handling") {
3321 Context context;
3322 CollisionDetection collision(&context);
3323 collision.disableMessages();
3324
3325 // Create test triangle
3326 uint triangle = context.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0.5f, 0, 1));
3327
3328 // Test 1: Zero direction vector (should be handled)
3329 vec3 zero_direction = make_vec3(0, 0, 0);
3330 CollisionDetection::HitResult zero_result = collision.castRay(make_vec3(0, -1, 0), zero_direction);
3331 DOCTEST_CHECK(zero_result.hit == false); // Should handle gracefully
3332
3333 // Test 2: Very small direction vector (should normalize)
3334 vec3 tiny_direction = make_vec3(1e-8f, 1e-8f, 1e-8f);
3335 CollisionDetection::HitResult tiny_result = collision.castRay(make_vec3(0.5f, -1, 0.5f), tiny_direction);
3336 // Should either hit or miss gracefully, not crash
3337
3338 // Test 3: Infinite max distance
3339 CollisionDetection::HitResult inf_result = collision.castRay(make_vec3(0.5f, -1, 0.5f), normalize(make_vec3(0, 1, 0)), std::numeric_limits<float>::infinity());
3340 DOCTEST_CHECK(inf_result.hit == true);
3341
3342 // Test 4: Negative max distance (should be treated as infinite)
3343 CollisionDetection::HitResult neg_result = collision.castRay(make_vec3(0.5f, -1, 0.5f), normalize(make_vec3(0, 1, 0)), -5.0f);
3344 DOCTEST_CHECK(neg_result.hit == true);
3345
3346 // Test 5: Empty target UUIDs (should use all primitives)
3347 std::vector<uint> empty_targets;
3348 CollisionDetection::HitResult empty_result = collision.castRay(make_vec3(0.5f, -1, 0.5f), normalize(make_vec3(0, 1, 0)), -1.0f, empty_targets);
3349 DOCTEST_CHECK(empty_result.hit == true);
3350
3351 // Test 6: Invalid target UUIDs (should be filtered out)
3352 std::vector<uint> invalid_targets = {99999, triangle, 88888};
3353 CollisionDetection::HitResult invalid_result = collision.castRay(make_vec3(0.5f, -1, 0.5f), normalize(make_vec3(0, 1, 0)), -1.0f, invalid_targets);
3354 DOCTEST_CHECK(invalid_result.hit == true);
3355 DOCTEST_CHECK(invalid_result.primitive_UUID == triangle);
3356}
3357
3358DOCTEST_TEST_CASE("CollisionDetection Ray Casting - Performance and Scalability") {
3359 Context context;
3360 CollisionDetection collision(&context);
3361 collision.disableMessages();
3362
3363 // Create larger set of test geometry
3364 std::vector<uint> triangles;
3365 for (int i = 0; i < 100; i++) {
3366 float x = i * 0.1f;
3367 uint triangle = context.addTriangle(make_vec3(x, 0, -0.5f), make_vec3(x + 0.05f, 0, -0.5f), make_vec3(x + 0.025f, 0, 0.5f));
3368 triangles.push_back(triangle);
3369 }
3370
3371 // Test batch casting with large number of rays - align with triangle centers
3372 std::vector<CollisionDetection::RayQuery> many_rays;
3373 for (int i = 0; i < 200; i++) {
3374 float x = (i % 100) * 0.1f + 0.025f; // Align with triangle centers
3375 many_rays.push_back(CollisionDetection::RayQuery(make_vec3(x, -1, 0), normalize(make_vec3(0, 1, 0))));
3376 }
3377
3378 auto start_time = std::chrono::high_resolution_clock::now();
3379
3381 std::vector<CollisionDetection::HitResult> results = collision.castRays(many_rays, &stats);
3382
3383 auto end_time = std::chrono::high_resolution_clock::now();
3384 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
3385
3386 DOCTEST_CHECK(results.size() == 200);
3387 DOCTEST_CHECK(stats.total_rays_cast == 200);
3388 DOCTEST_CHECK(stats.total_hits >= 100); // Should hit many triangles
3389
3390 // Performance should be reasonable (less than 1 second for 200 rays)
3391 DOCTEST_CHECK(duration.count() < 1000);
3392
3393 // Performance info available in stats but not printed during normal test runs
3394 // Uncomment for performance benchmarking:
3395 // std::cout << "Batch ray casting performance: " << duration.count() << " ms for " << many_rays.size() << " rays (" << stats.total_hits << " hits)" << std::endl;
3396}
3397
3398DOCTEST_TEST_CASE("CollisionDetection Ray Casting - Integration with Existing BVH") {
3399 Context context;
3400 CollisionDetection collision(&context);
3401 collision.disableMessages();
3402
3403 // Create test geometry
3404 uint triangle1 = context.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0.5f, 0, 1));
3405 uint triangle2 = context.addTriangle(make_vec3(2, 0, 0), make_vec3(3, 0, 0), make_vec3(2.5f, 0, 1));
3406
3407 // Test that ray casting works with manually built BVH
3408 collision.buildBVH();
3409
3410 CollisionDetection::HitResult result1 = collision.castRay(make_vec3(0.5f, -1, 0.5f), normalize(make_vec3(0, 1, 0)));
3411 DOCTEST_CHECK(result1.hit == true);
3412 DOCTEST_CHECK(result1.primitive_UUID == triangle1);
3413
3414 CollisionDetection::HitResult result2 = collision.castRay(make_vec3(2.5f, -1, 0.5f), normalize(make_vec3(0, 1, 0)));
3415 DOCTEST_CHECK(result2.hit == true);
3416 DOCTEST_CHECK(result2.primitive_UUID == triangle2);
3417
3418 // Test that ray casting works with automatic BVH rebuilds
3419 collision.enableAutomaticBVHRebuilds();
3420
3421 // Add new geometry after BVH was built
3422 uint triangle3 = context.addTriangle(make_vec3(4, 0, 0), make_vec3(5, 0, 0), make_vec3(4.5f, 0, 1));
3423
3424 // Should automatically rebuild BVH and find new triangle
3425 CollisionDetection::HitResult result3 = collision.castRay(make_vec3(4.5f, -1, 0.5f), normalize(make_vec3(0, 1, 0)));
3426 DOCTEST_CHECK(result3.hit == true);
3427 DOCTEST_CHECK(result3.primitive_UUID == triangle3);
3428}
3429
3430DOCTEST_TEST_CASE("CollisionDetection Ray Casting - Compatibility with Other Plugin Methods") {
3431 Context context;
3432 CollisionDetection collision(&context);
3433 collision.disableMessages();
3434
3435 // Create overlapping test geometry
3436 auto triangles = CollisionTests::generateOverlappingCluster(&context, 5);
3437
3438 // Test that ray casting and collision detection can coexist
3439 auto collisions = collision.findCollisions(triangles[0]);
3440 DOCTEST_CHECK(collisions.size() > 0);
3441
3442 // Test that existing collision detection methods still work
3443 DOCTEST_CHECK(triangles.size() == 5);
3444
3445 // Test cone intersection functionality
3446 auto cone_result = collision.findOptimalConePath(make_vec3(0, -2, 0), make_vec3(0, 1, 0), M_PI / 6, 3.0f);
3447
3448 // Verify that both collision detection and ray casting infrastructure coexist
3449 // This test mainly verifies compatibility, not specific ray hits
3450 DOCTEST_CHECK(collisions.size() > 0); // Collision detection works
3451 DOCTEST_CHECK(true); // Ray casting infrastructure is available (compilation test)
3452}
3453
3454// ================================================================
3455// OPTIMIZATION TEST CASES
3456// ================================================================
3457
3458DOCTEST_TEST_CASE("CollisionDetection - BVH Optimization Mode Management") {
3459 Context context;
3460 CollisionDetection collision(&context);
3461 collision.disableMessages();
3462
3463 // Create test geometry to trigger BVH construction
3464 uint triangle = context.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0.5f, 0, 1));
3465 auto sphere_uuids = context.addSphere(10, make_vec3(2, 0, 0.5f), 0.5f);
3466
3467 // Test 1: Default mode should be SOA_UNCOMPRESSED
3468 DOCTEST_CHECK(collision.getBVHOptimizationMode() == CollisionDetection::BVHOptimizationMode::SOA_UNCOMPRESSED);
3469
3470 // Test 2: Mode switching (only SOA_UNCOMPRESSED available now)
3471 DOCTEST_CHECK_NOTHROW(collision.setBVHOptimizationMode(CollisionDetection::BVHOptimizationMode::SOA_UNCOMPRESSED));
3472 DOCTEST_CHECK(collision.getBVHOptimizationMode() == CollisionDetection::BVHOptimizationMode::SOA_UNCOMPRESSED);
3473
3474 // Test 4: Build BVH to populate memory statistics
3475 collision.buildBVH();
3476
3477 // Test 5: Convert between optimization modes to populate all memory structures
3478 collision.setBVHOptimizationMode(CollisionDetection::BVHOptimizationMode::SOA_UNCOMPRESSED);
3479 collision.setBVHOptimizationMode(CollisionDetection::BVHOptimizationMode::SOA_UNCOMPRESSED);
3480
3481 // Test 6: Memory usage comparison
3482 auto memory_stats = collision.getBVHMemoryUsage();
3483 DOCTEST_CHECK(memory_stats.soa_memory_bytes > 0);
3484
3485 // With quantized mode removed, quantized_memory_bytes should be 0
3486 DOCTEST_CHECK(memory_stats.quantized_memory_bytes == 0);
3487 DOCTEST_CHECK(memory_stats.quantized_reduction_percent == 0.0f);
3488}
3489
3490DOCTEST_TEST_CASE("CollisionDetection - Optimized Ray Casting Correctness") {
3491 Context context;
3492 CollisionDetection collision(&context);
3493 collision.disableMessages();
3494
3495 // Create diverse test geometry with known positions
3496 uint triangle = context.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0.5f, 0, 1));
3497 auto sphere_uuids = context.addSphere(8, make_vec3(3, 0, 0.5f), 0.5f);
3498
3499 // Create test rays with known expected outcomes
3500 std::vector<CollisionDetection::RayQuery> rays;
3501 rays.push_back(CollisionDetection::RayQuery(make_vec3(0.5f, -1, 0.5f), make_vec3(0, 1, 0))); // Should hit triangle
3502 rays.push_back(CollisionDetection::RayQuery(make_vec3(3, -1, 0.5f), make_vec3(0, 1, 0))); // Should hit sphere
3503 rays.push_back(CollisionDetection::RayQuery(make_vec3(10, -1, 0), make_vec3(0, 1, 0))); // Should miss both
3504
3505 // Test legacy and optimized (SoA) modes produce consistent results
3506 std::vector<CollisionDetection::HitResult> legacy_results, soa_results;
3507
3508 // Legacy mode
3509 collision.setBVHOptimizationMode(CollisionDetection::BVHOptimizationMode::SOA_UNCOMPRESSED);
3510 legacy_results = collision.castRays(rays);
3511
3512 // SOA optimized mode
3513 collision.setBVHOptimizationMode(CollisionDetection::BVHOptimizationMode::SOA_UNCOMPRESSED);
3514 soa_results = collision.castRaysOptimized(rays);
3515
3516 // Verify both modes produce equivalent results
3517 DOCTEST_REQUIRE(legacy_results.size() == 3);
3518 DOCTEST_REQUIRE(soa_results.size() == 3);
3519
3520 for (size_t i = 0; i < legacy_results.size(); i++) {
3521 // Hit/miss should be consistent across both modes
3522 DOCTEST_CHECK(legacy_results[i].hit == soa_results[i].hit);
3523
3524 if (legacy_results[i].hit) {
3525 // Primitive UUID should match
3526 DOCTEST_CHECK(legacy_results[i].primitive_UUID == soa_results[i].primitive_UUID);
3527
3528 // Distance should be very close
3529 DOCTEST_CHECK(std::abs(legacy_results[i].distance - soa_results[i].distance) < 0.001f);
3530 }
3531 }
3532
3533 // Verify expected hit pattern: ray 0 and 1 should hit, ray 2 should miss
3534 DOCTEST_CHECK(legacy_results[0].hit == true); // Triangle hit
3535 DOCTEST_CHECK(legacy_results[1].hit == true); // Sphere hit
3536 DOCTEST_CHECK(legacy_results[2].hit == false); // Miss
3537}
3538
3539DOCTEST_TEST_CASE("CollisionDetection - Ray Streaming Interface") {
3540 Context context;
3541 CollisionDetection collision(&context);
3542 collision.disableMessages();
3543 collision.setBVHOptimizationMode(CollisionDetection::BVHOptimizationMode::SOA_UNCOMPRESSED);
3544
3545 // Create test geometry grid
3546 for (int i = 0; i < 5; i++) {
3547 float x = i * 2.0f;
3548 context.addTriangle(make_vec3(x, 0, 0), make_vec3(x + 1, 0, 0), make_vec3(x + 0.5f, 0, 1));
3549 }
3550
3551 // Test ray streaming interface
3553 std::vector<CollisionDetection::RayQuery> batch;
3554
3555 // Create batch of rays
3556 for (int i = 0; i < 50; i++) {
3557 float x = (i % 5) * 2.0f + 0.5f; // Align with triangles
3558 batch.push_back(CollisionDetection::RayQuery(make_vec3(x, -1, 0.5f), make_vec3(0, 1, 0)));
3559 }
3560 stream.addRays(batch);
3561
3562 DOCTEST_CHECK(stream.total_rays == 50);
3563 DOCTEST_CHECK(stream.packets.size() > 0);
3564
3565 // Process stream
3567 bool success = collision.processRayStream(stream, &stats);
3568 DOCTEST_CHECK(success == true);
3569 DOCTEST_CHECK(stats.total_rays_cast == 50);
3570
3571 // Verify results
3572 auto results = stream.getAllResults();
3573 DOCTEST_CHECK(results.size() == 50);
3574
3575 // All rays should hit (they're aligned with triangles)
3576 size_t hit_count = 0;
3577 for (const auto &result: results) {
3578 if (result.hit)
3579 hit_count++;
3580 }
3581 DOCTEST_CHECK(hit_count > 40); // Most rays should hit the triangles
3582}
3583
3584DOCTEST_TEST_CASE("CollisionDetection - BVH Layout Conversion Methods") {
3585 Context context;
3586 CollisionDetection collision(&context);
3587 collision.disableMessages();
3588
3589 // Create test geometry
3590 uint triangle1 = context.addTriangle(make_vec3(0, 0, 0), make_vec3(2, 0, 0), make_vec3(1, 0, 2));
3591 auto sphere_uuids = context.addSphere(12, make_vec3(5, 0, 1), 0.8f);
3592 uint triangle2 = context.addTriangle(make_vec3(-2, 1, 0), make_vec3(0, 1, 0), make_vec3(-1, 1, 1.5f));
3593
3594 // Build BVH
3595 collision.setBVHOptimizationMode(CollisionDetection::BVHOptimizationMode::SOA_UNCOMPRESSED);
3596 collision.buildBVH();
3597
3598 // Test rays
3599 std::vector<CollisionDetection::RayQuery> test_rays = {
3600 CollisionDetection::RayQuery(make_vec3(1, -1, 1), make_vec3(0, 1, 0)), // Should hit triangle1
3601 CollisionDetection::RayQuery(make_vec3(5, -1, 1), make_vec3(0, 1, 0)), // Should hit sphere
3602 CollisionDetection::RayQuery(make_vec3(-1, 0, 0.75f), make_vec3(0, 1, 0)) // Should hit triangle2
3603 };
3604
3605 // Test legacy and SoA modes
3606 auto legacy_results = collision.castRays(test_rays);
3607 auto soa_results = collision.castRaysOptimized(test_rays);
3608 auto memory_stats = collision.getBVHMemoryUsage();
3609
3610 // Validation
3611 DOCTEST_REQUIRE(legacy_results.size() == 3);
3612 DOCTEST_REQUIRE(soa_results.size() == 3);
3613
3614 // Count hits to verify consistency
3615 size_t legacy_hits = 0, soa_hits = 0;
3616 for (size_t i = 0; i < 3; i++) {
3617 if (legacy_results[i].hit)
3618 legacy_hits++;
3619 if (soa_results[i].hit)
3620 soa_hits++;
3621 }
3622
3623 // SoA should produce consistent results with legacy mode
3624 DOCTEST_CHECK(legacy_hits == soa_hits);
3625
3626 // Memory usage verification (without quantized mode)
3627 DOCTEST_CHECK(memory_stats.soa_memory_bytes > 0);
3628 DOCTEST_CHECK(memory_stats.quantized_memory_bytes == 0); // No quantized mode
3629 DOCTEST_CHECK(memory_stats.quantized_reduction_percent == 0.0f);
3630}
3631
3632DOCTEST_TEST_CASE("CollisionDetection - RayPacket Edge Cases and Functionality") {
3633 Context context;
3634 CollisionDetection collision(&context);
3635 collision.disableMessages();
3636
3637 // Test 1: Empty RayPacket behavior
3638 CollisionDetection::RayPacket empty_packet;
3639 DOCTEST_CHECK(empty_packet.ray_count == 0);
3640 DOCTEST_CHECK(empty_packet.getMemoryUsage() == 0);
3641 DOCTEST_CHECK(empty_packet.toRayQueries().empty());
3642
3643 // Clear empty packet should not crash
3644 DOCTEST_CHECK_NOTHROW(empty_packet.clear());
3645
3646 // Test 2: RayPacket capacity management
3647 CollisionDetection::RayPacket capacity_packet;
3648 capacity_packet.reserve(100);
3649
3650 // Add rays up to and beyond initial capacity
3651 std::vector<CollisionDetection::RayQuery> test_queries;
3652 for (int i = 0; i < 150; i++) {
3653 float x = i * 0.1f;
3654 CollisionDetection::RayQuery query(make_vec3(x, 0, 0), make_vec3(0, 0, 1));
3655 test_queries.push_back(query);
3656 capacity_packet.addRay(query);
3657 }
3658
3659 DOCTEST_CHECK(capacity_packet.ray_count == 150);
3660 DOCTEST_CHECK(capacity_packet.origins.size() == 150);
3661 DOCTEST_CHECK(capacity_packet.directions.size() == 150);
3662 DOCTEST_CHECK(capacity_packet.results.size() == 150);
3663
3664 // Test 3: RayPacket conversion accuracy
3665 auto converted_queries = capacity_packet.toRayQueries();
3666 DOCTEST_REQUIRE(converted_queries.size() == 150);
3667
3668 for (size_t i = 0; i < 150; i++) {
3669 DOCTEST_CHECK(converted_queries[i].origin.magnitude() == test_queries[i].origin.magnitude());
3670 DOCTEST_CHECK(converted_queries[i].direction.magnitude() == test_queries[i].direction.magnitude());
3671 DOCTEST_CHECK(converted_queries[i].max_distance == test_queries[i].max_distance);
3672 }
3673
3674 // Test 4: Memory usage calculation
3675 size_t expected_memory = (150 * 2) * sizeof(helios::vec3) + // origins + directions
3676 150 * sizeof(float) + // max_distances
3677 150 * sizeof(CollisionDetection::HitResult); // results
3678 size_t actual_memory = capacity_packet.getMemoryUsage();
3679 DOCTEST_CHECK(actual_memory >= expected_memory); // Account for target_UUIDs overhead
3680
3681 // Test 5: Clear functionality
3682 capacity_packet.clear();
3683 DOCTEST_CHECK(capacity_packet.ray_count == 0);
3684 DOCTEST_CHECK(capacity_packet.origins.empty());
3685 DOCTEST_CHECK(capacity_packet.directions.empty());
3686 DOCTEST_CHECK(capacity_packet.results.empty());
3687 DOCTEST_CHECK(capacity_packet.getMemoryUsage() == 0);
3688}
3689
3690DOCTEST_TEST_CASE("CollisionDetection - RayStream Batch Management") {
3691 Context context;
3692 CollisionDetection collision(&context);
3693 collision.disableMessages();
3694 collision.setBVHOptimizationMode(CollisionDetection::BVHOptimizationMode::SOA_UNCOMPRESSED);
3695
3696 // Create test geometry
3697 for (int i = 0; i < 3; i++) {
3698 float x = i * 3.0f;
3699 context.addTriangle(make_vec3(x, 0, 0), make_vec3(x + 1, 0, 0), make_vec3(x + 0.5f, 0, 1));
3700 }
3701
3702 // Test 1: Large ray stream with multiple packets
3703 CollisionDetection::RayStream large_stream;
3704 std::vector<CollisionDetection::RayQuery> large_batch;
3705
3706 // Create more rays than fit in a single packet
3707 size_t total_rays = CollisionDetection::RAY_BATCH_SIZE * 2.5; // 2.5 packets worth
3708 for (size_t i = 0; i < total_rays; i++) {
3709 float x = (i % 3) * 3.0f + 0.5f;
3710 large_batch.push_back(CollisionDetection::RayQuery(make_vec3(x, -1, 0.5f), make_vec3(0, 1, 0)));
3711 }
3712
3713 large_stream.addRays(large_batch);
3714 DOCTEST_CHECK(large_stream.total_rays == total_rays);
3715 DOCTEST_CHECK(large_stream.packets.size() == 3); // Should create 3 packets
3716
3717 // Test 2: Stream processing and memory usage
3718 size_t stream_memory_before = large_stream.getMemoryUsage();
3719 DOCTEST_CHECK(stream_memory_before > 0);
3720
3722 bool large_success = collision.processRayStream(large_stream, &large_stats);
3723 DOCTEST_CHECK(large_success == true);
3724 DOCTEST_CHECK(large_stats.total_rays_cast == total_rays);
3725
3726 // Test 3: Results aggregation
3727 auto all_results = large_stream.getAllResults();
3728 DOCTEST_CHECK(all_results.size() == total_rays);
3729
3730 // Verify reasonable hit rate (rays are aligned with triangles)
3731 size_t hit_count = 0;
3732 for (const auto &result: all_results) {
3733 if (result.hit)
3734 hit_count++;
3735 }
3736 float hit_rate = float(hit_count) / float(total_rays);
3737 DOCTEST_CHECK(hit_rate > 0.8f); // Expect high hit rate
3738
3739 // Test 4: Empty stream handling
3740 CollisionDetection::RayStream empty_stream;
3741 DOCTEST_CHECK(empty_stream.total_rays == 0);
3742 DOCTEST_CHECK(empty_stream.packets.empty());
3743 DOCTEST_CHECK(empty_stream.getMemoryUsage() == 0);
3744
3746 bool empty_success = collision.processRayStream(empty_stream, &empty_stats);
3747 DOCTEST_CHECK(empty_success == true);
3748 DOCTEST_CHECK(empty_stats.total_rays_cast == 0);
3749
3750 // Test 5: Stream clear and reuse
3751 large_stream.clear();
3752 DOCTEST_CHECK(large_stream.total_rays == 0);
3753 DOCTEST_CHECK(large_stream.packets.empty());
3754 DOCTEST_CHECK(large_stream.current_packet == 0);
3755 DOCTEST_CHECK(large_stream.getMemoryUsage() == 0);
3756}
3757
3758DOCTEST_TEST_CASE("CollisionDetection - SoA Precision Validation") {
3759 Context context;
3760 CollisionDetection collision(&context);
3761 collision.disableMessages();
3762
3763 // Create test geometry
3764 uint triangle1 = context.addTriangle(make_vec3(0, 0, 0), make_vec3(2, 0, 0), make_vec3(1, 0, 2));
3765 uint triangle2 = context.addTriangle(make_vec3(10, 0, 0), make_vec3(12, 0, 0), make_vec3(11, 0, 2));
3766 auto sphere_uuids = context.addSphere(12, make_vec3(5, 5, 1), 1.0f);
3767
3768 // Build BVH
3769 collision.setBVHOptimizationMode(CollisionDetection::BVHOptimizationMode::SOA_UNCOMPRESSED);
3770 collision.buildBVH();
3771
3772 std::vector<CollisionDetection::RayQuery> precision_test_rays = {// Ray hitting first triangle
3774 // Ray hitting second triangle
3775 CollisionDetection::RayQuery(make_vec3(11, -1, 1), make_vec3(0, 1, 0)),
3776 // Ray hitting sphere
3778 // Miss rays
3780
3781 auto legacy_results = collision.castRays(precision_test_rays);
3782 auto soa_results = collision.castRaysOptimized(precision_test_rays);
3783
3784 DOCTEST_REQUIRE(legacy_results.size() == precision_test_rays.size());
3785 DOCTEST_REQUIRE(soa_results.size() == precision_test_rays.size());
3786
3787 // SoA should exactly match legacy results (no precision loss)
3788 for (size_t i = 0; i < precision_test_rays.size(); i++) {
3789 DOCTEST_CHECK(legacy_results[i].hit == soa_results[i].hit);
3790
3791 if (legacy_results[i].hit && soa_results[i].hit) {
3792 DOCTEST_CHECK(legacy_results[i].primitive_UUID == soa_results[i].primitive_UUID);
3793 DOCTEST_CHECK(std::abs(legacy_results[i].distance - soa_results[i].distance) < 0.001f);
3794 }
3795 }
3796
3797 // Memory usage verification (no quantized mode)
3798 auto memory_stats = collision.getBVHMemoryUsage();
3799 DOCTEST_CHECK(memory_stats.soa_memory_bytes > 0);
3800 DOCTEST_CHECK(memory_stats.quantized_memory_bytes == 0);
3801 DOCTEST_CHECK(memory_stats.quantized_reduction_percent == 0.0f);
3802}
3803
3804DOCTEST_TEST_CASE("CollisionDetection - Error Handling and Edge Cases") {
3805 Context context;
3806 CollisionDetection collision(&context);
3807 collision.disableMessages();
3808
3809 // Test 1: Mode conversion with empty BVH should not crash
3810 collision.setBVHOptimizationMode(CollisionDetection::BVHOptimizationMode::SOA_UNCOMPRESSED);
3811 DOCTEST_CHECK_NOTHROW(collision.setBVHOptimizationMode(CollisionDetection::BVHOptimizationMode::SOA_UNCOMPRESSED));
3812 DOCTEST_CHECK_NOTHROW(collision.setBVHOptimizationMode(CollisionDetection::BVHOptimizationMode::SOA_UNCOMPRESSED));
3813
3814 // Test 2: Repeated mode setting should be handled efficiently
3815 auto initial_mode = collision.getBVHOptimizationMode();
3816 DOCTEST_CHECK_NOTHROW(collision.setBVHOptimizationMode(initial_mode)); // No-op
3817 DOCTEST_CHECK(collision.getBVHOptimizationMode() == initial_mode);
3818
3819 // Test 3: Memory usage queries with empty structures
3820 auto empty_memory_stats = collision.getBVHMemoryUsage();
3821 DOCTEST_CHECK(empty_memory_stats.soa_memory_bytes == 0);
3822 DOCTEST_CHECK(empty_memory_stats.quantized_memory_bytes == 0);
3823
3824 // Test 4: Ray casting on empty BVH structures
3825 std::vector<CollisionDetection::RayQuery> empty_test_rays = {CollisionDetection::RayQuery(make_vec3(0, 0, 0), make_vec3(0, 0, 1))};
3826
3827 DOCTEST_CHECK_NOTHROW(collision.castRays(empty_test_rays));
3828 DOCTEST_CHECK_NOTHROW(collision.castRaysOptimized(empty_test_rays));
3829
3830 auto empty_results = collision.castRaysOptimized(empty_test_rays);
3831 DOCTEST_CHECK(empty_results.size() == 1);
3832 DOCTEST_CHECK(empty_results[0].hit == false);
3833
3834 // Test 5: Stream processing with empty stream
3835 CollisionDetection::RayStream empty_stream;
3837 DOCTEST_CHECK_NOTHROW(collision.processRayStream(empty_stream, &empty_stats));
3838
3839 // Add geometry and test normal operation recovery
3840 uint recovery_triangle = context.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0.5f, 0, 1));
3841 collision.buildBVH();
3842
3843 std::vector<CollisionDetection::RayQuery> recovery_rays = {CollisionDetection::RayQuery(make_vec3(0.5f, -1, 0.5f), make_vec3(0, 1, 0))};
3844
3845 auto recovery_results = collision.castRaysOptimized(recovery_rays);
3846 DOCTEST_CHECK(recovery_results.size() == 1);
3847 DOCTEST_CHECK(recovery_results[0].hit == true); // Should now hit the triangle
3848}
3849
3850DOCTEST_TEST_CASE("CollisionDetection - Memory and Statistics Validation") {
3851 Context context;
3852 CollisionDetection collision(&context);
3853 collision.disableMessages();
3854
3855 // Create a reasonable amount of test geometry for meaningful statistics
3856 for (int i = 0; i < 8; i++) {
3857 float x = i * 2.0f;
3858 float y = (i % 2) * 2.0f;
3859 context.addTriangle(make_vec3(x, y, 0), make_vec3(x + 1, y, 0), make_vec3(x + 0.5f, y, 1));
3860 }
3861 auto sphere_uuids = context.addSphere(16, make_vec3(10, 10, 1), 1.5f);
3862
3863 // Build BVH in all modes and collect statistics
3864 collision.setBVHOptimizationMode(CollisionDetection::BVHOptimizationMode::SOA_UNCOMPRESSED);
3865 collision.buildBVH();
3866 auto legacy_memory = collision.getBVHMemoryUsage();
3867
3868 collision.setBVHOptimizationMode(CollisionDetection::BVHOptimizationMode::SOA_UNCOMPRESSED);
3869 auto memory_stats = collision.getBVHMemoryUsage();
3870
3871 // Test memory usage statistics accuracy (without quantized mode)
3872 DOCTEST_CHECK(memory_stats.soa_memory_bytes > 0);
3873 DOCTEST_CHECK(memory_stats.quantized_memory_bytes == 0);
3874 DOCTEST_CHECK(memory_stats.quantized_reduction_percent == 0.0f);
3875
3876 // Test ray tracing statistics collection
3877 std::vector<CollisionDetection::RayQuery> stat_test_rays;
3878 for (int i = 0; i < 20; i++) {
3879 float x = (i % 4) * 2.0f + 0.5f;
3880 float y = (i / 4) * 2.0f + 0.5f;
3881 stat_test_rays.push_back(CollisionDetection::RayQuery(make_vec3(x, y, -1), make_vec3(0, 0, 1)));
3882 }
3883
3885 auto stat_results = collision.castRaysOptimized(stat_test_rays, &stats);
3886
3887 // Validate statistics collection
3888 DOCTEST_CHECK(stats.total_rays_cast == 20);
3889 DOCTEST_CHECK(stat_results.size() == 20);
3890 DOCTEST_CHECK(stats.total_hits <= stats.total_rays_cast); // Hits can't exceed rays
3891
3892 if (stats.total_hits > 0) {
3893 DOCTEST_CHECK(stats.average_ray_distance > 0.0f);
3894 DOCTEST_CHECK(stats.bvh_nodes_visited > 0); // Should visit at least some BVH nodes
3895 }
3896
3897 // Test stream processing statistics
3898 CollisionDetection::RayStream stats_stream;
3899 stats_stream.addRays(stat_test_rays);
3900
3902 bool stream_success = collision.processRayStream(stats_stream, &stream_stats);
3903 DOCTEST_CHECK(stream_success == true);
3904 DOCTEST_CHECK(stream_stats.total_rays_cast == 20);
3905
3906 // Stream stats should be consistent with direct ray casting
3907 DOCTEST_CHECK(stream_stats.total_hits == stats.total_hits);
3908 DOCTEST_CHECK(std::abs(stream_stats.average_ray_distance - stats.average_ray_distance) < 0.01f);
3909}
3910
3911// ============================================================================
3912// VOXEL PRIMITIVE INTERSECTION TESTS
3913// ============================================================================
3914
3915DOCTEST_TEST_CASE("CollisionDetection Voxel Primitive Intersection - Basic Ray-AABB Tests") {
3916 Context context;
3917 CollisionDetection collision(&context);
3918 collision.disableMessages();
3919 collision.disableGPUAcceleration(); // Test CPU implementation first
3920
3921 // Create a simple 2x2x2 voxel at origin
3922 uint voxel_uuid = context.addVoxel(make_vec3(0, 0, 0), make_vec3(2, 2, 2));
3923
3924 collision.buildBVH();
3925
3926 // Test 1: Ray hitting voxel center (should hit)
3927 {
3929 ray.origin = make_vec3(0, 0, -5);
3930 ray.direction = make_vec3(0, 0, 1);
3931 ray.max_distance = 10.0f;
3932
3933 auto results = collision.castRays({ray});
3934 DOCTEST_CHECK(results.size() == 1);
3935 DOCTEST_CHECK(results[0].hit == true);
3936 DOCTEST_CHECK(results[0].distance > 3.9f);
3937 DOCTEST_CHECK(results[0].distance < 4.1f); // Distance ~4 to reach voxel face
3938 DOCTEST_CHECK(results[0].primitive_UUID == voxel_uuid);
3939 }
3940
3941 // Test 2: Ray missing voxel (should miss)
3942 {
3944 ray.origin = make_vec3(5, 0, -5);
3945 ray.direction = make_vec3(0, 0, 1);
3946 ray.max_distance = 10.0f;
3947
3948 auto results = collision.castRays({ray});
3949 DOCTEST_CHECK(results.size() == 1);
3950 DOCTEST_CHECK(results[0].hit == false);
3951 }
3952
3953 // Test 3: Ray starting inside voxel (should hit exit face)
3954 {
3956 ray.origin = make_vec3(0, 0, 0); // Inside voxel
3957 ray.direction = make_vec3(0, 0, 1);
3958 ray.max_distance = 10.0f;
3959
3960 auto results = collision.castRays({ray});
3961 DOCTEST_CHECK(results.size() == 1);
3962 DOCTEST_CHECK(results[0].hit == true);
3963 DOCTEST_CHECK(results[0].distance > 0.9f);
3964 DOCTEST_CHECK(results[0].distance < 1.1f); // Distance ~1 to exit face
3965 }
3966}
3967
3968DOCTEST_TEST_CASE("CollisionDetection Voxel Primitive Intersection - Multiple Voxels") {
3969 Context context;
3970 CollisionDetection collision(&context);
3971 collision.disableMessages();
3972 collision.disableGPUAcceleration();
3973
3974 // Create a line of 3 voxels
3975 uint voxel1 = context.addVoxel(make_vec3(-4, 0, 0), make_vec3(2, 2, 2));
3976 uint voxel2 = context.addVoxel(make_vec3(0, 0, 0), make_vec3(2, 2, 2));
3977 uint voxel3 = context.addVoxel(make_vec3(4, 0, 0), make_vec3(2, 2, 2));
3978
3979 collision.buildBVH();
3980
3981 // Test 1: Ray hitting first voxel only
3982 {
3984 ray.origin = make_vec3(-4, 0, -5);
3985 ray.direction = make_vec3(0, 0, 1);
3986 ray.max_distance = 10.0f;
3987
3988 auto results = collision.castRays({ray});
3989 DOCTEST_CHECK(results.size() == 1);
3990 DOCTEST_CHECK(results[0].hit == true);
3991 DOCTEST_CHECK(results[0].primitive_UUID == voxel1);
3992 }
3993
3994 // Test 2: Ray passing through multiple voxels (should hit closest)
3995 {
3997 ray.origin = make_vec3(-8, 0, 0);
3998 ray.direction = make_vec3(1, 0, 0);
3999 ray.max_distance = 20.0f;
4000
4001 auto results = collision.castRays({ray});
4002 DOCTEST_CHECK(results.size() == 1);
4003 DOCTEST_CHECK(results[0].hit == true);
4004 DOCTEST_CHECK(results[0].primitive_UUID == voxel1); // Should hit first voxel
4005 DOCTEST_CHECK(results[0].distance > 2.9f);
4006 DOCTEST_CHECK(results[0].distance < 3.1f); // Distance ~3 to first voxel
4007 }
4008}
4009
4010DOCTEST_TEST_CASE("CollisionDetection Voxel Primitive Intersection - GPU vs CPU Consistency") {
4011 Context context;
4012 CollisionDetection collision(&context);
4013 collision.disableMessages();
4014
4015 // Create test scene with various sized voxels
4016 std::vector<uint> voxel_uuids;
4017 voxel_uuids.push_back(context.addVoxel(make_vec3(-3, -3, 0), make_vec3(1, 1, 1))); // Small voxel
4018 voxel_uuids.push_back(context.addVoxel(make_vec3(0, 0, 0), make_vec3(2, 2, 2))); // Medium voxel
4019 voxel_uuids.push_back(context.addVoxel(make_vec3(4, 2, 1), make_vec3(3, 3, 3))); // Large voxel
4020
4021 // Create diverse set of test rays
4022 std::vector<CollisionDetection::RayQuery> test_rays;
4023
4024 // Rays from different angles and positions
4025 for (int i = 0; i < 5; i++) {
4026 for (int j = 0; j < 3; j++) {
4028 ray.origin = make_vec3(i * 2.0f - 4.0f, j * 2.0f - 2.0f, -8.0f);
4029 ray.direction = normalize(make_vec3(0.1f * i, 0.1f * j, 1.0f));
4030 ray.max_distance = 20.0f;
4031 test_rays.push_back(ray);
4032 }
4033 }
4034
4035 collision.buildBVH();
4036
4037 // Test CPU implementation
4038 collision.disableGPUAcceleration();
4039 auto cpu_results = collision.castRays(test_rays);
4040
4041 // Test GPU implementation
4042 std::vector<CollisionDetection::HitResult> gpu_results;
4043 {
4044 helios::capture_cerr capture;
4045 collision.enableGPUAcceleration();
4046 gpu_results = collision.castRays(test_rays);
4047 } // Capture destroyed before assertions
4048
4049 // Compare results
4050 DOCTEST_CHECK(cpu_results.size() == gpu_results.size());
4051 DOCTEST_CHECK(cpu_results.size() == test_rays.size());
4052
4053 for (size_t i = 0; i < cpu_results.size(); i++) {
4054 DOCTEST_CHECK(cpu_results[i].hit == gpu_results[i].hit);
4055
4056 if (cpu_results[i].hit && gpu_results[i].hit) {
4057 // Check distance consistency (allow small floating point differences)
4058 DOCTEST_CHECK(std::abs(cpu_results[i].distance - gpu_results[i].distance) < 0.01f);
4059 DOCTEST_CHECK(cpu_results[i].primitive_UUID == gpu_results[i].primitive_UUID);
4060 }
4061 }
4062}
4063
4064// -------- ENHANCED MATHEMATICAL ACCURACY VALIDATION TESTS --------
4065
4066DOCTEST_TEST_CASE("CollisionDetection Mathematical Accuracy - Ray-Triangle Intersection Algorithms") {
4067 Context context;
4068 CollisionDetection collision(&context);
4069 collision.disableMessages();
4070 collision.disableGPUAcceleration(); // Use CPU for deterministic results
4071
4072 // Test 1: Known analytical triangle intersection
4073 vec3 v0 = make_vec3(0, 0, 0);
4074 vec3 v1 = make_vec3(1, 0, 0);
4075 vec3 v2 = make_vec3(0, 1, 0);
4076 uint triangle_uuid = context.addTriangle(v0, v1, v2);
4077
4078 // Ray hitting center of triangle at (1/3, 1/3, 0)
4079 vec3 ray_origin = make_vec3(1.0f / 3.0f, 1.0f / 3.0f, -1.0f);
4080 vec3 ray_direction = make_vec3(0, 0, 1);
4081
4082 CollisionDetection::HitResult result = collision.castRay(ray_origin, ray_direction);
4083
4084 DOCTEST_CHECK(result.hit == true);
4085 DOCTEST_CHECK(result.primitive_UUID == triangle_uuid);
4086
4087 // Mathematical validation: distance should be exactly 1.0
4088 DOCTEST_CHECK(std::abs(result.distance - 1.0f) < 1e-6f);
4089
4090 // Intersection point should be exactly at (1/3, 1/3, 0)
4091 DOCTEST_CHECK(std::abs(result.intersection_point.x - 1.0f / 3.0f) < 1e-6f);
4092 DOCTEST_CHECK(std::abs(result.intersection_point.y - 1.0f / 3.0f) < 1e-6f);
4093 DOCTEST_CHECK(std::abs(result.intersection_point.z - 0.0f) < 1e-6f);
4094
4095 // Normal should be (0, 0, 1) for this triangle
4096 vec3 expected_normal = normalize(cross(v1 - v0, v2 - v0));
4097 float normal_dot = result.normal.x * expected_normal.x + result.normal.y * expected_normal.y + result.normal.z * expected_normal.z;
4098 DOCTEST_CHECK(std::abs(normal_dot - 1.0f) < 1e-6f);
4099}
4100
4101DOCTEST_TEST_CASE("CollisionDetection Mathematical Accuracy - Edge Case Intersections") {
4102 Context context;
4103 CollisionDetection collision(&context);
4104 collision.disableMessages();
4105 collision.disableGPUAcceleration();
4106
4107 // Test 1: Ray hitting triangle edge
4108 vec3 v0 = make_vec3(0, 0, 0);
4109 vec3 v1 = make_vec3(2, 0, 0);
4110 vec3 v2 = make_vec3(1, 2, 0);
4111 uint triangle_uuid = context.addTriangle(v0, v1, v2);
4112
4113 // Ray hitting midpoint of edge v0-v1 at (1, 0, 0)
4114 vec3 ray_origin = make_vec3(1, 0, -1);
4115 vec3 ray_direction = make_vec3(0, 0, 1);
4116
4117 CollisionDetection::HitResult result = collision.castRay(ray_origin, ray_direction);
4118
4119 DOCTEST_CHECK(result.hit == true);
4120 DOCTEST_CHECK(std::abs(result.intersection_point.x - 1.0f) < 1e-6f);
4121 DOCTEST_CHECK(std::abs(result.intersection_point.y - 0.0f) < 1e-6f);
4122 DOCTEST_CHECK(std::abs(result.intersection_point.z - 0.0f) < 1e-6f);
4123
4124 // Test 2: Ray hitting triangle vertex (may miss due to numerical precision)
4125 vec3 vertex_ray_origin = make_vec3(0, 0, -1);
4126 vec3 vertex_ray_direction = make_vec3(0, 0, 1);
4127
4128 CollisionDetection::HitResult vertex_result = collision.castRay(vertex_ray_origin, vertex_ray_direction);
4129
4130 // Vertex hits can be numerically challenging - allow miss but check consistency
4131 if (vertex_result.hit) {
4132 DOCTEST_CHECK(std::abs(vertex_result.intersection_point.x - 0.0f) < 1e-3f);
4133 DOCTEST_CHECK(std::abs(vertex_result.intersection_point.y - 0.0f) < 1e-3f);
4134 DOCTEST_CHECK(std::abs(vertex_result.intersection_point.z - 0.0f) < 1e-3f);
4135 }
4136}
4137
4138DOCTEST_TEST_CASE("CollisionDetection Mathematical Accuracy - Barycentric Coordinate Validation") {
4139 Context context;
4140 CollisionDetection collision(&context);
4141 collision.disableMessages();
4142 collision.disableGPUAcceleration();
4143
4144 // Create equilateral triangle for precise barycentric testing
4145 float sqrt3 = std::sqrt(3.0f);
4146 vec3 v0 = make_vec3(-1, -sqrt3 / 3.0f, 0);
4147 vec3 v1 = make_vec3(1, -sqrt3 / 3.0f, 0);
4148 vec3 v2 = make_vec3(0, 2.0f * sqrt3 / 3.0f, 0);
4149 uint triangle_uuid = context.addTriangle(v0, v1, v2);
4150
4151 // Test centroid hit (barycentric coordinates: 1/3, 1/3, 1/3)
4152 vec3 centroid = (v0 + v1 + v2) * (1.0f / 3.0f);
4153 vec3 ray_origin = make_vec3(centroid.x, centroid.y, -1);
4154 vec3 ray_direction = make_vec3(0, 0, 1);
4155
4156 CollisionDetection::HitResult result = collision.castRay(ray_origin, ray_direction);
4157
4158 DOCTEST_CHECK(result.hit == true);
4159 DOCTEST_CHECK(std::abs(result.intersection_point.x - centroid.x) < 1e-6f);
4160 DOCTEST_CHECK(std::abs(result.intersection_point.y - centroid.y) < 1e-6f);
4161
4162 // Test points with known barycentric coordinates
4163 vec3 midpoint_v0_v1 = (v0 + v1) * 0.5f; // (0.5, 0.5, 0) barycentric
4164 vec3 midpoint_ray_origin = make_vec3(midpoint_v0_v1.x, midpoint_v0_v1.y, -1);
4165
4166 CollisionDetection::HitResult midpoint_result = collision.castRay(midpoint_ray_origin, ray_direction);
4167
4168 DOCTEST_CHECK(midpoint_result.hit == true);
4169 DOCTEST_CHECK(std::abs(midpoint_result.intersection_point.x - midpoint_v0_v1.x) < 1e-6f);
4170 DOCTEST_CHECK(std::abs(midpoint_result.intersection_point.y - midpoint_v0_v1.y) < 1e-6f);
4171}
4172
4173// -------- COMPREHENSIVE GPU-SPECIFIC TESTS --------
4174
4175DOCTEST_TEST_CASE("CollisionDetection GPU-Specific - Direct castRaysGPU Testing") {
4176 Context context;
4177 CollisionDetection collision(&context);
4178 collision.disableMessages();
4179
4180 // Create diverse test geometry
4181 std::vector<uint> uuids;
4182 uuids.push_back(context.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0.5f, 1, 0)));
4183 uuids.push_back(context.addTriangle(make_vec3(2, 0, 0), make_vec3(3, 0, 0), make_vec3(2.5f, 1, 0)));
4184 uuids.push_back(context.addPatch(make_vec3(5, 0, 0), make_vec2(1, 1)));
4185
4186 // Create comprehensive ray set
4187 std::vector<CollisionDetection::RayQuery> queries;
4188 for (int i = 0; i < 100; i++) {
4190 query.origin = make_vec3(i * 0.1f - 2, -1, 0.5f);
4191 query.direction = normalize(make_vec3(0, 1, 0));
4192 query.max_distance = 5.0f;
4193 queries.push_back(query);
4194 }
4195
4196 // Test GPU functionality if available
4197 try {
4198 collision.enableGPUAcceleration();
4199#ifdef HELIOS_CUDA_AVAILABLE
4200 if (collision.isGPUAccelerationEnabled()) {
4202 std::vector<CollisionDetection::HitResult> gpu_results = collision.castRaysGPU(queries, gpu_stats);
4203
4204 DOCTEST_CHECK(gpu_results.size() == queries.size());
4205 DOCTEST_CHECK(gpu_stats.total_rays_cast == queries.size());
4206
4207 // Test against CPU reference
4208 collision.disableGPUAcceleration();
4210 std::vector<CollisionDetection::HitResult> cpu_results = collision.castRays(queries, &cpu_stats);
4211
4212 // Compare results
4213 DOCTEST_CHECK(cpu_results.size() == gpu_results.size());
4214
4215 int consistent_hits = 0;
4216 for (size_t i = 0; i < cpu_results.size(); i++) {
4217 if (cpu_results[i].hit == gpu_results[i].hit) {
4218 consistent_hits++;
4219 if (cpu_results[i].hit) {
4220 // Allow small floating-point differences in GPU vs CPU
4221 DOCTEST_CHECK(std::abs(cpu_results[i].distance - gpu_results[i].distance) < 0.001f);
4222 DOCTEST_CHECK(cpu_results[i].primitive_UUID == gpu_results[i].primitive_UUID);
4223 }
4224 }
4225 }
4226
4227 // Should have high consistency (allow for some GPU/CPU differences)
4228 DOCTEST_CHECK(consistent_hits >= (int) (0.95f * queries.size()));
4229
4230 } else {
4231 DOCTEST_WARN("GPU acceleration not available - skipping direct GPU tests");
4232 }
4233#endif
4234 } catch (std::exception &e) {
4235 DOCTEST_WARN((std::string("GPU test failed (expected on non-NVIDIA systems): ") + e.what()).c_str());
4236 }
4237}
4238
4239DOCTEST_TEST_CASE("CollisionDetection GPU-Specific - Error Handling and Edge Cases") {
4240 Context context;
4241 CollisionDetection collision(&context);
4242 collision.disableMessages();
4243
4244 try {
4245 collision.enableGPUAcceleration();
4246#ifdef HELIOS_CUDA_AVAILABLE
4247 if (collision.isGPUAccelerationEnabled()) {
4248
4249 // Test 1: Empty ray queries
4250 std::vector<CollisionDetection::RayQuery> empty_queries;
4252 std::vector<CollisionDetection::HitResult> results = collision.castRaysGPU(empty_queries, stats);
4253 DOCTEST_CHECK(results.empty());
4254 DOCTEST_CHECK(stats.total_rays_cast == 0);
4255
4256 // Test 2: Large batch processing
4257 std::vector<CollisionDetection::RayQuery> large_batch;
4258 for (int i = 0; i < 10000; i++) {
4260 query.origin = make_vec3(0, 0, i * 0.001f);
4261 query.direction = make_vec3(0, 0, 1);
4262 query.max_distance = 1.0f;
4263 large_batch.push_back(query);
4264 }
4265
4267 std::vector<CollisionDetection::HitResult> large_results = collision.castRaysGPU(large_batch, large_stats);
4268 DOCTEST_CHECK(large_results.size() == large_batch.size());
4269 DOCTEST_CHECK(large_stats.total_rays_cast == large_batch.size());
4270
4271 // Test 3: Degenerate rays
4272 std::vector<CollisionDetection::RayQuery> degenerate_queries;
4274 degenerate.origin = make_vec3(0, 0, 0);
4275 degenerate.direction = make_vec3(0, 0, 0); // Zero direction
4276 degenerate_queries.push_back(degenerate);
4277
4278 CollisionDetection::RayTracingStats degenerate_stats;
4279 std::vector<CollisionDetection::HitResult> degenerate_results = collision.castRaysGPU(degenerate_queries, degenerate_stats);
4280 DOCTEST_CHECK(degenerate_results.size() == 1);
4281 DOCTEST_CHECK(degenerate_results[0].hit == false); // Should handle gracefully
4282
4283 } else {
4284 DOCTEST_WARN("GPU acceleration not available - skipping GPU error handling tests");
4285 }
4286#endif
4287 } catch (std::exception &e) {
4288 DOCTEST_WARN((std::string("GPU error handling test failed: ") + e.what()).c_str());
4289 }
4290}
4291
4292// -------- FLOATING-POINT PRECISION EDGE CASE TESTS --------
4293
4294DOCTEST_TEST_CASE("CollisionDetection Floating-Point Precision - Extreme Values") {
4295 Context context;
4296 CollisionDetection collision(&context);
4297 collision.disableMessages();
4298 collision.disableGPUAcceleration();
4299
4300 // Test 1: Very small triangles
4301 float epsilon = 1e-6f;
4302 vec3 v0_small = make_vec3(0, 0, 0);
4303 vec3 v1_small = make_vec3(epsilon, 0, 0);
4304 vec3 v2_small = make_vec3(epsilon / 2.0f, epsilon, 0);
4305 uint small_triangle = context.addTriangle(v0_small, v1_small, v2_small);
4306
4307 vec3 ray_origin = make_vec3(epsilon / 3.0f, epsilon / 3.0f, -epsilon);
4308 vec3 ray_direction = make_vec3(0, 0, 1);
4309
4310 CollisionDetection::HitResult small_result = collision.castRay(ray_origin, ray_direction);
4311 // Should either hit or miss consistently, not produce NaN/inf
4312 DOCTEST_CHECK(std::isfinite(small_result.distance));
4313 DOCTEST_CHECK(std::isfinite(small_result.intersection_point.x));
4314 DOCTEST_CHECK(std::isfinite(small_result.intersection_point.y));
4315 DOCTEST_CHECK(std::isfinite(small_result.intersection_point.z));
4316
4317 // Test 2: Very large triangles
4318 float large_scale = 1e6f;
4319 vec3 v0_large = make_vec3(-large_scale, -large_scale, 0);
4320 vec3 v1_large = make_vec3(large_scale, -large_scale, 0);
4321 vec3 v2_large = make_vec3(0, large_scale, 0);
4322 uint large_triangle = context.addTriangle(v0_large, v1_large, v2_large);
4323
4324 vec3 large_ray_origin = make_vec3(0, 0, -large_scale);
4325 vec3 large_ray_direction = make_vec3(0, 0, 1);
4326
4327 CollisionDetection::HitResult large_result = collision.castRay(large_ray_origin, large_ray_direction);
4328 DOCTEST_CHECK(std::isfinite(large_result.distance));
4329 if (large_result.hit) {
4330 DOCTEST_CHECK(std::isfinite(large_result.intersection_point.x));
4331 DOCTEST_CHECK(std::isfinite(large_result.intersection_point.y));
4332 DOCTEST_CHECK(std::isfinite(large_result.intersection_point.z));
4333 }
4334}
4335
4336DOCTEST_TEST_CASE("CollisionDetection Floating-Point Precision - Near-Parallel Rays") {
4337 Context context;
4338 CollisionDetection collision(&context);
4339 collision.disableMessages();
4340 collision.disableGPUAcceleration();
4341
4342 // Create triangle in XY plane
4343 vec3 v0 = make_vec3(0, 0, 0);
4344 vec3 v1 = make_vec3(1, 0, 0);
4345 vec3 v2 = make_vec3(0.5f, 1, 0);
4346 uint triangle_uuid = context.addTriangle(v0, v1, v2);
4347
4348 // Test rays nearly parallel to triangle plane
4349 float tiny_angle = 1e-6f;
4350 vec3 near_parallel_origin = make_vec3(0.5f, 0.5f, -1);
4351 vec3 near_parallel_direction = normalize(make_vec3(0, tiny_angle, 1));
4352
4353 CollisionDetection::HitResult near_parallel_result = collision.castRay(near_parallel_origin, near_parallel_direction);
4354
4355 // Should handle gracefully without numerical instability
4356 DOCTEST_CHECK(std::isfinite(near_parallel_result.distance));
4357 if (near_parallel_result.hit) {
4358 DOCTEST_CHECK(std::isfinite(near_parallel_result.intersection_point.x));
4359 DOCTEST_CHECK(std::isfinite(near_parallel_result.intersection_point.y));
4360 DOCTEST_CHECK(std::isfinite(near_parallel_result.intersection_point.z));
4361 DOCTEST_CHECK(near_parallel_result.distance > 0);
4362 }
4363}
4364
4365DOCTEST_TEST_CASE("CollisionDetection Floating-Point Precision - Boundary Conditions") {
4366 Context context;
4367 CollisionDetection collision(&context);
4368 collision.disableMessages();
4369 collision.disableGPUAcceleration();
4370
4371 // Create unit triangle
4372 vec3 v0 = make_vec3(0, 0, 0);
4373 vec3 v1 = make_vec3(1, 0, 0);
4374 vec3 v2 = make_vec3(0, 1, 0);
4375 uint triangle_uuid = context.addTriangle(v0, v1, v2);
4376
4377 // Test rays just outside triangle boundaries
4378 float boundary_offset = 1e-8f;
4379
4380 std::vector<vec3> boundary_origins = {
4381 make_vec3(-boundary_offset, 0.5f, -1), // Just outside left edge
4382 make_vec3(1 + boundary_offset, 0.5f, -1), // Just outside right edge
4383 make_vec3(0.5f, -boundary_offset, -1), // Just outside bottom edge
4384 make_vec3(0.5f + boundary_offset, 0.5f + boundary_offset, -1) // Just outside diagonal edge
4385 };
4386
4387 for (const auto &origin: boundary_origins) {
4388 vec3 ray_direction = make_vec3(0, 0, 1);
4389 CollisionDetection::HitResult result = collision.castRay(origin, ray_direction);
4390
4391 // Results should be consistent and not produce artifacts
4392 DOCTEST_CHECK(std::isfinite(result.distance));
4393 if (result.hit) {
4394 DOCTEST_CHECK(std::isfinite(result.intersection_point.x));
4395 DOCTEST_CHECK(std::isfinite(result.intersection_point.y));
4396 DOCTEST_CHECK(std::isfinite(result.intersection_point.z));
4397 }
4398 }
4399}
4400
4401// -------- COMPLEX GEOMETRY ACCURACY VALIDATION TESTS --------
4402
4403DOCTEST_TEST_CASE("CollisionDetection Complex Geometry - Multi-Primitive Accuracy") {
4404 Context context;
4405 CollisionDetection collision(&context);
4406 collision.disableMessages();
4407 collision.disableGPUAcceleration();
4408
4409 // Create complex scene with overlapping and adjacent primitives
4410 std::vector<uint> uuids;
4411
4412 // Grid of triangles with known intersection patterns
4413 for (int i = 0; i < 5; i++) {
4414 for (int j = 0; j < 5; j++) {
4415 float x = i * 0.8f;
4416 float y = j * 0.8f;
4417 uint uuid = context.addTriangle(make_vec3(x, y, 0), make_vec3(x + 0.5f, y, 0), make_vec3(x + 0.25f, y + 0.5f, 0));
4418 uuids.push_back(uuid);
4419 }
4420 }
4421
4422 // Test systematic ray grid
4423 int correct_predictions = 0;
4424 int total_predictions = 0;
4425
4426 for (int i = 0; i < 10; i++) {
4427 for (int j = 0; j < 10; j++) {
4428 float x = i * 0.4f;
4429 float y = j * 0.4f;
4430
4431 vec3 ray_origin = make_vec3(x, y, -1);
4432 vec3 ray_direction = make_vec3(0, 0, 1);
4433
4434 CollisionDetection::HitResult result = collision.castRay(ray_origin, ray_direction);
4435
4436 // Manually determine if this point should hit any triangle
4437 bool should_hit = false;
4438 for (int ti = 0; ti < 5; ti++) {
4439 for (int tj = 0; tj < 5; tj++) {
4440 float tx = ti * 0.8f;
4441 float ty = tj * 0.8f;
4442
4443 // Simple point-in-triangle test for validation
4444 vec3 p = make_vec3(x, y, 0);
4445 vec3 a = make_vec3(tx, ty, 0);
4446 vec3 b = make_vec3(tx + 0.5f, ty, 0);
4447 vec3 c = make_vec3(tx + 0.25f, ty + 0.5f, 0);
4448
4449 // Barycentric coordinate test
4450 vec3 v0 = c - a;
4451 vec3 v1 = b - a;
4452 vec3 v2 = p - a;
4453
4454 float dot00 = v0.x * v0.x + v0.y * v0.y + v0.z * v0.z;
4455 float dot01 = v0.x * v1.x + v0.y * v1.y + v0.z * v1.z;
4456 float dot02 = v0.x * v2.x + v0.y * v2.y + v0.z * v2.z;
4457 float dot11 = v1.x * v1.x + v1.y * v1.y + v1.z * v1.z;
4458 float dot12 = v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;
4459
4460 float inv_denom = 1.0f / (dot00 * dot11 - dot01 * dot01);
4461 float u = (dot11 * dot02 - dot01 * dot12) * inv_denom;
4462 float v = (dot00 * dot12 - dot01 * dot02) * inv_denom;
4463
4464 // Use epsilon tolerance for floating-point edge cases
4465 const float EPSILON = 1e-6f;
4466 if ((u >= -EPSILON) && (v >= -EPSILON) && (u + v <= 1 + EPSILON)) {
4467 should_hit = true;
4468 break;
4469 }
4470 }
4471 if (should_hit)
4472 break;
4473 }
4474
4475 if (result.hit == should_hit) {
4476 correct_predictions++;
4477 }
4478 total_predictions++;
4479 }
4480 }
4481
4482 // Should have perfect accuracy in complex geometry with proper floating-point handling
4483 float accuracy = (float) correct_predictions / (float) total_predictions;
4484 DOCTEST_CHECK(accuracy == 1.0f);
4485}
4486
4487DOCTEST_TEST_CASE("CollisionDetection Complex Geometry - Stress Test Validation") {
4488 Context context;
4489 CollisionDetection collision(&context);
4490 collision.disableMessages();
4491 collision.disableGPUAcceleration();
4492
4493 // Create stress test geometry - many small overlapping triangles
4494 std::vector<uint> stress_uuids;
4495 for (int i = 0; i < 1000; i++) {
4496 float x = (rand() % 100) * 0.01f;
4497 float y = (rand() % 100) * 0.01f;
4498 float z = (rand() % 10) * 0.01f;
4499 float size = 0.1f + (rand() % 10) * 0.01f;
4500
4501 uint uuid = context.addTriangle(make_vec3(x, y, z), make_vec3(x + size, y, z), make_vec3(x + size / 2.0f, y + size, z));
4502 stress_uuids.push_back(uuid);
4503 }
4504
4505 // Test random rays for consistency and correctness
4506 std::vector<CollisionDetection::RayQuery> stress_queries;
4507 for (int i = 0; i < 500; i++) {
4509 query.origin = make_vec3((rand() % 200) * 0.01f - 1.0f, (rand() % 200) * 0.01f - 1.0f, -1.0f);
4510 query.direction = normalize(make_vec3(0, 0, 1));
4511 query.max_distance = 10.0f;
4512 stress_queries.push_back(query);
4513 }
4514
4516 std::vector<CollisionDetection::HitResult> stress_results = collision.castRays(stress_queries, &stats);
4517
4518 DOCTEST_CHECK(stress_results.size() == stress_queries.size());
4519 DOCTEST_CHECK(stats.total_rays_cast == stress_queries.size());
4520
4521 // Validate all results are mathematically sound
4522 int valid_results = 0;
4523 for (const auto &result: stress_results) {
4524 if (std::isfinite(result.distance) && std::isfinite(result.intersection_point.x) && std::isfinite(result.intersection_point.y) && std::isfinite(result.intersection_point.z) && (result.hit ? result.distance >= 0 : true)) {
4525 valid_results++;
4526 }
4527 }
4528
4529 DOCTEST_CHECK(valid_results == (int) stress_results.size());
4530}
4531
4532// -------- PERFORMANCE REGRESSION TESTS --------
4533
4534DOCTEST_TEST_CASE("CollisionDetection Performance Regression - BVH Construction Timing") {
4535 Context context;
4536 CollisionDetection collision(&context);
4537 collision.disableMessages();
4538
4539 // Create large geometry set
4540 std::vector<uint> large_geometry;
4541 for (int i = 0; i < 5000; i++) {
4542 float x = i * 0.1f;
4543 uint uuid = context.addTriangle(make_vec3(x, -0.5f, 0), make_vec3(x + 0.05f, -0.5f, 0), make_vec3(x + 0.025f, 0.5f, 0));
4544 large_geometry.push_back(uuid);
4545 }
4546
4547 // Measure BVH construction time
4548 auto start_time = std::chrono::high_resolution_clock::now();
4549 collision.buildBVH();
4550 auto end_time = std::chrono::high_resolution_clock::now();
4551
4552 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
4553
4554 // BVH construction should complete in reasonable time (< 5 seconds for 5k primitives)
4555 DOCTEST_CHECK(duration.count() < 5000);
4556
4557 // Verify BVH validity after construction
4558 DOCTEST_CHECK(collision.isBVHValid() == true);
4559 DOCTEST_CHECK(collision.getPrimitiveCount() == large_geometry.size());
4560
4561 size_t node_count, leaf_count, max_depth;
4562 collision.getBVHStatistics(node_count, leaf_count, max_depth);
4563
4564 // Sanity checks on BVH structure
4565 DOCTEST_CHECK(node_count > 0);
4566 DOCTEST_CHECK(leaf_count > 0);
4567 DOCTEST_CHECK(max_depth > 0);
4568 DOCTEST_CHECK(max_depth < 50); // Should not be excessively deep
4569}
4570
4571DOCTEST_TEST_CASE("CollisionDetection Performance Regression - Ray Casting Throughput") {
4572 Context context;
4573 CollisionDetection collision(&context);
4574 collision.disableMessages();
4575 collision.disableGPUAcceleration();
4576
4577 // Create moderate complexity scene
4578 for (int i = 0; i < 1000; i++) {
4579 float x = (i % 50) * 0.2f;
4580 float y = (i / 50) * 0.2f;
4581 uint uuid = context.addTriangle(make_vec3(x, y, 0), make_vec3(x + 0.1f, y, 0), make_vec3(x + 0.05f, y + 0.1f, 0));
4582 }
4583
4584 // Create large ray batch
4585 std::vector<CollisionDetection::RayQuery> throughput_queries;
4586 for (int i = 0; i < 10000; i++) {
4588 query.origin = make_vec3((i % 100) * 0.1f, (i / 100) * 0.1f, -1.0f);
4589 query.direction = normalize(make_vec3(0, 0, 1));
4590 throughput_queries.push_back(query);
4591 }
4592
4593 // Measure ray casting performance
4594 auto start_time = std::chrono::high_resolution_clock::now();
4596 std::vector<CollisionDetection::HitResult> results = collision.castRays(throughput_queries, &stats);
4597 auto end_time = std::chrono::high_resolution_clock::now();
4598
4599 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
4600
4601 // Should process 10k rays in reasonable time (< 2 seconds)
4602 DOCTEST_CHECK(duration.count() < 2000);
4603
4604 // Verify results quality
4605 DOCTEST_CHECK(results.size() == throughput_queries.size());
4606 DOCTEST_CHECK(stats.total_rays_cast == throughput_queries.size());
4607
4608 // Calculate rays per second
4609 float rays_per_second = (float) throughput_queries.size() / (duration.count() / 1000.0f);
4610 DOCTEST_CHECK(rays_per_second > 1000.0f); // Should achieve at least 1k rays/sec
4611}
4612
4613DOCTEST_TEST_CASE("CollisionDetection Performance Regression - Memory Usage Validation") {
4614 Context context;
4615 CollisionDetection collision(&context);
4616 collision.disableMessages();
4617
4618 // Test memory efficiency with different BVH modes
4619 for (int i = 0; i < 2000; i++) {
4620 float x = i * 0.05f;
4621 uint uuid = context.addTriangle(make_vec3(x, -0.25f, 0), make_vec3(x + 0.025f, -0.25f, 0), make_vec3(x + 0.0125f, 0.25f, 0));
4622 }
4623
4624 // Test different optimization modes
4625 collision.setBVHOptimizationMode(CollisionDetection::BVHOptimizationMode::SOA_UNCOMPRESSED);
4626 collision.buildBVH();
4627
4628 auto memory_stats = collision.getBVHMemoryUsage();
4629
4630 // Memory usage should be reasonable (< 100MB for 2k primitives)
4631 DOCTEST_CHECK(memory_stats.soa_memory_bytes < 100 * 1024 * 1024);
4632 DOCTEST_CHECK(memory_stats.soa_memory_bytes > 0);
4633
4634 // Test ray streaming memory efficiency
4636 std::vector<CollisionDetection::RayQuery> stream_queries;
4637
4638 for (int i = 0; i < 5000; i++) {
4640 query.origin = make_vec3(i * 0.01f, 0, -1);
4641 query.direction = normalize(make_vec3(0, 0, 1));
4642 stream_queries.push_back(query);
4643 }
4644
4645 stream.addRays(stream_queries);
4646
4647 // Stream memory usage should be reasonable
4648 size_t stream_memory = stream.getMemoryUsage();
4649 DOCTEST_CHECK(stream_memory < 50 * 1024 * 1024); // < 50MB for 5k rays
4650 DOCTEST_CHECK(stream_memory > 0);
4651}
4652
4653// -------- RAY-PATCH MATHEMATICAL ACCURACY TESTS --------
4654
4655DOCTEST_TEST_CASE("CollisionDetection Mathematical Accuracy - Ray-Patch Intersection") {
4656 Context context;
4657 CollisionDetection collision(&context);
4658 collision.disableMessages();
4659 collision.disableGPUAcceleration(); // Use CPU for deterministic results
4660
4661 // Test 1: Known analytical patch intersection
4662 vec3 v0 = make_vec3(0, 0, 0); // Bottom-left
4663 vec3 v1 = make_vec3(1, 0, 0); // Bottom-right
4664 vec3 v2 = make_vec3(0, 1, 0); // Top-left
4665 vec3 v3 = make_vec3(1, 1, 0); // Top-right
4666 uint patch_uuid = context.addPatch(v0, make_vec2(1, 1));
4667
4668 // Ray hitting center of patch at (0.5, 0.5, 0)
4669 vec3 ray_origin = make_vec3(0.5f, 0.5f, -1.0f);
4670 vec3 ray_direction = make_vec3(0, 0, 1);
4671
4672 CollisionDetection::HitResult result = collision.castRay(ray_origin, ray_direction);
4673
4674 DOCTEST_CHECK(result.hit == true);
4675 DOCTEST_CHECK(result.primitive_UUID == patch_uuid);
4676
4677 // Mathematical validation: distance should be exactly 1.0
4678 DOCTEST_CHECK(std::abs(result.distance - 1.0f) < 1e-6f);
4679
4680 // Intersection point should be exactly at (0.5, 0.5, 0)
4681 DOCTEST_CHECK(std::abs(result.intersection_point.x - 0.5f) < 1e-6f);
4682 DOCTEST_CHECK(std::abs(result.intersection_point.y - 0.5f) < 1e-6f);
4683 DOCTEST_CHECK(std::abs(result.intersection_point.z - 0.0f) < 1e-6f);
4684}
4685
4686DOCTEST_TEST_CASE("CollisionDetection Mathematical Accuracy - Ray-Patch Edge Cases") {
4687 Context context;
4688 CollisionDetection collision(&context);
4689 collision.disableMessages();
4690 collision.disableGPUAcceleration();
4691
4692 // Create a patch at origin
4693 uint patch_uuid = context.addPatch(make_vec3(0, 0, 0), make_vec2(2, 2));
4694
4695 // Test 1: Ray hitting patch edge (should HIT with proper epsilon tolerance)
4696 vec3 edge_ray_origin = make_vec3(1, 0, -1); // Hit right edge at (1, 0, 0)
4697 vec3 edge_ray_direction = make_vec3(0, 0, 1);
4698
4699 CollisionDetection::HitResult edge_result = collision.castRay(edge_ray_origin, edge_ray_direction);
4700
4701 DOCTEST_CHECK(edge_result.hit == true);
4702 DOCTEST_CHECK(std::abs(edge_result.intersection_point.x - 1.0f) < 1e-6f);
4703 DOCTEST_CHECK(std::abs(edge_result.intersection_point.y - 0.0f) < 1e-6f);
4704 DOCTEST_CHECK(std::abs(edge_result.intersection_point.z - 0.0f) < 1e-6f);
4705
4706 // Test 2: Ray hitting patch corner (should HIT with proper epsilon tolerance)
4707 vec3 corner_ray_origin = make_vec3(0, 0, -1); // Hit corner at (0, 0, 0)
4708 vec3 corner_ray_direction = make_vec3(0, 0, 1);
4709
4710 CollisionDetection::HitResult corner_result = collision.castRay(corner_ray_origin, corner_ray_direction);
4711
4712 DOCTEST_CHECK(corner_result.hit == true);
4713 DOCTEST_CHECK(std::abs(corner_result.intersection_point.x - 0.0f) < 1e-6f);
4714 DOCTEST_CHECK(std::abs(corner_result.intersection_point.y - 0.0f) < 1e-6f);
4715 DOCTEST_CHECK(std::abs(corner_result.intersection_point.z - 0.0f) < 1e-6f);
4716}
4717
4718DOCTEST_TEST_CASE("CollisionDetection Complex Geometry - Multi-Patch Accuracy") {
4719 Context context;
4720 CollisionDetection collision(&context);
4721 collision.disableMessages();
4722 collision.disableGPUAcceleration();
4723
4724 // Create complex scene with overlapping patch edges - similar to triangle test
4725 std::vector<uint> uuids;
4726
4727 // Grid of patches with known intersection patterns
4728 for (int i = 0; i < 5; i++) {
4729 for (int j = 0; j < 5; j++) {
4730 float x = i * 0.8f;
4731 float y = j * 0.8f;
4732 uint uuid = context.addPatch(make_vec3(x, y, 0), make_vec2(0.5f, 0.5f));
4733 uuids.push_back(uuid);
4734 }
4735 }
4736
4737 // Test systematic ray grid
4738 int correct_predictions = 0;
4739 int total_predictions = 0;
4740
4741 for (int i = 0; i < 10; i++) {
4742 for (int j = 0; j < 10; j++) {
4743 float x = i * 0.4f;
4744 float y = j * 0.4f;
4745
4746 vec3 ray_origin = make_vec3(x, y, -1);
4747 vec3 ray_direction = make_vec3(0, 0, 1);
4748
4749 CollisionDetection::HitResult result = collision.castRay(ray_origin, ray_direction);
4750
4751 // Manually determine if this point should hit any patch
4752 bool should_hit = false;
4753 for (int ti = 0; ti < 5; ti++) {
4754 for (int tj = 0; tj < 5; tj++) {
4755 float px = ti * 0.8f;
4756 float py = tj * 0.8f;
4757
4758 // Check if ray point is within patch bounds
4759 const float EPSILON = 1e-6f;
4760 if ((x >= px - 0.25f - EPSILON) && (x <= px + 0.25f + EPSILON) && (y >= py - 0.25f - EPSILON) && (y <= py + 0.25f + EPSILON)) {
4761 should_hit = true;
4762 break;
4763 }
4764 }
4765 if (should_hit)
4766 break;
4767 }
4768
4769 if (result.hit == should_hit) {
4770 correct_predictions++;
4771 }
4772 total_predictions++;
4773 }
4774 }
4775
4776 // Should have perfect accuracy in complex patch geometry
4777 float accuracy = (float) correct_predictions / (float) total_predictions;
4778 DOCTEST_CHECK(accuracy == 1.0f);
4779}
4780
4781// -------- RAY-VOXEL MATHEMATICAL ACCURACY TESTS --------
4782
4783DOCTEST_TEST_CASE("CollisionDetection Mathematical Accuracy - Ray-Voxel Intersection") {
4784 Context context;
4785 CollisionDetection collision(&context);
4786 collision.disableMessages();
4787 collision.disableGPUAcceleration(); // Use CPU for deterministic results
4788
4789 // Test 1: Known analytical voxel intersection
4790 uint voxel_uuid = context.addVoxel(make_vec3(0.5f, 0.5f, 0.5f), make_vec3(1, 1, 1));
4791
4792 // Ray hitting center of voxel front face at (0.5, 0.5, 0)
4793 vec3 ray_origin = make_vec3(0.5f, 0.5f, -1.0f);
4794 vec3 ray_direction = make_vec3(0, 0, 1);
4795
4796 CollisionDetection::HitResult result = collision.castRay(ray_origin, ray_direction);
4797
4798 DOCTEST_CHECK(result.hit == true);
4799 DOCTEST_CHECK(result.primitive_UUID == voxel_uuid);
4800
4801 // Mathematical validation: distance should be exactly 1.0 to reach front face
4802 DOCTEST_CHECK(std::abs(result.distance - 1.0f) < 1e-6f);
4803
4804 // Intersection point should be exactly at (0.5, 0.5, 0) - front face of voxel
4805 DOCTEST_CHECK(std::abs(result.intersection_point.x - 0.5f) < 1e-6f);
4806 DOCTEST_CHECK(std::abs(result.intersection_point.y - 0.5f) < 1e-6f);
4807 DOCTEST_CHECK(std::abs(result.intersection_point.z - 0.0f) < 1e-6f);
4808}
4809
4810DOCTEST_TEST_CASE("CollisionDetection Mathematical Accuracy - Ray-Voxel Edge Cases") {
4811 Context context;
4812 CollisionDetection collision(&context);
4813 collision.disableMessages();
4814 collision.disableGPUAcceleration();
4815
4816 // Create a voxel at origin with size (2, 2, 2) - extends from (-1,-1,-1) to (1,1,1)
4817 uint voxel_uuid = context.addVoxel(make_vec3(0, 0, 0), make_vec3(2, 2, 2));
4818
4819 // Test 1: Ray hitting voxel edge (should HIT with proper epsilon tolerance)
4820 vec3 edge_ray_origin = make_vec3(1, 0, -2); // Hit right edge at (1, 0, -1)
4821 vec3 edge_ray_direction = make_vec3(0, 0, 1);
4822
4823 CollisionDetection::HitResult edge_result = collision.castRay(edge_ray_origin, edge_ray_direction);
4824
4825 DOCTEST_CHECK(edge_result.hit == true);
4826 DOCTEST_CHECK(edge_result.primitive_UUID == voxel_uuid);
4827 DOCTEST_CHECK(std::abs(edge_result.distance - 1.0f) < 1e-6f);
4828 DOCTEST_CHECK(std::abs(edge_result.intersection_point.x - 1.0f) < 1e-6f);
4829 DOCTEST_CHECK(std::abs(edge_result.intersection_point.y - 0.0f) < 1e-6f);
4830 DOCTEST_CHECK(std::abs(edge_result.intersection_point.z + 1.0f) < 1e-6f);
4831
4832 // Test 2: Ray hitting voxel corner (should HIT)
4833 vec3 corner_ray_origin = make_vec3(-1, -1, -2); // Hit corner at (-1, -1, -1)
4834 vec3 corner_ray_direction = make_vec3(0, 0, 1);
4835
4836 CollisionDetection::HitResult corner_result = collision.castRay(corner_ray_origin, corner_ray_direction);
4837
4838 DOCTEST_CHECK(corner_result.hit == true);
4839 DOCTEST_CHECK(std::abs(corner_result.intersection_point.x + 1.0f) < 1e-6f);
4840 DOCTEST_CHECK(std::abs(corner_result.intersection_point.y + 1.0f) < 1e-6f);
4841 DOCTEST_CHECK(std::abs(corner_result.intersection_point.z + 1.0f) < 1e-6f);
4842}
4843
4844DOCTEST_TEST_CASE("CollisionDetection Complex Geometry - Multi-Voxel Accuracy") {
4845 Context context;
4846 CollisionDetection collision(&context);
4847 collision.disableMessages();
4848 collision.disableGPUAcceleration();
4849
4850 // Create complex scene with voxel grid - similar to patch test but in 3D
4851 std::vector<uint> uuids;
4852
4853 // Grid of voxels with known intersection patterns (3x3x3 grid)
4854 for (int i = 0; i < 3; i++) {
4855 for (int j = 0; j < 3; j++) {
4856 for (int k = 0; k < 3; k++) {
4857 float x = i * 1.5f; // Voxel centers spaced 1.5 units apart
4858 float y = j * 1.5f;
4859 float z = k * 1.5f;
4860 uint uuid = context.addVoxel(make_vec3(x, y, z), make_vec3(1, 1, 1)); // Size 1x1x1
4861 uuids.push_back(uuid);
4862 }
4863 }
4864 }
4865
4866 // Test systematic ray grid in XY plane at z=-1
4867 int correct_predictions = 0;
4868 int total_predictions = 0;
4869
4870 for (int i = 0; i < 10; i++) {
4871 for (int j = 0; j < 10; j++) {
4872 float x = i * 0.4f; // Ray grid with 0.4 spacing
4873 float y = j * 0.4f;
4874
4875 vec3 ray_origin = make_vec3(x, y, -1);
4876 vec3 ray_direction = make_vec3(0, 0, 1);
4877
4878 CollisionDetection::HitResult result = collision.castRay(ray_origin, ray_direction);
4879
4880 // Manually determine if this point should hit any voxel at z=0 plane
4881 bool should_hit = false;
4882 for (int vi = 0; vi < 3; vi++) {
4883 for (int vj = 0; vj < 3; vj++) {
4884 for (int vk = 0; vk < 3; vk++) {
4885 float vx = vi * 1.5f; // voxel center x
4886 float vy = vj * 1.5f; // voxel center y
4887 float vz = vk * 1.5f; // voxel center z
4888
4889 // Voxel bounds: center ± size/2 = center ± 0.5
4890 float voxel_x_min = vx - 0.5f;
4891 float voxel_x_max = vx + 0.5f;
4892 float voxel_y_min = vy - 0.5f;
4893 float voxel_y_max = vy + 0.5f;
4894 float voxel_z_min = vz - 0.5f;
4895 float voxel_z_max = vz + 0.5f;
4896
4897 // Check if ray hits this voxel
4898 const float EPSILON = 1e-6f;
4899 if ((x >= voxel_x_min - EPSILON) && (x <= voxel_x_max + EPSILON) && (y >= voxel_y_min - EPSILON) && (y <= voxel_y_max + EPSILON)) {
4900 // Check if ray z-range overlaps with voxel z-range
4901 if (voxel_z_max >= -1.0f - EPSILON) {
4902 should_hit = true;
4903 break;
4904 }
4905 }
4906 }
4907 if (should_hit)
4908 break;
4909 }
4910 if (should_hit)
4911 break;
4912 }
4913
4914 if (result.hit == should_hit) {
4915 correct_predictions++;
4916 }
4917 total_predictions++;
4918 }
4919 }
4920
4921 // Should have perfect accuracy in complex voxel geometry
4922 float accuracy = (float) correct_predictions / (float) total_predictions;
4923 DOCTEST_CHECK(accuracy == 1.0f);
4924}
4925
4926DOCTEST_TEST_CASE("CollisionDetection Ray Classification - Basic getVoxelRayHitCounts Functionality") {
4927 Context context;
4928 CollisionDetection collision(&context);
4929 collision.disableMessages();
4930
4931 // Create a simple test geometry - single triangle in the middle of a voxel
4932 uint triangle_uuid = context.addTriangle(make_vec3(-0.5, -0.5, 0), make_vec3(0.5, -0.5, 0), make_vec3(0, 0.5, 0));
4933
4934 // Set up voxel grid centered at origin
4935 vec3 grid_center(0, 0, 0);
4936 vec3 grid_size(4, 4, 4);
4937 int3 grid_divisions(2, 2, 2); // 2x2x2 grid
4938
4939 // Test rays with known behavior
4940 std::vector<vec3> ray_origins;
4941 std::vector<vec3> ray_directions;
4942
4943 // Ray 1: hits triangle before entering voxel (0,0,0)
4944 ray_origins.push_back(make_vec3(0, 0, -3)); // Start outside grid
4945 ray_directions.push_back(make_vec3(0, 0, 1)); // Ray towards triangle at z=0
4946
4947 // Ray 2: passes through voxel without hitting anything
4948 ray_origins.push_back(make_vec3(-1.5, -1.5, -3)); // Ray in corner voxel (0,0,0)
4949 ray_directions.push_back(make_vec3(0, 0, 1)); // Parallel to z-axis, misses triangle
4950
4951 // Ray 3: hits triangle inside voxel
4952 ray_origins.push_back(make_vec3(0, 0, -0.5)); // Start inside voxel (1,1,0)
4953 ray_directions.push_back(make_vec3(0, 0, 1)); // Hit triangle at z=0
4954
4955 // Calculate voxel ray path lengths with classification
4956 collision.calculateVoxelRayPathLengths(grid_center, grid_size, grid_divisions, ray_origins, ray_directions);
4957
4958 // Test voxel (1,1,0) - contains the triangle and center rays
4959 int hit_before, hit_after, hit_inside;
4960 collision.getVoxelRayHitCounts(make_int3(1, 1, 0), hit_before, hit_after, hit_inside);
4961
4962 // Verify hit counts - expect rays 1 and 3 to intersect this voxel
4963 DOCTEST_CHECK(hit_before >= 0); // May have rays hitting before voxel
4964 DOCTEST_CHECK(hit_after >= 0); // May have rays reaching after voxel entry
4965 DOCTEST_CHECK(hit_inside >= 0); // May have rays hitting inside voxel
4966
4967 // Test voxel (0,0,0) - corner voxel with ray 2
4968 collision.getVoxelRayHitCounts(make_int3(0, 0, 0), hit_before, hit_after, hit_inside);
4969
4970 // For corner voxel, ray 2 should pass through without hitting geometry
4971 DOCTEST_CHECK(hit_before >= 0);
4972 DOCTEST_CHECK(hit_after >= 0);
4973 DOCTEST_CHECK(hit_inside >= 0);
4974}
4975
4976DOCTEST_TEST_CASE("CollisionDetection Ray Classification - getVoxelRayPathLengths Individual Lengths") {
4977 Context context;
4978 CollisionDetection collision(&context);
4979 collision.disableMessages();
4980
4981 // Simple 1x1x1 voxel grid for precise control
4982 vec3 grid_center(0, 0, 0);
4983 vec3 grid_size(2, 2, 2); // Voxel spans from (-1,-1,-1) to (1,1,1)
4984 int3 grid_divisions(1, 1, 1);
4985
4986 // Create rays with known path lengths through the voxel
4987 std::vector<vec3> ray_origins;
4988 std::vector<vec3> ray_directions;
4989
4990 // Ray 1: Straight through center, should have path length = 2.0
4991 ray_origins.push_back(make_vec3(0, 0, -2));
4992 ray_directions.push_back(make_vec3(0, 0, 1));
4993
4994 // Ray 2: Diagonal corner-to-corner, path length = 2*sqrt(3) ≈ 3.464
4995 ray_origins.push_back(make_vec3(-2, -2, -2));
4996 ray_directions.push_back(normalize(make_vec3(1, 1, 1)));
4997
4998 // Ray 3: Edge crossing, specific path length calculation
4999 ray_origins.push_back(make_vec3(0, -2, -2));
5000 ray_directions.push_back(normalize(make_vec3(0, 1, 1))); // Path length = 2*sqrt(2) ≈ 2.828
5001
5002 collision.calculateVoxelRayPathLengths(grid_center, grid_size, grid_divisions, ray_origins, ray_directions);
5003
5004 // Get individual path lengths for the single voxel (0,0,0)
5005 std::vector<float> path_lengths = collision.getVoxelRayPathLengths(make_int3(0, 0, 0));
5006
5007 // Should have path lengths for all rays that intersected the voxel
5008 DOCTEST_CHECK(path_lengths.size() >= 1); // At least one ray should intersect
5009
5010 // Verify path lengths are reasonable (between 0 and voxel diagonal)
5011 float max_diagonal = 2.0f * sqrt(3.0f); // Maximum possible path through voxel
5012 for (float length: path_lengths) {
5013 DOCTEST_CHECK(length > 0.0f);
5014 DOCTEST_CHECK(length <= max_diagonal + 1e-6f); // Allow small numerical tolerance
5015 }
5016
5017 // Check that we can identify the expected path lengths (within tolerance)
5018 bool found_center_ray = false;
5019 bool found_diagonal_ray = false;
5020
5021 for (float length: path_lengths) {
5022 if (std::abs(length - 2.0f) < 0.1f) { // Center ray
5023 found_center_ray = true;
5024 }
5025 if (std::abs(length - 2.0f * sqrt(3.0f)) < 0.1f) { // Diagonal ray
5026 found_diagonal_ray = true;
5027 }
5028 }
5029
5030 DOCTEST_CHECK(found_center_ray); // Should find the straight-through ray
5031}
5032
5033DOCTEST_TEST_CASE("CollisionDetection Ray Classification - Beer's Law Scenario with Geometry") {
5034 Context context;
5035 CollisionDetection collision(&context);
5036 collision.disableMessages();
5037
5038 // Create realistic Beer's law test scenario
5039 // Single patch in center voxel to create known occlusion pattern
5040 uint patch_uuid = context.addPatch(make_vec3(0, 0, 0), make_vec2(0.8, 0.8));
5041
5042 vec3 grid_center(0, 0, 0);
5043 vec3 grid_size(6, 6, 6);
5044 int3 grid_divisions(3, 3, 3); // 3x3x3 grid
5045
5046 // Grid of parallel rays from below (simulating LiDAR from ground)
5047 std::vector<vec3> ray_origins;
5048 std::vector<vec3> ray_directions;
5049
5050 int num_rays_per_axis = 10;
5051 for (int i = 0; i < num_rays_per_axis; i++) {
5052 for (int j = 0; j < num_rays_per_axis; j++) {
5053 float x = -2.5f + (5.0f * i) / (num_rays_per_axis - 1); // Spread across grid
5054 float y = -2.5f + (5.0f * j) / (num_rays_per_axis - 1);
5055
5056 ray_origins.push_back(make_vec3(x, y, -4));
5057 ray_directions.push_back(make_vec3(0, 0, 1)); // All rays pointing up
5058 }
5059 }
5060
5061 collision.calculateVoxelRayPathLengths(grid_center, grid_size, grid_divisions, ray_origins, ray_directions);
5062
5063 // Test center voxel (1,1,1) which contains the patch
5064 int hit_before, hit_after, hit_inside;
5065 collision.getVoxelRayHitCounts(make_int3(1, 1, 1), hit_before, hit_after, hit_inside);
5066
5067 // With parallel upward rays and patch at z=0, expect:
5068 // - hit_before = 0 (no geometry below patch)
5069 // - hit_inside > 0 (some rays hit the patch inside voxel)
5070 // - hit_after depends on rays that pass through without hitting
5071
5072 DOCTEST_CHECK(hit_before >= 0);
5073 DOCTEST_CHECK(hit_after >= 0);
5074 DOCTEST_CHECK(hit_inside >= 0);
5075 DOCTEST_CHECK((hit_before + hit_after + hit_inside) > 0); // Total hits should be positive
5076
5077 // Verify path lengths exist for this voxel
5078 std::vector<float> path_lengths = collision.getVoxelRayPathLengths(make_int3(1, 1, 1));
5079 DOCTEST_CHECK(path_lengths.size() > 0); // Should have rays passing through center voxel
5080
5081 // Test corner voxel that should have fewer intersections
5082 collision.getVoxelRayHitCounts(make_int3(0, 0, 0), hit_before, hit_after, hit_inside);
5083
5084 // Corner voxel should have some ray intersections but likely no geometry hits
5085 // (depending on ray pattern)
5086 DOCTEST_CHECK(hit_before >= 0);
5087 DOCTEST_CHECK(hit_after >= 0);
5088 DOCTEST_CHECK(hit_inside >= 0);
5089}
5090
5091DOCTEST_TEST_CASE("CollisionDetection Ray Classification - Edge Cases and Boundary Conditions") {
5092 Context context;
5093 CollisionDetection collision(&context);
5094 collision.disableMessages();
5095
5096 // Create geometry at voxel boundaries to test edge cases
5097 uint triangle1 = context.addTriangle(make_vec3(-1, -1, -1), make_vec3(1, -1, -1), make_vec3(0, 1, -1)); // Bottom boundary
5098 uint triangle2 = context.addTriangle(make_vec3(-1, -1, 1), make_vec3(1, -1, 1), make_vec3(0, 1, 1)); // Top boundary
5099
5100 vec3 grid_center(0, 0, 0);
5101 vec3 grid_size(4, 4, 4);
5102 int3 grid_divisions(2, 2, 2);
5103
5104 std::vector<vec3> ray_origins;
5105 std::vector<vec3> ray_directions;
5106
5107 // Edge case 1: Ray starting inside voxel
5108 ray_origins.push_back(make_vec3(0, 0, -0.5)); // Inside voxel (1,1,0)
5109 ray_directions.push_back(make_vec3(0, 0, 1));
5110
5111 // Edge case 2: Ray grazing voxel corner
5112 ray_origins.push_back(make_vec3(-1.99, -1.99, -3)); // Almost missing voxel (0,0,0)
5113 ray_directions.push_back(make_vec3(0, 0, 1));
5114
5115 // Edge case 3: Ray parallel to voxel face (should miss or barely graze)
5116 ray_origins.push_back(make_vec3(-2, 0, 0)); // At voxel boundary
5117 ray_directions.push_back(make_vec3(1, 0, 0)); // Parallel to YZ face
5118
5119 // Edge case 4: Ray exactly hitting voxel corner
5120 ray_origins.push_back(make_vec3(-2, -2, -2));
5121 ray_directions.push_back(normalize(make_vec3(1, 1, 1))); // Towards corner
5122
5123 collision.calculateVoxelRayPathLengths(grid_center, grid_size, grid_divisions, ray_origins, ray_directions);
5124
5125 // Test that edge cases are handled without crashes or invalid data
5126 for (int i = 0; i < grid_divisions.x; i++) {
5127 for (int j = 0; j < grid_divisions.y; j++) {
5128 for (int k = 0; k < grid_divisions.z; k++) {
5129 int hit_before, hit_after, hit_inside;
5130 int3 voxel_idx = make_int3(i, j, k);
5131
5132 // Should not throw exceptions for valid voxel indices
5133 collision.getVoxelRayHitCounts(voxel_idx, hit_before, hit_after, hit_inside);
5134
5135 // Hit counts should be non-negative
5136 DOCTEST_CHECK(hit_before >= 0);
5137 DOCTEST_CHECK(hit_after >= 0);
5138 DOCTEST_CHECK(hit_inside >= 0);
5139
5140 // Path lengths should be valid
5141 std::vector<float> path_lengths = collision.getVoxelRayPathLengths(voxel_idx);
5142 for (float length: path_lengths) {
5143 DOCTEST_CHECK(length > 0.0f); // All path lengths should be positive
5144 DOCTEST_CHECK(length < 100.0f); // Reasonable upper bound
5145 }
5146 }
5147 }
5148 }
5149}
5150
5151DOCTEST_TEST_CASE("CollisionDetection Ray Classification - Error Handling and Invalid Inputs") {
5152 Context context;
5153 CollisionDetection collision(&context);
5154 collision.disableMessages();
5155
5156 // Test error handling for invalid voxel indices
5157 bool caught_negative_exception = false;
5158 bool caught_large_exception = false;
5159 std::string negative_error_msg;
5160 std::string large_error_msg;
5161
5162 {
5163 capture_cerr capture;
5164
5165 // Test invalid negative indices
5166 try {
5167 int hit_before, hit_after, hit_inside;
5168 collision.getVoxelRayHitCounts(make_int3(-1, 0, 0), hit_before, hit_after, hit_inside);
5169 } catch (const std::exception &e) {
5170 caught_negative_exception = true;
5171 negative_error_msg = e.what();
5172 }
5173
5174 // Test invalid too-large indices
5175 try {
5176 std::vector<float> path_lengths = collision.getVoxelRayPathLengths(make_int3(100, 100, 100));
5177 } catch (const std::exception &e) {
5178 caught_large_exception = true;
5179 large_error_msg = e.what();
5180 }
5181 } // capture destroyed here
5182
5183 // Assertions after capture is destroyed
5184 DOCTEST_CHECK(caught_negative_exception);
5185 DOCTEST_CHECK(negative_error_msg.find("Invalid voxel indices") != std::string::npos);
5186 DOCTEST_CHECK(caught_large_exception);
5187 DOCTEST_CHECK(large_error_msg.find("Invalid voxel indices") != std::string::npos);
5188
5189 // Test accessing data before initialization
5190 int hit_before, hit_after, hit_inside;
5191 collision.getVoxelRayHitCounts(make_int3(0, 0, 0), hit_before, hit_after, hit_inside);
5192
5193 // Should return zeros when not initialized
5194 DOCTEST_CHECK(hit_before == 0);
5195 DOCTEST_CHECK(hit_after == 0);
5196 DOCTEST_CHECK(hit_inside == 0);
5197
5198 std::vector<float> path_lengths = collision.getVoxelRayPathLengths(make_int3(0, 0, 0));
5199 DOCTEST_CHECK(path_lengths.empty()); // Should return empty vector when not initialized
5200}
5201
5202DOCTEST_TEST_CASE("CollisionDetection Ray Classification - Beer's Law Integration Test") {
5203 Context context;
5204 CollisionDetection collision(&context);
5205 collision.disableMessages();
5206
5207 // Create realistic vegetation scenario for Beer's law testing
5208 // Multiple patches to create realistic occlusion pattern
5209 std::vector<uint> vegetation_uuids;
5210
5211 // Create a sparse canopy layer
5212 for (int i = 0; i < 3; i++) {
5213 for (int j = 0; j < 3; j++) {
5214 if ((i + j) % 2 == 0) { // Checkerboard pattern for sparse coverage
5215 float x = -2.0f + i * 2.0f;
5216 float y = -2.0f + j * 2.0f;
5217 float z = 1.0f + i * 0.5f; // Varying height
5218
5219 uint patch_uuid = context.addPatch(make_vec3(x, y, z), make_vec2(1.2, 1.2));
5220 vegetation_uuids.push_back(patch_uuid);
5221 }
5222 }
5223 }
5224
5225 vec3 grid_center(0, 0, 0);
5226 vec3 grid_size(8, 8, 6);
5227 int3 grid_divisions(4, 4, 3); // 4x4x3 grid
5228
5229 // LiDAR-style ray pattern from below
5230 std::vector<vec3> ray_origins;
5231 std::vector<vec3> ray_directions;
5232
5233 int rays_per_axis = 20;
5234 for (int i = 0; i < rays_per_axis; i++) {
5235 for (int j = 0; j < rays_per_axis; j++) {
5236 float x = -3.5f + (7.0f * i) / (rays_per_axis - 1);
5237 float y = -3.5f + (7.0f * j) / (rays_per_axis - 1);
5238
5239 ray_origins.push_back(make_vec3(x, y, -3));
5240 ray_directions.push_back(make_vec3(0, 0, 1));
5241 }
5242 }
5243
5244 collision.calculateVoxelRayPathLengths(grid_center, grid_size, grid_divisions, ray_origins, ray_directions);
5245
5246 // Analyze Beer's law statistics for each voxel
5247 bool found_realistic_data = false;
5248
5249 for (int i = 0; i < grid_divisions.x; i++) {
5250 for (int j = 0; j < grid_divisions.y; j++) {
5251 for (int k = 0; k < grid_divisions.z; k++) {
5252 int hit_before, hit_after, hit_inside;
5253 collision.getVoxelRayHitCounts(make_int3(i, j, k), hit_before, hit_after, hit_inside);
5254
5255 std::vector<float> path_lengths = collision.getVoxelRayPathLengths(make_int3(i, j, k));
5256
5257 if (!path_lengths.empty() && (hit_before + hit_after + hit_inside) > 0) {
5258 found_realistic_data = true;
5259
5260 // Beer's law validation: P_trans / P_denom should be between 0 and 1
5261 int P_denom = path_lengths.size(); // Total rays through voxel
5262 int P_trans = P_denom - hit_inside; // Rays not hitting inside voxel
5263
5264 DOCTEST_CHECK(P_trans >= 0);
5265 DOCTEST_CHECK(P_trans <= P_denom);
5266
5267 if (P_denom > 0) {
5268 float transmission_probability = static_cast<float>(P_trans) / static_cast<float>(P_denom);
5269 DOCTEST_CHECK(transmission_probability >= 0.0f);
5270 DOCTEST_CHECK(transmission_probability <= 1.0f);
5271
5272 // If we have hits inside, transmission should be less than 1
5273 if (hit_inside > 0) {
5274 DOCTEST_CHECK(transmission_probability < 1.0f);
5275 }
5276 }
5277
5278 // Average path length calculation (r_bar)
5279 float total_path_length = 0.0f;
5280 for (float length: path_lengths) {
5281 total_path_length += length;
5282 }
5283 float r_bar = total_path_length / P_denom;
5284
5285 DOCTEST_CHECK(r_bar > 0.0f);
5286 DOCTEST_CHECK(r_bar < 10.0f); // Reasonable for this voxel size
5287
5288 // For Beer's law: LAD = -ln(P_trans/P_denom) / (r_bar * G_theta)
5289 // We can't test the full formula without G_theta, but we can verify components
5290 if (P_trans < P_denom && P_trans > 0) {
5291 float ln_arg = static_cast<float>(P_trans) / static_cast<float>(P_denom);
5292 DOCTEST_CHECK(ln_arg > 0.0f); // ln argument must be positive
5293 DOCTEST_CHECK(ln_arg <= 1.0f); // Probability can't exceed 1
5294 }
5295 }
5296 }
5297 }
5298 }
5299
5300 DOCTEST_CHECK(found_realistic_data); // Should have found some meaningful data
5301}
5302
5303DOCTEST_TEST_CASE("CollisionDetection calculateVoxelPathLengths Enhanced Method") {
5304 Context context;
5305 CollisionDetection collision(&context);
5306 collision.disableMessages();
5307
5308 // Test 1: Basic functionality with simple ray-voxel setup
5309 {
5310 vec3 scan_origin = make_vec3(0.0f, 0.0f, 0.0f);
5311
5312 // Create rays pointing in positive X direction
5313 std::vector<vec3> ray_directions;
5314 ray_directions.push_back(normalize(make_vec3(1.0f, 0.0f, 0.0f))); // Straight along X
5315 ray_directions.push_back(normalize(make_vec3(1.0f, 0.1f, 0.0f))); // Slight Y offset
5316 ray_directions.push_back(normalize(make_vec3(1.0f, 0.0f, 0.1f))); // Slight Z offset
5317
5318 // Create voxels that should intersect with rays
5319 std::vector<vec3> voxel_centers;
5320 std::vector<vec3> voxel_sizes;
5321
5322 voxel_centers.push_back(make_vec3(2.0f, 0.0f, 0.0f)); // On ray path
5323 voxel_centers.push_back(make_vec3(5.0f, 0.0f, 0.0f)); // Further along ray path
5324 voxel_centers.push_back(make_vec3(2.0f, 3.0f, 0.0f)); // Off ray path
5325
5326 voxel_sizes.push_back(make_vec3(1.0f, 1.0f, 1.0f));
5327 voxel_sizes.push_back(make_vec3(1.0f, 1.0f, 1.0f));
5328 voxel_sizes.push_back(make_vec3(1.0f, 1.0f, 1.0f));
5329
5330 auto result = collision.calculateVoxelPathLengths(scan_origin, ray_directions, voxel_centers, voxel_sizes);
5331
5332 // Verify result structure
5333 DOCTEST_CHECK(result.size() == 3); // One vector per voxel
5334
5335 // First voxel should be hit by multiple rays
5336 DOCTEST_CHECK(result[0].size() > 0);
5337
5338 // Second voxel should also be hit by multiple rays
5339 DOCTEST_CHECK(result[1].size() > 0);
5340
5341 // Third voxel (off path) should have fewer or no hits
5342 // (This depends on the exact geometry, so we just check it's valid)
5343 DOCTEST_CHECK(result[2].size() >= 0);
5344
5345 // Verify that path_length field is populated correctly
5346 for (size_t voxel_idx = 0; voxel_idx < 2; ++voxel_idx) {
5347 for (const auto &hit: result[voxel_idx]) {
5348 DOCTEST_CHECK(hit.path_length > 0.0f);
5349 DOCTEST_CHECK(hit.path_length <= 2.0f); // Should be at most the voxel diagonal
5350 DOCTEST_CHECK(hit.hit == false); // These are voxel traversals, not primitive hits
5351 DOCTEST_CHECK(hit.distance == -1.0f); // Not applicable for voxel traversals
5352 DOCTEST_CHECK(hit.primitive_UUID == 0); // No primitive
5353 }
5354 }
5355 }
5356
5357 // Test 2: Performance test with larger numbers of rays and voxels
5358 {
5359 vec3 scan_origin = make_vec3(0.0f, 0.0f, 0.0f);
5360
5361 // Create 1000 rays in various directions
5362 std::vector<vec3> ray_directions;
5363 for (int i = 0; i < 1000; ++i) {
5364 float theta = i * 0.01f; // Small angle variations
5365 ray_directions.push_back(normalize(make_vec3(1.0f, sin(theta), cos(theta))));
5366 }
5367
5368 // Create 100 voxels in a grid pattern
5369 std::vector<vec3> voxel_centers;
5370 std::vector<vec3> voxel_sizes;
5371 for (int x = 0; x < 10; ++x) {
5372 for (int y = 0; y < 10; ++y) {
5373 voxel_centers.push_back(make_vec3(x + 1.0f, y - 5.0f, 0.0f));
5374 voxel_sizes.push_back(make_vec3(0.5f, 0.5f, 0.5f));
5375 }
5376 }
5377
5378 // Time the calculation
5379 auto start_time = std::chrono::high_resolution_clock::now();
5380 auto result = collision.calculateVoxelPathLengths(scan_origin, ray_directions, voxel_centers, voxel_sizes);
5381 auto end_time = std::chrono::high_resolution_clock::now();
5382
5383 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
5384
5385 // Verify performance requirement (should be much faster than 2 seconds for this smaller test)
5386 DOCTEST_CHECK(duration.count() < 500); // 500ms for 1K rays x 100 voxels
5387
5388 // Verify result structure
5389 DOCTEST_CHECK(result.size() == 100); // One vector per voxel
5390
5391 // Count total intersections and verify path lengths
5392 size_t total_intersections = 0;
5393 for (size_t i = 0; i < 100; ++i) {
5394 total_intersections += result[i].size();
5395 for (const auto &hit: result[i]) {
5396 DOCTEST_CHECK(hit.path_length > 0.0f);
5397 DOCTEST_CHECK(hit.path_length <= 1.0f); // Max voxel diagonal for 0.5x0.5x0.5 voxel
5398 }
5399 }
5400
5401 // Should have found some intersections
5402 DOCTEST_CHECK(total_intersections > 0);
5403 }
5404
5405 // Test 3: Edge cases and error handling
5406 {
5407 vec3 scan_origin = make_vec3(0.0f, 0.0f, 0.0f);
5408
5409 // Test empty rays
5410 std::vector<vec3> empty_rays;
5411 std::vector<vec3> voxel_centers = {make_vec3(1.0f, 0.0f, 0.0f)};
5412 std::vector<vec3> voxel_sizes = {make_vec3(1.0f, 1.0f, 1.0f)};
5413
5414 auto result = collision.calculateVoxelPathLengths(scan_origin, empty_rays, voxel_centers, voxel_sizes);
5415 DOCTEST_CHECK(result.empty());
5416
5417 // Test empty voxels
5418 std::vector<vec3> ray_directions = {normalize(make_vec3(1.0f, 0.0f, 0.0f))};
5419 std::vector<vec3> empty_voxels;
5420 std::vector<vec3> empty_sizes;
5421
5422 result = collision.calculateVoxelPathLengths(scan_origin, ray_directions, empty_voxels, empty_sizes);
5423 DOCTEST_CHECK(result.empty());
5424
5425 // Test mismatched voxel center/size arrays
5426 std::vector<vec3> mismatched_sizes = {make_vec3(1.0f, 1.0f, 1.0f), make_vec3(2.0f, 2.0f, 2.0f)};
5427
5428 bool threw_exception = false;
5429 {
5430 capture_cerr capture;
5431 try {
5432 collision.calculateVoxelPathLengths(scan_origin, ray_directions, voxel_centers, mismatched_sizes);
5433 } catch (const std::exception &) {
5434 threw_exception = true;
5435 }
5436 } // capture destroyed here
5437
5438 DOCTEST_CHECK(threw_exception);
5439 }
5440
5441 // Test 4: Geometric accuracy - ray exactly through voxel center
5442 {
5443 vec3 scan_origin = make_vec3(0.0f, 0.0f, 0.0f);
5444 vec3 ray_direction = normalize(make_vec3(1.0f, 0.0f, 0.0f));
5445 vec3 voxel_center = make_vec3(5.0f, 0.0f, 0.0f);
5446 vec3 voxel_size = make_vec3(2.0f, 2.0f, 2.0f);
5447
5448 auto result = collision.calculateVoxelPathLengths(scan_origin, {ray_direction}, {voxel_center}, {voxel_size});
5449
5450 DOCTEST_CHECK(result.size() == 1); // One voxel
5451 DOCTEST_CHECK(result[0].size() == 1); // One ray hit
5452
5453 // Path length through center of cube should be exactly the voxel width (2.0)
5454 float path_length = result[0][0].path_length;
5455 DOCTEST_CHECK(std::abs(path_length - 2.0f) < 1e-4f);
5456 }
5457
5458 // Test 5: Multiple rays through same voxel at different angles
5459 {
5460 vec3 scan_origin = make_vec3(-1.0f, 0.0f, 0.0f);
5461 std::vector<vec3> ray_directions;
5462
5463 // Rays at different angles through the same voxel
5464 ray_directions.push_back(normalize(make_vec3(1.0f, 0.0f, 0.0f))); // Straight through
5465 ray_directions.push_back(normalize(make_vec3(1.0f, 0.2f, 0.0f))); // Diagonal
5466 ray_directions.push_back(normalize(make_vec3(1.0f, 0.0f, 0.2f))); // Different diagonal
5467
5468 vec3 voxel_center = make_vec3(1.0f, 0.0f, 0.0f);
5469 vec3 voxel_size = make_vec3(1.0f, 1.0f, 1.0f);
5470
5471 auto result = collision.calculateVoxelPathLengths(scan_origin, ray_directions, {voxel_center}, {voxel_size});
5472
5473 // All rays should intersect
5474 DOCTEST_CHECK(result.size() == 1); // One voxel
5475 DOCTEST_CHECK(result[0].size() == 3); // Three ray hits
5476
5477 // Path lengths should be different for different angles
5478 std::vector<float> path_lengths;
5479 for (const auto &hit: result[0]) {
5480 path_lengths.push_back(hit.path_length);
5481 }
5482
5483 // Verify all path lengths are reasonable
5484 for (float path: path_lengths) {
5485 DOCTEST_CHECK(path > 0.5f); // At least half the voxel width
5486 DOCTEST_CHECK(path < 2.0f); // At most the full diagonal
5487 }
5488
5489 // The straight ray should generally be the shortest, but due to OpenMP ordering
5490 // we can't guarantee which hit comes first, so just verify they're not all the same
5491 DOCTEST_CHECK(!(path_lengths[0] == path_lengths[1] && path_lengths[1] == path_lengths[2]));
5492 }
5493
5494 // Test 6: Usage pattern test - verify the exact API the LiDAR plugin will use
5495 {
5496 vec3 scan_origin = make_vec3(0.0f, 0.0f, 0.0f);
5497 std::vector<vec3> ray_directions = {normalize(make_vec3(1.0f, 0.0f, 0.0f)), normalize(make_vec3(1.0f, 0.1f, 0.0f)), normalize(make_vec3(1.0f, 0.0f, 0.1f))};
5498
5499 std::vector<vec3> voxel_centers = {make_vec3(2.0f, 0.0f, 0.0f), make_vec3(5.0f, 0.0f, 0.0f)};
5500
5501 std::vector<vec3> voxel_sizes = {make_vec3(1.0f, 1.0f, 1.0f), make_vec3(1.0f, 1.0f, 1.0f)};
5502
5503 // This is exactly how the LiDAR plugin will use it
5504 auto result = collision.calculateVoxelPathLengths(scan_origin, ray_directions, voxel_centers, voxel_sizes);
5505
5506 // LiDAR plugin usage pattern:
5507 for (size_t c = 0; c < voxel_centers.size(); ++c) {
5508 std::vector<float> dr_agg;
5509 uint hit_after_agg = 0;
5510
5511 // Extract path lengths and ray count for this voxel
5512 for (const auto &hit: result[c]) {
5513 dr_agg.push_back(hit.path_length); // Direct assignment as specified
5514 hit_after_agg++; // Count rays
5515 }
5516
5517 // Verify the data is usable
5518 DOCTEST_CHECK(hit_after_agg == result[c].size());
5519 for (float path_length: dr_agg) {
5520 DOCTEST_CHECK(path_length > 0.0f);
5521 DOCTEST_CHECK(path_length <= 2.0f);
5522 }
5523 }
5524 }
5525}
5526
5527DOCTEST_TEST_CASE("CollisionDetection VoxelIntersection Integration - Patch with no slices") {
5528 Context context;
5529
5530 // Create patch using center and size instead of vertices
5531 vec3 patch_center = make_vec3(0.5, 0, 0.5);
5532 vec2 patch_size = make_vec2(1, 1);
5533
5534 uint prim_UUID = context.addPatch(patch_center, patch_size);
5535
5536 vec3 grid_center = make_vec3(0, 1, 0);
5537 vec3 grid_size = make_vec3(10, 10, 10);
5538
5539 CollisionDetection collisiondetection(&context);
5540 collisiondetection.disableMessages();
5541
5542 std::vector<uint> voxel_UUIDs = collisiondetection.slicePrimitivesUsingGrid(std::vector<uint>{prim_UUID}, grid_center, grid_size, make_int3(2, 2, 2));
5543
5544 DOCTEST_CHECK(voxel_UUIDs.size() >= 1);
5545}
5546
5547DOCTEST_TEST_CASE("CollisionDetection VoxelIntersection Integration - Patch with one slice") {
5548 Context context;
5549
5550 // Create patch using center and size instead of vertices
5551 vec3 patch_center = make_vec3(0.5, 0, 0);
5552 vec2 patch_size = make_vec2(2, 2);
5553
5554 uint prim_UUID = context.addPatch(patch_center, patch_size);
5555
5556 vec3 grid_center = make_vec3(0, 0, 0);
5557 vec3 grid_size = make_vec3(1, 10, 10);
5558
5559 CollisionDetection collisiondetection(&context);
5560 collisiondetection.disableMessages();
5561
5562 std::vector<uint> voxel_UUIDs = collisiondetection.slicePrimitivesUsingGrid(std::vector<uint>{prim_UUID}, grid_center, grid_size, make_int3(2, 1, 1));
5563
5564 DOCTEST_CHECK(voxel_UUIDs.size() >= 1);
5565}
5566
5567DOCTEST_TEST_CASE("CollisionDetection VoxelIntersection Integration - Patch with 2 slices") {
5568 Context context;
5569
5570 // Create patch using center and size instead of vertices
5571 vec3 patch_center = make_vec3(1, 0, 0);
5572 vec2 patch_size = make_vec2(3, 1);
5573
5574 uint prim_UUID = context.addPatch(patch_center, patch_size);
5575
5576 vec3 grid_center = make_vec3(0, 0, 0);
5577 vec3 grid_size = make_vec3(2, 10, 1);
5578
5579 CollisionDetection collisiondetection(&context);
5580 collisiondetection.disableMessages();
5581
5582 std::vector<uint> voxel_UUIDs = collisiondetection.slicePrimitivesUsingGrid(std::vector<uint>{prim_UUID}, grid_center, grid_size, make_int3(3, 1, 1));
5583
5584 DOCTEST_CHECK(voxel_UUIDs.size() >= 1);
5585}
5586
5587DOCTEST_TEST_CASE("CollisionDetection VoxelIntersection Integration - Patch with 3 slices and no vertices inside voxel") {
5588 Context context;
5589
5590 // Create patch using center and size instead of vertices
5591 vec3 patch_center = make_vec3(2, 0, 0);
5592 vec2 patch_size = make_vec2(3, 2);
5593
5594 uint prim_UUID = context.addPatch(patch_center, patch_size);
5595
5596 vec3 grid_center = make_vec3(0, 0, 0);
5597 vec3 grid_size = make_vec3(4, 10, 10);
5598
5599 CollisionDetection collisiondetection(&context);
5600 collisiondetection.disableMessages();
5601
5602 std::vector<uint> voxel_UUIDs = collisiondetection.slicePrimitivesUsingGrid(std::vector<uint>{prim_UUID}, grid_center, grid_size, make_int3(4, 1, 1));
5603
5604 DOCTEST_CHECK(voxel_UUIDs.size() >= 1);
5605}
5606
5607DOCTEST_TEST_CASE("CollisionDetection VoxelIntersection Integration - Areas after slicing (non-textured)") {
5608 Context context;
5609
5610 // Create patch using center and size instead of vertices
5611 vec3 patch_center = make_vec3(0, 0, 0);
5612 vec2 patch_size = make_vec2(2, 2);
5613
5614 uint prim_UUID = context.addPatch(patch_center, patch_size);
5615
5616 float area_patch = context.getPrimitiveArea(prim_UUID);
5617
5618 vec3 grid_center = make_vec3(0, 0, 0);
5619 vec3 grid_size = make_vec3(2, 10, 2);
5620
5621 CollisionDetection collisiondetection(&context);
5622 collisiondetection.disableMessages();
5623
5624 std::vector<uint> voxel_UUIDs = collisiondetection.slicePrimitivesUsingGrid(std::vector<uint>{prim_UUID}, grid_center, grid_size, make_int3(2, 1, 2));
5625
5626 float area_tot = 0;
5627 for (uint UUID: voxel_UUIDs) {
5628 area_tot += context.getPrimitiveArea(UUID);
5629 }
5630
5631 DOCTEST_CHECK(fabs(area_tot - area_patch) / area_patch < 0.05f);
5632}
5633
5634DOCTEST_TEST_CASE("CollisionDetection VoxelIntersection Integration - Textured areas after slicing") {
5635 Context context;
5636
5637 // Create textured patch using center, size, and texture file
5638 vec3 patch_center = make_vec3(0, 0, 0);
5639 vec2 patch_size = make_vec2(2, 2);
5640
5641 uint prim_UUID = context.addPatch(patch_center, patch_size, make_SphericalCoord(0, 0), "lib/images/disk_texture.png");
5642
5643 float area_patch = context.getPrimitiveArea(prim_UUID);
5644
5645 vec3 grid_center = make_vec3(0, 0, 0);
5646 vec3 grid_size = make_vec3(2, 10, 2);
5647
5648 CollisionDetection collisiondetection(&context);
5649 collisiondetection.disableMessages();
5650
5651 std::vector<uint> voxel_UUIDs = collisiondetection.slicePrimitivesUsingGrid(std::vector<uint>{prim_UUID}, grid_center, grid_size, make_int3(2, 1, 2));
5652
5653 float area_tot = 0;
5654 for (uint UUID: voxel_UUIDs) {
5655 area_tot += context.getPrimitiveArea(UUID);
5656 }
5657
5658 DOCTEST_CHECK(fabs(area_tot - area_patch) / area_patch < 0.05f);
5659}
5660
5661DOCTEST_TEST_CASE("CollisionDetection VoxelIntersection Integration - Cropping non-textured primitives") {
5662 Context context;
5663
5664 // Create two triangles to form a quad (replacement for addPolygon)
5665 uint tri1_UUID = context.addTriangle(make_vec3(-1, 0, 1), make_vec3(1, 0, 1), make_vec3(1, 0, -1));
5666 uint tri2_UUID = context.addTriangle(make_vec3(1, 0, -1), make_vec3(-1, 0, -1), make_vec3(-1, 0, 1));
5667 uint prim_UUID = tri1_UUID; // Use first triangle for the test
5668
5669 vec3 grid_center = make_vec3(0, 0, 0);
5670 vec3 grid_size = make_vec3(1, 10, 1);
5671
5672 CollisionDetection collisiondetection(&context);
5673 collisiondetection.disableMessages();
5674
5675 // Test slicing the primitive within the voxel bounds
5676 std::vector<uint> voxel_UUIDs = collisiondetection.slicePrimitivesUsingGrid(std::vector<uint>{prim_UUID}, grid_center, grid_size, make_int3(1, 1, 1));
5677
5678 DOCTEST_CHECK(voxel_UUIDs.size() >= 1);
5679}
5680
5681DOCTEST_TEST_CASE("CollisionDetection VoxelIntersection Integration - Cropping textured primitives") {
5682 Context context;
5683
5684 // Create textured patch using center, size, and texture file
5685 vec3 patch_center = make_vec3(0, 0, 0);
5686 vec2 patch_size = make_vec2(2, 2);
5687
5688 uint prim_UUID = context.addPatch(patch_center, patch_size, make_SphericalCoord(0, 0), "lib/images/disk_texture.png");
5689
5690 vec3 grid_center = make_vec3(0, 0, 0);
5691 vec3 grid_size = make_vec3(1, 10, 1);
5692
5693 CollisionDetection collisiondetection(&context);
5694 collisiondetection.disableMessages();
5695
5696 // Test slicing the textured primitive within the voxel bounds
5697 std::vector<uint> voxel_UUIDs = collisiondetection.slicePrimitivesUsingGrid(std::vector<uint>{prim_UUID}, grid_center, grid_size, make_int3(1, 1, 1));
5698
5699 DOCTEST_CHECK(voxel_UUIDs.size() >= 1);
5700}
5701
5702DOCTEST_TEST_CASE("CollisionDetection VoxelIntersection Integration - Basic functionality test") {
5703 Context context;
5704
5705 // Create patch using center and size instead of vertices
5706 vec3 patch_center = make_vec3(0, 0, 0);
5707 vec2 patch_size = make_vec2(2, 2);
5708
5709 uint prim_UUID = context.addPatch(patch_center, patch_size);
5710
5711 CollisionDetection collisiondetection(&context);
5712 collisiondetection.disableMessages();
5713
5714 // Test slicing the primitive within a grid
5715 vec3 grid_center = make_vec3(0, 0, 0);
5716 vec3 grid_size = make_vec3(3, 3, 3);
5717 std::vector<uint> sliced_UUIDs = collisiondetection.slicePrimitivesUsingGrid(std::vector<uint>{prim_UUID}, grid_center, grid_size, make_int3(2, 1, 2));
5718
5719 // Check that slicing worked and produced some output
5720 DOCTEST_CHECK(sliced_UUIDs.size() >= 1);
5721}
5722
5723// ============================================================================
5724// VOXEL-PRIMITIVE INTERSECTION TESTS (calculatePrimitiveVoxelIntersection)
5725// ============================================================================
5726
5727DOCTEST_TEST_CASE("CollisionDetection VoxelIntersection Integration - calculatePrimitiveVoxelIntersection Basic") {
5728 Context context;
5729 CollisionDetection collisiondetection(&context);
5730 collisiondetection.disableMessages();
5731
5732 // Create a voxel at origin
5733 vec3 voxel_center = make_vec3(0, 0, 0);
5734 vec3 voxel_size = make_vec3(2, 2, 2);
5735 uint voxel_uuid = context.addVoxel(voxel_center, voxel_size);
5736
5737 // Create a patch inside the voxel
5738 vec3 patch_center = make_vec3(0.5, 0.5, 0.5);
5739 vec2 patch_size = make_vec2(0.5, 0.5);
5740 uint patch_uuid = context.addPatch(patch_center, patch_size);
5741
5742 // Create a patch outside the voxel
5743 vec3 outside_center = make_vec3(5, 5, 5);
5744 uint outside_uuid = context.addPatch(outside_center, patch_size);
5745
5746 // Calculate intersection
5747 collisiondetection.calculatePrimitiveVoxelIntersection();
5748
5749 // Voxel should have "inside_UUIDs" data containing the inside patch
5750 DOCTEST_CHECK(context.doesPrimitiveDataExist(voxel_uuid, "inside_UUIDs"));
5751
5752 std::vector<uint> inside_prims;
5753 context.getPrimitiveData(voxel_uuid, "inside_UUIDs", inside_prims);
5754
5755 DOCTEST_CHECK(inside_prims.size() == 1);
5756 DOCTEST_CHECK(inside_prims[0] == patch_uuid);
5757}
5758
5759DOCTEST_TEST_CASE("CollisionDetection VoxelIntersection Integration - calculatePrimitiveVoxelIntersection Axis-Aligned") {
5760 Context context;
5761 CollisionDetection collisiondetection(&context);
5762 collisiondetection.disableMessages();
5763
5764 // Create voxel at origin
5765 uint voxel_uuid = context.addVoxel(make_vec3(0, 0, 0), make_vec3(2, 2, 2));
5766
5767 // Create patch with centroid on X-axis (tests divide-by-zero fix for y,z components)
5768 uint patch_x = context.addPatch(make_vec3(0.5, 0, 0), make_vec2(0.2, 0.2));
5769
5770 // Create patch with centroid on Y-axis (tests divide-by-zero fix for x,z components)
5771 uint patch_y = context.addPatch(make_vec3(0, 0.5, 0), make_vec2(0.2, 0.2));
5772
5773 // Create patch with centroid on Z-axis (tests divide-by-zero fix for x,y components)
5774 uint patch_z = context.addPatch(make_vec3(0, 0, 0.5), make_vec2(0.2, 0.2));
5775
5776 // This should NOT crash with divide-by-zero
5777 collisiondetection.calculatePrimitiveVoxelIntersection();
5778
5779 // All three patches should be detected inside the voxel
5780 DOCTEST_CHECK(context.doesPrimitiveDataExist(voxel_uuid, "inside_UUIDs"));
5781
5782 std::vector<uint> inside_prims;
5783 context.getPrimitiveData(voxel_uuid, "inside_UUIDs", inside_prims);
5784
5785 DOCTEST_CHECK(inside_prims.size() == 3);
5786}
5787
5788DOCTEST_TEST_CASE("CollisionDetection VoxelIntersection Integration - calculatePrimitiveVoxelIntersection Multiple Voxels") {
5789 Context context;
5790 CollisionDetection collisiondetection(&context);
5791 collisiondetection.disableMessages();
5792
5793 // Create 3 voxels in a row
5794 uint voxel1 = context.addVoxel(make_vec3(-2, 0, 0), make_vec3(1, 1, 1));
5795 uint voxel2 = context.addVoxel(make_vec3(0, 0, 0), make_vec3(1, 1, 1));
5796 uint voxel3 = context.addVoxel(make_vec3(2, 0, 0), make_vec3(1, 1, 1));
5797
5798 // Create primitives in different voxels
5799 uint patch1 = context.addPatch(make_vec3(-2, 0, 0), make_vec2(0.2, 0.2));
5800 uint patch2 = context.addPatch(make_vec3(0, 0, 0), make_vec2(0.2, 0.2));
5801 uint patch3a = context.addPatch(make_vec3(2, 0, 0), make_vec2(0.2, 0.2));
5802 uint patch3b = context.addPatch(make_vec3(2, 0.2, 0.2), make_vec2(0.1, 0.1));
5803
5804 collisiondetection.calculatePrimitiveVoxelIntersection();
5805
5806 // Check voxel 1 contains patch1
5807 std::vector<uint> inside1;
5808 context.getPrimitiveData(voxel1, "inside_UUIDs", inside1);
5809 DOCTEST_CHECK(inside1.size() == 1);
5810 DOCTEST_CHECK(inside1[0] == patch1);
5811
5812 // Check voxel 2 contains patch2
5813 std::vector<uint> inside2;
5814 context.getPrimitiveData(voxel2, "inside_UUIDs", inside2);
5815 DOCTEST_CHECK(inside2.size() == 1);
5816 DOCTEST_CHECK(inside2[0] == patch2);
5817
5818 // Check voxel 3 contains both patch3a and patch3b
5819 std::vector<uint> inside3;
5820 context.getPrimitiveData(voxel3, "inside_UUIDs", inside3);
5821 DOCTEST_CHECK(inside3.size() == 2);
5822}
5823
5824DOCTEST_TEST_CASE("CollisionDetection VoxelIntersection Integration - calculatePrimitiveVoxelIntersection Empty Inputs") {
5825 Context context;
5826 CollisionDetection collisiondetection(&context);
5827 collisiondetection.disableMessages();
5828
5829 // Test with no voxels (should handle gracefully)
5830 uint patch_uuid = context.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1));
5831 collisiondetection.calculatePrimitiveVoxelIntersection();
5832 // Should complete without error
5833
5834 // Test with no primitives (should handle gracefully)
5835 Context context2;
5836 CollisionDetection collisiondetection2(&context2);
5837 collisiondetection2.disableMessages();
5838 uint voxel_uuid = context2.addVoxel(make_vec3(0, 0, 0), make_vec3(1, 1, 1));
5839 collisiondetection2.calculatePrimitiveVoxelIntersection();
5840 // Should complete without error
5841 DOCTEST_CHECK(!context2.doesPrimitiveDataExist(voxel_uuid, "inside_UUIDs"));
5842}
5843
5844DOCTEST_TEST_CASE("CollisionDetection VoxelIntersection Integration - calculatePrimitiveVoxelIntersection Specific UUIDs") {
5845 Context context;
5846 CollisionDetection collisiondetection(&context);
5847 collisiondetection.disableMessages();
5848
5849 // Create voxel and primitives
5850 uint voxel_uuid = context.addVoxel(make_vec3(0, 0, 0), make_vec3(2, 2, 2));
5851 uint patch1 = context.addPatch(make_vec3(0.5, 0.5, 0.5), make_vec2(0.2, 0.2));
5852 uint patch2 = context.addPatch(make_vec3(-0.5, -0.5, -0.5), make_vec2(0.2, 0.2));
5853 uint patch3 = context.addPatch(make_vec3(5, 5, 5), make_vec2(0.2, 0.2)); // Outside
5854
5855 // Test with specific UUIDs only
5856 std::vector<uint> test_uuids = {voxel_uuid, patch1, patch3};
5857 collisiondetection.calculatePrimitiveVoxelIntersection(test_uuids);
5858
5859 // Should only find patch1, not patch2 (wasn't in test UUIDs)
5860 std::vector<uint> inside_prims;
5861 context.getPrimitiveData(voxel_uuid, "inside_UUIDs", inside_prims);
5862
5863 DOCTEST_CHECK(inside_prims.size() == 1);
5864 DOCTEST_CHECK(inside_prims[0] == patch1);
5865}
5866
5867DOCTEST_TEST_CASE("CollisionDetection GPU/CPU Ray Casting Parity") {
5868 // Test that GPU and CPU ray-casting produce similar results for large ray batches
5869 // This test exposes the bug where GPU returns far fewer hits than CPU when
5870 // BVH is transferred to GPU memory
5871
5872 Context context;
5873 CollisionDetection cd(&context);
5874 cd.disableMessages();
5875
5876 // Create a simple 2m x 2m wall of triangles at x=0
5877 // This ensures rays shooting in +x direction will hit
5878 int grid_size = 50;
5879 float wall_size = 2.0f;
5880 float spacing = wall_size / grid_size;
5881
5882 for (int iy = 0; iy < grid_size; iy++) {
5883 for (int iz = 0; iz < grid_size; iz++) {
5884 float y = -wall_size/2.0f + iy * spacing;
5885 float z = -wall_size/2.0f + iz * spacing;
5886
5887 // Create two triangles per grid cell to form a quad
5888 vec3 v0(0, y, z);
5889 vec3 v1(0, y + spacing, z);
5890 vec3 v2(0, y + spacing, z + spacing);
5891 vec3 v3(0, y, z + spacing);
5892
5893 context.addTriangle(v0, v1, v2);
5894 context.addTriangle(v0, v2, v3);
5895 }
5896 }
5897
5898 uint triangle_count = context.getPrimitiveCount();
5899 std::cout << "Test geometry: " << triangle_count << " triangles" << std::endl;
5900
5901 // Build BVH (this will call transferBVHToGPU when HELIOS_CUDA_AVAILABLE is defined)
5902 cd.buildBVH();
5903
5904 // Create rays shooting at the wall from x=-2
5905 std::vector<CollisionDetection::RayQuery> ray_queries;
5906 int rays_per_dim = 1050; // 1,102,500 total rays (exceeds 1M GPU threshold to test GPU path)
5907
5908 for (int iy = 0; iy < rays_per_dim; iy++) {
5909 for (int iz = 0; iz < rays_per_dim; iz++) {
5910 float y = -wall_size/2.0f + (iy + 0.5f) * wall_size / rays_per_dim;
5911 float z = -wall_size/2.0f + (iz + 0.5f) * wall_size / rays_per_dim;
5912 vec3 origin(-2.0f, y, z);
5913 vec3 direction(1.0f, 0.0f, 0.0f);
5914 ray_queries.emplace_back(origin, direction, 10.0f);
5915 }
5916 }
5917
5918 std::cout << "Casting " << ray_queries.size() << " rays through wall (testing GPU path)..." << std::endl;
5919
5920 // Test with GPU disabled first (to get baseline)
5921 cd.disableGPUAcceleration();
5922 std::vector<CollisionDetection::HitResult> cpu_results = cd.castRays(ray_queries);
5923
5924 size_t cpu_hits = 0;
5925 for (const auto& result : cpu_results) {
5926 if (result.hit) cpu_hits++;
5927 }
5928
5929 // Test with GPU enabled
5930 cd.enableGPUAcceleration();
5931 std::vector<CollisionDetection::HitResult> gpu_results = cd.castRays(ray_queries);
5932
5933 size_t gpu_hits = 0;
5934 for (const auto& result : gpu_results) {
5935 if (result.hit) gpu_hits++;
5936 }
5937
5938 std::cout << "CPU hits: " << cpu_hits << " (" << (100.0*cpu_hits/ray_queries.size()) << "%)" << std::endl;
5939 std::cout << "GPU hits: " << gpu_hits << " (" << (100.0*gpu_hits/ray_queries.size()) << "%)" << std::endl;
5940
5941 // CPU should find most rays hit the wall (>90%)
5942 float cpu_hit_rate = static_cast<float>(cpu_hits) / ray_queries.size();
5943 DOCTEST_CHECK(cpu_hit_rate > 0.90f);
5944
5945 // GPU and CPU should produce similar hit counts (within 5%)
5946 // This will FAIL if GPU ray-casting has bugs
5947 if (cpu_hits > 0) {
5948 float hit_ratio = static_cast<float>(gpu_hits) / static_cast<float>(cpu_hits);
5949 DOCTEST_CHECK(hit_ratio > 0.95f);
5950 DOCTEST_CHECK(hit_ratio < 1.05f);
5951 }
5952}
5953