mirror of
https://github.com/orange-cpp/omath.git
synced 2026-02-13 23:13:26 +00:00
Compare commits
114 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fdb2ad099a | |||
| 88ce5e6b8c | |||
| 29da13d244 | |||
| d16984a8b2 | |||
| d935caf1a4 | |||
| 897484bea1 | |||
| a03620c18f | |||
|
|
4a8e7e85ce | ||
| 1499ac3213 | |||
|
|
f3a6a1a3ae | ||
| c312ccad0c | |||
| 939be67643 | |||
| 43a063807d | |||
| 4fd7f8efa6 | |||
| 52ca23383d | |||
| ce21c217f1 | |||
| 09b64cc702 | |||
| d085681efe | |||
|
|
2f7746caeb | ||
| 94ee8751af | |||
|
|
82b9b671f6 | ||
| 082b5f69b8 | |||
|
|
735a565446 | ||
| 852bf5c56f | |||
|
|
de5c8bc84d | ||
| 35d9de1550 | |||
|
|
201d8f5547 | ||
| 5a7b9d2338 | |||
| 3d67827704 | |||
| 45a37eb413 | |||
| 90c4ea2036 | |||
| e10cbf9356 | |||
| 4ad44badb9 | |||
| adce4a808a | |||
| 257b06c552 | |||
| a94c78f834 | |||
| b6ac0a1d61 | |||
| f3b74fe433 | |||
| 2ddf29b158 | |||
| bf30957acf | |||
| c9ac61935e | |||
| 60a3a42140 | |||
| 17e21cde4b | |||
| 7fb5ea47dd | |||
| d7189eb7d4 | |||
| ff35571231 | |||
| 3744a6cdec | |||
| 0fd9a5aed8 | |||
| 3dd792c2d5 | |||
| 584969da44 | |||
| acf36c3e04 | |||
| 27c1d147c5 | |||
| 3831bc0999 | |||
| d23bc3204d | |||
| c158f08430 | |||
| e05eba42c3 | |||
| e97be8c142 | |||
| e97d097b2b | |||
| 58aa03c4a9 | |||
| e1399d1814 | |||
| 1964d3d36f | |||
| d7a009eb67 | |||
| 0e03805439 | |||
| eafefb40ec | |||
| 9e4c778e8f | |||
| 0788fd6122 | |||
| 3685f13344 | |||
| d4d8f70fff | |||
| 918858e255 | |||
| 1aff083ef3 | |||
| 6414922884 | |||
| 57ba809076 | |||
| 6fd3a695cf | |||
| f6857cac90 | |||
| b994e47357 | |||
| 82b21d0458 | |||
| daa1abc047 | |||
| 3a66b66c6a | |||
| 6c89c72041 | |||
| e54d5e7388 | |||
| 9a89e2467e | |||
| 48bf06f69c | |||
| 8feddf872a | |||
| 99ebdeb188 | |||
| ba267cbcb8 | |||
| b98093b244 | |||
| 58392144ca | |||
| f1394a24e5 | |||
| e396e00016 | |||
| 0dc4890107 | |||
| dd8c41b19f | |||
| 0283935918 | |||
| 12f888b8d4 | |||
| a5b24f90dc | |||
|
|
df4947ceb3 | ||
|
|
190a8bf91e | ||
|
|
d118e88f6b | ||
| 1553139a80 | |||
| 798caa2b0d | |||
| 88d4447b20 | |||
| ee458a24f7 | |||
| fa91f21e39 | |||
| 873bdd2036 | |||
| 20aecac2ae | |||
| 09fd92ccad | |||
| 2b21caf58f | |||
| 40e26be72e | |||
| 2699053102 | |||
| 06b597f37c | |||
| 6d3b543648 | |||
| c515dc89a9 | |||
| 66919af46a | |||
| 28ef194586 | |||
| 05bc7577b5 |
590
.github/workflows/cmake-multi-platform.yml
vendored
590
.github/workflows/cmake-multi-platform.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Omath CI (Arch Linux / Windows)
|
||||
name: Omath CI
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -10,24 +10,82 @@ concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
|
||||
##############################################################################
|
||||
# 1) ARCH LINUX – Clang / Ninja
|
||||
# 1) Linux – Clang / Ninja
|
||||
##############################################################################
|
||||
jobs:
|
||||
arch-build-and-test:
|
||||
name: Arch Linux (Clang)
|
||||
runs-on: ubuntu-latest
|
||||
container: archlinux:latest
|
||||
linux-build-and-test:
|
||||
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
|
||||
coverage: true
|
||||
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 \
|
||||
llvm-21
|
||||
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
|
||||
coverage: false
|
||||
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
|
||||
coverage: false
|
||||
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 with pacman
|
||||
- name: Install basic tool-chain
|
||||
shell: bash
|
||||
run: |
|
||||
pacman -Sy --noconfirm archlinux-keyring
|
||||
pacman -Syu --noconfirm --needed \
|
||||
git base-devel clang cmake ninja zip unzip fmt
|
||||
run: ${{ matrix.install_cmd }}
|
||||
|
||||
- name: Checkout repository (with sub-modules)
|
||||
uses: actions/checkout@v4
|
||||
@@ -38,29 +96,82 @@ jobs:
|
||||
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 linux-release-vcpkg -DOMATH_BUILD_TESTS=ON -DOMATH_BUILD_BENCHMARK=OFF -DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests"
|
||||
run: |
|
||||
cmake --preset ${{ matrix.preset }} \
|
||||
-DVCPKG_INSTALL_OPTIONS="--allow-unsupported" \
|
||||
-DOMATH_BUILD_TESTS=ON \
|
||||
-DOMATH_BUILD_BENCHMARK=OFF \
|
||||
-DOMATH_ENABLE_COVERAGE=${{ matrix.coverage == true && 'ON' || 'OFF' }} \
|
||||
-DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests"
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: cmake --build cmake-build/build/linux-release-vcpkg --target unit_tests omath
|
||||
run: cmake --build cmake-build/build/${{ matrix.preset }} --target unit_tests omath
|
||||
|
||||
- name: Run unit_tests
|
||||
shell: bash
|
||||
run: ./out/Release/unit_tests
|
||||
|
||||
- name: Run Coverage
|
||||
if: ${{ matrix.coverage == true }}
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get install lcov
|
||||
chmod +x scripts/coverage-llvm.sh
|
||||
./scripts/coverage-llvm.sh \
|
||||
"${{ github.workspace }}" \
|
||||
"cmake-build/build/${{ matrix.preset }}" \
|
||||
"./out/Release/unit_tests" \
|
||||
"cmake-build/build/${{ matrix.preset }}/coverage"
|
||||
|
||||
- name: Upload Coverage Report
|
||||
if: ${{ matrix.coverage == true }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-report-linux
|
||||
path: cmake-build/build/${{ matrix.preset }}/coverage/
|
||||
|
||||
- name: Upload logs on failure
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: linux-build-logs-${{ matrix.triplet }}
|
||||
path: |
|
||||
cmake-build/build/${{ matrix.preset }}/**/*.log
|
||||
${{ env.VCPKG_ROOT }}/buildtrees/**/*.log
|
||||
|
||||
##############################################################################
|
||||
# 2) Windows – MSVC / Ninja
|
||||
##############################################################################
|
||||
windows-build-and-test:
|
||||
name: Windows (MSVC)
|
||||
runs-on: windows-latest
|
||||
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
|
||||
- name: Windows (MSVC) (x86-windows)
|
||||
runner: windows-latest
|
||||
arch: amd64_x86
|
||||
preset: windows-release-vcpkg-x86
|
||||
triplet: x86-windows
|
||||
- name: Windows (MSVC) (arm64-windows)
|
||||
runner: windows-11-arm
|
||||
arch: arm64
|
||||
preset: windows-release-vcpkg-arm64
|
||||
triplet: arm64-windows
|
||||
fail-fast: false
|
||||
env:
|
||||
OMATH_BUILD_VIA_VCPKG: ON
|
||||
|
||||
VCPKG_ROOT: ${{ github.workspace }}/vcpkg
|
||||
steps:
|
||||
- name: Checkout repository (with sub-modules)
|
||||
uses: actions/checkout@v4
|
||||
@@ -72,25 +183,80 @@ jobs:
|
||||
|
||||
- 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 windows-release-vcpkg -DOMATH_BUILD_TESTS=ON -DOMATH_BUILD_BENCHMARK=OFF -DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests"
|
||||
run: cmake --preset ${{ matrix.preset }} -DOMATH_BUILD_TESTS=ON -DOMATH_BUILD_BENCHMARK=OFF -DOMATH_ENABLE_COVERAGE=OFF -DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests"
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: cmake --build cmake-build/build/windows-release-vcpkg --target unit_tests omath
|
||||
run: cmake --build cmake-build/build/${{ matrix.preset }} --target unit_tests omath
|
||||
|
||||
- name: Run unit_tests.exe
|
||||
shell: bash
|
||||
run: ./out/Release/unit_tests.exe
|
||||
|
||||
- name: Install OpenCppCoverage with Chocolatey
|
||||
if: ${{ matrix.triplet == 'x64-windows' }}
|
||||
run: choco install opencppcoverage -y
|
||||
|
||||
- name: Build Debug for Coverage
|
||||
if: ${{ matrix.triplet == 'x64-windows' }}
|
||||
shell: bash
|
||||
run: |
|
||||
cmake --preset ${{ matrix.preset }} \
|
||||
-DOMATH_BUILD_TESTS=ON \
|
||||
-DOMATH_BUILD_BENCHMARK=OFF \
|
||||
-DOMATH_ENABLE_COVERAGE=ON \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests"
|
||||
cmake --build cmake-build/build/${{ matrix.preset }} --config Debug --target unit_tests omath
|
||||
|
||||
- name: Run Coverage
|
||||
if: ${{ matrix.triplet == 'x64-windows' }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
$env:Path = "C:\Program Files\OpenCppCoverage;$env:Path"
|
||||
cmake --build cmake-build/build/${{ matrix.preset }} --target coverage --config Debug
|
||||
|
||||
- name: Upload Coverage
|
||||
if: ${{ matrix.triplet == 'x64-windows' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-report-windows
|
||||
path: cmake-build/build/${{ matrix.preset }}/coverage/
|
||||
|
||||
- name: Upload logs on failure
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-build-logs-${{ matrix.triplet }}
|
||||
path: |
|
||||
cmake-build/build/${{ matrix.preset }}/**/*.log
|
||||
${{ env.VCPKG_ROOT }}/buildtrees/**/*.log
|
||||
|
||||
##############################################################################
|
||||
# 3) macOS – AppleClang / Ninja
|
||||
##############################################################################
|
||||
macosx-build-and-test:
|
||||
name: macOS (AppleClang)
|
||||
runs-on: macOS-latest
|
||||
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
|
||||
coverage: true
|
||||
- name: macOS (AppleClang) (x64-osx)
|
||||
runner: macos-15-intel
|
||||
preset: darwin-release-vcpkg-x64
|
||||
triplet: x64-osx
|
||||
coverage: false
|
||||
fail-fast: false
|
||||
env:
|
||||
VCPKG_ROOT: ${{ github.workspace }}/vcpkg
|
||||
steps:
|
||||
@@ -111,12 +277,388 @@ jobs:
|
||||
|
||||
- name: Configure (cmake --preset)
|
||||
shell: bash
|
||||
run: cmake --preset darwin-release-vcpkg -DOMATH_BUILD_TESTS=ON -DOMATH_BUILD_BENCHMARK=OFF -DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests"
|
||||
run: |
|
||||
cmake --preset ${{ matrix.preset }} \
|
||||
-DOMATH_BUILD_TESTS=ON \
|
||||
-DOMATH_BUILD_BENCHMARK=OFF \
|
||||
-DOMATH_ENABLE_COVERAGE=${{ matrix.coverage == true && 'ON' || 'OFF' }} \
|
||||
-DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests"
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: cmake --build cmake-build/build/darwin-release-vcpkg --target unit_tests omath
|
||||
run: cmake --build cmake-build/build/${{ matrix.preset }} --target unit_tests omath
|
||||
|
||||
- name: Run unit_tests
|
||||
shell: bash
|
||||
run: ./out/Release/unit_tests
|
||||
|
||||
- name: Run Coverage
|
||||
if: ${{ matrix.coverage == true }}
|
||||
shell: bash
|
||||
run: |
|
||||
brew install lcov
|
||||
chmod +x scripts/coverage-llvm.sh
|
||||
./scripts/coverage-llvm.sh \
|
||||
"${{ github.workspace }}" \
|
||||
"cmake-build/build/${{ matrix.preset }}" \
|
||||
"./out/Release/unit_tests" \
|
||||
"cmake-build/build/${{ matrix.preset }}/coverage"
|
||||
|
||||
- name: Upload Coverage Report
|
||||
if: ${{ matrix.coverage == true }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-report-macos
|
||||
path: cmake-build/build/${{ matrix.preset }}/coverage/
|
||||
|
||||
- name: Upload logs on failure
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: macos-build-logs-${{ matrix.triplet }}
|
||||
path: |
|
||||
cmake-build/build/${{ matrix.preset }}/**/*.log
|
||||
${{ env.VCPKG_ROOT }}/buildtrees/**/*.log
|
||||
|
||||
##############################################################################
|
||||
# 4) iOS – AppleClang / Xcode / arm64-ios
|
||||
##############################################################################
|
||||
ios-build:
|
||||
name: iOS (AppleClang) (${{ matrix.triplet }})
|
||||
runs-on: macOS-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- triplet: arm64-ios
|
||||
preset: ios-release-vcpkg
|
||||
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=ON \
|
||||
-DOMATH_BUILD_BENCHMARK=OFF \
|
||||
-DVCPKG_MANIFEST_FEATURES="imgui;tests"
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: |
|
||||
cmake --build cmake-build/build/${{ matrix.preset }} --config Release --target unit_tests omath
|
||||
|
||||
- name: Upload logs on failure
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ios-build-logs-${{ matrix.triplet }}
|
||||
path: |
|
||||
cmake-build/build/${{ matrix.preset }}/**/*.log
|
||||
${{ env.VCPKG_ROOT }}/buildtrees/**/*.log
|
||||
|
||||
##############################################################################
|
||||
# 5) FreeBSD – Clang / Ninja
|
||||
##############################################################################
|
||||
freebsd-build-and-test:
|
||||
name: FreeBSD (Clang) (${{ matrix.triplet }})
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- triplet: x64-freebsd
|
||||
preset: freebsd-release-vcpkg
|
||||
arch: x86-64
|
||||
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 Test
|
||||
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=ON \
|
||||
-DOMATH_BUILD_BENCHMARK=OFF \
|
||||
-DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests" \
|
||||
-DVCPKG_INSTALL_OPTIONS="--allow-unsupported"
|
||||
cmake --build cmake-build/build/${{ matrix.preset }} --target unit_tests omath
|
||||
./out/Release/unit_tests
|
||||
|
||||
- name: Upload logs on failure
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: freebsd-build-logs-${{ matrix.triplet }}
|
||||
path: |
|
||||
cmake-build/build/${{ matrix.preset }}/**/*.log
|
||||
${{ env.VCPKG_ROOT }}/buildtrees/**/*.log
|
||||
|
||||
##############################################################################
|
||||
# 6) Android NDK – Clang / Ninja
|
||||
##############################################################################
|
||||
android-build:
|
||||
name: Android NDK (${{ matrix.triplet }})
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- triplet: arm-neon-android
|
||||
preset: android-arm-neon-release-vcpkg
|
||||
- triplet: arm64-android
|
||||
preset: android-arm64-release-vcpkg
|
||||
- triplet: x64-android
|
||||
preset: android-x64-release-vcpkg
|
||||
- triplet: x86-android
|
||||
preset: android-x86-release-vcpkg
|
||||
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=ON \
|
||||
-DOMATH_BUILD_BENCHMARK=OFF \
|
||||
-DVCPKG_MANIFEST_FEATURES="imgui;tests"
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: |
|
||||
cmake --build cmake-build/build/${{ matrix.preset }} --target unit_tests omath
|
||||
|
||||
- name: Upload logs on failure
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: android-build-logs-${{ matrix.triplet }}
|
||||
path: |
|
||||
cmake-build/build/${{ matrix.preset }}/**/*.log
|
||||
${{ env.VCPKG_ROOT }}/buildtrees/**/*.log
|
||||
|
||||
##############################################################################
|
||||
# 7) WebAssembly (Emscripten) – Clang / Ninja / wasm32-emscripten
|
||||
##############################################################################
|
||||
wasm-build-and-test:
|
||||
name: WebAssembly (Emscripten) (${{ matrix.triplet }})
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- triplet: wasm32-emscripten
|
||||
preset: wasm-release-vcpkg
|
||||
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
|
||||
# Verify toolchain file exists
|
||||
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=ON \
|
||||
-DOMATH_BUILD_BENCHMARK=OFF \
|
||||
-DVCPKG_MANIFEST_FEATURES="imgui;tests"
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: |
|
||||
cmake --build cmake-build/build/${{ matrix.preset }} --target unit_tests omath
|
||||
|
||||
- name: Upload logs on failure
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: wasm-build-logs-${{ matrix.triplet }}
|
||||
path: |
|
||||
cmake-build/build/${{ matrix.preset }}/**/*.log
|
||||
${{ env.VCPKG_ROOT }}/buildtrees/**/*.log
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Run WASM Unit Tests
|
||||
run: node out/Release/unit_tests.js
|
||||
|
||||
##############################################################################
|
||||
# 8) Windows MSYS2 MinGW – GCC / Ninja
|
||||
##############################################################################
|
||||
mingw-build-and-test:
|
||||
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
|
||||
- name: UCRT64 (MSYS2) (x64-mingw-dynamic)
|
||||
msystem: UCRT64
|
||||
pkg_prefix: mingw-w64-ucrt-x86_64
|
||||
preset: mingw-release-vcpkg
|
||||
- name: MINGW32 (MSYS2) (x86-mingw-dynamic)
|
||||
msystem: MINGW32
|
||||
pkg_prefix: mingw-w64-i686
|
||||
preset: mingw32-release-vcpkg
|
||||
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=ON \
|
||||
-DOMATH_BUILD_BENCHMARK=OFF \
|
||||
-DVCPKG_MANIFEST_FEATURES="imgui;tests"
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
cmake --build cmake-build/build/${{ matrix.preset }} --target unit_tests omath
|
||||
|
||||
- name: Run unit_tests.exe
|
||||
run: |
|
||||
./out/Release/unit_tests.exe
|
||||
|
||||
- name: Upload logs on failure
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: mingw-build-logs-${{ matrix.msystem }}
|
||||
path: |
|
||||
cmake-build/build/${{ matrix.preset }}/**/*.log
|
||||
${{ env.VCPKG_ROOT }}/buildtrees/**/*.log
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -2,4 +2,7 @@
|
||||
/out
|
||||
*.DS_Store
|
||||
/extlibs/vcpkg
|
||||
.idea/workspace.xml
|
||||
.idea/workspace.xml
|
||||
/build/
|
||||
/clang-coverage/
|
||||
*.gcov
|
||||
2
.idea/omath.iml
generated
2
.idea/omath.iml
generated
@@ -1,2 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module classpath="CMake" type="CPP_MODULE" version="4" />
|
||||
<module classpath="CIDR" type="CPP_MODULE" version="4" />
|
||||
@@ -5,6 +5,7 @@ project(omath VERSION ${OMATH_VERSION} LANGUAGES CXX)
|
||||
|
||||
include(CMakePackageConfigHelpers)
|
||||
include(CheckCXXCompilerFlag)
|
||||
include(cmake/Coverage.cmake)
|
||||
|
||||
if (MSVC)
|
||||
check_cxx_compiler_flag("/arch:AVX2" COMPILER_SUPPORTS_AVX2)
|
||||
@@ -23,7 +24,7 @@ option(OMATH_STATIC_MSVC_RUNTIME_LIBRARY "Force Omath to link static runtime" OF
|
||||
option(OMATH_SUPRESS_SAFETY_CHECKS "Supress some safety checks in release build to improve general performance" ON)
|
||||
option(OMATH_USE_UNITY_BUILD "Will enable unity build to speed up compilation" OFF)
|
||||
option(OMATH_ENABLE_LEGACY "Will enable legacy classes that MUST be used ONLY for backward compatibility" ON)
|
||||
|
||||
option(OMATH_ENABLE_COVERAGE "Enable coverage" OFF)
|
||||
|
||||
if (VCPKG_MANIFEST_FEATURES)
|
||||
foreach (omath_feature IN LISTS VCPKG_MANIFEST_FEATURES)
|
||||
@@ -35,6 +36,8 @@ if (VCPKG_MANIFEST_FEATURES)
|
||||
set(OMATH_BUILD_TESTS ON)
|
||||
elseif (omath_feature STREQUAL "benchmark")
|
||||
set(OMATH_BUILD_BENCHMARK ON)
|
||||
elseif (omath_feature STREQUAL "examples")
|
||||
set(OMATH_BUILD_EXAMPLES ON)
|
||||
endif ()
|
||||
|
||||
endforeach ()
|
||||
@@ -133,11 +136,19 @@ if (OMATH_USE_AVX2)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
if(EMSCRIPTEN)
|
||||
target_compile_options(${PROJECT_NAME} PUBLIC -fexceptions)
|
||||
target_link_options(${PROJECT_NAME} PUBLIC -fexceptions)
|
||||
endif()
|
||||
|
||||
target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_23)
|
||||
|
||||
if (OMATH_BUILD_TESTS)
|
||||
add_subdirectory(tests)
|
||||
target_compile_definitions(${PROJECT_NAME} PUBLIC OMATH_BUILD_TESTS)
|
||||
if(OMATH_ENABLE_COVERAGE)
|
||||
omath_setup_coverage(${PROJECT_NAME})
|
||||
endif()
|
||||
endif ()
|
||||
|
||||
if (OMATH_BUILD_BENCHMARK)
|
||||
@@ -148,6 +159,7 @@ if (OMATH_BUILD_EXAMPLES)
|
||||
add_subdirectory(examples)
|
||||
endif ()
|
||||
|
||||
|
||||
if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC" AND OMATH_THREAT_WARNING_AS_ERROR)
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE /W4 /WX)
|
||||
elseif (OMATH_THREAT_WARNING_AS_ERROR)
|
||||
@@ -186,7 +198,6 @@ install(EXPORT ${PROJECT_NAME}Targets
|
||||
DESTINATION lib/cmake/${PROJECT_NAME} COMPONENT ${PROJECT_NAME}
|
||||
)
|
||||
|
||||
|
||||
# Generate the omathConfigVersion.cmake file
|
||||
write_basic_package_version_file(
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/omathConfigVersion.cmake"
|
||||
|
||||
@@ -1,15 +1,49 @@
|
||||
{
|
||||
"version": 3,
|
||||
"version": 6,
|
||||
"cmakeMinimumRequired": {
|
||||
"major": 3,
|
||||
"minor": 25,
|
||||
"patch": 0
|
||||
},
|
||||
|
||||
"configurePresets": [
|
||||
{
|
||||
"name": "windows-base",
|
||||
"name": "base",
|
||||
"hidden": true,
|
||||
"generator": "Ninja",
|
||||
"binaryDir": "${sourceDir}/cmake-build/build/${presetName}",
|
||||
"installDir": "${sourceDir}/cmake-build/install/${presetName}",
|
||||
"installDir": "${sourceDir}/cmake-build/install/${presetName}"
|
||||
},
|
||||
{
|
||||
"name": "vcpkg-base",
|
||||
"hidden": true,
|
||||
"cacheVariables": {
|
||||
"CMAKE_CXX_COMPILER": "cl.exe",
|
||||
"CMAKE_MAKE_PROGRAM": "Ninja"
|
||||
"OMATH_BUILD_VIA_VCPKG": "ON",
|
||||
"CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
|
||||
"VCPKG_INSTALLED_DIR": "${sourceDir}/cmake-build/vcpkg_installed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "debug",
|
||||
"hidden": true,
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Debug"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "release",
|
||||
"hidden": true,
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Release"
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"name": "windows-base",
|
||||
"hidden": true,
|
||||
"inherits": "base",
|
||||
"cacheVariables": {
|
||||
"CMAKE_CXX_COMPILER": "cl.exe"
|
||||
},
|
||||
"condition": {
|
||||
"type": "equals",
|
||||
@@ -18,59 +52,88 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "windows-base-vcpkg",
|
||||
"name": "windows-vcpkg-base",
|
||||
"hidden": true,
|
||||
"inherits": "windows-base",
|
||||
"inherits": ["windows-base", "vcpkg-base"],
|
||||
"cacheVariables": {
|
||||
"OMATH_BUILD_VIA_VCPKG": "ON",
|
||||
"CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
|
||||
"VCPKG_INSTALLED_DIR": "${sourceDir}/cmake-build/vcpkg_installed",
|
||||
"VCPKG_MANIFEST_FEATURES": "tests;imgui;avx2"
|
||||
"VCPKG_MANIFEST_FEATURES": "tests;imgui;avx2;examples"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "windows-debug",
|
||||
"displayName": "Debug",
|
||||
"inherits": "windows-base",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Debug"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "windows-debug-vcpkg",
|
||||
"displayName": "Debug",
|
||||
"inherits": "windows-base-vcpkg",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Debug"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "windows-release-vcpkg",
|
||||
"displayName": "Release",
|
||||
"inherits": "windows-base-vcpkg",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Release",
|
||||
"OMATH_BUILD_VIA_VCPKG": "ON"
|
||||
}
|
||||
"displayName": "Windows Debug",
|
||||
"inherits": ["windows-base", "debug"]
|
||||
},
|
||||
{
|
||||
"name": "windows-release",
|
||||
"displayName": "Release",
|
||||
"inherits": "windows-base",
|
||||
"displayName": "Windows Release",
|
||||
"inherits": ["windows-base", "release"]
|
||||
},
|
||||
{
|
||||
"name": "windows-debug-vcpkg",
|
||||
"displayName": "Windows Debug (vcpkg)",
|
||||
"inherits": ["windows-vcpkg-base", "debug"]
|
||||
},
|
||||
{
|
||||
"name": "windows-release-vcpkg",
|
||||
"displayName": "Windows Release (vcpkg)",
|
||||
"inherits": ["windows-vcpkg-base", "release"]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "windows-x86-vcpkg-base",
|
||||
"hidden": true,
|
||||
"inherits": ["windows-base", "vcpkg-base"],
|
||||
"architecture": {
|
||||
"value": "x86",
|
||||
"strategy": "external"
|
||||
},
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Release"
|
||||
"VCPKG_TARGET_TRIPLET": "x86-windows",
|
||||
"VCPKG_HOST_TRIPLET": "x64-windows",
|
||||
"VCPKG_MANIFEST_FEATURES": "tests;imgui;avx2;examples"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "windows-debug-vcpkg-x86",
|
||||
"displayName": "Windows x86 Debug (vcpkg)",
|
||||
"inherits": ["windows-x86-vcpkg-base", "debug"]
|
||||
},
|
||||
{
|
||||
"name": "windows-release-vcpkg-x86",
|
||||
"displayName": "Windows x86 Release (vcpkg)",
|
||||
"inherits": ["windows-x86-vcpkg-base", "release"]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "windows-arm64-vcpkg-base",
|
||||
"hidden": true,
|
||||
"inherits": ["windows-base", "vcpkg-base"],
|
||||
"architecture": {
|
||||
"value": "arm64",
|
||||
"strategy": "external"
|
||||
},
|
||||
"cacheVariables": {
|
||||
"VCPKG_TARGET_TRIPLET": "arm64-windows",
|
||||
"VCPKG_HOST_TRIPLET": "arm64-windows",
|
||||
"VCPKG_MANIFEST_FEATURES": "tests;imgui;examples"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "windows-debug-vcpkg-arm64",
|
||||
"displayName": "Windows ARM64 Debug (vcpkg)",
|
||||
"inherits": ["windows-arm64-vcpkg-base", "debug"]
|
||||
},
|
||||
{
|
||||
"name": "windows-release-vcpkg-arm64",
|
||||
"displayName": "Windows ARM64 Release (vcpkg)",
|
||||
"inherits": ["windows-arm64-vcpkg-base", "release"]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "linux-base",
|
||||
"hidden": true,
|
||||
"generator": "Ninja",
|
||||
"binaryDir": "${sourceDir}/cmake-build/build/${presetName}",
|
||||
"installDir": "${sourceDir}/cmake-build/install/${presetName}",
|
||||
"cacheVariables": {
|
||||
"CMAKE_CXX_COMPILER": "clang++",
|
||||
"CMAKE_MAKE_PROGRAM": "ninja"
|
||||
},
|
||||
"inherits": "base",
|
||||
"condition": {
|
||||
"type": "equals",
|
||||
"lhs": "${hostSystemName}",
|
||||
@@ -78,57 +141,88 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "linux-base-vcpkg",
|
||||
"name": "linux-vcpkg-base",
|
||||
"hidden": true,
|
||||
"inherits": "linux-base",
|
||||
"inherits": ["linux-base", "vcpkg-base"],
|
||||
"cacheVariables": {
|
||||
"OMATH_BUILD_VIA_VCPKG": "ON",
|
||||
"CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
|
||||
"VCPKG_INSTALLED_DIR": "${sourceDir}/cmake-build/vcpkg_installed",
|
||||
"VCPKG_MANIFEST_FEATURES": "tests;imgui;avx2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "linux-debug",
|
||||
"displayName": "Linux Debug",
|
||||
"inherits": "linux-base",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Debug"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "linux-debug-vcpkg",
|
||||
"displayName": "Linux Debug",
|
||||
"inherits": "linux-base-vcpkg",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Debug"
|
||||
}
|
||||
"inherits": ["linux-base", "debug"]
|
||||
},
|
||||
{
|
||||
"name": "linux-release",
|
||||
"displayName": "Linux Release",
|
||||
"inherits": "linux-debug",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Release"
|
||||
}
|
||||
"inherits": ["linux-base", "release"]
|
||||
},
|
||||
{
|
||||
"name": "linux-debug-vcpkg",
|
||||
"displayName": "Linux Debug (vcpkg)",
|
||||
"inherits": ["linux-vcpkg-base", "debug"]
|
||||
},
|
||||
{
|
||||
"name": "linux-release-vcpkg",
|
||||
"displayName": "Linux Release",
|
||||
"inherits": "linux-base-vcpkg",
|
||||
"displayName": "Linux Release (vcpkg)",
|
||||
"inherits": ["linux-vcpkg-base", "release"]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "linux-x86-vcpkg-base",
|
||||
"hidden": true,
|
||||
"inherits": ["linux-base", "vcpkg-base"],
|
||||
"architecture": {
|
||||
"value": "x86",
|
||||
"strategy": "external"
|
||||
},
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Release"
|
||||
"CMAKE_C_FLAGS": "-m32",
|
||||
"CMAKE_CXX_FLAGS": "-m32",
|
||||
"VCPKG_TARGET_TRIPLET": "x86-linux",
|
||||
"VCPKG_HOST_TRIPLET": "x64-linux",
|
||||
"VCPKG_MANIFEST_FEATURES": "tests;imgui"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "linux-debug-vcpkg-x86",
|
||||
"displayName": "Linux x86 Debug (vcpkg)",
|
||||
"inherits": ["linux-x86-vcpkg-base", "debug"]
|
||||
},
|
||||
{
|
||||
"name": "linux-release-vcpkg-x86",
|
||||
"displayName": "Linux x86 Release (vcpkg)",
|
||||
"inherits": ["linux-x86-vcpkg-base", "release"]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "linux-arm64-vcpkg-base",
|
||||
"hidden": true,
|
||||
"inherits": ["linux-base", "vcpkg-base"],
|
||||
"cacheVariables": {
|
||||
"VCPKG_TARGET_TRIPLET": "arm64-linux",
|
||||
"VCPKG_HOST_TRIPLET": "arm64-linux",
|
||||
"VCPKG_MANIFEST_FEATURES": "tests;imgui"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "linux-debug-vcpkg-arm64",
|
||||
"displayName": "Linux ARM64 Debug (vcpkg)",
|
||||
"inherits": ["linux-arm64-vcpkg-base", "debug"]
|
||||
},
|
||||
{
|
||||
"name": "linux-release-vcpkg-arm64",
|
||||
"displayName": "Linux ARM64 Release (vcpkg)",
|
||||
"inherits": ["linux-arm64-vcpkg-base", "release"]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "darwin-base",
|
||||
"hidden": true,
|
||||
"generator": "Ninja",
|
||||
"binaryDir": "${sourceDir}/cmake-build/build/${presetName}",
|
||||
"installDir": "${sourceDir}/cmake-build/install/${presetName}",
|
||||
"inherits": "base",
|
||||
"cacheVariables": {
|
||||
"CMAKE_CXX_COMPILER": "clang++",
|
||||
"CMAKE_MAKE_PROGRAM": "ninja"
|
||||
"CMAKE_CXX_COMPILER": "clang++"
|
||||
},
|
||||
"condition": {
|
||||
"type": "equals",
|
||||
@@ -137,47 +231,456 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "darwin-base-vcpkg",
|
||||
"name": "darwin-vcpkg-base",
|
||||
"hidden": true,
|
||||
"inherits": "darwin-base",
|
||||
"inherits": ["darwin-base", "vcpkg-base"],
|
||||
"cacheVariables": {
|
||||
"OMATH_BUILD_VIA_VCPKG": "ON",
|
||||
"CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
|
||||
"VCPKG_INSTALLED_DIR": "${sourceDir}/cmake-build/vcpkg_installed",
|
||||
"VCPKG_MANIFEST_FEATURES": "tests;imgui;avx2"
|
||||
"VCPKG_MANIFEST_FEATURES": "tests;imgui;avx2;examples"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "darwin-debug",
|
||||
"displayName": "Darwin Debug",
|
||||
"inherits": "darwin-base",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Debug"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "darwin-debug-vcpkg",
|
||||
"displayName": "Darwin Debug",
|
||||
"inherits": "darwin-base-vcpkg",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Debug"
|
||||
}
|
||||
"displayName": "macOS Debug",
|
||||
"inherits": ["darwin-base", "debug"]
|
||||
},
|
||||
{
|
||||
"name": "darwin-release",
|
||||
"displayName": "Darwin Release",
|
||||
"inherits": "darwin-base",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Release"
|
||||
}
|
||||
"displayName": "macOS Release",
|
||||
"inherits": ["darwin-base", "release"]
|
||||
},
|
||||
{
|
||||
"name": "darwin-debug-vcpkg",
|
||||
"displayName": "macOS Debug (vcpkg)",
|
||||
"inherits": ["darwin-vcpkg-base", "debug"]
|
||||
},
|
||||
{
|
||||
"name": "darwin-release-vcpkg",
|
||||
"displayName": "Darwin Release",
|
||||
"inherits": "darwin-base-vcpkg",
|
||||
"displayName": "macOS Release (vcpkg)",
|
||||
"inherits": ["darwin-vcpkg-base", "release"]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "darwin-x64-vcpkg-base",
|
||||
"hidden": true,
|
||||
"inherits": ["darwin-base", "vcpkg-base"],
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Release"
|
||||
"CMAKE_OSX_ARCHITECTURES": "x86_64",
|
||||
"VCPKG_TARGET_TRIPLET": "x64-osx",
|
||||
"VCPKG_HOST_TRIPLET": "x64-osx",
|
||||
"VCPKG_MANIFEST_FEATURES": "tests;imgui;avx2;examples"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "darwin-debug-vcpkg-x64",
|
||||
"displayName": "macOS x64 Debug (vcpkg)",
|
||||
"inherits": ["darwin-x64-vcpkg-base", "debug"]
|
||||
},
|
||||
{
|
||||
"name": "darwin-release-vcpkg-x64",
|
||||
"displayName": "macOS x64 Release (vcpkg)",
|
||||
"inherits": ["darwin-x64-vcpkg-base", "release"]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "ios-base",
|
||||
"hidden": true,
|
||||
"inherits": "base",
|
||||
"cacheVariables": {
|
||||
"CMAKE_SYSTEM_NAME": "iOS",
|
||||
"CMAKE_OSX_DEPLOYMENT_TARGET": "18.5",
|
||||
"CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED": "NO",
|
||||
"CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED": "NO"
|
||||
},
|
||||
"condition": {
|
||||
"type": "equals",
|
||||
"lhs": "${hostSystemName}",
|
||||
"rhs": "Darwin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ios-vcpkg-base",
|
||||
"hidden": true,
|
||||
"inherits": ["ios-base", "vcpkg-base"],
|
||||
"cacheVariables": {
|
||||
"VCPKG_TARGET_TRIPLET": "arm64-ios",
|
||||
"VCPKG_HOST_TRIPLET": "arm64-osx",
|
||||
"VCPKG_MANIFEST_FEATURES": "tests;imgui"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ios-debug-vcpkg",
|
||||
"displayName": "iOS Debug (vcpkg)",
|
||||
"inherits": ["ios-vcpkg-base", "debug"]
|
||||
},
|
||||
{
|
||||
"name": "ios-release-vcpkg",
|
||||
"displayName": "iOS Release (vcpkg)",
|
||||
"inherits": ["ios-vcpkg-base", "release"]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "freebsd-base",
|
||||
"hidden": true,
|
||||
"inherits": "base",
|
||||
"cacheVariables": {
|
||||
"CMAKE_C_COMPILER": "clang",
|
||||
"CMAKE_CXX_COMPILER": "clang++"
|
||||
},
|
||||
"condition": {
|
||||
"type": "equals",
|
||||
"lhs": "${hostSystemName}",
|
||||
"rhs": "FreeBSD"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "freebsd-vcpkg-base",
|
||||
"hidden": true,
|
||||
"inherits": ["freebsd-base", "vcpkg-base"],
|
||||
"cacheVariables": {
|
||||
"VCPKG_MANIFEST_FEATURES": "tests;imgui;avx2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "freebsd-debug",
|
||||
"displayName": "FreeBSD Debug",
|
||||
"inherits": ["freebsd-base", "debug"]
|
||||
},
|
||||
{
|
||||
"name": "freebsd-release",
|
||||
"displayName": "FreeBSD Release",
|
||||
"inherits": ["freebsd-base", "release"]
|
||||
},
|
||||
{
|
||||
"name": "freebsd-debug-vcpkg",
|
||||
"displayName": "FreeBSD Debug (vcpkg)",
|
||||
"inherits": ["freebsd-vcpkg-base", "debug"]
|
||||
},
|
||||
{
|
||||
"name": "freebsd-release-vcpkg",
|
||||
"displayName": "FreeBSD Release (vcpkg)",
|
||||
"inherits": ["freebsd-vcpkg-base", "release"]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "android-base",
|
||||
"hidden": true,
|
||||
"inherits": "base",
|
||||
"cacheVariables": {
|
||||
"CMAKE_SYSTEM_NAME": "Android",
|
||||
"CMAKE_SYSTEM_VERSION": "24",
|
||||
"CMAKE_ANDROID_NDK": "$env{ANDROID_NDK_HOME}",
|
||||
"CMAKE_ANDROID_STL_TYPE": "c++_static"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "android-vcpkg-base",
|
||||
"hidden": true,
|
||||
"inherits": ["android-base", "vcpkg-base"],
|
||||
"cacheVariables": {
|
||||
"VCPKG_MANIFEST_FEATURES": "tests;imgui"
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"name": "android-arm64-base",
|
||||
"hidden": true,
|
||||
"inherits": "android-base",
|
||||
"cacheVariables": {
|
||||
"CMAKE_ANDROID_ARCH_ABI": "arm64-v8a"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "android-arm64-vcpkg-base",
|
||||
"hidden": true,
|
||||
"inherits": "android-vcpkg-base",
|
||||
"cacheVariables": {
|
||||
"CMAKE_ANDROID_ARCH_ABI": "arm64-v8a",
|
||||
"VCPKG_TARGET_TRIPLET": "arm64-android"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "android-arm64-debug",
|
||||
"displayName": "Android arm64-v8a Debug",
|
||||
"inherits": ["android-arm64-base", "debug"]
|
||||
},
|
||||
{
|
||||
"name": "android-arm64-release",
|
||||
"displayName": "Android arm64-v8a Release",
|
||||
"inherits": ["android-arm64-base", "release"]
|
||||
},
|
||||
{
|
||||
"name": "android-arm64-debug-vcpkg",
|
||||
"displayName": "Android arm64-v8a Debug (vcpkg)",
|
||||
"inherits": ["android-arm64-vcpkg-base", "debug"]
|
||||
},
|
||||
{
|
||||
"name": "android-arm64-release-vcpkg",
|
||||
"displayName": "Android arm64-v8a Release (vcpkg)",
|
||||
"inherits": ["android-arm64-vcpkg-base", "release"]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "android-arm-neon-base",
|
||||
"hidden": true,
|
||||
"inherits": "android-base",
|
||||
"cacheVariables": {
|
||||
"CMAKE_ANDROID_ARCH_ABI": "armeabi-v7a",
|
||||
"CMAKE_ANDROID_ARM_NEON": "ON"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "android-arm-neon-vcpkg-base",
|
||||
"hidden": true,
|
||||
"inherits": "android-vcpkg-base",
|
||||
"cacheVariables": {
|
||||
"CMAKE_ANDROID_ARCH_ABI": "armeabi-v7a",
|
||||
"CMAKE_ANDROID_ARM_NEON": "ON",
|
||||
"VCPKG_TARGET_TRIPLET": "arm-neon-android"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "android-arm-neon-debug",
|
||||
"displayName": "Android armeabi-v7a NEON Debug",
|
||||
"inherits": ["android-arm-neon-base", "debug"]
|
||||
},
|
||||
{
|
||||
"name": "android-arm-neon-release",
|
||||
"displayName": "Android armeabi-v7a NEON Release",
|
||||
"inherits": ["android-arm-neon-base", "release"]
|
||||
},
|
||||
{
|
||||
"name": "android-arm-neon-debug-vcpkg",
|
||||
"displayName": "Android armeabi-v7a NEON Debug (vcpkg)",
|
||||
"inherits": ["android-arm-neon-vcpkg-base", "debug"]
|
||||
},
|
||||
{
|
||||
"name": "android-arm-neon-release-vcpkg",
|
||||
"displayName": "Android armeabi-v7a NEON Release (vcpkg)",
|
||||
"inherits": ["android-arm-neon-vcpkg-base", "release"]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "android-x64-base",
|
||||
"hidden": true,
|
||||
"inherits": "android-base",
|
||||
"cacheVariables": {
|
||||
"CMAKE_ANDROID_ARCH_ABI": "x86_64"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "android-x64-vcpkg-base",
|
||||
"hidden": true,
|
||||
"inherits": "android-vcpkg-base",
|
||||
"cacheVariables": {
|
||||
"CMAKE_ANDROID_ARCH_ABI": "x86_64",
|
||||
"VCPKG_TARGET_TRIPLET": "x64-android"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "android-x64-debug",
|
||||
"displayName": "Android x86_64 Debug",
|
||||
"inherits": ["android-x64-base", "debug"]
|
||||
},
|
||||
{
|
||||
"name": "android-x64-release",
|
||||
"displayName": "Android x86_64 Release",
|
||||
"inherits": ["android-x64-base", "release"]
|
||||
},
|
||||
{
|
||||
"name": "android-x64-debug-vcpkg",
|
||||
"displayName": "Android x86_64 Debug (vcpkg)",
|
||||
"inherits": ["android-x64-vcpkg-base", "debug"]
|
||||
},
|
||||
{
|
||||
"name": "android-x64-release-vcpkg",
|
||||
"displayName": "Android x86_64 Release (vcpkg)",
|
||||
"inherits": ["android-x64-vcpkg-base", "release"]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "android-x86-base",
|
||||
"hidden": true,
|
||||
"inherits": "android-base",
|
||||
"cacheVariables": {
|
||||
"CMAKE_ANDROID_ARCH_ABI": "x86"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "android-x86-vcpkg-base",
|
||||
"hidden": true,
|
||||
"inherits": "android-vcpkg-base",
|
||||
"cacheVariables": {
|
||||
"CMAKE_ANDROID_ARCH_ABI": "x86",
|
||||
"VCPKG_TARGET_TRIPLET": "x86-android"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "android-x86-debug",
|
||||
"displayName": "Android x86 Debug",
|
||||
"inherits": ["android-x86-base", "debug"]
|
||||
},
|
||||
{
|
||||
"name": "android-x86-release",
|
||||
"displayName": "Android x86 Release",
|
||||
"inherits": ["android-x86-base", "release"]
|
||||
},
|
||||
{
|
||||
"name": "android-x86-debug-vcpkg",
|
||||
"displayName": "Android x86 Debug (vcpkg)",
|
||||
"inherits": ["android-x86-vcpkg-base", "debug"]
|
||||
},
|
||||
{
|
||||
"name": "android-x86-release-vcpkg",
|
||||
"displayName": "Android x86 Release (vcpkg)",
|
||||
"inherits": ["android-x86-vcpkg-base", "release"]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "android-debug",
|
||||
"displayName": "Android Debug (default: arm64)",
|
||||
"inherits": "android-arm64-debug"
|
||||
},
|
||||
{
|
||||
"name": "android-release",
|
||||
"displayName": "Android Release (default: arm64)",
|
||||
"inherits": "android-arm64-release"
|
||||
},
|
||||
{
|
||||
"name": "android-debug-vcpkg",
|
||||
"displayName": "Android Debug (default: arm64, vcpkg)",
|
||||
"inherits": "android-arm64-debug-vcpkg"
|
||||
},
|
||||
{
|
||||
"name": "android-release-vcpkg",
|
||||
"displayName": "Android Release (default: arm64, vcpkg)",
|
||||
"inherits": "android-arm64-release-vcpkg"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "wasm-base",
|
||||
"hidden": true,
|
||||
"inherits": "base"
|
||||
},
|
||||
{
|
||||
"name": "wasm-vcpkg-base",
|
||||
"hidden": true,
|
||||
"inherits": ["wasm-base", "vcpkg-base"],
|
||||
"cacheVariables": {
|
||||
"VCPKG_CHAINLOAD_TOOLCHAIN_FILE": "$env{EMSDK}/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake",
|
||||
"VCPKG_TARGET_TRIPLET": "wasm32-emscripten",
|
||||
"VCPKG_MANIFEST_FEATURES": "tests;imgui"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "wasm-debug-vcpkg",
|
||||
"displayName": "WebAssembly Debug (vcpkg)",
|
||||
"inherits": ["wasm-vcpkg-base", "debug"]
|
||||
},
|
||||
{
|
||||
"name": "wasm-release-vcpkg",
|
||||
"displayName": "WebAssembly Release (vcpkg)",
|
||||
"inherits": ["wasm-vcpkg-base", "release"]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "mingw-base",
|
||||
"hidden": true,
|
||||
"inherits": "base",
|
||||
"cacheVariables": {
|
||||
"CMAKE_C_COMPILER": "gcc",
|
||||
"CMAKE_CXX_COMPILER": "g++"
|
||||
},
|
||||
"condition": {
|
||||
"type": "equals",
|
||||
"lhs": "${hostSystemName}",
|
||||
"rhs": "Windows"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "mingw-vcpkg-base",
|
||||
"hidden": true,
|
||||
"inherits": ["mingw-base", "vcpkg-base"],
|
||||
"environment": {
|
||||
"VCPKG_DEFAULT_HOST_TRIPLET": "x64-mingw-dynamic"
|
||||
},
|
||||
"cacheVariables": {
|
||||
"VCPKG_TARGET_TRIPLET": "x64-mingw-dynamic",
|
||||
"VCPKG_HOST_TRIPLET": "x64-mingw-dynamic",
|
||||
"VCPKG_MANIFEST_FEATURES": "tests;imgui"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "mingw-debug",
|
||||
"displayName": "MinGW x64 Debug",
|
||||
"inherits": ["mingw-base", "debug"]
|
||||
},
|
||||
{
|
||||
"name": "mingw-release",
|
||||
"displayName": "MinGW x64 Release",
|
||||
"inherits": ["mingw-base", "release"]
|
||||
},
|
||||
{
|
||||
"name": "mingw-debug-vcpkg",
|
||||
"displayName": "MinGW x64 Debug (vcpkg)",
|
||||
"inherits": ["mingw-vcpkg-base", "debug"]
|
||||
},
|
||||
{
|
||||
"name": "mingw-release-vcpkg",
|
||||
"displayName": "MinGW x64 Release (vcpkg)",
|
||||
"inherits": ["mingw-vcpkg-base", "release"]
|
||||
},
|
||||
{
|
||||
"name": "mingw-ucrt-release-vcpkg",
|
||||
"displayName": "MinGW UCRT64 Release (vcpkg)",
|
||||
"inherits": ["mingw-vcpkg-base", "release"]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "mingw32-base",
|
||||
"hidden": true,
|
||||
"inherits": "base",
|
||||
"cacheVariables": {
|
||||
"CMAKE_C_COMPILER": "gcc",
|
||||
"CMAKE_CXX_COMPILER": "g++"
|
||||
},
|
||||
"condition": {
|
||||
"type": "equals",
|
||||
"lhs": "${hostSystemName}",
|
||||
"rhs": "Windows"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "mingw32-vcpkg-base",
|
||||
"hidden": true,
|
||||
"inherits": ["mingw32-base", "vcpkg-base"],
|
||||
"environment": {
|
||||
"VCPKG_DEFAULT_HOST_TRIPLET": "x86-mingw-dynamic"
|
||||
},
|
||||
"cacheVariables": {
|
||||
"VCPKG_TARGET_TRIPLET": "x86-mingw-dynamic",
|
||||
"VCPKG_HOST_TRIPLET": "x86-mingw-dynamic",
|
||||
"VCPKG_MANIFEST_FEATURES": "tests;imgui"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "mingw32-debug",
|
||||
"displayName": "MinGW x86 Debug",
|
||||
"inherits": ["mingw32-base", "debug"]
|
||||
},
|
||||
{
|
||||
"name": "mingw32-release",
|
||||
"displayName": "MinGW x86 Release",
|
||||
"inherits": ["mingw32-base", "release"]
|
||||
},
|
||||
{
|
||||
"name": "mingw32-debug-vcpkg",
|
||||
"displayName": "MinGW x86 Debug (vcpkg)",
|
||||
"inherits": ["mingw32-vcpkg-base", "debug"]
|
||||
},
|
||||
{
|
||||
"name": "mingw32-release-vcpkg",
|
||||
"displayName": "MinGW x86 Release (vcpkg)",
|
||||
"inherits": ["mingw32-vcpkg-base", "release"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,93 +1,201 @@
|
||||
## 🎯 Goal
|
||||
# UNIVERSAL DECLARATION OF CODE OF CONDUCT
|
||||
_Declaration of Community Rights and Responsibilities_
|
||||
|
||||
My goal is to provide a space where it is safe for everyone to contribute to,
|
||||
and get support for, open-source software in a respectful and cooperative
|
||||
manner.
|
||||
## Preamble
|
||||
|
||||
I value all contributions and want to make this project and its
|
||||
surrounding community a place for everyone.
|
||||
Whereas the Orange++ community is founded on cooperation, mutual respect and support for the development of open-source software;
|
||||
|
||||
As members, contributors, and everyone else who may participate in the
|
||||
development, I strive to keep the entire experience civil.
|
||||
Whereas it is essential that all participants can contribute and seek assistance in an environment that is safe, inclusive and free from discrimination and harassment;
|
||||
|
||||
## 📜 Standards
|
||||
Whereas the dignity and equality of all participants, regardless of their traits or background, must be respected and protected;
|
||||
|
||||
Our community standards exist in order to make sure everyone feels comfortable
|
||||
contributing to the project(s) together.
|
||||
Now, therefore, this Community Code of Conduct is proclaimed as a common standard of behaviour for all members, contributors and participants in projects led by Orange++ and its official communities.
|
||||
|
||||
Our standards are:
|
||||
- Do not harass, attack, or in any other way discriminate against anyone, including
|
||||
for their protected traits, including, but not limited to, sex, religion, race,
|
||||
appearance, gender, identity, nationality, sexuality, etc.
|
||||
- Do not go off-topic, do not post spam.
|
||||
- Treat everyone with respect.
|
||||
---
|
||||
|
||||
Examples of breaking each rule respectively include:
|
||||
- Harassment, bullying or inappropriate jokes about another person.
|
||||
- Posting distasteful imagery, trolling, or posting things unrelated to the topic at hand.
|
||||
- Treating someone as worse because of their lack of understanding of an issue.
|
||||
## Article 1
|
||||
|
||||
## ⚡ Enforcement
|
||||
This Code of Conduct establishes standards of behaviour intended to:
|
||||
|
||||
Enforcement of this CoC is done by Orange++ and/or other core contributors.
|
||||
1. Provide a safe and welcoming environment for all participants.
|
||||
2. Encourage respectful and constructive collaboration.
|
||||
3. Prevent harassment, discrimination, and other harmful conduct.
|
||||
|
||||
I, as the core developer, will strive my best to keep this community civil and
|
||||
following the standards outlined above.
|
||||
All individuals who participate in Orange++ projects or official communities, whether online or offline, are expected to adhere to this Code of Conduct.
|
||||
|
||||
### 🚩 Reporting incidents
|
||||
---
|
||||
|
||||
If you believe an incident of breaking these standards has occurred, but nobody has
|
||||
taken appropriate action, you can privately contact the people responsible for dealing
|
||||
with such incidents in multiple ways:
|
||||
## Article 2
|
||||
|
||||
All participants are equal in dignity and rights within the community.
|
||||
|
||||
No person shall be harassed, attacked, or discriminated against on the basis of protected or personal traits, including but not limited to:
|
||||
|
||||
- sex;
|
||||
- religion or belief;
|
||||
- race or ethnicity;
|
||||
- appearance;
|
||||
- gender or gender identity;
|
||||
- nationality;
|
||||
- sexual orientation;
|
||||
- or any other similar characteristic.
|
||||
|
||||
Treating someone as lesser or unworthy because of their knowledge, experience, or level of understanding of an issue is incompatible with this Code.
|
||||
|
||||
---
|
||||
|
||||
## Article 3
|
||||
|
||||
Participants shall treat one another with respect at all times.
|
||||
|
||||
Participants shall:
|
||||
|
||||
1. Engage in discussion in good faith and assume good intent where reasonable.
|
||||
2. Provide feedback and criticism in a constructive and considerate manner.
|
||||
3. Recognize that people have different backgrounds, perspectives, and levels of expertise.
|
||||
|
||||
Examples of conduct contrary to this Article include, but are not limited to:
|
||||
|
||||
- harassment, bullying, personal attacks or degrading comments;
|
||||
- inappropriate or offensive jokes or remarks about another person;
|
||||
- persistent disruption of discussions or activities.
|
||||
|
||||
---
|
||||
|
||||
## Article 4
|
||||
|
||||
Participants shall remain on topic and avoid posting spam or irrelevant material.
|
||||
|
||||
Content that is distasteful, deliberately inflammatory, or unrelated to the project or discussion at hand is prohibited.
|
||||
|
||||
Examples of prohibited conduct under this Article include:
|
||||
|
||||
- posting trolling or inflammatory messages;
|
||||
- sharing disturbing or inappropriate imagery unrelated to the topic;
|
||||
- repeatedly derailing conversations away from their intended purpose.
|
||||
|
||||
---
|
||||
|
||||
## Article 5
|
||||
|
||||
The following standards shall guide all participation in Orange++ projects and official communities:
|
||||
|
||||
1. Do not harass, attack, or discriminate against any person.
|
||||
2. Do not go off-topic and do not post spam.
|
||||
3. Treat all participants with respect.
|
||||
|
||||
These standards apply equally to maintainers, contributors, and all other participants, regardless of status or seniority.
|
||||
|
||||
---
|
||||
|
||||
## Article 6
|
||||
|
||||
Enforcement of this Code of Conduct is carried out by Orange++ and/or other core contributors (hereinafter “members”).
|
||||
|
||||
Members shall strive to:
|
||||
|
||||
1. Act fairly, consistently, and transparently.
|
||||
2. Consider the context and severity of each incident.
|
||||
3. Maintain a civil and welcoming environment for the community as a whole.
|
||||
|
||||
Where appropriate, members may consult individuals with relevant lived experience, particularly when an incident concerns a marginalized group, while preserving confidentiality as required by this Code.
|
||||
|
||||
---
|
||||
|
||||
## Article 7
|
||||
|
||||
Any participant who believes that a breach of this Code of Conduct has occurred and has not been appropriately addressed may report the incident privately.
|
||||
|
||||
Reports may be submitted through any of the following channels:
|
||||
|
||||
**E-mail**
|
||||
|
||||
***E-Mail***
|
||||
- `orange-cpp@yandex.ru`
|
||||
|
||||
***Discord***
|
||||
**Discord**
|
||||
|
||||
- `@orange_cpp`
|
||||
|
||||
***Telegram***
|
||||
**Telegram**
|
||||
|
||||
- `@orange_cpp`
|
||||
|
||||
I guarantee your privacy and will not share those reports with anyone.
|
||||
The reporting party’s privacy shall be respected, and reports shall not be shared beyond those responsible for handling them, except where required by law or with the explicit consent of the reporting party.
|
||||
|
||||
## ⚖️ Enforcement Strategy
|
||||
---
|
||||
|
||||
Depending on the severity of the infraction, any action from the list below may be applied.
|
||||
Please keep in mind cases are reviewed on a per-case basis and members are the ultimate
|
||||
deciding factor in the type of punishment.
|
||||
## Article 8
|
||||
|
||||
If the matter benefited from an outside opinion, a member might reach for more opinions
|
||||
from people unrelated, however, the final decision regarding the action
|
||||
to be taken is still up to the member.
|
||||
Depending on the nature and severity of the infraction, and taking into account past behaviour, members may apply one or more of the following measures.
|
||||
|
||||
For example, if the matter at hand regards a representative of a marginalized group or minority,
|
||||
the member might ask for a first-hand opinion from another representative of such group.
|
||||
**1. Correction / Edit**
|
||||
|
||||
### ✏️ Correction/Edit
|
||||
Where a message is misleading, poorly worded, or likely to cause misunderstanding, members may:
|
||||
|
||||
If your message is found to be misleading or poorly worded, a member might
|
||||
edit your message.
|
||||
1. Request that the author clarify or correct the message; or
|
||||
2. Edit the message where the platform permits and such action is appropriate and transparent.
|
||||
|
||||
### ⚠️ Warning/Deletion
|
||||
**2. Warning / Deletion**
|
||||
|
||||
If your message is found inappropriate, a member might give you a public or private warning,
|
||||
and/or delete your message.
|
||||
Where a message is inappropriate or in breach of the standards, members may:
|
||||
|
||||
### 🔇 Mute
|
||||
1. Issue a public or private warning; and/or
|
||||
2. Delete the message.
|
||||
|
||||
If your message is disruptive, or you have been repeatedly violating the standards,
|
||||
a member might mute (or temporarily ban) you.
|
||||
**3. Mute / Temporary Ban**
|
||||
|
||||
### ⛔ Ban
|
||||
Where a participant is repeatedly violating the standards, or where their behaviour is significantly disruptive, members may:
|
||||
|
||||
If your message is hateful, very disruptive, or other, less serious infractions are repeated
|
||||
ignoring previous punishments, a member might ban you permanently.
|
||||
1. Temporarily mute the participant; or
|
||||
2. Temporarily suspend or ban the participant from the community.
|
||||
|
||||
## 🔎 Scope
|
||||
**4. Permanent Ban**
|
||||
|
||||
This CoC shall apply to all projects ran under the Orange++ lead and all _official_ communities
|
||||
outside of GitHub.
|
||||
Where a message is hateful or severely disruptive, or where less serious infractions are repeated despite prior measures, members may permanently ban the participant.
|
||||
|
||||
However, it is worth noting that official communities outside of GitHub might have their own,
|
||||
additional sets of rules.
|
||||
Each case shall be considered individually. The final decision regarding the appropriate measure lies with the members responsible for enforcement.
|
||||
|
||||
---
|
||||
|
||||
## Article 9
|
||||
|
||||
Reports of misconduct and information regarding enforcement actions shall be handled with care and confidentiality.
|
||||
|
||||
The personal data of reporters, witnesses, and involved parties shall not be disclosed to third parties, except:
|
||||
|
||||
1. Where such disclosure is required by law; or
|
||||
2. Where explicit consent has been given by the person concerned.
|
||||
|
||||
The maintainer guarantees that every report will be treated with discretion and respect.
|
||||
|
||||
---
|
||||
|
||||
## Article 10
|
||||
|
||||
This Code of Conduct applies to:
|
||||
|
||||
1. All projects led under the Orange++ name or leadership; and
|
||||
2. All official communities associated with Orange++ outside of GitHub.
|
||||
|
||||
Official communities outside of GitHub may maintain additional rules specific to their platform. In case of overlap, participants are expected to follow:
|
||||
|
||||
1. The rules of the platform or community; and
|
||||
2. This Code of Conduct, insofar as it is applicable.
|
||||
|
||||
---
|
||||
|
||||
## Article 11
|
||||
|
||||
This Code of Conduct shall be interpreted in a manner consistent with its purpose: to promote a safe, respectful and inclusive community.
|
||||
|
||||
The maintainer and core contributors may review and revise this Code of Conduct periodically in light of community needs and experience.
|
||||
|
||||
Significant changes should be communicated to the community in a timely and clear manner.
|
||||
|
||||
---
|
||||
|
||||
## Article 12
|
||||
|
||||
Nothing in this Community Code of Conduct may be interpreted as granting to any maintainer, member, contributor, or participant any right to engage in any activity or to perform any act that aims at undermining, limiting, or destroying the rights, protections, and standards set forth herein.
|
||||
|
||||
No rule, policy, custom, or decision within the Orange++ projects or their official communities may be invoked to justify harassment, discrimination, retaliation, or any other conduct contrary to this Code of Conduct.
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||

|
||||

|
||||

|
||||

|
||||
[](https://www.codefactor.io/repository/github/orange-cpp/omath)
|
||||

|
||||
[](https://repology.org/project/orange-math/versions)
|
||||
@@ -106,6 +107,10 @@ if (auto screen = camera.world_to_screen(world_position)) {
|
||||
|
||||
![TF2 Preview]
|
||||
|
||||
<br>
|
||||
|
||||
![OpenGL Preview]
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
@@ -135,6 +140,7 @@ if (auto screen = camera.world_to_screen(world_position)) {
|
||||
[BO2 Preview]: docs/images/showcase/cod_bo2.png
|
||||
[CS2 Preview]: docs/images/showcase/cs2.jpeg
|
||||
[TF2 Preview]: docs/images/showcase/tf2.jpg
|
||||
[OpenGL Preview]: docs/images/showcase/opengl.png
|
||||
<!----------------------------------{ Buttons }--------------------------------->
|
||||
[QUICKSTART]: docs/getting_started.md
|
||||
[INSTALL]: INSTALL.md
|
||||
|
||||
122
cmake/Coverage.cmake
Normal file
122
cmake/Coverage.cmake
Normal file
@@ -0,0 +1,122 @@
|
||||
# cmake/Coverage.cmake
|
||||
include_guard(GLOBAL)
|
||||
|
||||
function(omath_setup_coverage TARGET_NAME)
|
||||
if(ANDROID OR IOS OR EMSCRIPTEN)
|
||||
return()
|
||||
endif()
|
||||
|
||||
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|AppleClang")
|
||||
# Apply to ALL configs when coverage is enabled (not just Debug)
|
||||
target_compile_options(${TARGET_NAME} PRIVATE
|
||||
-fprofile-instr-generate
|
||||
-fcoverage-mapping
|
||||
-g
|
||||
-O0
|
||||
)
|
||||
target_link_options(${TARGET_NAME} PRIVATE
|
||||
-fprofile-instr-generate
|
||||
-fcoverage-mapping
|
||||
)
|
||||
|
||||
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||
target_compile_options(${TARGET_NAME} PRIVATE
|
||||
--coverage
|
||||
-g
|
||||
-O0
|
||||
)
|
||||
target_link_options(${TARGET_NAME} PRIVATE
|
||||
--coverage
|
||||
)
|
||||
|
||||
elseif(MSVC)
|
||||
target_compile_options(${TARGET_NAME} PRIVATE
|
||||
/Zi
|
||||
/Od
|
||||
/Ob0
|
||||
)
|
||||
target_link_options(${TARGET_NAME} PRIVATE
|
||||
/DEBUG:FULL
|
||||
/INCREMENTAL:NO
|
||||
)
|
||||
endif()
|
||||
|
||||
# Create coverage target only once
|
||||
if(TARGET coverage)
|
||||
return()
|
||||
endif()
|
||||
|
||||
if(MSVC OR MINGW)
|
||||
# Windows: OpenCppCoverage
|
||||
find_program(OPENCPPCOVERAGE_EXECUTABLE
|
||||
NAMES OpenCppCoverage OpenCppCoverage.exe
|
||||
PATHS
|
||||
"$ENV{ProgramFiles}/OpenCppCoverage"
|
||||
"$ENV{ProgramW6432}/OpenCppCoverage"
|
||||
"C:/Program Files/OpenCppCoverage"
|
||||
DOC "Path to OpenCppCoverage executable"
|
||||
)
|
||||
|
||||
if(NOT OPENCPPCOVERAGE_EXECUTABLE)
|
||||
message(WARNING "OpenCppCoverage not found. Install with: choco install opencppcoverage")
|
||||
set(OPENCPPCOVERAGE_EXECUTABLE "C:/Program Files/OpenCppCoverage/OpenCppCoverage.exe")
|
||||
else()
|
||||
message(STATUS "Found OpenCppCoverage: ${OPENCPPCOVERAGE_EXECUTABLE}")
|
||||
endif()
|
||||
|
||||
file(TO_NATIVE_PATH "${CMAKE_SOURCE_DIR}" COVERAGE_ROOT_PATH)
|
||||
file(TO_NATIVE_PATH "${CMAKE_BINARY_DIR}/coverage" COVERAGE_OUTPUT_PATH)
|
||||
file(TO_NATIVE_PATH "${CMAKE_BINARY_DIR}/coverage.xml" COVERAGE_XML_PATH)
|
||||
file(TO_NATIVE_PATH "${OPENCPPCOVERAGE_EXECUTABLE}" OPENCPPCOVERAGE_NATIVE)
|
||||
|
||||
add_custom_target(coverage
|
||||
DEPENDS unit_tests
|
||||
COMMAND "${OPENCPPCOVERAGE_NATIVE}"
|
||||
--verbose
|
||||
--sources "${COVERAGE_ROOT_PATH}"
|
||||
--modules "${COVERAGE_ROOT_PATH}"
|
||||
--excluded_sources "*\\tests\\*"
|
||||
--excluded_sources "*\\gtest\\*"
|
||||
--excluded_sources "*\\googletest\\*"
|
||||
--excluded_sources "*\\_deps\\*"
|
||||
--excluded_sources "*\\vcpkg_installed\\*"
|
||||
--export_type "html:${COVERAGE_OUTPUT_PATH}"
|
||||
--export_type "cobertura:${COVERAGE_XML_PATH}"
|
||||
--cover_children
|
||||
-- "$<TARGET_FILE:unit_tests>"
|
||||
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
|
||||
COMMENT "Running OpenCppCoverage"
|
||||
)
|
||||
|
||||
elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang|AppleClang")
|
||||
# Linux/macOS: LLVM coverage via script
|
||||
add_custom_target(coverage
|
||||
DEPENDS unit_tests
|
||||
COMMAND bash "${CMAKE_SOURCE_DIR}/scripts/coverage-llvm.sh"
|
||||
"${CMAKE_SOURCE_DIR}"
|
||||
"${CMAKE_BINARY_DIR}"
|
||||
"$<TARGET_FILE:unit_tests>"
|
||||
"${CMAKE_BINARY_DIR}/coverage"
|
||||
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
|
||||
COMMENT "Running LLVM coverage"
|
||||
)
|
||||
|
||||
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||
# GCC: lcov/gcov
|
||||
add_custom_target(coverage
|
||||
DEPENDS unit_tests
|
||||
COMMAND $<TARGET_FILE:unit_tests> || true
|
||||
COMMAND lcov --capture --directory "${CMAKE_BINARY_DIR}"
|
||||
--output-file "${CMAKE_BINARY_DIR}/coverage.info"
|
||||
--ignore-errors mismatch,gcov
|
||||
COMMAND lcov --remove "${CMAKE_BINARY_DIR}/coverage.info"
|
||||
"*/tests/*" "*/gtest/*" "*/googletest/*" "*/_deps/*" "/usr/*"
|
||||
--output-file "${CMAKE_BINARY_DIR}/coverage.info"
|
||||
--ignore-errors unused
|
||||
COMMAND genhtml "${CMAKE_BINARY_DIR}/coverage.info"
|
||||
--output-directory "${CMAKE_BINARY_DIR}/coverage"
|
||||
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
|
||||
COMMENT "Running lcov/genhtml"
|
||||
)
|
||||
endif()
|
||||
endfunction()
|
||||
465
docs/3d_primitives/mesh.md
Normal file
465
docs/3d_primitives/mesh.md
Normal file
@@ -0,0 +1,465 @@
|
||||
# `omath::primitives::Mesh` — 3D mesh with transformation support
|
||||
|
||||
> Header: `omath/3d_primitives/mesh.hpp`
|
||||
> Namespace: `omath::primitives`
|
||||
> Depends on: `omath::Vector3<T>`, `omath::Mat4X4`, `omath::Triangle<Vector3<T>>`
|
||||
> Purpose: represent and transform 3D meshes in different engine coordinate systems
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
`Mesh` represents a 3D polygonal mesh with vertex data and transformation capabilities. It stores:
|
||||
* **Vertex buffer (VBO)** — array of 3D vertex positions
|
||||
* **Index buffer (VAO)** — array of triangular faces (indices into VBO)
|
||||
* **Transformation** — position, rotation, and scale with caching
|
||||
|
||||
The mesh supports transformation from local space to world space using engine-specific coordinate systems through the `MeshTrait` template parameter.
|
||||
|
||||
---
|
||||
|
||||
## Template Declaration
|
||||
|
||||
```cpp
|
||||
template<class Mat4X4, class RotationAngles, class MeshTypeTrait, class Type = float>
|
||||
class Mesh final;
|
||||
```
|
||||
|
||||
### Template Parameters
|
||||
|
||||
* `Mat4X4` — Matrix type for transformations (typically `omath::Mat4X4`)
|
||||
* `RotationAngles` — Rotation representation (e.g., `ViewAngles` with pitch/yaw/roll)
|
||||
* `MeshTypeTrait` — Engine-specific transformation trait (see [Engine Traits](#engine-traits))
|
||||
* `Type` — Scalar type for vertex coordinates (default `float`)
|
||||
|
||||
---
|
||||
|
||||
## Type Aliases
|
||||
|
||||
```cpp
|
||||
using NumericType = Type;
|
||||
```
|
||||
|
||||
Common engine-specific aliases:
|
||||
|
||||
```cpp
|
||||
// Source Engine
|
||||
using Mesh = omath::primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float>;
|
||||
|
||||
// Unity Engine
|
||||
using Mesh = omath::primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float>;
|
||||
|
||||
// Unreal Engine
|
||||
using Mesh = omath::primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float>;
|
||||
|
||||
// Frostbite, IW Engine, OpenGL similar...
|
||||
```
|
||||
|
||||
Use the pre-defined type aliases in engine namespaces:
|
||||
```cpp
|
||||
using namespace omath::source_engine;
|
||||
Mesh my_mesh = /* ... */; // Uses SourceEngine::Mesh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Members
|
||||
|
||||
### Vertex Data
|
||||
|
||||
```cpp
|
||||
std::vector<Vector3<NumericType>> m_vertex_buffer; // VBO: vertex positions
|
||||
std::vector<Vector3<std::size_t>> m_vertex_array_object; // VAO: face indices
|
||||
```
|
||||
|
||||
* `m_vertex_buffer` — array of vertex positions in **local space**
|
||||
* `m_vertex_array_object` — array of triangular faces, each containing 3 indices into `m_vertex_buffer`
|
||||
|
||||
**Public access**: These members are public for direct manipulation when needed.
|
||||
|
||||
---
|
||||
|
||||
## Constructor
|
||||
|
||||
```cpp
|
||||
Mesh(std::vector<Vector3<NumericType>> vbo,
|
||||
std::vector<Vector3<std::size_t>> vao,
|
||||
Vector3<NumericType> scale = {1, 1, 1});
|
||||
```
|
||||
|
||||
Creates a mesh from vertex and index data.
|
||||
|
||||
**Parameters**:
|
||||
* `vbo` — vertex buffer (moved into mesh)
|
||||
* `vao` — index buffer / vertex array object (moved into mesh)
|
||||
* `scale` — initial scale (default `{1, 1, 1}`)
|
||||
|
||||
**Example**:
|
||||
```cpp
|
||||
std::vector<Vector3<float>> vertices = {
|
||||
{0, 0, 0}, {1, 0, 0}, {0, 1, 0}, {0, 0, 1}
|
||||
};
|
||||
|
||||
std::vector<Vector3<std::size_t>> faces = {
|
||||
{0, 1, 2}, // Triangle 1
|
||||
{0, 1, 3}, // Triangle 2
|
||||
{0, 2, 3}, // Triangle 3
|
||||
{1, 2, 3} // Triangle 4
|
||||
};
|
||||
|
||||
using namespace omath::source_engine;
|
||||
Mesh tetrahedron(std::move(vertices), std::move(faces));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Transformation Methods
|
||||
|
||||
### Setting Transform Components
|
||||
|
||||
```cpp
|
||||
void set_origin(const Vector3<NumericType>& new_origin);
|
||||
void set_scale(const Vector3<NumericType>& new_scale);
|
||||
void set_rotation(const RotationAngles& new_rotation_angles);
|
||||
```
|
||||
|
||||
Update the mesh's transformation. **Side effect**: invalidates the cached transformation matrix, which will be recomputed on the next `get_to_world_matrix()` call.
|
||||
|
||||
**Example**:
|
||||
```cpp
|
||||
mesh.set_origin({10, 0, 5});
|
||||
mesh.set_scale({2, 2, 2});
|
||||
|
||||
ViewAngles angles;
|
||||
angles.pitch = PitchAngle::from_degrees(45.0f);
|
||||
angles.yaw = YawAngle::from_degrees(30.0f);
|
||||
mesh.set_rotation(angles);
|
||||
```
|
||||
|
||||
### Getting Transform Components
|
||||
|
||||
```cpp
|
||||
[[nodiscard]] const Vector3<NumericType>& get_origin() const;
|
||||
[[nodiscard]] const Vector3<NumericType>& get_scale() const;
|
||||
[[nodiscard]] const RotationAngles& get_rotation_angles() const;
|
||||
```
|
||||
|
||||
Retrieve current transformation components.
|
||||
|
||||
### Transformation Matrix
|
||||
|
||||
```cpp
|
||||
[[nodiscard]] const Mat4X4& get_to_world_matrix() const;
|
||||
```
|
||||
|
||||
Returns the cached local-to-world transformation matrix. The matrix is computed lazily on first access after any transformation change:
|
||||
|
||||
```
|
||||
M = Translation(origin) × Scale(scale) × Rotation(angles)
|
||||
```
|
||||
|
||||
The rotation matrix is computed using the engine-specific `MeshTrait::rotation_matrix()` method.
|
||||
|
||||
**Caching**: The matrix is stored in a `mutable std::optional` and recomputed only when invalidated by `set_*` methods.
|
||||
|
||||
---
|
||||
|
||||
## Vertex Transformation
|
||||
|
||||
### `vertex_to_world_space`
|
||||
|
||||
```cpp
|
||||
[[nodiscard]]
|
||||
Vector3<float> vertex_to_world_space(const Vector3<float>& vertex) const;
|
||||
```
|
||||
|
||||
Transforms a vertex from local space to world space by multiplying with the transformation matrix.
|
||||
|
||||
**Algorithm**:
|
||||
1. Convert vertex to column matrix: `[x, y, z, 1]ᵀ`
|
||||
2. Multiply by transformation matrix: `M × vertex`
|
||||
3. Extract the resulting 3D position
|
||||
|
||||
**Usage**:
|
||||
```cpp
|
||||
Vector3<float> local_vertex{1, 0, 0};
|
||||
Vector3<float> world_vertex = mesh.vertex_to_world_space(local_vertex);
|
||||
```
|
||||
|
||||
**Note**: This is used internally by `MeshCollider` to provide world-space support functions for GJK/EPA.
|
||||
|
||||
---
|
||||
|
||||
## Face Transformation
|
||||
|
||||
### `make_face_in_world_space`
|
||||
|
||||
```cpp
|
||||
[[nodiscard]]
|
||||
Triangle<Vector3<float>> make_face_in_world_space(
|
||||
const std::vector<Vector3<std::size_t>>::const_iterator vao_iterator
|
||||
) const;
|
||||
```
|
||||
|
||||
Creates a triangle in world space from a face index iterator.
|
||||
|
||||
**Parameters**:
|
||||
* `vao_iterator` — iterator to an element in `m_vertex_array_object`
|
||||
|
||||
**Returns**: `Triangle` with all three vertices transformed to world space.
|
||||
|
||||
**Example**:
|
||||
```cpp
|
||||
for (auto it = mesh.m_vertex_array_object.begin();
|
||||
it != mesh.m_vertex_array_object.end();
|
||||
++it) {
|
||||
Triangle<Vector3<float>> world_triangle = mesh.make_face_in_world_space(it);
|
||||
// Render or process the triangle
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Creating a Box Mesh
|
||||
|
||||
```cpp
|
||||
using namespace omath::source_engine;
|
||||
|
||||
std::vector<Vector3<float>> box_vbo = {
|
||||
// Bottom face
|
||||
{-0.5f, -0.5f, 0.0f}, { 0.5f, -0.5f, 0.0f},
|
||||
{ 0.5f, 0.5f, 0.0f}, {-0.5f, 0.5f, 0.0f},
|
||||
// Top face
|
||||
{-0.5f, -0.5f, 1.0f}, { 0.5f, -0.5f, 1.0f},
|
||||
{ 0.5f, 0.5f, 1.0f}, {-0.5f, 0.5f, 1.0f}
|
||||
};
|
||||
|
||||
std::vector<Vector3<std::size_t>> box_vao = {
|
||||
// Bottom
|
||||
{0, 1, 2}, {0, 2, 3},
|
||||
// Top
|
||||
{4, 6, 5}, {4, 7, 6},
|
||||
// Sides
|
||||
{0, 4, 5}, {0, 5, 1},
|
||||
{1, 5, 6}, {1, 6, 2},
|
||||
{2, 6, 7}, {2, 7, 3},
|
||||
{3, 7, 4}, {3, 4, 0}
|
||||
};
|
||||
|
||||
Mesh box(std::move(box_vbo), std::move(box_vao));
|
||||
box.set_origin({0, 0, 50});
|
||||
box.set_scale({10, 10, 10});
|
||||
```
|
||||
|
||||
### Transforming Mesh Over Time
|
||||
|
||||
```cpp
|
||||
void update_mesh(Mesh& mesh, float delta_time) {
|
||||
// Rotate mesh
|
||||
auto rotation = mesh.get_rotation_angles();
|
||||
rotation.yaw = YawAngle::from_degrees(
|
||||
rotation.yaw.as_degrees() + 45.0f * delta_time
|
||||
);
|
||||
mesh.set_rotation(rotation);
|
||||
|
||||
// Oscillate position
|
||||
auto origin = mesh.get_origin();
|
||||
origin.z = 50.0f + 10.0f * std::sin(current_time * 2.0f);
|
||||
mesh.set_origin(origin);
|
||||
}
|
||||
```
|
||||
|
||||
### Collision Detection
|
||||
|
||||
```cpp
|
||||
using namespace omath::collision;
|
||||
using namespace omath::source_engine;
|
||||
|
||||
Mesh mesh_a(vbo_a, vao_a);
|
||||
mesh_a.set_origin({0, 0, 0});
|
||||
|
||||
Mesh mesh_b(vbo_b, vao_b);
|
||||
mesh_b.set_origin({5, 0, 0});
|
||||
|
||||
MeshCollider collider_a(std::move(mesh_a));
|
||||
MeshCollider collider_b(std::move(mesh_b));
|
||||
|
||||
auto result = GjkAlgorithm<MeshCollider<Mesh>>::check_collision(
|
||||
collider_a, collider_b
|
||||
);
|
||||
```
|
||||
|
||||
### Rendering Transformed Triangles
|
||||
|
||||
```cpp
|
||||
void render_mesh(const Mesh& mesh) {
|
||||
for (auto it = mesh.m_vertex_array_object.begin();
|
||||
it != mesh.m_vertex_array_object.end();
|
||||
++it) {
|
||||
|
||||
Triangle<Vector3<float>> tri = mesh.make_face_in_world_space(it);
|
||||
|
||||
// Draw triangle with your renderer
|
||||
draw_triangle(tri.m_vertex1, tri.m_vertex2, tri.m_vertex3);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Engine Traits
|
||||
|
||||
Each game engine has a corresponding `MeshTrait` that provides the `rotation_matrix` function:
|
||||
|
||||
```cpp
|
||||
class MeshTrait final {
|
||||
public:
|
||||
[[nodiscard]]
|
||||
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||
};
|
||||
```
|
||||
|
||||
### Available Engines
|
||||
|
||||
| Engine | Namespace | Header |
|
||||
|--------|-----------|--------|
|
||||
| Source Engine | `omath::source_engine` | `engines/source_engine/mesh.hpp` |
|
||||
| Unity | `omath::unity_engine` | `engines/unity_engine/mesh.hpp` |
|
||||
| Unreal | `omath::unreal_engine` | `engines/unreal_engine/mesh.hpp` |
|
||||
| Frostbite | `omath::frostbite_engine` | `engines/frostbite_engine/mesh.hpp` |
|
||||
| IW Engine | `omath::iw_engine` | `engines/iw_engine/mesh.hpp` |
|
||||
| OpenGL | `omath::opengl_engine` | `engines/opengl_engine/mesh.hpp` |
|
||||
|
||||
**Example** (Source Engine):
|
||||
```cpp
|
||||
using namespace omath::source_engine;
|
||||
|
||||
// Uses source_engine::MeshTrait automatically
|
||||
Mesh my_mesh(vertices, indices);
|
||||
```
|
||||
|
||||
See [MeshTrait Documentation](#mesh-trait-documentation) for engine-specific details.
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Matrix Caching
|
||||
|
||||
The transformation matrix is computed lazily and cached:
|
||||
* **First access**: O(matrix multiply) ≈ 64 float operations
|
||||
* **Subsequent access**: O(1) — returns cached matrix
|
||||
* **Cache invalidation**: Any `set_*` call invalidates the cache
|
||||
|
||||
**Best practice**: Batch transformation updates before accessing the matrix:
|
||||
```cpp
|
||||
// Good: single matrix recomputation
|
||||
mesh.set_origin(new_origin);
|
||||
mesh.set_rotation(new_rotation);
|
||||
mesh.set_scale(new_scale);
|
||||
auto matrix = mesh.get_to_world_matrix(); // Computes once
|
||||
|
||||
// Bad: three matrix recomputations
|
||||
mesh.set_origin(new_origin);
|
||||
auto m1 = mesh.get_to_world_matrix(); // Compute
|
||||
mesh.set_rotation(new_rotation);
|
||||
auto m2 = mesh.get_to_world_matrix(); // Compute again
|
||||
mesh.set_scale(new_scale);
|
||||
auto m3 = mesh.get_to_world_matrix(); // Compute again
|
||||
```
|
||||
|
||||
### Memory Layout
|
||||
|
||||
* **VBO**: Contiguous `std::vector` for cache-friendly access
|
||||
* **VAO**: Contiguous indices for cache-friendly face iteration
|
||||
* **Matrix**: Cached in `std::optional` (no allocation)
|
||||
|
||||
### Transformation Cost
|
||||
|
||||
* `vertex_to_world_space`: ~15-20 FLOPs per vertex (4×4 matrix multiply)
|
||||
* `make_face_in_world_space`: ~60 FLOPs (3 vertices)
|
||||
|
||||
For high-frequency transformations, consider:
|
||||
* Caching transformed vertices if the mesh doesn't change
|
||||
* Using simpler proxy geometry for collision
|
||||
* Batching transformations
|
||||
|
||||
---
|
||||
|
||||
## Coordinate System Details
|
||||
|
||||
Different engines use different coordinate systems:
|
||||
|
||||
| Engine | Up Axis | Forward Axis | Handedness |
|
||||
|--------|---------|--------------|------------|
|
||||
| Source | +Z | +Y | Right |
|
||||
| Unity | +Y | +Z | Left |
|
||||
| Unreal | +Z | +X | Left |
|
||||
| Frostbite | +Y | +Z | Right |
|
||||
| IW Engine | +Z | +Y | Right |
|
||||
| OpenGL | +Y | +Z | Right |
|
||||
|
||||
The `MeshTrait::rotation_matrix` function accounts for these differences, ensuring correct transformations in each engine's space.
|
||||
|
||||
---
|
||||
|
||||
## Limitations & Edge Cases
|
||||
|
||||
### Empty Mesh
|
||||
|
||||
A mesh with no vertices or faces is valid but not useful:
|
||||
```cpp
|
||||
Mesh empty_mesh({}, {}); // Valid but meaningless
|
||||
```
|
||||
|
||||
For collision detection, ensure `m_vertex_buffer` is non-empty.
|
||||
|
||||
### Index Validity
|
||||
|
||||
No bounds checking is performed on indices in `m_vertex_array_object`. Ensure all indices are valid:
|
||||
```cpp
|
||||
assert(face.x < mesh.m_vertex_buffer.size());
|
||||
assert(face.y < mesh.m_vertex_buffer.size());
|
||||
assert(face.z < mesh.m_vertex_buffer.size());
|
||||
```
|
||||
|
||||
### Degenerate Triangles
|
||||
|
||||
Faces with duplicate indices or collinear vertices will produce degenerate triangles. The mesh doesn't validate this; users must ensure clean geometry.
|
||||
|
||||
### Thread Safety
|
||||
|
||||
* **Read-only**: Safe to read from multiple threads (including const methods)
|
||||
* **Modification**: Not thread-safe; synchronize `set_*` calls externally
|
||||
* **Matrix cache**: Uses `mutable` member; not thread-safe even for const methods
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [MeshCollider Documentation](../collision/mesh_collider.md) - Collision wrapper for meshes
|
||||
- [GJK Algorithm Documentation](../collision/gjk_algorithm.md) - Uses mesh for collision detection
|
||||
- [EPA Algorithm Documentation](../collision/epa_algorithm.md) - Penetration depth with meshes
|
||||
- [Triangle Documentation](../linear_algebra/triangle.md) - Triangle primitive
|
||||
- [Mat4X4 Documentation](../linear_algebra/mat.md) - Transformation matrices
|
||||
- [Box Documentation](box.md) - Box primitive
|
||||
- [Plane Documentation](plane.md) - Plane primitive
|
||||
|
||||
---
|
||||
|
||||
## Mesh Trait Documentation
|
||||
|
||||
For engine-specific `MeshTrait` details, see:
|
||||
|
||||
- [Source Engine MeshTrait](../engines/source_engine/mesh_trait.md)
|
||||
- [Unity Engine MeshTrait](../engines/unity_engine/mesh_trait.md)
|
||||
- [Unreal Engine MeshTrait](../engines/unreal_engine/mesh_trait.md)
|
||||
- [Frostbite Engine MeshTrait](../engines/frostbite/mesh_trait.md)
|
||||
- [IW Engine MeshTrait](../engines/iw_engine/mesh_trait.md)
|
||||
- [OpenGL Engine MeshTrait](../engines/opengl_engine/mesh_trait.md)
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 13 Nov 2025*
|
||||
@@ -11,10 +11,10 @@ OMath is organized into several logical modules:
|
||||
### Core Mathematics
|
||||
- **Linear Algebra** - Vectors, matrices, triangles
|
||||
- **Trigonometry** - Angles, view angles, trigonometric functions
|
||||
- **3D Primitives** - Boxes, planes, geometric shapes
|
||||
- **3D Primitives** - Boxes, planes, meshes, geometric shapes
|
||||
|
||||
### Game Development
|
||||
- **Collision Detection** - Ray tracing, intersection tests
|
||||
- **Collision Detection** - Ray tracing, GJK/EPA algorithms, mesh collision, intersection tests
|
||||
- **Projectile Prediction** - Ballistics and aim-assist calculations
|
||||
- **Projection** - Camera systems and world-to-screen transformations
|
||||
- **Pathfinding** - A* algorithm, navigation meshes
|
||||
@@ -131,6 +131,41 @@ omath::opengl_engine::Camera // OpenGL
|
||||
|
||||
## Collision Detection
|
||||
|
||||
### GJK/EPA Algorithms
|
||||
|
||||
Advanced convex shape collision detection using the Gilbert-Johnson-Keerthi and Expanding Polytope algorithms:
|
||||
|
||||
```cpp
|
||||
namespace omath::collision {
|
||||
template<class ColliderType>
|
||||
class GjkAlgorithm;
|
||||
|
||||
template<class ColliderType>
|
||||
class Epa;
|
||||
}
|
||||
```
|
||||
|
||||
**GJK (Gilbert-Johnson-Keerthi):**
|
||||
* Detects collision between two convex shapes
|
||||
* Returns a 4-point simplex when collision is detected
|
||||
* O(k) complexity where k is typically < 20 iterations
|
||||
* Works with any collider implementing `find_abs_furthest_vertex()`
|
||||
|
||||
**EPA (Expanding Polytope Algorithm):**
|
||||
* Computes penetration depth and separation normal
|
||||
* Takes GJK's output simplex as input
|
||||
* Provides contact information for physics simulation
|
||||
* Configurable iteration limit and convergence tolerance
|
||||
|
||||
**Supporting Types:**
|
||||
|
||||
| Type | Description | Key Features |
|
||||
|------|-------------|--------------|
|
||||
| `Simplex<VectorType>` | 1-4 point geometric simplex | Fixed capacity, GJK iteration support |
|
||||
| `MeshCollider<MeshType>` | Convex mesh collider | Support function for GJK/EPA |
|
||||
| `GjkHitInfo<VertexType>` | Collision result | Hit flag and simplex |
|
||||
| `Epa::Result` | Penetration info | Depth, normal, iteration count |
|
||||
|
||||
### LineTracer
|
||||
|
||||
Ray-casting and line tracing utilities:
|
||||
@@ -142,7 +177,7 @@ namespace omath::collision {
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Ray-triangle intersection
|
||||
- Ray-triangle intersection (Möller-Trumbore algorithm)
|
||||
- Ray-plane intersection
|
||||
- Ray-box intersection
|
||||
- Distance calculations
|
||||
@@ -154,6 +189,14 @@ namespace omath::collision {
|
||||
|------|-------------|-------------|
|
||||
| `Plane` | Infinite plane | `intersects_ray()`, `distance_to_point()` |
|
||||
| `Box` | Axis-aligned bounding box | `contains()`, `intersects()` |
|
||||
| `Mesh` | Polygonal mesh with transforms | `vertex_to_world_space()`, `make_face_in_world_space()` |
|
||||
|
||||
**Mesh Features:**
|
||||
* Vertex buffer (VBO) and index buffer (VAO/EBO) storage
|
||||
* Position, rotation, and scale transformations
|
||||
* Cached transformation matrix
|
||||
* Engine-specific coordinate system support
|
||||
* Compatible with `MeshCollider` for collision detection
|
||||
|
||||
---
|
||||
|
||||
@@ -241,6 +284,13 @@ Implements camera math for an engine:
|
||||
- `calc_view_matrix()` - Build view matrix from angles and position
|
||||
- `calc_projection_matrix()` - Build projection matrix from FOV and viewport
|
||||
|
||||
### MeshTrait
|
||||
|
||||
Provides mesh transformation for an engine:
|
||||
- `rotation_matrix()` - Build rotation matrix from engine-specific angles
|
||||
- Handles coordinate system differences (Y-up vs Z-up, left/right-handed)
|
||||
- Used by `Mesh` class for local-to-world transformations
|
||||
|
||||
### PredEngineTrait
|
||||
|
||||
Provides physics/ballistics specific to an engine:
|
||||
@@ -251,18 +301,18 @@ Provides physics/ballistics specific to an engine:
|
||||
|
||||
### Available Traits
|
||||
|
||||
| Engine | Camera Trait | Pred Engine Trait | Constants | Formulas |
|
||||
|--------|--------------|-------------------|-----------|----------|
|
||||
| Source Engine | ✓ | ✓ | ✓ | ✓ |
|
||||
| Unity Engine | ✓ | ✓ | ✓ | ✓ |
|
||||
| Unreal Engine | ✓ | ✓ | ✓ | ✓ |
|
||||
| Frostbite | ✓ | ✓ | ✓ | ✓ |
|
||||
| IW Engine | ✓ | ✓ | ✓ | ✓ |
|
||||
| OpenGL | ✓ | ✓ | ✓ | ✓ |
|
||||
| Engine | Camera Trait | Mesh Trait | Pred Engine Trait | Constants | Formulas |
|
||||
|--------|--------------|------------|-------------------|-----------|----------|
|
||||
| Source Engine | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| Unity Engine | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| Unreal Engine | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| Frostbite | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| IW Engine | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| OpenGL | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
|
||||
**Documentation:**
|
||||
- See `docs/engines/<engine_name>/` for detailed per-engine docs
|
||||
- Each engine has separate docs for camera_trait, pred_engine_trait, constants, and formulas
|
||||
- Each engine has separate docs for camera_trait, mesh_trait, pred_engine_trait, constants, and formulas
|
||||
|
||||
---
|
||||
|
||||
@@ -524,4 +574,4 @@ UnityCamera camera{pos, SourceAngles{}}; // Wrong!
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 1 Nov 2025*
|
||||
*Last updated: 13 Nov 2025*
|
||||
|
||||
322
docs/collision/epa_algorithm.md
Normal file
322
docs/collision/epa_algorithm.md
Normal file
@@ -0,0 +1,322 @@
|
||||
# `omath::collision::Epa` — Expanding Polytope Algorithm for penetration depth
|
||||
|
||||
> Header: `omath/collision/epa_algorithm.hpp`
|
||||
> Namespace: `omath::collision`
|
||||
> Depends on: `Simplex<VertexType>`, collider types with `find_abs_furthest_vertex` method
|
||||
> Algorithm: **EPA** (Expanding Polytope Algorithm) for penetration depth and contact normal
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The **EPA (Expanding Polytope Algorithm)** calculates the **penetration depth** and **separation normal** between two intersecting convex shapes. It is typically used as a follow-up to the GJK algorithm after a collision has been detected.
|
||||
|
||||
EPA takes a 4-point simplex containing the origin (from GJK) and iteratively expands it to find the point on the Minkowski difference closest to the origin. This point gives both:
|
||||
* **Depth**: minimum translation distance to separate the shapes
|
||||
* **Normal**: direction of separation (pointing from shape B to shape A)
|
||||
|
||||
`Epa` is a template class working with any collider type that implements the support function interface.
|
||||
|
||||
---
|
||||
|
||||
## `Epa::Result`
|
||||
|
||||
```cpp
|
||||
struct Result final {
|
||||
bool success{false}; // true if EPA converged
|
||||
Vertex normal{}; // outward normal (from B to A)
|
||||
float depth{0.0f}; // penetration depth
|
||||
int iterations{0}; // number of iterations performed
|
||||
int num_vertices{0}; // final polytope vertex count
|
||||
int num_faces{0}; // final polytope face count
|
||||
};
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
* `success` — `true` if EPA successfully computed depth and normal; `false` if it failed to converge
|
||||
* `normal` — unit vector pointing from shape B toward shape A (separation direction)
|
||||
* `depth` — minimum distance to move shape A along `normal` to separate the shapes
|
||||
* `iterations` — actual iteration count (useful for performance tuning)
|
||||
* `num_vertices`, `num_faces` — final polytope size (for diagnostics)
|
||||
|
||||
---
|
||||
|
||||
## `Epa::Params`
|
||||
|
||||
```cpp
|
||||
struct Params final {
|
||||
int max_iterations{64}; // maximum iterations before giving up
|
||||
float tolerance{1e-4f}; // absolute tolerance on distance growth
|
||||
};
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
* `max_iterations` — safety limit to prevent infinite loops (default 64)
|
||||
* `tolerance` — convergence threshold: stop when distance grows less than this (default 1e-4)
|
||||
|
||||
---
|
||||
|
||||
## `Epa` Template Class
|
||||
|
||||
```cpp
|
||||
template<class ColliderType>
|
||||
class Epa final {
|
||||
public:
|
||||
using Vertex = typename ColliderType::VertexType;
|
||||
static_assert(EpaVector<Vertex>, "VertexType must satisfy EpaVector concept");
|
||||
|
||||
// Solve for penetration depth and normal
|
||||
[[nodiscard]]
|
||||
static Result solve(
|
||||
const ColliderType& a,
|
||||
const ColliderType& b,
|
||||
const Simplex<Vertex>& simplex,
|
||||
const Params params = {}
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Precondition
|
||||
|
||||
The `simplex` parameter must:
|
||||
* Have exactly 4 points (`simplex.size() == 4`)
|
||||
* Contain the origin (i.e., be a valid GJK result with `hit == true`)
|
||||
|
||||
Violating this precondition leads to undefined behavior.
|
||||
|
||||
---
|
||||
|
||||
## Collider Requirements
|
||||
|
||||
Any type used as `ColliderType` must provide:
|
||||
|
||||
```cpp
|
||||
// Type alias for vertex type (typically Vector3<float>)
|
||||
using VertexType = /* ... */;
|
||||
|
||||
// Find the farthest point in world space along the given direction
|
||||
[[nodiscard]]
|
||||
VertexType find_abs_furthest_vertex(const VertexType& direction) const;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Algorithm Details
|
||||
|
||||
### Expanding Polytope
|
||||
|
||||
EPA maintains a convex polytope (polyhedron) in Minkowski difference space `A - B`. Starting from the 4-point tetrahedron (simplex from GJK), it repeatedly:
|
||||
|
||||
1. **Find closest face** to the origin
|
||||
2. **Support query** in the direction of the face normal
|
||||
3. **Expand polytope** by adding the new support point
|
||||
4. **Update faces** to maintain convexity
|
||||
|
||||
The algorithm terminates when:
|
||||
* **Convergence**: the distance from origin to polytope stops growing (within tolerance)
|
||||
* **Max iterations**: safety limit reached
|
||||
* **Failure cases**: degenerate polytope or numerical issues
|
||||
|
||||
### Minkowski Difference
|
||||
|
||||
Like GJK, EPA operates in Minkowski difference space where `point = a - b` for points in shapes A and B. The closest point on this polytope to the origin gives the minimum separation.
|
||||
|
||||
### Face Winding
|
||||
|
||||
Faces are stored with outward-pointing normals. The algorithm uses a priority queue to efficiently find the face closest to the origin.
|
||||
|
||||
---
|
||||
|
||||
## Vertex Type Requirements
|
||||
|
||||
The `VertexType` must satisfy the `EpaVector` concept:
|
||||
|
||||
```cpp
|
||||
template<class V>
|
||||
concept EpaVector = requires(const V& a, const V& b, float s) {
|
||||
{ a - b } -> std::same_as<V>;
|
||||
{ a.cross(b) } -> std::same_as<V>;
|
||||
{ a.dot(b) } -> std::same_as<float>;
|
||||
{ -a } -> std::same_as<V>;
|
||||
{ a * s } -> std::same_as<V>;
|
||||
{ a / s } -> std::same_as<V>;
|
||||
};
|
||||
```
|
||||
|
||||
`omath::Vector3<float>` satisfies this concept.
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic EPA Usage
|
||||
|
||||
```cpp
|
||||
using namespace omath::collision;
|
||||
using namespace omath::source_engine;
|
||||
|
||||
// First, run GJK to detect collision
|
||||
MeshCollider<Mesh> collider_a(mesh_a);
|
||||
MeshCollider<Mesh> collider_b(mesh_b);
|
||||
|
||||
auto gjk_result = GjkAlgorithm<MeshCollider<Mesh>>::check_collision(
|
||||
collider_a,
|
||||
collider_b
|
||||
);
|
||||
|
||||
if (gjk_result.hit) {
|
||||
// Collision detected, use EPA to get penetration info
|
||||
auto epa_result = Epa<MeshCollider<Mesh>>::solve(
|
||||
collider_a,
|
||||
collider_b,
|
||||
gjk_result.simplex
|
||||
);
|
||||
|
||||
if (epa_result.success) {
|
||||
std::cout << "Penetration depth: " << epa_result.depth << "\n";
|
||||
std::cout << "Separation normal: "
|
||||
<< "(" << epa_result.normal.x << ", "
|
||||
<< epa_result.normal.y << ", "
|
||||
<< epa_result.normal.z << ")\n";
|
||||
|
||||
// Apply separation: move A away from B
|
||||
Vector3<float> correction = epa_result.normal * epa_result.depth;
|
||||
mesh_a.set_origin(mesh_a.get_origin() + correction);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Parameters
|
||||
|
||||
```cpp
|
||||
// Use custom convergence settings
|
||||
Epa<Collider>::Params params;
|
||||
params.max_iterations = 128; // Allow more iterations for complex shapes
|
||||
params.tolerance = 1e-5f; // Tighter tolerance for more accuracy
|
||||
|
||||
auto result = Epa<Collider>::solve(a, b, simplex, params);
|
||||
```
|
||||
|
||||
### Physics Integration
|
||||
|
||||
```cpp
|
||||
void resolve_collision(PhysicsBody& body_a, PhysicsBody& body_b) {
|
||||
auto gjk_result = GjkAlgorithm<Collider>::check_collision(
|
||||
body_a.collider, body_b.collider
|
||||
);
|
||||
|
||||
if (!gjk_result.hit)
|
||||
return; // No collision
|
||||
|
||||
auto epa_result = Epa<Collider>::solve(
|
||||
body_a.collider,
|
||||
body_b.collider,
|
||||
gjk_result.simplex
|
||||
);
|
||||
|
||||
if (epa_result.success) {
|
||||
// Separate bodies
|
||||
float mass_sum = body_a.mass + body_b.mass;
|
||||
float ratio_a = body_b.mass / mass_sum;
|
||||
float ratio_b = body_a.mass / mass_sum;
|
||||
|
||||
body_a.position += epa_result.normal * (epa_result.depth * ratio_a);
|
||||
body_b.position -= epa_result.normal * (epa_result.depth * ratio_b);
|
||||
|
||||
// Apply collision response
|
||||
apply_impulse(body_a, body_b, epa_result.normal);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
* **Time complexity**: O(k × f) where k is iterations and f is faces per iteration (typically f grows slowly)
|
||||
* **Space complexity**: O(n) where n is the number of polytope vertices (typically < 100)
|
||||
* **Typical iterations**: 4-20 for most collisions
|
||||
* **Worst case**: 64 iterations (configurable limit)
|
||||
|
||||
### Performance Tips
|
||||
|
||||
1. **Adjust max_iterations**: Balance accuracy vs. performance for your use case
|
||||
2. **Tolerance tuning**: Larger tolerance = faster convergence but less accurate
|
||||
3. **Shape complexity**: Simpler shapes (fewer faces) converge faster
|
||||
4. **Deep penetrations**: Require more iterations; consider broad-phase separation
|
||||
|
||||
---
|
||||
|
||||
## Limitations & Edge Cases
|
||||
|
||||
* **Requires valid simplex**: Must be called with a 4-point simplex containing the origin (from successful GJK)
|
||||
* **Convex shapes only**: Like GJK, EPA only works with convex colliders
|
||||
* **Convergence failure**: Can fail to converge for degenerate or very thin shapes (check `result.success`)
|
||||
* **Numerical precision**: Extreme scale differences or very small shapes may cause issues
|
||||
* **Deep penetration**: Very deep intersections may require many iterations or fail to converge
|
||||
|
||||
### Error Handling
|
||||
|
||||
```cpp
|
||||
auto result = Epa<Collider>::solve(a, b, simplex);
|
||||
|
||||
if (!result.success) {
|
||||
// EPA failed to converge
|
||||
// Fallback options:
|
||||
// 1. Use a default separation (e.g., axis between centers)
|
||||
// 2. Increase max_iterations and retry
|
||||
// 3. Log a warning and skip this collision
|
||||
std::cerr << "EPA failed after " << result.iterations << " iterations\n";
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Theory & Background
|
||||
|
||||
### Why EPA after GJK?
|
||||
|
||||
GJK determines **if** shapes intersect but doesn't compute penetration depth. EPA extends GJK's final simplex to find the exact depth and normal needed for:
|
||||
* **Collision response** — separating objects realistically
|
||||
* **Contact manifolds** — generating contact points for physics
|
||||
* **Constraint solving** — iterative physics solvers
|
||||
|
||||
### Comparison with SAT
|
||||
|
||||
| Feature | EPA | SAT (Separating Axis Theorem) |
|
||||
|---------|-----|-------------------------------|
|
||||
| Works with | Any convex shape | Polytopes (faces/edges) |
|
||||
| Penetration depth | Yes | Yes |
|
||||
| Complexity | Iterative | Per-axis projection |
|
||||
| Best for | General convex | Boxes, prisms |
|
||||
| Typical speed | Moderate | Fast (few axes) |
|
||||
|
||||
EPA is more general; SAT is faster for axis-aligned shapes.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Details
|
||||
|
||||
The EPA implementation in OMath:
|
||||
* Uses a **priority queue** to efficiently find the closest face
|
||||
* Maintains face winding for consistent normals
|
||||
* Handles **edge cases**: degenerate faces, numerical instability
|
||||
* Prevents infinite loops with iteration limits
|
||||
* Returns detailed diagnostics (iteration count, polytope size)
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [GJK Algorithm Documentation](gjk_algorithm.md) - Collision detection (required before EPA)
|
||||
- [Simplex Documentation](simplex.md) - Input simplex structure
|
||||
- [MeshCollider Documentation](mesh_collider.md) - Mesh-based collider
|
||||
- [Mesh Documentation](../3d_primitives/mesh.md) - Mesh primitive
|
||||
- [Tutorials - Collision Detection](../tutorials.md#tutorial-4-collision-detection) - Complete collision tutorial
|
||||
- [API Overview](../api_overview.md) - High-level API reference
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 13 Nov 2025*
|
||||
216
docs/collision/gjk_algorithm.md
Normal file
216
docs/collision/gjk_algorithm.md
Normal file
@@ -0,0 +1,216 @@
|
||||
# `omath::collision::GjkAlgorithm` — Gilbert-Johnson-Keerthi collision detection
|
||||
|
||||
> Header: `omath/collision/gjk_algorithm.hpp`
|
||||
> Namespace: `omath::collision`
|
||||
> Depends on: `Simplex<VertexType>`, collider types with `find_abs_furthest_vertex` method
|
||||
> Algorithm: **GJK** (Gilbert-Johnson-Keerthi) for convex shape collision detection
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The **GJK algorithm** determines whether two convex shapes intersect by iteratively constructing a simplex in Minkowski difference space. The algorithm is widely used in physics engines and collision detection systems due to its efficiency and robustness.
|
||||
|
||||
`GjkAlgorithm` is a template class that works with any collider type implementing the required support function interface:
|
||||
|
||||
* `find_abs_furthest_vertex(direction)` — returns the farthest point in the collider along the given direction.
|
||||
|
||||
The algorithm returns a `GjkHitInfo` containing:
|
||||
* `hit` — boolean indicating whether the shapes intersect
|
||||
* `simplex` — a 4-point simplex containing the origin (valid only when `hit == true`)
|
||||
|
||||
---
|
||||
|
||||
## `GjkHitInfo`
|
||||
|
||||
```cpp
|
||||
template<class VertexType>
|
||||
struct GjkHitInfo final {
|
||||
bool hit{false}; // true if collision detected
|
||||
Simplex<VertexType> simplex; // 4-point simplex (valid only if hit == true)
|
||||
};
|
||||
```
|
||||
|
||||
The `simplex` field is only meaningful when `hit == true` and contains 4 points. This simplex can be passed to the EPA algorithm for penetration depth calculation.
|
||||
|
||||
---
|
||||
|
||||
## `GjkAlgorithm`
|
||||
|
||||
```cpp
|
||||
template<class ColliderType>
|
||||
class GjkAlgorithm final {
|
||||
using VertexType = typename ColliderType::VertexType;
|
||||
|
||||
public:
|
||||
// Find support vertex in Minkowski difference
|
||||
[[nodiscard]]
|
||||
static VertexType find_support_vertex(
|
||||
const ColliderType& collider_a,
|
||||
const ColliderType& collider_b,
|
||||
const VertexType& direction
|
||||
);
|
||||
|
||||
// Check if two convex shapes intersect
|
||||
[[nodiscard]]
|
||||
static GjkHitInfo<VertexType> check_collision(
|
||||
const ColliderType& collider_a,
|
||||
const ColliderType& collider_b
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Collider Requirements
|
||||
|
||||
Any type used as `ColliderType` must provide:
|
||||
|
||||
```cpp
|
||||
// Type alias for vertex type (typically Vector3<float>)
|
||||
using VertexType = /* ... */;
|
||||
|
||||
// Find the farthest point in world space along the given direction
|
||||
[[nodiscard]]
|
||||
VertexType find_abs_furthest_vertex(const VertexType& direction) const;
|
||||
```
|
||||
|
||||
Common collider types:
|
||||
* `MeshCollider<MeshType>` — for arbitrary triangle meshes
|
||||
* Custom colliders for spheres, boxes, capsules, etc.
|
||||
|
||||
---
|
||||
|
||||
## Algorithm Details
|
||||
|
||||
### Minkowski Difference
|
||||
|
||||
GJK operates in the **Minkowski difference** space `A - B`, where a point in this space represents the difference between points in shapes A and B. The shapes intersect if and only if the origin lies within this Minkowski difference.
|
||||
|
||||
### Support Function
|
||||
|
||||
The support function finds the point in the Minkowski difference farthest along a given direction:
|
||||
|
||||
```cpp
|
||||
support(A, B, dir) = A.furthest(dir) - B.furthest(-dir)
|
||||
```
|
||||
|
||||
This is computed by `find_support_vertex`.
|
||||
|
||||
### Simplex Iteration
|
||||
|
||||
The algorithm builds a simplex incrementally:
|
||||
1. Start with an initial direction (typically vector between shape centers)
|
||||
2. Add support vertices in directions that move the simplex toward the origin
|
||||
3. Simplify the simplex to keep only points closest to the origin
|
||||
4. Repeat until either:
|
||||
* Origin is contained (collision detected, returns 4-point simplex)
|
||||
* No progress can be made (no collision)
|
||||
|
||||
Maximum 64 iterations are performed to prevent infinite loops in edge cases.
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Collision Check
|
||||
|
||||
```cpp
|
||||
using namespace omath::collision;
|
||||
using namespace omath::source_engine;
|
||||
|
||||
// Create mesh colliders
|
||||
Mesh mesh_a = /* ... */;
|
||||
Mesh mesh_b = /* ... */;
|
||||
|
||||
MeshCollider collider_a(mesh_a);
|
||||
MeshCollider collider_b(mesh_b);
|
||||
|
||||
// Check for collision
|
||||
auto result = GjkAlgorithm<MeshCollider<Mesh>>::check_collision(
|
||||
collider_a,
|
||||
collider_b
|
||||
);
|
||||
|
||||
if (result.hit) {
|
||||
std::cout << "Collision detected!\n";
|
||||
// Can pass result.simplex to EPA for penetration depth
|
||||
}
|
||||
```
|
||||
|
||||
### Combined with EPA
|
||||
|
||||
```cpp
|
||||
auto gjk_result = GjkAlgorithm<Collider>::check_collision(a, b);
|
||||
|
||||
if (gjk_result.hit) {
|
||||
// Get penetration depth and normal using EPA
|
||||
auto epa_result = Epa<Collider>::solve(
|
||||
a, b, gjk_result.simplex
|
||||
);
|
||||
|
||||
if (epa_result.success) {
|
||||
std::cout << "Penetration depth: " << epa_result.depth << "\n";
|
||||
std::cout << "Separation normal: " << epa_result.normal << "\n";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
* **Time complexity**: O(k) where k is the number of iterations (typically < 20 for most cases)
|
||||
* **Space complexity**: O(1) — only stores a 4-point simplex
|
||||
* **Best case**: 4-8 iterations for well-separated objects
|
||||
* **Worst case**: 64 iterations (hard limit)
|
||||
* **Cache efficient**: operates on small fixed-size data structures
|
||||
|
||||
### Optimization Tips
|
||||
|
||||
1. **Initial direction**: Use vector between shape centers for faster convergence
|
||||
2. **Early exit**: GJK quickly rejects non-intersecting shapes
|
||||
3. **Warm starting**: Reuse previous simplex for continuous collision detection
|
||||
4. **Broad phase**: Use spatial partitioning before GJK (AABB trees, grids)
|
||||
|
||||
---
|
||||
|
||||
## Limitations & Edge Cases
|
||||
|
||||
* **Convex shapes only**: GJK only works with convex colliders. For concave shapes, decompose into convex parts or use a mesh collider wrapper.
|
||||
* **Degenerate simplices**: The algorithm handles degenerate cases, but numerical precision can cause issues with very thin or flat shapes.
|
||||
* **Iteration limit**: Hard limit of 64 iterations prevents infinite loops but may miss collisions in extreme cases.
|
||||
* **Zero-length directions**: The simplex update logic guards against zero-length vectors, returning safe fallbacks.
|
||||
|
||||
---
|
||||
|
||||
## Vertex Type Requirements
|
||||
|
||||
The `VertexType` must satisfy the `GjkVector` concept (defined in `simplex.hpp`):
|
||||
|
||||
```cpp
|
||||
template<class V>
|
||||
concept GjkVector = requires(const V& a, const V& b) {
|
||||
{ -a } -> std::same_as<V>;
|
||||
{ a - b } -> std::same_as<V>;
|
||||
{ a.cross(b) } -> std::same_as<V>;
|
||||
{ a.point_to_same_direction(b) } -> std::same_as<bool>;
|
||||
};
|
||||
```
|
||||
|
||||
`omath::Vector3<float>` satisfies this concept.
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [EPA Algorithm Documentation](epa_algorithm.md) - Penetration depth calculation
|
||||
- [Simplex Documentation](simplex.md) - Simplex data structure
|
||||
- [MeshCollider Documentation](mesh_collider.md) - Mesh-based collider
|
||||
- [Mesh Documentation](../3d_primitives/mesh.md) - Mesh primitive
|
||||
- [LineTracer Documentation](line_tracer.md) - Ray-triangle intersection
|
||||
- [Tutorials - Collision Detection](../tutorials.md#tutorial-4-collision-detection) - Complete collision tutorial
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 13 Nov 2025*
|
||||
371
docs/collision/mesh_collider.md
Normal file
371
docs/collision/mesh_collider.md
Normal file
@@ -0,0 +1,371 @@
|
||||
# `omath::collision::MeshCollider` — Convex hull collider for meshes
|
||||
|
||||
> Header: `omath/collision/mesh_collider.hpp`
|
||||
> Namespace: `omath::collision`
|
||||
> Depends on: `omath::primitives::Mesh`, `omath::Vector3<T>`
|
||||
> Purpose: wrap a mesh to provide collision detection support for GJK/EPA
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
`MeshCollider` wraps a `Mesh` object to provide the **support function** interface required by the GJK and EPA collision detection algorithms. The support function finds the vertex of the mesh farthest along a given direction, which is essential for constructing Minkowski difference simplices.
|
||||
|
||||
**Important**: `MeshCollider` assumes the mesh represents a **convex hull**. For non-convex shapes, you must either:
|
||||
* Decompose into convex parts
|
||||
* Use the convex hull of the mesh
|
||||
* Use a different collision detection algorithm
|
||||
|
||||
---
|
||||
|
||||
## Template Declaration
|
||||
|
||||
```cpp
|
||||
template<class MeshType>
|
||||
class MeshCollider;
|
||||
```
|
||||
|
||||
### MeshType Requirements
|
||||
|
||||
The `MeshType` must be an instantiation of `omath::primitives::Mesh` or provide:
|
||||
|
||||
```cpp
|
||||
struct MeshType {
|
||||
using NumericType = /* float, double, etc. */;
|
||||
|
||||
std::vector<Vector3<NumericType>> m_vertex_buffer;
|
||||
|
||||
// Transform vertex from local to world space
|
||||
Vector3<NumericType> vertex_to_world_space(const Vector3<NumericType>&) const;
|
||||
};
|
||||
```
|
||||
|
||||
Common types:
|
||||
* `omath::source_engine::Mesh`
|
||||
* `omath::unity_engine::Mesh`
|
||||
* `omath::unreal_engine::Mesh`
|
||||
* `omath::frostbite_engine::Mesh`
|
||||
* `omath::iw_engine::Mesh`
|
||||
* `omath::opengl_engine::Mesh`
|
||||
|
||||
---
|
||||
|
||||
## Type Aliases
|
||||
|
||||
```cpp
|
||||
using NumericType = typename MeshType::NumericType;
|
||||
using VertexType = Vector3<NumericType>;
|
||||
```
|
||||
|
||||
* `NumericType` — scalar type (typically `float`)
|
||||
* `VertexType` — 3D vector type for vertices
|
||||
|
||||
---
|
||||
|
||||
## Constructor
|
||||
|
||||
```cpp
|
||||
explicit MeshCollider(MeshType mesh);
|
||||
```
|
||||
|
||||
Creates a collider from a mesh. The mesh is **moved** into the collider, so pass by value:
|
||||
|
||||
```cpp
|
||||
omath::source_engine::Mesh my_mesh = /* ... */;
|
||||
MeshCollider collider(std::move(my_mesh));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Methods
|
||||
|
||||
### `find_furthest_vertex`
|
||||
|
||||
```cpp
|
||||
[[nodiscard]]
|
||||
const VertexType& find_furthest_vertex(const VertexType& direction) const;
|
||||
```
|
||||
|
||||
Finds the vertex in the mesh's **local space** that has the maximum dot product with `direction`.
|
||||
|
||||
**Algorithm**: Linear search through all vertices (O(n) where n is vertex count).
|
||||
|
||||
**Returns**: Const reference to the vertex in `m_vertex_buffer`.
|
||||
|
||||
---
|
||||
|
||||
### `find_abs_furthest_vertex`
|
||||
|
||||
```cpp
|
||||
[[nodiscard]]
|
||||
VertexType find_abs_furthest_vertex(const VertexType& direction) const;
|
||||
```
|
||||
|
||||
Finds the vertex farthest along `direction` and transforms it to **world space**. This is the primary method used by GJK/EPA.
|
||||
|
||||
**Steps**:
|
||||
1. Find furthest vertex in local space using `find_furthest_vertex`
|
||||
2. Transform to world space using `mesh.vertex_to_world_space()`
|
||||
|
||||
**Returns**: Vertex position in world coordinates.
|
||||
|
||||
**Usage in GJK**:
|
||||
```cpp
|
||||
// GJK support function for Minkowski difference
|
||||
VertexType support = collider_a.find_abs_furthest_vertex(direction)
|
||||
- collider_b.find_abs_furthest_vertex(-direction);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Collision Detection
|
||||
|
||||
```cpp
|
||||
using namespace omath::collision;
|
||||
using namespace omath::source_engine;
|
||||
|
||||
// Create meshes with vertex data
|
||||
std::vector<Vector3<float>> vbo_a = {
|
||||
{-1, -1, -1}, {1, -1, -1}, {1, 1, -1}, {-1, 1, -1},
|
||||
{-1, -1, 1}, {1, -1, 1}, {1, 1, 1}, {-1, 1, 1}
|
||||
};
|
||||
std::vector<Vector3<std::size_t>> vao_a = /* face indices */;
|
||||
|
||||
Mesh mesh_a(vbo_a, vao_a);
|
||||
mesh_a.set_origin({0, 0, 0});
|
||||
|
||||
Mesh mesh_b(vbo_b, vao_b);
|
||||
mesh_b.set_origin({5, 0, 0}); // Positioned away from mesh_a
|
||||
|
||||
// Wrap in colliders
|
||||
MeshCollider<Mesh> collider_a(std::move(mesh_a));
|
||||
MeshCollider<Mesh> collider_b(std::move(mesh_b));
|
||||
|
||||
// Run GJK
|
||||
auto result = GjkAlgorithm<MeshCollider<Mesh>>::check_collision(
|
||||
collider_a, collider_b
|
||||
);
|
||||
|
||||
if (result.hit) {
|
||||
std::cout << "Collision detected!\n";
|
||||
}
|
||||
```
|
||||
|
||||
### With EPA for Penetration Depth
|
||||
|
||||
```cpp
|
||||
auto gjk_result = GjkAlgorithm<MeshCollider<Mesh>>::check_collision(
|
||||
collider_a, collider_b
|
||||
);
|
||||
|
||||
if (gjk_result.hit) {
|
||||
auto epa_result = Epa<MeshCollider<Mesh>>::solve(
|
||||
collider_a, collider_b, gjk_result.simplex
|
||||
);
|
||||
|
||||
if (epa_result.success) {
|
||||
std::cout << "Penetration: " << epa_result.depth << " units\n";
|
||||
std::cout << "Normal: " << epa_result.normal << "\n";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Mesh Creation
|
||||
|
||||
```cpp
|
||||
// Create a simple box mesh
|
||||
std::vector<Vector3<float>> box_vertices = {
|
||||
{-0.5f, -0.5f, -0.5f}, { 0.5f, -0.5f, -0.5f},
|
||||
{ 0.5f, 0.5f, -0.5f}, {-0.5f, 0.5f, -0.5f},
|
||||
{-0.5f, -0.5f, 0.5f}, { 0.5f, -0.5f, 0.5f},
|
||||
{ 0.5f, 0.5f, 0.5f}, {-0.5f, 0.5f, 0.5f}
|
||||
};
|
||||
|
||||
std::vector<Vector3<std::size_t>> box_indices = {
|
||||
{0, 1, 2}, {0, 2, 3}, // Front face
|
||||
{4, 6, 5}, {4, 7, 6}, // Back face
|
||||
{0, 4, 5}, {0, 5, 1}, // Bottom face
|
||||
{2, 6, 7}, {2, 7, 3}, // Top face
|
||||
{0, 3, 7}, {0, 7, 4}, // Left face
|
||||
{1, 5, 6}, {1, 6, 2} // Right face
|
||||
};
|
||||
|
||||
using namespace omath::source_engine;
|
||||
Mesh box_mesh(box_vertices, box_indices);
|
||||
box_mesh.set_origin({10, 0, 0});
|
||||
box_mesh.set_scale({2, 2, 2});
|
||||
|
||||
MeshCollider<Mesh> box_collider(std::move(box_mesh));
|
||||
```
|
||||
|
||||
### Oriented Collision
|
||||
|
||||
```cpp
|
||||
// Create rotated mesh
|
||||
Mesh mesh(vertices, indices);
|
||||
mesh.set_origin({5, 5, 5});
|
||||
mesh.set_scale({1, 1, 1});
|
||||
|
||||
// Set rotation (engine-specific angles)
|
||||
ViewAngles rotation;
|
||||
rotation.pitch = PitchAngle::from_degrees(45.0f);
|
||||
rotation.yaw = YawAngle::from_degrees(30.0f);
|
||||
mesh.set_rotation(rotation);
|
||||
|
||||
// Collider automatically handles transformation
|
||||
MeshCollider<Mesh> collider(std::move(mesh));
|
||||
|
||||
// Support function returns world-space vertices
|
||||
auto support = collider.find_abs_furthest_vertex({0, 1, 0});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Linear Search
|
||||
|
||||
`find_furthest_vertex` performs a **linear search** through all vertices:
|
||||
* **Time complexity**: O(n) per support query
|
||||
* **GJK iterations**: ~10-20 support queries per collision test
|
||||
* **Total cost**: O(k × n) where k is GJK iterations
|
||||
|
||||
For meshes with many vertices (>1000), consider:
|
||||
* Using simpler proxy geometry (bounding box, convex hull with fewer vertices)
|
||||
* Pre-computing hierarchical structures
|
||||
* Using specialized collision shapes when possible
|
||||
|
||||
### Caching Opportunities
|
||||
|
||||
The implementation uses `std::ranges::max_element`, which is cache-friendly for contiguous vertex buffers. For optimal performance:
|
||||
* Store vertices contiguously in memory
|
||||
* Avoid pointer-based or scattered vertex storage
|
||||
* Consider SoA (Structure of Arrays) layout for SIMD optimization
|
||||
|
||||
### World Space Transformation
|
||||
|
||||
The `vertex_to_world_space` call involves matrix multiplication:
|
||||
* **Cost**: ~15-20 floating-point operations per vertex
|
||||
* **Optimization**: The mesh caches its transformation matrix
|
||||
* **Update cost**: Only recomputed when origin/rotation/scale changes
|
||||
|
||||
---
|
||||
|
||||
## Limitations & Edge Cases
|
||||
|
||||
### Convex Hull Requirement
|
||||
|
||||
**Critical**: GJK/EPA only work with **convex shapes**. If your mesh is concave:
|
||||
|
||||
#### Option 1: Convex Decomposition
|
||||
```cpp
|
||||
// Decompose concave mesh into convex parts
|
||||
std::vector<Mesh> convex_parts = decompose_mesh(concave_mesh);
|
||||
|
||||
for (const auto& part : convex_parts) {
|
||||
MeshCollider collider(part);
|
||||
// Test each part separately
|
||||
}
|
||||
```
|
||||
|
||||
#### Option 2: Use Convex Hull
|
||||
```cpp
|
||||
// Compute convex hull of vertices
|
||||
auto hull_vertices = compute_convex_hull(mesh.m_vertex_buffer);
|
||||
Mesh hull_mesh(hull_vertices, hull_indices);
|
||||
MeshCollider collider(std::move(hull_mesh));
|
||||
```
|
||||
|
||||
#### Option 3: Different Algorithm
|
||||
Use triangle-based collision (e.g., LineTracer) for true concave support.
|
||||
|
||||
### Empty Mesh
|
||||
|
||||
Behavior is undefined if `m_vertex_buffer` is empty. Always ensure:
|
||||
```cpp
|
||||
assert(!mesh.m_vertex_buffer.empty());
|
||||
MeshCollider collider(std::move(mesh));
|
||||
```
|
||||
|
||||
### Degenerate Meshes
|
||||
|
||||
* **Single vertex**: Treated as a point (degenerates to sphere collision)
|
||||
* **Two vertices**: Line segment (may cause GJK issues)
|
||||
* **Coplanar vertices**: Flat mesh; EPA may have convergence issues
|
||||
|
||||
**Recommendation**: Use at least 4 non-coplanar vertices for robustness.
|
||||
|
||||
---
|
||||
|
||||
## Coordinate Systems
|
||||
|
||||
`MeshCollider` supports different engine coordinate systems through the `MeshTrait`:
|
||||
|
||||
| Engine | Up Axis | Handedness | Rotation Order |
|
||||
|--------|---------|------------|----------------|
|
||||
| Source Engine | Z | Right-handed | Pitch/Yaw/Roll |
|
||||
| Unity | Y | Left-handed | Pitch/Yaw/Roll |
|
||||
| Unreal | Z | Left-handed | Roll/Pitch/Yaw |
|
||||
| Frostbite | Y | Right-handed | Pitch/Yaw/Roll |
|
||||
| IW Engine | Z | Right-handed | Pitch/Yaw/Roll |
|
||||
| OpenGL | Y | Right-handed | Pitch/Yaw/Roll |
|
||||
|
||||
The `vertex_to_world_space` method handles these differences transparently.
|
||||
|
||||
---
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Custom Support Function
|
||||
|
||||
For specialized collision shapes, implement a custom collider:
|
||||
|
||||
```cpp
|
||||
class SphereCollider {
|
||||
public:
|
||||
using VertexType = Vector3<float>;
|
||||
|
||||
Vector3<float> center;
|
||||
float radius;
|
||||
|
||||
VertexType find_abs_furthest_vertex(const VertexType& direction) const {
|
||||
auto normalized = direction.normalized();
|
||||
return center + normalized * radius;
|
||||
}
|
||||
};
|
||||
|
||||
// Use with GJK/EPA
|
||||
auto result = GjkAlgorithm<SphereCollider>::check_collision(sphere_a, sphere_b);
|
||||
```
|
||||
|
||||
### Debugging Support Queries
|
||||
|
||||
```cpp
|
||||
class DebugMeshCollider : public MeshCollider<Mesh> {
|
||||
public:
|
||||
using MeshCollider::MeshCollider;
|
||||
|
||||
VertexType find_abs_furthest_vertex(const VertexType& direction) const {
|
||||
auto result = MeshCollider::find_abs_furthest_vertex(direction);
|
||||
std::cout << "Support query: direction=" << direction
|
||||
<< " -> vertex=" << result << "\n";
|
||||
return result;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [GJK Algorithm Documentation](gjk_algorithm.md) - Uses `MeshCollider` for collision detection
|
||||
- [EPA Algorithm Documentation](epa_algorithm.md) - Uses `MeshCollider` for penetration depth
|
||||
- [Simplex Documentation](simplex.md) - Data structure used by GJK
|
||||
- [Mesh Documentation](../3d_primitives/mesh.md) - Underlying mesh primitive
|
||||
- [Tutorials - Collision Detection](../tutorials.md#tutorial-4-collision-detection) - Complete collision tutorial
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 13 Nov 2025*
|
||||
327
docs/collision/simplex.md
Normal file
327
docs/collision/simplex.md
Normal file
@@ -0,0 +1,327 @@
|
||||
# `omath::collision::Simplex` — Fixed-capacity simplex for GJK/EPA
|
||||
|
||||
> Header: `omath/collision/simplex.hpp`
|
||||
> Namespace: `omath::collision`
|
||||
> Depends on: `Vector3<float>` (or any type satisfying `GjkVector` concept)
|
||||
> Purpose: store and manipulate simplices in GJK and EPA algorithms
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
`Simplex` is a lightweight container for up to 4 points, used internally by the GJK and EPA collision detection algorithms. A simplex in this context is a geometric shape defined by 1 to 4 vertices:
|
||||
|
||||
* **1 point** — a single vertex
|
||||
* **2 points** — a line segment
|
||||
* **3 points** — a triangle
|
||||
* **4 points** — a tetrahedron
|
||||
|
||||
The GJK algorithm builds simplices incrementally to detect collisions, and EPA extends a 4-point simplex to compute penetration depth.
|
||||
|
||||
---
|
||||
|
||||
## Template & Concepts
|
||||
|
||||
```cpp
|
||||
template<GjkVector VectorType = Vector3<float>>
|
||||
class Simplex final;
|
||||
```
|
||||
|
||||
### `GjkVector` Concept
|
||||
|
||||
The vertex type must satisfy:
|
||||
|
||||
```cpp
|
||||
template<class V>
|
||||
concept GjkVector = requires(const V& a, const V& b) {
|
||||
{ -a } -> std::same_as<V>;
|
||||
{ a - b } -> std::same_as<V>;
|
||||
{ a.cross(b) } -> std::same_as<V>;
|
||||
{ a.point_to_same_direction(b) } -> std::same_as<bool>;
|
||||
};
|
||||
```
|
||||
|
||||
`omath::Vector3<float>` satisfies this concept and is the default.
|
||||
|
||||
---
|
||||
|
||||
## Constructors & Assignment
|
||||
|
||||
```cpp
|
||||
constexpr Simplex() = default;
|
||||
|
||||
constexpr Simplex& operator=(std::initializer_list<VectorType> list) noexcept;
|
||||
```
|
||||
|
||||
### Initialization
|
||||
|
||||
```cpp
|
||||
// Empty simplex
|
||||
Simplex<Vector3<float>> s;
|
||||
|
||||
// Initialize with points
|
||||
Simplex<Vector3<float>> s2;
|
||||
s2 = {v1, v2, v3}; // 3-point simplex (triangle)
|
||||
```
|
||||
|
||||
**Constraint**: Maximum 4 points. Passing more triggers an assertion in debug builds.
|
||||
|
||||
---
|
||||
|
||||
## Core Methods
|
||||
|
||||
### Adding Points
|
||||
|
||||
```cpp
|
||||
constexpr void push_front(const VectorType& p) noexcept;
|
||||
```
|
||||
|
||||
Inserts a point at the **front** (index 0), shifting existing points back. If the simplex is already at capacity (4 points), the last point is discarded.
|
||||
|
||||
**Usage pattern in GJK**:
|
||||
```cpp
|
||||
simplex.push_front(new_support_point);
|
||||
// Now simplex[0] is the newest point
|
||||
```
|
||||
|
||||
### Size & Capacity
|
||||
|
||||
```cpp
|
||||
[[nodiscard]] constexpr std::size_t size() const noexcept;
|
||||
[[nodiscard]] static constexpr std::size_t capacity = 4;
|
||||
```
|
||||
|
||||
* `size()` — current number of points (0-4)
|
||||
* `capacity` — maximum points (always 4)
|
||||
|
||||
### Element Access
|
||||
|
||||
```cpp
|
||||
[[nodiscard]] constexpr VectorType& operator[](std::size_t index) noexcept;
|
||||
[[nodiscard]] constexpr const VectorType& operator[](std::size_t index) const noexcept;
|
||||
```
|
||||
|
||||
Access points by index. **No bounds checking** — index must be `< size()`.
|
||||
|
||||
```cpp
|
||||
if (simplex.size() >= 2) {
|
||||
auto edge = simplex[1] - simplex[0];
|
||||
}
|
||||
```
|
||||
|
||||
### Iterators
|
||||
|
||||
```cpp
|
||||
[[nodiscard]] constexpr auto begin() noexcept;
|
||||
[[nodiscard]] constexpr auto end() noexcept;
|
||||
[[nodiscard]] constexpr auto begin() const noexcept;
|
||||
[[nodiscard]] constexpr auto end() const noexcept;
|
||||
```
|
||||
|
||||
Standard iterator support for range-based loops:
|
||||
|
||||
```cpp
|
||||
for (const auto& vertex : simplex) {
|
||||
std::cout << vertex << "\n";
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GJK-Specific Methods
|
||||
|
||||
These methods implement the core logic for simplifying simplices in the GJK algorithm.
|
||||
|
||||
### `contains_origin`
|
||||
|
||||
```cpp
|
||||
[[nodiscard]] constexpr bool contains_origin() noexcept;
|
||||
```
|
||||
|
||||
Determines if the origin lies within the current simplex. This is the **core GJK test**: if true, the shapes intersect.
|
||||
|
||||
* For a **1-point** simplex, always returns `false` (can't contain origin)
|
||||
* For a **2-point** simplex (line), checks if origin projects onto the segment
|
||||
* For a **3-point** simplex (triangle), checks if origin projects onto the triangle
|
||||
* For a **4-point** simplex (tetrahedron), checks if origin is inside
|
||||
|
||||
**Side effect**: Simplifies the simplex by removing points not needed to maintain proximity to the origin. After calling, `size()` may have decreased.
|
||||
|
||||
**Return value**:
|
||||
* `true` — origin is contained (collision detected)
|
||||
* `false` — origin not contained; simplex has been simplified toward origin
|
||||
|
||||
### `next_direction`
|
||||
|
||||
```cpp
|
||||
[[nodiscard]] constexpr VectorType next_direction() const noexcept;
|
||||
```
|
||||
|
||||
Computes the next search direction for GJK. This is the direction from the simplex toward the origin, used to query the next support point.
|
||||
|
||||
* Must be called **after** `contains_origin()` returns `false`
|
||||
* Behavior is **undefined** if called when `size() == 0` or when origin is already contained
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### GJK Iteration (Simplified)
|
||||
|
||||
```cpp
|
||||
Simplex<Vector3<float>> simplex;
|
||||
Vector3<float> direction{1, 0, 0}; // Initial search direction
|
||||
|
||||
for (int i = 0; i < 64; ++i) {
|
||||
// Get support point in current direction
|
||||
auto support = find_support_vertex(collider_a, collider_b, direction);
|
||||
|
||||
// Check if we made progress
|
||||
if (support.dot(direction) <= 0)
|
||||
break; // No collision possible
|
||||
|
||||
simplex.push_front(support);
|
||||
|
||||
// Check if simplex contains origin
|
||||
if (simplex.contains_origin()) {
|
||||
// Collision detected!
|
||||
assert(simplex.size() == 4);
|
||||
return GjkHitInfo{true, simplex};
|
||||
}
|
||||
|
||||
// Get next search direction
|
||||
direction = simplex.next_direction();
|
||||
}
|
||||
|
||||
// No collision
|
||||
return GjkHitInfo{false, {}};
|
||||
```
|
||||
|
||||
### Manual Simplex Construction
|
||||
|
||||
```cpp
|
||||
using Vec3 = Vector3<float>;
|
||||
|
||||
Simplex<Vec3> simplex;
|
||||
simplex = {
|
||||
Vec3{0.0f, 0.0f, 0.0f},
|
||||
Vec3{1.0f, 0.0f, 0.0f},
|
||||
Vec3{0.0f, 1.0f, 0.0f},
|
||||
Vec3{0.0f, 0.0f, 1.0f}
|
||||
};
|
||||
|
||||
assert(simplex.size() == 4);
|
||||
|
||||
// Check if origin is inside this tetrahedron
|
||||
bool has_collision = simplex.contains_origin();
|
||||
```
|
||||
|
||||
### Iterating Over Points
|
||||
|
||||
```cpp
|
||||
void print_simplex(const Simplex<Vector3<float>>& s) {
|
||||
std::cout << "Simplex with " << s.size() << " points:\n";
|
||||
for (std::size_t i = 0; i < s.size(); ++i) {
|
||||
const auto& p = s[i];
|
||||
std::cout << " [" << i << "] = ("
|
||||
<< p.x << ", " << p.y << ", " << p.z << ")\n";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Simplex Simplification
|
||||
|
||||
The `contains_origin()` method implements different tests based on simplex size:
|
||||
|
||||
#### Line Segment (2 points)
|
||||
|
||||
Checks if origin projects onto segment `[A, B]`:
|
||||
* If yes, keeps both points
|
||||
* If no, keeps only the closer point
|
||||
|
||||
#### Triangle (3 points)
|
||||
|
||||
Tests the origin against the triangle plane and edges using cross products. Simplifies to:
|
||||
* The full triangle if origin projects onto its surface
|
||||
* An edge if origin is closest to that edge
|
||||
* A single vertex otherwise
|
||||
|
||||
#### Tetrahedron (4 points)
|
||||
|
||||
Tests origin against all four faces:
|
||||
* If origin is inside, returns `true` (collision)
|
||||
* If outside, reduces to the face/edge/vertex closest to origin
|
||||
|
||||
### Direction Calculation
|
||||
|
||||
The `next_direction()` method computes:
|
||||
* For **line**: perpendicular from line toward origin
|
||||
* For **triangle**: perpendicular from triangle toward origin
|
||||
* Implementation uses cross products and projections to avoid sqrt when possible
|
||||
|
||||
---
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
* **Storage**: Fixed 4 × `sizeof(VectorType)` + size counter
|
||||
* **Push front**: O(n) where n is current size (max 4, so effectively O(1))
|
||||
* **Contains origin**: O(1) for each case (line, triangle, tetrahedron)
|
||||
* **Next direction**: O(1) — simple cross products and subtractions
|
||||
* **No heap allocations**: All storage is inline
|
||||
|
||||
**constexpr**: All methods are `constexpr`, enabling compile-time usage where feasible.
|
||||
|
||||
---
|
||||
|
||||
## Edge Cases & Constraints
|
||||
|
||||
### Degenerate Simplices
|
||||
|
||||
* **Zero-length edges**: Can occur if support points coincide. The algorithm handles this by checking `point_to_same_direction` before divisions.
|
||||
* **Collinear points**: Triangle simplification detects and handles collinear cases by reducing to a line.
|
||||
* **Flat tetrahedron**: If the 4th point is coplanar with the first 3, the origin containment test may have reduced precision.
|
||||
|
||||
### Assertions
|
||||
|
||||
* **Capacity**: `operator=` asserts `list.size() <= 4` in debug builds
|
||||
* **Index bounds**: No bounds checking in release builds — ensure `index < size()`
|
||||
|
||||
### Thread Safety
|
||||
|
||||
* **Read-only**: Safe to read from multiple threads
|
||||
* **Modification**: Not thread-safe; synchronize writes externally
|
||||
|
||||
---
|
||||
|
||||
## Relationship to GJK & EPA
|
||||
|
||||
### In GJK
|
||||
|
||||
* Starts empty or with an initial point
|
||||
* Grows via `push_front` as support points are added
|
||||
* Shrinks via `contains_origin` as it's simplified
|
||||
* Once it reaches 4 points and contains origin, GJK succeeds
|
||||
|
||||
### In EPA
|
||||
|
||||
* Takes a 4-point simplex from GJK as input
|
||||
* Uses the tetrahedron as the initial polytope
|
||||
* Does not directly use the `Simplex` class for expansion (EPA maintains a more complex polytope structure)
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [GJK Algorithm Documentation](gjk_algorithm.md) - Uses `Simplex` for collision detection
|
||||
- [EPA Algorithm Documentation](epa_algorithm.md) - Takes 4-point `Simplex` as input
|
||||
- [MeshCollider Documentation](mesh_collider.md) - Provides support function for GJK/EPA
|
||||
- [Vector3 Documentation](../linear_algebra/vector3.md) - Default vertex type
|
||||
- [Tutorials - Collision Detection](../tutorials.md#tutorial-4-collision-detection) - Collision tutorial
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 13 Nov 2025*
|
||||
119
docs/engines/frostbite/mesh_trait.md
Normal file
119
docs/engines/frostbite/mesh_trait.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# `omath::frostbite_engine::MeshTrait` — mesh transformation trait for Frostbite Engine
|
||||
|
||||
> Header: `omath/engines/frostbite_engine/traits/mesh_trait.hpp`
|
||||
> Namespace: `omath::frostbite_engine`
|
||||
> Purpose: provide Frostbite Engine-specific rotation matrix computation for `omath::primitives::Mesh`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
`MeshTrait` is a trait class that provides the `rotation_matrix` function for transforming meshes in Frostbite's coordinate system. It serves as a template parameter to `omath::primitives::Mesh`, enabling engine-specific rotation behavior.
|
||||
|
||||
---
|
||||
|
||||
## Coordinate System
|
||||
|
||||
**Frostbite Engine** uses:
|
||||
* **Up axis**: +Y
|
||||
* **Forward axis**: +Z
|
||||
* **Right axis**: +X
|
||||
* **Handedness**: Right-handed
|
||||
* **Rotation order**: Pitch (X) → Yaw (Y) → Roll (Z)
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```cpp
|
||||
namespace omath::frostbite_engine {
|
||||
|
||||
class MeshTrait final {
|
||||
public:
|
||||
[[nodiscard]]
|
||||
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||
};
|
||||
|
||||
} // namespace omath::frostbite_engine
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Method: `rotation_matrix`
|
||||
|
||||
```cpp
|
||||
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||
```
|
||||
|
||||
Computes a 4×4 rotation matrix from Frostbite-style Euler angles.
|
||||
|
||||
**Parameters**:
|
||||
* `rotation` — `ViewAngles` containing pitch, yaw, and roll angles
|
||||
|
||||
**Returns**: 4×4 rotation matrix suitable for mesh transformation
|
||||
|
||||
**Implementation**: Delegates to `frostbite_engine::rotation_matrix(rotation)` defined in `formulas.hpp`.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### With Mesh
|
||||
|
||||
```cpp
|
||||
using namespace omath::frostbite_engine;
|
||||
|
||||
// Create mesh (MeshTrait is used automatically)
|
||||
Mesh my_mesh(vertices, indices);
|
||||
|
||||
// Set rotation using ViewAngles
|
||||
ViewAngles angles;
|
||||
angles.pitch = PitchAngle::from_degrees(30.0f);
|
||||
angles.yaw = YawAngle::from_degrees(45.0f);
|
||||
angles.roll = RollAngle::from_degrees(0.0f);
|
||||
|
||||
my_mesh.set_rotation(angles);
|
||||
|
||||
// The rotation matrix is computed using MeshTrait::rotation_matrix
|
||||
auto matrix = my_mesh.get_to_world_matrix();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rotation Conventions
|
||||
|
||||
Frostbite uses a right-handed Y-up coordinate system:
|
||||
|
||||
1. **Pitch** (rotation around X-axis / right axis)
|
||||
* Positive pitch looks upward (+Y direction)
|
||||
* Range: typically [-89°, 89°]
|
||||
|
||||
2. **Yaw** (rotation around Y-axis / up axis)
|
||||
* Positive yaw rotates counterclockwise when viewed from above (right-handed)
|
||||
* Range: [-180°, 180°]
|
||||
|
||||
3. **Roll** (rotation around Z-axis / forward axis)
|
||||
* Positive roll tilts right
|
||||
* Range: [-180°, 180°]
|
||||
|
||||
---
|
||||
|
||||
## Type Alias
|
||||
|
||||
```cpp
|
||||
namespace omath::frostbite_engine {
|
||||
using Mesh = primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Mesh Documentation](../../3d_primitives/mesh.md) - Mesh primitive
|
||||
- [Formulas Documentation](formulas.md) - Frostbite rotation formula
|
||||
- [CameraTrait Documentation](camera_trait.md) - Camera trait
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 13 Nov 2025*
|
||||
119
docs/engines/iw_engine/mesh_trait.md
Normal file
119
docs/engines/iw_engine/mesh_trait.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# `omath::iw_engine::MeshTrait` — mesh transformation trait for IW Engine
|
||||
|
||||
> Header: `omath/engines/iw_engine/traits/mesh_trait.hpp`
|
||||
> Namespace: `omath::iw_engine`
|
||||
> Purpose: provide IW Engine-specific rotation matrix computation for `omath::primitives::Mesh`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
`MeshTrait` is a trait class that provides the `rotation_matrix` function for transforming meshes in IW Engine's (Infinity Ward) coordinate system. It serves as a template parameter to `omath::primitives::Mesh`, enabling engine-specific rotation behavior.
|
||||
|
||||
---
|
||||
|
||||
## Coordinate System
|
||||
|
||||
**IW Engine** (Call of Duty) uses:
|
||||
* **Up axis**: +Z
|
||||
* **Forward axis**: +Y
|
||||
* **Right axis**: +X
|
||||
* **Handedness**: Right-handed
|
||||
* **Rotation order**: Pitch (X) → Yaw (Z) → Roll (Y)
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```cpp
|
||||
namespace omath::iw_engine {
|
||||
|
||||
class MeshTrait final {
|
||||
public:
|
||||
[[nodiscard]]
|
||||
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||
};
|
||||
|
||||
} // namespace omath::iw_engine
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Method: `rotation_matrix`
|
||||
|
||||
```cpp
|
||||
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||
```
|
||||
|
||||
Computes a 4×4 rotation matrix from IW Engine-style Euler angles.
|
||||
|
||||
**Parameters**:
|
||||
* `rotation` — `ViewAngles` containing pitch, yaw, and roll angles
|
||||
|
||||
**Returns**: 4×4 rotation matrix suitable for mesh transformation
|
||||
|
||||
**Implementation**: Delegates to `iw_engine::rotation_matrix(rotation)` defined in `formulas.hpp`.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### With Mesh
|
||||
|
||||
```cpp
|
||||
using namespace omath::iw_engine;
|
||||
|
||||
// Create mesh (MeshTrait is used automatically)
|
||||
Mesh my_mesh(vertices, indices);
|
||||
|
||||
// Set rotation using ViewAngles
|
||||
ViewAngles angles;
|
||||
angles.pitch = PitchAngle::from_degrees(30.0f);
|
||||
angles.yaw = YawAngle::from_degrees(45.0f);
|
||||
angles.roll = RollAngle::from_degrees(0.0f);
|
||||
|
||||
my_mesh.set_rotation(angles);
|
||||
|
||||
// The rotation matrix is computed using MeshTrait::rotation_matrix
|
||||
auto matrix = my_mesh.get_to_world_matrix();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rotation Conventions
|
||||
|
||||
IW Engine uses a right-handed Z-up coordinate system (similar to Source Engine):
|
||||
|
||||
1. **Pitch** (rotation around X-axis / right axis)
|
||||
* Positive pitch looks upward (+Z direction)
|
||||
* Range: typically [-89°, 89°]
|
||||
|
||||
2. **Yaw** (rotation around Z-axis / up axis)
|
||||
* Positive yaw rotates counterclockwise when viewed from above
|
||||
* Range: [-180°, 180°]
|
||||
|
||||
3. **Roll** (rotation around Y-axis / forward axis)
|
||||
* Positive roll tilts right
|
||||
* Range: [-180°, 180°]
|
||||
|
||||
---
|
||||
|
||||
## Type Alias
|
||||
|
||||
```cpp
|
||||
namespace omath::iw_engine {
|
||||
using Mesh = primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Mesh Documentation](../../3d_primitives/mesh.md) - Mesh primitive
|
||||
- [Formulas Documentation](formulas.md) - IW Engine rotation formula
|
||||
- [CameraTrait Documentation](camera_trait.md) - Camera trait
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 13 Nov 2025*
|
||||
121
docs/engines/opengl_engine/mesh_trait.md
Normal file
121
docs/engines/opengl_engine/mesh_trait.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# `omath::opengl_engine::MeshTrait` — mesh transformation trait for OpenGL
|
||||
|
||||
> Header: `omath/engines/opengl_engine/traits/mesh_trait.hpp`
|
||||
> Namespace: `omath::opengl_engine`
|
||||
> Purpose: provide OpenGL-specific rotation matrix computation for `omath::primitives::Mesh`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
`MeshTrait` is a trait class that provides the `rotation_matrix` function for transforming meshes in OpenGL's canonical coordinate system. It serves as a template parameter to `omath::primitives::Mesh`, enabling engine-specific rotation behavior.
|
||||
|
||||
---
|
||||
|
||||
## Coordinate System
|
||||
|
||||
**OpenGL** (canonical) uses:
|
||||
* **Up axis**: +Y
|
||||
* **Forward axis**: +Z (toward viewer)
|
||||
* **Right axis**: +X
|
||||
* **Handedness**: Right-handed
|
||||
* **Rotation order**: Pitch (X) → Yaw (Y) → Roll (Z)
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```cpp
|
||||
namespace omath::opengl_engine {
|
||||
|
||||
class MeshTrait final {
|
||||
public:
|
||||
[[nodiscard]]
|
||||
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||
};
|
||||
|
||||
} // namespace omath::opengl_engine
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Method: `rotation_matrix`
|
||||
|
||||
```cpp
|
||||
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||
```
|
||||
|
||||
Computes a 4×4 rotation matrix from OpenGL-style Euler angles.
|
||||
|
||||
**Parameters**:
|
||||
* `rotation` — `ViewAngles` containing pitch, yaw, and roll angles
|
||||
|
||||
**Returns**: 4×4 rotation matrix suitable for mesh transformation
|
||||
|
||||
**Implementation**: Delegates to `opengl_engine::rotation_matrix(rotation)` defined in `formulas.hpp`.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### With Mesh
|
||||
|
||||
```cpp
|
||||
using namespace omath::opengl_engine;
|
||||
|
||||
// Create mesh (MeshTrait is used automatically)
|
||||
Mesh my_mesh(vertices, indices);
|
||||
|
||||
// Set rotation using ViewAngles
|
||||
ViewAngles angles;
|
||||
angles.pitch = PitchAngle::from_degrees(30.0f);
|
||||
angles.yaw = YawAngle::from_degrees(45.0f);
|
||||
angles.roll = RollAngle::from_degrees(0.0f);
|
||||
|
||||
my_mesh.set_rotation(angles);
|
||||
|
||||
// The rotation matrix is computed using MeshTrait::rotation_matrix
|
||||
auto matrix = my_mesh.get_to_world_matrix();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rotation Conventions
|
||||
|
||||
OpenGL uses a right-handed Y-up coordinate system:
|
||||
|
||||
1. **Pitch** (rotation around X-axis / right axis)
|
||||
* Positive pitch looks upward (+Y direction)
|
||||
* Range: typically [-89°, 89°]
|
||||
|
||||
2. **Yaw** (rotation around Y-axis / up axis)
|
||||
* Positive yaw rotates counterclockwise when viewed from above (right-handed)
|
||||
* Range: [-180°, 180°]
|
||||
|
||||
3. **Roll** (rotation around Z-axis / depth axis)
|
||||
* Positive roll tilts right
|
||||
* Range: [-180°, 180°]
|
||||
|
||||
**Note**: In OpenGL, +Z points toward the viewer in view space, but away from the viewer in world space.
|
||||
|
||||
---
|
||||
|
||||
## Type Alias
|
||||
|
||||
```cpp
|
||||
namespace omath::opengl_engine {
|
||||
using Mesh = primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Mesh Documentation](../../3d_primitives/mesh.md) - Mesh primitive
|
||||
- [Formulas Documentation](formulas.md) - OpenGL rotation formula
|
||||
- [CameraTrait Documentation](camera_trait.md) - Camera trait
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 13 Nov 2025*
|
||||
182
docs/engines/source_engine/mesh_trait.md
Normal file
182
docs/engines/source_engine/mesh_trait.md
Normal file
@@ -0,0 +1,182 @@
|
||||
# `omath::source_engine::MeshTrait` — mesh transformation trait for Source Engine
|
||||
|
||||
> Header: `omath/engines/source_engine/traits/mesh_trait.hpp`
|
||||
> Namespace: `omath::source_engine`
|
||||
> Purpose: provide Source Engine-specific rotation matrix computation for `omath::primitives::Mesh`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
`MeshTrait` is a trait class that provides the `rotation_matrix` function for transforming meshes in Source Engine's coordinate system. It serves as a template parameter to `omath::primitives::Mesh`, enabling engine-specific rotation behavior.
|
||||
|
||||
---
|
||||
|
||||
## Coordinate System
|
||||
|
||||
**Source Engine** uses:
|
||||
* **Up axis**: +Z
|
||||
* **Forward axis**: +Y
|
||||
* **Right axis**: +X
|
||||
* **Handedness**: Right-handed
|
||||
* **Rotation order**: Pitch (X) → Yaw (Z) → Roll (Y)
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```cpp
|
||||
namespace omath::source_engine {
|
||||
|
||||
class MeshTrait final {
|
||||
public:
|
||||
[[nodiscard]]
|
||||
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||
};
|
||||
|
||||
} // namespace omath::source_engine
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Method: `rotation_matrix`
|
||||
|
||||
```cpp
|
||||
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||
```
|
||||
|
||||
Computes a 4×4 rotation matrix from Source Engine-style Euler angles.
|
||||
|
||||
**Parameters**:
|
||||
* `rotation` — `ViewAngles` containing pitch, yaw, and roll angles
|
||||
|
||||
**Returns**: 4×4 rotation matrix suitable for mesh transformation
|
||||
|
||||
**Implementation**: Delegates to `source_engine::rotation_matrix(rotation)` defined in `formulas.hpp`.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### With Mesh
|
||||
|
||||
```cpp
|
||||
using namespace omath::source_engine;
|
||||
|
||||
// Create mesh (MeshTrait is used automatically)
|
||||
Mesh my_mesh(vertices, indices);
|
||||
|
||||
// Set rotation using ViewAngles
|
||||
ViewAngles angles;
|
||||
angles.pitch = PitchAngle::from_degrees(30.0f);
|
||||
angles.yaw = YawAngle::from_degrees(45.0f);
|
||||
angles.roll = RollAngle::from_degrees(0.0f);
|
||||
|
||||
my_mesh.set_rotation(angles);
|
||||
|
||||
// The rotation matrix is computed using MeshTrait::rotation_matrix
|
||||
auto matrix = my_mesh.get_to_world_matrix();
|
||||
```
|
||||
|
||||
### Direct Usage
|
||||
|
||||
```cpp
|
||||
using namespace omath::source_engine;
|
||||
|
||||
ViewAngles angles;
|
||||
angles.pitch = PitchAngle::from_degrees(45.0f);
|
||||
angles.yaw = YawAngle::from_degrees(90.0f);
|
||||
angles.roll = RollAngle::from_degrees(0.0f);
|
||||
|
||||
Mat4X4 rot_matrix = MeshTrait::rotation_matrix(angles);
|
||||
|
||||
// Use the matrix directly
|
||||
Vector3<float> local_point{1, 0, 0};
|
||||
auto rotated = rot_matrix * mat_column_from_vector(local_point);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rotation Conventions
|
||||
|
||||
The rotation matrix is built following Source Engine's conventions:
|
||||
|
||||
1. **Pitch** (rotation around X-axis / right axis)
|
||||
* Positive pitch looks upward (+Z direction)
|
||||
* Range: typically [-89°, 89°]
|
||||
|
||||
2. **Yaw** (rotation around Z-axis / up axis)
|
||||
* Positive yaw rotates counterclockwise when viewed from above
|
||||
* Range: [-180°, 180°]
|
||||
|
||||
3. **Roll** (rotation around Y-axis / forward axis)
|
||||
* Positive roll tilts right
|
||||
* Range: [-180°, 180°]
|
||||
|
||||
**Composition**: The matrices are combined in the order Pitch × Yaw × Roll, producing a rotation that:
|
||||
* First applies roll around the forward axis
|
||||
* Then applies yaw around the up axis
|
||||
* Finally applies pitch around the right axis
|
||||
|
||||
This matches Source Engine's internal rotation order.
|
||||
|
||||
---
|
||||
|
||||
## Related Functions
|
||||
|
||||
The trait delegates to the formula defined in `formulas.hpp`:
|
||||
|
||||
```cpp
|
||||
[[nodiscard]]
|
||||
Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept;
|
||||
```
|
||||
|
||||
See [Formulas Documentation](formulas.md) for details on the rotation matrix computation.
|
||||
|
||||
---
|
||||
|
||||
## Type Alias
|
||||
|
||||
The Source Engine mesh type is pre-defined:
|
||||
|
||||
```cpp
|
||||
namespace omath::source_engine {
|
||||
using Mesh = primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float>;
|
||||
}
|
||||
```
|
||||
|
||||
Use this alias to ensure correct trait usage:
|
||||
|
||||
```cpp
|
||||
using namespace omath::source_engine;
|
||||
|
||||
// Correct: uses Source Engine trait
|
||||
Mesh my_mesh(vbo, vao);
|
||||
|
||||
// Avoid: manually specifying template parameters
|
||||
primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float> verbose_mesh(vbo, vao);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
* **Angle ranges**: Ensure angles are within valid ranges (pitch: [-89°, 89°], yaw/roll: [-180°, 180°])
|
||||
* **Performance**: Matrix computation is O(1) with ~64 floating-point operations
|
||||
* **Caching**: The mesh caches the transformation matrix; recomputed only when rotation changes
|
||||
* **Compatibility**: Works with all Source Engine games (CS:GO, TF2, Portal, Half-Life 2, etc.)
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Mesh Documentation](../../3d_primitives/mesh.md) - Mesh primitive using this trait
|
||||
- [MeshCollider Documentation](../../collision/mesh_collider.md) - Collision wrapper for meshes
|
||||
- [Formulas Documentation](formulas.md) - Source Engine rotation formula
|
||||
- [CameraTrait Documentation](camera_trait.md) - Camera transformation trait
|
||||
- [Constants Documentation](constants.md) - Source Engine constants
|
||||
- [API Overview](../../api_overview.md) - High-level API reference
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 13 Nov 2025*
|
||||
119
docs/engines/unity_engine/mesh_trait.md
Normal file
119
docs/engines/unity_engine/mesh_trait.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# `omath::unity_engine::MeshTrait` — mesh transformation trait for Unity Engine
|
||||
|
||||
> Header: `omath/engines/unity_engine/traits/mesh_trait.hpp`
|
||||
> Namespace: `omath::unity_engine`
|
||||
> Purpose: provide Unity Engine-specific rotation matrix computation for `omath::primitives::Mesh`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
`MeshTrait` is a trait class that provides the `rotation_matrix` function for transforming meshes in Unity's coordinate system. It serves as a template parameter to `omath::primitives::Mesh`, enabling engine-specific rotation behavior.
|
||||
|
||||
---
|
||||
|
||||
## Coordinate System
|
||||
|
||||
**Unity Engine** uses:
|
||||
* **Up axis**: +Y
|
||||
* **Forward axis**: +Z
|
||||
* **Right axis**: +X
|
||||
* **Handedness**: Left-handed
|
||||
* **Rotation order**: Pitch (X) → Yaw (Y) → Roll (Z)
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```cpp
|
||||
namespace omath::unity_engine {
|
||||
|
||||
class MeshTrait final {
|
||||
public:
|
||||
[[nodiscard]]
|
||||
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||
};
|
||||
|
||||
} // namespace omath::unity_engine
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Method: `rotation_matrix`
|
||||
|
||||
```cpp
|
||||
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||
```
|
||||
|
||||
Computes a 4×4 rotation matrix from Unity-style Euler angles.
|
||||
|
||||
**Parameters**:
|
||||
* `rotation` — `ViewAngles` containing pitch, yaw, and roll angles
|
||||
|
||||
**Returns**: 4×4 rotation matrix suitable for mesh transformation
|
||||
|
||||
**Implementation**: Delegates to `unity_engine::rotation_matrix(rotation)` defined in `formulas.hpp`.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### With Mesh
|
||||
|
||||
```cpp
|
||||
using namespace omath::unity_engine;
|
||||
|
||||
// Create mesh (MeshTrait is used automatically)
|
||||
Mesh my_mesh(vertices, indices);
|
||||
|
||||
// Set rotation using ViewAngles
|
||||
ViewAngles angles;
|
||||
angles.pitch = PitchAngle::from_degrees(30.0f);
|
||||
angles.yaw = YawAngle::from_degrees(45.0f);
|
||||
angles.roll = RollAngle::from_degrees(0.0f);
|
||||
|
||||
my_mesh.set_rotation(angles);
|
||||
|
||||
// The rotation matrix is computed using MeshTrait::rotation_matrix
|
||||
auto matrix = my_mesh.get_to_world_matrix();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rotation Conventions
|
||||
|
||||
Unity uses a left-handed coordinate system with Y-up:
|
||||
|
||||
1. **Pitch** (rotation around X-axis / right axis)
|
||||
* Positive pitch looks upward (+Y direction)
|
||||
* Range: typically [-89°, 89°]
|
||||
|
||||
2. **Yaw** (rotation around Y-axis / up axis)
|
||||
* Positive yaw rotates clockwise when viewed from above (left-handed)
|
||||
* Range: [-180°, 180°]
|
||||
|
||||
3. **Roll** (rotation around Z-axis / forward axis)
|
||||
* Positive roll tilts right
|
||||
* Range: [-180°, 180°]
|
||||
|
||||
---
|
||||
|
||||
## Type Alias
|
||||
|
||||
```cpp
|
||||
namespace omath::unity_engine {
|
||||
using Mesh = primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Mesh Documentation](../../3d_primitives/mesh.md) - Mesh primitive
|
||||
- [Formulas Documentation](formulas.md) - Unity rotation formula
|
||||
- [CameraTrait Documentation](camera_trait.md) - Camera trait
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 13 Nov 2025*
|
||||
121
docs/engines/unreal_engine/mesh_trait.md
Normal file
121
docs/engines/unreal_engine/mesh_trait.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# `omath::unreal_engine::MeshTrait` — mesh transformation trait for Unreal Engine
|
||||
|
||||
> Header: `omath/engines/unreal_engine/traits/mesh_trait.hpp`
|
||||
> Namespace: `omath::unreal_engine`
|
||||
> Purpose: provide Unreal Engine-specific rotation matrix computation for `omath::primitives::Mesh`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
`MeshTrait` is a trait class that provides the `rotation_matrix` function for transforming meshes in Unreal Engine's coordinate system. It serves as a template parameter to `omath::primitives::Mesh`, enabling engine-specific rotation behavior.
|
||||
|
||||
---
|
||||
|
||||
## Coordinate System
|
||||
|
||||
**Unreal Engine** uses:
|
||||
* **Up axis**: +Z
|
||||
* **Forward axis**: +X
|
||||
* **Right axis**: +Y
|
||||
* **Handedness**: Left-handed
|
||||
* **Rotation order**: Roll (Y) → Pitch (X) → Yaw (Z)
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```cpp
|
||||
namespace omath::unreal_engine {
|
||||
|
||||
class MeshTrait final {
|
||||
public:
|
||||
[[nodiscard]]
|
||||
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||
};
|
||||
|
||||
} // namespace omath::unreal_engine
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Method: `rotation_matrix`
|
||||
|
||||
```cpp
|
||||
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||
```
|
||||
|
||||
Computes a 4×4 rotation matrix from Unreal-style Euler angles.
|
||||
|
||||
**Parameters**:
|
||||
* `rotation` — `ViewAngles` containing pitch, yaw, and roll angles
|
||||
|
||||
**Returns**: 4×4 rotation matrix suitable for mesh transformation
|
||||
|
||||
**Implementation**: Delegates to `unreal_engine::rotation_matrix(rotation)` defined in `formulas.hpp`.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### With Mesh
|
||||
|
||||
```cpp
|
||||
using namespace omath::unreal_engine;
|
||||
|
||||
// Create mesh (MeshTrait is used automatically)
|
||||
Mesh my_mesh(vertices, indices);
|
||||
|
||||
// Set rotation using ViewAngles
|
||||
ViewAngles angles;
|
||||
angles.pitch = PitchAngle::from_degrees(30.0f);
|
||||
angles.yaw = YawAngle::from_degrees(45.0f);
|
||||
angles.roll = RollAngle::from_degrees(0.0f);
|
||||
|
||||
my_mesh.set_rotation(angles);
|
||||
|
||||
// The rotation matrix is computed using MeshTrait::rotation_matrix
|
||||
auto matrix = my_mesh.get_to_world_matrix();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rotation Conventions
|
||||
|
||||
Unreal uses a left-handed Z-up coordinate system:
|
||||
|
||||
1. **Roll** (rotation around Y-axis / right axis)
|
||||
* Positive roll rotates forward axis upward
|
||||
* Range: [-180°, 180°]
|
||||
|
||||
2. **Pitch** (rotation around X-axis / forward axis)
|
||||
* Positive pitch looks upward
|
||||
* Range: typically [-89°, 89°]
|
||||
|
||||
3. **Yaw** (rotation around Z-axis / up axis)
|
||||
* Positive yaw rotates clockwise when viewed from above (left-handed)
|
||||
* Range: [-180°, 180°]
|
||||
|
||||
**Note**: Unreal applies rotations in Roll-Pitch-Yaw order, different from most other engines.
|
||||
|
||||
---
|
||||
|
||||
## Type Alias
|
||||
|
||||
```cpp
|
||||
namespace omath::unreal_engine {
|
||||
using Mesh = primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Mesh Documentation](../../3d_primitives/mesh.md) - Mesh primitive
|
||||
- [Formulas Documentation](formulas.md) - Unreal rotation formula
|
||||
- [CameraTrait Documentation](camera_trait.md) - Camera trait
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 13 Nov 2025*
|
||||
BIN
docs/images/showcase/opengl.png
Normal file
BIN
docs/images/showcase/opengl.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 224 KiB |
65
docs/styles/links.css
Normal file
65
docs/styles/links.css
Normal file
@@ -0,0 +1,65 @@
|
||||
/* Normal links */
|
||||
a {
|
||||
color: orange;
|
||||
}
|
||||
|
||||
|
||||
/* On hover/focus */
|
||||
a:hover,
|
||||
a:focus {
|
||||
color: #ff9900; /* a slightly different orange, optional */
|
||||
}
|
||||
/* Navbar background */
|
||||
.navbar,
|
||||
.navbar-default,
|
||||
.navbar-inverse {
|
||||
background-color: #a26228 !important; /* your orange */
|
||||
border-color: #ff6600 !important;
|
||||
}
|
||||
|
||||
/* Navbar brand + links */
|
||||
.navbar .navbar-brand,
|
||||
.navbar .navbar-nav > li > a {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* Active and hover states */
|
||||
.navbar .navbar-nav > .active > a,
|
||||
.navbar .navbar-nav > .active > a:focus,
|
||||
.navbar .navbar-nav > .active > a:hover,
|
||||
.navbar .navbar-nav > li > a:hover,
|
||||
.navbar .navbar-nav > li > a:focus {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
/* === DROPDOWN MENU BACKGROUND === */
|
||||
.navbar .dropdown-menu {
|
||||
border-color: #ff6600 !important;
|
||||
}
|
||||
|
||||
/* Caret icon (the little triangle) */
|
||||
.navbar .dropdown-toggle .caret {
|
||||
border-top-color: #ffffff !important;
|
||||
border-bottom-color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* === BOOTSTRAP 3 STYLE ITEMS (mkdocs + bootswatch darkly often use this) === */
|
||||
.navbar .dropdown-menu > li > a {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.navbar .dropdown-menu > li > a:hover,
|
||||
.navbar .dropdown-menu > li > a:focus {
|
||||
background-color: #e65c00 !important; /* darker orange on hover */
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* === BOOTSTRAP 4+ STYLE ITEMS (if your theme uses .dropdown-item) === */
|
||||
.navbar .dropdown-item {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.navbar .dropdown-item:hover,
|
||||
.navbar .dropdown-item:focus {
|
||||
background-color: #e65c00 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
@@ -1,9 +1,32 @@
|
||||
project(examples)
|
||||
|
||||
add_executable(example_projection_matrix_builder example_proj_mat_builder.cpp)
|
||||
set_target_properties(example_projection_matrix_builder PROPERTIES CXX_STANDARD 26)
|
||||
set_target_properties(example_projection_matrix_builder PROPERTIES
|
||||
CXX_STANDARD 26
|
||||
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 26)
|
||||
target_link_libraries(example_signature_scan PRIVATE omath::omath)
|
||||
set_target_properties(example_signature_scan PROPERTIES
|
||||
CXX_STANDARD 26
|
||||
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 26
|
||||
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(GLEW REQUIRED)
|
||||
find_package(glfw3 CONFIG REQUIRED)
|
||||
target_link_libraries(example_glfw3 PRIVATE omath::omath GLEW::GLEW glfw)
|
||||
339
examples/example_glfw3.cpp
Normal file
339
examples/example_glfw3.cpp
Normal file
@@ -0,0 +1,339 @@
|
||||
// main.cpp
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
// --- OpenGL / windowing ---
|
||||
#include <GL/glew.h> // GLEW must come before GLFW
|
||||
#include <GLFW/glfw3.h>
|
||||
|
||||
// --- your math / engine stuff ---
|
||||
#include "omath/3d_primitives/mesh.hpp"
|
||||
#include "omath/engines/opengl_engine/camera.hpp"
|
||||
#include "omath/engines/opengl_engine/constants.hpp"
|
||||
#include "omath/engines/opengl_engine/mesh.hpp"
|
||||
#include "omath/linear_algebra/vector3.hpp"
|
||||
|
||||
using omath::Vector3;
|
||||
|
||||
// ---------------- TYPE ALIASES (ADAPT TO YOUR LIB) ----------------
|
||||
|
||||
// Your 4x4 matrix type
|
||||
using Mat4x4 = omath::opengl_engine::Mat4X4;
|
||||
|
||||
// Rotation angles for the Mesh
|
||||
using RotationAngles = omath::opengl_engine::ViewAngles;
|
||||
|
||||
// For brevity, alias the templates instantiated with your types
|
||||
using VertexType = omath::primitives::Vertex<Vector3<float>>;
|
||||
using CubeMesh = omath::opengl_engine::Mesh;
|
||||
using MyCamera = omath::opengl_engine::Camera;
|
||||
|
||||
// ---------------- SHADERS ----------------
|
||||
|
||||
static const char* vertexShaderSource = R"(
|
||||
#version 330 core
|
||||
layout (location = 0) in vec3 aPos;
|
||||
layout (location = 1) in vec3 aNormal;
|
||||
layout (location = 2) in vec3 aUv;
|
||||
|
||||
uniform mat4 uMVP;
|
||||
uniform mat4 uModel;
|
||||
|
||||
out vec3 vNormal;
|
||||
out vec3 vUv;
|
||||
|
||||
void main() {
|
||||
vNormal = aNormal;
|
||||
vUv = aUv;
|
||||
gl_Position = uMVP * uModel * vec4(aPos, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
static const char* fragmentShaderSource = R"(
|
||||
#version 330 core
|
||||
in vec3 vNormal;
|
||||
in vec3 vUv;
|
||||
|
||||
out vec4 FragColor;
|
||||
|
||||
void main() {
|
||||
vec3 baseColor = normalize(abs(vNormal));
|
||||
FragColor = vec4(baseColor, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
GLuint compileShader(GLenum type, const char* src)
|
||||
{
|
||||
GLuint shader = glCreateShader(type);
|
||||
glShaderSource(shader, 1, &src, nullptr);
|
||||
glCompileShader(shader);
|
||||
|
||||
GLint ok = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
|
||||
if (!ok)
|
||||
{
|
||||
char log[1024];
|
||||
glGetShaderInfoLog(shader, sizeof(log), nullptr, log);
|
||||
std::cerr << "Shader compile error: " << log << std::endl;
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
GLuint createShaderProgram()
|
||||
{
|
||||
GLuint vs = compileShader(GL_VERTEX_SHADER, vertexShaderSource);
|
||||
GLuint fs = compileShader(GL_FRAGMENT_SHADER, fragmentShaderSource);
|
||||
|
||||
GLuint prog = glCreateProgram();
|
||||
glAttachShader(prog, vs);
|
||||
glAttachShader(prog, fs);
|
||||
glLinkProgram(prog);
|
||||
|
||||
GLint ok = GL_FALSE;
|
||||
glGetProgramiv(prog, GL_LINK_STATUS, &ok);
|
||||
if (!ok)
|
||||
{
|
||||
char log[1024];
|
||||
glGetProgramInfoLog(prog, sizeof(log), nullptr, log);
|
||||
std::cerr << "Program link error: " << log << std::endl;
|
||||
}
|
||||
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(fs);
|
||||
return prog;
|
||||
}
|
||||
|
||||
void framebuffer_size_callback(GLFWwindow* /*window*/, int w, int h)
|
||||
{
|
||||
glViewport(0, 0, w, h);
|
||||
}
|
||||
|
||||
// ---------------- MAIN ----------------
|
||||
|
||||
int main()
|
||||
{
|
||||
// ---------- GLFW init ----------
|
||||
if (!glfwInit())
|
||||
{
|
||||
std::cerr << "Failed to init GLFW\n";
|
||||
return -1;
|
||||
}
|
||||
|
||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
|
||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
|
||||
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
|
||||
#ifdef __APPLE__
|
||||
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
|
||||
#endif
|
||||
|
||||
constexpr int SCR_WIDTH = 800;
|
||||
constexpr int SCR_HEIGHT = 600;
|
||||
|
||||
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "omath cube + camera (GLEW)", nullptr, nullptr);
|
||||
if (!window)
|
||||
{
|
||||
std::cerr << "Failed to create GLFW window\n";
|
||||
glfwTerminate();
|
||||
return -1;
|
||||
}
|
||||
|
||||
glfwMakeContextCurrent(window);
|
||||
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
|
||||
|
||||
// ---------- GLEW init ----------
|
||||
glewExperimental = GL_TRUE;
|
||||
GLenum glewErr = glewInit();
|
||||
if (glewErr != GLEW_OK)
|
||||
{
|
||||
std::cerr << "Failed to initialize GLEW: " << reinterpret_cast<const char*>(glewGetErrorString(glewErr))
|
||||
<< "\n";
|
||||
glfwTerminate();
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ---------- GL state ----------
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
|
||||
// Face culling
|
||||
glEnable(GL_CULL_FACE);
|
||||
glCullFace(GL_BACK); // cull back faces
|
||||
glFrontFace(GL_CCW); // counter-clockwise is front
|
||||
|
||||
// ---------- Build Cube Mesh (CPU side) ----------
|
||||
std::vector<VertexType> vbo;
|
||||
vbo.reserve(8);
|
||||
|
||||
Vector3<float> p000{-0.5f, -0.5f, -0.5f};
|
||||
Vector3<float> p001{-0.5f, -0.5f, 0.5f};
|
||||
Vector3<float> p010{-0.5f, 0.5f, -0.5f};
|
||||
Vector3<float> p011{-0.5f, 0.5f, 0.5f};
|
||||
Vector3<float> p100{0.5f, -0.5f, -0.5f};
|
||||
Vector3<float> p101{0.5f, -0.5f, 0.5f};
|
||||
Vector3<float> p110{0.5f, 0.5f, -0.5f};
|
||||
Vector3<float> p111{0.5f, 0.5f, 0.5f};
|
||||
|
||||
VertexType v0{p000, Vector3<float>{-1, -1, -1}, omath::Vector2<float>{0, 0}};
|
||||
VertexType v1{p001, Vector3<float>{-1, -1, 1}, omath::Vector2<float>{0, 1}};
|
||||
VertexType v2{p010, Vector3<float>{-1, 1, -1}, omath::Vector2<float>{1, 0}};
|
||||
VertexType v3{p011, Vector3<float>{-1, 1, 1}, omath::Vector2<float>{1, 1}};
|
||||
VertexType v4{p100, Vector3<float>{1, -1, -1}, omath::Vector2<float>{0, 0}};
|
||||
VertexType v5{p101, Vector3<float>{1, -1, 1}, omath::Vector2<float>{0, 1}};
|
||||
VertexType v6{p110, Vector3<float>{1, 1, -1}, omath::Vector2<float>{1, 0}};
|
||||
VertexType v7{p111, Vector3<float>{1, 1, 1}, omath::Vector2<float>{1, 1}};
|
||||
|
||||
vbo.push_back(v0); // 0
|
||||
vbo.push_back(v1); // 1
|
||||
vbo.push_back(v2); // 2
|
||||
vbo.push_back(v3); // 3
|
||||
vbo.push_back(v4); // 4
|
||||
vbo.push_back(v5); // 5
|
||||
vbo.push_back(v6); // 6
|
||||
vbo.push_back(v7); // 7
|
||||
|
||||
using Idx = Vector3<std::uint32_t>;
|
||||
std::vector<Idx> ebo;
|
||||
ebo.reserve(12);
|
||||
|
||||
// front (z+)
|
||||
ebo.emplace_back(1, 5, 7);
|
||||
ebo.emplace_back(1, 7, 3);
|
||||
|
||||
// back (z-)
|
||||
ebo.emplace_back(0, 2, 6);
|
||||
ebo.emplace_back(0, 6, 4);
|
||||
|
||||
// left (x-)
|
||||
ebo.emplace_back(0, 1, 3);
|
||||
ebo.emplace_back(0, 3, 2);
|
||||
|
||||
// right (x+)
|
||||
ebo.emplace_back(4, 6, 7);
|
||||
ebo.emplace_back(4, 7, 5);
|
||||
|
||||
// bottom (y-)
|
||||
ebo.emplace_back(0, 4, 5);
|
||||
ebo.emplace_back(0, 5, 1);
|
||||
|
||||
// top (y+)
|
||||
ebo.emplace_back(2, 3, 7);
|
||||
ebo.emplace_back(2, 7, 6);
|
||||
|
||||
CubeMesh cube{std::move(vbo), std::move(ebo)};
|
||||
cube.set_origin({0.f, 0.f, 0.f});
|
||||
cube.set_scale({2.f, 2.f, 2.f});
|
||||
cube.set_rotation(RotationAngles{});
|
||||
|
||||
// ---------- OpenGL buffers ----------
|
||||
GLuint VAO = 0, VBO = 0, EBO_GL = 0;
|
||||
glGenVertexArrays(1, &VAO);
|
||||
glGenBuffers(1, &VBO);
|
||||
glGenBuffers(1, &EBO_GL);
|
||||
|
||||
glBindVertexArray(VAO);
|
||||
|
||||
// upload vertex buffer
|
||||
glBindBuffer(GL_ARRAY_BUFFER, VBO);
|
||||
glBufferData(GL_ARRAY_BUFFER, cube.m_vertex_buffer.size() * sizeof(VertexType), cube.m_vertex_buffer.data(),
|
||||
GL_STATIC_DRAW);
|
||||
|
||||
// flatten EBO to GL indices
|
||||
std::vector<GLuint> flatIndices;
|
||||
flatIndices.reserve(cube.m_vertex_array_object.size() * 3);
|
||||
for (const auto& tri : cube.m_vertex_array_object)
|
||||
{
|
||||
flatIndices.push_back(tri.x);
|
||||
flatIndices.push_back(tri.y);
|
||||
flatIndices.push_back(tri.z);
|
||||
}
|
||||
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO_GL);
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, flatIndices.size() * sizeof(GLuint), flatIndices.data(), GL_STATIC_DRAW);
|
||||
|
||||
// vertex layout: position / normal / uv (each Vector3<float>)
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(VertexType), (void*)offsetof(VertexType, position));
|
||||
|
||||
glEnableVertexAttribArray(1);
|
||||
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(VertexType), (void*)offsetof(VertexType, normal));
|
||||
|
||||
glEnableVertexAttribArray(2);
|
||||
glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, sizeof(VertexType), (void*)offsetof(VertexType, uv));
|
||||
|
||||
glBindVertexArray(0);
|
||||
|
||||
// ---------- Camera setup ----------
|
||||
omath::projection::ViewPort viewPort{static_cast<float>(SCR_WIDTH), static_cast<float>(SCR_HEIGHT)};
|
||||
|
||||
Vector3<float> camPos{0.f, 0.f, 3.f};
|
||||
|
||||
float nearPlane = 0.1f;
|
||||
float farPlane = 100.f;
|
||||
auto fov = omath::projection::FieldOfView::from_degrees(90.f);
|
||||
|
||||
MyCamera camera{camPos, {}, viewPort, fov, nearPlane, farPlane};
|
||||
|
||||
// ---------- Shader ----------
|
||||
GLuint shaderProgram = createShaderProgram();
|
||||
GLint uMvpLoc = glGetUniformLocation(shaderProgram, "uMVP");
|
||||
GLint uModel = glGetUniformLocation(shaderProgram, "uModel");
|
||||
|
||||
static float old_frame_time = glfwGetTime();
|
||||
|
||||
// ---------- Main loop ----------
|
||||
while (!glfwWindowShouldClose(window))
|
||||
{
|
||||
glfwPollEvents();
|
||||
|
||||
float currentTime = glfwGetTime();
|
||||
float deltaTime = currentTime - old_frame_time;
|
||||
old_frame_time = currentTime;
|
||||
|
||||
int fbW = 0, fbH = 0;
|
||||
glfwGetFramebufferSize(window, &fbW, &fbH);
|
||||
glViewport(0, 0, fbW, fbH);
|
||||
|
||||
viewPort.m_width = static_cast<float>(fbW);
|
||||
viewPort.m_height = static_cast<float>(fbH);
|
||||
camera.set_view_port(viewPort);
|
||||
|
||||
glClearColor(0.1f, 0.15f, 0.2f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
RotationAngles rot = cube.get_rotation_angles();
|
||||
rot.yaw += omath::opengl_engine::YawAngle ::from_degrees(40.f * deltaTime);
|
||||
rot.roll += omath::opengl_engine::RollAngle::from_degrees(40.f * deltaTime);
|
||||
|
||||
if (rot.pitch.as_degrees() == 90.f)
|
||||
rot.pitch = omath::opengl_engine::PitchAngle::from_degrees(-90.f);
|
||||
rot.pitch += omath::opengl_engine::PitchAngle::from_degrees(40.f * deltaTime);
|
||||
cube.set_rotation(rot);
|
||||
|
||||
const Mat4x4& viewProj = camera.get_view_projection_matrix();
|
||||
const auto& model = cube.get_to_world_matrix();
|
||||
|
||||
glUseProgram(shaderProgram);
|
||||
|
||||
// Send matrices to GPU
|
||||
const float* mvpPtr = viewProj.raw_array().data();
|
||||
const float* modelPtr = model.raw_array().data();
|
||||
|
||||
glUniformMatrix4fv(uMvpLoc, 1, GL_FALSE, mvpPtr);
|
||||
glUniformMatrix4fv(uModel, 1, GL_FALSE, modelPtr);
|
||||
|
||||
glBindVertexArray(VAO);
|
||||
glDrawElements(GL_TRIANGLES, static_cast<GLsizei>(flatIndices.size()), GL_UNSIGNED_INT, nullptr);
|
||||
|
||||
glfwSwapBuffers(window);
|
||||
}
|
||||
|
||||
// ---------- Cleanup ----------
|
||||
glDeleteVertexArrays(1, &VAO);
|
||||
glDeleteBuffers(1, &VBO);
|
||||
glDeleteBuffers(1, &EBO_GL);
|
||||
glDeleteProgram(shaderProgram);
|
||||
|
||||
glfwDestroyWindow(window);
|
||||
glfwTerminate();
|
||||
return 0;
|
||||
}
|
||||
@@ -6,35 +6,57 @@
|
||||
#include <omath/linear_algebra/mat.hpp>
|
||||
#include <omath/linear_algebra/vector3.hpp>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
namespace omath::primitives
|
||||
{
|
||||
template<class Mat4X4, class RotationAngles, class MeshTypeTrait, class Type = float>
|
||||
template<class VecType = Vector3<float>, class UvT = Vector2<float>>
|
||||
struct Vertex final
|
||||
{
|
||||
using VectorType = VecType;
|
||||
using UvType = UvT;
|
||||
VectorType position;
|
||||
VectorType normal;
|
||||
UvType uv;
|
||||
};
|
||||
|
||||
template<typename T> concept HasPosition = requires(T vertex) { vertex.position; };
|
||||
template<typename T> concept HasNormal = requires(T vertex) { vertex.normal; };
|
||||
template<typename T> concept HasUv = requires(T vertex) { vertex.uv; };
|
||||
|
||||
template<class Mat4X4, class RotationAngles, class MeshTypeTrait, class VertType = Vertex<>>
|
||||
class Mesh final
|
||||
{
|
||||
public:
|
||||
using NumericType = Type;
|
||||
using VectorType = VertType::VectorType;
|
||||
using VertexType = VertType;
|
||||
|
||||
private:
|
||||
using Vbo = std::vector<Vector3<NumericType>>;
|
||||
using Vao = std::vector<Vector3<std::size_t>>;
|
||||
using Vbo = std::vector<VertexType>;
|
||||
using Ebo = std::vector<Vector3<std::uint32_t>>;
|
||||
|
||||
public:
|
||||
Vbo m_vertex_buffer;
|
||||
Vao m_vertex_array_object;
|
||||
Ebo m_element_buffer_object;
|
||||
|
||||
Mesh(Vbo vbo, Vao vao, const Vector3<NumericType> scale = {1, 1, 1,})
|
||||
: m_vertex_buffer(std::move(vbo)), m_vertex_array_object(std::move(vao)), m_scale(scale)
|
||||
Mesh(Vbo vbo, Ebo vao,
|
||||
const VectorType scale =
|
||||
{
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
})
|
||||
: m_vertex_buffer(std::move(vbo)), m_element_buffer_object(std::move(vao)), m_scale(std::move(scale))
|
||||
{
|
||||
}
|
||||
void set_origin(const Vector3<NumericType>& new_origin)
|
||||
void set_origin(const VectorType& new_origin)
|
||||
{
|
||||
m_origin = new_origin;
|
||||
m_to_world_matrix = std::nullopt;
|
||||
}
|
||||
|
||||
void set_scale(const Vector3<NumericType>& new_scale)
|
||||
void set_scale(const VectorType& new_scale)
|
||||
{
|
||||
m_scale = new_scale;
|
||||
m_to_world_matrix = std::nullopt;
|
||||
@@ -47,13 +69,13 @@ namespace omath::primitives
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
const Vector3<NumericType>& get_origin() const
|
||||
const VectorType& get_origin() const
|
||||
{
|
||||
return m_origin;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
const Vector3<NumericType>& get_scale() const
|
||||
const VectorType& get_scale() const
|
||||
{
|
||||
return m_scale;
|
||||
}
|
||||
@@ -69,31 +91,34 @@ namespace omath::primitives
|
||||
{
|
||||
if (m_to_world_matrix)
|
||||
return m_to_world_matrix.value();
|
||||
m_to_world_matrix =
|
||||
mat_translation(m_origin) * mat_scale(m_scale) * MeshTypeTrait::rotation_matrix(m_rotation_angles);
|
||||
m_to_world_matrix = mat_translation<float, Mat4X4::get_store_ordering()>(m_origin)
|
||||
* MeshTypeTrait::rotation_matrix(m_rotation_angles)
|
||||
* mat_scale<float, Mat4X4::get_store_ordering()>(m_scale);
|
||||
|
||||
return m_to_world_matrix.value();
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
Vector3<float> vertex_to_world_space(const Vector3<float>& vertex) const
|
||||
VectorType vertex_position_to_world_space(const Vector3<float>& vertex_position) const
|
||||
requires HasPosition<VertexType>
|
||||
{
|
||||
auto abs_vec = get_to_world_matrix() * mat_column_from_vector(vertex);
|
||||
auto abs_vec = get_to_world_matrix() * mat_column_from_vector<typename Mat4X4::ContainedType, Mat4X4::get_store_ordering()>(vertex_position);
|
||||
|
||||
return {abs_vec.at(0, 0), abs_vec.at(1, 0), abs_vec.at(2, 0)};
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
Triangle<Vector3<float>> make_face_in_world_space(const Vao::const_iterator vao_iterator) const
|
||||
Triangle<VectorType> make_face_in_world_space(const Ebo::const_iterator vao_iterator) const
|
||||
requires HasPosition<VertexType>
|
||||
{
|
||||
return {vertex_to_world_space(m_vertex_buffer.at(vao_iterator->x)),
|
||||
vertex_to_world_space(m_vertex_buffer.at(vao_iterator->y)),
|
||||
vertex_to_world_space(m_vertex_buffer.at(vao_iterator->z))};
|
||||
return {vertex_position_to_world_space(m_vertex_buffer.at(vao_iterator->x).position),
|
||||
vertex_position_to_world_space(m_vertex_buffer.at(vao_iterator->y).position),
|
||||
vertex_position_to_world_space(m_vertex_buffer.at(vao_iterator->z).position)};
|
||||
}
|
||||
|
||||
private:
|
||||
Vector3<NumericType> m_origin;
|
||||
Vector3<NumericType> m_scale;
|
||||
VectorType m_origin;
|
||||
VectorType m_scale;
|
||||
|
||||
RotationAngles m_rotation_angles;
|
||||
|
||||
|
||||
23
include/omath/collision/collider_interface.hpp
Normal file
23
include/omath/collision/collider_interface.hpp
Normal file
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// Created by Vladislav on 06.12.2025.
|
||||
//
|
||||
#pragma once
|
||||
#include <omath/linear_algebra/vector3.hpp>
|
||||
|
||||
namespace omath::collision
|
||||
{
|
||||
template<class VecType = Vector3<float>>
|
||||
class ColliderInterface
|
||||
{
|
||||
public:
|
||||
using VectorType = VecType;
|
||||
virtual ~ColliderInterface() = default;
|
||||
|
||||
[[nodiscard]]
|
||||
virtual VectorType find_abs_furthest_vertex_position(const VectorType& direction) const = 0;
|
||||
|
||||
[[nodiscard]]
|
||||
virtual const VectorType& get_origin() const = 0;
|
||||
virtual void set_origin(const VectorType& new_origin) = 0;
|
||||
};
|
||||
}
|
||||
303
include/omath/collision/epa_algorithm.hpp
Normal file
303
include/omath/collision/epa_algorithm.hpp
Normal file
@@ -0,0 +1,303 @@
|
||||
#pragma once
|
||||
#include "simplex.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <memory_resource>
|
||||
#include <queue>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace omath::collision
|
||||
{
|
||||
template<class V>
|
||||
concept EpaVector = requires(const V& a, const V& b, float s) {
|
||||
{ a - b } -> std::same_as<V>;
|
||||
{ a.cross(b) } -> std::same_as<V>;
|
||||
{ a.dot(b) } -> std::same_as<float>;
|
||||
{ -a } -> std::same_as<V>;
|
||||
{ a * s } -> std::same_as<V>;
|
||||
{ a / s } -> std::same_as<V>;
|
||||
};
|
||||
|
||||
template<class ColliderInterfaceType>
|
||||
class Epa final
|
||||
{
|
||||
public:
|
||||
using VectorType = ColliderInterfaceType::VectorType;
|
||||
static_assert(EpaVector<VectorType>, "VertexType must satisfy EpaVector concept");
|
||||
|
||||
struct Result final
|
||||
{
|
||||
VectorType normal{}; // from A to B
|
||||
VectorType penetration_vector;
|
||||
float depth{0.0f};
|
||||
int iterations{0};
|
||||
int num_vertices{0};
|
||||
int num_faces{0};
|
||||
};
|
||||
|
||||
struct Params final
|
||||
{
|
||||
int max_iterations{64};
|
||||
float tolerance{1e-4f}; // absolute tolerance on distance growth
|
||||
};
|
||||
// Precondition: simplex.size()==4 and contains the origin.
|
||||
[[nodiscard]]
|
||||
static std::optional<Result> solve(const ColliderInterfaceType& a, const ColliderInterfaceType& b,
|
||||
const Simplex<VectorType>& simplex, const Params params = {},
|
||||
std::pmr::memory_resource& mem_resource = *std::pmr::get_default_resource())
|
||||
{
|
||||
// --- Build initial polytope from simplex (4 points) ---
|
||||
std::pmr::vector<VectorType> vertexes = build_initial_polytope_from_simplex(simplex, mem_resource);
|
||||
|
||||
// Initial tetra faces (windings corrected in make_face)
|
||||
std::pmr::vector<Face> faces = create_initial_tetra_faces(mem_resource, vertexes);
|
||||
|
||||
auto heap = rebuild_heap(faces, mem_resource);
|
||||
|
||||
Result out{};
|
||||
|
||||
for (int it = 0; it < params.max_iterations; ++it)
|
||||
{
|
||||
// If heap might be stale after face edits, rebuild lazily.
|
||||
if (heap.empty())
|
||||
break;
|
||||
// Rebuild when the "closest" face changed (simple cheap guard)
|
||||
// (We could keep face handles; this is fine for small Ns.)
|
||||
|
||||
if (const auto top = heap.top(); faces[top.idx].d != top.d)
|
||||
heap = rebuild_heap(faces, mem_resource);
|
||||
|
||||
if (heap.empty())
|
||||
break;
|
||||
|
||||
//FIXME: STORE REF VALUE, DO NOT USE
|
||||
// AFTER IF STATEMENT BLOCK
|
||||
const Face& face = faces[heap.top().idx];
|
||||
|
||||
// Get the furthest point in face normal direction
|
||||
const VectorType p = support_point(a, b, face.n);
|
||||
const float p_dist = face.n.dot(p);
|
||||
|
||||
// Converged if we can’t push the face closer than tolerance
|
||||
if (p_dist - face.d <= params.tolerance)
|
||||
{
|
||||
out.normal = face.n;
|
||||
out.depth = face.d; // along unit normal
|
||||
out.iterations = it + 1;
|
||||
out.num_vertices = static_cast<int>(vertexes.size());
|
||||
out.num_faces = static_cast<int>(faces.size());
|
||||
|
||||
out.penetration_vector = out.normal * out.depth;
|
||||
return out;
|
||||
}
|
||||
|
||||
// Add new vertex
|
||||
const int new_idx = static_cast<int>(vertexes.size());
|
||||
vertexes.emplace_back(p);
|
||||
|
||||
const auto [to_delete, boundary] = mark_visible_and_collect_horizon(faces, p);
|
||||
|
||||
erase_marked(faces, to_delete);
|
||||
|
||||
// Stitch new faces around the horizon
|
||||
for (const auto& e : boundary)
|
||||
faces.emplace_back(make_face(vertexes, e.a, e.b, new_idx));
|
||||
|
||||
// Rebuild heap after topology change
|
||||
heap = rebuild_heap(faces, mem_resource);
|
||||
|
||||
if (!std::isfinite(vertexes.back().dot(vertexes.back())))
|
||||
break; // safety
|
||||
out.iterations = it + 1;
|
||||
}
|
||||
|
||||
if (faces.empty())
|
||||
return std::nullopt;
|
||||
|
||||
const auto best = *std::ranges::min_element(faces, [](const auto& first, const auto& second)
|
||||
{ return first.d < second.d; });
|
||||
out.normal = best.n;
|
||||
out.depth = best.d;
|
||||
out.num_vertices = static_cast<int>(vertexes.size());
|
||||
out.num_faces = static_cast<int>(faces.size());
|
||||
|
||||
out.penetration_vector = out.normal * out.depth;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
private:
|
||||
struct Face final
|
||||
{
|
||||
int i0, i1, i2;
|
||||
VectorType n; // unit outward normal
|
||||
float d; // n · v0 (>=0 ideally because origin is inside)
|
||||
};
|
||||
|
||||
struct Edge final
|
||||
{
|
||||
int a, b;
|
||||
};
|
||||
|
||||
struct HeapItem final
|
||||
{
|
||||
float d;
|
||||
int idx;
|
||||
};
|
||||
struct HeapCmp final
|
||||
{
|
||||
[[nodiscard]]
|
||||
static bool operator()(const HeapItem& lhs, const HeapItem& rhs) noexcept
|
||||
{
|
||||
return lhs.d > rhs.d; // min-heap by distance
|
||||
}
|
||||
};
|
||||
|
||||
using Heap = std::priority_queue<HeapItem, std::pmr::vector<HeapItem>, HeapCmp>;
|
||||
|
||||
[[nodiscard]]
|
||||
static Heap rebuild_heap(const std::pmr::vector<Face>& faces, auto& memory_resource)
|
||||
{
|
||||
std::pmr::vector<HeapItem> storage{&memory_resource};
|
||||
storage.reserve(faces.size()); // optional but recommended
|
||||
|
||||
Heap h{HeapCmp{}, std::move(storage)};
|
||||
|
||||
for (int i = 0; i < static_cast<int>(faces.size()); ++i)
|
||||
h.emplace(faces[i].d, i);
|
||||
|
||||
return h; // allocator is preserved
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
static bool visible_from(const Face& f, const VectorType& p)
|
||||
{
|
||||
// positive if p is in front of the face
|
||||
return f.n.dot(p) - f.d > 1e-7f;
|
||||
}
|
||||
|
||||
static void add_edge_boundary(std::pmr::vector<Edge>& boundary, int a, int b)
|
||||
{
|
||||
// Keep edges that appear only once; erase if opposite already present
|
||||
auto itb = std::ranges::find_if(boundary, [&](const Edge& e) { return e.a == b && e.b == a; });
|
||||
if (itb != boundary.end())
|
||||
boundary.erase(itb); // internal edge cancels out
|
||||
else
|
||||
boundary.emplace_back(a, b); // horizon edge (directed)
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
static Face make_face(const std::pmr::vector<VectorType>& vertexes, int i0, int i1, int i2)
|
||||
{
|
||||
const VectorType& a0 = vertexes[i0];
|
||||
const VectorType& a1 = vertexes[i1];
|
||||
const VectorType& a2 = vertexes[i2];
|
||||
VectorType n = (a1 - a0).cross(a2 - a0);
|
||||
if (n.dot(n) <= 1e-30f)
|
||||
{
|
||||
n = any_perp_vec(a1 - a0); // degenerate guard
|
||||
}
|
||||
// Ensure normal points outward (away from origin): require n·a0 >= 0
|
||||
if (n.dot(a0) < 0.0f)
|
||||
{
|
||||
std::swap(i1, i2);
|
||||
n = -n;
|
||||
}
|
||||
const float inv_len = 1.0f / std::sqrt(std::max(n.dot(n), 1e-30f));
|
||||
n = n * inv_len;
|
||||
const float d = n.dot(a0);
|
||||
return {i0, i1, i2, n, d};
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
static VectorType support_point(const ColliderInterfaceType& a, const ColliderInterfaceType& b,
|
||||
const VectorType& dir)
|
||||
{
|
||||
return a.find_abs_furthest_vertex_position(dir) - b.find_abs_furthest_vertex_position(-dir);
|
||||
}
|
||||
|
||||
template<class V>
|
||||
[[nodiscard]]
|
||||
static constexpr bool near_zero_vec(const V& v, const float eps = 1e-7f)
|
||||
{
|
||||
return v.dot(v) <= eps * eps;
|
||||
}
|
||||
|
||||
template<class V>
|
||||
[[nodiscard]]
|
||||
static constexpr V any_perp_vec(const V& v)
|
||||
{
|
||||
for (const auto& dir : {V{1, 0, 0}, V{0, 1, 0}, V{0, 0, 1}})
|
||||
if (const auto d = v.cross(dir); !near_zero_vec(d))
|
||||
return d;
|
||||
return V{1, 0, 0};
|
||||
}
|
||||
[[nodiscard]]
|
||||
static std::pmr::vector<Face> create_initial_tetra_faces(std::pmr::memory_resource& mem_resource,
|
||||
const std::pmr::vector<VectorType>& vertexes)
|
||||
{
|
||||
std::pmr::vector<Face> faces{&mem_resource};
|
||||
faces.reserve(4);
|
||||
faces.emplace_back(make_face(vertexes, 0, 1, 2));
|
||||
faces.emplace_back(make_face(vertexes, 0, 2, 3));
|
||||
faces.emplace_back(make_face(vertexes, 0, 3, 1));
|
||||
faces.emplace_back(make_face(vertexes, 1, 3, 2));
|
||||
return faces;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
static std::pmr::vector<VectorType> build_initial_polytope_from_simplex(const Simplex<VectorType>& simplex,
|
||||
std::pmr::memory_resource& mem_resource)
|
||||
{
|
||||
std::pmr::vector<VectorType> vertexes{&mem_resource};
|
||||
vertexes.reserve(simplex.size());
|
||||
|
||||
for (std::size_t i = 0; i < simplex.size(); ++i)
|
||||
vertexes.emplace_back(simplex[i]);
|
||||
|
||||
return vertexes;
|
||||
}
|
||||
static void erase_marked(std::pmr::vector<Face>& faces, const std::pmr::vector<bool>& to_delete)
|
||||
{
|
||||
auto* mr = faces.get_allocator().resource(); // keep same resource
|
||||
std::pmr::vector<Face> kept{mr};
|
||||
kept.reserve(faces.size());
|
||||
|
||||
for (std::size_t i = 0; i < faces.size(); ++i)
|
||||
if (!to_delete[i])
|
||||
kept.emplace_back(faces[i]);
|
||||
|
||||
faces.swap(kept);
|
||||
}
|
||||
struct Horizon
|
||||
{
|
||||
std::pmr::vector<bool> to_delete;
|
||||
std::pmr::vector<Edge> boundary;
|
||||
};
|
||||
|
||||
static Horizon mark_visible_and_collect_horizon(const std::pmr::vector<Face>& faces, const VectorType& p)
|
||||
{
|
||||
auto* mr = faces.get_allocator().resource();
|
||||
|
||||
Horizon horizon{std::pmr::vector<bool>(faces.size(), false, mr), std::pmr::vector<Edge>(mr)};
|
||||
horizon.boundary.reserve(faces.size());
|
||||
|
||||
for (std::size_t i = 0; i < faces.size(); ++i)
|
||||
if (visible_from(faces[i], p))
|
||||
{
|
||||
const auto& rf = faces[i];
|
||||
horizon.to_delete[i] = true;
|
||||
add_edge_boundary(horizon.boundary, rf.i0, rf.i1);
|
||||
add_edge_boundary(horizon.boundary, rf.i1, rf.i2);
|
||||
add_edge_boundary(horizon.boundary, rf.i2, rf.i0);
|
||||
}
|
||||
|
||||
return horizon;
|
||||
}
|
||||
};
|
||||
} // namespace omath::collision
|
||||
@@ -3,31 +3,44 @@
|
||||
//
|
||||
|
||||
#pragma once
|
||||
#include "mesh_collider.hpp"
|
||||
#include "omath/linear_algebra/vector3.hpp"
|
||||
#include "simplex.hpp"
|
||||
|
||||
namespace omath::collision
|
||||
{
|
||||
template<class ColliderType>
|
||||
template<class VertexType>
|
||||
struct GjkHitInfo final
|
||||
{
|
||||
bool hit{false};
|
||||
Simplex<VertexType> simplex; // valid only if hit == true and size==4
|
||||
};
|
||||
|
||||
template<class ColliderInterfaceType>
|
||||
class GjkAlgorithm final
|
||||
{
|
||||
using VectorType = ColliderInterfaceType::VectorType;
|
||||
|
||||
public:
|
||||
[[nodiscard]]
|
||||
static ColliderType::VertexType find_support_vertex(const ColliderType& collider_a,
|
||||
const ColliderType& collider_b,
|
||||
const ColliderType::VertexType& direction)
|
||||
static VectorType find_support_vertex(const ColliderInterfaceType& collider_a,
|
||||
const ColliderInterfaceType& collider_b, const VectorType& direction)
|
||||
{
|
||||
return collider_a.find_abs_furthest_vertex(direction) - collider_b.find_abs_furthest_vertex(-direction);
|
||||
return collider_a.find_abs_furthest_vertex_position(direction)
|
||||
- collider_b.find_abs_furthest_vertex_position(-direction);
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
static bool is_collide(const ColliderType& collider_a, const ColliderType& collider_b)
|
||||
static bool is_collide(const ColliderInterfaceType& collider_a, const ColliderInterfaceType& collider_b)
|
||||
{
|
||||
// Get initial support point in any direction
|
||||
auto support = find_support_vertex(collider_a, collider_b, {1, 0, 0});
|
||||
return is_collide_with_simplex_info(collider_a, collider_b).hit;
|
||||
}
|
||||
|
||||
Simplex<typename ColliderType::VertexType> simplex;
|
||||
[[nodiscard]]
|
||||
static GjkHitInfo<VectorType> is_collide_with_simplex_info(const ColliderInterfaceType& collider_a,
|
||||
const ColliderInterfaceType& collider_b)
|
||||
{
|
||||
auto support = find_support_vertex(collider_a, collider_b, VectorType{1, 0, 0});
|
||||
|
||||
Simplex<VectorType> simplex;
|
||||
simplex.push_front(support);
|
||||
|
||||
auto direction = -support;
|
||||
@@ -37,12 +50,12 @@ namespace omath::collision
|
||||
support = find_support_vertex(collider_a, collider_b, direction);
|
||||
|
||||
if (support.dot(direction) <= 0.f)
|
||||
return false;
|
||||
return {false, simplex};
|
||||
|
||||
simplex.push_front(support);
|
||||
|
||||
if (simplex.handle(direction))
|
||||
return true;
|
||||
return {true, simplex};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,35 +3,53 @@
|
||||
//
|
||||
|
||||
#pragma once
|
||||
#include "collider_interface.hpp"
|
||||
#include "omath/linear_algebra/vector3.hpp"
|
||||
|
||||
#ifdef OMATH_BUILD_TESTS
|
||||
// ReSharper disable once CppInconsistentNaming
|
||||
class UnitTestColider_FindFurthestVertex_Test;
|
||||
#endif
|
||||
|
||||
namespace omath::collision
|
||||
{
|
||||
template<class MeshType>
|
||||
class MeshCollider
|
||||
class MeshCollider final : public ColliderInterface<typename MeshType::VertexType::VectorType>
|
||||
{
|
||||
#ifdef OMATH_BUILD_TESTS
|
||||
friend UnitTestColider_FindFurthestVertex_Test;
|
||||
#endif
|
||||
public:
|
||||
using NumericType = typename MeshType::NumericType;
|
||||
|
||||
using VertexType = Vector3<NumericType>;
|
||||
using VertexType = MeshType::VertexType;
|
||||
using VectorType = MeshType::VertexType::VectorType;
|
||||
explicit MeshCollider(MeshType mesh): m_mesh(std::move(mesh))
|
||||
{
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
const Vector3<float>& find_furthest_vertex(const Vector3<float>& direction) const
|
||||
VectorType find_abs_furthest_vertex_position(const VectorType& direction) const override
|
||||
{
|
||||
return *std::ranges::max_element(m_mesh.m_vertex_buffer, [&direction](const auto& first, const auto& second)
|
||||
{ return first.dot(direction) < second.dot(direction); });
|
||||
return m_mesh.vertex_position_to_world_space(find_furthest_vertex(direction).position);
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
Vector3<float> find_abs_furthest_vertex(const Vector3<float>& direction) const
|
||||
const VectorType& get_origin() const override
|
||||
{
|
||||
return m_mesh.vertex_to_world_space(find_furthest_vertex(direction));
|
||||
return m_mesh.get_origin();
|
||||
}
|
||||
void set_origin(const VectorType& new_origin) override
|
||||
{
|
||||
m_mesh.set_origin(new_origin);
|
||||
}
|
||||
|
||||
private:
|
||||
[[nodiscard]]
|
||||
const VertexType& find_furthest_vertex(const VectorType& direction) const
|
||||
{
|
||||
return *std::ranges::max_element(
|
||||
m_mesh.m_vertex_buffer, [&direction](const auto& first, const auto& second)
|
||||
{ return first.position.dot(direction) < second.position.dot(direction); });
|
||||
}
|
||||
MeshType m_mesh;
|
||||
};
|
||||
} // namespace omath::collision
|
||||
@@ -47,10 +47,13 @@ namespace omath::collision
|
||||
++m_size;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
constexpr const VectorType& operator[](std::size_t i) const noexcept
|
||||
{
|
||||
return m_points[i];
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
constexpr VectorType& operator[](std::size_t i) noexcept
|
||||
{
|
||||
return m_points[i];
|
||||
@@ -126,23 +129,23 @@ namespace omath::collision
|
||||
}
|
||||
|
||||
template<class V>
|
||||
[[nodiscard]]
|
||||
static constexpr bool near_zero(const V& v, const float eps = 1e-7f)
|
||||
{
|
||||
return v.dot(v) <= eps * eps;
|
||||
}
|
||||
|
||||
template<class V>
|
||||
[[nodiscard]]
|
||||
static constexpr V any_perp(const V& v)
|
||||
{
|
||||
// try cross with axes until non-zero
|
||||
V d = v.cross(V{1, 0, 0});
|
||||
if (near_zero(d))
|
||||
d = v.cross(V{0, 1, 0});
|
||||
if (near_zero(d))
|
||||
d = v.cross(V{0, 0, 1});
|
||||
return d;
|
||||
for (const auto& dir : {V{1, 0, 0}, {0, 1, 0}, {0, 0, 1}})
|
||||
if (const auto d = v.cross(dir); !near_zero(d))
|
||||
return d;
|
||||
std::unreachable();
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
constexpr bool handle_line(VectorType& direction)
|
||||
{
|
||||
const auto& a = m_points[0];
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
|
||||
namespace omath::frostbite_engine
|
||||
{
|
||||
using Mesh = primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float>;
|
||||
using Mesh = primitives::Mesh<Mat4X4, ViewAngles, MeshTrait>;
|
||||
}
|
||||
@@ -8,5 +8,5 @@
|
||||
|
||||
namespace omath::iw_engine
|
||||
{
|
||||
using Mesh = primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float>;
|
||||
using Mesh = primitives::Mesh<Mat4X4, ViewAngles, MeshTrait>;
|
||||
}
|
||||
@@ -8,5 +8,5 @@
|
||||
|
||||
namespace omath::opengl_engine
|
||||
{
|
||||
using Mesh = primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float>;
|
||||
using Mesh = primitives::Mesh<Mat4X4, ViewAngles, MeshTrait>;
|
||||
}
|
||||
@@ -8,5 +8,5 @@
|
||||
|
||||
namespace omath::source_engine
|
||||
{
|
||||
using Mesh = primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float>;
|
||||
using Mesh = primitives::Mesh<Mat4X4, ViewAngles, MeshTrait>;
|
||||
}
|
||||
@@ -17,6 +17,13 @@
|
||||
|
||||
#undef near
|
||||
#undef far
|
||||
// Undefine FreeBSD/BSD system macros that conflict with method names
|
||||
#ifdef minor
|
||||
#undef minor
|
||||
#endif
|
||||
#ifdef major
|
||||
#undef major
|
||||
#endif
|
||||
namespace omath
|
||||
{
|
||||
struct MatSize
|
||||
@@ -46,7 +53,7 @@ namespace omath
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
constexpr static MatStoreType get_store_ordering() noexcept
|
||||
consteval static MatStoreType get_store_ordering() noexcept
|
||||
{
|
||||
return StoreType;
|
||||
}
|
||||
@@ -373,7 +380,7 @@ namespace omath
|
||||
{
|
||||
const auto det = determinant();
|
||||
|
||||
if (det == 0)
|
||||
if (std::abs(det) < std::numeric_limits<Type>::epsilon())
|
||||
return std::nullopt;
|
||||
|
||||
const auto transposed_mat = transposed();
|
||||
|
||||
@@ -26,12 +26,12 @@ namespace omath
|
||||
{
|
||||
}
|
||||
|
||||
Vector3<float> m_vertex1;
|
||||
Vector3<float> m_vertex2;
|
||||
Vector3<float> m_vertex3;
|
||||
Vector m_vertex1;
|
||||
Vector m_vertex2;
|
||||
Vector m_vertex3;
|
||||
|
||||
[[nodiscard]]
|
||||
constexpr Vector3<float> calculate_normal() const
|
||||
constexpr Vector calculate_normal() const
|
||||
{
|
||||
const auto b = side_b_vector();
|
||||
const auto a = side_a_vector();
|
||||
@@ -40,25 +40,25 @@ namespace omath
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
float side_a_length() const
|
||||
Vector::ContainedType side_a_length() const
|
||||
{
|
||||
return m_vertex1.distance_to(m_vertex2);
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
float side_b_length() const
|
||||
Vector::ContainedType side_b_length() const
|
||||
{
|
||||
return m_vertex3.distance_to(m_vertex2);
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
constexpr Vector3<float> side_a_vector() const
|
||||
constexpr Vector side_a_vector() const
|
||||
{
|
||||
return m_vertex1 - m_vertex2;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
constexpr float hypot() const
|
||||
constexpr Vector::ContainedType hypot() const
|
||||
{
|
||||
return m_vertex1.distance_to(m_vertex3);
|
||||
}
|
||||
@@ -72,12 +72,12 @@ namespace omath
|
||||
return std::abs(side_a * side_a + side_b * side_b - hypot_value * hypot_value) <= 0.0001f;
|
||||
}
|
||||
[[nodiscard]]
|
||||
constexpr Vector3<float> side_b_vector() const
|
||||
constexpr Vector side_b_vector() const
|
||||
{
|
||||
return m_vertex3 - m_vertex2;
|
||||
}
|
||||
[[nodiscard]]
|
||||
constexpr Vector3<float> mid_point() const
|
||||
constexpr Vector mid_point() const
|
||||
{
|
||||
return (m_vertex1 + m_vertex2 + m_vertex3) / 3;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace omath
|
||||
class Vector2
|
||||
{
|
||||
public:
|
||||
using ContainedType = Type;
|
||||
Type x = static_cast<Type>(0);
|
||||
Type y = static_cast<Type>(0);
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace omath
|
||||
class Vector3 : public Vector2<Type>
|
||||
{
|
||||
public:
|
||||
using ContainedType = Type;
|
||||
Type z = static_cast<Type>(0);
|
||||
constexpr Vector3(const Type& x, const Type& y, const Type& z) noexcept: Vector2<Type>(x, y), z(z)
|
||||
{
|
||||
@@ -232,10 +233,10 @@ namespace omath
|
||||
return Angle<float, 0.f, 180.f, AngleFlags::Clamped>::from_radians(std::acos(dot(other) / bottom));
|
||||
}
|
||||
|
||||
[[nodiscard]] bool is_perpendicular(const Vector3& other) 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))
|
||||
return angle->as_degrees() == static_cast<Type>(90);
|
||||
return std::abs(angle->as_degrees() - static_cast<Type>(90)) <= epsilon;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace omath
|
||||
class Vector4 : public Vector3<Type>
|
||||
{
|
||||
public:
|
||||
using ContainedType = Type;
|
||||
Type w;
|
||||
|
||||
constexpr Vector4(const Type& x, const Type& y, const Type& z, const Type& w): Vector3<Type>(x, y, z), w(w)
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
|
||||
// Collision detection
|
||||
#include "omath/collision/line_tracer.hpp"
|
||||
|
||||
#include "omath/collision/gjk_algorithm.hpp"
|
||||
#include "omath/collision/epa_algorithm.hpp"
|
||||
// Pathfinding algorithms
|
||||
#include "omath/pathfinding/a_star.hpp"
|
||||
#include "omath/pathfinding/navigation_mesh.hpp"
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "omath/linear_algebra/mat.hpp"
|
||||
#include "omath/linear_algebra/triangle.hpp"
|
||||
#include "omath/linear_algebra/vector3.hpp"
|
||||
#include "omath/projection/error_codes.hpp"
|
||||
#include <cmath>
|
||||
#include <expected>
|
||||
#include <omath/trigonometry/angle.hpp>
|
||||
#include <type_traits>
|
||||
@@ -175,16 +177,64 @@ namespace omath::projection
|
||||
std::unreachable();
|
||||
}
|
||||
|
||||
[[nodiscard]] bool is_culled_by_frustum(const Triangle<Vector3<float>>& triangle) const noexcept
|
||||
{
|
||||
// Transform to clip space (before perspective divide)
|
||||
auto to_clip = [this](const Vector3<float>& point)
|
||||
{
|
||||
auto clip = get_view_projection_matrix()
|
||||
* mat_column_from_vector<float, Mat4X4Type::get_store_ordering()>(point);
|
||||
return std::array<float, 4>{
|
||||
clip.at(0, 0), // x
|
||||
clip.at(1, 0), // y
|
||||
clip.at(2, 0), // z
|
||||
clip.at(3, 0) // w
|
||||
};
|
||||
};
|
||||
|
||||
const auto c0 = to_clip(triangle.m_vertex1);
|
||||
const auto c1 = to_clip(triangle.m_vertex2);
|
||||
const auto c2 = to_clip(triangle.m_vertex3);
|
||||
|
||||
// If all vertices are behind the camera (w <= 0), trivially reject
|
||||
if (c0[3] <= 0.f && c1[3] <= 0.f && c2[3] <= 0.f)
|
||||
return true;
|
||||
|
||||
// Helper: all three vertices outside the same clip plane
|
||||
auto all_outside_plane = [](const int axis, const std::array<float, 4>& a, const std::array<float, 4>& b,
|
||||
const std::array<float, 4>& c, const bool positive_side)
|
||||
{
|
||||
if (positive_side)
|
||||
return a[axis] > a[3] && b[axis] > b[3] && c[axis] > c[3];
|
||||
return a[axis] < -a[3] && b[axis] < -b[3] && c[axis] < -c[3];
|
||||
};
|
||||
|
||||
// Clip volume in clip space (OpenGL-style):
|
||||
// -w <= x <= w
|
||||
// -w <= y <= w
|
||||
// -w <= z <= w
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
if (all_outside_plane(i, c0, c1, c2, false))
|
||||
return true; // x < -w (left)
|
||||
if (all_outside_plane(i, c0, c1, c2, true))
|
||||
return true; // x > w (right)
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::expected<Vector3<float>, Error>
|
||||
world_to_view_port(const Vector3<float>& world_position) const noexcept
|
||||
{
|
||||
auto projected = get_view_projection_matrix()
|
||||
* mat_column_from_vector<float, Mat4X4Type::get_store_ordering()>(world_position);
|
||||
|
||||
if (projected.at(3, 0) == 0.0f)
|
||||
const auto& w = projected.at(3, 0);
|
||||
if (w <= std::numeric_limits<float>::epsilon())
|
||||
return std::unexpected(Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS);
|
||||
|
||||
projected /= projected.at(3, 0);
|
||||
projected /= w;
|
||||
|
||||
if (is_ndc_out_of_bounds(projected))
|
||||
return std::unexpected(Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS);
|
||||
@@ -202,10 +252,12 @@ namespace omath::projection
|
||||
auto inverted_projection =
|
||||
inv_view_proj.value() * mat_column_from_vector<float, Mat4X4Type::get_store_ordering()>(ndc);
|
||||
|
||||
if (!inverted_projection.at(3, 0))
|
||||
const auto& w = inverted_projection.at(3, 0);
|
||||
|
||||
if (std::abs(w) < std::numeric_limits<float>::epsilon())
|
||||
return std::unexpected(Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS);
|
||||
|
||||
inverted_projection /= inverted_projection.at(3, 0);
|
||||
inverted_projection /= w;
|
||||
|
||||
return Vector3<float>{inverted_projection.at(0, 0), inverted_projection.at(1, 0),
|
||||
inverted_projection.at(2, 0)};
|
||||
@@ -242,7 +294,9 @@ namespace omath::projection
|
||||
template<class Type>
|
||||
[[nodiscard]] constexpr static bool is_ndc_out_of_bounds(const Type& ndc) noexcept
|
||||
{
|
||||
return std::ranges::any_of(ndc.raw_array(), [](const auto& val) { return val < -1 || val > 1; });
|
||||
constexpr auto eps = std::numeric_limits<float>::epsilon();
|
||||
return std::ranges::any_of(ndc.raw_array(),
|
||||
[](const auto& val) { return val < -1.0f - eps || val > 1.0f + eps; });
|
||||
}
|
||||
|
||||
// NDC REPRESENTATION:
|
||||
@@ -299,7 +353,7 @@ namespace omath::projection
|
||||
if constexpr (screen_start == ScreenStart::TOP_LEFT_CORNER)
|
||||
return {screen_pos.x / m_view_port.m_width * 2.f - 1.f, 1.f - screen_pos.y / m_view_port.m_height * 2.f,
|
||||
screen_pos.z};
|
||||
else if (screen_start == ScreenStart::BOTTOM_LEFT_CORNER)
|
||||
else if constexpr (screen_start == ScreenStart::BOTTOM_LEFT_CORNER)
|
||||
return {screen_pos.x / m_view_port.m_width * 2.f - 1.f,
|
||||
(screen_pos.y / m_view_port.m_height - 0.5f) * 2.f, screen_pos.z};
|
||||
else
|
||||
|
||||
@@ -46,27 +46,26 @@ namespace omath
|
||||
|
||||
switch (i % 6)
|
||||
{
|
||||
case 0:
|
||||
r = value, g = t, b = p;
|
||||
break;
|
||||
case 1:
|
||||
r = q, g = value, b = p;
|
||||
break;
|
||||
case 2:
|
||||
r = p, g = value, b = t;
|
||||
break;
|
||||
case 3:
|
||||
r = p, g = q, b = value;
|
||||
break;
|
||||
case 4:
|
||||
r = t, g = p, b = value;
|
||||
break;
|
||||
case 5:
|
||||
r = value, g = p, b = q;
|
||||
break;
|
||||
|
||||
default:
|
||||
return {0.f, 0.f, 0.f, 0.f};
|
||||
case 0:
|
||||
r = value, g = t, b = p;
|
||||
break;
|
||||
case 1:
|
||||
r = q, g = value, b = p;
|
||||
break;
|
||||
case 2:
|
||||
r = p, g = value, b = t;
|
||||
break;
|
||||
case 3:
|
||||
r = p, g = q, b = value;
|
||||
break;
|
||||
case 4:
|
||||
r = t, g = p, b = value;
|
||||
break;
|
||||
case 5:
|
||||
r = value, g = p, b = q;
|
||||
break;
|
||||
default:
|
||||
std::unreachable();
|
||||
}
|
||||
|
||||
return {r, g, b, 1.f};
|
||||
@@ -190,7 +189,7 @@ template<>
|
||||
struct std::formatter<omath::Color> // NOLINT(*-dcl58-cpp)
|
||||
{
|
||||
[[nodiscard]]
|
||||
static constexpr auto parse(std::format_parse_context& ctx)
|
||||
static constexpr auto parse(const std::format_parse_context& ctx)
|
||||
{
|
||||
return ctx.begin();
|
||||
}
|
||||
@@ -207,6 +206,6 @@ struct std::formatter<omath::Color> // NOLINT(*-dcl58-cpp)
|
||||
if constexpr (std::is_same_v<typename FormatContext::char_type, char8_t>)
|
||||
return std::format_to(ctx.out(), u8"{}", col.to_u8string());
|
||||
|
||||
return std::unreachable();
|
||||
std::unreachable();
|
||||
}
|
||||
};
|
||||
@@ -51,9 +51,13 @@ namespace omath
|
||||
|
||||
const auto whole_range_size = static_cast<std::ptrdiff_t>(std::distance(begin, end));
|
||||
|
||||
const std::ptrdiff_t scan_size = whole_range_size - static_cast<std::ptrdiff_t>(pattern.size());
|
||||
const auto pattern_size = static_cast<std::ptrdiff_t>(parsed_pattern->size());
|
||||
const std::ptrdiff_t scan_size = whole_range_size - pattern_size;
|
||||
|
||||
for (std::ptrdiff_t i = 0; i < scan_size; i++)
|
||||
if (scan_size < 0)
|
||||
return end;
|
||||
|
||||
for (std::ptrdiff_t i = 0; i <= scan_size; i++)
|
||||
{
|
||||
bool found = true;
|
||||
|
||||
|
||||
@@ -3,4 +3,5 @@ theme:
|
||||
name: darkly
|
||||
extra_css:
|
||||
- styles/center.css
|
||||
- styles/custom-header.css
|
||||
- styles/custom-header.css
|
||||
- styles/links.css
|
||||
169
scripts/coverage-llvm.sh
Executable file
169
scripts/coverage-llvm.sh
Executable file
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/coverage-llvm.sh
|
||||
# LLVM coverage script that generates LCOV-style reports
|
||||
|
||||
set -e
|
||||
|
||||
SOURCE_DIR="${1:-.}"
|
||||
BINARY_DIR="${2:-cmake-build/build}"
|
||||
TEST_BINARY="${3:-}"
|
||||
OUTPUT_DIR="${4:-${BINARY_DIR}/coverage}"
|
||||
|
||||
echo "[*] Source dir: ${SOURCE_DIR}"
|
||||
echo "[*] Binary dir: ${BINARY_DIR}"
|
||||
echo "[*] Output dir: ${OUTPUT_DIR}"
|
||||
|
||||
# Find llvm tools - handle versioned names (Linux) and xcrun (macOS)
|
||||
find_llvm_tool() {
|
||||
local tool_name="$1"
|
||||
|
||||
# macOS: use xcrun
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
if xcrun --find "${tool_name}" &>/dev/null; then
|
||||
echo "xcrun ${tool_name}"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Try versioned names (Linux with LLVM 21, 20, 19, etc.)
|
||||
for version in 21 20 19 18 17 ""; do
|
||||
local versioned_name="${tool_name}${version:+-$version}"
|
||||
if command -v "${versioned_name}" &>/dev/null; then
|
||||
echo "${versioned_name}"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
return 1
|
||||
}
|
||||
|
||||
LLVM_PROFDATA=$(find_llvm_tool "llvm-profdata")
|
||||
LLVM_COV=$(find_llvm_tool "llvm-cov")
|
||||
|
||||
if [[ -z "${LLVM_PROFDATA}" ]] || [[ -z "${LLVM_COV}" ]]; then
|
||||
echo "Error: llvm-profdata or llvm-cov not found" >&2
|
||||
echo "On Linux, install llvm or clang package" >&2
|
||||
echo "On macOS, Xcode command line tools should provide these" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[*] Using: ${LLVM_PROFDATA}"
|
||||
echo "[*] Using: ${LLVM_COV}"
|
||||
|
||||
# Find test binary
|
||||
if [[ -z "${TEST_BINARY}" ]]; then
|
||||
for path in \
|
||||
"${SOURCE_DIR}/out/Debug/unit_tests" \
|
||||
"${SOURCE_DIR}/out/Release/unit_tests" \
|
||||
"${BINARY_DIR}/unit_tests" \
|
||||
"${BINARY_DIR}/tests/unit_tests"; do
|
||||
if [[ -x "${path}" ]]; then
|
||||
TEST_BINARY="${path}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ -z "${TEST_BINARY}" ]] || [[ ! -x "${TEST_BINARY}" ]]; then
|
||||
echo "Error: unit_tests binary not found" >&2
|
||||
echo "Searched in: out/Debug, out/Release, ${BINARY_DIR}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[*] Test binary: ${TEST_BINARY}"
|
||||
|
||||
# Clean previous coverage data
|
||||
rm -rf "${OUTPUT_DIR}"
|
||||
rm -f "${BINARY_DIR}"/*.profraw "${BINARY_DIR}"/*.profdata
|
||||
mkdir -p "${OUTPUT_DIR}"
|
||||
|
||||
# Run tests with profiling enabled
|
||||
PROFILE_FILE="${BINARY_DIR}/default_%p.profraw"
|
||||
echo "[*] Running tests with LLVM_PROFILE_FILE=${PROFILE_FILE}"
|
||||
|
||||
export LLVM_PROFILE_FILE="${PROFILE_FILE}"
|
||||
"${TEST_BINARY}" || echo "[!] Some tests failed, continuing with coverage..."
|
||||
|
||||
# Find all generated .profraw files
|
||||
PROFRAW_FILES=$(find "${BINARY_DIR}" -name "*.profraw" -type f 2>/dev/null)
|
||||
if [[ -z "${PROFRAW_FILES}" ]]; then
|
||||
# Also check current directory
|
||||
PROFRAW_FILES=$(find . -maxdepth 3 -name "*.profraw" -type f 2>/dev/null)
|
||||
fi
|
||||
|
||||
if [[ -z "${PROFRAW_FILES}" ]]; then
|
||||
echo "Error: No .profraw files generated" >&2
|
||||
echo "Make sure the binary was built with -fprofile-instr-generate -fcoverage-mapping" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[*] Found profraw files:"
|
||||
echo "${PROFRAW_FILES}"
|
||||
|
||||
# Merge profiles
|
||||
PROFDATA_FILE="${BINARY_DIR}/coverage.profdata"
|
||||
echo "[*] Merging profiles into ${PROFDATA_FILE}"
|
||||
${LLVM_PROFDATA} merge -sparse ${PROFRAW_FILES} -o "${PROFDATA_FILE}"
|
||||
|
||||
# Generate text summary
|
||||
echo "[*] Coverage Summary:"
|
||||
${LLVM_COV} report "${TEST_BINARY}" \
|
||||
-instr-profile="${PROFDATA_FILE}" \
|
||||
-ignore-filename-regex="tests/.*" \
|
||||
-ignore-filename-regex="googletest/.*" \
|
||||
-ignore-filename-regex="gtest/.*" \
|
||||
-ignore-filename-regex="_deps/.*" \
|
||||
-ignore-filename-regex="vcpkg_installed/.*"
|
||||
|
||||
# Export lcov format (for tools like codecov)
|
||||
LCOV_FILE="${OUTPUT_DIR}/coverage.lcov"
|
||||
echo "[*] Exporting LCOV format to ${LCOV_FILE}"
|
||||
${LLVM_COV} export "${TEST_BINARY}" \
|
||||
-instr-profile="${PROFDATA_FILE}" \
|
||||
-format=lcov \
|
||||
-ignore-filename-regex="tests/.*" \
|
||||
-ignore-filename-regex="googletest/.*" \
|
||||
-ignore-filename-regex="gtest/.*" \
|
||||
-ignore-filename-regex="_deps/.*" \
|
||||
-ignore-filename-regex="vcpkg_installed/.*" \
|
||||
> "${LCOV_FILE}" || true
|
||||
|
||||
# Generate LCOV-style HTML report using genhtml
|
||||
if command -v genhtml >/dev/null 2>&1; then
|
||||
echo "[*] Generating LCOV-style HTML report using genhtml"
|
||||
genhtml "${LCOV_FILE}" \
|
||||
--ignore-errors inconsistent,corrupt \
|
||||
--output-directory "${OUTPUT_DIR}" \
|
||||
--title "Omath Coverage Report" \
|
||||
--show-details \
|
||||
--legend \
|
||||
--demangle-cpp \
|
||||
--num-spaces 4 \
|
||||
--sort \
|
||||
--function-coverage \
|
||||
--branch-coverage
|
||||
|
||||
echo "[*] LCOV-style HTML report generated at: ${OUTPUT_DIR}/index.html"
|
||||
else
|
||||
echo "[!] genhtml not found. Installing lcov package..."
|
||||
echo "[!] On Ubuntu/Debian: sudo apt-get install lcov"
|
||||
echo "[!] On macOS: brew install lcov"
|
||||
echo "[!] Falling back to LLVM HTML report..."
|
||||
|
||||
# Fall back to LLVM HTML report
|
||||
${LLVM_COV} show "${TEST_BINARY}" \
|
||||
-instr-profile="${PROFDATA_FILE}" \
|
||||
-format=html \
|
||||
-output-dir="${OUTPUT_DIR}" \
|
||||
-show-line-counts-or-regions \
|
||||
-show-instantiations=false \
|
||||
-ignore-filename-regex="tests/.*" \
|
||||
-ignore-filename-regex="googletest/.*" \
|
||||
-ignore-filename-regex="gtest/.*" \
|
||||
-ignore-filename-regex="_deps/.*" \
|
||||
-ignore-filename-regex="vcpkg_installed/.*"
|
||||
fi
|
||||
|
||||
echo "[*] Coverage report generated at: ${OUTPUT_DIR}/index.html"
|
||||
echo "[*] LCOV file at: ${LCOV_FILE}"
|
||||
8
scripts/coverage.bat.in
Normal file
8
scripts/coverage.bat.in
Normal file
@@ -0,0 +1,8 @@
|
||||
@echo off
|
||||
REM scripts/coverage.bat.in
|
||||
REM Simple wrapper to run coverage.ps1
|
||||
|
||||
set SOURCE_DIR=@CMAKE_SOURCE_DIR@
|
||||
set BINARY_DIR=@CMAKE_BINARY_DIR@
|
||||
|
||||
powershell -ExecutionPolicy Bypass -File "%BINARY_DIR%\scripts\coverage.ps1" -SourceDir "%SOURCE_DIR%" -BinaryDir "%BINARY_DIR%" %*
|
||||
132
scripts/coverage.ps1.in
Normal file
132
scripts/coverage.ps1.in
Normal file
@@ -0,0 +1,132 @@
|
||||
# scripts/coverage.ps1.in
|
||||
# Windows coverage script using OpenCppCoverage
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$SourceDir,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$BinaryDir,
|
||||
|
||||
[string]$TestBinary = "",
|
||||
[switch]$Cobertura,
|
||||
[switch]$Html
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# CMake-injected variables
|
||||
$LCOV_IGNORE_ERRORS = '@LCOV_IGNORE_ERRORS@'
|
||||
|
||||
# Resolve paths
|
||||
$SourceDir = Resolve-Path $SourceDir
|
||||
$BinaryDir = Resolve-Path $BinaryDir
|
||||
|
||||
Write-Host "[*] Source directory: $SourceDir" -ForegroundColor Cyan
|
||||
Write-Host "[*] Binary directory: $BinaryDir" -ForegroundColor Cyan
|
||||
|
||||
# Find test binary
|
||||
if (-not $TestBinary) {
|
||||
$searchPaths = @(
|
||||
"$BinaryDir\Debug\unit_tests.exe",
|
||||
"$BinaryDir\Release\unit_tests.exe",
|
||||
"$BinaryDir\unit_tests.exe",
|
||||
"$SourceDir\out\Debug\unit_tests.exe",
|
||||
"$SourceDir\out\Release\unit_tests.exe"
|
||||
)
|
||||
|
||||
foreach ($path in $searchPaths) {
|
||||
if (Test-Path $path) {
|
||||
$TestBinary = $path
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $TestBinary -or -not (Test-Path $TestBinary)) {
|
||||
Write-Error "unit_tests.exe not found. Searched: $($searchPaths -join ', ')"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$TestBinary = Resolve-Path $TestBinary
|
||||
Write-Host "[*] Test binary: $TestBinary" -ForegroundColor Cyan
|
||||
|
||||
# Check for OpenCppCoverage
|
||||
$opencppcov = Get-Command "OpenCppCoverage" -ErrorAction SilentlyContinue
|
||||
if (-not $opencppcov) {
|
||||
# Try common installation paths
|
||||
$possiblePaths = @(
|
||||
"$env:ProgramFiles\OpenCppCoverage\OpenCppCoverage.exe",
|
||||
"${env:ProgramFiles(x86)}\OpenCppCoverage\OpenCppCoverage.exe",
|
||||
"$env:LOCALAPPDATA\Programs\OpenCppCoverage\OpenCppCoverage.exe"
|
||||
)
|
||||
|
||||
foreach ($path in $possiblePaths) {
|
||||
if (Test-Path $path) {
|
||||
$opencppcov = Get-Item $path
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $opencppcov) {
|
||||
Write-Host @"
|
||||
OpenCppCoverage not found!
|
||||
|
||||
Install it from: https://github.com/OpenCppCoverage/OpenCppCoverage/releases
|
||||
|
||||
Or via Chocolatey:
|
||||
choco install opencppcoverage
|
||||
|
||||
Or via winget:
|
||||
winget install OpenCppCoverage.OpenCppCoverage
|
||||
"@ -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
$OpenCppCoveragePath = if ($opencppcov.Source) { $opencppcov.Source } else { $opencppcov.FullName }
|
||||
Write-Host "[*] Using OpenCppCoverage: $OpenCppCoveragePath" -ForegroundColor Cyan
|
||||
|
||||
# Create output directory
|
||||
$CoverageDir = Join-Path $BinaryDir "coverage"
|
||||
if (-not (Test-Path $CoverageDir)) {
|
||||
New-Item -ItemType Directory -Path $CoverageDir | Out-Null
|
||||
}
|
||||
|
||||
# Build OpenCppCoverage arguments
|
||||
$coverageArgs = @(
|
||||
"--sources", "$SourceDir\include",
|
||||
"--sources", "$SourceDir\source",
|
||||
"--excluded_sources", "*\tests\*",
|
||||
"--excluded_sources", "*\googletest\*",
|
||||
"--excluded_sources", "*\gtest\*",
|
||||
"--excluded_sources", "*\_deps\*",
|
||||
"--excluded_sources", "*\vcpkg_installed\*",
|
||||
"--export_type", "html:$CoverageDir",
|
||||
"--export_type", "cobertura:$CoverageDir\coverage.xml",
|
||||
"--cover_children",
|
||||
"--"
|
||||
)
|
||||
|
||||
Write-Host "[*] Running OpenCppCoverage..." -ForegroundColor Cyan
|
||||
Write-Host " Command: $OpenCppCoveragePath $($coverageArgs -join ' ') $TestBinary"
|
||||
|
||||
& $OpenCppCoveragePath @coverageArgs $TestBinary
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Warning "OpenCppCoverage exited with code $LASTEXITCODE (tests may have failed)"
|
||||
}
|
||||
|
||||
# Check outputs
|
||||
$htmlIndex = Join-Path $CoverageDir "index.html"
|
||||
$coberturaXml = Join-Path $CoverageDir "coverage.xml"
|
||||
|
||||
if (Test-Path $htmlIndex) {
|
||||
Write-Host "[*] HTML coverage report: $htmlIndex" -ForegroundColor Green
|
||||
}
|
||||
|
||||
if (Test-Path $coberturaXml) {
|
||||
Write-Host "[*] Cobertura XML report: $coberturaXml" -ForegroundColor Green
|
||||
}
|
||||
|
||||
Write-Host "[*] Coverage collection complete!" -ForegroundColor Green
|
||||
@@ -37,13 +37,6 @@ namespace omath::unreal_engine
|
||||
Mat4X4 calc_perspective_projection_matrix(const float field_of_view, const float aspect_ratio, const float near,
|
||||
const float far) noexcept
|
||||
{
|
||||
const float fov_half_tan = std::tan(angles::degrees_to_radians(field_of_view) / 2.f);
|
||||
|
||||
return {
|
||||
{1.f / (aspect_ratio * fov_half_tan), 0, 0, 0},
|
||||
{0, 1.f / (fov_half_tan), 0, 0},
|
||||
{0, 0, (far + near) / (far - near), -(2.f * far * near) / (far - near)},
|
||||
{0, 0, -1.f, 0},
|
||||
};
|
||||
return mat_perspective_left_handed(field_of_view, aspect_ratio, near, far);
|
||||
}
|
||||
} // namespace omath::unreal_engine
|
||||
|
||||
@@ -320,7 +320,7 @@ namespace omath
|
||||
return std::visit(
|
||||
[base_address, &pattern](const auto& nt_header) -> std::optional<std::uintptr_t>
|
||||
{
|
||||
// Define .code segment as scan area
|
||||
// Define .text segment as scan area
|
||||
const auto start = nt_header.optional_header.base_of_code;
|
||||
const auto scan_size = nt_header.optional_header.size_code;
|
||||
|
||||
|
||||
@@ -14,12 +14,19 @@ set_target_properties(${PROJECT_NAME} PROPERTIES
|
||||
CXX_STANDARD 23
|
||||
CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
|
||||
|
||||
if (TARGET gtest) # GTest is being linked as submodule
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE gtest gtest_main omath::omath)
|
||||
else() # GTest is being linked as vcpkg package
|
||||
find_package(GTest CONFIG REQUIRED)
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE GTest::gtest GTest::gtest_main omath::omath)
|
||||
endif()
|
||||
gtest_discover_tests(${PROJECT_NAME})
|
||||
|
||||
if(OMATH_ENABLE_COVERAGE)
|
||||
include(${CMAKE_SOURCE_DIR}/cmake/Coverage.cmake)
|
||||
omath_setup_coverage(${PROJECT_NAME})
|
||||
endif()
|
||||
|
||||
# Skip test discovery for Android/iOS builds or when cross-compiling - binaries cannot run on host
|
||||
if (NOT (ANDROID OR IOS OR EMSCRIPTEN))
|
||||
gtest_discover_tests(${PROJECT_NAME})
|
||||
endif()
|
||||
|
||||
297
tests/engines/unit_test_traits_engines.cpp
Normal file
297
tests/engines/unit_test_traits_engines.cpp
Normal file
@@ -0,0 +1,297 @@
|
||||
// Tests for engine trait headers to improve header coverage
|
||||
#include <gtest/gtest.h>
|
||||
#include <omath/engines/frostbite_engine/traits/pred_engine_trait.hpp>
|
||||
#include <omath/engines/frostbite_engine/traits/mesh_trait.hpp>
|
||||
#include <omath/engines/frostbite_engine/traits/camera_trait.hpp>
|
||||
|
||||
#include <omath/engines/iw_engine/traits/pred_engine_trait.hpp>
|
||||
#include <omath/engines/iw_engine/traits/mesh_trait.hpp>
|
||||
#include <omath/engines/iw_engine/traits/camera_trait.hpp>
|
||||
|
||||
#include <omath/engines/opengl_engine/traits/pred_engine_trait.hpp>
|
||||
#include <omath/engines/opengl_engine/traits/mesh_trait.hpp>
|
||||
#include <omath/engines/opengl_engine/traits/camera_trait.hpp>
|
||||
|
||||
#include <omath/engines/unity_engine/traits/pred_engine_trait.hpp>
|
||||
#include <omath/engines/unity_engine/traits/mesh_trait.hpp>
|
||||
#include <omath/engines/unity_engine/traits/camera_trait.hpp>
|
||||
|
||||
#include <omath/engines/unreal_engine/traits/pred_engine_trait.hpp>
|
||||
#include <omath/engines/unreal_engine/traits/mesh_trait.hpp>
|
||||
#include <omath/engines/unreal_engine/traits/camera_trait.hpp>
|
||||
|
||||
#include <omath/projectile_prediction/projectile.hpp>
|
||||
#include <omath/projectile_prediction/target.hpp>
|
||||
#include <optional>
|
||||
|
||||
using namespace omath;
|
||||
|
||||
// Small helper to compare matrices roughly (templated to avoid concrete typedef)
|
||||
template<typename MatT>
|
||||
static void expect_matrix_near(const MatT& a, const MatT& b, float eps = 1e-5f)
|
||||
{
|
||||
for (std::size_t r = 0; r < 4; ++r)
|
||||
for (std::size_t c = 0; c < 4; ++c)
|
||||
EXPECT_NEAR(a.at(r, c), b.at(r, c), eps);
|
||||
}
|
||||
|
||||
// Generic tests for PredEngineTrait behaviour across engines
|
||||
TEST(TraitTests, Frostbite_Pred_And_Mesh_And_Camera)
|
||||
{
|
||||
namespace E = omath::frostbite_engine;
|
||||
|
||||
projectile_prediction::Projectile p;
|
||||
p.m_origin = {0.f, 0.f, 0.f};
|
||||
p.m_launch_speed = 10.f;
|
||||
p.m_gravity_scale = 1.f;
|
||||
|
||||
const auto pos = E::PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 1.f, 9.81f);
|
||||
EXPECT_NEAR(pos.x, 0.f, 1e-4f);
|
||||
EXPECT_NEAR(pos.z, 10.f, 1e-4f);
|
||||
EXPECT_NEAR(pos.y, -9.81f * 0.5f, 1e-4f);
|
||||
|
||||
projectile_prediction::Target t;
|
||||
t.m_origin = {0.f, 5.f, 0.f};
|
||||
t.m_velocity = {2.f, 0.f, 0.f};
|
||||
t.m_is_airborne = true;
|
||||
const auto pred = E::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
|
||||
EXPECT_NEAR(pred.x, 4.f, 1e-6f);
|
||||
EXPECT_NEAR(pred.y, 5.f - 9.81f * (2.f * 2.f) * 0.5f, 1e-6f);
|
||||
|
||||
// Also test non-airborne path (no gravity applied)
|
||||
t.m_is_airborne = false;
|
||||
const auto pred_ground = E::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
|
||||
EXPECT_NEAR(pred_ground.x, 4.f, 1e-6f);
|
||||
EXPECT_NEAR(pred_ground.y, 5.f, 1e-6f);
|
||||
|
||||
EXPECT_NEAR(E::PredEngineTrait::calc_vector_2d_distance({3.f, 0.f, 4.f}), 5.f, 1e-6f);
|
||||
EXPECT_NEAR(E::PredEngineTrait::get_vector_height_coordinate({1.f, 2.5f, 3.f}), 2.5f, 1e-6f);
|
||||
|
||||
std::optional<float> pitch = 45.f;
|
||||
auto vp = E::PredEngineTrait::calc_viewpoint_from_angles(p, {10.f, 0.f, 0.f}, pitch);
|
||||
EXPECT_NEAR(vp.y, 0.f + 10.f * std::tan(angles::degrees_to_radians(45.f)), 1e-6f);
|
||||
|
||||
// Direct angles
|
||||
Vector3<float> origin{0.f, 0.f, 0.f};
|
||||
Vector3<float> view_to{0.f, 1.f, 1.f};
|
||||
const auto pitch_calc = E::PredEngineTrait::calc_direct_pitch_angle(origin, view_to);
|
||||
const auto dir = (view_to - origin).normalized();
|
||||
EXPECT_NEAR(pitch_calc, angles::radians_to_degrees(std::asin(dir.y)), 1e-3f);
|
||||
|
||||
const auto yaw_calc = E::PredEngineTrait::calc_direct_yaw_angle(origin, view_to);
|
||||
EXPECT_NEAR(yaw_calc, angles::radians_to_degrees(std::atan2(dir.x, dir.z)), 1e-3f);
|
||||
|
||||
// MeshTrait simply forwards to rotation_matrix; ensure it compiles and returns something
|
||||
E::ViewAngles va;
|
||||
const auto m1 = E::MeshTrait::rotation_matrix(va);
|
||||
const auto m2 = E::rotation_matrix(va);
|
||||
expect_matrix_near(m1, m2);
|
||||
|
||||
// CameraTrait look at should be callable
|
||||
const auto angles = E::CameraTrait::calc_look_at_angle({0, 0, 0}, {0, 1, 1});
|
||||
(void)angles;
|
||||
const auto proj = E::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(60.f), {1280.f, 720.f}, 0.1f, 1000.f);
|
||||
const auto expected = E::calc_perspective_projection_matrix(60.f, 1280.f / 720.f, 0.1f, 1000.f);
|
||||
expect_matrix_near(proj, expected);
|
||||
}
|
||||
|
||||
TEST(TraitTests, IW_Pred_And_Mesh_And_Camera)
|
||||
{
|
||||
namespace E = omath::iw_engine;
|
||||
|
||||
projectile_prediction::Projectile p;
|
||||
p.m_origin = {0.f, 0.f, 0.f};
|
||||
p.m_launch_speed = 10.f;
|
||||
p.m_gravity_scale = 1.f;
|
||||
|
||||
const auto pos = E::PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 1.f, 9.81f);
|
||||
EXPECT_NEAR(pos.x, 10.f, 1e-4f);
|
||||
EXPECT_NEAR(pos.z, -9.81f * 0.5f, 1e-4f);
|
||||
|
||||
projectile_prediction::Target t;
|
||||
t.m_origin = {0.f, 0.f, 5.f};
|
||||
t.m_velocity = {0.f, 0.f, 2.f};
|
||||
t.m_is_airborne = true;
|
||||
const auto pred = E::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
|
||||
// predicted = origin + velocity * t -> z = 5 + 2*2 = 9; then gravity applied
|
||||
EXPECT_NEAR(pred.z, 9.f - 9.81f * (2.f * 2.f) * 0.5f, 1e-6f);
|
||||
|
||||
EXPECT_NEAR(E::PredEngineTrait::calc_vector_2d_distance({3.f, 4.f, 0.f}), 5.f, 1e-6f);
|
||||
EXPECT_NEAR(E::PredEngineTrait::get_vector_height_coordinate({1.f, 2.5f, 3.f}), 3.f, 1e-6f);
|
||||
|
||||
std::optional<float> pitch = 45.f;
|
||||
auto vp = E::PredEngineTrait::calc_viewpoint_from_angles(p, {10.f, 0.f, 0.f}, pitch);
|
||||
EXPECT_NEAR(vp.z, 0.f + 10.f * std::tan(angles::degrees_to_radians(45.f)), 1e-6f);
|
||||
|
||||
Vector3<float> origin{0.f, 0.f, 0.f};
|
||||
Vector3<float> view_to{1.f, 1.f, 1.f};
|
||||
const auto pitch_calc = E::PredEngineTrait::calc_direct_pitch_angle(origin, view_to);
|
||||
const auto dist = origin.distance_to(view_to);
|
||||
EXPECT_NEAR(pitch_calc, angles::radians_to_degrees(std::asin((view_to.z - origin.z) / dist)), 1e-3f);
|
||||
|
||||
const auto yaw_calc = E::PredEngineTrait::calc_direct_yaw_angle(origin, view_to);
|
||||
const auto delta = view_to - origin;
|
||||
EXPECT_NEAR(yaw_calc, angles::radians_to_degrees(std::atan2(delta.y, delta.x)), 1e-3f);
|
||||
|
||||
E::ViewAngles va;
|
||||
expect_matrix_near(E::MeshTrait::rotation_matrix(va), E::rotation_matrix(va));
|
||||
|
||||
const auto proj = E::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(45.f), {1920.f, 1080.f}, 0.1f, 1000.f);
|
||||
const auto expected = E::calc_perspective_projection_matrix(45.f, 1920.f / 1080.f, 0.1f, 1000.f);
|
||||
expect_matrix_near(proj, expected);
|
||||
|
||||
// non-airborne
|
||||
t.m_is_airborne = false;
|
||||
const auto pred_ground_iw = E::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
|
||||
EXPECT_NEAR(pred_ground_iw.z, 9.f, 1e-6f);
|
||||
}
|
||||
|
||||
TEST(TraitTests, OpenGL_Pred_And_Mesh_And_Camera)
|
||||
{
|
||||
namespace E = omath::opengl_engine;
|
||||
|
||||
projectile_prediction::Projectile p;
|
||||
p.m_origin = {0.f, 0.f, 0.f};
|
||||
p.m_launch_speed = 10.f;
|
||||
p.m_gravity_scale = 1.f;
|
||||
|
||||
const auto pos = E::PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 1.f, 9.81f);
|
||||
EXPECT_NEAR(pos.z, -10.f, 1e-4f);
|
||||
EXPECT_NEAR(pos.y, -9.81f * 0.5f, 1e-4f);
|
||||
|
||||
projectile_prediction::Target t;
|
||||
t.m_origin = {0.f, 5.f, 0.f};
|
||||
t.m_velocity = {2.f, 0.f, 0.f};
|
||||
t.m_is_airborne = true;
|
||||
const auto pred = E::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
|
||||
EXPECT_NEAR(pred.x, 4.f, 1e-6f);
|
||||
EXPECT_NEAR(pred.y, 5.f - 9.81f * (2.f * 2.f) * 0.5f, 1e-6f);
|
||||
|
||||
EXPECT_NEAR(E::PredEngineTrait::calc_vector_2d_distance({3.f, 0.f, 4.f}), 5.f, 1e-6f);
|
||||
EXPECT_NEAR(E::PredEngineTrait::get_vector_height_coordinate({1.f, 2.5f, 3.f}), 2.5f, 1e-6f);
|
||||
|
||||
std::optional<float> pitch = 45.f;
|
||||
auto vp = E::PredEngineTrait::calc_viewpoint_from_angles(p, {10.f, 0.f, 0.f}, pitch);
|
||||
EXPECT_NEAR(vp.y, 0.f + 10.f * std::tan(angles::degrees_to_radians(45.f)), 1e-6f);
|
||||
|
||||
Vector3<float> origin{0.f, 0.f, 0.f};
|
||||
Vector3<float> view_to{0.f, 1.f, 1.f};
|
||||
const auto pitch_calc = E::PredEngineTrait::calc_direct_pitch_angle(origin, view_to);
|
||||
const auto dir = (view_to - origin).normalized();
|
||||
EXPECT_NEAR(pitch_calc, angles::radians_to_degrees(std::asin(dir.y)), 1e-3f);
|
||||
|
||||
const auto yaw_calc = E::PredEngineTrait::calc_direct_yaw_angle(origin, view_to);
|
||||
EXPECT_NEAR(yaw_calc, angles::radians_to_degrees(-std::atan2(dir.x, -dir.z)), 1e-3f);
|
||||
|
||||
E::ViewAngles va;
|
||||
expect_matrix_near(E::MeshTrait::rotation_matrix(va), E::rotation_matrix(va));
|
||||
|
||||
const auto proj = E::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(60.f), {1280.f, 720.f}, 0.1f, 1000.f);
|
||||
const auto expected = E::calc_perspective_projection_matrix(60.f, 1280.f / 720.f, 0.1f, 1000.f);
|
||||
expect_matrix_near(proj, expected);
|
||||
|
||||
// non-airborne
|
||||
t.m_is_airborne = false;
|
||||
const auto pred_ground_gl = E::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
|
||||
EXPECT_NEAR(pred_ground_gl.x, 4.f, 1e-6f);
|
||||
}
|
||||
|
||||
TEST(TraitTests, Unity_Pred_And_Mesh_And_Camera)
|
||||
{
|
||||
namespace E = omath::unity_engine;
|
||||
|
||||
projectile_prediction::Projectile p;
|
||||
p.m_origin = {0.f, 0.f, 0.f};
|
||||
p.m_launch_speed = 10.f;
|
||||
p.m_gravity_scale = 1.f;
|
||||
|
||||
const auto pos = E::PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 1.f, 9.81f);
|
||||
EXPECT_NEAR(pos.z, 10.f, 1e-4f);
|
||||
EXPECT_NEAR(pos.y, -9.81f * 0.5f, 1e-4f);
|
||||
|
||||
projectile_prediction::Target t;
|
||||
t.m_origin = {0.f, 5.f, 0.f};
|
||||
t.m_velocity = {2.f, 0.f, 0.f};
|
||||
t.m_is_airborne = true;
|
||||
const auto pred = E::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
|
||||
EXPECT_NEAR(pred.x, 4.f, 1e-6f);
|
||||
EXPECT_NEAR(pred.y, 5.f - 9.81f * (2.f * 2.f) * 0.5f, 1e-6f);
|
||||
|
||||
EXPECT_NEAR(E::PredEngineTrait::calc_vector_2d_distance({3.f, 0.f, 4.f}), 5.f, 1e-6f);
|
||||
EXPECT_NEAR(E::PredEngineTrait::get_vector_height_coordinate({1.f, 2.5f, 3.f}), 2.5f, 1e-6f);
|
||||
|
||||
std::optional<float> pitch = 45.f;
|
||||
auto vp = E::PredEngineTrait::calc_viewpoint_from_angles(p, {10.f, 0.f, 0.f}, pitch);
|
||||
EXPECT_NEAR(vp.y, 0.f + 10.f * std::tan(angles::degrees_to_radians(45.f)), 1e-6f);
|
||||
|
||||
Vector3<float> origin{0.f, 0.f, 0.f};
|
||||
Vector3<float> view_to{0.f, 1.f, 1.f};
|
||||
const auto pitch_calc = E::PredEngineTrait::calc_direct_pitch_angle(origin, view_to);
|
||||
const auto dir = (view_to - origin).normalized();
|
||||
EXPECT_NEAR(pitch_calc, angles::radians_to_degrees(std::asin(dir.y)), 1e-3f);
|
||||
|
||||
const auto yaw_calc = E::PredEngineTrait::calc_direct_yaw_angle(origin, view_to);
|
||||
EXPECT_NEAR(yaw_calc, angles::radians_to_degrees(std::atan2(dir.x, dir.z)), 1e-3f);
|
||||
|
||||
E::ViewAngles va;
|
||||
expect_matrix_near(E::MeshTrait::rotation_matrix(va), E::rotation_matrix(va));
|
||||
|
||||
const auto proj = E::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(60.f), {1280.f, 720.f}, 0.1f, 1000.f);
|
||||
const auto expected = E::calc_perspective_projection_matrix(60.f, 1280.f / 720.f, 0.1f, 1000.f);
|
||||
expect_matrix_near(proj, expected);
|
||||
|
||||
// non-airborne
|
||||
t.m_is_airborne = false;
|
||||
const auto pred_ground_unity = E::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
|
||||
EXPECT_NEAR(pred_ground_unity.x, 4.f, 1e-6f);
|
||||
}
|
||||
|
||||
TEST(TraitTests, Unreal_Pred_And_Mesh_And_Camera)
|
||||
{
|
||||
namespace E = omath::unreal_engine;
|
||||
|
||||
projectile_prediction::Projectile p;
|
||||
p.m_origin = {0.f, 0.f, 0.f};
|
||||
p.m_launch_speed = 10.f;
|
||||
p.m_gravity_scale = 1.f;
|
||||
|
||||
const auto pos = E::PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 1.f, 9.81f);
|
||||
EXPECT_NEAR(pos.x, 10.f, 1e-4f);
|
||||
EXPECT_NEAR(pos.y, -9.81f * 0.5f, 1e-4f);
|
||||
|
||||
projectile_prediction::Target t;
|
||||
t.m_origin = {0.f, 5.f, 0.f};
|
||||
t.m_velocity = {2.f, 0.f, 0.f};
|
||||
t.m_is_airborne = true;
|
||||
const auto pred = E::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
|
||||
EXPECT_NEAR(pred.x, 4.f, 1e-6f);
|
||||
EXPECT_NEAR(pred.y, 5.f - 9.81f * (2.f * 2.f) * 0.5f, 1e-6f);
|
||||
|
||||
EXPECT_NEAR(E::PredEngineTrait::calc_vector_2d_distance({3.f, 0.f, 4.f}), 5.f, 1e-6f);
|
||||
EXPECT_NEAR(E::PredEngineTrait::get_vector_height_coordinate({1.f, 2.5f, 3.f}), 2.5f, 1e-6f);
|
||||
|
||||
std::optional<float> pitch = 45.f;
|
||||
auto vp = E::PredEngineTrait::calc_viewpoint_from_angles(p, {10.f, 0.f, 0.f}, pitch);
|
||||
EXPECT_NEAR(vp.z, 0.f + 10.f * std::tan(angles::degrees_to_radians(45.f)), 1e-6f);
|
||||
|
||||
Vector3<float> origin{0.f, 0.f, 0.f};
|
||||
Vector3<float> view_to{1.f, 1.f, 1.f};
|
||||
const auto pitch_calc = E::PredEngineTrait::calc_direct_pitch_angle(origin, view_to);
|
||||
const auto dir = (view_to - origin).normalized();
|
||||
EXPECT_NEAR(pitch_calc, angles::radians_to_degrees(std::asin(dir.z)), 1e-3f);
|
||||
|
||||
const auto yaw_calc = E::PredEngineTrait::calc_direct_yaw_angle(origin, view_to);
|
||||
EXPECT_NEAR(yaw_calc, angles::radians_to_degrees(std::atan2(dir.y, dir.x)), 1e-3f);
|
||||
|
||||
E::ViewAngles va;
|
||||
expect_matrix_near(E::MeshTrait::rotation_matrix(va), E::rotation_matrix(va));
|
||||
|
||||
const auto proj = E::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(60.f), {1280.f, 720.f}, 0.1f, 1000.f);
|
||||
const auto expected = E::calc_perspective_projection_matrix(60.f, 1280.f / 720.f, 0.1f, 1000.f);
|
||||
expect_matrix_near(proj, expected);
|
||||
|
||||
// non-airborne
|
||||
t.m_is_airborne = false;
|
||||
const auto pred_ground_unreal = E::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
|
||||
EXPECT_NEAR(pred_ground_unreal.x, 4.f, 1e-6f);
|
||||
}
|
||||
@@ -82,6 +82,18 @@ TEST(unit_test_unreal_engine, ProjectTargetMovedFromCamera)
|
||||
EXPECT_NEAR(projected->y, 360, 0.00001f);
|
||||
}
|
||||
}
|
||||
TEST(unit_test_unreal_engine, ProjectTargetMovedFromCameraBehind)
|
||||
{
|
||||
constexpr auto fov = omath::projection::FieldOfView::from_degrees(60.f);
|
||||
const auto cam = omath::unreal_engine::Camera({0, 0, 0}, {}, {1280.f, 720.f}, fov, 0.01f, 10000.f);
|
||||
|
||||
for (float distance = 0.02f; distance < 9000.f; distance += 100.f)
|
||||
{
|
||||
const auto projected = cam.world_to_screen({-distance, 0, 0});
|
||||
|
||||
EXPECT_FALSE(projected.has_value());
|
||||
}
|
||||
}
|
||||
|
||||
TEST(unit_test_unreal_engine, CameraSetAndGetFov)
|
||||
{
|
||||
|
||||
@@ -1,8 +1,128 @@
|
||||
//
|
||||
// Created by Vlad on 18.08.2024.
|
||||
//
|
||||
// Extra unit tests for the project's A* implementation
|
||||
#include <array>
|
||||
#include <gtest/gtest.h>
|
||||
#include <omath/pathfinding/a_star.hpp>
|
||||
#include <omath/pathfinding/navigation_mesh.hpp>
|
||||
#include <utility>
|
||||
|
||||
using namespace omath;
|
||||
using namespace omath::pathfinding;
|
||||
|
||||
TEST(AStarExtra, TrivialNeighbor)
|
||||
{
|
||||
NavigationMesh nav;
|
||||
Vector3<float> v1{0.f, 0.f, 0.f};
|
||||
Vector3<float> v2{1.f, 0.f, 0.f};
|
||||
nav.m_vertex_map[v1] = {v2};
|
||||
nav.m_vertex_map[v2] = {v1};
|
||||
|
||||
const auto path = Astar::find_path(v1, v2, nav);
|
||||
ASSERT_EQ(path.size(), 1u);
|
||||
EXPECT_EQ(path.front(), v2);
|
||||
}
|
||||
|
||||
TEST(AStarExtra, StartEqualsGoal)
|
||||
{
|
||||
NavigationMesh nav;
|
||||
constexpr Vector3<float> v{1.f, 1.f, 0.f};
|
||||
nav.m_vertex_map[v] = {};
|
||||
|
||||
const auto path = Astar::find_path(v, v, nav);
|
||||
ASSERT_EQ(path.size(), 1u);
|
||||
EXPECT_EQ(path.front(), v);
|
||||
}
|
||||
|
||||
TEST(AStarExtra, BlockedNoPathBetweenTwoVertices)
|
||||
{
|
||||
NavigationMesh nav;
|
||||
constexpr Vector3<float> left{0.f, 0.f, 0.f};
|
||||
constexpr Vector3<float> right{2.f, 0.f, 0.f};
|
||||
// both vertices present but no connections
|
||||
nav.m_vertex_map[left] = {};
|
||||
nav.m_vertex_map[right] = {};
|
||||
|
||||
const auto path = Astar::find_path(left, right, nav);
|
||||
// disconnected vertices -> empty result
|
||||
EXPECT_TRUE(path.empty());
|
||||
}
|
||||
|
||||
TEST(AStarExtra, LongerPathAvoidsBlock)
|
||||
{
|
||||
NavigationMesh nav;
|
||||
// build 3x3 grid of vertices, block center (1,1)
|
||||
auto idx = [&](const int x, const int y)
|
||||
{ return Vector3<float>{static_cast<float>(x), static_cast<float>(y), 0.f}; };
|
||||
for (int y = 0; y < 3; ++y)
|
||||
{
|
||||
for (int x = 0; x < 3; ++x)
|
||||
{
|
||||
Vector3<float> v = idx(x, y);
|
||||
if (x == 1 && y == 1)
|
||||
continue; // center is omitted (blocked)
|
||||
std::vector<Vector3<float>> neigh;
|
||||
constexpr std::array<std::pair<int, int>, 4> offs{{{1, 0}, {-1, 0}, {0, 1}, {0, -1}}};
|
||||
for (auto [dx, dy] : offs)
|
||||
{
|
||||
const int nx = x + dx, ny = y + dy;
|
||||
if (nx < 0 || nx >= 3 || ny < 0 || ny >= 3)
|
||||
continue;
|
||||
if (nx == 1 && ny == 1)
|
||||
continue; // neighbor is the blocked center
|
||||
neigh.push_back(idx(nx, ny));
|
||||
}
|
||||
nav.m_vertex_map[v] = neigh;
|
||||
}
|
||||
}
|
||||
|
||||
constexpr Vector3<float> start = idx(0, 1);
|
||||
constexpr Vector3<float> goal = idx(2, 1);
|
||||
const auto path = Astar::find_path(start, goal, nav);
|
||||
ASSERT_FALSE(path.empty());
|
||||
EXPECT_EQ(path.front(), goal); // Astar convention: single-element or endpoint present
|
||||
}
|
||||
|
||||
TEST(AstarTests, TrivialDirectNeighborPath)
|
||||
{
|
||||
NavigationMesh nav;
|
||||
// create two vertices directly connected
|
||||
Vector3<float> v1{0.f, 0.f, 0.f};
|
||||
Vector3<float> v2{1.f, 0.f, 0.f};
|
||||
nav.m_vertex_map.emplace(v1, std::vector<Vector3<float>>{v2});
|
||||
nav.m_vertex_map.emplace(v2, std::vector<Vector3<float>>{v1});
|
||||
|
||||
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);
|
||||
EXPECT_EQ(path.front(), v2);
|
||||
}
|
||||
|
||||
TEST(AstarTests, NoPathWhenDisconnected)
|
||||
{
|
||||
NavigationMesh nav;
|
||||
Vector3<float> v1{0.f, 0.f, 0.f};
|
||||
constexpr Vector3<float> v2{10.f, 0.f, 0.f};
|
||||
// nav has only v1
|
||||
nav.m_vertex_map.emplace(v1, std::vector<Vector3<float>>{});
|
||||
|
||||
const auto path = Astar::find_path(v1, v2, nav);
|
||||
// When the nav mesh contains only the start vertex, the closest
|
||||
// vertex for both start and end will be the same vertex. In that
|
||||
// case Astar returns a single-element path with the start vertex.
|
||||
ASSERT_EQ(path.size(), 1u);
|
||||
EXPECT_EQ(path.front(), v1);
|
||||
}
|
||||
|
||||
TEST(AstarTests, EmptyNavReturnsNoPath)
|
||||
{
|
||||
const NavigationMesh nav;
|
||||
constexpr Vector3<float> v1{0.f, 0.f, 0.f};
|
||||
constexpr Vector3<float> v2{1.f, 0.f, 0.f};
|
||||
|
||||
const auto path = Astar::find_path(v1, v2, nav);
|
||||
EXPECT_TRUE(path.empty());
|
||||
}
|
||||
|
||||
TEST(unit_test_a_star, finding_right_path)
|
||||
{
|
||||
|
||||
@@ -13,11 +13,11 @@ namespace
|
||||
{
|
||||
|
||||
// Handy aliases (defaults: Type=float, [0,360], Normalized)
|
||||
using Deg = Angle<float, float(0), float(360), AngleFlags::Normalized>;
|
||||
using Pitch = Angle<float, float(-90), float(90), AngleFlags::Clamped>;
|
||||
using Turn = Angle<float, float(-180), float(180), AngleFlags::Normalized>;
|
||||
using Deg = Angle<float, static_cast<float>(0), static_cast<float>(360), AngleFlags::Normalized>;
|
||||
using Pitch = Angle<float, static_cast<float>(-90), static_cast<float>(90), AngleFlags::Clamped>;
|
||||
using Turn = Angle<float, static_cast<float>(-180), static_cast<float>(180), AngleFlags::Normalized>;
|
||||
|
||||
constexpr float kEps = 1e-5f;
|
||||
constexpr float k_eps = 1e-5f;
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace
|
||||
|
||||
TEST(UnitTestAngle, DefaultConstructor_IsZeroDegrees)
|
||||
{
|
||||
Deg a; // default ctor
|
||||
constexpr Deg a; // default ctor
|
||||
EXPECT_FLOAT_EQ(*a, 0.0f);
|
||||
EXPECT_FLOAT_EQ(a.as_degrees(), 0.0f);
|
||||
}
|
||||
@@ -44,8 +44,8 @@ TEST(UnitTestAngle, FromDegrees_Normalized_WrapsBelowMin)
|
||||
|
||||
TEST(UnitTestAngle, FromDegrees_Clamped_ClampsToRange)
|
||||
{
|
||||
const Pitch hi = Pitch::from_degrees(100.0f);
|
||||
const Pitch lo = Pitch::from_degrees(-120.0f);
|
||||
constexpr Pitch hi = Pitch::from_degrees(100.0f);
|
||||
constexpr Pitch lo = Pitch::from_degrees(-120.0f);
|
||||
|
||||
EXPECT_FLOAT_EQ(hi.as_degrees(), 90.0f);
|
||||
EXPECT_FLOAT_EQ(lo.as_degrees(), -90.0f);
|
||||
@@ -80,8 +80,8 @@ TEST(UnitTestAngle, DereferenceReturnsDegrees)
|
||||
TEST(UnitTestAngle, SinCosTanCot_BasicCases)
|
||||
{
|
||||
const Deg a0 = Deg::from_degrees(0.0f);
|
||||
EXPECT_NEAR(a0.sin(), 0.0f, kEps);
|
||||
EXPECT_NEAR(a0.cos(), 1.0f, kEps);
|
||||
EXPECT_NEAR(a0.sin(), 0.0f, k_eps);
|
||||
EXPECT_NEAR(a0.cos(), 1.0f, k_eps);
|
||||
// cot(0) -> cos/sin -> div by 0: allow inf or nan
|
||||
const float cot0 = a0.cot();
|
||||
EXPECT_TRUE(std::isinf(cot0) || std::isnan(cot0));
|
||||
@@ -99,7 +99,7 @@ TEST(UnitTestAngle, Atan_IsAtanOfRadians)
|
||||
{
|
||||
// atan(as_radians). For 0° -> atan(0)=0.
|
||||
const Deg a0 = Deg::from_degrees(0.0f);
|
||||
EXPECT_NEAR(a0.atan(), 0.0f, kEps);
|
||||
EXPECT_NEAR(a0.atan(), 0.0f, k_eps);
|
||||
|
||||
const Deg a45 = Deg::from_degrees(45.0f);
|
||||
// atan(pi/4) ≈ 0.665773...
|
||||
|
||||
@@ -7,20 +7,32 @@
|
||||
|
||||
TEST(UnitTestColider, CheckToWorld)
|
||||
{
|
||||
omath::source_engine::Mesh mesh = {std::vector<omath::Vector3<float>>{{1.f, 1.f, 1.f}, {-1.f, -1.f, -1.f}}, {}};
|
||||
omath::source_engine::Mesh mesh = {
|
||||
std::vector<omath::primitives::Vertex<>>{
|
||||
{ { 1.f, 1.f, 1.f }, {}, {} },
|
||||
{ {-1.f, -1.f, -1.f }, {}, {} }
|
||||
},
|
||||
{}
|
||||
};
|
||||
mesh.set_origin({0, 2, 0});
|
||||
const omath::source_engine::MeshCollider collider(mesh);
|
||||
|
||||
const auto vertex = collider.find_abs_furthest_vertex({1.f, 0.f, 0.f});
|
||||
const auto vertex = collider.find_abs_furthest_vertex_position({1.f, 0.f, 0.f});
|
||||
|
||||
EXPECT_EQ(vertex, omath::Vector3<float>(1.f, 3.f, 1.f));
|
||||
}
|
||||
|
||||
TEST(UnitTestColider, FindFurthestVertex)
|
||||
{
|
||||
const omath::source_engine::Mesh mesh = {{{1.f, 1.f, 1.f}, {-1.f, -1.f, -1.f}}, {}};
|
||||
const omath::source_engine::Mesh mesh = {
|
||||
{
|
||||
{ { 1.f, 1.f, 1.f }, {}, {} }, // position, normal, uv
|
||||
{ {-1.f, -1.f, -1.f }, {}, {} }
|
||||
},
|
||||
{}
|
||||
};
|
||||
const omath::source_engine::MeshCollider collider(mesh);
|
||||
const auto vertex = collider.find_furthest_vertex({1.f, 0.f, 0.f});
|
||||
const auto vertex = collider.find_furthest_vertex({1.f, 0.f, 0.f}).position;
|
||||
EXPECT_EQ(vertex, omath::Vector3<float>(1.f, 1.f, 1.f));
|
||||
}
|
||||
|
||||
|
||||
112
tests/general/unit_test_collision_extra.cpp
Normal file
112
tests/general/unit_test_collision_extra.cpp
Normal file
@@ -0,0 +1,112 @@
|
||||
// Extra collision tests: Simplex, MeshCollider, EPA
|
||||
#include <gtest/gtest.h>
|
||||
#include <omath/collision/simplex.hpp>
|
||||
#include <omath/collision/mesh_collider.hpp>
|
||||
#include <omath/collision/epa_algorithm.hpp>
|
||||
#include <omath/engines/source_engine/collider.hpp>
|
||||
|
||||
using namespace omath;
|
||||
using namespace omath::collision;
|
||||
|
||||
TEST(SimplexTest, HandleEmptySimplex)
|
||||
{
|
||||
Simplex<Vector3<float>> simplex;
|
||||
Vector3<float> direction{1, 0, 0};
|
||||
|
||||
EXPECT_EQ(simplex.size(), 0);
|
||||
EXPECT_FALSE(simplex.handle(direction));
|
||||
}
|
||||
|
||||
TEST(SimplexTest, HandleLineCollinearWithXAxis)
|
||||
{
|
||||
using Vec3 = Vector3<float>;
|
||||
Simplex<Vec3> simplex;
|
||||
|
||||
simplex.push_front(Vec3{1, 0, 0});
|
||||
simplex.push_front(Vec3{-1, 0, 0});
|
||||
|
||||
Vec3 direction{};
|
||||
std::ignore = simplex.handle(direction);
|
||||
|
||||
EXPECT_NEAR(direction.x, 0.f, 1e-6f);
|
||||
}
|
||||
|
||||
TEST(CollisionExtra, SimplexLineHandle)
|
||||
{
|
||||
Simplex<Vector3<float>> s;
|
||||
s = { Vector3<float>{1.f,0.f,0.f}, Vector3<float>{2.f,0.f,0.f} };
|
||||
Vector3<float> dir{0,0,0};
|
||||
EXPECT_FALSE(s.handle(dir));
|
||||
// direction should not be zero
|
||||
EXPECT_GT(dir.length_sqr(), 0.0f);
|
||||
}
|
||||
|
||||
TEST(CollisionExtra, SimplexTriangleHandle)
|
||||
{
|
||||
Simplex<Vector3<float>> s;
|
||||
s = { Vector3<float>{1.f,0.f,0.f}, Vector3<float>{0.f,1.f,0.f}, Vector3<float>{0.f,0.f,1.f} };
|
||||
Vector3<float> dir{0,0,0};
|
||||
EXPECT_FALSE(s.handle(dir));
|
||||
EXPECT_GT(dir.length_sqr(), 0.0f);
|
||||
}
|
||||
|
||||
TEST(CollisionExtra, SimplexTetrahedronInside)
|
||||
{
|
||||
Simplex<Vector3<float>> s;
|
||||
// tetra that surrounds origin roughly
|
||||
s = { Vector3<float>{1.f,0.f,0.f}, Vector3<float>{0.f,1.f,0.f}, Vector3<float>{0.f,0.f,1.f}, Vector3<float>{-1.f,-1.f,-1.f} };
|
||||
Vector3<float> dir{0,0,0};
|
||||
// if origin inside, handle returns true
|
||||
const bool inside = s.handle(dir);
|
||||
EXPECT_TRUE(inside);
|
||||
}
|
||||
|
||||
TEST(CollisionExtra, MeshColliderOriginAndFurthest)
|
||||
{
|
||||
omath::source_engine::Mesh mesh = {
|
||||
std::vector<omath::primitives::Vertex<>>{
|
||||
{ { 1.f, 1.f, 1.f }, {}, {} },
|
||||
{ {-1.f, -1.f, -1.f }, {}, {} }
|
||||
},
|
||||
{}
|
||||
};
|
||||
mesh.set_origin({0, 2, 0});
|
||||
omath::source_engine::MeshCollider collider(mesh);
|
||||
|
||||
EXPECT_EQ(collider.get_origin(), omath::Vector3<float>(0,2,0));
|
||||
collider.set_origin({1,2,3});
|
||||
EXPECT_EQ(collider.get_origin(), omath::Vector3<float>(1,2,3));
|
||||
|
||||
const auto v = collider.find_abs_furthest_vertex_position({1.f,0.f,0.f});
|
||||
// the original vertex at (1,1,1) translated by origin (1,2,3) becomes (2,3,4)
|
||||
EXPECT_EQ(v, omath::Vector3<float>(2.f,3.f,4.f));
|
||||
}
|
||||
|
||||
TEST(CollisionExtra, EPAConvergesOnSimpleCase)
|
||||
{
|
||||
// Build two simple colliders using simple meshes that overlap
|
||||
omath::source_engine::Mesh meshA = {
|
||||
std::vector<omath::primitives::Vertex<>>{{ {0.f,0.f,0.f}, {}, {} }, { {1.f,0.f,0.f}, {}, {} } },
|
||||
{}
|
||||
};
|
||||
omath::source_engine::Mesh mesh_b = meshA;
|
||||
mesh_b.set_origin({0.5f, 0.f, 0.f}); // translate to overlap
|
||||
|
||||
omath::source_engine::MeshCollider a(meshA);
|
||||
omath::source_engine::MeshCollider b(mesh_b);
|
||||
|
||||
// Create a simplex that approximately contains the origin in Minkowski space
|
||||
Simplex<omath::Vector3<float>> simplex;
|
||||
simplex = { omath::Vector3<float>{0.5f,0.f,0.f}, omath::Vector3<float>{-0.5f,0.f,0.f}, omath::Vector3<float>{0.f,0.5f,0.f}, omath::Vector3<float>{0.f,-0.5f,0.f} };
|
||||
|
||||
auto pool = std::pmr::monotonic_buffer_resource(1024);
|
||||
auto res = Epa<omath::source_engine::MeshCollider>::solve(a, b, simplex, {}, pool);
|
||||
// EPA may or may not converge depending on numerics; ensure it returns optionally
|
||||
// but if it does, fields should be finite
|
||||
if (res.has_value())
|
||||
{
|
||||
auto r = *res;
|
||||
EXPECT_TRUE(std::isfinite(r.depth));
|
||||
EXPECT_GT(r.normal.length_sqr(), 0.0f);
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
//
|
||||
// Created by Vlad on 01.09.2024.
|
||||
//
|
||||
#include <omath/utility/color.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using namespace omath;
|
||||
|
||||
class unit_test_color : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
Color color1;
|
||||
Color color2;
|
||||
|
||||
void SetUp() override
|
||||
{
|
||||
color1 = Color::red();
|
||||
color2 = Color::green();
|
||||
}
|
||||
};
|
||||
|
||||
// Test constructors
|
||||
TEST_F(unit_test_color, Constructor_Float)
|
||||
{
|
||||
constexpr Color color(0.5f, 0.5f, 0.5f, 1.0f);
|
||||
EXPECT_FLOAT_EQ(color.x, 0.5f);
|
||||
EXPECT_FLOAT_EQ(color.y, 0.5f);
|
||||
EXPECT_FLOAT_EQ(color.z, 0.5f);
|
||||
EXPECT_FLOAT_EQ(color.w, 1.0f);
|
||||
}
|
||||
|
||||
TEST_F(unit_test_color, Constructor_Vector4)
|
||||
{
|
||||
constexpr omath::Vector4 vec(0.2f, 0.4f, 0.6f, 0.8f);
|
||||
constexpr Color color(vec);
|
||||
EXPECT_FLOAT_EQ(color.x, 0.2f);
|
||||
EXPECT_FLOAT_EQ(color.y, 0.4f);
|
||||
EXPECT_FLOAT_EQ(color.z, 0.6f);
|
||||
EXPECT_FLOAT_EQ(color.w, 0.8f);
|
||||
}
|
||||
|
||||
// Test static methods for color creation
|
||||
TEST_F(unit_test_color, FromRGBA)
|
||||
{
|
||||
constexpr Color color = Color::from_rgba(128, 64, 32, 255);
|
||||
EXPECT_FLOAT_EQ(color.x, 128.0f / 255.0f);
|
||||
EXPECT_FLOAT_EQ(color.y, 64.0f / 255.0f);
|
||||
EXPECT_FLOAT_EQ(color.z, 32.0f / 255.0f);
|
||||
EXPECT_FLOAT_EQ(color.w, 1.0f);
|
||||
}
|
||||
|
||||
TEST_F(unit_test_color, FromHSV)
|
||||
{
|
||||
constexpr Color color = Color::from_hsv(0.0f, 1.0f, 1.0f); // Red in HSV
|
||||
EXPECT_FLOAT_EQ(color.x, 1.0f);
|
||||
EXPECT_FLOAT_EQ(color.y, 0.0f);
|
||||
EXPECT_FLOAT_EQ(color.z, 0.0f);
|
||||
EXPECT_FLOAT_EQ(color.w, 1.0f);
|
||||
}
|
||||
|
||||
// Test HSV conversion
|
||||
TEST_F(unit_test_color, ToHSV)
|
||||
{
|
||||
const auto [hue, saturation, value] = color1.to_hsv(); // Red color
|
||||
EXPECT_FLOAT_EQ(hue, 0.0f);
|
||||
EXPECT_FLOAT_EQ(saturation, 1.0f);
|
||||
EXPECT_FLOAT_EQ(value, 1.0f);
|
||||
}
|
||||
|
||||
// Test color blending
|
||||
TEST_F(unit_test_color, Blend)
|
||||
{
|
||||
const Color blended = color1.blend(color2, 0.5f);
|
||||
EXPECT_FLOAT_EQ(blended.x, 0.5f);
|
||||
EXPECT_FLOAT_EQ(blended.y, 0.5f);
|
||||
EXPECT_FLOAT_EQ(blended.z, 0.0f);
|
||||
EXPECT_FLOAT_EQ(blended.w, 1.0f);
|
||||
}
|
||||
|
||||
// Test predefined colors
|
||||
TEST_F(unit_test_color, PredefinedColors)
|
||||
{
|
||||
constexpr Color red = Color::red();
|
||||
constexpr Color green = Color::green();
|
||||
constexpr Color blue = Color::blue();
|
||||
|
||||
EXPECT_FLOAT_EQ(red.x, 1.0f);
|
||||
EXPECT_FLOAT_EQ(red.y, 0.0f);
|
||||
EXPECT_FLOAT_EQ(red.z, 0.0f);
|
||||
EXPECT_FLOAT_EQ(red.w, 1.0f);
|
||||
|
||||
EXPECT_FLOAT_EQ(green.x, 0.0f);
|
||||
EXPECT_FLOAT_EQ(green.y, 1.0f);
|
||||
EXPECT_FLOAT_EQ(green.z, 0.0f);
|
||||
EXPECT_FLOAT_EQ(green.w, 1.0f);
|
||||
|
||||
EXPECT_FLOAT_EQ(blue.x, 0.0f);
|
||||
EXPECT_FLOAT_EQ(blue.y, 0.0f);
|
||||
EXPECT_FLOAT_EQ(blue.z, 1.0f);
|
||||
EXPECT_FLOAT_EQ(blue.w, 1.0f);
|
||||
}
|
||||
|
||||
// Test non-member function: Blend for Vector3
|
||||
TEST_F(unit_test_color, BlendVector3)
|
||||
{
|
||||
constexpr Color v1(1.0f, 0.0f, 0.0f, 1.f); // Red
|
||||
constexpr Color v2(0.0f, 1.0f, 0.0f, 1.f); // Green
|
||||
constexpr Color blended = v1.blend(v2, 0.5f);
|
||||
EXPECT_FLOAT_EQ(blended.x, 0.5f);
|
||||
EXPECT_FLOAT_EQ(blended.y, 0.5f);
|
||||
EXPECT_FLOAT_EQ(blended.z, 0.0f);
|
||||
}
|
||||
293
tests/general/unit_test_color_grouped.cpp
Normal file
293
tests/general/unit_test_color_grouped.cpp
Normal file
@@ -0,0 +1,293 @@
|
||||
// Combined color tests
|
||||
// This file merges multiple color-related unit test files into one grouped TU
|
||||
// to make the tests look more organized.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <omath/utility/color.hpp>
|
||||
#include <format>
|
||||
#include <algorithm>
|
||||
|
||||
using namespace omath;
|
||||
|
||||
class UnitTestColorGrouped : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
Color color1;
|
||||
Color color2;
|
||||
|
||||
void SetUp() override
|
||||
{
|
||||
color1 = Color::red();
|
||||
color2 = Color::green();
|
||||
}
|
||||
};
|
||||
|
||||
// From original unit_test_color.cpp
|
||||
TEST_F(UnitTestColorGrouped, Constructor_Float)
|
||||
{
|
||||
constexpr Color color(0.5f, 0.5f, 0.5f, 1.0f);
|
||||
EXPECT_FLOAT_EQ(color.x, 0.5f);
|
||||
EXPECT_FLOAT_EQ(color.y, 0.5f);
|
||||
EXPECT_FLOAT_EQ(color.z, 0.5f);
|
||||
EXPECT_FLOAT_EQ(color.w, 1.0f);
|
||||
}
|
||||
|
||||
TEST_F(UnitTestColorGrouped, Constructor_Vector4)
|
||||
{
|
||||
constexpr omath::Vector4 vec(0.2f, 0.4f, 0.6f, 0.8f);
|
||||
constexpr Color color(vec);
|
||||
EXPECT_FLOAT_EQ(color.x, 0.2f);
|
||||
EXPECT_FLOAT_EQ(color.y, 0.4f);
|
||||
EXPECT_FLOAT_EQ(color.z, 0.6f);
|
||||
EXPECT_FLOAT_EQ(color.w, 0.8f);
|
||||
}
|
||||
|
||||
TEST_F(UnitTestColorGrouped, FromRGBA)
|
||||
{
|
||||
constexpr Color color = Color::from_rgba(128, 64, 32, 255);
|
||||
EXPECT_FLOAT_EQ(color.x, 128.0f / 255.0f);
|
||||
EXPECT_FLOAT_EQ(color.y, 64.0f / 255.0f);
|
||||
EXPECT_FLOAT_EQ(color.z, 32.0f / 255.0f);
|
||||
EXPECT_FLOAT_EQ(color.w, 1.0f);
|
||||
}
|
||||
|
||||
TEST_F(UnitTestColorGrouped, FromHSV)
|
||||
{
|
||||
constexpr Color color = Color::from_hsv(0.0f, 1.0f, 1.0f); // Red in HSV
|
||||
EXPECT_FLOAT_EQ(color.x, 1.0f);
|
||||
EXPECT_FLOAT_EQ(color.y, 0.0f);
|
||||
EXPECT_FLOAT_EQ(color.z, 0.0f);
|
||||
EXPECT_FLOAT_EQ(color.w, 1.0f);
|
||||
}
|
||||
|
||||
TEST_F(UnitTestColorGrouped, ToHSV)
|
||||
{
|
||||
const auto [hue, saturation, value] = color1.to_hsv(); // Red color
|
||||
EXPECT_FLOAT_EQ(hue, 0.0f);
|
||||
EXPECT_FLOAT_EQ(saturation, 1.0f);
|
||||
EXPECT_FLOAT_EQ(value, 1.0f);
|
||||
}
|
||||
|
||||
TEST_F(UnitTestColorGrouped, Blend)
|
||||
{
|
||||
const Color blended = color1.blend(color2, 0.5f);
|
||||
EXPECT_FLOAT_EQ(blended.x, 0.5f);
|
||||
EXPECT_FLOAT_EQ(blended.y, 0.5f);
|
||||
EXPECT_FLOAT_EQ(blended.z, 0.0f);
|
||||
EXPECT_FLOAT_EQ(blended.w, 1.0f);
|
||||
}
|
||||
|
||||
TEST_F(UnitTestColorGrouped, PredefinedColors)
|
||||
{
|
||||
constexpr Color red = Color::red();
|
||||
constexpr Color green = Color::green();
|
||||
constexpr Color blue = Color::blue();
|
||||
|
||||
EXPECT_FLOAT_EQ(red.x, 1.0f);
|
||||
EXPECT_FLOAT_EQ(red.y, 0.0f);
|
||||
EXPECT_FLOAT_EQ(red.z, 0.0f);
|
||||
EXPECT_FLOAT_EQ(red.w, 1.0f);
|
||||
|
||||
EXPECT_FLOAT_EQ(green.x, 0.0f);
|
||||
EXPECT_FLOAT_EQ(green.y, 1.0f);
|
||||
EXPECT_FLOAT_EQ(green.z, 0.0f);
|
||||
EXPECT_FLOAT_EQ(green.w, 1.0f);
|
||||
|
||||
EXPECT_FLOAT_EQ(blue.x, 0.0f);
|
||||
EXPECT_FLOAT_EQ(blue.y, 0.0f);
|
||||
EXPECT_FLOAT_EQ(blue.z, 1.0f);
|
||||
EXPECT_FLOAT_EQ(blue.w, 1.0f);
|
||||
}
|
||||
|
||||
TEST_F(UnitTestColorGrouped, BlendVector3)
|
||||
{
|
||||
constexpr Color v1(1.0f, 0.0f, 0.0f, 1.f); // Red
|
||||
constexpr Color v2(0.0f, 1.0f, 0.0f, 1.f); // Green
|
||||
constexpr Color blended = v1.blend(v2, 0.5f);
|
||||
EXPECT_FLOAT_EQ(blended.x, 0.5f);
|
||||
EXPECT_FLOAT_EQ(blended.y, 0.5f);
|
||||
EXPECT_FLOAT_EQ(blended.z, 0.0f);
|
||||
}
|
||||
|
||||
// From unit_test_color_extra.cpp
|
||||
TEST(UnitTestColorGrouped_Extra, SetHueSaturationValue)
|
||||
{
|
||||
Color c = Color::red();
|
||||
const auto h1 = c.to_hsv();
|
||||
EXPECT_FLOAT_EQ(h1.hue, 0.f);
|
||||
|
||||
c.set_hue(0.5f);
|
||||
const auto h2 = c.to_hsv();
|
||||
EXPECT_NEAR(h2.hue, 0.5f, 1e-3f);
|
||||
|
||||
c = Color::from_hsv(0.25f, 0.8f, 0.6f);
|
||||
c.set_saturation(0.3f);
|
||||
const auto h3 = c.to_hsv();
|
||||
EXPECT_NEAR(h3.saturation, 0.3f, 1e-3f);
|
||||
|
||||
c.set_value(1.0f);
|
||||
const auto h4 = c.to_hsv();
|
||||
EXPECT_NEAR(h4.value, 1.0f, 1e-3f);
|
||||
}
|
||||
|
||||
TEST(UnitTestColorGrouped_Extra, ToStringVariants)
|
||||
{
|
||||
constexpr Color c = Color::from_rgba(10, 20, 30, 255);
|
||||
auto s = c.to_string();
|
||||
EXPECT_NE(s.find("r:"), std::string::npos);
|
||||
|
||||
const auto ws = c.to_wstring();
|
||||
EXPECT_FALSE(ws.empty());
|
||||
|
||||
const auto u8 = c.to_u8string();
|
||||
EXPECT_FALSE(u8.empty());
|
||||
}
|
||||
|
||||
TEST(UnitTestColorGrouped_Extra, BlendEdgeCases)
|
||||
{
|
||||
constexpr Color a = Color::red();
|
||||
constexpr Color b = Color::blue();
|
||||
constexpr auto r0 = a.blend(b, 0.f);
|
||||
EXPECT_FLOAT_EQ(r0.x, a.x);
|
||||
constexpr auto r1 = a.blend(b, 1.f);
|
||||
EXPECT_FLOAT_EQ(r1.x, b.x);
|
||||
}
|
||||
|
||||
// From unit_test_color_more.cpp
|
||||
TEST(UnitTestColorGrouped_More, DefaultCtorIsZero)
|
||||
{
|
||||
constexpr Color c;
|
||||
EXPECT_FLOAT_EQ(c.x, 0.0f);
|
||||
EXPECT_FLOAT_EQ(c.y, 0.0f);
|
||||
EXPECT_FLOAT_EQ(c.z, 0.0f);
|
||||
EXPECT_FLOAT_EQ(c.w, 0.0f);
|
||||
}
|
||||
|
||||
TEST(UnitTestColorGrouped_More, FloatCtorAndClampForRGB)
|
||||
{
|
||||
constexpr Color c(1.2f, -0.5f, 0.5f, 2.0f);
|
||||
EXPECT_FLOAT_EQ(c.x, 1.0f);
|
||||
EXPECT_FLOAT_EQ(c.y, 0.0f);
|
||||
EXPECT_FLOAT_EQ(c.z, 0.5f);
|
||||
EXPECT_FLOAT_EQ(c.w, 2.0f);
|
||||
}
|
||||
|
||||
TEST(UnitTestColorGrouped_More, FromRgbaProducesScaledComponents)
|
||||
{
|
||||
constexpr Color c = Color::from_rgba(25u, 128u, 230u, 64u);
|
||||
EXPECT_NEAR(c.x, 25.0f/255.0f, 1e-6f);
|
||||
EXPECT_NEAR(c.y, 128.0f/255.0f, 1e-6f);
|
||||
EXPECT_NEAR(c.z, 230.0f/255.0f, 1e-6f);
|
||||
EXPECT_NEAR(c.w, 64.0f/255.0f, 1e-6f);
|
||||
}
|
||||
|
||||
TEST(UnitTestColorGrouped_More, BlendProducesIntermediate)
|
||||
{
|
||||
constexpr Color c0(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
constexpr Color c1(1.0f, 1.0f, 1.0f, 0.0f);
|
||||
constexpr Color mid = c0.blend(c1, 0.5f);
|
||||
EXPECT_FLOAT_EQ(mid.x, 0.5f);
|
||||
EXPECT_FLOAT_EQ(mid.y, 0.5f);
|
||||
EXPECT_FLOAT_EQ(mid.z, 0.5f);
|
||||
EXPECT_FLOAT_EQ(mid.w, 0.5f);
|
||||
}
|
||||
|
||||
TEST(UnitTestColorGrouped_More, HsvRoundTrip)
|
||||
{
|
||||
constexpr Color red = Color::red();
|
||||
const auto hsv = red.to_hsv();
|
||||
const Color back = Color::from_hsv(hsv);
|
||||
EXPECT_NEAR(back.x, 1.0f, 1e-6f);
|
||||
EXPECT_NEAR(back.y, 0.0f, 1e-6f);
|
||||
EXPECT_NEAR(back.z, 0.0f, 1e-6f);
|
||||
}
|
||||
|
||||
TEST(UnitTestColorGrouped_More, ToStringContainsComponents)
|
||||
{
|
||||
constexpr Color c = Color::from_rgba(10, 20, 30, 40);
|
||||
std::string s = c.to_string();
|
||||
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);
|
||||
}
|
||||
|
||||
// From unit_test_color_more2.cpp
|
||||
TEST(UnitTestColorGrouped_More2, FromRgbaAndToString)
|
||||
{
|
||||
constexpr auto c = Color::from_rgba(255, 128, 0, 64);
|
||||
const auto s = c.to_string();
|
||||
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, FromHsvCases)
|
||||
{
|
||||
constexpr float eps = 1e-5f;
|
||||
|
||||
auto check_hue = [&](float h) {
|
||||
SCOPED_TRACE(::testing::Message() << "h=" << h);
|
||||
Color c = Color::from_hsv(h, 1.f, 1.f);
|
||||
EXPECT_TRUE(std::isfinite(c.x));
|
||||
EXPECT_TRUE(std::isfinite(c.y));
|
||||
EXPECT_TRUE(std::isfinite(c.z));
|
||||
EXPECT_GE(c.x, -eps);
|
||||
EXPECT_LE(c.x, 1.f + eps);
|
||||
EXPECT_GE(c.y, -eps);
|
||||
EXPECT_LE(c.y, 1.f + eps);
|
||||
EXPECT_GE(c.z, -eps);
|
||||
EXPECT_LE(c.z, 1.f + eps);
|
||||
|
||||
float mx = std::max({c.x, c.y, c.z});
|
||||
float mn = std::min({c.x, c.y, c.z});
|
||||
EXPECT_GE(mx, 0.999f);
|
||||
EXPECT_LE(mn, 1e-3f + 1e-4f);
|
||||
};
|
||||
|
||||
check_hue(0.f / 6.f);
|
||||
check_hue(1.f / 6.f);
|
||||
check_hue(2.f / 6.f);
|
||||
check_hue(3.f / 6.f);
|
||||
check_hue(4.f / 6.f);
|
||||
check_hue(5.f / 6.f);
|
||||
}
|
||||
|
||||
TEST(UnitTestColorGrouped_More2, ToHsvAndSetters)
|
||||
{
|
||||
Color c{0.2f, 0.4f, 0.6f, 1.f};
|
||||
const auto hsv = c.to_hsv();
|
||||
EXPECT_NEAR(hsv.value, 0.6f, 1e-6f);
|
||||
|
||||
c.set_hue(0.0f);
|
||||
EXPECT_TRUE(std::isfinite(c.x));
|
||||
|
||||
c.set_saturation(0.0f);
|
||||
EXPECT_TRUE(std::isfinite(c.y));
|
||||
|
||||
c.set_value(0.5f);
|
||||
EXPECT_TRUE(std::isfinite(c.z));
|
||||
}
|
||||
|
||||
TEST(UnitTestColorGrouped_More2, BlendAndStaticColors)
|
||||
{
|
||||
constexpr Color a = Color::red();
|
||||
constexpr Color b = Color::blue();
|
||||
constexpr auto mid = a.blend(b, 0.5f);
|
||||
EXPECT_GT(mid.x, 0.f);
|
||||
EXPECT_GT(mid.z, 0.f);
|
||||
|
||||
constexpr auto all_a = a.blend(b, -1.f);
|
||||
EXPECT_NEAR(all_a.x, a.x, 1e-6f);
|
||||
|
||||
constexpr auto all_b = a.blend(b, 2.f);
|
||||
EXPECT_NEAR(all_b.z, b.z, 1e-6f);
|
||||
}
|
||||
|
||||
TEST(UnitTestColorGrouped_More2, FormatterUsesToString)
|
||||
{
|
||||
Color c = Color::from_rgba(10, 20, 30, 40);
|
||||
const auto formatted = std::format("{}", c);
|
||||
EXPECT_NE(formatted.find("r:10"), std::string::npos);
|
||||
}
|
||||
153
tests/general/unit_test_epa.cpp
Normal file
153
tests/general/unit_test_epa.cpp
Normal file
@@ -0,0 +1,153 @@
|
||||
#include "omath/collision/epa_algorithm.hpp" // Epa<Collider> + GjkAlgorithmWithSimplex<Collider>
|
||||
#include "omath/collision/gjk_algorithm.hpp"
|
||||
#include "omath/collision/simplex.hpp"
|
||||
#include "omath/engines/source_engine/collider.hpp"
|
||||
#include "omath/engines/source_engine/mesh.hpp"
|
||||
#include "omath/linear_algebra/vector3.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory_resource>
|
||||
|
||||
using Mesh = omath::source_engine::Mesh;
|
||||
using Collider = omath::source_engine::MeshCollider;
|
||||
using GJK = omath::collision::GjkAlgorithm<Collider>;
|
||||
using EPA = omath::collision::Epa<Collider>;
|
||||
|
||||
TEST(UnitTestEpa, TestCollisionTrue)
|
||||
{
|
||||
// Unit cube [-1,1]^3
|
||||
std::vector<omath::primitives::Vertex<>> 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}, {}, {} }
|
||||
};
|
||||
std::vector<omath::Vector3<std::uint32_t>> vao; // not needed
|
||||
|
||||
Mesh a(vbo, vao, {1, 1, 1});
|
||||
Mesh b(vbo, vao, {1, 1, 1});
|
||||
|
||||
// Overlap along +X by 0.5
|
||||
a.set_origin({0, 0, 0});
|
||||
b.set_origin({0.5f, 0, 0});
|
||||
|
||||
Collider A(a), B(b);
|
||||
|
||||
// GJK
|
||||
auto gjk = GJK::is_collide_with_simplex_info(A, B);
|
||||
ASSERT_TRUE(gjk.hit) << "GJK should report collision";
|
||||
|
||||
// EPA
|
||||
EPA::Params params;
|
||||
auto pool = std::make_shared<std::pmr::monotonic_buffer_resource>(1024);
|
||||
params.max_iterations = 64;
|
||||
params.tolerance = 1e-4f;
|
||||
auto epa = EPA::solve(A, B, gjk.simplex, params, *pool);
|
||||
ASSERT_TRUE(epa.has_value()) << "EPA should converge";
|
||||
|
||||
// Normal is unit
|
||||
EXPECT_NEAR(epa->normal.dot(epa->normal), 1.0f, 1e-5f);
|
||||
|
||||
// For this setup, depth ≈ 1.5 (2 - 0.5)
|
||||
EXPECT_NEAR(epa->depth, 1.5f, 1e-3f);
|
||||
|
||||
// Normal axis sanity: near X axis
|
||||
EXPECT_NEAR(std::abs(epa->normal.x), 1.0f, 1e-3f);
|
||||
EXPECT_NEAR(epa->normal.y, 0.0f, 1e-3f);
|
||||
EXPECT_NEAR(epa->normal.z, 0.0f, 1e-3f);
|
||||
|
||||
// Try both signs with a tiny margin (avoid grazing contacts)
|
||||
constexpr float margin = 1.0f + 1e-3f;
|
||||
const auto pen = epa->penetration_vector;
|
||||
|
||||
Mesh b_plus = b;
|
||||
b_plus.set_origin(b_plus.get_origin() + pen * margin);
|
||||
Mesh b_minus = b;
|
||||
b_minus.set_origin(b_minus.get_origin() - pen * margin);
|
||||
|
||||
Collider B_plus(b_plus), B_minus(b_minus);
|
||||
|
||||
const bool sep_plus = !GJK::is_collide_with_simplex_info(A, B_plus).hit;
|
||||
const bool sep_minus = !GJK::is_collide_with_simplex_info(A, B_minus).hit;
|
||||
|
||||
// Exactly one direction should separate
|
||||
EXPECT_NE(sep_plus, sep_minus) << "Exactly one of ±penetration must separate";
|
||||
|
||||
// Optional: pick the resolving direction and assert round-trip
|
||||
const auto resolve = sep_plus ? (pen * margin) : (-pen * margin);
|
||||
|
||||
Mesh b_resolved = b;
|
||||
b_resolved.set_origin(b_resolved.get_origin() + resolve);
|
||||
EXPECT_FALSE(GJK::is_collide(A, Collider(b_resolved))) << "Resolved position should be non-colliding";
|
||||
|
||||
// Moving the other way should still collide
|
||||
Mesh b_wrong = b;
|
||||
b_wrong.set_origin(b_wrong.get_origin() - resolve);
|
||||
EXPECT_TRUE(GJK::is_collide(A, Collider(b_wrong)));
|
||||
}
|
||||
TEST(UnitTestEpa, TestCollisionTrue2)
|
||||
{
|
||||
std::vector<omath::primitives::Vertex<>> 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 }, {}, {} }
|
||||
};
|
||||
std::vector<omath::Vector3<std::uint32_t>> vao; // not needed
|
||||
|
||||
Mesh a(vbo, vao, {1, 1, 1});
|
||||
Mesh b(vbo, vao, {1, 1, 1});
|
||||
|
||||
// Overlap along +X by 0.5
|
||||
a.set_origin({0, 0, 0});
|
||||
b.set_origin({0.5f, 0, 0});
|
||||
|
||||
Collider A(a), B(b);
|
||||
|
||||
// --- GJK must detect collision and provide simplex ---
|
||||
auto gjk = GJK::is_collide_with_simplex_info(A, B);
|
||||
ASSERT_TRUE(gjk.hit) << "GJK should report collision for overlapping cubes";
|
||||
// --- EPA penetration ---
|
||||
EPA::Params params;
|
||||
params.max_iterations = 64;
|
||||
params.tolerance = 1e-4f;
|
||||
auto pool = std::make_shared<std::pmr::monotonic_buffer_resource>(1024);
|
||||
auto epa = EPA::solve(A, B, gjk.simplex, params, *pool);
|
||||
ASSERT_TRUE(epa.has_value()) << "EPA should converge";
|
||||
|
||||
// Normal is unit-length
|
||||
EXPECT_NEAR(epa->normal.dot(epa->normal), 1.0f, 1e-5f);
|
||||
|
||||
// For centers at 0 and +0.5 and half-extent 1 -> depth ≈ 1.5
|
||||
EXPECT_NEAR(epa->depth, 1.5f, 1e-3f);
|
||||
|
||||
// Axis sanity: mostly X
|
||||
EXPECT_NEAR(std::abs(epa->normal.x), 1.0f, 1e-3f);
|
||||
EXPECT_NEAR(epa->normal.y, 0.0f, 1e-3f);
|
||||
EXPECT_NEAR(epa->normal.z, 0.0f, 1e-3f);
|
||||
|
||||
constexpr float margin = 1.0f + 1e-3f; // tiny slack to avoid grazing
|
||||
const auto pen = epa->normal * epa->depth;
|
||||
|
||||
// Apply once: B + pen must separate; the opposite must still collide
|
||||
Mesh b_resolved = b;
|
||||
b_resolved.set_origin(b_resolved.get_origin() + pen * margin);
|
||||
EXPECT_FALSE(GJK::is_collide(A, Collider(b_resolved))) << "Applying penetration should separate";
|
||||
|
||||
Mesh b_wrong = b;
|
||||
b_wrong.set_origin(b_wrong.get_origin() - pen * margin);
|
||||
EXPECT_TRUE(GJK::is_collide(A, Collider(b_wrong))) << "Opposite direction should still intersect";
|
||||
|
||||
// Some book-keeping sanity
|
||||
EXPECT_GT(epa->iterations, 0);
|
||||
EXPECT_LT(epa->iterations, params.max_iterations);
|
||||
EXPECT_GE(epa->num_faces, 4);
|
||||
EXPECT_GT(epa->num_vertices, 4);
|
||||
}
|
||||
48
tests/general/unit_test_epa_internal.cpp
Normal file
48
tests/general/unit_test_epa_internal.cpp
Normal file
@@ -0,0 +1,48 @@
|
||||
#include "omath/collision/epa_algorithm.hpp"
|
||||
#include "omath/collision/simplex.hpp"
|
||||
#include "omath/linear_algebra/vector3.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using Vector3f = omath::Vector3<float>;
|
||||
|
||||
// Dummy collider type that exposes VectorType and returns small offsets
|
||||
struct DummyCollider
|
||||
{
|
||||
using VectorType = Vector3f;
|
||||
[[nodiscard]]
|
||||
static VectorType find_abs_furthest_vertex_position(const VectorType& dir) noexcept
|
||||
{
|
||||
// map direction to a small point so support_point is finite
|
||||
return Vector3f{dir.x * 0.01f, dir.y * 0.01f, dir.z * 0.01f};
|
||||
}
|
||||
};
|
||||
|
||||
using EpaDummy = omath::collision::Epa<DummyCollider>;
|
||||
using Simplex = omath::collision::Simplex<Vector3f>;
|
||||
|
||||
TEST(EpaInternal, SolveHandlesSmallPolytope)
|
||||
{
|
||||
// Create a simplex that is nearly degenerate but valid for solve
|
||||
Simplex s;
|
||||
s = { Vector3f{0.01f, 0.f, 0.f}, Vector3f{0.f, 0.01f, 0.f}, Vector3f{0.f, 0.f, 0.01f}, Vector3f{-0.01f, -0.01f, -0.01f} };
|
||||
|
||||
constexpr DummyCollider a;
|
||||
constexpr DummyCollider b;
|
||||
EpaDummy::Params params;
|
||||
params.max_iterations = 16;
|
||||
params.tolerance = 1e-6f;
|
||||
|
||||
const auto result = EpaDummy::solve(a, b, s, params);
|
||||
|
||||
// Should either return a valid result or gracefully return nullopt
|
||||
if (result)
|
||||
{
|
||||
EXPECT_TRUE(std::isfinite(result->depth));
|
||||
EXPECT_TRUE(std::isfinite(result->normal.x));
|
||||
EXPECT_GT(result->num_faces, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
SUCCEED() << "Epa::solve returned nullopt for small polytope (acceptable)";
|
||||
}
|
||||
}
|
||||
51
tests/general/unit_test_epa_more.cpp
Normal file
51
tests/general/unit_test_epa_more.cpp
Normal file
@@ -0,0 +1,51 @@
|
||||
#include "omath/collision/epa_algorithm.hpp"
|
||||
#include "omath/collision/simplex.hpp"
|
||||
#include "omath/linear_algebra/vector3.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using Vector3f = omath::Vector3<float>;
|
||||
|
||||
// Minimal collider interface matching Epa's expectations
|
||||
struct DegenerateCollider
|
||||
{
|
||||
using VectorType = Vector3f;
|
||||
// returns furthest point along dir
|
||||
VectorType find_abs_furthest_vertex_position(const VectorType& dir) const noexcept
|
||||
{
|
||||
// Always return points on a small circle in XY plane so some faces become degenerate
|
||||
if (dir.x > 0.5f) return {0.01f, 0.f, 0.f};
|
||||
if (dir.x < -0.5f) return {-0.01f, 0.f, 0.f};
|
||||
if (dir.y > 0.5f) return {0.f, 0.01f, 0.f};
|
||||
if (dir.y < -0.5f) return {0.f, -0.01f, 0.f};
|
||||
return {0.f, 0.f, 0.01f};
|
||||
}
|
||||
};
|
||||
|
||||
using Epa = omath::collision::Epa<DegenerateCollider>;
|
||||
using Simplex = omath::collision::Simplex<Vector3f>;
|
||||
|
||||
TEST(EpaExtra, DegenerateFaceHandled)
|
||||
{
|
||||
// Prepare a simplex with near-collinear points to force degenerate face handling
|
||||
Simplex s;
|
||||
s = { Vector3f{0.01f, 0.f, 0.f}, Vector3f{0.02f, 0.f, 0.f}, Vector3f{0.03f, 0.f, 0.f}, Vector3f{0.0f, 0.0f, 0.01f} };
|
||||
|
||||
constexpr DegenerateCollider a;
|
||||
constexpr DegenerateCollider b;
|
||||
Epa::Params params;
|
||||
params.max_iterations = 4;
|
||||
params.tolerance = 1e-6f;
|
||||
|
||||
const auto result = Epa::solve(a, b, s, params);
|
||||
|
||||
// The algorithm should either return a valid result or gracefully exit (not crash)
|
||||
if (result)
|
||||
{
|
||||
EXPECT_TRUE(std::isfinite(result->depth));
|
||||
EXPECT_TRUE(std::isfinite(result->normal.x));
|
||||
}
|
||||
else
|
||||
{
|
||||
SUCCEED() << "EPA returned nullopt for degenerate input (acceptable)";
|
||||
}
|
||||
}
|
||||
@@ -8,15 +8,18 @@
|
||||
namespace
|
||||
{
|
||||
const omath::source_engine::Mesh mesh = {
|
||||
{{-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}},
|
||||
{}};
|
||||
{
|
||||
{ {-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}, {}, {} }
|
||||
},
|
||||
{}
|
||||
};
|
||||
}
|
||||
TEST(UnitTestGjk, TestCollisionTrue)
|
||||
{
|
||||
|
||||
@@ -19,9 +19,9 @@ namespace
|
||||
// -----------------------------------------------------------------------------
|
||||
// Constants & helpers
|
||||
// -----------------------------------------------------------------------------
|
||||
constexpr float kTol = 1e-5f;
|
||||
constexpr float k_tol = 1e-5f;
|
||||
|
||||
bool VecEqual(const Vec3& a, const Vec3& b, float tol = kTol)
|
||||
bool vec_equal(const Vec3& a, const Vec3& b, const float tol = k_tol)
|
||||
{
|
||||
return std::fabs(a.x - b.x) < tol &&
|
||||
std::fabs(a.y - b.y) < tol &&
|
||||
@@ -58,8 +58,8 @@ namespace
|
||||
|
||||
TEST_P(CanTraceLineParam, VariousRays)
|
||||
{
|
||||
const auto& p = GetParam();
|
||||
EXPECT_EQ(LineTracer::can_trace_line(p.ray, triangle), p.expected_clear);
|
||||
const auto& [ray, expected_clear] = GetParam();
|
||||
EXPECT_EQ(LineTracer::can_trace_line(ray, triangle), expected_clear);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
@@ -85,8 +85,8 @@ namespace
|
||||
constexpr Vec3 expected{0.3f, 0.3f, 0.f};
|
||||
|
||||
const Vec3 hit = LineTracer::get_ray_hit_point(ray, triangle);
|
||||
ASSERT_FALSE(VecEqual(hit, ray.end));
|
||||
EXPECT_TRUE(VecEqual(hit, expected));
|
||||
ASSERT_FALSE(vec_equal(hit, ray.end));
|
||||
EXPECT_TRUE(vec_equal(hit, expected));
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
65
tests/general/unit_test_line_tracer.cpp
Normal file
65
tests/general/unit_test_line_tracer.cpp
Normal file
@@ -0,0 +1,65 @@
|
||||
#include "omath/collision/line_tracer.hpp"
|
||||
#include "omath/linear_algebra/vector3.hpp"
|
||||
#include "omath/linear_algebra/triangle.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using omath::Vector3;
|
||||
|
||||
TEST(LineTracerTests, ParallelRayReturnsEnd)
|
||||
{
|
||||
// Triangle in XY plane
|
||||
constexpr omath::Triangle<Vector3<float>> tri{ {0.f,0.f,0.f}, {1.f,0.f,0.f}, {0.f,1.f,0.f} };
|
||||
omath::collision::Ray ray;
|
||||
ray.start = Vector3<float>{0.f,0.f,1.f};
|
||||
ray.end = Vector3<float>{1.f,1.f,2.f}; // direction parallel to plane normal (z) -> but choose parallel to plane? make direction parallel to triangle plane
|
||||
ray.end = Vector3<float>{1.f,1.f,1.f};
|
||||
|
||||
// For a ray parallel to the triangle plane the algorithm should return ray.end
|
||||
const auto hit = omath::collision::LineTracer::get_ray_hit_point(ray, tri);
|
||||
EXPECT_TRUE(hit == ray.end);
|
||||
EXPECT_TRUE(omath::collision::LineTracer::can_trace_line(ray, tri));
|
||||
}
|
||||
|
||||
TEST(LineTracerTests, MissesTriangleReturnsEnd)
|
||||
{
|
||||
constexpr omath::Triangle<Vector3<float>> tri{ {0.f,0.f,0.f}, {1.f,0.f,0.f}, {0.f,1.f,0.f} };
|
||||
omath::collision::Ray ray;
|
||||
ray.start = Vector3<float>{2.f,2.f,-1.f};
|
||||
ray.end = Vector3<float>{2.f,2.f,1.f}; // passes above the triangle area
|
||||
|
||||
const auto hit = omath::collision::LineTracer::get_ray_hit_point(ray, tri);
|
||||
EXPECT_TRUE(hit == ray.end);
|
||||
}
|
||||
|
||||
TEST(LineTracerTests, HitTriangleReturnsPointInsideSegment)
|
||||
{
|
||||
constexpr omath::Triangle<Vector3<float>> tri{ {0.f,0.f,0.f}, {2.f,0.f,0.f}, {0.f,2.f,0.f} };
|
||||
omath::collision::Ray ray;
|
||||
ray.start = Vector3<float>{0.25f,0.25f,-1.f};
|
||||
ray.end = Vector3<float>{0.25f,0.25f,1.f};
|
||||
|
||||
const auto hit = omath::collision::LineTracer::get_ray_hit_point(ray, tri);
|
||||
// Should return a point between start and end (z approximately 0)
|
||||
EXPECT_NE(hit, ray.end);
|
||||
EXPECT_NEAR(hit.z, 0.f, 1e-4f);
|
||||
// t_hit should be between 0 and 1 along the ray direction
|
||||
const auto dir = ray.direction_vector();
|
||||
// find t such that start + dir * t == hit (only check z comp for stability)
|
||||
const float t = (hit.z - ray.start.z) / dir.z;
|
||||
EXPECT_GT(t, 0.f);
|
||||
EXPECT_LT(t, 1.f);
|
||||
}
|
||||
|
||||
TEST(LineTracerTests, InfiniteLengthEarlyOut)
|
||||
{
|
||||
constexpr omath::Triangle<Vector3<float>> tri{ {0.f,0.f,0.f}, {1.f,0.f,0.f}, {0.f,1.f,0.f} };
|
||||
omath::collision::Ray ray;
|
||||
ray.start = Vector3<float>{0.25f,0.25f,0.f};
|
||||
ray.end = Vector3<float>{0.25f,0.25f,1.f};
|
||||
ray.infinite_length = true;
|
||||
|
||||
// If t_hit <= epsilon the algorithm should return ray.end when infinite_length is true.
|
||||
// Using start on the triangle plane should produce t_hit <= epsilon.
|
||||
const auto hit = omath::collision::LineTracer::get_ray_hit_point(ray, tri);
|
||||
EXPECT_TRUE(hit == ray.end);
|
||||
}
|
||||
48
tests/general/unit_test_line_tracer_extra.cpp
Normal file
48
tests/general/unit_test_line_tracer_extra.cpp
Normal file
@@ -0,0 +1,48 @@
|
||||
// Extra LineTracer tests
|
||||
#include <gtest/gtest.h>
|
||||
#include <omath/collision/line_tracer.hpp>
|
||||
#include <omath/linear_algebra/vector3.hpp>
|
||||
|
||||
using namespace omath;
|
||||
using namespace omath::collision;
|
||||
|
||||
TEST(LineTracerExtra, MissParallel)
|
||||
{
|
||||
constexpr Triangle<Vector3<float>> tri({0,0,0},{1,0,0},{0,1,0});
|
||||
constexpr Ray ray{ {0.3f,0.3f,1.f}, {0.3f,0.3f,2.f}, false }; // parallel above triangle
|
||||
const auto hit = LineTracer::get_ray_hit_point(ray, tri);
|
||||
EXPECT_EQ(hit, ray.end);
|
||||
}
|
||||
|
||||
TEST(LineTracerExtra, HitCenter)
|
||||
{
|
||||
constexpr Triangle<Vector3<float>> tri({0,0,0},{1,0,0},{0,1,0});
|
||||
constexpr Ray ray{ {0.3f,0.3f,-1.f}, {0.3f,0.3f,1.f}, false };
|
||||
const auto hit = LineTracer::get_ray_hit_point(ray, tri);
|
||||
ASSERT_FALSE(hit == ray.end);
|
||||
EXPECT_NEAR(hit.x, 0.3f, 1e-6f);
|
||||
EXPECT_NEAR(hit.y, 0.3f, 1e-6f);
|
||||
EXPECT_NEAR(hit.z, 0.f, 1e-6f);
|
||||
}
|
||||
|
||||
TEST(LineTracerExtra, HitOnEdge)
|
||||
{
|
||||
constexpr Triangle<Vector3<float>> tri({0,0,0},{1,0,0},{0,1,0});
|
||||
constexpr Ray ray{ {0.0f,0.0f,1.f}, {0.0f,0.0f,0.f}, false };
|
||||
const auto hit = LineTracer::get_ray_hit_point(ray, tri);
|
||||
// hitting exact vertex/edge may be considered miss; ensure function handles without crash
|
||||
if (hit != ray.end)
|
||||
{
|
||||
EXPECT_NEAR(hit.x, 0.0f, 1e-6f);
|
||||
EXPECT_NEAR(hit.y, 0.0f, 1e-6f);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(LineTracerExtra, InfiniteRayIgnoredIfBehind)
|
||||
{
|
||||
constexpr Triangle<Vector3<float>> tri({0,0,0},{1,0,0},{0,1,0});
|
||||
// Ray pointing away but infinite_length true should be ignored
|
||||
constexpr Ray ray{ {0.5f,0.5f,-1.f}, {0.5f,0.5f,-2.f}, true };
|
||||
const auto hit = LineTracer::get_ray_hit_point(ray, tri);
|
||||
EXPECT_EQ(hit, ray.end);
|
||||
}
|
||||
105
tests/general/unit_test_line_tracer_more.cpp
Normal file
105
tests/general/unit_test_line_tracer_more.cpp
Normal file
@@ -0,0 +1,105 @@
|
||||
#include "omath/collision/line_tracer.hpp"
|
||||
#include "omath/linear_algebra/triangle.hpp"
|
||||
#include "omath/linear_algebra/vector3.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using omath::Vector3;
|
||||
using omath::collision::Ray;
|
||||
using omath::collision::LineTracer;
|
||||
using Triangle3 = omath::Triangle<Vector3<float>>;
|
||||
|
||||
TEST(LineTracerMore, ParallelRayReturnsEnd)
|
||||
{
|
||||
// Ray parallel to triangle plane: construct triangle in XY plane and ray along X axis
|
||||
constexpr Triangle3 tri(Vector3<float>{0.f,0.f,0.f}, Vector3<float>{1.f,0.f,0.f}, Vector3<float>{0.f,1.f,0.f});
|
||||
Ray ray; ray.start = {0.f,0.f,1.f}; ray.end = {1.f,0.f,1.f};
|
||||
|
||||
const auto hit = LineTracer::get_ray_hit_point(ray, tri);
|
||||
EXPECT_EQ(hit, ray.end);
|
||||
}
|
||||
|
||||
TEST(LineTracerMore, UOutOfRangeReturnsEnd)
|
||||
{
|
||||
// Construct a ray that misses due to u < 0
|
||||
constexpr Triangle3 tri(Vector3<float>{0.f,0.f,0.f}, Vector3<float>{1.f,0.f,0.f}, Vector3<float>{0.f,1.f,0.f});
|
||||
Ray ray; ray.start = {-1.f,-1.f,-1.f}; ray.end = {-0.5f,-1.f,1.f};
|
||||
|
||||
const auto hit = LineTracer::get_ray_hit_point(ray, tri);
|
||||
EXPECT_EQ(hit, ray.end);
|
||||
}
|
||||
|
||||
TEST(LineTracerMore, VOutOfRangeReturnsEnd)
|
||||
{
|
||||
// Construct ray that has v < 0
|
||||
constexpr Triangle3 tri(Vector3<float>{0.f,0.f,0.f}, Vector3<float>{1.f,0.f,0.f}, Vector3<float>{0.f,1.f,0.f});
|
||||
Ray ray; ray.start = {2.f,2.f,-1.f}; ray.end = {2.f,2.f,1.f};
|
||||
|
||||
const auto hit = LineTracer::get_ray_hit_point(ray, tri);
|
||||
EXPECT_EQ(hit, ray.end);
|
||||
}
|
||||
|
||||
TEST(LineTracerMore, THitTooSmallReturnsEnd)
|
||||
{
|
||||
constexpr Triangle3 tri(Vector3<float>{0.f,0.f,0.f}, Vector3<float>{1.f,0.f,0.f}, Vector3<float>{0.f,1.f,0.f});
|
||||
Ray ray; ray.start = {0.f,0.f,0.0000000001f}; ray.end = {0.f,0.f,1.f};
|
||||
|
||||
const auto hit = LineTracer::get_ray_hit_point(ray, tri);
|
||||
EXPECT_EQ(hit, ray.end);
|
||||
}
|
||||
|
||||
TEST(LineTracerMore, THitGreaterThanOneReturnsEnd)
|
||||
{
|
||||
constexpr Triangle3 tri(Vector3<float>{0.f,0.f,0.f}, Vector3<float>{1.f,0.f,0.f}, Vector3<float>{0.f,1.f,0.f});
|
||||
// Choose a ray and compute t_hit locally to assert consistency
|
||||
Ray ray; ray.start = {0.f,0.f,-1.f}; ray.end = {0.f,0.f,-0.5f};
|
||||
|
||||
const auto hit = LineTracer::get_ray_hit_point(ray, tri);
|
||||
|
||||
constexpr float k_epsilon = std::numeric_limits<float>::epsilon();
|
||||
constexpr auto side_a = tri.side_a_vector();
|
||||
constexpr auto side_b = tri.side_b_vector();
|
||||
const auto ray_dir = ray.direction_vector();
|
||||
const auto p = ray_dir.cross(side_b);
|
||||
const auto det = side_a.dot(p);
|
||||
|
||||
if (std::abs(det) < k_epsilon)
|
||||
{
|
||||
EXPECT_EQ(hit, ray.end);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto inv_det = 1.0f / det;
|
||||
const auto tvec = ray.start - tri.m_vertex2;
|
||||
const auto q = tvec.cross(side_a);
|
||||
const auto t_hit = side_b.dot(q) * inv_det;
|
||||
|
||||
if (t_hit <= k_epsilon || t_hit > 1.0f)
|
||||
EXPECT_EQ(hit, ray.end) << "t_hit=" << t_hit << " hit=" << hit.x << "," << hit.y << "," << hit.z;
|
||||
else
|
||||
EXPECT_NE(hit, ray.end) << "t_hit=" << t_hit << " hit=" << hit.x << "," << hit.y << "," << hit.z;
|
||||
}
|
||||
|
||||
TEST(LineTracerMore, InfiniteLengthWithSmallTHitReturnsEnd)
|
||||
{
|
||||
Triangle3 tri(Vector3<float>{0.f,0.f,0.f}, Vector3<float>{1.f,0.f,0.f}, Vector3<float>{0.f,1.f,0.f});
|
||||
constexpr Triangle3 tri2(Vector3<float>{0.f,0.f,-1e-8f}, Vector3<float>{1.f,0.f,-1e-8f}, Vector3<float>{0.f,1.f,-1e-8f});
|
||||
Ray ray; ray.start = {0.f,0.f,0.f}; ray.end = {0.f,0.f,1.f}; ray.infinite_length = true;
|
||||
// Create triangle slightly behind so t_hit <= eps
|
||||
tri = tri2;
|
||||
|
||||
const auto hit = LineTracer::get_ray_hit_point(ray, tri);
|
||||
EXPECT_EQ(hit, ray.end);
|
||||
}
|
||||
|
||||
TEST(LineTracerMore, SuccessfulHitReturnsPoint)
|
||||
{
|
||||
constexpr Triangle3 tri(Vector3<float>{0.f,0.f,0.f}, Vector3<float>{1.f,0.f,0.f}, Vector3<float>{0.f,1.f,0.f});
|
||||
Ray ray; ray.start = {0.1f,0.1f,-1.f}; ray.end = {0.1f,0.1f,1.f};
|
||||
|
||||
const auto hit = LineTracer::get_ray_hit_point(ray, tri);
|
||||
EXPECT_NE(hit, ray.end);
|
||||
// Hit should be on plane z=0 and near x=0.1,y=0.1
|
||||
EXPECT_NEAR(hit.z, 0.f, 1e-6f);
|
||||
EXPECT_NEAR(hit.x, 0.1f, 1e-3f);
|
||||
EXPECT_NEAR(hit.y, 0.1f, 1e-3f);
|
||||
}
|
||||
57
tests/general/unit_test_line_tracer_more2.cpp
Normal file
57
tests/general/unit_test_line_tracer_more2.cpp
Normal file
@@ -0,0 +1,57 @@
|
||||
#include "omath/collision/line_tracer.hpp"
|
||||
#include "omath/linear_algebra/triangle.hpp"
|
||||
#include "omath/linear_algebra/vector3.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using omath::Vector3;
|
||||
using omath::collision::Ray;
|
||||
using omath::collision::LineTracer;
|
||||
using Triangle3 = omath::Triangle<Vector3<float>>;
|
||||
|
||||
TEST(LineTracerMore2, UGreaterThanOneReturnsEnd)
|
||||
{
|
||||
constexpr Triangle3 tri({0.f,0.f,0.f},{1.f,0.f,0.f},{0.f,1.f,0.f});
|
||||
// choose ray so barycentric u > 1
|
||||
Ray ray; ray.start = {2.f, -1.f, -1.f}; ray.end = {2.f, -1.f, 1.f};
|
||||
|
||||
const auto hit = LineTracer::get_ray_hit_point(ray, tri);
|
||||
EXPECT_EQ(hit, ray.end);
|
||||
}
|
||||
|
||||
TEST(LineTracerMore2, VGreaterThanOneReturnsEnd)
|
||||
{
|
||||
constexpr Triangle3 tri({0.f,0.f,0.f},{1.f,0.f,0.f},{0.f,1.f,0.f});
|
||||
// choose ray so barycentric v > 1
|
||||
Ray ray; ray.start = {-1.f, 2.f, -1.f}; ray.end = {-1.f, 2.f, 1.f};
|
||||
|
||||
const auto hit = LineTracer::get_ray_hit_point(ray, tri);
|
||||
EXPECT_EQ(hit, ray.end);
|
||||
}
|
||||
|
||||
TEST(LineTracerMore2, UPlusVGreaterThanOneReturnsEnd)
|
||||
{
|
||||
constexpr Triangle3 tri({0.f,0.f,0.f},{1.f,0.f,0.f},{0.f,1.f,0.f});
|
||||
// Ray aimed so u+v > 1 (outside triangle region)
|
||||
Ray ray; ray.start = {1.f, 1.f, -1.f}; ray.end = {1.f, 1.f, 1.f};
|
||||
|
||||
const auto hit = LineTracer::get_ray_hit_point(ray, tri);
|
||||
EXPECT_EQ(hit, ray.end);
|
||||
}
|
||||
|
||||
TEST(LineTracerMore2, DirectionVectorNormalizedProducesUnitLength)
|
||||
{
|
||||
Ray r; r.start = {0.f,0.f,0.f}; r.end = {0.f,3.f,4.f};
|
||||
const auto dir = r.direction_vector_normalized();
|
||||
const auto len = dir.length();
|
||||
EXPECT_NEAR(len, 1.f, 1e-6f);
|
||||
}
|
||||
|
||||
TEST(LineTracerMore2, ZeroLengthRayHandled)
|
||||
{
|
||||
constexpr Triangle3 tri({0.f,0.f,0.f},{1.f,0.f,0.f},{0.f,1.f,0.f});
|
||||
Ray ray; ray.start = {0.f,0.f,0.f}; ray.end = {0.f,0.f,0.f};
|
||||
|
||||
// Zero-length ray: direction length == 0; algorithm should handle without crash
|
||||
const auto hit = LineTracer::get_ray_hit_point(ray, tri);
|
||||
EXPECT_EQ(hit, ray.end);
|
||||
}
|
||||
57
tests/general/unit_test_linear_algebra_cover_more_ops.cpp
Normal file
57
tests/general/unit_test_linear_algebra_cover_more_ops.cpp
Normal file
@@ -0,0 +1,57 @@
|
||||
// Added to increase coverage for vector3/vector4/mat headers
|
||||
#include <gtest/gtest.h>
|
||||
#include <stdexcept>
|
||||
#include <sstream>
|
||||
|
||||
#include "omath/linear_algebra/vector3.hpp"
|
||||
#include "omath/linear_algebra/vector4.hpp"
|
||||
#include "omath/linear_algebra/mat.hpp"
|
||||
|
||||
using namespace omath;
|
||||
|
||||
TEST(Vector3ScalarOps, InPlaceScalarOperators)
|
||||
{
|
||||
Vector3<float> v{1.f, 2.f, 3.f};
|
||||
|
||||
v += 1.f;
|
||||
EXPECT_FLOAT_EQ(v.x, 2.f);
|
||||
EXPECT_FLOAT_EQ(v.y, 3.f);
|
||||
EXPECT_FLOAT_EQ(v.z, 4.f);
|
||||
|
||||
v /= 2.f;
|
||||
EXPECT_FLOAT_EQ(v.x, 1.f);
|
||||
EXPECT_FLOAT_EQ(v.y, 1.5f);
|
||||
EXPECT_FLOAT_EQ(v.z, 2.f);
|
||||
|
||||
v -= 0.5f;
|
||||
EXPECT_FLOAT_EQ(v.x, 0.5f);
|
||||
EXPECT_FLOAT_EQ(v.y, 1.0f);
|
||||
EXPECT_FLOAT_EQ(v.z, 1.5f);
|
||||
}
|
||||
|
||||
TEST(Vector4BinaryOps, ElementWiseMulDiv)
|
||||
{
|
||||
constexpr Vector4<float> a{2.f, 4.f, 6.f, 8.f};
|
||||
constexpr Vector4<float> b{1.f, 2.f, 3.f, 4.f};
|
||||
|
||||
constexpr auto m = a * b;
|
||||
EXPECT_FLOAT_EQ(m.x, 2.f);
|
||||
EXPECT_FLOAT_EQ(m.y, 8.f);
|
||||
EXPECT_FLOAT_EQ(m.z, 18.f);
|
||||
EXPECT_FLOAT_EQ(m.w, 32.f);
|
||||
|
||||
constexpr auto d = a / b;
|
||||
EXPECT_FLOAT_EQ(d.x, 2.f);
|
||||
EXPECT_FLOAT_EQ(d.y, 2.f);
|
||||
EXPECT_FLOAT_EQ(d.z, 2.f);
|
||||
EXPECT_FLOAT_EQ(d.w, 2.f);
|
||||
}
|
||||
|
||||
TEST(MatInitExceptions, InvalidInitializerLists)
|
||||
{
|
||||
// Wrong number of rows
|
||||
EXPECT_THROW((Mat<2,2,float>{ {1.f,2.f} }), std::invalid_argument);
|
||||
|
||||
// Row with wrong number of columns
|
||||
EXPECT_THROW((Mat<2,2,float>{ {1.f,2.f}, {1.f} }), std::invalid_argument);
|
||||
}
|
||||
50
tests/general/unit_test_linear_algebra_cover_remaining.cpp
Normal file
50
tests/general/unit_test_linear_algebra_cover_remaining.cpp
Normal file
@@ -0,0 +1,50 @@
|
||||
// Additional coverage tests for Vector4 and Mat
|
||||
#include <gtest/gtest.h>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "omath/linear_algebra/vector4.hpp"
|
||||
#include "omath/linear_algebra/mat.hpp"
|
||||
|
||||
using namespace omath;
|
||||
|
||||
static void make_bad_mat_rows()
|
||||
{
|
||||
// wrong number of rows -> should throw inside initializer-list ctor
|
||||
[[maybe_unused]] const Mat<2, 2, float> m{{1.f, 2.f}};
|
||||
}
|
||||
|
||||
static void make_bad_mat_cols()
|
||||
{
|
||||
// row with wrong number of columns -> should throw
|
||||
[[maybe_unused]] const Mat<2, 2, float> m{{1.f, 2.f}, {1.f}};
|
||||
}
|
||||
|
||||
TEST(Vector4Operator, Subtraction)
|
||||
{
|
||||
constexpr Vector4<float> a{5.f, 6.f, 7.f, 8.f};
|
||||
constexpr Vector4<float> b{1.f, 2.f, 3.f, 4.f};
|
||||
|
||||
constexpr auto r = a - b;
|
||||
EXPECT_FLOAT_EQ(r.x, 4.f);
|
||||
EXPECT_FLOAT_EQ(r.y, 4.f);
|
||||
EXPECT_FLOAT_EQ(r.z, 4.f);
|
||||
EXPECT_FLOAT_EQ(r.w, 4.f);
|
||||
}
|
||||
|
||||
TEST(MatInitializerExceptions, ForcedThrowLines)
|
||||
{
|
||||
EXPECT_THROW(make_bad_mat_rows(), std::invalid_argument);
|
||||
EXPECT_THROW(make_bad_mat_cols(), std::invalid_argument);
|
||||
}
|
||||
|
||||
TEST(MatSelfAssignment, CopyAndMoveSelfAssign)
|
||||
{
|
||||
Mat<2,2,float> m{{1.f,2.f},{3.f,4.f}};
|
||||
// self copy-assignment
|
||||
m = m;
|
||||
EXPECT_FLOAT_EQ(m.at(0, 0), 1.f);
|
||||
|
||||
// self move-assignment
|
||||
m = std::move(m);
|
||||
EXPECT_FLOAT_EQ(m.at(0, 0), 1.f);
|
||||
}
|
||||
64
tests/general/unit_test_linear_algebra_extra.cpp
Normal file
64
tests/general/unit_test_linear_algebra_extra.cpp
Normal file
@@ -0,0 +1,64 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <omath/linear_algebra/vector2.hpp>
|
||||
#include <omath/linear_algebra/vector3.hpp>
|
||||
#include <omath/linear_algebra/vector4.hpp>
|
||||
#include <omath/linear_algebra/mat.hpp>
|
||||
#include <functional>
|
||||
#include <format>
|
||||
|
||||
using namespace omath;
|
||||
|
||||
TEST(LinearAlgebraExtra, FormatterAndHashVector2)
|
||||
{
|
||||
Vector2<float> v{1.0f, 2.0f};
|
||||
const std::string s = std::format("{}", v);
|
||||
EXPECT_EQ(s, "[1, 2]");
|
||||
|
||||
const std::size_t h1 = std::hash<Vector2<float>>{}(v);
|
||||
const std::size_t h2 = std::hash<Vector2<float>>{}(Vector2<float>{1.0f, 2.0f});
|
||||
const std::size_t h3 = std::hash<Vector2<float>>{}(Vector2<float>{2.0f, 3.0f});
|
||||
|
||||
EXPECT_EQ(h1, h2);
|
||||
EXPECT_NE(h1, h3);
|
||||
}
|
||||
|
||||
TEST(LinearAlgebraExtra, FormatterAndHashVector3)
|
||||
{
|
||||
Vector3<float> v{1.0f, 2.0f, 3.0f};
|
||||
const std::string s = std::format("{}", v);
|
||||
EXPECT_EQ(s, "[1, 2, 3]");
|
||||
|
||||
const std::size_t h1 = std::hash<Vector3<float>>{}(v);
|
||||
const std::size_t h2 = std::hash<Vector3<float>>{}(Vector3<float>{1.0f, 2.0f, 3.0f});
|
||||
EXPECT_EQ(h1, h2);
|
||||
|
||||
// point_to_same_direction
|
||||
EXPECT_TRUE((Vector3<float>{1,0,0}.point_to_same_direction(Vector3<float>{2,0,0})));
|
||||
EXPECT_FALSE((Vector3<float>{1,0,0}.point_to_same_direction(Vector3<float>{-1,0,0})));
|
||||
}
|
||||
|
||||
TEST(LinearAlgebraExtra, FormatterAndHashVector4)
|
||||
{
|
||||
Vector4<float> v{1.0f, 2.0f, 3.0f, 4.0f};
|
||||
const std::string s = std::format("{}", v);
|
||||
EXPECT_EQ(s, "[1, 2, 3, 4]");
|
||||
|
||||
const std::size_t h1 = std::hash<Vector4<float>>{}(v);
|
||||
const std::size_t h2 = std::hash<Vector4<float>>{}(Vector4<float>{1.0f, 2.0f, 3.0f, 4.0f});
|
||||
EXPECT_EQ(h1, h2);
|
||||
}
|
||||
|
||||
TEST(LinearAlgebraExtra, MatRawArrayAndOperators)
|
||||
{
|
||||
Mat<2,2> m{{1.0f, 2.0f},{3.0f,4.0f}};
|
||||
const auto raw = m.raw_array();
|
||||
EXPECT_EQ(raw.size(), 4);
|
||||
EXPECT_FLOAT_EQ(raw[0], 1.0f);
|
||||
EXPECT_FLOAT_EQ(raw[3], 4.0f);
|
||||
|
||||
// operator[] index access
|
||||
EXPECT_FLOAT_EQ(m.at(0,0), 1.0f);
|
||||
EXPECT_FLOAT_EQ(m.at(1,1), 4.0f);
|
||||
}
|
||||
|
||||
|
||||
56
tests/general/unit_test_linear_algebra_helpers.cpp
Normal file
56
tests/general/unit_test_linear_algebra_helpers.cpp
Normal file
@@ -0,0 +1,56 @@
|
||||
#include "omath/linear_algebra/triangle.hpp"
|
||||
#include "omath/linear_algebra/vector3.hpp"
|
||||
#include "omath/linear_algebra/vector4.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
// This test file exercises the non-inlined helpers added to headers
|
||||
// (Vector3, Triangle, Vector4) to encourage symbol emission and
|
||||
// runtime execution so coverage tools can attribute hits back to the
|
||||
// header lines.
|
||||
|
||||
using namespace omath;
|
||||
|
||||
TEST(LinearAlgebraHelpers, Vector3NoInlineHelpersExecute)
|
||||
{
|
||||
constexpr Vector3<float> a{1.f, 2.f, 3.f};
|
||||
constexpr Vector3<float> b{4.f, 5.f, 6.f};
|
||||
|
||||
// Execute helpers that were made non-inlined
|
||||
const auto l = a.length();
|
||||
const auto ang = a.angle_between(b);
|
||||
const auto perp = a.is_perpendicular(b);
|
||||
const auto norm = a.normalized();
|
||||
|
||||
(void)l; (void)ang; (void)perp; (void)norm;
|
||||
SUCCEED();
|
||||
}
|
||||
|
||||
TEST(LinearAlgebraHelpers, TriangleNoInlineHelpersExecute)
|
||||
{
|
||||
constexpr Vector3<float> v1{0.f,0.f,0.f};
|
||||
constexpr Vector3<float> v2{3.f,0.f,0.f};
|
||||
constexpr Vector3<float> v3{3.f,4.f,0.f};
|
||||
|
||||
constexpr Triangle<Vector3<float>> t{v1, v2, v3};
|
||||
|
||||
const auto n = t.calculate_normal();
|
||||
const auto a = t.side_a_length();
|
||||
const auto b = t.side_b_length();
|
||||
const auto h = t.hypot();
|
||||
const auto r = t.is_rectangular();
|
||||
|
||||
(void)n; (void)a; (void)b; (void)h; (void)r;
|
||||
SUCCEED();
|
||||
}
|
||||
|
||||
TEST(LinearAlgebraHelpers, Vector4NoInlineHelpersExecute)
|
||||
{
|
||||
Vector4<float> v{1.f,2.f,3.f,4.f};
|
||||
|
||||
const auto l = v.length();
|
||||
const auto s = v.sum();
|
||||
v.clamp(-10.f, 10.f);
|
||||
|
||||
(void)l; (void)s;
|
||||
SUCCEED();
|
||||
}
|
||||
74
tests/general/unit_test_linear_algebra_instantiate.cpp
Normal file
74
tests/general/unit_test_linear_algebra_instantiate.cpp
Normal file
@@ -0,0 +1,74 @@
|
||||
// Instantiation-only tests to force out-of-line template emission
|
||||
#include <gtest/gtest.h>
|
||||
#include <format>
|
||||
#include <functional>
|
||||
|
||||
#include "omath/linear_algebra/vector3.hpp"
|
||||
#include "omath/linear_algebra/vector4.hpp"
|
||||
#include "omath/linear_algebra/mat.hpp"
|
||||
|
||||
using namespace omath;
|
||||
|
||||
TEST(LinearAlgebraInstantiate, Vector3AndVector4AndMatCoverage) {
|
||||
// Vector3 usage
|
||||
Vector3<float> a{1.f, 2.f, 3.f};
|
||||
Vector3<float> b{4.f, 5.f, 6.f};
|
||||
|
||||
// call various methods
|
||||
volatile float d0 = a.distance_to_sqr(b);
|
||||
volatile float d1 = a.dot(b);
|
||||
volatile auto c = a.cross(b);
|
||||
auto tup = a.as_tuple();
|
||||
volatile bool dir = a.point_to_same_direction(b);
|
||||
|
||||
// non-inlined helpers
|
||||
volatile float ln = a.length();
|
||||
auto ang = a.angle_between(b);
|
||||
volatile bool perp = a.is_perpendicular(b, 0.1f);
|
||||
volatile auto anorm = a.normalized();
|
||||
|
||||
// formatter and hash instantiations (char only)
|
||||
(void)std::format("{}", a);
|
||||
(void)std::hash<Vector3<float>>{}(a);
|
||||
|
||||
// Vector4 usage
|
||||
Vector4<float> v4{1.f, -2.f, 3.f, -4.f};
|
||||
volatile float v4len = v4.length();
|
||||
volatile float v4sum = v4.sum();
|
||||
v4.clamp(-2.f, 2.f);
|
||||
(void)std::format("{}", v4);
|
||||
(void)std::hash<Vector4<float>>{}(v4);
|
||||
|
||||
// Mat usage: instantiate several sizes and store orders
|
||||
Mat<1,1> m1{{42.f}};
|
||||
volatile float m1det = m1.determinant();
|
||||
|
||||
Mat<2,2> m2{{{1.f,2.f},{3.f,4.f}}};
|
||||
volatile float det2 = m2.determinant();
|
||||
auto tr2 = m2.transposed();
|
||||
auto minor00 = m2.minor(0,0);
|
||||
auto algc = m2.alg_complement(0,1);
|
||||
auto rarr = m2.raw_array();
|
||||
auto inv2 = m2.inverted();
|
||||
|
||||
Mat<3,3> m3{{{1.f,2.f,3.f},{4.f,5.f,6.f},{7.f,8.f,9.f}}};
|
||||
volatile float det3 = m3.determinant();
|
||||
auto strip = m3.strip(0,0);
|
||||
auto min = m3.minor(2,2);
|
||||
|
||||
// to_string/wstring/u8string and to_screen_mat
|
||||
auto s = m2.to_string();
|
||||
auto ws = m2.to_wstring();
|
||||
auto u8s = m2.to_u8string();
|
||||
auto screen = Mat<4,4>::to_screen_mat(800.f, 600.f);
|
||||
|
||||
// call non-inlined mat helpers
|
||||
volatile auto det = m2.determinant();
|
||||
volatile auto inv = m2.inverted();
|
||||
volatile auto trans = m2.transposed();
|
||||
volatile auto raw = m2.raw_array();
|
||||
|
||||
// simple sanity checks (not strict, only to use values)
|
||||
EXPECT_EQ(std::get<0>(tup), 1.f);
|
||||
EXPECT_TRUE(det2 != 0.f || inv2 == std::nullopt);
|
||||
}
|
||||
64
tests/general/unit_test_linear_algebra_more.cpp
Normal file
64
tests/general/unit_test_linear_algebra_more.cpp
Normal file
@@ -0,0 +1,64 @@
|
||||
#include "omath/linear_algebra/triangle.hpp"
|
||||
#include "omath/linear_algebra/vector3.hpp"
|
||||
#include "omath/linear_algebra/vector4.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using namespace omath;
|
||||
|
||||
TEST(LinearAlgebraMore, Vector3EdgeCases)
|
||||
{
|
||||
constexpr Vector3<float> zero{0.f,0.f,0.f};
|
||||
constexpr Vector3<float> v{1.f,0.f,0.f};
|
||||
|
||||
// angle_between should be unexpected when one vector has zero length
|
||||
const auto angle = zero.angle_between(v);
|
||||
EXPECT_FALSE(static_cast<bool>(angle));
|
||||
|
||||
// normalized of zero should return zero
|
||||
const auto nz = zero.normalized();
|
||||
EXPECT_EQ(nz.x, 0.f);
|
||||
EXPECT_EQ(nz.y, 0.f);
|
||||
EXPECT_EQ(nz.z, 0.f);
|
||||
|
||||
// perpendicular case: x-axis and y-axis
|
||||
constexpr Vector3<float> x{1.f,0.f,0.f};
|
||||
constexpr Vector3<float> y{0.f,1.f,0.f};
|
||||
EXPECT_TRUE(x.is_perpendicular(y));
|
||||
}
|
||||
|
||||
TEST(LinearAlgebraMore, TriangleRectangularAndDegenerate)
|
||||
{
|
||||
constexpr Vector3<float> v1{0.f,0.f,0.f};
|
||||
constexpr Vector3<float> v2{3.f,0.f,0.f};
|
||||
constexpr Vector3<float> v3{3.f,4.f,0.f}; // 3-4-5 triangle, rectangular at v2
|
||||
|
||||
constexpr Triangle<Vector3<float>> t{v1,v2,v3};
|
||||
|
||||
EXPECT_NEAR(t.side_a_length(), 3.f, 1e-6f);
|
||||
EXPECT_NEAR(t.side_b_length(), 4.f, 1e-6f);
|
||||
EXPECT_NEAR(t.hypot(), 5.f, 1e-6f);
|
||||
EXPECT_TRUE(t.is_rectangular());
|
||||
|
||||
// Degenerate: all points same
|
||||
constexpr Triangle<Vector3<float>> d{v1,v1,v1};
|
||||
EXPECT_NEAR(d.side_a_length(), 0.f, 1e-6f);
|
||||
EXPECT_NEAR(d.side_b_length(), 0.f, 1e-6f);
|
||||
EXPECT_NEAR(d.hypot(), 0.f, 1e-6f);
|
||||
}
|
||||
|
||||
TEST(LinearAlgebraMore, Vector4ClampAndComparisons)
|
||||
{
|
||||
Vector4<float> v{10.f, -20.f, 30.f, -40.f};
|
||||
const auto s = v.sum();
|
||||
EXPECT_NEAR(s, -20.f, 1e-6f);
|
||||
|
||||
v.clamp(-10.f, 10.f);
|
||||
EXPECT_LE(v.x, 10.f);
|
||||
EXPECT_GE(v.x, -10.f);
|
||||
EXPECT_LE(v.y, 10.f);
|
||||
EXPECT_GE(v.y, -10.f);
|
||||
|
||||
constexpr Vector4<float> a{1.f,2.f,3.f,4.f};
|
||||
constexpr Vector4<float> b{2.f,2.f,2.f,2.f};
|
||||
EXPECT_TRUE(a < b || a > b || a == b); // just exercise comparisons
|
||||
}
|
||||
87
tests/general/unit_test_linear_algebra_more2.cpp
Normal file
87
tests/general/unit_test_linear_algebra_more2.cpp
Normal file
@@ -0,0 +1,87 @@
|
||||
// Tests to exercise non-inlined helpers and remaining branches in linear algebra
|
||||
#include "gtest/gtest.h"
|
||||
#include "omath/linear_algebra/vector3.hpp"
|
||||
#include "omath/linear_algebra/vector4.hpp"
|
||||
#include "omath/linear_algebra/mat.hpp"
|
||||
|
||||
using namespace omath;
|
||||
|
||||
TEST(LinearAlgebraMore2, Vector3NonInlinedHelpers)
|
||||
{
|
||||
Vector3<float> v{3.f, 4.f, 0.f};
|
||||
EXPECT_FLOAT_EQ(v.length(), 5.0f);
|
||||
|
||||
auto vn = v.normalized();
|
||||
EXPECT_NEAR(vn.length(), 1.0f, 1e-6f);
|
||||
|
||||
Vector3<float> zero{0.f,0.f,0.f};
|
||||
auto ang = v.angle_between(zero);
|
||||
EXPECT_FALSE(ang.has_value());
|
||||
|
||||
Vector3<float> a{1.f,0.f,0.f};
|
||||
Vector3<float> b{0.f,1.f,0.f};
|
||||
EXPECT_TRUE(a.is_perpendicular(b));
|
||||
EXPECT_FALSE(a.is_perpendicular(a));
|
||||
|
||||
auto tup = v.as_tuple();
|
||||
EXPECT_EQ(std::get<0>(tup), 3.f);
|
||||
EXPECT_EQ(std::get<1>(tup), 4.f);
|
||||
EXPECT_EQ(std::get<2>(tup), 0.f);
|
||||
|
||||
EXPECT_TRUE(a.point_to_same_direction(Vector3<float>{2.f,0.f,0.f}));
|
||||
|
||||
// exercise hash specialization for Vector3<float>
|
||||
std::hash<Vector3<float>> hasher;
|
||||
auto hv = hasher(v);
|
||||
(void)hv;
|
||||
}
|
||||
|
||||
TEST(LinearAlgebraMore2, Vector4NonInlinedHelpers)
|
||||
{
|
||||
Vector4<float> v{1.f,2.f,3.f,4.f};
|
||||
EXPECT_FLOAT_EQ(v.length(), v.length());
|
||||
EXPECT_FLOAT_EQ(v.sum(), v.sum());
|
||||
|
||||
// clamp noinline should modify the vector
|
||||
v.clamp(0.f, 2.5f);
|
||||
EXPECT_GE(v.x, 0.f);
|
||||
EXPECT_LE(v.z, 2.5f);
|
||||
|
||||
constexpr Vector4<float> shorter{0.1f,0.1f,0.1f,0.1f};
|
||||
EXPECT_TRUE(shorter < v);
|
||||
EXPECT_FALSE(v < shorter);
|
||||
}
|
||||
|
||||
TEST(LinearAlgebraMore2, MatNonInlinedAndStringHelpers)
|
||||
{
|
||||
Mat<2,2,float> m{{{4.f,7.f},{2.f,6.f}}};
|
||||
EXPECT_FLOAT_EQ(m.determinant(), 10.0f);
|
||||
|
||||
auto maybe_inv = m.inverted();
|
||||
EXPECT_TRUE(maybe_inv.has_value());
|
||||
const auto& inv = maybe_inv.value();
|
||||
|
||||
// m * inv should be identity (approximately)
|
||||
auto prod = m * inv;
|
||||
EXPECT_NEAR(prod.at(0,0), 1.0f, 1e-5f);
|
||||
EXPECT_NEAR(prod.at(1,1), 1.0f, 1e-5f);
|
||||
EXPECT_NEAR(prod.at(0,1), 0.0f, 1e-5f);
|
||||
|
||||
// transposed and to_string variants
|
||||
auto t = m.transposed();
|
||||
EXPECT_EQ(t.at(0,1), m.at(1,0));
|
||||
|
||||
auto raw = m.raw_array();
|
||||
EXPECT_EQ(raw.size(), static_cast<size_t>(4));
|
||||
|
||||
auto s = m.to_string();
|
||||
EXPECT_NE(s.size(), 0u);
|
||||
auto ws = m.to_wstring();
|
||||
EXPECT_NE(ws.size(), 0u);
|
||||
auto u8_s = m.to_u8string();
|
||||
EXPECT_NE(u8_s.size(), 0u);
|
||||
|
||||
// to_screen_mat static helper
|
||||
auto screen = Mat<4,4,float>::to_screen_mat(800.f, 600.f);
|
||||
EXPECT_NEAR(screen.at(0,0), 800.f/2.f, 1e-6f);
|
||||
}
|
||||
@@ -154,12 +154,12 @@ TEST_F(UnitTestMat, AssignmentOperator_Move)
|
||||
// Test static methods
|
||||
TEST_F(UnitTestMat, StaticMethod_ToScreenMat)
|
||||
{
|
||||
Mat<4, 4> screenMat = Mat<4, 4>::to_screen_mat(800.0f, 600.0f);
|
||||
EXPECT_FLOAT_EQ(screenMat.at(0, 0), 400.0f);
|
||||
EXPECT_FLOAT_EQ(screenMat.at(1, 1), -300.0f);
|
||||
EXPECT_FLOAT_EQ(screenMat.at(3, 0), 400.0f);
|
||||
EXPECT_FLOAT_EQ(screenMat.at(3, 1), 300.0f);
|
||||
EXPECT_FLOAT_EQ(screenMat.at(3, 3), 1.0f);
|
||||
Mat<4, 4> screen_mat = Mat<4, 4>::to_screen_mat(800.0f, 600.0f);
|
||||
EXPECT_FLOAT_EQ(screen_mat.at(0, 0), 400.0f);
|
||||
EXPECT_FLOAT_EQ(screen_mat.at(1, 1), -300.0f);
|
||||
EXPECT_FLOAT_EQ(screen_mat.at(3, 0), 400.0f);
|
||||
EXPECT_FLOAT_EQ(screen_mat.at(3, 1), 300.0f);
|
||||
EXPECT_FLOAT_EQ(screen_mat.at(3, 3), 1.0f);
|
||||
}
|
||||
|
||||
|
||||
@@ -220,8 +220,8 @@ TEST(UnitTestMatStandalone, Equanity)
|
||||
constexpr omath::Vector3<float> left_handed = {0, 2, 10};
|
||||
constexpr omath::Vector3<float> right_handed = {0, 2, -10};
|
||||
|
||||
auto proj_left_handed = omath::mat_perspective_left_handed(90.f, 16.f / 9.f, 0.1, 1000);
|
||||
auto proj_right_handed = omath::mat_perspective_right_handed(90.f, 16.f / 9.f, 0.1, 1000);
|
||||
const auto proj_left_handed = omath::mat_perspective_left_handed(90.f, 16.f / 9.f, 0.1, 1000);
|
||||
const auto proj_right_handed = omath::mat_perspective_right_handed(90.f, 16.f / 9.f, 0.1, 1000);
|
||||
|
||||
auto ndc_left_handed = proj_left_handed * omath::mat_column_from_vector(left_handed);
|
||||
auto ndc_right_handed = proj_right_handed * omath::mat_column_from_vector(right_handed);
|
||||
@@ -231,3 +231,13 @@ TEST(UnitTestMatStandalone, Equanity)
|
||||
|
||||
EXPECT_EQ(ndc_left_handed, ndc_right_handed);
|
||||
}
|
||||
TEST(UnitTestMatStandalone, MatPerspectiveLeftHanded)
|
||||
{
|
||||
const auto perspective_proj = mat_perspective_left_handed(90.f, 16.f/9.f, 0.1f, 1000.f);
|
||||
auto projected = perspective_proj
|
||||
* mat_column_from_vector<float>({0, 0, 0.1001});
|
||||
|
||||
projected /= projected.at(3, 0);
|
||||
|
||||
EXPECT_TRUE(projected.at(2, 0) > -1.0f && projected.at(2, 0) < 0.f);
|
||||
}
|
||||
24
tests/general/unit_test_mat_coverage_extra.cpp
Normal file
24
tests/general/unit_test_mat_coverage_extra.cpp
Normal file
@@ -0,0 +1,24 @@
|
||||
// Added to exercise Mat initializer-list exception branches and determinant fallback
|
||||
#include <gtest/gtest.h>
|
||||
#include <omath/linear_algebra/mat.hpp>
|
||||
|
||||
using namespace omath;
|
||||
|
||||
TEST(MatCoverageExtra, InitListRowsMismatchThrows) {
|
||||
// Rows mismatch: provide 3 rows for a 2x2 Mat
|
||||
EXPECT_THROW((Mat<2,2>{ {1,2}, {3,4}, {5,6} }), std::invalid_argument);
|
||||
}
|
||||
|
||||
TEST(MatCoverageExtra, InitListColumnsMismatchThrows) {
|
||||
// Columns mismatch: second row has wrong number of columns
|
||||
EXPECT_THROW((Mat<2,2>{ {1,2}, {3} }), std::invalid_argument);
|
||||
}
|
||||
|
||||
TEST(MatCoverageExtra, DeterminantFallbackIsCallable) {
|
||||
// Call determinant for 1x1 and 2x2 matrices to cover determinant paths
|
||||
const Mat<1,1> m1{{3.14f}};
|
||||
EXPECT_FLOAT_EQ(m1.determinant(), 3.14f);
|
||||
|
||||
const Mat<2,2> m2{{{1.0f,2.0f},{3.0f,4.0f}}};
|
||||
EXPECT_FLOAT_EQ(m2.determinant(), -2.0f);
|
||||
}
|
||||
21
tests/general/unit_test_mat_more.cpp
Normal file
21
tests/general/unit_test_mat_more.cpp
Normal file
@@ -0,0 +1,21 @@
|
||||
// Unit tests to exercise Mat extra branches
|
||||
#include "gtest/gtest.h"
|
||||
#include "omath/linear_algebra/mat.hpp"
|
||||
|
||||
using omath::Mat;
|
||||
|
||||
TEST(MatMore, InitListAndMultiply)
|
||||
{
|
||||
Mat<3,3,float> m{{{1.f,2.f,3.f}, {0.f,1.f,4.f}, {5.f,6.f,0.f}}};
|
||||
// multiply by scalar and check element
|
||||
auto r = m * 1.f;
|
||||
EXPECT_EQ(r.at(0,0), m.at(0,0));
|
||||
EXPECT_EQ(r.at(1,2), m.at(1,2));
|
||||
}
|
||||
|
||||
TEST(MatMore, Determinant)
|
||||
{
|
||||
const Mat<2,2,double> m{{{1.0,2.0},{2.0,4.0}}}; // singular
|
||||
const double det = m.determinant();
|
||||
EXPECT_DOUBLE_EQ(det, 0.0);
|
||||
}
|
||||
33
tests/general/unit_test_navigation_mesh.cpp
Normal file
33
tests/general/unit_test_navigation_mesh.cpp
Normal file
@@ -0,0 +1,33 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "omath/pathfinding/navigation_mesh.hpp"
|
||||
|
||||
using namespace omath;
|
||||
using namespace omath::pathfinding;
|
||||
|
||||
TEST(NavigationMeshTests, SerializeDeserializeRoundTrip)
|
||||
{
|
||||
NavigationMesh nav;
|
||||
Vector3<float> a{0.f,0.f,0.f};
|
||||
Vector3<float> b{1.f,0.f,0.f};
|
||||
Vector3<float> c{0.f,1.f,0.f};
|
||||
|
||||
nav.m_vertex_map.emplace(a, std::vector<Vector3<float>>{b,c});
|
||||
nav.m_vertex_map.emplace(b, std::vector<Vector3<float>>{a});
|
||||
nav.m_vertex_map.emplace(c, std::vector<Vector3<float>>{a});
|
||||
|
||||
auto data = nav.serialize();
|
||||
NavigationMesh nav2;
|
||||
EXPECT_NO_THROW(nav2.deserialize(data));
|
||||
|
||||
// verify neighbors preserved
|
||||
EXPECT_EQ(nav2.m_vertex_map.size(), nav.m_vertex_map.size());
|
||||
EXPECT_EQ(nav2.get_neighbors(a).size(), 2u);
|
||||
}
|
||||
|
||||
TEST(NavigationMeshTests, GetClosestVertexWhenEmpty)
|
||||
{
|
||||
const NavigationMesh nav;
|
||||
constexpr Vector3<float> p{5.f,5.f,5.f};
|
||||
const auto res = nav.get_closest_vertex(p);
|
||||
EXPECT_FALSE(res.has_value());
|
||||
}
|
||||
31
tests/general/unit_test_pattern_scan_extra.cpp
Normal file
31
tests/general/unit_test_pattern_scan_extra.cpp
Normal file
@@ -0,0 +1,31 @@
|
||||
// Extra tests for PatternScanner behavior
|
||||
#include <gtest/gtest.h>
|
||||
#include <omath/utility/pattern_scan.hpp>
|
||||
|
||||
using namespace omath;
|
||||
|
||||
TEST(unit_test_pattern_scan_extra, IteratorScanFound)
|
||||
{
|
||||
std::vector<std::byte> buf = {static_cast<std::byte>(0xDE), static_cast<std::byte>(0xAD),
|
||||
static_cast<std::byte>(0xBE), static_cast<std::byte>(0xEF),
|
||||
static_cast<std::byte>(0x00)};
|
||||
const auto it = PatternScanner::scan_for_pattern(buf.begin(), buf.end(), "DE AD BE EF");
|
||||
EXPECT_NE(it, buf.end());
|
||||
EXPECT_EQ(std::distance(buf.begin(), it), 0);
|
||||
}
|
||||
|
||||
TEST(unit_test_pattern_scan_extra, IteratorScanNotFound)
|
||||
{
|
||||
std::vector<std::byte> buf = {static_cast<std::byte>(0x00), static_cast<std::byte>(0x11),
|
||||
static_cast<std::byte>(0x22)};
|
||||
const auto it = PatternScanner::scan_for_pattern(buf.begin(), buf.end(), "FF EE DD");
|
||||
EXPECT_EQ(it, buf.end());
|
||||
}
|
||||
|
||||
TEST(unit_test_pattern_scan_extra, ParseInvalidPattern)
|
||||
{
|
||||
// invalid hex token should cause the public scan to return end (no match)
|
||||
std::vector<std::byte> buf = {static_cast<std::byte>(0x00), static_cast<std::byte>(0x11)};
|
||||
const auto it = PatternScanner::scan_for_pattern(buf.begin(), buf.end(), "GG HH");
|
||||
EXPECT_EQ(it, buf.end());
|
||||
}
|
||||
11
tests/general/unit_test_pe_pattern_scan_extra.cpp
Normal file
11
tests/general/unit_test_pe_pattern_scan_extra.cpp
Normal file
@@ -0,0 +1,11 @@
|
||||
// Tests for PePatternScanner basic behavior
|
||||
#include <gtest/gtest.h>
|
||||
#include <omath/utility/pe_pattern_scan.hpp>
|
||||
|
||||
using namespace omath;
|
||||
|
||||
TEST(unit_test_pe_pattern_scan_extra, MissingFileReturnsNull)
|
||||
{
|
||||
const auto res = PePatternScanner::scan_for_pattern_in_file("/non/existent/file.exe", "55 8B EC");
|
||||
EXPECT_FALSE(res.has_value());
|
||||
}
|
||||
114
tests/general/unit_test_pe_pattern_scan_file.cpp
Normal file
114
tests/general/unit_test_pe_pattern_scan_file.cpp
Normal file
@@ -0,0 +1,114 @@
|
||||
// Unit test for PePatternScanner::scan_for_pattern_in_file using a synthetic PE-like file
|
||||
#include <gtest/gtest.h>
|
||||
#include <omath/utility/pe_pattern_scan.hpp>
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
using namespace omath;
|
||||
|
||||
// Helper: write a trivial PE-like file with DOS header and a single section named .text
|
||||
static bool write_minimal_pe_file(const std::string& path, const std::vector<std::uint8_t>& section_bytes)
|
||||
{
|
||||
std::ofstream f(path, std::ios::binary);
|
||||
if (!f.is_open()) return false;
|
||||
|
||||
// Write DOS header (e_magic = 0x5A4D, e_lfanew at offset 0x3C)
|
||||
std::vector<std::uint8_t> dos(64, 0);
|
||||
dos[0] = 'M'; dos[1] = 'Z';
|
||||
// e_lfanew -> place NT headers right after DOS (offset 0x80)
|
||||
std::uint32_t e_lfanew = 0x80;
|
||||
std::memcpy(dos.data() + 0x3C, &e_lfanew, sizeof(e_lfanew));
|
||||
f.write(reinterpret_cast<const char*>(dos.data()), dos.size());
|
||||
|
||||
// Pad up to e_lfanew
|
||||
if (f.tellp() < static_cast<std::streampos>(e_lfanew))
|
||||
{
|
||||
std::vector<char> pad(e_lfanew - static_cast<std::uint32_t>(f.tellp()), 0);
|
||||
f.write(pad.data(), pad.size());
|
||||
}
|
||||
|
||||
// NT headers signature 'PE\0\0'
|
||||
f.put('P'); f.put('E'); f.put('\0'); f.put('\0');
|
||||
|
||||
// FileHeader: machine, num_sections
|
||||
std::uint16_t machine = 0x8664; // x64
|
||||
std::uint16_t num_sections = 1;
|
||||
std::uint32_t dummy32 = 0;
|
||||
std::uint32_t dummy32b = 0;
|
||||
std::uint16_t size_optional = 0xF0; // reasonable
|
||||
std::uint16_t characteristics = 0;
|
||||
f.write(reinterpret_cast<const char*>(&machine), sizeof(machine));
|
||||
f.write(reinterpret_cast<const char*>(&num_sections), sizeof(num_sections));
|
||||
f.write(reinterpret_cast<const char*>(&dummy32), sizeof(dummy32));
|
||||
f.write(reinterpret_cast<const char*>(&dummy32b), sizeof(dummy32b));
|
||||
std::uint32_t num_symbols = 0;
|
||||
f.write(reinterpret_cast<const char*>(&num_symbols), sizeof(num_symbols));
|
||||
f.write(reinterpret_cast<const char*>(&size_optional), sizeof(size_optional));
|
||||
f.write(reinterpret_cast<const char*>(&characteristics), sizeof(characteristics));
|
||||
|
||||
// OptionalHeader (x64) minimal: magic 0x20b, image_base, size_of_code, size_of_headers
|
||||
std::uint16_t magic = 0x20b;
|
||||
f.write(reinterpret_cast<const char*>(&magic), sizeof(magic));
|
||||
// filler for rest of optional header up to size_optional
|
||||
std::vector<std::uint8_t> opt(size_optional - sizeof(magic), 0);
|
||||
// set size_code near end
|
||||
// we'll set image_base and size_code fields in reasonable positions for extractor
|
||||
// For simplicity, leave zeros; extractor primarily uses optional_header.image_base and size_code later,
|
||||
// but we will craft a SectionHeader that points to raw data we append below.
|
||||
f.write(reinterpret_cast<const char*>(opt.data()), opt.size());
|
||||
|
||||
// Section header (name 8 bytes, then remaining 36 bytes)
|
||||
char name[8] = {'.','t','e','x','t',0,0,0};
|
||||
f.write(name, 8);
|
||||
|
||||
// Write placeholder bytes for the rest of the section header and remember its start
|
||||
constexpr std::uint32_t section_header_rest = 36u;
|
||||
const std::streampos header_rest_pos = f.tellp();
|
||||
std::vector<char> placeholder(section_header_rest, 0);
|
||||
f.write(placeholder.data(), placeholder.size());
|
||||
|
||||
// Now write section raw data and remember its file offset
|
||||
const std::streampos data_pos = f.tellp();
|
||||
f.write(reinterpret_cast<const char*>(section_bytes.data()), static_cast<std::streamsize>(section_bytes.size()));
|
||||
|
||||
// Patch section header fields: virtual_size, virtual_address, size_raw_data, ptr_raw_data
|
||||
const std::uint32_t virtual_size = static_cast<std::uint32_t>(section_bytes.size());
|
||||
constexpr std::uint32_t virtual_address = 0x1000u;
|
||||
const std::uint32_t size_raw_data = static_cast<std::uint32_t>(section_bytes.size());
|
||||
const std::uint32_t ptr_raw_data = static_cast<std::uint32_t>(data_pos);
|
||||
|
||||
// Seek back to the header_rest_pos and write fields in order
|
||||
f.seekp(header_rest_pos, std::ios::beg);
|
||||
f.write(reinterpret_cast<const char*>(&virtual_size), sizeof(virtual_size));
|
||||
f.write(reinterpret_cast<const char*>(&virtual_address), sizeof(virtual_address));
|
||||
f.write(reinterpret_cast<const char*>(&size_raw_data), sizeof(size_raw_data));
|
||||
f.write(reinterpret_cast<const char*>(&ptr_raw_data), sizeof(ptr_raw_data));
|
||||
|
||||
// Seek back to end for consistency
|
||||
f.seekp(0, std::ios::end);
|
||||
|
||||
f.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST(unit_test_pe_pattern_scan_file, ScanFindsPattern)
|
||||
{
|
||||
constexpr std::string_view path = "./test_minimal_pe.bin";
|
||||
const std::vector<std::uint8_t> bytes = {0x55, 0x8B, 0xEC, 0x90, 0x90}; // pattern at offset 0
|
||||
ASSERT_TRUE(write_minimal_pe_file(path.data(), bytes));
|
||||
|
||||
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "55 8B EC", ".text");
|
||||
EXPECT_TRUE(res.has_value());
|
||||
}
|
||||
|
||||
TEST(unit_test_pe_pattern_scan_file, ScanMissingPattern)
|
||||
{
|
||||
constexpr std::string_view path = "./test_minimal_pe_2.bin";
|
||||
const std::vector<std::uint8_t> bytes = {0x00, 0x01, 0x02, 0x03};
|
||||
ASSERT_TRUE(write_minimal_pe_file(path.data(), bytes));
|
||||
|
||||
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "FF EE DD", ".text");
|
||||
EXPECT_FALSE(res.has_value());
|
||||
}
|
||||
69
tests/general/unit_test_pe_pattern_scan_loaded.cpp
Normal file
69
tests/general/unit_test_pe_pattern_scan_loaded.cpp
Normal file
@@ -0,0 +1,69 @@
|
||||
// Tests for PePatternScanner::scan_for_pattern_in_loaded_module
|
||||
#include <gtest/gtest.h>
|
||||
#include <omath/utility/pe_pattern_scan.hpp>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
using namespace omath;
|
||||
|
||||
static std::vector<std::uint8_t> make_fake_module(std::uint32_t base_of_code,
|
||||
std::uint32_t size_code,
|
||||
const std::vector<std::uint8_t>& code_bytes)
|
||||
{
|
||||
constexpr std::uint32_t e_lfanew = 0x80;
|
||||
const std::uint32_t total_size = e_lfanew + 0x200 + size_code + 0x100;
|
||||
std::vector<std::uint8_t> buf(total_size, 0);
|
||||
|
||||
// DOS header: e_magic at 0, e_lfanew at offset 0x3C
|
||||
buf[0] = 0x4D; buf[1] = 0x5A; // 'M' 'Z' (little-endian 0x5A4D)
|
||||
constexpr std::uint32_t le = e_lfanew;
|
||||
std::memcpy(buf.data() + 0x3C, &le, sizeof(le));
|
||||
|
||||
// NT signature at e_lfanew
|
||||
constexpr std::uint32_t nt_sig = 0x4550; // 'PE\0\0'
|
||||
std::memcpy(buf.data() + e_lfanew, &nt_sig, sizeof(nt_sig));
|
||||
|
||||
// FileHeader is 20 bytes: we only need to ensure its size is present; leave zeros
|
||||
|
||||
// OptionalHeader magic (optional header begins at e_lfanew + 4 + sizeof(FileHeader) == e_lfanew + 24)
|
||||
constexpr std::uint16_t opt_magic = 0x020B; // x64
|
||||
std::memcpy(buf.data() + e_lfanew + 24, &opt_magic, sizeof(opt_magic));
|
||||
|
||||
// size_code is at offset 4 inside OptionalHeader -> absolute e_lfanew + 28
|
||||
std::memcpy(buf.data() + e_lfanew + 28, &size_code, sizeof(size_code));
|
||||
|
||||
// base_of_code is at offset 20 inside OptionalHeader -> absolute e_lfanew + 44
|
||||
std::memcpy(buf.data() + e_lfanew + 44, &base_of_code, sizeof(base_of_code));
|
||||
|
||||
// place code bytes at offset base_of_code
|
||||
if (base_of_code + code_bytes.size() <= buf.size())
|
||||
std::memcpy(buf.data() + base_of_code, code_bytes.data(), code_bytes.size());
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
TEST(PePatternScanLoaded, FindsPatternAtBase)
|
||||
{
|
||||
const std::vector<std::uint8_t> code = {0x90, 0x01, 0x02, 0x03, 0x04};
|
||||
auto buf = make_fake_module(0x200, static_cast<std::uint32_t>(code.size()), code);
|
||||
|
||||
const auto res = PePatternScanner::scan_for_pattern_in_loaded_module(buf.data(), "90 01 02");
|
||||
ASSERT_TRUE(res.has_value());
|
||||
// address should point somewhere in our buffer; check offset
|
||||
const uintptr_t addr = res.value();
|
||||
const uintptr_t base = reinterpret_cast<uintptr_t>(buf.data());
|
||||
EXPECT_EQ(addr - base, 0x200u);
|
||||
}
|
||||
|
||||
TEST(PePatternScanLoaded, WildcardMatches)
|
||||
{
|
||||
const std::vector<std::uint8_t> code = {0xDE, 0xAD, 0xBE, 0xEF};
|
||||
auto buf = make_fake_module(0x300, static_cast<std::uint32_t>(code.size()), code);
|
||||
|
||||
const auto res = PePatternScanner::scan_for_pattern_in_loaded_module(buf.data(), "DE ?? BE");
|
||||
ASSERT_TRUE(res.has_value());
|
||||
const uintptr_t addr = res.value();
|
||||
const uintptr_t base = reinterpret_cast<uintptr_t>(buf.data());
|
||||
EXPECT_EQ(addr - base, 0x300u);
|
||||
}
|
||||
197
tests/general/unit_test_pe_pattern_scan_more.cpp
Normal file
197
tests/general/unit_test_pe_pattern_scan_more.cpp
Normal file
@@ -0,0 +1,197 @@
|
||||
// Additional tests for PePatternScanner to exercise edge cases and loaded-module scanning
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <gtest/gtest.h>
|
||||
#include <omath/utility/pe_pattern_scan.hpp>
|
||||
#include <vector>
|
||||
|
||||
using namespace omath;
|
||||
|
||||
static bool write_bytes(const std::string& path, const std::vector<std::uint8_t>& data)
|
||||
{
|
||||
std::ofstream f(path, std::ios::binary);
|
||||
if (!f.is_open())
|
||||
return false;
|
||||
f.write(reinterpret_cast<const char*>(data.data()), data.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST(unit_test_pe_pattern_scan_more, InvalidDosHeader)
|
||||
{
|
||||
constexpr std::string_view path = "./test_bad_dos.bin";
|
||||
std::vector<std::uint8_t> data(128, 0);
|
||||
// write wrong magic
|
||||
data[0] = 'N';
|
||||
data[1] = 'Z';
|
||||
ASSERT_TRUE(write_bytes(path.data(), data));
|
||||
|
||||
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "55 8B EC", ".text");
|
||||
EXPECT_FALSE(res.has_value());
|
||||
}
|
||||
|
||||
TEST(unit_test_pe_pattern_scan_more, InvalidNtSignature)
|
||||
{
|
||||
constexpr std::string_view path = "./test_bad_nt.bin";
|
||||
std::vector<std::uint8_t> data(256, 0);
|
||||
// valid DOS header
|
||||
data[0] = 'M';
|
||||
data[1] = 'Z';
|
||||
// point e_lfanew to 0x80
|
||||
constexpr std::uint32_t e_lfanew = 0x80;
|
||||
std::memcpy(data.data() + 0x3C, &e_lfanew, sizeof(e_lfanew));
|
||||
// write garbage at e_lfanew (not 'PE\0\0')
|
||||
data[e_lfanew + 0] = 'X';
|
||||
data[e_lfanew + 1] = 'Y';
|
||||
data[e_lfanew + 2] = 'Z';
|
||||
data[e_lfanew + 3] = 'W';
|
||||
ASSERT_TRUE(write_bytes(path.data(), data));
|
||||
|
||||
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "55 8B EC", ".text");
|
||||
EXPECT_FALSE(res.has_value());
|
||||
}
|
||||
|
||||
TEST(unit_test_pe_pattern_scan_more, SectionNotFound)
|
||||
{
|
||||
// reuse minimal writer but with section named .data and search .text
|
||||
constexpr std::string_view path = "./test_section_not_found.bin";
|
||||
std::ofstream f(path.data(), std::ios::binary);
|
||||
ASSERT_TRUE(f.is_open());
|
||||
// DOS
|
||||
std::vector<std::uint8_t> dos(64, 0);
|
||||
dos[0] = 'M';
|
||||
dos[1] = 'Z';
|
||||
std::uint32_t e_lfanew = 0x80;
|
||||
std::memcpy(dos.data() + 0x3C, &e_lfanew, sizeof(e_lfanew));
|
||||
f.write(reinterpret_cast<char*>(dos.data()), dos.size());
|
||||
// pad
|
||||
std::vector<char> pad(e_lfanew - static_cast<std::uint32_t>(f.tellp()), 0);
|
||||
f.write(pad.data(), pad.size());
|
||||
// NT sig
|
||||
f.put('P');
|
||||
f.put('E');
|
||||
f.put('\0');
|
||||
f.put('\0');
|
||||
// FileHeader minimal
|
||||
std::uint16_t machine = 0x8664;
|
||||
std::uint16_t num_sections = 1;
|
||||
std::uint32_t z = 0;
|
||||
std::uint32_t z2 = 0;
|
||||
std::uint32_t numsym = 0;
|
||||
std::uint16_t size_opt = 0xF0;
|
||||
std::uint16_t ch = 0;
|
||||
f.write(reinterpret_cast<char*>(&machine), sizeof(machine));
|
||||
f.write(reinterpret_cast<char*>(&num_sections), sizeof(num_sections));
|
||||
f.write(reinterpret_cast<char*>(&z), sizeof(z));
|
||||
f.write(reinterpret_cast<char*>(&z2), sizeof(z2));
|
||||
f.write(reinterpret_cast<char*>(&numsym), sizeof(numsym));
|
||||
f.write(reinterpret_cast<char*>(&size_opt), sizeof(size_opt));
|
||||
f.write(reinterpret_cast<char*>(&ch), sizeof(ch));
|
||||
// Optional header magic
|
||||
std::uint16_t magic = 0x20b;
|
||||
f.write(reinterpret_cast<char*>(&magic), sizeof(magic));
|
||||
std::vector<std::uint8_t> opt(size_opt - sizeof(magic), 0);
|
||||
f.write(reinterpret_cast<char*>(opt.data()), opt.size());
|
||||
// Section header named .data
|
||||
char name[8] = {'.', 'd', 'a', 't', 'a', 0, 0, 0};
|
||||
f.write(name, 8);
|
||||
std::uint32_t vs = 4, va = 0x1000, srd = 4, prd = 0x200;
|
||||
f.write(reinterpret_cast<char*>(&vs), 4);
|
||||
f.write(reinterpret_cast<char*>(&va), 4);
|
||||
f.write(reinterpret_cast<char*>(&srd), 4);
|
||||
f.write(reinterpret_cast<char*>(&prd), 4);
|
||||
std::vector<char> rest(16, 0);
|
||||
f.write(rest.data(), rest.size());
|
||||
// section bytes
|
||||
std::vector<std::uint8_t> sec = {0x00, 0x01, 0x02, 0x03};
|
||||
f.write(reinterpret_cast<char*>(sec.data()), sec.size());
|
||||
f.close();
|
||||
|
||||
auto res = PePatternScanner::scan_for_pattern_in_file(path, "00 01", ".text");
|
||||
EXPECT_FALSE(res.has_value());
|
||||
}
|
||||
|
||||
TEST(unit_test_pe_pattern_scan_more, LoadedModuleScanFinds)
|
||||
{
|
||||
// Create an in-memory buffer that mimics loaded module layout
|
||||
// Define local header structs matching those in source
|
||||
struct DosHeader
|
||||
{
|
||||
std::uint16_t e_magic;
|
||||
std::uint16_t e_cblp;
|
||||
std::uint16_t e_cp;
|
||||
std::uint16_t e_crlc;
|
||||
std::uint16_t e_cparhdr;
|
||||
std::uint16_t e_minalloc;
|
||||
std::uint16_t e_maxalloc;
|
||||
std::uint16_t e_ss;
|
||||
std::uint16_t e_sp;
|
||||
std::uint16_t e_csum;
|
||||
std::uint16_t e_ip;
|
||||
std::uint16_t e_cs;
|
||||
std::uint16_t e_lfarlc;
|
||||
std::uint16_t e_ovno;
|
||||
std::uint16_t e_res[4];
|
||||
std::uint16_t e_oemid;
|
||||
std::uint16_t e_oeminfo;
|
||||
std::uint16_t e_res2[10];
|
||||
std::uint32_t e_lfanew;
|
||||
};
|
||||
struct FileHeader
|
||||
{
|
||||
std::uint16_t machine;
|
||||
std::uint16_t num_sections;
|
||||
std::uint32_t timedate_stamp;
|
||||
std::uint32_t ptr_symbols;
|
||||
std::uint32_t num_symbols;
|
||||
std::uint16_t size_optional_header;
|
||||
std::uint16_t characteristics;
|
||||
};
|
||||
struct OptionalHeaderX64
|
||||
{
|
||||
std::uint16_t magic;
|
||||
std::uint16_t linker_version;
|
||||
std::uint32_t size_code;
|
||||
std::uint32_t size_init_data;
|
||||
std::uint32_t size_uninit_data;
|
||||
std::uint32_t entry_point;
|
||||
std::uint32_t base_of_code;
|
||||
std::uint64_t image_base;
|
||||
std::uint32_t section_alignment;
|
||||
std::uint32_t file_alignment; /* rest omitted */
|
||||
std::uint32_t size_image;
|
||||
std::uint32_t size_headers; /* keep space */
|
||||
std::uint8_t pad[200];
|
||||
};
|
||||
struct ImageNtHeadersX64
|
||||
{
|
||||
std::uint32_t signature;
|
||||
FileHeader file_header;
|
||||
OptionalHeaderX64 optional_header;
|
||||
};
|
||||
|
||||
const std::vector<std::uint8_t> pattern_bytes = {0xDE, 0xAD, 0xBE, 0xEF, 0x90};
|
||||
constexpr std::uint32_t base_of_code = 0x200; // will place bytes at offset 0x200
|
||||
const std::uint32_t size_code = static_cast<std::uint32_t>(pattern_bytes.size());
|
||||
|
||||
const std::uint32_t bufsize = 0x400 + size_code;
|
||||
std::vector<std::uint8_t> buf(bufsize, 0);
|
||||
// DOS header
|
||||
const auto dos = reinterpret_cast<DosHeader*>(buf.data());
|
||||
dos->e_magic = 0x5A4D;
|
||||
dos->e_lfanew = 0x80;
|
||||
// NT headers
|
||||
const auto nt = reinterpret_cast<ImageNtHeadersX64*>(buf.data() + dos->e_lfanew);
|
||||
nt->signature = 0x4550; // 'PE\0\0'
|
||||
nt->file_header.machine = 0x8664;
|
||||
nt->file_header.num_sections = 1;
|
||||
nt->optional_header.magic = 0x020B; // x64
|
||||
nt->optional_header.base_of_code = base_of_code;
|
||||
nt->optional_header.size_code = size_code;
|
||||
|
||||
// place code at base_of_code
|
||||
std::memcpy(buf.data() + base_of_code, pattern_bytes.data(), pattern_bytes.size());
|
||||
|
||||
const auto res = PePatternScanner::scan_for_pattern_in_loaded_module(buf.data(), "DE AD BE EF");
|
||||
EXPECT_TRUE(res.has_value());
|
||||
}
|
||||
294
tests/general/unit_test_pe_pattern_scan_more2.cpp
Normal file
294
tests/general/unit_test_pe_pattern_scan_more2.cpp
Normal file
@@ -0,0 +1,294 @@
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <gtest/gtest.h>
|
||||
#include <omath/utility/pe_pattern_scan.hpp>
|
||||
#include <vector>
|
||||
|
||||
using namespace omath;
|
||||
|
||||
// Local minimal FileHeader used by tests when constructing raw NT headers
|
||||
struct TestFileHeader
|
||||
{
|
||||
std::uint16_t machine;
|
||||
std::uint16_t num_sections;
|
||||
std::uint32_t timedate_stamp;
|
||||
std::uint32_t ptr_symbols;
|
||||
std::uint32_t num_symbols;
|
||||
std::uint16_t size_optional_header;
|
||||
std::uint16_t characteristics;
|
||||
};
|
||||
|
||||
static bool write_bytes(const std::string& path, const std::vector<std::uint8_t>& data)
|
||||
{
|
||||
std::ofstream f(path, std::ios::binary);
|
||||
if (!f.is_open())
|
||||
return false;
|
||||
f.write(reinterpret_cast<const char*>(data.data()), data.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
// Helper: write a trivial PE-like file with DOS header and a single section named .text
|
||||
static bool write_minimal_pe_file(const std::string& path, const std::vector<std::uint8_t>& section_bytes)
|
||||
{
|
||||
std::ofstream f(path, std::ios::binary);
|
||||
if (!f.is_open())
|
||||
return false;
|
||||
|
||||
// Write DOS header (e_magic = 0x5A4D, e_lfanew at offset 0x3C)
|
||||
std::vector<std::uint8_t> dos(64, 0);
|
||||
dos[0] = 'M';
|
||||
dos[1] = 'Z';
|
||||
std::uint32_t e_lfanew = 0x80;
|
||||
std::memcpy(dos.data() + 0x3C, &e_lfanew, sizeof(e_lfanew));
|
||||
f.write(reinterpret_cast<const char*>(dos.data()), dos.size());
|
||||
|
||||
// Pad up to e_lfanew
|
||||
if (f.tellp() < static_cast<std::streampos>(e_lfanew))
|
||||
{
|
||||
std::vector<char> pad(e_lfanew - static_cast<std::uint32_t>(f.tellp()), 0);
|
||||
f.write(pad.data(), pad.size());
|
||||
}
|
||||
|
||||
// NT headers signature 'PE\0\0'
|
||||
f.put('P');
|
||||
f.put('E');
|
||||
f.put('\0');
|
||||
f.put('\0');
|
||||
|
||||
// FileHeader minimal
|
||||
std::uint16_t machine = 0x8664; // x64
|
||||
std::uint16_t num_sections = 1;
|
||||
std::uint32_t dummy32 = 0;
|
||||
std::uint32_t dummy32b = 0;
|
||||
std::uint16_t size_optional = 0xF0;
|
||||
std::uint16_t characteristics = 0;
|
||||
f.write(reinterpret_cast<const char*>(&machine), sizeof(machine));
|
||||
f.write(reinterpret_cast<const char*>(&num_sections), sizeof(num_sections));
|
||||
f.write(reinterpret_cast<const char*>(&dummy32), sizeof(dummy32));
|
||||
f.write(reinterpret_cast<const char*>(&dummy32b), sizeof(dummy32b));
|
||||
std::uint32_t num_symbols = 0;
|
||||
f.write(reinterpret_cast<const char*>(&num_symbols), sizeof(num_symbols));
|
||||
f.write(reinterpret_cast<const char*>(&size_optional), sizeof(size_optional));
|
||||
f.write(reinterpret_cast<const char*>(&characteristics), sizeof(characteristics));
|
||||
|
||||
// OptionalHeader minimal filler
|
||||
std::uint16_t magic = 0x20b;
|
||||
f.write(reinterpret_cast<const char*>(&magic), sizeof(magic));
|
||||
std::vector<std::uint8_t> opt(size_optional - sizeof(magic), 0);
|
||||
f.write(reinterpret_cast<const char*>(opt.data()), opt.size());
|
||||
|
||||
// Section header (name 8 bytes, then remaining 36 bytes)
|
||||
char name[8] = {'.', 't', 'e', 'x', 't', 0, 0, 0};
|
||||
f.write(name, 8);
|
||||
|
||||
constexpr std::uint32_t section_header_rest = 36u;
|
||||
const std::streampos header_rest_pos = f.tellp();
|
||||
std::vector<char> placeholder(section_header_rest, 0);
|
||||
f.write(placeholder.data(), placeholder.size());
|
||||
|
||||
// Now write section raw data and remember its file offset
|
||||
const std::streampos data_pos = f.tellp();
|
||||
f.write(reinterpret_cast<const char*>(section_bytes.data()), static_cast<std::streamsize>(section_bytes.size()));
|
||||
|
||||
// Patch section header fields
|
||||
const std::uint32_t virtual_size = static_cast<std::uint32_t>(section_bytes.size());
|
||||
constexpr std::uint32_t virtual_address = 0x1000u;
|
||||
const std::uint32_t size_raw_data = static_cast<std::uint32_t>(section_bytes.size());
|
||||
const std::uint32_t ptr_raw_data = static_cast<std::uint32_t>(data_pos);
|
||||
|
||||
f.seekp(header_rest_pos, std::ios::beg);
|
||||
f.write(reinterpret_cast<const char*>(&virtual_size), sizeof(virtual_size));
|
||||
f.write(reinterpret_cast<const char*>(&virtual_address), sizeof(virtual_address));
|
||||
f.write(reinterpret_cast<const char*>(&size_raw_data), sizeof(size_raw_data));
|
||||
f.write(reinterpret_cast<const char*>(&ptr_raw_data), sizeof(ptr_raw_data));
|
||||
f.seekp(0, std::ios::end);
|
||||
|
||||
f.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST(unit_test_pe_pattern_scan_more2, LoadedModuleNullBaseReturnsNull)
|
||||
{
|
||||
const auto res = PePatternScanner::scan_for_pattern_in_loaded_module(nullptr, "DE AD");
|
||||
EXPECT_FALSE(res.has_value());
|
||||
}
|
||||
|
||||
TEST(unit_test_pe_pattern_scan_more2, LoadedModuleInvalidOptionalHeaderReturnsNull)
|
||||
{
|
||||
// Construct in-memory buffer with DOS header but invalid optional header magic
|
||||
std::vector<std::uint8_t> buf(0x200, 0);
|
||||
struct DosHeader
|
||||
{
|
||||
std::uint16_t e_magic;
|
||||
std::uint8_t pad[0x3A];
|
||||
std::uint32_t e_lfanew;
|
||||
};
|
||||
const auto dos = reinterpret_cast<DosHeader*>(buf.data());
|
||||
dos->e_magic = 0x5A4D;
|
||||
dos->e_lfanew = 0x80;
|
||||
|
||||
// Place an NT header with wrong optional magic at e_lfanew
|
||||
const auto nt_ptr = buf.data() + dos->e_lfanew;
|
||||
// write signature
|
||||
nt_ptr[0] = 'P';
|
||||
nt_ptr[1] = 'E';
|
||||
nt_ptr[2] = 0;
|
||||
nt_ptr[3] = 0;
|
||||
// craft FileHeader with size_optional_header large enough
|
||||
constexpr std::uint16_t size_opt = 0xE0;
|
||||
// file header starts at offset 4
|
||||
std::memcpy(nt_ptr + 4 + 12, &size_opt,
|
||||
sizeof(size_opt)); // size_optional_header located after 12 bytes into FileHeader
|
||||
// write optional header magic to be invalid value
|
||||
constexpr std::uint16_t bad_magic = 0x9999;
|
||||
std::memcpy(nt_ptr + 4 + sizeof(std::uint32_t) + sizeof(std::uint16_t) + sizeof(std::uint16_t), &bad_magic,
|
||||
sizeof(bad_magic));
|
||||
|
||||
const auto res = PePatternScanner::scan_for_pattern_in_loaded_module(buf.data(), "DE AD");
|
||||
EXPECT_FALSE(res.has_value());
|
||||
}
|
||||
|
||||
TEST(unit_test_pe_pattern_scan_more2, FileX86OptionalHeaderScanFindsPattern)
|
||||
{
|
||||
constexpr std::string_view path = "./test_pe_x86.bin";
|
||||
const std::vector<std::uint8_t> pattern = {0xDE, 0xAD, 0xBE, 0xEF};
|
||||
|
||||
// Use helper from this file to write a consistent minimal PE file with .text section
|
||||
ASSERT_TRUE(write_minimal_pe_file(path.data(), pattern));
|
||||
|
||||
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "DE AD BE EF", ".text");
|
||||
ASSERT_TRUE(res.has_value());
|
||||
EXPECT_GE(res->virtual_base_addr, 0u);
|
||||
EXPECT_GE(res->raw_base_addr, 0u);
|
||||
EXPECT_EQ(res->target_offset, 0);
|
||||
}
|
||||
|
||||
TEST(unit_test_pe_pattern_scan_more2, FilePatternNotFoundReturnsNull)
|
||||
{
|
||||
const std::string path = "./test_pe_no_pattern.bin";
|
||||
std::vector<std::uint8_t> data(512, 0);
|
||||
// minimal DOS/NT headers to make extract_section fail earlier or return empty data
|
||||
data[0] = 'M';
|
||||
data[1] = 'Z';
|
||||
constexpr std::uint32_t e_lfanew = 0x80;
|
||||
std::memcpy(data.data() + 0x3C, &e_lfanew, sizeof(e_lfanew));
|
||||
// NT signature
|
||||
data[e_lfanew + 0] = 'P';
|
||||
data[e_lfanew + 1] = 'E';
|
||||
data[e_lfanew + 2] = 0;
|
||||
data[e_lfanew + 3] = 0;
|
||||
// FileHeader: one section, size_optional_header set low
|
||||
constexpr std::uint16_t num_sections = 1;
|
||||
constexpr std::uint16_t size_optional_header = 0xE0;
|
||||
std::memcpy(data.data() + e_lfanew + 6, &num_sections, sizeof(num_sections));
|
||||
std::memcpy(data.data() + e_lfanew + 4 + 12, &size_optional_header, sizeof(size_optional_header));
|
||||
// Optional header magic x64
|
||||
constexpr std::uint16_t magic = 0x020B;
|
||||
std::memcpy(data.data() + e_lfanew + 4 + sizeof(TestFileHeader), &magic, sizeof(magic));
|
||||
// Section header .text with small data that does not contain the pattern
|
||||
constexpr std::size_t offset_to_segment_table = e_lfanew + 4 + sizeof(TestFileHeader) + size_optional_header;
|
||||
constexpr char name[8] = {'.', 't', 'e', 'x', 't', 0, 0, 0};
|
||||
std::memcpy(data.data() + offset_to_segment_table, name, 8);
|
||||
std::uint32_t vs = 4, va = 0x1000, srd = 4, prd = 0x200;
|
||||
std::memcpy(data.data() + offset_to_segment_table + 8, &vs, 4);
|
||||
std::memcpy(data.data() + offset_to_segment_table + 12, &va, 4);
|
||||
std::memcpy(data.data() + offset_to_segment_table + 16, &srd, 4);
|
||||
std::memcpy(data.data() + offset_to_segment_table + 20, &prd, 4);
|
||||
// write file
|
||||
ASSERT_TRUE(write_bytes(path, data));
|
||||
|
||||
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "AA BB CC", ".text");
|
||||
EXPECT_FALSE(res.has_value());
|
||||
}
|
||||
// Extra tests for pe_pattern_scan edge cases (on-disk API)
|
||||
|
||||
TEST(PePatternScanMore2, PatternAtStartFound)
|
||||
{
|
||||
const std::string path = "./test_pe_more_start.bin";
|
||||
const std::vector<std::uint8_t> bytes = {0x90, 0x01, 0x02, 0x03, 0x04};
|
||||
ASSERT_TRUE(write_minimal_pe_file(path, bytes));
|
||||
|
||||
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "90 01 02", ".text");
|
||||
EXPECT_TRUE(res.has_value());
|
||||
}
|
||||
|
||||
TEST(PePatternScanMore2, PatternAtEndFound)
|
||||
{
|
||||
const std::string path = "./test_pe_more_end.bin";
|
||||
std::vector<std::uint8_t> bytes = {0x00, 0x11, 0x22, 0x33, 0x44};
|
||||
ASSERT_TRUE(write_minimal_pe_file(path, bytes));
|
||||
|
||||
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "22 33 44", ".text");
|
||||
if (!res.has_value())
|
||||
{
|
||||
// Try to locate the section header and print the raw section bytes the scanner would read
|
||||
std::ifstream in(path, std::ios::binary);
|
||||
ASSERT_TRUE(in.is_open());
|
||||
// search for ".text" name
|
||||
in.seekg(0, std::ios::beg);
|
||||
std::vector<char> filebuf((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
|
||||
const auto it = std::search(filebuf.begin(), filebuf.end(), std::begin(".text"), std::end(".text") - 1);
|
||||
if (it != filebuf.end())
|
||||
{
|
||||
const size_t pos = std::distance(filebuf.begin(), it);
|
||||
// after name, next fields: virtual_size (4), virtual_address(4), size_raw_data(4), ptr_raw_data(4)
|
||||
const size_t meta_off = pos + 8;
|
||||
uint32_t virtual_size{};
|
||||
uint32_t virtual_address{};
|
||||
uint32_t size_raw_data{};
|
||||
uint32_t ptr_raw_data{};
|
||||
std::memcpy(&virtual_size, filebuf.data() + meta_off, sizeof(virtual_size));
|
||||
std::memcpy(&virtual_address, filebuf.data() + meta_off + 4, sizeof(virtual_address));
|
||||
std::memcpy(&size_raw_data, filebuf.data() + meta_off + 8, sizeof(size_raw_data));
|
||||
std::memcpy(&ptr_raw_data, filebuf.data() + meta_off + 12, sizeof(ptr_raw_data));
|
||||
|
||||
std::cerr << "Parsed section header: virtual_size=" << virtual_size << " virtual_address=0x" << std::hex
|
||||
<< virtual_address << std::dec << " size_raw_data=" << size_raw_data
|
||||
<< " ptr_raw_data=" << ptr_raw_data << "\n";
|
||||
|
||||
if (ptr_raw_data + size_raw_data <= filebuf.size())
|
||||
{
|
||||
std::cerr << "Extracted section bytes:\n";
|
||||
for (size_t i = 0; i < size_raw_data; i += 16)
|
||||
{
|
||||
std::fprintf(stderr, "%04zx: ", i);
|
||||
for (size_t j = 0; j < 16 && i + j < size_raw_data; ++j)
|
||||
std::fprintf(stderr, "%02x ", static_cast<uint8_t>(filebuf[ptr_raw_data + i + j]));
|
||||
std::fprintf(stderr, "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(res.has_value());
|
||||
}
|
||||
|
||||
TEST(PePatternScanMore2, WildcardMatches)
|
||||
{
|
||||
const std::string path = "./test_pe_more_wild.bin";
|
||||
const std::vector<std::uint8_t> bytes = {0xDE, 0xAD, 0xBE, 0xEF};
|
||||
ASSERT_TRUE(write_minimal_pe_file(path, bytes));
|
||||
|
||||
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "DE ?? BE", ".text");
|
||||
EXPECT_TRUE(res.has_value());
|
||||
}
|
||||
|
||||
TEST(PePatternScanMore2, PatternLongerThanBuffer)
|
||||
{
|
||||
const std::string path = "./test_pe_more_small.bin";
|
||||
const std::vector<std::uint8_t> bytes = {0xAA, 0xBB};
|
||||
ASSERT_TRUE(write_minimal_pe_file(path, bytes));
|
||||
|
||||
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "AA BB CC", ".text");
|
||||
EXPECT_FALSE(res.has_value());
|
||||
}
|
||||
|
||||
TEST(PePatternScanMore2, InvalidPatternParse)
|
||||
{
|
||||
const std::string path = "./test_pe_more_invalid.bin";
|
||||
const std::vector<std::uint8_t> bytes = {0x01, 0x02, 0x03};
|
||||
ASSERT_TRUE(write_minimal_pe_file(path, bytes));
|
||||
|
||||
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "01 GG 03", ".text");
|
||||
EXPECT_FALSE(res.has_value());
|
||||
}
|
||||
66
tests/general/unit_test_pred_engine_trait.cpp
Normal file
66
tests/general/unit_test_pred_engine_trait.cpp
Normal file
@@ -0,0 +1,66 @@
|
||||
// Tests for PredEngineTrait
|
||||
#include <gtest/gtest.h>
|
||||
#include <omath/engines/source_engine/traits/pred_engine_trait.hpp>
|
||||
#include <omath/projectile_prediction/projectile.hpp>
|
||||
#include <omath/projectile_prediction/target.hpp>
|
||||
|
||||
using namespace omath;
|
||||
using namespace omath::source_engine;
|
||||
|
||||
TEST(PredEngineTrait, PredictProjectilePositionBasic)
|
||||
{
|
||||
projectile_prediction::Projectile p;
|
||||
p.m_origin = {0.f, 0.f, 0.f};
|
||||
p.m_launch_speed = 10.f;
|
||||
p.m_gravity_scale = 1.f;
|
||||
|
||||
const auto pos = PredEngineTrait::predict_projectile_position(p, /*pitch*/ 0.f, /*yaw*/ 0.f, /*time*/ 1.f,
|
||||
/*gravity*/ 9.81f);
|
||||
// With zero pitch and yaw forward vector is along X; expect x ~10, z reduced by gravity*0.5
|
||||
EXPECT_NEAR(pos.x, 10.f, 1e-3f);
|
||||
EXPECT_NEAR(pos.z, -9.81f * 0.5f, 1e-3f);
|
||||
}
|
||||
|
||||
TEST(PredEngineTrait, PredictTargetPositionAirborne)
|
||||
{
|
||||
projectile_prediction::Target t;
|
||||
t.m_origin = {0.f, 0.f, 10.f};
|
||||
t.m_velocity = {1.f, 0.f, 0.f};
|
||||
t.m_is_airborne = true;
|
||||
|
||||
const auto pred = PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
|
||||
EXPECT_NEAR(pred.x, 2.f, 1e-6f);
|
||||
// z should have been reduced by gravity* t^2
|
||||
EXPECT_NEAR(pred.z, 10.f - 9.81f * 4.f * 0.5f, 1e-6f);
|
||||
}
|
||||
|
||||
TEST(PredEngineTrait, CalcVector2dDistance)
|
||||
{
|
||||
constexpr Vector3<float> d{3.f, 4.f, 0.f};
|
||||
EXPECT_NEAR(PredEngineTrait::calc_vector_2d_distance(d), 5.f, 1e-6f);
|
||||
}
|
||||
|
||||
TEST(PredEngineTrait, CalcViewpointFromAngles)
|
||||
{
|
||||
projectile_prediction::Projectile p;
|
||||
p.m_origin = {0.f, 0.f, 0.f};
|
||||
p.m_launch_speed = 10.f;
|
||||
|
||||
constexpr Vector3<float> predicted{10.f, 0.f, 0.f};
|
||||
constexpr std::optional<float> pitch = 45.f;
|
||||
const auto vp = PredEngineTrait::calc_viewpoint_from_angles(p, predicted, pitch);
|
||||
// For 45 degrees, height = delta2d * tan(45deg) = 10 * 1 = 10
|
||||
EXPECT_NEAR(vp.z, 10.f, 1e-6f);
|
||||
}
|
||||
|
||||
TEST(PredEngineTrait, DirectAngles)
|
||||
{
|
||||
constexpr Vector3<float> origin{0.f, 0.f, 0.f};
|
||||
constexpr Vector3<float> target{0.f, 1.f, 1.f};
|
||||
// yaw should be 90 degrees (pointing along y)
|
||||
EXPECT_NEAR(PredEngineTrait::calc_direct_yaw_angle(origin, target), 90.f, 1e-3f);
|
||||
// pitch should be asin(z/distance)
|
||||
const float dist = origin.distance_to(target);
|
||||
EXPECT_NEAR(PredEngineTrait::calc_direct_pitch_angle(origin, target),
|
||||
angles::radians_to_degrees(std::asin((target.z - origin.z) / dist)), 1e-3f);
|
||||
}
|
||||
100
tests/general/unit_test_proj_pred_engine_legacy_more.cpp
Normal file
100
tests/general/unit_test_proj_pred_engine_legacy_more.cpp
Normal file
@@ -0,0 +1,100 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <omath/projectile_prediction/proj_pred_engine_legacy.hpp>
|
||||
#include <omath/projectile_prediction/projectile.hpp>
|
||||
#include <omath/projectile_prediction/target.hpp>
|
||||
#include <omath/linear_algebra/vector3.hpp>
|
||||
|
||||
using omath::projectile_prediction::Projectile;
|
||||
using omath::projectile_prediction::Target;
|
||||
using omath::Vector3;
|
||||
|
||||
// Fake engine trait where gravity is effectively zero and projectile prediction always hits the target
|
||||
struct FakeEngineZeroGravity
|
||||
{
|
||||
static Vector3<float> predict_target_position(const Target& t, float /*time*/, float /*gravity*/) noexcept
|
||||
{
|
||||
return t.m_origin;
|
||||
}
|
||||
static Vector3<float> predict_projectile_position(const Projectile& /*p*/, float /*pitch*/, float /*yaw*/, float /*time*/, float /*gravity*/) noexcept
|
||||
{
|
||||
// Return a fixed point matching typical target used in the test
|
||||
return Vector3<float>{100.f, 0.f, 0.f};
|
||||
}
|
||||
static float calc_vector_2d_distance(const Vector3<float>& v) noexcept { return std::hypot(v.x, v.y); }
|
||||
static float get_vector_height_coordinate(const Vector3<float>& v) noexcept { return v.z; }
|
||||
static Vector3<float> calc_viewpoint_from_angles(const Projectile& /*p*/, Vector3<float> /*v*/, std::optional<float> /*maybe_pitch*/) noexcept
|
||||
{
|
||||
return Vector3<float>{1.f, 2.f, 3.f};
|
||||
}
|
||||
static float calc_direct_pitch_angle(const Vector3<float>& /*a*/, const Vector3<float>& /*b*/) noexcept { return 12.5f; }
|
||||
static float calc_direct_yaw_angle(const Vector3<float>& /*a*/, const Vector3<float>& /*b*/) noexcept { return 0.f; }
|
||||
};
|
||||
|
||||
TEST(ProjPredLegacyMore, ZeroGravityUsesDirectPitchAndReturnsViewpoint)
|
||||
{
|
||||
constexpr Projectile proj{ .m_origin = {0.f, 0.f, 0.f}, .m_launch_speed = 10.f, .m_gravity_scale = 0.f };
|
||||
constexpr Target target{ .m_origin = {100.f, 0.f, 0.f}, .m_velocity = {0.f,0.f,0.f}, .m_is_airborne = false };
|
||||
|
||||
using Engine = omath::projectile_prediction::ProjPredEngineLegacy<FakeEngineZeroGravity>;
|
||||
const Engine engine(9.8f, 0.1f, 5.f, 1e-3f);
|
||||
|
||||
const auto res = engine.maybe_calculate_aim_point(proj, target);
|
||||
ASSERT_TRUE(res.has_value());
|
||||
const auto v = res.value();
|
||||
EXPECT_NEAR(v.x, 1.f, 1e-6f);
|
||||
EXPECT_NEAR(v.y, 2.f, 1e-6f);
|
||||
EXPECT_NEAR(v.z, 3.f, 1e-6f);
|
||||
}
|
||||
|
||||
// Fake trait producing no valid launch angle (root < 0)
|
||||
struct FakeEngineNoSolution
|
||||
{
|
||||
static Vector3<float> predict_target_position(const Target& t, float /*time*/, float /*gravity*/) noexcept { return t.m_origin; }
|
||||
static Vector3<float> predict_projectile_position(const Projectile& /*p*/, float /*pitch*/, float /*yaw*/, float /*time*/, float /*gravity*/) noexcept { return Vector3<float>{0.f,0.f,0.f}; }
|
||||
static float calc_vector_2d_distance(const Vector3<float>& /*v*/) noexcept { return 10000.f; }
|
||||
static float get_vector_height_coordinate(const Vector3<float>& /*v*/) noexcept { return 0.f; }
|
||||
static Vector3<float> calc_viewpoint_from_angles(const Projectile& /*p*/, Vector3<float> /*v*/, std::optional<float> /*maybe_pitch*/) noexcept { return Vector3<float>{}; }
|
||||
static float calc_direct_pitch_angle(const Vector3<float>& /*a*/, const Vector3<float>& /*b*/) noexcept { return 0.f; }
|
||||
static float calc_direct_yaw_angle(const Vector3<float>& /*a*/, const Vector3<float>& /*b*/) noexcept { return 0.f; }
|
||||
};
|
||||
|
||||
TEST(ProjPredLegacyMore, NoSolutionRootReturnsNullopt)
|
||||
{
|
||||
// Very slow projectile and large distance -> quadratic root negative
|
||||
constexpr Projectile proj{ .m_origin = {0.f,0.f,0.f}, .m_launch_speed = 1.f, .m_gravity_scale = 1.f };
|
||||
constexpr Target target{ .m_origin = {10000.f, 0.f, 0.f}, .m_velocity = {0.f,0.f,0.f}, .m_is_airborne = false };
|
||||
|
||||
using Engine = omath::projectile_prediction::ProjPredEngineLegacy<FakeEngineNoSolution>;
|
||||
const Engine engine(9.8f, 0.5f, 2.f, 1.f);
|
||||
|
||||
const auto res = engine.maybe_calculate_aim_point(proj, target);
|
||||
EXPECT_FALSE(res.has_value());
|
||||
}
|
||||
|
||||
// Fake trait where an angle exists but the projectile does not reach target (miss)
|
||||
struct FakeEngineAngleButMiss
|
||||
{
|
||||
static Vector3<float> predict_target_position(const Target& t, float /*time*/, float /*gravity*/) noexcept { return t.m_origin; }
|
||||
static Vector3<float> predict_projectile_position(const Projectile& /*p*/, float /*pitch*/, float /*yaw*/, float /*time*/, float /*gravity*/) noexcept
|
||||
{
|
||||
// always return a point far from the target
|
||||
return Vector3<float>{0.f, 0.f, 1000.f};
|
||||
}
|
||||
static float calc_vector_2d_distance(const Vector3<float>& v) noexcept { return std::hypot(v.x, v.y); }
|
||||
static float get_vector_height_coordinate(const Vector3<float>& v) noexcept { return v.z; }
|
||||
static Vector3<float> calc_viewpoint_from_angles(const Projectile& /*p*/, Vector3<float> /*v*/, std::optional<float> /*maybe_pitch*/) noexcept { return Vector3<float>{9.f,9.f,9.f}; }
|
||||
static float calc_direct_pitch_angle(const Vector3<float>& /*a*/, const Vector3<float>& /*b*/) noexcept { return 1.f; }
|
||||
static float calc_direct_yaw_angle(const Vector3<float>& /*a*/, const Vector3<float>& /*b*/) noexcept { return 0.f; }
|
||||
};
|
||||
|
||||
TEST(ProjPredLegacyMore, AngleComputedButMissReturnsNullopt)
|
||||
{
|
||||
constexpr Projectile proj{ .m_origin = {0.f,0.f,0.f}, .m_launch_speed = 100.f, .m_gravity_scale = 1.f };
|
||||
constexpr Target target{ .m_origin = {10.f, 0.f, 0.f}, .m_velocity = {0.f,0.f,0.f}, .m_is_airborne = false };
|
||||
|
||||
using Engine = omath::projectile_prediction::ProjPredEngineLegacy<FakeEngineAngleButMiss>;
|
||||
const Engine engine(9.8f, 0.1f, 1.f, 0.1f);
|
||||
|
||||
const auto res = engine.maybe_calculate_aim_point(proj, target);
|
||||
EXPECT_FALSE(res.has_value());
|
||||
}
|
||||
54
tests/general/unit_test_simplex_additional.cpp
Normal file
54
tests/general/unit_test_simplex_additional.cpp
Normal file
@@ -0,0 +1,54 @@
|
||||
#include "omath/collision/simplex.hpp"
|
||||
#include "omath/linear_algebra/vector3.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using omath::Vector3;
|
||||
|
||||
TEST(SimplexAdditional, RegionACSelectsAC)
|
||||
{
|
||||
// Construct points that force the Region AC branch where ac points toward the origin
|
||||
Vector3<float> a{1.f, 0.f, 0.f};
|
||||
Vector3<float> b{2.f, 0.f, 0.f};
|
||||
Vector3<float> c{0.f, 1.f, 0.f};
|
||||
|
||||
omath::collision::Simplex<omath::Vector3<float>> s;
|
||||
s = { a, b, c };
|
||||
|
||||
omath::Vector3<float> dir{0.f, 0.f, 0.f};
|
||||
const bool hit = s.handle(dir);
|
||||
|
||||
// Should not report a collision; simplex should reduce to {a, c}
|
||||
EXPECT_FALSE(hit);
|
||||
EXPECT_EQ(s.size(), 2u);
|
||||
EXPECT_TRUE(s[0] == a);
|
||||
EXPECT_TRUE(s[1] == c);
|
||||
// direction should be finite and non-zero
|
||||
EXPECT_TRUE(std::isfinite(dir.x));
|
||||
EXPECT_TRUE(std::isfinite(dir.y));
|
||||
EXPECT_TRUE(std::isfinite(dir.z));
|
||||
}
|
||||
|
||||
TEST(SimplexAdditional, AbcAboveSetsDirection)
|
||||
{
|
||||
// Choose triangle so abc points roughly toward the origin (abc · ao > 0)
|
||||
Vector3<float> a{-1.f, 0.f, 0.f};
|
||||
Vector3<float> b{0.f, 1.f, 0.f};
|
||||
Vector3<float> c{0.f, 0.f, 1.f};
|
||||
|
||||
omath::collision::Simplex<omath::Vector3<float>> s;
|
||||
s = { a, b, c };
|
||||
|
||||
omath::Vector3<float> dir{0.f, 0.f, 0.f};
|
||||
const bool hit = s.handle(dir);
|
||||
|
||||
EXPECT_FALSE(hit);
|
||||
|
||||
const auto ab = b - a;
|
||||
const auto ac = c - a;
|
||||
const auto abc = ab.cross(ac);
|
||||
|
||||
// direction should equal abc (above triangle case)
|
||||
EXPECT_NEAR(dir.x, abc.x, 1e-6f);
|
||||
EXPECT_NEAR(dir.y, abc.y, 1e-6f);
|
||||
EXPECT_NEAR(dir.z, abc.z, 1e-6f);
|
||||
}
|
||||
174
tests/general/unit_test_simplex_more.cpp
Normal file
174
tests/general/unit_test_simplex_more.cpp
Normal file
@@ -0,0 +1,174 @@
|
||||
#include "omath/collision/simplex.hpp"
|
||||
#include "omath/linear_algebra/vector3.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using omath::Vector3;
|
||||
using Simplex = omath::collision::Simplex<Vector3<float>>;
|
||||
|
||||
TEST(SimplexExtra, HandleLine_CollinearProducesPerp)
|
||||
{
|
||||
// a and b placed so ab points roughly same dir as ao and are collinear
|
||||
Vector3<float> a{2.f, 0.f, 0.f};
|
||||
Vector3<float> b{1.f, 0.f, 0.f};
|
||||
|
||||
Simplex s;
|
||||
s = {a, b};
|
||||
|
||||
Vector3<float> dir{0.f, 0.f, 0.f};
|
||||
const bool hit = s.handle(dir);
|
||||
|
||||
// Should not report collision for a line simplex
|
||||
EXPECT_FALSE(hit);
|
||||
// Direction must be finite and not zero
|
||||
EXPECT_TRUE(std::isfinite(dir.x));
|
||||
EXPECT_TRUE(std::isfinite(dir.y));
|
||||
EXPECT_TRUE(std::isfinite(dir.z));
|
||||
constexpr auto zero = Vector3<float>{0.f, 0.f, 0.f};
|
||||
EXPECT_FALSE(dir == zero);
|
||||
|
||||
// Ensure direction is (approximately) perpendicular to ab
|
||||
const auto ab = b - a;
|
||||
const float dot = dir.dot(ab);
|
||||
EXPECT_NEAR(dot, 0.0f, 1e-4f);
|
||||
}
|
||||
|
||||
TEST(SimplexExtra, HandleLine_NonCollinearProducesValidDirection)
|
||||
{
|
||||
Vector3<float> a{2.f, 0.f, 0.f};
|
||||
Vector3<float> b{1.f, 1.f, 0.f};
|
||||
|
||||
Simplex s;
|
||||
s = {a, b};
|
||||
|
||||
Vector3<float> dir{0.f, 0.f, 0.f};
|
||||
const bool hit = s.handle(dir);
|
||||
|
||||
EXPECT_FALSE(hit);
|
||||
EXPECT_TRUE(std::isfinite(dir.x));
|
||||
EXPECT_TRUE(std::isfinite(dir.y));
|
||||
EXPECT_TRUE(std::isfinite(dir.z));
|
||||
}
|
||||
|
||||
TEST(SimplexExtra, HandleTriangle_FlipWinding)
|
||||
{
|
||||
// Construct points where triangle winding will be flipped
|
||||
Vector3<float> a{1.f, 0.f, 0.f};
|
||||
Vector3<float> b{0.f, 1.f, 0.f};
|
||||
Vector3<float> c{0.f, -1.f, 0.f};
|
||||
|
||||
Simplex s;
|
||||
s = {a, b, c};
|
||||
|
||||
Vector3<float> dir{0.f, 0.f, 0.f};
|
||||
const bool hit = s.handle(dir);
|
||||
|
||||
EXPECT_FALSE(hit);
|
||||
EXPECT_TRUE(std::isfinite(dir.x));
|
||||
EXPECT_TRUE(std::isfinite(dir.y));
|
||||
EXPECT_TRUE(std::isfinite(dir.z));
|
||||
}
|
||||
|
||||
TEST(SimplexExtra, HandleTetrahedron_InsideReturnsTrue)
|
||||
{
|
||||
// Simple tetra that should contain the origin
|
||||
Vector3<float> a{1.f, 0.f, 0.f};
|
||||
Vector3<float> b{0.f, 1.f, 0.f};
|
||||
Vector3<float> c{0.f, 0.f, 1.f};
|
||||
Vector3<float> d{-0.2f, -0.2f, -0.2f};
|
||||
|
||||
Simplex s;
|
||||
s = {a, b, c, d};
|
||||
|
||||
Vector3<float> dir{0.f, 0.f, 0.f};
|
||||
const bool hit = s.handle(dir);
|
||||
|
||||
// If origin is inside, handle_tetrahedron should return true
|
||||
EXPECT_TRUE(hit);
|
||||
}
|
||||
// Additional sanity tests (avoid reusing Simplex alias above to prevent ambiguity)
|
||||
TEST(SimplexMore, PushFrontAndAccess)
|
||||
{
|
||||
omath::collision::Simplex<omath::Vector3<float>> s;
|
||||
s.push_front(omath::Vector3<float>{1.f, 0.f, 0.f});
|
||||
s.push_front(omath::Vector3<float>{2.f, 0.f, 0.f});
|
||||
s.push_front(omath::Vector3<float>{3.f, 0.f, 0.f});
|
||||
|
||||
EXPECT_EQ(s.size(), 3u);
|
||||
constexpr omath::Vector3<float> exp_front{3.f, 0.f, 0.f};
|
||||
constexpr omath::Vector3<float> exp_back{1.f, 0.f, 0.f};
|
||||
EXPECT_TRUE(s.front() == exp_front);
|
||||
EXPECT_TRUE(s.back() == exp_back);
|
||||
const auto d = s.data();
|
||||
EXPECT_TRUE(d[0] == exp_front);
|
||||
}
|
||||
|
||||
TEST(SimplexMore, ClearAndEmpty)
|
||||
{
|
||||
omath::collision::Simplex<omath::Vector3<float>> s;
|
||||
s.push_front(omath::Vector3<float>{1.f, 1.f, 1.f});
|
||||
EXPECT_FALSE(s.empty());
|
||||
s.clear();
|
||||
EXPECT_TRUE(s.empty());
|
||||
}
|
||||
|
||||
TEST(SimplexMore, HandleLineCollinearProducesPerp)
|
||||
{
|
||||
omath::collision::Simplex<omath::Vector3<float>> s;
|
||||
s = {omath::Vector3<float>{2.f, 0.f, 0.f}, omath::Vector3<float>{1.f, 0.f, 0.f}};
|
||||
omath::Vector3<float> dir{0.f, 0.f, 0.f};
|
||||
const bool res = s.handle(dir);
|
||||
EXPECT_FALSE(res);
|
||||
EXPECT_GT(dir.length_sqr(), 0.0f);
|
||||
}
|
||||
|
||||
TEST(SimplexMore, HandleTriangleFlipWinding)
|
||||
{
|
||||
constexpr omath::Vector3<float> a{1.f, 0.f, 0.f};
|
||||
constexpr omath::Vector3<float> b{0.f, 1.f, 0.f};
|
||||
constexpr omath::Vector3<float> c{0.f, 0.f, 1.f};
|
||||
omath::collision::Simplex<omath::Vector3<float>> s;
|
||||
s = {a, b, c};
|
||||
omath::Vector3<float> dir{0.f, 0.f, 0.f};
|
||||
|
||||
constexpr auto ab = b - a;
|
||||
constexpr auto ac = c - a;
|
||||
const auto abc = ab.cross(ac);
|
||||
|
||||
const bool res = s.handle(dir);
|
||||
EXPECT_FALSE(res);
|
||||
const auto expected = -abc;
|
||||
EXPECT_NEAR(dir.x, expected.x, 1e-6f);
|
||||
EXPECT_NEAR(dir.y, expected.y, 1e-6f);
|
||||
EXPECT_NEAR(dir.z, expected.z, 1e-6f);
|
||||
}
|
||||
|
||||
TEST(SimplexMore, HandleTetrahedronInsideTrue)
|
||||
{
|
||||
omath::collision::Simplex<omath::Vector3<float>> s;
|
||||
s = {omath::Vector3<float>{1.f, 0.f, 0.f}, omath::Vector3<float>{0.f, 1.f, 0.f},
|
||||
omath::Vector3<float>{0.f, 0.f, 1.f}, omath::Vector3<float>{-1.f, -1.f, -1.f}};
|
||||
omath::Vector3<float> dir{0.f, 0.f, 0.f};
|
||||
const bool inside = s.handle(dir);
|
||||
EXPECT_TRUE(inside);
|
||||
}
|
||||
|
||||
TEST(SimplexMore, HandlePointSetsDirection)
|
||||
{
|
||||
omath::collision::Simplex<omath::Vector3<float>> s;
|
||||
s = {omath::Vector3<float>{1.f, 2.f, 3.f}};
|
||||
omath::Vector3<float> dir{0.f, 0.f, 0.f};
|
||||
EXPECT_FALSE(s.handle(dir));
|
||||
EXPECT_NEAR(dir.x, -1.f, 1e-6f);
|
||||
EXPECT_NEAR(dir.y, -2.f, 1e-6f);
|
||||
EXPECT_NEAR(dir.z, -3.f, 1e-6f);
|
||||
}
|
||||
|
||||
TEST(SimplexMore, HandleLineReducesToPointWhenAoOpposite)
|
||||
{
|
||||
omath::collision::Simplex<omath::Vector3<float>> s;
|
||||
s = {omath::Vector3<float>{1.f, 0.f, 0.f}, omath::Vector3<float>{2.f, 0.f, 0.f}};
|
||||
omath::Vector3<float> dir{0.f, 0.f, 0.f};
|
||||
EXPECT_FALSE(s.handle(dir));
|
||||
EXPECT_EQ(s.size(), 1u);
|
||||
EXPECT_NEAR(dir.x, -1.f, 1e-6f);
|
||||
}
|
||||
@@ -99,15 +99,15 @@ TEST_F(UnitTestTriangle, SideLengths)
|
||||
// Test side vectors
|
||||
TEST_F(UnitTestTriangle, SideVectors)
|
||||
{
|
||||
const Vector3 sideA_t1 = t1.side_a_vector(); // m_vertex1 - m_vertex2
|
||||
EXPECT_FLOAT_EQ(sideA_t1.x, 0.0f - 1.0f);
|
||||
EXPECT_FLOAT_EQ(sideA_t1.y, 0.0f - 0.0f);
|
||||
EXPECT_FLOAT_EQ(sideA_t1.z, 0.0f - 0.0f);
|
||||
const Vector3 side_a_t1 = t1.side_a_vector(); // m_vertex1 - m_vertex2
|
||||
EXPECT_FLOAT_EQ(side_a_t1.x, 0.0f - 1.0f);
|
||||
EXPECT_FLOAT_EQ(side_a_t1.y, 0.0f - 0.0f);
|
||||
EXPECT_FLOAT_EQ(side_a_t1.z, 0.0f - 0.0f);
|
||||
|
||||
const Vector3 sideB_t1 = t1.side_b_vector(); // m_vertex3 - m_vertex2
|
||||
EXPECT_FLOAT_EQ(sideB_t1.x, 0.0f - 1.0f);
|
||||
EXPECT_FLOAT_EQ(sideB_t1.y, 1.0f - 0.0f);
|
||||
EXPECT_FLOAT_EQ(sideB_t1.z, 0.0f - 0.0f);
|
||||
const Vector3 side_b_t1 = t1.side_b_vector(); // m_vertex3 - m_vertex2
|
||||
EXPECT_FLOAT_EQ(side_b_t1.x, 0.0f - 1.0f);
|
||||
EXPECT_FLOAT_EQ(side_b_t1.y, 1.0f - 0.0f);
|
||||
EXPECT_FLOAT_EQ(side_b_t1.z, 0.0f - 0.0f);
|
||||
}
|
||||
|
||||
TEST_F(UnitTestTriangle, IsRectangular)
|
||||
|
||||
@@ -306,7 +306,7 @@ TEST_F(UnitTestVector2, DivisionAssignmentOperator_VectorWithZero)
|
||||
// Test operations with infinity and NaN
|
||||
TEST_F(UnitTestVector2, Operator_WithInfinity)
|
||||
{
|
||||
constexpr Vector2 v_inf(INFINITY, INFINITY);
|
||||
const Vector2 v_inf(INFINITY, INFINITY);
|
||||
const Vector2 result = v1 + v_inf;
|
||||
EXPECT_TRUE(std::isinf(result.x));
|
||||
EXPECT_TRUE(std::isinf(result.y));
|
||||
|
||||
@@ -10,6 +10,61 @@
|
||||
|
||||
using namespace omath;
|
||||
|
||||
TEST(Vector3More, ConstructorsAndEquality)
|
||||
{
|
||||
constexpr Vector3<float> a;
|
||||
EXPECT_EQ(a.x, 0.f);
|
||||
EXPECT_EQ(a.y, 0.f);
|
||||
EXPECT_EQ(a.z, 0.f);
|
||||
|
||||
constexpr Vector3<float> b{1.f, 2.f, 3.f};
|
||||
EXPECT_EQ(b.x, 1.f);
|
||||
EXPECT_EQ(b.y, 2.f);
|
||||
EXPECT_EQ(b.z, 3.f);
|
||||
|
||||
const Vector3<float> c = b;
|
||||
EXPECT_EQ(c, b);
|
||||
}
|
||||
|
||||
TEST(Vector3More, ArithmeticAndDotCross)
|
||||
{
|
||||
constexpr Vector3<float> a{1.f, 0.f, 0.f};
|
||||
constexpr Vector3<float> b{0.f, 1.f, 0.f};
|
||||
const auto c = a + b;
|
||||
constexpr Vector3<float> expect_c{1.f,1.f,0.f};
|
||||
EXPECT_EQ(c, expect_c);
|
||||
|
||||
const auto d = a - b;
|
||||
constexpr Vector3<float> expect_d{1.f,-1.f,0.f};
|
||||
EXPECT_EQ(d, expect_d);
|
||||
|
||||
const auto e = a * 2.f;
|
||||
constexpr Vector3<float> expect_e{2.f,0.f,0.f};
|
||||
EXPECT_EQ(e, expect_e);
|
||||
|
||||
EXPECT_FLOAT_EQ(a.dot(b), 0.f);
|
||||
// manual cross product check
|
||||
const auto cr = Vector3<float>{ a.y * b.z - a.z * b.y,
|
||||
a.z * b.x - a.x * b.z,
|
||||
a.x * b.y - a.y * b.x };
|
||||
constexpr Vector3<float> expect_cr{0.f,0.f,1.f};
|
||||
EXPECT_EQ(cr, expect_cr);
|
||||
}
|
||||
|
||||
TEST(Vector3More, NormalizationEdgeCases)
|
||||
{
|
||||
constexpr Vector3<double> z{0.0,0.0,0.0};
|
||||
const auto zn = z.normalized();
|
||||
EXPECT_DOUBLE_EQ(zn.x, 0.0);
|
||||
EXPECT_DOUBLE_EQ(zn.y, 0.0);
|
||||
EXPECT_DOUBLE_EQ(zn.z, 0.0);
|
||||
|
||||
constexpr Vector3<double> v{3.0,4.0,0.0};
|
||||
const auto vn = v.normalized();
|
||||
EXPECT_NEAR(vn.x, 0.6, 1e-12);
|
||||
EXPECT_NEAR(vn.y, 0.8, 1e-12);
|
||||
}
|
||||
|
||||
class UnitTestVector3 : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
@@ -260,7 +315,7 @@ TEST_F(UnitTestVector3, Division_ByZeroScalar)
|
||||
// Test operations with infinity
|
||||
TEST_F(UnitTestVector3, Addition_WithInfinity)
|
||||
{
|
||||
constexpr Vector3 v_inf(INFINITY, INFINITY, INFINITY);
|
||||
const Vector3 v_inf(INFINITY, INFINITY, INFINITY);
|
||||
const Vector3 result = v1 + v_inf;
|
||||
EXPECT_TRUE(std::isinf(result.x));
|
||||
EXPECT_TRUE(std::isinf(result.y));
|
||||
@@ -390,8 +445,10 @@ TEST_F(UnitTestVector3, AsTuple)
|
||||
// Test AsTuple method
|
||||
TEST_F(UnitTestVector3, AngleBeatween)
|
||||
{
|
||||
EXPECT_EQ(Vector3(0.0f, 0.0f, 1.0f).angle_between({1, 0 ,0}).value().as_degrees(), 90.0f);
|
||||
EXPECT_EQ(Vector3(0.0f, 0.0f, 1.0f).angle_between({0.0f, 0.0f, 1.0f}).value().as_degrees(), 0.0f);
|
||||
EXPECT_NEAR(Vector3(0.0f, 0.0f, 1.0f).angle_between({1, 0, 0}).value().as_degrees(),
|
||||
90.0f, 0.001f);
|
||||
EXPECT_NEAR(Vector3(0.0f, 0.0f, 1.0f).angle_between({0.0f, 0.0f, 1.0f}).value().as_degrees(),
|
||||
0.0f, 0.001f);
|
||||
EXPECT_FALSE(Vector3(0.0f, 0.0f, 0.0f).angle_between({0.0f, 0.0f, 1.0f}).has_value());
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,32 @@
|
||||
|
||||
using namespace omath;
|
||||
|
||||
TEST(Vector4More, ConstructorsAndClamp)
|
||||
{
|
||||
constexpr Vector4<float> a;
|
||||
EXPECT_EQ(a.x, 0.f);
|
||||
EXPECT_EQ(a.y, 0.f);
|
||||
EXPECT_EQ(a.z, 0.f);
|
||||
EXPECT_EQ(a.w, 0.f);
|
||||
|
||||
Vector4<float> b{1.f, -2.f, 3.5f, 4.f};
|
||||
b.clamp(0.f, 3.f);
|
||||
EXPECT_GE(b.x, 0.f);
|
||||
EXPECT_GE(b.y, 0.f);
|
||||
EXPECT_LE(b.z, 3.f);
|
||||
}
|
||||
|
||||
TEST(Vector4More, ComparisonsAndHashFormatter)
|
||||
{
|
||||
constexpr Vector4<int> a{1,2,3,4};
|
||||
constexpr Vector4<int> b{1,2,3,5};
|
||||
EXPECT_NE(a, b);
|
||||
|
||||
// exercise to_string via formatting if available by converting via std::format
|
||||
// call length and comparison to exercise more branches
|
||||
EXPECT_LT(a.length(), b.length());
|
||||
}
|
||||
|
||||
class UnitTestVector4 : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "General purpose math library",
|
||||
"homepage": "https://github.com/orange-cpp/omath",
|
||||
"license": "Zlib",
|
||||
"supports": "windows | linux",
|
||||
"supports": "windows | linux | macos",
|
||||
"dependencies": [
|
||||
{
|
||||
"name": "vcpkg-cmake",
|
||||
@@ -26,6 +26,13 @@
|
||||
"benchmark"
|
||||
]
|
||||
},
|
||||
"examples": {
|
||||
"description": "Build benchmarks",
|
||||
"dependencies": [
|
||||
"glfw3",
|
||||
"glew"
|
||||
]
|
||||
},
|
||||
"imgui": {
|
||||
"description": "Omath will define method to convert omath types to imgui types",
|
||||
"dependencies": [
|
||||
|
||||
Reference in New Issue
Block a user