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