Compare commits

...

151 Commits

Author SHA1 Message Date
927508a76b Merge pull request #172 from orange-cpp/feaute/methods_calling_improvement
Feaute/methods calling improvement
2026-03-19 01:33:42 +03:00
f390b386d7 fix 2026-03-19 01:06:16 +03:00
012d837e8b fix windows x32 bit 2026-03-19 00:57:54 +03:00
6236c8fd68 added nodiscard 2026-03-18 21:24:35 +03:00
06dc36089f added overload 2026-03-18 21:19:09 +03:00
91136a61c4 improvement 2026-03-18 21:12:18 +03:00
9cdffcbdb1 added tests 2026-03-18 20:12:46 +03:00
a3e93ac259 added nttp 2026-03-18 20:05:32 +03:00
59f6d7a361 added call_method 2026-03-18 19:58:52 +03:00
dcf1ef1ea9 Merge pull request #171 from orange-cpp/feaute/projectile_pred_improvement
Feaute/projectile pred improvement
2026-03-17 21:58:59 +03:00
89bd879187 added tolerance depending on arch 2026-03-17 21:15:39 +03:00
aa08c7cb65 improved projectile prediction 2026-03-17 20:43:26 +03:00
a5c0ca0cbd added stuff 2026-03-17 20:31:46 +03:00
624683aed6 added unreachanble 2026-03-17 19:53:15 +03:00
f46672b2c6 Merge pull request #170 from orange-cpp/feature/projectile_aim_widget
add projectile
2026-03-17 19:51:12 +03:00
b8e61f49fa add projectile 2026-03-17 19:36:35 +03:00
37ea091282 Merge pull request #169 from orange-cpp/feaute/hud_features
Feaute/hud features
2026-03-16 14:39:58 +03:00
29a2743728 renamed args 2026-03-16 13:17:16 +03:00
1117eb37f1 added icon 2026-03-16 13:13:41 +03:00
b6b0d4db13 added aim dot 2026-03-16 03:24:53 +03:00
2e8a74aaaf imroved spacer 2026-03-16 03:06:14 +03:00
d8632dc74c added progress ring 2026-03-16 03:03:23 +03:00
fd531c930c added spacer 2026-03-16 02:21:24 +03:00
a91673216d added const 2026-03-16 02:10:05 +03:00
6487554844 corrected code style 2026-03-16 01:54:45 +03:00
1744172694 updated credits 2026-03-15 20:42:13 +03:00
114b2a6e58 Update README to enhance library description and features 2026-03-15 20:21:08 +03:00
d90a85d8b6 Merge pull request #168 from orange-cpp/feature/hud_declarative
Feature/hud declarative
2026-03-15 20:02:32 +03:00
e0a7179812 fix 2026-03-15 19:43:55 +03:00
a99dd24d6b improvement 2026-03-15 19:39:02 +03:00
d62dec9a8f changed api 2026-03-15 19:10:15 +03:00
1a176d8f09 fix 2026-03-15 18:48:22 +03:00
8e6ed19abf added dashed bar 2026-03-15 18:39:40 +03:00
311ab45722 Merge pull request #167 from orange-cpp/feaute/sig_scan_file_in_mem
added stuff
2026-03-15 17:37:42 +03:00
130277c1ae refactored test 2026-03-15 17:20:28 +03:00
4f1c42d6f6 tests fix 2026-03-15 17:04:21 +03:00
ccea4a0f0d added stuff 2026-03-15 16:54:47 +03:00
3fb98397e4 Merge pull request #166 from orange-cpp/feature/hud_improvement
Feature/hud improvement
2026-03-15 14:01:33 +03:00
56256c40fb cleaned code 2026-03-15 13:47:41 +03:00
46c94ae541 decomposed Run 2026-03-15 13:44:25 +03:00
a45f095b9c added skeleton 2026-03-15 04:59:47 +03:00
e849d23c47 improved dashed box 2026-03-15 04:56:10 +03:00
adad66599a adde dash box 2026-03-15 04:49:01 +03:00
69bdfc3307 improved example 2026-03-15 04:43:19 +03:00
55304c5df1 fixed bug 2026-03-15 04:28:56 +03:00
19d796cd4e improvement 2026-03-15 04:23:07 +03:00
d31ea6ed4d added more stuff 2026-03-15 04:17:30 +03:00
977d772687 fix 2026-03-13 22:20:57 +03:00
746f1b84a8 hot fix 2026-03-13 22:16:42 +03:00
af399a14ed Merge pull request #165 from orange-cpp/feature/hud
Feature/hud
2026-03-13 22:11:26 +03:00
6fb420642b updated props 2026-03-13 21:58:14 +03:00
6a2b4b90b4 fix 2026-03-13 21:49:56 +03:00
371d8154ee fix 2026-03-13 21:40:30 +03:00
d6a2165f83 fix 2026-03-13 21:37:03 +03:00
bb1b5ad14a removed shit 2026-03-13 21:32:44 +03:00
f188257e0f added stuff 2026-03-13 21:28:16 +03:00
87966c82b9 added realization 2026-03-13 21:09:12 +03:00
9da19582b5 added files 2026-03-13 20:51:59 +03:00
29f3e2565d Merge pull request #164 from orange-cpp/feaute/disk_optimization
avoid saving files on disk
2026-03-13 03:55:56 +03:00
e083b15e0b update 2026-03-13 03:42:12 +03:00
ed9da79d08 avoid saving files on disk 2026-03-13 03:33:57 +03:00
2002bcca83 Merge pull request #163 from orange-cpp/feature/serailization
Feature/serailization
2026-03-11 14:47:23 +03:00
ffacba71e2 changed to string view 2026-03-11 14:31:45 +03:00
6081a9c426 added throw test 2026-03-11 14:30:01 +03:00
8bbd504356 added check 2026-03-11 14:23:12 +03:00
1d54039f57 added events 2026-03-11 14:19:58 +03:00
93fc93d4f6 added more tests 2026-03-11 14:16:26 +03:00
b8a578774c improved serialization 2026-03-11 14:12:52 +03:00
bfa6c77776 Merge pull request #162 from orange-cpp/feature/scanner_example
Auto stash before checking out "origin/main"
2026-03-10 21:39:20 +03:00
1341ef9925 Auto stash before checking out "origin/main" 2026-03-10 20:06:00 +03:00
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
50765f69c5 removed unused var 2026-02-23 04:36:48 +03:00
1169534133 fix 2026-02-23 04:32:13 +03:00
783501aab9 Enhance installation guide with prebuilt binaries section
Updated vcpkg section and added instructions for using prebuilt binaries from GitHub Releases.
2026-02-21 10:00:19 +03:00
1a79566731 Merge pull request #156 from orange-cpp/copilot/add-automatic-binary-attach
Add release workflow with binary uploads for all CI platforms
2026-02-21 09:31:12 +03:00
copilot-swe-agent[bot]
9183406e7a Add all CI pipeline platforms to release workflow (Linux x86, iOS, FreeBSD, Android, WebAssembly, MinGW)
Co-authored-by: orange-cpp <59374393+orange-cpp@users.noreply.github.com>
2026-02-21 04:55:36 +00:00
copilot-swe-agent[bot]
b592af91a9 Add release workflow to automatically attach binaries to new releases
Co-authored-by: orange-cpp <59374393+orange-cpp@users.noreply.github.com>
2026-02-21 04:45:20 +00:00
copilot-swe-agent[bot]
af9c043e95 Initial plan 2026-02-21 04:41:46 +00:00
2aaa8633b9 updated tag 2026-02-20 09:07:33 +03:00
0d1cc2088f Merge pull request #155 from orange-cpp/feaute/examples-folders
retructurized examples
2026-02-20 08:55:20 +03:00
260b3b97f9 fix 2026-02-20 08:53:03 +03:00
e0904690e6 fixed formating 2026-02-20 08:52:26 +03:00
7021d9d365 fixed formating 2026-02-20 08:46:43 +03:00
862d52593a retructurized examples 2026-02-20 07:48:19 +03:00
97a3111791 fixed code style 2026-02-20 04:02:26 +03:00
a9bec00973 Merge pull request #154 from luadebug/example1
Example with opengl triangle
2026-02-20 03:37:58 +03:00
Saikari
66debb46fa Refactor to use Color class and Triangle structure for better clarity in drawing functions 2026-02-20 03:01:34 +03:00
Saikari
b8323d3bc0 simplify complexity 2026-02-20 02:56:27 +03:00
Saikari
f363fa6f1a Fix text 2026-02-20 02:53:20 +03:00
Saikari
0546272493 Use dot not quad 2026-02-20 02:50:55 +03:00
Saikari
58801bfab3 Add one more example 2026-02-20 02:43:27 +03:00
4567cfa318 updated read me 2026-02-19 12:03:19 +03:00
1db9340dbf updated read me 2026-02-19 08:42:58 +03:00
7b9a181b0e Merge pull request #153 from orange-cpp/feature/cryengine-support
Feature/cryengine support
2026-02-19 08:34:33 +03:00
0876af3c14 added warning back 2026-02-19 08:34:21 +03:00
e46a369ddc added more tests 2026-02-19 08:06:34 +03:00
630ffa69f6 clang format 2026-02-19 08:00:23 +03:00
609970cdfe fix 2026-02-19 07:57:04 +03:00
e935155022 improved tests 2026-02-19 07:47:23 +03:00
800082e4b3 added mesh 2026-02-19 06:35:08 +03:00
02d909f77d added mesh trait 2026-02-19 01:06:08 +03:00
49319a1c7c added more files 2026-02-19 01:05:09 +03:00
d0f3e5059a added formulas 2026-02-19 01:05:09 +03:00
c9874f30d0 add 2026-02-19 01:05:09 +03:00
3080faeaa9 hotfix: projectile prediction 2026-02-19 01:01:38 +03:00
131 changed files with 10549 additions and 922 deletions

View File

@@ -12,6 +12,7 @@ AlignConsecutiveMacros: AcrossEmptyLinesAndComments
AlignTrailingComments: false AlignTrailingComments: false
AllowShortBlocksOnASingleLine: Never AllowShortBlocksOnASingleLine: Never
AllowShortFunctionsOnASingleLine: None AllowShortFunctionsOnASingleLine: None
AllowShortLambdasOnASingleLine: None
AllowShortIfStatementsOnASingleLine: false AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: false AllowShortLoopsOnASingleLine: false
BreakTemplateDeclarations: Leave BreakTemplateDeclarations: Leave

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

