Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions CMakePresets.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,15 @@
},
{
"name": "windows-debug-vcpkg",
"displayName": "Debug",
"displayName": "Windows Debug Vcpkg",
"inherits": "windows-base-vcpkg",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "windows-release-vcpkg",
"displayName": "Release",
"displayName": "Windows Release Vcpkg",
"inherits": "windows-base-vcpkg",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
Expand Down Expand Up @@ -157,7 +157,7 @@
},
{
"name": "darwin-debug-vcpkg",
"displayName": "Darwin Debug",
"displayName": "Darwin Debug Vcpkg",
"inherits": "darwin-base-vcpkg",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
Expand All @@ -173,7 +173,7 @@
},
{
"name": "darwin-release-vcpkg",
"displayName": "Darwin Release",
"displayName": "Darwin Release Vcpkg",
"inherits": "darwin-base-vcpkg",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
Expand Down
2 changes: 1 addition & 1 deletion include/omath/collision/collider_interface.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Created by Vladislav on 06.12.2025.
//
#pragma once

#include <omath/linear_algebra/vector3.hpp>

namespace omath::collision
{
Expand Down
33 changes: 19 additions & 14 deletions include/omath/collision/epa_algorithm.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,158 +50,163 @@
[[nodiscard]]
static std::optional<Result> solve(const ColliderInterfaceType& a, const ColliderInterfaceType& b,
const Simplex<VectorType>& simplex, const Params params = {},
std::shared_ptr<std::pmr::memory_resource> mem_resource = {
std::shared_ptr<void>{}, std::pmr::get_default_resource()})
std::pmr::memory_resource& mem_resource = *std::pmr::get_default_resource())
{
// --- Build initial polytope from simplex (4 points) ---
std::pmr::vector<VectorType> vertexes{mem_resource.get()};
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]);

// Initial tetra faces (windings corrected in make_face)
std::pmr::vector<Face> faces{mem_resource.get()};
std::pmr::vector<Face> faces{&mem_resource};
faces.reserve(4);
faces.emplace_back(make_face(vertexes, 0, 1, 2));
faces.emplace_back(make_face(vertexes, 0, 2, 3));
faces.emplace_back(make_face(vertexes, 0, 3, 1));
faces.emplace_back(make_face(vertexes, 1, 3, 2));

auto heap = rebuild_heap(faces);
auto heap = rebuild_heap(faces, mem_resource);

Result out{};

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);
heap = rebuild_heap(faces, mem_resource);

if (heap.empty())
break;

const int fidx = heap.top().idx;
const Face face = faces[fidx];

// Get the furthest point in face normal direction
const VectorType p = support_point(a, b, face.n);
const float p_dist = face.n.dot(p);

// Converged if we 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.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);

// Mark faces visible from p and collect their horizon
std::pmr::vector<bool> to_delete(faces.size(), false, mem_resource.get()); // uses single bits
std::pmr::vector<Edge> boundary{mem_resource.get()};
std::pmr::vector<bool> to_delete(faces.size(), false, &mem_resource); // uses single bits
std::pmr::vector<Edge> boundary{&mem_resource};
boundary.reserve(faces.size() * 2);

for (int i = 0; i < static_cast<int>(faces.size()); ++i)
{
if (to_delete[i])
continue;
if (visible_from(faces[i], p))
{
const auto& rf = faces[i];
to_delete[i] = true;
add_edge_boundary(boundary, rf.i0, rf.i1);
add_edge_boundary(boundary, rf.i1, rf.i2);
add_edge_boundary(boundary, rf.i2, rf.i0);
}
}

// Remove visible faces
std::pmr::vector<Face> new_faces{mem_resource.get()};
std::pmr::vector<Face> new_faces{&mem_resource};
new_faces.reserve(faces.size() + boundary.size());
for (int i = 0; i < static_cast<int>(faces.size()); ++i)
if (!to_delete[i])
new_faces.emplace_back(faces[i]);
faces.swap(new_faces);

// Stitch new faces around the horizon
for (const auto& e : boundary)
faces.emplace_back(make_face(vertexes, e.a, e.b, new_idx));

// Rebuild heap after topology change
heap = rebuild_heap(faces);
heap = rebuild_heap(faces, mem_resource);

if (!std::isfinite(vertexes.back().dot(vertexes.back())))
break; // safety
out.iterations = it + 1;
}

// Fallback: pick closest face as best-effort answer
if (!faces.empty())
{
auto best = faces[0];
for (const auto& f : faces)
if (f.d < best.d)
best = f;
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;
}
return std::nullopt;
}

