Skip to content

Commit 6413c5d

Browse files
committed
added ray tracer check for obb in line tracer
1 parent 94f8805 commit 6413c5d

2 files changed

Lines changed: 282 additions & 0 deletions

File tree

include/omath/collision/line_tracer.hpp

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#pragma once
55

66
#include "omath/3d_primitives/aabb.hpp"
7+
#include "omath/3d_primitives/obb.hpp"
78
#include "omath/linear_algebra/triangle.hpp"
89
#include "omath/linear_algebra/vector3.hpp"
910

@@ -36,6 +37,7 @@ namespace omath::collision
3637
{
3738
using TriangleType = Triangle<typename RayType::VectorType>;
3839
using AABBType = primitives::Aabb<typename RayType::VectorType::ContainedType>;
40+
using OBBType = primitives::Obb<typename RayType::VectorType::ContainedType>;
3941

4042
public:
4143
LineTracer() = delete;
@@ -137,6 +139,61 @@ namespace omath::collision
137139
return ray.start + dir * t_hit;
138140
}
139141

142+
// Slab method ray-OBB intersection. Project the ray into the OBB's local frame
143+
// (axes are orthonormal, so the inverse rotation is just a transpose / dot products),
144+
// then run the standard slab test against the local box [-half_extents, +half_extents].
145+
// The ray parameter t is invariant under rigid transform, so the hit point is recovered
146+
// in world space as ray.start + dir * t_hit.
147+
[[nodiscard]]
148+
constexpr static auto get_ray_hit_point(const RayType& ray, const OBBType& obb) noexcept
149+
{
150+
using T = typename RayType::VectorType::ContainedType;
151+
152+
const auto offset = ray.start - obb.center;
153+
const auto dir = ray.direction_vector();
154+
155+
const T local_start[3] = {offset.dot(obb.axis_x), offset.dot(obb.axis_y), offset.dot(obb.axis_z)};
156+
const T local_dir[3] = {dir.dot(obb.axis_x), dir.dot(obb.axis_y), dir.dot(obb.axis_z)};
157+
const T half[3] = {obb.half_extents.x, obb.half_extents.y, obb.half_extents.z};
158+
159+
auto t_min = -std::numeric_limits<T>::infinity();
160+
auto t_max = std::numeric_limits<T>::infinity();
161+
162+
const auto process_axis = [&](const T& d, const T& origin, const T& h) -> bool
163+
{
164+
constexpr T k_epsilon = std::numeric_limits<T>::epsilon();
165+
if (std::abs(d) < k_epsilon)
166+
return origin >= -h && origin <= h;
167+
168+
const T inv = T(1) / d;
169+
T t0 = (-h - origin) * inv;
170+
T t1 = (h - origin) * inv;
171+
if (t0 > t1)
172+
std::swap(t0, t1);
173+
174+
t_min = std::max(t_min, t0);
175+
t_max = std::min(t_max, t1);
176+
return t_min <= t_max;
177+
};
178+
179+
if (!process_axis(local_dir[0], local_start[0], half[0]))
180+
return ray.end;
181+
if (!process_axis(local_dir[1], local_start[1], half[1]))
182+
return ray.end;
183+
if (!process_axis(local_dir[2], local_start[2], half[2]))
184+
return ray.end;
185+
186+
const T t_hit = std::max(T(0), t_min);
187+
188+
if (t_max < T(0))
189+
return ray.end; // box entirely behind origin
190+
191+
if (!ray.infinite_length && t_hit > T(1))
192+
return ray.end; // box beyond ray endpoint
193+
194+
return ray.start + dir * t_hit;
195+
}
196+
140197
template<class MeshType>
141198
[[nodiscard]]
142199
constexpr static auto get_ray_hit_point(const RayType& ray, const MeshType& mesh) noexcept
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
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

Comments
 (0)