618
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,618 @@
name: Release
on:
release:
types: [published]
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: write
jobs:
##############################################################################
# 1) Linux Clang / Ninja
##############################################################################
linux-release:
name: ${{ matrix.name }}
runs-on: ${{ matrix.runner }}
strategy:
matrix:
include:
- name: Linux (Clang) (x64-linux)
triplet: x64-linux
runner: ubuntu-latest
preset: linux-release-vcpkg
archive: omath-linux-x64
install_cmd: |
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
sudo add-apt-repository -y "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-21 main"
sudo apt-get update
sudo apt-get install -y git build-essential cmake ninja-build \
zip unzip curl pkg-config ca-certificates \
clang-21 lld-21 libc++-21-dev libc++abi-21-dev
sudo update-alternatives --install /usr/bin/cc cc /usr/bin/clang-21 100
sudo update-alternatives --install /usr/bin/c++ c++ /usr/bin/clang++-21 100
sudo update-alternatives --install /usr/bin/lld lld /usr/bin/lld-21 100
- name: Linux (Clang) (x86-linux)
triplet: x86-linux
runner: ubuntu-latest
preset: linux-release-vcpkg-x86
archive: omath-linux-x86
install_cmd: |
# Add LLVM 21 repository
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
sudo add-apt-repository -y "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-21 main"
# Add GCC Toolchain PPA
sudo add-apt-repository -y "deb http://archive.ubuntu.com/ubuntu plucky main universe"
# Enable i386 architecture
sudo dpkg --add-architecture i386
sudo apt-get update
# Install Clang 21
sudo apt-get install -y git build-essential cmake ninja-build \
zip unzip curl pkg-config ca-certificates \
clang-21 lld-21 libc++-21-dev libc++abi-21-dev
sudo apt-get install -y -t plucky binutils
# Install GCC 15 with multilib support
sudo apt-get install -y gcc-15-multilib g++-15-multilib
# Set up alternatives for Clang
sudo update-alternatives --install /usr/bin/cc cc /usr/bin/clang-21 100
sudo update-alternatives --install /usr/bin/c++ c++ /usr/bin/clang++-21 100
sudo update-alternatives --install /usr/bin/lld lld /usr/bin/lld-21 100
# Set up alternatives for GCC
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-15 100
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-15 100
- name: Linux (Clang) (arm64-linux)
triplet: arm64-linux
runner: ubuntu-24.04-arm
preset: linux-release-vcpkg-arm64
archive: omath-linux-arm64
install_cmd: |
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
sudo add-apt-repository -y "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-21 main"
sudo apt-get update
sudo apt-get install -y git build-essential cmake ninja-build \
zip unzip curl pkg-config ca-certificates \
clang-21 lld-21 libc++-21-dev libc++abi-21-dev
sudo update-alternatives --install /usr/bin/cc cc /usr/bin/clang-21 100
sudo update-alternatives --install /usr/bin/c++ c++ /usr/bin/clang++-21 100
sudo update-alternatives --install /usr/bin/lld lld /usr/bin/lld-21 100
fail-fast: false
env:
VCPKG_ROOT: ${{ github.workspace }}/vcpkg
steps:
- name: Install basic tool-chain
shell: bash
run: ${{ matrix.install_cmd }}
- name: Checkout repository (with sub-modules)
uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up vcpkg
shell: bash
run: |
git clone https://github.com/microsoft/vcpkg "$VCPKG_ROOT"
cd "$VCPKG_ROOT"
./bootstrap-vcpkg.sh
- name: Configure (cmake --preset)
shell: bash
run: |
cmake --preset ${{ matrix.preset }} \
-DVCPKG_INSTALL_OPTIONS="--allow-unsupported" \
-DOMATH_BUILD_TESTS=OFF \
-DOMATH_BUILD_BENCHMARK=OFF \
-DVCPKG_MANIFEST_FEATURES="imgui;avx2"
- name: Build
shell: bash
run: cmake --build cmake-build/build/${{ matrix.preset }} --target omath
- name: Install
shell: bash
run: cmake --install cmake-build/build/${{ matrix.preset }} --prefix staging
- name: Package
shell: bash
run: tar -czf ${{ matrix.archive }}.tar.gz -C staging .
- name: Upload release asset
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: gh release upload "${{ github.event.release.tag_name }}" "${{ matrix.archive }}.tar.gz" --clobber
##############################################################################
# 2) Windows MSVC / Ninja
##############################################################################
windows-release:
name: ${{ matrix.name }}
runs-on: ${{ matrix.runner }}
strategy:
matrix:
include:
- name: Windows (MSVC) (x64-windows)
runner: windows-latest
arch: amd64
preset: windows-release-vcpkg
triplet: x64-windows
archive: omath-windows-x64
- name: Windows (MSVC) (x86-windows)
runner: windows-latest
arch: amd64_x86
preset: windows-release-vcpkg-x86
triplet: x86-windows
archive: omath-windows-x86
- name: Windows (MSVC) (arm64-windows)
runner: windows-11-arm
arch: arm64
preset: windows-release-vcpkg-arm64
triplet: arm64-windows
archive: omath-windows-arm64
fail-fast: false
env:
VCPKG_ROOT: ${{ github.workspace }}/vcpkg
steps:
- name: Checkout repository (with sub-modules)
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Ninja
uses: seanmiddleditch/gha-setup-ninja@v4
- name: Set up MSVC developer command-prompt
uses: ilammy/msvc-dev-cmd@v1
with:
arch: ${{ matrix.arch }}
- name: Configure (cmake --preset)
shell: bash
run: |
cmake --preset ${{ matrix.preset }} \
-DOMATH_BUILD_TESTS=OFF \
-DOMATH_BUILD_BENCHMARK=OFF \
-DVCPKG_MANIFEST_FEATURES="imgui;avx2"
- name: Build
shell: bash
run: cmake --build cmake-build/build/${{ matrix.preset }} --target omath
- name: Install
shell: bash
run: cmake --install cmake-build/build/${{ matrix.preset }} --prefix staging
- name: Package
shell: pwsh
run: Compress-Archive -Path staging\* -DestinationPath "${{ matrix.archive }}.zip"
- name: Upload release asset
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: gh release upload "${{ github.event.release.tag_name }}" "${{ matrix.archive }}.zip" --clobber
##############################################################################
# 3) macOS AppleClang / Ninja
##############################################################################
macos-release:
name: ${{ matrix.name }}
runs-on: ${{ matrix.runner }}
strategy:
matrix:
include:
- name: macOS (AppleClang) (arm64-osx)
runner: macos-latest
preset: darwin-release-vcpkg
triplet: arm64-osx
archive: omath-macos-arm64
- name: macOS (AppleClang) (x64-osx)
runner: macos-15-intel
preset: darwin-release-vcpkg-x64
triplet: x64-osx
archive: omath-macos-x64
fail-fast: false
env:
VCPKG_ROOT: ${{ github.workspace }}/vcpkg
steps:
- name: Install basic tool-chain with Homebrew
shell: bash
run: |
brew install cmake ninja
- name: Checkout repository (with sub-modules)
uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up vcpkg
shell: bash
run: |
git clone https://github.com/microsoft/vcpkg "$VCPKG_ROOT"
- name: Configure (cmake --preset)
shell: bash
run: |
cmake --preset ${{ matrix.preset }} \
-DOMATH_BUILD_TESTS=OFF \
-DOMATH_BUILD_BENCHMARK=OFF \
-DVCPKG_MANIFEST_FEATURES="imgui;avx2"
- name: Build
shell: bash
run: cmake --build cmake-build/build/${{ matrix.preset }} --target omath
- name: Install
shell: bash
run: cmake --install cmake-build/build/${{ matrix.preset }} --prefix staging
- name: Package
shell: bash
run: tar -czf ${{ matrix.archive }}.tar.gz -C staging .
- name: Upload release asset
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: gh release upload "${{ github.event.release.tag_name }}" "${{ matrix.archive }}.tar.gz" --clobber
##############################################################################
# 4) iOS AppleClang / Xcode / arm64-ios
##############################################################################
ios-release:
name: iOS (AppleClang) (${{ matrix.triplet }})
runs-on: macOS-latest
strategy:
matrix:
include:
- triplet: arm64-ios
preset: ios-release-vcpkg
archive: omath-ios-arm64
fail-fast: false
env:
VCPKG_ROOT: ${{ github.workspace }}/vcpkg
steps:
- name: Install CMake tooling
shell: bash
run: |
brew install cmake ninja
- name: Checkout repository (with sub-modules)
uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up vcpkg
shell: bash
run: |
git clone https://github.com/microsoft/vcpkg "$VCPKG_ROOT"
cd "$VCPKG_ROOT"
./bootstrap-vcpkg.sh
- name: Configure (cmake --preset)
shell: bash
run: |
cmake --preset ${{ matrix.preset }} \
-DVCPKG_INSTALL_OPTIONS="--allow-unsupported" \
-DOMATH_BUILD_TESTS=OFF \
-DOMATH_BUILD_BENCHMARK=OFF \
-DVCPKG_MANIFEST_FEATURES="imgui"
- name: Build
shell: bash
run: |
cmake --build cmake-build/build/${{ matrix.preset }} --config Release --target omath
- name: Install
shell: bash
run: cmake --install cmake-build/build/${{ matrix.preset }} --prefix staging
- name: Package
shell: bash
run: tar -czf ${{ matrix.archive }}.tar.gz -C staging .
- name: Upload release asset
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: gh release upload "${{ github.event.release.tag_name }}" "${{ matrix.archive }}.tar.gz" --clobber
##############################################################################
# 5) FreeBSD Clang / Ninja
##############################################################################
freebsd-release:
name: FreeBSD (Clang) (${{ matrix.triplet }})
runs-on: ubuntu-latest
strategy:
matrix:
include:
- triplet: x64-freebsd
preset: freebsd-release-vcpkg
arch: x86-64
archive: omath-freebsd-x64
fail-fast: false
env:
VCPKG_ROOT: ${{ github.workspace }}/tmp/vcpkg
steps:
- name: Checkout repository (with sub-modules)
uses: actions/checkout@v4
with:
submodules: recursive
- name: Build and Package
uses: cross-platform-actions/action@v0.31.0
with:
operating_system: freebsd
architecture: ${{ matrix.arch }}
version: '15.0'
memory: '12G'
cpu_count: 4
run: |
sudo pkg install -y git curl zip unzip gmake llvm gsed bash perl5 openssl 7-zip coreutils cmake ninja pkgconf patchelf
git config --global --add safe.directory `pwd`
# Build vcpkg in /tmp to avoid sshfs timestamp sync issues
export VCPKG_ROOT=/tmp/vcpkg
rm -rf "$VCPKG_ROOT"
git clone https://github.com/microsoft/vcpkg "$VCPKG_ROOT"
cd "$VCPKG_ROOT"
./bootstrap-vcpkg.sh
cd -
export VCPKG_FORCE_SYSTEM_BINARIES=0
cmake --preset ${{ matrix.preset }} \
-DOMATH_BUILD_TESTS=OFF \
-DOMATH_BUILD_BENCHMARK=OFF \
-DVCPKG_MANIFEST_FEATURES="imgui;avx2" \
-DVCPKG_INSTALL_OPTIONS="--allow-unsupported"
cmake --build cmake-build/build/${{ matrix.preset }} --target omath
cmake --install cmake-build/build/${{ matrix.preset }} --prefix staging
tar -czf ${{ matrix.archive }}.tar.gz -C staging .
- name: Upload release asset
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: gh release upload "${{ github.event.release.tag_name }}" "${{ matrix.archive }}.tar.gz" --clobber
##############################################################################
# 6) Android NDK Clang / Ninja
##############################################################################
android-release:
name: Android NDK (${{ matrix.triplet }})
runs-on: ubuntu-latest
strategy:
matrix:
include:
- triplet: arm-neon-android
preset: android-arm-neon-release-vcpkg
archive: omath-android-arm-neon
- triplet: arm64-android
preset: android-arm64-release-vcpkg
archive: omath-android-arm64
- triplet: x64-android
preset: android-x64-release-vcpkg
archive: omath-android-x64
- triplet: x86-android
preset: android-x86-release-vcpkg
archive: omath-android-x86
fail-fast: false
env:
VCPKG_ROOT: ${{ github.workspace }}/vcpkg
ANDROID_NDK_HOME: ${{ github.workspace }}/android-ndk
steps:
- name: Checkout repository (with sub-modules)
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Android NDK
shell: bash
run: |
NDK_VERSION="r28b"
NDK_ZIP="android-ndk-${NDK_VERSION}-linux.zip"
wget -q "https://dl.google.com/android/repository/${NDK_ZIP}"
unzip -q "${NDK_ZIP}" -d "${{ github.workspace }}"
mv "${{ github.workspace }}/android-ndk-${NDK_VERSION}" "$ANDROID_NDK_HOME"
rm "${NDK_ZIP}"
echo "ANDROID_NDK_HOME=${ANDROID_NDK_HOME}" >> $GITHUB_ENV
- name: Install basic tool-chain
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y ninja-build cmake
- name: Set up vcpkg
shell: bash
run: |
git clone https://github.com/microsoft/vcpkg "$VCPKG_ROOT"
cd "$VCPKG_ROOT"
./bootstrap-vcpkg.sh
- name: Configure (cmake --preset)
shell: bash
run: |
cmake --preset ${{ matrix.preset }} \
-DVCPKG_INSTALL_OPTIONS="--allow-unsupported" \
-DOMATH_BUILD_TESTS=OFF \
-DOMATH_BUILD_BENCHMARK=OFF \
-DVCPKG_MANIFEST_FEATURES="imgui"
- name: Build
shell: bash
run: |
cmake --build cmake-build/build/${{ matrix.preset }} --target omath
- name: Install
shell: bash
run: cmake --install cmake-build/build/${{ matrix.preset }} --prefix staging
- name: Package
shell: bash
run: tar -czf ${{ matrix.archive }}.tar.gz -C staging .
- name: Upload release asset
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: gh release upload "${{ github.event.release.tag_name }}" "${{ matrix.archive }}.tar.gz" --clobber
##############################################################################
# 7) WebAssembly (Emscripten) Clang / Ninja / wasm32-emscripten
##############################################################################
wasm-release:
name: WebAssembly (Emscripten) (${{ matrix.triplet }})
runs-on: ubuntu-latest
strategy:
matrix:
include:
- triplet: wasm32-emscripten
preset: wasm-release-vcpkg
archive: omath-wasm32
fail-fast: false
env:
VCPKG_ROOT: ${{ github.workspace }}/vcpkg
steps:
- name: Checkout repository (with sub-modules)
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install basic tool-chain
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y ninja-build
- name: Setup Emscripten
uses: mymindstorm/setup-emsdk@v14
with:
version: 'latest'
- name: Verify Emscripten
shell: bash
run: |
echo "EMSDK=$EMSDK"
emcc --version
ls -la "$EMSDK/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake"
- name: Set up vcpkg
shell: bash
run: |
git clone https://github.com/microsoft/vcpkg "$VCPKG_ROOT"
cd "$VCPKG_ROOT"
./bootstrap-vcpkg.sh
- name: Configure (cmake --preset)
shell: bash
run: |
cmake --preset ${{ matrix.preset }} \
-DVCPKG_INSTALL_OPTIONS="--allow-unsupported" \
-DOMATH_BUILD_TESTS=OFF \
-DOMATH_BUILD_BENCHMARK=OFF \
-DVCPKG_MANIFEST_FEATURES="imgui"
- name: Build
shell: bash
run: |
cmake --build cmake-build/build/${{ matrix.preset }} --target omath
- name: Install
shell: bash
run: cmake --install cmake-build/build/${{ matrix.preset }} --prefix staging
- name: Package
shell: bash
run: tar -czf ${{ matrix.archive }}.tar.gz -C staging .
- name: Upload release asset
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: gh release upload "${{ github.event.release.tag_name }}" "${{ matrix.archive }}.tar.gz" --clobber
##############################################################################
# 8) Windows MSYS2 MinGW GCC / Ninja
##############################################################################
mingw-release:
name: ${{ matrix.name }}
runs-on: windows-latest
strategy:
matrix:
include:
- name: MINGW64 (MSYS2) (x64-mingw-dynamic)
msystem: MINGW64
pkg_prefix: mingw-w64-x86_64
preset: mingw-release-vcpkg
archive: omath-mingw64-x64
- name: UCRT64 (MSYS2) (x64-mingw-dynamic)
msystem: UCRT64
pkg_prefix: mingw-w64-ucrt-x86_64
preset: mingw-release-vcpkg
archive: omath-ucrt64-x64
- name: MINGW32 (MSYS2) (x86-mingw-dynamic)
msystem: MINGW32
pkg_prefix: mingw-w64-i686
preset: mingw32-release-vcpkg
archive: omath-mingw32-x86
fail-fast: false
defaults:
run:
shell: msys2 {0}
env:
VCPKG_ROOT: ${{ github.workspace }}/vcpkg
steps:
- name: Setup MSYS2
uses: msys2/setup-msys2@v2
with:
msystem: ${{ matrix.msystem }}
update: true
install: >-
${{ matrix.pkg_prefix }}-toolchain
${{ matrix.pkg_prefix }}-cmake
${{ matrix.pkg_prefix }}-ninja
${{ matrix.pkg_prefix }}-pkg-config
git
base-devel
- name: Checkout repository (with sub-modules)
uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up vcpkg
run: |
git clone https://github.com/microsoft/vcpkg "$VCPKG_ROOT"
cd "$VCPKG_ROOT"
./bootstrap-vcpkg.sh
- name: Configure (cmake --preset)
run: |
cmake --preset ${{ matrix.preset }} \
-DVCPKG_INSTALL_OPTIONS="--allow-unsupported" \
-DOMATH_BUILD_TESTS=OFF \
-DOMATH_BUILD_BENCHMARK=OFF \
-DVCPKG_MANIFEST_FEATURES="imgui"
- name: Build
run: |
cmake --build cmake-build/build/${{ matrix.preset }} --target omath
- name: Install
run: cmake --install cmake-build/build/${{ matrix.preset }} --prefix staging
- name: Package
shell: pwsh
run: Compress-Archive -Path staging\* -DestinationPath "${{ matrix.archive }}.zip"
- name: Upload release asset
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: gh release upload "${{ github.event.release.tag_name }}" "${{ matrix.archive }}.zip" --clobber

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}")
@@ -130,11 +147,10 @@ set_target_properties(
CXX_STANDARD 23 CXX_STANDARD 23
CXX_STANDARD_REQUIRED ON) CXX_STANDARD_REQUIRED ON)
if (OMATH_STATIC_MSVC_RUNTIME_LIBRARY) if(OMATH_STATIC_MSVC_RUNTIME_LIBRARY)
set_target_properties(${PROJECT_NAME} PROPERTIES set_target_properties(${PROJECT_NAME} PROPERTIES MSVC_RUNTIME_LIBRARY
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>" "MultiThreaded$<$<CONFIG:Debug>:Debug>")
) endif()
endif ()
if(OMATH_USE_AVX2) if(OMATH_USE_AVX2)
if(MSVC) if(MSVC)
@@ -175,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

@@ -5,6 +5,7 @@ Thanks to everyone who made this possible, including:
- Saikari aka luadebug for VCPKG port and awesome new initial logo design. - Saikari aka luadebug for VCPKG port and awesome new initial logo design.
- AmbushedRaccoon for telegram post about omath to boost repository activity. - AmbushedRaccoon for telegram post about omath to boost repository activity.
- Billy O'Neal aka BillyONeal for fixing compilation issues due to C math library compatibility. - Billy O'Neal aka BillyONeal for fixing compilation issues due to C math library compatibility.
- Alex2772 for reference of AUI declarative interface design for omath::hud
And a big hand to everyone else who has contributed over the past! And a big hand to everyone else who has contributed over the past!

View File

@@ -1,6 +1,6 @@
# 📥Installation Guide # 📥Installation Guide
## <img width="28px" src="https://vcpkg.io/assets/mark/mark.svg" /> Using vcpkg ## <img width="28px" src="https://vcpkg.io/assets/mark/mark.svg" /> Using vcpkg (recomended)
**Note**: Support vcpkg for package management **Note**: Support vcpkg for package management
1. Install [vcpkg](https://github.com/microsoft/vcpkg) 1. Install [vcpkg](https://github.com/microsoft/vcpkg)
2. Run the following command to install the orange-math package: 2. Run the following command to install the orange-math package:
@@ -28,6 +28,46 @@ target("...")
add_packages("omath") add_packages("omath")
``` ```
## <img width="28px" src="https://github.githubassets.com/favicons/favicon.svg" /> Using prebuilt binaries (GitHub Releases)
**Note**: This is the fastest option if you dont want to build from source.
1. **Go to the Releases page**
- Open the projects GitHub **Releases** page and choose the latest version.
2. **Download the correct asset for your platform**
- Pick the archive that matches your OS and architecture (for example: Windows x64 / Linux x64 / macOS arm64).
3. **Extract the archive**
- You should end up with something like:
- `include/` (headers)
- `lib/` or `bin/` (library files / DLLs)
- sometimes `cmake/` (CMake package config)
4. **Use it in your project**
### Option A: CMake package (recommended if the release includes CMake config files)
If the extracted folder contains something like `lib/cmake/omath` or `cmake/omath`, you can point CMake to it:
```cmake
# Example: set this to the extracted prebuilt folder
list(APPEND CMAKE_PREFIX_PATH "path/to/omath-prebuilt")
find_package(omath CONFIG REQUIRED)
target_link_libraries(main PRIVATE omath::omath)
```
### Option B: Manual include + link (works with any layout)
If theres no CMake package config, link it manually:
```cmake
target_include_directories(main PRIVATE "path/to/omath-prebuilt/include")
# Choose ONE depending on what you downloaded:
# - Static library: .lib / .a
# - Shared library: .dll + .lib import (Windows), .so (Linux), .dylib (macOS)
target_link_directories(main PRIVATE "path/to/omath-prebuilt/lib")
target_link_libraries(main PRIVATE omath) # or the actual library filename
```
## <img width="28px" src="https://upload.wikimedia.org/wikipedia/commons/e/ef/CMake_logo.svg?" /> Build from source using CMake ## <img width="28px" src="https://upload.wikimedia.org/wikipedia/commons/e/ef/CMake_logo.svg?" /> Build from source using CMake
1. **Preparation** 1. **Preparation**

View File

@@ -9,11 +9,12 @@
[![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)
OMath is a 100% independent, constexpr template blazingly fast math library that doesn't have legacy C++ code. OMath is a 100% independent, constexpr template blazingly fast math/physics/games/mods/cheats development framework that doesn't have legacy C++ code.
It provides the latest features, is highly customizable, has all for cheat development, DirectX/OpenGL/Vulkan support, premade support for different game engines, much more constexpr stuff than in other libraries and more... It provides the latest features, is highly customizable, has all for cheat development, DirectX/OpenGL/Vulkan support, premade support for different game engines, much more constexpr stuff than in other libraries and more...
<br> <br>
@@ -80,9 +81,12 @@ if (auto screen = camera.world_to_screen(world_position)) {
- **Collision Detection**: Production ready code to handle collision detection by using simple interfaces. - **Collision Detection**: Production ready code to handle collision detection by using simple interfaces.
- **No Additional Dependencies**: No additional dependencies need to use OMath except unit test execution - **No Additional Dependencies**: No additional dependencies need to use OMath except unit test execution
- **Ready for meta-programming**: Omath use templates for common types like Vectors, Matrixes etc, to handle all types! - **Ready for meta-programming**: Omath use templates for common types like Vectors, Matrixes etc, to handle all types!
- **Engine support**: Supports coordinate systems of **Source, Unity, Unreal, Frostbite, IWEngine 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 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.
- **Handy**: Allow to design wall hacks in modern jetpack compose like way.
- **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>
# Gallery # Gallery

View File

@@ -1 +1 @@
4.7.1 5.0.0

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

@@ -1,35 +1,8 @@
project(examples) add_subdirectory(example_barycentric)
add_subdirectory(example_glfw3)
add_executable(example_projection_matrix_builder example_proj_mat_builder.cpp) add_subdirectory(example_proj_mat_builder)
set_target_properties( add_subdirectory(example_signature_scan)
example_projection_matrix_builder add_subdirectory(example_hud)
PROPERTIES CXX_STANDARD 23
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}")
target_link_libraries(example_projection_matrix_builder PRIVATE omath::omath)
add_executable(example_signature_scan example_signature_scan.cpp)
set_target_properties(
example_signature_scan
PROPERTIES CXX_STANDARD 23
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}")
target_link_libraries(example_signature_scan PRIVATE omath::omath)
add_executable(example_glfw3 example_glfw3.cpp)
set_target_properties(
example_glfw3
PROPERTIES CXX_STANDARD 23
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}")
find_package(OpenGL)
find_package(GLEW REQUIRED)
find_package(glfw3 CONFIG REQUIRED)
target_link_libraries(example_glfw3 PRIVATE omath::omath GLEW::GLEW glfw OpenGL::GL)
if(OMATH_ENABLE_VALGRIND) if(OMATH_ENABLE_VALGRIND)
omath_setup_valgrind(example_projection_matrix_builder) omath_setup_valgrind(example_projection_matrix_builder)

View File

@@ -0,0 +1,14 @@
project(example_barycentric)
add_executable(${PROJECT_NAME} example_barycentric.cpp)
set_target_properties(
${PROJECT_NAME}
PROPERTIES CXX_STANDARD 23
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}")
find_package(OpenGL)
find_package(GLEW REQUIRED)
find_package(glfw3 CONFIG REQUIRED)
target_link_libraries(${PROJECT_NAME} PRIVATE omath::omath GLEW::GLEW glfw OpenGL::GL)

View File

@@ -0,0 +1,427 @@
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <cmath>
#include <iostream>
#include <omath/omath.hpp>
#include <vector>
using omath::Color;
using omath::Triangle;
using omath::Vector3;
static const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in float aPointSize;
layout (location = 3) in float aIsLine;
out vec3 vColor;
out float vIsLine;
void main() {
gl_Position = vec4(aPos, 1.0);
vColor = aColor;
gl_PointSize = aPointSize;
vIsLine = aIsLine;
}
)";
static const char* fragmentShaderSource = R"(
#version 330 core
in vec3 vColor;
in float vIsLine;
out vec4 FragColor;
void main() {
if (vIsLine < 0.5) {
// Calculate distance from center of the point
vec2 coord = gl_PointCoord - vec2(0.5);
if(length(coord) > 0.5)
discard;
}
FragColor = vec4(vColor, 1.0);
}
)";
GLuint compileShader(GLenum type, const char* src)
{
GLuint shader = glCreateShader(type);
glShaderSource(shader, 1, &src, nullptr);
glCompileShader(shader);
GLint ok;
glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
if (!ok)
{
char log[1024];
glGetShaderInfoLog(shader, sizeof(log), nullptr, log);
std::cerr << "Shader error: " << log << std::endl;
}
return shader;
}
void drawChar(char c, float x, float y, float scale, const Color& color, std::vector<float>& lines)
{
float w = 0.5f * scale;
float h = 1.0f * scale;
auto add = [&](float x1, float y1, float x2, float y2)
{
lines.push_back(x + x1 * w);
lines.push_back(y + y1 * h);
lines.push_back(0.0f);
lines.push_back(color.x);
lines.push_back(color.y);
lines.push_back(color.z);
lines.push_back(1.0f); // size
lines.push_back(1.0f); // isLine
lines.push_back(x + x2 * w);
lines.push_back(y + y2 * h);
lines.push_back(0.0f);
lines.push_back(color.x);
lines.push_back(color.y);
lines.push_back(color.z);
lines.push_back(1.0f); // size
lines.push_back(1.0f); // isLine
};
switch (c)
{
case '0':
add(0, 0, 1, 0);
add(1, 0, 1, 1);
add(1, 1, 0, 1);
add(0, 1, 0, 0);
break;
case '1':
add(0.5f, 0, 0.5f, 1);
add(0.25f, 0.75f, 0.5f, 1);
add(0.25f, 0, 0.75f, 0);
break;
case '2':
add(0, 1, 1, 1);
add(1, 1, 1, 0.5f);
add(1, 0.5f, 0, 0.5f);
add(0, 0.5f, 0, 0);
add(0, 0, 1, 0);
break;
case '3':
add(0, 1, 1, 1);
add(1, 1, 1, 0);
add(1, 0, 0, 0);
add(0, 0.5f, 1, 0.5f);
break;
case '4':
add(0, 1, 0, 0.5f);
add(0, 0.5f, 1, 0.5f);
add(1, 1, 1, 0);
break;
case '5':
add(1, 1, 0, 1);
add(0, 1, 0, 0.5f);
add(0, 0.5f, 1, 0.5f);
add(1, 0.5f, 1, 0);
add(1, 0, 0, 0);
break;
case '6':
add(1, 1, 0, 1);
add(0, 1, 0, 0);
add(0, 0, 1, 0);
add(1, 0, 1, 0.5f);
add(1, 0.5f, 0, 0.5f);
break;
case '7':
add(0, 1, 1, 1);
add(1, 1, 0.5f, 0);
break;
case '8':
add(0, 0, 1, 0);
add(1, 0, 1, 1);
add(1, 1, 0, 1);
add(0, 1, 0, 0);
add(0, 0.5f, 1, 0.5f);
break;
case '9':
add(1, 0.5f, 0, 0.5f);
add(0, 0.5f, 0, 1);
add(0, 1, 1, 1);
add(1, 1, 1, 0);
add(1, 0, 0, 0);
break;
case '.':
add(0.4f, 0, 0.6f, 0);
add(0.6f, 0, 0.6f, 0.2f);
add(0.6f, 0.2f, 0.4f, 0.2f);
add(0.4f, 0.2f, 0.4f, 0);
break;
}
}
void drawText(const std::string& text, float x, float y, float scale, const Color& color, std::vector<float>& lines)
{
float cursor = x;
for (char c : text)
{
drawChar(c, cursor, y, scale, color, lines);
cursor += (c == '.' ? 0.3f : 0.7f) * scale;
}
}
GLFWwindow* initWindow(int width, int height, const char* title)
{
if (!glfwInit())
{
std::cerr << "Failed to initialize GLFW\n";
return nullptr;
}
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow* window = glfwCreateWindow(width, height, title, NULL, NULL);
if (!window)
{
std::cerr << "Failed to create GLFW window\n";
glfwTerminate();
return nullptr;
}
glfwMakeContextCurrent(window);
// Check if context is valid using standard GL
const GLubyte* renderer = glGetString(GL_RENDERER);
const GLubyte* version = glGetString(GL_VERSION);
if (renderer && version)
{
std::cout << "Renderer: " << renderer << "\n";
std::cout << "OpenGL version supported: " << version << "\n";
}
else
{
std::cerr << "Failed to get GL_RENDERER or GL_VERSION. Context might be invalid.\n";
}
glewExperimental = GL_TRUE;
GLenum glewErr = glewInit();
if (glewErr != GLEW_OK)
{
// Ignore GLEW_ERROR_NO_GLX_DISPLAY if we have a valid context (e.g. Wayland)
if (glewErr == GLEW_ERROR_NO_GLX_DISPLAY && renderer)
{
std::cerr << "GLEW warning: " << glewGetErrorString(glewErr) << " (Ignored because context seems valid)\n";
}
else
{
std::cerr << "Failed to initialize GLEW: " << glewGetErrorString(glewErr) << "\n";
glfwTerminate();
return nullptr;
}
}
return window;
}
GLuint createShaderProgram()
{
GLuint vs = compileShader(GL_VERTEX_SHADER, vertexShaderSource);
GLuint fs = compileShader(GL_FRAGMENT_SHADER, fragmentShaderSource);
GLuint shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vs);
glAttachShader(shaderProgram, fs);
glLinkProgram(shaderProgram);
return shaderProgram;
}
void generatePointCloud(std::vector<float>& pointCloud, const Triangle<Vector3<float>>& triangle)
{
const auto& A = triangle.m_vertex1;
const auto& B = triangle.m_vertex2;
const auto& C = triangle.m_vertex3;
// Iterating over barycentric coordinates (u, v, w) from 0.0 to 1.0
for (float u = 0.0f; u <= 1.0f; u += 0.015f)
{
for (float v = 0.0f; v <= 1.0f - u; v += 0.015f)
{
float w = 1.0f - u - v;
if (w >= 0.0f && w <= 1.0f)
{
Vector3<float> P = A * u + B * v + C * w;
pointCloud.push_back(P.x);
pointCloud.push_back(P.y);
pointCloud.push_back(P.z);
pointCloud.push_back(u);
pointCloud.push_back(v);
pointCloud.push_back(w);
pointCloud.push_back(2.0f); // size
pointCloud.push_back(0.0f); // isLine
}
}
}
}
void setupBuffers(GLuint& VAO_cloud, GLuint& VBO_cloud, const std::vector<float>& pointCloud, GLuint& VAO_dyn,
GLuint& VBO_dyn)
{
glGenVertexArrays(1, &VAO_cloud);
glGenBuffers(1, &VBO_cloud);
glBindVertexArray(VAO_cloud);
glBindBuffer(GL_ARRAY_BUFFER, VBO_cloud);
glBufferData(GL_ARRAY_BUFFER, pointCloud.size() * sizeof(float), pointCloud.data(), GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
glVertexAttribPointer(3, 1, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(7 * sizeof(float)));
glEnableVertexAttribArray(3);
glGenVertexArrays(1, &VAO_dyn);
glGenBuffers(1, &VBO_dyn);
glBindVertexArray(VAO_dyn);
glBindBuffer(GL_ARRAY_BUFFER, VBO_dyn);
glBufferData(GL_ARRAY_BUFFER, 1000 * 8 * sizeof(float), NULL, GL_DYNAMIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
glVertexAttribPointer(3, 1, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(7 * sizeof(float)));
glEnableVertexAttribArray(3);
}
void updateDynamicData(std::vector<float>& dynData, float u, float v, float w, const Vector3<float>& P,
const Triangle<Vector3<float>>& triangle)
{
const auto& A = triangle.m_vertex1;
const auto& B = triangle.m_vertex2;
const auto& C = triangle.m_vertex3;
float sizeA = 10.0f + u * 30.0f;
float sizeB = 10.0f + v * 30.0f;
float sizeC = 10.0f + w * 30.0f;
float sizeP = 12.0f;
dynData = {// Lines from P to A, B, C
P.x, P.y, P.z, u, v, w, 1.0f, 1.0f, A.x, A.y, A.z, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f,
P.x, P.y, P.z, u, v, w, 1.0f, 1.0f, B.x, B.y, B.z, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f,
P.x, P.y, P.z, u, v, w, 1.0f, 1.0f, C.x, C.y, C.z, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
// The animated dot itself (White)
P.x, P.y, P.z, 1.0f, 1.0f, 1.0f, sizeP, 0.0f,
// The 3 corner dots
A.x, A.y, A.z, 1.0f, 0.0f, 0.0f, sizeA, 0.0f, B.x, B.y, B.z, 0.0f, 1.0f, 0.0f, sizeB, 0.0f, C.x, C.y,
C.z, 0.0f, 0.0f, 1.0f, sizeC, 0.0f};
char bufA[16], bufB[16], bufC[16];
snprintf(bufA, sizeof(bufA), "%.2f", u);
snprintf(bufB, sizeof(bufB), "%.2f", v);
snprintf(bufC, sizeof(bufC), "%.2f", w);
// Keep text at a fixed distance from the dots
float distA = 0.13f;
float distB = 0.13f;
float distC = 0.13f;
drawText(bufA, A.x - 0.05f, A.y + distA, 0.1f, Color(1, 0, 0, 1), dynData);
drawText(bufB, B.x - 0.15f - distB, B.y - 0.05f - distB, 0.1f, Color(0, 1, 0, 1), dynData);
drawText(bufC, C.x + 0.05f + distC, C.y - 0.05f - distC, 0.1f, Color(0, 0, 1, 1), dynData);
}
int main()
{
GLFWwindow* window = initWindow(800, 800, "Barycentric Coordinates");
if (!window)
return -1;
GLuint shaderProgram = createShaderProgram();
// Triangle vertices as shown in the picture (Red, Green, Blue)
// Scaled down slightly to leave room for text
Triangle<Vector3<float>> triangle(Vector3<float>(0.0f, 0.6f, 0.0f), // Red dot (top)
Vector3<float>(-0.6f, -0.6f, 0.0f), // Green dot (bottom left)
Vector3<float>(0.6f, -0.6f, 0.0f) // Blue dot (bottom right)
);
std::vector<float> pointCloud;
generatePointCloud(pointCloud, triangle);
GLuint VAO_cloud, VBO_cloud, VAO_dyn, VBO_dyn;
setupBuffers(VAO_cloud, VBO_cloud, pointCloud, VAO_dyn, VBO_dyn);
glEnable(GL_PROGRAM_POINT_SIZE);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
while (!glfwWindowShouldClose(window))
{
glClearColor(0.02f, 0.02f, 0.02f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(shaderProgram);
// Draw the point cloud (the iterated points)
glBindVertexArray(VAO_cloud);
glDrawArrays(GL_POINTS, 0, pointCloud.size() / 8);
// Animate the white dot to simulate dragging
float t = glfwGetTime();
float u = (std::sin(t * 1.5f) * 0.5f + 0.5f);
float v = (std::cos(t * 1.1f) * 0.5f + 0.5f);
if (u + v > 1.0f)
{
u = 1.0f - u;
v = 1.0f - v;
}
float w = 1.0f - u - v;
if (w > 1.0f)
{
float diff = w - 1.0f;
w = 1.0f;
u += diff / 2.0f;
v += diff / 2.0f;
}
else if (w < 0.0f)
{
float diff = -w;
w = 0.0f;
u -= diff / 2.0f;
v -= diff / 2.0f;
}
Vector3<float> P = triangle.m_vertex1 * u + triangle.m_vertex2 * v + triangle.m_vertex3 * w;
std::vector<float> dynData;
updateDynamicData(dynData, u, v, w, P, triangle);
glBindVertexArray(VAO_dyn);
glBindBuffer(GL_ARRAY_BUFFER, VBO_dyn);
glBufferSubData(GL_ARRAY_BUFFER, 0, dynData.size() * sizeof(float), dynData.data());
// Draw lines
glDrawArrays(GL_LINES, 0, 6);
// Draw text lines
int numTextVertices = (dynData.size() / 8) - 10;
if (numTextVertices > 0)
{
glDrawArrays(GL_LINES, 10, numTextVertices);
}
// Draw dots
glDrawArrays(GL_POINTS, 6, 4);
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwTerminate();
return 0;
}

View File

@@ -0,0 +1,14 @@
project(example_glfw3)
add_executable(${PROJECT_NAME} example_glfw3.cpp)
set_target_properties(
${PROJECT_NAME}
PROPERTIES CXX_STANDARD 23
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}")
find_package(OpenGL)
find_package(GLEW REQUIRED)
find_package(glfw3 CONFIG REQUIRED)
target_link_libraries(${PROJECT_NAME} PRIVATE omath::omath GLEW::GLEW glfw OpenGL::GL)

View File

@@ -342,7 +342,7 @@ int main()
old_mouse_time = glfwGetTime(); old_mouse_time = glfwGetTime();
mouse_capture = !mouse_capture; mouse_capture = !mouse_capture;
glfwSetInputMode(window, GLFW_CURSOR, mouse_capture ? GLFW_CURSOR_CAPTURED : GLFW_CURSOR_NORMAL); glfwSetInputMode(window, GLFW_CURSOR, mouse_capture ? GLFW_CURSOR_HIDDEN : GLFW_CURSOR_NORMAL);
} }
if (mouse_capture) if (mouse_capture)
{ {

View File

@@ -0,0 +1,16 @@
project(example_hud)
add_executable(${PROJECT_NAME} main.cpp gui/main_window.cpp gui/main_window.hpp)
set_target_properties(
${PROJECT_NAME}
PROPERTIES CXX_STANDARD 23
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}")
find_package(OpenGL)
find_package(GLEW REQUIRED)
find_package(glfw3 CONFIG REQUIRED)
target_link_libraries(${PROJECT_NAME} PRIVATE glfw imgui::imgui omath::omath OpenGL::GL)

View File

@@ -0,0 +1,263 @@
//
// Created by Orange on 11/11/2024.
//
#include "main_window.hpp"
#include "omath/hud/renderer_realizations/imgui_renderer.hpp"
#include <GLFW/glfw3.h>
#include <imgui.h>
#include <imgui_impl_glfw.h>
#include <imgui_impl_opengl3.h>
#include <omath/hud/entity_overlay.hpp>
namespace imgui_desktop::gui
{
bool MainWindow::m_canMoveWindow = false;
MainWindow::MainWindow(const std::string_view& caption, int width, int height)
{
if (!glfwInit())
std::exit(EXIT_FAILURE);
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
glfwWindowHint(GLFW_TRANSPARENT_FRAMEBUFFER, true);
m_window = glfwCreateWindow(width, height, caption.data(), nullptr, nullptr);
glfwMakeContextCurrent(m_window);
ImGui::CreateContext();
ImGui::StyleColorsDark();
ImGui::GetStyle().Colors[ImGuiCol_WindowBg] = {0.05f, 0.05f, 0.05f, 0.92f};
ImGui::GetStyle().AntiAliasedLines = false;
ImGui::GetStyle().AntiAliasedFill = false;
ImGui_ImplGlfw_InitForOpenGL(m_window, true);
ImGui_ImplOpenGL3_Init("#version 150");
}
void MainWindow::Run()
{
while (!glfwWindowShouldClose(m_window) && m_opened)
{
glfwPollEvents();
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
const auto* vp = ImGui::GetMainViewport();
ImGui::GetBackgroundDrawList()->AddRectFilled({}, vp->Size, ImColor(30, 30, 30, 220));
draw_controls();
draw_overlay();
ImGui::Render();
present();
}
glfwDestroyWindow(m_window);
}
void MainWindow::draw_controls()
{
const auto* vp = ImGui::GetMainViewport();
ImGui::SetNextWindowPos({0.f, 0.f});
ImGui::SetNextWindowSize({280.f, vp->Size.y});
ImGui::Begin("Controls", &m_opened,
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse);
ImGui::PushItemWidth(160.f);
if (ImGui::CollapsingHeader("Entity", ImGuiTreeNodeFlags_DefaultOpen))
{
ImGui::SliderFloat("X##ent", &m_entity_x, 100.f, vp->Size.x - 100.f);
ImGui::SliderFloat("Top Y", &m_entity_top_y, 20.f, m_entity_bottom_y - 20.f);
ImGui::SliderFloat("Bottom Y", &m_entity_bottom_y, m_entity_top_y + 20.f, vp->Size.y - 20.f);
}
if (ImGui::CollapsingHeader("Box", ImGuiTreeNodeFlags_DefaultOpen))
{
ImGui::Checkbox("Box##chk", &m_show_box);
ImGui::SameLine();
ImGui::Checkbox("Cornered", &m_show_cornered_box);
ImGui::SameLine();
ImGui::Checkbox("Dashed", &m_show_dashed_box);
ImGui::ColorEdit4("Color##box", reinterpret_cast<float*>(&m_box_color), ImGuiColorEditFlags_NoInputs);
ImGui::ColorEdit4("Fill##box", reinterpret_cast<float*>(&m_box_fill), ImGuiColorEditFlags_NoInputs);
ImGui::SliderFloat("Thickness", &m_box_thickness, 0.5f, 5.f);
ImGui::SliderFloat("Corner ratio", &m_corner_ratio, 0.05f, 0.5f);
ImGui::Separator();
ImGui::ColorEdit4("Dash color", reinterpret_cast<float*>(&m_dash_color), ImGuiColorEditFlags_NoInputs);
ImGui::SliderFloat("Dash length", &m_dash_len, 2.f, 30.f);
ImGui::SliderFloat("Dash gap", &m_dash_gap, 1.f, 20.f);
ImGui::SliderFloat("Dash thick", &m_dash_thickness, 0.5f, 5.f);
}
if (ImGui::CollapsingHeader("Bars", ImGuiTreeNodeFlags_DefaultOpen))
{
ImGui::ColorEdit4("Color##bar", reinterpret_cast<float*>(&m_bar_color), ImGuiColorEditFlags_NoInputs);
ImGui::ColorEdit4("BG##bar", reinterpret_cast<float*>(&m_bar_bg_color), ImGuiColorEditFlags_NoInputs);
ImGui::ColorEdit4("Outline##bar", reinterpret_cast<float*>(&m_bar_outline_color),
ImGuiColorEditFlags_NoInputs);
ImGui::SliderFloat("Width##bar", &m_bar_width, 1.f, 20.f);
ImGui::SliderFloat("Value##bar", &m_bar_value, 0.f, 1.f);
ImGui::SliderFloat("Offset##bar", &m_bar_offset, 1.f, 20.f);
ImGui::Checkbox("Right##bar", &m_show_right_bar);
ImGui::SameLine();
ImGui::Checkbox("Left##bar", &m_show_left_bar);
ImGui::Checkbox("Top##bar", &m_show_top_bar);
ImGui::SameLine();
ImGui::Checkbox("Bottom##bar", &m_show_bottom_bar);
ImGui::Checkbox("Right dashed##bar", &m_show_right_dashed_bar);
ImGui::SameLine();
ImGui::Checkbox("Left dashed##bar", &m_show_left_dashed_bar);
ImGui::Checkbox("Top dashed##bar", &m_show_top_dashed_bar);
ImGui::SameLine();
ImGui::Checkbox("Bot dashed##bar", &m_show_bottom_dashed_bar);
ImGui::SliderFloat("Dash len##bar", &m_bar_dash_len, 2.f, 20.f);
ImGui::SliderFloat("Dash gap##bar", &m_bar_dash_gap, 1.f, 15.f);
}
if (ImGui::CollapsingHeader("Labels", ImGuiTreeNodeFlags_DefaultOpen))
{
ImGui::Checkbox("Outlined", &m_outlined);
ImGui::SliderFloat("Offset##lbl", &m_label_offset, 0.f, 15.f);
ImGui::Checkbox("Right##lbl", &m_show_right_labels);
ImGui::SameLine();
ImGui::Checkbox("Left##lbl", &m_show_left_labels);
ImGui::Checkbox("Top##lbl", &m_show_top_labels);
ImGui::SameLine();
ImGui::Checkbox("Bottom##lbl", &m_show_bottom_labels);
ImGui::Checkbox("Ctr top##lbl", &m_show_centered_top);
ImGui::SameLine();
ImGui::Checkbox("Ctr bot##lbl", &m_show_centered_bottom);
}
if (ImGui::CollapsingHeader("Skeleton"))
{
ImGui::Checkbox("Show##skel", &m_show_skeleton);
ImGui::ColorEdit4("Color##skel", reinterpret_cast<float*>(&m_skel_color), ImGuiColorEditFlags_NoInputs);
ImGui::SliderFloat("Thick##skel", &m_skel_thickness, 0.5f, 5.f);
}
if (ImGui::CollapsingHeader("Progress Ring"))
{
ImGui::Checkbox("Show##ring", &m_show_ring);
ImGui::ColorEdit4("Color##ring", reinterpret_cast<float*>(&m_ring_color), ImGuiColorEditFlags_NoInputs);
ImGui::ColorEdit4("BG##ring", reinterpret_cast<float*>(&m_ring_bg), ImGuiColorEditFlags_NoInputs);
ImGui::SliderFloat("Radius##ring", &m_ring_radius, 4.f, 30.f);
ImGui::SliderFloat("Value##ring", &m_ring_ratio, 0.f, 1.f);
ImGui::SliderFloat("Thick##ring", &m_ring_thickness, 0.5f, 6.f);
ImGui::SliderFloat("Offset##ring", &m_ring_offset, 0.f, 15.f);
}
if (ImGui::CollapsingHeader("Scan Marker"))
{
ImGui::Checkbox("Show##scan", &m_show_scan);
ImGui::ColorEdit4("Fill##scan", reinterpret_cast<float*>(&m_scan_color), ImGuiColorEditFlags_NoInputs);
ImGui::ColorEdit4("Outline##scan", reinterpret_cast<float*>(&m_scan_outline), ImGuiColorEditFlags_NoInputs);
ImGui::SliderFloat("Thick##scan", &m_scan_outline_thickness, 0.5f, 5.f);
}
if (ImGui::CollapsingHeader("Aim Dot"))
{
ImGui::Checkbox("Show##aim", &m_show_aim);
ImGui::ColorEdit4("Color##aim", reinterpret_cast<float*>(&m_aim_color), ImGuiColorEditFlags_NoInputs);
ImGui::SliderFloat("Radius##aim", &m_aim_radius, 1.f, 10.f);
}
if (ImGui::CollapsingHeader("Projectile Aim"))
{
ImGui::Checkbox("Show##proj", &m_show_proj);
ImGui::ColorEdit4("Color##proj", reinterpret_cast<float*>(&m_proj_color), ImGuiColorEditFlags_NoInputs);
ImGui::SliderFloat("Size##proj", &m_proj_size, 1.f, 30.f);
ImGui::SliderFloat("Line width##proj", &m_proj_line_width, 0.5f, 5.f);
ImGui::SliderFloat("Pos X##proj", &m_proj_pos_x, 0.f, vp->Size.x);
ImGui::SliderFloat("Pos Y##proj", &m_proj_pos_y, 0.f, vp->Size.y);
ImGui::Combo("Figure##proj", &m_proj_figure, "Circle\0Square\0");
}
if (ImGui::CollapsingHeader("Snap Line"))
{
ImGui::Checkbox("Show##snap", &m_show_snap);
ImGui::ColorEdit4("Color##snap", reinterpret_cast<float*>(&m_snap_color), ImGuiColorEditFlags_NoInputs);
ImGui::SliderFloat("Width##snap", &m_snap_width, 0.5f, 5.f);
}
ImGui::PopItemWidth();
ImGui::End();
}
void MainWindow::draw_overlay()
{
using namespace omath::hud::widget;
using omath::hud::when;
const auto* vp = ImGui::GetMainViewport();
const Bar bar{m_bar_color, m_bar_outline_color, m_bar_bg_color, m_bar_width, m_bar_value, m_bar_offset};
const DashedBar dbar{m_bar_color, m_bar_outline_color, m_bar_bg_color, m_bar_width,
m_bar_value, m_bar_dash_len, m_bar_dash_gap, m_bar_offset};
omath::hud::EntityOverlay({m_entity_x, m_entity_top_y}, {m_entity_x, m_entity_bottom_y},
std::make_shared<omath::hud::ImguiHudRenderer>())
.contents(
// ── Boxes ────────────────────────────────────────────────────
when(m_show_box, Box{m_box_color, m_box_fill, m_box_thickness}),
when(m_show_cornered_box, CorneredBox{omath::Color::from_rgba(255, 0, 255, 255), m_box_fill,
m_corner_ratio, m_box_thickness}),
when(m_show_dashed_box, DashedBox{m_dash_color, m_dash_len, m_dash_gap, m_dash_thickness}),
RightSide{
when(m_show_right_bar, bar),
when(m_show_right_dashed_bar, dbar),
when(m_show_right_labels,
Label{{0.f, 1.f, 0.f, 1.f}, m_label_offset, m_outlined, "Health: 100/100"}),
when(m_show_right_labels,
Label{{1.f, 0.f, 0.f, 1.f}, m_label_offset, m_outlined, "Shield: 125/125"}),
when(m_show_right_labels,
Label{{1.f, 0.f, 1.f, 1.f}, m_label_offset, m_outlined, "*LOCKED*"}),
SpaceVertical{10},
when(m_show_ring, ProgressRing{m_ring_color, m_ring_bg, m_ring_radius, m_ring_ratio,
m_ring_thickness, m_ring_offset}),
},
LeftSide{
when(m_show_left_bar, bar),
when(m_show_left_dashed_bar, dbar),
when(m_show_left_labels, Label{omath::Color::from_rgba(255, 128, 0, 255),
m_label_offset, m_outlined, "Armor: 75"}),
when(m_show_left_labels, Label{omath::Color::from_rgba(0, 200, 255, 255),
m_label_offset, m_outlined, "Level: 42"}),
},
TopSide{
when(m_show_top_bar, bar),
when(m_show_top_dashed_bar, dbar),
when(m_show_centered_top, Centered{Label{omath::Color::from_rgba(0, 255, 255, 255),
m_label_offset, m_outlined, "*VISIBLE*"}}),
when(m_show_top_labels, Label{omath::Color::from_rgba(255, 255, 0, 255), m_label_offset,
m_outlined, "*SCOPED*"}),
when(m_show_top_labels, Label{omath::Color::from_rgba(255, 0, 0, 255), m_label_offset,
m_outlined, "*BLEEDING*"}),
},
BottomSide{
when(m_show_bottom_bar, bar),
when(m_show_bottom_dashed_bar, dbar),
when(m_show_centered_bottom, Centered{Label{omath::Color::from_rgba(255, 255, 255, 255),
m_label_offset, m_outlined, "PlayerName"}}),
when(m_show_bottom_labels, Label{omath::Color::from_rgba(200, 200, 0, 255),
m_label_offset, m_outlined, "42m"}),
},
when(m_show_aim, AimDot{{m_entity_x, m_entity_top_y+40.f}, m_aim_color, m_aim_radius}),
when(m_show_scan, ScanMarker{m_scan_color, m_scan_outline, m_scan_outline_thickness}),
when(m_show_skeleton, Skeleton{m_skel_color, m_skel_thickness}),
when(m_show_proj, ProjectileAim{{m_proj_pos_x, m_proj_pos_y}, m_proj_color, m_proj_size, m_proj_line_width, static_cast<ProjectileAim::Figure>(m_proj_figure)}),
when(m_show_snap, SnapLine{{vp->Size.x / 2.f, vp->Size.y}, m_snap_color, m_snap_width}));
}
void MainWindow::present()
{
int w, h;
glfwGetFramebufferSize(m_window, &w, &h);
glViewport(0, 0, w, h);
glClearColor(0.f, 0.f, 0.f, 0.f);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
glfwSwapBuffers(m_window);
}
} // namespace imgui_desktop::gui

View File

@@ -0,0 +1,94 @@
//
// Created by Orange on 11/11/2024.
//
#pragma once
#include <omath/hud/entity_overlay.hpp>
#include <omath/utility/color.hpp>
#include <string_view>
struct GLFWwindow;
namespace imgui_desktop::gui
{
class MainWindow
{
public:
MainWindow(const std::string_view& caption, int width, int height);
void Run();
private:
void draw_controls();
void draw_overlay();
void present();
GLFWwindow* m_window = nullptr;
static bool m_canMoveWindow;
bool m_opened = true;
// Entity
float m_entity_x = 550.f, m_entity_top_y = 150.f, m_entity_bottom_y = 450.f;
// Box
omath::Color m_box_color{1.f, 1.f, 1.f, 1.f};
omath::Color m_box_fill{0.f, 0.f, 0.f, 0.f};
float m_box_thickness = 1.f, m_corner_ratio = 0.2f;
bool m_show_box = true, m_show_cornered_box = true, m_show_dashed_box = false;
// Dashed box
omath::Color m_dash_color = omath::Color::from_rgba(255, 200, 0, 255);
float m_dash_len = 8.f, m_dash_gap = 5.f, m_dash_thickness = 1.f;
// Bars
omath::Color m_bar_color{0.f, 1.f, 0.f, 1.f};
omath::Color m_bar_bg_color{0.f, 0.f, 0.f, 0.5f};
omath::Color m_bar_outline_color{0.f, 0.f, 0.f, 1.f};
float m_bar_width = 4.f, m_bar_value = 0.75f, m_bar_offset = 5.f;
bool m_show_right_bar = true, m_show_left_bar = true;
bool m_show_top_bar = true, m_show_bottom_bar = true;
bool m_show_right_dashed_bar = false, m_show_left_dashed_bar = false;
bool m_show_top_dashed_bar = false, m_show_bottom_dashed_bar = false;
float m_bar_dash_len = 6.f, m_bar_dash_gap = 4.f;
// Labels
float m_label_offset = 3.f;
bool m_outlined = true;
bool m_show_right_labels = true, m_show_left_labels = true;
bool m_show_top_labels = true, m_show_bottom_labels = true;
bool m_show_centered_top = true, m_show_centered_bottom = true;
// Skeleton
omath::Color m_skel_color = omath::Color::from_rgba(255, 255, 255, 200);
float m_skel_thickness = 1.f;
bool m_show_skeleton = false;
// Progress ring
omath::Color m_ring_color = omath::Color::from_rgba(0, 200, 255, 255);
omath::Color m_ring_bg{0.3f, 0.3f, 0.3f, 0.5f};
float m_ring_radius = 10.f, m_ring_ratio = 0.65f, m_ring_thickness = 2.5f, m_ring_offset = 5.f;
bool m_show_ring = false;
// Scan marker
omath::Color m_scan_color = omath::Color::from_rgba(255, 200, 0, 150);
omath::Color m_scan_outline = omath::Color::from_rgba(255, 200, 0, 255);
float m_scan_outline_thickness = 2.f;
bool m_show_scan = false;
// Aim dot
omath::Color m_aim_color = omath::Color::from_rgba(255, 0, 0, 255);
float m_aim_radius = 3.f;
bool m_show_aim = false;
// Snap line
omath::Color m_snap_color = omath::Color::from_rgba(255, 50, 50, 255);
float m_snap_width = 1.5f;
bool m_show_snap = true;
// Projectile aim
omath::Color m_proj_color = omath::Color::from_rgba(255, 50, 50, 255);
float m_proj_size = 10.f;
float m_proj_line_width = 1.5f;
float m_proj_pos_x = 300.f, m_proj_pos_y = 30.f;
int m_proj_figure = 1; // 0=circle, 1=square
bool m_show_proj = true;
};
} // namespace imgui_desktop::gui

View File

@@ -0,0 +1,8 @@
//
// Created by orange on 13.03.2026.
//
#include "gui/main_window.hpp"
int main()
{
imgui_desktop::gui::MainWindow("omath::hud", 800, 600).Run();
}

View File

@@ -0,0 +1,10 @@
project(example_projection_matrix_builder)
add_executable(${PROJECT_NAME} example_proj_mat_builder.cpp)
set_target_properties(
${PROJECT_NAME}
PROPERTIES CXX_STANDARD 23
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}")
target_link_libraries(${PROJECT_NAME} PRIVATE omath::omath)

View File

@@ -0,0 +1,10 @@
project(example_signature_scan)
add_executable(${PROJECT_NAME} example_signature_scan.cpp)
set_target_properties(
${PROJECT_NAME}
PROPERTIES CXX_STANDARD 23
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}")
target_link_libraries(${PROJECT_NAME} PRIVATE omath::omath)

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

@@ -62,20 +62,13 @@ namespace omath::detail
return splitmix64(base_seed() + 0xD1B54A32D192ED03ull * (Stream + 1)); return splitmix64(base_seed() + 0xD1B54A32D192ED03ull * (Stream + 1));
} }
[[nodiscard]]
consteval std::uint64_t bounded_u64(const std::uint64_t x, const std::uint64_t bound)
{
return (x * bound) >> 64;
}
template<std::int64_t Lo, std::int64_t Hi, std::uint64_t Stream> template<std::int64_t Lo, std::int64_t Hi, std::uint64_t Stream>
[[nodiscard]] [[nodiscard]]
consteval std::int64_t rand_uint8_t() consteval std::int64_t rand_uint8_t()
{ {
static_assert(Lo <= Hi); static_assert(Lo <= Hi);
const std::uint64_t span = static_cast<std::uint64_t>(Hi - Lo) + 1ull;
const std::uint64_t r = rand_u64<Stream>(); const std::uint64_t r = rand_u64<Stream>();
return static_cast<std::int64_t>(bounded_u64(r, span)) + Lo; return static_cast<std::int64_t>(r) + Lo;
} }
[[nodiscard]] [[nodiscard]]
consteval std::uint64_t rand_u64(const std::uint64_t seed, const std::uint64_t i) consteval std::uint64_t rand_u64(const std::uint64_t seed, const std::uint64_t i)

View File

@@ -0,0 +1,13 @@
//
// Created by Vlad on 3/22/2025.
//
#pragma once
#include "omath/engines/cry_engine/constants.hpp"
#include "omath/projection/camera.hpp"
#include "traits/camera_trait.hpp"
namespace omath::cry_engine
{
using Camera = projection::Camera<Mat4X4, ViewAngles, CameraTrait>;
} // namespace omath::cry_engine

View File

@@ -0,0 +1,25 @@
//
// Created by Vlad on 10/21/2025.
//
#pragma once
#include "omath/linear_algebra/mat.hpp"
#include "omath/linear_algebra/vector3.hpp"
#include <omath/trigonometry/angle.hpp>
#include <omath/trigonometry/view_angles.hpp>
namespace omath::cry_engine
{
constexpr Vector3<float> k_abs_up = {0, 0, 1};
constexpr Vector3<float> k_abs_right = {1, 0, 0};
constexpr Vector3<float> k_abs_forward = {0, 1, 0};
using Mat4X4 = Mat<4, 4, float, MatStoreType::ROW_MAJOR>;
using Mat3X3 = Mat<4, 4, float, MatStoreType::ROW_MAJOR>;
using Mat1X3 = Mat<1, 3, float, MatStoreType::ROW_MAJOR>;
using PitchAngle = Angle<float, -90.f, 90.f, AngleFlags::Clamped>;
using YawAngle = Angle<float, -180.f, 180.f, AngleFlags::Normalized>;
using RollAngle = Angle<float, -180.f, 180.f, AngleFlags::Normalized>;
using ViewAngles = omath::ViewAngles<PitchAngle, YawAngle, RollAngle>;
} // namespace omath::cry_engine

View File

@@ -0,0 +1,74 @@
//
// Created by Vlad on 3/22/2025.
//
#pragma once
#include "omath/engines/cry_engine/constants.hpp"
namespace omath::cry_engine
{
[[nodiscard]]
Vector3<float> forward_vector(const ViewAngles& angles) noexcept;
[[nodiscard]]
Vector3<float> right_vector(const ViewAngles& angles) noexcept;
[[nodiscard]]
Vector3<float> up_vector(const ViewAngles& angles) noexcept;
[[nodiscard]] Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3<float>& cam_origin) noexcept;
[[nodiscard]]
Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept;
[[nodiscard]]
Mat4X4 calc_perspective_projection_matrix(float field_of_view, float aspect_ratio, float near, float far) noexcept;
template<class FloatingType>
requires std::is_floating_point_v<FloatingType>
[[nodiscard]]
constexpr FloatingType units_to_centimeters(const FloatingType& units)
{
return units / static_cast<FloatingType>(100);
}
template<class FloatingType>
requires std::is_floating_point_v<FloatingType>
[[nodiscard]]
constexpr FloatingType units_to_meters(const FloatingType& units)
{
return units;
}
template<class FloatingType>
requires std::is_floating_point_v<FloatingType>
[[nodiscard]]
constexpr FloatingType units_to_kilometers(const FloatingType& units)
{
return units_to_meters(units) / static_cast<FloatingType>(1000);
}
template<class FloatingType>
requires std::is_floating_point_v<FloatingType>
[[nodiscard]]
constexpr FloatingType centimeters_to_units(const FloatingType& centimeters)
{
return centimeters * static_cast<FloatingType>(100);
}
template<class FloatingType>
requires std::is_floating_point_v<FloatingType>
[[nodiscard]]
constexpr FloatingType meters_to_units(const FloatingType& meters)
{
return meters;
}
template<class FloatingType>
requires std::is_floating_point_v<FloatingType>
[[nodiscard]]
constexpr FloatingType kilometers_to_units(const FloatingType& kilometers)
{
return meters_to_units(kilometers * static_cast<FloatingType>(1000));
}
} // namespace omath::cry_engine

View File

@@ -0,0 +1,12 @@
//
// Created by Vladislav on 09.11.2025.
//
#pragma once
#include "constants.hpp"
#include "omath/3d_primitives/mesh.hpp"
#include "traits/mesh_trait.hpp"
namespace omath::cry_engine
{
using Mesh = primitives::Mesh<Mat4X4, ViewAngles, MeshTrait>;
}

View File

@@ -0,0 +1,24 @@
//
// Created by Vlad on 8/10/2025.
//
#pragma once
#include "omath/engines/cry_engine/formulas.hpp"
#include "omath/projection/camera.hpp"
namespace omath::cry_engine
{
class CameraTrait final
{
public:
[[nodiscard]]
static ViewAngles calc_look_at_angle(const Vector3<float>& cam_origin, const Vector3<float>& look_at) noexcept;
[[nodiscard]]
static Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3<float>& cam_origin) noexcept;
[[nodiscard]]
static Mat4X4 calc_projection_matrix(const projection::FieldOfView& fov, const projection::ViewPort& view_port,
float near, float far) noexcept;
};
} // namespace omath::cry_engine

View File

@@ -0,0 +1,19 @@
//
// Created by Vladislav on 09.11.2025.
//
#pragma once
#include <omath/engines/cry_engine/constants.hpp>
#include <omath/engines/cry_engine/formulas.hpp>
namespace omath::cry_engine
{
class MeshTrait final
{
public:
[[nodiscard]]
static Mat4X4 rotation_matrix(const ViewAngles& rotation)
{
return cry_engine::rotation_matrix(rotation);
}
};
} // namespace omath::cry_engine

View File

@@ -0,0 +1,77 @@
//
// Created by Vlad on 8/6/2025.
//
#pragma once
#include "omath/engines/cry_engine/formulas.hpp"
#include "omath/projectile_prediction/projectile.hpp"
#include "omath/projectile_prediction/target.hpp"
#include <optional>
namespace omath::cry_engine
{
class PredEngineTrait final
{
public:
constexpr static Vector3<float> predict_projectile_position(const projectile_prediction::Projectile& projectile,
const float pitch, const float yaw,
const float time, const float gravity) noexcept
{
const auto launch_pos = projectile.m_origin + projectile.m_launch_offset;
auto current_pos = launch_pos
+ forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw),
RollAngle::from_degrees(0)})
* projectile.m_launch_speed * time;
current_pos.z -= (gravity * projectile.m_gravity_scale) * (time * time) * 0.5f;
return current_pos;
}
[[nodiscard]]
static constexpr Vector3<float> predict_target_position(const projectile_prediction::Target& target,
const float time, const float gravity) noexcept
{
auto predicted = target.m_origin + target.m_velocity * time;
if (target.m_is_airborne)
predicted.z -= gravity * (time * time) * 0.5f;
return predicted;
}
[[nodiscard]]
static float calc_vector_2d_distance(const Vector3<float>& delta) noexcept
{
return std::sqrt(delta.x * delta.x + delta.y * delta.y);
}
[[nodiscard]]
constexpr static float get_vector_height_coordinate(const Vector3<float>& vec) noexcept
{
return vec.z;
}
[[nodiscard]]
static Vector3<float> calc_viewpoint_from_angles(const projectile_prediction::Projectile& projectile,
Vector3<float> predicted_target_position,
const std::optional<float> projectile_pitch) noexcept
{
const auto delta2d = calc_vector_2d_distance(predicted_target_position - projectile.m_origin);
const auto height = delta2d * std::tan(angles::degrees_to_radians(projectile_pitch.value()));
return {predicted_target_position.x, predicted_target_position.y, projectile.m_origin.z + height};
}
// Due to specification of maybe_calculate_projectile_launch_pitch_angle, pitch angle must be:
// 89 look up, -89 look down
[[nodiscard]]
static float calc_direct_pitch_angle(const Vector3<float>& origin, const Vector3<float>& view_to) noexcept
{
const auto direction = (view_to - origin).normalized();
return angles::radians_to_degrees(std::asin(direction.z));
}
[[nodiscard]]
static float calc_direct_yaw_angle(const Vector3<float>& origin, const Vector3<float>& view_to) noexcept
{
const auto direction = (view_to - origin).normalized();
return angles::radians_to_degrees(-std::atan2(direction.x, direction.y));
};
};
} // namespace omath::cry_engine

