diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..0d5d0d47 --- /dev/null +++ b/.clang-format @@ -0,0 +1,64 @@ +# Generated by CLion for Stroustrup +# The Stroustrup style, named after Bjarne Stroustrup, the creator of C++, is similar to the K&R style but differs +# in its treatment of the class definitions and the placement of braces in certain contexts. The opening brace is +# placed on the same line as the control statement, and the closing brace is on its own line. +BasedOnStyle: LLVM + +AccessModifierOffset: -4 +AlignConsecutiveAssignments: None +AlignConsecutiveBitFields: None +AlignConsecutiveDeclarations: None +AlignConsecutiveMacros: AcrossEmptyLinesAndComments +AlignTrailingComments: false +AllowShortBlocksOnASingleLine: Never +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +BreakTemplateDeclarations: Leave +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: true + AfterClass: true + AfterFunction: true + AfterControlStatement: true + SplitEmptyFunction: true + AfterEnum: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: true + BeforeElse: true + BeforeLambdaBody: true + BeforeWhile: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: NonAssignment +BreakBeforeConceptDeclarations: false +ColumnLimit: 120 +IncludeBlocks: Merge +IndentExternBlock: Indent +IndentRequiresClause: false +IndentWidth: 4 +ContinuationIndentWidth: 8 +KeepEmptyLinesAtTheStartOfBlocks: false +NamespaceIndentation: All +PointerAlignment: Left +SortUsingDeclarations: true +SpaceAfterTemplateKeyword: false +SpaceBeforeCtorInitializerColon: false +SpaceBeforeParens: Custom +SpaceBeforeParensOptions: + AfterControlStatements: true + AfterFunctionDeclarationName: false + AfterFunctionDefinitionName: false + AfterForeachMacros: true + AfterIfMacros: true + AfterOverloadedOperator: false + BeforeNonEmptyParentheses: false +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyParentheses: false +SpacesInCStyleCastParentheses: false +SpacesInConditionalStatement: false +SpacesInContainerLiterals: false +SpacesInParentheses: false \ No newline at end of file diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 00000000..fd8c681c --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,147 @@ +# Generated from CLion Inspection settings +--- +Checks: '-*, +bugprone-argument-comment, +bugprone-assert-side-effect, +bugprone-bad-signal-to-kill-thread, +bugprone-branch-clone, +bugprone-copy-constructor-init, +bugprone-dangling-handle, +bugprone-dynamic-static-initializers, +bugprone-fold-init-type, +bugprone-forward-declaration-namespace, +bugprone-forwarding-reference-overload, +bugprone-inaccurate-erase, +bugprone-incorrect-roundings, +bugprone-integer-division, +bugprone-lambda-function-name, +bugprone-macro-parentheses, +bugprone-macro-repeated-side-effects, +bugprone-misplaced-operator-in-strlen-in-alloc, +bugprone-misplaced-pointer-arithmetic-in-alloc, +bugprone-misplaced-widening-cast, +bugprone-move-forwarding-reference, +bugprone-multiple-statement-macro, +bugprone-no-escape, +bugprone-parent-virtual-call, +bugprone-posix-return, +bugprone-reserved-identifier, +bugprone-sizeof-container, +bugprone-sizeof-expression, +bugprone-spuriously-wake-up-functions, +bugprone-string-constructor, +bugprone-string-integer-assignment, +bugprone-string-literal-with-embedded-nul, +bugprone-suspicious-enum-usage, +bugprone-suspicious-include, +bugprone-suspicious-memset-usage, +bugprone-suspicious-missing-comma, +bugprone-suspicious-semicolon, +bugprone-suspicious-string-compare, +bugprone-suspicious-memory-comparison, +bugprone-suspicious-realloc-usage, +bugprone-swapped-arguments, +bugprone-terminating-continue, +bugprone-throw-keyword-missing, +bugprone-too-small-loop-variable, +bugprone-undefined-memory-manipulation, +bugprone-undelegated-constructor, +bugprone-unhandled-self-assignment, +bugprone-unused-raii, +bugprone-unused-return-value, +bugprone-use-after-move, +bugprone-virtual-near-miss, +cert-dcl21-cpp, +cert-dcl58-cpp, +cert-err34-c, +cert-err52-cpp, +cert-err60-cpp, +cert-flp30-c, +cert-msc50-cpp, +cert-msc51-cpp, +cert-str34-c, +cppcoreguidelines-interfaces-global-init, +cppcoreguidelines-narrowing-conversions, +cppcoreguidelines-pro-type-member-init, +cppcoreguidelines-pro-type-static-cast-downcast, +cppcoreguidelines-slicing, +google-default-arguments, +google-explicit-constructor, +google-runtime-operator, +hicpp-exception-baseclass, +hicpp-multiway-paths-covered, +misc-misplaced-const, +misc-new-delete-overloads, +misc-no-recursion, +misc-non-copyable-objects, +misc-throw-by-value-catch-by-reference, +misc-unconventional-assign-operator, +misc-uniqueptr-reset-release, +modernize-avoid-bind, +modernize-concat-nested-namespaces, +modernize-deprecated-headers, +modernize-deprecated-ios-base-aliases, +modernize-loop-convert, +modernize-make-shared, +modernize-make-unique, +modernize-pass-by-value, +modernize-raw-string-literal, +modernize-redundant-void-arg, +modernize-replace-auto-ptr, +modernize-replace-disallow-copy-and-assign-macro, +modernize-replace-random-shuffle, +modernize-return-braced-init-list, +modernize-shrink-to-fit, +modernize-unary-static-assert, +modernize-use-auto, +modernize-use-bool-literals, +modernize-use-emplace, +modernize-use-equals-default, +modernize-use-equals-delete, +modernize-use-nodiscard, +modernize-use-noexcept, +modernize-use-nullptr, +modernize-use-override, +modernize-use-transparent-functors, +modernize-use-uncaught-exceptions, +mpi-buffer-deref, +mpi-type-mismatch, +openmp-use-default-none, +performance-faster-string-find, +performance-for-range-copy, +performance-implicit-conversion-in-loop, +performance-inefficient-algorithm, +performance-inefficient-string-concatenation, +performance-inefficient-vector-operation, +performance-move-const-arg, +performance-move-constructor-init, +performance-no-automatic-move, +performance-noexcept-move-constructor, +performance-trivially-destructible, +performance-type-promotion-in-math-fn, +performance-unnecessary-copy-initialization, +performance-unnecessary-value-param, +portability-simd-intrinsics, +readability-avoid-const-params-in-decls, +readability-const-return-type, +readability-container-size-empty, +readability-convert-member-functions-to-static, +readability-delete-null-pointer, +readability-deleted-default, +readability-inconsistent-declaration-parameter-name, +readability-make-member-function-const, +readability-misleading-indentation, +readability-misplaced-array-index, +readability-non-const-parameter, +readability-redundant-control-flow, +readability-redundant-declaration, +readability-redundant-function-ptr-dereference, +readability-redundant-smartptr-get, +readability-redundant-string-cstr, +readability-redundant-string-init, +readability-simplify-subscript-expr, +readability-static-accessed-through-instance, +readability-static-definition-in-anonymous-namespace, +readability-string-compare, +readability-uniqueptr-delete-release, +readability-use-anyofallof' \ No newline at end of file diff --git a/.cmake-format b/.cmake-format new file mode 100644 index 00000000..981cd194 --- /dev/null +++ b/.cmake-format @@ -0,0 +1,25 @@ +format: + line_width: 100 + tab_size: 4 + use_tabchars: false + max_subgroups_hwrap: 3 + max_pargs_hwrap: 5 + separate_ctrl_name_with_space: false + separate_fn_name_with_space: false + dangle_parens: false + dangle_align: child + line_ending: unix + command_case: canonical + keyword_case: upper + enable_sort: true + autosort: true +markup: + bullet_char: "*" + enum_char: . + enable_markup: false +additional_commands: + target_link_libraries: + kwargs: + PUBLIC: "*" + SHARED: "*" + PRIVATE: "*" diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..997504b4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# SCM syntax highlighting & preventing 3-way merges +pixi.lock merge=binary linguist-language=YAML linguist-generated=true -diff diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..dd84ea78 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md new file mode 100644 index 00000000..48d5f81f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/custom.md @@ -0,0 +1,10 @@ +--- +name: Custom issue template +about: Describe this issue template's purpose here. +title: '' +labels: '' +assignees: '' + +--- + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..bbcbbe7d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/psd/omath.psd b/.github/psd/omath.psd new file mode 100644 index 00000000..5d249654 Binary files /dev/null and b/.github/psd/omath.psd differ diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml new file mode 100644 index 00000000..2c6b9858 --- /dev/null +++ b/.github/workflows/cmake-multi-platform.yml @@ -0,0 +1,813 @@ +name: Omath CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +############################################################################## +# 1) Linux – Clang / Ninja +############################################################################## +jobs: + 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 + shell: bash + run: ${{ matrix.install_cmd }} + + - name: Checkout repository (with sub-modules) + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up vcpkg + shell: bash + run: | + git clone https://github.com/microsoft/vcpkg "$VCPKG_ROOT" + cd "$VCPKG_ROOT" + ./bootstrap-vcpkg.sh + + - name: Configure (cmake --preset) + shell: bash + run: | + cmake --preset ${{ matrix.preset }} \ + -DVCPKG_INSTALL_OPTIONS="--allow-unsupported" \ + -DOMATH_BUILD_TESTS=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/${{ 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: ${{ 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: + VCPKG_ROOT: ${{ github.workspace }}/vcpkg + steps: + - name: Checkout repository (with sub-modules) + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Ninja + uses: seanmiddleditch/gha-setup-ninja@v4 + + - name: Set up MSVC developer command-prompt + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: ${{ matrix.arch }} + + - name: Configure (cmake --preset) + shell: bash + run: | + cmake --preset ${{ matrix.preset }} \ + -DOMATH_BUILD_TESTS=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/${{ matrix.preset }} --target unit_tests omath + + - name: Run unit_tests.exe + shell: bash + run: ./out/Release/unit_tests.exe + + ########################################################################## + # Coverage (x64-windows only) + ########################################################################## + - name: Install LLVM + if: ${{ matrix.triplet == 'x64-windows' }} + run: | + choco install llvm -y + + - name: Clean Build Directory for Coverage + if: ${{ matrix.triplet == 'x64-windows' }} + shell: pwsh + run: | + $buildDir = "cmake-build/build/${{ matrix.preset }}" + if (Test-Path $buildDir) { + Write-Host "Cleaning build directory to prevent compiler conflict: $buildDir" + Remove-Item -Path $buildDir -Recurse -Force + } + + - name: Build Debug for Coverage + if: ${{ matrix.triplet == 'x64-windows' }} + shell: bash + run: | + cmake --preset ${{ matrix.preset }} \ + -DCMAKE_C_COMPILER="C:/Program Files/LLVM/bin/clang-cl.exe" \ + -DCMAKE_CXX_COMPILER="C:/Program Files/LLVM/bin/clang-cl.exe" \ + -DCMAKE_LINKER="C:/Program Files/LLVM/bin/lld-link.exe" \ + -DOMATH_BUILD_TESTS=ON \ + -DOMATH_BUILD_BENCHMARK=OFF \ + -DOMATH_ENABLE_COVERAGE=ON \ + -DOMATH_THREAT_WARNING_AS_ERROR=OFF \ + -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 Tests (Generates .profraw) + if: ${{ matrix.triplet == 'x64-windows' }} + shell: pwsh + env: + LLVM_PROFILE_FILE: "cmake-build/build/${{ matrix.preset }}/unit_tests.profraw" + run: | + ./out/Debug/unit_tests.exe + + - name: Process Coverage (llvm-profdata & llvm-cov) + if: ${{ matrix.triplet == 'x64-windows' }} + shell: pwsh + run: | + $BUILD_DIR = "cmake-build/build/${{ matrix.preset }}" + $EXE_PATH = "./out/Debug/unit_tests.exe" + + # 1. Merge raw profile data (essential step) + & "C:/Program Files/LLVM/bin/llvm-profdata.exe" merge ` + -sparse "$BUILD_DIR/unit_tests.profraw" ` + -o "$BUILD_DIR/unit_tests.profdata" + + # 2. Export to LCOV format + # NOTE: We explicitly ignore vcpkg_installed and system headers + & "C:/Program Files/LLVM/bin/llvm-cov.exe" export "$EXE_PATH" ` + -instr-profile="$BUILD_DIR/unit_tests.profdata" ` + -format=lcov ` + -ignore-filename-regex="vcpkg_installed|external|tests" ` + > "$BUILD_DIR/lcov.info" + + if (Test-Path "$BUILD_DIR/lcov.info") { + Write-Host "✅ LCOV info created at $BUILD_DIR/lcov.info" + } else { + Write-Error "Failed to create LCOV info" + exit 1 + } + + - name: Install LCOV (for genhtml) + if: ${{ matrix.triplet == 'x64-windows' }} + run: choco install lcov -y + + - name: Generate HTML Report + if: ${{ matrix.triplet == 'x64-windows' }} + shell: bash + run: | + BUILD_DIR="cmake-build/build/${{ matrix.preset }}" + LCOV_INFO="${BUILD_DIR}/lcov.info" + HTML_DIR="${BUILD_DIR}/coverage-html" + + # Fix paths for genhtml (Perl hates backslashes) + sed -i 's|\\|/|g' "${LCOV_INFO}" + + # Locate genhtml provided by 'choco install lcov' + # It is typically in ProgramData/chocolatey/lib/lcov/tools/bin + GENHTML=$(find /c/ProgramData/chocolatey -name genhtml -print -quit) + + if [ -z "$GENHTML" ]; then + echo "Error: genhtml executable not found" + exit 1 + fi + + echo "Using genhtml: $GENHTML" + mkdir -p "$HTML_DIR" + + # Run genhtml + # Added --demangle-cpp if your version supports it, otherwise remove it + perl "$GENHTML" \ + "${LCOV_INFO}" \ + --output-directory "$HTML_DIR" \ + --title "OMath Coverage Report" \ + --legend \ + --show-details \ + --branch-coverage \ + --ignore-errors source + + echo "✅ LCOV HTML report generated at $HTML_DIR" + + - name: Upload Coverage (HTML Report) + if: ${{ matrix.triplet == 'x64-windows' }} + uses: actions/upload-artifact@v4 + with: + name: coverage-html-windows + path: cmake-build/build/${{ matrix.preset }}/coverage-html/ + + - 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: ${{ 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: + - name: Install basic tool-chain with Homebrew + shell: bash + run: | + brew install cmake ninja + + - name: Checkout repository (with sub-modules) + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up vcpkg + shell: bash + run: | + git clone https://github.com/microsoft/vcpkg "$VCPKG_ROOT" + + - name: Configure (cmake --preset) + shell: bash + run: | + cmake --preset ${{ matrix.preset }} \ + -DOMATH_BUILD_TESTS=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/${{ 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 + + + ############################################################################## + # 9) Valgrind Memory Check + ############################################################################## + valgrind-memory-check: + name: Valgrind Analysis (All Targets) + runs-on: ubuntu-latest + needs: [linux-build-and-test] + env: + VCPKG_ROOT: ${{ github.workspace }}/vcpkg + steps: + - name: Install toolchain + shell: bash + run: | + 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 valgrind libxmu-dev libxi-dev libgl-dev libxinerama-dev libxcursor-dev xorg-dev libglu1-mesa-dev + sudo update-alternatives --install /usr/bin/cc cc /usr/bin/clang-21 100 + sudo update-alternatives --install /usr/bin/c++ c++ /usr/bin/clang++-21 100 + sudo update-alternatives --install /usr/bin/lld lld /usr/bin/lld-21 100 + + - name: 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 linux-release-vcpkg \ + -DCMAKE_BUILD_TYPE=Debug \ + -DOMATH_BUILD_EXAMPLES=OFF \ + -DOMATH_BUILD_TESTS=ON \ + -DOMATH_BUILD_BENCHMARK=ON \ + -DOMATH_ENABLE_VALGRIND=ON \ + -DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests;benchmark" + + - name: Build All Targets + shell: bash + run: cmake --build cmake-build/build/linux-release-vcpkg + + - name: Run Valgrind (All Registered Targets) + shell: bash + working-directory: cmake-build/build/linux-release-vcpkg + run: | + cmake --build . --target valgrind_all diff --git a/.gitignore b/.gitignore index 28c6c4bc..a01caf31 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,15 @@ /cmake-build/ -/.idea \ No newline at end of file +/out +*.DS_Store +/extlibs/vcpkg +.idea/workspace.xml +/build/ +/clang-coverage/ +*.gcov +*.bin +/site/ +# pixi lock +pixi.lock +# pixi environments +.pixi/* +!.pixi/config.toml diff --git a/.gitmodules b/.gitmodules index c16587cb..e69de29b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "extlibs/googletest"] - path = extlibs/googletest - url = https://github.com/google/googletest.git diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..13566b81 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 00000000..5d6ba7c9 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,209 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 00000000..6e6eec11 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/developer-tools.xml b/.idea/developer-tools.xml new file mode 100644 index 00000000..360c55c3 --- /dev/null +++ b/.idea/developer-tools.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/editor.xml b/.idea/editor.xml new file mode 100644 index 00000000..fde53486 --- /dev/null +++ b/.idea/editor.xml @@ -0,0 +1,435 @@ + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000..e9caadd2 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 00000000..dd4c951e --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..1046494f --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml index a572362a..58855d11 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/.idea/omath.iml b/.idea/omath.iml new file mode 100644 index 00000000..4c942354 --- /dev/null +++ b/.idea/omath.iml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/.idea/uml.iml b/.idea/uml.iml deleted file mode 100644 index f08604bb..00000000 --- a/.idea/uml.iml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 2dfef162..e96e0c6f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,12 +1,231 @@ cmake_minimum_required(VERSION 3.26) -project(uml) -set(CMAKE_CXX_STANDARD 26) -option(BUILD_TESTS "Build test programs" ON) -add_library(uml STATIC source/Vector3.cpp) +file(READ VERSION OMATH_VERSION) +project(omath VERSION ${OMATH_VERSION} LANGUAGES CXX) -add_subdirectory(source) -add_subdirectory(extlibs) -add_subdirectory(tests) +include(CMakePackageConfigHelpers) +include(CheckCXXCompilerFlag) -target_include_directories(uml PUBLIC include) \ No newline at end of file +include(cmake/Coverage.cmake) +include(cmake/Valgrind.cmake) + +if(MSVC) + check_cxx_compiler_flag("/arch:AVX2" COMPILER_SUPPORTS_AVX2) +else() + check_cxx_compiler_flag("-mavx2" COMPILER_SUPPORTS_AVX2) +endif() + +option(OMATH_BUILD_TESTS "Build unit tests" OFF) +option(OMATH_BUILD_BENCHMARK "Build benchmarks" OFF) +option(OMATH_THREAT_WARNING_AS_ERROR + "Set highest level of warnings and force compiler to treat them as errors" ON) +option(OMATH_BUILD_AS_SHARED_LIBRARY "Build Omath as .so or .dll" OFF) +option(OMATH_USE_AVX2 "Omath will use AVX2 to boost performance" ${COMPILER_SUPPORTS_AVX2}) +option(OMATH_IMGUI_INTEGRATION "Omath will define method to convert omath types to imgui types" OFF) +option(OMATH_BUILD_EXAMPLES "Build example projects with you can learn & play" OFF) +option(OMATH_STATIC_MSVC_RUNTIME_LIBRARY "Force Omath to link static runtime" OFF) +option(OMATH_ENABLE_LEGACY + "Will enable legacy classes that MUST be used ONLY for backward compatibility" ON) +option(OMATH_SUPRESS_SAFETY_CHECKS + "Supress some safety checks in release build to improve general performance" ON) +option(OMATH_ENABLE_COVERAGE "Enable coverage" OFF) +option(OMATH_ENABLE_FORCE_INLINE + "Will for compiler to make some functions to be force inlined no matter what" ON) +if(VCPKG_MANIFEST_FEATURES) + foreach(omath_feature IN LISTS VCPKG_MANIFEST_FEATURES) + if(omath_feature STREQUAL "imgui") + set(OMATH_IMGUI_INTEGRATION ON) + elseif(omath_feature STREQUAL "avx2") + set(OMATH_USE_AVX2 ${COMPILER_SUPPORTS_AVX2}) + elseif(omath_feature STREQUAL "tests") + 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() +endif() + +if(OMATH_USE_AVX2 AND NOT COMPILER_SUPPORTS_AVX2) + message( + WARNING "OMATH_USE_AVX2 requested, but compiler/target does not support AVX2. Disabling.") + set(OMATH_USE_AVX2 OFF CACHE BOOL "Omath will use AVX2 to boost performance" FORCE) +endif() + +if(${PROJECT_IS_TOP_LEVEL}) + message( + STATUS + "[${PROJECT_NAME}]: Building on ${CMAKE_HOST_SYSTEM_NAME}, compiler ${CMAKE_CXX_COMPILER_ID}" + ) + message(STATUS "[${PROJECT_NAME}]: Warnings as errors ${OMATH_THREAT_WARNING_AS_ERROR}") + message(STATUS "[${PROJECT_NAME}]: Build unit tests ${OMATH_BUILD_TESTS}") + message(STATUS "[${PROJECT_NAME}]: Build benchmark ${OMATH_BUILD_BENCHMARK}") + message(STATUS "[${PROJECT_NAME}]: As dynamic library ${OMATH_BUILD_AS_SHARED_LIBRARY}") + message(STATUS "[${PROJECT_NAME}]: Static C++ runtime ${OMATH_STATIC_MSVC_RUNTIME_LIBRARY}") + message(STATUS "[${PROJECT_NAME}]: CMake unity build ${OMATH_USE_UNITY_BUILD}") + message(STATUS "[${PROJECT_NAME}]: Example projects ${OMATH_BUILD_EXAMPLES}") + message(STATUS "[${PROJECT_NAME}]: AVX2 feature status ${OMATH_USE_AVX2}") + message(STATUS "[${PROJECT_NAME}]: ImGUI integration feature status ${OMATH_IMGUI_INTEGRATION}") + message(STATUS "[${PROJECT_NAME}]: Legacy features support ${OMATH_ENABLE_LEGACY}") + message(STATUS "[${PROJECT_NAME}]: Building using vcpkg ${OMATH_BUILD_VIA_VCPKG}") + message(STATUS "[${PROJECT_NAME}]: Coverage feature status ${OMATH_ENABLE_COVERAGE}") + message(STATUS "[${PROJECT_NAME}]: Valgrind feature status ${OMATH_ENABLE_VALGRIND}") +endif() + +file(GLOB_RECURSE OMATH_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/source/*.cpp") +file(GLOB_RECURSE OMATH_HEADERS CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/include/*.hpp") + +if(OMATH_BUILD_AS_SHARED_LIBRARY) + add_library(${PROJECT_NAME} SHARED ${OMATH_SOURCES} ${OMATH_HEADERS}) +else() + add_library(${PROJECT_NAME} STATIC ${OMATH_SOURCES} ${OMATH_HEADERS}) +endif() + +add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) + +target_compile_definitions(${PROJECT_NAME} PUBLIC OMATH_VERSION="${PROJECT_VERSION}") + +if(OMATH_IMGUI_INTEGRATION) + target_compile_definitions(${PROJECT_NAME} PUBLIC OMATH_IMGUI_INTEGRATION) + + # IMGUI is being linked as submodule + if(TARGET imgui) + target_link_libraries(${PROJECT_NAME} PUBLIC imgui) + install( + TARGETS imgui + EXPORT omathTargets + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin) + else() + # Assume that IMGUI linked via VCPKG. + find_package(imgui CONFIG REQUIRED) + target_link_libraries(${PROJECT_NAME} PUBLIC imgui::imgui) + endif() + +endif() + +if(OMATH_USE_AVX2) + target_compile_definitions(${PROJECT_NAME} PUBLIC OMATH_USE_AVX2) +endif() + +if(OMATH_SUPRESS_SAFETY_CHECKS) + target_compile_definitions(${PROJECT_NAME} PUBLIC OMATH_SUPRESS_SAFETY_CHECKS) +endif() + +if(OMATH_ENABLE_LEGACY) + target_compile_definitions(${PROJECT_NAME} PUBLIC OMATH_ENABLE_LEGACY) +endif() + +if(OMATH_ENABLE_FORCE_INLINE) + target_compile_definitions(${PROJECT_NAME} PUBLIC OMATH_ENABLE_FORCE_INLINE) +endif() + +set_target_properties( + ${PROJECT_NAME} + PROPERTIES ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}" + CXX_STANDARD 23 + CXX_STANDARD_REQUIRED ON) + +if (OMATH_STATIC_MSVC_RUNTIME_LIBRARY) + set_target_properties(${PROJECT_NAME} PROPERTIES + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>" + ) +endif () + +if(OMATH_USE_AVX2) + if(MSVC) + target_compile_options(${PROJECT_NAME} PUBLIC /arch:AVX2) + elseif(EMSCRIPTEN) + target_compile_options(${PROJECT_NAME} PUBLIC -msimd128 -mavx2) + elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang|AppleClang") + target_compile_options(${PROJECT_NAME} PUBLIC -mfma -mavx2) + 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) + add_subdirectory(benchmark) +endif() + +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) + target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic -Werror) +endif() + +# Windows SDK redefine min/max via preprocessor and break std::min and std::max +if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + target_compile_definitions(${PROJECT_NAME} INTERFACE NOMINMAX) +endif() + +target_include_directories( + ${PROJECT_NAME} + PUBLIC $ # Use this path + # when building + # the project + $ # Use this path when the project is + # installed + ) + +# Installation rules + +# Install the library +install( + TARGETS ${PROJECT_NAME} + EXPORT ${PROJECT_NAME}Targets + ARCHIVE DESTINATION lib COMPONENT ${PROJECT_NAME} # For static libraries + LIBRARY DESTINATION lib COMPONENT ${PROJECT_NAME} # For shared libraries + RUNTIME DESTINATION bin COMPONENT ${PROJECT_NAME} # For executables (on + # Windows) + ) + +# Install headers as part of omath_component +install(DIRECTORY include/ DESTINATION include COMPONENT ${PROJECT_NAME}) + +# Export omath target for CMake find_package support, also under omath_component +install( + EXPORT ${PROJECT_NAME}Targets + FILE ${PROJECT_NAME}Targets.cmake + NAMESPACE ${PROJECT_NAME}:: + DESTINATION lib/cmake/${PROJECT_NAME} + COMPONENT ${PROJECT_NAME}) + +# Generate the omathConfigVersion.cmake file +write_basic_package_version_file("${CMAKE_CURRENT_BINARY_DIR}/omathConfigVersion.cmake" + VERSION ${PROJECT_VERSION} COMPATIBILITY AnyNewerVersion) + +# Generate the omathConfig.cmake file from the template (which is in the cmake/ +# folder) +configure_package_config_file( + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/omathConfig.cmake.in" # Path to the .in + # file + "${CMAKE_CURRENT_BINARY_DIR}/omathConfig.cmake" # Output path for the + # generated file + INSTALL_DESTINATION lib/cmake/${PROJECT_NAME}) + +# Install the generated config files +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/omathConfig.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/omathConfigVersion.cmake" + DESTINATION lib/cmake/${PROJECT_NAME}) diff --git a/CMakePresets.json b/CMakePresets.json index 782b622c..5efed251 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -1,14 +1,48 @@ { - "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": { + "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_C_COMPILER": "cl.exe", "CMAKE_CXX_COMPILER": "cl.exe" }, "condition": { @@ -18,51 +52,540 @@ } }, { - "name": "x64-debug", - "displayName": "x64 Debug", - "inherits": "windows-base", + "name": "windows-vcpkg-base", + "hidden": true, + "inherits": ["windows-base", "vcpkg-base"], + "cacheVariables": { + "VCPKG_MANIFEST_FEATURES": "tests;imgui;avx2;examples" + } + }, + { + "name": "windows-debug", + "displayName": "Windows Debug", + "inherits": ["windows-base", "debug"] + }, + { + "name": "windows-release", + "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": "x64", + "value": "x86", "strategy": "external" }, "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug" + "VCPKG_TARGET_TRIPLET": "x86-windows", + "VCPKG_HOST_TRIPLET": "x64-windows", + "VCPKG_MANIFEST_FEATURES": "tests;imgui;avx2;examples" } }, { - "name": "x64-release", - "displayName": "x64 Release", - "inherits": "x64-debug", + "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": { - "CMAKE_BUILD_TYPE": "Release" + "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, + "inherits": "base", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + } + }, + { + "name": "linux-vcpkg-base", + "hidden": true, + "inherits": ["linux-base", "vcpkg-base"], + "cacheVariables": { + "VCPKG_MANIFEST_FEATURES": "tests;imgui;avx2" } }, { - "name": "x86-debug", - "displayName": "x86 Debug", - "inherits": "windows-base", + "name": "linux-debug", + "displayName": "Linux Debug", + "inherits": ["linux-base", "debug"] + }, + { + "name": "linux-release", + "displayName": "Linux 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 (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": "Debug" + "CMAKE_C_FLAGS": "-m32", + "CMAKE_CXX_FLAGS": "-m32", + "VCPKG_TARGET_TRIPLET": "x86-linux", + "VCPKG_HOST_TRIPLET": "x64-linux", + "VCPKG_MANIFEST_FEATURES": "tests;imgui" } }, { - "name": "x86-release", - "displayName": "x86 Release", - "inherits": "x86-debug", + "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": { - "CMAKE_BUILD_TYPE": "Release" + "VCPKG_TARGET_TRIPLET": "arm64-linux", + "VCPKG_HOST_TRIPLET": "arm64-linux", + "VCPKG_MANIFEST_FEATURES": "tests;imgui" } }, { - "name": "linux-base", + "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++" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + } + }, + { + "name": "darwin-vcpkg-base", + "hidden": true, + "inherits": ["darwin-base", "vcpkg-base"], + "cacheVariables": { + "VCPKG_MANIFEST_FEATURES": "tests;imgui;avx2;examples" + } + }, + { + "name": "darwin-debug", + "displayName": "macOS Debug", + "inherits": ["darwin-base", "debug"] + }, + { + "name": "darwin-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": "macOS Release (vcpkg)", + "inherits": ["darwin-vcpkg-base", "release"] + }, + + { + "name": "darwin-x64-vcpkg-base", + "hidden": true, + "inherits": ["darwin-base", "vcpkg-base"], + "cacheVariables": { + "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++" @@ -70,24 +593,94 @@ "condition": { "type": "equals", "lhs": "${hostSystemName}", - "rhs": "Linux" + "rhs": "Windows" } }, { - "name": "linux-debug", - "displayName": "Linux Debug", - "inherits": "linux-base", + "name": "mingw-vcpkg-base", + "hidden": true, + "inherits": ["mingw-base", "vcpkg-base"], + "environment": { + "VCPKG_DEFAULT_HOST_TRIPLET": "x64-mingw-dynamic" + }, "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug" + "VCPKG_TARGET_TRIPLET": "x64-mingw-dynamic", + "VCPKG_HOST_TRIPLET": "x64-mingw-dynamic", + "VCPKG_MANIFEST_FEATURES": "tests;imgui" } }, { - "name": "linux-release", - "displayName": "Linux Release", - "inherits": "linux-debug", + "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_BUILD_TYPE": "Release" + "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"] } ] -} \ No newline at end of file +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..af61241c --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,201 @@ +# UNIVERSAL DECLARATION OF CODE OF CONDUCT +_Declaration of Community Rights and Responsibilities_ + +## Preamble + +Whereas the Orange++ community is founded on cooperation, mutual respect and support for the development of open-source software; + +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; + +Whereas the dignity and equality of all participants, regardless of their traits or background, must be respected and protected; + +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. + +--- + +## Article 1 + +This Code of Conduct establishes standards of behaviour intended to: + +1. Provide a safe and welcoming environment for all participants. +2. Encourage respectful and constructive collaboration. +3. Prevent harassment, discrimination, and other harmful conduct. + +All individuals who participate in Orange++ projects or official communities, whether online or offline, are expected to adhere to this Code of Conduct. + +--- + +## 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** + +- `orange-cpp@yandex.ru` + +**Discord** + +- `@orange_cpp` + +**Telegram** + +- `@orange_cpp` + +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. + +--- + +## Article 8 + +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. + +**1. Correction / Edit** + +Where a message is misleading, poorly worded, or likely to cause misunderstanding, members may: + +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. + +**2. Warning / Deletion** + +Where a message is inappropriate or in breach of the standards, members may: + +1. Issue a public or private warning; and/or +2. Delete the message. + +**3. Mute / Temporary Ban** + +Where a participant is repeatedly violating the standards, or where their behaviour is significantly disruptive, members may: + +1. Temporarily mute the participant; or +2. Temporarily suspend or ban the participant from the community. + +**4. Permanent Ban** + +Where a message is hateful or severely disruptive, or where less serious infractions are repeated despite prior measures, members may permanently ban the participant. + +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. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..4c9fa196 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,32 @@ +## 🤝 Contributing to OMath or other Orange's Projects + +### ❕ Prerequisites + +- A working up-to-date OMath installation +- C++ knowledge +- Git knowledge +- Ability to ask for help (Feel free to create empty pull-request or PM a maintainer + in [Telegram](https://t.me/orange_cpp)) + +### ⏬ Setting up OMath + +Please read INSTALL.md file in repository + +### 🔀 Pull requests and Branches + +In order to send code back to the official OMath repository, you must first create a copy of OMath on your github +account ([fork](https://help.github.com/articles/creating-a-pull-request-from-a-fork/)) and +then [create a pull request](https://help.github.com/articles/creating-a-pull-request-from-a-fork/) back to OMath. + +OMath development is performed on multiple branches. Changes are then pull requested into master. By default, changes +merged into master will not roll out to stable build users unless the `stable` tag is updated. + +### 📜 Code-Style + +The orange code-style can be found in `.clang-format`. + +### 📦 Building + +OMath has already created the `cmake-build` and `out` directories where cmake/bin files are located. By default, you +can build OMath by running `cmake --build cmake-build/build/windows-release --target omath -j 6` in the source +directory. diff --git a/CREDITS.md b/CREDITS.md new file mode 100644 index 00000000..b8719f74 --- /dev/null +++ b/CREDITS.md @@ -0,0 +1,13 @@ +# OMATH CREDITS + +Thanks to everyone who made this possible, including: + +- Saikari aka luadebug for VCPKG port and awesome new initial logo design. +- AmbushedRaccoon for telegram post about omath to boost repository activity. +- Billy O'Neal aka BillyONeal for fixing compilation issues due to C math library compatibility. + +And a big hand to everyone else who has contributed over the past! + +THANKS! <3 + + -- Orange++ diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 00000000..0cbcadee --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,68 @@ +# 📥Installation Guide + +## Using vcpkg +**Note**: Support vcpkg for package management +1. Install [vcpkg](https://github.com/microsoft/vcpkg) +2. Run the following command to install the orange-math package: +``` +vcpkg install orange-math +``` +CMakeLists.txt +```cmake +find_package(omath CONFIG REQUIRED) +target_link_libraries(main PRIVATE omath::omath) +``` +For detailed commands on installing different versions and more information, please refer to Microsoft's [official instructions](https://learn.microsoft.com/en-us/vcpkg/get_started/overview). + +## Using xrepo +**Note**: Support xrepo for package management +1. Install [xmake](https://xmake.io/) +2. Run the following command to install the omath package: +``` +xrepo install omath +``` +xmake.lua +```xmake +add_requires("omath") +target("...") + add_packages("omath") +``` + +## Build from source using CMake +1. **Preparation** + + Install needed tools: cmake, clang, git, msvc (windows only). + + 1. **Linux:** + ```bash + sudo pacman -Sy cmake ninja clang git + ``` + 2. **MacOS:** + ```bash + brew install llvm git cmake ninja + ``` + 3. **Windows:** + + Install Visual Studio from [here](https://visualstudio.microsoft.com/downloads/) and Git from [here](https://git-scm.com/downloads). + + Use x64 Native Tools shell to execute needed commands down below. +2. **Clone the repository:** + ```bash + git clone https://github.com/orange-cpp/omath.git + ``` +3. **Navigate to the project directory:** + ```bash + cd omath + ``` +4. **Build the project using CMake:** + ```bash + cmake --preset windows-release -S . + cmake --build cmake-build/build/windows-release --target omath -j 6 + ``` + Use **\-\** preset to build suitable version for yourself. Like **windows-release** or **linux-release**. + + | Platform Name | Build Config | + |---------------|---------------| + | windows | release/debug | + | linux | release/debug | + | darwin | release/debug | diff --git a/LICENSE b/LICENSE index f288702d..5d5e9128 100644 --- a/LICENSE +++ b/LICENSE @@ -1,674 +1,17 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. +Copyright (C) 2023-2026 Orange++ orange_github@proton.me + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. diff --git a/README.md b/README.md new file mode 100644 index 00000000..0a9c5016 --- /dev/null +++ b/README.md @@ -0,0 +1,151 @@ +
+ +![banner](docs/images/logos/omath_logo_macro.png) + +![GitHub License](https://img.shields.io/github/license/orange-cpp/omath) +![GitHub contributors](https://img.shields.io/github/contributors/orange-cpp/omath) +![GitHub top language](https://img.shields.io/github/languages/top/orange-cpp/omath) +![GitHub repo size](https://img.shields.io/github/repo-size/orange-cpp/omath) +[![CodeFactor](https://www.codefactor.io/repository/github/orange-cpp/omath/badge)](https://www.codefactor.io/repository/github/orange-cpp/omath) +![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/orange-cpp/omath/cmake-multi-platform.yml) +[![Vcpkg package](https://repology.org/badge/version-for-repo/vcpkg/orange-math.svg)](https://repology.org/project/orange-math/versions) +![GitHub forks](https://img.shields.io/github/forks/orange-cpp/omath) +[![discord badge](https://dcbadge.limes.pink/api/server/https://discord.gg/eDgdaWbqwZ?style=flat)](https://discord.gg/eDgdaWbqwZ) +[![telegram badge](https://img.shields.io/badge/Telegram-2CA5E0?style=flat-squeare&logo=telegram&logoColor=white)](https://t.me/orangennotes) + +OMath is a 100% independent, constexpr template blazingly fast math library that doesn't have legacy C++ code. + +It provides the latest features, is highly customizable, has all for cheat development, DirectX/OpenGL/Vulkan support, premade support for different game engines, much more constexpr stuff than in other libraries and more... +
+
+ +--- + +**[
 Install 
][INSTALL]**  +**[
 Examples 
][EXAMPLES]**  +**[
 Documentation 
][DOCUMENTATION]**  +**[
 Contribute 
][CONTRIBUTING]**  +**[
 Donate 
][SPONSOR]**  + +--- + +
+ +
+ + + + +## Quick Example + +```cpp +#include + +using namespace omath; + +// 3D vector operations +Vector3 a{1, 2, 3}; +Vector3 b{4, 5, 6}; + +auto dot = a.dot(b); // 32.0 +auto cross = a.cross(b); // (-3, 6, -3) +auto distance = a.distance_to(b); // ~5.196 +auto normalized = a.normalized(); // Unit vector + +// World-to-screen projection (Source Engine example) +using namespace omath::source_engine; +Camera camera(position, angles, viewport, fov, near_plane, far_plane); + +if (auto screen = camera.world_to_screen(world_position)) { + // Draw at screen->x, screen->y +} +``` + +**[See more examples and tutorials][TUTORIALS]** + +# Features +- **Efficiency**: Optimized for performance, ensuring quick computations using AVX2. +- **Versatility**: Includes a wide array of mathematical functions and algorithms. +- **Ease of Use**: Simplified interface for convenient integration into various projects. +- **Projectile Prediction**: Projectile prediction engine with O(N) algo complexity, that can power you projectile aim-bot. +- **3D Projection**: No need to find view-projection matrix anymore you can make your own projection pipeline. +- **Collision Detection**: Production ready code to handle collision detection by using simple interfaces. +- **No Additional Dependencies**: No additional dependencies need to use OMath except unit test execution +- **Ready for meta-programming**: Omath use templates for common types like Vectors, Matrixes etc, to handle all types! +- **Engine support**: Supports coordinate systems of **Source, Unity, Unreal, Frostbite, IWEngine and canonical OpenGL**. +- **Cross platform**: Supports Windows, MacOS and Linux. +- **Algorithms**: Has ability to scan for byte pattern with wildcards in PE files/modules, binary slices, works even with Wine apps. +
+ +# Gallery + +
+ +[![Youtube Video](docs/images/yt_previews/img.png)](https://youtu.be/lM_NJ1yCunw?si=-Qf5yzDcWbaxAXGQ) + +
+ +![APEX Preview] + +
+ +![BO2 Preview] + +
+ +![CS2 Preview] + +
+ +![TF2 Preview] + +
+ +![OpenGL Preview] + +
+
+ +
+ +## Documentation + +- **[Getting Started Guide](http://libomath.org/getting_started/)** - Installation and first steps +- **[API Overview](http://libomath.org/api_overview/)** - Complete API reference +- **[Tutorials](http://libomath.org/tutorials/)** - Step-by-step guides +- **[FAQ](http://libomath.org/faq/)** - Common questions and answers +- **[Troubleshooting](http://libomath.org/troubleshooting/)** - Solutions to common issues +- **[Best Practices](http://libomath.org/best_practices/)** - Guidelines for effective usage + +## Community & Support + +- **Discord**: [Join our community](https://discord.gg/eDgdaWbqwZ) +- **Telegram**: [@orangennotes](https://t.me/orangennotes) +- **Issues**: [Report bugs or request features](https://github.com/orange-cpp/omath/issues) +- **Contributing**: See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines + +# Acknowledgments +- [All contributors](https://github.com/orange-cpp/omath/graphs/contributors) + + +[APEX Preview]: docs/images/showcase/apex.png +[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 + +[QUICKSTART]: docs/getting_started.md +[INSTALL]: INSTALL.md +[DOCUMENTATION]: http://libomath.org +[TUTORIALS]: docs/tutorials.md +[CONTRIBUTING]: CONTRIBUTING.md +[EXAMPLES]: examples +[SPONSOR]: https://boosty.to/orangecpp/purchase/3568644?ssource=DIRECT&share=subscription_link diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..c946db88 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +# Security Policy + +## Reporting a Vulnerability + +Please report security issues to `orange_github@proton.me` diff --git a/VERSION b/VERSION new file mode 100644 index 00000000..cfacfe40 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +4.7.1 \ No newline at end of file diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt new file mode 100644 index 00000000..29a2b1ae --- /dev/null +++ b/benchmark/CMakeLists.txt @@ -0,0 +1,24 @@ +project(omath_benchmark) + +file(GLOB_RECURSE OMATH_BENCHMARK_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp") +add_executable(${PROJECT_NAME} ${OMATH_BENCHMARK_SOURCES}) + +set_target_properties( + ${PROJECT_NAME} + PROPERTIES 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}" + CXX_STANDARD 23 + CXX_STANDARD_REQUIRED ON) + +if(TARGET benchmark::benchmark) # Benchmark is being linked as submodule + target_link_libraries(${PROJECT_NAME} PRIVATE benchmark::benchmark omath) +else() + find_package(benchmark CONFIG REQUIRED) # Benchmark is being linked as vcpkg + # package + target_link_libraries(${PROJECT_NAME} PRIVATE benchmark::benchmark omath) +endif() + +if(OMATH_ENABLE_VALGRIND) + omath_setup_valgrind(${PROJECT_NAME}) +endif() diff --git a/benchmark/benchmark_mat.cpp b/benchmark/benchmark_mat.cpp new file mode 100644 index 00000000..2e543821 --- /dev/null +++ b/benchmark/benchmark_mat.cpp @@ -0,0 +1,65 @@ +// +// Created by Vlad on 9/17/2025. +// +#include + +#include +using namespace omath; + + +void mat_float_multiplication_col_major(benchmark::State& state) +{ + using MatType = Mat<128, 128, float, MatStoreType::COLUMN_MAJOR>; + MatType a; + MatType b; + a.set(3.f); + b.set(7.f); + + + for ([[maybe_unused]] const auto _ : state) + std::ignore = a * b; +} +void mat_float_multiplication_row_major(benchmark::State& state) +{ + using MatType = Mat<128, 128, float, MatStoreType::ROW_MAJOR>; + MatType a; + MatType b; + a.set(3.f); + b.set(7.f); + + + for ([[maybe_unused]] const auto _ : state) + std::ignore = a * b; +} + +void mat_double_multiplication_row_major(benchmark::State& state) +{ + using MatType = Mat<128, 128, double, MatStoreType::ROW_MAJOR>; + MatType a; + MatType b; + a.set(3.f); + b.set(7.f); + + + for ([[maybe_unused]] const auto _ : state) + std::ignore = a * b; +} + +void mat_double_multiplication_col_major(benchmark::State& state) +{ + using MatType = Mat<128, 128, double, MatStoreType::COLUMN_MAJOR>; + MatType a; + MatType b; + a.set(3.f); + b.set(7.f); + + + for ([[maybe_unused]] const auto _ : state) + std::ignore = a * b; +} + +BENCHMARK(mat_float_multiplication_col_major)->Iterations(5000); +BENCHMARK(mat_float_multiplication_row_major)->Iterations(5000); + +BENCHMARK(mat_double_multiplication_col_major)->Iterations(5000); +BENCHMARK(mat_double_multiplication_row_major)->Iterations(5000); \ No newline at end of file diff --git a/benchmark/benchmark_projectile_pred.cpp b/benchmark/benchmark_projectile_pred.cpp new file mode 100644 index 00000000..a6337547 --- /dev/null +++ b/benchmark/benchmark_projectile_pred.cpp @@ -0,0 +1,23 @@ +// +// Created by Vlad on 9/18/2025. +// +#include +#include +using namespace omath; + +using namespace omath::projectile_prediction; + +constexpr float simulation_time_step = 1.f / 1000.f; +constexpr float hit_distance_tolerance = 5.f; + +void source_engine_projectile_prediction(benchmark::State& state) +{ + constexpr Target target{.m_origin = {100, 0, 90}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; + constexpr Projectile projectile = {.m_origin = {3, 2, 1}, .m_launch_speed = 5000, .m_gravity_scale = 0.4}; + + for ([[maybe_unused]] const auto _: state) + std::ignore = ProjPredEngineLegacy(400, simulation_time_step, 50, hit_distance_tolerance) + .maybe_calculate_aim_point(projectile, target); +} + +BENCHMARK(source_engine_projectile_prediction)->Iterations(10'000); \ No newline at end of file diff --git a/benchmark/main.cpp b/benchmark/main.cpp new file mode 100644 index 00000000..790aeaea --- /dev/null +++ b/benchmark/main.cpp @@ -0,0 +1,5 @@ +// +// Created by Vlad on 9/17/2025. +// +#include +BENCHMARK_MAIN(); \ No newline at end of file diff --git a/cmake/Coverage.cmake b/cmake/Coverage.cmake new file mode 100644 index 00000000..57914096 --- /dev/null +++ b/cmake/Coverage.cmake @@ -0,0 +1,67 @@ +# 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" AND MSVC) + target_compile_options(${TARGET_NAME} PRIVATE -fprofile-instr-generate -fcoverage-mapping + /Zi) + target_link_options( + ${TARGET_NAME} + PRIVATE + -fprofile-instr-generate + -fcoverage-mapping + /DEBUG:FULL + /INCREMENTAL:NO + /PROFILE) + elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang|AppleClang") + 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) + endif() + + if(TARGET coverage) + return() + endif() + + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND MSVC) + message(STATUS "MSVC detected: Use VS Code Coverage from CI workflow") + add_custom_target( + coverage + DEPENDS unit_tests + COMMAND $ + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + COMMENT "Running tests for coverage (use VS Code Coverage from CI)") + + elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang|AppleClang") + add_custom_target( + coverage + DEPENDS unit_tests + COMMAND bash "${CMAKE_SOURCE_DIR}/scripts/coverage-llvm.sh" "${CMAKE_SOURCE_DIR}" + "${CMAKE_BINARY_DIR}" "$" "${CMAKE_BINARY_DIR}/coverage" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + COMMENT "Running LLVM coverage") + + elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + add_custom_target( + coverage + DEPENDS unit_tests + COMMAND $ || 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() diff --git a/cmake/Valgrind.cmake b/cmake/Valgrind.cmake new file mode 100644 index 00000000..bab197e4 --- /dev/null +++ b/cmake/Valgrind.cmake @@ -0,0 +1,41 @@ +# cmake/Valgrind.cmake + +if(DEFINED __OMATH_VALGRIND_INCLUDED) + return() +endif() +set(__OMATH_VALGRIND_INCLUDED TRUE) + +find_program(VALGRIND_EXECUTABLE valgrind) +option(OMATH_ENABLE_VALGRIND "Enable Valgrind target for memory checking" ON) + +if(OMATH_ENABLE_VALGRIND AND NOT TARGET valgrind_all) + add_custom_target(valgrind_all) +endif() + +function(omath_setup_valgrind TARGET_NAME) + if(NOT OMATH_ENABLE_VALGRIND) + return() + endif() + + if(NOT VALGRIND_EXECUTABLE) + message(WARNING "OMATH_ENABLE_VALGRIND is ON, but 'valgrind' executable was not found.") + return() + endif() + + set(VALGRIND_FLAGS --leak-check=full --show-leak-kinds=all --track-origins=yes + --error-exitcode=99) + + set(VALGRIND_TARGET "valgrind_${TARGET_NAME}") + + if(NOT TARGET ${VALGRIND_TARGET}) + add_custom_target( + ${VALGRIND_TARGET} + DEPENDS ${TARGET_NAME} + COMMAND ${VALGRIND_EXECUTABLE} ${VALGRIND_FLAGS} $ + WORKING_DIRECTORY $ + COMMENT "Running Valgrind memory check on ${TARGET_NAME}..." + USES_TERMINAL) + + add_dependencies(valgrind_all ${VALGRIND_TARGET}) + endif() +endfunction() diff --git a/cmake/omathConfig.cmake.in b/cmake/omathConfig.cmake.in new file mode 100644 index 00000000..1e3c774b --- /dev/null +++ b/cmake/omathConfig.cmake.in @@ -0,0 +1,11 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) + +if (@OMATH_IMGUI_INTEGRATION@) + find_dependency(imgui CONFIG) +endif() + +# Load the targets for the omath library +include("${CMAKE_CURRENT_LIST_DIR}/omathTargets.cmake") +check_required_components(omath) diff --git a/docs/3d_primitives/box.md b/docs/3d_primitives/box.md new file mode 100644 index 00000000..84e392d6 --- /dev/null +++ b/docs/3d_primitives/box.md @@ -0,0 +1,118 @@ +# `omath::primitives::create_box` — Build an oriented box as 12 triangles + +> Header: your project’s `primitives/box.hpp` (declares `create_box`) +> Namespace: `omath::primitives` +> Depends on: `omath::Triangle>`, `omath::Vector3` + +```cpp +[[nodiscard]] +std::array>, 12> +create_box(const Vector3& top, + const Vector3& bottom, + const Vector3& dir_forward, + const Vector3& dir_right, + float ratio = 4.f) noexcept; +``` + +--- + +## What it does + +Constructs a **rectangular cuboid (“box”)** oriented in 3D space and returns its surface as **12 triangles** (2 per face × 6 faces). The box’s central axis runs from `bottom` → `top`. The **up** direction is inferred from that segment; the **forward** and **right** directions define the box’s orientation around that axis. + +The lateral half-extents are derived from the axis length and `ratio`: + +> Let `H = |top - bottom|`. Lateral half-size ≈ `H / ratio` along both `dir_forward` and `dir_right` +> (i.e., the cross-section is a square of side `2H/ratio`). + +> **Note:** This describes the intended behavior from the interface. If you rely on a different sizing rule, document it next to your implementation. + +--- + +## Parameters + +* `top` + Center of the **top face**. + +* `bottom` + Center of the **bottom face**. + +* `dir_forward` + A direction that orients the box around its up axis. Should be **non-zero** and **not collinear** with `top - bottom`. + +* `dir_right` + A direction roughly orthogonal to both `dir_forward` and `top - bottom`. Used to fully fix orientation. + +* `ratio` (default `4.0f`) + Controls thickness relative to height. Larger values → thinner box. + With the default rule above, half-extent = `|top-bottom|/ratio`. + +--- + +## Return value + +`std::array>, 12>` — the six faces of the box, triangulated. +Winding is intended to be **outward-facing** (right-handed coordinates). Do not rely on a specific **face ordering**; treat the array as opaque unless your implementation guarantees an order. + +--- + +## Expected math & robustness + +* Define `u = normalize(top - bottom)`. +* Re-orthonormalize the basis to avoid skew: + + ```cpp + f = normalize(dir_forward - u * u.dot(dir_forward)); // drop any up component + r = normalize(u.cross(f)); // right-handed basis + // (Optionally recompute f = r.cross(u) for orthogonality) + ``` +* Half-extents: `h = length(top - bottom) / ratio; hf = h * f; hr = h * r`. +* Corners (top): `t±r±f = top ± hr ± hf`; (bottom): `b±r±f = bottom ± hr ± hf`. +* Triangulate each face with consistent CCW winding when viewed from outside. + +--- + +## Example + +```cpp +using omath::Vector3; +using omath::Triangle; +using omath::primitives::create_box; + +// Axis from bottom to top (height 2) +Vector3 bottom{0, 0, 0}; +Vector3 top {0, 2, 0}; + +// Orientation around the axis +Vector3 forward{0, 0, 1}; +Vector3 right {1, 0, 0}; + +// Ratio 4 → lateral half-size = height/4 = 0.5 +auto tris = create_box(top, bottom, forward, right, 4.0f); + +// Use the triangles (normals, rendering, collision, etc.) +for (const auto& tri : tris) { + auto n = tri.calculate_normal(); + (void)n; +} +``` + +--- + +## Usage notes & pitfalls + +* **Degenerate axis**: If `top == bottom`, the box is undefined (zero height). Guard against this. +* **Directions**: Provide **non-zero**, **reasonably orthogonal** `dir_forward`/`dir_right`. A robust implementation should project/normalize internally, but callers should still pass sensible inputs. +* **Winding**: If your renderer or collision expects a specific winding, verify with a unit test and flip vertex order per face if necessary. +* **Thickness policy**: This doc assumes both lateral half-extents equal `|top-bottom|/ratio`. If your implementation diverges (e.g., separate forward/right ratios), document it. + +--- + +## See also + +* `omath::Triangle` (vertex utilities: normals, centroid, etc.) +* `omath::Vector3` (geometry operations used by the construction) + +--- + +*Last updated: 31 Oct 2025* diff --git a/docs/3d_primitives/mesh.md b/docs/3d_primitives/mesh.md new file mode 100644 index 00000000..803aaef8 --- /dev/null +++ b/docs/3d_primitives/mesh.md @@ -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`, `omath::Mat4X4`, `omath::Triangle>` +> 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 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; + +// Unity Engine +using Mesh = omath::primitives::Mesh; + +// Unreal Engine +using Mesh = omath::primitives::Mesh; + +// 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> m_vertex_buffer; // VBO: vertex positions +std::vector> 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> vbo, + std::vector> vao, + Vector3 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> vertices = { + {0, 0, 0}, {1, 0, 0}, {0, 1, 0}, {0, 0, 1} +}; + +std::vector> 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& new_origin); +void set_scale(const Vector3& 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& get_origin() const; +[[nodiscard]] const Vector3& 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 vertex_to_world_space(const Vector3& 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 local_vertex{1, 0, 0}; +Vector3 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> make_face_in_world_space( + const std::vector>::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> 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> 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> 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>::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> 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* diff --git a/docs/3d_primitives/plane.md b/docs/3d_primitives/plane.md new file mode 100644 index 00000000..c4619a3a --- /dev/null +++ b/docs/3d_primitives/plane.md @@ -0,0 +1,98 @@ +# `omath::primitives::create_plane` — Build an oriented quad (2 triangles) + +> Header: your project’s `primitives/plane.hpp` +> Namespace: `omath::primitives` +> Depends on: `omath::Triangle>`, `omath::Vector3` + +```cpp +[[nodiscard]] +std::array>, 2> +create_plane(const Vector3& vertex_a, + const Vector3& vertex_b, + const Vector3& direction, + float size) noexcept; +``` + +--- + +## What it does + +Creates a **rectangle (quad)** in 3D oriented by the edge **A→B** and a second in-plane **direction**. The quad is returned as **two triangles** suitable for rendering or collision. + +* Edge axis: `e = vertex_b - vertex_a` +* Width axis: “direction”, **projected to be perpendicular to `e`** so the quad is planar and well-formed. +* Normal (by right-hand rule): `n ∝ e × width`. + +> **Sizing convention** +> Typical construction uses **half-width = `size`** along the (normalized, orthogonalized) *direction*, i.e. the total width is `2*size`. +> If your implementation interprets `size` as full width, adjust your expectations accordingly. + +--- + +## Parameters + +* `vertex_a`, `vertex_b` — two adjacent quad vertices defining the **long edge** of the plane. +* `direction` — a vector indicating the **cross-edge direction** within the plane (does not need to be orthogonal or normalized). +* `size` — **half-width** of the quad along the (processed) `direction`. + +--- + +## Return + +`std::array>, 2>` — the quad triangulated (consistent CCW winding, outward normal per `e × width`). + +--- + +## Robust construction (expected math) + +1. `e = vertex_b - vertex_a` +2. Make `d` perpendicular to `e`: + + ``` + d = direction - e * (e.dot(direction) / e.length_sqr()); + if (d.length_sqr() == 0) pick an arbitrary perpendicular to e + d = d.normalized(); + ``` +3. Offsets: `w = d * size` +4. Four corners: + + ``` + A0 = vertex_a - w; A1 = vertex_a + w; + B0 = vertex_b - w; B1 = vertex_b + w; + ``` +5. Triangles (CCW when viewed from +normal): + + ``` + T0 = Triangle{ A0, A1, B1 } + T1 = Triangle{ A0, B1, B0 } + ``` + +--- + +## Example + +```cpp +using omath::Vector3; +using omath::Triangle; +using omath::primitives::create_plane; + +Vector3 a{ -1, 0, -1 }; // edge start +Vector3 b{ 1, 0, -1 }; // edge end +Vector3 dir{ 0, 0, 1 }; // cross-edge direction within the plane (roughly +Z) +float half_width = 2.0f; + +auto quad = create_plane(a, b, dir, half_width); + +// e.g., compute normals +for (const auto& tri : quad) { + auto n = tri.calculate_normal(); (void)n; +} +``` + +--- + +## Notes & edge cases + +* **Degenerate edge**: if `vertex_a == vertex_b`, the plane is undefined. +* **Collinearity**: if `direction` is parallel to `vertex_b - vertex_a`, the function must choose an alternate perpendicular; expect a fallback. +* **Winding**: If your renderer expects a specific face order, verify and swap the two vertices in each triangle as needed. diff --git a/docs/api_overview.md b/docs/api_overview.md new file mode 100644 index 00000000..9d0b3257 --- /dev/null +++ b/docs/api_overview.md @@ -0,0 +1,577 @@ +# API Overview + +This document provides a high-level overview of OMath's API, organized by functionality area. + +--- + +## Module Organization + +OMath is organized into several logical modules: + +### Core Mathematics +- **Linear Algebra** - Vectors, matrices, triangles +- **Trigonometry** - Angles, view angles, trigonometric functions +- **3D Primitives** - Boxes, planes, meshes, geometric shapes + +### Game Development +- **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 + +### Engine Support +- **Source Engine** - Valve's Source Engine (CS:GO, TF2, etc.) +- **Unity Engine** - Unity game engine +- **Unreal Engine** - Epic's Unreal Engine +- **Frostbite Engine** - EA's Frostbite Engine +- **IW Engine** - Infinity Ward's engine (Call of Duty) +- **OpenGL Engine** - Canonical OpenGL coordinate system + +### Utilities +- **Color** - RGBA color representation +- **Pattern Scanning** - Memory pattern search (wildcards, PE files) +- **Reverse Engineering** - Internal/external memory manipulation + +--- + +## Core Types + +### Vectors + +All vector types are template-based and support arithmetic types. + +| Type | Description | Key Methods | +|------|-------------|-------------| +| `Vector2` | 2D vector | `length()`, `normalized()`, `dot()`, `distance_to()` | +| `Vector3` | 3D vector | `length()`, `normalized()`, `dot()`, `cross()`, `angle_between()` | +| `Vector4` | 4D vector | Extends Vector3 with `w` component | + +**Common aliases:** +```cpp +using Vec2f = Vector2; +using Vec3f = Vector3; +using Vec4f = Vector4; +``` + +**Key features:** +- Component-wise arithmetic (+, -, *, /) +- Scalar multiplication/division +- Dot and cross products +- Safe normalization (returns original if length is zero) +- Distance calculations +- Angle calculations with error handling +- Hash support for `float` variants +- `std::formatter` support + +### Matrices + +| Type | Description | Key Methods | +|------|-------------|-------------| +| `Mat4X4` | 4×4 matrix | `identity()`, `transpose()`, `determinant()`, `inverse()` | + +**Use cases:** +- Transformation matrices +- View matrices +- Projection matrices +- Model-view-projection pipelines + +### Angles + +Strong-typed angle system with compile-time range enforcement: + +| Type | Range | Description | +|------|-------|-------------| +| `Angle` | Custom | Generic angle type with bounds | +| `PitchAngle` | [-89°, 89°] | Vertical camera rotation | +| `YawAngle` | [-180°, 180°] | Horizontal camera rotation | +| `RollAngle` | [-180°, 180°] | Camera roll | +| `ViewAngles` | - | Composite pitch/yaw/roll | + +**Features:** +- Automatic normalization/clamping based on flags +- Conversions between degrees and radians +- Type-safe arithmetic +- Prevents common angle bugs + +--- + +## Projection System + +### Camera + +Generic camera template that works with any engine trait: + +```cpp +template +class Camera; +``` + +**Engine-specific cameras:** +```cpp +omath::source_engine::Camera // Source Engine +omath::unity_engine::Camera // Unity +omath::unreal_engine::Camera // Unreal +omath::frostbite_engine::Camera // Frostbite +omath::iw_engine::Camera // IW Engine +omath::opengl_engine::Camera // OpenGL +``` + +**Core methods:** +- `world_to_screen(Vector3)` - Project 3D point to 2D screen +- `get_view_matrix()` - Get current view matrix +- `get_projection_matrix()` - Get current projection matrix +- `update(position, angles)` - Update camera state + +**Supporting types:** +- `ViewPort` - Screen dimensions and aspect ratio +- `FieldOfView` - FOV in degrees with validation +- `ProjectionError` - Error codes for projection failures + +--- + +## 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 GjkAlgorithm; + + template + 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` | 1-4 point geometric simplex | Fixed capacity, GJK iteration support | +| `MeshCollider` | Convex mesh collider | Support function for GJK/EPA | +| `GjkHitInfo` | Collision result | Hit flag and simplex | +| `Epa::Result` | Penetration info | Depth, normal, iteration count | + +### LineTracer + +Ray-casting and line tracing utilities: + +```cpp +namespace omath::collision { + class LineTracer; +} +``` + +**Features:** +- Ray-triangle intersection (Möller-Trumbore algorithm) +- Ray-plane intersection +- Ray-box intersection +- Distance calculations +- Normal calculations at hit points + +### 3D Primitives + +| Type | Description | Key Methods | +|------|-------------|-------------| +| `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 + +--- + +## Projectile Prediction + +### Interfaces + +**`ProjPredEngineInterface`** - Base interface for all prediction engines + +```cpp +virtual std::optional> +maybe_calculate_aim_point(const Projectile&, const Target&) const = 0; +``` + +### Implementations + +| Engine | Description | Optimizations | +|--------|-------------|---------------| +| `ProjPredEngineLegacy` | Standard implementation | Portable, works everywhere | +| `ProjPredEngineAVX2` | AVX2 optimized | 2-4x faster on modern CPUs | + +### Supporting Types + +**`Projectile`** - Defines projectile properties: +```cpp +struct Projectile { + Vector3 origin; + float speed; + Vector3 gravity; + // ... additional properties +}; +``` + +**`Target`** - Defines target state: +```cpp +struct Target { + Vector3 position; + Vector3 velocity; + // ... additional properties +}; +``` + +--- + +## Pathfinding + +### A* Algorithm + +```cpp +namespace omath::pathfinding { + template + class AStar; +} +``` + +**Features:** +- Generic node type support +- Customizable heuristics +- Efficient priority queue implementation +- Path reconstruction + +### Navigation Mesh + +```cpp +namespace omath::pathfinding { + class NavigationMesh; +} +``` + +**Features:** +- Triangle-based navigation +- Neighbor connectivity +- Walkable area definitions + +--- + +## Engine Traits + +Each game engine has a trait system providing engine-specific math: + +### CameraTrait + +Implements camera math for an engine: +- `calc_look_at_angle()` - Calculate angles to look at a point +- `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: +- Gravity vectors +- Coordinate system conventions +- Unit conversions +- Physics parameters + +### Available Traits + +| Engine | Camera Trait | Mesh Trait | Pred Engine Trait | Constants | Formulas | +|--------|--------------|------------|-------------------|-----------|----------| +| Source Engine | ✓ | ✓ | ✓ | ✓ | ✓ | +| Unity Engine | ✓ | ✓ | ✓ | ✓ | ✓ | +| Unreal Engine | ✓ | ✓ | ✓ | ✓ | ✓ | +| Frostbite | ✓ | ✓ | ✓ | ✓ | ✓ | +| IW Engine | ✓ | ✓ | ✓ | ✓ | ✓ | +| OpenGL | ✓ | ✓ | ✓ | ✓ | ✓ | + +**Documentation:** +- See `docs/engines//` for detailed per-engine docs +- Each engine has separate docs for camera_trait, mesh_trait, pred_engine_trait, constants, and formulas + +--- + +## Utility Functions + +### Color + +```cpp +struct Color { + uint8_t r, g, b, a; + + // Conversions + static Color from_hsv(float h, float s, float v); + static Color from_hex(uint32_t hex); + uint32_t to_hex() const; + + // Blending + Color blend(const Color& other, float t) const; +}; +``` + +### Pattern Scanning + +**Binary pattern search with wildcards:** + +```cpp +// Pattern with wildcards (?? = any byte) +PatternView pattern{"48 8B 05 ?? ?? ?? ?? 48 85 C0"}; + +// Scan memory +auto result = pattern_scan(memory_buffer, pattern); +if (result) { + std::cout << "Found at offset: " << result->offset << "\n"; +} +``` + +**PE file scanning:** + +```cpp +PEPatternScanner scanner("target.exe"); +if (auto addr = scanner.scan_pattern(pattern)) { + std::cout << "Found at RVA: " << *addr << "\n"; +} +``` + +### Reverse Engineering + +**External memory access:** +```cpp +ExternalRevObject process("game.exe"); +Vector3 position = process.read>(address); +process.write(address, new_position); +``` + +**Internal memory access:** +```cpp +InternalRevObject memory; +auto value = memory.read(address); +memory.write(address, new_value); +``` + +--- + +## Concepts and Constraints + +OMath uses C++20 concepts for type safety: + +```cpp +template +concept Arithmetic = std::is_arithmetic_v; + +template +concept CameraEngineConcept = requires(EngineTrait t) { + { t.calc_look_at_angle(...) } -> /* returns angles */; + { t.calc_view_matrix(...) } -> /* returns matrix */; + { t.calc_projection_matrix(...) } -> /* returns matrix */; +}; +``` + +--- + +## Error Handling + +OMath uses modern C++ error handling: + +### std::expected (C++23) + +```cpp +std::expected, Vector3Error> +angle_between(const Vector3& other) const; + +if (auto angle = v1.angle_between(v2)) { + // Success: use *angle +} else { + // Error: angle.error() gives Vector3Error +} +``` + +### std::optional + +```cpp +std::optional> +world_to_screen(const Vector3& world); + +if (auto screen = camera.world_to_screen(pos)) { + // Success: use screen->x, screen->y +} else { + // Point not visible +} +``` + +### Error Codes + +```cpp +enum class ProjectionError { + SUCCESS = 0, + POINT_BEHIND_CAMERA, + INVALID_VIEWPORT, + // ... +}; +``` + +--- + +## Performance Considerations + +### constexpr Support + +Most operations are `constexpr` where possible: + +```cpp +constexpr Vector3 v{1, 2, 3}; +constexpr auto len_sq = v.length_sqr(); // Computed at compile time +``` + +### AVX2 Optimizations + +Use AVX2 variants when available: + +```cpp +// Standard: portable but slower +ProjPredEngineLegacy legacy_engine; + +// AVX2: 2-4x faster on modern CPUs +ProjPredEngineAVX2 fast_engine; +``` + +**When to use AVX2:** +- Modern Intel/AMD processors (2013+) +- Performance-critical paths +- Batch operations + +**When to use Legacy:** +- Older processors +- ARM platforms +- Guaranteed compatibility + +### Cache Efficiency + +```cpp +// Good: contiguous storage +std::vector> positions; + +// Good: structure of arrays for SIMD +struct Particles { + std::vector x, y, z; +}; +``` + +--- + +## Platform Support + +| Platform | Support | Notes | +|----------|---------|-------| +| Windows | ✓ | MSVC, Clang, GCC | +| Linux | ✓ | GCC, Clang | +| macOS | ✓ | Clang | + +**Minimum requirements:** +- C++20 compiler +- C++23 recommended for `std::expected` + +--- + +## Thread Safety + +- **Vector/Matrix types**: Thread-safe (immutable operations) +- **Camera**: Not thread-safe (mutable state) +- **Pattern scanning**: Thread-safe (read-only operations) +- **Memory access**: Depends on OS/process synchronization + +**Thread-safe example:** +```cpp +// Safe: each thread gets its own camera +std::vector threads; +for (int i = 0; i < num_threads; ++i) { + threads.emplace_back([i]() { + Camera camera = /* create camera */; + // Use camera in this thread + }); +} +``` + +--- + +## Best Practices + +### 1. Use Type Aliases + +```cpp +using Vec3f = omath::Vector3; +using Mat4 = omath::Mat4X4; +``` + +### 2. Prefer constexpr When Possible + +```cpp +constexpr auto compute_at_compile_time() { + Vector3 v{1, 2, 3}; + return v.length_sqr(); +} +``` + +### 3. Check Optional/Expected Results + +```cpp +// Good +if (auto result = camera.world_to_screen(pos)) { + use(*result); +} + +// Bad - may crash +auto result = camera.world_to_screen(pos); +use(result->x); // Undefined behavior if nullopt +``` + +### 4. Use Engine-Specific Types + +```cpp +// Good: uses correct coordinate system +using namespace omath::source_engine; +Camera camera = /* ... */; + +// Bad: mixing engine types +using UnityCamera = omath::unity_engine::Camera; +using SourceAngles = omath::source_engine::ViewAngles; +UnityCamera camera{pos, SourceAngles{}}; // Wrong! +``` + +--- + +## See Also + +- [Getting Started Guide](getting_started.md) +- [Installation Instructions](install.md) +- [Examples Directory](../examples/) +- Individual module documentation in respective folders + +--- + +*Last updated: 13 Nov 2025* diff --git a/docs/best_practices.md b/docs/best_practices.md new file mode 100644 index 00000000..d38584ff --- /dev/null +++ b/docs/best_practices.md @@ -0,0 +1,532 @@ +# Best Practices + +Guidelines for using OMath effectively and avoiding common pitfalls. + +--- + +## Code Organization + +### Use Type Aliases + +Define clear type aliases for commonly used types: + +```cpp +// Good: Clear and concise +using Vec3f = omath::Vector3; +using Vec2f = omath::Vector2; +using Mat4 = omath::Mat4X4; + +Vec3f position{1.0f, 2.0f, 3.0f}; +``` + +```cpp +// Avoid: Verbose and repetitive +omath::Vector3 position{1.0f, 2.0f, 3.0f}; +omath::Vector3 velocity{0.0f, 0.0f, 0.0f}; +``` + +### Namespace Usage + +Be selective with `using namespace`: + +```cpp +// Good: Specific namespace for your engine +using namespace omath::source_engine; + +// Good: Import specific types +using omath::Vector3; +using omath::Vector2; + +// Avoid: Too broad +using namespace omath; // Imports everything +``` + +### Include What You Use + +```cpp +// Good: Include specific headers +#include +#include + +// Okay for development +#include + +// Production: Include only what you need +// to reduce compile times +``` + +--- + +## Error Handling + +### Always Check Optional Results + +```cpp +// Good: Check before using +if (auto screen = camera.world_to_screen(world_pos)) { + draw_at(screen->x, screen->y); +} else { + // Handle point not visible +} + +// Bad: Unchecked access can crash +auto screen = camera.world_to_screen(world_pos); +draw_at(screen->x, screen->y); // Undefined behavior if nullopt! +``` + +### Handle Expected Errors + +```cpp +// Good: Handle error case +if (auto angle = v1.angle_between(v2)) { + use_angle(*angle); +} else { + switch (angle.error()) { + case Vector3Error::IMPOSSIBLE_BETWEEN_ANGLE: + // Handle zero-length vector + break; + } +} + +// Bad: Assume success +auto angle = v1.angle_between(v2); +use_angle(*angle); // Throws if error! +``` + +### Validate Inputs + +```cpp +// Good: Validate before expensive operations +bool is_valid_projectile(const Projectile& proj) { + return proj.speed > 0.0f && + std::isfinite(proj.speed) && + std::isfinite(proj.origin.length()); +} + +if (is_valid_projectile(proj) && is_valid_target(target)) { + auto aim = engine.maybe_calculate_aim_point(proj, target); +} +``` + +--- + +## Performance + +### Use constexpr When Possible + +```cpp +// Good: Computed at compile time +constexpr Vector3 gravity{0.0f, 0.0f, -9.81f}; +constexpr float max_range = 1000.0f; +constexpr float max_range_sq = max_range * max_range; + +// Use in runtime calculations +if (position.length_sqr() < max_range_sq) { + // ... +} +``` + +### Prefer Squared Distance + +```cpp +// Good: Avoids expensive sqrt +constexpr float max_dist_sq = 100.0f * 100.0f; +for (const auto& entity : entities) { + if (entity.pos.distance_to_sqr(player_pos) < max_dist_sq) { + // Process nearby entity + } +} + +// Avoid: Unnecessary sqrt calls +constexpr float max_dist = 100.0f; +for (const auto& entity : entities) { + if (entity.pos.distance_to(player_pos) < max_dist) { + // More expensive + } +} +``` + +### Cache Expensive Calculations + +```cpp +// Good: Update camera once per frame +void update_frame() { + camera.update(current_position, current_angles); + + // All projections use cached matrices + for (const auto& entity : entities) { + if (auto screen = camera.world_to_screen(entity.pos)) { + draw_entity(screen->x, screen->y); + } + } +} + +// Bad: Camera recreated each call +for (const auto& entity : entities) { + Camera cam(pos, angles, viewport, fov, near, far); // Expensive! + auto screen = cam.world_to_screen(entity.pos); +} +``` + +### Choose the Right Engine + +```cpp +// Good: Use AVX2 when available +#ifdef __AVX2__ + using Engine = ProjPredEngineAVX2; +#else + using Engine = ProjPredEngineLegacy; +#endif + +Engine prediction_engine; + +// Or runtime detection +Engine* create_best_engine() { + if (cpu_supports_avx2()) { + return new ProjPredEngineAVX2(); + } + return new ProjPredEngineLegacy(); +} +``` + +### Minimize Allocations + +```cpp +// Good: Reuse vectors +std::vector> positions; +positions.reserve(expected_count); + +// In loop +positions.clear(); // Doesn't deallocate +for (...) { + positions.push_back(compute_position()); +} + +// Bad: Allocate every time +for (...) { + std::vector> positions; // Allocates each iteration + // ... +} +``` + +--- + +## Type Safety + +### Use Strong Angle Types + +```cpp +// Good: Type-safe angles +PitchAngle pitch = PitchAngle::from_degrees(45.0f); +YawAngle yaw = YawAngle::from_degrees(90.0f); +ViewAngles angles{pitch, yaw, RollAngle::from_degrees(0.0f)}; + +// Bad: Raw floats lose safety +float pitch = 45.0f; // No range checking +float yaw = 90.0f; // Can go out of bounds +``` + +### Match Engine Types + +```cpp +// Good: Use matching types from same engine +using namespace omath::source_engine; +Camera camera = /* ... */; +ViewAngles angles = /* ... */; + +// Bad: Mixing engine types +using UnityCamera = omath::unity_engine::Camera; +using SourceAngles = omath::source_engine::ViewAngles; +UnityCamera camera{pos, SourceAngles{}, ...}; // May cause issues! +``` + +### Template Type Parameters + +```cpp +// Good: Explicit and clear +Vector3 position; +Vector3 high_precision_pos; + +// Okay: Use default float +Vector3<> position; // Defaults to float + +// Avoid: Mixing types unintentionally +Vector3 a; +Vector3 b; +auto result = a + b; // Type mismatch! +``` + +--- + +## Testing & Validation + +### Test Edge Cases + +```cpp +void test_projection() { + Camera camera = setup_camera(); + + // Test normal case + assert(camera.world_to_screen({100, 100, 100}).has_value()); + + // Test edge cases + assert(!camera.world_to_screen({0, 0, -100}).has_value()); // Behind + assert(!camera.world_to_screen({1e10, 0, 0}).has_value()); // Too far + + // Test boundaries + Vector3 at_near{0, 0, camera.near_plane() + 0.1f}; + assert(camera.world_to_screen(at_near).has_value()); +} +``` + +### Validate Assumptions + +```cpp +void validate_game_data() { + // Validate FOV + float fov = read_game_fov(); + assert(fov > 1.0f && fov < 179.0f); + + // Validate positions + Vector3 pos = read_player_position(); + assert(std::isfinite(pos.x)); + assert(std::isfinite(pos.y)); + assert(std::isfinite(pos.z)); + + // Validate viewport + ViewPort vp = read_viewport(); + assert(vp.width > 0 && vp.height > 0); +} +``` + +### Use Assertions + +```cpp +// Good: Catch errors early in development +void shoot_projectile(const Projectile& proj) { + assert(proj.speed > 0.0f && "Projectile speed must be positive"); + assert(std::isfinite(proj.origin.length()) && "Invalid projectile origin"); + + // Continue with logic +} + +// Add debug-only checks +#ifndef NDEBUG + if (!is_valid_input(data)) { + std::cerr << "Warning: Invalid input detected\n"; + } +#endif +``` + +--- + +## Memory & Resources + +### RAII for Resources + +```cpp +// Good: Automatic cleanup +class GameOverlay { + Camera camera_; + std::vector entities_; + +public: + GameOverlay(/* ... */) : camera_(/* ... */) { + entities_.reserve(1000); + } + + // Resources cleaned up automatically + ~GameOverlay() = default; +}; +``` + +### Avoid Unnecessary Copies + +```cpp +// Good: Pass by const reference +void draw_entities(const std::vector>& positions) { + for (const auto& pos : positions) { + // Process position + } +} + +// Bad: Copies entire vector +void draw_entities(std::vector> positions) { + // Expensive copy! +} + +// Good: Move when transferring ownership +std::vector> compute_positions(); +auto positions = compute_positions(); // Move, not copy +``` + +### Use Structured Bindings + +```cpp +// Good: Clear and concise +if (auto [success, screen_pos] = try_project(world_pos); success) { + draw_at(screen_pos.x, screen_pos.y); +} + +// Good: Decompose results +auto [x, y, z] = position.as_tuple(); +``` + +--- + +## Documentation + +### Document Assumptions + +```cpp +// Good: Clear documentation +/** + * Projects world position to screen space. + * + * @param world_pos Position in world coordinates (meters) + * @return Screen position if visible, nullopt if behind camera or out of view + * + * @note Assumes camera.update() was called this frame + * @note Screen coordinates are in viewport space [0, width] x [0, height] + */ +std::optional> project(const Vector3& world_pos); +``` + +### Explain Non-Obvious Code + +```cpp +// Good: Explain the math +// Use squared distance to avoid expensive sqrt +// max_range = 100.0 → max_range_sq = 10000.0 +constexpr float max_range_sq = 100.0f * 100.0f; +if (dist_sq < max_range_sq) { + // Entity is in range +} + +// Explain engine-specific quirks +// Source Engine uses Z-up coordinates, but angles are in degrees +// Pitch: [-89, 89], Yaw: [-180, 180], Roll: [-180, 180] +ViewAngles angles{pitch, yaw, roll}; +``` + +--- + +## Debugging + +### Add Debug Visualization + +```cpp +#ifndef NDEBUG +void debug_draw_projection() { + // Draw camera frustum + draw_frustum(camera); + + // Draw world axes + draw_line({0,0,0}, {100,0,0}, Color::Red); // X + draw_line({0,0,0}, {0,100,0}, Color::Green); // Y + draw_line({0,0,0}, {0,0,100}, Color::Blue); // Z + + // Draw projected points + for (const auto& entity : entities) { + if (auto screen = camera.world_to_screen(entity.pos)) { + draw_cross(screen->x, screen->y); + } + } +} +#endif +``` + +### Log Important Values + +```cpp +void debug_projection_failure(const Vector3& pos) { + std::cerr << "Projection failed for position: " + << pos.x << ", " << pos.y << ", " << pos.z << "\n"; + + auto view_matrix = camera.get_view_matrix(); + std::cerr << "View matrix:\n"; + // Print matrix... + + std::cerr << "Camera position: " + << camera.position().x << ", " + << camera.position().y << ", " + << camera.position().z << "\n"; +} +``` + +### Use Debug Builds + +```cmake +# CMakeLists.txt +if(CMAKE_BUILD_TYPE STREQUAL "Debug") + target_compile_definitions(your_target PRIVATE + DEBUG_PROJECTION=1 + VALIDATE_INPUTS=1 + ) +endif() +``` + +```cpp +#ifdef DEBUG_PROJECTION + std::cout << "Projecting: " << world_pos << "\n"; +#endif + +#ifdef VALIDATE_INPUTS + assert(std::isfinite(world_pos.length())); +#endif +``` + +--- + +## Platform Considerations + +### Cross-Platform Code + +```cpp +// Good: Platform-agnostic +constexpr float PI = 3.14159265359f; + +// Avoid: Platform-specific +#ifdef _WIN32 + // Windows-only code +#endif +``` + +### Handle Different Compilers + +```cpp +// Good: Compiler-agnostic +#if defined(_MSC_VER) + // MSVC-specific +#elif defined(__GNUC__) + // GCC/Clang-specific +#endif + +// Use OMath's built-in compatibility +// It handles compiler differences automatically +``` + +--- + +## Summary + +**Key principles:** +1. **Safety first**: Always check optional/expected results +2. **Performance matters**: Use constexpr, avoid allocations, cache results +3. **Type safety**: Use strong types, match engine types +4. **Clear code**: Use aliases, document assumptions, explain non-obvious logic +5. **Test thoroughly**: Validate inputs, test edge cases, add assertions +6. **Debug effectively**: Add visualization, log values, use debug builds + +--- + +## See Also + +- [Troubleshooting Guide](troubleshooting.md) +- [FAQ](faq.md) +- [API Overview](api_overview.md) +- [Tutorials](tutorials.md) + +--- + +*Last updated: 1 Nov 2025* diff --git a/docs/collision/collider_interface.md b/docs/collision/collider_interface.md new file mode 100644 index 00000000..7346af54 --- /dev/null +++ b/docs/collision/collider_interface.md @@ -0,0 +1,93 @@ +# `omath::collision::ColliderInterface` — Abstract collider base class + +> Header: `omath/collision/collider_interface.hpp` +> Namespace: `omath::collision` +> Depends on: `omath/linear_algebra/vector3.hpp` + +`ColliderInterface` is the abstract base class for all colliders used by the GJK and EPA algorithms. Any shape that can report its furthest vertex along a given direction can implement this interface and be used for collision detection. + +--- + +## API + +```cpp +namespace omath::collision { + +template> +class ColliderInterface { +public: + using VectorType = VecType; + + virtual ~ColliderInterface() = default; + + // Return the world-space position of the vertex furthest along `direction`. + [[nodiscard]] + virtual VectorType find_abs_furthest_vertex_position( + const VectorType& direction) const = 0; + + // Get the collider's origin (center / position). + [[nodiscard]] + virtual const VectorType& get_origin() const = 0; + + // Reposition the collider. + virtual void set_origin(const VectorType& new_origin) = 0; +}; + +} // namespace omath::collision +``` + +--- + +## Implementing a custom collider + +To create a new collider shape, derive from `ColliderInterface` and implement the three pure-virtual methods: + +```cpp +#include "omath/collision/collider_interface.hpp" + +class SphereCollider final + : public omath::collision::ColliderInterface> +{ +public: + SphereCollider(omath::Vector3 center, float radius) + : m_center(center), m_radius(radius) {} + + [[nodiscard]] + omath::Vector3 find_abs_furthest_vertex_position( + const omath::Vector3& direction) const override + { + return m_center + direction.normalized() * m_radius; + } + + [[nodiscard]] + const omath::Vector3& get_origin() const override + { return m_center; } + + void set_origin(const omath::Vector3& new_origin) override + { m_center = new_origin; } + +private: + omath::Vector3 m_center; + float m_radius; +}; +``` + +--- + +## Notes + +* **Template parameter**: The default vector type is `Vector3`, but any vector type with a `dot()` method can be used. +* **GJK/EPA compatibility**: Both `GjkAlgorithm` and `EpaAlgorithm` accept any type satisfying the `ColliderInterface` contract through their template parameters. + +--- + +## See also + +* [GJK Algorithm](gjk_algorithm.md) — collision detection using GJK. +* [EPA Algorithm](epa_algorithm.md) — penetration depth via EPA. +* [Mesh Collider](mesh_collider.md) — concrete collider wrapping a `Mesh`. +* [Simplex](simplex.md) — simplex data structure used by GJK/EPA. + +--- + +*Last updated: Feb 2026* diff --git a/docs/collision/epa_algorithm.md b/docs/collision/epa_algorithm.md new file mode 100644 index 00000000..68c23f6b --- /dev/null +++ b/docs/collision/epa_algorithm.md @@ -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`, 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 Epa final { +public: + using Vertex = typename ColliderType::VertexType; + static_assert(EpaVector, "VertexType must satisfy EpaVector concept"); + + // Solve for penetration depth and normal + [[nodiscard]] + static Result solve( + const ColliderType& a, + const ColliderType& b, + const Simplex& 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) +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 +concept EpaVector = requires(const V& a, const V& b, float s) { + { a - b } -> std::same_as; + { a.cross(b) } -> std::same_as; + { a.dot(b) } -> std::same_as; + { -a } -> std::same_as; + { a * s } -> std::same_as; + { a / s } -> std::same_as; +}; +``` + +`omath::Vector3` 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 collider_a(mesh_a); +MeshCollider collider_b(mesh_b); + +auto gjk_result = GjkAlgorithm>::check_collision( + collider_a, + collider_b +); + +if (gjk_result.hit) { + // Collision detected, use EPA to get penetration info + auto epa_result = Epa>::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 correction = epa_result.normal * epa_result.depth; + mesh_a.set_origin(mesh_a.get_origin() + correction); + } +} +``` + +### Custom Parameters + +```cpp +// Use custom convergence settings +Epa::Params params; +params.max_iterations = 128; // Allow more iterations for complex shapes +params.tolerance = 1e-5f; // Tighter tolerance for more accuracy + +auto result = Epa::solve(a, b, simplex, params); +``` + +### Physics Integration + +```cpp +void resolve_collision(PhysicsBody& body_a, PhysicsBody& body_b) { + auto gjk_result = GjkAlgorithm::check_collision( + body_a.collider, body_b.collider + ); + + if (!gjk_result.hit) + return; // No collision + + auto epa_result = Epa::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::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* diff --git a/docs/collision/gjk_algorithm.md b/docs/collision/gjk_algorithm.md new file mode 100644 index 00000000..4fa13926 --- /dev/null +++ b/docs/collision/gjk_algorithm.md @@ -0,0 +1,216 @@ +# `omath::collision::GjkAlgorithm` — Gilbert-Johnson-Keerthi collision detection + +> Header: `omath/collision/gjk_algorithm.hpp` +> Namespace: `omath::collision` +> Depends on: `Simplex`, 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 +struct GjkHitInfo final { + bool hit{false}; // true if collision detected + Simplex 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 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 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) +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` — 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>::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::check_collision(a, b); + +if (gjk_result.hit) { + // Get penetration depth and normal using EPA + auto epa_result = Epa::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 +concept GjkVector = requires(const V& a, const V& b) { + { -a } -> std::same_as; + { a - b } -> std::same_as; + { a.cross(b) } -> std::same_as; + { a.point_to_same_direction(b) } -> std::same_as; +}; +``` + +`omath::Vector3` 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* diff --git a/docs/collision/line_tracer.md b/docs/collision/line_tracer.md new file mode 100644 index 00000000..9e9e1f8a --- /dev/null +++ b/docs/collision/line_tracer.md @@ -0,0 +1,181 @@ +# `omath::collision::Ray` & `LineTracer` — Ray–Triangle intersection (Möller–Trumbore) + +> Headers: your project’s `ray.hpp` (includes `omath/linear_algebra/triangle.hpp`, `omath/linear_algebra/vector3.hpp`) +> Namespace: `omath::collision` +> Depends on: `omath::Vector3`, `omath::Triangle>` +> Algorithm: **Möller–Trumbore** ray–triangle intersection (no allocation) + +--- + +## Overview + +These types provide a minimal, fast path to test and compute intersections between a **ray or line segment** and a **single triangle**: + +* `Ray` — start/end points plus a flag to treat the ray as **infinite** (half-line) or a **finite segment**. +* `LineTracer` — static helpers: + + * `can_trace_line(ray, triangle)` → `true` if they intersect. + * `get_ray_hit_point(ray, triangle)` → the hit point (precondition: intersection exists). + +--- + +## `Ray` + +```cpp +class Ray { +public: + omath::Vector3 start; // ray origin + omath::Vector3 end; // end point (for finite segment) or a point along the direction + bool infinite_length = false; + + [[nodiscard]] omath::Vector3 direction_vector() const noexcept; + [[nodiscard]] omath::Vector3 direction_vector_normalized() const noexcept; +}; +``` + +### Semantics + +* **Direction**: `direction_vector() == end - start`. + The normalized variant returns a unit vector (or `{0,0,0}` if the direction length is zero). +* **Extent**: + + * `infinite_length == true` → treat as a **semi-infinite ray** from `start` along `direction`. + * `infinite_length == false` → treat as a **closed segment** from `start` to `end`. + +> Tip: For an infinite ray that points along some vector `d`, set `end = start + d`. + +--- + +## `LineTracer` + +```cpp +class LineTracer { +public: + LineTracer() = delete; + + [[nodiscard]] + static bool can_trace_line( + const Ray& ray, + const omath::Triangle>& triangle + ) noexcept; + + // Möller–Trumbore intersection + [[nodiscard]] + static omath::Vector3 get_ray_hit_point( + const Ray& ray, + const omath::Triangle>& triangle + ) noexcept; +}; +``` + +### Behavior & contract + +* **Intersection test**: `can_trace_line` returns `true` iff the ray/segment intersects the triangle (within the ray’s extent). +* **Hit point**: `get_ray_hit_point` **assumes** there is an intersection. + Call **only after** `can_trace_line(...) == true`. Otherwise the result is unspecified. +* **Triangle winding**: Standard Möller–Trumbore works with either winding; no backface culling is implied here. +* **Degenerate inputs**: A zero-length ray or degenerate triangle yields **no hit** under typical Möller–Trumbore tolerances. + +--- + +## Quick examples + +### 1) Segment vs triangle + +```cpp +using omath::Vector3; +using omath::Triangle; +using omath::collision::Ray; +using omath::collision::LineTracer; + +Triangle> tri( + Vector3{0, 0, 0}, + Vector3{1, 0, 0}, + Vector3{0, 1, 0} +); + +Ray seg; +seg.start = {0.25f, 0.25f, 1.0f}; +seg.end = {0.25f, 0.25f,-1.0f}; +seg.infinite_length = false; // finite segment + +if (LineTracer::can_trace_line(seg, tri)) { + Vector3 hit = LineTracer::get_ray_hit_point(seg, tri); + // use hit +} +``` + +### 2) Infinite ray + +```cpp +Ray ray; +ray.start = {0.5f, 0.5f, 1.0f}; +ray.end = ray.start + Vector3{0, 0, -1}; // direction only +ray.infinite_length = true; + +bool hit = LineTracer::can_trace_line(ray, tri); +``` + +--- + +## Notes & edge cases + +* **Normalization**: `direction_vector_normalized()` returns `{0,0,0}` for a zero-length direction (safe, but unusable for tracing). +* **Precision**: The underlying algorithm uses EPS thresholds to reject nearly parallel cases; results near edges can be sensitive to floating-point error. If you need robust edge inclusion/exclusion, document and enforce a policy (e.g., inclusive barycentric range with small epsilon). +* **Hit location**: The point returned by `get_ray_hit_point` lies **on the triangle plane** and within its area by construction (when `can_trace_line` is `true`). + +--- + +## API summary + +```cpp +namespace omath::collision { + +class Ray { +public: + Vector3 start, end; + bool infinite_length = false; + + [[nodiscard]] Vector3 direction_vector() const noexcept; + [[nodiscard]] Vector3 direction_vector_normalized() const noexcept; +}; + +class LineTracer { +public: + LineTracer() = delete; + + [[nodiscard]] static bool can_trace_line( + const Ray&, + const omath::Triangle>& + ) noexcept; + + [[nodiscard]] static Vector3 get_ray_hit_point( + const Ray&, + const omath::Triangle>& + ) noexcept; // precondition: can_trace_line(...) == true +}; + +} // namespace omath::collision +``` + +--- + +## Implementation hints (if you extend it) + +* Expose a variant that returns **barycentric coordinates** `(u, v, w)` alongside the hit point to support texture lookup or edge tests. +* Provide an overload returning `std::optional>` (or `expected`) for safer one-shot queries without a separate test call. +* If you need backface culling, add a flag or dedicated function (reject hits where the signed distance is negative with respect to triangle normal). + +--- + +## See Also + +- [Plane Documentation](../3d_primitives/plane.md) - Ray-plane intersection +- [Box Documentation](../3d_primitives/box.md) - AABB collision detection +- [Triangle Documentation](../linear_algebra/triangle.md) - Triangle primitives +- [Tutorials - Collision Detection](../tutorials.md#tutorial-4-collision-detection) - Complete collision tutorial +- [Getting Started Guide](../getting_started.md) - Quick start with OMath + +--- + +*Last updated: 1 Nov 2025* diff --git a/docs/collision/mesh_collider.md b/docs/collision/mesh_collider.md new file mode 100644 index 00000000..2e1b6f12 --- /dev/null +++ b/docs/collision/mesh_collider.md @@ -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` +> 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 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> m_vertex_buffer; + + // Transform vertex from local to world space + Vector3 vertex_to_world_space(const Vector3&) 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` — 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> 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> 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 collider_a(std::move(mesh_a)); +MeshCollider collider_b(std::move(mesh_b)); + +// Run GJK +auto result = GjkAlgorithm>::check_collision( + collider_a, collider_b +); + +if (result.hit) { + std::cout << "Collision detected!\n"; +} +``` + +### With EPA for Penetration Depth + +```cpp +auto gjk_result = GjkAlgorithm>::check_collision( + collider_a, collider_b +); + +if (gjk_result.hit) { + auto epa_result = Epa>::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> 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> 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 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 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 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; + + Vector3 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::check_collision(sphere_a, sphere_b); +``` + +### Debugging Support Queries + +```cpp +class DebugMeshCollider : public MeshCollider { +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* diff --git a/docs/collision/simplex.md b/docs/collision/simplex.md new file mode 100644 index 00000000..c786e784 --- /dev/null +++ b/docs/collision/simplex.md @@ -0,0 +1,327 @@ +# `omath::collision::Simplex` — Fixed-capacity simplex for GJK/EPA + +> Header: `omath/collision/simplex.hpp` +> Namespace: `omath::collision` +> Depends on: `Vector3` (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> +class Simplex final; +``` + +### `GjkVector` Concept + +The vertex type must satisfy: + +```cpp +template +concept GjkVector = requires(const V& a, const V& b) { + { -a } -> std::same_as; + { a - b } -> std::same_as; + { a.cross(b) } -> std::same_as; + { a.point_to_same_direction(b) } -> std::same_as; +}; +``` + +`omath::Vector3` satisfies this concept and is the default. + +--- + +## Constructors & Assignment + +```cpp +constexpr Simplex() = default; + +constexpr Simplex& operator=(std::initializer_list list) noexcept; +``` + +### Initialization + +```cpp +// Empty simplex +Simplex> s; + +// Initialize with points +Simplex> 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> simplex; +Vector3 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; + +Simplex 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>& 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* diff --git a/docs/containers/encrypted_variable.md b/docs/containers/encrypted_variable.md new file mode 100644 index 00000000..b9bfe7c9 --- /dev/null +++ b/docs/containers/encrypted_variable.md @@ -0,0 +1,115 @@ +# `omath::EncryptedVariable` — Compile-time XOR-encrypted variable + +> Header: `omath/containers/encrypted_variable.hpp` +> Namespace: `omath` +> Depends on: ``, ``, ``, `` + +`EncryptedVariable` keeps a value XOR-encrypted in memory at rest, using a **compile-time generated random key**. It is designed to hinder static analysis and memory scanners from reading sensitive values (e.g., game constants, keys, thresholds) directly from process memory. + +--- + +## Key concepts + +* **Compile-time key generation** — a unique random byte array is produced at compile time via SplitMix64 + FNV-1a seeded from `__FILE__`, `__DATE__`, and `__TIME__`. Each `OMATH_DEF_CRYPT_VAR` expansion receives a distinct key. +* **XOR cipher** — `encrypt()` / `decrypt()` toggle the encrypted state by XOR-ing the raw bytes of the stored value with the key. +* **VarAnchor (RAII guard)** — `drop_anchor()` returns a `VarAnchor` that decrypts on construction and re-encrypts on destruction, ensuring the plaintext window is as short as possible. + +--- + +## API + +```cpp +namespace omath { + +template key> +class EncryptedVariable final { +public: + using value_type = std::remove_cvref_t; + + constexpr explicit EncryptedVariable(const value_type& data); + + [[nodiscard]] constexpr bool is_encrypted() const; + + constexpr void decrypt(); + constexpr void encrypt(); + + [[nodiscard]] constexpr value_type& value(); + [[nodiscard]] constexpr const value_type& value() const; + + [[nodiscard]] constexpr auto drop_anchor(); // returns VarAnchor + + constexpr ~EncryptedVariable(); // decrypts on destruction +}; + +template +class VarAnchor final { +public: + constexpr VarAnchor(EncryptedVarType& var); // decrypts + constexpr ~VarAnchor(); // re-encrypts +}; + +} // namespace omath +``` + +### Helper macros + +```cpp +// Generate a compile-time random byte array of length N +#define OMATH_CT_RAND_ARRAY_BYTE(N) /* ... */ + +// Declare a type alias for EncryptedVariable with KEY_SIZE random bytes +#define OMATH_DEF_CRYPT_VAR(TYPE, KEY_SIZE) /* ... */ +``` + +--- + +## Usage examples + +### Basic encrypt / decrypt + +```cpp +#include "omath/containers/encrypted_variable.hpp" + +// Define an encrypted float with a 16-byte key +using EncFloat = OMATH_DEF_CRYPT_VAR(float, 16); + +EncFloat secret(3.14f); // encrypted immediately +// secret.value() is XOR-scrambled in memory + +secret.decrypt(); +float v = secret.value(); // v == 3.14f +secret.encrypt(); // scrambled again +``` + +### RAII guard with `drop_anchor()` + +```cpp +EncFloat secret(42.0f); + +{ + auto anchor = secret.drop_anchor(); // decrypts + float val = secret.value(); // safe to read + // ... use val ... +} // anchor destroyed → re-encrypts automatically +``` + +--- + +## Notes & edge cases + +* **Force-inline**: When `OMATH_ENABLE_FORCE_INLINE` is defined, encrypt/decrypt operations use compiler-specific force-inline attributes to reduce the call-site footprint visible in disassembly. +* **Not cryptographically secure**: XOR with a static key is an obfuscation technique, not encryption. It raises the bar for casual memory scanning but does not resist a determined attacker who can read the binary. +* **Destructor decrypts**: The destructor calls `decrypt()` so the value is in plaintext at the end of its lifetime (e.g., for logging or cleanup). +* **Thread safety**: No internal synchronization. Protect concurrent access with external locks. +* **`constexpr` support**: All operations are `constexpr`-friendly (C++20). + +--- + +## See also + +* [Pattern Scan](../utility/pattern_scan.md) — scan memory for byte patterns. +* [Getting Started](../getting_started.md) — quick start with OMath. + +--- + +*Last updated: Feb 2026* diff --git a/docs/engines/frostbite/camera_trait.md b/docs/engines/frostbite/camera_trait.md new file mode 100644 index 00000000..bb03cd02 --- /dev/null +++ b/docs/engines/frostbite/camera_trait.md @@ -0,0 +1,108 @@ +# `omath::frostbite_engine::CameraTrait` — plug-in trait for `projection::Camera` + +> Header: `omath/engines/frostbite_engine/traits/camera_trait.hpp` • Impl: `omath/engines/frostbite_engine/traits/camera_trait.cpp` +> Namespace: `omath::frostbite_engine` +> Purpose: provide Frostbite-style **look-at**, **view**, and **projection** math to the generic `omath::projection::Camera` (satisfies `CameraEngineConcept`). + +--- + +## Summary + +`CameraTrait` exposes three `static` functions: + +* `calc_look_at_angle(origin, look_at)` – computes Euler angles so the camera at `origin` looks at `look_at`. Implementation normalizes the direction, computes **pitch** as `-asin(dir.y)` and **yaw** as `atan2(dir.x, dir.z)`; **roll** is `0`. Pitch/yaw are returned using the project’s strong angle types (`PitchAngle`, `YawAngle`, `RollAngle`). +* `calc_view_matrix(angles, origin)` – delegates to Frostbite formulas `frostbite_engine::calc_view_matrix`, producing a `Mat4X4` view matrix for the given angles and origin. +* `calc_projection_matrix(fov, viewport, near, far)` – builds a perspective projection by calling `calc_perspective_projection_matrix(fov_degrees, aspect, near, far)`, where `aspect = viewport.aspect_ratio()`. Accepts `FieldOfView` (degrees). + +The trait’s types (`ViewAngles`, `Mat4X4`, angle aliases) and helpers live in the Frostbite engine math headers included by the trait (`formulas.hpp`) and the shared projection header (`projection/camera.hpp`). + +--- + +## API + +```cpp +namespace omath::frostbite_engine { + +class CameraTrait final { +public: + // Compute Euler angles (pitch/yaw/roll) to look from cam_origin to look_at. + static ViewAngles + calc_look_at_angle(const Vector3& cam_origin, + const Vector3& look_at) noexcept; + + // Build view matrix for given angles and origin. + static Mat4X4 + calc_view_matrix(const ViewAngles& angles, + const Vector3& cam_origin) noexcept; + + // Build perspective projection from FOV (deg), viewport, near/far. + static Mat4X4 + calc_projection_matrix(const projection::FieldOfView& fov, + const projection::ViewPort& view_port, + float near, float far) noexcept; +}; + +} // namespace omath::frostbite_engine +``` + +Uses: `Vector3`, `ViewAngles` (pitch/yaw/roll), `Mat4X4`, `projection::FieldOfView`, `projection::ViewPort`. + +--- + +## Behavior & conventions + +* **Angles from look-at**: + + ``` + dir = normalize(look_at - origin) + pitch = -asin(dir.y) // +Y is up + yaw = atan2(dir.x, dir.z) + roll = 0 + ``` + + Returned as `PitchAngle::from_radians(...)`, `YawAngle::from_radians(...)`, etc. + +* **View matrix**: built by the Frostbite engine helper `frostbite_engine::calc_view_matrix(angles, origin)` to match the engine’s handedness and axis conventions. + +* **Projection**: uses `calc_perspective_projection_matrix(fov.as_degrees(), viewport.aspect_ratio(), near, far)`. Pass your **vertical FOV** in degrees via `FieldOfView`; the helper computes a standard perspective matrix. + +--- + +## Using with `projection::Camera` + +Create a camera whose math is driven by this trait: + +```cpp +using Mat4 = Mat4X4; // from Frostbite math headers +using Angs = ViewAngles; // pitch/yaw/roll type +using FBcam = omath::projection::Camera; + +omath::projection::ViewPort vp{1920.f, 1080.f}; +auto fov = omath::projection::FieldOfView::from_degrees(70.f); + +FBcam cam( + /*position*/ {0.f, 1.7f, -3.f}, + /*angles*/ omath::frostbite_engine::CameraTrait::calc_look_at_angle({0,1.7f,-3},{0,1.7f,0}), + /*viewport*/ vp, + /*fov*/ fov, + /*near*/ 0.1f, + /*far*/ 1000.f +); +``` + +This satisfies `CameraEngineConcept` expected by `projection::Camera` (look-at, view, projection) as declared in the trait header. + +--- + +## Notes & tips + +* Ensure your `ViewAngles` aliases (`PitchAngle`, `YawAngle`, `RollAngle`) match the project’s angle policy (ranges/normalization). The implementation constructs them **from radians**. +* `aspect_ratio()` is taken directly from `ViewPort` (`width / height`), so keep both positive and non-zero. +* `near` must be > 0 and `< far` for a valid projection matrix (enforced by your math helpers). + +--- + +## See also + +* Frostbite math helpers in `omath/engines/frostbite_engine/formulas.hpp` (view/projection builders used above). +* Generic camera wrapper `omath::projection::Camera` and its `CameraEngineConcept` (this trait is designed to plug straight into it). diff --git a/docs/engines/frostbite/constants.md b/docs/engines/frostbite/constants.md new file mode 100644 index 00000000..61d1fbe4 --- /dev/null +++ b/docs/engines/frostbite/constants.md @@ -0,0 +1,59 @@ +Nice! A clean little “types + constants” header. A few quick fixes and polish: + +## Issues / suggestions + +1. **Mat3X3 alias is wrong** + +* You wrote `using Mat3X3 = Mat<4, 4, float, MatStoreType::ROW_MAJOR>;` +* That should be `Mat<3, 3, ...>`. + +2. **`constexpr` globals in a header → make them `inline constexpr`** + +* Since this is in a header included by multiple TUs, use `inline constexpr` to avoid ODR/link issues (C++17+). + +3. **Consider column-major vs row-major** + +* Most game/graphics stacks (GLSL/HLSL, many engines) lean column-major and column vectors. If the rest of your math lib or shaders assume column-major, align these typedefs now to avoid silent transposes later. If row-major is intentional, all good—just be consistent. + +4. **Naming consistency** + +* If you prefer `k_` prefix, keep it; otherwise consider `kAbsUp`/`ABS_UP` to match your codebase’s conventions. + +5. **`Mat1X3` as a “row vector”** + +* If you actually use it as a 3-component vector, consider just `Vector3` (clearer) and reserve `Mat1X3` for real row-vector math. + +--- + +## Tidied version + +```cpp +// Created by Vlad on 10/21/2025. +#pragma once + +#include "omath/linear_algebra/mat.hpp" +#include "omath/linear_algebra/vector3.hpp" +#include +#include + +namespace omath::frostbite_engine +{ + // Inline to avoid ODR across translation units + inline constexpr Vector3 k_abs_up = {0.0f, 1.0f, 0.0f}; + inline constexpr Vector3 k_abs_right = {1.0f, 0.0f, 0.0f}; + inline constexpr Vector3 k_abs_forward = {0.0f, 0.0f, 1.0f}; + + // NOTE: verify row/column major matches the rest of your engine + using Mat4X4 = Mat<4, 4, float, MatStoreType::ROW_MAJOR>; + using Mat3X3 = Mat<3, 3, float, MatStoreType::ROW_MAJOR>; + using Mat1X3 = Mat<1, 3, float, MatStoreType::ROW_MAJOR>; + + using PitchAngle = Angle; + using YawAngle = Angle; + using RollAngle = Angle; + + using ViewAngles = omath::ViewAngles; +} // namespace omath::frostbite_engine +``` + +If you share how your matrices multiply vectors (row vs column) and your world handedness, I can double-check the axis constants and angle normalization to make sure yaw/pitch signs line up with your camera and `atan2` usage. diff --git a/docs/engines/frostbite/formulas.md b/docs/engines/frostbite/formulas.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/engines/frostbite/mesh_trait.md b/docs/engines/frostbite/mesh_trait.md new file mode 100644 index 00000000..f6b235fe --- /dev/null +++ b/docs/engines/frostbite/mesh_trait.md @@ -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; +} +``` + +--- + +## 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* diff --git a/docs/engines/frostbite/pred_engine_trait.md b/docs/engines/frostbite/pred_engine_trait.md new file mode 100644 index 00000000..a5b77518 --- /dev/null +++ b/docs/engines/frostbite/pred_engine_trait.md @@ -0,0 +1,105 @@ +// Created by Vlad on 8/6/2025. +#pragma once + +#include // sqrt, hypot, tan, asin, atan2 +#include + +#include "omath/engines/frostbite_engine/formulas.hpp" +#include "omath/projectile_prediction/projectile.hpp" +#include "omath/projectile_prediction/target.hpp" + +namespace omath::frostbite_engine +{ +class PredEngineTrait final +{ +public: +// Predict projectile position given launch angles (degrees), time (s), and world gravity (m/s^2). +// Note: kept runtime function; remove constexpr to avoid CTAD surprises across toolchains. +static Vector3 predict_projectile_position( +const projectile_prediction::Projectile& projectile, +float pitch_deg, float yaw_deg, +float time, float gravity) noexcept +{ +// Engine convention: negative pitch looks up (your original used -pitch). +const auto fwd = forward_vector({ +PitchAngle::from_degrees(-pitch_deg), +YawAngle::from_degrees(yaw_deg), +RollAngle::from_degrees(0.0f) +}); + + Vector3 pos = + projectile.m_origin + + fwd * (projectile.m_launch_speed * time); + + // s = 1/2 a t^2 downward + pos.y -= (gravity * projectile.m_gravity_scale) * (time * time) * 0.5f; + return pos; + } + + [[nodiscard]] + static Vector3 predict_target_position( + const projectile_prediction::Target& target, + float time, float gravity) noexcept + { + Vector3 predicted = target.m_origin + target.m_velocity * time; + + if (target.m_is_airborne) { + // If targets also have a gravity scale in your model, multiply here. + predicted.y -= gravity * (time * time) * 0.5f; + } + return predicted; + } + + [[nodiscard]] + static float calc_vector_2d_distance(const Vector3& delta) noexcept + { + // More stable than sqrt(x*x + z*z) + return std::hypot(delta.x, delta.z); + } + + [[nodiscard]] + static float get_vector_height_coordinate(const Vector3& vec) noexcept + { + return vec.y; + } + + // Computes a viewpoint above the predicted target, using an optional projectile pitch. + // If pitch is absent, we leave Y unchanged (or you can choose a sensible default). + [[nodiscard]] + static Vector3 calc_viewpoint_from_angles( + const projectile_prediction::Projectile& projectile, + const Vector3& predicted_target_position, + const std::optional projectile_pitch_deg) noexcept + { + // Lateral separation from projectile to target (X/Z plane). + const auto delta2d = calc_vector_2d_distance(predicted_target_position - projectile.m_origin); + + float y = predicted_target_position.y; + if (projectile_pitch_deg.has_value()) { + const float pitch_rad = angles::degrees_to_radians(*projectile_pitch_deg); + const float height = delta2d * std::tan(pitch_rad); + y += height; + } + + // Use the target's Z, not the projectile's Z (likely bugfix). + return { predicted_target_position.x, y, predicted_target_position.z }; + } + + // Due to maybe_calculate_projectile_launch_pitch_angle spec: +89° up, -89° down. + [[nodiscard]] + static float calc_direct_pitch_angle(const Vector3& origin, + const Vector3& view_to) noexcept + { + const auto direction = (view_to - origin).normalized(); + return angles::radians_to_degrees(std::asin(direction.y)); + } + + [[nodiscard]] + static float calc_direct_yaw_angle(const Vector3& origin, + const Vector3& view_to) noexcept + { + const auto direction = (view_to - origin).normalized(); + return angles::radians_to_degrees(std::atan2(direction.x, direction.z)); + } + }; +} // namespace omath::frostbite_engine diff --git a/docs/engines/iw_engine/camera_trait.md b/docs/engines/iw_engine/camera_trait.md new file mode 100644 index 00000000..d693abd4 --- /dev/null +++ b/docs/engines/iw_engine/camera_trait.md @@ -0,0 +1,109 @@ +# `omath::iw_engine::CameraTrait` — plug-in trait for `projection::Camera` + +> Header: `omath/engines/iw_engine/traits/camera_trait.hpp` • Impl: `omath/engines/iw_engine/traits/camera_trait.cpp` +> Namespace: `omath::iw_engine` +> Purpose: provide IW Engine (Call of Duty)-style **look-at**, **view**, and **projection** math to the generic `omath::projection::Camera` (satisfies `CameraEngineConcept`). + +--- + +## Summary + +`CameraTrait` exposes three `static` functions: + +* `calc_look_at_angle(origin, look_at)` – computes Euler angles so the camera at `origin` looks at `look_at`. Implementation normalizes the direction, computes **pitch** as `asin(dir.z)` and **yaw** as `atan2(dir.y, dir.x)`; **roll** is `0`. Pitch/yaw are returned using the project's strong angle types (`PitchAngle`, `YawAngle`, `RollAngle`). +* `calc_view_matrix(angles, origin)` – delegates to IW Engine formulas `iw_engine::calc_view_matrix`, producing a `Mat4X4` view matrix for the given angles and origin. +* `calc_projection_matrix(fov, viewport, near, far)` – builds a perspective projection by calling `calc_perspective_projection_matrix(fov_degrees, aspect, near, far)`, where `aspect = viewport.aspect_ratio()`. Accepts `FieldOfView` (degrees). + +The trait's types (`ViewAngles`, `Mat4X4`, angle aliases) and helpers live in the IW Engine math headers included by the trait (`formulas.hpp`) and the shared projection header (`projection/camera.hpp`). + +--- + +## API + +```cpp +namespace omath::iw_engine { + +class CameraTrait final { +public: + // Compute Euler angles (pitch/yaw/roll) to look from cam_origin to look_at. + static ViewAngles + calc_look_at_angle(const Vector3& cam_origin, + const Vector3& look_at) noexcept; + + // Build view matrix for given angles and origin. + static Mat4X4 + calc_view_matrix(const ViewAngles& angles, + const Vector3& cam_origin) noexcept; + + // Build perspective projection from FOV (deg), viewport, near/far. + static Mat4X4 + calc_projection_matrix(const projection::FieldOfView& fov, + const projection::ViewPort& view_port, + float near, float far) noexcept; +}; + +} // namespace omath::iw_engine +``` + +Uses: `Vector3`, `ViewAngles` (pitch/yaw/roll), `Mat4X4`, `projection::FieldOfView`, `projection::ViewPort`. + +--- + +## Behavior & conventions + +* **Angles from look-at** (Z-up coordinate system): + + ``` + dir = normalize(look_at - origin) + pitch = asin(dir.z) // +Z is up + yaw = atan2(dir.y, dir.x) // horizontal rotation + roll = 0 + ``` + + Returned as `PitchAngle::from_radians(...)`, `YawAngle::from_radians(...)`, etc. + +* **View matrix**: built by the IW Engine helper `iw_engine::calc_view_matrix(angles, origin)` to match the engine's handedness and axis conventions. + +* **Projection**: uses `calc_perspective_projection_matrix(fov.as_degrees(), viewport.aspect_ratio(), near, far)`. Pass your **vertical FOV** in degrees via `FieldOfView`; the helper computes a standard perspective matrix. + +--- + +## Using with `projection::Camera` + +Create a camera whose math is driven by this trait: + +```cpp +using Mat4 = Mat4X4; // from IW Engine math headers +using Angs = ViewAngles; // pitch/yaw/roll type +using IWcam = omath::projection::Camera; + +omath::projection::ViewPort vp{1920.f, 1080.f}; +auto fov = omath::projection::FieldOfView::from_degrees(65.f); + +IWcam cam( + /*position*/ {500.f, 200.f, 100.f}, + /*angles*/ omath::iw_engine::CameraTrait::calc_look_at_angle({500,200,100},{0,0,100}), + /*viewport*/ vp, + /*fov*/ fov, + /*near*/ 0.1f, + /*far*/ 5000.f +); +``` + +This satisfies `CameraEngineConcept` expected by `projection::Camera` (look-at, view, projection) as declared in the trait header. + +--- + +## Notes & tips + +* Ensure your `ViewAngles` aliases (`PitchAngle`, `YawAngle`, `RollAngle`) match the project's angle policy (ranges/normalization). The implementation constructs them **from radians**. +* `aspect_ratio()` is taken directly from `ViewPort` (`width / height`), so keep both positive and non-zero. +* `near` must be > 0 and `< far` for a valid projection matrix (enforced by your math helpers). +* IW Engine uses **Z-up**: pitch angles control vertical look, positive = up. + +--- + +## See also + +* IW Engine math helpers in `omath/engines/iw_engine/formulas.hpp` (view/projection builders used above). +* Generic camera wrapper `omath::projection::Camera` and its `CameraEngineConcept` (this trait is designed to plug straight into it). diff --git a/docs/engines/iw_engine/constants.md b/docs/engines/iw_engine/constants.md new file mode 100644 index 00000000..cb9d9d98 --- /dev/null +++ b/docs/engines/iw_engine/constants.md @@ -0,0 +1,77 @@ +# `omath::iw_engine` — types & constants + +> Header: `omath/engines/iw_engine/constants.hpp` +> Namespace: `omath::iw_engine` +> Purpose: define IW Engine (Call of Duty) coordinate system, matrix types, and angle ranges + +--- + +## Summary + +The **IW Engine** (Infinity Ward Engine, used in Call of Duty series) uses a **Z-up, right-handed** coordinate system: + +* **Up** = `{0, 0, 1}` (Z-axis) +* **Right** = `{0, -1, 0}` (negative Y-axis) +* **Forward** = `{1, 0, 0}` (X-axis) + +Matrices are **row-major**. Angles are **clamped pitch** (±89°) and **normalized yaw/roll** (±180°). + +--- + +## Constants + +```cpp +namespace omath::iw_engine { + constexpr Vector3 k_abs_up = {0, 0, 1}; + constexpr Vector3 k_abs_right = {0, -1, 0}; + constexpr Vector3 k_abs_forward = {1, 0, 0}; +} +``` + +These basis vectors define the engine's **world coordinate frame**. + +--- + +## Matrix types + +```cpp +using Mat4X4 = Mat<4, 4, float, MatStoreType::ROW_MAJOR>; +using Mat3X3 = Mat<4, 4, float, MatStoreType::ROW_MAJOR>; +using Mat1X3 = Mat<1, 3, float, MatStoreType::ROW_MAJOR>; +``` + +**Row-major** storage means rows are contiguous in memory. Suitable for CPU-side transforms and typical C++ math libraries. + +--- + +## Angle types + +```cpp +using PitchAngle = Angle; +using YawAngle = Angle; +using RollAngle = Angle; + +using ViewAngles = omath::ViewAngles; +``` + +* **PitchAngle**: clamped to **[-89°, +89°]** (looking down vs. up) +* **YawAngle**: normalized to **[-180°, +180°]** (horizontal rotation) +* **RollAngle**: normalized to **[-180°, +180°]** (camera roll) + +`ViewAngles` bundles all three into a single type for camera/view transforms. + +--- + +## Coordinate system notes + +* **Z-up**: gravity points along `-Z`, height increases with `+Z` +* **Right-handed**: cross product `forward × right = up` holds +* This matches **IW Engine** (Call of Duty series: Modern Warfare, Black Ops, etc.) conventions + +--- + +## See also + +* `omath/engines/iw_engine/formulas.hpp` — view/projection matrix builders +* `omath/trigonometry/angle.hpp` — angle normalization & clamping helpers +* `omath/trigonometry/view_angles.hpp` — generic pitch/yaw/roll wrapper diff --git a/docs/engines/iw_engine/formulas.md b/docs/engines/iw_engine/formulas.md new file mode 100644 index 00000000..283b4baa --- /dev/null +++ b/docs/engines/iw_engine/formulas.md @@ -0,0 +1,135 @@ +# `omath::iw_engine` — formulas & matrix helpers + +> Header: `omath/engines/iw_engine/formulas.hpp` +> Namespace: `omath::iw_engine` +> Purpose: compute direction vectors, rotation matrices, view matrices, and perspective projections for IW Engine (Call of Duty) + +--- + +## Summary + +This header provides **IW Engine**-specific math for: + +* **Direction vectors** (`forward`, `right`, `up`) from `ViewAngles` +* **Rotation matrices** from Euler angles +* **View matrices** (camera transforms) +* **Perspective projection** matrices + +All functions respect IW Engine's **Z-up, right-handed** coordinate system. + +--- + +## API + +```cpp +namespace omath::iw_engine { + + // Compute forward direction from pitch/yaw/roll + [[nodiscard]] + Vector3 forward_vector(const ViewAngles& angles) noexcept; + + // Compute right direction from pitch/yaw/roll + [[nodiscard]] + Vector3 right_vector(const ViewAngles& angles) noexcept; + + // Compute up direction from pitch/yaw/roll + [[nodiscard]] + Vector3 up_vector(const ViewAngles& angles) noexcept; + + // Build 3x3 rotation matrix from angles + [[nodiscard]] + Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept; + + // Build view matrix (camera space transform) + [[nodiscard]] + Mat4X4 calc_view_matrix(const ViewAngles& angles, + const Vector3& cam_origin) noexcept; + + // Build perspective projection matrix + [[nodiscard]] + Mat4X4 calc_perspective_projection_matrix(float field_of_view, + float aspect_ratio, + float near, float far) noexcept; + +} // namespace omath::iw_engine +``` + +--- + +## Direction vectors + +Given camera angles (pitch/yaw/roll): + +* `forward_vector(angles)` → unit vector pointing where the camera looks +* `right_vector(angles)` → unit vector pointing to the camera's right +* `up_vector(angles)` → unit vector pointing upward relative to the camera + +These are used for movement, aim direction, and building coordinate frames. + +--- + +## Rotation & view matrices + +* `rotation_matrix(angles)` → 3×3 (or 4×4) rotation matrix from Euler angles +* `calc_view_matrix(angles, origin)` → camera view matrix + +The view matrix transforms world coordinates into camera space (origin at camera, axes aligned with camera orientation). + +--- + +## Perspective projection + +```cpp +Mat4X4 proj = calc_perspective_projection_matrix( + fov_degrees, // vertical field of view (e.g., 65) + aspect_ratio, // width / height (e.g., 16/9) + near_plane, // e.g., 0.1 + far_plane // e.g., 5000.0 +); +``` + +Produces a **perspective projection matrix** suitable for 3D rendering pipelines. Combined with the view matrix, this implements the standard camera transform chain. + +--- + +## Usage example + +```cpp +using namespace omath::iw_engine; + +// Camera setup +ViewAngles angles = { + PitchAngle::from_degrees(-10.0f), + YawAngle::from_degrees(90.0f), + RollAngle::from_degrees(0.0f) +}; +Vector3 cam_pos{500.0f, 200.0f, 100.0f}; + +// Compute direction +auto forward = forward_vector(angles); +auto right = right_vector(angles); +auto up = up_vector(angles); + +// Build matrices +auto view_mat = calc_view_matrix(angles, cam_pos); +auto proj_mat = calc_perspective_projection_matrix(65.0f, 16.0f/9.0f, 0.1f, 5000.0f); + +// Use view_mat and proj_mat for rendering... +``` + +--- + +## Conventions + +* **Angles**: pitch (up/down), yaw (left/right), roll (tilt) +* **Pitch**: positive = looking up, negative = looking down +* **Yaw**: increases counter-clockwise from the +X axis +* **Coordinate system**: Z-up, X-forward, Y-right (negative in code convention) + +--- + +## See also + +* `omath/engines/iw_engine/constants.hpp` — coordinate frame & angle types +* `omath/engines/iw_engine/traits/camera_trait.hpp` — plug-in for generic `Camera` +* `omath/projection/camera.hpp` — generic camera wrapper using these formulas diff --git a/docs/engines/iw_engine/mesh_trait.md b/docs/engines/iw_engine/mesh_trait.md new file mode 100644 index 00000000..7f8b86a7 --- /dev/null +++ b/docs/engines/iw_engine/mesh_trait.md @@ -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; +} +``` + +--- + +## 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* diff --git a/docs/engines/iw_engine/pred_engine_trait.md b/docs/engines/iw_engine/pred_engine_trait.md new file mode 100644 index 00000000..85af5c13 --- /dev/null +++ b/docs/engines/iw_engine/pred_engine_trait.md @@ -0,0 +1,198 @@ +# `omath::iw_engine::PredEngineTrait` — projectile prediction trait + +> Header: `omath/engines/iw_engine/traits/pred_engine_trait.hpp` +> Namespace: `omath::iw_engine` +> Purpose: provide IW Engine (Call of Duty)-specific projectile and target prediction for ballistic calculations + +--- + +## Summary + +`PredEngineTrait` implements engine-specific helpers for **projectile prediction**: + +* `predict_projectile_position` – computes where a projectile will be after `time` seconds +* `predict_target_position` – computes where a moving target will be after `time` seconds +* `calc_vector_2d_distance` – horizontal distance (X/Y plane, ignoring Z) +* `get_vector_height_coordinate` – extracts vertical coordinate (Z in IW Engine) +* `calc_viewpoint_from_angles` – computes aim point given pitch angle +* `calc_direct_pitch_angle` – pitch angle to look from origin to target +* `calc_direct_yaw_angle` – yaw angle to look from origin to target + +These methods satisfy the `PredEngineTraitConcept` required by generic projectile prediction algorithms. + +--- + +## API + +```cpp +namespace omath::iw_engine { + +class PredEngineTrait final { +public: + // Predict projectile position after `time` seconds + static constexpr Vector3 + predict_projectile_position(const projectile_prediction::Projectile& projectile, + float pitch, float yaw, float time, + float gravity) noexcept; + + // Predict target position after `time` seconds + static constexpr Vector3 + predict_target_position(const projectile_prediction::Target& target, + float time, float gravity) noexcept; + + // Compute horizontal (2D) distance + static float + calc_vector_2d_distance(const Vector3& delta) noexcept; + + // Get vertical coordinate (Z in IW Engine) + static constexpr float + get_vector_height_coordinate(const Vector3& vec) noexcept; + + // Compute aim point from angles + static Vector3 + calc_viewpoint_from_angles(const projectile_prediction::Projectile& projectile, + Vector3 predicted_target_position, + std::optional projectile_pitch) noexcept; + + // Compute pitch angle to look at target + static float + calc_direct_pitch_angle(const Vector3& origin, + const Vector3& view_to) noexcept; + + // Compute yaw angle to look at target + static float + calc_direct_yaw_angle(const Vector3& origin, + const Vector3& view_to) noexcept; +}; + +} // namespace omath::iw_engine +``` + +--- + +## Projectile prediction + +```cpp +auto pos = PredEngineTrait::predict_projectile_position( + projectile, // initial position, speed, gravity scale + pitch_deg, // launch pitch (positive = up) + yaw_deg, // launch yaw + time, // time in seconds + gravity // gravity constant (e.g., 800 units/s²) +); +``` + +Computes: + +1. Forward vector from pitch/yaw (using `forward_vector`) +2. Initial velocity: `forward * launch_speed` +3. Position after `time`: `origin + velocity*time - 0.5*gravity*gravityScale*time²` (Z component only) + +**Note**: Negative pitch in `forward_vector` convention → positive pitch looks up. + +--- + +## Target prediction + +```cpp +auto pos = PredEngineTrait::predict_target_position( + target, // position, velocity, airborne flag + time, // time in seconds + gravity // gravity constant +); +``` + +Simple linear extrapolation plus gravity if target is airborne: + +``` +predicted = origin + velocity * time +if (airborne) + predicted.z -= 0.5 * gravity * time² +``` + +--- + +## Distance & height helpers + +* `calc_vector_2d_distance(delta)` → `sqrt(delta.x² + delta.y²)` (horizontal distance) +* `get_vector_height_coordinate(vec)` → `vec.z` (vertical coordinate in IW Engine) + +Used to compute ballistic arc parameters. + +--- + +## Aim angle calculation + +* `calc_direct_pitch_angle(origin, target)` → pitch in degrees to look from `origin` to `target` + - Formula: `asin(Δz / distance)` converted to degrees + - Positive = looking up, negative = looking down + +* `calc_direct_yaw_angle(origin, target)` → yaw in degrees to look from `origin` to `target` + - Formula: `atan2(Δy, Δx)` converted to degrees + - Horizontal rotation around Z-axis + +--- + +## Viewpoint from angles + +```cpp +auto aim_point = PredEngineTrait::calc_viewpoint_from_angles( + projectile, + predicted_target_pos, + optional_pitch_deg +); +``` + +Computes where to aim in 3D space given a desired pitch angle. Uses horizontal distance and `tan(pitch)` to compute height offset. + +--- + +## Conventions + +* **Coordinate system**: Z-up (height increases with Z) +* **Angles**: pitch in [-89°, +89°], yaw in [-180°, +180°] +* **Gravity**: applied downward along -Z axis +* **Pitch convention**: +89° = straight up, -89° = straight down + +--- + +## Usage example + +```cpp +using namespace omath::iw_engine; +using namespace omath::projectile_prediction; + +Projectile proj{ + .m_origin = {0, 0, 100}, + .m_launch_speed = 1200.0f, + .m_gravity_scale = 1.0f +}; + +Target tgt{ + .m_origin = {800, 300, 100}, + .m_velocity = {15, 8, 0}, + .m_is_airborne = false +}; + +float gravity = 800.0f; +float time = 0.5f; + +// Predict where target will be +auto target_pos = PredEngineTrait::predict_target_position(tgt, time, gravity); + +// Compute aim angles +float pitch = PredEngineTrait::calc_direct_pitch_angle(proj.m_origin, target_pos); +float yaw = PredEngineTrait::calc_direct_yaw_angle(proj.m_origin, target_pos); + +// Predict projectile position with those angles +auto proj_pos = PredEngineTrait::predict_projectile_position(proj, pitch, yaw, time, gravity); +``` + +--- + +## See also + +* `omath/engines/iw_engine/formulas.hpp` — direction vectors and matrix builders +* `omath/projectile_prediction/projectile.hpp` — `Projectile` struct +* `omath/projectile_prediction/target.hpp` — `Target` struct +* Generic projectile prediction algorithms that use `PredEngineTraitConcept` diff --git a/docs/engines/opengl_engine/camera_trait.md b/docs/engines/opengl_engine/camera_trait.md new file mode 100644 index 00000000..a7e288a8 --- /dev/null +++ b/docs/engines/opengl_engine/camera_trait.md @@ -0,0 +1,110 @@ +# `omath::opengl_engine::CameraTrait` — plug-in trait for `projection::Camera` + +> Header: `omath/engines/opengl_engine/traits/camera_trait.hpp` • Impl: `omath/engines/opengl_engine/traits/camera_trait.cpp` +> Namespace: `omath::opengl_engine` +> Purpose: provide OpenGL-style **look-at**, **view**, and **projection** math to the generic `omath::projection::Camera` (satisfies `CameraEngineConcept`). + +--- + +## Summary + +`CameraTrait` exposes three `static` functions: + +* `calc_look_at_angle(origin, look_at)` – computes Euler angles so the camera at `origin` looks at `look_at`. Implementation normalizes the direction, computes **pitch** as `asin(dir.y)` and **yaw** as `-atan2(dir.x, -dir.z)`; **roll** is `0`. Pitch/yaw are returned using the project's strong angle types (`PitchAngle`, `YawAngle`, `RollAngle`). +* `calc_view_matrix(angles, origin)` – delegates to OpenGL formulas `opengl_engine::calc_view_matrix`, producing a `Mat4X4` view matrix (column-major) for the given angles and origin. +* `calc_projection_matrix(fov, viewport, near, far)` – builds a perspective projection by calling `calc_perspective_projection_matrix(fov_degrees, aspect, near, far)`, where `aspect = viewport.aspect_ratio()`. Accepts `FieldOfView` (degrees). + +The trait's types (`ViewAngles`, `Mat4X4`, angle aliases) and helpers live in the OpenGL math headers included by the trait (`formulas.hpp`) and the shared projection header (`projection/camera.hpp`). + +--- + +## API + +```cpp +namespace omath::opengl_engine { + +class CameraTrait final { +public: + // Compute Euler angles (pitch/yaw/roll) to look from cam_origin to look_at. + static ViewAngles + calc_look_at_angle(const Vector3& cam_origin, + const Vector3& look_at) noexcept; + + // Build view matrix for given angles and origin (column-major). + static Mat4X4 + calc_view_matrix(const ViewAngles& angles, + const Vector3& cam_origin) noexcept; + + // Build perspective projection from FOV (deg), viewport, near/far (column-major). + static Mat4X4 + calc_projection_matrix(const projection::FieldOfView& fov, + const projection::ViewPort& view_port, + float near, float far) noexcept; +}; + +} // namespace omath::opengl_engine +``` + +Uses: `Vector3`, `ViewAngles` (pitch/yaw/roll), `Mat4X4`, `projection::FieldOfView`, `projection::ViewPort`. + +--- + +## Behavior & conventions + +* **Angles from look-at** (Y-up, -Z forward coordinate system): + + ``` + dir = normalize(look_at - origin) + pitch = asin(dir.y) // +Y is up + yaw = -atan2(dir.x, -dir.z) // horizontal rotation + roll = 0 + ``` + + Returned as `PitchAngle::from_radians(...)`, `YawAngle::from_radians(...)`, etc. + +* **View matrix**: built by the OpenGL helper `opengl_engine::calc_view_matrix(angles, origin)` to match OpenGL's right-handed, Y-up, -Z forward conventions. Matrix is **column-major**. + +* **Projection**: uses `calc_perspective_projection_matrix(fov.as_degrees(), viewport.aspect_ratio(), near, far)`. Pass your **vertical FOV** in degrees via `FieldOfView`; the helper computes a standard perspective matrix (column-major). + +--- + +## Using with `projection::Camera` + +Create a camera whose math is driven by this trait: + +```cpp +using Mat4 = Mat4X4; // from OpenGL math headers (column-major) +using Angs = ViewAngles; // pitch/yaw/roll type +using GLcam = omath::projection::Camera; + +omath::projection::ViewPort vp{1920.f, 1080.f}; +auto fov = omath::projection::FieldOfView::from_degrees(45.f); + +GLcam cam( + /*position*/ {5.f, 3.f, 5.f}, + /*angles*/ omath::opengl_engine::CameraTrait::calc_look_at_angle({5,3,5},{0,0,0}), + /*viewport*/ vp, + /*fov*/ fov, + /*near*/ 0.1f, + /*far*/ 100.f +); +``` + +This satisfies `CameraEngineConcept` expected by `projection::Camera` (look-at, view, projection) as declared in the trait header. + +--- + +## Notes & tips + +* Ensure your `ViewAngles` aliases (`PitchAngle`, `YawAngle`, `RollAngle`) match the project's angle policy (ranges/normalization). The implementation constructs them **from radians**. +* `aspect_ratio()` is taken directly from `ViewPort` (`width / height`), so keep both positive and non-zero. +* `near` must be > 0 and `< far` for a valid projection matrix (enforced by your math helpers). +* OpenGL uses **Y-up, -Z forward**: pitch angles control vertical look (positive = up), yaw controls horizontal rotation. +* Matrices are **column-major** (no transpose needed for OpenGL shaders). + +--- + +## See also + +* OpenGL math helpers in `omath/engines/opengl_engine/formulas.hpp` (view/projection builders used above). +* Generic camera wrapper `omath::projection::Camera` and its `CameraEngineConcept` (this trait is designed to plug straight into it). diff --git a/docs/engines/opengl_engine/constants.md b/docs/engines/opengl_engine/constants.md new file mode 100644 index 00000000..8bcf9ce8 --- /dev/null +++ b/docs/engines/opengl_engine/constants.md @@ -0,0 +1,78 @@ +# `omath::opengl_engine` — types & constants + +> Header: `omath/engines/opengl_engine/constants.hpp` +> Namespace: `omath::opengl_engine` +> Purpose: define OpenGL coordinate system, matrix types, and angle ranges + +--- + +## Summary + +The **OpenGL Engine** uses a **Y-up, right-handed** coordinate system: + +* **Up** = `{0, 1, 0}` (Y-axis) +* **Right** = `{1, 0, 0}` (X-axis) +* **Forward** = `{0, 0, -1}` (negative Z-axis) + +Matrices are **column-major**. Angles are **clamped pitch** (±90°) and **normalized yaw/roll** (±180°). + +--- + +## Constants + +```cpp +namespace omath::opengl_engine { + constexpr Vector3 k_abs_up = {0, 1, 0}; + constexpr Vector3 k_abs_right = {1, 0, 0}; + constexpr Vector3 k_abs_forward = {0, 0, -1}; +} +``` + +These basis vectors define the engine's **world coordinate frame**. + +--- + +## Matrix types + +```cpp +using Mat4X4 = Mat<4, 4, float, MatStoreType::COLUMN_MAJOR>; +using Mat3X3 = Mat<4, 4, float, MatStoreType::COLUMN_MAJOR>; +using Mat1X3 = Mat<1, 3, float, MatStoreType::COLUMN_MAJOR>; +``` + +**Column-major** storage means columns are contiguous in memory. This matches OpenGL's native matrix layout and shader expectations (GLSL). + +--- + +## Angle types + +```cpp +using PitchAngle = Angle; +using YawAngle = Angle; +using RollAngle = Angle; + +using ViewAngles = omath::ViewAngles; +``` + +* **PitchAngle**: clamped to **[-90°, +90°]** (looking down vs. up) +* **YawAngle**: normalized to **[-180°, +180°]** (horizontal rotation) +* **RollAngle**: normalized to **[-180°, +180°]** (camera roll) + +`ViewAngles` bundles all three into a single type for camera/view transforms. + +--- + +## Coordinate system notes + +* **Y-up**: gravity points along `-Y`, height increases with `+Y` +* **Right-handed**: cross product `right × up = forward` (forward is `-Z`) +* **Forward = -Z**: the camera looks down the negative Z-axis (OpenGL convention) +* This matches **OpenGL** conventions for 3D graphics pipelines + +--- + +## See also + +* `omath/engines/opengl_engine/formulas.hpp` — view/projection matrix builders +* `omath/trigonometry/angle.hpp` — angle normalization & clamping helpers +* `omath/trigonometry/view_angles.hpp` — generic pitch/yaw/roll wrapper diff --git a/docs/engines/opengl_engine/formulas.md b/docs/engines/opengl_engine/formulas.md new file mode 100644 index 00000000..c2515d55 --- /dev/null +++ b/docs/engines/opengl_engine/formulas.md @@ -0,0 +1,140 @@ +# `omath::opengl_engine` — formulas & matrix helpers + +> Header: `omath/engines/opengl_engine/formulas.hpp` +> Namespace: `omath::opengl_engine` +> Purpose: compute direction vectors, rotation matrices, view matrices, and perspective projections for OpenGL + +--- + +## Summary + +This header provides **OpenGL**-specific math for: + +* **Direction vectors** (`forward`, `right`, `up`) from `ViewAngles` +* **Rotation matrices** from Euler angles +* **View matrices** (camera transforms) +* **Perspective projection** matrices + +All functions respect OpenGL's **Y-up, right-handed** coordinate system with **forward = -Z**. + +--- + +## API + +```cpp +namespace omath::opengl_engine { + + // Compute forward direction from pitch/yaw/roll + [[nodiscard]] + Vector3 forward_vector(const ViewAngles& angles) noexcept; + + // Compute right direction from pitch/yaw/roll + [[nodiscard]] + Vector3 right_vector(const ViewAngles& angles) noexcept; + + // Compute up direction from pitch/yaw/roll + [[nodiscard]] + Vector3 up_vector(const ViewAngles& angles) noexcept; + + // Build 3x3 rotation matrix from angles + [[nodiscard]] + Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept; + + // Build view matrix (camera space transform) + [[nodiscard]] + Mat4X4 calc_view_matrix(const ViewAngles& angles, + const Vector3& cam_origin) noexcept; + + // Build perspective projection matrix + [[nodiscard]] + Mat4X4 calc_perspective_projection_matrix(float field_of_view, + float aspect_ratio, + float near, float far) noexcept; + +} // namespace omath::opengl_engine +``` + +--- + +## Direction vectors + +Given camera angles (pitch/yaw/roll): + +* `forward_vector(angles)` → unit vector pointing where the camera looks (typically `-Z` direction) +* `right_vector(angles)` → unit vector pointing to the camera's right (`+X` direction) +* `up_vector(angles)` → unit vector pointing upward relative to the camera (`+Y` direction) + +These are used for movement, aim direction, and building coordinate frames. + +--- + +## Rotation & view matrices + +* `rotation_matrix(angles)` → 3×3 (or 4×4) rotation matrix from Euler angles (column-major) +* `calc_view_matrix(angles, origin)` → camera view matrix (column-major) + +The view matrix transforms world coordinates into camera space (origin at camera, axes aligned with camera orientation). + +**Note**: Matrices are **column-major** to match OpenGL/GLSL conventions. No transpose needed when uploading to shaders. + +--- + +## Perspective projection + +```cpp +Mat4X4 proj = calc_perspective_projection_matrix( + fov_degrees, // vertical field of view (e.g., 45) + aspect_ratio, // width / height (e.g., 16/9) + near_plane, // e.g., 0.1 + far_plane // e.g., 100.0 +); +``` + +Produces a **perspective projection matrix** suitable for OpenGL rendering. Combined with the view matrix, this implements the standard camera transform chain. + +--- + +## Usage example + +```cpp +using namespace omath::opengl_engine; + +// Camera setup +ViewAngles angles = { + PitchAngle::from_degrees(-20.0f), + YawAngle::from_degrees(135.0f), + RollAngle::from_degrees(0.0f) +}; +Vector3 cam_pos{5.0f, 3.0f, 5.0f}; + +// Compute direction +auto forward = forward_vector(angles); +auto right = right_vector(angles); +auto up = up_vector(angles); + +// Build matrices (column-major for OpenGL) +auto view_mat = calc_view_matrix(angles, cam_pos); +auto proj_mat = calc_perspective_projection_matrix(45.0f, 16.0f/9.0f, 0.1f, 100.0f); + +// Upload to OpenGL shaders (no transpose needed) +glUniformMatrix4fv(view_loc, 1, GL_FALSE, view_mat.data()); +glUniformMatrix4fv(proj_loc, 1, GL_FALSE, proj_mat.data()); +``` + +--- + +## Conventions + +* **Angles**: pitch (up/down), yaw (left/right), roll (tilt) +* **Pitch**: positive = looking up, negative = looking down +* **Yaw**: increases counter-clockwise from the -Z axis +* **Coordinate system**: Y-up, -Z-forward, X-right (right-handed) +* **Matrix storage**: column-major (matches OpenGL/GLSL) + +--- + +## See also + +* `omath/engines/opengl_engine/constants.hpp` — coordinate frame & angle types +* `omath/engines/opengl_engine/traits/camera_trait.hpp` — plug-in for generic `Camera` +* `omath/projection/camera.hpp` — generic camera wrapper using these formulas diff --git a/docs/engines/opengl_engine/mesh_trait.md b/docs/engines/opengl_engine/mesh_trait.md new file mode 100644 index 00000000..009e2070 --- /dev/null +++ b/docs/engines/opengl_engine/mesh_trait.md @@ -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; +} +``` + +--- + +## 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* diff --git a/docs/engines/opengl_engine/pred_engine_trait.md b/docs/engines/opengl_engine/pred_engine_trait.md new file mode 100644 index 00000000..84cccc38 --- /dev/null +++ b/docs/engines/opengl_engine/pred_engine_trait.md @@ -0,0 +1,199 @@ +# `omath::opengl_engine::PredEngineTrait` — projectile prediction trait + +> Header: `omath/engines/opengl_engine/traits/pred_engine_trait.hpp` +> Namespace: `omath::opengl_engine` +> Purpose: provide OpenGL-specific projectile and target prediction for ballistic calculations + +--- + +## Summary + +`PredEngineTrait` implements engine-specific helpers for **projectile prediction**: + +* `predict_projectile_position` – computes where a projectile will be after `time` seconds +* `predict_target_position` – computes where a moving target will be after `time` seconds +* `calc_vector_2d_distance` – horizontal distance (X/Z plane, ignoring Y) +* `get_vector_height_coordinate` – extracts vertical coordinate (Y in OpenGL) +* `calc_viewpoint_from_angles` – computes aim point given pitch angle +* `calc_direct_pitch_angle` – pitch angle to look from origin to target +* `calc_direct_yaw_angle` – yaw angle to look from origin to target + +These methods satisfy the `PredEngineTraitConcept` required by generic projectile prediction algorithms. + +--- + +## API + +```cpp +namespace omath::opengl_engine { + +class PredEngineTrait final { +public: + // Predict projectile position after `time` seconds + static constexpr Vector3 + predict_projectile_position(const projectile_prediction::Projectile& projectile, + float pitch, float yaw, float time, + float gravity) noexcept; + + // Predict target position after `time` seconds + static constexpr Vector3 + predict_target_position(const projectile_prediction::Target& target, + float time, float gravity) noexcept; + + // Compute horizontal (2D) distance + static float + calc_vector_2d_distance(const Vector3& delta) noexcept; + + // Get vertical coordinate (Y in OpenGL) + static constexpr float + get_vector_height_coordinate(const Vector3& vec) noexcept; + + // Compute aim point from angles + static Vector3 + calc_viewpoint_from_angles(const projectile_prediction::Projectile& projectile, + Vector3 predicted_target_position, + std::optional projectile_pitch) noexcept; + + // Compute pitch angle to look at target + static float + calc_direct_pitch_angle(const Vector3& origin, + const Vector3& view_to) noexcept; + + // Compute yaw angle to look at target + static float + calc_direct_yaw_angle(const Vector3& origin, + const Vector3& view_to) noexcept; +}; + +} // namespace omath::opengl_engine +``` + +--- + +## Projectile prediction + +```cpp +auto pos = PredEngineTrait::predict_projectile_position( + projectile, // initial position, speed, gravity scale + pitch_deg, // launch pitch (positive = up) + yaw_deg, // launch yaw + time, // time in seconds + gravity // gravity constant (e.g., 9.81 m/s²) +); +``` + +Computes: + +1. Forward vector from pitch/yaw (using `forward_vector`) +2. Initial velocity: `forward * launch_speed` +3. Position after `time`: `origin + velocity*time - 0.5*gravity*gravityScale*time²` (Y component only) + +**Note**: Negative pitch in `forward_vector` convention → positive pitch looks up. + +--- + +## Target prediction + +```cpp +auto pos = PredEngineTrait::predict_target_position( + target, // position, velocity, airborne flag + time, // time in seconds + gravity // gravity constant +); +``` + +Simple linear extrapolation plus gravity if target is airborne: + +``` +predicted = origin + velocity * time +if (airborne) + predicted.y -= 0.5 * gravity * time² +``` + +--- + +## Distance & height helpers + +* `calc_vector_2d_distance(delta)` → `sqrt(delta.x² + delta.z²)` (horizontal distance) +* `get_vector_height_coordinate(vec)` → `vec.y` (vertical coordinate in OpenGL) + +Used to compute ballistic arc parameters. + +--- + +## Aim angle calculation + +* `calc_direct_pitch_angle(origin, target)` → pitch in degrees to look from `origin` to `target` + - Formula: `asin(Δy / distance)` converted to degrees (direction normalized first) + - Positive = looking up, negative = looking down + +* `calc_direct_yaw_angle(origin, target)` → yaw in degrees to look from `origin` to `target` + - Formula: `-atan2(Δx, -Δz)` converted to degrees (direction normalized first) + - Horizontal rotation around Y-axis (accounts for -Z forward convention) + +--- + +## Viewpoint from angles + +```cpp +auto aim_point = PredEngineTrait::calc_viewpoint_from_angles( + projectile, + predicted_target_pos, + optional_pitch_deg +); +``` + +Computes where to aim in 3D space given a desired pitch angle. Uses horizontal distance and `tan(pitch)` to compute height offset. Result has adjusted Y coordinate. + +--- + +## Conventions + +* **Coordinate system**: Y-up, -Z forward (height increases with Y) +* **Angles**: pitch in [-90°, +90°], yaw in [-180°, +180°] +* **Gravity**: applied downward along -Y axis +* **Pitch convention**: +90° = straight up, -90° = straight down +* **Forward direction**: negative Z-axis + +--- + +## Usage example + +```cpp +using namespace omath::opengl_engine; +using namespace omath::projectile_prediction; + +Projectile proj{ + .m_origin = {0, 2, 0}, + .m_launch_speed = 30.0f, + .m_gravity_scale = 1.0f +}; + +Target tgt{ + .m_origin = {10, 2, -15}, + .m_velocity = {0.5f, 0, -1.0f}, + .m_is_airborne = false +}; + +float gravity = 9.81f; +float time = 0.5f; + +// Predict where target will be +auto target_pos = PredEngineTrait::predict_target_position(tgt, time, gravity); + +// Compute aim angles +float pitch = PredEngineTrait::calc_direct_pitch_angle(proj.m_origin, target_pos); +float yaw = PredEngineTrait::calc_direct_yaw_angle(proj.m_origin, target_pos); + +// Predict projectile position with those angles +auto proj_pos = PredEngineTrait::predict_projectile_position(proj, pitch, yaw, time, gravity); +``` + +--- + +## See also + +* `omath/engines/opengl_engine/formulas.hpp` — direction vectors and matrix builders +* `omath/projectile_prediction/projectile.hpp` — `Projectile` struct +* `omath/projectile_prediction/target.hpp` — `Target` struct +* Generic projectile prediction algorithms that use `PredEngineTraitConcept` diff --git a/docs/engines/source_engine/camera_trait.md b/docs/engines/source_engine/camera_trait.md new file mode 100644 index 00000000..610fc610 --- /dev/null +++ b/docs/engines/source_engine/camera_trait.md @@ -0,0 +1,113 @@ +# `omath::source_engine::CameraTrait` — plug-in trait for `projection::Camera` + +> Header: `omath/engines/source_engine/traits/camera_trait.hpp` • Impl: `omath/engines/source_engine/traits/camera_trait.cpp` +> Namespace: `omath::source_engine` +> Purpose: provide Source Engine-style **look-at**, **view**, and **projection** math to the generic `omath::projection::Camera` (satisfies `CameraEngineConcept`). + +--- + +## Summary + +`CameraTrait` exposes three `static` functions: + +* `calc_look_at_angle(origin, look_at)` – computes Euler angles so the camera at `origin` looks at `look_at`. Implementation normalizes the direction, computes **pitch** as `asin(dir.z)` and **yaw** as `atan2(dir.y, dir.x)`; **roll** is `0`. Pitch/yaw are returned using the project's strong angle types (`PitchAngle`, `YawAngle`, `RollAngle`). +* `calc_view_matrix(angles, origin)` – delegates to Source Engine formulas `source_engine::calc_view_matrix`, producing a `Mat4X4` view matrix for the given angles and origin. +* `calc_projection_matrix(fov, viewport, near, far)` – builds a perspective projection by calling `calc_perspective_projection_matrix(fov_degrees, aspect, near, far)`, where `aspect = viewport.aspect_ratio()`. Accepts `FieldOfView` (degrees). + +The trait's types (`ViewAngles`, `Mat4X4`, angle aliases) and helpers live in the Source Engine math headers included by the trait (`formulas.hpp`) and the shared projection header (`projection/camera.hpp`). + +--- + +## API + +```cpp +namespace omath::source_engine { + +class CameraTrait final { +public: + // Compute Euler angles (pitch/yaw/roll) to look from cam_origin to look_at. + static ViewAngles + calc_look_at_angle(const Vector3& cam_origin, + const Vector3& look_at) noexcept; + + // Build view matrix for given angles and origin. + static Mat4X4 + calc_view_matrix(const ViewAngles& angles, + const Vector3& cam_origin) noexcept; + + // Build perspective projection from FOV (deg), viewport, near/far. + static Mat4X4 + calc_projection_matrix(const projection::FieldOfView& fov, + const projection::ViewPort& view_port, + float near, float far) noexcept; +}; + +} // namespace omath::source_engine +``` + +Uses: `Vector3`, `ViewAngles` (pitch/yaw/roll), `Mat4X4`, `projection::FieldOfView`, `projection::ViewPort`. + +--- + +## Behavior & conventions + +* **Angles from look-at** (Z-up coordinate system): + + ``` + dir = normalize(look_at - origin) + pitch = asin(dir.z) // +Z is up + yaw = atan2(dir.y, dir.x) // horizontal rotation + roll = 0 + ``` + + Returned as `PitchAngle::from_radians(...)`, `YawAngle::from_radians(...)`, etc. + +* **View matrix**: built by the Source Engine helper `source_engine::calc_view_matrix(angles, origin)` to match the engine's handedness and axis conventions. + +* **Projection**: uses `calc_perspective_projection_matrix(fov.as_degrees(), viewport.aspect_ratio(), near, far)`. Pass your **vertical FOV** in degrees via `FieldOfView`; the helper computes a standard perspective matrix. + +--- + +## Using with `projection::Camera` + +Create a camera whose math is driven by this trait: + +```cpp +using Mat4 = Mat4X4; // from Source Engine math headers +using Angs = ViewAngles; // pitch/yaw/roll type +using SEcam = omath::projection::Camera; + +omath::projection::ViewPort vp{1920.f, 1080.f}; +auto fov = omath::projection::FieldOfView::from_degrees(90.f); + +SEcam cam( + /*position*/ {100.f, 50.f, 80.f}, + /*angles*/ omath::source_engine::CameraTrait::calc_look_at_angle({100,50,80},{0,0,80}), + /*viewport*/ vp, + /*fov*/ fov, + /*near*/ 0.1f, + /*far*/ 1000.f +); +``` + +This satisfies `CameraEngineConcept` expected by `projection::Camera` (look-at, view, projection) as declared in the trait header. + +--- + +## Notes & tips + +* Ensure your `ViewAngles` aliases (`PitchAngle`, `YawAngle`, `RollAngle`) match the project's angle policy (ranges/normalization). The implementation constructs them **from radians**. +* `aspect_ratio()` is taken directly from `ViewPort` (`width / height`), so keep both positive and non-zero. +* `near` must be > 0 and `< far` for a valid projection matrix (enforced by your math helpers). +* Source Engine uses **Z-up**: pitch angles control vertical look, positive = up. + +--- + +## See also + +* [Source Engine Formulas](formulas.md) - View/projection matrix builders +* [Source Engine Constants](constants.md) - Engine-specific constants +* [Source Engine Pred Engine Trait](pred_engine_trait.md) - Projectile prediction for Source Engine +* [Generic Camera Documentation](../../projection/camera.md) - Camera base class +* [Getting Started Guide](../../getting_started.md) - Quick start with OMath +* [Tutorials - World-to-Screen](../../tutorials.md#tutorial-2-world-to-screen-projection) - Projection tutorial diff --git a/docs/engines/source_engine/constants.md b/docs/engines/source_engine/constants.md new file mode 100644 index 00000000..829a9341 --- /dev/null +++ b/docs/engines/source_engine/constants.md @@ -0,0 +1,77 @@ +# `omath::source_engine` — types & constants + +> Header: `omath/engines/source_engine/constants.hpp` +> Namespace: `omath::source_engine` +> Purpose: define Source Engine coordinate system, matrix types, and angle ranges + +--- + +## Summary + +The **Source Engine** uses a **Z-up, right-handed** coordinate system: + +* **Up** = `{0, 0, 1}` (Z-axis) +* **Right** = `{0, -1, 0}` (negative Y-axis) +* **Forward** = `{1, 0, 0}` (X-axis) + +Matrices are **row-major**. Angles are **clamped pitch** (±89°) and **normalized yaw/roll** (±180°). + +--- + +## Constants + +```cpp +namespace omath::source_engine { + constexpr Vector3 k_abs_up = {0, 0, 1}; + constexpr Vector3 k_abs_right = {0, -1, 0}; + constexpr Vector3 k_abs_forward = {1, 0, 0}; +} +``` + +These basis vectors define the engine's **world coordinate frame**. + +--- + +## Matrix types + +```cpp +using Mat4X4 = Mat<4, 4, float, MatStoreType::ROW_MAJOR>; +using Mat3X3 = Mat<4, 4, float, MatStoreType::ROW_MAJOR>; +using Mat1X3 = Mat<1, 3, float, MatStoreType::ROW_MAJOR>; +``` + +**Row-major** storage means rows are contiguous in memory. Suitable for CPU-side transforms and typical C++ math libraries. + +--- + +## Angle types + +```cpp +using PitchAngle = Angle; +using YawAngle = Angle; +using RollAngle = Angle; + +using ViewAngles = omath::ViewAngles; +``` + +* **PitchAngle**: clamped to **[-89°, +89°]** (looking down vs. up) +* **YawAngle**: normalized to **[-180°, +180°]** (horizontal rotation) +* **RollAngle**: normalized to **[-180°, +180°]** (camera roll) + +`ViewAngles` bundles all three into a single type for camera/view transforms. + +--- + +## Coordinate system notes + +* **Z-up**: gravity points along `-Z`, height increases with `+Z` +* **Right-handed**: cross product `forward × right = up` holds +* This matches **Source Engine** (Half-Life 2, TF2, CS:GO, etc.) conventions + +--- + +## See also + +* `omath/engines/source_engine/formulas.hpp` — view/projection matrix builders +* `omath/trigonometry/angle.hpp` — angle normalization & clamping helpers +* `omath/trigonometry/view_angles.hpp` — generic pitch/yaw/roll wrapper diff --git a/docs/engines/source_engine/formulas.md b/docs/engines/source_engine/formulas.md new file mode 100644 index 00000000..eba0c907 --- /dev/null +++ b/docs/engines/source_engine/formulas.md @@ -0,0 +1,135 @@ +# `omath::source_engine` — formulas & matrix helpers + +> Header: `omath/engines/source_engine/formulas.hpp` +> Namespace: `omath::source_engine` +> Purpose: compute direction vectors, rotation matrices, view matrices, and perspective projections for Source Engine + +--- + +## Summary + +This header provides **Source Engine**-specific math for: + +* **Direction vectors** (`forward`, `right`, `up`) from `ViewAngles` +* **Rotation matrices** from Euler angles +* **View matrices** (camera transforms) +* **Perspective projection** matrices + +All functions respect Source Engine's **Z-up, right-handed** coordinate system. + +--- + +## API + +```cpp +namespace omath::source_engine { + + // Compute forward direction from pitch/yaw/roll + [[nodiscard]] + Vector3 forward_vector(const ViewAngles& angles) noexcept; + + // Compute right direction from pitch/yaw/roll + [[nodiscard]] + Vector3 right_vector(const ViewAngles& angles) noexcept; + + // Compute up direction from pitch/yaw/roll + [[nodiscard]] + Vector3 up_vector(const ViewAngles& angles) noexcept; + + // Build 3x3 rotation matrix from angles + [[nodiscard]] + Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept; + + // Build view matrix (camera space transform) + [[nodiscard]] + Mat4X4 calc_view_matrix(const ViewAngles& angles, + const Vector3& cam_origin) noexcept; + + // Build perspective projection matrix + [[nodiscard]] + Mat4X4 calc_perspective_projection_matrix(float field_of_view, + float aspect_ratio, + float near, float far) noexcept; + +} // namespace omath::source_engine +``` + +--- + +## Direction vectors + +Given camera angles (pitch/yaw/roll): + +* `forward_vector(angles)` → unit vector pointing where the camera looks +* `right_vector(angles)` → unit vector pointing to the camera's right +* `up_vector(angles)` → unit vector pointing upward relative to the camera + +These are used for movement, aim direction, and building coordinate frames. + +--- + +## Rotation & view matrices + +* `rotation_matrix(angles)` → 3×3 (or 4×4) rotation matrix from Euler angles +* `calc_view_matrix(angles, origin)` → camera view matrix + +The view matrix transforms world coordinates into camera space (origin at camera, axes aligned with camera orientation). + +--- + +## Perspective projection + +```cpp +Mat4X4 proj = calc_perspective_projection_matrix( + fov_degrees, // vertical field of view (e.g., 90) + aspect_ratio, // width / height (e.g., 16/9) + near_plane, // e.g., 0.1 + far_plane // e.g., 1000.0 +); +``` + +Produces a **perspective projection matrix** suitable for 3D rendering pipelines. Combined with the view matrix, this implements the standard camera transform chain. + +--- + +## Usage example + +```cpp +using namespace omath::source_engine; + +// Camera setup +ViewAngles angles = { + PitchAngle::from_degrees(-15.0f), + YawAngle::from_degrees(45.0f), + RollAngle::from_degrees(0.0f) +}; +Vector3 cam_pos{100.0f, 50.0f, 80.0f}; + +// Compute direction +auto forward = forward_vector(angles); +auto right = right_vector(angles); +auto up = up_vector(angles); + +// Build matrices +auto view_mat = calc_view_matrix(angles, cam_pos); +auto proj_mat = calc_perspective_projection_matrix(90.0f, 16.0f/9.0f, 0.1f, 1000.0f); + +// Use view_mat and proj_mat for rendering... +``` + +--- + +## Conventions + +* **Angles**: pitch (up/down), yaw (left/right), roll (tilt) +* **Pitch**: positive = looking up, negative = looking down +* **Yaw**: increases counter-clockwise from the +X axis +* **Coordinate system**: Z-up, X-forward, Y-right (negative in code convention) + +--- + +## See also + +* `omath/engines/source_engine/constants.hpp` — coordinate frame & angle types +* `omath/engines/source_engine/traits/camera_trait.hpp` — plug-in for generic `Camera` +* `omath/projection/camera.hpp` — generic camera wrapper using these formulas diff --git a/docs/engines/source_engine/mesh_trait.md b/docs/engines/source_engine/mesh_trait.md new file mode 100644 index 00000000..e866dbc6 --- /dev/null +++ b/docs/engines/source_engine/mesh_trait.md @@ -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 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; +} +``` + +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 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* diff --git a/docs/engines/source_engine/pred_engine_trait.md b/docs/engines/source_engine/pred_engine_trait.md new file mode 100644 index 00000000..63f50241 --- /dev/null +++ b/docs/engines/source_engine/pred_engine_trait.md @@ -0,0 +1,198 @@ +# `omath::source_engine::PredEngineTrait` — projectile prediction trait + +> Header: `omath/engines/source_engine/traits/pred_engine_trait.hpp` +> Namespace: `omath::source_engine` +> Purpose: provide Source Engine-specific projectile and target prediction for ballistic calculations + +--- + +## Summary + +`PredEngineTrait` implements engine-specific helpers for **projectile prediction**: + +* `predict_projectile_position` – computes where a projectile will be after `time` seconds +* `predict_target_position` – computes where a moving target will be after `time` seconds +* `calc_vector_2d_distance` – horizontal distance (X/Y plane, ignoring Z) +* `get_vector_height_coordinate` – extracts vertical coordinate (Z in Source Engine) +* `calc_viewpoint_from_angles` – computes aim point given pitch angle +* `calc_direct_pitch_angle` – pitch angle to look from origin to target +* `calc_direct_yaw_angle` – yaw angle to look from origin to target + +These methods satisfy the `PredEngineTraitConcept` required by generic projectile prediction algorithms. + +--- + +## API + +```cpp +namespace omath::source_engine { + +class PredEngineTrait final { +public: + // Predict projectile position after `time` seconds + static constexpr Vector3 + predict_projectile_position(const projectile_prediction::Projectile& projectile, + float pitch, float yaw, float time, + float gravity) noexcept; + + // Predict target position after `time` seconds + static constexpr Vector3 + predict_target_position(const projectile_prediction::Target& target, + float time, float gravity) noexcept; + + // Compute horizontal (2D) distance + static float + calc_vector_2d_distance(const Vector3& delta) noexcept; + + // Get vertical coordinate (Z in Source Engine) + static constexpr float + get_vector_height_coordinate(const Vector3& vec) noexcept; + + // Compute aim point from angles + static Vector3 + calc_viewpoint_from_angles(const projectile_prediction::Projectile& projectile, + Vector3 predicted_target_position, + std::optional projectile_pitch) noexcept; + + // Compute pitch angle to look at target + static float + calc_direct_pitch_angle(const Vector3& origin, + const Vector3& view_to) noexcept; + + // Compute yaw angle to look at target + static float + calc_direct_yaw_angle(const Vector3& origin, + const Vector3& view_to) noexcept; +}; + +} // namespace omath::source_engine +``` + +--- + +## Projectile prediction + +```cpp +auto pos = PredEngineTrait::predict_projectile_position( + projectile, // initial position, speed, gravity scale + pitch_deg, // launch pitch (positive = up) + yaw_deg, // launch yaw + time, // time in seconds + gravity // gravity constant (e.g., 800 units/s²) +); +``` + +Computes: + +1. Forward vector from pitch/yaw (using `forward_vector`) +2. Initial velocity: `forward * launch_speed` +3. Position after `time`: `origin + velocity*time - 0.5*gravity*gravityScale*time²` (Z component only) + +**Note**: Negative pitch in `forward_vector` convention → positive pitch looks up. + +--- + +## Target prediction + +```cpp +auto pos = PredEngineTrait::predict_target_position( + target, // position, velocity, airborne flag + time, // time in seconds + gravity // gravity constant +); +``` + +Simple linear extrapolation plus gravity if target is airborne: + +``` +predicted = origin + velocity * time +if (airborne) + predicted.z -= 0.5 * gravity * time² +``` + +--- + +## Distance & height helpers + +* `calc_vector_2d_distance(delta)` → `sqrt(delta.x² + delta.y²)` (horizontal distance) +* `get_vector_height_coordinate(vec)` → `vec.z` (vertical coordinate in Source Engine) + +Used to compute ballistic arc parameters. + +--- + +## Aim angle calculation + +* `calc_direct_pitch_angle(origin, target)` → pitch in degrees to look from `origin` to `target` + - Formula: `asin(Δz / distance)` converted to degrees + - Positive = looking up, negative = looking down + +* `calc_direct_yaw_angle(origin, target)` → yaw in degrees to look from `origin` to `target` + - Formula: `atan2(Δy, Δx)` converted to degrees + - Horizontal rotation around Z-axis + +--- + +## Viewpoint from angles + +```cpp +auto aim_point = PredEngineTrait::calc_viewpoint_from_angles( + projectile, + predicted_target_pos, + optional_pitch_deg +); +``` + +Computes where to aim in 3D space given a desired pitch angle. Uses horizontal distance and `tan(pitch)` to compute height offset. + +--- + +## Conventions + +* **Coordinate system**: Z-up (height increases with Z) +* **Angles**: pitch in [-89°, +89°], yaw in [-180°, +180°] +* **Gravity**: applied downward along -Z axis +* **Pitch convention**: +89° = straight up, -89° = straight down + +--- + +## Usage example + +```cpp +using namespace omath::source_engine; +using namespace omath::projectile_prediction; + +Projectile proj{ + .m_origin = {0, 0, 100}, + .m_launch_speed = 1000.0f, + .m_gravity_scale = 1.0f +}; + +Target tgt{ + .m_origin = {500, 200, 100}, + .m_velocity = {10, 5, 0}, + .m_is_airborne = false +}; + +float gravity = 800.0f; +float time = 0.5f; + +// Predict where target will be +auto target_pos = PredEngineTrait::predict_target_position(tgt, time, gravity); + +// Compute aim angles +float pitch = PredEngineTrait::calc_direct_pitch_angle(proj.m_origin, target_pos); +float yaw = PredEngineTrait::calc_direct_yaw_angle(proj.m_origin, target_pos); + +// Predict projectile position with those angles +auto proj_pos = PredEngineTrait::predict_projectile_position(proj, pitch, yaw, time, gravity); +``` + +--- + +## See also + +* `omath/engines/source_engine/formulas.hpp` — direction vectors and matrix builders +* `omath/projectile_prediction/projectile.hpp` — `Projectile` struct +* `omath/projectile_prediction/target.hpp` — `Target` struct +* Generic projectile prediction algorithms that use `PredEngineTraitConcept` diff --git a/docs/engines/unity_engine/camera_trait.md b/docs/engines/unity_engine/camera_trait.md new file mode 100644 index 00000000..1611eef1 --- /dev/null +++ b/docs/engines/unity_engine/camera_trait.md @@ -0,0 +1,109 @@ +# `omath::unity_engine::CameraTrait` — plug-in trait for `projection::Camera` + +> Header: `omath/engines/unity_engine/traits/camera_trait.hpp` • Impl: `omath/engines/unity_engine/traits/camera_trait.cpp` +> Namespace: `omath::unity_engine` +> Purpose: provide Unity Engine-style **look-at**, **view**, and **projection** math to the generic `omath::projection::Camera` (satisfies `CameraEngineConcept`). + +--- + +## Summary + +`CameraTrait` exposes three `static` functions: + +* `calc_look_at_angle(origin, look_at)` – computes Euler angles so the camera at `origin` looks at `look_at`. Implementation normalizes the direction, computes **pitch** as `asin(dir.y)` and **yaw** as `atan2(dir.x, dir.z)`; **roll** is `0`. Pitch/yaw are returned using the project's strong angle types (`PitchAngle`, `YawAngle`, `RollAngle`). +* `calc_view_matrix(angles, origin)` – delegates to Unity Engine formulas `unity_engine::calc_view_matrix`, producing a `Mat4X4` view matrix for the given angles and origin. +* `calc_projection_matrix(fov, viewport, near, far)` – builds a perspective projection by calling `calc_perspective_projection_matrix(fov_degrees, aspect, near, far)`, where `aspect = viewport.aspect_ratio()`. Accepts `FieldOfView` (degrees). + +The trait's types (`ViewAngles`, `Mat4X4`, angle aliases) and helpers live in the Unity Engine math headers included by the trait (`formulas.hpp`) and the shared projection header (`projection/camera.hpp`). + +--- + +## API + +```cpp +namespace omath::unity_engine { + +class CameraTrait final { +public: + // Compute Euler angles (pitch/yaw/roll) to look from cam_origin to look_at. + static ViewAngles + calc_look_at_angle(const Vector3& cam_origin, + const Vector3& look_at) noexcept; + + // Build view matrix for given angles and origin. + static Mat4X4 + calc_view_matrix(const ViewAngles& angles, + const Vector3& cam_origin) noexcept; + + // Build perspective projection from FOV (deg), viewport, near/far. + static Mat4X4 + calc_projection_matrix(const projection::FieldOfView& fov, + const projection::ViewPort& view_port, + float near, float far) noexcept; +}; + +} // namespace omath::unity_engine +``` + +Uses: `Vector3`, `ViewAngles` (pitch/yaw/roll), `Mat4X4`, `projection::FieldOfView`, `projection::ViewPort`. + +--- + +## Behavior & conventions + +* **Angles from look-at** (Y-up coordinate system): + + ``` + dir = normalize(look_at - origin) + pitch = asin(dir.y) // +Y is up + yaw = atan2(dir.x, dir.z) // horizontal rotation + roll = 0 + ``` + + Returned as `PitchAngle::from_radians(...)`, `YawAngle::from_radians(...)`, etc. + +* **View matrix**: built by the Unity Engine helper `unity_engine::calc_view_matrix(angles, origin)` to match the engine's handedness and axis conventions. + +* **Projection**: uses `calc_perspective_projection_matrix(fov.as_degrees(), viewport.aspect_ratio(), near, far)`. Pass your **vertical FOV** in degrees via `FieldOfView`; the helper computes a standard perspective matrix. + +--- + +## Using with `projection::Camera` + +Create a camera whose math is driven by this trait: + +```cpp +using Mat4 = Mat4X4; // from Unity Engine math headers +using Angs = ViewAngles; // pitch/yaw/roll type +using UEcam = omath::projection::Camera; + +omath::projection::ViewPort vp{1920.f, 1080.f}; +auto fov = omath::projection::FieldOfView::from_degrees(60.f); + +UEcam cam( + /*position*/ {10.f, 5.f, -10.f}, + /*angles*/ omath::unity_engine::CameraTrait::calc_look_at_angle({10,5,-10},{0,5,0}), + /*viewport*/ vp, + /*fov*/ fov, + /*near*/ 0.3f, + /*far*/ 1000.f +); +``` + +This satisfies `CameraEngineConcept` expected by `projection::Camera` (look-at, view, projection) as declared in the trait header. + +--- + +## Notes & tips + +* Ensure your `ViewAngles` aliases (`PitchAngle`, `YawAngle`, `RollAngle`) match the project's angle policy (ranges/normalization). The implementation constructs them **from radians**. +* `aspect_ratio()` is taken directly from `ViewPort` (`width / height`), so keep both positive and non-zero. +* `near` must be > 0 and `< far` for a valid projection matrix (enforced by your math helpers). +* Unity Engine uses **Y-up**: pitch angles control vertical look, positive = up. + +--- + +## See also + +* Unity Engine math helpers in `omath/engines/unity_engine/formulas.hpp` (view/projection builders used above). +* Generic camera wrapper `omath::projection::Camera` and its `CameraEngineConcept` (this trait is designed to plug straight into it). diff --git a/docs/engines/unity_engine/constants.md b/docs/engines/unity_engine/constants.md new file mode 100644 index 00000000..fc25d6a0 --- /dev/null +++ b/docs/engines/unity_engine/constants.md @@ -0,0 +1,77 @@ +# `omath::unity_engine` — types & constants + +> Header: `omath/engines/unity_engine/constants.hpp` +> Namespace: `omath::unity_engine` +> Purpose: define Unity Engine coordinate system, matrix types, and angle ranges + +--- + +## Summary + +The **Unity Engine** uses a **Y-up, left-handed** coordinate system: + +* **Up** = `{0, 1, 0}` (Y-axis) +* **Right** = `{1, 0, 0}` (X-axis) +* **Forward** = `{0, 0, 1}` (Z-axis) + +Matrices are **row-major**. Angles are **clamped pitch** (±90°) and **normalized yaw/roll** (±180°). + +--- + +## Constants + +```cpp +namespace omath::unity_engine { + constexpr Vector3 k_abs_up = {0, 1, 0}; + constexpr Vector3 k_abs_right = {1, 0, 0}; + constexpr Vector3 k_abs_forward = {0, 0, 1}; +} +``` + +These basis vectors define the engine's **world coordinate frame**. + +--- + +## Matrix types + +```cpp +using Mat4X4 = Mat<4, 4, float, MatStoreType::ROW_MAJOR>; +using Mat3X3 = Mat<4, 4, float, MatStoreType::ROW_MAJOR>; +using Mat1X3 = Mat<1, 3, float, MatStoreType::ROW_MAJOR>; +``` + +**Row-major** storage means rows are contiguous in memory. Suitable for CPU-side transforms and typical C++ math libraries. + +--- + +## Angle types + +```cpp +using PitchAngle = Angle; +using YawAngle = Angle; +using RollAngle = Angle; + +using ViewAngles = omath::ViewAngles; +``` + +* **PitchAngle**: clamped to **[-90°, +90°]** (looking down vs. up) +* **YawAngle**: normalized to **[-180°, +180°]** (horizontal rotation) +* **RollAngle**: normalized to **[-180°, +180°]** (camera roll) + +`ViewAngles` bundles all three into a single type for camera/view transforms. + +--- + +## Coordinate system notes + +* **Y-up**: gravity points along `-Y`, height increases with `+Y` +* **Left-handed**: cross product `forward × right = up` with left-hand rule +* This matches **Unity Engine** conventions for 3D games and simulations + +--- + +## See also + +* `omath/engines/unity_engine/formulas.hpp` — view/projection matrix builders +* `omath/trigonometry/angle.hpp` — angle normalization & clamping helpers +* `omath/trigonometry/view_angles.hpp` — generic pitch/yaw/roll wrapper diff --git a/docs/engines/unity_engine/formulas.md b/docs/engines/unity_engine/formulas.md new file mode 100644 index 00000000..7ed0b90a --- /dev/null +++ b/docs/engines/unity_engine/formulas.md @@ -0,0 +1,135 @@ +# `omath::unity_engine` — formulas & matrix helpers + +> Header: `omath/engines/unity_engine/formulas.hpp` +> Namespace: `omath::unity_engine` +> Purpose: compute direction vectors, rotation matrices, view matrices, and perspective projections for Unity Engine + +--- + +## Summary + +This header provides **Unity Engine**-specific math for: + +* **Direction vectors** (`forward`, `right`, `up`) from `ViewAngles` +* **Rotation matrices** from Euler angles +* **View matrices** (camera transforms) +* **Perspective projection** matrices + +All functions respect Unity Engine's **Y-up, left-handed** coordinate system. + +--- + +## API + +```cpp +namespace omath::unity_engine { + + // Compute forward direction from pitch/yaw/roll + [[nodiscard]] + Vector3 forward_vector(const ViewAngles& angles) noexcept; + + // Compute right direction from pitch/yaw/roll + [[nodiscard]] + Vector3 right_vector(const ViewAngles& angles) noexcept; + + // Compute up direction from pitch/yaw/roll + [[nodiscard]] + Vector3 up_vector(const ViewAngles& angles) noexcept; + + // Build 3x3 rotation matrix from angles + [[nodiscard]] + Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept; + + // Build view matrix (camera space transform) + [[nodiscard]] + Mat4X4 calc_view_matrix(const ViewAngles& angles, + const Vector3& cam_origin) noexcept; + + // Build perspective projection matrix + [[nodiscard]] + Mat4X4 calc_perspective_projection_matrix(float field_of_view, + float aspect_ratio, + float near, float far) noexcept; + +} // namespace omath::unity_engine +``` + +--- + +## Direction vectors + +Given camera angles (pitch/yaw/roll): + +* `forward_vector(angles)` → unit vector pointing where the camera looks +* `right_vector(angles)` → unit vector pointing to the camera's right +* `up_vector(angles)` → unit vector pointing upward relative to the camera + +These are used for movement, aim direction, and building coordinate frames. + +--- + +## Rotation & view matrices + +* `rotation_matrix(angles)` → 3×3 (or 4×4) rotation matrix from Euler angles +* `calc_view_matrix(angles, origin)` → camera view matrix + +The view matrix transforms world coordinates into camera space (origin at camera, axes aligned with camera orientation). + +--- + +## Perspective projection + +```cpp +Mat4X4 proj = calc_perspective_projection_matrix( + fov_degrees, // vertical field of view (e.g., 60) + aspect_ratio, // width / height (e.g., 16/9) + near_plane, // e.g., 0.3 + far_plane // e.g., 1000.0 +); +``` + +Produces a **perspective projection matrix** suitable for 3D rendering pipelines. Combined with the view matrix, this implements the standard camera transform chain. + +--- + +## Usage example + +```cpp +using namespace omath::unity_engine; + +// Camera setup +ViewAngles angles = { + PitchAngle::from_degrees(-15.0f), + YawAngle::from_degrees(45.0f), + RollAngle::from_degrees(0.0f) +}; +Vector3 cam_pos{10.0f, 5.0f, -10.0f}; + +// Compute direction +auto forward = forward_vector(angles); +auto right = right_vector(angles); +auto up = up_vector(angles); + +// Build matrices +auto view_mat = calc_view_matrix(angles, cam_pos); +auto proj_mat = calc_perspective_projection_matrix(60.0f, 16.0f/9.0f, 0.3f, 1000.0f); + +// Use view_mat and proj_mat for rendering... +``` + +--- + +## Conventions + +* **Angles**: pitch (up/down), yaw (left/right), roll (tilt) +* **Pitch**: positive = looking up, negative = looking down +* **Yaw**: increases counter-clockwise from the +Z axis +* **Coordinate system**: Y-up, Z-forward, X-right (left-handed) + +--- + +## See also + +* `omath/engines/unity_engine/constants.hpp` — coordinate frame & angle types +* `omath/engines/unity_engine/traits/camera_trait.hpp` — plug-in for generic `Camera` +* `omath/projection/camera.hpp` — generic camera wrapper using these formulas diff --git a/docs/engines/unity_engine/mesh_trait.md b/docs/engines/unity_engine/mesh_trait.md new file mode 100644 index 00000000..3886ac71 --- /dev/null +++ b/docs/engines/unity_engine/mesh_trait.md @@ -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; +} +``` + +--- + +## 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* diff --git a/docs/engines/unity_engine/pred_engine_trait.md b/docs/engines/unity_engine/pred_engine_trait.md new file mode 100644 index 00000000..13992def --- /dev/null +++ b/docs/engines/unity_engine/pred_engine_trait.md @@ -0,0 +1,198 @@ +# `omath::unity_engine::PredEngineTrait` — projectile prediction trait + +> Header: `omath/engines/unity_engine/traits/pred_engine_trait.hpp` +> Namespace: `omath::unity_engine` +> Purpose: provide Unity Engine-specific projectile and target prediction for ballistic calculations + +--- + +## Summary + +`PredEngineTrait` implements engine-specific helpers for **projectile prediction**: + +* `predict_projectile_position` – computes where a projectile will be after `time` seconds +* `predict_target_position` – computes where a moving target will be after `time` seconds +* `calc_vector_2d_distance` – horizontal distance (X/Z plane, ignoring Y) +* `get_vector_height_coordinate` – extracts vertical coordinate (Y in Unity Engine) +* `calc_viewpoint_from_angles` – computes aim point given pitch angle +* `calc_direct_pitch_angle` – pitch angle to look from origin to target +* `calc_direct_yaw_angle` – yaw angle to look from origin to target + +These methods satisfy the `PredEngineTraitConcept` required by generic projectile prediction algorithms. + +--- + +## API + +```cpp +namespace omath::unity_engine { + +class PredEngineTrait final { +public: + // Predict projectile position after `time` seconds + static constexpr Vector3 + predict_projectile_position(const projectile_prediction::Projectile& projectile, + float pitch, float yaw, float time, + float gravity) noexcept; + + // Predict target position after `time` seconds + static constexpr Vector3 + predict_target_position(const projectile_prediction::Target& target, + float time, float gravity) noexcept; + + // Compute horizontal (2D) distance + static float + calc_vector_2d_distance(const Vector3& delta) noexcept; + + // Get vertical coordinate (Y in Unity Engine) + static constexpr float + get_vector_height_coordinate(const Vector3& vec) noexcept; + + // Compute aim point from angles + static Vector3 + calc_viewpoint_from_angles(const projectile_prediction::Projectile& projectile, + Vector3 predicted_target_position, + std::optional projectile_pitch) noexcept; + + // Compute pitch angle to look at target + static float + calc_direct_pitch_angle(const Vector3& origin, + const Vector3& view_to) noexcept; + + // Compute yaw angle to look at target + static float + calc_direct_yaw_angle(const Vector3& origin, + const Vector3& view_to) noexcept; +}; + +} // namespace omath::unity_engine +``` + +--- + +## Projectile prediction + +```cpp +auto pos = PredEngineTrait::predict_projectile_position( + projectile, // initial position, speed, gravity scale + pitch_deg, // launch pitch (positive = up) + yaw_deg, // launch yaw + time, // time in seconds + gravity // gravity constant (e.g., 9.81 m/s²) +); +``` + +Computes: + +1. Forward vector from pitch/yaw (using `forward_vector`) +2. Initial velocity: `forward * launch_speed` +3. Position after `time`: `origin + velocity*time - 0.5*gravity*gravityScale*time²` (Y component only) + +**Note**: Negative pitch in `forward_vector` convention → positive pitch looks up. + +--- + +## Target prediction + +```cpp +auto pos = PredEngineTrait::predict_target_position( + target, // position, velocity, airborne flag + time, // time in seconds + gravity // gravity constant +); +``` + +Simple linear extrapolation plus gravity if target is airborne: + +``` +predicted = origin + velocity * time +if (airborne) + predicted.y -= 0.5 * gravity * time² +``` + +--- + +## Distance & height helpers + +* `calc_vector_2d_distance(delta)` → `sqrt(delta.x² + delta.z²)` (horizontal distance) +* `get_vector_height_coordinate(vec)` → `vec.y` (vertical coordinate in Unity Engine) + +Used to compute ballistic arc parameters. + +--- + +## Aim angle calculation + +* `calc_direct_pitch_angle(origin, target)` → pitch in degrees to look from `origin` to `target` + - Formula: `asin(Δy / distance)` converted to degrees (direction normalized first) + - Positive = looking up, negative = looking down + +* `calc_direct_yaw_angle(origin, target)` → yaw in degrees to look from `origin` to `target` + - Formula: `atan2(Δx, Δz)` converted to degrees (direction normalized first) + - Horizontal rotation around Y-axis + +--- + +## Viewpoint from angles + +```cpp +auto aim_point = PredEngineTrait::calc_viewpoint_from_angles( + projectile, + predicted_target_pos, + optional_pitch_deg +); +``` + +Computes where to aim in 3D space given a desired pitch angle. Uses horizontal distance and `tan(pitch)` to compute height offset. Result has adjusted Y coordinate. + +--- + +## Conventions + +* **Coordinate system**: Y-up (height increases with Y) +* **Angles**: pitch in [-90°, +90°], yaw in [-180°, +180°] +* **Gravity**: applied downward along -Y axis +* **Pitch convention**: +90° = straight up, -90° = straight down + +--- + +## Usage example + +```cpp +using namespace omath::unity_engine; +using namespace omath::projectile_prediction; + +Projectile proj{ + .m_origin = {0, 2, 0}, + .m_launch_speed = 50.0f, + .m_gravity_scale = 1.0f +}; + +Target tgt{ + .m_origin = {20, 2, 15}, + .m_velocity = {1, 0, 0.5f}, + .m_is_airborne = false +}; + +float gravity = 9.81f; +float time = 0.5f; + +// Predict where target will be +auto target_pos = PredEngineTrait::predict_target_position(tgt, time, gravity); + +// Compute aim angles +float pitch = PredEngineTrait::calc_direct_pitch_angle(proj.m_origin, target_pos); +float yaw = PredEngineTrait::calc_direct_yaw_angle(proj.m_origin, target_pos); + +// Predict projectile position with those angles +auto proj_pos = PredEngineTrait::predict_projectile_position(proj, pitch, yaw, time, gravity); +``` + +--- + +## See also + +* `omath/engines/unity_engine/formulas.hpp` — direction vectors and matrix builders +* `omath/projectile_prediction/projectile.hpp` — `Projectile` struct +* `omath/projectile_prediction/target.hpp` — `Target` struct +* Generic projectile prediction algorithms that use `PredEngineTraitConcept` diff --git a/docs/engines/unreal_engine/camera_trait.md b/docs/engines/unreal_engine/camera_trait.md new file mode 100644 index 00000000..7a02fc69 --- /dev/null +++ b/docs/engines/unreal_engine/camera_trait.md @@ -0,0 +1,109 @@ +# `omath::unreal_engine::CameraTrait` — plug-in trait for `projection::Camera` + +> Header: `omath/engines/unreal_engine/traits/camera_trait.hpp` • Impl: `omath/engines/unreal_engine/traits/camera_trait.cpp` +> Namespace: `omath::unreal_engine` +> Purpose: provide Unreal Engine-style **look-at**, **view**, and **projection** math to the generic `omath::projection::Camera` (satisfies `CameraEngineConcept`). + +--- + +## Summary + +`CameraTrait` exposes three `static` functions: + +* `calc_look_at_angle(origin, look_at)` – computes Euler angles so the camera at `origin` looks at `look_at`. Implementation normalizes the direction, computes **pitch** as `asin(dir.z)` and **yaw** as `atan2(dir.y, dir.x)`; **roll** is `0`. Pitch/yaw are returned using the project's strong angle types (`PitchAngle`, `YawAngle`, `RollAngle`). +* `calc_view_matrix(angles, origin)` – delegates to Unreal Engine formulas `unreal_engine::calc_view_matrix`, producing a `Mat4X4` view matrix for the given angles and origin. +* `calc_projection_matrix(fov, viewport, near, far)` – builds a perspective projection by calling `calc_perspective_projection_matrix(fov_degrees, aspect, near, far)`, where `aspect = viewport.aspect_ratio()`. Accepts `FieldOfView` (degrees). + +The trait's types (`ViewAngles`, `Mat4X4`, angle aliases) and helpers live in the Unreal Engine math headers included by the trait (`formulas.hpp`) and the shared projection header (`projection/camera.hpp`). + +--- + +## API + +```cpp +namespace omath::unreal_engine { + +class CameraTrait final { +public: + // Compute Euler angles (pitch/yaw/roll) to look from cam_origin to look_at. + static ViewAngles + calc_look_at_angle(const Vector3& cam_origin, + const Vector3& look_at) noexcept; + + // Build view matrix for given angles and origin. + static Mat4X4 + calc_view_matrix(const ViewAngles& angles, + const Vector3& cam_origin) noexcept; + + // Build perspective projection from FOV (deg), viewport, near/far. + static Mat4X4 + calc_projection_matrix(const projection::FieldOfView& fov, + const projection::ViewPort& view_port, + float near, float far) noexcept; +}; + +} // namespace omath::unreal_engine +``` + +Uses: `Vector3`, `ViewAngles` (pitch/yaw/roll), `Mat4X4`, `projection::FieldOfView`, `projection::ViewPort`. + +--- + +## Behavior & conventions + +* **Angles from look-at** (Z-up coordinate system): + + ``` + dir = normalize(look_at - origin) + pitch = asin(dir.z) // +Z is up + yaw = atan2(dir.y, dir.x) // horizontal rotation + roll = 0 + ``` + + Returned as `PitchAngle::from_radians(...)`, `YawAngle::from_radians(...)`, etc. + +* **View matrix**: built by the Unreal Engine helper `unreal_engine::calc_view_matrix(angles, origin)` to match the engine's handedness and axis conventions. + +* **Projection**: uses `calc_perspective_projection_matrix(fov.as_degrees(), viewport.aspect_ratio(), near, far)`. Pass your **vertical FOV** in degrees via `FieldOfView`; the helper computes a standard perspective matrix. + +--- + +## Using with `projection::Camera` + +Create a camera whose math is driven by this trait: + +```cpp +using Mat4 = Mat4X4; // from Unreal Engine math headers +using Angs = ViewAngles; // pitch/yaw/roll type +using UEcam = omath::projection::Camera; + +omath::projection::ViewPort vp{1920.f, 1080.f}; +auto fov = omath::projection::FieldOfView::from_degrees(90.f); + +UEcam cam( + /*position*/ {1000.f, 500.f, 200.f}, + /*angles*/ omath::unreal_engine::CameraTrait::calc_look_at_angle({1000,500,200},{0,0,200}), + /*viewport*/ vp, + /*fov*/ fov, + /*near*/ 10.f, + /*far*/ 100000.f +); +``` + +This satisfies `CameraEngineConcept` expected by `projection::Camera` (look-at, view, projection) as declared in the trait header. + +--- + +## Notes & tips + +* Ensure your `ViewAngles` aliases (`PitchAngle`, `YawAngle`, `RollAngle`) match the project's angle policy (ranges/normalization). The implementation constructs them **from radians**. +* `aspect_ratio()` is taken directly from `ViewPort` (`width / height`), so keep both positive and non-zero. +* `near` must be > 0 and `< far` for a valid projection matrix (enforced by your math helpers). +* Unreal Engine uses **Z-up**: pitch angles control vertical look, positive = up. + +--- + +## See also + +* Unreal Engine math helpers in `omath/engines/unreal_engine/formulas.hpp` (view/projection builders used above). +* Generic camera wrapper `omath::projection::Camera` and its `CameraEngineConcept` (this trait is designed to plug straight into it). diff --git a/docs/engines/unreal_engine/constants.md b/docs/engines/unreal_engine/constants.md new file mode 100644 index 00000000..5b66bd7e --- /dev/null +++ b/docs/engines/unreal_engine/constants.md @@ -0,0 +1,77 @@ +# `omath::unreal_engine` — types & constants + +> Header: `omath/engines/unreal_engine/constants.hpp` +> Namespace: `omath::unreal_engine` +> Purpose: define Unreal Engine coordinate system, matrix types, and angle ranges + +--- + +## Summary + +The **Unreal Engine** uses a **Z-up, left-handed** coordinate system: + +* **Up** = `{0, 0, 1}` (Z-axis) +* **Right** = `{0, 1, 0}` (Y-axis) +* **Forward** = `{1, 0, 0}` (X-axis) + +Matrices are **row-major**. Angles are **clamped pitch** (±90°) and **normalized yaw/roll** (±180°). + +--- + +## Constants + +```cpp +namespace omath::unreal_engine { + constexpr Vector3 k_abs_up = {0, 0, 1}; + constexpr Vector3 k_abs_right = {0, 1, 0}; + constexpr Vector3 k_abs_forward = {1, 0, 0}; +} +``` + +These basis vectors define the engine's **world coordinate frame**. + +--- + +## Matrix types + +```cpp +using Mat4X4 = Mat<4, 4, float, MatStoreType::ROW_MAJOR>; +using Mat3X3 = Mat<4, 4, float, MatStoreType::ROW_MAJOR>; +using Mat1X3 = Mat<1, 3, float, MatStoreType::ROW_MAJOR>; +``` + +**Row-major** storage means rows are contiguous in memory. Suitable for CPU-side transforms and typical C++ math libraries. + +--- + +## Angle types + +```cpp +using PitchAngle = Angle; +using YawAngle = Angle; +using RollAngle = Angle; + +using ViewAngles = omath::ViewAngles; +``` + +* **PitchAngle**: clamped to **[-90°, +90°]** (looking down vs. up) +* **YawAngle**: normalized to **[-180°, +180°]** (horizontal rotation) +* **RollAngle**: normalized to **[-180°, +180°]** (camera roll) + +`ViewAngles` bundles all three into a single type for camera/view transforms. + +--- + +## Coordinate system notes + +* **Z-up**: gravity points along `-Z`, height increases with `+Z` +* **Left-handed**: cross product `forward × right = up` with left-hand rule +* This matches **Unreal Engine** conventions for 3D games and simulations + +--- + +## See also + +* `omath/engines/unreal_engine/formulas.hpp` — view/projection matrix builders +* `omath/trigonometry/angle.hpp` — angle normalization & clamping helpers +* `omath/trigonometry/view_angles.hpp` — generic pitch/yaw/roll wrapper diff --git a/docs/engines/unreal_engine/formulas.md b/docs/engines/unreal_engine/formulas.md new file mode 100644 index 00000000..3f4ef3cb --- /dev/null +++ b/docs/engines/unreal_engine/formulas.md @@ -0,0 +1,135 @@ +# `omath::unreal_engine` — formulas & matrix helpers + +> Header: `omath/engines/unreal_engine/formulas.hpp` +> Namespace: `omath::unreal_engine` +> Purpose: compute direction vectors, rotation matrices, view matrices, and perspective projections for Unreal Engine + +--- + +## Summary + +This header provides **Unreal Engine**-specific math for: + +* **Direction vectors** (`forward`, `right`, `up`) from `ViewAngles` +* **Rotation matrices** from Euler angles +* **View matrices** (camera transforms) +* **Perspective projection** matrices + +All functions respect Unreal Engine's **Z-up, left-handed** coordinate system. + +--- + +## API + +```cpp +namespace omath::unreal_engine { + + // Compute forward direction from pitch/yaw/roll + [[nodiscard]] + Vector3 forward_vector(const ViewAngles& angles) noexcept; + + // Compute right direction from pitch/yaw/roll + [[nodiscard]] + Vector3 right_vector(const ViewAngles& angles) noexcept; + + // Compute up direction from pitch/yaw/roll + [[nodiscard]] + Vector3 up_vector(const ViewAngles& angles) noexcept; + + // Build 3x3 rotation matrix from angles + [[nodiscard]] + Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept; + + // Build view matrix (camera space transform) + [[nodiscard]] + Mat4X4 calc_view_matrix(const ViewAngles& angles, + const Vector3& cam_origin) noexcept; + + // Build perspective projection matrix + [[nodiscard]] + Mat4X4 calc_perspective_projection_matrix(float field_of_view, + float aspect_ratio, + float near, float far) noexcept; + +} // namespace omath::unreal_engine +``` + +--- + +## Direction vectors + +Given camera angles (pitch/yaw/roll): + +* `forward_vector(angles)` → unit vector pointing where the camera looks +* `right_vector(angles)` → unit vector pointing to the camera's right +* `up_vector(angles)` → unit vector pointing upward relative to the camera + +These are used for movement, aim direction, and building coordinate frames. + +--- + +## Rotation & view matrices + +* `rotation_matrix(angles)` → 3×3 (or 4×4) rotation matrix from Euler angles +* `calc_view_matrix(angles, origin)` → camera view matrix + +The view matrix transforms world coordinates into camera space (origin at camera, axes aligned with camera orientation). + +--- + +## Perspective projection + +```cpp +Mat4X4 proj = calc_perspective_projection_matrix( + fov_degrees, // vertical field of view (e.g., 90) + aspect_ratio, // width / height (e.g., 16/9) + near_plane, // e.g., 10.0 + far_plane // e.g., 100000.0 +); +``` + +Produces a **perspective projection matrix** suitable for 3D rendering pipelines. Combined with the view matrix, this implements the standard camera transform chain. + +--- + +## Usage example + +```cpp +using namespace omath::unreal_engine; + +// Camera setup +ViewAngles angles = { + PitchAngle::from_degrees(-20.0f), + YawAngle::from_degrees(45.0f), + RollAngle::from_degrees(0.0f) +}; +Vector3 cam_pos{1000.0f, 500.0f, 200.0f}; + +// Compute direction +auto forward = forward_vector(angles); +auto right = right_vector(angles); +auto up = up_vector(angles); + +// Build matrices +auto view_mat = calc_view_matrix(angles, cam_pos); +auto proj_mat = calc_perspective_projection_matrix(90.0f, 16.0f/9.0f, 10.0f, 100000.0f); + +// Use view_mat and proj_mat for rendering... +``` + +--- + +## Conventions + +* **Angles**: pitch (up/down), yaw (left/right), roll (tilt) +* **Pitch**: positive = looking up, negative = looking down +* **Yaw**: increases counter-clockwise from the +X axis +* **Coordinate system**: Z-up, X-forward, Y-right (left-handed) + +--- + +## See also + +* `omath/engines/unreal_engine/constants.hpp` — coordinate frame & angle types +* `omath/engines/unreal_engine/traits/camera_trait.hpp` — plug-in for generic `Camera` +* `omath/projection/camera.hpp` — generic camera wrapper using these formulas diff --git a/docs/engines/unreal_engine/mesh_trait.md b/docs/engines/unreal_engine/mesh_trait.md new file mode 100644 index 00000000..7bbbbecc --- /dev/null +++ b/docs/engines/unreal_engine/mesh_trait.md @@ -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; +} +``` + +--- + +## 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* diff --git a/docs/engines/unreal_engine/pred_engine_trait.md b/docs/engines/unreal_engine/pred_engine_trait.md new file mode 100644 index 00000000..dd337db2 --- /dev/null +++ b/docs/engines/unreal_engine/pred_engine_trait.md @@ -0,0 +1,200 @@ +# `omath::unreal_engine::PredEngineTrait` — projectile prediction trait + +> Header: `omath/engines/unreal_engine/traits/pred_engine_trait.hpp` +> Namespace: `omath::unreal_engine` +> Purpose: provide Unreal Engine-specific projectile and target prediction for ballistic calculations + +--- + +## Summary + +`PredEngineTrait` implements engine-specific helpers for **projectile prediction**: + +* `predict_projectile_position` – computes where a projectile will be after `time` seconds +* `predict_target_position` – computes where a moving target will be after `time` seconds +* `calc_vector_2d_distance` – horizontal distance (X/Y plane, ignoring Z) +* `get_vector_height_coordinate` – extracts vertical coordinate (Y in Unreal Engine, note: code uses Z) +* `calc_viewpoint_from_angles` – computes aim point given pitch angle +* `calc_direct_pitch_angle` – pitch angle to look from origin to target +* `calc_direct_yaw_angle` – yaw angle to look from origin to target + +These methods satisfy the `PredEngineTraitConcept` required by generic projectile prediction algorithms. + +--- + +## API + +```cpp +namespace omath::unreal_engine { + +class PredEngineTrait final { +public: + // Predict projectile position after `time` seconds + static constexpr Vector3 + predict_projectile_position(const projectile_prediction::Projectile& projectile, + float pitch, float yaw, float time, + float gravity) noexcept; + + // Predict target position after `time` seconds + static constexpr Vector3 + predict_target_position(const projectile_prediction::Target& target, + float time, float gravity) noexcept; + + // Compute horizontal (2D) distance + static float + calc_vector_2d_distance(const Vector3& delta) noexcept; + + // Get vertical coordinate (implementation returns Y, but UE is Z-up) + static constexpr float + get_vector_height_coordinate(const Vector3& vec) noexcept; + + // Compute aim point from angles + static Vector3 + calc_viewpoint_from_angles(const projectile_prediction::Projectile& projectile, + Vector3 predicted_target_position, + std::optional projectile_pitch) noexcept; + + // Compute pitch angle to look at target + static float + calc_direct_pitch_angle(const Vector3& origin, + const Vector3& view_to) noexcept; + + // Compute yaw angle to look at target + static float + calc_direct_yaw_angle(const Vector3& origin, + const Vector3& view_to) noexcept; +}; + +} // namespace omath::unreal_engine +``` + +--- + +## Projectile prediction + +```cpp +auto pos = PredEngineTrait::predict_projectile_position( + projectile, // initial position, speed, gravity scale + pitch_deg, // launch pitch (positive = up) + yaw_deg, // launch yaw + time, // time in seconds + gravity // gravity constant (e.g., 980 cm/s²) +); +``` + +Computes: + +1. Forward vector from pitch/yaw (using `forward_vector`) +2. Initial velocity: `forward * launch_speed` +3. Position after `time`: `origin + velocity*time - 0.5*gravity*gravityScale*time²` (Y component per implementation, though UE is Z-up) + +**Note**: Negative pitch in `forward_vector` convention → positive pitch looks up. + +--- + +## Target prediction + +```cpp +auto pos = PredEngineTrait::predict_target_position( + target, // position, velocity, airborne flag + time, // time in seconds + gravity // gravity constant +); +``` + +Simple linear extrapolation plus gravity if target is airborne: + +``` +predicted = origin + velocity * time +if (airborne) + predicted.y -= 0.5 * gravity * time² // Note: implementation uses Y +``` + +--- + +## Distance & height helpers + +* `calc_vector_2d_distance(delta)` → `sqrt(delta.x² + delta.z²)` (horizontal distance in X/Z plane) +* `get_vector_height_coordinate(vec)` → `vec.y` (implementation returns Y; UE convention is Z-up) + +Used to compute ballistic arc parameters. + +--- + +## Aim angle calculation + +* `calc_direct_pitch_angle(origin, target)` → pitch in degrees to look from `origin` to `target` + - Formula: `asin(Δz / distance)` converted to degrees (direction normalized first) + - Positive = looking up, negative = looking down + +* `calc_direct_yaw_angle(origin, target)` → yaw in degrees to look from `origin` to `target` + - Formula: `atan2(Δy, Δx)` converted to degrees (direction normalized first) + - Horizontal rotation around Z-axis + +--- + +## Viewpoint from angles + +```cpp +auto aim_point = PredEngineTrait::calc_viewpoint_from_angles( + projectile, + predicted_target_pos, + optional_pitch_deg +); +``` + +Computes where to aim in 3D space given a desired pitch angle. Uses horizontal distance and `tan(pitch)` to compute height offset. + +--- + +## Conventions + +* **Coordinate system**: Z-up (height increases with Z) +* **Angles**: pitch in [-90°, +90°], yaw in [-180°, +180°] +* **Gravity**: applied downward (implementation uses Y component, but UE is Z-up) +* **Pitch convention**: +90° = straight up, -90° = straight down + +**Note**: Some implementation details (gravity application to Y coordinate) may need adjustment for full Unreal Engine Z-up consistency. + +--- + +## Usage example + +```cpp +using namespace omath::unreal_engine; +using namespace omath::projectile_prediction; + +Projectile proj{ + .m_origin = {0, 0, 200}, + .m_launch_speed = 5000.0f, + .m_gravity_scale = 1.0f +}; + +Target tgt{ + .m_origin = {2000, 1000, 200}, + .m_velocity = {50, 20, 0}, + .m_is_airborne = false +}; + +float gravity = 980.0f; // cm/s² in Unreal units +float time = 0.5f; + +// Predict where target will be +auto target_pos = PredEngineTrait::predict_target_position(tgt, time, gravity); + +// Compute aim angles +float pitch = PredEngineTrait::calc_direct_pitch_angle(proj.m_origin, target_pos); +float yaw = PredEngineTrait::calc_direct_yaw_angle(proj.m_origin, target_pos); + +// Predict projectile position with those angles +auto proj_pos = PredEngineTrait::predict_projectile_position(proj, pitch, yaw, time, gravity); +``` + +--- + +## See also + +* `omath/engines/unreal_engine/formulas.hpp` — direction vectors and matrix builders +* `omath/projectile_prediction/projectile.hpp` — `Projectile` struct +* `omath/projectile_prediction/target.hpp` — `Target` struct +* Generic projectile prediction algorithms that use `PredEngineTraitConcept` diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 00000000..60f4ef93 --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,406 @@ +# FAQ + +Common questions and answers about OMath. + +--- + +## General Questions + +### What is OMath? + +OMath is a modern C++ math library designed for game development, graphics programming, and high-performance computing. It provides: +- Vector and matrix operations +- 3D projection and camera systems +- Projectile prediction +- Collision detection +- Support for multiple game engines (Source, Unity, Unreal, etc.) +- Pattern scanning utilities + +### Why choose OMath over other math libraries? + +- **Modern C++**: Uses C++20/23 features (concepts, `constexpr`, `std::expected`) +- **No legacy code**: Built from scratch without legacy baggage +- **Game engine support**: Pre-configured for Source, Unity, Unreal, Frostbite, etc. +- **Zero dependencies**: No external dependencies needed (except for testing) +- **Performance**: AVX2 optimizations available +- **Type safety**: Strong typing prevents common errors +- **Cross-platform**: Works on Windows, Linux, and macOS + +### Is OMath suitable for production use? + +Yes! OMath is production-ready and used in various projects. It has: +- Comprehensive test coverage +- Clear error handling +- Well-documented API +- Active maintenance and community support + +--- + +## Installation & Setup + +### How do I install OMath? + +Three main methods: + +**vcpkg (recommended):** +```bash +vcpkg install orange-math +``` + +**xrepo:** +```bash +xrepo install omath +``` + +**From source:** +See [Installation Guide](install.md) + +### What are the minimum requirements? + +- **Compiler**: C++20 support required + - GCC 10+ + - Clang 11+ + - MSVC 2019 16.10+ +- **CMake**: 3.15+ (if building from source) +- **Platform**: Windows, Linux, or macOS + +### Do I need C++23? + +C++23 is **recommended** but not required. Some features like `std::expected` work better with C++23, but fallbacks are available for C++20. + +### Can I use OMath in a C++17 project? + +No, OMath requires C++20 minimum due to use of concepts, `constexpr` enhancements, and other C++20 features. + +--- + +## Usage Questions + +### How do I include OMath in my project? + +**Full library:** +```cpp +#include +``` + +**Specific components:** +```cpp +#include +#include +``` + +### Which game engine should I use? + +Choose based on your target game or application: + +| Engine | Use For | +|--------|---------| +| **Source Engine** | CS:GO, TF2, CS2, Half-Life, Portal, L4D | +| **Unity Engine** | Unity games (many indie and mobile games) | +| **Unreal Engine** | Fortnite, Unreal games | +| **Frostbite** | Battlefield, Star Wars games (EA titles) | +| **IW Engine** | Call of Duty series | +| **OpenGL** | Custom OpenGL applications, generic 3D | + +### How do I switch between engines? + +Just change the namespace: + +```cpp +// Source Engine +using namespace omath::source_engine; +Camera cam = /* ... */; + +// Unity Engine +using namespace omath::unity_engine; +Camera cam = /* ... */; +``` + +Each engine has the same API but different coordinate system handling. + +### What if my game isn't listed? + +Use the **OpenGL engine** as a starting point - it uses canonical OpenGL conventions. You may need to adjust coordinate transformations based on your specific game. + +--- + +## Performance Questions + +### Should I use the AVX2 or Legacy engine? + +**Use AVX2 if:** +- Target modern CPUs (2013+) +- Need maximum performance +- Can accept reduced compatibility + +**Use Legacy if:** +- Need broad compatibility +- Target older CPUs or ARM +- Unsure about target hardware + +The API is identical - just change the class: +```cpp +// Legacy (compatible) +ProjPredEngineLegacy engine; + +// AVX2 (faster) +ProjPredEngineAVX2 engine; +``` + +### How much faster is AVX2? + +Typically 2-4x faster for projectile prediction calculations, depending on the CPU and specific use case. + +### Are vector operations constexpr? + +Yes! Most operations are `constexpr` and can be evaluated at compile-time: + +```cpp +constexpr Vector3 v{1, 2, 3}; +constexpr auto len_sq = v.length_sqr(); // Computed at compile time +``` + +### Is OMath thread-safe? + +- **Immutable operations** (vector math, etc.) are thread-safe +- **Mutable state** (Camera updates) is NOT thread-safe +- Use separate instances per thread or synchronize access + +--- + +## Troubleshooting + +### `world_to_screen()` always returns `nullopt` + +Check: +1. **Is the point behind the camera?** Points behind the camera cannot be projected. +2. **Are near/far planes correct?** Ensure `near < far` and both are positive. +3. **Is FOV valid?** FOV should be between 1° and 179°. +4. **Are camera angles normalized?** Use engine-provided angle types. + +### Angles are wrapping incorrectly + +Use the correct angle type: +```cpp +// Good: uses proper angle type +PitchAngle pitch = PitchAngle::from_degrees(45.0f); + +// Bad: raw float loses normalization +float pitch = 45.0f; +``` + +### Projection seems mirrored or inverted + +You may be using the wrong engine trait. Each engine has different coordinate conventions: +- **Source/Unity**: Z-up +- **Unreal**: Z-up, different handedness +- **OpenGL**: Y-up + +Ensure you're using the trait matching your game. + +### Pattern scanning finds multiple matches + +This is normal! Patterns may appear multiple times. Solutions: +1. Make the pattern more specific (more bytes, fewer wildcards) +2. Use additional context (nearby code patterns) +3. Verify each match programmatically + +### Projectile prediction returns `nullopt` + +Common reasons: +1. **Target too fast**: Target velocity exceeds projectile speed +2. **Out of range**: Distance exceeds max flight time +3. **Invalid input**: Check projectile speed > 0 +4. **Gravity too strong**: Projectile can't reach target height + +### Compilation errors about `std::expected` + +If using C++20 (not C++23), you may need a backport library like `tl::expected`: + +```cmake +# CMakeLists.txt +find_package(tl-expected CONFIG REQUIRED) +target_link_libraries(your_target PRIVATE tl::expected) +``` + +Or upgrade to C++23 if possible. + +--- + +## Feature Questions + +### Can I use OMath with DirectX/OpenGL/Vulkan? + +Yes! OMath matrices and vectors work with all graphics APIs. Use: +- **OpenGL**: `opengl_engine` traits +- **DirectX**: Use appropriate engine trait or OpenGL as base +- **Vulkan**: Use OpenGL traits as starting point + +### Does OMath support quaternions? + +Not currently. Quaternion support may be added in future versions. For now, use euler angles (ViewAngles) or convert manually. + +### Can I extend OMath with custom engine traits? + +Yes! Implement the `CameraEngineConcept`: + +```cpp +class MyEngineTrait { +public: + static ViewAngles calc_look_at_angle( + const Vector3& origin, + const Vector3& target + ); + + static Mat4X4 calc_view_matrix( + const ViewAngles& angles, + const Vector3& origin + ); + + static Mat4X4 calc_projection_matrix( + const FieldOfView& fov, + const ViewPort& viewport, + float near, float far + ); +}; + +// Use with Camera +using MyCamera = Camera; +``` + +### Does OMath support SIMD for vector operations? + +AVX2 support is available for projectile prediction. General vector SIMD may be added in future versions. The library already compiles to efficient code with compiler optimizations enabled. + +### Can I use OMath for machine learning? + +OMath is optimized for game development and graphics, not ML. For machine learning, consider libraries like Eigen or xtensor which are designed for that domain. + +--- + +## Debugging Questions + +### How do I print vectors? + +OMath provides `std::formatter` support: + +```cpp +#include +#include + +Vector3 v{1, 2, 3}; +std::cout << std::format("{}", v) << "\n"; // Prints: [1, 2, 3] +``` + +### How do I visualize projection problems? + +1. Check if `world_to_screen()` succeeds +2. Print camera matrices: + ```cpp + auto view = camera.get_view_matrix(); + auto proj = camera.get_projection_matrix(); + // Print matrix values + ``` +3. Test with known good points (e.g., origin, simple positions) +4. Verify viewport and FOV values + +### How can I debug pattern scanning? + +```cpp +PatternView pattern{"48 8B 05 ?? ?? ?? ??"}; + +// Print pattern details +std::cout << "Pattern length: " << pattern.size() << "\n"; +std::cout << "Pattern bytes: "; +for (auto byte : pattern) { + if (byte.has_value()) { + std::cout << std::hex << (int)*byte << " "; + } else { + std::cout << "?? "; + } +} +std::cout << "\n"; +``` + +--- + +## Contributing + +### How can I contribute to OMath? + +See [CONTRIBUTING.md](https://github.com/orange-cpp/omath/blob/master/CONTRIBUTING.md) for guidelines. Contributions welcome: +- Bug fixes +- New features +- Documentation improvements +- Test coverage +- Examples + +### Where do I report bugs? + +[GitHub Issues](https://github.com/orange-cpp/omath/issues) + +Please include: +- OMath version +- Compiler and version +- Minimal reproducible example +- Expected vs actual behavior + +### How do I request a feature? + +Open a GitHub issue with: +- Use case description +- Proposed API (if applicable) +- Why existing features don't meet your needs + +--- + +## License & Legal + +### What license does OMath use? + +OMath uses a custom "libomath" license. See [LICENSE](https://github.com/orange-cpp/omath/blob/master/LICENSE) for full details. + +### Can I use OMath in commercial projects? + +Check the LICENSE file for commercial use terms. + +### Can I use OMath for game cheating/hacking? + +OMath is a math library and can be used for various purposes. However: +- Using it to cheat in online games may violate game ToS +- Creating cheats may be illegal in your jurisdiction +- The developers do not condone cheating in online games + +Use responsibly and ethically. + +--- + +## Getting Help + +### Where can I get help? + +- **Documentation**: [http://libomath.org](http://libomath.org) +- **Discord**: [Join community](https://discord.gg/eDgdaWbqwZ) +- **Telegram**: [@orangennotes](https://t.me/orangennotes) +- **GitHub Issues**: [Report bugs/ask questions](https://github.com/orange-cpp/omath/issues) + +### Is there a Discord/community? + +Yes! Join our Discord: [https://discord.gg/eDgdaWbqwZ](https://discord.gg/eDgdaWbqwZ) + +### Are there video tutorials? + +Check our [YouTube channel](https://youtu.be/lM_NJ1yCunw?si=-Qf5yzDcWbaxAXGQ) for demonstrations and tutorials. + +--- + +## Didn't find your answer? + +- Search the [documentation](index.md) +- Check [tutorials](tutorials.md) +- Ask on [Discord](https://discord.gg/eDgdaWbqwZ) +- Open a [GitHub issue](https://github.com/orange-cpp/omath/issues) + +--- + +*Last updated: 1 Nov 2025* diff --git a/docs/getting_started.md b/docs/getting_started.md new file mode 100644 index 00000000..2d05f738 --- /dev/null +++ b/docs/getting_started.md @@ -0,0 +1,305 @@ +# Getting Started +Welcome to OMath! This guide will help you get up and running with the library quickly. + +## What is OMath? + +OMath is a modern, blazingly fast C++ math library designed for: +- **Game development** and cheat development +- **Graphics programming** (DirectX/OpenGL/Vulkan) +- **3D applications** with support for multiple game engines +- **High-performance computing** with AVX2 optimizations + +Key features: +- 100% independent, no legacy C++ code +- Fully `constexpr` template-based design +- Zero additional dependencies (except for unit tests) +- Cross-platform (Windows, macOS, Linux) +- Built-in support for Source, Unity, Unreal, Frostbite, IWEngine, and OpenGL coordinate systems + +--- + +## Installation + +Choose one of the following methods to install OMath: + +### Using vcpkg (Recommended) + +```bash +vcpkg install orange-math +``` + +Then in your CMakeLists.txt: +```cmake +find_package(omath CONFIG REQUIRED) +target_link_libraries(your_target PRIVATE omath::omath) +``` + +### Using xrepo + +```bash +xrepo install omath +``` + +Then in your xmake.lua: +```lua +add_requires("omath") +target("your_target") + add_packages("omath") +``` + +### Building from Source + +See the detailed [Installation Guide](install.md) for complete instructions. + +--- + +## Quick Example + +Here's a simple example to get you started: + +```cpp +#include +#include + +int main() { + using namespace omath; + + // Create 3D vectors + Vector3 a{1.0f, 2.0f, 3.0f}; + Vector3 b{4.0f, 5.0f, 6.0f}; + + // Vector operations + auto sum = a + b; // Vector addition + auto dot_product = a.dot(b); // Dot product: 32.0 + auto cross_product = a.cross(b); // Cross product: (-3, 6, -3) + auto length = a.length(); // Length: ~3.74 + auto normalized = a.normalized(); // Unit vector + + std::cout << "Sum: [" << sum.x << ", " << sum.y << ", " << sum.z << "]\n"; + std::cout << "Dot product: " << dot_product << "\n"; + std::cout << "Length: " << length << "\n"; + + return 0; +} +``` + +--- + +## Core Concepts + +### 1. Vectors + +OMath provides 2D, 3D, and 4D vector types: + +```cpp +using namespace omath; + +Vector2 vec2{1.0f, 2.0f}; +Vector3 vec3{1.0f, 2.0f, 3.0f}; +Vector4 vec4{1.0f, 2.0f, 3.0f, 4.0f}; +``` + +All vector types support: +- Arithmetic operations (+, -, *, /) +- Dot and cross products (where applicable) +- Length and distance calculations +- Normalization +- Component-wise operations + +See: [Vector2](linear_algebra/vector2.md), [Vector3](linear_algebra/vector3.md), [Vector4](linear_algebra/vector4.md) + +### 2. Matrices + +4x4 matrices for transformations: + +```cpp +using namespace omath; + +Mat4X4 matrix = Mat4X4::identity(); +// Use for transformations, projections, etc. +``` + +See: [Matrix Documentation](linear_algebra/mat.md) + +### 3. Angles + +Strong-typed angle system with automatic range management: + +```cpp +using namespace omath; + +auto angle = Angle::from_degrees(45.0f); +auto radians = angle.as_radians(); + +// View angles for camera systems +ViewAngles view{ + PitchAngle::from_degrees(-10.0f), + YawAngle::from_degrees(90.0f), + RollAngle::from_degrees(0.0f) +}; +``` + +See: [Angle](trigonometry/angle.md), [View Angles](trigonometry/view_angles.md) + +### 4. 3D Projection + +Built-in camera and projection systems: + +```cpp +using namespace omath; +using namespace omath::projection; + +ViewPort viewport{1920.0f, 1080.0f}; +auto fov = FieldOfView::from_degrees(90.0f); + +// Example using Source Engine +using namespace omath::source_engine; +Camera cam( + Vector3{0, 0, 100}, // Position + ViewAngles{}, // Angles + viewport, + fov, + 0.1f, // near plane + 1000.0f // far plane +); + +// Project 3D point to 2D screen +Vector3 world_pos{100, 50, 75}; +if (auto screen_pos = cam.world_to_screen(world_pos)) { + std::cout << "Screen: " << screen_pos->x << ", " << screen_pos->y << "\n"; +} +``` + +See: [Camera](projection/camera.md) + +### 5. Game Engine Support + +OMath provides pre-configured traits for major game engines: + +```cpp +// Source Engine +#include +using SourceCamera = omath::source_engine::Camera; + +// Unity Engine +#include +using UnityCamera = omath::unity_engine::Camera; + +// Unreal Engine +#include +using UnrealCamera = omath::unreal_engine::Camera; + +// And more: OpenGL, Frostbite, IWEngine +``` + +Each engine has its own coordinate system conventions automatically handled. + +See: Engine-specific docs in [engines/](engines/) folder + +--- + +## Common Use Cases + +### World-to-Screen Projection + +```cpp +using namespace omath; +using namespace omath::source_engine; + +Camera cam = /* initialize camera */; +Vector3 enemy_position{100, 200, 50}; + +if (auto screen = cam.world_to_screen(enemy_position)) { + // Draw ESP box at screen->x, screen->y + std::cout << "Enemy on screen at: " << screen->x << ", " << screen->y << "\n"; +} else { + // Enemy not visible (behind camera or outside frustum) +} +``` + +### Projectile Prediction + +```cpp +using namespace omath::projectile_prediction; + +Projectile bullet{ + Vector3{0, 0, 0}, // shooter position + 1000.0f, // muzzle velocity (m/s) + Vector3{0, 0, -9.81f} // gravity +}; + +Target enemy{ + Vector3{100, 200, 50}, // position + Vector3{10, 0, 0} // velocity +}; + +// Calculate where to aim +ProjPredEngineLegacy engine; +if (auto aim_point = engine.maybe_calculate_aim_point(bullet, enemy)) { + // Aim at *aim_point to hit moving target +} +``` + +See: [Projectile Prediction](projectile_prediction/projectile_engine.md) + +### Collision Detection + +```cpp +using namespace omath; + +// Ray-plane intersection +Plane ground{ + Vector3{0, 0, 0}, // point on plane + Vector3{0, 0, 1} // normal (pointing up) +}; + +Vector3 ray_origin{0, 0, 100}; +Vector3 ray_direction{0, 0, -1}; + +if (auto hit = ground.intersects_ray(ray_origin, ray_direction)) { + std::cout << "Hit ground at: " << hit->x << ", " << hit->y << ", " << hit->z << "\n"; +} +``` + +See: [Collision Detection](collision/line_tracer.md) + +### Pattern Scanning + +```cpp +#include + +using namespace omath; + +std::vector memory = /* ... */; +PatternView pattern{"48 8B 05 ?? ?? ?? ?? 48 85 C0"}; + +if (auto result = pattern_scan(memory, pattern)) { + std::cout << "Pattern found at offset: " << result->offset << "\n"; +} +``` + +See: [Pattern Scanning](utility/pattern_scan.md) + +--- + +## Next Steps + +Now that you have the basics, explore these topics: + +1. **[API Reference](index.md)** - Complete API documentation +2. **[Examples](../examples/)** - Working code examples +3. **[Engine-Specific Features](engines/)** - Deep dive into game engine support +4. **[Advanced Topics](#)** - Performance optimization, custom traits, etc. + +--- + +## Getting Help + +- **Documentation**: [http://libomath.org](http://libomath.org) +- **Discord**: [Join our community](https://discord.gg/eDgdaWbqwZ) +- **Telegram**: [@orangennotes](https://t.me/orangennotes) +- **Issues**: [GitHub Issues](https://github.com/orange-cpp/omath/issues) + +--- + +*Last updated: 1 Nov 2025* diff --git a/docs/images/logos/omath_logo_macro.png b/docs/images/logos/omath_logo_macro.png new file mode 100644 index 00000000..6835f1b8 Binary files /dev/null and b/docs/images/logos/omath_logo_macro.png differ diff --git a/docs/images/logos/omath_logo_mega.png b/docs/images/logos/omath_logo_mega.png new file mode 100644 index 00000000..db50b1a2 Binary files /dev/null and b/docs/images/logos/omath_logo_mega.png differ diff --git a/docs/images/logos/omath_logo_micro.png b/docs/images/logos/omath_logo_micro.png new file mode 100644 index 00000000..54eae1d1 Binary files /dev/null and b/docs/images/logos/omath_logo_micro.png differ diff --git a/docs/images/logos/omath_logo_nano.png b/docs/images/logos/omath_logo_nano.png new file mode 100644 index 00000000..07dacfb0 Binary files /dev/null and b/docs/images/logos/omath_logo_nano.png differ diff --git a/docs/images/showcase/apex.png b/docs/images/showcase/apex.png new file mode 100644 index 00000000..17ba276f Binary files /dev/null and b/docs/images/showcase/apex.png differ diff --git a/docs/images/showcase/cod_bo2.png b/docs/images/showcase/cod_bo2.png new file mode 100644 index 00000000..37c352d3 Binary files /dev/null and b/docs/images/showcase/cod_bo2.png differ diff --git a/docs/images/showcase/cs2.jpeg b/docs/images/showcase/cs2.jpeg new file mode 100644 index 00000000..6fd47cd2 Binary files /dev/null and b/docs/images/showcase/cs2.jpeg differ diff --git a/docs/images/showcase/opengl.png b/docs/images/showcase/opengl.png new file mode 100644 index 00000000..4226873a Binary files /dev/null and b/docs/images/showcase/opengl.png differ diff --git a/docs/images/showcase/tf2.jpg b/docs/images/showcase/tf2.jpg new file mode 100644 index 00000000..23b96f45 Binary files /dev/null and b/docs/images/showcase/tf2.jpg differ diff --git a/docs/images/yt_previews/img.png b/docs/images/yt_previews/img.png new file mode 100644 index 00000000..deed0710 Binary files /dev/null and b/docs/images/yt_previews/img.png differ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..36921807 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,239 @@ +
+ +

+ omath banner +

+ + +

+ license: libomath + GitHub contributors + Top language + + CodeFactor + + GitHub Actions Workflow Status + + Vcpkg package + + GitHub forks + + Join us on Discord + + + Telegram + +

+
+ +OMath is a 100% independent, constexpr template blazingly fast math library that doesn't have legacy C++ code. + +It provides the latest features, is highly customizable, has all for cheat development, DirectX/OpenGL/Vulkan support, premade support for different game engines, much more constexpr stuff than in other libraries and more... + +--- + +## 🚀 Quick Start + +**New to OMath?** Start here: + +- **[Getting Started Guide](getting_started.md)** - Installation and first steps +- **[API Overview](api_overview.md)** - High-level API reference +- **[Installation Instructions](install.md)** - Detailed setup guide + +**Quick example:** + +```cpp +#include + +using namespace omath; + +Vector3 a{1, 2, 3}; +Vector3 b{4, 5, 6}; + +auto dot = a.dot(b); // 32.0 +auto cross = a.cross(b); // (-3, 6, -3) +auto distance = a.distance_to(b); // ~5.196 +``` + +--- + +## 📚 Documentation Structure + +### Core Mathematics + +**Linear Algebra** +- [Vector2](linear_algebra/vector2.md) - 2D vectors with full operator support +- [Vector3](linear_algebra/vector3.md) - 3D vectors, dot/cross products, angles +- [Vector4](linear_algebra/vector4.md) - 4D vectors (homogeneous coordinates) +- [Mat4X4](linear_algebra/mat.md) - 4×4 matrices for transformations +- [Triangle](linear_algebra/triangle.md) - Triangle primitive and utilities + +**Trigonometry** +- [Angle](trigonometry/angle.md) - Strong-typed angle system with range enforcement +- [Angles](trigonometry/angles.md) - Angle utilities and conversions +- [View Angles](trigonometry/view_angles.md) - Pitch/Yaw/Roll for camera systems + +**3D Primitives** +- [Box](3d_primitives/box.md) - Axis-aligned bounding boxes +- [Plane](3d_primitives/plane.md) - Infinite planes and intersections + +### Game Development Features + +**Projection & Camera** +- [Camera](projection/camera.md) - Generic camera system with engine traits +- [Error Codes](projection/error_codes.md) - Projection error handling + +**Collision Detection** +- [Line Tracer](collision/line_tracer.md) - Ray-triangle, ray-plane intersections + +**Projectile Prediction** +- [Projectile Engine Interface](projectile_prediction/projectile_engine.md) - Base interface +- [Projectile](projectile_prediction/projectile.md) - Projectile properties +- [Target](projectile_prediction/target.md) - Target state representation +- [Legacy Engine](projectile_prediction/proj_pred_engine_legacy.md) - Standard implementation +- [AVX2 Engine](projectile_prediction/proj_pred_engine_avx2.md) - Optimized implementation + +**Pathfinding** +- [A* Algorithm](pathfinding/a_star.md) - A* pathfinding implementation +- [Navigation Mesh](pathfinding/navigation_mesh.md) - Triangle-based navigation + +### Game Engine Support + +OMath provides built-in support for multiple game engines with proper coordinate system handling: + +**Source Engine** (Valve - CS:GO, TF2, etc.) +- [Camera Trait](engines/source_engine/camera_trait.md) +- [Pred Engine Trait](engines/source_engine/pred_engine_trait.md) +- [Constants](engines/source_engine/constants.md) +- [Formulas](engines/source_engine/formulas.md) + +**Unity Engine** +- [Camera Trait](engines/unity_engine/camera_trait.md) +- [Pred Engine Trait](engines/unity_engine/pred_engine_trait.md) +- [Constants](engines/unity_engine/constants.md) +- [Formulas](engines/unity_engine/formulas.md) + +**Unreal Engine** (Epic Games) +- [Camera Trait](engines/unreal_engine/camera_trait.md) +- [Pred Engine Trait](engines/unreal_engine/pred_engine_trait.md) +- [Constants](engines/unreal_engine/constants.md) +- [Formulas](engines/unreal_engine/formulas.md) + +**Frostbite Engine** (EA - Battlefield, etc.) +- [Camera Trait](engines/frostbite/camera_trait.md) +- [Pred Engine Trait](engines/frostbite/pred_engine_trait.md) +- [Constants](engines/frostbite/constants.md) +- [Formulas](engines/frostbite/formulas.md) + +**IW Engine** (Infinity Ward - Call of Duty) +- [Camera Trait](engines/iw_engine/camera_trait.md) +- [Pred Engine Trait](engines/iw_engine/pred_engine_trait.md) +- [Constants](engines/iw_engine/constants.md) +- [Formulas](engines/iw_engine/formulas.md) + +**OpenGL Engine** (Canonical OpenGL) +- [Camera Trait](engines/opengl_engine/camera_trait.md) +- [Pred Engine Trait](engines/opengl_engine/pred_engine_trait.md) +- [Constants](engines/opengl_engine/constants.md) +- [Formulas](engines/opengl_engine/formulas.md) + +### Utilities + +**Color** +- [Color](utility/color.md) - RGBA color with conversions + +**Pattern Scanning & Memory** +- [Pattern Scan](utility/pattern_scan.md) - Binary pattern search with wildcards +- [PE Pattern Scan](utility/pe_pattern_scan.md) - PE file pattern scanning + +**Reverse Engineering** +- [External Rev Object](rev_eng/external_rev_object.md) - External process memory access +- [Internal Rev Object](rev_eng/internal_rev_object.md) - Internal memory access + +--- + +## ✨ Key Features + +- **Efficiency**: Optimized for performance, ensuring quick computations using AVX2. +- **Versatility**: Includes a wide array of mathematical functions and algorithms. +- **Ease of Use**: Simplified interface for convenient integration into various projects. +- **Projectile Prediction**: Projectile prediction engine with O(N) algo complexity, that can power you projectile aim-bot. +- **3D Projection**: No need to find view-projection matrix anymore you can make your own projection pipeline. +- **Collision Detection**: Production ready code to handle collision detection by using simple interfaces. +- **No Additional Dependencies**: No additional dependencies need to use OMath except unit test execution +- **Ready for meta-programming**: Omath use templates for common types like Vectors, Matrixes etc, to handle all types! +- **Engine support**: Supports coordinate systems of **Source, Unity, Unreal, Frostbite, IWEngine and canonical OpenGL**. +- **Cross platform**: Supports Windows, MacOS and Linux. +- **Algorithms**: Has ability to scan for byte pattern with wildcards in PE files/modules, binary slices, works even with Wine apps. + +--- + +## 📖 Common Use Cases + +### World-to-Screen Projection +Project 3D world coordinates to 2D screen space for ESP overlays, UI elements, or visualization. + +### Projectile Prediction +Calculate aim points for moving targets considering projectile speed, gravity, and target velocity. + +### Collision Detection +Perform ray-casting, line tracing, and intersection tests for hit detection and physics. + +### Pattern Scanning +Search for byte patterns in memory for reverse engineering, modding, or tool development. + +### Pathfinding +Find optimal paths through 3D spaces using A* algorithm and navigation meshes. + +--- + +## 🎮 Gallery + +
+ +[![Youtube Video](images/yt_previews/img.png)](https://youtu.be/lM_NJ1yCunw?si=-Qf5yzDcWbaxAXGQ) + +
+ +![APEX Preview] + +
+ +![BO2 Preview] + +
+ +![CS2 Preview] + +
+ +![TF2 Preview] + +
+
+ +--- + +## 🤝 Community & Support + +- **Documentation**: [http://libomath.org](http://libomath.org) +- **GitHub**: [orange-cpp/omath](https://github.com/orange-cpp/omath) +- **Discord**: [Join our community](https://discord.gg/eDgdaWbqwZ) +- **Telegram**: [@orangennotes](https://t.me/orangennotes) +- **Issues**: [Report bugs or request features](https://github.com/orange-cpp/omath/issues) + +--- + +## 💡 Contributing + +OMath is open source and welcomes contributions! See [CONTRIBUTING.md](https://github.com/orange-cpp/omath/blob/master/CONTRIBUTING.md) for guidelines. + +--- + +*Last updated: 1 Nov 2025* + + +[APEX Preview]: images/showcase/apex.png +[BO2 Preview]: images/showcase/cod_bo2.png +[CS2 Preview]: images/showcase/cs2.jpeg +[TF2 Preview]: images/showcase/tf2.jpg \ No newline at end of file diff --git a/docs/install.md b/docs/install.md new file mode 100644 index 00000000..4a3cd311 --- /dev/null +++ b/docs/install.md @@ -0,0 +1,68 @@ +# Installation + +## Using vcpkg +**Note**: Support vcpkg for package management +1. Install [vcpkg](https://github.com/microsoft/vcpkg) +2. Run the following command to install the orange-math package: +``` +vcpkg install orange-math +``` +CMakeLists.txt +```cmake +find_package(omath CONFIG REQUIRED) +target_link_libraries(main PRIVATE omath::omath) +``` +For detailed commands on installing different versions and more information, please refer to Microsoft's [official instructions](https://learn.microsoft.com/en-us/vcpkg/get_started/overview). + +## Using xrepo +**Note**: Support xrepo for package management +1. Install [xmake](https://xmake.io/) +2. Run the following command to install the omath package: +``` +xrepo install omath +``` +xmake.lua +```xmake +add_requires("omath") +target("...") + add_packages("omath") +``` + +## Build from source using CMake +1. **Preparation** + + Install needed tools: cmake, clang, git, msvc (windows only). + + 1. **Linux:** + ```bash + sudo pacman -Sy cmake ninja clang git + ``` + 2. **MacOS:** + ```bash + brew install llvm git cmake ninja + ``` + 3. **Windows:** + + Install Visual Studio from [here](https://visualstudio.microsoft.com/downloads/) and Git from [here](https://git-scm.com/downloads). + + Use x64 Native Tools shell to execute needed commands down below. +2. **Clone the repository:** + ```bash + git clone https://github.com/orange-cpp/omath.git + ``` +3. **Navigate to the project directory:** + ```bash + cd omath + ``` +4. **Build the project using CMake:** + ```bash + cmake --preset windows-release -S . + cmake --build cmake-build/build/windows-release --target omath -j 6 + ``` + Use **\-\** preset to build suitable version for yourself. Like **windows-release** or **linux-release**. + + | Platform Name | Build Config | + |---------------|---------------| + | windows | release/debug | + | linux | release/debug | + | darwin | release/debug | diff --git a/docs/javascripts/liquid-glass.js b/docs/javascripts/liquid-glass.js new file mode 100644 index 00000000..e5e78fce --- /dev/null +++ b/docs/javascripts/liquid-glass.js @@ -0,0 +1,76 @@ +/** + * Dynamic Liquid Glass — mouse-tracking specular highlight + * + * Creates a radial-gradient light spot that follows the cursor across + * glass-styled elements, giving them an interactive "liquid glass" + * refraction/reflection feel inspired by Apple's Liquid Glass design. + */ +(function () { + "use strict"; + + var SELECTORS = [ + ".md-header", + ".md-content", + ".md-sidebar__scrollwrap", + ".highlight", + ".md-search__form", + ".md-footer" + ]; + + /** Apply the radial highlight via CSS custom properties. */ + function applyHighlight(el, x, y) { + var rect = el.getBoundingClientRect(); + var px = x - rect.left; + var py = y - rect.top; + el.style.setProperty("--glass-x", px + "px"); + el.style.setProperty("--glass-y", py + "px"); + el.classList.add("glass-active"); + } + + function clearHighlight(el) { + el.classList.remove("glass-active"); + } + + /** Bind events once the DOM is ready. */ + function init() { + var elements = []; + SELECTORS.forEach(function (sel) { + var nodes = document.querySelectorAll(sel); + for (var i = 0; i < nodes.length; i++) { + elements.push(nodes[i]); + } + }); + + var ticking = false; + document.addEventListener("mousemove", function (e) { + if (ticking) return; + ticking = true; + requestAnimationFrame(function () { + elements.forEach(function (el) { + var rect = el.getBoundingClientRect(); + if ( + e.clientX >= rect.left && + e.clientX <= rect.right && + e.clientY >= rect.top && + e.clientY <= rect.bottom + ) { + applyHighlight(el, e.clientX, e.clientY); + } else { + clearHighlight(el); + } + }); + ticking = false; + }); + }); + + document.addEventListener("mouseleave", function () { + elements.forEach(clearHighlight); + }); + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); + } +})(); diff --git a/docs/linear_algebra/mat.md b/docs/linear_algebra/mat.md new file mode 100644 index 00000000..8d1efd68 --- /dev/null +++ b/docs/linear_algebra/mat.md @@ -0,0 +1,428 @@ +# `omath::Mat` — Matrix class (C++20/23) + +> Header: your project’s `mat.hpp` (requires `vector3.hpp`) +> Namespace: `omath` +> Requires: **C++23** (uses multi-parameter `operator[]`) +> SIMD (optional): define **`OMATH_USE_AVX2`** to enable AVX2-accelerated multiplication for `float`/`double`. + +--- + +## Overview + +`omath::Mat` is a compile-time, fixed-size matrix with: + +* **Row/column counts** as template parameters (no heap allocations). +* **Row-major** or **column-major** storage (compile-time via `MatStoreType`). +* **Arithmetic** and **linear algebra**: matrix × matrix, scalar ops, transpose, determinant, inverse (optional), etc. +* **Transform helpers**: translation, axis rotations, look-at, perspective & orthographic projections. +* **I/O helpers**: `to_string`/`to_wstring`/`to_u8string` and `std::formatter` specializations. + +--- + +## Template parameters + +| Parameter | Description | Default | +| ----------- | ------------------------------------------------------------------------ | ----------- | +| `Rows` | Number of rows (size_t, compile-time) | — | +| `Columns` | Number of columns (size_t, compile-time) | — | +| `Type` | Element type (arithmetic) | `float` | +| `StoreType` | Storage order: `MatStoreType::ROW_MAJOR` or `MatStoreType::COLUMN_MAJOR` | `ROW_MAJOR` | + +```cpp +enum class MatStoreType : uint8_t { ROW_MAJOR = 0, COLUMN_MAJOR }; +``` + +--- + +## Quick start + +```cpp +#include "mat.hpp" +using omath::Mat; + +// 4x4 float, row-major +Mat<4,4> I = { + {1,0,0,0}, + {0,1,0,0}, + {0,0,1,0}, + {0,0,0,1}, +}; + +// Multiply 4x4 transforms +Mat<4,4> A = { {1,2,3,0},{0,1,4,0},{5,6,0,0},{0,0,0,1} }; +Mat<4,4> B = { {2,0,0,0},{0,2,0,0},{0,0,2,0},{0,0,0,1} }; +Mat<4,4> C = A * B; // matrix × matrix + +// Scalar ops +auto D = C * 0.5f; // scale all entries + +// Indexing (C++23 multi-parameter operator[]) +float a03 = A[0,3]; // same as A.at(0,3) +A[1,2] = 42.0f; + +// Transpose, determinant, inverse +auto AT = A.transposed(); +float det = A.determinant(); // only for square matrices +auto inv = A.inverted(); // std::optional; std::nullopt if non-invertible +``` + +> **Note** +> Multiplication requires the **same** `StoreType` and `Type` on both operands, and dimensions must match at compile time. + +--- + +## Construction + +```cpp +Mat(); // zero-initialized +Mat(std::initializer_list> rows); +explicit Mat(const Type* raw_data); // copies Rows*Columns elements +Mat(const Mat&); Mat(Mat&&); +``` + +* **Zeroing/setting** + + ```cpp + m.clear(); // set all entries to 0 + m.set(3.14f); // set all entries to a value + ``` + +* **Shape & metadata** + + ```cpp + Mat<>::row_count(); // constexpr size_t + Mat<>::columns_count(); // constexpr size_t + Mat<>::size(); // constexpr MatSize {rows, columns} + Mat<>::get_store_ordering(); // constexpr MatStoreType + using ContainedType = Type; // alias + ``` + +--- + +## Element access + +```cpp +T& at(size_t r, size_t c); +T const& at(size_t r, size_t c) const; + +T& operator[](size_t r, size_t c); // C++23 +T const& operator[](size_t r, size_t c) const; // C++23 +``` + +> **Bounds checking** +> In debug builds you may enable/disable range checks via your compile-time macros (see the source guard around `at()`). + +--- + +## Arithmetic + +* **Matrix × matrix** + + ```cpp + // (Rows x Columns) * (Columns x OtherColumns) -> (Rows x OtherColumns) + template + Mat + operator*(const Mat&) const; + ``` + + * Complexity: `O(Rows * Columns * OtherColumns)`. + * AVX2-accelerated when `OMATH_USE_AVX2` is defined and `Type` is `float` or `double`. + +* **Scalars** + + ```cpp + Mat operator*(const Type& s) const; Mat& operator*=(const Type& s); + Mat operator/(const Type& s) const; Mat& operator/=(const Type& s); + ``` + +* **Transpose** + + ```cpp + Mat transposed() const noexcept; + ``` + +* **Determinant (square only)** + + ```cpp + Type determinant() const; // 1x1, 2x2 fast path; larger uses Laplace expansion + ``` + +* **Inverse (square only)** + + ```cpp + std::optional inverted() const; // nullopt if det == 0 + ``` + +* **Minors & cofactors (square only)** + + ```cpp + Mat strip(size_t r, size_t c) const; + Type minor(size_t r, size_t c) const; + Type alg_complement(size_t r, size_t c) const; // cofactor + ``` + +* **Utilities** + + ```cpp + Type sum() const noexcept; + auto& raw_array(); // std::array& + auto const& raw_array() const; + ``` + +* **Comparison / formatting** + + ```cpp + bool operator==(const Mat&) const; + bool operator!=(const Mat&) const; + + std::string to_string() const noexcept; + std::wstring to_wstring() const noexcept; + std::u8string to_u8string() const noexcept; + ``` + +// std::formatter specialization provided for char, wchar_t, char8_t + +```` + +--- + +## Storage order notes + +- **Row-major**: `index = row * Columns + column` +- **Column-major**: `index = row + column * Rows` + +Choose one **consistently** across your math types and shader conventions. Mixed orders are supported by the type system but not for cross-multiplying (store types must match). + +--- + +## Transform helpers + +### From vectors + +```cpp +template +Mat<1,4,T,St> mat_row_from_vector(const Vector3& v); + +template +Mat<4,1,T,St> mat_column_from_vector(const Vector3& v); +```` + +### Translation + +```cpp +template +Mat<4,4,T,St> mat_translation(const Vector3& d) noexcept; +``` + +### Axis rotations + +```cpp +// Angle type must provide angle.cos() and angle.sin() +template +Mat<4,4,T,St> mat_rotation_axis_x(const Angle& a) noexcept; + +template +Mat<4,4,T,St> mat_rotation_axis_y(const Angle& a) noexcept; + +template +Mat<4,4,T,St> mat_rotation_axis_z(const Angle& a) noexcept; +``` + +### Camera/view + +```cpp +template +Mat<4,4,T,St> mat_camera_view(const Vector3& forward, + const Vector3& right, + const Vector3& up, + const Vector3& camera_origin) noexcept; +``` + +### Perspective projections + +```cpp +template +Mat<4,4,T,St> mat_perspective_left_handed (float fov_deg, float aspect, float near, float far) noexcept; + +template +Mat<4,4,T,St> mat_perspective_right_handed(float fov_deg, float aspect, float near, float far) noexcept; +``` + +### Orthographic projections + +```cpp +template +Mat<4,4,T,St> mat_ortho_left_handed (T left, T right, T bottom, T top, T near, T far) noexcept; + +template +Mat<4,4,T,St> mat_ortho_right_handed(T left, T right, T bottom, T top, T near, T far) noexcept; +``` + +### Look-at matrices + +```cpp +template +Mat<4,4,T,St> mat_look_at_left_handed (const Vector3& eye, + const Vector3& center, + const Vector3& up); + +template +Mat<4,4,T,St> mat_look_at_right_handed(const Vector3& eye, + const Vector3& center, + const Vector3& up); +``` + +--- + +## Screen-space helper + +```cpp +template +static constexpr Mat<4,4> to_screen_mat(const Type& screen_w, const Type& screen_h) noexcept; +// Maps NDC to screen space (origin top-left, y down) +``` + +--- + +## Examples + +### 1) Building a left-handed camera and perspective + +```cpp +using V3 = omath::Vector3; +using M4 = omath::Mat<4,4,float, omath::MatStoreType::COLUMN_MAJOR>; + +V3 eye{0, 1, -5}, center{0, 0, 0}, up{0, 1, 0}; +M4 view = omath::mat_look_at_left_handed(eye, center, up); + +float fov = 60.f, aspect = 16.f/9.f, n = 0.1f, f = 100.f; +M4 proj = omath::mat_perspective_left_handed(fov, aspect, n, f); + +// final VP +M4 vp = proj * view; +``` + +### 2) Inverting a transform safely + +```cpp +omath::Mat<4,4> T = omath::mat_translation(omath::Vector3{2,3,4}); +if (auto inv = T.inverted()) { + // use *inv +} else { + // handle non-invertible +} +``` + +### 3) Formatting for logs + +```cpp +omath::Mat<2,2> A = { {1,2},{3,4} }; +std::string s = A.to_string(); // "[[ 1.000, 2.000]\n [ 3.000, 4.000]]" +std::string f = std::format("A = {}", A); // uses std::formatter +``` + +--- + +## Performance + +* **Cache-friendly kernels** per storage order when AVX2 is not enabled. +* **AVX2 path** (`OMATH_USE_AVX2`) for `float`/`double` implements FMAs with 256-bit vectors for both row-major and column-major multiplication. +* Complexity for `A(R×K) * B(K×C)`: **O(RKC)** regardless of storage order. + +--- + +## Constraints & concepts + +```cpp +template +concept MatTemplateEqual = + (M1::rows == M2::rows) && + (M1::columns == M2::columns) && + std::is_same_v && + (M1::store_type == M2::store_type); +``` + +> Use this concept to constrain generic functions that operate on like-shaped matrices. + +--- + +## Exceptions + +* `std::invalid_argument` — initializer list dimensions mismatch. +* `std::out_of_range` — out-of-bounds in `at()` when bounds checking is active (see source guard). +* `inverted()` does **not** throw; returns `std::nullopt` if `determinant() == 0`. + +--- + +## Build switches + +* **`OMATH_USE_AVX2`** — enable AVX2 vectorized multiplication paths (`` required). +* **Debug checks** — the `at()` method contains a conditional range check; refer to the preprocessor guard in the code to enable/disable in your configuration. + +--- + +## Known requirements & interoperability + +* **C++23** is required for multi-parameter `operator[]`. If you target pre-C++23, use `at(r,c)` instead. +* All binary operations require matching `Type` and `StoreType`. Convert explicitly if needed. + +--- + +## See also + +* `omath::Vector3` +* Projection helpers: `mat_perspective_*`, `mat_ortho_*` +* View helpers: `mat_look_at_*`, `mat_camera_view` +* Construction helpers: `mat_row_from_vector`, `mat_column_from_vector`, `mat_translation`, `mat_rotation_axis_*` + +--- + +## Appendix: API summary (signatures) + +```cpp +// Core +Mat(); Mat(const Mat&); Mat(Mat&&); +Mat(std::initializer_list>); +explicit Mat(const Type* raw); +Mat& operator=(const Mat&); Mat& operator=(Mat&&); + +static constexpr size_t row_count(); +static constexpr size_t columns_count(); +static consteval MatSize size(); +static constexpr MatStoreType get_store_ordering(); + +T& at(size_t r, size_t c); +T const& at(size_t r, size_t c) const; +T& operator[](size_t r, size_t c); +T const& operator[](size_t r, size_t c) const; + +void clear(); +void set(const Type& v); +Type sum() const noexcept; + +template Mat operator*(const Mat&) const; +Mat& operator*=(const Type&); Mat operator*(const Type&) const; +Mat& operator/=(const Type&); Mat operator/(const Type&) const; + +Mat transposed() const noexcept; +Type determinant() const; // square only +std::optional inverted() const; // square only + +Mat strip(size_t r, size_t c) const; +Type minor(size_t r, size_t c) const; +Type alg_complement(size_t r, size_t c) const; + +auto& raw_array(); auto const& raw_array() const; +std::string to_string() const noexcept; +std::wstring to_wstring() const noexcept; +std::u8string to_u8string() const noexcept; + +bool operator==(const Mat&) const; +bool operator!=(const Mat&) const; + +// Helpers (see sections above) +``` + +--- + +*Last updated: 31 Oct 2025* diff --git a/docs/linear_algebra/triangle.md b/docs/linear_algebra/triangle.md new file mode 100644 index 00000000..fc80f886 --- /dev/null +++ b/docs/linear_algebra/triangle.md @@ -0,0 +1,173 @@ +# `omath::Triangle` — Simple 3D triangle utility + +> Header: your project’s `triangle.hpp` +> Namespace: `omath` +> Depends on: `omath::Vector3` (from `vector3.hpp`) + +A tiny helper around three `Vector3` vertices with convenience methods for normals, edge vectors/lengths, a right-angle test (at **`v2`**), and the triangle centroid. + +> **Note on the template parameter** +> +> The class is declared as `template class Triangle`, but the stored vertices are concretely `Vector3`. In practice this type is currently **fixed to `Vector3`**. You can ignore the template parameter or refactor to store `Vector` if you intend true genericity. + +--- + +## Vertex layout & naming + +``` +v1 +|\ +| \ +a | \ hypot = |v1 - v3| +| \ +v2 -- v3 + b + +a = |v1 - v2| (side_a_length) +b = |v3 - v2| (side_b_length) +``` + +* **`side_a_vector()`** = `v1 - v2` (points from v2 → v1) +* **`side_b_vector()`** = `v3 - v2` (points from v2 → v3) +* **Right-angle check** uses `a² + b² ≈ hypot²` with an epsilon of `1e-4`. + +--- + +## Quick start + +```cpp +#include "triangle.hpp" +using omath::Vector3; +using omath::Triangle; + +Triangle tri( // template arg unused; any placeholder ok + Vector3{0,0,0}, // v1 + Vector3{0,0,1}, // v2 (right angle is tested at v2) + Vector3{1,0,1} // v3 +); + +auto n = tri.calculate_normal(); // unit normal (right-handed: (v3-v2) × (v1-v2)) +float a = tri.side_a_length(); // |v1 - v2| +float b = tri.side_b_length(); // |v3 - v2| +float hyp = tri.hypot(); // |v1 - v3| +bool rect = tri.is_rectangular(); // true if ~right triangle at v2 +auto C = tri.mid_point(); // centroid (average of v1,v2,v3) +``` + +--- + +## Data members + +```cpp +Vector3 m_vertex1; // v1 +Vector3 m_vertex2; // v2 (the corner tested by is_rectangular) +Vector3 m_vertex3; // v3 +``` + +--- + +## Constructors + +```cpp +constexpr Triangle() = default; +constexpr Triangle(const Vector3& v1, + const Vector3& v2, + const Vector3& v3); +``` + +--- + +## Methods + +```cpp +// Normal (unit) using right-handed cross product: +// n = (v3 - v2) × (v1 - v2), then normalized() +[[nodiscard]] constexpr Vector3 calculate_normal() const; + +// Edge lengths with the naming from the diagram +[[nodiscard]] float side_a_length() const; // |v1 - v2| +[[nodiscard]] float side_b_length() const; // |v3 - v2| + +// Edge vectors (from v2 to the other vertex) +[[nodiscard]] constexpr Vector3 side_a_vector() const; // v1 - v2 +[[nodiscard]] constexpr Vector3 side_b_vector() const; // v3 - v2 + +// Hypotenuse length between v1 and v3 +[[nodiscard]] constexpr float hypot() const; // |v1 - v3| + +// Right-triangle check at vertex v2 (Pythagoras with epsilon 1e-4) +[[nodiscard]] constexpr bool is_rectangular() const; + +// Centroid of the triangle (average of the 3 vertices) +[[nodiscard]] constexpr Vector3 mid_point() const; // actually the centroid +``` + +### Notes & edge cases + +* **Normal direction** follows the right-hand rule for the ordered vertices `{v2 → v3} × {v2 → v1}`. + Swap vertex order to flip the normal. +* **Degenerate triangles** (collinear or overlapping vertices) yield a **zero vector** normal (since `normalized()` of the zero vector returns the zero vector in your math types). +* **`mid_point()` is the centroid**, not the midpoint of any single edge. If you need the midpoint of edge `v1–v2`, use `(m_vertex1 + m_vertex2) * 0.5f`. + +--- + +## Examples + +### Area and plane from existing API + +```cpp +const auto a = tri.side_a_vector(); +const auto b = tri.side_b_vector(); +const auto n = b.cross(a); // unnormalized normal +float area = 0.5f * n.length(); // triangle area + +// Plane equation n̂·(x - v2) = 0 +auto nhat = n.length() > 0 ? n / n.length() : n; +float d = -nhat.dot(tri.m_vertex2); +``` + +### Project a point onto the triangle’s plane + +```cpp +Vector3 p{0.3f, 1.0f, 0.7f}; +auto n = tri.calculate_normal(); +float t = n.dot(tri.m_vertex2 - p); // signed distance along normal +auto projected = p + n * t; // on-plane projection +``` + +--- + +## API summary (signatures) + +```cpp +class Triangle final { +public: + constexpr Triangle(); + constexpr Triangle(const Vector3& v1, + const Vector3& v2, + const Vector3& v3); + + Vector3 m_vertex1, m_vertex2, m_vertex3; + + [[nodiscard]] constexpr Vector3 calculate_normal() const; + [[nodiscard]] float side_a_length() const; + [[nodiscard]] float side_b_length() const; + [[nodiscard]] constexpr Vector3 side_a_vector() const; + [[nodiscard]] constexpr Vector3 side_b_vector() const; + [[nodiscard]] constexpr float hypot() const; + [[nodiscard]] constexpr bool is_rectangular() const; + [[nodiscard]] constexpr Vector3 mid_point() const; +}; +``` + +--- + +## Suggestions (optional improvements) + +* If generic vectors are intended, store `Vector m_vertex*;` and constrain `Vector` to the required ops (`-`, `cross`, `normalized`, `distance_to`, `+`, `/`). +* Consider renaming `mid_point()` → `centroid()` to avoid ambiguity. +* Expose an `area()` helper and (optionally) a barycentric coordinate routine if you plan to use this in rasterization or intersection tests. + +--- + +*Last updated: 31 Oct 2025* diff --git a/docs/linear_algebra/vector2.md b/docs/linear_algebra/vector2.md new file mode 100644 index 00000000..7f044754 --- /dev/null +++ b/docs/linear_algebra/vector2.md @@ -0,0 +1,300 @@ +# `omath::Vector2` — 2D vector (C++20/23) + +> Header: your project’s `vector2.hpp` +> Namespace: `omath` +> Template: `template requires std::is_arithmetic_v` + +`Vector2` is a lightweight, POD-like 2D math type with arithmetic, geometry helpers, comparisons, hashing (for `float`), optional ImGui interop, and `std::formatter` support. + +--- + +## Quick start + +```cpp +#include "vector2.hpp" +using omath::Vector2; + +using Vec2f = Vector2; + +Vec2f a{3.f, 4.f}; +Vec2f b{1.f, 2.f}; + +auto d = a.distance_to(b); // ≈ 3.1623 +auto dot = a.dot(b); // 11 +auto len = a.length(); // 5 +auto unit_a = a.normalized(); // (0.6, 0.8) + +// Component-wise mutate +Vec2f c{2, 3}; +c *= b; // c -> (2*1, 3*2) = (2, 6) + +// Scalar ops (non-mutating + mutating) +auto scaled = a * 0.5f; // (1.5, 2) +a *= 2.f; // (6, 8) + +// Ordering by length() +bool shorter = (b < a); + +// Formatted printing +std::string s = std::format("a = {}", a); // "a = [6, 8]" +``` + +--- + +## Members + +```cpp +Type x{0}; +Type y{0}; +``` + +--- + +## Constructors + +```cpp +constexpr Vector2(); // (0,0) +constexpr Vector2(const Type& x, const Type& y) noexcept; +``` + +--- + +## Equality & ordering + +```cpp +constexpr bool operator==(const Vector2&) const noexcept; // component-wise equality +constexpr bool operator!=(const Vector2&) const noexcept; + +bool operator< (const Vector2&) const noexcept; // compares by length() +bool operator> (const Vector2&) const noexcept; +bool operator<=(const Vector2&) const noexcept; +bool operator>=(const Vector2&) const noexcept; +``` + +> **Note:** `<`, `>`, `<=`, `>=` order vectors by **magnitude** (not lexicographically). + +--- + +## Arithmetic + +### With another vector (component-wise, **mutating**) + +```cpp +Vector2& operator+=(const Vector2&) noexcept; +Vector2& operator-=(const Vector2&) noexcept; +Vector2& operator*=(const Vector2&) noexcept; // Hadamard product (x*=x, y*=y) +Vector2& operator/=(const Vector2&) noexcept; +``` + +> Non-mutating `v * u` / `v / u` (vector × vector) are **not** provided. +> Use `v *= u` (mutating) or build a new vector explicitly. + +### With a scalar + +```cpp +Vector2& operator*=(const Type& v) noexcept; +Vector2& operator/=(const Type& v) noexcept; +Vector2& operator+=(const Type& v) noexcept; +Vector2& operator-=(const Type& v) noexcept; + +constexpr Vector2 operator*(const Type& v) const noexcept; +constexpr Vector2 operator/(const Type& v) const noexcept; +``` + +### Binary (+/−) with another vector (non-mutating) + +```cpp +constexpr Vector2 operator+(const Vector2&) const noexcept; +constexpr Vector2 operator-(const Vector2&) const noexcept; +``` + +### Unary + +```cpp +constexpr Vector2 operator-() const noexcept; // negation +``` + +--- + +## Geometry & helpers + +```cpp +Type distance_to (const Vector2&) const noexcept; // sqrt of squared distance +constexpr Type distance_to_sqr(const Vector2&) const noexcept; + +constexpr Type dot(const Vector2&) const noexcept; + +#ifndef _MSC_VER +constexpr Type length() const noexcept; // uses std::hypot; constexpr on non-MSVC +constexpr Vector2 normalized() const noexcept; // returns *this if length==0 +#else +Type length() const noexcept; +Vector2 normalized() const noexcept; +#endif + +constexpr Type length_sqr() const noexcept; // x*x + y*y +Vector2& abs() noexcept; // component-wise absolute (constexpr-friendly impl) + +constexpr Type sum() const noexcept; // x + y +constexpr std::tuple as_tuple() const noexcept; +``` + +--- + +## ImGui integration (optional) + +Define `OMATH_IMGUI_INTEGRATION` **before** including the header. + +```cpp +#ifdef OMATH_IMGUI_INTEGRATION +constexpr ImVec2 to_im_vec2() const noexcept; // {float(x), float(y)} +static Vector2 from_im_vec2(const ImVec2&) noexcept; +#endif +``` + +--- + +## Hashing & formatting + +* **Hash (for `Vector2`)** + + ```cpp + template<> struct std::hash> { + std::size_t operator()(const omath::Vector2&) const noexcept; + }; + ``` + + Example: + + ```cpp + std::unordered_set> set; + set.insert({1.f, 2.f}); + ``` + +* **`std::formatter`** (for any `Type`) + + ```cpp + // prints "[x, y]" for char / wchar_t / char8_t + template + struct std::formatter>; + ``` + +--- + +## Notes & invariants + +* `Type` must be arithmetic (e.g., `float`, `double`, `int`, …). +* `normalized()` returns the input unchanged if `length() == 0`. +* `abs()` uses a constexpr-friendly implementation (not `std::abs`) to allow compile-time evaluation. +* On MSVC, `length()`/`normalized()` are not `constexpr` due to library constraints; they’re still `noexcept`. + +--- + +## Examples + +### Component-wise operations and scalar scaling + +```cpp +omath::Vector2 u{2, 3}, v{4, 5}; + +u += v; // (6, 8) +u -= v; // (2, 3) +u *= v; // (8, 15) Hadamard product (mutates u) +auto w = v * 2.0f; // (8, 10) non-mutating scalar multiply +``` + +### Geometry helpers + +```cpp +omath::Vector2 p{0.0, 0.0}, q{3.0, 4.0}; + +auto dsq = p.distance_to_sqr(q); // 25 +auto d = p.distance_to(q); // 5 +auto dot = p.dot(q); // 0 +auto uq = q.normalized(); // (0.6, 0.8) +``` + +### Using as a key in unordered containers (`float`) + +```cpp +std::unordered_map, int> counts; +counts[{1.f, 2.f}] = 42; +``` + +### ImGui interop + +```cpp +#define OMATH_IMGUI_INTEGRATION +#include "vector2.hpp" + +omath::Vector2 v{10, 20}; +ImVec2 iv = v.to_im_vec2(); +v = omath::Vector2::from_im_vec2(iv); +``` + +--- + +## API summary (signatures) + +```cpp +// Constructors +constexpr Vector2(); +constexpr Vector2(const Type& x, const Type& y) noexcept; + +// Equality & ordering +constexpr bool operator==(const Vector2&) const noexcept; +constexpr bool operator!=(const Vector2&) const noexcept; +bool operator< (const Vector2&) const noexcept; +bool operator> (const Vector2&) const noexcept; +bool operator<=(const Vector2&) const noexcept; +bool operator>=(const Vector2&) const noexcept; + +// Compound (vector/vector and scalar) +Vector2& operator+=(const Vector2&) noexcept; +Vector2& operator-=(const Vector2&) noexcept; +Vector2& operator*=(const Vector2&) noexcept; +Vector2& operator/=(const Vector2&) noexcept; +Vector2& operator*=(const Type&) noexcept; +Vector2& operator/=(const Type&) noexcept; +Vector2& operator+=(const Type&) noexcept; +Vector2& operator-=(const Type&) noexcept; + +// Non-mutating arithmetic +constexpr Vector2 operator+(const Vector2&) const noexcept; +constexpr Vector2 operator-(const Vector2&) const noexcept; +constexpr Vector2 operator*(const Type&) const noexcept; +constexpr Vector2 operator/(const Type&) const noexcept; +constexpr Vector2 operator-() const noexcept; + +// Geometry +Type distance_to(const Vector2&) const noexcept; +constexpr Type distance_to_sqr(const Vector2&) const noexcept; +constexpr Type dot(const Vector2&) const noexcept; +Type length() const noexcept; // constexpr on non-MSVC +Vector2 normalized() const noexcept; // constexpr on non-MSVC +constexpr Type length_sqr() const noexcept; +Vector2& abs() noexcept; +constexpr Type sum() const noexcept; +constexpr std::tuple as_tuple() const noexcept; + +// ImGui (optional) +#ifdef OMATH_IMGUI_INTEGRATION +constexpr ImVec2 to_im_vec2() const noexcept; +static Vector2 from_im_vec2(const ImVec2&) noexcept; +#endif + +// Hash (float) and formatter are specialized in the header +``` + +--- + +## See Also + +- [Vector3 Documentation](vector3.md) - 3D vector operations +- [Vector4 Documentation](vector4.md) - 4D vector operations +- [Getting Started Guide](../getting_started.md) - Quick start with OMath +- [Tutorials](../tutorials.md) - Step-by-step examples + +--- + +*Last updated: 1 Nov 2025* diff --git a/docs/linear_algebra/vector3.md b/docs/linear_algebra/vector3.md new file mode 100644 index 00000000..15dda160 --- /dev/null +++ b/docs/linear_algebra/vector3.md @@ -0,0 +1,307 @@ +# `omath::Vector3` — 3D vector (C++20/23) + +> Header: your project’s `vector3.hpp` +> Namespace: `omath` +> Template: `template requires std::is_arithmetic_v` +> Depends on: `omath::Vector2` (base class), `omath::Angle` (for `angle_between`) +> C++: uses `std::expected` ⇒ **C++23** recommended (or a backport) + +`Vector3` extends `Vector2` with a `z` component and 3D operations: arithmetic, geometry (dot, cross, distance), normalization, angle-between with robust error signaling, hashing (for `float`) and `std::formatter` support. + +--- + +## Quick start + +```cpp +#include "vector3.hpp" +using omath::Vector3; + +using Vec3f = Vector3; + +Vec3f a{3, 4, 0}; +Vec3f b{1, 2, 2}; + +auto d = a.distance_to(b); // Euclidean distance +auto dot = a.dot(b); // 3*1 + 4*2 + 0*2 = 11 +auto cr = a.cross(b); // (8, -6, 2) +auto len = a.length(); // hypot(x,y,z) +auto unit = a.normalized(); // safe normalize (returns a if length==0) + +if (auto ang = a.angle_between(b)) { + float deg = ang->as_degrees(); // [0, 180], clamped +} else { + // vectors have zero length -> no defined angle +} +``` + +--- + +## Data members + +```cpp +Type x{0}; // inherited from Vector2 +Type y{0}; // inherited from Vector2 +Type z{0}; +``` + +--- + +## Constructors + +```cpp +constexpr Vector3() noexcept; +constexpr Vector3(const Type& x, const Type& y, const Type& z) noexcept; +``` + +--- + +## Equality & ordering + +```cpp +constexpr bool operator==(const Vector3&) const noexcept; // component-wise +constexpr bool operator!=(const Vector3&) const noexcept; + +bool operator< (const Vector3&) const noexcept; // compare by length() +bool operator> (const Vector3&) const noexcept; +bool operator<=(const Vector3&) const noexcept; +bool operator>=(const Vector3&) const noexcept; +``` + +> **Note:** Ordering uses **magnitude**, not lexicographic order. + +--- + +## Arithmetic (mutating) + +Component-wise with another vector: + +```cpp +Vector3& operator+=(const Vector3&) noexcept; +Vector3& operator-=(const Vector3&) noexcept; +Vector3& operator*=(const Vector3&) noexcept; // Hadamard product +Vector3& operator/=(const Vector3&) noexcept; +``` + +With a scalar: + +```cpp +Vector3& operator*=(const Type& v) noexcept; +Vector3& operator/=(const Type& v) noexcept; +Vector3& operator+=(const Type& v) noexcept; +Vector3& operator-=(const Type& v) noexcept; +``` + +--- + +## Arithmetic (non-mutating) + +```cpp +constexpr Vector3 operator-() const noexcept; +constexpr Vector3 operator+(const Vector3&) const noexcept; +constexpr Vector3 operator-(const Vector3&) const noexcept; +constexpr Vector3 operator*(const Vector3&) const noexcept; // Hadamard +constexpr Vector3 operator/(const Vector3&) const noexcept; // Hadamard +constexpr Vector3 operator*(const Type& scalar) const noexcept; +constexpr Vector3 operator/(const Type& scalar) const noexcept; +``` + +--- + +## Geometry & helpers + +```cpp +// Distances & lengths +Type distance_to(const Vector3&) const; // sqrt of squared distance +constexpr Type distance_to_sqr(const Vector3&) const noexcept; +#ifndef _MSC_VER +constexpr Type length() const; // hypot(x,y,z) +constexpr Type length_2d() const; // 2D length from base +constexpr Vector3 normalized() const; // returns *this if length==0 +#else +Type length() const noexcept; +Type length_2d() const noexcept; +Vector3 normalized() const noexcept; +#endif +constexpr Type length_sqr() const noexcept; + +// Products +constexpr Type dot(const Vector3&) const noexcept; +constexpr Vector3 cross(const Vector3&) const noexcept; // right-handed + +// Sums & tuples +constexpr Type sum() const noexcept; // x + y + z +constexpr Type sum_2d() const noexcept; // x + y +constexpr auto as_tuple() const noexcept -> std::tuple; + +// Utilities +Vector3& abs() noexcept; // component-wise absolute +``` + +--- + +## Angles & orthogonality + +```cpp +enum class Vector3Error { IMPOSSIBLE_BETWEEN_ANGLE }; + +// Angle in degrees, clamped to [0,180]. Error if any vector has zero length. +std::expected< + omath::Angle, + Vector3Error +> angle_between(const Vector3& other) const noexcept; + +bool is_perpendicular(const Vector3& other) const noexcept; // true if angle == 90° +``` + +--- + +## Hashing & formatting + +* **Hash (for `Vector3`)** + + ```cpp + template<> struct std::hash> { + std::size_t operator()(const omath::Vector3&) const noexcept; + }; + ``` + + Example: + + ```cpp + std::unordered_map, int> counts; + counts[{1.f, 2.f, 3.f}] = 7; + ``` + +* **`std::formatter`** (all character types) + + ```cpp + template + struct std::formatter>; // prints "[x, y, z]" + ``` + +--- + +## Error handling + +* `angle_between()` returns `std::unexpected(Vector3Error::IMPOSSIBLE_BETWEEN_ANGLE)` if either vector length is zero. +* Other operations are total for arithmetic `Type` (no throwing behavior in this class). + +--- + +## Examples + +### Cross product & perpendicular check + +```cpp +omath::Vector3 x{1,0,0}, y{0,1,0}; +auto z = x.cross(y); // (0,0,1) +bool perp = x.is_perpendicular(y); // true +``` + +### Safe normalization and angle + +```cpp +omath::Vector3 u{0,0,0}, v{1,1,0}; +auto nu = u.normalized(); // returns {0,0,0} +if (auto ang = u.angle_between(v)) { + // won't happen: u has zero length → error +} else { + // handle degenerate case +} +``` + +### Hadamard vs scalar multiply + +```cpp +omath::Vector3 a{2,3,4}, b{5,6,7}; +auto h = a * b; // (10, 18, 28) component-wise +auto s = a * 2.f; // (4, 6, 8) scalar +``` + +--- + +## API summary (signatures) + +```cpp +// Ctors +constexpr Vector3() noexcept; +constexpr Vector3(const Type& x, const Type& y, const Type& z) noexcept; + +// Equality & ordering +constexpr bool operator==(const Vector3&) const noexcept; +constexpr bool operator!=(const Vector3&) const noexcept; +bool operator< (const Vector3&) const noexcept; +bool operator> (const Vector3&) const noexcept; +bool operator<=(const Vector3&) const noexcept; +bool operator>=(const Vector3&) const noexcept; + +// Mutating arithmetic +Vector3& operator+=(const Vector3&) noexcept; +Vector3& operator-=(const Vector3&) noexcept; +Vector3& operator*=(const Vector3&) noexcept; +Vector3& operator/=(const Vector3&) noexcept; +Vector3& operator*=(const Type&) noexcept; +Vector3& operator/=(const Type&) noexcept; +Vector3& operator+=(const Type&) noexcept; +Vector3& operator-=(const Type&) noexcept; + +// Non-mutating arithmetic +constexpr Vector3 operator-() const noexcept; +constexpr Vector3 operator+(const Vector3&) const noexcept; +constexpr Vector3 operator-(const Vector3&) const noexcept; +constexpr Vector3 operator*(const Vector3&) const noexcept; +constexpr Vector3 operator/(const Vector3&) const noexcept; +constexpr Vector3 operator*(const Type&) const noexcept; +constexpr Vector3 operator/(const Type&) const noexcept; + +// Geometry +Type distance_to(const Vector3&) const; +constexpr Type distance_to_sqr(const Vector3&) const noexcept; +#ifndef _MSC_VER +constexpr Type length() const; +constexpr Type length_2d() const; +constexpr Vector3 normalized() const; +#else +Type length() const noexcept; +Type length_2d() const noexcept; +Vector3 normalized() const noexcept; +#endif +constexpr Type length_sqr() const noexcept; +constexpr Type dot(const Vector3&) const noexcept; +constexpr Vector3 cross(const Vector3&) const noexcept; + +Vector3& abs() noexcept; +constexpr Type sum() const noexcept; +constexpr Type sum_2d() const noexcept; +constexpr auto as_tuple() const noexcept -> std::tuple; + +// Angles +std::expected, omath::Vector3Error> +angle_between(const Vector3&) const noexcept; +bool is_perpendicular(const Vector3&) const noexcept; + +// Hash (float) and formatter specializations provided below the class +``` + +--- + +## Notes + +* Inherits all public API of `Vector2` (including `x`, `y`, many operators, and helpers used internally). +* `normalized()` returns the original vector if its length is zero (no NaNs). +* `cross()` uses the standard right-handed definition. +* `length()`/`normalized()` are `constexpr` on non-MSVC; MSVC builds provide `noexcept` runtime versions. + +--- + +## See Also + +- [Vector2 Documentation](vector2.md) - 2D vector operations +- [Vector4 Documentation](vector4.md) - 4D vector operations +- [Angle Documentation](../trigonometry/angle.md) - Working with angles +- [Getting Started Guide](../getting_started.md) - Quick start with OMath +- [Tutorials](../tutorials.md) - Practical examples including vector math + +--- + +*Last updated: 1 Nov 2025* diff --git a/docs/linear_algebra/vector4.md b/docs/linear_algebra/vector4.md new file mode 100644 index 00000000..833fa736 --- /dev/null +++ b/docs/linear_algebra/vector4.md @@ -0,0 +1,253 @@ +# `omath::Vector4` — 4D vector (C++20/23) + +> Header: your project’s `vector4.hpp` +> Namespace: `omath` +> Template: `template requires std::is_arithmetic_v` +> Inherits: `omath::Vector3` (brings `x`, `y`, `z` and most scalar ops) + +`Vector4` extends `Vector3` with a `w` component and 4D operations: component-wise arithmetic, scalar ops, dot/length helpers, clamping, hashing (for `float`) and `std::formatter` support. Optional ImGui interop is available behind a macro. + +--- + +## Quick start + +```cpp +#include "vector4.hpp" +using omath::Vector4; + +using Vec4f = Vector4; + +Vec4f a{1, 2, 3, 1}; +Vec4f b{4, 5, 6, 2}; + +// Component-wise & scalar ops +auto c = a + b; // (5, 7, 9, 3) +c *= 0.5f; // (2.5, 3.5, 4.5, 1.5) +auto h = a * b; // Hadamard: (4, 10, 18, 2) + +// Dot / length +float d = a.dot(b); // 1*4 + 2*5 + 3*6 + 1*2 = 32 +float L = a.length(); // sqrt(x²+y²+z²+w²) + +// Clamp (x,y,z only; see notes) +Vec4f col{1.4f, -0.2f, 0.7f, 42.f}; +col.clamp(0.f, 1.f); // -> (1, 0, 0.7, w unchanged) +``` + +--- + +## Data members + +```cpp +// Inherited from Vector3: +Type x{0}; +Type y{0}; +Type z{0}; + +// Added in Vector4: +Type w{0}; +``` + +--- + +## Constructors + +```cpp +constexpr Vector4() noexcept; // (0,0,0,0) +constexpr Vector4(const Type& x, const Type& y, + const Type& z, const Type& w); // value-init +``` + +--- + +## Equality & ordering + +```cpp +constexpr bool operator==(const Vector4&) const noexcept; // component-wise +constexpr bool operator!=(const Vector4&) const noexcept; + +bool operator< (const Vector4&) const noexcept; // compare by length() +bool operator> (const Vector4&) const noexcept; +bool operator<=(const Vector4&) const noexcept; +bool operator>=(const Vector4&) const noexcept; +``` + +> **Note:** Ordering uses **magnitude** (Euclidean norm), not lexicographic order. + +--- + +## Arithmetic (mutating) + +With another vector (component-wise): + +```cpp +Vector4& operator+=(const Vector4&) noexcept; +Vector4& operator-=(const Vector4&) noexcept; +Vector4& operator*=(const Vector4&) noexcept; // Hadamard +Vector4& operator/=(const Vector4&) noexcept; +``` + +With a scalar: + +```cpp +Vector4& operator*=(const Type& v) noexcept; +Vector4& operator/=(const Type& v) noexcept; + +// From base class (inherited): +Vector4& operator+=(const Type& v) noexcept; // adds v to x,y,z (and w via base? see notes) +Vector4& operator-=(const Type& v) noexcept; +``` + +--- + +## Arithmetic (non-mutating) + +```cpp +constexpr Vector4 operator-() const noexcept; +constexpr Vector4 operator+(const Vector4&) const noexcept; +constexpr Vector4 operator-(const Vector4&) const noexcept; +constexpr Vector4 operator*(const Vector4&) const noexcept; // Hadamard +constexpr Vector4 operator/(const Vector4&) const noexcept; // Hadamard +constexpr Vector4 operator*(const Type& scalar) const noexcept; +constexpr Vector4 operator/(const Type& scalar) const noexcept; +``` + +--- + +## Geometry & helpers + +```cpp +constexpr Type length_sqr() const noexcept; // x² + y² + z² + w² +Type length() const noexcept; // std::sqrt(length_sqr()) +constexpr Type dot(const Vector4& rhs) const noexcept; + +Vector4& abs() noexcept; // component-wise absolute +Vector4& clamp(const Type& min, const Type& max) noexcept; + // clamps x,y,z; leaves w unchanged (see notes) +constexpr Type sum() const noexcept; // x + y + z + w +``` + +--- + +## ImGui integration (optional) + +Guarded by `OMATH_IMGUI_INTEGRATION`: + +```cpp +#ifdef OMATH_IMGUI_INTEGRATION +constexpr ImVec4 to_im_vec4() const noexcept; +// NOTE: Provided signature returns Vector4 and (in current code) sets only x,y,z. +// See "Notes & caveats" for a corrected version you may prefer. +static Vector4 from_im_vec4(const ImVec4& other) noexcept; +#endif +``` + +--- + +## Hashing & formatting + +* **Hash specialization** (only for `Vector4`): + + ```cpp + template<> struct std::hash> { + std::size_t operator()(const omath::Vector4&) const noexcept; + }; + ``` + + Example: + + ```cpp + std::unordered_map, int> counts; + counts[{1.f, 2.f, 3.f, 1.f}] = 7; + ``` + +* **`std::formatter`** (for any `Type`, all character kinds): + + ```cpp + template + struct std::formatter>; // -> "[x, y, z, w]" + ``` + +--- + +## Notes & caveats (as implemented) + +* `clamp(min,max)` **clamps only `x`, `y`, `z`** and **does not clamp `w`**. This may be intentional (e.g., when `w` is a homogeneous coordinate) — document your intent in your codebase. + If you want to clamp `w` too: + + ```cpp + w = std::clamp(w, min, max); + ``` + +* **ImGui interop**: + + * The header references `ImVec4` but does not include `` itself. Ensure it’s included **before** this header whenever `OMATH_IMGUI_INTEGRATION` is defined. + * `from_im_vec4` currently returns `Vector4` and (in the snippet shown) initializes **only x,y,z**. A more consistent version would be: + + ```cpp + #ifdef OMATH_IMGUI_INTEGRATION + static Vector4 from_im_vec4(const ImVec4& v) noexcept { + return {static_cast(v.x), static_cast(v.y), + static_cast(v.z), static_cast(v.w)}; + } + #endif + ``` + +* Many scalar compound operators (`+= Type`, `-= Type`) are inherited from `Vector3`. + +--- + +## API summary (signatures) + +```cpp +// Ctors +constexpr Vector4() noexcept; +constexpr Vector4(const Type& x, const Type& y, const Type& z, const Type& w); + +// Equality & ordering +constexpr bool operator==(const Vector4&) const noexcept; +constexpr bool operator!=(const Vector4&) const noexcept; +bool operator< (const Vector4&) const noexcept; +bool operator> (const Vector4&) const noexcept; +bool operator<=(const Vector4&) const noexcept; +bool operator>=(const Vector4&) const noexcept; + +// Mutating arithmetic +Vector4& operator+=(const Vector4&) noexcept; +Vector4& operator-=(const Vector4&) noexcept; +Vector4& operator*=(const Vector4&) noexcept; +Vector4& operator/=(const Vector4&) noexcept; +Vector4& operator*=(const Type&) noexcept; +Vector4& operator/=(const Type&) noexcept; +// (inherited) Vector4& operator+=(const Type&) noexcept; +// (inherited) Vector4& operator-=(const Type&) noexcept; + +// Non-mutating arithmetic +constexpr Vector4 operator-() const noexcept; +constexpr Vector4 operator+(const Vector4&) const noexcept; +constexpr Vector4 operator-(const Vector4&) const noexcept; +constexpr Vector4 operator*(const Vector4&) const noexcept; +constexpr Vector4 operator/(const Vector4&) const noexcept; +constexpr Vector4 operator*(const Type&) const noexcept; +constexpr Vector4 operator/(const Type&) const noexcept; + +// Geometry & helpers +constexpr Type length_sqr() const noexcept; +Type length() const noexcept; +constexpr Type dot(const Vector4&) const noexcept; +Vector4& abs() noexcept; +Vector4& clamp(const Type& min, const Type& max) noexcept; +constexpr Type sum() const noexcept; + +// ImGui (optional) +#ifdef OMATH_IMGUI_INTEGRATION +constexpr ImVec4 to_im_vec4() const noexcept; +static Vector4 from_im_vec4(const ImVec4&) noexcept; // see note for preferred template version +#endif + +// Hash (float) and formatter specializations provided below the class +``` + +--- + +*Last updated: 31 Oct 2025* diff --git a/docs/pathfinding/a_star.md b/docs/pathfinding/a_star.md new file mode 100644 index 00000000..df44820c --- /dev/null +++ b/docs/pathfinding/a_star.md @@ -0,0 +1,188 @@ +# `omath::pathfinding::Astar` — Pathfinding over a navigation mesh + +> Header: your project’s `pathfinding/astar.hpp` +> Namespace: `omath::pathfinding` +> Inputs: start/end as `Vector3`, a `NavigationMesh` +> Output: ordered list of waypoints `std::vector>` + +`Astar` exposes a single public function, `find_path`, that computes a path of 3D waypoints on a navigation mesh. Internally it reconstructs the result with `reconstruct_final_path` from a closed set keyed by `Vector3`. + +--- + +## API + +```cpp +namespace omath::pathfinding { + +struct PathNode; // holds per-node search data (see "Expected PathNode fields") + +class Astar final { +public: + [[nodiscard]] + static std::vector> + find_path(const Vector3& start, + const Vector3& end, + const NavigationMesh& nav_mesh) noexcept; + +private: + [[nodiscard]] + static std::vector> + reconstruct_final_path( + const std::unordered_map, PathNode>& closed_list, + const Vector3& current) noexcept; +}; + +} // namespace omath::pathfinding +``` + +### Semantics + +* Returns a **polyline** of 3D points from `start` to `end`. +* If no path exists, the function typically returns an **empty vector** (behavior depends on implementation details; keep this contract in unit tests). + +--- + +## What `NavigationMesh` is expected to provide + +The header doesn’t constrain `NavigationMesh`, but for A* it commonly needs: + +* **Neighborhood queries**: given a position or node key → iterable neighbors. +* **Traversal cost**: `g(u,v)` (often Euclidean distance or edge weight). +* **Heuristic**: `h(x,end)` (commonly straight-line distance on the mesh). +* **Projection / snap**: the ability to map `start`/`end` to valid nodes/points on the mesh (if they are off-mesh). + +> If your `NavigationMesh` doesn’t directly expose these, `Astar::find_path` likely does the adapter work (snapping to the nearest convex polygon/portal nodes and expanding across adjacency). + +--- + +## Expected `PathNode` fields + +Although not visible here, `PathNode` typically carries: + +* `Vector3 parent;` — predecessor position or key for backtracking +* `float g;` — cost from `start` +* `float h;` — heuristic to `end` +* `float f;` — `g + h` + +`reconstruct_final_path(closed_list, current)` walks `parent` links from `current` back to the start, **reverses** the chain, and returns the path. + +--- + +## Heuristic & optimality + +* Use an **admissible** heuristic (never overestimates true cost) to keep A* optimal. + The usual choice is **Euclidean distance** in 3D: + + ```cpp + h(x, goal) = (goal - x).length(); + ``` +* For best performance, make it **consistent** (triangle inequality holds). Euclidean distance is consistent on standard navmeshes. + +--- + +## Complexity + +Let `V` be explored vertices (or portal nodes) and `E` the traversed edges. + +* With a binary heap open list: **O(E log V)** time, **O(V)** memory. +* With a d-ary heap or pairing heap you may reduce practical constants. + +--- + +## Typical usage + +```cpp +#include "omath/pathfinding/astar.hpp" +#include "omath/pathfinding/navigation_mesh.hpp" + +using omath::Vector3; +using omath::pathfinding::Astar; + +NavigationMesh nav = /* ... load/build mesh ... */; + +Vector3 start{2.5f, 0.0f, -1.0f}; +Vector3 goal {40.0f, 0.0f, 12.0f}; + +auto path = Astar::find_path(start, goal, nav); + +if (!path.empty()) { + // feed to your agent/renderer + for (const auto& p : path) { + // draw waypoint p or push to steering + } +} else { + // handle "no path" (e.g., unreachable or disconnected mesh) +} +``` + +--- + +## Notes & recommendations + +* **Start/end snapping**: If `start` or `end` are outside the mesh, decide whether to snap to the nearest polygon/portal or fail early. Keep this behavior consistent and document it where `NavigationMesh` is defined. +* **Numerical stability**: Prefer squared distances when only comparing (`dist2`) to avoid unnecessary `sqrt`. +* **Tie-breaking**: When `f` ties are common (grid-like graphs), bias toward larger `g` or smaller `h` to reduce zig-zagging. +* **Smoothing**: A* returns a polyline that may hug polygon edges. Consider: + + * **String pulling / Funnel algorithm** over the corridor of polygons to get a straightened path. + * **Raycast smoothing** (visibility checks) to remove redundant interior points. +* **Hashing `Vector3`**: Your repo defines `std::hash>`. Ensure equality/precision rules for using float keys are acceptable (or use discrete node IDs instead). + +--- + +## Testing checklist + +* Start/end on the **same polygon** → direct path of 2 points. +* **Disconnected components** → empty result. +* **Narrow corridors** → path stays inside. +* **Obstacles blocking** → no path. +* **Floating-point noise** → still reconstructs a valid chain from parents. + +--- + +## Minimal pseudo-implementation outline (for reference) + +```cpp +// Pseudocode only — matches the header’s intent +std::vector find_path(start, goal, mesh) { + auto [snode, gnode] = mesh.snap_to_nodes(start, goal); + OpenSet open; // min-heap by f + std::unordered_map closed; + + open.push({snode, g=0, h=heuristic(snode, gnode)}); + parents.clear(); + + while (!open.empty()) { + auto current = open.pop_min(); // node with lowest f + + if (current.pos == gnode.pos) + return reconstruct_final_path(closed, current.pos); + + for (auto [nbr, cost] : mesh.neighbors(current.pos)) { + float tentative_g = current.g + cost; + if (auto it = closed.find(nbr); it == closed.end() || tentative_g < it->second.g) { + closed[nbr] = { .parent = current.pos, + .g = tentative_g, + .h = heuristic(nbr, gnode.pos), + .f = tentative_g + heuristic(nbr, gnode.pos) }; + open.push(closed[nbr]); + } + } + } + return {}; // no path +} +``` + +--- + +## FAQ + +* **Why return `std::vector>` and not polygon IDs?** + Waypoints are directly usable by agents/steering and for rendering. If you also need the corridor (polygon chain), extend the API or `PathNode` to store it. + +* **Does `find_path` modify the mesh?** + No; it should be a read-only search over `NavigationMesh`. + +--- + +*Last updated: 31 Oct 2025* diff --git a/docs/pathfinding/navigation_mesh.md b/docs/pathfinding/navigation_mesh.md new file mode 100644 index 00000000..a03f6c59 --- /dev/null +++ b/docs/pathfinding/navigation_mesh.md @@ -0,0 +1,113 @@ +# `omath::pathfinding::NavigationMesh` — Lightweight vertex graph for A* + +> Header: your project’s `pathfinding/navigation_mesh.hpp` +> Namespace: `omath::pathfinding` +> Nodes: `Vector3` (3D points) +> Storage: adjacency map `unordered_map, std::vector>>` + +A minimal navigation mesh represented as a **vertex/edge graph**. Each vertex is a `Vector3` and neighbors are stored in an adjacency list. Designed to pair with `Astar::find_path`. + +--- + +## API + +```cpp +class NavigationMesh final { +public: + // Nearest graph vertex to an arbitrary 3D point. + // On success -> closest vertex; on failure -> std::string error (e.g., empty mesh). + [[nodiscard]] + std::expected, std::string> + get_closest_vertex(const Vector3& point) const noexcept; + + // Read-only neighbor list for a vertex key. + // If vertex is absent, implementation should return an empty list (see notes). + [[nodiscard]] + const std::vector>& + get_neighbors(const Vector3& vertex) const noexcept; + + // True if the graph has no vertices/edges. + [[nodiscard]] + bool empty() const; + + // Serialize/deserialize the graph (opaque binary). + [[nodiscard]] std::vector serialize() const noexcept; + void deserialize(const std::vector& raw) noexcept; + + // Public adjacency (vertex -> neighbors) + std::unordered_map, std::vector>> m_vertex_map; +}; +``` + +--- + +## Quick start + +```cpp +using omath::Vector3; +using omath::pathfinding::NavigationMesh; + +// Build a tiny mesh (triangle) +NavigationMesh nav; +nav.m_vertex_map[ {0,0,0} ] = { {1,0,0}, {0,0,1} }; +nav.m_vertex_map[ {1,0,0} ] = { {0,0,0}, {0,0,1} }; +nav.m_vertex_map[ {0,0,1} ] = { {0,0,0}, {1,0,0} }; + +// Query the closest node to an arbitrary point +auto q = nav.get_closest_vertex({0.3f, 0.0f, 0.2f}); +if (q) { + const auto& v = *q; + const auto& nbrs = nav.get_neighbors(v); + (void)nbrs; +} +``` + +--- + +## Semantics & expectations + +* **Nearest vertex** + `get_closest_vertex(p)` should scan known vertices and return the one with minimal Euclidean distance to `p`. If the mesh is empty, expect an error (`unexpected` with a message). + +* **Neighbors** + `get_neighbors(v)` returns the adjacency list for `v`. If `v` is not present, a conventional behavior is to return a **reference to a static empty vector** (since the API is `noexcept` and returns by reference). Verify in your implementation. + +* **Graph invariants** (recommended) + + * Neighbor links are **symmetric** for undirected navigation (if `u` has `v`, then `v` has `u`). + * No self-loops unless explicitly desired. + * Vertices are unique keys; hashing uses `std::hash>` (be mindful of floating-point equality). + +--- + +## Serialization + +* `serialize()` → opaque, implementation-defined binary of the current `m_vertex_map`. +* `deserialize(raw)` → restores the internal map from `raw`. + Keep versioning in mind if you evolve the format (e.g., add a header/magic/version). + +--- + +## Performance + +Let `V = m_vertex_map.size()` and `E = Σ|neighbors(v)|`. + +* `get_closest_vertex`: **O(V)** (linear scan) unless you back it with a spatial index (KD-tree, grid, etc.). +* `get_neighbors`: **O(1)** average (hash lookup). +* Memory: **O(V + E)**. + +--- + +## Usage notes + +* **Floating-point keys**: Using `Vector3` as an unordered_map key relies on your `std::hash>` and exact `operator==`. Avoid building meshes with numerically “close but not equal” duplicates; consider canonicalizing or using integer IDs if needed. +* **Pathfinding**: Pair with `Astar::find_path(start, end, nav)`; the A* heuristic can use straight-line distance between vertex positions. + +--- + +## Minimal test ideas + +* Empty mesh → `get_closest_vertex` returns error; `empty() == true`. +* Single vertex → nearest always that vertex; neighbors empty. +* Symmetric edges → `get_neighbors(a)` contains `b` and vice versa. +* Serialization round-trip preserves vertex/edge counts and neighbor lists. diff --git a/docs/projectile_prediction/proj_pred_engine_avx2.md b/docs/projectile_prediction/proj_pred_engine_avx2.md new file mode 100644 index 00000000..56969f71 --- /dev/null +++ b/docs/projectile_prediction/proj_pred_engine_avx2.md @@ -0,0 +1,161 @@ +# `omath::projectile_prediction::ProjPredEngineAvx2` — AVX2-accelerated ballistic aim solver + +> Header: your project’s `projectile_prediction/proj_pred_engine_avx2.hpp` +> Namespace: `omath::projectile_prediction` +> Inherits: `ProjPredEngineInterface` +> Depends on: `Vector3`, `Projectile`, `Target` +> CPU: Uses AVX2 when available; falls back to scalar elsewhere (fields are marked `[[maybe_unused]]` for non-x86/AVX2 builds). + +This engine computes a **world-space aim point** (and implicitly the firing **yaw/pitch**) to intersect a moving target under a **constant gravity** model and **constant muzzle speed**. It typically scans candidate times of flight and solves for the elevation (`pitch`) that makes the vertical and horizontal kinematics meet at the same time. + +--- + +## API + +```cpp +class ProjPredEngineAvx2 final : public ProjPredEngineInterface { +public: + [[nodiscard]] + std::optional> + maybe_calculate_aim_point(const Projectile& projectile, + const Target& target) const override; + + ProjPredEngineAvx2(float gravity_constant, + float simulation_time_step, + float maximum_simulation_time); + ~ProjPredEngineAvx2() override = default; + +private: + // Solve for pitch at a fixed time-of-flight t. + [[nodiscard]] + static std::optional + calculate_pitch(const Vector3& proj_origin, + const Vector3& target_pos, + float bullet_gravity, float v0, float time); + + // Tunables (may be unused on non-AVX2 builds) + [[maybe_unused]] const float m_gravity_constant; // |g| (e.g., 9.81) + [[maybe_unused]] const float m_simulation_time_step; // Δt (e.g., 1/240 s) + [[maybe_unused]] const float m_maximum_simulation_time; // Tmax (e.g., 3 s) +}; +``` + +### Parameters (constructor) + +* `gravity_constant` — magnitude of gravity (units consistent with your world, e.g., **m/s²**). +* `simulation_time_step` — Δt used to scan candidate intercept times. +* `maximum_simulation_time` — cap on time of flight; larger allows longer-range solutions but increases cost. + +### Return (solver) + +* `maybe_calculate_aim_point(...)` + + * **`Vector3`**: a world-space **aim point** yielding an intercept under the model. + * **`std::nullopt`**: no feasible solution (e.g., target receding too fast, out of range, or kinematics inconsistent). + +--- + +## How it solves (expected flow) + +1. **Predict target at time `t`** (constant-velocity model unless your `Target` carries more): + + ``` + T(t) = target.position + target.velocity * t + ``` +2. **Horizontal/vertical kinematics at fixed `t`** with muzzle speed `v0` and gravity `g`: + + * Let `Δ = T(t) - proj_origin`, `d = length(Δ.xz)`, `h = Δ.y`. + * Required initial components: + + ``` + cosθ = d / (v0 * t) + sinθ = (h + 0.5 * g * t^2) / (v0 * t) + ``` + * If `cosθ` ∈ [−1,1] and `sinθ` ∈ [−1,1] and `sin²θ + cos²θ ≈ 1`, then + + ``` + θ = atan2(sinθ, cosθ) + ``` + + That is what `calculate_pitch(...)` returns on success. +3. **Yaw** is the azimuth toward `Δ.xz`. +4. **Pick the earliest feasible `t`** in `[Δt, Tmax]` (scanned in steps of `Δt`; AVX2 batches several `t` at once). +5. **Return the aim point.** Common choices: + + * The **impact point** `T(t*)` (useful as a HUD marker), or + * A point along the **initial firing ray** at some convenient range using `(yaw, pitch)`; both are consistent—pick the convention your caller expects. + +> The private `calculate_pitch(...)` matches step **2** and returns `nullopt` if the trigonometric constraints are violated for that `t`. + +--- + +## AVX2 notes + +* On x86/x64 with AVX2, candidate times `t` can be evaluated **8 at a time** using FMA (great for dense scans). +* On ARM/ARM64 (no AVX2), code falls back to scalar math; the `[[maybe_unused]]` members acknowledge compilation without SIMD. + +--- + +## Usage example + +```cpp +using namespace omath::projectile_prediction; + +ProjPredEngineAvx2 solver( + /*gravity*/ 9.81f, + /*dt*/ 1.0f/240.0f, + /*Tmax*/ 3.0f +); + +Projectile proj; // fill: origin, muzzle_speed, etc. +Target tgt; // fill: position, velocity + +if (auto aim = solver.maybe_calculate_aim_point(proj, tgt)) { + // Aim your weapon at *aim and fire with muzzle speed proj.v0 + // If you need yaw/pitch explicitly, replicate the pitch solve and azimuth. +} else { + // No solution (out of envelope) — pick a fallback +} +``` + +--- + +## Edge cases & failure modes + +* **Zero or tiny `v0`** → no solution. +* **Target collinear & receding faster than `v0`** → no solution. +* **`t` constraints**: if viable solutions exist only beyond `Tmax`, you’ll get `nullopt`. +* **Geometric infeasibility** at a given `t` (e.g., `d > v0*t`) causes `calculate_pitch` to fail that sample. +* **Numerical tolerance**: check `sin²θ + cos²θ` against 1 with a small epsilon (e.g., `1e-3`). + +--- + +## Performance & tuning + +* Work is roughly `O(Nt)` where `Nt ≈ Tmax / Δt`. +* Smaller `Δt` → better accuracy, higher cost. With AVX2 you can afford smaller steps. +* If you frequently miss solutions **between** steps, consider: + + * **Coarse-to-fine**: coarse scan, then local refine around the best `t`. + * **Newton on time**: root-find `‖horizontal‖ − v0 t cosθ(t) = 0` shaped from the kinematics. + +--- + +## Testing checklist + +* **Stationary target** at same height → θ ≈ 0, aim point ≈ target. +* **Higher target** → positive pitch; **lower target** → negative pitch. +* **Perpendicular moving target** → feasible at moderate speeds. +* **Very fast receding target** → `nullopt`. +* **Boundary**: `d ≈ v0*Tmax` and `h` large → verify pass/fail around thresholds. + +--- + +## See also + +* `ProjPredEngineInterface` — base interface and general contract +* `Projectile`, `Target` — data carriers for solver inputs (speed, origin, position, velocity, etc.) + +--- + +*Last updated: 1 Nov 2025* diff --git a/docs/projectile_prediction/proj_pred_engine_legacy.md b/docs/projectile_prediction/proj_pred_engine_legacy.md new file mode 100644 index 00000000..fca8b174 --- /dev/null +++ b/docs/projectile_prediction/proj_pred_engine_legacy.md @@ -0,0 +1,184 @@ +# `omath::projectile_prediction::ProjPredEngineLegacy` — Legacy trait-based aim solver + +> Header: `omath/projectile_prediction/proj_pred_engine_legacy.hpp` +> Namespace: `omath::projectile_prediction` +> Inherits: `ProjPredEngineInterface` +> Template param (default): `EngineTrait = source_engine::PredEngineTrait` +> Purpose: compute a world-space **aim point** to hit a (possibly moving) target using a **discrete time scan** and a **closed-form ballistic pitch** under constant gravity. + +--- + +## Overview + +`ProjPredEngineLegacy` is a portable, trait-driven projectile lead solver. At each simulation time step `t` it: + +1. **Predicts target position** with `EngineTrait::predict_target_position(target, t, g)`. +2. **Computes launch pitch** via a gravity-aware closed form (or a direct angle if gravity is zero). +3. **Validates** that a projectile fired with that pitch (and direct yaw) actually reaches the predicted target within a **distance tolerance** at time `t`. +4. On success, **returns an aim point** computed by `EngineTrait::calc_viewpoint_from_angles(...)`. + +If no time step yields a feasible solution up to `maximum_simulation_time`, returns `std::nullopt`. + +--- + +## API + +```cpp +template +requires PredEngineConcept +class ProjPredEngineLegacy final : public ProjPredEngineInterface { +public: + ProjPredEngineLegacy(float gravity_constant, + float simulation_time_step, + float maximum_simulation_time, + float distance_tolerance); + + [[nodiscard]] + std::optional> + maybe_calculate_aim_point(const Projectile& projectile, + const Target& target) const override; + +private: + // Closed-form ballistic pitch solver (internal) + std::optional + maybe_calculate_projectile_launch_pitch_angle(const Projectile& projectile, + const Vector3& target_position) const noexcept; + + bool is_projectile_reached_target(const Vector3& target_position, + const Projectile& projectile, + float pitch, float time) const noexcept; + + const float m_gravity_constant; + const float m_simulation_time_step; + const float m_maximum_simulation_time; + const float m_distance_tolerance; +}; +``` + +### Constructor parameters + +* `gravity_constant` — magnitude of gravity (e.g., `9.81f`), world units/s². +* `simulation_time_step` — Δt for the scan (e.g., `1/240.f`). +* `maximum_simulation_time` — search horizon in seconds. +* `distance_tolerance` — max allowed miss distance at time `t` to accept a solution. + +--- + +## Trait requirements (`PredEngineConcept`) + +Your `EngineTrait` must expose **noexcept** static methods with these signatures: + +```cpp +Vector3 predict_projectile_position(const Projectile&, float pitch_deg, float yaw_deg, + float time, float gravity) noexcept; + +Vector3 predict_target_position(const Target&, float time, float gravity) noexcept; + +float calc_vector_2d_distance(const Vector3& v) noexcept; // typically length in XZ plane +float get_vector_height_coordinate(const Vector3& v) noexcept; // typically Y + +Vector3 calc_viewpoint_from_angles(const Projectile&, Vector3 target, + std::optional maybe_pitch_deg) noexcept; + +float calc_direct_pitch_angle(const Vector3& from, const Vector3& to) noexcept; +float calc_direct_yaw_angle (const Vector3& from, const Vector3& to) noexcept; +``` + +> This design lets you adapt different game/physics conventions (axes, units, handedness) without changing the solver. + +--- + +## Algorithm details + +### Time scan + +For `t = 0 .. maximum_simulation_time` in steps of `simulation_time_step`: + +1. `T = EngineTrait::predict_target_position(target, t, g)` +2. `pitch = maybe_calculate_projectile_launch_pitch_angle(projectile, T)` + + * If `std::nullopt`: continue +3. `yaw = EngineTrait::calc_direct_yaw_angle(projectile.m_origin, T)` +4. `P = EngineTrait::predict_projectile_position(projectile, pitch, yaw, t, g)` +5. Accept if `|P - T| <= distance_tolerance` +6. Return `EngineTrait::calc_viewpoint_from_angles(projectile, T, pitch)` + +### Closed-form pitch (gravity on) + +Implements the classic ballistic formula (low-arc branch), where: + +* `v` = muzzle speed, +* `g` = `gravity_constant * projectile.m_gravity_scale`, +* `x` = horizontal (2D) distance to target, +* `y` = vertical offset to target. + +[ +\theta ;=; \arctan!\left(\frac{v^{2} ;-; \sqrt{v^{4}-g!\left(gx^{2}+2yv^{2}\right)}}{gx}\right) +] + +* If the **discriminant** ( v^{4}-g(gx^{2}+2yv^{2}) < 0 ) ⇒ **no real solution**. +* If `g == 0`, falls back to `EngineTrait::calc_direct_pitch_angle(...)`. +* Returns **degrees** (internally converts from radians). + +--- + +## Usage example + +```cpp +using namespace omath::projectile_prediction; + +ProjPredEngineLegacy solver( + /*gravity*/ 9.81f, + /*dt*/ 1.f / 240.f, + /*Tmax*/ 3.0f, + /*tol*/ 0.05f +); + +Projectile proj; // fill: m_origin, m_launch_speed, m_gravity_scale, etc. +Target tgt; // fill: position/velocity as required by your trait + +if (auto aim = solver.maybe_calculate_aim_point(proj, tgt)) { + // Drive your turret/reticle toward *aim +} else { + // No feasible intercept in the given horizon +} +``` + +--- + +## Behavior & edge cases + +* **Zero gravity or zero distance**: uses direct pitch toward the target. +* **Negative discriminant** in the pitch formula: returns `std::nullopt` for that time step. +* **Very small `x`** (horizontal distance): the formula’s denominator `gx` approaches zero; your trait’s direct pitch helper provides a stable fallback. +* **Tolerance**: `distance_tolerance` controls acceptance; tighten for accuracy, loosen for robustness. + +--- + +## Complexity & tuning + +* Time: **O(T)** where ( T \approx \frac{\text{maximum_simulation_time}}{\text{simulation_time_step}} ) + plus trait costs for prediction and angle math per step. +* Smaller `simulation_time_step` improves precision but increases runtime. +* If needed, do a **coarse-to-fine** search: coarse Δt scan, then refine around the best hit time. + +--- + +## Testing checklist + +* Stationary, level target → pitch ≈ 0 for short ranges; accepted within tolerance. +* Elevated/depressed targets → pitch positive/negative as expected. +* Receding fast target → unsolved within horizon ⇒ `nullopt`. +* Gravity scale = 0 → identical to straight-line solution. +* Near-horizon shots (large range, small arc) → discriminant near zero; verify stability. + +--- + +## Notes + +* All angles produced/consumed by the trait in this implementation are **degrees**. +* `calc_viewpoint_from_angles` defines what “aim point” means in your engine (e.g., a point along the initial ray or the predicted impact point). Keep this consistent with your HUD/reticle. + +--- + +*Last updated: 1 Nov 2025* diff --git a/docs/projectile_prediction/projectile.md b/docs/projectile_prediction/projectile.md new file mode 100644 index 00000000..a741a88d --- /dev/null +++ b/docs/projectile_prediction/projectile.md @@ -0,0 +1,96 @@ +# `omath::projectile_prediction::Projectile` — Projectile parameters for aim solvers + +> Header: `omath/projectile_prediction/projectile.hpp` +> Namespace: `omath::projectile_prediction` +> Used by: `ProjPredEngineInterface` implementations (e.g., `ProjPredEngineLegacy`, `ProjPredEngineAvx2`) + +`Projectile` is a tiny data holder that describes how a projectile is launched: **origin** (world position), **launch speed**, and a **gravity scale** (multiplier applied to the engine’s gravity constant). + +--- + +## API + +```cpp +namespace omath::projectile_prediction { + +class Projectile final { +public: + Vector3 m_origin; // Launch position (world space) + float m_launch_speed{}; // Initial speed magnitude (units/sec) + float m_gravity_scale{}; // Multiplier for global gravity (dimensionless) +}; + +} // namespace omath::projectile_prediction +``` + +--- + +## Field semantics + +* **`m_origin`** + World-space position where the projectile is spawned (e.g., muzzle or emitter point). + +* **`m_launch_speed`** + Initial speed **magnitude** in your world units per second. Direction is determined by the solver (from yaw/pitch). + + * Must be **non-negative**. Zero disables meaningful ballistic solutions. + +* **`m_gravity_scale`** + Multiplies the engine’s gravity constant provided to the solver (e.g., `g = gravity_constant * m_gravity_scale`). + + * Use `1.0f` for normal gravity, `0.0f` for no-drop projectiles, other values to simulate heavier/lighter rounds. + +> Units must be consistent across your project (e.g., meters & seconds). If `gravity_constant = 9.81f`, then `m_launch_speed` is in m/s and positions are in meters. + +--- + +## Typical usage + +```cpp +using namespace omath::projectile_prediction; + +Projectile proj; +proj.m_origin = { 0.0f, 1.6f, 0.0f }; // player eye / muzzle height +proj.m_launch_speed = 850.0f; // e.g., 850 m/s +proj.m_gravity_scale = 1.0f; // normal gravity + +// With an aim solver: +auto aim = engine->maybe_calculate_aim_point(proj, target); +if (aim) { + // rotate/aim toward *aim and fire +} +``` + +--- + +## With gravity-aware solver (outline) + +Engines typically compute the firing angles to reach a predicted target position: + +* Horizontal distance `x` and vertical offset `y` are derived from `target - m_origin`. +* Gravity used is `g = gravity_constant * m_gravity_scale`. +* Launch direction has speed `m_launch_speed` and angles solved by the engine. + +If `m_gravity_scale == 0`, engines usually fall back to straight-line (no-drop) solutions. + +--- + +## Validation & tips + +* Keep `m_launch_speed ≥ 0`. Negative values are nonsensical. +* If your weapon can vary muzzle speed (charge-up, attachments), update `m_launch_speed` per shot. +* For different ammo types (tracers, grenades), prefer tweaking **`m_gravity_scale`** (and possibly the engine’s gravity constant) to match observed arc. + +--- + +## See also + +* `ProjPredEngineInterface` — common interface for aim solvers +* `ProjPredEngineLegacy` — trait-based, time-stepped ballistic solver +* `ProjPredEngineAvx2` — AVX2-accelerated solver with fixed-time pitch solve +* `Target` — target state consumed by the solvers +* `Vector3` — math type used for positions and directions + +--- + +*Last updated: 1 Nov 2025* diff --git a/docs/projectile_prediction/projectile_engine.md b/docs/projectile_prediction/projectile_engine.md new file mode 100644 index 00000000..a47ff637 --- /dev/null +++ b/docs/projectile_prediction/projectile_engine.md @@ -0,0 +1,162 @@ +# `omath::projectile_prediction::ProjPredEngineInterface` — Aim-point solver interface + +> Header: your project’s `projectile_prediction/proj_pred_engine_interface.hpp` +> Namespace: `omath::projectile_prediction` +> Depends on: `Vector3`, `Projectile`, `Target` +> Purpose: **contract** for engines that compute a lead/aim point to hit a moving target. + +--- + +## Overview + +`ProjPredEngineInterface` defines a single pure-virtual method that attempts to compute the **world-space aim point** where a projectile should be launched to intersect a target under the engine’s physical model (e.g., constant projectile speed, gravity, drag, max flight time, etc.). + +If a valid solution exists, the engine returns the 3D aim point. Otherwise, it returns `std::nullopt` (no feasible intercept). + +--- + +## API + +```cpp +namespace omath::projectile_prediction { + +class ProjPredEngineInterface { +public: + [[nodiscard]] + virtual std::optional> + maybe_calculate_aim_point(const Projectile& projectile, + const Target& target) const = 0; + + virtual ~ProjPredEngineInterface() = default; +}; + +} // namespace omath::projectile_prediction +``` + +### Semantics + +* **Input** + + * `Projectile` — engine-specific projectile properties (typical: muzzle speed, gravity vector, drag flag/coeff, max range / flight time). + * `Target` — target state (typical: position, velocity, possibly acceleration). + +* **Output** + + * `std::optional>` + + * `value()` — world-space point to aim at **now** so that the projectile intersects the target under the model. + * `std::nullopt` — no solution (e.g., target outruns projectile, blocked by constraints, numerical failure). + +* **No side effects**: method is `const` and should not modify inputs. + +--- + +## Typical usage + +```cpp +using namespace omath::projectile_prediction; + +std::unique_ptr engine = /* your implementation */; +Projectile proj = /* fill from weapon config */; +Target tgt = /* read from tracking system */; + +if (auto aim = engine->maybe_calculate_aim_point(proj, tgt)) { + // Rotate/steer to (*aim) +} else { + // Fall back: no-lead, predictive UI, or do not fire +} +``` + +--- + +## Implementation guidance (for engine authors) + +**Common models:** + +1. **No gravity, constant speed** + Closed form intersect time `t` solves `‖p_t + v_t t − p_0‖ = v_p t`. + Choose the smallest non-negative real root; aim point = `p_t + v_t t`. + +2. **Gravity (constant g), constant speed** + Solve ballistics with vertical drop: either numerical (Newton–Raphson on time) or 2D elevation + azimuth decomposition. Ensure convergence caps and time bounds. + +3. **Drag** + Typically requires numeric integration (e.g., RK4) wrapped in a root find on time-of-flight. + +**Robustness tips:** + +* **Feasibility checks:** return `nullopt` when: + + * projectile speed ≤ 0; target too fast in receding direction; solution time outside `[0, t_max]`. +* **Bounds:** clamp search time to reasonable `[t_min, t_max]` (e.g., `[0, max_flight_time]` or by range). +* **Tolerances:** use epsilons for convergence (e.g., `|f(t)| < 1e-4`, `|Δt| < 1e-4 s`). +* **Determinism:** fix iteration counts or seeds if needed for replayability. + +--- + +## Example: constant-speed, no-gravity intercept (closed form) + +```cpp +// Solve ||p + v t|| = s t where p = target_pos - shooter_pos, v = target_vel, s = projectile_speed +// Quadratic: (v·v - s^2) t^2 + 2 (p·v) t + (p·p) = 0 +inline std::optional intercept_time_no_gravity(const Vector3& p, + const Vector3& v, + float s) { + const float a = v.dot(v) - s*s; + const float b = 2.f * p.dot(v); + const float c = p.dot(p); + if (std::abs(a) < 1e-6f) { // near linear + if (std::abs(b) < 1e-6f) return std::nullopt; + float t = -c / b; + return t >= 0.f ? std::optional{t} : std::nullopt; + } + const float disc = b*b - 4.f*a*c; + if (disc < 0.f) return std::nullopt; + const float sqrtD = std::sqrt(disc); + float t1 = (-b - sqrtD) / (2.f*a); + float t2 = (-b + sqrtD) / (2.f*a); + float t = (t1 >= 0.f ? t1 : t2); + return t >= 0.f ? std::optional{t} : std::nullopt; +} +``` + +Aim point (given shooter origin `S`, target pos `T`, vel `V`): + +``` +p = T - S +t* = intercept_time_no_gravity(p, V, speed) +aim = T + V * t* +``` + +Return `nullopt` if `t*` is absent. + +--- + +## Testing checklist + +* **Stationary target**: aim point equals target position when `s > 0`. +* **Target perpendicular motion**: lead equals lateral displacement `V⊥ * t`. +* **Receding too fast**: expect `nullopt`. +* **Gravity model**: verify arc solutions exist for short & long trajectories (if implemented). +* **Numerics**: convergence within max iterations; monotonic improvement of residuals. + +--- + +## Notes + +* This is an **interface** only; concrete engines (e.g., `SimpleNoGravityEngine`, `BallisticGravityEngine`) should document their assumptions (gravity, drag, wind, bounds) and units (meters, seconds). +* The coordinate system and handedness should be consistent with `Vector3` and the rest of your math stack. + +--- + +## See Also + +- [Projectile Documentation](projectile.md) - Projectile properties +- [Target Documentation](target.md) - Target state representation +- [Legacy Implementation](proj_pred_engine_legacy.md) - Standard projectile prediction engine +- [AVX2 Implementation](proj_pred_engine_avx2.md) - Optimized AVX2 engine +- [Tutorials - Projectile Prediction](../tutorials.md#tutorial-3-projectile-prediction-aim-bot) - Complete aim-bot tutorial + +--- + +*Last updated: 1 Nov 2025* diff --git a/docs/projectile_prediction/target.md b/docs/projectile_prediction/target.md new file mode 100644 index 00000000..45944635 --- /dev/null +++ b/docs/projectile_prediction/target.md @@ -0,0 +1,70 @@ +# `omath::projectile_prediction::Target` — Target state for aim solvers + +> Header: `omath/projectile_prediction/target.hpp` +> Namespace: `omath::projectile_prediction` +> Used by: `ProjPredEngineInterface` implementations (e.g., Legacy/AVX2 engines) + +A small POD-style container describing a target’s **current pose** and **motion** for projectile lead/aim computations. + +--- + +## API + +```cpp +namespace omath::projectile_prediction { + +class Target final { +public: + Vector3 m_origin; // Current world-space position of the target + Vector3 m_velocity; // World-space linear velocity (units/sec) + bool m_is_airborne{}; // Domain hint (e.g., ignore ground snapping) +}; + +} // namespace omath::projectile_prediction +``` + +--- + +## Field semantics + +* **`m_origin`** — target position in world coordinates (same units as your `Vector3` grid). +* **`m_velocity`** — instantaneous linear velocity. Solvers commonly assume **constant velocity** between “now” and impact unless your trait injects gravity/accel. +* **`m_is_airborne`** — optional hint for engine/trait logic (e.g., apply gravity to the target, skip ground friction/snap). Exact meaning is engine-dependent. + +> Keep units consistent with your projectile model (e.g., meters & seconds). If projectiles use `g = 9.81 m/s²`, velocity should be in m/s and positions in meters. + +--- + +## Typical usage + +```cpp +using namespace omath::projectile_prediction; + +Target tgt; +tgt.m_origin = { 42.0f, 1.8f, -7.5f }; +tgt.m_velocity = { 3.0f, 0.0f, 0.0f }; // moving +X at 3 units/s +tgt.m_is_airborne = false; + +// Feed into an aim solver with a Projectile +auto aim = engine->maybe_calculate_aim_point(projectile, tgt); +``` + +--- + +## Notes & tips + +* If you track acceleration (e.g., gravity on ragdolls), your **EngineTrait** may derive it from `m_is_airborne` and world gravity; otherwise most solvers treat the target’s motion as linear. +* For highly agile targets, refresh `m_origin`/`m_velocity` every tick and re-solve; don’t reuse stale aim points. +* Precision: `Vector3` is typically enough; if you need sub-millimeter accuracy over long ranges, consider double-precision internally in your trait. + +--- + +## See also + +* `Projectile` — shooter origin, muzzle speed, gravity scale +* `ProjPredEngineInterface` — common interface for aim solvers +* `ProjPredEngineLegacy`, `ProjPredEngineAvx2` — concrete solvers using this data + +--- + +*Last updated: 1 Nov 2025* diff --git a/docs/projection/camera.md b/docs/projection/camera.md new file mode 100644 index 00000000..42f7ebd1 --- /dev/null +++ b/docs/projection/camera.md @@ -0,0 +1,270 @@ +# `omath::projection::Camera` — Generic, trait-driven camera with screen/world conversion + +> Header: `omath/projection/camera.hpp` (this header) +> Namespace: `omath::projection` +> Template: `Camera` +> Requires: `CameraEngineConcept` +> Key features: **lazy view-projection caching**, world↔screen helpers, pluggable math via a **Trait** + +--- + +## Overview + +`Camera` is a small, zero-allocation camera wrapper. It delegates the math for **view**, **projection**, and **look-at** to a **Trait** (`TraitClass`), which lets you plug in different coordinate systems or conventions without changing the camera code. The class caches the **View×Projection** matrix and invalidates it when any parameter changes. + +Alongside the camera, the header defines: + +* `struct ViewPort { float m_width, m_height; float aspect_ratio() const; }` +* `using FieldOfView = Angle;` + +--- + +## Template & trait requirements + +```cpp +template +concept CameraEngineConcept = requires( + const omath::Vector3& cam_origin, + const omath::Vector3& look_at, + const ViewAnglesType& angles, + const omath::projection::FieldOfView& fov, + const omath::projection::ViewPort& viewport, + float znear, float zfar +) { + { T::calc_look_at_angle(cam_origin, look_at) } noexcept -> std::same_as; + { T::calc_view_matrix(angles, cam_origin) } noexcept -> std::same_as; + { T::calc_projection_matrix(fov, viewport, znear, zfar)}noexcept -> std::same_as; +}; +``` + +Your `Mat4X4Type` must behave like the library’s `Mat<4,4,...>` (supports `*`, `/`, `inverted()`, `.at(r,c)`, `.raw_array()`, and `static constexpr get_store_ordering()`). + +--- + +## Quick start + +```cpp +using Mat4 = omath::Mat<4,4,float, omath::MatStoreType::COLUMN_MAJOR>; + +// Example trait (sketch): assumes Y-up, column-major, left-handed +struct MyCamTrait { + static ViewAnglesType calc_look_at_angle(const Vector3& eye, + const Vector3& at) noexcept; + static Mat4 calc_view_matrix(const ViewAnglesType& ang, + const Vector3& eye) noexcept; + static Mat4 calc_projection_matrix(const FieldOfView& fov, + const ViewPort& vp, + float znear, float zfar) noexcept; +}; + +using Camera = omath::projection::Camera; + +omath::projection::ViewPort vp{1920, 1080}; +omath::projection::FieldOfView fov = omath::angles::degrees(70.f); + +Camera cam(/*position*/ {0,1.7f, -3}, + /*angles*/ MyViewAngles{/*...*/}, + /*viewport*/ vp, fov, + /*near*/ 0.1f, + /*far*/ 1000.f); + +// Project world → screen (origin top-left) +auto s = cam.world_to_screen({1, 1, 0}); +if (s) { + // s->x, s->y in pixels; s->z in NDC depth +} +``` + +--- + +## API + +```cpp +enum class ScreenStart { TOP_LEFT_CORNER, BOTTOM_LEFT_CORNER }; + +class Camera final { +public: + ~Camera() = default; + + Camera(const Vector3& position, + const ViewAnglesType& view_angles, + const ViewPort& view_port, + const FieldOfView& fov, + float near, float far) noexcept; + + void look_at(const Vector3& target); // recomputes view angles; invalidates cache + + // Lazily computed and cached: + const Mat4X4Type& get_view_projection_matrix() const noexcept; + + // Setters (all invalidate cached VP): + void set_field_of_view(const FieldOfView&) noexcept; + void set_near_plane(float) noexcept; + void set_far_plane(float) noexcept; + void set_view_angles(const ViewAnglesType&) noexcept; + void set_origin(const Vector3&) noexcept; + void set_view_port(const ViewPort&) noexcept; + + // Getters: + const FieldOfView& get_field_of_view() const noexcept; + const float& get_near_plane() const noexcept; + const float& get_far_plane() const noexcept; + const ViewAnglesType& get_view_angles() const noexcept; + const Vector3& get_origin() const noexcept; + + // World → Screen (pixels) via NDC; choose screen origin: + template + std::expected, Error> + world_to_screen(const Vector3& world) const noexcept; + + // World → NDC (aka “viewport” in this code) ∈ [-1,1]^3 + std::expected, Error> + world_to_view_port(const Vector3& world) const noexcept; + + // NDC → World (uses inverse VP) + std::expected, Error> + view_port_to_screen(const Vector3& ndc) const noexcept; + + // Screen (pixels) → World + std::expected, Error> + screen_to_world(const Vector3& screen) const noexcept; + + // 2D overload (z defaults to 1, i.e., far plane ray-end in NDC) + std::expected, Error> + screen_to_world(const Vector2& screen) const noexcept; + +protected: + ViewPort m_view_port{}; + FieldOfView m_field_of_view; + mutable std::optional m_view_projection_matrix; + float m_far_plane_distance{}; + float m_near_plane_distance{}; + ViewAnglesType m_view_angles; + Vector3 m_origin; + +private: + static constexpr bool is_ndc_out_of_bounds(const Mat4X4Type& ndc) noexcept; + Vector3 ndc_to_screen_position_from_top_left_corner(const Vector3& ndc) const noexcept; + Vector3 ndc_to_screen_position_from_bottom_left_corner(const Vector3& ndc) const noexcept; + Vector3 screen_to_ndc(const Vector3& screen) const noexcept; +}; +``` + +### Error handling + +All conversions return `std::expected<..., Error>` with errors from `error_codes.hpp`, notably: + +* `Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS` — clip space W=0 or NDC outside `[-1,1]`. +* `Error::INV_VIEW_PROJ_MAT_DET_EQ_ZERO` — non-invertible View×Projection matrix. + +--- + +## Coordinate spaces & conversions + +### World → NDC (`world_to_view_port`) + +1. Build (or reuse cached) `VP = P * V` (projection * view). +2. Multiply by homogeneous column from the world point. +3. Reject if `w == 0`. +4. Perspective divide → NDC in `[-1,1]^3`. +5. Reject if any component is out of range. + +Returns `{x_ndc, y_ndc, z_ndc}`. + +### NDC → Screen (pixels) + +The class offers two origins: + +* **Top-left (default)** + + ``` + x_px = (x_ndc + 1)/2 * width + y_px = ( -y_ndc/2 + 0.5) * height // flips Y + ``` +* **Bottom-left** + + ``` + x_px = (x_ndc + 1)/2 * width + y_px = ( y_ndc/2 + 0.5) * height + ``` + +### Screen (pixels) → NDC + +``` +x_ndc = screen_x / width * 2 - 1 +y_ndc = 1 - screen_y / height * 2 // Top-left screen origin assumed here +z_ndc = screen_z // Caller-provided (e.g., 0..1 depth) +``` + +### NDC → World (`view_port_to_screen`) + +Despite the method name, this function **unprojects** an NDC point back to world space: + +1. Compute `VP^{-1}`; if not invertible → error. +2. Multiply by NDC (homogeneous 4D) and divide by `w`. +3. Return world point. + +> Tip: to build a **world-space ray** from a screen pixel, unproject at `z=0` (near) and `z=1` (far). + +--- + +## Caching & invalidation + +* `get_view_projection_matrix()` computes `P*V` once and caches it. +* Any setter (`set_*`) or `look_at()` clears the cache (`m_view_projection_matrix = std::nullopt`). + +--- + +## Notes & gotchas + +* **Matrix order**: The camera multiplies `P * V`. Make sure your **Trait** matches this convention. +* **Store ordering**: The `Mat4X4Type::get_store_ordering()` is used when building homogeneous columns; ensure it’s consistent with your matrix implementation. +* **Naming quirk**: `view_port_to_screen()` returns a **world** point from **NDC** (it’s an unproject). Consider renaming to `ndc_to_world()` in your codebase for clarity. +* **FOV units**: `FieldOfView` uses the project’s `Angle` type; pass degrees via `angles::degrees(...)`. + +--- + +## Minimal trait sketch (column-major, left-handed) + +```cpp +struct LHCTrait { + static MyAngles calc_look_at_angle(const Vector3& eye, + const Vector3& at) noexcept { /* ... */ } + + static Mat4 calc_view_matrix(const MyAngles& ang, + const Vector3& eye) noexcept { + // Build from forward/right/up and translation + } + + static Mat4 calc_projection_matrix(const FieldOfView& fov, + const ViewPort& vp, + float zn, float zf) noexcept { + return omath::mat_perspective_left_handed( + fov.as_degrees(), vp.aspect_ratio(), zn, zf + ); + } +}; +``` + +--- + +## Testing checklist + +* World point centered in view projects to **screen center**. +* Points outside frustum → `WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS`. +* Inverting `VP` fails gracefully for singular matrices. +* `ScreenStart` switch flips Y as expected. +* Screen→World ray: unproject `(x,y,0)` and `(x,y,1)` and verify direction passes through the camera frustum. + +--- + +## See Also + +- [Engine-Specific Camera Traits](../engines/) - Camera implementations for different game engines +- [View Angles Documentation](../trigonometry/view_angles.md) - Understanding pitch/yaw/roll +- [Getting Started Guide](../getting_started.md) - Quick start with projection +- [Tutorials - World-to-Screen](../tutorials.md#tutorial-2-world-to-screen-projection) - Complete projection tutorial + +--- + +*Last updated: 1 Nov 2025* diff --git a/docs/projection/error_codes.md b/docs/projection/error_codes.md new file mode 100644 index 00000000..435751bb --- /dev/null +++ b/docs/projection/error_codes.md @@ -0,0 +1,79 @@ +# `omath::projection::Error` — Error codes for world/screen projection + +> Header: `omath/projection/error_codes.hpp` +> Namespace: `omath::projection` +> Type: `enum class Error : uint16_t` + +These error codes are returned by camera/projection helpers (e.g., `Camera::world_to_screen`, `Camera::screen_to_world`) wrapped in `std::expected<..., Error>`. Use them to distinguish **clipping/visibility** problems from **matrix/math** failures. + +--- + +## Enum values + +```cpp +enum class Error : uint16_t { + WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS, + INV_VIEW_PROJ_MAT_DET_EQ_ZERO, +}; +``` + +* **`WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS`** + The input point cannot produce a valid on-screen coordinate: + + * Clip-space `w == 0` (point at/infinite or behind camera plane), or + * After projection, any NDC component is outside `[-1, 1]`. + +* **`INV_VIEW_PROJ_MAT_DET_EQ_ZERO`** + The **View × Projection** matrix is not invertible (determinant ≈ 0). + Unprojection (`screen_to_world` / `view_port_to_screen`) requires an invertible matrix. + +--- + +## Typical usage + +```cpp +using omath::projection::Error; + +auto pix = cam.world_to_screen(point); +if (!pix) { + switch (pix.error()) { + case Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS: + // Cull label/marker; point is off-screen or behind camera. + break; + case Error::INV_VIEW_PROJ_MAT_DET_EQ_ZERO: + // Investigate camera/projection setup; near/far/FOV or trait bug. + break; + } +} + +// Unproject a screen pixel (top-left origin) at depth 1.0 +if (auto world = cam.screen_to_world({sx, sy, 1.0f})) { + // use *world +} else if (world.error() == Error::INV_VIEW_PROJ_MAT_DET_EQ_ZERO) { + // handle singular VP matrix +} +``` + +--- + +## When you might see these errors + +* **Out-of-bounds** + + * The world point lies outside the camera frustum. + * The point is behind the camera (clip `w <= 0`). + * Extremely large coordinates cause overflow and fail NDC bounds. + +* **Non-invertible VP** + + * Degenerate projection settings (e.g., `near == far`, zero FOV). + * Trait builds `P` or `V` incorrectly (wrong handedness/order). + * Numerical issues from nearly singular configurations. + +--- + +## Recommendations + +* Validate camera setup: `near > 0`, `far > near`, sensible FOV (e.g., 30°–120°). +* For UI markers: treat `WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS` as a simple **cull** signal. +* Log `INV_VIEW_PROJ_MAT_DET_EQ_ZERO` — it usually indicates a configuration or math bug worth fixing rather than hiding. diff --git a/docs/rev_eng/external_rev_object.md b/docs/rev_eng/external_rev_object.md new file mode 100644 index 00000000..3a983fe1 --- /dev/null +++ b/docs/rev_eng/external_rev_object.md @@ -0,0 +1,164 @@ +# `omath::rev_eng::ExternalReverseEngineeredObject` — typed offsets over external memory + +> Header: `omath/rev_eng/external_reverse_engineered_object.hpp` +> Namespace: `omath::rev_eng` +> Pattern: **CRTP-style wrapper** around a user-provided *ExternalMemoryManagementTrait* that actually reads/writes another process or device’s memory. + +A tiny base class for reverse-engineered objects that live **outside** your address space. You pass an absolute base address and a trait with `read_memory` / `write_memory`. Your derived types then expose strongly-typed getters/setters that delegate into the trait using **byte offsets**. + +--- + +## Quick look + +```cpp +template +class ExternalReverseEngineeredObject { +public: + explicit ExternalReverseEngineeredObject(std::uintptr_t addr) + : m_object_address(addr) {} + +protected: + template + [[nodiscard]] Type get_by_offset(std::ptrdiff_t offset) const { + return ExternalMemoryManagementTrait::read_memory(m_object_address + offset); + } + + template + void set_by_offset(std::ptrdiff_t offset, const Type& value) const { + ExternalMemoryManagementTrait::write_memory(m_object_address + offset, value); + } + +private: + std::uintptr_t m_object_address{}; +}; +``` + +--- + +## Trait requirements + +Your `ExternalMemoryManagementTrait` must provide: + +```cpp +// Reads sizeof(T) bytes starting at absolute address and returns T. +template +static T read_memory(std::uintptr_t absolute_address); + +// Writes sizeof(T) bytes to absolute address. +template +static void write_memory(std::uintptr_t absolute_address, const T& value); +``` + +> Tip: If your implementation prefers returning `bool`/`expected<>` for writes, either: +> +> * make `write_memory` `void` and throw/log internally, or +> * adjust `set_by_offset` in your fork to surface the status. + +### Common implementations + +* **Windows**: wrap `ReadProcessMemory` / `WriteProcessMemory` with a stored `HANDLE` (often captured via a singleton or embedded in the trait). +* **Linux**: `/proc//mem`, `process_vm_readv/writev`, `ptrace`. +* **Device/FPGA**: custom MMIO/driver APIs. + +--- + +## How to use (derive and map fields) + +Create a concrete type for your target structure and map known offsets: + +```cpp +struct WinRPMTrait { + template + static T read_memory(std::uintptr_t addr) { + T out{}; + SIZE_T n{}; + if (!ReadProcessMemory(g_handle, reinterpret_cast(addr), &out, sizeof(T), &n) || n != sizeof(T)) + throw std::runtime_error("RPM failed"); + return out; + } + template + static void write_memory(std::uintptr_t addr, const T& val) { + SIZE_T n{}; + if (!WriteProcessMemory(g_handle, reinterpret_cast(addr), &val, sizeof(T), &n) || n != sizeof(T)) + throw std::runtime_error("WPM failed"); + } +}; + +class Player final : public omath::rev_eng::ExternalReverseEngineeredObject { + using Base = omath::rev_eng::ExternalReverseEngineeredObject; +public: + using Base::Base; // inherit ctor (takes base address) + + // Offsets taken from your RE notes (in bytes) + Vector3 position() const { return get_by_offset>(0x30); } + void set_position(const Vector3& p) const { set_by_offset(0x30, p); } + + float health() const { return get_by_offset(0x100); } + void set_health(float h) const { set_by_offset(0x100, h); } +}; +``` + +Then: + +```cpp +Player p{ /* base address you discovered */ 0x7FF6'1234'0000ull }; +auto pos = p.position(); +p.set_health(100.f); +``` + +--- + +## Design notes & constraints + +* **Offsets are byte offsets** from the object’s **base address** passed to the constructor. +* **Type safety is on you**: `Type` must match the external layout at that offset (endian, packing, alignment). +* **No lifetime tracking**: if the target object relocates/frees, you must update/recreate the wrapper with the new base address. +* **Thread safety**: the class itself is stateless; thread safety depends on your trait implementation. +* **Endianness**: assumes the host and target endianness agree, or your trait handles conversion. +* **Error handling**: this header doesn’t prescribe it; adopt exceptions/expected/logging inside the trait. + +--- + +## Best practices + +* Centralize offsets in one place (constexprs or a small struct) and **comment source/version** (e.g., *game v1.2.3*). +* Wrap fragile multi-field writes in a trait-level **transaction** if your platform supports it. +* Validate pointers/guards (e.g., vtable signature, canary) before trusting offsets. +* Prefer **plain old data** (`struct` without virtuals) for `Type` to ensure trivial byte copies. + +--- + +## Minimal trait sketch (POSIX, `process_vm_readv`) + +```cpp +struct LinuxPvmTrait { + static pid_t pid; + + template static T read_memory(std::uintptr_t addr) { + T out{}; + iovec local{ &out, sizeof(out) }, remote{ reinterpret_cast(addr), sizeof(out) }; + if (process_vm_readv(pid, &local, 1, &remote, 1, 0) != ssize_t(sizeof(out))) + throw std::runtime_error("pvm_readv failed"); + return out; + } + + template static void write_memory(std::uintptr_t addr, const T& val) { + iovec local{ const_cast(&val), sizeof(val) }, remote{ reinterpret_cast(addr), sizeof(val) }; + if (process_vm_writev(pid, &local, 1, &remote, 1, 0) != ssize_t(sizeof(val))) + throw std::runtime_error("pvm_writev failed"); + } +}; +``` + +--- + +## Troubleshooting + +* **Garbled values** → wrong offset/Type, or target’s structure changed between versions. +* **Access denied** → missing privileges (admin/root), wrong process handle, or page protections. +* **Crashes in trait** → add bounds/sanity checks; many APIs fail on unmapped pages. +* **Writes “stick” only briefly** → the target may constantly overwrite (server authority / anti-cheat / replication). + +--- + +*Last updated: 1 Nov 2025* diff --git a/docs/rev_eng/internal_rev_object.md b/docs/rev_eng/internal_rev_object.md new file mode 100644 index 00000000..3fce7be7 --- /dev/null +++ b/docs/rev_eng/internal_rev_object.md @@ -0,0 +1,142 @@ +# `omath::rev_eng::InternalReverseEngineeredObject` — raw in-process offset/VTABLE access + +> Header: `omath/rev_eng/internal_reverse_engineered_object.hpp` +> Namespace: `omath::rev_eng` +> Purpose: Convenience base for **internal** (same-process) RE wrappers that: +> +> * read/write fields by **byte offset** from `this` +> * call **virtual methods** by **vtable index** + +--- + +## At a glance + +```cpp +class InternalReverseEngineeredObject { +protected: + template + [[nodiscard]] Type& get_by_offset(std::ptrdiff_t offset); + + template + [[nodiscard]] const Type& get_by_offset(std::ptrdiff_t offset) const; + + template + ReturnType call_virtual_method(auto... arg_list); +}; +``` + +* `get_by_offset(off)` — returns a **reference** to `T` located at `reinterpret_cast(this) + off`. +* `call_virtual_method(args...)` — fetches the function pointer from `(*reinterpret_cast(this))[id]` and invokes it as a free function with implicit `this` passed explicitly. + +On MSVC builds the function pointer type uses `__thiscall`; on non-MSVC it uses a plain function pointer taking `void*` as the first parameter (the typical Itanium ABI shape). + +--- + +## Example: wrapping a reverse-engineered class + +```cpp +struct Player : omath::rev_eng::InternalReverseEngineeredObject { + // Field offsets (document game/app version!) + static constexpr std::ptrdiff_t kHealth = 0x100; + static constexpr std::ptrdiff_t kPosition = 0x30; + + // Accessors + float& health() { return get_by_offset(kHealth); } + const float& health() const { return get_by_offset(kHealth); } + + Vector3& position() { return get_by_offset>(kPosition); } + const Vector3& position() const { return get_by_offset>(kPosition); } + + // Virtuals (vtable indices discovered via RE) + int getTeam() { return call_virtual_method<27, int>(); } + void setArmor(float val) { call_virtual_method<42, void>(val); } // signature must match! +}; +``` + +Usage: + +```cpp +auto* p = /* pointer to live Player instance within the same process */; +p->health() = 100.f; +int team = p->getTeam(); +``` + +--- + +## How `call_virtual_method` resolves the signature + +```cpp +template +ReturnType call_virtual_method(auto... arg_list) { +#ifdef _MSC_VER + using Fn = ReturnType(__thiscall*)(void*, decltype(arg_list)...); +#else + using Fn = ReturnType(*)(void*, decltype(arg_list)...); +#endif + return (*reinterpret_cast(this))[id](this, arg_list...); +} +``` + +* The **first parameter** is always `this` (`void*`). +* Remaining parameter types are deduced from the **actual arguments** (`decltype(arg_list)...`). + Ensure you pass arguments with the correct types (e.g., `int32_t` vs `int`, pointer/ref qualifiers), or define thin wrappers that cast to the exact signature you recovered. + +> ⚠ On 32-bit MSVC the `__thiscall` distinction matters; on 64-bit MSVC it’s ignored (all member funcs use the common x64 calling convention). + +--- + +## Safety notes (read before using!) + +Working at this level is inherently unsafe; be deliberate: + +1. **Correct offsets & alignment** + + * `get_by_offset` assumes `this + offset` is **properly aligned** for `T` and points to an object of type `T`. + * Wrong offsets or misalignment ⇒ **undefined behavior** (UB), crashes, silent corruption. + +2. **Object layout assumptions** + + * The vtable pointer is assumed to be at the **start of the most-derived subobject at `this`**. + * With **multiple/virtual inheritance**, the desired subobject’s vptr may be at a non-zero offset. If so, adjust `this` to that subobject before calling, e.g.: + + ```cpp + auto* sub = reinterpret_cast(reinterpret_cast(this) + kSubobjectOffset); + // … then reinterpret sub instead of this inside a custom helper + ``` + +3. **ABI & calling convention** + + * Indices and signatures are **compiler/ABI-specific**. Recheck after updates or different builds (MSVC vs Clang/LLVM-MSVC vs MinGW). + +4. **Strict aliasing** + + * Reinterpreting memory as unrelated `T` can violate aliasing rules. Prefer **trivially copyable** PODs and exact original types where possible. + +5. **Const-correctness** + + * The `const` overload returns `const T&` but still reinterprets memory; do not write through it. Use the non-const overload to mutate. + +6. **Thread safety** + + * No synchronization is provided. Ensure the underlying object isn’t concurrently mutated in incompatible ways. + +--- + +## Tips & patterns + +* **Centralize offsets** in `constexpr` with comments (`// game v1.2.3, sig XYZ`). +* **Guard reads**: if you have a canary or RTTI/vtable hash, check it before relying on offsets. +* **Prefer accessors** returning references**:** lets you both read and write with natural syntax. +* **Wrap tricky virtuals**: if a method takes complex/reference params, wrap `call_virtual_method` in a strongly typed member that casts exactly as needed. + +--- + +## Troubleshooting + +* **Crash on virtual call** → wrong index or wrong `this` (subobject), or mismatched signature (args/ret or calling conv). +* **Weird field values** → wrong offset, wrong type size/packing, stale layout after an update. +* **Only in 32-bit** → double-check `__thiscall` and parameter passing (register vs stack). + +--- + +*Last updated: 1 Nov 2025* diff --git a/docs/styles/center.css b/docs/styles/center.css new file mode 100644 index 00000000..c1b0e2a3 --- /dev/null +++ b/docs/styles/center.css @@ -0,0 +1,4 @@ +/* docs/css/custom.css */ +.center-text { + text-align: center; +} \ No newline at end of file diff --git a/docs/styles/fonts.css b/docs/styles/fonts.css new file mode 100644 index 00000000..a180d715 --- /dev/null +++ b/docs/styles/fonts.css @@ -0,0 +1,172 @@ +/* cyrillic-ext */ +@font-face { + font-family: 'Roboto Condensed'; + font-style: normal; + font-weight: 300; + src: url(https://fonts.gstatic.com/s/robotocondensed/v31/ieVl2ZhZI2eCN5jzbjEETS9weq8-19-7DRs5.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Roboto Condensed'; + font-style: normal; + font-weight: 300; + src: url(https://fonts.gstatic.com/s/robotocondensed/v31/ieVl2ZhZI2eCN5jzbjEETS9weq8-19a7DRs5.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Roboto Condensed'; + font-style: normal; + font-weight: 300; + src: url(https://fonts.gstatic.com/s/robotocondensed/v31/ieVl2ZhZI2eCN5jzbjEETS9weq8-1967DRs5.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Roboto Condensed'; + font-style: normal; + font-weight: 300; + src: url(https://fonts.gstatic.com/s/robotocondensed/v31/ieVl2ZhZI2eCN5jzbjEETS9weq8-19G7DRs5.woff2) format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'Roboto Condensed'; + font-style: normal; + font-weight: 300; + src: url(https://fonts.gstatic.com/s/robotocondensed/v31/ieVl2ZhZI2eCN5jzbjEETS9weq8-1927DRs5.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Roboto Condensed'; + font-style: normal; + font-weight: 300; + src: url(https://fonts.gstatic.com/s/robotocondensed/v31/ieVl2ZhZI2eCN5jzbjEETS9weq8-19y7DRs5.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Roboto Condensed'; + font-style: normal; + font-weight: 300; + src: url(https://fonts.gstatic.com/s/robotocondensed/v31/ieVl2ZhZI2eCN5jzbjEETS9weq8-19K7DQ.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Roboto Condensed'; + font-style: normal; + font-weight: 400; + src: url(https://fonts.gstatic.com/s/robotocondensed/v31/ieVl2ZhZI2eCN5jzbjEETS9weq8-19-7DRs5.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Roboto Condensed'; + font-style: normal; + font-weight: 400; + src: url(https://fonts.gstatic.com/s/robotocondensed/v31/ieVl2ZhZI2eCN5jzbjEETS9weq8-19a7DRs5.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Roboto Condensed'; + font-style: normal; + font-weight: 400; + src: url(https://fonts.gstatic.com/s/robotocondensed/v31/ieVl2ZhZI2eCN5jzbjEETS9weq8-1967DRs5.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Roboto Condensed'; + font-style: normal; + font-weight: 400; + src: url(https://fonts.gstatic.com/s/robotocondensed/v31/ieVl2ZhZI2eCN5jzbjEETS9weq8-19G7DRs5.woff2) format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'Roboto Condensed'; + font-style: normal; + font-weight: 400; + src: url(https://fonts.gstatic.com/s/robotocondensed/v31/ieVl2ZhZI2eCN5jzbjEETS9weq8-1927DRs5.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Roboto Condensed'; + font-style: normal; + font-weight: 400; + src: url(https://fonts.gstatic.com/s/robotocondensed/v31/ieVl2ZhZI2eCN5jzbjEETS9weq8-19y7DRs5.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Roboto Condensed'; + font-style: normal; + font-weight: 400; + src: url(https://fonts.gstatic.com/s/robotocondensed/v31/ieVl2ZhZI2eCN5jzbjEETS9weq8-19K7DQ.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Roboto Condensed'; + font-style: normal; + font-weight: 700; + src: url(https://fonts.gstatic.com/s/robotocondensed/v31/ieVl2ZhZI2eCN5jzbjEETS9weq8-19-7DRs5.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Roboto Condensed'; + font-style: normal; + font-weight: 700; + src: url(https://fonts.gstatic.com/s/robotocondensed/v31/ieVl2ZhZI2eCN5jzbjEETS9weq8-19a7DRs5.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Roboto Condensed'; + font-style: normal; + font-weight: 700; + src: url(https://fonts.gstatic.com/s/robotocondensed/v31/ieVl2ZhZI2eCN5jzbjEETS9weq8-1967DRs5.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Roboto Condensed'; + font-style: normal; + font-weight: 700; + src: url(https://fonts.gstatic.com/s/robotocondensed/v31/ieVl2ZhZI2eCN5jzbjEETS9weq8-19G7DRs5.woff2) format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'Roboto Condensed'; + font-style: normal; + font-weight: 700; + src: url(https://fonts.gstatic.com/s/robotocondensed/v31/ieVl2ZhZI2eCN5jzbjEETS9weq8-1927DRs5.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Roboto Condensed'; + font-style: normal; + font-weight: 700; + src: url(https://fonts.gstatic.com/s/robotocondensed/v31/ieVl2ZhZI2eCN5jzbjEETS9weq8-19y7DRs5.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Roboto Condensed'; + font-style: normal; + font-weight: 700; + src: url(https://fonts.gstatic.com/s/robotocondensed/v31/ieVl2ZhZI2eCN5jzbjEETS9weq8-19K7DQ.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +body { + font-family: 'Roboto Condensed', sans-serif; +} diff --git a/docs/styles/liquid-glass.css b/docs/styles/liquid-glass.css new file mode 100644 index 00000000..56453097 --- /dev/null +++ b/docs/styles/liquid-glass.css @@ -0,0 +1,271 @@ +/* ============================================================ + Apple Liquid Glass Design — glassmorphism overrides + for Material for MkDocs (slate / dark mode) + ============================================================ */ + +/* ---------- shared glass mixin values ---------- */ +:root { + --glass-bg: rgba(255, 255, 255, 0.04); + --glass-bg-hover: rgba(255, 255, 255, 0.07); + --glass-border: rgba(255, 255, 255, 0.08); + --glass-border-strong: rgba(255, 255, 255, 0.12); + --glass-blur: 16px; + --glass-radius: 14px; + --glass-shadow: 0 4px 24px rgba(0, 0, 0, 0.25); + --glass-shadow-sm: 0 2px 12px rgba(0, 0, 0, 0.18); + --glass-accent: rgba(255, 152, 0, 0.12); +} + +/* ---------- header / top-bar ---------- */ +.md-header { + background: rgba(30, 30, 30, 0.55) !important; + -webkit-backdrop-filter: saturate(180%) blur(var(--glass-blur)); + backdrop-filter: saturate(180%) blur(var(--glass-blur)); + border-bottom: 1px solid var(--glass-border); + box-shadow: var(--glass-shadow-sm); +} + +/* ---------- navigation sidebar ---------- */ +.md-sidebar { + background: transparent !important; +} + +.md-sidebar__scrollwrap { + background: var(--glass-bg); + -webkit-backdrop-filter: saturate(160%) blur(var(--glass-blur)); + backdrop-filter: saturate(160%) blur(var(--glass-blur)); + border-right: 1px solid var(--glass-border); + border-radius: 0 var(--glass-radius) var(--glass-radius) 0; +} + +/* Remove the gradient mask at the top of the table of contents */ +.md-sidebar__scrollwrap { + mask-image: none !important; + -webkit-mask-image: none !important; +} + +/* active nav item — subtle glass highlight */ +.md-nav__item--active > .md-nav__link { + background: var(--glass-accent) !important; + border-radius: 8px; +} + +/* ---------- content area ---------- */ +.md-main__inner { + background: transparent; +} + +.md-content { + background: var(--glass-bg); + -webkit-backdrop-filter: saturate(140%) blur(12px); + backdrop-filter: saturate(140%) blur(12px); + border: 1px solid var(--glass-border); + border-radius: var(--glass-radius); + box-shadow: var(--glass-shadow); + margin: 12px 0; + padding: 4px; +} + +/* ---------- code blocks ---------- */ +.highlight { + background: rgba(0, 0, 0, 0.25) !important; + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + border: 1px solid var(--glass-border-strong); + border-radius: 12px !important; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04), + var(--glass-shadow-sm); + overflow: hidden; +} + +.highlight code { + background: transparent !important; +} + +/* inline code */ +:not(pre) > code { + background: rgba(255, 255, 255, 0.06) !important; + border: 1px solid var(--glass-border); + border-radius: 6px; + padding: 2px 6px; +} + +/* ---------- tables ---------- */ +.md-typeset table:not([class]) { + background: var(--glass-bg); + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + border: 1px solid var(--glass-border); + border-radius: 12px; + overflow: hidden; + box-shadow: var(--glass-shadow-sm); +} + +.md-typeset table:not([class]) th { + background: rgba(255, 152, 0, 0.08); +} + +/* ---------- admonitions / call-outs ---------- */ +.md-typeset .admonition, +.md-typeset details { + background: var(--glass-bg) !important; + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + border: 1px solid var(--glass-border-strong) !important; + border-radius: 12px !important; + box-shadow: var(--glass-shadow-sm); +} + +/* ---------- tabs ---------- */ +.md-typeset .tabbed-set { + background: var(--glass-bg); + border: 1px solid var(--glass-border); + border-radius: 12px; + overflow: hidden; +} + +/* ---------- search bar ---------- */ +.md-search__form { + background: rgba(255, 255, 255, 0.06) !important; + -webkit-backdrop-filter: blur(12px); + backdrop-filter: blur(12px); + border: 1px solid var(--glass-border); + border-radius: 10px !important; +} + +/* search results / output — liquid glass */ +.md-search__output { + background: rgba(30, 30, 30, 0.70) !important; + -webkit-backdrop-filter: saturate(160%) blur(var(--glass-blur)); + backdrop-filter: saturate(160%) blur(var(--glass-blur)); + border: 1px solid var(--glass-border); + border-radius: 0 0 var(--glass-radius) var(--glass-radius); + box-shadow: var(--glass-shadow); +} + +.md-search-result__link { + border-radius: 8px; + transition: background 0.2s ease; +} + +.md-search-result__link:hover { + background: var(--glass-bg-hover) !important; +} + +/* ---------- footer ---------- */ +.md-footer { + background: rgba(30, 30, 30, 0.50) !important; + -webkit-backdrop-filter: saturate(180%) blur(var(--glass-blur)); + backdrop-filter: saturate(180%) blur(var(--glass-blur)); + border-top: 1px solid var(--glass-border); +} + +/* ---------- horizontal rules — subtle glow ---------- */ +.md-typeset hr { + border-image: linear-gradient( + to right, + transparent, + rgba(255, 152, 0, 0.25), + transparent + ) 1; +} + +/* ---------- scrollbar ---------- */ +* { + scrollbar-width: thin; + scrollbar-color: rgba(255, 255, 255, 0.10) transparent; +} + +::-webkit-scrollbar { + width: 6px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.10); + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.18); +} + +/* ---------- links — subtle glass-glow on hover ---------- */ +.md-typeset a:hover { + text-shadow: 0 0 8px rgba(255, 152, 0, 0.3); +} + +/* ---------- images — soft glass frame ---------- */ +.md-typeset img { + border-radius: 10px; +} + +/* ============================================================ + Dynamic Liquid Glass — mouse-tracking specular highlight + ============================================================ */ + +/* Shared: every glass surface gets a hidden radial-light overlay + that becomes visible when JS adds the .glass-active class and + sets --glass-x / --glass-y custom properties. */ +.md-header, +.md-content, +.md-sidebar__scrollwrap, +.highlight, +.md-search__form, +.md-footer { + position: relative; + overflow: hidden; +} + +.md-header::after, +.md-content::after, +.md-sidebar__scrollwrap::after, +.highlight::after, +.md-search__form::after, +.md-footer::after { + content: ""; + position: absolute; + inset: 0; + pointer-events: none; + opacity: 0; + transition: opacity 0.3s ease; + background: radial-gradient( + circle 220px at var(--glass-x, 50%) var(--glass-y, 50%), + rgba(255, 200, 120, 0.10) 0%, + rgba(255, 152, 0, 0.04) 40%, + transparent 70% + ); + z-index: 1; +} + +.md-header.glass-active::after, +.md-content.glass-active::after, +.md-sidebar__scrollwrap.glass-active::after, +.highlight.glass-active::after, +.md-search__form.glass-active::after, +.md-footer.glass-active::after { + opacity: 1; +} + +/* Keep header text / nav icons above the overlay */ +.md-header > *, +.md-content > *, +.md-sidebar__scrollwrap > *, +.md-search__form > *, +.md-footer > * { + position: relative; + z-index: 2; +} + +/* Highlight code blocks get a slightly brighter spot */ +.highlight.glass-active::after { + background: radial-gradient( + circle 180px at var(--glass-x, 50%) var(--glass-y, 50%), + rgba(255, 200, 120, 0.12) 0%, + rgba(255, 152, 0, 0.05) 35%, + transparent 65% + ); +} diff --git a/docs/trigonometry/angle.md b/docs/trigonometry/angle.md new file mode 100644 index 00000000..b4b46701 --- /dev/null +++ b/docs/trigonometry/angle.md @@ -0,0 +1,165 @@ +# `omath::Angle` — templated angle with normalize/clamper + trig + +> Header: `omath/trigonometry/angle.hpp` +> Namespace: `omath` +> Template: `Angle` +> Requires: `std::is_arithmetic_v` +> Formatters: `std::formatter` for `char`, `wchar_t`, `char8_t` → `"{}deg"` + +--- + +## Overview + +`Angle` is a tiny value-type that stores an angle in **degrees** and automatically **normalizes** or **clamps** it into a compile-time range. It exposes conversions to/from radians, common trig (`sin/cos/tan/cot`), arithmetic with wrap/clamp semantics, and lightweight formatting. + +Two behaviors via `AngleFlags`: + +* `AngleFlags::Normalized` (default): values are wrapped into `[min, max]` using `angles::wrap_angle`. +* `AngleFlags::Clamped`: values are clamped to `[min, max]` using `std::clamp`. + +--- + +## API + +```cpp +namespace omath { + +enum class AngleFlags { Normalized = 0, Clamped = 1 }; + +template +requires std::is_arithmetic_v +class Angle { +public: + // Construction + static constexpr Angle from_degrees(const Type& deg) noexcept; + static constexpr Angle from_radians(const Type& rad) noexcept; + constexpr Angle() noexcept; // 0 deg, adjusted by flags/range + + // Accessors / conversions (degrees stored internally) + constexpr const Type& operator*() const noexcept; // raw degrees reference + constexpr Type as_degrees() const noexcept; + constexpr Type as_radians() const noexcept; + + // Trig (computed from radians) + Type sin() const noexcept; + Type cos() const noexcept; + Type tan() const noexcept; + Type atan() const noexcept; // atan(as_radians()) (rarely used) + Type cot() const noexcept; // cos()/sin() (watch sin≈0) + + // Arithmetic (wraps or clamps per flags and [min,max]) + constexpr Angle& operator+=(const Angle&) noexcept; + constexpr Angle& operator-=(const Angle&) noexcept; + constexpr Angle operator+(const Angle&) noexcept; + constexpr Angle operator-(const Angle&) noexcept; + constexpr Angle operator-() const noexcept; + + // Comparison (partial ordering) + constexpr std::partial_ordering operator<=>(const Angle&) const noexcept = default; +}; + +} // namespace omath +``` + +### Formatting + +```cpp +std::format("{}", Angle::from_degrees(45)); // "45deg" +``` + +Formatters exist for `char`, `wchar_t`, and `char8_t`. + +--- + +## Usage examples + +### Defaults (0–360, normalized) + +```cpp +using Deg = omath::Angle<>; // float, [0,360], Normalized + +auto a = Deg::from_degrees(370); // -> 10deg +auto b = Deg::from_radians(omath::angles::pi); // -> 180deg + +a += Deg::from_degrees(355); // 10 + 355 -> 365 -> wraps -> 5deg + +float s = a.sin(); // sin(5°) +``` + +### Clamped range + +```cpp +using Fov = omath::Angle; +auto fov = Fov::from_degrees(200.f); // -> 179deg (clamped) +``` + +### Signed, normalized range + +```cpp +using SignedDeg = omath::Angle; + +auto x = SignedDeg::from_degrees(190.f); // -> -170deg +auto y = SignedDeg::from_degrees(-200.f); // -> 160deg +auto z = x + y; // -170 + 160 = -10deg (wrapped if needed) +``` + +### Get/set raw degrees + +```cpp +auto yaw = SignedDeg::from_degrees(-45.f); +float deg = *yaw; // same as yaw.as_degrees() +``` + +--- + +## Semantics & notes + +* **Storage & units:** Internally stores **degrees** (`Type m_angle`). `as_radians()`/`from_radians()` use the project helpers in `omath::angles`. +* **Arithmetic honors policy:** `operator+=`/`-=` and the binary `+`/`-` apply **wrap** or **clamp** in `[min,max]`, mirroring construction behavior. +* **`atan()`**: returns `std::atan(as_radians())` (the arctangent of the *radian value*). This is mathematically unusual for an angle type and is rarely useful; prefer `tan()`/`atan2` in client code when solving geometry problems. +* **`cot()` / `tan()` singularities:** Near multiples where `sin() ≈ 0` or `cos() ≈ 0`, results blow up. Guard in your usage if inputs can approach these points. +* **Comparison:** `operator<=>` is defaulted. With normalization, distinct representatives can compare as expected (e.g., `-180` vs `180` in signed ranges are distinct endpoints). +* **No implicit numeric conversion:** There’s **no `operator Type()`**. Use `as_degrees()`/`as_radians()` (or `*angle`) explicitly—this intentional friction avoids unit mistakes. + +--- + +## Customization patterns + +* **Radians workflow:** Keep angles in degrees internally but wrap helper creators: + + ```cpp + inline Deg degf(float d) { return Deg::from_degrees(d); } + inline Deg radf(float r) { return Deg::from_radians(r); } + ``` +* **Compile-time policy:** Pick ranges/flags at the type level to enforce invariants (e.g., `YawDeg = Angle`; `FovDeg = Angle`). + +--- + +## Pitfalls & gotchas + +* Ensure `min < max` at compile time for meaningful wrap/clamp behavior. +* For normalized signed ranges, decide whether your `wrap_angle(min,max)` treats endpoints half-open (e.g., `[-180,180)`) to avoid duplicate representations; the formatter will print the stored value verbatim. +* If you need **sum of many angles**, accumulating in radians then converting back can improve numeric stability at extreme values. + +--- + +## Minimal tests + +```cpp +using A = omath::Angle<>; +REQUIRE(A::from_degrees(360).as_degrees() == 0.f); +REQUIRE(A::from_degrees(-1).as_degrees() == 359.f); + +using S = omath::Angle; +REQUIRE(S::from_degrees( 181).as_degrees() == -179.f); +REQUIRE(S::from_degrees(-181).as_degrees() == 179.f); + +using C = omath::Angle; +REQUIRE(C::from_degrees(5).as_degrees() == 10.f); +REQUIRE(C::from_degrees(25).as_degrees() == 20.f); +``` + +--- + +*Last updated: 1 Nov 2025* diff --git a/docs/trigonometry/angles.md b/docs/trigonometry/angles.md new file mode 100644 index 00000000..afef6cbe --- /dev/null +++ b/docs/trigonometry/angles.md @@ -0,0 +1,107 @@ +# `omath::angles` — angle conversions, FOV helpers, and wrapping + +> Header: `omath/trigonometry/angles.hpp` +> Namespace: `omath::angles` +> All functions are `[[nodiscard]]` and `noexcept` where applicable. + +A small set of constexpr-friendly utilities for converting between degrees/radians, converting horizontal/vertical field of view, and wrapping angles into a closed interval. + +--- + +## API + +```cpp +// Degrees ↔ Radians (Type must be floating-point) +template +requires std::is_floating_point_v +constexpr Type radians_to_degrees(const Type& radians) noexcept; + +template +requires std::is_floating_point_v +constexpr Type degrees_to_radians(const Type& degrees) noexcept; + +// FOV conversion (inputs/outputs in degrees, aspect = width/height) +template +requires std::is_floating_point_v +Type horizontal_fov_to_vertical(const Type& horizontal_fov, const Type& aspect) noexcept; + +template +requires std::is_floating_point_v +Type vertical_fov_to_horizontal(const Type& vertical_fov, const Type& aspect) noexcept; + +// Wrap angle into [min, max] (any arithmetic type) +template +requires std::is_arithmetic_v +Type wrap_angle(const Type& angle, const Type& min, const Type& max) noexcept; +``` + +--- + +## Usage + +### Degrees ↔ Radians + +```cpp +float rad = omath::angles::degrees_to_radians(180.0f); // π +double deg = omath::angles::radians_to_degrees(std::numbers::pi); // 180 +``` + +### Horizontal ↔ Vertical FOV + +* `aspect` = **width / height**. +* Inputs/outputs are **degrees**. + +```cpp +float hdeg = 90.0f; +float aspect = 16.0f / 9.0f; + +float vdeg = omath::angles::horizontal_fov_to_vertical(hdeg, aspect); // ~58.0° +float hdeg2 = omath::angles::vertical_fov_to_horizontal(vdeg, aspect); // ≈ 90.0° +``` + +Formulas (in radians): + +* `v = 2 * atan( tan(h/2) / aspect )` +* `h = 2 * atan( tan(v/2) * aspect )` + +### Wrapping angles (or any periodic value) + +Wrap any numeric `angle` into `[min, max]`: + +```cpp +// Wrap degrees into [0, 360] +float a = omath::angles::wrap_angle( 370.0f, 0.0f, 360.0f); // 10 +float b = omath::angles::wrap_angle( -15.0f, 0.0f, 360.0f); // 345 +// Signed range [-180,180] +float c = omath::angles::wrap_angle( 200.0f, -180.0f, 180.0f); // -160 +``` + +--- + +## Notes & edge cases + +* **Type requirements** + + * Converters & FOV helpers require **floating-point** `Type`. + * `wrap_angle` accepts any arithmetic `Type` (floats or integers). +* **Aspect ratio** must be **positive** and finite. For `aspect == 0` the FOV helpers are undefined. +* **Units**: FOV functions accept/return **degrees** but compute internally in radians. +* **Wrapping interval**: Behavior assumes `max > min`. The result lies in the **closed interval** `[min, max]` with modulo arithmetic; if you need half-open behavior (e.g., `[min,max)`), adjust your range or post-process endpoint cases. +* **constexpr**: Converters are `constexpr`; FOV helpers are runtime constexpr-compatible except for `std::atan/std::tan` constraints on some standard libraries. + +--- + +## Quick tests + +```cpp +using namespace omath::angles; + +static_assert(degrees_to_radians(180.0) == std::numbers::pi); +static_assert(radians_to_degrees(std::numbers::pi_v) == 180.0f); + +float v = horizontal_fov_to_vertical(90.0f, 16.0f/9.0f); +float h = vertical_fov_to_horizontal(v, 16.0f/9.0f); +assert(std::abs(h - 90.0f) < 1e-5f); + +assert(wrap_angle(360.0f, 0.0f, 360.0f) == 0.0f || wrap_angle(360.0f, 0.0f, 360.0f) == 360.0f); +``` diff --git a/docs/trigonometry/view_angles.md b/docs/trigonometry/view_angles.md new file mode 100644 index 00000000..5f6b12b9 --- /dev/null +++ b/docs/trigonometry/view_angles.md @@ -0,0 +1,87 @@ +# `omath::ViewAngles` — tiny POD for pitch/yaw/roll + +> Header: your project’s `view_angles.hpp` +> Namespace: `omath` +> Kind: **aggregate struct** (POD), no methods, no allocation + +A minimal container for Euler angles. You choose the types for each component (e.g., raw `float` or the strong `omath::Angle<>` type), and plug it into systems like `projection::Camera`. + +--- + +## API + +```cpp +namespace omath { + template + struct ViewAngles { + PitchType pitch; + YawType yaw; + RollType roll; + }; +} +``` + +* Aggregate: supports brace-init, aggregate copying, and `constexpr` usage when the component types do. +* Semantics (units/handedness/ranges) are **entirely defined by your chosen types**. + +--- + +## Common aliases + +```cpp +// Simple, raw degrees as floats (be careful with wrapping!) +using ViewAnglesF = omath::ViewAngles; + +// Safer, policy-based angles (recommended) +using PitchDeg = omath::Angle; +using YawDeg = omath::Angle; +using RollDeg = omath::Angle; +using ViewAnglesDeg = omath::ViewAngles; +``` + +--- + +## Examples + +### Basic construction + +```cpp +omath::ViewAngles a{ 10.f, 45.f, 0.f }; // pitch, yaw, roll in degrees +``` + +### With `omath::Angle<>` (automatic wrap/clamper) + +```cpp +ViewAnglesDeg v{ + PitchDeg::from_degrees( 95.f), // -> 89deg (clamped) + YawDeg::from_degrees (-190.f), // -> 170deg (wrapped) + RollDeg::from_degrees ( 30.f) +}; +``` + +### Using with `projection::Camera` + +```cpp +using Mat4 = omath::Mat<4,4,float, omath::MatStoreType::COLUMN_MAJOR>; +using Cam = omath::projection::Camera; + +omath::projection::ViewPort vp{1920,1080}; +auto fov = omath::angles::degrees_to_radians(70.f); // or your Angle type + +Cam cam(/*position*/ {0,1.7f,-3}, + /*angles*/ ViewAnglesDeg{ PitchDeg::from_degrees(0), + YawDeg::from_degrees(0), + RollDeg::from_degrees(0) }, + /*viewport*/ vp, + /*fov*/ omath::Angle::from_degrees(70.f), + /*near*/ 0.1f, + /*far*/ 1000.f); +``` + +--- + +## Notes & tips + +* **Ranges/units**: pick types that encode your policy (e.g., signed yaw in `[-180,180]`, pitch clamped to avoid gimbal flips). +* **Handedness & order**: this struct doesn’t impose rotation order. Your math/trait layer (e.g., `MyCameraTrait`) must define how `(pitch, yaw, roll)` map to a view matrix (common orders: ZYX or XYZ). +* **Zero-cost**: with plain `float`s this is as cheap as three scalars; with `Angle<>` you gain safety at the cost of tiny wrap/clamp logic on construction/arithmetic. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 00000000..7fc8574d --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,525 @@ +# Troubleshooting + +Solutions to common problems when using OMath. + +--- + +## Build & Compilation Issues + +### Error: C++20 features not available + +**Problem:** Compiler doesn't support C++20. + +**Solution:** +Upgrade your compiler: +- **GCC**: Version 10 or newer +- **Clang**: Version 11 or newer +- **MSVC**: Visual Studio 2019 16.10 or newer + +Set C++20 in CMakeLists.txt: +```cmake +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +``` + +### Error: `std::expected` not found + +**Problem:** Using C++20 without C++23's `std::expected`. + +**Solutions:** + +1. **Upgrade to C++23** (recommended): + ```cmake + set(CMAKE_CXX_STANDARD 23) + ``` + +2. **Use a backport library**: + ```cmake + find_package(tl-expected CONFIG REQUIRED) + target_link_libraries(your_target PRIVATE tl::expected) + ``` + +### Error: `omath/omath.hpp` not found + +**Problem:** OMath not installed or not in include path. + +**Solution:** + +Check installation: +```bash +# vcpkg +vcpkg list | grep omath + +# Check if files exist +ls /path/to/vcpkg/installed/x64-linux/include/omath +``` + +In CMakeLists.txt: +```cmake +find_package(omath CONFIG REQUIRED) +target_link_libraries(your_target PRIVATE omath::omath) +``` + +### Linker errors with AVX2 engine + +**Problem:** Undefined references to AVX2 functions. + +**Solution:** + +Enable AVX2 in your build: +```cmake +if(MSVC) + target_compile_options(your_target PRIVATE /arch:AVX2) +else() + target_compile_options(your_target PRIVATE -mavx2) +endif() +``` + +Or use the legacy engine instead: +```cpp +// Use this instead of ProjPredEngineAVX2 +ProjPredEngineLegacy engine; +``` + +--- + +## Runtime Issues + +### `world_to_screen()` always returns `nullopt` + +**Common causes:** + +1. **Point behind camera** + ```cpp + // Point is behind the camera + Vector3 behind = camera_pos - Vector3{0, 0, 100}; + auto result = camera.world_to_screen(behind); // Returns nullopt + ``` + + **Fix:** Only project points in front of camera. Check Z-coordinate in view space. + +2. **Invalid near/far planes** + ```cpp + // Bad: near >= far + Camera camera(pos, angles, viewport, fov, 100.0f, 1.0f); + + // Good: near < far + Camera camera(pos, angles, viewport, fov, 0.1f, 1000.0f); + ``` + +3. **Invalid FOV** + ```cpp + // Bad: FOV out of range + auto fov = FieldOfView::from_degrees(0.0f); // Too small + auto fov = FieldOfView::from_degrees(180.0f); // Too large + + // Good: FOV in valid range + auto fov = FieldOfView::from_degrees(90.0f); + ``` + +4. **Uninitialized camera** + ```cpp + // Make sure camera is properly initialized + camera.update(current_position, current_angles); + ``` + +**Debugging:** +```cpp +Vector3 world_pos{100, 100, 100}; + +// Check projection step by step +std::cout << "World pos: " << world_pos.x << ", " + << world_pos.y << ", " << world_pos.z << "\n"; + +auto view_matrix = camera.get_view_matrix(); +// Transform to view space manually and check if Z > 0 + +if (auto screen = camera.world_to_screen(world_pos)) { + std::cout << "Success: " << screen->x << ", " << screen->y << "\n"; +} else { + std::cout << "Failed - check if point is behind camera\n"; +} +``` + +### Angles wrapping incorrectly + +**Problem:** Angles not normalizing to expected ranges. + +**Solution:** + +Use proper angle types: +```cpp +// Wrong: using raw floats +float pitch = 95.0f; // Out of valid range! + +// Right: using typed angles +auto pitch = PitchAngle::from_degrees(89.0f); // Clamped to valid range +``` + +For custom ranges: +```cpp +// Define custom angle with wrapping +auto angle = Angle::from_degrees(270.0f); +// Result: -90° (wrapped) +``` + +### Projection appears mirrored or inverted + +**Problem:** Using wrong engine trait for your game. + +**Solution:** + +Different engines have different coordinate systems: + +| Symptom | Likely Issue | Fix | +|---------|-------------|-----| +| Upside down | Y-axis inverted | Try different engine or negate Y | +| Left-right flipped | Wrong handedness | Check engine documentation | +| Rotated 90° | Axis swap | Verify engine coordinate system | + +```cpp +// Try different engine traits +using namespace omath::source_engine; // Z-up, left-handed +using namespace omath::unity_engine; // Y-up, left-handed +using namespace omath::unreal_engine; // Z-up, left-handed (different conventions) +using namespace omath::opengl_engine; // Y-up, right-handed +``` + +If still wrong, manually transform coordinates: +```cpp +// Example: swap Y and Z for Y-up to Z-up conversion +Vector3 convert_y_up_to_z_up(const Vector3& pos) { + return Vector3{pos.x, pos.z, pos.y}; +} +``` + +--- + +## Projectile Prediction Issues + +### `maybe_calculate_aim_point()` returns `nullopt` + +**Common causes:** + +1. **Target moving too fast** + ```cpp + Target target; + target.velocity = Vector3{1000, 0, 0}; // Very fast! + + Projectile proj; + proj.speed = 500.0f; // Too slow to catch target + + // Returns nullopt - projectile can't catch target + ``` + + **Fix:** Check if projectile speed > target speed in the direction of motion. + +2. **Zero projectile speed** + ```cpp + Projectile proj; + proj.speed = 0.0f; // Invalid! + + // Returns nullopt + ``` + + **Fix:** Ensure `proj.speed > 0`. + +3. **Invalid positions** + ```cpp + // NaN or infinite values + target.position = Vector3{NAN, 0, 0}; + + // Returns nullopt + ``` + + **Fix:** Validate all input values are finite. + +4. **Target out of range** + ```cpp + // Target very far away + float distance = shooter_pos.distance_to(target.position); + float max_range = proj.speed * max_flight_time; + + if (distance > max_range) { + // Will return nullopt + } + ``` + +**Debugging:** +```cpp +Projectile proj{/* ... */}; +Target target{/* ... */}; + +// Check inputs +assert(proj.speed > 0); +assert(std::isfinite(target.position.length())); +assert(std::isfinite(target.velocity.length())); + +// Check if target is reachable +float distance = proj.origin.distance_to(target.position); +float target_speed = target.velocity.length(); + +std::cout << "Distance: " << distance << "\n"; +std::cout << "Projectile speed: " << proj.speed << "\n"; +std::cout << "Target speed: " << target_speed << "\n"; + +if (target_speed >= proj.speed) { + std::cout << "Target may be too fast!\n"; +} +``` + +### Aim point is inaccurate + +**Problem:** Calculated aim point doesn't hit target. + +**Possible causes:** + +1. **Unit mismatch** + ```cpp + // All units must match! + proj.speed = 800.0f; // meters per second + target.velocity = Vector3{2, 1, 0}; // Must also be m/s! + + // If using different units (e.g., game units vs meters), convert: + float game_units_to_meters = 0.01905f; // Example for Source + target.velocity = game_velocity * game_units_to_meters; + ``` + +2. **Wrong gravity vector** + ```cpp + // Source Engine: Z-up + proj.gravity = Vector3{0, 0, -9.81f}; + + // Unity: Y-up + proj.gravity = Vector3{0, -9.81f, 0}; + ``` + +3. **Target velocity not updated** + ```cpp + // Update target velocity each frame + target.velocity = current_velocity; // Not last frame's velocity! + ``` + +--- + +## Pattern Scanning Issues + +### Pattern not found when it should be + +**Problem:** `pattern_scan()` returns `nullopt` but pattern exists. + +**Solutions:** + +1. **Pattern syntax error** + ```cpp + // Wrong: missing spaces + PatternView pattern{"488B05????????"}; + + // Right: spaces between bytes + PatternView pattern{"48 8B 05 ?? ?? ?? ??"}; + ``` + +2. **Pattern too specific** + ```cpp + // May fail if any byte is different + PatternView pattern{"48 8B 05 01 02 03 04 48 85 C0"}; + + // Better: use wildcards for variable bytes + PatternView pattern{"48 8B 05 ?? ?? ?? ?? 48 85 C0"}; + ``` + +3. **Searching wrong memory region** + ```cpp + // Make sure you're scanning the right memory + std::vector code_section = get_code_section(); + auto result = pattern_scan(code_section, pattern); + ``` + +4. **Pattern might have multiple matches** + ```cpp + // Find all matches instead of just first + size_t offset = 0; + while (offset < memory.size()) { + auto result = pattern_scan( + std::span(memory.begin() + offset, memory.end()), + pattern + ); + if (result) { + std::cout << "Match at: " << offset + result->offset << "\n"; + offset += result->offset + 1; + } else { + break; + } + } + ``` + +### Pattern found at wrong location + +**Problem:** Pattern matches unintended code. + +**Solutions:** + +1. **Make pattern more specific** + ```cpp + // Too generic + PatternView pattern{"48 8B"}; + + // More specific - include more context + PatternView pattern{"48 8B 05 ?? ?? ?? ?? 48 85 C0 74 ??"}; + ``` + +2. **Verify found address** + ```cpp + if (auto result = pattern_scan(memory, pattern)) { + // Verify by checking nearby bytes + size_t offset = result->offset; + + // Check if instruction makes sense + if (memory[offset] == 0x48 && memory[offset + 1] == 0x8B) { + // Looks good + } + } + ``` + +3. **Use multiple patterns** + ```cpp + // Find reference function first + auto ref_pattern = PatternView{"E8 ?? ?? ?? ?? 85 C0"}; + auto ref_result = pattern_scan(memory, ref_pattern); + + // Then search near that location + // This provides context validation + ``` + +--- + +## Vector & Math Issues + +### `normalized()` returns zero vector + +**Problem:** Normalizing a zero-length vector. + +**Behavior:** +```cpp +Vector3 zero{0, 0, 0}; +auto result = zero.normalized(); // Returns {0, 0, 0} +``` + +This is **intentional** to avoid NaN. Check vector length first: +```cpp +if (v.length() > 0.001f) { + auto normalized = v.normalized(); + // Use normalized vector +} else { + // Handle zero-length case +} +``` + +### `angle_between()` returns error + +**Problem:** One or both vectors have zero length. + +**Solution:** +```cpp +auto angle_result = v1.angle_between(v2); + +if (angle_result) { + float degrees = angle_result->as_degrees(); +} else { + // Handle error - one or both vectors have zero length + std::cerr << "Cannot compute angle between zero-length vectors\n"; +} +``` + +### Cross product seems wrong + +**Problem:** Unexpected cross product result. + +**Check:** +1. **Right-handed system** + ```cpp + Vector3 x{1, 0, 0}; + Vector3 y{0, 1, 0}; + auto z = x.cross(y); // Should be {0, 0, 1} in right-handed system + ``` + +2. **Order matters** + ```cpp + auto cross1 = a.cross(b); // {x1, y1, z1} + auto cross2 = b.cross(a); // {-x1, -y1, -z1} (opposite direction!) + ``` + +--- + +## Performance Issues + +### Code is slower than expected + +**Solutions:** + +1. **Enable optimizations** + ```cmake + # CMakeLists.txt + target_compile_options(your_target PRIVATE + $<$:-O3> + $<$:-march=native> + ) + ``` + +2. **Use AVX2 engine** + ```cpp + // Instead of + ProjPredEngineLegacy engine; + + // Use + ProjPredEngineAVX2 engine; + ``` + +3. **Avoid unnecessary operations** + ```cpp + // Bad: recompute every frame + for (auto& entity : entities) { + float dist = entity.pos.distance_to(player_pos); // Expensive sqrt! + if (dist < 100.0f) { /* ... */ } + } + + // Good: use squared distance + constexpr float max_dist_sq = 100.0f * 100.0f; + for (auto& entity : entities) { + float dist_sq = entity.pos.distance_to_sqr(player_pos); // No sqrt! + if (dist_sq < max_dist_sq) { /* ... */ } + } + ``` + +4. **Cache matrices** + ```cpp + // Bad: recompute matrix every call + for (auto& pos : positions) { + auto screen = camera.world_to_screen(pos); // Recomputes matrices! + } + + // Good: matrices are cached in camera automatically + camera.update(pos, angles); // Updates matrices once + for (auto& pos : positions) { + auto screen = camera.world_to_screen(pos); // Uses cached matrices + } + ``` + +--- + +## Getting More Help + +If your issue isn't covered here: + +1. **Check the docs**: [API Overview](api_overview.md), [Tutorials](tutorials.md) +2. **Search GitHub issues**: [Issues page](https://github.com/orange-cpp/omath/issues) +3. **Ask on Discord**: [Join community](https://discord.gg/eDgdaWbqwZ) +4. **Open a new issue**: Include: + - OMath version + - Compiler and version + - Minimal reproducible example + - What you expected vs what happened + +--- + +*Last updated: 1 Nov 2025* diff --git a/docs/tutorials.md b/docs/tutorials.md new file mode 100644 index 00000000..d911973f --- /dev/null +++ b/docs/tutorials.md @@ -0,0 +1,616 @@ +# Tutorials + +This page provides step-by-step tutorials for common OMath use cases. + +--- + +## Tutorial 1: Basic Vector Math + +Learn the fundamentals of vector operations in OMath. + +### Step 1: Include OMath + +```cpp +#include +#include + +using namespace omath; +``` + +### Step 2: Create Vectors + +```cpp +// 2D vectors +Vector2 v2a{3.0f, 4.0f}; +Vector2 v2b{1.0f, 2.0f}; + +// 3D vectors +Vector3 v3a{1.0f, 2.0f, 3.0f}; +Vector3 v3b{4.0f, 5.0f, 6.0f}; + +// 4D vectors (often used for homogeneous coordinates) +Vector4 v4{1.0f, 2.0f, 3.0f, 1.0f}; +``` + +### Step 3: Perform Operations + +```cpp +// Addition +auto sum = v3a + v3b; // {5, 7, 9} + +// Subtraction +auto diff = v3a - v3b; // {-3, -3, -3} + +// Scalar multiplication +auto scaled = v3a * 2.0f; // {2, 4, 6} + +// Dot product +float dot = v3a.dot(v3b); // 32.0 + +// Cross product (3D only) +auto cross = v3a.cross(v3b); // {-3, 6, -3} + +// Length +float len = v3a.length(); // ~3.74 + +// Normalization (safe - returns original if length is zero) +auto normalized = v3a.normalized(); + +// Distance between vectors +float dist = v3a.distance_to(v3b); // ~5.196 +``` + +### Step 4: Angle Calculations + +```cpp +if (auto angle = v3a.angle_between(v3b)) { + std::cout << "Angle in degrees: " << angle->as_degrees() << "\n"; + std::cout << "Angle in radians: " << angle->as_radians() << "\n"; +} else { + std::cout << "Cannot compute angle (zero-length vector)\n"; +} + +// Check if perpendicular +if (v3a.is_perpendicular(v3b)) { + std::cout << "Vectors are perpendicular\n"; +} +``` + +**Key takeaways:** +- All vector operations are type-safe and constexpr-friendly +- Safe normalization never produces NaN +- Angle calculations use `std::expected` for error handling + +--- + +## Tutorial 2: World-to-Screen Projection + +Project 3D coordinates to 2D screen space for overlays and ESP. + +### Step 1: Choose Your Game Engine + +```cpp +#include + +// For Source Engine games (CS:GO, TF2, etc.) +using namespace omath::source_engine; + +// Or for other engines: +// using namespace omath::unity_engine; +// using namespace omath::unreal_engine; +// using namespace omath::frostbite_engine; +``` + +### Step 2: Set Up the Camera + +```cpp +using namespace omath; +using namespace omath::projection; + +// Define viewport (screen dimensions) +ViewPort viewport{1920.0f, 1080.0f}; + +// Define field of view +auto fov = FieldOfView::from_degrees(90.0f); + +// Camera position and angles +Vector3 camera_pos{0.0f, 0.0f, 100.0f}; +ViewAngles camera_angles{ + PitchAngle::from_degrees(0.0f), + YawAngle::from_degrees(0.0f), + RollAngle::from_degrees(0.0f) +}; + +// Create camera (using Source Engine in this example) +Camera camera( + camera_pos, + camera_angles, + viewport, + fov, + 0.1f, // near plane + 1000.0f // far plane +); +``` + +### Step 3: Project 3D Points + +```cpp +// 3D world position (e.g., enemy player position) +Vector3 enemy_pos{150.0f, 200.0f, 75.0f}; + +// Project to screen +if (auto screen = camera.world_to_screen(enemy_pos)) { + std::cout << "Enemy on screen at: " + << screen->x << ", " << screen->y << "\n"; + + // Draw ESP box or marker at screen->x, screen->y + // Note: screen coordinates are in viewport space (0-width, 0-height) +} else { + // Enemy is not visible (behind camera or outside frustum) + std::cout << "Enemy not visible\n"; +} +``` + +### Step 4: Update Camera for Each Frame + +```cpp +void render_frame() { + // Read current camera data from game + Vector3 new_pos = read_camera_position(); + ViewAngles new_angles = read_camera_angles(); + + // Update camera + camera.update(new_pos, new_angles); + + // Project all entities + for (const auto& entity : entities) { + if (auto screen = camera.world_to_screen(entity.position)) { + draw_esp_box(screen->x, screen->y); + } + } +} +``` + +**Key takeaways:** +- Choose the engine trait that matches your target game +- `world_to_screen()` returns `std::optional` - always check the result +- Update camera each frame for accurate projections +- Screen coordinates are in the viewport space you defined + +--- + +## Tutorial 3: Projectile Prediction (Aim-Bot) + +Calculate where to aim to hit a moving target. + +### Step 1: Define Projectile Properties + +```cpp +#include +#include + +using namespace omath; +using namespace omath::projectile_prediction; + +// Define your weapon's projectile +Projectile bullet; +bullet.origin = Vector3{0, 0, 0}; // Shooter position +bullet.speed = 800.0f; // Muzzle velocity (m/s or game units/s) +bullet.gravity = Vector3{0, 0, -9.81f}; // Gravity vector +``` + +### Step 2: Define Target State + +```cpp +// Target information (enemy player) +Target enemy; +enemy.position = Vector3{100, 200, 50}; // Current position +enemy.velocity = Vector3{10, 5, 0}; // Current velocity +``` + +### Step 3: Calculate Aim Point + +```cpp +// Create prediction engine +// Use AVX2 version if available for better performance: +// ProjPredEngineAVX2 engine; +ProjPredEngineLegacy engine; + +// Calculate where to aim +if (auto aim_point = engine.maybe_calculate_aim_point(bullet, enemy)) { + std::cout << "Aim at: " + << aim_point->x << ", " + << aim_point->y << ", " + << aim_point->z << "\n"; + + // Calculate angles to aim_point + Vector3 aim_direction = (*aim_point - bullet.origin).normalized(); + + // Convert to view angles (engine-specific) + // ViewAngles angles = calculate_angles_to_direction(aim_direction); + // set_aim_angles(angles); +} else { + // Cannot hit target (too fast, out of range, etc.) + std::cout << "Target cannot be hit\n"; +} +``` + +### Step 4: Handle Different Scenarios + +```cpp +// Stationary target +Target stationary; +stationary.position = Vector3{100, 100, 100}; +stationary.velocity = Vector3{0, 0, 0}; +// aim_point will equal position for stationary targets + +// Fast-moving target +Target fast; +fast.position = Vector3{100, 100, 100}; +fast.velocity = Vector3{50, 0, 0}; // Moving very fast +// May return nullopt if target is too fast + +// Target at different heights +Target aerial; +aerial.position = Vector3{100, 100, 200}; // High up +aerial.velocity = Vector3{5, 5, -10}; // Falling +// Gravity will be factored into the calculation +``` + +### Step 5: Performance Optimization + +```cpp +// For better performance on modern CPUs, use AVX2: +#include + +ProjPredEngineAVX2 fast_engine; // 2-4x faster than legacy + +// Use the same way as legacy engine +if (auto aim = fast_engine.maybe_calculate_aim_point(bullet, enemy)) { + // Process aim point +} +``` + +**Key takeaways:** +- Always check if aim point exists before using +- Velocity must be in same units as position/speed +- Gravity vector points down (typically negative Z or Y depending on engine) +- Use AVX2 engine when possible for better performance +- Returns `nullopt` when target is unreachable + +--- + +## Tutorial 4: Collision Detection + +Perform ray-casting and intersection tests. + +### Step 1: Ray-Plane Intersection + +```cpp +#include + +using namespace omath; + +// Define a ground plane (Z=0, normal pointing up) +Plane ground{ + Vector3{0, 0, 0}, // Point on plane + Vector3{0, 0, 1} // Normal vector (Z-up) +}; + +// Define a ray (e.g., looking downward from above) +Vector3 ray_origin{10, 20, 100}; +Vector3 ray_direction{0, 0, -1}; // Pointing down + +// Test intersection +if (auto hit = ground.intersects_ray(ray_origin, ray_direction)) { + std::cout << "Hit ground at: " + << hit->x << ", " << hit->y << ", " << hit->z << "\n"; + // Expected: (10, 20, 0) +} else { + std::cout << "Ray does not intersect plane\n"; +} +``` + +### Step 2: Distance to Plane + +```cpp +// Calculate signed distance from point to plane +Vector3 point{10, 20, 50}; +float distance = ground.distance_to_point(point); + +std::cout << "Distance to ground: " << distance << "\n"; +// Expected: 50.0 (50 units above ground) + +// Negative distance means point is below the plane +Vector3 below{10, 20, -5}; +float dist_below = ground.distance_to_point(below); +// Expected: -5.0 +``` + +### Step 3: Axis-Aligned Bounding Box + +```cpp +#include + +// Create a bounding box +Box bbox{ + Vector3{0, 0, 0}, // Min corner + Vector3{100, 100, 100} // Max corner +}; + +// Test if point is inside +Vector3 inside{50, 50, 50}; +if (bbox.contains(inside)) { + std::cout << "Point is inside box\n"; +} + +Vector3 outside{150, 50, 50}; +if (!bbox.contains(outside)) { + std::cout << "Point is outside box\n"; +} + +// Box-box intersection +Box other{ + Vector3{50, 50, 50}, + Vector3{150, 150, 150} +}; + +if (bbox.intersects(other)) { + std::cout << "Boxes overlap\n"; +} +``` + +### Step 4: Line Tracing + +```cpp +#include + +using namespace omath::collision; + +// Ray-triangle intersection +Vector3 v0{0, 0, 0}; +Vector3 v1{100, 0, 0}; +Vector3 v2{0, 100, 0}; + +Vector3 ray_start{25, 25, 100}; +Vector3 ray_dir{0, 0, -1}; + +LineTracer tracer; +if (auto hit = tracer.ray_triangle_intersect(ray_start, ray_dir, v0, v1, v2)) { + std::cout << "Hit triangle at: " + << hit->point.x << ", " + << hit->point.y << ", " + << hit->point.z << "\n"; + std::cout << "Hit distance: " << hit->distance << "\n"; + std::cout << "Surface normal: " + << hit->normal.x << ", " + << hit->normal.y << ", " + << hit->normal.z << "\n"; +} +``` + +**Key takeaways:** +- Plane normals should be unit vectors +- Ray direction should typically be normalized +- Signed distance indicates which side of plane a point is on +- AABB tests are very fast for broad-phase collision detection +- Line tracer provides hit point, distance, and surface normal + +--- + +## Tutorial 5: Pattern Scanning + +Search for byte patterns in memory. + +### Step 1: Basic Pattern Scanning + +```cpp +#include +#include + +using namespace omath; + +// Memory to search (e.g., from a loaded module) +std::vector memory = { + 0x48, 0x8B, 0x05, 0xAA, 0xBB, 0xCC, 0xDD, + 0x48, 0x85, 0xC0, 0x74, 0x10, + // ... more bytes +}; + +// Pattern with wildcards (?? = match any byte) +PatternView pattern{"48 8B 05 ?? ?? ?? ?? 48 85 C0"}; + +// Scan for pattern +if (auto result = pattern_scan(memory, pattern)) { + std::cout << "Pattern found at offset: " << result->offset << "\n"; + + // Extract wildcard values if needed + // result->wildcards contains the matched bytes at ?? positions +} else { + std::cout << "Pattern not found\n"; +} +``` + +### Step 2: PE File Scanning + +```cpp +#include + +// Scan a PE file (EXE or DLL) +PEPatternScanner scanner("game.exe"); + +PatternView pattern{"E8 ?? ?? ?? ?? 85 C0 75 ??"}; + +if (auto rva = scanner.scan_pattern(pattern)) { + std::cout << "Pattern found at RVA: 0x" + << std::hex << *rva << std::dec << "\n"; + + // Convert RVA to absolute address if needed + uintptr_t base_address = get_module_base("game.exe"); + uintptr_t absolute = base_address + *rva; +} else { + std::cout << "Pattern not found in PE file\n"; +} +``` + +### Step 3: Multiple Patterns + +```cpp +// Search for multiple patterns +std::vector patterns{ + PatternView{"48 8B 05 ?? ?? ?? ??"}, + PatternView{"E8 ?? ?? ?? ?? 85 C0"}, + PatternView{"FF 15 ?? ?? ?? ?? 48 8B"} +}; + +for (size_t i = 0; i < patterns.size(); ++i) { + if (auto result = pattern_scan(memory, patterns[i])) { + std::cout << "Pattern " << i << " found at: " + << result->offset << "\n"; + } +} +``` + +### Step 4: Pattern with Masks + +```cpp +// Alternative: use mask-based patterns +// Pattern: bytes to match +std::vector pattern_bytes{0x48, 0x8B, 0x05, 0x00, 0x00, 0x00, 0x00}; + +// Mask: 'x' = must match, '?' = wildcard +std::string mask{"xxx????"}; + +// Custom scan function +auto scan_with_mask = [&](const std::vector& data) { + for (size_t i = 0; i < data.size() - pattern_bytes.size(); ++i) { + bool match = true; + for (size_t j = 0; j < pattern_bytes.size(); ++j) { + if (mask[j] == 'x' && data[i + j] != pattern_bytes[j]) { + match = false; + break; + } + } + if (match) return i; + } + return size_t(-1); +}; +``` + +**Key takeaways:** +- Use `??` in pattern strings for wildcards +- PE scanner works with files and modules +- Pattern scanning is useful for finding functions, vtables, or data +- Always validate found addresses before use +- Patterns may have multiple matches - consider context + +--- + +## Tutorial 6: Angles and View Angles + +Work with game camera angles properly. + +### Step 1: Understanding Angle Types + +```cpp +#include + +using namespace omath; + +// Generic angle with custom range +auto angle1 = Angle::from_degrees(45.0f); +auto angle2 = Angle::from_degrees(270.0f); + +// Specialized camera angles +auto pitch = PitchAngle::from_degrees(-10.0f); // Looking down +auto yaw = YawAngle::from_degrees(90.0f); // Looking right +auto roll = RollAngle::from_degrees(0.0f); // No tilt +``` + +### Step 2: Angle Conversions + +```cpp +// Create from degrees +auto deg_angle = PitchAngle::from_degrees(45.0f); + +// Get as radians +float radians = deg_angle.as_radians(); +std::cout << "45° = " << radians << " radians\n"; + +// Get as degrees +float degrees = deg_angle.as_degrees(); +std::cout << "Value: " << degrees << "°\n"; +``` + +### Step 3: View Angles (Camera) + +```cpp +// Pitch: vertical rotation (-89° to 89°) +// Yaw: horizontal rotation (-180° to 180°) +// Roll: camera tilt (-180° to 180°) + +ViewAngles camera_angles{ + PitchAngle::from_degrees(-15.0f), // Looking slightly down + YawAngle::from_degrees(45.0f), // Facing northeast + RollAngle::from_degrees(0.0f) // No tilt +}; + +// Access individual components +float pitch_val = camera_angles.pitch.as_degrees(); +float yaw_val = camera_angles.yaw.as_degrees(); +float roll_val = camera_angles.roll.as_degrees(); +``` + +### Step 4: Calculating Look-At Angles + +```cpp +using namespace omath::source_engine; // Or your game's engine + +Vector3 camera_pos{0, 0, 100}; +Vector3 target_pos{100, 100, 100}; + +// Calculate angles to look at target +ViewAngles look_at = CameraTrait::calc_look_at_angle(camera_pos, target_pos); + +std::cout << "Pitch: " << look_at.pitch.as_degrees() << "°\n"; +std::cout << "Yaw: " << look_at.yaw.as_degrees() << "°\n"; +std::cout << "Roll: " << look_at.roll.as_degrees() << "°\n"; +``` + +### Step 5: Angle Arithmetic + +```cpp +// Angles support arithmetic with automatic normalization +auto angle1 = YawAngle::from_degrees(170.0f); +auto angle2 = YawAngle::from_degrees(20.0f); + +// Addition (wraps around) +auto sum = angle1 + angle2; // 190° → normalized to -170° + +// Subtraction +auto diff = angle2 - angle1; // -150° + +// Scaling +auto scaled = angle1 * 2.0f; +``` + +**Key takeaways:** +- Use specialized angle types for camera angles (PitchAngle, YawAngle, RollAngle) +- Angles automatically normalize to their valid ranges +- Each game engine may have different angle conventions +- Use engine traits to calculate look-at angles correctly + +--- + +## Next Steps + +Now that you've completed these tutorials, explore: + +- **[API Overview](api_overview.md)** - Complete API reference +- **[Engine Documentation](engines/)** - Engine-specific features +- **[Examples](../examples/)** - More code examples +- **[Getting Started](getting_started.md)** - Quick start guide + +--- + +*Last updated: 1 Nov 2025* diff --git a/docs/utility/color.md b/docs/utility/color.md new file mode 100644 index 00000000..e4b864b0 --- /dev/null +++ b/docs/utility/color.md @@ -0,0 +1,190 @@ +# `omath::Color` — RGBA color with HSV helpers (C++20/23) + +> Header: your project’s `color.hpp` +> Namespace: `omath` +> Inherits: `Vector4` (`x=r`, `y=g`, `z=b`, `w=a`) +> Depends on: ``, `Vector4`, optionally ImGui (`OMATH_IMGUI_INTEGRATION`) +> Formatting: provides `std::formatter` + +`Color` is a tiny RGBA utility on top of `Vector4`. It offers sRGB-style channel construction, HSV↔RGB conversion, in-place HSV setters, linear blending, and string/formatter helpers. + +--- + +## Quick start + +```cpp +#include "color.hpp" +using omath::Color; + +// RGBA in [0,1] (r,g,b clamped to [0,1] on construction) +Color c{0.2f, 0.4f, 0.8f, 0.5f}; + +// From 8-bit channels +auto red = Color::from_rgba(255, 0, 0, 255); +auto green = Color::from_rgba(0, 255, 0, 160); + +// From HSV (h ∈ [0,1], s ∈ [0,1], v ∈ [0,1]) +auto cyan = Color::from_hsv(0.5f, 1.0f, 1.0f); // a = 1 + +// Read/modify via HSV +auto hsv = cyan.to_hsv(); // hue ∈ [0,1], saturation ∈ [0,1], value ∈ [0,1] +cyan.set_value(0.6f); // converts back to RGB (alpha becomes 1) + +// Blend linearly (lerp) +auto mid = red.blend(green, 0.5f); + +// Printable (0–255 per channel) +std::string s = std::format("{}", mid); // "[r:128, g:128, b:0, a:207]" for example +``` + +--- + +## Data model + +* Inherits `Vector4`: + + * `x` = **red**, `y` = **green**, `z` = **blue**, `w` = **alpha**. +* Construction clamps **RGB** to `[0,1]` (via `Vector4::clamp(0,1)`), **alpha is not clamped** by that call (see notes). + +--- + +## Construction & factories + +```cpp +// RGBA in [0,1] (RGB clamped to [0,1]; alpha untouched by clamp) +constexpr Color(float r, float g, float b, float a) noexcept; + +// Default +constexpr Color() noexcept; + +// From 8-bit RGBA (0–255) → normalized to [0,1] +constexpr static Color from_rgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a) noexcept; + +// From HSV where hue ∈ [0,1], saturation ∈ [0,1], value ∈ [0,1] +struct Hsv { float hue{}, saturation{}, value{}; }; + +constexpr static Color from_hsv(float hue, float saturation, float value) noexcept; +constexpr static Color from_hsv(const Hsv& hsv) noexcept; // delegates to the above + +// Construct from a Vector4 (RGB clamped, alpha not clamped) +constexpr explicit Color(const Vector4& vec) noexcept; +``` + +**HSV details** + +* `from_hsv(h, s, v)`: `h` is **normalized** (`[0,1]`); it is clamped, then mapped to the 6 hue sectors; **alpha = 1.0**. +* `to_hsv()`: returns `Hsv{h,s,v}` with **`h ∈ [0,1]`** (internally computes degrees and divides by 360), `s,v ∈ [0,1]`. + +--- + +## Mutators + +```cpp +constexpr void set_hue(float h) noexcept; // h ∈ [0,1] recommended +constexpr void set_saturation(float s) noexcept; // s ∈ [0,1] +constexpr void set_value(float v) noexcept; // v ∈ [0,1] + +// Linear blend: (1-ratio)*this + ratio*other, ratio clamped to [0,1] +constexpr Color blend(const Color& other, float ratio) const noexcept; +``` + +> ⚠️ **Alpha reset on HSV setters:** each `set_*` converts HSV→RGB using `from_hsv(...)`, which **sets alpha to 1.0** (overwriting previous `w`). If you need to preserve alpha: +> +> ```cpp +> float a = col.w; +> col.set_value(0.5f); +> col.w = a; +> ``` + +--- + +## Constants + +```cpp +static constexpr Color red(); // (1,0,0,1) +static constexpr Color green(); // (0,1,0,1) +static constexpr Color blue(); // (0,0,1,1) +``` + +--- + +## String & formatting + +```cpp +// "[r:R, g:G, b:B, a:A]" with each channel shown as 0–255 integer +std::string to_string() const noexcept; +std::wstring to_wstring() const noexcept; +std::u8string to_u8string() const noexcept; + +// Formatter forwards to the above (char/wchar_t/char8_t) +template<> struct std::formatter; +``` + +--- + +## ImGui (optional) + +```cpp +#ifdef OMATH_IMGUI_INTEGRATION +ImColor to_im_color() const noexcept; // constructs from Vector4's to_im_vec4() +#endif +``` + +Ensure `` is included somewhere before this header when the macro is enabled. + +--- + +## Notes & caveats + +* **Alpha clamping:** `Vector4::clamp(min,max)` (called by `Color` ctors) clamps **x,y,z** only in the provided `Vector4` implementation; `w` is **left unchanged**. If you require strict `[0,1]` alpha, clamp it yourself: + + ```cpp + col.w = std::clamp(col.w, 0.0f, 1.0f); + ``` +* **HSV range:** The API consistently uses **normalized hue** (`[0,1]`). Convert degrees ↔ normalized as `h_norm = h_deg / 360.f`. +* **Blend space:** `blend` is a **linear** interpolation in RGBA; it is not perceptually uniform. + +--- + +## API summary + +```cpp +struct Hsv { float hue{}, saturation{}, value{}; }; + +class Color final : public Vector4 { +public: + constexpr Color(float r, float g, float b, float a) noexcept; + constexpr Color() noexcept; + constexpr explicit Color(const Vector4& vec) noexcept; + + static constexpr Color from_rgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a) noexcept; + static constexpr Color from_hsv(float hue, float saturation, float value) noexcept; + static constexpr Color from_hsv(const Hsv& hsv) noexcept; + + constexpr Hsv to_hsv() const noexcept; + + constexpr void set_hue(float h) noexcept; + constexpr void set_saturation(float s) noexcept; + constexpr void set_value(float v) noexcept; + + constexpr Color blend(const Color& other, float ratio) const noexcept; + + static constexpr Color red(); + static constexpr Color green(); + static constexpr Color blue(); + +#ifdef OMATH_IMGUI_INTEGRATION + ImColor to_im_color() const noexcept; +#endif + + std::string to_string() const noexcept; + std::wstring to_wstring() const noexcept; + std::u8string to_u8string() const noexcept; +}; + +// formatter provided +``` + +--- + +*Last updated: 31 Oct 2025* diff --git a/docs/utility/elf_pattern_scan.md b/docs/utility/elf_pattern_scan.md new file mode 100644 index 00000000..1c084729 --- /dev/null +++ b/docs/utility/elf_pattern_scan.md @@ -0,0 +1,142 @@ +# `omath::ElfPatternScanner` — Scan ELF binaries for byte patterns + +> Header: `omath/utility/elf_pattern_scan.hpp` +> Namespace: `omath` +> Platform: **Linux / ELF (Executable and Linkable Format) binaries** +> Depends on: ``, ``, ``, ``, `omath/utility/section_scan_result.hpp` +> Companion: works well with `omath::PatternScanner` (same pattern grammar) + +`ElfPatternScanner` searches **ELF** binaries for a hex pattern (with wildcards). You can scan: + +* a **loaded module** in the current process, or +* an **ELF file on disk** (by section name; defaults to **`.text`**). + +--- + +## Pattern string grammar (same as `PatternScanner`) + +* **Hex byte**: two hex digits → one byte (`90`, `4F`, `00`, `ff`). +* **Wildcard byte**: `?` or `??` matches **any byte**. +* **Whitespace**: ignored (use to group tokens). + +✔️ `"48 8B ?? ?? 89"`, `"55 48 89 E5"`, `"??"` +❌ odd digit counts, non-hex characters (besides `?` and whitespace) + +--- + +## API + +```cpp +namespace omath { + +class ElfPatternScanner final { +public: + // Scan a module already loaded in *this* process. + // module_base_address: base address of the loaded ELF (e.g., from dlopen / /proc/self/maps) + // Returns absolute address (process VA) of the first match, or nullopt. + static std::optional + scan_for_pattern_in_loaded_module( + const void* module_base_address, + const std::string_view& pattern, + const std::string_view& target_section_name = ".text"); + + // Scan an ELF file on disk, by section name (default ".text"). + // Returns section bases (virtual + raw) and match offset within the section, or nullopt. + static std::optional + scan_for_pattern_in_file( + const std::filesystem::path& path_to_file, + const std::string_view& pattern, + const std::string_view& target_section_name = ".text"); +}; + +} // namespace omath +``` + +--- + +## Return values + +* **Loaded module**: `std::optional` + + * `value()` = **process virtual address** of the first match. + * `nullopt` = no match or parse/ELF error. + +* **File scan**: `std::optional` + + * `virtual_base_addr` = virtual address base of the scanned section. + * `raw_base_addr` = file offset of section start. + * `target_offset` = offset from section base to the first matched byte. + * To get addresses: + + * **Virtual address** of hit = `virtual_base_addr + target_offset` + * **Raw file offset** of hit = `raw_base_addr + target_offset` + +--- + +## Usage examples + +### Scan a loaded module (current process) + +```cpp +#include +#include "omath/utility/elf_pattern_scan.hpp" + +using omath::ElfPatternScanner; + +void* handle = dlopen("libexample.so", RTLD_LAZY); +if (handle) { + auto addr = ElfPatternScanner::scan_for_pattern_in_loaded_module( + handle, "55 48 89 E5 ?? ?? 48" + ); + if (addr) { + std::uintptr_t hit_va = *addr; + // ... + } + dlclose(handle); +} +``` + +### Scan an ELF file on disk + +```cpp +#include "omath/utility/elf_pattern_scan.hpp" +using omath::ElfPatternScanner; + +auto res = ElfPatternScanner::scan_for_pattern_in_file( + "/usr/lib/libexample.so", "55 48 89 E5" +); +if (res) { + auto va_hit = res->virtual_base_addr + res->target_offset; + auto raw_hit = res->raw_base_addr + res->target_offset; +} +``` + +### Scan another section (e.g., ".rodata") + +```cpp +auto res = ElfPatternScanner::scan_for_pattern_in_file( + "myapp", "48 8D 0D ?? ?? ?? ??", ".rodata" +); +``` + +--- + +## Notes & edge cases + +* **ELF only**: these functions assume a valid ELF layout. Non-ELF files or corrupted headers yield `nullopt`. +* **Section name**: defaults to **`.text`**; pass a different name to target other sections. +* **Performance**: Pattern matching is **O(N × M)** (sliding window with wildcards). For large binaries, prefer scanning only necessary sections. +* **Architecture**: works for 32-bit and 64-bit ELF binaries. + +--- + +## See also + +* [`omath::PatternScanner`](pattern_scan.md) — raw buffer/iterator scanning with the same pattern grammar. +* [`omath::PePatternScanner`](pe_pattern_scan.md) — PE (Windows) binary scanner. +* [`omath::MachOPatternScanner`](macho_pattern_scan.md) — Mach-O (macOS) binary scanner. +* [`omath::SectionScanResult`](section_scan_result.md) — return type for file-based scans. + +--- + +*Last updated: Feb 2026* diff --git a/docs/utility/macho_pattern_scan.md b/docs/utility/macho_pattern_scan.md new file mode 100644 index 00000000..2dc94491 --- /dev/null +++ b/docs/utility/macho_pattern_scan.md @@ -0,0 +1,142 @@ +# `omath::MachOPatternScanner` — Scan Mach-O binaries for byte patterns + +> Header: `omath/utility/macho_pattern_scan.hpp` +> Namespace: `omath` +> Platform: **macOS / Mach-O binaries** +> Depends on: ``, ``, ``, ``, `omath/utility/section_scan_result.hpp` +> Companion: works well with `omath::PatternScanner` (same pattern grammar) + +`MachOPatternScanner` searches **Mach-O** binaries for a hex pattern (with wildcards). You can scan: + +* a **loaded module** in the current process, or +* a **Mach-O file on disk** (by section name; defaults to **`__text`**). + +--- + +## Pattern string grammar (same as `PatternScanner`) + +* **Hex byte**: two hex digits → one byte (`90`, `4F`, `00`, `ff`). +* **Wildcard byte**: `?` or `??` matches **any byte**. +* **Whitespace**: ignored (use to group tokens). + +✔️ `"48 8B ?? ?? 89"`, `"55 48 89 E5"`, `"??"` +❌ odd digit counts, non-hex characters (besides `?` and whitespace) + +--- + +## API + +```cpp +namespace omath { + +class MachOPatternScanner final { +public: + // Scan a module already loaded in *this* process. + // module_base_address: base address of the loaded Mach-O image + // Returns absolute address (process VA) of the first match, or nullopt. + static std::optional + scan_for_pattern_in_loaded_module( + const void* module_base_address, + const std::string_view& pattern, + const std::string_view& target_section_name = "__text"); + + // Scan a Mach-O file on disk, by section name (default "__text"). + // Returns section bases (virtual + raw) and match offset within the section, or nullopt. + static std::optional + scan_for_pattern_in_file( + const std::filesystem::path& path_to_file, + const std::string_view& pattern, + const std::string_view& target_section_name = "__text"); +}; + +} // namespace omath +``` + +--- + +## Return values + +* **Loaded module**: `std::optional` + + * `value()` = **process virtual address** of the first match. + * `nullopt` = no match or parse/Mach-O error. + +* **File scan**: `std::optional` + + * `virtual_base_addr` = virtual address base of the scanned section. + * `raw_base_addr` = file offset of section start. + * `target_offset` = offset from section base to the first matched byte. + * To get addresses: + + * **Virtual address** of hit = `virtual_base_addr + target_offset` + * **Raw file offset** of hit = `raw_base_addr + target_offset` + +--- + +## Usage examples + +### Scan a loaded module (current process) + +```cpp +#include +#include "omath/utility/macho_pattern_scan.hpp" + +using omath::MachOPatternScanner; + +void* handle = dlopen("libexample.dylib", RTLD_LAZY); +if (handle) { + auto addr = MachOPatternScanner::scan_for_pattern_in_loaded_module( + handle, "55 48 89 E5 ?? ?? 48" + ); + if (addr) { + std::uintptr_t hit_va = *addr; + // ... + } + dlclose(handle); +} +``` + +### Scan a Mach-O file on disk + +```cpp +#include "omath/utility/macho_pattern_scan.hpp" +using omath::MachOPatternScanner; + +auto res = MachOPatternScanner::scan_for_pattern_in_file( + "/usr/local/lib/libexample.dylib", "55 48 89 E5" +); +if (res) { + auto va_hit = res->virtual_base_addr + res->target_offset; + auto raw_hit = res->raw_base_addr + res->target_offset; +} +``` + +### Scan another section (e.g., "__cstring") + +```cpp +auto res = MachOPatternScanner::scan_for_pattern_in_file( + "myapp", "48 8D 0D ?? ?? ?? ??", "__cstring" +); +``` + +--- + +## Notes & edge cases + +* **Mach-O only**: these functions assume a valid Mach-O layout. Non-Mach-O files or corrupted headers yield `nullopt`. +* **Section name**: defaults to **`__text`** (note the double underscore, per Mach-O convention); pass a different name to target other sections. +* **Performance**: Pattern matching is **O(N × M)** (sliding window with wildcards). For large binaries, prefer scanning only necessary sections. +* **Architecture**: works for 64-bit Mach-O binaries (x86_64 and arm64). + +--- + +## See also + +* [`omath::PatternScanner`](pattern_scan.md) — raw buffer/iterator scanning with the same pattern grammar. +* [`omath::PePatternScanner`](pe_pattern_scan.md) — PE (Windows) binary scanner. +* [`omath::ElfPatternScanner`](elf_pattern_scan.md) — ELF (Linux) binary scanner. +* [`omath::SectionScanResult`](section_scan_result.md) — return type for file-based scans. + +--- + +*Last updated: Feb 2026* diff --git a/docs/utility/pattern_scan.md b/docs/utility/pattern_scan.md new file mode 100644 index 00000000..182c5b52 --- /dev/null +++ b/docs/utility/pattern_scan.md @@ -0,0 +1,194 @@ +# `omath::PatternScanner` — Fast byte-pattern search with wildcards + +> Header: your project’s `pattern_scanner.hpp` +> Namespace: `omath` +> Core API: `scan_for_pattern(...)` (span or iterators) +> Errors: `PatternScanError::INVALID_PATTERN_STRING` (from `parse_pattern`) +> C++: uses `std::span`, `std::expected`, `std::byte` + +`PatternScanner` scans a contiguous byte range for a **hex pattern** that may include **wildcards**. It returns an iterator to the **first match** or the end iterator if not found (or if the pattern string is invalid). + +--- + +## Quick start + +```cpp +#include "pattern_scanner.hpp" +using omath::PatternScanner; + +std::vector buf = /* ... bytes ... */; +std::span s{buf.data(), buf.size()}; + +// Example pattern: "48 8B ?? ?? 89" (hex bytes with '?' as any byte) +auto it = PatternScanner::scan_for_pattern(s, "48 8B ?? ?? 89"); +if (it != s.end()) { + // Found at offset: + auto offset = static_cast(it - s.begin()); +} +``` + +Or with iterators: + +```cpp +auto it = PatternScanner::scan_for_pattern(buf.begin(), buf.end(), "DE AD BE EF"); +if (it != buf.end()) { /* ... */ } +``` + +--- + +## Pattern string grammar + +The pattern string is parsed into a sequence of byte **tokens**: + +* **Hex byte**: two hexadecimal digits form one byte. + Examples: `90`, `4F`, `00`, `ff` +* **Wildcard byte**: `?` or `??` matches **any single byte**. +* **Separators**: any ASCII whitespace (space, tab, newline) is **ignored** and may be used to group tokens. + +> ✔️ Valid: `"48 8B ?? 05 00"`, `"90 90 90"`, `"??"` +> ❌ Invalid: odd number of hex digits in a token, non-hex characters (besides `?` and whitespace) + +If the string cannot be parsed into a clean sequence of tokens, `parse_pattern()` returns `std::unexpected(PatternScanError::INVALID_PATTERN_STRING)`, and the public scan function returns **end**. + +--- + +## API + +```cpp +namespace omath { + +enum class PatternScanError { INVALID_PATTERN_STRING }; + +class PatternScanner final { +public: + // Contiguous range (span) overload + [[nodiscard]] + static std::span::iterator + scan_for_pattern(const std::span& range, + const std::string_view& pattern); + + // Deleted rvalue-span overload (prevents dangling) + static std::span::iterator + scan_for_pattern(std::span&&, + const std::string_view&) = delete; + + // Iterator overload + template + requires std::input_or_output_iterator> + static IteratorType + scan_for_pattern(const IteratorType& begin, + const IteratorType& end, + const std::string_view& pattern); +private: + [[nodiscard]] + static std::expected>, PatternScanError> + parse_pattern(const std::string_view& pattern_string); +}; + +} // namespace omath +``` + +### Return value + +* On success: iterator to the **first** matching position. +* On failure / not found / invalid pattern: **`end`**. + +--- + +## Complexity + +Let `N` be the number of bytes in the range and `M` the number of **pattern tokens**. + +* Time: **O(N × M)** (simple sliding window with early break). +* Space: **O(M)** for the parsed pattern vector. + +--- + +## Examples + +### Find a function prologue + +```cpp +// x86-64: push rbp; mov rbp, rsp +auto it = PatternScanner::scan_for_pattern(s, "55 48 89 E5"); +if (it != s.end()) { + // ... process +} +``` + +### Skip variable bytes with wildcards + +```cpp +// mov rax, ; call +auto it = PatternScanner::scan_for_pattern(s, "48 B8 ?? ?? ?? ?? ?? ?? ?? ?? E8 ?? ?? ?? ??"); +``` + +### Iterator-based scan (subrange) + +```cpp +auto sub_begin = buf.begin() + 1024; +auto sub_end = buf.begin() + 4096; +auto it = PatternScanner::scan_for_pattern(sub_begin, sub_end, "DE AD ?? BE EF"); +``` + +--- + +## Behavior & edge cases + +* **Empty or too-short**: If the effective pattern token count `M` is `0` or `M > N`, the function returns `end`. +* **Wildcards**: Each `?`/`??` token matches **exactly one** byte. +* **No exceptions**: Invalid pattern → parsed as `unexpected`, public API returns `end`. +* **No ownership**: The span overload takes `const std::span&` and returns a **mutable iterator** into that span. You must ensure the underlying memory stays alive. The rvalue-span overload is **deleted** to prevent dangling. + +--- + +## Notes & caveats (implementation-sensitive) + +* Although the iterator overload is constrained with `std::input_or_output_iterator`, the implementation uses `*(begin + i + j)` and arithmetic on iterators. In practice this means you should pass **random-access / contiguous iterators** (e.g., from `std::vector` or `std::span`). Using non-random-access iterators would be ill-formed. +* The inner matching loop compares a parsed token (`std::optional`) to the candidate byte; `std::nullopt` is treated as a **wildcard** (always matches). +* **Implementation note:** the outer scan currently derives its scan bound from the **pattern string length**; conceptually it should use the **number of parsed tokens** (hex/wildcard bytes). If you plan to accept spaces (or other non-byte characters) in the pattern string, ensure the scan window uses the parsed token count to avoid false negatives near the end of the buffer. + +--- + +## Testing hooks + +The class befriends several unit tests: + +``` +unit_test_pattern_scan_read_test_Test +unit_test_pattern_scan_corner_case_1_Test +unit_test_pattern_scan_corner_case_2_Test +unit_test_pattern_scan_corner_case_3_Test +unit_test_pattern_scan_corner_case_4_Test +``` + +Use these to validate parsing correctness, wildcard handling, and boundary conditions. + +--- + +## Troubleshooting + +* **Always returns end**: verify the pattern string is valid hex/wildcards and that you’re scanning the intended subrange. Try a simpler pattern (e.g., a single known byte) to sanity-check. +* **Crashes or compile errors with iterator overload**: use iterators that support random access (e.g., from `std::vector`), or prefer the `std::span` overload. +* **Ambiguous wildcards**: this scanner treats `?` and `??` as **byte-wide** wildcards (not per-nibble). If you need nibble-level masks, extend `parse_pattern` to support patterns like `A?`/`?F` with bitmask matching. + +--- + +## Minimal unit test sketch + +```cpp +TEST(pattern, basic) { + std::array data{ + std::byte{0x48}, std::byte{0x8B}, std::byte{0x01}, std::byte{0x02}, + std::byte{0x89}, std::byte{0x50}, std::byte{0x90}, std::byte{0xCC} + }; + std::span s{data.data(), data.size()}; + auto it = omath::PatternScanner::scan_for_pattern(s, "48 8B ?? ?? 89"); + ASSERT_NE(it, s.end()); + EXPECT_EQ(static_cast(it - s.begin()), 0U); +} +``` + +--- + +*Last updated: 31 Oct 2025* diff --git a/docs/utility/pe_pattern_scan.md b/docs/utility/pe_pattern_scan.md new file mode 100644 index 00000000..026afa7a --- /dev/null +++ b/docs/utility/pe_pattern_scan.md @@ -0,0 +1,155 @@ +# `omath::PePatternScanner` — Scan PE images for byte patterns + +> Header: your project’s `pe_pattern_scanner.hpp` +> Namespace: `omath` +> Platform: **Windows / PE (Portable Executable) images** +> Depends on: ``, ``, `` +> Companion: works well with `omath::PatternScanner` (same pattern grammar) + +`PePatternScanner` searches **Portable Executable (PE)** binaries for a hex pattern (with wildcards). You can scan: + +* a **loaded module** in the current process, or +* a **PE file on disk** (by section name; defaults to **`.text`**). + +--- + +## Pattern string grammar (same as `PatternScanner`) + +* **Hex byte**: two hex digits → one byte (`90`, `4F`, `00`, `ff`). +* **Wildcard byte**: `?` or `??` matches **any byte**. +* **Whitespace**: ignored (use to group tokens). + +✔️ `"48 8B ?? ?? 89"`, `"55 8B EC"`, `"??"` +❌ odd digit counts, non-hex characters (besides `?` and whitespace) + +--- + +## API + +```cpp +namespace omath { + +struct PeSectionScanResult { + std::uint64_t virtual_base_addr; // RVA base of the scanned section (ImageBase + SectionRVA) + std::uint64_t raw_base_addr; // file offset (start of section in the file) + std::ptrdiff_t target_offset; // offset from section base to the first matched byte +}; + +class PePatternScanner final { +public: + // Scan a module already loaded in *this* process. + // module_base_address: HMODULE / ImageBase (e.g., from GetModuleHandle) + // Returns absolute address (process VA) of the first match, or nullopt. + static std::optional + scan_for_pattern_in_loaded_module(const void* module_base_address, + const std::string_view& pattern); + + // Scan a PE file on disk, by section name (default ".text"). + // Returns section bases (virtual + raw) and match offset within the section, or nullopt. + static std::optional + scan_for_pattern_in_file(const std::filesystem::path& path_to_file, + const std::string_view& pattern, + const std::string_view& target_section_name = ".text"); +}; + +} // namespace omath +``` + +--- + +## Return values + +* **Loaded module**: `std::optional` + + * `value()` = **process virtual address** (ImageBase + SectionRVA + match offset). + * `nullopt` = no match or parse/PE error. + +* **File scan**: `std::optional` + + * `virtual_base_addr` = **ImageBase + SectionRVA** of the scanned section (as if mapped). + * `raw_base_addr` = **file offset** of section start. + * `target_offset` = offset from the section base to the **first matched byte**. + * To get addresses: + + * **Virtual (RVA)** of hit = `virtual_base_addr + target_offset` + * **Raw file offset** of hit = `raw_base_addr + target_offset` + +--- + +## Usage examples + +### Scan a loaded module (current process) + +```cpp +#include +#include "pe_pattern_scanner.hpp" + +using omath::PePatternScanner; + +auto hMod = ::GetModuleHandleW(L"kernel32.dll"); +if (hMod) { + auto addr = PePatternScanner::scan_for_pattern_in_loaded_module( + hMod, "48 8B ?? ?? 89" // hex + wildcards + ); + if (addr) { + // Use the absolute process VA: + std::uintptr_t hit_va = *addr; + // ... + } +} +``` + +### Scan a PE file on disk (default section “.text”) + +```cpp +#include "pe_pattern_scanner.hpp" +using omath::PePatternScanner; + +auto res = PePatternScanner::scan_for_pattern_in_file( + R"(C:\Windows\System32\kernel32.dll)", "55 8B EC" +); +if (res) { + auto rva_hit = res->virtual_base_addr + res->target_offset; + auto raw_hit = res->raw_base_addr + res->target_offset; + // rva_hit: where it would be in memory; raw_hit: file offset +} +``` + +### Scan another section (e.g., “.rdata”) + +```cpp +auto res = PePatternScanner::scan_for_pattern_in_file( + "foo.dll", "48 8D 0D ?? ?? ?? ??", ".rdata" +); +``` + +--- + +## Notes & edge cases + +* **PE only**: these functions assume a valid **PE/COFF** layout. Non-PE files or corrupted headers yield `nullopt`. +* **Section name**: `scan_for_pattern_in_file` defaults to **`.text`**; pass a different name to target other sections. +* **Alignment & RVAs**: `virtual_base_addr` is computed from section headers (RVA aligned per section alignment). The returned “virtual” base is suitable for RVA math; the **process VA** returned by the “loaded module” API already includes the image base. +* **Architecture**: works for 32-bit and 64-bit PEs; `std::uintptr_t` size matches the build architecture. +* **Performance**: Pattern matching is **O(N × M)** (sliding window with wildcards). For large images, prefer scanning only necessary sections. +* **Wildcards**: Each `?` matches **one byte** (no nibble masks). If you need `A?`-style nibble wildcards, extend the parser (see `PatternScanner`). +* **Safety**: For loaded modules, you must have access to the memory; scanning read-only sections is fine, but never write. For file scans, ensure the file path is accessible. + +--- + +## Troubleshooting + +* **`nullopt`**: Verify the pattern (valid tokens), correct **section**, and that you’re scanning the intended module/file (check bitness and version). +* **“Loaded module” address math**: If you need an **offset from the module base**, compute `offset = hit_va - reinterpret_cast(module_base_address)`. +* **Multiple matches**: Only the **first** match is returned. If you need all matches, extend the implementation to continue scanning from `target_offset + 1`. + +--- + +## See also + +* `omath::PatternScanner` — raw buffer/iterator scanning with the same pattern grammar. +* `omath::Triangle`, `omath::Vector3` — math types used elsewhere in the library. + +--- + +*Last updated: 31 Oct 2025* diff --git a/docs/utility/section_scan_result.md b/docs/utility/section_scan_result.md new file mode 100644 index 00000000..3b565ef4 --- /dev/null +++ b/docs/utility/section_scan_result.md @@ -0,0 +1,58 @@ +# `omath::SectionScanResult` — File-based pattern scan result + +> Header: `omath/utility/section_scan_result.hpp` +> Namespace: `omath` +> Depends on: ``, `` + +`SectionScanResult` is the return type for file-based pattern scans across all binary formats (PE, ELF, Mach-O). It carries the section's virtual and raw base addresses together with the offset to the matched pattern. + +--- + +## API + +```cpp +namespace omath { + +struct SectionScanResult final { + std::uintptr_t virtual_base_addr; // virtual address base of the scanned section + std::uintptr_t raw_base_addr; // file offset of the section start + std::ptrdiff_t target_offset; // offset from section base to the first matched byte +}; + +} // namespace omath +``` + +--- + +## Computing addresses from a result + +```cpp +omath::SectionScanResult res = /* ... */; + +// Virtual address of the match (as if the binary were loaded at its preferred base) +auto va_hit = res.virtual_base_addr + res.target_offset; + +// Raw file offset of the match +auto raw_hit = res.raw_base_addr + res.target_offset; +``` + +--- + +## Notes + +* `virtual_base_addr` is computed from the section header (RVA for PE, `sh_addr` for ELF, `addr` for Mach-O). +* `raw_base_addr` is the file offset where the section data begins on disk. +* `target_offset` is always relative to the section base — add it to either address to locate the match. + +--- + +## See also + +* [`omath::PePatternScanner`](pe_pattern_scan.md) — PE (Windows) binary scanner. +* [`omath::ElfPatternScanner`](elf_pattern_scan.md) — ELF (Linux) binary scanner. +* [`omath::MachOPatternScanner`](macho_pattern_scan.md) — Mach-O (macOS) binary scanner. +* [`omath::PatternScanner`](pattern_scan.md) — raw buffer/iterator scanning. + +--- + +*Last updated: Feb 2026* diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt new file mode 100644 index 00000000..e8f005cd --- /dev/null +++ b/examples/CMakeLists.txt @@ -0,0 +1,38 @@ +project(examples) + +add_executable(example_projection_matrix_builder example_proj_mat_builder.cpp) +set_target_properties( + example_projection_matrix_builder + PROPERTIES CXX_STANDARD 23 + ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}") +target_link_libraries(example_projection_matrix_builder PRIVATE omath::omath) + +add_executable(example_signature_scan example_signature_scan.cpp) +set_target_properties( + example_signature_scan + PROPERTIES CXX_STANDARD 23 + ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}") +target_link_libraries(example_signature_scan PRIVATE omath::omath) + +add_executable(example_glfw3 example_glfw3.cpp) +set_target_properties( + example_glfw3 + PROPERTIES CXX_STANDARD 23 + ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}") + +find_package(OpenGL) +find_package(GLEW REQUIRED) +find_package(glfw3 CONFIG REQUIRED) +target_link_libraries(example_glfw3 PRIVATE omath::omath GLEW::GLEW glfw OpenGL::OpenGL) + +if(OMATH_ENABLE_VALGRIND) + omath_setup_valgrind(example_projection_matrix_builder) + omath_setup_valgrind(example_signature_scan) + omath_setup_valgrind(example_glfw3) +endif() diff --git a/examples/example_glfw3.cpp b/examples/example_glfw3.cpp new file mode 100644 index 00000000..a37e876c --- /dev/null +++ b/examples/example_glfw3.cpp @@ -0,0 +1,359 @@ +// main.cpp +#include +#include +#include + +// --- OpenGL / windowing --- +#include // GLEW must come before GLFW +#include + +// --- 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>; +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; + } + + std::cout << "GLFW Version: " << glfwGetVersionString() << "\n"; + + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); + // Force GLX context creation API to ensure compatibility with GLEW + glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_NATIVE_CONTEXT_API); + +#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); + + // Check if context is valid using standard GL + const GLubyte* renderer = glGetString(GL_RENDERER); + const GLubyte* version = glGetString(GL_VERSION); + if (renderer && version) { + std::cout << "Renderer: " << renderer << "\n"; + std::cout << "OpenGL version supported: " << version << "\n"; + } else { + std::cerr << "Failed to get GL_RENDERER or GL_VERSION. Context might be invalid.\n"; + } + + // ---------- GLEW init ---------- + glewExperimental = GL_TRUE; + GLenum glewErr = glewInit(); + if (glewErr != GLEW_OK) + { + // Ignore NO_GLX_DISPLAY if we have a valid context + if (glewErr == GLEW_ERROR_NO_GLX_DISPLAY && renderer) { + std::cerr << "GLEW warning: " << glewGetErrorString(glewErr) << " (Ignored because context seems valid)\n"; + } else { + std::cerr << "Failed to initialize GLEW: " << reinterpret_cast(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 vbo; + vbo.reserve(8); + + Vector3 p000{-0.5f, -0.5f, -0.5f}; + Vector3 p001{-0.5f, -0.5f, 0.5f}; + Vector3 p010{-0.5f, 0.5f, -0.5f}; + Vector3 p011{-0.5f, 0.5f, 0.5f}; + Vector3 p100{0.5f, -0.5f, -0.5f}; + Vector3 p101{0.5f, -0.5f, 0.5f}; + Vector3 p110{0.5f, 0.5f, -0.5f}; + Vector3 p111{0.5f, 0.5f, 0.5f}; + + VertexType v0{p000, Vector3{-1, -1, -1}, omath::Vector2{0, 0}}; + VertexType v1{p001, Vector3{-1, -1, 1}, omath::Vector2{0, 1}}; + VertexType v2{p010, Vector3{-1, 1, -1}, omath::Vector2{1, 0}}; + VertexType v3{p011, Vector3{-1, 1, 1}, omath::Vector2{1, 1}}; + VertexType v4{p100, Vector3{1, -1, -1}, omath::Vector2{0, 0}}; + VertexType v5{p101, Vector3{1, -1, 1}, omath::Vector2{0, 1}}; + VertexType v6{p110, Vector3{1, 1, -1}, omath::Vector2{1, 0}}; + VertexType v7{p111, Vector3{1, 1, 1}, omath::Vector2{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::vector 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 flatIndices; + flatIndices.reserve(cube.m_element_buffer_object.size() * 3); + for (const auto& tri : cube.m_element_buffer_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) + 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(SCR_WIDTH), static_cast(SCR_HEIGHT)}; + + Vector3 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(fbW); + viewPort.m_height = static_cast(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(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; +} \ No newline at end of file diff --git a/examples/example_proj_mat_builder.cpp b/examples/example_proj_mat_builder.cpp new file mode 100644 index 00000000..5857e373 --- /dev/null +++ b/examples/example_proj_mat_builder.cpp @@ -0,0 +1,40 @@ +// +// Created by Vlad on 3/19/2025. +// + + +#include +#include +#include + + +int main() +{ + std::println("OMATH Projection Matrix Builder"); + + float fov = 0; + float near = 0; + float far = 0; + float view_port_width = 0; + float view_port_height = 0; + + std::print("Enter camera fov: "); + std::cin >> fov; + + std::print("Enter camera z near: "); + std::cin >> near; + + std::print("Enter camera z far: "); + std::cin >> far; + + std::print("Enter camera screen width: "); + std::cin >> view_port_width; + + std::print("Enter camera screen height: "); + std::cin >> view_port_height; + + const auto mat = + omath::opengl_engine::calc_perspective_projection_matrix(fov, view_port_width / view_port_height, near, far); + + std::print("{}", mat.to_string()); +}; diff --git a/examples/example_signature_scan.cpp b/examples/example_signature_scan.cpp new file mode 100644 index 00000000..e6b22839 --- /dev/null +++ b/examples/example_signature_scan.cpp @@ -0,0 +1,39 @@ +// +// Created by Vlad on 11/8/2025. +// + +#include +#include +#include + +int main() +{ + std::println("OMATH Signature Scanner"); + + std::print("Enter path to PE file: "); + std::string file_path; + std::getline(std::cin, file_path); // allows spaces + + std::print("Enter target section: "); + std::string section; + std::getline(std::cin, section); + + std::print("Enter signature: "); + std::string signature; + std::getline(std::cin, signature); + + std::println("[LOG] Performing scan...."); + + const auto result = omath::PePatternScanner::scan_for_pattern_in_file(file_path, signature, section); + + if (!result) + { + std::println("[ERROR] Scan failed or signature was not found"); + return -1; + } + + std::println("Found at virtual 0x{:x} , raw 0x{:x}", result->virtual_base_addr + result->target_offset, + result->raw_base_addr + result->target_offset); + + return 0; +} \ No newline at end of file diff --git a/extlibs/CMakeLists.txt b/extlibs/CMakeLists.txt deleted file mode 100644 index bf73c7af..00000000 --- a/extlibs/CMakeLists.txt +++ /dev/null @@ -1 +0,0 @@ -add_subdirectory(googletest) \ No newline at end of file diff --git a/extlibs/googletest b/extlibs/googletest deleted file mode 160000 index d83fee13..00000000 --- a/extlibs/googletest +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d83fee138a9ae6cb7c03688a2d08d4043a39815d diff --git a/include/omath/3d_primitives/box.hpp b/include/omath/3d_primitives/box.hpp new file mode 100644 index 00000000..0e1a7e14 --- /dev/null +++ b/include/omath/3d_primitives/box.hpp @@ -0,0 +1,62 @@ +// +// Created by Vlad on 4/18/2025. +// + +#pragma once +#include "mesh.hpp" +#include "omath/engines/opengl_engine/camera.hpp" +#include "omath/engines/opengl_engine/traits/mesh_trait.hpp" +#include "omath/linear_algebra/triangle.hpp" +#include "omath/linear_algebra/vector3.hpp" +#include + +namespace omath::primitives +{ + template + [[nodiscard]] + BoxMeshType create_box(const Vector3& top, const Vector3& bottom, const Vector3& dir_forward, + const Vector3& dir_right, const float ratio = 4.f) noexcept + { + const auto height = top.distance_to(bottom); + const auto side_size = height / ratio; + + // corner layout (0‑3 bottom, 4‑7 top) + std::array, 8> p; + p[0] = bottom + (dir_forward + dir_right) * side_size; // front‑right‑bottom + p[1] = bottom + (dir_forward - dir_right) * side_size; // front‑left‑bottom + p[2] = bottom + (-dir_forward + dir_right) * side_size; // back‑right‑bottom + p[3] = bottom + (-dir_forward - dir_right) * side_size; // back‑left‑bottom + p[4] = top + (dir_forward + dir_right) * side_size; // front‑right‑top + p[5] = top + (dir_forward - dir_right) * side_size; // front‑left‑top + p[6] = top + (-dir_forward + dir_right) * side_size; // back‑right‑top + p[7] = top + (-dir_forward - dir_right) * side_size; // back‑left‑top + + std::array, 12> poly; + + // bottom face (+Y up ⇒ wind CW when viewed from above) + poly[0] = {0, 2, 3}; + poly[1] = {0, 3, 1}; + + // top face + poly[2] = {4, 7, 6}; + poly[3] = {4, 5, 7}; + + // front face + poly[4] = {0, 5, 1}; + poly[5] = {0, 4, 5}; + + // right face + poly[6] = {0, 6, 2}; + poly[7] = {0, 4, 6}; + + // back face + poly[8] = {2, 7, 3}; + poly[9] = {2, 6, 7}; + + // left face + poly[10] = {1, 7, 5}; + poly[11] = {1, 3, 7}; + + return BoxMeshType{std::move(p), std::move(poly)}; + } +} // namespace omath::primitives diff --git a/include/omath/3d_primitives/mesh.hpp b/include/omath/3d_primitives/mesh.hpp new file mode 100644 index 00000000..00a8e504 --- /dev/null +++ b/include/omath/3d_primitives/mesh.hpp @@ -0,0 +1,134 @@ +// +// Created by Vladislav on 09.11.2025. +// +#pragma once +#include "omath/linear_algebra/triangle.hpp" +#include +#include +#include +#include +#include + +namespace omath::primitives +{ + template, class UvT = Vector2> + struct Vertex final + { + using VectorType = VecType; + using UvType = UvT; + VectorType position; + VectorType normal; + UvType uv; + }; + + template concept HasPosition = requires(T vertex) { vertex.position; }; + template concept HasNormal = requires(T vertex) { vertex.normal; }; + template concept HasUv = requires(T vertex) { vertex.uv; }; + + template, + class VboType = std::vector, class EboType = std::vector>> + class Mesh final + { + public: + using VectorType = VertType::VectorType; + using VertexType = VboType::value_type; + + private: + using Vbo = VboType; + using Ebo = EboType; + + public: + Vbo m_vertex_buffer; + Ebo m_element_buffer_object; + + 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 VectorType& new_origin) + { + m_origin = new_origin; + m_to_world_matrix = std::nullopt; + } + + void set_scale(const VectorType& new_scale) + { + m_scale = new_scale; + m_to_world_matrix = std::nullopt; + } + + void set_rotation(const RotationAngles& new_rotation_angles) + { + m_rotation_angles = new_rotation_angles; + m_to_world_matrix = std::nullopt; + } + + [[nodiscard]] + const VectorType& get_origin() const + { + return m_origin; + } + + [[nodiscard]] + const VectorType& get_scale() const + { + return m_scale; + } + + [[nodiscard]] + const RotationAngles& get_rotation_angles() const + { + return m_rotation_angles; + } + + [[nodiscard]] + const Mat4X4& get_to_world_matrix() const + { + if (m_to_world_matrix) + return m_to_world_matrix.value(); + m_to_world_matrix = mat_translation(m_origin) + * MeshTypeTrait::rotation_matrix(m_rotation_angles) + * mat_scale(m_scale); + + return m_to_world_matrix.value(); + } + + [[nodiscard]] + VectorType vertex_position_to_world_space(const Vector3& vertex_position) const + { + auto abs_vec = get_to_world_matrix() + * mat_column_from_vector( + vertex_position); + + return {abs_vec.at(0, 0), abs_vec.at(1, 0), abs_vec.at(2, 0)}; + } + + [[nodiscard]] + Triangle make_face_in_world_space(const Ebo::const_iterator vao_iterator) const + { + if constexpr (HasPosition) + { + 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)}; + } + return {vertex_position_to_world_space(m_vertex_buffer.at(vao_iterator->x)), + vertex_position_to_world_space(m_vertex_buffer.at(vao_iterator->y)), + vertex_position_to_world_space(m_vertex_buffer.at(vao_iterator->z))}; + } + + private: + VectorType m_origin; + VectorType m_scale; + + RotationAngles m_rotation_angles; + + mutable std::optional m_to_world_matrix; + }; +} // namespace omath::primitives \ No newline at end of file diff --git a/include/omath/3d_primitives/plane.hpp b/include/omath/3d_primitives/plane.hpp new file mode 100644 index 00000000..d5c83326 --- /dev/null +++ b/include/omath/3d_primitives/plane.hpp @@ -0,0 +1,32 @@ +// +// Created by Vlad on 8/28/2025. +// + +#pragma once +#include "mesh.hpp" +#include "omath/engines/opengl_engine/camera.hpp" +#include "omath/engines/opengl_engine/mesh.hpp" +#include "omath/engines/opengl_engine/traits/mesh_trait.hpp" +#include "omath/linear_algebra/triangle.hpp" +#include "omath/linear_algebra/vector3.hpp" +#include + +namespace omath::primitives +{ + template + [[nodiscard]] + PlaneMeshType create_plane(const Vector3& vertex_a, const Vector3& vertex_b, + const Vector3& direction, const float size) noexcept + { + const auto second_vertex_a = vertex_a + direction * size; + const auto second_vertex_b = vertex_b + direction * size; + + std::array, 4> grid = {vertex_a, vertex_b, second_vertex_a, second_vertex_b}; + + std::array, 2> poly; + poly[0] = {1, 1, 2}; + poly[1] = {0, 1, 3}; + + return PlaneMeshType(std::move(grid), std::move(poly)); + } +} // namespace omath::primitives diff --git a/include/omath/collision/collider_interface.hpp b/include/omath/collision/collider_interface.hpp new file mode 100644 index 00000000..7047d7ce --- /dev/null +++ b/include/omath/collision/collider_interface.hpp @@ -0,0 +1,23 @@ +// +// Created by Vladislav on 06.12.2025. +// +#pragma once +#include + +namespace omath::collision +{ + template> + 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; + }; +} \ No newline at end of file diff --git a/include/omath/collision/epa_algorithm.hpp b/include/omath/collision/epa_algorithm.hpp new file mode 100644 index 00000000..1328dabb --- /dev/null +++ b/include/omath/collision/epa_algorithm.hpp @@ -0,0 +1,303 @@ +#pragma once +#include "simplex.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace omath::collision +{ + template + concept EpaVector = requires(const V& a, const V& b, float s) { + { a - b } -> std::same_as; + { a.cross(b) } -> std::same_as; + { a.dot(b) } -> std::same_as; + { -a } -> std::same_as; + { a * s } -> std::same_as; + { a / s } -> std::same_as; + }; + + template + class Epa final + { + public: + using VectorType = ColliderInterfaceType::VectorType; + static_assert(EpaVector, "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 solve(const ColliderInterfaceType& a, const ColliderInterfaceType& b, + const Simplex& 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 vertexes = build_initial_polytope_from_simplex(simplex, mem_resource); + + // Initial tetra faces (windings corrected in make_face) + std::pmr::vector 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(vertexes.size()); + out.num_faces = static_cast(faces.size()); + + out.penetration_vector = out.normal * out.depth; + return out; + } + + // Add new vertex + const int new_idx = static_cast(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(vertexes.size()); + out.num_faces = static_cast(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, HeapCmp>; + + [[nodiscard]] + static Heap rebuild_heap(const std::pmr::vector& faces, auto& memory_resource) + { + std::pmr::vector storage{&memory_resource}; + storage.reserve(faces.size()); // optional but recommended + + Heap h{HeapCmp{}, std::move(storage)}; + + for (int i = 0; i < static_cast(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& 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& 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 + [[nodiscard]] + static constexpr bool near_zero_vec(const V& v, const float eps = 1e-7f) + { + return v.dot(v) <= eps * eps; + } + + template + [[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 create_initial_tetra_faces(std::pmr::memory_resource& mem_resource, + const std::pmr::vector& vertexes) + { + std::pmr::vector 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 build_initial_polytope_from_simplex(const Simplex& simplex, + std::pmr::memory_resource& mem_resource) + { + std::pmr::vector 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& faces, const std::pmr::vector& to_delete) + { + auto* mr = faces.get_allocator().resource(); // keep same resource + std::pmr::vector 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 to_delete; + std::pmr::vector boundary; + }; + + static Horizon mark_visible_and_collect_horizon(const std::pmr::vector& faces, const VectorType& p) + { + auto* mr = faces.get_allocator().resource(); + + Horizon horizon{std::pmr::vector(faces.size(), false, mr), std::pmr::vector(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 diff --git a/include/omath/collision/gjk_algorithm.hpp b/include/omath/collision/gjk_algorithm.hpp new file mode 100644 index 00000000..b29bd25e --- /dev/null +++ b/include/omath/collision/gjk_algorithm.hpp @@ -0,0 +1,62 @@ +// +// Created by Vlad on 11/9/2025. +// + +#pragma once +#include "simplex.hpp" + +namespace omath::collision +{ + template + struct GjkHitInfo final + { + bool hit{false}; + Simplex simplex; // valid only if hit == true and size==4 + }; + + template + class GjkAlgorithm final + { + using VectorType = ColliderInterfaceType::VectorType; + + public: + [[nodiscard]] + static VectorType find_support_vertex(const ColliderInterfaceType& collider_a, + const ColliderInterfaceType& collider_b, const VectorType& direction) + { + return collider_a.find_abs_furthest_vertex_position(direction) + - collider_b.find_abs_furthest_vertex_position(-direction); + } + + [[nodiscard]] + static bool is_collide(const ColliderInterfaceType& collider_a, const ColliderInterfaceType& collider_b) + { + return is_collide_with_simplex_info(collider_a, collider_b).hit; + } + + [[nodiscard]] + static GjkHitInfo 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 simplex; + simplex.push_front(support); + + auto direction = -support; + + while (true) + { + support = find_support_vertex(collider_a, collider_b, direction); + + if (support.dot(direction) <= 0.f) + return {false, simplex}; + + simplex.push_front(support); + + if (simplex.handle(direction)) + return {true, simplex}; + } + } + }; +} // namespace omath::collision \ No newline at end of file diff --git a/include/omath/collision/line_tracer.hpp b/include/omath/collision/line_tracer.hpp new file mode 100644 index 00000000..3bba86e4 --- /dev/null +++ b/include/omath/collision/line_tracer.hpp @@ -0,0 +1,110 @@ +// +// Created by Orange on 11/13/2024. +// +#pragma once + +#include "omath/linear_algebra/triangle.hpp" +#include "omath/linear_algebra/vector3.hpp" + +namespace omath::collision +{ + template> + class Ray final + { + public: + using VectorType = T; + VectorType start; + VectorType end; + bool infinite_length = false; + + [[nodiscard]] + constexpr VectorType direction_vector() const noexcept + { + return end - start; + } + + [[nodiscard]] + constexpr VectorType direction_vector_normalized() const noexcept + { + return direction_vector().normalized(); + } + }; + + template> + class LineTracer final + { + using TriangleType = Triangle; + + public: + LineTracer() = delete; + + [[nodiscard]] + constexpr static bool can_trace_line(const RayType& ray, const TriangleType& triangle) noexcept + { + return get_ray_hit_point(ray, triangle) == ray.end; + } + + // Realization of Möller–Trumbore intersection algorithm + // https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm + [[nodiscard]] + constexpr static auto get_ray_hit_point(const RayType& ray, const TriangleType& triangle) noexcept + { + constexpr float k_epsilon = std::numeric_limits::epsilon(); + + const auto side_a = triangle.side_a_vector(); + const auto side_b = triangle.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) + return ray.end; + + const auto inv_det = 1 / det; + const auto t = ray.start - triangle.m_vertex2; + const auto u = t.dot(p) * inv_det; + + if ((u < 0 && std::abs(u) > k_epsilon) || (u > 1 && std::abs(u - 1) > k_epsilon)) + return ray.end; + + const auto q = t.cross(side_a); + // ReSharper disable once CppTooWideScopeInitStatement + const auto v = ray_dir.dot(q) * inv_det; + + if ((v < 0 && std::abs(v) > k_epsilon) || (u + v > 1 && std::abs(u + v - 1) > k_epsilon)) + return ray.end; + + const auto t_hit = side_b.dot(q) * inv_det; + + if (ray.infinite_length && t_hit <= k_epsilon) + return ray.end; + + if (t_hit <= k_epsilon || t_hit > 1 - k_epsilon) + return ray.end; + + return ray.start + ray_dir * t_hit; + } + + template + [[nodiscard]] + constexpr static auto get_ray_hit_point(const RayType& ray, const MeshType& mesh) noexcept + { + auto mesh_hit = ray.end; + + const auto begin = mesh.m_element_buffer_object.cbegin(); + const auto end = mesh.m_element_buffer_object.cend(); + for (auto current = begin; current < end; current = std::next(current)) + { + const auto face = mesh.make_face_in_world_space(current); + + auto ray_stop_point = get_ray_hit_point(ray, face); + if (ray_stop_point.distance_to(ray.start) < mesh_hit.distance_to(ray.start)) + mesh_hit = ray_stop_point; + } + + return mesh_hit; + } + }; +} // namespace omath::collision diff --git a/include/omath/collision/mesh_collider.hpp b/include/omath/collision/mesh_collider.hpp new file mode 100644 index 00000000..a9a88704 --- /dev/null +++ b/include/omath/collision/mesh_collider.hpp @@ -0,0 +1,55 @@ +// +// Created by Vlad on 11/9/2025. +// + +#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 MeshCollider final : public ColliderInterface + { +#ifdef OMATH_BUILD_TESTS + friend UnitTestColider_FindFurthestVertex_Test; +#endif + public: + using VertexType = MeshType::VertexType; + using VectorType = MeshType::VertexType::VectorType; + explicit MeshCollider(MeshType mesh): m_mesh(std::move(mesh)) + { + } + + [[nodiscard]] + VectorType find_abs_furthest_vertex_position(const VectorType& direction) const override + { + return m_mesh.vertex_position_to_world_space(find_furthest_vertex(direction).position); + } + + [[nodiscard]] + const VectorType& get_origin() const override + { + 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 \ No newline at end of file diff --git a/include/omath/collision/simplex.hpp b/include/omath/collision/simplex.hpp new file mode 100644 index 00000000..c7c99e73 --- /dev/null +++ b/include/omath/collision/simplex.hpp @@ -0,0 +1,250 @@ +#pragma once +#include "omath/linear_algebra/vector3.hpp" +#include +#include +#include +#include + +namespace omath::collision +{ + + // Minimal structural contract for the vector type used by GJK. + template + concept GjkVector = requires(const V& a, const V& b) { + { -a } -> std::same_as; + { a - b } -> std::same_as; + { a.cross(b) } -> std::same_as; + { a.point_to_same_direction(b) } -> std::same_as; + }; + + template> + class Simplex final + { + std::array m_points{}; + std::size_t m_size{0}; + + public: + static constexpr std::size_t capacity = 4; + + constexpr Simplex() = default; + + constexpr Simplex& operator=(std::initializer_list list) noexcept + { + assert(list.size() <= capacity && "Simplex can have at most 4 points"); + m_size = 0; + for (const auto& p : list) + m_points[m_size++] = p; + return *this; + } + + constexpr void push_front(const VectorType& p) noexcept + { + const std::size_t limit = (m_size < capacity) ? m_size : capacity - 1; + for (std::size_t i = limit; i > 0; --i) + m_points[i] = m_points[i - 1]; + m_points[0] = p; + if (m_size < capacity) + ++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]; + } + + [[nodiscard]] constexpr std::size_t size() const noexcept + { + return m_size; + } + + [[nodiscard]] constexpr bool empty() const noexcept + { + return m_size == 0; + } + + [[nodiscard]] constexpr const VectorType& front() const noexcept + { + return m_points[0]; + } + + [[nodiscard]] constexpr const VectorType& back() const noexcept + { + return m_points[m_size - 1]; + } + + [[nodiscard]] constexpr const VectorType* data() const noexcept + { + return m_points.data(); + } + + [[nodiscard]] constexpr auto begin() const noexcept + { + return m_points.begin(); + } + + [[nodiscard]] constexpr auto end() const noexcept + { + return m_points.begin() + m_size; + } + + constexpr void clear() noexcept + { + m_size = 0; + } + + // GJK step: updates simplex + next search direction. + // Returns true iff the origin lies inside the tetrahedron. + [[nodiscard]] constexpr bool handle(VectorType& direction) noexcept + { + switch (m_size) + { + case 0: + return false; + case 1: + return handle_point(direction); + case 2: + return handle_line(direction); + case 3: + return handle_triangle(direction); + case 4: + return handle_tetrahedron(direction); + default: + std::unreachable(); + } + } + + private: + [[nodiscard]] constexpr bool handle_point(VectorType& direction) noexcept + { + const auto& a = m_points[0]; + direction = -a; + return false; + } + + template + [[nodiscard]] + static constexpr bool near_zero(const V& v, const float eps = 1e-7f) noexcept + { + return v.dot(v) <= eps * eps; + } + + template + [[nodiscard]] + static constexpr V any_perp(const V& v) + { + 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) noexcept + { + const auto& a = m_points[0]; + const auto& b = m_points[1]; + + const auto ab = b - a; + const auto ao = -a; + + if (ab.point_to_same_direction(ao)) + { + // ReSharper disable once CppTooWideScopeInitStatement + auto n = ab.cross(ao); // Needed to valid handle collision if colliders placed at same origin pos + direction = near_zero(n) ? any_perp(ab) : n.cross(ab); + return false; + } + *this = {a}; + direction = ao; + return false; + } + + [[nodiscard]] constexpr bool handle_triangle(VectorType& direction) noexcept + { + const auto& a = m_points[0]; + const auto& b = m_points[1]; + const auto& c = m_points[2]; + + const auto ab = b - a; + const auto ac = c - a; + const auto ao = -a; + + const auto abc = ab.cross(ac); + + // Region AC + if (abc.cross(ac).point_to_same_direction(ao)) + { + if (ac.point_to_same_direction(ao)) + { + *this = {a, c}; + direction = ac.cross(ao).cross(ac); + return false; + } + *this = {a, b}; + return handle_line(direction); + } + + // Region AB + if (ab.cross(abc).point_to_same_direction(ao)) + { + *this = {a, b}; + return handle_line(direction); + } + + // Above or below triangle + if (abc.point_to_same_direction(ao)) + { + direction = abc; + } + else + { + *this = {a, c, b}; // flip winding + direction = -abc; + } + return false; + } + + [[nodiscard]] constexpr bool handle_tetrahedron(VectorType& direction) noexcept + { + const auto& a = m_points[0]; + const auto& b = m_points[1]; + const auto& c = m_points[2]; + const auto& d = m_points[3]; + + const auto ab = b - a; + const auto ac = c - a; + const auto ad = d - a; + const auto ao = -a; + + const auto abc = ab.cross(ac); + const auto acd = ac.cross(ad); + const auto adb = ad.cross(ab); + + if (abc.point_to_same_direction(ao)) + { + *this = {a, b, c}; + return handle_triangle(direction); + } + if (acd.point_to_same_direction(ao)) + { + *this = {a, c, d}; + return handle_triangle(direction); + } + if (adb.point_to_same_direction(ao)) + { + *this = {a, d, b}; + return handle_triangle(direction); + } + // Origin inside tetrahedron + return true; + } + }; + +} // namespace omath::collision diff --git a/include/omath/containers/encrypted_variable.hpp b/include/omath/containers/encrypted_variable.hpp new file mode 100644 index 00000000..3d1c250f --- /dev/null +++ b/include/omath/containers/encrypted_variable.hpp @@ -0,0 +1,201 @@ +// +// Created by Vladislav on 04.01.2026. +// +#pragma once +#include +#include +#include +#include +#ifdef OMATH_ENABLE_FORCE_INLINE +#ifdef _MSC_VER +#define OMATH_FORCE_INLINE __forceinline +#else +#define OMATH_FORCE_INLINE __attribute__((always_inline)) inline +#endif +#else +#define OMATH_FORCE_INLINE +#endif + +namespace omath::detail +{ + [[nodiscard]] + consteval std::uint64_t fnv1a_64(const char* s) + { + std::uint64_t h = 14695981039346656037ull; + while (*s) + { + h ^= static_cast(*s++); + h *= 1099511628211ull; + } + return h; + } + + // SplitMix64 mixer (good quality for seeding / scrambling) + [[nodiscard]] + consteval std::uint64_t splitmix64(std::uint64_t x) + { + x += 0x9E3779B97F4A7C15ull; + x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9ull; + x = (x ^ (x >> 27)) * 0x94D049BB133111EBull; + return x ^ (x >> 31); + } + + // Choose your policy: + // - If you want reproducible builds, REMOVE __DATE__/__TIME__. + // - If you want "different each build", keep them. + [[nodiscard]] + consteval std::uint64_t base_seed() + { + std::uint64_t h = 0; + h ^= fnv1a_64(__FILE__); + h ^= splitmix64(fnv1a_64(__DATE__)); + h ^= splitmix64(fnv1a_64(__TIME__)); + return splitmix64(h); + } + + // Produce a "random" 64-bit value for a given stream index (compile-time) + template + [[nodiscard]] + consteval std::uint64_t rand_u64() + { + // Stream is usually __COUNTER__ so each call site differs + return splitmix64(base_seed() + 0xD1B54A32D192ED03ull * (Stream + 1)); + } + + [[nodiscard]] + consteval std::uint64_t bounded_u64(const std::uint64_t x, const std::uint64_t bound) + { + return (x * bound) >> 64; + } + + template + [[nodiscard]] + consteval std::int64_t rand_uint8_t() + { + static_assert(Lo <= Hi); + const std::uint64_t span = static_cast(Hi - Lo) + 1ull; + const std::uint64_t r = rand_u64(); + return static_cast(bounded_u64(r, span)) + Lo; + } + [[nodiscard]] + consteval std::uint64_t rand_u64(const std::uint64_t seed, const std::uint64_t i) + { + return splitmix64(seed + 0xD1B54A32D192ED03ull * (i + 1ull)); + } + + // Convert to int (uses low 32 bits; you can also use high bits if you prefer) + [[nodiscard]] + consteval std::uint8_t rand_uint8t(const std::uint64_t seed, const std::uint64_t i) + { + return static_cast(rand_u64(seed, i)); // narrowing is fine/deterministic + } + template + [[nodiscard]] + consteval std::array make_array_impl(std::index_sequence) + { + return {rand_uint8t(Seed, static_cast(I))...}; + } + + template + [[nodiscard]] + consteval std::array make_array() + { + return make_array_impl(std::make_index_sequence{}); + } +} // namespace omath::detail + +namespace omath +{ + template + class VarAnchor; + + template key> + class EncryptedVariable final + { + using value_type = std::remove_cvref_t; + + bool m_is_encrypted{}; + value_type m_data{}; + + OMATH_FORCE_INLINE constexpr void xor_contained_var_by_key() + { + // Safe, keeps const-correctness, and avoids reinterpret_cast issues + auto bytes = std::as_writable_bytes(std::span{&m_data, 1}); + + for (std::size_t i = 0; i < bytes.size(); ++i) + { + const std::uint8_t k = static_cast(key[i % key_size] + (i * key_size)); + bytes[i] ^= static_cast(k); + } + } + + public: + OMATH_FORCE_INLINE constexpr explicit EncryptedVariable(const value_type& data) + : m_is_encrypted(false), m_data(data) + { + encrypt(); + } + + [[nodiscard]] constexpr bool is_encrypted() const + { + return m_is_encrypted; + } + + OMATH_FORCE_INLINE constexpr void decrypt() + { + if (!m_is_encrypted) + return; + xor_contained_var_by_key(); + m_is_encrypted = false; + } + + OMATH_FORCE_INLINE constexpr void encrypt() + { + if (m_is_encrypted) + return; + xor_contained_var_by_key(); + m_is_encrypted = true; + } + + [[nodiscard]] OMATH_FORCE_INLINE constexpr value_type& value() + { + return m_data; + } + [[nodiscard]] OMATH_FORCE_INLINE constexpr const value_type& value() const + { + return m_data; + } + + constexpr OMATH_FORCE_INLINE ~EncryptedVariable() + { + decrypt(); + } + + [[nodiscard]] constexpr OMATH_FORCE_INLINE auto drop_anchor() + { + return VarAnchor{*this}; + } + }; + + template + class VarAnchor final + { + public: + // ReSharper disable once CppNonExplicitConvertingConstructor + OMATH_FORCE_INLINE constexpr VarAnchor(EncryptedVarType& var): m_var(var) + { + m_var.decrypt(); + } + OMATH_FORCE_INLINE constexpr ~VarAnchor() + { + m_var.encrypt(); + } + + private: + EncryptedVarType& m_var; + }; +} // namespace omath + +#define OMATH_CT_RAND_ARRAY_BYTE(N) \ + (::omath::detail::make_array<(N), (::omath::detail::base_seed() ^ static_cast(__COUNTER__))>()) +#define OMATH_DEF_CRYPT_VAR(TYPE, KEY_SIZE) omath::EncryptedVariable \ No newline at end of file diff --git a/include/omath/engines/frostbite_engine/camera.hpp b/include/omath/engines/frostbite_engine/camera.hpp new file mode 100644 index 00000000..e16c8b66 --- /dev/null +++ b/include/omath/engines/frostbite_engine/camera.hpp @@ -0,0 +1,13 @@ +// +// Created by Vlad on 3/22/2025. +// + +#pragma once +#include "omath/engines/frostbite_engine/constants.hpp" +#include "omath/projection/camera.hpp" +#include "traits/camera_trait.hpp" + +namespace omath::frostbite_engine +{ + using Camera = projection::Camera; +} // namespace omath::unity_engine \ No newline at end of file diff --git a/include/omath/engines/frostbite_engine/constants.hpp b/include/omath/engines/frostbite_engine/constants.hpp new file mode 100644 index 00000000..8c0aff11 --- /dev/null +++ b/include/omath/engines/frostbite_engine/constants.hpp @@ -0,0 +1,25 @@ +// +// Created by Vlad on 10/21/2025. +// + +#pragma once +#include "omath/linear_algebra/mat.hpp" +#include "omath/linear_algebra/vector3.hpp" +#include +#include + +namespace omath::frostbite_engine +{ + constexpr Vector3 k_abs_up = {0, 1, 0}; + constexpr Vector3 k_abs_right = {1, 0, 0}; + constexpr Vector3 k_abs_forward = {0, 0, 1}; + + using Mat4X4 = Mat<4, 4, float, MatStoreType::ROW_MAJOR>; + using Mat3X3 = Mat<4, 4, float, MatStoreType::ROW_MAJOR>; + using Mat1X3 = Mat<1, 3, float, MatStoreType::ROW_MAJOR>; + using PitchAngle = Angle; + using YawAngle = Angle; + using RollAngle = Angle; + + using ViewAngles = omath::ViewAngles; +} // namespace omath::frostbite_engine \ No newline at end of file diff --git a/include/omath/engines/frostbite_engine/formulas.hpp b/include/omath/engines/frostbite_engine/formulas.hpp new file mode 100644 index 00000000..89a92746 --- /dev/null +++ b/include/omath/engines/frostbite_engine/formulas.hpp @@ -0,0 +1,26 @@ +// +// Created by Vlad on 3/22/2025. +// + +#pragma once +#include "omath/engines/frostbite_engine/constants.hpp" + +namespace omath::frostbite_engine +{ + [[nodiscard]] + Vector3 forward_vector(const ViewAngles& angles) noexcept; + + [[nodiscard]] + Vector3 right_vector(const ViewAngles& angles) noexcept; + + [[nodiscard]] + Vector3 up_vector(const ViewAngles& angles) noexcept; + + [[nodiscard]] Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3& cam_origin) noexcept; + + [[nodiscard]] + Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept; + + [[nodiscard]] + Mat4X4 calc_perspective_projection_matrix(float field_of_view, float aspect_ratio, float near, float far) noexcept; +} // namespace omath::unity_engine diff --git a/include/omath/engines/frostbite_engine/mesh.hpp b/include/omath/engines/frostbite_engine/mesh.hpp new file mode 100644 index 00000000..7c3d7277 --- /dev/null +++ b/include/omath/engines/frostbite_engine/mesh.hpp @@ -0,0 +1,12 @@ +// +// Created by Vladislav on 09.11.2025. +// +#pragma once +#include "constants.hpp" +#include "omath/3d_primitives/mesh.hpp" +#include "traits/mesh_trait.hpp" + +namespace omath::frostbite_engine +{ + using Mesh = primitives::Mesh; +} \ No newline at end of file diff --git a/include/omath/engines/frostbite_engine/primitives.hpp b/include/omath/engines/frostbite_engine/primitives.hpp new file mode 100644 index 00000000..3398d39b --- /dev/null +++ b/include/omath/engines/frostbite_engine/primitives.hpp @@ -0,0 +1,17 @@ +// +// Created by Vladislav on 27.01.2026. +// + +#pragma once +#include "mesh.hpp" +#include "omath/engines/frostbite_engine/traits/mesh_trait.hpp" +#include "omath/linear_algebra/vector3.hpp" +#include +namespace omath::frostbite_engine +{ + using BoxMesh = primitives::Mesh, std::array, 8>, + std::array, 12>>; + + using PlaneMesh = primitives::Mesh, + std::array, 4>, std::array, 2>>; +} // namespace omath::frostbite_engine \ No newline at end of file diff --git a/include/omath/engines/frostbite_engine/traits/camera_trait.hpp b/include/omath/engines/frostbite_engine/traits/camera_trait.hpp new file mode 100644 index 00000000..8152178d --- /dev/null +++ b/include/omath/engines/frostbite_engine/traits/camera_trait.hpp @@ -0,0 +1,24 @@ +// +// Created by Vlad on 8/10/2025. +// + +#pragma once +#include "omath/engines/frostbite_engine/formulas.hpp" +#include "omath/projection/camera.hpp" + +namespace omath::frostbite_engine +{ + class CameraTrait final + { + public: + [[nodiscard]] + static ViewAngles calc_look_at_angle(const Vector3& cam_origin, const Vector3& look_at) noexcept; + + [[nodiscard]] + static Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3& cam_origin) noexcept; + [[nodiscard]] + static Mat4X4 calc_projection_matrix(const projection::FieldOfView& fov, const projection::ViewPort& view_port, + float near, float far) noexcept; + }; + +} // namespace omath::unreal_engine \ No newline at end of file diff --git a/include/omath/engines/frostbite_engine/traits/mesh_trait.hpp b/include/omath/engines/frostbite_engine/traits/mesh_trait.hpp new file mode 100644 index 00000000..e113c4ea --- /dev/null +++ b/include/omath/engines/frostbite_engine/traits/mesh_trait.hpp @@ -0,0 +1,19 @@ +// +// Created by Vladislav on 09.11.2025. +// +#pragma once +#include +#include + +namespace omath::frostbite_engine +{ + class MeshTrait final + { + public: + [[nodiscard]] + static Mat4X4 rotation_matrix(const ViewAngles& rotation) + { + return frostbite_engine::rotation_matrix(rotation); + } + }; +} // namespace omath::frostbite_engine \ No newline at end of file diff --git a/include/omath/engines/frostbite_engine/traits/pred_engine_trait.hpp b/include/omath/engines/frostbite_engine/traits/pred_engine_trait.hpp new file mode 100644 index 00000000..70314f7b --- /dev/null +++ b/include/omath/engines/frostbite_engine/traits/pred_engine_trait.hpp @@ -0,0 +1,76 @@ +// +// Created by Vlad on 8/6/2025. +// +#pragma once +#include "omath/engines/frostbite_engine/formulas.hpp" +#include "omath/projectile_prediction/projectile.hpp" +#include "omath/projectile_prediction/target.hpp" +#include + +namespace omath::frostbite_engine +{ + class PredEngineTrait final + { + public: + constexpr static Vector3 predict_projectile_position(const projectile_prediction::Projectile& projectile, + const float pitch, const float yaw, + const float time, const float gravity) noexcept + { + auto current_pos = projectile.m_origin + + forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw), + RollAngle::from_degrees(0)}) + * projectile.m_launch_speed * time; + current_pos.y -= (gravity * projectile.m_gravity_scale) * (time * time) * 0.5f; + + return current_pos; + } + [[nodiscard]] + static constexpr Vector3 predict_target_position(const projectile_prediction::Target& target, + const float time, const float gravity) noexcept + { + auto predicted = target.m_origin + target.m_velocity * time; + + if (target.m_is_airborne) + predicted.y -= gravity * (time * time) * 0.5f; + + return predicted; + } + [[nodiscard]] + static float calc_vector_2d_distance(const Vector3& delta) noexcept + { + return std::sqrt(delta.x * delta.x + delta.z * delta.z); + } + + [[nodiscard]] + constexpr static float get_vector_height_coordinate(const Vector3& vec) noexcept + { + return vec.y; + } + + [[nodiscard]] + static Vector3 calc_viewpoint_from_angles(const projectile_prediction::Projectile& projectile, + Vector3 predicted_target_position, + const std::optional projectile_pitch) noexcept + { + const auto delta2d = calc_vector_2d_distance(predicted_target_position - projectile.m_origin); + const auto height = delta2d * std::tan(angles::degrees_to_radians(projectile_pitch.value())); + + return {predicted_target_position.x, predicted_target_position.y + height, projectile.m_origin.z}; + } + // Due to specification of maybe_calculate_projectile_launch_pitch_angle, pitch angle must be: + // 89 look up, -89 look down + [[nodiscard]] + static float calc_direct_pitch_angle(const Vector3& origin, const Vector3& view_to) noexcept + { + const auto direction = (view_to - origin).normalized(); + return angles::radians_to_degrees(std::asin(direction.y)); + } + [[nodiscard]] + static float calc_direct_yaw_angle(const Vector3& origin, const Vector3& view_to) noexcept + { + const auto direction = (view_to - origin).normalized(); + + return angles::radians_to_degrees(std::atan2(direction.x, direction.z)); + }; + }; +} // namespace omath::unity_engine diff --git a/include/omath/engines/iw_engine/camera.hpp b/include/omath/engines/iw_engine/camera.hpp new file mode 100644 index 00000000..2efe29b1 --- /dev/null +++ b/include/omath/engines/iw_engine/camera.hpp @@ -0,0 +1,13 @@ +// +// Created by Vlad on 3/17/2025. +// + +#pragma once +#include "omath/engines/iw_engine/constants.hpp" +#include "omath/projection/camera.hpp" +#include "traits/camera_trait.hpp" + +namespace omath::iw_engine +{ + using Camera = projection::Camera; +} // namespace omath::iw_engine \ No newline at end of file diff --git a/include/omath/engines/iw_engine/constants.hpp b/include/omath/engines/iw_engine/constants.hpp new file mode 100644 index 00000000..a9183fcf --- /dev/null +++ b/include/omath/engines/iw_engine/constants.hpp @@ -0,0 +1,25 @@ +// +// Created by Vlad on 3/17/2025. +// + +#pragma once +#include "omath/linear_algebra/mat.hpp" +#include "omath/linear_algebra/vector3.hpp" +#include +#include + +namespace omath::iw_engine +{ + constexpr Vector3 k_abs_up = {0, 0, 1}; + constexpr Vector3 k_abs_right = {0, -1, 0}; + constexpr Vector3 k_abs_forward = {1, 0, 0}; + + using Mat4X4 = Mat<4, 4, float, MatStoreType::ROW_MAJOR>; + using Mat3X3 = Mat<4, 4, float, MatStoreType::ROW_MAJOR>; + using Mat1X3 = Mat<1, 3, float, MatStoreType::ROW_MAJOR>; + using PitchAngle = Angle; + using YawAngle = Angle; + using RollAngle = Angle; + + using ViewAngles = omath::ViewAngles; +} // namespace omath::iw_engine \ No newline at end of file diff --git a/include/omath/engines/iw_engine/formulas.hpp b/include/omath/engines/iw_engine/formulas.hpp new file mode 100644 index 00000000..433987da --- /dev/null +++ b/include/omath/engines/iw_engine/formulas.hpp @@ -0,0 +1,26 @@ +// +// Created by Vlad on 3/17/2025. +// + +#pragma once +#include "omath/engines/iw_engine/constants.hpp" + +namespace omath::iw_engine +{ + [[nodiscard]] + Vector3 forward_vector(const ViewAngles& angles) noexcept; + + [[nodiscard]] + Vector3 right_vector(const ViewAngles& angles) noexcept; + + [[nodiscard]] + Vector3 up_vector(const ViewAngles& angles) noexcept; + + [[nodiscard]] + Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept; + + [[nodiscard]] Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3& cam_origin) noexcept; + + [[nodiscard]] + Mat4X4 calc_perspective_projection_matrix(float field_of_view, float aspect_ratio, float near, float far) noexcept; +} // namespace omath::iw_engine diff --git a/include/omath/engines/iw_engine/mesh.hpp b/include/omath/engines/iw_engine/mesh.hpp new file mode 100644 index 00000000..d7dcc900 --- /dev/null +++ b/include/omath/engines/iw_engine/mesh.hpp @@ -0,0 +1,12 @@ +// +// Created by Vladislav on 09.11.2025. +// +#pragma once +#include "constants.hpp" +#include "omath/3d_primitives/mesh.hpp" +#include "traits/mesh_trait.hpp" + +namespace omath::iw_engine +{ + using Mesh = primitives::Mesh; +} \ No newline at end of file diff --git a/include/omath/engines/iw_engine/primitives.hpp b/include/omath/engines/iw_engine/primitives.hpp new file mode 100644 index 00000000..f95d46c6 --- /dev/null +++ b/include/omath/engines/iw_engine/primitives.hpp @@ -0,0 +1,17 @@ +// +// Created by Vladislav on 27.01.2026. +// + +#pragma once +#include "mesh.hpp" +#include "omath/engines/iw_engine/traits/mesh_trait.hpp" +#include "omath/linear_algebra/vector3.hpp" +#include +namespace omath::iw_engine +{ + using BoxMesh = primitives::Mesh, std::array, 8>, + std::array, 12>>; + + using PlaneMesh = primitives::Mesh, + std::array, 4>, std::array, 2>>; +} // namespace omath::iw_engine \ No newline at end of file diff --git a/include/omath/engines/iw_engine/traits/camera_trait.hpp b/include/omath/engines/iw_engine/traits/camera_trait.hpp new file mode 100644 index 00000000..88c21569 --- /dev/null +++ b/include/omath/engines/iw_engine/traits/camera_trait.hpp @@ -0,0 +1,24 @@ +// +// Created by Vlad on 8/10/2025. +// + +#pragma once +#include "omath/engines/iw_engine/constants.hpp" +#include "omath/projection/camera.hpp" + +namespace omath::iw_engine +{ + class CameraTrait final + { + public: + [[nodiscard]] + static ViewAngles calc_look_at_angle(const Vector3& cam_origin, const Vector3& look_at) noexcept; + + [[nodiscard]] + static Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3& cam_origin) noexcept; + [[nodiscard]] + static Mat4X4 calc_projection_matrix(const projection::FieldOfView& fov, const projection::ViewPort& view_port, + float near, float far) noexcept; + }; + +} // namespace omath::iw_engine \ No newline at end of file diff --git a/include/omath/engines/iw_engine/traits/mesh_trait.hpp b/include/omath/engines/iw_engine/traits/mesh_trait.hpp new file mode 100644 index 00000000..4cbc5c95 --- /dev/null +++ b/include/omath/engines/iw_engine/traits/mesh_trait.hpp @@ -0,0 +1,19 @@ +// +// Created by Vladislav on 09.11.2025. +// +#pragma once +#include +#include + +namespace omath::iw_engine +{ + class MeshTrait final + { + public: + [[nodiscard]] + static Mat4X4 rotation_matrix(const ViewAngles& rotation) + { + return iw_engine::rotation_matrix(rotation); + } + }; +} // namespace omath::iw_engine \ No newline at end of file diff --git a/include/omath/engines/iw_engine/traits/pred_engine_trait.hpp b/include/omath/engines/iw_engine/traits/pred_engine_trait.hpp new file mode 100644 index 00000000..7961268a --- /dev/null +++ b/include/omath/engines/iw_engine/traits/pred_engine_trait.hpp @@ -0,0 +1,79 @@ +// +// Created by Vlad on 8/6/2025. +// +#pragma once + +#include "omath/engines/iw_engine/formulas.hpp" +#include "omath/projectile_prediction/projectile.hpp" +#include "omath/projectile_prediction/target.hpp" +#include + +namespace omath::iw_engine +{ + class PredEngineTrait final + { + public: + constexpr static Vector3 predict_projectile_position(const projectile_prediction::Projectile& projectile, + const float pitch, const float yaw, + const float time, const float gravity) noexcept + { + auto current_pos = projectile.m_origin + + forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw), + RollAngle::from_degrees(0)}) + * projectile.m_launch_speed * time; + current_pos.z -= (gravity * projectile.m_gravity_scale) * (time * time) * 0.5f; + + return current_pos; + } + [[nodiscard]] + static constexpr Vector3 predict_target_position(const projectile_prediction::Target& target, + const float time, const float gravity) noexcept + { + auto predicted = target.m_origin + target.m_velocity * time; + + if (target.m_is_airborne) + predicted.z -= gravity * (time * time) * 0.5f; + + return predicted; + } + [[nodiscard]] + static float calc_vector_2d_distance(const Vector3& delta) noexcept + { + return std::sqrt(delta.x * delta.x + delta.y * delta.y); + } + + [[nodiscard]] + constexpr static float get_vector_height_coordinate(const Vector3& vec) noexcept + { + return vec.z; + } + + [[nodiscard]] + static Vector3 calc_viewpoint_from_angles(const projectile_prediction::Projectile& projectile, + Vector3 predicted_target_position, + const std::optional projectile_pitch) noexcept + { + const auto delta2d = calc_vector_2d_distance(predicted_target_position - projectile.m_origin); + const auto height = delta2d * std::tan(angles::degrees_to_radians(projectile_pitch.value())); + + return {predicted_target_position.x, predicted_target_position.y, projectile.m_origin.z + height}; + } + // Due to specification of maybe_calculate_projectile_launch_pitch_angle, pitch angle must be: + // 89 look up, -89 look down + [[nodiscard]] + static float calc_direct_pitch_angle(const Vector3& origin, const Vector3& view_to) noexcept + { + const auto distance = origin.distance_to(view_to); + const auto delta = view_to - origin; + + return angles::radians_to_degrees(std::asin(delta.z / distance)); + } + [[nodiscard]] + static float calc_direct_yaw_angle(const Vector3& origin, const Vector3& view_to) noexcept + { + const auto delta = view_to - origin; + + return angles::radians_to_degrees(std::atan2(delta.y, delta.x)); + }; + }; +} // namespace omath::iw_engine \ No newline at end of file diff --git a/include/omath/engines/opengl_engine/camera.hpp b/include/omath/engines/opengl_engine/camera.hpp new file mode 100644 index 00000000..e69ee340 --- /dev/null +++ b/include/omath/engines/opengl_engine/camera.hpp @@ -0,0 +1,12 @@ +// +// Created by Orange on 12/23/2024. +// +#pragma once +#include "omath/engines/opengl_engine/constants.hpp" +#include "omath/projection/camera.hpp" +#include "traits/camera_trait.hpp" + +namespace omath::opengl_engine +{ + using Camera = projection::Camera; +} // namespace omath::opengl_engine \ No newline at end of file diff --git a/include/omath/engines/opengl_engine/constants.hpp b/include/omath/engines/opengl_engine/constants.hpp new file mode 100644 index 00000000..a4faa0da --- /dev/null +++ b/include/omath/engines/opengl_engine/constants.hpp @@ -0,0 +1,25 @@ +// +// Created by Orange on 12/23/2024. +// +#pragma once + +#include "omath/linear_algebra/mat.hpp" +#include "omath/linear_algebra/vector3.hpp" +#include +#include + +namespace omath::opengl_engine +{ + constexpr Vector3 k_abs_up = {0, 1, 0}; + constexpr Vector3 k_abs_right = {1, 0, 0}; + constexpr Vector3 k_abs_forward = {0, 0, -1}; + + using Mat4X4 = Mat<4, 4, float, MatStoreType::COLUMN_MAJOR>; + using Mat3X3 = Mat<4, 4, float, MatStoreType::COLUMN_MAJOR>; + using Mat1X3 = Mat<1, 3, float, MatStoreType::COLUMN_MAJOR>; + using PitchAngle = Angle; + using YawAngle = Angle; + using RollAngle = Angle; + + using ViewAngles = omath::ViewAngles; +} // namespace omath::opengl_engine \ No newline at end of file diff --git a/include/omath/engines/opengl_engine/formulas.hpp b/include/omath/engines/opengl_engine/formulas.hpp new file mode 100644 index 00000000..31714f56 --- /dev/null +++ b/include/omath/engines/opengl_engine/formulas.hpp @@ -0,0 +1,26 @@ +// +// Created by Orange on 12/23/2024. +// +#pragma once +#include "omath/engines/opengl_engine/constants.hpp" + + +namespace omath::opengl_engine +{ + [[nodiscard]] + Vector3 forward_vector(const ViewAngles& angles) noexcept; + + [[nodiscard]] + Vector3 right_vector(const ViewAngles& angles) noexcept; + + [[nodiscard]] + Vector3 up_vector(const ViewAngles& angles) noexcept; + + [[nodiscard]] Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3& cam_origin) noexcept; + + [[nodiscard]] + Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept; + + [[nodiscard]] + Mat4X4 calc_perspective_projection_matrix(float field_of_view, float aspect_ratio, float near, float far) noexcept; +} // namespace omath::opengl_engine diff --git a/include/omath/engines/opengl_engine/mesh.hpp b/include/omath/engines/opengl_engine/mesh.hpp new file mode 100644 index 00000000..f8f6b099 --- /dev/null +++ b/include/omath/engines/opengl_engine/mesh.hpp @@ -0,0 +1,12 @@ +// +// Created by Vladislav on 09.11.2025. +// +#pragma once +#include "constants.hpp" +#include "omath/3d_primitives/mesh.hpp" +#include "traits/mesh_trait.hpp" + +namespace omath::opengl_engine +{ + using Mesh = primitives::Mesh; +} \ No newline at end of file diff --git a/include/omath/engines/opengl_engine/primitives.hpp b/include/omath/engines/opengl_engine/primitives.hpp new file mode 100644 index 00000000..9390237b --- /dev/null +++ b/include/omath/engines/opengl_engine/primitives.hpp @@ -0,0 +1,17 @@ +// +// Created by Vladislav on 27.01.2026. +// + +#pragma once +#include "mesh.hpp" +#include "omath/engines/opengl_engine/traits/mesh_trait.hpp" +#include "omath/linear_algebra/vector3.hpp" +#include +namespace omath::opengl_engine +{ + using BoxMesh = primitives::Mesh, std::array, 8>, + std::array, 12>>; + + using PlaneMesh = primitives::Mesh, + std::array, 4>, std::array, 2>>; +} // namespace omath::opengl_engine \ No newline at end of file diff --git a/include/omath/engines/opengl_engine/traits/camera_trait.hpp b/include/omath/engines/opengl_engine/traits/camera_trait.hpp new file mode 100644 index 00000000..3fb57c07 --- /dev/null +++ b/include/omath/engines/opengl_engine/traits/camera_trait.hpp @@ -0,0 +1,24 @@ +// +// Created by Vlad on 8/10/2025. +// + +#pragma once +#include "omath/engines/opengl_engine/constants.hpp" +#include "omath/projection/camera.hpp" + +namespace omath::opengl_engine +{ + class CameraTrait final + { + public: + [[nodiscard]] + static ViewAngles calc_look_at_angle(const Vector3& cam_origin, const Vector3& look_at) noexcept; + + [[nodiscard]] + static Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3& cam_origin) noexcept; + [[nodiscard]] + static Mat4X4 calc_projection_matrix(const projection::FieldOfView& fov, const projection::ViewPort& view_port, + float near, float far) noexcept; + }; + +} // namespace omath::opengl_engine \ No newline at end of file diff --git a/include/omath/engines/opengl_engine/traits/mesh_trait.hpp b/include/omath/engines/opengl_engine/traits/mesh_trait.hpp new file mode 100644 index 00000000..2de8f8ef --- /dev/null +++ b/include/omath/engines/opengl_engine/traits/mesh_trait.hpp @@ -0,0 +1,19 @@ +// +// Created by Vladislav on 09.11.2025. +// +#pragma once +#include +#include + +namespace omath::opengl_engine +{ + class MeshTrait final + { + public: + [[nodiscard]] + static Mat4X4 rotation_matrix(const ViewAngles& rotation) + { + return opengl_engine::rotation_matrix(rotation); + } + }; +} // namespace omath::opengl_engine \ No newline at end of file diff --git a/include/omath/engines/opengl_engine/traits/pred_engine_trait.hpp b/include/omath/engines/opengl_engine/traits/pred_engine_trait.hpp new file mode 100644 index 00000000..ab2e607b --- /dev/null +++ b/include/omath/engines/opengl_engine/traits/pred_engine_trait.hpp @@ -0,0 +1,76 @@ +// +// Created by Vlad on 8/6/2025. +// +#pragma once +#include "omath/engines/opengl_engine/formulas.hpp" +#include "omath/projectile_prediction/projectile.hpp" +#include "omath/projectile_prediction/target.hpp" +#include + +namespace omath::opengl_engine +{ + class PredEngineTrait final + { + public: + constexpr static Vector3 predict_projectile_position(const projectile_prediction::Projectile& projectile, + const float pitch, const float yaw, + const float time, const float gravity) noexcept + { + auto current_pos = projectile.m_origin + + forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw), + RollAngle::from_degrees(0)}) + * projectile.m_launch_speed * time; + current_pos.y -= (gravity * projectile.m_gravity_scale) * (time * time) * 0.5f; + + return current_pos; + } + [[nodiscard]] + static constexpr Vector3 predict_target_position(const projectile_prediction::Target& target, + const float time, const float gravity) noexcept + { + auto predicted = target.m_origin + target.m_velocity * time; + + if (target.m_is_airborne) + predicted.y -= gravity * (time * time) * 0.5f; + + return predicted; + } + [[nodiscard]] + static float calc_vector_2d_distance(const Vector3& delta) noexcept + { + return std::sqrt(delta.x * delta.x + delta.z * delta.z); + } + + [[nodiscard]] + constexpr static float get_vector_height_coordinate(const Vector3& vec) noexcept + { + return vec.y; + } + + [[nodiscard]] + static Vector3 calc_viewpoint_from_angles(const projectile_prediction::Projectile& projectile, + Vector3 predicted_target_position, + const std::optional projectile_pitch) noexcept + { + const auto delta2d = calc_vector_2d_distance(predicted_target_position - projectile.m_origin); + const auto height = delta2d * std::tan(angles::degrees_to_radians(projectile_pitch.value())); + + return {predicted_target_position.x, predicted_target_position.y + height, projectile.m_origin.z}; + } + // Due to specification of maybe_calculate_projectile_launch_pitch_angle, pitch angle must be: + // 89 look up, -89 look down + [[nodiscard]] + static float calc_direct_pitch_angle(const Vector3& origin, const Vector3& view_to) noexcept + { + const auto direction = (view_to - origin).normalized(); + return angles::radians_to_degrees(std::asin(direction.y)); + } + [[nodiscard]] + static float calc_direct_yaw_angle(const Vector3& origin, const Vector3& view_to) noexcept + { + const auto direction = (view_to - origin).normalized(); + + return angles::radians_to_degrees(-std::atan2(direction.x, -direction.z)); + }; + }; +} // namespace omath::opengl_engine diff --git a/include/omath/engines/source_engine/camera.hpp b/include/omath/engines/source_engine/camera.hpp new file mode 100644 index 00000000..6769d40d --- /dev/null +++ b/include/omath/engines/source_engine/camera.hpp @@ -0,0 +1,11 @@ +// +// Created by Orange on 12/4/2024. +// +#pragma once +#include "omath/engines/source_engine/constants.hpp" +#include "omath/projection/camera.hpp" +#include "traits/camera_trait.hpp" +namespace omath::source_engine +{ + using Camera = projection::Camera; +} // namespace omath::source_engine \ No newline at end of file diff --git a/include/omath/engines/source_engine/collider.hpp b/include/omath/engines/source_engine/collider.hpp new file mode 100644 index 00000000..2c8d9b98 --- /dev/null +++ b/include/omath/engines/source_engine/collider.hpp @@ -0,0 +1,13 @@ +// +// Created by Vladislav on 09.11.2025. +// + +#pragma once +#include "mesh.hpp" +#include "omath/3d_primitives/mesh.hpp" +#include "omath/collision/mesh_collider.hpp" + +namespace omath::source_engine +{ + using MeshCollider = collision::MeshCollider; +} \ No newline at end of file diff --git a/include/omath/engines/source_engine/constants.hpp b/include/omath/engines/source_engine/constants.hpp new file mode 100644 index 00000000..743be74a --- /dev/null +++ b/include/omath/engines/source_engine/constants.hpp @@ -0,0 +1,25 @@ +// +// Created by Orange on 12/4/2024. +// +#pragma once + +#include "omath/linear_algebra/mat.hpp" +#include "omath/linear_algebra/vector3.hpp" +#include +#include + +namespace omath::source_engine +{ + constexpr Vector3 k_abs_up = {0, 0, 1}; + constexpr Vector3 k_abs_right = {0, -1, 0}; + constexpr Vector3 k_abs_forward = {1, 0, 0}; + + using Mat4X4 = Mat<4, 4, float, MatStoreType::ROW_MAJOR>; + using Mat3X3 = Mat<4, 4, float, MatStoreType::ROW_MAJOR>; + using Mat1X3 = Mat<1, 3, float, MatStoreType::ROW_MAJOR>; + using PitchAngle = Angle; + using YawAngle = Angle; + using RollAngle = Angle; + + using ViewAngles = omath::ViewAngles; +} // namespace omath::source_engine diff --git a/include/omath/engines/source_engine/formulas.hpp b/include/omath/engines/source_engine/formulas.hpp new file mode 100644 index 00000000..a51a6333 --- /dev/null +++ b/include/omath/engines/source_engine/formulas.hpp @@ -0,0 +1,25 @@ +// +// Created by Orange on 12/4/2024. +// +#pragma once +#include "omath/engines/source_engine/constants.hpp" + +namespace omath::source_engine +{ + [[nodiscard]] + Vector3 forward_vector(const ViewAngles& angles) noexcept; + + [[nodiscard]] + Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept; + + [[nodiscard]] + Vector3 right_vector(const ViewAngles& angles) noexcept; + + [[nodiscard]] + Vector3 up_vector(const ViewAngles& angles) noexcept; + + [[nodiscard]] Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3& cam_origin) noexcept; + + [[nodiscard]] + Mat4X4 calc_perspective_projection_matrix(float field_of_view, float aspect_ratio, float near, float far) noexcept; +} // namespace omath::source_engine diff --git a/include/omath/engines/source_engine/mesh.hpp b/include/omath/engines/source_engine/mesh.hpp new file mode 100644 index 00000000..a8b2ef36 --- /dev/null +++ b/include/omath/engines/source_engine/mesh.hpp @@ -0,0 +1,12 @@ +// +// Created by Vladislav on 09.11.2025. +// +#pragma once +#include "constants.hpp" +#include "omath/3d_primitives/mesh.hpp" +#include "traits/mesh_trait.hpp" + +namespace omath::source_engine +{ + using Mesh = primitives::Mesh; +} \ No newline at end of file diff --git a/include/omath/engines/source_engine/primitives.hpp b/include/omath/engines/source_engine/primitives.hpp new file mode 100644 index 00000000..f8a9d264 --- /dev/null +++ b/include/omath/engines/source_engine/primitives.hpp @@ -0,0 +1,17 @@ +// +// Created by Vladislav on 27.01.2026. +// + +#pragma once +#include "mesh.hpp" +#include "omath/engines/source_engine/traits/mesh_trait.hpp" +#include "omath/linear_algebra/vector3.hpp" +#include +namespace omath::source_engine +{ + using BoxMesh = primitives::Mesh, std::array, 8>, + std::array, 12>>; + + using PlaneMesh = primitives::Mesh, + std::array, 4>, std::array, 2>>; +} // namespace omath::source_engine \ No newline at end of file diff --git a/include/omath/engines/source_engine/traits/camera_trait.hpp b/include/omath/engines/source_engine/traits/camera_trait.hpp new file mode 100644 index 00000000..d027d25e --- /dev/null +++ b/include/omath/engines/source_engine/traits/camera_trait.hpp @@ -0,0 +1,24 @@ +// +// Created by Vlad on 8/10/2025. +// + +#pragma once +#include "omath/engines/source_engine/constants.hpp" +#include "omath/projection/camera.hpp" + +namespace omath::source_engine +{ + class CameraTrait final + { + public: + [[nodiscard]] + static ViewAngles calc_look_at_angle(const Vector3& cam_origin, const Vector3& look_at) noexcept; + + [[nodiscard]] + static Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3& cam_origin) noexcept; + [[nodiscard]] + static Mat4X4 calc_projection_matrix(const projection::FieldOfView& fov, const projection::ViewPort& view_port, + float near, float far) noexcept; + }; + +} // namespace omath::source_engine \ No newline at end of file diff --git a/include/omath/engines/source_engine/traits/mesh_trait.hpp b/include/omath/engines/source_engine/traits/mesh_trait.hpp new file mode 100644 index 00000000..2320465f --- /dev/null +++ b/include/omath/engines/source_engine/traits/mesh_trait.hpp @@ -0,0 +1,19 @@ +// +// Created by Vladislav on 09.11.2025. +// +#pragma once +#include +#include + +namespace omath::source_engine +{ + class MeshTrait final + { + public: + [[nodiscard]] + static Mat4X4 rotation_matrix(const ViewAngles& rotation) + { + return source_engine::rotation_matrix(rotation); + } + }; +} // namespace omath::source_engine \ No newline at end of file diff --git a/include/omath/engines/source_engine/traits/pred_engine_trait.hpp b/include/omath/engines/source_engine/traits/pred_engine_trait.hpp new file mode 100644 index 00000000..ca9771e1 --- /dev/null +++ b/include/omath/engines/source_engine/traits/pred_engine_trait.hpp @@ -0,0 +1,79 @@ +// +// Created by Vlad on 8/3/2025. +// + +#pragma once +#include "omath/engines/source_engine/formulas.hpp" +#include "omath/projectile_prediction/projectile.hpp" +#include "omath/projectile_prediction/target.hpp" +#include + +namespace omath::source_engine +{ + class PredEngineTrait final + { + public: + constexpr static Vector3 predict_projectile_position(const projectile_prediction::Projectile& projectile, + const float pitch, const float yaw, + const float time, const float gravity) noexcept + { + auto current_pos = projectile.m_origin + + forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw), + RollAngle::from_degrees(0)}) + * projectile.m_launch_speed * time; + current_pos.z -= (gravity * projectile.m_gravity_scale) * (time * time) * 0.5f; + + return current_pos; + } + [[nodiscard]] + static constexpr Vector3 predict_target_position(const projectile_prediction::Target& target, + const float time, const float gravity) noexcept + { + auto predicted = target.m_origin + target.m_velocity * time; + + if (target.m_is_airborne) + predicted.z -= gravity * (time * time) * 0.5f; + + return predicted; + } + [[nodiscard]] + static float calc_vector_2d_distance(const Vector3& delta) noexcept + { + return std::sqrt(delta.x * delta.x + delta.y * delta.y); + } + + [[nodiscard]] + constexpr static float get_vector_height_coordinate(const Vector3& vec) noexcept + { + return vec.z; + } + + [[nodiscard]] + static Vector3 calc_viewpoint_from_angles(const projectile_prediction::Projectile& projectile, + Vector3 predicted_target_position, + const std::optional projectile_pitch) noexcept + { + const auto delta2d = calc_vector_2d_distance(predicted_target_position - projectile.m_origin); + const auto height = delta2d * std::tan(angles::degrees_to_radians(projectile_pitch.value())); + + return {predicted_target_position.x, predicted_target_position.y, projectile.m_origin.z + height}; + } + // Due to specification of maybe_calculate_projectile_launch_pitch_angle, pitch angle must be: + // 89 look up, -89 look down + [[nodiscard]] + static float calc_direct_pitch_angle(const Vector3& origin, const Vector3& view_to) noexcept + { + const auto distance = origin.distance_to(view_to); + const auto delta = view_to - origin; + + return angles::radians_to_degrees(std::asin(delta.z / distance)); + } + [[nodiscard]] + static float calc_direct_yaw_angle(const Vector3& origin, const Vector3& view_to) noexcept + { + const auto delta = view_to - origin; + + return angles::radians_to_degrees(std::atan2(delta.y, delta.x)); + }; + }; +} // namespace omath::source_engine \ No newline at end of file diff --git a/include/omath/engines/unity_engine/camera.hpp b/include/omath/engines/unity_engine/camera.hpp new file mode 100644 index 00000000..bc3d6d2b --- /dev/null +++ b/include/omath/engines/unity_engine/camera.hpp @@ -0,0 +1,13 @@ +// +// Created by Vlad on 3/22/2025. +// + +#pragma once +#include "omath/engines/unity_engine/constants.hpp" +#include "omath/projection/camera.hpp" +#include "traits/camera_trait.hpp" + +namespace omath::unity_engine +{ + using Camera = projection::Camera; +} // namespace omath::unity_engine \ No newline at end of file diff --git a/include/omath/engines/unity_engine/constants.hpp b/include/omath/engines/unity_engine/constants.hpp new file mode 100644 index 00000000..a938c1cc --- /dev/null +++ b/include/omath/engines/unity_engine/constants.hpp @@ -0,0 +1,26 @@ +// +// Created by Vlad on 3/22/2025. +// + +#pragma once + +#include "omath/linear_algebra/mat.hpp" +#include "omath/linear_algebra/vector3.hpp" +#include +#include + +namespace omath::unity_engine +{ + constexpr Vector3 k_abs_up = {0, 1, 0}; + constexpr Vector3 k_abs_right = {1, 0, 0}; + constexpr Vector3 k_abs_forward = {0, 0, 1}; + + using Mat4X4 = Mat<4, 4, float, MatStoreType::ROW_MAJOR>; + using Mat3X3 = Mat<4, 4, float, MatStoreType::ROW_MAJOR>; + using Mat1X3 = Mat<1, 3, float, MatStoreType::ROW_MAJOR>; + using PitchAngle = Angle; + using YawAngle = Angle; + using RollAngle = Angle; + + using ViewAngles = omath::ViewAngles; +} // namespace omath::unity_engine diff --git a/include/omath/engines/unity_engine/formulas.hpp b/include/omath/engines/unity_engine/formulas.hpp new file mode 100644 index 00000000..be071367 --- /dev/null +++ b/include/omath/engines/unity_engine/formulas.hpp @@ -0,0 +1,26 @@ +// +// Created by Vlad on 3/22/2025. +// + +#pragma once +#include "omath/engines/unity_engine/constants.hpp" + +namespace omath::unity_engine +{ + [[nodiscard]] + Vector3 forward_vector(const ViewAngles& angles) noexcept; + + [[nodiscard]] + Vector3 right_vector(const ViewAngles& angles) noexcept; + + [[nodiscard]] + Vector3 up_vector(const ViewAngles& angles) noexcept; + + [[nodiscard]] Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3& cam_origin) noexcept; + + [[nodiscard]] + Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept; + + [[nodiscard]] + Mat4X4 calc_perspective_projection_matrix(float field_of_view, float aspect_ratio, float near, float far) noexcept; +} // namespace omath::unity_engine diff --git a/include/omath/engines/unity_engine/mesh.hpp b/include/omath/engines/unity_engine/mesh.hpp new file mode 100644 index 00000000..6c78b2f3 --- /dev/null +++ b/include/omath/engines/unity_engine/mesh.hpp @@ -0,0 +1,12 @@ +// +// Created by Vladislav on 09.11.2025. +// +#pragma once +#include "constants.hpp" +#include "omath/3d_primitives/mesh.hpp" +#include "traits/mesh_trait.hpp" + +namespace omath::unity_engine +{ + using Mesh = primitives::Mesh; +} \ No newline at end of file diff --git a/include/omath/engines/unity_engine/primitives.hpp b/include/omath/engines/unity_engine/primitives.hpp new file mode 100644 index 00000000..7e4c55d8 --- /dev/null +++ b/include/omath/engines/unity_engine/primitives.hpp @@ -0,0 +1,17 @@ +// +// Created by Vladislav on 27.01.2026. +// + +#pragma once +#include "mesh.hpp" +#include "omath/engines/unity_engine/traits/mesh_trait.hpp" +#include "omath/linear_algebra/vector3.hpp" +#include +namespace omath::unity_engine +{ + using BoxMesh = primitives::Mesh, std::array, 8>, + std::array, 12>>; + + using PlaneMesh = primitives::Mesh, + std::array, 4>, std::array, 2>>; +} // namespace omath::unity_engine \ No newline at end of file diff --git a/include/omath/engines/unity_engine/traits/camera_trait.hpp b/include/omath/engines/unity_engine/traits/camera_trait.hpp new file mode 100644 index 00000000..2d98b9db --- /dev/null +++ b/include/omath/engines/unity_engine/traits/camera_trait.hpp @@ -0,0 +1,24 @@ +// +// Created by Vlad on 8/10/2025. +// + +#pragma once +#include "omath/engines/unity_engine/formulas.hpp" +#include "omath/projection/camera.hpp" + +namespace omath::unity_engine +{ + class CameraTrait final + { + public: + [[nodiscard]] + static ViewAngles calc_look_at_angle(const Vector3& cam_origin, const Vector3& look_at) noexcept; + + [[nodiscard]] + static Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3& cam_origin) noexcept; + [[nodiscard]] + static Mat4X4 calc_projection_matrix(const projection::FieldOfView& fov, const projection::ViewPort& view_port, + float near, float far) noexcept; + }; + +} // namespace omath::unity_engine \ No newline at end of file diff --git a/include/omath/engines/unity_engine/traits/mesh_trait.hpp b/include/omath/engines/unity_engine/traits/mesh_trait.hpp new file mode 100644 index 00000000..56278202 --- /dev/null +++ b/include/omath/engines/unity_engine/traits/mesh_trait.hpp @@ -0,0 +1,19 @@ +// +// Created by Vladislav on 09.11.2025. +// +#pragma once +#include +#include + +namespace omath::unity_engine +{ + class MeshTrait final + { + public: + [[nodiscard]] + static Mat4X4 rotation_matrix(const ViewAngles& rotation) + { + return unity_engine::rotation_matrix(rotation); + } + }; +} // namespace omath::unity_engine \ No newline at end of file diff --git a/include/omath/engines/unity_engine/traits/pred_engine_trait.hpp b/include/omath/engines/unity_engine/traits/pred_engine_trait.hpp new file mode 100644 index 00000000..dadcb7e0 --- /dev/null +++ b/include/omath/engines/unity_engine/traits/pred_engine_trait.hpp @@ -0,0 +1,76 @@ +// +// Created by Vlad on 8/6/2025. +// +#pragma once +#include "omath/engines/unity_engine/formulas.hpp" +#include "omath/projectile_prediction/projectile.hpp" +#include "omath/projectile_prediction/target.hpp" +#include + +namespace omath::unity_engine +{ + class PredEngineTrait final + { + public: + constexpr static Vector3 predict_projectile_position(const projectile_prediction::Projectile& projectile, + const float pitch, const float yaw, + const float time, const float gravity) noexcept + { + auto current_pos = projectile.m_origin + + forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw), + RollAngle::from_degrees(0)}) + * projectile.m_launch_speed * time; + current_pos.y -= (gravity * projectile.m_gravity_scale) * (time * time) * 0.5f; + + return current_pos; + } + [[nodiscard]] + static constexpr Vector3 predict_target_position(const projectile_prediction::Target& target, + const float time, const float gravity) noexcept + { + auto predicted = target.m_origin + target.m_velocity * time; + + if (target.m_is_airborne) + predicted.y -= gravity * (time * time) * 0.5f; + + return predicted; + } + [[nodiscard]] + static float calc_vector_2d_distance(const Vector3& delta) noexcept + { + return std::sqrt(delta.x * delta.x + delta.z * delta.z); + } + + [[nodiscard]] + constexpr static float get_vector_height_coordinate(const Vector3& vec) noexcept + { + return vec.y; + } + + [[nodiscard]] + static Vector3 calc_viewpoint_from_angles(const projectile_prediction::Projectile& projectile, + Vector3 predicted_target_position, + const std::optional projectile_pitch) noexcept + { + const auto delta2d = calc_vector_2d_distance(predicted_target_position - projectile.m_origin); + const auto height = delta2d * std::tan(angles::degrees_to_radians(projectile_pitch.value())); + + return {predicted_target_position.x, predicted_target_position.y + height, projectile.m_origin.z}; + } + // Due to specification of maybe_calculate_projectile_launch_pitch_angle, pitch angle must be: + // 89 look up, -89 look down + [[nodiscard]] + static float calc_direct_pitch_angle(const Vector3& origin, const Vector3& view_to) noexcept + { + const auto direction = (view_to - origin).normalized(); + return angles::radians_to_degrees(std::asin(direction.y)); + } + [[nodiscard]] + static float calc_direct_yaw_angle(const Vector3& origin, const Vector3& view_to) noexcept + { + const auto direction = (view_to - origin).normalized(); + + return angles::radians_to_degrees(std::atan2(direction.x, direction.z)); + }; + }; +} // namespace omath::unity_engine diff --git a/include/omath/engines/unreal_engine/camera.hpp b/include/omath/engines/unreal_engine/camera.hpp new file mode 100644 index 00000000..30d5daf5 --- /dev/null +++ b/include/omath/engines/unreal_engine/camera.hpp @@ -0,0 +1,13 @@ +// +// Created by Vlad on 3/22/2025. +// + +#pragma once +#include "omath/engines/unreal_engine/constants.hpp" +#include "omath/projection/camera.hpp" +#include "traits/camera_trait.hpp" + +namespace omath::unreal_engine +{ + using Camera = projection::Camera; +} // namespace omath::unreal_engine \ No newline at end of file diff --git a/include/omath/engines/unreal_engine/constants.hpp b/include/omath/engines/unreal_engine/constants.hpp new file mode 100644 index 00000000..98ecf049 --- /dev/null +++ b/include/omath/engines/unreal_engine/constants.hpp @@ -0,0 +1,26 @@ +// +// Created by Vlad on 3/22/2025. +// + +#pragma once + +#include "omath/linear_algebra/mat.hpp" +#include "omath/linear_algebra/vector3.hpp" +#include +#include + +namespace omath::unreal_engine +{ + constexpr Vector3 k_abs_up = {0, 0, 1}; + constexpr Vector3 k_abs_right = {0, 1, 0}; + constexpr Vector3 k_abs_forward = {1, 0, 0}; + + using Mat4X4 = Mat<4, 4, float, MatStoreType::ROW_MAJOR>; + using Mat3X3 = Mat<4, 4, float, MatStoreType::ROW_MAJOR>; + using Mat1X3 = Mat<1, 3, float, MatStoreType::ROW_MAJOR>; + using PitchAngle = Angle; + using YawAngle = Angle; + using RollAngle = Angle; + + using ViewAngles = omath::ViewAngles; +} // namespace omath::unreal_engine diff --git a/include/omath/engines/unreal_engine/formulas.hpp b/include/omath/engines/unreal_engine/formulas.hpp new file mode 100644 index 00000000..4e601ce8 --- /dev/null +++ b/include/omath/engines/unreal_engine/formulas.hpp @@ -0,0 +1,26 @@ +// +// Created by Vlad on 3/22/2025. +// + +#pragma once +#include "omath/engines/unreal_engine/constants.hpp" + +namespace omath::unreal_engine +{ + [[nodiscard]] + Vector3 forward_vector(const ViewAngles& angles) noexcept; + + [[nodiscard]] + Vector3 right_vector(const ViewAngles& angles) noexcept; + + [[nodiscard]] + Vector3 up_vector(const ViewAngles& angles) noexcept; + + [[nodiscard]] Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3& cam_origin) noexcept; + + [[nodiscard]] + Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept; + + [[nodiscard]] + Mat4X4 calc_perspective_projection_matrix(float field_of_view, float aspect_ratio, float near, float far) noexcept; +} // namespace omath::unreal_engine diff --git a/include/omath/engines/unreal_engine/mesh.hpp b/include/omath/engines/unreal_engine/mesh.hpp new file mode 100644 index 00000000..8fb73458 --- /dev/null +++ b/include/omath/engines/unreal_engine/mesh.hpp @@ -0,0 +1,12 @@ +// +// Created by Vladislav on 09.11.2025. +// +#pragma once +#include "constants.hpp" +#include "omath/3d_primitives/mesh.hpp" +#include "traits/mesh_trait.hpp" + +namespace omath::unreal_engine +{ + using Mesh = primitives::Mesh; +} \ No newline at end of file diff --git a/include/omath/engines/unreal_engine/primitives.hpp b/include/omath/engines/unreal_engine/primitives.hpp new file mode 100644 index 00000000..2a71e764 --- /dev/null +++ b/include/omath/engines/unreal_engine/primitives.hpp @@ -0,0 +1,17 @@ +// +// Created by Vladislav on 27.01.2026. +// + +#pragma once +#include "mesh.hpp" +#include "omath/engines/unreal_engine/traits/mesh_trait.hpp" +#include "omath/linear_algebra/vector3.hpp" +#include +namespace omath::unreal_engine +{ + using BoxMesh = primitives::Mesh, std::array, 8>, + std::array, 12>>; + + using PlaneMesh = primitives::Mesh, + std::array, 4>, std::array, 2>>; +} // namespace omath::unreal_engine \ No newline at end of file diff --git a/include/omath/engines/unreal_engine/traits/camera_trait.hpp b/include/omath/engines/unreal_engine/traits/camera_trait.hpp new file mode 100644 index 00000000..f2de27e4 --- /dev/null +++ b/include/omath/engines/unreal_engine/traits/camera_trait.hpp @@ -0,0 +1,24 @@ +// +// Created by Vlad on 8/10/2025. +// + +#pragma once +#include "omath/engines/unreal_engine/formulas.hpp" +#include "omath/projection/camera.hpp" + +namespace omath::unreal_engine +{ + class CameraTrait final + { + public: + [[nodiscard]] + static ViewAngles calc_look_at_angle(const Vector3& cam_origin, const Vector3& look_at) noexcept; + + [[nodiscard]] + static Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3& cam_origin) noexcept; + [[nodiscard]] + static Mat4X4 calc_projection_matrix(const projection::FieldOfView& fov, const projection::ViewPort& view_port, + float near, float far) noexcept; + }; + +} // namespace omath::unreal_engine \ No newline at end of file diff --git a/include/omath/engines/unreal_engine/traits/mesh_trait.hpp b/include/omath/engines/unreal_engine/traits/mesh_trait.hpp new file mode 100644 index 00000000..5da0a2d6 --- /dev/null +++ b/include/omath/engines/unreal_engine/traits/mesh_trait.hpp @@ -0,0 +1,19 @@ +// +// Created by Vladislav on 09.11.2025. +// +#pragma once +#include +#include + +namespace omath::unreal_engine +{ + class MeshTrait final + { + public: + [[nodiscard]] + static Mat4X4 rotation_matrix(const ViewAngles& rotation) + { + return unreal_engine::rotation_matrix(rotation); + } + }; +} // namespace omath::unreal_engine \ No newline at end of file diff --git a/include/omath/engines/unreal_engine/traits/pred_engine_trait.hpp b/include/omath/engines/unreal_engine/traits/pred_engine_trait.hpp new file mode 100644 index 00000000..dbc04f05 --- /dev/null +++ b/include/omath/engines/unreal_engine/traits/pred_engine_trait.hpp @@ -0,0 +1,77 @@ +// +// Created by Vlad on 8/6/2025. +// +#pragma once +#include "omath/engines/unreal_engine/formulas.hpp" +#include "omath/projectile_prediction/projectile.hpp" +#include "omath/projectile_prediction/target.hpp" +#include + +namespace omath::unreal_engine +{ + class PredEngineTrait final + { + public: + constexpr static Vector3 predict_projectile_position(const projectile_prediction::Projectile& projectile, + const float pitch, const float yaw, + const float time, const float gravity) noexcept + { + auto current_pos = projectile.m_origin + + forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw), + RollAngle::from_degrees(0)}) + * projectile.m_launch_speed * time; + current_pos.y -= (gravity * projectile.m_gravity_scale) * (time * time) * 0.5f; + + return current_pos; + } + [[nodiscard]] + static constexpr Vector3 predict_target_position(const projectile_prediction::Target& target, + const float time, const float gravity) noexcept + { + auto predicted = target.m_origin + target.m_velocity * time; + + if (target.m_is_airborne) + predicted.y -= gravity * (time * time) * 0.5f; + + return predicted; + } + [[nodiscard]] + static float calc_vector_2d_distance(const Vector3& delta) noexcept + { + return std::sqrt(delta.x * delta.x + delta.z * delta.z); + } + + [[nodiscard]] + constexpr static float get_vector_height_coordinate(const Vector3& vec) noexcept + { + return vec.y; + } + + [[nodiscard]] + static Vector3 calc_viewpoint_from_angles(const projectile_prediction::Projectile& projectile, + Vector3 predicted_target_position, + const std::optional projectile_pitch) noexcept + { + const auto delta2d = calc_vector_2d_distance(predicted_target_position - projectile.m_origin); + const auto height = delta2d * std::tan(angles::degrees_to_radians(projectile_pitch.value())); + + return {predicted_target_position.x, predicted_target_position.y, projectile.m_origin.z + height}; + } + // Due to specification of maybe_calculate_projectile_launch_pitch_angle, pitch angle must be: + // 89 look up, -89 look down + [[nodiscard]] + static float calc_direct_pitch_angle(const Vector3& origin, const Vector3& view_to) noexcept + { + const auto direction = (view_to - origin).normalized(); + + return angles::radians_to_degrees(std::asin(direction.z)); + } + [[nodiscard]] + static float calc_direct_yaw_angle(const Vector3& origin, const Vector3& view_to) noexcept + { + const auto direction = (view_to - origin).normalized(); + + return angles::radians_to_degrees(std::atan2(direction.y, direction.x)); + }; + }; +} // namespace omath::unreal_engine diff --git a/include/omath/linear_algebra/mat.hpp b/include/omath/linear_algebra/mat.hpp new file mode 100644 index 00000000..f0a7aa4b --- /dev/null +++ b/include/omath/linear_algebra/mat.hpp @@ -0,0 +1,755 @@ +// +// Created by vlad on 9/29/2024. +// +#pragma once +#include "vector3.hpp" +#include +#include +#include +#include +#include +#include +#include + +#ifdef OMATH_USE_AVX2 +#include +#endif + +#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 + { + size_t rows, columns; + }; + + enum class MatStoreType : uint8_t + { + ROW_MAJOR = 0, + COLUMN_MAJOR + }; + + template concept MatTemplateEqual + = (M1::rows == M2::rows) && (M1::columns == M2::columns) + && std::is_same_v && (M1::store_type == M2::store_type); + + template + requires std::is_arithmetic_v + class Mat final + { + public: + using ContainedType = Type; + constexpr Mat() noexcept + { + clear(); + } + + [[nodiscard]] + consteval static MatStoreType get_store_ordering() noexcept + { + return StoreType; + } + constexpr Mat(const std::initializer_list>& rows) + { + if (rows.size() != Rows) + throw std::invalid_argument("Initializer list rows size does not match template parameter Rows"); + + auto row_it = rows.begin(); + for (size_t i = 0; i < Rows; ++i, ++row_it) + { + if (row_it->size() != Columns) + throw std::invalid_argument( + "All rows must have the same number of columns as template parameter Columns"); + + auto col_it = row_it->begin(); + for (size_t j = 0; j < Columns; ++j, ++col_it) + { + at(i, j) = std::move(*col_it); + } + } + } + + constexpr explicit Mat(const Type* raw_data) + { + std::copy_n(raw_data, Rows * Columns, m_data.begin()); + } + + constexpr Mat(const Mat& other) noexcept + { + m_data = other.m_data; + } + + [[nodiscard]] + constexpr Type& operator[](const size_t row, const size_t col) + { + return at(row, col); + } + + [[nodiscard]] + constexpr const Type& operator[](const size_t row, const size_t col) const + { + return at(row, col); + } + + constexpr Mat(Mat&& other) noexcept + { + m_data = std::move(other.m_data); + } + + [[nodiscard]] + static constexpr size_t row_count() noexcept + { + return Rows; + } + + [[nodiscard]] + static constexpr size_t columns_count() noexcept + { + return Columns; + } + + [[nodiscard]] + static consteval MatSize size() noexcept + { + return {Rows, Columns}; + } + + [[nodiscard]] + constexpr const Type& at(const size_t row_index, const size_t column_index) const + { +#if !defined(NDEBUG) && defined(OMATH_SUPRESS_SAFETY_CHECKS) + if (row_index >= Rows || column_index >= Columns) + throw std::out_of_range("Index out of range"); +#endif + if constexpr (StoreType == MatStoreType::ROW_MAJOR) + return m_data[row_index * Columns + column_index]; + + else if constexpr (StoreType == MatStoreType::COLUMN_MAJOR) + return m_data[row_index + column_index * Rows]; + + else + { + static_assert(false, "Invalid matrix access convention"); + std::unreachable(); + } + } + + [[nodiscard]] constexpr Type& at(const size_t row_index, const size_t column_index) + { + return const_cast(std::as_const(*this).at(row_index, column_index)); + } + + [[nodiscard]] + constexpr Type sum() const noexcept + { + return std::accumulate(m_data.begin(), m_data.end(), static_cast(0)); + } + + constexpr void clear() noexcept + { + set(static_cast(0)); + } + + constexpr void set(const Type& value) noexcept + { + std::ranges::fill(m_data, value); + } + + // Operator overloading for multiplication with another Mat + template [[nodiscard]] + constexpr Mat + operator*(const Mat& other) const + { +#ifdef OMATH_USE_AVX2 + if constexpr (StoreType == MatStoreType::ROW_MAJOR) + return avx_multiply_row_major(other); + else if constexpr (StoreType == MatStoreType::COLUMN_MAJOR) + return avx_multiply_col_major(other); +#else + if constexpr (StoreType == MatStoreType::ROW_MAJOR) + return cache_friendly_multiply_row_major(other); + else if constexpr (StoreType == MatStoreType::COLUMN_MAJOR) + return cache_friendly_multiply_col_major(other); +#endif + else + std::unreachable(); + } + + constexpr Mat& operator*=(const Type& f) noexcept + { + std::ranges::for_each(m_data, [&f](auto& val) { val *= f; }); + return *this; + } + + template constexpr Mat + operator*=(const Mat& other) + { + return *this = *this * other; + } + + [[nodiscard]] + constexpr Mat operator*(const Type& value) const noexcept + { + Mat result(*this); + result *= value; + return result; + } + + constexpr Mat& operator/=(const Type& value) noexcept + { + std::ranges::for_each(m_data, [&value](auto& val) { val /= value; }); + return *this; + } + + [[nodiscard]] + constexpr Mat operator/(const Type& value) const noexcept + { + Mat result(*this); + result /= value; + return result; + } + + constexpr Mat& operator=(const Mat& other) noexcept + { + if (this != &other) + m_data = other.m_data; + + return *this; + } + + constexpr Mat& operator=(Mat&& other) noexcept + { + if (this != &other) + m_data = std::move(other.m_data); + + return *this; + } + + [[nodiscard]] + constexpr Mat transposed() const noexcept + { + Mat transposed; + for (size_t i = 0; i < Rows; ++i) + for (size_t j = 0; j < Columns; ++j) + transposed.at(j, i) = at(i, j); + + return transposed; + } + + [[nodiscard]] + constexpr Type determinant() const + { + static_assert(Rows == Columns, "Determinant is only defined for square matrices."); + + if constexpr (Rows == 1) + return at(0, 0); + + if constexpr (Rows == 2) + return at(0, 0) * at(1, 1) - at(0, 1) * at(1, 0); + + if constexpr (Rows > 2) + { + Type det = 0; + for (size_t column = 0; column < Columns; ++column) + { + const Type cofactor = at(0, column) * alg_complement(0, column); + det += cofactor; + } + return det; + } + std::unreachable(); + } + + [[nodiscard]] + constexpr Mat strip(const size_t row, const size_t column) const + { + static_assert(Rows - 1 > 0 && Columns - 1 > 0); + Mat result; + for (size_t i = 0, m = 0; i < Rows; ++i) + { + if (i == row) + continue; + for (size_t j = 0, n = 0; j < Columns; ++j) + { + if (j == column) + continue; + result.at(m, n) = at(i, j); + ++n; + } + ++m; + } + return result; + } + + [[nodiscard]] + constexpr Type minor(const size_t row, const size_t column) const + { + return strip(row, column).determinant(); + } + + [[nodiscard]] + constexpr Type alg_complement(const size_t row, const size_t column) const + { + const auto minor_value = minor(row, column); + return (row + column + 2) % 2 == 0 ? minor_value : -minor_value; + } + + [[nodiscard]] + constexpr const std::array& raw_array() const + { + return m_data; + } + + [[nodiscard]] + constexpr std::array& raw_array() + { + return m_data; + } + + [[nodiscard]] + std::string to_string() const noexcept + { + std::ostringstream oss; + oss << "[["; + + for (size_t i = 0; i < Rows; ++i) + { + if (i > 0) + oss << " ["; + + for (size_t j = 0; j < Columns; ++j) + { + oss << std::setw(9) << std::fixed << std::setprecision(3) << at(i, j); + if (j != Columns - 1) + oss << ", "; + } + oss << (i == Rows - 1 ? "]]" : "]\n"); + } + return oss.str(); + } + + [[nodiscard]] + std::wstring to_wstring() const noexcept + { + const auto ascii_string = to_string(); + return {ascii_string.cbegin(), ascii_string.cend()}; + } + + [[nodiscard]] + // ReSharper disable once CppInconsistentNaming + std::u8string to_u8string() const noexcept + { + const auto ascii_string = to_string(); + return {ascii_string.cbegin(), ascii_string.cend()}; + } + + [[nodiscard]] + bool operator==(const Mat& mat) const + { + return m_data == mat.m_data; + } + + [[nodiscard]] + bool operator!=(const Mat& mat) const + { + return !operator==(mat); + } + + // Static methods that return fixed-size matrices + [[nodiscard]] + constexpr static Mat<4, 4> to_screen_mat(const Type& screen_width, const Type& screen_height) noexcept + { + return { + {screen_width / 2, 0, 0, 0}, + {0, -screen_height / 2, 0, 0}, + {0, 0, 1, 0}, + {screen_width / 2, screen_height / 2, 0, 1}, + }; + } + + [[nodiscard]] + constexpr std::optional inverted() const + { + const auto det = determinant(); + + if (std::abs(det) < std::numeric_limits::epsilon()) + return std::nullopt; + + const auto transposed_mat = transposed(); + Mat result; + + for (std::size_t row = 0; row < Rows; row++) + for (std::size_t column = 0; column < Rows; column++) + result.at(row, column) = transposed_mat.alg_complement(row, column); + + result /= det; + + return {result}; + } + + private: + std::array m_data; + + template [[nodiscard]] + constexpr Mat + cache_friendly_multiply_row_major(const Mat& other) const + { + Mat result; + for (std::size_t row_index = 0; row_index < Rows; ++row_index) + for (std::size_t column_index = 0; column_index < Columns; ++column_index) + { + const Type& current_number = at(row_index, column_index); + for (std::size_t other_column = 0; other_column < OtherColumns; ++other_column) + result.at(row_index, other_column) += current_number * other.at(column_index, other_column); + } + return result; + } + + template [[nodiscard]] + constexpr Mat cache_friendly_multiply_col_major( + const Mat& other) const + { + Mat result; + for (std::size_t other_column = 0; other_column < OtherColumns; ++other_column) + for (std::size_t column_index = 0; column_index < Columns; ++column_index) + { + const Type& current_number = other.at(column_index, other_column); + for (std::size_t row_index = 0; row_index < Rows; ++row_index) + result.at(row_index, other_column) += at(row_index, column_index) * current_number; + } + return result; + } +#ifdef OMATH_USE_AVX2 + template [[nodiscard]] + constexpr Mat + avx_multiply_col_major(const Mat& other) const + { + Mat result; + + const Type* this_mat_data = this->raw_array().data(); + const Type* other_mat_data = other.raw_array().data(); + Type* result_mat_data = result.raw_array().data(); + + if constexpr (std::is_same_v) + { + // ReSharper disable once CppTooWideScopeInitStatement + constexpr std::size_t vector_size = 8; + for (std::size_t j = 0; j < OtherColumns; ++j) + { + auto* c_col = reinterpret_cast(result_mat_data + j * Rows); + for (std::size_t k = 0; k < Columns; ++k) + { + const float bkj = reinterpret_cast(other_mat_data)[k + j * Columns]; + const __m256 bkj_vec = _mm256_set1_ps(bkj); + + const auto* a_col_k = reinterpret_cast(this_mat_data + k * Rows); + + std::size_t i = 0; + for (; i + vector_size <= Rows; i += vector_size) + { + __m256 cvec = _mm256_loadu_ps(c_col + i); + const __m256 a_vec = _mm256_loadu_ps(a_col_k + i); + cvec = _mm256_fmadd_ps(a_vec, bkj_vec, cvec); + _mm256_storeu_ps(c_col + i, cvec); + } + for (; i < Rows; ++i) + c_col[i] += a_col_k[i] * bkj; + } + } + } + else if (std::is_same_v) + { // double + // ReSharper disable once CppTooWideScopeInitStatement + constexpr std::size_t vector_size = 4; + for (std::size_t j = 0; j < OtherColumns; ++j) + { + auto* c_col = reinterpret_cast(result_mat_data + j * Rows); + for (std::size_t k = 0; k < Columns; ++k) + { + const double bkj = reinterpret_cast(other_mat_data)[k + j * Columns]; + const __m256d bkj_vec = _mm256_set1_pd(bkj); + + const auto* a_col_k = reinterpret_cast(this_mat_data + k * Rows); + + std::size_t i = 0; + for (; i + vector_size <= Rows; i += vector_size) + { + __m256d cvec = _mm256_loadu_pd(c_col + i); + const __m256d a_vec = _mm256_loadu_pd(a_col_k + i); + cvec = _mm256_fmadd_pd(a_vec, bkj_vec, cvec); + _mm256_storeu_pd(c_col + i, cvec); + } + for (; i < Rows; ++i) + c_col[i] += a_col_k[i] * bkj; + } + } + } + else + std::unreachable(); + + return result; + } + + template [[nodiscard]] + constexpr Mat + avx_multiply_row_major(const Mat& other) const + { + Mat result; + + const Type* this_mat_data = this->raw_array().data(); + const Type* other_mat_data = other.raw_array().data(); + Type* result_mat_data = result.raw_array().data(); + + if constexpr (std::is_same_v) + { + // ReSharper disable once CppTooWideScopeInitStatement + constexpr std::size_t vector_size = 8; + for (std::size_t i = 0; i < Rows; ++i) + { + Type* c_row = result_mat_data + i * OtherColumns; + for (std::size_t k = 0; k < Columns; ++k) + { + const auto aik = static_cast(this_mat_data[i * Columns + k]); + const __m256 aik_vec = _mm256_set1_ps(aik); + const auto* b_row = reinterpret_cast(other_mat_data + k * OtherColumns); + + std::size_t j = 0; + for (; j + vector_size <= OtherColumns; j += vector_size) + { + __m256 cvec = _mm256_loadu_ps(c_row + j); + const __m256 b_vec = _mm256_loadu_ps(b_row + j); + cvec = _mm256_fmadd_ps(b_vec, aik_vec, cvec); + + _mm256_storeu_ps(c_row + j, cvec); + } + for (; j < OtherColumns; ++j) + c_row[j] += aik * b_row[j]; + } + } + } + else if (std::is_same_v) + { // double + // ReSharper disable once CppTooWideScopeInitStatement + constexpr std::size_t vector_size = 4; + for (std::size_t i = 0; i < Rows; ++i) + { + Type* c_row = result_mat_data + i * OtherColumns; + for (std::size_t k = 0; k < Columns; ++k) + { + const auto aik = static_cast(this_mat_data[i * Columns + k]); + const __m256d aik_vec = _mm256_set1_pd(aik); + const auto* b_row = reinterpret_cast(other_mat_data + k * OtherColumns); + + std::size_t j = 0; + for (; j + vector_size <= OtherColumns; j += vector_size) + { + __m256d cvec = _mm256_loadu_pd(c_row + j); + const __m256d b_vec = _mm256_loadu_pd(b_row + j); + cvec = _mm256_fmadd_pd(b_vec, aik_vec, cvec); + + _mm256_storeu_pd(c_row + j, cvec); + } + for (; j < OtherColumns; ++j) + c_row[j] += aik * b_row[j]; + } + } + } + else + std::unreachable(); + return result; + } +#endif + }; + + template [[nodiscard]] + constexpr static Mat<1, 4, Type, St> mat_row_from_vector(const Vector3& vector) noexcept + { + return {{vector.x, vector.y, vector.z, 1}}; + } + + template [[nodiscard]] + constexpr static Mat<4, 1, Type, St> mat_column_from_vector(const Vector3& vector) noexcept + { + return {{vector.x}, {vector.y}, {vector.z}, {1}}; + } + + template + [[nodiscard]] + constexpr Mat<4, 4, Type, St> mat_translation(const Vector3& diff) noexcept + { + return + { + {1, 0, 0, diff.x}, + {0, 1, 0, diff.y}, + {0, 0, 1, diff.z}, + {0, 0, 0, 1}, + }; + } + template + [[nodiscard]] + constexpr Mat<4, 4, Type, St> mat_scale(const Vector3& scale) noexcept + { + return { + {scale.x, 0, 0, 0}, + {0, scale.y, 0, 0}, + {0, 0, scale.z, 0}, + {0, 0, 0, 1}, + }; + } + + template + [[nodiscard]] + Mat<4, 4, Type, St> mat_rotation_axis_x(const Angle& angle) noexcept + { + return + { + {1, 0, 0, 0}, + {0, angle.cos(), -angle.sin(), 0}, + {0, angle.sin(), angle.cos(), 0}, + {0, 0, 0, 1} + }; + } + + template + [[nodiscard]] + Mat<4, 4, Type, St> mat_rotation_axis_y(const Angle& angle) noexcept + { + return + { + {angle.cos(), 0, angle.sin(), 0}, + {0 , 1, 0, 0}, + {-angle.sin(), 0, angle.cos(), 0}, + {0 , 0, 0, 1} + }; + } + + template + [[nodiscard]] + Mat<4, 4, Type, St> mat_rotation_axis_z(const Angle& angle) noexcept + { + return + { + {angle.cos(), -angle.sin(), 0, 0}, + {angle.sin(), angle.cos(), 0, 0}, + { 0, 0, 1, 0}, + { 0, 0, 0, 1}, + }; + } + + template + [[nodiscard]] + static Mat<4, 4, Type, St> mat_camera_view(const Vector3& forward, const Vector3& right, + const Vector3& up, const Vector3& camera_origin) noexcept + { + return Mat<4, 4, Type, St> + { + {right.x, right.y, right.z, 0}, + {up.x, up.y, up.z, 0}, + {forward.x, forward.y, forward.z, 0}, + {0, 0, 0, 1}, + } * mat_translation(-camera_origin); + } + + template + [[nodiscard]] + Mat<4, 4, Type, St> mat_perspective_left_handed(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.f, 0.f, 0.f}, + {0.f, 1.f / fov_half_tan, 0.f, 0.f}, + {0.f, 0.f, (far + near) / (far - near), -(2.f * near * far) / (far - near)}, + {0.f, 0.f, 1.f, 0.f}}; + } + + template + [[nodiscard]] + Mat<4, 4, Type, St> mat_perspective_right_handed(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.f, 0.f, 0.f}, + {0.f, 1.f / fov_half_tan, 0.f, 0.f}, + {0.f, 0.f, -(far + near) / (far - near), -(2.f * near * far) / (far - near)}, + {0.f, 0.f, -1.f, 0.f}}; + } + template + [[nodiscard]] + Mat<4, 4, Type, St> mat_ortho_left_handed(const Type left, const Type right, const Type bottom, const Type top, + const Type near, const Type far) noexcept + { + return + { + { static_cast(2) / (right - left), 0.f, 0.f, -(right + left) / (right - left)}, + { 0.f, static_cast(2) / (top - bottom), 0.f, -(top + bottom) / (top - bottom)}, + { 0.f, 0.f, static_cast(2) / (far - near), -(far + near) / (far - near) }, + { 0.f, 0.f, 0.f, 1.f } + }; + } + template + [[nodiscard]] + Mat<4, 4, Type, St> mat_ortho_right_handed(const Type left, const Type right, const Type bottom, const Type top, + const Type near, const Type far) noexcept + { + return + { + { static_cast(2) / (right - left), 0.f, 0.f, -(right + left) / (right - left)}, + { 0.f, static_cast(2) / (top - bottom), 0.f, -(top + bottom) / (top - bottom)}, + { 0.f, 0.f, -static_cast(2) / (far - near), -(far + near) / (far - near) }, + { 0.f, 0.f, 0.f, 1.f } + }; + } + template + Mat<4, 4, T, St> mat_look_at_left_handed(const Vector3& eye, const Vector3& center, const Vector3& up) + { + const Vector3 f = (center - eye).normalized(); + const Vector3 s = f.cross(up).normalized(); + const Vector3 u = s.cross(f); + return mat_camera_view(f, s, u, eye); + } + + template + Mat<4, 4, T, St>mat_look_at_right_handed(const Vector3& eye, const Vector3& center, const Vector3& up) + { + const Vector3 f = (center - eye).normalized(); + const Vector3 s = f.cross(up).normalized(); + const Vector3 u = s.cross(f); + return mat_camera_view(-f, s, u, eye); + } + +} // namespace omath + +template +struct std::formatter> // NOLINT(*-dcl58-cpp) +{ + using MatType = omath::Mat; + [[nodiscard]] + static constexpr auto parse(std::format_parse_context& ctx) + { + return ctx.begin(); + } + + template + [[nodiscard]] + static auto format(const MatType& mat, FormatContext& ctx) + { + if constexpr (std::is_same_v) + return std::format_to(ctx.out(), "{}", mat.to_string()); + + if constexpr (std::is_same_v) + return std::format_to(ctx.out(), L"{}", mat.to_wstring()); + + if constexpr (std::is_same_v) + return std::format_to(ctx.out(), u8"{}", mat.to_u8string()); + } +}; \ No newline at end of file diff --git a/include/omath/linear_algebra/triangle.hpp b/include/omath/linear_algebra/triangle.hpp new file mode 100644 index 00000000..1d8e68c4 --- /dev/null +++ b/include/omath/linear_algebra/triangle.hpp @@ -0,0 +1,85 @@ +// +// Created by Orange on 11/13/2024. +// +#pragma once +#include "vector3.hpp" + +namespace omath +{ + /* + v1 + |\ + | \ + a | \ hypot + | \ + v2 ----- v3 + b + */ + + template + class Triangle final + { + public: + constexpr Triangle() = default; + constexpr Triangle(const Vector& vertex1, const Vector& vertex2, const Vector& vertex3) + : m_vertex1(vertex1), m_vertex2(vertex2), m_vertex3(vertex3) + { + } + + Vector m_vertex1; + Vector m_vertex2; + Vector m_vertex3; + + [[nodiscard]] + constexpr Vector calculate_normal() const + { + const auto b = side_b_vector(); + const auto a = side_a_vector(); + + return b.cross(a).normalized(); + } + + [[nodiscard]] + Vector::ContainedType side_a_length() const + { + return m_vertex1.distance_to(m_vertex2); + } + + [[nodiscard]] + Vector::ContainedType side_b_length() const + { + return m_vertex3.distance_to(m_vertex2); + } + + [[nodiscard]] + constexpr Vector side_a_vector() const + { + return m_vertex1 - m_vertex2; + } + + [[nodiscard]] + constexpr Vector::ContainedType hypot() const + { + return m_vertex1.distance_to(m_vertex3); + } + [[nodiscard]] + constexpr bool is_rectangular() const + { + const auto side_a = side_a_length(); + const auto side_b = side_b_length(); + const auto hypot_value = hypot(); + + return std::abs(side_a * side_a + side_b * side_b - hypot_value * hypot_value) <= 0.0001f; + } + [[nodiscard]] + constexpr Vector side_b_vector() const + { + return m_vertex3 - m_vertex2; + } + [[nodiscard]] + constexpr Vector mid_point() const + { + return (m_vertex1 + m_vertex2 + m_vertex3) / 3; + } + }; +} // namespace omath diff --git a/include/omath/linear_algebra/vector2.hpp b/include/omath/linear_algebra/vector2.hpp new file mode 100644 index 00000000..2ee37503 --- /dev/null +++ b/include/omath/linear_algebra/vector2.hpp @@ -0,0 +1,275 @@ +// +// Created by Vlad on 02.09.2024. +// + +#pragma once +#include +#include +#include + +#ifdef OMATH_IMGUI_INTEGRATION +#include +#endif + +namespace omath +{ + + template + requires std::is_arithmetic_v + class Vector2 + { + public: + using ContainedType = Type; + Type x = static_cast(0); + Type y = static_cast(0); + + // Constructors + constexpr Vector2() = default; + + constexpr Vector2(const Type& x, const Type& y) noexcept: x(x), y(y) + { + } + + // Equality operators + [[nodiscard]] + constexpr bool operator==(const Vector2& other) const noexcept + { + return x == other.x && y == other.y; + } + + [[nodiscard]] + constexpr bool operator!=(const Vector2& other) const noexcept + { + return !(*this == other); + } + + // Compound assignment operators + constexpr Vector2& operator+=(const Vector2& other) noexcept + { + x += other.x; + y += other.y; + + return *this; + } + + constexpr Vector2& operator-=(const Vector2& other) noexcept + { + x -= other.x; + y -= other.y; + + return *this; + } + + constexpr Vector2& operator*=(const Vector2& other) noexcept + { + x *= other.x; + y *= other.y; + + return *this; + } + + constexpr Vector2& operator/=(const Vector2& other) noexcept + { + x /= other.x; + y /= other.y; + + return *this; + } + + constexpr Vector2& operator*=(const Type& value) noexcept + { + x *= value; + y *= value; + + return *this; + } + + constexpr Vector2& operator/=(const Type& value) noexcept + { + x /= value; + y /= value; + + return *this; + } + + constexpr Vector2& operator+=(const Type& value) noexcept + { + x += value; + y += value; + + return *this; + } + + constexpr Vector2& operator-=(const Type& value) noexcept + { + x -= value; + y -= value; + + return *this; + } + + // Basic vector operations + [[nodiscard]] Type distance_to(const Vector2& other) const noexcept + { + return std::sqrt(distance_to_sqr(other)); + } + + [[nodiscard]] constexpr Type distance_to_sqr(const Vector2& other) const noexcept + { + return (x - other.x) * (x - other.x) + (y - other.y) * (y - other.y); + } + + [[nodiscard]] constexpr Type dot(const Vector2& other) const noexcept + { + return x * other.x + y * other.y; + } + +#ifndef _MSC_VER + [[nodiscard]] constexpr Type length() const noexcept + { + return std::hypot(this->x, this->y); + } + + [[nodiscard]] constexpr Vector2 normalized() const noexcept + { + const Type len = length(); + return len > 0.f ? *this / len : *this; + } +#else + [[nodiscard]] Type length() const noexcept + { + return std::hypot(x, y); + } + + [[nodiscard]] Vector2 normalized() const noexcept + { + const Type len = length(); + return len > static_cast(0) ? *this / len : *this; + } +#endif + [[nodiscard]] constexpr Type length_sqr() const noexcept + { + return x * x + y * y; + } + + constexpr Vector2& abs() noexcept + { + // FIXME: Replace with std::abs, if it will become constexprable + x = x < static_cast(0) ? -x : x; + y = y < static_cast(0) ? -y : y; + return *this; + } + + [[nodiscard]] constexpr Vector2 operator-() const noexcept + { + return {-x, -y}; + } + + // Binary arithmetic operators + [[nodiscard]] constexpr Vector2 operator+(const Vector2& other) const noexcept + { + return {x + other.x, y + other.y}; + } + + [[nodiscard]] constexpr Vector2 operator-(const Vector2& other) const noexcept + { + return {x - other.x, y - other.y}; + } + + [[nodiscard]] constexpr Vector2 operator*(const Type& value) const noexcept + { + return {x * value, y * value}; + } + + [[nodiscard]] constexpr Vector2 operator/(const Type& value) const noexcept + { + return {x / value, y / value}; + } + + // Sum of elements + [[nodiscard]] constexpr Type sum() const noexcept + { + return x + y; + } + + [[nodiscard]] + bool operator<(const Vector2& other) const noexcept + { + return length() < other.length(); + } + [[nodiscard]] + bool operator>(const Vector2& other) const noexcept + { + return length() > other.length(); + } + + [[nodiscard]] + bool operator<=(const Vector2& other) const noexcept + { + return length() <= other.length(); + } + + [[nodiscard]] + bool operator>=(const Vector2& other) const noexcept + { + return length() >= other.length(); + } + + [[nodiscard]] + constexpr std::tuple as_tuple() const noexcept + { + return std::make_tuple(x, y); + } +#ifdef OMATH_IMGUI_INTEGRATION + [[nodiscard]] + constexpr ImVec2 to_im_vec2() const noexcept + { + return {static_cast(this->x), static_cast(this->y)}; + } + [[nodiscard]] + static Vector2 from_im_vec2(const ImVec2& other) noexcept + { + return {static_cast(other.x), static_cast(other.y)}; + } +#endif + }; +} // namespace omath + +template<> struct std::hash> +{ + [[nodiscard]] + std::size_t operator()(const omath::Vector2& vec) const noexcept + { + std::size_t hash = 0; + constexpr std::hash hasher; + + hash ^= hasher(vec.x) + 0x9e3779b9 + (hash << 6) + (hash >> 2); + hash ^= hasher(vec.y) + 0x9e3779b9 + (hash << 6) + (hash >> 2); + + return hash; + } +}; + +template +struct std::formatter> // NOLINT(*-dcl58-cpp) +{ + [[nodiscard]] + static constexpr auto parse(std::format_parse_context& ctx) + { + return ctx.begin(); + } + + template + [[nodiscard]] + static auto format(const omath::Vector2& vec, FormatContext& ctx) + { + if constexpr (std::is_same_v) + return std::format_to(ctx.out(), "[{}, {}]", vec.x, vec.y); + + if constexpr (std::is_same_v) + return std::format_to(ctx.out(), L"[{}, {}]", vec.x, vec.y); + + if constexpr (std::is_same_v) + return std::format_to(ctx.out(), u8"[{}, {}]", vec.x, vec.y); + } +}; diff --git a/include/omath/linear_algebra/vector3.hpp b/include/omath/linear_algebra/vector3.hpp new file mode 100644 index 00000000..ab481f2c --- /dev/null +++ b/include/omath/linear_algebra/vector3.hpp @@ -0,0 +1,318 @@ +// +// Created by vlad on 10/28/23. +// + +#pragma once + +#include "omath/trigonometry/angle.hpp" +#include "omath/linear_algebra/vector2.hpp" +#include +#include +#include + +namespace omath +{ + + enum class Vector3Error + { + IMPOSSIBLE_BETWEEN_ANGLE, + }; + + template + requires std::is_arithmetic_v + class Vector3 : public Vector2 + { + public: + using ContainedType = Type; + Type z = static_cast(0); + constexpr Vector3(const Type& x, const Type& y, const Type& z) noexcept: Vector2(x, y), z(z) + { + } + constexpr Vector3() noexcept: Vector2() {}; + + [[nodiscard]] constexpr bool operator==(const Vector3& other) const noexcept + { + return Vector2::operator==(other) && (other.z == z); + } + + [[nodiscard]] constexpr bool operator!=(const Vector3& other) const noexcept + { + return !(*this == other); + } + + constexpr Vector3& operator+=(const Vector3& other) noexcept + { + Vector2::operator+=(other); + z += other.z; + + return *this; + } + + constexpr Vector3& operator-=(const Vector3& other) noexcept + { + Vector2::operator-=(other); + z -= other.z; + + return *this; + } + + constexpr Vector3& operator*=(const Type& value) noexcept + { + Vector2::operator*=(value); + z *= value; + + return *this; + } + + constexpr Vector3& operator*=(const Vector3& other) noexcept + { + Vector2::operator*=(other); + z *= other.z; + + return *this; + } + + constexpr Vector3& operator/=(const Vector3& other) noexcept + { + Vector2::operator/=(other); + z /= other.z; + + return *this; + } + + constexpr Vector3& operator+=(const Type& value) noexcept + { + Vector2::operator+=(value); + z += value; + + return *this; + } + + constexpr Vector3& operator/=(const Type& value) noexcept + { + Vector2::operator/=(value); + z /= value; + + return *this; + } + + constexpr Vector3& operator-=(const Type& value) noexcept + { + Vector2::operator-=(value); + z -= value; + + return *this; + } + + constexpr Vector3& abs() noexcept + { + Vector2::abs(); + z = z < 0.f ? -z : z; + + return *this; + } + + [[nodiscard]] constexpr Type distance_to_sqr(const Vector3& other) const noexcept + { + return (*this - other).length_sqr(); + } + + [[nodiscard]] constexpr Type dot(const Vector3& other) const noexcept + { + return Vector2::dot(other) + z * other.z; + } + +#ifndef _MSC_VER + [[nodiscard]] constexpr Type length() const + { + return std::hypot(this->x, this->y, z); + } + + [[nodiscard]] constexpr Type length_2d() const + { + return Vector2::length(); + } + [[nodiscard]] Type distance_to(const Vector3& other) const + { + return (*this - other).length(); + } + [[nodiscard]] constexpr Vector3 normalized() const + { + const Type length_value = this->length(); + + return length_value != 0 ? *this / length_value : *this; + } +#else + [[nodiscard]] Type length() const noexcept + { + return std::hypot(this->x, this->y, z); + } + + [[nodiscard]] Vector3 normalized() const noexcept + { + const Type len = this->length(); + + return len != static_cast(0) ? *this / len : *this; + } + + [[nodiscard]] Type length_2d() const noexcept + { + return Vector2::length(); + } + + [[nodiscard]] Type distance_to(const Vector3& v_other) const noexcept + { + return (*this - v_other).length(); + } +#endif + + [[nodiscard]] constexpr Type length_sqr() const noexcept + { + return Vector2::length_sqr() + z * z; + } + + [[nodiscard]] constexpr Vector3 operator-() const noexcept + { + return {-this->x, -this->y, -z}; + } + + [[nodiscard]] constexpr Vector3 operator+(const Vector3& other) const noexcept + { + return {this->x + other.x, this->y + other.y, z + other.z}; + } + + [[nodiscard]] constexpr Vector3 operator-(const Vector3& other) const noexcept + { + return {this->x - other.x, this->y - other.y, z - other.z}; + } + + [[nodiscard]] constexpr Vector3 operator*(const Type& value) const noexcept + { + return {this->x * value, this->y * value, z * value}; + } + + [[nodiscard]] constexpr Vector3 operator*(const Vector3& other) const noexcept + { + return {this->x * other.x, this->y * other.y, z * other.z}; + } + + [[nodiscard]] constexpr Vector3 operator/(const Type& value) const noexcept + { + return {this->x / value, this->y / value, z / value}; + } + + [[nodiscard]] constexpr Vector3 operator/(const Vector3& other) const noexcept + { + return {this->x / other.x, this->y / other.y, z / other.z}; + } + + [[nodiscard]] constexpr Vector3 cross(const Vector3& other) const noexcept + { + return {this->y * other.z - z * other.y, z * other.x - this->x * other.z, + this->x * other.y - this->y * other.x}; + } + + [[nodiscard]] constexpr Type sum() const noexcept + { + return sum_2d() + z; + } + + [[nodiscard]] + bool point_to_same_direction(const Vector3& other) const + { + return dot(other) > static_cast(0); + } + [[nodiscard]] std::expected, Vector3Error> + angle_between(const Vector3& other) const noexcept + { + const auto bottom = length() * other.length(); + + if (bottom == static_cast(0)) + return std::unexpected(Vector3Error::IMPOSSIBLE_BETWEEN_ANGLE); + + return Angle::from_radians(std::acos(dot(other) / bottom)); + } + + [[nodiscard]] bool is_perpendicular(const Vector3& other, Type epsilon = static_cast(0.0001)) const noexcept + { + if (const auto angle = angle_between(other)) + return std::abs(angle->as_degrees() - static_cast(90)) <= epsilon; + + return false; + } + + [[nodiscard]] constexpr Type sum_2d() const noexcept + { + return Vector2::sum(); + } + + [[nodiscard]] constexpr std::tuple as_tuple() const noexcept + { + return std::make_tuple(this->x, this->y, z); + } + + [[nodiscard]] + bool operator<(const Vector3& other) const noexcept + { + return length() < other.length(); + } + + [[nodiscard]] + bool operator>(const Vector3& other) const noexcept + { + return length() > other.length(); + } + + [[nodiscard]] + bool operator<=(const Vector3& other) const noexcept + { + return length() <= other.length(); + } + + [[nodiscard]] + bool operator>=(const Vector3& other) const noexcept + { + return length() >= other.length(); + } + }; +} // namespace omath + +template<> struct std::hash> +{ + [[nodiscard]] + std::size_t operator()(const omath::Vector3& vec) const noexcept + { + std::size_t hash = 0; + constexpr std::hash hasher; + + hash ^= hasher(vec.x) + 0x9e3779b9 + (hash << 6) + (hash >> 2); + hash ^= hasher(vec.y) + 0x9e3779b9 + (hash << 6) + (hash >> 2); + hash ^= hasher(vec.z) + 0x9e3779b9 + (hash << 6) + (hash >> 2); + + return hash; + } +}; + +template +struct std::formatter> // NOLINT(*-dcl58-cpp) +{ + [[nodiscard]] + static constexpr auto parse(std::format_parse_context& ctx) + { + return ctx.begin(); + } + + template + [[nodiscard]] + static auto format(const omath::Vector3& vec, FormatContext& ctx) + { + if constexpr (std::is_same_v) + return std::format_to(ctx.out(), "[{}, {}, {}]", vec.x, vec.y, vec.z); + + if constexpr (std::is_same_v) + return std::format_to(ctx.out(), L"[{}, {}, {}]", vec.x, vec.y, vec.z); + + if constexpr (std::is_same_v) + return std::format_to(ctx.out(), u8"[{}, {}, {}]", vec.x, vec.y, vec.z); + } +}; diff --git a/include/omath/linear_algebra/vector4.hpp b/include/omath/linear_algebra/vector4.hpp new file mode 100644 index 00000000..f045df9a --- /dev/null +++ b/include/omath/linear_algebra/vector4.hpp @@ -0,0 +1,243 @@ +// +// Vector4.h +// +#pragma once + +#include +#include "omath/linear_algebra/vector3.hpp" + +namespace omath +{ + template + requires std::is_arithmetic_v + class Vector4 : public Vector3 + { + public: + using ContainedType = Type; + Type w; + + constexpr Vector4(const Type& x, const Type& y, const Type& z, const Type& w): Vector3(x, y, z), w(w) + { + } + constexpr Vector4() noexcept: Vector3(), w(static_cast(0)) {}; + + [[nodiscard]] + constexpr bool operator==(const Vector4& other) const noexcept + { + return Vector3::operator==(other) && w == other.w; + } + + [[nodiscard]] + constexpr bool operator!=(const Vector4& other) const noexcept + { + return !(*this == other); + } + + constexpr Vector4& operator+=(const Vector4& other) noexcept + { + Vector3::operator+=(other); + w += other.w; + + return *this; + } + + constexpr Vector4& operator-=(const Vector4& other) noexcept + { + Vector3::operator-=(other); + w -= other.w; + + return *this; + } + + constexpr Vector4& operator*=(const Type& value) noexcept + { + Vector3::operator*=(value); + w *= value; + + return *this; + } + + constexpr Vector4& operator*=(const Vector4& other) noexcept + { + Vector3::operator*=(other); + w *= other.w; + + return *this; + } + + constexpr Vector4& operator/=(const Type& value) noexcept + { + Vector3::operator/=(value); + w /= value; + + return *this; + } + + constexpr Vector4& operator/=(const Vector4& other) noexcept + { + Vector3::operator/=(other); + w /= other.w; + return *this; + } + + [[nodiscard]] constexpr Type length_sqr() const noexcept + { + return Vector3::length_sqr() + w * w; + } + + [[nodiscard]] constexpr Type dot(const Vector4& other) const noexcept + { + return Vector3::dot(other) + w * other.w; + } + + [[nodiscard]] Type length() const noexcept + { + return std::sqrt(length_sqr()); + } + + constexpr Vector4& abs() noexcept + { + Vector3::abs(); + w = w < 0.f ? -w : w; + + return *this; + } + constexpr Vector4& clamp(const Type& min, const Type& max) noexcept + { + this->x = std::clamp(this->x, min, max); + this->y = std::clamp(this->y, min, max); + this->z = std::clamp(this->z, min, max); + + return *this; + } + + [[nodiscard]] + constexpr Vector4 operator-() const noexcept + { + return {-this->x, -this->y, -this->z, -w}; + } + + [[nodiscard]] + constexpr Vector4 operator+(const Vector4& other) const noexcept + { + return {this->x + other.x, this->y + other.y, this->z + other.z, w + other.w}; + } + + [[nodiscard]] + constexpr Vector4 operator-(const Vector4& other) const noexcept + { + return {this->x - other.x, this->y - other.y, this->z - other.z, w - other.w}; + } + + [[nodiscard]] + constexpr Vector4 operator*(const Type& value) const noexcept + { + return {this->x * value, this->y * value, this->z * value, w * value}; + } + + [[nodiscard]] + constexpr Vector4 operator*(const Vector4& other) const noexcept + { + return {this->x * other.x, this->y * other.y, this->z * other.z, w * other.w}; + } + + [[nodiscard]] + constexpr Vector4 operator/(const Type& value) const noexcept + { + return {this->x / value, this->y / value, this->z / value, w / value}; + } + + [[nodiscard]] + constexpr Vector4 operator/(const Vector4& other) const noexcept + { + return {this->x / other.x, this->y / other.y, this->z / other.z, w / other.w}; + } + + [[nodiscard]] + constexpr Type sum() const noexcept + { + return Vector3::sum() + w; + } + + [[nodiscard]] + bool operator<(const Vector4& other) const noexcept + { + return length() < other.length(); + } + + [[nodiscard]] + bool operator>(const Vector4& other) const noexcept + { + return length() > other.length(); + } + + [[nodiscard]] + bool operator<=(const Vector4& other) const noexcept + { + return length() <= other.length(); + } + + [[nodiscard]] + bool operator>=(const Vector4& other) const noexcept + { + return length() >= other.length(); + } + +#ifdef OMATH_IMGUI_INTEGRATION + [[nodiscard]] + constexpr ImVec4 to_im_vec4() const noexcept + { + return { + static_cast(this->x), + static_cast(this->y), + static_cast(this->z), + static_cast(w), + }; + } + [[nodiscard]] + static Vector4 from_im_vec4(const ImVec4& other) noexcept + { + return {static_cast(other.x), static_cast(other.y), static_cast(other.z)}; + } +#endif +}; +} // namespace omath + +template<> struct std::hash> +{ + [[nodiscard]] + std::size_t operator()(const omath::Vector4& vec) const noexcept + { + std::size_t hash = 0; + constexpr std::hash hasher; + + hash ^= hasher(vec.x) + 0x9e3779b9 + (hash << 6) + (hash >> 2); + hash ^= hasher(vec.y) + 0x9e3779b9 + (hash << 6) + (hash >> 2); + hash ^= hasher(vec.z) + 0x9e3779b9 + (hash << 6) + (hash >> 2); + hash ^= hasher(vec.w) + 0x9e3779b9 + (hash << 6) + (hash >> 2); + return hash; + } +}; + +template +struct std::formatter> // NOLINT(*-dcl58-cpp) +{ + [[nodiscard]] + static constexpr auto parse(std::format_parse_context& ctx) + { + return ctx.begin(); + } + template + [[nodiscard]] + static auto format(const omath::Vector4& vec, FormatContext& ctx) + { + if constexpr (std::is_same_v) + return std::format_to(ctx.out(), "[{}, {}, {}, {}]", vec.x, vec.y, vec.z, vec.w); + + if constexpr (std::is_same_v) + return std::format_to(ctx.out(), L"[{}, {}, {}, {}]", vec.x, vec.y, vec.z, vec.w); + + if constexpr (std::is_same_v) + return std::format_to(ctx.out(), u8"[{}, {}, {}, {}]", vec.x, vec.y, vec.z, vec.w); + } +}; \ No newline at end of file diff --git a/include/omath/omath.hpp b/include/omath/omath.hpp new file mode 100644 index 00000000..a9615f9b --- /dev/null +++ b/include/omath/omath.hpp @@ -0,0 +1,101 @@ +// +// omath.hpp - Main header file that includes all omath library components +// Created for the omath library +// + +#pragma once + +// Basic math utilities +#include "omath/trigonometry/angles.hpp" +#include "omath/trigonometry/angle.hpp" + +// Vector classes (in dependency order) +#include "omath/linear_algebra/vector2.hpp" +#include "omath/linear_algebra/vector4.hpp" +#include "omath/linear_algebra/vector3.hpp" + +// Matrix classes +#include "omath/linear_algebra/mat.hpp" + +// Color functionality +#include "omath/utility/color.hpp" + +// Geometric primitives +#include "omath/linear_algebra/triangle.hpp" +#include "omath/trigonometry/view_angles.hpp" + +// 3D primitives +#include "omath/3d_primitives/box.hpp" +#include "omath/3d_primitives/plane.hpp" + +// 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" + +// Projectile prediction +#include "omath/projectile_prediction/projectile.hpp" +#include "omath/projectile_prediction/target.hpp" +#include "omath/projectile_prediction/proj_pred_engine.hpp" +#include "omath/projectile_prediction/proj_pred_engine_legacy.hpp" +#include "omath/projectile_prediction/proj_pred_engine_avx2.hpp" + +// Projection functionality +#include "omath/projection/error_codes.hpp" +#include "omath/projection/camera.hpp" + +// Engine-specific implementations + +// IW Engine +#include "omath/engines/iw_engine/constants.hpp" +#include "omath/engines/iw_engine/formulas.hpp" +#include "omath/engines/iw_engine/camera.hpp" +#include "omath/engines/iw_engine/traits/camera_trait.hpp" +#include "omath/engines/iw_engine/traits/pred_engine_trait.hpp" + +// OpenGL Engine +#include "omath/engines/opengl_engine/constants.hpp" +#include "omath/engines/opengl_engine/formulas.hpp" +#include "omath/engines/opengl_engine/camera.hpp" +#include "omath/engines/opengl_engine/traits/camera_trait.hpp" +#include "omath/engines/opengl_engine/traits/pred_engine_trait.hpp" + +// Source Engine +#include "omath/engines/source_engine/constants.hpp" +#include "omath/engines/source_engine/formulas.hpp" +#include "omath/engines/source_engine/camera.hpp" +#include "omath/engines/source_engine/traits/camera_trait.hpp" +#include "omath/engines/source_engine/traits/pred_engine_trait.hpp" + +// Unity Engine +#include "omath/engines/unity_engine/constants.hpp" +#include "omath/engines/unity_engine/formulas.hpp" +#include "omath/engines/unity_engine/camera.hpp" +#include "omath/engines/unity_engine/traits/camera_trait.hpp" +#include "omath/engines/unity_engine/traits/pred_engine_trait.hpp" + +//Frostbite Engine +#include "omath/engines/frostbite_engine/constants.hpp" +#include "omath/engines/frostbite_engine/formulas.hpp" +#include "omath/engines/frostbite_engine/camera.hpp" +#include "omath/engines/frostbite_engine/traits/camera_trait.hpp" +#include "omath/engines/frostbite_engine/traits/pred_engine_trait.hpp" + + +// Unreal Engine +#include "omath/engines/unreal_engine/constants.hpp" +#include "omath/engines/unreal_engine/formulas.hpp" +#include "omath/engines/unreal_engine/camera.hpp" +#include "omath/engines/unreal_engine/traits/camera_trait.hpp" +#include "omath/engines/unreal_engine/traits/pred_engine_trait.hpp" + +// Reverse Engineering +#include "omath/rev_eng/external_rev_object.hpp" +#include "omath/rev_eng/internal_rev_object.hpp" + +// Utility +#include "omath/utility/pattern_scan.hpp" +#include "omath/utility/pe_pattern_scan.hpp" \ No newline at end of file diff --git a/include/omath/pathfinding/a_star.hpp b/include/omath/pathfinding/a_star.hpp new file mode 100644 index 00000000..c785afd4 --- /dev/null +++ b/include/omath/pathfinding/a_star.hpp @@ -0,0 +1,26 @@ +// +// Created by Vlad on 28.07.2024. +// + +#pragma once +#include "omath/linear_algebra/vector3.hpp" +#include "omath/pathfinding/navigation_mesh.hpp" +#include + +namespace omath::pathfinding +{ + struct PathNode; + class Astar final + { + public: + [[nodiscard]] + static std::vector> find_path(const Vector3& start, const Vector3& end, + const NavigationMesh& nav_mesh) noexcept; + + private: + [[nodiscard]] + static std::vector> + reconstruct_final_path(const std::unordered_map, PathNode>& closed_list, + const Vector3& current) noexcept; + }; +} // namespace omath::pathfinding diff --git a/include/omath/pathfinding/navigation_mesh.hpp b/include/omath/pathfinding/navigation_mesh.hpp new file mode 100644 index 00000000..35cc6e6b --- /dev/null +++ b/include/omath/pathfinding/navigation_mesh.hpp @@ -0,0 +1,37 @@ +// +// Created by Vlad on 28.07.2024. +// + +#pragma once + +#include "omath/linear_algebra/vector3.hpp" +#include +#include +#include + +namespace omath::pathfinding +{ + + enum Error + { + }; + + class NavigationMesh final + { + public: + [[nodiscard]] + std::expected, std::string> get_closest_vertex(const Vector3& point) const noexcept; + + [[nodiscard]] + const std::vector>& get_neighbors(const Vector3& vertex) const noexcept; + + [[nodiscard]] + bool empty() const; + + [[nodiscard]] std::vector serialize() const noexcept; + + void deserialize(const std::vector& raw) noexcept; + + std::unordered_map, std::vector>> m_vertex_map; + }; +} // namespace omath::pathfinding diff --git a/include/omath/projectile_prediction/proj_pred_engine.hpp b/include/omath/projectile_prediction/proj_pred_engine.hpp new file mode 100644 index 00000000..bbd5a546 --- /dev/null +++ b/include/omath/projectile_prediction/proj_pred_engine.hpp @@ -0,0 +1,19 @@ +// +// Created by Vlad on 2/23/2025. +// +#pragma once +#include "omath/linear_algebra/vector3.hpp" +#include "omath/projectile_prediction/projectile.hpp" +#include "omath/projectile_prediction/target.hpp" + +namespace omath::projectile_prediction +{ + class ProjPredEngineInterface + { + public: + [[nodiscard]] + virtual std::optional> maybe_calculate_aim_point(const Projectile& projectile, + const Target& target) const = 0; + virtual ~ProjPredEngineInterface() = default; + }; +} // namespace omath::projectile_prediction diff --git a/include/omath/projectile_prediction/proj_pred_engine_avx2.hpp b/include/omath/projectile_prediction/proj_pred_engine_avx2.hpp new file mode 100644 index 00000000..e4a7dc51 --- /dev/null +++ b/include/omath/projectile_prediction/proj_pred_engine_avx2.hpp @@ -0,0 +1,28 @@ +// +// Created by Vlad on 2/23/2025. +// +#pragma once +#include "omath/projectile_prediction/proj_pred_engine.hpp" + +namespace omath::projectile_prediction +{ + class ProjPredEngineAvx2 final : public ProjPredEngineInterface + { + public: + [[nodiscard]] std::optional> + maybe_calculate_aim_point(const Projectile& projectile, const Target& target) const override; + + ProjPredEngineAvx2(float gravity_constant, float simulation_time_step, float maximum_simulation_time); + ~ProjPredEngineAvx2() override = default; + + private: + [[nodiscard]] static std::optional calculate_pitch(const Vector3& proj_origin, + const Vector3& target_pos, + float bullet_gravity, float v0, float time) ; + + // We use [[maybe_unused]] here since AVX2 is not available for ARM and ARM64 CPU + [[maybe_unused]] const float m_gravity_constant; + [[maybe_unused]] const float m_simulation_time_step; + [[maybe_unused]] const float m_maximum_simulation_time; + }; +} // namespace omath::projectile_prediction diff --git a/include/omath/projectile_prediction/proj_pred_engine_legacy.hpp b/include/omath/projectile_prediction/proj_pred_engine_legacy.hpp new file mode 100644 index 00000000..bd75554e --- /dev/null +++ b/include/omath/projectile_prediction/proj_pred_engine_legacy.hpp @@ -0,0 +1,136 @@ +// +// Created by Vlad on 6/9/2024. +// + +#pragma once + +#include "omath/engines/source_engine/traits/pred_engine_trait.hpp" +#include "omath/linear_algebra/vector3.hpp" +#include "omath/projectile_prediction/proj_pred_engine.hpp" +#include "omath/projectile_prediction/projectile.hpp" +#include "omath/projectile_prediction/target.hpp" +#include + +namespace omath::projectile_prediction +{ + template + concept PredEngineConcept = + requires(const Projectile& projectile, const Target& target, const Vector3& vec_a, + const Vector3& vec_b, + Vector3 v3, // by-value for calc_viewpoint_from_angles + float pitch, float yaw, float time, float gravity, std::optional maybe_pitch) { + // Presence + return types + { + T::predict_projectile_position(projectile, pitch, yaw, time, gravity) + } -> std::same_as>; + { T::predict_target_position(target, time, gravity) } -> std::same_as>; + { T::calc_vector_2d_distance(vec_a) } -> std::same_as; + { T::get_vector_height_coordinate(vec_b) } -> std::same_as; + { T::calc_viewpoint_from_angles(projectile, v3, maybe_pitch) } -> std::same_as>; + { T::calc_direct_pitch_angle(vec_a, vec_b) } -> std::same_as; + { T::calc_direct_yaw_angle(vec_a, vec_b) } -> std::same_as; + + // Enforce noexcept as in PredEngineTrait + requires noexcept(T::predict_projectile_position(projectile, pitch, yaw, time, gravity)); + requires noexcept(T::predict_target_position(target, time, gravity)); + requires noexcept(T::calc_vector_2d_distance(vec_a)); + requires noexcept(T::get_vector_height_coordinate(vec_b)); + requires noexcept(T::calc_viewpoint_from_angles(projectile, v3, maybe_pitch)); + requires noexcept(T::calc_direct_pitch_angle(vec_a, vec_b)); + requires noexcept(T::calc_direct_yaw_angle(vec_a, vec_b)); + }; + template + requires PredEngineConcept + class ProjPredEngineLegacy final : public ProjPredEngineInterface + { + public: + explicit ProjPredEngineLegacy(const float gravity_constant, const float simulation_time_step, + const float maximum_simulation_time, const float distance_tolerance) + : m_gravity_constant(gravity_constant), m_simulation_time_step(simulation_time_step), + m_maximum_simulation_time(maximum_simulation_time), m_distance_tolerance(distance_tolerance) + { + } + + [[nodiscard]] + std::optional> maybe_calculate_aim_point(const Projectile& projectile, + const Target& target) const override + { + for (float time = 0.f; time < m_maximum_simulation_time; time += m_simulation_time_step) + { + const auto predicted_target_position = + EngineTrait::predict_target_position(target, time, m_gravity_constant); + + const auto projectile_pitch = + maybe_calculate_projectile_launch_pitch_angle(projectile, predicted_target_position); + + if (!projectile_pitch.has_value()) [[unlikely]] + continue; + + if (!is_projectile_reached_target(predicted_target_position, projectile, projectile_pitch.value(), + time)) + continue; + + return EngineTrait::calc_viewpoint_from_angles(projectile, predicted_target_position, projectile_pitch); + } + return std::nullopt; + } + + private: + const float m_gravity_constant; + const float m_simulation_time_step; + const float m_maximum_simulation_time; + const float m_distance_tolerance; + + // Realization of this formula: + // https://stackoverflow.com/questions/54917375/how-to-calculate-the-angle-to-shoot-a-bullet-in-order-to-hit-a-moving-target + /* + \[ + \theta \;=\; \arctan\!\Biggl( + \frac{% + v^{2}\;\pm\;\sqrt{\,v^{4}-g\!\left(gx^{2}+2yv^{2}\right)\,} + }{% + gx + }\Biggr) + \] + */ + [[nodiscard]] + std::optional + maybe_calculate_projectile_launch_pitch_angle(const Projectile& projectile, + const Vector3& target_position) const noexcept + { + const auto bullet_gravity = m_gravity_constant * projectile.m_gravity_scale; + + if (bullet_gravity == 0.f) + return EngineTrait::calc_direct_pitch_angle(projectile.m_origin, target_position); + + const auto delta = target_position - projectile.m_origin; + + const auto distance2d = EngineTrait::calc_vector_2d_distance(delta); + const auto distance2d_sqr = distance2d * distance2d; + const auto launch_speed_sqr = projectile.m_launch_speed * projectile.m_launch_speed; + + float root = launch_speed_sqr * launch_speed_sqr + - bullet_gravity + * (bullet_gravity * distance2d_sqr + + 2.0f * EngineTrait::get_vector_height_coordinate(delta) * launch_speed_sqr); + + if (root < 0.0f) [[unlikely]] + return std::nullopt; + + root = std::sqrt(root); + const float angle = std::atan((launch_speed_sqr - root) / (bullet_gravity * distance2d)); + + return angles::radians_to_degrees(angle); + } + [[nodiscard]] + bool is_projectile_reached_target(const Vector3& target_position, const Projectile& projectile, + const float pitch, const float time) const noexcept + { + const auto yaw = EngineTrait::calc_direct_yaw_angle(projectile.m_origin, target_position); + const auto projectile_position = + EngineTrait::predict_projectile_position(projectile, pitch, yaw, time, m_gravity_constant); + + return projectile_position.distance_to(target_position) <= m_distance_tolerance; + } + }; +} // namespace omath::projectile_prediction diff --git a/include/omath/projectile_prediction/projectile.hpp b/include/omath/projectile_prediction/projectile.hpp new file mode 100644 index 00000000..c4560ed4 --- /dev/null +++ b/include/omath/projectile_prediction/projectile.hpp @@ -0,0 +1,17 @@ +// +// Created by Vlad on 6/9/2024. +// + +#pragma once +#include "omath/linear_algebra/vector3.hpp" + +namespace omath::projectile_prediction +{ + class Projectile final + { + public: + Vector3 m_origin; + float m_launch_speed{}; + float m_gravity_scale{}; + }; +} // namespace omath::projectile_prediction \ No newline at end of file diff --git a/include/omath/projectile_prediction/target.hpp b/include/omath/projectile_prediction/target.hpp new file mode 100644 index 00000000..5b3f5db2 --- /dev/null +++ b/include/omath/projectile_prediction/target.hpp @@ -0,0 +1,17 @@ +// +// Created by Vlad on 6/9/2024. +// + +#pragma once +#include "omath/linear_algebra/vector3.hpp" + +namespace omath::projectile_prediction +{ + class Target final + { + public: + Vector3 m_origin; + Vector3 m_velocity; + bool m_is_airborne{}; + }; +} // namespace omath::projectile_prediction \ No newline at end of file diff --git a/include/omath/projection/camera.hpp b/include/omath/projection/camera.hpp new file mode 100644 index 00000000..37a3ef8a --- /dev/null +++ b/include/omath/projection/camera.hpp @@ -0,0 +1,363 @@ +// +// Created by Vlad on 27.08.2024. +// + +#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 +#include +#include +#include + +#ifdef OMATH_BUILD_TESTS +// ReSharper disable CppInconsistentNaming +class UnitTestProjection_Projection_Test; +class UnitTestProjection_ScreenToNdcTopLeft_Test; +class UnitTestProjection_ScreenToNdcBottomLeft_Test; +// ReSharper restore CppInconsistentNaming + +#endif + +namespace omath::projection +{ + class ViewPort final + { + public: + float m_width; + float m_height; + + [[nodiscard]] constexpr float aspect_ratio() const + { + return m_width / m_height; + } + }; + using FieldOfView = Angle; + + template + concept CameraEngineConcept = + requires(const Vector3& cam_origin, const Vector3& look_at, const ViewAnglesType& angles, + const FieldOfView& fov, const ViewPort& viewport, float znear, float zfar) { + // Presence + return types + { T::calc_look_at_angle(cam_origin, look_at) } -> std::same_as; + { T::calc_view_matrix(angles, cam_origin) } -> std::same_as; + { T::calc_projection_matrix(fov, viewport, znear, zfar) } -> std::same_as; + + // Enforce noexcept as in the trait declaration + requires noexcept(T::calc_look_at_angle(cam_origin, look_at)); + requires noexcept(T::calc_view_matrix(angles, cam_origin)); + requires noexcept(T::calc_projection_matrix(fov, viewport, znear, zfar)); + }; + + template + requires CameraEngineConcept + class Camera final + { +#ifdef OMATH_BUILD_TESTS + friend UnitTestProjection_Projection_Test; + friend UnitTestProjection_ScreenToNdcTopLeft_Test; + friend UnitTestProjection_ScreenToNdcBottomLeft_Test; +#endif + public: + enum class ScreenStart + { + TOP_LEFT_CORNER, + BOTTOM_LEFT_CORNER, + }; + + ~Camera() = default; + Camera(const Vector3& position, const ViewAnglesType& view_angles, const ViewPort& view_port, + const FieldOfView& fov, const float near, const float far) noexcept + : m_view_port(view_port), m_field_of_view(fov), m_far_plane_distance(far), m_near_plane_distance(near), + m_view_angles(view_angles), m_origin(position) + { + } + + void look_at(const Vector3& target) + { + m_view_angles = TraitClass::calc_look_at_angle(m_origin, target); + m_view_projection_matrix = std::nullopt; + } + + protected: + [[nodiscard]] Mat4X4Type calc_view_projection_matrix() const noexcept + { + return TraitClass::calc_projection_matrix(m_field_of_view, m_view_port, m_near_plane_distance, + m_far_plane_distance) + * TraitClass::calc_view_matrix(m_view_angles, m_origin); + } + + public: + [[nodiscard]] const Mat4X4Type& get_view_projection_matrix() const noexcept + { + if (!m_view_projection_matrix.has_value()) + m_view_projection_matrix = calc_view_projection_matrix(); + + return m_view_projection_matrix.value(); + } + + void set_field_of_view(const FieldOfView& fov) noexcept + { + m_field_of_view = fov; + m_view_projection_matrix = std::nullopt; + } + + void set_near_plane(const float near) noexcept + { + m_near_plane_distance = near; + m_view_projection_matrix = std::nullopt; + } + + void set_far_plane(const float far) noexcept + { + m_far_plane_distance = far; + m_view_projection_matrix = std::nullopt; + } + + void set_view_angles(const ViewAnglesType& view_angles) noexcept + { + m_view_angles = view_angles; + m_view_projection_matrix = std::nullopt; + } + + void set_origin(const Vector3& origin) noexcept + { + m_origin = origin; + m_view_projection_matrix = std::nullopt; + } + + void set_view_port(const ViewPort& view_port) noexcept + { + m_view_port = view_port; + m_view_projection_matrix = std::nullopt; + } + + [[nodiscard]] const FieldOfView& get_field_of_view() const noexcept + { + return m_field_of_view; + } + + [[nodiscard]] const float& get_near_plane() const noexcept + { + return m_near_plane_distance; + } + + [[nodiscard]] const float& get_far_plane() const noexcept + { + return m_far_plane_distance; + } + + [[nodiscard]] const ViewAnglesType& get_view_angles() const noexcept + { + return m_view_angles; + } + + [[nodiscard]] const Vector3& get_origin() const noexcept + { + return m_origin; + } + + template + [[nodiscard]] std::expected, Error> + world_to_screen(const Vector3& world_position) const noexcept + { + const auto normalized_cords = world_to_view_port(world_position); + + if (!normalized_cords.has_value()) + return std::unexpected{normalized_cords.error()}; + + if constexpr (screen_start == ScreenStart::TOP_LEFT_CORNER) + return ndc_to_screen_position_from_top_left_corner(*normalized_cords); + else if constexpr (screen_start == ScreenStart::BOTTOM_LEFT_CORNER) + return ndc_to_screen_position_from_bottom_left_corner(*normalized_cords); + else + std::unreachable(); + } + + [[nodiscard]] bool is_culled_by_frustum(const Triangle>& triangle) const noexcept + { + // Transform to clip space (before perspective divide) + auto to_clip = [this](const Vector3& point) + { + auto clip = get_view_projection_matrix() + * mat_column_from_vector(point); + return std::array{ + 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& a, const std::array& b, + const std::array& 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, Error> + world_to_view_port(const Vector3& world_position) const noexcept + { + auto projected = get_view_projection_matrix() + * mat_column_from_vector(world_position); + + const auto& w = projected.at(3, 0); + if (w <= std::numeric_limits::epsilon()) + return std::unexpected(Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS); + + projected /= w; + + if (is_ndc_out_of_bounds(projected)) + return std::unexpected(Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS); + + return Vector3{projected.at(0, 0), projected.at(1, 0), projected.at(2, 0)}; + } + [[nodiscard]] + std::expected, Error> view_port_to_screen(const Vector3& ndc) const noexcept + { + const auto inv_view_proj = get_view_projection_matrix().inverted(); + + if (!inv_view_proj) + return std::unexpected(Error::INV_VIEW_PROJ_MAT_DET_EQ_ZERO); + + auto inverted_projection = + inv_view_proj.value() * mat_column_from_vector(ndc); + + const auto& w = inverted_projection.at(3, 0); + + if (std::abs(w) < std::numeric_limits::epsilon()) + return std::unexpected(Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS); + + inverted_projection /= w; + + return Vector3{inverted_projection.at(0, 0), inverted_projection.at(1, 0), + inverted_projection.at(2, 0)}; + } + + template + [[nodiscard]] + std::expected, Error> screen_to_world(const Vector3& screen_pos) const noexcept + { + return view_port_to_screen(screen_to_ndc(screen_pos)); + } + + template + [[nodiscard]] + std::expected, Error> screen_to_world(const Vector2& screen_pos) const noexcept + { + const auto& [x, y] = screen_pos; + return screen_to_world({x, y, 1.f}); + } + + protected: + ViewPort m_view_port{}; + Angle m_field_of_view; + + mutable std::optional m_view_projection_matrix; + + float m_far_plane_distance; + float m_near_plane_distance; + + ViewAnglesType m_view_angles; + Vector3 m_origin; + + private: + template + [[nodiscard]] constexpr static bool is_ndc_out_of_bounds(const Type& ndc) noexcept + { + constexpr auto eps = std::numeric_limits::epsilon(); + return std::ranges::any_of(ndc.raw_array(), + [](const auto& val) { return val < -1.0f - eps || val > 1.0f + eps; }); + } + + // NDC REPRESENTATION: + /* + ^ + | y + 1 | + | + | + -1 ---------0--------- 1 --> x + | + | + -1 | + v + */ + + [[nodiscard]] Vector3 + ndc_to_screen_position_from_top_left_corner(const Vector3& ndc) const noexcept + { + /* + +------------------------> + | (0, 0) + | + | + | + | + | + | + ⌄ + */ + return {(ndc.x + 1.f) / 2.f * m_view_port.m_width, (ndc.y / -2.f + 0.5f) * m_view_port.m_height, ndc.z}; + } + + [[nodiscard]] Vector3 + ndc_to_screen_position_from_bottom_left_corner(const Vector3& ndc) const noexcept + { + /* + ^ + | + | + | + | + | + | + | (0, 0) + +------------------------> + */ + return {(ndc.x + 1.f) / 2.f * m_view_port.m_width, (ndc.y / 2.f + 0.5f) * m_view_port.m_height, ndc.z}; + } + + template + [[nodiscard]] Vector3 screen_to_ndc(const Vector3& screen_pos) const noexcept + { + 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 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 + std::unreachable(); + } + }; +} // namespace omath::projection diff --git a/include/omath/projection/error_codes.hpp b/include/omath/projection/error_codes.hpp new file mode 100644 index 00000000..0129af2c --- /dev/null +++ b/include/omath/projection/error_codes.hpp @@ -0,0 +1,15 @@ +// +// Created by Vlad on 03.09.2024. +// + +#pragma once +#include + +namespace omath::projection +{ + enum class Error : uint16_t + { + WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS, + INV_VIEW_PROJ_MAT_DET_EQ_ZERO, + }; +} \ No newline at end of file diff --git a/include/omath/rev_eng/external_rev_object.hpp b/include/omath/rev_eng/external_rev_object.hpp new file mode 100644 index 00000000..9d42b72e --- /dev/null +++ b/include/omath/rev_eng/external_rev_object.hpp @@ -0,0 +1,35 @@ +// +// Created by Vlad on 10/4/2025. +// + +#pragma once +#include +#include + +namespace omath::rev_eng +{ + template + class ExternalReverseEngineeredObject + { + public: + explicit ExternalReverseEngineeredObject(const std::uintptr_t addr): m_object_address(addr) + { + } + private: + std::uintptr_t m_object_address{}; + + protected: + template + [[nodiscard]] + Type get_by_offset(const std::ptrdiff_t offset) const + { + return ExternalMemoryManagementTrait::read_memory(m_object_address+offset); + } + + template + void set_by_offset(const std::ptrdiff_t offset, const Type& value) const + { + return ExternalMemoryManagementTrait::write_memory(m_object_address+offset, value); + } + }; +} // namespace omath::rev_eng \ No newline at end of file diff --git a/include/omath/rev_eng/internal_rev_object.hpp b/include/omath/rev_eng/internal_rev_object.hpp new file mode 100644 index 00000000..56f205a9 --- /dev/null +++ b/include/omath/rev_eng/internal_rev_object.hpp @@ -0,0 +1,48 @@ +// +// Created by Vlad on 8/8/2025. +// + +#pragma once +#include +#include + +namespace omath::rev_eng +{ + class InternalReverseEngineeredObject + { + protected: + template + [[nodiscard]] Type& get_by_offset(const std::ptrdiff_t offset) + { + return *reinterpret_cast(reinterpret_cast(this) + offset); + } + + template + [[nodiscard]] const Type& get_by_offset(const std::ptrdiff_t offset) const + { + return *reinterpret_cast(reinterpret_cast(this) + offset); + } + + template + ReturnType call_virtual_method(auto... arg_list) + { +#ifdef _MSC_VER + using VirtualMethodType = ReturnType(__thiscall*)(void*, decltype(arg_list)...); +#else + using VirtualMethodType = ReturnType (*)(void*, decltype(arg_list)...); +#endif + return (*reinterpret_cast(this))[id](this, arg_list...); + } + template + ReturnType call_virtual_method(auto... arg_list) const + { +#ifdef _MSC_VER + using VirtualMethodType = ReturnType(__thiscall*)(void*, decltype(arg_list)...); +#else + using VirtualMethodType = ReturnType (*)(void*, decltype(arg_list)...); +#endif + return (*static_cast((void*)(this)))[id]( + const_cast(static_cast(this)), arg_list...); + } + }; +} // namespace omath::rev_eng diff --git a/include/omath/trigonometry/angle.hpp b/include/omath/trigonometry/angle.hpp new file mode 100644 index 00000000..ba785969 --- /dev/null +++ b/include/omath/trigonometry/angle.hpp @@ -0,0 +1,211 @@ +// +// Created by Orange on 11/30/2024. +// + +#pragma once +#include "omath/trigonometry/angles.hpp" +#include +#include +#include + +namespace omath +{ + enum class AngleFlags + { + Normalized = 0, + Clamped = 1, + }; + + template + requires std::is_arithmetic_v + class Angle + { + Type m_angle; + constexpr explicit Angle(const Type& degrees) noexcept + { + if constexpr (flags == AngleFlags::Normalized) + m_angle = angles::wrap_angle(degrees, min, max); + + else if constexpr (flags == AngleFlags::Clamped) + m_angle = std::clamp(degrees, min, max); + else + { + static_assert(false); + std::unreachable(); + } + } + + public: + [[nodiscard]] + constexpr static Angle from_degrees(const Type& degrees) noexcept + { + return Angle{degrees}; + } + constexpr Angle() noexcept: m_angle(0) + { + } + [[nodiscard]] + constexpr static Angle from_radians(const Type& degrees) noexcept + { + return Angle{angles::radians_to_degrees(degrees)}; + } + + [[nodiscard]] + constexpr const Type& operator*() const noexcept + { + return m_angle; + } + + [[nodiscard]] + constexpr Type as_degrees() const noexcept + { + return m_angle; + } + + [[nodiscard]] + constexpr Type as_radians() const noexcept + { + return angles::degrees_to_radians(m_angle); + } + + [[nodiscard]] + Type sin() const noexcept + { + return std::sin(as_radians()); + } + + [[nodiscard]] + Type cos() const noexcept + { + return std::cos(as_radians()); + } + + [[nodiscard]] + Type tan() const noexcept + { + return std::tan(as_radians()); + } + + [[nodiscard]] + Type atan() const noexcept + { + return std::atan(as_radians()); + } + + [[nodiscard]] + Type cot() const noexcept + { + return cos() / sin(); + } + + constexpr Angle& operator+=(const Angle& other) noexcept + { + if constexpr (flags == AngleFlags::Normalized) + m_angle = angles::wrap_angle(m_angle + other.m_angle, min, max); + + else if constexpr (flags == AngleFlags::Clamped) + m_angle = std::clamp(m_angle + other.m_angle, min, max); + else + { + static_assert(false); + std::unreachable(); + } + + return *this; + } + + [[nodiscard]] + constexpr std::partial_ordering operator<=>(const Angle& other) const noexcept = default; + + constexpr Angle& operator-=(const Angle& other) noexcept + { + return operator+=(-other); + } + + [[nodiscard]] + constexpr Angle operator+(const Angle& other) noexcept + { + if constexpr (flags == AngleFlags::Normalized) + return Angle{angles::wrap_angle(m_angle + other.m_angle, min, max)}; + + else if constexpr (flags == AngleFlags::Clamped) + return Angle{std::clamp(m_angle + other.m_angle, min, max)}; + + else + static_assert(false); + + std::unreachable(); + } + + [[nodiscard]] + constexpr Angle operator-(const Angle& other) noexcept + { + return operator+(-other); + } + + [[nodiscard]] + constexpr Angle operator-() const noexcept + { + return Angle{-m_angle}; + } + }; +} // namespace omath + +template +struct std::formatter, char> // NOLINT(*-dcl58-cpp) +{ + using AngleT = omath::Angle; + + static constexpr auto parse(std::format_parse_context& ctx) + { + return ctx.begin(); + } + + template + [[nodiscard]] + auto format(const AngleT& a, FormatContext& ctx) const + { + static_assert(std::is_same_v); + return std::format_to(ctx.out(), "{}deg", a.as_degrees()); + } +}; + +// wchar_t formatter +template +struct std::formatter, wchar_t> // NOLINT(*-dcl58-cpp) +{ + using AngleT = omath::Angle; + + static constexpr auto parse(std::wformat_parse_context& ctx) + { + return ctx.begin(); + } + + template + [[nodiscard]] + auto format(const AngleT& a, FormatContext& ctx) const + { + static_assert(std::is_same_v); + return std::format_to(ctx.out(), L"{}deg", a.as_degrees()); + } +}; + +// wchar_t formatter +template +struct std::formatter, char8_t> // NOLINT(*-dcl58-cpp) +{ + using AngleT = omath::Angle; + + static constexpr auto parse(std::wformat_parse_context& ctx) + { + return ctx.begin(); + } + + template + [[nodiscard]] + auto format(const AngleT& a, FormatContext& ctx) const + { + static_assert(std::is_same_v); + return std::format_to(ctx.out(), u8"{}deg", a.as_degrees()); + } +}; diff --git a/include/omath/trigonometry/angles.hpp b/include/omath/trigonometry/angles.hpp new file mode 100644 index 00000000..ce07dd83 --- /dev/null +++ b/include/omath/trigonometry/angles.hpp @@ -0,0 +1,64 @@ +// +// Created by vlad on 11/6/23. +// + +#pragma once +#include +#include + +namespace omath::angles +{ + template + requires std::is_floating_point_v + [[nodiscard]] constexpr Type radians_to_degrees(const Type& radians) noexcept + { + return radians * (static_cast(180) / std::numbers::pi_v); + } + + template + requires std::is_floating_point_v + [[nodiscard]] constexpr Type degrees_to_radians(const Type& degrees) noexcept + { + return degrees * (std::numbers::pi_v / static_cast(180)); + } + + template + requires std::is_floating_point_v + [[nodiscard]] Type horizontal_fov_to_vertical(const Type& horizontal_fov, const Type& aspect) noexcept + { + const auto fov_rad = degrees_to_radians(horizontal_fov); + + const auto vert_fov = static_cast(2) * std::atan(std::tan(fov_rad / static_cast(2)) / aspect); + + return radians_to_degrees(vert_fov); + } + + template + requires std::is_floating_point_v + [[nodiscard]] Type vertical_fov_to_horizontal(const Type& vertical_fov, const Type& aspect) noexcept + { + const auto fov_as_radians = degrees_to_radians(vertical_fov); + + const auto horizontal_fov = + static_cast(2) * std::atan(std::tan(fov_as_radians / static_cast(2)) * aspect); + + return radians_to_degrees(horizontal_fov); + } + + template + requires std::is_arithmetic_v + [[nodiscard]] Type wrap_angle(const Type& angle, const Type& min, const Type& max) noexcept + { + if (angle <= max && angle >= min) + return angle; + + const Type range = max - min; + + Type wrapped_angle = std::fmod(angle - min, range); + + if (wrapped_angle < 0) + wrapped_angle += range; + + return wrapped_angle + min; + } +} // namespace omath::angles diff --git a/include/omath/trigonometry/view_angles.hpp b/include/omath/trigonometry/view_angles.hpp new file mode 100644 index 00000000..cd63640a --- /dev/null +++ b/include/omath/trigonometry/view_angles.hpp @@ -0,0 +1,15 @@ +// +// Created by Orange on 11/30/2024. +// +#pragma once + +namespace omath +{ + template + struct ViewAngles + { + PitchType pitch; + YawType yaw; + RollType roll; + }; +} // namespace omath diff --git a/include/omath/utility/color.hpp b/include/omath/utility/color.hpp new file mode 100644 index 00000000..a1a3c437 --- /dev/null +++ b/include/omath/utility/color.hpp @@ -0,0 +1,211 @@ +// +// Created by vlad on 2/4/24. +// + +#pragma once + +#include "omath/linear_algebra/vector4.hpp" +#include + +namespace omath +{ + struct Hsv final + { + float hue{}; + float saturation{}; + float value{}; + }; + + class Color final : public Vector4 + { + public: + constexpr Color(const float r, const float g, const float b, const float a) noexcept: Vector4(r, g, b, a) + { + clamp(0.f, 1.f); + } + + constexpr explicit Color() noexcept = default; + [[nodiscard]] + constexpr static Color from_rgba(const uint8_t r, const uint8_t g, const uint8_t b, const uint8_t a) noexcept + { + return Color{Vector4(r, g, b, a) / 255.f}; + } + + [[nodiscard]] + constexpr static Color from_hsv(float hue, const float saturation, const float value) noexcept + { + float r{}, g{}, b{}; + + hue = std::clamp(hue, 0.f, 1.f); + + const int i = static_cast(hue * 6.f); + const float f = hue * 6.f - static_cast(i); + const float p = value * (1 - saturation); + const float q = value * (1 - f * saturation); + const float t = value * (1 - (1 - f) * saturation); + + 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: + std::unreachable(); + } + + return {r, g, b, 1.f}; + } + + [[nodiscard]] + constexpr static Color from_hsv(const Hsv& hsv) noexcept + { + return from_hsv(hsv.hue, hsv.saturation, hsv.value); + } + + [[nodiscard]] + constexpr Hsv to_hsv() const noexcept + { + Hsv hsv_data; + + const float& red = x; + const float& green = y; + const float& blue = z; + + const float max = std::max({red, green, blue}); + const float min = std::min({red, green, blue}); + const float delta = max - min; + + if (delta == 0.f) + hsv_data.hue = 0.f; + + else if (max == red) + hsv_data.hue = 60.f * (std::fmod(static_cast((green - blue) / delta), 6.f)); + else if (max == green) + hsv_data.hue = 60.f * (((blue - red) / delta) + 2.f); + else if (max == blue) + hsv_data.hue = 60.f * (((red - green) / delta) + 4.f); + + if (hsv_data.hue < 0.f) + hsv_data.hue += 360.f; + + hsv_data.hue /= 360.f; + hsv_data.saturation = max == 0.f ? 0.f : delta / max; + hsv_data.value = max; + + return hsv_data; + } + + constexpr explicit Color(const Vector4& vec) noexcept: Vector4(vec) + { + clamp(0.f, 1.f); + } + constexpr void set_hue(const float hue) noexcept + { + auto hsv = to_hsv(); + hsv.hue = hue; + + *this = from_hsv(hsv); + } + + constexpr void set_saturation(const float saturation) noexcept + { + auto hsv = to_hsv(); + hsv.saturation = saturation; + + *this = from_hsv(hsv); + } + + constexpr void set_value(const float value) noexcept + { + auto hsv = to_hsv(); + hsv.value = value; + + *this = from_hsv(hsv); + } + [[nodiscard]] + constexpr Color blend(const Color& other, float ratio) const noexcept + { + ratio = std::clamp(ratio, 0.f, 1.f); + return Color(*this * (1.f - ratio) + other * ratio); + } + + [[nodiscard]] static constexpr Color red() + { + return {1.f, 0.f, 0.f, 1.f}; + } + [[nodiscard]] static constexpr Color green() + { + return {0.f, 1.f, 0.f, 1.f}; + } + [[nodiscard]] static constexpr Color blue() + { + return {0.f, 0.f, 1.f, 1.f}; + } +#ifdef OMATH_IMGUI_INTEGRATION + [[nodiscard]] + ImColor to_im_color() const noexcept + { + return {to_im_vec4()}; + } +#endif + [[nodiscard]] std::string to_string() const noexcept + { + return std::format("[r:{}, g:{}, b:{}, a:{}]", + static_cast(x * 255.f), + static_cast(y * 255.f), + static_cast(z * 255.f), + static_cast(w * 255.f)); + } + [[nodiscard]] std::wstring to_wstring() const noexcept + { + const auto ascii_string = to_string(); + return {ascii_string.cbegin(), ascii_string.cend()}; + } + + // ReSharper disable once CppInconsistentNaming + [[nodiscard]] std::u8string to_u8string() const noexcept + { + const auto ascii_string = to_string(); + return {ascii_string.cbegin(), ascii_string.cend()}; + } + }; +} // namespace omath +template<> +struct std::formatter // NOLINT(*-dcl58-cpp) +{ + [[nodiscard]] + static constexpr auto parse(const std::format_parse_context& ctx) + { + return ctx.begin(); + } + + template + [[nodiscard]] + static auto format(const omath::Color& col, FormatContext& ctx) + { + if constexpr (std::is_same_v) + return std::format_to(ctx.out(), "{}", col.to_string()); + if constexpr (std::is_same_v) + return std::format_to(ctx.out(), L"{}", col.to_wstring()); + + if constexpr (std::is_same_v) + return std::format_to(ctx.out(), u8"{}", col.to_u8string()); + + std::unreachable(); + } +}; \ No newline at end of file diff --git a/include/omath/utility/elf_pattern_scan.hpp b/include/omath/utility/elf_pattern_scan.hpp new file mode 100644 index 00000000..97375dec --- /dev/null +++ b/include/omath/utility/elf_pattern_scan.hpp @@ -0,0 +1,25 @@ +// +// Created by Vladislav on 30.12.2025. +// +#pragma once +#include +#include +#include +#include +#include "section_scan_result.hpp" +namespace omath +{ + class ElfPatternScanner final + { + public: + [[nodiscard]] + static std::optional + scan_for_pattern_in_loaded_module(const void* module_base_address, const std::string_view& pattern, + const std::string_view& target_section_name = ".text"); + + [[nodiscard]] + static std::optional + scan_for_pattern_in_file(const std::filesystem::path& path_to_file, const std::string_view& pattern, + const std::string_view& target_section_name = ".text"); + }; +} // namespace omath \ No newline at end of file diff --git a/include/omath/utility/macho_pattern_scan.hpp b/include/omath/utility/macho_pattern_scan.hpp new file mode 100644 index 00000000..a4bc9a46 --- /dev/null +++ b/include/omath/utility/macho_pattern_scan.hpp @@ -0,0 +1,25 @@ +// +// Created by Copilot on 04.02.2026. +// +#pragma once +#include +#include +#include +#include +#include "section_scan_result.hpp" +namespace omath +{ + class MachOPatternScanner final + { + public: + [[nodiscard]] + static std::optional + scan_for_pattern_in_loaded_module(const void* module_base_address, const std::string_view& pattern, + const std::string_view& target_section_name = "__text"); + + [[nodiscard]] + static std::optional + scan_for_pattern_in_file(const std::filesystem::path& path_to_file, const std::string_view& pattern, + const std::string_view& target_section_name = "__text"); + }; +} // namespace omath diff --git a/include/omath/utility/pattern_scan.hpp b/include/omath/utility/pattern_scan.hpp new file mode 100644 index 00000000..9f8dd937 --- /dev/null +++ b/include/omath/utility/pattern_scan.hpp @@ -0,0 +1,82 @@ +// +// Created by Vlad on 10/4/2025. +// + +#pragma once +#include +#include +#include +#include +#include + +// ReSharper disable CppInconsistentNaming +class unit_test_pattern_scan_read_test_Test; +class unit_test_pattern_scan_corner_case_1_Test; +class unit_test_pattern_scan_corner_case_2_Test; +class unit_test_pattern_scan_corner_case_3_Test; +class unit_test_pattern_scan_corner_case_4_Test; +// ReSharper restore CppInconsistentNaming +namespace omath +{ + enum class PatternScanError + { + INVALID_PATTERN_STRING + }; + class PatternScanner final + { + friend unit_test_pattern_scan_read_test_Test; + friend unit_test_pattern_scan_corner_case_1_Test; + friend unit_test_pattern_scan_corner_case_2_Test; + friend unit_test_pattern_scan_corner_case_3_Test; + friend unit_test_pattern_scan_corner_case_4_Test; + + public: + [[nodiscard]] + static std::span::iterator scan_for_pattern(const std::span& range, + const std::string_view& pattern); + + [[nodiscard]] + static std::span::iterator scan_for_pattern(std::span&& range, + const std::string_view& pattern) = delete; + + template + requires std::input_or_output_iterator> + static IteratorType scan_for_pattern(const IteratorType& begin, const IteratorType& end, + const std::string_view& pattern) + { + const auto parsed_pattern = parse_pattern(pattern); + + if (!parsed_pattern) [[unlikely]] + return end; + + const auto whole_range_size = static_cast(std::distance(begin, end)); + + const auto pattern_size = static_cast(parsed_pattern->size()); + const std::ptrdiff_t scan_size = whole_range_size - pattern_size; + + if (scan_size < 0) + return end; + + for (std::ptrdiff_t i = 0; i <= scan_size; i++) + { + bool found = true; + + for (std::ptrdiff_t j = 0; j < static_cast(parsed_pattern->size()); j++) + { + found = parsed_pattern->at(j) == std::nullopt || parsed_pattern->at(j) == *(begin + i + j); + + if (!found) + break; + } + if (found) + return begin + i; + } + return end; + } + + private: + [[nodiscard]] + static std::expected>, PatternScanError> + parse_pattern(const std::string_view& pattern_string); + }; +} // namespace omath \ No newline at end of file diff --git a/include/omath/utility/pe_pattern_scan.hpp b/include/omath/utility/pe_pattern_scan.hpp new file mode 100644 index 00000000..12d67927 --- /dev/null +++ b/include/omath/utility/pe_pattern_scan.hpp @@ -0,0 +1,27 @@ +// +// Created by Vlad on 10/7/2025. +// + +#pragma once +#include +#include +#include +#include +#include "section_scan_result.hpp" +namespace omath +{ + + class PePatternScanner final + { + public: + [[nodiscard]] + static std::optional + scan_for_pattern_in_loaded_module(const void* module_base_address, const std::string_view& pattern, + const std::string_view& target_section_name = ".text"); + + [[nodiscard]] + static std::optional + scan_for_pattern_in_file(const std::filesystem::path& path_to_file, const std::string_view& pattern, + const std::string_view& target_section_name = ".text"); + }; +} // namespace omath \ No newline at end of file diff --git a/include/omath/utility/section_scan_result.hpp b/include/omath/utility/section_scan_result.hpp new file mode 100644 index 00000000..00051199 --- /dev/null +++ b/include/omath/utility/section_scan_result.hpp @@ -0,0 +1,16 @@ +// +// Created by Vladislav on 01.01.2026. +// + +#pragma once +#include +#include +namespace omath +{ + struct SectionScanResult final + { + std::uintptr_t virtual_base_addr; + std::uintptr_t raw_base_addr; + std::ptrdiff_t target_offset; + }; +} \ No newline at end of file diff --git a/include/uml/ProjectilePredictor.h b/include/uml/ProjectilePredictor.h deleted file mode 100644 index 1a18b347..00000000 --- a/include/uml/ProjectilePredictor.h +++ /dev/null @@ -1,56 +0,0 @@ -// -// Created by vlad on 11/6/23. -// - -#pragma once -#include -#include -#include - - -namespace uml::prediction -{ - struct Projectile - { - Vector3 m_origin; - float m_velocity{}; - float m_gravityMultiplier = 1.f; - }; - struct Target - { - Vector3 m_origin; - Vector3 m_vecVelocity; - bool m_IsAirborne; - }; - class ProjectilePredictor - { - public: - explicit ProjectilePredictor(float gravityValue, - float maxTimeToTravel = 3.f, - float timeStep = 0.1f); - - - [[nodiscard]] - std::optional PredictPointToAim(const Target& target, - const Projectile& projectile) const; - - private: - - const float m_gravity; - const float m_maxTravelTime; - const float m_timeStepSize; - - [[nodiscard]] - Vector3 LinearPrediction(const Target& target, float time) const; - - [[nodiscard]] - std::optional - MaybeCalculateProjectileLaunchPitchAngle(const Projectile& projectile, - const Vector3& targetPosition) const; - - [[nodiscard]] - std::optional ProjectileTravelTime(const Vector3& end, const Projectile& projectile, - const float angle) const; - }; - -}; diff --git a/include/uml/Vector3.h b/include/uml/Vector3.h deleted file mode 100644 index 06f6507d..00000000 --- a/include/uml/Vector3.h +++ /dev/null @@ -1,65 +0,0 @@ -// -// Created by vlad on 10/28/23. -// - -#pragma once - -namespace uml -{ - class Vector3 { - public: - float x = 0.f; - float y = 0.f; - float z = 0.f; - Vector3(const float x, const float y, const float z) - { - this->x = x; - this->y = y; - this->z = z; - } - Vector3() = default; - - bool operator==(const Vector3& src) const; - bool operator!=(const Vector3& src) const; - - Vector3& operator+=(const Vector3& v); - Vector3& operator-=(const Vector3& v); - Vector3& operator*=(float fl); - Vector3& operator*=(const Vector3& v); - Vector3& operator/=(const Vector3& v); - Vector3& operator+=(float fl); - Vector3& operator/=(float fl); - Vector3& operator-=(float fl); - - [[nodiscard]] float DistTo(const Vector3& vOther) const; - Vector3& Abs(); - [[nodiscard]] float DistToSqr(const Vector3& vOther) const; - [[nodiscard]] float Dot(const Vector3& vOther) const; - [[nodiscard]] float Length() const; - [[nodiscard]] float LengthSqr() const; - [[nodiscard]] float Length2D() const; - - Vector3 operator-() const; - Vector3 operator+(const Vector3& v) const; - Vector3 operator-(const Vector3& v) const; - Vector3 operator*(float fl) const; - Vector3 operator*(const Vector3& v) const; - Vector3 operator/(float fl) const; - Vector3 operator/(const Vector3& v) const; - - template - const type& As() const - { - return *reinterpret_cast(this); - } - template - type& As() - { - return *reinterpret_cast(this); - } - [[nodiscard]] static Vector3 CreateVelocity(const Vector3& angles, float length); - [[nodiscard]] float Sum() const; - [[nodiscard]] float Sum2D() const; - [[nodiscard]] Vector3 ViewAngleTo(const Vector3& other) const; - }; -} diff --git a/include/uml/Vector4.h b/include/uml/Vector4.h deleted file mode 100644 index 6d928f4b..00000000 --- a/include/uml/Vector4.h +++ /dev/null @@ -1,42 +0,0 @@ -// -// Vector4.h -// -#pragma once - -#include - -namespace uml -{ - class Vector4 : public Vector3 - { - public: - float w = 0.f; - - Vector4(float x = 0.f, float y = 0.f, float z = 0.f, float w = 0.f) : Vector3(x, y, z), w(w) {} - Vector4() = default; - - bool operator==(const Vector4& src) const; - bool operator!=(const Vector4& src) const; - - Vector4& operator+=(const Vector4& v); - Vector4& operator-=(const Vector4& v); - Vector4& operator*=(float scalar); - Vector4& operator*=(const Vector4& v); - Vector4& operator/=(float scalar); - Vector4& operator/=(const Vector4& v); - - [[nodiscard]] float Length() const; - [[nodiscard]] float LengthSqr() const; - [[nodiscard]] float Dot(const Vector4& vOther) const; - Vector4& Abs(); - Vector4 operator-() const; - Vector4 operator+(const Vector4& v) const; - Vector4 operator-(const Vector4& v) const; - Vector4 operator*(float scalar) const; - Vector4 operator*(const Vector4& v) const; - Vector4 operator/(float scalar) const; - Vector4 operator/(const Vector4& v) const; - - [[nodiscard]] float Sum() const; - }; -} diff --git a/include/uml/angles.h b/include/uml/angles.h deleted file mode 100644 index 67268967..00000000 --- a/include/uml/angles.h +++ /dev/null @@ -1,11 +0,0 @@ -// -// Created by vlad on 11/6/23. -// - -#pragma once - -namespace uml::angles -{ - [[nodiscard]] float RadToDeg(float rads); - [[nodiscard]] float DegToRad(float degrees); -} \ No newline at end of file diff --git a/include/uml/color.h b/include/uml/color.h deleted file mode 100644 index f811395a..00000000 --- a/include/uml/color.h +++ /dev/null @@ -1,29 +0,0 @@ -// -// Created by vlad on 2/4/24. -// - -#pragma once - -#include "uml/Vector3.h" -#include -#include "uml/Vector4.h" - - -namespace uml::color -{ - [[nodiscard]] - Vector3 Blend(const Vector3& first, const Vector3& second, float ratio); - - class Color : public Vector4 - { - public: - Color(float r, float g, float b, float a); - static Color FromRGBA(uint8_t r, uint8_t g, uint8_t b, uint8_t a); - explicit Color(const Vector4& vec); - [[nodiscard]] Color Blend(const Color& other, float ratio) const; - - [[nodiscard]] static Color Red() {return {1.f, 0.f, 0.f, 1.f};} - [[nodiscard]] static Color Green() {return {0.f, 1.f, 0.f, 1.f};} - [[nodiscard]] static Color Blue() {return {0.f, 0.f, 1.f, 1.f};} - }; -} \ No newline at end of file diff --git a/include/uml/matrix.h b/include/uml/matrix.h deleted file mode 100644 index 25a13050..00000000 --- a/include/uml/matrix.h +++ /dev/null @@ -1,73 +0,0 @@ -#pragma once -#include -#include - -namespace uml -{ - class Vector3; - - class matrix - { - public: - matrix(size_t rows, size_t columns); - - explicit matrix(const std::vector> &rows); - - [[nodiscard]] static matrix to_screen_matrix(float screenWidth, float screenHeight); - - matrix(const matrix &other); - - matrix(size_t rows, size_t columns, const float *pRaw); - - matrix(matrix &&other) noexcept; - - [[nodiscard]] size_t get_rows_count() const noexcept; - - [[nodiscard]] size_t get_columns_count() const noexcept; - - [[nodiscard]] std::pair get_size() const noexcept; - - float &at(size_t iRow, size_t iCol); - - float get_sum(); - void set_from_raw(const float* pRawMatrix); - matrix transpose(); - - void set(float val); - - [[nodiscard]] const float &at(size_t iRow, size_t iCol) const; - - matrix operator*(const matrix &other) const; - - matrix operator*(float f) const; - - matrix operator*(const Vector3 &vec3) const; - - matrix &operator*=(float f); - - matrix &operator/=(float f); - - void clear(); - - [[nodiscard]] matrix strip(size_t row, size_t column) const; - - [[nodiscard]] float minor(size_t i, size_t j) const; - - [[nodiscard]] float alg_complement(size_t i, size_t j) const; - - [[nodiscard]] float det() const; - [[nodiscard]] const float* raw() const; - matrix &operator=(const matrix &other); - - matrix &operator=(matrix &&other) noexcept; - - matrix operator/(float f) const; - - ~matrix(); - - private: - size_t m_rows = 0; - size_t m_columns = 0; - std::unique_ptr m_pData = nullptr; - }; -} \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..a979febf --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,19 @@ +site_name: OMATH Docs +theme: + name: material + palette: + scheme: slate + primary: deep orange + accent: orange + font: + text: Roboto Condensed +markdown_extensions: + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.superfences +extra_css: + - styles/fonts.css + - styles/center.css + - styles/liquid-glass.css +extra_javascript: + - javascripts/liquid-glass.js \ No newline at end of file diff --git a/pixi.toml b/pixi.toml new file mode 100644 index 00000000..4a6418a5 --- /dev/null +++ b/pixi.toml @@ -0,0 +1,69 @@ +[workspace] +name = "omath" +version = "5.7.0" +description = "Cross-platform modern general purpose math library written in C++23 that suitable for cheat/game development." +authors = [ + "orange-cpp " +] + +license = "Zlib" +license-file = "LICENSE" +readme = "README.md" +documentation = "http://libomath.org" +repository = "https://github.com/orange-cpp/omath" + +channels = ["conda-forge"] +platforms = ["win-64", "linux-64", "linux-aarch64", "osx-64", "osx-arm64"] + +[tasks] +format = { cwd = "pixi", cmd = "cmake -P fmt.cmake" } +configure = { cmd = "cmake -G Ninja -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DOMATH_USE_AVX2=OFF -DOMATH_IMGUI_INTEGRATION=ON -DOMATH_BUILD_TESTS=ON -DOMATH_BUILD_BENCHMARK=ON -DOMATH_BUILD_EXAMPLES=ON .", depends-on = ["format"] } +build = { cmd = "cmake --build build --config Debug -j", depends-on = ["configure"] } +examples = { cwd = "pixi", cmd = "cmake -DCMAKE_BUILD_TYPE=Debug -P run.examples.cmake", depends-on = ["build"] } +tests = { cwd = "pixi", cmd = "cmake -DCMAKE_BUILD_TYPE=Debug -P run.unit.tests.cmake", depends-on = ["build"] } +benchmark = { cwd = "pixi", cmd = "cmake -DCMAKE_BUILD_TYPE=Debug -P run.benchmark.cmake", depends-on = ["build"] } + +[dependencies] +benchmark = ">=1.9.5,<2" +ccache = ">=4.12.2,<5" +cmake = ">=4.2.3,<5" +cmake-format = ">=0.6.13,<0.7" +cxx-compiler = ">=1.11.0,<2" +imgui = ">=1.92.3,<2" +gtest = ">=1.17.0,<2" +glew = ">=2.3.0,<3" +glfw = ">=3.4,<4" +ninja = ">=1.13.2,<2" + +[target.linux-64.dependencies] +mesa-libgl-devel-cos7-x86_64 = ">=18.3.4,<19" +xorg-x11-server-xvfb-cos7-x86_64 = ">=1.20.4,<2" + +[target.linux-64.activation.env] +__GLX_VENDOR_LIBRARY_NAME = "mesa" +EGL_PLATFORM = "x11" +GLFW_PLATFORM = "x11" + +[target.linux-64.tasks] +examples = { cwd = "pixi", cmd = "xvfb-run -a -s '-screen 0 1024x768x24 +extension GLX +render' cmake -DCMAKE_BUILD_TYPE=Debug -P run.examples.cmake", depends-on = ["build"] } + +[target.win-64.dependencies] +mesa-libgl-devel-cos7-x86_64 = ">=18.3.4,<19" + +[target.osx-64.dependencies] +mesa-libgl-devel-cos7-x86_64 = ">=18.3.4,<19" + +[target.osx-arm64.dependencies] +mesa-libgl-devel-cos7-aarch64 = ">=18.3.4,<19" + +[target.linux-aarch64.dependencies] +mesa-libgl-devel-cos7-aarch64 = ">=18.3.4,<19" +xorg-x11-server-xvfb-cos7-aarch64 = ">=1.20.4,<2" + +[target.linux-aarch64.activation.env] +__GLX_VENDOR_LIBRARY_NAME = "mesa" +EGL_PLATFORM = "x11" +GLFW_PLATFORM = "x11" + +[target.linux-aarch64.tasks] +examples = { cwd = "pixi", cmd = "xvfb-run -a -s '-screen 0 1024x768x24 +extension GLX +render' cmake -DCMAKE_BUILD_TYPE=Debug -P run.examples.cmake", depends-on = ["build"] } diff --git a/pixi/fmt.cmake b/pixi/fmt.cmake new file mode 100644 index 00000000..978fb3b6 --- /dev/null +++ b/pixi/fmt.cmake @@ -0,0 +1,36 @@ +# cmake/Format.cmake + +# Find cmake-format executable +find_program(CMAKE_FORMAT_EXECUTABLE NAMES cmake-format) + +if(NOT CMAKE_FORMAT_EXECUTABLE) + message(FATAL_ERROR "cmake-format not found. Please install it first.") +endif() + +# Get the project root directory (assuming this script is in cmake/ +# subdirectory) +get_filename_component(PROJECT_ROOT "../${CMAKE_CURRENT_LIST_DIR}" ABSOLUTE) + +# Iterate over all files in the project root +file(GLOB_RECURSE ALL_FILES "${PROJECT_ROOT}/*") + +foreach(FILE ${ALL_FILES}) + # Basic ignores for common directories to avoid formatting external/build + # files Note: We check for substrings in the full path + if("${FILE}" MATCHES "/\\.git/" + OR "${FILE}" MATCHES "/build/" + OR "${FILE}" MATCHES "/cmake-build/" + OR "${FILE}" MATCHES "/\\.pixi/" + OR "${FILE}" MATCHES "/vcpkg_installed/") + continue() + endif() + + get_filename_component(FILENAME "${FILE}" NAME) + + # Check if file ends with .cmake or matches exactly to CMakeLists.txt + if("${FILENAME}" STREQUAL "CMakeLists.txt" OR "${FILENAME}" MATCHES "\\.cmake$") + message(STATUS "Formatting ${FILE}") + execute_process(COMMAND ${CMAKE_FORMAT_EXECUTABLE} --config-files + "${PROJECT_ROOT}/.cmake-format" -i "${FILE}") + endif() +endforeach() diff --git a/pixi/run.benchmark.cmake b/pixi/run.benchmark.cmake new file mode 100644 index 00000000..643f55b0 --- /dev/null +++ b/pixi/run.benchmark.cmake @@ -0,0 +1,63 @@ +# cmake/run.examples.cmake + +# Get the project root directory (assuming this script is in cmake/ subdirectory) +get_filename_component(PROJECT_ROOT "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE) + +# Default to Debug if CMAKE_BUILD_TYPE is not specified +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "Debug") +else() + message(STATUS "CMAKE_BUILD_TYPE is set to: ${CMAKE_BUILD_TYPE}") +endif() + +# Define the directory where executables are located +# Based on CMakeLists.txt: "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}" +set(EXAMPLES_BIN_DIR "${PROJECT_ROOT}/out/${CMAKE_BUILD_TYPE}") + +if(NOT EXISTS "${EXAMPLES_BIN_DIR}") + message(FATAL_ERROR "Examples binary directory not found: ${EXAMPLES_BIN_DIR}. Please build the project first.") +endif() + +message(STATUS "Looking for benchmark executables in: ${EXAMPLES_BIN_DIR}") + +# Find all files starting with "omath_benchmark" +file(GLOB EXAMPLE_FILES "${EXAMPLES_BIN_DIR}/omath_benchmark*") + +foreach(EXAMPLE_PATH ${EXAMPLE_FILES}) + # Skip directories + if(IS_DIRECTORY "${EXAMPLE_PATH}") + continue() + endif() + + get_filename_component(FILENAME "${EXAMPLE_PATH}" NAME) + get_filename_component(EXTENSION "${EXAMPLE_PATH}" EXT) + + # Filter out potential debug symbols or non-executable artifacts + if(EXTENSION STREQUAL ".pdb" OR EXTENSION STREQUAL ".ilk" OR EXTENSION STREQUAL ".obj") + continue() + endif() + + # On Windows, we expect .exe + if(MSVC AND NOT EXTENSION STREQUAL ".exe") + continue() + endif() + + # On Linux/macOS, check permissions or just try to run it. + + message(STATUS "-------------------------------------------------") + message(STATUS "Running benchmark: ${FILENAME}") + message(STATUS "-------------------------------------------------") + + execute_process( + COMMAND "${EXAMPLE_PATH}" + WORKING_DIRECTORY "${PROJECT_ROOT}" + RESULT_VARIABLE EXIT_CODE + ) + + if(NOT EXIT_CODE EQUAL 0) + message(WARNING "Benchmark ${FILENAME} exited with error code: ${EXIT_CODE}") + else() + message(STATUS "Benchmark ${FILENAME} completed successfully.") + endif() + +endforeach() diff --git a/pixi/run.examples.cmake b/pixi/run.examples.cmake new file mode 100644 index 00000000..39d63088 --- /dev/null +++ b/pixi/run.examples.cmake @@ -0,0 +1,63 @@ +# cmake/run.examples.cmake + +# Get the project root directory (assuming this script is in cmake/ subdirectory) +get_filename_component(PROJECT_ROOT "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE) + +# Default to Debug if CMAKE_BUILD_TYPE is not specified +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "Debug") +else() + message(STATUS "CMAKE_BUILD_TYPE is set to: ${CMAKE_BUILD_TYPE}") +endif() + +# Define the directory where executables are located +# Based on CMakeLists.txt: "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}" +set(EXAMPLES_BIN_DIR "${PROJECT_ROOT}/out/${CMAKE_BUILD_TYPE}") + +if(NOT EXISTS "${EXAMPLES_BIN_DIR}") + message(FATAL_ERROR "Examples binary directory not found: ${EXAMPLES_BIN_DIR}. Please build the project first.") +endif() + +message(STATUS "Looking for example executables in: ${EXAMPLES_BIN_DIR}") + +# Find all files starting with "example_" +file(GLOB EXAMPLE_FILES "${EXAMPLES_BIN_DIR}/example_*") + +foreach(EXAMPLE_PATH ${EXAMPLE_FILES}) + # Skip directories + if(IS_DIRECTORY "${EXAMPLE_PATH}") + continue() + endif() + + get_filename_component(FILENAME "${EXAMPLE_PATH}" NAME) + get_filename_component(EXTENSION "${EXAMPLE_PATH}" EXT) + + # Filter out potential debug symbols or non-executable artifacts + if(EXTENSION STREQUAL ".pdb" OR EXTENSION STREQUAL ".ilk" OR EXTENSION STREQUAL ".obj") + continue() + endif() + + # On Windows, we expect .exe + if(MSVC AND NOT EXTENSION STREQUAL ".exe") + continue() + endif() + + # On Linux/macOS, check permissions or just try to run it. + + message(STATUS "-------------------------------------------------") + message(STATUS "Running example: ${FILENAME}") + message(STATUS "-------------------------------------------------") + + execute_process( + COMMAND "${EXAMPLE_PATH}" + WORKING_DIRECTORY "${PROJECT_ROOT}" + RESULT_VARIABLE EXIT_CODE + ) + + if(NOT EXIT_CODE EQUAL 0) + message(WARNING "Example ${FILENAME} exited with error code: ${EXIT_CODE}") + else() + message(STATUS "Example ${FILENAME} completed successfully.") + endif() + +endforeach() diff --git a/pixi/run.unit.tests.cmake b/pixi/run.unit.tests.cmake new file mode 100644 index 00000000..36e995b3 --- /dev/null +++ b/pixi/run.unit.tests.cmake @@ -0,0 +1,63 @@ +# cmake/run.examples.cmake + +# Get the project root directory (assuming this script is in cmake/ subdirectory) +get_filename_component(PROJECT_ROOT "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE) + +# Default to Debug if CMAKE_BUILD_TYPE is not specified +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "Debug") +else() + message(STATUS "CMAKE_BUILD_TYPE is set to: ${CMAKE_BUILD_TYPE}") +endif() + +# Define the directory where executables are located +# Based on CMakeLists.txt: "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}" +set(EXAMPLES_BIN_DIR "${PROJECT_ROOT}/out/${CMAKE_BUILD_TYPE}") + +if(NOT EXISTS "${EXAMPLES_BIN_DIR}") + message(FATAL_ERROR "Examples binary directory not found: ${EXAMPLES_BIN_DIR}. Please build the project first.") +endif() + +message(STATUS "Looking for unit test executables in: ${EXAMPLES_BIN_DIR}") + +# Find all files starting with "unit_tests" +file(GLOB EXAMPLE_FILES "${EXAMPLES_BIN_DIR}/unit_tests*") + +foreach(EXAMPLE_PATH ${EXAMPLE_FILES}) + # Skip directories + if(IS_DIRECTORY "${EXAMPLE_PATH}") + continue() + endif() + + get_filename_component(FILENAME "${EXAMPLE_PATH}" NAME) + get_filename_component(EXTENSION "${EXAMPLE_PATH}" EXT) + + # Filter out potential debug symbols or non-executable artifacts + if(EXTENSION STREQUAL ".pdb" OR EXTENSION STREQUAL ".ilk" OR EXTENSION STREQUAL ".obj") + continue() + endif() + + # On Windows, we expect .exe + if(MSVC AND NOT EXTENSION STREQUAL ".exe") + continue() + endif() + + # On Linux/macOS, check permissions or just try to run it. + + message(STATUS "-------------------------------------------------") + message(STATUS "Running unit_tests: ${FILENAME}") + message(STATUS "-------------------------------------------------") + + execute_process( + COMMAND "${EXAMPLE_PATH}" + WORKING_DIRECTORY "${PROJECT_ROOT}" + RESULT_VARIABLE EXIT_CODE + ) + + if(NOT EXIT_CODE EQUAL 0) + message(WARNING "Example ${FILENAME} exited with error code: ${EXIT_CODE}") + else() + message(STATUS "Example ${FILENAME} completed successfully.") + endif() + +endforeach() diff --git a/readme.md b/readme.md deleted file mode 100644 index 3e6a8658..00000000 --- a/readme.md +++ /dev/null @@ -1,58 +0,0 @@ -# Universal Math Library (UML) - -## Overview -The Universal Math Library (UML) is a comprehensive, open-source library aimed at providing efficient, reliable, and versatile mathematical functions and algorithms. Developed primarily in C++, this library is designed to cater to a wide range of mathematical operations essential in scientific computing, engineering, and academic research. - -## Features -- **Efficiency**: Optimized for performance, ensuring quick computations. -- **Versatility**: Includes a wide array of mathematical functions and algorithms. -- **Ease of Use**: Simplified interface for convenient integration into various projects. - -## Getting Started -### Prerequisites -- C++ Compiler -- CMake (for building the project) - -### Installation -1. Clone the repository: - ``` - git clone https://github.com/VladislavAlpatov/uml.git - ``` -2. Navigate to the project directory: - ``` - cd uml - ``` -3. Build the project using CMake: - ``` - cmake --preset x64-release -S . - cmake --build cmake-build/build/x64-release --target server -j 6 - ``` - -## Usage -Simple world to screen function -```c++ -std::optional WorldToScreen(uml::Vector3 worldPosition, float width, float height) - { - auto projected = (GetViewProjectionMatrix() * worldPosition).transpose(); - - projected /= projected.at(0, 3); - - const auto out = projected * uml::matrix::to_screen_matrix(width, - height); - - if (out.at(0,2) <= 0.f) - return std::nullopt; - auto final = uml::Vector3(out.at(0,0), - out.at(0, 1), - out.at(0,2)); - return {final}; - } -``` -## Contributing -Contributions to UML are welcome! Please read `CONTRIBUTING.md` for details on our code of conduct and the process for submitting pull requests. - -## License -This project is licensed under the GPL V3 - see the `LICENSE` file for details. - -## Acknowledgments -- Vladislav Alpatov | [Telegram](https://t.me/nullifiedvlad) diff --git a/scripts/coverage-llvm.sh b/scripts/coverage-llvm.sh new file mode 100755 index 00000000..8f45249d --- /dev/null +++ b/scripts/coverage-llvm.sh @@ -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}" diff --git a/scripts/coverage.bat.in b/scripts/coverage.bat.in new file mode 100644 index 00000000..1e619a00 --- /dev/null +++ b/scripts/coverage.bat.in @@ -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%" %* diff --git a/scripts/coverage.ps1.in b/scripts/coverage.ps1.in new file mode 100644 index 00000000..6f3abc32 --- /dev/null +++ b/scripts/coverage.ps1.in @@ -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 diff --git a/scripts/valgrind.sh b/scripts/valgrind.sh new file mode 100755 index 00000000..7b04ba84 --- /dev/null +++ b/scripts/valgrind.sh @@ -0,0 +1,77 @@ +#!/bin/bash + +# ============================================================================= +# Local Reproduction of "Valgrind Analysis" GitHub Action +# ============================================================================= + +# Stop on error, undefined variables, or pipe failures +set -euo pipefail + +# --- 1. Environment Setup --- + +# Determine VCPKG_ROOT +# If VCPKG_ROOT is not set in your shell, we check if a local folder exists. +if [[ -z "${VCPKG_ROOT:-}" ]]; then + if [[ -d "./vcpkg" ]]; then + export VCPKG_ROOT="$(pwd)/vcpkg" + echo "Found local vcpkg at: $VCPKG_ROOT" + + # Bootstrap vcpkg if the executable doesn't exist + if [[ ! -f "$VCPKG_ROOT/vcpkg" ]]; then + echo "Bootstrapping vcpkg..." + "$VCPKG_ROOT/bootstrap-vcpkg.sh" + fi + else + echo "Error: VCPKG_ROOT is not set and ./vcpkg directory not found." + echo "Please install vcpkg or set the VCPKG_ROOT environment variable." + exit 1 + fi +else + echo "Using existing VCPKG_ROOT: $VCPKG_ROOT" +fi + +# Set the build directory matching the YAML's preset expectation +# Assuming the preset writes to: cmake-build/build/linux-release-vcpkg +BUILD_DIR="cmake-build/build/linux-release-vcpkg" + +# Check if Valgrind is installed +if ! command -v valgrind &> /dev/null; then + echo "Error: valgrind is not installed. Please install it (e.g., sudo apt install valgrind)." + exit 1 +fi + +echo "----------------------------------------------------" +echo "Starting Configuration (Debug Build with Valgrind)..." +echo "----------------------------------------------------" + +# --- 2. Configure (CMake) --- +# We force CMAKE_BUILD_TYPE=Debug even though the preset says 'release' +# to ensure Valgrind has access to debug symbols (line numbers). + +cmake --preset linux-release-vcpkg \ + -DCMAKE_BUILD_TYPE=Debug \ + -DOMATH_BUILD_EXAMPLES=OFF \ + -DOMATH_BUILD_TESTS=ON \ + -DOMATH_BUILD_BENCHMARK=ON \ + -DOMATH_ENABLE_VALGRIND=ON \ + -DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests;benchmark" + +echo "----------------------------------------------------" +echo "Building Targets..." +echo "----------------------------------------------------" + +# --- 3. Build --- +# Using the specific build directory defined by the preset structure +cmake --build "$BUILD_DIR" + +echo "----------------------------------------------------" +echo "Running Valgrind Analysis..." +echo "----------------------------------------------------" + +# --- 4. Run Valgrind --- +# Runs the specific custom target defined in your CMakeLists.txt +cmake --build "$BUILD_DIR" --target valgrind_all + +echo "----------------------------------------------------" +echo "Valgrind Analysis Complete." +echo "----------------------------------------------------" diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt deleted file mode 100644 index 80bd6363..00000000 --- a/source/CMakeLists.txt +++ /dev/null @@ -1,7 +0,0 @@ -target_sources(uml PRIVATE - Vector3.cpp - matrix.cpp - angles.cpp - ProjectilePredictor.cpp - color.cpp - Vector4.cpp) \ No newline at end of file diff --git a/source/ProjectilePredictor.cpp b/source/ProjectilePredictor.cpp deleted file mode 100644 index c2fe2b61..00000000 --- a/source/ProjectilePredictor.cpp +++ /dev/null @@ -1,106 +0,0 @@ -// -// Created by vlad on 11/6/23. -// - -#include "uml/ProjectilePredictor.h" -#include "uml/Vector3.h" -#include "uml/angles.h" - -#include -#include - -namespace uml::prediction -{ - ProjectilePredictor::ProjectilePredictor(float gravityValue, - float maxTimeToTravel, - float timeStep) - : m_gravity(gravityValue), m_maxTravelTime(maxTimeToTravel), m_timeStepSize(timeStep) - { - - } - - std::optional ProjectilePredictor::PredictPointToAim( - const Target &target, const Projectile &projectile) const - { - for (float time = 0.0f; time <= m_maxTravelTime; time += m_timeStepSize) - { - auto predictedTargetPosition = LinearPrediction(target, time); - - const auto projectilePitch = - MaybeCalculateProjectileLaunchPitchAngle(projectile, predictedTargetPosition); - - if (!projectilePitch.has_value()) [[unlikely]] - return std::nullopt; - - const auto timeToHit = ProjectileTravelTime(predictedTargetPosition, - projectile, - projectilePitch.value()); - if (!timeToHit.has_value() || timeToHit.value() > time) - continue; - - const auto delta2d = (predictedTargetPosition - projectile.m_origin).Length2D(); - const auto height = delta2d * std::tan(angles::DegToRad(projectilePitch.value())); - - predictedTargetPosition.z = projectile.m_origin.z + height; - - return predictedTargetPosition; - } - - - return std::nullopt; - } - - Vector3 ProjectilePredictor::LinearPrediction(const Target &target, float time) const - { - auto predicted = target.m_origin + target.m_vecVelocity * time; - - if (target.m_IsAirborne) - predicted.z -= m_gravity * std::pow(time, 2.f) * 0.5f; - - return predicted; - } - - std::optional - ProjectilePredictor::MaybeCalculateProjectileLaunchPitchAngle(const Projectile &projectile, - const Vector3 &targetPosition) - const - { - const auto bulletGravity = m_gravity * projectile.m_gravityMultiplier; - const auto delta = targetPosition - projectile.m_origin;; - - const auto distance2d = delta.Length2D(); - - - float root = std::pow(projectile.m_velocity, 4.f) - bulletGravity * (bulletGravity * - std::pow(distance2d, 2.f) + 2.0f * delta.z * std::pow(projectile.m_velocity, 2.f)); - - if (root < 0.0f) [[unlikely]] - return std::nullopt; - - root = std::sqrt(root); - const float angle = std::atan((std::pow(projectile.m_velocity, 2.f) - root) / (bulletGravity * distance2d)); - - return angles::RadToDeg(angle); - } - - std::optional ProjectilePredictor::ProjectileTravelTime(const Vector3 &end, - const Projectile &projectile, - const float angle) const - { - auto launchAngles = projectile.m_origin.ViewAngleTo(end); - launchAngles.x = angle; - - const auto velocity = Vector3::CreateVelocity(launchAngles, projectile.m_velocity); - - for (float time = 0.0f; time <= m_maxTravelTime; time += m_timeStepSize) - { - auto currentPos = projectile.m_origin + velocity * time; - currentPos.z -= m_gravity * projectile.m_gravityMultiplier * std::pow(time, 2.f) * 0.5f; - - if (currentPos.DistTo(end) <= 25.f) - return time; - } - - return std::nullopt; - } -} diff --git a/source/Vector3.cpp b/source/Vector3.cpp deleted file mode 100644 index 47698891..00000000 --- a/source/Vector3.cpp +++ /dev/null @@ -1,213 +0,0 @@ -// -// Created by vlad on 10/28/23. -// - -#include -#include -#include - -namespace uml -{ - bool Vector3::operator==(const Vector3 &src) const - { - return (src.x == x) and (src.y == y) and (src.z == z); - } - - bool Vector3::operator!=(const Vector3 &src) const - { - return (src.x != x) or (src.y != y) or (src.z != z); - } - - Vector3 &Vector3::operator+=(const Vector3 &v) - { - x += v.x; - y += v.y; - z += v.z; - - return *this; - } - - Vector3 &Vector3::operator-=(const Vector3 &v) - { - x -= v.x; - y -= v.y; - z -= v.z; - - return *this; - } - - Vector3 &Vector3::operator*=(const float fl) - { - x *= fl; - y *= fl; - z *= fl; - - return *this; - } - - Vector3 &Vector3::operator*=(const Vector3 &v) - { - x *= v.x; - y *= v.y; - z *= v.z; - - return *this; - } - - Vector3 &Vector3::operator/=(const Vector3 &v) - { - x /= v.x; - y /= v.y; - z /= v.z; - - return *this; - } - - Vector3 &Vector3::operator+=(const float fl) - { - x += fl; - y += fl; - z += fl; - - return *this; - } - - Vector3 &Vector3::operator/=(const float fl) - { - x /= fl; - y /= fl; - z /= fl; - - return *this; - } - - Vector3 &Vector3::operator-=(const float fl) - { - x -= fl; - y -= fl; - z -= fl; - - return *this; - } - - float Vector3::DistTo(const Vector3 &vOther) const - { - Vector3 delta; - - delta.x = x - vOther.x; - delta.y = y - vOther.y; - delta.z = z - vOther.z; - - return delta.Length(); - } - - Vector3 &Vector3::Abs() - { - x = std::abs(x); - y = std::abs(y); - z = std::abs(z); - - return *this; - } - - float Vector3::DistToSqr(const Vector3 &vOther) const - { - Vector3 delta; - - delta.x = x - vOther.x; - delta.y = y - vOther.y; - delta.z = z - vOther.z; - - return delta.LengthSqr(); - } - - float Vector3::Dot(const Vector3 &vOther) const - { - return (x * vOther.x + y * vOther.y + z * vOther.z); - } - - float Vector3::Length() const - { - return std::sqrt(x * x + y * y + z * z); - } - - float Vector3::LengthSqr() const - { - return (x * x + y * y + z * z); - } - - float Vector3::Length2D() const - { - return std::sqrt(x * x + y * y); - - } - - Vector3 Vector3::operator-() const - { - return {-x, -y, -z}; - } - - Vector3 Vector3::operator+(const Vector3 &v) const - { - return {x + v.x, y + v.y, z + v.z}; - } - - Vector3 Vector3::operator-(const Vector3 &v) const - { - return {x - v.x, y - v.y, z - v.z}; - } - - Vector3 Vector3::operator*(float fl) const - { - return {x * fl, y * fl, z * fl}; - } - - Vector3 Vector3::operator*(const Vector3 &v) const - { - return {x * v.x, y * v.y, z * v.z}; - } - - Vector3 Vector3::operator/(const float fl) const - { - return {x / fl, y / fl, z / fl}; - } - - Vector3 Vector3::operator/(const Vector3 &v) const - { - return {x / v.x, y / v.y, z / v.z}; - } - - Vector3 Vector3::CreateVelocity(const Vector3 &angles, const float length) - { - return - { - std::cos(angles::DegToRad(angles.x)) * std::cos(angles::DegToRad(angles.y)) * length, - std::cos(angles::DegToRad(angles.x)) * std::sin(angles::DegToRad(angles.y)) * length, - std::sin(angles::DegToRad(angles.x)) * length, - }; - } - - float Vector3::Sum() const - { - return x + y + z; - } - - float Vector3::Sum2D() const - { - return x + y; - } - - Vector3 Vector3::ViewAngleTo(const Vector3 &other) const - { - const float distance = DistTo(other); - const auto delta = other - *this; - - // Make x negative since -89 is top and 89 is bottom - return - { - -angles::RadToDeg(asinf(delta.z / distance)), - angles::RadToDeg(atan2f(delta.y, delta.x)), - 0.f - }; - } -} \ No newline at end of file diff --git a/source/Vector4.cpp b/source/Vector4.cpp deleted file mode 100644 index fadd1778..00000000 --- a/source/Vector4.cpp +++ /dev/null @@ -1,123 +0,0 @@ -// -// Vector4.cpp -// - -#include "uml/Vector4.h" -#include - -namespace uml -{ - bool Vector4::operator==(const Vector4& src) const - { - return Vector3::operator==(src) && w == src.w; - } - - bool Vector4::operator!=(const Vector4& src) const - { - return !(*this == src); - } - - Vector4& Vector4::operator+=(const Vector4& v) - { - Vector3::operator+=(v); - w += v.w; - return *this; - } - - Vector4& Vector4::operator-=(const Vector4& v) - { - Vector3::operator-=(v); - w -= v.w; - return *this; - } - - Vector4& Vector4::operator*=(float scalar) - { - Vector3::operator*=(scalar); - w *= scalar; - return *this; - } - - Vector4& Vector4::operator*=(const Vector4& v) - { - Vector3::operator*=(v); - w *= v.w; - return *this; - } - - Vector4& Vector4::operator/=(float scalar) - { - Vector3::operator/=(scalar); - w /= scalar; - return *this; - } - - Vector4& Vector4::operator/=(const Vector4& v) - { - Vector3::operator/=(v); - w /= v.w; - return *this; - } - - float Vector4::Length() const - { - return std::sqrt(LengthSqr()); - } - - float Vector4::LengthSqr() const - { - return Vector3::LengthSqr() + w * w; - } - - float Vector4::Dot(const Vector4& vOther) const - { - return Vector3::Dot(vOther) + w * vOther.w; - } - - Vector4& Vector4::Abs() - { - Vector3::Abs(); - w = std::abs(w); - return *this; - } - - Vector4 Vector4::operator-() const - { - return Vector4(-x, -y, -z, -w); - } - - Vector4 Vector4::operator+(const Vector4& v) const - { - return Vector4(x + v.x, y + v.y, z + v.z, w + v.w); - } - - Vector4 Vector4::operator-(const Vector4& v) const - { - return Vector4(x - v.x, y - v.y, z - v.z, w - v.w); - } - - Vector4 Vector4::operator*(float scalar) const - { - return Vector4(x * scalar, y * scalar, z * scalar, w * scalar); - } - - Vector4 Vector4::operator*(const Vector4& v) const - { - return Vector4(x * v.x, y * v.y, z * v.z, w * v.w); - } - - Vector4 Vector4::operator/(float scalar) const - { - return Vector4(x / scalar, y / scalar, z / scalar, w / scalar); - } - - Vector4 Vector4::operator/(const Vector4& v) const - { - return Vector4(x / v.x, y / v.y, z / v.z, w / v.w); - } - - float Vector4::Sum() const - { - return x + y + z + w; - } -} diff --git a/source/angles.cpp b/source/angles.cpp deleted file mode 100644 index 01950901..00000000 --- a/source/angles.cpp +++ /dev/null @@ -1,20 +0,0 @@ -// -// Created by vlad on 11/6/23. -// - -#include "uml/angles.h" -#include - - -namespace uml::angles -{ - float RadToDeg(float rads) - { - return rads * 180.f / std::numbers::pi_v; - } - - float DegToRad(float degrees) - { - return degrees * std::numbers::pi_v / 180.f; - } -} diff --git a/source/color.cpp b/source/color.cpp deleted file mode 100644 index 876d0566..00000000 --- a/source/color.cpp +++ /dev/null @@ -1,36 +0,0 @@ -// -// Created by vlad on 2/4/24. -// - -#include "uml/color.h" -#include - -namespace uml::color -{ - Vector3 Blend(const Vector3 &first, const Vector3 &second, float ratio) - { - return first * (1.f - std::clamp(ratio, 0.f, 1.f)) + second * ratio; - } - - Color Color::Blend(const Color &other, float ratio) const - { - return Color( (*this * (1.f - ratio)) + (other * ratio) ); - } - - Color::Color(float r, float g, float b, float a) : Vector4(r,g,b,a) - { - - } - - Color::Color(const Vector4 &vec) : Vector4(vec) - { - - } - - Color Color::FromRGBA(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 255) - { - - return Color{Vector4(r, g, b, a) / 255.f}; - - } -} \ No newline at end of file diff --git a/source/engines/frostbite_engine/formulas.cpp b/source/engines/frostbite_engine/formulas.cpp new file mode 100644 index 00000000..021cdc7c --- /dev/null +++ b/source/engines/frostbite_engine/formulas.cpp @@ -0,0 +1,42 @@ +// +// Created by Vlad on 3/22/2025. +// +#include "omath/engines/frostbite_engine/formulas.hpp" + +namespace omath::frostbite_engine +{ + Vector3 forward_vector(const ViewAngles& angles) noexcept + { + const auto vec = rotation_matrix(angles) * mat_column_from_vector(k_abs_forward); + + return {vec.at(0, 0), vec.at(1, 0), vec.at(2, 0)}; + } + Vector3 right_vector(const ViewAngles& angles) noexcept + { + const auto vec = rotation_matrix(angles) * mat_column_from_vector(k_abs_right); + + return {vec.at(0, 0), vec.at(1, 0), vec.at(2, 0)}; + } + Vector3 up_vector(const ViewAngles& angles) noexcept + { + const auto vec = rotation_matrix(angles) * mat_column_from_vector(k_abs_up); + + return {vec.at(0, 0), vec.at(1, 0), vec.at(2, 0)}; + } + Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3& cam_origin) noexcept + { + return mat_camera_view(forward_vector(angles), right_vector(angles), + up_vector(angles), cam_origin); + } + Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept + { + return mat_rotation_axis_z(angles.roll) + * mat_rotation_axis_y(angles.yaw) + * mat_rotation_axis_x(angles.pitch); + } + Mat4X4 calc_perspective_projection_matrix(const float field_of_view, const float aspect_ratio, const float near, + const float far) noexcept + { + return mat_perspective_left_handed(field_of_view, aspect_ratio, near, far); + } +} // namespace omath::unity_engine diff --git a/source/engines/frostbite_engine/traits/camera_trait.cpp b/source/engines/frostbite_engine/traits/camera_trait.cpp new file mode 100644 index 00000000..8b44d4f4 --- /dev/null +++ b/source/engines/frostbite_engine/traits/camera_trait.cpp @@ -0,0 +1,26 @@ +// +// Created by Vlad on 8/11/2025. +// +#include "omath/engines/frostbite_engine/traits/camera_trait.hpp" + +namespace omath::frostbite_engine +{ + + ViewAngles CameraTrait::calc_look_at_angle(const Vector3& cam_origin, const Vector3& look_at) noexcept + { + const auto direction = (look_at - cam_origin).normalized(); + + return {PitchAngle::from_radians(-std::asin(direction.y)), + YawAngle::from_radians(std::atan2(direction.x, direction.z)), RollAngle::from_radians(0.f)}; + } + Mat4X4 CameraTrait::calc_view_matrix(const ViewAngles& angles, const Vector3& cam_origin) noexcept + { + return frostbite_engine::calc_view_matrix(angles, cam_origin); + } + Mat4X4 CameraTrait::calc_projection_matrix(const projection::FieldOfView& fov, + const projection::ViewPort& view_port, const float near, + const float far) noexcept + { + return calc_perspective_projection_matrix(fov.as_degrees(), view_port.aspect_ratio(), near, far); + } +} // namespace omath::unity_engine \ No newline at end of file diff --git a/source/engines/iw_engine/formulas.cpp b/source/engines/iw_engine/formulas.cpp new file mode 100644 index 00000000..c346d50c --- /dev/null +++ b/source/engines/iw_engine/formulas.cpp @@ -0,0 +1,53 @@ +// +// Created by Vlad on 3/19/2025. +// +#include "omath/engines/iw_engine/formulas.hpp" + +namespace omath::iw_engine +{ + + Vector3 forward_vector(const ViewAngles& angles) noexcept + { + const auto vec = rotation_matrix(angles) * mat_column_from_vector(k_abs_forward); + + return {vec.at(0, 0), vec.at(1, 0), vec.at(2, 0)}; + } + + Vector3 right_vector(const ViewAngles& angles) noexcept + { + const auto vec = rotation_matrix(angles) * mat_column_from_vector(k_abs_right); + + return {vec.at(0, 0), vec.at(1, 0), vec.at(2, 0)}; + } + Vector3 up_vector(const ViewAngles& angles) noexcept + { + const auto vec = rotation_matrix(angles) * mat_column_from_vector(k_abs_up); + + return {vec.at(0, 0), vec.at(1, 0), vec.at(2, 0)}; + } + Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept + { + return mat_rotation_axis_z(angles.yaw) * mat_rotation_axis_y(angles.pitch) * mat_rotation_axis_x(angles.roll); + } + + Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3& cam_origin) noexcept + { + return mat_camera_view(forward_vector(angles), right_vector(angles), up_vector(angles), cam_origin); + } + + Mat4X4 calc_perspective_projection_matrix(const float field_of_view, const float aspect_ratio, const float near, + const float far) noexcept + { + // NOTE: Need magic number to fix fov calculation, since IW engine inherit Quake proj matrix calculation + constexpr auto k_multiply_factor = 0.75f; + + const float fov_half_tan = std::tan(angles::degrees_to_radians(field_of_view) / 2.f) * k_multiply_factor; + + 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, 0}, + }; + }; +} // namespace omath::iw_engine diff --git a/source/engines/iw_engine/traits/camera_trait.cpp b/source/engines/iw_engine/traits/camera_trait.cpp new file mode 100644 index 00000000..427a3c58 --- /dev/null +++ b/source/engines/iw_engine/traits/camera_trait.cpp @@ -0,0 +1,26 @@ +// +// Created by Vlad on 8/11/2025. +// +#include "omath/engines/iw_engine/traits/camera_trait.hpp" +#include "omath/engines/iw_engine/formulas.hpp" +namespace omath::iw_engine +{ + + ViewAngles CameraTrait::calc_look_at_angle(const Vector3& cam_origin, const Vector3& look_at) noexcept + { + const auto direction = (look_at - cam_origin).normalized(); + + return {PitchAngle::from_radians(-std::asin(direction.z)), + YawAngle::from_radians(std::atan2(direction.y, direction.x)), RollAngle::from_radians(0.f)}; + } + Mat4X4 CameraTrait::calc_view_matrix(const ViewAngles& angles, const Vector3& cam_origin) noexcept + { + return iw_engine::calc_view_matrix(angles, cam_origin); + } + Mat4X4 CameraTrait::calc_projection_matrix(const projection::FieldOfView& fov, + const projection::ViewPort& view_port, const float near, + const float far) noexcept + { + return calc_perspective_projection_matrix(fov.as_degrees(), view_port.aspect_ratio(), near, far); + } +} // namespace omath::iw_engine \ No newline at end of file diff --git a/source/engines/opengl_engine/formulas.cpp b/source/engines/opengl_engine/formulas.cpp new file mode 100644 index 00000000..da8ab494 --- /dev/null +++ b/source/engines/opengl_engine/formulas.cpp @@ -0,0 +1,51 @@ +// +// Created by Vlad on 3/19/2025. +// +#include "omath/engines/opengl_engine/formulas.hpp" + +namespace omath::opengl_engine +{ + + Vector3 forward_vector(const ViewAngles& angles) noexcept + { + const auto vec + = rotation_matrix(angles) * mat_column_from_vector(k_abs_forward); + + return {vec.at(0, 0), vec.at(1, 0), vec.at(2, 0)}; + } + Vector3 right_vector(const ViewAngles& angles) noexcept + { + const auto vec + = rotation_matrix(angles) * mat_column_from_vector(k_abs_right); + + return {vec.at(0, 0), vec.at(1, 0), vec.at(2, 0)}; + } + Vector3 up_vector(const ViewAngles& angles) noexcept + { + const auto vec = rotation_matrix(angles) * mat_column_from_vector(k_abs_up); + + return {vec.at(0, 0), vec.at(1, 0), vec.at(2, 0)}; + } + Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3& cam_origin) noexcept + { + return mat_look_at_right_handed(cam_origin, cam_origin+forward_vector(angles), up_vector(angles)); + } + Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept + { + return mat_rotation_axis_z(angles.roll) + * mat_rotation_axis_y(angles.yaw) + * mat_rotation_axis_x(angles.pitch); + } + 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, 0}, + }; + } +} // namespace omath::opengl_engine diff --git a/source/engines/opengl_engine/traits/camera_trait.cpp b/source/engines/opengl_engine/traits/camera_trait.cpp new file mode 100644 index 00000000..d034bef1 --- /dev/null +++ b/source/engines/opengl_engine/traits/camera_trait.cpp @@ -0,0 +1,27 @@ +// +// Created by Vlad on 8/11/2025. +// +#include "omath/engines/opengl_engine/traits/camera_trait.hpp" +#include "omath/engines/opengl_engine/formulas.hpp" + +namespace omath::opengl_engine +{ + + ViewAngles CameraTrait::calc_look_at_angle(const Vector3& cam_origin, const Vector3& look_at) noexcept + { + const auto direction = (look_at - cam_origin).normalized(); + + return {PitchAngle::from_radians(std::asin(direction.y)), + YawAngle::from_radians(-std::atan2(direction.x, -direction.z)), RollAngle::from_radians(0.f)}; + } + Mat4X4 CameraTrait::calc_view_matrix(const ViewAngles& angles, const Vector3& cam_origin) noexcept + { + return opengl_engine::calc_view_matrix(angles, cam_origin); + } + Mat4X4 CameraTrait::calc_projection_matrix(const projection::FieldOfView& fov, + const projection::ViewPort& view_port, const float near, + const float far) noexcept + { + return calc_perspective_projection_matrix(fov.as_degrees(), view_port.aspect_ratio(), near, far); + } +} // namespace omath::opengl_engine \ No newline at end of file diff --git a/source/engines/source_engine/formulas.cpp b/source/engines/source_engine/formulas.cpp new file mode 100644 index 00000000..326507e8 --- /dev/null +++ b/source/engines/source_engine/formulas.cpp @@ -0,0 +1,53 @@ +// +// Created by Vlad on 3/19/2025. +// +#include + +namespace omath::source_engine +{ + Vector3 forward_vector(const ViewAngles& angles) noexcept + { + const auto vec = rotation_matrix(angles) * mat_column_from_vector(k_abs_forward); + + return {vec.at(0, 0), vec.at(1, 0), vec.at(2, 0)}; + } + + Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept + { + return mat_rotation_axis_z(angles.yaw) * mat_rotation_axis_y(angles.pitch) * mat_rotation_axis_x(angles.roll); + } + + Vector3 right_vector(const ViewAngles& angles) noexcept + { + const auto vec = rotation_matrix(angles) * mat_column_from_vector(k_abs_right); + + return {vec.at(0, 0), vec.at(1, 0), vec.at(2, 0)}; + } + Vector3 up_vector(const ViewAngles& angles) noexcept + { + const auto vec = rotation_matrix(angles) * mat_column_from_vector(k_abs_up); + + return {vec.at(0, 0), vec.at(1, 0), vec.at(2, 0)}; + } + + Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3& cam_origin) noexcept + { + return mat_camera_view(forward_vector(angles), right_vector(angles), up_vector(angles), cam_origin); + } + + Mat4X4 calc_perspective_projection_matrix(const float field_of_view, const float aspect_ratio, const float near, + const float far) noexcept + { + // NOTE: Need magic number to fix fov calculation, since source inherit Quake proj matrix calculation + constexpr auto k_multiply_factor = 0.75f; + + const float fov_half_tan = std::tan(angles::degrees_to_radians(field_of_view) / 2.f) * k_multiply_factor; + + 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, 0}, + }; + } +} // namespace omath::source_engine diff --git a/source/engines/source_engine/traits/camera_trait.cpp b/source/engines/source_engine/traits/camera_trait.cpp new file mode 100644 index 00000000..071326e2 --- /dev/null +++ b/source/engines/source_engine/traits/camera_trait.cpp @@ -0,0 +1,27 @@ +// +// Created by Vlad on 8/11/2025. +// +#include "omath/engines/source_engine/traits/camera_trait.hpp" +#include "omath/engines/source_engine/formulas.hpp" +namespace omath::source_engine +{ + + ViewAngles CameraTrait::calc_look_at_angle(const Vector3& cam_origin, const Vector3& look_at) noexcept + { + const auto direction = (look_at - cam_origin).normalized(); + + + return {PitchAngle::from_radians(-std::asin(direction.z)), + YawAngle::from_radians(std::atan2(direction.y, direction.x)), RollAngle::from_radians(0.f)}; + } + Mat4X4 CameraTrait::calc_view_matrix(const ViewAngles& angles, const Vector3& cam_origin) noexcept + { + return source_engine::calc_view_matrix(angles, cam_origin); + } + Mat4X4 CameraTrait::calc_projection_matrix(const projection::FieldOfView& fov, + const projection::ViewPort& view_port, const float near, + const float far) noexcept + { + return calc_perspective_projection_matrix(fov.as_degrees(), view_port.aspect_ratio(), near, far); + } +} // namespace omath::source_engine \ No newline at end of file diff --git a/source/engines/unity_engine/formulas.cpp b/source/engines/unity_engine/formulas.cpp new file mode 100644 index 00000000..f1298260 --- /dev/null +++ b/source/engines/unity_engine/formulas.cpp @@ -0,0 +1,42 @@ +// +// Created by Vlad on 3/22/2025. +// +#include "omath/engines/unity_engine/formulas.hpp" + +namespace omath::unity_engine +{ + Vector3 forward_vector(const ViewAngles& angles) noexcept + { + const auto vec = rotation_matrix(angles) * mat_column_from_vector(k_abs_forward); + + return {vec.at(0, 0), vec.at(1, 0), vec.at(2, 0)}; + } + Vector3 right_vector(const ViewAngles& angles) noexcept + { + const auto vec = rotation_matrix(angles) * mat_column_from_vector(k_abs_right); + + return {vec.at(0, 0), vec.at(1, 0), vec.at(2, 0)}; + } + Vector3 up_vector(const ViewAngles& angles) noexcept + { + const auto vec = rotation_matrix(angles) * mat_column_from_vector(k_abs_up); + + return {vec.at(0, 0), vec.at(1, 0), vec.at(2, 0)}; + } + Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3& cam_origin) noexcept + { + return mat_camera_view(-forward_vector(angles), right_vector(angles), + up_vector(angles), cam_origin); + } + Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept + { + return mat_rotation_axis_z(angles.roll) + * mat_rotation_axis_y(angles.yaw) + * mat_rotation_axis_x(angles.pitch); + } + Mat4X4 calc_perspective_projection_matrix(const float field_of_view, const float aspect_ratio, const float near, + const float far) noexcept + { + return omath::mat_perspective_right_handed(field_of_view, aspect_ratio, near, far); + } +} // namespace omath::unity_engine diff --git a/source/engines/unity_engine/traits/camera_trait.cpp b/source/engines/unity_engine/traits/camera_trait.cpp new file mode 100644 index 00000000..b981befd --- /dev/null +++ b/source/engines/unity_engine/traits/camera_trait.cpp @@ -0,0 +1,26 @@ +// +// Created by Vlad on 8/11/2025. +// +#include "omath/engines/unity_engine/traits/camera_trait.hpp" + +namespace omath::unity_engine +{ + + ViewAngles CameraTrait::calc_look_at_angle(const Vector3& cam_origin, const Vector3& look_at) noexcept + { + const auto direction = (look_at - cam_origin).normalized(); + + return {PitchAngle::from_radians(-std::asin(direction.y)), + YawAngle::from_radians(std::atan2(direction.x, direction.z)), RollAngle::from_radians(0.f)}; + } + Mat4X4 CameraTrait::calc_view_matrix(const ViewAngles& angles, const Vector3& cam_origin) noexcept + { + return unity_engine::calc_view_matrix(angles, cam_origin); + } + Mat4X4 CameraTrait::calc_projection_matrix(const projection::FieldOfView& fov, + const projection::ViewPort& view_port, const float near, + const float far) noexcept + { + return calc_perspective_projection_matrix(fov.as_degrees(), view_port.aspect_ratio(), near, far); + } +} // namespace omath::unity_engine \ No newline at end of file diff --git a/source/engines/unreal_engine/formulas.cpp b/source/engines/unreal_engine/formulas.cpp new file mode 100644 index 00000000..9b401246 --- /dev/null +++ b/source/engines/unreal_engine/formulas.cpp @@ -0,0 +1,42 @@ +// +// Created by Vlad on 3/22/2025. +// +#include "omath/engines/unreal_engine/formulas.hpp" + +namespace omath::unreal_engine +{ + Vector3 forward_vector(const ViewAngles& angles) noexcept + { + const auto vec = rotation_matrix(angles) * mat_column_from_vector(k_abs_forward); + + return {vec.at(0, 0), vec.at(1, 0), vec.at(2, 0)}; + } + Vector3 right_vector(const ViewAngles& angles) noexcept + { + const auto vec = rotation_matrix(angles) * mat_column_from_vector(k_abs_right); + + return {vec.at(0, 0), vec.at(1, 0), vec.at(2, 0)}; + } + Vector3 up_vector(const ViewAngles& angles) noexcept + { + const auto vec = rotation_matrix(angles) * mat_column_from_vector(k_abs_up); + + return {vec.at(0, 0), vec.at(1, 0), vec.at(2, 0)}; + } + Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3& cam_origin) noexcept + { + return mat_camera_view(forward_vector(angles), -right_vector(angles), + up_vector(angles), cam_origin); + } + Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept + { + return mat_rotation_axis_x(angles.roll) + * mat_rotation_axis_z(angles.yaw) + * mat_rotation_axis_y(angles.pitch); + } + Mat4X4 calc_perspective_projection_matrix(const float field_of_view, const float aspect_ratio, const float near, + const float far) noexcept + { + return mat_perspective_left_handed(field_of_view, aspect_ratio, near, far); + } +} // namespace omath::unreal_engine diff --git a/source/engines/unreal_engine/traits/camera_trait.cpp b/source/engines/unreal_engine/traits/camera_trait.cpp new file mode 100644 index 00000000..9eedce3c --- /dev/null +++ b/source/engines/unreal_engine/traits/camera_trait.cpp @@ -0,0 +1,26 @@ +// +// Created by Vlad on 8/11/2025. +// +#include "omath/engines/unreal_engine/traits/camera_trait.hpp" + +namespace omath::unreal_engine +{ + + ViewAngles CameraTrait::calc_look_at_angle(const Vector3& cam_origin, const Vector3& look_at) noexcept + { + const auto direction = (look_at - cam_origin).normalized(); + + return {PitchAngle::from_radians(-std::asin(direction.z)), + YawAngle::from_radians(std::atan2(direction.y, direction.x)), RollAngle::from_radians(0.f)}; + } + Mat4X4 CameraTrait::calc_view_matrix(const ViewAngles& angles, const Vector3& cam_origin) noexcept + { + return unreal_engine::calc_view_matrix(angles, cam_origin); + } + Mat4X4 CameraTrait::calc_projection_matrix(const projection::FieldOfView& fov, + const projection::ViewPort& view_port, const float near, + const float far) noexcept + { + return calc_perspective_projection_matrix(fov.as_degrees(), view_port.aspect_ratio(), near, far); + } +} // namespace omath::unreal_engine \ No newline at end of file diff --git a/source/matrix.cpp b/source/matrix.cpp deleted file mode 100644 index bb511ffc..00000000 --- a/source/matrix.cpp +++ /dev/null @@ -1,299 +0,0 @@ -/* - * Copyright (c) 2022. - * Created by Alpatov Softworks with love in Russia. - */ - -#include "uml/matrix.h" -#include "uml/Vector3.h" -#include -#include -#include - - -namespace uml -{ - matrix::matrix(const size_t rows, const size_t columns) - { - if (rows == 0 and columns == 0) - throw std::runtime_error("Matrix cannot be 0x0"); - - m_rows = rows; - m_columns = columns; - - m_pData = std::make_unique(m_rows * m_columns); - - set(0.f); - } - - matrix::matrix(const std::vector> &rows) - { - m_rows = rows.size(); - m_columns = rows[0].size(); - - - m_pData = std::make_unique(m_rows * m_columns); - - for (size_t i = 0; i < m_rows; ++i) - for (size_t j = 0; j < m_columns; ++j) - at(i,j) = rows[i][j]; - } - - matrix::matrix(const matrix &other) - { - m_rows = other.m_rows; - m_columns = other.m_columns; - - m_pData = std::make_unique(m_rows * m_columns); - - for (size_t i = 0; i < m_rows; ++i) - for (size_t j = 0; j < m_columns; ++j) - at(i, j) = other.at(i, j); - } - - matrix::matrix(const size_t rows, const size_t columns, const float *pRaw) - { - m_rows = rows; - m_columns = columns; - - - m_pData = std::make_unique(m_rows * m_columns); - - for (size_t i = 0; i < rows*columns; ++i) - at(i / rows, i % columns) = pRaw[i]; - - } - - size_t matrix::get_rows_count() const noexcept - { - return m_rows; - } - - matrix::matrix(matrix &&other) noexcept - { - m_rows = other.m_rows; - m_columns = other.m_columns; - m_pData = std::move(other.m_pData); - - } - - size_t matrix::get_columns_count() const noexcept - { - return m_columns; - } - - std::pair matrix::get_size() const noexcept - { - return {get_rows_count(), get_columns_count()}; - } - - float &matrix::at(const size_t iRow, const size_t iCol) - { - return const_cast(std::as_const(*this).at(iRow, iCol)); - } - - float matrix::get_sum() - { - float sum = 0; - - for (size_t i = 0; i < get_rows_count(); i++) - for (size_t j = 0; j < get_columns_count(); j++) - sum += at(i, j); - - return sum; - } - - const float &matrix::at(const size_t iRow, const size_t iCol) const - { - return m_pData[iRow * m_columns + iCol]; - } - - matrix matrix::operator*(const matrix &other) const - { - if (m_columns != other.m_rows) - throw std::runtime_error("n != m"); - - auto outMat = matrix(m_rows, other.m_columns); - - for (size_t d = 0; d < m_rows; ++d) - for (size_t i = 0; i < other.m_columns; ++i) - for (size_t j = 0; j < other.m_rows; ++j) - outMat.at(d, i) += at(d, j) * other.at(j, i); - - - return outMat; - } - - matrix matrix::operator*(const float f) const - { - auto out = *this; - for (size_t i = 0; i < m_rows; ++i) - for (size_t j = 0; j < m_columns; ++j) - out.at(i, j) *= f; - - return out; - } - - matrix &matrix::operator*=(const float f) - { - for (size_t i = 0; i < get_rows_count(); i++) - for (size_t j = 0; j < get_columns_count(); j++) - at(i, j) *= f; - return *this; - } - - void matrix::clear() - { - set(0.f); - } - - matrix matrix::operator*(const Vector3 &vec3) const - { - auto vecmatrix = matrix(m_rows, 1); - vecmatrix.set(1.f); - vecmatrix.at(0, 0) = vec3.x; - vecmatrix.at(1, 0) = vec3.y; - vecmatrix.at(2, 0) = vec3.z; - - return *this * vecmatrix; - - } - - - matrix &matrix::operator=(const matrix &other) - { - if (this == &other) - return *this; - - for (size_t i = 0; i < m_rows; ++i) - for (size_t j = 0; j < m_columns; ++j) - at(i, j) = other.at(i, j); - - return *this; - - } - - matrix &matrix::operator=(matrix &&other) noexcept - { - if (this == &other) - return *this; - - m_rows = other.m_rows; - m_columns = other.m_columns; - m_pData = std::move(other.m_pData); - - return *this; - - } - - matrix &matrix::operator/=(const float f) - { - for (size_t i = 0; i < m_rows; ++i) - for (size_t j = 0; j < m_columns; ++j) - at(i, j) /= f; - - return *this; - } - - matrix matrix::operator/(const float f) const - { - auto out = *this; - for (size_t i = 0; i < m_rows; ++i) - for (size_t j = 0; j < m_columns; ++j) - out.at(i, j) /= f; - - return out; - } - - float matrix::det() const - { - if (m_rows + m_columns == 2) - return at(0, 0); - - if (m_rows == 2 and m_columns == 2) - return at(0, 0) * at(1, 1) - at(0, 1) * at(1, 0); - - float fDet = 0; - for (size_t i = 0; i < m_columns; i++) - fDet += alg_complement(0, i) * at(0, i); - - return fDet; - } - - float matrix::alg_complement(const size_t i, const size_t j) const - { - const auto tmp = minor(i, j); - return ((i + j + 2) % 2 == 0) ? tmp : -tmp; - } - - matrix matrix::transpose() - { - matrix transposed = {m_columns, m_rows}; - - for (size_t i = 0; i < m_rows; ++i) - for (size_t j = 0; j < m_columns; ++j) - transposed.at(j, i) = at(i, j); - - return transposed; - } - - matrix::~matrix() = default; - - void matrix::set(const float val) - { - for (size_t i = 0; i < m_rows; ++i) - for (size_t j = 0; j < m_columns; ++j) - at(i, j) = val; - } - - matrix matrix::strip(const size_t row, const size_t column) const - { - matrix stripped = {m_rows - 1, m_columns - 1}; - size_t iStripRowIndex = 0; - - for (size_t i = 0; i < m_rows; i++) - { - if (i == row) - continue; - - size_t iStripColumnIndex = 0; - for (size_t j = 0; j < m_columns; ++j) - { - if (j == column) - continue; - - stripped.at(iStripRowIndex, iStripColumnIndex) = at(i, j); - iStripColumnIndex++; - } - - iStripRowIndex++; - } - - return stripped; - } - - float matrix::minor(const size_t i, const size_t j) const - { - return strip(i, j).det(); - } - - matrix matrix::to_screen_matrix(float screenWidth, float screenHeight) - { - return matrix({ - {screenWidth / 2.f, 0.f, 0.f, 0.f}, - {0.f, -screenHeight / 2.f, 0.f, 0.f}, - {0.f, 0.f, 1.f, 0.f}, - {screenWidth / 2.f, screenHeight / 2.f, 0.f, 1.f}, - }); - } - - const float * matrix::raw() const - { - return m_pData.get(); - } - - void matrix::set_from_raw(const float *pRawMatrix) - { - for (size_t i = 0; i < m_columns*m_rows; ++i) - at(i / m_rows, i % m_columns) = pRawMatrix[i]; - } -} \ No newline at end of file diff --git a/source/pathfinding/a_star.cpp b/source/pathfinding/a_star.cpp new file mode 100644 index 00000000..a2bff259 --- /dev/null +++ b/source/pathfinding/a_star.cpp @@ -0,0 +1,115 @@ +// +// Created by Vlad on 28.07.2024. +// +#include "omath/pathfinding/a_star.hpp" +#include +#include +#include +#include +#include + +namespace +{ + struct OpenListNode final + { + omath::Vector3 position; + float f_cost; + + [[nodiscard]] + bool operator>(const OpenListNode& other) const noexcept + { + return f_cost > other.f_cost; + } + }; +} + +namespace omath::pathfinding +{ + struct PathNode final + { + std::optional> came_from; + float g_cost = 0.f; + }; + + std::vector> + Astar::reconstruct_final_path(const std::unordered_map, PathNode>& closed_list, + const Vector3& current) noexcept + { + std::vector> path; + std::optional current_opt = current; + + while (current_opt) + { + path.push_back(*current_opt); + + auto it = closed_list.find(*current_opt); + + if (it == closed_list.end()) + break; + + current_opt = it->second.came_from; + } + + std::ranges::reverse(path); + return path; + } + + std::vector> Astar::find_path(const Vector3& start, const Vector3& end, + const NavigationMesh& nav_mesh) noexcept + { + std::unordered_map, PathNode> closed_list; + std::unordered_map, PathNode> node_data; + std::priority_queue, std::greater<>> open_list; + + auto maybe_start_vertex = nav_mesh.get_closest_vertex(start); + auto maybe_end_vertex = nav_mesh.get_closest_vertex(end); + + if (!maybe_start_vertex || !maybe_end_vertex) + return {}; + + const auto start_vertex = maybe_start_vertex.value(); + const auto end_vertex = maybe_end_vertex.value(); + + node_data.emplace(start_vertex, PathNode{std::nullopt, 0.f}); + open_list.push({start_vertex, start_vertex.distance_to(end_vertex)}); + + while (!open_list.empty()) + { + auto current = open_list.top().position; + open_list.pop(); + + if (closed_list.contains(current)) + continue; + + auto current_node_it = node_data.find(current); + if (current_node_it == node_data.end()) + continue; + + const auto current_node = current_node_it->second; + + if (current == end_vertex) + return reconstruct_final_path(closed_list, current); + + closed_list.emplace(current, current_node); + + for (const auto& neighbor: nav_mesh.get_neighbors(current)) + { + if (closed_list.contains(neighbor)) + continue; + + const float tentative_g_cost = current_node.g_cost + neighbor.distance_to(current); + + auto node_it = node_data.find(neighbor); + + if (node_it == node_data.end() || tentative_g_cost < node_it->second.g_cost) + { + node_data[neighbor] = PathNode{current, tentative_g_cost}; + const float f_cost = tentative_g_cost + neighbor.distance_to(end_vertex); + open_list.push({neighbor, f_cost}); + } + } + } + + return {}; + } +} // namespace omath::pathfinding diff --git a/source/pathfinding/navigation_mesh.cpp b/source/pathfinding/navigation_mesh.cpp new file mode 100644 index 00000000..370bc8fd --- /dev/null +++ b/source/pathfinding/navigation_mesh.cpp @@ -0,0 +1,106 @@ +// +// Created by Vlad on 28.07.2024. +// +#include "omath/pathfinding/navigation_mesh.hpp" +#include +#include +#include +#include +namespace omath::pathfinding +{ + std::expected, std::string> + NavigationMesh::get_closest_vertex(const Vector3& point) const noexcept + { + const auto res = std::ranges::min_element(m_vertex_map, [&point](const auto& a, const auto& b) + { return a.first.distance_to(point) < b.first.distance_to(point); }); + + if (res == m_vertex_map.cend()) + return std::unexpected("Failed to get closest point"); + + return res->first; + } + + const std::vector>& NavigationMesh::get_neighbors(const Vector3& vertex) const noexcept + { + return m_vertex_map.at(vertex); + } + + bool NavigationMesh::empty() const + { + return m_vertex_map.empty(); + } + + std::vector NavigationMesh::serialize() const noexcept + { + std::vector raw; + + // Pre-calculate total size for better performance + std::size_t total_size = 0; + for (const auto& [vertex, neighbors] : m_vertex_map) + { + total_size += sizeof(vertex) + sizeof(std::uint16_t) + sizeof(Vector3) * neighbors.size(); + } + raw.reserve(total_size); + + auto dump_to_vector = [&raw](const T& t) + { + const auto* byte_ptr = reinterpret_cast(&t); + raw.insert(raw.end(), byte_ptr, byte_ptr + sizeof(T)); + }; + + for (const auto& [vertex, neighbors] : m_vertex_map) + { + // Clamp neighbors count to fit in uint16_t (prevents silent data corruption) + // NOTE: If neighbors.size() > 65535, only the first 65535 neighbors will be serialized. + // This is a limitation of the current serialization format using uint16_t for count. + const auto clamped_count = + std::min(neighbors.size(), std::numeric_limits::max()); + const auto neighbors_count = static_cast(clamped_count); + + dump_to_vector(vertex); + dump_to_vector(neighbors_count); + + // Only serialize up to the clamped count + for (std::size_t i = 0; i < clamped_count; ++i) + dump_to_vector(neighbors[i]); + } + return raw; + } + + void NavigationMesh::deserialize(const std::vector& raw) noexcept + { + auto load_from_vector = [](const std::vector& vec, std::size_t& offset, auto& value) + { + if (offset + sizeof(value) > vec.size()) + throw std::runtime_error("Deserialize: Invalid input data size."); + + std::copy_n(vec.data() + offset, sizeof(value), reinterpret_cast(&value)); + offset += sizeof(value); + }; + + m_vertex_map.clear(); + + std::size_t offset = 0; + + while (offset < raw.size()) + { + Vector3 vertex; + load_from_vector(raw, offset, vertex); + + std::uint16_t neighbors_count; + load_from_vector(raw, offset, neighbors_count); + + std::vector> neighbors; + neighbors.reserve(neighbors_count); + + for (std::size_t i = 0; i < neighbors_count; ++i) + { + Vector3 neighbor; + load_from_vector(raw, offset, neighbor); + neighbors.push_back(neighbor); + } + + m_vertex_map.emplace(vertex, std::move(neighbors)); + } + } +} // namespace omath::pathfinding diff --git a/source/projectile_prediction/proj_pred_engine_avx2.cpp b/source/projectile_prediction/proj_pred_engine_avx2.cpp new file mode 100644 index 00000000..9605db6d --- /dev/null +++ b/source/projectile_prediction/proj_pred_engine_avx2.cpp @@ -0,0 +1,165 @@ +// +// Created by Vlad on 2/23/2025. +// +#include "omath/projectile_prediction/proj_pred_engine_avx2.hpp" +#include +#include + +#if defined(OMATH_USE_AVX2) && defined(__i386__) && defined(__x86_64__) +#include +#else +#include +#endif + +namespace omath::projectile_prediction +{ + std::optional> + ProjPredEngineAvx2::maybe_calculate_aim_point([[maybe_unused]] const Projectile& projectile, + [[maybe_unused]] const Target& target) const + { +#if defined(OMATH_USE_AVX2) && defined(__i386__) && defined(__x86_64__) + const float bullet_gravity = m_gravity_constant * projectile.m_gravity_scale; + const float v0 = projectile.m_launch_speed; + const float v0_sqr = v0 * v0; + const Vector3 proj_origin = projectile.m_origin; + + constexpr int SIMD_FACTOR = 8; + float current_time = m_simulation_time_step; + + for (; current_time <= m_maximum_simulation_time; current_time += m_simulation_time_step * SIMD_FACTOR) + { + const __m256 times + = _mm256_setr_ps(current_time, current_time + m_simulation_time_step, + current_time + m_simulation_time_step * 2, current_time + m_simulation_time_step * 3, + current_time + m_simulation_time_step * 4, current_time + m_simulation_time_step * 5, + current_time + m_simulation_time_step * 6, current_time + m_simulation_time_step * 7); + + const __m256 target_x + = _mm256_fmadd_ps(_mm256_set1_ps(target.m_velocity.x), times, _mm256_set1_ps(target.m_origin.x)); + const __m256 target_y + = _mm256_fmadd_ps(_mm256_set1_ps(target.m_velocity.y), times, _mm256_set1_ps(target.m_origin.y)); + const __m256 times_sq = _mm256_mul_ps(times, times); + const __m256 target_z = _mm256_fmadd_ps(_mm256_set1_ps(target.m_velocity.z), times, + _mm256_fnmadd_ps(_mm256_set1_ps(0.5f * m_gravity_constant), times_sq, + _mm256_set1_ps(target.m_origin.z))); + + const __m256 delta_x = _mm256_sub_ps(target_x, _mm256_set1_ps(proj_origin.x)); + const __m256 delta_y = _mm256_sub_ps(target_y, _mm256_set1_ps(proj_origin.y)); + const __m256 delta_z = _mm256_sub_ps(target_z, _mm256_set1_ps(proj_origin.z)); + + const __m256 d_sqr = _mm256_add_ps(_mm256_mul_ps(delta_x, delta_x), _mm256_mul_ps(delta_y, delta_y)); + + const __m256 bg_times_sq = _mm256_mul_ps(_mm256_set1_ps(bullet_gravity), times_sq); + const __m256 term = _mm256_add_ps(delta_z, _mm256_mul_ps(_mm256_set1_ps(0.5f), bg_times_sq)); + const __m256 term_sq = _mm256_mul_ps(term, term); + const __m256 numerator = _mm256_add_ps(d_sqr, term_sq); + const __m256 denominator = _mm256_add_ps(times_sq, _mm256_set1_ps(1e-8f)); // Avoid division by zero + const __m256 required_v0_sqr = _mm256_div_ps(numerator, denominator); + + const __m256 v0_sqr_vec = _mm256_set1_ps(v0_sqr + 1e-3f); + const __m256 mask = _mm256_cmp_ps(required_v0_sqr, v0_sqr_vec, _CMP_LE_OQ); + + const unsigned valid_mask = _mm256_movemask_ps(mask); + + if (!valid_mask) + continue; + + alignas(32) float valid_times[SIMD_FACTOR]; + _mm256_store_ps(valid_times, times); + + for (int i = 0; i < SIMD_FACTOR; ++i) + { + if (!(valid_mask & (1 << i))) + continue; + + const float candidate_time = valid_times[i]; + + if (candidate_time > m_maximum_simulation_time) + continue; + + // Fine search around candidate time + for (float fine_time = candidate_time - m_simulation_time_step * 2; + fine_time <= candidate_time + m_simulation_time_step * 2; fine_time += m_simulation_time_step) + { + if (fine_time < 0) + continue; + + // Manually compute predicted target position to avoid adding method to Target + Vector3 target_pos = target.m_origin + target.m_velocity * fine_time; + if (target.m_is_airborne) + target_pos.z -= 0.5f * m_gravity_constant * fine_time * fine_time; + + const auto pitch = calculate_pitch(proj_origin, target_pos, bullet_gravity, v0, fine_time); + if (!pitch) + continue; + + const Vector3 delta = target_pos - proj_origin; + const float d = std::sqrt(delta.x * delta.x + delta.y * delta.y); + const float height = d * std::tan(angles::degrees_to_radians(*pitch)); + return Vector3(target_pos.x, target_pos.y, proj_origin.z + height); + } + } + } + + // Fallback scalar processing for remaining times + for (; current_time <= m_maximum_simulation_time; current_time += m_simulation_time_step) + { + Vector3 target_pos = target.m_origin + target.m_velocity * current_time; + if (target.m_is_airborne) + target_pos.z -= 0.5f * m_gravity_constant * current_time * current_time; + + const auto pitch = calculate_pitch(proj_origin, target_pos, bullet_gravity, v0, current_time); + if (!pitch) + continue; + + const Vector3 delta = target_pos - proj_origin; + const float d = std::sqrt(delta.x * delta.x + delta.y * delta.y); + const float height = d * std::tan(angles::degrees_to_radians(*pitch)); + return Vector3(target_pos.x, target_pos.y, proj_origin.z + height); + } + + return std::nullopt; +#else + throw std::runtime_error( + std::format("{} AVX2 feature is not enabled!", std::source_location::current().function_name())); +#endif + } + ProjPredEngineAvx2::ProjPredEngineAvx2(const float gravity_constant, const float simulation_time_step, + const float maximum_simulation_time) + : m_gravity_constant(gravity_constant), m_simulation_time_step(simulation_time_step), + m_maximum_simulation_time(maximum_simulation_time) + { + } + std::optional ProjPredEngineAvx2::calculate_pitch([[maybe_unused]] const Vector3& proj_origin, + [[maybe_unused]] const Vector3& target_pos, + [[maybe_unused]] const float bullet_gravity, + [[maybe_unused]] const float v0, + [[maybe_unused]] const float time) + { +#if defined(OMATH_USE_AVX2) && defined(__i386__) && defined(__x86_64__) + if (time <= 0.0f) + return std::nullopt; + + const Vector3 delta = target_pos - proj_origin; + const float d_sqr = delta.x * delta.x + delta.y * delta.y; + const float h = delta.z; + + const float term = h + 0.5f * bullet_gravity * time * time; + const float required_v0_sqr = (d_sqr + term * term) / (time * time); + const float v0_sqr = v0 * v0; + + if (required_v0_sqr > v0_sqr + 1e-3f) + return std::nullopt; + + if (d_sqr == 0.0f) + return term >= 0.0f ? 90.0f : -90.0f; + + const float d = std::sqrt(d_sqr); + const float tan_theta = term / d; + return angles::radians_to_degrees(std::atan(tan_theta)); +#else + throw std::runtime_error( + std::format("{} AVX2 feature is not enabled!", std::source_location::current().function_name())); +#endif + } +} // namespace omath::projectile_prediction diff --git a/source/utility/elf_pattern_scan.cpp b/source/utility/elf_pattern_scan.cpp new file mode 100644 index 00000000..be5d3a03 --- /dev/null +++ b/source/utility/elf_pattern_scan.cpp @@ -0,0 +1,325 @@ +// +// Created by Vladislav on 30.12.2025. +// +#include "omath/utility/pattern_scan.hpp" +#include +#include +#include +#include +#include +#include + +#pragma pack(push, 1) + +namespace +{ + // Common + constexpr uint8_t ei_nident = 16; + constexpr uint8_t ei_class = 4; + + constexpr uint8_t elfclass32 = 1; + constexpr uint8_t elfclass64 = 2; + // ReSharper disable CppDeclaratorNeverUsed + struct Elf32Ehdr final + { + unsigned char e_ident[ei_nident]; + uint16_t e_type; + uint16_t e_machine; + uint32_t e_version; + uint32_t e_entry; + uint32_t e_phoff; + uint32_t e_shoff; + uint32_t e_flags; + uint16_t e_ehsize; + uint16_t e_phentsize; + uint16_t e_phnum; + uint16_t e_shentsize; + uint16_t e_shnum; + uint16_t e_shstrndx; + }; + + struct Elf64Ehdr final + { + unsigned char e_ident[ei_nident]; + uint16_t e_type; + uint16_t e_machine; + uint32_t e_version; + uint64_t e_entry; + uint64_t e_phoff; + uint64_t e_shoff; + uint32_t e_flags; + uint16_t e_ehsize; + uint16_t e_phentsize; + uint16_t e_phnum; + uint16_t e_shentsize; + uint16_t e_shnum; + uint16_t e_shstrndx; + }; + + struct Elf32Shdr final + { + uint32_t sh_name; + uint32_t sh_type; + uint32_t sh_flags; + uint32_t sh_addr; + uint32_t sh_offset; + uint32_t sh_size; + uint32_t sh_link; + uint32_t sh_info; + uint32_t sh_addralign; + uint32_t sh_entsize; + }; + + struct Elf64Shdr final + { + uint32_t sh_name; + uint32_t sh_type; + uint64_t sh_flags; + uint64_t sh_addr; + uint64_t sh_offset; + uint64_t sh_size; + uint32_t sh_link; + uint32_t sh_info; + uint64_t sh_addralign; + uint64_t sh_entsize; + }; + // ReSharper restore CppDeclaratorNeverUsed +#pragma pack(pop) +} // namespace + +namespace +{ + enum class FileArch : std::int8_t + { + x32, + x64, + }; + template + struct ElfHeaders + { + using FileHeader = std::conditional_t; + using SectionHeader = std::conditional_t; + FileHeader file_header; + SectionHeader section_header; + }; + [[nodiscard]] + bool not_elf_file(std::fstream& file) + { + constexpr std::string_view valid_elf_signature = "\x7F" + "ELF"; + std::array elf_signature{}; + const std::streampos back_up_pose = file.tellg(); + + file.seekg(0, std::ios_base::beg); + file.read(elf_signature.data(), 4); + file.seekg(back_up_pose, std::ios_base::beg); + + return std::string_view{elf_signature.data(), 4} != valid_elf_signature; + } + [[nodiscard]] + std::optional get_file_arch(std::fstream& file) + { + std::array e_ident{}; + const std::streampos back_up_pose = file.tellg(); + + file.seekg(0, std::ios_base::beg); + file.read(e_ident.data(), e_ident.size()); + file.seekg(back_up_pose, std::ios_base::beg); + + if (e_ident[ei_class] == elfclass64) + return FileArch::x64; + + if (e_ident[ei_class] == elfclass32) + return FileArch::x32; + + return std::nullopt; + } + struct ExtractedSection final + { + std::uintptr_t virtual_base_addr{}; + std::uintptr_t raw_base_addr{}; + std::vector data; + }; + [[maybe_unused]] + std::optional get_elf_section_by_name(const std::filesystem::path& path, + const std::string_view& section_name) + { + std::fstream file(path, std::ios::binary | std::ios::in); + + if (!file.is_open()) [[unlikely]] + return std::nullopt; + + if (not_elf_file(file)) [[unlikely]] + return std::nullopt; + + const auto architecture = get_file_arch(file); + + if (!architecture.has_value()) [[unlikely]] + return std::nullopt; + + std::variant, ElfHeaders> elf_headers; + if (architecture.value() == FileArch::x64) + elf_headers = ElfHeaders{}; + else if (architecture.value() == FileArch::x32) + elf_headers = ElfHeaders{}; + + return std::visit( + [&](auto& header) -> std::optional + { + auto& [file_header, section_header] = header; + file.seekg(0, std::ios_base::beg); + if (!file.read(reinterpret_cast(&file_header), sizeof(file_header))) [[unlikely]] + return std::nullopt; + + const std::streamoff shstr_off = + static_cast(file_header.e_shoff) + + static_cast(file_header.e_shstrndx) * sizeof(section_header); + file.seekg(shstr_off, std::ios_base::beg); + + if (!file.read(reinterpret_cast(§ion_header), sizeof(section_header))) [[unlikely]] + return std::nullopt; + + std::vector shstrtab(static_cast(section_header.sh_size)); + + file.seekg(section_header.sh_offset, std::ios_base::beg); + + if (!file.read(shstrtab.data(), static_cast(shstrtab.size()))) [[unlikely]] + return std::nullopt; + + for (std::uint16_t i = 0; i < file_header.e_shnum; ++i) + { + decltype(section_header) current_section{}; + const std::streamoff off = static_cast(file_header.e_shoff) + + static_cast(i) * sizeof(current_section); + + file.seekg(off, std::ios_base::beg); + if (!file.read(reinterpret_cast(¤t_section), sizeof(current_section))) [[unlikely]] + return std::nullopt; + + if (current_section.sh_name >= shstrtab.size()) [[unlikely]] + continue; + + // ReSharper disable once CppTooWideScopeInitStatement + const std::string_view name = &shstrtab[current_section.sh_name]; + if (section_name != name) + continue; + + ExtractedSection out; + + out.virtual_base_addr = static_cast(current_section.sh_addr); + out.raw_base_addr = static_cast(current_section.sh_offset); + out.data.resize(static_cast(current_section.sh_size)); + + file.seekg(static_cast(out.raw_base_addr), std::ios_base::beg); + if (!file.read(reinterpret_cast(out.data.data()), + static_cast(out.data.size()))) [[unlikely]] + return std::nullopt; + + return out; + } + return std::nullopt; + }, + elf_headers); + } + + template + std::optional scan_in_module_impl(const std::byte* base, const std::string_view pattern, + const std::string_view target_section_name) + { + const auto* file_header = reinterpret_cast(base); + + const auto shoff = static_cast(file_header->e_shoff); + const auto shnum = static_cast(file_header->e_shnum); + const auto shstrnd = static_cast(file_header->e_shstrndx); + + const auto shstrtab_off = shoff + shstrnd * sizeof(SectionHeader); + const auto* shstrtab_hdr = reinterpret_cast(base + shstrtab_off); + + const auto shstrtab = reinterpret_cast(base + static_cast(shstrtab_hdr->sh_offset)); + + const auto shstrtab_size = static_cast(shstrtab_hdr->sh_size); + + for (std::size_t i = 0; i < shnum; ++i) + { + const auto section_off = shoff + i * sizeof(SectionHeader); + const auto* section = reinterpret_cast(base + section_off); + + if (section->sh_size == 0) + continue; + + if (std::cmp_greater_equal(section->sh_name, shstrtab_size)) + continue; + + if (std::string_view{shstrtab + section->sh_name} != target_section_name) + continue; + + const auto* section_begin = base + static_cast(section->sh_addr); + const auto* section_end = section_begin + static_cast(section->sh_size); + + const auto scan_result = omath::PatternScanner::scan_for_pattern(section_begin, section_end, pattern); + if (scan_result == section_end) + return std::nullopt; + + return reinterpret_cast(scan_result); + } + + return std::nullopt; + } +} // namespace +namespace omath +{ + std::optional + ElfPatternScanner::scan_for_pattern_in_loaded_module(const void* module_base_address, + const std::string_view& pattern, + const std::string_view& target_section_name) + { + if (module_base_address == nullptr) [[unlikely]] + return std::nullopt; + + const auto* base = static_cast(module_base_address); + + // Validate ELF signature. + constexpr std::string_view valid_elf_signature = "\x7F" + "ELF"; + if (std::string_view{reinterpret_cast(base), valid_elf_signature.size()} != valid_elf_signature) + return std::nullopt; + + // Detect architecture. + const auto ei_class_value = static_cast(base[ei_class]); + const auto arch = ei_class_value == elfclass64 ? FileArch::x64 + : ei_class_value == elfclass32 ? FileArch::x32 + : std::optional{}; + if (!arch.has_value()) [[unlikely]] + return std::nullopt; + + if (arch == FileArch::x64) + return scan_in_module_impl(static_cast(module_base_address), + pattern, target_section_name); + if (arch == FileArch::x32) + return scan_in_module_impl(static_cast(module_base_address), + pattern, target_section_name); + + std::unreachable(); + } + std::optional + ElfPatternScanner::scan_for_pattern_in_file(const std::filesystem::path& path_to_file, + const std::string_view& pattern, + const std::string_view& target_section_name) + { + const auto pe_section = get_elf_section_by_name(path_to_file, target_section_name); + + if (!pe_section.has_value()) [[unlikely]] + return std::nullopt; + + const auto scan_result = + PatternScanner::scan_for_pattern(pe_section->data.cbegin(), pe_section->data.cend(), pattern); + + if (scan_result == pe_section->data.cend()) + return std::nullopt; + const auto offset = std::distance(pe_section->data.begin(), scan_result); + + return SectionScanResult{.virtual_base_addr = pe_section->virtual_base_addr, + .raw_base_addr = pe_section->raw_base_addr, + .target_offset = offset}; + } +} // namespace omath \ No newline at end of file diff --git a/source/utility/macho_pattern_scan.cpp b/source/utility/macho_pattern_scan.cpp new file mode 100644 index 00000000..84aa14c0 --- /dev/null +++ b/source/utility/macho_pattern_scan.cpp @@ -0,0 +1,349 @@ +// +// Created by Copilot on 04.02.2026. +// +#include "omath/utility/macho_pattern_scan.hpp" +#include "omath/utility/pattern_scan.hpp" +#include +#include +#include +#include + +#pragma pack(push, 1) + +namespace +{ + // Mach-O magic numbers + constexpr std::uint32_t mh_magic_32 = 0xFEEDFACE; + constexpr std::uint32_t mh_magic_64 = 0xFEEDFACF; + constexpr std::uint32_t mh_cigam_32 = 0xCEFAEDFE; // Byte-swapped 32-bit + constexpr std::uint32_t mh_cigam_64 = 0xCFFAEDFE; // Byte-swapped 64-bit + + // Load command types + constexpr std::uint32_t lc_segment = 0x1; + constexpr std::uint32_t lc_segment_64 = 0x19; + + // ReSharper disable CppDeclaratorNeverUsed + // Mach-O header for 32-bit + struct MachHeader32 final + { + std::uint32_t magic; + std::uint32_t cputype; + std::uint32_t cpusubtype; + std::uint32_t filetype; + std::uint32_t ncmds; + std::uint32_t sizeofcmds; + std::uint32_t flags; + }; + + // Mach-O header for 64-bit + struct MachHeader64 final + { + std::uint32_t magic; + std::uint32_t cputype; + std::uint32_t cpusubtype; + std::uint32_t filetype; + std::uint32_t ncmds; + std::uint32_t sizeofcmds; + std::uint32_t flags; + std::uint32_t reserved; + }; + + // Load command header + struct LoadCommand final + { + std::uint32_t cmd; + std::uint32_t cmdsize; + }; + + // Segment command for 32-bit + struct SegmentCommand32 final + { + std::uint32_t cmd; + std::uint32_t cmdsize; + char segname[16]; + std::uint32_t vmaddr; + std::uint32_t vmsize; + std::uint32_t fileoff; + std::uint32_t filesize; + std::uint32_t maxprot; + std::uint32_t initprot; + std::uint32_t nsects; + std::uint32_t flags; + }; + + // Segment command for 64-bit + struct SegmentCommand64 final + { + std::uint32_t cmd; + std::uint32_t cmdsize; + char segname[16]; + std::uint64_t vmaddr; + std::uint64_t vmsize; + std::uint64_t fileoff; + std::uint64_t filesize; + std::uint32_t maxprot; + std::uint32_t initprot; + std::uint32_t nsects; + std::uint32_t flags; + }; + + // Section for 32-bit + struct Section32 final + { + char sectname[16]; + char segname[16]; + std::uint32_t addr; + std::uint32_t size; + std::uint32_t offset; + std::uint32_t align; + std::uint32_t reloff; + std::uint32_t nreloc; + std::uint32_t flags; + std::uint32_t reserved1; + std::uint32_t reserved2; + }; + + // Section for 64-bit + struct Section64 final + { + char sectname[16]; + char segname[16]; + std::uint64_t addr; + std::uint64_t size; + std::uint32_t offset; + std::uint32_t align; + std::uint32_t reloff; + std::uint32_t nreloc; + std::uint32_t flags; + std::uint32_t reserved1; + std::uint32_t reserved2; + std::uint32_t reserved3; + }; + // ReSharper enable CppDeclaratorNeverUsed +#pragma pack(pop) + + enum class MachOArch : std::int8_t + { + x32, + x64, + }; + + struct ExtractedSection final + { + std::uintptr_t virtual_base_addr{}; + std::uintptr_t raw_base_addr{}; + std::vector data; + }; + + [[nodiscard]] + std::optional get_macho_arch(std::fstream& file) + { + std::uint32_t magic{}; + const std::streampos backup_pos = file.tellg(); + + file.seekg(0, std::ios_base::beg); + file.read(reinterpret_cast(&magic), sizeof(magic)); + file.seekg(backup_pos, std::ios_base::beg); + + if (magic == mh_magic_64 || magic == mh_cigam_64) + return MachOArch::x64; + + if (magic == mh_magic_32 || magic == mh_cigam_32) + return MachOArch::x32; + + return std::nullopt; + } + + [[nodiscard]] + bool is_macho_file(std::fstream& file) + { + return get_macho_arch(file).has_value(); + } + + [[nodiscard]] + std::string_view get_section_name(const char* sectname) + { + // Mach-O section names are fixed 16-byte arrays, not necessarily null-terminated + return std::string_view(sectname, std::min(std::strlen(sectname), std::size_t{16})); + } + + template + std::optional extract_section_impl(std::fstream& file, const std::string_view& section_name) + { + HeaderType header{}; + file.seekg(0, std::ios_base::beg); + if (!file.read(reinterpret_cast(&header), sizeof(header))) [[unlikely]] + return std::nullopt; + + std::streamoff cmd_offset = sizeof(header); + + for (std::uint32_t i = 0; i < header.ncmds; ++i) + { + LoadCommand lc{}; + file.seekg(cmd_offset, std::ios_base::beg); + if (!file.read(reinterpret_cast(&lc), sizeof(lc))) [[unlikely]] + return std::nullopt; + + if (lc.cmd != segment_cmd) + { + cmd_offset += static_cast(lc.cmdsize); + continue; + } + SegmentType segment{}; + file.seekg(cmd_offset, std::ios_base::beg); + if (!file.read(reinterpret_cast(&segment), sizeof(segment))) [[unlikely]] + return std::nullopt; + + if (!segment.nsects) + { + cmd_offset += static_cast(lc.cmdsize); + continue; + } + std::streamoff sect_offset = cmd_offset + static_cast(sizeof(segment)); + + for (std::uint32_t j = 0; j < segment.nsects; ++j) + { + SectionType section{}; + file.seekg(sect_offset, std::ios_base::beg); + if (!file.read(reinterpret_cast(§ion), sizeof(section))) [[unlikely]] + return std::nullopt; + + if (get_section_name(section.sectname) != section_name) + { + sect_offset += static_cast(sizeof(section)); + continue; + } + + ExtractedSection out; + out.virtual_base_addr = static_cast(section.addr); + out.raw_base_addr = static_cast(section.offset); + out.data.resize(static_cast(section.size)); + + file.seekg(static_cast(section.offset), std::ios_base::beg); + if (!file.read(reinterpret_cast(out.data.data()), static_cast(out.data.size()))) + [[unlikely]] + return std::nullopt; + + return out; + } + } + + return std::nullopt; + } + + [[nodiscard]] + std::optional get_macho_section_by_name(const std::filesystem::path& path, + const std::string_view& section_name) + { + std::fstream file(path, std::ios::binary | std::ios::in); + + if (!file.is_open()) [[unlikely]] + return std::nullopt; + + if (!is_macho_file(file)) [[unlikely]] + return std::nullopt; + + const auto arch = get_macho_arch(file); + + if (!arch.has_value()) [[unlikely]] + return std::nullopt; + + if (arch.value() == MachOArch::x64) + return extract_section_impl(file, section_name); + return extract_section_impl(file, section_name); + } + + template + std::optional scan_in_module_impl(const std::byte* base, const std::string_view pattern, + const std::string_view target_section_name) + { + const auto* header = reinterpret_cast(base); + + std::size_t cmd_offset = sizeof(HeaderType); + + for (std::uint32_t i = 0; i < header->ncmds; ++i) + { + const auto* lc = reinterpret_cast(base + cmd_offset); + + if (lc->cmd != segment_cmd) + { + cmd_offset += lc->cmdsize; + continue; + } + const auto* segment = reinterpret_cast(base + cmd_offset); + std::size_t sect_offset = cmd_offset + sizeof(SegmentType); + + for (std::uint32_t j = 0; j < segment->nsects; ++j) + { + const auto* section = reinterpret_cast(base + sect_offset); + + if (get_section_name(section->sectname) != target_section_name && section->size > 0) + { + sect_offset += sizeof(SectionType); + continue; + } + const auto* section_begin = base + static_cast(section->addr); + const auto* section_end = section_begin + static_cast(section->size); + + const auto scan_result = omath::PatternScanner::scan_for_pattern(section_begin, section_end, pattern); + + if (scan_result != section_end) + return reinterpret_cast(scan_result); + } + } + + return std::nullopt; + } + +} // namespace + +namespace omath +{ + std::optional + MachOPatternScanner::scan_for_pattern_in_loaded_module(const void* module_base_address, + const std::string_view& pattern, + const std::string_view& target_section_name) + { + if (module_base_address == nullptr) [[unlikely]] + return std::nullopt; + + const auto* base = static_cast(module_base_address); + + // Read magic to determine architecture + std::uint32_t magic{}; + std::memcpy(&magic, base, sizeof(magic)); + + if (magic == mh_magic_64 || magic == mh_cigam_64) + return scan_in_module_impl(base, pattern, + target_section_name); + + if (magic == mh_magic_32 || magic == mh_cigam_32) + return scan_in_module_impl(base, pattern, + target_section_name); + + return std::nullopt; + } + + std::optional + MachOPatternScanner::scan_for_pattern_in_file(const std::filesystem::path& path_to_file, + const std::string_view& pattern, + const std::string_view& target_section_name) + { + const auto macho_section = get_macho_section_by_name(path_to_file, target_section_name); + + if (!macho_section.has_value()) [[unlikely]] + return std::nullopt; + + const auto scan_result = + PatternScanner::scan_for_pattern(macho_section->data.cbegin(), macho_section->data.cend(), pattern); + + if (scan_result == macho_section->data.cend()) + return std::nullopt; + + const auto offset = std::distance(macho_section->data.begin(), scan_result); + + return SectionScanResult{.virtual_base_addr = macho_section->virtual_base_addr, + .raw_base_addr = macho_section->raw_base_addr, + .target_offset = offset}; + } +} // namespace omath diff --git a/source/utility/pattern_scan.cpp b/source/utility/pattern_scan.cpp new file mode 100644 index 00000000..b6443479 --- /dev/null +++ b/source/utility/pattern_scan.cpp @@ -0,0 +1,75 @@ +// +// Created by Vlad on 10/4/2025. +// +#include "omath/utility/pattern_scan.hpp" +#include +#include +#include + +namespace +{ + [[nodiscard]] + constexpr bool is_wildcard(const std::string_view& byte_str) + { + return byte_str == "?" || byte_str == "??"; + } + + [[nodiscard]] + constexpr bool invalid_byte_str_size(const std::string_view& byte_str) + { + return byte_str.empty() || byte_str.size() >= 3; + } +} + +namespace omath +{ + + std::span::iterator + PatternScanner::scan_for_pattern(const std::span& range, const std::string_view& pattern) + { + return scan_for_pattern(range.begin(), range.end(), pattern); + } + std::expected>, PatternScanError> + PatternScanner::parse_pattern(const std::string_view& pattern_string) + { + std::vector> pattern; + + auto start = pattern_string.cbegin(); + + while (start != pattern_string.cend()) + { + const auto end = std::ranges::find(start, pattern_string.cend(), ' '); + + const auto sting_view_start = std::distance(pattern_string.cbegin(), start); + const auto sting_view_end = std::distance(start, end); + + const std::string_view byte_str = pattern_string.substr(sting_view_start, sting_view_end); + + if (invalid_byte_str_size(byte_str)) [[unlikely]] + { + start = end != pattern_string.end() ? std::next(end) : end; + continue; + } + + if (is_wildcard(byte_str)) + { + pattern.emplace_back(std::nullopt); + + start = end != pattern_string.end() ? std::next(end) : end; + continue; + } + + std::uint8_t value = 0; + // ReSharper disable once CppTooWideScopeInitStatement + const auto [_, error_code] = std::from_chars(byte_str.data(), byte_str.data() + byte_str.size(), value, 16); + + if (error_code != std::errc{}) [[unlikely]] + return std::unexpected(PatternScanError::INVALID_PATTERN_STRING); + + pattern.emplace_back(static_cast(value)); + + start = end != pattern_string.end() ? std::next(end) : end; + } + return pattern; + } +} // namespace omath \ No newline at end of file diff --git a/source/utility/pe_pattern_scan.cpp b/source/utility/pe_pattern_scan.cpp new file mode 100644 index 00000000..58ec3793 --- /dev/null +++ b/source/utility/pe_pattern_scan.cpp @@ -0,0 +1,386 @@ +// +// Created by Vlad on 10/7/2025. +// +#include "omath/utility/pe_pattern_scan.hpp" +#include "omath/utility/pattern_scan.hpp" +#include +#include +#include +#include + +// Internal PE shit defines +// Big thx for linuxpe sources as ref +// Link: https://github.com/can1357/linux-pe +namespace +{ + constexpr std::uint16_t opt_hdr32_magic = 0x010B; + constexpr std::uint16_t opt_hdr64_magic = 0x020B; + + // Standard fields. + // ReSharper disable CppDeclaratorNeverUsed + struct DataDirectory final + { + std::uint32_t rva; + std::uint32_t size; + }; + struct OptionalHeaderX64 final + { + 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; + + // NT additional fields. + std::uint64_t image_base; + std::uint32_t section_alignment; + std::uint32_t file_alignment; + + std::uint32_t os_version; + std::uint32_t img_version; + std::uint32_t subsystem_version; + std::uint32_t win32_version_value; + + std::uint32_t size_image; + std::uint32_t size_headers; + + std::uint32_t checksum; + std::uint16_t subsystem; + std::uint16_t characteristics; + + std::uint64_t size_stack_reserve; + std::uint64_t size_stack_commit; + std::uint64_t size_heap_reserve; + std::uint64_t size_heap_commit; + + std::uint32_t ldr_flags; + + std::uint32_t num_data_directories; + DataDirectory data_directories[16]; + }; + struct OptionalHeaderX86 final + { + // Standard fields. + 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::uint32_t base_of_data{}; + + // NT additional fields. + std::uint32_t image_base{}; + std::uint32_t section_alignment{}; + std::uint32_t file_alignment{}; + + std::uint32_t os_version{}; + std::uint32_t img_version{}; + std::uint32_t subsystem_version{}; + std::uint32_t win32_version_value{}; + + std::uint32_t size_image{}; + std::uint32_t size_headers{}; + + std::uint32_t checksum{}; + std::uint16_t subsystem{}; + std::uint16_t characteristics{}; + + std::uint32_t size_stack_reserve{}; + std::uint32_t size_stack_commit{}; + std::uint32_t size_heap_reserve{}; + std::uint32_t size_heap_commit{}; + + std::uint32_t ldr_flags{}; + + std::uint32_t num_data_directories{}; + DataDirectory data_directories[16]{}; + }; + template + using OptionalHeader = std::conditional_t; + + struct FileHeader final + { + 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 DosHeader final + { + 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; + }; + + enum class NtArchitecture + { + x32_bit, + x64_bit, + }; + template + struct ImageNtHeaders final + { + std::uint32_t signature; + FileHeader file_header; + OptionalHeader optional_header; + }; + + struct SectionHeader final + { + char name[8]; + union + { + std::uint32_t physical_address; + std::uint32_t virtual_size; + }; + std::uint32_t virtual_address; + + std::uint32_t size_raw_data; + std::uint32_t ptr_raw_data; + + std::uint32_t ptr_relocs; + std::uint32_t ptr_line_numbers; + std::uint32_t num_relocs; + std::uint32_t num_line_numbers; + + std::uint32_t characteristics; + }; + // ReSharper restore CppDeclaratorNeverUsed + + using NtHeaderVariant = + std::variant, ImageNtHeaders>; +} // namespace + +// Internal PE scanner functions +namespace +{ + [[nodiscard]] + std::optional get_nt_header_from_file(std::fstream& file, const DosHeader& dos_header) + { + ImageNtHeaders x86_headers{}; + file.seekg(dos_header.e_lfanew, std::ios::beg); + file.read(reinterpret_cast(&x86_headers), sizeof(x86_headers)); + + if (x86_headers.optional_header.magic == opt_hdr32_magic) + return x86_headers; + + if (x86_headers.optional_header.magic != opt_hdr64_magic) + return std::nullopt; + + ImageNtHeaders x64_headers{}; + file.seekg(dos_header.e_lfanew, std::ios::beg); + file.read(reinterpret_cast(&x64_headers), sizeof(x64_headers)); + + return x64_headers; + } + + [[nodiscard]] + std::optional get_nt_header_from_loaded_module(const void* module_base_address) + { + const auto module_byte_ptr = static_cast(module_base_address); + ImageNtHeaders x86_headers{}; + const auto dos_header = static_cast(module_base_address); + + x86_headers = *reinterpret_cast*>(module_byte_ptr + + dos_header->e_lfanew); + + if (x86_headers.optional_header.magic == opt_hdr32_magic) + return x86_headers; + + if (x86_headers.optional_header.magic != opt_hdr64_magic) + return std::nullopt; + + return *reinterpret_cast*>(module_byte_ptr + + dos_header->e_lfanew); + } + + [[nodiscard]] + constexpr bool invalid_dos_header_file(const DosHeader& dos_header) + { + constexpr std::uint16_t dos_hdr_magic = 0x5A4D; + return dos_header.e_magic != dos_hdr_magic; + } + [[nodiscard]] + constexpr bool invalid_nt_header_file(const NtHeaderVariant& variant) + { + constexpr std::uint32_t nt_hdr_magic = 0x4550; + return std::visit([&nt_hdr_magic](const auto& header) -> bool { return header.signature != nt_hdr_magic; }, + variant); + } + + struct ExtractedSection final + { + std::uintptr_t virtual_base_addr; + std::uintptr_t raw_base_addr; + std::vector data; + }; + + [[nodiscard]] + std::optional extract_section_from_pe_file(const std::filesystem::path& path_to_file, + const std::string_view& section_name) + { + std::fstream file(path_to_file, std::ios::binary | std::ios::in); + + if (!file.is_open()) [[unlikely]] + return std::nullopt; + + DosHeader dos_header{}; + file.read(reinterpret_cast(&dos_header), sizeof(dos_header)); + + if (invalid_dos_header_file(dos_header)) [[unlikely]] + return std::nullopt; + + const auto nt_headers = get_nt_header_from_file(file, dos_header); + + if (!nt_headers) [[unlikely]] + return std::nullopt; + + if (invalid_nt_header_file(nt_headers.value())) [[unlikely]] + return std::nullopt; + + return std::visit( + [&file, &dos_header, §ion_name](auto& concrete_headers) -> std::optional + { + constexpr std::size_t size_of_signature = sizeof(concrete_headers.signature); + const auto offset_to_segment_table = dos_header.e_lfanew + + concrete_headers.file_header.size_optional_header + + sizeof(FileHeader) + size_of_signature; + + file.seekg(static_cast(offset_to_segment_table), std::ios::beg); + + for (std::size_t i = 0; i < concrete_headers.file_header.num_sections; i++) + { + SectionHeader current_section{}; + file.read(reinterpret_cast(¤t_section), sizeof(current_section)); + + if (std::string_view(current_section.name) != section_name) + continue; + + std::vector section_data(current_section.size_raw_data); + + file.seekg(current_section.ptr_raw_data, std::ios::beg); + file.read(reinterpret_cast(section_data.data()), + static_cast(section_data.size())); + return ExtractedSection{ + .virtual_base_addr = static_cast( + current_section.virtual_address + concrete_headers.optional_header.image_base), + .raw_base_addr = static_cast(current_section.ptr_raw_data), + .data = std::move(section_data)}; + } + return std::nullopt; + }, + nt_headers.value()); + } +} // namespace + +namespace omath +{ + + std::optional + PePatternScanner::scan_for_pattern_in_loaded_module(const void* module_base_address, + const std::string_view& pattern, + const std::string_view& target_section_name) + { + const auto base_address = reinterpret_cast(module_base_address); + const auto* base_bytes = static_cast(module_base_address); + + if (!base_address) + return std::nullopt; + + const auto* dos_header = static_cast(module_base_address); + + if (invalid_dos_header_file(*dos_header)) [[unlikely]] + return std::nullopt; + + const auto nt_header_variant = get_nt_header_from_loaded_module(module_base_address); + + if (!nt_header_variant) [[unlikely]] + return std::nullopt; + + return std::visit( + [base_bytes, base_address, lfanew = dos_header->e_lfanew, &target_section_name, + &pattern](const auto& nt_header) -> std::optional + { + constexpr std::size_t signature_size = sizeof(nt_header.signature); + const auto section_table_off = static_cast(lfanew) + signature_size + + sizeof(FileHeader) + nt_header.file_header.size_optional_header; + + const auto* section_table = reinterpret_cast(base_bytes + section_table_off); + + for (std::size_t i = 0; i < nt_header.file_header.num_sections; ++i) + { + const auto* section = section_table + i; + + if (std::string_view{section->name} != target_section_name || section->size_raw_data == 0) + continue; + + const auto section_size = section->virtual_size != 0 + ? static_cast(section->virtual_size) + : static_cast(section->size_raw_data); + + const auto* section_begin = + reinterpret_cast(base_address + section->virtual_address); + const auto scan_range = std::span{section_begin, section_size}; + + const auto result = + PatternScanner::scan_for_pattern(scan_range.begin(), scan_range.end(), pattern); + + if (result != scan_range.end()) + return reinterpret_cast(&*result); + } + + return std::nullopt; + }, + nt_header_variant.value()); + } + std::optional + PePatternScanner::scan_for_pattern_in_file(const std::filesystem::path& path_to_file, + const std::string_view& pattern, + const std::string_view& target_section_name) + { + const auto pe_section = extract_section_from_pe_file(path_to_file, target_section_name); + + if (!pe_section.has_value()) [[unlikely]] + return std::nullopt; + + const auto scan_result = + PatternScanner::scan_for_pattern(pe_section->data.cbegin(), pe_section->data.cend(), pattern); + + if (scan_result == pe_section->data.cend()) + return std::nullopt; + const auto offset = std::distance(pe_section->data.begin(), scan_result); + + return SectionScanResult{.virtual_base_addr = pe_section->virtual_base_addr, + .raw_base_addr = pe_section->raw_base_addr, + .target_offset = offset}; + } +} // namespace omath \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index aaefac72..de4f83bd 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,9 +1,38 @@ enable_testing() -project(unit-tests) +project(unit_tests) -file(GLOB TEST_SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp) include(GoogleTest) -add_executable(unit-tests UnitTestColor.cpp) -target_link_libraries(unit-tests PRIVATE gtest gtest_main uml) \ No newline at end of file +file(GLOB_RECURSE UNIT_TESTS_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp") +add_executable(${PROJECT_NAME} ${UNIT_TESTS_SOURCES}) + +set_target_properties( + ${PROJECT_NAME} + PROPERTIES 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}" + 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() + +if(OMATH_ENABLE_COVERAGE) + include(${CMAKE_SOURCE_DIR}/cmake/Coverage.cmake) + omath_setup_coverage(${PROJECT_NAME}) +endif() + +if(OMATH_ENABLE_VALGRIND) + omath_setup_valgrind(${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() diff --git a/tests/UnitTestColor.cpp b/tests/UnitTestColor.cpp deleted file mode 100644 index 3e45514b..00000000 --- a/tests/UnitTestColor.cpp +++ /dev/null @@ -1,13 +0,0 @@ -#include -#include - -TEST(x,x) -{ - uml::prediction::Target target{.m_origin = {100, 0, 60}, .m_vecVelocity = {0,0, 0}, .m_IsAirborne = false}; - uml::prediction::Projectile proj = {.m_origin = {3,2,1}, .m_velocity = 400, .m_gravityMultiplier= 0.4}; - auto vel = uml::prediction::ProjectilePredictor(400).PredictPointToAim(target, proj); - - auto pitch = proj.m_origin.ViewAngleTo(vel.value()).x; - - // printf("pitch: %f", pitch); -} \ No newline at end of file diff --git a/tests/engines/unit_test_frostbite_engine.cpp b/tests/engines/unit_test_frostbite_engine.cpp new file mode 100644 index 00000000..a10e8c16 --- /dev/null +++ b/tests/engines/unit_test_frostbite_engine.cpp @@ -0,0 +1,236 @@ +// +// Created by Vlad on 10/23/2025. +// +#include +#include +#include +#include +#include +#include + +TEST(unit_test_frostbite_engine, ForwardVector) +{ + const auto forward = omath::frostbite_engine::forward_vector({}); + + EXPECT_EQ(forward, omath::frostbite_engine::k_abs_forward); +} + +TEST(unit_test_frostbite_engine, ForwardVectorRotationYaw) +{ + omath::frostbite_engine::ViewAngles angles; + + angles.yaw = omath::frostbite_engine::YawAngle::from_degrees(90.f); + + const auto forward = omath::frostbite_engine::forward_vector(angles); + EXPECT_NEAR(forward.x, omath::frostbite_engine::k_abs_right.x, 0.00001f); + EXPECT_NEAR(forward.y, omath::frostbite_engine::k_abs_right.y, 0.00001f); + EXPECT_NEAR(forward.z, omath::frostbite_engine::k_abs_right.z, 0.00001f); +} + +TEST(unit_test_frostbite_engine, ForwardVectorRotationPitch) +{ + omath::frostbite_engine::ViewAngles angles; + + angles.pitch = omath::frostbite_engine::PitchAngle::from_degrees(-90.f); + + const auto forward = omath::frostbite_engine::forward_vector(angles); + EXPECT_NEAR(forward.x, omath::frostbite_engine::k_abs_up.x, 0.00001f); + EXPECT_NEAR(forward.y, omath::frostbite_engine::k_abs_up.y, 0.00001f); + EXPECT_NEAR(forward.z, omath::frostbite_engine::k_abs_up.z, 0.00001f); +} + +TEST(unit_test_frostbite_engine, ForwardVectorRotationRoll) +{ + omath::frostbite_engine::ViewAngles angles; + + angles.roll = omath::frostbite_engine::RollAngle::from_degrees(-90.f); + + const auto forward = omath::frostbite_engine::up_vector(angles); + EXPECT_NEAR(forward.x, omath::frostbite_engine::k_abs_right.x, 0.00001f); + EXPECT_NEAR(forward.y, omath::frostbite_engine::k_abs_right.y, 0.00001f); + EXPECT_NEAR(forward.z, omath::frostbite_engine::k_abs_right.z, 0.00001f); +} + +TEST(unit_test_frostbite_engine, RightVector) +{ + const auto right = omath::frostbite_engine::right_vector({}); + + EXPECT_EQ(right, omath::frostbite_engine::k_abs_right); +} + +TEST(unit_test_frostbite_engine, UpVector) +{ + const auto up = omath::frostbite_engine::up_vector({}); + EXPECT_EQ(up, omath::frostbite_engine::k_abs_up); +} + +TEST(unit_test_frostbite_engine, ProjectTargetMovedFromCamera) +{ + constexpr auto fov = omath::projection::FieldOfView::from_degrees(60.f); + const auto cam = omath::frostbite_engine::Camera({0, 0, 0}, {}, {1280.f, 720.f}, fov, 0.01f, 1000.f); + + for (float distance = 0.02f; distance < 100.f; distance += 0.01f) + { + const auto projected = cam.world_to_screen({0, 0, distance}); + + EXPECT_TRUE(projected.has_value()); + + if (!projected.has_value()) + continue; + + EXPECT_NEAR(projected->x, 640, 0.00001f); + EXPECT_NEAR(projected->y, 360, 0.00001f); + } +} +TEST(unit_test_frostbite_engine, Project) +{ + constexpr auto fov = omath::projection::FieldOfView::from_degrees(60.f); + + const auto cam = omath::frostbite_engine::Camera({0, 0, 0}, {}, {1280.f, 720.f}, fov, 0.03f, 1000.f); + const auto proj = cam.world_to_screen({10.f, 3, 10.f}); + + EXPECT_NEAR(proj->x, 1263.538, 0.001f); + EXPECT_NEAR(proj->y, 547.061f, 0.001f); +} + +TEST(unit_test_frostbite_engine, CameraSetAndGetFov) +{ + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::frostbite_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); + + EXPECT_EQ(cam.get_field_of_view().as_degrees(), 90.f); + cam.set_field_of_view(omath::projection::FieldOfView::from_degrees(50.f)); + + EXPECT_EQ(cam.get_field_of_view().as_degrees(), 50.f); +} + +TEST(unit_test_frostbite_engine, CameraSetAndGetOrigin) +{ + auto cam = omath::frostbite_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, {}, 0.01f, 1000.f); + + EXPECT_EQ(cam.get_origin(), omath::Vector3{}); + cam.set_field_of_view(omath::projection::FieldOfView::from_degrees(50.f)); + + EXPECT_EQ(cam.get_field_of_view().as_degrees(), 50.f); +} +TEST(unit_test_frostbite_engine, loook_at_random_all_axis) +{ + std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source + std::uniform_real_distribution dist(-1000.f, 1000.f); + + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::frostbite_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.001f, 10000.f); + + std::size_t failed_points = 0; + + for (int i = 0; i < 1000; i++) + { + const auto position_to_look = omath::Vector3{dist(gen), dist(gen), dist(gen)}; + + if (cam.get_origin().distance_to(position_to_look) < 10) + continue; + + cam.look_at(position_to_look); + + auto projected_pos = cam.world_to_view_port(position_to_look); + + EXPECT_TRUE(projected_pos.has_value()); + + if (!projected_pos) + continue; + + if (std::abs(projected_pos->x - 0.f) >= 0.0001f || std::abs(projected_pos->y - 0.f) >= 0.0001f) + failed_points++; + } + EXPECT_LE(failed_points, 100); +} + +TEST(unit_test_frostbite_engine, loook_at_random_x_axis) +{ + std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source + std::uniform_real_distribution dist(-1000.f, 1000.f); + + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::frostbite_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.001f, 10000.f); + + std::size_t failed_points = 0; + for (int i = 0; i < 1000; i++) + { + const auto position_to_look = omath::Vector3{dist(gen), 0.f, 0.f}; + if (cam.get_origin().distance_to(position_to_look) < 10) + continue; + + cam.look_at(position_to_look); + + auto projected_pos = cam.world_to_view_port(position_to_look); + + EXPECT_TRUE(projected_pos.has_value()); + + if (!projected_pos) + continue; + + if (std::abs(projected_pos->x - 0.f) >= 0.001f || std::abs(projected_pos->y - 0.f) >= 0.001f) + failed_points++; + } + EXPECT_LE(failed_points, 100); +} + +TEST(unit_test_frostbite_engine, loook_at_random_y_axis) +{ + std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source + std::uniform_real_distribution dist(-1000.f, 1000.f); + + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::frostbite_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.001f, 10000.f); + + std::size_t failed_points = 0; + for (int i = 0; i < 1000; i++) + { + const auto position_to_look = omath::Vector3{0.f, dist(gen), 0.f}; + if (cam.get_origin().distance_to(position_to_look) < 10) + continue; + + cam.look_at(position_to_look); + + auto projected_pos = cam.world_to_view_port(position_to_look); + + EXPECT_TRUE(projected_pos.has_value()); + + if (!projected_pos) + continue; + + if (std::abs(projected_pos->x - 0.f) >= 0.01f || std::abs(projected_pos->y - 0.f) >= 0.01f) + failed_points++; + } + EXPECT_LE(failed_points, 100); +} + +TEST(unit_test_frostbite_engine, loook_at_random_z_axis) +{ + std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source + std::uniform_real_distribution dist(-1000.f, 1000.f); + + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::frostbite_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.001f, 10000.f); + + std::size_t failed_points = 0; + for (int i = 0; i < 1000; i++) + { + const auto position_to_look = omath::Vector3{0.f, 0.f, dist(gen)}; + if (cam.get_origin().distance_to(position_to_look) < 10) + continue; + + cam.look_at(position_to_look); + + auto projected_pos = cam.world_to_view_port(position_to_look); + + EXPECT_TRUE(projected_pos.has_value()); + + if (!projected_pos) + continue; + + if (std::abs(projected_pos->x - 0.f) >= 0.01f || std::abs(projected_pos->y - 0.f) >= 0.01f) + failed_points++; + } + EXPECT_LE(failed_points, 100); +} \ No newline at end of file diff --git a/tests/engines/unit_test_iw_engine.cpp b/tests/engines/unit_test_iw_engine.cpp new file mode 100644 index 00000000..ed274578 --- /dev/null +++ b/tests/engines/unit_test_iw_engine.cpp @@ -0,0 +1,226 @@ +// +// Created by Vlad on 3/17/2025. +// +#include +#include +#include +#include +#include + +TEST(unit_test_iw_engine, ForwardVector) +{ + const auto forward = omath::iw_engine::forward_vector({}); + + EXPECT_EQ(forward, omath::iw_engine::k_abs_forward); +} + +TEST(unit_test_iw_engine, RightVector) +{ + const auto right = omath::iw_engine::right_vector({}); + + EXPECT_EQ(right, omath::iw_engine::k_abs_right); +} + +TEST(unit_test_iw_engine, UpVector) +{ + const auto up = omath::iw_engine::up_vector({}); + EXPECT_EQ(up, omath::iw_engine::k_abs_up); +} + +TEST(unit_test_iw_engine, ForwardVectorRotationYaw) +{ + omath::iw_engine::ViewAngles angles; + + angles.yaw = omath::iw_engine::YawAngle::from_degrees(-90.f); + + const auto forward = omath::iw_engine::forward_vector(angles); + EXPECT_NEAR(forward.x, omath::iw_engine::k_abs_right.x, 0.00001f); + EXPECT_NEAR(forward.y, omath::iw_engine::k_abs_right.y, 0.00001f); + EXPECT_NEAR(forward.z, omath::iw_engine::k_abs_right.z, 0.00001f); +} + +TEST(unit_test_iw_engine, ForwardVectorRotationPitch) +{ + omath::iw_engine::ViewAngles angles; + + angles.pitch = omath::iw_engine::PitchAngle::from_degrees(-89.f); + + const auto forward = omath::iw_engine::forward_vector(angles); + EXPECT_NEAR(forward.x, omath::iw_engine::k_abs_up.x, 0.02f); + EXPECT_NEAR(forward.y, omath::iw_engine::k_abs_up.y, 0.01f); + EXPECT_NEAR(forward.z, omath::iw_engine::k_abs_up.z, 0.01f); +} + +TEST(unit_test_iw_engine, ForwardVectorRotationRoll) +{ + omath::iw_engine::ViewAngles angles; + + angles.roll = omath::iw_engine::RollAngle::from_degrees(90.f); + + const auto forward = omath::iw_engine::up_vector(angles); + EXPECT_NEAR(forward.x, omath::iw_engine::k_abs_right.x, 0.00001f); + EXPECT_NEAR(forward.y, omath::iw_engine::k_abs_right.y, 0.00001f); + EXPECT_NEAR(forward.z, omath::iw_engine::k_abs_right.z, 0.00001f); +} + +TEST(unit_test_iw_engine, ProjectTargetMovedFromCamera) +{ + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + const auto cam = omath::iw_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); + + for (float distance = 0.02f; distance < 1000.f; distance += 0.01f) + { + const auto projected = cam.world_to_screen({distance, 0, 0}); + + EXPECT_TRUE(projected.has_value()); + + if (!projected.has_value()) + continue; + + EXPECT_NEAR(projected->x, 960, 0.00001f); + EXPECT_NEAR(projected->y, 540, 0.00001f); + } +} + +TEST(unit_test_iw_engine, CameraSetAndGetFov) +{ + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::iw_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); + + EXPECT_EQ(cam.get_field_of_view().as_degrees(), 90.f); + cam.set_field_of_view(omath::projection::FieldOfView::from_degrees(50.f)); + + EXPECT_EQ(cam.get_field_of_view().as_degrees(), 50.f); +} + +TEST(unit_test_iw_engine, CameraSetAndGetOrigin) +{ + auto cam = omath::iw_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, {}, 0.01f, 1000.f); + + EXPECT_EQ(cam.get_origin(), omath::Vector3{}); + cam.set_field_of_view(omath::projection::FieldOfView::from_degrees(50.f)); + + EXPECT_EQ(cam.get_field_of_view().as_degrees(), 50.f); +} + +TEST(unit_test_iw_engine, loook_at_random_all_axis) +{ + std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source + std::uniform_real_distribution dist(-1000.f, 1000.f); + + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::iw_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.001f, 10000.f); + + std::size_t failed_points = 0; + for (int i = 0; i < 1000; i++) + { + const auto position_to_look = omath::Vector3{dist(gen), dist(gen), dist(gen)}; + + if (cam.get_origin().distance_to(position_to_look) < 10) + continue; + + cam.look_at(position_to_look); + + auto projected_pos = cam.world_to_view_port(position_to_look); + + EXPECT_TRUE(projected_pos.has_value()); + + if (!projected_pos) + continue; + + if (std::abs(projected_pos->x - 0.f) >= 0.001f || std::abs(projected_pos->y - 0.f) >= 0.001f) + failed_points++; + } + EXPECT_LE(failed_points, 100); +} + +TEST(unit_test_iw_engine, loook_at_random_x_axis) +{ + std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source + std::uniform_real_distribution dist(-1000.f, 1000.f); + + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::iw_engine::Camera({dist(gen), dist(gen), dist(gen)}, {}, {1920.f, 1080.f}, fov, 0.001f, 10000.f); + + std::size_t failed_points = 0; + for (int i = 0; i < 1000; i++) + { + const auto position_to_look = omath::Vector3{dist(gen), 0.f, 0.f}; + + if (cam.get_origin().distance_to(position_to_look) < 10) + continue; + + cam.look_at(position_to_look); + + auto projected_pos = cam.world_to_view_port(position_to_look); + + EXPECT_TRUE(projected_pos.has_value()); + + if (!projected_pos) + continue; + + if (std::abs(projected_pos->x - 0.f) >= 0.01f || std::abs(projected_pos->y - 0.f) >= 0.01f) + failed_points++; + } + EXPECT_LE(failed_points, 100); +} + +TEST(unit_test_iw_engine, loook_at_random_y_axis) +{ + std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source + std::uniform_real_distribution dist(-1000.f, 1000.f); + + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::iw_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.001f, 10000.f); + + std::size_t failed_points = 0; + for (int i = 0; i < 1000; i++) + { + const auto position_to_look = omath::Vector3{0.f, dist(gen), 0.f}; + if (cam.get_origin().distance_to(position_to_look) < 10) + continue; + + cam.look_at(position_to_look); + + auto projected_pos = cam.world_to_view_port(position_to_look); + + EXPECT_TRUE(projected_pos.has_value()); + + if (!projected_pos) + continue; + + if (std::abs(projected_pos->x - 0.f) >= 0.01f || std::abs(projected_pos->y - 0.f) >= 0.01f) + failed_points++; + } + EXPECT_LE(failed_points, 100); +} + +TEST(unit_test_iw_engine, loook_at_random_z_axis) +{ + std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source + std::uniform_real_distribution dist(-1000.f, 1000.f); + + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::iw_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.001f, 10000.f); + + std::size_t failed_points = 0; + for (int i = 0; i < 1000; i++) + { + const auto position_to_look = omath::Vector3{0.f, 0.f, dist(gen)}; + if (cam.get_origin().distance_to(position_to_look) < 10) + continue; + + cam.look_at(position_to_look); + + auto projected_pos = cam.world_to_view_port(position_to_look); + + EXPECT_TRUE(projected_pos.has_value()); + + if (!projected_pos) + continue; + + if (std::abs(projected_pos->x - 0.f) >= 0.01f || std::abs(projected_pos->y - 0.f) >= 0.025f) + failed_points++; + } + EXPECT_LE(failed_points, 100); +} \ No newline at end of file diff --git a/tests/engines/unit_test_open_gl.cpp b/tests/engines/unit_test_open_gl.cpp new file mode 100644 index 00000000..48b808bf --- /dev/null +++ b/tests/engines/unit_test_open_gl.cpp @@ -0,0 +1,221 @@ +// +// Created by Orange on 11/23/2024. +// +#include +#include +#include +#include +#include + +TEST(unit_test_opengl, ForwardVector) +{ + const auto forward = omath::opengl_engine::forward_vector({}); + EXPECT_EQ(forward, omath::opengl_engine::k_abs_forward); +} + +TEST(unit_test_opengl, RightVector) +{ + const auto right = omath::opengl_engine::right_vector({}); + EXPECT_EQ(right, omath::opengl_engine::k_abs_right); +} + +TEST(unit_test_opengl, UpVector) +{ + const auto up = omath::opengl_engine::up_vector({}); + EXPECT_EQ(up, omath::opengl_engine::k_abs_up); +} + +TEST(unit_test_opengl, ForwardVectorRotationYaw) +{ + omath::opengl_engine::ViewAngles angles; + + angles.yaw = omath::opengl_engine::YawAngle::from_degrees(-90.f); + + const auto forward = omath::opengl_engine::forward_vector(angles); + EXPECT_NEAR(forward.x, omath::opengl_engine::k_abs_right.x, 0.00001f); + EXPECT_NEAR(forward.y, omath::opengl_engine::k_abs_right.y, 0.00001f); + EXPECT_NEAR(forward.z, omath::opengl_engine::k_abs_right.z, 0.00001f); +} + +TEST(unit_test_opengl, ForwardVectorRotationPitch) +{ + omath::opengl_engine::ViewAngles angles; + + angles.pitch = omath::opengl_engine::PitchAngle::from_degrees(90.f); + + const auto forward = omath::opengl_engine::forward_vector(angles); + EXPECT_NEAR(forward.x, omath::opengl_engine::k_abs_up.x, 0.00001f); + EXPECT_NEAR(forward.y, omath::opengl_engine::k_abs_up.y, 0.00001f); + EXPECT_NEAR(forward.z, omath::opengl_engine::k_abs_up.z, 0.00001f); +} + +TEST(unit_test_opengl, ForwardVectorRotationRoll) +{ + omath::opengl_engine::ViewAngles angles; + + angles.roll = omath::opengl_engine::RollAngle::from_degrees(-90.f); + + const auto forward = omath::opengl_engine::up_vector(angles); + EXPECT_NEAR(forward.x, omath::opengl_engine::k_abs_right.x, 0.00001f); + EXPECT_NEAR(forward.y, omath::opengl_engine::k_abs_right.y, 0.00001f); + EXPECT_NEAR(forward.z, omath::opengl_engine::k_abs_right.z, 0.00001f); +} + +TEST(unit_test_opengl, ProjectTargetMovedFromCamera) +{ + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + const auto cam = omath::opengl_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); + + for (float distance = -10.f; distance > -1000.f; distance -= 0.01f) + { + const auto projected = cam.world_to_screen({0, 0, distance}); + + EXPECT_TRUE(projected.has_value()); + + if (!projected.has_value()) + continue; + + EXPECT_NEAR(projected->x, 960, 0.00001f); + EXPECT_NEAR(projected->y, 540, 0.00001f); + } +} + +TEST(unit_test_opengl, CameraSetAndGetFov) +{ + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::opengl_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); + + EXPECT_EQ(cam.get_field_of_view().as_degrees(), 90.f); + cam.set_field_of_view(omath::projection::FieldOfView::from_degrees(50.f)); + + EXPECT_EQ(cam.get_field_of_view().as_degrees(), 50.f); +} + +TEST(unit_test_opengl, CameraSetAndGetOrigin) +{ + auto cam = omath::opengl_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, {}, 0.01f, 1000.f); + + EXPECT_EQ(cam.get_origin(), omath::Vector3{}); + cam.set_field_of_view(omath::projection::FieldOfView::from_degrees(50.f)); + + EXPECT_EQ(cam.get_field_of_view().as_degrees(), 50.f); +} +TEST(unit_test_opengl_engine, loook_at_random_all_axis) +{ + std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source + std::uniform_real_distribution dist(-1000.f, 1000.f); + + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::opengl_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.001f, 10000.f); + + std::size_t failed_points = 0; + for (int i = 0; i < 1000; i++) + { + const auto position_to_look = omath::Vector3{dist(gen), dist(gen), dist(gen)}; + + if (cam.get_origin().distance_to(position_to_look) < 10) + continue; + + cam.look_at(position_to_look); + + auto projected_pos = cam.world_to_view_port(position_to_look); + + EXPECT_TRUE(projected_pos.has_value()); + + if (!projected_pos) + continue; + + if (std::abs(projected_pos->x - 0.f) >= 0.0001f || std::abs(projected_pos->y - 0.f) >= 0.0001f) + failed_points++; + } + EXPECT_LE(failed_points, 100); +} + +TEST(unit_test_opengl_engine, loook_at_random_x_axis) +{ + std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source + std::uniform_real_distribution dist(-1000.f, 1000.f); + + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::opengl_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.001f, 10000.f); + std::size_t failed_points = 0; + for (int i = 0; i < 1000; i++) + { + const auto position_to_look = omath::Vector3{dist(gen), 0.f, 0.f}; + + if (cam.get_origin().distance_to(position_to_look) < 10) + continue; + + cam.look_at(position_to_look); + + auto projected_pos = cam.world_to_view_port(position_to_look); + + EXPECT_TRUE(projected_pos.has_value()); + + if (!projected_pos) + continue; + + if (std::abs(projected_pos->x - 0.f) >= 0.01f || std::abs(projected_pos->y - 0.f) >= 0.01f) + failed_points++; + } + EXPECT_LE(failed_points, 100); +} + +TEST(unit_test_opengl_engine, loook_at_random_y_axis) +{ + std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source + std::uniform_real_distribution dist(-1000.f, 1000.f); + + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::opengl_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.001f, 10000.f); + std::size_t failed_points = 0; + for (int i = 0; i < 1000; i++) + { + const auto position_to_look = omath::Vector3{0.f, dist(gen), 0.f}; + + if (cam.get_origin().distance_to(position_to_look) < 10) + continue; + + cam.look_at(position_to_look); + + auto projected_pos = cam.world_to_view_port(position_to_look); + + EXPECT_TRUE(projected_pos.has_value()); + + if (!projected_pos) + continue; + + if (std::abs(projected_pos->x - 0.f) >= 0.01f || std::abs(projected_pos->y - 0.f) >= 0.01f) + failed_points++; + } + EXPECT_LE(failed_points, 100); +} + +TEST(unit_test_opengl_engine, loook_at_random_z_axis) +{ + std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source + std::uniform_real_distribution dist(-1000.f, 1000.f); + + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::opengl_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.001f, 10000.f); + std::size_t failed_points = 0; + for (int i = 0; i < 1000; i++) + { + const auto position_to_look = omath::Vector3{0.f, 0.f, dist(gen)}; + + if (cam.get_origin().distance_to(position_to_look) < 10) + continue; + cam.look_at(position_to_look); + + auto projected_pos = cam.world_to_view_port(position_to_look); + + EXPECT_TRUE(projected_pos.has_value()); + + if (!projected_pos) + continue; + + if (std::abs(projected_pos->x - 0.f) >= 0.01f || std::abs(projected_pos->y - 0.f) >= 0.01f) + failed_points++; + } + EXPECT_LE(failed_points, 100); +} \ No newline at end of file diff --git a/tests/engines/unit_test_source_engine.cpp b/tests/engines/unit_test_source_engine.cpp new file mode 100644 index 00000000..ac480e46 --- /dev/null +++ b/tests/engines/unit_test_source_engine.cpp @@ -0,0 +1,245 @@ +// +// Created by Orange on 11/23/2024. +// +#include +#include +#include +#include +#include + +TEST(unit_test_source_engine, ForwardVector) +{ + const auto forward = omath::source_engine::forward_vector({}); + + EXPECT_EQ(forward, omath::source_engine::k_abs_forward); +} + +TEST(unit_test_source_engine, RightVector) +{ + const auto right = omath::source_engine::right_vector({}); + + EXPECT_EQ(right, omath::source_engine::k_abs_right); +} + +TEST(unit_test_source_engine, UpVector) +{ + const auto up = omath::source_engine::up_vector({}); + EXPECT_EQ(up, omath::source_engine::k_abs_up); +} + +TEST(unit_test_source_engine, ForwardVectorRotationYaw) +{ + omath::source_engine::ViewAngles angles; + + angles.yaw = omath::source_engine::YawAngle::from_degrees(-90.f); + + const auto forward = omath::source_engine::forward_vector(angles); + EXPECT_NEAR(forward.x, omath::source_engine::k_abs_right.x, 0.00001f); + EXPECT_NEAR(forward.y, omath::source_engine::k_abs_right.y, 0.00001f); + EXPECT_NEAR(forward.z, omath::source_engine::k_abs_right.z, 0.00001f); +} + +TEST(unit_test_source_engine, ForwardVectorRotationPitch) +{ + omath::source_engine::ViewAngles angles; + + angles.pitch = omath::source_engine::PitchAngle::from_degrees(-89.f); + + const auto forward = omath::source_engine::forward_vector(angles); + EXPECT_NEAR(forward.x, omath::source_engine::k_abs_up.x, 0.02f); + EXPECT_NEAR(forward.y, omath::source_engine::k_abs_up.y, 0.01f); + EXPECT_NEAR(forward.z, omath::source_engine::k_abs_up.z, 0.01f); +} + +TEST(unit_test_source_engine, ForwardVectorRotationRoll) +{ + omath::source_engine::ViewAngles angles; + + angles.roll = omath::source_engine::RollAngle::from_degrees(90.f); + + const auto forward = omath::source_engine::up_vector(angles); + EXPECT_NEAR(forward.x, omath::source_engine::k_abs_right.x, 0.00001f); + EXPECT_NEAR(forward.y, omath::source_engine::k_abs_right.y, 0.00001f); + EXPECT_NEAR(forward.z, omath::source_engine::k_abs_right.z, 0.00001f); +} + +TEST(unit_test_source_engine, ProjectTargetMovedFromCamera) +{ + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + const auto cam = omath::source_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); + + for (float distance = 0.02f; distance < 1000.f; distance += 0.01f) + { + const auto projected = cam.world_to_screen({distance, 0, 0}); + + EXPECT_TRUE(projected.has_value()); + + if (!projected.has_value()) + continue; + + EXPECT_NEAR(projected->x, 960, 0.00001f); + EXPECT_NEAR(projected->y, 540, 0.00001f); + } +} + +TEST(unit_test_source_engine, ProjectTargetMovedUp) +{ + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + const auto cam = omath::source_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); + + auto prev = 1080.f; + for (float distance = 0.0f; distance < 10.f; distance += 1.f) + { + const auto projected = cam.world_to_screen({100.f, 0, distance}); + EXPECT_TRUE(projected.has_value()); + + if (!projected.has_value()) + continue; + + EXPECT_TRUE(projected->y < prev); + + prev = projected->y; + } +} + +TEST(unit_test_source_engine, CameraSetAndGetFov) +{ + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::source_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); + + EXPECT_EQ(cam.get_field_of_view().as_degrees(), 90.f); + cam.set_field_of_view(omath::projection::FieldOfView::from_degrees(50.f)); + + EXPECT_EQ(cam.get_field_of_view().as_degrees(), 50.f); +} + +TEST(unit_test_source_engine, CameraSetAndGetOrigin) +{ + auto cam = omath::source_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, {}, 0.01f, 1000.f); + + EXPECT_EQ(cam.get_origin(), omath::Vector3{}); + cam.set_field_of_view(omath::projection::FieldOfView::from_degrees(50.f)); + + EXPECT_EQ(cam.get_field_of_view().as_degrees(), 50.f); +} + +TEST(unit_test_source_engine, loook_at_random_all_axis) +{ + std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source + std::uniform_real_distribution dist(-1000.f, 1000.f); + + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::source_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.001f, 10000.f); + + std::size_t failed_points = 0; + for (int i = 0; i < 1000; i++) + { + const auto position_to_look = omath::Vector3{dist(gen), dist(gen), dist(gen)}; + + if (cam.get_origin().distance_to(position_to_look) < 10) + continue; + + cam.look_at(position_to_look); + + auto projected_pos = cam.world_to_view_port(position_to_look); + + EXPECT_TRUE(projected_pos.has_value()); + + if (!projected_pos) + continue; + + if (std::abs(projected_pos->x - 0.f) >= 0.0001f || std::abs(projected_pos->y - 0.f) >= 0.0001f) + failed_points++; + } + EXPECT_LE(failed_points, 100); +} + +TEST(unit_test_source_engine, loook_at_random_x_axis) +{ + std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source + std::uniform_real_distribution dist(-1000.f, 1000.f); + + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::source_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.001f, 10000.f); + + std::size_t failed_points = 0; + for (int i = 0; i < 1000; i++) + { + const auto position_to_look = omath::Vector3{dist(gen), 0.f, 0.f}; + + if (cam.get_origin().distance_to(position_to_look) < 10) + continue; + cam.look_at(position_to_look); + + auto projected_pos = cam.world_to_view_port(position_to_look); + + EXPECT_TRUE(projected_pos.has_value()); + + if (!projected_pos) + continue; + + if (std::abs(projected_pos->x - 0.f) >= 0.01f || std::abs(projected_pos->y - 0.f) >= 0.01f) + failed_points++; + } + EXPECT_LE(failed_points, 100); +} + +TEST(unit_test_source_engine, loook_at_random_y_axis) +{ + std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source + std::uniform_real_distribution dist(-1000.f, 1000.f); + + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::source_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.001f, 10000.f); + + std::size_t failed_points = 0; + for (int i = 0; i < 1000; i++) + { + const auto position_to_look = omath::Vector3{0.f, dist(gen), 0.f}; + + if (cam.get_origin().distance_to(position_to_look) < 10) + continue; + cam.look_at(position_to_look); + + auto projected_pos = cam.world_to_view_port(position_to_look); + + EXPECT_TRUE(projected_pos.has_value()); + + if (!projected_pos) + continue; + + if (std::abs(projected_pos->x - 0.f) >= 0.01f || std::abs(projected_pos->y - 0.f) >= 0.01f) + failed_points++; + } + EXPECT_LE(failed_points, 100); +} + +TEST(unit_test_source_engine, loook_at_random_z_axis) +{ + std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source + std::uniform_real_distribution dist(-1000.f, 1000.f); + + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::source_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.001f, 10000.f); + + std::size_t failed_points = 0; + for (int i = 0; i < 1000; i++) + { + const auto position_to_look = omath::Vector3{0.f, 0.f, dist(gen)}; + + if (cam.get_origin().distance_to(position_to_look) < 10) + continue; + cam.look_at(position_to_look); + + auto projected_pos = cam.world_to_view_port(position_to_look); + + EXPECT_TRUE(projected_pos.has_value()); + + if (!projected_pos) + continue; + + if (std::abs(projected_pos->x - 0.f) >= 0.01f || std::abs(projected_pos->y - 0.f) >= 0.025f) + failed_points++; + } + EXPECT_LE(failed_points, 100); +} \ No newline at end of file diff --git a/tests/engines/unit_test_traits_engines.cpp b/tests/engines/unit_test_traits_engines.cpp new file mode 100644 index 00000000..f891a3e3 --- /dev/null +++ b/tests/engines/unit_test_traits_engines.cpp @@ -0,0 +1,297 @@ +// Tests for engine trait headers to improve header coverage +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +using namespace omath; + +// Small helper to compare matrices roughly (templated to avoid concrete typedef) +template +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 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 origin{0.f, 0.f, 0.f}; + Vector3 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 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 origin{0.f, 0.f, 0.f}; + Vector3 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 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 origin{0.f, 0.f, 0.f}; + Vector3 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 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 origin{0.f, 0.f, 0.f}; + Vector3 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 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 origin{0.f, 0.f, 0.f}; + Vector3 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); +} diff --git a/tests/engines/unit_test_unity_engine.cpp b/tests/engines/unit_test_unity_engine.cpp new file mode 100644 index 00000000..46735cc4 --- /dev/null +++ b/tests/engines/unit_test_unity_engine.cpp @@ -0,0 +1,236 @@ +// +// Created by Orange on 11/27/2024. +// +#include +#include +#include +#include +#include +#include + +TEST(unit_test_unity_engine, ForwardVector) +{ + const auto forward = omath::unity_engine::forward_vector({}); + + EXPECT_EQ(forward, omath::unity_engine::k_abs_forward); +} + +TEST(unit_test_unity_engine, ForwardVectorRotationYaw) +{ + omath::unity_engine::ViewAngles angles; + + angles.yaw = omath::unity_engine::YawAngle::from_degrees(90.f); + + const auto forward = omath::unity_engine::forward_vector(angles); + EXPECT_NEAR(forward.x, omath::unity_engine::k_abs_right.x, 0.00001f); + EXPECT_NEAR(forward.y, omath::unity_engine::k_abs_right.y, 0.00001f); + EXPECT_NEAR(forward.z, omath::unity_engine::k_abs_right.z, 0.00001f); +} + +TEST(unit_test_unity_engine, ForwardVectorRotationPitch) +{ + omath::unity_engine::ViewAngles angles; + + angles.pitch = omath::unity_engine::PitchAngle::from_degrees(-90.f); + + const auto forward = omath::unity_engine::forward_vector(angles); + EXPECT_NEAR(forward.x, omath::unity_engine::k_abs_up.x, 0.00001f); + EXPECT_NEAR(forward.y, omath::unity_engine::k_abs_up.y, 0.00001f); + EXPECT_NEAR(forward.z, omath::unity_engine::k_abs_up.z, 0.00001f); +} + +TEST(unit_test_unity_engine, ForwardVectorRotationRoll) +{ + omath::unity_engine::ViewAngles angles; + + angles.roll = omath::unity_engine::RollAngle::from_degrees(-90.f); + + const auto forward = omath::unity_engine::up_vector(angles); + EXPECT_NEAR(forward.x, omath::unity_engine::k_abs_right.x, 0.00001f); + EXPECT_NEAR(forward.y, omath::unity_engine::k_abs_right.y, 0.00001f); + EXPECT_NEAR(forward.z, omath::unity_engine::k_abs_right.z, 0.00001f); +} + +TEST(unit_test_unity_engine, RightVector) +{ + const auto right = omath::unity_engine::right_vector({}); + + EXPECT_EQ(right, omath::unity_engine::k_abs_right); +} + +TEST(unit_test_unity_engine, UpVector) +{ + const auto up = omath::unity_engine::up_vector({}); + EXPECT_EQ(up, omath::unity_engine::k_abs_up); +} + +TEST(unit_test_unity_engine, ProjectTargetMovedFromCamera) +{ + constexpr auto fov = omath::projection::FieldOfView::from_degrees(60.f); + const auto cam = omath::unity_engine::Camera({0, 0, 0}, {}, {1280.f, 720.f}, fov, 0.01f, 1000.f); + + for (float distance = 0.02f; distance < 100.f; distance += 0.01f) + { + const auto projected = cam.world_to_screen({0, 0, distance}); + + EXPECT_TRUE(projected.has_value()); + + if (!projected.has_value()) + continue; + + EXPECT_NEAR(projected->x, 640, 0.00001f); + EXPECT_NEAR(projected->y, 360, 0.00001f); + } +} +TEST(unit_test_unity_engine, Project) +{ + constexpr auto fov = omath::projection::FieldOfView::from_degrees(60.f); + + const auto cam = omath::unity_engine::Camera({0, 0, 0}, {}, {1280.f, 720.f}, fov, 0.03f, 1000.f); + const auto proj = cam.world_to_screen({10.f, 3, 10.f}); + + EXPECT_NEAR(proj->x, 1263.538, 0.001f); + EXPECT_NEAR(proj->y, 547.061f, 0.001f); +} + +TEST(unit_test_unity_engine, CameraSetAndGetFov) +{ + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::unity_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); + + EXPECT_EQ(cam.get_field_of_view().as_degrees(), 90.f); + cam.set_field_of_view(omath::projection::FieldOfView::from_degrees(50.f)); + + EXPECT_EQ(cam.get_field_of_view().as_degrees(), 50.f); +} + +TEST(unit_test_unity_engine, CameraSetAndGetOrigin) +{ + auto cam = omath::unity_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, {}, 0.01f, 1000.f); + + EXPECT_EQ(cam.get_origin(), omath::Vector3{}); + cam.set_field_of_view(omath::projection::FieldOfView::from_degrees(50.f)); + + EXPECT_EQ(cam.get_field_of_view().as_degrees(), 50.f); +} +TEST(unit_test_unity_engine, loook_at_random_all_axis) +{ + std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source + std::uniform_real_distribution dist(-1000.f, 1000.f); + + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::unity_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.001f, 10000.f); + + std::size_t failed_points = 0; + + for (int i = 0; i < 1000; i++) + { + const auto position_to_look = omath::Vector3{dist(gen), dist(gen), dist(gen)}; + + if (cam.get_origin().distance_to(position_to_look) < 10) + continue; + + cam.look_at(position_to_look); + + auto projected_pos = cam.world_to_view_port(position_to_look); + + EXPECT_TRUE(projected_pos.has_value()); + + if (!projected_pos) + continue; + + if (std::abs(projected_pos->x - 0.f) >= 0.0001f || std::abs(projected_pos->y - 0.f) >= 0.0001f) + failed_points++; + } + EXPECT_LE(failed_points, 100); +} + +TEST(unit_test_unity_engine, loook_at_random_x_axis) +{ + std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source + std::uniform_real_distribution dist(-1000.f, 1000.f); + + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::unity_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.001f, 10000.f); + + std::size_t failed_points = 0; + for (int i = 0; i < 1000; i++) + { + const auto position_to_look = omath::Vector3{dist(gen), 0.f, 0.f}; + if (cam.get_origin().distance_to(position_to_look) < 10) + continue; + + cam.look_at(position_to_look); + + auto projected_pos = cam.world_to_view_port(position_to_look); + + EXPECT_TRUE(projected_pos.has_value()); + + if (!projected_pos) + continue; + + if (std::abs(projected_pos->x - 0.f) >= 0.001f || std::abs(projected_pos->y - 0.f) >= 0.001f) + failed_points++; + } + EXPECT_LE(failed_points, 100); +} + +TEST(unit_test_unity_engine, loook_at_random_y_axis) +{ + std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source + std::uniform_real_distribution dist(-1000.f, 1000.f); + + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::unity_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.001f, 10000.f); + + std::size_t failed_points = 0; + for (int i = 0; i < 1000; i++) + { + const auto position_to_look = omath::Vector3{0.f, dist(gen), 0.f}; + if (cam.get_origin().distance_to(position_to_look) < 10) + continue; + + cam.look_at(position_to_look); + + auto projected_pos = cam.world_to_view_port(position_to_look); + + EXPECT_TRUE(projected_pos.has_value()); + + if (!projected_pos) + continue; + + if (std::abs(projected_pos->x - 0.f) >= 0.01f || std::abs(projected_pos->y - 0.f) >= 0.01f) + failed_points++; + } + EXPECT_LE(failed_points, 100); +} + +TEST(unit_test_unity_engine, loook_at_random_z_axis) +{ + std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source + std::uniform_real_distribution dist(-1000.f, 1000.f); + + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::unity_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.001f, 10000.f); + + std::size_t failed_points = 0; + for (int i = 0; i < 1000; i++) + { + const auto position_to_look = omath::Vector3{0.f, 0.f, dist(gen)}; + if (cam.get_origin().distance_to(position_to_look) < 10) + continue; + + cam.look_at(position_to_look); + + auto projected_pos = cam.world_to_view_port(position_to_look); + + EXPECT_TRUE(projected_pos.has_value()); + + if (!projected_pos) + continue; + + if (std::abs(projected_pos->x - 0.f) >= 0.01f || std::abs(projected_pos->y - 0.f) >= 0.01f) + failed_points++; + } + EXPECT_LE(failed_points, 100); +} \ No newline at end of file diff --git a/tests/engines/unit_test_unreal_engine.cpp b/tests/engines/unit_test_unreal_engine.cpp new file mode 100644 index 00000000..6b2f499d --- /dev/null +++ b/tests/engines/unit_test_unreal_engine.cpp @@ -0,0 +1,241 @@ +// +// Created by Orange on 11/27/2024. +// +#include +#include +#include +#include +#include +#include + +TEST(unit_test_unreal_engine, ForwardVector) +{ + const auto forward = omath::unreal_engine::forward_vector({}); + + EXPECT_EQ(forward, omath::unreal_engine::k_abs_forward); +} + +TEST(unit_test_unreal_engine, ForwardVectorRotationYaw) +{ + omath::unreal_engine::ViewAngles angles; + + angles.yaw = omath::unreal_engine::YawAngle::from_degrees(90.f); + + const auto forward = omath::unreal_engine::forward_vector(angles); + EXPECT_NEAR(forward.x, omath::unreal_engine::k_abs_right.x, 0.00001f); + EXPECT_NEAR(forward.y, omath::unreal_engine::k_abs_right.y, 0.00001f); + EXPECT_NEAR(forward.z, omath::unreal_engine::k_abs_right.z, 0.00001f); +} + +TEST(unit_test_unreal_engine, ForwardVectorRotationPitch) +{ + omath::unreal_engine::ViewAngles angles; + + angles.pitch = omath::unreal_engine::PitchAngle::from_degrees(-90.f); + + const auto forward = omath::unreal_engine::forward_vector(angles); + EXPECT_NEAR(forward.x, omath::unreal_engine::k_abs_up.x, 0.00001f); + EXPECT_NEAR(forward.y, omath::unreal_engine::k_abs_up.y, 0.00001f); + EXPECT_NEAR(forward.z, omath::unreal_engine::k_abs_up.z, 0.00001f); +} + +TEST(unit_test_unreal_engine, ForwardVectorRotationRoll) +{ + omath::unreal_engine::ViewAngles angles; + + angles.roll = omath::unreal_engine::RollAngle::from_degrees(-90.f); + + const auto forward = omath::unreal_engine::up_vector(angles); + EXPECT_NEAR(forward.x, omath::unreal_engine::k_abs_right.x, 0.00001f); + EXPECT_NEAR(forward.y, omath::unreal_engine::k_abs_right.y, 0.00001f); + EXPECT_NEAR(forward.z, omath::unreal_engine::k_abs_right.z, 0.00001f); +} + +TEST(unit_test_unreal_engine, RightVector) +{ + const auto right = omath::unreal_engine::right_vector({}); + + EXPECT_EQ(right, omath::unreal_engine::k_abs_right); +} + +TEST(unit_test_unreal_engine, UpVector) +{ + const auto up = omath::unreal_engine::up_vector({}); + EXPECT_EQ(up, omath::unreal_engine::k_abs_up); +} + +TEST(unit_test_unreal_engine, ProjectTargetMovedFromCamera) +{ + 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, 1000.f); + + for (float distance = 0.02f; distance < 100.f; distance += 0.01f) + { + const auto projected = cam.world_to_screen({distance, 0, 0}); + + EXPECT_TRUE(projected.has_value()); + + if (!projected.has_value()) + continue; + + EXPECT_NEAR(projected->x, 640, 0.00001f); + EXPECT_NEAR(projected->y, 360, 0.00001f); + } +} +TEST(unit_test_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) +{ + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::unreal_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.01f, 1000.f); + + EXPECT_EQ(cam.get_field_of_view().as_degrees(), 90.f); + cam.set_field_of_view(omath::projection::FieldOfView::from_degrees(50.f)); + + EXPECT_EQ(cam.get_field_of_view().as_degrees(), 50.f); +} + +TEST(unit_test_unreal_engine, CameraSetAndGetOrigin) +{ + auto cam = omath::unreal_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, {}, 0.01f, 1000.f); + + EXPECT_EQ(cam.get_origin(), omath::Vector3{}); + cam.set_field_of_view(omath::projection::FieldOfView::from_degrees(50.f)); + + EXPECT_EQ(cam.get_field_of_view().as_degrees(), 50.f); +} + +TEST(unit_test_unreal_engine, loook_at_random_all_axis) +{ + std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source + std::uniform_real_distribution dist(-1000.f, 1000.f); + + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::unreal_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.001f, 10000.f); + + + std::size_t failed_points = 0; + for (int i = 0; i < 100; i++) + { + const auto position_to_look = omath::Vector3{dist(gen), dist(gen), dist(gen)}; + + if (cam.get_origin().distance_to(position_to_look) < 10) + continue; + cam.look_at(position_to_look); + + auto projected_pos = cam.world_to_view_port(position_to_look); + + EXPECT_TRUE(projected_pos.has_value()); + + if (!projected_pos) + continue; + + if (std::abs(projected_pos->x - 0.f) >= 0.0001f || std::abs(projected_pos->y - 0.f) >= 0.0001f) + failed_points++; + } + EXPECT_LE(failed_points, 100); +} + +TEST(unit_test_unreal_engine, loook_at_random_x_axis) +{ + std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source + std::uniform_real_distribution dist(-1000.f, 1000.f); + + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::unreal_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.001f, 10000.f); + + std::size_t failed_points = 0; + for (int i = 0; i < 1000; i++) + { + const auto position_to_look = omath::Vector3{dist(gen), dist(gen), dist(gen)}; + + if (cam.get_origin().distance_to(position_to_look) < 10) + continue; + + cam.look_at(position_to_look); + + auto projected_pos = cam.world_to_view_port(position_to_look); + + EXPECT_TRUE(projected_pos.has_value()); + + if (!projected_pos) + continue; + + if (std::abs(projected_pos->x - 0.f) >= 0.01f || std::abs(projected_pos->y - 0.f) >= 0.01f) + failed_points++; + } + EXPECT_LE(failed_points, 100); +} + +TEST(unit_test_unreal_engine, loook_at_random_y_axis) +{ + std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source + std::uniform_real_distribution dist(-1000.f, 1000.f); + + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::unreal_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.001f, 10000.f); + + std::size_t failed_points = 0; + for (int i = 0; i < 1000; i++) + { + const auto position_to_look = omath::Vector3{0.f, dist(gen), 0.f}; + + if (cam.get_origin().distance_to(position_to_look) < 10) + continue; + + cam.look_at(position_to_look); + + auto projected_pos = cam.world_to_view_port(position_to_look); + + EXPECT_TRUE(projected_pos.has_value()); + + if (!projected_pos) + continue; + + if (std::abs(projected_pos->x - 0.f) >= 0.01f || std::abs(projected_pos->y - 0.f) >= 0.01f) + failed_points++; + } + EXPECT_LE(failed_points, 100); +} + +TEST(unit_test_unreal_engine, loook_at_random_z_axis) +{ + std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source + std::uniform_real_distribution dist(-1000.f, 1000.f); + + constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); + auto cam = omath::unreal_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.001f, 10000.f); + + std::size_t failed_points = 0; + for (int i = 0; i < 1000; i++) + { + const auto position_to_look = omath::Vector3{0.f, 0.f, dist(gen)}; + + if (cam.get_origin().distance_to(position_to_look) < 10) + continue; + + cam.look_at(position_to_look); + + auto projected_pos = cam.world_to_view_port(position_to_look); + + EXPECT_TRUE(projected_pos.has_value()); + + if (!projected_pos) + continue; + + if (std::abs(projected_pos->x - 0.f) >= 0.01f || std::abs(projected_pos->y - 0.f) >= 0.01f) + failed_points++; + } + EXPECT_LE(failed_points, 100); +} \ No newline at end of file diff --git a/tests/general/unit_test_a_star.cpp b/tests/general/unit_test_a_star.cpp new file mode 100644 index 00000000..fc7011ad --- /dev/null +++ b/tests/general/unit_test_a_star.cpp @@ -0,0 +1,136 @@ +// Extra unit tests for the project's A* implementation +#include +#include +#include +#include +#include + +using namespace omath; +using namespace omath::pathfinding; + +TEST(AStarExtra, TrivialNeighbor) +{ + NavigationMesh nav; + Vector3 v1{0.f, 0.f, 0.f}; + Vector3 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 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 left{0.f, 0.f, 0.f}; + constexpr Vector3 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{static_cast(x), static_cast(y), 0.f}; }; + for (int y = 0; y < 3; ++y) + { + for (int x = 0; x < 3; ++x) + { + Vector3 v = idx(x, y); + if (x == 1 && y == 1) + continue; // center is omitted (blocked) + std::vector> neigh; + constexpr std::array, 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 start = idx(0, 1); + constexpr Vector3 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 v1{0.f, 0.f, 0.f}; + Vector3 v2{1.f, 0.f, 0.f}; + nav.m_vertex_map.emplace(v1, std::vector>{v2}); + nav.m_vertex_map.emplace(v2, std::vector>{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 v1{0.f, 0.f, 0.f}; + constexpr Vector3 v2{10.f, 0.f, 0.f}; + // nav has only v1 + nav.m_vertex_map.emplace(v1, std::vector>{}); + + 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 v1{0.f, 0.f, 0.f}; + constexpr Vector3 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) +{ + omath::pathfinding::NavigationMesh mesh; + + mesh.m_vertex_map[{0.f, 0.f, 0.f}] = {{0.f, 1.f, 0.f}}; + mesh.m_vertex_map[{0.f, 1.f, 0.f}] = {{0.f, 2.f, 0.f}}; + mesh.m_vertex_map[{0.f, 2.f, 0.f}] = {{0.f, 3.f, 0.f}}; + mesh.m_vertex_map[{0.f, 3.f, 0.f}] = {}; + std::ignore = omath::pathfinding::Astar::find_path({}, {0.f, 3.f, 0.f}, mesh); +} \ No newline at end of file diff --git a/tests/general/unit_test_angle.cpp b/tests/general/unit_test_angle.cpp new file mode 100644 index 00000000..d1fc410f --- /dev/null +++ b/tests/general/unit_test_angle.cpp @@ -0,0 +1,192 @@ +// +// Created by Orange on 11/30/2024. +// + +#include +#include +#include +#include + +using namespace omath; + +namespace +{ + + // Handy aliases (defaults: Type=float, [0,360], Normalized) + using Deg = Angle(0), static_cast(360), AngleFlags::Normalized>; + using Pitch = Angle(-90), static_cast(90), AngleFlags::Clamped>; + using Turn = Angle(-180), static_cast(180), AngleFlags::Normalized>; + + constexpr float k_eps = 1e-5f; + +} // namespace + +// ---------- Construction / factories ---------- + +TEST(UnitTestAngle, DefaultConstructor_IsZeroDegrees) +{ + constexpr Deg a; // default ctor + EXPECT_FLOAT_EQ(*a, 0.0f); + EXPECT_FLOAT_EQ(a.as_degrees(), 0.0f); +} + +TEST(UnitTestAngle, FromDegrees_Normalized_WrapsAboveMax) +{ + const Deg a = Deg::from_degrees(370.0f); + EXPECT_FLOAT_EQ(a.as_degrees(), 10.0f); +} + +TEST(UnitTestAngle, FromDegrees_Normalized_WrapsBelowMin) +{ + const Deg a = Deg::from_degrees(-10.0f); + EXPECT_FLOAT_EQ(a.as_degrees(), 350.0f); +} + +TEST(UnitTestAngle, FromDegrees_Clamped_ClampsToRange) +{ + 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); +} + +TEST(UnitTestAngle, FromRadians_And_AsRadians) +{ + const Deg a = Deg::from_radians(std::numbers::pi_v); + EXPECT_FLOAT_EQ(a.as_degrees(), 180.0f); + + const Deg b = Deg::from_degrees(180.0f); + EXPECT_NEAR(b.as_radians(), std::numbers::pi_v, 1e-6f); +} + +// ---------- Unary minus & deref ---------- + +TEST(UnitTestAngle, UnaryMinus_Normalized) +{ + const Deg a = Deg::from_degrees(30.0f); + const Deg b = -a; // wraps to 330 in [0,360) + EXPECT_FLOAT_EQ(b.as_degrees(), 330.0f); +} + +TEST(UnitTestAngle, DereferenceReturnsDegrees) +{ + const Deg a = Deg::from_degrees(42.0f); + EXPECT_FLOAT_EQ(*a, 42.0f); +} + +// ---------- Trigonometric helpers ---------- + +TEST(UnitTestAngle, SinCosTanCot_BasicCases) +{ + const Deg a0 = Deg::from_degrees(0.0f); + 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)); + + const Deg a45 = Deg::from_degrees(45.0f); + EXPECT_NEAR(a45.tan(), 1.0f, 1e-4f); + EXPECT_NEAR(a45.cot(), 1.0f, 1e-4f); + + const Deg a90 = Deg::from_degrees(90.0f); + EXPECT_NEAR(a90.sin(), 1.0f, 1e-4f); + EXPECT_NEAR(a90.cos(), 0.0f, 1e-4f); +} + +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, k_eps); + + const Deg a45 = Deg::from_degrees(45.0f); + // atan(pi/4) ≈ 0.665773... + EXPECT_NEAR(a45.atan(), 0.66577375f, 1e-6f); +} + +// ---------- Compound arithmetic ---------- + +TEST(UnitTestAngle, PlusEquals_Normalized_Wraps) +{ + Deg a = Deg::from_degrees(350.0f); + a += Deg::from_degrees(20.0f); // 370 -> 10 + EXPECT_FLOAT_EQ(a.as_degrees(), 10.0f); +} + +TEST(UnitTestAngle, MinusEquals_Normalized_Wraps) +{ + Deg a = Deg::from_degrees(10.0f); + a -= Deg::from_degrees(30.0f); // -20 -> 340 + EXPECT_FLOAT_EQ(a.as_degrees(), 340.0f); +} + +TEST(UnitTestAngle, PlusEquals_Clamped_Clamps) +{ + Pitch p = Pitch::from_degrees(80.0f); + p += Pitch::from_degrees(30.0f); // 110 -> clamp to 90 + EXPECT_FLOAT_EQ(p.as_degrees(), 90.0f); +} + +TEST(UnitTestAngle, MinusEquals_Clamped_Clamps) +{ + Pitch p = Pitch::from_degrees(-70.0f); + p -= Pitch::from_degrees(40.0f); // -110 -> clamp to -90 + EXPECT_FLOAT_EQ(p.as_degrees(), -90.0f); +} + +// ---------- Alternative ranges ---------- + +TEST(UnitTestAngle, NormalizedRange_Neg180To180) +{ + const Turn a = Turn::from_degrees(190.0f); // -> -170 + const Turn b = Turn::from_degrees(-190.0f); // -> 170 + + EXPECT_FLOAT_EQ(a.as_degrees(), -170.0f); + EXPECT_FLOAT_EQ(b.as_degrees(), 170.0f); +} + +// ---------- Comparisons (via <=>) ---------- + +TEST(UnitTestAngle, Comparisons_WorkWithPartialOrdering) +{ + const Deg a = Deg::from_degrees(10.0f); + const Deg b = Deg::from_degrees(20.0f); + const Deg c = Deg::from_degrees(10.0f); + + EXPECT_TRUE(a < b); + EXPECT_TRUE(b > a); + EXPECT_TRUE(a <= c); + EXPECT_TRUE(c >= a); +} + +// ---------- std::format formatter ---------- + +TEST(UnitTestAngle, Formatter_PrintsDegreesWithSuffix) +{ + const Deg a = Deg::from_degrees(15.0f); + EXPECT_EQ(std::format("{}", a), "15deg"); + + const Deg b = Deg::from_degrees(10.5f); + EXPECT_EQ(std::format("{}", b), "10.5deg"); + + const Turn t = Turn::from_degrees(-170.0f); + EXPECT_EQ(std::format("{}", t), "-170deg"); +} + +TEST(UnitTestAngle, BinaryPlus_ReturnsWrappedSum) +{ + Angle<> a = Deg::from_degrees(350.0f); + const Deg b = Deg::from_degrees(20.0f); + const Deg c = a + b; // expect 10° + EXPECT_FLOAT_EQ(c.as_degrees(), 10.0f); +} + +TEST(UnitTestAngle, BinaryMinus_ReturnsWrappedDiff) +{ + Angle<> a = Deg::from_degrees(10.0f); + const Deg b = Deg::from_degrees(30.0f); + const Deg c = a - b; // expect 340° + EXPECT_FLOAT_EQ(c.as_degrees(), 340.0f); +} diff --git a/tests/general/unit_test_angles.cpp b/tests/general/unit_test_angles.cpp new file mode 100644 index 00000000..42f81741 --- /dev/null +++ b/tests/general/unit_test_angles.cpp @@ -0,0 +1,49 @@ +// +// Created by Orange on 11/30/2024. +// +#include +#include + +TEST(unit_test_angles, radians_to_deg) +{ + constexpr float rad = 67; + + EXPECT_NEAR(omath::angles::radians_to_degrees(rad), 3838.82f, 0.01f); +} + +TEST(unit_test_angles, degrees_to_radians) +{ + constexpr float degree = 90; + + EXPECT_NEAR(omath::angles::degrees_to_radians(degree), 1.5708f, 0.01f); +} + +TEST(unit_test_angles, horizontal_fov_to_verical) +{ + constexpr float h_fov = 90; + constexpr float aspect_ration = 16.0f / 9.0f; + const auto vertical_fov = omath::angles::horizontal_fov_to_vertical(h_fov, aspect_ration); + + EXPECT_NEAR(vertical_fov, 58.71f, 0.01f); +} + +TEST(unit_test_angles, vertical_to_horizontal) +{ + constexpr float v_fov = 58.71; + constexpr float aspect_ration = 16.0f / 9.0f; + const auto horizontal_fov = omath::angles::vertical_fov_to_horizontal(v_fov, aspect_ration); + + EXPECT_NEAR(horizontal_fov, 89.99f, 0.01f); +} +TEST(unit_test_angles, wrap_angle) +{ + const float wrapped = omath::angles::wrap_angle(361.f, 0.f, 360.f); + + EXPECT_NEAR(wrapped, 1.f, 0.01f); +} +TEST(unit_test_angles, wrap_angle_negative_range) +{ + const float wrapped = omath::angles::wrap_angle(-90.f, 0.f, 360.f); + + EXPECT_NEAR(wrapped, 270.f, 0.01f); +} \ No newline at end of file diff --git a/tests/general/unit_test_box_primitive.cpp b/tests/general/unit_test_box_primitive.cpp new file mode 100644 index 00000000..215f2229 --- /dev/null +++ b/tests/general/unit_test_box_primitive.cpp @@ -0,0 +1,6 @@ +// +// Created by Vlad on 4/18/2025. +// +#include +#include + diff --git a/tests/general/unit_test_colider.cpp b/tests/general/unit_test_colider.cpp new file mode 100644 index 00000000..ce3a9606 --- /dev/null +++ b/tests/general/unit_test_colider.cpp @@ -0,0 +1,40 @@ +// +// Created by Vlad on 11/9/2025. +// +#include "omath/engines/source_engine/collider.hpp" +#include +#include + +TEST(UnitTestColider, CheckToWorld) +{ + omath::source_engine::Mesh mesh = { + std::vector>{ + { { 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_position({1.f, 0.f, 0.f}); + + EXPECT_EQ(vertex, omath::Vector3(1.f, 3.f, 1.f)); +} + +TEST(UnitTestColider, FindFurthestVertex) +{ + 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}).position; + EXPECT_EQ(vertex, omath::Vector3(1.f, 1.f, 1.f)); +} + + + diff --git a/tests/general/unit_test_collision_extra.cpp b/tests/general/unit_test_collision_extra.cpp new file mode 100644 index 00000000..0db0ead6 --- /dev/null +++ b/tests/general/unit_test_collision_extra.cpp @@ -0,0 +1,107 @@ +// Extra collision tests: Simplex, MeshCollider, EPA +#include +#include +#include +#include +#include + +using namespace omath; +using namespace omath::collision; + +TEST(SimplexTest, HandleEmptySimplex) +{ + Simplex> simplex; + Vector3 direction{1, 0, 0}; + + EXPECT_EQ(simplex.size(), 0); + EXPECT_FALSE(simplex.handle(direction)); +} + +TEST(SimplexTest, HandleLineCollinearWithXAxis) +{ + using Vec3 = Vector3; + Simplex 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> s; + s = {Vector3{1.f, 0.f, 0.f}, Vector3{2.f, 0.f, 0.f}}; + Vector3 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> s; + s = {Vector3{1.f, 0.f, 0.f}, Vector3{0.f, 1.f, 0.f}, Vector3{0.f, 0.f, 1.f}}; + Vector3 dir{0, 0, 0}; + EXPECT_FALSE(s.handle(dir)); + EXPECT_GT(dir.length_sqr(), 0.0f); +} + +TEST(CollisionExtra, SimplexTetrahedronInside) +{ + Simplex> s; + // tetra that surrounds origin roughly + s = {Vector3{1.f, 0.f, 0.f}, Vector3{0.f, 1.f, 0.f}, Vector3{0.f, 0.f, 1.f}, + Vector3{-1.f, -1.f, -1.f}}; + Vector3 dir{0, 0, 0}; + // if origin inside, handle returns true + const bool inside = s.handle(dir); + EXPECT_TRUE(inside); +} + +TEST(CollisionExtra, MeshColliderOriginAndFurthest) +{ + source_engine::Mesh mesh = { + std::vector>{{{1.f, 1.f, 1.f}, {}, {}}, {{-1.f, -1.f, -1.f}, {}, {}}}, {}}; + mesh.set_origin({0, 2, 0}); + source_engine::MeshCollider collider(mesh); + + EXPECT_EQ(collider.get_origin(), omath::Vector3(0, 2, 0)); + collider.set_origin({1, 2, 3}); + EXPECT_EQ(collider.get_origin(), omath::Vector3(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(2.f, 3.f, 4.f)); +} + +TEST(CollisionExtra, EPAConvergesOnSimpleCase) +{ + // Build two simple colliders using simple meshes that overlap + source_engine::Mesh meshA = { + std::vector>{{{0.f, 0.f, 0.f}, {}, {}}, {{1.f, 0.f, 0.f}, {}, {}}}, {}}; + source_engine::Mesh mesh_b = meshA; + mesh_b.set_origin({0.5f, 0.f, 0.f}); // translate to overlap + + source_engine::MeshCollider a(meshA); + source_engine::MeshCollider b(mesh_b); + + // Create a simplex that approximately contains the origin in Minkowski space + Simplex> simplex; + simplex = {omath::Vector3{0.5f, 0.f, 0.f}, omath::Vector3{-0.5f, 0.f, 0.f}, + omath::Vector3{0.f, 0.5f, 0.f}, omath::Vector3{0.f, -0.5f, 0.f}}; + + auto pool = std::pmr::monotonic_buffer_resource(1024); + auto res = Epa::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); + } +} diff --git a/tests/general/unit_test_color_grouped.cpp b/tests/general/unit_test_color_grouped.cpp new file mode 100644 index 00000000..a32eaf1a --- /dev/null +++ b/tests/general/unit_test_color_grouped.cpp @@ -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 +#include +#include +#include + +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); +} diff --git a/tests/general/unit_test_elf_scanner.cpp b/tests/general/unit_test_elf_scanner.cpp new file mode 100644 index 00000000..c2a9a283 --- /dev/null +++ b/tests/general/unit_test_elf_scanner.cpp @@ -0,0 +1,17 @@ +// +// Created by Vladislav on 30.12.2025. +// +// /Users/vladislav/Downloads/valencia +#include +#include +#include +TEST(unit_test_elf_pattern_scan_file, ScanMissingPattern) +{ + //FIXME: Implement normal tests :) + //constexpr std::string_view path = "/Users/vladislav/Downloads/crackme"; + + //const auto res = omath::ElfPatternScanner::scan_for_pattern_in_file(path, "F3 0F 1E FA 55 48 89 E5 B8 00 00 00 00", ".text"); + //EXPECT_TRUE(res.has_value()); + + //std::println("In virtual mem: 0x{:x}", res->virtual_base_addr+res->target_offset); +} diff --git a/tests/general/unit_test_epa.cpp b/tests/general/unit_test_epa.cpp new file mode 100644 index 00000000..0c4dbb3c --- /dev/null +++ b/tests/general/unit_test_epa.cpp @@ -0,0 +1,153 @@ +#include "omath/collision/epa_algorithm.hpp" // Epa + GjkAlgorithmWithSimplex +#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 +#include + +using Mesh = omath::source_engine::Mesh; +using Collider = omath::source_engine::MeshCollider; +using Gjk = omath::collision::GjkAlgorithm; +using EPA = omath::collision::Epa; + +TEST(UnitTestEpa, TestCollisionTrue) +{ + // Unit cube [-1,1]^3 + std::vector> 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> 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 [hit, simplex] = Gjk::is_collide_with_simplex_info(A, B); + ASSERT_TRUE(hit) << "GJK should report collision"; + + // EPA + EPA::Params params; + auto pool = std::make_shared(1024); + params.max_iterations = 64; + params.tolerance = 1e-4f; + auto epa = EPA::solve(A, B, 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> 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> 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(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); +} diff --git a/tests/general/unit_test_epa_internal.cpp b/tests/general/unit_test_epa_internal.cpp new file mode 100644 index 00000000..e3c0c1ba --- /dev/null +++ b/tests/general/unit_test_epa_internal.cpp @@ -0,0 +1,46 @@ +#include "omath/collision/epa_algorithm.hpp" +#include "omath/collision/simplex.hpp" +#include "omath/linear_algebra/vector3.hpp" +#include + +using Vector3f = omath::Vector3; + +// 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; +using Simplex = omath::collision::Simplex; + +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; + + // Should either return a valid result or gracefully return nullopt + if (const auto result = EpaDummy::solve(a, b, s, params)) + { + 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)"; + } +} diff --git a/tests/general/unit_test_epa_more.cpp b/tests/general/unit_test_epa_more.cpp new file mode 100644 index 00000000..ac4a799e --- /dev/null +++ b/tests/general/unit_test_epa_more.cpp @@ -0,0 +1,52 @@ +#include "omath/collision/epa_algorithm.hpp" +#include "omath/collision/simplex.hpp" +#include "omath/linear_algebra/vector3.hpp" +#include + +using Vector3f = omath::Vector3; + +// Minimal collider interface matching Epa's expectations +struct DegenerateCollider +{ + using VectorType = Vector3f; + // returns furthest point along dir + [[nodiscard]] + static VectorType find_abs_furthest_vertex_position(const VectorType& dir) 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; +using Simplex = omath::collision::Simplex; + +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)"; + } +} diff --git a/tests/general/unit_test_gjk.cpp b/tests/general/unit_test_gjk.cpp new file mode 100644 index 00000000..722fae72 --- /dev/null +++ b/tests/general/unit_test_gjk.cpp @@ -0,0 +1,61 @@ +// +// Created by Vlad on 11/9/2025. +// +#include "omath/engines/source_engine/collider.hpp" +#include +#include +#include +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}, {}, {} } + }, + {} + }; +} +TEST(UnitTestGjk, TestCollisionTrue) +{ + const omath::source_engine::MeshCollider collider_a(mesh); + + auto mesh_b = mesh; + mesh_b.set_origin({0.f, 0.5f, 0.f}); + + const omath::source_engine::MeshCollider collider_b(mesh_b); + + using GjkAlgorithm = omath::collision::GjkAlgorithm; + + const auto result = GjkAlgorithm::is_collide(collider_a, collider_b); + + EXPECT_TRUE(result); +} +TEST(UnitTestGjk, TestCollisionFalse) +{ + const omath::source_engine::MeshCollider collider_a(mesh); + auto mesh_b = mesh; + mesh_b.set_origin({0.f, 2.1f, 0.f}); + const omath::source_engine::MeshCollider collider_b(mesh_b); + + const auto result = + omath::collision::GjkAlgorithm::is_collide(collider_a, collider_b); + + EXPECT_FALSE(result); +} + +TEST(UnitTestGjk, TestCollisionEqualOrigin) +{ + const omath::source_engine::MeshCollider collider_a(mesh); + const omath::source_engine::MeshCollider collider_b(mesh); + + const auto result = + omath::collision::GjkAlgorithm::is_collide(collider_a, collider_b); + + EXPECT_TRUE(result); +} \ No newline at end of file diff --git a/tests/general/unit_test_imgui_intergration.cpp b/tests/general/unit_test_imgui_intergration.cpp new file mode 100644 index 00000000..0e6ddea1 --- /dev/null +++ b/tests/general/unit_test_imgui_intergration.cpp @@ -0,0 +1,39 @@ +// +// Created by Vlad on 10/23/2025. +// +#ifdef OMATH_IMGUI_INTEGRATION +#include +#include + +#define IMGUI_DEFINE_MATH_OPERATORS +#include + + + +using namespace omath; + +TEST(unit_test_imgui_intergration, Vector2ToImVec2) +{ + constexpr Vector2 omath_vector_2d = {1.f, 2.f}; + constexpr ImVec2 imgui_vector_2d = {1, 2.f}; + + constexpr auto converted = omath_vector_2d.to_im_vec2(); + EXPECT_NEAR(converted.x, imgui_vector_2d.x, 1.e-5f); + EXPECT_NEAR(converted.y, imgui_vector_2d.y, 1.e-5f); +} + +TEST(unit_test_imgui_intergration, Vector4ToImVec4) +{ + constexpr Vector4 omath_vector_2d = {1.f, 2.f, 3.f, 4.f}; + constexpr ImVec4 imgui_vector_4d = {1, 2.f, 3.f, 4.f}; + + constexpr auto converted = omath_vector_2d.to_im_vec4(); + + EXPECT_NEAR(converted.x, imgui_vector_4d.x, 1.e-5f); + EXPECT_NEAR(converted.y, imgui_vector_4d.y, 1.e-5f); + EXPECT_NEAR(converted.z, imgui_vector_4d.z, 1.e-5f); + EXPECT_NEAR(converted.w, imgui_vector_4d.w, 1.e-5f); +} + + +#endif diff --git a/tests/general/unit_test_line_trace.cpp b/tests/general/unit_test_line_trace.cpp new file mode 100644 index 00000000..4aa6507b --- /dev/null +++ b/tests/general/unit_test_line_trace.cpp @@ -0,0 +1,127 @@ +// +// Revised unit‑test suite for LineTracer (segment‑based Möller–Trumbore) +// Pure ASCII: avoids non‑standard characters that MSVC rejects. +// +#include "omath/linear_algebra/triangle.hpp" +#include "omath/collision/line_tracer.hpp" +#include "omath/linear_algebra/vector3.hpp" +#include "gtest/gtest.h" +#include + +using namespace omath; +using namespace omath::collision; + +using Vec3 = Vector3; + +namespace +{ + + // ----------------------------------------------------------------------------- + // Constants & helpers + // ----------------------------------------------------------------------------- + constexpr float k_tol = 1e-5f; + + 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 && + std::fabs(a.z - b.z) < tol; + } + + // ----------------------------------------------------------------------------- + // Fixture with one canonical right‑angled triangle in the XY plane. + // ----------------------------------------------------------------------------- + class LineTracerFixture : public ::testing::Test + { + protected: + LineTracerFixture() : + triangle({0.f, 0.f, 0.f}, {1.f, 0.f, 0.f}, {0.f, 1.f, 0.f}) + { + } + + Triangle triangle; + }; + + // ----------------------------------------------------------------------------- + // Data‑driven tests for CanTraceLine + // ----------------------------------------------------------------------------- + struct TraceCase + { + Ray<> ray; + bool expected_clear; // true => segment does NOT hit the triangle + friend std::ostream& operator<<(std::ostream& os, const TraceCase& tc) + { + os << "{ RayStart: (" << tc.ray.start.x << ", " << tc.ray.start.y << ", " << tc.ray.start.z << "), " + << "RayEnd: (" << tc.ray.end.x << ", " << tc.ray.end.y << ", " << tc.ray.end.z << "), " + << "Expected: " << (tc.expected_clear ? "True" : "False") << " }"; + return os; + } + }; + + class CanTraceLineParam : public LineTracerFixture, + public ::testing::WithParamInterface + { + }; + + TEST_P(CanTraceLineParam, VariousRays) + { + const auto& [ray, expected_clear] = GetParam(); + EXPECT_EQ(LineTracer<>::can_trace_line(ray, triangle), expected_clear); + } + + INSTANTIATE_TEST_SUITE_P( + BasicScenarios, + CanTraceLineParam, + ::testing::Values( + TraceCase{Ray{{ 0.3f, 0.3f, -1.f},{ 0.3f, 0.3f, 1.f}}, false}, // hit through centre + TraceCase{Ray{{ 0.3f, 0.3f, 1.f},{ 0.3f, 0.3f, 2.f}}, true}, // parallel above + TraceCase{Ray{{ 0.3f, 0.3f, 0.f},{ 0.3f, 0.3f,-1.f}}, true}, // starts inside, goes away + TraceCase{Ray{{ 2.0f, 2.0f, -1.f},{ 2.0f, 2.0f, 1.f}}, true}, // misses entirely + TraceCase{Ray{{-1.0f,-1.0f, 0.f},{ 1.5f, 1.5f, 0.f}},true}, // lies in plane, outside tri + TraceCase{Ray{{-1.0f,-1.0f, -1.f},{ 0.0f, 0.0f, 0.f}}, true}, // endpoint on vertex + TraceCase{Ray{{-1.0f, 0.0f, -1.f},{ 0.5f, 0.0f, 0.f}}, true} // endpoint on edge + ) + ); + + // ----------------------------------------------------------------------------- + // Validate that the reported hit point is correct for a genuine intersection. + // ----------------------------------------------------------------------------- + TEST_F(LineTracerFixture, HitPointCorrect) + { + constexpr Ray ray{{0.3f, 0.3f, -1.f}, {0.3f, 0.3f, 1.f}}; + constexpr Vec3 expected{0.3f, 0.3f, 0.f}; + + const Vec3 hit = LineTracer<>::get_ray_hit_point(ray, triangle); + ASSERT_FALSE(vec_equal(hit, ray.end)); + EXPECT_TRUE(vec_equal(hit, expected)); + } + + // ----------------------------------------------------------------------------- + // Triangle far beyond the ray should not block. + // ----------------------------------------------------------------------------- + TEST_F(LineTracerFixture, DistantTriangleClear) + { + constexpr Ray short_ray{{0.f, 0.f, 0.f}, {0.f, 0.f, 1.f}}; + constexpr Triangle distant{{1000.f, 1000.f, 1000.f}, + {1001.f, 1000.f, 1000.f}, + {1000.f, 1001.f, 1000.f}}; + + EXPECT_TRUE(LineTracer<>::can_trace_line(short_ray, distant)); + } + + TEST(unit_test_unity_engine, CantHit) + { + constexpr omath::Triangle> triangle{{2, 0, 0}, {2, 2, 0}, {2, 2, 2}}; + + constexpr Ray ray{{}, {1.0, 0, 0}, false}; + + EXPECT_TRUE(omath::collision::LineTracer<>::can_trace_line(ray, triangle)); + } + TEST(unit_test_unity_engine, CanHit) + { + constexpr omath::Triangle> triangle{{2, 0, 0}, {2, 2, 0}, {2, 2, 2}}; + + constexpr Ray ray{{}, {2.1, 0, 0}, false}; + EXPECT_FALSE(omath::collision::LineTracer<>::can_trace_line(ray, triangle)); + } +} // namespace diff --git a/tests/general/unit_test_line_tracer.cpp b/tests/general/unit_test_line_tracer.cpp new file mode 100644 index 00000000..ac56a7b1 --- /dev/null +++ b/tests/general/unit_test_line_tracer.cpp @@ -0,0 +1,65 @@ +#include "omath/collision/line_tracer.hpp" +#include "omath/linear_algebra/vector3.hpp" +#include "omath/linear_algebra/triangle.hpp" +#include + +using omath::Vector3; + +TEST(LineTracerTests, ParallelRayReturnsEnd) +{ + // Triangle in XY plane + constexpr omath::Triangle> 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{0.f,0.f,1.f}; + ray.end = Vector3{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{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> 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{2.f,2.f,-1.f}; + ray.end = Vector3{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> 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{0.25f,0.25f,-1.f}; + ray.end = Vector3{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> 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{0.25f,0.25f,0.f}; + ray.end = Vector3{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); +} diff --git a/tests/general/unit_test_line_tracer_extra.cpp b/tests/general/unit_test_line_tracer_extra.cpp new file mode 100644 index 00000000..dd95749a --- /dev/null +++ b/tests/general/unit_test_line_tracer_extra.cpp @@ -0,0 +1,47 @@ +// Extra LineTracer tests +#include +#include +#include + +using namespace omath; +using namespace omath::collision; + +TEST(LineTracerExtra, MissParallel) +{ + constexpr Triangle> 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> 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> 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 }; + // hitting exact vertex/edge may be considered miss; ensure function handles without crash + if (const auto hit = LineTracer<>::get_ray_hit_point(ray, tri); hit != ray.end) + { + EXPECT_NEAR(hit.x, 0.0f, 1e-6f); + EXPECT_NEAR(hit.y, 0.0f, 1e-6f); + } +} + +TEST(LineTracerExtra, InfiniteRayIgnoredIfBehind) +{ + constexpr Triangle> 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); +} diff --git a/tests/general/unit_test_line_tracer_more.cpp b/tests/general/unit_test_line_tracer_more.cpp new file mode 100644 index 00000000..13732090 --- /dev/null +++ b/tests/general/unit_test_line_tracer_more.cpp @@ -0,0 +1,105 @@ +#include "omath/collision/line_tracer.hpp" +#include "omath/linear_algebra/triangle.hpp" +#include "omath/linear_algebra/vector3.hpp" +#include + +using omath::Vector3; +using omath::collision::Ray; +using omath::collision::LineTracer; +using Triangle3 = omath::Triangle>; + +TEST(LineTracerMore, ParallelRayReturnsEnd) +{ + // Ray parallel to triangle plane: construct triangle in XY plane and ray along X axis + constexpr Triangle3 tri(Vector3{0.f,0.f,0.f}, Vector3{1.f,0.f,0.f}, Vector3{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{0.f,0.f,0.f}, Vector3{1.f,0.f,0.f}, Vector3{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{0.f,0.f,0.f}, Vector3{1.f,0.f,0.f}, Vector3{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{0.f,0.f,0.f}, Vector3{1.f,0.f,0.f}, Vector3{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{0.f,0.f,0.f}, Vector3{1.f,0.f,0.f}, Vector3{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::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{0.f,0.f,0.f}, Vector3{1.f,0.f,0.f}, Vector3{0.f,1.f,0.f}); + constexpr Triangle3 tri2(Vector3{0.f,0.f,-1e-8f}, Vector3{1.f,0.f,-1e-8f}, Vector3{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{0.f,0.f,0.f}, Vector3{1.f,0.f,0.f}, Vector3{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); +} diff --git a/tests/general/unit_test_line_tracer_more2.cpp b/tests/general/unit_test_line_tracer_more2.cpp new file mode 100644 index 00000000..7b762b79 --- /dev/null +++ b/tests/general/unit_test_line_tracer_more2.cpp @@ -0,0 +1,57 @@ +#include "omath/collision/line_tracer.hpp" +#include "omath/linear_algebra/triangle.hpp" +#include "omath/linear_algebra/vector3.hpp" +#include + +using omath::Vector3; +using omath::collision::Ray; +using omath::collision::LineTracer; +using Triangle3 = omath::Triangle>; + +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); +} diff --git a/tests/general/unit_test_linear_algebra_cover_more_ops.cpp b/tests/general/unit_test_linear_algebra_cover_more_ops.cpp new file mode 100644 index 00000000..b49ea48e --- /dev/null +++ b/tests/general/unit_test_linear_algebra_cover_more_ops.cpp @@ -0,0 +1,57 @@ +// Added to increase coverage for vector3/vector4/mat headers +#include +#include +#include + +#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 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 a{2.f, 4.f, 6.f, 8.f}; + constexpr Vector4 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); +} diff --git a/tests/general/unit_test_linear_algebra_cover_remaining.cpp b/tests/general/unit_test_linear_algebra_cover_remaining.cpp new file mode 100644 index 00000000..be42f383 --- /dev/null +++ b/tests/general/unit_test_linear_algebra_cover_remaining.cpp @@ -0,0 +1,50 @@ +// Additional coverage tests for Vector4 and Mat +#include +#include + +#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 a{5.f, 6.f, 7.f, 8.f}; + constexpr Vector4 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); +} diff --git a/tests/general/unit_test_linear_algebra_extra.cpp b/tests/general/unit_test_linear_algebra_extra.cpp new file mode 100644 index 00000000..c5926e84 --- /dev/null +++ b/tests/general/unit_test_linear_algebra_extra.cpp @@ -0,0 +1,64 @@ +#include +#include +#include +#include +#include +#include +#include + +using namespace omath; + +TEST(LinearAlgebraExtra, FormatterAndHashVector2) +{ + Vector2 v{1.0f, 2.0f}; + const std::string s = std::format("{}", v); + EXPECT_EQ(s, "[1, 2]"); + + const std::size_t h1 = std::hash>{}(v); + const std::size_t h2 = std::hash>{}(Vector2{1.0f, 2.0f}); + const std::size_t h3 = std::hash>{}(Vector2{2.0f, 3.0f}); + + EXPECT_EQ(h1, h2); + EXPECT_NE(h1, h3); +} + +TEST(LinearAlgebraExtra, FormatterAndHashVector3) +{ + Vector3 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>{}(v); + const std::size_t h2 = std::hash>{}(Vector3{1.0f, 2.0f, 3.0f}); + EXPECT_EQ(h1, h2); + + // point_to_same_direction + EXPECT_TRUE((Vector3{1,0,0}.point_to_same_direction(Vector3{2,0,0}))); + EXPECT_FALSE((Vector3{1,0,0}.point_to_same_direction(Vector3{-1,0,0}))); +} + +TEST(LinearAlgebraExtra, FormatterAndHashVector4) +{ + Vector4 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>{}(v); + const std::size_t h2 = std::hash>{}(Vector4{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); +} + + diff --git a/tests/general/unit_test_linear_algebra_helpers.cpp b/tests/general/unit_test_linear_algebra_helpers.cpp new file mode 100644 index 00000000..3483fa0d --- /dev/null +++ b/tests/general/unit_test_linear_algebra_helpers.cpp @@ -0,0 +1,56 @@ +#include "omath/linear_algebra/triangle.hpp" +#include "omath/linear_algebra/vector3.hpp" +#include "omath/linear_algebra/vector4.hpp" +#include + +// 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 a{1.f, 2.f, 3.f}; + constexpr Vector3 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 v1{0.f,0.f,0.f}; + constexpr Vector3 v2{3.f,0.f,0.f}; + constexpr Vector3 v3{3.f,4.f,0.f}; + + constexpr Triangle> 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 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(); +} diff --git a/tests/general/unit_test_linear_algebra_instantiate.cpp b/tests/general/unit_test_linear_algebra_instantiate.cpp new file mode 100644 index 00000000..338968cf --- /dev/null +++ b/tests/general/unit_test_linear_algebra_instantiate.cpp @@ -0,0 +1,74 @@ +// Instantiation-only tests to force out-of-line template emission +#include +#include +#include + +#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 a{1.f, 2.f, 3.f}; + Vector3 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>{}(a); + + // Vector4 usage + Vector4 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>{}(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); +} diff --git a/tests/general/unit_test_linear_algebra_more.cpp b/tests/general/unit_test_linear_algebra_more.cpp new file mode 100644 index 00000000..e29a7eed --- /dev/null +++ b/tests/general/unit_test_linear_algebra_more.cpp @@ -0,0 +1,64 @@ +#include "omath/linear_algebra/triangle.hpp" +#include "omath/linear_algebra/vector3.hpp" +#include "omath/linear_algebra/vector4.hpp" +#include + +using namespace omath; + +TEST(LinearAlgebraMore, Vector3EdgeCases) +{ + constexpr Vector3 zero{0.f,0.f,0.f}; + constexpr Vector3 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(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 x{1.f,0.f,0.f}; + constexpr Vector3 y{0.f,1.f,0.f}; + EXPECT_TRUE(x.is_perpendicular(y)); +} + +TEST(LinearAlgebraMore, TriangleRectangularAndDegenerate) +{ + constexpr Vector3 v1{0.f,0.f,0.f}; + constexpr Vector3 v2{3.f,0.f,0.f}; + constexpr Vector3 v3{3.f,4.f,0.f}; // 3-4-5 triangle, rectangular at v2 + + constexpr Triangle> 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> 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 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 a{1.f,2.f,3.f,4.f}; + constexpr Vector4 b{2.f,2.f,2.f,2.f}; + EXPECT_TRUE(a < b || a > b || a == b); // just exercise comparisons +} diff --git a/tests/general/unit_test_linear_algebra_more2.cpp b/tests/general/unit_test_linear_algebra_more2.cpp new file mode 100644 index 00000000..e9c4e3f9 --- /dev/null +++ b/tests/general/unit_test_linear_algebra_more2.cpp @@ -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 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 zero{0.f,0.f,0.f}; + auto ang = v.angle_between(zero); + EXPECT_FALSE(ang.has_value()); + + Vector3 a{1.f,0.f,0.f}; + Vector3 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{2.f,0.f,0.f})); + + // exercise hash specialization for Vector3 + std::hash> hasher; + auto hv = hasher(v); + (void)hv; +} + +TEST(LinearAlgebraMore2, Vector4NonInlinedHelpers) +{ + Vector4 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 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(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); +} diff --git a/tests/general/unit_test_macho_scanner.cpp b/tests/general/unit_test_macho_scanner.cpp new file mode 100644 index 00000000..1e15bdcd --- /dev/null +++ b/tests/general/unit_test_macho_scanner.cpp @@ -0,0 +1,358 @@ +// +// Created by Copilot on 04.02.2026. +// +// Unit tests for MachOPatternScanner +#include +#include +#include +#include +#include +#include + +using namespace omath; + +namespace +{ + // Mach-O magic numbers + constexpr std::uint32_t mh_magic_64 = 0xFEEDFACF; + constexpr std::uint32_t mh_magic_32 = 0xFEEDFACE; + constexpr std::uint32_t lc_segment = 0x1; + constexpr std::uint32_t lc_segment_64 = 0x19; + + constexpr std::string_view segment_name = "__TEXT"; + constexpr std::string_view section_name = "__text"; +#pragma pack(push, 1) + struct MachHeader64 + { + std::uint32_t magic; + std::uint32_t cputype; + std::uint32_t cpusubtype; + std::uint32_t filetype; + std::uint32_t ncmds; + std::uint32_t sizeofcmds; + std::uint32_t flags; + std::uint32_t reserved; + }; + + struct MachHeader32 + { + std::uint32_t magic; + std::uint32_t cputype; + std::uint32_t cpusubtype; + std::uint32_t filetype; + std::uint32_t ncmds; + std::uint32_t sizeofcmds; + std::uint32_t flags; + }; + + struct SegmentCommand64 + { + std::uint32_t cmd; + std::uint32_t cmdsize; + char segname[16]; + std::uint64_t vmaddr; + std::uint64_t vmsize; + std::uint64_t fileoff; + std::uint64_t filesize; + std::uint32_t maxprot; + std::uint32_t initprot; + std::uint32_t nsects; + std::uint32_t flags; + }; + + struct SegmentCommand32 + { + std::uint32_t cmd; + std::uint32_t cmdsize; + char segname[16]; + std::uint32_t vmaddr; + std::uint32_t vmsize; + std::uint32_t fileoff; + std::uint32_t filesize; + std::uint32_t maxprot; + std::uint32_t initprot; + std::uint32_t nsects; + std::uint32_t flags; + }; + + struct Section64 + { + char sectname[16]; + char segname[16]; + std::uint64_t addr; + std::uint64_t size; + std::uint32_t offset; + std::uint32_t align; + std::uint32_t reloff; + std::uint32_t nreloc; + std::uint32_t flags; + std::uint32_t reserved1; + std::uint32_t reserved2; + std::uint32_t reserved3; + }; + + struct Section32 + { + char sectname[16]; + char segname[16]; + std::uint32_t addr; + std::uint32_t size; + std::uint32_t offset; + std::uint32_t align; + std::uint32_t reloff; + std::uint32_t nreloc; + std::uint32_t flags; + std::uint32_t reserved1; + std::uint32_t reserved2; + }; +#pragma pack(pop) + + // Helper function to create a minimal 64-bit Mach-O file with a __text section + bool write_minimal_macho64_file(const std::string& path, const std::vector& section_bytes) + { + std::ofstream f(path, std::ios::binary); + if (!f.is_open()) + return false; + + // Calculate sizes + constexpr std::size_t header_size = sizeof(MachHeader64); + constexpr std::size_t segment_size = sizeof(SegmentCommand64); + constexpr std::size_t section_size = sizeof(Section64); + constexpr std::size_t load_cmd_size = segment_size + section_size; + // Section data will start after headers + const std::size_t section_offset = header_size + load_cmd_size; + + // Create Mach-O header + MachHeader64 header{}; + header.magic = mh_magic_64; + header.cputype = 0x01000007; // CPU_TYPE_X86_64 + header.cpusubtype = 0x3; // CPU_SUBTYPE_X86_64_ALL + header.filetype = 0x2; // MH_EXECUTE + header.ncmds = 1; + header.sizeofcmds = static_cast(load_cmd_size); + header.flags = 0; + header.reserved = 0; + + f.write(reinterpret_cast(&header), sizeof(header)); + + // Create segment command + SegmentCommand64 segment{}; + segment.cmd = lc_segment_64; + segment.cmdsize = static_cast(load_cmd_size); + std::ranges::copy(segment_name, segment.segname); + segment.vmaddr = 0x100000000; + segment.vmsize = section_bytes.size(); + segment.fileoff = section_offset; + segment.filesize = section_bytes.size(); + segment.maxprot = 7; // VM_PROT_ALL + segment.initprot = 5; // VM_PROT_READ | VM_PROT_EXECUTE + segment.nsects = 1; + segment.flags = 0; + + f.write(reinterpret_cast(&segment), sizeof(segment)); + + // Create section + Section64 section{}; + std::ranges::copy(section_name, section.sectname); + std::ranges::copy(segment_name, segment.segname); + section.addr = 0x100000000; + section.size = section_bytes.size(); + section.offset = static_cast(section_offset); + section.align = 0; + section.reloff = 0; + section.nreloc = 0; + section.flags = 0; + section.reserved1 = 0; + section.reserved2 = 0; + section.reserved3 = 0; + + f.write(reinterpret_cast(§ion), sizeof(section)); + + // Write section data + f.write(reinterpret_cast(section_bytes.data()), static_cast(section_bytes.size())); + + f.close(); + return true; + } + + // Helper function to create a minimal 32-bit Mach-O file with a __text section + bool write_minimal_macho32_file(const std::string& path, const std::vector& section_bytes) + { + std::ofstream f(path, std::ios::binary); + if (!f.is_open()) + return false; + + // Calculate sizes + constexpr std::size_t header_size = sizeof(MachHeader32); + constexpr std::size_t segment_size = sizeof(SegmentCommand32); + constexpr std::size_t section_size = sizeof(Section32); + constexpr std::size_t load_cmd_size = segment_size + section_size; + + // Section data will start after headers + constexpr std::size_t section_offset = header_size + load_cmd_size; + + // Create Mach-O header + MachHeader32 header{}; + header.magic = mh_magic_32; + header.cputype = 0x7; // CPU_TYPE_X86 + header.cpusubtype = 0x3; // CPU_SUBTYPE_X86_ALL + header.filetype = 0x2; // MH_EXECUTE + header.ncmds = 1; + header.sizeofcmds = static_cast(load_cmd_size); + header.flags = 0; + + f.write(reinterpret_cast(&header), sizeof(header)); + + // Create segment command + SegmentCommand32 segment{}; + segment.cmd = lc_segment; + segment.cmdsize = static_cast(load_cmd_size); + std::ranges::copy(segment_name, segment.segname); + segment.vmaddr = 0x1000; + segment.vmsize = static_cast(section_bytes.size()); + segment.fileoff = static_cast(section_offset); + segment.filesize = static_cast(section_bytes.size()); + segment.maxprot = 7; // VM_PROT_ALL + segment.initprot = 5; // VM_PROT_READ | VM_PROT_EXECUTE + segment.nsects = 1; + segment.flags = 0; + + f.write(reinterpret_cast(&segment), sizeof(segment)); + + // Create section + Section32 section{}; + std::ranges::copy(section_name, section.sectname); + std::ranges::copy(segment_name, segment.segname); + section.addr = 0x1000; + section.size = static_cast(section_bytes.size()); + section.offset = static_cast(section_offset); + section.align = 0; + section.reloff = 0; + section.nreloc = 0; + section.flags = 0; + section.reserved1 = 0; + section.reserved2 = 0; + + f.write(reinterpret_cast(§ion), sizeof(section)); + + // Write section data + f.write(reinterpret_cast(section_bytes.data()), static_cast(section_bytes.size())); + + f.close(); + return true; + } + +} // namespace + +// Test scanning for a pattern that exists in a 64-bit Mach-O file +TEST(unit_test_macho_pattern_scan_file, ScanFindsPattern64) +{ + constexpr std::string_view path = "./test_minimal_macho64.bin"; + const std::vector bytes = {0x55, 0x48, 0x89, 0xE5, 0x90, 0x90}; // push rbp; mov rbp, rsp; nop; nop + ASSERT_TRUE(write_minimal_macho64_file(path.data(), bytes)); + + const auto res = MachOPatternScanner::scan_for_pattern_in_file(path, "55 48 89 E5", "__text"); + EXPECT_TRUE(res.has_value()); + if (res.has_value()) + { + EXPECT_EQ(res->target_offset, 0); + } +} + +// Test scanning for a pattern that exists in a 32-bit Mach-O file +TEST(unit_test_macho_pattern_scan_file, ScanFindsPattern32) +{ + constexpr std::string_view path = "./test_minimal_macho32.bin"; + const std::vector bytes = {0x55, 0x89, 0xE5, 0x90, 0x90}; // push ebp; mov ebp, esp; nop; nop + ASSERT_TRUE(write_minimal_macho32_file(path.data(), bytes)); + + const auto res = MachOPatternScanner::scan_for_pattern_in_file(path, "55 89 E5", "__text"); + EXPECT_TRUE(res.has_value()); + if (res.has_value()) + { + EXPECT_EQ(res->target_offset, 0); + } +} + +// Test scanning for a pattern that does not exist +TEST(unit_test_macho_pattern_scan_file, ScanMissingPattern) +{ + constexpr std::string_view path = "./test_minimal_macho_missing.bin"; + const std::vector bytes = {0x00, 0x01, 0x02, 0x03}; + ASSERT_TRUE(write_minimal_macho64_file(path.data(), bytes)); + + const auto res = MachOPatternScanner::scan_for_pattern_in_file(path, "FF EE DD", "__text"); + EXPECT_FALSE(res.has_value()); +} + +// Test scanning for a pattern at a non-zero offset +TEST(unit_test_macho_pattern_scan_file, ScanPatternAtOffset) +{ + constexpr std::string_view path = "./test_minimal_macho_offset.bin"; + const std::vector bytes = {0x90, 0x90, 0x90, 0x55, 0x48, 0x89, 0xE5}; // nops then pattern + ASSERT_TRUE(write_minimal_macho64_file(path.data(), bytes)); + + const auto res = MachOPatternScanner::scan_for_pattern_in_file(path, "55 48 89 E5", "__text"); + EXPECT_TRUE(res.has_value()); + if (res.has_value()) + { + EXPECT_EQ(res->target_offset, 3); + } +} + +// Test scanning with wildcards +TEST(unit_test_macho_pattern_scan_file, ScanWithWildcard) +{ + constexpr std::string_view path = "./test_minimal_macho_wildcard.bin"; + const std::vector bytes = {0x55, 0x48, 0x89, 0xE5, 0x90}; + ASSERT_TRUE(write_minimal_macho64_file(path.data(), bytes)); + + const auto res = MachOPatternScanner::scan_for_pattern_in_file(path, "55 ? 89 E5", "__text"); + EXPECT_TRUE(res.has_value()); +} + +// Test scanning a non-existent file +TEST(unit_test_macho_pattern_scan_file, ScanNonExistentFile) +{ + const auto res = MachOPatternScanner::scan_for_pattern_in_file("/non/existent/file.bin", "55 48", "__text"); + EXPECT_FALSE(res.has_value()); +} + +// Test scanning an invalid (non-Mach-O) file +TEST(unit_test_macho_pattern_scan_file, ScanInvalidFile) +{ + constexpr std::string_view path = "./test_invalid_macho.bin"; + std::ofstream f(path.data(), std::ios::binary); + const std::vector garbage = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05}; + f.write(reinterpret_cast(garbage.data()), static_cast(garbage.size())); + f.close(); + + const auto res = MachOPatternScanner::scan_for_pattern_in_file(path, "55 48", "__text"); + EXPECT_FALSE(res.has_value()); +} + +// Test scanning for a non-existent section +TEST(unit_test_macho_pattern_scan_file, ScanNonExistentSection) +{ + constexpr std::string_view path = "./test_minimal_macho_nosect.bin"; + const std::vector bytes = {0x55, 0x48, 0x89, 0xE5}; + ASSERT_TRUE(write_minimal_macho64_file(path.data(), bytes)); + + const auto res = MachOPatternScanner::scan_for_pattern_in_file(path, "55 48", "__nonexistent"); + EXPECT_FALSE(res.has_value()); +} + +// Test scanning with null module base address +TEST(unit_test_macho_pattern_scan_loaded, ScanNullModule) +{ + const auto res = MachOPatternScanner::scan_for_pattern_in_loaded_module(nullptr, "55 48", "__text"); + EXPECT_FALSE(res.has_value()); +} + +// Test scanning in loaded module with invalid magic +TEST(unit_test_macho_pattern_scan_loaded, ScanInvalidMagic) +{ + std::vector invalid_data(256, 0x00); + const auto res = MachOPatternScanner::scan_for_pattern_in_loaded_module(invalid_data.data(), "55 48", "__text"); + EXPECT_FALSE(res.has_value()); +} diff --git a/tests/general/unit_test_mat.cpp b/tests/general/unit_test_mat.cpp new file mode 100644 index 00000000..7cfc9c1d --- /dev/null +++ b/tests/general/unit_test_mat.cpp @@ -0,0 +1,243 @@ +// UnitTestMat.cpp +#include "omath/linear_algebra/mat.hpp" +#include "omath/linear_algebra/vector3.hpp" +#include + +using namespace omath; + +class UnitTestMat : public ::testing::Test +{ +protected: + Mat<2, 2> m1; + Mat<2, 2> m2; + + void SetUp() override + { + m1 = Mat<2, 2>(); + m2 = Mat<2, 2>{{1.0f, 2.0f}, {3.0f, 4.0f}}; + } +}; + +// Test constructors +TEST_F(UnitTestMat, Constructor_Default) +{ + Mat<3, 3> m; + EXPECT_EQ(m.row_count(), 3); + EXPECT_EQ(m.columns_count(), 3); + for (size_t i = 0; i < 3; ++i) + for (size_t j = 0; j < 3; ++j) + EXPECT_FLOAT_EQ(m.at(i, j), 0.0f); +} + +TEST_F(UnitTestMat, Constructor_InitializerList) +{ + constexpr Mat<2, 2> m{{1.0f, 2.0f}, {3.0f, 4.0f}}; + EXPECT_EQ(m.row_count(), 2); + EXPECT_EQ(m.columns_count(), 2); + EXPECT_FLOAT_EQ(m.at(0, 0), 1.0f); + EXPECT_FLOAT_EQ(m.at(0, 1), 2.0f); + EXPECT_FLOAT_EQ(m.at(1, 0), 3.0f); + EXPECT_FLOAT_EQ(m.at(1, 1), 4.0f); +} + +TEST_F(UnitTestMat, Operator_SquareBrackets) +{ + EXPECT_EQ((m2[0, 0]), 1.0f); + EXPECT_EQ((m2[0, 1]), 2.0f); + EXPECT_EQ((m2[1, 0]), 3.0f); + EXPECT_EQ((m2[1, 1]), 4.0f); +} + +TEST_F(UnitTestMat, Constructor_Copy) +{ + Mat<2, 2> m3 = m2; + EXPECT_EQ(m3.row_count(), m2.row_count()); + EXPECT_EQ(m3.columns_count(), m2.columns_count()); + EXPECT_FLOAT_EQ(m3.at(0, 0), m2.at(0, 0)); + EXPECT_FLOAT_EQ(m3.at(1, 1), m2.at(1, 1)); +} + +TEST_F(UnitTestMat, Constructor_Move) +{ + Mat<2, 2> m3 = std::move(m2); + EXPECT_EQ(m3.row_count(), 2); + EXPECT_EQ(m3.columns_count(), 2); + EXPECT_FLOAT_EQ(m3.at(0, 0), 1.0f); + EXPECT_FLOAT_EQ(m3.at(1, 1), 4.0f); + // m2 is in a valid but unspecified state after move +} + +// Test matrix operations +TEST_F(UnitTestMat, Operator_Multiplication_Matrix) +{ + Mat<2, 2> m3 = m2 * m2; + EXPECT_EQ(m3.row_count(), 2); + EXPECT_EQ(m3.columns_count(), 2); + EXPECT_FLOAT_EQ(m3.at(0, 0), 7.0f); + EXPECT_FLOAT_EQ(m3.at(0, 1), 10.0f); + EXPECT_FLOAT_EQ(m3.at(1, 0), 15.0f); + EXPECT_FLOAT_EQ(m3.at(1, 1), 22.0f); +} + +TEST_F(UnitTestMat, Operator_Multiplication_Scalar) +{ + Mat<2, 2> m3 = m2 * 2.0f; + EXPECT_FLOAT_EQ(m3.at(0, 0), 2.0f); + EXPECT_FLOAT_EQ(m3.at(1, 1), 8.0f); +} + +TEST_F(UnitTestMat, Operator_Division_Scalar) +{ + Mat<2, 2> m3 = m2 / 2.0f; + EXPECT_FLOAT_EQ(m3.at(0, 0), 0.5f); + EXPECT_FLOAT_EQ(m3.at(1, 1), 2.0f); +} + +// Test matrix functions +TEST_F(UnitTestMat, Transpose) +{ + Mat<2, 2> m3 = m2.transposed(); + EXPECT_FLOAT_EQ(m3.at(0, 0), m2.at(0, 0)); + EXPECT_FLOAT_EQ(m3.at(0, 1), m2.at(1, 0)); + EXPECT_FLOAT_EQ(m3.at(1, 0), m2.at(0, 1)); + EXPECT_FLOAT_EQ(m3.at(1, 1), m2.at(1, 1)); +} + +TEST_F(UnitTestMat, Determinant) +{ + const float det = m2.determinant(); + EXPECT_FLOAT_EQ(det, -2.0f); +} + +TEST_F(UnitTestMat, Sum) +{ + const float sum = m2.sum(); + EXPECT_FLOAT_EQ(sum, 10.0f); +} + +TEST_F(UnitTestMat, Clear) +{ + m2.clear(); + for (size_t i = 0; i < m2.row_count(); ++i) + for (size_t j = 0; j < m2.columns_count(); ++j) + EXPECT_FLOAT_EQ(m2.at(i, j), 0.0f); +} + +TEST_F(UnitTestMat, ToString) +{ + const std::string str = std::format("{}", m2); + EXPECT_FALSE(str.empty()); + EXPECT_EQ(str, "[[ 1.000, 2.000]\n [ 3.000, 4.000]]"); +} + +// Test assignment operators +TEST_F(UnitTestMat, AssignmentOperator_Copy) +{ + Mat<2, 2> m3; + m3 = m2; + EXPECT_EQ(m3.row_count(), m2.row_count()); + EXPECT_EQ(m3.columns_count(), m2.columns_count()); + EXPECT_FLOAT_EQ(m3.at(0, 0), m2.at(0, 0)); +} + +TEST_F(UnitTestMat, AssignmentOperator_Move) +{ + Mat<2, 2> m3; + m3 = std::move(m2); + EXPECT_EQ(m3.row_count(), 2); + EXPECT_EQ(m3.columns_count(), 2); + EXPECT_FLOAT_EQ(m3.at(0, 0), 1.0f); + EXPECT_FLOAT_EQ(m3.at(1, 1), 4.0f); + // m2 is in a valid but unspecified state after move +} + +// Test static methods +TEST_F(UnitTestMat, StaticMethod_ToScreenMat) +{ + 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); +} + + +// Test exception handling in At() method +TEST_F(UnitTestMat, Method_At_OutOfRange) +{ +#if !defined(NDEBUG) && defined(OMATH_SUPRESS_SAFETY_CHECKS) + EXPECT_THROW(std::ignore = m2.at(2, 0), std::out_of_range); + EXPECT_THROW(std::ignore = m2.at(0, 2), std::out_of_range); +#endif +} + +// Test Determinant for 3x3 matrix +TEST(UnitTestMatStandalone, Determinant_3x3) +{ + constexpr auto det = Mat<3, 3>{{6, 1, 1}, {4, -2, 5}, {2, 8, 7}}.determinant(); + EXPECT_FLOAT_EQ(det, -306.0f); +} + +// Test Minor for 3x3 matrix +TEST(UnitTestMatStandalone, Strip_3x3) +{ + constexpr Mat<3, 3> m{{3, 0, 2}, {2, 0, -2}, {0, 1, 1}}; + auto minor = m.strip(0, 0); + EXPECT_EQ(minor.row_count(), 2); + EXPECT_EQ(minor.columns_count(), 2); + EXPECT_FLOAT_EQ(minor.at(0, 0), 0.0f); + EXPECT_FLOAT_EQ(minor.at(0, 1), -2.0f); + EXPECT_FLOAT_EQ(minor.at(1, 0), 1.0f); + EXPECT_FLOAT_EQ(minor.at(1, 1), 1.0f); +} + +// Test Transpose for non-square matrix +TEST(UnitTestMatStandalone, Transpose_NonSquare) +{ + constexpr Mat<2, 3> m{{1.0f, 2.0f, 3.0f}, {4.0f, 5.0f, 6.0f}}; + auto transposed = m.transposed(); + EXPECT_EQ(transposed.row_count(), 3); + EXPECT_EQ(transposed.columns_count(), 2); + EXPECT_FLOAT_EQ(transposed.at(0, 0), 1.0f); + EXPECT_FLOAT_EQ(transposed.at(1, 0), 2.0f); + EXPECT_FLOAT_EQ(transposed.at(2, 0), 3.0f); + EXPECT_FLOAT_EQ(transposed.at(0, 1), 4.0f); + EXPECT_FLOAT_EQ(transposed.at(1, 1), 5.0f); + EXPECT_FLOAT_EQ(transposed.at(2, 1), 6.0f); +} + +TEST(UnitTestMatStandalone, Enverse) +{ + constexpr Mat<2, 2> m{{1.0f, 3.0f}, {2.0f, 5.0f}}; + constexpr Mat<2,2> mv{{-5.0f, 3.0f}, {2.0f, -1.0f}}; + + EXPECT_EQ(mv, m.inverted()); +} + +TEST(UnitTestMatStandalone, Equanity) +{ + constexpr omath::Vector3 left_handed = {0, 2, 10}; + constexpr omath::Vector3 right_handed = {0, 2, -10}; + + 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); + + ndc_left_handed /= ndc_left_handed.at(3, 0); + ndc_right_handed /= ndc_right_handed.at(3, 0); + + 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({0, 0, 0.1001}); + + projected /= projected.at(3, 0); + + EXPECT_TRUE(projected.at(2, 0) > -1.0f && projected.at(2, 0) < 0.f); +} \ No newline at end of file diff --git a/tests/general/unit_test_mat_coverage_extra.cpp b/tests/general/unit_test_mat_coverage_extra.cpp new file mode 100644 index 00000000..bddb26ee --- /dev/null +++ b/tests/general/unit_test_mat_coverage_extra.cpp @@ -0,0 +1,24 @@ +// Added to exercise Mat initializer-list exception branches and determinant fallback +#include +#include + +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); +} diff --git a/tests/general/unit_test_mat_more.cpp b/tests/general/unit_test_mat_more.cpp new file mode 100644 index 00000000..bb023fc4 --- /dev/null +++ b/tests/general/unit_test_mat_more.cpp @@ -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); +} diff --git a/tests/general/unit_test_navigation_mesh.cpp b/tests/general/unit_test_navigation_mesh.cpp new file mode 100644 index 00000000..b5ba7b0f --- /dev/null +++ b/tests/general/unit_test_navigation_mesh.cpp @@ -0,0 +1,33 @@ +#include +#include "omath/pathfinding/navigation_mesh.hpp" + +using namespace omath; +using namespace omath::pathfinding; + +TEST(NavigationMeshTests, SerializeDeserializeRoundTrip) +{ + NavigationMesh nav; + Vector3 a{0.f,0.f,0.f}; + Vector3 b{1.f,0.f,0.f}; + Vector3 c{0.f,1.f,0.f}; + + nav.m_vertex_map.emplace(a, std::vector>{b,c}); + nav.m_vertex_map.emplace(b, std::vector>{a}); + nav.m_vertex_map.emplace(c, std::vector>{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 p{5.f,5.f,5.f}; + const auto res = nav.get_closest_vertex(p); + EXPECT_FALSE(res.has_value()); +} diff --git a/tests/general/unit_test_pattern_scan.cpp b/tests/general/unit_test_pattern_scan.cpp new file mode 100644 index 00000000..b974c401 --- /dev/null +++ b/tests/general/unit_test_pattern_scan.cpp @@ -0,0 +1,53 @@ +// +// Created by Vlad on 10/4/2025. +// +#include "omath/utility/pe_pattern_scan.hpp" +#include "gtest/gtest.h" +#include +#include +#include +TEST(unit_test_pattern_scan, read_test) +{ + const auto result = omath::PatternScanner::parse_pattern("FF ? ?? E9"); + + EXPECT_EQ(result->at(0), static_cast(0xFF)); + EXPECT_EQ(result->at(1), std::nullopt); + EXPECT_EQ(result->at(2), std::nullopt); + EXPECT_EQ(result->at(3), static_cast(0xE9)); +} + +TEST(unit_test_pattern_scan, corner_case_1) +{ + const auto result = omath::PatternScanner::parse_pattern(" FF ? ?? E9"); + + EXPECT_EQ(result->at(0), static_cast(0xFF)); + EXPECT_EQ(result->at(1), std::nullopt); + EXPECT_EQ(result->at(2), std::nullopt); + EXPECT_EQ(result->at(3), static_cast(0xE9)); +} + +TEST(unit_test_pattern_scan, corner_case_2) +{ + const auto result = omath::PatternScanner::parse_pattern(" FF ? ?? E9 "); + + EXPECT_EQ(result->at(0), static_cast(0xFF)); + EXPECT_EQ(result->at(1), std::nullopt); + EXPECT_EQ(result->at(2), std::nullopt); + EXPECT_EQ(result->at(3), static_cast(0xE9)); +} + +TEST(unit_test_pattern_scan, corner_case_3) +{ + const auto result = omath::PatternScanner::parse_pattern(" FF ? ?? E9 "); + + EXPECT_EQ(result->at(0), static_cast(0xFF)); + EXPECT_EQ(result->at(1), std::nullopt); + EXPECT_EQ(result->at(2), std::nullopt); + EXPECT_EQ(result->at(3), static_cast(0xE9)); +} + +TEST(unit_test_pattern_scan, corner_case_4) +{ + const auto result = omath::PatternScanner::parse_pattern("X ? ?? E9 "); + EXPECT_FALSE(result.has_value()); +} \ No newline at end of file diff --git a/tests/general/unit_test_pattern_scan_extra.cpp b/tests/general/unit_test_pattern_scan_extra.cpp new file mode 100644 index 00000000..cb371731 --- /dev/null +++ b/tests/general/unit_test_pattern_scan_extra.cpp @@ -0,0 +1,31 @@ +// Extra tests for PatternScanner behavior +#include +#include + +using namespace omath; + +TEST(unit_test_pattern_scan_extra, IteratorScanFound) +{ + std::vector buf = {static_cast(0xDE), static_cast(0xAD), + static_cast(0xBE), static_cast(0xEF), + static_cast(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 buf = {static_cast(0x00), static_cast(0x11), + static_cast(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 buf = {static_cast(0x00), static_cast(0x11)}; + const auto it = PatternScanner::scan_for_pattern(buf.begin(), buf.end(), "GG HH"); + EXPECT_EQ(it, buf.end()); +} diff --git a/tests/general/unit_test_pe_pattern_scan_extra.cpp b/tests/general/unit_test_pe_pattern_scan_extra.cpp new file mode 100644 index 00000000..d06b332f --- /dev/null +++ b/tests/general/unit_test_pe_pattern_scan_extra.cpp @@ -0,0 +1,11 @@ +// Tests for PePatternScanner basic behavior +#include +#include + +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()); +} diff --git a/tests/general/unit_test_pe_pattern_scan_file.cpp b/tests/general/unit_test_pe_pattern_scan_file.cpp new file mode 100644 index 00000000..5e2abb84 --- /dev/null +++ b/tests/general/unit_test_pe_pattern_scan_file.cpp @@ -0,0 +1,114 @@ +// Unit test for PePatternScanner::scan_for_pattern_in_file using a synthetic PE-like file +#include +#include +#include +#include +#include +#include + +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& 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 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(dos.data()), dos.size()); + + // Pad up to e_lfanew + if (f.tellp() < static_cast(e_lfanew)) + { + std::vector pad(e_lfanew - static_cast(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(&machine), sizeof(machine)); + f.write(reinterpret_cast(&num_sections), sizeof(num_sections)); + f.write(reinterpret_cast(&dummy32), sizeof(dummy32)); + f.write(reinterpret_cast(&dummy32b), sizeof(dummy32b)); + std::uint32_t num_symbols = 0; + f.write(reinterpret_cast(&num_symbols), sizeof(num_symbols)); + f.write(reinterpret_cast(&size_optional), sizeof(size_optional)); + f.write(reinterpret_cast(&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(&magic), sizeof(magic)); + // filler for rest of optional header up to size_optional + std::vector 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(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 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(section_bytes.data()), static_cast(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(section_bytes.size()); + constexpr std::uint32_t virtual_address = 0x1000u; + const std::uint32_t size_raw_data = static_cast(section_bytes.size()); + const std::uint32_t ptr_raw_data = static_cast(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(&virtual_size), sizeof(virtual_size)); + f.write(reinterpret_cast(&virtual_address), sizeof(virtual_address)); + f.write(reinterpret_cast(&size_raw_data), sizeof(size_raw_data)); + f.write(reinterpret_cast(&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 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 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()); +} diff --git a/tests/general/unit_test_pe_pattern_scan_loaded.cpp b/tests/general/unit_test_pe_pattern_scan_loaded.cpp new file mode 100644 index 00000000..4db2b571 --- /dev/null +++ b/tests/general/unit_test_pe_pattern_scan_loaded.cpp @@ -0,0 +1,98 @@ +// Tests for PePatternScanner::scan_for_pattern_in_loaded_module +#include +#include +#include +#include +#include + +using namespace omath; + +static std::vector make_fake_module(std::uint32_t base_of_code, + std::uint32_t size_code, + const std::vector& code_bytes) +{ + // Constants + constexpr std::uint32_t e_lfanew = 0x80; + constexpr std::uint32_t nt_sig = 0x4550; // "PE\0\0" + constexpr std::uint16_t opt_magic = 0x020B; // PE32+ + constexpr std::uint16_t num_sections = 1; + constexpr std::uint16_t opt_hdr_size = 0xF0; // Standard PE32+ optional header size + constexpr std::uint32_t section_table_off = e_lfanew + 4 + 20 + opt_hdr_size; // sig(4) + FileHdr(20) + constexpr std::uint32_t section_header_size = 40; + constexpr std::uint32_t text_characteristics = 0x60000020; // code | execute | read + + const std::uint32_t headers_end = section_table_off + section_header_size; + const std::uint32_t code_end = base_of_code + size_code; + const std::uint32_t total_size = std::max(headers_end, code_end) + 0x100; // leave some padding + std::vector buf(total_size, 0); + + auto w16 = [&](std::size_t off, std::uint16_t v) { std::memcpy(buf.data() + off, &v, sizeof(v)); }; + auto w32 = [&](std::size_t off, std::uint32_t v) { std::memcpy(buf.data() + off, &v, sizeof(v)); }; + auto w64 = [&](std::size_t off, std::uint64_t v) { std::memcpy(buf.data() + off, &v, sizeof(v)); }; + + // DOS header + w16(0x00, 0x5A4D); // e_magic "MZ" + w32(0x3C, e_lfanew); // e_lfanew + + // NT signature + w32(e_lfanew, nt_sig); + + // FileHeader (starts at e_lfanew + 4) + const std::size_t fh_off = e_lfanew + 4; + w16(fh_off + 2, num_sections); // NumberOfSections + w16(fh_off + 16, opt_hdr_size); // SizeOfOptionalHeader + + // OptionalHeader PE32+ (starts at e_lfanew + 4 + 20) + const std::size_t opt_off = fh_off + 20; + w16(opt_off + 0, opt_magic); // Magic + w32(opt_off + 4, size_code); // SizeOfCode + w32(opt_off + 16, 0); // AddressOfEntryPoint (unused in test) + w32(opt_off + 20, base_of_code); // BaseOfCode + w64(opt_off + 24, 0); // ImageBase + w32(opt_off + 32, 0x1000); // SectionAlignment + w32(opt_off + 36, 0x200); // FileAlignment + w32(opt_off + 56, code_end); // SizeOfImage (simple upper bound) + w32(opt_off + 60, headers_end); // SizeOfHeaders + w32(opt_off + 108, 0); // NumberOfRvaAndSizes (0 directories) + + // Section header (.text) at section_table_off + const std::size_t sh_off = section_table_off; + std::memcpy(buf.data() + sh_off + 0, ".text", 5); // Name[8] + w32(sh_off + 8, size_code); // VirtualSize + w32(sh_off + 12, base_of_code); // VirtualAddress + w32(sh_off + 16, size_code); // SizeOfRawData + w32(sh_off + 20, base_of_code); // PointerToRawData + w32(sh_off + 36, text_characteristics); // Characteristics + + // Place code bytes at BaseOfCode + 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 code = {0x90, 0x01, 0x02, 0x03, 0x04}; + auto buf = make_fake_module(0x200, static_cast(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(buf.data()); + EXPECT_EQ(addr - base, 0x200u); +} + +TEST(PePatternScanLoaded, WildcardMatches) +{ + const std::vector code = {0xDE, 0xAD, 0xBE, 0xEF}; + constexpr std::uint32_t base_of_code = 0x300; + auto buf = make_fake_module(base_of_code, static_cast(code.size()), code); + + const auto res = PePatternScanner::scan_for_pattern_in_loaded_module(buf.data(), "DE ?? BE", ".text"); + ASSERT_TRUE(res.has_value()); + const uintptr_t addr = res.value(); + const uintptr_t base = reinterpret_cast(buf.data()); + EXPECT_EQ(addr - base, base_of_code); +} \ No newline at end of file diff --git a/tests/general/unit_test_pe_pattern_scan_more.cpp b/tests/general/unit_test_pe_pattern_scan_more.cpp new file mode 100644 index 00000000..20265a70 --- /dev/null +++ b/tests/general/unit_test_pe_pattern_scan_more.cpp @@ -0,0 +1,233 @@ +// Additional tests for PePatternScanner to exercise edge cases and loaded-module scanning +#include +#include +#include +#include +#include +#include + +using namespace omath; + +static bool write_bytes(const std::string& path, const std::vector& data) +{ + std::ofstream f(path, std::ios::binary); + if (!f.is_open()) + return false; + f.write(reinterpret_cast(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 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 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 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(dos.data()), dos.size()); + // pad + std::vector pad(e_lfanew - static_cast(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(&machine), sizeof(machine)); + f.write(reinterpret_cast(&num_sections), sizeof(num_sections)); + f.write(reinterpret_cast(&z), sizeof(z)); + f.write(reinterpret_cast(&z2), sizeof(z2)); + f.write(reinterpret_cast(&numsym), sizeof(numsym)); + f.write(reinterpret_cast(&size_opt), sizeof(size_opt)); + f.write(reinterpret_cast(&ch), sizeof(ch)); + // Optional header magic + std::uint16_t magic = 0x20b; + f.write(reinterpret_cast(&magic), sizeof(magic)); + std::vector opt(size_opt - sizeof(magic), 0); + f.write(reinterpret_cast(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(&vs), 4); + f.write(reinterpret_cast(&va), 4); + f.write(reinterpret_cast(&srd), 4); + f.write(reinterpret_cast(&prd), 4); + std::vector rest(16, 0); + f.write(rest.data(), rest.size()); + // section bytes + std::vector sec = {0x00, 0x01, 0x02, 0x03}; + f.write(reinterpret_cast(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 SectionHeader + { + char name[8]; + union + { + std::uint32_t physical_address; + std::uint32_t virtual_size; + }; + std::uint32_t virtual_address; + std::uint32_t size_raw_data; + std::uint32_t ptr_raw_data; + std::uint32_t ptr_relocs; + std::uint32_t ptr_line_numbers; + std::uint32_t num_relocs; + std::uint32_t num_line_numbers; + std::uint32_t characteristics; + }; + struct ImageNtHeadersX64 + { + std::uint32_t signature; + FileHeader file_header; + OptionalHeaderX64 optional_header; + }; + + const std::vector 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(pattern_bytes.size()); + + const std::uint32_t bufsize = 0x400 + size_code; + std::vector buf(bufsize, 0); + + // DOS header + const auto dos = reinterpret_cast(buf.data()); + dos->e_magic = 0x5A4D; + dos->e_lfanew = 0x80; + + // NT headers + const auto nt = reinterpret_cast(buf.data() + dos->e_lfanew); + nt->signature = 0x4550; // 'PE\0\0' + nt->file_header.machine = 0x8664; + nt->file_header.num_sections = 1; + nt->file_header.size_optional_header = static_cast(sizeof(OptionalHeaderX64)); + + nt->optional_header.magic = 0x020B; // x64 + nt->optional_header.base_of_code = base_of_code; + nt->optional_header.size_code = size_code; + + // Compute section table offset: e_lfanew + 4 (sig) + FileHeader + OptionalHeader + const std::size_t section_table_off = + static_cast(dos->e_lfanew) + 4 + sizeof(FileHeader) + sizeof(OptionalHeaderX64); + nt->optional_header.size_headers = static_cast(section_table_off + sizeof(SectionHeader)); + + // Section header (.text) + const auto sect = reinterpret_cast(buf.data() + section_table_off); + std::memset(sect, 0, sizeof(SectionHeader)); + std::memcpy(sect->name, ".text", 5); + sect->virtual_size = size_code; + sect->virtual_address = base_of_code; + sect->size_raw_data = size_code; + sect->ptr_raw_data = base_of_code; + sect->characteristics = 0x60000020; // code | execute | read + + // 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", ".text"); + EXPECT_TRUE(res.has_value()); +} diff --git a/tests/general/unit_test_pe_pattern_scan_more2.cpp b/tests/general/unit_test_pe_pattern_scan_more2.cpp new file mode 100644 index 00000000..7b4ddeb4 --- /dev/null +++ b/tests/general/unit_test_pe_pattern_scan_more2.cpp @@ -0,0 +1,294 @@ +#include +#include +#include +#include +#include +#include + +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& data) +{ + std::ofstream f(path, std::ios::binary); + if (!f.is_open()) + return false; + f.write(reinterpret_cast(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& 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 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(dos.data()), dos.size()); + + // Pad up to e_lfanew + if (f.tellp() < static_cast(e_lfanew)) + { + std::vector pad(e_lfanew - static_cast(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(&machine), sizeof(machine)); + f.write(reinterpret_cast(&num_sections), sizeof(num_sections)); + f.write(reinterpret_cast(&dummy32), sizeof(dummy32)); + f.write(reinterpret_cast(&dummy32b), sizeof(dummy32b)); + std::uint32_t num_symbols = 0; + f.write(reinterpret_cast(&num_symbols), sizeof(num_symbols)); + f.write(reinterpret_cast(&size_optional), sizeof(size_optional)); + f.write(reinterpret_cast(&characteristics), sizeof(characteristics)); + + // OptionalHeader minimal filler + std::uint16_t magic = 0x20b; + f.write(reinterpret_cast(&magic), sizeof(magic)); + std::vector opt(size_optional - sizeof(magic), 0); + f.write(reinterpret_cast(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 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(section_bytes.data()), static_cast(section_bytes.size())); + + // Patch section header fields + const std::uint32_t virtual_size = static_cast(section_bytes.size()); + constexpr std::uint32_t virtual_address = 0x1000u; + const std::uint32_t size_raw_data = static_cast(section_bytes.size()); + const std::uint32_t ptr_raw_data = static_cast(data_pos); + + f.seekp(header_rest_pos, std::ios::beg); + f.write(reinterpret_cast(&virtual_size), sizeof(virtual_size)); + f.write(reinterpret_cast(&virtual_address), sizeof(virtual_address)); + f.write(reinterpret_cast(&size_raw_data), sizeof(size_raw_data)); + f.write(reinterpret_cast(&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 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(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 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 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 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 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 filebuf((std::istreambuf_iterator(in)), std::istreambuf_iterator()); + 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(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 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 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 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()); +} diff --git a/tests/general/unit_test_pred_engine_trait.cpp b/tests/general/unit_test_pred_engine_trait.cpp new file mode 100644 index 00000000..8ea766b7 --- /dev/null +++ b/tests/general/unit_test_pred_engine_trait.cpp @@ -0,0 +1,66 @@ +// Tests for PredEngineTrait +#include +#include +#include +#include + +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 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 predicted{10.f, 0.f, 0.f}; + constexpr std::optional 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 origin{0.f, 0.f, 0.f}; + constexpr Vector3 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); +} diff --git a/tests/general/unit_test_prediction.cpp b/tests/general/unit_test_prediction.cpp new file mode 100644 index 00000000..a0c22d5b --- /dev/null +++ b/tests/general/unit_test_prediction.cpp @@ -0,0 +1,18 @@ +#include +#include +#include +TEST(UnitTestPrediction, PredictionTest) +{ + constexpr omath::projectile_prediction::Target target{ + .m_origin = {100, 0, 90}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; + constexpr omath::projectile_prediction::Projectile proj = { + .m_origin = {3, 2, 1}, .m_launch_speed = 5000, .m_gravity_scale = 0.4}; + const auto viewPoint = + omath::projectile_prediction::ProjPredEngineLegacy(400, 1.f / 1000.f, 50, 5.f).maybe_calculate_aim_point(proj, target); + + + const auto [pitch, yaw, _] =omath::source_engine::CameraTrait::calc_look_at_angle(proj.m_origin, viewPoint.value()); + + EXPECT_NEAR(-42.547142, pitch.as_degrees(), 0.01f); + EXPECT_NEAR(-1.181189, yaw.as_degrees(), 0.01f); +} diff --git a/tests/general/unit_test_primitive_box.cpp b/tests/general/unit_test_primitive_box.cpp new file mode 100644 index 00000000..c4a9a0dc --- /dev/null +++ b/tests/general/unit_test_primitive_box.cpp @@ -0,0 +1,16 @@ +// +// Created by Vladislav on 11.01.2026. +// +#include "omath/3d_primitives/box.hpp" +#include "omath/collision/line_tracer.hpp" +#include "omath/engines/opengl_engine/primitives.hpp" +#include + +TEST(test, test) +{ + auto result = omath::primitives::create_box( + {0.f, 30.f, 0.f}, {}, omath::opengl_engine::k_abs_forward, omath::opengl_engine::k_abs_right); + + omath::collision::Ray ray{.start = {0, 0, 0}, .end = {-100, 0, 0}}; + std::ignore = omath::collision::LineTracer<>::get_ray_hit_point(ray, result); +} \ No newline at end of file diff --git a/tests/general/unit_test_proj_pred_engine_legacy_more.cpp b/tests/general/unit_test_proj_pred_engine_legacy_more.cpp new file mode 100644 index 00000000..26080b42 --- /dev/null +++ b/tests/general/unit_test_proj_pred_engine_legacy_more.cpp @@ -0,0 +1,100 @@ +#include +#include +#include +#include +#include + +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 predict_target_position(const Target& t, float /*time*/, float /*gravity*/) noexcept + { + return t.m_origin; + } + static Vector3 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{100.f, 0.f, 0.f}; + } + static float calc_vector_2d_distance(const Vector3& v) noexcept { return std::hypot(v.x, v.y); } + static float get_vector_height_coordinate(const Vector3& v) noexcept { return v.z; } + static Vector3 calc_viewpoint_from_angles(const Projectile& /*p*/, Vector3 /*v*/, std::optional /*maybe_pitch*/) noexcept + { + return Vector3{1.f, 2.f, 3.f}; + } + static float calc_direct_pitch_angle(const Vector3& /*a*/, const Vector3& /*b*/) noexcept { return 12.5f; } + static float calc_direct_yaw_angle(const Vector3& /*a*/, const Vector3& /*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; + 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 predict_target_position(const Target& t, float /*time*/, float /*gravity*/) noexcept { return t.m_origin; } + static Vector3 predict_projectile_position(const Projectile& /*p*/, float /*pitch*/, float /*yaw*/, float /*time*/, float /*gravity*/) noexcept { return Vector3{0.f,0.f,0.f}; } + static float calc_vector_2d_distance(const Vector3& /*v*/) noexcept { return 10000.f; } + static float get_vector_height_coordinate(const Vector3& /*v*/) noexcept { return 0.f; } + static Vector3 calc_viewpoint_from_angles(const Projectile& /*p*/, Vector3 /*v*/, std::optional /*maybe_pitch*/) noexcept { return Vector3{}; } + static float calc_direct_pitch_angle(const Vector3& /*a*/, const Vector3& /*b*/) noexcept { return 0.f; } + static float calc_direct_yaw_angle(const Vector3& /*a*/, const Vector3& /*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; + 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 predict_target_position(const Target& t, float /*time*/, float /*gravity*/) noexcept { return t.m_origin; } + static Vector3 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{0.f, 0.f, 1000.f}; + } + static float calc_vector_2d_distance(const Vector3& v) noexcept { return std::hypot(v.x, v.y); } + static float get_vector_height_coordinate(const Vector3& v) noexcept { return v.z; } + static Vector3 calc_viewpoint_from_angles(const Projectile& /*p*/, Vector3 /*v*/, std::optional /*maybe_pitch*/) noexcept { return Vector3{9.f,9.f,9.f}; } + static float calc_direct_pitch_angle(const Vector3& /*a*/, const Vector3& /*b*/) noexcept { return 1.f; } + static float calc_direct_yaw_angle(const Vector3& /*a*/, const Vector3& /*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; + 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()); +} diff --git a/tests/general/unit_test_projection.cpp b/tests/general/unit_test_projection.cpp new file mode 100644 index 00000000..f507b01d --- /dev/null +++ b/tests/general/unit_test_projection.cpp @@ -0,0 +1,99 @@ +// +// Created by Vlad on 27.08.2024. +// +#include "omath/engines/unity_engine/camera.hpp" +#include +#include +#include +#include +#include +#include + +TEST(UnitTestProjection, Projection) +{ + constexpr auto fov = omath::Angle::from_degrees(90.f); + const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov, + 0.01f, 1000.f); + + const auto projected = cam.world_to_screen({1000.f, 0, 50.f}); + const auto result = cam.screen_to_world(projected.value()); + const auto result2 = cam.world_to_screen(result.value()); + + EXPECT_EQ(static_cast>(projected.value()), + static_cast>(result2.value())); + EXPECT_NEAR(projected->x, 960.f, 0.001f); + EXPECT_NEAR(projected->y, 504.f, 0.001f); + EXPECT_NEAR(projected->z, 1.f, 0.001f); +} +TEST(UnitTestProjection, ScreenToNdcTopLeft) +{ + constexpr auto fov = omath::Angle::from_degrees(90.f); + const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov, + 0.01f, 1000.f); + using ScreenStart = omath::source_engine::Camera::ScreenStart; + + const auto ndc_top_left = cam.screen_to_ndc({1500, 300, 1.f}); + EXPECT_NEAR(ndc_top_left.x, 0.5625f, 0.0001f); + EXPECT_NEAR(ndc_top_left.y, 0.4444f, 0.0001f); +} + +TEST(UnitTestProjection, ScreenToNdcBottomLeft) +{ + constexpr auto fov = omath::projection::FieldOfView::from_degrees(60.f); + + const auto cam = omath::unity_engine::Camera({0, 0, 0}, {}, {1280.f, 720.f}, fov, 0.03f, 1000.f); + using ScreenStart = omath::unity_engine::Camera::ScreenStart; + + const auto ndc_bottom_left = + cam.screen_to_ndc({1263.53833f, 547.061523f, 0.99405992f}); + EXPECT_NEAR(ndc_bottom_left.x, 0.974278628f, 0.0001f); + EXPECT_NEAR(ndc_bottom_left.y, 0.519615293f, 0.0001f); +} + +TEST(UnitTestProjection, ScreenToWorldTopLeftCorner) +{ + std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source + + std::uniform_real_distribution dist_x(1.f, 1900.f); + std::uniform_real_distribution dist_y(1.f, 1070.f); + + constexpr auto fov = omath::Angle::from_degrees(90.f); + const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov, + 0.01f, 1000.f); + using ScreenStart = omath::source_engine::Camera::ScreenStart; + + for (int i = 0; i < 100; i++) + { + const auto initial_screen_cords = omath::Vector2{dist_x(gen), dist_y(gen)}; + + const auto world_cords = cam.screen_to_world(initial_screen_cords); + const auto screen_cords = cam.world_to_screen(world_cords.value()); + + EXPECT_NEAR(screen_cords->x, initial_screen_cords.x, 0.001f); + EXPECT_NEAR(screen_cords->y, initial_screen_cords.y, 0.001f); + } +} + +TEST(UnitTestProjection, ScreenToWorldBottomLeftCorner) +{ + std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source + + std::uniform_real_distribution dist_x(1.f, 1900.f); + std::uniform_real_distribution dist_y(1.f, 1070.f); + + constexpr auto fov = omath::Angle::from_degrees(90.f); + const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov, + 0.01f, 1000.f); + using ScreenStart = omath::source_engine::Camera::ScreenStart; + + for (int i = 0; i < 100; i++) + { + const auto initial_screen_cords = omath::Vector2{dist_x(gen), dist_y(gen)}; + + const auto world_cords = cam.screen_to_world(initial_screen_cords); + const auto screen_cords = cam.world_to_screen(world_cords.value()); + + EXPECT_NEAR(screen_cords->x, initial_screen_cords.x, 0.001f); + EXPECT_NEAR(screen_cords->y, initial_screen_cords.y, 0.001f); + } +} \ No newline at end of file diff --git a/tests/general/unit_test_reverse_enineering.cpp b/tests/general/unit_test_reverse_enineering.cpp new file mode 100644 index 00000000..b1b0eb4a --- /dev/null +++ b/tests/general/unit_test_reverse_enineering.cpp @@ -0,0 +1,67 @@ +// +// Created by Vlad on 10/4/2025. +// +#include "omath/linear_algebra/vector3.hpp" +#include +#include + +class Player final +{ +public: + [[nodiscard]] virtual int foo() const + { + return 1; + } + [[nodiscard]] virtual int bar() const + { + return 2; + } + omath::Vector3 m_origin{1.f, 2.f, 3.f}; + int m_health{123}; +}; + +class RevPlayer final : omath::rev_eng::InternalReverseEngineeredObject +{ +public: + [[nodiscard]] + omath::Vector3 get_origin() const + { + return get_by_offset>(sizeof(std::uintptr_t)); + } + + [[nodiscard]] + int get_health() const + { + return get_by_offset(sizeof(std::uintptr_t) + sizeof(omath::Vector3)); + } + + [[nodiscard]] + int rev_foo() const + { + return call_virtual_method<0, int>(); + } + + [[nodiscard]] + int rev_bar() const + { + return call_virtual_method<1, int>(); + } + + [[nodiscard]] int rev_bar_const() const + { + return call_virtual_method<1, int>(); + } +}; + +TEST(unit_test_reverse_enineering, read_test) +{ + Player player_original; + const auto player_reversed = reinterpret_cast(&player_original); + + EXPECT_EQ(player_original.m_origin, player_reversed->get_origin()); + EXPECT_EQ(player_original.m_health, player_reversed->get_health()); + + EXPECT_EQ(player_original.bar(), player_reversed->rev_bar()); + EXPECT_EQ(player_original.foo(), player_reversed->rev_foo()); + EXPECT_EQ(player_original.bar(), player_reversed->rev_bar_const()); +} \ No newline at end of file diff --git a/tests/general/unit_test_simplex_additional.cpp b/tests/general/unit_test_simplex_additional.cpp new file mode 100644 index 00000000..4cd9d70d --- /dev/null +++ b/tests/general/unit_test_simplex_additional.cpp @@ -0,0 +1,54 @@ +#include "omath/collision/simplex.hpp" +#include "omath/linear_algebra/vector3.hpp" +#include + +using omath::Vector3; + +TEST(SimplexAdditional, RegionACSelectsAC) +{ + // Construct points that force the Region AC branch where ac points toward the origin + Vector3 a{1.f, 0.f, 0.f}; + Vector3 b{2.f, 0.f, 0.f}; + Vector3 c{0.f, 1.f, 0.f}; + + omath::collision::Simplex> s; + s = { a, b, c }; + + omath::Vector3 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 a{-1.f, 0.f, 0.f}; + Vector3 b{0.f, 1.f, 0.f}; + Vector3 c{0.f, 0.f, 1.f}; + + omath::collision::Simplex> s; + s = { a, b, c }; + + omath::Vector3 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); +} diff --git a/tests/general/unit_test_simplex_more.cpp b/tests/general/unit_test_simplex_more.cpp new file mode 100644 index 00000000..f70ac852 --- /dev/null +++ b/tests/general/unit_test_simplex_more.cpp @@ -0,0 +1,174 @@ +#include "omath/collision/simplex.hpp" +#include "omath/linear_algebra/vector3.hpp" +#include + +using omath::Vector3; +using Simplex = omath::collision::Simplex>; + +TEST(SimplexExtra, HandleLine_CollinearProducesPerp) +{ + // a and b placed so ab points roughly same dir as ao and are collinear + Vector3 a{2.f, 0.f, 0.f}; + Vector3 b{1.f, 0.f, 0.f}; + + Simplex s; + s = {a, b}; + + Vector3 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{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 a{2.f, 0.f, 0.f}; + Vector3 b{1.f, 1.f, 0.f}; + + Simplex s; + s = {a, b}; + + Vector3 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 a{1.f, 0.f, 0.f}; + Vector3 b{0.f, 1.f, 0.f}; + Vector3 c{0.f, -1.f, 0.f}; + + Simplex s; + s = {a, b, c}; + + Vector3 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 a{1.f, 0.f, 0.f}; + Vector3 b{0.f, 1.f, 0.f}; + Vector3 c{0.f, 0.f, 1.f}; + Vector3 d{-0.2f, -0.2f, -0.2f}; + + Simplex s; + s = {a, b, c, d}; + + Vector3 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> s; + s.push_front(omath::Vector3{1.f, 0.f, 0.f}); + s.push_front(omath::Vector3{2.f, 0.f, 0.f}); + s.push_front(omath::Vector3{3.f, 0.f, 0.f}); + + EXPECT_EQ(s.size(), 3u); + constexpr omath::Vector3 exp_front{3.f, 0.f, 0.f}; + constexpr omath::Vector3 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> s; + s.push_front(omath::Vector3{1.f, 1.f, 1.f}); + EXPECT_FALSE(s.empty()); + s.clear(); + EXPECT_TRUE(s.empty()); +} + +TEST(SimplexMore, HandleLineCollinearProducesPerp) +{ + omath::collision::Simplex> s; + s = {omath::Vector3{2.f, 0.f, 0.f}, omath::Vector3{1.f, 0.f, 0.f}}; + omath::Vector3 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 a{1.f, 0.f, 0.f}; + constexpr omath::Vector3 b{0.f, 1.f, 0.f}; + constexpr omath::Vector3 c{0.f, 0.f, 1.f}; + omath::collision::Simplex> s; + s = {a, b, c}; + omath::Vector3 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> s; + s = {omath::Vector3{1.f, 0.f, 0.f}, omath::Vector3{0.f, 1.f, 0.f}, + omath::Vector3{0.f, 0.f, 1.f}, omath::Vector3{-1.f, -1.f, -1.f}}; + omath::Vector3 dir{0.f, 0.f, 0.f}; + const bool inside = s.handle(dir); + EXPECT_TRUE(inside); +} + +TEST(SimplexMore, HandlePointSetsDirection) +{ + omath::collision::Simplex> s; + s = {omath::Vector3{1.f, 2.f, 3.f}}; + omath::Vector3 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> s; + s = {omath::Vector3{1.f, 0.f, 0.f}, omath::Vector3{2.f, 0.f, 0.f}}; + omath::Vector3 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); +} diff --git a/tests/general/unit_test_triangle.cpp b/tests/general/unit_test_triangle.cpp new file mode 100644 index 00000000..f972a8a5 --- /dev/null +++ b/tests/general/unit_test_triangle.cpp @@ -0,0 +1,131 @@ +// +// Created by Orange on 1/6/2025. +// +#include "omath/linear_algebra/triangle.hpp" +#include "omath/linear_algebra/vector3.hpp" +#include // For std::sqrt, std::isinf, std::isnan +#include + +using namespace omath; + +class UnitTestTriangle : public ::testing::Test +{ +protected: + // Define some Triangles to use in tests + Triangle> t1; + Triangle> t2; + Triangle> t3; + + constexpr void SetUp() override + { + // Triangle with vertices (0, 0, 0), (1, 0, 0), (0, 1, 0) + t1 = Triangle>( + Vector3(0.0f, 0.0f, 0.0f), + Vector3(1.0f, 0.0f, 0.0f), + Vector3(0.0f, 1.0f, 0.0f) + ); + + // Triangle with vertices (1, 2, 3), (4, 5, 6), (7, 8, 9) + t2 = Triangle>( + Vector3(1.0f, 2.0f, 3.0f), + Vector3(4.0f, 5.0f, 6.0f), + Vector3(7.0f, 8.0f, 9.0f) + ); + + // An isosceles right triangle + t3 = Triangle>( + Vector3(0.0f, 0.0f, 0.0f), + Vector3(2.0f, 0.0f, 0.0f), + Vector3(0.0f, 2.0f, 0.0f) + ); + } +}; + +// Test constructor and vertices +TEST_F(UnitTestTriangle, Constructor) +{ + constexpr Triangle> t( + Vector3(1.0f, 2.0f, 3.0f), + Vector3(4.0f, 5.0f, 6.0f), + Vector3(7.0f, 8.0f, 9.0f) + ); + + EXPECT_FLOAT_EQ(t.m_vertex1.x, 1.0f); + EXPECT_FLOAT_EQ(t.m_vertex1.y, 2.0f); + EXPECT_FLOAT_EQ(t.m_vertex1.z, 3.0f); + + EXPECT_FLOAT_EQ(t.m_vertex2.x, 4.0f); + EXPECT_FLOAT_EQ(t.m_vertex2.y, 5.0f); + EXPECT_FLOAT_EQ(t.m_vertex2.z, 6.0f); + + EXPECT_FLOAT_EQ(t.m_vertex3.x, 7.0f); + EXPECT_FLOAT_EQ(t.m_vertex3.y, 8.0f); + EXPECT_FLOAT_EQ(t.m_vertex3.z, 9.0f); +} + +// Test CalculateNormal +TEST_F(UnitTestTriangle, CalculateNormal) +{ + // For t1, the normal should point in the +Z direction (0, 0, 1) or (0, 0, -1) + const Vector3 normal_t1 = t1.calculate_normal(); + // Check if it's normalized and pointed along Z (sign can differ, so use absolute check) + EXPECT_NEAR(std::fabs(normal_t1.z), 1.0f, 1e-5f); + EXPECT_NEAR(normal_t1.length(), 1.0f, 1e-5f); + + + // For t3, we expect the normal to be along +Z as well + const Vector3 normal_t3 = t3.calculate_normal(); + EXPECT_NEAR(std::fabs(normal_t3.z), 1.0f, 1e-5f); +} + +// Test side lengths +TEST_F(UnitTestTriangle, SideLengths) +{ + // For t1 side lengths + EXPECT_FLOAT_EQ(t1.side_a_length(), std::sqrt(1.0f)); // distance between (0,0,0) and (1,0,0) + EXPECT_FLOAT_EQ(t1.side_b_length(), std::sqrt(1.0f + 1.0f)); // distance between (4,5,6) & (7,8,9)... but we are testing t1, so let's be accurate: + // Actually, for t1: vertex2=(1,0,0), vertex3=(0,1,0) + // Dist between (0,1,0) and (1,0,0) = sqrt((1-0)^2 + (0-1)^2) = sqrt(1 + 1) = sqrt(2) + EXPECT_FLOAT_EQ(t1.side_b_length(), std::sqrt(2.0f)); + + // For t3, side a = distance between vertex1=(0,0,0) and vertex2=(2,0,0), which is 2 + // side b = distance between vertex3=(0,2,0) and vertex2=(2,0,0), which is sqrt(2^2 + (-2)^2)= sqrt(8)= 2.828... + // We'll just check side a first: + EXPECT_FLOAT_EQ(t3.side_a_length(), 2.0f); + // Then side b: + EXPECT_FLOAT_EQ(t3.side_b_length(), std::sqrt(8.0f)); +} + +// Test side vectors +TEST_F(UnitTestTriangle, SideVectors) +{ + 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 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) +{ + EXPECT_TRUE(Triangle>({2,0,0}, {}, {0,2,0}).is_rectangular()); +} +// Test midpoint +TEST_F(UnitTestTriangle, MidPoint) +{ + // For t1, midpoint of (0,0,0), (1,0,0), (0,1,0) + const Vector3 mid1 = t1.mid_point(); + EXPECT_FLOAT_EQ(mid1.x, (0.0f + 1.0f + 0.0f) / 3.0f); + EXPECT_FLOAT_EQ(mid1.y, (0.0f + 0.0f + 1.0f) / 3.0f); + EXPECT_FLOAT_EQ(mid1.z, 0.0f); + + // For t2, midpoint of (1,2,3), (4,5,6), (7,8,9) + const Vector3 mid2 = t2.mid_point(); + EXPECT_FLOAT_EQ(mid2.x, (1.0f + 4.0f + 7.0f) / 3.0f); + EXPECT_FLOAT_EQ(mid2.y, (2.0f + 5.0f + 8.0f) / 3.0f); + EXPECT_FLOAT_EQ(mid2.z, (3.0f + 6.0f + 9.0f) / 3.0f); +} diff --git a/tests/general/unit_test_var_encryption.cpp b/tests/general/unit_test_var_encryption.cpp new file mode 100644 index 00000000..108e84b4 --- /dev/null +++ b/tests/general/unit_test_var_encryption.cpp @@ -0,0 +1,17 @@ +// +// Created by Vladislav on 04.01.2026. +// +#include "omath/linear_algebra/vector3.hpp" +#include +#include +TEST(Enc, Test) +{ + constexpr omath::Vector3 original = {1.f, 2.f, 3.f}; + OMATH_DEF_CRYPT_VAR(omath::Vector3, 128) var{original}; + { + omath::VarAnchor _ = var.drop_anchor(); + + EXPECT_EQ(original, var.value()); + } + EXPECT_NE(original, var.value()); +} \ No newline at end of file diff --git a/tests/general/unit_test_vector2.cpp b/tests/general/unit_test_vector2.cpp new file mode 100644 index 00000000..1aeb7cdf --- /dev/null +++ b/tests/general/unit_test_vector2.cpp @@ -0,0 +1,375 @@ +// +// Created by Vlad on 02.09.2024. +// + +#include +#include // For FLT_MAX and FLT_MIN +#include // For std::isinf and std::isnan +#include + +using namespace omath; + +class UnitTestVector2 : public ::testing::Test +{ +protected: + Vector2 v1; + Vector2 v2; + + constexpr void SetUp() override + { + v1 = Vector2(1.0f, 2.0f); + v2 = Vector2(4.0f, 5.0f); + } +}; + +// Test constructor and default values +TEST_F(UnitTestVector2, Constructor_Default) +{ + constexpr Vector2 v; + EXPECT_FLOAT_EQ(v.x, 0.0f); + EXPECT_FLOAT_EQ(v.y, 0.0f); +} + +TEST_F(UnitTestVector2, Constructor_Values) +{ + constexpr Vector2 v(1.0f, 2.0f); + EXPECT_FLOAT_EQ(v.x, 1.0f); + EXPECT_FLOAT_EQ(v.y, 2.0f); +} + +// Test equality operators +TEST_F(UnitTestVector2, EqualityOperator) +{ + constexpr Vector2 v3(1.0f, 2.0f); + EXPECT_TRUE(v1 == v3); + EXPECT_FALSE(v1 == v2); +} + +TEST_F(UnitTestVector2, InequalityOperator) +{ + constexpr Vector2 v3(1.0f, 2.0f); + EXPECT_FALSE(v1 != v3); + EXPECT_TRUE(v1 != v2); +} + +// Test arithmetic operators +TEST_F(UnitTestVector2, AdditionOperator) +{ + constexpr Vector2 v3 = Vector2(1.0f, 2.0f) + Vector2(4.0f, 5.0f); + EXPECT_FLOAT_EQ(v3.x, 5.0f); + EXPECT_FLOAT_EQ(v3.y, 7.0f); +} + +TEST_F(UnitTestVector2, SubtractionOperator) +{ + constexpr Vector2 v3 = Vector2(4.0f, 5.0f) - Vector2(1.0f, 2.0f); + EXPECT_FLOAT_EQ(v3.x, 3.0f); + EXPECT_FLOAT_EQ(v3.y, 3.0f); +} + +TEST_F(UnitTestVector2, MultiplicationOperator) +{ + constexpr Vector2 v3 = Vector2(1.0f, 2.0f) * 2.0f; + EXPECT_FLOAT_EQ(v3.x, 2.0f); + EXPECT_FLOAT_EQ(v3.y, 4.0f); +} + +TEST_F(UnitTestVector2, DivisionOperator) +{ + constexpr Vector2 v3 = Vector2(4.0f, 5.0f) / 2.0f; + EXPECT_FLOAT_EQ(v3.x, 2.0f); + EXPECT_FLOAT_EQ(v3.y, 2.5f); +} + +TEST_F(UnitTestVector2, NegationOperator) +{ + constexpr Vector2 v3 = -Vector2(1.0f, 2.0f); + EXPECT_FLOAT_EQ(v3.x, -1.0f); + EXPECT_FLOAT_EQ(v3.y, -2.0f); +} + +// Test compound assignment operators +TEST_F(UnitTestVector2, AdditionAssignmentOperator) +{ + v1 += v2; + EXPECT_FLOAT_EQ(v1.x, 5.0f); + EXPECT_FLOAT_EQ(v1.y, 7.0f); +} + +TEST_F(UnitTestVector2, SubtractionAssignmentOperator) +{ + v1 -= v2; + EXPECT_FLOAT_EQ(v1.x, -3.0f); + EXPECT_FLOAT_EQ(v1.y, -3.0f); +} + +TEST_F(UnitTestVector2, MultiplicationAssignmentOperator) +{ + v1 *= 2.0f; + EXPECT_FLOAT_EQ(v1.x, 2.0f); + EXPECT_FLOAT_EQ(v1.y, 4.0f); +} + +TEST_F(UnitTestVector2, DivisionAssignmentOperator) +{ + v1 /= 2.0f; + EXPECT_FLOAT_EQ(v1.x, 0.5f); + EXPECT_FLOAT_EQ(v1.y, 1.0f); +} + +// New tests for compound assignment with vectors +TEST_F(UnitTestVector2, MultiplicationAssignmentOperator_Vector) +{ + v1 *= v2; + EXPECT_FLOAT_EQ(v1.x, 1.0f * 4.0f); + EXPECT_FLOAT_EQ(v1.y, 2.0f * 5.0f); +} + +TEST_F(UnitTestVector2, DivisionAssignmentOperator_Vector) +{ + v1 /= v2; + EXPECT_FLOAT_EQ(v1.x, 1.0f / 4.0f); + EXPECT_FLOAT_EQ(v1.y, 2.0f / 5.0f); +} + +// New tests for compound assignment with floats +TEST_F(UnitTestVector2, AdditionAssignmentOperator_Float) +{ + v1 += 3.0f; + EXPECT_FLOAT_EQ(v1.x, 4.0f); + EXPECT_FLOAT_EQ(v1.y, 5.0f); +} + +TEST_F(UnitTestVector2, SubtractionAssignmentOperator_Float) +{ + v1 -= 1.0f; + EXPECT_FLOAT_EQ(v1.x, 0.0f); + EXPECT_FLOAT_EQ(v1.y, 1.0f); +} + +// Test other member functions +TEST_F(UnitTestVector2, DistTo) +{ + const float dist = v1.distance_to(v2); + EXPECT_FLOAT_EQ(dist, std::sqrt(18.0f)); +} + +TEST_F(UnitTestVector2, DistTo_SamePoint) +{ + const float dist = v1.distance_to(v1); + EXPECT_FLOAT_EQ(dist, 0.0f); +} + +TEST_F(UnitTestVector2, DistToSqr) +{ + constexpr float distSqr = Vector2(1.0f, 2.0f).distance_to_sqr(Vector2(4.0f, 5.0f)); + EXPECT_FLOAT_EQ(distSqr, 18.0f); +} + +TEST_F(UnitTestVector2, DistToSqr_SamePoint) +{ + constexpr float distSqr = Vector2(1.0f, 2.0f).distance_to_sqr(Vector2(1.0f, 2.0f)); + EXPECT_FLOAT_EQ(distSqr, 0.0f); +} + +TEST_F(UnitTestVector2, DotProduct) +{ + constexpr float dot = Vector2(1.0f, 2.0f).dot(Vector2(4.0f, 5.0f)); + EXPECT_FLOAT_EQ(dot, 14.0f); +} + +TEST_F(UnitTestVector2, DotProduct_PerpendicularVectors) +{ + constexpr float dot = Vector2(1.0f, 0.0f).dot(Vector2(0.0f, 1.0f)); + EXPECT_FLOAT_EQ(dot, 0.0f); +} + +TEST_F(UnitTestVector2, DotProduct_ParallelVectors) +{ + constexpr float dot = Vector2(1.0f, 1.0f).dot(Vector2(2.0f, 2.0f)); + EXPECT_FLOAT_EQ(dot, 4.0f); +} + +TEST_F(UnitTestVector2, Length) +{ + const float length = v1.length(); + EXPECT_FLOAT_EQ(length, std::sqrt(5.0f)); +} + +TEST_F(UnitTestVector2, Length_ZeroVector) +{ + constexpr Vector2 v_zero(0.0f, 0.0f); + const float length = v_zero.length(); + EXPECT_FLOAT_EQ(length, 0.0f); +} + +TEST_F(UnitTestVector2, Length_LargeValues) +{ + constexpr Vector2 v_large(FLT_MAX, FLT_MAX); + const float length = v_large.length(); + EXPECT_TRUE(std::isinf(length)); +} + +TEST_F(UnitTestVector2, LengthSqr) +{ + constexpr float lengthSqr = Vector2(1.0f, 2.0f).length_sqr(); + EXPECT_FLOAT_EQ(lengthSqr, 5.0f); +} + +TEST_F(UnitTestVector2, Abs) +{ + Vector2 v3(-1.0f, -2.0f); + v3.abs(); + EXPECT_FLOAT_EQ(v3.x, 1.0f); + EXPECT_FLOAT_EQ(v3.y, 2.0f); +} + +TEST_F(UnitTestVector2, Abs_PositiveValues) +{ + Vector2 v3(1.0f, 2.0f); + v3.abs(); + EXPECT_FLOAT_EQ(v3.x, 1.0f); + EXPECT_FLOAT_EQ(v3.y, 2.0f); +} + +TEST_F(UnitTestVector2, Abs_ZeroValues) +{ + Vector2 v3(0.0f, 0.0f); + v3.abs(); + EXPECT_FLOAT_EQ(v3.x, 0.0f); + EXPECT_FLOAT_EQ(v3.y, 0.0f); +} + +TEST_F(UnitTestVector2, Sum) +{ + constexpr float sum = Vector2(1.0f, 2.0f).sum(); + EXPECT_FLOAT_EQ(sum, 3.0f); +} + +TEST_F(UnitTestVector2, Sum_NegativeValues) +{ + constexpr float sum = Vector2(-1.0f, -2.0f).sum(); + EXPECT_FLOAT_EQ(sum, -3.0f); +} + +TEST_F(UnitTestVector2, Normalized) +{ + const Vector2 v3 = v1.normalized(); + EXPECT_NEAR(v3.x, 0.44721f, 0.0001f); + EXPECT_NEAR(v3.y, 0.89443f, 0.0001f); +} + +TEST_F(UnitTestVector2, Normalized_ZeroVector) +{ + constexpr Vector2 v_zero(0.0f, 0.0f); + const Vector2 v_norm = v_zero.normalized(); + EXPECT_FLOAT_EQ(v_norm.x, 0.0f); + EXPECT_FLOAT_EQ(v_norm.y, 0.0f); +} + +// Test AsTuple method +TEST_F(UnitTestVector2, AsTuple) +{ + const auto tuple = v1.as_tuple(); + EXPECT_FLOAT_EQ(std::get<0>(tuple), v1.x); + EXPECT_FLOAT_EQ(std::get<1>(tuple), v1.y); +} + +// Test division by zero +TEST_F(UnitTestVector2, DivisionOperator_DivideByZero) +{ + constexpr Vector2 v(1.0f, 2.0f); + constexpr float zero = 0.0f; + const Vector2 result = v / zero; + EXPECT_TRUE(std::isinf(result.x) || std::isnan(result.x)); + EXPECT_TRUE(std::isinf(result.y) || std::isnan(result.y)); +} + +TEST_F(UnitTestVector2, DivisionAssignmentOperator_DivideByZero) +{ + Vector2 v(1.0f, 2.0f); + constexpr float zero = 0.0f; + v /= zero; + EXPECT_TRUE(std::isinf(v.x) || std::isnan(v.x)); + EXPECT_TRUE(std::isinf(v.y) || std::isnan(v.y)); +} + +TEST_F(UnitTestVector2, DivisionAssignmentOperator_VectorWithZero) +{ + Vector2 v(1.0f, 2.0f); + constexpr Vector2 v_zero(0.0f, 1.0f); + v /= v_zero; + EXPECT_TRUE(std::isinf(v.x) || std::isnan(v.x)); + EXPECT_FLOAT_EQ(v.y, 2.0f / 1.0f); +} + +// Test operations with infinity and NaN +TEST_F(UnitTestVector2, Operator_WithInfinity) +{ + const Vector2 v_inf(INFINITY, INFINITY); + const Vector2 result = v1 + v_inf; + EXPECT_TRUE(std::isinf(result.x)); + EXPECT_TRUE(std::isinf(result.y)); +} + +TEST_F(UnitTestVector2, Operator_WithNaN) +{ + constexpr Vector2 v_nan(NAN, NAN); + const Vector2 result = v1 + v_nan; + EXPECT_TRUE(std::isnan(result.x)); + EXPECT_TRUE(std::isnan(result.y)); +} + +// Test negative values in arithmetic operations +TEST_F(UnitTestVector2, AdditionOperator_NegativeValues) +{ + constexpr Vector2 v_neg(-1.0f, -2.0f); + const Vector2 result = v1 + v_neg; + EXPECT_FLOAT_EQ(result.x, 0.0f); + EXPECT_FLOAT_EQ(result.y, 0.0f); +} + +TEST_F(UnitTestVector2, SubtractionOperator_NegativeValues) +{ + constexpr Vector2 v_neg(-1.0f, -2.0f); + const Vector2 result = v1 - v_neg; + EXPECT_FLOAT_EQ(result.x, 2.0f); + EXPECT_FLOAT_EQ(result.y, 4.0f); +} + +// Test negation of zero vector +TEST_F(UnitTestVector2, NegationOperator_ZeroVector) +{ + constexpr Vector2 v_zero(0.0f, 0.0f); + constexpr Vector2 result = -v_zero; + EXPECT_FLOAT_EQ(result.x, -0.0f); + EXPECT_FLOAT_EQ(result.y, -0.0f); +} + +TEST_F(UnitTestVector2, LessOperator) +{ + EXPECT_TRUE(v1 < v2); +} + +TEST_F(UnitTestVector2, GreaterOperator) +{ + EXPECT_TRUE(v2 > v1); +} +TEST_F(UnitTestVector2, LessEqualOperator) +{ + EXPECT_TRUE(omath::Vector2{} <= omath::Vector2{}); + EXPECT_TRUE(omath::Vector2{} <= omath::Vector2(1.f, 1.f)); +} + +TEST_F(UnitTestVector2, GreaterEqualOperator) +{ + EXPECT_TRUE(omath::Vector2{} >= omath::Vector2{}); + EXPECT_TRUE(omath::Vector2(1.f, 1.f) >= omath::Vector2{}); +} + + +// Static assertions (compile-time checks) +static_assert(Vector2(1.0f, 2.0f).length_sqr() == 5.0f, "LengthSqr should be 5"); +static_assert(Vector2(1.0f, 2.0f).dot(Vector2(4.0f, 5.0f)) == 14.0f, "Dot product should be 14"); +static_assert(Vector2(4.0f, 5.0f).distance_to_sqr(Vector2(1.0f, 2.0f)) == 18.0f, "DistToSqr should be 18"); +static_assert(Vector2(-1.0f, -2.0f).abs() == Vector2(1.0f, 2.0f), "Abs should convert negative values to positive"); diff --git a/tests/general/unit_test_vector3.cpp b/tests/general/unit_test_vector3.cpp new file mode 100644 index 00000000..492033b8 --- /dev/null +++ b/tests/general/unit_test_vector3.cpp @@ -0,0 +1,487 @@ +// +// Created by Vlad on 01.09.2024. +// + +#include +#include // For FLT_MAX, FLT_MIN +#include +#include +#include // For std::numeric_limits + +using namespace omath; + +TEST(Vector3More, ConstructorsAndEquality) +{ + constexpr Vector3 a; + EXPECT_EQ(a.x, 0.f); + EXPECT_EQ(a.y, 0.f); + EXPECT_EQ(a.z, 0.f); + + constexpr Vector3 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 c = b; + EXPECT_EQ(c, b); +} + +TEST(Vector3More, ArithmeticAndDotCross) +{ + constexpr Vector3 a{1.f, 0.f, 0.f}; + constexpr Vector3 b{0.f, 1.f, 0.f}; + const auto c = a + b; + constexpr Vector3 expect_c{1.f,1.f,0.f}; + EXPECT_EQ(c, expect_c); + + const auto d = a - b; + constexpr Vector3 expect_d{1.f,-1.f,0.f}; + EXPECT_EQ(d, expect_d); + + const auto e = a * 2.f; + constexpr Vector3 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{ 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 expect_cr{0.f,0.f,1.f}; + EXPECT_EQ(cr, expect_cr); +} + +TEST(Vector3More, NormalizationEdgeCases) +{ + constexpr Vector3 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 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: + Vector3 v1; + Vector3 v2; + + void SetUp() override + { + v1 = Vector3(1.0f, 2.0f, 3.0f); + v2 = Vector3(4.0f, 5.0f, 6.0f); + } +}; + +// Test constructor and default values +TEST_F(UnitTestVector3, Constructor_Default) +{ + constexpr Vector3 v; + EXPECT_FLOAT_EQ(v.x, 0.0f); + EXPECT_FLOAT_EQ(v.y, 0.0f); + EXPECT_FLOAT_EQ(v.z, 0.0f); +} + +TEST_F(UnitTestVector3, Constructor_Values) +{ + constexpr Vector3 v(1.0f, 2.0f, 3.0f); + EXPECT_FLOAT_EQ(v.x, 1.0f); + EXPECT_FLOAT_EQ(v.y, 2.0f); + EXPECT_FLOAT_EQ(v.z, 3.0f); +} + +// Test equality operators +TEST_F(UnitTestVector3, EqualityOperator) +{ + constexpr Vector3 v3(1.0f, 2.0f, 3.0f); + EXPECT_TRUE(v1 == v3); + EXPECT_FALSE(v1 == v2); +} + +TEST_F(UnitTestVector3, InequalityOperator) +{ + constexpr Vector3 v3(1.0f, 2.0f, 3.0f); + EXPECT_FALSE(v1 != v3); + EXPECT_TRUE(v1 != v2); +} + +// Test arithmetic operators +TEST_F(UnitTestVector3, AdditionOperator) +{ + constexpr Vector3 v3 = Vector3(1.0f, 2.0f, 3.0f) + Vector3(4.0f, 5.0f, 6.0f); + EXPECT_FLOAT_EQ(v3.x, 5.0f); + EXPECT_FLOAT_EQ(v3.y, 7.0f); + EXPECT_FLOAT_EQ(v3.z, 9.0f); +} + +TEST_F(UnitTestVector3, SubtractionOperator) +{ + constexpr Vector3 v3 = Vector3(4.0f, 5.0f, 6.0f) - Vector3(1.0f, 2.0f, 3.0f); + EXPECT_FLOAT_EQ(v3.x, 3.0f); + EXPECT_FLOAT_EQ(v3.y, 3.0f); + EXPECT_FLOAT_EQ(v3.z, 3.0f); +} + +TEST_F(UnitTestVector3, MultiplicationOperator_Scalar) +{ + constexpr Vector3 v3 = Vector3(1.0f, 2.0f, 3.0f) * 2.0f; + EXPECT_FLOAT_EQ(v3.x, 2.0f); + EXPECT_FLOAT_EQ(v3.y, 4.0f); + EXPECT_FLOAT_EQ(v3.z, 6.0f); +} + +TEST_F(UnitTestVector3, MultiplicationOperator_Vector) +{ + constexpr auto v3 = Vector3(1.0f, 2.0f, 3.0f) * Vector3(4.0f, 5.0f, 6.0f); + EXPECT_FLOAT_EQ(v3.x, 4.0f); + EXPECT_FLOAT_EQ(v3.y, 10.0f); + EXPECT_FLOAT_EQ(v3.z, 18.0f); +} + +TEST_F(UnitTestVector3, DivisionOperator_Scalar) +{ + constexpr auto v3 = Vector3(4.0f, 5.0f, 6.0f) / 2.0f; + EXPECT_FLOAT_EQ(v3.x, 2.0f); + EXPECT_FLOAT_EQ(v3.y, 2.5f); + EXPECT_FLOAT_EQ(v3.z, 3.0f); +} + +TEST_F(UnitTestVector3, DivisionOperator_Vector) +{ + constexpr auto v3 = Vector3(4.0f, 5.0f, 6.0f) / Vector3(1.0f, 2.0f, 3.0f); + EXPECT_FLOAT_EQ(v3.x, 4.0f); + EXPECT_FLOAT_EQ(v3.y, 2.5f); + EXPECT_FLOAT_EQ(v3.z, 2.0f); +} + +// Test compound assignment operators +TEST_F(UnitTestVector3, AdditionAssignmentOperator) +{ + v1 += v2; + EXPECT_FLOAT_EQ(v1.x, 5.0f); + EXPECT_FLOAT_EQ(v1.y, 7.0f); + EXPECT_FLOAT_EQ(v1.z, 9.0f); +} + +TEST_F(UnitTestVector3, SubtractionAssignmentOperator) +{ + v1 -= v2; + EXPECT_FLOAT_EQ(v1.x, -3.0f); + EXPECT_FLOAT_EQ(v1.y, -3.0f); + EXPECT_FLOAT_EQ(v1.z, -3.0f); +} + +TEST_F(UnitTestVector3, MultiplicationAssignmentOperator_Scalar) +{ + v1 *= 2.0f; + EXPECT_FLOAT_EQ(v1.x, 2.0f); + EXPECT_FLOAT_EQ(v1.y, 4.0f); + EXPECT_FLOAT_EQ(v1.z, 6.0f); +} + +TEST_F(UnitTestVector3, MultiplicationAssignmentOperator_Vector) +{ + v1 *= v2; + EXPECT_FLOAT_EQ(v1.x, 4.0f); + EXPECT_FLOAT_EQ(v1.y, 10.0f); + EXPECT_FLOAT_EQ(v1.z, 18.0f); +} + +TEST_F(UnitTestVector3, DivisionAssignmentOperator_Scalar) +{ + v1 /= 2.0f; + EXPECT_FLOAT_EQ(v1.x, 0.5f); + EXPECT_FLOAT_EQ(v1.y, 1.0f); + EXPECT_FLOAT_EQ(v1.z, 1.5f); +} + +TEST_F(UnitTestVector3, DivisionAssignmentOperator_Vector) +{ + v1 /= v2; + EXPECT_FLOAT_EQ(v1.x, 0.25f); + EXPECT_FLOAT_EQ(v1.y, 0.4f); + EXPECT_FLOAT_EQ(v1.z, 0.5f); +} + +TEST_F(UnitTestVector3, NegationOperator) +{ + constexpr auto v3 = -Vector3(1.0f, 2.0f, 3.0f); + EXPECT_FLOAT_EQ(v3.x, -1.0f); + EXPECT_FLOAT_EQ(v3.y, -2.0f); + EXPECT_FLOAT_EQ(v3.z, -3.0f); +} + +// Test other member functions +TEST_F(UnitTestVector3, DistToSqr) +{ + constexpr auto distSqr = Vector3(1.0f, 2.0f, 3.0f).distance_to_sqr(Vector3(4.0f, 5.0f, 6.0f)); + EXPECT_FLOAT_EQ(distSqr, 27.0f); +} + +TEST_F(UnitTestVector3, DotProduct) +{ + constexpr auto dot = Vector3(1.0f, 2.0f, 3.0f).dot(Vector3(4.0f, 5.0f, 6.0f)); + EXPECT_FLOAT_EQ(dot, 32.0f); +} + +TEST_F(UnitTestVector3, LengthSqr) +{ + constexpr auto lengthSqr = Vector3(1.0f, 2.0f, 3.0f).length_sqr(); + EXPECT_FLOAT_EQ(lengthSqr, 14.0f); +} + +TEST_F(UnitTestVector3, Abs) +{ + auto v3 = Vector3(-1.0f, -2.0f, -3.0f); + v3.abs(); + EXPECT_FLOAT_EQ(v3.x, 1.0f); + EXPECT_FLOAT_EQ(v3.y, 2.0f); + EXPECT_FLOAT_EQ(v3.z, 3.0f); +} + +TEST_F(UnitTestVector3, Sum) +{ + constexpr auto sum = Vector3(1.0f, 2.0f, 3.0f).sum(); + EXPECT_FLOAT_EQ(sum, 6.0f); +} + +TEST_F(UnitTestVector3, Sum2D) +{ + constexpr auto sum2D = Vector3(1.0f, 2.0f, 3.0f).sum_2d(); + EXPECT_FLOAT_EQ(sum2D, 3.0f); +} + +TEST_F(UnitTestVector3, CrossProduct) +{ + constexpr Vector3 v3 = Vector3(1.0f, 2.0f, 3.0f).cross(Vector3(4.0f, 5.0f, 6.0f)); + EXPECT_FLOAT_EQ(v3.x, -3.0f); + EXPECT_FLOAT_EQ(v3.y, 6.0f); + EXPECT_FLOAT_EQ(v3.z, -3.0f); +} + +// New tests to cover corner cases + +// Test operations with zero vectors +TEST_F(UnitTestVector3, Addition_WithZeroVector) +{ + constexpr Vector3 v_zero(0.0f, 0.0f, 0.0f); + const Vector3 result = v1 + v_zero; + EXPECT_FLOAT_EQ(result.x, v1.x); + EXPECT_FLOAT_EQ(result.y, v1.y); + EXPECT_FLOAT_EQ(result.z, v1.z); +} + +TEST_F(UnitTestVector3, Subtraction_WithZeroVector) +{ + constexpr Vector3 v_zero(0.0f, 0.0f, 0.0f); + const Vector3 result = v1 - v_zero; + EXPECT_FLOAT_EQ(result.x, v1.x); + EXPECT_FLOAT_EQ(result.y, v1.y); + EXPECT_FLOAT_EQ(result.z, v1.z); +} + +TEST_F(UnitTestVector3, Multiplication_WithZeroVector) +{ + constexpr Vector3 v_zero(0.0f, 0.0f, 0.0f); + const Vector3 result = v1 * v_zero; + EXPECT_FLOAT_EQ(result.x, 0.0f); + EXPECT_FLOAT_EQ(result.y, 0.0f); + EXPECT_FLOAT_EQ(result.z, 0.0f); +} + +TEST_F(UnitTestVector3, Division_ByZeroVector) +{ + constexpr Vector3 v_zero(0.0f, 0.0f, 0.0f); + const Vector3 result = v1 / v_zero; + EXPECT_TRUE(std::isinf(result.x) || std::isnan(result.x)); + EXPECT_TRUE(std::isinf(result.y) || std::isnan(result.y)); + EXPECT_TRUE(std::isinf(result.z) || std::isnan(result.z)); +} + +TEST_F(UnitTestVector3, Division_ByZeroScalar) +{ + constexpr float zero = 0.0f; + const Vector3 result = v1 / zero; + EXPECT_TRUE(std::isinf(result.x) || std::isnan(result.x)); + EXPECT_TRUE(std::isinf(result.y) || std::isnan(result.y)); + EXPECT_TRUE(std::isinf(result.z) || std::isnan(result.z)); +} + +// Test operations with infinity +TEST_F(UnitTestVector3, Addition_WithInfinity) +{ + 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)); + EXPECT_TRUE(std::isinf(result.z)); +} + +TEST_F(UnitTestVector3, Subtraction_WithInfinity) +{ + constexpr Vector3 v_inf(INFINITY, INFINITY, INFINITY); + const Vector3 result = v1 - v_inf; + EXPECT_TRUE(std::isinf(result.x)); + EXPECT_TRUE(std::isinf(result.y)); + EXPECT_TRUE(std::isinf(result.z)); +} + +// Test operations with NaN +TEST_F(UnitTestVector3, Multiplication_WithNaN) +{ + constexpr Vector3 v_nan(NAN, NAN, NAN); + const Vector3 result = v1 * v_nan; + EXPECT_TRUE(std::isnan(result.x)); + EXPECT_TRUE(std::isnan(result.y)); + EXPECT_TRUE(std::isnan(result.z)); +} + +TEST_F(UnitTestVector3, Division_WithNaN) +{ + constexpr Vector3 v_nan(NAN, NAN, NAN); + const Vector3 result = v1 / v_nan; + EXPECT_TRUE(std::isnan(result.x)); + EXPECT_TRUE(std::isnan(result.y)); + EXPECT_TRUE(std::isnan(result.z)); +} + +// Test Length, Length2D, and Normalized +TEST_F(UnitTestVector3, Length) +{ + const float length = v1.length(); + EXPECT_FLOAT_EQ(length, std::sqrt(14.0f)); +} + +TEST_F(UnitTestVector3, Length_ZeroVector) +{ + constexpr Vector3 v_zero(0.0f, 0.0f, 0.0f); + const float length = v_zero.length(); + EXPECT_FLOAT_EQ(length, 0.0f); +} + +TEST_F(UnitTestVector3, Length_LargeValues) +{ + constexpr Vector3 v_large(FLT_MAX, FLT_MAX, FLT_MAX); + const float length = v_large.length(); + EXPECT_TRUE(std::isinf(length)); +} + +TEST_F(UnitTestVector3, Length2D) +{ + const float length2D = v1.length_2d(); + EXPECT_FLOAT_EQ(length2D, std::sqrt(5.0f)); +} + +TEST_F(UnitTestVector3, Normalized) +{ + const Vector3 v_norm = v1.normalized(); + const float length = v_norm.length(); + EXPECT_NEAR(length, 1.0f, 0.0001f); +} + +TEST_F(UnitTestVector3, Normalized_ZeroVector) +{ + constexpr Vector3 v_zero(0.0f, 0.0f, 0.0f); + const Vector3 v_norm = v_zero.normalized(); + EXPECT_FLOAT_EQ(v_norm.x, 0.0f); + EXPECT_FLOAT_EQ(v_norm.y, 0.0f); + EXPECT_FLOAT_EQ(v_norm.z, 0.0f); +} + +// Test Cross Product edge cases +TEST_F(UnitTestVector3, CrossProduct_ParallelVectors) +{ + constexpr Vector3 v_a(1.0f, 2.0f, 3.0f); + constexpr Vector3 v_b = v_a * 2.0f; // Parallel to v_a + constexpr Vector3 cross = v_a.cross(v_b); + EXPECT_FLOAT_EQ(cross.x, 0.0f); + EXPECT_FLOAT_EQ(cross.y, 0.0f); + EXPECT_FLOAT_EQ(cross.z, 0.0f); +} + +TEST_F(UnitTestVector3, CrossProduct_OrthogonalVectors) +{ + constexpr Vector3 v_a(1.0f, 0.0f, 0.0f); + constexpr Vector3 v_b(0.0f, 1.0f, 0.0f); + constexpr Vector3 cross = v_a.cross(v_b); + EXPECT_FLOAT_EQ(cross.x, 0.0f); + EXPECT_FLOAT_EQ(cross.y, 0.0f); + EXPECT_FLOAT_EQ(cross.z, 1.0f); +} + +// Test negative values +TEST_F(UnitTestVector3, Addition_NegativeValues) +{ + constexpr Vector3 v_neg(-1.0f, -2.0f, -3.0f); + const Vector3 result = v1 + v_neg; + EXPECT_FLOAT_EQ(result.x, 0.0f); + EXPECT_FLOAT_EQ(result.y, 0.0f); + EXPECT_FLOAT_EQ(result.z, 0.0f); +} + +TEST_F(UnitTestVector3, Subtraction_NegativeValues) +{ + constexpr Vector3 v_neg(-1.0f, -2.0f, -3.0f); + const Vector3 result = v1 - v_neg; + EXPECT_FLOAT_EQ(result.x, 2.0f); + EXPECT_FLOAT_EQ(result.y, 4.0f); + EXPECT_FLOAT_EQ(result.z, 6.0f); +} + +// Test AsTuple method +TEST_F(UnitTestVector3, AsTuple) +{ + const auto tuple = v1.as_tuple(); + EXPECT_FLOAT_EQ(std::get<0>(tuple), v1.x); + EXPECT_FLOAT_EQ(std::get<1>(tuple), v1.y); + EXPECT_FLOAT_EQ(std::get<2>(tuple), v1.z); +} + +// Test AsTuple method +TEST_F(UnitTestVector3, AngleBeatween) +{ + 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()); +} + +TEST_F(UnitTestVector3, IsPerpendicular) +{ + EXPECT_EQ(Vector3(0.0f, 0.0f, 1.0f).is_perpendicular({1, 0 ,0}), true); + EXPECT_EQ(Vector3(0.0f, 0.0f, 1.0f).is_perpendicular({0.0f, 0.0f, 1.0f}), false); + EXPECT_FALSE(Vector3(0.0f, 0.0f, 0.0f).is_perpendicular({0.0f, 0.0f, 1.0f})); +} + +TEST_F(UnitTestVector3, LessOperator) +{ + EXPECT_TRUE(v1 < v2); +} + +TEST_F(UnitTestVector3, GreaterOperator) +{ + EXPECT_TRUE(v2 > v1); +} +TEST_F(UnitTestVector3, LessEqualOperator) +{ + EXPECT_TRUE(omath::Vector3{} <= omath::Vector3{}); + EXPECT_TRUE(omath::Vector3{} <= omath::Vector3(1.f, 1.f, 1.f)); +} + +TEST_F(UnitTestVector3, GreaterEqualOperator) +{ + EXPECT_TRUE(omath::Vector3{} >= omath::Vector3{}); + EXPECT_TRUE(omath::Vector3(1.f, 1.f, 1.f) >= omath::Vector3{}); +} + +// Static assertions (compile-time checks) +static_assert(Vector3(1.0f, 2.0f, 3.0f).length_sqr() == 14.0f, "LengthSqr should be 14"); +static_assert(Vector3(1.0f, 2.0f, 3.0f).dot(Vector3(4.0f, 5.0f, 6.0f)) == 32.0f, "Dot product should be 32"); +static_assert(Vector3(4.0f, 5.0f, 6.0f).distance_to_sqr(Vector3(1.0f, 2.0f, 3.0f)) == 27.0f, "DistToSqr should be 27"); +static_assert(Vector3(-1.0f, -2.0f, -3.0f).abs() == Vector3(1.0f, 2.0f, 3.0f), "Abs should convert negative values to positive"); diff --git a/tests/general/unit_test_vector4.cpp b/tests/general/unit_test_vector4.cpp new file mode 100644 index 00000000..ad777a77 --- /dev/null +++ b/tests/general/unit_test_vector4.cpp @@ -0,0 +1,263 @@ +// +// Created by vlad on 9/24/2024. +// +// +// Vector4Test.cpp +// + +#include +#include +#include // For std::numeric_limits + +using namespace omath; + +TEST(Vector4More, ConstructorsAndClamp) +{ + constexpr Vector4 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 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 a{1,2,3,4}; + constexpr Vector4 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: + Vector4 v1; + Vector4 v2; + + void SetUp() override + { + v1 = Vector4(1.0f, 2.0f, 3.0f, 4.0f); + v2 = Vector4(4.0f, 5.0f, 6.0f, 7.0f); + } +}; + +// Test constructor and default values +TEST_F(UnitTestVector4, Constructor_Default) +{ + constexpr Vector4 v; + EXPECT_FLOAT_EQ(v.x, 0.0f); + EXPECT_FLOAT_EQ(v.y, 0.0f); + EXPECT_FLOAT_EQ(v.z, 0.0f); + EXPECT_FLOAT_EQ(v.w, 0.0f); +} + +TEST_F(UnitTestVector4, Constructor_Values) +{ + constexpr Vector4 v(1.0f, 2.0f, 3.0f, 4.0f); + EXPECT_FLOAT_EQ(v.x, 1.0f); + EXPECT_FLOAT_EQ(v.y, 2.0f); + EXPECT_FLOAT_EQ(v.z, 3.0f); + EXPECT_FLOAT_EQ(v.w, 4.0f); +} + +// Test equality operators +TEST_F(UnitTestVector4, EqualityOperator) +{ + constexpr Vector4 v3(1.0f, 2.0f, 3.0f, 4.0f); + EXPECT_TRUE(v1 == v3); + EXPECT_FALSE(v1 == v2); +} + +TEST_F(UnitTestVector4, InequalityOperator) +{ + constexpr Vector4 v3(1.0f, 2.0f, 3.0f, 4.0f); + EXPECT_FALSE(v1 != v3); + EXPECT_TRUE(v1 != v2); +} + +// Test arithmetic operators +TEST_F(UnitTestVector4, AdditionOperator) +{ + constexpr Vector4 v3 = Vector4(1.0f, 2.0f, 3.0f, 4.0f) + Vector4(4.0f, 5.0f, 6.0f, 7.0f); + EXPECT_FLOAT_EQ(v3.x, 5.0f); + EXPECT_FLOAT_EQ(v3.y, 7.0f); + EXPECT_FLOAT_EQ(v3.z, 9.0f); + EXPECT_FLOAT_EQ(v3.w, 11.0f); +} + +TEST_F(UnitTestVector4, SubtractionOperator) +{ + constexpr Vector4 v3 = Vector4(4.0f, 5.0f, 6.0f, 7.0f) - Vector4(1.0f, 2.0f, 3.0f, 4.0f); + EXPECT_FLOAT_EQ(v3.x, 3.0f); + EXPECT_FLOAT_EQ(v3.y, 3.0f); + EXPECT_FLOAT_EQ(v3.z, 3.0f); + EXPECT_FLOAT_EQ(v3.w, 3.0f); +} + +TEST_F(UnitTestVector4, MultiplicationOperator_Scalar) +{ + constexpr Vector4 v3 = Vector4(1.0f, 2.0f, 3.0f, 4.0f) * 2.0f; + EXPECT_FLOAT_EQ(v3.x, 2.0f); + EXPECT_FLOAT_EQ(v3.y, 4.0f); + EXPECT_FLOAT_EQ(v3.z, 6.0f); + EXPECT_FLOAT_EQ(v3.w, 8.0f); +} + +TEST_F(UnitTestVector4, MultiplicationOperator_Vector) +{ + constexpr Vector4 v3 = Vector4(1.0f, 2.0f, 3.0f, 4.0f) * Vector4(4.0f, 5.0f, 6.0f, 7.0f); + EXPECT_FLOAT_EQ(v3.x, 4.0f); + EXPECT_FLOAT_EQ(v3.y, 10.0f); + EXPECT_FLOAT_EQ(v3.z, 18.0f); + EXPECT_FLOAT_EQ(v3.w, 28.0f); +} + +TEST_F(UnitTestVector4, DivisionOperator_Scalar) +{ + constexpr Vector4 v3 = Vector4(4.0f, 5.0f, 6.0f, 7.0f) / 2.0f; + EXPECT_FLOAT_EQ(v3.x, 2.0f); + EXPECT_FLOAT_EQ(v3.y, 2.5f); + EXPECT_FLOAT_EQ(v3.z, 3.0f); + EXPECT_FLOAT_EQ(v3.w, 3.5f); +} + +TEST_F(UnitTestVector4, DivisionOperator_Vector) +{ + constexpr Vector4 v3 = Vector4(4.0f, 5.0f, 6.0f, 7.0f) / Vector4(1.0f, 2.0f, 3.0f, 4.0f); + EXPECT_FLOAT_EQ(v3.x, 4.0f); + EXPECT_FLOAT_EQ(v3.y, 2.5f); + EXPECT_FLOAT_EQ(v3.z, 2.0f); + EXPECT_FLOAT_EQ(v3.w, 1.75f); +} + +// Test compound assignment operators +TEST_F(UnitTestVector4, AdditionAssignmentOperator) +{ + v1 += v2; + EXPECT_FLOAT_EQ(v1.x, 5.0f); + EXPECT_FLOAT_EQ(v1.y, 7.0f); + EXPECT_FLOAT_EQ(v1.z, 9.0f); + EXPECT_FLOAT_EQ(v1.w, 11.0f); +} + +TEST_F(UnitTestVector4, SubtractionAssignmentOperator) +{ + v1 -= v2; + EXPECT_FLOAT_EQ(v1.x, -3.0f); + EXPECT_FLOAT_EQ(v1.y, -3.0f); + EXPECT_FLOAT_EQ(v1.z, -3.0f); + EXPECT_FLOAT_EQ(v1.w, -3.0f); +} + +TEST_F(UnitTestVector4, MultiplicationAssignmentOperator_Scalar) +{ + v1 *= 2.0f; + EXPECT_FLOAT_EQ(v1.x, 2.0f); + EXPECT_FLOAT_EQ(v1.y, 4.0f); + EXPECT_FLOAT_EQ(v1.z, 6.0f); + EXPECT_FLOAT_EQ(v1.w, 8.0f); +} + +TEST_F(UnitTestVector4, MultiplicationAssignmentOperator_Vector) +{ + v1 *= v2; + EXPECT_FLOAT_EQ(v1.x, 4.0f); + EXPECT_FLOAT_EQ(v1.y, 10.0f); + EXPECT_FLOAT_EQ(v1.z, 18.0f); + EXPECT_FLOAT_EQ(v1.w, 28.0f); +} + +TEST_F(UnitTestVector4, DivisionAssignmentOperator_Scalar) +{ + v1 /= 2.0f; + EXPECT_FLOAT_EQ(v1.x, 0.5f); + EXPECT_FLOAT_EQ(v1.y, 1.0f); + EXPECT_FLOAT_EQ(v1.z, 1.5f); + EXPECT_FLOAT_EQ(v1.w, 2.0f); +} + +TEST_F(UnitTestVector4, DivisionAssignmentOperator_Vector) +{ + v1 /= v2; + EXPECT_FLOAT_EQ(v1.x, 0.25f); + EXPECT_FLOAT_EQ(v1.y, 0.4f); + EXPECT_FLOAT_EQ(v1.z, 0.5f); + EXPECT_FLOAT_EQ(v1.w, 4.0f / 7.0f); +} + +TEST_F(UnitTestVector4, NegationOperator) +{ + constexpr Vector4 v3 = -Vector4(1.0f, 2.0f, 3.0f, 4.0f); + EXPECT_FLOAT_EQ(v3.x, -1.0f); + EXPECT_FLOAT_EQ(v3.y, -2.0f); + EXPECT_FLOAT_EQ(v3.z, -3.0f); + EXPECT_FLOAT_EQ(v3.w, -4.0f); +} + +// Test other member functions +TEST_F(UnitTestVector4, LengthSqr) +{ + constexpr float lengthSqr = Vector4(1.0f, 2.0f, 3.0f, 4.0f).length_sqr(); + EXPECT_FLOAT_EQ(lengthSqr, 30.0f); +} + +TEST_F(UnitTestVector4, DotProduct) +{ + constexpr float dot = Vector4(1.0f, 2.0f, 3.0f, 4.0f).dot(Vector4(4.0f, 5.0f, 6.0f, 7.0f)); + EXPECT_FLOAT_EQ(dot, 60.0f); +} + +TEST_F(UnitTestVector4, Abs) +{ + Vector4 v3 = Vector4(-1.0f, -2.0f, -3.0f, -4.0f); + v3.abs(); + EXPECT_FLOAT_EQ(v3.x, 1.0f); + EXPECT_FLOAT_EQ(v3.y, 2.0f); + EXPECT_FLOAT_EQ(v3.z, 3.0f); + EXPECT_FLOAT_EQ(v3.w, 4.0f); +} + +TEST_F(UnitTestVector4, Sum) +{ + constexpr float sum = Vector4(1.0f, 2.0f, 3.0f, 4.0f).sum(); + EXPECT_FLOAT_EQ(sum, 10.0f); +} + +TEST_F(UnitTestVector4, Clamp) +{ + Vector4 v3 = Vector4(1.0f, 2.0f, 3.0f, 4.0f); + v3.clamp(1.5f, 2.5f); + EXPECT_FLOAT_EQ(v3.x, 1.5f); + EXPECT_FLOAT_EQ(v3.y, 2.0f); + EXPECT_FLOAT_EQ(v3.z, 2.5f); + EXPECT_FLOAT_EQ(v3.w, 4.0f); // w is not clamped in this method +} +TEST_F(UnitTestVector4, LessOperator) +{ + EXPECT_TRUE(v1 < v2); +} + +TEST_F(UnitTestVector4, GreaterOperator) +{ + EXPECT_TRUE(v2 > v1); +} +TEST_F(UnitTestVector4, LessEqualOperator) +{ + EXPECT_TRUE(omath::Vector4{} <= omath::Vector4{}); + EXPECT_TRUE(omath::Vector4{} <= omath::Vector4(1.f, 1.f, 1.f, 1.f)); +} + +TEST_F(UnitTestVector4, GreaterEqualOperator) +{ + EXPECT_TRUE(omath::Vector4{} >= omath::Vector4{}); + EXPECT_TRUE(omath::Vector4(1.f, 1.f, 1.f, 1.f) >= omath::Vector4{}); +} \ No newline at end of file diff --git a/tests/general/unit_test_view_angles.cpp b/tests/general/unit_test_view_angles.cpp new file mode 100644 index 00000000..ed45c11c --- /dev/null +++ b/tests/general/unit_test_view_angles.cpp @@ -0,0 +1,4 @@ +// +// Created by Orange on 11/30/2024. +// +#include \ No newline at end of file diff --git a/tests/main.cpp b/tests/main.cpp new file mode 100644 index 00000000..05667d1c --- /dev/null +++ b/tests/main.cpp @@ -0,0 +1,8 @@ +#include + +int main(int argc, char** argv) +{ + testing::InitGoogleTest(&argc, argv); + + return RUN_ALL_TESTS(); +} diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json new file mode 100644 index 00000000..f14bf6ba --- /dev/null +++ b/vcpkg-configuration.json @@ -0,0 +1,14 @@ +{ + "default-registry": { + "kind": "git", + "baseline": "b1b19307e2d2ec1eefbdb7ea069de7d4bcd31f01", + "repository": "https://github.com/microsoft/vcpkg" + }, + "registries": [ + { + "kind": "artifact", + "location": "https://github.com/microsoft/vcpkg-ce-catalog/archive/refs/heads/main.zip", + "name": "microsoft" + } + ] +} diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 00000000..c4c0d421 --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,49 @@ +{ + "name": "omath", + "version": "3.10.1", + "description": "General purpose math library", + "homepage": "https://github.com/orange-cpp/omath", + "license": "Zlib", + "supports": "windows | linux | macos", + "dependencies": [ + { + "name": "vcpkg-cmake", + "host": true + }, + { + "name": "vcpkg-cmake-config", + "host": true + } + ], + "features": { + "avx2": { + "description": "Omath will use AVX2 to boost performance", + "supports": "!arm" + }, + "benchmark": { + "description": "Build benchmarks", + "dependencies": [ + "benchmark" + ] + }, + "examples": { + "description": "Build benchmarks", + "dependencies": [ + "glfw3", + "glew" + ] + }, + "imgui": { + "description": "Omath will define method to convert omath types to imgui types", + "dependencies": [ + "imgui" + ] + }, + "tests": { + "description": "Build unit-tests using GTest", + "dependencies": [ + "gtest" + ] + } + } +} diff --git a/xmake.lua b/xmake.lua new file mode 100644 index 00000000..26030257 --- /dev/null +++ b/xmake.lua @@ -0,0 +1,113 @@ +add_rules("mode.debug", "mode.release") + +add_rules("utils.install.cmake_importfiles") +add_rules("utils.install.pkgconfig_importfiles") + +set_version("4.6.1", {soname = "4.6"}) +set_languages("cxx23") +set_warnings("all") + +option("avx2") + set_default(true) + set_showmenu(true) + + after_check(function (option) + import("core.base.cpu") + option:enable(cpu.has_feature("avx2")) + end) +option_end() + +option("imgui") + set_default(true) + set_showmenu(true) +option_end() + +option("examples") + set_default(true) + set_showmenu(true) +option_end() + +option("tests") + set_default(true) + set_showmenu(true) +option_end() + +if has_config("avx2") then + add_defines("OMATH_ENABLE_AVX2") + add_vectorexts("avx2") +end + +if has_config("imgui") then + add_defines("OMATH_IMGUI_INTEGRATION") + add_requires("imgui") + add_packages("imgui") +end + +if has_config("examples") then + add_requires("glew", "glfw") +end + +if has_config("tests") then + add_requires("gtest") +end + +target("omath") + set_kind("static") + add_files("source/**.cpp") + add_includedirs("include", {public = true}) + add_headerfiles("include/(**.hpp)", {prefixdir = "omath"}) + on_config(function (target) + if has_config("avx2") then + cprint("${green} ✔️ AVX2 supported") + else + cprint("${red} ❌ AVX2 not supported") + end + end) + +target("example_projection_matrix_builder") + set_languages("cxx26") + set_kind("binary") + add_files("examples/example_proj_mat_builder.cpp") + add_deps("omath") + set_enabled(has_config("examples")) + +target("example_signature_scan") + set_languages("cxx26") + set_kind("binary") + add_files("examples/example_signature_scan.cpp") + add_deps("omath") + set_enabled(has_config("examples")) + +target("example_glfw3") + set_languages("cxx26") + set_kind("binary") + add_files("examples/example_glfw3.cpp") + add_deps("omath") + add_packages("glew", "glfw") + set_enabled(has_config("examples")) + +for _, file in ipairs(os.files("tests/**.cpp")) do + local name = path.basename(file) + target(name) + set_languages("cxx23") + set_kind("binary") + add_files(file, "tests/main.cpp") + add_deps("omath") + add_packages("gtest") + add_defines("OMATH_BUILD_TESTS") + add_tests("default") + set_default(false) + set_enabled(has_config("tests")) +end + +task("check") + on_run(function () + import("core.project.task") + for _, file in ipairs(os.files("tests/**.cpp")) do + local name = path.basename(file) + task.run("run", {target = name}) + end + end) + set_menu { + usage = "xmake check", description = "Run tests !", options = {} + }