Compare commits

..

9 Commits

Author SHA1 Message Date
414b2af289 added gjk tests 2026-03-02 19:58:31 +03:00
a79ad6948c optimized 2026-03-02 19:40:45 +03:00
ea2c7c3d7f added benchmark 2026-03-02 19:40:37 +03:00
91c2e0d74b Merge pull request #159 from orange-cpp/feature/color_update
Feature/color update
2026-03-01 13:53:18 +03:00
52e9b906ff added const 2026-03-01 13:32:13 +03:00
cc6d625c2d added more formaters 2026-03-01 13:30:32 +03:00
5eaec70846 fixed tests 2026-03-01 13:22:15 +03:00
2063c4d33a updated color 2026-03-01 13:15:09 +03:00
60bf8ca30f moved file 2026-03-01 13:00:24 +03:00
7 changed files with 690 additions and 189 deletions

View File

@@ -0,0 +1,161 @@
//
// Created by Vlad on 3/2/2026.
//
#include <benchmark/benchmark.h>
#include <memory_resource>
#include <omath/collision/epa_algorithm.hpp>
#include <omath/collision/gjk_algorithm.hpp>
#include <omath/engines/source_engine/collider.hpp>
#include <omath/engines/source_engine/mesh.hpp>
using Mesh = omath::source_engine::Mesh;
using Collider = omath::source_engine::MeshCollider;
using Gjk = omath::collision::GjkAlgorithm<Collider>;
using Epa = omath::collision::Epa<Collider>;
namespace
{
// Unit cube with half-extent 1 — 8 vertices in [-1,1]^3.
const std::vector<omath::primitives::Vertex<>> k_cube_vbo = {
{ { -1.f, -1.f, -1.f }, {}, {} },
{ { -1.f, -1.f, 1.f }, {}, {} },
{ { -1.f, 1.f, -1.f }, {}, {} },
{ { -1.f, 1.f, 1.f }, {}, {} },
{ { 1.f, 1.f, 1.f }, {}, {} },
{ { 1.f, 1.f, -1.f }, {}, {} },
{ { 1.f, -1.f, 1.f }, {}, {} },
{ { 1.f, -1.f, -1.f }, {}, {} },
};
const std::vector<omath::Vector3<std::uint32_t>> k_empty_vao{};
} // namespace
// ---------------------------------------------------------------------------
// GJK benchmarks
// ---------------------------------------------------------------------------
// Separated cubes — origin distance 2.1, no overlap.
// Exercises the early-exit path and the centroid-based initial direction.
static void BM_Gjk_Separated(benchmark::State& state)
{
const Collider a{Mesh{k_cube_vbo, k_empty_vao}};
Mesh mesh_b{k_cube_vbo, k_empty_vao};
mesh_b.set_origin({0.f, 2.1f, 0.f});
const Collider b{mesh_b};
for ([[maybe_unused]] auto _ : state)
benchmark::DoNotOptimize(Gjk::is_collide(a, b));
}
// Overlapping cubes — B offset by 0.5 along X, ~1.5 units penetration depth.
static void BM_Gjk_Overlapping(benchmark::State& state)
{
const Collider a{Mesh{k_cube_vbo, k_empty_vao}};
Mesh mesh_b{k_cube_vbo, k_empty_vao};
mesh_b.set_origin({0.5f, 0.f, 0.f});
const Collider b{mesh_b};
for ([[maybe_unused]] auto _ : state)
benchmark::DoNotOptimize(Gjk::is_collide(a, b));
}
// Identical cubes at the same origin — deep overlap / worst case for GJK.
static void BM_Gjk_SameOrigin(benchmark::State& state)
{
const Collider a{Mesh{k_cube_vbo, k_empty_vao}};
const Collider b{Mesh{k_cube_vbo, k_empty_vao}};
for ([[maybe_unused]] auto _ : state)
benchmark::DoNotOptimize(Gjk::is_collide(a, b));
}
// ---------------------------------------------------------------------------
// EPA benchmarks
// ---------------------------------------------------------------------------
// EPA with a pre-allocated monotonic buffer (reset each iteration).
// Isolates algorithmic cost from allocator overhead.
static void BM_Epa_MonotonicBuffer(benchmark::State& state)
{
const Collider a{Mesh{k_cube_vbo, k_empty_vao}};
Mesh mesh_b{k_cube_vbo, k_empty_vao};
mesh_b.set_origin({0.5f, 0.f, 0.f});
const Collider b{mesh_b};
const auto [hit, simplex] = Gjk::is_collide_with_simplex_info(a, b);
if (!hit)
return; // shouldn't happen, but guard for safety
constexpr Epa::Params params{.max_iterations = 64, .tolerance = 1e-4f};
// Pre-allocate a 32 KiB stack buffer — enough for typical polytope growth.
constexpr std::size_t k_buf_size = 32768;
alignas(std::max_align_t) char buf[k_buf_size];
std::pmr::monotonic_buffer_resource mr{buf, k_buf_size, std::pmr::null_memory_resource()};
for ([[maybe_unused]] auto _ : state)
{
mr.release(); // reset the buffer without touching the upstream resource
benchmark::DoNotOptimize(Epa::solve(a, b, simplex, params, mr));
}
}
// EPA with the default (malloc-backed) memory resource.
// Shows total cost including allocator pressure.
static void BM_Epa_DefaultResource(benchmark::State& state)
{
const Collider a{Mesh{k_cube_vbo, k_empty_vao}};
Mesh mesh_b{k_cube_vbo, k_empty_vao};
mesh_b.set_origin({0.5f, 0.f, 0.f});
const Collider b{mesh_b};
const auto [hit, simplex] = Gjk::is_collide_with_simplex_info(a, b);
if (!hit)
return;
constexpr Epa::Params params{.max_iterations = 64, .tolerance = 1e-4f};
for ([[maybe_unused]] auto _ : state)
benchmark::DoNotOptimize(Epa::solve(a, b, simplex, params));
}
// ---------------------------------------------------------------------------
// Combined GJK + EPA pipeline
// ---------------------------------------------------------------------------
// Full collision pipeline: GJK detects contact, EPA resolves penetration.
// This is the hot path in a physics engine tick.
static void BM_GjkEpa_Pipeline(benchmark::State& state)
{
const Collider a{Mesh{k_cube_vbo, k_empty_vao}};
Mesh mesh_b{k_cube_vbo, k_empty_vao};
mesh_b.set_origin({0.5f, 0.f, 0.f});
const Collider b{mesh_b};
constexpr Epa::Params params{.max_iterations = 64, .tolerance = 1e-4f};
constexpr std::size_t k_buf_size = 32768;
alignas(std::max_align_t) char buf[k_buf_size];
std::pmr::monotonic_buffer_resource mr{buf, k_buf_size, std::pmr::null_memory_resource()};
for ([[maybe_unused]] auto _ : state)
{
mr.release();
const auto [hit, simplex] = Gjk::is_collide_with_simplex_info(a, b);
if (hit)
benchmark::DoNotOptimize(Epa::solve(a, b, simplex, params, mr));
}
}
BENCHMARK(BM_Gjk_Separated)->Iterations(100'000);
BENCHMARK(BM_Gjk_Overlapping)->Iterations(100'000);
BENCHMARK(BM_Gjk_SameOrigin)->Iterations(100'000);
BENCHMARK(BM_Epa_MonotonicBuffer)->Iterations(100'000);
BENCHMARK(BM_Epa_DefaultResource)->Iterations(100'000);
BENCHMARK(BM_GjkEpa_Pipeline)->Iterations(100'000);

View File

@@ -50,89 +50,102 @@ namespace omath::collision
int max_iterations{64};
FloatingType tolerance{1e-4}; // absolute tolerance on distance growth
};
// Precondition: simplex.size()==4 and contains the origin.
[[nodiscard]]
static std::optional<Result> solve(const ColliderInterfaceType& a, const ColliderInterfaceType& b,
const Simplex<VectorType>& simplex, const Params params = {},
std::pmr::memory_resource& mem_resource = *std::pmr::get_default_resource())
{
// --- Build initial polytope from simplex (4 points) ---
std::pmr::vector<VectorType> vertexes = build_initial_polytope_from_simplex(simplex, mem_resource);
// Initial tetra faces (windings corrected in make_face)
std::pmr::vector<Face> faces = create_initial_tetra_faces(mem_resource, vertexes);
auto heap = rebuild_heap(faces, mem_resource);
// Build initial min-heap by distance.
Heap heap = rebuild_heap(faces, mem_resource);
Result out{};
// Hoisted outside the loop to reuse the allocation across iterations.
std::pmr::vector<Edge> boundary{&mem_resource};
boundary.reserve(16);
for (int it = 0; it < params.max_iterations; ++it)
{
// If heap might be stale after face edits, rebuild lazily.
if (heap.empty())
break;
// Rebuild when the "closest" face changed (simple cheap guard)
// (We could keep face handles; this is fine for small Ns.)
if (const auto top = heap.top(); faces[top.idx].d != top.d)
heap = rebuild_heap(faces, mem_resource);
// Lazily discard stale (deleted or index-mismatched) heap entries.
while (!heap.empty())
{
const auto& top = heap.top();
if (!faces[top.idx].deleted && faces[top.idx].d == top.d)
break;
heap.pop();
}
if (heap.empty())
break;
// FIXME: STORE REF VALUE, DO NOT USE
// AFTER IF STATEMENT BLOCK
const Face& face = faces[heap.top().idx];
// Get the furthest point in face normal direction
const VectorType p = support_point(a, b, face.n);
const auto p_dist = face.n.dot(p);
// Converged if we cant push the face closer than tolerance
// Converged: new support can't push the face closer than tolerance.
if (p_dist - face.d <= params.tolerance)
{
out.normal = face.n;
out.depth = face.d; // along unit normal
out.depth = face.d;
out.iterations = it + 1;
out.num_vertices = static_cast<int>(vertexes.size());
out.num_faces = static_cast<int>(faces.size());
out.penetration_vector = out.normal * out.depth;
return out;
}
// Add new vertex
const int new_idx = static_cast<int>(vertexes.size());
vertexes.emplace_back(p);
const auto [to_delete, boundary] = mark_visible_and_collect_horizon(faces, p);
// Tombstone visible faces and collect the horizon boundary.
// This avoids copying the faces array (O(n)) each iteration.
boundary.clear();
for (auto& f : faces)
{
if (!f.deleted && visible_from(f, p))
{
f.deleted = true;
add_edge_boundary(boundary, f.i0, f.i1);
add_edge_boundary(boundary, f.i1, f.i2);
add_edge_boundary(boundary, f.i2, f.i0);
}
}
erase_marked(faces, to_delete);
// Stitch new faces around the horizon
// Stitch new faces around the horizon and push them directly onto the
// heap — no full O(n log n) rebuild needed.
for (const auto& e : boundary)
{
const int fi = static_cast<int>(faces.size());
faces.emplace_back(make_face(vertexes, e.a, e.b, new_idx));
// Rebuild heap after topology change
heap = rebuild_heap(faces, mem_resource);
heap.emplace(faces.back().d, fi);
}
if (!std::isfinite(vertexes.back().dot(vertexes.back())))
break; // safety
out.iterations = it + 1;
}
if (faces.empty())
// Find the best surviving (non-deleted) face.
const Face* best = nullptr;
for (const auto& f : faces)
if (!f.deleted && (best == nullptr || f.d < best->d))
best = &f;
if (!best)
return std::nullopt;
const auto best = *std::ranges::min_element(faces, [](const auto& first, const auto& second)
{ return first.d < second.d; });
out.normal = best.n;
out.depth = best.d;
out.normal = best->n;
out.depth = best->d;
out.num_vertices = static_cast<int>(vertexes.size());
out.num_faces = static_cast<int>(faces.size());
out.penetration_vector = out.normal * out.depth;
return out;
}
@@ -140,8 +153,9 @@ namespace omath::collision
struct Face final
{
int i0, i1, i2;
VectorType n; // unit outward normal
FloatingType d; // n · v0 (>=0 ideally because origin is inside)
VectorType n; // unit outward normal
FloatingType d; // n · v0 (>= 0 ideally because origin is inside)
bool deleted{false}; // tombstone flag — avoids O(n) compaction per iteration
};
struct Edge final
@@ -154,6 +168,7 @@ namespace omath::collision
FloatingType d;
int idx;
};
struct HeapCmp final
{
[[nodiscard]]
@@ -169,31 +184,28 @@ namespace omath::collision
static Heap rebuild_heap(const std::pmr::vector<Face>& faces, auto& memory_resource)
{
std::pmr::vector<HeapItem> storage{&memory_resource};
storage.reserve(faces.size()); // optional but recommended
storage.reserve(faces.size());
Heap h{HeapCmp{}, std::move(storage)};
for (int i = 0; i < static_cast<int>(faces.size()); ++i)
h.emplace(faces[i].d, i);
return h; // allocator is preserved
if (!faces[i].deleted)
h.emplace(faces[i].d, i);
return h;
}
[[nodiscard]]
static bool visible_from(const Face& f, const VectorType& p)
{
// positive if p is in front of the face
return f.n.dot(p) - f.d > static_cast<FloatingType>(1e-7);
}
static void add_edge_boundary(std::pmr::vector<Edge>& boundary, int a, int b)
{
// Keep edges that appear only once; erase if opposite already present
// Keep edges that appear only once; cancel if opposite already present.
auto itb = std::ranges::find_if(boundary, [&](const Edge& e) { return e.a == b && e.b == a; });
if (itb != boundary.end())
boundary.erase(itb); // internal edge cancels out
boundary.erase(itb);
else
boundary.emplace_back(a, b); // horizon edge (directed)
boundary.emplace_back(a, b);
}
[[nodiscard]]
@@ -204,9 +216,7 @@ namespace omath::collision
const VectorType& a2 = vertexes[i2];
VectorType n = (a1 - a0).cross(a2 - a0);
if (n.dot(n) <= static_cast<FloatingType>(1e-30))
{
n = any_perp_vec(a1 - a0); // degenerate guard
}
// Ensure normal points outward (away from origin): require n·a0 >= 0
if (n.dot(a0) < static_cast<FloatingType>(0.0))
{
@@ -243,6 +253,7 @@ namespace omath::collision
return d;
return V{1, 0, 0};
}
[[nodiscard]]
static std::pmr::vector<Face> create_initial_tetra_faces(std::pmr::memory_resource& mem_resource,
const std::pmr::vector<VectorType>& vertexes)
@@ -262,48 +273,9 @@ namespace omath::collision
{
std::pmr::vector<VectorType> vertexes{&mem_resource};
vertexes.reserve(simplex.size());
for (std::size_t i = 0; i < simplex.size(); ++i)
vertexes.emplace_back(simplex[i]);
return vertexes;
}
static void erase_marked(std::pmr::vector<Face>& faces, const std::pmr::vector<bool>& to_delete)
{
auto* mr = faces.get_allocator().resource(); // keep same resource
std::pmr::vector<Face> kept{mr};
kept.reserve(faces.size());
for (std::size_t i = 0; i < faces.size(); ++i)
if (!to_delete[i])
kept.emplace_back(faces[i]);
faces.swap(kept);
}
struct Horizon
{
std::pmr::vector<bool> to_delete;
std::pmr::vector<Edge> boundary;
};
static Horizon mark_visible_and_collect_horizon(const std::pmr::vector<Face>& faces, const VectorType& p)
{
auto* mr = faces.get_allocator().resource();
Horizon horizon{std::pmr::vector<bool>(faces.size(), false, mr), std::pmr::vector<Edge>(mr)};
horizon.boundary.reserve(faces.size());
for (std::size_t i = 0; i < faces.size(); ++i)
if (visible_from(faces[i], p))
{
const auto& rf = faces[i];
horizon.to_delete[i] = true;
add_edge_boundary(horizon.boundary, rf.i0, rf.i1);
add_edge_boundary(horizon.boundary, rf.i1, rf.i2);
add_edge_boundary(horizon.boundary, rf.i2, rf.i0);
}
return horizon;
}
};
} // namespace omath::collision

View File

@@ -43,7 +43,20 @@ namespace omath::collision
const ColliderInterfaceType& collider_b,
const GjkSettings& settings = {})
{
auto support = find_support_vertex(collider_a, collider_b, VectorType{1, 0, 0});
// Use centroid difference as initial direction — greatly reduces iterations for separated shapes.
VectorType initial_dir;
if constexpr (requires { collider_b.get_origin() - collider_a.get_origin(); })
{
initial_dir = collider_b.get_origin() - collider_a.get_origin();
if (initial_dir.dot(initial_dir) < settings.epsilon * settings.epsilon)
initial_dir = VectorType{1, 0, 0};
}
else
{
initial_dir = VectorType{1, 0, 0};
}
auto support = find_support_vertex(collider_a, collider_b, initial_dir);
Simplex<VectorType> simplex;
simplex.push_front(support);

View File

@@ -16,19 +16,28 @@ namespace omath
float value{};
};
class Color final : public Vector4<float>
class Color final
{
Vector4<float> m_value;
public:
constexpr Color(const float r, const float g, const float b, const float a) noexcept: Vector4(r, g, b, a)
constexpr const Vector4<float>& value() const
{
clamp(0.f, 1.f);
return m_value;
}
constexpr Color(const float r, const float g, const float b, const float a) noexcept: m_value(r, g, b, a)
{
m_value.clamp(0.f, 1.f);
}
constexpr explicit Color(const Vector4<float>& value) : m_value(value)
{
m_value.clamp(0.f, 1.f);
}
constexpr explicit Color() noexcept = default;
[[nodiscard]]
constexpr static Color from_rgba(const uint8_t r, const uint8_t g, const uint8_t b, const uint8_t a) noexcept
{
return Color{Vector4(r, g, b, a) / 255.f};
return Color(Vector4<float>(r, g, b, a) / 255.f);
}
[[nodiscard]]
@@ -82,9 +91,9 @@ namespace omath
{
Hsv hsv_data;
const float& red = x;
const float& green = y;
const float& blue = z;
const float& red = m_value.x;
const float& green = m_value.y;
const float& blue = m_value.z;
const float max = std::max({red, green, blue});
const float min = std::min({red, green, blue});
@@ -109,11 +118,6 @@ namespace omath
return hsv_data;
}
constexpr explicit Color(const Vector4& vec) noexcept: Vector4(vec)
{
clamp(0.f, 1.f);
}
constexpr void set_hue(const float hue) noexcept
{
auto hsv = to_hsv();
@@ -141,7 +145,7 @@ namespace omath
constexpr Color blend(const Color& other, float ratio) const noexcept
{
ratio = std::clamp(ratio, 0.f, 1.f);
return Color(*this * (1.f - ratio) + other * ratio);
return Color(this->m_value * (1.f - ratio) + other.m_value * ratio);
}
[[nodiscard]] static constexpr Color red()
@@ -160,16 +164,26 @@ namespace omath
[[nodiscard]]
ImColor to_im_color() const noexcept
{
return {to_im_vec4()};
return {m_value.to_im_vec4()};
}
#endif
[[nodiscard]] std::string to_string() const noexcept
{
return std::format("[r:{}, g:{}, b:{}, a:{}]",
static_cast<int>(x * 255.f),
static_cast<int>(y * 255.f),
static_cast<int>(z * 255.f),
static_cast<int>(w * 255.f));
static_cast<int>(m_value.x * 255.f),
static_cast<int>(m_value.y * 255.f),
static_cast<int>(m_value.z * 255.f),
static_cast<int>(m_value.w * 255.f));
}
[[nodiscard]] std::string to_rgbf_string() const noexcept
{
return std::format("[r:{}, g:{}, b:{}, a:{}]",
m_value.x, m_value.y, m_value.z, m_value.w);
}
[[nodiscard]] std::string to_hsv_string() const noexcept
{
const auto [hue, saturation, value] = to_hsv();
return std::format("[h:{}, s:{}, v:{}]", hue, saturation, value);
}
[[nodiscard]] std::wstring to_wstring() const noexcept
{
@@ -188,23 +202,55 @@ namespace omath
template<>
struct std::formatter<omath::Color> // NOLINT(*-dcl58-cpp)
{
[[nodiscard]]
static constexpr auto parse(const std::format_parse_context& ctx)
enum class ColorFormat { rgb, rgbf, hsv };
ColorFormat color_format = ColorFormat::rgb;
constexpr auto parse(std::format_parse_context& ctx)
{
return ctx.begin();
const auto it = ctx.begin();
const auto end = ctx.end();
if (it == end || *it == '}')
return it;
const std::string_view spec(it, end);
if (spec.starts_with("rgbf"))
{
color_format = ColorFormat::rgbf;
return it + 4;
}
if (spec.starts_with("rgb"))
{
color_format = ColorFormat::rgb;
return it + 3;
}
if (spec.starts_with("hsv"))
{
color_format = ColorFormat::hsv;
return it + 3;
}
throw std::format_error("Invalid format specifier for omath::Color. Use rgb, rgbf, or hsv.");
}
template<class FormatContext>
[[nodiscard]]
static auto format(const omath::Color& col, FormatContext& ctx)
auto format(const omath::Color& col, FormatContext& ctx) const
{
if constexpr (std::is_same_v<typename FormatContext::char_type, char>)
return std::format_to(ctx.out(), "{}", col.to_string());
if constexpr (std::is_same_v<typename FormatContext::char_type, wchar_t>)
return std::format_to(ctx.out(), L"{}", col.to_wstring());
std::string str;
switch (color_format)
{
case ColorFormat::rgb: str = col.to_string(); break;
case ColorFormat::rgbf: str = col.to_rgbf_string(); break;
case ColorFormat::hsv: str = col.to_hsv_string(); break;
}
if constexpr (std::is_same_v<typename FormatContext::char_type, char>)
return std::format_to(ctx.out(), "{}", str);
if constexpr (std::is_same_v<typename FormatContext::char_type, wchar_t>)
return std::format_to(ctx.out(), L"{}", std::wstring(str.cbegin(), str.cend()));
if constexpr (std::is_same_v<typename FormatContext::char_type, char8_t>)
return std::format_to(ctx.out(), u8"{}", col.to_u8string());
return std::format_to(ctx.out(), u8"{}", std::u8string(str.cbegin(), str.cend()));
std::unreachable();
}

View File

@@ -26,38 +26,38 @@ protected:
TEST_F(UnitTestColorGrouped, Constructor_Float)
{
constexpr Color color(0.5f, 0.5f, 0.5f, 1.0f);
EXPECT_FLOAT_EQ(color.x, 0.5f);
EXPECT_FLOAT_EQ(color.y, 0.5f);
EXPECT_FLOAT_EQ(color.z, 0.5f);
EXPECT_FLOAT_EQ(color.w, 1.0f);
EXPECT_FLOAT_EQ(color.value().x, 0.5f);
EXPECT_FLOAT_EQ(color.value().y, 0.5f);
EXPECT_FLOAT_EQ(color.value().z, 0.5f);
EXPECT_FLOAT_EQ(color.value().w, 1.0f);
}
TEST_F(UnitTestColorGrouped, Constructor_Vector4)
{
constexpr omath::Vector4 vec(0.2f, 0.4f, 0.6f, 0.8f);
constexpr Color color(vec);
EXPECT_FLOAT_EQ(color.x, 0.2f);
EXPECT_FLOAT_EQ(color.y, 0.4f);
EXPECT_FLOAT_EQ(color.z, 0.6f);
EXPECT_FLOAT_EQ(color.w, 0.8f);
EXPECT_FLOAT_EQ(color.value().x, 0.2f);
EXPECT_FLOAT_EQ(color.value().y, 0.4f);
EXPECT_FLOAT_EQ(color.value().z, 0.6f);
EXPECT_FLOAT_EQ(color.value().w, 0.8f);
}
TEST_F(UnitTestColorGrouped, FromRGBA)
{
constexpr Color color = Color::from_rgba(128, 64, 32, 255);
EXPECT_FLOAT_EQ(color.x, 128.0f / 255.0f);
EXPECT_FLOAT_EQ(color.y, 64.0f / 255.0f);
EXPECT_FLOAT_EQ(color.z, 32.0f / 255.0f);
EXPECT_FLOAT_EQ(color.w, 1.0f);
EXPECT_FLOAT_EQ(color.value().x, 128.0f / 255.0f);
EXPECT_FLOAT_EQ(color.value().y, 64.0f / 255.0f);
EXPECT_FLOAT_EQ(color.value().z, 32.0f / 255.0f);
EXPECT_FLOAT_EQ(color.value().w, 1.0f);
}
TEST_F(UnitTestColorGrouped, FromHSV)
{
constexpr Color color = Color::from_hsv(0.0f, 1.0f, 1.0f); // Red in HSV
EXPECT_FLOAT_EQ(color.x, 1.0f);
EXPECT_FLOAT_EQ(color.y, 0.0f);
EXPECT_FLOAT_EQ(color.z, 0.0f);
EXPECT_FLOAT_EQ(color.w, 1.0f);
EXPECT_FLOAT_EQ(color.value().x, 1.0f);
EXPECT_FLOAT_EQ(color.value().y, 0.0f);
EXPECT_FLOAT_EQ(color.value().z, 0.0f);
EXPECT_FLOAT_EQ(color.value().w, 1.0f);
}
TEST_F(UnitTestColorGrouped, ToHSV)
@@ -71,10 +71,10 @@ TEST_F(UnitTestColorGrouped, ToHSV)
TEST_F(UnitTestColorGrouped, Blend)
{
const Color blended = color1.blend(color2, 0.5f);
EXPECT_FLOAT_EQ(blended.x, 0.5f);
EXPECT_FLOAT_EQ(blended.y, 0.5f);
EXPECT_FLOAT_EQ(blended.z, 0.0f);
EXPECT_FLOAT_EQ(blended.w, 1.0f);
EXPECT_FLOAT_EQ(blended.value().x, 0.5f);
EXPECT_FLOAT_EQ(blended.value().y, 0.5f);
EXPECT_FLOAT_EQ(blended.value().z, 0.0f);
EXPECT_FLOAT_EQ(blended.value().w, 1.0f);
}
TEST_F(UnitTestColorGrouped, PredefinedColors)
@@ -83,20 +83,20 @@ TEST_F(UnitTestColorGrouped, PredefinedColors)
constexpr Color green = Color::green();
constexpr Color blue = Color::blue();
EXPECT_FLOAT_EQ(red.x, 1.0f);
EXPECT_FLOAT_EQ(red.y, 0.0f);
EXPECT_FLOAT_EQ(red.z, 0.0f);
EXPECT_FLOAT_EQ(red.w, 1.0f);
EXPECT_FLOAT_EQ(red.value().x, 1.0f);
EXPECT_FLOAT_EQ(red.value().y, 0.0f);
EXPECT_FLOAT_EQ(red.value().z, 0.0f);
EXPECT_FLOAT_EQ(red.value().w, 1.0f);
EXPECT_FLOAT_EQ(green.x, 0.0f);
EXPECT_FLOAT_EQ(green.y, 1.0f);
EXPECT_FLOAT_EQ(green.z, 0.0f);
EXPECT_FLOAT_EQ(green.w, 1.0f);
EXPECT_FLOAT_EQ(green.value().x, 0.0f);
EXPECT_FLOAT_EQ(green.value().y, 1.0f);
EXPECT_FLOAT_EQ(green.value().z, 0.0f);
EXPECT_FLOAT_EQ(green.value().w, 1.0f);
EXPECT_FLOAT_EQ(blue.x, 0.0f);
EXPECT_FLOAT_EQ(blue.y, 0.0f);
EXPECT_FLOAT_EQ(blue.z, 1.0f);
EXPECT_FLOAT_EQ(blue.w, 1.0f);
EXPECT_FLOAT_EQ(blue.value().x, 0.0f);
EXPECT_FLOAT_EQ(blue.value().y, 0.0f);
EXPECT_FLOAT_EQ(blue.value().z, 1.0f);
EXPECT_FLOAT_EQ(blue.value().w, 1.0f);
}
TEST_F(UnitTestColorGrouped, BlendVector3)
@@ -104,9 +104,9 @@ TEST_F(UnitTestColorGrouped, BlendVector3)
constexpr Color v1(1.0f, 0.0f, 0.0f, 1.f); // Red
constexpr Color v2(0.0f, 1.0f, 0.0f, 1.f); // Green
constexpr Color blended = v1.blend(v2, 0.5f);
EXPECT_FLOAT_EQ(blended.x, 0.5f);
EXPECT_FLOAT_EQ(blended.y, 0.5f);
EXPECT_FLOAT_EQ(blended.z, 0.0f);
EXPECT_FLOAT_EQ(blended.value().x, 0.5f);
EXPECT_FLOAT_EQ(blended.value().y, 0.5f);
EXPECT_FLOAT_EQ(blended.value().z, 0.0f);
}
// From unit_test_color_extra.cpp
@@ -148,37 +148,37 @@ TEST(UnitTestColorGrouped_Extra, BlendEdgeCases)
constexpr Color a = Color::red();
constexpr Color b = Color::blue();
constexpr auto r0 = a.blend(b, 0.f);
EXPECT_FLOAT_EQ(r0.x, a.x);
EXPECT_FLOAT_EQ(r0.value().x, a.value().x);
constexpr auto r1 = a.blend(b, 1.f);
EXPECT_FLOAT_EQ(r1.x, b.x);
EXPECT_FLOAT_EQ(r1.value().x, b.value().x);
}
// From unit_test_color_more.cpp
TEST(UnitTestColorGrouped_More, DefaultCtorIsZero)
{
constexpr Color c;
EXPECT_FLOAT_EQ(c.x, 0.0f);
EXPECT_FLOAT_EQ(c.y, 0.0f);
EXPECT_FLOAT_EQ(c.z, 0.0f);
EXPECT_FLOAT_EQ(c.w, 0.0f);
EXPECT_FLOAT_EQ(c.value().x, 0.0f);
EXPECT_FLOAT_EQ(c.value().y, 0.0f);
EXPECT_FLOAT_EQ(c.value().z, 0.0f);
EXPECT_FLOAT_EQ(c.value().w, 0.0f);
}
TEST(UnitTestColorGrouped_More, FloatCtorAndClampForRGB)
{
constexpr Color c(1.2f, -0.5f, 0.5f, 2.0f);
EXPECT_FLOAT_EQ(c.x, 1.0f);
EXPECT_FLOAT_EQ(c.y, 0.0f);
EXPECT_FLOAT_EQ(c.z, 0.5f);
EXPECT_FLOAT_EQ(c.w, 2.0f);
EXPECT_FLOAT_EQ(c.value().x, 1.0f);
EXPECT_FLOAT_EQ(c.value().y, 0.0f);
EXPECT_FLOAT_EQ(c.value().z, 0.5f);
EXPECT_FLOAT_EQ(c.value().w, 2.0f);
}
TEST(UnitTestColorGrouped_More, FromRgbaProducesScaledComponents)
{
constexpr Color c = Color::from_rgba(25u, 128u, 230u, 64u);
EXPECT_NEAR(c.x, 25.0f/255.0f, 1e-6f);
EXPECT_NEAR(c.y, 128.0f/255.0f, 1e-6f);
EXPECT_NEAR(c.z, 230.0f/255.0f, 1e-6f);
EXPECT_NEAR(c.w, 64.0f/255.0f, 1e-6f);
EXPECT_NEAR(c.value().x, 25.0f/255.0f, 1e-6f);
EXPECT_NEAR(c.value().y, 128.0f/255.0f, 1e-6f);
EXPECT_NEAR(c.value().z, 230.0f/255.0f, 1e-6f);
EXPECT_NEAR(c.value().w, 64.0f/255.0f, 1e-6f);
}
TEST(UnitTestColorGrouped_More, BlendProducesIntermediate)
@@ -186,10 +186,10 @@ TEST(UnitTestColorGrouped_More, BlendProducesIntermediate)
constexpr Color c0(0.0f, 0.0f, 0.0f, 1.0f);
constexpr Color c1(1.0f, 1.0f, 1.0f, 0.0f);
constexpr Color mid = c0.blend(c1, 0.5f);
EXPECT_FLOAT_EQ(mid.x, 0.5f);
EXPECT_FLOAT_EQ(mid.y, 0.5f);
EXPECT_FLOAT_EQ(mid.z, 0.5f);
EXPECT_FLOAT_EQ(mid.w, 0.5f);
EXPECT_FLOAT_EQ(mid.value().x, 0.5f);
EXPECT_FLOAT_EQ(mid.value().y, 0.5f);
EXPECT_FLOAT_EQ(mid.value().z, 0.5f);
EXPECT_FLOAT_EQ(mid.value().w, 0.5f);
}
TEST(UnitTestColorGrouped_More, HsvRoundTrip)
@@ -197,9 +197,9 @@ TEST(UnitTestColorGrouped_More, HsvRoundTrip)
constexpr Color red = Color::red();
const auto hsv = red.to_hsv();
const Color back = Color::from_hsv(hsv);
EXPECT_NEAR(back.x, 1.0f, 1e-6f);
EXPECT_NEAR(back.y, 0.0f, 1e-6f);
EXPECT_NEAR(back.z, 0.0f, 1e-6f);
EXPECT_NEAR(back.value().x, 1.0f, 1e-6f);
EXPECT_NEAR(back.value().y, 0.0f, 1e-6f);
EXPECT_NEAR(back.value().z, 0.0f, 1e-6f);
}
TEST(UnitTestColorGrouped_More, ToStringContainsComponents)
@@ -230,18 +230,18 @@ TEST(UnitTestColorGrouped_More2, FromHsvCases)
auto check_hue = [&](float h) {
SCOPED_TRACE(::testing::Message() << "h=" << h);
Color c = Color::from_hsv(h, 1.f, 1.f);
EXPECT_TRUE(std::isfinite(c.x));
EXPECT_TRUE(std::isfinite(c.y));
EXPECT_TRUE(std::isfinite(c.z));
EXPECT_GE(c.x, -eps);
EXPECT_LE(c.x, 1.f + eps);
EXPECT_GE(c.y, -eps);
EXPECT_LE(c.y, 1.f + eps);
EXPECT_GE(c.z, -eps);
EXPECT_LE(c.z, 1.f + eps);
EXPECT_TRUE(std::isfinite(c.value().x));
EXPECT_TRUE(std::isfinite(c.value().y));
EXPECT_TRUE(std::isfinite(c.value().z));
EXPECT_GE(c.value().x, -eps);
EXPECT_LE(c.value().x, 1.f + eps);
EXPECT_GE(c.value().y, -eps);
EXPECT_LE(c.value().y, 1.f + eps);
EXPECT_GE(c.value().z, -eps);
EXPECT_LE(c.value().z, 1.f + eps);
float mx = std::max({c.x, c.y, c.z});
float mn = std::min({c.x, c.y, c.z});
float mx = std::max({c.value().x, c.value().y, c.value().z});
float mn = std::min({c.value().x, c.value().y, c.value().z});
EXPECT_GE(mx, 0.999f);
EXPECT_LE(mn, 1e-3f + 1e-4f);
};
@@ -261,13 +261,13 @@ TEST(UnitTestColorGrouped_More2, ToHsvAndSetters)
EXPECT_NEAR(hsv.value, 0.6f, 1e-6f);
c.set_hue(0.0f);
EXPECT_TRUE(std::isfinite(c.x));
EXPECT_TRUE(std::isfinite(c.value().x));
c.set_saturation(0.0f);
EXPECT_TRUE(std::isfinite(c.y));
EXPECT_TRUE(std::isfinite(c.value().y));
c.set_value(0.5f);
EXPECT_TRUE(std::isfinite(c.z));
EXPECT_TRUE(std::isfinite(c.value().z));
}
TEST(UnitTestColorGrouped_More2, BlendAndStaticColors)
@@ -275,14 +275,14 @@ TEST(UnitTestColorGrouped_More2, BlendAndStaticColors)
constexpr Color a = Color::red();
constexpr Color b = Color::blue();
constexpr auto mid = a.blend(b, 0.5f);
EXPECT_GT(mid.x, 0.f);
EXPECT_GT(mid.z, 0.f);
EXPECT_GT(mid.value().x, 0.f);
EXPECT_GT(mid.value().z, 0.f);
constexpr auto all_a = a.blend(b, -1.f);
EXPECT_NEAR(all_a.x, a.x, 1e-6f);
EXPECT_NEAR(all_a.value().x, a.value().x, 1e-6f);
constexpr auto all_b = a.blend(b, 2.f);
EXPECT_NEAR(all_b.z, b.z, 1e-6f);
EXPECT_NEAR(all_b.value().z, b.value().z, 1e-6f);
}
TEST(UnitTestColorGrouped_More2, FormatterUsesToString)
@@ -291,3 +291,35 @@ TEST(UnitTestColorGrouped_More2, FormatterUsesToString)
const auto formatted = std::format("{}", c);
EXPECT_NE(formatted.find("r:10"), std::string::npos);
}
TEST(UnitTestColorGrouped_More2, FormatterRgb)
{
constexpr Color c = Color::from_rgba(255, 128, 0, 64);
const auto s = std::format("{:rgb}", c);
EXPECT_NE(s.find("r:255"), std::string::npos);
EXPECT_NE(s.find("g:128"), std::string::npos);
EXPECT_NE(s.find("b:0"), std::string::npos);
EXPECT_NE(s.find("a:64"), std::string::npos);
}
TEST(UnitTestColorGrouped_More2, FormatterRgbf)
{
constexpr Color c(0.5f, 0.25f, 1.0f, 0.75f);
const auto s = std::format("{:rgbf}", c);
EXPECT_NE(s.find("r:"), std::string::npos);
EXPECT_NE(s.find("g:"), std::string::npos);
EXPECT_NE(s.find("b:"), std::string::npos);
EXPECT_NE(s.find("a:"), std::string::npos);
// Values should be in [0,1] float range, not 0-255
EXPECT_EQ(s.find("r:127"), std::string::npos);
EXPECT_EQ(s.find("r:255"), std::string::npos);
}
TEST(UnitTestColorGrouped_More2, FormatterHsv)
{
const Color c = Color::red();
const auto s = std::format("{:hsv}", c);
EXPECT_NE(s.find("h:"), std::string::npos);
EXPECT_NE(s.find("s:"), std::string::npos);
EXPECT_NE(s.find("v:"), std::string::npos);
}