View File

@@ -16,7 +16,8 @@ namespace omath::frostbite_engine
const float pitch, const float yaw, const float pitch, const float yaw,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
auto current_pos = projectile.m_origin const auto launch_pos = projectile.m_origin + projectile.m_launch_offset;
auto current_pos = launch_pos
+ forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw), + forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw),
RollAngle::from_degrees(0)}) RollAngle::from_degrees(0)})
* projectile.m_launch_speed * time; * projectile.m_launch_speed * time;
@@ -55,7 +56,7 @@ namespace omath::frostbite_engine
const auto delta2d = calc_vector_2d_distance(predicted_target_position - projectile.m_origin); const auto delta2d = calc_vector_2d_distance(predicted_target_position - projectile.m_origin);
const auto height = delta2d * std::tan(angles::degrees_to_radians(projectile_pitch.value())); const auto height = delta2d * std::tan(angles::degrees_to_radians(projectile_pitch.value()));
return {predicted_target_position.x, predicted_target_position.y + height, projectile.m_origin.z}; return {predicted_target_position.x, projectile.m_origin.y + height, predicted_target_position.z};
} }
// Due to specification of maybe_calculate_projectile_launch_pitch_angle, pitch angle must be: // Due to specification of maybe_calculate_projectile_launch_pitch_angle, pitch angle must be:
// 89 look up, -89 look down // 89 look up, -89 look down

View File

@@ -17,7 +17,8 @@ namespace omath::iw_engine
const float pitch, const float yaw, const float pitch, const float yaw,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
auto current_pos = projectile.m_origin const auto launch_pos = projectile.m_origin + projectile.m_launch_offset;
auto current_pos = launch_pos
+ forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw), + forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw),
RollAngle::from_degrees(0)}) RollAngle::from_degrees(0)})
* projectile.m_launch_speed * time; * projectile.m_launch_speed * time;

View File

@@ -16,7 +16,8 @@ namespace omath::opengl_engine
const float pitch, const float yaw, const float pitch, const float yaw,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
auto current_pos = projectile.m_origin const auto launch_pos = projectile.m_origin + projectile.m_launch_offset;
auto current_pos = launch_pos
+ forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw), + forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw),
RollAngle::from_degrees(0)}) RollAngle::from_degrees(0)})
* projectile.m_launch_speed * time; * projectile.m_launch_speed * time;
@@ -55,7 +56,7 @@ namespace omath::opengl_engine
const auto delta2d = calc_vector_2d_distance(predicted_target_position - projectile.m_origin); const auto delta2d = calc_vector_2d_distance(predicted_target_position - projectile.m_origin);
const auto height = delta2d * std::tan(angles::degrees_to_radians(projectile_pitch.value())); const auto height = delta2d * std::tan(angles::degrees_to_radians(projectile_pitch.value()));
return {predicted_target_position.x, predicted_target_position.y + height, projectile.m_origin.z}; return {predicted_target_position.x, projectile.m_origin.y + height, predicted_target_position.z};
} }
// Due to specification of maybe_calculate_projectile_launch_pitch_angle, pitch angle must be: // Due to specification of maybe_calculate_projectile_launch_pitch_angle, pitch angle must be:
// 89 look up, -89 look down // 89 look up, -89 look down

View File

@@ -17,7 +17,8 @@ namespace omath::source_engine
const float pitch, const float yaw, const float pitch, const float yaw,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
auto current_pos = projectile.m_origin const auto launch_pos = projectile.m_origin + projectile.m_launch_offset;
auto current_pos = launch_pos
+ forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw), + forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw),
RollAngle::from_degrees(0)}) RollAngle::from_degrees(0)})
* projectile.m_launch_speed * time; * projectile.m_launch_speed * time;

View File

@@ -16,7 +16,8 @@ namespace omath::unity_engine
const float pitch, const float yaw, const float pitch, const float yaw,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
auto current_pos = projectile.m_origin const auto launch_pos = projectile.m_origin + projectile.m_launch_offset;
auto current_pos = launch_pos
+ forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw), + forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw),
RollAngle::from_degrees(0)}) RollAngle::from_degrees(0)})
* projectile.m_launch_speed * time; * projectile.m_launch_speed * time;
@@ -55,7 +56,7 @@ namespace omath::unity_engine
const auto delta2d = calc_vector_2d_distance(predicted_target_position - projectile.m_origin); const auto delta2d = calc_vector_2d_distance(predicted_target_position - projectile.m_origin);
const auto height = delta2d * std::tan(angles::degrees_to_radians(projectile_pitch.value())); const auto height = delta2d * std::tan(angles::degrees_to_radians(projectile_pitch.value()));
return {predicted_target_position.x, predicted_target_position.y + height, projectile.m_origin.z}; return {predicted_target_position.x, projectile.m_origin.y + height, predicted_target_position.z};
} }
// Due to specification of maybe_calculate_projectile_launch_pitch_angle, pitch angle must be: // Due to specification of maybe_calculate_projectile_launch_pitch_angle, pitch angle must be:
// 89 look up, -89 look down // 89 look up, -89 look down

View File

@@ -16,7 +16,8 @@ namespace omath::unreal_engine
const float pitch, const float yaw, const float pitch, const float yaw,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
auto current_pos = projectile.m_origin const auto launch_pos = projectile.m_origin + projectile.m_launch_offset;
auto current_pos = launch_pos
+ forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw), + forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw),
RollAngle::from_degrees(0)}) RollAngle::from_degrees(0)})
* projectile.m_launch_speed * time; * projectile.m_launch_speed * time;

View File

@@ -0,0 +1,23 @@
//
// Created by orange on 13.03.2026.
//
#pragma once
#include "omath/linear_algebra/vector2.hpp"
#include <array>
namespace omath::hud
{
class CanvasBox final
{
public:
CanvasBox(Vector2<float> top, Vector2<float> bottom, float ratio = 4.f);
[[nodiscard]]
std::array<Vector2<float>, 4> as_array() const;
Vector2<float> top_left_corner;
Vector2<float> top_right_corner;
Vector2<float> bottom_left_corner;
Vector2<float> bottom_right_corner;
};
} // namespace omath::hud

View File

@@ -0,0 +1,202 @@
//
// Created by orange on 13.03.2026.
//
#pragma once
#include "canvas_box.hpp"
#include "entity_overlay_widgets.hpp"
#include "hud_renderer_interface.hpp"
#include "omath/linear_algebra/vector2.hpp"
#include "omath/utility/color.hpp"
#include <memory>
#include <string_view>
namespace omath::hud
{
class EntityOverlay final
{
public:
EntityOverlay(const Vector2<float>& top, const Vector2<float>& bottom,
const std::shared_ptr<HudRendererInterface>& renderer);
// ── Boxes ────────────────────────────────────────────────────────
EntityOverlay& add_2d_box(const Color& box_color, const Color& fill_color = Color{0.f, 0.f, 0.f, 0.f},
float thickness = 1.f);
EntityOverlay& add_cornered_2d_box(const Color& box_color, const Color& fill_color = Color{0.f, 0.f, 0.f, 0.f},
float corner_ratio_len = 0.2f, float thickness = 1.f);
EntityOverlay& add_dashed_box(const Color& color, float dash_len = 8.f, float gap_len = 5.f,
float thickness = 1.f);
// ── Bars ─────────────────────────────────────────────────────────
EntityOverlay& add_right_bar(const Color& color, const Color& outline_color, const Color& bg_color, float width,
float ratio, float offset = 5.f);
EntityOverlay& add_left_bar(const Color& color, const Color& outline_color, const Color& bg_color, float width,
float ratio, float offset = 5.f);
EntityOverlay& add_top_bar(const Color& color, const Color& outline_color, const Color& bg_color, float height,
float ratio, float offset = 5.f);
EntityOverlay& add_bottom_bar(const Color& color, const Color& outline_color, const Color& bg_color,
float height, float ratio, float offset = 5.f);
EntityOverlay& add_right_dashed_bar(const Color& color, const Color& outline_color, const Color& bg_color,
float width, float ratio, float dash_len, float gap_len,
float offset = 5.f);
EntityOverlay& add_left_dashed_bar(const Color& color, const Color& outline_color, const Color& bg_color,
float width, float ratio, float dash_len, float gap_len, float offset = 5.f);
EntityOverlay& add_top_dashed_bar(const Color& color, const Color& outline_color, const Color& bg_color,
float height, float ratio, float dash_len, float gap_len, float offset = 5.f);
EntityOverlay& add_bottom_dashed_bar(const Color& color, const Color& outline_color, const Color& bg_color,
float height, float ratio, float dash_len, float gap_len,
float offset = 5.f);
// ── Labels ───────────────────────────────────────────────────────
EntityOverlay& add_right_label(const Color& color, float offset, bool outlined, const std::string_view& text);
EntityOverlay& add_left_label(const Color& color, float offset, bool outlined, const std::string_view& text);
EntityOverlay& add_top_label(const Color& color, float offset, bool outlined, std::string_view text);
EntityOverlay& add_bottom_label(const Color& color, float offset, bool outlined, std::string_view text);
EntityOverlay& add_centered_top_label(const Color& color, float offset, bool outlined,
const std::string_view& text);
EntityOverlay& add_centered_bottom_label(const Color& color, float offset, bool outlined,
const std::string_view& text);
template<typename... Args>
EntityOverlay& add_right_label(const Color& color, const float offset, const bool outlined, std::format_string<Args...> fmt,
Args&&... args)
{
return add_right_label(color, offset, outlined,
std::string_view{std::vformat(fmt.get(), std::make_format_args(args...))});
}
template<typename... Args>
EntityOverlay& add_left_label(const Color& color, const float offset, const bool outlined, std::format_string<Args...> fmt,
Args&&... args)
{
return add_left_label(color, offset, outlined,
std::string_view{std::vformat(fmt.get(), std::make_format_args(args...))});
}
template<typename... Args>
EntityOverlay& add_top_label(const Color& color, const float offset, const bool outlined, std::format_string<Args...> fmt,
Args&&... args)
{
return add_top_label(color, offset, outlined,
std::string_view{std::vformat(fmt.get(), std::make_format_args(args...))});
}
template<typename... Args>
EntityOverlay& add_bottom_label(const Color& color, const float offset, const bool outlined,
std::format_string<Args...> fmt, Args&&... args)
{
return add_bottom_label(color, offset, outlined,
std::string_view{std::vformat(fmt.get(), std::make_format_args(args...))});
}
template<typename... Args>
EntityOverlay& add_centered_top_label(const Color& color, const float offset, const bool outlined,
std::format_string<Args...> fmt, Args&&... args)
{
return add_centered_top_label(color, offset, outlined,
std::string_view{std::vformat(fmt.get(), std::make_format_args(args...))});
}
template<typename... Args>
EntityOverlay& add_centered_bottom_label(const Color& color, const float offset, const bool outlined,
std::format_string<Args...> fmt, Args&&... args)
{
return add_centered_bottom_label(color, offset, outlined,
std::string_view{std::vformat(fmt.get(), std::make_format_args(args...))});
}
// ── Spacers ─────────────────────────────────────────────────────
EntityOverlay& add_right_space_vertical(float size);
EntityOverlay& add_right_space_horizontal(float size);
EntityOverlay& add_left_space_vertical(float size);
EntityOverlay& add_left_space_horizontal(float size);
EntityOverlay& add_top_space_vertical(float size);
EntityOverlay& add_top_space_horizontal(float size);
EntityOverlay& add_bottom_space_vertical(float size);
EntityOverlay& add_bottom_space_horizontal(float size);
// ── Progress rings ──────────────────────────────────────────────
EntityOverlay& add_right_progress_ring(const Color& color, const Color& bg, float radius, float ratio,
float thickness = 2.f, float offset = 5.f, int segments = 0);
EntityOverlay& add_left_progress_ring(const Color& color, const Color& bg, float radius, float ratio,
float thickness = 2.f, float offset = 5.f, int segments = 0);
EntityOverlay& add_top_progress_ring(const Color& color, const Color& bg, float radius, float ratio,
float thickness = 2.f, float offset = 5.f, int segments = 0);
EntityOverlay& add_bottom_progress_ring(const Color& color, const Color& bg, float radius, float ratio,
float thickness = 2.f, float offset = 5.f, int segments = 0);
// ── Icons ────────────────────────────────────────────────────────
EntityOverlay& add_right_icon(const std::any& texture_id, float width, float height,
const Color& tint = Color{1.f, 1.f, 1.f, 1.f}, float offset = 5.f);
EntityOverlay& add_left_icon(const std::any& texture_id, float width, float height,
const Color& tint = Color{1.f, 1.f, 1.f, 1.f}, float offset = 5.f);
EntityOverlay& add_top_icon(const std::any& texture_id, float width, float height,
const Color& tint = Color{1.f, 1.f, 1.f, 1.f}, float offset = 5.f);
EntityOverlay& add_bottom_icon(const std::any& texture_id, float width, float height,
const Color& tint = Color{1.f, 1.f, 1.f, 1.f}, float offset = 5.f);
// ── Misc ─────────────────────────────────────────────────────────
EntityOverlay& add_snap_line(const Vector2<float>& start_pos, const Color& color, float width);
EntityOverlay& add_skeleton(const Color& color, float thickness = 1.f);
// ── Declarative interface ─────────────────────────────────────────
/// Pass any combination of widget:: descriptor structs (and std::optional<W>
/// from when()) to render them all in declaration order.
template<typename... Widgets>
EntityOverlay& contents(Widgets&&... widgets)
{
(dispatch(std::forward<Widgets>(widgets)), ...);
return *this;
}
private:
// optional<W> dispatch — enables when() conditional widgets
template<typename W>
void dispatch(const std::optional<W>& w)
{
if (w)
dispatch(*w);
}
void dispatch(const widget::Box& box);
void dispatch(const widget::CorneredBox& cornered_box);
void dispatch(const widget::DashedBox& dashed_box);
void dispatch(const widget::RightSide& right_side);
void dispatch(const widget::LeftSide& left_side);
void dispatch(const widget::TopSide& top_side);
void dispatch(const widget::BottomSide& bottom_side);
void dispatch(const widget::Skeleton& skeleton);
void dispatch(const widget::SnapLine& snap_line);
void dispatch(const widget::ScanMarker& scan_marker);
void dispatch(const widget::AimDot& aim_dot);
void dispatch(const widget::ProjectileAim& proj_widget);
void draw_progress_ring(const Vector2<float>& center, const widget::ProgressRing& ring);
void draw_outlined_text(const Vector2<float>& position, const Color& color, const std::string_view& text);
void draw_dashed_line(const Vector2<float>& from, const Vector2<float>& to, const Color& color, float dash_len,
float gap_len, float thickness) const;
void draw_dashed_fill(const Vector2<float>& origin, const Vector2<float>& step_dir,
const Vector2<float>& perp_dir, float full_len, float filled_len, const Color& fill_color,
const Color& split_color, float dash_len, float gap_len) const;
CanvasBox m_canvas;
Vector2<float> m_text_cursor_right;
Vector2<float> m_text_cursor_top;
Vector2<float> m_text_cursor_bottom;
Vector2<float> m_text_cursor_left;
std::shared_ptr<HudRendererInterface> m_renderer;
};
} // namespace omath::hud

View File

