.
+Copyright (C) 2023-2026 Orange++ orange_github@proton.me
+
+This software is provided 'as-is', without any express or implied
+warranty. In no event will the authors be held liable for any damages
+arising from the use of this software.
+
+Permission is granted to anyone to use this software for any purpose,
+including commercial applications, and to alter it and redistribute it
+freely, subject to the following restrictions:
+
+1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+3. This notice may not be removed or altered from any source distribution.
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..0a9c5016
--- /dev/null
+++ b/README.md
@@ -0,0 +1,151 @@
+
+
+
+
+
+
+
+
+[](https://www.codefactor.io/repository/github/orange-cpp/omath)
+
+[](https://repology.org/project/orange-math/versions)
+
+[](https://discord.gg/eDgdaWbqwZ)
+[](https://t.me/orangennotes)
+
+OMath is a 100% independent, constexpr template blazingly fast math library that doesn't have legacy C++ code.
+
+It provides the latest features, is highly customizable, has all for cheat development, DirectX/OpenGL/Vulkan support, premade support for different game engines, much more constexpr stuff than in other libraries and more...
+
+
+
+---
+
+**[
Install
][INSTALL]**
+**[
Examples
][EXAMPLES]**
+**[
Documentation
][DOCUMENTATION]**
+**[
Contribute
][CONTRIBUTING]**
+**[
Donate
][SPONSOR]**
+
+---
+
+
+
+
+
+
+
+
+## Quick Example
+
+```cpp
+#include
+
+using namespace omath;
+
+// 3D vector operations
+Vector3 a{1, 2, 3};
+Vector3 b{4, 5, 6};
+
+auto dot = a.dot(b); // 32.0
+auto cross = a.cross(b); // (-3, 6, -3)
+auto distance = a.distance_to(b); // ~5.196
+auto normalized = a.normalized(); // Unit vector
+
+// World-to-screen projection (Source Engine example)
+using namespace omath::source_engine;
+Camera camera(position, angles, viewport, fov, near_plane, far_plane);
+
+if (auto screen = camera.world_to_screen(world_position)) {
+ // Draw at screen->x, screen->y
+}
+```
+
+**[See more examples and tutorials][TUTORIALS]**
+
+# Features
+- **Efficiency**: Optimized for performance, ensuring quick computations using AVX2.
+- **Versatility**: Includes a wide array of mathematical functions and algorithms.
+- **Ease of Use**: Simplified interface for convenient integration into various projects.
+- **Projectile Prediction**: Projectile prediction engine with O(N) algo complexity, that can power you projectile aim-bot.
+- **3D Projection**: No need to find view-projection matrix anymore you can make your own projection pipeline.
+- **Collision Detection**: Production ready code to handle collision detection by using simple interfaces.
+- **No Additional Dependencies**: No additional dependencies need to use OMath except unit test execution
+- **Ready for meta-programming**: Omath use templates for common types like Vectors, Matrixes etc, to handle all types!
+- **Engine support**: Supports coordinate systems of **Source, Unity, Unreal, Frostbite, IWEngine and canonical OpenGL**.
+- **Cross platform**: Supports Windows, MacOS and Linux.
+- **Algorithms**: Has ability to scan for byte pattern with wildcards in PE files/modules, binary slices, works even with Wine apps.
+
+
+# Gallery
+
+
+
+[](https://youtu.be/lM_NJ1yCunw?si=-Qf5yzDcWbaxAXGQ)
+
+
+
+![APEX Preview]
+
+
+
+![BO2 Preview]
+
+
+
+![CS2 Preview]
+
+
+
+![TF2 Preview]
+
+
+
+![OpenGL Preview]
+
+
+
+
+
+
+## Documentation
+
+- **[Getting Started Guide](http://libomath.org/getting_started/)** - Installation and first steps
+- **[API Overview](http://libomath.org/api_overview/)** - Complete API reference
+- **[Tutorials](http://libomath.org/tutorials/)** - Step-by-step guides
+- **[FAQ](http://libomath.org/faq/)** - Common questions and answers
+- **[Troubleshooting](http://libomath.org/troubleshooting/)** - Solutions to common issues
+- **[Best Practices](http://libomath.org/best_practices/)** - Guidelines for effective usage
+
+## Community & Support
+
+- **Discord**: [Join our community](https://discord.gg/eDgdaWbqwZ)
+- **Telegram**: [@orangennotes](https://t.me/orangennotes)
+- **Issues**: [Report bugs or request features](https://github.com/orange-cpp/omath/issues)
+- **Contributing**: See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines
+
+# Acknowledgments
+- [All contributors](https://github.com/orange-cpp/omath/graphs/contributors)
+
+
+[APEX Preview]: docs/images/showcase/apex.png
+[BO2 Preview]: docs/images/showcase/cod_bo2.png
+[CS2 Preview]: docs/images/showcase/cs2.jpeg
+[TF2 Preview]: docs/images/showcase/tf2.jpg
+[OpenGL Preview]: docs/images/showcase/opengl.png
+
+[QUICKSTART]: docs/getting_started.md
+[INSTALL]: INSTALL.md
+[DOCUMENTATION]: http://libomath.org
+[TUTORIALS]: docs/tutorials.md
+[CONTRIBUTING]: CONTRIBUTING.md
+[EXAMPLES]: examples
+[SPONSOR]: https://boosty.to/orangecpp/purchase/3568644?ssource=DIRECT&share=subscription_link
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 00000000..c946db88
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,5 @@
+# Security Policy
+
+## Reporting a Vulnerability
+
+Please report security issues to `orange_github@proton.me`
diff --git a/VERSION b/VERSION
new file mode 100644
index 00000000..cfacfe40
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+4.7.1
\ No newline at end of file
diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt
new file mode 100644
index 00000000..29a2b1ae
--- /dev/null
+++ b/benchmark/CMakeLists.txt
@@ -0,0 +1,24 @@
+project(omath_benchmark)
+
+file(GLOB_RECURSE OMATH_BENCHMARK_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp")
+add_executable(${PROJECT_NAME} ${OMATH_BENCHMARK_SOURCES})
+
+set_target_properties(
+ ${PROJECT_NAME}
+ PROPERTIES ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}"
+ LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}"
+ RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}"
+ CXX_STANDARD 23
+ CXX_STANDARD_REQUIRED ON)
+
+if(TARGET benchmark::benchmark) # Benchmark is being linked as submodule
+ target_link_libraries(${PROJECT_NAME} PRIVATE benchmark::benchmark omath)
+else()
+ find_package(benchmark CONFIG REQUIRED) # Benchmark is being linked as vcpkg
+ # package
+ target_link_libraries(${PROJECT_NAME} PRIVATE benchmark::benchmark omath)
+endif()
+
+if(OMATH_ENABLE_VALGRIND)
+ omath_setup_valgrind(${PROJECT_NAME})
+endif()
diff --git a/benchmark/benchmark_mat.cpp b/benchmark/benchmark_mat.cpp
new file mode 100644
index 00000000..2e543821
--- /dev/null
+++ b/benchmark/benchmark_mat.cpp
@@ -0,0 +1,65 @@
+//
+// Created by Vlad on 9/17/2025.
+//
+#include
+
+#include
+using namespace omath;
+
+
+void mat_float_multiplication_col_major(benchmark::State& state)
+{
+ using MatType = Mat<128, 128, float, MatStoreType::COLUMN_MAJOR>;
+ MatType a;
+ MatType b;
+ a.set(3.f);
+ b.set(7.f);
+
+
+ for ([[maybe_unused]] const auto _ : state)
+ std::ignore = a * b;
+}
+void mat_float_multiplication_row_major(benchmark::State& state)
+{
+ using MatType = Mat<128, 128, float, MatStoreType::ROW_MAJOR>;
+ MatType a;
+ MatType b;
+ a.set(3.f);
+ b.set(7.f);
+
+
+ for ([[maybe_unused]] const auto _ : state)
+ std::ignore = a * b;
+}
+
+void mat_double_multiplication_row_major(benchmark::State& state)
+{
+ using MatType = Mat<128, 128, double, MatStoreType::ROW_MAJOR>;
+ MatType a;
+ MatType b;
+ a.set(3.f);
+ b.set(7.f);
+
+
+ for ([[maybe_unused]] const auto _ : state)
+ std::ignore = a * b;
+}
+
+void mat_double_multiplication_col_major(benchmark::State& state)
+{
+ using MatType = Mat<128, 128, double, MatStoreType::COLUMN_MAJOR>;
+ MatType a;
+ MatType b;
+ a.set(3.f);
+ b.set(7.f);
+
+
+ for ([[maybe_unused]] const auto _ : state)
+ std::ignore = a * b;
+}
+
+BENCHMARK(mat_float_multiplication_col_major)->Iterations(5000);
+BENCHMARK(mat_float_multiplication_row_major)->Iterations(5000);
+
+BENCHMARK(mat_double_multiplication_col_major)->Iterations(5000);
+BENCHMARK(mat_double_multiplication_row_major)->Iterations(5000);
\ No newline at end of file
diff --git a/benchmark/benchmark_projectile_pred.cpp b/benchmark/benchmark_projectile_pred.cpp
new file mode 100644
index 00000000..a6337547
--- /dev/null
+++ b/benchmark/benchmark_projectile_pred.cpp
@@ -0,0 +1,23 @@
+//
+// Created by Vlad on 9/18/2025.
+//
+#include
+#include
+using namespace omath;
+
+using namespace omath::projectile_prediction;
+
+constexpr float simulation_time_step = 1.f / 1000.f;
+constexpr float hit_distance_tolerance = 5.f;
+
+void source_engine_projectile_prediction(benchmark::State& state)
+{
+ constexpr Target target{.m_origin = {100, 0, 90}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
+ constexpr Projectile projectile = {.m_origin = {3, 2, 1}, .m_launch_speed = 5000, .m_gravity_scale = 0.4};
+
+ for ([[maybe_unused]] const auto _: state)
+ std::ignore = ProjPredEngineLegacy(400, simulation_time_step, 50, hit_distance_tolerance)
+ .maybe_calculate_aim_point(projectile, target);
+}
+
+BENCHMARK(source_engine_projectile_prediction)->Iterations(10'000);
\ No newline at end of file
diff --git a/benchmark/main.cpp b/benchmark/main.cpp
new file mode 100644
index 00000000..790aeaea
--- /dev/null
+++ b/benchmark/main.cpp
@@ -0,0 +1,5 @@
+//
+// Created by Vlad on 9/17/2025.
+//
+#include
+BENCHMARK_MAIN();
\ No newline at end of file
diff --git a/cmake/Coverage.cmake b/cmake/Coverage.cmake
new file mode 100644
index 00000000..57914096
--- /dev/null
+++ b/cmake/Coverage.cmake
@@ -0,0 +1,67 @@
+# cmake/Coverage.cmake
+include_guard(GLOBAL)
+
+function(omath_setup_coverage TARGET_NAME)
+ if(ANDROID OR IOS OR EMSCRIPTEN)
+ return()
+ endif()
+
+ if(CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND MSVC)
+ target_compile_options(${TARGET_NAME} PRIVATE -fprofile-instr-generate -fcoverage-mapping
+ /Zi)
+ target_link_options(
+ ${TARGET_NAME}
+ PRIVATE
+ -fprofile-instr-generate
+ -fcoverage-mapping
+ /DEBUG:FULL
+ /INCREMENTAL:NO
+ /PROFILE)
+ elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang|AppleClang")
+ target_compile_options(${TARGET_NAME} PRIVATE -fprofile-instr-generate -fcoverage-mapping
+ -g -O0)
+ target_link_options(${TARGET_NAME} PRIVATE -fprofile-instr-generate -fcoverage-mapping)
+ elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
+ target_compile_options(${TARGET_NAME} PRIVATE --coverage -g -O0)
+ target_link_options(${TARGET_NAME} PRIVATE --coverage)
+ endif()
+
+ if(TARGET coverage)
+ return()
+ endif()
+
+ if(CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND MSVC)
+ message(STATUS "MSVC detected: Use VS Code Coverage from CI workflow")
+ add_custom_target(
+ coverage
+ DEPENDS unit_tests
+ COMMAND $
+ WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
+ COMMENT "Running tests for coverage (use VS Code Coverage from CI)")
+
+ elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang|AppleClang")
+ add_custom_target(
+ coverage
+ DEPENDS unit_tests
+ COMMAND bash "${CMAKE_SOURCE_DIR}/scripts/coverage-llvm.sh" "${CMAKE_SOURCE_DIR}"
+ "${CMAKE_BINARY_DIR}" "$" "${CMAKE_BINARY_DIR}/coverage"
+ WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
+ COMMENT "Running LLVM coverage")
+
+ elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
+ add_custom_target(
+ coverage
+ DEPENDS unit_tests
+ COMMAND $ || true
+ COMMAND lcov --capture --directory "${CMAKE_BINARY_DIR}" --output-file
+ "${CMAKE_BINARY_DIR}/coverage.info" --ignore-errors mismatch,gcov
+ COMMAND
+ lcov --remove "${CMAKE_BINARY_DIR}/coverage.info" "*/tests/*" "*/gtest/*"
+ "*/googletest/*" "*/_deps/*" "/usr/*" --output-file
+ "${CMAKE_BINARY_DIR}/coverage.info" --ignore-errors unused
+ COMMAND genhtml "${CMAKE_BINARY_DIR}/coverage.info" --output-directory
+ "${CMAKE_BINARY_DIR}/coverage"
+ WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
+ COMMENT "Running lcov/genhtml")
+ endif()
+endfunction()
diff --git a/cmake/Valgrind.cmake b/cmake/Valgrind.cmake
new file mode 100644
index 00000000..bab197e4
--- /dev/null
+++ b/cmake/Valgrind.cmake
@@ -0,0 +1,41 @@
+# cmake/Valgrind.cmake
+
+if(DEFINED __OMATH_VALGRIND_INCLUDED)
+ return()
+endif()
+set(__OMATH_VALGRIND_INCLUDED TRUE)
+
+find_program(VALGRIND_EXECUTABLE valgrind)
+option(OMATH_ENABLE_VALGRIND "Enable Valgrind target for memory checking" ON)
+
+if(OMATH_ENABLE_VALGRIND AND NOT TARGET valgrind_all)
+ add_custom_target(valgrind_all)
+endif()
+
+function(omath_setup_valgrind TARGET_NAME)
+ if(NOT OMATH_ENABLE_VALGRIND)
+ return()
+ endif()
+
+ if(NOT VALGRIND_EXECUTABLE)
+ message(WARNING "OMATH_ENABLE_VALGRIND is ON, but 'valgrind' executable was not found.")
+ return()
+ endif()
+
+ set(VALGRIND_FLAGS --leak-check=full --show-leak-kinds=all --track-origins=yes
+ --error-exitcode=99)
+
+ set(VALGRIND_TARGET "valgrind_${TARGET_NAME}")
+
+ if(NOT TARGET ${VALGRIND_TARGET})
+ add_custom_target(
+ ${VALGRIND_TARGET}
+ DEPENDS ${TARGET_NAME}
+ COMMAND ${VALGRIND_EXECUTABLE} ${VALGRIND_FLAGS} $
+ WORKING_DIRECTORY $
+ COMMENT "Running Valgrind memory check on ${TARGET_NAME}..."
+ USES_TERMINAL)
+
+ add_dependencies(valgrind_all ${VALGRIND_TARGET})
+ endif()
+endfunction()
diff --git a/cmake/omathConfig.cmake.in b/cmake/omathConfig.cmake.in
new file mode 100644
index 00000000..1e3c774b
--- /dev/null
+++ b/cmake/omathConfig.cmake.in
@@ -0,0 +1,11 @@
+@PACKAGE_INIT@
+
+include(CMakeFindDependencyMacro)
+
+if (@OMATH_IMGUI_INTEGRATION@)
+ find_dependency(imgui CONFIG)
+endif()
+
+# Load the targets for the omath library
+include("${CMAKE_CURRENT_LIST_DIR}/omathTargets.cmake")
+check_required_components(omath)
diff --git a/docs/3d_primitives/box.md b/docs/3d_primitives/box.md
new file mode 100644
index 00000000..84e392d6
--- /dev/null
+++ b/docs/3d_primitives/box.md
@@ -0,0 +1,118 @@
+# `omath::primitives::create_box` — Build an oriented box as 12 triangles
+
+> Header: your project’s `primitives/box.hpp` (declares `create_box`)
+> Namespace: `omath::primitives`
+> Depends on: `omath::Triangle>`, `omath::Vector3`
+
+```cpp
+[[nodiscard]]
+std::array>, 12>
+create_box(const Vector3& top,
+ const Vector3& bottom,
+ const Vector3& dir_forward,
+ const Vector3& dir_right,
+ float ratio = 4.f) noexcept;
+```
+
+---
+
+## What it does
+
+Constructs a **rectangular cuboid (“box”)** oriented in 3D space and returns its surface as **12 triangles** (2 per face × 6 faces). The box’s central axis runs from `bottom` → `top`. The **up** direction is inferred from that segment; the **forward** and **right** directions define the box’s orientation around that axis.
+
+The lateral half-extents are derived from the axis length and `ratio`:
+
+> Let `H = |top - bottom|`. Lateral half-size ≈ `H / ratio` along both `dir_forward` and `dir_right`
+> (i.e., the cross-section is a square of side `2H/ratio`).
+
+> **Note:** This describes the intended behavior from the interface. If you rely on a different sizing rule, document it next to your implementation.
+
+---
+
+## Parameters
+
+* `top`
+ Center of the **top face**.
+
+* `bottom`
+ Center of the **bottom face**.
+
+* `dir_forward`
+ A direction that orients the box around its up axis. Should be **non-zero** and **not collinear** with `top - bottom`.
+
+* `dir_right`
+ A direction roughly orthogonal to both `dir_forward` and `top - bottom`. Used to fully fix orientation.
+
+* `ratio` (default `4.0f`)
+ Controls thickness relative to height. Larger values → thinner box.
+ With the default rule above, half-extent = `|top-bottom|/ratio`.
+
+---
+
+## Return value
+
+`std::array>, 12>` — the six faces of the box, triangulated.
+Winding is intended to be **outward-facing** (right-handed coordinates). Do not rely on a specific **face ordering**; treat the array as opaque unless your implementation guarantees an order.
+
+---
+
+## Expected math & robustness
+
+* Define `u = normalize(top - bottom)`.
+* Re-orthonormalize the basis to avoid skew:
+
+ ```cpp
+ f = normalize(dir_forward - u * u.dot(dir_forward)); // drop any up component
+ r = normalize(u.cross(f)); // right-handed basis
+ // (Optionally recompute f = r.cross(u) for orthogonality)
+ ```
+* Half-extents: `h = length(top - bottom) / ratio; hf = h * f; hr = h * r`.
+* Corners (top): `t±r±f = top ± hr ± hf`; (bottom): `b±r±f = bottom ± hr ± hf`.
+* Triangulate each face with consistent CCW winding when viewed from outside.
+
+---
+
+## Example
+
+```cpp
+using omath::Vector3;
+using omath::Triangle;
+using omath::primitives::create_box;
+
+// Axis from bottom to top (height 2)
+Vector3 bottom{0, 0, 0};
+Vector3 top {0, 2, 0};
+
+// Orientation around the axis
+Vector3 forward{0, 0, 1};
+Vector3 right {1, 0, 0};
+
+// Ratio 4 → lateral half-size = height/4 = 0.5
+auto tris = create_box(top, bottom, forward, right, 4.0f);
+
+// Use the triangles (normals, rendering, collision, etc.)
+for (const auto& tri : tris) {
+ auto n = tri.calculate_normal();
+ (void)n;
+}
+```
+
+---
+
+## Usage notes & pitfalls
+
+* **Degenerate axis**: If `top == bottom`, the box is undefined (zero height). Guard against this.
+* **Directions**: Provide **non-zero**, **reasonably orthogonal** `dir_forward`/`dir_right`. A robust implementation should project/normalize internally, but callers should still pass sensible inputs.
+* **Winding**: If your renderer or collision expects a specific winding, verify with a unit test and flip vertex order per face if necessary.
+* **Thickness policy**: This doc assumes both lateral half-extents equal `|top-bottom|/ratio`. If your implementation diverges (e.g., separate forward/right ratios), document it.
+
+---
+
+## See also
+
+* `omath::Triangle` (vertex utilities: normals, centroid, etc.)
+* `omath::Vector3` (geometry operations used by the construction)
+
+---
+
+*Last updated: 31 Oct 2025*
diff --git a/docs/3d_primitives/mesh.md b/docs/3d_primitives/mesh.md
new file mode 100644
index 00000000..803aaef8
--- /dev/null
+++ b/docs/3d_primitives/mesh.md
@@ -0,0 +1,465 @@
+# `omath::primitives::Mesh` — 3D mesh with transformation support
+
+> Header: `omath/3d_primitives/mesh.hpp`
+> Namespace: `omath::primitives`
+> Depends on: `omath::Vector3`, `omath::Mat4X4`, `omath::Triangle>`
+> Purpose: represent and transform 3D meshes in different engine coordinate systems
+
+---
+
+## Overview
+
+`Mesh` represents a 3D polygonal mesh with vertex data and transformation capabilities. It stores:
+* **Vertex buffer (VBO)** — array of 3D vertex positions
+* **Index buffer (VAO)** — array of triangular faces (indices into VBO)
+* **Transformation** — position, rotation, and scale with caching
+
+The mesh supports transformation from local space to world space using engine-specific coordinate systems through the `MeshTrait` template parameter.
+
+---
+
+## Template Declaration
+
+```cpp
+template
+class Mesh final;
+```
+
+### Template Parameters
+
+* `Mat4X4` — Matrix type for transformations (typically `omath::Mat4X4`)
+* `RotationAngles` — Rotation representation (e.g., `ViewAngles` with pitch/yaw/roll)
+* `MeshTypeTrait` — Engine-specific transformation trait (see [Engine Traits](#engine-traits))
+* `Type` — Scalar type for vertex coordinates (default `float`)
+
+---
+
+## Type Aliases
+
+```cpp
+using NumericType = Type;
+```
+
+Common engine-specific aliases:
+
+```cpp
+// Source Engine
+using Mesh = omath::primitives::Mesh;
+
+// Unity Engine
+using Mesh = omath::primitives::Mesh;
+
+// Unreal Engine
+using Mesh = omath::primitives::Mesh;
+
+// Frostbite, IW Engine, OpenGL similar...
+```
+
+Use the pre-defined type aliases in engine namespaces:
+```cpp
+using namespace omath::source_engine;
+Mesh my_mesh = /* ... */; // Uses SourceEngine::Mesh
+```
+
+---
+
+## Data Members
+
+### Vertex Data
+
+```cpp
+std::vector> m_vertex_buffer; // VBO: vertex positions
+std::vector> m_vertex_array_object; // VAO: face indices
+```
+
+* `m_vertex_buffer` — array of vertex positions in **local space**
+* `m_vertex_array_object` — array of triangular faces, each containing 3 indices into `m_vertex_buffer`
+
+**Public access**: These members are public for direct manipulation when needed.
+
+---
+
+## Constructor
+
+```cpp
+Mesh(std::vector> vbo,
+ std::vector> vao,
+ Vector3 scale = {1, 1, 1});
+```
+
+Creates a mesh from vertex and index data.
+
+**Parameters**:
+* `vbo` — vertex buffer (moved into mesh)
+* `vao` — index buffer / vertex array object (moved into mesh)
+* `scale` — initial scale (default `{1, 1, 1}`)
+
+**Example**:
+```cpp
+std::vector> vertices = {
+ {0, 0, 0}, {1, 0, 0}, {0, 1, 0}, {0, 0, 1}
+};
+
+std::vector> faces = {
+ {0, 1, 2}, // Triangle 1
+ {0, 1, 3}, // Triangle 2
+ {0, 2, 3}, // Triangle 3
+ {1, 2, 3} // Triangle 4
+};
+
+using namespace omath::source_engine;
+Mesh tetrahedron(std::move(vertices), std::move(faces));
+```
+
+---
+
+## Transformation Methods
+
+### Setting Transform Components
+
+```cpp
+void set_origin(const Vector3& new_origin);
+void set_scale(const Vector3& new_scale);
+void set_rotation(const RotationAngles& new_rotation_angles);
+```
+
+Update the mesh's transformation. **Side effect**: invalidates the cached transformation matrix, which will be recomputed on the next `get_to_world_matrix()` call.
+
+**Example**:
+```cpp
+mesh.set_origin({10, 0, 5});
+mesh.set_scale({2, 2, 2});
+
+ViewAngles angles;
+angles.pitch = PitchAngle::from_degrees(45.0f);
+angles.yaw = YawAngle::from_degrees(30.0f);
+mesh.set_rotation(angles);
+```
+
+### Getting Transform Components
+
+```cpp
+[[nodiscard]] const Vector3& get_origin() const;
+[[nodiscard]] const Vector3& get_scale() const;
+[[nodiscard]] const RotationAngles& get_rotation_angles() const;
+```
+
+Retrieve current transformation components.
+
+### Transformation Matrix
+
+```cpp
+[[nodiscard]] const Mat4X4& get_to_world_matrix() const;
+```
+
+Returns the cached local-to-world transformation matrix. The matrix is computed lazily on first access after any transformation change:
+
+```
+M = Translation(origin) × Scale(scale) × Rotation(angles)
+```
+
+The rotation matrix is computed using the engine-specific `MeshTrait::rotation_matrix()` method.
+
+**Caching**: The matrix is stored in a `mutable std::optional` and recomputed only when invalidated by `set_*` methods.
+
+---
+
+## Vertex Transformation
+
+### `vertex_to_world_space`
+
+```cpp
+[[nodiscard]]
+Vector3 vertex_to_world_space(const Vector3& vertex) const;
+```
+
+Transforms a vertex from local space to world space by multiplying with the transformation matrix.
+
+**Algorithm**:
+1. Convert vertex to column matrix: `[x, y, z, 1]ᵀ`
+2. Multiply by transformation matrix: `M × vertex`
+3. Extract the resulting 3D position
+
+**Usage**:
+```cpp
+Vector3 local_vertex{1, 0, 0};
+Vector3 world_vertex = mesh.vertex_to_world_space(local_vertex);
+```
+
+**Note**: This is used internally by `MeshCollider` to provide world-space support functions for GJK/EPA.
+
+---
+
+## Face Transformation
+
+### `make_face_in_world_space`
+
+```cpp
+[[nodiscard]]
+Triangle> make_face_in_world_space(
+ const std::vector>::const_iterator vao_iterator
+) const;
+```
+
+Creates a triangle in world space from a face index iterator.
+
+**Parameters**:
+* `vao_iterator` — iterator to an element in `m_vertex_array_object`
+
+**Returns**: `Triangle` with all three vertices transformed to world space.
+
+**Example**:
+```cpp
+for (auto it = mesh.m_vertex_array_object.begin();
+ it != mesh.m_vertex_array_object.end();
+ ++it) {
+ Triangle> world_triangle = mesh.make_face_in_world_space(it);
+ // Render or process the triangle
+}
+```
+
+---
+
+## Usage Examples
+
+### Creating a Box Mesh
+
+```cpp
+using namespace omath::source_engine;
+
+std::vector> box_vbo = {
+ // Bottom face
+ {-0.5f, -0.5f, 0.0f}, { 0.5f, -0.5f, 0.0f},
+ { 0.5f, 0.5f, 0.0f}, {-0.5f, 0.5f, 0.0f},
+ // Top face
+ {-0.5f, -0.5f, 1.0f}, { 0.5f, -0.5f, 1.0f},
+ { 0.5f, 0.5f, 1.0f}, {-0.5f, 0.5f, 1.0f}
+};
+
+std::vector> box_vao = {
+ // Bottom
+ {0, 1, 2}, {0, 2, 3},
+ // Top
+ {4, 6, 5}, {4, 7, 6},
+ // Sides
+ {0, 4, 5}, {0, 5, 1},
+ {1, 5, 6}, {1, 6, 2},
+ {2, 6, 7}, {2, 7, 3},
+ {3, 7, 4}, {3, 4, 0}
+};
+
+Mesh box(std::move(box_vbo), std::move(box_vao));
+box.set_origin({0, 0, 50});
+box.set_scale({10, 10, 10});
+```
+
+### Transforming Mesh Over Time
+
+```cpp
+void update_mesh(Mesh& mesh, float delta_time) {
+ // Rotate mesh
+ auto rotation = mesh.get_rotation_angles();
+ rotation.yaw = YawAngle::from_degrees(
+ rotation.yaw.as_degrees() + 45.0f * delta_time
+ );
+ mesh.set_rotation(rotation);
+
+ // Oscillate position
+ auto origin = mesh.get_origin();
+ origin.z = 50.0f + 10.0f * std::sin(current_time * 2.0f);
+ mesh.set_origin(origin);
+}
+```
+
+### Collision Detection
+
+```cpp
+using namespace omath::collision;
+using namespace omath::source_engine;
+
+Mesh mesh_a(vbo_a, vao_a);
+mesh_a.set_origin({0, 0, 0});
+
+Mesh mesh_b(vbo_b, vao_b);
+mesh_b.set_origin({5, 0, 0});
+
+MeshCollider collider_a(std::move(mesh_a));
+MeshCollider collider_b(std::move(mesh_b));
+
+auto result = GjkAlgorithm>::check_collision(
+ collider_a, collider_b
+);
+```
+
+### Rendering Transformed Triangles
+
+```cpp
+void render_mesh(const Mesh& mesh) {
+ for (auto it = mesh.m_vertex_array_object.begin();
+ it != mesh.m_vertex_array_object.end();
+ ++it) {
+
+ Triangle> tri = mesh.make_face_in_world_space(it);
+
+ // Draw triangle with your renderer
+ draw_triangle(tri.m_vertex1, tri.m_vertex2, tri.m_vertex3);
+ }
+}
+```
+
+---
+
+## Engine Traits
+
+Each game engine has a corresponding `MeshTrait` that provides the `rotation_matrix` function:
+
+```cpp
+class MeshTrait final {
+public:
+ [[nodiscard]]
+ static Mat4X4 rotation_matrix(const ViewAngles& rotation);
+};
+```
+
+### Available Engines
+
+| Engine | Namespace | Header |
+|--------|-----------|--------|
+| Source Engine | `omath::source_engine` | `engines/source_engine/mesh.hpp` |
+| Unity | `omath::unity_engine` | `engines/unity_engine/mesh.hpp` |
+| Unreal | `omath::unreal_engine` | `engines/unreal_engine/mesh.hpp` |
+| Frostbite | `omath::frostbite_engine` | `engines/frostbite_engine/mesh.hpp` |
+| IW Engine | `omath::iw_engine` | `engines/iw_engine/mesh.hpp` |
+| OpenGL | `omath::opengl_engine` | `engines/opengl_engine/mesh.hpp` |
+
+**Example** (Source Engine):
+```cpp
+using namespace omath::source_engine;
+
+// Uses source_engine::MeshTrait automatically
+Mesh my_mesh(vertices, indices);
+```
+
+See [MeshTrait Documentation](#mesh-trait-documentation) for engine-specific details.
+
+---
+
+## Performance Considerations
+
+### Matrix Caching
+
+The transformation matrix is computed lazily and cached:
+* **First access**: O(matrix multiply) ≈ 64 float operations
+* **Subsequent access**: O(1) — returns cached matrix
+* **Cache invalidation**: Any `set_*` call invalidates the cache
+
+**Best practice**: Batch transformation updates before accessing the matrix:
+```cpp
+// Good: single matrix recomputation
+mesh.set_origin(new_origin);
+mesh.set_rotation(new_rotation);
+mesh.set_scale(new_scale);
+auto matrix = mesh.get_to_world_matrix(); // Computes once
+
+// Bad: three matrix recomputations
+mesh.set_origin(new_origin);
+auto m1 = mesh.get_to_world_matrix(); // Compute
+mesh.set_rotation(new_rotation);
+auto m2 = mesh.get_to_world_matrix(); // Compute again
+mesh.set_scale(new_scale);
+auto m3 = mesh.get_to_world_matrix(); // Compute again
+```
+
+### Memory Layout
+
+* **VBO**: Contiguous `std::vector` for cache-friendly access
+* **VAO**: Contiguous indices for cache-friendly face iteration
+* **Matrix**: Cached in `std::optional` (no allocation)
+
+### Transformation Cost
+
+* `vertex_to_world_space`: ~15-20 FLOPs per vertex (4×4 matrix multiply)
+* `make_face_in_world_space`: ~60 FLOPs (3 vertices)
+
+For high-frequency transformations, consider:
+* Caching transformed vertices if the mesh doesn't change
+* Using simpler proxy geometry for collision
+* Batching transformations
+
+---
+
+## Coordinate System Details
+
+Different engines use different coordinate systems:
+
+| Engine | Up Axis | Forward Axis | Handedness |
+|--------|---------|--------------|------------|
+| Source | +Z | +Y | Right |
+| Unity | +Y | +Z | Left |
+| Unreal | +Z | +X | Left |
+| Frostbite | +Y | +Z | Right |
+| IW Engine | +Z | +Y | Right |
+| OpenGL | +Y | +Z | Right |
+
+The `MeshTrait::rotation_matrix` function accounts for these differences, ensuring correct transformations in each engine's space.
+
+---
+
+## Limitations & Edge Cases
+
+### Empty Mesh
+
+A mesh with no vertices or faces is valid but not useful:
+```cpp
+Mesh empty_mesh({}, {}); // Valid but meaningless
+```
+
+For collision detection, ensure `m_vertex_buffer` is non-empty.
+
+### Index Validity
+
+No bounds checking is performed on indices in `m_vertex_array_object`. Ensure all indices are valid:
+```cpp
+assert(face.x < mesh.m_vertex_buffer.size());
+assert(face.y < mesh.m_vertex_buffer.size());
+assert(face.z < mesh.m_vertex_buffer.size());
+```
+
+### Degenerate Triangles
+
+Faces with duplicate indices or collinear vertices will produce degenerate triangles. The mesh doesn't validate this; users must ensure clean geometry.
+
+### Thread Safety
+
+* **Read-only**: Safe to read from multiple threads (including const methods)
+* **Modification**: Not thread-safe; synchronize `set_*` calls externally
+* **Matrix cache**: Uses `mutable` member; not thread-safe even for const methods
+
+---
+
+## See Also
+
+- [MeshCollider Documentation](../collision/mesh_collider.md) - Collision wrapper for meshes
+- [GJK Algorithm Documentation](../collision/gjk_algorithm.md) - Uses mesh for collision detection
+- [EPA Algorithm Documentation](../collision/epa_algorithm.md) - Penetration depth with meshes
+- [Triangle Documentation](../linear_algebra/triangle.md) - Triangle primitive
+- [Mat4X4 Documentation](../linear_algebra/mat.md) - Transformation matrices
+- [Box Documentation](box.md) - Box primitive
+- [Plane Documentation](plane.md) - Plane primitive
+
+---
+
+## Mesh Trait Documentation
+
+For engine-specific `MeshTrait` details, see:
+
+- [Source Engine MeshTrait](../engines/source_engine/mesh_trait.md)
+- [Unity Engine MeshTrait](../engines/unity_engine/mesh_trait.md)
+- [Unreal Engine MeshTrait](../engines/unreal_engine/mesh_trait.md)
+- [Frostbite Engine MeshTrait](../engines/frostbite/mesh_trait.md)
+- [IW Engine MeshTrait](../engines/iw_engine/mesh_trait.md)
+- [OpenGL Engine MeshTrait](../engines/opengl_engine/mesh_trait.md)
+
+---
+
+*Last updated: 13 Nov 2025*
diff --git a/docs/3d_primitives/plane.md b/docs/3d_primitives/plane.md
new file mode 100644
index 00000000..c4619a3a
--- /dev/null
+++ b/docs/3d_primitives/plane.md
@@ -0,0 +1,98 @@
+# `omath::primitives::create_plane` — Build an oriented quad (2 triangles)
+
+> Header: your project’s `primitives/plane.hpp`
+> Namespace: `omath::primitives`
+> Depends on: `omath::Triangle>`, `omath::Vector3`
+
+```cpp
+[[nodiscard]]
+std::array>, 2>
+create_plane(const Vector3& vertex_a,
+ const Vector3& vertex_b,
+ const Vector3& direction,
+ float size) noexcept;
+```
+
+---
+
+## What it does
+
+Creates a **rectangle (quad)** in 3D oriented by the edge **A→B** and a second in-plane **direction**. The quad is returned as **two triangles** suitable for rendering or collision.
+
+* Edge axis: `e = vertex_b - vertex_a`
+* Width axis: “direction”, **projected to be perpendicular to `e`** so the quad is planar and well-formed.
+* Normal (by right-hand rule): `n ∝ e × width`.
+
+> **Sizing convention**
+> Typical construction uses **half-width = `size`** along the (normalized, orthogonalized) *direction*, i.e. the total width is `2*size`.
+> If your implementation interprets `size` as full width, adjust your expectations accordingly.
+
+---
+
+## Parameters
+
+* `vertex_a`, `vertex_b` — two adjacent quad vertices defining the **long edge** of the plane.
+* `direction` — a vector indicating the **cross-edge direction** within the plane (does not need to be orthogonal or normalized).
+* `size` — **half-width** of the quad along the (processed) `direction`.
+
+---
+
+## Return
+
+`std::array>, 2>` — the quad triangulated (consistent CCW winding, outward normal per `e × width`).
+
+---
+
+## Robust construction (expected math)
+
+1. `e = vertex_b - vertex_a`
+2. Make `d` perpendicular to `e`:
+
+ ```
+ d = direction - e * (e.dot(direction) / e.length_sqr());
+ if (d.length_sqr() == 0) pick an arbitrary perpendicular to e
+ d = d.normalized();
+ ```
+3. Offsets: `w = d * size`
+4. Four corners:
+
+ ```
+ A0 = vertex_a - w; A1 = vertex_a + w;
+ B0 = vertex_b - w; B1 = vertex_b + w;
+ ```
+5. Triangles (CCW when viewed from +normal):
+
+ ```
+ T0 = Triangle{ A0, A1, B1 }
+ T1 = Triangle{ A0, B1, B0 }
+ ```
+
+---
+
+## Example
+
+```cpp
+using omath::Vector3;
+using omath::Triangle;
+using omath::primitives::create_plane;
+
+Vector3 a{ -1, 0, -1 }; // edge start
+Vector3 b{ 1, 0, -1 }; // edge end
+Vector3 dir{ 0, 0, 1 }; // cross-edge direction within the plane (roughly +Z)
+float half_width = 2.0f;
+
+auto quad = create_plane(a, b, dir, half_width);
+
+// e.g., compute normals
+for (const auto& tri : quad) {
+ auto n = tri.calculate_normal(); (void)n;
+}
+```
+
+---
+
+## Notes & edge cases
+
+* **Degenerate edge**: if `vertex_a == vertex_b`, the plane is undefined.
+* **Collinearity**: if `direction` is parallel to `vertex_b - vertex_a`, the function must choose an alternate perpendicular; expect a fallback.
+* **Winding**: If your renderer expects a specific face order, verify and swap the two vertices in each triangle as needed.
diff --git a/docs/api_overview.md b/docs/api_overview.md
new file mode 100644
index 00000000..9d0b3257
--- /dev/null
+++ b/docs/api_overview.md
@@ -0,0 +1,577 @@
+# API Overview
+
+This document provides a high-level overview of OMath's API, organized by functionality area.
+
+---
+
+## Module Organization
+
+OMath is organized into several logical modules:
+
+### Core Mathematics
+- **Linear Algebra** - Vectors, matrices, triangles
+- **Trigonometry** - Angles, view angles, trigonometric functions
+- **3D Primitives** - Boxes, planes, meshes, geometric shapes
+
+### Game Development
+- **Collision Detection** - Ray tracing, GJK/EPA algorithms, mesh collision, intersection tests
+- **Projectile Prediction** - Ballistics and aim-assist calculations
+- **Projection** - Camera systems and world-to-screen transformations
+- **Pathfinding** - A* algorithm, navigation meshes
+
+### Engine Support
+- **Source Engine** - Valve's Source Engine (CS:GO, TF2, etc.)
+- **Unity Engine** - Unity game engine
+- **Unreal Engine** - Epic's Unreal Engine
+- **Frostbite Engine** - EA's Frostbite Engine
+- **IW Engine** - Infinity Ward's engine (Call of Duty)
+- **OpenGL Engine** - Canonical OpenGL coordinate system
+
+### Utilities
+- **Color** - RGBA color representation
+- **Pattern Scanning** - Memory pattern search (wildcards, PE files)
+- **Reverse Engineering** - Internal/external memory manipulation
+
+---
+
+## Core Types
+
+### Vectors
+
+All vector types are template-based and support arithmetic types.
+
+| Type | Description | Key Methods |
+|------|-------------|-------------|
+| `Vector2` | 2D vector | `length()`, `normalized()`, `dot()`, `distance_to()` |
+| `Vector3` | 3D vector | `length()`, `normalized()`, `dot()`, `cross()`, `angle_between()` |
+| `Vector4` | 4D vector | Extends Vector3 with `w` component |
+
+**Common aliases:**
+```cpp
+using Vec2f = Vector2;
+using Vec3f = Vector3;
+using Vec4f = Vector4;
+```
+
+**Key features:**
+- Component-wise arithmetic (+, -, *, /)
+- Scalar multiplication/division
+- Dot and cross products
+- Safe normalization (returns original if length is zero)
+- Distance calculations
+- Angle calculations with error handling
+- Hash support for `float` variants
+- `std::formatter` support
+
+### Matrices
+
+| Type | Description | Key Methods |
+|------|-------------|-------------|
+| `Mat4X4` | 4×4 matrix | `identity()`, `transpose()`, `determinant()`, `inverse()` |
+
+**Use cases:**
+- Transformation matrices
+- View matrices
+- Projection matrices
+- Model-view-projection pipelines
+
+### Angles
+
+Strong-typed angle system with compile-time range enforcement:
+
+| Type | Range | Description |
+|------|-------|-------------|
+| `Angle` | Custom | Generic angle type with bounds |
+| `PitchAngle` | [-89°, 89°] | Vertical camera rotation |
+| `YawAngle` | [-180°, 180°] | Horizontal camera rotation |
+| `RollAngle` | [-180°, 180°] | Camera roll |
+| `ViewAngles` | - | Composite pitch/yaw/roll |
+
+**Features:**
+- Automatic normalization/clamping based on flags
+- Conversions between degrees and radians
+- Type-safe arithmetic
+- Prevents common angle bugs
+
+---
+
+## Projection System
+
+### Camera
+
+Generic camera template that works with any engine trait:
+
+```cpp
+template
+class Camera;
+```
+
+**Engine-specific cameras:**
+```cpp
+omath::source_engine::Camera // Source Engine
+omath::unity_engine::Camera // Unity
+omath::unreal_engine::Camera // Unreal
+omath::frostbite_engine::Camera // Frostbite
+omath::iw_engine::Camera // IW Engine
+omath::opengl_engine::Camera // OpenGL
+```
+
+**Core methods:**
+- `world_to_screen(Vector3)` - Project 3D point to 2D screen
+- `get_view_matrix()` - Get current view matrix
+- `get_projection_matrix()` - Get current projection matrix
+- `update(position, angles)` - Update camera state
+
+**Supporting types:**
+- `ViewPort` - Screen dimensions and aspect ratio
+- `FieldOfView` - FOV in degrees with validation
+- `ProjectionError` - Error codes for projection failures
+
+---
+
+## Collision Detection
+
+### GJK/EPA Algorithms
+
+Advanced convex shape collision detection using the Gilbert-Johnson-Keerthi and Expanding Polytope algorithms:
+
+```cpp
+namespace omath::collision {
+ template
+ class GjkAlgorithm;
+
+ template
+ class Epa;
+}
+```
+
+**GJK (Gilbert-Johnson-Keerthi):**
+* Detects collision between two convex shapes
+* Returns a 4-point simplex when collision is detected
+* O(k) complexity where k is typically < 20 iterations
+* Works with any collider implementing `find_abs_furthest_vertex()`
+
+**EPA (Expanding Polytope Algorithm):**
+* Computes penetration depth and separation normal
+* Takes GJK's output simplex as input
+* Provides contact information for physics simulation
+* Configurable iteration limit and convergence tolerance
+
+**Supporting Types:**
+
+| Type | Description | Key Features |
+|------|-------------|--------------|
+| `Simplex` | 1-4 point geometric simplex | Fixed capacity, GJK iteration support |
+| `MeshCollider` | Convex mesh collider | Support function for GJK/EPA |
+| `GjkHitInfo` | Collision result | Hit flag and simplex |
+| `Epa::Result` | Penetration info | Depth, normal, iteration count |
+
+### LineTracer
+
+Ray-casting and line tracing utilities:
+
+```cpp
+namespace omath::collision {
+ class LineTracer;
+}
+```
+
+**Features:**
+- Ray-triangle intersection (Möller-Trumbore algorithm)
+- Ray-plane intersection
+- Ray-box intersection
+- Distance calculations
+- Normal calculations at hit points
+
+### 3D Primitives
+
+| Type | Description | Key Methods |
+|------|-------------|-------------|
+| `Plane` | Infinite plane | `intersects_ray()`, `distance_to_point()` |
+| `Box` | Axis-aligned bounding box | `contains()`, `intersects()` |
+| `Mesh` | Polygonal mesh with transforms | `vertex_to_world_space()`, `make_face_in_world_space()` |
+
+**Mesh Features:**
+* Vertex buffer (VBO) and index buffer (VAO/EBO) storage
+* Position, rotation, and scale transformations
+* Cached transformation matrix
+* Engine-specific coordinate system support
+* Compatible with `MeshCollider` for collision detection
+
+---
+
+## Projectile Prediction
+
+### Interfaces
+
+**`ProjPredEngineInterface`** - Base interface for all prediction engines
+
+```cpp
+virtual std::optional>
+maybe_calculate_aim_point(const Projectile&, const Target&) const = 0;
+```
+
+### Implementations
+
+| Engine | Description | Optimizations |
+|--------|-------------|---------------|
+| `ProjPredEngineLegacy` | Standard implementation | Portable, works everywhere |
+| `ProjPredEngineAVX2` | AVX2 optimized | 2-4x faster on modern CPUs |
+
+### Supporting Types
+
+**`Projectile`** - Defines projectile properties:
+```cpp
+struct Projectile {
+ Vector3 origin;
+ float speed;
+ Vector3 gravity;
+ // ... additional properties
+};
+```
+
+**`Target`** - Defines target state:
+```cpp
+struct Target {
+ Vector3 position;
+ Vector3 velocity;
+ // ... additional properties
+};
+```
+
+---
+
+## Pathfinding
+
+### A* Algorithm
+
+```cpp
+namespace omath::pathfinding {
+ template
+ class AStar;
+}
+```
+
+**Features:**
+- Generic node type support
+- Customizable heuristics
+- Efficient priority queue implementation
+- Path reconstruction
+
+### Navigation Mesh
+
+```cpp
+namespace omath::pathfinding {
+ class NavigationMesh;
+}
+```
+
+**Features:**
+- Triangle-based navigation
+- Neighbor connectivity
+- Walkable area definitions
+
+---
+
+## Engine Traits
+
+Each game engine has a trait system providing engine-specific math:
+
+### CameraTrait
+
+Implements camera math for an engine:
+- `calc_look_at_angle()` - Calculate angles to look at a point
+- `calc_view_matrix()` - Build view matrix from angles and position
+- `calc_projection_matrix()` - Build projection matrix from FOV and viewport
+
+### MeshTrait
+
+Provides mesh transformation for an engine:
+- `rotation_matrix()` - Build rotation matrix from engine-specific angles
+- Handles coordinate system differences (Y-up vs Z-up, left/right-handed)
+- Used by `Mesh` class for local-to-world transformations
+
+### PredEngineTrait
+
+Provides physics/ballistics specific to an engine:
+- Gravity vectors
+- Coordinate system conventions
+- Unit conversions
+- Physics parameters
+
+### Available Traits
+
+| Engine | Camera Trait | Mesh Trait | Pred Engine Trait | Constants | Formulas |
+|--------|--------------|------------|-------------------|-----------|----------|
+| Source Engine | ✓ | ✓ | ✓ | ✓ | ✓ |
+| Unity Engine | ✓ | ✓ | ✓ | ✓ | ✓ |
+| Unreal Engine | ✓ | ✓ | ✓ | ✓ | ✓ |
+| Frostbite | ✓ | ✓ | ✓ | ✓ | ✓ |
+| IW Engine | ✓ | ✓ | ✓ | ✓ | ✓ |
+| OpenGL | ✓ | ✓ | ✓ | ✓ | ✓ |
+
+**Documentation:**
+- See `docs/engines//` for detailed per-engine docs
+- Each engine has separate docs for camera_trait, mesh_trait, pred_engine_trait, constants, and formulas
+
+---
+
+## Utility Functions
+
+### Color
+
+```cpp
+struct Color {
+ uint8_t r, g, b, a;
+
+ // Conversions
+ static Color from_hsv(float h, float s, float v);
+ static Color from_hex(uint32_t hex);
+ uint32_t to_hex() const;
+
+ // Blending
+ Color blend(const Color& other, float t) const;
+};
+```
+
+### Pattern Scanning
+
+**Binary pattern search with wildcards:**
+
+```cpp
+// Pattern with wildcards (?? = any byte)
+PatternView pattern{"48 8B 05 ?? ?? ?? ?? 48 85 C0"};
+
+// Scan memory
+auto result = pattern_scan(memory_buffer, pattern);
+if (result) {
+ std::cout << "Found at offset: " << result->offset << "\n";
+}
+```
+
+**PE file scanning:**
+
+```cpp
+PEPatternScanner scanner("target.exe");
+if (auto addr = scanner.scan_pattern(pattern)) {
+ std::cout << "Found at RVA: " << *addr << "\n";
+}
+```
+
+### Reverse Engineering
+
+**External memory access:**
+```cpp
+ExternalRevObject process("game.exe");
+Vector3 position = process.read>(address);
+process.write(address, new_position);
+```
+
+**Internal memory access:**
+```cpp
+InternalRevObject memory;
+auto value = memory.read(address);
+memory.write(address, new_value);
+```
+
+---
+
+## Concepts and Constraints
+
+OMath uses C++20 concepts for type safety:
+
+```cpp
+template
+concept Arithmetic = std::is_arithmetic_v;
+
+template
+concept CameraEngineConcept = requires(EngineTrait t) {
+ { t.calc_look_at_angle(...) } -> /* returns angles */;
+ { t.calc_view_matrix(...) } -> /* returns matrix */;
+ { t.calc_projection_matrix(...) } -> /* returns matrix */;
+};
+```
+
+---
+
+## Error Handling
+
+OMath uses modern C++ error handling:
+
+### std::expected (C++23)
+
+```cpp
+std::expected, Vector3Error>
+angle_between(const Vector3& other) const;
+
+if (auto angle = v1.angle_between(v2)) {
+ // Success: use *angle
+} else {
+ // Error: angle.error() gives Vector3Error
+}
+```
+
+### std::optional
+
+```cpp
+std::optional>
+world_to_screen(const Vector3& world);
+
+if (auto screen = camera.world_to_screen(pos)) {
+ // Success: use screen->x, screen->y
+} else {
+ // Point not visible
+}
+```
+
+### Error Codes
+
+```cpp
+enum class ProjectionError {
+ SUCCESS = 0,
+ POINT_BEHIND_CAMERA,
+ INVALID_VIEWPORT,
+ // ...
+};
+```
+
+---
+
+## Performance Considerations
+
+### constexpr Support
+
+Most operations are `constexpr` where possible:
+
+```cpp
+constexpr Vector3 v{1, 2, 3};
+constexpr auto len_sq = v.length_sqr(); // Computed at compile time
+```
+
+### AVX2 Optimizations
+
+Use AVX2 variants when available:
+
+```cpp
+// Standard: portable but slower
+ProjPredEngineLegacy legacy_engine;
+
+// AVX2: 2-4x faster on modern CPUs
+ProjPredEngineAVX2 fast_engine;
+```
+
+**When to use AVX2:**
+- Modern Intel/AMD processors (2013+)
+- Performance-critical paths
+- Batch operations
+
+**When to use Legacy:**
+- Older processors
+- ARM platforms
+- Guaranteed compatibility
+
+### Cache Efficiency
+
+```cpp
+// Good: contiguous storage
+std::vector> positions;
+
+// Good: structure of arrays for SIMD
+struct Particles {
+ std::vector x, y, z;
+};
+```
+
+---
+
+## Platform Support
+
+| Platform | Support | Notes |
+|----------|---------|-------|
+| Windows | ✓ | MSVC, Clang, GCC |
+| Linux | ✓ | GCC, Clang |
+| macOS | ✓ | Clang |
+
+**Minimum requirements:**
+- C++20 compiler
+- C++23 recommended for `std::expected`
+
+---
+
+## Thread Safety
+
+- **Vector/Matrix types**: Thread-safe (immutable operations)
+- **Camera**: Not thread-safe (mutable state)
+- **Pattern scanning**: Thread-safe (read-only operations)
+- **Memory access**: Depends on OS/process synchronization
+
+**Thread-safe example:**
+```cpp
+// Safe: each thread gets its own camera
+std::vector threads;
+for (int i = 0; i < num_threads; ++i) {
+ threads.emplace_back([i]() {
+ Camera camera = /* create camera */;
+ // Use camera in this thread
+ });
+}
+```
+
+---
+
+## Best Practices
+
+### 1. Use Type Aliases
+
+```cpp
+using Vec3f = omath::Vector3;
+using Mat4 = omath::Mat4X4;
+```
+
+### 2. Prefer constexpr When Possible
+
+```cpp
+constexpr auto compute_at_compile_time() {
+ Vector3 v{1, 2, 3};
+ return v.length_sqr();
+}
+```
+
+### 3. Check Optional/Expected Results
+
+```cpp
+// Good
+if (auto result = camera.world_to_screen(pos)) {
+ use(*result);
+}
+
+// Bad - may crash
+auto result = camera.world_to_screen(pos);
+use(result->x); // Undefined behavior if nullopt
+```
+
+### 4. Use Engine-Specific Types
+
+```cpp
+// Good: uses correct coordinate system
+using namespace omath::source_engine;
+Camera camera = /* ... */;
+
+// Bad: mixing engine types
+using UnityCamera = omath::unity_engine::Camera;
+using SourceAngles = omath::source_engine::ViewAngles;
+UnityCamera camera{pos, SourceAngles{}}; // Wrong!
+```
+
+---
+
+## See Also
+
+- [Getting Started Guide](getting_started.md)
+- [Installation Instructions](install.md)
+- [Examples Directory](../examples/)
+- Individual module documentation in respective folders
+
+---
+
+*Last updated: 13 Nov 2025*
diff --git a/docs/best_practices.md b/docs/best_practices.md
new file mode 100644
index 00000000..d38584ff
--- /dev/null
+++ b/docs/best_practices.md
@@ -0,0 +1,532 @@
+# Best Practices
+
+Guidelines for using OMath effectively and avoiding common pitfalls.
+
+---
+
+## Code Organization
+
+### Use Type Aliases
+
+Define clear type aliases for commonly used types:
+
+```cpp
+// Good: Clear and concise
+using Vec3f = omath::Vector3;
+using Vec2f = omath::Vector2;
+using Mat4 = omath::Mat4X4;
+
+Vec3f position{1.0f, 2.0f, 3.0f};
+```
+
+```cpp
+// Avoid: Verbose and repetitive
+omath::Vector3 position{1.0f, 2.0f, 3.0f};
+omath::Vector3 velocity{0.0f, 0.0f, 0.0f};
+```
+
+### Namespace Usage
+
+Be selective with `using namespace`:
+
+```cpp
+// Good: Specific namespace for your engine
+using namespace omath::source_engine;
+
+// Good: Import specific types
+using omath::Vector3;
+using omath::Vector2;
+
+// Avoid: Too broad
+using namespace omath; // Imports everything
+```
+
+### Include What You Use
+
+```cpp
+// Good: Include specific headers
+#include
+#include
+
+// Okay for development
+#include
+
+// Production: Include only what you need
+// to reduce compile times
+```
+
+---
+
+## Error Handling
+
+### Always Check Optional Results
+
+```cpp
+// Good: Check before using
+if (auto screen = camera.world_to_screen(world_pos)) {
+ draw_at(screen->x, screen->y);
+} else {
+ // Handle point not visible
+}
+
+// Bad: Unchecked access can crash
+auto screen = camera.world_to_screen(world_pos);
+draw_at(screen->x, screen->y); // Undefined behavior if nullopt!
+```
+
+### Handle Expected Errors
+
+```cpp
+// Good: Handle error case
+if (auto angle = v1.angle_between(v2)) {
+ use_angle(*angle);
+} else {
+ switch (angle.error()) {
+ case Vector3Error::IMPOSSIBLE_BETWEEN_ANGLE:
+ // Handle zero-length vector
+ break;
+ }
+}
+
+// Bad: Assume success
+auto angle = v1.angle_between(v2);
+use_angle(*angle); // Throws if error!
+```
+
+### Validate Inputs
+
+```cpp
+// Good: Validate before expensive operations
+bool is_valid_projectile(const Projectile& proj) {
+ return proj.speed > 0.0f &&
+ std::isfinite(proj.speed) &&
+ std::isfinite(proj.origin.length());
+}
+
+if (is_valid_projectile(proj) && is_valid_target(target)) {
+ auto aim = engine.maybe_calculate_aim_point(proj, target);
+}
+```
+
+---
+
+## Performance
+
+### Use constexpr When Possible
+
+```cpp
+// Good: Computed at compile time
+constexpr Vector3 gravity{0.0f, 0.0f, -9.81f};
+constexpr float max_range = 1000.0f;
+constexpr float max_range_sq = max_range * max_range;
+
+// Use in runtime calculations
+if (position.length_sqr() < max_range_sq) {
+ // ...
+}
+```
+
+### Prefer Squared Distance
+
+```cpp
+// Good: Avoids expensive sqrt
+constexpr float max_dist_sq = 100.0f * 100.0f;
+for (const auto& entity : entities) {
+ if (entity.pos.distance_to_sqr(player_pos) < max_dist_sq) {
+ // Process nearby entity
+ }
+}
+
+// Avoid: Unnecessary sqrt calls
+constexpr float max_dist = 100.0f;
+for (const auto& entity : entities) {
+ if (entity.pos.distance_to(player_pos) < max_dist) {
+ // More expensive
+ }
+}
+```
+
+### Cache Expensive Calculations
+
+```cpp
+// Good: Update camera once per frame
+void update_frame() {
+ camera.update(current_position, current_angles);
+
+ // All projections use cached matrices
+ for (const auto& entity : entities) {
+ if (auto screen = camera.world_to_screen(entity.pos)) {
+ draw_entity(screen->x, screen->y);
+ }
+ }
+}
+
+// Bad: Camera recreated each call
+for (const auto& entity : entities) {
+ Camera cam(pos, angles, viewport, fov, near, far); // Expensive!
+ auto screen = cam.world_to_screen(entity.pos);
+}
+```
+
+### Choose the Right Engine
+
+```cpp
+// Good: Use AVX2 when available
+#ifdef __AVX2__
+ using Engine = ProjPredEngineAVX2;
+#else
+ using Engine = ProjPredEngineLegacy;
+#endif
+
+Engine prediction_engine;
+
+// Or runtime detection
+Engine* create_best_engine() {
+ if (cpu_supports_avx2()) {
+ return new ProjPredEngineAVX2();
+ }
+ return new ProjPredEngineLegacy();
+}
+```
+
+### Minimize Allocations
+
+```cpp
+// Good: Reuse vectors
+std::vector> positions;
+positions.reserve(expected_count);
+
+// In loop
+positions.clear(); // Doesn't deallocate
+for (...) {
+ positions.push_back(compute_position());
+}
+
+// Bad: Allocate every time
+for (...) {
+ std::vector> positions; // Allocates each iteration
+ // ...
+}
+```
+
+---
+
+## Type Safety
+
+### Use Strong Angle Types
+
+```cpp
+// Good: Type-safe angles
+PitchAngle pitch = PitchAngle::from_degrees(45.0f);
+YawAngle yaw = YawAngle::from_degrees(90.0f);
+ViewAngles angles{pitch, yaw, RollAngle::from_degrees(0.0f)};
+
+// Bad: Raw floats lose safety
+float pitch = 45.0f; // No range checking
+float yaw = 90.0f; // Can go out of bounds
+```
+
+### Match Engine Types
+
+```cpp
+// Good: Use matching types from same engine
+using namespace omath::source_engine;
+Camera camera = /* ... */;
+ViewAngles angles = /* ... */;
+
+// Bad: Mixing engine types
+using UnityCamera = omath::unity_engine::Camera;
+using SourceAngles = omath::source_engine::ViewAngles;
+UnityCamera camera{pos, SourceAngles{}, ...}; // May cause issues!
+```
+
+### Template Type Parameters
+
+```cpp
+// Good: Explicit and clear
+Vector3 position;
+Vector3 high_precision_pos;
+
+// Okay: Use default float
+Vector3<> position; // Defaults to float
+
+// Avoid: Mixing types unintentionally
+Vector3 a;
+Vector3 b;
+auto result = a + b; // Type mismatch!
+```
+
+---
+
+## Testing & Validation
+
+### Test Edge Cases
+
+```cpp
+void test_projection() {
+ Camera camera = setup_camera();
+
+ // Test normal case
+ assert(camera.world_to_screen({100, 100, 100}).has_value());
+
+ // Test edge cases
+ assert(!camera.world_to_screen({0, 0, -100}).has_value()); // Behind
+ assert(!camera.world_to_screen({1e10, 0, 0}).has_value()); // Too far
+
+ // Test boundaries
+ Vector3 at_near{0, 0, camera.near_plane() + 0.1f};
+ assert(camera.world_to_screen(at_near).has_value());
+}
+```
+
+### Validate Assumptions
+
+```cpp
+void validate_game_data() {
+ // Validate FOV
+ float fov = read_game_fov();
+ assert(fov > 1.0f && fov < 179.0f);
+
+ // Validate positions
+ Vector3 pos = read_player_position();
+ assert(std::isfinite(pos.x));
+ assert(std::isfinite(pos.y));
+ assert(std::isfinite(pos.z));
+
+ // Validate viewport
+ ViewPort vp = read_viewport();
+ assert(vp.width > 0 && vp.height > 0);
+}
+```
+
+### Use Assertions
+
+```cpp
+// Good: Catch errors early in development
+void shoot_projectile(const Projectile& proj) {
+ assert(proj.speed > 0.0f && "Projectile speed must be positive");
+ assert(std::isfinite(proj.origin.length()) && "Invalid projectile origin");
+
+ // Continue with logic
+}
+
+// Add debug-only checks
+#ifndef NDEBUG
+ if (!is_valid_input(data)) {
+ std::cerr << "Warning: Invalid input detected\n";
+ }
+#endif
+```
+
+---
+
+## Memory & Resources
+
+### RAII for Resources
+
+```cpp
+// Good: Automatic cleanup
+class GameOverlay {
+ Camera camera_;
+ std::vector entities_;
+
+public:
+ GameOverlay(/* ... */) : camera_(/* ... */) {
+ entities_.reserve(1000);
+ }
+
+ // Resources cleaned up automatically
+ ~GameOverlay() = default;
+};
+```
+
+### Avoid Unnecessary Copies
+
+```cpp
+// Good: Pass by const reference
+void draw_entities(const std::vector>& positions) {
+ for (const auto& pos : positions) {
+ // Process position
+ }
+}
+
+// Bad: Copies entire vector
+void draw_entities(std::vector> positions) {
+ // Expensive copy!
+}
+
+// Good: Move when transferring ownership
+std::vector> compute_positions();
+auto positions = compute_positions(); // Move, not copy
+```
+
+### Use Structured Bindings
+
+```cpp
+// Good: Clear and concise
+if (auto [success, screen_pos] = try_project(world_pos); success) {
+ draw_at(screen_pos.x, screen_pos.y);
+}
+
+// Good: Decompose results
+auto [x, y, z] = position.as_tuple();
+```
+
+---
+
+## Documentation
+
+### Document Assumptions
+
+```cpp
+// Good: Clear documentation
+/**
+ * Projects world position to screen space.
+ *
+ * @param world_pos Position in world coordinates (meters)
+ * @return Screen position if visible, nullopt if behind camera or out of view
+ *
+ * @note Assumes camera.update() was called this frame
+ * @note Screen coordinates are in viewport space [0, width] x [0, height]
+ */
+std::optional> project(const Vector3& world_pos);
+```
+
+### Explain Non-Obvious Code
+
+```cpp
+// Good: Explain the math
+// Use squared distance to avoid expensive sqrt
+// max_range = 100.0 → max_range_sq = 10000.0
+constexpr float max_range_sq = 100.0f * 100.0f;
+if (dist_sq < max_range_sq) {
+ // Entity is in range
+}
+
+// Explain engine-specific quirks
+// Source Engine uses Z-up coordinates, but angles are in degrees
+// Pitch: [-89, 89], Yaw: [-180, 180], Roll: [-180, 180]
+ViewAngles angles{pitch, yaw, roll};
+```
+
+---
+
+## Debugging
+
+### Add Debug Visualization
+
+```cpp
+#ifndef NDEBUG
+void debug_draw_projection() {
+ // Draw camera frustum
+ draw_frustum(camera);
+
+ // Draw world axes
+ draw_line({0,0,0}, {100,0,0}, Color::Red); // X
+ draw_line({0,0,0}, {0,100,0}, Color::Green); // Y
+ draw_line({0,0,0}, {0,0,100}, Color::Blue); // Z
+
+ // Draw projected points
+ for (const auto& entity : entities) {
+ if (auto screen = camera.world_to_screen(entity.pos)) {
+ draw_cross(screen->x, screen->y);
+ }
+ }
+}
+#endif
+```
+
+### Log Important Values
+
+```cpp
+void debug_projection_failure(const Vector3& pos) {
+ std::cerr << "Projection failed for position: "
+ << pos.x << ", " << pos.y << ", " << pos.z << "\n";
+
+ auto view_matrix = camera.get_view_matrix();
+ std::cerr << "View matrix:\n";
+ // Print matrix...
+
+ std::cerr << "Camera position: "
+ << camera.position().x << ", "
+ << camera.position().y << ", "
+ << camera.position().z << "\n";
+}
+```
+
+### Use Debug Builds
+
+```cmake
+# CMakeLists.txt
+if(CMAKE_BUILD_TYPE STREQUAL "Debug")
+ target_compile_definitions(your_target PRIVATE
+ DEBUG_PROJECTION=1
+ VALIDATE_INPUTS=1
+ )
+endif()
+```
+
+```cpp
+#ifdef DEBUG_PROJECTION
+ std::cout << "Projecting: " << world_pos << "\n";
+#endif
+
+#ifdef VALIDATE_INPUTS
+ assert(std::isfinite(world_pos.length()));
+#endif
+```
+
+---
+
+## Platform Considerations
+
+### Cross-Platform Code
+
+```cpp
+// Good: Platform-agnostic
+constexpr float PI = 3.14159265359f;
+
+// Avoid: Platform-specific
+#ifdef _WIN32
+ // Windows-only code
+#endif
+```
+
+### Handle Different Compilers
+
+```cpp
+// Good: Compiler-agnostic
+#if defined(_MSC_VER)
+ // MSVC-specific
+#elif defined(__GNUC__)
+ // GCC/Clang-specific
+#endif
+
+// Use OMath's built-in compatibility
+// It handles compiler differences automatically
+```
+
+---
+
+## Summary
+
+**Key principles:**
+1. **Safety first**: Always check optional/expected results
+2. **Performance matters**: Use constexpr, avoid allocations, cache results
+3. **Type safety**: Use strong types, match engine types
+4. **Clear code**: Use aliases, document assumptions, explain non-obvious logic
+5. **Test thoroughly**: Validate inputs, test edge cases, add assertions
+6. **Debug effectively**: Add visualization, log values, use debug builds
+
+---
+
+## See Also
+
+- [Troubleshooting Guide](troubleshooting.md)
+- [FAQ](faq.md)
+- [API Overview](api_overview.md)
+- [Tutorials](tutorials.md)
+
+---
+
+*Last updated: 1 Nov 2025*
diff --git a/docs/collision/collider_interface.md b/docs/collision/collider_interface.md
new file mode 100644
index 00000000..7346af54
--- /dev/null
+++ b/docs/collision/collider_interface.md
@@ -0,0 +1,93 @@
+# `omath::collision::ColliderInterface` — Abstract collider base class
+
+> Header: `omath/collision/collider_interface.hpp`
+> Namespace: `omath::collision`
+> Depends on: `omath/linear_algebra/vector3.hpp`
+
+`ColliderInterface` is the abstract base class for all colliders used by the GJK and EPA algorithms. Any shape that can report its furthest vertex along a given direction can implement this interface and be used for collision detection.
+
+---
+
+## API
+
+```cpp
+namespace omath::collision {
+
+template>
+class ColliderInterface {
+public:
+ using VectorType = VecType;
+
+ virtual ~ColliderInterface() = default;
+
+ // Return the world-space position of the vertex furthest along `direction`.
+ [[nodiscard]]
+ virtual VectorType find_abs_furthest_vertex_position(
+ const VectorType& direction) const = 0;
+
+ // Get the collider's origin (center / position).
+ [[nodiscard]]
+ virtual const VectorType& get_origin() const = 0;
+
+ // Reposition the collider.
+ virtual void set_origin(const VectorType& new_origin) = 0;
+};
+
+} // namespace omath::collision
+```
+
+---
+
+## Implementing a custom collider
+
+To create a new collider shape, derive from `ColliderInterface` and implement the three pure-virtual methods:
+
+```cpp
+#include "omath/collision/collider_interface.hpp"
+
+class SphereCollider final
+ : public omath::collision::ColliderInterface>
+{
+public:
+ SphereCollider(omath::Vector3 center, float radius)
+ : m_center(center), m_radius(radius) {}
+
+ [[nodiscard]]
+ omath::Vector3 find_abs_furthest_vertex_position(
+ const omath::Vector3& direction) const override
+ {
+ return m_center + direction.normalized() * m_radius;
+ }
+
+ [[nodiscard]]
+ const omath::Vector3& get_origin() const override
+ { return m_center; }
+
+ void set_origin(const omath::Vector3& new_origin) override
+ { m_center = new_origin; }
+
+private:
+ omath::Vector3 m_center;
+ float m_radius;
+};
+```
+
+---
+
+## Notes
+
+* **Template parameter**: The default vector type is `Vector3`, but any vector type with a `dot()` method can be used.
+* **GJK/EPA compatibility**: Both `GjkAlgorithm` and `EpaAlgorithm` accept any type satisfying the `ColliderInterface` contract through their template parameters.
+
+---
+
+## See also
+
+* [GJK Algorithm](gjk_algorithm.md) — collision detection using GJK.
+* [EPA Algorithm](epa_algorithm.md) — penetration depth via EPA.
+* [Mesh Collider](mesh_collider.md) — concrete collider wrapping a `Mesh`.
+* [Simplex](simplex.md) — simplex data structure used by GJK/EPA.
+
+---
+
+*Last updated: Feb 2026*
diff --git a/docs/collision/epa_algorithm.md b/docs/collision/epa_algorithm.md
new file mode 100644
index 00000000..68c23f6b
--- /dev/null
+++ b/docs/collision/epa_algorithm.md
@@ -0,0 +1,322 @@
+# `omath::collision::Epa` — Expanding Polytope Algorithm for penetration depth
+
+> Header: `omath/collision/epa_algorithm.hpp`
+> Namespace: `omath::collision`
+> Depends on: `Simplex`, collider types with `find_abs_furthest_vertex` method
+> Algorithm: **EPA** (Expanding Polytope Algorithm) for penetration depth and contact normal
+
+---
+
+## Overview
+
+The **EPA (Expanding Polytope Algorithm)** calculates the **penetration depth** and **separation normal** between two intersecting convex shapes. It is typically used as a follow-up to the GJK algorithm after a collision has been detected.
+
+EPA takes a 4-point simplex containing the origin (from GJK) and iteratively expands it to find the point on the Minkowski difference closest to the origin. This point gives both:
+* **Depth**: minimum translation distance to separate the shapes
+* **Normal**: direction of separation (pointing from shape B to shape A)
+
+`Epa` is a template class working with any collider type that implements the support function interface.
+
+---
+
+## `Epa::Result`
+
+```cpp
+struct Result final {
+ bool success{false}; // true if EPA converged
+ Vertex normal{}; // outward normal (from B to A)
+ float depth{0.0f}; // penetration depth
+ int iterations{0}; // number of iterations performed
+ int num_vertices{0}; // final polytope vertex count
+ int num_faces{0}; // final polytope face count
+};
+```
+
+### Fields
+
+* `success` — `true` if EPA successfully computed depth and normal; `false` if it failed to converge
+* `normal` — unit vector pointing from shape B toward shape A (separation direction)
+* `depth` — minimum distance to move shape A along `normal` to separate the shapes
+* `iterations` — actual iteration count (useful for performance tuning)
+* `num_vertices`, `num_faces` — final polytope size (for diagnostics)
+
+---
+
+## `Epa::Params`
+
+```cpp
+struct Params final {
+ int max_iterations{64}; // maximum iterations before giving up
+ float tolerance{1e-4f}; // absolute tolerance on distance growth
+};
+```
+
+### Fields
+
+* `max_iterations` — safety limit to prevent infinite loops (default 64)
+* `tolerance` — convergence threshold: stop when distance grows less than this (default 1e-4)
+
+---
+
+## `Epa` Template Class
+
+```cpp
+template
+class Epa final {
+public:
+ using Vertex = typename ColliderType::VertexType;
+ static_assert(EpaVector, "VertexType must satisfy EpaVector concept");
+
+ // Solve for penetration depth and normal
+ [[nodiscard]]
+ static Result solve(
+ const ColliderType& a,
+ const ColliderType& b,
+ const Simplex& simplex,
+ const Params params = {}
+ );
+};
+```
+
+### Precondition
+
+The `simplex` parameter must:
+* Have exactly 4 points (`simplex.size() == 4`)
+* Contain the origin (i.e., be a valid GJK result with `hit == true`)
+
+Violating this precondition leads to undefined behavior.
+
+---
+
+## Collider Requirements
+
+Any type used as `ColliderType` must provide:
+
+```cpp
+// Type alias for vertex type (typically Vector3)
+using VertexType = /* ... */;
+
+// Find the farthest point in world space along the given direction
+[[nodiscard]]
+VertexType find_abs_furthest_vertex(const VertexType& direction) const;
+```
+
+---
+
+## Algorithm Details
+
+### Expanding Polytope
+
+EPA maintains a convex polytope (polyhedron) in Minkowski difference space `A - B`. Starting from the 4-point tetrahedron (simplex from GJK), it repeatedly:
+
+1. **Find closest face** to the origin
+2. **Support query** in the direction of the face normal
+3. **Expand polytope** by adding the new support point
+4. **Update faces** to maintain convexity
+
+The algorithm terminates when:
+* **Convergence**: the distance from origin to polytope stops growing (within tolerance)
+* **Max iterations**: safety limit reached
+* **Failure cases**: degenerate polytope or numerical issues
+
+### Minkowski Difference
+
+Like GJK, EPA operates in Minkowski difference space where `point = a - b` for points in shapes A and B. The closest point on this polytope to the origin gives the minimum separation.
+
+### Face Winding
+
+Faces are stored with outward-pointing normals. The algorithm uses a priority queue to efficiently find the face closest to the origin.
+
+---
+
+## Vertex Type Requirements
+
+The `VertexType` must satisfy the `EpaVector` concept:
+
+```cpp
+template
+concept EpaVector = requires(const V& a, const V& b, float s) {
+ { a - b } -> std::same_as;
+ { a.cross(b) } -> std::same_as;
+ { a.dot(b) } -> std::same_as;
+ { -a } -> std::same_as;
+ { a * s } -> std::same_as;
+ { a / s } -> std::same_as;
+};
+```
+
+`omath::Vector3` satisfies this concept.
+
+---
+
+## Usage Examples
+
+### Basic EPA Usage
+
+```cpp
+using namespace omath::collision;
+using namespace omath::source_engine;
+
+// First, run GJK to detect collision
+MeshCollider collider_a(mesh_a);
+MeshCollider collider_b(mesh_b);
+
+auto gjk_result = GjkAlgorithm>::check_collision(
+ collider_a,
+ collider_b
+);
+
+if (gjk_result.hit) {
+ // Collision detected, use EPA to get penetration info
+ auto epa_result = Epa>::solve(
+ collider_a,
+ collider_b,
+ gjk_result.simplex
+ );
+
+ if (epa_result.success) {
+ std::cout << "Penetration depth: " << epa_result.depth << "\n";
+ std::cout << "Separation normal: "
+ << "(" << epa_result.normal.x << ", "
+ << epa_result.normal.y << ", "
+ << epa_result.normal.z << ")\n";
+
+ // Apply separation: move A away from B
+ Vector3 correction = epa_result.normal * epa_result.depth;
+ mesh_a.set_origin(mesh_a.get_origin() + correction);
+ }
+}
+```
+
+### Custom Parameters
+
+```cpp
+// Use custom convergence settings
+Epa::Params params;
+params.max_iterations = 128; // Allow more iterations for complex shapes
+params.tolerance = 1e-5f; // Tighter tolerance for more accuracy
+
+auto result = Epa::solve(a, b, simplex, params);
+```
+
+### Physics Integration
+
+```cpp
+void resolve_collision(PhysicsBody& body_a, PhysicsBody& body_b) {
+ auto gjk_result = GjkAlgorithm::check_collision(
+ body_a.collider, body_b.collider
+ );
+
+ if (!gjk_result.hit)
+ return; // No collision
+
+ auto epa_result = Epa::solve(
+ body_a.collider,
+ body_b.collider,
+ gjk_result.simplex
+ );
+
+ if (epa_result.success) {
+ // Separate bodies
+ float mass_sum = body_a.mass + body_b.mass;
+ float ratio_a = body_b.mass / mass_sum;
+ float ratio_b = body_a.mass / mass_sum;
+
+ body_a.position += epa_result.normal * (epa_result.depth * ratio_a);
+ body_b.position -= epa_result.normal * (epa_result.depth * ratio_b);
+
+ // Apply collision response
+ apply_impulse(body_a, body_b, epa_result.normal);
+ }
+}
+```
+
+---
+
+## Performance Characteristics
+
+* **Time complexity**: O(k × f) where k is iterations and f is faces per iteration (typically f grows slowly)
+* **Space complexity**: O(n) where n is the number of polytope vertices (typically < 100)
+* **Typical iterations**: 4-20 for most collisions
+* **Worst case**: 64 iterations (configurable limit)
+
+### Performance Tips
+
+1. **Adjust max_iterations**: Balance accuracy vs. performance for your use case
+2. **Tolerance tuning**: Larger tolerance = faster convergence but less accurate
+3. **Shape complexity**: Simpler shapes (fewer faces) converge faster
+4. **Deep penetrations**: Require more iterations; consider broad-phase separation
+
+---
+
+## Limitations & Edge Cases
+
+* **Requires valid simplex**: Must be called with a 4-point simplex containing the origin (from successful GJK)
+* **Convex shapes only**: Like GJK, EPA only works with convex colliders
+* **Convergence failure**: Can fail to converge for degenerate or very thin shapes (check `result.success`)
+* **Numerical precision**: Extreme scale differences or very small shapes may cause issues
+* **Deep penetration**: Very deep intersections may require many iterations or fail to converge
+
+### Error Handling
+
+```cpp
+auto result = Epa::solve(a, b, simplex);
+
+if (!result.success) {
+ // EPA failed to converge
+ // Fallback options:
+ // 1. Use a default separation (e.g., axis between centers)
+ // 2. Increase max_iterations and retry
+ // 3. Log a warning and skip this collision
+ std::cerr << "EPA failed after " << result.iterations << " iterations\n";
+}
+```
+
+---
+
+## Theory & Background
+
+### Why EPA after GJK?
+
+GJK determines **if** shapes intersect but doesn't compute penetration depth. EPA extends GJK's final simplex to find the exact depth and normal needed for:
+* **Collision response** — separating objects realistically
+* **Contact manifolds** — generating contact points for physics
+* **Constraint solving** — iterative physics solvers
+
+### Comparison with SAT
+
+| Feature | EPA | SAT (Separating Axis Theorem) |
+|---------|-----|-------------------------------|
+| Works with | Any convex shape | Polytopes (faces/edges) |
+| Penetration depth | Yes | Yes |
+| Complexity | Iterative | Per-axis projection |
+| Best for | General convex | Boxes, prisms |
+| Typical speed | Moderate | Fast (few axes) |
+
+EPA is more general; SAT is faster for axis-aligned shapes.
+
+---
+
+## Implementation Details
+
+The EPA implementation in OMath:
+* Uses a **priority queue** to efficiently find the closest face
+* Maintains face winding for consistent normals
+* Handles **edge cases**: degenerate faces, numerical instability
+* Prevents infinite loops with iteration limits
+* Returns detailed diagnostics (iteration count, polytope size)
+
+---
+
+## See Also
+
+- [GJK Algorithm Documentation](gjk_algorithm.md) - Collision detection (required before EPA)
+- [Simplex Documentation](simplex.md) - Input simplex structure
+- [MeshCollider Documentation](mesh_collider.md) - Mesh-based collider
+- [Mesh Documentation](../3d_primitives/mesh.md) - Mesh primitive
+- [Tutorials - Collision Detection](../tutorials.md#tutorial-4-collision-detection) - Complete collision tutorial
+- [API Overview](../api_overview.md) - High-level API reference
+
+---
+
+*Last updated: 13 Nov 2025*
diff --git a/docs/collision/gjk_algorithm.md b/docs/collision/gjk_algorithm.md
new file mode 100644
index 00000000..4fa13926
--- /dev/null
+++ b/docs/collision/gjk_algorithm.md
@@ -0,0 +1,216 @@
+# `omath::collision::GjkAlgorithm` — Gilbert-Johnson-Keerthi collision detection
+
+> Header: `omath/collision/gjk_algorithm.hpp`
+> Namespace: `omath::collision`
+> Depends on: `Simplex`, collider types with `find_abs_furthest_vertex` method
+> Algorithm: **GJK** (Gilbert-Johnson-Keerthi) for convex shape collision detection
+
+---
+
+## Overview
+
+The **GJK algorithm** determines whether two convex shapes intersect by iteratively constructing a simplex in Minkowski difference space. The algorithm is widely used in physics engines and collision detection systems due to its efficiency and robustness.
+
+`GjkAlgorithm` is a template class that works with any collider type implementing the required support function interface:
+
+* `find_abs_furthest_vertex(direction)` — returns the farthest point in the collider along the given direction.
+
+The algorithm returns a `GjkHitInfo` containing:
+* `hit` — boolean indicating whether the shapes intersect
+* `simplex` — a 4-point simplex containing the origin (valid only when `hit == true`)
+
+---
+
+## `GjkHitInfo`
+
+```cpp
+template
+struct GjkHitInfo final {
+ bool hit{false}; // true if collision detected
+ Simplex simplex; // 4-point simplex (valid only if hit == true)
+};
+```
+
+The `simplex` field is only meaningful when `hit == true` and contains 4 points. This simplex can be passed to the EPA algorithm for penetration depth calculation.
+
+---
+
+## `GjkAlgorithm`
+
+```cpp
+template
+class GjkAlgorithm final {
+ using VertexType = typename ColliderType::VertexType;
+
+public:
+ // Find support vertex in Minkowski difference
+ [[nodiscard]]
+ static VertexType find_support_vertex(
+ const ColliderType& collider_a,
+ const ColliderType& collider_b,
+ const VertexType& direction
+ );
+
+ // Check if two convex shapes intersect
+ [[nodiscard]]
+ static GjkHitInfo check_collision(
+ const ColliderType& collider_a,
+ const ColliderType& collider_b
+ );
+};
+```
+
+---
+
+## Collider Requirements
+
+Any type used as `ColliderType` must provide:
+
+```cpp
+// Type alias for vertex type (typically Vector3)
+using VertexType = /* ... */;
+
+// Find the farthest point in world space along the given direction
+[[nodiscard]]
+VertexType find_abs_furthest_vertex(const VertexType& direction) const;
+```
+
+Common collider types:
+* `MeshCollider` — for arbitrary triangle meshes
+* Custom colliders for spheres, boxes, capsules, etc.
+
+---
+
+## Algorithm Details
+
+### Minkowski Difference
+
+GJK operates in the **Minkowski difference** space `A - B`, where a point in this space represents the difference between points in shapes A and B. The shapes intersect if and only if the origin lies within this Minkowski difference.
+
+### Support Function
+
+The support function finds the point in the Minkowski difference farthest along a given direction:
+
+```cpp
+support(A, B, dir) = A.furthest(dir) - B.furthest(-dir)
+```
+
+This is computed by `find_support_vertex`.
+
+### Simplex Iteration
+
+The algorithm builds a simplex incrementally:
+1. Start with an initial direction (typically vector between shape centers)
+2. Add support vertices in directions that move the simplex toward the origin
+3. Simplify the simplex to keep only points closest to the origin
+4. Repeat until either:
+ * Origin is contained (collision detected, returns 4-point simplex)
+ * No progress can be made (no collision)
+
+Maximum 64 iterations are performed to prevent infinite loops in edge cases.
+
+---
+
+## Usage Examples
+
+### Basic Collision Check
+
+```cpp
+using namespace omath::collision;
+using namespace omath::source_engine;
+
+// Create mesh colliders
+Mesh mesh_a = /* ... */;
+Mesh mesh_b = /* ... */;
+
+MeshCollider collider_a(mesh_a);
+MeshCollider collider_b(mesh_b);
+
+// Check for collision
+auto result = GjkAlgorithm>::check_collision(
+ collider_a,
+ collider_b
+);
+
+if (result.hit) {
+ std::cout << "Collision detected!\n";
+ // Can pass result.simplex to EPA for penetration depth
+}
+```
+
+### Combined with EPA
+
+```cpp
+auto gjk_result = GjkAlgorithm::check_collision(a, b);
+
+if (gjk_result.hit) {
+ // Get penetration depth and normal using EPA
+ auto epa_result = Epa::solve(
+ a, b, gjk_result.simplex
+ );
+
+ if (epa_result.success) {
+ std::cout << "Penetration depth: " << epa_result.depth << "\n";
+ std::cout << "Separation normal: " << epa_result.normal << "\n";
+ }
+}
+```
+
+---
+
+## Performance Characteristics
+
+* **Time complexity**: O(k) where k is the number of iterations (typically < 20 for most cases)
+* **Space complexity**: O(1) — only stores a 4-point simplex
+* **Best case**: 4-8 iterations for well-separated objects
+* **Worst case**: 64 iterations (hard limit)
+* **Cache efficient**: operates on small fixed-size data structures
+
+### Optimization Tips
+
+1. **Initial direction**: Use vector between shape centers for faster convergence
+2. **Early exit**: GJK quickly rejects non-intersecting shapes
+3. **Warm starting**: Reuse previous simplex for continuous collision detection
+4. **Broad phase**: Use spatial partitioning before GJK (AABB trees, grids)
+
+---
+
+## Limitations & Edge Cases
+
+* **Convex shapes only**: GJK only works with convex colliders. For concave shapes, decompose into convex parts or use a mesh collider wrapper.
+* **Degenerate simplices**: The algorithm handles degenerate cases, but numerical precision can cause issues with very thin or flat shapes.
+* **Iteration limit**: Hard limit of 64 iterations prevents infinite loops but may miss collisions in extreme cases.
+* **Zero-length directions**: The simplex update logic guards against zero-length vectors, returning safe fallbacks.
+
+---
+
+## Vertex Type Requirements
+
+The `VertexType` must satisfy the `GjkVector` concept (defined in `simplex.hpp`):
+
+```cpp
+template
+concept GjkVector = requires(const V& a, const V& b) {
+ { -a } -> std::same_as;
+ { a - b } -> std::same_as;
+ { a.cross(b) } -> std::same_as;
+ { a.point_to_same_direction(b) } -> std::same_as;
+};
+```
+
+`omath::Vector3` satisfies this concept.
+
+---
+
+## See Also
+
+- [EPA Algorithm Documentation](epa_algorithm.md) - Penetration depth calculation
+- [Simplex Documentation](simplex.md) - Simplex data structure
+- [MeshCollider Documentation](mesh_collider.md) - Mesh-based collider
+- [Mesh Documentation](../3d_primitives/mesh.md) - Mesh primitive
+- [LineTracer Documentation](line_tracer.md) - Ray-triangle intersection
+- [Tutorials - Collision Detection](../tutorials.md#tutorial-4-collision-detection) - Complete collision tutorial
+
+---
+
+*Last updated: 13 Nov 2025*
diff --git a/docs/collision/line_tracer.md b/docs/collision/line_tracer.md
new file mode 100644
index 00000000..9e9e1f8a
--- /dev/null
+++ b/docs/collision/line_tracer.md
@@ -0,0 +1,181 @@
+# `omath::collision::Ray` & `LineTracer` — Ray–Triangle intersection (Möller–Trumbore)
+
+> Headers: your project’s `ray.hpp` (includes `omath/linear_algebra/triangle.hpp`, `omath/linear_algebra/vector3.hpp`)
+> Namespace: `omath::collision`
+> Depends on: `omath::Vector3`, `omath::Triangle>`
+> Algorithm: **Möller–Trumbore** ray–triangle intersection (no allocation)
+
+---
+
+## Overview
+
+These types provide a minimal, fast path to test and compute intersections between a **ray or line segment** and a **single triangle**:
+
+* `Ray` — start/end points plus a flag to treat the ray as **infinite** (half-line) or a **finite segment**.
+* `LineTracer` — static helpers:
+
+ * `can_trace_line(ray, triangle)` → `true` if they intersect.
+ * `get_ray_hit_point(ray, triangle)` → the hit point (precondition: intersection exists).
+
+---
+
+## `Ray`
+
+```cpp
+class Ray {
+public:
+ omath::Vector3 start; // ray origin
+ omath::Vector3 end; // end point (for finite segment) or a point along the direction
+ bool infinite_length = false;
+
+ [[nodiscard]] omath::Vector3 direction_vector() const noexcept;
+ [[nodiscard]] omath::Vector3 direction_vector_normalized() const noexcept;
+};
+```
+
+### Semantics
+
+* **Direction**: `direction_vector() == end - start`.
+ The normalized variant returns a unit vector (or `{0,0,0}` if the direction length is zero).
+* **Extent**:
+
+ * `infinite_length == true` → treat as a **semi-infinite ray** from `start` along `direction`.
+ * `infinite_length == false` → treat as a **closed segment** from `start` to `end`.
+
+> Tip: For an infinite ray that points along some vector `d`, set `end = start + d`.
+
+---
+
+## `LineTracer`
+
+```cpp
+class LineTracer {
+public:
+ LineTracer() = delete;
+
+ [[nodiscard]]
+ static bool can_trace_line(
+ const Ray& ray,
+ const omath::Triangle>& triangle
+ ) noexcept;
+
+ // Möller–Trumbore intersection
+ [[nodiscard]]
+ static omath::Vector3 get_ray_hit_point(
+ const Ray& ray,
+ const omath::Triangle>& triangle
+ ) noexcept;
+};
+```
+
+### Behavior & contract
+
+* **Intersection test**: `can_trace_line` returns `true` iff the ray/segment intersects the triangle (within the ray’s extent).
+* **Hit point**: `get_ray_hit_point` **assumes** there is an intersection.
+ Call **only after** `can_trace_line(...) == true`. Otherwise the result is unspecified.
+* **Triangle winding**: Standard Möller–Trumbore works with either winding; no backface culling is implied here.
+* **Degenerate inputs**: A zero-length ray or degenerate triangle yields **no hit** under typical Möller–Trumbore tolerances.
+
+---
+
+## Quick examples
+
+### 1) Segment vs triangle
+
+```cpp
+using omath::Vector3;
+using omath::Triangle;
+using omath::collision::Ray;
+using omath::collision::LineTracer;
+
+Triangle> tri(
+ Vector3{0, 0, 0},
+ Vector3{1, 0, 0},
+ Vector3{0, 1, 0}
+);
+
+Ray seg;
+seg.start = {0.25f, 0.25f, 1.0f};
+seg.end = {0.25f, 0.25f,-1.0f};
+seg.infinite_length = false; // finite segment
+
+if (LineTracer::can_trace_line(seg, tri)) {
+ Vector3 hit = LineTracer::get_ray_hit_point(seg, tri);
+ // use hit
+}
+```
+
+### 2) Infinite ray
+
+```cpp
+Ray ray;
+ray.start = {0.5f, 0.5f, 1.0f};
+ray.end = ray.start + Vector3{0, 0, -1}; // direction only
+ray.infinite_length = true;
+
+bool hit = LineTracer::can_trace_line(ray, tri);
+```
+
+---
+
+## Notes & edge cases
+
+* **Normalization**: `direction_vector_normalized()` returns `{0,0,0}` for a zero-length direction (safe, but unusable for tracing).
+* **Precision**: The underlying algorithm uses EPS thresholds to reject nearly parallel cases; results near edges can be sensitive to floating-point error. If you need robust edge inclusion/exclusion, document and enforce a policy (e.g., inclusive barycentric range with small epsilon).
+* **Hit location**: The point returned by `get_ray_hit_point` lies **on the triangle plane** and within its area by construction (when `can_trace_line` is `true`).
+
+---
+
+## API summary
+
+```cpp
+namespace omath::collision {
+
+class Ray {
+public:
+ Vector3 start, end;
+ bool infinite_length = false;
+
+ [[nodiscard]] Vector3 direction_vector() const noexcept;
+ [[nodiscard]] Vector3 direction_vector_normalized() const noexcept;
+};
+
+class LineTracer {
+public:
+ LineTracer() = delete;
+
+ [[nodiscard]] static bool can_trace_line(
+ const Ray&,
+ const omath::Triangle>&
+ ) noexcept;
+
+ [[nodiscard]] static Vector3 get_ray_hit_point(
+ const Ray&,
+ const omath::Triangle>&
+ ) noexcept; // precondition: can_trace_line(...) == true
+};
+
+} // namespace omath::collision
+```
+
+---
+
+## Implementation hints (if you extend it)
+
+* Expose a variant that returns **barycentric coordinates** `(u, v, w)` alongside the hit point to support texture lookup or edge tests.
+* Provide an overload returning `std::optional>` (or `expected`) for safer one-shot queries without a separate test call.
+* If you need backface culling, add a flag or dedicated function (reject hits where the signed distance is negative with respect to triangle normal).
+
+---
+
+## See Also
+
+- [Plane Documentation](../3d_primitives/plane.md) - Ray-plane intersection
+- [Box Documentation](../3d_primitives/box.md) - AABB collision detection
+- [Triangle Documentation](../linear_algebra/triangle.md) - Triangle primitives
+- [Tutorials - Collision Detection](../tutorials.md#tutorial-4-collision-detection) - Complete collision tutorial
+- [Getting Started Guide](../getting_started.md) - Quick start with OMath
+
+---
+
+*Last updated: 1 Nov 2025*
diff --git a/docs/collision/mesh_collider.md b/docs/collision/mesh_collider.md
new file mode 100644
index 00000000..2e1b6f12
--- /dev/null
+++ b/docs/collision/mesh_collider.md
@@ -0,0 +1,371 @@
+# `omath::collision::MeshCollider` — Convex hull collider for meshes
+
+> Header: `omath/collision/mesh_collider.hpp`
+> Namespace: `omath::collision`
+> Depends on: `omath::primitives::Mesh`, `omath::Vector3`
+> Purpose: wrap a mesh to provide collision detection support for GJK/EPA
+
+---
+
+## Overview
+
+`MeshCollider` wraps a `Mesh` object to provide the **support function** interface required by the GJK and EPA collision detection algorithms. The support function finds the vertex of the mesh farthest along a given direction, which is essential for constructing Minkowski difference simplices.
+
+**Important**: `MeshCollider` assumes the mesh represents a **convex hull**. For non-convex shapes, you must either:
+* Decompose into convex parts
+* Use the convex hull of the mesh
+* Use a different collision detection algorithm
+
+---
+
+## Template Declaration
+
+```cpp
+template
+class MeshCollider;
+```
+
+### MeshType Requirements
+
+The `MeshType` must be an instantiation of `omath::primitives::Mesh` or provide:
+
+```cpp
+struct MeshType {
+ using NumericType = /* float, double, etc. */;
+
+ std::vector> m_vertex_buffer;
+
+ // Transform vertex from local to world space
+ Vector3 vertex_to_world_space(const Vector3&) const;
+};
+```
+
+Common types:
+* `omath::source_engine::Mesh`
+* `omath::unity_engine::Mesh`
+* `omath::unreal_engine::Mesh`
+* `omath::frostbite_engine::Mesh`
+* `omath::iw_engine::Mesh`
+* `omath::opengl_engine::Mesh`
+
+---
+
+## Type Aliases
+
+```cpp
+using NumericType = typename MeshType::NumericType;
+using VertexType = Vector3;
+```
+
+* `NumericType` — scalar type (typically `float`)
+* `VertexType` — 3D vector type for vertices
+
+---
+
+## Constructor
+
+```cpp
+explicit MeshCollider(MeshType mesh);
+```
+
+Creates a collider from a mesh. The mesh is **moved** into the collider, so pass by value:
+
+```cpp
+omath::source_engine::Mesh my_mesh = /* ... */;
+MeshCollider collider(std::move(my_mesh));
+```
+
+---
+
+## Methods
+
+### `find_furthest_vertex`
+
+```cpp
+[[nodiscard]]
+const VertexType& find_furthest_vertex(const VertexType& direction) const;
+```
+
+Finds the vertex in the mesh's **local space** that has the maximum dot product with `direction`.
+
+**Algorithm**: Linear search through all vertices (O(n) where n is vertex count).
+
+**Returns**: Const reference to the vertex in `m_vertex_buffer`.
+
+---
+
+### `find_abs_furthest_vertex`
+
+```cpp
+[[nodiscard]]
+VertexType find_abs_furthest_vertex(const VertexType& direction) const;
+```
+
+Finds the vertex farthest along `direction` and transforms it to **world space**. This is the primary method used by GJK/EPA.
+
+**Steps**:
+1. Find furthest vertex in local space using `find_furthest_vertex`
+2. Transform to world space using `mesh.vertex_to_world_space()`
+
+**Returns**: Vertex position in world coordinates.
+
+**Usage in GJK**:
+```cpp
+// GJK support function for Minkowski difference
+VertexType support = collider_a.find_abs_furthest_vertex(direction)
+ - collider_b.find_abs_furthest_vertex(-direction);
+```
+
+---
+
+## Usage Examples
+
+### Basic Collision Detection
+
+```cpp
+using namespace omath::collision;
+using namespace omath::source_engine;
+
+// Create meshes with vertex data
+std::vector> vbo_a = {
+ {-1, -1, -1}, {1, -1, -1}, {1, 1, -1}, {-1, 1, -1},
+ {-1, -1, 1}, {1, -1, 1}, {1, 1, 1}, {-1, 1, 1}
+};
+std::vector> vao_a = /* face indices */;
+
+Mesh mesh_a(vbo_a, vao_a);
+mesh_a.set_origin({0, 0, 0});
+
+Mesh mesh_b(vbo_b, vao_b);
+mesh_b.set_origin({5, 0, 0}); // Positioned away from mesh_a
+
+// Wrap in colliders
+MeshCollider collider_a(std::move(mesh_a));
+MeshCollider collider_b(std::move(mesh_b));
+
+// Run GJK
+auto result = GjkAlgorithm>::check_collision(
+ collider_a, collider_b
+);
+
+if (result.hit) {
+ std::cout << "Collision detected!\n";
+}
+```
+
+### With EPA for Penetration Depth
+
+```cpp
+auto gjk_result = GjkAlgorithm>::check_collision(
+ collider_a, collider_b
+);
+
+if (gjk_result.hit) {
+ auto epa_result = Epa>::solve(
+ collider_a, collider_b, gjk_result.simplex
+ );
+
+ if (epa_result.success) {
+ std::cout << "Penetration: " << epa_result.depth << " units\n";
+ std::cout << "Normal: " << epa_result.normal << "\n";
+ }
+}
+```
+
+### Custom Mesh Creation
+
+```cpp
+// Create a simple box mesh
+std::vector> box_vertices = {
+ {-0.5f, -0.5f, -0.5f}, { 0.5f, -0.5f, -0.5f},
+ { 0.5f, 0.5f, -0.5f}, {-0.5f, 0.5f, -0.5f},
+ {-0.5f, -0.5f, 0.5f}, { 0.5f, -0.5f, 0.5f},
+ { 0.5f, 0.5f, 0.5f}, {-0.5f, 0.5f, 0.5f}
+};
+
+std::vector> box_indices = {
+ {0, 1, 2}, {0, 2, 3}, // Front face
+ {4, 6, 5}, {4, 7, 6}, // Back face
+ {0, 4, 5}, {0, 5, 1}, // Bottom face
+ {2, 6, 7}, {2, 7, 3}, // Top face
+ {0, 3, 7}, {0, 7, 4}, // Left face
+ {1, 5, 6}, {1, 6, 2} // Right face
+};
+
+using namespace omath::source_engine;
+Mesh box_mesh(box_vertices, box_indices);
+box_mesh.set_origin({10, 0, 0});
+box_mesh.set_scale({2, 2, 2});
+
+MeshCollider box_collider(std::move(box_mesh));
+```
+
+### Oriented Collision
+
+```cpp
+// Create rotated mesh
+Mesh mesh(vertices, indices);
+mesh.set_origin({5, 5, 5});
+mesh.set_scale({1, 1, 1});
+
+// Set rotation (engine-specific angles)
+ViewAngles rotation;
+rotation.pitch = PitchAngle::from_degrees(45.0f);
+rotation.yaw = YawAngle::from_degrees(30.0f);
+mesh.set_rotation(rotation);
+
+// Collider automatically handles transformation
+MeshCollider collider(std::move(mesh));
+
+// Support function returns world-space vertices
+auto support = collider.find_abs_furthest_vertex({0, 1, 0});
+```
+
+---
+
+## Performance Considerations
+
+### Linear Search
+
+`find_furthest_vertex` performs a **linear search** through all vertices:
+* **Time complexity**: O(n) per support query
+* **GJK iterations**: ~10-20 support queries per collision test
+* **Total cost**: O(k × n) where k is GJK iterations
+
+For meshes with many vertices (>1000), consider:
+* Using simpler proxy geometry (bounding box, convex hull with fewer vertices)
+* Pre-computing hierarchical structures
+* Using specialized collision shapes when possible
+
+### Caching Opportunities
+
+The implementation uses `std::ranges::max_element`, which is cache-friendly for contiguous vertex buffers. For optimal performance:
+* Store vertices contiguously in memory
+* Avoid pointer-based or scattered vertex storage
+* Consider SoA (Structure of Arrays) layout for SIMD optimization
+
+### World Space Transformation
+
+The `vertex_to_world_space` call involves matrix multiplication:
+* **Cost**: ~15-20 floating-point operations per vertex
+* **Optimization**: The mesh caches its transformation matrix
+* **Update cost**: Only recomputed when origin/rotation/scale changes
+
+---
+
+## Limitations & Edge Cases
+
+### Convex Hull Requirement
+
+**Critical**: GJK/EPA only work with **convex shapes**. If your mesh is concave:
+
+#### Option 1: Convex Decomposition
+```cpp
+// Decompose concave mesh into convex parts
+std::vector convex_parts = decompose_mesh(concave_mesh);
+
+for (const auto& part : convex_parts) {
+ MeshCollider collider(part);
+ // Test each part separately
+}
+```
+
+#### Option 2: Use Convex Hull
+```cpp
+// Compute convex hull of vertices
+auto hull_vertices = compute_convex_hull(mesh.m_vertex_buffer);
+Mesh hull_mesh(hull_vertices, hull_indices);
+MeshCollider collider(std::move(hull_mesh));
+```
+
+#### Option 3: Different Algorithm
+Use triangle-based collision (e.g., LineTracer) for true concave support.
+
+### Empty Mesh
+
+Behavior is undefined if `m_vertex_buffer` is empty. Always ensure:
+```cpp
+assert(!mesh.m_vertex_buffer.empty());
+MeshCollider collider(std::move(mesh));
+```
+
+### Degenerate Meshes
+
+* **Single vertex**: Treated as a point (degenerates to sphere collision)
+* **Two vertices**: Line segment (may cause GJK issues)
+* **Coplanar vertices**: Flat mesh; EPA may have convergence issues
+
+**Recommendation**: Use at least 4 non-coplanar vertices for robustness.
+
+---
+
+## Coordinate Systems
+
+`MeshCollider` supports different engine coordinate systems through the `MeshTrait`:
+
+| Engine | Up Axis | Handedness | Rotation Order |
+|--------|---------|------------|----------------|
+| Source Engine | Z | Right-handed | Pitch/Yaw/Roll |
+| Unity | Y | Left-handed | Pitch/Yaw/Roll |
+| Unreal | Z | Left-handed | Roll/Pitch/Yaw |
+| Frostbite | Y | Right-handed | Pitch/Yaw/Roll |
+| IW Engine | Z | Right-handed | Pitch/Yaw/Roll |
+| OpenGL | Y | Right-handed | Pitch/Yaw/Roll |
+
+The `vertex_to_world_space` method handles these differences transparently.
+
+---
+
+## Advanced Usage
+
+### Custom Support Function
+
+For specialized collision shapes, implement a custom collider:
+
+```cpp
+class SphereCollider {
+public:
+ using VertexType = Vector3;
+
+ Vector3 center;
+ float radius;
+
+ VertexType find_abs_furthest_vertex(const VertexType& direction) const {
+ auto normalized = direction.normalized();
+ return center + normalized * radius;
+ }
+};
+
+// Use with GJK/EPA
+auto result = GjkAlgorithm::check_collision(sphere_a, sphere_b);
+```
+
+### Debugging Support Queries
+
+```cpp
+class DebugMeshCollider : public MeshCollider {
+public:
+ using MeshCollider::MeshCollider;
+
+ VertexType find_abs_furthest_vertex(const VertexType& direction) const {
+ auto result = MeshCollider::find_abs_furthest_vertex(direction);
+ std::cout << "Support query: direction=" << direction
+ << " -> vertex=" << result << "\n";
+ return result;
+ }
+};
+```
+
+---
+
+## See Also
+
+- [GJK Algorithm Documentation](gjk_algorithm.md) - Uses `MeshCollider` for collision detection
+- [EPA Algorithm Documentation](epa_algorithm.md) - Uses `MeshCollider` for penetration depth
+- [Simplex Documentation](simplex.md) - Data structure used by GJK
+- [Mesh Documentation](../3d_primitives/mesh.md) - Underlying mesh primitive
+- [Tutorials - Collision Detection](../tutorials.md#tutorial-4-collision-detection) - Complete collision tutorial
+
+---
+
+*Last updated: 13 Nov 2025*
diff --git a/docs/collision/simplex.md b/docs/collision/simplex.md
new file mode 100644
index 00000000..c786e784
--- /dev/null
+++ b/docs/collision/simplex.md
@@ -0,0 +1,327 @@
+# `omath::collision::Simplex` — Fixed-capacity simplex for GJK/EPA
+
+> Header: `omath/collision/simplex.hpp`
+> Namespace: `omath::collision`
+> Depends on: `Vector3` (or any type satisfying `GjkVector` concept)
+> Purpose: store and manipulate simplices in GJK and EPA algorithms
+
+---
+
+## Overview
+
+`Simplex` is a lightweight container for up to 4 points, used internally by the GJK and EPA collision detection algorithms. A simplex in this context is a geometric shape defined by 1 to 4 vertices:
+
+* **1 point** — a single vertex
+* **2 points** — a line segment
+* **3 points** — a triangle
+* **4 points** — a tetrahedron
+
+The GJK algorithm builds simplices incrementally to detect collisions, and EPA extends a 4-point simplex to compute penetration depth.
+
+---
+
+## Template & Concepts
+
+```cpp
+template>
+class Simplex final;
+```
+
+### `GjkVector` Concept
+
+The vertex type must satisfy:
+
+```cpp
+template
+concept GjkVector = requires(const V& a, const V& b) {
+ { -a } -> std::same_as;
+ { a - b } -> std::same_as;
+ { a.cross(b) } -> std::same_as;
+ { a.point_to_same_direction(b) } -> std::same_as;
+};
+```
+
+`omath::Vector3` satisfies this concept and is the default.
+
+---
+
+## Constructors & Assignment
+
+```cpp
+constexpr Simplex() = default;
+
+constexpr Simplex& operator=(std::initializer_list list) noexcept;
+```
+
+### Initialization
+
+```cpp
+// Empty simplex
+Simplex> s;
+
+// Initialize with points
+Simplex> s2;
+s2 = {v1, v2, v3}; // 3-point simplex (triangle)
+```
+
+**Constraint**: Maximum 4 points. Passing more triggers an assertion in debug builds.
+
+---
+
+## Core Methods
+
+### Adding Points
+
+```cpp
+constexpr void push_front(const VectorType& p) noexcept;
+```
+
+Inserts a point at the **front** (index 0), shifting existing points back. If the simplex is already at capacity (4 points), the last point is discarded.
+
+**Usage pattern in GJK**:
+```cpp
+simplex.push_front(new_support_point);
+// Now simplex[0] is the newest point
+```
+
+### Size & Capacity
+
+```cpp
+[[nodiscard]] constexpr std::size_t size() const noexcept;
+[[nodiscard]] static constexpr std::size_t capacity = 4;
+```
+
+* `size()` — current number of points (0-4)
+* `capacity` — maximum points (always 4)
+
+### Element Access
+
+```cpp
+[[nodiscard]] constexpr VectorType& operator[](std::size_t index) noexcept;
+[[nodiscard]] constexpr const VectorType& operator[](std::size_t index) const noexcept;
+```
+
+Access points by index. **No bounds checking** — index must be `< size()`.
+
+```cpp
+if (simplex.size() >= 2) {
+ auto edge = simplex[1] - simplex[0];
+}
+```
+
+### Iterators
+
+```cpp
+[[nodiscard]] constexpr auto begin() noexcept;
+[[nodiscard]] constexpr auto end() noexcept;
+[[nodiscard]] constexpr auto begin() const noexcept;
+[[nodiscard]] constexpr auto end() const noexcept;
+```
+
+Standard iterator support for range-based loops:
+
+```cpp
+for (const auto& vertex : simplex) {
+ std::cout << vertex << "\n";
+}
+```
+
+---
+
+## GJK-Specific Methods
+
+These methods implement the core logic for simplifying simplices in the GJK algorithm.
+
+### `contains_origin`
+
+```cpp
+[[nodiscard]] constexpr bool contains_origin() noexcept;
+```
+
+Determines if the origin lies within the current simplex. This is the **core GJK test**: if true, the shapes intersect.
+
+* For a **1-point** simplex, always returns `false` (can't contain origin)
+* For a **2-point** simplex (line), checks if origin projects onto the segment
+* For a **3-point** simplex (triangle), checks if origin projects onto the triangle
+* For a **4-point** simplex (tetrahedron), checks if origin is inside
+
+**Side effect**: Simplifies the simplex by removing points not needed to maintain proximity to the origin. After calling, `size()` may have decreased.
+
+**Return value**:
+* `true` — origin is contained (collision detected)
+* `false` — origin not contained; simplex has been simplified toward origin
+
+### `next_direction`
+
+```cpp
+[[nodiscard]] constexpr VectorType next_direction() const noexcept;
+```
+
+Computes the next search direction for GJK. This is the direction from the simplex toward the origin, used to query the next support point.
+
+* Must be called **after** `contains_origin()` returns `false`
+* Behavior is **undefined** if called when `size() == 0` or when origin is already contained
+
+---
+
+## Usage Examples
+
+### GJK Iteration (Simplified)
+
+```cpp
+Simplex> simplex;
+Vector3 direction{1, 0, 0}; // Initial search direction
+
+for (int i = 0; i < 64; ++i) {
+ // Get support point in current direction
+ auto support = find_support_vertex(collider_a, collider_b, direction);
+
+ // Check if we made progress
+ if (support.dot(direction) <= 0)
+ break; // No collision possible
+
+ simplex.push_front(support);
+
+ // Check if simplex contains origin
+ if (simplex.contains_origin()) {
+ // Collision detected!
+ assert(simplex.size() == 4);
+ return GjkHitInfo{true, simplex};
+ }
+
+ // Get next search direction
+ direction = simplex.next_direction();
+}
+
+// No collision
+return GjkHitInfo{false, {}};
+```
+
+### Manual Simplex Construction
+
+```cpp
+using Vec3 = Vector3;
+
+Simplex simplex;
+simplex = {
+ Vec3{0.0f, 0.0f, 0.0f},
+ Vec3{1.0f, 0.0f, 0.0f},
+ Vec3{0.0f, 1.0f, 0.0f},
+ Vec3{0.0f, 0.0f, 1.0f}
+};
+
+assert(simplex.size() == 4);
+
+// Check if origin is inside this tetrahedron
+bool has_collision = simplex.contains_origin();
+```
+
+### Iterating Over Points
+
+```cpp
+void print_simplex(const Simplex>& s) {
+ std::cout << "Simplex with " << s.size() << " points:\n";
+ for (std::size_t i = 0; i < s.size(); ++i) {
+ const auto& p = s[i];
+ std::cout << " [" << i << "] = ("
+ << p.x << ", " << p.y << ", " << p.z << ")\n";
+ }
+}
+```
+
+---
+
+## Implementation Details
+
+### Simplex Simplification
+
+The `contains_origin()` method implements different tests based on simplex size:
+
+#### Line Segment (2 points)
+
+Checks if origin projects onto segment `[A, B]`:
+* If yes, keeps both points
+* If no, keeps only the closer point
+
+#### Triangle (3 points)
+
+Tests the origin against the triangle plane and edges using cross products. Simplifies to:
+* The full triangle if origin projects onto its surface
+* An edge if origin is closest to that edge
+* A single vertex otherwise
+
+#### Tetrahedron (4 points)
+
+Tests origin against all four faces:
+* If origin is inside, returns `true` (collision)
+* If outside, reduces to the face/edge/vertex closest to origin
+
+### Direction Calculation
+
+The `next_direction()` method computes:
+* For **line**: perpendicular from line toward origin
+* For **triangle**: perpendicular from triangle toward origin
+* Implementation uses cross products and projections to avoid sqrt when possible
+
+---
+
+## Performance Characteristics
+
+* **Storage**: Fixed 4 × `sizeof(VectorType)` + size counter
+* **Push front**: O(n) where n is current size (max 4, so effectively O(1))
+* **Contains origin**: O(1) for each case (line, triangle, tetrahedron)
+* **Next direction**: O(1) — simple cross products and subtractions
+* **No heap allocations**: All storage is inline
+
+**constexpr**: All methods are `constexpr`, enabling compile-time usage where feasible.
+
+---
+
+## Edge Cases & Constraints
+
+### Degenerate Simplices
+
+* **Zero-length edges**: Can occur if support points coincide. The algorithm handles this by checking `point_to_same_direction` before divisions.
+* **Collinear points**: Triangle simplification detects and handles collinear cases by reducing to a line.
+* **Flat tetrahedron**: If the 4th point is coplanar with the first 3, the origin containment test may have reduced precision.
+
+### Assertions
+
+* **Capacity**: `operator=` asserts `list.size() <= 4` in debug builds
+* **Index bounds**: No bounds checking in release builds — ensure `index < size()`
+
+### Thread Safety
+
+* **Read-only**: Safe to read from multiple threads
+* **Modification**: Not thread-safe; synchronize writes externally
+
+---
+
+## Relationship to GJK & EPA
+
+### In GJK
+
+* Starts empty or with an initial point
+* Grows via `push_front` as support points are added
+* Shrinks via `contains_origin` as it's simplified
+* Once it reaches 4 points and contains origin, GJK succeeds
+
+### In EPA
+
+* Takes a 4-point simplex from GJK as input
+* Uses the tetrahedron as the initial polytope
+* Does not directly use the `Simplex` class for expansion (EPA maintains a more complex polytope structure)
+
+---
+
+## See Also
+
+- [GJK Algorithm Documentation](gjk_algorithm.md) - Uses `Simplex` for collision detection
+- [EPA Algorithm Documentation](epa_algorithm.md) - Takes 4-point `Simplex` as input
+- [MeshCollider Documentation](mesh_collider.md) - Provides support function for GJK/EPA
+- [Vector3 Documentation](../linear_algebra/vector3.md) - Default vertex type
+- [Tutorials - Collision Detection](../tutorials.md#tutorial-4-collision-detection) - Collision tutorial
+
+---
+
+*Last updated: 13 Nov 2025*
diff --git a/docs/containers/encrypted_variable.md b/docs/containers/encrypted_variable.md
new file mode 100644
index 00000000..b9bfe7c9
--- /dev/null
+++ b/docs/containers/encrypted_variable.md
@@ -0,0 +1,115 @@
+# `omath::EncryptedVariable` — Compile-time XOR-encrypted variable
+
+> Header: `omath/containers/encrypted_variable.hpp`
+> Namespace: `omath`
+> Depends on: ``, ``, ``, ``
+
+`EncryptedVariable` keeps a value XOR-encrypted in memory at rest, using a **compile-time generated random key**. It is designed to hinder static analysis and memory scanners from reading sensitive values (e.g., game constants, keys, thresholds) directly from process memory.
+
+---
+
+## Key concepts
+
+* **Compile-time key generation** — a unique random byte array is produced at compile time via SplitMix64 + FNV-1a seeded from `__FILE__`, `__DATE__`, and `__TIME__`. Each `OMATH_DEF_CRYPT_VAR` expansion receives a distinct key.
+* **XOR cipher** — `encrypt()` / `decrypt()` toggle the encrypted state by XOR-ing the raw bytes of the stored value with the key.
+* **VarAnchor (RAII guard)** — `drop_anchor()` returns a `VarAnchor` that decrypts on construction and re-encrypts on destruction, ensuring the plaintext window is as short as possible.
+
+---
+
+## API
+
+```cpp
+namespace omath {
+
+template key>
+class EncryptedVariable final {
+public:
+ using value_type = std::remove_cvref_t;
+
+ constexpr explicit EncryptedVariable(const value_type& data);
+
+ [[nodiscard]] constexpr bool is_encrypted() const;
+
+ constexpr void decrypt();
+ constexpr void encrypt();
+
+ [[nodiscard]] constexpr value_type& value();
+ [[nodiscard]] constexpr const value_type& value() const;
+
+ [[nodiscard]] constexpr auto drop_anchor(); // returns VarAnchor
+
+ constexpr ~EncryptedVariable(); // decrypts on destruction
+};
+
+template
+class VarAnchor final {
+public:
+ constexpr VarAnchor(EncryptedVarType& var); // decrypts
+ constexpr ~VarAnchor(); // re-encrypts
+};
+
+} // namespace omath
+```
+
+### Helper macros
+
+```cpp
+// Generate a compile-time random byte array of length N
+#define OMATH_CT_RAND_ARRAY_BYTE(N) /* ... */
+
+// Declare a type alias for EncryptedVariable with KEY_SIZE random bytes
+#define OMATH_DEF_CRYPT_VAR(TYPE, KEY_SIZE) /* ... */
+```
+
+---
+
+## Usage examples
+
+### Basic encrypt / decrypt
+
+```cpp
+#include "omath/containers/encrypted_variable.hpp"
+
+// Define an encrypted float with a 16-byte key
+using EncFloat = OMATH_DEF_CRYPT_VAR(float, 16);
+
+EncFloat secret(3.14f); // encrypted immediately
+// secret.value() is XOR-scrambled in memory
+
+secret.decrypt();
+float v = secret.value(); // v == 3.14f
+secret.encrypt(); // scrambled again
+```
+
+### RAII guard with `drop_anchor()`
+
+```cpp
+EncFloat secret(42.0f);
+
+{
+ auto anchor = secret.drop_anchor(); // decrypts
+ float val = secret.value(); // safe to read
+ // ... use val ...
+} // anchor destroyed → re-encrypts automatically
+```
+
+---
+
+## Notes & edge cases
+
+* **Force-inline**: When `OMATH_ENABLE_FORCE_INLINE` is defined, encrypt/decrypt operations use compiler-specific force-inline attributes to reduce the call-site footprint visible in disassembly.
+* **Not cryptographically secure**: XOR with a static key is an obfuscation technique, not encryption. It raises the bar for casual memory scanning but does not resist a determined attacker who can read the binary.
+* **Destructor decrypts**: The destructor calls `decrypt()` so the value is in plaintext at the end of its lifetime (e.g., for logging or cleanup).
+* **Thread safety**: No internal synchronization. Protect concurrent access with external locks.
+* **`constexpr` support**: All operations are `constexpr`-friendly (C++20).
+
+---
+
+## See also
+
+* [Pattern Scan](../utility/pattern_scan.md) — scan memory for byte patterns.
+* [Getting Started](../getting_started.md) — quick start with OMath.
+
+---
+
+*Last updated: Feb 2026*
diff --git a/docs/engines/frostbite/camera_trait.md b/docs/engines/frostbite/camera_trait.md
new file mode 100644
index 00000000..bb03cd02
--- /dev/null
+++ b/docs/engines/frostbite/camera_trait.md
@@ -0,0 +1,108 @@
+# `omath::frostbite_engine::CameraTrait` — plug-in trait for `projection::Camera`
+
+> Header: `omath/engines/frostbite_engine/traits/camera_trait.hpp` • Impl: `omath/engines/frostbite_engine/traits/camera_trait.cpp`
+> Namespace: `omath::frostbite_engine`
+> Purpose: provide Frostbite-style **look-at**, **view**, and **projection** math to the generic `omath::projection::Camera` (satisfies `CameraEngineConcept`).
+
+---
+
+## Summary
+
+`CameraTrait` exposes three `static` functions:
+
+* `calc_look_at_angle(origin, look_at)` – computes Euler angles so the camera at `origin` looks at `look_at`. Implementation normalizes the direction, computes **pitch** as `-asin(dir.y)` and **yaw** as `atan2(dir.x, dir.z)`; **roll** is `0`. Pitch/yaw are returned using the project’s strong angle types (`PitchAngle`, `YawAngle`, `RollAngle`).
+* `calc_view_matrix(angles, origin)` – delegates to Frostbite formulas `frostbite_engine::calc_view_matrix`, producing a `Mat4X4` view matrix for the given angles and origin.
+* `calc_projection_matrix(fov, viewport, near, far)` – builds a perspective projection by calling `calc_perspective_projection_matrix(fov_degrees, aspect, near, far)`, where `aspect = viewport.aspect_ratio()`. Accepts `FieldOfView` (degrees).
+
+The trait’s types (`ViewAngles`, `Mat4X4`, angle aliases) and helpers live in the Frostbite engine math headers included by the trait (`formulas.hpp`) and the shared projection header (`projection/camera.hpp`).
+
+---
+
+## API
+
+```cpp
+namespace omath::frostbite_engine {
+
+class CameraTrait final {
+public:
+ // Compute Euler angles (pitch/yaw/roll) to look from cam_origin to look_at.
+ static ViewAngles
+ calc_look_at_angle(const Vector3& cam_origin,
+ const Vector3& look_at) noexcept;
+
+ // Build view matrix for given angles and origin.
+ static Mat4X4
+ calc_view_matrix(const ViewAngles& angles,
+ const Vector3& cam_origin) noexcept;
+
+ // Build perspective projection from FOV (deg), viewport, near/far.
+ static Mat4X4
+ calc_projection_matrix(const projection::FieldOfView& fov,
+ const projection::ViewPort& view_port,
+ float near, float far) noexcept;
+};
+
+} // namespace omath::frostbite_engine
+```
+
+Uses: `Vector3