View File

@@ -0,0 +1,277 @@
//
// Comprehensive GJK tests.
// Covers: all 6 axis directions, diagonal cases, boundary touching,
// asymmetric sizes, nesting, symmetry, simplex info, far separation.
//
#include <gtest/gtest.h>
#include <omath/collision/gjk_algorithm.hpp>
#include <omath/engines/source_engine/collider.hpp>
#include <omath/engines/source_engine/mesh.hpp>
using Mesh = omath::source_engine::Mesh;
using Collider = omath::source_engine::MeshCollider;
using Gjk = omath::collision::GjkAlgorithm<Collider>;
using Vec3 = omath::Vector3<float>;
namespace
{
// Unit cube [-1, 1]^3 in local space.
const std::vector<omath::primitives::Vertex<>> k_cube_vbo = {
{ { -1.f, -1.f, -1.f }, {}, {} },
{ { -1.f, -1.f, 1.f }, {}, {} },
{ { -1.f, 1.f, -1.f }, {}, {} },
{ { -1.f, 1.f, 1.f }, {}, {} },
{ { 1.f, 1.f, 1.f }, {}, {} },
{ { 1.f, 1.f, -1.f }, {}, {} },
{ { 1.f, -1.f, 1.f }, {}, {} },
{ { 1.f, -1.f, -1.f }, {}, {} },
};
const std::vector<omath::Vector3<std::uint32_t>> k_empty_ebo{};
Collider make_cube(const Vec3& origin = {}, const Vec3& scale = { 1, 1, 1 })
{
Mesh m{ k_cube_vbo, k_empty_ebo, scale };
m.set_origin(origin);
return Collider{ m };
}
} // namespace
// ---------------------------------------------------------------------------
// Separation — expect false
// ---------------------------------------------------------------------------
TEST(GjkComprehensive, Separated_AlongPosX)
{
// A extends to x=1, B starts at x=1.1 → clear gap
EXPECT_FALSE(Gjk::is_collide(make_cube({ 0, 0, 0 }), make_cube({ 2.1f, 0, 0 })));
}
TEST(GjkComprehensive, Separated_AlongNegX)
{
// B to the left of A
EXPECT_FALSE(Gjk::is_collide(make_cube({ 0, 0, 0 }), make_cube({ -2.1f, 0, 0 })));
}
TEST(GjkComprehensive, Separated_AlongPosY)
{
EXPECT_FALSE(Gjk::is_collide(make_cube({ 0, 0, 0 }), make_cube({ 0, 2.1f, 0 })));
}
TEST(GjkComprehensive, Separated_AlongNegY)
{
EXPECT_FALSE(Gjk::is_collide(make_cube({ 0, 0, 0 }), make_cube({ 0, -2.1f, 0 })));
}
TEST(GjkComprehensive, Separated_AlongPosZ)
{
EXPECT_FALSE(Gjk::is_collide(make_cube({ 0, 0, 0 }), make_cube({ 0, 0, 2.1f })));
}
TEST(GjkComprehensive, Separated_AlongNegZ)
{
EXPECT_FALSE(Gjk::is_collide(make_cube({ 0, 0, 0 }), make_cube({ 0, 0, -2.1f })));
}
TEST(GjkComprehensive, Separated_AlongDiagonal)
{
// All components exceed 2.0 — no overlap on any axis
EXPECT_FALSE(Gjk::is_collide(make_cube({ 0, 0, 0 }), make_cube({ 2.1f, 2.1f, 2.1f })));
}
TEST(GjkComprehensive, Separated_LargeDistance)
{
EXPECT_FALSE(Gjk::is_collide(make_cube({ 0, 0, 0 }), make_cube({ 100.f, 0, 0 })));
}
TEST(GjkComprehensive, Separated_AsymmetricSizes)
{
// Big (scale 2, half-ext 2), small (scale 0.5, half-ext 0.5) at 2.6 → gap of 0.1
EXPECT_FALSE(Gjk::is_collide(make_cube({ 0, 0, 0 }, { 2, 2, 2 }), make_cube({ 2.6f, 0, 0 }, { 0.5f, 0.5f, 0.5f })));
}
// ---------------------------------------------------------------------------
// Overlap — expect true
// ---------------------------------------------------------------------------
TEST(GjkComprehensive, Overlapping_AlongPosX)
{
// B offset 1.5 → overlap depth 0.5 in X
EXPECT_TRUE(Gjk::is_collide(make_cube({ 0, 0, 0 }), make_cube({ 1.5f, 0, 0 })));
}
TEST(GjkComprehensive, Overlapping_AlongNegX)
{
EXPECT_TRUE(Gjk::is_collide(make_cube({ 0, 0, 0 }), make_cube({ -1.5f, 0, 0 })));
}
TEST(GjkComprehensive, Overlapping_AlongPosZ)
{
EXPECT_TRUE(Gjk::is_collide(make_cube({ 0, 0, 0 }), make_cube({ 0, 0, 1.5f })));
}
TEST(GjkComprehensive, Overlapping_AlongNegZ)
{
EXPECT_TRUE(Gjk::is_collide(make_cube({ 0, 0, 0 }), make_cube({ 0, 0, -1.5f })));
}
TEST(GjkComprehensive, Overlapping_AlongDiagonalXY)
{
// Minkowski sum extends ±2 on each axis; offset (1,1,0) is inside
EXPECT_TRUE(Gjk::is_collide(make_cube({ 0, 0, 0 }), make_cube({ 1.f, 1.f, 0.f })));
}
TEST(GjkComprehensive, Overlapping_AlongDiagonalXYZ)
{
// All three axes overlap: (1,1,1) is inside the Minkowski sum
EXPECT_TRUE(Gjk::is_collide(make_cube({ 0, 0, 0 }), make_cube({ 1.f, 1.f, 1.f })));
}
TEST(GjkComprehensive, FullyNested_SmallInsideBig)
{
// Small cube (half-ext 0.5) fully inside big cube (half-ext 2)
EXPECT_TRUE(Gjk::is_collide(make_cube({ 0, 0, 0 }, { 2, 2, 2 }), make_cube({ 0, 0, 0 }, { 0.5f, 0.5f, 0.5f })));
}
TEST(GjkComprehensive, FullyNested_OffCenter)
{
// Small at (0.5, 0, 0) still fully inside big (half-ext 2)
EXPECT_TRUE(Gjk::is_collide(make_cube({ 0, 0, 0 }, { 2, 2, 2 }), make_cube({ 0.5f, 0, 0 }, { 0.5f, 0.5f, 0.5f })));
}
TEST(GjkComprehensive, Overlapping_AsymmetricSizes)
{
// Big (scale 2, half-ext 2) and small (scale 0.5, half-ext 0.5) at 2.0 → overlap 0.5 in X
EXPECT_TRUE(Gjk::is_collide(make_cube({ 0, 0, 0 }, { 2, 2, 2 }), make_cube({ 2.0f, 0, 0 }, { 0.5f, 0.5f, 0.5f })));
}
// ---------------------------------------------------------------------------
// Boundary cases
// ---------------------------------------------------------------------------
TEST(GjkComprehensive, BoundaryCase_JustColliding)
{
// B at 1.999 — 0.001 overlap in X
EXPECT_TRUE(Gjk::is_collide(make_cube({ 0, 0, 0 }), make_cube({ 1.999f, 0, 0 })));
}
TEST(GjkComprehensive, BoundaryCase_JustSeparated)
{
// B at 2.001 — 0.001 gap in X
EXPECT_FALSE(Gjk::is_collide(make_cube({ 0, 0, 0 }), make_cube({ 2.001f, 0, 0 })));
}
// ---------------------------------------------------------------------------
// Symmetry
// ---------------------------------------------------------------------------
TEST(GjkComprehensive, Symmetry_WhenColliding)
{
const auto a = make_cube({ 0, 0, 0 });
const auto b = make_cube({ 1.5f, 0, 0 });
EXPECT_EQ(Gjk::is_collide(a, b), Gjk::is_collide(b, a));
}
TEST(GjkComprehensive, Symmetry_WhenSeparated)
{
const auto a = make_cube({ 0, 0, 0 });
const auto b = make_cube({ 2.1f, 0.5f, 0 });
EXPECT_EQ(Gjk::is_collide(a, b), Gjk::is_collide(b, a));
}
TEST(GjkComprehensive, Symmetry_DiagonalSeparation)
{
const auto a = make_cube({ 0, 0, 0 });
const auto b = make_cube({ 1.5f, 1.5f, 1.5f });
EXPECT_EQ(Gjk::is_collide(a, b), Gjk::is_collide(b, a));
}
// ---------------------------------------------------------------------------
// Simplex info
// ---------------------------------------------------------------------------
TEST(GjkComprehensive, SimplexInfo_HitProducesSimplex4)
{
// On collision the simplex must be a full tetrahedron (4 points)
const auto [hit, simplex] = Gjk::is_collide_with_simplex_info(make_cube({ 0, 0, 0 }), make_cube({ 0.5f, 0, 0 }));
EXPECT_TRUE(hit);
EXPECT_EQ(simplex.size(), 4u);
}
TEST(GjkComprehensive, SimplexInfo_MissProducesLessThan4)
{
// On non-collision the simplex can never be a full tetrahedron
const auto [hit, simplex] = Gjk::is_collide_with_simplex_info(make_cube({ 0, 0, 0 }), make_cube({ 2.1f, 0, 0 }));
EXPECT_FALSE(hit);
EXPECT_LT(simplex.size(), 4u);
}
TEST(GjkComprehensive, SimplexInfo_HitAlongY)
{
const auto [hit, simplex] = Gjk::is_collide_with_simplex_info(make_cube({ 0, 0, 0 }), make_cube({ 0, 1.5f, 0 }));
EXPECT_TRUE(hit);
EXPECT_EQ(simplex.size(), 4u);
}
TEST(GjkComprehensive, SimplexInfo_HitAlongZ)
{
const auto [hit, simplex] = Gjk::is_collide_with_simplex_info(make_cube({ 0, 0, 0 }), make_cube({ 0, 0, 1.5f }));
EXPECT_TRUE(hit);
EXPECT_EQ(simplex.size(), 4u);
}
TEST(GjkComprehensive, SimplexInfo_MissAlongDiagonal)
{
const auto [hit, simplex] = Gjk::is_collide_with_simplex_info(make_cube({ 0, 0, 0 }), make_cube({ 2.1f, 2.1f, 2.1f }));
EXPECT_FALSE(hit);
EXPECT_LT(simplex.size(), 4u);
}
// ---------------------------------------------------------------------------
// Non-trivial geometry — tetrahedron shaped colliders
// ---------------------------------------------------------------------------
TEST(GjkComprehensive, TetrahedronShapes_Overlapping)
{
// A rough tetrahedron mesh; two of them close enough to overlap
const std::vector<omath::primitives::Vertex<>> tet_vbo = {
{ { 0.f, 1.f, 0.f }, {}, {} },
{ { -1.f, -1.f, 1.f }, {}, {} },
{ { 1.f, -1.f, 1.f }, {}, {} },
{ { 0.f, -1.f, -1.f }, {}, {} },
};
Mesh m_a{ tet_vbo, k_empty_ebo };
Mesh m_b{ tet_vbo, k_empty_ebo };
m_b.set_origin({ 0.5f, 0.f, 0.f });
EXPECT_TRUE(Gjk::is_collide(Collider{ m_a }, Collider{ m_b }));
}
TEST(GjkComprehensive, TetrahedronShapes_Separated)
{
const std::vector<omath::primitives::Vertex<>> tet_vbo = {
{ { 0.f, 1.f, 0.f }, {}, {} },
{ { -1.f, -1.f, 1.f }, {}, {} },
{ { 1.f, -1.f, 1.f }, {}, {} },
{ { 0.f, -1.f, -1.f }, {}, {} },
};
Mesh m_a{ tet_vbo, k_empty_ebo };
Mesh m_b{ tet_vbo, k_empty_ebo };
m_b.set_origin({ 3.f, 0.f, 0.f });
EXPECT_FALSE(Gjk::is_collide(Collider{ m_a }, Collider{ m_b }));
}
// ---------------------------------------------------------------------------
// Determinism
// ---------------------------------------------------------------------------
TEST(GjkComprehensive, Deterministic_SameResultOnRepeatedCalls)
{
const auto a = make_cube({ 0, 0, 0 });
const auto b = make_cube({ 1.2f, 0.3f, 0.1f });
const bool first = Gjk::is_collide(a, b);
for (int i = 0; i < 10; ++i)
EXPECT_EQ(Gjk::is_collide(a, b), first);
}