Compare commits

...

90 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
orange 580f39210e added typecasting for vectors 2026-04-26 02:14:14 +03:00
orange 3e6b0e7180 fixe IW engine 2026-04-26 01:55:16 +03:00
orange e1d6c38a8e Merge pull request #186 from orange-cpp/feauture/camera_numeric_template
Feauture/camera numeric template
2026-04-25 21:48:06 +03:00
orange 77a8770aee patch 2026-04-25 21:31:59 +03:00
orange 9234704010 fixed projectile prediction for double 2026-04-25 21:05:00 +03:00
orange 4c65781c6f improvement 2026-04-25 05:34:53 +03:00
orange 29b49685be fix 2026-04-25 05:29:59 +03:00
orange 92582079c5 added types impl 2026-04-25 05:29:21 +03:00
orange 13c7f7eb5a fixed lua 2026-04-25 05:16:18 +03:00
orange 65cb803cfb update 2026-04-25 05:09:07 +03:00
orange 607c034be7 fix 2026-04-25 04:51:10 +03:00
orange 0487e285ef updated for unreal 2026-04-25 04:42:52 +03:00
orange 180f2f2afa added template + concept 2026-04-24 18:51:06 +03:00
orange 35bb1bc3c0 Merge pull request #185 from orange-cpp/feature/unreal-engine-projection-fix
Feature/unreal engine projection fix
2026-04-23 22:46:10 +03:00
orange e62e8672b3 fixed tests 2026-04-23 22:32:44 +03:00
orange 11c053e28c fixed rotation ordering 2026-04-23 21:24:46 +03:00
orange 56ebc47553 remove axys invertion 2026-04-23 20:02:34 +03:00
orange b3ba9eaadf updated formulas 2026-04-23 19:48:55 +03:00
orange 42a8a5a763 also fixed for source 2026-04-23 19:23:13 +03:00
orange 2eccb4023f fix 2026-04-23 18:33:00 +03:00
orange 3eb9daf10b +90 up -90 down fix for camera view angles 2026-04-23 02:05:54 +03:00
orange 27cb511510 Merge pull request #184 from orange-cpp/feature/more-tests
Feature/more tests
2026-04-20 01:50:23 +03:00
orange 27b24b5fe7 added gource script 2026-04-20 01:36:08 +03:00
orange 4186ae8d76 added more tests 2026-04-20 01:17:11 +03:00
orange 8e6e3211c2 Merge pull request #183 from orange-cpp/featue/aabb-improvement
Featue/aabb improvement
2026-04-19 23:49:25 +03:00
orange 1c0619ff7b added new methods 2026-04-19 23:20:29 +03:00
orange dfd18e96fb added aabb improvemnt 2026-04-19 23:07:58 +03:00
orange 20930c629a added method to get camera matrix 2026-04-18 15:40:38 +03:00
orange 0845a2e863 clarified interfaces 2026-04-18 12:54:37 +03:00
orange f3f454b02e Merge pull request #182 from orange-cpp/feature/camera_upgrade
Feature/camera upgrade
2026-04-15 18:59:18 +03:00
orange 0419043720 Update include/omath/engines/frostbite_engine/camera.hpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-15 03:48:03 +03:00
orange 79f64d9679 fixed unreal bug, improved interface 2026-04-15 03:38:02 +03:00
orange dbe29926dc fixed unity bug 2026-04-15 03:25:53 +03:00
orange 9d30446c55 added ability to get view angles from view matrix 2026-04-15 03:08:06 +03:00
orange ba80aebfae Merge pull request #181 from orange-cpp/feature/walk-bot
Feature/walk bot
2026-04-14 15:23:49 +03:00
orange 9c1b6d0ba3 added tests improved API 2026-04-12 21:21:23 +03:00
orange ea07d17dbb improved walkbot 2026-04-12 12:05:40 +03:00
orange bb974da0e2 improvement 2026-04-12 11:16:39 +03:00
orange fde764c1fa added code 2026-04-12 11:12:17 +03:00
va_alpatov 28e86fc355 tests hotfix 2026-04-11 20:23:08 +03:00
va_alpatov 93e7a9457a fixed pathfinding bug 2026-04-11 20:06:39 +03:00
119 changed files with 9331 additions and 812 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/=CppNotAllPathsReturnValue/@EntryIndexedValue" value="WARNING" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppObjectMemberMightNotBeInitialized/@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/=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/=CppParameterMayBeConst/@EntryIndexedValue" value="HINT" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppParameterMayBeConstPtrOrRef/@EntryIndexedValue" value="SUGGESTION" 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" /> <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) cmake_minimum_required(VERSION 3.26)
file(READ VERSION OMATH_VERSION) file(READ VERSION OMATH_VERSION)
project(omath VERSION ${OMATH_VERSION} LANGUAGES CXX) 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_COVERAGE "Enable coverage" OFF)
option(OMATH_ENABLE_FORCE_INLINE option(OMATH_ENABLE_FORCE_INLINE
"Will for compiler to make some functions to be force inlined no matter what" ON) "Will for compiler to make some functions to be force inlined no matter what" ON)
option(OMATH_ENABLE_LUA option(OMATH_ENABLE_LUA
"omath bindings for lua" OFF) "omath bindings for lua" OFF)
option(OMATH_ENABLE_HOOKING "omath will HooksManager that can hook DirectX/OpenGL automatically" OFF)
if(VCPKG_MANIFEST_FEATURES) if(VCPKG_MANIFEST_FEATURES)
foreach(omath_feature IN LISTS VCPKG_MANIFEST_FEATURES) foreach(omath_feature IN LISTS VCPKG_MANIFEST_FEATURES)
if(omath_feature STREQUAL "imgui") if(omath_feature STREQUAL "imgui")
@@ -48,6 +48,8 @@ if(VCPKG_MANIFEST_FEATURES)
set(OMATH_BUILD_EXAMPLES ON) set(OMATH_BUILD_EXAMPLES ON)
elseif(omath_feature STREQUAL "lua") elseif(omath_feature STREQUAL "lua")
set(OMATH_ENABLE_LUA ON) set(OMATH_ENABLE_LUA ON)
elseif(omath_feature STREQUAL "hooking")
set(OMATH_ENABLE_HOOKING ON)
endif() endif()
endforeach() endforeach()
@@ -80,6 +82,10 @@ if(${PROJECT_IS_TOP_LEVEL})
message(STATUS "[${PROJECT_NAME}]: Lua feature status ${OMATH_ENABLE_LUA}") message(STATUS "[${PROJECT_NAME}]: Lua feature status ${OMATH_ENABLE_LUA}")
endif() 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_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/source/*.cpp")
file(GLOB_RECURSE OMATH_HEADERS CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/include/*.hpp") 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}) target_include_directories(${PROJECT_NAME} PRIVATE ${SOL2_INCLUDE_DIRS})
endif () 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}) add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME})
target_compile_definitions(${PROJECT_NAME} PUBLIC OMATH_VERSION="${PROJECT_VERSION}") target_compile_definitions(${PROJECT_NAME} PUBLIC OMATH_VERSION="${PROJECT_VERSION}")
@@ -147,10 +167,6 @@ set_target_properties(
CXX_STANDARD 23 CXX_STANDARD 23
CXX_STANDARD_REQUIRED ON) 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(OMATH_USE_AVX2)
if(MSVC) if(MSVC)
+10 -6
View File
@@ -56,7 +56,9 @@
"hidden": true, "hidden": true,
"inherits": ["windows-base", "vcpkg-base"], "inherits": ["windows-base", "vcpkg-base"],
"cacheVariables": { "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" "strategy": "external"
}, },
"cacheVariables": { "cacheVariables": {
"VCPKG_TARGET_TRIPLET": "x86-windows", "VCPKG_TARGET_TRIPLET": "x86-windows-static",
"VCPKG_HOST_TRIPLET": "x64-windows", "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" "strategy": "external"
}, },
"cacheVariables": { "cacheVariables": {
"VCPKG_TARGET_TRIPLET": "arm64-windows", "VCPKG_TARGET_TRIPLET": "arm64-windows-static",
"VCPKG_HOST_TRIPLET": "arm64-windows", "VCPKG_HOST_TRIPLET": "arm64-windows-static",
"VCPKG_MANIFEST_FEATURES": "tests;imgui;examples" "VCPKG_MANIFEST_FEATURES": "tests;imgui;examples",
"OMATH_STATIC_MSVC_RUNTIME_LIBRARY": "ON"
} }
}, },
{ {
+1 -1
View File
@@ -1 +1 @@
5.0.0 5.2.0
+3 -3
View File
@@ -12,11 +12,11 @@ constexpr float hit_distance_tolerance = 5.f;
void source_engine_projectile_prediction(benchmark::State& state) void source_engine_projectile_prediction(benchmark::State& state)
{ {
constexpr Target target{.m_origin = {100, 0, 90}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; constexpr Target<float> target{.m_origin = {100, 0, 90}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
constexpr Projectile projectile = {.m_origin = {3, 2, 1}, .m_launch_speed = 5000, .m_gravity_scale = 0.4}; constexpr Projectile<float> projectile = {.m_origin = {3, 2, 1}, .m_launch_speed = 5000.f, .m_gravity_scale = 0.4f};
for ([[maybe_unused]] const auto _: state) for ([[maybe_unused]] const auto _: state)
std::ignore = ProjPredEngineLegacy(400, simulation_time_step, 50, hit_distance_tolerance) std::ignore = ProjPredEngineLegacy<>(400.f, simulation_time_step, 50.f, hit_distance_tolerance)
.maybe_calculate_aim_point(projectile, target); .maybe_calculate_aim_point(projectile, target);
} }
+14
View File
@@ -4,6 +4,20 @@ add_subdirectory(example_proj_mat_builder)
add_subdirectory(example_signature_scan) add_subdirectory(example_signature_scan)
add_subdirectory(example_hud) 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) if(OMATH_ENABLE_VALGRIND)
omath_setup_valgrind(example_projection_matrix_builder) omath_setup_valgrind(example_projection_matrix_builder)
omath_setup_valgrind(example_signature_scan) 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;
}
+6 -6
View File
@@ -318,22 +318,22 @@ int main()
glfwPollEvents(); glfwPollEvents();
omath::Vector3<float> move_dir; omath::Vector3<float> move_dir;
if (glfwGetKey(window, GLFW_KEY_W)) if (glfwGetKey(window, GLFW_KEY_W))
move_dir += camera.get_forward(); move_dir += camera.get_abs_forward();
if (glfwGetKey(window, GLFW_KEY_A)) if (glfwGetKey(window, GLFW_KEY_A))
move_dir -= camera.get_right(); move_dir -= camera.get_abs_right();
if (glfwGetKey(window, GLFW_KEY_S)) if (glfwGetKey(window, GLFW_KEY_S))
move_dir -= camera.get_forward(); move_dir -= camera.get_abs_forward();
if (glfwGetKey(window, GLFW_KEY_D)) if (glfwGetKey(window, GLFW_KEY_D))
move_dir += camera.get_right(); move_dir += camera.get_abs_right();
if (glfwGetKey(window, GLFW_KEY_SPACE)) if (glfwGetKey(window, GLFW_KEY_SPACE))
move_dir += camera.get_up(); move_dir += camera.get_abs_up();
if (glfwGetKey(window, GLFW_KEY_LEFT_CONTROL)) if (glfwGetKey(window, GLFW_KEY_LEFT_CONTROL))
move_dir -= camera.get_up(); move_dir -= camera.get_abs_up();
auto delta = glfwGetTime() - old_mouse_time; auto delta = glfwGetTime() - old_mouse_time;
+3 -2
View File
@@ -82,6 +82,7 @@ namespace imgui_desktop::gui
ImGui::Checkbox("Dashed", &m_show_dashed_box); ImGui::Checkbox("Dashed", &m_show_dashed_box);
ImGui::ColorEdit4("Color##box", reinterpret_cast<float*>(&m_box_color), ImGuiColorEditFlags_NoInputs); 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("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("Thickness", &m_box_thickness, 0.5f, 5.f);
ImGui::SliderFloat("Corner ratio", &m_corner_ratio, 0.05f, 0.5f); ImGui::SliderFloat("Corner ratio", &m_corner_ratio, 0.05f, 0.5f);
ImGui::Separator(); ImGui::Separator();
@@ -199,9 +200,9 @@ namespace imgui_desktop::gui
std::make_shared<omath::hud::ImguiHudRenderer>()) std::make_shared<omath::hud::ImguiHudRenderer>())
.contents( .contents(
// ── Boxes ──────────────────────────────────────────────────── // ── 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, 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}), when(m_show_dashed_box, DashedBox{m_dash_color, m_dash_len, m_dash_gap, m_dash_thickness}),
RightSide{ RightSide{
when(m_show_right_bar, bar), when(m_show_right_bar, bar),
+1
View File
@@ -31,6 +31,7 @@ namespace imgui_desktop::gui
// Box // Box
omath::Color m_box_color{1.f, 1.f, 1.f, 1.f}; 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_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; 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; 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;
}
+39
View File
@@ -7,6 +7,8 @@
namespace omath::primitives namespace omath::primitives
{ {
enum class UpAxis { X, Y, Z };
template<class Type> template<class Type>
struct Aabb final struct Aabb final
{ {
@@ -24,5 +26,42 @@ namespace omath::primitives
{ {
return (max - min) / static_cast<Type>(2); return (max - min) / static_cast<Type>(2);
} }
template<UpAxis Up = UpAxis::Y>
[[nodiscard]]
constexpr Vector3<Type> top() const noexcept
{
const auto aabb_center = center();
if constexpr (Up == UpAxis::Z)
return {aabb_center.x, aabb_center.y, max.z};
else if constexpr (Up == UpAxis::X)
return {max.x, aabb_center.y, aabb_center.z};
else if constexpr (Up == UpAxis::Y)
return {aabb_center.x, max.y, aabb_center.z};
else
std::unreachable();
}
template<UpAxis Up = UpAxis::Y>
[[nodiscard]]
constexpr Vector3<Type> bottom() const noexcept
{
const auto aabb_center = center();
if constexpr (Up == UpAxis::Z)
return {aabb_center.x, aabb_center.y, min.z};
else if constexpr (Up == UpAxis::X)
return {min.x, aabb_center.y, aabb_center.z};
else if constexpr (Up == UpAxis::Y)
return {aabb_center.x, min.y, aabb_center.z};
else
std::unreachable();
}
[[nodiscard]]
constexpr bool is_collide(const Aabb& other) const noexcept
{
return min.x <= other.max.x && max.x >= other.min.x &&
min.y <= other.max.y && max.y >= other.min.y &&min.z <= other.max.z && max.z >= other.min.z;
}
}; };
} // namespace omath::primitives } // namespace omath::primitives
+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 struct Params final
{ {
int max_iterations{64}; 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. // Precondition: simplex.size()==4 and contains the origin.
[[nodiscard]] [[nodiscard]]
+57
View File
@@ -4,6 +4,7 @@
#pragma once #pragma once
#include "omath/3d_primitives/aabb.hpp" #include "omath/3d_primitives/aabb.hpp"
#include "omath/3d_primitives/obb.hpp"
#include "omath/linear_algebra/triangle.hpp" #include "omath/linear_algebra/triangle.hpp"
#include "omath/linear_algebra/vector3.hpp" #include "omath/linear_algebra/vector3.hpp"
@@ -36,6 +37,7 @@ namespace omath::collision
{ {
using TriangleType = Triangle<typename RayType::VectorType>; using TriangleType = Triangle<typename RayType::VectorType>;
using AABBType = primitives::Aabb<typename RayType::VectorType::ContainedType>; using AABBType = primitives::Aabb<typename RayType::VectorType::ContainedType>;
using OBBType = primitives::Obb<typename RayType::VectorType::ContainedType>;
public: public:
LineTracer() = delete; LineTracer() = delete;
@@ -137,6 +139,61 @@ namespace omath::collision
return ray.start + dir * t_hit; 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> template<class MeshType>
[[nodiscard]] [[nodiscard]]
constexpr static auto get_ray_hit_point(const RayType& ray, const MeshType& mesh) noexcept constexpr static auto get_ray_hit_point(const RayType& ray, const MeshType& mesh) noexcept
+1 -1
View File
@@ -9,5 +9,5 @@
namespace omath::cry_engine namespace omath::cry_engine
{ {
using Camera = projection::Camera<Mat4X4, ViewAngles, CameraTrait, false, NDCDepthRange::ZERO_TO_ONE>; using Camera = projection::Camera<Mat4X4, ViewAngles, CameraTrait, NDCDepthRange::ZERO_TO_ONE>;
} // namespace omath::cry_engine } // namespace omath::cry_engine
@@ -15,7 +15,7 @@ namespace omath::cry_engine
constexpr Vector3<float> k_abs_forward = {0, 1, 0}; constexpr Vector3<float> k_abs_forward = {0, 1, 0};
using Mat4X4 = Mat<4, 4, float, MatStoreType::ROW_MAJOR>; 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 Mat1X3 = Mat<1, 3, float, MatStoreType::ROW_MAJOR>;
using PitchAngle = Angle<float, -90.f, 90.f, AngleFlags::Clamped>; using PitchAngle = Angle<float, -90.f, 90.f, AngleFlags::Clamped>;
using YawAngle = Angle<float, -180.f, 180.f, AngleFlags::Normalized>; using YawAngle = Angle<float, -180.f, 180.f, AngleFlags::Normalized>;
+10 -1
View File
@@ -21,9 +21,18 @@ namespace omath::cry_engine
[[nodiscard]] [[nodiscard]]
Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept; 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]] [[nodiscard]]
Mat4X4 calc_perspective_projection_matrix(float field_of_view, float aspect_ratio, float near, float far, 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> template<class FloatingType>
requires std::is_floating_point_v<FloatingType> requires std::is_floating_point_v<FloatingType>
@@ -12,7 +12,7 @@ namespace omath::cry_engine
class PredEngineTrait final class PredEngineTrait final
{ {
public: public:
constexpr static Vector3<float> predict_projectile_position(const projectile_prediction::Projectile& projectile, constexpr static Vector3<float> predict_projectile_position(const projectile_prediction::Projectile<float>& projectile,
const float pitch, const float yaw, const float pitch, const float yaw,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
@@ -26,7 +26,7 @@ namespace omath::cry_engine
return current_pos; return current_pos;
} }
[[nodiscard]] [[nodiscard]]
static constexpr Vector3<float> predict_target_position(const projectile_prediction::Target& target, static constexpr Vector3<float> predict_target_position(const projectile_prediction::Target<float>& target,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
auto predicted = target.m_origin + target.m_velocity * time; auto predicted = target.m_origin + target.m_velocity * time;
@@ -49,7 +49,7 @@ namespace omath::cry_engine
} }
[[nodiscard]] [[nodiscard]]
static Vector3<float> calc_viewpoint_from_angles(const projectile_prediction::Projectile& projectile, static Vector3<float> calc_viewpoint_from_angles(const projectile_prediction::Projectile<float>& projectile,
Vector3<float> predicted_target_position, Vector3<float> predicted_target_position,
const std::optional<float> projectile_pitch) noexcept const std::optional<float> projectile_pitch) noexcept
{ {
@@ -9,5 +9,5 @@
namespace omath::frostbite_engine namespace omath::frostbite_engine
{ {
using Camera = projection::Camera<Mat4X4, ViewAngles, CameraTrait, false, NDCDepthRange::ZERO_TO_ONE>; using Camera = projection::Camera<Mat4X4, ViewAngles, CameraTrait, NDCDepthRange::ZERO_TO_ONE>;
} // namespace omath::unity_engine } // namespace omath::frostbite_engine
@@ -21,6 +21,15 @@ namespace omath::frostbite_engine
[[nodiscard]] [[nodiscard]]
Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept; 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]] [[nodiscard]]
Mat4X4 calc_perspective_projection_matrix(float field_of_view, float aspect_ratio, float near, float far, 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::NEGATIVE_ONE_TO_ONE) noexcept;
@@ -12,7 +12,7 @@ namespace omath::frostbite_engine
class PredEngineTrait final class PredEngineTrait final
{ {
public: public:
constexpr static Vector3<float> predict_projectile_position(const projectile_prediction::Projectile& projectile, constexpr static Vector3<float> predict_projectile_position(const projectile_prediction::Projectile<float>& projectile,
const float pitch, const float yaw, const float pitch, const float yaw,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
@@ -26,7 +26,7 @@ namespace omath::frostbite_engine
return current_pos; return current_pos;
} }
[[nodiscard]] [[nodiscard]]
static constexpr Vector3<float> predict_target_position(const projectile_prediction::Target& target, static constexpr Vector3<float> predict_target_position(const projectile_prediction::Target<float>& target,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
auto predicted = target.m_origin + target.m_velocity * time; auto predicted = target.m_origin + target.m_velocity * time;
@@ -49,7 +49,7 @@ namespace omath::frostbite_engine
} }
[[nodiscard]] [[nodiscard]]
static Vector3<float> calc_viewpoint_from_angles(const projectile_prediction::Projectile& projectile, static Vector3<float> calc_viewpoint_from_angles(const projectile_prediction::Projectile<float>& projectile,
Vector3<float> predicted_target_position, Vector3<float> predicted_target_position,
const std::optional<float> projectile_pitch) noexcept const std::optional<float> projectile_pitch) noexcept
{ {
+1 -1
View File
@@ -9,5 +9,5 @@
namespace omath::iw_engine namespace omath::iw_engine
{ {
using Camera = projection::Camera<Mat4X4, ViewAngles, CameraTrait, false, NDCDepthRange::ZERO_TO_ONE>; using Camera = projection::Camera<Mat4X4, ViewAngles, CameraTrait, NDCDepthRange::ZERO_TO_ONE>;
} // namespace omath::iw_engine } // namespace omath::iw_engine
@@ -19,6 +19,15 @@ namespace omath::iw_engine
[[nodiscard]] [[nodiscard]]
Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept; 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]] Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3<float>& cam_origin) noexcept;
[[nodiscard]] [[nodiscard]]
@@ -13,7 +13,7 @@ namespace omath::iw_engine
class PredEngineTrait final class PredEngineTrait final
{ {
public: public:
constexpr static Vector3<float> predict_projectile_position(const projectile_prediction::Projectile& projectile, constexpr static Vector3<float> predict_projectile_position(const projectile_prediction::Projectile<float>& projectile,
const float pitch, const float yaw, const float pitch, const float yaw,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
@@ -27,7 +27,7 @@ namespace omath::iw_engine
return current_pos; return current_pos;
} }
[[nodiscard]] [[nodiscard]]
static constexpr Vector3<float> predict_target_position(const projectile_prediction::Target& target, static constexpr Vector3<float> predict_target_position(const projectile_prediction::Target<float>& target,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
auto predicted = target.m_origin + target.m_velocity * time; auto predicted = target.m_origin + target.m_velocity * time;
@@ -50,7 +50,7 @@ namespace omath::iw_engine
} }
[[nodiscard]] [[nodiscard]]
static Vector3<float> calc_viewpoint_from_angles(const projectile_prediction::Projectile& projectile, static Vector3<float> calc_viewpoint_from_angles(const projectile_prediction::Projectile<float>& projectile,
Vector3<float> predicted_target_position, Vector3<float> predicted_target_position,
const std::optional<float> projectile_pitch) noexcept const std::optional<float> projectile_pitch) noexcept
{ {
@@ -8,5 +8,5 @@
namespace omath::opengl_engine namespace omath::opengl_engine
{ {
using Camera = projection::Camera<Mat4X4, ViewAngles, CameraTrait, true, NDCDepthRange::NEGATIVE_ONE_TO_ONE>; using Camera = projection::Camera<Mat4X4, ViewAngles, CameraTrait, NDCDepthRange::NEGATIVE_ONE_TO_ONE, {.inverted_forward = true}>;
} // namespace omath::opengl_engine } // namespace omath::opengl_engine
@@ -20,6 +20,15 @@ namespace omath::opengl_engine
[[nodiscard]] [[nodiscard]]
Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept; 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]] [[nodiscard]]
Mat4X4 calc_perspective_projection_matrix(float field_of_view, float aspect_ratio, float near, float far, 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::NEGATIVE_ONE_TO_ONE) noexcept;
@@ -12,7 +12,7 @@ namespace omath::opengl_engine
class PredEngineTrait final class PredEngineTrait final
{ {
public: public:
constexpr static Vector3<float> predict_projectile_position(const projectile_prediction::Projectile& projectile, constexpr static Vector3<float> predict_projectile_position(const projectile_prediction::Projectile<float>& projectile,
const float pitch, const float yaw, const float pitch, const float yaw,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
@@ -26,7 +26,7 @@ namespace omath::opengl_engine
return current_pos; return current_pos;
} }
[[nodiscard]] [[nodiscard]]
static constexpr Vector3<float> predict_target_position(const projectile_prediction::Target& target, static constexpr Vector3<float> predict_target_position(const projectile_prediction::Target<float>& target,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
auto predicted = target.m_origin + target.m_velocity * time; auto predicted = target.m_origin + target.m_velocity * time;
@@ -49,7 +49,7 @@ namespace omath::opengl_engine
} }
[[nodiscard]] [[nodiscard]]
static Vector3<float> calc_viewpoint_from_angles(const projectile_prediction::Projectile& projectile, static Vector3<float> calc_viewpoint_from_angles(const projectile_prediction::Projectile<float>& projectile,
Vector3<float> predicted_target_position, Vector3<float> predicted_target_position,
const std::optional<float> projectile_pitch) noexcept const std::optional<float> projectile_pitch) noexcept
{ {
@@ -7,5 +7,5 @@
#include "traits/camera_trait.hpp" #include "traits/camera_trait.hpp"
namespace omath::source_engine namespace omath::source_engine
{ {
using Camera = projection::Camera<Mat4X4, ViewAngles, CameraTrait, false, NDCDepthRange::ZERO_TO_ONE>; using Camera = projection::Camera<Mat4X4, ViewAngles, CameraTrait, NDCDepthRange::ZERO_TO_ONE>;
} // namespace omath::source_engine } // namespace omath::source_engine
@@ -12,6 +12,15 @@ namespace omath::source_engine
[[nodiscard]] [[nodiscard]]
Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept; 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]] [[nodiscard]]
Vector3<float> right_vector(const ViewAngles& angles) noexcept; Vector3<float> right_vector(const ViewAngles& angles) noexcept;
@@ -13,7 +13,7 @@ namespace omath::source_engine
class PredEngineTrait final class PredEngineTrait final
{ {
public: public:
constexpr static Vector3<float> predict_projectile_position(const projectile_prediction::Projectile& projectile, constexpr static Vector3<float> predict_projectile_position(const projectile_prediction::Projectile<float>& projectile,
const float pitch, const float yaw, const float pitch, const float yaw,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
@@ -27,7 +27,7 @@ namespace omath::source_engine
return current_pos; return current_pos;
} }
[[nodiscard]] [[nodiscard]]
static constexpr Vector3<float> predict_target_position(const projectile_prediction::Target& target, static constexpr Vector3<float> predict_target_position(const projectile_prediction::Target<float>& target,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
auto predicted = target.m_origin + target.m_velocity * time; auto predicted = target.m_origin + target.m_velocity * time;
@@ -50,7 +50,7 @@ namespace omath::source_engine
} }
[[nodiscard]] [[nodiscard]]
static Vector3<float> calc_viewpoint_from_angles(const projectile_prediction::Projectile& projectile, static Vector3<float> calc_viewpoint_from_angles(const projectile_prediction::Projectile<float>& projectile,
Vector3<float> predicted_target_position, Vector3<float> predicted_target_position,
const std::optional<float> projectile_pitch) noexcept const std::optional<float> projectile_pitch) noexcept
{ {
@@ -9,5 +9,5 @@
namespace omath::unity_engine namespace omath::unity_engine
{ {
using Camera = projection::Camera<Mat4X4, ViewAngles, CameraTrait, false, NDCDepthRange::ZERO_TO_ONE>; using Camera = projection::Camera<Mat4X4, ViewAngles, CameraTrait, NDCDepthRange::ZERO_TO_ONE, {.inverted_forward = true}>;
} // namespace omath::unity_engine } // namespace omath::unity_engine
@@ -21,6 +21,15 @@ namespace omath::unity_engine
[[nodiscard]] [[nodiscard]]
Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept; 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]] [[nodiscard]]
Mat4X4 calc_perspective_projection_matrix(float field_of_view, float aspect_ratio, float near, float far, 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::NEGATIVE_ONE_TO_ONE) noexcept;
@@ -12,7 +12,7 @@ namespace omath::unity_engine
class PredEngineTrait final class PredEngineTrait final
{ {
public: public:
constexpr static Vector3<float> predict_projectile_position(const projectile_prediction::Projectile& projectile, constexpr static Vector3<float> predict_projectile_position(const projectile_prediction::Projectile<float>& projectile,
const float pitch, const float yaw, const float pitch, const float yaw,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
@@ -26,7 +26,7 @@ namespace omath::unity_engine
return current_pos; return current_pos;
} }
[[nodiscard]] [[nodiscard]]
static constexpr Vector3<float> predict_target_position(const projectile_prediction::Target& target, static constexpr Vector3<float> predict_target_position(const projectile_prediction::Target<float>& target,
const float time, const float gravity) noexcept const float time, const float gravity) noexcept
{ {
auto predicted = target.m_origin + target.m_velocity * time; auto predicted = target.m_origin + target.m_velocity * time;
@@ -49,7 +49,7 @@ namespace omath::unity_engine
} }
[[nodiscard]] [[nodiscard]]
static Vector3<float> calc_viewpoint_from_angles(const projectile_prediction::Projectile& projectile, static Vector3<float> calc_viewpoint_from_angles(const projectile_prediction::Projectile<float>& projectile,
Vector3<float> predicted_target_position, Vector3<float> predicted_target_position,
const std::optional<float> projectile_pitch) noexcept const std::optional<float> projectile_pitch) noexcept
{ {
@@ -9,5 +9,5 @@
namespace omath::unreal_engine namespace omath::unreal_engine
{ {
using Camera = projection::Camera<Mat4X4, ViewAngles, CameraTrait, false, NDCDepthRange::ZERO_TO_ONE>; using Camera = projection::Camera<Mat4X4, ViewAngles, CameraTrait, NDCDepthRange::ZERO_TO_ONE, {}, double>;
} // namespace omath::unreal_engine } // namespace omath::unreal_engine
@@ -11,16 +11,16 @@
namespace omath::unreal_engine namespace omath::unreal_engine
{ {
constexpr Vector3<float> k_abs_up = {0, 0, 1}; constexpr Vector3<double> k_abs_up = {0, 0, 1};
constexpr Vector3<float> k_abs_right = {0, 1, 0}; constexpr Vector3<double> k_abs_right = {0, 1, 0};
constexpr Vector3<float> k_abs_forward = {1, 0, 0}; constexpr Vector3<double> k_abs_forward = {1, 0, 0};
using Mat4X4 = Mat<4, 4, float, MatStoreType::ROW_MAJOR>; using Mat4X4 = Mat<4, 4, double, MatStoreType::ROW_MAJOR>;
using Mat3X3 = Mat<4, 4, float, MatStoreType::ROW_MAJOR>; using Mat3X3 = Mat<4, 4, double, MatStoreType::ROW_MAJOR>;
using Mat1X3 = Mat<1, 3, float, MatStoreType::ROW_MAJOR>; using Mat1X3 = Mat<1, 3, double, MatStoreType::ROW_MAJOR>;
using PitchAngle = Angle<float, -90.f, 90.f, AngleFlags::Clamped>; using PitchAngle = Angle<double, -90., 90., AngleFlags::Clamped>;
using YawAngle = Angle<float, -180.f, 180.f, AngleFlags::Normalized>; using YawAngle = Angle<double, -180., 180., AngleFlags::Normalized>;
using RollAngle = Angle<float, -180.f, 180.f, AngleFlags::Normalized>; using RollAngle = Angle<double, -180., 180., AngleFlags::Normalized>;
using ViewAngles = omath::ViewAngles<PitchAngle, YawAngle, RollAngle>; using ViewAngles = omath::ViewAngles<PitchAngle, YawAngle, RollAngle>;
} // namespace omath::unreal_engine } // namespace omath::unreal_engine
@@ -8,21 +8,30 @@
namespace omath::unreal_engine namespace omath::unreal_engine
{ {
[[nodiscard]] [[nodiscard]]
Vector3<float> forward_vector(const ViewAngles& angles) noexcept; Vector3<double> forward_vector(const ViewAngles& angles) noexcept;
[[nodiscard]] [[nodiscard]]
Vector3<float> right_vector(const ViewAngles& angles) noexcept; Vector3<double> right_vector(const ViewAngles& angles) noexcept;
[[nodiscard]] [[nodiscard]]
Vector3<float> up_vector(const ViewAngles& angles) noexcept; Vector3<double> up_vector(const ViewAngles& angles) noexcept;
[[nodiscard]] Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3<float>& cam_origin) noexcept; [[nodiscard]] Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3<double>& cam_origin) noexcept;
[[nodiscard]] [[nodiscard]]
Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept; Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept;
[[nodiscard]] [[nodiscard]]
Mat4X4 calc_perspective_projection_matrix(float field_of_view, float aspect_ratio, float near, float far, 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; NDCDepthRange ndc_depth_range = NDCDepthRange::NEGATIVE_ONE_TO_ONE) noexcept;
template<class FloatingType> template<class FloatingType>
@@ -12,13 +12,13 @@ namespace omath::unreal_engine
{ {
public: public:
[[nodiscard]] [[nodiscard]]
static ViewAngles calc_look_at_angle(const Vector3<float>& cam_origin, const Vector3<float>& look_at) noexcept; static ViewAngles calc_look_at_angle(const Vector3<double>& cam_origin, const Vector3<double>& look_at) noexcept;
[[nodiscard]] [[nodiscard]]
static Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3<float>& cam_origin) noexcept; static Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3<double>& cam_origin) noexcept;
[[nodiscard]] [[nodiscard]]
static Mat4X4 calc_projection_matrix(const projection::FieldOfView& fov, const projection::ViewPort& view_port, static Mat4X4 calc_projection_matrix(const projection::FieldOfView& fov, const projection::ViewPort& view_port,
float near, float far, NDCDepthRange ndc_depth_range) noexcept; double near, double far, NDCDepthRange ndc_depth_range) noexcept;
}; };
} // namespace omath::unreal_engine } // namespace omath::unreal_engine
@@ -12,67 +12,72 @@ namespace omath::unreal_engine
class PredEngineTrait final class PredEngineTrait final
{ {
public: public:
constexpr static Vector3<float> predict_projectile_position(const projectile_prediction::Projectile& projectile, static Vector3<double> predict_projectile_position(const projectile_prediction::Projectile<double>& projectile,
const float pitch, const float yaw, const double pitch, const double yaw,
const float time, const float gravity) noexcept const double time, const double gravity) noexcept
{ {
const auto launch_pos = projectile.m_origin + projectile.m_launch_offset; const auto launch_pos = projectile.m_origin + projectile.m_launch_offset;
const auto fwd_d = forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw),
RollAngle::from_degrees(0)});
auto current_pos = launch_pos auto current_pos = launch_pos
+ forward_vector({PitchAngle::from_degrees(-pitch), YawAngle::from_degrees(yaw), + Vector3<double>{fwd_d.x, fwd_d.y, fwd_d.z}
RollAngle::from_degrees(0)})
* projectile.m_launch_speed * time; * projectile.m_launch_speed * time;
current_pos.y -= (gravity * projectile.m_gravity_scale) * (time * time) * 0.5f; current_pos.y -= (gravity * projectile.m_gravity_scale) * (time * time) * 0.5;
return current_pos; return current_pos;
} }
[[nodiscard]] [[nodiscard]]
static constexpr Vector3<float> predict_target_position(const projectile_prediction::Target& target, static Vector3<double> predict_target_position(const projectile_prediction::Target<double>& target,
const float time, const float gravity) noexcept const double time, const double gravity) noexcept
{ {
auto predicted = target.m_origin + target.m_velocity * time; auto predicted = target.m_origin + target.m_velocity * time;
if (target.m_is_airborne) if (target.m_is_airborne)
predicted.y -= gravity * (time * time) * 0.5f; predicted.y -= gravity * (time * time) * 0.5;
return predicted; return predicted;
} }
[[nodiscard]] [[nodiscard]]
static float calc_vector_2d_distance(const Vector3<float>& delta) noexcept static double calc_vector_2d_distance(const Vector3<double>& delta) noexcept
{ {
return std::sqrt(delta.x * delta.x + delta.z * delta.z); return std::sqrt(delta.x * delta.x + delta.z * delta.z);
} }
[[nodiscard]] [[nodiscard]]
constexpr static float get_vector_height_coordinate(const Vector3<float>& vec) noexcept static double get_vector_height_coordinate(const Vector3<double>& vec) noexcept
{ {
return vec.y; return vec.y;
} }
[[nodiscard]] [[nodiscard]]
static Vector3<float> calc_viewpoint_from_angles(const projectile_prediction::Projectile& projectile, static Vector3<double> calc_viewpoint_from_angles(const projectile_prediction::Projectile<double>& projectile,
Vector3<float> predicted_target_position, Vector3<double> predicted_target_position,
const std::optional<float> projectile_pitch) noexcept const std::optional<double> projectile_pitch) noexcept
{ {
const auto delta2d = calc_vector_2d_distance(predicted_target_position - projectile.m_origin); const auto delta2d = calc_vector_2d_distance(predicted_target_position - projectile.m_origin);
const auto height = delta2d * std::tan(angles::degrees_to_radians(projectile_pitch.value())); const auto height = delta2d * std::tan(angles::degrees_to_radians(projectile_pitch.value()));
return {predicted_target_position.x, predicted_target_position.y, projectile.m_origin.z + height}; return {predicted_target_position.x, predicted_target_position.y, projectile.m_origin.z + height};
} }
// Due to specification of maybe_calculate_projectile_launch_pitch_angle, pitch angle must be: // Due to specification of maybe_calculate_projectile_launch_pitch_angle, pitch angle must be:
// 89 look up, -89 look down // 89 look up, -89 look down
[[nodiscard]] [[nodiscard]]
static float calc_direct_pitch_angle(const Vector3<float>& origin, const Vector3<float>& view_to) noexcept static double calc_direct_pitch_angle(const Vector3<double>& origin, const Vector3<double>& view_to) noexcept
{ {
const auto direction = (view_to - origin).normalized(); const auto direction = (view_to - origin).normalized();
return angles::radians_to_degrees(std::asin(direction.z)); return angles::radians_to_degrees(std::asin(direction.z));
} }
[[nodiscard]] [[nodiscard]]
static float calc_direct_yaw_angle(const Vector3<float>& origin, const Vector3<float>& view_to) noexcept static double calc_direct_yaw_angle(const Vector3<double>& origin, const Vector3<double>& view_to) noexcept
{ {
const auto direction = (view_to - origin).normalized(); const auto direction = (view_to - origin).normalized();
return angles::radians_to_degrees(std::atan2(direction.y, direction.x)); return angles::radians_to_degrees(std::atan2(direction.y, direction.x));
}; }
}; };
} // namespace omath::unreal_engine } // namespace omath::unreal_engine
+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 ──────────────────────────────────────────────────────── // ── Boxes ────────────────────────────────────────────────────────
EntityOverlay& add_2d_box(const Color& box_color, const Color& fill_color = Color{0.f, 0.f, 0.f, 0.f}, 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}, 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); 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, 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 color;
Color fill{0.f, 0.f, 0.f, 0.f}; Color fill{0.f, 0.f, 0.f, 0.f};
Color outline{0.f, 0.f, 0.f, 0.f};
float thickness = 1.f; float thickness = 1.f;
}; };
@@ -33,6 +34,7 @@ namespace omath::hud::widget
{ {
Color color; Color color;
Color fill{0.f, 0.f, 0.f, 0.f}; 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 corner_ratio = 0.2f;
float thickness = 1.f; float thickness = 1.f;
}; };
+169 -67
View File
@@ -5,6 +5,7 @@
#include "vector3.hpp" #include "vector3.hpp"
#include <algorithm> #include <algorithm>
#include <array> #include <array>
#include <cmath>
#include <iomanip> #include <iomanip>
#include <numeric> #include <numeric>
#include <sstream> #include <sstream>
@@ -58,7 +59,7 @@ namespace omath
clear(); clear();
} }
[[nodiscard]] [[nodiscard("You must use store ordering")]]
consteval static MatStoreType get_store_ordering() noexcept consteval static MatStoreType get_store_ordering() noexcept
{ {
return StoreType; return StoreType;
@@ -93,13 +94,13 @@ namespace omath
m_data = other.m_data; m_data = other.m_data;
} }
[[nodiscard]] [[nodiscard("You must use element reference")]]
constexpr Type& operator[](const size_t row, const size_t col) constexpr Type& operator[](const size_t row, const size_t col)
{ {
return at(row, 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 constexpr const Type& operator[](const size_t row, const size_t col) const
{ {
return at(row, col); return at(row, col);
@@ -110,25 +111,25 @@ namespace omath
m_data = std::move(other.m_data); m_data = std::move(other.m_data);
} }
[[nodiscard]] [[nodiscard("You must use row count")]]
static constexpr size_t row_count() noexcept static constexpr size_t row_count() noexcept
{ {
return Rows; return Rows;
} }
[[nodiscard]] [[nodiscard("You must use column count")]]
static constexpr size_t columns_count() noexcept static constexpr size_t columns_count() noexcept
{ {
return Columns; return Columns;
} }
[[nodiscard]] [[nodiscard("You must use matrix size")]]
static consteval MatSize size() noexcept static constexpr MatSize size() noexcept
{ {
return {Rows, Columns}; 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 constexpr const Type& at(const size_t row_index, const size_t column_index) const
{ {
#if !defined(NDEBUG) && defined(OMATH_SUPRESS_SAFETY_CHECKS) #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)); 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 constexpr Type sum() const noexcept
{ {
return std::accumulate(m_data.begin(), m_data.end(), static_cast<Type>(0)); 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 // 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> constexpr Mat<Rows, OtherColumns, Type, StoreType>
operator*(const Mat<Columns, OtherColumns, Type, StoreType>& other) const operator*(const Mat<Columns, OtherColumns, Type, StoreType>& other) const
{ {
@@ -201,7 +202,7 @@ namespace omath
return *this = *this * other; return *this = *this * other;
} }
[[nodiscard]] [[nodiscard("You must use result matrix")]]
constexpr Mat operator*(const Type& value) const noexcept constexpr Mat operator*(const Type& value) const noexcept
{ {
Mat result(*this); Mat result(*this);
@@ -215,7 +216,7 @@ namespace omath
return *this; return *this;
} }
[[nodiscard]] [[nodiscard("You must use result matrix")]]
constexpr Mat operator/(const Type& value) const noexcept constexpr Mat operator/(const Type& value) const noexcept
{ {
Mat result(*this); Mat result(*this);
@@ -239,7 +240,7 @@ namespace omath
return *this; return *this;
} }
[[nodiscard]] [[nodiscard("You must use transposed matrix")]]
constexpr Mat<Columns, Rows, Type, StoreType> transposed() const noexcept constexpr Mat<Columns, Rows, Type, StoreType> transposed() const noexcept
{ {
Mat<Columns, Rows, Type, StoreType> transposed; Mat<Columns, Rows, Type, StoreType> transposed;
@@ -250,7 +251,7 @@ namespace omath
return transposed; return transposed;
} }
[[nodiscard]] [[nodiscard("You must use determinant")]]
constexpr Type determinant() const constexpr Type determinant() const
{ {
static_assert(Rows == Columns, "Determinant is only defined for square matrices."); static_assert(Rows == Columns, "Determinant is only defined for square matrices.");
@@ -271,10 +272,11 @@ namespace omath
} }
return det; return det;
} }
else // For no reason MSVC triggers on it as unreachable code so we keep else here.
std::unreachable(); 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 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); static_assert(Rows - 1 > 0 && Columns - 1 > 0);
@@ -295,32 +297,32 @@ namespace omath
return result; return result;
} }
[[nodiscard]] [[nodiscard("You must use minor")]]
constexpr Type minor(const size_t row, const size_t column) const constexpr Type minor(const size_t row, const size_t column) const
{ {
return strip(row, column).determinant(); 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 constexpr Type alg_complement(const size_t row, const size_t column) const
{ {
const auto minor_value = minor(row, column); const auto minor_value = minor(row, column);
return (row + column + 2) % 2 == 0 ? minor_value : -minor_value; 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 constexpr const std::array<Type, Rows * Columns>& raw_array() const
{ {
return m_data; return m_data;
} }
[[nodiscard]] [[nodiscard("You must use raw array")]]
constexpr std::array<Type, Rows * Columns>& raw_array() constexpr std::array<Type, Rows * Columns>& raw_array()
{ {
return m_data; return m_data;
} }
[[nodiscard]] [[nodiscard("You must use string representation")]]
std::string to_string() const noexcept std::string to_string() const noexcept
{ {
std::ostringstream oss; std::ostringstream oss;
@@ -342,14 +344,14 @@ namespace omath
return oss.str(); return oss.str();
} }
[[nodiscard]] [[nodiscard("You must use wide string representation")]]
std::wstring to_wstring() const noexcept std::wstring to_wstring() const noexcept
{ {
const auto ascii_string = to_string(); const auto ascii_string = to_string();
return {ascii_string.cbegin(), ascii_string.cend()}; return {ascii_string.cbegin(), ascii_string.cend()};
} }
[[nodiscard]] [[nodiscard("You must use UTF-8 string representation")]]
// ReSharper disable once CppInconsistentNaming // ReSharper disable once CppInconsistentNaming
std::u8string to_u8string() const noexcept std::u8string to_u8string() const noexcept
{ {
@@ -357,20 +359,20 @@ namespace omath
return {ascii_string.cbegin(), ascii_string.cend()}; return {ascii_string.cbegin(), ascii_string.cend()};
} }
[[nodiscard]] [[nodiscard("You must use comparison result")]]
bool operator==(const Mat& mat) const bool operator==(const Mat& mat) const
{ {
return m_data == mat.m_data; return m_data == mat.m_data;
} }
[[nodiscard]] [[nodiscard("You must use comparison result")]]
bool operator!=(const Mat& mat) const bool operator!=(const Mat& mat) const
{ {
return !operator==(mat); return !operator==(mat);
} }
// Static methods that return fixed-size matrices // 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 constexpr static Mat<4, 4> to_screen_mat(const Type& screen_width, const Type& screen_height) noexcept
{ {
return { return {
@@ -381,7 +383,7 @@ namespace omath
}; };
} }
[[nodiscard]] [[nodiscard("You must use inverted matrix")]]
constexpr std::optional<Mat> inverted() const constexpr std::optional<Mat> inverted() const
{ {
const auto det = determinant(); const auto det = determinant();
@@ -404,7 +406,7 @@ namespace omath
private: private:
std::array<Type, Rows * Columns> m_data; 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> constexpr Mat<Rows, OtherColumns, Type, MatStoreType::ROW_MAJOR>
cache_friendly_multiply_row_major(const Mat<Columns, OtherColumns, Type, MatStoreType::ROW_MAJOR>& other) const cache_friendly_multiply_row_major(const Mat<Columns, OtherColumns, Type, MatStoreType::ROW_MAJOR>& other) const
{ {
@@ -419,7 +421,7 @@ namespace omath
return result; 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( constexpr Mat<Rows, OtherColumns, Type, MatStoreType::COLUMN_MAJOR> cache_friendly_multiply_col_major(
const Mat<Columns, OtherColumns, Type, MatStoreType::COLUMN_MAJOR>& other) const const Mat<Columns, OtherColumns, Type, MatStoreType::COLUMN_MAJOR>& other) const
{ {
@@ -434,7 +436,7 @@ namespace omath
return result; return result;
} }
#ifdef OMATH_USE_AVX2 #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> constexpr Mat<Rows, OtherColumns, Type, MatStoreType::COLUMN_MAJOR>
avx_multiply_col_major(const Mat<Columns, OtherColumns, Type, MatStoreType::COLUMN_MAJOR>& other) const avx_multiply_col_major(const Mat<Columns, OtherColumns, Type, MatStoreType::COLUMN_MAJOR>& other) const
{ {
@@ -504,7 +506,7 @@ namespace omath
return result; 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> constexpr Mat<Rows, OtherColumns, Type, MatStoreType::ROW_MAJOR>
avx_multiply_row_major(const Mat<Columns, OtherColumns, Type, MatStoreType::ROW_MAJOR>& other) const avx_multiply_row_major(const Mat<Columns, OtherColumns, Type, MatStoreType::ROW_MAJOR>& other) const
{ {
@@ -575,20 +577,20 @@ namespace omath
#endif #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 constexpr static Mat<1, 4, Type, St> mat_row_from_vector(const Vector3<Type>& vector) noexcept
{ {
return {{vector.x, vector.y, vector.z, 1}}; 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 constexpr static Mat<4, 1, Type, St> mat_column_from_vector(const Vector3<Type>& vector) noexcept
{ {
return {{vector.x}, {vector.y}, {vector.z}, {1}}; return {{vector.x}, {vector.y}, {vector.z}, {1}};
} }
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR> 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 constexpr Mat<4, 4, Type, St> mat_translation(const Vector3<Type>& diff) noexcept
{ {
return return
@@ -600,7 +602,7 @@ namespace omath
}; };
} }
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR> 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 constexpr Mat<4, 4, Type, St> mat_scale(const Vector3<Type>& scale) noexcept
{ {
return { 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> 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 Mat<4, 4, Type, St> mat_rotation_axis_x(const Angle& angle) noexcept
{ {
return return
@@ -625,7 +674,7 @@ namespace omath
} }
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR, class Angle> 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 Mat<4, 4, Type, St> mat_rotation_axis_y(const Angle& angle) noexcept
{ {
return return
@@ -638,7 +687,7 @@ namespace omath
} }
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR, class Angle> 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 Mat<4, 4, Type, St> mat_rotation_axis_z(const Angle& angle) noexcept
{ {
return return
@@ -651,7 +700,7 @@ namespace omath
} }
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR> 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, 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 const Vector3<Type>& up, const Vector3<Type>& camera_origin) noexcept
{ {
@@ -666,50 +715,103 @@ namespace omath
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR, template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR,
NDCDepthRange DepthRange = NDCDepthRange::NEGATIVE_ONE_TO_ONE> NDCDepthRange DepthRange = NDCDepthRange::NEGATIVE_ONE_TO_ONE>
[[nodiscard]] [[nodiscard("You must use perspective matrix")]]
Mat<4, 4, Type, St> mat_perspective_left_handed(const float field_of_view, const float aspect_ratio, Mat<4, 4, Type, St> mat_perspective_left_handed_vertical_fov(const Type field_of_view, const Type aspect_ratio,
const float near, const float far) noexcept const Type near, const Type far) noexcept
{ {
const float fov_half_tan = std::tan(angles::degrees_to_radians(field_of_view) / 2.f); const auto fov_half_tan = std::tan(angles::degrees_to_radians(field_of_view) / Type{2});
if constexpr (DepthRange == NDCDepthRange::ZERO_TO_ONE) if constexpr (DepthRange == NDCDepthRange::ZERO_TO_ONE)
return {{1.f / (aspect_ratio * fov_half_tan), 0.f, 0.f, 0.f}, return {{Type{1} / (aspect_ratio * fov_half_tan), Type{0}, Type{0}, Type{0}},
{0.f, 1.f / fov_half_tan, 0.f, 0.f}, {Type{0}, Type{1} / fov_half_tan, Type{0}, Type{0}},
{0.f, 0.f, far / (far - near), -(near * far) / (far - near)}, {Type{0}, Type{0}, far / (far - near), -(near * far) / (far - near)},
{0.f, 0.f, 1.f, 0.f}}; {Type{0}, Type{0}, Type{1}, Type{0}}};
else if constexpr (DepthRange == NDCDepthRange::NEGATIVE_ONE_TO_ONE) else if constexpr (DepthRange == NDCDepthRange::NEGATIVE_ONE_TO_ONE)
return {{1.f / (aspect_ratio * fov_half_tan), 0.f, 0.f, 0.f}, return {{Type{1} / (aspect_ratio * fov_half_tan), Type{0}, Type{0}, Type{0}},
{0.f, 1.f / fov_half_tan, 0.f, 0.f}, {Type{0}, Type{1} / fov_half_tan, Type{0}, Type{0}},
{0.f, 0.f, (far + near) / (far - near), -(2.f * near * far) / (far - near)}, {Type{0}, Type{0}, (far + near) / (far - near), -(Type{2} * near * far) / (far - near)},
{0.f, 0.f, 1.f, 0.f}}; {Type{0}, Type{0}, Type{1}, Type{0}}};
else else
std::unreachable(); std::unreachable();
} }
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR, template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR,
NDCDepthRange DepthRange = NDCDepthRange::NEGATIVE_ONE_TO_ONE> NDCDepthRange DepthRange = NDCDepthRange::NEGATIVE_ONE_TO_ONE>
[[nodiscard]] [[nodiscard("You must use perspective matrix")]]
Mat<4, 4, Type, St> mat_perspective_right_handed(const float field_of_view, const float aspect_ratio, Mat<4, 4, Type, St> mat_perspective_right_handed_vertical_fov(const Type field_of_view, const Type aspect_ratio,
const float near, const float far) noexcept const Type near, const Type far) noexcept
{ {
const float fov_half_tan = std::tan(angles::degrees_to_radians(field_of_view) / 2.f); const auto fov_half_tan = std::tan(angles::degrees_to_radians(field_of_view) / Type{2});
if constexpr (DepthRange == NDCDepthRange::ZERO_TO_ONE) if constexpr (DepthRange == NDCDepthRange::ZERO_TO_ONE)
return {{1.f / (aspect_ratio * fov_half_tan), 0.f, 0.f, 0.f}, return {{Type{1} / (aspect_ratio * fov_half_tan), Type{0}, Type{0}, Type{0}},
{0.f, 1.f / fov_half_tan, 0.f, 0.f}, {Type{0}, Type{1} / fov_half_tan, Type{0}, Type{0}},
{0.f, 0.f, -far / (far - near), -(near * far) / (far - near)}, {Type{0}, Type{0}, -far / (far - near), -(near * far) / (far - near)},
{0.f, 0.f, -1.f, 0.f}}; {Type{0}, Type{0}, -Type{1}, Type{0}}};
else if constexpr (DepthRange == NDCDepthRange::NEGATIVE_ONE_TO_ONE) else if constexpr (DepthRange == NDCDepthRange::NEGATIVE_ONE_TO_ONE)
return {{1.f / (aspect_ratio * fov_half_tan), 0.f, 0.f, 0.f}, return {{Type{1} / (aspect_ratio * fov_half_tan), Type{0}, Type{0}, Type{0}},
{0.f, 1.f / fov_half_tan, 0.f, 0.f}, {Type{0}, Type{1} / fov_half_tan, Type{0}, Type{0}},
{0.f, 0.f, -(far + near) / (far - near), -(2.f * near * far) / (far - near)}, {Type{0}, Type{0}, -(far + near) / (far - near), -(Type{2} * near * far) / (far - near)},
{0.f, 0.f, -1.f, 0.f}}; {Type{0}, Type{0}, -Type{1}, Type{0}}};
else
std::unreachable();
}
// Horizontal-FOV variants — use these when the engine reports FOV as
// horizontal (UE's FMinimalViewInfo::FOV, Quake-family fov_x, etc.).
// 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("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
{
const auto inv_tan_half_hfov = Type{1} / std::tan(angles::degrees_to_radians(horizontal_fov) / Type{2});
const auto x_axis = inv_tan_half_hfov;
const auto y_axis = inv_tan_half_hfov * aspect_ratio;
if constexpr (DepthRange == NDCDepthRange::ZERO_TO_ONE)
return {{x_axis, Type{0}, Type{0}, Type{0}},
{Type{0}, y_axis, Type{0}, Type{0}},
{Type{0}, Type{0}, far / (far - near), -(near * far) / (far - near)},
{Type{0}, Type{0}, Type{1}, Type{0}}};
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), -(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("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
{
const auto inv_tan_half_hfov = Type{1} / std::tan(angles::degrees_to_radians(horizontal_fov) / Type{2});
const auto x_axis = inv_tan_half_hfov;
const auto y_axis = inv_tan_half_hfov * aspect_ratio;
if constexpr (DepthRange == NDCDepthRange::ZERO_TO_ONE)
return {{x_axis, Type{0}, Type{0}, Type{0}},
{Type{0}, y_axis, Type{0}, Type{0}},
{Type{0}, Type{0}, -far / (far - near), -(near * far) / (far - near)},
{Type{0}, Type{0}, -Type{1}, Type{0}}};
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), -(Type{2} * near * far) / (far - near)},
{Type{0}, Type{0}, -Type{1}, Type{0}}};
else else
std::unreachable(); std::unreachable();
} }
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR, template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR,
NDCDepthRange DepthRange = NDCDepthRange::NEGATIVE_ONE_TO_ONE> 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, 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 const Type near, const Type far) noexcept
{ {
@@ -734,7 +836,7 @@ namespace omath
} }
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR, template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR,
NDCDepthRange DepthRange = NDCDepthRange::NEGATIVE_ONE_TO_ONE> 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, 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 const Type near, const Type far) noexcept
{ {
@@ -781,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) struct std::formatter<omath::Mat<Rows, Columns, Type, StoreType>> // NOLINT(*-dcl58-cpp)
{ {
using MatType = omath::Mat<Rows, Columns, Type, StoreType>; using MatType = omath::Mat<Rows, Columns, Type, StoreType>;
[[nodiscard]] [[nodiscard("You must use parse iterator")]]
static constexpr auto parse(std::format_parse_context& ctx) static constexpr auto parse(std::format_parse_context& ctx)
{ {
return ctx.begin(); return ctx.begin();
} }
template<class FormatContext> template<class FormatContext>
[[nodiscard]] [[nodiscard("You must use format iterator")]]
static auto format(const MatType& mat, FormatContext& ctx) static auto format(const MatType& mat, FormatContext& ctx)
{ {
if constexpr (std::is_same_v<typename FormatContext::char_type, char>) if constexpr (std::is_same_v<typename FormatContext::char_type, char>)
+50 -27
View File
@@ -26,18 +26,24 @@ namespace omath
// Constructors // Constructors
constexpr Vector2() = default; constexpr Vector2() = default;
template<class CastedType>
requires std::is_arithmetic_v<CastedType>
[[nodiscard("You must use casted vector")]] constexpr explicit operator Vector2<CastedType>() const noexcept
{
return {static_cast<CastedType>(x), static_cast<CastedType>(y)};
}
constexpr Vector2(const Type& x, const Type& y) noexcept: x(x), y(y) constexpr Vector2(const Type& x, const Type& y) noexcept: x(x), y(y)
{ {
} }
// Equality operators // Equality operators
[[nodiscard]] [[nodiscard("You must use comparison result")]]
constexpr bool operator==(const Vector2& other) const noexcept constexpr bool operator==(const Vector2& other) const noexcept
{ {
return x == other.x && y == other.y; return x == other.x && y == other.y;
} }
[[nodiscard]] [[nodiscard("You must use comparison result")]]
constexpr bool operator!=(const Vector2& other) const noexcept constexpr bool operator!=(const Vector2& other) const noexcept
{ {
return !(*this == other); return !(*this == other);
@@ -109,45 +115,51 @@ namespace omath
} }
// Basic vector operations // 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)); 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); 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; return x * other.x + y * other.y;
} }
#ifndef _MSC_VER #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); 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(); const Type len = length();
return len > 0.f ? *this / len : *this; return len > 0.f ? *this / len : *this;
} }
#else #else
[[nodiscard]] Type length() const noexcept [[nodiscard("You must use length")]]
Type length() const noexcept
{ {
return std::hypot(x, y); return std::hypot(x, y);
} }
[[nodiscard]] Vector2 normalized() const noexcept [[nodiscard("You must use normalized vector")]]
Vector2 normalized() const noexcept
{ {
const Type len = length(); const Type len = length();
return len > static_cast<Type>(0) ? *this / len : *this; return len > static_cast<Type>(0) ? *this / len : *this;
} }
#endif #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; return x * x + y * y;
} }
@@ -159,80 +171,91 @@ namespace omath
y = y < static_cast<Type>(0) ? -y : y; y = y < static_cast<Type>(0) ? -y : y;
return *this; 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}; return {-x, -y};
} }
// Binary arithmetic operators // 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}; 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}; 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}; 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}; return {x / value, y / value};
} }
// Sum of elements // Sum of elements
[[nodiscard]] constexpr Type sum() const noexcept [[nodiscard("You must use sum of elements")]]
constexpr Type sum() const noexcept
{ {
return x + y; return x + y;
} }
[[nodiscard]] [[nodiscard("You must use comparison result")]]
bool operator<(const Vector2& other) const noexcept bool operator<(const Vector2& other) const noexcept
{ {
return length() < other.length(); return length() < other.length();
} }
[[nodiscard]] [[nodiscard("You must use comparison result")]]
bool operator>(const Vector2& other) const noexcept bool operator>(const Vector2& other) const noexcept
{ {
return length() > other.length(); return length() > other.length();
} }
[[nodiscard]] [[nodiscard("You must use comparison result")]]
bool operator<=(const Vector2& other) const noexcept bool operator<=(const Vector2& other) const noexcept
{ {
return length() <= other.length(); return length() <= other.length();
} }
[[nodiscard]] [[nodiscard("You must use comparison result")]]
bool operator>=(const Vector2& other) const noexcept bool operator>=(const Vector2& other) const noexcept
{ {
return length() >= other.length(); return length() >= other.length();
} }
[[nodiscard]] [[nodiscard("You must use tuple")]]
constexpr std::tuple<Type, Type> as_tuple() const noexcept constexpr std::tuple<Type, Type> as_tuple() const noexcept
{ {
return std::make_tuple(x, y); return std::make_tuple(x, y);
} }
[[nodiscard]] [[nodiscard("You must use array")]]
constexpr std::array<Type, 2> as_array() const noexcept constexpr std::array<Type, 2> as_array() const noexcept
{ {
return {x, y}; return {x, y};
} }
#ifdef OMATH_IMGUI_INTEGRATION #ifdef OMATH_IMGUI_INTEGRATION
[[nodiscard]] [[nodiscard("You must use ImVec2")]]
constexpr ImVec2 to_im_vec2() const noexcept constexpr ImVec2 to_im_vec2() const noexcept
{ {
return {static_cast<float>(this->x), static_cast<float>(this->y)}; 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 static Vector2 from_im_vec2(const ImVec2& other) noexcept
{ {
return {static_cast<Type>(other.x), static_cast<Type>(other.y)}; return {static_cast<Type>(other.x), static_cast<Type>(other.y)};
@@ -243,7 +266,7 @@ namespace omath
template<> struct std::hash<omath::Vector2<float>> 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 operator()(const omath::Vector2<float>& vec) const noexcept
{ {
std::size_t hash = 0; std::size_t hash = 0;
@@ -259,14 +282,14 @@ template<> struct std::hash<omath::Vector2<float>>
template<class Type> template<class Type>
struct std::formatter<omath::Vector2<Type>> // NOLINT(*-dcl58-cpp) 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) static constexpr auto parse(std::format_parse_context& ctx)
{ {
return ctx.begin(); return ctx.begin();
} }
template<class FormatContext> template<class FormatContext>
[[nodiscard]] [[nodiscard("You must use format iterator")]]
static auto format(const omath::Vector2<Type>& vec, FormatContext& ctx) static auto format(const omath::Vector2<Type>& vec, FormatContext& ctx)
{ {
if constexpr (std::is_same_v<typename FormatContext::char_type, char>) if constexpr (std::is_same_v<typename FormatContext::char_type, char>)
+70 -36
View File
@@ -30,12 +30,22 @@ namespace omath
} }
constexpr Vector3() noexcept: Vector2<Type>() {}; constexpr Vector3() noexcept: Vector2<Type>() {};
[[nodiscard]] constexpr bool operator==(const Vector3& other) const noexcept template<class CastedType>
requires std::is_arithmetic_v<CastedType>
[[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("You must use comparison result")]]
constexpr bool operator==(const Vector3& other) const noexcept
{ {
return Vector2<Type>::operator==(other) && (other.z == z); 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); return !(*this == other);
} }
@@ -111,118 +121,140 @@ namespace omath
return *this; 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(); 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; return Vector2<Type>::dot(other) + z * other.z;
} }
#ifndef _MSC_VER #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); 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(); 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(); 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(); const Type length_value = this->length();
return length_value != 0 ? *this / length_value : *this; return length_value != 0 ? *this / length_value : *this;
} }
#else #else
[[nodiscard]] Type length() const noexcept [[nodiscard("You must use length")]]
Type length() const noexcept
{ {
return std::hypot(this->x, this->y, z); 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(); const Type len = this->length();
return len != static_cast<Type>(0) ? *this / len : *this; 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(); 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(); return (*this - v_other).length();
} }
#endif #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; 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}; 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}; 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}; 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}; 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}; 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}; 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}; 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, return {this->y * other.z - z * other.y, z * other.x - this->x * other.z,
this->x * other.y - this->y * other.x}; 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; return sum_2d() + z;
} }
[[nodiscard]] [[nodiscard("You must use direction check result")]]
bool point_to_same_direction(const Vector3& other) const bool point_to_same_direction(const Vector3& other) const
{ {
return dot(other) > static_cast<Type>(0); 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 angle_between(const Vector3& other) const noexcept
{ {
const auto bottom = length() * other.length(); const auto bottom = length() * other.length();
@@ -233,8 +265,8 @@ namespace omath
return Angle<float, 0.f, 180.f, AngleFlags::Clamped>::from_radians(std::acos(dot(other) / bottom)); return Angle<float, 0.f, 180.f, AngleFlags::Clamped>::from_radians(std::acos(dot(other) / bottom));
} }
[[nodiscard]] bool is_perpendicular(const Vector3& other, [[nodiscard("You must use perpendicularity check result")]]
Type epsilon = static_cast<Type>(0.0001)) const noexcept bool is_perpendicular(const Vector3& other, Type epsilon = static_cast<Type>(0.0001)) const noexcept
{ {
if (const auto angle = angle_between(other)) if (const auto angle = angle_between(other))
return std::abs(angle->as_degrees() - static_cast<Type>(90)) <= epsilon; return std::abs(angle->as_degrees() - static_cast<Type>(90)) <= epsilon;
@@ -242,41 +274,43 @@ namespace omath
return false; 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(); 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); return std::make_tuple(this->x, this->y, z);
} }
[[nodiscard]] [[nodiscard("You must use comparison result")]]
bool operator<(const Vector3& other) const noexcept bool operator<(const Vector3& other) const noexcept
{ {
return length() < other.length(); return length() < other.length();
} }
[[nodiscard]] [[nodiscard("You must use comparison result")]]
bool operator>(const Vector3& other) const noexcept bool operator>(const Vector3& other) const noexcept
{ {
return length() > other.length(); return length() > other.length();
} }
[[nodiscard]] [[nodiscard("You must use comparison result")]]
bool operator<=(const Vector3& other) const noexcept bool operator<=(const Vector3& other) const noexcept
{ {
return length() <= other.length(); return length() <= other.length();
} }
[[nodiscard]] [[nodiscard("You must use comparison result")]]
bool operator>=(const Vector3& other) const noexcept bool operator>=(const Vector3& other) const noexcept
{ {
return length() >= other.length(); return length() >= other.length();
} }
[[nodiscard]] [[nodiscard("You must use array")]]
constexpr std::array<Type, 3> as_array() const noexcept constexpr std::array<Type, 3> as_array() const noexcept
{ {
return {this->x, this->y, z}; return {this->x, this->y, z};
@@ -286,7 +320,7 @@ namespace omath
template<> struct std::hash<omath::Vector3<float>> 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 operator()(const omath::Vector3<float>& vec) const noexcept
{ {
std::size_t hash = 0; std::size_t hash = 0;
@@ -303,14 +337,14 @@ template<> struct std::hash<omath::Vector3<float>>
template<class Type> template<class Type>
struct std::formatter<omath::Vector3<Type>> // NOLINT(*-dcl58-cpp) 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) static constexpr auto parse(std::format_parse_context& ctx)
{ {
return ctx.begin(); return ctx.begin();
} }
template<class FormatContext> template<class FormatContext>
[[nodiscard]] [[nodiscard("You must use format iterator")]]
static auto format(const omath::Vector3<Type>& vec, FormatContext& ctx) static auto format(const omath::Vector3<Type>& vec, FormatContext& ctx)
{ {
if constexpr (std::is_same_v<typename FormatContext::char_type, char>) if constexpr (std::is_same_v<typename FormatContext::char_type, char>)
+39 -23
View File
@@ -21,13 +21,22 @@ namespace omath
} }
constexpr Vector4() noexcept: Vector3<Type>(), w(static_cast<Type>(0)) {}; constexpr Vector4() noexcept: Vector3<Type>(), w(static_cast<Type>(0)) {};
[[nodiscard]]
template<class CastedType>
requires std::is_arithmetic_v<CastedType>
[[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("You must use comparison result")]]
constexpr bool operator==(const Vector4& other) const noexcept constexpr bool operator==(const Vector4& other) const noexcept
{ {
return Vector3<Type>::operator==(other) && w == other.w; return Vector3<Type>::operator==(other) && w == other.w;
} }
[[nodiscard]] [[nodiscard("You must use comparison result")]]
constexpr bool operator!=(const Vector4& other) const noexcept constexpr bool operator!=(const Vector4& other) const noexcept
{ {
return !(*this == other); return !(*this == other);
@@ -80,17 +89,19 @@ namespace omath
return *this; 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; 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; 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()); return std::sqrt(length_sqr());
} }
@@ -102,6 +113,11 @@ namespace omath
return *this; 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 constexpr Vector4& clamp(const Type& min, const Type& max) noexcept
{ {
this->x = std::clamp(this->x, min, max); this->x = std::clamp(this->x, min, max);
@@ -111,86 +127,86 @@ namespace omath
return *this; return *this;
} }
[[nodiscard]] [[nodiscard("You must use negated vector")]]
constexpr Vector4 operator-() const noexcept constexpr Vector4 operator-() const noexcept
{ {
return {-this->x, -this->y, -this->z, -w}; return {-this->x, -this->y, -this->z, -w};
} }
[[nodiscard]] [[nodiscard("You must use result vector")]]
constexpr Vector4 operator+(const Vector4& other) const noexcept constexpr Vector4 operator+(const Vector4& other) const noexcept
{ {
return {this->x + other.x, this->y + other.y, this->z + other.z, w + other.w}; 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 constexpr Vector4 operator-(const Vector4& other) const noexcept
{ {
return {this->x - other.x, this->y - other.y, this->z - other.z, w - other.w}; 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 constexpr Vector4 operator*(const Type& value) const noexcept
{ {
return {this->x * value, this->y * value, this->z * value, w * value}; 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 constexpr Vector4 operator*(const Vector4& other) const noexcept
{ {
return {this->x * other.x, this->y * other.y, this->z * other.z, w * other.w}; 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 constexpr Vector4 operator/(const Type& value) const noexcept
{ {
return {this->x / value, this->y / value, this->z / value, w / value}; 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 constexpr Vector4 operator/(const Vector4& other) const noexcept
{ {
return {this->x / other.x, this->y / other.y, this->z / other.z, w / other.w}; 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 constexpr Type sum() const noexcept
{ {
return Vector3<Type>::sum() + w; return Vector3<Type>::sum() + w;
} }
[[nodiscard]] [[nodiscard("You must use comparison result")]]
bool operator<(const Vector4& other) const noexcept bool operator<(const Vector4& other) const noexcept
{ {
return length() < other.length(); return length() < other.length();
} }
[[nodiscard]] [[nodiscard("You must use comparison result")]]
bool operator>(const Vector4& other) const noexcept bool operator>(const Vector4& other) const noexcept
{ {
return length() > other.length(); return length() > other.length();
} }
[[nodiscard]] [[nodiscard("You must use comparison result")]]
bool operator<=(const Vector4& other) const noexcept bool operator<=(const Vector4& other) const noexcept
{ {
return length() <= other.length(); return length() <= other.length();
} }
[[nodiscard]] [[nodiscard("You must use comparison result")]]
bool operator>=(const Vector4& other) const noexcept bool operator>=(const Vector4& other) const noexcept
{ {
return length() >= other.length(); return length() >= other.length();
} }
[[nodiscard]] [[nodiscard("You must use array")]]
constexpr std::array<Type, 4> as_array() const noexcept constexpr std::array<Type, 4> as_array() const noexcept
{ {
return {this->x, this->y, this->z, w}; return {this->x, this->y, this->z, w};
} }
#ifdef OMATH_IMGUI_INTEGRATION #ifdef OMATH_IMGUI_INTEGRATION
[[nodiscard]] [[nodiscard("You must use ImVec4")]]
constexpr ImVec4 to_im_vec4() const noexcept constexpr ImVec4 to_im_vec4() const noexcept
{ {
return { return {
@@ -200,7 +216,7 @@ namespace omath
static_cast<float>(w), static_cast<float>(w),
}; };
} }
[[nodiscard]] [[nodiscard("You must use vector from ImVec4")]]
static Vector4<float> from_im_vec4(const ImVec4& other) noexcept 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)}; return {static_cast<Type>(other.x), static_cast<Type>(other.y), static_cast<Type>(other.z)};
@@ -211,7 +227,7 @@ namespace omath
template<> struct std::hash<omath::Vector4<float>> 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 operator()(const omath::Vector4<float>& vec) const noexcept
{ {
std::size_t hash = 0; std::size_t hash = 0;
@@ -228,13 +244,13 @@ template<> struct std::hash<omath::Vector4<float>>
template<class Type> template<class Type>
struct std::formatter<omath::Vector4<Type>> // NOLINT(*-dcl58-cpp) 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) static constexpr auto parse(std::format_parse_context& ctx)
{ {
return ctx.begin(); return ctx.begin();
} }
template<class FormatContext> template<class FormatContext>
[[nodiscard]] [[nodiscard("You must use format iterator")]]
static auto format(const omath::Vector4<Type>& vec, FormatContext& ctx) static auto format(const omath::Vector4<Type>& vec, FormatContext& ctx)
{ {
if constexpr (std::is_same_v<typename FormatContext::char_type, char>) 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_vec2(sol::table& omath_table);
static void register_vec3(sol::table& omath_table); static void register_vec3(sol::table& omath_table);
static void register_vec4(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_color(sol::table& omath_table);
static void register_hud(sol::table& omath_table);
static void register_triangle(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_shared_types(sol::table& omath_table);
static void register_engines(sol::table& omath_table); static void register_engines(sol::table& omath_table);
static void register_pattern_scan(sol::table& omath_table); static void register_pattern_scan(sol::table& omath_table);
+46
View File
@@ -0,0 +1,46 @@
//
// Created by orange on 4/12/2026.
//
#pragma once
#include "navigation_mesh.hpp"
#include "omath/linear_algebra/vector3.hpp"
#include <functional>
#include <memory>
namespace omath::pathfinding
{
enum class WalkBotStatus
{
IDLE,
PATHING,
FINISHED
};
class WalkBot
{
public:
WalkBot() = default;
explicit WalkBot(const std::shared_ptr<NavigationMesh>& mesh, float min_node_distance = 1.f);
void set_nav_mesh(const std::shared_ptr<NavigationMesh>& mesh);
void set_min_node_distance(float distance);
void set_target(const Vector3<float>& target);
// Clear navigation state so the bot can be re-routed without stale
// visited-node memory.
void reset();
// Call every game tick with the current bot world position.
void update(const Vector3<float>& bot_position);
void on_path(const std::function<void(const Vector3<float>&)>& callback);
void on_status(const std::function<void(WalkBotStatus)>& callback);
private:
std::weak_ptr<NavigationMesh> m_nav_mesh;
std::optional<std::function<void(const Vector3<float>&)>> m_on_next_path_node;
std::optional<std::function<void(WalkBotStatus)>> m_on_status_update;
std::optional<Vector3<float>> m_last_visited;
std::optional<Vector3<float>> m_target;
float m_min_node_distance{1.f};
};
} // namespace omath::pathfinding
@@ -8,22 +8,24 @@
namespace omath::projectile_prediction namespace omath::projectile_prediction
{ {
template<class ArithmeticType = float>
struct AimAngles struct AimAngles
{ {
float pitch{}; ArithmeticType pitch{};
float yaw{}; ArithmeticType yaw{};
}; };
template<class ArithmeticType = float>
class ProjPredEngineInterface class ProjPredEngineInterface
{ {
public: public:
[[nodiscard]] [[nodiscard]]
virtual std::optional<Vector3<float>> maybe_calculate_aim_point(const Projectile& projectile, virtual std::optional<Vector3<ArithmeticType>> maybe_calculate_aim_point(
const Target& target) const = 0; const Projectile<ArithmeticType>& projectile, const Target<ArithmeticType>& target) const = 0;
[[nodiscard]] [[nodiscard]]
virtual std::optional<AimAngles> maybe_calculate_aim_angles(const Projectile& projectile, virtual std::optional<AimAngles<ArithmeticType>> maybe_calculate_aim_angles(
const Target& target) const = 0; const Projectile<ArithmeticType>& projectile, const Target<ArithmeticType>& target) const = 0;
virtual ~ProjPredEngineInterface() = default; virtual ~ProjPredEngineInterface() = default;
}; };
@@ -6,14 +6,14 @@
namespace omath::projectile_prediction namespace omath::projectile_prediction
{ {
class ProjPredEngineAvx2 final : public ProjPredEngineInterface class ProjPredEngineAvx2 final : public ProjPredEngineInterface<float>
{ {
public: public:
[[nodiscard]] std::optional<Vector3<float>> [[nodiscard]] std::optional<Vector3<float>>
maybe_calculate_aim_point(const Projectile& projectile, const Target& target) const override; maybe_calculate_aim_point(const Projectile<float>& projectile, const Target<float>& target) const override;
[[nodiscard]] std::optional<AimAngles> [[nodiscard]] std::optional<AimAngles<float>>
maybe_calculate_aim_angles(const Projectile& projectile, const Target& target) const override; maybe_calculate_aim_angles(const Projectile<float>& projectile, const Target<float>& target) const override;
ProjPredEngineAvx2(float gravity_constant, float simulation_time_step, float maximum_simulation_time); ProjPredEngineAvx2(float gravity_constant, float simulation_time_step, float maximum_simulation_time);
~ProjPredEngineAvx2() override = default; ~ProjPredEngineAvx2() override = default;
@@ -13,24 +13,23 @@
namespace omath::projectile_prediction namespace omath::projectile_prediction
{ {
template<class T> template<class T, class ArithmeticType>
concept PredEngineConcept = concept PredEngineConcept =
requires(const Projectile& projectile, const Target& target, const Vector3<float>& vec_a, requires(const Projectile<ArithmeticType>& projectile, const Target<ArithmeticType>& target,
const Vector3<float>& vec_b, const Vector3<ArithmeticType>& vec_a, const Vector3<ArithmeticType>& vec_b,
Vector3<float> v3, // by-value for calc_viewpoint_from_angles Vector3<ArithmeticType> v3,
float pitch, float yaw, float time, float gravity, std::optional<float> maybe_pitch) { ArithmeticType pitch, ArithmeticType yaw, ArithmeticType time, ArithmeticType gravity,
// Presence + return types std::optional<ArithmeticType> maybe_pitch) {
{ {
T::predict_projectile_position(projectile, pitch, yaw, time, gravity) T::predict_projectile_position(projectile, pitch, yaw, time, gravity)
} -> std::same_as<Vector3<float>>; } -> std::same_as<Vector3<ArithmeticType>>;
{ T::predict_target_position(target, time, gravity) } -> std::same_as<Vector3<float>>; { T::predict_target_position(target, time, gravity) } -> std::same_as<Vector3<ArithmeticType>>;
{ T::calc_vector_2d_distance(vec_a) } -> std::same_as<float>; { T::calc_vector_2d_distance(vec_a) } -> std::same_as<ArithmeticType>;
{ T::get_vector_height_coordinate(vec_b) } -> std::same_as<float>; { T::get_vector_height_coordinate(vec_b) } -> std::same_as<ArithmeticType>;
{ T::calc_viewpoint_from_angles(projectile, v3, maybe_pitch) } -> std::same_as<Vector3<float>>; { T::calc_viewpoint_from_angles(projectile, v3, maybe_pitch) } -> std::same_as<Vector3<ArithmeticType>>;
{ T::calc_direct_pitch_angle(vec_a, vec_b) } -> std::same_as<float>; { T::calc_direct_pitch_angle(vec_a, vec_b) } -> std::same_as<ArithmeticType>;
{ T::calc_direct_yaw_angle(vec_a, vec_b) } -> std::same_as<float>; { T::calc_direct_yaw_angle(vec_a, vec_b) } -> std::same_as<ArithmeticType>;
// Enforce noexcept as in PredEngineTrait
requires noexcept(T::predict_projectile_position(projectile, pitch, yaw, time, gravity)); requires noexcept(T::predict_projectile_position(projectile, pitch, yaw, time, gravity));
requires noexcept(T::predict_target_position(target, time, gravity)); requires noexcept(T::predict_target_position(target, time, gravity));
requires noexcept(T::calc_vector_2d_distance(vec_a)); requires noexcept(T::calc_vector_2d_distance(vec_a));
@@ -39,21 +38,24 @@ namespace omath::projectile_prediction
requires noexcept(T::calc_direct_pitch_angle(vec_a, vec_b)); requires noexcept(T::calc_direct_pitch_angle(vec_a, vec_b));
requires noexcept(T::calc_direct_yaw_angle(vec_a, vec_b)); requires noexcept(T::calc_direct_yaw_angle(vec_a, vec_b));
}; };
template<class EngineTrait = source_engine::PredEngineTrait>
requires PredEngineConcept<EngineTrait> template<class EngineTrait = source_engine::PredEngineTrait, class ArithmeticType = float>
class ProjPredEngineLegacy final : public ProjPredEngineInterface requires PredEngineConcept<EngineTrait, ArithmeticType>
class ProjPredEngineLegacy final : public ProjPredEngineInterface<ArithmeticType>
{ {
public: public:
explicit ProjPredEngineLegacy(const float gravity_constant, const float simulation_time_step, explicit ProjPredEngineLegacy(const ArithmeticType gravity_constant,
const float maximum_simulation_time, const float distance_tolerance) const ArithmeticType simulation_time_step,
const ArithmeticType maximum_simulation_time,
const ArithmeticType distance_tolerance)
: m_gravity_constant(gravity_constant), m_simulation_time_step(simulation_time_step), : m_gravity_constant(gravity_constant), m_simulation_time_step(simulation_time_step),
m_maximum_simulation_time(maximum_simulation_time), m_distance_tolerance(distance_tolerance) m_maximum_simulation_time(maximum_simulation_time), m_distance_tolerance(distance_tolerance)
{ {
} }
[[nodiscard]] [[nodiscard]]
std::optional<Vector3<float>> maybe_calculate_aim_point(const Projectile& projectile, std::optional<Vector3<ArithmeticType>> maybe_calculate_aim_point(
const Target& target) const override const Projectile<ArithmeticType>& projectile, const Target<ArithmeticType>& target) const override
{ {
const auto solution = find_solution(projectile, target); const auto solution = find_solution(projectile, target);
if (!solution) if (!solution)
@@ -64,28 +66,31 @@ namespace omath::projectile_prediction
} }
[[nodiscard]] [[nodiscard]]
std::optional<AimAngles> maybe_calculate_aim_angles(const Projectile& projectile, std::optional<AimAngles<ArithmeticType>> maybe_calculate_aim_angles(
const Target& target) const override const Projectile<ArithmeticType>& projectile, const Target<ArithmeticType>& target) const override
{ {
const auto solution = find_solution(projectile, target); const auto solution = find_solution(projectile, target);
if (!solution) if (!solution)
return std::nullopt; return std::nullopt;
const auto yaw = EngineTrait::calc_direct_yaw_angle(projectile.m_origin + projectile.m_launch_offset, solution->predicted_target_position); const auto yaw = EngineTrait::calc_direct_yaw_angle(
return AimAngles{solution->pitch, yaw}; projectile.m_origin + projectile.m_launch_offset, solution->predicted_target_position);
return AimAngles<ArithmeticType>{solution->pitch, yaw};
} }
private: private:
struct Solution struct Solution
{ {
Vector3<float> predicted_target_position; Vector3<ArithmeticType> predicted_target_position;
float pitch; ArithmeticType pitch;
}; };
[[nodiscard]] [[nodiscard]]
std::optional<Solution> find_solution(const Projectile& projectile, const Target& target) const std::optional<Solution> find_solution(const Projectile<ArithmeticType>& projectile,
const Target<ArithmeticType>& target) const
{ {
for (float time = 0.f; time < m_maximum_simulation_time; time += m_simulation_time_step) for (ArithmeticType time = ArithmeticType{0}; time < m_maximum_simulation_time;
time += m_simulation_time_step)
{ {
const auto predicted_target_position = const auto predicted_target_position =
EngineTrait::predict_target_position(target, time, m_gravity_constant); EngineTrait::predict_target_position(target, time, m_gravity_constant);
@@ -105,10 +110,10 @@ namespace omath::projectile_prediction
return std::nullopt; return std::nullopt;
} }
const float m_gravity_constant; const ArithmeticType m_gravity_constant;
const float m_simulation_time_step; const ArithmeticType m_simulation_time_step;
const float m_maximum_simulation_time; const ArithmeticType m_maximum_simulation_time;
const float m_distance_tolerance; const ArithmeticType m_distance_tolerance;
// Realization of this formula: // Realization of this formula:
// https://stackoverflow.com/questions/54917375/how-to-calculate-the-angle-to-shoot-a-bullet-in-order-to-hit-a-moving-target // https://stackoverflow.com/questions/54917375/how-to-calculate-the-angle-to-shoot-a-bullet-in-order-to-hit-a-moving-target
@@ -123,15 +128,15 @@ namespace omath::projectile_prediction
\] \]
*/ */
[[nodiscard]] [[nodiscard]]
std::optional<float> std::optional<ArithmeticType>
maybe_calculate_projectile_launch_pitch_angle(const Projectile& projectile, maybe_calculate_projectile_launch_pitch_angle(const Projectile<ArithmeticType>& projectile,
const Vector3<float>& target_position) const noexcept const Vector3<ArithmeticType>& target_position) const noexcept
{ {
const auto bullet_gravity = m_gravity_constant * projectile.m_gravity_scale; const auto bullet_gravity = m_gravity_constant * projectile.m_gravity_scale;
const auto launch_origin = projectile.m_origin + projectile.m_launch_offset; const auto launch_origin = projectile.m_origin + projectile.m_launch_offset;
if (bullet_gravity == 0.f) if (bullet_gravity == ArithmeticType{0})
return EngineTrait::calc_direct_pitch_angle(launch_origin, target_position); return EngineTrait::calc_direct_pitch_angle(launch_origin, target_position);
const auto delta = target_position - launch_origin; const auto delta = target_position - launch_origin;
@@ -140,24 +145,28 @@ namespace omath::projectile_prediction
const auto distance2d_sqr = distance2d * distance2d; const auto distance2d_sqr = distance2d * distance2d;
const auto launch_speed_sqr = projectile.m_launch_speed * projectile.m_launch_speed; const auto launch_speed_sqr = projectile.m_launch_speed * projectile.m_launch_speed;
float root = launch_speed_sqr * launch_speed_sqr ArithmeticType root = launch_speed_sqr * launch_speed_sqr
- bullet_gravity - bullet_gravity
* (bullet_gravity * distance2d_sqr * (bullet_gravity * distance2d_sqr
+ 2.0f * EngineTrait::get_vector_height_coordinate(delta) * launch_speed_sqr); + ArithmeticType{2} * EngineTrait::get_vector_height_coordinate(delta)
* launch_speed_sqr);
if (root < 0.0f) [[unlikely]] if (root < ArithmeticType{0}) [[unlikely]]
return std::nullopt; return std::nullopt;
root = std::sqrt(root); root = std::sqrt(root);
const float angle = std::atan((launch_speed_sqr - root) / (bullet_gravity * distance2d)); const ArithmeticType angle = std::atan((launch_speed_sqr - root) / (bullet_gravity * distance2d));
return angles::radians_to_degrees(angle); return angles::radians_to_degrees(angle);
} }
[[nodiscard]] [[nodiscard]]
bool is_projectile_reached_target(const Vector3<float>& target_position, const Projectile& projectile, bool is_projectile_reached_target(const Vector3<ArithmeticType>& target_position,
const float pitch, const float time) const noexcept const Projectile<ArithmeticType>& projectile,
const ArithmeticType pitch, const ArithmeticType time) const noexcept
{ {
const auto yaw = EngineTrait::calc_direct_yaw_angle(projectile.m_origin + projectile.m_launch_offset, target_position); const auto yaw = EngineTrait::calc_direct_yaw_angle(
projectile.m_origin + projectile.m_launch_offset, target_position);
const auto projectile_position = const auto projectile_position =
EngineTrait::predict_projectile_position(projectile, pitch, yaw, time, m_gravity_constant); EngineTrait::predict_projectile_position(projectile, pitch, yaw, time, m_gravity_constant);
@@ -7,12 +7,13 @@
namespace omath::projectile_prediction namespace omath::projectile_prediction
{ {
template <class ArithmeticType = float>
class Projectile final class Projectile final
{ {
public: public:
Vector3<float> m_origin; Vector3<ArithmeticType> m_origin;
Vector3<float> m_launch_offset{0.f, 0.f, 0.f}; Vector3<ArithmeticType> m_launch_offset{};
float m_launch_speed{}; ArithmeticType m_launch_speed{};
float m_gravity_scale{}; ArithmeticType m_gravity_scale{};
}; };
} // namespace omath::projectile_prediction } // namespace omath::projectile_prediction
@@ -7,11 +7,12 @@
namespace omath::projectile_prediction namespace omath::projectile_prediction
{ {
template <class ArithmeticType = float>
class Target final class Target final
{ {
public: public:
Vector3<float> m_origin; Vector3<ArithmeticType> m_origin;
Vector3<float> m_velocity; Vector3<ArithmeticType> m_velocity;
bool m_is_airborne{}; bool m_is_airborne{};
}; };
} // namespace omath::projectile_prediction } // namespace omath::projectile_prediction
+279 -172
View File
@@ -5,6 +5,7 @@
#pragma once #pragma once
#include "omath/3d_primitives/aabb.hpp" #include "omath/3d_primitives/aabb.hpp"
#include "omath/3d_primitives/obb.hpp"
#include "omath/linear_algebra/mat.hpp" #include "omath/linear_algebra/mat.hpp"
#include "omath/linear_algebra/triangle.hpp" #include "omath/linear_algebra/triangle.hpp"
#include "omath/linear_algebra/vector3.hpp" #include "omath/linear_algebra/vector3.hpp"
@@ -31,7 +32,7 @@ namespace omath::projection
float m_width; float m_width;
float m_height; 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; return m_width / m_height;
} }
@@ -42,25 +43,32 @@ namespace omath::projection
AUTO, AUTO,
MANUAL, MANUAL,
}; };
template<class T, class MatType, class ViewAnglesType> struct CameraAxes
{
bool inverted_forward = false;
bool inverted_right = false;
};
template<class T, class MatType, class ViewAnglesType, class NumericType>
concept CameraEngineConcept = concept CameraEngineConcept =
requires(const Vector3<float>& cam_origin, const Vector3<float>& look_at, const ViewAnglesType& angles, requires(const Vector3<NumericType>& cam_origin, const Vector3<NumericType>& look_at,
const FieldOfView& fov, const ViewPort& viewport, float znear, float zfar, const ViewAnglesType& angles, const FieldOfView& fov, const ViewPort& viewport, NumericType z_near,
NDCDepthRange ndc_depth_range) { NumericType z_far, NDCDepthRange ndc_depth_range) {
// Presence + return types // Presence + return types
{ T::calc_look_at_angle(cam_origin, look_at) } -> std::same_as<ViewAnglesType>; { T::calc_look_at_angle(cam_origin, look_at) } -> std::same_as<ViewAnglesType>;
{ T::calc_view_matrix(angles, cam_origin) } -> std::same_as<MatType>; { T::calc_view_matrix(angles, cam_origin) } -> std::same_as<MatType>;
{ T::calc_projection_matrix(fov, viewport, znear, zfar, ndc_depth_range) } -> std::same_as<MatType>; { T::calc_projection_matrix(fov, viewport, z_near, z_far, ndc_depth_range) } -> std::same_as<MatType>;
requires std::is_floating_point_v<NumericType>;
// Enforce noexcept as in the trait declaration // Enforce noexcept as in the trait declaration
requires noexcept(T::calc_look_at_angle(cam_origin, look_at)); requires noexcept(T::calc_look_at_angle(cam_origin, look_at));
requires noexcept(T::calc_view_matrix(angles, cam_origin)); requires noexcept(T::calc_view_matrix(angles, cam_origin));
requires noexcept(T::calc_projection_matrix(fov, viewport, znear, zfar, ndc_depth_range)); requires noexcept(T::calc_projection_matrix(fov, viewport, z_near, z_far, ndc_depth_range));
}; };
template<class Mat4X4Type, class ViewAnglesType, class TraitClass, bool inverted_z = false, template<class Mat4X4Type, class ViewAnglesType, class TraitClass,
NDCDepthRange depth_range = NDCDepthRange::NEGATIVE_ONE_TO_ONE> NDCDepthRange depth_range = NDCDepthRange::NEGATIVE_ONE_TO_ONE, CameraAxes axes = {},
requires CameraEngineConcept<TraitClass, Mat4X4Type, ViewAnglesType> class NumericType = float>
requires CameraEngineConcept<TraitClass, Mat4X4Type, ViewAnglesType, NumericType>
class Camera final class Camera final
{ {
#ifdef OMATH_BUILD_TESTS #ifdef OMATH_BUILD_TESTS
@@ -76,50 +84,114 @@ namespace omath::projection
}; };
~Camera() = default; ~Camera() = default;
Camera(const Vector3<float>& position, const ViewAnglesType& view_angles, const ViewPort& view_port, Camera(const Vector3<NumericType>& position, const ViewAnglesType& view_angles, const ViewPort& view_port,
const FieldOfView& fov, const float near, const float far) noexcept const FieldOfView& fov, const NumericType near, const NumericType far) noexcept
: m_view_port(view_port), m_field_of_view(fov), m_far_plane_distance(far), m_near_plane_distance(near), : m_view_port(view_port), m_field_of_view(fov), m_far_plane_distance(far), m_near_plane_distance(near),
m_view_angles(view_angles), m_origin(position) m_view_angles(view_angles), m_origin(position)
{ {
} }
void look_at(const Vector3<float>& target) struct ProjectionParams final
{
FieldOfView fov;
NumericType aspect_ratio{};
};
// Recovers vertical FOV and aspect ratio from a perspective projection matrix
// 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("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]
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("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]};
if constexpr (axes.inverted_forward)
forward_vector = -forward_vector;
return TraitClass::calc_look_at_angle({}, forward_vector);
}
[[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.
// Recovering origin: origin = -R^T * t
return {
-(view_matrix[0, 0] * view_matrix[0, 3] + view_matrix[1, 0] * view_matrix[1, 3]
+ view_matrix[2, 0] * view_matrix[2, 3]),
-(view_matrix[0, 1] * view_matrix[0, 3] + view_matrix[1, 1] * view_matrix[1, 3]
+ view_matrix[2, 1] * view_matrix[2, 3]),
-(view_matrix[0, 2] * view_matrix[0, 3] + view_matrix[1, 2] * view_matrix[1, 3]
+ view_matrix[2, 2] * view_matrix[2, 3]),
};
}
void look_at(const Vector3<NumericType>& target)
{ {
m_view_angles = TraitClass::calc_look_at_angle(m_origin, target); m_view_angles = TraitClass::calc_look_at_angle(m_origin, target);
m_view_projection_matrix = std::nullopt; m_view_projection_matrix = std::nullopt;
m_view_matrix = std::nullopt; m_view_matrix = std::nullopt;
} }
[[nodiscard]] [[nodiscard("You must use calculated look-at angles")]]
ViewAnglesType calc_look_at_angles(const Vector3<float>& look_to) const ViewAnglesType calc_look_at_angles(const Vector3<NumericType>& look_to) const
{ {
return TraitClass::calc_look_at_angle(m_origin, look_to); return TraitClass::calc_look_at_angle(m_origin, look_to);
} }
[[nodiscard]] [[nodiscard("You must use forward vector")]]
Vector3<float> get_forward() const noexcept Vector3<NumericType> get_forward() const noexcept
{ {
const auto& view_matrix = get_view_matrix(); const auto& view_matrix = get_view_matrix();
if constexpr (inverted_z)
return -Vector3<float>{view_matrix[2, 0], view_matrix[2, 1], view_matrix[2, 2]};
return {view_matrix[2, 0], view_matrix[2, 1], view_matrix[2, 2]}; return {view_matrix[2, 0], view_matrix[2, 1], view_matrix[2, 2]};
} }
[[nodiscard]] [[nodiscard("You must use right vector")]]
Vector3<float> get_right() const noexcept Vector3<NumericType> get_right() const noexcept
{ {
const auto& view_matrix = get_view_matrix(); const auto& view_matrix = get_view_matrix();
return {view_matrix[0, 0], view_matrix[0, 1], view_matrix[0, 2]}; return {view_matrix[0, 0], view_matrix[0, 1], view_matrix[0, 2]};
} }
[[nodiscard]] [[nodiscard("You must use up vector")]]
Vector3<float> get_up() const noexcept Vector3<NumericType> get_up() const noexcept
{ {
const auto& view_matrix = get_view_matrix(); const auto& view_matrix = get_view_matrix();
return {view_matrix[1, 0], view_matrix[1, 1], view_matrix[1, 2]}; return {view_matrix[1, 0], view_matrix[1, 1], view_matrix[1, 2]};
} }
[[nodiscard("You must use absolute forward vector")]]
Vector3<NumericType> get_abs_forward() const noexcept
{
if constexpr (axes.inverted_forward)
return -get_forward();
return get_forward();
}
[[nodiscard]] const Mat4X4Type& get_view_projection_matrix() const noexcept [[nodiscard("You must use absolute right vector")]]
Vector3<NumericType> get_abs_right() const noexcept
{
if constexpr (axes.inverted_right)
return -get_right();
return get_right();
}
[[nodiscard("You must use absolute up vector")]]
Vector3<NumericType> get_abs_up() const noexcept
{
return get_up();
}
[[nodiscard("You must use view-projection matrix")]]
const Mat4X4Type& get_view_projection_matrix() const noexcept
{ {
if (!m_view_projection_matrix.has_value()) if (!m_view_projection_matrix.has_value())
m_view_projection_matrix = get_projection_matrix() * get_view_matrix(); m_view_projection_matrix = get_projection_matrix() * get_view_matrix();
@@ -127,19 +199,18 @@ namespace omath::projection
return m_view_projection_matrix.value(); 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()) if (!m_view_matrix.has_value())
m_view_matrix = TraitClass::calc_view_matrix(m_view_angles, m_origin); m_view_matrix = TraitClass::calc_view_matrix(m_view_angles, m_origin);
return m_view_matrix.value(); 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()) if (!m_projection_matrix.has_value())
m_projection_matrix = TraitClass::calc_projection_matrix(m_field_of_view, m_view_port, m_projection_matrix = TraitClass::calc_projection_matrix(
m_near_plane_distance, m_far_plane_distance, m_field_of_view, m_view_port, m_near_plane_distance, m_far_plane_distance, depth_range);
depth_range);
return m_projection_matrix.value(); return m_projection_matrix.value();
} }
@@ -151,14 +222,14 @@ namespace omath::projection
m_projection_matrix = std::nullopt; m_projection_matrix = std::nullopt;
} }
void set_near_plane(const float near_plane) noexcept void set_near_plane(const NumericType near_plane) noexcept
{ {
m_near_plane_distance = near_plane; m_near_plane_distance = near_plane;
m_view_projection_matrix = std::nullopt; m_view_projection_matrix = std::nullopt;
m_projection_matrix = std::nullopt; m_projection_matrix = std::nullopt;
} }
void set_far_plane(const float far_plane) noexcept void set_far_plane(const NumericType far_plane) noexcept
{ {
m_far_plane_distance = far_plane; m_far_plane_distance = far_plane;
m_view_projection_matrix = std::nullopt; m_view_projection_matrix = std::nullopt;
@@ -172,7 +243,7 @@ namespace omath::projection
m_view_matrix = std::nullopt; m_view_matrix = std::nullopt;
} }
void set_origin(const Vector3<float>& origin) noexcept void set_origin(const Vector3<NumericType>& origin) noexcept
{ {
m_origin = origin; m_origin = origin;
m_view_projection_matrix = std::nullopt; m_view_projection_matrix = std::nullopt;
@@ -185,34 +256,34 @@ namespace omath::projection
m_projection_matrix = std::nullopt; 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; return m_field_of_view;
} }
[[nodiscard]] const float& get_near_plane() const noexcept [[nodiscard("You must use near plane")]] const NumericType& get_near_plane() const noexcept
{ {
return m_near_plane_distance; return m_near_plane_distance;
} }
[[nodiscard]] const float& get_far_plane() const noexcept [[nodiscard("You must use far plane")]] const NumericType& get_far_plane() const noexcept
{ {
return m_far_plane_distance; 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; return m_view_angles;
} }
[[nodiscard]] const Vector3<float>& get_origin() const noexcept [[nodiscard("You must use origin")]] const Vector3<NumericType>& get_origin() const noexcept
{ {
return m_origin; return m_origin;
} }
template<ScreenStart screen_start = ScreenStart::TOP_LEFT_CORNER> template<ScreenStart screen_start = ScreenStart::TOP_LEFT_CORNER>
[[nodiscard]] std::expected<Vector3<float>, Error> [[nodiscard("You must use screen position")]] std::expected<Vector3<NumericType>, Error>
world_to_screen(const Vector3<float>& world_position) const noexcept world_to_screen(const Vector3<NumericType>& world_position) const noexcept
{ {
const auto normalized_cords = world_to_view_port(world_position); const auto normalized_cords = world_to_view_port(world_position);
@@ -227,8 +298,8 @@ namespace omath::projection
std::unreachable(); std::unreachable();
} }
template<ScreenStart screen_start = ScreenStart::TOP_LEFT_CORNER> template<ScreenStart screen_start = ScreenStart::TOP_LEFT_CORNER>
[[nodiscard]] std::expected<Vector3<float>, Error> [[nodiscard("You must use unclipped screen position")]] std::expected<Vector3<NumericType>, Error>
world_to_screen_unclipped(const Vector3<float>& world_position) const noexcept world_to_screen_unclipped(const Vector3<NumericType>& world_position) const noexcept
{ {
const auto normalized_cords = world_to_view_port(world_position, ViewPortClipping::MANUAL); const auto normalized_cords = world_to_view_port(world_position, ViewPortClipping::MANUAL);
@@ -243,14 +314,15 @@ namespace omath::projection
std::unreachable(); std::unreachable();
} }
[[nodiscard]] bool is_culled_by_frustum(const Triangle<Vector3<float>>& 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) // Transform to clip space (before perspective divide)
auto to_clip = [this](const Vector3<float>& point) auto to_clip = [this](const Vector3<NumericType>& point)
{ {
auto clip = get_view_projection_matrix() auto clip = get_view_projection_matrix()
* mat_column_from_vector<float, Mat4X4Type::get_store_ordering()>(point); * mat_column_from_vector<NumericType, Mat4X4Type::get_store_ordering()>(point);
return std::array<float, 4>{ return std::array<NumericType, 4>{
clip.at(0, 0), // x clip.at(0, 0), // x
clip.at(1, 0), // y clip.at(1, 0), // y
clip.at(2, 0), // z clip.at(2, 0), // z
@@ -263,12 +335,13 @@ namespace omath::projection
const auto c2 = to_clip(triangle.m_vertex3); const auto c2 = to_clip(triangle.m_vertex3);
// If all vertices are behind the camera (w <= 0), trivially reject // If all vertices are behind the camera (w <= 0), trivially reject
if (c0[3] <= 0.f && c1[3] <= 0.f && c2[3] <= 0.f) if (c0[3] <= NumericType{0} && c1[3] <= NumericType{0} && c2[3] <= NumericType{0})
return true; return true;
// Helper: all three vertices outside the same clip plane // Helper: all three vertices outside the same clip plane
auto all_outside_plane = [](const int axis, const std::array<float, 4>& a, const std::array<float, 4>& b, auto all_outside_plane = [](const int axis, const std::array<NumericType, 4>& a,
const std::array<float, 4>& c, const bool positive_side) const std::array<NumericType, 4>& b, const std::array<NumericType, 4>& c,
const bool positive_side)
{ {
if (positive_side) if (positive_side)
return a[axis] > a[3] && b[axis] > b[3] && c[axis] > c[3]; return a[axis] > a[3] && b[axis] > b[3] && c[axis] > c[3];
@@ -309,9 +382,131 @@ namespace omath::projection
return false; return false;
} }
[[nodiscard]] bool is_aabb_culled_by_frustum(const primitives::Aabb<float>& 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(); // 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] : 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;
const auto pz = c >= NumericType{0} ? aabb.max.z : aabb.min.z;
if (a * px + b * py + c * pz + d < NumericType{0})
return true;
}
return false;
}
[[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
{
auto projected = get_view_projection_matrix()
* mat_column_from_vector<NumericType, Mat4X4Type::get_store_ordering()>(world_position);
const auto& w = projected.at(3, 0);
constexpr auto eps = std::numeric_limits<NumericType>::epsilon();
if (w <= eps)
return std::unexpected(Error::PERSPECTIVE_DIVIDER_LESS_EQ_ZERO);
projected /= w;
// ReSharper disable once CppTooWideScope
const auto clipped_automatically = clipping == ViewPortClipping::AUTO && is_ndc_out_of_bounds(projected);
if (clipped_automatically)
return std::unexpected(Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS);
// ReSharper disable once CppTooWideScope
constexpr auto z_min = depth_range == NDCDepthRange::ZERO_TO_ONE ? NumericType{0} : -NumericType{1};
const auto clipped_manually =
clipping == ViewPortClipping::MANUAL
&& (projected.at(2, 0) < z_min - eps || projected.at(2, 0) > NumericType{1} + eps);
if (clipped_manually)
return std::unexpected(Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS);
return Vector3<NumericType>{projected.at(0, 0), projected.at(1, 0), projected.at(2, 0)};
}
[[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();
if (!inv_view_proj)
return std::unexpected(Error::INV_VIEW_PROJ_MAT_DET_EQ_ZERO);
auto inverted_projection =
inv_view_proj.value() * mat_column_from_vector<NumericType, Mat4X4Type::get_store_ordering()>(ndc);
const auto& w = inverted_projection.at(3, 0);
if (std::abs(w) < std::numeric_limits<NumericType>::epsilon())
return std::unexpected(Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS);
inverted_projection /= w;
return Vector3<NumericType>{inverted_projection.at(0, 0), inverted_projection.at(1, 0),
inverted_projection.at(2, 0)};
}
template<ScreenStart screen_start = ScreenStart::TOP_LEFT_CORNER>
[[nodiscard("You must use world position")]]
std::expected<Vector3<NumericType>, Error>
screen_to_world(const Vector3<NumericType>& screen_pos) const noexcept
{
return view_port_to_world(screen_to_ndc<screen_start>(screen_pos));
}
template<ScreenStart screen_start = ScreenStart::TOP_LEFT_CORNER>
[[nodiscard("You must use world position")]]
std::expected<Vector3<NumericType>, Error>
screen_to_world(const Vector2<NumericType>& screen_pos) const noexcept
{
const auto& [x, y] = screen_pos;
return screen_to_world<screen_start>({x, y, 1});
}
protected:
ViewPort m_view_port{};
FieldOfView m_field_of_view;
mutable std::optional<Mat4X4Type> m_view_projection_matrix;
mutable std::optional<Mat4X4Type> m_projection_matrix;
mutable std::optional<Mat4X4Type> m_view_matrix;
NumericType m_far_plane_distance;
NumericType m_near_plane_distance;
ViewAnglesType m_view_angles;
Vector3<NumericType> m_origin;
private:
struct FrustumPlane final
{
NumericType a, b, c, d;
};
// Gribb-Hartmann: extract 6 frustum planes from the 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. // Each plane is (a, b, c, d) such that ax + by + cz + d >= 0 means inside.
@@ -322,22 +517,21 @@ namespace omath::projection
// Top = r3 - r1 // Top = r3 - r1
// Near = r3 + r2 ([-1,1]) or r2 ([0,1]) // Near = r3 + r2 ([-1,1]) or r2 ([0,1])
// Far = r3 - r2 // Far = r3 - r2
struct Plane final [[nodiscard("You must use frustum planes")]] std::array<FrustumPlane, 6> extract_frustum_planes() const noexcept
{ {
float a, b, c, d; const auto& m = get_view_projection_matrix();
};
const auto extract_plane = [&m](const int sign, const int row) -> Plane const auto extract_plane = [&m](const int sign, const int row) -> FrustumPlane
{ {
return { return {
m.at(3, 0) + static_cast<float>(sign) * m.at(row, 0), m.at(3, 0) + static_cast<NumericType>(sign) * m.at(row, 0),
m.at(3, 1) + static_cast<float>(sign) * m.at(row, 1), m.at(3, 1) + static_cast<NumericType>(sign) * m.at(row, 1),
m.at(3, 2) + static_cast<float>(sign) * m.at(row, 2), m.at(3, 2) + static_cast<NumericType>(sign) * m.at(row, 2),
m.at(3, 3) + static_cast<float>(sign) * m.at(row, 3), m.at(3, 3) + static_cast<NumericType>(sign) * m.at(row, 3),
}; };
}; };
std::array<Plane, 6> planes = { std::array<FrustumPlane, 6> planes = {
extract_plane(1, 0), // left extract_plane(1, 0), // left
extract_plane(-1, 0), // right extract_plane(-1, 0), // right
extract_plane(1, 1), // bottom extract_plane(1, 1), // bottom
@@ -351,122 +545,32 @@ namespace omath::projection
else else
planes[5] = extract_plane(1, 2); planes[5] = extract_plane(1, 2);
// For each plane, find the AABB corner most in the direction of the plane normal return planes;
// (the "positive vertex"). If it's outside, the entire AABB is outside.
for (const auto& [a, b, c, d] : planes)
{
const float px = a >= 0.f ? aabb.max.x : aabb.min.x;
const float py = b >= 0.f ? aabb.max.y : aabb.min.y;
const float pz = c >= 0.f ? aabb.max.z : aabb.min.z;
if (a * px + b * py + c * pz + d < 0.f)
return true;
} }
return false;
}
[[nodiscard]] std::expected<Vector3<float>, Error>
world_to_view_port(const Vector3<float>& world_position,
const ViewPortClipping& clipping = ViewPortClipping::AUTO) const noexcept
{
auto projected = get_view_projection_matrix()
* mat_column_from_vector<float, Mat4X4Type::get_store_ordering()>(world_position);
const auto& w = projected.at(3, 0);
constexpr auto eps = std::numeric_limits<float>::epsilon();
if (w <= eps)
return std::unexpected(Error::PERSPECTIVE_DIVIDER_LESS_EQ_ZERO);
projected /= w;
// ReSharper disable once CppTooWideScope
const auto clipped_automatically = clipping == ViewPortClipping::AUTO && is_ndc_out_of_bounds(projected);
if (clipped_automatically)
return std::unexpected(Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS);
// ReSharper disable once CppTooWideScope
constexpr auto z_min = depth_range == NDCDepthRange::ZERO_TO_ONE ? 0.0f : -1.0f;
const auto clipped_manually = clipping == ViewPortClipping::MANUAL && (projected.at(2, 0) < z_min - eps
|| projected.at(2, 0) > 1.0f + eps);
if (clipped_manually)
return std::unexpected(Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS);
return Vector3<float>{projected.at(0, 0), projected.at(1, 0), projected.at(2, 0)};
}
[[nodiscard]]
std::expected<Vector3<float>, Error> view_port_to_world(const Vector3<float>& ndc) const noexcept
{
const auto inv_view_proj = get_view_projection_matrix().inverted();
if (!inv_view_proj)
return std::unexpected(Error::INV_VIEW_PROJ_MAT_DET_EQ_ZERO);
auto inverted_projection =
inv_view_proj.value() * mat_column_from_vector<float, Mat4X4Type::get_store_ordering()>(ndc);
const auto& w = inverted_projection.at(3, 0);
if (std::abs(w) < std::numeric_limits<float>::epsilon())
return std::unexpected(Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS);
inverted_projection /= w;
return Vector3<float>{inverted_projection.at(0, 0), inverted_projection.at(1, 0),
inverted_projection.at(2, 0)};
}
template<ScreenStart screen_start = ScreenStart::TOP_LEFT_CORNER>
[[nodiscard]]
std::expected<Vector3<float>, Error> screen_to_world(const Vector3<float>& screen_pos) const noexcept
{
return view_port_to_world(screen_to_ndc<screen_start>(screen_pos));
}
template<ScreenStart screen_start = ScreenStart::TOP_LEFT_CORNER>
[[nodiscard]]
std::expected<Vector3<float>, Error> screen_to_world(const Vector2<float>& screen_pos) const noexcept
{
const auto& [x, y] = screen_pos;
return screen_to_world<screen_start>({x, y, 1.f});
}
protected:
ViewPort m_view_port{};
Angle<float, 0.f, 180.f, AngleFlags::Clamped> m_field_of_view;
mutable std::optional<Mat4X4Type> m_view_projection_matrix;
mutable std::optional<Mat4X4Type> m_projection_matrix;
mutable std::optional<Mat4X4Type> m_view_matrix;
float m_far_plane_distance;
float m_near_plane_distance;
ViewAnglesType m_view_angles;
Vector3<float> m_origin;
private:
template<class Type> 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<float>::epsilon(); constexpr auto eps = std::numeric_limits<NumericType>::epsilon();
const auto& data = ndc.raw_array(); const auto& data = ndc.raw_array();
// x and y are always in [-1, 1] // x and y are always in [-1, 1]
if (data[0] < -1.0f - eps || data[0] > 1.0f + eps) if (data[0] < -NumericType{1} - eps || data[0] > NumericType{1} + eps)
return true; return true;
if (data[1] < -1.0f - eps || data[1] > 1.0f + eps) if (data[1] < -NumericType{1} - eps || data[1] > NumericType{1} + eps)
return true; return true;
return is_ndc_z_value_out_of_bounds(data[2]); return is_ndc_z_value_out_of_bounds(data[2]);
} }
template<class ZType> 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 static bool is_ndc_z_value_out_of_bounds(const ZType& z_ndc) noexcept
{ {
constexpr auto eps = std::numeric_limits<float>::epsilon(); constexpr auto eps = std::numeric_limits<NumericType>::epsilon();
if constexpr (depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE) if constexpr (depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE)
return z_ndc < -1.0f - eps || z_ndc > 1.0f + eps; return z_ndc < -NumericType{1} - eps || z_ndc > NumericType{1} + eps;
if constexpr (depth_range == NDCDepthRange::ZERO_TO_ONE) if constexpr (depth_range == NDCDepthRange::ZERO_TO_ONE)
return z_ndc < 0.0f - eps || z_ndc > 1.0f + eps; return z_ndc < NumericType{0} - eps || z_ndc > NumericType{1} + eps;
std::unreachable(); std::unreachable();
} }
@@ -485,8 +589,8 @@ namespace omath::projection
v v
*/ */
[[nodiscard]] Vector3<float> [[nodiscard("You must use screen position")]] Vector3<NumericType>
ndc_to_screen_position_from_top_left_corner(const Vector3<float>& ndc) const noexcept ndc_to_screen_position_from_top_left_corner(const Vector3<NumericType>& ndc) const noexcept
{ {
/* /*
+------------------------> +------------------------>
@@ -499,11 +603,12 @@ namespace omath::projection
| |
*/ */
return {(ndc.x + 1.f) / 2.f * m_view_port.m_width, (ndc.y / -2.f + 0.5f) * m_view_port.m_height, ndc.z}; return {(ndc.x + NumericType{1}) / NumericType{2} * m_view_port.m_width,
(ndc.y / -NumericType{2} + NumericType{0.5}) * m_view_port.m_height, ndc.z};
} }
[[nodiscard]] Vector3<float> [[nodiscard("You must use screen position")]] Vector3<NumericType>
ndc_to_screen_position_from_bottom_left_corner(const Vector3<float>& ndc) const noexcept ndc_to_screen_position_from_bottom_left_corner(const Vector3<NumericType>& ndc) const noexcept
{ {
/* /*
^ ^
@@ -516,18 +621,20 @@ namespace omath::projection
| (0, 0) | (0, 0)
+------------------------> +------------------------>
*/ */
return {(ndc.x + 1.f) / 2.f * m_view_port.m_width, (ndc.y / 2.f + 0.5f) * m_view_port.m_height, ndc.z}; return {(ndc.x + NumericType{1}) / NumericType{2} * m_view_port.m_width,
(ndc.y / NumericType{2} + NumericType{0.5}) * m_view_port.m_height, ndc.z};
} }
template<ScreenStart screen_start = ScreenStart::TOP_LEFT_CORNER> template<ScreenStart screen_start = ScreenStart::TOP_LEFT_CORNER>
[[nodiscard]] Vector3<float> screen_to_ndc(const Vector3<float>& 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) if constexpr (screen_start == ScreenStart::TOP_LEFT_CORNER)
return {screen_pos.x / m_view_port.m_width * 2.f - 1.f, 1.f - screen_pos.y / m_view_port.m_height * 2.f, return {screen_pos.x / m_view_port.m_width * NumericType{2} - NumericType{1},
screen_pos.z}; NumericType{1} - screen_pos.y / m_view_port.m_height * NumericType{2}, screen_pos.z};
else if constexpr (screen_start == ScreenStart::BOTTOM_LEFT_CORNER) else if constexpr (screen_start == ScreenStart::BOTTOM_LEFT_CORNER)
return {screen_pos.x / m_view_port.m_width * 2.f - 1.f, return {screen_pos.x / m_view_port.m_width * NumericType{2} - NumericType{1},
(screen_pos.y / m_view_port.m_height - 0.5f) * 2.f, screen_pos.z}; (screen_pos.y / m_view_port.m_height - NumericType{0.5}) * NumericType{2}, screen_pos.z};
else else
std::unreachable(); std::unreachable();
} }
+61
View File
@@ -0,0 +1,61 @@
#!/bin/bash
# =============================================================================
# Gource Timelapse — renders the repository history as a video
# Requires: gource, ffmpeg
# =============================================================================
set -euo pipefail
# --- Config (override via env vars) ---
OUTPUT="${OUTPUT:-gource-timelapse.mp4}"
SECONDS_PER_DAY="${SECONDS_PER_DAY:-0.1}"
RESOLUTION="${RESOLUTION:-1920x1080}"
FPS="${FPS:-60}"
TITLE="${TITLE:-omath}"
# --- Dependency checks ---
for cmd in gource ffmpeg; do
if ! command -v "$cmd" &>/dev/null; then
echo "Error: '$cmd' is not installed."
echo " macOS: brew install $cmd"
echo " Linux: sudo apt install $cmd"
exit 1
fi
done
echo "----------------------------------------------------"
echo "Rendering gource timelapse → $OUTPUT"
echo " Resolution : $RESOLUTION"
echo " FPS : $FPS"
echo " Speed : ${SECONDS_PER_DAY}s per day"
echo "----------------------------------------------------"
gource \
--title "$TITLE" \
--seconds-per-day "$SECONDS_PER_DAY" \
--auto-skip-seconds 0.1 \
--time-scale 3 \
--max-files 0 \
--hide filenames \
--date-format "%Y-%m-%d" \
--multi-sampling \
--bloom-multiplier 0.5 \
--elasticity 0.05 \
--${RESOLUTION%x*}x${RESOLUTION#*x} \
--output-framerate "$FPS" \
--output-ppm-stream - \
| ffmpeg -y \
-r "$FPS" \
-f image2pipe \
-vcodec ppm \
-i - \
-vcodec libx264 \
-preset fast \
-pix_fmt yuv420p \
-crf 18 \
"$OUTPUT"
echo "----------------------------------------------------"
echo "Done: $OUTPUT"
echo "----------------------------------------------------"
+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_y<float, MatStoreType::ROW_MAJOR>(angles.roll)
* mat_rotation_axis_x<float, MatStoreType::ROW_MAJOR>(angles.pitch); * 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, 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 const float far, const NDCDepthRange ndc_depth_range) noexcept
{ {
if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE) 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); field_of_view, aspect_ratio, near, far);
if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE) 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); field_of_view, aspect_ratio, near, far);
std::unreachable(); 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, return calc_perspective_projection_matrix(fov.as_degrees(), view_port.aspect_ratio(), near, far,
ndc_depth_range); 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_y<float, MatStoreType::ROW_MAJOR>(angles.yaw)
* mat_rotation_axis_x<float, MatStoreType::ROW_MAJOR>(angles.pitch); * 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, 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 const float far, const NDCDepthRange ndc_depth_range) noexcept
{ {
if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE) 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); field_of_view, aspect_ratio, near, far);
if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE) 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); field_of_view, aspect_ratio, near, far);
std::unreachable(); std::unreachable();
+33 -16
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); 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 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); return mat_camera_view(forward_vector(angles), right_vector(angles), up_vector(angles), cam_origin);
@@ -38,25 +58,22 @@ namespace omath::iw_engine
Mat4X4 calc_perspective_projection_matrix(const float field_of_view, const float aspect_ratio, const float near, 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 const float far, const NDCDepthRange ndc_depth_range) noexcept
{ {
// NOTE: Need magic number to fix fov calculation, since IW engine inherit Quake proj matrix calculation // InfinityWard Engine (inherited from Quake) stores FOV as horizontal FOV at a 4:3
constexpr auto k_multiply_factor = 0.75f; // reference aspect. Convert to true vertical FOV, then delegate to the
// standard vertical-FOV left-handed builder with the caller's actual
const float fov_half_tan = std::tan(angles::degrees_to_radians(field_of_view) / 2.f) * k_multiply_factor; // aspect ratio.
// vfov = 2 · atan( tan(hfov_4:3 / 2) / (4/3) )
constexpr float k_source_reference_aspect = 4.f / 3.f;
const auto vertical_fov = angles::horizontal_fov_to_vertical(field_of_view, k_source_reference_aspect);
if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE) if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE)
return { return mat_perspective_left_handed_vertical_fov<
{1.f / (aspect_ratio * fov_half_tan), 0, 0, 0}, float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
{0, 1.f / (fov_half_tan), 0, 0}, vertical_fov, aspect_ratio, near, far);
{0, 0, far / (far - near), -(near * far) / (far - near)},
{0, 0, 1, 0},
};
if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE) if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE)
return { return mat_perspective_left_handed_vertical_fov<
{1.f / (aspect_ratio * fov_half_tan), 0, 0, 0}, float, MatStoreType::ROW_MAJOR, NDCDepthRange::NEGATIVE_ONE_TO_ONE>(
{0, 1.f / (fov_half_tan), 0, 0}, vertical_fov, aspect_ratio, near, far);
{0, 0, (far + near) / (far - near), -(2.f * far * near) / (far - near)},
{0, 0, 1, 0},
};
std::unreachable(); std::unreachable();
}; };
} // namespace omath::iw_engine } // 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_y<float, MatStoreType::COLUMN_MAJOR>(angles.yaw)
* mat_rotation_axis_x<float, MatStoreType::COLUMN_MAJOR>(angles.pitch); * 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, 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 const float far, const NDCDepthRange ndc_depth_range) noexcept
{ {
if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE) 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); field_of_view, aspect_ratio, near, far);
if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE) 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); field_of_view, aspect_ratio, near, far);
std::unreachable(); std::unreachable();
+33 -16
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); 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 Vector3<float> right_vector(const ViewAngles& angles) noexcept
{ {
const auto vec = rotation_matrix(angles) * mat_column_from_vector(k_abs_right); const auto vec = rotation_matrix(angles) * mat_column_from_vector(k_abs_right);
@@ -38,25 +58,22 @@ namespace omath::source_engine
Mat4X4 calc_perspective_projection_matrix(const float field_of_view, const float aspect_ratio, const float near, 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 const float far, const NDCDepthRange ndc_depth_range) noexcept
{ {
// NOTE: Need magic number to fix fov calculation, since source inherit Quake proj matrix calculation // Source (inherited from Quake) stores FOV as horizontal FOV at a 4:3
constexpr auto k_multiply_factor = 0.75f; // reference aspect. Convert to true vertical FOV, then delegate to the
// standard vertical-FOV left-handed builder with the caller's actual
const float fov_half_tan = std::tan(angles::degrees_to_radians(field_of_view) / 2.f) * k_multiply_factor; // aspect ratio.
// vfov = 2 · atan( tan(hfov_4:3 / 2) / (4/3) )
constexpr float k_source_reference_aspect = 4.f / 3.f;
const auto vertical_fov = angles::horizontal_fov_to_vertical(field_of_view, k_source_reference_aspect);
if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE) if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE)
return { return mat_perspective_left_handed_vertical_fov<
{1.f / (aspect_ratio * fov_half_tan), 0, 0, 0}, float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
{0, 1.f / (fov_half_tan), 0, 0}, vertical_fov, aspect_ratio, near, far);
{0, 0, far / (far - near), -(near * far) / (far - near)},
{0, 0, 1, 0},
};
if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE) if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE)
return { return mat_perspective_left_handed_vertical_fov<
{1.f / (aspect_ratio * fov_half_tan), 0, 0, 0}, float, MatStoreType::ROW_MAJOR, NDCDepthRange::NEGATIVE_ONE_TO_ONE>(
{0, 1.f / (fov_half_tan), 0, 0}, vertical_fov, aspect_ratio, near, far);
{0, 0, (far + near) / (far - near), -(2.f * far * near) / (far - near)},
{0, 0, 1, 0},
};
std::unreachable(); std::unreachable();
} }
} // namespace omath::source_engine } // 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_y<float, MatStoreType::ROW_MAJOR>(angles.yaw)
* mat_rotation_axis_x<float, MatStoreType::ROW_MAJOR>(angles.pitch); * 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, 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 const float far, const NDCDepthRange ndc_depth_range) noexcept
{ {
if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE) 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); field_of_view, aspect_ratio, near, far);
if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE) 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, NDCDepthRange::NEGATIVE_ONE_TO_ONE>(field_of_view, aspect_ratio,
near, far); near, far);
std::unreachable(); std::unreachable();
+47 -16
View File
@@ -2,45 +2,76 @@
// Created by Vlad on 3/22/2025. // Created by Vlad on 3/22/2025.
// //
#include "omath/engines/unreal_engine/formulas.hpp" #include "omath/engines/unreal_engine/formulas.hpp"
namespace omath::unreal_engine namespace omath::unreal_engine
{ {
Vector3<float> forward_vector(const ViewAngles& angles) noexcept Vector3<double> forward_vector(const ViewAngles& angles) noexcept
{ {
const auto vec = rotation_matrix(angles) * mat_column_from_vector(k_abs_forward); const auto vec = rotation_matrix(angles) * mat_column_from_vector(k_abs_forward);
return {vec.at(0, 0), vec.at(1, 0), vec.at(2, 0)}; return {vec.at(0, 0), vec.at(1, 0), vec.at(2, 0)};
} }
Vector3<float> right_vector(const ViewAngles& angles) noexcept Vector3<double> right_vector(const ViewAngles& angles) noexcept
{ {
const auto vec = rotation_matrix(angles) * mat_column_from_vector(k_abs_right); const auto vec = rotation_matrix(angles) * mat_column_from_vector(k_abs_right);
return {vec.at(0, 0), vec.at(1, 0), vec.at(2, 0)}; return {vec.at(0, 0), vec.at(1, 0), vec.at(2, 0)};
} }
Vector3<float> up_vector(const ViewAngles& angles) noexcept Vector3<double> up_vector(const ViewAngles& angles) noexcept
{ {
const auto vec = rotation_matrix(angles) * mat_column_from_vector(k_abs_up); const auto vec = rotation_matrix(angles) * mat_column_from_vector(k_abs_up);
return {vec.at(0, 0), vec.at(1, 0), vec.at(2, 0)}; return {vec.at(0, 0), vec.at(1, 0), vec.at(2, 0)};
} }
Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3<float>& cam_origin) noexcept Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3<double>& cam_origin) noexcept
{ {
return mat_camera_view<float, MatStoreType::ROW_MAJOR>(forward_vector(angles), -right_vector(angles), return mat_camera_view<double, MatStoreType::ROW_MAJOR>(forward_vector(angles), right_vector(angles),
up_vector(angles), cam_origin); up_vector(angles), cam_origin);
} }
Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept
{ {
return mat_rotation_axis_x<float, MatStoreType::ROW_MAJOR>(angles.roll) // UE FRotator is intrinsic Z-Y-X (Yaw → Pitch → Roll applied in local
* mat_rotation_axis_z<float, MatStoreType::ROW_MAJOR>(angles.yaw) // frame), which for column-vector composition is Rz·Ry·Rx.
* mat_rotation_axis_y<float, MatStoreType::ROW_MAJOR>(angles.pitch); // Pitch and roll axes in omath spin opposite to UE's convention, so
// both carry a sign flip.
return mat_rotation_axis_z<double, MatStoreType::ROW_MAJOR>(angles.yaw)
* mat_rotation_axis_y<double, MatStoreType::ROW_MAJOR>(-angles.pitch)
* mat_rotation_axis_x<double, MatStoreType::ROW_MAJOR>(-angles.roll);
} }
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>(
field_of_view, aspect_ratio, near, far);
return mat_perspective_left_handed(field_of_view, aspect_ratio, near, far);
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
{
// UE stores horizontal FOV in FMinimalViewInfo — use the left-handed
// horizontal-FOV builder directly.
if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE)
return mat_perspective_left_handed_horizontal_fov<
double, 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_horizontal_fov<
double, MatStoreType::ROW_MAJOR, NDCDepthRange::NEGATIVE_ONE_TO_ONE>(
field_of_view, aspect_ratio, near, far);
std::unreachable();
} }
} // namespace omath::unreal_engine } // namespace omath::unreal_engine
@@ -6,20 +6,20 @@
namespace omath::unreal_engine namespace omath::unreal_engine
{ {
ViewAngles CameraTrait::calc_look_at_angle(const Vector3<float>& cam_origin, const Vector3<float>& look_at) noexcept ViewAngles CameraTrait::calc_look_at_angle(const Vector3<double>& cam_origin, const Vector3<double>& look_at) noexcept
{ {
const auto direction = (look_at - cam_origin).normalized(); const auto direction = (look_at - cam_origin).normalized();
return {PitchAngle::from_radians(-std::asin(direction.z)), return {PitchAngle::from_radians(std::asin(direction.z)),
YawAngle::from_radians(std::atan2(direction.y, direction.x)), RollAngle::from_radians(0.f)}; YawAngle::from_radians(std::atan2(direction.y, direction.x)), RollAngle::from_radians(0.f)};
} }
Mat4X4 CameraTrait::calc_view_matrix(const ViewAngles& angles, const Vector3<float>& cam_origin) noexcept Mat4X4 CameraTrait::calc_view_matrix(const ViewAngles& angles, const Vector3<double>& cam_origin) noexcept
{ {
return unreal_engine::calc_view_matrix(angles, cam_origin); return unreal_engine::calc_view_matrix(angles, cam_origin);
} }
Mat4X4 CameraTrait::calc_projection_matrix(const projection::FieldOfView& fov, Mat4X4 CameraTrait::calc_projection_matrix(const projection::FieldOfView& fov,
const projection::ViewPort& view_port, const float near, const projection::ViewPort& view_port, const double near,
const float far, const NDCDepthRange ndc_depth_range) noexcept const double far, const NDCDepthRange ndc_depth_range) noexcept
{ {
return calc_perspective_projection_matrix(fov.as_degrees(), view_port.aspect_ratio(), near, far, return calc_perspective_projection_matrix(fov.as_degrees(), view_port.aspect_ratio(), near, far,
ndc_depth_range); ndc_depth_range);
+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 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(); 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); m_renderer->add_polyline({points.data(), points.size()}, box_color, thickness);
if (fill_color.value().w > 0.f) if (fill_color.value().w > 0.f)
@@ -17,41 +21,46 @@ namespace omath::hud
return *this; return *this;
} }
EntityOverlay& EntityOverlay::add_cornered_2d_box(const Color& box_color, const Color& fill_color, 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 = const auto corner_line_length =
std::abs((m_canvas.top_left_corner - m_canvas.top_right_corner).x * corner_ratio_len); std::abs((m_canvas.top_left_corner - m_canvas.top_right_corner).x * corner_ratio_len);
if (fill_color.value().w > 0.f) if (fill_color.value().w > 0.f)
add_2d_box(fill_color, fill_color); 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 // Left Side
m_renderer->add_line(m_canvas.top_left_corner, draw_corner_line(m_canvas.top_left_corner,
m_canvas.top_left_corner + Vector2<float>{corner_line_length, 0.f}, box_color, thickness); m_canvas.top_left_corner + Vector2<float>{corner_line_length, 0.f});
m_renderer->add_line(m_canvas.top_left_corner, draw_corner_line(m_canvas.top_left_corner,
m_canvas.top_left_corner + Vector2<float>{0.f, corner_line_length}, box_color, thickness); m_canvas.top_left_corner + Vector2<float>{0.f, corner_line_length});
m_renderer->add_line(m_canvas.bottom_left_corner, draw_corner_line(m_canvas.bottom_left_corner,
m_canvas.bottom_left_corner - Vector2<float>{0.f, corner_line_length}, box_color, m_canvas.bottom_left_corner - Vector2<float>{0.f, corner_line_length});
thickness);
m_renderer->add_line(m_canvas.bottom_left_corner, draw_corner_line(m_canvas.bottom_left_corner,
m_canvas.bottom_left_corner + Vector2<float>{corner_line_length, 0.f}, box_color, m_canvas.bottom_left_corner + Vector2<float>{corner_line_length, 0.f});
thickness);
// Right Side // Right Side
m_renderer->add_line(m_canvas.top_right_corner, draw_corner_line(m_canvas.top_right_corner,
m_canvas.top_right_corner - Vector2<float>{corner_line_length, 0.f}, box_color, thickness); m_canvas.top_right_corner - Vector2<float>{corner_line_length, 0.f});
m_renderer->add_line(m_canvas.top_right_corner, draw_corner_line(m_canvas.top_right_corner,
m_canvas.top_right_corner + Vector2<float>{0.f, corner_line_length}, box_color, thickness); m_canvas.top_right_corner + Vector2<float>{0.f, corner_line_length});
m_renderer->add_line(m_canvas.bottom_right_corner, draw_corner_line(m_canvas.bottom_right_corner,
m_canvas.bottom_right_corner - Vector2<float>{0.f, corner_line_length}, box_color, m_canvas.bottom_right_corner - Vector2<float>{0.f, corner_line_length});
thickness);
m_renderer->add_line(m_canvas.bottom_right_corner, draw_corner_line(m_canvas.bottom_right_corner,
m_canvas.bottom_right_corner - Vector2<float>{corner_line_length, 0.f}, box_color, m_canvas.bottom_right_corner - Vector2<float>{corner_line_length, 0.f});
thickness);
return *this; return *this;
} }
@@ -591,12 +600,13 @@ namespace omath::hud
// ── widget dispatch ─────────────────────────────────────────────────────── // ── widget dispatch ───────────────────────────────────────────────────────
void EntityOverlay::dispatch(const widget::Box& box) 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) 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) void EntityOverlay::dispatch(const widget::DashedBox& dashed_box)
+5
View File
@@ -17,8 +17,13 @@ namespace omath::lua
register_vec2(omath_table); register_vec2(omath_table);
register_vec3(omath_table); register_vec3(omath_table);
register_vec4(omath_table); register_vec4(omath_table);
register_matrices(omath_table);
register_quaternion(omath_table);
register_color(omath_table); register_color(omath_table);
register_hud(omath_table);
register_triangle(omath_table); register_triangle(omath_table);
register_3d_primitives(omath_table);
register_collision(omath_table);
register_shared_types(omath_table); register_shared_types(omath_table);
register_engines(omath_table); register_engines(omath_table);
register_pattern_scan(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
+51 -20
View File
@@ -14,6 +14,8 @@
#include <omath/engines/unreal_engine/camera.hpp> #include <omath/engines/unreal_engine/camera.hpp>
#include <sol/sol.hpp> #include <sol/sol.hpp>
#include <string_view> #include <string_view>
#include <type_traits>
#include <utility>
namespace namespace
{ {
@@ -78,50 +80,79 @@ namespace
} }
// Register an engine: alias shared types, register unique Camera // Register an engine: alias shared types, register unique Camera
template<class EngineTraits> template<class EngineTraits, class ArithmeticType = float>
requires std::is_arithmetic_v<ArithmeticType>
void register_engine(sol::table& omath_table, const char* subtable_name) void register_engine(sol::table& omath_table, const char* subtable_name)
{ {
using PitchAngle = typename EngineTraits::PitchAngle; using PitchAngle = typename EngineTraits::PitchAngle;
using ViewAngles = typename EngineTraits::ViewAngles; using ViewAngles = typename EngineTraits::ViewAngles;
using Camera = typename EngineTraits::Camera; 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 engine_table = omath_table[subtable_name].get_or_create<sol::table>();
auto types = omath_table["_types"].get<sol::table>(); auto types = omath_table["_types"].get<sol::table>();
set_engine_aliases<PitchAngle, ViewAngles>(engine_table, types); set_engine_aliases<PitchAngle, ViewAngles>(engine_table, types);
engine_table.new_usertype<Camera>( auto camera_type = engine_table.new_usertype<Camera>(
"Camera", "Camera",
sol::constructors<Camera(const omath::Vector3<float>&, const ViewAngles&, sol::constructors<Camera(const omath::Vector3<ArithmeticType>&, const ViewAngles&,
const omath::projection::ViewPort&, const omath::projection::FieldOfView&, const omath::projection::ViewPort&, const omath::projection::FieldOfView&,
float, float)>(), 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,
"world_to_screen", camera_type["look_at"] = &Camera::look_at;
[](const Camera& cam, const omath::Vector3<float>& pos) camera_type["get_forward"] = &Camera::get_forward;
-> std::tuple<sol::optional<omath::Vector3<float>>, sol::optional<std::string>> 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;
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); auto result = cam.world_to_screen(pos);
if (result) if (result)
return {*result, sol::nullopt}; return {*result, sol::nullopt};
return {sol::nullopt, projection_error_to_string(result.error())}; return {sol::nullopt, projection_error_to_string(result.error())};
}, };
"screen_to_world", camera_type["screen_to_world"] = [](const Camera& cam, const omath::Vector3<ArithmeticType>& pos)
[](const Camera& cam, const omath::Vector3<float>& pos) -> std::tuple<sol::optional<omath::Vector3<ArithmeticType>>, sol::optional<std::string>>
-> std::tuple<sol::optional<omath::Vector3<float>>, sol::optional<std::string>>
{ {
auto result = cam.screen_to_world(pos); auto result = cam.screen_to_world(pos);
if (result) if (result)
return {*result, sol::nullopt}; return {*result, sol::nullopt};
return {sol::nullopt, projection_error_to_string(result.error())}; return {sol::nullopt, projection_error_to_string(result.error())};
}); };
} }
// ---- Engine trait structs ----------------------------------------------- // ---- Engine trait structs -----------------------------------------------
@@ -224,7 +255,7 @@ namespace omath::lua
register_engine<IWEngineTraits>(omath_table, "iw"); register_engine<IWEngineTraits>(omath_table, "iw");
register_engine<SourceEngineTraits>(omath_table, "source"); register_engine<SourceEngineTraits>(omath_table, "source");
register_engine<UnityEngineTraits>(omath_table, "unity"); register_engine<UnityEngineTraits>(omath_table, "unity");
register_engine<UnrealEngineTraits>(omath_table, "unreal"); register_engine<UnrealEngineTraits, double>(omath_table, "unreal");
register_engine<CryEngineTraits>(omath_table, "cry"); register_engine<CryEngineTraits>(omath_table, "cry");
} }
} // namespace omath::lua::detail } // namespace omath::lua::detail
+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
@@ -87,11 +87,11 @@ namespace omath::pathfinding
const auto current_node = current_node_it->second; const auto current_node = current_node_it->second;
closed_list.emplace(current, current_node);
if (current == end_vertex) if (current == end_vertex)
return reconstruct_final_path(closed_list, current); return reconstruct_final_path(closed_list, current);
closed_list.emplace(current, current_node);
for (const auto& neighbor: nav_mesh.get_neighbors(current)) for (const auto& neighbor: nav_mesh.get_neighbors(current))
{ {
if (closed_list.contains(neighbor)) if (closed_list.contains(neighbor))
+92
View File
@@ -0,0 +1,92 @@
//
// Created by orange on 4/12/2026.
//
#include "omath/pathfinding/walk_bot.hpp"
#include "omath/pathfinding/a_star.hpp"
namespace omath::pathfinding
{
WalkBot::WalkBot(const std::shared_ptr<NavigationMesh>& mesh, const float min_node_distance)
: m_nav_mesh(mesh), m_min_node_distance(min_node_distance) {}
void WalkBot::set_nav_mesh(const std::shared_ptr<NavigationMesh>& mesh)
{
m_nav_mesh = mesh;
}
void WalkBot::set_min_node_distance(const float distance)
{
m_min_node_distance = distance;
}
void WalkBot::set_target(const Vector3<float>& target)
{
m_target = target;
}
void WalkBot::reset()
{
m_last_visited.reset();
}
void WalkBot::update(const Vector3<float>& bot_position)
{
if (!m_target.has_value())
return;
if (m_target->distance_to(bot_position) <= m_min_node_distance)
{
if (m_on_status_update.has_value())
m_on_status_update->operator()(WalkBotStatus::FINISHED);
return;
}
if (!m_on_next_path_node.has_value())
return;
const auto nav_mesh = m_nav_mesh.lock();
if (!nav_mesh)
{
if (m_on_status_update.has_value())
m_on_status_update->operator()(WalkBotStatus::IDLE);
return;
}
const auto path = Astar::find_path(bot_position, *m_target, *nav_mesh);
if (path.empty())
{
if (m_on_status_update.has_value())
m_on_status_update->operator()(WalkBotStatus::IDLE);
return;
}
const auto& nearest = path.front();
// Record the nearest node as visited once we are close enough to it.
if (nearest.distance_to(bot_position) <= m_min_node_distance)
m_last_visited = nearest;
// If the nearest node was already visited, advance to the next one so
// we never oscillate back to a node we just left.
// If the bot was displaced (blown back), nearest will be an unvisited
// node, so we route to it first before continuing forward.
if (m_last_visited.has_value() && *m_last_visited == nearest && path.size() > 1)
m_on_next_path_node->operator()(path[1]);
else
m_on_next_path_node->operator()(nearest);
if (m_on_status_update.has_value())
m_on_status_update->operator()(WalkBotStatus::PATHING);
}
void WalkBot::on_path(const std::function<void(const Vector3<float>&)>& callback)
{
m_on_next_path_node = callback;
}
void WalkBot::on_status(const std::function<void(WalkBotStatus)>& callback)
{
m_on_status_update = callback;
}
} // namespace omath::pathfinding
@@ -14,8 +14,8 @@
namespace omath::projectile_prediction namespace omath::projectile_prediction
{ {
std::optional<Vector3<float>> std::optional<Vector3<float>>
ProjPredEngineAvx2::maybe_calculate_aim_point([[maybe_unused]] const Projectile& projectile, ProjPredEngineAvx2::maybe_calculate_aim_point([[maybe_unused]] const Projectile<float>& projectile,
[[maybe_unused]] const Target& target) const [[maybe_unused]] const Target<float>& target) const
{ {
#if defined(OMATH_USE_AVX2) && defined(__i386__) && defined(__x86_64__) #if defined(OMATH_USE_AVX2) && defined(__i386__) && defined(__x86_64__)
const float bullet_gravity = m_gravity_constant * projectile.m_gravity_scale; const float bullet_gravity = m_gravity_constant * projectile.m_gravity_scale;
@@ -124,9 +124,9 @@ namespace omath::projectile_prediction
std::format("{} AVX2 feature is not enabled!", std::source_location::current().function_name())); std::format("{} AVX2 feature is not enabled!", std::source_location::current().function_name()));
#endif #endif
} }
std::optional<AimAngles> std::optional<AimAngles<float>>
ProjPredEngineAvx2::maybe_calculate_aim_angles([[maybe_unused]] const Projectile& projectile, ProjPredEngineAvx2::maybe_calculate_aim_angles([[maybe_unused]] const Projectile<float>& projectile,
[[maybe_unused]] const Target& target) const [[maybe_unused]] const Target<float>& target) const
{ {
#if defined(OMATH_USE_AVX2) && defined(__i386__) && defined(__x86_64__) #if defined(OMATH_USE_AVX2) && defined(__i386__) && defined(__x86_64__)
const float bullet_gravity = m_gravity_constant * projectile.m_gravity_scale; const float bullet_gravity = m_gravity_constant * projectile.m_gravity_scale;
@@ -201,7 +201,7 @@ namespace omath::projectile_prediction
const Vector3 delta = target_pos - projectile.m_origin; const Vector3 delta = target_pos - projectile.m_origin;
const float yaw = angles::radians_to_degrees(std::atan2(delta.y, delta.x)); const float yaw = angles::radians_to_degrees(std::atan2(delta.y, delta.x));
return AimAngles{*pitch, yaw}; return AimAngles<float>{*pitch, yaw};
} }
} }
} }
+2 -2
View File
@@ -29,11 +29,11 @@ TEST(unit_test_cry_engine, look_at_right)
} }
TEST(unit_test_cry_engine, look_at_up) 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 // ReSharper disable once CppTooWideScopeInitStatement
const auto dir_vector = cry_engine::forward_vector(angles); 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); EXPECT_NEAR(result, etalon, 0.0001f);
} }
@@ -453,3 +453,184 @@ TEST(unit_test_frostbite_engine, ViewAnglesAsVector3NormalizedYaw)
EXPECT_NEAR(vec.y, -90.f, 0.01f); EXPECT_NEAR(vec.y, -90.f, 0.01f);
} }
// ---------------------------------------------------------------------------
// extract_projection_params
// ---------------------------------------------------------------------------
// Tolerance: tan/atan round-trip in single precision introduces ~1e-5 rad
// error, which is ~5.7e-4 degrees.
static constexpr float k_fov_tolerance_deg = 0.001f;
static constexpr float k_aspect_tolerance = 1e-5f;
TEST(unit_test_frostbite_engine, ExtractProjectionParams_BasicRoundTrip)
{
// Build a matrix with known inputs and verify both outputs are recovered.
constexpr float fov_deg = 60.f;
constexpr float aspect = 16.f / 9.f;
const auto mat = omath::frostbite_engine::calc_perspective_projection_matrix(
fov_deg, aspect, 0.1f, 1000.f, omath::NDCDepthRange::ZERO_TO_ONE);
const auto [fov, ar] = omath::frostbite_engine::Camera::extract_projection_params(mat);
EXPECT_NEAR(fov.as_degrees(), fov_deg, k_fov_tolerance_deg);
EXPECT_NEAR(ar, aspect, k_aspect_tolerance);
}
TEST(unit_test_frostbite_engine, ExtractProjectionParams_NegOneToOneDepthRange)
{
// The FOV/aspect encoding in rows 0 and 1 is identical for both NDC
// depth ranges, so extraction must work the same way.
constexpr float fov_deg = 75.f;
constexpr float aspect = 4.f / 3.f;
const auto mat = omath::frostbite_engine::calc_perspective_projection_matrix(
fov_deg, aspect, 0.1f, 500.f, omath::NDCDepthRange::NEGATIVE_ONE_TO_ONE);
const auto [fov, ar] = omath::frostbite_engine::Camera::extract_projection_params(mat);
EXPECT_NEAR(fov.as_degrees(), fov_deg, k_fov_tolerance_deg);
EXPECT_NEAR(ar, aspect, k_aspect_tolerance);
}
TEST(unit_test_frostbite_engine, ExtractProjectionParams_Fov45)
{
constexpr float fov_deg = 45.f;
constexpr float aspect = 16.f / 9.f;
const auto mat = omath::frostbite_engine::calc_perspective_projection_matrix(
fov_deg, aspect, 0.01f, 1000.f);
const auto [fov, ar] = omath::frostbite_engine::Camera::extract_projection_params(mat);
EXPECT_NEAR(fov.as_degrees(), fov_deg, k_fov_tolerance_deg);
EXPECT_NEAR(ar, aspect, k_aspect_tolerance);
}
TEST(unit_test_frostbite_engine, ExtractProjectionParams_Fov90)
{
constexpr float fov_deg = 90.f;
constexpr float aspect = 16.f / 9.f;
const auto mat = omath::frostbite_engine::calc_perspective_projection_matrix(
fov_deg, aspect, 0.01f, 1000.f);
const auto [fov, ar] = omath::frostbite_engine::Camera::extract_projection_params(mat);
EXPECT_NEAR(fov.as_degrees(), fov_deg, k_fov_tolerance_deg);
EXPECT_NEAR(ar, aspect, k_aspect_tolerance);
}
TEST(unit_test_frostbite_engine, ExtractProjectionParams_Fov120)
{
constexpr float fov_deg = 120.f;
constexpr float aspect = 16.f / 9.f;
const auto mat = omath::frostbite_engine::calc_perspective_projection_matrix(
fov_deg, aspect, 0.01f, 1000.f);
const auto [fov, ar] = omath::frostbite_engine::Camera::extract_projection_params(mat);
EXPECT_NEAR(fov.as_degrees(), fov_deg, k_fov_tolerance_deg);
EXPECT_NEAR(ar, aspect, k_aspect_tolerance);
}
TEST(unit_test_frostbite_engine, ExtractProjectionParams_AspectRatio_4by3)
{
constexpr float fov_deg = 60.f;
constexpr float aspect = 4.f / 3.f;
const auto mat = omath::frostbite_engine::calc_perspective_projection_matrix(
fov_deg, aspect, 0.1f, 500.f);
const auto [fov, ar] = omath::frostbite_engine::Camera::extract_projection_params(mat);
EXPECT_NEAR(fov.as_degrees(), fov_deg, k_fov_tolerance_deg);
EXPECT_NEAR(ar, aspect, k_aspect_tolerance);
}
TEST(unit_test_frostbite_engine, ExtractProjectionParams_AspectRatio_Ultrawide)
{
constexpr float fov_deg = 90.f;
constexpr float aspect = 21.f / 9.f;
const auto mat = omath::frostbite_engine::calc_perspective_projection_matrix(
fov_deg, aspect, 0.1f, 500.f);
const auto [fov, ar] = omath::frostbite_engine::Camera::extract_projection_params(mat);
EXPECT_NEAR(fov.as_degrees(), fov_deg, k_fov_tolerance_deg);
EXPECT_NEAR(ar, aspect, k_aspect_tolerance);
}
TEST(unit_test_frostbite_engine, ExtractProjectionParams_AspectRatio_Square)
{
constexpr float fov_deg = 90.f;
constexpr float aspect = 1.f;
const auto mat = omath::frostbite_engine::calc_perspective_projection_matrix(
fov_deg, aspect, 0.1f, 500.f);
const auto [fov, ar] = omath::frostbite_engine::Camera::extract_projection_params(mat);
EXPECT_NEAR(fov.as_degrees(), fov_deg, k_fov_tolerance_deg);
EXPECT_NEAR(ar, aspect, k_aspect_tolerance);
}
TEST(unit_test_frostbite_engine, ExtractProjectionParams_FovAndAspectAreIndependent)
{
// Changing only FOV must not affect recovered aspect ratio, and vice versa.
constexpr float aspect = 16.f / 9.f;
for (const float fov_deg : {45.f, 60.f, 90.f, 110.f})
{
const auto mat = omath::frostbite_engine::calc_perspective_projection_matrix(
fov_deg, aspect, 0.1f, 1000.f);
const auto [fov, ar] = omath::frostbite_engine::Camera::extract_projection_params(mat);
EXPECT_NEAR(fov.as_degrees(), fov_deg, k_fov_tolerance_deg);
EXPECT_NEAR(ar, aspect, k_aspect_tolerance);
}
}
TEST(unit_test_frostbite_engine, ExtractProjectionParams_ViaCamera_RoundTrip)
{
// End-to-end: construct a Camera, retrieve its projection matrix, then
// recover the FOV and aspect ratio and compare against the original inputs.
constexpr auto fov_in = omath::projection::FieldOfView::from_degrees(90.f);
constexpr float aspect = 1920.f / 1080.f;
const auto cam = omath::frostbite_engine::Camera(
{0.f, 0.f, 0.f}, {}, {1920.f, 1080.f}, fov_in, 0.01f, 1000.f);
const auto [fov_out, ar_out] =
omath::frostbite_engine::Camera::extract_projection_params(cam.get_projection_matrix());
EXPECT_NEAR(fov_out.as_degrees(), fov_in.as_degrees(), k_fov_tolerance_deg);
EXPECT_NEAR(ar_out, aspect, k_aspect_tolerance);
}
TEST(unit_test_frostbite_engine, ExtractProjectionParams_ViaCamera_AfterFovChange)
{
// Verify that the extracted FOV tracks the camera's FOV after set_field_of_view().
auto cam = omath::frostbite_engine::Camera(
{0.f, 0.f, 0.f}, {}, {1920.f, 1080.f},
omath::projection::FieldOfView::from_degrees(60.f), 0.01f, 1000.f);
cam.set_field_of_view(omath::projection::FieldOfView::from_degrees(110.f));
const auto [fov, ar] =
omath::frostbite_engine::Camera::extract_projection_params(cam.get_projection_matrix());
EXPECT_NEAR(fov.as_degrees(), 110.f, k_fov_tolerance_deg);
EXPECT_NEAR(ar, 1920.f / 1080.f, k_aspect_tolerance);
}
TEST(unit_test_frostbite_engine, ExtractProjectionParams_ViaCamera_AfterViewportChange)
{
// Verify that the extracted aspect ratio tracks the viewport after set_view_port().
auto cam = omath::frostbite_engine::Camera(
{0.f, 0.f, 0.f}, {}, {1920.f, 1080.f},
omath::projection::FieldOfView::from_degrees(90.f), 0.01f, 1000.f);
cam.set_view_port({1280.f, 720.f});
const auto [fov, ar] =
omath::frostbite_engine::Camera::extract_projection_params(cam.get_projection_matrix());
EXPECT_NEAR(fov.as_degrees(), 90.f, k_fov_tolerance_deg);
EXPECT_NEAR(ar, 1280.f / 720.f, k_aspect_tolerance);
}
+56 -55
View File
@@ -44,46 +44,46 @@ static void expect_matrix_near(const MatT& a, const MatT& b, float eps = 1e-5f)
#include <omath/engines/cry_engine/traits/pred_engine_trait.hpp> #include <omath/engines/cry_engine/traits/pred_engine_trait.hpp>
// Helper: verify that zero offset matches default-initialized offset behavior // Helper: verify that zero offset matches default-initialized offset behavior
template<typename Trait> template<typename Trait, typename AT = float>
static void verify_launch_offset_at_time_zero(const Vector3<float>& origin, const Vector3<float>& offset) static void verify_launch_offset_at_time_zero(const Vector3<AT>& origin, const Vector3<AT>& offset)
{ {
projectile_prediction::Projectile p; projectile_prediction::Projectile<AT> p;
p.m_origin = origin; p.m_origin = origin;
p.m_launch_offset = offset; p.m_launch_offset = offset;
p.m_launch_speed = 100.f; p.m_launch_speed = static_cast<AT>(100);
p.m_gravity_scale = 1.f; p.m_gravity_scale = static_cast<AT>(1);
const auto pos = Trait::predict_projectile_position(p, 0.f, 0.f, 0.f, 9.81f); const auto pos = Trait::predict_projectile_position(p, AT{0}, AT{0}, AT{0}, static_cast<AT>(9.81));
const auto expected = origin + offset; const auto expected = origin + offset;
EXPECT_NEAR(pos.x, expected.x, 1e-4f); EXPECT_NEAR(static_cast<double>(pos.x), static_cast<double>(expected.x), 1e-4);
EXPECT_NEAR(pos.y, expected.y, 1e-4f); EXPECT_NEAR(static_cast<double>(pos.y), static_cast<double>(expected.y), 1e-4);
EXPECT_NEAR(pos.z, expected.z, 1e-4f); EXPECT_NEAR(static_cast<double>(pos.z), static_cast<double>(expected.z), 1e-4);
} }
template<typename Trait> template<typename Trait, typename AT = float>
static void verify_zero_offset_matches_default() static void verify_zero_offset_matches_default()
{ {
projectile_prediction::Projectile p; projectile_prediction::Projectile<AT> p;
p.m_origin = {10.f, 20.f, 30.f}; p.m_origin = {static_cast<AT>(10), static_cast<AT>(20), static_cast<AT>(30)};
p.m_launch_offset = {0.f, 0.f, 0.f}; p.m_launch_offset = {};
p.m_launch_speed = 50.f; p.m_launch_speed = static_cast<AT>(50);
p.m_gravity_scale = 1.f; p.m_gravity_scale = static_cast<AT>(1);
projectile_prediction::Projectile p2; projectile_prediction::Projectile<AT> p2;
p2.m_origin = {10.f, 20.f, 30.f}; p2.m_origin = {static_cast<AT>(10), static_cast<AT>(20), static_cast<AT>(30)};
p2.m_launch_speed = 50.f; p2.m_launch_speed = static_cast<AT>(50);
p2.m_gravity_scale = 1.f; p2.m_gravity_scale = static_cast<AT>(1);
const auto pos1 = Trait::predict_projectile_position(p, 15.f, 30.f, 1.f, 9.81f); const auto pos1 = Trait::predict_projectile_position(p, static_cast<AT>(15), static_cast<AT>(30), static_cast<AT>(1), static_cast<AT>(9.81));
const auto pos2 = Trait::predict_projectile_position(p2, 15.f, 30.f, 1.f, 9.81f); const auto pos2 = Trait::predict_projectile_position(p2, static_cast<AT>(15), static_cast<AT>(30), static_cast<AT>(1), static_cast<AT>(9.81));
#if defined(__x86_64__) || defined(_M_X64) || defined(__aarch64__) || defined(_M_ARM64) #if defined(__x86_64__) || defined(_M_X64) || defined(__aarch64__) || defined(_M_ARM64)
constexpr float tol = 1e-6f; constexpr double tol = 1e-6;
#else #else
constexpr float tol = 1e-4f; constexpr double tol = 1e-4;
#endif #endif
EXPECT_NEAR(pos1.x, pos2.x, tol); EXPECT_NEAR(static_cast<double>(pos1.x), static_cast<double>(pos2.x), tol);
EXPECT_NEAR(pos1.y, pos2.y, tol); EXPECT_NEAR(static_cast<double>(pos1.y), static_cast<double>(pos2.y), tol);
EXPECT_NEAR(pos1.z, pos2.z, tol); EXPECT_NEAR(static_cast<double>(pos1.z), static_cast<double>(pos2.z), tol);
} }
TEST(LaunchOffsetTests, Source_OffsetAtTimeZero) TEST(LaunchOffsetTests, Source_OffsetAtTimeZero)
@@ -128,11 +128,11 @@ TEST(LaunchOffsetTests, Unity_ZeroOffsetMatchesDefault)
} }
TEST(LaunchOffsetTests, Unreal_OffsetAtTimeZero) TEST(LaunchOffsetTests, Unreal_OffsetAtTimeZero)
{ {
verify_launch_offset_at_time_zero<unreal_engine::PredEngineTrait>({0, 0, 0}, {5, 3, -2}); verify_launch_offset_at_time_zero<unreal_engine::PredEngineTrait, double>({0, 0, 0}, {5, 3, -2});
} }
TEST(LaunchOffsetTests, Unreal_ZeroOffsetMatchesDefault) TEST(LaunchOffsetTests, Unreal_ZeroOffsetMatchesDefault)
{ {
verify_zero_offset_matches_default<unreal_engine::PredEngineTrait>(); verify_zero_offset_matches_default<unreal_engine::PredEngineTrait, double>();
} }
TEST(LaunchOffsetTests, CryEngine_OffsetAtTimeZero) TEST(LaunchOffsetTests, CryEngine_OffsetAtTimeZero)
{ {
@@ -401,38 +401,38 @@ TEST(TraitTests, Unreal_Pred_And_Mesh_And_Camera)
{ {
namespace e = omath::unreal_engine; namespace e = omath::unreal_engine;
projectile_prediction::Projectile p; projectile_prediction::Projectile<double> p;
p.m_origin = {0.f, 0.f, 0.f}; p.m_origin = {0.0, 0.0, 0.0};
p.m_launch_speed = 10.f; p.m_launch_speed = 10.0;
p.m_gravity_scale = 1.f; p.m_gravity_scale = 1.0;
const auto pos = e::PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 1.f, 9.81f); const auto pos = e::PredEngineTrait::predict_projectile_position(p, 0.0, 0.0, 1.0, 9.81);
EXPECT_NEAR(pos.x, 10.f, 1e-4f); EXPECT_NEAR(pos.x, 10.0, 1e-4);
EXPECT_NEAR(pos.y, -9.81f * 0.5f, 1e-4f); EXPECT_NEAR(pos.y, -9.81 * 0.5, 1e-4);
projectile_prediction::Target t; projectile_prediction::Target<double> t;
t.m_origin = {0.f, 5.f, 0.f}; t.m_origin = {0.0, 5.0, 0.0};
t.m_velocity = {2.f, 0.f, 0.f}; t.m_velocity = {2.0, 0.0, 0.0};
t.m_is_airborne = true; t.m_is_airborne = true;
const auto pred = e::PredEngineTrait::predict_target_position(t, 2.f, 9.81f); const auto pred = e::PredEngineTrait::predict_target_position(t, 2.0, 9.81);
EXPECT_NEAR(pred.x, 4.f, 1e-6f); EXPECT_NEAR(pred.x, 4.0, 1e-6);
EXPECT_NEAR(pred.y, 5.f - 9.81f * (2.f * 2.f) * 0.5f, 1e-6f); EXPECT_NEAR(pred.y, 5.0 - 9.81 * (2.0 * 2.0) * 0.5, 1e-6);
EXPECT_NEAR(e::PredEngineTrait::calc_vector_2d_distance({3.f, 0.f, 4.f}), 5.f, 1e-6f); EXPECT_NEAR(e::PredEngineTrait::calc_vector_2d_distance({3.0, 0.0, 4.0}), 5.0, 1e-6);
EXPECT_NEAR(e::PredEngineTrait::get_vector_height_coordinate({1.f, 2.5f, 3.f}), 2.5f, 1e-6f); EXPECT_NEAR(e::PredEngineTrait::get_vector_height_coordinate({1.0, 2.5, 3.0}), 2.5, 1e-6);
std::optional<float> pitch = 45.f; std::optional<double> pitch = 45.0;
auto vp = e::PredEngineTrait::calc_viewpoint_from_angles(p, {10.f, 0.f, 0.f}, pitch); auto vp = e::PredEngineTrait::calc_viewpoint_from_angles(p, Vector3<double>{10.0, 0.0, 0.0}, pitch);
EXPECT_NEAR(vp.z, 0.f + 10.f * std::tan(angles::degrees_to_radians(45.f)), 1e-6f); EXPECT_NEAR(vp.z, 0.0 + 10.0 * std::tan(angles::degrees_to_radians(45.0)), 1e-6);
Vector3<float> origin{0.f, 0.f, 0.f}; Vector3<double> origin{0.0, 0.0, 0.0};
Vector3<float> view_to{1.f, 1.f, 1.f}; Vector3<double> view_to{1.0, 1.0, 1.0};
const auto pitch_calc = e::PredEngineTrait::calc_direct_pitch_angle(origin, view_to); const auto pitch_calc = e::PredEngineTrait::calc_direct_pitch_angle(origin, view_to);
const auto dir = (view_to - origin).normalized(); const auto dir = (view_to - origin).normalized();
EXPECT_NEAR(pitch_calc, angles::radians_to_degrees(std::asin(dir.z)), 1e-3f); EXPECT_NEAR(pitch_calc, angles::radians_to_degrees(std::asin(dir.z)), 1e-3);
const auto yaw_calc = e::PredEngineTrait::calc_direct_yaw_angle(origin, view_to); const auto yaw_calc = e::PredEngineTrait::calc_direct_yaw_angle(origin, view_to);
EXPECT_NEAR(yaw_calc, angles::radians_to_degrees(std::atan2(dir.y, dir.x)), 1e-3f); EXPECT_NEAR(yaw_calc, angles::radians_to_degrees(std::atan2(dir.y, dir.x)), 1e-3);
e::ViewAngles va; e::ViewAngles va;
expect_matrix_near(e::MeshTrait::rotation_matrix(va), e::rotation_matrix(va)); expect_matrix_near(e::MeshTrait::rotation_matrix(va), e::rotation_matrix(va));
@@ -448,8 +448,8 @@ TEST(TraitTests, Unreal_Pred_And_Mesh_And_Camera)
// non-airborne // non-airborne
t.m_is_airborne = false; t.m_is_airborne = false;
const auto pred_ground_unreal = e::PredEngineTrait::predict_target_position(t, 2.f, 9.81f); const auto pred_ground_unreal = e::PredEngineTrait::predict_target_position(t, 2.0, 9.81);
EXPECT_NEAR(pred_ground_unreal.x, 4.f, 1e-6f); EXPECT_NEAR(pred_ground_unreal.x, 4.0, 1e-6);
} }
// ── NDC Depth Range tests for Source and CryEngine camera traits ──────────── // ── NDC Depth Range tests for Source and CryEngine camera traits ────────────
@@ -499,10 +499,11 @@ TEST(NDCDepthRangeTests, CryEngine_BothDepthRanges)
// ── Verify Z mapping for ZERO_TO_ONE across all engines ───────────────────── // ── Verify Z mapping for ZERO_TO_ONE across all engines ─────────────────────
// Helper: projects a point at given z through a left-handed projection matrix and returns NDC z // Helper: projects a point at given z through a left-handed projection matrix and returns NDC z
static float project_z_lh(const Mat<4, 4>& proj, float z) template<class Type = float, MatStoreType Store = MatStoreType::ROW_MAJOR>
static float project_z_lh(const Mat<4, 4, Type, Store>& proj, float z)
{ {
auto clip = proj * mat_column_from_vector<float>({0, 0, z}); auto clip = proj * mat_column_from_vector<Type, Store>({0, 0, static_cast<Type>(z)});
return clip.at(2, 0) / clip.at(3, 0); return static_cast<float>(clip.at(2, 0) / clip.at(3, 0));
} }
TEST(NDCDepthRangeTests, Source_ZeroToOne_ZRange) TEST(NDCDepthRangeTests, Source_ZeroToOne_ZRange)
@@ -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);
}
+8 -8
View File
@@ -32,7 +32,7 @@ TEST(unit_test_unreal_engine, ForwardVectorRotationPitch)
{ {
omath::unreal_engine::ViewAngles angles; omath::unreal_engine::ViewAngles angles;
angles.pitch = omath::unreal_engine::PitchAngle::from_degrees(-90.f); angles.pitch = omath::unreal_engine::PitchAngle::from_degrees(90.f);
const auto forward = omath::unreal_engine::forward_vector(angles); const auto forward = omath::unreal_engine::forward_vector(angles);
EXPECT_NEAR(forward.x, omath::unreal_engine::k_abs_up.x, 0.00001f); EXPECT_NEAR(forward.x, omath::unreal_engine::k_abs_up.x, 0.00001f);
@@ -44,7 +44,7 @@ TEST(unit_test_unreal_engine, ForwardVectorRotationRoll)
{ {
omath::unreal_engine::ViewAngles angles; omath::unreal_engine::ViewAngles angles;
angles.roll = omath::unreal_engine::RollAngle::from_degrees(-90.f); angles.roll = omath::unreal_engine::RollAngle::from_degrees(90.f);
const auto forward = omath::unreal_engine::up_vector(angles); const auto forward = omath::unreal_engine::up_vector(angles);
EXPECT_NEAR(forward.x, omath::unreal_engine::k_abs_right.x, 0.00001f); EXPECT_NEAR(forward.x, omath::unreal_engine::k_abs_right.x, 0.00001f);
@@ -111,7 +111,7 @@ TEST(unit_test_unreal_engine, CameraSetAndGetOrigin)
{ {
auto cam = omath::unreal_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, {}, 0.01f, 1000.f); auto cam = omath::unreal_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, {}, 0.01f, 1000.f);
EXPECT_EQ(cam.get_origin(), omath::Vector3<float>{}); EXPECT_EQ(cam.get_origin(), omath::Vector3<double>{});
cam.set_field_of_view(omath::projection::FieldOfView::from_degrees(50.f)); cam.set_field_of_view(omath::projection::FieldOfView::from_degrees(50.f));
EXPECT_EQ(cam.get_field_of_view().as_degrees(), 50.f); EXPECT_EQ(cam.get_field_of_view().as_degrees(), 50.f);
@@ -129,7 +129,7 @@ TEST(unit_test_unreal_engine, loook_at_random_all_axis)
std::size_t failed_points = 0; std::size_t failed_points = 0;
for (int i = 0; i < 100; i++) for (int i = 0; i < 100; i++)
{ {
const auto position_to_look = omath::Vector3<float>{dist(gen), dist(gen), dist(gen)}; const auto position_to_look = omath::Vector3<double>{dist(gen), dist(gen), dist(gen)};
if (cam.get_origin().distance_to(position_to_look) < 10) if (cam.get_origin().distance_to(position_to_look) < 10)
continue; continue;
@@ -151,7 +151,7 @@ TEST(unit_test_unreal_engine, loook_at_random_all_axis)
TEST(unit_test_unreal_engine, loook_at_random_x_axis) TEST(unit_test_unreal_engine, loook_at_random_x_axis)
{ {
std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source std::mt19937 gen(std::random_device{}()); // Seed with a non-deterministic source
std::uniform_real_distribution<float> dist(-1000.f, 1000.f); std::uniform_real_distribution<double> dist(-1000.f, 1000.f);
constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f); constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f);
auto cam = omath::unreal_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.001f, 10000.f); auto cam = omath::unreal_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.001f, 10000.f);
@@ -159,7 +159,7 @@ TEST(unit_test_unreal_engine, loook_at_random_x_axis)
std::size_t failed_points = 0; std::size_t failed_points = 0;
for (int i = 0; i < 1000; i++) for (int i = 0; i < 1000; i++)
{ {
const auto position_to_look = omath::Vector3<float>{dist(gen), dist(gen), dist(gen)}; const auto position_to_look = omath::Vector3<double>{dist(gen), dist(gen), dist(gen)};
if (cam.get_origin().distance_to(position_to_look) < 10) if (cam.get_origin().distance_to(position_to_look) < 10)
continue; continue;
@@ -190,7 +190,7 @@ TEST(unit_test_unreal_engine, loook_at_random_y_axis)
std::size_t failed_points = 0; std::size_t failed_points = 0;
for (int i = 0; i < 1000; i++) for (int i = 0; i < 1000; i++)
{ {
const auto position_to_look = omath::Vector3<float>{0.f, dist(gen), 0.f}; const auto position_to_look = omath::Vector3<double>{0.f, dist(gen), 0.f};
if (cam.get_origin().distance_to(position_to_look) < 10) if (cam.get_origin().distance_to(position_to_look) < 10)
continue; continue;
@@ -221,7 +221,7 @@ TEST(unit_test_unreal_engine, loook_at_random_z_axis)
std::size_t failed_points = 0; std::size_t failed_points = 0;
for (int i = 0; i < 1000; i++) for (int i = 0; i < 1000; i++)
{ {
const auto position_to_look = omath::Vector3<float>{0.f, 0.f, dist(gen)}; const auto position_to_look = omath::Vector3<double>{0.f, 0.f, dist(gen)};
if (cam.get_origin().distance_to(position_to_look) < 10) if (cam.get_origin().distance_to(position_to_look) < 10)
continue; continue;
+22 -12
View File
@@ -20,8 +20,8 @@
#include <vector> #include <vector>
#if defined(__linux__) #if defined(__linux__)
# include <unistd.h>
#include <fcntl.h> #include <fcntl.h>
#include <unistd.h>
#if defined(__ANDROID__) #if defined(__ANDROID__)
#if __ANDROID_API__ >= 30 #if __ANDROID_API__ >= 30
#include <sys/mman.h> #include <sys/mman.h>
@@ -57,9 +57,11 @@ public:
MemFdFile(MemFdFile&& o) noexcept MemFdFile(MemFdFile&& o) noexcept
: m_path(std::move(o.m_path)) : m_path(std::move(o.m_path))
#if defined(OMATH_TEST_USE_MEMFD) #if defined(OMATH_TEST_USE_MEMFD)
, m_fd(o.m_fd) ,
m_fd(o.m_fd)
#else #else
, m_temp_path(std::move(o.m_temp_path)) ,
m_temp_path(std::move(o.m_temp_path))
#endif #endif
{ {
#if defined(OMATH_TEST_USE_MEMFD) #if defined(OMATH_TEST_USE_MEMFD)
@@ -69,9 +71,15 @@ public:
#endif #endif
} }
[[nodiscard]] bool valid() const { return !m_path.empty(); } [[nodiscard]] bool valid() const
{
return !m_path.empty();
}
[[nodiscard]] const std::filesystem::path& path() const { return m_path; } [[nodiscard]] const std::filesystem::path& path() const
{
return m_path;
}
static MemFdFile create(const std::vector<std::uint8_t>& data) static MemFdFile create(const std::vector<std::uint8_t>& data)
{ {
@@ -163,25 +171,27 @@ inline std::vector<std::uint8_t> build_minimal_pe(const std::vector<std::uint8_t
std::vector<std::uint8_t> buf(data_off + section_bytes.size(), 0u); std::vector<std::uint8_t> buf(data_off + section_bytes.size(), 0u);
buf[0] = 'M'; buf[1] = 'Z'; buf[0] = 'M';
buf[1] = 'Z';
std::memcpy(buf.data() + 0x3Cu, &e_lfanew, 4); std::memcpy(buf.data() + 0x3Cu, &e_lfanew, 4);
buf[nt_off] = 'P'; buf[nt_off + 1] = 'E'; buf[nt_off] = 'P';
buf[nt_off + 1] = 'E';
const std::uint16_t machine = 0x8664u, num_sections = 1u; constexpr std::uint16_t machine = 0x8664u, num_sections = 1u;
std::memcpy(buf.data() + fh_off, &machine, 2); std::memcpy(buf.data() + fh_off, &machine, 2);
std::memcpy(buf.data() + fh_off + 2, &num_sections, 2); std::memcpy(buf.data() + fh_off + 2, &num_sections, 2);
std::memcpy(buf.data() + fh_off + 16, &size_opt, 2); std::memcpy(buf.data() + fh_off + 16, &size_opt, 2);
const std::uint16_t magic = 0x20Bu; constexpr std::uint16_t magic = 0x20Bu;
std::memcpy(buf.data() + oh_off, &magic, 2); std::memcpy(buf.data() + oh_off, &magic, 2);
const char name[8] = {'.','t','e','x','t',0,0,0}; constexpr char name[8] = {'.', 't', 'e', 'x', 't', 0, 0, 0};
std::memcpy(buf.data() + sh_off, name, 8); std::memcpy(buf.data() + sh_off, name, 8);
const auto vsize = static_cast<std::uint32_t>(section_bytes.size()); const auto vsize = static_cast<std::uint32_t>(section_bytes.size());
const std::uint32_t vaddr = 0x1000u; constexpr std::uint32_t vaddr = 0x1000u;
const auto ptr_raw = static_cast<std::uint32_t>(data_off); constexpr auto ptr_raw = static_cast<std::uint32_t>(data_off);
std::memcpy(buf.data() + sh_off + 8, &vsize, 4); std::memcpy(buf.data() + sh_off + 8, &vsize, 4);
std::memcpy(buf.data() + sh_off + 12, &vaddr, 4); std::memcpy(buf.data() + sh_off + 12, &vaddr, 4);
std::memcpy(buf.data() + sh_off + 16, &vsize, 4); std::memcpy(buf.data() + sh_off + 16, &vsize, 4);
+7 -5
View File
@@ -40,8 +40,9 @@ TEST(AStarExtra, TrivialNeighbor)
nav.m_vertex_map[v2] = {v1}; nav.m_vertex_map[v2] = {v1};
const auto path = Astar::find_path(v1, v2, nav); const auto path = Astar::find_path(v1, v2, nav);
ASSERT_EQ(path.size(), 1u); ASSERT_EQ(path.size(), 2u);
EXPECT_EQ(path.front(), v2); EXPECT_EQ(path.front(), v1);
EXPECT_EQ(path.back(), v2);
} }
TEST(AStarExtra, StartEqualsGoal) TEST(AStarExtra, StartEqualsGoal)
@@ -101,7 +102,7 @@ TEST(AStarExtra, LongerPathAvoidsBlock)
constexpr Vector3<float> goal = idx(2, 1); constexpr Vector3<float> goal = idx(2, 1);
const auto path = Astar::find_path(start, goal, nav); const auto path = Astar::find_path(start, goal, nav);
ASSERT_FALSE(path.empty()); ASSERT_FALSE(path.empty());
EXPECT_EQ(path.front(), goal); EXPECT_EQ(path.back(), goal);
} }
TEST(AstarTests, TrivialDirectNeighborPath) TEST(AstarTests, TrivialDirectNeighborPath)
@@ -114,8 +115,9 @@ TEST(AstarTests, TrivialDirectNeighborPath)
nav.m_vertex_map.emplace(v2, std::vector<Vector3<float>>{v1}); nav.m_vertex_map.emplace(v2, std::vector<Vector3<float>>{v1});
const auto path = Astar::find_path(v1, v2, nav); const auto path = Astar::find_path(v1, v2, nav);
ASSERT_EQ(path.size(), 1u); ASSERT_EQ(path.size(), 2u);
EXPECT_EQ(path.front(), v2); EXPECT_EQ(path.front(), v1);
EXPECT_EQ(path.back(), v2);
} }
TEST(AstarTests, NoPathWhenDisconnected) TEST(AstarTests, NoPathWhenDisconnected)
+240
View File
@@ -0,0 +1,240 @@
//
// Created by Vladislav on 19.04.2026.
//
#include <gtest/gtest.h>
#include "omath/3d_primitives/aabb.hpp"
using AABB = omath::primitives::Aabb<float>;
using Vec3 = omath::Vector3<float>;
// --- center() ---
TEST(AabbTests, CenterOfSymmetricBox)
{
constexpr AABB box{{-1.f, -1.f, -1.f}, {1.f, 1.f, 1.f}};
constexpr auto c = box.center();
EXPECT_FLOAT_EQ(c.x, 0.f);
EXPECT_FLOAT_EQ(c.y, 0.f);
EXPECT_FLOAT_EQ(c.z, 0.f);
}
TEST(AabbTests, CenterOfOffsetBox)
{
constexpr AABB box{{1.f, 2.f, 3.f}, {3.f, 6.f, 7.f}};
constexpr auto c = box.center();
EXPECT_FLOAT_EQ(c.x, 2.f);
EXPECT_FLOAT_EQ(c.y, 4.f);
EXPECT_FLOAT_EQ(c.z, 5.f);
}
TEST(AabbTests, CenterOfDegenerateBox)
{
constexpr AABB box{{5.f, 5.f, 5.f}, {5.f, 5.f, 5.f}};
constexpr auto c = box.center();
EXPECT_FLOAT_EQ(c.x, 5.f);
EXPECT_FLOAT_EQ(c.y, 5.f);
EXPECT_FLOAT_EQ(c.z, 5.f);
}
// --- extents() ---
TEST(AabbTests, ExtentsOfSymmetricBox)
{
constexpr AABB box{{-2.f, -3.f, -4.f}, {2.f, 3.f, 4.f}};
constexpr auto e = box.extents();
EXPECT_FLOAT_EQ(e.x, 2.f);
EXPECT_FLOAT_EQ(e.y, 3.f);
EXPECT_FLOAT_EQ(e.z, 4.f);
}
TEST(AabbTests, ExtentsOfUnitBox)
{
constexpr AABB box{{0.f, 0.f, 0.f}, {2.f, 2.f, 2.f}};
constexpr auto e = box.extents();
EXPECT_FLOAT_EQ(e.x, 1.f);
EXPECT_FLOAT_EQ(e.y, 1.f);
EXPECT_FLOAT_EQ(e.z, 1.f);
}
TEST(AabbTests, ExtentsOfDegenerateBox)
{
constexpr AABB box{{3.f, 3.f, 3.f}, {3.f, 3.f, 3.f}};
constexpr auto e = box.extents();
EXPECT_FLOAT_EQ(e.x, 0.f);
EXPECT_FLOAT_EQ(e.y, 0.f);
EXPECT_FLOAT_EQ(e.z, 0.f);
}
using UpAxis = omath::primitives::UpAxis;
// --- top() ---
TEST(AabbTests, TopYUpSymmetricBox)
{
constexpr AABB box{{-1.f, -2.f, -3.f}, {1.f, 2.f, 3.f}};
constexpr auto t = box.top<UpAxis::Y>();
EXPECT_FLOAT_EQ(t.x, 0.f);
EXPECT_FLOAT_EQ(t.y, 2.f);
EXPECT_FLOAT_EQ(t.z, 0.f);
}
TEST(AabbTests, TopYUpOffsetBox)
{
constexpr AABB box{{1.f, 4.f, 2.f}, {3.f, 10.f, 6.f}};
constexpr auto t = box.top<UpAxis::Y>();
EXPECT_FLOAT_EQ(t.x, 2.f);
EXPECT_FLOAT_EQ(t.y, 10.f);
EXPECT_FLOAT_EQ(t.z, 4.f);
}
TEST(AabbTests, TopZUpSymmetricBox)
{
constexpr AABB box{{-1.f, -2.f, -3.f}, {1.f, 2.f, 3.f}};
constexpr auto t = box.top<UpAxis::Z>();
EXPECT_FLOAT_EQ(t.x, 0.f);
EXPECT_FLOAT_EQ(t.y, 0.f);
EXPECT_FLOAT_EQ(t.z, 3.f);
}
TEST(AabbTests, TopZUpOffsetBox)
{
constexpr AABB box{{1.f, 4.f, 2.f}, {3.f, 10.f, 6.f}};
constexpr auto t = box.top<UpAxis::Z>();
EXPECT_FLOAT_EQ(t.x, 2.f);
EXPECT_FLOAT_EQ(t.y, 7.f);
EXPECT_FLOAT_EQ(t.z, 6.f);
}
TEST(AabbTests, TopDefaultIsYUp)
{
constexpr AABB box{{0.f, 0.f, 0.f}, {2.f, 4.f, 6.f}};
EXPECT_EQ(box.top(), box.top<UpAxis::Y>());
}
// --- bottom() ---
TEST(AabbTests, BottomYUpSymmetricBox)
{
constexpr AABB box{{-1.f, -2.f, -3.f}, {1.f, 2.f, 3.f}};
constexpr auto b = box.bottom<UpAxis::Y>();
EXPECT_FLOAT_EQ(b.x, 0.f);
EXPECT_FLOAT_EQ(b.y, -2.f);
EXPECT_FLOAT_EQ(b.z, 0.f);
}
TEST(AabbTests, BottomYUpOffsetBox)
{
constexpr AABB box{{1.f, 4.f, 2.f}, {3.f, 10.f, 6.f}};
constexpr auto b = box.bottom<UpAxis::Y>();
EXPECT_FLOAT_EQ(b.x, 2.f);
EXPECT_FLOAT_EQ(b.y, 4.f);
EXPECT_FLOAT_EQ(b.z, 4.f);
}
TEST(AabbTests, BottomZUpSymmetricBox)
{
constexpr AABB box{{-1.f, -2.f, -3.f}, {1.f, 2.f, 3.f}};
constexpr auto b = box.bottom<UpAxis::Z>();
EXPECT_FLOAT_EQ(b.x, 0.f);
EXPECT_FLOAT_EQ(b.y, 0.f);
EXPECT_FLOAT_EQ(b.z, -3.f);
}
TEST(AabbTests, BottomZUpOffsetBox)
{
constexpr AABB box{{1.f, 4.f, 2.f}, {3.f, 10.f, 6.f}};
constexpr auto b = box.bottom<UpAxis::Z>();
EXPECT_FLOAT_EQ(b.x, 2.f);
EXPECT_FLOAT_EQ(b.y, 7.f);
EXPECT_FLOAT_EQ(b.z, 2.f);
}
TEST(AabbTests, BottomDefaultIsYUp)
{
constexpr AABB box{{0.f, 0.f, 0.f}, {2.f, 4.f, 6.f}};
EXPECT_EQ(box.bottom(), box.bottom<UpAxis::Y>());
}
TEST(AabbTests, TopAndBottomAreSymmetric)
{
constexpr AABB box{{-1.f, -2.f, -3.f}, {1.f, 2.f, 3.f}};
EXPECT_FLOAT_EQ(box.top<UpAxis::Y>().y, -box.bottom<UpAxis::Y>().y);
EXPECT_FLOAT_EQ(box.top<UpAxis::Z>().z, -box.bottom<UpAxis::Z>().z);
}
// --- is_collide() ---
TEST(AabbTests, OverlappingBoxesCollide)
{
constexpr AABB a{{-1.f, -1.f, -1.f}, {1.f, 1.f, 1.f}};
constexpr AABB b{{0.f, 0.f, 0.f}, {2.f, 2.f, 2.f}};
EXPECT_TRUE(a.is_collide(b));
EXPECT_TRUE(b.is_collide(a));
}
TEST(AabbTests, SeparatedBoxesDoNotCollide)
{
constexpr AABB a{{-1.f, -1.f, -1.f}, {1.f, 1.f, 1.f}};
constexpr AABB b{{2.f, 2.f, 2.f}, {4.f, 4.f, 4.f}};
EXPECT_FALSE(a.is_collide(b));
EXPECT_FALSE(b.is_collide(a));
}
TEST(AabbTests, TouchingFacesCollide)
{
constexpr AABB a{{-1.f, -1.f, -1.f}, {1.f, 1.f, 1.f}};
constexpr AABB b{{1.f, -1.f, -1.f}, {3.f, 1.f, 1.f}};
EXPECT_TRUE(a.is_collide(b));
EXPECT_TRUE(b.is_collide(a));
}
TEST(AabbTests, ContainedBoxCollides)
{
constexpr AABB outer{{-3.f, -3.f, -3.f}, {3.f, 3.f, 3.f}};
constexpr AABB inner{{-1.f, -1.f, -1.f}, {1.f, 1.f, 1.f}};
EXPECT_TRUE(outer.is_collide(inner));
EXPECT_TRUE(inner.is_collide(outer));
}
TEST(AabbTests, SeparatedOnXAxisDoNotCollide)
{
constexpr AABB a{{0.f, 0.f, 0.f}, {1.f, 1.f, 1.f}};
constexpr AABB b{{2.f, 0.f, 0.f}, {3.f, 1.f, 1.f}};
EXPECT_FALSE(a.is_collide(b));
}
TEST(AabbTests, SeparatedOnYAxisDoNotCollide)
{
constexpr AABB a{{0.f, 0.f, 0.f}, {1.f, 1.f, 1.f}};
constexpr AABB b{{0.f, 2.f, 0.f}, {1.f, 3.f, 1.f}};
EXPECT_FALSE(a.is_collide(b));
}
TEST(AabbTests, SeparatedOnZAxisDoNotCollide)
{
constexpr AABB a{{0.f, 0.f, 0.f}, {1.f, 1.f, 1.f}};
constexpr AABB b{{0.f, 0.f, 2.f}, {1.f, 1.f, 3.f}};
EXPECT_FALSE(a.is_collide(b));
}
TEST(AabbTests, IdenticalBoxesCollide)
{
constexpr AABB a{{-1.f, -1.f, -1.f}, {1.f, 1.f, 1.f}};
EXPECT_TRUE(a.is_collide(a));
}
TEST(AabbTests, DegeneratePointBoxCollidesWhenInsideOther)
{
constexpr AABB box{{-1.f, -1.f, -1.f}, {1.f, 1.f, 1.f}};
constexpr AABB point{{0.f, 0.f, 0.f}, {0.f, 0.f, 0.f}};
EXPECT_TRUE(box.is_collide(point));
EXPECT_TRUE(point.is_collide(box));
}
TEST(AabbTests, DegeneratePointBoxDoesNotCollideWhenOutside)
{
constexpr AABB box{{-1.f, -1.f, -1.f}, {1.f, 1.f, 1.f}};
constexpr AABB point{{5.f, 0.f, 0.f}, {5.f, 0.f, 0.f}};
EXPECT_FALSE(box.is_collide(point));
EXPECT_FALSE(point.is_collide(box));
}
@@ -0,0 +1,275 @@
//
// Created by Vladislav on 20.04.2026.
//
#include <gtest/gtest.h>
#include <omath/engines/cry_engine/traits/pred_engine_trait.hpp>
#include <omath/projectile_prediction/projectile.hpp>
#include <omath/projectile_prediction/target.hpp>
using namespace omath;
using namespace omath::cry_engine;
// ---- predict_projectile_position ----
TEST(CryPredEngineTrait, PredictProjectilePositionAtTimeZero)
{
projectile_prediction::Projectile p;
p.m_origin = {1.f, 2.f, 3.f};
p.m_launch_offset = {4.f, 5.f, 6.f};
p.m_launch_speed = 100.f;
p.m_gravity_scale = 1.f;
const auto pos = PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 0.f, 9.81f);
// At t=0 no velocity is applied, just origin+offset
EXPECT_NEAR(pos.x, 5.f, 1e-4f);
EXPECT_NEAR(pos.y, 7.f, 1e-4f);
EXPECT_NEAR(pos.z, 9.f, 1e-4f);
}
TEST(CryPredEngineTrait, PredictProjectilePositionZeroAnglesForwardIsY)
{
// Cry engine forward = +Y. At pitch=0, yaw=0 the projectile travels along +Y.
projectile_prediction::Projectile p;
p.m_origin = {0.f, 0.f, 0.f};
p.m_launch_speed = 10.f;
p.m_gravity_scale = 0.f; // no gravity so we isolate direction
const auto pos = PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 1.f, 9.81f);
EXPECT_NEAR(pos.x, 0.f, 1e-4f);
EXPECT_NEAR(pos.y, 10.f, 1e-4f);
EXPECT_NEAR(pos.z, 0.f, 1e-4f);
}
TEST(CryPredEngineTrait, PredictProjectilePositionGravityDropsZ)
{
projectile_prediction::Projectile p;
p.m_origin = {0.f, 0.f, 0.f};
p.m_launch_speed = 10.f;
p.m_gravity_scale = 1.f;
const auto pos = PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 2.f, 9.81f);
// z = 0 - (9.81 * 1) * (4) * 0.5 = -19.62
EXPECT_NEAR(pos.z, -9.81f * 4.f * 0.5f, 1e-3f);
}
TEST(CryPredEngineTrait, PredictProjectilePositionGravityScaleZeroNoZDrop)
{
projectile_prediction::Projectile p;
p.m_origin = {0.f, 0.f, 0.f};
p.m_launch_speed = 10.f;
p.m_gravity_scale = 0.f;
const auto pos = PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 3.f, 9.81f);
EXPECT_NEAR(pos.z, 0.f, 1e-4f);
}
TEST(CryPredEngineTrait, PredictProjectilePositionWithLaunchOffset)
{
projectile_prediction::Projectile p;
p.m_origin = {5.f, 0.f, 0.f};
p.m_launch_offset = {0.f, 0.f, 2.f};
p.m_launch_speed = 10.f;
p.m_gravity_scale = 0.f;
const auto pos = PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 1.f, 0.f);
// launch position = {5, 0, 2}, travels along +Y by 10
EXPECT_NEAR(pos.x, 5.f, 1e-4f);
EXPECT_NEAR(pos.y, 10.f, 1e-4f);
EXPECT_NEAR(pos.z, 2.f, 1e-4f);
}
// ---- predict_target_position ----
TEST(CryPredEngineTrait, PredictTargetPositionGroundedStationary)
{
projectile_prediction::Target t;
t.m_origin = {10.f, 20.f, 5.f};
t.m_velocity = {0.f, 0.f, 0.f};
t.m_is_airborne = false;
const auto pred = PredEngineTrait::predict_target_position(t, 5.f, 9.81f);
EXPECT_NEAR(pred.x, 10.f, 1e-6f);
EXPECT_NEAR(pred.y, 20.f, 1e-6f);
EXPECT_NEAR(pred.z, 5.f, 1e-6f);
}
TEST(CryPredEngineTrait, PredictTargetPositionGroundedMoving)
{
projectile_prediction::Target t;
t.m_origin = {0.f, 0.f, 0.f};
t.m_velocity = {3.f, 4.f, 0.f};
t.m_is_airborne = false;
const auto pred = PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
EXPECT_NEAR(pred.x, 6.f, 1e-6f);
EXPECT_NEAR(pred.y, 8.f, 1e-6f);
EXPECT_NEAR(pred.z, 0.f, 1e-6f); // grounded — no gravity
}
TEST(CryPredEngineTrait, PredictTargetPositionAirborneGravityDropsZ)
{
projectile_prediction::Target t;
t.m_origin = {0.f, 0.f, 20.f};
t.m_velocity = {0.f, 0.f, 0.f};
t.m_is_airborne = true;
const auto pred = PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
// z = 20 - 9.81 * 4 * 0.5 = 20 - 19.62 = 0.38
EXPECT_NEAR(pred.z, 20.f - 9.81f * 4.f * 0.5f, 1e-4f);
}
TEST(CryPredEngineTrait, PredictTargetPositionAirborneMovingWithGravity)
{
projectile_prediction::Target t;
t.m_origin = {0.f, 0.f, 50.f};
t.m_velocity = {10.f, 5.f, 0.f};
t.m_is_airborne = true;
const auto pred = PredEngineTrait::predict_target_position(t, 3.f, 9.81f);
EXPECT_NEAR(pred.x, 30.f, 1e-4f);
EXPECT_NEAR(pred.y, 15.f, 1e-4f);
EXPECT_NEAR(pred.z, 50.f - 9.81f * 9.f * 0.5f, 1e-4f);
}
// ---- calc_vector_2d_distance ----
TEST(CryPredEngineTrait, CalcVector2dDistance_3_4_5)
{
EXPECT_NEAR(PredEngineTrait::calc_vector_2d_distance({3.f, 4.f, 999.f}), 5.f, 1e-5f);
}
TEST(CryPredEngineTrait, CalcVector2dDistance_ZeroVector)
{
EXPECT_NEAR(PredEngineTrait::calc_vector_2d_distance({0.f, 0.f, 0.f}), 0.f, 1e-6f);
}
TEST(CryPredEngineTrait, CalcVector2dDistance_ZIgnored)
{
// Z does not affect the 2D distance
EXPECT_NEAR(PredEngineTrait::calc_vector_2d_distance({0.f, 5.f, 100.f}),
PredEngineTrait::calc_vector_2d_distance({0.f, 5.f, 0.f}), 1e-6f);
}
// ---- get_vector_height_coordinate ----
TEST(CryPredEngineTrait, GetVectorHeightCoordinate_ReturnsZ)
{
// Cry engine up = +Z
EXPECT_FLOAT_EQ(PredEngineTrait::get_vector_height_coordinate({1.f, 2.f, 7.f}), 7.f);
}
// ---- calc_direct_pitch_angle ----
TEST(CryPredEngineTrait, CalcDirectPitchAngle_Flat)
{
// Target at same height → pitch = 0
EXPECT_NEAR(PredEngineTrait::calc_direct_pitch_angle({0.f, 0.f, 0.f}, {0.f, 100.f, 0.f}), 0.f, 1e-4f);
}
TEST(CryPredEngineTrait, CalcDirectPitchAngle_LookingUp)
{
// Target at 45° above (equal XY distance and Z height)
// direction to {0, 1, 1} normalized = {0, 0.707, 0.707}, asin(0.707) = 45°
EXPECT_NEAR(PredEngineTrait::calc_direct_pitch_angle({0.f, 0.f, 0.f}, {0.f, 1.f, 1.f}), 45.f, 1e-3f);
}
TEST(CryPredEngineTrait, CalcDirectPitchAngle_LookingDown)
{
// Target directly below
EXPECT_NEAR(PredEngineTrait::calc_direct_pitch_angle({0.f, 0.f, 10.f}, {0.f, 0.f, 0.f}), -90.f, 1e-3f);
}
TEST(CryPredEngineTrait, CalcDirectPitchAngle_LookingDirectlyUp)
{
EXPECT_NEAR(PredEngineTrait::calc_direct_pitch_angle({0.f, 0.f, 0.f}, {0.f, 0.f, 100.f}), 90.f, 1e-3f);
}
// ---- calc_direct_yaw_angle ----
TEST(CryPredEngineTrait, CalcDirectYawAngle_ForwardAlongY)
{
// Cry engine forward = +Y → yaw = 0
EXPECT_NEAR(PredEngineTrait::calc_direct_yaw_angle({0.f, 0.f, 0.f}, {0.f, 100.f, 0.f}), 0.f, 1e-4f);
}
TEST(CryPredEngineTrait, CalcDirectYawAngle_AlongPositiveX)
{
// direction = {1, 0, 0}, yaw = -atan2(1, 0) = -90°
EXPECT_NEAR(PredEngineTrait::calc_direct_yaw_angle({0.f, 0.f, 0.f}, {100.f, 0.f, 0.f}), -90.f, 1e-3f);
}
TEST(CryPredEngineTrait, CalcDirectYawAngle_AlongNegativeX)
{
// direction = {-1, 0, 0}, yaw = -atan2(-1, 0) = 90°
EXPECT_NEAR(PredEngineTrait::calc_direct_yaw_angle({0.f, 0.f, 0.f}, {-100.f, 0.f, 0.f}), 90.f, 1e-3f);
}
TEST(CryPredEngineTrait, CalcDirectYawAngle_BackwardAlongNegY)
{
// direction = {0, -1, 0}, yaw = -atan2(0, -1) = ±180°
const float yaw = PredEngineTrait::calc_direct_yaw_angle({0.f, 0.f, 0.f}, {0.f, -100.f, 0.f});
EXPECT_NEAR(std::abs(yaw), 180.f, 1e-3f);
}
TEST(CryPredEngineTrait, CalcDirectYawAngle_OffOriginCamera)
{
// Same relative direction regardless of camera position
const float yaw_a = PredEngineTrait::calc_direct_yaw_angle({0.f, 0.f, 0.f}, {0.f, 100.f, 0.f});
const float yaw_b = PredEngineTrait::calc_direct_yaw_angle({50.f, 50.f, 0.f}, {50.f, 150.f, 0.f});
EXPECT_NEAR(yaw_a, yaw_b, 1e-4f);
}
// ---- calc_viewpoint_from_angles ----
TEST(CryPredEngineTrait, CalcViewpointFromAngles_45Degrees)
{
projectile_prediction::Projectile p;
p.m_origin = {0.f, 0.f, 0.f};
p.m_launch_speed = 10.f;
// Target along +Y at distance 10; pitch=45° → height = 10 * tan(45°) = 10
const Vector3<float> target{0.f, 10.f, 0.f};
const auto vp = PredEngineTrait::calc_viewpoint_from_angles(p, target, 45.f);
EXPECT_NEAR(vp.x, 0.f, 1e-4f);
EXPECT_NEAR(vp.y, 10.f, 1e-4f);
EXPECT_NEAR(vp.z, 10.f, 1e-3f);
}
TEST(CryPredEngineTrait, CalcViewpointFromAngles_ZeroPitch)
{
projectile_prediction::Projectile p;
p.m_origin = {0.f, 0.f, 5.f};
p.m_launch_speed = 1.f;
const Vector3<float> target{3.f, 4.f, 0.f};
const auto vp = PredEngineTrait::calc_viewpoint_from_angles(p, target, 0.f);
// tan(0) = 0 → viewpoint Z = origin.z + 0 = 5
EXPECT_NEAR(vp.x, 3.f, 1e-4f);
EXPECT_NEAR(vp.y, 4.f, 1e-4f);
EXPECT_NEAR(vp.z, 5.f, 1e-4f);
}
TEST(CryPredEngineTrait, CalcViewpointXYMatchesPredictedTargetXY)
{
projectile_prediction::Projectile p;
p.m_origin = {1.f, 2.f, 3.f};
p.m_launch_speed = 50.f;
const Vector3<float> target{10.f, 20.f, 5.f};
const auto vp = PredEngineTrait::calc_viewpoint_from_angles(p, target, 30.f);
// X and Y always match the predicted target position
EXPECT_NEAR(vp.x, target.x, 1e-4f);
EXPECT_NEAR(vp.y, target.y, 1e-4f);
}
+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 // UnitTestMat.cpp
#include "omath/linear_algebra/mat.hpp" #include "omath/linear_algebra/mat.hpp"
#include "omath/linear_algebra/vector3.hpp" #include "omath/linear_algebra/vector3.hpp"
#include "omath/trigonometry/angles.hpp"
#include <gtest/gtest.h> #include <gtest/gtest.h>
using namespace omath; using namespace omath;
@@ -220,8 +221,8 @@ TEST(UnitTestMatStandalone, Equanity)
constexpr omath::Vector3<float> left_handed = {0, 2, 10}; constexpr omath::Vector3<float> left_handed = {0, 2, 10};
constexpr omath::Vector3<float> right_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.1, 1000); 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(90.f, 16.f / 9.f, 0.1, 1000); 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_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); auto ndc_right_handed = proj_right_handed * omath::mat_column_from_vector(right_handed);
@@ -233,7 +234,7 @@ TEST(UnitTestMatStandalone, Equanity)
} }
TEST(UnitTestMatStandalone, MatPerspectiveLeftHanded) 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 auto projected = perspective_proj
* mat_column_from_vector<float>({0, 0, 0.1001}); * mat_column_from_vector<float>({0, 0, 0.1001});
@@ -244,7 +245,7 @@ TEST(UnitTestMatStandalone, MatPerspectiveLeftHanded)
TEST(UnitTestMatStandalone, MatPerspectiveLeftHandedZeroToOne) 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); 90.f, 16.f / 9.f, 0.1f, 1000.f);
// Near plane point should map to z ~ 0 // Near plane point should map to z ~ 0
@@ -266,7 +267,7 @@ TEST(UnitTestMatStandalone, MatPerspectiveLeftHandedZeroToOne)
TEST(UnitTestMatStandalone, MatPerspectiveRightHandedZeroToOne) 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); 90.f, 16.f / 9.f, 0.1f, 1000.f);
// Near plane point (negative z for right-handed) should map to z ~ 0 // Near plane point (negative z for right-handed) should map to z ~ 0
@@ -289,8 +290,8 @@ TEST(UnitTestMatStandalone, MatPerspectiveRightHandedZeroToOne)
TEST(UnitTestMatStandalone, MatPerspectiveNegativeOneToOneRange) TEST(UnitTestMatStandalone, MatPerspectiveNegativeOneToOneRange)
{ {
// Verify existing [-1, 1] behavior with explicit template arg matches default // 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_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<float, MatStoreType::ROW_MAJOR, 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); NDCDepthRange::NEGATIVE_ONE_TO_ONE>(90.f, 16.f / 9.f, 0.1f, 1000.f);
EXPECT_EQ(proj_default, proj_explicit); EXPECT_EQ(proj_default, proj_explicit);
@@ -306,15 +307,174 @@ TEST(UnitTestMatStandalone, MatPerspectiveNegativeOneToOneRange)
EXPECT_NEAR(far_pt.at(2, 0), 1.0f, 1e-3f); 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) TEST(UnitTestMatStandalone, MatPerspectiveZeroToOneEquanity)
{ {
// LH and RH should produce same NDC for mirrored z // LH and RH should produce same NDC for mirrored z
constexpr omath::Vector3<float> left_handed = {0, 2, 10}; constexpr omath::Vector3<float> left_handed = {0, 2, 10};
constexpr omath::Vector3<float> right_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); 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); 90.f, 16.f / 9.f, 0.1f, 1000.f);
auto ndc_lh = proj_lh * mat_column_from_vector(left_handed); 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));
}

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