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