Compare commits

...

49 Commits

Author SHA1 Message Date
orange 56ed7e2f6e added outlined option for box 2026-05-23 11:56:52 +03:00
orange 39d0d0683d improvement 2026-05-22 12:35:14 +03:00
orange a04bceaeb6 Merge pull request #194 from orange-cpp/feature/code-style-skill
added code style skill for codex/claude
2026-05-22 12:05:39 +03:00
orange f0fe5821ed added code style skill for codex/claude 2026-05-22 12:05:06 +03:00
orange d8c5ea16fe Merge pull request #193 from orange-cpp/feature/nodiscrad-messages
Feature/nodiscrad messages
2026-05-22 11:50:24 +03:00
orange 848202cbd8 added nodiscard messages 2026-05-22 09:00:32 +03:00
orange 37128d18e7 camera message 2026-05-22 08:50:21 +03:00
orange 8433ef05ca Merge pull request #192 from orange-cpp/feature/lua
added more lua stuff
2026-05-18 11:11:13 +03:00
orange d9f2428e0e fixed for mingw 2026-05-18 10:54:48 +03:00
orange 23d3b7c9f5 windows fix 2026-05-18 10:34:23 +03:00
orange 36a7865b29 fixed arm 2026-05-18 10:28:07 +03:00
orange 2130d02090 patch 2026-05-18 10:22:36 +03:00
orange f602ab6538 Merge pull request #191 from orange-cpp/feature/mat_improvement
added improvement
2026-05-17 10:11:20 +03:00
orange e4087165b9 added safety check 2026-05-17 09:38:19 +03:00
orange cebcfc411d added improvement 2026-05-17 09:08:36 +03:00
orange a4fac65b7c added more lua stuff 2026-05-16 10:33:06 +03:00
va_alpatov 7f88bf8b21 hotfix 2026-05-16 05:35:41 +03:00
orange 93e70667f0 Merge pull request #190 from orange-cpp/feature/more-obb-tests
Feature/more obb tests
2026-05-14 03:05:50 +03:00
orange bdef596f16 added more tests 2026-05-14 02:30:21 +03:00
orange 7a2ac25e8d fixed inconsistant types 2026-05-14 01:37:33 +03:00
orange b6f41ed653 improved naming 2026-05-13 07:51:17 +03:00
orange 5ebba4a630 added constexpr 2026-05-13 07:03:31 +03:00
orange c73afa95cc update 2026-05-13 05:16:10 +03:00
orange 3ca657a048 added codex instructions 2026-05-13 05:12:19 +03:00
orange 9d1de20128 removed gitmodules 2026-05-12 08:23:17 +03:00
orange 6413c5d59c added ray tracer check for obb in line tracer 2026-05-07 05:30:47 +03:00
orange 94f88056cb Merge pull request #189 from orange-cpp/feature/obb
added obb
2026-05-07 05:23:20 +03:00
orange fbc35391c4 added obb 2026-05-07 05:04:35 +03:00
orange 6b637f6267 Merge pull request #188 from orange-cpp/feature/opengl_hook
Feature/opengl hook
2026-05-06 23:41:00 +03:00
orange fa52c9e985 added opengl for linux 2026-05-06 22:00:23 +03:00
orange 6ced4acdb6 removed copying 2026-05-06 20:10:34 +03:00
orange d90164cab8 added opengl hook 2026-05-06 20:05:45 +03:00
orange 29255cbb0e added claud config + skills 2026-05-06 04:04:57 +03:00
orange 8ad936f9f1 added separated mutexes for each call back 2026-05-04 20:55:42 +03:00
orange 57c834ded4 code style fixes 2026-05-04 19:42:39 +03:00
orange e25b1b3fc8 updated version 2026-05-04 06:09:27 +03:00
orange f2794230c3 Merge pull request #187 from orange-cpp/feature/hooking
Feature/hooking
2026-05-04 04:38:22 +03:00
orange 0515236c6c fix 2026-05-04 04:21:29 +03:00
orange 0215b7e0b7 using static for windows 2026-05-04 04:02:19 +03:00
orange 77b0ed3c81 fixed code style 2026-05-04 00:47:20 +03:00
orange 51bf4461ff fixed dx12 overlay 2026-05-04 00:45:48 +03:00
orange 232b48c3dd fixed dx12 hook 2026-05-04 00:10:53 +03:00
orange 105df90d05 decomposed method 2026-05-03 22:16:16 +03:00
orange 3aba53c8f8 fix 2026-05-03 21:59:48 +03:00
orange 71171acf36 added hooking of dx9 2026-05-03 21:58:51 +03:00
orange 1789b1ef51 added dx11 hook 2026-05-03 21:54:03 +03:00
orange 064d0cebbc update 2026-05-03 21:38:31 +03:00
orange 06d2752059 added dx12 hooking 2026-05-03 21:35:08 +03:00
orange 7e55b1d00e code clean up 2026-04-30 02:15:02 +03:00
77 changed files with 6198 additions and 323 deletions
+92
View File
@@ -0,0 +1,92 @@
---
name: code-style
description: omath project C++ code style derived from .idea/codeStyles/Project.xml and .clang-format. Use when writing, editing, or reviewing C++ code in this repo so formatting and naming match the rest of the codebase.
---
# omath Code Style
Authoritative sources: `.clang-format` (formatting) and `.idea/codeStyles/Project.xml` (Rider/CLion naming + formatting). When in doubt, run clang-format — it is the enforced formatter (`clangFormatSettings.ENABLED = true`).
## Formatting
Base style: LLVM with Stroustrup-style braces.
- **Indent**: 4 spaces, no tabs. Tab width 4. Continuation indent 8.
- **Column limit**: 120.
- **Namespace indentation**: `All` — indent contents of every namespace.
- **Access modifier offset**: -4 (access specifiers sit at the class column; members indent one level deeper).
- **Pointer/reference alignment**: Left, with a space *before* `*` / `&` in declarations: `const Vector3& other`, `Type* ptr`.
- **Include blocks**: Merge. Sort using-declarations.
- **Keep blank lines**: max 2 in code and declarations. No blank line at the start of a block.
- **Align trailing comments**: false.
- **Break before binary operators**: non-assignment.
### Braces (Allman / next-line for everything)
Opening brace on its own line after:
class, struct, union, enum, namespace, function, control statement (`if`/`for`/`while`/`switch`), `case` label, lambda body, `catch`, `else`, `while` (of do-while), extern block.
Empty functions, records, and namespaces still split (`SplitEmptyFunction/Record/Namespace: true`).
### Short-form rules (all disabled)
Never collapse onto one line: blocks, functions, lambdas, `if` statements, loops.
### Templates
`template<class T>` goes on its own line, declaration follows on the next line:
```cpp
template<class Type>
requires std::is_arithmetic_v<Type>
class Vector3 : public Vector2<Type>
{
...
};
```
No space after `template` keyword. `requires` clause is not extra-indented.
### Spaces
- After control-statement keywords (`if (`, `for (`, `while (`).
- **Not** before `(` in function declarations/definitions/calls.
- After C-style cast: `(int) x`.
- Around range-based-for colon: `for (auto& x : xs)`.
- After commas in template args/params.
- Inside empty parens/braces/templates: no.
## Naming
| Kind | Style | Example |
|---|---|---|
| Namespaces | `snake_case` | `omath::pathfinding`, `omath::primitives` |
| Types (class, struct, enum, union, concept, type alias, typedef, template parameter) | `PascalCase` | `Vector3`, `NavigationMesh`, `Astar`, `ContainedType` |
| Functions (free + member) | `snake_case` | `find_path`, `distance_to_sqr`, `create_box` |
| Fields (class/struct/union members) | `snake_case` | `dir_forward`, `nav_mesh` |
| Variables (global, local, lambda) | `snake_case` | `length_value`, `side_size` |
| Parameters | `snake_case` | `dir_right`, `v_other` |
| Macros | `UPPER_SNAKE_CASE` | `OMATH_FOO_BAR` |
| Enumerators | `UPPER_SNAKE_CASE` | `IMPOSSIBLE_BETWEEN_ANGLE`, `WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS` |
Enum types themselves are PascalCase (`enum class Vector3Error`, `enum class Error`); their members are UPPER_SNAKE_CASE.
## Files
- Headers: `.hpp`, sources: `.cpp`. Both use `snake_case` filenames (e.g. `vector3.hpp`, `proj_pred_engine_avx2.hpp`).
- C headers: `.h`, sources: `.c` (no enforced filename style).
- CUDA: `.cu` / `.cuh`.
- C++ modules: `.ixx`, `.mxx`, `.cppm`, `.ccm`, `.cxxm`, `.c++m`.
- Header guard: `#pragma once` only — no `#ifndef` guards.
- File header comment is optional and follows the form `// Created by <name> on <date>`.
## Idioms used throughout the codebase
- Prefer `[[nodiscard]]`, `noexcept`, and `constexpr` on math / value-type methods.
- `namespace omath` is the root; sub-features live in nested namespaces (`omath::collision`, `omath::engines::source_engine`, etc.).
- Closing namespace brace gets a trailing comment: `} // namespace omath::primitives`.
- Use `std::expected<T, E>` with an `enum class …Error` for fallible operations (see `Vector3Error`, `projection::Error`).
## When editing
Match the surrounding style exactly. If a region disagrees with this guide, prefer the existing local style — don't reformat unrelated code (per the project's CLAUDE.md "Surgical Changes" rule). Run clang-format on touched files before committing.
@@ -0,0 +1,67 @@
---
name: karpathy-guidelines
description: Behavioral guidelines to reduce common LLM coding mistakes. Use when writing, reviewing, or refactoring code to avoid overcomplication, make surgical changes, surface assumptions, and define verifiable success criteria.
license: MIT
---
# Karpathy Guidelines
Behavioral guidelines to reduce common LLM coding mistakes, derived from [Andrej Karpathy's observations](https://x.com/karpathy/status/2015883857489522876) on LLM coding pitfalls.
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
## 1. Think Before Coding
**Don't assume. Don't hide confusion. Surface tradeoffs.**
Before implementing:
- State your assumptions explicitly. If uncertain, ask.
- If multiple interpretations exist, present them - don't pick silently.
- If a simpler approach exists, say so. Push back when warranted.
- If something is unclear, stop. Name what's confusing. Ask.
## 2. Simplicity First
**Minimum code that solves the problem. Nothing speculative.**
- No features beyond what was asked.
- No abstractions for single-use code.
- No "flexibility" or "configurability" that wasn't requested.
- No error handling for impossible scenarios.
- If you write 200 lines and it could be 50, rewrite it.
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
## 3. Surgical Changes
**Touch only what you must. Clean up only your own mess.**
When editing existing code:
- Don't "improve" adjacent code, comments, or formatting.
- Don't refactor things that aren't broken.
- Match existing style, even if you'd do it differently.
- If you notice unrelated dead code, mention it - don't delete it.
When your changes create orphans:
- Remove imports/variables/functions that YOUR changes made unused.
- Don't remove pre-existing dead code unless asked.
The test: Every changed line should trace directly to the user's request.
## 4. Goal-Driven Execution
**Define success criteria. Loop until verified.**
Transform tasks into verifiable goals:
- "Add validation" → "Write tests for invalid inputs, then make them pass"
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
- "Refactor X" → "Ensure tests pass before and after"
For multi-step tasks, state a brief plan:
```
1. [Step] → verify: [check]
2. [Step] → verify: [check]
3. [Step] → verify: [check]
```
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
+92
View File
@@ -0,0 +1,92 @@
---
name: code-style
description: omath project C++ code style derived from .idea/codeStyles/Project.xml and .clang-format. Use when writing, editing, or reviewing C++ code in this repo so formatting and naming match the rest of the codebase.
---
# omath Code Style
Authoritative sources: `.clang-format` (formatting) and `.idea/codeStyles/Project.xml` (Rider/CLion naming + formatting). When in doubt, run clang-format — it is the enforced formatter (`clangFormatSettings.ENABLED = true`).
## Formatting
Base style: LLVM with Stroustrup-style braces.
- **Indent**: 4 spaces, no tabs. Tab width 4. Continuation indent 8.
- **Column limit**: 120.
- **Namespace indentation**: `All` — indent contents of every namespace.
- **Access modifier offset**: -4 (access specifiers sit at the class column; members indent one level deeper).
- **Pointer/reference alignment**: Left, with a space *before* `*` / `&` in declarations: `const Vector3& other`, `Type* ptr`.
- **Include blocks**: Merge. Sort using-declarations.
- **Keep blank lines**: max 2 in code and declarations. No blank line at the start of a block.
- **Align trailing comments**: false.
- **Break before binary operators**: non-assignment.
### Braces (Allman / next-line for everything)
Opening brace on its own line after:
class, struct, union, enum, namespace, function, control statement (`if`/`for`/`while`/`switch`), `case` label, lambda body, `catch`, `else`, `while` (of do-while), extern block.
Empty functions, records, and namespaces still split (`SplitEmptyFunction/Record/Namespace: true`).
### Short-form rules (all disabled)
Never collapse onto one line: blocks, functions, lambdas, `if` statements, loops.
### Templates
`template<class T>` goes on its own line, declaration follows on the next line:
```cpp
template<class Type>
requires std::is_arithmetic_v<Type>
class Vector3 : public Vector2<Type>
{
...
};
```
No space after `template` keyword. `requires` clause is not extra-indented.
### Spaces
- After control-statement keywords (`if (`, `for (`, `while (`).
- **Not** before `(` in function declarations/definitions/calls.
- After C-style cast: `(int) x`.
- Around range-based-for colon: `for (auto& x : xs)`.
- After commas in template args/params.
- Inside empty parens/braces/templates: no.
## Naming
| Kind | Style | Example |
|---|---|---|
| Namespaces | `snake_case` | `omath::pathfinding`, `omath::primitives` |
| Types (class, struct, enum, union, concept, type alias, typedef, template parameter) | `PascalCase` | `Vector3`, `NavigationMesh`, `Astar`, `ContainedType` |
| Functions (free + member) | `snake_case` | `find_path`, `distance_to_sqr`, `create_box` |
| Fields (class/struct/union members) | `snake_case` | `dir_forward`, `nav_mesh` |
| Variables (global, local, lambda) | `snake_case` | `length_value`, `side_size` |
| Parameters | `snake_case` | `dir_right`, `v_other` |
| Macros | `UPPER_SNAKE_CASE` | `OMATH_FOO_BAR` |
| Enumerators | `UPPER_SNAKE_CASE` | `IMPOSSIBLE_BETWEEN_ANGLE`, `WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS` |
Enum types themselves are PascalCase (`enum class Vector3Error`, `enum class Error`); their members are UPPER_SNAKE_CASE.
## Files
- Headers: `.hpp`, sources: `.cpp`. Both use `snake_case` filenames (e.g. `vector3.hpp`, `proj_pred_engine_avx2.hpp`).
- C headers: `.h`, sources: `.c` (no enforced filename style).
- CUDA: `.cu` / `.cuh`.
- C++ modules: `.ixx`, `.mxx`, `.cppm`, `.ccm`, `.cxxm`, `.c++m`.
- Header guard: `#pragma once` only — no `#ifndef` guards.
- File header comment is optional and follows the form `// Created by <name> on <date>`.
## Idioms used throughout the codebase
- Prefer `[[nodiscard]]`, `noexcept`, and `constexpr` on math / value-type methods.
- `namespace omath` is the root; sub-features live in nested namespaces (`omath::collision`, `omath::engines::source_engine`, etc.).
- Closing namespace brace gets a trailing comment: `} // namespace omath::primitives`.
- Use `std::expected<T, E>` with an `enum class …Error` for fallible operations (see `Vector3Error`, `projection::Error`).
## When editing
Match the surrounding style exactly. If a region disagrees with this guide, prefer the existing local style — don't reformat unrelated code (per the project's CLAUDE.md "Surgical Changes" rule). Run clang-format on touched files before committing.
@@ -0,0 +1,67 @@
---
name: karpathy-guidelines
description: Behavioral guidelines to reduce common LLM coding mistakes. Use when writing, reviewing, or refactoring code to avoid overcomplication, make surgical changes, surface assumptions, and define verifiable success criteria.
license: MIT
---
# Karpathy Guidelines
Behavioral guidelines to reduce common LLM coding mistakes, derived from [Andrej Karpathy's observations](https://x.com/karpathy/status/2015883857489522876) on LLM coding pitfalls.
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
## 1. Think Before Coding
**Don't assume. Don't hide confusion. Surface tradeoffs.**
Before implementing:
- State your assumptions explicitly. If uncertain, ask.
- If multiple interpretations exist, present them - don't pick silently.
- If a simpler approach exists, say so. Push back when warranted.
- If something is unclear, stop. Name what's confusing. Ask.
## 2. Simplicity First
**Minimum code that solves the problem. Nothing speculative.**
- No features beyond what was asked.
- No abstractions for single-use code.
- No "flexibility" or "configurability" that wasn't requested.
- No error handling for impossible scenarios.
- If you write 200 lines and it could be 50, rewrite it.
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
## 3. Surgical Changes
**Touch only what you must. Clean up only your own mess.**
When editing existing code:
- Don't "improve" adjacent code, comments, or formatting.
- Don't refactor things that aren't broken.
- Match existing style, even if you'd do it differently.
- If you notice unrelated dead code, mention it - don't delete it.
When your changes create orphans:
- Remove imports/variables/functions that YOUR changes made unused.
- Don't remove pre-existing dead code unless asked.
The test: Every changed line should trace directly to the user's request.
## 4. Goal-Driven Execution
**Define success criteria. Loop until verified.**
Transform tasks into verifiable goals:
- "Add validation" → "Write tests for invalid inputs, then make them pass"
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
- "Refactor X" → "Ensure tests pass before and after"
For multi-step tasks, state a brief plan:
```
1. [Step] → verify: [check]
2. [Step] → verify: [check]
3. [Step] → verify: [check]
```
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
View File
+1
View File
@@ -139,6 +139,7 @@
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppNotAllPathsReturnValue/@EntryIndexedValue" value="WARNING" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppObjectMemberMightNotBeInitialized/@EntryIndexedValue" value="WARNING" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppOutParameterMustBeWritten/@EntryIndexedValue" value="WARNING" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppOverrideWithDifferentVisibility/@EntryIndexedValue" value="WARNING" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppParameterMayBeConst/@EntryIndexedValue" value="HINT" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppParameterMayBeConstPtrOrRef/@EntryIndexedValue" value="SUGGESTION" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppParameterNamesMismatch/@EntryIndexedValue" value="HINT" type="string" />
+65
View File
@@ -0,0 +1,65 @@
# AGENTS.md
Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
## 1. Think Before Coding
**Don't assume. Don't hide confusion. Surface tradeoffs.**
Before implementing:
- State your assumptions explicitly. If uncertain, ask.
- If multiple interpretations exist, present them - don't pick silently.
- If a simpler approach exists, say so. Push back when warranted.
- If something is unclear, stop. Name what's confusing. Ask.
## 2. Simplicity First
**Minimum code that solves the problem. Nothing speculative.**
- No features beyond what was asked.
- No abstractions for single-use code.
- No "flexibility" or "configurability" that wasn't requested.
- No error handling for impossible scenarios.
- If you write 200 lines and it could be 50, rewrite it.
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
## 3. Surgical Changes
**Touch only what you must. Clean up only your own mess.**
When editing existing code:
- Don't "improve" adjacent code, comments, or formatting.
- Don't refactor things that aren't broken.
- Match existing style, even if you'd do it differently.
- If you notice unrelated dead code, mention it - don't delete it.
When your changes create orphans:
- Remove imports/variables/functions that YOUR changes made unused.
- Don't remove pre-existing dead code unless asked.
The test: Every changed line should trace directly to the user's request.
## 4. Goal-Driven Execution
**Define success criteria. Loop until verified.**
Transform tasks into verifiable goals:
- "Add validation" → "Write tests for invalid inputs, then make them pass"
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
- "Refactor X" → "Ensure tests pass before and after"
For multi-step tasks, state a brief plan:
```
1. [Step] → verify: [check]
2. [Step] → verify: [check]
3. [Step] → verify: [check]
```
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
---
**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.
+65
View File
@@ -0,0 +1,65 @@
# CLAUDE.md
Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
## 1. Think Before Coding
**Don't assume. Don't hide confusion. Surface tradeoffs.**
Before implementing:
- State your assumptions explicitly. If uncertain, ask.
- If multiple interpretations exist, present them - don't pick silently.
- If a simpler approach exists, say so. Push back when warranted.
- If something is unclear, stop. Name what's confusing. Ask.
## 2. Simplicity First
**Minimum code that solves the problem. Nothing speculative.**
- No features beyond what was asked.
- No abstractions for single-use code.
- No "flexibility" or "configurability" that wasn't requested.
- No error handling for impossible scenarios.
- If you write 200 lines and it could be 50, rewrite it.
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
## 3. Surgical Changes
**Touch only what you must. Clean up only your own mess.**
When editing existing code:
- Don't "improve" adjacent code, comments, or formatting.
- Don't refactor things that aren't broken.
- Match existing style, even if you'd do it differently.
- If you notice unrelated dead code, mention it - don't delete it.
When your changes create orphans:
- Remove imports/variables/functions that YOUR changes made unused.
- Don't remove pre-existing dead code unless asked.
The test: Every changed line should trace directly to the user's request.
## 4. Goal-Driven Execution
**Define success criteria. Loop until verified.**
Transform tasks into verifiable goals:
- "Add validation" → "Write tests for invalid inputs, then make them pass"
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
- "Refactor X" → "Ensure tests pass before and after"
For multi-step tasks, state a brief plan:
```
1. [Step] → verify: [check]
2. [Step] → verify: [check]
3. [Step] → verify: [check]
```
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
---
**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.
+22 -6
View File
@@ -1,5 +1,4 @@
cmake_minimum_required(VERSION 3.26)
file(READ VERSION OMATH_VERSION)
project(omath VERSION ${OMATH_VERSION} LANGUAGES CXX)
@@ -31,9 +30,10 @@ option(OMATH_SUPRESS_SAFETY_CHECKS
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)
option(OMATH_ENABLE_LUA
"omath bindings for lua" OFF)
option(OMATH_ENABLE_HOOKING "omath will HooksManager that can hook DirectX/OpenGL automatically" OFF)
if(VCPKG_MANIFEST_FEATURES)
foreach(omath_feature IN LISTS VCPKG_MANIFEST_FEATURES)
if(omath_feature STREQUAL "imgui")
@@ -48,6 +48,8 @@ if(VCPKG_MANIFEST_FEATURES)
set(OMATH_BUILD_EXAMPLES ON)
elseif(omath_feature STREQUAL "lua")
set(OMATH_ENABLE_LUA ON)
elseif(omath_feature STREQUAL "hooking")
set(OMATH_ENABLE_HOOKING ON)
endif()
endforeach()
@@ -80,6 +82,10 @@ if(${PROJECT_IS_TOP_LEVEL})
message(STATUS "[${PROJECT_NAME}]: Lua feature status ${OMATH_ENABLE_LUA}")
endif()
if(OMATH_STATIC_MSVC_RUNTIME_LIBRARY)
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>" CACHE STRING "" FORCE)
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")
@@ -100,6 +106,20 @@ if (OMATH_ENABLE_LUA)
target_include_directories(${PROJECT_NAME} PRIVATE ${SOL2_INCLUDE_DIRS})
endif ()
if (OMATH_ENABLE_HOOKING)
target_compile_definitions(${PROJECT_NAME} PUBLIC OMATH_ENABLE_HOOKING)
find_package(safetyhook CONFIG REQUIRED)
target_link_libraries(${PROJECT_NAME} PRIVATE safetyhook::safetyhook)
if (WIN32)
target_link_libraries(${PROJECT_NAME} PRIVATE d3d9 d3d11 d3d12 dxgi opengl32 gdi32)
elseif (UNIX AND NOT APPLE)
find_package(OpenGL REQUIRED)
target_link_libraries(${PROJECT_NAME} PRIVATE OpenGL::GL ${CMAKE_DL_LIBS})
endif ()
endif ()
add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME})
target_compile_definitions(${PROJECT_NAME} PUBLIC OMATH_VERSION="${PROJECT_VERSION}")
@@ -147,10 +167,6 @@ set_target_properties(
CXX_STANDARD 23
CXX_STANDARD_REQUIRED ON)
if(OMATH_STATIC_MSVC_RUNTIME_LIBRARY)
set_target_properties(${PROJECT_NAME} PROPERTIES MSVC_RUNTIME_LIBRARY
"MultiThreaded$<$<CONFIG:Debug>:Debug>")
endif()
if(OMATH_USE_AVX2)
if(MSVC)
+10 -6
View File
@@ -56,7 +56,9 @@
"hidden": true,
"inherits": ["windows-base", "vcpkg-base"],
"cacheVariables": {
"VCPKG_MANIFEST_FEATURES": "tests;imgui;avx2;examples"
"VCPKG_TARGET_TRIPLET": "x64-windows-static",
"VCPKG_MANIFEST_FEATURES": "tests;imgui;avx2;examples;hooking",
"OMATH_STATIC_MSVC_RUNTIME_LIBRARY": "ON"
}
},
{
@@ -89,9 +91,10 @@
"strategy": "external"
},
"cacheVariables": {
"VCPKG_TARGET_TRIPLET": "x86-windows",
"VCPKG_TARGET_TRIPLET": "x86-windows-static",
"VCPKG_HOST_TRIPLET": "x64-windows",
"VCPKG_MANIFEST_FEATURES": "tests;imgui;avx2;examples"
"VCPKG_MANIFEST_FEATURES": "tests;imgui;avx2;examples",
"OMATH_STATIC_MSVC_RUNTIME_LIBRARY": "ON"
}
},
{
@@ -114,9 +117,10 @@
"strategy": "external"
},
"cacheVariables": {
"VCPKG_TARGET_TRIPLET": "arm64-windows",
"VCPKG_HOST_TRIPLET": "arm64-windows",
"VCPKG_MANIFEST_FEATURES": "tests;imgui;examples"
"VCPKG_TARGET_TRIPLET": "arm64-windows-static",
"VCPKG_HOST_TRIPLET": "arm64-windows-static",
"VCPKG_MANIFEST_FEATURES": "tests;imgui;examples",
"OMATH_STATIC_MSVC_RUNTIME_LIBRARY": "ON"
}
},
{
+1 -1
View File
@@ -1 +1 @@
5.0.0
5.2.0
+14
View File
@@ -4,6 +4,20 @@ add_subdirectory(example_proj_mat_builder)
add_subdirectory(example_signature_scan)
add_subdirectory(example_hud)
if(OMATH_ENABLE_HOOKING AND WIN32)
# Requires imgui with dx9-binding, dx11-binding, dx12-binding, opengl3-binding, win32-binding.
# Install via: vcpkg install imgui[dx9-binding,dx11-binding,dx12-binding,opengl3-binding,win32-binding]
find_package(imgui CONFIG QUIET)
if(imgui_FOUND)
add_subdirectory(example_dx9_hook)
add_subdirectory(example_dx11_hook)
add_subdirectory(example_dx12_hook)
add_subdirectory(example_opengl_hook)
else()
message(STATUS "[omath] imgui not found - hook examples skipped")
endif()
endif()
if(OMATH_ENABLE_VALGRIND)
omath_setup_valgrind(example_projection_matrix_builder)
omath_setup_valgrind(example_signature_scan)
+13
View File
@@ -0,0 +1,13 @@
project(example_dx11_hook)
add_library(${PROJECT_NAME} SHARED dllmain.cpp)
set_target_properties(${PROJECT_NAME} PROPERTIES
CXX_STANDARD 23
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>"
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(imgui CONFIG REQUIRED)
target_link_libraries(${PROJECT_NAME} PRIVATE omath::omath imgui::imgui d3d11 dxgi)
+154
View File
@@ -0,0 +1,154 @@
#include "omath/hooks/hooks_manager.hpp"
#include <Windows.h>
#include <d3d11.h>
#include <dxgi.h>
#include <imgui.h>
#include <imgui_impl_dx11.h>
#include <imgui_impl_win32.h>
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND, UINT, WPARAM, LPARAM);
namespace
{
bool g_initialized = false;
bool g_init_attempted = false;
ID3D11Device* g_device = nullptr;
ID3D11DeviceContext* g_context = nullptr;
ID3D11RenderTargetView* g_render_target_view = nullptr;
void create_render_target(IDXGISwapChain* swap_chain)
{
ID3D11Texture2D* back_buffer = nullptr;
if (FAILED(swap_chain->GetBuffer(0, IID_PPV_ARGS(&back_buffer))))
return;
g_device->CreateRenderTargetView(back_buffer, nullptr, &g_render_target_view);
back_buffer->Release();
}
void init(IDXGISwapChain* swap_chain)
{
g_init_attempted = true;
if (FAILED(swap_chain->GetDevice(IID_PPV_ARGS(&g_device))))
return;
g_device->GetImmediateContext(&g_context);
DXGI_SWAP_CHAIN_DESC desc{};
swap_chain->GetDesc(&desc);
create_render_target(swap_chain);
ImGui::CreateContext();
ImGui::StyleColorsDark();
ImGui::GetIO().IniFilename = nullptr;
ImGui::GetIO().LogFilename = nullptr;
ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange;
ImGui_ImplWin32_Init(desc.OutputWindow);
ImGui_ImplDX11_Init(g_device, g_context);
auto& mgr = omath::hooks::HooksManager::get();
mgr.set_on_wnd_proc(
[](HWND h, UINT msg, WPARAM wp, LPARAM lp) -> std::optional<LRESULT>
{
if (ImGui_ImplWin32_WndProcHandler(h, msg, wp, lp))
return 0;
return std::nullopt;
});
std::ignore = mgr.hook_wnd_proc(desc.OutputWindow);
g_initialized = true;
}
void on_present(IDXGISwapChain* swap_chain, UINT, UINT)
{
if (!g_initialized)
{
if (!g_init_attempted)
init(swap_chain);
return;
}
if (!g_render_target_view)
create_render_target(swap_chain);
g_context->OMSetRenderTargets(1, &g_render_target_view, nullptr);
ImGui_ImplDX11_NewFrame();
ImGui_ImplWin32_NewFrame();
ImGui::NewFrame();
ImGui::SetNextWindowSize({300.f, 80.f}, ImGuiCond_Once);
ImGui::SetNextWindowPos({10.f, 10.f}, ImGuiCond_Once);
ImGui::Begin("omath | DX11 hook");
ImGui::Text("Hook active");
ImGui::Text("FPS: %.1f", ImGui::GetIO().Framerate);
ImGui::End();
ImGui::Render();
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
}
void on_resize_buffers(IDXGISwapChain*, UINT, UINT, UINT, DXGI_FORMAT, UINT)
{
if (g_render_target_view)
{
g_render_target_view->Release();
g_render_target_view = nullptr;
}
}
} // namespace
BOOL WINAPI DllMain(HINSTANCE h_instance, DWORD reason, LPVOID)
{
if (reason == DLL_PROCESS_ATTACH)
{
DisableThreadLibraryCalls(h_instance);
CreateThread(
nullptr, 0,
[](LPVOID) -> DWORD
{
while (!GetModuleHandle("d3d11.dll"))
Sleep(100);
auto& mgr = omath::hooks::HooksManager::get();
mgr.set_on_present(on_present);
mgr.set_on_resize_buffers(on_resize_buffers);
mgr.hook_dx11();
return 0;
},
nullptr, 0, nullptr);
}
else if (reason == DLL_PROCESS_DETACH)
{
auto& mgr = omath::hooks::HooksManager::get();
mgr.unhook_wnd_proc();
mgr.unhook_dx11();
if (g_initialized)
{
ImGui_ImplDX11_Shutdown();
ImGui_ImplWin32_Shutdown();
ImGui::DestroyContext();
}
if (g_render_target_view)
{
g_render_target_view->Release();
g_render_target_view = nullptr;
}
if (g_context)
{
g_context->Release();
g_context = nullptr;
}
if (g_device)
{
g_device->Release();
g_device = nullptr;
}
}
return TRUE;
}
+13
View File
@@ -0,0 +1,13 @@
project(example_dx12_hook)
add_library(${PROJECT_NAME} MODULE dllmain.cpp)
set_target_properties(${PROJECT_NAME} PROPERTIES
CXX_STANDARD 23
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>"
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(imgui CONFIG REQUIRED)
target_link_libraries(${PROJECT_NAME} PRIVATE omath::omath imgui::imgui d3d12 dxgi)
+254
View File
@@ -0,0 +1,254 @@
#include "omath/hooks/hooks_manager.hpp"
#include <Windows.h>
#include <d3d12.h>
#include <dxgi1_4.h>
#include <imgui.h>
#include <imgui_impl_dx12.h>
#include <imgui_impl_win32.h>
#include <tuple>
#include <vector>
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND, UINT, WPARAM, LPARAM);
bool show_menu = true;
namespace
{
struct frame_context
{
ID3D12Resource* render_target = nullptr;
D3D12_CPU_DESCRIPTOR_HANDLE rtv_handle = {};
};
bool g_initialized = false;
bool g_init_attempted = false;
ID3D12Device* g_device = nullptr;
ID3D12CommandQueue* g_command_queue = nullptr;
IDXGISwapChain3* g_swap_chain = nullptr;
ID3D12DescriptorHeap* g_rtv_heap = nullptr;
ID3D12DescriptorHeap* g_srv_heap = nullptr;
ID3D12GraphicsCommandList* g_command_list = nullptr;
ID3D12CommandAllocator* g_command_allocator = nullptr;
std::vector<frame_context> g_frames;
void init(IDXGISwapChain* swap_chain)
{
g_init_attempted = true;
if (FAILED(swap_chain->QueryInterface(IID_PPV_ARGS(&g_swap_chain))))
return;
if (FAILED(swap_chain->GetDevice(IID_PPV_ARGS(&g_device))))
return;
DXGI_SWAP_CHAIN_DESC desc{};
swap_chain->GetDesc(&desc);
const UINT buffer_count = desc.BufferCount;
{
D3D12_DESCRIPTOR_HEAP_DESC heap_desc{};
heap_desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
heap_desc.NumDescriptors = buffer_count;
heap_desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
if (FAILED(g_device->CreateDescriptorHeap(&heap_desc, IID_PPV_ARGS(&g_srv_heap))))
return;
}
{
D3D12_DESCRIPTOR_HEAP_DESC heap_desc{};
heap_desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
heap_desc.NumDescriptors = buffer_count;
heap_desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
heap_desc.NodeMask = 1;
if (FAILED(g_device->CreateDescriptorHeap(&heap_desc, IID_PPV_ARGS(&g_rtv_heap))))
return;
}
if (FAILED(g_device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT,
IID_PPV_ARGS(&g_command_allocator))))
return;
g_frames.resize(buffer_count);
const UINT rtv_size = g_device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
D3D12_CPU_DESCRIPTOR_HANDLE rtv_handle = g_rtv_heap->GetCPUDescriptorHandleForHeapStart();
for (UINT i = 0; i < buffer_count; ++i)
{
g_frames[i].rtv_handle = rtv_handle;
if (FAILED(swap_chain->GetBuffer(i, IID_PPV_ARGS(&g_frames[i].render_target))))
return;
g_device->CreateRenderTargetView(g_frames[i].render_target, nullptr, rtv_handle);
rtv_handle.ptr += rtv_size;
}
if (FAILED(g_device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, g_command_allocator, nullptr,
IID_PPV_ARGS(&g_command_list))))
return;
g_command_list->Close();
ImGui::CreateContext();
ImGui::StyleColorsDark();
ImGui::GetIO().IniFilename = nullptr;
ImGui::GetIO().LogFilename = nullptr;
ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
ImGui_ImplWin32_Init(desc.OutputWindow);
ImGui_ImplDX12_Init(g_device, static_cast<int>(buffer_count), desc.BufferDesc.Format, g_srv_heap,
g_srv_heap->GetCPUDescriptorHandleForHeapStart(),
g_srv_heap->GetGPUDescriptorHandleForHeapStart());
ImGui_ImplDX12_CreateDeviceObjects();
auto& mgr = omath::hooks::HooksManager::get();
mgr.set_on_wnd_proc(
[](HWND h, UINT msg, WPARAM wp, LPARAM lp) -> std::optional<LRESULT>
{
if (!show_menu)
return std::nullopt;
ImGui_ImplWin32_WndProcHandler(h, msg, wp, lp);
return true;
});
std::ignore = mgr.hook_wnd_proc(desc.OutputWindow);
g_initialized = true;
}
void on_execute_command_lists(ID3D12CommandQueue* queue, UINT, ID3D12CommandList* const*)
{
if (!g_command_queue)
g_command_queue = queue;
}
void on_present(IDXGISwapChain* swap_chain, UINT, UINT)
{
if (!g_initialized)
{
if (!g_init_attempted && g_command_queue)
init(swap_chain);
return;
}
if (!g_command_queue)
return;
if (GetAsyncKeyState(VK_INSERT) & 1)
show_menu = !show_menu;
if (!show_menu)
return;
ImGui_ImplDX12_NewFrame();
ImGui_ImplWin32_NewFrame();
ImGui::NewFrame();
ImGui::GetIO().MouseDrawCursor = true;
ImGui::ShowDemoWindow();
ImGui::EndFrame();
const UINT buf_idx = g_swap_chain->GetCurrentBackBufferIndex();
auto& fc = g_frames[buf_idx];
g_command_allocator->Reset();
g_command_list->Reset(g_command_allocator, nullptr);
D3D12_RESOURCE_BARRIER barrier{};
barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
barrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE;
barrier.Transition.pResource = fc.render_target;
barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_PRESENT;
barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET;
g_command_list->ResourceBarrier(1, &barrier);
g_command_list->OMSetRenderTargets(1, &fc.rtv_handle, FALSE, nullptr);
g_command_list->SetDescriptorHeaps(1, &g_srv_heap);
ImGui::Render();
ImGui_ImplDX12_RenderDrawData(ImGui::GetDrawData(), g_command_list);
barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET;
barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT;
g_command_list->ResourceBarrier(1, &barrier);
g_command_list->Close();
ID3D12CommandList* cmd_lists[] = {g_command_list};
g_command_queue->ExecuteCommandLists(1, cmd_lists);
}
void release_dx12_resources()
{
for (auto& fc : g_frames)
{
if (fc.render_target)
{
fc.render_target->Release();
fc.render_target = nullptr;
}
}
g_frames.clear();
if (g_command_allocator)
{
g_command_allocator->Release();
g_command_allocator = nullptr;
}
if (g_command_list)
{
g_command_list->Release();
g_command_list = nullptr;
}
if (g_srv_heap)
{
g_srv_heap->Release();
g_srv_heap = nullptr;
}
if (g_rtv_heap)
{
g_rtv_heap->Release();
g_rtv_heap = nullptr;
}
if (g_swap_chain)
{
g_swap_chain->Release();
g_swap_chain = nullptr;
}
if (g_device)
{
g_device->Release();
g_device = nullptr;
}
}
} // namespace
BOOL WINAPI DllMain(HINSTANCE h_instance, DWORD reason, LPVOID)
{
if (reason == DLL_PROCESS_ATTACH)
{
DisableThreadLibraryCalls(h_instance);
CreateThread(
nullptr, 0,
[](LPVOID) -> DWORD
{
while (!GetModuleHandle("d3d12.dll"))
Sleep(100);
auto& mgr = omath::hooks::HooksManager::get();
mgr.set_on_present(on_present);
mgr.set_on_execute_command_lists(on_execute_command_lists);
std::ignore = mgr.hook_dx12();
return 0;
},
nullptr, 0, nullptr);
}
else if (reason == DLL_PROCESS_DETACH)
{
auto& mgr = omath::hooks::HooksManager::get();
mgr.unhook_wnd_proc();
mgr.unhook_dx12();
if (g_initialized)
{
ImGui_ImplDX12_Shutdown();
ImGui_ImplWin32_Shutdown();
ImGui::DestroyContext();
}
release_dx12_resources();
}
return TRUE;
}
+13
View File
@@ -0,0 +1,13 @@
project(example_dx9_hook)
add_library(${PROJECT_NAME} MODULE dllmain.cpp)
set_target_properties(${PROJECT_NAME} PROPERTIES
CXX_STANDARD 23
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>"
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(imgui CONFIG REQUIRED)
target_link_libraries(${PROJECT_NAME} PRIVATE omath::omath imgui::imgui d3d9)
+107
View File
@@ -0,0 +1,107 @@
#include <Windows.h>
#include <d3d9.h>
#include <imgui.h>
#include <imgui_impl_dx9.h>
#include <imgui_impl_win32.h>
#include "omath/hooks/hooks_manager.hpp"
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND, UINT, WPARAM, LPARAM);
namespace
{
bool g_initialized = false;
bool g_init_attempted = false;
void init(IDirect3DDevice9* device)
{
g_init_attempted = true;
D3DDEVICE_CREATION_PARAMETERS params{};
if (FAILED(device->GetCreationParameters(&params)))
return;
ImGui::CreateContext();
ImGui::StyleColorsDark();
ImGui::GetIO().IniFilename = nullptr;
ImGui::GetIO().LogFilename = nullptr;
ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange;
ImGui_ImplWin32_Init(params.hFocusWindow);
ImGui_ImplDX9_Init(device);
auto& mgr = omath::hooks::HooksManager::get();
mgr.set_on_wnd_proc([](HWND h, UINT msg, WPARAM wp, LPARAM lp) -> std::optional<LRESULT> {
if (ImGui_ImplWin32_WndProcHandler(h, msg, wp, lp))
return 0;
return std::nullopt;
});
mgr.hook_wnd_proc(params.hFocusWindow);
g_initialized = true;
}
void on_present(IDirect3DDevice9* device, const RECT*, const RECT*, HWND, const RGNDATA*)
{
if (!g_initialized)
{
if (!g_init_attempted)
init(device);
return;
}
ImGui_ImplDX9_NewFrame();
ImGui_ImplWin32_NewFrame();
ImGui::NewFrame();
ImGui::SetNextWindowSize({300.f, 80.f}, ImGuiCond_Once);
ImGui::SetNextWindowPos({10.f, 10.f}, ImGuiCond_Once);
ImGui::Begin("omath | DX9 hook");
ImGui::Text("Hook active");
ImGui::Text("FPS: %.1f", ImGui::GetIO().Framerate);
ImGui::End();
ImGui::EndFrame();
ImGui::Render();
ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData());
}
void on_reset(IDirect3DDevice9*, D3DPRESENT_PARAMETERS*)
{
if (g_initialized)
ImGui_ImplDX9_InvalidateDeviceObjects();
}
} // namespace
BOOL WINAPI DllMain(HINSTANCE h_instance, DWORD reason, LPVOID)
{
if (reason == DLL_PROCESS_ATTACH)
{
DisableThreadLibraryCalls(h_instance);
CreateThread(nullptr, 0, [](LPVOID) -> DWORD
{
while (!GetModuleHandle("d3d9.dll"))
Sleep(100);
auto& mgr = omath::hooks::HooksManager::get();
mgr.set_on_dx9_present(on_present);
mgr.set_on_dx9_reset(on_reset);
mgr.hook_dx9();
return 0;
}, nullptr, 0, nullptr);
}
else if (reason == DLL_PROCESS_DETACH)
{
auto& mgr = omath::hooks::HooksManager::get();
mgr.unhook_wnd_proc();
mgr.unhook_dx9();
if (g_initialized)
{
ImGui_ImplDX9_Shutdown();
ImGui_ImplWin32_Shutdown();
ImGui::DestroyContext();
}
}
return TRUE;
}
+3 -2
View File
@@ -82,6 +82,7 @@ namespace imgui_desktop::gui
ImGui::Checkbox("Dashed", &m_show_dashed_box);
ImGui::ColorEdit4("Color##box", reinterpret_cast<float*>(&m_box_color), ImGuiColorEditFlags_NoInputs);
ImGui::ColorEdit4("Fill##box", reinterpret_cast<float*>(&m_box_fill), ImGuiColorEditFlags_NoInputs);
ImGui::ColorEdit4("Outline##box", reinterpret_cast<float*>(&m_box_outline), ImGuiColorEditFlags_NoInputs);
ImGui::SliderFloat("Thickness", &m_box_thickness, 0.5f, 5.f);
ImGui::SliderFloat("Corner ratio", &m_corner_ratio, 0.05f, 0.5f);
ImGui::Separator();
@@ -199,9 +200,9 @@ namespace imgui_desktop::gui
std::make_shared<omath::hud::ImguiHudRenderer>())
.contents(
// ── Boxes ────────────────────────────────────────────────────
when(m_show_box, Box{m_box_color, m_box_fill, m_box_thickness}),
when(m_show_box, Box{m_box_color, m_box_fill, m_box_outline, m_box_thickness}),
when(m_show_cornered_box, CorneredBox{omath::Color::from_rgba(255, 0, 255, 255), m_box_fill,
m_corner_ratio, m_box_thickness}),
m_box_outline, m_corner_ratio, m_box_thickness}),
when(m_show_dashed_box, DashedBox{m_dash_color, m_dash_len, m_dash_gap, m_dash_thickness}),
RightSide{
when(m_show_right_bar, bar),
+1
View File
@@ -31,6 +31,7 @@ namespace imgui_desktop::gui
// Box
omath::Color m_box_color{1.f, 1.f, 1.f, 1.f};
omath::Color m_box_fill{0.f, 0.f, 0.f, 0.f};
omath::Color m_box_outline{0.f, 0.f, 0.f, 0.f};
float m_box_thickness = 1.f, m_corner_ratio = 0.2f;
bool m_show_box = true, m_show_cornered_box = true, m_show_dashed_box = false;
@@ -0,0 +1,13 @@
project(example_opengl_hook)
add_library(${PROJECT_NAME} MODULE dllmain.cpp)
set_target_properties(${PROJECT_NAME} PROPERTIES
CXX_STANDARD 23
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>"
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(imgui CONFIG REQUIRED)
target_link_libraries(${PROJECT_NAME} PRIVATE omath::omath imgui::imgui opengl32 gdi32)
+116
View File
@@ -0,0 +1,116 @@
#include "omath/hooks/hooks_manager.hpp"
#include <Windows.h>
#include <chrono>
#include <imgui.h>
#include <imgui_impl_opengl3.h>
#include <imgui_impl_win32.h>
#include <optional>
#include <thread>
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND, UINT, WPARAM, LPARAM);
namespace
{
bool g_initialized = false;
bool g_init_attempted = false;
bool g_show_menu = true;
constexpr auto g_module_wait_delay = std::chrono::milliseconds{100};
void init(HDC hdc)
{
g_init_attempted = true;
const HWND hwnd = WindowFromDC(hdc);
if (!hwnd)
return;
ImGui::CreateContext();
ImGui::StyleColorsDark();
ImGui::GetIO().IniFilename = nullptr;
ImGui::GetIO().LogFilename = nullptr;
ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange;
ImGui_ImplWin32_Init(hwnd);
ImGui_ImplOpenGL3_Init();
auto& mgr = omath::hooks::HooksManager::get();
mgr.set_on_wnd_proc(
[](HWND h, UINT msg, WPARAM wp, LPARAM lp) -> std::optional<LRESULT>
{
if (!g_show_menu)
return std::nullopt;
if (ImGui_ImplWin32_WndProcHandler(h, msg, wp, lp))
return 0;
return std::nullopt;
});
(void)mgr.hook_wnd_proc(hwnd);
g_initialized = true;
}
void on_swap_buffers(HDC hdc)
{
if (!g_initialized)
{
if (!g_init_attempted)
init(hdc);
return;
}
if (GetAsyncKeyState(VK_INSERT) & 1)
g_show_menu = !g_show_menu;
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplWin32_NewFrame();
ImGui::NewFrame();
if (g_show_menu)
{
ImGui::SetNextWindowSize({300.f, 100.f}, ImGuiCond_Once);
ImGui::SetNextWindowPos({10.f, 10.f}, ImGuiCond_Once);
ImGui::Begin("omath | OpenGL hook");
ImGui::Text("Hook active");
ImGui::Text("FPS: %.1f", ImGui::GetIO().Framerate);
ImGui::Text("INSERT toggles this window");
ImGui::End();
}
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
}
void hook_when_opengl_is_loaded()
{
while (!GetModuleHandle("opengl32.dll"))
std::this_thread::sleep_for(g_module_wait_delay);
auto& mgr = omath::hooks::HooksManager::get();
mgr.set_on_opengl_swap_buffers(on_swap_buffers);
(void)mgr.hook_opengl();
}
} // namespace
BOOL WINAPI DllMain(HINSTANCE h_instance, DWORD reason, LPVOID)
{
if (reason == DLL_PROCESS_ATTACH)
{
DisableThreadLibraryCalls(h_instance);
std::thread{hook_when_opengl_is_loaded}.detach();
}
else if (reason == DLL_PROCESS_DETACH)
{
auto& mgr = omath::hooks::HooksManager::get();
mgr.unhook_wnd_proc();
mgr.unhook_opengl();
if (g_initialized)
{
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplWin32_Shutdown();
ImGui::DestroyContext();
}
}
return true;
}
+37
View File
@@ -0,0 +1,37 @@
//
// Created by Vladislav on 07.05.2026.
//
#pragma once
#include "omath/linear_algebra/vector3.hpp"
#include <array>
#include <type_traits>
namespace omath::primitives
{
// Oriented bounding box: a rectangular cuboid defined by a center, three
// orthonormal local axes, and the half-size along each of those axes.
template<class Type>
requires std::is_floating_point_v<Type>
struct Obb final
{
Vector3<Type> center;
Vector3<Type> axis_x;
Vector3<Type> axis_y;
Vector3<Type> axis_z;
Vector3<Type> half_extents;
[[nodiscard]]
constexpr std::array<Vector3<Type>, 8> vertices() const noexcept
{
const auto ex = axis_x * half_extents.x;
const auto ey = axis_y * half_extents.y;
const auto ez = axis_z * half_extents.z;
return {
center - ex - ey - ez, center + ex - ey - ez, center - ex + ey - ez, center + ex + ey - ez,
center - ex - ey + ez, center + ex - ey + ez, center - ex + ey + ez, center + ex + ey + ez,
};
}
};
} // namespace omath::primitives
+1 -1
View File
@@ -49,7 +49,7 @@ namespace omath::collision
struct Params final
{
int max_iterations{64};
FloatingType tolerance{1e-4}; // absolute tolerance on distance growth
FloatingType tolerance{1e-4f}; // absolute tolerance on distance growth
};
// Precondition: simplex.size()==4 and contains the origin.
[[nodiscard]]
+57
View File
@@ -4,6 +4,7 @@
#pragma once
#include "omath/3d_primitives/aabb.hpp"
#include "omath/3d_primitives/obb.hpp"
#include "omath/linear_algebra/triangle.hpp"
#include "omath/linear_algebra/vector3.hpp"
@@ -36,6 +37,7 @@ namespace omath::collision
{
using TriangleType = Triangle<typename RayType::VectorType>;
using AABBType = primitives::Aabb<typename RayType::VectorType::ContainedType>;
using OBBType = primitives::Obb<typename RayType::VectorType::ContainedType>;
public:
LineTracer() = delete;
@@ -137,6 +139,61 @@ namespace omath::collision
return ray.start + dir * t_hit;
}
// Slab method ray-OBB intersection. Project the ray into the OBB's local frame
// (axes are orthonormal, so the inverse rotation is just a transpose / dot products),
// then run the standard slab test against the local box [-half_extents, +half_extents].
// The ray parameter t is invariant under rigid transform, so the hit point is recovered
// in world space as ray.start + dir * t_hit.
[[nodiscard]]
constexpr static auto get_ray_hit_point(const RayType& ray, const OBBType& obb) noexcept
{
using T = typename RayType::VectorType::ContainedType;
const auto offset = ray.start - obb.center;
const auto dir = ray.direction_vector();
const T local_start[3] = {offset.dot(obb.axis_x), offset.dot(obb.axis_y), offset.dot(obb.axis_z)};
const T local_dir[3] = {dir.dot(obb.axis_x), dir.dot(obb.axis_y), dir.dot(obb.axis_z)};
const T half[3] = {obb.half_extents.x, obb.half_extents.y, obb.half_extents.z};
auto t_min = -std::numeric_limits<T>::infinity();
auto t_max = std::numeric_limits<T>::infinity();
const auto process_axis = [&](const T& d, const T& origin, const T& h) -> bool
{
constexpr T k_epsilon = std::numeric_limits<T>::epsilon();
if (std::abs(d) < k_epsilon)
return origin >= -h && origin <= h;
const T inv = T(1) / d;
T t0 = (-h - origin) * inv;
T t1 = (h - origin) * inv;
if (t0 > t1)
std::swap(t0, t1);
t_min = std::max(t_min, t0);
t_max = std::min(t_max, t1);
return t_min <= t_max;
};
if (!process_axis(local_dir[0], local_start[0], half[0]))
return ray.end;
if (!process_axis(local_dir[1], local_start[1], half[1]))
return ray.end;
if (!process_axis(local_dir[2], local_start[2], half[2]))
return ray.end;
const T t_hit = std::max(T(0), t_min);
if (t_max < T(0))
return ray.end; // box entirely behind origin
if (!ray.infinite_length && t_hit > T(1))
return ray.end; // box beyond ray endpoint
return ray.start + dir * t_hit;
}
template<class MeshType>
[[nodiscard]]
constexpr static auto get_ray_hit_point(const RayType& ray, const MeshType& mesh) noexcept
@@ -15,7 +15,7 @@ namespace omath::cry_engine
constexpr Vector3<float> k_abs_forward = {0, 1, 0};
using Mat4X4 = Mat<4, 4, float, MatStoreType::ROW_MAJOR>;
using Mat3X3 = Mat<4, 4, float, MatStoreType::ROW_MAJOR>;
using Mat3X3 = Mat<3, 3, float, MatStoreType::ROW_MAJOR>;
using Mat1X3 = Mat<1, 3, float, MatStoreType::ROW_MAJOR>;
using PitchAngle = Angle<float, -90.f, 90.f, AngleFlags::Clamped>;
using YawAngle = Angle<float, -180.f, 180.f, AngleFlags::Normalized>;
+10 -1
View File
@@ -21,9 +21,18 @@ namespace omath::cry_engine
[[nodiscard]]
Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept;
[[nodiscard]]
Vector3<float> extract_origin(const Mat4X4& mat) noexcept;
[[nodiscard]]
Vector3<float> extract_scale(const Mat4X4& mat) noexcept;
[[nodiscard]]
ViewAngles extract_rotation_angles(const Mat4X4& mat) noexcept;
[[nodiscard]]
Mat4X4 calc_perspective_projection_matrix(float field_of_view, float aspect_ratio, float near, float far,
NDCDepthRange ndc_depth_range = NDCDepthRange::NEGATIVE_ONE_TO_ONE) noexcept;
NDCDepthRange ndc_depth_range = NDCDepthRange::ZERO_TO_ONE) noexcept;
template<class FloatingType>
requires std::is_floating_point_v<FloatingType>
@@ -21,6 +21,15 @@ namespace omath::frostbite_engine
[[nodiscard]]
Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept;
[[nodiscard]]
Vector3<float> extract_origin(const Mat4X4& mat) noexcept;
[[nodiscard]]
Vector3<float> extract_scale(const Mat4X4& mat) noexcept;
[[nodiscard]]
ViewAngles extract_rotation_angles(const Mat4X4& mat) noexcept;
[[nodiscard]]
Mat4X4 calc_perspective_projection_matrix(float field_of_view, float aspect_ratio, float near, float far,
NDCDepthRange ndc_depth_range = NDCDepthRange::NEGATIVE_ONE_TO_ONE) noexcept;
@@ -19,6 +19,15 @@ namespace omath::iw_engine
[[nodiscard]]
Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept;
[[nodiscard]]
Vector3<float> extract_origin(const Mat4X4& mat) noexcept;
[[nodiscard]]
Vector3<float> extract_scale(const Mat4X4& mat) noexcept;
[[nodiscard]]
ViewAngles extract_rotation_angles(const Mat4X4& mat) noexcept;
[[nodiscard]] Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3<float>& cam_origin) noexcept;
[[nodiscard]]
@@ -20,6 +20,15 @@ namespace omath::opengl_engine
[[nodiscard]]
Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept;
[[nodiscard]]
Vector3<float> extract_origin(const Mat4X4& mat) noexcept;
[[nodiscard]]
Vector3<float> extract_scale(const Mat4X4& mat) noexcept;
[[nodiscard]]
ViewAngles extract_rotation_angles(const Mat4X4& mat) noexcept;
[[nodiscard]]
Mat4X4 calc_perspective_projection_matrix(float field_of_view, float aspect_ratio, float near, float far,
NDCDepthRange ndc_depth_range = NDCDepthRange::NEGATIVE_ONE_TO_ONE) noexcept;
@@ -12,6 +12,15 @@ namespace omath::source_engine
[[nodiscard]]
Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept;
[[nodiscard]]
Vector3<float> extract_origin(const Mat4X4& mat) noexcept;
[[nodiscard]]
Vector3<float> extract_scale(const Mat4X4& mat) noexcept;
[[nodiscard]]
ViewAngles extract_rotation_angles(const Mat4X4& mat) noexcept;
[[nodiscard]]
Vector3<float> right_vector(const ViewAngles& angles) noexcept;
@@ -21,6 +21,15 @@ namespace omath::unity_engine
[[nodiscard]]
Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept;
[[nodiscard]]
Vector3<float> extract_origin(const Mat4X4& mat) noexcept;
[[nodiscard]]
Vector3<float> extract_scale(const Mat4X4& mat) noexcept;
[[nodiscard]]
ViewAngles extract_rotation_angles(const Mat4X4& mat) noexcept;
[[nodiscard]]
Mat4X4 calc_perspective_projection_matrix(float field_of_view, float aspect_ratio, float near, float far,
NDCDepthRange ndc_depth_range = NDCDepthRange::NEGATIVE_ONE_TO_ONE) noexcept;
@@ -21,6 +21,15 @@ namespace omath::unreal_engine
[[nodiscard]]
Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept;
[[nodiscard]]
Vector3<double> extract_origin(const Mat4X4& mat) noexcept;
[[nodiscard]]
Vector3<double> extract_scale(const Mat4X4& mat) noexcept;
[[nodiscard]]
ViewAngles extract_rotation_angles(const Mat4X4& mat) noexcept;
[[nodiscard]]
Mat4X4 calc_perspective_projection_matrix(double field_of_view, double aspect_ratio, double near, double far,
NDCDepthRange ndc_depth_range = NDCDepthRange::NEGATIVE_ONE_TO_ONE) noexcept;
+214
View File
@@ -0,0 +1,214 @@
#pragma once
#ifdef OMATH_ENABLE_HOOKING
#include <cstdint>
#include <functional>
#include <memory>
#include <optional>
#include <shared_mutex>
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#include <d3d12.h>
#include <d3d9.h>
#include <dxgi.h>
#endif // _WIN32
#ifdef __linux__
#include <GL/glx.h>
#endif // __linux__
#include <safetyhook.hpp>
namespace omath::hooks
{
class HooksManager final
{
HooksManager() = default;
public:
#ifdef _WIN32
// IDXGISwapChain callbacks — shared between DX11 and DX12 (same interface, same signature).
using present_callback = std::function<void(IDXGISwapChain*, UINT, UINT)>;
using resize_buffers_callback = std::function<void(IDXGISwapChain*, UINT, UINT, UINT, DXGI_FORMAT, UINT)>;
using execute_command_lists_callback =
std::function<void(ID3D12CommandQueue*, UINT, ID3D12CommandList* const*)>;
// IDirect3DDevice9 callbacks — DX9 only.
using dx9_present_callback =
std::function<void(IDirect3DDevice9*, const RECT*, const RECT*, HWND, const RGNDATA*)>;
using dx9_reset_callback = std::function<void(IDirect3DDevice9*, D3DPRESENT_PARAMETERS*)>;
using dx9_end_scene_callback = std::function<void(IDirect3DDevice9*)>;
// OpenGL callback — Windows. Fires before the hooked buffer swap function calls the original.
using opengl_swap_buffers_callback = std::function<void(HDC)>;
// Return nullopt to pass the message to the original WndProc; return a value to intercept it.
using wnd_proc_callback = std::function<std::optional<LRESULT>(HWND, UINT, WPARAM, LPARAM)>;
#endif // _WIN32
#ifdef __linux__
// OpenGL/GLX callback — Linux. Fires before glXSwapBuffers calls the original.
using opengl_swap_buffers_callback = std::function<void(Display*, GLXDrawable)>;
#endif // __linux__
template<typename Callback>
using callback_ptr = std::shared_ptr<const Callback>;
[[nodiscard]] static HooksManager& get();
HooksManager(const HooksManager&) = delete;
HooksManager& operator=(const HooksManager&) = delete;
~HooksManager();
#ifdef _WIN32
[[nodiscard]] bool hook_dx9();
void unhook_dx9();
void set_on_dx9_present(dx9_present_callback callback);
void set_on_dx9_reset(dx9_reset_callback callback);
void set_on_dx9_end_scene(dx9_end_scene_callback callback);
[[nodiscard]] bool hook_dx11();
void unhook_dx11();
[[nodiscard]] bool hook_dx12();
void unhook_dx12();
#endif // _WIN32
[[nodiscard]] bool hook_opengl();
void unhook_opengl();
void set_on_opengl_swap_buffers(opengl_swap_buffers_callback callback);
#ifdef _WIN32
// Present and ResizeBuffers callbacks fire for whichever of DX11/DX12 is hooked.
void set_on_present(present_callback callback);
void set_on_resize_buffers(resize_buffers_callback callback);
void set_on_execute_command_lists(execute_command_lists_callback callback);
[[nodiscard]] bool hook_wnd_proc(HWND hwnd);
void unhook_wnd_proc();
void set_on_wnd_proc(wnd_proc_callback callback);
#endif // _WIN32
private:
#ifdef _WIN32
[[nodiscard]]
static HRESULT __stdcall dx9_present_detour(IDirect3DDevice9* p_device, const RECT* p_source_rect,
const RECT* p_dest_rect, HWND h_dest_window_override,
const RGNDATA* p_dirty_region);
[[nodiscard]]
static HRESULT __stdcall dx9_reset_detour(IDirect3DDevice9* p_device,
D3DPRESENT_PARAMETERS* p_presentation_parameters);
[[nodiscard]]
static HRESULT __stdcall dx9_end_scene_detour(IDirect3DDevice9* p_device);
[[nodiscard]]
static HRESULT __stdcall dx11_present_detour(IDXGISwapChain* p_swap_chain, UINT sync_interval, UINT flags);
[[nodiscard]]
static HRESULT __stdcall dx11_resize_buffers_detour(IDXGISwapChain* p_swap_chain, UINT buffer_count, UINT width,
UINT height, DXGI_FORMAT new_format, UINT swap_chain_flags);
[[nodiscard]]
static HRESULT __stdcall dx12_present_detour(IDXGISwapChain* p_swap_chain, UINT sync_interval, UINT flags);
[[nodiscard]]
static HRESULT __stdcall dx12_resize_buffers_detour(IDXGISwapChain* p_swap_chain, UINT buffer_count, UINT width,
UINT height, DXGI_FORMAT new_format, UINT swap_chain_flags);
static void __stdcall dx12_execute_command_lists_detour(ID3D12CommandQueue* p_command_queue,
UINT num_command_lists,
ID3D12CommandList* const* pp_command_lists);
[[nodiscard]]
static BOOL __stdcall opengl_wgl_swap_buffers_detour(HDC hdc);
[[nodiscard]]
static BOOL __stdcall opengl_swap_buffers_detour(HDC hdc);
[[nodiscard]]
static LRESULT __stdcall wnd_proc_detour(HWND hwnd, UINT msg, WPARAM w_param, LPARAM l_param);
#endif // _WIN32
#ifdef __linux__
static void opengl_glx_swap_buffers_detour(Display* display, GLXDrawable drawable);
#endif // __linux__
mutable std::shared_mutex m_hook_state_mutex;
#ifdef _WIN32
mutable std::shared_mutex m_dx9_present_mutex;
mutable std::shared_mutex m_dx9_reset_mutex;
mutable std::shared_mutex m_dx9_end_scene_mutex;
mutable std::shared_mutex m_present_mutex;
mutable std::shared_mutex m_resize_buffers_mutex;
mutable std::shared_mutex m_execute_command_lists_mutex;
mutable std::shared_mutex m_wnd_proc_mutex;
#endif // _WIN32
mutable std::shared_mutex m_opengl_swap_buffers_mutex;
#ifdef _WIN32
bool m_is_dx9_hooked = false;
bool m_is_dx11_hooked = false;
bool m_is_dx12_hooked = false;
bool m_is_wnd_proc_hooked = false;
HWND m_hooked_hwnd = nullptr;
WNDPROC m_original_wndproc = nullptr;
safetyhook::InlineHook m_dx9_present_hook;
safetyhook::InlineHook m_dx9_reset_hook;
safetyhook::InlineHook m_dx9_end_scene_hook;
safetyhook::InlineHook m_dx11_present_hook;
safetyhook::InlineHook m_dx11_resize_buffers_hook;
safetyhook::InlineHook m_dx12_present_hook;
safetyhook::InlineHook m_dx12_resize_buffers_hook;
safetyhook::InlineHook m_dx12_execute_command_lists_hook;
safetyhook::InlineHook m_opengl_wgl_swap_buffers_hook;
safetyhook::InlineHook m_opengl_swap_buffers_hook;
#endif // _WIN32
#ifdef __linux__
safetyhook::InlineHook m_opengl_glx_swap_buffers_hook;
#endif // __linux__
bool m_is_opengl_hooked = false;
#ifdef _WIN32
callback_ptr<dx9_present_callback> m_dx9_present_cb;
callback_ptr<dx9_reset_callback> m_dx9_reset_cb;
callback_ptr<dx9_end_scene_callback> m_dx9_end_scene_cb;
callback_ptr<present_callback> m_present_cb;
callback_ptr<resize_buffers_callback> m_resize_buffers_cb;
callback_ptr<execute_command_lists_callback> m_execute_command_lists_cb;
callback_ptr<wnd_proc_callback> m_wnd_proc_cb;
#endif // _WIN32
callback_ptr<opengl_swap_buffers_callback> m_opengl_swap_buffers_cb;
};
} // namespace omath::hooks
#else // !OMATH_ENABLE_HOOKING
namespace omath::hooks
{
class HooksManager final
{
HooksManager() = default;
public:
[[nodiscard]] static HooksManager& get();
HooksManager(const HooksManager&) = delete;
~HooksManager();
};
} // namespace omath::hooks
#endif
+2 -1
View File
@@ -20,9 +20,10 @@ namespace omath::hud
// ── Boxes ────────────────────────────────────────────────────────
EntityOverlay& add_2d_box(const Color& box_color, const Color& fill_color = Color{0.f, 0.f, 0.f, 0.f},
float thickness = 1.f);
const Color& outline_color = Color{0.f, 0.f, 0.f, 0.f}, float thickness = 1.f);
EntityOverlay& add_cornered_2d_box(const Color& box_color, const Color& fill_color = Color{0.f, 0.f, 0.f, 0.f},
const Color& outline_color = Color{0.f, 0.f, 0.f, 0.f},
float corner_ratio_len = 0.2f, float thickness = 1.f);
EntityOverlay& add_dashed_box(const Color& color, float dash_len = 8.f, float gap_len = 5.f,
@@ -26,6 +26,7 @@ namespace omath::hud::widget
{
Color color;
Color fill{0.f, 0.f, 0.f, 0.f};
Color outline{0.f, 0.f, 0.f, 0.f};
float thickness = 1.f;
};
@@ -33,6 +34,7 @@ namespace omath::hud::widget
{
Color color;
Color fill{0.f, 0.f, 0.f, 0.f};
Color outline{0.f, 0.f, 0.f, 0.f};
float corner_ratio = 0.2f;
float thickness = 1.f;
};
+101 -52
View File
@@ -5,6 +5,7 @@
#include "vector3.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <iomanip>
#include <numeric>
#include <sstream>
@@ -58,7 +59,7 @@ namespace omath
clear();
}
[[nodiscard]]
[[nodiscard("You must use store ordering")]]
consteval static MatStoreType get_store_ordering() noexcept
{
return StoreType;
@@ -93,13 +94,13 @@ namespace omath
m_data = other.m_data;
}
[[nodiscard]]
[[nodiscard("You must use element reference")]]
constexpr Type& operator[](const size_t row, const size_t col)
{
return at(row, col);
}
[[nodiscard]]
[[nodiscard("You must use element reference")]]
constexpr const Type& operator[](const size_t row, const size_t col) const
{
return at(row, col);
@@ -110,25 +111,25 @@ namespace omath
m_data = std::move(other.m_data);
}
[[nodiscard]]
[[nodiscard("You must use row count")]]
static constexpr size_t row_count() noexcept
{
return Rows;
}
[[nodiscard]]
[[nodiscard("You must use column count")]]
static constexpr size_t columns_count() noexcept
{
return Columns;
}
[[nodiscard]]
static consteval MatSize size() noexcept
[[nodiscard("You must use matrix size")]]
static constexpr MatSize size() noexcept
{
return {Rows, Columns};
}
[[nodiscard]]
[[nodiscard("You must use element reference")]]
constexpr const Type& at(const size_t row_index, const size_t column_index) const
{
#if !defined(NDEBUG) && defined(OMATH_SUPRESS_SAFETY_CHECKS)
@@ -148,12 +149,12 @@ namespace omath
}
}
[[nodiscard]] constexpr Type& at(const size_t row_index, const size_t column_index)
[[nodiscard("You must use element reference")]] constexpr Type& at(const size_t row_index, const size_t column_index)
{
return const_cast<Type&>(std::as_const(*this).at(row_index, column_index));
}
[[nodiscard]]
[[nodiscard("You must use sum of elements")]]
constexpr Type sum() const noexcept
{
return std::accumulate(m_data.begin(), m_data.end(), static_cast<Type>(0));
@@ -170,7 +171,7 @@ namespace omath
}
// Operator overloading for multiplication with another Mat
template<size_t OtherColumns> [[nodiscard]]
template<size_t OtherColumns> [[nodiscard("You must use result matrix")]]
constexpr Mat<Rows, OtherColumns, Type, StoreType>
operator*(const Mat<Columns, OtherColumns, Type, StoreType>& other) const
{
@@ -201,7 +202,7 @@ namespace omath
return *this = *this * other;
}
[[nodiscard]]
[[nodiscard("You must use result matrix")]]
constexpr Mat operator*(const Type& value) const noexcept
{
Mat result(*this);
@@ -215,7 +216,7 @@ namespace omath
return *this;
}
[[nodiscard]]
[[nodiscard("You must use result matrix")]]
constexpr Mat operator/(const Type& value) const noexcept
{
Mat result(*this);
@@ -239,7 +240,7 @@ namespace omath
return *this;
}
[[nodiscard]]
[[nodiscard("You must use transposed matrix")]]
constexpr Mat<Columns, Rows, Type, StoreType> transposed() const noexcept
{
Mat<Columns, Rows, Type, StoreType> transposed;
@@ -250,7 +251,7 @@ namespace omath
return transposed;
}
[[nodiscard]]
[[nodiscard("You must use determinant")]]
constexpr Type determinant() const
{
static_assert(Rows == Columns, "Determinant is only defined for square matrices.");
@@ -271,10 +272,11 @@ namespace omath
}
return det;
}
std::unreachable();
else // For no reason MSVC triggers on it as unreachable code so we keep else here.
std::unreachable();
}
[[nodiscard]]
[[nodiscard("You must use stripped matrix")]]
constexpr Mat<Rows - 1, Columns - 1, Type, StoreType> strip(const size_t row, const size_t column) const
{
static_assert(Rows - 1 > 0 && Columns - 1 > 0);
@@ -295,32 +297,32 @@ namespace omath
return result;
}
[[nodiscard]]
[[nodiscard("You must use minor")]]
constexpr Type minor(const size_t row, const size_t column) const
{
return strip(row, column).determinant();
}
[[nodiscard]]
[[nodiscard("You must use algebraic complement")]]
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]]
[[nodiscard("You must use raw array")]]
constexpr const std::array<Type, Rows * Columns>& raw_array() const
{
return m_data;
}
[[nodiscard]]
[[nodiscard("You must use raw array")]]
constexpr std::array<Type, Rows * Columns>& raw_array()
{
return m_data;
}
[[nodiscard]]
[[nodiscard("You must use string representation")]]
std::string to_string() const noexcept
{
std::ostringstream oss;
@@ -342,14 +344,14 @@ namespace omath
return oss.str();
}
[[nodiscard]]
[[nodiscard("You must use wide string representation")]]
std::wstring to_wstring() const noexcept
{
const auto ascii_string = to_string();
return {ascii_string.cbegin(), ascii_string.cend()};
}
[[nodiscard]]
[[nodiscard("You must use UTF-8 string representation")]]
// ReSharper disable once CppInconsistentNaming
std::u8string to_u8string() const noexcept
{
@@ -357,20 +359,20 @@ namespace omath
return {ascii_string.cbegin(), ascii_string.cend()};
}
[[nodiscard]]
[[nodiscard("You must use comparison result")]]
bool operator==(const Mat& mat) const
{
return m_data == mat.m_data;
}
[[nodiscard]]
[[nodiscard("You must use comparison result")]]
bool operator!=(const Mat& mat) const
{
return !operator==(mat);
}
// Static methods that return fixed-size matrices
[[nodiscard]]
[[nodiscard("You must use screen matrix")]]
constexpr static Mat<4, 4> to_screen_mat(const Type& screen_width, const Type& screen_height) noexcept
{
return {
@@ -381,7 +383,7 @@ namespace omath
};
}
[[nodiscard]]
[[nodiscard("You must use inverted matrix")]]
constexpr std::optional<Mat> inverted() const
{
const auto det = determinant();
@@ -404,7 +406,7 @@ namespace omath
private:
std::array<Type, Rows * Columns> m_data;
template<size_t OtherColumns> [[nodiscard]]
template<size_t OtherColumns> [[nodiscard("You must use result matrix")]]
constexpr Mat<Rows, OtherColumns, Type, MatStoreType::ROW_MAJOR>
cache_friendly_multiply_row_major(const Mat<Columns, OtherColumns, Type, MatStoreType::ROW_MAJOR>& other) const
{
@@ -419,7 +421,7 @@ namespace omath
return result;
}
template<size_t OtherColumns> [[nodiscard]]
template<size_t OtherColumns> [[nodiscard("You must use result matrix")]]
constexpr Mat<Rows, OtherColumns, Type, MatStoreType::COLUMN_MAJOR> cache_friendly_multiply_col_major(
const Mat<Columns, OtherColumns, Type, MatStoreType::COLUMN_MAJOR>& other) const
{
@@ -434,7 +436,7 @@ namespace omath
return result;
}
#ifdef OMATH_USE_AVX2
template<size_t OtherColumns> [[nodiscard]]
template<size_t OtherColumns> [[nodiscard("You must use result matrix")]]
constexpr Mat<Rows, OtherColumns, Type, MatStoreType::COLUMN_MAJOR>
avx_multiply_col_major(const Mat<Columns, OtherColumns, Type, MatStoreType::COLUMN_MAJOR>& other) const
{
@@ -504,7 +506,7 @@ namespace omath
return result;
}
template<size_t OtherColumns> [[nodiscard]]
template<size_t OtherColumns> [[nodiscard("You must use result matrix")]]
constexpr Mat<Rows, OtherColumns, Type, MatStoreType::ROW_MAJOR>
avx_multiply_row_major(const Mat<Columns, OtherColumns, Type, MatStoreType::ROW_MAJOR>& other) const
{
@@ -575,20 +577,20 @@ namespace omath
#endif
};
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR> [[nodiscard]]
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR> [[nodiscard("You must use row matrix")]]
constexpr static Mat<1, 4, Type, St> mat_row_from_vector(const Vector3<Type>& vector) noexcept
{
return {{vector.x, vector.y, vector.z, 1}};
}
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR> [[nodiscard]]
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR> [[nodiscard("You must use column matrix")]]
constexpr static Mat<4, 1, Type, St> mat_column_from_vector(const Vector3<Type>& vector) noexcept
{
return {{vector.x}, {vector.y}, {vector.z}, {1}};
}
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR>
[[nodiscard]]
[[nodiscard("You must use translation matrix")]]
constexpr Mat<4, 4, Type, St> mat_translation(const Vector3<Type>& diff) noexcept
{
return
@@ -600,7 +602,7 @@ namespace omath
};
}
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR>
[[nodiscard]]
[[nodiscard("You must use scale matrix")]]
constexpr Mat<4, 4, Type, St> mat_scale(const Vector3<Type>& scale) noexcept
{
return {
@@ -611,8 +613,55 @@ namespace omath
};
}
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR>
[[nodiscard("You must use extracted origin")]]
constexpr Vector3<Type> mat_extract_origin(const Mat<4, 4, Type, St>& mat) noexcept
{
return {mat.at(0, 3), mat.at(1, 3), mat.at(2, 3)};
}
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR>
[[nodiscard("You must use extracted scale")]]
Vector3<Type> mat_extract_scale(const Mat<4, 4, Type, St>& mat) noexcept
{
auto column_length = [](const Type x, const Type y, const Type z) {
return static_cast<Type>(std::sqrt(x * x + y * y + z * z));
};
const auto scale_x = column_length(mat.at(0, 0), mat.at(1, 0), mat.at(2, 0));
const auto scale_y = column_length(mat.at(0, 1), mat.at(1, 1), mat.at(2, 1));
const auto scale_z = column_length(mat.at(0, 2), mat.at(1, 2), mat.at(2, 2));
constexpr auto epsilon = std::numeric_limits<Type>::epsilon();
return {
std::abs(scale_x) < epsilon ? Type{1} : scale_x,
std::abs(scale_y) < epsilon ? Type{1} : scale_y,
std::abs(scale_z) < epsilon ? Type{1} : scale_z,
};
}
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR>
requires std::is_floating_point_v<Type>
[[nodiscard("You must use extracted rotation")]]
Vector3<Type> mat_extract_rotation_zyx(const Mat<4, 4, Type, St>& mat) noexcept
{
const auto scale = mat_extract_scale(mat);
const auto m00 = mat.at(0, 0) / scale.x;
const auto m10 = mat.at(1, 0) / scale.x;
const auto m20 = mat.at(2, 0) / scale.x;
const auto m21 = mat.at(2, 1) / scale.y;
const auto m22 = mat.at(2, 2) / scale.z;
return {
angles::radians_to_degrees(std::atan2(m21, m22)),
angles::radians_to_degrees(std::asin(std::clamp(-m20, Type{-1}, Type{1}))),
angles::radians_to_degrees(std::atan2(m10, m00)),
};
}
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR, class Angle>
[[nodiscard]]
[[nodiscard("You must use rotation matrix")]]
Mat<4, 4, Type, St> mat_rotation_axis_x(const Angle& angle) noexcept
{
return
@@ -625,7 +674,7 @@ namespace omath
}
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR, class Angle>
[[nodiscard]]
[[nodiscard("You must use rotation matrix")]]
Mat<4, 4, Type, St> mat_rotation_axis_y(const Angle& angle) noexcept
{
return
@@ -638,7 +687,7 @@ namespace omath
}
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR, class Angle>
[[nodiscard]]
[[nodiscard("You must use rotation matrix")]]
Mat<4, 4, Type, St> mat_rotation_axis_z(const Angle& angle) noexcept
{
return
@@ -651,7 +700,7 @@ namespace omath
}
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR>
[[nodiscard]]
[[nodiscard("You must use camera view matrix")]]
static Mat<4, 4, Type, St> mat_camera_view(const Vector3<Type>& forward, const Vector3<Type>& right,
const Vector3<Type>& up, const Vector3<Type>& camera_origin) noexcept
{
@@ -666,8 +715,8 @@ namespace omath
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR,
NDCDepthRange DepthRange = NDCDepthRange::NEGATIVE_ONE_TO_ONE>
[[nodiscard]]
Mat<4, 4, Type, St> mat_perspective_left_handed(const Type field_of_view, const Type aspect_ratio,
[[nodiscard("You must use perspective matrix")]]
Mat<4, 4, Type, St> mat_perspective_left_handed_vertical_fov(const Type field_of_view, const Type aspect_ratio,
const Type near, const Type far) noexcept
{
const auto fov_half_tan = std::tan(angles::degrees_to_radians(field_of_view) / Type{2});
@@ -688,8 +737,8 @@ namespace omath
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR,
NDCDepthRange DepthRange = NDCDepthRange::NEGATIVE_ONE_TO_ONE>
[[nodiscard]]
Mat<4, 4, Type, St> mat_perspective_right_handed(const Type field_of_view, const Type aspect_ratio,
[[nodiscard("You must use perspective matrix")]]
Mat<4, 4, Type, St> mat_perspective_right_handed_vertical_fov(const Type field_of_view, const Type aspect_ratio,
const Type near, const Type far) noexcept
{
const auto fov_half_tan = std::tan(angles::degrees_to_radians(field_of_view) / Type{2});
@@ -713,7 +762,7 @@ namespace omath
// X and Y scales derived as: X = 1 / tan(hfov/2), Y = aspect / tan(hfov/2).
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR,
NDCDepthRange DepthRange = NDCDepthRange::NEGATIVE_ONE_TO_ONE>
[[nodiscard]]
[[nodiscard("You must use perspective matrix")]]
Mat<4, 4, Type, St> mat_perspective_left_handed_horizontal_fov(const Type horizontal_fov,
const Type aspect_ratio, const Type near,
const Type far) noexcept
@@ -730,7 +779,7 @@ namespace omath
else if constexpr (DepthRange == NDCDepthRange::NEGATIVE_ONE_TO_ONE)
return {{x_axis, Type{0}, Type{0}, Type{0}},
{Type{0}, y_axis, Type{0}, Type{0}},
{Type{0}, Type{0}, (far + near) / (far - near), -(2.f * near * far) / (far - near)},
{Type{0}, Type{0}, (far + near) / (far - near), -(Type{2} * near * far) / (far - near)},
{Type{0}, Type{0}, Type{1}, Type{0}}};
else
std::unreachable();
@@ -738,7 +787,7 @@ namespace omath
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR,
NDCDepthRange DepthRange = NDCDepthRange::NEGATIVE_ONE_TO_ONE>
[[nodiscard]]
[[nodiscard("You must use perspective matrix")]]
Mat<4, 4, Type, St> mat_perspective_right_handed_horizontal_fov(const Type horizontal_fov,
const Type aspect_ratio, const Type near,
const Type far) noexcept
@@ -755,14 +804,14 @@ namespace omath
else if constexpr (DepthRange == NDCDepthRange::NEGATIVE_ONE_TO_ONE)
return {{x_axis, Type{0}, Type{0}, Type{0}},
{Type{0}, y_axis, Type{0}, Type{0}},
{Type{0}, Type{0}, -(far + near) / (far - near), -(2.f * near * far) / (far - near)},
{Type{0}, Type{0}, -(far + near) / (far - near), -(Type{2} * near * far) / (far - near)},
{Type{0}, Type{0}, -Type{1}, Type{0}}};
else
std::unreachable();
}
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR,
NDCDepthRange DepthRange = NDCDepthRange::NEGATIVE_ONE_TO_ONE>
[[nodiscard]]
[[nodiscard("You must use ortho matrix")]]
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
{
@@ -787,7 +836,7 @@ namespace omath
}
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR,
NDCDepthRange DepthRange = NDCDepthRange::NEGATIVE_ONE_TO_ONE>
[[nodiscard]]
[[nodiscard("You must use ortho matrix")]]
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
{
@@ -834,14 +883,14 @@ template<size_t Rows, size_t Columns, class Type, omath::MatStoreType StoreType>
struct std::formatter<omath::Mat<Rows, Columns, Type, StoreType>> // NOLINT(*-dcl58-cpp)
{
using MatType = omath::Mat<Rows, Columns, Type, StoreType>;
[[nodiscard]]
[[nodiscard("You must use parse iterator")]]
static constexpr auto parse(std::format_parse_context& ctx)
{
return ctx.begin();
}
template<class FormatContext>
[[nodiscard]]
[[nodiscard("You must use format iterator")]]
static auto format(const MatType& mat, FormatContext& ctx)
{
if constexpr (std::is_same_v<typename FormatContext::char_type, char>)
+45 -28
View File
@@ -28,7 +28,7 @@ namespace omath
template<class CastedType>
requires std::is_arithmetic_v<CastedType>
[[nodiscard]] constexpr explicit operator Vector2<CastedType>() const noexcept
[[nodiscard("You must use casted vector")]] constexpr explicit operator Vector2<CastedType>() const noexcept
{
return {static_cast<CastedType>(x), static_cast<CastedType>(y)};
}
@@ -37,13 +37,13 @@ namespace omath
}
// Equality operators
[[nodiscard]]
[[nodiscard("You must use comparison result")]]
constexpr bool operator==(const Vector2& other) const noexcept
{
return x == other.x && y == other.y;
}
[[nodiscard]]
[[nodiscard("You must use comparison result")]]
constexpr bool operator!=(const Vector2& other) const noexcept
{
return !(*this == other);
@@ -115,45 +115,51 @@ namespace omath
}
// Basic vector operations
[[nodiscard]] Type distance_to(const Vector2& other) const noexcept
[[nodiscard("You must use distance")]]
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
[[nodiscard("You must use squared distance")]]
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
[[nodiscard("You must use dot product")]]
constexpr Type dot(const Vector2& other) const noexcept
{
return x * other.x + y * other.y;
}
#ifndef _MSC_VER
[[nodiscard]] constexpr Type length() const noexcept
[[nodiscard("You must use length")]] constexpr Type length() const noexcept
{
return std::hypot(this->x, this->y);
}
[[nodiscard]] constexpr Vector2 normalized() const noexcept
[[nodiscard("You must use normalized vector")]] constexpr Vector2 normalized() const noexcept
{
const Type len = length();
return len > 0.f ? *this / len : *this;
}
#else
[[nodiscard]] Type length() const noexcept
[[nodiscard("You must use length")]]
Type length() const noexcept
{
return std::hypot(x, y);
}
[[nodiscard]] Vector2 normalized() const noexcept
[[nodiscard("You must use normalized vector")]]
Vector2 normalized() const noexcept
{
const Type len = length();
return len > static_cast<Type>(0) ? *this / len : *this;
}
#endif
[[nodiscard]] constexpr Type length_sqr() const noexcept
[[nodiscard("You must use squared length")]]
constexpr Type length_sqr() const noexcept
{
return x * x + y * y;
}
@@ -165,80 +171,91 @@ namespace omath
y = y < static_cast<Type>(0) ? -y : y;
return *this;
}
[[nodiscard("You must use absed vector")]]
constexpr Vector2 abs() const noexcept
{
return Vector2{*this}.abs();
}
[[nodiscard]] constexpr Vector2 operator-() const noexcept
[[nodiscard("You must use negated vector")]]
constexpr Vector2 operator-() const noexcept
{
return {-x, -y};
}
// Binary arithmetic operators
[[nodiscard]] constexpr Vector2 operator+(const Vector2& other) const noexcept
[[nodiscard("You must use result vector")]]
constexpr Vector2 operator+(const Vector2& other) const noexcept
{
return {x + other.x, y + other.y};
}
[[nodiscard]] constexpr Vector2 operator-(const Vector2& other) const noexcept
[[nodiscard("You must use result vector")]]
constexpr Vector2 operator-(const Vector2& other) const noexcept
{
return {x - other.x, y - other.y};
}
[[nodiscard]] constexpr Vector2 operator*(const Type& value) const noexcept
[[nodiscard("You must use result vector")]]
constexpr Vector2 operator*(const Type& value) const noexcept
{
return {x * value, y * value};
}
[[nodiscard]] constexpr Vector2 operator/(const Type& value) const noexcept
[[nodiscard("You must use result vector")]]
constexpr Vector2 operator/(const Type& value) const noexcept
{
return {x / value, y / value};
}
// Sum of elements
[[nodiscard]] constexpr Type sum() const noexcept
[[nodiscard("You must use sum of elements")]]
constexpr Type sum() const noexcept
{
return x + y;
}
[[nodiscard]]
[[nodiscard("You must use comparison result")]]
bool operator<(const Vector2& other) const noexcept
{
return length() < other.length();
}
[[nodiscard]]
[[nodiscard("You must use comparison result")]]
bool operator>(const Vector2& other) const noexcept
{
return length() > other.length();
}
[[nodiscard]]
[[nodiscard("You must use comparison result")]]
bool operator<=(const Vector2& other) const noexcept
{
return length() <= other.length();
}
[[nodiscard]]
[[nodiscard("You must use comparison result")]]
bool operator>=(const Vector2& other) const noexcept
{
return length() >= other.length();
}
[[nodiscard]]
[[nodiscard("You must use tuple")]]
constexpr std::tuple<Type, Type> as_tuple() const noexcept
{
return std::make_tuple(x, y);
}
[[nodiscard]]
[[nodiscard("You must use array")]]
constexpr std::array<Type, 2> as_array() const noexcept
{
return {x, y};
}
#ifdef OMATH_IMGUI_INTEGRATION
[[nodiscard]]
[[nodiscard("You must use ImVec2")]]
constexpr ImVec2 to_im_vec2() const noexcept
{
return {static_cast<float>(this->x), static_cast<float>(this->y)};
}
[[nodiscard]]
[[nodiscard("You must use vector from ImVec2")]]
static Vector2 from_im_vec2(const ImVec2& other) noexcept
{
return {static_cast<Type>(other.x), static_cast<Type>(other.y)};
@@ -249,7 +266,7 @@ namespace omath
template<> struct std::hash<omath::Vector2<float>>
{
[[nodiscard]]
[[nodiscard("You must use hash value")]]
std::size_t operator()(const omath::Vector2<float>& vec) const noexcept
{
std::size_t hash = 0;
@@ -265,14 +282,14 @@ template<> struct std::hash<omath::Vector2<float>>
template<class Type>
struct std::formatter<omath::Vector2<Type>> // NOLINT(*-dcl58-cpp)
{
[[nodiscard]]
[[nodiscard("You must use parse iterator")]]
static constexpr auto parse(std::format_parse_context& ctx)
{
return ctx.begin();
}
template<class FormatContext>
[[nodiscard]]
[[nodiscard("You must use format iterator")]]
static auto format(const omath::Vector2<Type>& vec, FormatContext& ctx)
{
if constexpr (std::is_same_v<typename FormatContext::char_type, char>)
+64 -37
View File
@@ -32,17 +32,20 @@ namespace omath
template<class CastedType>
requires std::is_arithmetic_v<CastedType>
[[nodiscard]] constexpr explicit operator Vector3<CastedType>() const noexcept
[[nodiscard("You must use casted vector")]]
constexpr explicit operator Vector3<CastedType>() const noexcept
{
return {static_cast<CastedType>(this->x), static_cast<CastedType>(this->y),
static_cast<CastedType>(this->z)};
}
[[nodiscard]] constexpr bool operator==(const Vector3& other) const noexcept
[[nodiscard("You must use comparison result")]]
constexpr bool operator==(const Vector3& other) const noexcept
{
return Vector2<Type>::operator==(other) && (other.z == z);
}
[[nodiscard]] constexpr bool operator!=(const Vector3& other) const noexcept
[[nodiscard("You must use comparison result")]]
constexpr bool operator!=(const Vector3& other) const noexcept
{
return !(*this == other);
}
@@ -118,118 +121,140 @@ namespace omath
return *this;
}
[[nodiscard("You must use absed vector")]]
constexpr Vector3 abs() const noexcept
{
return Vector3{*this}.abs();
}
[[nodiscard]] constexpr Type distance_to_sqr(const Vector3& other) const noexcept
[[nodiscard("You must use squared distance")]]
constexpr Type distance_to_sqr(const Vector3& other) const noexcept
{
return (*this - other).length_sqr();
}
[[nodiscard]] constexpr Type dot(const Vector3& other) const noexcept
[[nodiscard("You must use dot product")]]
constexpr Type dot(const Vector3& other) const noexcept
{
return Vector2<Type>::dot(other) + z * other.z;
}
#ifndef _MSC_VER
[[nodiscard]] constexpr Type length() const
[[nodiscard("You must use length")]] constexpr Type length() const
{
return std::hypot(this->x, this->y, z);
}
[[nodiscard]] constexpr Type length_2d() const
[[nodiscard("You must use 2D length")]] constexpr Type length_2d() const
{
return Vector2<Type>::length();
}
[[nodiscard]] Type distance_to(const Vector3& other) const
[[nodiscard("You must use distance")]] Type distance_to(const Vector3& other) const
{
return (*this - other).length();
}
[[nodiscard]] constexpr Vector3 normalized() const
[[nodiscard("You must use normalized vector")]] constexpr Vector3 normalized() const
{
const Type length_value = this->length();
return length_value != 0 ? *this / length_value : *this;
}
#else
[[nodiscard]] Type length() const noexcept
[[nodiscard("You must use length")]]
Type length() const noexcept
{
return std::hypot(this->x, this->y, z);
}
[[nodiscard]] Vector3 normalized() const noexcept
[[nodiscard("You must use normalized vector")]]
Vector3 normalized() const noexcept
{
const Type len = this->length();
return len != static_cast<Type>(0) ? *this / len : *this;
}
[[nodiscard]] Type length_2d() const noexcept
[[nodiscard("You must use 2D length")]]
Type length_2d() const noexcept
{
return Vector2<Type>::length();
}
[[nodiscard]] Type distance_to(const Vector3& v_other) const noexcept
[[nodiscard("You must use distance")]]
Type distance_to(const Vector3& v_other) const noexcept
{
return (*this - v_other).length();
}
#endif
[[nodiscard]] constexpr Type length_sqr() const noexcept
[[nodiscard("You must use squared length")]]
constexpr Type length_sqr() const noexcept
{
return Vector2<Type>::length_sqr() + z * z;
}
[[nodiscard]] constexpr Vector3 operator-() const noexcept
[[nodiscard("You must use negated vector")]]
constexpr Vector3 operator-() const noexcept
{
return {-this->x, -this->y, -z};
}
[[nodiscard]] constexpr Vector3 operator+(const Vector3& other) const noexcept
[[nodiscard("You must use result vector")]]
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
[[nodiscard("You must use result vector")]]
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
[[nodiscard("You must use result vector")]]
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
[[nodiscard("You must use result vector")]]
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
[[nodiscard("You must use result vector")]]
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
[[nodiscard("You must use result vector")]]
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
[[nodiscard("You must use cross product")]]
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
[[nodiscard("You must use sum of elements")]]
constexpr Type sum() const noexcept
{
return sum_2d() + z;
}
[[nodiscard]]
[[nodiscard("You must use direction check result")]]
bool point_to_same_direction(const Vector3& other) const
{
return dot(other) > static_cast<Type>(0);
}
[[nodiscard]] std::expected<Angle<float, 0.f, 180.f, AngleFlags::Clamped>, Vector3Error>
[[nodiscard("You must use angle between vectors")]]
std::expected<Angle<float, 0.f, 180.f, AngleFlags::Clamped>, Vector3Error>
angle_between(const Vector3& other) const noexcept
{
const auto bottom = length() * other.length();
@@ -240,8 +265,8 @@ namespace omath
return Angle<float, 0.f, 180.f, AngleFlags::Clamped>::from_radians(std::acos(dot(other) / bottom));
}
[[nodiscard]] bool is_perpendicular(const Vector3& other,
Type epsilon = static_cast<Type>(0.0001)) const noexcept
[[nodiscard("You must use perpendicularity check result")]]
bool is_perpendicular(const Vector3& other, Type epsilon = static_cast<Type>(0.0001)) const noexcept
{
if (const auto angle = angle_between(other))
return std::abs(angle->as_degrees() - static_cast<Type>(90)) <= epsilon;
@@ -249,41 +274,43 @@ namespace omath
return false;
}
[[nodiscard]] constexpr Type sum_2d() const noexcept
[[nodiscard("You must use 2D sum")]]
constexpr Type sum_2d() const noexcept
{
return Vector2<Type>::sum();
}
[[nodiscard]] constexpr std::tuple<Type, Type, Type> as_tuple() const noexcept
[[nodiscard("You must use tuple")]]
constexpr std::tuple<Type, Type, Type> as_tuple() const noexcept
{
return std::make_tuple(this->x, this->y, z);
}
[[nodiscard]]
[[nodiscard("You must use comparison result")]]
bool operator<(const Vector3& other) const noexcept
{
return length() < other.length();
}
[[nodiscard]]
[[nodiscard("You must use comparison result")]]
bool operator>(const Vector3& other) const noexcept
{
return length() > other.length();
}
[[nodiscard]]
[[nodiscard("You must use comparison result")]]
bool operator<=(const Vector3& other) const noexcept
{
return length() <= other.length();
}
[[nodiscard]]
[[nodiscard("You must use comparison result")]]
bool operator>=(const Vector3& other) const noexcept
{
return length() >= other.length();
}
[[nodiscard]]
[[nodiscard("You must use array")]]
constexpr std::array<Type, 3> as_array() const noexcept
{
return {this->x, this->y, z};
@@ -293,7 +320,7 @@ namespace omath
template<> struct std::hash<omath::Vector3<float>>
{
[[nodiscard]]
[[nodiscard("You must use hash value")]]
std::size_t operator()(const omath::Vector3<float>& vec) const noexcept
{
std::size_t hash = 0;
@@ -310,14 +337,14 @@ template<> struct std::hash<omath::Vector3<float>>
template<class Type>
struct std::formatter<omath::Vector3<Type>> // NOLINT(*-dcl58-cpp)
{
[[nodiscard]]
[[nodiscard("You must use parse iterator")]]
static constexpr auto parse(std::format_parse_context& ctx)
{
return ctx.begin();
}
template<class FormatContext>
[[nodiscard]]
[[nodiscard("You must use format iterator")]]
static auto format(const omath::Vector3<Type>& vec, FormatContext& ctx)
{
if constexpr (std::is_same_v<typename FormatContext::char_type, char>)
+31 -24
View File
@@ -24,19 +24,19 @@ namespace omath
template<class CastedType>
requires std::is_arithmetic_v<CastedType>
[[nodiscard]] constexpr explicit operator Vector4<CastedType>() const noexcept
[[nodiscard("You must use casted vector")]] constexpr explicit operator Vector4<CastedType>() const noexcept
{
return {static_cast<CastedType>(this->x), static_cast<CastedType>(this->y),
static_cast<CastedType>(this->z), static_cast<CastedType>(this->w)};
}
[[nodiscard]]
[[nodiscard("You must use comparison result")]]
constexpr bool operator==(const Vector4& other) const noexcept
{
return Vector3<Type>::operator==(other) && w == other.w;
}
[[nodiscard]]
[[nodiscard("You must use comparison result")]]
constexpr bool operator!=(const Vector4& other) const noexcept
{
return !(*this == other);
@@ -89,17 +89,19 @@ namespace omath
return *this;
}
[[nodiscard]] constexpr Type length_sqr() const noexcept
[[nodiscard("You must use squared length")]]
constexpr Type length_sqr() const noexcept
{
return Vector3<Type>::length_sqr() + w * w;
}
[[nodiscard]] constexpr Type dot(const Vector4& other) const noexcept
[[nodiscard("You must use dot product")]]
constexpr Type dot(const Vector4& other) const noexcept
{
return Vector3<Type>::dot(other) + w * other.w;
}
[[nodiscard]] Type length() const noexcept
[[nodiscard("You must use length")]] Type length() const noexcept
{
return std::sqrt(length_sqr());
}
@@ -111,6 +113,11 @@ namespace omath
return *this;
}
[[nodiscard("You must use absed vector")]]
constexpr Vector4 abs() const noexcept
{
return Vector4{*this}.abs();
}
constexpr Vector4& clamp(const Type& min, const Type& max) noexcept
{
this->x = std::clamp(this->x, min, max);
@@ -120,86 +127,86 @@ namespace omath
return *this;
}
[[nodiscard]]
[[nodiscard("You must use negated vector")]]
constexpr Vector4 operator-() const noexcept
{
return {-this->x, -this->y, -this->z, -w};
}
[[nodiscard]]
[[nodiscard("You must use result vector")]]
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]]
[[nodiscard("You must use result vector")]]
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]]
[[nodiscard("You must use result vector")]]
constexpr Vector4 operator*(const Type& value) const noexcept
{
return {this->x * value, this->y * value, this->z * value, w * value};
}
[[nodiscard]]
[[nodiscard("You must use result vector")]]
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]]
[[nodiscard("You must use result vector")]]
constexpr Vector4 operator/(const Type& value) const noexcept
{
return {this->x / value, this->y / value, this->z / value, w / value};
}
[[nodiscard]]
[[nodiscard("You must use result vector")]]
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]]
[[nodiscard("You must use sum of elements")]]
constexpr Type sum() const noexcept
{
return Vector3<Type>::sum() + w;
}
[[nodiscard]]
[[nodiscard("You must use comparison result")]]
bool operator<(const Vector4& other) const noexcept
{
return length() < other.length();
}
[[nodiscard]]
[[nodiscard("You must use comparison result")]]
bool operator>(const Vector4& other) const noexcept
{
return length() > other.length();
}
[[nodiscard]]
[[nodiscard("You must use comparison result")]]
bool operator<=(const Vector4& other) const noexcept
{
return length() <= other.length();
}
[[nodiscard]]
[[nodiscard("You must use comparison result")]]
bool operator>=(const Vector4& other) const noexcept
{
return length() >= other.length();
}
[[nodiscard]]
[[nodiscard("You must use array")]]
constexpr std::array<Type, 4> as_array() const noexcept
{
return {this->x, this->y, this->z, w};
}
#ifdef OMATH_IMGUI_INTEGRATION
[[nodiscard]]
[[nodiscard("You must use ImVec4")]]
constexpr ImVec4 to_im_vec4() const noexcept
{
return {
@@ -209,7 +216,7 @@ namespace omath
static_cast<float>(w),
};
}
[[nodiscard]]
[[nodiscard("You must use vector from ImVec4")]]
static Vector4<float> from_im_vec4(const ImVec4& other) noexcept
{
return {static_cast<Type>(other.x), static_cast<Type>(other.y), static_cast<Type>(other.z)};
@@ -220,7 +227,7 @@ namespace omath
template<> struct std::hash<omath::Vector4<float>>
{
[[nodiscard]]
[[nodiscard("You must use hash value")]]
std::size_t operator()(const omath::Vector4<float>& vec) const noexcept
{
std::size_t hash = 0;
@@ -237,13 +244,13 @@ template<> struct std::hash<omath::Vector4<float>>
template<class Type>
struct std::formatter<omath::Vector4<Type>> // NOLINT(*-dcl58-cpp)
{
[[nodiscard]]
[[nodiscard("You must use parse iterator")]]
static constexpr auto parse(std::format_parse_context& ctx)
{
return ctx.begin();
}
template<class FormatContext>
[[nodiscard]]
[[nodiscard("You must use format iterator")]]
static auto format(const omath::Vector4<Type>& vec, FormatContext& ctx)
{
if constexpr (std::is_same_v<typename FormatContext::char_type, char>)
+5
View File
@@ -15,8 +15,13 @@ namespace omath::lua
static void register_vec2(sol::table& omath_table);
static void register_vec3(sol::table& omath_table);
static void register_vec4(sol::table& omath_table);
static void register_matrices(sol::table& omath_table);
static void register_quaternion(sol::table& omath_table);
static void register_color(sol::table& omath_table);
static void register_hud(sol::table& omath_table);
static void register_triangle(sol::table& omath_table);
static void register_3d_primitives(sol::table& omath_table);
static void register_collision(sol::table& omath_table);
static void register_shared_types(sol::table& omath_table);
static void register_engines(sol::table& omath_table);
static void register_pattern_scan(sol::table& omath_table);
+107 -74
View File
@@ -5,6 +5,7 @@
#pragma once
#include "omath/3d_primitives/aabb.hpp"
#include "omath/3d_primitives/obb.hpp"
#include "omath/linear_algebra/mat.hpp"
#include "omath/linear_algebra/triangle.hpp"
#include "omath/linear_algebra/vector3.hpp"
@@ -31,7 +32,7 @@ namespace omath::projection
float m_width;
float m_height;
[[nodiscard]] constexpr float aspect_ratio() const
[[nodiscard("You must use aspect ratio")]] constexpr float aspect_ratio() const
{
return m_width / m_height;
}
@@ -100,17 +101,18 @@ namespace omath::projection
// built by any of the engine traits. Both variants (ZERO_TO_ONE and
// NEGATIVE_ONE_TO_ONE) share the same m[0,0]/m[1,1] layout, so this works
// regardless of the NDC depth range.
[[nodiscard]]
[[nodiscard("You must use extracted projection params")]]
static ProjectionParams extract_projection_params(const Mat4X4Type& proj_matrix) noexcept
{
// m[1,1] == 1 / tan(fov/2) => fov = 2 * atan(1 / m[1,1])
const auto f = proj_matrix.at(1, 1);
// m[0,0] == m[1,1] / aspect_ratio => aspect = m[1,1] / m[0,0]
return {FieldOfView::from_radians(NumericType{2} * std::atan(NumericType{1} / f)),
const auto fov_radians = NumericType{2} * std::atan(NumericType{1} / f);
return {FieldOfView::from_radians(static_cast<typename FieldOfView::ArithmeticType>(fov_radians)),
f / proj_matrix.at(0, 0)};
}
[[nodiscard]]
[[nodiscard("You must use calculated view angles")]]
static ViewAnglesType calc_view_angles_from_view_matrix(const Mat4X4Type& view_matrix) noexcept
{
Vector3<NumericType> forward_vector = {view_matrix[2, 0], view_matrix[2, 1], view_matrix[2, 2]};
@@ -119,7 +121,7 @@ namespace omath::projection
return TraitClass::calc_look_at_angle({}, forward_vector);
}
[[nodiscard]]
[[nodiscard("You must use calculated origin")]]
static Vector3<NumericType> calc_origin_from_view_matrix(const Mat4X4Type& view_matrix) noexcept
{
// The view matrix is R * T(-origin), so the last column stores t = -R * origin.
@@ -140,33 +142,33 @@ namespace omath::projection
m_view_projection_matrix = std::nullopt;
m_view_matrix = std::nullopt;
}
[[nodiscard]]
[[nodiscard("You must use calculated look-at angles")]]
ViewAnglesType calc_look_at_angles(const Vector3<NumericType>& look_to) const
{
return TraitClass::calc_look_at_angle(m_origin, look_to);
}
[[nodiscard]]
[[nodiscard("You must use forward vector")]]
Vector3<NumericType> get_forward() const noexcept
{
const auto& view_matrix = get_view_matrix();
return {view_matrix[2, 0], view_matrix[2, 1], view_matrix[2, 2]};
}
[[nodiscard]]
[[nodiscard("You must use right vector")]]
Vector3<NumericType> get_right() const noexcept
{
const auto& view_matrix = get_view_matrix();
return {view_matrix[0, 0], view_matrix[0, 1], view_matrix[0, 2]};
}
[[nodiscard]]
[[nodiscard("You must use up vector")]]
Vector3<NumericType> get_up() const noexcept
{
const auto& view_matrix = get_view_matrix();
return {view_matrix[1, 0], view_matrix[1, 1], view_matrix[1, 2]};
}
[[nodiscard]]
[[nodiscard("You must use absolute forward vector")]]
Vector3<NumericType> get_abs_forward() const noexcept
{
if constexpr (axes.inverted_forward)
@@ -174,7 +176,7 @@ namespace omath::projection
return get_forward();
}
[[nodiscard]]
[[nodiscard("You must use absolute right vector")]]
Vector3<NumericType> get_abs_right() const noexcept
{
if constexpr (axes.inverted_right)
@@ -182,13 +184,14 @@ namespace omath::projection
return get_right();
}
[[nodiscard]]
[[nodiscard("You must use absolute up vector")]]
Vector3<NumericType> get_abs_up() const noexcept
{
return get_up();
}
[[nodiscard]] const Mat4X4Type& get_view_projection_matrix() const noexcept
[[nodiscard("You must use view-projection matrix")]]
const Mat4X4Type& get_view_projection_matrix() const noexcept
{
if (!m_view_projection_matrix.has_value())
m_view_projection_matrix = get_projection_matrix() * get_view_matrix();
@@ -196,14 +199,14 @@ namespace omath::projection
return m_view_projection_matrix.value();
}
[[nodiscard]] const Mat4X4Type& get_view_matrix() const noexcept
[[nodiscard("You must use view matrix")]] const Mat4X4Type& get_view_matrix() const noexcept
{
if (!m_view_matrix.has_value())
m_view_matrix = TraitClass::calc_view_matrix(m_view_angles, m_origin);
return m_view_matrix.value();
}
[[nodiscard]] const Mat4X4Type& get_projection_matrix() const noexcept
[[nodiscard("You must use projection matrix")]] const Mat4X4Type& get_projection_matrix() const noexcept
{
if (!m_projection_matrix.has_value())
m_projection_matrix = TraitClass::calc_projection_matrix(
@@ -253,33 +256,33 @@ namespace omath::projection
m_projection_matrix = std::nullopt;
}
[[nodiscard]] const FieldOfView& get_field_of_view() const noexcept
[[nodiscard("You must use field of view")]] const FieldOfView& get_field_of_view() const noexcept
{
return m_field_of_view;
}
[[nodiscard]] const NumericType& get_near_plane() const noexcept
[[nodiscard("You must use near plane")]] const NumericType& get_near_plane() const noexcept
{
return m_near_plane_distance;
}
[[nodiscard]] const NumericType& get_far_plane() const noexcept
[[nodiscard("You must use far plane")]] const NumericType& get_far_plane() const noexcept
{
return m_far_plane_distance;
}
[[nodiscard]] const ViewAnglesType& get_view_angles() const noexcept
[[nodiscard("You must use view angles")]] const ViewAnglesType& get_view_angles() const noexcept
{
return m_view_angles;
}
[[nodiscard]] const Vector3<NumericType>& get_origin() const noexcept
[[nodiscard("You must use origin")]] const Vector3<NumericType>& get_origin() const noexcept
{
return m_origin;
}
template<ScreenStart screen_start = ScreenStart::TOP_LEFT_CORNER>
[[nodiscard]] std::expected<Vector3<NumericType>, Error>
[[nodiscard("You must use screen position")]] std::expected<Vector3<NumericType>, Error>
world_to_screen(const Vector3<NumericType>& world_position) const noexcept
{
const auto normalized_cords = world_to_view_port(world_position);
@@ -295,7 +298,7 @@ namespace omath::projection
std::unreachable();
}
template<ScreenStart screen_start = ScreenStart::TOP_LEFT_CORNER>
[[nodiscard]] std::expected<Vector3<NumericType>, Error>
[[nodiscard("You must use unclipped screen position")]] std::expected<Vector3<NumericType>, Error>
world_to_screen_unclipped(const Vector3<NumericType>& world_position) const noexcept
{
const auto normalized_cords = world_to_view_port(world_position, ViewPortClipping::MANUAL);
@@ -311,7 +314,8 @@ namespace omath::projection
std::unreachable();
}
[[nodiscard]] bool is_culled_by_frustum(const Triangle<Vector3<NumericType>>& triangle) const noexcept
[[nodiscard("You must use frustum culling result")]] bool
is_culled_by_frustum(const Triangle<Vector3<NumericType>>& triangle) const noexcept
{
// Transform to clip space (before perspective divide)
auto to_clip = [this](const Vector3<NumericType>& point)
@@ -378,51 +382,12 @@ namespace omath::projection
return false;
}
[[nodiscard]] bool is_aabb_culled_by_frustum(const primitives::Aabb<NumericType>& aabb) const noexcept
[[nodiscard("You must use AABB frustum culling result")]] bool
is_aabb_culled_by_frustum(const primitives::Aabb<NumericType>& aabb) const noexcept
{
const auto& m = get_view_projection_matrix();
// Gribb-Hartmann: extract 6 frustum planes from the view-projection matrix.
// Each plane is (a, b, c, d) such that ax + by + cz + d >= 0 means inside.
// For a 4x4 matrix with rows r0..r3:
// Left = r3 + r0
// Right = r3 - r0
// Bottom = r3 + r1
// Top = r3 - r1
// Near = r3 + r2 ([-1,1]) or r2 ([0,1])
// Far = r3 - r2
struct Plane final
{
NumericType a, b, c, d;
};
const auto extract_plane = [&m](const int sign, const int row) -> Plane
{
return {
m.at(3, 0) + static_cast<NumericType>(sign) * m.at(row, 0),
m.at(3, 1) + static_cast<NumericType>(sign) * m.at(row, 1),
m.at(3, 2) + static_cast<NumericType>(sign) * m.at(row, 2),
m.at(3, 3) + static_cast<NumericType>(sign) * m.at(row, 3),
};
};
std::array<Plane, 6> planes = {
extract_plane(1, 0), // left
extract_plane(-1, 0), // right
extract_plane(1, 1), // bottom
extract_plane(-1, 1), // top
extract_plane(-1, 2), // far
};
// Near plane depends on NDC depth range
if constexpr (depth_range == NDCDepthRange::ZERO_TO_ONE)
planes[5] = {m.at(2, 0), m.at(2, 1), m.at(2, 2), m.at(2, 3)};
else
planes[5] = extract_plane(1, 2);
// For each plane, find the AABB corner most in the direction of the plane normal
// (the "positive vertex"). If it's outside, the entire AABB is outside.
for (const auto& [a, b, c, d] : planes)
for (const auto& [a, b, c, d] : extract_frustum_planes())
{
const auto px = a >= NumericType{0} ? aabb.max.x : aabb.min.x;
const auto py = b >= NumericType{0} ? aabb.max.y : aabb.min.y;
@@ -435,7 +400,28 @@ namespace omath::projection
return false;
}
[[nodiscard]] std::expected<Vector3<NumericType>, Error>
[[nodiscard("You must use OBB frustum culling result")]] bool
is_obb_culled_by_frustum(const primitives::Obb<NumericType>& obb) const noexcept
{
// For each plane, project the OBB extents onto the plane normal to get the
// effective radius, then test the center's signed distance against it.
for (const auto& [a, b, c, d] : extract_frustum_planes())
{
const Vector3<NumericType> normal{a, b, c};
const auto center_distance = normal.dot(obb.center) + d;
const auto radius = obb.half_extents.x * std::abs(normal.dot(obb.axis_x))
+ obb.half_extents.y * std::abs(normal.dot(obb.axis_y))
+ obb.half_extents.z * std::abs(normal.dot(obb.axis_z));
if (center_distance + radius < NumericType{0})
return true;
}
return false;
}
[[nodiscard("You must use view port position")]] std::expected<Vector3<NumericType>, Error>
world_to_view_port(const Vector3<NumericType>& world_position,
const ViewPortClipping& clipping = ViewPortClipping::AUTO) const noexcept
{
@@ -464,7 +450,7 @@ namespace omath::projection
return Vector3<NumericType>{projected.at(0, 0), projected.at(1, 0), projected.at(2, 0)};
}
[[nodiscard]]
[[nodiscard("You must use world position")]]
std::expected<Vector3<NumericType>, Error> view_port_to_world(const Vector3<NumericType>& ndc) const noexcept
{
const auto inv_view_proj = get_view_projection_matrix().inverted();
@@ -487,7 +473,7 @@ namespace omath::projection
}
template<ScreenStart screen_start = ScreenStart::TOP_LEFT_CORNER>
[[nodiscard]]
[[nodiscard("You must use world position")]]
std::expected<Vector3<NumericType>, Error>
screen_to_world(const Vector3<NumericType>& screen_pos) const noexcept
{
@@ -495,7 +481,7 @@ namespace omath::projection
}
template<ScreenStart screen_start = ScreenStart::TOP_LEFT_CORNER>
[[nodiscard]]
[[nodiscard("You must use world position")]]
std::expected<Vector3<NumericType>, Error>
screen_to_world(const Vector2<NumericType>& screen_pos) const noexcept
{
@@ -517,8 +503,54 @@ namespace omath::projection
Vector3<NumericType> m_origin;
private:
struct FrustumPlane final
{
NumericType a, b, c, d;
};
// Gribb-Hartmann: extract 6 frustum planes from the view-projection matrix.
// Each plane is (a, b, c, d) such that ax + by + cz + d >= 0 means inside.
// For a 4x4 matrix with rows r0..r3:
// Left = r3 + r0
// Right = r3 - r0
// Bottom = r3 + r1
// Top = r3 - r1
// Near = r3 + r2 ([-1,1]) or r2 ([0,1])
// Far = r3 - r2
[[nodiscard("You must use frustum planes")]] std::array<FrustumPlane, 6> extract_frustum_planes() const noexcept
{
const auto& m = get_view_projection_matrix();
const auto extract_plane = [&m](const int sign, const int row) -> FrustumPlane
{
return {
m.at(3, 0) + static_cast<NumericType>(sign) * m.at(row, 0),
m.at(3, 1) + static_cast<NumericType>(sign) * m.at(row, 1),
m.at(3, 2) + static_cast<NumericType>(sign) * m.at(row, 2),
m.at(3, 3) + static_cast<NumericType>(sign) * m.at(row, 3),
};
};
std::array<FrustumPlane, 6> planes = {
extract_plane(1, 0), // left
extract_plane(-1, 0), // right
extract_plane(1, 1), // bottom
extract_plane(-1, 1), // top
extract_plane(-1, 2), // far
};
// Near plane depends on NDC depth range
if constexpr (depth_range == NDCDepthRange::ZERO_TO_ONE)
planes[5] = {m.at(2, 0), m.at(2, 1), m.at(2, 2), m.at(2, 3)};
else
planes[5] = extract_plane(1, 2);
return planes;
}
template<class Type>
[[nodiscard]] constexpr static bool is_ndc_out_of_bounds(const Type& ndc) noexcept
[[nodiscard("You must use NDC bounds check result")]] constexpr static bool
is_ndc_out_of_bounds(const Type& ndc) noexcept
{
constexpr auto eps = std::numeric_limits<NumericType>::epsilon();
@@ -531,7 +563,7 @@ namespace omath::projection
return is_ndc_z_value_out_of_bounds(data[2]);
}
template<class ZType>
[[nodiscard]]
[[nodiscard("You must use NDC z bounds check result")]]
constexpr static bool is_ndc_z_value_out_of_bounds(const ZType& z_ndc) noexcept
{
constexpr auto eps = std::numeric_limits<NumericType>::epsilon();
@@ -557,7 +589,7 @@ namespace omath::projection
v
*/
[[nodiscard]] Vector3<NumericType>
[[nodiscard("You must use screen position")]] Vector3<NumericType>
ndc_to_screen_position_from_top_left_corner(const Vector3<NumericType>& ndc) const noexcept
{
/*
@@ -575,7 +607,7 @@ namespace omath::projection
(ndc.y / -NumericType{2} + NumericType{0.5}) * m_view_port.m_height, ndc.z};
}
[[nodiscard]] Vector3<NumericType>
[[nodiscard("You must use screen position")]] Vector3<NumericType>
ndc_to_screen_position_from_bottom_left_corner(const Vector3<NumericType>& ndc) const noexcept
{
/*
@@ -594,7 +626,8 @@ namespace omath::projection
}
template<ScreenStart screen_start = ScreenStart::TOP_LEFT_CORNER>
[[nodiscard]] Vector3<NumericType> screen_to_ndc(const Vector3<NumericType>& screen_pos) const noexcept
[[nodiscard("You must use NDC position")]] Vector3<NumericType>
screen_to_ndc(const Vector3<NumericType>& screen_pos) const noexcept
{
if constexpr (screen_start == ScreenStart::TOP_LEFT_CORNER)
return {screen_pos.x / m_view_port.m_width * NumericType{2} - NumericType{1},
+24 -3
View File
@@ -34,16 +34,37 @@ namespace omath::cry_engine
* mat_rotation_axis_y<float, MatStoreType::ROW_MAJOR>(angles.roll)
* mat_rotation_axis_x<float, MatStoreType::ROW_MAJOR>(angles.pitch);
}
Vector3<float> extract_origin(const Mat4X4& mat) noexcept
{
return mat_extract_origin(mat);
}
Vector3<float> extract_scale(const Mat4X4& mat) noexcept
{
return mat_extract_scale(mat);
}
ViewAngles extract_rotation_angles(const Mat4X4& mat) noexcept
{
const auto angles = mat_extract_rotation_zyx(mat);
return {
PitchAngle::from_degrees(angles.x),
YawAngle::from_degrees(angles.z),
RollAngle::from_degrees(angles.y),
};
}
Mat4X4 calc_perspective_projection_matrix(const float field_of_view, const float aspect_ratio, const float near,
const float far, const NDCDepthRange ndc_depth_range) noexcept
{
if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE)
return mat_perspective_left_handed<float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
return mat_perspective_left_handed_vertical_fov<float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
field_of_view, aspect_ratio, near, far);
if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE)
return mat_perspective_left_handed<float, MatStoreType::ROW_MAJOR, NDCDepthRange::NEGATIVE_ONE_TO_ONE>(
return mat_perspective_left_handed_vertical_fov<float, MatStoreType::ROW_MAJOR, NDCDepthRange::NEGATIVE_ONE_TO_ONE>(
field_of_view, aspect_ratio, near, far);
std::unreachable();
}
} // namespace omath::unity_engine
} // namespace omath::cry_engine
@@ -24,4 +24,4 @@ namespace omath::cry_engine
return calc_perspective_projection_matrix(fov.as_degrees(), view_port.aspect_ratio(), near, far,
ndc_depth_range);
}
} // namespace omath::unity_engine
} // namespace omath::cry_engine
+23 -2
View File
@@ -34,15 +34,36 @@ namespace omath::frostbite_engine
* mat_rotation_axis_y<float, MatStoreType::ROW_MAJOR>(angles.yaw)
* mat_rotation_axis_x<float, MatStoreType::ROW_MAJOR>(angles.pitch);
}
Vector3<float> extract_origin(const Mat4X4& mat) noexcept
{
return mat_extract_origin(mat);
}
Vector3<float> extract_scale(const Mat4X4& mat) noexcept
{
return mat_extract_scale(mat);
}
ViewAngles extract_rotation_angles(const Mat4X4& mat) noexcept
{
const auto angles = mat_extract_rotation_zyx(mat);
return {
PitchAngle::from_degrees(angles.x),
YawAngle::from_degrees(angles.y),
RollAngle::from_degrees(angles.z),
};
}
Mat4X4 calc_perspective_projection_matrix(const float field_of_view, const float aspect_ratio, const float near,
const float far, const NDCDepthRange ndc_depth_range) noexcept
{
if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE)
return mat_perspective_left_handed<float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
return mat_perspective_left_handed_vertical_fov<float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
field_of_view, aspect_ratio, near, far);
if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE)
return mat_perspective_left_handed<float, MatStoreType::ROW_MAJOR, NDCDepthRange::NEGATIVE_ONE_TO_ONE>(
return mat_perspective_left_handed_vertical_fov<float, MatStoreType::ROW_MAJOR, NDCDepthRange::NEGATIVE_ONE_TO_ONE>(
field_of_view, aspect_ratio, near, far);
std::unreachable();
+25 -7
View File
@@ -30,6 +30,26 @@ namespace omath::iw_engine
return mat_rotation_axis_z(angles.yaw) * mat_rotation_axis_y(angles.pitch) * mat_rotation_axis_x(angles.roll);
}
Vector3<float> extract_origin(const Mat4X4& mat) noexcept
{
return mat_extract_origin(mat);
}
Vector3<float> extract_scale(const Mat4X4& mat) noexcept
{
return mat_extract_scale(mat);
}
ViewAngles extract_rotation_angles(const Mat4X4& mat) noexcept
{
const auto angles = mat_extract_rotation_zyx(mat);
return {
PitchAngle::from_degrees(angles.y),
YawAngle::from_degrees(angles.z),
RollAngle::from_degrees(angles.x),
};
}
Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3<float>& cam_origin) noexcept
{
return mat_camera_view(forward_vector(angles), right_vector(angles), up_vector(angles), cam_origin);
@@ -44,18 +64,16 @@ namespace omath::iw_engine
// aspect ratio.
// vfov = 2 · atan( tan(hfov_4:3 / 2) / (4/3) )
constexpr float k_source_reference_aspect = 4.f / 3.f;
const float half_hfov_4_3 = angles::degrees_to_radians(field_of_view) / 2.f;
const float vfov_deg = angles::radians_to_degrees(
2.f * std::atan(std::tan(half_hfov_4_3) / k_source_reference_aspect));
const auto vertical_fov = angles::horizontal_fov_to_vertical(field_of_view, k_source_reference_aspect);
if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE)
return mat_perspective_left_handed<
return mat_perspective_left_handed_vertical_fov<
float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
vfov_deg, aspect_ratio, near, far);
vertical_fov, aspect_ratio, near, far);
if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE)
return mat_perspective_left_handed<
return mat_perspective_left_handed_vertical_fov<
float, MatStoreType::ROW_MAJOR, NDCDepthRange::NEGATIVE_ONE_TO_ONE>(
vfov_deg, aspect_ratio, near, far);
vertical_fov, aspect_ratio, near, far);
std::unreachable();
};
} // namespace omath::iw_engine
+23 -2
View File
@@ -36,15 +36,36 @@ namespace omath::opengl_engine
* mat_rotation_axis_y<float, MatStoreType::COLUMN_MAJOR>(angles.yaw)
* mat_rotation_axis_x<float, MatStoreType::COLUMN_MAJOR>(angles.pitch);
}
Vector3<float> extract_origin(const Mat4X4& mat) noexcept
{
return mat_extract_origin(mat);
}
Vector3<float> extract_scale(const Mat4X4& mat) noexcept
{
return mat_extract_scale(mat);
}
ViewAngles extract_rotation_angles(const Mat4X4& mat) noexcept
{
const auto angles = mat_extract_rotation_zyx(mat);
return {
PitchAngle::from_degrees(angles.x),
YawAngle::from_degrees(angles.y),
RollAngle::from_degrees(angles.z),
};
}
Mat4X4 calc_perspective_projection_matrix(const float field_of_view, const float aspect_ratio, const float near,
const float far, const NDCDepthRange ndc_depth_range) noexcept
{
if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE)
return mat_perspective_right_handed<float, MatStoreType::COLUMN_MAJOR, NDCDepthRange::NEGATIVE_ONE_TO_ONE>(
return mat_perspective_right_handed_vertical_fov<float, MatStoreType::COLUMN_MAJOR, NDCDepthRange::NEGATIVE_ONE_TO_ONE>(
field_of_view, aspect_ratio, near, far);
if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE)
return mat_perspective_right_handed<float, MatStoreType::COLUMN_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
return mat_perspective_right_handed_vertical_fov<float, MatStoreType::COLUMN_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
field_of_view, aspect_ratio, near, far);
std::unreachable();
+25 -7
View File
@@ -17,6 +17,26 @@ namespace omath::source_engine
return mat_rotation_axis_z(angles.yaw) * mat_rotation_axis_y(angles.pitch) * mat_rotation_axis_x(angles.roll);
}
Vector3<float> extract_origin(const Mat4X4& mat) noexcept
{
return mat_extract_origin(mat);
}
Vector3<float> extract_scale(const Mat4X4& mat) noexcept
{
return mat_extract_scale(mat);
}
ViewAngles extract_rotation_angles(const Mat4X4& mat) noexcept
{
const auto angles = mat_extract_rotation_zyx(mat);
return {
PitchAngle::from_degrees(angles.y),
YawAngle::from_degrees(angles.z),
RollAngle::from_degrees(angles.x),
};
}
Vector3<float> right_vector(const ViewAngles& angles) noexcept
{
const auto vec = rotation_matrix(angles) * mat_column_from_vector(k_abs_right);
@@ -44,18 +64,16 @@ namespace omath::source_engine
// aspect ratio.
// vfov = 2 · atan( tan(hfov_4:3 / 2) / (4/3) )
constexpr float k_source_reference_aspect = 4.f / 3.f;
const float half_hfov_4_3 = angles::degrees_to_radians(field_of_view) / 2.f;
const float vfov_deg = angles::radians_to_degrees(
2.f * std::atan(std::tan(half_hfov_4_3) / k_source_reference_aspect));
const auto vertical_fov = angles::horizontal_fov_to_vertical(field_of_view, k_source_reference_aspect);
if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE)
return mat_perspective_left_handed<
return mat_perspective_left_handed_vertical_fov<
float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
vfov_deg, aspect_ratio, near, far);
vertical_fov, aspect_ratio, near, far);
if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE)
return mat_perspective_left_handed<
return mat_perspective_left_handed_vertical_fov<
float, MatStoreType::ROW_MAJOR, NDCDepthRange::NEGATIVE_ONE_TO_ONE>(
vfov_deg, aspect_ratio, near, far);
vertical_fov, aspect_ratio, near, far);
std::unreachable();
}
} // namespace omath::source_engine
+23 -2
View File
@@ -34,14 +34,35 @@ namespace omath::unity_engine
* mat_rotation_axis_y<float, MatStoreType::ROW_MAJOR>(angles.yaw)
* mat_rotation_axis_x<float, MatStoreType::ROW_MAJOR>(angles.pitch);
}
Vector3<float> extract_origin(const Mat4X4& mat) noexcept
{
return mat_extract_origin(mat);
}
Vector3<float> extract_scale(const Mat4X4& mat) noexcept
{
return mat_extract_scale(mat);
}
ViewAngles extract_rotation_angles(const Mat4X4& mat) noexcept
{
const auto angles = mat_extract_rotation_zyx(mat);
return {
PitchAngle::from_degrees(angles.x),
YawAngle::from_degrees(angles.y),
RollAngle::from_degrees(angles.z),
};
}
Mat4X4 calc_perspective_projection_matrix(const float field_of_view, const float aspect_ratio, const float near,
const float far, const NDCDepthRange ndc_depth_range) noexcept
{
if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE)
return omath::mat_perspective_right_handed<float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
return omath::mat_perspective_right_handed_vertical_fov<float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
field_of_view, aspect_ratio, near, far);
if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE)
return omath::mat_perspective_right_handed<float, MatStoreType::ROW_MAJOR,
return omath::mat_perspective_right_handed_vertical_fov<float, MatStoreType::ROW_MAJOR,
NDCDepthRange::NEGATIVE_ONE_TO_ONE>(field_of_view, aspect_ratio,
near, far);
std::unreachable();
+20
View File
@@ -39,6 +39,26 @@ namespace omath::unreal_engine
}
Vector3<double> extract_origin(const Mat4X4& mat) noexcept
{
return mat_extract_origin(mat);
}
Vector3<double> extract_scale(const Mat4X4& mat) noexcept
{
return mat_extract_scale(mat);
}
ViewAngles extract_rotation_angles(const Mat4X4& mat) noexcept
{
const auto angles = mat_extract_rotation_zyx(mat);
return {
PitchAngle::from_degrees(-angles.y),
YawAngle::from_degrees(angles.z),
RollAngle::from_degrees(-angles.x),
};
}
Mat4X4 calc_perspective_projection_matrix(const double field_of_view, const double aspect_ratio, const double near,
const double far, const NDCDepthRange ndc_depth_range) noexcept
{
+742
View File
@@ -0,0 +1,742 @@
#include "omath/hooks/hooks_manager.hpp"
#ifdef OMATH_ENABLE_HOOKING
#ifdef _WIN32
#include <d3d11.h>
#endif // _WIN32
#ifdef __linux__
#include <dlfcn.h>
#endif // __linux__
namespace
{
#ifdef _WIN32
thread_local bool g_is_inside_opengl_swap_buffers = false;
class DummyWindow final
{
WNDCLASSEX m_window_class{};
HWND m_window_handle = nullptr;
public:
DummyWindow()
{
m_window_class.cbSize = sizeof(WNDCLASSEX);
m_window_class.style = CS_HREDRAW | CS_VREDRAW;
m_window_class.lpfnWndProc = DefWindowProc;
m_window_class.hInstance = GetModuleHandle(nullptr);
m_window_class.lpszClassName = "OM";
RegisterClassEx(&m_window_class);
m_window_handle = CreateWindow(m_window_class.lpszClassName, "Dummy", WS_OVERLAPPEDWINDOW, 0, 0, 100, 100,
nullptr, nullptr, m_window_class.hInstance, nullptr);
}
~DummyWindow()
{
if (m_window_handle)
DestroyWindow(m_window_handle);
UnregisterClass(m_window_class.lpszClassName, m_window_class.hInstance);
}
[[nodiscard]] HWND handle() const
{
return m_window_handle;
}
[[nodiscard]] bool valid() const
{
return m_window_handle != nullptr;
}
};
void* vtable_fn(void* com_obj, std::size_t index)
{
return (*reinterpret_cast<void***>(com_obj))[index];
}
void* module_proc(const char* module_name, const char* proc_name)
{
const HMODULE module = GetModuleHandle(module_name);
if (!module)
return nullptr;
return reinterpret_cast<void*>(GetProcAddress(module, proc_name));
}
struct dx12_vtable_fns
{
void* present;
void* resize_buffers;
void* execute_command_lists;
};
// RAII wrapper so all early-return paths release COM objects automatically.
struct dx12_com_objects
{
IDXGIFactory* factory = nullptr;
ID3D12Device* device = nullptr;
ID3D12CommandQueue* command_queue = nullptr;
ID3D12CommandAllocator* command_allocator = nullptr;
ID3D12GraphicsCommandList* command_list = nullptr;
IDXGISwapChain* swap_chain = nullptr;
dx12_com_objects() = default;
dx12_com_objects(const dx12_com_objects&) = delete;
dx12_com_objects& operator=(const dx12_com_objects&) = delete;
~dx12_com_objects()
{
if (swap_chain)
swap_chain->Release();
if (command_list)
command_list->Release();
if (command_allocator)
command_allocator->Release();
if (command_queue)
command_queue->Release();
if (device)
device->Release();
if (factory)
factory->Release();
}
};
std::optional<dx12_vtable_fns> read_dx12_vtable_fns(HWND hwnd)
{
using create_dxgi_factory_fn = HRESULT(__stdcall*)(REFIID, void**);
using d3d12_create_device_fn = HRESULT(__stdcall*)(IUnknown*, D3D_FEATURE_LEVEL, REFIID, void**);
const HMODULE d3d12_module = GetModuleHandle("d3d12.dll");
const HMODULE dxgi_module = GetModuleHandle("dxgi.dll");
if (!d3d12_module || !dxgi_module)
return std::nullopt;
const auto create_dxgi_factory =
reinterpret_cast<create_dxgi_factory_fn>(GetProcAddress(dxgi_module, "CreateDXGIFactory"));
const auto d3d12_create_device =
reinterpret_cast<d3d12_create_device_fn>(GetProcAddress(d3d12_module, "D3D12CreateDevice"));
if (!create_dxgi_factory || !d3d12_create_device)
return std::nullopt;
dx12_com_objects objs;
if (FAILED(create_dxgi_factory(__uuidof(IDXGIFactory), reinterpret_cast<void**>(&objs.factory))))
return std::nullopt;
IDXGIAdapter* adapter = nullptr;
if (objs.factory->EnumAdapters(0, &adapter) == DXGI_ERROR_NOT_FOUND)
return std::nullopt;
const HRESULT device_hr = d3d12_create_device(adapter, D3D_FEATURE_LEVEL_11_0, __uuidof(ID3D12Device),
reinterpret_cast<void**>(&objs.device));
adapter->Release();
if (FAILED(device_hr))
return std::nullopt;
D3D12_COMMAND_QUEUE_DESC queue_desc{};
queue_desc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
if (FAILED(objs.device->CreateCommandQueue(&queue_desc, __uuidof(ID3D12CommandQueue),
reinterpret_cast<void**>(&objs.command_queue))))
return std::nullopt;
if (FAILED(objs.device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, __uuidof(ID3D12CommandAllocator),
reinterpret_cast<void**>(&objs.command_allocator))))
return std::nullopt;
if (FAILED(objs.device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, objs.command_allocator, nullptr,
__uuidof(ID3D12GraphicsCommandList),
reinterpret_cast<void**>(&objs.command_list))))
return std::nullopt;
DXGI_SWAP_CHAIN_DESC swap_chain_desc{};
swap_chain_desc.BufferDesc.Width = 100;
swap_chain_desc.BufferDesc.Height = 100;
swap_chain_desc.BufferDesc.RefreshRate = {60, 1};
swap_chain_desc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
swap_chain_desc.SampleDesc = {1, 0};
swap_chain_desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
swap_chain_desc.BufferCount = 2;
swap_chain_desc.OutputWindow = hwnd;
swap_chain_desc.Windowed = TRUE;
swap_chain_desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
swap_chain_desc.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
if (FAILED(objs.factory->CreateSwapChain(objs.command_queue, &swap_chain_desc, &objs.swap_chain)))
return std::nullopt;
// objs destructor releases all COM objects after we capture the addresses.
return dx12_vtable_fns{
vtable_fn(objs.swap_chain, 8), // IDXGISwapChain::Present
vtable_fn(objs.swap_chain, 13), // IDXGISwapChain::ResizeBuffers
vtable_fn(objs.command_queue, 10), // ID3D12CommandQueue::ExecuteCommandLists
};
}
#endif // _WIN32
} // namespace
namespace omath::hooks
{
HooksManager& HooksManager::get()
{
static HooksManager obj;
return obj;
}
HooksManager::~HooksManager()
{
#ifdef _WIN32
unhook_wnd_proc();
unhook_dx9();
unhook_dx11();
unhook_dx12();
#endif // _WIN32
unhook_opengl();
}
#ifdef _WIN32
bool HooksManager::hook_dx9()
{
std::unique_lock lock(m_hook_state_mutex);
if (m_is_dx9_hooked)
return true;
const DummyWindow window;
if (!window.valid())
return false;
const HMODULE d3d9_module = GetModuleHandle("d3d9.dll");
if (!d3d9_module)
return false;
using direct3d_create9_fn = IDirect3D9*(__stdcall*)(UINT);
const auto direct3d_create9 =
reinterpret_cast<direct3d_create9_fn>(GetProcAddress(d3d9_module, "Direct3DCreate9"));
if (!direct3d_create9)
return false;
IDirect3D9* d3d9 = direct3d_create9(D3D_SDK_VERSION);
if (!d3d9)
return false;
D3DPRESENT_PARAMETERS pp{};
pp.SwapEffect = D3DSWAPEFFECT_DISCARD;
pp.hDeviceWindow = window.handle();
pp.Windowed = TRUE;
IDirect3DDevice9* device = nullptr;
if (FAILED(d3d9->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, window.handle(),
D3DCREATE_SOFTWARE_VERTEXPROCESSING, &pp, &device)))
{
d3d9->Release();
return false;
}
// IDirect3DDevice9 vtable indices (from IUnknown base):
// Reset = 16
// Present = 17
// EndScene = 42
m_dx9_present_hook =
safetyhook::create_inline(vtable_fn(device, 17), reinterpret_cast<void*>(&dx9_present_detour));
m_dx9_reset_hook = safetyhook::create_inline(vtable_fn(device, 16), reinterpret_cast<void*>(&dx9_reset_detour));
m_dx9_end_scene_hook =
safetyhook::create_inline(vtable_fn(device, 42), reinterpret_cast<void*>(&dx9_end_scene_detour));
device->Release();
d3d9->Release();
if (!m_dx9_present_hook || !m_dx9_reset_hook || !m_dx9_end_scene_hook)
{
m_dx9_present_hook = {};
m_dx9_reset_hook = {};
m_dx9_end_scene_hook = {};
return false;
}
m_is_dx9_hooked = true;
return true;
}
void HooksManager::unhook_dx9()
{
std::unique_lock lock(m_hook_state_mutex);
m_dx9_present_hook = {};
m_dx9_reset_hook = {};
m_dx9_end_scene_hook = {};
m_is_dx9_hooked = false;
}
void HooksManager::set_on_dx9_present(dx9_present_callback callback)
{
std::unique_lock lock(m_dx9_present_mutex);
m_dx9_present_cb = callback ? std::make_shared<dx9_present_callback>(std::move(callback)) : nullptr;
}
void HooksManager::set_on_dx9_reset(dx9_reset_callback callback)
{
std::unique_lock lock(m_dx9_reset_mutex);
m_dx9_reset_cb = callback ? std::make_shared<dx9_reset_callback>(std::move(callback)) : nullptr;
}
void HooksManager::set_on_dx9_end_scene(dx9_end_scene_callback callback)
{
std::unique_lock lock(m_dx9_end_scene_mutex);
m_dx9_end_scene_cb = callback ? std::make_shared<dx9_end_scene_callback>(std::move(callback)) : nullptr;
}
bool HooksManager::hook_dx11()
{
std::unique_lock lock(m_hook_state_mutex);
if (m_is_dx11_hooked)
return true;
const DummyWindow window;
if (!window.valid())
return false;
const HMODULE d3d11_module = GetModuleHandle("d3d11.dll");
if (!d3d11_module)
return false;
using d3d11_create_device_and_swap_chain_fn =
HRESULT(__stdcall*)(IDXGIAdapter*, D3D_DRIVER_TYPE, HMODULE, UINT, const D3D_FEATURE_LEVEL*, UINT, UINT,
const DXGI_SWAP_CHAIN_DESC*, IDXGISwapChain**, ID3D11Device**, D3D_FEATURE_LEVEL*,
ID3D11DeviceContext**);
const auto create_device_and_swap_chain = reinterpret_cast<d3d11_create_device_and_swap_chain_fn>(
GetProcAddress(d3d11_module, "D3D11CreateDeviceAndSwapChain"));
if (!create_device_and_swap_chain)
return false;
DXGI_SWAP_CHAIN_DESC swap_chain_desc{};
swap_chain_desc.BufferDesc.Width = 100;
swap_chain_desc.BufferDesc.Height = 100;
swap_chain_desc.BufferDesc.RefreshRate = {60, 1};
swap_chain_desc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
swap_chain_desc.SampleDesc = {1, 0};
swap_chain_desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
swap_chain_desc.BufferCount = 1;
swap_chain_desc.OutputWindow = window.handle();
swap_chain_desc.Windowed = TRUE;
swap_chain_desc.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
constexpr D3D_FEATURE_LEVEL feature_levels[] = {D3D_FEATURE_LEVEL_11_0};
ID3D11Device* device = nullptr;
ID3D11DeviceContext* device_context = nullptr;
IDXGISwapChain* swap_chain = nullptr;
if (FAILED(create_device_and_swap_chain(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, feature_levels, 1,
D3D11_SDK_VERSION, &swap_chain_desc, &swap_chain, &device, nullptr,
&device_context)))
return false;
m_dx11_present_hook = safetyhook::create_inline(vtable_fn(swap_chain, 8), // IDXGISwapChain::Present
reinterpret_cast<void*>(&dx11_present_detour));
m_dx11_resize_buffers_hook =
safetyhook::create_inline(vtable_fn(swap_chain, 13), // IDXGISwapChain::ResizeBuffers
reinterpret_cast<void*>(&dx11_resize_buffers_detour));
swap_chain->Release();
device_context->Release();
device->Release();
if (!m_dx11_present_hook || !m_dx11_resize_buffers_hook)
{
m_dx11_present_hook = {};
m_dx11_resize_buffers_hook = {};
return false;
}
m_is_dx11_hooked = true;
return true;
}
void HooksManager::unhook_dx11()
{
std::unique_lock lock(m_hook_state_mutex);
m_dx11_present_hook = {};
m_dx11_resize_buffers_hook = {};
m_is_dx11_hooked = false;
}
bool HooksManager::hook_dx12()
{
std::unique_lock lock(m_hook_state_mutex);
if (m_is_dx12_hooked)
return true;
const DummyWindow window;
if (!window.valid())
return false;
const auto fns = read_dx12_vtable_fns(window.handle());
if (!fns)
return false;
m_dx12_present_hook = safetyhook::create_inline(fns->present, reinterpret_cast<void*>(&dx12_present_detour));
m_dx12_resize_buffers_hook =
safetyhook::create_inline(fns->resize_buffers, reinterpret_cast<void*>(&dx12_resize_buffers_detour));
m_dx12_execute_command_lists_hook = safetyhook::create_inline(
fns->execute_command_lists, reinterpret_cast<void*>(&dx12_execute_command_lists_detour));
if (!m_dx12_present_hook || !m_dx12_resize_buffers_hook || !m_dx12_execute_command_lists_hook)
{
m_dx12_present_hook = {};
m_dx12_resize_buffers_hook = {};
m_dx12_execute_command_lists_hook = {};
return false;
}
m_is_dx12_hooked = true;
return true;
}
void HooksManager::unhook_dx12()
{
std::unique_lock lock(m_hook_state_mutex);
m_dx12_present_hook = {};
m_dx12_resize_buffers_hook = {};
m_dx12_execute_command_lists_hook = {};
m_is_dx12_hooked = false;
}
bool HooksManager::hook_opengl()
{
std::unique_lock lock(m_hook_state_mutex);
if (m_is_opengl_hooked)
return true;
if (void* wgl_swap_buffers = module_proc("opengl32.dll", "wglSwapBuffers"))
{
m_opengl_wgl_swap_buffers_hook = safetyhook::create_inline(
wgl_swap_buffers, reinterpret_cast<void*>(&opengl_wgl_swap_buffers_detour));
}
if (void* swap_buffers = module_proc("gdi32.dll", "SwapBuffers"))
{
m_opengl_swap_buffers_hook =
safetyhook::create_inline(swap_buffers, reinterpret_cast<void*>(&opengl_swap_buffers_detour));
}
if (!m_opengl_wgl_swap_buffers_hook && !m_opengl_swap_buffers_hook)
{
m_opengl_wgl_swap_buffers_hook = {};
m_opengl_swap_buffers_hook = {};
return false;
}
m_is_opengl_hooked = true;
return true;
}
void HooksManager::unhook_opengl()
{
std::unique_lock lock(m_hook_state_mutex);
m_opengl_wgl_swap_buffers_hook = {};
m_opengl_swap_buffers_hook = {};
m_is_opengl_hooked = false;
}
void HooksManager::set_on_present(present_callback callback)
{
std::unique_lock lock(m_present_mutex);
m_present_cb = callback ? std::make_shared<present_callback>(std::move(callback)) : nullptr;
}
void HooksManager::set_on_resize_buffers(resize_buffers_callback callback)
{
std::unique_lock lock(m_resize_buffers_mutex);
m_resize_buffers_cb = callback ? std::make_shared<resize_buffers_callback>(std::move(callback)) : nullptr;
}
void HooksManager::set_on_execute_command_lists(execute_command_lists_callback callback)
{
std::unique_lock lock(m_execute_command_lists_mutex);
m_execute_command_lists_cb =
callback ? std::make_shared<execute_command_lists_callback>(std::move(callback)) : nullptr;
}
bool HooksManager::hook_wnd_proc(HWND hwnd)
{
std::unique_lock lock(m_hook_state_mutex);
if (m_is_wnd_proc_hooked)
return true;
const auto prev = reinterpret_cast<WNDPROC>(
SetWindowLongPtr(hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(&wnd_proc_detour)));
if (!prev)
return false;
m_hooked_hwnd = hwnd;
m_original_wndproc = prev;
m_is_wnd_proc_hooked = true;
return true;
}
void HooksManager::unhook_wnd_proc()
{
std::unique_lock lock(m_hook_state_mutex);
if (!m_is_wnd_proc_hooked)
return;
SetWindowLongPtr(m_hooked_hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(m_original_wndproc));
m_hooked_hwnd = nullptr;
m_original_wndproc = nullptr;
m_is_wnd_proc_hooked = false;
}
void HooksManager::set_on_wnd_proc(wnd_proc_callback callback)
{
std::unique_lock lock(m_wnd_proc_mutex);
m_wnd_proc_cb = callback ? std::make_shared<wnd_proc_callback>(std::move(callback)) : nullptr;
}
// Detour implementations: copy a shared_ptr to the callback under shared lock, call it unlocked,
// then call original. This avoids copying captured lambda state every frame and still avoids
// a deadlock if the callback itself calls set_on_*().
HRESULT __stdcall HooksManager::dx9_present_detour(IDirect3DDevice9* p_device, const RECT* p_source_rect,
const RECT* p_dest_rect, HWND h_dest_window_override,
const RGNDATA* p_dirty_region)
{
auto& mgr = get();
callback_ptr<dx9_present_callback> cb;
{
std::shared_lock lock(mgr.m_dx9_present_mutex);
cb = mgr.m_dx9_present_cb;
}
if (cb)
(*cb)(p_device, p_source_rect, p_dest_rect, h_dest_window_override, p_dirty_region);
return mgr.m_dx9_present_hook.call<HRESULT>(p_device, p_source_rect, p_dest_rect, h_dest_window_override,
p_dirty_region);
}
HRESULT __stdcall HooksManager::dx9_reset_detour(IDirect3DDevice9* p_device,
D3DPRESENT_PARAMETERS* p_presentation_parameters)
{
auto& mgr = get();
callback_ptr<dx9_reset_callback> cb;
{
std::shared_lock lock(mgr.m_dx9_reset_mutex);
cb = mgr.m_dx9_reset_cb;
}
if (cb)
(*cb)(p_device, p_presentation_parameters);
return mgr.m_dx9_reset_hook.call<HRESULT>(p_device, p_presentation_parameters);
}
HRESULT __stdcall HooksManager::dx9_end_scene_detour(IDirect3DDevice9* p_device)
{
auto& mgr = get();
callback_ptr<dx9_end_scene_callback> cb;
{
std::shared_lock lock(mgr.m_dx9_end_scene_mutex);
cb = mgr.m_dx9_end_scene_cb;
}
if (cb)
(*cb)(p_device);
return mgr.m_dx9_end_scene_hook.call<HRESULT>(p_device);
}
HRESULT __stdcall HooksManager::dx11_present_detour(IDXGISwapChain* p_swap_chain, UINT sync_interval, UINT flags)
{
auto& mgr = get();
callback_ptr<present_callback> cb;
{
std::shared_lock lock(mgr.m_present_mutex);
cb = mgr.m_present_cb;
}
if (cb)
(*cb)(p_swap_chain, sync_interval, flags);
return mgr.m_dx11_present_hook.call<HRESULT>(p_swap_chain, sync_interval, flags);
}
HRESULT __stdcall HooksManager::dx11_resize_buffers_detour(IDXGISwapChain* p_swap_chain, UINT buffer_count,
UINT width, UINT height, DXGI_FORMAT new_format,
UINT swap_chain_flags)
{
auto& mgr = get();
callback_ptr<resize_buffers_callback> cb;
{
std::shared_lock lock(mgr.m_resize_buffers_mutex);
cb = mgr.m_resize_buffers_cb;
}
if (cb)
(*cb)(p_swap_chain, buffer_count, width, height, new_format, swap_chain_flags);
return mgr.m_dx11_resize_buffers_hook.call<HRESULT>(p_swap_chain, buffer_count, width, height, new_format,
swap_chain_flags);
}
HRESULT __stdcall HooksManager::dx12_present_detour(IDXGISwapChain* p_swap_chain, UINT sync_interval, UINT flags)
{
auto& mgr = get();
callback_ptr<present_callback> cb;
{
std::shared_lock lock(mgr.m_present_mutex);
cb = mgr.m_present_cb;
}
if (cb)
(*cb)(p_swap_chain, sync_interval, flags);
return mgr.m_dx12_present_hook.call<HRESULT>(p_swap_chain, sync_interval, flags);
}
HRESULT __stdcall HooksManager::dx12_resize_buffers_detour(IDXGISwapChain* p_swap_chain, UINT buffer_count,
UINT width, UINT height, DXGI_FORMAT new_format,
UINT swap_chain_flags)
{
auto& mgr = get();
callback_ptr<resize_buffers_callback> cb;
{
std::shared_lock lock(mgr.m_resize_buffers_mutex);
cb = mgr.m_resize_buffers_cb;
}
if (cb)
(*cb)(p_swap_chain, buffer_count, width, height, new_format, swap_chain_flags);
return mgr.m_dx12_resize_buffers_hook.call<HRESULT>(p_swap_chain, buffer_count, width, height, new_format,
swap_chain_flags);
}
void __stdcall HooksManager::dx12_execute_command_lists_detour(ID3D12CommandQueue* p_command_queue,
UINT num_command_lists,
ID3D12CommandList* const* pp_command_lists)
{
auto& mgr = get();
callback_ptr<execute_command_lists_callback> cb;
{
std::shared_lock lock(mgr.m_execute_command_lists_mutex);
cb = mgr.m_execute_command_lists_cb;
}
if (cb)
(*cb)(p_command_queue, num_command_lists, pp_command_lists);
mgr.m_dx12_execute_command_lists_hook.call<void>(p_command_queue, num_command_lists, pp_command_lists);
}
BOOL __stdcall HooksManager::opengl_wgl_swap_buffers_detour(HDC hdc)
{
auto& mgr = get();
if (!g_is_inside_opengl_swap_buffers)
return mgr.m_opengl_wgl_swap_buffers_hook.call<BOOL>(hdc);
g_is_inside_opengl_swap_buffers = true;
callback_ptr<opengl_swap_buffers_callback> cb;
{
std::shared_lock lock(mgr.m_opengl_swap_buffers_mutex);
cb = mgr.m_opengl_swap_buffers_cb;
}
if (cb)
(*cb)(hdc);
const BOOL result = mgr.m_opengl_wgl_swap_buffers_hook.call<BOOL>(hdc);
g_is_inside_opengl_swap_buffers = false;
return result;
}
BOOL __stdcall HooksManager::opengl_swap_buffers_detour(HDC hdc)
{
auto& mgr = get();
if (g_is_inside_opengl_swap_buffers)
return mgr.m_opengl_swap_buffers_hook.call<BOOL>(hdc);
g_is_inside_opengl_swap_buffers = true;
callback_ptr<opengl_swap_buffers_callback> cb;
{
std::shared_lock lock(mgr.m_opengl_swap_buffers_mutex);
cb = mgr.m_opengl_swap_buffers_cb;
}
if (cb)
(*cb)(hdc);
const BOOL result = mgr.m_opengl_swap_buffers_hook.call<BOOL>(hdc);
g_is_inside_opengl_swap_buffers = false;
return result;
}
LRESULT __stdcall HooksManager::wnd_proc_detour(HWND hwnd, UINT msg, WPARAM w_param, LPARAM l_param)
{
auto& mgr = get();
callback_ptr<wnd_proc_callback> cb;
WNDPROC original;
{
std::shared_lock lock(mgr.m_wnd_proc_mutex);
cb = mgr.m_wnd_proc_cb;
original = mgr.m_original_wndproc;
}
if (cb)
{
if (const auto result = (*cb)(hwnd, msg, w_param, l_param))
return *result;
}
return CallWindowProc(original, hwnd, msg, w_param, l_param);
}
#endif // _WIN32
#ifdef __linux__
bool HooksManager::hook_opengl()
{
std::unique_lock lock(m_hook_state_mutex);
if (m_is_opengl_hooked)
return true;
void* glx_swap_buffers = dlsym(RTLD_DEFAULT, "glXSwapBuffers");
if (!glx_swap_buffers)
return false;
m_opengl_glx_swap_buffers_hook = safetyhook::create_inline(
glx_swap_buffers, reinterpret_cast<void*>(&opengl_glx_swap_buffers_detour));
if (!m_opengl_glx_swap_buffers_hook)
return false;
m_is_opengl_hooked = true;
return true;
}
void HooksManager::unhook_opengl()
{
std::unique_lock lock(m_hook_state_mutex);
m_opengl_glx_swap_buffers_hook = {};
m_is_opengl_hooked = false;
}
void HooksManager::opengl_glx_swap_buffers_detour(Display* display, GLXDrawable drawable)
{
auto& mgr = get();
callback_ptr<opengl_swap_buffers_callback> cb;
{
std::shared_lock lock(mgr.m_opengl_swap_buffers_mutex);
cb = mgr.m_opengl_swap_buffers_cb;
}
if (cb)
(*cb)(display, drawable);
mgr.m_opengl_glx_swap_buffers_hook.call<void>(display, drawable);
}
#endif // __linux__
void HooksManager::set_on_opengl_swap_buffers(opengl_swap_buffers_callback callback)
{
std::unique_lock lock(m_opengl_swap_buffers_mutex);
m_opengl_swap_buffers_cb =
callback ? std::make_shared<opengl_swap_buffers_callback>(std::move(callback)) : nullptr;
}
} // namespace omath::hooks
#else // !OMATH_ENABLE_HOOKING
namespace omath::hooks
{
HooksManager& HooksManager::get()
{
static HooksManager obj;
return obj;
}
HooksManager::~HooksManager() = default;
} // namespace omath::hooks
#endif
+34 -24
View File
@@ -5,10 +5,14 @@
namespace omath::hud
{
EntityOverlay& EntityOverlay::add_2d_box(const Color& box_color, const Color& fill_color, const float thickness)
EntityOverlay& EntityOverlay::add_2d_box(const Color& box_color, const Color& fill_color,
const Color& outline_color, const float thickness)
{
const auto points = m_canvas.as_array();
if (outline_color.value().w > 0.f)
m_renderer->add_polyline({points.data(), points.size()}, outline_color, thickness + 2.f);
m_renderer->add_polyline({points.data(), points.size()}, box_color, thickness);
if (fill_color.value().w > 0.f)
@@ -17,41 +21,46 @@ namespace omath::hud
return *this;
}
EntityOverlay& EntityOverlay::add_cornered_2d_box(const Color& box_color, const Color& fill_color,
const float corner_ratio_len, const float thickness)
const Color& outline_color, const float corner_ratio_len,
const float thickness)
{
const auto corner_line_length =
std::abs((m_canvas.top_left_corner - m_canvas.top_right_corner).x * corner_ratio_len);
if (fill_color.value().w > 0.f)
add_2d_box(fill_color, fill_color);
const auto draw_corner_line = [&](const Vector2<float>& a, const Vector2<float>& b)
{
if (outline_color.value().w > 0.f)
m_renderer->add_line(a, b, outline_color, thickness + 2.f);
m_renderer->add_line(a, b, box_color, thickness);
};
// Left Side
m_renderer->add_line(m_canvas.top_left_corner,
m_canvas.top_left_corner + Vector2<float>{corner_line_length, 0.f}, box_color, thickness);
draw_corner_line(m_canvas.top_left_corner,
m_canvas.top_left_corner + Vector2<float>{corner_line_length, 0.f});
m_renderer->add_line(m_canvas.top_left_corner,
m_canvas.top_left_corner + Vector2<float>{0.f, corner_line_length}, box_color, thickness);
draw_corner_line(m_canvas.top_left_corner,
m_canvas.top_left_corner + Vector2<float>{0.f, corner_line_length});
m_renderer->add_line(m_canvas.bottom_left_corner,
m_canvas.bottom_left_corner - Vector2<float>{0.f, corner_line_length}, box_color,
thickness);
draw_corner_line(m_canvas.bottom_left_corner,
m_canvas.bottom_left_corner - Vector2<float>{0.f, corner_line_length});
m_renderer->add_line(m_canvas.bottom_left_corner,
m_canvas.bottom_left_corner + Vector2<float>{corner_line_length, 0.f}, box_color,
thickness);
draw_corner_line(m_canvas.bottom_left_corner,
m_canvas.bottom_left_corner + Vector2<float>{corner_line_length, 0.f});
// Right Side
m_renderer->add_line(m_canvas.top_right_corner,
m_canvas.top_right_corner - Vector2<float>{corner_line_length, 0.f}, box_color, thickness);
draw_corner_line(m_canvas.top_right_corner,
m_canvas.top_right_corner - Vector2<float>{corner_line_length, 0.f});
m_renderer->add_line(m_canvas.top_right_corner,
m_canvas.top_right_corner + Vector2<float>{0.f, corner_line_length}, box_color, thickness);
draw_corner_line(m_canvas.top_right_corner,
m_canvas.top_right_corner + Vector2<float>{0.f, corner_line_length});
m_renderer->add_line(m_canvas.bottom_right_corner,
m_canvas.bottom_right_corner - Vector2<float>{0.f, corner_line_length}, box_color,
thickness);
draw_corner_line(m_canvas.bottom_right_corner,
m_canvas.bottom_right_corner - Vector2<float>{0.f, corner_line_length});
m_renderer->add_line(m_canvas.bottom_right_corner,
m_canvas.bottom_right_corner - Vector2<float>{corner_line_length, 0.f}, box_color,
thickness);
draw_corner_line(m_canvas.bottom_right_corner,
m_canvas.bottom_right_corner - Vector2<float>{corner_line_length, 0.f});
return *this;
}
@@ -591,12 +600,13 @@ namespace omath::hud
// ── widget dispatch ───────────────────────────────────────────────────────
void EntityOverlay::dispatch(const widget::Box& box)
{
add_2d_box(box.color, box.fill, box.thickness);
add_2d_box(box.color, box.fill, box.outline, box.thickness);
}
void EntityOverlay::dispatch(const widget::CorneredBox& cornered_box)
{
add_cornered_2d_box(cornered_box.color, cornered_box.fill, cornered_box.corner_ratio, cornered_box.thickness);
add_cornered_2d_box(cornered_box.color, cornered_box.fill, cornered_box.outline, cornered_box.corner_ratio,
cornered_box.thickness);
}
void EntityOverlay::dispatch(const widget::DashedBox& dashed_box)
+5
View File
@@ -17,8 +17,13 @@ namespace omath::lua
register_vec2(omath_table);
register_vec3(omath_table);
register_vec4(omath_table);
register_matrices(omath_table);
register_quaternion(omath_table);
register_color(omath_table);
register_hud(omath_table);
register_triangle(omath_table);
register_3d_primitives(omath_table);
register_collision(omath_table);
register_shared_types(omath_table);
register_engines(omath_table);
register_pattern_scan(omath_table);
+304
View File
@@ -0,0 +1,304 @@
//
// Created by orange on 07.03.2026.
//
#ifdef OMATH_ENABLE_LUA
#include "omath/lua/lua.hpp"
#include <omath/3d_primitives/aabb.hpp>
#include <omath/3d_primitives/obb.hpp>
#include <omath/collision/collider_interface.hpp>
#include <omath/collision/epa_algorithm.hpp>
#include <omath/collision/gjk_algorithm.hpp>
#include <omath/collision/line_tracer.hpp>
#include <omath/linear_algebra/triangle.hpp>
#include <omath/linear_algebra/vector3.hpp>
#include <sol/sol.hpp>
#include <algorithm>
#include <stdexcept>
#include <vector>
namespace
{
using Vec3f = omath::Vector3<float>;
using Triangle3f = omath::Triangle<Vec3f>;
using Ray3f = omath::collision::Ray<Vec3f>;
using LineTracer3f = omath::collision::LineTracer<Ray3f>;
using Aabbf = omath::primitives::Aabb<float>;
using Obbf = omath::primitives::Obb<float>;
template<class Object, class Value>
auto lua_field(Value Object::* member)
{
return sol::property(
[member](const Object& object) -> const Value&
{
return object.*member;
},
[member](Object& object, const Value& value)
{
object.*member = value;
});
}
class LuaConvexCollider final : public omath::collision::ColliderInterface<Vec3f>
{
public:
using VectorType = Vec3f;
explicit LuaConvexCollider(std::vector<Vec3f> vertices, const Vec3f& origin = {})
: m_vertices(std::move(vertices)), m_origin(origin)
{
if (m_vertices.empty())
throw std::invalid_argument("convex collider must contain at least one vertex");
}
[[nodiscard]]
Vec3f find_abs_furthest_vertex_position(const Vec3f& direction) const override
{
const auto furthest = std::ranges::max_element(
m_vertices,
[&direction](const Vec3f& first, const Vec3f& second)
{
return first.dot(direction) < second.dot(direction);
});
return m_origin + *furthest;
}
[[nodiscard]]
const Vec3f& get_origin() const override
{
return m_origin;
}
void set_origin(const Vec3f& new_origin) override
{
m_origin = new_origin;
}
[[nodiscard]]
std::size_t vertex_count() const noexcept
{
return m_vertices.size();
}
[[nodiscard]]
const std::vector<Vec3f>& vertices() const noexcept
{
return m_vertices;
}
private:
std::vector<Vec3f> m_vertices;
Vec3f m_origin;
};
std::vector<Vec3f> vec3_table_to_vector(const sol::table& points)
{
std::vector<Vec3f> result;
for (std::size_t i = 1;; ++i)
{
const auto point = points[i].get<sol::optional<Vec3f>>();
if (!point)
break;
result.push_back(*point);
}
return result;
}
sol::table vec3_array_to_table(const auto& points, sol::this_state state)
{
sol::state_view lua(state);
sol::table result = lua.create_table(static_cast<int>(points.size()), 0);
for (std::size_t i = 0; i < points.size(); ++i)
result[i + 1] = points[i];
return result;
}
Vec3f aabb_top(const Aabbf& aabb, const omath::primitives::UpAxis axis)
{
switch (axis)
{
case omath::primitives::UpAxis::X:
return aabb.top<omath::primitives::UpAxis::X>();
case omath::primitives::UpAxis::Y:
return aabb.top<omath::primitives::UpAxis::Y>();
case omath::primitives::UpAxis::Z:
return aabb.top<omath::primitives::UpAxis::Z>();
}
std::unreachable();
}
Vec3f aabb_bottom(const Aabbf& aabb, const omath::primitives::UpAxis axis)
{
switch (axis)
{
case omath::primitives::UpAxis::X:
return aabb.bottom<omath::primitives::UpAxis::X>();
case omath::primitives::UpAxis::Y:
return aabb.bottom<omath::primitives::UpAxis::Y>();
case omath::primitives::UpAxis::Z:
return aabb.bottom<omath::primitives::UpAxis::Z>();
}
std::unreachable();
}
bool ray_hits_triangle(const Ray3f& ray, const Triangle3f& triangle)
{
return !(LineTracer3f::get_ray_hit_point(ray, triangle) == ray.end);
}
bool ray_hits_aabb(const Ray3f& ray, const Aabbf& aabb)
{
return !(LineTracer3f::get_ray_hit_point(ray, aabb) == ray.end);
}
bool ray_hits_obb(const Ray3f& ray, const Obbf& obb)
{
return !(LineTracer3f::get_ray_hit_point(ray, obb) == ray.end);
}
} // namespace
namespace omath::lua
{
void LuaInterpreter::register_3d_primitives(sol::table& omath_table)
{
auto primitives_table = omath_table["primitives"].get_or_create<sol::table>();
primitives_table.new_enum("UpAxis", "X", omath::primitives::UpAxis::X, "Y", omath::primitives::UpAxis::Y, "Z",
omath::primitives::UpAxis::Z);
primitives_table.new_usertype<Aabbf>(
"Aabb",
sol::factories([]() { return Aabbf{}; },
[](const Vec3f& min, const Vec3f& max) { return Aabbf{min, max}; }),
"min", lua_field(&Aabbf::min), "max", lua_field(&Aabbf::max), "center", &Aabbf::center, "extents",
&Aabbf::extents,
"top",
[](const Aabbf& aabb, sol::optional<omath::primitives::UpAxis> axis)
{
return aabb_top(aabb, axis.value_or(omath::primitives::UpAxis::Y));
},
"bottom",
[](const Aabbf& aabb, sol::optional<omath::primitives::UpAxis> axis)
{
return aabb_bottom(aabb, axis.value_or(omath::primitives::UpAxis::Y));
},
"is_collide", &Aabbf::is_collide,
"as_table",
[](const Aabbf& aabb, sol::this_state state) -> sol::table
{
sol::state_view lua(state);
sol::table result = lua.create_table();
result["min"] = aabb.min;
result["max"] = aabb.max;
return result;
});
primitives_table.new_usertype<Obbf>(
"Obb",
sol::factories(
[]()
{
return Obbf{};
},
[](const Vec3f& center, const Vec3f& axis_x, const Vec3f& axis_y, const Vec3f& axis_z,
const Vec3f& half_extents)
{
return Obbf{center, axis_x, axis_y, axis_z, half_extents};
}),
"center", lua_field(&Obbf::center), "axis_x", lua_field(&Obbf::axis_x), "axis_y",
lua_field(&Obbf::axis_y), "axis_z", lua_field(&Obbf::axis_z), "half_extents",
lua_field(&Obbf::half_extents),
"vertices",
[](const Obbf& obb, sol::this_state state)
{
return vec3_array_to_table(obb.vertices(), state);
});
}
void LuaInterpreter::register_collision(sol::table& omath_table)
{
auto collision_table = omath_table["collision"].get_or_create<sol::table>();
collision_table.new_usertype<Ray3f>(
"Ray",
sol::factories([]() { return Ray3f{}; },
[](const Vec3f& start, const Vec3f& end) { return Ray3f{start, end}; },
[](const Vec3f& start, const Vec3f& end, const bool infinite_length)
{ return Ray3f{start, end, infinite_length}; }),
"start", lua_field(&Ray3f::start), "end", lua_field(&Ray3f::end), "infinite_length",
lua_field(&Ray3f::infinite_length),
"direction_vector", &Ray3f::direction_vector, "direction_vector_normalized",
&Ray3f::direction_vector_normalized);
collision_table.new_usertype<LuaConvexCollider>(
"ConvexCollider",
sol::factories([](const sol::table& vertices) { return LuaConvexCollider(vec3_table_to_vector(vertices)); },
[](const sol::table& vertices, const Vec3f& origin)
{ return LuaConvexCollider(vec3_table_to_vector(vertices), origin); }),
"find_abs_furthest_vertex_position", &LuaConvexCollider::find_abs_furthest_vertex_position,
"get_origin", &LuaConvexCollider::get_origin, "set_origin", &LuaConvexCollider::set_origin,
"vertex_count", &LuaConvexCollider::vertex_count,
"vertices",
[](const LuaConvexCollider& collider, sol::this_state state)
{
return vec3_array_to_table(collider.vertices(), state);
});
collision_table.new_usertype<LineTracer3f>(
"LineTracer", sol::no_constructor, "can_trace_line", &LineTracer3f::can_trace_line,
"get_ray_hit_point",
sol::overload(
[](const Ray3f& ray, const Triangle3f& triangle)
{
return LineTracer3f::get_ray_hit_point(ray, triangle);
},
[](const Ray3f& ray, const Aabbf& aabb)
{
return LineTracer3f::get_ray_hit_point(ray, aabb);
},
[](const Ray3f& ray, const Obbf& obb)
{
return LineTracer3f::get_ray_hit_point(ray, obb);
}),
"ray_hits_triangle", &ray_hits_triangle, "ray_hits_aabb", &ray_hits_aabb, "ray_hits_obb",
&ray_hits_obb);
collision_table["gjk_support_vertex"] =
[](const LuaConvexCollider& collider_a, const LuaConvexCollider& collider_b, const Vec3f& direction)
{ return omath::collision::GjkAlgorithm<LuaConvexCollider>::find_support_vertex(collider_a, collider_b, direction); };
collision_table["gjk_collide"] = [](const LuaConvexCollider& collider_a, const LuaConvexCollider& collider_b)
{ return omath::collision::GjkAlgorithm<LuaConvexCollider>::is_collide(collider_a, collider_b); };
collision_table["epa_solve"] = [](const LuaConvexCollider& collider_a, const LuaConvexCollider& collider_b,
sol::this_state state) -> sol::object
{
using Gjk = omath::collision::GjkAlgorithm<LuaConvexCollider>;
using Epa = omath::collision::Epa<LuaConvexCollider>;
sol::state_view lua(state);
const auto gjk = Gjk::is_collide_with_simplex_info(collider_a, collider_b);
if (!gjk.hit)
return sol::nil;
const auto epa = Epa::solve(collider_a, collider_b, gjk.simplex);
if (!epa)
return sol::nil;
sol::table result = lua.create_table();
result["normal"] = epa->normal;
result["penetration_vector"] = epa->penetration_vector;
result["depth"] = epa->depth;
result["iterations"] = epa->iterations;
result["num_vertices"] = epa->num_vertices;
result["num_faces"] = epa->num_faces;
return sol::make_object(lua, result);
};
}
} // namespace omath::lua
#endif
+57 -27
View File
@@ -14,6 +14,8 @@
#include <omath/engines/unreal_engine/camera.hpp>
#include <sol/sol.hpp>
#include <string_view>
#include <type_traits>
#include <utility>
namespace
{
@@ -85,44 +87,72 @@ namespace
using PitchAngle = typename EngineTraits::PitchAngle;
using ViewAngles = typename EngineTraits::ViewAngles;
using Camera = typename EngineTraits::Camera;
using Mat4X4 = std::remove_cvref_t<decltype(std::declval<const Camera&>().get_view_matrix())>;
auto engine_table = omath_table[subtable_name].get_or_create<sol::table>();
auto types = omath_table["_types"].get<sol::table>();
set_engine_aliases<PitchAngle, ViewAngles>(engine_table, types);
engine_table.new_usertype<Camera>(
auto camera_type = engine_table.new_usertype<Camera>(
"Camera",
sol::constructors<Camera(const omath::Vector3<ArithmeticType>&, const ViewAngles&,
const omath::projection::ViewPort&, const omath::projection::FieldOfView&,
ArithmeticType, ArithmeticType)>(),
"look_at", &Camera::look_at, "get_forward", &Camera::get_forward, "get_right", &Camera::get_right,
"get_up", &Camera::get_up, "get_origin", &Camera::get_origin, "get_view_angles",
&Camera::get_view_angles, "get_near_plane", &Camera::get_near_plane, "get_far_plane",
&Camera::get_far_plane, "get_field_of_view", &Camera::get_field_of_view, "set_origin",
&Camera::set_origin, "set_view_angles", &Camera::set_view_angles, "set_view_port",
&Camera::set_view_port, "set_field_of_view", &Camera::set_field_of_view, "set_near_plane",
&Camera::set_near_plane, "set_far_plane", &Camera::set_far_plane,
ArithmeticType, ArithmeticType)>());
"world_to_screen",
[](const Camera& cam, const omath::Vector3<ArithmeticType>& pos)
-> std::tuple<sol::optional<omath::Vector3<ArithmeticType>>, sol::optional<std::string>>
{
auto result = cam.world_to_screen(pos);
if (result)
return {*result, sol::nullopt};
return {sol::nullopt, projection_error_to_string(result.error())};
},
camera_type["look_at"] = &Camera::look_at;
camera_type["get_forward"] = &Camera::get_forward;
camera_type["get_right"] = &Camera::get_right;
camera_type["get_up"] = &Camera::get_up;
camera_type["get_origin"] = &Camera::get_origin;
camera_type["get_view_angles"] = &Camera::get_view_angles;
camera_type["get_near_plane"] = &Camera::get_near_plane;
camera_type["get_far_plane"] = &Camera::get_far_plane;
camera_type["get_field_of_view"] = &Camera::get_field_of_view;
camera_type["set_origin"] = &Camera::set_origin;
camera_type["set_view_angles"] = &Camera::set_view_angles;
camera_type["set_view_port"] = &Camera::set_view_port;
camera_type["set_field_of_view"] = &Camera::set_field_of_view;
camera_type["set_near_plane"] = &Camera::set_near_plane;
camera_type["set_far_plane"] = &Camera::set_far_plane;
"screen_to_world",
[](const Camera& cam, const omath::Vector3<ArithmeticType>& pos)
-> std::tuple<sol::optional<omath::Vector3<ArithmeticType>>, sol::optional<std::string>>
{
auto result = cam.screen_to_world(pos);
if (result)
return {*result, sol::nullopt};
return {sol::nullopt, projection_error_to_string(result.error())};
});
camera_type["get_view_matrix"] = [](const Camera& cam) -> Mat4X4
{
return cam.get_view_matrix();
};
camera_type["get_projection_matrix"] = [](const Camera& cam) -> Mat4X4
{
return cam.get_projection_matrix();
};
camera_type["get_view_projection_matrix"] = [](const Camera& cam) -> Mat4X4
{
return cam.get_view_projection_matrix();
};
camera_type["extract_projection_params"] = [](const Mat4X4& projection_matrix)
{
const auto params = Camera::extract_projection_params(projection_matrix);
return std::make_tuple(params.fov, params.aspect_ratio);
};
camera_type["calc_view_angles_from_view_matrix"] = &Camera::calc_view_angles_from_view_matrix;
camera_type["calc_origin_from_view_matrix"] = &Camera::calc_origin_from_view_matrix;
camera_type["world_to_screen"] = [](const Camera& cam, const omath::Vector3<ArithmeticType>& pos)
-> std::tuple<sol::optional<omath::Vector3<ArithmeticType>>, sol::optional<std::string>>
{
auto result = cam.world_to_screen(pos);
if (result)
return {*result, sol::nullopt};
return {sol::nullopt, projection_error_to_string(result.error())};
};
camera_type["screen_to_world"] = [](const Camera& cam, const omath::Vector3<ArithmeticType>& pos)
-> std::tuple<sol::optional<omath::Vector3<ArithmeticType>>, sol::optional<std::string>>
{
auto result = cam.screen_to_world(pos);
if (result)
return {*result, sol::nullopt};
return {sol::nullopt, projection_error_to_string(result.error())};
};
}
// ---- Engine trait structs -----------------------------------------------
+519
View File
@@ -0,0 +1,519 @@
//
// Created by orange on 07.03.2026.
//
#ifdef OMATH_ENABLE_LUA
#include "omath/lua/lua.hpp"
#include <omath/hud/canvas_box.hpp>
#include <omath/hud/entity_overlay.hpp>
#include <sol/sol.hpp>
#include <any>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>
namespace
{
[[noreturn]]
void throw_lua_error(sol::protected_function_result& result)
{
sol::error err = result;
throw err;
}
class LuaHudRenderer final : public omath::hud::HudRendererInterface
{
public:
explicit LuaHudRenderer(sol::table callbacks): m_callbacks(std::move(callbacks))
{
}
void add_line(const omath::Vector2<float>& line_start, const omath::Vector2<float>& line_end,
const omath::Color& color, const float thickness) override
{
call_optional("add_line", line_start, line_end, color, thickness);
}
void add_polyline(const std::span<const omath::Vector2<float>>& vertexes, const omath::Color& color,
const float thickness) override
{
call_optional("add_polyline", make_points_table(vertexes), color, thickness);
}
void add_filled_polyline(const std::span<const omath::Vector2<float>>& vertexes,
const omath::Color& color) override
{
call_optional("add_filled_polyline", make_points_table(vertexes), color);
}
void add_rectangle(const omath::Vector2<float>& min, const omath::Vector2<float>& max,
const omath::Color& color) override
{
call_optional("add_rectangle", min, max, color);
}
void add_filled_rectangle(const omath::Vector2<float>& min, const omath::Vector2<float>& max,
const omath::Color& color) override
{
call_optional("add_filled_rectangle", min, max, color);
}
void add_circle(const omath::Vector2<float>& center, const float radius, const omath::Color& color,
const float thickness, const int segments) override
{
call_optional("add_circle", center, radius, color, thickness, segments);
}
void add_filled_circle(const omath::Vector2<float>& center, const float radius, const omath::Color& color,
const int segments) override
{
call_optional("add_filled_circle", center, radius, color, segments);
}
void add_arc(const omath::Vector2<float>& center, const float radius, const float a_min, const float a_max,
const omath::Color& color, const float thickness, const int segments) override
{
call_optional("add_arc", center, radius, a_min, a_max, color, thickness, segments);
}
void add_image(const std::any& texture_id, const omath::Vector2<float>& min, const omath::Vector2<float>& max,
const omath::Color& tint) override
{
const auto callback = callback_for("add_image");
if (!callback)
return;
sol::object texture = sol::nil;
if (const auto lua_object = std::any_cast<sol::object>(&texture_id))
texture = *lua_object;
auto result = (*callback)(texture, min, max, tint);
if (!result.valid())
throw_lua_error(result);
}
void add_text(const omath::Vector2<float>& position, const omath::Color& color,
const std::string_view& text) override
{
call_optional("add_text", position, color, std::string{text});
}
[[nodiscard]]
omath::Vector2<float> calc_text_size(const std::string_view& text) override
{
const auto callback = callback_for("calc_text_size");
if (!callback)
return {};
auto result = (*callback)(std::string{text});
if (!result.valid())
throw_lua_error(result);
return result.get<omath::Vector2<float>>();
}
private:
sol::main_table m_callbacks;
[[nodiscard]]
sol::optional<sol::protected_function> callback_for(const char* name) const
{
const sol::object callback = m_callbacks[name];
if (!callback.valid() || callback == sol::nil)
return sol::nullopt;
return callback.as<sol::protected_function>();
}
template<class... Args>
void call_optional(const char* name, Args&&... args) const
{
const auto callback = callback_for(name);
if (!callback)
return;
auto result = (*callback)(std::forward<Args>(args)...);
if (!result.valid())
throw_lua_error(result);
}
[[nodiscard]]
sol::table make_points_table(const std::span<const omath::Vector2<float>>& vertexes) const
{
sol::state_view lua(m_callbacks.lua_state());
sol::table points = lua.create_table(static_cast<int>(vertexes.size()), 0);
for (std::size_t i = 0; i < vertexes.size(); ++i)
points[i + 1] = vertexes[i];
return points;
}
};
[[nodiscard]]
omath::Color transparent_black()
{
return {0.f, 0.f, 0.f, 0.f};
}
[[nodiscard]]
omath::Color opaque_white()
{
return {1.f, 1.f, 1.f, 1.f};
}
[[nodiscard]]
omath::hud::EntityOverlay make_overlay(const omath::Vector2<float>& top, const omath::Vector2<float>& bottom,
const std::shared_ptr<LuaHudRenderer>& renderer)
{
if (!renderer)
throw std::invalid_argument("hud renderer must not be nil");
return {top, bottom, renderer};
}
[[nodiscard]]
std::any texture_id_from_lua_object(const sol::object& texture_id)
{
return texture_id;
}
std::vector<omath::Vector2<float>> points_from_table(const sol::table& points)
{
std::vector<omath::Vector2<float>> result;
for (std::size_t i = 1;; ++i)
{
const auto point = points[i].get<sol::optional<omath::Vector2<float>>>();
if (!point)
break;
result.push_back(*point);
}
return result;
}
template<class Object, class Value>
auto lua_field(Value Object::* member)
{
return sol::property(
[member](const Object& object) -> const Value&
{
return object.*member;
},
[member](Object& object, const Value& value)
{
object.*member = value;
});
}
} // namespace
namespace omath::lua
{
void LuaInterpreter::register_hud(sol::table& omath_table)
{
auto hud_table = omath_table["hud"].get_or_create<sol::table>();
hud_table.new_usertype<omath::hud::CanvasBox>(
"CanvasBox",
sol::factories([](const omath::Vector2<float>& top, const omath::Vector2<float>& bottom)
{ return omath::hud::CanvasBox(top, bottom); },
[](const omath::Vector2<float>& top, const omath::Vector2<float>& bottom,
const float ratio) { return omath::hud::CanvasBox(top, bottom, ratio); }),
"top_left_corner", lua_field(&omath::hud::CanvasBox::top_left_corner), "top_right_corner",
lua_field(&omath::hud::CanvasBox::top_right_corner), "bottom_left_corner",
lua_field(&omath::hud::CanvasBox::bottom_left_corner), "bottom_right_corner",
lua_field(&omath::hud::CanvasBox::bottom_right_corner),
"as_table",
[](const omath::hud::CanvasBox& box, sol::this_state s) -> sol::table
{
sol::state_view lua(s);
sol::table t = lua.create_table(4, 0);
const auto points = box.as_array();
for (std::size_t i = 0; i < points.size(); ++i)
t[i + 1] = points[i];
return t;
});
hud_table.new_usertype<LuaHudRenderer>(
"Renderer",
sol::factories([](const sol::table& callbacks)
{ return std::make_shared<LuaHudRenderer>(callbacks); }),
"add_line", &LuaHudRenderer::add_line,
"add_polyline",
[](LuaHudRenderer& renderer, const sol::table& points, const omath::Color& color,
const float thickness)
{
const auto vertices = points_from_table(points);
renderer.add_polyline({vertices.data(), vertices.size()}, color, thickness);
},
"add_filled_polyline",
[](LuaHudRenderer& renderer, const sol::table& points, const omath::Color& color)
{
const auto vertices = points_from_table(points);
renderer.add_filled_polyline({vertices.data(), vertices.size()}, color);
},
"add_rectangle", &LuaHudRenderer::add_rectangle, "add_filled_rectangle",
&LuaHudRenderer::add_filled_rectangle, "add_circle",
[](LuaHudRenderer& renderer, const omath::Vector2<float>& center, const float radius,
const omath::Color& color, const float thickness, sol::optional<int> segments)
{
renderer.add_circle(center, radius, color, thickness, segments.value_or(0));
},
"add_filled_circle",
[](LuaHudRenderer& renderer, const omath::Vector2<float>& center, const float radius,
const omath::Color& color, sol::optional<int> segments)
{
renderer.add_filled_circle(center, radius, color, segments.value_or(0));
},
"add_arc",
[](LuaHudRenderer& renderer, const omath::Vector2<float>& center, const float radius,
const float a_min, const float a_max, const omath::Color& color, const float thickness,
sol::optional<int> segments)
{
renderer.add_arc(center, radius, a_min, a_max, color, thickness, segments.value_or(0));
},
"add_image",
[](LuaHudRenderer& renderer, const sol::object& texture_id, const omath::Vector2<float>& min,
const omath::Vector2<float>& max, sol::optional<omath::Color> tint)
{
renderer.add_image(texture_id_from_lua_object(texture_id), min, max,
tint.value_or(opaque_white()));
},
"add_text", &LuaHudRenderer::add_text, "calc_text_size", &LuaHudRenderer::calc_text_size);
hud_table.new_usertype<omath::hud::EntityOverlay>(
"EntityOverlay",
sol::factories(&make_overlay),
"add_2d_box",
[](omath::hud::EntityOverlay& overlay, const omath::Color& box_color,
sol::optional<omath::Color> fill_color, sol::optional<omath::Color> outline_color,
sol::optional<float> thickness) -> omath::hud::EntityOverlay&
{
return overlay.add_2d_box(box_color, fill_color.value_or(transparent_black()),
outline_color.value_or(transparent_black()), thickness.value_or(1.f));
},
"add_cornered_2d_box",
[](omath::hud::EntityOverlay& overlay, const omath::Color& box_color,
sol::optional<omath::Color> fill_color, sol::optional<omath::Color> outline_color,
sol::optional<float> corner_ratio_len,
sol::optional<float> thickness) -> omath::hud::EntityOverlay&
{
return overlay.add_cornered_2d_box(box_color, fill_color.value_or(transparent_black()),
outline_color.value_or(transparent_black()),
corner_ratio_len.value_or(0.2f), thickness.value_or(1.f));
},
"add_dashed_box",
[](omath::hud::EntityOverlay& overlay, const omath::Color& color, sol::optional<float> dash_len,
sol::optional<float> gap_len, sol::optional<float> thickness) -> omath::hud::EntityOverlay&
{
return overlay.add_dashed_box(color, dash_len.value_or(8.f), gap_len.value_or(5.f),
thickness.value_or(1.f));
},
"add_right_bar",
[](omath::hud::EntityOverlay& overlay, const omath::Color& color, const omath::Color& outline_color,
const omath::Color& bg_color, const float width, const float ratio,
sol::optional<float> offset) -> omath::hud::EntityOverlay&
{
return overlay.add_right_bar(color, outline_color, bg_color, width, ratio, offset.value_or(5.f));
},
"add_left_bar",
[](omath::hud::EntityOverlay& overlay, const omath::Color& color, const omath::Color& outline_color,
const omath::Color& bg_color, const float width, const float ratio,
sol::optional<float> offset) -> omath::hud::EntityOverlay&
{
return overlay.add_left_bar(color, outline_color, bg_color, width, ratio, offset.value_or(5.f));
},
"add_top_bar",
[](omath::hud::EntityOverlay& overlay, const omath::Color& color, const omath::Color& outline_color,
const omath::Color& bg_color, const float height, const float ratio,
sol::optional<float> offset) -> omath::hud::EntityOverlay&
{
return overlay.add_top_bar(color, outline_color, bg_color, height, ratio, offset.value_or(5.f));
},
"add_bottom_bar",
[](omath::hud::EntityOverlay& overlay, const omath::Color& color, const omath::Color& outline_color,
const omath::Color& bg_color, const float height, const float ratio,
sol::optional<float> offset) -> omath::hud::EntityOverlay&
{
return overlay.add_bottom_bar(color, outline_color, bg_color, height, ratio, offset.value_or(5.f));
},
"add_right_dashed_bar",
[](omath::hud::EntityOverlay& overlay, const omath::Color& color, const omath::Color& outline_color,
const omath::Color& bg_color, const float width, const float ratio, const float dash_len,
const float gap_len, sol::optional<float> offset) -> omath::hud::EntityOverlay&
{
return overlay.add_right_dashed_bar(color, outline_color, bg_color, width, ratio, dash_len, gap_len,
offset.value_or(5.f));
},
"add_left_dashed_bar",
[](omath::hud::EntityOverlay& overlay, const omath::Color& color, const omath::Color& outline_color,
const omath::Color& bg_color, const float width, const float ratio, const float dash_len,
const float gap_len, sol::optional<float> offset) -> omath::hud::EntityOverlay&
{
return overlay.add_left_dashed_bar(color, outline_color, bg_color, width, ratio, dash_len, gap_len,
offset.value_or(5.f));
},
"add_top_dashed_bar",
[](omath::hud::EntityOverlay& overlay, const omath::Color& color, const omath::Color& outline_color,
const omath::Color& bg_color, const float height, const float ratio, const float dash_len,
const float gap_len, sol::optional<float> offset) -> omath::hud::EntityOverlay&
{
return overlay.add_top_dashed_bar(color, outline_color, bg_color, height, ratio, dash_len, gap_len,
offset.value_or(5.f));
},
"add_bottom_dashed_bar",
[](omath::hud::EntityOverlay& overlay, const omath::Color& color, const omath::Color& outline_color,
const omath::Color& bg_color, const float height, const float ratio, const float dash_len,
const float gap_len, sol::optional<float> offset) -> omath::hud::EntityOverlay&
{
return overlay.add_bottom_dashed_bar(color, outline_color, bg_color, height, ratio, dash_len,
gap_len, offset.value_or(5.f));
},
"add_right_label",
[](omath::hud::EntityOverlay& overlay, const omath::Color& color, const float offset,
const bool outlined, const std::string& text) -> omath::hud::EntityOverlay&
{
return overlay.add_right_label(color, offset, outlined, text);
},
"add_left_label",
[](omath::hud::EntityOverlay& overlay, const omath::Color& color, const float offset,
const bool outlined, const std::string& text) -> omath::hud::EntityOverlay&
{
return overlay.add_left_label(color, offset, outlined, text);
},
"add_top_label",
[](omath::hud::EntityOverlay& overlay, const omath::Color& color, const float offset,
const bool outlined, const std::string& text) -> omath::hud::EntityOverlay&
{
return overlay.add_top_label(color, offset, outlined, text);
},
"add_bottom_label",
[](omath::hud::EntityOverlay& overlay, const omath::Color& color, const float offset,
const bool outlined, const std::string& text) -> omath::hud::EntityOverlay&
{
return overlay.add_bottom_label(color, offset, outlined, text);
},
"add_centered_top_label",
[](omath::hud::EntityOverlay& overlay, const omath::Color& color, const float offset,
const bool outlined, const std::string& text) -> omath::hud::EntityOverlay&
{
return overlay.add_centered_top_label(color, offset, outlined, text);
},
"add_centered_bottom_label",
[](omath::hud::EntityOverlay& overlay, const omath::Color& color, const float offset,
const bool outlined, const std::string& text) -> omath::hud::EntityOverlay&
{
return overlay.add_centered_bottom_label(color, offset, outlined, text);
},
"add_right_space_vertical", &omath::hud::EntityOverlay::add_right_space_vertical,
"add_right_space_horizontal", &omath::hud::EntityOverlay::add_right_space_horizontal,
"add_left_space_vertical", &omath::hud::EntityOverlay::add_left_space_vertical,
"add_left_space_horizontal", &omath::hud::EntityOverlay::add_left_space_horizontal,
"add_top_space_vertical", &omath::hud::EntityOverlay::add_top_space_vertical,
"add_top_space_horizontal", &omath::hud::EntityOverlay::add_top_space_horizontal,
"add_bottom_space_vertical", &omath::hud::EntityOverlay::add_bottom_space_vertical,
"add_bottom_space_horizontal", &omath::hud::EntityOverlay::add_bottom_space_horizontal,
"add_right_progress_ring",
[](omath::hud::EntityOverlay& overlay, const omath::Color& color, const omath::Color& bg,
const float radius, const float ratio, sol::optional<float> thickness, sol::optional<float> offset,
sol::optional<int> segments) -> omath::hud::EntityOverlay&
{
return overlay.add_right_progress_ring(color, bg, radius, ratio, thickness.value_or(2.f),
offset.value_or(5.f), segments.value_or(0));
},
"add_left_progress_ring",
[](omath::hud::EntityOverlay& overlay, const omath::Color& color, const omath::Color& bg,
const float radius, const float ratio, sol::optional<float> thickness, sol::optional<float> offset,
sol::optional<int> segments) -> omath::hud::EntityOverlay&
{
return overlay.add_left_progress_ring(color, bg, radius, ratio, thickness.value_or(2.f),
offset.value_or(5.f), segments.value_or(0));
},
"add_top_progress_ring",
[](omath::hud::EntityOverlay& overlay, const omath::Color& color, const omath::Color& bg,
const float radius, const float ratio, sol::optional<float> thickness, sol::optional<float> offset,
sol::optional<int> segments) -> omath::hud::EntityOverlay&
{
return overlay.add_top_progress_ring(color, bg, radius, ratio, thickness.value_or(2.f),
offset.value_or(5.f), segments.value_or(0));
},
"add_bottom_progress_ring",
[](omath::hud::EntityOverlay& overlay, const omath::Color& color, const omath::Color& bg,
const float radius, const float ratio, sol::optional<float> thickness, sol::optional<float> offset,
sol::optional<int> segments) -> omath::hud::EntityOverlay&
{
return overlay.add_bottom_progress_ring(color, bg, radius, ratio, thickness.value_or(2.f),
offset.value_or(5.f), segments.value_or(0));
},
"add_right_icon",
[](omath::hud::EntityOverlay& overlay, const sol::object& texture_id, const float width,
const float height, sol::optional<omath::Color> tint,
sol::optional<float> offset) -> omath::hud::EntityOverlay&
{
return overlay.add_right_icon(texture_id_from_lua_object(texture_id), width, height,
tint.value_or(opaque_white()), offset.value_or(5.f));
},
"add_left_icon",
[](omath::hud::EntityOverlay& overlay, const sol::object& texture_id, const float width,
const float height, sol::optional<omath::Color> tint,
sol::optional<float> offset) -> omath::hud::EntityOverlay&
{
return overlay.add_left_icon(texture_id_from_lua_object(texture_id), width, height,
tint.value_or(opaque_white()), offset.value_or(5.f));
},
"add_top_icon",
[](omath::hud::EntityOverlay& overlay, const sol::object& texture_id, const float width,
const float height, sol::optional<omath::Color> tint,
sol::optional<float> offset) -> omath::hud::EntityOverlay&
{
return overlay.add_top_icon(texture_id_from_lua_object(texture_id), width, height,
tint.value_or(opaque_white()), offset.value_or(5.f));
},
"add_bottom_icon",
[](omath::hud::EntityOverlay& overlay, const sol::object& texture_id, const float width,
const float height, sol::optional<omath::Color> tint,
sol::optional<float> offset) -> omath::hud::EntityOverlay&
{
return overlay.add_bottom_icon(texture_id_from_lua_object(texture_id), width, height,
tint.value_or(opaque_white()), offset.value_or(5.f));
},
"add_snap_line", &omath::hud::EntityOverlay::add_snap_line, "add_skeleton",
[](omath::hud::EntityOverlay& overlay, const omath::Color& color,
sol::optional<float> thickness) -> omath::hud::EntityOverlay&
{
return overlay.add_skeleton(color, thickness.value_or(1.f));
},
"add_scan_marker",
[](omath::hud::EntityOverlay& overlay, const omath::Color& color,
sol::optional<omath::Color> outline, sol::optional<float> outline_thickness)
-> omath::hud::EntityOverlay&
{
return overlay.contents(omath::hud::widget::ScanMarker{
color, outline.value_or(transparent_black()), outline_thickness.value_or(1.f)});
},
"add_aim_dot",
[](omath::hud::EntityOverlay& overlay, const omath::Vector2<float>& position,
const omath::Color& color, sol::optional<float> radius) -> omath::hud::EntityOverlay&
{
return overlay.contents(omath::hud::widget::AimDot{position, color, radius.value_or(3.f)});
},
"add_projectile_aim",
[](omath::hud::EntityOverlay& overlay, const omath::Vector2<float>& position,
const omath::Color& color, sol::optional<float> size, sol::optional<float> line_size,
sol::optional<omath::hud::widget::ProjectileAim::Figure> figure) -> omath::hud::EntityOverlay&
{
return overlay.contents(omath::hud::widget::ProjectileAim{
position, color, size.value_or(3.f), line_size.value_or(1.f),
figure.value_or(omath::hud::widget::ProjectileAim::Figure::SQUARE)});
});
hud_table.new_enum("ProjectileAimFigure", "CIRCLE", omath::hud::widget::ProjectileAim::Figure::CIRCLE,
"SQUARE", omath::hud::widget::ProjectileAim::Figure::SQUARE);
}
} // namespace omath::lua
#endif
+156
View File
@@ -0,0 +1,156 @@
//
// Created by orange on 07.03.2026.
//
#ifdef OMATH_ENABLE_LUA
#include "omath/lua/lua.hpp"
#include <omath/linear_algebra/mat.hpp>
#include <sol/sol.hpp>
#include <stdexcept>
namespace
{
template<std::size_t Limit>
std::size_t checked_index(const int index)
{
if (index < 0 || index >= static_cast<int>(Limit))
throw std::out_of_range("matrix index is out of range");
return static_cast<std::size_t>(index);
}
template<std::size_t Rows, std::size_t Columns, class Type, omath::MatStoreType StoreType>
omath::Mat<Rows, Columns, Type, StoreType> mat_from_rows(const sol::table& rows)
{
omath::Mat<Rows, Columns, Type, StoreType> result;
for (std::size_t row = 0; row < Rows; ++row)
{
const auto row_table = rows[row + 1].get<sol::optional<sol::table>>();
if (!row_table)
throw std::invalid_argument("matrix rows must be tables");
for (std::size_t column = 0; column < Columns; ++column)
{
const auto value = (*row_table)[column + 1].get<sol::optional<Type>>();
if (!value)
throw std::invalid_argument("matrix row has missing value");
result.at(row, column) = *value;
}
}
return result;
}
template<std::size_t Rows, std::size_t Columns, class Type, omath::MatStoreType StoreType>
sol::table mat_as_table(const omath::Mat<Rows, Columns, Type, StoreType>& mat, sol::this_state state)
{
sol::state_view lua(state);
sol::table rows = lua.create_table();
for (std::size_t row = 0; row < Rows; ++row)
{
sol::table row_table = lua.create_table();
for (std::size_t column = 0; column < Columns; ++column)
row_table[column + 1] = mat.at(row, column);
rows[row + 1] = row_table;
}
return rows;
}
template<std::size_t Size, class Type, omath::MatStoreType StoreType, bool RegisterMat4Helpers = false>
void register_square_mat(sol::table& omath_table, const char* name)
{
using MatType = omath::Mat<Size, Size, Type, StoreType>;
auto type = omath_table.new_usertype<MatType>(
name, sol::constructors<MatType()>(),
"from_rows", &mat_from_rows<Size, Size, Type, StoreType>, "row_count",
[]()
{
return Size;
},
"columns_count",
[]()
{
return Size;
},
"at",
[](const MatType& mat, const int row, const int column)
{
return mat.at(checked_index<Size>(row), checked_index<Size>(column));
},
"set_at",
[](MatType& mat, const int row, const int column, const Type value)
{
mat.at(checked_index<Size>(row), checked_index<Size>(column)) = value;
return mat;
},
sol::meta_function::multiplication,
sol::overload(
[](const MatType& lhs, const MatType& rhs)
{
return lhs * rhs;
},
[](const MatType& mat, const Type scalar)
{
return mat * scalar;
},
[](const Type scalar, const MatType& mat)
{
return mat * scalar;
}),
sol::meta_function::division,
[](const MatType& mat, const Type scalar)
{
return mat / scalar;
},
sol::meta_function::equal_to, &MatType::operator==, sol::meta_function::to_string, &MatType::to_string,
"transposed", &MatType::transposed, "determinant", &MatType::determinant, "sum", &MatType::sum, "clear",
&MatType::clear, "set", &MatType::set, "inverted",
[](const MatType& mat) -> sol::optional<MatType>
{
auto result = mat.inverted();
if (!result)
return sol::nullopt;
return *result;
},
"as_table", &mat_as_table<Size, Size, Type, StoreType>);
if constexpr (RegisterMat4Helpers)
{
type["to_screen_mat"] = [](const Type screen_width, const Type screen_height)
{
return MatType{
{screen_width / Type{2}, Type{0}, Type{0}, Type{0}},
{Type{0}, -screen_height / Type{2}, Type{0}, Type{0}},
{Type{0}, Type{0}, Type{1}, Type{0}},
{screen_width / Type{2}, screen_height / Type{2}, Type{0}, Type{1}},
};
};
type["translation"] = &omath::mat_translation<Type, StoreType>;
type["scale"] = &omath::mat_scale<Type, StoreType>;
type["look_at_left_handed"] = &omath::mat_look_at_left_handed<Type, StoreType>;
type["look_at_right_handed"] = &omath::mat_look_at_right_handed<Type, StoreType>;
type["perspective_left_handed_vertical_fov"] =
&omath::mat_perspective_left_handed_vertical_fov<Type, StoreType>;
type["perspective_right_handed_vertical_fov"] =
&omath::mat_perspective_right_handed_vertical_fov<Type, StoreType>;
}
}
} // namespace
namespace omath::lua
{
void LuaInterpreter::register_matrices(sol::table& omath_table)
{
register_square_mat<3, float, omath::MatStoreType::ROW_MAJOR>(omath_table, "Mat3");
register_square_mat<4, float, omath::MatStoreType::ROW_MAJOR, true>(omath_table, "Mat4");
register_square_mat<4, float, omath::MatStoreType::COLUMN_MAJOR, true>(omath_table, "Mat4ColumnMajor");
register_square_mat<4, double, omath::MatStoreType::ROW_MAJOR>(omath_table, "Mat4d");
}
} // namespace omath::lua
#endif
+95
View File
@@ -0,0 +1,95 @@
//
// Created by orange on 07.03.2026.
//
#ifdef OMATH_ENABLE_LUA
#include "omath/lua/lua.hpp"
#include <omath/linear_algebra/quaternion.hpp>
#include <sol/sol.hpp>
namespace omath::lua
{
void LuaInterpreter::register_quaternion(sol::table& omath_table)
{
using Quatf = omath::Quaternion<float>;
using Vec3f = omath::Vector3<float>;
omath_table.new_usertype<Quatf>(
"Quaternion", sol::constructors<Quatf(), Quatf(float, float, float, float)>(),
"from_axis_angle", &Quatf::from_axis_angle,
"x",
sol::property(
[](const Quatf& q)
{
return q.x;
},
[](Quatf& q, float val)
{
q.x = val;
}),
"y",
sol::property(
[](const Quatf& q)
{
return q.y;
},
[](Quatf& q, float val)
{
q.y = val;
}),
"z",
sol::property(
[](const Quatf& q)
{
return q.z;
},
[](Quatf& q, float val)
{
q.z = val;
}),
"w",
sol::property(
[](const Quatf& q)
{
return q.w;
},
[](Quatf& q, float val)
{
q.w = val;
}),
sol::meta_function::addition, sol::resolve<Quatf(const Quatf&) const>(&Quatf::operator+),
sol::meta_function::multiplication,
sol::overload(sol::resolve<Quatf(const Quatf&) const>(&Quatf::operator*),
sol::resolve<Quatf(const float&) const>(&Quatf::operator*),
[](float s, const Quatf& q)
{
return q * s;
}),
sol::meta_function::unary_minus, sol::resolve<Quatf() const>(&Quatf::operator-),
sol::meta_function::equal_to, &Quatf::operator==, sol::meta_function::to_string,
[](const Quatf& q)
{
return std::format("Quaternion({}, {}, {}, {})", q.x, q.y, q.z, q.w);
},
"conjugate", &Quatf::conjugate, "dot", &Quatf::dot, "length", &Quatf::length, "length_sqr",
&Quatf::length_sqr, "normalized", &Quatf::normalized, "inverse", &Quatf::inverse, "rotate",
sol::resolve<Vec3f(const Vec3f&) const>(&Quatf::rotate), "to_rotation_matrix3",
&Quatf::to_rotation_matrix3, "to_rotation_matrix4", &Quatf::to_rotation_matrix4,
"as_table",
[](const Quatf& q, sol::this_state s) -> sol::table
{
sol::state_view lua(s);
sol::table t = lua.create_table();
t["x"] = q.x;
t["y"] = q.y;
t["z"] = q.z;
t["w"] = q.w;
return t;
});
}
} // namespace omath::lua
#endif
+2 -2
View File
@@ -29,11 +29,11 @@ TEST(unit_test_cry_engine, look_at_right)
}
TEST(unit_test_cry_engine, look_at_up)
{
const auto angles = cry_engine::CameraTrait::calc_look_at_angle({}, cry_engine::k_abs_right);
const auto angles = cry_engine::CameraTrait::calc_look_at_angle({}, cry_engine::k_abs_up);
// ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = cry_engine::forward_vector(angles);
for (const auto& [result, etalon] : std::views::zip(dir_vector.as_array(), cry_engine::k_abs_right.as_array()))
for (const auto& [result, etalon] : std::views::zip(dir_vector.as_array(), cry_engine::k_abs_up.as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
@@ -0,0 +1,126 @@
#include <gtest/gtest.h>
#include <omath/engines/cry_engine/formulas.hpp>
#include <omath/engines/frostbite_engine/formulas.hpp>
#include <omath/engines/iw_engine/formulas.hpp>
#include <omath/engines/opengl_engine/formulas.hpp>
#include <omath/engines/source_engine/formulas.hpp>
#include <omath/engines/unity_engine/formulas.hpp>
#include <omath/engines/unreal_engine/formulas.hpp>
template<class Type>
static void expect_vector_near(const omath::Vector3<Type>& actual, const omath::Vector3<Type>& expected,
const double epsilon)
{
EXPECT_NEAR(static_cast<double>(actual.x), static_cast<double>(expected.x), epsilon);
EXPECT_NEAR(static_cast<double>(actual.y), static_cast<double>(expected.y), epsilon);
EXPECT_NEAR(static_cast<double>(actual.z), static_cast<double>(expected.z), epsilon);
}
template<class Mat4X4, class ViewAnglesType, class RotationMatrixFn, class ExtractOriginFn, class ExtractScaleFn,
class ExtractRotationAnglesFn>
static void verify_transform_extractors(const omath::Vector3<typename Mat4X4::ContainedType>& origin,
const omath::Vector3<typename Mat4X4::ContainedType>& scale,
const ViewAnglesType& angles, RotationMatrixFn rotation_matrix,
ExtractOriginFn extract_origin, ExtractScaleFn extract_scale,
ExtractRotationAnglesFn extract_rotation_angles)
{
using Type = typename Mat4X4::ContainedType;
constexpr auto store_ordering = Mat4X4::get_store_ordering();
const auto transform = omath::mat_translation<Type, store_ordering>(origin) * rotation_matrix(angles)
* omath::mat_scale<Type, store_ordering>(scale);
expect_vector_near(extract_origin(transform), origin, 1e-4);
expect_vector_near(extract_scale(transform), scale, 1e-4);
expect_vector_near(extract_rotation_angles(transform).as_vector3(), angles.as_vector3(), 1e-3);
}
TEST(TransformExtraction, SourceEngine)
{
namespace e = omath::source_engine;
const e::ViewAngles angles{
e::PitchAngle::from_degrees(20.f),
e::YawAngle::from_degrees(-35.f),
e::RollAngle::from_degrees(15.f),
};
verify_transform_extractors<e::Mat4X4>({12.f, -3.f, 8.f}, {2.f, 3.f, 4.f}, angles, e::rotation_matrix,
e::extract_origin, e::extract_scale, e::extract_rotation_angles);
}
TEST(TransformExtraction, IwEngine)
{
namespace e = omath::iw_engine;
const e::ViewAngles angles{
e::PitchAngle::from_degrees(-18.f),
e::YawAngle::from_degrees(42.f),
e::RollAngle::from_degrees(-11.f),
};
verify_transform_extractors<e::Mat4X4>({-7.f, 5.f, 13.f}, {1.5f, 2.5f, 3.5f}, angles, e::rotation_matrix,
e::extract_origin, e::extract_scale, e::extract_rotation_angles);
}
TEST(TransformExtraction, UnrealEngine)
{
namespace e = omath::unreal_engine;
const e::ViewAngles angles{
e::PitchAngle::from_degrees(30.0),
e::YawAngle::from_degrees(45.0),
e::RollAngle::from_degrees(-20.0),
};
verify_transform_extractors<e::Mat4X4>({100.0, -50.0, 25.0}, {2.0, 4.0, 6.0}, angles, e::rotation_matrix,
e::extract_origin, e::extract_scale, e::extract_rotation_angles);
}
TEST(TransformExtraction, UnityEngine)
{
namespace e = omath::unity_engine;
const e::ViewAngles angles{
e::PitchAngle::from_degrees(15.f),
e::YawAngle::from_degrees(-25.f),
e::RollAngle::from_degrees(35.f),
};
verify_transform_extractors<e::Mat4X4>({4.f, 9.f, -2.f}, {0.5f, 3.f, 7.f}, angles, e::rotation_matrix,
e::extract_origin, e::extract_scale, e::extract_rotation_angles);
}
TEST(TransformExtraction, FrostbiteEngine)
{
namespace e = omath::frostbite_engine;
const e::ViewAngles angles{
e::PitchAngle::from_degrees(-12.f),
e::YawAngle::from_degrees(33.f),
e::RollAngle::from_degrees(-27.f),
};
verify_transform_extractors<e::Mat4X4>({6.f, -8.f, 10.f}, {1.25f, 2.75f, 4.25f}, angles, e::rotation_matrix,
e::extract_origin, e::extract_scale, e::extract_rotation_angles);
}
TEST(TransformExtraction, CryEngine)
{
namespace e = omath::cry_engine;
const e::ViewAngles angles{
e::PitchAngle::from_degrees(-18.f),
e::YawAngle::from_degrees(40.f),
e::RollAngle::from_degrees(22.f),
};
verify_transform_extractors<e::Mat4X4>({3.f, 14.f, -6.f}, {2.f, 5.f, 8.f}, angles, e::rotation_matrix,
e::extract_origin, e::extract_scale, e::extract_rotation_angles);
}
TEST(TransformExtraction, OpenGlEngine)
{
namespace e = omath::opengl_engine;
const e::ViewAngles angles{
e::PitchAngle::from_degrees(24.f),
e::YawAngle::from_degrees(-31.f),
e::RollAngle::from_degrees(17.f),
};
verify_transform_extractors<e::Mat4X4>({-9.f, 2.f, 11.f}, {3.f, 6.f, 9.f}, angles, e::rotation_matrix,
e::extract_origin, e::extract_scale, e::extract_rotation_angles);
}
+281
View File
@@ -0,0 +1,281 @@
//
// Created by Vladislav on 07.05.2026.
//
#include "omath/3d_primitives/obb.hpp"
#include "omath/collision/line_tracer.hpp"
#include <cmath>
#include <gtest/gtest.h>
#include <numbers>
using Vec3 = omath::Vector3<float>;
using Ray = omath::collision::Ray<>;
using LineTracer = omath::collision::LineTracer<>;
using OBB = omath::primitives::Obb<float>;
namespace
{
Ray make_ray(const Vec3 start, const Vec3 end, const bool infinite = false)
{
Ray r;
r.start = start;
r.end = end;
r.infinite_length = infinite;
return r;
}
constexpr OBB axis_aligned_obb(const Vec3& center, const Vec3& half_extents) noexcept
{
return OBB{center, {1.f, 0.f, 0.f}, {0.f, 1.f, 0.f}, {0.f, 0.f, 1.f}, half_extents};
}
OBB rotated_around_z(const Vec3& center, const Vec3& half_extents, const float radians) noexcept
{
const auto c = std::cos(radians);
const auto s = std::sin(radians);
return OBB{center, {c, s, 0.f}, {-s, c, 0.f}, {0.f, 0.f, 1.f}, half_extents};
}
OBB rotated_around_y(const Vec3& center, const Vec3& half_extents, const float radians) noexcept
{
const auto c = std::cos(radians);
const auto s = std::sin(radians);
return OBB{center, {c, 0.f, -s}, {0.f, 1.f, 0.f}, {s, 0.f, c}, half_extents};
}
} // namespace
// --- axis-aligned OBB behaves like AABB ---
TEST(LineTracerOBBTests, AxisAlignedHitAlongZ)
{
const auto box = axis_aligned_obb({0.f, 0.f, 0.f}, {1.f, 1.f, 1.f});
const auto ray = make_ray({0.f, 0.f, -5.f}, {0.f, 0.f, 5.f});
const auto hit = LineTracer::get_ray_hit_point(ray, box);
EXPECT_NE(hit, ray.end);
EXPECT_NEAR(hit.x, 0.f, 1e-4f);
EXPECT_NEAR(hit.y, 0.f, 1e-4f);
EXPECT_NEAR(hit.z, -1.f, 1e-4f);
}
TEST(LineTracerOBBTests, AxisAlignedHitAlongX)
{
const auto box = axis_aligned_obb({0.f, 0.f, 0.f}, {1.f, 1.f, 1.f});
const auto ray = make_ray({-5.f, 0.f, 0.f}, {5.f, 0.f, 0.f});
const auto hit = LineTracer::get_ray_hit_point(ray, box);
EXPECT_NE(hit, ray.end);
EXPECT_NEAR(hit.x, -1.f, 1e-4f);
}
TEST(LineTracerOBBTests, MissReturnsEnd)
{
const auto box = axis_aligned_obb({0.f, 0.f, 0.f}, {1.f, 1.f, 1.f});
const auto ray = make_ray({0.f, 5.f, -5.f}, {0.f, 5.f, 5.f});
const auto hit = LineTracer::get_ray_hit_point(ray, box);
EXPECT_EQ(hit, ray.end);
}
TEST(LineTracerOBBTests, RayTooShortReturnsEnd)
{
const auto box = axis_aligned_obb({4.f, 0.f, 0.f}, {1.f, 1.f, 1.f});
const auto ray = make_ray({0.f, 0.f, 0.f}, {2.f, 0.f, 0.f});
const auto hit = LineTracer::get_ray_hit_point(ray, box);
EXPECT_EQ(hit, ray.end);
}
TEST(LineTracerOBBTests, InfiniteRayHits)
{
const auto box = axis_aligned_obb({4.f, 0.f, 0.f}, {1.f, 1.f, 1.f});
const auto ray = make_ray({0.f, 0.f, 0.f}, {2.f, 0.f, 0.f}, true);
const auto hit = LineTracer::get_ray_hit_point(ray, box);
EXPECT_NE(hit, ray.end);
EXPECT_NEAR(hit.x, 3.f, 1e-4f);
}
TEST(LineTracerOBBTests, RayStartsInsideBox)
{
const auto box = axis_aligned_obb({0.f, 0.f, 0.f}, {1.f, 1.f, 1.f});
const auto ray = make_ray({0.f, 0.f, 0.f}, {5.f, 0.f, 0.f});
const auto hit = LineTracer::get_ray_hit_point(ray, box);
EXPECT_NE(hit, ray.end);
EXPECT_NEAR(hit.x, 0.f, 1e-4f);
EXPECT_NEAR(hit.y, 0.f, 1e-4f);
EXPECT_NEAR(hit.z, 0.f, 1e-4f);
}
TEST(LineTracerOBBTests, RayBehindBoxReturnsEnd)
{
const auto box = axis_aligned_obb({4.f, 0.f, 0.f}, {1.f, 1.f, 1.f});
const auto ray = make_ray({10.f, 0.f, 0.f}, {20.f, 0.f, 0.f});
const auto hit = LineTracer::get_ray_hit_point(ray, box);
EXPECT_EQ(hit, ray.end);
}
// --- rotated OBB ---
TEST(LineTracerOBBTests, RotatedBoxHitOnRotatedFace)
{
// Box centred at the origin, rotated 45° around Z. After rotation, the box's "near" face
// (originally x=-1) is now perpendicular to the (1, 1, 0)/√2 direction. A ray approaching
// from +X (along world -X) first hits the box at the rotated face — at x = √2 ≈ 1.414.
const auto box = rotated_around_z({0.f, 0.f, 0.f}, {1.f, 1.f, 1.f}, std::numbers::pi_v<float> / 4.f);
const auto ray = make_ray({5.f, 0.f, 0.f}, {-5.f, 0.f, 0.f});
const auto hit = LineTracer::get_ray_hit_point(ray, box);
EXPECT_NE(hit, ray.end);
EXPECT_NEAR(hit.x, std::numbers::sqrt2_v<float>, 1e-4f);
EXPECT_NEAR(hit.y, 0.f, 1e-4f);
EXPECT_NEAR(hit.z, 0.f, 1e-4f);
}
TEST(LineTracerOBBTests, RotatedAroundYBoxHitOnRotatedFace)
{
const auto box = rotated_around_y({0.f, 0.f, 0.f}, {1.f, 1.f, 1.f}, std::numbers::pi_v<float> / 4.f);
const auto ray = make_ray({0.f, 0.f, 5.f}, {0.f, 0.f, -5.f});
const auto hit = LineTracer::get_ray_hit_point(ray, box);
EXPECT_NE(hit, ray.end);
EXPECT_NEAR(hit.x, 0.f, 1e-4f);
EXPECT_NEAR(hit.y, 0.f, 1e-4f);
EXPECT_NEAR(hit.z, std::numbers::sqrt2_v<float>, 1e-4f);
}
TEST(LineTracerOBBTests, RotatedBoxMissesWhereAabbWouldHit)
{
// A unit cube rotated 45° around Z has an XY footprint that is a diamond reaching
// (±√2, 0) and (0, ±√2). The AABB envelope spans x,y ∈ [-√2, √2], but at y just below √2
// the diamond is essentially a point. A ray at y = 1.43 is outside the diamond entirely
// (|x| + |y| ≤ √2 ⇒ |x| ≤ √2 - 1.43 < 0), yet it would still pass through the AABB
// envelope of the rotated box.
const auto box = rotated_around_z({0.f, 0.f, 0.f}, {1.f, 1.f, 1.f}, std::numbers::pi_v<float> / 4.f);
const auto ray = make_ray({-5.f, 1.43f, 0.f}, {5.f, 1.43f, 0.f});
const auto hit = LineTracer::get_ray_hit_point(ray, box);
EXPECT_EQ(hit, ray.end);
}
TEST(LineTracerOBBTests, RotatedThinBoxHitFromTheSide)
{
// Long, thin axis-aligned slab along X, rotated 90° around Z so it now points along Y.
// A ray from +X straight back along -X must miss (the slab is thin in X), but a ray along
// -Y from +Y must hit.
const auto box = rotated_around_z({0.f, 0.f, 0.f}, {5.f, 0.5f, 1.f}, std::numbers::pi_v<float> / 2.f);
const auto ray_along_x = make_ray({10.f, 0.f, 0.f}, {-10.f, 0.f, 0.f});
const auto hit_x = LineTracer::get_ray_hit_point(ray_along_x, box);
EXPECT_NE(hit_x, ray_along_x.end);
EXPECT_NEAR(hit_x.x, 0.5f, 1e-4f); // hit on the rotated slab's narrow side
const auto ray_along_y = make_ray({0.f, 10.f, 0.f}, {0.f, -10.f, 0.f});
const auto hit_y = LineTracer::get_ray_hit_point(ray_along_y, box);
EXPECT_NE(hit_y, ray_along_y.end);
EXPECT_NEAR(hit_y.y, 5.f, 1e-4f); // hit on the long end at y=+5
}
TEST(LineTracerOBBTests, RotatedAndTranslatedBoxHit)
{
const auto box = rotated_around_z({10.f, 5.f, 0.f}, {1.f, 1.f, 1.f}, std::numbers::pi_v<float> / 4.f);
// Ray approaches the rotated box from +X.
const auto ray = make_ray({20.f, 5.f, 0.f}, {0.f, 5.f, 0.f});
const auto hit = LineTracer::get_ray_hit_point(ray, box);
EXPECT_NE(hit, ray.end);
EXPECT_NEAR(hit.x, 10.f + std::numbers::sqrt2_v<float>, 1e-4f);
EXPECT_NEAR(hit.y, 5.f, 1e-4f);
}
TEST(LineTracerOBBTests, RayStartsInsideRotatedBox)
{
const auto box = rotated_around_z({2.f, 3.f, 0.f}, {2.f, 1.f, 1.f}, std::numbers::pi_v<float> / 6.f);
const auto ray = make_ray({2.f, 3.f, 0.f}, {10.f, 3.f, 0.f});
const auto hit = LineTracer::get_ray_hit_point(ray, box);
EXPECT_NE(hit, ray.end);
EXPECT_NEAR(hit.x, 2.f, 1e-4f);
EXPECT_NEAR(hit.y, 3.f, 1e-4f);
EXPECT_NEAR(hit.z, 0.f, 1e-4f);
}
TEST(LineTracerOBBTests, TangentRayHitsRotatedBox)
{
const auto box = rotated_around_z({0.f, 0.f, 0.f}, {1.f, 1.f, 1.f}, std::numbers::pi_v<float> / 4.f);
const auto ray = make_ray({-5.f, std::numbers::sqrt2_v<float>, 0.f},
{5.f, std::numbers::sqrt2_v<float>, 0.f});
const auto hit = LineTracer::get_ray_hit_point(ray, box);
EXPECT_NE(hit, ray.end);
EXPECT_NEAR(hit.x, 0.f, 1e-4f);
EXPECT_NEAR(hit.y, std::numbers::sqrt2_v<float>, 1e-4f);
EXPECT_NEAR(hit.z, 0.f, 1e-4f);
}
TEST(LineTracerOBBTests, DegeneratePlaneBoxCanBeHit)
{
const auto box = axis_aligned_obb({0.f, 0.f, 0.f}, {1.f, 1.f, 0.f});
const auto ray = make_ray({0.f, 0.f, -5.f}, {0.f, 0.f, 5.f});
const auto hit = LineTracer::get_ray_hit_point(ray, box);
EXPECT_NE(hit, ray.end);
EXPECT_NEAR(hit.x, 0.f, 1e-4f);
EXPECT_NEAR(hit.y, 0.f, 1e-4f);
EXPECT_NEAR(hit.z, 0.f, 1e-4f);
}
TEST(LineTracerOBBTests, ParallelRayOutsideMisses)
{
// Ray runs parallel to a slab face, completely outside the slab.
const auto box = axis_aligned_obb({0.f, 0.f, 0.f}, {1.f, 1.f, 1.f});
const auto ray = make_ray({-5.f, 2.f, 0.f}, {5.f, 2.f, 0.f});
const auto hit = LineTracer::get_ray_hit_point(ray, box);
EXPECT_EQ(hit, ray.end);
}
TEST(LineTracerOBBTests, ParallelRayInsideHits)
{
// Ray runs parallel to a slab face but inside the slab — should still hit the entry plane.
const auto box = axis_aligned_obb({0.f, 0.f, 0.f}, {1.f, 1.f, 1.f});
const auto ray = make_ray({-5.f, 0.5f, 0.f}, {5.f, 0.5f, 0.f});
const auto hit = LineTracer::get_ray_hit_point(ray, box);
EXPECT_NE(hit, ray.end);
EXPECT_NEAR(hit.x, -1.f, 1e-4f);
}
TEST(LineTracerOBBTests, MatchesAabbForAxisAlignedBox)
{
using AABB = omath::primitives::Aabb<float>;
struct
{
Vec3 center;
Vec3 half;
Vec3 ray_start;
Vec3 ray_end;
} cases[] = {
{{0.f, 0.f, 0.f}, {1.f, 1.f, 1.f}, {-5.f, 0.f, 0.f}, {5.f, 0.f, 0.f}},
{{0.f, 0.f, 0.f}, {1.f, 1.f, 1.f}, {0.f, -5.f, 0.f}, {0.f, 5.f, 0.f}},
{{4.f, 0.f, 0.f}, {1.f, 1.f, 1.f}, {0.f, 0.f, 0.f}, {2.f, 0.f, 0.f}}, // too short
{{0.f, 0.f, 0.f}, {1.f, 1.f, 1.f}, {-5.f, 5.f, 0.f}, {5.f, 5.f, 0.f}}, // miss
{{2.f, 3.f, -1.f}, {0.5f, 0.5f, 0.5f}, {0.f, 0.f, 0.f}, {10.f, 15.f, -5.f}}, // diagonal
};
for (const auto& [center, half, start, end]: cases)
{
const AABB aabb{center - half, center + half};
const auto obb = axis_aligned_obb(center, half);
const auto ray = make_ray(start, end);
const auto aabb_hit = LineTracer::get_ray_hit_point(ray, aabb);
const auto obb_hit = LineTracer::get_ray_hit_point(ray, obb);
EXPECT_NEAR(aabb_hit.x, obb_hit.x, 1e-4f);
EXPECT_NEAR(aabb_hit.y, obb_hit.y, 1e-4f);
EXPECT_NEAR(aabb_hit.z, obb_hit.z, 1e-4f);
}
}
+169 -9
View File
@@ -1,6 +1,7 @@
// UnitTestMat.cpp
#include "omath/linear_algebra/mat.hpp"
#include "omath/linear_algebra/vector3.hpp"
#include "omath/trigonometry/angles.hpp"
#include <gtest/gtest.h>
using namespace omath;
@@ -220,8 +221,8 @@ TEST(UnitTestMatStandalone, Equanity)
constexpr omath::Vector3<float> left_handed = {0, 2, 10};
constexpr omath::Vector3<float> right_handed = {0, 2, -10};
const auto proj_left_handed = omath::mat_perspective_left_handed(90.f, 16.f / 9.f, 0.1f, 1000.f);
const auto proj_right_handed = omath::mat_perspective_right_handed(90.f, 16.f / 9.f, 0.1f, 1000.f);
const auto proj_left_handed = omath::mat_perspective_left_handed_vertical_fov(90.f, 16.f / 9.f, 0.1f, 1000.f);
const auto proj_right_handed = omath::mat_perspective_right_handed_vertical_fov(90.f, 16.f / 9.f, 0.1f, 1000.f);
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);
@@ -233,7 +234,7 @@ TEST(UnitTestMatStandalone, Equanity)
}
TEST(UnitTestMatStandalone, MatPerspectiveLeftHanded)
{
const auto perspective_proj = mat_perspective_left_handed(90.f, 16.f/9.f, 0.1f, 1000.f);
const auto perspective_proj = mat_perspective_left_handed_vertical_fov(90.f, 16.f/9.f, 0.1f, 1000.f);
auto projected = perspective_proj
* mat_column_from_vector<float>({0, 0, 0.1001});
@@ -244,7 +245,7 @@ TEST(UnitTestMatStandalone, MatPerspectiveLeftHanded)
TEST(UnitTestMatStandalone, MatPerspectiveLeftHandedZeroToOne)
{
const auto proj = mat_perspective_left_handed<float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
const auto proj = mat_perspective_left_handed_vertical_fov<float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
90.f, 16.f / 9.f, 0.1f, 1000.f);
// Near plane point should map to z ~ 0
@@ -266,7 +267,7 @@ TEST(UnitTestMatStandalone, MatPerspectiveLeftHandedZeroToOne)
TEST(UnitTestMatStandalone, MatPerspectiveRightHandedZeroToOne)
{
const auto proj = mat_perspective_right_handed<float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
const auto proj = mat_perspective_right_handed_vertical_fov<float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
90.f, 16.f / 9.f, 0.1f, 1000.f);
// Near plane point (negative z for right-handed) should map to z ~ 0
@@ -289,8 +290,8 @@ TEST(UnitTestMatStandalone, MatPerspectiveRightHandedZeroToOne)
TEST(UnitTestMatStandalone, MatPerspectiveNegativeOneToOneRange)
{
// Verify existing [-1, 1] behavior with explicit template arg matches default
const auto proj_default = mat_perspective_left_handed(90.f, 16.f / 9.f, 0.1f, 1000.f);
const auto proj_explicit = mat_perspective_left_handed<float, MatStoreType::ROW_MAJOR,
const auto proj_default = mat_perspective_left_handed_vertical_fov(90.f, 16.f / 9.f, 0.1f, 1000.f);
const auto proj_explicit = mat_perspective_left_handed_vertical_fov<float, MatStoreType::ROW_MAJOR,
NDCDepthRange::NEGATIVE_ONE_TO_ONE>(90.f, 16.f / 9.f, 0.1f, 1000.f);
EXPECT_EQ(proj_default, proj_explicit);
@@ -306,15 +307,174 @@ TEST(UnitTestMatStandalone, MatPerspectiveNegativeOneToOneRange)
EXPECT_NEAR(far_pt.at(2, 0), 1.0f, 1e-3f);
}
TEST(UnitTestMatStandalone, MatPerspectiveRightHandedNegOneToOne)
{
const auto proj = mat_perspective_right_handed_vertical_fov<float, MatStoreType::ROW_MAJOR,
NDCDepthRange::NEGATIVE_ONE_TO_ONE>(90.f, 16.f / 9.f, 0.1f, 1000.f);
// Near plane (negative z for RH) should map to z ~ -1
auto near_pt = proj * mat_column_from_vector<float>({0, 0, -0.1f});
near_pt /= near_pt.at(3, 0);
EXPECT_NEAR(near_pt.at(2, 0), -1.0f, 1e-3f);
// Far plane should map to z ~ 1
auto far_pt = proj * mat_column_from_vector<float>({0, 0, -1000.f});
far_pt /= far_pt.at(3, 0);
EXPECT_NEAR(far_pt.at(2, 0), 1.0f, 1e-3f);
// Mid-range point should be in (-1, 1)
auto mid_pt = proj * mat_column_from_vector<float>({0, 0, -500.f});
mid_pt /= mid_pt.at(3, 0);
EXPECT_GT(mid_pt.at(2, 0), -1.0f);
EXPECT_LT(mid_pt.at(2, 0), 1.0f);
}
TEST(UnitTestMatStandalone, MatPerspectiveLeftHandedHorizontalFovZeroToOne)
{
// hfov=90 deg, aspect=16/9 => tan(hfov/2)=1, so x_axis=1 and y_axis=aspect
const auto proj = mat_perspective_left_handed_horizontal_fov<float, MatStoreType::ROW_MAJOR,
NDCDepthRange::ZERO_TO_ONE>(90.f, 16.f / 9.f, 0.1f, 1000.f);
// Near plane should map to z ~ 0
auto near_pt = proj * mat_column_from_vector<float>({0, 0, 0.1f});
near_pt /= near_pt.at(3, 0);
EXPECT_NEAR(near_pt.at(2, 0), 0.0f, 1e-4f);
// Far plane should map to z ~ 1
auto far_pt = proj * mat_column_from_vector<float>({0, 0, 1000.f});
far_pt /= far_pt.at(3, 0);
EXPECT_NEAR(far_pt.at(2, 0), 1.0f, 1e-4f);
// Right edge of horizontal frustum at near plane (view_x = tan(hfov/2)*near = 0.1)
auto right_edge = proj * mat_column_from_vector<float>({0.1f, 0, 0.1f});
right_edge /= right_edge.at(3, 0);
EXPECT_NEAR(right_edge.at(0, 0), 1.0f, 1e-4f);
}
TEST(UnitTestMatStandalone, MatPerspectiveLeftHandedHorizontalFovNegOneToOne)
{
const auto proj = mat_perspective_left_handed_horizontal_fov<float, MatStoreType::ROW_MAJOR,
NDCDepthRange::NEGATIVE_ONE_TO_ONE>(90.f, 16.f / 9.f, 0.1f, 1000.f);
auto near_pt = proj * mat_column_from_vector<float>({0, 0, 0.1f});
near_pt /= near_pt.at(3, 0);
EXPECT_NEAR(near_pt.at(2, 0), -1.0f, 1e-3f);
auto far_pt = proj * mat_column_from_vector<float>({0, 0, 1000.f});
far_pt /= far_pt.at(3, 0);
EXPECT_NEAR(far_pt.at(2, 0), 1.0f, 1e-3f);
auto right_edge = proj * mat_column_from_vector<float>({0.1f, 0, 0.1f});
right_edge /= right_edge.at(3, 0);
EXPECT_NEAR(right_edge.at(0, 0), 1.0f, 1e-4f);
}
TEST(UnitTestMatStandalone, MatPerspectiveRightHandedHorizontalFovZeroToOne)
{
const auto proj = mat_perspective_right_handed_horizontal_fov<float, MatStoreType::ROW_MAJOR,
NDCDepthRange::ZERO_TO_ONE>(90.f, 16.f / 9.f, 0.1f, 1000.f);
auto near_pt = proj * mat_column_from_vector<float>({0, 0, -0.1f});
near_pt /= near_pt.at(3, 0);
EXPECT_NEAR(near_pt.at(2, 0), 0.0f, 1e-4f);
auto far_pt = proj * mat_column_from_vector<float>({0, 0, -1000.f});
far_pt /= far_pt.at(3, 0);
EXPECT_NEAR(far_pt.at(2, 0), 1.0f, 1e-4f);
auto right_edge = proj * mat_column_from_vector<float>({0.1f, 0, -0.1f});
right_edge /= right_edge.at(3, 0);
EXPECT_NEAR(right_edge.at(0, 0), 1.0f, 1e-4f);
}
TEST(UnitTestMatStandalone, MatPerspectiveRightHandedHorizontalFovNegOneToOne)
{
const auto proj = mat_perspective_right_handed_horizontal_fov<float, MatStoreType::ROW_MAJOR,
NDCDepthRange::NEGATIVE_ONE_TO_ONE>(90.f, 16.f / 9.f, 0.1f, 1000.f);
auto near_pt = proj * mat_column_from_vector<float>({0, 0, -0.1f});
near_pt /= near_pt.at(3, 0);
EXPECT_NEAR(near_pt.at(2, 0), -1.0f, 1e-3f);
auto far_pt = proj * mat_column_from_vector<float>({0, 0, -1000.f});
far_pt /= far_pt.at(3, 0);
EXPECT_NEAR(far_pt.at(2, 0), 1.0f, 1e-3f);
auto right_edge = proj * mat_column_from_vector<float>({0.1f, 0, -0.1f});
right_edge /= right_edge.at(3, 0);
EXPECT_NEAR(right_edge.at(0, 0), 1.0f, 1e-4f);
}
TEST(UnitTestMatStandalone, MatPerspectiveHorizontalVsVerticalFovEquivalence)
{
constexpr float hfov_deg = 90.f;
constexpr float aspect = 16.f / 9.f;
const float vfov_deg = angles::horizontal_fov_to_vertical(hfov_deg, aspect);
const auto proj_h = mat_perspective_left_handed_horizontal_fov(hfov_deg, aspect, 0.1f, 1000.f);
const auto proj_v = mat_perspective_left_handed_vertical_fov(vfov_deg, aspect, 0.1f, 1000.f);
for (size_t i = 0; i < 4; ++i)
for (size_t j = 0; j < 4; ++j)
EXPECT_NEAR(proj_h.at(i, j), proj_v.at(i, j), 1e-4f);
}
// Handedness contract: clip_w sign tells front-of-camera vs behind.
// LH: +z view-space is in front (clip_w > 0), -z is behind (clip_w < 0).
// RH: -z view-space is in front (clip_w > 0), +z is behind (clip_w < 0).
TEST(UnitTestMatStandalone, MatPerspectiveLeftHandedVerticalFovHandedness)
{
const auto proj = mat_perspective_left_handed_vertical_fov(90.f, 16.f / 9.f, 0.1f, 1000.f);
const auto in_front = proj * mat_column_from_vector<float>({0, 0, 1.f});
const auto behind = proj * mat_column_from_vector<float>({0, 0, -1.f});
EXPECT_GT(in_front.at(3, 0), 0.0f);
EXPECT_LT(behind.at(3, 0), 0.0f);
}
TEST(UnitTestMatStandalone, MatPerspectiveRightHandedVerticalFovHandedness)
{
const auto proj = mat_perspective_right_handed_vertical_fov(90.f, 16.f / 9.f, 0.1f, 1000.f);
const auto in_front = proj * mat_column_from_vector<float>({0, 0, -1.f});
const auto behind = proj * mat_column_from_vector<float>({0, 0, 1.f});
EXPECT_GT(in_front.at(3, 0), 0.0f);
EXPECT_LT(behind.at(3, 0), 0.0f);
}
TEST(UnitTestMatStandalone, MatPerspectiveLeftHandedHorizontalFovHandedness)
{
const auto proj = mat_perspective_left_handed_horizontal_fov(90.f, 16.f / 9.f, 0.1f, 1000.f);
const auto in_front = proj * mat_column_from_vector<float>({0, 0, 1.f});
const auto behind = proj * mat_column_from_vector<float>({0, 0, -1.f});
EXPECT_GT(in_front.at(3, 0), 0.0f);
EXPECT_LT(behind.at(3, 0), 0.0f);
}
TEST(UnitTestMatStandalone, MatPerspectiveRightHandedHorizontalFovHandedness)
{
const auto proj = mat_perspective_right_handed_horizontal_fov(90.f, 16.f / 9.f, 0.1f, 1000.f);
const auto in_front = proj * mat_column_from_vector<float>({0, 0, -1.f});
const auto behind = proj * mat_column_from_vector<float>({0, 0, 1.f});
EXPECT_GT(in_front.at(3, 0), 0.0f);
EXPECT_LT(behind.at(3, 0), 0.0f);
}
TEST(UnitTestMatStandalone, MatPerspectiveZeroToOneEquanity)
{
// LH and RH should produce same NDC for mirrored z
constexpr omath::Vector3<float> left_handed = {0, 2, 10};
constexpr omath::Vector3<float> right_handed = {0, 2, -10};
const auto proj_lh = mat_perspective_left_handed<float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
const auto proj_lh = mat_perspective_left_handed_vertical_fov<float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
90.f, 16.f / 9.f, 0.1f, 1000.f);
const auto proj_rh = mat_perspective_right_handed<float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
const auto proj_rh = mat_perspective_right_handed_vertical_fov<float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
90.f, 16.f / 9.f, 0.1f, 1000.f);
auto ndc_lh = proj_lh * mat_column_from_vector(left_handed);
+359
View File
@@ -0,0 +1,359 @@
//
// Created by Vladislav on 07.05.2026.
//
#include <cmath>
#include <gtest/gtest.h>
#include <numbers>
#include <omath/3d_primitives/obb.hpp>
#include <omath/engines/opengl_engine/camera.hpp>
#include <omath/engines/source_engine/camera.hpp>
#include <omath/engines/unity_engine/camera.hpp>
#include <omath/projection/camera.hpp>
using ObbF = omath::primitives::Obb<float>;
using ObbD = omath::primitives::Obb<double>;
using Vec3F = omath::Vector3<float>;
using Vec3D = omath::Vector3<double>;
namespace
{
constexpr ObbF axis_aligned_obb(const Vec3F& center, const Vec3F& half_extents) noexcept
{
return ObbF{center, {1.f, 0.f, 0.f}, {0.f, 1.f, 0.f}, {0.f, 0.f, 1.f}, half_extents};
}
ObbF rotated_around_z(const Vec3F& center, const Vec3F& half_extents, const float radians) noexcept
{
const auto c = std::cos(radians);
const auto s = std::sin(radians);
return ObbF{center, {c, s, 0.f}, {-s, c, 0.f}, {0.f, 0.f, 1.f}, half_extents};
}
ObbF rotated_around_y(const Vec3F& center, const Vec3F& half_extents, const float radians) noexcept
{
const auto c = std::cos(radians);
const auto s = std::sin(radians);
return ObbF{center, {c, 0.f, -s}, {0.f, 1.f, 0.f}, {s, 0.f, c}, half_extents};
}
void expect_vec_near(const Vec3F& actual, const Vec3F& expected, const float epsilon = 1e-5f)
{
EXPECT_NEAR(actual.x, expected.x, epsilon);
EXPECT_NEAR(actual.y, expected.y, epsilon);
EXPECT_NEAR(actual.z, expected.z, epsilon);
}
} // namespace
// --- struct-level tests ---
TEST(ObbTests, VerticesOfAxisAlignedUnitBox)
{
constexpr auto box = axis_aligned_obb({0.f, 0.f, 0.f}, {1.f, 1.f, 1.f});
constexpr auto v = box.vertices();
EXPECT_EQ(v[0], (Vec3F{-1.f, -1.f, -1.f}));
EXPECT_EQ(v[1], (Vec3F{1.f, -1.f, -1.f}));
EXPECT_EQ(v[2], (Vec3F{-1.f, 1.f, -1.f}));
EXPECT_EQ(v[3], (Vec3F{1.f, 1.f, -1.f}));
EXPECT_EQ(v[4], (Vec3F{-1.f, -1.f, 1.f}));
EXPECT_EQ(v[5], (Vec3F{1.f, -1.f, 1.f}));
EXPECT_EQ(v[6], (Vec3F{-1.f, 1.f, 1.f}));
EXPECT_EQ(v[7], (Vec3F{1.f, 1.f, 1.f}));
}
TEST(ObbTests, VerticesOfTranslatedBox)
{
constexpr auto box = axis_aligned_obb({10.f, 20.f, 30.f}, {1.f, 2.f, 3.f});
constexpr auto v = box.vertices();
EXPECT_EQ(v[0], (Vec3F{9.f, 18.f, 27.f}));
EXPECT_EQ(v[7], (Vec3F{11.f, 22.f, 33.f}));
}
TEST(ObbTests, VerticesOfRotatedBox)
{
constexpr auto pi = std::numbers::pi_v<float>;
const auto box = rotated_around_z({0.f, 0.f, 0.f}, {1.f, 1.f, 1.f}, pi / 2.f);
const auto v = box.vertices();
// After 90° rotation around Z, local +X maps to world +Y, local +Y maps to world -X.
// The eight vertices are still the same eight points (a cube is symmetric), but their
// ordering changes. Check that the corner set as a whole is still |coord| == 1.
for (const auto& corner : v)
{
EXPECT_NEAR(std::abs(corner.x), 1.f, 1e-5f);
EXPECT_NEAR(std::abs(corner.y), 1.f, 1e-5f);
EXPECT_NEAR(std::abs(corner.z), 1.f, 1e-5f);
}
}
TEST(ObbTests, VerticesOfTranslatedNonUniformRotatedBox)
{
const auto box = rotated_around_z({2.f, 3.f, 4.f}, {2.f, 1.f, 0.5f}, std::numbers::pi_v<float> / 2.f);
const auto v = box.vertices();
expect_vec_near(v[0], {3.f, 1.f, 3.5f});
expect_vec_near(v[1], {3.f, 5.f, 3.5f});
expect_vec_near(v[2], {1.f, 1.f, 3.5f});
expect_vec_near(v[3], {1.f, 5.f, 3.5f});
expect_vec_near(v[4], {3.f, 1.f, 4.5f});
expect_vec_near(v[5], {3.f, 5.f, 4.5f});
expect_vec_near(v[6], {1.f, 1.f, 4.5f});
expect_vec_near(v[7], {1.f, 5.f, 4.5f});
}
TEST(ObbTests, VerticesCollapseWhenOneExtentIsZero)
{
constexpr auto box = axis_aligned_obb({1.f, 2.f, 3.f}, {2.f, 0.f, 4.f});
constexpr auto v = box.vertices();
EXPECT_EQ(v[0], v[2]);
EXPECT_EQ(v[1], v[3]);
EXPECT_EQ(v[4], v[6]);
EXPECT_EQ(v[5], v[7]);
EXPECT_EQ(v[0], (Vec3F{-1.f, 2.f, -1.f}));
EXPECT_EQ(v[1], (Vec3F{3.f, 2.f, -1.f}));
EXPECT_EQ(v[4], (Vec3F{-1.f, 2.f, 7.f}));
EXPECT_EQ(v[5], (Vec3F{3.f, 2.f, 7.f}));
}
TEST(ObbTests, DoublePrecisionInstantiation)
{
constexpr ObbD box{{0.0, 0.0, 0.0}, {1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}, {2.0, 3.0, 4.0}};
constexpr auto v = box.vertices();
EXPECT_DOUBLE_EQ(v[0].x, -2.0);
EXPECT_DOUBLE_EQ(v[0].y, -3.0);
EXPECT_DOUBLE_EQ(v[0].z, -4.0);
EXPECT_DOUBLE_EQ(v[7].x, 2.0);
EXPECT_DOUBLE_EQ(v[7].y, 3.0);
EXPECT_DOUBLE_EQ(v[7].z, 4.0);
}
// --- frustum culling tests (Source Engine: +X forward, +Y left, +Z up) ---
TEST(ObbTests, AxisAlignedInFrontNotCulled)
{
constexpr auto fov = omath::projection::FieldOfView::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);
constexpr auto obb = axis_aligned_obb({100.f, 0.f, 0.f}, {10.f, 1.f, 1.f});
EXPECT_FALSE(cam.is_obb_culled_by_frustum(obb));
}
TEST(ObbTests, AxisAlignedBehindCameraCulled)
{
constexpr auto fov = omath::projection::FieldOfView::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);
constexpr auto obb = axis_aligned_obb({-150.f, 0.f, 0.f}, {50.f, 1.f, 1.f});
EXPECT_TRUE(cam.is_obb_culled_by_frustum(obb));
}
TEST(ObbTests, AxisAlignedBeyondFarPlaneCulled)
{
constexpr auto fov = omath::projection::FieldOfView::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);
constexpr auto obb = axis_aligned_obb({1750.f, 0.f, 0.f}, {250.f, 1.f, 1.f});
EXPECT_TRUE(cam.is_obb_culled_by_frustum(obb));
}
TEST(ObbTests, AxisAlignedStraddlingFarPlaneNotCulled)
{
constexpr auto fov = omath::projection::FieldOfView::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);
constexpr auto obb = axis_aligned_obb({1005.f, 0.f, 0.f}, {10.f, 1.f, 1.f});
EXPECT_FALSE(cam.is_obb_culled_by_frustum(obb));
}
TEST(ObbTests, AxisAlignedFarLeftCulled)
{
constexpr auto fov = omath::projection::FieldOfView::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);
constexpr auto obb = axis_aligned_obb({100.f, 4500.f, 0.f}, {10.f, 500.f, 1.f});
EXPECT_TRUE(cam.is_obb_culled_by_frustum(obb));
}
TEST(ObbTests, AxisAlignedFarRightCulled)
{
constexpr auto fov = omath::projection::FieldOfView::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);
constexpr auto obb = axis_aligned_obb({100.f, -4500.f, 0.f}, {10.f, 500.f, 1.f});
EXPECT_TRUE(cam.is_obb_culled_by_frustum(obb));
}
TEST(ObbTests, AxisAlignedAboveCulled)
{
constexpr auto fov = omath::projection::FieldOfView::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);
constexpr auto obb = axis_aligned_obb({100.f, 0.f, 5500.f}, {10.f, 1.f, 500.f});
EXPECT_TRUE(cam.is_obb_culled_by_frustum(obb));
}
TEST(ObbTests, AxisAlignedBelowCulled)
{
constexpr auto fov = omath::projection::FieldOfView::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);
constexpr auto obb = axis_aligned_obb({100.f, 0.f, -5500.f}, {10.f, 1.f, 500.f});
EXPECT_TRUE(cam.is_obb_culled_by_frustum(obb));
}
TEST(ObbTests, MatchesAabbForAxisAlignedBox)
{
// For axis-aligned OBBs, the result must agree with is_aabb_culled_by_frustum.
constexpr auto fov = omath::projection::FieldOfView::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 struct
{
Vec3F center;
Vec3F half;
} cases[] = {
{{100.f, 0.f, 0.f}, {10.f, 1.f, 1.f}}, // in front
{{-150.f, 0.f, 0.f}, {50.f, 1.f, 1.f}}, // behind
{{1750.f, 0.f, 0.f}, {250.f, 1.f, 1.f}}, // beyond far
{{100.f, 4500.f, 0.f}, {10.f, 500.f, 1.f}}, // far left
{{0.f, 0.f, 0.f}, {500.f, 500.f, 500.f}}, // encloses camera
{{275.f, 0.f, 0.f}, {225.f, 1.f, 1.f}}, // straddles near
};
for (const auto& [center, half]: cases)
{
const omath::primitives::Aabb<float> aabb{center - half, center + half};
const auto obb = axis_aligned_obb(center, half);
EXPECT_EQ(cam.is_obb_culled_by_frustum(obb), cam.is_aabb_culled_by_frustum(aabb))
<< "mismatch for center (" << center.x << "," << center.y << "," << center.z << ")";
}
}
TEST(ObbTests, RotationCanPullBoxIntoFrustum)
{
// Tall thin column sitting just outside the +Y frustum boundary at X=50.
// Axis-aligned: every corner has Y≈100 at X≈50, all outside the +Y plane → culled.
// Rotated 90° around world Y: the 50-unit extent now points along world +X, so the rod
// sweeps forward to X≈100 where the +Y plane is far more permissive — front end inside,
// box no longer fully outside → not culled.
constexpr auto fov = omath::projection::FieldOfView::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);
constexpr Vec3F center{50.f, 100.f, 0.f};
constexpr Vec3F half{1.f, 1.f, 50.f};
constexpr auto axis_aligned = axis_aligned_obb(center, half);
EXPECT_TRUE(cam.is_obb_culled_by_frustum(axis_aligned));
const auto rotated = rotated_around_y(center, half, std::numbers::pi_v<float> / 2.f);
EXPECT_FALSE(cam.is_obb_culled_by_frustum(rotated));
}
TEST(ObbTests, RotationCanPushBoxOutOfFrustum)
{
// Long forward-pointing rod whose front end pokes into the frustum near the +Y boundary.
// Axis-aligned (long along X): the front end at X≈100 has Y=129 just inside the +Y plane,
// so part of the rod is visible → not culled.
// Rotated 90° around Z: the rod's long axis now points along world Y, so all corners
// shift to Y∈[80,180] at X≈50 — every corner is outside the +Y plane → culled.
constexpr auto fov = omath::projection::FieldOfView::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);
constexpr Vec3F center{50.f, 130.f, 0.f};
constexpr Vec3F half{50.f, 1.f, 1.f};
constexpr auto axis_aligned = axis_aligned_obb(center, half);
EXPECT_FALSE(cam.is_obb_culled_by_frustum(axis_aligned));
const auto rotated = rotated_around_z(center, half, std::numbers::pi_v<float> / 2.f);
EXPECT_TRUE(cam.is_obb_culled_by_frustum(rotated));
}
TEST(ObbTests, RotatedBoxStraddlingFrustumNotCulled)
{
constexpr auto fov = omath::projection::FieldOfView::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);
// Box centred in front, rotated 30° — clearly straddles into the frustum.
const auto obb = rotated_around_z({200.f, 0.f, 0.f}, {50.f, 50.f, 50.f},
std::numbers::pi_v<float> / 6.f);
EXPECT_FALSE(cam.is_obb_culled_by_frustum(obb));
}
TEST(ObbTests, OpenGlEngineRotatedInFrontNotCulled)
{
// OpenGL: -Z forward, COLUMN_MAJOR, NEGATIVE_ONE_TO_ONE
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);
const auto obb = rotated_around_z({0.f, 0.f, -100.f}, {5.f, 5.f, 5.f},
std::numbers::pi_v<float> / 4.f);
EXPECT_FALSE(cam.is_obb_culled_by_frustum(obb));
}
TEST(ObbTests, OpenGlEngineBehindCulled)
{
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);
constexpr auto obb = axis_aligned_obb({0.f, 0.f, 100.f}, {5.f, 5.f, 5.f});
EXPECT_TRUE(cam.is_obb_culled_by_frustum(obb));
}
TEST(ObbTests, OpenGlEngineStraddlingFarPlaneNotCulled)
{
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, 100.f);
const auto obb = rotated_around_z({0.f, 0.f, -105.f}, {5.f, 5.f, 10.f},
std::numbers::pi_v<float> / 4.f);
EXPECT_FALSE(cam.is_obb_culled_by_frustum(obb));
}
TEST(ObbTests, UnityEngineBeyondFarCulled)
{
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, 500.f);
const auto obb = rotated_around_z({0.f, 0.f, 700.f}, {5.f, 5.f, 5.f},
std::numbers::pi_v<float> / 4.f);
EXPECT_TRUE(cam.is_obb_culled_by_frustum(obb));
}
TEST(ObbTests, DegenerateZeroVolumeInsideNotCulled)
{
constexpr auto fov = omath::projection::FieldOfView::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);
// Zero-extent OBB — collapses to a point, but still must not be culled if the centre is inside.
constexpr auto obb = axis_aligned_obb({100.f, 0.f, 0.f}, {0.f, 0.f, 0.f});
EXPECT_FALSE(cam.is_obb_culled_by_frustum(obb));
}
TEST(ObbTests, EnclosingCameraNotCulled)
{
constexpr auto fov = omath::projection::FieldOfView::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);
// Huge rotated box that fully encloses the camera origin.
const auto obb = rotated_around_z({0.f, 0.f, 0.f}, {500.f, 500.f, 500.f},
std::numbers::pi_v<float> / 5.f);
EXPECT_FALSE(cam.is_obb_culled_by_frustum(obb));
}
+32
View File
@@ -240,6 +240,38 @@ TEST_F(UnitTestVector2, Abs_ZeroValues)
EXPECT_FLOAT_EQ(v3.y, 0.0f);
}
TEST_F(UnitTestVector2, Abs_Const_ReturnsAbsoluteCopy)
{
const Vector2 v3(-1.0f, -2.0f);
const Vector2 result = v3.abs();
EXPECT_FLOAT_EQ(result.x, 1.0f);
EXPECT_FLOAT_EQ(result.y, 2.0f);
}
TEST_F(UnitTestVector2, Abs_Const_DoesNotMutateSource)
{
const Vector2 v3(-1.0f, -2.0f);
(void)v3.abs();
EXPECT_FLOAT_EQ(v3.x, -1.0f);
EXPECT_FLOAT_EQ(v3.y, -2.0f);
}
TEST_F(UnitTestVector2, Abs_Const_PositiveValues)
{
const Vector2 v3(1.0f, 2.0f);
const Vector2 result = v3.abs();
EXPECT_FLOAT_EQ(result.x, 1.0f);
EXPECT_FLOAT_EQ(result.y, 2.0f);
}
TEST_F(UnitTestVector2, Abs_Const_MixedSigns)
{
const Vector2 v3(-3.5f, 4.5f);
const Vector2 result = v3.abs();
EXPECT_FLOAT_EQ(result.x, 3.5f);
EXPECT_FLOAT_EQ(result.y, 4.5f);
}
TEST_F(UnitTestVector2, Sum)
{
constexpr float sum = Vector2(1.0f, 2.0f).sum();
+36
View File
@@ -244,6 +244,42 @@ TEST_F(UnitTestVector3, Abs)
EXPECT_FLOAT_EQ(v3.z, 3.0f);
}
TEST_F(UnitTestVector3, Abs_Const_ReturnsAbsoluteCopy)
{
const Vector3 v3(-1.0f, -2.0f, -3.0f);
const Vector3 result = v3.abs();
EXPECT_FLOAT_EQ(result.x, 1.0f);
EXPECT_FLOAT_EQ(result.y, 2.0f);
EXPECT_FLOAT_EQ(result.z, 3.0f);
}
TEST_F(UnitTestVector3, Abs_Const_DoesNotMutateSource)
{
const Vector3 v3(-1.0f, -2.0f, -3.0f);
(void)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, Abs_Const_PositiveValues)
{
const Vector3 v3(1.0f, 2.0f, 3.0f);
const Vector3 result = v3.abs();
EXPECT_FLOAT_EQ(result.x, 1.0f);
EXPECT_FLOAT_EQ(result.y, 2.0f);
EXPECT_FLOAT_EQ(result.z, 3.0f);
}
TEST_F(UnitTestVector3, Abs_Const_MixedSigns)
{
const Vector3 v3(-1.5f, 2.5f, -3.5f);
const Vector3 result = v3.abs();
EXPECT_FLOAT_EQ(result.x, 1.5f);
EXPECT_FLOAT_EQ(result.y, 2.5f);
EXPECT_FLOAT_EQ(result.z, 3.5f);
}
TEST_F(UnitTestVector3, Sum)
{
constexpr auto sum = Vector3(1.0f, 2.0f, 3.0f).sum();
+40
View File
@@ -226,6 +226,46 @@ TEST_F(UnitTestVector4, Abs)
EXPECT_FLOAT_EQ(v3.w, 4.0f);
}
TEST_F(UnitTestVector4, Abs_Const_ReturnsAbsoluteCopy)
{
const Vector4 v3(-1.0f, -2.0f, -3.0f, -4.0f);
const Vector4 result = v3.abs();
EXPECT_FLOAT_EQ(result.x, 1.0f);
EXPECT_FLOAT_EQ(result.y, 2.0f);
EXPECT_FLOAT_EQ(result.z, 3.0f);
EXPECT_FLOAT_EQ(result.w, 4.0f);
}
TEST_F(UnitTestVector4, Abs_Const_DoesNotMutateSource)
{
const Vector4 v3(-1.0f, -2.0f, -3.0f, -4.0f);
(void)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, Abs_Const_PositiveValues)
{
const Vector4 v3(1.0f, 2.0f, 3.0f, 4.0f);
const Vector4 result = v3.abs();
EXPECT_FLOAT_EQ(result.x, 1.0f);
EXPECT_FLOAT_EQ(result.y, 2.0f);
EXPECT_FLOAT_EQ(result.z, 3.0f);
EXPECT_FLOAT_EQ(result.w, 4.0f);
}
TEST_F(UnitTestVector4, Abs_Const_MixedSigns)
{
const Vector4 v3(-1.5f, 2.5f, -3.5f, 4.5f);
const Vector4 result = v3.abs();
EXPECT_FLOAT_EQ(result.x, 1.5f);
EXPECT_FLOAT_EQ(result.y, 2.5f);
EXPECT_FLOAT_EQ(result.z, 3.5f);
EXPECT_FLOAT_EQ(result.w, 4.5f);
}
TEST_F(UnitTestVector4, Sum)
{
constexpr float sum = Vector4(1.0f, 2.0f, 3.0f, 4.0f).sum();
+169
View File
@@ -0,0 +1,169 @@
local function approx(a, b, eps) return math.abs(a - b) < (eps or 1e-4) end
local function cube_points()
return {
omath.Vec3.new(-1, -1, -1),
omath.Vec3.new(1, -1, -1),
omath.Vec3.new(-1, 1, -1),
omath.Vec3.new(1, 1, -1),
omath.Vec3.new(-1, -1, 1),
omath.Vec3.new(1, -1, 1),
omath.Vec3.new(-1, 1, 1),
omath.Vec3.new(1, 1, 1),
}
end
function Collision_Aabb_constructor_and_fields()
local aabb = omath.primitives.Aabb.new(omath.Vec3.new(-1, -2, -3), omath.Vec3.new(1, 2, 3))
assert(aabb.min.x == -1 and aabb.max.z == 3)
aabb.max = omath.Vec3.new(2, 3, 4)
assert(aabb.max.x == 2 and aabb.max.y == 3 and aabb.max.z == 4)
end
function Collision_Aabb_center_extents()
local aabb = omath.primitives.Aabb.new(omath.Vec3.new(-1, -2, -3), omath.Vec3.new(3, 6, 9))
local center = aabb:center()
local extents = aabb:extents()
assert(center.x == 1 and center.y == 2 and center.z == 3)
assert(extents.x == 2 and extents.y == 4 and extents.z == 6)
end
function Collision_Aabb_top_bottom_default_axis()
local aabb = omath.primitives.Aabb.new(omath.Vec3.new(-1, -2, -3), omath.Vec3.new(3, 6, 9))
assert(aabb:top().y == 6)
assert(aabb:bottom().y == -2)
end
function Collision_Aabb_top_bottom_explicit_axis()
local aabb = omath.primitives.Aabb.new(omath.Vec3.new(-1, -2, -3), omath.Vec3.new(3, 6, 9))
assert(aabb:top(omath.primitives.UpAxis.X).x == 3)
assert(aabb:bottom(omath.primitives.UpAxis.Z).z == -3)
end
function Collision_Aabb_is_collide()
local a = omath.primitives.Aabb.new(omath.Vec3.new(-1, -1, -1), omath.Vec3.new(1, 1, 1))
local b = omath.primitives.Aabb.new(omath.Vec3.new(0, 0, 0), omath.Vec3.new(2, 2, 2))
local c = omath.primitives.Aabb.new(omath.Vec3.new(3, 3, 3), omath.Vec3.new(4, 4, 4))
assert(a:is_collide(b))
assert(not a:is_collide(c))
end
function Collision_Aabb_as_table()
local aabb = omath.primitives.Aabb.new(omath.Vec3.new(-1, -2, -3), omath.Vec3.new(1, 2, 3))
local t = aabb:as_table()
assert(t.min.x == -1 and t.max.z == 3)
end
function Collision_Obb_constructor_and_vertices()
local obb = omath.primitives.Obb.new(
omath.Vec3.new(0, 0, 0),
omath.Vec3.new(1, 0, 0),
omath.Vec3.new(0, 1, 0),
omath.Vec3.new(0, 0, 1),
omath.Vec3.new(1, 2, 3)
)
local vertices = obb:vertices()
assert(#vertices == 8)
assert(vertices[1].x == -1 and vertices[1].y == -2 and vertices[1].z == -3)
assert(vertices[8].x == 1 and vertices[8].y == 2 and vertices[8].z == 3)
end
function Collision_Ray_constructor_and_direction()
local ray = omath.collision.Ray.new(omath.Vec3.new(1, 2, 3), omath.Vec3.new(4, 6, 3), true)
local dir = ray:direction_vector()
local normal = ray:direction_vector_normalized()
assert(ray.infinite_length)
assert(dir.x == 3 and dir.y == 4 and dir.z == 0)
assert(approx(normal:length(), 1))
end
function Collision_LineTracer_triangle_hit()
local tri = omath.Triangle.new(omath.Vec3.new(0, 0, 0), omath.Vec3.new(2, 0, 0), omath.Vec3.new(0, 2, 0))
local ray = omath.collision.Ray.new(omath.Vec3.new(0.5, 0.5, -1), omath.Vec3.new(0.5, 0.5, 1))
local hit = omath.collision.LineTracer.get_ray_hit_point(ray, tri)
assert(omath.collision.LineTracer.ray_hits_triangle(ray, tri))
assert(not omath.collision.LineTracer.can_trace_line(ray, tri))
assert(approx(hit.x, 0.5) and approx(hit.y, 0.5) and approx(hit.z, 0))
end
function Collision_LineTracer_triangle_miss()
local tri = omath.Triangle.new(omath.Vec3.new(0, 0, 0), omath.Vec3.new(1, 0, 0), omath.Vec3.new(0, 1, 0))
local ray = omath.collision.Ray.new(omath.Vec3.new(2, 2, -1), omath.Vec3.new(2, 2, 1))
local hit = omath.collision.LineTracer.get_ray_hit_point(ray, tri)
assert(not omath.collision.LineTracer.ray_hits_triangle(ray, tri))
assert(omath.collision.LineTracer.can_trace_line(ray, tri))
assert(hit == ray["end"])
end
function Collision_LineTracer_aabb_hit()
local aabb = omath.primitives.Aabb.new(omath.Vec3.new(-1, -1, -1), omath.Vec3.new(1, 1, 1))
local ray = omath.collision.Ray.new(omath.Vec3.new(0, 0, -3), omath.Vec3.new(0, 0, 3))
local hit = omath.collision.LineTracer.get_ray_hit_point(ray, aabb)
assert(omath.collision.LineTracer.ray_hits_aabb(ray, aabb))
assert(approx(hit.x, 0) and approx(hit.y, 0) and approx(hit.z, -1))
end
function Collision_LineTracer_aabb_miss()
local aabb = omath.primitives.Aabb.new(omath.Vec3.new(-1, -1, -1), omath.Vec3.new(1, 1, 1))
local ray = omath.collision.Ray.new(omath.Vec3.new(0, 3, -3), omath.Vec3.new(0, 3, 3))
local hit = omath.collision.LineTracer.get_ray_hit_point(ray, aabb)
assert(not omath.collision.LineTracer.ray_hits_aabb(ray, aabb))
assert(hit == ray["end"])
end
function Collision_LineTracer_obb_hit()
local obb = omath.primitives.Obb.new(
omath.Vec3.new(0, 0, 0),
omath.Vec3.new(1, 0, 0),
omath.Vec3.new(0, 1, 0),
omath.Vec3.new(0, 0, 1),
omath.Vec3.new(1, 1, 1)
)
local ray = omath.collision.Ray.new(omath.Vec3.new(0, 0, -3), omath.Vec3.new(0, 0, 3))
local hit = omath.collision.LineTracer.get_ray_hit_point(ray, obb)
assert(omath.collision.LineTracer.ray_hits_obb(ray, obb))
assert(approx(hit.z, -1))
end
function Collision_ConvexCollider_vertices_and_support()
local collider = omath.collision.ConvexCollider.new(cube_points(), omath.Vec3.new(2, 0, 0))
assert(collider:vertex_count() == 8)
assert(#collider:vertices() == 8)
local support = collider:find_abs_furthest_vertex_position(omath.Vec3.new(1, 0, 0))
assert(support.x == 3)
end
function Collision_Gjk_support_vertex()
local a = omath.collision.ConvexCollider.new(cube_points())
local b = omath.collision.ConvexCollider.new(cube_points())
local support = omath.collision.gjk_support_vertex(a, b, omath.Vec3.new(1, 0, 0))
assert(support.x == 2 and support.y == 0 and support.z == 0)
end
function Collision_Gjk_collide_true()
local a = omath.collision.ConvexCollider.new(cube_points())
local b = omath.collision.ConvexCollider.new(cube_points(), omath.Vec3.new(1, 0, 0))
assert(omath.collision.gjk_collide(a, b))
end
function Collision_Gjk_collide_false()
local a = omath.collision.ConvexCollider.new(cube_points())
local b = omath.collision.ConvexCollider.new(cube_points(), omath.Vec3.new(5, 0, 0))
assert(not omath.collision.gjk_collide(a, b))
end
function Collision_Epa_solve_hit()
local a = omath.collision.ConvexCollider.new(cube_points())
local b = omath.collision.ConvexCollider.new(cube_points(), omath.Vec3.new(1.5, 0, 0))
local result = omath.collision.epa_solve(a, b)
assert(result ~= nil)
assert(result.depth > 0)
assert(approx(result.normal:length(), 1))
assert(approx(result.penetration_vector:length(), result.depth))
end
function Collision_Epa_solve_miss()
local a = omath.collision.ConvexCollider.new(cube_points())
local b = omath.collision.ConvexCollider.new(cube_points(), omath.Vec3.new(5, 0, 0))
assert(omath.collision.epa_solve(a, b) == nil)
end
+248
View File
@@ -0,0 +1,248 @@
local function approx(a, b, eps) return math.abs(a - b) < (eps or 1e-4) end
local function color() return omath.Color.new(1, 0, 0, 1) end
local function fill() return omath.Color.new(0, 0, 0, 0.5) end
local function outline() return omath.Color.new(0, 0, 0, 1) end
local function bg() return omath.Color.new(0.2, 0.2, 0.2, 1) end
local function renderer_with(commands)
return omath.hud.Renderer.new({
add_line = function(a, b, c, thickness)
table.insert(commands, { kind = "line", a = a, b = b, color = c, thickness = thickness })
end,
add_polyline = function(points, c, thickness)
table.insert(commands, { kind = "polyline", points = points, color = c, thickness = thickness })
end,
add_filled_polyline = function(points, c)
table.insert(commands, { kind = "filled_polyline", points = points, color = c })
end,
add_rectangle = function(min, max, c)
table.insert(commands, { kind = "rectangle", min = min, max = max, color = c })
end,
add_filled_rectangle = function(min, max, c)
table.insert(commands, { kind = "filled_rectangle", min = min, max = max, color = c })
end,
add_circle = function(center, radius, c, thickness, segments)
table.insert(commands, {
kind = "circle",
center = center,
radius = radius,
color = c,
thickness = thickness,
segments = segments,
})
end,
add_filled_circle = function(center, radius, c, segments)
table.insert(commands, { kind = "filled_circle", center = center, radius = radius, color = c, segments = segments })
end,
add_arc = function(center, radius, a_min, a_max, c, thickness, segments)
table.insert(commands, {
kind = "arc",
center = center,
radius = radius,
a_min = a_min,
a_max = a_max,
color = c,
thickness = thickness,
segments = segments,
})
end,
add_image = function(texture, min, max, tint)
table.insert(commands, { kind = "image", texture = texture, min = min, max = max, tint = tint })
end,
add_text = function(pos, c, text)
table.insert(commands, { kind = "text", pos = pos, color = c, text = text })
end,
calc_text_size = function(text)
return omath.Vec2.new(#text * 6, 10)
end,
})
end
local function overlay(commands)
return omath.hud.EntityOverlay.new(omath.Vec2.new(100, 10), omath.Vec2.new(100, 50), renderer_with(commands))
end
function Hud_CanvasBox_default_ratio()
local box = omath.hud.CanvasBox.new(omath.Vec2.new(100, 10), omath.Vec2.new(100, 50))
assert(approx(box.top_left_corner.x, 90) and approx(box.top_left_corner.y, 10))
assert(approx(box.top_right_corner.x, 110) and approx(box.bottom_right_corner.y, 50))
end
function Hud_CanvasBox_custom_ratio()
local box = omath.hud.CanvasBox.new(omath.Vec2.new(100, 10), omath.Vec2.new(100, 50), 2)
assert(approx(box.top_left_corner.x, 80) and approx(box.bottom_right_corner.x, 120))
end
function Hud_CanvasBox_as_table()
local points = omath.hud.CanvasBox.new(omath.Vec2.new(100, 10), omath.Vec2.new(100, 50)):as_table()
assert(#points == 4)
assert(approx(points[1].x, 90) and approx(points[3].x, 110))
end
function Hud_Renderer_callbacks()
local commands = {}
local renderer = renderer_with(commands)
renderer:add_line(omath.Vec2.new(1, 2), omath.Vec2.new(3, 4), color(), 2)
renderer:add_text(omath.Vec2.new(5, 6), color(), "hp")
local size = renderer:calc_text_size("abcd")
assert(#commands == 2)
assert(commands[1].kind == "line" and commands[2].text == "hp")
assert(size.x == 24 and size.y == 10)
end
function Hud_Renderer_polyline_table()
local commands = {}
local renderer = renderer_with(commands)
renderer:add_polyline({ omath.Vec2.new(1, 1), omath.Vec2.new(2, 2) }, color(), 3)
assert(commands[1].kind == "polyline")
assert(#commands[1].points == 2 and commands[1].points[2].x == 2)
end
function Hud_Renderer_circle_defaults()
local commands = {}
local renderer = renderer_with(commands)
renderer:add_circle(omath.Vec2.new(1, 2), 5, color(), 2)
renderer:add_filled_circle(omath.Vec2.new(3, 4), 6, color())
assert(commands[1].kind == "circle" and commands[1].segments == 0)
assert(commands[2].kind == "filled_circle" and commands[2].segments == 0)
end
function Hud_EntityOverlay_add_2d_box()
local commands = {}
overlay(commands):add_2d_box(color(), fill(), outline(), 2)
assert(#commands == 3)
assert(commands[1].kind == "polyline" and approx(commands[1].thickness, 4))
assert(commands[2].kind == "polyline" and approx(commands[2].thickness, 2) and #commands[2].points == 4)
assert(commands[3].kind == "filled_polyline")
end
function Hud_EntityOverlay_add_cornered_2d_box()
local commands = {}
overlay(commands):add_cornered_2d_box(color(), fill(), outline(), 0.25, 2)
assert(#commands == 18)
assert(commands[1].kind == "polyline" and commands[2].kind == "filled_polyline")
assert(commands[3].kind == "line" and approx(commands[3].thickness, 4))
assert(commands[4].kind == "line" and approx(commands[4].thickness, 2))
assert(commands[18].kind == "line")
end
function Hud_EntityOverlay_add_dashed_box()
local commands = {}
overlay(commands):add_dashed_box(color(), 8, 5, 1)
assert(#commands > 4)
assert(commands[1].kind == "line")
end
function Hud_EntityOverlay_add_bar()
local commands = {}
overlay(commands):add_right_bar(color(), outline(), bg(), 4, 0.5)
assert(#commands == 3)
assert(commands[1].kind == "filled_rectangle" and commands[2].kind == "filled_rectangle")
assert(commands[3].kind == "rectangle")
end
function Hud_EntityOverlay_add_dashed_bar()
local commands = {}
overlay(commands):add_bottom_dashed_bar(color(), outline(), bg(), 4, 0.5, 8, 5)
assert(#commands >= 3)
assert(commands[1].kind == "filled_rectangle")
end
function Hud_EntityOverlay_add_label()
local commands = {}
overlay(commands):add_right_label(color(), 5, false, "HP")
assert(#commands == 1)
assert(commands[1].kind == "text" and commands[1].text == "HP")
assert(approx(commands[1].pos.x, 115))
end
function Hud_EntityOverlay_add_outlined_label()
local commands = {}
overlay(commands):add_left_label(color(), 5, true, "HP")
assert(#commands == 9)
assert(commands[9].kind == "text" and commands[9].text == "HP")
end
function Hud_EntityOverlay_add_centered_label()
local commands = {}
overlay(commands):add_centered_top_label(color(), 5, false, "HP")
assert(#commands == 1)
assert(commands[1].kind == "text")
assert(approx(commands[1].pos.x, 94))
end
function Hud_EntityOverlay_add_spaces()
local commands = {}
overlay(commands):add_right_space_vertical(10):add_right_label(color(), 5, false, "HP")
assert(#commands == 1)
assert(approx(commands[1].pos.y, 20))
end
function Hud_EntityOverlay_add_progress_ring()
local commands = {}
overlay(commands):add_right_progress_ring(color(), bg(), 6, 0.25, 2, 5, 16)
assert(#commands == 2)
assert(commands[1].kind == "circle" and commands[2].kind == "arc")
assert(commands[2].segments == 16)
end
function Hud_EntityOverlay_add_progress_ring_defaults()
local commands = {}
overlay(commands):add_right_progress_ring(color(), bg(), 6, 0.25)
assert(#commands == 2)
assert(commands[1].thickness == 2 and commands[1].segments == 0)
end
function Hud_EntityOverlay_add_icon()
local commands = {}
overlay(commands):add_right_icon("texture-id", 8, 6, color(), 5)
assert(#commands == 1)
assert(commands[1].kind == "image" and commands[1].texture == "texture-id")
end
function Hud_EntityOverlay_add_snap_line()
local commands = {}
overlay(commands):add_snap_line(omath.Vec2.new(0, 0), color(), 2)
assert(#commands == 1)
assert(commands[1].kind == "line")
assert(approx(commands[1].b.x, 100) and approx(commands[1].b.y, 50))
end
function Hud_EntityOverlay_add_skeleton()
local commands = {}
overlay(commands):add_skeleton(color())
assert(#commands == 15)
assert(commands[1].kind == "line")
end
function Hud_EntityOverlay_add_scan_marker()
local commands = {}
overlay(commands):add_scan_marker(color(), outline(), 2)
assert(#commands == 2)
assert(commands[1].kind == "filled_polyline" and #commands[1].points == 3)
assert(commands[2].kind == "polyline" and commands[2].thickness == 2)
end
function Hud_EntityOverlay_add_aim_dot()
local commands = {}
overlay(commands):add_aim_dot(omath.Vec2.new(5, 6), color())
assert(#commands == 1)
assert(commands[1].kind == "filled_circle" and commands[1].radius == 3)
end
function Hud_EntityOverlay_add_projectile_aim_circle()
local commands = {}
overlay(commands):add_projectile_aim(omath.Vec2.new(5, 6), color(), 4, 2, omath.hud.ProjectileAimFigure.CIRCLE)
assert(#commands == 2)
assert(commands[1].kind == "line")
assert(commands[2].kind == "filled_circle" and commands[2].radius == 4)
end
function Hud_EntityOverlay_add_projectile_aim_square_default()
local commands = {}
overlay(commands):add_projectile_aim(omath.Vec2.new(5, 6), color())
assert(#commands == 2)
assert(commands[1].kind == "line")
assert(commands[2].kind == "filled_rectangle")
end
+195
View File
@@ -0,0 +1,195 @@
local function approx(a, b, eps) return math.abs(a - b) < (eps or 1e-5) end
function Mat4_Constructor_default()
local m = omath.Mat4.new()
assert(m:row_count() == 4 and m:columns_count() == 4)
assert(m:at(0, 0) == 0 and m:at(3, 3) == 0)
end
function Mat4_FromRows()
local m = omath.Mat4.from_rows({
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 16},
})
assert(m:at(0, 0) == 1 and m:at(3, 3) == 16)
end
function Mat4_SetAt()
local m = omath.Mat4.new()
m:set_at(2, 3, 42)
assert(m:at(2, 3) == 42)
end
function Mat4_SetAndClear()
local m = omath.Mat4.new()
m:set(2)
assert(m:sum() == 32)
m:clear()
assert(m:sum() == 0)
end
function Mat4_Multiplication_matrix()
local identity = omath.Mat4.from_rows({
{1, 0, 0, 0},
{0, 1, 0, 0},
{0, 0, 1, 0},
{0, 0, 0, 1},
})
local m = omath.Mat4.from_rows({
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 16},
})
local result = m * identity
assert(result == m)
end
function Mat4_Multiplication_scalar()
local m = omath.Mat4.from_rows({
{1, 0, 0, 0},
{0, 1, 0, 0},
{0, 0, 1, 0},
{0, 0, 0, 1},
}) * 2
assert(m:at(0, 0) == 2 and m:at(3, 3) == 2)
end
function Mat4_Multiplication_scalar_reversed()
local m = 2 * omath.Mat4.from_rows({
{1, 0, 0, 0},
{0, 1, 0, 0},
{0, 0, 1, 0},
{0, 0, 0, 1},
})
assert(m:at(0, 0) == 2 and m:at(3, 3) == 2)
end
function Mat4_Division_scalar()
local m = omath.Mat4.from_rows({
{2, 0, 0, 0},
{0, 2, 0, 0},
{0, 0, 2, 0},
{0, 0, 0, 2},
}) / 2
assert(m:at(0, 0) == 1 and m:at(3, 3) == 1)
end
function Mat4_Transposed()
local m = omath.Mat4.from_rows({
{1, 2, 0, 0},
{3, 4, 0, 0},
{0, 0, 1, 0},
{0, 0, 0, 1},
}):transposed()
assert(m:at(0, 1) == 3 and m:at(1, 0) == 2)
end
function Mat4_Determinant()
local m = omath.Mat4.from_rows({
{1, 0, 0, 0},
{0, 2, 0, 0},
{0, 0, 3, 0},
{0, 0, 0, 4},
})
assert(m:determinant() == 24)
end
function Mat4_Inverted_success()
local m = omath.Mat4.from_rows({
{2, 0, 0, 0},
{0, 2, 0, 0},
{0, 0, 2, 0},
{0, 0, 0, 2},
})
local inv = m:inverted()
assert(inv ~= nil)
assert(approx(inv:at(0, 0), 0.5) and approx(inv:at(3, 3), 0.5))
end
function Mat4_Inverted_singular()
local m = omath.Mat4.new()
assert(m:inverted() == nil)
end
function Mat4_ToScreenMat()
local m = omath.Mat4.to_screen_mat(800, 600)
assert(m:at(0, 0) == 400 and m:at(1, 1) == -300 and m:at(3, 1) == 300)
end
function Mat4_Translation()
local m = omath.Mat4.translation(omath.Vec3.new(1, 2, 3))
assert(m:at(0, 3) == 1 and m:at(1, 3) == 2 and m:at(2, 3) == 3)
end
function Mat4_Scale()
local m = omath.Mat4.scale(omath.Vec3.new(2, 3, 4))
assert(m:at(0, 0) == 2 and m:at(1, 1) == 3 and m:at(2, 2) == 4)
end
function Mat4_Perspective()
local m = omath.Mat4.perspective_left_handed_vertical_fov(90, 16 / 9, 0.1, 1000)
assert(m:at(0, 0) > 0 and m:at(1, 1) > 0)
end
function Mat4_AsTable()
local m = omath.Mat4.from_rows({
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 16},
})
local t = m:as_table()
assert(t[1][1] == 1 and t[4][4] == 16)
end
function Mat4_ToString()
local m = omath.Mat4.from_rows({
{1, 2, 0, 0},
{0, 1, 0, 0},
{0, 0, 1, 0},
{0, 0, 0, 1},
})
assert(string.find(tostring(m), "%[%[") ~= nil)
end
function Mat3_FromRows()
local m = omath.Mat3.from_rows({
{1, 2, 3},
{0, 1, 4},
{5, 6, 0},
})
assert(m:determinant() == 1)
end
function Mat4ColumnMajor_FromRows()
local m = omath.Mat4ColumnMajor.from_rows({
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 16},
})
assert(m:at(0, 1) == 2 and m:at(3, 2) == 15)
end
function Mat4ColumnMajor_ToScreenMat()
local m = omath.Mat4ColumnMajor.to_screen_mat(800, 600)
assert(m:at(0, 0) == 400 and m:at(1, 1) == -300 and m:at(3, 1) == 300)
end
function Mat4ColumnMajor_Translation()
local m = omath.Mat4ColumnMajor.translation(omath.Vec3.new(1, 2, 3))
assert(m:at(0, 3) == 1 and m:at(1, 3) == 2 and m:at(2, 3) == 3)
end
function Mat4d_FromRows()
local m = omath.Mat4d.from_rows({
{1, 0, 0, 0},
{0, 2, 0, 0},
{0, 0, 3, 0},
{0, 0, 0, 4},
})
assert(m:determinant() == 24)
end
+111
View File
@@ -0,0 +1,111 @@
local function approx(a, b, eps) return math.abs(a - b) < (eps or 1e-5) end
function Quaternion_Constructor_default()
local q = omath.Quaternion.new()
assert(q.x == 0 and q.y == 0 and q.z == 0 and q.w == 1)
end
function Quaternion_Constructor_xyzw()
local q = omath.Quaternion.new(1, 2, 3, 4)
assert(q.x == 1 and q.y == 2 and q.z == 3 and q.w == 4)
end
function Quaternion_Field_mutation()
local q = omath.Quaternion.new()
q.x = 1; q.y = 2; q.z = 3; q.w = 4
assert(q.x == 1 and q.y == 2 and q.z == 3 and q.w == 4)
end
function Quaternion_FromAxisAngle_zero_angle()
local q = omath.Quaternion.from_axis_angle(omath.Vec3.new(0, 0, 1), 0)
assert(approx(q.x, 0) and approx(q.y, 0) and approx(q.z, 0) and approx(q.w, 1))
end
function Quaternion_Addition()
local q = omath.Quaternion.new(1, 2, 3, 4) + omath.Quaternion.new(4, 3, 2, 1)
assert(q.x == 5 and q.y == 5 and q.z == 5 and q.w == 5)
end
function Quaternion_Multiplication_scalar()
local q = omath.Quaternion.new(1, 2, 3, 4) * 2
assert(q.x == 2 and q.y == 4 and q.z == 6 and q.w == 8)
end
function Quaternion_Multiplication_scalar_reversed()
local q = 2 * omath.Quaternion.new(1, 2, 3, 4)
assert(q.x == 2 and q.y == 4 and q.z == 6 and q.w == 8)
end
function Quaternion_Multiplication_quaternion()
local i = omath.Quaternion.new(1, 0, 0, 0)
local j = omath.Quaternion.new(0, 1, 0, 0)
local k = i * j
assert(k.x == 0 and k.y == 0 and k.z == 1 and k.w == 0)
end
function Quaternion_UnaryMinus()
local q = -omath.Quaternion.new(1, -2, 3, -4)
assert(q.x == -1 and q.y == 2 and q.z == -3 and q.w == 4)
end
function Quaternion_EqualTo_true()
assert(omath.Quaternion.new(1, 2, 3, 4) == omath.Quaternion.new(1, 2, 3, 4))
end
function Quaternion_EqualTo_false()
assert(not (omath.Quaternion.new(1, 2, 3, 4) == omath.Quaternion.new(1, 2, 3, 5)))
end
function Quaternion_ToString()
assert(tostring(omath.Quaternion.new(1, 2, 3, 4)) == "Quaternion(1, 2, 3, 4)")
end
function Quaternion_Conjugate()
local q = omath.Quaternion.new(1, 2, 3, 4):conjugate()
assert(q.x == -1 and q.y == -2 and q.z == -3 and q.w == 4)
end
function Quaternion_Dot()
assert(omath.Quaternion.new(1, 0, 0, 0):dot(omath.Quaternion.new(0, 1, 0, 0)) == 0)
end
function Quaternion_Length()
assert(approx(omath.Quaternion.new(1, 1, 1, 1):length(), 2))
end
function Quaternion_LengthSqr()
assert(omath.Quaternion.new(1, 2, 3, 4):length_sqr() == 30)
end
function Quaternion_Normalized()
local q = omath.Quaternion.new(1, 1, 1, 1):normalized()
assert(approx(q:length(), 1))
assert(approx(q.x, 0.5) and approx(q.y, 0.5) and approx(q.z, 0.5) and approx(q.w, 0.5))
end
function Quaternion_Inverse()
local q = omath.Quaternion.from_axis_angle(omath.Vec3.new(0, 0, 1), math.pi / 3)
local identity = q * q:inverse()
assert(approx(identity.x, 0) and approx(identity.y, 0) and approx(identity.z, 0) and approx(identity.w, 1))
end
function Quaternion_Rotate()
local q = omath.Quaternion.from_axis_angle(omath.Vec3.new(0, 0, 1), math.pi / 2)
local v = q:rotate(omath.Vec3.new(1, 0, 0))
assert(approx(v.x, 0, 1e-4) and approx(v.y, 1, 1e-4) and approx(v.z, 0, 1e-4))
end
function Quaternion_ToRotationMatrix3()
local m = omath.Quaternion.new():to_rotation_matrix3()
assert(m:at(0, 0) == 1 and m:at(1, 1) == 1 and m:at(2, 2) == 1)
end
function Quaternion_ToRotationMatrix4()
local m = omath.Quaternion.new():to_rotation_matrix4()
assert(m:at(0, 0) == 1 and m:at(1, 1) == 1 and m:at(2, 2) == 1 and m:at(3, 3) == 1)
end
function Quaternion_AsTable()
local t = omath.Quaternion.new(1, 2, 3, 4):as_table()
assert(t.x == 1 and t.y == 2 and t.z == 3 and t.w == 4)
end
+40
View File
@@ -175,6 +175,46 @@ function Source_Camera_get_up()
assert(approx(make_camera():get_up():length(), 1.0))
end
function Source_Camera_get_view_matrix()
local view = make_camera():get_view_matrix()
assert(view ~= nil)
assert(view:row_count() == 4 and view:columns_count() == 4)
end
function Source_Camera_get_projection_matrix()
local projection = make_camera():get_projection_matrix()
assert(projection ~= nil)
assert(projection:at(0, 0) > 0 and projection:at(1, 1) > 0)
end
function Source_Camera_get_view_projection_matrix()
local view_projection = make_camera():get_view_projection_matrix()
assert(view_projection ~= nil)
assert(view_projection:row_count() == 4 and view_projection:columns_count() == 4)
end
function Source_Camera_extract_projection_params()
local projection = make_camera():get_projection_matrix()
local fov, aspect_ratio = omath.source.Camera.extract_projection_params(projection)
assert(fov ~= nil)
assert(approx(aspect_ratio, 1920 / 1080))
end
function Source_Camera_calc_view_angles_from_view_matrix()
local cam = make_camera()
local view = cam:get_view_matrix()
local angles = omath.source.Camera.calc_view_angles_from_view_matrix(view)
assert(approx(angles.pitch:as_degrees(), 0))
assert(approx(angles.yaw:as_degrees(), 0))
end
function Source_Camera_calc_origin_from_view_matrix()
local cam = make_camera()
cam:set_origin(omath.Vec3.new(1, 2, 3))
local origin = omath.source.Camera.calc_origin_from_view_matrix(cam:get_view_matrix())
assert(approx(origin.x, 1) and approx(origin.y, 2) and approx(origin.z, 3))
end
function Source_Camera_world_to_screen_success()
local cam = make_camera()
cam:look_at(omath.Vec3.new(1, 0, 0))
+53
View File
@@ -0,0 +1,53 @@
//
// Created by orange on 07.03.2026.
//
#include <gtest/gtest.h>
#include <lua.hpp>
#include <omath/lua/lua.hpp>
class LuaCollision : public ::testing::Test
{
protected:
lua_State* L = nullptr;
void SetUp() override
{
L = luaL_newstate();
luaL_openlibs(L);
omath::lua::LuaInterpreter::register_lib(L);
if (luaL_dofile(L, LUA_SCRIPTS_DIR "/collision_tests.lua") != LUA_OK)
FAIL() << lua_tostring(L, -1);
}
void TearDown() override { lua_close(L); }
void check(const char* func_name)
{
lua_getglobal(L, func_name);
if (lua_pcall(L, 0, 0, 0) != LUA_OK)
{
FAIL() << lua_tostring(L, -1);
lua_pop(L, 1);
}
}
};
TEST_F(LuaCollision, Aabb_constructor_and_fields) { check("Collision_Aabb_constructor_and_fields"); }
TEST_F(LuaCollision, Aabb_center_extents) { check("Collision_Aabb_center_extents"); }
TEST_F(LuaCollision, Aabb_top_bottom_default_axis) { check("Collision_Aabb_top_bottom_default_axis"); }
TEST_F(LuaCollision, Aabb_top_bottom_explicit_axis) { check("Collision_Aabb_top_bottom_explicit_axis"); }
TEST_F(LuaCollision, Aabb_is_collide) { check("Collision_Aabb_is_collide"); }
TEST_F(LuaCollision, Aabb_as_table) { check("Collision_Aabb_as_table"); }
TEST_F(LuaCollision, Obb_constructor_and_vertices) { check("Collision_Obb_constructor_and_vertices"); }
TEST_F(LuaCollision, Ray_constructor_and_direction) { check("Collision_Ray_constructor_and_direction"); }
TEST_F(LuaCollision, LineTracer_triangle_hit) { check("Collision_LineTracer_triangle_hit"); }
TEST_F(LuaCollision, LineTracer_triangle_miss) { check("Collision_LineTracer_triangle_miss"); }
TEST_F(LuaCollision, LineTracer_aabb_hit) { check("Collision_LineTracer_aabb_hit"); }
TEST_F(LuaCollision, LineTracer_aabb_miss) { check("Collision_LineTracer_aabb_miss"); }
TEST_F(LuaCollision, LineTracer_obb_hit) { check("Collision_LineTracer_obb_hit"); }
TEST_F(LuaCollision, ConvexCollider_vertices_support) { check("Collision_ConvexCollider_vertices_and_support"); }
TEST_F(LuaCollision, Gjk_support_vertex) { check("Collision_Gjk_support_vertex"); }
TEST_F(LuaCollision, Gjk_collide_true) { check("Collision_Gjk_collide_true"); }
TEST_F(LuaCollision, Gjk_collide_false) { check("Collision_Gjk_collide_false"); }
TEST_F(LuaCollision, Epa_solve_hit) { check("Collision_Epa_solve_hit"); }
TEST_F(LuaCollision, Epa_solve_miss) { check("Collision_Epa_solve_miss"); }
+58
View File
@@ -0,0 +1,58 @@
//
// Created by orange on 07.03.2026.
//
#include <gtest/gtest.h>
#include <lua.hpp>
#include <omath/lua/lua.hpp>
class LuaHud : public ::testing::Test
{
protected:
lua_State* L = nullptr;
void SetUp() override
{
L = luaL_newstate();
luaL_openlibs(L);
omath::lua::LuaInterpreter::register_lib(L);
if (luaL_dofile(L, LUA_SCRIPTS_DIR "/hud_tests.lua") != LUA_OK)
FAIL() << lua_tostring(L, -1);
}
void TearDown() override { lua_close(L); }
void check(const char* func_name)
{
lua_getglobal(L, func_name);
if (lua_pcall(L, 0, 0, 0) != LUA_OK)
{
FAIL() << lua_tostring(L, -1);
lua_pop(L, 1);
}
}
};
TEST_F(LuaHud, CanvasBox_default_ratio) { check("Hud_CanvasBox_default_ratio"); }
TEST_F(LuaHud, CanvasBox_custom_ratio) { check("Hud_CanvasBox_custom_ratio"); }
TEST_F(LuaHud, CanvasBox_as_table) { check("Hud_CanvasBox_as_table"); }
TEST_F(LuaHud, Renderer_callbacks) { check("Hud_Renderer_callbacks"); }
TEST_F(LuaHud, Renderer_polyline_table) { check("Hud_Renderer_polyline_table"); }
TEST_F(LuaHud, Renderer_circle_defaults) { check("Hud_Renderer_circle_defaults"); }
TEST_F(LuaHud, EntityOverlay_add_2d_box) { check("Hud_EntityOverlay_add_2d_box"); }
TEST_F(LuaHud, EntityOverlay_add_cornered_2d_box) { check("Hud_EntityOverlay_add_cornered_2d_box"); }
TEST_F(LuaHud, EntityOverlay_add_dashed_box) { check("Hud_EntityOverlay_add_dashed_box"); }
TEST_F(LuaHud, EntityOverlay_add_bar) { check("Hud_EntityOverlay_add_bar"); }
TEST_F(LuaHud, EntityOverlay_add_dashed_bar) { check("Hud_EntityOverlay_add_dashed_bar"); }
TEST_F(LuaHud, EntityOverlay_add_label) { check("Hud_EntityOverlay_add_label"); }
TEST_F(LuaHud, EntityOverlay_add_outlined_label) { check("Hud_EntityOverlay_add_outlined_label"); }
TEST_F(LuaHud, EntityOverlay_add_centered_label) { check("Hud_EntityOverlay_add_centered_label"); }
TEST_F(LuaHud, EntityOverlay_add_spaces) { check("Hud_EntityOverlay_add_spaces"); }
TEST_F(LuaHud, EntityOverlay_add_progress_ring) { check("Hud_EntityOverlay_add_progress_ring"); }
TEST_F(LuaHud, EntityOverlay_add_progress_ring_defaults) { check("Hud_EntityOverlay_add_progress_ring_defaults"); }
TEST_F(LuaHud, EntityOverlay_add_icon) { check("Hud_EntityOverlay_add_icon"); }
TEST_F(LuaHud, EntityOverlay_add_snap_line) { check("Hud_EntityOverlay_add_snap_line"); }
TEST_F(LuaHud, EntityOverlay_add_skeleton) { check("Hud_EntityOverlay_add_skeleton"); }
TEST_F(LuaHud, EntityOverlay_add_scan_marker) { check("Hud_EntityOverlay_add_scan_marker"); }
TEST_F(LuaHud, EntityOverlay_add_aim_dot) { check("Hud_EntityOverlay_add_aim_dot"); }
TEST_F(LuaHud, EntityOverlay_add_projectile_aim_circle) { check("Hud_EntityOverlay_add_projectile_aim_circle"); }
TEST_F(LuaHud, EntityOverlay_add_projectile_aim_square_default) { check("Hud_EntityOverlay_add_projectile_aim_square_default"); }
+129
View File
@@ -0,0 +1,129 @@
//
// Created by orange on 07.03.2026.
//
#include <gtest/gtest.h>
#include <lua.hpp>
#include <omath/lua/lua.hpp>
class LuaMat : public ::testing::Test
{
protected:
lua_State* L = nullptr;
void SetUp() override
{
L = luaL_newstate();
luaL_openlibs(L);
omath::lua::LuaInterpreter::register_lib(L);
if (luaL_dofile(L, LUA_SCRIPTS_DIR "/mat_tests.lua") != LUA_OK)
FAIL() << lua_tostring(L, -1);
}
void TearDown() override
{
lua_close(L);
}
void check(const char* func_name)
{
lua_getglobal(L, func_name);
if (lua_pcall(L, 0, 0, 0) != LUA_OK)
{
FAIL() << lua_tostring(L, -1);
lua_pop(L, 1);
}
}
};
TEST_F(LuaMat, Constructor_default)
{
check("Mat4_Constructor_default");
}
TEST_F(LuaMat, FromRows)
{
check("Mat4_FromRows");
}
TEST_F(LuaMat, SetAt)
{
check("Mat4_SetAt");
}
TEST_F(LuaMat, SetAndClear)
{
check("Mat4_SetAndClear");
}
TEST_F(LuaMat, Multiplication_matrix)
{
check("Mat4_Multiplication_matrix");
}
TEST_F(LuaMat, Multiplication_scalar)
{
check("Mat4_Multiplication_scalar");
}
TEST_F(LuaMat, Multiplication_scalar_reversed)
{
check("Mat4_Multiplication_scalar_reversed");
}
TEST_F(LuaMat, Division_scalar)
{
check("Mat4_Division_scalar");
}
TEST_F(LuaMat, Transposed)
{
check("Mat4_Transposed");
}
TEST_F(LuaMat, Determinant)
{
check("Mat4_Determinant");
}
TEST_F(LuaMat, Inverted_success)
{
check("Mat4_Inverted_success");
}
TEST_F(LuaMat, Inverted_singular)
{
check("Mat4_Inverted_singular");
}
TEST_F(LuaMat, ToScreenMat)
{
check("Mat4_ToScreenMat");
}
TEST_F(LuaMat, Translation)
{
check("Mat4_Translation");
}
TEST_F(LuaMat, Scale)
{
check("Mat4_Scale");
}
TEST_F(LuaMat, Perspective)
{
check("Mat4_Perspective");
}
TEST_F(LuaMat, AsTable)
{
check("Mat4_AsTable");
}
TEST_F(LuaMat, ToString)
{
check("Mat4_ToString");
}
TEST_F(LuaMat, Mat3_FromRows)
{
check("Mat3_FromRows");
}
TEST_F(LuaMat, Mat4ColumnMajor_FromRows)
{
check("Mat4ColumnMajor_FromRows");
}
TEST_F(LuaMat, Mat4ColumnMajor_ToScreenMat)
{
check("Mat4ColumnMajor_ToScreenMat");
}
TEST_F(LuaMat, Mat4ColumnMajor_Translation)
{
check("Mat4ColumnMajor_Translation");
}
TEST_F(LuaMat, Mat4d_FromRows)
{
check("Mat4d_FromRows");
}
+125
View File
@@ -0,0 +1,125 @@
//
// Created by orange on 07.03.2026.
//
#include <gtest/gtest.h>
#include <lua.hpp>
#include <omath/lua/lua.hpp>
class LuaQuaternion : public ::testing::Test
{
protected:
lua_State* L = nullptr;
void SetUp() override
{
L = luaL_newstate();
luaL_openlibs(L);
omath::lua::LuaInterpreter::register_lib(L);
if (luaL_dofile(L, LUA_SCRIPTS_DIR "/quaternion_tests.lua") != LUA_OK)
FAIL() << lua_tostring(L, -1);
}
void TearDown() override
{
lua_close(L);
}
void check(const char* func_name)
{
lua_getglobal(L, func_name);
if (lua_pcall(L, 0, 0, 0) != LUA_OK)
{
FAIL() << lua_tostring(L, -1);
lua_pop(L, 1);
}
}
};
TEST_F(LuaQuaternion, Constructor_default)
{
check("Quaternion_Constructor_default");
}
TEST_F(LuaQuaternion, Constructor_xyzw)
{
check("Quaternion_Constructor_xyzw");
}
TEST_F(LuaQuaternion, Field_mutation)
{
check("Quaternion_Field_mutation");
}
TEST_F(LuaQuaternion, FromAxisAngle_zero_angle)
{
check("Quaternion_FromAxisAngle_zero_angle");
}
TEST_F(LuaQuaternion, Addition)
{
check("Quaternion_Addition");
}
TEST_F(LuaQuaternion, Multiplication_scalar)
{
check("Quaternion_Multiplication_scalar");
}
TEST_F(LuaQuaternion, Multiplication_scalar_reversed)
{
check("Quaternion_Multiplication_scalar_reversed");
}
TEST_F(LuaQuaternion, Multiplication_quaternion)
{
check("Quaternion_Multiplication_quaternion");
}
TEST_F(LuaQuaternion, UnaryMinus)
{
check("Quaternion_UnaryMinus");
}
TEST_F(LuaQuaternion, EqualTo_true)
{
check("Quaternion_EqualTo_true");
}
TEST_F(LuaQuaternion, EqualTo_false)
{
check("Quaternion_EqualTo_false");
}
TEST_F(LuaQuaternion, ToString)
{
check("Quaternion_ToString");
}
TEST_F(LuaQuaternion, Conjugate)
{
check("Quaternion_Conjugate");
}
TEST_F(LuaQuaternion, Dot)
{
check("Quaternion_Dot");
}
TEST_F(LuaQuaternion, Length)
{
check("Quaternion_Length");
}
TEST_F(LuaQuaternion, LengthSqr)
{
check("Quaternion_LengthSqr");
}
TEST_F(LuaQuaternion, Normalized)
{
check("Quaternion_Normalized");
}
TEST_F(LuaQuaternion, Inverse)
{
check("Quaternion_Inverse");
}
TEST_F(LuaQuaternion, Rotate)
{
check("Quaternion_Rotate");
}
TEST_F(LuaQuaternion, ToRotationMatrix3)
{
check("Quaternion_ToRotationMatrix3");
}
TEST_F(LuaQuaternion, ToRotationMatrix4)
{
check("Quaternion_ToRotationMatrix4");
}
TEST_F(LuaQuaternion, AsTable)
{
check("Quaternion_AsTable");
}
@@ -74,6 +74,12 @@ TEST_F(LuaSourceEngine, Camera_look_at) { check("Source_Camera_l
TEST_F(LuaSourceEngine, Camera_get_forward) { check("Source_Camera_get_forward"); }
TEST_F(LuaSourceEngine, Camera_get_right) { check("Source_Camera_get_right"); }
TEST_F(LuaSourceEngine, Camera_get_up) { check("Source_Camera_get_up"); }
TEST_F(LuaSourceEngine, Camera_get_view_matrix) { check("Source_Camera_get_view_matrix"); }
TEST_F(LuaSourceEngine, Camera_get_projection_matrix) { check("Source_Camera_get_projection_matrix"); }
TEST_F(LuaSourceEngine, Camera_get_view_projection_matrix) { check("Source_Camera_get_view_projection_matrix"); }
TEST_F(LuaSourceEngine, Camera_extract_projection_params) { check("Source_Camera_extract_projection_params"); }
TEST_F(LuaSourceEngine, Camera_calc_view_angles_from_view_matrix) { check("Source_Camera_calc_view_angles_from_view_matrix"); }
TEST_F(LuaSourceEngine, Camera_calc_origin_from_view_matrix) { check("Source_Camera_calc_origin_from_view_matrix"); }
TEST_F(LuaSourceEngine, Camera_world_to_screen_success) { check("Source_Camera_world_to_screen_success"); }
TEST_F(LuaSourceEngine, Camera_world_to_screen_error) { check("Source_Camera_world_to_screen_error"); }
TEST_F(LuaSourceEngine, Camera_screen_to_world) { check("Source_Camera_screen_to_world"); }
+26
View File
@@ -16,6 +16,15 @@
}
],
"features": {
"all": {
"description": "Enable all additional features",
"dependencies": [
{
"name": "omath",
"features": ["imgui", "lua", "hooking"]
}
]
},
"avx2": {
"description": "omath will use AVX2 to boost performance",
"supports": "!arm"
@@ -26,15 +35,32 @@
"benchmark"
]
},
"hooking": {
"description": "Add interface for automatic hooking of DirectX",
"dependencies": [
"safetyhook"
],
"supports": "(windows | linux) & !arm & !uwp"
},
"examples": {
"description": "Build examples",
"dependencies": [
"glfw3",
"glew",
"opengl",
{
"name": "omath",
"features": ["hooking"],
"platform": "windows & !arm & !uwp"
},
{
"name": "imgui",
"features": ["glfw-binding", "opengl3-binding"]
},
{
"name": "imgui",
"features": ["dx9-binding", "dx11-binding", "dx12-binding", "win32-binding"],
"platform": "windows & !arm & !uwp"
}
]
},