Compare commits

...

47 Commits

Author SHA1 Message Date
3ccbb0b25b Merge pull request #161 from orange-cpp/feature/lua-bindings
Feature/lua bindings
2026-03-10 19:41:34 +03:00
68993db48a updated read me 2026-03-10 19:22:39 +03:00
337204b3bf fixed missing source 2026-03-10 19:09:29 +03:00
1e601d2d8f android fix 2026-03-10 19:01:54 +03:00
2b4a75d011 added lua status to build logs 2026-03-10 19:00:36 +03:00
99a30e8fdf added pattern scan to lua 2026-03-10 18:55:55 +03:00
0cdd1d021f added triangle to lua 2026-03-10 18:39:20 +03:00
a79ac9743a added badge, added triangle 2026-03-10 18:39:10 +03:00
f26afb703b added bigobj for msvc and mingw 2026-03-10 18:29:13 +03:00
f237ee5f6a removed useless headers 2026-03-10 18:15:54 +03:00
9058ea9b39 refactored to class 2026-03-10 18:14:29 +03:00
f707ac1adb fixed forward decl 2026-03-10 18:04:31 +03:00
cbdabd3fc2 removed useless header 2026-03-10 17:57:11 +03:00
30e3feb4f8 fix 2026-03-10 16:38:00 +03:00
0726fdef32 webasm fix 2026-03-10 16:31:49 +03:00
0ffe0c2bdc potential fix 2026-03-10 16:24:21 +03:00
e9600ad42b splited lua into multiple sources 2026-03-10 16:15:02 +03:00
673835618c restructurized stuff 2026-03-10 15:57:22 +03:00
afb2a13dd6 added color 2026-03-08 23:08:23 +03:00
943472cf64 migrated to sol2
decomposed method

added vector2, vector4

refactored tests

added opengl engine to lua

added other engines

added source tests

removed tons of lua files
2026-03-08 12:55:35 +03:00
9752accb14 Merge pull request #160 from orange-cpp/feaure/gjk-epa-improvement
Feaure/gjk epa improvement
2026-03-03 18:25:37 +03:00
7373e6d3df added std namspace to int64_t type 2026-03-03 10:00:46 +03:00
68f4c8cc72 added nodiscard 2026-03-03 09:38:05 +03:00
2dafc8a49d added additional method 2026-03-03 09:22:11 +03:00
11fe49e801 added const 2026-03-03 08:51:13 +03:00
dee705a391 improvement 2026-03-03 08:43:30 +03:00
bfe147ef80 Merge remote-tracking branch 'orange-cpp/feaure/gjk-epa-improvement' into feaure/gjk-epa-improvement 2026-03-03 08:27:50 +03:00
2c70288a8f added epa tests 2026-03-03 08:27:26 +03:00
529322fe34 decomposed method 2026-03-03 08:14:12 +03:00
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
6fca106edc Merge pull request #158 from orange-cpp/feature/quaternions
added files
2026-03-01 09:04:18 +03:00
78cb644920 added files 2026-03-01 08:23:26 +03:00
646a920e4c fixed potential deadlock 2026-02-27 08:47:46 +03:00
52687a70c7 fixed formating 2026-02-27 07:41:05 +03:00
a9eff7d320 Merge pull request #157 from orange-cpp/feature/mesh_improvement
Feature/mesh improvement
2026-02-26 16:39:21 +03:00
211e4c3d9b optimization 2026-02-26 16:19:54 +03:00
74dc2234f7 fixed collider when rotated 2026-02-26 16:17:41 +03:00
7ebbed6763 added funding
edit
2026-02-23 07:18:25 +03:00
e271bccaf5 added codeowners 2026-02-23 06:45:43 +03:00
45 changed files with 3853 additions and 208 deletions

4
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,4 @@
# These are supported funding model platforms
open_collective: libomathorg
github: orange-cpp

View File

@@ -107,7 +107,7 @@ jobs:
-DOMATH_BUILD_TESTS=ON \ -DOMATH_BUILD_TESTS=ON \
-DOMATH_BUILD_BENCHMARK=OFF \ -DOMATH_BUILD_BENCHMARK=OFF \
-DOMATH_ENABLE_COVERAGE=${{ matrix.coverage == true && 'ON' || 'OFF' }} \ -DOMATH_ENABLE_COVERAGE=${{ matrix.coverage == true && 'ON' || 'OFF' }} \
-DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests" -DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests;lua"
- name: Build - name: Build
shell: bash shell: bash
@@ -193,7 +193,7 @@ jobs:
-DOMATH_BUILD_TESTS=ON \ -DOMATH_BUILD_TESTS=ON \
-DOMATH_BUILD_BENCHMARK=OFF \ -DOMATH_BUILD_BENCHMARK=OFF \
-DOMATH_ENABLE_COVERAGE=OFF \ -DOMATH_ENABLE_COVERAGE=OFF \
-DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests" -DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests;lua"
- name: Build - name: Build
shell: bash shell: bash
@@ -234,7 +234,7 @@ jobs:
-DOMATH_ENABLE_COVERAGE=ON \ -DOMATH_ENABLE_COVERAGE=ON \
-DOMATH_THREAT_WARNING_AS_ERROR=OFF \ -DOMATH_THREAT_WARNING_AS_ERROR=OFF \
-DCMAKE_BUILD_TYPE=Debug \ -DCMAKE_BUILD_TYPE=Debug \
-DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests" -DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests;lua"
cmake --build cmake-build/build/${{ matrix.preset }} --config Debug --target unit_tests omath cmake --build cmake-build/build/${{ matrix.preset }} --config Debug --target unit_tests omath
- name: Run Tests (Generates .profraw) - name: Run Tests (Generates .profraw)
@@ -373,7 +373,7 @@ jobs:
-DOMATH_BUILD_TESTS=ON \ -DOMATH_BUILD_TESTS=ON \
-DOMATH_BUILD_BENCHMARK=OFF \ -DOMATH_BUILD_BENCHMARK=OFF \
-DOMATH_ENABLE_COVERAGE=${{ matrix.coverage == true && 'ON' || 'OFF' }} \ -DOMATH_ENABLE_COVERAGE=${{ matrix.coverage == true && 'ON' || 'OFF' }} \
-DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests" -DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests;lua"
- name: Build - name: Build
shell: bash shell: bash
@@ -450,7 +450,7 @@ jobs:
-DVCPKG_INSTALL_OPTIONS="--allow-unsupported" \ -DVCPKG_INSTALL_OPTIONS="--allow-unsupported" \
-DOMATH_BUILD_TESTS=ON \ -DOMATH_BUILD_TESTS=ON \
-DOMATH_BUILD_BENCHMARK=OFF \ -DOMATH_BUILD_BENCHMARK=OFF \
-DVCPKG_MANIFEST_FEATURES="imgui;tests" -DVCPKG_MANIFEST_FEATURES="imgui;tests;lua"
- name: Build - name: Build
shell: bash shell: bash
@@ -509,7 +509,7 @@ jobs:
cmake --preset ${{ matrix.preset }} \ cmake --preset ${{ matrix.preset }} \
-DOMATH_BUILD_TESTS=ON \ -DOMATH_BUILD_TESTS=ON \
-DOMATH_BUILD_BENCHMARK=OFF \ -DOMATH_BUILD_BENCHMARK=OFF \
-DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests" \ -DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests;lua" \
-DVCPKG_INSTALL_OPTIONS="--allow-unsupported" -DVCPKG_INSTALL_OPTIONS="--allow-unsupported"
cmake --build cmake-build/build/${{ matrix.preset }} --target unit_tests omath cmake --build cmake-build/build/${{ matrix.preset }} --target unit_tests omath
./out/Release/unit_tests ./out/Release/unit_tests
@@ -581,7 +581,7 @@ jobs:
-DVCPKG_INSTALL_OPTIONS="--allow-unsupported" \ -DVCPKG_INSTALL_OPTIONS="--allow-unsupported" \
-DOMATH_BUILD_TESTS=ON \ -DOMATH_BUILD_TESTS=ON \
-DOMATH_BUILD_BENCHMARK=OFF \ -DOMATH_BUILD_BENCHMARK=OFF \
-DVCPKG_MANIFEST_FEATURES="imgui;tests" -DVCPKG_MANIFEST_FEATURES="imgui;tests;lua"
- name: Build - name: Build
shell: bash shell: bash
@@ -650,7 +650,7 @@ jobs:
-DVCPKG_INSTALL_OPTIONS="--allow-unsupported" \ -DVCPKG_INSTALL_OPTIONS="--allow-unsupported" \
-DOMATH_BUILD_TESTS=ON \ -DOMATH_BUILD_TESTS=ON \
-DOMATH_BUILD_BENCHMARK=OFF \ -DOMATH_BUILD_BENCHMARK=OFF \
-DVCPKG_MANIFEST_FEATURES="imgui;tests" -DVCPKG_MANIFEST_FEATURES="imgui;tests;lua"
- name: Build - name: Build
shell: bash shell: bash
@@ -735,7 +735,7 @@ jobs:
-DVCPKG_INSTALL_OPTIONS="--allow-unsupported" \ -DVCPKG_INSTALL_OPTIONS="--allow-unsupported" \
-DOMATH_BUILD_TESTS=ON \ -DOMATH_BUILD_TESTS=ON \
-DOMATH_BUILD_BENCHMARK=OFF \ -DOMATH_BUILD_BENCHMARK=OFF \
-DVCPKG_MANIFEST_FEATURES="imgui;tests" -DVCPKG_MANIFEST_FEATURES="imgui;tests;lua"
- name: Build - name: Build
run: | run: |
@@ -800,7 +800,7 @@ jobs:
-DOMATH_BUILD_TESTS=ON \ -DOMATH_BUILD_TESTS=ON \
-DOMATH_BUILD_BENCHMARK=ON \ -DOMATH_BUILD_BENCHMARK=ON \
-DOMATH_ENABLE_VALGRIND=ON \ -DOMATH_ENABLE_VALGRIND=ON \
-DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests;benchmark" -DVCPKG_MANIFEST_FEATURES="imgui;avx2;lua;tests;benchmark"
- name: Build All Targets - name: Build All Targets
shell: bash shell: bash

5
.luarc.json Normal file
View File

@@ -0,0 +1,5 @@
{
"diagnostics.globals": [
"omath"
]
}

View File