@@ -0,0 +1,233 @@
//
// Created by orange on 15.03.2026.
//
#pragma once
#include "omath/linear_algebra/vector2.hpp"
#include "omath/utility/color.hpp"
#include <any>
#include <initializer_list>
#include <optional>
#include <string_view>
#include <variant>
namespace omath::hud::widget
{
// ── Overloaded helper for std::visit ──────────────────────────────────────
template<typename... Ts>
struct Overloaded : Ts...
{
using Ts::operator()...;
};
template<typename... Ts>
Overloaded(Ts...) -> Overloaded<Ts...>;
// ── Standalone widgets ────────────────────────────────────────────────────
struct Box
{
Color color;
Color fill{0.f, 0.f, 0.f, 0.f};
float thickness = 1.f;
};
struct CorneredBox
{
Color color;
Color fill{0.f, 0.f, 0.f, 0.f};
float corner_ratio = 0.2f;
float thickness = 1.f;
};
struct DashedBox
{
Color color;
float dash_len = 8.f;
float gap_len = 5.f;
float thickness = 1.f;
};
struct Skeleton
{
Color color;
float thickness = 1.f;
};
struct SnapLine
{
Vector2<float> start;
Color color;
float width;
};
struct ScanMarker
{
Color color;
Color outline{0.f, 0.f, 0.f, 0.f};
float outline_thickness = 1.f;
};
/// Dot at an absolute screen position.
struct AimDot
{
Vector2<float> position;
Color color;
float radius = 3.f;
};
struct ProjectileAim
{
enum class Figure
{
CIRCLE,
SQUARE,
};
Vector2<float> position;
Color color;
float size = 3.f;
float line_size = 1.f;
Figure figure = Figure::SQUARE;
};
// ── Side-agnostic widgets (used inside XxxSide containers) ────────────────
/// A filled bar. `size` is width for left/right sides, height for top/bottom.
struct Bar
{
Color color;
Color outline;
Color bg;
float size;
float ratio;
float offset = 5.f;
};
/// A dashed bar. Same field semantics as Bar plus dash parameters.
struct DashedBar
{
Color color;
Color outline;
Color bg;
float size;
float ratio;
float dash_len;
float gap_len;
float offset = 5.f;
};
struct Label
{
Color color;
float offset;
bool outlined;
std::string_view text;
};
/// Wraps a Label to request horizontal centering (only applied in TopSide / BottomSide).
template<typename W>
struct Centered
{
W child;
};
template<typename W>
Centered(W) -> Centered<W>;
/// Empty vertical gap that advances the Y cursor without drawing.
struct SpaceVertical
{
float size;
};
/// Empty horizontal gap that advances the X cursor without drawing.
struct SpaceHorizontal
{
float size;
};
struct ProgressRing
{
Color color;
Color bg{0.3f, 0.3f, 0.3f, 0.5f};
float radius = 12.f;
float ratio;
float thickness = 2.f;
float offset = 5.f;
int segments = 32;
};
struct Icon
{
std::any texture_id;
float width;
float height;
Color tint{1.f, 1.f, 1.f, 1.f};
float offset = 5.f;
};
// ── Side widget variant ───────────────────────────────────────────────────
struct None
{
}; ///< No-op placeholder — used by widget::when for disabled elements.
using SideWidget =
std::variant<None, Bar, DashedBar, Label, Centered<Label>, SpaceVertical, SpaceHorizontal, ProgressRing, Icon>;
// ── Side containers ───────────────────────────────────────────────────────
// Storing std::initializer_list<SideWidget> is safe here: the backing array
// is a const SideWidget[] on the caller's stack whose lifetime matches the
// temporary side-container object, which is consumed within the same
// full-expression by EntityOverlay::dispatch. No heap allocation occurs.
struct RightSide
{
std::initializer_list<SideWidget> children;
RightSide(const std::initializer_list<SideWidget> c): children(c)
{
}
};
struct LeftSide
{
std::initializer_list<SideWidget> children;
LeftSide(const std::initializer_list<SideWidget> c): children(c)
{
}
};
struct TopSide
{
std::initializer_list<SideWidget> children;
TopSide(const std::initializer_list<SideWidget> c): children(c)
{
}
};
struct BottomSide
{
std::initializer_list<SideWidget> children;
BottomSide(const std::initializer_list<SideWidget> c): children(c)
{
}
};
} // namespace omath::hud::widget
namespace omath::hud::widget
{
/// Inside XxxSide containers: returns the widget as a SideWidget when condition is true,
/// or None{} otherwise. Preferred over hud::when for types inside the SideWidget variant.
template<typename W>
requires std::constructible_from<SideWidget, W>
SideWidget when(const bool condition, W widget)
{
if (condition)
return SideWidget{std::move(widget)};
return None{};
}
} // namespace omath::hud::widget
namespace omath::hud
{
/// Top-level: returns an engaged optional<W> when condition is true, std::nullopt otherwise.
/// Designed for use with EntityOverlay::contents() for top-level widget types.
template<typename W>
std::optional<W> when(const bool condition, W widget)
{
if (condition)
return std::move(widget);
return std::nullopt;
}
} // namespace omath::hud

View File

@@ -0,0 +1,47 @@
//
// Created by orange on 13.03.2026.
//
#pragma once
#include "omath/linear_algebra/vector2.hpp"
#include "omath/utility/color.hpp"
#include <any>
#include <span>
namespace omath::hud
{
class HudRendererInterface
{
public:
virtual ~HudRendererInterface() = default;
virtual void add_line(const Vector2<float>& line_start, const Vector2<float>& line_end, const Color& color,
float thickness) = 0;
virtual void add_polyline(const std::span<const Vector2<float>>& vertexes, const Color& color,
float thickness) = 0;
virtual void add_filled_polyline(const std::span<const Vector2<float>>& vertexes, const Color& color) = 0;
virtual void add_rectangle(const Vector2<float>& min, const Vector2<float>& max, const Color& color) = 0;
virtual void add_filled_rectangle(const Vector2<float>& min, const Vector2<float>& max, const Color& color) = 0;
virtual void add_circle(const Vector2<float>& center, float radius, const Color& color, float thickness,
int segments = 0) = 0;
virtual void add_filled_circle(const Vector2<float>& center, float radius, const Color& color,
int segments = 0) = 0;
/// Draw an arc (partial circle outline). Angles in radians, 0 = right (+X), counter-clockwise.
virtual void add_arc(const Vector2<float>& center, float radius, float a_min, float a_max, const Color& color,
float thickness, int segments = 0) = 0;
/// Draw a textured quad. texture_id is renderer-specific (e.g. ImTextureID for ImGui).
virtual void add_image(const std::any& texture_id, const Vector2<float>& min, const Vector2<float>& max,
const Color& tint = Color{1.f, 1.f, 1.f, 1.f}) = 0;
virtual void add_text(const Vector2<float>& position, const Color& color, const std::string_view& text) = 0;
[[nodiscard]]
virtual Vector2<float> calc_text_size(const std::string_view& text) = 0;
};
} // namespace omath::hud

View File

@@ -0,0 +1,33 @@
//
// Created by orange on 13.03.2026.
//
#pragma once
#include <omath/hud/hud_renderer_interface.hpp>
#ifdef OMATH_IMGUI_INTEGRATION
namespace omath::hud
{
class ImguiHudRenderer final : public HudRendererInterface
{
public:
~ImguiHudRenderer() override;
void add_line(const Vector2<float>& line_start, const Vector2<float>& line_end, const Color& color,
float thickness) override;
void add_polyline(const std::span<const Vector2<float>>& vertexes, const Color& color, float thickness) override;
void add_filled_polyline(const std::span<const Vector2<float>>& vertexes, const Color& color) override;
void add_rectangle(const Vector2<float>& min, const Vector2<float>& max, const Color& color) override;
void add_filled_rectangle(const Vector2<float>& min, const Vector2<float>& max, const Color& color) override;
void add_circle(const Vector2<float>& center, float radius, const Color& color, float thickness,
int segments = 0) override;
void add_filled_circle(const Vector2<float>& center, float radius, const Color& color,
int segments = 0) override;
void add_arc(const Vector2<float>& center, float radius, float a_min, float a_max, const Color& color,
float thickness, int segments = 0) override;
void add_image(const std::any& texture_id, const Vector2<float>& min, const Vector2<float>& max,
const Color& tint = Color{1.f, 1.f, 1.f, 1.f}) override;
void add_text(const Vector2<float>& position, const Color& color, const std::string_view& text) override;
[[nodiscard]]
virtual Vector2<float> calc_text_size(const std::string_view& text) override;
};
} // namespace omath::hud
#endif // OMATH_IMGUI_INTEGRATION

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

View File

@@ -220,6 +220,12 @@ namespace omath
{ {
return std::make_tuple(x, y); return std::make_tuple(x, y);
} }
[[nodiscard]]
constexpr std::array<Type, 2> as_array() const noexcept
{
return {x, y};
}
#ifdef OMATH_IMGUI_INTEGRATION #ifdef OMATH_IMGUI_INTEGRATION
[[nodiscard]] [[nodiscard]]
constexpr ImVec2 to_im_vec2() const noexcept constexpr ImVec2 to_im_vec2() const noexcept

View File

@@ -4,8 +4,8 @@
#pragma once #pragma once
#include "omath/trigonometry/angle.hpp"
#include "omath/linear_algebra/vector2.hpp" #include "omath/linear_algebra/vector2.hpp"
#include "omath/trigonometry/angle.hpp"
#include <cstdint> #include <cstdint>
#include <expected> #include <expected>
#include <functional> #include <functional>
@@ -233,7 +233,8 @@ namespace omath
return Angle<float, 0.f, 180.f, AngleFlags::Clamped>::from_radians(std::acos(dot(other) / bottom)); return Angle<float, 0.f, 180.f, AngleFlags::Clamped>::from_radians(std::acos(dot(other) / bottom));
} }
[[nodiscard]] bool is_perpendicular(const Vector3& other, Type epsilon = static_cast<Type>(0.0001)) const noexcept [[nodiscard]] bool is_perpendicular(const Vector3& other,
Type epsilon = static_cast<Type>(0.0001)) const noexcept
{ {
if (const auto angle = angle_between(other)) if (const auto angle = angle_between(other))
return std::abs(angle->as_degrees() - static_cast<Type>(90)) <= epsilon; return std::abs(angle->as_degrees() - static_cast<Type>(90)) <= epsilon;
@@ -274,6 +275,12 @@ namespace omath
{ {
return length() >= other.length(); return length() >= other.length();
} }
[[nodiscard]]
constexpr std::array<Type, 3> as_array() const noexcept
{
return {this->x, this->y, z};
}
}; };
} // namespace omath } // namespace omath

View File

@@ -3,8 +3,8 @@
// //
#pragma once #pragma once
#include <algorithm>
#include "omath/linear_algebra/vector3.hpp" #include "omath/linear_algebra/vector3.hpp"
#include <algorithm>
namespace omath namespace omath
{ {
@@ -183,6 +183,12 @@ namespace omath
return length() >= other.length(); return length() >= other.length();
} }
[[nodiscard]]
constexpr std::array<Type, 4> as_array() const noexcept
{
return {this->x, this->y, this->z, w};
}
#ifdef OMATH_IMGUI_INTEGRATION #ifdef OMATH_IMGUI_INTEGRATION
[[nodiscard]] [[nodiscard]]
constexpr ImVec4 to_im_vec4() const noexcept constexpr ImVec4 to_im_vec4() const noexcept
@@ -200,7 +206,7 @@ namespace omath
return {static_cast<Type>(other.x), static_cast<Type>(other.y), static_cast<Type>(other.z)}; return {static_cast<Type>(other.x), static_cast<Type>(other.y), static_cast<Type>(other.z)};
} }
#endif #endif
}; };
} // namespace omath } // namespace omath
template<> struct std::hash<omath::Vector4<float>> template<> struct std::hash<omath::Vector4<float>>

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

@@ -6,7 +6,9 @@
#include "omath/linear_algebra/vector3.hpp" #include "omath/linear_algebra/vector3.hpp"
#include <expected> #include <expected>
#include <optional>
#include <string> #include <string>
#include <unordered_map>
#include <vector> #include <vector>
namespace omath::pathfinding namespace omath::pathfinding
@@ -28,10 +30,20 @@ namespace omath::pathfinding
[[nodiscard]] [[nodiscard]]
bool empty() const; bool empty() const;
[[nodiscard]] std::vector<uint8_t> serialize() const noexcept; // Events -- per-vertex optional tag (e.g. "jump", "teleport")
void set_event(const Vector3<float>& vertex, const std::string_view& event_id);
void clear_event(const Vector3<float>& vertex);
void deserialize(const std::vector<uint8_t>& raw) noexcept; [[nodiscard]]
std::optional<std::string> get_event(const Vector3<float>& vertex) const noexcept;
[[nodiscard]] std::string serialize() const noexcept;
void deserialize(const std::string& raw);
std::unordered_map<Vector3<float>, std::vector<Vector3<float>>> m_vertex_map; std::unordered_map<Vector3<float>, std::vector<Vector3<float>>> m_vertex_map;
private:
std::unordered_map<Vector3<float>, std::string> m_vertex_events;
}; };
} // namespace omath::pathfinding } // namespace omath::pathfinding

View File

@@ -8,12 +8,23 @@
namespace omath::projectile_prediction namespace omath::projectile_prediction
{ {
struct AimAngles
{
float pitch{};
float yaw{};
};
class ProjPredEngineInterface class ProjPredEngineInterface
{ {
public: public:
[[nodiscard]] [[nodiscard]]
virtual std::optional<Vector3<float>> maybe_calculate_aim_point(const Projectile& projectile, virtual std::optional<Vector3<float>> maybe_calculate_aim_point(const Projectile& projectile,
const Target& target) const = 0; const Target& target) const = 0;
[[nodiscard]]
virtual std::optional<AimAngles> maybe_calculate_aim_angles(const Projectile& projectile,
const Target& target) const = 0;
virtual ~ProjPredEngineInterface() = default; virtual ~ProjPredEngineInterface() = default;
}; };
} // namespace omath::projectile_prediction } // namespace omath::projectile_prediction

View File

@@ -12,6 +12,9 @@ namespace omath::projectile_prediction
[[nodiscard]] std::optional<Vector3<float>> [[nodiscard]] std::optional<Vector3<float>>
maybe_calculate_aim_point(const Projectile& projectile, const Target& target) const override; maybe_calculate_aim_point(const Projectile& projectile, const Target& target) const override;
[[nodiscard]] std::optional<AimAngles>
maybe_calculate_aim_angles(const Projectile& projectile, const Target& target) const override;
ProjPredEngineAvx2(float gravity_constant, float simulation_time_step, float maximum_simulation_time); ProjPredEngineAvx2(float gravity_constant, float simulation_time_step, float maximum_simulation_time);
~ProjPredEngineAvx2() override = default; ~ProjPredEngineAvx2() override = default;

View File

@@ -54,6 +54,36 @@ namespace omath::projectile_prediction
[[nodiscard]] [[nodiscard]]
std::optional<Vector3<float>> maybe_calculate_aim_point(const Projectile& projectile, std::optional<Vector3<float>> maybe_calculate_aim_point(const Projectile& projectile,
const Target& target) const override const Target& target) const override
{
const auto solution = find_solution(projectile, target);
if (!solution)
return std::nullopt;
return EngineTrait::calc_viewpoint_from_angles(projectile, solution->predicted_target_position,
solution->pitch);
}
[[nodiscard]]
std::optional<AimAngles> maybe_calculate_aim_angles(const Projectile& projectile,
const Target& target) const override
{
const auto solution = find_solution(projectile, target);
if (!solution)
return std::nullopt;
const auto yaw = EngineTrait::calc_direct_yaw_angle(projectile.m_origin + projectile.m_launch_offset, solution->predicted_target_position);
return AimAngles{solution->pitch, yaw};
}
private:
struct Solution
{
Vector3<float> predicted_target_position;
float pitch;
};
[[nodiscard]]
std::optional<Solution> find_solution(const Projectile& projectile, const Target& target) const
{ {
for (float time = 0.f; time < m_maximum_simulation_time; time += m_simulation_time_step) for (float time = 0.f; time < m_maximum_simulation_time; time += m_simulation_time_step)
{ {
@@ -70,12 +100,11 @@ namespace omath::projectile_prediction
time)) time))
continue; continue;
return EngineTrait::calc_viewpoint_from_angles(projectile, predicted_target_position, projectile_pitch); return Solution{predicted_target_position, projectile_pitch.value()};
} }
return std::nullopt; return std::nullopt;
} }
private:
const float m_gravity_constant; const float m_gravity_constant;
const float m_simulation_time_step; const float m_simulation_time_step;
const float m_maximum_simulation_time; const float m_maximum_simulation_time;
@@ -100,10 +129,12 @@ namespace omath::projectile_prediction
{ {
const auto bullet_gravity = m_gravity_constant * projectile.m_gravity_scale; const auto bullet_gravity = m_gravity_constant * projectile.m_gravity_scale;
if (bullet_gravity == 0.f) const auto launch_origin = projectile.m_origin + projectile.m_launch_offset;
return EngineTrait::calc_direct_pitch_angle(projectile.m_origin, target_position);
const auto delta = target_position - projectile.m_origin; if (bullet_gravity == 0.f)
return EngineTrait::calc_direct_pitch_angle(launch_origin, target_position);
const auto delta = target_position - launch_origin;
const auto distance2d = EngineTrait::calc_vector_2d_distance(delta); const auto distance2d = EngineTrait::calc_vector_2d_distance(delta);
const auto distance2d_sqr = distance2d * distance2d; const auto distance2d_sqr = distance2d * distance2d;
@@ -126,7 +157,7 @@ namespace omath::projectile_prediction
bool is_projectile_reached_target(const Vector3<float>& target_position, const Projectile& projectile, bool is_projectile_reached_target(const Vector3<float>& target_position, const Projectile& projectile,
const float pitch, const float time) const noexcept const float pitch, const float time) const noexcept
{ {
const auto yaw = EngineTrait::calc_direct_yaw_angle(projectile.m_origin, target_position); const auto yaw = EngineTrait::calc_direct_yaw_angle(projectile.m_origin + projectile.m_launch_offset, target_position);
const auto projectile_position = const auto projectile_position =
EngineTrait::predict_projectile_position(projectile, pitch, yaw, time, m_gravity_constant); EngineTrait::predict_projectile_position(projectile, pitch, yaw, time, m_gravity_constant);

View File

@@ -11,6 +11,7 @@ namespace omath::projectile_prediction
{ {
public: public:
Vector3<float> m_origin; Vector3<float> m_origin;
Vector3<float> m_launch_offset{0.f, 0.f, 0.f};
float m_launch_speed{}; float m_launch_speed{};
float m_gravity_scale{}; float m_gravity_scale{};
}; };

View File

@@ -3,11 +3,43 @@
// //
#pragma once #pragma once
#include <cassert>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <string_view>
#ifdef _WIN32
#include "omath/utility/pe_pattern_scan.hpp"
#include <windows.h>
#elif defined(__APPLE__)
#include "omath/utility/macho_pattern_scan.hpp"
#include <mach-o/dyld.h>
#else
#include "omath/utility/elf_pattern_scan.hpp"
#include <link.h>
#endif
namespace omath::rev_eng namespace omath::rev_eng
{ {
template<std::size_t N>
struct FixedString final
{
char data[N]{};
// ReSharper disable once CppNonExplicitConvertingConstructor
constexpr FixedString(const char (&str)[N]) noexcept // NOLINT(*-explicit-constructor)
{
for (std::size_t i = 0; i < N; ++i)
data[i] = str[i];
}
// ReSharper disable once CppNonExplicitConversionOperator
constexpr operator std::string_view() const noexcept // NOLINT(*-explicit-constructor)
{
return {data, N - 1};
}
};
template<std::size_t N>
FixedString(const char (&)[N]) -> FixedString<N>;
class InternalReverseEngineeredObject class InternalReverseEngineeredObject
{ {
protected: protected:
@@ -23,26 +55,123 @@ namespace omath::rev_eng
return *reinterpret_cast<Type*>(reinterpret_cast<std::uintptr_t>(this) + offset); return *reinterpret_cast<Type*>(reinterpret_cast<std::uintptr_t>(this) + offset);
} }
template<std::size_t id, class ReturnType> template<class ReturnType>
ReturnType call_method(const void* ptr, auto... arg_list)
{
#ifdef _MSC_VER
using MethodType = ReturnType(__thiscall*)(void*, decltype(arg_list)...);
#else
using MethodType = ReturnType (*)(void*, decltype(arg_list)...);
#endif
return reinterpret_cast<MethodType>(const_cast<void*>(ptr))(this, arg_list...);
}
template<class ReturnType>
ReturnType call_method(const void* ptr, auto... arg_list) const
{
#ifdef _MSC_VER
using MethodType = ReturnType(__thiscall*)(const void*, decltype(arg_list)...);
#else
using MethodType = ReturnType (*)(const void*, decltype(arg_list)...);
#endif
return reinterpret_cast<MethodType>(const_cast<void*>(ptr))(this, arg_list...);
}
template<FixedString ModuleName, FixedString Pattern, class ReturnType>
ReturnType call_method(auto... arg_list)
{
static const auto* address = resolve_pattern(ModuleName, Pattern);
return call_method<ReturnType>(address, arg_list...);
}
template<FixedString ModuleName, FixedString Pattern, class ReturnType>
ReturnType call_method(auto... arg_list) const
{
static const auto* address = resolve_pattern(ModuleName, Pattern);
return call_method<ReturnType>(address, arg_list...);
}
template<class ReturnType>
ReturnType call_method(const std::string_view& module_name,const std::string_view& pattern, auto... arg_list)
{
static const auto* address = resolve_pattern(module_name, pattern);
return call_method<ReturnType>(address, arg_list...);
}
template<class ReturnType>
ReturnType call_method(const std::string_view& module_name,const std::string_view& pattern, auto... arg_list) const
{
static const auto* address = resolve_pattern(module_name, pattern);
return call_method<ReturnType>(address, arg_list...);
}
template<std::size_t Id, class ReturnType>
ReturnType call_virtual_method(auto... arg_list) ReturnType call_virtual_method(auto... arg_list)
{ {
#ifdef _MSC_VER const auto vtable = *reinterpret_cast<void***>(this);
using VirtualMethodType = ReturnType(__thiscall*)(void*, decltype(arg_list)...); return call_method<ReturnType>(vtable[Id], arg_list...);
#else
using VirtualMethodType = ReturnType (*)(void*, decltype(arg_list)...);
#endif
return (*reinterpret_cast<VirtualMethodType**>(this))[id](this, arg_list...);
} }
template<std::size_t id, class ReturnType> template<std::size_t Id, class ReturnType>
ReturnType call_virtual_method(auto... arg_list) const ReturnType call_virtual_method(auto... arg_list) const
{ {
#ifdef _MSC_VER const auto vtable = *reinterpret_cast<void* const* const*>(this);
using VirtualMethodType = ReturnType(__thiscall*)(void*, decltype(arg_list)...); return call_method<ReturnType>(vtable[Id], arg_list...);
}
private:
[[nodiscard]]
static const void* resolve_pattern(const std::string_view module_name, const std::string_view pattern)
{
const auto* base = get_module_base(module_name);
assert(base && "Failed to find module");
#ifdef _WIN32
const auto result = PePatternScanner::scan_for_pattern_in_loaded_module(base, pattern);
#elif defined(__APPLE__)
const auto result = MachOPatternScanner::scan_for_pattern_in_loaded_module(base, pattern);
#else #else
using VirtualMethodType = ReturnType (*)(void*, decltype(arg_list)...); const auto result = ElfPatternScanner::scan_for_pattern_in_loaded_module(base, pattern);
#endif
assert(result.has_value() && "Pattern scan failed");
return reinterpret_cast<const void*>(*result);
}
[[nodiscard]]
static const void* get_module_base(const std::string_view module_name)
{
#ifdef _WIN32
return GetModuleHandleA(module_name.data());
#elif defined(__APPLE__)
// On macOS, iterate loaded images to find the module by name
const auto count = _dyld_image_count();
for (std::uint32_t i = 0; i < count; ++i)
{
const auto* name = _dyld_get_image_name(i);
if (name && std::string_view{name}.find(module_name) != std::string_view::npos)
return static_cast<const void*>(_dyld_get_image_header(i));
}
return nullptr;
#else
// On Linux, use dl_iterate_phdr to find loaded module by name
struct CallbackData
{
std::string_view name;
const void* base;
} cb_data{module_name, nullptr};
dl_iterate_phdr(
[](dl_phdr_info* info, std::size_t, void* data) -> int
{
auto* cb = static_cast<CallbackData*>(data);
if (info->dlpi_name
&& std::string_view{info->dlpi_name}.find(cb->name) != std::string_view::npos)
{
cb->base = reinterpret_cast<const void*>(info->dlpi_addr);
return 1;
}
return 0;
},
&cb_data);
return cb_data.base;
#endif #endif
return (*static_cast<VirtualMethodType**>((void*)(this)))[id](
const_cast<void*>(static_cast<const void*>(this)), arg_list...);
} }
}; };
} // namespace omath::rev_eng } // namespace omath::rev_eng

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

View File

@@ -5,6 +5,7 @@
#include <cstdint> #include <cstdint>
#include <filesystem> #include <filesystem>
#include <optional> #include <optional>
#include <span>
#include <string_view> #include <string_view>
#include "section_scan_result.hpp" #include "section_scan_result.hpp"
namespace omath namespace omath
@@ -21,5 +22,10 @@ namespace omath
static std::optional<SectionScanResult> static std::optional<SectionScanResult>
scan_for_pattern_in_file(const std::filesystem::path& path_to_file, const std::string_view& pattern, scan_for_pattern_in_file(const std::filesystem::path& path_to_file, const std::string_view& pattern,
const std::string_view& target_section_name = ".text"); const std::string_view& target_section_name = ".text");
[[nodiscard]]
static std::optional<SectionScanResult>
scan_for_pattern_in_memory_file(std::span<const std::byte> file_data, const std::string_view& pattern,
const std::string_view& target_section_name = ".text");
}; };
} // namespace omath } // namespace omath

View File

@@ -5,6 +5,7 @@
#include <cstdint> #include <cstdint>
#include <filesystem> #include <filesystem>
#include <optional> #include <optional>
#include <span>
#include <string_view> #include <string_view>
#include "section_scan_result.hpp" #include "section_scan_result.hpp"
namespace omath namespace omath
@@ -21,5 +22,10 @@ namespace omath
static std::optional<SectionScanResult> static std::optional<SectionScanResult>
scan_for_pattern_in_file(const std::filesystem::path& path_to_file, const std::string_view& pattern, scan_for_pattern_in_file(const std::filesystem::path& path_to_file, const std::string_view& pattern,
const std::string_view& target_section_name = "__text"); const std::string_view& target_section_name = "__text");
[[nodiscard]]
static std::optional<SectionScanResult>
scan_for_pattern_in_memory_file(std::span<const std::byte> file_data, const std::string_view& pattern,
const std::string_view& target_section_name = "__text");
}; };
} // namespace omath } // namespace omath

View File

@@ -6,6 +6,7 @@
#include <cstdint> #include <cstdint>
#include <filesystem> #include <filesystem>
#include <optional> #include <optional>
#include <span>
#include <string_view> #include <string_view>
#include "section_scan_result.hpp" #include "section_scan_result.hpp"
namespace omath namespace omath
@@ -23,5 +24,10 @@ namespace omath
static std::optional<SectionScanResult> static std::optional<SectionScanResult>
scan_for_pattern_in_file(const std::filesystem::path& path_to_file, const std::string_view& pattern, scan_for_pattern_in_file(const std::filesystem::path& path_to_file, const std::string_view& pattern,
const std::string_view& target_section_name = ".text"); const std::string_view& target_section_name = ".text");
[[nodiscard]]
static std::optional<SectionScanResult>
scan_for_pattern_in_memory_file(std::span<const std::byte> file_data, const std::string_view& pattern,
const std::string_view& target_section_name = ".text");
}; };
} // namespace omath } // namespace omath

View File

@@ -15,7 +15,10 @@ endif()
set(EXAMPLES_BIN_DIR "${PROJECT_ROOT}/out/${CMAKE_BUILD_TYPE}") set(EXAMPLES_BIN_DIR "${PROJECT_ROOT}/out/${CMAKE_BUILD_TYPE}")
if(NOT EXISTS "${EXAMPLES_BIN_DIR}") if(NOT EXISTS "${EXAMPLES_BIN_DIR}")
message(FATAL_ERROR "Examples binary directory not found: ${EXAMPLES_BIN_DIR}. Please build the project first.") message(
FATAL_ERROR
"Examples binary directory not found: ${EXAMPLES_BIN_DIR}. Please build the project first."
)
endif() endif()
message(STATUS "Looking for benchmark executables in: ${EXAMPLES_BIN_DIR}") message(STATUS "Looking for benchmark executables in: ${EXAMPLES_BIN_DIR}")
@@ -43,16 +46,13 @@ foreach(EXAMPLE_PATH ${EXAMPLE_FILES})
endif() endif()
# On Linux/macOS, check permissions or just try to run it. # On Linux/macOS, check permissions or just try to run it.
message(STATUS "-------------------------------------------------") message(STATUS "-------------------------------------------------")
message(STATUS "Running benchmark: ${FILENAME}") message(STATUS "Running benchmark: ${FILENAME}")
message(STATUS "-------------------------------------------------") message(STATUS "-------------------------------------------------")
execute_process( execute_process(COMMAND "${EXAMPLE_PATH}" WORKING_DIRECTORY "${PROJECT_ROOT}"
COMMAND "${EXAMPLE_PATH}" RESULT_VARIABLE EXIT_CODE)
WORKING_DIRECTORY "${PROJECT_ROOT}"
RESULT_VARIABLE EXIT_CODE
)
if(NOT EXIT_CODE EQUAL 0) if(NOT EXIT_CODE EQUAL 0)
message(WARNING "Benchmark ${FILENAME} exited with error code: ${EXIT_CODE}") message(WARNING "Benchmark ${FILENAME} exited with error code: ${EXIT_CODE}")

View File

@@ -15,7 +15,10 @@ endif()
set(EXAMPLES_BIN_DIR "${PROJECT_ROOT}/out/${CMAKE_BUILD_TYPE}") set(EXAMPLES_BIN_DIR "${PROJECT_ROOT}/out/${CMAKE_BUILD_TYPE}")
if(NOT EXISTS "${EXAMPLES_BIN_DIR}") if(NOT EXISTS "${EXAMPLES_BIN_DIR}")
message(FATAL_ERROR "Examples binary directory not found: ${EXAMPLES_BIN_DIR}. Please build the project first.") message(
FATAL_ERROR
"Examples binary directory not found: ${EXAMPLES_BIN_DIR}. Please build the project first."
)
endif() endif()
message(STATUS "Looking for example executables in: ${EXAMPLES_BIN_DIR}") message(STATUS "Looking for example executables in: ${EXAMPLES_BIN_DIR}")
@@ -43,16 +46,13 @@ foreach(EXAMPLE_PATH ${EXAMPLE_FILES})
endif() endif()
# On Linux/macOS, check permissions or just try to run it. # On Linux/macOS, check permissions or just try to run it.
message(STATUS "-------------------------------------------------") message(STATUS "-------------------------------------------------")
message(STATUS "Running example: ${FILENAME}") message(STATUS "Running example: ${FILENAME}")
message(STATUS "-------------------------------------------------") message(STATUS "-------------------------------------------------")
execute_process( execute_process(COMMAND "${EXAMPLE_PATH}" WORKING_DIRECTORY "${PROJECT_ROOT}"
COMMAND "${EXAMPLE_PATH}" RESULT_VARIABLE EXIT_CODE)
WORKING_DIRECTORY "${PROJECT_ROOT}"
RESULT_VARIABLE EXIT_CODE
)
if(NOT EXIT_CODE EQUAL 0) if(NOT EXIT_CODE EQUAL 0)
message(WARNING "Example ${FILENAME} exited with error code: ${EXIT_CODE}") message(WARNING "Example ${FILENAME} exited with error code: ${EXIT_CODE}")

View File

