|
| 1 | +// |
| 2 | +// Created by Vladislav on 07.05.2026. |
| 3 | +// |
| 4 | +#include "omath/3d_primitives/obb.hpp" |
| 5 | +#include "omath/collision/line_tracer.hpp" |
| 6 | +#include <cmath> |
| 7 | +#include <gtest/gtest.h> |
| 8 | +#include <numbers> |
| 9 | + |
| 10 | +using Vec3 = omath::Vector3<float>; |
| 11 | +using Ray = omath::collision::Ray<>; |
| 12 | +using LineTracer = omath::collision::LineTracer<>; |
| 13 | +using OBB = omath::primitives::Obb<float>; |
| 14 | + |
| 15 | +namespace |
| 16 | +{ |
| 17 | + Ray make_ray(const Vec3 start, const Vec3 end, const bool infinite = false) |
| 18 | + { |
| 19 | + Ray r; |
| 20 | + r.start = start; |
| 21 | + r.end = end; |
| 22 | + r.infinite_length = infinite; |
| 23 | + return r; |
| 24 | + } |
| 25 | + |
| 26 | + constexpr OBB axis_aligned_obb(const Vec3& center, const Vec3& half_extents) noexcept |
| 27 | + { |
| 28 | + return OBB{center, {1.f, 0.f, 0.f}, {0.f, 1.f, 0.f}, {0.f, 0.f, 1.f}, half_extents}; |
| 29 | + } |
| 30 | + |
| 31 | + OBB rotated_around_z(const Vec3& center, const Vec3& half_extents, const float radians) noexcept |
| 32 | + { |
| 33 | + const auto c = std::cos(radians); |
| 34 | + const auto s = std::sin(radians); |
| 35 | + return OBB{center, {c, s, 0.f}, {-s, c, 0.f}, {0.f, 0.f, 1.f}, half_extents}; |
| 36 | + } |
| 37 | +} // namespace |
| 38 | + |
| 39 | +// --- axis-aligned OBB behaves like AABB --- |
| 40 | + |
| 41 | +TEST(LineTracerOBBTests, AxisAlignedHitAlongZ) |
| 42 | +{ |
| 43 | + const auto box = axis_aligned_obb({0.f, 0.f, 0.f}, {1.f, 1.f, 1.f}); |
| 44 | + const auto ray = make_ray({0.f, 0.f, -5.f}, {0.f, 0.f, 5.f}); |
| 45 | + |
| 46 | + const auto hit = LineTracer::get_ray_hit_point(ray, box); |
| 47 | + EXPECT_NE(hit, ray.end); |
| 48 | + EXPECT_NEAR(hit.x, 0.f, 1e-4f); |
| 49 | + EXPECT_NEAR(hit.y, 0.f, 1e-4f); |
| 50 | + EXPECT_NEAR(hit.z, -1.f, 1e-4f); |
| 51 | +} |
| 52 | + |
| 53 | +TEST(LineTracerOBBTests, AxisAlignedHitAlongX) |
| 54 | +{ |
| 55 | + const auto box = axis_aligned_obb({0.f, 0.f, 0.f}, {1.f, 1.f, 1.f}); |
| 56 | + const auto ray = make_ray({-5.f, 0.f, 0.f}, {5.f, 0.f, 0.f}); |
| 57 | + |
| 58 | + const auto hit = LineTracer::get_ray_hit_point(ray, box); |
| 59 | + EXPECT_NE(hit, ray.end); |
| 60 | + EXPECT_NEAR(hit.x, -1.f, 1e-4f); |
| 61 | +} |
| 62 | + |
| 63 | +TEST(LineTracerOBBTests, MissReturnsEnd) |
| 64 | +{ |
| 65 | + const auto box = axis_aligned_obb({0.f, 0.f, 0.f}, {1.f, 1.f, 1.f}); |
| 66 | + const auto ray = make_ray({0.f, 5.f, -5.f}, {0.f, 5.f, 5.f}); |
| 67 | + |
| 68 | + const auto hit = LineTracer::get_ray_hit_point(ray, box); |
| 69 | + EXPECT_EQ(hit, ray.end); |
| 70 | +} |
| 71 | + |
| 72 | +TEST(LineTracerOBBTests, RayTooShortReturnsEnd) |
| 73 | +{ |
| 74 | + const auto box = axis_aligned_obb({4.f, 0.f, 0.f}, {1.f, 1.f, 1.f}); |
| 75 | + const auto ray = make_ray({0.f, 0.f, 0.f}, {2.f, 0.f, 0.f}); |
| 76 | + |
| 77 | + const auto hit = LineTracer::get_ray_hit_point(ray, box); |
| 78 | + EXPECT_EQ(hit, ray.end); |
| 79 | +} |
| 80 | + |
| 81 | +TEST(LineTracerOBBTests, InfiniteRayHits) |
| 82 | +{ |
| 83 | + const auto box = axis_aligned_obb({4.f, 0.f, 0.f}, {1.f, 1.f, 1.f}); |
| 84 | + const auto ray = make_ray({0.f, 0.f, 0.f}, {2.f, 0.f, 0.f}, true); |
| 85 | + |
| 86 | + const auto hit = LineTracer::get_ray_hit_point(ray, box); |
| 87 | + EXPECT_NE(hit, ray.end); |
| 88 | + EXPECT_NEAR(hit.x, 3.f, 1e-4f); |
| 89 | +} |
| 90 | + |
| 91 | +TEST(LineTracerOBBTests, RayStartsInsideBox) |
| 92 | +{ |
| 93 | + const auto box = axis_aligned_obb({0.f, 0.f, 0.f}, {1.f, 1.f, 1.f}); |
| 94 | + const auto ray = make_ray({0.f, 0.f, 0.f}, {5.f, 0.f, 0.f}); |
| 95 | + |
| 96 | + const auto hit = LineTracer::get_ray_hit_point(ray, box); |
| 97 | + EXPECT_NE(hit, ray.end); |
| 98 | + EXPECT_NEAR(hit.x, 0.f, 1e-4f); |
| 99 | + EXPECT_NEAR(hit.y, 0.f, 1e-4f); |
| 100 | + EXPECT_NEAR(hit.z, 0.f, 1e-4f); |
| 101 | +} |
| 102 | + |
| 103 | +TEST(LineTracerOBBTests, RayBehindBoxReturnsEnd) |
| 104 | +{ |
| 105 | + const auto box = axis_aligned_obb({4.f, 0.f, 0.f}, {1.f, 1.f, 1.f}); |
| 106 | + const auto ray = make_ray({10.f, 0.f, 0.f}, {20.f, 0.f, 0.f}); |
| 107 | + |
| 108 | + const auto hit = LineTracer::get_ray_hit_point(ray, box); |
| 109 | + EXPECT_EQ(hit, ray.end); |
| 110 | +} |
| 111 | + |
| 112 | +// --- rotated OBB --- |
| 113 | + |
| 114 | +TEST(LineTracerOBBTests, RotatedBoxHitOnRotatedFace) |
| 115 | +{ |
| 116 | + // Box centred at the origin, rotated 45° around Z. After rotation, the box's "near" face |
| 117 | + // (originally x=-1) is now perpendicular to the (1, 1, 0)/√2 direction. A ray approaching |
| 118 | + // from +X (along world -X) first hits the box at the rotated face — at x = √2 ≈ 1.414. |
| 119 | + const auto box = rotated_around_z({0.f, 0.f, 0.f}, {1.f, 1.f, 1.f}, std::numbers::pi_v<float> / 4.f); |
| 120 | + const auto ray = make_ray({5.f, 0.f, 0.f}, {-5.f, 0.f, 0.f}); |
| 121 | + |
| 122 | + const auto hit = LineTracer::get_ray_hit_point(ray, box); |
| 123 | + EXPECT_NE(hit, ray.end); |
| 124 | + EXPECT_NEAR(hit.x, std::numbers::sqrt2_v<float>, 1e-4f); |
| 125 | + EXPECT_NEAR(hit.y, 0.f, 1e-4f); |
| 126 | + EXPECT_NEAR(hit.z, 0.f, 1e-4f); |
| 127 | +} |
| 128 | + |
| 129 | +TEST(LineTracerOBBTests, RotatedBoxMissesWhereAabbWouldHit) |
| 130 | +{ |
| 131 | + // A unit cube rotated 45° around Z has an XY footprint that is a diamond reaching |
| 132 | + // (±√2, 0) and (0, ±√2). The AABB envelope spans x,y ∈ [-√2, √2], but at y just below √2 |
| 133 | + // the diamond is essentially a point. A ray at y = 1.43 is outside the diamond entirely |
| 134 | + // (|x| + |y| ≤ √2 ⇒ |x| ≤ √2 - 1.43 < 0), yet it would still pass through the AABB |
| 135 | + // envelope of the rotated box. |
| 136 | + const auto box = rotated_around_z({0.f, 0.f, 0.f}, {1.f, 1.f, 1.f}, std::numbers::pi_v<float> / 4.f); |
| 137 | + const auto ray = make_ray({-5.f, 1.43f, 0.f}, {5.f, 1.43f, 0.f}); |
| 138 | + |
| 139 | + const auto hit = LineTracer::get_ray_hit_point(ray, box); |
| 140 | + EXPECT_EQ(hit, ray.end); |
| 141 | +} |
| 142 | + |
| 143 | +TEST(LineTracerOBBTests, RotatedThinBoxHitFromTheSide) |
| 144 | +{ |
| 145 | + // Long, thin axis-aligned slab along X, rotated 90° around Z so it now points along Y. |
| 146 | + // A ray from +X straight back along -X must miss (the slab is thin in X), but a ray along |
| 147 | + // -Y from +Y must hit. |
| 148 | + const auto box = rotated_around_z({0.f, 0.f, 0.f}, {5.f, 0.5f, 1.f}, std::numbers::pi_v<float> / 2.f); |
| 149 | + |
| 150 | + const auto ray_along_x = make_ray({10.f, 0.f, 0.f}, {-10.f, 0.f, 0.f}); |
| 151 | + const auto hit_x = LineTracer::get_ray_hit_point(ray_along_x, box); |
| 152 | + EXPECT_NE(hit_x, ray_along_x.end); |
| 153 | + EXPECT_NEAR(hit_x.x, 0.5f, 1e-4f); // hit on the rotated slab's narrow side |
| 154 | + |
| 155 | + const auto ray_along_y = make_ray({0.f, 10.f, 0.f}, {0.f, -10.f, 0.f}); |
| 156 | + const auto hit_y = LineTracer::get_ray_hit_point(ray_along_y, box); |
| 157 | + EXPECT_NE(hit_y, ray_along_y.end); |
| 158 | + EXPECT_NEAR(hit_y.y, 5.f, 1e-4f); // hit on the long end at y=+5 |
| 159 | +} |
| 160 | + |
| 161 | +TEST(LineTracerOBBTests, RotatedAndTranslatedBoxHit) |
| 162 | +{ |
| 163 | + const auto box = rotated_around_z({10.f, 5.f, 0.f}, {1.f, 1.f, 1.f}, std::numbers::pi_v<float> / 4.f); |
| 164 | + // Ray approaches the rotated box from +X. |
| 165 | + const auto ray = make_ray({20.f, 5.f, 0.f}, {0.f, 5.f, 0.f}); |
| 166 | + |
| 167 | + const auto hit = LineTracer::get_ray_hit_point(ray, box); |
| 168 | + EXPECT_NE(hit, ray.end); |
| 169 | + EXPECT_NEAR(hit.x, 10.f + std::numbers::sqrt2_v<float>, 1e-4f); |
| 170 | + EXPECT_NEAR(hit.y, 5.f, 1e-4f); |
| 171 | +} |
| 172 | + |
| 173 | +TEST(LineTracerOBBTests, ParallelRayOutsideMisses) |
| 174 | +{ |
| 175 | + // Ray runs parallel to a slab face, completely outside the slab. |
| 176 | + const auto box = axis_aligned_obb({0.f, 0.f, 0.f}, {1.f, 1.f, 1.f}); |
| 177 | + const auto ray = make_ray({-5.f, 2.f, 0.f}, {5.f, 2.f, 0.f}); |
| 178 | + |
| 179 | + const auto hit = LineTracer::get_ray_hit_point(ray, box); |
| 180 | + EXPECT_EQ(hit, ray.end); |
| 181 | +} |
| 182 | + |
| 183 | +TEST(LineTracerOBBTests, ParallelRayInsideHits) |
| 184 | +{ |
| 185 | + // Ray runs parallel to a slab face but inside the slab — should still hit the entry plane. |
| 186 | + const auto box = axis_aligned_obb({0.f, 0.f, 0.f}, {1.f, 1.f, 1.f}); |
| 187 | + const auto ray = make_ray({-5.f, 0.5f, 0.f}, {5.f, 0.5f, 0.f}); |
| 188 | + |
| 189 | + const auto hit = LineTracer::get_ray_hit_point(ray, box); |
| 190 | + EXPECT_NE(hit, ray.end); |
| 191 | + EXPECT_NEAR(hit.x, -1.f, 1e-4f); |
| 192 | +} |
| 193 | + |
| 194 | +TEST(LineTracerOBBTests, MatchesAabbForAxisAlignedBox) |
| 195 | +{ |
| 196 | + using AABB = omath::primitives::Aabb<float>; |
| 197 | + |
| 198 | + struct |
| 199 | + { |
| 200 | + Vec3 center; |
| 201 | + Vec3 half; |
| 202 | + Vec3 ray_start; |
| 203 | + Vec3 ray_end; |
| 204 | + } cases[] = { |
| 205 | + {{0.f, 0.f, 0.f}, {1.f, 1.f, 1.f}, {-5.f, 0.f, 0.f}, {5.f, 0.f, 0.f}}, |
| 206 | + {{0.f, 0.f, 0.f}, {1.f, 1.f, 1.f}, {0.f, -5.f, 0.f}, {0.f, 5.f, 0.f}}, |
| 207 | + {{4.f, 0.f, 0.f}, {1.f, 1.f, 1.f}, {0.f, 0.f, 0.f}, {2.f, 0.f, 0.f}}, // too short |
| 208 | + {{0.f, 0.f, 0.f}, {1.f, 1.f, 1.f}, {-5.f, 5.f, 0.f}, {5.f, 5.f, 0.f}}, // miss |
| 209 | + {{2.f, 3.f, -1.f}, {0.5f, 0.5f, 0.5f}, {0.f, 0.f, 0.f}, {10.f, 15.f, -5.f}}, // diagonal |
| 210 | + }; |
| 211 | + |
| 212 | + for (const auto& [center, half, start, end]: cases) |
| 213 | + { |
| 214 | + const AABB aabb{center - half, center + half}; |
| 215 | + const auto obb = axis_aligned_obb(center, half); |
| 216 | + const auto ray = make_ray(start, end); |
| 217 | + |
| 218 | + const auto aabb_hit = LineTracer::get_ray_hit_point(ray, aabb); |
| 219 | + const auto obb_hit = LineTracer::get_ray_hit_point(ray, obb); |
| 220 | + |
| 221 | + EXPECT_NEAR(aabb_hit.x, obb_hit.x, 1e-4f); |
| 222 | + EXPECT_NEAR(aabb_hit.y, obb_hit.y, 1e-4f); |
| 223 | + EXPECT_NEAR(aabb_hit.z, obb_hit.z, 1e-4f); |
| 224 | + } |
| 225 | +} |
0 commit comments