@@ -31,6 +31,9 @@ option(OMATH_SUPRESS_SAFETY_CHECKS
option(OMATH_ENABLE_COVERAGE "Enable coverage" OFF) option(OMATH_ENABLE_COVERAGE "Enable coverage" OFF)
option(OMATH_ENABLE_FORCE_INLINE option(OMATH_ENABLE_FORCE_INLINE
"Will for compiler to make some functions to be force inlined no matter what" ON) "Will for compiler to make some functions to be force inlined no matter what" ON)
option(OMATH_ENABLE_LUA
"omath bindings for lua" OFF)
if(VCPKG_MANIFEST_FEATURES) if(VCPKG_MANIFEST_FEATURES)
foreach(omath_feature IN LISTS VCPKG_MANIFEST_FEATURES) foreach(omath_feature IN LISTS VCPKG_MANIFEST_FEATURES)
if(omath_feature STREQUAL "imgui") if(omath_feature STREQUAL "imgui")
@@ -43,6 +46,8 @@ if(VCPKG_MANIFEST_FEATURES)
set(OMATH_BUILD_BENCHMARK ON) set(OMATH_BUILD_BENCHMARK ON)
elseif(omath_feature STREQUAL "examples") elseif(omath_feature STREQUAL "examples")
set(OMATH_BUILD_EXAMPLES ON) set(OMATH_BUILD_EXAMPLES ON)
elseif(omath_feature STREQUAL "lua")
set(OMATH_ENABLE_LUA ON)
endif() endif()
endforeach() endforeach()
@@ -72,6 +77,7 @@ if(${PROJECT_IS_TOP_LEVEL})
message(STATUS "[${PROJECT_NAME}]: Building using vcpkg ${OMATH_BUILD_VIA_VCPKG}") message(STATUS "[${PROJECT_NAME}]: Building using vcpkg ${OMATH_BUILD_VIA_VCPKG}")
message(STATUS "[${PROJECT_NAME}]: Coverage feature status ${OMATH_ENABLE_COVERAGE}") message(STATUS "[${PROJECT_NAME}]: Coverage feature status ${OMATH_ENABLE_COVERAGE}")
message(STATUS "[${PROJECT_NAME}]: Valgrind feature status ${OMATH_ENABLE_VALGRIND}") message(STATUS "[${PROJECT_NAME}]: Valgrind feature status ${OMATH_ENABLE_VALGRIND}")
message(STATUS "[${PROJECT_NAME}]: Lua feature status ${OMATH_ENABLE_LUA}")
endif() endif()
file(GLOB_RECURSE OMATH_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/source/*.cpp") file(GLOB_RECURSE OMATH_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/source/*.cpp")
@@ -83,6 +89,17 @@ else()
add_library(${PROJECT_NAME} STATIC ${OMATH_SOURCES} ${OMATH_HEADERS}) add_library(${PROJECT_NAME} STATIC ${OMATH_SOURCES} ${OMATH_HEADERS})
endif() endif()
if (OMATH_ENABLE_LUA)
target_compile_definitions(${PROJECT_NAME} PUBLIC OMATH_ENABLE_LUA)
find_package(Lua REQUIRED)
target_include_directories(${PROJECT_NAME} PRIVATE ${LUA_INCLUDE_DIR})
target_link_libraries(${PROJECT_NAME} PRIVATE ${LUA_LIBRARIES})
find_path(SOL2_INCLUDE_DIRS "sol/abort.hpp")
target_include_directories(${PROJECT_NAME} PRIVATE ${SOL2_INCLUDE_DIRS})
endif ()
add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME})
target_compile_definitions(${PROJECT_NAME} PUBLIC OMATH_VERSION="${PROJECT_VERSION}") target_compile_definitions(${PROJECT_NAME} PUBLIC OMATH_VERSION="${PROJECT_VERSION}")
@@ -174,6 +191,12 @@ elseif(OMATH_THREAT_WARNING_AS_ERROR)
target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic -Werror) target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic -Werror)
endif() endif()
if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
target_compile_options(${PROJECT_NAME} PRIVATE /bigobj)
endif()
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_HOST_SYSTEM_NAME EQUAL "Windows")
target_compile_options(${PROJECT_NAME} PRIVATE -mbig-obj)
endif()
# Windows SDK redefine min/max via preprocessor and break std::min and std::max # Windows SDK redefine min/max via preprocessor and break std::min and std::max
if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
target_compile_definitions(${PROJECT_NAME} INTERFACE NOMINMAX) target_compile_definitions(${PROJECT_NAME} INTERFACE NOMINMAX)

View File

@@ -145,7 +145,7 @@
"hidden": true, "hidden": true,
"inherits": ["linux-base", "vcpkg-base"], "inherits": ["linux-base", "vcpkg-base"],
"cacheVariables": { "cacheVariables": {
"VCPKG_MANIFEST_FEATURES": "tests;imgui;avx2" "VCPKG_MANIFEST_FEATURES": "tests;imgui;avx2;lua"
} }
}, },
{ {
@@ -235,7 +235,7 @@
"hidden": true, "hidden": true,
"inherits": ["darwin-base", "vcpkg-base"], "inherits": ["darwin-base", "vcpkg-base"],
"cacheVariables": { "cacheVariables": {
"VCPKG_MANIFEST_FEATURES": "tests;imgui;avx2;examples" "VCPKG_MANIFEST_FEATURES": "tests;imgui;avx2;examples;lua"
} }
}, },
{ {

37
CODEOWNERS Normal file
View File

@@ -0,0 +1,37 @@
## List of maintainers for the omath library
## This file purpose is to give newcomers to the project the responsible
## developers when submitting a pull request on GitHub, or opening a bug
## report in issues.
## This file will notably establish who is responsible for a specific
## area of omath. Being a maintainer means the following:
## - that person has good knownledge in the area
## - that person is able to enforce consistency in the area
## - that person may be available for giving help in the area
## - that person has push access on the repository
## Being a maintainer does not mean the following:
## - that person is dedicated to the area
## - that person is working full-time on the area/on omath
## - that person is paid
## - that person is always available
# omath core source code
/source @orange-cpp
/include @orange-cpp
# Tests and becnchmarks
/benchmark @orange-cpp
/tests @orange-cpp @luadebug
# Examples and documentation
/examples @luadebug @orange-cpp
/docs @orange-cpp
# Misc like formating
/scripts @luadebug
/pixi @luadebug
# CI/CD
/.github/workflows @luadbg @orange-cpp

View File

@@ -9,6 +9,7 @@
[![CodeFactor](https://www.codefactor.io/repository/github/orange-cpp/omath/badge)](https://www.codefactor.io/repository/github/orange-cpp/omath) [![CodeFactor](https://www.codefactor.io/repository/github/orange-cpp/omath/badge)](https://www.codefactor.io/repository/github/orange-cpp/omath)
![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/orange-cpp/omath/cmake-multi-platform.yml) ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/orange-cpp/omath/cmake-multi-platform.yml)
[![Vcpkg package](https://repology.org/badge/version-for-repo/vcpkg/orange-math.svg)](https://repology.org/project/orange-math/versions) [![Vcpkg package](https://repology.org/badge/version-for-repo/vcpkg/orange-math.svg)](https://repology.org/project/orange-math/versions)
![Conan Center](https://img.shields.io/conan/v/omath)
![GitHub forks](https://img.shields.io/github/forks/orange-cpp/omath) ![GitHub forks](https://img.shields.io/github/forks/orange-cpp/omath)
[![discord badge](https://dcbadge.limes.pink/api/server/https://discord.gg/eDgdaWbqwZ?style=flat)](https://discord.gg/eDgdaWbqwZ) [![discord badge](https://dcbadge.limes.pink/api/server/https://discord.gg/eDgdaWbqwZ?style=flat)](https://discord.gg/eDgdaWbqwZ)
[![telegram badge](https://img.shields.io/badge/Telegram-2CA5E0?style=flat-squeare&logo=telegram&logoColor=white)](https://t.me/orangennotes) [![telegram badge](https://img.shields.io/badge/Telegram-2CA5E0?style=flat-squeare&logo=telegram&logoColor=white)](https://t.me/orangennotes)
@@ -83,6 +84,7 @@ if (auto screen = camera.world_to_screen(world_position)) {
- **Engine support**: Supports coordinate systems of **Source, Unity, Unreal, Frostbite, IWEngine, CryEngine and canonical OpenGL**. - **Engine support**: Supports coordinate systems of **Source, Unity, Unreal, Frostbite, IWEngine, CryEngine and canonical OpenGL**.
- **Cross platform**: Supports Windows, MacOS and Linux. - **Cross platform**: Supports Windows, MacOS and Linux.
- **Algorithms**: Has ability to scan for byte pattern with wildcards in ELF/Mach-O/PE files/modules, binary slices, works even with Wine apps. - **Algorithms**: Has ability to scan for byte pattern with wildcards in ELF/Mach-O/PE files/modules, binary slices, works even with Wine apps.
- **Scripting**: Supports to make scripts in Lua out of box
- **Battle tested**: It's already used by some big players on the market like wraith.su and bluedream.ltd - **Battle tested**: It's already used by some big players on the market like wraith.su and bluedream.ltd
<div align = center> <div align = center>

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

@@ -8,6 +8,7 @@
#include <memory> #include <memory>
#include <memory_resource> #include <memory_resource>
#include <queue> #include <queue>
#include <unordered_map>
#include <utility> #include <utility>
#include <vector> #include <vector>
@@ -56,83 +57,76 @@ namespace omath::collision
const Simplex<VectorType>& simplex, const Params params = {}, const Simplex<VectorType>& simplex, const Params params = {},
std::pmr::memory_resource& mem_resource = *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 = build_initial_polytope_from_simplex(simplex, mem_resource); 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); 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{}; Result out{};
// Hoisted outside the loop to reuse bucket allocation across iterations.
// Initial bucket count 16 covers a typical horizon without rehashing.
BoundaryMap boundary{16, &mem_resource};
for (int it = 0; it < params.max_iterations; ++it) for (int it = 0; it < params.max_iterations; ++it)
{ {
// If heap might be stale after face edits, rebuild lazily. // Lazily discard stale (deleted or index-mismatched) heap entries.
if (heap.empty()) discard_stale_heap_entries(faces, heap);
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);
if (heap.empty()) if (heap.empty())
break; break;
// FIXME: STORE REF VALUE, DO NOT USE
// AFTER IF STATEMENT BLOCK
const Face& face = faces[heap.top().idx]; 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 VectorType p = support_point(a, b, face.n);
const auto p_dist = face.n.dot(p); 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) if (p_dist - face.d <= params.tolerance)
{ {
out.normal = face.n; out.normal = face.n;
out.depth = face.d; // along unit normal out.depth = face.d;
out.iterations = it + 1; out.iterations = it + 1;
out.num_vertices = static_cast<int>(vertexes.size()); out.num_vertices = static_cast<int>(vertexes.size());
out.num_faces = static_cast<int>(faces.size()); out.num_faces = static_cast<int>(faces.size());
out.penetration_vector = out.normal * out.depth; out.penetration_vector = out.normal * out.depth;
return out; return out;
} }
// Add new vertex
const int new_idx = static_cast<int>(vertexes.size()); const int new_idx = static_cast<int>(vertexes.size());
vertexes.emplace_back(p); 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.
tombstone_visible_faces(faces, boundary, p);
erase_marked(faces, to_delete); // Stitch new faces around the horizon and push them directly onto the
// heap — no full O(n log n) rebuild needed.
// Stitch new faces around the horizon for (const auto& [key, e] : boundary)
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)); faces.emplace_back(make_face(vertexes, e.a, e.b, new_idx));
heap.emplace(faces.back().d, fi);
// Rebuild heap after topology change }
heap = rebuild_heap(faces, mem_resource);
if (!std::isfinite(vertexes.back().dot(vertexes.back()))) if (!std::isfinite(vertexes.back().dot(vertexes.back())))
break; // safety break; // safety
out.iterations = it + 1; out.iterations = it + 1;
} }
if (faces.empty()) // Find the best surviving (non-deleted) face.
const Face* best = find_best_surviving_face(faces);
if (!best)
return std::nullopt; return std::nullopt;
const auto best = *std::ranges::min_element(faces, [](const auto& first, const auto& second) out.normal = best->n;
{ return first.d < second.d; }); out.depth = best->d;
out.normal = best.n;
out.depth = best.d;
out.num_vertices = static_cast<int>(vertexes.size()); out.num_vertices = static_cast<int>(vertexes.size());
out.num_faces = static_cast<int>(faces.size()); out.num_faces = static_cast<int>(faces.size());
out.penetration_vector = out.normal * out.depth; out.penetration_vector = out.normal * out.depth;
return out; return out;
} }
@@ -141,7 +135,8 @@ namespace omath::collision
{ {
int i0, i1, i2; int i0, i1, i2;
VectorType n; // unit outward normal VectorType n; // unit outward normal
FloatingType d; // n · v0 (>=0 ideally because origin is inside) FloatingType d; // n · v0 (>= 0 ideally because origin is inside)
bool deleted{false}; // tombstone flag — avoids O(n) compaction per iteration
}; };
struct Edge final struct Edge final
@@ -154,6 +149,7 @@ namespace omath::collision
FloatingType d; FloatingType d;
int idx; int idx;
}; };
struct HeapCmp final struct HeapCmp final
{ {
[[nodiscard]] [[nodiscard]]
@@ -165,35 +161,44 @@ namespace omath::collision
using Heap = std::priority_queue<HeapItem, std::pmr::vector<HeapItem>, HeapCmp>; using Heap = std::priority_queue<HeapItem, std::pmr::vector<HeapItem>, HeapCmp>;
// Horizon boundary: maps packed(a,b) → Edge.
// Opposite edges cancel in O(1) via hash lookup instead of O(h) linear scan.
using BoundaryMap = std::pmr::unordered_map<std::int64_t, Edge>;
[[nodiscard]]
static constexpr std::int64_t pack_edge(const int a, const int b) noexcept
{
return (static_cast<std::int64_t>(a) << 32) | static_cast<std::uint32_t>(b);
}
[[nodiscard]] [[nodiscard]]
static Heap rebuild_heap(const std::pmr::vector<Face>& faces, auto& memory_resource) static Heap rebuild_heap(const std::pmr::vector<Face>& faces, auto& memory_resource)
{ {
std::pmr::vector<HeapItem> storage{&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)}; Heap h{HeapCmp{}, std::move(storage)};
for (int i = 0; i < static_cast<int>(faces.size()); ++i) for (int i = 0; i < static_cast<int>(faces.size()); ++i)
h.emplace(faces[i].d, i); if (!faces[i].deleted)
h.emplace(faces[i].d, i);
return h; // allocator is preserved return h;
} }
[[nodiscard]] [[nodiscard]]
static bool visible_from(const Face& f, const VectorType& p) 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); 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) static void add_edge_boundary(BoundaryMap& boundary, int a, int b)
{ {
// Keep edges that appear only once; erase if opposite already present // O(1) cancel: if the opposite edge (b→a) is already in the map it is an
auto itb = std::ranges::find_if(boundary, [&](const Edge& e) { return e.a == b && e.b == a; }); // internal edge shared by two visible faces and must be removed.
if (itb != boundary.end()) // Otherwise this is a horizon edge and we insert it.
boundary.erase(itb); // internal edge cancels out const std::int64_t rev = pack_edge(b, a);
if (const auto it = boundary.find(rev); it != boundary.end())
boundary.erase(it);
else else
boundary.emplace_back(a, b); // horizon edge (directed) boundary.emplace(pack_edge(a, b), Edge{a, b});
} }
[[nodiscard]] [[nodiscard]]
@@ -204,9 +209,7 @@ namespace omath::collision
const VectorType& a2 = vertexes[i2]; const VectorType& a2 = vertexes[i2];
VectorType n = (a1 - a0).cross(a2 - a0); VectorType n = (a1 - a0).cross(a2 - a0);
if (n.dot(n) <= static_cast<FloatingType>(1e-30)) if (n.dot(n) <= static_cast<FloatingType>(1e-30))
{
n = any_perp_vec(a1 - a0); // degenerate guard n = any_perp_vec(a1 - a0); // degenerate guard
}
// Ensure normal points outward (away from origin): require n·a0 >= 0 // Ensure normal points outward (away from origin): require n·a0 >= 0
if (n.dot(a0) < static_cast<FloatingType>(0.0)) if (n.dot(a0) < static_cast<FloatingType>(0.0))
{ {
@@ -243,6 +246,7 @@ namespace omath::collision
return d; return d;
return V{1, 0, 0}; return V{1, 0, 0};
} }
[[nodiscard]] [[nodiscard]]
static std::pmr::vector<Face> create_initial_tetra_faces(std::pmr::memory_resource& mem_resource, static std::pmr::vector<Face> create_initial_tetra_faces(std::pmr::memory_resource& mem_resource,
const std::pmr::vector<VectorType>& vertexes) const std::pmr::vector<VectorType>& vertexes)
@@ -262,48 +266,45 @@ namespace omath::collision
{ {
std::pmr::vector<VectorType> vertexes{&mem_resource}; std::pmr::vector<VectorType> vertexes{&mem_resource};
vertexes.reserve(simplex.size()); vertexes.reserve(simplex.size());
for (std::size_t i = 0; i < simplex.size(); ++i) for (std::size_t i = 0; i < simplex.size(); ++i)
vertexes.emplace_back(simplex[i]); vertexes.emplace_back(simplex[i]);
return vertexes; return vertexes;
} }
static void erase_marked(std::pmr::vector<Face>& faces, const std::pmr::vector<bool>& to_delete)
static const Face* find_best_surviving_face(const std::pmr::vector<Face>& faces)
{ {
auto* mr = faces.get_allocator().resource(); // keep same resource const Face* best = nullptr;
std::pmr::vector<Face> kept{mr}; for (const auto& f : faces)
kept.reserve(faces.size()); if (!f.deleted && (best == nullptr || f.d < best->d))
best = &f;
for (std::size_t i = 0; i < faces.size(); ++i) return best;
if (!to_delete[i])
kept.emplace_back(faces[i]);
faces.swap(kept);
} }
struct Horizon static void tombstone_visible_faces(std::pmr::vector<Face>& faces, BoundaryMap& boundary,
const VectorType& p)
{ {
std::pmr::vector<bool> to_delete; boundary.clear();
std::pmr::vector<Edge> boundary; for (auto& f : faces)
}; {
if (!f.deleted && visible_from(f, p))
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]; f.deleted = true;
horizon.to_delete[i] = true; add_edge_boundary(boundary, f.i0, f.i1);
add_edge_boundary(horizon.boundary, rf.i0, rf.i1); add_edge_boundary(boundary, f.i1, f.i2);
add_edge_boundary(horizon.boundary, rf.i1, rf.i2); add_edge_boundary(boundary, f.i2, f.i0);
add_edge_boundary(horizon.boundary, rf.i2, rf.i0);
} }
}
}
return horizon; static void discard_stale_heap_entries(const std::pmr::vector<Face>& faces,
std::priority_queue<HeapItem, std::pmr::vector<HeapItem>, HeapCmp>& heap)
{
while (!heap.empty())
{
const auto& top = heap.top();
if (!faces[top.idx].deleted && faces[top.idx].d == top.d)
break;
heap.pop();
}
} }
}; };
} // namespace omath::collision } // namespace omath::collision

View File

@@ -14,11 +14,15 @@ namespace omath::collision
Simplex<VertexType> simplex; // valid only if hit == true and size==4 Simplex<VertexType> simplex; // valid only if hit == true and size==4
}; };
struct GjkSettings final
{
float epsilon = 1e-6f;
std::size_t max_iterations = 64;
};
template<class ColliderInterfaceType> template<class ColliderInterfaceType>
class GjkAlgorithm final class GjkAlgorithm final
{ {
using VectorType = ColliderInterfaceType::VectorType; using VectorType = ColliderInterfaceType::VectorType;
public: public:
[[nodiscard]] [[nodiscard]]
static VectorType find_support_vertex(const ColliderInterfaceType& collider_a, static VectorType find_support_vertex(const ColliderInterfaceType& collider_a,
@@ -36,20 +40,34 @@ namespace omath::collision
[[nodiscard]] [[nodiscard]]
static GjkHitInfo<VectorType> is_collide_with_simplex_info(const ColliderInterfaceType& collider_a, static GjkHitInfo<VectorType> is_collide_with_simplex_info(const ColliderInterfaceType& collider_a,
const ColliderInterfaceType& collider_b) 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<VectorType> simplex;
simplex.push_front(support); simplex.push_front(support);
auto direction = -support; auto direction = -support;
while (true) for (std::size_t iteration = 0; iteration < settings.max_iterations; ++iteration)
{ {
support = find_support_vertex(collider_a, collider_b, direction); support = find_support_vertex(collider_a, collider_b, direction);
if (support.dot(direction) <= 0.f) if (support.dot(direction) <= settings.epsilon)
return {false, simplex}; return {false, simplex};
simplex.push_front(support); simplex.push_front(support);
@@ -57,6 +75,7 @@ namespace omath::collision
if (simplex.handle(direction)) if (simplex.handle(direction))
return {true, simplex}; return {true, simplex};
} }
return {false, simplex};
} }
}; };
} // namespace omath::collision } // namespace omath::collision

View File

@@ -42,13 +42,40 @@ namespace omath::collision
m_mesh.set_origin(new_origin); m_mesh.set_origin(new_origin);
} }
[[nodiscard]]
const MeshType& get_mesh() const
{
return m_mesh;
}
[[nodiscard]]
MeshType& get_mesh()
{
return m_mesh;
}
private: private:
[[nodiscard]] [[nodiscard]]
const VertexType& find_furthest_vertex(const VectorType& direction) const const VertexType& find_furthest_vertex(const VectorType& direction) const
{ {
return *std::ranges::max_element( // The support query arrives in world space, but vertex positions are stored
m_mesh.m_vertex_buffer, [&direction](const auto& first, const auto& second) // in local space. We need argmax_v { world(v) · d }.
{ return first.position.dot(direction) < second.position.dot(direction); }); //
// world(v) = M·v (ignoring translation, which is constant across vertices)
// world(v) · d = v · Mᵀ·d
//
// So we transform the direction to local space once — O(1) — then compare
// raw local positions, which is far cheaper than calling
// vertex_position_to_world_space (full 4×4 multiply) for every vertex.
//
// d_local = upper-left 3×3 of M, transposed, times d_world:
// d_local[j] = sum_i M.at(i,j) * d[i] (i.e. column j of M dotted with d)
const auto& m = m_mesh.get_to_world_matrix();
const VectorType d_local = {
m[0, 0] * direction.x + m[1, 0] * direction.y + m[2, 0] * direction.z,
m[0, 1] * direction.x + m[1, 1] * direction.y + m[2, 1] * direction.z,
m[0, 2] * direction.x + m[1, 2] * direction.y + m[2, 2] * direction.z,
};
return *std::ranges::max_element(m_mesh.m_vertex_buffer, [&d_local](const auto& first, const auto& second)
{ return first.position.dot(d_local) < second.position.dot(d_local); });
} }
MeshType m_mesh; MeshType m_mesh;
}; };

View File

@@ -0,0 +1,219 @@
//
// Created by vlad on 3/1/2026.
//
#pragma once
#include "omath/linear_algebra/mat.hpp"
#include "omath/linear_algebra/vector3.hpp"
#include <array>
#include <cmath>
#include <format>
namespace omath
{
template<class Type>
requires std::is_arithmetic_v<Type>
class Quaternion
{
public:
using ContainedType = Type;
Type x = static_cast<Type>(0);
Type y = static_cast<Type>(0);
Type z = static_cast<Type>(0);
Type w = static_cast<Type>(1); // identity quaternion
constexpr Quaternion() noexcept = default;
constexpr Quaternion(const Type& x, const Type& y, const Type& z, const Type& w) noexcept
: x(x), y(y), z(z), w(w)
{
}
// Factory: build from a normalized axis and an angle in radians
[[nodiscard]]
static Quaternion from_axis_angle(const Vector3<Type>& axis, const Type& angle_rad) noexcept
{
const Type half = angle_rad / static_cast<Type>(2);
const Type s = std::sin(half);
return {axis.x * s, axis.y * s, axis.z * s, std::cos(half)};
}
[[nodiscard]] constexpr bool operator==(const Quaternion& other) const noexcept
{
return x == other.x && y == other.y && z == other.z && w == other.w;
}
[[nodiscard]] constexpr bool operator!=(const Quaternion& other) const noexcept
{
return !(*this == other);
}
// Hamilton product: this * other
[[nodiscard]] constexpr Quaternion operator*(const Quaternion& other) const noexcept
{
return {
w * other.x + x * other.w + y * other.z - z * other.y,
w * other.y - x * other.z + y * other.w + z * other.x,
w * other.z + x * other.y - y * other.x + z * other.w,
w * other.w - x * other.x - y * other.y - z * other.z,
};
}
constexpr Quaternion& operator*=(const Quaternion& other) noexcept
{
return *this = *this * other;
}
[[nodiscard]] constexpr Quaternion operator*(const Type& scalar) const noexcept
{
return {x * scalar, y * scalar, z * scalar, w * scalar};
}
constexpr Quaternion& operator*=(const Type& scalar) noexcept
{
x *= scalar;
y *= scalar;
z *= scalar;
w *= scalar;
return *this;
}
[[nodiscard]] constexpr Quaternion operator+(const Quaternion& other) const noexcept
{
return {x + other.x, y + other.y, z + other.z, w + other.w};
}
constexpr Quaternion& operator+=(const Quaternion& other) noexcept
{
x += other.x;
y += other.y;
z += other.z;
w += other.w;
return *this;
}
[[nodiscard]] constexpr Quaternion operator-() const noexcept
{
return {-x, -y, -z, -w};
}
// Conjugate: negates the vector part (x, y, z)
[[nodiscard]] constexpr Quaternion conjugate() const noexcept
{
return {-x, -y, -z, w};
}
[[nodiscard]] constexpr Type dot(const Quaternion& other) const noexcept
{
return x * other.x + y * other.y + z * other.z + w * other.w;
}
[[nodiscard]] constexpr Type length_sqr() const noexcept
{
return x * x + y * y + z * z + w * w;
}
#ifndef _MSC_VER
[[nodiscard]] constexpr Type length() const noexcept
{
return std::sqrt(length_sqr());
}
[[nodiscard]] constexpr Quaternion normalized() const noexcept
{
const Type len = length();
return len != static_cast<Type>(0) ? *this * (static_cast<Type>(1) / len) : *this;
}
#else
[[nodiscard]] Type length() const noexcept
{
return std::sqrt(length_sqr());
}
[[nodiscard]] Quaternion normalized() const noexcept
{
const Type len = length();
return len != static_cast<Type>(0) ? *this * (static_cast<Type>(1) / len) : *this;
}
#endif
// Inverse: q* / |q|^2 (for unit quaternions inverse == conjugate)
[[nodiscard]] constexpr Quaternion inverse() const noexcept
{
return conjugate() * (static_cast<Type>(1) / length_sqr());
}
// Rotate a 3D vector: v' = q * pure(v) * q^-1
// Computed via Rodrigues' formula to avoid full quaternion product overhead
[[nodiscard]] constexpr Vector3<Type> rotate(const Vector3<Type>& v) const noexcept
{
const Vector3<Type> q_vec{x, y, z};
const Vector3<Type> cross = q_vec.cross(v);
return v + cross * (static_cast<Type>(2) * w) + q_vec.cross(cross) * static_cast<Type>(2);
}
// 3x3 rotation matrix from this (unit) quaternion
[[nodiscard]] constexpr Mat<3, 3, Type> to_rotation_matrix3() const noexcept
{
const Type xx = x * x, yy = y * y, zz = z * z;
const Type xy = x * y, xz = x * z, yz = y * z;
const Type wx = w * x, wy = w * y, wz = w * z;
const Type one = static_cast<Type>(1);
const Type two = static_cast<Type>(2);
return {
{one - two * (yy + zz), two * (xy - wz), two * (xz + wy) },
{two * (xy + wz), one - two * (xx + zz), two * (yz - wx) },
{two * (xz - wy), two * (yz + wx), one - two * (xx + yy)},
};
}
// 4x4 rotation matrix (with homogeneous row/column)
[[nodiscard]] constexpr Mat<4, 4, Type> to_rotation_matrix4() const noexcept
{
const Type xx = x * x, yy = y * y, zz = z * z;
const Type xy = x * y, xz = x * z, yz = y * z;
const Type wx = w * x, wy = w * y, wz = w * z;
const Type one = static_cast<Type>(1);
const Type two = static_cast<Type>(2);
const Type zero = static_cast<Type>(0);
return {
{one - two * (yy + zz), two * (xy - wz), two * (xz + wy), zero},
{two * (xy + wz), one - two * (xx + zz), two * (yz - wx), zero},
{two * (xz - wy), two * (yz + wx), one - two * (xx + yy), zero},
{zero, zero, zero, one },
};
}
[[nodiscard]] constexpr std::array<Type, 4> as_array() const noexcept
{
return {x, y, z, w};
}
};
} // namespace omath
template<class Type>
struct std::formatter<omath::Quaternion<Type>> // NOLINT(*-dcl58-cpp)
{
[[nodiscard]]
static constexpr auto parse(std::format_parse_context& ctx)
{
return ctx.begin();
}
template<class FormatContext>
[[nodiscard]]
static auto format(const omath::Quaternion<Type>& q, FormatContext& ctx)
{
if constexpr (std::is_same_v<typename FormatContext::char_type, char>)
return std::format_to(ctx.out(), "[{}, {}, {}, {}]", q.x, q.y, q.z, q.w);
if constexpr (std::is_same_v<typename FormatContext::char_type, wchar_t>)
return std::format_to(ctx.out(), L"[{}, {}, {}, {}]", q.x, q.y, q.z, q.w);
if constexpr (std::is_same_v<typename FormatContext::char_type, char8_t>)
return std::format_to(ctx.out(), u8"[{}, {}, {}, {}]", q.x, q.y, q.z, q.w);
}
};

25
include/omath/lua/lua.hpp Normal file
View File

@@ -0,0 +1,25 @@
//
// Created by orange on 07.03.2026.
//
#pragma once
#ifdef OMATH_ENABLE_LUA
#include <sol/forward.hpp>
namespace omath::lua
{
class LuaInterpreter final
{
public:
static void register_lib(lua_State* lua_state);
private:
static void register_vec2(sol::table& omath_table);
static void register_vec3(sol::table& omath_table);
static void register_vec4(sol::table& omath_table);
static void register_color(sol::table& omath_table);
static void register_triangle(sol::table& omath_table);
static void register_shared_types(sol::table& omath_table);
static void register_engines(sol::table& omath_table);
static void register_pattern_scan(sol::table& omath_table);
};
}
#endif

View File

@@ -17,6 +17,9 @@
// Matrix classes // Matrix classes
#include "omath/linear_algebra/mat.hpp" #include "omath/linear_algebra/mat.hpp"
// Quaternion
#include "omath/linear_algebra/quaternion.hpp"
// Color functionality // Color functionality
#include "omath/utility/color.hpp" #include "omath/utility/color.hpp"

View File

@@ -16,19 +16,28 @@ namespace omath
float value{}; float value{};
}; };
class Color final : public Vector4<float> class Color final
{ {
Vector4<float> m_value;
public: 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; constexpr explicit Color() noexcept = default;
[[nodiscard]] [[nodiscard]]
constexpr static Color from_rgba(const uint8_t r, const uint8_t g, const uint8_t b, const uint8_t a) noexcept 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]] [[nodiscard]]
@@ -82,9 +91,9 @@ namespace omath
{ {
Hsv hsv_data; Hsv hsv_data;
const float& red = x; const float& red = m_value.x;
const float& green = y; const float& green = m_value.y;
const float& blue = z; const float& blue = m_value.z;
const float max = std::max({red, green, blue}); const float max = std::max({red, green, blue});
const float min = std::min({red, green, blue}); const float min = std::min({red, green, blue});
@@ -109,11 +118,6 @@ namespace omath
return hsv_data; 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 constexpr void set_hue(const float hue) noexcept
{ {
auto hsv = to_hsv(); auto hsv = to_hsv();
@@ -141,7 +145,7 @@ namespace omath
constexpr Color blend(const Color& other, float ratio) const noexcept constexpr Color blend(const Color& other, float ratio) const noexcept
{ {
ratio = std::clamp(ratio, 0.f, 1.f); 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() [[nodiscard]] static constexpr Color red()
@@ -160,16 +164,26 @@ namespace omath
[[nodiscard]] [[nodiscard]]
ImColor to_im_color() const noexcept ImColor to_im_color() const noexcept
{ {
return {to_im_vec4()}; return {m_value.to_im_vec4()};
} }
#endif #endif
[[nodiscard]] std::string to_string() const noexcept [[nodiscard]] std::string to_string() const noexcept
{ {
return std::format("[r:{}, g:{}, b:{}, a:{}]", return std::format("[r:{}, g:{}, b:{}, a:{}]",
static_cast<int>(x * 255.f), static_cast<int>(m_value.x * 255.f),
static_cast<int>(y * 255.f), static_cast<int>(m_value.y * 255.f),
static_cast<int>(z * 255.f), static_cast<int>(m_value.z * 255.f),
static_cast<int>(w * 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 [[nodiscard]] std::wstring to_wstring() const noexcept
{ {
@@ -188,23 +202,55 @@ namespace omath
template<> template<>
struct std::formatter<omath::Color> // NOLINT(*-dcl58-cpp) struct std::formatter<omath::Color> // NOLINT(*-dcl58-cpp)
{ {
[[nodiscard]] enum class ColorFormat { rgb, rgbf, hsv };
static constexpr auto parse(const std::format_parse_context& ctx) 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> template<class FormatContext>
[[nodiscard]] auto format(const omath::Color& col, FormatContext& ctx) const
static auto format(const omath::Color& col, FormatContext& ctx)
{ {
if constexpr (std::is_same_v<typename FormatContext::char_type, char>) std::string str;
return std::format_to(ctx.out(), "{}", col.to_string()); switch (color_format)
if constexpr (std::is_same_v<typename FormatContext::char_type, wchar_t>) {
return std::format_to(ctx.out(), L"{}", col.to_wstring()); 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>) 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(); std::unreachable();
} }

27
source/lua/lua.cpp Normal file
View File

@@ -0,0 +1,27 @@
//
// Created by orange on 07.03.2026.
//
#ifdef OMATH_ENABLE_LUA
#include "lua.hpp"
#include <sol/sol.hpp>
#include "omath/lua/lua.hpp"
namespace omath::lua
{
void LuaInterpreter::register_lib(lua_State* lua_state)
{
sol::state_view lua(lua_state);
auto omath_table = lua["omath"].get_or_create<sol::table>();
register_vec2(omath_table);
register_vec3(omath_table);
register_vec4(omath_table);
register_color(omath_table);
register_triangle(omath_table);
register_shared_types(omath_table);
register_engines(omath_table);
register_pattern_scan(omath_table);
}
} // namespace omath::lua
#endif

46
source/lua/lua_color.cpp Normal file
View File

@@ -0,0 +1,46 @@
//
// Created by orange on 07.03.2026.
//
#ifdef OMATH_ENABLE_LUA
#include "omath/lua/lua.hpp"
#include <sol/sol.hpp>
#include <omath/utility/color.hpp>
namespace omath::lua
{
void LuaInterpreter::register_color(sol::table& omath_table)
{
omath_table.new_usertype<omath::Color>(
"Color",
sol::factories([](float r, float g, float b, float a) { return omath::Color(r, g, b, a); },
[]() { return omath::Color(); }),
"from_rgba", [](uint8_t r, uint8_t g, uint8_t b, uint8_t a)
{ return omath::Color::from_rgba(r, g, b, a); }, "from_hsv",
sol::overload([](float h, float s, float v) { return omath::Color::from_hsv(h, s, v); },
[](const omath::Hsv& hsv) { return omath::Color::from_hsv(hsv); }),
"red", []() { return omath::Color::red(); }, "green", []() { return omath::Color::green(); }, "blue",
[]() { return omath::Color::blue(); },
"r", sol::property([](const omath::Color& c) { return c.value().x; }), "g",
sol::property([](const omath::Color& c) { return c.value().y; }), "b",
sol::property([](const omath::Color& c) { return c.value().z; }), "a",
sol::property([](const omath::Color& c) { return c.value().w; }),
"to_hsv", &omath::Color::to_hsv, "set_hue", &omath::Color::set_hue, "set_saturation",
&omath::Color::set_saturation, "set_value", &omath::Color::set_value, "blend", &omath::Color::blend,
sol::meta_function::to_string, &omath::Color::to_string);
omath_table.new_usertype<omath::Hsv>(
"Hsv", sol::constructors<omath::Hsv()>(), "hue",
sol::property([](const omath::Hsv& h) { return h.hue; }, [](omath::Hsv& h, float val) { h.hue = val; }),
"saturation",
sol::property([](const omath::Hsv& h) { return h.saturation; },
[](omath::Hsv& h, float val) { h.saturation = val; }),
"value",
sol::property([](const omath::Hsv& h) { return h.value; },
[](omath::Hsv& h, float val) { h.value = val; }));
}
} // namespace omath::lua::detail
#endif

227
source/lua/lua_engines.cpp Normal file
View File

@@ -0,0 +1,227 @@
//
// Created by orange on 07.03.2026.
//
#ifdef OMATH_ENABLE_LUA
#include "omath/lua/lua.hpp"
#include <omath/engines/cry_engine/camera.hpp>
#include <omath/engines/frostbite_engine/camera.hpp>
#include <omath/engines/iw_engine/camera.hpp>
#include <omath/engines/opengl_engine/camera.hpp>
#include <omath/engines/source_engine/camera.hpp>
#include <omath/engines/unity_engine/camera.hpp>
#include <omath/engines/unreal_engine/camera.hpp>
#include <sol/sol.hpp>
#include <string_view>
namespace
{
// ---- Canonical shared C++ type aliases ----------------------------------
// Each unique template instantiation must be registered exactly once.
using PitchAngle90 = omath::Angle<float, -90.f, 90.f, omath::AngleFlags::Clamped>;
using PitchAngle89 = omath::Angle<float, -89.f, 89.f, omath::AngleFlags::Clamped>;
using SharedYawRoll = omath::Angle<float, -180.f, 180.f, omath::AngleFlags::Normalized>;
using SharedFoV = omath::Angle<float, 0.f, 180.f, omath::AngleFlags::Clamped>;
using ViewAngles90 = omath::ViewAngles<PitchAngle90, SharedYawRoll, SharedYawRoll>;
using ViewAngles89 = omath::ViewAngles<PitchAngle89, SharedYawRoll, SharedYawRoll>;
std::string projection_error_to_string(omath::projection::Error e)
{
switch (e)
{
case omath::projection::Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS:
return "world position is out of screen bounds";
case omath::projection::Error::INV_VIEW_PROJ_MAT_DET_EQ_ZERO:
return "inverse view-projection matrix determinant is zero";
}
return "unknown error";
}
template<class AngleType>
void register_angle(sol::table& table, const char* name)
{
table.new_usertype<AngleType>(
name, sol::no_constructor, "from_degrees", &AngleType::from_degrees, "from_radians",
&AngleType::from_radians, "as_degrees", &AngleType::as_degrees, "as_radians", &AngleType::as_radians,
"sin", &AngleType::sin, "cos", &AngleType::cos, "tan", &AngleType::tan, "cot", &AngleType::cot,
sol::meta_function::addition, [](const AngleType& a, const AngleType& b)
{ return AngleType::from_degrees(a.as_degrees() + b.as_degrees()); }, sol::meta_function::subtraction,
[](const AngleType& a, const AngleType& b)
{ return AngleType::from_degrees(a.as_degrees() - b.as_degrees()); }, sol::meta_function::unary_minus,
[](const AngleType& a) { return AngleType::from_degrees(-a.as_degrees()); },
sol::meta_function::equal_to, [](const AngleType& a, const AngleType& b) { return a == b; },
sol::meta_function::to_string, [](const AngleType& a) { return std::format("{}deg", a.as_degrees()); });
}
// Set aliases in an engine subtable pointing to the already-registered shared types
template<class PitchAngleType, class ViewAnglesType>
void set_engine_aliases(sol::table& engine_table, sol::table& types)
{
if constexpr (std::is_same_v<PitchAngleType, PitchAngle90>)
engine_table["PitchAngle"] = types["PitchAngle90"];
else
engine_table["PitchAngle"] = types["PitchAngle89"];
engine_table["YawAngle"] = types["YawRoll"];
engine_table["RollAngle"] = types["YawRoll"];
engine_table["FieldOfView"] = types["FieldOfView"];
engine_table["ViewPort"] = types["ViewPort"];
if constexpr (std::is_same_v<ViewAnglesType, ViewAngles90>)
engine_table["ViewAngles"] = types["ViewAngles90"];
else
engine_table["ViewAngles"] = types["ViewAngles89"];
}
// Register an engine: alias shared types, register unique Camera
template<class EngineTraits>
void register_engine(sol::table& omath_table, const char* subtable_name)
{
using PitchAngle = typename EngineTraits::PitchAngle;
using ViewAngles = typename EngineTraits::ViewAngles;
using Camera = typename EngineTraits::Camera;
auto engine_table = omath_table[subtable_name].get_or_create<sol::table>();
auto types = omath_table["_types"].get<sol::table>();
set_engine_aliases<PitchAngle, ViewAngles>(engine_table, types);
engine_table.new_usertype<Camera>(
"Camera",
sol::constructors<Camera(const omath::Vector3<float>&, const ViewAngles&,
const omath::projection::ViewPort&, const omath::projection::FieldOfView&,
float, float)>(),
"look_at", &Camera::look_at, "get_forward", &Camera::get_forward, "get_right", &Camera::get_right,
"get_up", &Camera::get_up, "get_origin", &Camera::get_origin, "get_view_angles",
&Camera::get_view_angles, "get_near_plane", &Camera::get_near_plane, "get_far_plane",
&Camera::get_far_plane, "get_field_of_view", &Camera::get_field_of_view, "set_origin",
&Camera::set_origin, "set_view_angles", &Camera::set_view_angles, "set_view_port",
&Camera::set_view_port, "set_field_of_view", &Camera::set_field_of_view, "set_near_plane",
&Camera::set_near_plane, "set_far_plane", &Camera::set_far_plane,
"world_to_screen",
[](const Camera& cam, const omath::Vector3<float>& pos)
-> std::tuple<sol::optional<omath::Vector3<float>>, sol::optional<std::string>>
{
auto result = cam.world_to_screen(pos);
if (result)
return {*result, sol::nullopt};
return {sol::nullopt, projection_error_to_string(result.error())};
},
"screen_to_world",
[](const Camera& cam, const omath::Vector3<float>& pos)
-> std::tuple<sol::optional<omath::Vector3<float>>, sol::optional<std::string>>
{
auto result = cam.screen_to_world(pos);
if (result)
return {*result, sol::nullopt};
return {sol::nullopt, projection_error_to_string(result.error())};
});
}
// ---- Engine trait structs -----------------------------------------------
struct OpenGLEngineTraits
{
using PitchAngle = omath::opengl_engine::PitchAngle;
using ViewAngles = omath::opengl_engine::ViewAngles;
using Camera = omath::opengl_engine::Camera;
};
struct FrostbiteEngineTraits
{
using PitchAngle = omath::frostbite_engine::PitchAngle;
using ViewAngles = omath::frostbite_engine::ViewAngles;
using Camera = omath::frostbite_engine::Camera;
};
struct IWEngineTraits
{
using PitchAngle = omath::iw_engine::PitchAngle;
using ViewAngles = omath::iw_engine::ViewAngles;
using Camera = omath::iw_engine::Camera;
};
struct SourceEngineTraits
{
using PitchAngle = omath::source_engine::PitchAngle;
using ViewAngles = omath::source_engine::ViewAngles;
using Camera = omath::source_engine::Camera;
};
struct UnityEngineTraits
{
using PitchAngle = omath::unity_engine::PitchAngle;
using ViewAngles = omath::unity_engine::ViewAngles;
using Camera = omath::unity_engine::Camera;
};
struct UnrealEngineTraits
{
using PitchAngle = omath::unreal_engine::PitchAngle;
using ViewAngles = omath::unreal_engine::ViewAngles;
using Camera = omath::unreal_engine::Camera;
};
struct CryEngineTraits
{
using PitchAngle = omath::cry_engine::PitchAngle;
using ViewAngles = omath::cry_engine::ViewAngles;
using Camera = omath::cry_engine::Camera;
};
} // namespace
namespace omath::lua
{
void LuaInterpreter::register_shared_types(sol::table& omath_table)
{
auto t = omath_table["_types"].get_or_create<sol::table>();
register_angle<PitchAngle90>(t, "PitchAngle90");
register_angle<PitchAngle89>(t, "PitchAngle89");
register_angle<SharedYawRoll>(t, "YawRoll");
register_angle<SharedFoV>(t, "FieldOfView");
t.new_usertype<omath::projection::ViewPort>(
"ViewPort", sol::factories([](float w, float h) { return omath::projection::ViewPort{w, h}; }), "width",
sol::property([](const omath::projection::ViewPort& vp) { return vp.m_width; },
[](omath::projection::ViewPort& vp, float val) { vp.m_width = val; }),
"height",
sol::property([](const omath::projection::ViewPort& vp) { return vp.m_height; },
[](omath::projection::ViewPort& vp, float val) { vp.m_height = val; }),
"aspect_ratio", &omath::projection::ViewPort::aspect_ratio);
t.new_usertype<ViewAngles90>(
"ViewAngles90",
sol::factories([](PitchAngle90 p, SharedYawRoll y, SharedYawRoll r) { return ViewAngles90{p, y, r}; }),
"pitch",
sol::property([](const ViewAngles90& va) { return va.pitch; },
[](ViewAngles90& va, const PitchAngle90& val) { va.pitch = val; }),
"yaw",
sol::property([](const ViewAngles90& va) { return va.yaw; },
[](ViewAngles90& va, const SharedYawRoll& val) { va.yaw = val; }),
"roll",
sol::property([](const ViewAngles90& va) { return va.roll; },
[](ViewAngles90& va, const SharedYawRoll& val) { va.roll = val; }));
t.new_usertype<ViewAngles89>(
"ViewAngles89",
sol::factories([](PitchAngle89 p, SharedYawRoll y, SharedYawRoll r) { return ViewAngles89{p, y, r}; }),
"pitch",
sol::property([](const ViewAngles89& va) { return va.pitch; },
[](ViewAngles89& va, const PitchAngle89& val) { va.pitch = val; }),
"yaw",
sol::property([](const ViewAngles89& va) { return va.yaw; },
[](ViewAngles89& va, const SharedYawRoll& val) { va.yaw = val; }),
"roll",
sol::property([](const ViewAngles89& va) { return va.roll; },
[](ViewAngles89& va, const SharedYawRoll& val) { va.roll = val; }));
}
void LuaInterpreter::register_engines(sol::table& omath_table)
{
register_engine<OpenGLEngineTraits>(omath_table, "opengl");
register_engine<FrostbiteEngineTraits>(omath_table, "frostbite");
register_engine<IWEngineTraits>(omath_table, "iw");
register_engine<SourceEngineTraits>(omath_table, "source");
register_engine<UnityEngineTraits>(omath_table, "unity");
register_engine<UnrealEngineTraits>(omath_table, "unreal");
register_engine<CryEngineTraits>(omath_table, "cry");
}
} // namespace omath::lua::detail
#endif

View File

@@ -0,0 +1,104 @@
//
// Created by orange on 10.03.2026.
//
#ifdef OMATH_ENABLE_LUA
#include "omath/lua/lua.hpp"
#include <format>
#include <omath/utility/elf_pattern_scan.hpp>
#include <omath/utility/macho_pattern_scan.hpp>
#include <omath/utility/pattern_scan.hpp>
#include <omath/utility/pe_pattern_scan.hpp>
#include <omath/utility/section_scan_result.hpp>
#include <sol/sol.hpp>
#endif
namespace omath::lua
{
void LuaInterpreter::register_pattern_scan(sol::table& omath_table)
{
omath_table.new_usertype<SectionScanResult>(
"SectionScanResult", sol::no_constructor,
"virtual_base_addr",
sol::property([](const SectionScanResult& r) { return r.virtual_base_addr; }),
"raw_base_addr",
sol::property([](const SectionScanResult& r) { return r.raw_base_addr; }),
"target_offset",
sol::property([](const SectionScanResult& r) { return r.target_offset; }),
sol::meta_function::to_string,
[](const SectionScanResult& r)
{
return std::format("SectionScanResult(vbase=0x{:X}, raw_base=0x{:X}, offset={})",
r.virtual_base_addr, r.raw_base_addr, r.target_offset);
});
// Generic scanner: accepts a Lua string as a byte buffer
auto ps_table = omath_table["PatternScanner"].get_or_create<sol::table>();
ps_table["scan"] = [](const std::string& data, const std::string& pattern) -> sol::optional<std::ptrdiff_t>
{
const auto* begin = reinterpret_cast<const std::byte*>(data.data());
const auto* end = begin + data.size();
const auto* result = PatternScanner::scan_for_pattern(begin, end, pattern);
if (result == end)
return sol::nullopt;
return std::distance(begin, result);
};
auto pe_table = omath_table["PePatternScanner"].get_or_create<sol::table>();
pe_table["scan_in_module"] = [](std::uintptr_t base_addr, const std::string& pattern,
sol::optional<std::string> section) -> sol::optional<std::uintptr_t>
{
auto result = PePatternScanner::scan_for_pattern_in_loaded_module(reinterpret_cast<const void*>(base_addr),
pattern, section.value_or(".text"));
if (!result)
return sol::nullopt;
return *result;
};
pe_table["scan_in_file"] = [](const std::string& path, const std::string& pattern,
sol::optional<std::string> section) -> sol::optional<SectionScanResult>
{
auto result = PePatternScanner::scan_for_pattern_in_file(std::filesystem::path(path), pattern,
section.value_or(".text"));
if (!result)
return sol::nullopt;
return *result;
};
auto elf_table = omath_table["ElfPatternScanner"].get_or_create<sol::table>();
elf_table["scan_in_module"] = [](std::uintptr_t base_addr, const std::string& pattern,
sol::optional<std::string> section) -> sol::optional<std::uintptr_t>
{
auto result = ElfPatternScanner::scan_for_pattern_in_loaded_module(reinterpret_cast<const void*>(base_addr),
pattern, section.value_or(".text"));
if (!result)
return sol::nullopt;
return *result;
};
elf_table["scan_in_file"] = [](const std::string& path, const std::string& pattern,
sol::optional<std::string> section) -> sol::optional<SectionScanResult>
{
auto result = ElfPatternScanner::scan_for_pattern_in_file(std::filesystem::path(path), pattern,
section.value_or(".text"));
if (!result)
return sol::nullopt;
return *result;
};
auto macho_table = omath_table["MachOPatternScanner"].get_or_create<sol::table>();
macho_table["scan_in_module"] = [](std::uintptr_t base_addr, const std::string& pattern,
sol::optional<std::string> section) -> sol::optional<std::uintptr_t>
{
auto result = MachOPatternScanner::scan_for_pattern_in_loaded_module(
reinterpret_cast<const void*>(base_addr), pattern, section.value_or("__text"));
if (!result)
return sol::nullopt;
return *result;
};
macho_table["scan_in_file"] = [](const std::string& path, const std::string& pattern,
sol::optional<std::string> section) -> sol::optional<SectionScanResult>
{
auto result = MachOPatternScanner::scan_for_pattern_in_file(std::filesystem::path(path), pattern,
section.value_or("__text"));
if (!result)
return sol::nullopt;
return *result;
};
}
} // namespace omath::lua

View File

@@ -0,0 +1,48 @@
//
// Created by orange on 10.03.2026.
//
#ifdef OMATH_ENABLE_LUA
#include "omath/lua/lua.hpp"
#include <sol/sol.hpp>
#include <omath/linear_algebra/triangle.hpp>
namespace omath::lua
{
void LuaInterpreter::register_triangle(sol::table& omath_table)
{
using Vec3f = omath::Vector3<float>;
using Tri3f = omath::Triangle<Vec3f>;
omath_table.new_usertype<Tri3f>(
"Triangle", sol::constructors<Tri3f(), Tri3f(const Vec3f&, const Vec3f&, const Vec3f&)>(),
"vertex1",
sol::property([](const Tri3f& t) { return t.m_vertex1; },
[](Tri3f& t, const Vec3f& v) { t.m_vertex1 = v; }),
"vertex2",
sol::property([](const Tri3f& t) { return t.m_vertex2; },
[](Tri3f& t, const Vec3f& v) { t.m_vertex2 = v; }),
"vertex3",
sol::property([](const Tri3f& t) { return t.m_vertex3; },
[](Tri3f& t, const Vec3f& v) { t.m_vertex3 = v; }),
"calculate_normal", &Tri3f::calculate_normal,
"side_a_length", &Tri3f::side_a_length,
"side_b_length", &Tri3f::side_b_length,
"side_a_vector", &Tri3f::side_a_vector,
"side_b_vector", &Tri3f::side_b_vector,
"hypot", &Tri3f::hypot,
"is_rectangular", &Tri3f::is_rectangular,
"mid_point", &Tri3f::mid_point,
sol::meta_function::to_string,
[](const Tri3f& t)
{
return std::format("Triangle(({}, {}, {}), ({}, {}, {}), ({}, {}, {}))",
t.m_vertex1.x, t.m_vertex1.y, t.m_vertex1.z,
t.m_vertex2.x, t.m_vertex2.y, t.m_vertex2.z,
t.m_vertex3.x, t.m_vertex3.y, t.m_vertex3.z);
});
}
} // namespace omath::lua
#endif

54
source/lua/lua_vec2.cpp Normal file
View File

@@ -0,0 +1,54 @@
//
// Created by orange on 07.03.2026.
//
#ifdef OMATH_ENABLE_LUA
#include "omath/lua/lua.hpp"
#include <omath/linear_algebra/vector2.hpp>
#include <sol/sol.hpp>
namespace omath::lua
{
void LuaInterpreter::register_vec2(sol::table& omath_table)
{
using Vec2f = omath::Vector2<float>;
omath_table.new_usertype<Vec2f>(
"Vec2", sol::constructors<Vec2f(), Vec2f(float, float)>(),
"x", sol::property([](const Vec2f& v) { return v.x; }, [](Vec2f& v, const float val) { v.x = val; }),
"y", sol::property([](const Vec2f& v) { return v.y; }, [](Vec2f& v, const float val) { v.y = val; }),
sol::meta_function::addition, sol::resolve<Vec2f(const Vec2f&) const>(&Vec2f::operator+),
sol::meta_function::subtraction, sol::resolve<Vec2f(const Vec2f&) const>(&Vec2f::operator-),
sol::meta_function::unary_minus, sol::resolve<Vec2f() const>(&Vec2f::operator-),
sol::meta_function::equal_to, &Vec2f::operator==,
sol::meta_function::less_than, sol::resolve<bool(const Vec2f&) const>(&Vec2f::operator<),
sol::meta_function::less_than_or_equal_to, sol::resolve<bool(const Vec2f&) const>(&Vec2f::operator<=),
sol::meta_function::to_string,
[](const Vec2f& v) { return std::format("Vec2({}, {})", v.x, v.y); },
sol::meta_function::multiplication,
sol::overload(sol::resolve<Vec2f(const float&) const>(&Vec2f::operator*),
[](const float s, const Vec2f& v) { return v * s; }),
sol::meta_function::division,
sol::resolve<Vec2f(const float&) const>(&Vec2f::operator/),
"length", &Vec2f::length,
"length_sqr", &Vec2f::length_sqr,
"normalized", &Vec2f::normalized,
"dot", &Vec2f::dot,
"distance_to", &Vec2f::distance_to,
"distance_to_sqr", &Vec2f::distance_to_sqr,
"sum", &Vec2f::sum,
"abs",
[](const Vec2f& v)
{
Vec2f copy = v;
copy.abs();
return copy;
});
}
} // namespace omath::lua::detail
#endif

81
source/lua/lua_vec3.cpp Normal file
View File

@@ -0,0 +1,81 @@
//
// Created by orange on 07.03.2026.
//
#ifdef OMATH_ENABLE_LUA
#include "omath/lua/lua.hpp"
#include <sol/sol.hpp>
#include <omath/linear_algebra/vector3.hpp>
namespace omath::lua
{
void LuaInterpreter::register_vec3(sol::table& omath_table)
{
using Vec3f = omath::Vector3<float>;
omath_table.new_usertype<Vec3f>(
"Vec3", sol::constructors<Vec3f(), Vec3f(float, float, float)>(),
"x", sol::property([](const Vec3f& v) { return v.x; }, [](Vec3f& v, float val) { v.x = val; }),
"y", sol::property([](const Vec3f& v) { return v.y; }, [](Vec3f& v, float val) { v.y = val; }),
"z", sol::property([](const Vec3f& v) { return v.z; }, [](Vec3f& v, float val) { v.z = val; }),
sol::meta_function::addition, sol::resolve<Vec3f(const Vec3f&) const>(&Vec3f::operator+),
sol::meta_function::subtraction, sol::resolve<Vec3f(const Vec3f&) const>(&Vec3f::operator-),
sol::meta_function::unary_minus, sol::resolve<Vec3f() const>(&Vec3f::operator-),
sol::meta_function::equal_to, &Vec3f::operator==, sol::meta_function::less_than,
sol::resolve<bool(const Vec3f&) const>(&Vec3f::operator<), sol::meta_function::less_than_or_equal_to,
sol::resolve<bool(const Vec3f&) const>(&Vec3f::operator<=), sol::meta_function::to_string,
[](const Vec3f& v) { return std::format("Vec3({}, {}, {})", v.x, v.y, v.z); },
sol::meta_function::multiplication,
sol::overload(sol::resolve<Vec3f(const float&) const>(&Vec3f::operator*),
sol::resolve<Vec3f(const Vec3f&) const>(&Vec3f::operator*),
[](const float s, const Vec3f& v) { return v * s; }),
sol::meta_function::division,
sol::overload(sol::resolve<Vec3f(const float&) const>(&Vec3f::operator/),
sol::resolve<Vec3f(const Vec3f&) const>(&Vec3f::operator/)),
"length", &Vec3f::length, "length_2d", &Vec3f::length_2d, "length_sqr", &Vec3f::length_sqr,
"normalized", &Vec3f::normalized, "dot", &Vec3f::dot, "cross", &Vec3f::cross, "distance_to",
&Vec3f::distance_to, "distance_to_sqr", &Vec3f::distance_to_sqr, "sum",
sol::resolve<float() const>(&Vec3f::sum), "sum_2d", &Vec3f::sum_2d, "point_to_same_direction",
&Vec3f::point_to_same_direction, "as_array", &Vec3f::as_array,
"abs",
[](const Vec3f& v)
{
Vec3f copy = v;
copy.abs();
return copy;
},
"angle_between",
[](const Vec3f& self,
const Vec3f& other) -> std::tuple<sol::optional<float>, sol::optional<std::string>>
{
auto result = self.angle_between(other);
if (result)
return std::make_tuple(sol::optional<float>(result->as_degrees()),
sol::optional<std::string>(sol::nullopt));
return std::make_tuple(sol::optional<float>(sol::nullopt),
sol::optional<std::string>("impossible angle (zero-length vector)"));
},
"is_perpendicular",
[](const Vec3f& self, const Vec3f& other, sol::optional<float> eps)
{ return self.is_perpendicular(other, eps.value_or(0.0001f)); },
"as_table",
[](const Vec3f& v, sol::this_state s) -> sol::table
{
sol::state_view lua(s);
sol::table t = lua.create_table();
t["x"] = v.x;
t["y"] = v.y;
t["z"] = v.z;
return t;
});
}
} // namespace omath::lua::detail
#endif

62
source/lua/lua_vec4.cpp Normal file
View File

@@ -0,0 +1,62 @@
//
// Created by orange on 07.03.2026.
//
#ifdef OMATH_ENABLE_LUA
#include "omath/lua/lua.hpp"
#include <sol/sol.hpp>
#include <omath/linear_algebra/vector4.hpp>
namespace omath::lua
{
void LuaInterpreter::register_vec4(sol::table& omath_table)
{
using Vec4f = omath::Vector4<float>;
omath_table.new_usertype<Vec4f>(
"Vec4", sol::constructors<Vec4f(), Vec4f(float, float, float, float)>(),
"x", sol::property([](const Vec4f& v) { return v.x; }, [](Vec4f& v, float val) { v.x = val; }),
"y", sol::property([](const Vec4f& v) { return v.y; }, [](Vec4f& v, float val) { v.y = val; }),
"z", sol::property([](const Vec4f& v) { return v.z; }, [](Vec4f& v, float val) { v.z = val; }),
"w", sol::property([](const Vec4f& v) { return v.w; }, [](Vec4f& v, float val) { v.w = val; }),
sol::meta_function::addition, sol::resolve<Vec4f(const Vec4f&) const>(&Vec4f::operator+),
sol::meta_function::subtraction, sol::resolve<Vec4f(const Vec4f&) const>(&Vec4f::operator-),
sol::meta_function::unary_minus, sol::resolve<Vec4f() const>(&Vec4f::operator-),
sol::meta_function::equal_to, &Vec4f::operator==,
sol::meta_function::less_than, sol::resolve<bool(const Vec4f&) const>(&Vec4f::operator<),
sol::meta_function::less_than_or_equal_to, sol::resolve<bool(const Vec4f&) const>(&Vec4f::operator<=),
sol::meta_function::to_string,
[](const Vec4f& v) { return std::format("Vec4({}, {}, {}, {})", v.x, v.y, v.z, v.w); },
sol::meta_function::multiplication,
sol::overload(sol::resolve<Vec4f(const float&) const>(&Vec4f::operator*),
sol::resolve<Vec4f(const Vec4f&) const>(&Vec4f::operator*),
[](const float s, const Vec4f& v) { return v * s; }),
sol::meta_function::division,
sol::overload(sol::resolve<Vec4f(const float&) const>(&Vec4f::operator/),
sol::resolve<Vec4f(const Vec4f&) const>(&Vec4f::operator/)),
"length", &Vec4f::length,
"length_sqr", &Vec4f::length_sqr,
"dot", &Vec4f::dot,
"sum", &Vec4f::sum,
"abs",
[](const Vec4f& v)
{
Vec4f copy = v;
copy.abs();
return copy;
},
"clamp",
[](Vec4f& v, float mn, float mx)
{
v.clamp(mn, mx);
return v;
});
}
} // namespace omath::lua::detail
#endif

View File

@@ -4,8 +4,8 @@ project(unit_tests)
include(GoogleTest) include(GoogleTest)
file(GLOB_RECURSE UNIT_TESTS_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp") file(GLOB_RECURSE UNIT_TESTS_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/general/*.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/engines/*.cpp")
add_executable(${PROJECT_NAME} ${UNIT_TESTS_SOURCES}) add_executable(${PROJECT_NAME} ${UNIT_TESTS_SOURCES} main.cpp)
set_target_properties( set_target_properties(
${PROJECT_NAME} ${PROJECT_NAME}
@@ -22,6 +22,16 @@ else() # GTest is being linked as vcpkg package
target_link_libraries(${PROJECT_NAME} PRIVATE GTest::gtest GTest::gtest_main omath::omath) target_link_libraries(${PROJECT_NAME} PRIVATE GTest::gtest GTest::gtest_main omath::omath)
endif() endif()
if (OMATH_ENABLE_LUA)
file(GLOB_RECURSE UNIT_TESTS_SOURCES_LUA CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/lua/*.cpp")
target_compile_definitions(${PROJECT_NAME} PRIVATE LUA_SCRIPTS_DIR="${CMAKE_CURRENT_SOURCE_DIR}/lua")
target_sources(${PROJECT_NAME} PRIVATE ${UNIT_TESTS_SOURCES_LUA})
if (EMSCRIPTEN)
target_link_options(${PROJECT_NAME} PRIVATE
"SHELL:--embed-file ${CMAKE_CURRENT_SOURCE_DIR}/lua@${CMAKE_CURRENT_SOURCE_DIR}/lua")
endif()
endif()
if(OMATH_ENABLE_COVERAGE) if(OMATH_ENABLE_COVERAGE)
include(${CMAKE_SOURCE_DIR}/cmake/Coverage.cmake) include(${CMAKE_SOURCE_DIR}/cmake/Coverage.cmake)
omath_setup_coverage(${PROJECT_NAME}) omath_setup_coverage(${PROJECT_NAME})
@@ -36,3 +46,4 @@ endif()
if(NOT (ANDROID OR IOS OR EMSCRIPTEN)) if(NOT (ANDROID OR IOS OR EMSCRIPTEN))
gtest_discover_tests(${PROJECT_NAME}) gtest_discover_tests(${PROJECT_NAME})
endif() endif()

View File

@@ -26,38 +26,38 @@ protected:
TEST_F(UnitTestColorGrouped, Constructor_Float) TEST_F(UnitTestColorGrouped, Constructor_Float)
{ {
constexpr Color color(0.5f, 0.5f, 0.5f, 1.0f); constexpr Color color(0.5f, 0.5f, 0.5f, 1.0f);
EXPECT_FLOAT_EQ(color.x, 0.5f); EXPECT_FLOAT_EQ(color.value().x, 0.5f);
EXPECT_FLOAT_EQ(color.y, 0.5f); EXPECT_FLOAT_EQ(color.value().y, 0.5f);
EXPECT_FLOAT_EQ(color.z, 0.5f); EXPECT_FLOAT_EQ(color.value().z, 0.5f);
EXPECT_FLOAT_EQ(color.w, 1.0f); EXPECT_FLOAT_EQ(color.value().w, 1.0f);
} }
TEST_F(UnitTestColorGrouped, Constructor_Vector4) TEST_F(UnitTestColorGrouped, Constructor_Vector4)
{ {
constexpr omath::Vector4 vec(0.2f, 0.4f, 0.6f, 0.8f); constexpr omath::Vector4 vec(0.2f, 0.4f, 0.6f, 0.8f);
constexpr Color color(vec); constexpr Color color(vec);
EXPECT_FLOAT_EQ(color.x, 0.2f); EXPECT_FLOAT_EQ(color.value().x, 0.2f);
EXPECT_FLOAT_EQ(color.y, 0.4f); EXPECT_FLOAT_EQ(color.value().y, 0.4f);
EXPECT_FLOAT_EQ(color.z, 0.6f); EXPECT_FLOAT_EQ(color.value().z, 0.6f);
EXPECT_FLOAT_EQ(color.w, 0.8f); EXPECT_FLOAT_EQ(color.value().w, 0.8f);
} }
TEST_F(UnitTestColorGrouped, FromRGBA) TEST_F(UnitTestColorGrouped, FromRGBA)
{ {
constexpr Color color = Color::from_rgba(128, 64, 32, 255); constexpr Color color = Color::from_rgba(128, 64, 32, 255);
EXPECT_FLOAT_EQ(color.x, 128.0f / 255.0f); EXPECT_FLOAT_EQ(color.value().x, 128.0f / 255.0f);
EXPECT_FLOAT_EQ(color.y, 64.0f / 255.0f); EXPECT_FLOAT_EQ(color.value().y, 64.0f / 255.0f);
EXPECT_FLOAT_EQ(color.z, 32.0f / 255.0f); EXPECT_FLOAT_EQ(color.value().z, 32.0f / 255.0f);
EXPECT_FLOAT_EQ(color.w, 1.0f); EXPECT_FLOAT_EQ(color.value().w, 1.0f);
} }
TEST_F(UnitTestColorGrouped, FromHSV) TEST_F(UnitTestColorGrouped, FromHSV)
{ {
constexpr Color color = Color::from_hsv(0.0f, 1.0f, 1.0f); // Red in HSV 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.value().x, 1.0f);
EXPECT_FLOAT_EQ(color.y, 0.0f); EXPECT_FLOAT_EQ(color.value().y, 0.0f);
EXPECT_FLOAT_EQ(color.z, 0.0f); EXPECT_FLOAT_EQ(color.value().z, 0.0f);
EXPECT_FLOAT_EQ(color.w, 1.0f); EXPECT_FLOAT_EQ(color.value().w, 1.0f);
} }
TEST_F(UnitTestColorGrouped, ToHSV) TEST_F(UnitTestColorGrouped, ToHSV)
@@ -71,10 +71,10 @@ TEST_F(UnitTestColorGrouped, ToHSV)
TEST_F(UnitTestColorGrouped, Blend) TEST_F(UnitTestColorGrouped, Blend)
{ {
const Color blended = color1.blend(color2, 0.5f); const Color blended = color1.blend(color2, 0.5f);
EXPECT_FLOAT_EQ(blended.x, 0.5f); EXPECT_FLOAT_EQ(blended.value().x, 0.5f);
EXPECT_FLOAT_EQ(blended.y, 0.5f); EXPECT_FLOAT_EQ(blended.value().y, 0.5f);
EXPECT_FLOAT_EQ(blended.z, 0.0f); EXPECT_FLOAT_EQ(blended.value().z, 0.0f);
EXPECT_FLOAT_EQ(blended.w, 1.0f); EXPECT_FLOAT_EQ(blended.value().w, 1.0f);
} }
TEST_F(UnitTestColorGrouped, PredefinedColors) TEST_F(UnitTestColorGrouped, PredefinedColors)
@@ -83,20 +83,20 @@ TEST_F(UnitTestColorGrouped, PredefinedColors)
constexpr Color green = Color::green(); constexpr Color green = Color::green();
constexpr Color blue = Color::blue(); constexpr Color blue = Color::blue();
EXPECT_FLOAT_EQ(red.x, 1.0f); EXPECT_FLOAT_EQ(red.value().x, 1.0f);
EXPECT_FLOAT_EQ(red.y, 0.0f); EXPECT_FLOAT_EQ(red.value().y, 0.0f);
EXPECT_FLOAT_EQ(red.z, 0.0f); EXPECT_FLOAT_EQ(red.value().z, 0.0f);
EXPECT_FLOAT_EQ(red.w, 1.0f); EXPECT_FLOAT_EQ(red.value().w, 1.0f);
EXPECT_FLOAT_EQ(green.x, 0.0f); EXPECT_FLOAT_EQ(green.value().x, 0.0f);
EXPECT_FLOAT_EQ(green.y, 1.0f); EXPECT_FLOAT_EQ(green.value().y, 1.0f);
EXPECT_FLOAT_EQ(green.z, 0.0f); EXPECT_FLOAT_EQ(green.value().z, 0.0f);
EXPECT_FLOAT_EQ(green.w, 1.0f); EXPECT_FLOAT_EQ(green.value().w, 1.0f);
EXPECT_FLOAT_EQ(blue.x, 0.0f); EXPECT_FLOAT_EQ(blue.value().x, 0.0f);
EXPECT_FLOAT_EQ(blue.y, 0.0f); EXPECT_FLOAT_EQ(blue.value().y, 0.0f);
EXPECT_FLOAT_EQ(blue.z, 1.0f); EXPECT_FLOAT_EQ(blue.value().z, 1.0f);
EXPECT_FLOAT_EQ(blue.w, 1.0f); EXPECT_FLOAT_EQ(blue.value().w, 1.0f);
} }
TEST_F(UnitTestColorGrouped, BlendVector3) 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 v1(1.0f, 0.0f, 0.0f, 1.f); // Red
constexpr Color v2(0.0f, 1.0f, 0.0f, 1.f); // Green constexpr Color v2(0.0f, 1.0f, 0.0f, 1.f); // Green
constexpr Color blended = v1.blend(v2, 0.5f); constexpr Color blended = v1.blend(v2, 0.5f);
EXPECT_FLOAT_EQ(blended.x, 0.5f); EXPECT_FLOAT_EQ(blended.value().x, 0.5f);
EXPECT_FLOAT_EQ(blended.y, 0.5f); EXPECT_FLOAT_EQ(blended.value().y, 0.5f);
EXPECT_FLOAT_EQ(blended.z, 0.0f); EXPECT_FLOAT_EQ(blended.value().z, 0.0f);
} }
// From unit_test_color_extra.cpp // From unit_test_color_extra.cpp
@@ -148,37 +148,37 @@ TEST(UnitTestColorGrouped_Extra, BlendEdgeCases)
constexpr Color a = Color::red(); constexpr Color a = Color::red();
constexpr Color b = Color::blue(); constexpr Color b = Color::blue();
constexpr auto r0 = a.blend(b, 0.f); 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); 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 // From unit_test_color_more.cpp
TEST(UnitTestColorGrouped_More, DefaultCtorIsZero) TEST(UnitTestColorGrouped_More, DefaultCtorIsZero)
{ {
constexpr Color c; constexpr Color c;
EXPECT_FLOAT_EQ(c.x, 0.0f); EXPECT_FLOAT_EQ(c.value().x, 0.0f);
EXPECT_FLOAT_EQ(c.y, 0.0f); EXPECT_FLOAT_EQ(c.value().y, 0.0f);
EXPECT_FLOAT_EQ(c.z, 0.0f); EXPECT_FLOAT_EQ(c.value().z, 0.0f);
EXPECT_FLOAT_EQ(c.w, 0.0f); EXPECT_FLOAT_EQ(c.value().w, 0.0f);
} }
TEST(UnitTestColorGrouped_More, FloatCtorAndClampForRGB) TEST(UnitTestColorGrouped_More, FloatCtorAndClampForRGB)
{ {
constexpr Color c(1.2f, -0.5f, 0.5f, 2.0f); constexpr Color c(1.2f, -0.5f, 0.5f, 2.0f);
EXPECT_FLOAT_EQ(c.x, 1.0f); EXPECT_FLOAT_EQ(c.value().x, 1.0f);
EXPECT_FLOAT_EQ(c.y, 0.0f); EXPECT_FLOAT_EQ(c.value().y, 0.0f);
EXPECT_FLOAT_EQ(c.z, 0.5f); EXPECT_FLOAT_EQ(c.value().z, 0.5f);
EXPECT_FLOAT_EQ(c.w, 2.0f); EXPECT_FLOAT_EQ(c.value().w, 2.0f);
} }
TEST(UnitTestColorGrouped_More, FromRgbaProducesScaledComponents) TEST(UnitTestColorGrouped_More, FromRgbaProducesScaledComponents)
{ {
constexpr Color c = Color::from_rgba(25u, 128u, 230u, 64u); constexpr Color c = Color::from_rgba(25u, 128u, 230u, 64u);
EXPECT_NEAR(c.x, 25.0f/255.0f, 1e-6f); EXPECT_NEAR(c.value().x, 25.0f/255.0f, 1e-6f);
EXPECT_NEAR(c.y, 128.0f/255.0f, 1e-6f); EXPECT_NEAR(c.value().y, 128.0f/255.0f, 1e-6f);
EXPECT_NEAR(c.z, 230.0f/255.0f, 1e-6f); EXPECT_NEAR(c.value().z, 230.0f/255.0f, 1e-6f);
EXPECT_NEAR(c.w, 64.0f/255.0f, 1e-6f); EXPECT_NEAR(c.value().w, 64.0f/255.0f, 1e-6f);
} }
TEST(UnitTestColorGrouped_More, BlendProducesIntermediate) 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 c0(0.0f, 0.0f, 0.0f, 1.0f);
constexpr Color c1(1.0f, 1.0f, 1.0f, 0.0f); constexpr Color c1(1.0f, 1.0f, 1.0f, 0.0f);
constexpr Color mid = c0.blend(c1, 0.5f); constexpr Color mid = c0.blend(c1, 0.5f);
EXPECT_FLOAT_EQ(mid.x, 0.5f); EXPECT_FLOAT_EQ(mid.value().x, 0.5f);
EXPECT_FLOAT_EQ(mid.y, 0.5f); EXPECT_FLOAT_EQ(mid.value().y, 0.5f);
EXPECT_FLOAT_EQ(mid.z, 0.5f); EXPECT_FLOAT_EQ(mid.value().z, 0.5f);
EXPECT_FLOAT_EQ(mid.w, 0.5f); EXPECT_FLOAT_EQ(mid.value().w, 0.5f);
} }
TEST(UnitTestColorGrouped_More, HsvRoundTrip) TEST(UnitTestColorGrouped_More, HsvRoundTrip)
@@ -197,9 +197,9 @@ TEST(UnitTestColorGrouped_More, HsvRoundTrip)
constexpr Color red = Color::red(); constexpr Color red = Color::red();
const auto hsv = red.to_hsv(); const auto hsv = red.to_hsv();
const Color back = Color::from_hsv(hsv); const Color back = Color::from_hsv(hsv);
EXPECT_NEAR(back.x, 1.0f, 1e-6f); EXPECT_NEAR(back.value().x, 1.0f, 1e-6f);
EXPECT_NEAR(back.y, 0.0f, 1e-6f); EXPECT_NEAR(back.value().y, 0.0f, 1e-6f);
EXPECT_NEAR(back.z, 0.0f, 1e-6f); EXPECT_NEAR(back.value().z, 0.0f, 1e-6f);
} }
TEST(UnitTestColorGrouped_More, ToStringContainsComponents) TEST(UnitTestColorGrouped_More, ToStringContainsComponents)
@@ -230,18 +230,18 @@ TEST(UnitTestColorGrouped_More2, FromHsvCases)
auto check_hue = [&](float h) { auto check_hue = [&](float h) {
SCOPED_TRACE(::testing::Message() << "h=" << h); SCOPED_TRACE(::testing::Message() << "h=" << h);
Color c = Color::from_hsv(h, 1.f, 1.f); Color c = Color::from_hsv(h, 1.f, 1.f);
EXPECT_TRUE(std::isfinite(c.x)); EXPECT_TRUE(std::isfinite(c.value().x));
EXPECT_TRUE(std::isfinite(c.y)); EXPECT_TRUE(std::isfinite(c.value().y));
EXPECT_TRUE(std::isfinite(c.z)); EXPECT_TRUE(std::isfinite(c.value().z));
EXPECT_GE(c.x, -eps); EXPECT_GE(c.value().x, -eps);
EXPECT_LE(c.x, 1.f + eps); EXPECT_LE(c.value().x, 1.f + eps);
EXPECT_GE(c.y, -eps); EXPECT_GE(c.value().y, -eps);
EXPECT_LE(c.y, 1.f + eps); EXPECT_LE(c.value().y, 1.f + eps);
EXPECT_GE(c.z, -eps); EXPECT_GE(c.value().z, -eps);
EXPECT_LE(c.z, 1.f + eps); EXPECT_LE(c.value().z, 1.f + eps);
float mx = std::max({c.x, c.y, c.z}); float mx = std::max({c.value().x, c.value().y, c.value().z});
float mn = std::min({c.x, c.y, c.z}); float mn = std::min({c.value().x, c.value().y, c.value().z});
EXPECT_GE(mx, 0.999f); EXPECT_GE(mx, 0.999f);
EXPECT_LE(mn, 1e-3f + 1e-4f); EXPECT_LE(mn, 1e-3f + 1e-4f);
}; };
@@ -261,13 +261,13 @@ TEST(UnitTestColorGrouped_More2, ToHsvAndSetters)
EXPECT_NEAR(hsv.value, 0.6f, 1e-6f); EXPECT_NEAR(hsv.value, 0.6f, 1e-6f);
c.set_hue(0.0f); c.set_hue(0.0f);
EXPECT_TRUE(std::isfinite(c.x)); EXPECT_TRUE(std::isfinite(c.value().x));
c.set_saturation(0.0f); c.set_saturation(0.0f);
EXPECT_TRUE(std::isfinite(c.y)); EXPECT_TRUE(std::isfinite(c.value().y));
c.set_value(0.5f); c.set_value(0.5f);
EXPECT_TRUE(std::isfinite(c.z)); EXPECT_TRUE(std::isfinite(c.value().z));
} }
TEST(UnitTestColorGrouped_More2, BlendAndStaticColors) TEST(UnitTestColorGrouped_More2, BlendAndStaticColors)
@@ -275,14 +275,14 @@ TEST(UnitTestColorGrouped_More2, BlendAndStaticColors)
constexpr Color a = Color::red(); constexpr Color a = Color::red();
constexpr Color b = Color::blue(); constexpr Color b = Color::blue();
constexpr auto mid = a.blend(b, 0.5f); constexpr auto mid = a.blend(b, 0.5f);
EXPECT_GT(mid.x, 0.f); EXPECT_GT(mid.value().x, 0.f);
EXPECT_GT(mid.z, 0.f); EXPECT_GT(mid.value().z, 0.f);
constexpr auto all_a = a.blend(b, -1.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); 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) TEST(UnitTestColorGrouped_More2, FormatterUsesToString)
@@ -291,3 +291,35 @@ TEST(UnitTestColorGrouped_More2, FormatterUsesToString)
const auto formatted = std::format("{}", c); const auto formatted = std::format("{}", c);
EXPECT_NE(formatted.find("r:10"), std::string::npos); 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,471 @@
//
// Comprehensive EPA tests.
// Covers: all 3 axis directions, multiple depth levels, penetration-vector
// round-trips, depth monotonicity, symmetry, asymmetric sizes, memory
// resource variants, tolerance sensitivity, and iteration bookkeeping.
//
#include <cmath>
#include <gtest/gtest.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>;
using Vec3 = omath::Vector3<float>;
namespace
{
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{};
constexpr Epa::Params k_default_params{ .max_iterations = 64, .tolerance = 1e-4f };
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 };
}
// Run GJK then EPA; asserts GJK hit and EPA converged.
Epa::Result solve(const Collider& a, const Collider& b,
const Epa::Params& params = k_default_params)
{
const auto [hit, simplex] = Gjk::is_collide_with_simplex_info(a, b);
EXPECT_TRUE(hit) << "GJK must detect collision before EPA can run";
auto result = Epa::solve(a, b, simplex, params);
EXPECT_TRUE(result.has_value()) << "EPA must converge";
return *result;
}
} // namespace
// ---------------------------------------------------------------------------
// Normal direction per axis
// ---------------------------------------------------------------------------
// For two unit cubes (half-extent 1) with B offset by d along an axis:
// depth = 2 - d (distance from origin to nearest face of Minkowski diff)
// normal component along that axis ≈ ±1
TEST(EpaComprehensive, NormalAlongX_Positive)
{
const auto r = solve(make_cube({ 0, 0, 0 }), make_cube({ 0.5f, 0, 0 }));
EXPECT_NEAR(std::abs(r.normal.x), 1.f, 1e-3f);
EXPECT_NEAR(r.normal.y, 0.f, 1e-3f);
EXPECT_NEAR(r.normal.z, 0.f, 1e-3f);
}
TEST(EpaComprehensive, NormalAlongX_Negative)
{
const auto r = solve(make_cube({ 0, 0, 0 }), make_cube({ -0.5f, 0, 0 }));
EXPECT_NEAR(std::abs(r.normal.x), 1.f, 1e-3f);
EXPECT_NEAR(r.normal.y, 0.f, 1e-3f);
EXPECT_NEAR(r.normal.z, 0.f, 1e-3f);
}
TEST(EpaComprehensive, NormalAlongY_Positive)
{
const auto r = solve(make_cube({ 0, 0, 0 }), make_cube({ 0, 0.5f, 0 }));
EXPECT_NEAR(r.normal.x, 0.f, 1e-3f);
EXPECT_NEAR(std::abs(r.normal.y), 1.f, 1e-3f);
EXPECT_NEAR(r.normal.z, 0.f, 1e-3f);
}
TEST(EpaComprehensive, NormalAlongY_Negative)
{
const auto r = solve(make_cube({ 0, 0, 0 }), make_cube({ 0, -0.5f, 0 }));
EXPECT_NEAR(r.normal.x, 0.f, 1e-3f);
EXPECT_NEAR(std::abs(r.normal.y), 1.f, 1e-3f);
EXPECT_NEAR(r.normal.z, 0.f, 1e-3f);
}
TEST(EpaComprehensive, NormalAlongZ_Positive)
{
const auto r = solve(make_cube({ 0, 0, 0 }), make_cube({ 0, 0, 0.5f }));
EXPECT_NEAR(r.normal.x, 0.f, 1e-3f);
EXPECT_NEAR(r.normal.y, 0.f, 1e-3f);
EXPECT_NEAR(std::abs(r.normal.z), 1.f, 1e-3f);
}
TEST(EpaComprehensive, NormalAlongZ_Negative)
{
const auto r = solve(make_cube({ 0, 0, 0 }), make_cube({ 0, 0, -0.5f }));
EXPECT_NEAR(r.normal.x, 0.f, 1e-3f);
EXPECT_NEAR(r.normal.y, 0.f, 1e-3f);
EXPECT_NEAR(std::abs(r.normal.z), 1.f, 1e-3f);
}
// ---------------------------------------------------------------------------
// Depth correctness (depth = 2 - offset for unit cubes)
// ---------------------------------------------------------------------------
TEST(EpaComprehensive, Depth_ShallowOverlap)
{
// offset 1.9 → depth 0.1
const auto r = solve(make_cube({ 0, 0, 0 }), make_cube({ 1.9f, 0, 0 }));
EXPECT_NEAR(r.depth, 0.1f, 1e-2f);
}
TEST(EpaComprehensive, Depth_QuarterOverlap)
{
// offset 1.5 → depth 0.5
const auto r = solve(make_cube({ 0, 0, 0 }), make_cube({ 1.5f, 0, 0 }));
EXPECT_NEAR(r.depth, 0.5f, 1e-2f);
}
TEST(EpaComprehensive, Depth_HalfOverlap)
{
// offset 1.0 → depth 1.0
const auto r = solve(make_cube({ 0, 0, 0 }), make_cube({ 1.0f, 0, 0 }));
EXPECT_NEAR(r.depth, 1.0f, 1e-2f);
}
TEST(EpaComprehensive, Depth_ThreeQuarterOverlap)
{
// offset 0.5 → depth 1.5
const auto r = solve(make_cube({ 0, 0, 0 }), make_cube({ 0.5f, 0, 0 }));
EXPECT_NEAR(r.depth, 1.5f, 1e-2f);
}
TEST(EpaComprehensive, Depth_AlongY_HalfOverlap)
{
const auto r = solve(make_cube({ 0, 0, 0 }), make_cube({ 0, 1.0f, 0 }));
EXPECT_NEAR(r.depth, 1.0f, 1e-2f);
}
TEST(EpaComprehensive, Depth_AlongZ_HalfOverlap)
{
const auto r = solve(make_cube({ 0, 0, 0 }), make_cube({ 0, 0, 1.0f }));
EXPECT_NEAR(r.depth, 1.0f, 1e-2f);
}
// ---------------------------------------------------------------------------
// Depth monotonicity — deeper overlap → larger depth
// ---------------------------------------------------------------------------
TEST(EpaComprehensive, DepthMonotonic_AlongX)
{
const float d1 = solve(make_cube({ 0, 0, 0 }), make_cube({ 1.9f, 0, 0 })).depth; // ~0.1
const float d2 = solve(make_cube({ 0, 0, 0 }), make_cube({ 1.5f, 0, 0 })).depth; // ~0.5
const float d3 = solve(make_cube({ 0, 0, 0 }), make_cube({ 1.0f, 0, 0 })).depth; // ~1.0
const float d4 = solve(make_cube({ 0, 0, 0 }), make_cube({ 0.5f, 0, 0 })).depth; // ~1.5
EXPECT_LT(d1, d2);
EXPECT_LT(d2, d3);
EXPECT_LT(d3, d4);
}
// ---------------------------------------------------------------------------
// Normal is a unit vector
// ---------------------------------------------------------------------------
TEST(EpaComprehensive, NormalIsUnit_AlongX)
{
const auto r = solve(make_cube({ 0, 0, 0 }), make_cube({ 0.5f, 0, 0 }));
EXPECT_NEAR(r.normal.dot(r.normal), 1.f, 1e-5f);
}
TEST(EpaComprehensive, NormalIsUnit_AlongY)
{
const auto r = solve(make_cube({ 0, 0, 0 }), make_cube({ 0, 1.2f, 0 }));
EXPECT_NEAR(r.normal.dot(r.normal), 1.f, 1e-5f);
}
TEST(EpaComprehensive, NormalIsUnit_AlongZ)
{
const auto r = solve(make_cube({ 0, 0, 0 }), make_cube({ 0, 0, 0.8f }));
EXPECT_NEAR(r.normal.dot(r.normal), 1.f, 1e-5f);
}
// ---------------------------------------------------------------------------
// Penetration vector = normal * depth
// ---------------------------------------------------------------------------
TEST(EpaComprehensive, PenetrationVectorLength_EqualsDepth)
{
const auto r = solve(make_cube({ 0, 0, 0 }), make_cube({ 0.5f, 0, 0 }));
const float pen_len = std::sqrt(r.penetration_vector.dot(r.penetration_vector));
EXPECT_NEAR(pen_len, r.depth, 1e-5f);
}
TEST(EpaComprehensive, PenetrationVectorDirection_ParallelToNormal)
{
const auto r = solve(make_cube({ 0, 0, 0 }), make_cube({ 0, 1.0f, 0 }));
// penetration_vector = normal * depth → cross product must be ~zero
const auto cross = r.penetration_vector.cross(r.normal);
EXPECT_NEAR(cross.dot(cross), 0.f, 1e-8f);
}
// ---------------------------------------------------------------------------
// Round-trip: applying penetration_vector separates the shapes
// ---------------------------------------------------------------------------
TEST(EpaComprehensive, RoundTrip_AlongX)
{
const auto a = make_cube({ 0, 0, 0 });
Mesh mesh_b{ k_cube_vbo, k_empty_ebo };
mesh_b.set_origin({ 0.5f, 0, 0 });
const auto b = Collider{ mesh_b };
const auto r = solve(a, b);
constexpr float margin = 1.f + 1e-3f;
// Move B along the penetration vector; it should separate from A
Mesh mesh_sep{ k_cube_vbo, k_empty_ebo };
mesh_sep.set_origin(mesh_b.get_origin() + r.penetration_vector * margin);
EXPECT_FALSE(Gjk::is_collide(a, Collider{ mesh_sep })) << "Applying pen vector must separate";
// Moving the wrong way must still collide
Mesh mesh_wrong{ k_cube_vbo, k_empty_ebo };
mesh_wrong.set_origin(mesh_b.get_origin() - r.penetration_vector * margin);
EXPECT_TRUE(Gjk::is_collide(a, Collider{ mesh_wrong })) << "Opposite direction must still collide";
}
TEST(EpaComprehensive, RoundTrip_AlongY)
{
const auto a = make_cube({ 0, 0, 0 });
Mesh mesh_b{ k_cube_vbo, k_empty_ebo };
mesh_b.set_origin({ 0, 0.8f, 0 });
const auto b = Collider{ mesh_b };
const auto r = solve(a, b);
constexpr float margin = 1.f + 1e-3f;
Mesh mesh_sep{ k_cube_vbo, k_empty_ebo };
mesh_sep.set_origin(mesh_b.get_origin() + r.penetration_vector * margin);
EXPECT_FALSE(Gjk::is_collide(a, Collider{ mesh_sep }));
Mesh mesh_wrong{ k_cube_vbo, k_empty_ebo };
mesh_wrong.set_origin(mesh_b.get_origin() - r.penetration_vector * margin);
EXPECT_TRUE(Gjk::is_collide(a, Collider{ mesh_wrong }));
}
TEST(EpaComprehensive, RoundTrip_AlongZ)
{
const auto a = make_cube({ 0, 0, 0 });
Mesh mesh_b{ k_cube_vbo, k_empty_ebo };
mesh_b.set_origin({ 0, 0, 1.2f });
const auto b = Collider{ mesh_b };
const auto r = solve(a, b);
constexpr float margin = 1.f + 1e-3f;
Mesh mesh_sep{ k_cube_vbo, k_empty_ebo };
mesh_sep.set_origin(mesh_b.get_origin() + r.penetration_vector * margin);
EXPECT_FALSE(Gjk::is_collide(a, Collider{ mesh_sep }));
}
// ---------------------------------------------------------------------------
// Symmetry — swapping A and B preserves depth
// ---------------------------------------------------------------------------
TEST(EpaComprehensive, Symmetry_DepthIsIndependentOfOrder)
{
const auto a = make_cube({ 0, 0, 0 });
const auto b = make_cube({ 0.5f, 0, 0 });
const float depth_ab = solve(a, b).depth;
const float depth_ba = solve(b, a).depth;
EXPECT_NEAR(depth_ab, depth_ba, 1e-2f);
}
TEST(EpaComprehensive, Symmetry_NormalsAreOpposite)
{
const auto a = make_cube({ 0, 0, 0 });
const auto b = make_cube({ 0.5f, 0, 0 });
const Vec3 n_ab = solve(a, b).normal;
const Vec3 n_ba = solve(b, a).normal;
// The normals should be anti-parallel: n_ab · n_ba ≈ -1
EXPECT_NEAR(n_ab.dot(n_ba), -1.f, 1e-3f);
}
// ---------------------------------------------------------------------------
// Asymmetric sizes
// ---------------------------------------------------------------------------
TEST(EpaComprehensive, LargeVsSmall_DepthCorrect)
{
// Big (half-ext 2) at origin, small (half-ext 0.5) at (2.0, 0, 0)
// Minkowski diff closest face in X at distance 0.5
const auto r = solve(make_cube({ 0, 0, 0 }, { 2, 2, 2 }), make_cube({ 2.0f, 0, 0 }, { 0.5f, 0.5f, 0.5f }));
EXPECT_NEAR(r.depth, 0.5f, 1e-2f);
EXPECT_NEAR(std::abs(r.normal.x), 1.f, 1e-3f);
}
TEST(EpaComprehensive, LargeVsSmall_RoundTrip)
{
const auto a = make_cube({ 0, 0, 0 }, { 2, 2, 2 });
Mesh mesh_b{ k_cube_vbo, k_empty_ebo, { 0.5f, 0.5f, 0.5f } };
mesh_b.set_origin({ 2.0f, 0, 0 });
const auto b = Collider{ mesh_b };
const auto r = solve(a, b);
constexpr float margin = 1.f + 1e-3f;
Mesh mesh_sep{ k_cube_vbo, k_empty_ebo, { 0.5f, 0.5f, 0.5f } };
mesh_sep.set_origin(mesh_b.get_origin() + r.penetration_vector * margin);
EXPECT_FALSE(Gjk::is_collide(a, Collider{ mesh_sep }));
}
// ---------------------------------------------------------------------------
// Memory resource variants
// ---------------------------------------------------------------------------
TEST(EpaComprehensive, MonotonicBuffer_ConvergesCorrectly)
{
const auto a = make_cube({ 0, 0, 0 });
const auto b = make_cube({ 0.5f, 0, 0 });
const auto [hit, simplex] = Gjk::is_collide_with_simplex_info(a, b);
ASSERT_TRUE(hit);
constexpr std::size_t k_buf = 32768;
alignas(std::max_align_t) char buf[k_buf];
std::pmr::monotonic_buffer_resource mr{ buf, k_buf, std::pmr::null_memory_resource() };
const auto r = Epa::solve(a, b, simplex, k_default_params, mr);
ASSERT_TRUE(r.has_value());
EXPECT_NEAR(r->depth, 1.5f, 1e-2f);
}
TEST(EpaComprehensive, MonotonicBuffer_MultipleReleaseCycles)
{
// Verify mr.release() correctly resets the buffer across multiple calls
const auto a = make_cube({ 0, 0, 0 });
const auto b = make_cube({ 0.5f, 0, 0 });
const auto [hit, simplex] = Gjk::is_collide_with_simplex_info(a, b);
ASSERT_TRUE(hit);
constexpr std::size_t k_buf = 32768;
alignas(std::max_align_t) char buf[k_buf];
std::pmr::monotonic_buffer_resource mr{ buf, k_buf, std::pmr::null_memory_resource() };
float first_depth = 0.f;
for (int i = 0; i < 5; ++i)
{
mr.release();
const auto r = Epa::solve(a, b, simplex, k_default_params, mr);
ASSERT_TRUE(r.has_value()) << "solve must converge on iteration " << i;
if (i == 0)
first_depth = r->depth;
else
EXPECT_NEAR(r->depth, first_depth, 1e-6f) << "depth must be deterministic";
}
}
TEST(EpaComprehensive, DefaultResource_ConvergesCorrectly)
{
const auto a = make_cube({ 0, 0, 0 });
const auto b = make_cube({ 1.0f, 0, 0 });
const auto [hit, simplex] = Gjk::is_collide_with_simplex_info(a, b);
ASSERT_TRUE(hit);
const auto r = Epa::solve(a, b, simplex);
ASSERT_TRUE(r.has_value());
EXPECT_NEAR(r->depth, 1.0f, 1e-2f);
}
// ---------------------------------------------------------------------------
// Tolerance sensitivity
// ---------------------------------------------------------------------------
TEST(EpaComprehensive, TighterTolerance_MoreAccurateDepth)
{
const auto a = make_cube({ 0, 0, 0 });
const auto b = make_cube({ 1.0f, 0, 0 });
const auto [hit, simplex] = Gjk::is_collide_with_simplex_info(a, b);
ASSERT_TRUE(hit);
const Epa::Params loose{ .max_iterations = 64, .tolerance = 1e-2f };
const Epa::Params tight{ .max_iterations = 64, .tolerance = 1e-5f };
const auto r_loose = Epa::solve(a, b, simplex, loose);
const auto r_tight = Epa::solve(a, b, simplex, tight);
ASSERT_TRUE(r_loose.has_value());
ASSERT_TRUE(r_tight.has_value());
// Tighter tolerance must yield a result at least as accurate
EXPECT_LE(std::abs(r_tight->depth - 1.0f), std::abs(r_loose->depth - 1.0f) + 1e-4f);
}
// ---------------------------------------------------------------------------
// Bookkeeping fields
// ---------------------------------------------------------------------------
TEST(EpaComprehensive, Bookkeeping_IterationsInBounds)
{
const auto a = make_cube({ 0, 0, 0 });
const auto b = make_cube({ 0.5f, 0, 0 });
const auto r = solve(a, b);
EXPECT_GT(r.iterations, 0);
EXPECT_LE(r.iterations, k_default_params.max_iterations);
}
TEST(EpaComprehensive, Bookkeeping_FacesAndVerticesGrow)
{
const auto a = make_cube({ 0, 0, 0 });
const auto b = make_cube({ 0.5f, 0, 0 });
const auto r = solve(a, b);
// Started with a tetrahedron (4 faces, 4 vertices); EPA must have expanded it
EXPECT_GE(r.num_faces, 4);
EXPECT_GE(r.num_vertices, 4);
}
TEST(EpaComprehensive, Bookkeeping_MaxIterationsRespected)
{
const auto a = make_cube({ 0, 0, 0 });
const auto b = make_cube({ 0.5f, 0, 0 });
const auto [hit, simplex] = Gjk::is_collide_with_simplex_info(a, b);
ASSERT_TRUE(hit);
constexpr Epa::Params tight{ .max_iterations = 3, .tolerance = 1e-10f };
const auto r = Epa::solve(a, b, simplex, tight);
// Must return something (fallback best-face path) and respect the cap
if (r.has_value())
EXPECT_LE(r->iterations, tight.max_iterations);
}
// ---------------------------------------------------------------------------
// Determinism
// ---------------------------------------------------------------------------
TEST(EpaComprehensive, Deterministic_SameResultOnRepeatedCalls)
{
const auto a = make_cube({ 0, 0, 0 });
const auto b = make_cube({ 0.7f, 0, 0 });
const auto [hit, simplex] = Gjk::is_collide_with_simplex_info(a, b);
ASSERT_TRUE(hit);
const auto first = Epa::solve(a, b, simplex);
ASSERT_TRUE(first.has_value());
for (int i = 0; i < 5; ++i)
{
const auto r = Epa::solve(a, b, simplex);
ASSERT_TRUE(r.has_value());
EXPECT_NEAR(r->depth, first->depth, 1e-6f);
EXPECT_NEAR(r->normal.x, first->normal.x, 1e-6f);
EXPECT_NEAR(r->normal.y, first->normal.y, 1e-6f);
EXPECT_NEAR(r->normal.z, first->normal.z, 1e-6f);
}
}

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);
}

View File

@@ -0,0 +1,402 @@
//
// Created by vlad on 3/1/2026.
//
#include <omath/linear_algebra/quaternion.hpp>
#include <cmath>
#include <gtest/gtest.h>
#include <numbers>
using namespace omath;
static constexpr float kEps = 1e-5f;
// ── Helpers ──────────────────────────────────────────────────────────────────
static void expect_quat_near(const Quaternion<float>& a, const Quaternion<float>& b, float eps = kEps)
{
EXPECT_NEAR(a.x, b.x, eps);
EXPECT_NEAR(a.y, b.y, eps);
EXPECT_NEAR(a.z, b.z, eps);
EXPECT_NEAR(a.w, b.w, eps);
}
static void expect_vec3_near(const Vector3<float>& a, const Vector3<float>& b, float eps = kEps)
{
EXPECT_NEAR(a.x, b.x, eps);
EXPECT_NEAR(a.y, b.y, eps);
EXPECT_NEAR(a.z, b.z, eps);
}
// ── Constructors ─────────────────────────────────────────────────────────────
TEST(Quaternion, DefaultConstructorIsIdentity)
{
constexpr Quaternion<float> q;
EXPECT_FLOAT_EQ(q.x, 0.f);
EXPECT_FLOAT_EQ(q.y, 0.f);
EXPECT_FLOAT_EQ(q.z, 0.f);
EXPECT_FLOAT_EQ(q.w, 1.f);
}
TEST(Quaternion, ValueConstructor)
{
constexpr Quaternion<float> q{1.f, 2.f, 3.f, 4.f};
EXPECT_FLOAT_EQ(q.x, 1.f);
EXPECT_FLOAT_EQ(q.y, 2.f);
EXPECT_FLOAT_EQ(q.z, 3.f);
EXPECT_FLOAT_EQ(q.w, 4.f);
}
TEST(Quaternion, DoubleInstantiation)
{
constexpr Quaternion<double> q{0.0, 0.0, 0.0, 1.0};
EXPECT_DOUBLE_EQ(q.w, 1.0);
}
// ── Equality ─────────────────────────────────────────────────────────────────
TEST(Quaternion, EqualityOperators)
{
constexpr Quaternion<float> a{1.f, 2.f, 3.f, 4.f};
constexpr Quaternion<float> b{1.f, 2.f, 3.f, 4.f};
constexpr Quaternion<float> c{1.f, 2.f, 3.f, 5.f};
EXPECT_TRUE(a == b);
EXPECT_FALSE(a == c);
EXPECT_FALSE(a != b);
EXPECT_TRUE(a != c);
}
// ── Arithmetic ───────────────────────────────────────────────────────────────
TEST(Quaternion, ScalarMultiply)
{
constexpr Quaternion<float> q{1.f, 2.f, 3.f, 4.f};
constexpr auto r = q * 2.f;
EXPECT_FLOAT_EQ(r.x, 2.f);
EXPECT_FLOAT_EQ(r.y, 4.f);
EXPECT_FLOAT_EQ(r.z, 6.f);
EXPECT_FLOAT_EQ(r.w, 8.f);
}
TEST(Quaternion, ScalarMultiplyAssign)
{
Quaternion<float> q{1.f, 2.f, 3.f, 4.f};
q *= 3.f;
EXPECT_FLOAT_EQ(q.x, 3.f);
EXPECT_FLOAT_EQ(q.y, 6.f);
EXPECT_FLOAT_EQ(q.z, 9.f);
EXPECT_FLOAT_EQ(q.w, 12.f);
}
TEST(Quaternion, Addition)
{
constexpr Quaternion<float> a{1.f, 2.f, 3.f, 4.f};
constexpr Quaternion<float> b{4.f, 3.f, 2.f, 1.f};
constexpr auto r = a + b;
EXPECT_FLOAT_EQ(r.x, 5.f);
EXPECT_FLOAT_EQ(r.y, 5.f);
EXPECT_FLOAT_EQ(r.z, 5.f);
EXPECT_FLOAT_EQ(r.w, 5.f);
}
TEST(Quaternion, AdditionAssign)
{
Quaternion<float> a{1.f, 0.f, 0.f, 0.f};
const Quaternion<float> b{0.f, 1.f, 0.f, 0.f};
a += b;
EXPECT_FLOAT_EQ(a.x, 1.f);
EXPECT_FLOAT_EQ(a.y, 1.f);
}
TEST(Quaternion, UnaryNegation)
{
constexpr Quaternion<float> q{1.f, -2.f, 3.f, -4.f};
constexpr auto r = -q;
EXPECT_FLOAT_EQ(r.x, -1.f);
EXPECT_FLOAT_EQ(r.y, 2.f);
EXPECT_FLOAT_EQ(r.z, -3.f);
EXPECT_FLOAT_EQ(r.w, 4.f);
}
// ── Hamilton product ──────────────────────────────────────────────────────────
TEST(Quaternion, MultiplyByIdentityIsNoop)
{
constexpr Quaternion<float> identity;
constexpr Quaternion<float> q{0.5f, 0.5f, 0.5f, 0.5f};
expect_quat_near(q * identity, q);
expect_quat_near(identity * q, q);
}
TEST(Quaternion, MultiplyAssign)
{
constexpr Quaternion<float> identity;
Quaternion<float> q{0.5f, 0.5f, 0.5f, 0.5f};
q *= identity;
expect_quat_near(q, {0.5f, 0.5f, 0.5f, 0.5f});
}
TEST(Quaternion, MultiplyKnownResult)
{
// i * j = k → (1,0,0,0) * (0,1,0,0) = (0,0,1,0)
constexpr Quaternion<float> i{1.f, 0.f, 0.f, 0.f};
constexpr Quaternion<float> j{0.f, 1.f, 0.f, 0.f};
constexpr auto k = i * j;
EXPECT_FLOAT_EQ(k.x, 0.f);
EXPECT_FLOAT_EQ(k.y, 0.f);
EXPECT_FLOAT_EQ(k.z, 1.f);
EXPECT_FLOAT_EQ(k.w, 0.f);
}
TEST(Quaternion, MultiplyByInverseGivesIdentity)
{
const Quaternion<float> q = Quaternion<float>::from_axis_angle({0.f, 0.f, 1.f},
std::numbers::pi_v<float> / 3.f);
const auto result = q * q.inverse();
expect_quat_near(result, Quaternion<float>{});
}
// ── Conjugate ────────────────────────────────────────────────────────────────
TEST(Quaternion, Conjugate)
{
constexpr Quaternion<float> q{1.f, 2.f, 3.f, 4.f};
constexpr auto c = q.conjugate();
EXPECT_FLOAT_EQ(c.x, -1.f);
EXPECT_FLOAT_EQ(c.y, -2.f);
EXPECT_FLOAT_EQ(c.z, -3.f);
EXPECT_FLOAT_EQ(c.w, 4.f);
}
TEST(Quaternion, ConjugateOfIdentityIsIdentity)
{
constexpr Quaternion<float> id;
constexpr auto c = id.conjugate();
EXPECT_FLOAT_EQ(c.x, 0.f);
EXPECT_FLOAT_EQ(c.y, 0.f);
EXPECT_FLOAT_EQ(c.z, 0.f);
EXPECT_FLOAT_EQ(c.w, 1.f);
}
// ── Dot / length ─────────────────────────────────────────────────────────────
TEST(Quaternion, Dot)
{
constexpr Quaternion<float> a{1.f, 0.f, 0.f, 0.f};
constexpr Quaternion<float> b{0.f, 1.f, 0.f, 0.f};
EXPECT_FLOAT_EQ(a.dot(b), 0.f);
EXPECT_FLOAT_EQ(a.dot(a), 1.f);
}
TEST(Quaternion, LengthSqrIdentity)
{
constexpr Quaternion<float> id;
EXPECT_FLOAT_EQ(id.length_sqr(), 1.f);
}
TEST(Quaternion, LengthSqrGeneral)
{
constexpr Quaternion<float> q{1.f, 2.f, 3.f, 4.f};
EXPECT_FLOAT_EQ(q.length_sqr(), 30.f);
}
TEST(Quaternion, LengthIdentity)
{
const Quaternion<float> id;
EXPECT_NEAR(id.length(), 1.f, kEps);
}
TEST(Quaternion, Normalized)
{
const Quaternion<float> q{1.f, 1.f, 1.f, 1.f};
const auto n = q.normalized();
EXPECT_NEAR(n.length(), 1.f, kEps);
EXPECT_NEAR(n.x, 0.5f, kEps);
EXPECT_NEAR(n.y, 0.5f, kEps);
EXPECT_NEAR(n.z, 0.5f, kEps);
EXPECT_NEAR(n.w, 0.5f, kEps);
}
TEST(Quaternion, NormalizedOfZeroLengthReturnsSelf)
{
// length_sqr = 0 would be UB, but zero-vector part + zero w is degenerate;
// we just verify the guard branch (divides by zero) doesn't crash by
// keeping length > 0 via the default constructor path.
const Quaternion<float> unit;
const auto n = unit.normalized();
expect_quat_near(n, unit);
}
// ── Inverse ───────────────────────────────────────────────────────────────────
TEST(Quaternion, InverseOfUnitIsConjugate)
{
const Quaternion<float> q = Quaternion<float>::from_axis_angle({1.f, 0.f, 0.f},
std::numbers::pi_v<float> / 4.f);
const auto inv = q.inverse();
const auto conj = q.conjugate();
expect_quat_near(inv, conj);
}
// ── from_axis_angle ──────────────────────────────────────────────────────────
TEST(Quaternion, FromAxisAngleZeroAngleIsIdentity)
{
const auto q = Quaternion<float>::from_axis_angle({1.f, 0.f, 0.f}, 0.f);
EXPECT_NEAR(q.x, 0.f, kEps);
EXPECT_NEAR(q.y, 0.f, kEps);
EXPECT_NEAR(q.z, 0.f, kEps);
EXPECT_NEAR(q.w, 1.f, kEps);
}
TEST(Quaternion, FromAxisAngle90DegZ)
{
const float half_pi = std::numbers::pi_v<float> / 2.f;
const auto q = Quaternion<float>::from_axis_angle({0.f, 0.f, 1.f}, half_pi);
const float s = std::sin(half_pi / 2.f);
const float c = std::cos(half_pi / 2.f);
EXPECT_NEAR(q.x, 0.f, kEps);
EXPECT_NEAR(q.y, 0.f, kEps);
EXPECT_NEAR(q.z, s, kEps);
EXPECT_NEAR(q.w, c, kEps);
}
// ── rotate ───────────────────────────────────────────────────────────────────
TEST(Quaternion, RotateByIdentityIsNoop)
{
constexpr Quaternion<float> id;
constexpr Vector3<float> v{1.f, 2.f, 3.f};
const auto r = id.rotate(v);
expect_vec3_near(r, v);
}
TEST(Quaternion, Rotate90DegAroundZ)
{
// Rotating (1,0,0) by 90° around Z should give (0,1,0)
const auto q = Quaternion<float>::from_axis_angle({0.f, 0.f, 1.f}, std::numbers::pi_v<float> / 2.f);
const auto r = q.rotate({1.f, 0.f, 0.f});
expect_vec3_near(r, {0.f, 1.f, 0.f});
}
TEST(Quaternion, Rotate180DegAroundY)
{
// Rotating (1,0,0) by 180° around Y should give (-1,0,0)
const auto q = Quaternion<float>::from_axis_angle({0.f, 1.f, 0.f}, std::numbers::pi_v<float>);
const auto r = q.rotate({1.f, 0.f, 0.f});
expect_vec3_near(r, {-1.f, 0.f, 0.f});
}
TEST(Quaternion, Rotate90DegAroundX)
{
// Rotating (0,1,0) by 90° around X should give (0,0,1)
const auto q = Quaternion<float>::from_axis_angle({1.f, 0.f, 0.f}, std::numbers::pi_v<float> / 2.f);
const auto r = q.rotate({0.f, 1.f, 0.f});
expect_vec3_near(r, {0.f, 0.f, 1.f});
}
// ── to_rotation_matrix3 ───────────────────────────────────────────────────────
TEST(Quaternion, RotationMatrix3FromIdentityIsIdentityMatrix)
{
constexpr Quaternion<float> id;
constexpr auto m = id.to_rotation_matrix3();
for (size_t i = 0; i < 3; ++i)
for (size_t j = 0; j < 3; ++j)
EXPECT_NEAR(m.at(i, j), i == j ? 1.f : 0.f, kEps);
}
TEST(Quaternion, RotationMatrix3From90DegZ)
{
// Expected: | 0 -1 0 |
// | 1 0 0 |
// | 0 0 1 |
const auto q = Quaternion<float>::from_axis_angle({0.f, 0.f, 1.f}, std::numbers::pi_v<float> / 2.f);
const auto m = q.to_rotation_matrix3();
EXPECT_NEAR(m.at(0, 0), 0.f, kEps);
EXPECT_NEAR(m.at(0, 1), -1.f, kEps);
EXPECT_NEAR(m.at(0, 2), 0.f, kEps);
EXPECT_NEAR(m.at(1, 0), 1.f, kEps);
EXPECT_NEAR(m.at(1, 1), 0.f, kEps);
EXPECT_NEAR(m.at(1, 2), 0.f, kEps);
EXPECT_NEAR(m.at(2, 0), 0.f, kEps);
EXPECT_NEAR(m.at(2, 1), 0.f, kEps);
EXPECT_NEAR(m.at(2, 2), 1.f, kEps);
}
TEST(Quaternion, RotationMatrix3ConsistentWithRotate)
{
// Matrix-vector multiply must agree with the rotate() method
const auto q = Quaternion<float>::from_axis_angle({1.f, 1.f, 0.f}, std::numbers::pi_v<float> / 3.f);
const Vector3<float> v{2.f, -1.f, 0.5f};
const auto rotated = q.rotate(v);
const auto m = q.to_rotation_matrix3();
// manual mat-vec multiply (row-major)
const float rx = m.at(0, 0) * v.x + m.at(0, 1) * v.y + m.at(0, 2) * v.z;
const float ry = m.at(1, 0) * v.x + m.at(1, 1) * v.y + m.at(1, 2) * v.z;
const float rz = m.at(2, 0) * v.x + m.at(2, 1) * v.y + m.at(2, 2) * v.z;
EXPECT_NEAR(rotated.x, rx, kEps);
EXPECT_NEAR(rotated.y, ry, kEps);
EXPECT_NEAR(rotated.z, rz, kEps);
}
// ── to_rotation_matrix4 ───────────────────────────────────────────────────────
TEST(Quaternion, RotationMatrix4FromIdentityIsIdentityMatrix)
{
constexpr Quaternion<float> id;
constexpr auto m = id.to_rotation_matrix4();
for (size_t i = 0; i < 4; ++i)
for (size_t j = 0; j < 4; ++j)
EXPECT_NEAR(m.at(i, j), i == j ? 1.f : 0.f, kEps);
}
TEST(Quaternion, RotationMatrix4HomogeneousRowAndColumn)
{
const auto q = Quaternion<float>::from_axis_angle({1.f, 0.f, 0.f}, std::numbers::pi_v<float> / 5.f);
const auto m = q.to_rotation_matrix4();
// Last row and last column must be (0,0,0,1)
for (size_t i = 0; i < 3; ++i)
{
EXPECT_NEAR(m.at(3, i), 0.f, kEps);
EXPECT_NEAR(m.at(i, 3), 0.f, kEps);
}
EXPECT_NEAR(m.at(3, 3), 1.f, kEps);
}
TEST(Quaternion, RotationMatrix4Upper3x3MatchesMatrix3)
{
const auto q = Quaternion<float>::from_axis_angle({0.f, 1.f, 0.f}, std::numbers::pi_v<float> / 7.f);
const auto m3 = q.to_rotation_matrix3();
const auto m4 = q.to_rotation_matrix4();
for (size_t i = 0; i < 3; ++i)
for (size_t j = 0; j < 3; ++j)
EXPECT_NEAR(m4.at(i, j), m3.at(i, j), kEps);
}
// ── as_array ──────────────────────────────────────────────────────────────────
TEST(Quaternion, AsArray)
{
constexpr Quaternion<float> q{1.f, 2.f, 3.f, 4.f};
constexpr auto arr = q.as_array();
EXPECT_FLOAT_EQ(arr[0], 1.f);
EXPECT_FLOAT_EQ(arr[1], 2.f);
EXPECT_FLOAT_EQ(arr[2], 3.f);
EXPECT_FLOAT_EQ(arr[3], 4.f);
}
// ── std::formatter ────────────────────────────────────────────────────────────
TEST(Quaternion, Formatter)
{
const Quaternion<float> q{1.f, 2.f, 3.f, 4.f};
const auto s = std::format("{}", q);
EXPECT_EQ(s, "[1, 2, 3, 4]");
}

96
tests/lua/color_tests.lua Normal file
View File

@@ -0,0 +1,96 @@
local function approx(a, b, eps) return math.abs(a - b) < (eps or 1e-4) end
function Color_Constructor_float()
local c = omath.Color.new(1, 0.5, 0.25, 1)
assert(approx(c.r, 1) and approx(c.g, 0.5) and approx(c.b, 0.25) and approx(c.a, 1))
end
function Color_Constructor_default()
local c = omath.Color.new()
assert(c ~= nil)
end
function Color_Constructor_clamping()
local c = omath.Color.new(2, -1, 0.5, 1)
assert(approx(c.r, 1) and approx(c.g, 0) and approx(c.b, 0.5))
end
function Color_from_rgba()
local c = omath.Color.from_rgba(255, 128, 0, 255)
assert(approx(c.r, 1) and approx(c.g, 128/255) and approx(c.b, 0) and approx(c.a, 1))
end
function Color_from_hsv_components()
local c = omath.Color.from_hsv(0, 1, 1)
assert(approx(c.r, 1) and approx(c.g, 0) and approx(c.b, 0))
end
function Color_from_hsv_struct()
local hsv = omath.Hsv.new()
hsv.hue = 0
hsv.saturation = 1
hsv.value = 1
local c = omath.Color.from_hsv(hsv)
assert(approx(c.r, 1) and approx(c.g, 0) and approx(c.b, 0))
end
function Color_red()
local c = omath.Color.red()
assert(approx(c.r, 1) and approx(c.g, 0) and approx(c.b, 0) and approx(c.a, 1))
end
function Color_green()
local c = omath.Color.green()
assert(approx(c.r, 0) and approx(c.g, 1) and approx(c.b, 0) and approx(c.a, 1))
end
function Color_blue()
local c = omath.Color.blue()
assert(approx(c.r, 0) and approx(c.g, 0) and approx(c.b, 1) and approx(c.a, 1))
end
function Color_to_hsv()
local hsv = omath.Color.red():to_hsv()
assert(approx(hsv.hue, 0) and approx(hsv.saturation, 1) and approx(hsv.value, 1))
end
function Color_set_hue()
local c = omath.Color.red()
c:set_hue(1/3)
assert(approx(c.g, 1, 1e-3))
end
function Color_set_saturation()
local c = omath.Color.red()
c:set_saturation(0)
assert(approx(c.r, c.g) and approx(c.g, c.b))
end
function Color_set_value()
local c = omath.Color.red()
c:set_value(0)
assert(approx(c.r, 0) and approx(c.g, 0) and approx(c.b, 0))
end
function Color_blend()
local c = omath.Color.red():blend(omath.Color.blue(), 0.5)
assert(approx(c.r, 0.5) and approx(c.b, 0.5))
end
function Color_blend_clamped_ratio()
local c = omath.Color.red():blend(omath.Color.blue(), 2.0)
assert(approx(c.r, 0) and approx(c.b, 1))
end
function Color_to_string()
local s = tostring(omath.Color.red())
assert(s == "[r:255, g:0, b:0, a:255]")
end
function Hsv_fields()
local hsv = omath.Hsv.new()
hsv.hue = 0.5
hsv.saturation = 0.8
hsv.value = 0.9
assert(approx(hsv.hue, 0.5) and approx(hsv.saturation, 0.8) and approx(hsv.value, 0.9))
end

View File

@@ -0,0 +1,197 @@
local function approx(a, b, eps) return math.abs(a - b) < (eps or 1e-4) end
local function make_camera()
local pos = omath.Vec3.new(0, 0, 0)
local pitch = omath.source.PitchAngle.from_degrees(0)
local yaw = omath.source.YawAngle.from_degrees(0)
local roll = omath.source.RollAngle.from_degrees(0)
local angles = omath.source.ViewAngles.new(pitch, yaw, roll)
local vp = omath.opengl.ViewPort.new(1920, 1080)
local fov = omath.source.FieldOfView.from_degrees(90)
return omath.source.Camera.new(pos, angles, vp, fov, 0.1, 1000)
end
-- PitchAngle
function Source_PitchAngle_from_degrees()
assert(omath.source.PitchAngle.from_degrees(45):as_degrees() == 45)
end
function Source_PitchAngle_clamping_max()
assert(omath.source.PitchAngle.from_degrees(100):as_degrees() == 89)
end
function Source_PitchAngle_clamping_min()
assert(omath.source.PitchAngle.from_degrees(-100):as_degrees() == -89)
end
function Source_PitchAngle_from_radians()
assert(approx(omath.source.PitchAngle.from_radians(math.pi / 4):as_degrees(), 45))
end
function Source_PitchAngle_as_radians()
assert(approx(omath.source.PitchAngle.from_degrees(0):as_radians(), 0))
end
function Source_PitchAngle_sin()
assert(approx(omath.source.PitchAngle.from_degrees(30):sin(), 0.5))
end
function Source_PitchAngle_cos()
assert(approx(omath.source.PitchAngle.from_degrees(60):cos(), 0.5))
end
function Source_PitchAngle_tan()
assert(approx(omath.source.PitchAngle.from_degrees(45):tan(), 1.0))
end
function Source_PitchAngle_addition()
local c = omath.source.PitchAngle.from_degrees(20) + omath.source.PitchAngle.from_degrees(15)
assert(c:as_degrees() == 35)
end
function Source_PitchAngle_addition_clamped()
local c = omath.source.PitchAngle.from_degrees(80) + omath.source.PitchAngle.from_degrees(20)
assert(c:as_degrees() == 89)
end
function Source_PitchAngle_subtraction()
local c = omath.source.PitchAngle.from_degrees(50) - omath.source.PitchAngle.from_degrees(20)
assert(c:as_degrees() == 30)
end
function Source_PitchAngle_unary_minus()
assert((-omath.source.PitchAngle.from_degrees(45)):as_degrees() == -45)
end
function Source_PitchAngle_equal_to()
local a = omath.source.PitchAngle.from_degrees(45)
assert(a == omath.source.PitchAngle.from_degrees(45))
assert(not (a == omath.source.PitchAngle.from_degrees(30)))
end
function Source_PitchAngle_to_string()
assert(tostring(omath.source.PitchAngle.from_degrees(45)) == "45deg")
end
-- YawAngle
function Source_YawAngle_from_degrees()
assert(omath.source.YawAngle.from_degrees(90):as_degrees() == 90)
end
function Source_YawAngle_normalization()
assert(approx(omath.source.YawAngle.from_degrees(200):as_degrees(), -160))
end
-- RollAngle
function Source_RollAngle_from_degrees()
assert(omath.source.RollAngle.from_degrees(45):as_degrees() == 45)
end
-- FieldOfView
function Source_FieldOfView_from_degrees()
assert(omath.source.FieldOfView.from_degrees(90):as_degrees() == 90)
end
function Source_FieldOfView_clamping()
assert(omath.source.FieldOfView.from_degrees(200):as_degrees() == 180)
end
-- ViewAngles
function Source_ViewAngles_new()
local angles = omath.source.ViewAngles.new(
omath.source.PitchAngle.from_degrees(30),
omath.source.YawAngle.from_degrees(90),
omath.source.RollAngle.from_degrees(0))
assert(angles.pitch:as_degrees() == 30)
assert(angles.yaw:as_degrees() == 90)
assert(angles.roll:as_degrees() == 0)
end
function Source_ViewAngles_field_mutation()
local angles = omath.source.ViewAngles.new(
omath.source.PitchAngle.from_degrees(0),
omath.source.YawAngle.from_degrees(0),
omath.source.RollAngle.from_degrees(0))
angles.pitch = omath.source.PitchAngle.from_degrees(45)
assert(angles.pitch:as_degrees() == 45)
end
-- Camera
function Source_Camera_constructor()
assert(make_camera() ~= nil)
end
function Source_Camera_get_set_origin()
local cam = make_camera()
cam:set_origin(omath.Vec3.new(1, 2, 3))
local o = cam:get_origin()
assert(approx(o.x, 1) and approx(o.y, 2) and approx(o.z, 3))
end
function Source_Camera_get_set_near_plane()
local cam = make_camera()
cam:set_near_plane(0.5)
assert(approx(cam:get_near_plane(), 0.5))
end
function Source_Camera_get_set_far_plane()
local cam = make_camera()
cam:set_far_plane(500)
assert(approx(cam:get_far_plane(), 500))
end
function Source_Camera_get_set_fov()
local cam = make_camera()
cam:set_field_of_view(omath.source.FieldOfView.from_degrees(60))
assert(approx(cam:get_field_of_view():as_degrees(), 60))
end
function Source_Camera_get_set_view_angles()
local cam = make_camera()
cam:set_view_angles(omath.source.ViewAngles.new(
omath.source.PitchAngle.from_degrees(30),
omath.source.YawAngle.from_degrees(90),
omath.source.RollAngle.from_degrees(0)))
assert(approx(cam:get_view_angles().pitch:as_degrees(), 30))
assert(approx(cam:get_view_angles().yaw:as_degrees(), 90))
end
function Source_Camera_look_at()
local cam = make_camera()
cam:look_at(omath.Vec3.new(10, 0, 0))
assert(cam:get_view_angles() ~= nil)
end
function Source_Camera_get_forward()
local fwd = make_camera():get_forward()
assert(approx(fwd:length(), 1.0))
end
function Source_Camera_get_right()
assert(approx(make_camera():get_right():length(), 1.0))
end
function Source_Camera_get_up()
assert(approx(make_camera():get_up():length(), 1.0))
end
function Source_Camera_world_to_screen_success()
local cam = make_camera()
cam:look_at(omath.Vec3.new(1, 0, 0))
local screen, err = cam:world_to_screen(omath.Vec3.new(5, 0, 0))
assert(screen ~= nil, "expected screen pos, got: " .. tostring(err))
end
function Source_Camera_world_to_screen_error()
local cam = make_camera()
cam:look_at(omath.Vec3.new(1, 0, 0))
local screen, err = cam:world_to_screen(omath.Vec3.new(-100, 0, 0))
assert(screen == nil and err ~= nil)
end
function Source_Camera_screen_to_world()
local cam = make_camera()
cam:look_at(omath.Vec3.new(1, 0, 0))
local world, err = cam:screen_to_world(omath.Vec3.new(960, 540, 1))
assert(world ~= nil, "expected world pos, got: " .. tostring(err))
end

View File

@@ -0,0 +1,82 @@
local function approx(a, b, eps) return math.abs(a - b) < (eps or 1e-5) end
function Triangle_Constructor_default()
local t = omath.Triangle.new()
assert(t.vertex1.x == 0 and t.vertex1.y == 0 and t.vertex1.z == 0)
assert(t.vertex2.x == 0 and t.vertex2.y == 0 and t.vertex2.z == 0)
assert(t.vertex3.x == 0 and t.vertex3.y == 0 and t.vertex3.z == 0)
end
function Triangle_Constructor_vertices()
local v1 = omath.Vec3.new(1, 0, 0)
local v2 = omath.Vec3.new(0, 1, 0)
local v3 = omath.Vec3.new(0, 0, 1)
local t = omath.Triangle.new(v1, v2, v3)
assert(t.vertex1.x == 1 and t.vertex1.y == 0 and t.vertex1.z == 0)
assert(t.vertex2.x == 0 and t.vertex2.y == 1 and t.vertex2.z == 0)
assert(t.vertex3.x == 0 and t.vertex3.y == 0 and t.vertex3.z == 1)
end
function Triangle_Vertex_mutation()
local t = omath.Triangle.new()
t.vertex1 = omath.Vec3.new(5, 6, 7)
assert(t.vertex1.x == 5 and t.vertex1.y == 6 and t.vertex1.z == 7)
end
-- Right triangle: v1=(0,3,0), v2=(0,0,0), v3=(4,0,0) — sides 3, 4, hypot 5
function Triangle_SideALength()
local t = omath.Triangle.new(omath.Vec3.new(0, 3, 0), omath.Vec3.new(0, 0, 0), omath.Vec3.new(4, 0, 0))
assert(approx(t:side_a_length(), 3.0))
end
function Triangle_SideBLength()
local t = omath.Triangle.new(omath.Vec3.new(0, 3, 0), omath.Vec3.new(0, 0, 0), omath.Vec3.new(4, 0, 0))
assert(approx(t:side_b_length(), 4.0))
end
function Triangle_Hypot()
local t = omath.Triangle.new(omath.Vec3.new(0, 3, 0), omath.Vec3.new(0, 0, 0), omath.Vec3.new(4, 0, 0))
assert(approx(t:hypot(), 5.0))
end
function Triangle_SideAVector()
local t = omath.Triangle.new(omath.Vec3.new(0, 3, 0), omath.Vec3.new(0, 0, 0), omath.Vec3.new(4, 0, 0))
local a = t:side_a_vector()
assert(approx(a.x, 0) and approx(a.y, 3) and approx(a.z, 0))
end
function Triangle_SideBVector()
local t = omath.Triangle.new(omath.Vec3.new(0, 3, 0), omath.Vec3.new(0, 0, 0), omath.Vec3.new(4, 0, 0))
local b = t:side_b_vector()
assert(approx(b.x, 4) and approx(b.y, 0) and approx(b.z, 0))
end
function Triangle_IsRectangular_true()
local t = omath.Triangle.new(omath.Vec3.new(0, 3, 0), omath.Vec3.new(0, 0, 0), omath.Vec3.new(4, 0, 0))
assert(t:is_rectangular() == true)
end
function Triangle_IsRectangular_false()
-- equilateral-ish triangle, not rectangular
local t = omath.Triangle.new(omath.Vec3.new(0, 1, 0), omath.Vec3.new(-1, 0, 0), omath.Vec3.new(1, 0, 0))
assert(t:is_rectangular() == false)
end
function Triangle_MidPoint()
local t = omath.Triangle.new(omath.Vec3.new(3, 0, 0), omath.Vec3.new(0, 3, 0), omath.Vec3.new(0, 0, 3))
local m = t:mid_point()
assert(approx(m.x, 1.0) and approx(m.y, 1.0) and approx(m.z, 1.0))
end
function Triangle_CalculateNormal()
-- flat triangle in XY plane — normal should be (0, 0, 1)
local t = omath.Triangle.new(omath.Vec3.new(0, 1, 0), omath.Vec3.new(0, 0, 0), omath.Vec3.new(1, 0, 0))
local n = t:calculate_normal()
assert(approx(n.x, 0) and approx(n.y, 0) and approx(n.z, 1))
end
function Triangle_ToString()
local t = omath.Triangle.new(omath.Vec3.new(1, 0, 0), omath.Vec3.new(0, 1, 0), omath.Vec3.new(0, 0, 1))
local s = tostring(t)
assert(s == "Triangle((1, 0, 0), (0, 1, 0), (0, 0, 1))")
end

View File

@@ -0,0 +1,51 @@
//
// Created by orange on 08.03.2026.
//
#include <gtest/gtest.h>
#include <lua.hpp>
#include <omath/lua/lua.hpp>
class LuaColor : public ::testing::Test
{
protected:
lua_State* L = nullptr;
void SetUp() override
{
L = luaL_newstate();
luaL_openlibs(L);
omath::lua::LuaInterpreter::register_lib(L);
if (luaL_dofile(L, LUA_SCRIPTS_DIR "/color_tests.lua") != LUA_OK)
FAIL() << lua_tostring(L, -1);
}
void TearDown() override { lua_close(L); }
void check(const char* func_name)
{
lua_getglobal(L, func_name);
if (lua_pcall(L, 0, 0, 0) != LUA_OK)
{
FAIL() << lua_tostring(L, -1);
lua_pop(L, 1);
}
}
};
TEST_F(LuaColor, Constructor_float) { check("Color_Constructor_float"); }
TEST_F(LuaColor, Constructor_default) { check("Color_Constructor_default"); }
TEST_F(LuaColor, Constructor_clamping) { check("Color_Constructor_clamping"); }
TEST_F(LuaColor, from_rgba) { check("Color_from_rgba"); }
TEST_F(LuaColor, from_hsv_components) { check("Color_from_hsv_components"); }
TEST_F(LuaColor, from_hsv_struct) { check("Color_from_hsv_struct"); }
TEST_F(LuaColor, red) { check("Color_red"); }
TEST_F(LuaColor, green) { check("Color_green"); }
TEST_F(LuaColor, blue) { check("Color_blue"); }
TEST_F(LuaColor, to_hsv) { check("Color_to_hsv"); }
TEST_F(LuaColor, set_hue) { check("Color_set_hue"); }
TEST_F(LuaColor, set_saturation) { check("Color_set_saturation"); }
TEST_F(LuaColor, set_value) { check("Color_set_value"); }
TEST_F(LuaColor, blend) { check("Color_blend"); }
TEST_F(LuaColor, blend_clamped_ratio) { check("Color_blend_clamped_ratio"); }
TEST_F(LuaColor, to_string) { check("Color_to_string"); }
TEST_F(LuaColor, Hsv_fields) { check("Hsv_fields"); }

View File

@@ -0,0 +1,79 @@
//
// Created by orange on 07.03.2026.
//
#include <gtest/gtest.h>
#include <lua.hpp>
#include <omath/lua/lua.hpp>
class LuaSourceEngine : public ::testing::Test
{
protected:
lua_State* L = nullptr;
void SetUp() override
{
L = luaL_newstate();
luaL_openlibs(L);
omath::lua::LuaInterpreter::register_lib(L);
if (luaL_dofile(L, LUA_SCRIPTS_DIR "/source_engine_tests.lua") != LUA_OK)
FAIL() << lua_tostring(L, -1);
}
void TearDown() override { lua_close(L); }
void check(const char* func_name)
{
lua_getglobal(L, func_name);
if (lua_pcall(L, 0, 0, 0) != LUA_OK)
{
FAIL() << lua_tostring(L, -1);
lua_pop(L, 1);
}
}
};
// PitchAngle
TEST_F(LuaSourceEngine, PitchAngle_from_degrees) { check("Source_PitchAngle_from_degrees"); }
TEST_F(LuaSourceEngine, PitchAngle_clamping_max) { check("Source_PitchAngle_clamping_max"); }
TEST_F(LuaSourceEngine, PitchAngle_clamping_min) { check("Source_PitchAngle_clamping_min"); }
TEST_F(LuaSourceEngine, PitchAngle_from_radians) { check("Source_PitchAngle_from_radians"); }
TEST_F(LuaSourceEngine, PitchAngle_as_radians) { check("Source_PitchAngle_as_radians"); }
TEST_F(LuaSourceEngine, PitchAngle_sin) { check("Source_PitchAngle_sin"); }
TEST_F(LuaSourceEngine, PitchAngle_cos) { check("Source_PitchAngle_cos"); }
TEST_F(LuaSourceEngine, PitchAngle_tan) { check("Source_PitchAngle_tan"); }
TEST_F(LuaSourceEngine, PitchAngle_addition) { check("Source_PitchAngle_addition"); }
TEST_F(LuaSourceEngine, PitchAngle_addition_clamped) { check("Source_PitchAngle_addition_clamped"); }
TEST_F(LuaSourceEngine, PitchAngle_subtraction) { check("Source_PitchAngle_subtraction"); }
TEST_F(LuaSourceEngine, PitchAngle_unary_minus) { check("Source_PitchAngle_unary_minus"); }
TEST_F(LuaSourceEngine, PitchAngle_equal_to) { check("Source_PitchAngle_equal_to"); }
TEST_F(LuaSourceEngine, PitchAngle_to_string) { check("Source_PitchAngle_to_string"); }
// YawAngle
TEST_F(LuaSourceEngine, YawAngle_from_degrees) { check("Source_YawAngle_from_degrees"); }
TEST_F(LuaSourceEngine, YawAngle_normalization) { check("Source_YawAngle_normalization"); }
// RollAngle
TEST_F(LuaSourceEngine, RollAngle_from_degrees) { check("Source_RollAngle_from_degrees"); }
// FieldOfView
TEST_F(LuaSourceEngine, FieldOfView_from_degrees) { check("Source_FieldOfView_from_degrees"); }
TEST_F(LuaSourceEngine, FieldOfView_clamping) { check("Source_FieldOfView_clamping"); }
// ViewAngles
TEST_F(LuaSourceEngine, ViewAngles_new) { check("Source_ViewAngles_new"); }
TEST_F(LuaSourceEngine, ViewAngles_field_mutation) { check("Source_ViewAngles_field_mutation"); }
// Camera
TEST_F(LuaSourceEngine, Camera_constructor) { check("Source_Camera_constructor"); }
TEST_F(LuaSourceEngine, Camera_get_set_origin) { check("Source_Camera_get_set_origin"); }
TEST_F(LuaSourceEngine, Camera_get_set_near_plane) { check("Source_Camera_get_set_near_plane"); }
TEST_F(LuaSourceEngine, Camera_get_set_far_plane) { check("Source_Camera_get_set_far_plane"); }
TEST_F(LuaSourceEngine, Camera_get_set_fov) { check("Source_Camera_get_set_fov"); }
TEST_F(LuaSourceEngine, Camera_get_set_view_angles) { check("Source_Camera_get_set_view_angles"); }
TEST_F(LuaSourceEngine, Camera_look_at) { check("Source_Camera_look_at"); }
TEST_F(LuaSourceEngine, Camera_get_forward) { check("Source_Camera_get_forward"); }
TEST_F(LuaSourceEngine, Camera_get_right) { check("Source_Camera_get_right"); }
TEST_F(LuaSourceEngine, Camera_get_up) { check("Source_Camera_get_up"); }
TEST_F(LuaSourceEngine, Camera_world_to_screen_success) { check("Source_Camera_world_to_screen_success"); }
TEST_F(LuaSourceEngine, Camera_world_to_screen_error) { check("Source_Camera_world_to_screen_error"); }
TEST_F(LuaSourceEngine, Camera_screen_to_world) { check("Source_Camera_screen_to_world"); }

View File

@@ -0,0 +1,47 @@
//
// Created by orange on 10.03.2026.
//
#include <gtest/gtest.h>
#include <lua.hpp>
#include <omath/lua/lua.hpp>
class LuaTriangle : public ::testing::Test
{
protected:
lua_State* L = nullptr;
void SetUp() override
{
L = luaL_newstate();
luaL_openlibs(L);
omath::lua::LuaInterpreter::register_lib(L);
if (luaL_dofile(L, LUA_SCRIPTS_DIR "/triangle_tests.lua") != LUA_OK)
FAIL() << lua_tostring(L, -1);
}
void TearDown() override { lua_close(L); }
void check(const char* func_name)
{
lua_getglobal(L, func_name);
if (lua_pcall(L, 0, 0, 0) != LUA_OK)
{
FAIL() << lua_tostring(L, -1);
lua_pop(L, 1);
}
}
};
TEST_F(LuaTriangle, Constructor_default) { check("Triangle_Constructor_default"); }
TEST_F(LuaTriangle, Constructor_vertices) { check("Triangle_Constructor_vertices"); }
TEST_F(LuaTriangle, Vertex_mutation) { check("Triangle_Vertex_mutation"); }
TEST_F(LuaTriangle, SideALength) { check("Triangle_SideALength"); }
TEST_F(LuaTriangle, SideBLength) { check("Triangle_SideBLength"); }
TEST_F(LuaTriangle, Hypot) { check("Triangle_Hypot"); }
TEST_F(LuaTriangle, SideAVector) { check("Triangle_SideAVector"); }
TEST_F(LuaTriangle, SideBVector) { check("Triangle_SideBVector"); }
TEST_F(LuaTriangle, IsRectangular_true) { check("Triangle_IsRectangular_true"); }
TEST_F(LuaTriangle, IsRectangular_false) { check("Triangle_IsRectangular_false"); }
TEST_F(LuaTriangle, MidPoint) { check("Triangle_MidPoint"); }
TEST_F(LuaTriangle, CalculateNormal) { check("Triangle_CalculateNormal"); }
TEST_F(LuaTriangle, ToString) { check("Triangle_ToString"); }

View File

@@ -0,0 +1,56 @@
//
// Created by orange on 07.03.2026.
//
#include <gtest/gtest.h>
#include <lua.hpp>
#include <omath/lua/lua.hpp>
class LuaVec2 : public ::testing::Test
{
protected:
lua_State* L = nullptr;
void SetUp() override
{
L = luaL_newstate();
luaL_openlibs(L);
omath::lua::LuaInterpreter::register_lib(L);
if (luaL_dofile(L, LUA_SCRIPTS_DIR "/vec2_tests.lua") != LUA_OK)
FAIL() << lua_tostring(L, -1);
}
void TearDown() override { lua_close(L); }
void check(const char* func_name)
{
lua_getglobal(L, func_name);
if (lua_pcall(L, 0, 0, 0) != LUA_OK)
{
FAIL() << lua_tostring(L, -1);
lua_pop(L, 1);
}
}
};
TEST_F(LuaVec2, Constructor_default) { check("Vec2_Constructor_default"); }
TEST_F(LuaVec2, Constructor_xy) { check("Vec2_Constructor_xy"); }
TEST_F(LuaVec2, Field_mutation) { check("Vec2_Field_mutation"); }
TEST_F(LuaVec2, Addition) { check("Vec2_Addition"); }
TEST_F(LuaVec2, Subtraction) { check("Vec2_Subtraction"); }
TEST_F(LuaVec2, UnaryMinus) { check("Vec2_UnaryMinus"); }
TEST_F(LuaVec2, Multiplication_scalar) { check("Vec2_Multiplication_scalar"); }
TEST_F(LuaVec2, Multiplication_scalar_reversed) { check("Vec2_Multiplication_scalar_reversed"); }
TEST_F(LuaVec2, Division_scalar) { check("Vec2_Division_scalar"); }
TEST_F(LuaVec2, EqualTo_true) { check("Vec2_EqualTo_true"); }
TEST_F(LuaVec2, EqualTo_false) { check("Vec2_EqualTo_false"); }
TEST_F(LuaVec2, LessThan) { check("Vec2_LessThan"); }
TEST_F(LuaVec2, LessThanOrEqual) { check("Vec2_LessThanOrEqual"); }
TEST_F(LuaVec2, ToString) { check("Vec2_ToString"); }
TEST_F(LuaVec2, Length) { check("Vec2_Length"); }
TEST_F(LuaVec2, LengthSqr) { check("Vec2_LengthSqr"); }
TEST_F(LuaVec2, Normalized) { check("Vec2_Normalized"); }
TEST_F(LuaVec2, Dot) { check("Vec2_Dot"); }
TEST_F(LuaVec2, DistanceTo) { check("Vec2_DistanceTo"); }
TEST_F(LuaVec2, DistanceToSqr) { check("Vec2_DistanceToSqr"); }
TEST_F(LuaVec2, Sum) { check("Vec2_Sum"); }
TEST_F(LuaVec2, Abs) { check("Vec2_Abs"); }

View File

@@ -0,0 +1,69 @@
//
// Created by orange on 07.03.2026.
//
#include <gtest/gtest.h>
#include <lua.hpp>
#include <omath/lua/lua.hpp>
class LuaVec3 : public ::testing::Test
{
protected:
lua_State* L = nullptr;
void SetUp() override
{
L = luaL_newstate();
luaL_openlibs(L);
omath::lua::LuaInterpreter::register_lib(L);
if (luaL_dofile(L, LUA_SCRIPTS_DIR "/vec3_tests.lua") != LUA_OK)
FAIL() << lua_tostring(L, -1);
}
void TearDown() override { lua_close(L); }
void check(const char* func_name)
{
lua_getglobal(L, func_name);
if (lua_pcall(L, 0, 0, 0) != LUA_OK)
{
FAIL() << lua_tostring(L, -1);
lua_pop(L, 1);
}
}
};
TEST_F(LuaVec3, Constructor_default) { check("Vec3_Constructor_default"); }
TEST_F(LuaVec3, Constructor_xyz) { check("Vec3_Constructor_xyz"); }
TEST_F(LuaVec3, Field_mutation) { check("Vec3_Field_mutation"); }
TEST_F(LuaVec3, Addition) { check("Vec3_Addition"); }
TEST_F(LuaVec3, Subtraction) { check("Vec3_Subtraction"); }
TEST_F(LuaVec3, UnaryMinus) { check("Vec3_UnaryMinus"); }
TEST_F(LuaVec3, Multiplication_scalar) { check("Vec3_Multiplication_scalar"); }
TEST_F(LuaVec3, Multiplication_scalar_reversed) { check("Vec3_Multiplication_scalar_reversed"); }
TEST_F(LuaVec3, Multiplication_vec) { check("Vec3_Multiplication_vec"); }
TEST_F(LuaVec3, Division_scalar) { check("Vec3_Division_scalar"); }
TEST_F(LuaVec3, Division_vec) { check("Vec3_Division_vec"); }
TEST_F(LuaVec3, EqualTo_true) { check("Vec3_EqualTo_true"); }
TEST_F(LuaVec3, EqualTo_false) { check("Vec3_EqualTo_false"); }
TEST_F(LuaVec3, LessThan) { check("Vec3_LessThan"); }
TEST_F(LuaVec3, LessThanOrEqual) { check("Vec3_LessThanOrEqual"); }
TEST_F(LuaVec3, ToString) { check("Vec3_ToString"); }
TEST_F(LuaVec3, Length) { check("Vec3_Length"); }
TEST_F(LuaVec3, Length2d) { check("Vec3_Length2d"); }
TEST_F(LuaVec3, LengthSqr) { check("Vec3_LengthSqr"); }
TEST_F(LuaVec3, Normalized) { check("Vec3_Normalized"); }
TEST_F(LuaVec3, Dot_perpendicular) { check("Vec3_Dot_perpendicular"); }
TEST_F(LuaVec3, Dot_parallel) { check("Vec3_Dot_parallel"); }
TEST_F(LuaVec3, Cross) { check("Vec3_Cross"); }
TEST_F(LuaVec3, DistanceTo) { check("Vec3_DistanceTo"); }
TEST_F(LuaVec3, DistanceToSqr) { check("Vec3_DistanceToSqr"); }
TEST_F(LuaVec3, Sum) { check("Vec3_Sum"); }
TEST_F(LuaVec3, Sum2d) { check("Vec3_Sum2d"); }
TEST_F(LuaVec3, Abs) { check("Vec3_Abs"); }
TEST_F(LuaVec3, PointToSameDirection_true) { check("Vec3_PointToSameDirection_true"); }
TEST_F(LuaVec3, PointToSameDirection_false) { check("Vec3_PointToSameDirection_false"); }
TEST_F(LuaVec3, IsPerpendicular_true) { check("Vec3_IsPerpendicular_true"); }
TEST_F(LuaVec3, IsPerpendicular_false) { check("Vec3_IsPerpendicular_false"); }
TEST_F(LuaVec3, AngleBetween_90deg) { check("Vec3_AngleBetween_90deg"); }
TEST_F(LuaVec3, AngleBetween_zero_vector_error) { check("Vec3_AngleBetween_zero_vector_error"); }
TEST_F(LuaVec3, AsTable) { check("Vec3_AsTable"); }

View File

@@ -0,0 +1,57 @@
//
// Created by orange on 07.03.2026.
//
#include <gtest/gtest.h>
#include <lua.hpp>
#include <omath/lua/lua.hpp>
class LuaVec4 : public ::testing::Test
{
protected:
lua_State* L = nullptr;
void SetUp() override
{
L = luaL_newstate();
luaL_openlibs(L);
omath::lua::LuaInterpreter::register_lib(L);
if (luaL_dofile(L, LUA_SCRIPTS_DIR "/vec4_tests.lua") != LUA_OK)
FAIL() << lua_tostring(L, -1);
}
void TearDown() override { lua_close(L); }
void check(const char* func_name)
{
lua_getglobal(L, func_name);
if (lua_pcall(L, 0, 0, 0) != LUA_OK)
{
FAIL() << lua_tostring(L, -1);
lua_pop(L, 1);
}
}
};
TEST_F(LuaVec4, Constructor_default) { check("Vec4_Constructor_default"); }
TEST_F(LuaVec4, Constructor_xyzw) { check("Vec4_Constructor_xyzw"); }
TEST_F(LuaVec4, Field_mutation) { check("Vec4_Field_mutation"); }
TEST_F(LuaVec4, Addition) { check("Vec4_Addition"); }
TEST_F(LuaVec4, Subtraction) { check("Vec4_Subtraction"); }
TEST_F(LuaVec4, UnaryMinus) { check("Vec4_UnaryMinus"); }
TEST_F(LuaVec4, Multiplication_scalar) { check("Vec4_Multiplication_scalar"); }
TEST_F(LuaVec4, Multiplication_scalar_reversed) { check("Vec4_Multiplication_scalar_reversed"); }
TEST_F(LuaVec4, Multiplication_vec) { check("Vec4_Multiplication_vec"); }
TEST_F(LuaVec4, Division_scalar) { check("Vec4_Division_scalar"); }
TEST_F(LuaVec4, Division_vec) { check("Vec4_Division_vec"); }
TEST_F(LuaVec4, EqualTo_true) { check("Vec4_EqualTo_true"); }
TEST_F(LuaVec4, EqualTo_false) { check("Vec4_EqualTo_false"); }
TEST_F(LuaVec4, LessThan) { check("Vec4_LessThan"); }
TEST_F(LuaVec4, LessThanOrEqual) { check("Vec4_LessThanOrEqual"); }
TEST_F(LuaVec4, ToString) { check("Vec4_ToString"); }
TEST_F(LuaVec4, Length) { check("Vec4_Length"); }
TEST_F(LuaVec4, LengthSqr) { check("Vec4_LengthSqr"); }
TEST_F(LuaVec4, Dot) { check("Vec4_Dot"); }
TEST_F(LuaVec4, Dot_perpendicular) { check("Vec4_Dot_perpendicular"); }
TEST_F(LuaVec4, Sum) { check("Vec4_Sum"); }
TEST_F(LuaVec4, Abs) { check("Vec4_Abs"); }
TEST_F(LuaVec4, Clamp) { check("Vec4_Clamp"); }

102
tests/lua/vec2_tests.lua Normal file
View File

@@ -0,0 +1,102 @@
local function approx(a, b) return math.abs(a - b) < 1e-5 end
function Vec2_Constructor_default()
local v = omath.Vec2.new()
assert(v.x == 0 and v.y == 0)
end
function Vec2_Constructor_xy()
local v = omath.Vec2.new(3, 4)
assert(v.x == 3 and v.y == 4)
end
function Vec2_Field_mutation()
local v = omath.Vec2.new(1, 2)
v.x = 9; v.y = 8
assert(v.x == 9 and v.y == 8)
end
function Vec2_Addition()
local c = omath.Vec2.new(1, 2) + omath.Vec2.new(3, 4)
assert(c.x == 4 and c.y == 6)
end
function Vec2_Subtraction()
local c = omath.Vec2.new(5, 7) - omath.Vec2.new(2, 3)
assert(c.x == 3 and c.y == 4)
end
function Vec2_UnaryMinus()
local b = -omath.Vec2.new(1, 2)
assert(b.x == -1 and b.y == -2)
end
function Vec2_Multiplication_scalar()
local b = omath.Vec2.new(2, 3) * 2.0
assert(b.x == 4 and b.y == 6)
end
function Vec2_Multiplication_scalar_reversed()
local b = 2.0 * omath.Vec2.new(2, 3)
assert(b.x == 4 and b.y == 6)
end
function Vec2_Division_scalar()
local b = omath.Vec2.new(4, 6) / 2.0
assert(b.x == 2 and b.y == 3)
end
function Vec2_EqualTo_true()
assert(omath.Vec2.new(1, 2) == omath.Vec2.new(1, 2))
end
function Vec2_EqualTo_false()
assert(not (omath.Vec2.new(1, 2) == omath.Vec2.new(9, 9)))
end
function Vec2_LessThan()
assert(omath.Vec2.new(1, 0) < omath.Vec2.new(3, 4))
end
function Vec2_LessThanOrEqual()
-- (3,4) and (4,3) both have length 5
assert(omath.Vec2.new(3, 4) <= omath.Vec2.new(4, 3))
end
function Vec2_ToString()
assert(tostring(omath.Vec2.new(1, 2)) == "Vec2(1, 2)")
end
function Vec2_Length()
assert(approx(omath.Vec2.new(3, 4):length(), 5.0))
end
function Vec2_LengthSqr()
assert(omath.Vec2.new(3, 4):length_sqr() == 25.0)
end
function Vec2_Normalized()
local n = omath.Vec2.new(3, 4):normalized()
assert(approx(n.x, 0.6) and approx(n.y, 0.8))
end
function Vec2_Dot()
assert(omath.Vec2.new(1, 2):dot(omath.Vec2.new(3, 4)) == 11.0)
end
function Vec2_DistanceTo()
assert(approx(omath.Vec2.new(0, 0):distance_to(omath.Vec2.new(3, 4)), 5.0))
end
function Vec2_DistanceToSqr()
assert(omath.Vec2.new(0, 0):distance_to_sqr(omath.Vec2.new(3, 4)) == 25.0)
end
function Vec2_Sum()
assert(omath.Vec2.new(3, 4):sum() == 7.0)
end
function Vec2_Abs()
local a = omath.Vec2.new(-3, -4):abs()
assert(a.x == 3 and a.y == 4)
end

163
tests/lua/vec3_tests.lua Normal file
View File

@@ -0,0 +1,163 @@
local function approx(a, b, eps) return math.abs(a - b) < (eps or 1e-5) end
function Vec3_Constructor_default()
local v = omath.Vec3.new()
assert(v.x == 0 and v.y == 0 and v.z == 0)
end
function Vec3_Constructor_xyz()
local v = omath.Vec3.new(1, 2, 3)
assert(v.x == 1 and v.y == 2 and v.z == 3)
end
function Vec3_Field_mutation()
local v = omath.Vec3.new(1, 2, 3)
v.x = 9; v.y = 8; v.z = 7
assert(v.x == 9 and v.y == 8 and v.z == 7)
end
function Vec3_Addition()
local c = omath.Vec3.new(1, 2, 3) + omath.Vec3.new(4, 5, 6)
assert(c.x == 5 and c.y == 7 and c.z == 9)
end
function Vec3_Subtraction()
local c = omath.Vec3.new(4, 5, 6) - omath.Vec3.new(1, 2, 3)
assert(c.x == 3 and c.y == 3 and c.z == 3)
end
function Vec3_UnaryMinus()
local b = -omath.Vec3.new(1, 2, 3)
assert(b.x == -1 and b.y == -2 and b.z == -3)
end
function Vec3_Multiplication_scalar()
local b = omath.Vec3.new(1, 2, 3) * 2.0
assert(b.x == 2 and b.y == 4 and b.z == 6)
end
function Vec3_Multiplication_scalar_reversed()
local b = 2.0 * omath.Vec3.new(1, 2, 3)
assert(b.x == 2 and b.y == 4 and b.z == 6)
end
function Vec3_Multiplication_vec()
local c = omath.Vec3.new(2, 3, 4) * omath.Vec3.new(2, 2, 2)
assert(c.x == 4 and c.y == 6 and c.z == 8)
end
function Vec3_Division_scalar()
local b = omath.Vec3.new(2, 4, 6) / 2.0
assert(b.x == 1 and b.y == 2 and b.z == 3)
end
function Vec3_Division_vec()
local c = omath.Vec3.new(4, 6, 8) / omath.Vec3.new(2, 2, 2)
assert(c.x == 2 and c.y == 3 and c.z == 4)
end
function Vec3_EqualTo_true()
assert(omath.Vec3.new(1, 2, 3) == omath.Vec3.new(1, 2, 3))
end
function Vec3_EqualTo_false()
assert(not (omath.Vec3.new(1, 2, 3) == omath.Vec3.new(9, 9, 9)))
end
function Vec3_LessThan()
assert(omath.Vec3.new(1, 0, 0) < omath.Vec3.new(3, 4, 0))
end
function Vec3_LessThanOrEqual()
-- (0,3,4) and (0,4,3) both have length 5
assert(omath.Vec3.new(0, 3, 4) <= omath.Vec3.new(0, 4, 3))
end
function Vec3_ToString()
assert(tostring(omath.Vec3.new(1, 2, 3)) == "Vec3(1, 2, 3)")
end
function Vec3_Length()
assert(approx(omath.Vec3.new(1, 2, 2):length(), 3.0))
end
function Vec3_Length2d()
assert(approx(omath.Vec3.new(3, 4, 99):length_2d(), 5.0))
end
function Vec3_LengthSqr()
assert(omath.Vec3.new(1, 2, 2):length_sqr() == 9.0)
end
function Vec3_Normalized()
local n = omath.Vec3.new(3, 0, 0):normalized()
assert(approx(n.x, 1.0) and approx(n.y, 0.0) and approx(n.z, 0.0))
end
function Vec3_Dot_perpendicular()
assert(omath.Vec3.new(1, 0, 0):dot(omath.Vec3.new(0, 1, 0)) == 0.0)
end
function Vec3_Dot_parallel()
local a = omath.Vec3.new(1, 2, 3)
assert(a:dot(a) == 14.0)
end
function Vec3_Cross()
local c = omath.Vec3.new(1, 0, 0):cross(omath.Vec3.new(0, 1, 0))
assert(approx(c.x, 0) and approx(c.y, 0) and approx(c.z, 1))
end
function Vec3_DistanceTo()
assert(approx(omath.Vec3.new(0, 0, 0):distance_to(omath.Vec3.new(1, 2, 2)), 3.0))
end
function Vec3_DistanceToSqr()
assert(omath.Vec3.new(0, 0, 0):distance_to_sqr(omath.Vec3.new(1, 2, 2)) == 9.0)
end
function Vec3_Sum()
assert(omath.Vec3.new(1, 2, 3):sum() == 6.0)
end
function Vec3_Sum2d()
assert(omath.Vec3.new(1, 2, 3):sum_2d() == 3.0)
end
function Vec3_Abs()
local a = omath.Vec3.new(-1, -2, -3):abs()
assert(a.x == 1 and a.y == 2 and a.z == 3)
end
function Vec3_PointToSameDirection_true()
assert(omath.Vec3.new(1, 1, 0):point_to_same_direction(omath.Vec3.new(2, 2, 0)) == true)
end
function Vec3_PointToSameDirection_false()
assert(omath.Vec3.new(1, 0, 0):point_to_same_direction(omath.Vec3.new(-1, 0, 0)) == false)
end
function Vec3_IsPerpendicular_true()
assert(omath.Vec3.new(1, 0, 0):is_perpendicular(omath.Vec3.new(0, 1, 0)) == true)
end
function Vec3_IsPerpendicular_false()
local a = omath.Vec3.new(1, 0, 0)
assert(a:is_perpendicular(a) == false)
end
function Vec3_AngleBetween_90deg()
local angle, err = omath.Vec3.new(1, 0, 0):angle_between(omath.Vec3.new(0, 1, 0))
assert(angle ~= nil, err)
assert(math.abs(angle - 90.0) < 1e-3)
end
function Vec3_AngleBetween_zero_vector_error()
local angle, err = omath.Vec3.new(0, 0, 0):angle_between(omath.Vec3.new(1, 0, 0))
assert(angle == nil and err ~= nil)
end
function Vec3_AsTable()
local t = omath.Vec3.new(1, 2, 3):as_table()
assert(t.x == 1 and t.y == 2 and t.z == 3)
end

110
tests/lua/vec4_tests.lua Normal file
View File

@@ -0,0 +1,110 @@
local function approx(a, b) return math.abs(a - b) < 1e-5 end
function Vec4_Constructor_default()
local v = omath.Vec4.new()
assert(v.x == 0 and v.y == 0 and v.z == 0 and v.w == 0)
end
function Vec4_Constructor_xyzw()
local v = omath.Vec4.new(1, 2, 3, 4)
assert(v.x == 1 and v.y == 2 and v.z == 3 and v.w == 4)
end
function Vec4_Field_mutation()
local v = omath.Vec4.new(1, 2, 3, 4)
v.w = 99
assert(v.w == 99)
end
function Vec4_Addition()
local c = omath.Vec4.new(1, 2, 3, 4) + omath.Vec4.new(4, 3, 2, 1)
assert(c.x == 5 and c.y == 5 and c.z == 5 and c.w == 5)
end
function Vec4_Subtraction()
local c = omath.Vec4.new(5, 5, 5, 5) - omath.Vec4.new(1, 2, 3, 4)
assert(c.x == 4 and c.y == 3 and c.z == 2 and c.w == 1)
end
function Vec4_UnaryMinus()
local b = -omath.Vec4.new(1, 2, 3, 4)
assert(b.x == -1 and b.y == -2 and b.z == -3 and b.w == -4)
end
function Vec4_Multiplication_scalar()
local b = omath.Vec4.new(1, 2, 3, 4) * 2.0
assert(b.x == 2 and b.y == 4 and b.z == 6 and b.w == 8)
end
function Vec4_Multiplication_scalar_reversed()
local b = 2.0 * omath.Vec4.new(1, 2, 3, 4)
assert(b.x == 2 and b.y == 4 and b.z == 6 and b.w == 8)
end
function Vec4_Multiplication_vec()
local c = omath.Vec4.new(2, 3, 4, 5) * omath.Vec4.new(2, 2, 2, 2)
assert(c.x == 4 and c.y == 6 and c.z == 8 and c.w == 10)
end
function Vec4_Division_scalar()
local b = omath.Vec4.new(2, 4, 6, 8) / 2.0
assert(b.x == 1 and b.y == 2 and b.z == 3 and b.w == 4)
end
function Vec4_Division_vec()
local c = omath.Vec4.new(4, 6, 8, 10) / omath.Vec4.new(2, 2, 2, 2)
assert(c.x == 2 and c.y == 3 and c.z == 4 and c.w == 5)
end
function Vec4_EqualTo_true()
assert(omath.Vec4.new(1, 2, 3, 4) == omath.Vec4.new(1, 2, 3, 4))
end
function Vec4_EqualTo_false()
assert(not (omath.Vec4.new(1, 2, 3, 4) == omath.Vec4.new(9, 9, 9, 9)))
end
function Vec4_LessThan()
assert(omath.Vec4.new(1, 0, 0, 0) < omath.Vec4.new(0, 0, 3, 4))
end
function Vec4_LessThanOrEqual()
-- (0,0,3,4) and (0,0,4,3) both have length 5
assert(omath.Vec4.new(0, 0, 3, 4) <= omath.Vec4.new(0, 0, 4, 3))
end
function Vec4_ToString()
assert(tostring(omath.Vec4.new(1, 2, 3, 4)) == "Vec4(1, 2, 3, 4)")
end
function Vec4_Length()
assert(approx(omath.Vec4.new(0, 0, 3, 4):length(), 5.0))
end
function Vec4_LengthSqr()
assert(omath.Vec4.new(0, 0, 3, 4):length_sqr() == 25.0)
end
function Vec4_Dot()
local a = omath.Vec4.new(1, 2, 3, 4)
assert(a:dot(a) == 30.0)
end
function Vec4_Dot_perpendicular()
assert(omath.Vec4.new(1, 0, 0, 0):dot(omath.Vec4.new(0, 1, 0, 0)) == 0.0)
end
function Vec4_Sum()
assert(omath.Vec4.new(1, 2, 3, 4):sum() == 10.0)
end
function Vec4_Abs()
local a = omath.Vec4.new(-1, -2, -3, -4):abs()
assert(a.x == 1 and a.y == 2 and a.z == 3 and a.w == 4)
end
function Vec4_Clamp()
local v = omath.Vec4.new(5, -3, 10, 99)
v:clamp(0, 7)
assert(v.x == 5 and v.y == 0 and v.z == 7)
end

View File

@@ -0,0 +1,29 @@
local a = omath.Vec2.new(1, 2)
local b = omath.Vec2.new(10, 20)
-- Operators
local c = a + b
local d = a - b
local e = a * 2.0
local f = -a
print("a + b = " .. tostring(c))
print("a - b = " .. tostring(d))
print("a * 2 = " .. tostring(e))
print("-a = " .. tostring(f))
print("a == Vec2(1,2): " .. tostring(a == omath.Vec2.new(1, 2)))
print("a < b: " .. tostring(a < b))
-- Field access + mutation
print("c.x = " .. c.x .. ", c.y = " .. c.y)
c.x = 99
print("c.x after mutation = " .. c.x)
-- Methods
print("a:length() = " .. a:length())
print("a:length_sqr() = " .. a:length_sqr())
print("a:normalized() = " .. tostring(a:normalized()))
print("a:dot(b) = " .. a:dot(b))
print("a:distance_to(b) = " .. a:distance_to(b))
print("a:distance_to_sqr(b) = " .. a:distance_to_sqr(b))
print("a:sum() = " .. a:sum())
print("a:abs() = " .. tostring(a:abs()))

View File

@@ -0,0 +1,55 @@
local a = omath.Vec3.new(1, 0, 0)
local b = omath.Vec3.new(0, 1, 0)
-- Operators
local c = a + b
local d = a - b
local e = a * 2.0
local f = -a
print("a + b = " .. tostring(c))
print("a - b = " .. tostring(d))
print("a * 2 = " .. tostring(e))
print("-a = " .. tostring(f))
print("a == Vec3(1,2,3): " .. tostring(a == omath.Vec3.new(1, 2, 3)))
print("a < b: " .. tostring(a < b))
-- Field access + mutation
print("c.x = " .. c.x .. ", c.y = " .. c.y .. ", c.z = " .. c.z)
c.x = 99
print("c.x after mutation = " .. c.x)
-- Methods
print("a:length() = " .. a:length())
print("a:length_2d() = " .. a:length_2d())
print("a:length_sqr() = " .. a:length_sqr())
print("a:normalized() = " .. tostring(a:normalized()))
print("a:dot(b) = " .. a:dot(b))
print("a:cross(b) = " .. tostring(a:cross(b)))
print("a:distance_to(b) = " .. a:distance_to(b))
print("a:distance_to_sqr(b) = " .. a:distance_to_sqr(b))
print("a:abs() = " .. tostring(a:abs()))
print("a:sum() = " .. a:sum())
print("a:sum_2d() = " .. a:sum_2d())
print("a:point_to_same_direction(b) = " .. tostring(a:point_to_same_direction(b)))
print("a:is_perpendicular(b) = " .. tostring(a:is_perpendicular(b)))
-- angle_between
local angle, err = a:angle_between(b)
if angle then
print("angle_between = " .. angle .. " degrees")
else
print("angle_between error: " .. err)
end
-- Zero vector edge case
local zero = omath.Vec3.new(0, 0, 0)
local ang2, err2 = zero:angle_between(a)
if ang2 then
print("zero angle = " .. ang2)
else
print("zero angle error: " .. err2)
end
-- as_table
local t = a:as_table()
print("as_table: x=" .. t.x .. " y=" .. t.y .. " z=" .. t.z)

View File

@@ -0,0 +1,31 @@
local a = omath.Vec4.new(1, 2, 3, 4)
local b = omath.Vec4.new(10, 20, 30, 40)
-- Operators
local c = a + b
local d = a - b
local e = a * 2.0
local f = -a
print("a + b = " .. tostring(c))
print("a - b = " .. tostring(d))
print("a * 2 = " .. tostring(e))
print("-a = " .. tostring(f))
print("a == Vec4(1,2,3,4): " .. tostring(a == omath.Vec4.new(1, 2, 3, 4)))
print("a < b: " .. tostring(a < b))
-- Field access + mutation
print("c.x=" .. c.x .. " c.y=" .. c.y .. " c.z=" .. c.z .. " c.w=" .. c.w)
c.w = 99
print("c.w after mutation = " .. c.w)
-- Methods
print("a:length() = " .. a:length())
print("a:length_sqr() = " .. a:length_sqr())
print("a:dot(b) = " .. a:dot(b))
print("a:sum() = " .. a:sum())
print("a:abs() = " .. tostring(a:abs()))
-- clamp
local clamped = omath.Vec4.new(5, -3, 10, 1)
clamped:clamp(0, 7)
print("clamp([5,-3,10,1], 0, 7).x=" .. clamped.x .. " .y=" .. clamped.y .. " .z=" .. clamped.z)

View File

@@ -17,7 +17,7 @@
], ],
"features": { "features": {
"avx2": { "avx2": {
"description": "Omath will use AVX2 to boost performance", "description": "omath will use AVX2 to boost performance",
"supports": "!arm" "supports": "!arm"
}, },
"benchmark": { "benchmark": {
@@ -35,7 +35,7 @@
] ]
}, },
"imgui": { "imgui": {
"description": "Omath will define method to convert omath types to imgui types", "description": "omath will define method to convert omath types to imgui types",
"dependencies": [ "dependencies": [
"imgui" "imgui"
] ]
@@ -45,6 +45,13 @@
"dependencies": [ "dependencies": [
"gtest" "gtest"
] ]
},
"lua": {
"description": "lua support for omath",
"dependencies": [
"lua",
"sol2"
]
} }
} }
} }