@@ -15,7 +15,10 @@ endif()
set(EXAMPLES_BIN_DIR "${PROJECT_ROOT}/out/${CMAKE_BUILD_TYPE}") set(EXAMPLES_BIN_DIR "${PROJECT_ROOT}/out/${CMAKE_BUILD_TYPE}")
if(NOT EXISTS "${EXAMPLES_BIN_DIR}") if(NOT EXISTS "${EXAMPLES_BIN_DIR}")
message(FATAL_ERROR "Examples binary directory not found: ${EXAMPLES_BIN_DIR}. Please build the project first.") message(
FATAL_ERROR
"Examples binary directory not found: ${EXAMPLES_BIN_DIR}. Please build the project first."
)
endif() endif()
message(STATUS "Looking for unit test executables in: ${EXAMPLES_BIN_DIR}") message(STATUS "Looking for unit test executables in: ${EXAMPLES_BIN_DIR}")
@@ -43,16 +46,13 @@ foreach(EXAMPLE_PATH ${EXAMPLE_FILES})
endif() endif()
# On Linux/macOS, check permissions or just try to run it. # On Linux/macOS, check permissions or just try to run it.
message(STATUS "-------------------------------------------------") message(STATUS "-------------------------------------------------")
message(STATUS "Running unit_tests: ${FILENAME}") message(STATUS "Running unit_tests: ${FILENAME}")
message(STATUS "-------------------------------------------------") message(STATUS "-------------------------------------------------")
execute_process( execute_process(COMMAND "${EXAMPLE_PATH}" WORKING_DIRECTORY "${PROJECT_ROOT}"
COMMAND "${EXAMPLE_PATH}" RESULT_VARIABLE EXIT_CODE)
WORKING_DIRECTORY "${PROJECT_ROOT}"
RESULT_VARIABLE EXIT_CODE
)
if(NOT EXIT_CODE EQUAL 0) if(NOT EXIT_CODE EQUAL 0)
message(WARNING "Example ${FILENAME} exited with error code: ${EXIT_CODE}") message(WARNING "Example ${FILENAME} exited with error code: ${EXIT_CODE}")

15
scripts/cmake-format.sh Normal file
View File

@@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -euo pipefail
# Format all CMakeLists.txt and *.cmake files in the repo (excluding common build dirs)
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_ROOT"
find . \
-path "./build" -prune -o \
-path "./cmake-build*" -prune -o \
-path "./out" -prune -o \
-path "./.git" -prune -o \
\( -name "CMakeLists.txt" -o -name "*.cmake" \) -print0 \
| xargs -0 cmake-format -i

View File

@@ -0,0 +1,42 @@
//
// Created by Vlad on 3/22/2025.
//
#include "omath/engines/cry_engine/formulas.hpp"
namespace omath::cry_engine
{
Vector3<float> forward_vector(const ViewAngles& angles) noexcept
{
const auto vec = rotation_matrix(angles) * mat_column_from_vector(k_abs_forward);
return {vec.at(0, 0), vec.at(1, 0), vec.at(2, 0)};
}
Vector3<float> right_vector(const ViewAngles& angles) noexcept
{
const auto vec = rotation_matrix(angles) * mat_column_from_vector(k_abs_right);
return {vec.at(0, 0), vec.at(1, 0), vec.at(2, 0)};
}
Vector3<float> up_vector(const ViewAngles& angles) noexcept
{
const auto vec = rotation_matrix(angles) * mat_column_from_vector(k_abs_up);
return {vec.at(0, 0), vec.at(1, 0), vec.at(2, 0)};
}
Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3<float>& cam_origin) noexcept
{
return mat_camera_view<float, MatStoreType::ROW_MAJOR>(forward_vector(angles), right_vector(angles),
up_vector(angles), cam_origin);
}
Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept
{
return mat_rotation_axis_z<float, MatStoreType::ROW_MAJOR>(angles.yaw)
* mat_rotation_axis_y<float, MatStoreType::ROW_MAJOR>(angles.roll)
* mat_rotation_axis_x<float, MatStoreType::ROW_MAJOR>(angles.pitch);
}
Mat4X4 calc_perspective_projection_matrix(const float field_of_view, const float aspect_ratio, const float near,
const float far) noexcept
{
return mat_perspective_left_handed(field_of_view, aspect_ratio, near, far);
}
} // namespace omath::unity_engine

View File

@@ -0,0 +1,26 @@
//
// Created by Vlad on 8/11/2025.
//
#include "omath/engines/cry_engine/traits/camera_trait.hpp"
namespace omath::cry_engine
{
ViewAngles CameraTrait::calc_look_at_angle(const Vector3<float>& cam_origin, const Vector3<float>& look_at) noexcept
{
const auto direction = (look_at - cam_origin).normalized();
return {PitchAngle::from_radians(std::asin(direction.z)),
YawAngle::from_radians(-std::atan2(direction.x, direction.y)), RollAngle::from_radians(0.f)};
}
Mat4X4 CameraTrait::calc_view_matrix(const ViewAngles& angles, const Vector3<float>& cam_origin) noexcept
{
return cry_engine::calc_view_matrix(angles, cam_origin);
}
Mat4X4 CameraTrait::calc_projection_matrix(const projection::FieldOfView& fov,
const projection::ViewPort& view_port, const float near,
const float far) noexcept
{
return calc_perspective_projection_matrix(fov.as_degrees(), view_port.aspect_ratio(), near, far);
}
} // namespace omath::unity_engine

27
source/hud/canvas_box.cpp Normal file
View File

@@ -0,0 +1,27 @@
//
// Created by orange on 13.03.2026.
//
//
// Created by Vlad on 6/17/2025.
//
#include "omath/hud/canvas_box.hpp"
namespace omath::hud
{
CanvasBox::CanvasBox(const Vector2<float> top, Vector2<float> bottom, const float ratio)
{
bottom.x = top.x;
const auto height = std::abs(top.y - bottom.y);
top_left_corner = top - Vector2<float>{height / ratio, 0};
top_right_corner = top + Vector2<float>{height / ratio, 0};
bottom_left_corner = bottom - Vector2<float>{height / ratio, 0};
bottom_right_corner = bottom + Vector2<float>{height / ratio, 0};
}
std::array<Vector2<float>, 4> CanvasBox::as_array() const
{
return {top_left_corner, top_right_corner, bottom_right_corner, bottom_left_corner};
}
} // namespace ohud

View File

@@ -0,0 +1,870 @@
//
// Created by orange on 13.03.2026.
//
#include "omath/hud/entity_overlay.hpp"
namespace omath::hud
{
EntityOverlay& EntityOverlay::add_2d_box(const Color& box_color, const Color& fill_color, const float thickness)
{
const auto points = m_canvas.as_array();
m_renderer->add_polyline({points.data(), points.size()}, box_color, thickness);
if (fill_color.value().w > 0.f)
m_renderer->add_filled_polyline({points.data(), points.size()}, fill_color);
return *this;
}
EntityOverlay& EntityOverlay::add_cornered_2d_box(const Color& box_color, const Color& fill_color,
const float corner_ratio_len, const float thickness)
{
const auto corner_line_length =
std::abs((m_canvas.top_left_corner - m_canvas.top_right_corner).x * corner_ratio_len);
if (fill_color.value().w > 0.f)
add_2d_box(fill_color, fill_color);
// Left Side
m_renderer->add_line(m_canvas.top_left_corner,
m_canvas.top_left_corner + Vector2<float>{corner_line_length, 0.f}, box_color, thickness);
m_renderer->add_line(m_canvas.top_left_corner,
m_canvas.top_left_corner + Vector2<float>{0.f, corner_line_length}, box_color, thickness);
m_renderer->add_line(m_canvas.bottom_left_corner,
m_canvas.bottom_left_corner - Vector2<float>{0.f, corner_line_length}, box_color,
thickness);
m_renderer->add_line(m_canvas.bottom_left_corner,
m_canvas.bottom_left_corner + Vector2<float>{corner_line_length, 0.f}, box_color,
thickness);
// Right Side
m_renderer->add_line(m_canvas.top_right_corner,
m_canvas.top_right_corner - Vector2<float>{corner_line_length, 0.f}, box_color, thickness);
m_renderer->add_line(m_canvas.top_right_corner,
m_canvas.top_right_corner + Vector2<float>{0.f, corner_line_length}, box_color, thickness);
m_renderer->add_line(m_canvas.bottom_right_corner,
m_canvas.bottom_right_corner - Vector2<float>{0.f, corner_line_length}, box_color,
thickness);
m_renderer->add_line(m_canvas.bottom_right_corner,
m_canvas.bottom_right_corner - Vector2<float>{corner_line_length, 0.f}, box_color,
thickness);
return *this;
}
EntityOverlay& EntityOverlay::add_right_bar(const Color& color, const Color& outline_color, const Color& bg_color,
const float width, float ratio, const float offset)
{
ratio = std::clamp(ratio, 0.f, 1.f);
const auto max_bar_height = std::abs(m_canvas.top_right_corner.y - m_canvas.bottom_right_corner.y);
const auto bar_start = Vector2<float>{m_text_cursor_right.x + offset, m_canvas.bottom_right_corner.y};
m_renderer->add_filled_rectangle(bar_start, bar_start + Vector2<float>(width, -max_bar_height), bg_color);
m_renderer->add_filled_rectangle(bar_start, bar_start + Vector2<float>(width, -max_bar_height * ratio), color);
m_renderer->add_rectangle(bar_start - Vector2<float>(1.f, 0.f),
bar_start + Vector2<float>(width, -max_bar_height), outline_color);
m_text_cursor_right.x += offset + width;
return *this;
}
EntityOverlay& EntityOverlay::add_left_bar(const Color& color, const Color& outline_color, const Color& bg_color,
const float width, float ratio, const float offset)
{
ratio = std::clamp(ratio, 0.f, 1.f);
const auto max_bar_height = std::abs(m_canvas.top_left_corner.y - m_canvas.bottom_right_corner.y);
const auto bar_start = Vector2<float>{m_text_cursor_left.x - (offset + width), m_canvas.bottom_left_corner.y};
m_renderer->add_filled_rectangle(bar_start, bar_start + Vector2<float>(width, -max_bar_height), bg_color);
m_renderer->add_filled_rectangle(bar_start, bar_start + Vector2<float>(width, -max_bar_height * ratio), color);
m_renderer->add_rectangle(bar_start - Vector2<float>(1.f, 0.f),
bar_start + Vector2<float>(width, -max_bar_height), outline_color);
m_text_cursor_left.x -= offset + width;
return *this;
}
EntityOverlay& EntityOverlay::add_right_label(const Color& color, const float offset, const bool outlined,
const std::string_view& text)
{
if (outlined)
draw_outlined_text(m_text_cursor_right + Vector2<float>{offset, 0.f}, color, text);
else
m_renderer->add_text(m_text_cursor_right + Vector2<float>{offset, 0.f}, color, text.data());
m_text_cursor_right.y += m_renderer->calc_text_size(text.data()).y;
return *this;
}
EntityOverlay& EntityOverlay::add_top_label(const Color& color, const float offset, const bool outlined,
const std::string_view text)
{
m_text_cursor_top.y -= m_renderer->calc_text_size(text.data()).y;
if (outlined)
draw_outlined_text(m_text_cursor_top + Vector2<float>{0.f, -offset}, color, text);
else
m_renderer->add_text(m_text_cursor_top + Vector2<float>{0.f, -offset}, color, text.data());
return *this;
}
EntityOverlay& EntityOverlay::add_top_bar(const Color& color, const Color& outline_color, const Color& bg_color,
const float height, float ratio, const float offset)
{
ratio = std::clamp(ratio, 0.f, 1.f);
const auto max_bar_width = std::abs(m_canvas.top_left_corner.x - m_canvas.bottom_right_corner.x);
const auto bar_start = Vector2<float>{m_canvas.top_left_corner.x, m_text_cursor_top.y - offset};
m_renderer->add_filled_rectangle(bar_start, bar_start + Vector2<float>(max_bar_width, -height), bg_color);
m_renderer->add_filled_rectangle(bar_start, bar_start + Vector2<float>(max_bar_width * ratio, -height), color);
m_renderer->add_rectangle(bar_start, bar_start + Vector2<float>(max_bar_width, -height), outline_color);
m_text_cursor_top.y -= offset + height;
return *this;
}
EntityOverlay& EntityOverlay::add_snap_line(const Vector2<float>& start_pos, const Color& color, const float width)
{
const Vector2<float> line_end =
m_canvas.bottom_left_corner
+ Vector2<float>{m_canvas.bottom_right_corner.x - m_canvas.bottom_left_corner.x, 0.f} / 2;
m_renderer->add_line(start_pos, line_end, color, width);
return *this;
}
void EntityOverlay::draw_dashed_fill(const Vector2<float>& origin, const Vector2<float>& step_dir,
const Vector2<float>& perp_dir, const float full_len, const float filled_len,
const Color& fill_color, const Color& split_color, const float dash_len,
const float gap_len) const
{
if (full_len <= 0.f)
return;
const float step = dash_len + gap_len;
const float n = std::floor((full_len + gap_len) / step);
if (n < 1.f)
return;
const float used = n * dash_len + (n - 1.f) * gap_len;
const float offset = (full_len - used) / 2.f;
const auto fill_rect = [&](const Vector2<float>& a, const Vector2<float>& b, const Color& c)
{
m_renderer->add_filled_rectangle({std::min(a.x, b.x), std::min(a.y, b.y)},
{std::max(a.x, b.x), std::max(a.y, b.y)}, c);
};
// Draw split lines (gaps) across the full bar first
// Leading gap
if (offset > 0.f)
fill_rect(origin, origin + step_dir * offset + perp_dir, split_color);
for (float i = 0.f; i < n; ++i)
{
const float dash_start = offset + i * step;
const float dash_end = dash_start + dash_len;
const float gap_start = dash_end;
const float gap_end = dash_start + step;
// Fill dash only up to filled_len
if (dash_start < filled_len)
{
const auto a = origin + step_dir * dash_start;
const auto b = a + step_dir * std::min(dash_len, filled_len - dash_start) + perp_dir;
fill_rect(a, b, fill_color);
}
// Split line (gap) — always drawn across full bar
if (i < n - 1.f && gap_start < full_len)
{
const auto a = origin + step_dir * gap_start;
const auto b = origin + step_dir * std::min(gap_end, full_len) + perp_dir;
fill_rect(a, b, split_color);
}
}
// Trailing gap
const float trail_start = offset + n * dash_len + (n - 1.f) * gap_len;
if (trail_start < full_len)
fill_rect(origin + step_dir * trail_start, origin + step_dir * full_len + perp_dir, split_color);
}
EntityOverlay& EntityOverlay::add_right_dashed_bar(const Color& color, const Color& outline_color,
const Color& bg_color, const float width, float ratio,
const float dash_len, const float gap_len, const float offset)
{
ratio = std::clamp(ratio, 0.f, 1.f);
const float height = std::abs(m_canvas.top_right_corner.y - m_canvas.bottom_right_corner.y);
const auto bar_start = Vector2<float>{m_text_cursor_right.x + offset, m_canvas.bottom_right_corner.y};
m_renderer->add_filled_rectangle(bar_start, bar_start + Vector2<float>{width, -height}, bg_color);
draw_dashed_fill(bar_start, {0.f, -1.f}, {width, 0.f}, height, height * ratio, color, outline_color, dash_len,
gap_len);
m_renderer->add_rectangle(bar_start - Vector2<float>{1.f, 0.f}, bar_start + Vector2<float>{width, -height},
outline_color);
m_text_cursor_right.x += offset + width;
return *this;
}
EntityOverlay& EntityOverlay::add_left_dashed_bar(const Color& color, const Color& outline_color,
const Color& bg_color, const float width, float ratio,
const float dash_len, const float gap_len, const float offset)
{
ratio = std::clamp(ratio, 0.f, 1.f);
const float height = std::abs(m_canvas.top_left_corner.y - m_canvas.bottom_left_corner.y);
const auto bar_start = Vector2<float>{m_text_cursor_left.x - (offset + width), m_canvas.bottom_left_corner.y};
m_renderer->add_filled_rectangle(bar_start, bar_start + Vector2<float>{width, -height}, bg_color);
draw_dashed_fill(bar_start, {0.f, -1.f}, {width, 0.f}, height, height * ratio, color, outline_color, dash_len,
gap_len);
m_renderer->add_rectangle(bar_start - Vector2<float>{1.f, 0.f}, bar_start + Vector2<float>{width, -height},
outline_color);
m_text_cursor_left.x -= offset + width;
return *this;
}
EntityOverlay& EntityOverlay::add_top_dashed_bar(const Color& color, const Color& outline_color,
const Color& bg_color, const float height, float ratio,
const float dash_len, const float gap_len, const float offset)
{
ratio = std::clamp(ratio, 0.f, 1.f);
const float bar_w = std::abs(m_canvas.top_left_corner.x - m_canvas.top_right_corner.x);
const auto bar_start = Vector2<float>{m_canvas.top_left_corner.x, m_text_cursor_top.y - offset};
m_renderer->add_filled_rectangle(bar_start, bar_start + Vector2<float>{bar_w, -height}, bg_color);
draw_dashed_fill(bar_start, {1.f, 0.f}, {0.f, -height}, bar_w, bar_w * ratio, color, outline_color, dash_len,
gap_len);
m_renderer->add_rectangle(bar_start, bar_start + Vector2<float>{bar_w, -height}, outline_color);
m_text_cursor_top.y -= offset + height;
return *this;
}
EntityOverlay& EntityOverlay::add_bottom_dashed_bar(const Color& color, const Color& outline_color,
const Color& bg_color, const float height, float ratio,
const float dash_len, const float gap_len, const float offset)
{
ratio = std::clamp(ratio, 0.f, 1.f);
const float bar_w = std::abs(m_canvas.bottom_left_corner.x - m_canvas.bottom_right_corner.x);
const auto bar_start = Vector2<float>{m_canvas.bottom_left_corner.x, m_text_cursor_bottom.y + offset};
m_renderer->add_filled_rectangle(bar_start, bar_start + Vector2<float>{bar_w, height}, bg_color);
draw_dashed_fill(bar_start, {1.f, 0.f}, {0.f, height}, bar_w, bar_w * ratio, color, outline_color, dash_len,
gap_len);
m_renderer->add_rectangle(bar_start, bar_start + Vector2<float>{bar_w, height}, outline_color);
m_text_cursor_bottom.y += offset + height;
return *this;
}
EntityOverlay& EntityOverlay::add_skeleton(const Color& color, const float thickness)
{
// Maps normalized (rx in [0,1], ry in [0,1]) to canvas screen position
const auto joint = [&](const float rx, const float ry) -> Vector2<float>
{
const auto top = m_canvas.top_left_corner + (m_canvas.top_right_corner - m_canvas.top_left_corner) * rx;
const auto bot =
m_canvas.bottom_left_corner + (m_canvas.bottom_right_corner - m_canvas.bottom_left_corner) * rx;
return top + (bot - top) * ry;
};
using B = std::pair<std::pair<float, float>, std::pair<float, float>>;
static constexpr std::array<B, 15> k_bones{{
// Spine
{{0.50f, 0.13f}, {0.50f, 0.22f}}, // head → neck
{{0.50f, 0.22f}, {0.50f, 0.38f}}, // neck → chest
{{0.50f, 0.38f}, {0.50f, 0.55f}}, // chest → pelvis
// Left arm
{{0.50f, 0.22f}, {0.25f, 0.25f}}, // neck → L shoulder
{{0.25f, 0.25f}, {0.13f, 0.42f}}, // L shoulder → L elbow
{{0.13f, 0.42f}, {0.08f, 0.56f}}, // L elbow → L hand
// Right arm
{{0.50f, 0.22f}, {0.75f, 0.25f}}, // neck → R shoulder
{{0.75f, 0.25f}, {0.87f, 0.42f}}, // R shoulder → R elbow
{{0.87f, 0.42f}, {0.92f, 0.56f}}, // R elbow → R hand
// Left leg
{{0.50f, 0.55f}, {0.36f, 0.58f}}, // pelvis → L hip
{{0.36f, 0.58f}, {0.32f, 0.77f}}, // L hip → L knee
{{0.32f, 0.77f}, {0.27f, 0.97f}}, // L knee → L foot
// Right leg
{{0.50f, 0.55f}, {0.64f, 0.58f}}, // pelvis → R hip
{{0.64f, 0.58f}, {0.68f, 0.77f}}, // R hip → R knee
{{0.68f, 0.77f}, {0.73f, 0.97f}}, // R knee → R foot
}};
for (const auto& [a, b] : k_bones)
m_renderer->add_line(joint(a.first, a.second), joint(b.first, b.second), color, thickness);
return *this;
}
void EntityOverlay::draw_dashed_line(const Vector2<float>& from, const Vector2<float>& to, const Color& color,
const float dash_len, const float gap_len, const float thickness) const
{
const auto total = (to - from).length();
if (total <= 0.f)
return;
const auto dir = (to - from).normalized();
const float step = dash_len + gap_len;
const float n_dashes = std::floor((total + gap_len) / step);
if (n_dashes < 1.f)
return;
const float used = n_dashes * dash_len + (n_dashes - 1.f) * gap_len;
const float offset = (total - used) / 2.f;
for (float i = 0.f; i < n_dashes; ++i)
{
const float pos = offset + i * step;
const auto dash_start = from + dir * pos;
const auto dash_end = from + dir * std::min(pos + dash_len, total);
m_renderer->add_line(dash_start, dash_end, color, thickness);
}
}
EntityOverlay& EntityOverlay::add_dashed_box(const Color& color, const float dash_len, const float gap_len,
const float thickness)
{
const float min_edge = std::min((m_canvas.top_right_corner - m_canvas.top_left_corner).length(),
(m_canvas.bottom_right_corner - m_canvas.top_right_corner).length());
const float corner_len = std::min(dash_len, min_edge / 2.f);
const auto draw_edge = [&](const Vector2<float>& from, const Vector2<float>& to)
{
const auto dir = (to - from).normalized();
m_renderer->add_line(from, from + dir * corner_len, color, thickness);
draw_dashed_line(from + dir * corner_len, to - dir * corner_len, color, dash_len, gap_len, thickness);
m_renderer->add_line(to - dir * corner_len, to, color, thickness);
};
draw_edge(m_canvas.top_left_corner, m_canvas.top_right_corner);
draw_edge(m_canvas.top_right_corner, m_canvas.bottom_right_corner);
draw_edge(m_canvas.bottom_right_corner, m_canvas.bottom_left_corner);
draw_edge(m_canvas.bottom_left_corner, m_canvas.top_left_corner);
return *this;
}
void EntityOverlay::draw_outlined_text(const Vector2<float>& position, const Color& color,
const std::string_view& text)
{
static constexpr std::array outline_offsets = {
Vector2<float>{-1, -1}, Vector2<float>{-1, 0}, Vector2<float>{-1, 1}, Vector2<float>{0, -1},
Vector2<float>{0, 1}, Vector2<float>{1, -1}, Vector2<float>{1, 0}, Vector2<float>{1, 1}};
for (const auto& outline_offset : outline_offsets)
m_renderer->add_text(position + outline_offset, Color{0.f, 0.f, 0.f, 1.f}, text.data());
m_renderer->add_text(position, color, text.data());
}
EntityOverlay& EntityOverlay::add_bottom_bar(const Color& color, const Color& outline_color, const Color& bg_color,
const float height, float ratio, const float offset)
{
ratio = std::clamp(ratio, 0.f, 1.f);
const auto max_bar_width = std::abs(m_canvas.bottom_right_corner.x - m_canvas.bottom_left_corner.x);
const auto bar_start = Vector2<float>{m_canvas.bottom_left_corner.x, m_text_cursor_bottom.y + offset};
m_renderer->add_filled_rectangle(bar_start, bar_start + Vector2<float>(max_bar_width, height), bg_color);
m_renderer->add_filled_rectangle(bar_start, bar_start + Vector2<float>(max_bar_width * ratio, height), color);
m_renderer->add_rectangle(bar_start, bar_start + Vector2<float>(max_bar_width, height), outline_color);
m_text_cursor_bottom.y += offset + height;
return *this;
}
EntityOverlay& EntityOverlay::add_bottom_label(const Color& color, const float offset, const bool outlined,
const std::string_view text)
{
const auto text_size = m_renderer->calc_text_size(text);
if (outlined)
draw_outlined_text(m_text_cursor_bottom + Vector2<float>{0.f, offset}, color, text);
else
m_renderer->add_text(m_text_cursor_bottom + Vector2<float>{0.f, offset}, color, text);
m_text_cursor_bottom.y += text_size.y;
return *this;
}
EntityOverlay& EntityOverlay::add_left_label(const Color& color, const float offset, const bool outlined,
const std::string_view& text)
{
const auto text_size = m_renderer->calc_text_size(text);
const auto pos = m_text_cursor_left + Vector2<float>{-(offset + text_size.x), 0.f};
if (outlined)
draw_outlined_text(pos, color, text);
else
m_renderer->add_text(pos, color, text);
m_text_cursor_left.y += text_size.y;
return *this;
}
EntityOverlay& EntityOverlay::add_centered_bottom_label(const Color& color, const float offset, const bool outlined,
const std::string_view& text)
{
const auto text_size = m_renderer->calc_text_size(text);
const auto box_center_x =
m_canvas.bottom_left_corner.x + (m_canvas.bottom_right_corner.x - m_canvas.bottom_left_corner.x) / 2.f;
const auto pos = Vector2<float>{box_center_x - text_size.x / 2.f, m_text_cursor_bottom.y + offset};
if (outlined)
draw_outlined_text(pos, color, text);
else
m_renderer->add_text(pos, color, text);
m_text_cursor_bottom.y += text_size.y;
return *this;
}
EntityOverlay& EntityOverlay::add_centered_top_label(const Color& color, const float offset, const bool outlined,
const std::string_view& text)
{
const auto text_size = m_renderer->calc_text_size(text);
const auto box_center_x =
m_canvas.top_left_corner.x + (m_canvas.top_right_corner.x - m_canvas.top_left_corner.x) / 2.f;
m_text_cursor_top.y -= text_size.y;
const auto pos = Vector2<float>{box_center_x - text_size.x / 2.f, m_text_cursor_top.y - offset};
if (outlined)
draw_outlined_text(pos, color, text);
else
m_renderer->add_text(pos, color, text);
return *this;
}
EntityOverlay::EntityOverlay(const Vector2<float>& top, const Vector2<float>& bottom,
const std::shared_ptr<HudRendererInterface>& renderer)
: m_canvas(top, bottom), m_text_cursor_right(m_canvas.top_right_corner),
m_text_cursor_top(m_canvas.top_left_corner), m_text_cursor_bottom(m_canvas.bottom_left_corner),
m_text_cursor_left(m_canvas.top_left_corner), m_renderer(renderer)
{
}
// ── Spacers ─────────────────────────────────────────────────────────────────
EntityOverlay& EntityOverlay::add_right_space_vertical(const float size)
{
m_text_cursor_right.y += size;
return *this;
}
EntityOverlay& EntityOverlay::add_right_space_horizontal(const float size)
{
m_text_cursor_right.x += size;
return *this;
}
EntityOverlay& EntityOverlay::add_left_space_vertical(const float size)
{
m_text_cursor_left.y += size;
return *this;
}
EntityOverlay& EntityOverlay::add_left_space_horizontal(const float size)
{
m_text_cursor_left.x -= size;
return *this;
}
EntityOverlay& EntityOverlay::add_top_space_vertical(const float size)
{
m_text_cursor_top.y -= size;
return *this;
}
EntityOverlay& EntityOverlay::add_top_space_horizontal(const float size)
{
m_text_cursor_top.x += size;
return *this;
}
EntityOverlay& EntityOverlay::add_bottom_space_vertical(const float size)
{
m_text_cursor_bottom.y += size;
return *this;
}
EntityOverlay& EntityOverlay::add_bottom_space_horizontal(const float size)
{
m_text_cursor_bottom.x += size;
return *this;
}
// ── Progress rings ──────────────────────────────────────────────────────────
EntityOverlay& EntityOverlay::add_right_progress_ring(const Color& color, const Color& bg, const float radius,
const float ratio, const float thickness, const float offset,
const int segments)
{
const auto cx = m_text_cursor_right.x + offset + radius;
const auto cy = m_text_cursor_right.y + radius;
draw_progress_ring({cx, cy}, widget::ProgressRing{color, bg, radius, ratio, thickness, offset, segments});
m_text_cursor_right.y += radius * 2.f;
return *this;
}
EntityOverlay& EntityOverlay::add_left_progress_ring(const Color& color, const Color& bg, const float radius,
const float ratio, const float thickness, const float offset,
const int segments)
{
const auto cx = m_text_cursor_left.x - offset - radius;
const auto cy = m_text_cursor_left.y + radius;
draw_progress_ring({cx, cy}, widget::ProgressRing{color, bg, radius, ratio, thickness, offset, segments});
m_text_cursor_left.y += radius * 2.f;
return *this;
}
EntityOverlay& EntityOverlay::add_top_progress_ring(const Color& color, const Color& bg, const float radius,
const float ratio, const float thickness, const float offset,
const int segments)
{
m_text_cursor_top.y -= radius * 2.f;
const auto cx = m_text_cursor_top.x + radius;
const auto cy = m_text_cursor_top.y - offset + radius;
draw_progress_ring({cx, cy}, widget::ProgressRing{color, bg, radius, ratio, thickness, offset, segments});
return *this;
}
EntityOverlay& EntityOverlay::add_bottom_progress_ring(const Color& color, const Color& bg, const float radius,
const float ratio, const float thickness, const float offset,
const int segments)
{
const auto cx = m_text_cursor_bottom.x + radius;
const auto cy = m_text_cursor_bottom.y + offset + radius;
draw_progress_ring({cx, cy}, widget::ProgressRing{color, bg, radius, ratio, thickness, offset, segments});
m_text_cursor_bottom.y += radius * 2.f;
return *this;
}
// ── Icons ────────────────────────────────────────────────────────────────────
EntityOverlay& EntityOverlay::add_right_icon(const std::any& texture_id, const float width, const float height,
const Color& tint, const float offset)
{
const auto pos = m_text_cursor_right + Vector2<float>{offset, 0.f};
m_renderer->add_image(texture_id, pos, pos + Vector2<float>{width, height}, tint);
m_text_cursor_right.y += height;
return *this;
}
EntityOverlay& EntityOverlay::add_left_icon(const std::any& texture_id, const float width, const float height,
const Color& tint, const float offset)
{
const auto pos = m_text_cursor_left + Vector2<float>{-(offset + width), 0.f};
m_renderer->add_image(texture_id, pos, pos + Vector2<float>{width, height}, tint);
m_text_cursor_left.y += height;
return *this;
}
EntityOverlay& EntityOverlay::add_top_icon(const std::any& texture_id, const float width, const float height,
const Color& tint, const float offset)
{
m_text_cursor_top.y -= height;
const auto pos = m_text_cursor_top + Vector2<float>{0.f, -offset};
m_renderer->add_image(texture_id, pos, pos + Vector2<float>{width, height}, tint);
return *this;
}
EntityOverlay& EntityOverlay::add_bottom_icon(const std::any& texture_id, const float width, const float height,
const Color& tint, const float offset)
{
const auto pos = m_text_cursor_bottom + Vector2<float>{0.f, offset};
m_renderer->add_image(texture_id, pos, pos + Vector2<float>{width, height}, tint);
m_text_cursor_bottom.y += height;
return *this;
}
// ── widget dispatch ───────────────────────────────────────────────────────
void EntityOverlay::dispatch(const widget::Box& box)
{
add_2d_box(box.color, box.fill, box.thickness);
}
void EntityOverlay::dispatch(const widget::CorneredBox& cornered_box)
{
add_cornered_2d_box(cornered_box.color, cornered_box.fill, cornered_box.corner_ratio, cornered_box.thickness);
}
void EntityOverlay::dispatch(const widget::DashedBox& dashed_box)
{
add_dashed_box(dashed_box.color, dashed_box.dash_len, dashed_box.gap_len, dashed_box.thickness);
}
void EntityOverlay::dispatch(const widget::Skeleton& skeleton)
{
add_skeleton(skeleton.color, skeleton.thickness);
}
void EntityOverlay::dispatch(const widget::SnapLine& snap_line)
{
add_snap_line(snap_line.start, snap_line.color, snap_line.width);
}
void EntityOverlay::dispatch(const widget::ScanMarker& scan_marker)
{
const auto box_width = std::abs(m_canvas.top_right_corner.x - m_canvas.top_left_corner.x);
const auto box_height = std::abs(m_canvas.bottom_left_corner.y - m_canvas.top_left_corner.y);
const auto center_x = (m_canvas.top_left_corner.x + m_canvas.top_right_corner.x) / 2.f;
const auto center_y = m_canvas.top_left_corner.y + box_height * 0.44f;
const auto side = std::min(box_width, box_height) * 0.5f;
const auto h = side * std::sqrt(3.f) / 2.f;
const std::array<Vector2<float>, 3> tri = {
Vector2<float>{center_x, center_y - h * 2.f / 3.f},
Vector2<float>{center_x - side / 2.f, center_y + h / 3.f},
Vector2<float>{center_x + side / 2.f, center_y + h / 3.f},
};
m_renderer->add_filled_polyline({tri.data(), tri.size()}, scan_marker.color);
if (scan_marker.outline.value().w > 0.f)
m_renderer->add_polyline({tri.data(), tri.size()}, scan_marker.outline, scan_marker.outline_thickness);
}
void EntityOverlay::dispatch(const widget::AimDot& aim_dot)
{
m_renderer->add_filled_circle(aim_dot.position, aim_dot.radius, aim_dot.color);
}
void EntityOverlay::dispatch(const widget::ProjectileAim& proj_widget)
{
const auto box_width = std::abs(m_canvas.top_right_corner.x - m_canvas.top_left_corner.x);
const auto box_height = std::abs(m_canvas.bottom_left_corner.y - m_canvas.top_left_corner.y);
const auto box_center = m_canvas.top_left_corner + Vector2{box_width, box_height} / 2.f;
m_renderer->add_line(box_center, proj_widget.position, proj_widget.color, proj_widget.line_size);
if (proj_widget.figure == widget::ProjectileAim::Figure::CIRCLE)
{
m_renderer->add_filled_circle(proj_widget.position, proj_widget.size, proj_widget.color);
return;
}
if (proj_widget.figure == widget::ProjectileAim::Figure::SQUARE)
{
const auto box_min = proj_widget.position - Vector2{proj_widget.size, proj_widget.size} / 2.f;
const auto box_max = proj_widget.position + Vector2{proj_widget.size, proj_widget.size} / 2.f;
m_renderer->add_filled_rectangle(box_min, box_max, proj_widget.color);
return;
}
std::unreachable();
}
void EntityOverlay::draw_progress_ring(const Vector2<float>& center, const widget::ProgressRing& ring)
{
constexpr auto pi = std::numbers::pi_v<float>;
const float ratio = std::clamp(ring.ratio, 0.f, 1.f);
m_renderer->add_circle(center, ring.radius, ring.bg, ring.thickness, ring.segments);
if (ratio > 0.f)
{
const float a_min = -pi / 2.f;
const float a_max = a_min + ratio * 2.f * pi;
m_renderer->add_arc(center, ring.radius, a_min, a_max, ring.color, ring.thickness, ring.segments);
}
}
// ── Side container dispatch ───────────────────────────────────────────────
void EntityOverlay::dispatch(const widget::RightSide& right_side)
{
for (const auto& child : right_side.children)
std::visit(
widget::Overloaded{
[](const widget::None&)
{
},
[this](const widget::Bar& w)
{
add_right_bar(w.color, w.outline, w.bg, w.size, w.ratio, w.offset);
},
[this](const widget::DashedBar& w)
{
add_right_dashed_bar(w.color, w.outline, w.bg, w.size, w.ratio, w.dash_len, w.gap_len,
w.offset);
},
[this](const widget::Label& w)
{
add_right_label(w.color, w.offset, w.outlined, w.text);
},
[this](const widget::Centered<widget::Label>& w)
{
add_right_label(w.child.color, w.child.offset, w.child.outlined, w.child.text);
},
[this](const widget::SpaceVertical& w)
{
add_right_space_vertical(w.size);
},
[this](const widget::SpaceHorizontal& w)
{
add_right_space_horizontal(w.size);
},
[this](const widget::ProgressRing& w)
{
add_right_progress_ring(w.color, w.bg, w.radius, w.ratio, w.thickness, w.offset,
w.segments);
},
[this](const widget::Icon& w)
{
add_right_icon(w.texture_id, w.width, w.height, w.tint, w.offset);
},
},
child);
}
void EntityOverlay::dispatch(const widget::LeftSide& left_side)
{
for (const auto& child : left_side.children)
std::visit(
widget::Overloaded{
[](const widget::None&)
{
},
[this](const widget::Bar& w)
{
add_left_bar(w.color, w.outline, w.bg, w.size, w.ratio, w.offset);
},
[this](const widget::DashedBar& w)
{
add_left_dashed_bar(w.color, w.outline, w.bg, w.size, w.ratio, w.dash_len, w.gap_len,
w.offset);
},
[this](const widget::Label& w)
{
add_left_label(w.color, w.offset, w.outlined, w.text);
},
[this](const widget::Centered<widget::Label>& w)
{
add_left_label(w.child.color, w.child.offset, w.child.outlined, w.child.text);
},
[this](const widget::SpaceVertical& w)
{
add_left_space_vertical(w.size);
},
[this](const widget::SpaceHorizontal& w)
{
add_left_space_horizontal(w.size);
},
[this](const widget::ProgressRing& w)
{
add_left_progress_ring(w.color, w.bg, w.radius, w.ratio, w.thickness, w.offset,
w.segments);
},
[this](const widget::Icon& w)
{
add_left_icon(w.texture_id, w.width, w.height, w.tint, w.offset);
},
},
child);
}
void EntityOverlay::dispatch(const widget::TopSide& top_side)
{
for (const auto& child : top_side.children)
std::visit(
widget::Overloaded{
[](const widget::None&)
{
},
[this](const widget::Bar& w)
{
add_top_bar(w.color, w.outline, w.bg, w.size, w.ratio, w.offset);
},
[this](const widget::DashedBar& w)
{
add_top_dashed_bar(w.color, w.outline, w.bg, w.size, w.ratio, w.dash_len, w.gap_len,
w.offset);
},
[this](const widget::Label& w)
{
add_top_label(w.color, w.offset, w.outlined, w.text);
},
[this](const widget::Centered<widget::Label>& w)
{
add_centered_top_label(w.child.color, w.child.offset, w.child.outlined, w.child.text);
},
[this](const widget::SpaceVertical& w)
{
add_top_space_vertical(w.size);
},
[this](const widget::SpaceHorizontal& w)
{
add_top_space_horizontal(w.size);
},
[this](const widget::ProgressRing& w)
{
add_top_progress_ring(w.color, w.bg, w.radius, w.ratio, w.thickness, w.offset,
w.segments);
},
[this](const widget::Icon& w)
{
add_top_icon(w.texture_id, w.width, w.height, w.tint, w.offset);
},
},
child);
}
void EntityOverlay::dispatch(const widget::BottomSide& bottom_side)
{
for (const auto& child : bottom_side.children)
std::visit(
widget::Overloaded{
[](const widget::None&)
{
},
[this](const widget::Bar& w)
{
add_bottom_bar(w.color, w.outline, w.bg, w.size, w.ratio, w.offset);
},
[this](const widget::DashedBar& w)
{
add_bottom_dashed_bar(w.color, w.outline, w.bg, w.size, w.ratio, w.dash_len, w.gap_len,
w.offset);
},
[this](const widget::Label& w)
{
add_bottom_label(w.color, w.offset, w.outlined, w.text);
},
[this](const widget::Centered<widget::Label>& w)
{
add_centered_bottom_label(w.child.color, w.child.offset, w.child.outlined,
w.child.text);
},
[this](const widget::SpaceVertical& w)
{
add_bottom_space_vertical(w.size);
},
[this](const widget::SpaceHorizontal& w)
{
add_bottom_space_horizontal(w.size);
},
[this](const widget::ProgressRing& w)
{
add_bottom_progress_ring(w.color, w.bg, w.radius, w.ratio, w.thickness, w.offset,
w.segments);
},
[this](const widget::Icon& w)
{
add_bottom_icon(w.texture_id, w.width, w.height, w.tint, w.offset);
},
},
child);
}
} // namespace omath::hud