private:
struct Face final
{
int i0, i1, i2;
VectorType n; // unit outward normal
float d; // n · v0 (>=0 ideally because origin is inside)
};

struct Edge final
{
int a, b;
};

struct HeapItem final
{
float d;
int idx;
};
struct HeapCmp final
{
[[nodiscard]]

Check notice on line 189 in include/omath/collision/epa_algorithm.hpp

View check run for this annotation

codefactor.io / CodeFactor

include/omath/collision/epa_algorithm.hpp#L53-L189

Complex Method
static bool operator()(const HeapItem& lhs, const HeapItem& rhs) noexcept
{
return lhs.d > rhs.d; // min-heap by distance
}
};
using Heap = std::priority_queue<HeapItem, std::vector<HeapItem>, HeapCmp>;

using Heap = std::priority_queue<HeapItem, std::pmr::vector<HeapItem>, HeapCmp>;

[[nodiscard]]
static Heap rebuild_heap(const std::pmr::vector<Face>& faces)
static Heap rebuild_heap(const std::pmr::vector<Face>& faces, auto& memory_resource)
{
Heap h;
std::pmr::vector<HeapItem> storage{ &memory_resource };
storage.reserve(faces.size()); // optional but recommended

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;

return h; // allocator is preserved
}

[[nodiscard]]
Expand Down
48 changes: 48 additions & 0 deletions include/omath/projection/camera.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#pragma once

#include "omath/linear_algebra/mat.hpp"
#include "omath/linear_algebra/triangle.hpp"
#include "omath/linear_algebra/vector3.hpp"
#include "omath/projection/error_codes.hpp"
#include <expected>
Expand Down Expand Up @@ -175,6 +176,53 @@ namespace omath::projection
std::unreachable();
}

[[nodiscard]] bool is_culled_by_frustum(const Triangle<Vector3<float>>& triangle) const noexcept
{
// Transform to clip space (before perspective divide)
auto to_clip = [this](const Vector3<float>& point)
{
auto clip = get_view_projection_matrix()
* mat_column_from_vector<float, Mat4X4Type::get_store_ordering()>(point);
return std::array<float, 4>{
clip.at(0, 0), // x
clip.at(1, 0), // y
clip.at(2, 0), // z
clip.at(3, 0) // w
};
};

const auto c0 = to_clip(triangle.m_vertex1);
const auto c1 = to_clip(triangle.m_vertex2);
const auto c2 = to_clip(triangle.m_vertex3);

// If all vertices are behind the camera (w <= 0), trivially reject
if (c0[3] <= 0.f && c1[3] <= 0.f && c2[3] <= 0.f)
return true;

// Helper: all three vertices outside the same clip plane
auto all_outside_plane = [](const int axis, const std::array<float, 4>& a, const std::array<float, 4>& b,
const std::array<float, 4>& c, const bool positive_side)
{
if (positive_side)
return a[axis] > a[3] && b[axis] > b[3] && c[axis] > c[3];
return a[axis] < -a[3] && b[axis] < -b[3] && c[axis] < -c[3];
};

// Clip volume in clip space (OpenGL-style):
// -w <= x <= w
// -w <= y <= w
// -w <= z <= w

for (int i = 0; i < 3; i++)
{
if (all_outside_plane(i, c0, c1, c2, false))
return true; // x < -w (left)
if (all_outside_plane(i, c0, c1, c2, true))
return true; // x > w (right)
}
return false;
}

[[nodiscard]] std::expected<Vector3<float>, Error>
world_to_view_port(const Vector3<float>& world_position) const noexcept
{
Expand Down
4 changes: 2 additions & 2 deletions tests/general/unit_test_epa.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ TEST(UnitTestEpa, TestCollisionTrue)
auto pool = std::make_shared<std::pmr::monotonic_buffer_resource>(1024);
params.max_iterations = 64;
params.tolerance = 1e-4f;
auto epa = EPA::solve(A, B, gjk.simplex, params, pool);
auto epa = EPA::solve(A, B, gjk.simplex, params, *pool);
ASSERT_TRUE(epa.has_value()) << "EPA should converge";

// Normal is unit
Expand Down Expand Up @@ -119,7 +119,7 @@ TEST(UnitTestEpa, TestCollisionTrue2)
params.max_iterations = 64;
params.tolerance = 1e-4f;
auto pool = std::make_shared<std::pmr::monotonic_buffer_resource>(1024);
auto epa = EPA::solve(A, B, gjk.simplex, params, pool);
auto epa = EPA::solve(A, B, gjk.simplex, params, *pool);
ASSERT_TRUE(epa.has_value()) << "EPA should converge";

// Normal is unit-length
Expand Down
Loading