View File

@@ -0,0 +1,82 @@
//
// Created by orange on 13.03.2026.
//
#include "omath/hud/renderer_realizations/imgui_renderer.hpp"
#ifdef OMATH_IMGUI_INTEGRATION
#include <imgui.h>
namespace omath::hud
{
ImguiHudRenderer::~ImguiHudRenderer() = default;
void ImguiHudRenderer::add_line(const Vector2<float>& line_start, const Vector2<float>& line_end,
const Color& color, const float thickness)
{
ImGui::GetBackgroundDrawList()->AddLine(line_start.to_im_vec2(), line_end.to_im_vec2(), color.to_im_color(),
thickness);
}
void ImguiHudRenderer::add_polyline(const std::span<const Vector2<float>>& vertexes, const Color& color,
const float thickness)
{
ImGui::GetBackgroundDrawList()->AddPolyline(reinterpret_cast<const ImVec2*>(vertexes.data()),
static_cast<int>(vertexes.size()), color.to_im_color(),
ImDrawFlags_Closed, thickness);
}
void ImguiHudRenderer::add_filled_polyline(const std::span<const Vector2<float>>& vertexes, const Color& color)
{
ImGui::GetBackgroundDrawList()->AddConvexPolyFilled(reinterpret_cast<const ImVec2*>(vertexes.data()),
static_cast<int>(vertexes.size()), color.to_im_color());
}
void ImguiHudRenderer::add_rectangle(const Vector2<float>& min, const Vector2<float>& max, const Color& color)
{
ImGui::GetBackgroundDrawList()->AddRect(min.to_im_vec2(), max.to_im_vec2(), color.to_im_color());
}
void ImguiHudRenderer::add_filled_rectangle(const Vector2<float>& min, const Vector2<float>& max,
const Color& color)
{
ImGui::GetBackgroundDrawList()->AddRectFilled(min.to_im_vec2(), max.to_im_vec2(), color.to_im_color());
}
void ImguiHudRenderer::add_circle(const Vector2<float>& center, const float radius, const Color& color,
const float thickness, const int segments)
{
ImGui::GetBackgroundDrawList()->AddCircle(center.to_im_vec2(), radius, color.to_im_color(), segments, thickness);
}
void ImguiHudRenderer::add_filled_circle(const Vector2<float>& center, const float radius, const Color& color,
const int segments)
{
ImGui::GetBackgroundDrawList()->AddCircleFilled(center.to_im_vec2(), radius, color.to_im_color(), segments);
}
void ImguiHudRenderer::add_arc(const Vector2<float>& center, const float radius, const float a_min, const float a_max,
const Color& color, const float thickness, const int segments)
{
ImGui::GetBackgroundDrawList()->PathArcTo(center.to_im_vec2(), radius, a_min, a_max, segments);
ImGui::GetBackgroundDrawList()->PathStroke(color.to_im_color(), ImDrawFlags_None, thickness);
}
void ImguiHudRenderer::add_image(const std::any& texture_id, const Vector2<float>& min, const Vector2<float>& max,
const Color& tint)
{
ImGui::GetBackgroundDrawList()->AddImage(std::any_cast<ImTextureID>(texture_id), min.to_im_vec2(),
max.to_im_vec2(), {0, 0}, {1, 1}, tint.to_im_color());
}
void ImguiHudRenderer::add_text(const Vector2<float>& position, const Color& color, const std::string_view& text)
{
ImGui::GetBackgroundDrawList()->AddText(position.to_im_vec2(), color.to_im_color(), text.data(),
text.data() + text.size());
}
[[nodiscard]]
Vector2<float> ImguiHudRenderer::calc_text_size(const std::string_view& text)
{
return Vector2<float>::from_im_vec2(ImGui::CalcTextSize(text.data()));
}
} // namespace omath::hud
#endif // OMATH_IMGUI_INTEGRATION

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>
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
#endif

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

@@ -3,9 +3,9 @@
// //
#include "omath/pathfinding/navigation_mesh.hpp" #include "omath/pathfinding/navigation_mesh.hpp"
#include <algorithm> #include <algorithm>
#include <cstring> #include <sstream>
#include <limits>
#include <stdexcept> #include <stdexcept>
namespace omath::pathfinding namespace omath::pathfinding
{ {
std::expected<Vector3<float>, std::string> std::expected<Vector3<float>, std::string>
@@ -30,77 +30,72 @@ namespace omath::pathfinding
return m_vertex_map.empty(); return m_vertex_map.empty();
} }
std::vector<uint8_t> NavigationMesh::serialize() const noexcept void NavigationMesh::set_event(const Vector3<float>& vertex, const std::string_view& event_id)
{ {
std::vector<std::uint8_t> raw; if (!m_vertex_map.contains(vertex))
throw std::invalid_argument(std::format("Vertex '{}' not found", vertex));
// Pre-calculate total size for better performance m_vertex_events[vertex] = event_id;
std::size_t total_size = 0;
for (const auto& [vertex, neighbors] : m_vertex_map)
{
total_size += sizeof(vertex) + sizeof(std::uint16_t) + sizeof(Vector3<float>) * neighbors.size();
}
raw.reserve(total_size);
auto dump_to_vector = [&raw]<typename T>(const T& t)
{
const auto* byte_ptr = reinterpret_cast<const std::uint8_t*>(&t);
raw.insert(raw.end(), byte_ptr, byte_ptr + sizeof(T));
};
for (const auto& [vertex, neighbors] : m_vertex_map)
{
// Clamp neighbors count to fit in uint16_t (prevents silent data corruption)
// NOTE: If neighbors.size() > 65535, only the first 65535 neighbors will be serialized.
// This is a limitation of the current serialization format using uint16_t for count.
const auto clamped_count =
std::min<std::size_t>(neighbors.size(), std::numeric_limits<std::uint16_t>::max());
const auto neighbors_count = static_cast<std::uint16_t>(clamped_count);
dump_to_vector(vertex);
dump_to_vector(neighbors_count);
// Only serialize up to the clamped count
for (std::size_t i = 0; i < clamped_count; ++i)
dump_to_vector(neighbors[i]);
}
return raw;
} }
void NavigationMesh::deserialize(const std::vector<uint8_t>& raw) noexcept void NavigationMesh::clear_event(const Vector3<float>& vertex)
{ {
auto load_from_vector = [](const std::vector<uint8_t>& vec, std::size_t& offset, auto& value) m_vertex_events.erase(vertex);
}
std::optional<std::string> NavigationMesh::get_event(const Vector3<float>& vertex) const noexcept
{
const auto it = m_vertex_events.find(vertex);
if (it == m_vertex_events.end())
return std::nullopt;
return it->second;
}
// Serialization format per vertex line:
// x y z neighbor_count event_id
// where event_id is "-" when no event is set.
// Neighbor lines follow: nx ny nz
std::string NavigationMesh::serialize() const noexcept
{
std::ostringstream oss;
for (const auto& [vertex, neighbors] : m_vertex_map)
{ {
if (offset + sizeof(value) > vec.size()) const auto event_it = m_vertex_events.find(vertex);
throw std::runtime_error("Deserialize: Invalid input data size."); const std::string& event = (event_it != m_vertex_events.end()) ? event_it->second : "-";
std::copy_n(vec.data() + offset, sizeof(value), reinterpret_cast<uint8_t*>(&value)); oss << vertex.x << ' ' << vertex.y << ' ' << vertex.z << ' ' << neighbors.size() << ' ' << event << '\n';
offset += sizeof(value);
};
for (const auto& n : neighbors)
oss << n.x << ' ' << n.y << ' ' << n.z << '\n';
}
return oss.str();
}
void NavigationMesh::deserialize(const std::string& raw)
{
m_vertex_map.clear(); m_vertex_map.clear();
m_vertex_events.clear();
std::istringstream iss(raw);
std::size_t offset = 0; Vector3<float> vertex;
std::size_t neighbors_count;
while (offset < raw.size()) std::string event;
while (iss >> vertex.x >> vertex.y >> vertex.z >> neighbors_count >> event)
{ {
Vector3<float> vertex;
load_from_vector(raw, offset, vertex);
std::uint16_t neighbors_count;
load_from_vector(raw, offset, neighbors_count);
std::vector<Vector3<float>> neighbors; std::vector<Vector3<float>> neighbors;
neighbors.reserve(neighbors_count); neighbors.reserve(neighbors_count);
for (std::size_t i = 0; i < neighbors_count; ++i) for (std::size_t i = 0; i < neighbors_count; ++i)
{ {
Vector3<float> neighbor; Vector3<float> n;
load_from_vector(raw, offset, neighbor); if (!(iss >> n.x >> n.y >> n.z))
neighbors.push_back(neighbor); throw std::runtime_error("Deserialize: Unexpected end of data.");
neighbors.push_back(n);
} }
m_vertex_map.emplace(vertex, std::move(neighbors)); m_vertex_map.emplace(vertex, std::move(neighbors));
if (event != "-")
m_vertex_events.emplace(vertex, std::move(event));
} }
} }
} // namespace omath::pathfinding } // namespace omath::pathfinding

View File

@@ -21,7 +21,7 @@ namespace omath::projectile_prediction
const float bullet_gravity = m_gravity_constant * projectile.m_gravity_scale; const float bullet_gravity = m_gravity_constant * projectile.m_gravity_scale;
const float v0 = projectile.m_launch_speed; const float v0 = projectile.m_launch_speed;
const float v0_sqr = v0 * v0; const float v0_sqr = v0 * v0;
const Vector3 proj_origin = projectile.m_origin; const Vector3 proj_origin = projectile.m_origin + projectile.m_launch_offset;
constexpr int SIMD_FACTOR = 8; constexpr int SIMD_FACTOR = 8;
float current_time = m_simulation_time_step; float current_time = m_simulation_time_step;
@@ -124,6 +124,110 @@ namespace omath::projectile_prediction
std::format("{} AVX2 feature is not enabled!", std::source_location::current().function_name())); std::format("{} AVX2 feature is not enabled!", std::source_location::current().function_name()));
#endif #endif
} }
std::optional<AimAngles>
ProjPredEngineAvx2::maybe_calculate_aim_angles([[maybe_unused]] const Projectile& projectile,
[[maybe_unused]] const Target& target) const
{
#if defined(OMATH_USE_AVX2) && defined(__i386__) && defined(__x86_64__)
const float bullet_gravity = m_gravity_constant * projectile.m_gravity_scale;
const float v0 = projectile.m_launch_speed;
const Vector3 proj_origin = projectile.m_origin + projectile.m_launch_offset;
constexpr int SIMD_FACTOR = 8;
float current_time = m_simulation_time_step;
for (; current_time <= m_maximum_simulation_time; current_time += m_simulation_time_step * SIMD_FACTOR)
{
const __m256 times
= _mm256_setr_ps(current_time, current_time + m_simulation_time_step,
current_time + m_simulation_time_step * 2, current_time + m_simulation_time_step * 3,
current_time + m_simulation_time_step * 4, current_time + m_simulation_time_step * 5,
current_time + m_simulation_time_step * 6, current_time + m_simulation_time_step * 7);
const __m256 target_x
= _mm256_fmadd_ps(_mm256_set1_ps(target.m_velocity.x), times, _mm256_set1_ps(target.m_origin.x));
const __m256 target_y
= _mm256_fmadd_ps(_mm256_set1_ps(target.m_velocity.y), times, _mm256_set1_ps(target.m_origin.y));
const __m256 times_sq = _mm256_mul_ps(times, times);
const __m256 target_z = _mm256_fmadd_ps(_mm256_set1_ps(target.m_velocity.z), times,
_mm256_fnmadd_ps(_mm256_set1_ps(0.5f * m_gravity_constant), times_sq,
_mm256_set1_ps(target.m_origin.z)));
const __m256 delta_x = _mm256_sub_ps(target_x, _mm256_set1_ps(proj_origin.x));
const __m256 delta_y = _mm256_sub_ps(target_y, _mm256_set1_ps(proj_origin.y));
const __m256 d_sqr = _mm256_add_ps(_mm256_mul_ps(delta_x, delta_x), _mm256_mul_ps(delta_y, delta_y));
const __m256 delta_z = _mm256_sub_ps(target_z, _mm256_set1_ps(proj_origin.z));
const __m256 bg_times_sq = _mm256_mul_ps(_mm256_set1_ps(bullet_gravity), times_sq);
const __m256 term = _mm256_add_ps(delta_z, _mm256_mul_ps(_mm256_set1_ps(0.5f), bg_times_sq));
const __m256 term_sq = _mm256_mul_ps(term, term);
const __m256 numerator = _mm256_add_ps(d_sqr, term_sq);
const __m256 denominator = _mm256_add_ps(times_sq, _mm256_set1_ps(1e-8f));
const __m256 required_v0_sqr = _mm256_div_ps(numerator, denominator);
const __m256 v0_sqr_vec = _mm256_set1_ps(v0 * v0 + 1e-3f);
const __m256 mask = _mm256_cmp_ps(required_v0_sqr, v0_sqr_vec, _CMP_LE_OQ);
const unsigned valid_mask = _mm256_movemask_ps(mask);
if (!valid_mask)
continue;
alignas(32) float valid_times[SIMD_FACTOR];
_mm256_store_ps(valid_times, times);
for (int i = 0; i < SIMD_FACTOR; ++i)
{
if (!(valid_mask & (1 << i)))
continue;
const float candidate_time = valid_times[i];
if (candidate_time > m_maximum_simulation_time)
continue;
for (float fine_time = candidate_time - m_simulation_time_step * 2;
fine_time <= candidate_time + m_simulation_time_step * 2; fine_time += m_simulation_time_step)
{
if (fine_time < 0)
continue;
Vector3 target_pos = target.m_origin + target.m_velocity * fine_time;
if (target.m_is_airborne)
target_pos.z -= 0.5f * m_gravity_constant * fine_time * fine_time;
const auto pitch = calculate_pitch(proj_origin, target_pos, bullet_gravity, v0, fine_time);
if (!pitch)
continue;
const Vector3 delta = target_pos - projectile.m_origin;
const float yaw = angles::radians_to_degrees(std::atan2(delta.y, delta.x));
return AimAngles{*pitch, yaw};
}
}
}
for (; current_time <= m_maximum_simulation_time; current_time += m_simulation_time_step)
{
Vector3 target_pos = target.m_origin + target.m_velocity * current_time;
if (target.m_is_airborne)
target_pos.z -= 0.5f * m_gravity_constant * current_time * current_time;
const auto pitch = calculate_pitch(proj_origin, target_pos, bullet_gravity, v0, current_time);
if (!pitch)
continue;
const Vector3 delta = target_pos - projectile.m_origin;
const float yaw = angles::radians_to_degrees(std::atan2(delta.y, delta.x));
return AimAngles{*pitch, yaw};
}
return std::nullopt;
#else
throw std::runtime_error(
std::format("{} AVX2 feature is not enabled!", std::source_location::current().function_name()));
#endif
}
ProjPredEngineAvx2::ProjPredEngineAvx2(const float gravity_constant, const float simulation_time_step, ProjPredEngineAvx2::ProjPredEngineAvx2(const float gravity_constant, const float simulation_time_step,
const float maximum_simulation_time) const float maximum_simulation_time)
: m_gravity_constant(gravity_constant), m_simulation_time_step(simulation_time_step), : m_gravity_constant(gravity_constant), m_simulation_time_step(simulation_time_step),

View File

@@ -5,6 +5,7 @@
#include <array> #include <array>
#include <fstream> #include <fstream>
#include <omath/utility/elf_pattern_scan.hpp> #include <omath/utility/elf_pattern_scan.hpp>
#include <span>
#include <utility> #include <utility>
#include <variant> #include <variant>
#include <vector> #include <vector>
@@ -140,6 +141,87 @@ namespace
std::uintptr_t raw_base_addr{}; std::uintptr_t raw_base_addr{};
std::vector<std::byte> data; std::vector<std::byte> data;
}; };
template<FileArch arch>
std::optional<ExtractedSection> get_elf_section_from_memory_impl(const std::span<const std::byte> data,
const std::string_view& section_name)
{
using FH = typename ElfHeaders<arch>::FileHeader;
using SH = typename ElfHeaders<arch>::SectionHeader;
if (data.size() < sizeof(FH))
return std::nullopt;
const auto* file_header = reinterpret_cast<const FH*>(data.data());
const auto shoff = static_cast<std::size_t>(file_header->e_shoff);
const auto shnum = static_cast<std::size_t>(file_header->e_shnum);
const auto shstrndx = static_cast<std::size_t>(file_header->e_shstrndx);
const auto shstrtab_hdr_off = shoff + shstrndx * sizeof(SH);
if (shstrtab_hdr_off + sizeof(SH) > data.size())
return std::nullopt;
const auto* shstrtab_hdr = reinterpret_cast<const SH*>(data.data() + shstrtab_hdr_off);
const auto shstrtab_off = static_cast<std::size_t>(shstrtab_hdr->sh_offset);
const auto shstrtab_size = static_cast<std::size_t>(shstrtab_hdr->sh_size);
if (shstrtab_off + shstrtab_size > data.size())
return std::nullopt;
const auto* shstrtab = reinterpret_cast<const char*>(data.data() + shstrtab_off);
for (std::size_t i = 0; i < shnum; ++i)
{
const auto sect_hdr_off = shoff + i * sizeof(SH);
if (sect_hdr_off + sizeof(SH) > data.size())
continue;
const auto* section = reinterpret_cast<const SH*>(data.data() + sect_hdr_off);
if (std::cmp_greater_equal(section->sh_name, shstrtab_size))
continue;
if (std::string_view{shstrtab + section->sh_name} != section_name)
continue;
const auto raw_off = static_cast<std::size_t>(section->sh_offset);
const auto sec_size = static_cast<std::size_t>(section->sh_size);
if (raw_off + sec_size > data.size())
return std::nullopt;
ExtractedSection out;
out.virtual_base_addr = static_cast<std::uintptr_t>(section->sh_addr);
out.raw_base_addr = raw_off;
out.data.assign(data.data() + raw_off, data.data() + raw_off + sec_size);
return out;
}
return std::nullopt;
}
std::optional<ExtractedSection> get_elf_section_by_name_from_memory(const std::span<const std::byte> data,
const std::string_view& section_name)
{
constexpr std::string_view valid_elf_signature = "\x7F"
"ELF";
if (data.size() < ei_nident)
return std::nullopt;
if (std::string_view{reinterpret_cast<const char*>(data.data()), valid_elf_signature.size()}
!= valid_elf_signature)
return std::nullopt;
const auto class_byte = static_cast<uint8_t>(data[ei_class]);
if (class_byte == elfclass64)
return get_elf_section_from_memory_impl<FileArch::x64>(data, section_name);
if (class_byte == elfclass32)
return get_elf_section_from_memory_impl<FileArch::x32>(data, section_name);
return std::nullopt;
}
[[maybe_unused]] [[maybe_unused]]
std::optional<ExtractedSection> get_elf_section_by_name(const std::filesystem::path& path, std::optional<ExtractedSection> get_elf_section_by_name(const std::filesystem::path& path,
const std::string_view& section_name) const std::string_view& section_name)
@@ -322,4 +404,27 @@ namespace omath
.raw_base_addr = pe_section->raw_base_addr, .raw_base_addr = pe_section->raw_base_addr,
.target_offset = offset}; .target_offset = offset};
} }
std::optional<SectionScanResult>
ElfPatternScanner::scan_for_pattern_in_memory_file(const std::span<const std::byte> file_data,
const std::string_view& pattern,
const std::string_view& target_section_name)
{
const auto section = get_elf_section_by_name_from_memory(file_data, target_section_name);
if (!section.has_value()) [[unlikely]]
return std::nullopt;
const auto scan_result =
PatternScanner::scan_for_pattern(section->data.cbegin(), section->data.cend(), pattern);
if (scan_result == section->data.cend())
return std::nullopt;
const auto offset = std::distance(section->data.begin(), scan_result);
return SectionScanResult{.virtual_base_addr = section->virtual_base_addr,
.raw_base_addr = section->raw_base_addr,
.target_offset = offset};
}
} // namespace omath } // namespace omath

View File

@@ -5,6 +5,7 @@
#include "omath/utility/pattern_scan.hpp" #include "omath/utility/pattern_scan.hpp"
#include <cstring> #include <cstring>
#include <fstream> #include <fstream>
#include <span>
#include <variant> #include <variant>
#include <vector> #include <vector>
@@ -231,6 +232,96 @@ namespace
return std::nullopt; return std::nullopt;
} }
template<typename HeaderType, typename SegmentType, typename SectionType, std::uint32_t segment_cmd>
std::optional<ExtractedSection> extract_section_from_memory_impl(const std::span<const std::byte> data,
const std::string_view& section_name)
{
if (data.size() < sizeof(HeaderType))
return std::nullopt;
const auto* header = reinterpret_cast<const HeaderType*>(data.data());
std::size_t cmd_offset = sizeof(HeaderType);
for (std::uint32_t i = 0; i < header->ncmds; ++i)
{
if (cmd_offset + sizeof(LoadCommand) > data.size())
return std::nullopt;
const auto* lc = reinterpret_cast<const LoadCommand*>(data.data() + cmd_offset);
if (lc->cmd != segment_cmd)
{
cmd_offset += lc->cmdsize;
continue;
}
if (cmd_offset + sizeof(SegmentType) > data.size())
return std::nullopt;
const auto* segment = reinterpret_cast<const SegmentType*>(data.data() + cmd_offset);
if (!segment->nsects)
{
cmd_offset += lc->cmdsize;
continue;
}
std::size_t sect_offset = cmd_offset + sizeof(SegmentType);
for (std::uint32_t j = 0; j < segment->nsects; ++j)
{
if (sect_offset + sizeof(SectionType) > data.size())
return std::nullopt;
const auto* section = reinterpret_cast<const SectionType*>(data.data() + sect_offset);
if (get_section_name(section->sectname) != section_name)
{
sect_offset += sizeof(SectionType);
continue;
}
const auto raw_off = static_cast<std::size_t>(section->offset);
const auto sec_size = static_cast<std::size_t>(section->size);
if (raw_off + sec_size > data.size())
return std::nullopt;
ExtractedSection out;
out.virtual_base_addr = static_cast<std::uintptr_t>(section->addr);
out.raw_base_addr = raw_off;
out.data.assign(data.data() + raw_off, data.data() + raw_off + sec_size);
return out;
}
cmd_offset += lc->cmdsize;
}
return std::nullopt;
}
[[nodiscard]]
std::optional<ExtractedSection> get_macho_section_by_name_from_memory(const std::span<const std::byte> data,
const std::string_view& section_name)
{
if (data.size() < sizeof(std::uint32_t))
return std::nullopt;
std::uint32_t magic{};
std::memcpy(&magic, data.data(), sizeof(magic));
if (magic == mh_magic_64 || magic == mh_cigam_64)
return extract_section_from_memory_impl<MachHeader64, SegmentCommand64, Section64, lc_segment_64>(
data, section_name);
if (magic == mh_magic_32 || magic == mh_cigam_32)
return extract_section_from_memory_impl<MachHeader32, SegmentCommand32, Section32, lc_segment>(data,
section_name);
return std::nullopt;
}
[[nodiscard]] [[nodiscard]]
std::optional<ExtractedSection> get_macho_section_by_name(const std::filesystem::path& path, std::optional<ExtractedSection> get_macho_section_by_name(const std::filesystem::path& path,
const std::string_view& section_name) const std::string_view& section_name)
@@ -346,4 +437,27 @@ namespace omath
.raw_base_addr = macho_section->raw_base_addr, .raw_base_addr = macho_section->raw_base_addr,
.target_offset = offset}; .target_offset = offset};
} }
std::optional<SectionScanResult>
MachOPatternScanner::scan_for_pattern_in_memory_file(const std::span<const std::byte> file_data,
const std::string_view& pattern,
const std::string_view& target_section_name)
{
const auto section = get_macho_section_by_name_from_memory(file_data, target_section_name);
if (!section.has_value()) [[unlikely]]
return std::nullopt;
const auto scan_result =
PatternScanner::scan_for_pattern(section->data.cbegin(), section->data.cend(), pattern);
if (scan_result == section->data.cend())
return std::nullopt;
const auto offset = std::distance(section->data.begin(), scan_result);
return SectionScanResult{.virtual_base_addr = section->virtual_base_addr,
.raw_base_addr = section->raw_base_addr,
.target_offset = offset};
}
} // namespace omath } // namespace omath

View File

@@ -7,6 +7,7 @@
#include <span> #include <span>
#include <stdexcept> #include <stdexcept>
#include <variant> #include <variant>
#include <vector>
// Internal PE shit defines // Internal PE shit defines
// Big thx for linuxpe sources as ref // Big thx for linuxpe sources as ref
@@ -244,6 +245,78 @@ namespace
std::vector<std::byte> data; std::vector<std::byte> data;
}; };
[[nodiscard]]
std::optional<ExtractedSection> extract_section_from_pe_memory(const std::span<const std::byte> data,
const std::string_view& section_name)
{
if (data.size() < sizeof(DosHeader))
return std::nullopt;
const auto* dos_header = reinterpret_cast<const DosHeader*>(data.data());
if (invalid_dos_header_file(*dos_header))
return std::nullopt;
const auto nt_off = static_cast<std::size_t>(dos_header->e_lfanew);
if (nt_off + sizeof(ImageNtHeaders<NtArchitecture::x32_bit>) > data.size())
return std::nullopt;
const auto* x86_hdrs =
reinterpret_cast<const ImageNtHeaders<NtArchitecture::x32_bit>*>(data.data() + nt_off);
NtHeaderVariant nt_headers;
if (x86_hdrs->optional_header.magic == opt_hdr32_magic)
nt_headers = *x86_hdrs;
else if (x86_hdrs->optional_header.magic == opt_hdr64_magic)
{
if (nt_off + sizeof(ImageNtHeaders<NtArchitecture::x64_bit>) > data.size())
return std::nullopt;
nt_headers = *reinterpret_cast<const ImageNtHeaders<NtArchitecture::x64_bit>*>(data.data() + nt_off);
}
else
return std::nullopt;
if (invalid_nt_header_file(nt_headers))
return std::nullopt;
return std::visit(
[&data, &section_name, nt_off](const auto& concrete_headers) -> std::optional<ExtractedSection>
{
constexpr std::size_t sig_size = sizeof(concrete_headers.signature);
const auto section_table_off = nt_off + sig_size + sizeof(FileHeader)
+ concrete_headers.file_header.size_optional_header;
for (std::size_t i = 0; i < concrete_headers.file_header.num_sections; ++i)
{
const auto sh_off = section_table_off + i * sizeof(SectionHeader);
if (sh_off + sizeof(SectionHeader) > data.size())
return std::nullopt;
const auto* section = reinterpret_cast<const SectionHeader*>(data.data() + sh_off);
if (std::string_view(section->name) != section_name)
continue;
const auto raw_off = static_cast<std::size_t>(section->ptr_raw_data);
const auto raw_size = static_cast<std::size_t>(section->size_raw_data);
if (raw_off + raw_size > data.size())
return std::nullopt;
std::vector<std::byte> section_data(data.data() + raw_off, data.data() + raw_off + raw_size);
return ExtractedSection{
.virtual_base_addr = static_cast<std::uintptr_t>(
section->virtual_address + concrete_headers.optional_header.image_base),
.raw_base_addr = raw_off,
.data = std::move(section_data)};
}
return std::nullopt;
},
nt_headers);
}
[[nodiscard]] [[nodiscard]]
std::optional<ExtractedSection> extract_section_from_pe_file(const std::filesystem::path& path_to_file, std::optional<ExtractedSection> extract_section_from_pe_file(const std::filesystem::path& path_to_file,
const std::string_view& section_name) const std::string_view& section_name)
@@ -383,4 +456,27 @@ namespace omath
.raw_base_addr = pe_section->raw_base_addr, .raw_base_addr = pe_section->raw_base_addr,
.target_offset = offset}; .target_offset = offset};
} }
std::optional<SectionScanResult>
PePatternScanner::scan_for_pattern_in_memory_file(const std::span<const std::byte> file_data,
const std::string_view& pattern,
const std::string_view& target_section_name)
{
const auto pe_section = extract_section_from_pe_memory(file_data, target_section_name);
if (!pe_section.has_value()) [[unlikely]]
return std::nullopt;
const auto scan_result =
PatternScanner::scan_for_pattern(pe_section->data.cbegin(), pe_section->data.cend(), pattern);
if (scan_result == pe_section->data.cend())
return std::nullopt;
const auto offset = std::distance(pe_section->data.begin(), scan_result);
return SectionScanResult{.virtual_base_addr = pe_section->virtual_base_addr,
.raw_base_addr = pe_section->raw_base_addr,
.target_offset = offset};
}
} // namespace omath } // namespace omath

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" "${CMAKE_CURRENT_SOURCE_DIR}/*.hpp")
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

@@ -0,0 +1,240 @@
//
// Created by Vladislav on 19.02.2026.
//
#include <gtest/gtest.h>
#include <omath/engines/cry_engine/camera.hpp>
#include <omath/engines/cry_engine/constants.hpp>
#include <omath/engines/cry_engine/formulas.hpp>
#include <random>
#include <ranges>
using namespace omath;
TEST(unit_test_cry_engine, look_at_forward)
{
const auto angles = cry_engine::CameraTrait::calc_look_at_angle({}, cry_engine::k_abs_forward);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = cry_engine::forward_vector(angles);
for (const auto& [result, etalon] : std::views::zip(dir_vector.as_array(), cry_engine::k_abs_forward.as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_cry_engine, look_at_right)
{
const auto angles = cry_engine::CameraTrait::calc_look_at_angle({}, cry_engine::k_abs_right);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = cry_engine::forward_vector(angles);
for (const auto& [result, etalon] : std::views::zip(dir_vector.as_array(), cry_engine::k_abs_right.as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_cry_engine, look_at_up)
{
const auto angles = cry_engine::CameraTrait::calc_look_at_angle({}, cry_engine::k_abs_right);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = cry_engine::forward_vector(angles);
for (const auto& [result, etalon] : std::views::zip(dir_vector.as_array(), cry_engine::k_abs_right.as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_cry_engine, look_at_back)
{
const auto angles = cry_engine::CameraTrait::calc_look_at_angle({}, -cry_engine::k_abs_forward);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = cry_engine::forward_vector(angles);
for (const auto& [result, etalon] : std::views::zip(dir_vector.as_array(), (-cry_engine::k_abs_forward).as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_cry_engine, look_at_left)
{
const auto angles = cry_engine::CameraTrait::calc_look_at_angle({}, -cry_engine::k_abs_right);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = cry_engine::forward_vector(angles);
for (const auto& [result, etalon] : std::views::zip(dir_vector.as_array(), (-cry_engine::k_abs_right).as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_cry_engine, look_at_down)
{
const auto angles = cry_engine::CameraTrait::calc_look_at_angle({}, -cry_engine::k_abs_up);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = cry_engine::forward_vector(angles);
for (const auto& [result, etalon] : std::views::zip(dir_vector.as_array(), (-cry_engine::k_abs_up).as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_cry_engine, RightVector)
{
const auto right = omath::cry_engine::right_vector({});
EXPECT_EQ(right, omath::cry_engine::k_abs_right);
}
TEST(unit_test_cry_engine, UpVector)
{
const auto up = omath::cry_engine::up_vector({});
EXPECT_EQ(up, omath::cry_engine::k_abs_up);
}
TEST(unit_test_cry_engine, ProjectTargetMovedFromCamera)
{
constexpr auto fov = omath::projection::FieldOfView::from_degrees(60.f);
const auto cam = omath::cry_engine::Camera({0, 0, 0}, {}, {1280.f, 720.f}, fov, 0.01f, 1000.f);
for (float distance = 0.02f; distance < 100.f; distance += 0.01f)
{
const auto projected = cam.world_to_screen({0, distance, 0});
EXPECT_TRUE(projected.has_value());
if (!projected.has_value())
continue;
EXPECT_NEAR(projected->x, 640, 0.00001f);
EXPECT_NEAR(projected->y, 360, 0.00001f);
}
}
TEST(unit_test_cry_engine, CameraSetAndGetFov)
{
constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f);
auto cam = omath::cry_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.01f, 1000.f);
EXPECT_EQ(cam.get_field_of_view().as_degrees(), 90.f);
cam.set_field_of_view(omath::projection::FieldOfView::from_degrees(50.f));
EXPECT_EQ(cam.get_field_of_view().as_degrees(), 50.f);
}
TEST(unit_test_cry_engine, CameraSetAndGetOrigin)
{
auto cam = omath::cry_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, {}, 0.01f, 1000.f);
EXPECT_EQ(cam.get_origin(), omath::Vector3<float>{});
cam.set_field_of_view(omath::projection::FieldOfView::from_degrees(50.f));
EXPECT_EQ(cam.get_field_of_view().as_degrees(), 50.f);
}
TEST(unit_test_cry_engine, loook_at_random_all_axis)
{
std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source
std::uniform_real_distribution<float> dist(-1000.f, 1000.f);
constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f);
auto cam = omath::cry_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.001f, 10000.f);
std::size_t failed_points = 0;
for (int i = 0; i < 1000; i++)
{
const auto position_to_look = omath::Vector3<float>{dist(gen), dist(gen), dist(gen)};
if (cam.get_origin().distance_to(position_to_look) < 10)
continue;
cam.look_at(position_to_look);
auto projected_pos = cam.world_to_view_port(position_to_look);
EXPECT_TRUE(projected_pos.has_value());
if (!projected_pos)
continue;
if (std::abs(projected_pos->x - 0.f) >= 0.0001f || std::abs(projected_pos->y - 0.f) >= 0.0001f)
failed_points++;
}
EXPECT_LE(failed_points, 100);
}
TEST(unit_test_cry_engine, loook_at_random_x_axis)
{
std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source
std::uniform_real_distribution<float> dist(-1000.f, 1000.f);
constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f);
auto cam = omath::cry_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.001f, 10000.f);
std::size_t failed_points = 0;
for (int i = 0; i < 1000; i++)
{
const auto position_to_look = omath::Vector3<float>{dist(gen), 0.f, 0.f};
if (cam.get_origin().distance_to(position_to_look) < 10)
continue;
cam.look_at(position_to_look);
auto projected_pos = cam.world_to_view_port(position_to_look);
EXPECT_TRUE(projected_pos.has_value());
if (!projected_pos)
continue;
if (std::abs(projected_pos->x - 0.f) >= 0.001f || std::abs(projected_pos->y - 0.f) >= 0.001f)
failed_points++;
}
EXPECT_LE(failed_points, 100);
}
TEST(unit_test_cry_engine, loook_at_random_y_axis)
{
std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source
std::uniform_real_distribution<float> dist(-1000.f, 1000.f);
constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f);
auto cam = omath::cry_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.001f, 10000.f);
std::size_t failed_points = 0;
for (int i = 0; i < 1000; i++)
{
const auto position_to_look = omath::Vector3<float>{0.f, dist(gen), 0.f};
if (cam.get_origin().distance_to(position_to_look) < 10)
continue;
cam.look_at(position_to_look);
auto projected_pos = cam.world_to_view_port(position_to_look);
EXPECT_TRUE(projected_pos.has_value());
if (!projected_pos)
continue;
if (std::abs(projected_pos->x - 0.f) >= 0.01f || std::abs(projected_pos->y - 0.f) >= 0.01f)
failed_points++;
}
EXPECT_LE(failed_points, 100);
}
TEST(unit_test_cry_engine, loook_at_random_z_axis)
{
std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source
std::uniform_real_distribution<float> dist(-1000.f, 1000.f);
constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f);
auto cam = omath::cry_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.001f, 10000.f);
std::size_t failed_points = 0;
for (int i = 0; i < 1000; i++)
{
const auto position_to_look = omath::Vector3<float>{0.f, 0.f, dist(gen)};
if (cam.get_origin().distance_to(position_to_look) < 10)
continue;
cam.look_at(position_to_look);
auto projected_pos = cam.world_to_view_port(position_to_look);
EXPECT_TRUE(projected_pos.has_value());
if (!projected_pos)
continue;
if (std::abs(projected_pos->x - 0.f) >= 0.01f || std::abs(projected_pos->y - 0.f) >= 0.01f)
failed_points++;
}
EXPECT_LE(failed_points, 100);
}

View File

@@ -7,6 +7,7 @@
#include <omath/engines/frostbite_engine/formulas.hpp> #include <omath/engines/frostbite_engine/formulas.hpp>
#include <print> #include <print>
#include <random> #include <random>
#include <ranges>
TEST(unit_test_frostbite_engine, UnitsToCentimeters_BasicValues) TEST(unit_test_frostbite_engine, UnitsToCentimeters_BasicValues)
{ {
@@ -352,4 +353,55 @@ TEST(unit_test_frostbite_engine, loook_at_random_z_axis)
failed_points++; failed_points++;
} }
EXPECT_LE(failed_points, 100); EXPECT_LE(failed_points, 100);
} }
TEST(unit_test_frostbite_engine, look_at_right)
{
const auto angles = omath::frostbite_engine::CameraTrait::calc_look_at_angle({}, omath::frostbite_engine::k_abs_right);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::frostbite_engine::forward_vector(angles);
for (const auto& [result, etalon] :
std::views::zip(dir_vector.as_array(), omath::frostbite_engine::k_abs_right.as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_frostbite_engine, look_at_up)
{
const auto angles = omath::frostbite_engine::CameraTrait::calc_look_at_angle({}, omath::frostbite_engine::k_abs_right);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::frostbite_engine::forward_vector(angles);
for (const auto& [result, etalon] :
std::views::zip(dir_vector.as_array(), omath::frostbite_engine::k_abs_right.as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_frostbite_engine, look_at_back)
{
const auto angles = omath::frostbite_engine::CameraTrait::calc_look_at_angle({}, -omath::frostbite_engine::k_abs_forward);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::frostbite_engine::forward_vector(angles);
for (const auto& [result, etalon] :
std::views::zip(dir_vector.as_array(), (-omath::frostbite_engine::k_abs_forward).as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_frostbite_engine, look_at_left)
{
const auto angles = omath::frostbite_engine::CameraTrait::calc_look_at_angle({}, -omath::frostbite_engine::k_abs_right);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::frostbite_engine::forward_vector(angles);
for (const auto& [result, etalon] :
std::views::zip(dir_vector.as_array(), (-omath::frostbite_engine::k_abs_right).as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_frostbite_engine, look_at_down)
{
const auto angles = omath::frostbite_engine::CameraTrait::calc_look_at_angle({}, -omath::frostbite_engine::k_abs_up);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::frostbite_engine::forward_vector(angles);
for (const auto& [result, etalon] :
std::views::zip(dir_vector.as_array(), (-omath::frostbite_engine::k_abs_up).as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}

View File

@@ -6,6 +6,7 @@
#include <omath/engines/iw_engine/constants.hpp> #include <omath/engines/iw_engine/constants.hpp>
#include <omath/engines/iw_engine/formulas.hpp> #include <omath/engines/iw_engine/formulas.hpp>
#include <random> #include <random>
#include <ranges>
TEST(unit_test_iw_engine, ForwardVector) TEST(unit_test_iw_engine, ForwardVector)
{ {
@@ -223,4 +224,60 @@ TEST(unit_test_iw_engine, loook_at_random_z_axis)
failed_points++; failed_points++;
} }
EXPECT_LE(failed_points, 100); EXPECT_LE(failed_points, 100);
}
TEST(unit_test_iw_engine, look_at_forward)
{
const auto angles = omath::iw_engine::CameraTrait::calc_look_at_angle({}, omath::iw_engine::k_abs_forward);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::iw_engine::forward_vector(angles);
for (const auto& [result, etalon] : std::views::zip(dir_vector.as_array(), omath::iw_engine::k_abs_forward.as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_iw_engine, look_at_right)
{
const auto angles = omath::iw_engine::CameraTrait::calc_look_at_angle({}, omath::iw_engine::k_abs_right);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::iw_engine::forward_vector(angles);
for (const auto& [result, etalon] : std::views::zip(dir_vector.as_array(), omath::iw_engine::k_abs_right.as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_iw_engine, look_at_up)
{
const auto angles = omath::iw_engine::CameraTrait::calc_look_at_angle({}, omath::iw_engine::k_abs_right);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::iw_engine::forward_vector(angles);
for (const auto& [result, etalon] : std::views::zip(dir_vector.as_array(), omath::iw_engine::k_abs_right.as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_iw_engine, look_at_back)
{
const auto angles = omath::iw_engine::CameraTrait::calc_look_at_angle({}, -omath::iw_engine::k_abs_forward);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::iw_engine::forward_vector(angles);
for (const auto& [result, etalon] : std::views::zip(dir_vector.as_array(), (-omath::iw_engine::k_abs_forward).as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_iw_engine, look_at_left)
{
const auto angles = omath::iw_engine::CameraTrait::calc_look_at_angle({}, -omath::iw_engine::k_abs_right);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::iw_engine::forward_vector(angles);
for (const auto& [result, etalon] : std::views::zip(dir_vector.as_array(), (-omath::iw_engine::k_abs_right).as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_iw_engine, look_at_down)
{
const auto angles = omath::iw_engine::CameraTrait::calc_look_at_angle({}, -omath::iw_engine::k_abs_up);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::iw_engine::forward_vector(angles);
EXPECT_NEAR(dir_vector.z, -0.99984f, 0.0001f);
EXPECT_NEAR(dir_vector.x,- 0.017f, 0.01f);
EXPECT_NEAR(dir_vector.y, 0.f, 0.001f);
} }

View File

@@ -6,6 +6,7 @@
#include <omath/engines/opengl_engine/constants.hpp> #include <omath/engines/opengl_engine/constants.hpp>
#include <omath/engines/opengl_engine/formulas.hpp> #include <omath/engines/opengl_engine/formulas.hpp>
#include <random> #include <random>
#include <ranges>
TEST(unit_test_opengl, UnitsToCentimeters_BasicValues) TEST(unit_test_opengl, UnitsToCentimeters_BasicValues)
{ {
@@ -337,4 +338,60 @@ TEST(unit_test_opengl_engine, loook_at_random_z_axis)
failed_points++; failed_points++;
} }
EXPECT_LE(failed_points, 100); EXPECT_LE(failed_points, 100);
}
TEST(unit_test_opengl_engine, look_at_forward)
{
const auto angles = omath::opengl_engine::CameraTrait::calc_look_at_angle({}, omath::opengl_engine::k_abs_forward);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::opengl_engine::forward_vector(angles);
for (const auto& [result, etalon] : std::views::zip(dir_vector.as_array(), omath::opengl_engine::k_abs_forward.as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_opengl_engine, look_at_right)
{
const auto angles = omath::opengl_engine::CameraTrait::calc_look_at_angle({}, omath::opengl_engine::k_abs_right);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::opengl_engine::forward_vector(angles);
for (const auto& [result, etalon] : std::views::zip(dir_vector.as_array(), omath::opengl_engine::k_abs_right.as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_opengl_engine, look_at_up)
{
const auto angles = omath::opengl_engine::CameraTrait::calc_look_at_angle({}, omath::opengl_engine::k_abs_right);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::opengl_engine::forward_vector(angles);
for (const auto& [result, etalon] : std::views::zip(dir_vector.as_array(), omath::opengl_engine::k_abs_right.as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_opengl_engine, look_at_back)
{
const auto angles = omath::opengl_engine::CameraTrait::calc_look_at_angle({}, -omath::opengl_engine::k_abs_forward);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::opengl_engine::forward_vector(angles);
for (const auto& [result, etalon] : std::views::zip(dir_vector.as_array(), (-omath::opengl_engine::k_abs_forward).as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_opengl_engine, look_at_left)
{
const auto angles = omath::opengl_engine::CameraTrait::calc_look_at_angle({}, -omath::opengl_engine::k_abs_right);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::opengl_engine::forward_vector(angles);
for (const auto& [result, etalon] : std::views::zip(dir_vector.as_array(), (-omath::opengl_engine::k_abs_right).as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_opengl_engine, look_at_down)
{
const auto angles = omath::opengl_engine::CameraTrait::calc_look_at_angle({}, -omath::opengl_engine::k_abs_up);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::opengl_engine::forward_vector(angles);
for (const auto& [result, etalon] : std::views::zip(dir_vector.as_array(), (-omath::opengl_engine::k_abs_up).as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
} }

View File

@@ -6,6 +6,7 @@
#include <omath/engines/source_engine/constants.hpp> #include <omath/engines/source_engine/constants.hpp>
#include <omath/engines/source_engine/formulas.hpp> #include <omath/engines/source_engine/formulas.hpp>
#include <random> #include <random>
#include <ranges>
TEST(unit_test_source_engine_units, HammerUnitsToCentimeters_BasicValues) TEST(unit_test_source_engine_units, HammerUnitsToCentimeters_BasicValues)
{ {
@@ -365,4 +366,60 @@ TEST(unit_test_source_engine, loook_at_random_z_axis)
failed_points++; failed_points++;
} }
EXPECT_LE(failed_points, 100); EXPECT_LE(failed_points, 100);
}
TEST(unit_test_source_engine, look_at_forward)
{
const auto angles = omath::source_engine::CameraTrait::calc_look_at_angle({}, omath::source_engine::k_abs_forward);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::source_engine::forward_vector(angles);
for (const auto& [result, etalon] : std::views::zip(dir_vector.as_array(), omath::source_engine::k_abs_forward.as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_source_engine, look_at_right)
{
const auto angles = omath::source_engine::CameraTrait::calc_look_at_angle({}, omath::source_engine::k_abs_right);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::source_engine::forward_vector(angles);
for (const auto& [result, etalon] : std::views::zip(dir_vector.as_array(), omath::source_engine::k_abs_right.as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_source_engine, look_at_up)
{
const auto angles = omath::source_engine::CameraTrait::calc_look_at_angle({}, omath::source_engine::k_abs_right);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::source_engine::forward_vector(angles);
for (const auto& [result, etalon] : std::views::zip(dir_vector.as_array(), omath::source_engine::k_abs_right.as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_source_engine, look_at_back)
{
const auto angles = omath::source_engine::CameraTrait::calc_look_at_angle({}, -omath::source_engine::k_abs_forward);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::source_engine::forward_vector(angles);
for (const auto& [result, etalon] : std::views::zip(dir_vector.as_array(), (-omath::source_engine::k_abs_forward).as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_source_engine, look_at_left)
{
const auto angles = omath::source_engine::CameraTrait::calc_look_at_angle({}, -omath::source_engine::k_abs_right);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::source_engine::forward_vector(angles);
for (const auto& [result, etalon] : std::views::zip(dir_vector.as_array(), (-omath::source_engine::k_abs_right).as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_source_engine, look_at_down)
{
const auto angles = omath::source_engine::CameraTrait::calc_look_at_angle({}, -omath::source_engine::k_abs_up);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::source_engine::forward_vector(angles);
EXPECT_NEAR(dir_vector.z, -0.99984f, 0.0001f);
EXPECT_NEAR(dir_vector.x,- 0.017f, 0.01f);
EXPECT_NEAR(dir_vector.y, 0.f, 0.001f);
} }

View File

@@ -20,6 +20,8 @@
#include <omath/engines/unreal_engine/traits/mesh_trait.hpp> #include <omath/engines/unreal_engine/traits/mesh_trait.hpp>
#include <omath/engines/unreal_engine/traits/camera_trait.hpp> #include <omath/engines/unreal_engine/traits/camera_trait.hpp>
#include <omath/engines/source_engine/traits/pred_engine_trait.hpp>
#include <omath/projectile_prediction/projectile.hpp> #include <omath/projectile_prediction/projectile.hpp>
#include <omath/projectile_prediction/target.hpp> #include <omath/projectile_prediction/target.hpp>
#include <optional> #include <optional>
@@ -35,6 +37,132 @@ static void expect_matrix_near(const MatT& a, const MatT& b, float eps = 1e-5f)
EXPECT_NEAR(a.at(r, c), b.at(r, c), eps); EXPECT_NEAR(a.at(r, c), b.at(r, c), eps);
} }
// ── Launch offset tests for all engines ──────────────────────────────────────
#include <omath/engines/cry_engine/traits/pred_engine_trait.hpp>
// Helper: verify that zero offset matches default-initialized offset behavior
template<typename Trait>
static void verify_launch_offset_at_time_zero(const Vector3<float>& origin, const Vector3<float>& offset)
{
projectile_prediction::Projectile p;
p.m_origin = origin;
p.m_launch_offset = offset;
p.m_launch_speed = 100.f;
p.m_gravity_scale = 1.f;
const auto pos = Trait::predict_projectile_position(p, 0.f, 0.f, 0.f, 9.81f);
const auto expected = origin + offset;
EXPECT_NEAR(pos.x, expected.x, 1e-4f);
EXPECT_NEAR(pos.y, expected.y, 1e-4f);
EXPECT_NEAR(pos.z, expected.z, 1e-4f);
}
template<typename Trait>
static void verify_zero_offset_matches_default()
{
projectile_prediction::Projectile p;
p.m_origin = {10.f, 20.f, 30.f};
p.m_launch_offset = {0.f, 0.f, 0.f};
p.m_launch_speed = 50.f;
p.m_gravity_scale = 1.f;
projectile_prediction::Projectile p2;
p2.m_origin = {10.f, 20.f, 30.f};
p2.m_launch_speed = 50.f;
p2.m_gravity_scale = 1.f;
const auto pos1 = Trait::predict_projectile_position(p, 15.f, 30.f, 1.f, 9.81f);
const auto pos2 = Trait::predict_projectile_position(p2, 15.f, 30.f, 1.f, 9.81f);
#if defined(__x86_64__) || defined(_M_X64) || defined(__aarch64__) || defined(_M_ARM64)
constexpr float tol = 1e-6f;
#else
constexpr float tol = 1e-4f;
#endif
EXPECT_NEAR(pos1.x, pos2.x, tol);
EXPECT_NEAR(pos1.y, pos2.y, tol);
EXPECT_NEAR(pos1.z, pos2.z, tol);
}
TEST(LaunchOffsetTests, Source_OffsetAtTimeZero)
{
verify_launch_offset_at_time_zero<source_engine::PredEngineTrait>({0, 0, 0}, {5, 3, -2});
}
TEST(LaunchOffsetTests, Source_ZeroOffsetMatchesDefault)
{
verify_zero_offset_matches_default<source_engine::PredEngineTrait>();
}
TEST(LaunchOffsetTests, Frostbite_OffsetAtTimeZero)
{
verify_launch_offset_at_time_zero<frostbite_engine::PredEngineTrait>({0, 0, 0}, {5, 3, -2});
}
TEST(LaunchOffsetTests, Frostbite_ZeroOffsetMatchesDefault)
{
verify_zero_offset_matches_default<frostbite_engine::PredEngineTrait>();
}
TEST(LaunchOffsetTests, IW_OffsetAtTimeZero)
{
verify_launch_offset_at_time_zero<iw_engine::PredEngineTrait>({0, 0, 0}, {5, 3, -2});
}
TEST(LaunchOffsetTests, IW_ZeroOffsetMatchesDefault)
{
verify_zero_offset_matches_default<iw_engine::PredEngineTrait>();
}
TEST(LaunchOffsetTests, OpenGL_OffsetAtTimeZero)
{
verify_launch_offset_at_time_zero<opengl_engine::PredEngineTrait>({0, 0, 0}, {5, 3, -2});
}
TEST(LaunchOffsetTests, OpenGL_ZeroOffsetMatchesDefault)
{
verify_zero_offset_matches_default<opengl_engine::PredEngineTrait>();
}
TEST(LaunchOffsetTests, Unity_OffsetAtTimeZero)
{
verify_launch_offset_at_time_zero<unity_engine::PredEngineTrait>({0, 0, 0}, {5, 3, -2});
}
TEST(LaunchOffsetTests, Unity_ZeroOffsetMatchesDefault)
{
verify_zero_offset_matches_default<unity_engine::PredEngineTrait>();
}
TEST(LaunchOffsetTests, Unreal_OffsetAtTimeZero)
{
verify_launch_offset_at_time_zero<unreal_engine::PredEngineTrait>({0, 0, 0}, {5, 3, -2});
}
TEST(LaunchOffsetTests, Unreal_ZeroOffsetMatchesDefault)
{
verify_zero_offset_matches_default<unreal_engine::PredEngineTrait>();
}
TEST(LaunchOffsetTests, CryEngine_OffsetAtTimeZero)
{
verify_launch_offset_at_time_zero<cry_engine::PredEngineTrait>({0, 0, 0}, {5, 3, -2});
}
TEST(LaunchOffsetTests, CryEngine_ZeroOffsetMatchesDefault)
{
verify_zero_offset_matches_default<cry_engine::PredEngineTrait>();
}
// Test that offset shifts the projectile position at t>0 as well
TEST(LaunchOffsetTests, OffsetShiftsTrajectory)
{
projectile_prediction::Projectile p_no_offset;
p_no_offset.m_origin = {0.f, 0.f, 0.f};
p_no_offset.m_launch_speed = 100.f;
p_no_offset.m_gravity_scale = 1.f;
projectile_prediction::Projectile p_with_offset;
p_with_offset.m_origin = {0.f, 0.f, 0.f};
p_with_offset.m_launch_offset = {10.f, 5.f, -3.f};
p_with_offset.m_launch_speed = 100.f;
p_with_offset.m_gravity_scale = 1.f;
const auto pos1 = source_engine::PredEngineTrait::predict_projectile_position(p_no_offset, 20.f, 45.f, 2.f, 9.81f);
const auto pos2 = source_engine::PredEngineTrait::predict_projectile_position(p_with_offset, 20.f, 45.f, 2.f, 9.81f);
// The difference should be exactly the launch offset
EXPECT_NEAR(pos2.x - pos1.x, 10.f, 1e-4f);
EXPECT_NEAR(pos2.y - pos1.y, 5.f, 1e-4f);
EXPECT_NEAR(pos2.z - pos1.z, -3.f, 1e-4f);
}
// Generic tests for PredEngineTrait behaviour across engines // Generic tests for PredEngineTrait behaviour across engines
TEST(TraitTests, Frostbite_Pred_And_Mesh_And_Camera) TEST(TraitTests, Frostbite_Pred_And_Mesh_And_Camera)
{ {

View File

@@ -7,6 +7,7 @@
#include <omath/engines/unity_engine/formulas.hpp> #include <omath/engines/unity_engine/formulas.hpp>
#include <print> #include <print>
#include <random> #include <random>
#include <ranges>
TEST(unit_test_unity_engine, UnitsToCentimeters_BasicValues) TEST(unit_test_unity_engine, UnitsToCentimeters_BasicValues)
{ {
@@ -207,7 +208,8 @@ TEST(unit_test_unity_engine, Project)
constexpr auto fov = omath::projection::FieldOfView::from_degrees(60.f); constexpr auto fov = omath::projection::FieldOfView::from_degrees(60.f);
const auto cam = omath::unity_engine::Camera({0, 0, 0}, {}, {1280.f, 720.f}, fov, 0.03f, 1000.f); const auto cam = omath::unity_engine::Camera({0, 0, 0}, {}, {1280.f, 720.f}, fov, 0.03f, 1000.f);
const auto proj = cam.world_to_screen<omath::unity_engine::Camera::ScreenStart::BOTTOM_LEFT_CORNER>({10.f, 3, 10.f}); const auto proj =
cam.world_to_screen<omath::unity_engine::Camera::ScreenStart::BOTTOM_LEFT_CORNER>({10.f, 3, 10.f});
EXPECT_NEAR(proj->x, 1263.538, 0.001f); EXPECT_NEAR(proj->x, 1263.538, 0.001f);
EXPECT_NEAR(proj->y, 547.061f, 0.001f); EXPECT_NEAR(proj->y, 547.061f, 0.001f);
@@ -353,4 +355,65 @@ TEST(unit_test_unity_engine, loook_at_random_z_axis)
failed_points++; failed_points++;
} }
EXPECT_LE(failed_points, 100); EXPECT_LE(failed_points, 100);
} }
TEST(unit_test_unity_engine, look_at_forward)
{
const auto angles = omath::unity_engine::CameraTrait::calc_look_at_angle({}, omath::unity_engine::k_abs_forward);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::unity_engine::forward_vector(angles);
for (const auto& [result, etalon] :
std::views::zip(dir_vector.as_array(), omath::unity_engine::k_abs_forward.as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_unity_engine, look_at_right)
{
const auto angles = omath::unity_engine::CameraTrait::calc_look_at_angle({}, omath::unity_engine::k_abs_right);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::unity_engine::forward_vector(angles);
for (const auto& [result, etalon] :
std::views::zip(dir_vector.as_array(), omath::unity_engine::k_abs_right.as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_unity_engine, look_at_up)
{
const auto angles = omath::unity_engine::CameraTrait::calc_look_at_angle({}, omath::unity_engine::k_abs_right);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::unity_engine::forward_vector(angles);
for (const auto& [result, etalon] :
std::views::zip(dir_vector.as_array(), omath::unity_engine::k_abs_right.as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_unity_engine, look_at_back)
{
const auto angles = omath::unity_engine::CameraTrait::calc_look_at_angle({}, -omath::unity_engine::k_abs_forward);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::unity_engine::forward_vector(angles);
for (const auto& [result, etalon] :
std::views::zip(dir_vector.as_array(), (-omath::unity_engine::k_abs_forward).as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_unity_engine, look_at_left)
{
const auto angles = omath::unity_engine::CameraTrait::calc_look_at_angle({}, -omath::unity_engine::k_abs_right);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::unity_engine::forward_vector(angles);
for (const auto& [result, etalon] :
std::views::zip(dir_vector.as_array(), (-omath::unity_engine::k_abs_right).as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_unity_engine, look_at_down)
{
const auto angles = omath::unity_engine::CameraTrait::calc_look_at_angle({}, -omath::unity_engine::k_abs_up);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::unity_engine::forward_vector(angles);
for (const auto& [result, etalon] :
std::views::zip(dir_vector.as_array(), (-omath::unity_engine::k_abs_up).as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}

View File

@@ -7,6 +7,7 @@
#include <omath/engines/unreal_engine/formulas.hpp> #include <omath/engines/unreal_engine/formulas.hpp>
#include <print> #include <print>
#include <random> #include <random>
#include <ranges>
TEST(unit_test_unreal_engine, ForwardVector) TEST(unit_test_unreal_engine, ForwardVector)
{ {
@@ -361,4 +362,59 @@ TEST(unit_test_unreal_engine, ConstexprConversions)
static_assert(cm == 100000.0, "units_to_centimeters constexpr failed"); static_assert(cm == 100000.0, "units_to_centimeters constexpr failed");
static_assert(m == 1000.0, "units_to_meters constexpr failed"); static_assert(m == 1000.0, "units_to_meters constexpr failed");
static_assert(km == 1.0, "units_to_kilometers constexpr failed"); static_assert(km == 1.0, "units_to_kilometers constexpr failed");
}
TEST(unit_test_unreal_engine, look_at_forward)
{
const auto angles = omath::unreal_engine::CameraTrait::calc_look_at_angle({}, omath::unreal_engine::k_abs_forward);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::unreal_engine::forward_vector(angles);
for (const auto& [result, etalon] : std::views::zip(dir_vector.as_array(), omath::unreal_engine::k_abs_forward.as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_unreal_engine, look_at_right)
{
const auto angles = omath::unreal_engine::CameraTrait::calc_look_at_angle({}, omath::unreal_engine::k_abs_right);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::unreal_engine::forward_vector(angles);
for (const auto& [result, etalon] : std::views::zip(dir_vector.as_array(), omath::unreal_engine::k_abs_right.as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_unreal_engine, look_at_up)
{
const auto angles = omath::unreal_engine::CameraTrait::calc_look_at_angle({}, omath::unreal_engine::k_abs_right);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::unreal_engine::forward_vector(angles);
for (const auto& [result, etalon] : std::views::zip(dir_vector.as_array(), omath::unreal_engine::k_abs_right.as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_unreal_engine, look_at_back)
{
const auto angles = omath::unreal_engine::CameraTrait::calc_look_at_angle({}, -omath::unreal_engine::k_abs_forward);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::unreal_engine::forward_vector(angles);
for (const auto& [result, etalon] : std::views::zip(dir_vector.as_array(), (-omath::unreal_engine::k_abs_forward).as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_unreal_engine, look_at_left)
{
const auto angles = omath::unreal_engine::CameraTrait::calc_look_at_angle({}, -omath::unreal_engine::k_abs_right);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::unreal_engine::forward_vector(angles);
for (const auto& [result, etalon] : std::views::zip(dir_vector.as_array(), (-omath::unreal_engine::k_abs_right).as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_unreal_engine, look_at_down)
{
const auto angles = omath::unreal_engine::CameraTrait::calc_look_at_angle({}, -omath::unreal_engine::k_abs_up);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = omath::unreal_engine::forward_vector(angles);
for (const auto& [result, etalon] : std::views::zip(dir_vector.as_array(), (-omath::unreal_engine::k_abs_up).as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
} }

View File

@@ -0,0 +1,192 @@
#pragma once
// Cross-platform helper for creating binary test "files" without writing to disk where possible.
//
// Strategy:
// - Linux (non-Android, or Android API >= 30): memfd_create → /proc/self/fd/<N> (no disk I/O)
// - All other platforms: anonymous temp file via std::tmpfile(), accessed via /proc/self/fd/<N>
// on Linux, or a named temp file (cleaned up on destruction) elsewhere.
//
// Usage:
// auto f = MemFdFile::create(myVector);
// ASSERT_TRUE(f.valid());
// scanner.scan_for_pattern_in_file(f.path(), ...);
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <random>
#include <string>
#include <vector>
#if defined(__linux__)
# include <unistd.h>
# include <fcntl.h>
# if defined(__ANDROID__)
# if __ANDROID_API__ >= 30
# include <sys/mman.h>
# define OMATH_TEST_USE_MEMFD 1
# endif
// Android < 30: fall through to tmpfile() path below
# else
// Desktop Linux: memfd_create available since glibc 2.27 / kernel 3.17
# include <sys/mman.h>
# define OMATH_TEST_USE_MEMFD 1
# endif
#endif
class MemFdFile
{
public:
MemFdFile() = default;
~MemFdFile()
{
#if defined(OMATH_TEST_USE_MEMFD)
if (m_fd >= 0)
::close(m_fd);
#else
if (!m_temp_path.empty())
std::filesystem::remove(m_temp_path);
#endif
}
MemFdFile(const MemFdFile&) = delete;
MemFdFile& operator=(const MemFdFile&) = delete;
MemFdFile(MemFdFile&& o) noexcept
: m_path(std::move(o.m_path))
#if defined(OMATH_TEST_USE_MEMFD)
, m_fd(o.m_fd)
#else
, m_temp_path(std::move(o.m_temp_path))
#endif
{
#if defined(OMATH_TEST_USE_MEMFD)
o.m_fd = -1;
#else
o.m_temp_path.clear();
#endif
}
[[nodiscard]] bool valid() const { return !m_path.empty(); }
[[nodiscard]] const std::filesystem::path& path() const { return m_path; }
static MemFdFile create(const std::vector<std::uint8_t>& data)
{
return create(data.data(), data.size());
}
static MemFdFile create(const std::uint8_t* data, std::size_t size)
{
MemFdFile f;
#if defined(OMATH_TEST_USE_MEMFD)
f.m_fd = static_cast<int>(::memfd_create("test_bin", 0));
if (f.m_fd < 0)
return f;
if (!write_all(f.m_fd, data, size))
{
::close(f.m_fd);
f.m_fd = -1;
return f;
}
f.m_path = "/proc/self/fd/" + std::to_string(f.m_fd);
#else
// Portable fallback: write to a uniquely-named temp file and delete on destruction
const auto tmp_dir = std::filesystem::temp_directory_path();
std::mt19937_64 rng(std::random_device{}());
const auto unique_name = "omath_test_" + std::to_string(rng()) + ".bin";
f.m_temp_path = (tmp_dir / unique_name).string();
f.m_path = f.m_temp_path;
std::ofstream out(f.m_temp_path, std::ios::binary | std::ios::trunc);
if (!out.is_open())
{
f.m_temp_path.clear();
f.m_path.clear();
return f;
}
out.write(reinterpret_cast<const char*>(data), static_cast<std::streamsize>(size));
if (!out)
{
out.close();
std::filesystem::remove(f.m_temp_path);
f.m_temp_path.clear();
f.m_path.clear();
}
#endif
return f;
}
private:
std::filesystem::path m_path;
#if defined(OMATH_TEST_USE_MEMFD)
int m_fd = -1;
static bool write_all(int fd, const std::uint8_t* data, std::size_t size)
{
std::size_t written = 0;
while (written < size)
{
const auto n = ::write(fd, data + written, size - written);
if (n <= 0)
return false;
written += static_cast<std::size_t>(n);
}
return true;
}
#else
std::string m_temp_path;
#endif
};
// ---------------------------------------------------------------------------
// Build a minimal PE binary in-memory with a single .text section.
// Layout (all offsets compile-time):
// 0x00: DOS header (64 B) 0x40: pad 0x80: NT sig 0x84: FileHeader (20 B)
// 0x98: OptionalHeader (0xF0 B) 0x188: SectionHeader (44 B) 0x1B4: section data
// ---------------------------------------------------------------------------
inline std::vector<std::uint8_t> build_minimal_pe(const std::vector<std::uint8_t>& section_bytes)
{
constexpr std::uint32_t e_lfanew = 0x80u;
constexpr std::uint16_t size_opt = 0xF0u;
constexpr std::size_t nt_off = e_lfanew;
constexpr std::size_t fh_off = nt_off + 4;
constexpr std::size_t oh_off = fh_off + 20;
constexpr std::size_t sh_off = oh_off + size_opt;
constexpr std::size_t data_off = sh_off + 44;
std::vector<std::uint8_t> buf(data_off + section_bytes.size(), 0u);
buf[0] = 'M'; buf[1] = 'Z';
std::memcpy(buf.data() + 0x3Cu, &e_lfanew, 4);
buf[nt_off] = 'P'; buf[nt_off + 1] = 'E';
const std::uint16_t machine = 0x8664u, num_sections = 1u;
std::memcpy(buf.data() + fh_off, &machine, 2);
std::memcpy(buf.data() + fh_off + 2, &num_sections, 2);
std::memcpy(buf.data() + fh_off + 16, &size_opt, 2);
const std::uint16_t magic = 0x20Bu;
std::memcpy(buf.data() + oh_off, &magic, 2);
const char name[8] = {'.','t','e','x','t',0,0,0};
std::memcpy(buf.data() + sh_off, name, 8);
const auto vsize = static_cast<std::uint32_t>(section_bytes.size());
const std::uint32_t vaddr = 0x1000u;
const auto ptr_raw = static_cast<std::uint32_t>(data_off);
std::memcpy(buf.data() + sh_off + 8, &vsize, 4);
std::memcpy(buf.data() + sh_off + 12, &vaddr, 4);
std::memcpy(buf.data() + sh_off + 16, &vsize, 4);
std::memcpy(buf.data() + sh_off + 20, &ptr_raw, 4);
std::memcpy(buf.data() + data_off, section_bytes.data(), section_bytes.size());
return buf;
}

View File

@@ -8,6 +8,29 @@
using namespace omath; using namespace omath;
using namespace omath::pathfinding; using namespace omath::pathfinding;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
static NavigationMesh make_linear_chain(int length)
{
// 0 -> 1 -> 2 -> ... -> length-1 (directed)
NavigationMesh nav;
for (int i = 0; i < length; ++i)
{
const Vector3<float> v{static_cast<float>(i), 0.f, 0.f};
if (i + 1 < length)
nav.m_vertex_map[v] = {Vector3<float>{static_cast<float>(i + 1), 0.f, 0.f}};
else
nav.m_vertex_map[v] = {};
}
return nav;
}
// ---------------------------------------------------------------------------
// Basic reachability
// ---------------------------------------------------------------------------
TEST(AStarExtra, TrivialNeighbor) TEST(AStarExtra, TrivialNeighbor)
{ {
NavigationMesh nav; NavigationMesh nav;
@@ -78,7 +101,7 @@ TEST(AStarExtra, LongerPathAvoidsBlock)
constexpr Vector3<float> goal = idx(2, 1); constexpr Vector3<float> goal = idx(2, 1);
const auto path = Astar::find_path(start, goal, nav); const auto path = Astar::find_path(start, goal, nav);
ASSERT_FALSE(path.empty()); ASSERT_FALSE(path.empty());
EXPECT_EQ(path.front(), goal); // Astar convention: single-element or endpoint present EXPECT_EQ(path.front(), goal);
} }
TEST(AstarTests, TrivialDirectNeighborPath) TEST(AstarTests, TrivialDirectNeighborPath)
@@ -91,9 +114,6 @@ TEST(AstarTests, TrivialDirectNeighborPath)
nav.m_vertex_map.emplace(v2, std::vector<Vector3<float>>{v1}); nav.m_vertex_map.emplace(v2, std::vector<Vector3<float>>{v1});
const auto path = Astar::find_path(v1, v2, nav); const auto path = Astar::find_path(v1, v2, nav);
// Current A* implementation returns the end vertex as the reconstructed
// path (single-element) in the simple neighbor scenario. Assert that the
// endpoint is present and reachable.
ASSERT_EQ(path.size(), 1u); ASSERT_EQ(path.size(), 1u);
EXPECT_EQ(path.front(), v2); EXPECT_EQ(path.front(), v2);
} }
@@ -133,4 +153,155 @@ TEST(unit_test_a_star, finding_right_path)
mesh.m_vertex_map[{0.f, 2.f, 0.f}] = {{0.f, 3.f, 0.f}}; mesh.m_vertex_map[{0.f, 2.f, 0.f}] = {{0.f, 3.f, 0.f}};
mesh.m_vertex_map[{0.f, 3.f, 0.f}] = {}; mesh.m_vertex_map[{0.f, 3.f, 0.f}] = {};
std::ignore = omath::pathfinding::Astar::find_path({}, {0.f, 3.f, 0.f}, mesh); std::ignore = omath::pathfinding::Astar::find_path({}, {0.f, 3.f, 0.f}, mesh);
} }
// ---------------------------------------------------------------------------
// Directed edges
// ---------------------------------------------------------------------------
TEST(AstarTests, DirectedEdge_ForwardPathExists)
{
// A -> B only; path from A to B should succeed
NavigationMesh nav;
constexpr Vector3<float> a{0.f, 0.f, 0.f};
constexpr Vector3<float> b{1.f, 0.f, 0.f};
nav.m_vertex_map[a] = {b};
nav.m_vertex_map[b] = {}; // no edge back
const auto path = Astar::find_path(a, b, nav);
ASSERT_FALSE(path.empty());
EXPECT_EQ(path.back(), b);
}
TEST(AstarTests, DirectedEdge_ReversePathMissing)
{
// A -> B only; path from B to A should fail
NavigationMesh nav;
constexpr Vector3<float> a{0.f, 0.f, 0.f};
constexpr Vector3<float> b{1.f, 0.f, 0.f};
nav.m_vertex_map[a] = {b};
nav.m_vertex_map[b] = {};
const auto path = Astar::find_path(b, a, nav);
EXPECT_TRUE(path.empty());
}
// ---------------------------------------------------------------------------
// Vertex snapping
// ---------------------------------------------------------------------------
TEST(AstarTests, OffMeshStart_SnapsToNearestVertex)
{
NavigationMesh nav;
constexpr Vector3<float> v1{0.f, 0.f, 0.f};
constexpr Vector3<float> v2{10.f, 0.f, 0.f};
nav.m_vertex_map[v1] = {v2};
nav.m_vertex_map[v2] = {v1};
// Start is slightly off v1 but closer to it than to v2
constexpr Vector3<float> off_start{0.1f, 0.f, 0.f};
const auto path = Astar::find_path(off_start, v2, nav);
ASSERT_FALSE(path.empty());
EXPECT_EQ(path.back(), v2);
}
TEST(AstarTests, OffMeshEnd_SnapsToNearestVertex)
{
NavigationMesh nav;
constexpr Vector3<float> v1{0.f, 0.f, 0.f};
constexpr Vector3<float> v2{10.f, 0.f, 0.f};
nav.m_vertex_map[v1] = {v2};
nav.m_vertex_map[v2] = {v1};
// Goal is slightly off v2 but closer to it than to v1
constexpr Vector3<float> off_goal{9.9f, 0.f, 0.f};
const auto path = Astar::find_path(v1, off_goal, nav);
ASSERT_FALSE(path.empty());
EXPECT_EQ(path.back(), v2);
}
// ---------------------------------------------------------------------------
// Cycle handling
// ---------------------------------------------------------------------------
TEST(AstarTests, CyclicGraph_FindsPathWithoutLooping)
{
// Triangle: A <-> B <-> C <-> A
NavigationMesh nav;
constexpr Vector3<float> a{0.f, 0.f, 0.f};
constexpr Vector3<float> b{1.f, 0.f, 0.f};
constexpr Vector3<float> c{0.5f, 1.f, 0.f};
nav.m_vertex_map[a] = {b, c};
nav.m_vertex_map[b] = {a, c};
nav.m_vertex_map[c] = {a, b};
const auto path = Astar::find_path(a, c, nav);
ASSERT_FALSE(path.empty());
EXPECT_EQ(path.back(), c);
}
TEST(AstarTests, SelfLoopVertex_DoesNotBreakSearch)
{
// Vertex with itself as a neighbor
NavigationMesh nav;
constexpr Vector3<float> a{0.f, 0.f, 0.f};
constexpr Vector3<float> b{1.f, 0.f, 0.f};
nav.m_vertex_map[a] = {a, b}; // self-loop on a
nav.m_vertex_map[b] = {a};
const auto path = Astar::find_path(a, b, nav);
ASSERT_FALSE(path.empty());
EXPECT_EQ(path.back(), b);
}
// ---------------------------------------------------------------------------
// Longer chains
// ---------------------------------------------------------------------------
TEST(AstarTests, LinearChain_ReachesEnd)
{
constexpr int kLength = 10;
const NavigationMesh nav = make_linear_chain(kLength);
const Vector3<float> start{0.f, 0.f, 0.f};
const Vector3<float> goal{static_cast<float>(kLength - 1), 0.f, 0.f};
const auto path = Astar::find_path(start, goal, nav);
ASSERT_FALSE(path.empty());
EXPECT_EQ(path.back(), goal);
}
TEST(AstarTests, LinearChain_MidpointReachable)
{
constexpr int kLength = 6;
const NavigationMesh nav = make_linear_chain(kLength);
const Vector3<float> start{0.f, 0.f, 0.f};
const Vector3<float> mid{3.f, 0.f, 0.f};
const auto path = Astar::find_path(start, mid, nav);
ASSERT_FALSE(path.empty());
EXPECT_EQ(path.back(), mid);
}
// ---------------------------------------------------------------------------
// Serialize -> pathfind integration
// ---------------------------------------------------------------------------
TEST(AstarTests, PathfindAfterSerializeDeserialize)
{
NavigationMesh nav;
constexpr Vector3<float> a{0.f, 0.f, 0.f};
constexpr Vector3<float> b{1.f, 0.f, 0.f};
constexpr Vector3<float> c{2.f, 0.f, 0.f};
nav.m_vertex_map[a] = {b};
nav.m_vertex_map[b] = {a, c};
nav.m_vertex_map[c] = {b};
NavigationMesh nav2;
nav2.deserialize(nav.serialize());
const auto path = Astar::find_path(a, c, nav2);
ASSERT_FALSE(path.empty());
EXPECT_EQ(path.back(), c);
}

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

@@ -1,17 +1,214 @@
// //
// Created by Vladislav on 30.12.2025. // Created by Vladislav on 30.12.2025.
// //
// /Users/vladislav/Downloads/valencia #include <algorithm>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <omath/utility/elf_pattern_scan.hpp> #include <omath/utility/elf_pattern_scan.hpp>
#include <print> #include <span>
TEST(unit_test_elf_pattern_scan_file, ScanMissingPattern) #include <vector>
using namespace omath;
// ---- helpers ---------------------------------------------------------------
// Minimal ELF64 file with a single .text section containing known bytes.
// Layout:
// 0x000 : ELF64 file header (64 bytes)
// 0x040 : section data (padded to 0x20 bytes)
// 0x060 : section name table ".text\0" + "\0" (empty name for SHN_UNDEF)
// 0x080 : section header table (3 entries × 64 bytes = 0xC0)
static std::vector<std::byte> make_elf64_with_text_section(const std::vector<std::uint8_t>& code_bytes)
{ {
//FIXME: Implement normal tests :) // Fixed layout constants
//constexpr std::string_view path = "/Users/vladislav/Downloads/crackme"; constexpr std::size_t text_off = 0x40;
constexpr std::size_t text_size = 0x20; // always 32 bytes (code padded with zeros)
constexpr std::size_t shstrtab_off = text_off + text_size;
// ".text\0" = 6 chars, prepend \0 for SHN_UNDEF → "\0.text\0"
constexpr std::size_t shstrtab_size = 8; // "\0.text\0\0"
constexpr std::size_t shdr_table_off = shstrtab_off + shstrtab_size;
constexpr std::size_t shdr_size = 64; // sizeof(Elf64_Shdr)
constexpr std::size_t num_sections = 3; // null + .text + .shstrtab
constexpr std::size_t total_size = shdr_table_off + num_sections * shdr_size;
//const auto res = omath::ElfPatternScanner::scan_for_pattern_in_file(path, "F3 0F 1E FA 55 48 89 E5 B8 00 00 00 00", ".text"); std::vector<std::byte> buf(total_size, std::byte{0});
//EXPECT_TRUE(res.has_value());
//std::println("In virtual mem: 0x{:x}", res->virtual_base_addr+res->target_offset); auto w8 = [&](std::size_t off, std::uint8_t v) { buf[off] = std::byte{v}; };
auto w16 = [&](std::size_t off, std::uint16_t v)
{ std::memcpy(buf.data() + off, &v, 2); };
auto w32 = [&](std::size_t off, std::uint32_t v)
{ std::memcpy(buf.data() + off, &v, 4); };
auto w64 = [&](std::size_t off, std::uint64_t v)
{ std::memcpy(buf.data() + off, &v, 8); };
// --- ELF64 file header ---
// e_ident
buf[0] = std::byte{0x7F};
buf[1] = std::byte{'E'};
buf[2] = std::byte{'L'};
buf[3] = std::byte{'F'};
w8(4, 2); // ELFCLASS64
w8(5, 1); // ELFDATA2LSB
w8(6, 1); // EV_CURRENT
// rest of e_ident is 0
w16(16, 2); // e_type = ET_EXEC
w16(18, 62); // e_machine = EM_X86_64
w32(20, 1); // e_version
w64(24, 0); // e_entry
w64(32, 0); // e_phoff
w64(40, static_cast<std::uint64_t>(shdr_table_off)); // e_shoff
w32(48, 0); // e_flags
w16(52, 64); // e_ehsize
w16(54, 56); // e_phentsize
w16(56, 0); // e_phnum
w16(58, static_cast<std::uint16_t>(shdr_size)); // e_shentsize
w16(60, static_cast<std::uint16_t>(num_sections)); // e_shnum
w16(62, 2); // e_shstrndx = 2 (.shstrtab is section index 2)
// --- section data (.text) ---
const std::size_t copy_len = std::min(code_bytes.size(), text_size);
for (std::size_t i = 0; i < copy_len; ++i)
buf[text_off + i] = std::byte{code_bytes[i]};
// --- .shstrtab data: "\0.text\0\0" ---
// index 0 → "" (SHN_UNDEF name)
// index 1 → ".text"
// index 7 → ".shstrtab" (we cheat and use index 1 for .shstrtab too, fine for test)
buf[shstrtab_off + 0] = std::byte{0};
buf[shstrtab_off + 1] = std::byte{'.'};
buf[shstrtab_off + 2] = std::byte{'t'};
buf[shstrtab_off + 3] = std::byte{'e'};
buf[shstrtab_off + 4] = std::byte{'x'};
buf[shstrtab_off + 5] = std::byte{'t'};
buf[shstrtab_off + 6] = std::byte{0};
buf[shstrtab_off + 7] = std::byte{0};
// --- section headers ---
// Elf64_Shdr fields (all offsets relative to start of a section header):
// 0 sh_name (4)
// 4 sh_type (4)
// 8 sh_flags (8)
// 16 sh_addr (8)
// 24 sh_offset (8)
// 32 sh_size (8)
// 40 sh_link (4)
// 44 sh_info (4)
// 48 sh_addralign(8)
// 56 sh_entsize (8)
// Section 0: null
// (all zeros already zeroed)
// Section 1: .text
{
const std::size_t base = shdr_table_off + 1 * shdr_size;
w32(base + 0, 1); // sh_name → index 1 in shstrtab → ".text"
w32(base + 4, 1); // sh_type = SHT_PROGBITS
w64(base + 8, 6); // sh_flags = SHF_ALLOC|SHF_EXECINSTR
w64(base + 16, static_cast<std::uint64_t>(text_off)); // sh_addr (same as offset in test)
w64(base + 24, static_cast<std::uint64_t>(text_off)); // sh_offset
w64(base + 32, static_cast<std::uint64_t>(text_size)); // sh_size
w64(base + 48, 16); // sh_addralign
}
// Section 2: .shstrtab
{
const std::size_t base = shdr_table_off + 2 * shdr_size;
w32(base + 0, 0); // sh_name → index 0 → "" (good enough for scanner)
w32(base + 4, 3); // sh_type = SHT_STRTAB
w64(base + 24, static_cast<std::uint64_t>(shstrtab_off)); // sh_offset
w64(base + 32, static_cast<std::uint64_t>(shstrtab_size)); // sh_size
}
return buf;
}
// ---- tests -----------------------------------------------------------------
TEST(unit_test_elf_pattern_scan_memory, finds_pattern)
{
const std::vector<std::uint8_t> code = {0x55, 0x48, 0x89, 0xE5, 0xC3};
const auto buf = make_elf64_with_text_section(code);
const auto span = std::span<const std::byte>{buf};
const auto result = ElfPatternScanner::scan_for_pattern_in_memory_file(span, "55 48 89 E5", ".text");
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result->target_offset, 0);
}
TEST(unit_test_elf_pattern_scan_memory, finds_pattern_with_wildcard)
{
const std::vector<std::uint8_t> code = {0xDE, 0xAD, 0xBE, 0xEF, 0x00};
const auto buf = make_elf64_with_text_section(code);
const auto result =
ElfPatternScanner::scan_for_pattern_in_memory_file(std::span<const std::byte>{buf}, "DE ?? BE EF", ".text");
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result->target_offset, 0);
}
TEST(unit_test_elf_pattern_scan_memory, pattern_not_found_returns_nullopt)
{
const std::vector<std::uint8_t> code = {0x01, 0x02, 0x03, 0x04};
const auto buf = make_elf64_with_text_section(code);
const auto result =
ElfPatternScanner::scan_for_pattern_in_memory_file(std::span<const std::byte>{buf}, "AA BB CC", ".text");
EXPECT_FALSE(result.has_value());
}
TEST(unit_test_elf_pattern_scan_memory, invalid_data_returns_nullopt)
{
const std::vector<std::byte> garbage(64, std::byte{0xFF});
const auto result =
ElfPatternScanner::scan_for_pattern_in_memory_file(std::span<const std::byte>{garbage}, "FF FF", ".text");
EXPECT_FALSE(result.has_value());
}
TEST(unit_test_elf_pattern_scan_memory, empty_data_returns_nullopt)
{
const auto result = ElfPatternScanner::scan_for_pattern_in_memory_file({}, "FF", ".text");
EXPECT_FALSE(result.has_value());
}
TEST(unit_test_elf_pattern_scan_memory, missing_section_returns_nullopt)
{
const std::vector<std::uint8_t> code = {0x90, 0x90};
const auto buf = make_elf64_with_text_section(code);
const auto result = ElfPatternScanner::scan_for_pattern_in_memory_file(std::span<const std::byte>{buf},
"90 90", ".nonexistent");
EXPECT_FALSE(result.has_value());
}
TEST(unit_test_elf_pattern_scan_memory, matches_file_scan)
{
// Write our synthetic ELF to a temp file and verify memory scan == file scan
const std::vector<std::uint8_t> code = {0x48, 0x89, 0xE5, 0xDE, 0xAD, 0xBE, 0xEF, 0x00};
const auto buf = make_elf64_with_text_section(code);
const auto tmp_path = std::filesystem::temp_directory_path() / "omath_elf_test.elf";
{
std::ofstream out(tmp_path, std::ios::binary);
out.write(reinterpret_cast<const char*>(buf.data()), static_cast<std::streamsize>(buf.size()));
}
const auto file_result = ElfPatternScanner::scan_for_pattern_in_file(tmp_path, "48 89 E5 DE AD", ".text");
const auto mem_result =
ElfPatternScanner::scan_for_pattern_in_memory_file(std::span<const std::byte>{buf}, "48 89 E5 DE AD", ".text");
std::filesystem::remove(tmp_path);
ASSERT_TRUE(file_result.has_value());
ASSERT_TRUE(mem_result.has_value());
EXPECT_EQ(file_result->virtual_base_addr, mem_result->virtual_base_addr);
EXPECT_EQ(file_result->raw_base_addr, mem_result->raw_base_addr);
EXPECT_EQ(file_result->target_offset, mem_result->target_offset);
} }

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

Some files were not shown because too many files have changed in this diff Show More