mirror of
https://github.com/orange-cpp/omath.git
synced 2026-02-12 22:53:27 +00:00
Add documentation for collision detection and mesh classes
Co-authored-by: orange-cpp <59374393+orange-cpp@users.noreply.github.com>
This commit is contained in:
465
docs/3d_primitives/mesh.md
Normal file
465
docs/3d_primitives/mesh.md
Normal file
@@ -0,0 +1,465 @@
|
|||||||
|
# `omath::primitives::Mesh` — 3D mesh with transformation support
|
||||||
|
|
||||||
|
> Header: `omath/3d_primitives/mesh.hpp`
|
||||||
|
> Namespace: `omath::primitives`
|
||||||
|
> Depends on: `omath::Vector3<T>`, `omath::Mat4X4`, `omath::Triangle<Vector3<T>>`
|
||||||
|
> Purpose: represent and transform 3D meshes in different engine coordinate systems
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
`Mesh` represents a 3D polygonal mesh with vertex data and transformation capabilities. It stores:
|
||||||
|
* **Vertex buffer (VBO)** — array of 3D vertex positions
|
||||||
|
* **Index buffer (VAO)** — array of triangular faces (indices into VBO)
|
||||||
|
* **Transformation** — position, rotation, and scale with caching
|
||||||
|
|
||||||
|
The mesh supports transformation from local space to world space using engine-specific coordinate systems through the `MeshTrait` template parameter.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Template Declaration
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
template<class Mat4X4, class RotationAngles, class MeshTypeTrait, class Type = float>
|
||||||
|
class Mesh final;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Template Parameters
|
||||||
|
|
||||||
|
* `Mat4X4` — Matrix type for transformations (typically `omath::Mat4X4`)
|
||||||
|
* `RotationAngles` — Rotation representation (e.g., `ViewAngles` with pitch/yaw/roll)
|
||||||
|
* `MeshTypeTrait` — Engine-specific transformation trait (see [Engine Traits](#engine-traits))
|
||||||
|
* `Type` — Scalar type for vertex coordinates (default `float`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Type Aliases
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
using NumericType = Type;
|
||||||
|
```
|
||||||
|
|
||||||
|
Common engine-specific aliases:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Source Engine
|
||||||
|
using Mesh = omath::primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float>;
|
||||||
|
|
||||||
|
// Unity Engine
|
||||||
|
using Mesh = omath::primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float>;
|
||||||
|
|
||||||
|
// Unreal Engine
|
||||||
|
using Mesh = omath::primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float>;
|
||||||
|
|
||||||
|
// Frostbite, IW Engine, OpenGL similar...
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the pre-defined type aliases in engine namespaces:
|
||||||
|
```cpp
|
||||||
|
using namespace omath::source_engine;
|
||||||
|
Mesh my_mesh = /* ... */; // Uses SourceEngine::Mesh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Members
|
||||||
|
|
||||||
|
### Vertex Data
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
std::vector<Vector3<NumericType>> m_vertex_buffer; // VBO: vertex positions
|
||||||
|
std::vector<Vector3<std::size_t>> m_vertex_array_object; // VAO: face indices
|
||||||
|
```
|
||||||
|
|
||||||
|
* `m_vertex_buffer` — array of vertex positions in **local space**
|
||||||
|
* `m_vertex_array_object` — array of triangular faces, each containing 3 indices into `m_vertex_buffer`
|
||||||
|
|
||||||
|
**Public access**: These members are public for direct manipulation when needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Constructor
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
Mesh(std::vector<Vector3<NumericType>> vbo,
|
||||||
|
std::vector<Vector3<std::size_t>> vao,
|
||||||
|
Vector3<NumericType> scale = {1, 1, 1});
|
||||||
|
```
|
||||||
|
|
||||||
|
Creates a mesh from vertex and index data.
|
||||||
|
|
||||||
|
**Parameters**:
|
||||||
|
* `vbo` — vertex buffer (moved into mesh)
|
||||||
|
* `vao` — index buffer / vertex array object (moved into mesh)
|
||||||
|
* `scale` — initial scale (default `{1, 1, 1}`)
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```cpp
|
||||||
|
std::vector<Vector3<float>> vertices = {
|
||||||
|
{0, 0, 0}, {1, 0, 0}, {0, 1, 0}, {0, 0, 1}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<Vector3<std::size_t>> faces = {
|
||||||
|
{0, 1, 2}, // Triangle 1
|
||||||
|
{0, 1, 3}, // Triangle 2
|
||||||
|
{0, 2, 3}, // Triangle 3
|
||||||
|
{1, 2, 3} // Triangle 4
|
||||||
|
};
|
||||||
|
|
||||||
|
using namespace omath::source_engine;
|
||||||
|
Mesh tetrahedron(std::move(vertices), std::move(faces));
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Transformation Methods
|
||||||
|
|
||||||
|
### Setting Transform Components
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void set_origin(const Vector3<NumericType>& new_origin);
|
||||||
|
void set_scale(const Vector3<NumericType>& new_scale);
|
||||||
|
void set_rotation(const RotationAngles& new_rotation_angles);
|
||||||
|
```
|
||||||
|
|
||||||
|
Update the mesh's transformation. **Side effect**: invalidates the cached transformation matrix, which will be recomputed on the next `get_to_world_matrix()` call.
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```cpp
|
||||||
|
mesh.set_origin({10, 0, 5});
|
||||||
|
mesh.set_scale({2, 2, 2});
|
||||||
|
|
||||||
|
ViewAngles angles;
|
||||||
|
angles.pitch = PitchAngle::from_degrees(45.0f);
|
||||||
|
angles.yaw = YawAngle::from_degrees(30.0f);
|
||||||
|
mesh.set_rotation(angles);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Getting Transform Components
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
[[nodiscard]] const Vector3<NumericType>& get_origin() const;
|
||||||
|
[[nodiscard]] const Vector3<NumericType>& get_scale() const;
|
||||||
|
[[nodiscard]] const RotationAngles& get_rotation_angles() const;
|
||||||
|
```
|
||||||
|
|
||||||
|
Retrieve current transformation components.
|
||||||
|
|
||||||
|
### Transformation Matrix
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
[[nodiscard]] const Mat4X4& get_to_world_matrix() const;
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns the cached local-to-world transformation matrix. The matrix is computed lazily on first access after any transformation change:
|
||||||
|
|
||||||
|
```
|
||||||
|
M = Translation(origin) × Scale(scale) × Rotation(angles)
|
||||||
|
```
|
||||||
|
|
||||||
|
The rotation matrix is computed using the engine-specific `MeshTrait::rotation_matrix()` method.
|
||||||
|
|
||||||
|
**Caching**: The matrix is stored in a `mutable std::optional` and recomputed only when invalidated by `set_*` methods.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Vertex Transformation
|
||||||
|
|
||||||
|
### `vertex_to_world_space`
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
[[nodiscard]]
|
||||||
|
Vector3<float> vertex_to_world_space(const Vector3<float>& vertex) const;
|
||||||
|
```
|
||||||
|
|
||||||
|
Transforms a vertex from local space to world space by multiplying with the transformation matrix.
|
||||||
|
|
||||||
|
**Algorithm**:
|
||||||
|
1. Convert vertex to column matrix: `[x, y, z, 1]ᵀ`
|
||||||
|
2. Multiply by transformation matrix: `M × vertex`
|
||||||
|
3. Extract the resulting 3D position
|
||||||
|
|
||||||
|
**Usage**:
|
||||||
|
```cpp
|
||||||
|
Vector3<float> local_vertex{1, 0, 0};
|
||||||
|
Vector3<float> world_vertex = mesh.vertex_to_world_space(local_vertex);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: This is used internally by `MeshCollider` to provide world-space support functions for GJK/EPA.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Face Transformation
|
||||||
|
|
||||||
|
### `make_face_in_world_space`
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
[[nodiscard]]
|
||||||
|
Triangle<Vector3<float>> make_face_in_world_space(
|
||||||
|
const std::vector<Vector3<std::size_t>>::const_iterator vao_iterator
|
||||||
|
) const;
|
||||||
|
```
|
||||||
|
|
||||||
|
Creates a triangle in world space from a face index iterator.
|
||||||
|
|
||||||
|
**Parameters**:
|
||||||
|
* `vao_iterator` — iterator to an element in `m_vertex_array_object`
|
||||||
|
|
||||||
|
**Returns**: `Triangle` with all three vertices transformed to world space.
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```cpp
|
||||||
|
for (auto it = mesh.m_vertex_array_object.begin();
|
||||||
|
it != mesh.m_vertex_array_object.end();
|
||||||
|
++it) {
|
||||||
|
Triangle<Vector3<float>> world_triangle = mesh.make_face_in_world_space(it);
|
||||||
|
// Render or process the triangle
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Creating a Box Mesh
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
using namespace omath::source_engine;
|
||||||
|
|
||||||
|
std::vector<Vector3<float>> box_vbo = {
|
||||||
|
// Bottom face
|
||||||
|
{-0.5f, -0.5f, 0.0f}, { 0.5f, -0.5f, 0.0f},
|
||||||
|
{ 0.5f, 0.5f, 0.0f}, {-0.5f, 0.5f, 0.0f},
|
||||||
|
// Top face
|
||||||
|
{-0.5f, -0.5f, 1.0f}, { 0.5f, -0.5f, 1.0f},
|
||||||
|
{ 0.5f, 0.5f, 1.0f}, {-0.5f, 0.5f, 1.0f}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<Vector3<std::size_t>> box_vao = {
|
||||||
|
// Bottom
|
||||||
|
{0, 1, 2}, {0, 2, 3},
|
||||||
|
// Top
|
||||||
|
{4, 6, 5}, {4, 7, 6},
|
||||||
|
// Sides
|
||||||
|
{0, 4, 5}, {0, 5, 1},
|
||||||
|
{1, 5, 6}, {1, 6, 2},
|
||||||
|
{2, 6, 7}, {2, 7, 3},
|
||||||
|
{3, 7, 4}, {3, 4, 0}
|
||||||
|
};
|
||||||
|
|
||||||
|
Mesh box(std::move(box_vbo), std::move(box_vao));
|
||||||
|
box.set_origin({0, 0, 50});
|
||||||
|
box.set_scale({10, 10, 10});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transforming Mesh Over Time
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void update_mesh(Mesh& mesh, float delta_time) {
|
||||||
|
// Rotate mesh
|
||||||
|
auto rotation = mesh.get_rotation_angles();
|
||||||
|
rotation.yaw = YawAngle::from_degrees(
|
||||||
|
rotation.yaw.as_degrees() + 45.0f * delta_time
|
||||||
|
);
|
||||||
|
mesh.set_rotation(rotation);
|
||||||
|
|
||||||
|
// Oscillate position
|
||||||
|
auto origin = mesh.get_origin();
|
||||||
|
origin.z = 50.0f + 10.0f * std::sin(current_time * 2.0f);
|
||||||
|
mesh.set_origin(origin);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Collision Detection
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
using namespace omath::collision;
|
||||||
|
using namespace omath::source_engine;
|
||||||
|
|
||||||
|
Mesh mesh_a(vbo_a, vao_a);
|
||||||
|
mesh_a.set_origin({0, 0, 0});
|
||||||
|
|
||||||
|
Mesh mesh_b(vbo_b, vao_b);
|
||||||
|
mesh_b.set_origin({5, 0, 0});
|
||||||
|
|
||||||
|
MeshCollider collider_a(std::move(mesh_a));
|
||||||
|
MeshCollider collider_b(std::move(mesh_b));
|
||||||
|
|
||||||
|
auto result = GjkAlgorithm<MeshCollider<Mesh>>::check_collision(
|
||||||
|
collider_a, collider_b
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rendering Transformed Triangles
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void render_mesh(const Mesh& mesh) {
|
||||||
|
for (auto it = mesh.m_vertex_array_object.begin();
|
||||||
|
it != mesh.m_vertex_array_object.end();
|
||||||
|
++it) {
|
||||||
|
|
||||||
|
Triangle<Vector3<float>> tri = mesh.make_face_in_world_space(it);
|
||||||
|
|
||||||
|
// Draw triangle with your renderer
|
||||||
|
draw_triangle(tri.m_vertex1, tri.m_vertex2, tri.m_vertex3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Engine Traits
|
||||||
|
|
||||||
|
Each game engine has a corresponding `MeshTrait` that provides the `rotation_matrix` function:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class MeshTrait final {
|
||||||
|
public:
|
||||||
|
[[nodiscard]]
|
||||||
|
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Available Engines
|
||||||
|
|
||||||
|
| Engine | Namespace | Header |
|
||||||
|
|--------|-----------|--------|
|
||||||
|
| Source Engine | `omath::source_engine` | `engines/source_engine/mesh.hpp` |
|
||||||
|
| Unity | `omath::unity_engine` | `engines/unity_engine/mesh.hpp` |
|
||||||
|
| Unreal | `omath::unreal_engine` | `engines/unreal_engine/mesh.hpp` |
|
||||||
|
| Frostbite | `omath::frostbite_engine` | `engines/frostbite_engine/mesh.hpp` |
|
||||||
|
| IW Engine | `omath::iw_engine` | `engines/iw_engine/mesh.hpp` |
|
||||||
|
| OpenGL | `omath::opengl_engine` | `engines/opengl_engine/mesh.hpp` |
|
||||||
|
|
||||||
|
**Example** (Source Engine):
|
||||||
|
```cpp
|
||||||
|
using namespace omath::source_engine;
|
||||||
|
|
||||||
|
// Uses source_engine::MeshTrait automatically
|
||||||
|
Mesh my_mesh(vertices, indices);
|
||||||
|
```
|
||||||
|
|
||||||
|
See [MeshTrait Documentation](#mesh-trait-documentation) for engine-specific details.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
### Matrix Caching
|
||||||
|
|
||||||
|
The transformation matrix is computed lazily and cached:
|
||||||
|
* **First access**: O(matrix multiply) ≈ 64 float operations
|
||||||
|
* **Subsequent access**: O(1) — returns cached matrix
|
||||||
|
* **Cache invalidation**: Any `set_*` call invalidates the cache
|
||||||
|
|
||||||
|
**Best practice**: Batch transformation updates before accessing the matrix:
|
||||||
|
```cpp
|
||||||
|
// Good: single matrix recomputation
|
||||||
|
mesh.set_origin(new_origin);
|
||||||
|
mesh.set_rotation(new_rotation);
|
||||||
|
mesh.set_scale(new_scale);
|
||||||
|
auto matrix = mesh.get_to_world_matrix(); // Computes once
|
||||||
|
|
||||||
|
// Bad: three matrix recomputations
|
||||||
|
mesh.set_origin(new_origin);
|
||||||
|
auto m1 = mesh.get_to_world_matrix(); // Compute
|
||||||
|
mesh.set_rotation(new_rotation);
|
||||||
|
auto m2 = mesh.get_to_world_matrix(); // Compute again
|
||||||
|
mesh.set_scale(new_scale);
|
||||||
|
auto m3 = mesh.get_to_world_matrix(); // Compute again
|
||||||
|
```
|
||||||
|
|
||||||
|
### Memory Layout
|
||||||
|
|
||||||
|
* **VBO**: Contiguous `std::vector` for cache-friendly access
|
||||||
|
* **VAO**: Contiguous indices for cache-friendly face iteration
|
||||||
|
* **Matrix**: Cached in `std::optional` (no allocation)
|
||||||
|
|
||||||
|
### Transformation Cost
|
||||||
|
|
||||||
|
* `vertex_to_world_space`: ~15-20 FLOPs per vertex (4×4 matrix multiply)
|
||||||
|
* `make_face_in_world_space`: ~60 FLOPs (3 vertices)
|
||||||
|
|
||||||
|
For high-frequency transformations, consider:
|
||||||
|
* Caching transformed vertices if the mesh doesn't change
|
||||||
|
* Using simpler proxy geometry for collision
|
||||||
|
* Batching transformations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Coordinate System Details
|
||||||
|
|
||||||
|
Different engines use different coordinate systems:
|
||||||
|
|
||||||
|
| Engine | Up Axis | Forward Axis | Handedness |
|
||||||
|
|--------|---------|--------------|------------|
|
||||||
|
| Source | +Z | +Y | Right |
|
||||||
|
| Unity | +Y | +Z | Left |
|
||||||
|
| Unreal | +Z | +X | Left |
|
||||||
|
| Frostbite | +Y | +Z | Right |
|
||||||
|
| IW Engine | +Z | +Y | Right |
|
||||||
|
| OpenGL | +Y | +Z | Right |
|
||||||
|
|
||||||
|
The `MeshTrait::rotation_matrix` function accounts for these differences, ensuring correct transformations in each engine's space.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Limitations & Edge Cases
|
||||||
|
|
||||||
|
### Empty Mesh
|
||||||
|
|
||||||
|
A mesh with no vertices or faces is valid but not useful:
|
||||||
|
```cpp
|
||||||
|
Mesh empty_mesh({}, {}); // Valid but meaningless
|
||||||
|
```
|
||||||
|
|
||||||
|
For collision detection, ensure `m_vertex_buffer` is non-empty.
|
||||||
|
|
||||||
|
### Index Validity
|
||||||
|
|
||||||
|
No bounds checking is performed on indices in `m_vertex_array_object`. Ensure all indices are valid:
|
||||||
|
```cpp
|
||||||
|
assert(face.x < mesh.m_vertex_buffer.size());
|
||||||
|
assert(face.y < mesh.m_vertex_buffer.size());
|
||||||
|
assert(face.z < mesh.m_vertex_buffer.size());
|
||||||
|
```
|
||||||
|
|
||||||
|
### Degenerate Triangles
|
||||||
|
|
||||||
|
Faces with duplicate indices or collinear vertices will produce degenerate triangles. The mesh doesn't validate this; users must ensure clean geometry.
|
||||||
|
|
||||||
|
### Thread Safety
|
||||||
|
|
||||||
|
* **Read-only**: Safe to read from multiple threads (including const methods)
|
||||||
|
* **Modification**: Not thread-safe; synchronize `set_*` calls externally
|
||||||
|
* **Matrix cache**: Uses `mutable` member; not thread-safe even for const methods
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [MeshCollider Documentation](../collision/mesh_collider.md) - Collision wrapper for meshes
|
||||||
|
- [GJK Algorithm Documentation](../collision/gjk_algorithm.md) - Uses mesh for collision detection
|
||||||
|
- [EPA Algorithm Documentation](../collision/epa_algorithm.md) - Penetration depth with meshes
|
||||||
|
- [Triangle Documentation](../linear_algebra/triangle.md) - Triangle primitive
|
||||||
|
- [Mat4X4 Documentation](../linear_algebra/mat.md) - Transformation matrices
|
||||||
|
- [Box Documentation](box.md) - Box primitive
|
||||||
|
- [Plane Documentation](plane.md) - Plane primitive
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mesh Trait Documentation
|
||||||
|
|
||||||
|
For engine-specific `MeshTrait` details, see:
|
||||||
|
|
||||||
|
- [Source Engine MeshTrait](../engines/source_engine/mesh_trait.md)
|
||||||
|
- [Unity Engine MeshTrait](../engines/unity_engine/mesh_trait.md)
|
||||||
|
- [Unreal Engine MeshTrait](../engines/unreal_engine/mesh_trait.md)
|
||||||
|
- [Frostbite Engine MeshTrait](../engines/frostbite/mesh_trait.md)
|
||||||
|
- [IW Engine MeshTrait](../engines/iw_engine/mesh_trait.md)
|
||||||
|
- [OpenGL Engine MeshTrait](../engines/opengl_engine/mesh_trait.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Last updated: 13 Nov 2025*
|
||||||
322
docs/collision/epa_algorithm.md
Normal file
322
docs/collision/epa_algorithm.md
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
# `omath::collision::Epa` — Expanding Polytope Algorithm for penetration depth
|
||||||
|
|
||||||
|
> Header: `omath/collision/epa_algorithm.hpp`
|
||||||
|
> Namespace: `omath::collision`
|
||||||
|
> Depends on: `Simplex<VertexType>`, collider types with `find_abs_furthest_vertex` method
|
||||||
|
> Algorithm: **EPA** (Expanding Polytope Algorithm) for penetration depth and contact normal
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The **EPA (Expanding Polytope Algorithm)** calculates the **penetration depth** and **separation normal** between two intersecting convex shapes. It is typically used as a follow-up to the GJK algorithm after a collision has been detected.
|
||||||
|
|
||||||
|
EPA takes a 4-point simplex containing the origin (from GJK) and iteratively expands it to find the point on the Minkowski difference closest to the origin. This point gives both:
|
||||||
|
* **Depth**: minimum translation distance to separate the shapes
|
||||||
|
* **Normal**: direction of separation (pointing from shape B to shape A)
|
||||||
|
|
||||||
|
`Epa` is a template class working with any collider type that implements the support function interface.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `Epa::Result`
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
struct Result final {
|
||||||
|
bool success{false}; // true if EPA converged
|
||||||
|
Vertex normal{}; // outward normal (from B to A)
|
||||||
|
float depth{0.0f}; // penetration depth
|
||||||
|
int iterations{0}; // number of iterations performed
|
||||||
|
int num_vertices{0}; // final polytope vertex count
|
||||||
|
int num_faces{0}; // final polytope face count
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
* `success` — `true` if EPA successfully computed depth and normal; `false` if it failed to converge
|
||||||
|
* `normal` — unit vector pointing from shape B toward shape A (separation direction)
|
||||||
|
* `depth` — minimum distance to move shape A along `normal` to separate the shapes
|
||||||
|
* `iterations` — actual iteration count (useful for performance tuning)
|
||||||
|
* `num_vertices`, `num_faces` — final polytope size (for diagnostics)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `Epa::Params`
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
struct Params final {
|
||||||
|
int max_iterations{64}; // maximum iterations before giving up
|
||||||
|
float tolerance{1e-4f}; // absolute tolerance on distance growth
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
* `max_iterations` — safety limit to prevent infinite loops (default 64)
|
||||||
|
* `tolerance` — convergence threshold: stop when distance grows less than this (default 1e-4)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `Epa` Template Class
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
template<class ColliderType>
|
||||||
|
class Epa final {
|
||||||
|
public:
|
||||||
|
using Vertex = typename ColliderType::VertexType;
|
||||||
|
static_assert(EpaVector<Vertex>, "VertexType must satisfy EpaVector concept");
|
||||||
|
|
||||||
|
// Solve for penetration depth and normal
|
||||||
|
[[nodiscard]]
|
||||||
|
static Result solve(
|
||||||
|
const ColliderType& a,
|
||||||
|
const ColliderType& b,
|
||||||
|
const Simplex<Vertex>& simplex,
|
||||||
|
const Params params = {}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Precondition
|
||||||
|
|
||||||
|
The `simplex` parameter must:
|
||||||
|
* Have exactly 4 points (`simplex.size() == 4`)
|
||||||
|
* Contain the origin (i.e., be a valid GJK result with `hit == true`)
|
||||||
|
|
||||||
|
Violating this precondition leads to undefined behavior.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Collider Requirements
|
||||||
|
|
||||||
|
Any type used as `ColliderType` must provide:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Type alias for vertex type (typically Vector3<float>)
|
||||||
|
using VertexType = /* ... */;
|
||||||
|
|
||||||
|
// Find the farthest point in world space along the given direction
|
||||||
|
[[nodiscard]]
|
||||||
|
VertexType find_abs_furthest_vertex(const VertexType& direction) const;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Algorithm Details
|
||||||
|
|
||||||
|
### Expanding Polytope
|
||||||
|
|
||||||
|
EPA maintains a convex polytope (polyhedron) in Minkowski difference space `A - B`. Starting from the 4-point tetrahedron (simplex from GJK), it repeatedly:
|
||||||
|
|
||||||
|
1. **Find closest face** to the origin
|
||||||
|
2. **Support query** in the direction of the face normal
|
||||||
|
3. **Expand polytope** by adding the new support point
|
||||||
|
4. **Update faces** to maintain convexity
|
||||||
|
|
||||||
|
The algorithm terminates when:
|
||||||
|
* **Convergence**: the distance from origin to polytope stops growing (within tolerance)
|
||||||
|
* **Max iterations**: safety limit reached
|
||||||
|
* **Failure cases**: degenerate polytope or numerical issues
|
||||||
|
|
||||||
|
### Minkowski Difference
|
||||||
|
|
||||||
|
Like GJK, EPA operates in Minkowski difference space where `point = a - b` for points in shapes A and B. The closest point on this polytope to the origin gives the minimum separation.
|
||||||
|
|
||||||
|
### Face Winding
|
||||||
|
|
||||||
|
Faces are stored with outward-pointing normals. The algorithm uses a priority queue to efficiently find the face closest to the origin.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Vertex Type Requirements
|
||||||
|
|
||||||
|
The `VertexType` must satisfy the `EpaVector` concept:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
template<class V>
|
||||||
|
concept EpaVector = requires(const V& a, const V& b, float s) {
|
||||||
|
{ a - b } -> std::same_as<V>;
|
||||||
|
{ a.cross(b) } -> std::same_as<V>;
|
||||||
|
{ a.dot(b) } -> std::same_as<float>;
|
||||||
|
{ -a } -> std::same_as<V>;
|
||||||
|
{ a * s } -> std::same_as<V>;
|
||||||
|
{ a / s } -> std::same_as<V>;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
`omath::Vector3<float>` satisfies this concept.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Basic EPA Usage
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
using namespace omath::collision;
|
||||||
|
using namespace omath::source_engine;
|
||||||
|
|
||||||
|
// First, run GJK to detect collision
|
||||||
|
MeshCollider<Mesh> collider_a(mesh_a);
|
||||||
|
MeshCollider<Mesh> collider_b(mesh_b);
|
||||||
|
|
||||||
|
auto gjk_result = GjkAlgorithm<MeshCollider<Mesh>>::check_collision(
|
||||||
|
collider_a,
|
||||||
|
collider_b
|
||||||
|
);
|
||||||
|
|
||||||
|
if (gjk_result.hit) {
|
||||||
|
// Collision detected, use EPA to get penetration info
|
||||||
|
auto epa_result = Epa<MeshCollider<Mesh>>::solve(
|
||||||
|
collider_a,
|
||||||
|
collider_b,
|
||||||
|
gjk_result.simplex
|
||||||
|
);
|
||||||
|
|
||||||
|
if (epa_result.success) {
|
||||||
|
std::cout << "Penetration depth: " << epa_result.depth << "\n";
|
||||||
|
std::cout << "Separation normal: "
|
||||||
|
<< "(" << epa_result.normal.x << ", "
|
||||||
|
<< epa_result.normal.y << ", "
|
||||||
|
<< epa_result.normal.z << ")\n";
|
||||||
|
|
||||||
|
// Apply separation: move A away from B
|
||||||
|
Vector3<float> correction = epa_result.normal * epa_result.depth;
|
||||||
|
mesh_a.set_origin(mesh_a.get_origin() + correction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Parameters
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Use custom convergence settings
|
||||||
|
Epa<Collider>::Params params;
|
||||||
|
params.max_iterations = 128; // Allow more iterations for complex shapes
|
||||||
|
params.tolerance = 1e-5f; // Tighter tolerance for more accuracy
|
||||||
|
|
||||||
|
auto result = Epa<Collider>::solve(a, b, simplex, params);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Physics Integration
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void resolve_collision(PhysicsBody& body_a, PhysicsBody& body_b) {
|
||||||
|
auto gjk_result = GjkAlgorithm<Collider>::check_collision(
|
||||||
|
body_a.collider, body_b.collider
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!gjk_result.hit)
|
||||||
|
return; // No collision
|
||||||
|
|
||||||
|
auto epa_result = Epa<Collider>::solve(
|
||||||
|
body_a.collider,
|
||||||
|
body_b.collider,
|
||||||
|
gjk_result.simplex
|
||||||
|
);
|
||||||
|
|
||||||
|
if (epa_result.success) {
|
||||||
|
// Separate bodies
|
||||||
|
float mass_sum = body_a.mass + body_b.mass;
|
||||||
|
float ratio_a = body_b.mass / mass_sum;
|
||||||
|
float ratio_b = body_a.mass / mass_sum;
|
||||||
|
|
||||||
|
body_a.position += epa_result.normal * (epa_result.depth * ratio_a);
|
||||||
|
body_b.position -= epa_result.normal * (epa_result.depth * ratio_b);
|
||||||
|
|
||||||
|
// Apply collision response
|
||||||
|
apply_impulse(body_a, body_b, epa_result.normal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance Characteristics
|
||||||
|
|
||||||
|
* **Time complexity**: O(k × f) where k is iterations and f is faces per iteration (typically f grows slowly)
|
||||||
|
* **Space complexity**: O(n) where n is the number of polytope vertices (typically < 100)
|
||||||
|
* **Typical iterations**: 4-20 for most collisions
|
||||||
|
* **Worst case**: 64 iterations (configurable limit)
|
||||||
|
|
||||||
|
### Performance Tips
|
||||||
|
|
||||||
|
1. **Adjust max_iterations**: Balance accuracy vs. performance for your use case
|
||||||
|
2. **Tolerance tuning**: Larger tolerance = faster convergence but less accurate
|
||||||
|
3. **Shape complexity**: Simpler shapes (fewer faces) converge faster
|
||||||
|
4. **Deep penetrations**: Require more iterations; consider broad-phase separation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Limitations & Edge Cases
|
||||||
|
|
||||||
|
* **Requires valid simplex**: Must be called with a 4-point simplex containing the origin (from successful GJK)
|
||||||
|
* **Convex shapes only**: Like GJK, EPA only works with convex colliders
|
||||||
|
* **Convergence failure**: Can fail to converge for degenerate or very thin shapes (check `result.success`)
|
||||||
|
* **Numerical precision**: Extreme scale differences or very small shapes may cause issues
|
||||||
|
* **Deep penetration**: Very deep intersections may require many iterations or fail to converge
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
auto result = Epa<Collider>::solve(a, b, simplex);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
// EPA failed to converge
|
||||||
|
// Fallback options:
|
||||||
|
// 1. Use a default separation (e.g., axis between centers)
|
||||||
|
// 2. Increase max_iterations and retry
|
||||||
|
// 3. Log a warning and skip this collision
|
||||||
|
std::cerr << "EPA failed after " << result.iterations << " iterations\n";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Theory & Background
|
||||||
|
|
||||||
|
### Why EPA after GJK?
|
||||||
|
|
||||||
|
GJK determines **if** shapes intersect but doesn't compute penetration depth. EPA extends GJK's final simplex to find the exact depth and normal needed for:
|
||||||
|
* **Collision response** — separating objects realistically
|
||||||
|
* **Contact manifolds** — generating contact points for physics
|
||||||
|
* **Constraint solving** — iterative physics solvers
|
||||||
|
|
||||||
|
### Comparison with SAT
|
||||||
|
|
||||||
|
| Feature | EPA | SAT (Separating Axis Theorem) |
|
||||||
|
|---------|-----|-------------------------------|
|
||||||
|
| Works with | Any convex shape | Polytopes (faces/edges) |
|
||||||
|
| Penetration depth | Yes | Yes |
|
||||||
|
| Complexity | Iterative | Per-axis projection |
|
||||||
|
| Best for | General convex | Boxes, prisms |
|
||||||
|
| Typical speed | Moderate | Fast (few axes) |
|
||||||
|
|
||||||
|
EPA is more general; SAT is faster for axis-aligned shapes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
The EPA implementation in OMath:
|
||||||
|
* Uses a **priority queue** to efficiently find the closest face
|
||||||
|
* Maintains face winding for consistent normals
|
||||||
|
* Handles **edge cases**: degenerate faces, numerical instability
|
||||||
|
* Prevents infinite loops with iteration limits
|
||||||
|
* Returns detailed diagnostics (iteration count, polytope size)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [GJK Algorithm Documentation](gjk_algorithm.md) - Collision detection (required before EPA)
|
||||||
|
- [Simplex Documentation](simplex.md) - Input simplex structure
|
||||||
|
- [MeshCollider Documentation](mesh_collider.md) - Mesh-based collider
|
||||||
|
- [Mesh Documentation](../3d_primitives/mesh.md) - Mesh primitive
|
||||||
|
- [Tutorials - Collision Detection](../tutorials.md#tutorial-4-collision-detection) - Complete collision tutorial
|
||||||
|
- [API Overview](../api_overview.md) - High-level API reference
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Last updated: 13 Nov 2025*
|
||||||
216
docs/collision/gjk_algorithm.md
Normal file
216
docs/collision/gjk_algorithm.md
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
# `omath::collision::GjkAlgorithm` — Gilbert-Johnson-Keerthi collision detection
|
||||||
|
|
||||||
|
> Header: `omath/collision/gjk_algorithm.hpp`
|
||||||
|
> Namespace: `omath::collision`
|
||||||
|
> Depends on: `Simplex<VertexType>`, collider types with `find_abs_furthest_vertex` method
|
||||||
|
> Algorithm: **GJK** (Gilbert-Johnson-Keerthi) for convex shape collision detection
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The **GJK algorithm** determines whether two convex shapes intersect by iteratively constructing a simplex in Minkowski difference space. The algorithm is widely used in physics engines and collision detection systems due to its efficiency and robustness.
|
||||||
|
|
||||||
|
`GjkAlgorithm` is a template class that works with any collider type implementing the required support function interface:
|
||||||
|
|
||||||
|
* `find_abs_furthest_vertex(direction)` — returns the farthest point in the collider along the given direction.
|
||||||
|
|
||||||
|
The algorithm returns a `GjkHitInfo` containing:
|
||||||
|
* `hit` — boolean indicating whether the shapes intersect
|
||||||
|
* `simplex` — a 4-point simplex containing the origin (valid only when `hit == true`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `GjkHitInfo`
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
template<class VertexType>
|
||||||
|
struct GjkHitInfo final {
|
||||||
|
bool hit{false}; // true if collision detected
|
||||||
|
Simplex<VertexType> simplex; // 4-point simplex (valid only if hit == true)
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
The `simplex` field is only meaningful when `hit == true` and contains 4 points. This simplex can be passed to the EPA algorithm for penetration depth calculation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `GjkAlgorithm`
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
template<class ColliderType>
|
||||||
|
class GjkAlgorithm final {
|
||||||
|
using VertexType = typename ColliderType::VertexType;
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Find support vertex in Minkowski difference
|
||||||
|
[[nodiscard]]
|
||||||
|
static VertexType find_support_vertex(
|
||||||
|
const ColliderType& collider_a,
|
||||||
|
const ColliderType& collider_b,
|
||||||
|
const VertexType& direction
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if two convex shapes intersect
|
||||||
|
[[nodiscard]]
|
||||||
|
static GjkHitInfo<VertexType> check_collision(
|
||||||
|
const ColliderType& collider_a,
|
||||||
|
const ColliderType& collider_b
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Collider Requirements
|
||||||
|
|
||||||
|
Any type used as `ColliderType` must provide:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Type alias for vertex type (typically Vector3<float>)
|
||||||
|
using VertexType = /* ... */;
|
||||||
|
|
||||||
|
// Find the farthest point in world space along the given direction
|
||||||
|
[[nodiscard]]
|
||||||
|
VertexType find_abs_furthest_vertex(const VertexType& direction) const;
|
||||||
|
```
|
||||||
|
|
||||||
|
Common collider types:
|
||||||
|
* `MeshCollider<MeshType>` — for arbitrary triangle meshes
|
||||||
|
* Custom colliders for spheres, boxes, capsules, etc.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Algorithm Details
|
||||||
|
|
||||||
|
### Minkowski Difference
|
||||||
|
|
||||||
|
GJK operates in the **Minkowski difference** space `A - B`, where a point in this space represents the difference between points in shapes A and B. The shapes intersect if and only if the origin lies within this Minkowski difference.
|
||||||
|
|
||||||
|
### Support Function
|
||||||
|
|
||||||
|
The support function finds the point in the Minkowski difference farthest along a given direction:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
support(A, B, dir) = A.furthest(dir) - B.furthest(-dir)
|
||||||
|
```
|
||||||
|
|
||||||
|
This is computed by `find_support_vertex`.
|
||||||
|
|
||||||
|
### Simplex Iteration
|
||||||
|
|
||||||
|
The algorithm builds a simplex incrementally:
|
||||||
|
1. Start with an initial direction (typically vector between shape centers)
|
||||||
|
2. Add support vertices in directions that move the simplex toward the origin
|
||||||
|
3. Simplify the simplex to keep only points closest to the origin
|
||||||
|
4. Repeat until either:
|
||||||
|
* Origin is contained (collision detected, returns 4-point simplex)
|
||||||
|
* No progress can be made (no collision)
|
||||||
|
|
||||||
|
Maximum 64 iterations are performed to prevent infinite loops in edge cases.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Basic Collision Check
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
using namespace omath::collision;
|
||||||
|
using namespace omath::source_engine;
|
||||||
|
|
||||||
|
// Create mesh colliders
|
||||||
|
Mesh mesh_a = /* ... */;
|
||||||
|
Mesh mesh_b = /* ... */;
|
||||||
|
|
||||||
|
MeshCollider collider_a(mesh_a);
|
||||||
|
MeshCollider collider_b(mesh_b);
|
||||||
|
|
||||||
|
// Check for collision
|
||||||
|
auto result = GjkAlgorithm<MeshCollider<Mesh>>::check_collision(
|
||||||
|
collider_a,
|
||||||
|
collider_b
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.hit) {
|
||||||
|
std::cout << "Collision detected!\n";
|
||||||
|
// Can pass result.simplex to EPA for penetration depth
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Combined with EPA
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
auto gjk_result = GjkAlgorithm<Collider>::check_collision(a, b);
|
||||||
|
|
||||||
|
if (gjk_result.hit) {
|
||||||
|
// Get penetration depth and normal using EPA
|
||||||
|
auto epa_result = Epa<Collider>::solve(
|
||||||
|
a, b, gjk_result.simplex
|
||||||
|
);
|
||||||
|
|
||||||
|
if (epa_result.success) {
|
||||||
|
std::cout << "Penetration depth: " << epa_result.depth << "\n";
|
||||||
|
std::cout << "Separation normal: " << epa_result.normal << "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance Characteristics
|
||||||
|
|
||||||
|
* **Time complexity**: O(k) where k is the number of iterations (typically < 20 for most cases)
|
||||||
|
* **Space complexity**: O(1) — only stores a 4-point simplex
|
||||||
|
* **Best case**: 4-8 iterations for well-separated objects
|
||||||
|
* **Worst case**: 64 iterations (hard limit)
|
||||||
|
* **Cache efficient**: operates on small fixed-size data structures
|
||||||
|
|
||||||
|
### Optimization Tips
|
||||||
|
|
||||||
|
1. **Initial direction**: Use vector between shape centers for faster convergence
|
||||||
|
2. **Early exit**: GJK quickly rejects non-intersecting shapes
|
||||||
|
3. **Warm starting**: Reuse previous simplex for continuous collision detection
|
||||||
|
4. **Broad phase**: Use spatial partitioning before GJK (AABB trees, grids)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Limitations & Edge Cases
|
||||||
|
|
||||||
|
* **Convex shapes only**: GJK only works with convex colliders. For concave shapes, decompose into convex parts or use a mesh collider wrapper.
|
||||||
|
* **Degenerate simplices**: The algorithm handles degenerate cases, but numerical precision can cause issues with very thin or flat shapes.
|
||||||
|
* **Iteration limit**: Hard limit of 64 iterations prevents infinite loops but may miss collisions in extreme cases.
|
||||||
|
* **Zero-length directions**: The simplex update logic guards against zero-length vectors, returning safe fallbacks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Vertex Type Requirements
|
||||||
|
|
||||||
|
The `VertexType` must satisfy the `GjkVector` concept (defined in `simplex.hpp`):
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
template<class V>
|
||||||
|
concept GjkVector = requires(const V& a, const V& b) {
|
||||||
|
{ -a } -> std::same_as<V>;
|
||||||
|
{ a - b } -> std::same_as<V>;
|
||||||
|
{ a.cross(b) } -> std::same_as<V>;
|
||||||
|
{ a.point_to_same_direction(b) } -> std::same_as<bool>;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
`omath::Vector3<float>` satisfies this concept.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [EPA Algorithm Documentation](epa_algorithm.md) - Penetration depth calculation
|
||||||
|
- [Simplex Documentation](simplex.md) - Simplex data structure
|
||||||
|
- [MeshCollider Documentation](mesh_collider.md) - Mesh-based collider
|
||||||
|
- [Mesh Documentation](../3d_primitives/mesh.md) - Mesh primitive
|
||||||
|
- [LineTracer Documentation](line_tracer.md) - Ray-triangle intersection
|
||||||
|
- [Tutorials - Collision Detection](../tutorials.md#tutorial-4-collision-detection) - Complete collision tutorial
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Last updated: 13 Nov 2025*
|
||||||
371
docs/collision/mesh_collider.md
Normal file
371
docs/collision/mesh_collider.md
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
# `omath::collision::MeshCollider` — Convex hull collider for meshes
|
||||||
|
|
||||||
|
> Header: `omath/collision/mesh_collider.hpp`
|
||||||
|
> Namespace: `omath::collision`
|
||||||
|
> Depends on: `omath::primitives::Mesh`, `omath::Vector3<T>`
|
||||||
|
> Purpose: wrap a mesh to provide collision detection support for GJK/EPA
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
`MeshCollider` wraps a `Mesh` object to provide the **support function** interface required by the GJK and EPA collision detection algorithms. The support function finds the vertex of the mesh farthest along a given direction, which is essential for constructing Minkowski difference simplices.
|
||||||
|
|
||||||
|
**Important**: `MeshCollider` assumes the mesh represents a **convex hull**. For non-convex shapes, you must either:
|
||||||
|
* Decompose into convex parts
|
||||||
|
* Use the convex hull of the mesh
|
||||||
|
* Use a different collision detection algorithm
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Template Declaration
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
template<class MeshType>
|
||||||
|
class MeshCollider;
|
||||||
|
```
|
||||||
|
|
||||||
|
### MeshType Requirements
|
||||||
|
|
||||||
|
The `MeshType` must be an instantiation of `omath::primitives::Mesh` or provide:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
struct MeshType {
|
||||||
|
using NumericType = /* float, double, etc. */;
|
||||||
|
|
||||||
|
std::vector<Vector3<NumericType>> m_vertex_buffer;
|
||||||
|
|
||||||
|
// Transform vertex from local to world space
|
||||||
|
Vector3<NumericType> vertex_to_world_space(const Vector3<NumericType>&) const;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Common types:
|
||||||
|
* `omath::source_engine::Mesh`
|
||||||
|
* `omath::unity_engine::Mesh`
|
||||||
|
* `omath::unreal_engine::Mesh`
|
||||||
|
* `omath::frostbite_engine::Mesh`
|
||||||
|
* `omath::iw_engine::Mesh`
|
||||||
|
* `omath::opengl_engine::Mesh`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Type Aliases
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
using NumericType = typename MeshType::NumericType;
|
||||||
|
using VertexType = Vector3<NumericType>;
|
||||||
|
```
|
||||||
|
|
||||||
|
* `NumericType` — scalar type (typically `float`)
|
||||||
|
* `VertexType` — 3D vector type for vertices
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Constructor
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
explicit MeshCollider(MeshType mesh);
|
||||||
|
```
|
||||||
|
|
||||||
|
Creates a collider from a mesh. The mesh is **moved** into the collider, so pass by value:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
omath::source_engine::Mesh my_mesh = /* ... */;
|
||||||
|
MeshCollider collider(std::move(my_mesh));
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Methods
|
||||||
|
|
||||||
|
### `find_furthest_vertex`
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
[[nodiscard]]
|
||||||
|
const VertexType& find_furthest_vertex(const VertexType& direction) const;
|
||||||
|
```
|
||||||
|
|
||||||
|
Finds the vertex in the mesh's **local space** that has the maximum dot product with `direction`.
|
||||||
|
|
||||||
|
**Algorithm**: Linear search through all vertices (O(n) where n is vertex count).
|
||||||
|
|
||||||
|
**Returns**: Const reference to the vertex in `m_vertex_buffer`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `find_abs_furthest_vertex`
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
[[nodiscard]]
|
||||||
|
VertexType find_abs_furthest_vertex(const VertexType& direction) const;
|
||||||
|
```
|
||||||
|
|
||||||
|
Finds the vertex farthest along `direction` and transforms it to **world space**. This is the primary method used by GJK/EPA.
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
1. Find furthest vertex in local space using `find_furthest_vertex`
|
||||||
|
2. Transform to world space using `mesh.vertex_to_world_space()`
|
||||||
|
|
||||||
|
**Returns**: Vertex position in world coordinates.
|
||||||
|
|
||||||
|
**Usage in GJK**:
|
||||||
|
```cpp
|
||||||
|
// GJK support function for Minkowski difference
|
||||||
|
VertexType support = collider_a.find_abs_furthest_vertex(direction)
|
||||||
|
- collider_b.find_abs_furthest_vertex(-direction);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Basic Collision Detection
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
using namespace omath::collision;
|
||||||
|
using namespace omath::source_engine;
|
||||||
|
|
||||||
|
// Create meshes with vertex data
|
||||||
|
std::vector<Vector3<float>> vbo_a = {
|
||||||
|
{-1, -1, -1}, {1, -1, -1}, {1, 1, -1}, {-1, 1, -1},
|
||||||
|
{-1, -1, 1}, {1, -1, 1}, {1, 1, 1}, {-1, 1, 1}
|
||||||
|
};
|
||||||
|
std::vector<Vector3<std::size_t>> vao_a = /* face indices */;
|
||||||
|
|
||||||
|
Mesh mesh_a(vbo_a, vao_a);
|
||||||
|
mesh_a.set_origin({0, 0, 0});
|
||||||
|
|
||||||
|
Mesh mesh_b(vbo_b, vao_b);
|
||||||
|
mesh_b.set_origin({5, 0, 0}); // Positioned away from mesh_a
|
||||||
|
|
||||||
|
// Wrap in colliders
|
||||||
|
MeshCollider<Mesh> collider_a(std::move(mesh_a));
|
||||||
|
MeshCollider<Mesh> collider_b(std::move(mesh_b));
|
||||||
|
|
||||||
|
// Run GJK
|
||||||
|
auto result = GjkAlgorithm<MeshCollider<Mesh>>::check_collision(
|
||||||
|
collider_a, collider_b
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.hit) {
|
||||||
|
std::cout << "Collision detected!\n";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### With EPA for Penetration Depth
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
auto gjk_result = GjkAlgorithm<MeshCollider<Mesh>>::check_collision(
|
||||||
|
collider_a, collider_b
|
||||||
|
);
|
||||||
|
|
||||||
|
if (gjk_result.hit) {
|
||||||
|
auto epa_result = Epa<MeshCollider<Mesh>>::solve(
|
||||||
|
collider_a, collider_b, gjk_result.simplex
|
||||||
|
);
|
||||||
|
|
||||||
|
if (epa_result.success) {
|
||||||
|
std::cout << "Penetration: " << epa_result.depth << " units\n";
|
||||||
|
std::cout << "Normal: " << epa_result.normal << "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Mesh Creation
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Create a simple box mesh
|
||||||
|
std::vector<Vector3<float>> box_vertices = {
|
||||||
|
{-0.5f, -0.5f, -0.5f}, { 0.5f, -0.5f, -0.5f},
|
||||||
|
{ 0.5f, 0.5f, -0.5f}, {-0.5f, 0.5f, -0.5f},
|
||||||
|
{-0.5f, -0.5f, 0.5f}, { 0.5f, -0.5f, 0.5f},
|
||||||
|
{ 0.5f, 0.5f, 0.5f}, {-0.5f, 0.5f, 0.5f}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<Vector3<std::size_t>> box_indices = {
|
||||||
|
{0, 1, 2}, {0, 2, 3}, // Front face
|
||||||
|
{4, 6, 5}, {4, 7, 6}, // Back face
|
||||||
|
{0, 4, 5}, {0, 5, 1}, // Bottom face
|
||||||
|
{2, 6, 7}, {2, 7, 3}, // Top face
|
||||||
|
{0, 3, 7}, {0, 7, 4}, // Left face
|
||||||
|
{1, 5, 6}, {1, 6, 2} // Right face
|
||||||
|
};
|
||||||
|
|
||||||
|
using namespace omath::source_engine;
|
||||||
|
Mesh box_mesh(box_vertices, box_indices);
|
||||||
|
box_mesh.set_origin({10, 0, 0});
|
||||||
|
box_mesh.set_scale({2, 2, 2});
|
||||||
|
|
||||||
|
MeshCollider<Mesh> box_collider(std::move(box_mesh));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Oriented Collision
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Create rotated mesh
|
||||||
|
Mesh mesh(vertices, indices);
|
||||||
|
mesh.set_origin({5, 5, 5});
|
||||||
|
mesh.set_scale({1, 1, 1});
|
||||||
|
|
||||||
|
// Set rotation (engine-specific angles)
|
||||||
|
ViewAngles rotation;
|
||||||
|
rotation.pitch = PitchAngle::from_degrees(45.0f);
|
||||||
|
rotation.yaw = YawAngle::from_degrees(30.0f);
|
||||||
|
mesh.set_rotation(rotation);
|
||||||
|
|
||||||
|
// Collider automatically handles transformation
|
||||||
|
MeshCollider<Mesh> collider(std::move(mesh));
|
||||||
|
|
||||||
|
// Support function returns world-space vertices
|
||||||
|
auto support = collider.find_abs_furthest_vertex({0, 1, 0});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
### Linear Search
|
||||||
|
|
||||||
|
`find_furthest_vertex` performs a **linear search** through all vertices:
|
||||||
|
* **Time complexity**: O(n) per support query
|
||||||
|
* **GJK iterations**: ~10-20 support queries per collision test
|
||||||
|
* **Total cost**: O(k × n) where k is GJK iterations
|
||||||
|
|
||||||
|
For meshes with many vertices (>1000), consider:
|
||||||
|
* Using simpler proxy geometry (bounding box, convex hull with fewer vertices)
|
||||||
|
* Pre-computing hierarchical structures
|
||||||
|
* Using specialized collision shapes when possible
|
||||||
|
|
||||||
|
### Caching Opportunities
|
||||||
|
|
||||||
|
The implementation uses `std::ranges::max_element`, which is cache-friendly for contiguous vertex buffers. For optimal performance:
|
||||||
|
* Store vertices contiguously in memory
|
||||||
|
* Avoid pointer-based or scattered vertex storage
|
||||||
|
* Consider SoA (Structure of Arrays) layout for SIMD optimization
|
||||||
|
|
||||||
|
### World Space Transformation
|
||||||
|
|
||||||
|
The `vertex_to_world_space` call involves matrix multiplication:
|
||||||
|
* **Cost**: ~15-20 floating-point operations per vertex
|
||||||
|
* **Optimization**: The mesh caches its transformation matrix
|
||||||
|
* **Update cost**: Only recomputed when origin/rotation/scale changes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Limitations & Edge Cases
|
||||||
|
|
||||||
|
### Convex Hull Requirement
|
||||||
|
|
||||||
|
**Critical**: GJK/EPA only work with **convex shapes**. If your mesh is concave:
|
||||||
|
|
||||||
|
#### Option 1: Convex Decomposition
|
||||||
|
```cpp
|
||||||
|
// Decompose concave mesh into convex parts
|
||||||
|
std::vector<Mesh> convex_parts = decompose_mesh(concave_mesh);
|
||||||
|
|
||||||
|
for (const auto& part : convex_parts) {
|
||||||
|
MeshCollider collider(part);
|
||||||
|
// Test each part separately
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Option 2: Use Convex Hull
|
||||||
|
```cpp
|
||||||
|
// Compute convex hull of vertices
|
||||||
|
auto hull_vertices = compute_convex_hull(mesh.m_vertex_buffer);
|
||||||
|
Mesh hull_mesh(hull_vertices, hull_indices);
|
||||||
|
MeshCollider collider(std::move(hull_mesh));
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Option 3: Different Algorithm
|
||||||
|
Use triangle-based collision (e.g., LineTracer) for true concave support.
|
||||||
|
|
||||||
|
### Empty Mesh
|
||||||
|
|
||||||
|
Behavior is undefined if `m_vertex_buffer` is empty. Always ensure:
|
||||||
|
```cpp
|
||||||
|
assert(!mesh.m_vertex_buffer.empty());
|
||||||
|
MeshCollider collider(std::move(mesh));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Degenerate Meshes
|
||||||
|
|
||||||
|
* **Single vertex**: Treated as a point (degenerates to sphere collision)
|
||||||
|
* **Two vertices**: Line segment (may cause GJK issues)
|
||||||
|
* **Coplanar vertices**: Flat mesh; EPA may have convergence issues
|
||||||
|
|
||||||
|
**Recommendation**: Use at least 4 non-coplanar vertices for robustness.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Coordinate Systems
|
||||||
|
|
||||||
|
`MeshCollider` supports different engine coordinate systems through the `MeshTrait`:
|
||||||
|
|
||||||
|
| Engine | Up Axis | Handedness | Rotation Order |
|
||||||
|
|--------|---------|------------|----------------|
|
||||||
|
| Source Engine | Z | Right-handed | Pitch/Yaw/Roll |
|
||||||
|
| Unity | Y | Left-handed | Pitch/Yaw/Roll |
|
||||||
|
| Unreal | Z | Left-handed | Roll/Pitch/Yaw |
|
||||||
|
| Frostbite | Y | Right-handed | Pitch/Yaw/Roll |
|
||||||
|
| IW Engine | Z | Right-handed | Pitch/Yaw/Roll |
|
||||||
|
| OpenGL | Y | Right-handed | Pitch/Yaw/Roll |
|
||||||
|
|
||||||
|
The `vertex_to_world_space` method handles these differences transparently.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Advanced Usage
|
||||||
|
|
||||||
|
### Custom Support Function
|
||||||
|
|
||||||
|
For specialized collision shapes, implement a custom collider:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class SphereCollider {
|
||||||
|
public:
|
||||||
|
using VertexType = Vector3<float>;
|
||||||
|
|
||||||
|
Vector3<float> center;
|
||||||
|
float radius;
|
||||||
|
|
||||||
|
VertexType find_abs_furthest_vertex(const VertexType& direction) const {
|
||||||
|
auto normalized = direction.normalized();
|
||||||
|
return center + normalized * radius;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use with GJK/EPA
|
||||||
|
auto result = GjkAlgorithm<SphereCollider>::check_collision(sphere_a, sphere_b);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Debugging Support Queries
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class DebugMeshCollider : public MeshCollider<Mesh> {
|
||||||
|
public:
|
||||||
|
using MeshCollider::MeshCollider;
|
||||||
|
|
||||||
|
VertexType find_abs_furthest_vertex(const VertexType& direction) const {
|
||||||
|
auto result = MeshCollider::find_abs_furthest_vertex(direction);
|
||||||
|
std::cout << "Support query: direction=" << direction
|
||||||
|
<< " -> vertex=" << result << "\n";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [GJK Algorithm Documentation](gjk_algorithm.md) - Uses `MeshCollider` for collision detection
|
||||||
|
- [EPA Algorithm Documentation](epa_algorithm.md) - Uses `MeshCollider` for penetration depth
|
||||||
|
- [Simplex Documentation](simplex.md) - Data structure used by GJK
|
||||||
|
- [Mesh Documentation](../3d_primitives/mesh.md) - Underlying mesh primitive
|
||||||
|
- [Tutorials - Collision Detection](../tutorials.md#tutorial-4-collision-detection) - Complete collision tutorial
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Last updated: 13 Nov 2025*
|
||||||
327
docs/collision/simplex.md
Normal file
327
docs/collision/simplex.md
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
# `omath::collision::Simplex` — Fixed-capacity simplex for GJK/EPA
|
||||||
|
|
||||||
|
> Header: `omath/collision/simplex.hpp`
|
||||||
|
> Namespace: `omath::collision`
|
||||||
|
> Depends on: `Vector3<float>` (or any type satisfying `GjkVector` concept)
|
||||||
|
> Purpose: store and manipulate simplices in GJK and EPA algorithms
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
`Simplex` is a lightweight container for up to 4 points, used internally by the GJK and EPA collision detection algorithms. A simplex in this context is a geometric shape defined by 1 to 4 vertices:
|
||||||
|
|
||||||
|
* **1 point** — a single vertex
|
||||||
|
* **2 points** — a line segment
|
||||||
|
* **3 points** — a triangle
|
||||||
|
* **4 points** — a tetrahedron
|
||||||
|
|
||||||
|
The GJK algorithm builds simplices incrementally to detect collisions, and EPA extends a 4-point simplex to compute penetration depth.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Template & Concepts
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
template<GjkVector VectorType = Vector3<float>>
|
||||||
|
class Simplex final;
|
||||||
|
```
|
||||||
|
|
||||||
|
### `GjkVector` Concept
|
||||||
|
|
||||||
|
The vertex type must satisfy:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
template<class V>
|
||||||
|
concept GjkVector = requires(const V& a, const V& b) {
|
||||||
|
{ -a } -> std::same_as<V>;
|
||||||
|
{ a - b } -> std::same_as<V>;
|
||||||
|
{ a.cross(b) } -> std::same_as<V>;
|
||||||
|
{ a.point_to_same_direction(b) } -> std::same_as<bool>;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
`omath::Vector3<float>` satisfies this concept and is the default.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Constructors & Assignment
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
constexpr Simplex() = default;
|
||||||
|
|
||||||
|
constexpr Simplex& operator=(std::initializer_list<VectorType> list) noexcept;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Initialization
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Empty simplex
|
||||||
|
Simplex<Vector3<float>> s;
|
||||||
|
|
||||||
|
// Initialize with points
|
||||||
|
Simplex<Vector3<float>> s2;
|
||||||
|
s2 = {v1, v2, v3}; // 3-point simplex (triangle)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Constraint**: Maximum 4 points. Passing more triggers an assertion in debug builds.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Methods
|
||||||
|
|
||||||
|
### Adding Points
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
constexpr void push_front(const VectorType& p) noexcept;
|
||||||
|
```
|
||||||
|
|
||||||
|
Inserts a point at the **front** (index 0), shifting existing points back. If the simplex is already at capacity (4 points), the last point is discarded.
|
||||||
|
|
||||||
|
**Usage pattern in GJK**:
|
||||||
|
```cpp
|
||||||
|
simplex.push_front(new_support_point);
|
||||||
|
// Now simplex[0] is the newest point
|
||||||
|
```
|
||||||
|
|
||||||
|
### Size & Capacity
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
[[nodiscard]] constexpr std::size_t size() const noexcept;
|
||||||
|
[[nodiscard]] static constexpr std::size_t capacity = 4;
|
||||||
|
```
|
||||||
|
|
||||||
|
* `size()` — current number of points (0-4)
|
||||||
|
* `capacity` — maximum points (always 4)
|
||||||
|
|
||||||
|
### Element Access
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
[[nodiscard]] constexpr VectorType& operator[](std::size_t index) noexcept;
|
||||||
|
[[nodiscard]] constexpr const VectorType& operator[](std::size_t index) const noexcept;
|
||||||
|
```
|
||||||
|
|
||||||
|
Access points by index. **No bounds checking** — index must be `< size()`.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
if (simplex.size() >= 2) {
|
||||||
|
auto edge = simplex[1] - simplex[0];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Iterators
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
[[nodiscard]] constexpr auto begin() noexcept;
|
||||||
|
[[nodiscard]] constexpr auto end() noexcept;
|
||||||
|
[[nodiscard]] constexpr auto begin() const noexcept;
|
||||||
|
[[nodiscard]] constexpr auto end() const noexcept;
|
||||||
|
```
|
||||||
|
|
||||||
|
Standard iterator support for range-based loops:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
for (const auto& vertex : simplex) {
|
||||||
|
std::cout << vertex << "\n";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## GJK-Specific Methods
|
||||||
|
|
||||||
|
These methods implement the core logic for simplifying simplices in the GJK algorithm.
|
||||||
|
|
||||||
|
### `contains_origin`
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
[[nodiscard]] constexpr bool contains_origin() noexcept;
|
||||||
|
```
|
||||||
|
|
||||||
|
Determines if the origin lies within the current simplex. This is the **core GJK test**: if true, the shapes intersect.
|
||||||
|
|
||||||
|
* For a **1-point** simplex, always returns `false` (can't contain origin)
|
||||||
|
* For a **2-point** simplex (line), checks if origin projects onto the segment
|
||||||
|
* For a **3-point** simplex (triangle), checks if origin projects onto the triangle
|
||||||
|
* For a **4-point** simplex (tetrahedron), checks if origin is inside
|
||||||
|
|
||||||
|
**Side effect**: Simplifies the simplex by removing points not needed to maintain proximity to the origin. After calling, `size()` may have decreased.
|
||||||
|
|
||||||
|
**Return value**:
|
||||||
|
* `true` — origin is contained (collision detected)
|
||||||
|
* `false` — origin not contained; simplex has been simplified toward origin
|
||||||
|
|
||||||
|
### `next_direction`
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
[[nodiscard]] constexpr VectorType next_direction() const noexcept;
|
||||||
|
```
|
||||||
|
|
||||||
|
Computes the next search direction for GJK. This is the direction from the simplex toward the origin, used to query the next support point.
|
||||||
|
|
||||||
|
* Must be called **after** `contains_origin()` returns `false`
|
||||||
|
* Behavior is **undefined** if called when `size() == 0` or when origin is already contained
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### GJK Iteration (Simplified)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
Simplex<Vector3<float>> simplex;
|
||||||
|
Vector3<float> direction{1, 0, 0}; // Initial search direction
|
||||||
|
|
||||||
|
for (int i = 0; i < 64; ++i) {
|
||||||
|
// Get support point in current direction
|
||||||
|
auto support = find_support_vertex(collider_a, collider_b, direction);
|
||||||
|
|
||||||
|
// Check if we made progress
|
||||||
|
if (support.dot(direction) <= 0)
|
||||||
|
break; // No collision possible
|
||||||
|
|
||||||
|
simplex.push_front(support);
|
||||||
|
|
||||||
|
// Check if simplex contains origin
|
||||||
|
if (simplex.contains_origin()) {
|
||||||
|
// Collision detected!
|
||||||
|
assert(simplex.size() == 4);
|
||||||
|
return GjkHitInfo{true, simplex};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get next search direction
|
||||||
|
direction = simplex.next_direction();
|
||||||
|
}
|
||||||
|
|
||||||
|
// No collision
|
||||||
|
return GjkHitInfo{false, {}};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Simplex Construction
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
using Vec3 = Vector3<float>;
|
||||||
|
|
||||||
|
Simplex<Vec3> simplex;
|
||||||
|
simplex = {
|
||||||
|
Vec3{0.0f, 0.0f, 0.0f},
|
||||||
|
Vec3{1.0f, 0.0f, 0.0f},
|
||||||
|
Vec3{0.0f, 1.0f, 0.0f},
|
||||||
|
Vec3{0.0f, 0.0f, 1.0f}
|
||||||
|
};
|
||||||
|
|
||||||
|
assert(simplex.size() == 4);
|
||||||
|
|
||||||
|
// Check if origin is inside this tetrahedron
|
||||||
|
bool has_collision = simplex.contains_origin();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Iterating Over Points
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void print_simplex(const Simplex<Vector3<float>>& s) {
|
||||||
|
std::cout << "Simplex with " << s.size() << " points:\n";
|
||||||
|
for (std::size_t i = 0; i < s.size(); ++i) {
|
||||||
|
const auto& p = s[i];
|
||||||
|
std::cout << " [" << i << "] = ("
|
||||||
|
<< p.x << ", " << p.y << ", " << p.z << ")\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### Simplex Simplification
|
||||||
|
|
||||||
|
The `contains_origin()` method implements different tests based on simplex size:
|
||||||
|
|
||||||
|
#### Line Segment (2 points)
|
||||||
|
|
||||||
|
Checks if origin projects onto segment `[A, B]`:
|
||||||
|
* If yes, keeps both points
|
||||||
|
* If no, keeps only the closer point
|
||||||
|
|
||||||
|
#### Triangle (3 points)
|
||||||
|
|
||||||
|
Tests the origin against the triangle plane and edges using cross products. Simplifies to:
|
||||||
|
* The full triangle if origin projects onto its surface
|
||||||
|
* An edge if origin is closest to that edge
|
||||||
|
* A single vertex otherwise
|
||||||
|
|
||||||
|
#### Tetrahedron (4 points)
|
||||||
|
|
||||||
|
Tests origin against all four faces:
|
||||||
|
* If origin is inside, returns `true` (collision)
|
||||||
|
* If outside, reduces to the face/edge/vertex closest to origin
|
||||||
|
|
||||||
|
### Direction Calculation
|
||||||
|
|
||||||
|
The `next_direction()` method computes:
|
||||||
|
* For **line**: perpendicular from line toward origin
|
||||||
|
* For **triangle**: perpendicular from triangle toward origin
|
||||||
|
* Implementation uses cross products and projections to avoid sqrt when possible
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance Characteristics
|
||||||
|
|
||||||
|
* **Storage**: Fixed 4 × `sizeof(VectorType)` + size counter
|
||||||
|
* **Push front**: O(n) where n is current size (max 4, so effectively O(1))
|
||||||
|
* **Contains origin**: O(1) for each case (line, triangle, tetrahedron)
|
||||||
|
* **Next direction**: O(1) — simple cross products and subtractions
|
||||||
|
* **No heap allocations**: All storage is inline
|
||||||
|
|
||||||
|
**constexpr**: All methods are `constexpr`, enabling compile-time usage where feasible.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Edge Cases & Constraints
|
||||||
|
|
||||||
|
### Degenerate Simplices
|
||||||
|
|
||||||
|
* **Zero-length edges**: Can occur if support points coincide. The algorithm handles this by checking `point_to_same_direction` before divisions.
|
||||||
|
* **Collinear points**: Triangle simplification detects and handles collinear cases by reducing to a line.
|
||||||
|
* **Flat tetrahedron**: If the 4th point is coplanar with the first 3, the origin containment test may have reduced precision.
|
||||||
|
|
||||||
|
### Assertions
|
||||||
|
|
||||||
|
* **Capacity**: `operator=` asserts `list.size() <= 4` in debug builds
|
||||||
|
* **Index bounds**: No bounds checking in release builds — ensure `index < size()`
|
||||||
|
|
||||||
|
### Thread Safety
|
||||||
|
|
||||||
|
* **Read-only**: Safe to read from multiple threads
|
||||||
|
* **Modification**: Not thread-safe; synchronize writes externally
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Relationship to GJK & EPA
|
||||||
|
|
||||||
|
### In GJK
|
||||||
|
|
||||||
|
* Starts empty or with an initial point
|
||||||
|
* Grows via `push_front` as support points are added
|
||||||
|
* Shrinks via `contains_origin` as it's simplified
|
||||||
|
* Once it reaches 4 points and contains origin, GJK succeeds
|
||||||
|
|
||||||
|
### In EPA
|
||||||
|
|
||||||
|
* Takes a 4-point simplex from GJK as input
|
||||||
|
* Uses the tetrahedron as the initial polytope
|
||||||
|
* Does not directly use the `Simplex` class for expansion (EPA maintains a more complex polytope structure)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [GJK Algorithm Documentation](gjk_algorithm.md) - Uses `Simplex` for collision detection
|
||||||
|
- [EPA Algorithm Documentation](epa_algorithm.md) - Takes 4-point `Simplex` as input
|
||||||
|
- [MeshCollider Documentation](mesh_collider.md) - Provides support function for GJK/EPA
|
||||||
|
- [Vector3 Documentation](../linear_algebra/vector3.md) - Default vertex type
|
||||||
|
- [Tutorials - Collision Detection](../tutorials.md#tutorial-4-collision-detection) - Collision tutorial
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Last updated: 13 Nov 2025*
|
||||||
119
docs/engines/frostbite/mesh_trait.md
Normal file
119
docs/engines/frostbite/mesh_trait.md
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
# `omath::frostbite_engine::MeshTrait` — mesh transformation trait for Frostbite Engine
|
||||||
|
|
||||||
|
> Header: `omath/engines/frostbite_engine/traits/mesh_trait.hpp`
|
||||||
|
> Namespace: `omath::frostbite_engine`
|
||||||
|
> Purpose: provide Frostbite Engine-specific rotation matrix computation for `omath::primitives::Mesh`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
`MeshTrait` is a trait class that provides the `rotation_matrix` function for transforming meshes in Frostbite's coordinate system. It serves as a template parameter to `omath::primitives::Mesh`, enabling engine-specific rotation behavior.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Coordinate System
|
||||||
|
|
||||||
|
**Frostbite Engine** uses:
|
||||||
|
* **Up axis**: +Y
|
||||||
|
* **Forward axis**: +Z
|
||||||
|
* **Right axis**: +X
|
||||||
|
* **Handedness**: Right-handed
|
||||||
|
* **Rotation order**: Pitch (X) → Yaw (Y) → Roll (Z)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
namespace omath::frostbite_engine {
|
||||||
|
|
||||||
|
class MeshTrait final {
|
||||||
|
public:
|
||||||
|
[[nodiscard]]
|
||||||
|
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace omath::frostbite_engine
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Method: `rotation_matrix`
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||||
|
```
|
||||||
|
|
||||||
|
Computes a 4×4 rotation matrix from Frostbite-style Euler angles.
|
||||||
|
|
||||||
|
**Parameters**:
|
||||||
|
* `rotation` — `ViewAngles` containing pitch, yaw, and roll angles
|
||||||
|
|
||||||
|
**Returns**: 4×4 rotation matrix suitable for mesh transformation
|
||||||
|
|
||||||
|
**Implementation**: Delegates to `frostbite_engine::rotation_matrix(rotation)` defined in `formulas.hpp`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### With Mesh
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
using namespace omath::frostbite_engine;
|
||||||
|
|
||||||
|
// Create mesh (MeshTrait is used automatically)
|
||||||
|
Mesh my_mesh(vertices, indices);
|
||||||
|
|
||||||
|
// Set rotation using ViewAngles
|
||||||
|
ViewAngles angles;
|
||||||
|
angles.pitch = PitchAngle::from_degrees(30.0f);
|
||||||
|
angles.yaw = YawAngle::from_degrees(45.0f);
|
||||||
|
angles.roll = RollAngle::from_degrees(0.0f);
|
||||||
|
|
||||||
|
my_mesh.set_rotation(angles);
|
||||||
|
|
||||||
|
// The rotation matrix is computed using MeshTrait::rotation_matrix
|
||||||
|
auto matrix = my_mesh.get_to_world_matrix();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rotation Conventions
|
||||||
|
|
||||||
|
Frostbite uses a right-handed Y-up coordinate system:
|
||||||
|
|
||||||
|
1. **Pitch** (rotation around X-axis / right axis)
|
||||||
|
* Positive pitch looks upward (+Y direction)
|
||||||
|
* Range: typically [-89°, 89°]
|
||||||
|
|
||||||
|
2. **Yaw** (rotation around Y-axis / up axis)
|
||||||
|
* Positive yaw rotates counterclockwise when viewed from above (right-handed)
|
||||||
|
* Range: [-180°, 180°]
|
||||||
|
|
||||||
|
3. **Roll** (rotation around Z-axis / forward axis)
|
||||||
|
* Positive roll tilts right
|
||||||
|
* Range: [-180°, 180°]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Type Alias
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
namespace omath::frostbite_engine {
|
||||||
|
using Mesh = primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Mesh Documentation](../../3d_primitives/mesh.md) - Mesh primitive
|
||||||
|
- [Formulas Documentation](formulas.md) - Frostbite rotation formula
|
||||||
|
- [CameraTrait Documentation](camera_trait.md) - Camera trait
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Last updated: 13 Nov 2025*
|
||||||
119
docs/engines/iw_engine/mesh_trait.md
Normal file
119
docs/engines/iw_engine/mesh_trait.md
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
# `omath::iw_engine::MeshTrait` — mesh transformation trait for IW Engine
|
||||||
|
|
||||||
|
> Header: `omath/engines/iw_engine/traits/mesh_trait.hpp`
|
||||||
|
> Namespace: `omath::iw_engine`
|
||||||
|
> Purpose: provide IW Engine-specific rotation matrix computation for `omath::primitives::Mesh`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
`MeshTrait` is a trait class that provides the `rotation_matrix` function for transforming meshes in IW Engine's (Infinity Ward) coordinate system. It serves as a template parameter to `omath::primitives::Mesh`, enabling engine-specific rotation behavior.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Coordinate System
|
||||||
|
|
||||||
|
**IW Engine** (Call of Duty) uses:
|
||||||
|
* **Up axis**: +Z
|
||||||
|
* **Forward axis**: +Y
|
||||||
|
* **Right axis**: +X
|
||||||
|
* **Handedness**: Right-handed
|
||||||
|
* **Rotation order**: Pitch (X) → Yaw (Z) → Roll (Y)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
namespace omath::iw_engine {
|
||||||
|
|
||||||
|
class MeshTrait final {
|
||||||
|
public:
|
||||||
|
[[nodiscard]]
|
||||||
|
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace omath::iw_engine
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Method: `rotation_matrix`
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||||
|
```
|
||||||
|
|
||||||
|
Computes a 4×4 rotation matrix from IW Engine-style Euler angles.
|
||||||
|
|
||||||
|
**Parameters**:
|
||||||
|
* `rotation` — `ViewAngles` containing pitch, yaw, and roll angles
|
||||||
|
|
||||||
|
**Returns**: 4×4 rotation matrix suitable for mesh transformation
|
||||||
|
|
||||||
|
**Implementation**: Delegates to `iw_engine::rotation_matrix(rotation)` defined in `formulas.hpp`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### With Mesh
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
using namespace omath::iw_engine;
|
||||||
|
|
||||||
|
// Create mesh (MeshTrait is used automatically)
|
||||||
|
Mesh my_mesh(vertices, indices);
|
||||||
|
|
||||||
|
// Set rotation using ViewAngles
|
||||||
|
ViewAngles angles;
|
||||||
|
angles.pitch = PitchAngle::from_degrees(30.0f);
|
||||||
|
angles.yaw = YawAngle::from_degrees(45.0f);
|
||||||
|
angles.roll = RollAngle::from_degrees(0.0f);
|
||||||
|
|
||||||
|
my_mesh.set_rotation(angles);
|
||||||
|
|
||||||
|
// The rotation matrix is computed using MeshTrait::rotation_matrix
|
||||||
|
auto matrix = my_mesh.get_to_world_matrix();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rotation Conventions
|
||||||
|
|
||||||
|
IW Engine uses a right-handed Z-up coordinate system (similar to Source Engine):
|
||||||
|
|
||||||
|
1. **Pitch** (rotation around X-axis / right axis)
|
||||||
|
* Positive pitch looks upward (+Z direction)
|
||||||
|
* Range: typically [-89°, 89°]
|
||||||
|
|
||||||
|
2. **Yaw** (rotation around Z-axis / up axis)
|
||||||
|
* Positive yaw rotates counterclockwise when viewed from above
|
||||||
|
* Range: [-180°, 180°]
|
||||||
|
|
||||||
|
3. **Roll** (rotation around Y-axis / forward axis)
|
||||||
|
* Positive roll tilts right
|
||||||
|
* Range: [-180°, 180°]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Type Alias
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
namespace omath::iw_engine {
|
||||||
|
using Mesh = primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Mesh Documentation](../../3d_primitives/mesh.md) - Mesh primitive
|
||||||
|
- [Formulas Documentation](formulas.md) - IW Engine rotation formula
|
||||||
|
- [CameraTrait Documentation](camera_trait.md) - Camera trait
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Last updated: 13 Nov 2025*
|
||||||
121
docs/engines/opengl_engine/mesh_trait.md
Normal file
121
docs/engines/opengl_engine/mesh_trait.md
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# `omath::opengl_engine::MeshTrait` — mesh transformation trait for OpenGL
|
||||||
|
|
||||||
|
> Header: `omath/engines/opengl_engine/traits/mesh_trait.hpp`
|
||||||
|
> Namespace: `omath::opengl_engine`
|
||||||
|
> Purpose: provide OpenGL-specific rotation matrix computation for `omath::primitives::Mesh`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
`MeshTrait` is a trait class that provides the `rotation_matrix` function for transforming meshes in OpenGL's canonical coordinate system. It serves as a template parameter to `omath::primitives::Mesh`, enabling engine-specific rotation behavior.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Coordinate System
|
||||||
|
|
||||||
|
**OpenGL** (canonical) uses:
|
||||||
|
* **Up axis**: +Y
|
||||||
|
* **Forward axis**: +Z (toward viewer)
|
||||||
|
* **Right axis**: +X
|
||||||
|
* **Handedness**: Right-handed
|
||||||
|
* **Rotation order**: Pitch (X) → Yaw (Y) → Roll (Z)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
namespace omath::opengl_engine {
|
||||||
|
|
||||||
|
class MeshTrait final {
|
||||||
|
public:
|
||||||
|
[[nodiscard]]
|
||||||
|
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace omath::opengl_engine
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Method: `rotation_matrix`
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||||
|
```
|
||||||
|
|
||||||
|
Computes a 4×4 rotation matrix from OpenGL-style Euler angles.
|
||||||
|
|
||||||
|
**Parameters**:
|
||||||
|
* `rotation` — `ViewAngles` containing pitch, yaw, and roll angles
|
||||||
|
|
||||||
|
**Returns**: 4×4 rotation matrix suitable for mesh transformation
|
||||||
|
|
||||||
|
**Implementation**: Delegates to `opengl_engine::rotation_matrix(rotation)` defined in `formulas.hpp`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### With Mesh
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
using namespace omath::opengl_engine;
|
||||||
|
|
||||||
|
// Create mesh (MeshTrait is used automatically)
|
||||||
|
Mesh my_mesh(vertices, indices);
|
||||||
|
|
||||||
|
// Set rotation using ViewAngles
|
||||||
|
ViewAngles angles;
|
||||||
|
angles.pitch = PitchAngle::from_degrees(30.0f);
|
||||||
|
angles.yaw = YawAngle::from_degrees(45.0f);
|
||||||
|
angles.roll = RollAngle::from_degrees(0.0f);
|
||||||
|
|
||||||
|
my_mesh.set_rotation(angles);
|
||||||
|
|
||||||
|
// The rotation matrix is computed using MeshTrait::rotation_matrix
|
||||||
|
auto matrix = my_mesh.get_to_world_matrix();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rotation Conventions
|
||||||
|
|
||||||
|
OpenGL uses a right-handed Y-up coordinate system:
|
||||||
|
|
||||||
|
1. **Pitch** (rotation around X-axis / right axis)
|
||||||
|
* Positive pitch looks upward (+Y direction)
|
||||||
|
* Range: typically [-89°, 89°]
|
||||||
|
|
||||||
|
2. **Yaw** (rotation around Y-axis / up axis)
|
||||||
|
* Positive yaw rotates counterclockwise when viewed from above (right-handed)
|
||||||
|
* Range: [-180°, 180°]
|
||||||
|
|
||||||
|
3. **Roll** (rotation around Z-axis / depth axis)
|
||||||
|
* Positive roll tilts right
|
||||||
|
* Range: [-180°, 180°]
|
||||||
|
|
||||||
|
**Note**: In OpenGL, +Z points toward the viewer in view space, but away from the viewer in world space.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Type Alias
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
namespace omath::opengl_engine {
|
||||||
|
using Mesh = primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Mesh Documentation](../../3d_primitives/mesh.md) - Mesh primitive
|
||||||
|
- [Formulas Documentation](formulas.md) - OpenGL rotation formula
|
||||||
|
- [CameraTrait Documentation](camera_trait.md) - Camera trait
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Last updated: 13 Nov 2025*
|
||||||
182
docs/engines/source_engine/mesh_trait.md
Normal file
182
docs/engines/source_engine/mesh_trait.md
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
# `omath::source_engine::MeshTrait` — mesh transformation trait for Source Engine
|
||||||
|
|
||||||
|
> Header: `omath/engines/source_engine/traits/mesh_trait.hpp`
|
||||||
|
> Namespace: `omath::source_engine`
|
||||||
|
> Purpose: provide Source Engine-specific rotation matrix computation for `omath::primitives::Mesh`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
`MeshTrait` is a trait class that provides the `rotation_matrix` function for transforming meshes in Source Engine's coordinate system. It serves as a template parameter to `omath::primitives::Mesh`, enabling engine-specific rotation behavior.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Coordinate System
|
||||||
|
|
||||||
|
**Source Engine** uses:
|
||||||
|
* **Up axis**: +Z
|
||||||
|
* **Forward axis**: +Y
|
||||||
|
* **Right axis**: +X
|
||||||
|
* **Handedness**: Right-handed
|
||||||
|
* **Rotation order**: Pitch (X) → Yaw (Z) → Roll (Y)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
namespace omath::source_engine {
|
||||||
|
|
||||||
|
class MeshTrait final {
|
||||||
|
public:
|
||||||
|
[[nodiscard]]
|
||||||
|
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace omath::source_engine
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Method: `rotation_matrix`
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||||
|
```
|
||||||
|
|
||||||
|
Computes a 4×4 rotation matrix from Source Engine-style Euler angles.
|
||||||
|
|
||||||
|
**Parameters**:
|
||||||
|
* `rotation` — `ViewAngles` containing pitch, yaw, and roll angles
|
||||||
|
|
||||||
|
**Returns**: 4×4 rotation matrix suitable for mesh transformation
|
||||||
|
|
||||||
|
**Implementation**: Delegates to `source_engine::rotation_matrix(rotation)` defined in `formulas.hpp`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### With Mesh
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
using namespace omath::source_engine;
|
||||||
|
|
||||||
|
// Create mesh (MeshTrait is used automatically)
|
||||||
|
Mesh my_mesh(vertices, indices);
|
||||||
|
|
||||||
|
// Set rotation using ViewAngles
|
||||||
|
ViewAngles angles;
|
||||||
|
angles.pitch = PitchAngle::from_degrees(30.0f);
|
||||||
|
angles.yaw = YawAngle::from_degrees(45.0f);
|
||||||
|
angles.roll = RollAngle::from_degrees(0.0f);
|
||||||
|
|
||||||
|
my_mesh.set_rotation(angles);
|
||||||
|
|
||||||
|
// The rotation matrix is computed using MeshTrait::rotation_matrix
|
||||||
|
auto matrix = my_mesh.get_to_world_matrix();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Direct Usage
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
using namespace omath::source_engine;
|
||||||
|
|
||||||
|
ViewAngles angles;
|
||||||
|
angles.pitch = PitchAngle::from_degrees(45.0f);
|
||||||
|
angles.yaw = YawAngle::from_degrees(90.0f);
|
||||||
|
angles.roll = RollAngle::from_degrees(0.0f);
|
||||||
|
|
||||||
|
Mat4X4 rot_matrix = MeshTrait::rotation_matrix(angles);
|
||||||
|
|
||||||
|
// Use the matrix directly
|
||||||
|
Vector3<float> local_point{1, 0, 0};
|
||||||
|
auto rotated = rot_matrix * mat_column_from_vector(local_point);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rotation Conventions
|
||||||
|
|
||||||
|
The rotation matrix is built following Source Engine's conventions:
|
||||||
|
|
||||||
|
1. **Pitch** (rotation around X-axis / right axis)
|
||||||
|
* Positive pitch looks upward (+Z direction)
|
||||||
|
* Range: typically [-89°, 89°]
|
||||||
|
|
||||||
|
2. **Yaw** (rotation around Z-axis / up axis)
|
||||||
|
* Positive yaw rotates counterclockwise when viewed from above
|
||||||
|
* Range: [-180°, 180°]
|
||||||
|
|
||||||
|
3. **Roll** (rotation around Y-axis / forward axis)
|
||||||
|
* Positive roll tilts right
|
||||||
|
* Range: [-180°, 180°]
|
||||||
|
|
||||||
|
**Composition**: The matrices are combined in the order Pitch × Yaw × Roll, producing a rotation that:
|
||||||
|
* First applies roll around the forward axis
|
||||||
|
* Then applies yaw around the up axis
|
||||||
|
* Finally applies pitch around the right axis
|
||||||
|
|
||||||
|
This matches Source Engine's internal rotation order.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Functions
|
||||||
|
|
||||||
|
The trait delegates to the formula defined in `formulas.hpp`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
[[nodiscard]]
|
||||||
|
Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept;
|
||||||
|
```
|
||||||
|
|
||||||
|
See [Formulas Documentation](formulas.md) for details on the rotation matrix computation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Type Alias
|
||||||
|
|
||||||
|
The Source Engine mesh type is pre-defined:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
namespace omath::source_engine {
|
||||||
|
using Mesh = primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use this alias to ensure correct trait usage:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
using namespace omath::source_engine;
|
||||||
|
|
||||||
|
// Correct: uses Source Engine trait
|
||||||
|
Mesh my_mesh(vbo, vao);
|
||||||
|
|
||||||
|
// Avoid: manually specifying template parameters
|
||||||
|
primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float> verbose_mesh(vbo, vao);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
* **Angle ranges**: Ensure angles are within valid ranges (pitch: [-89°, 89°], yaw/roll: [-180°, 180°])
|
||||||
|
* **Performance**: Matrix computation is O(1) with ~64 floating-point operations
|
||||||
|
* **Caching**: The mesh caches the transformation matrix; recomputed only when rotation changes
|
||||||
|
* **Compatibility**: Works with all Source Engine games (CS:GO, TF2, Portal, Half-Life 2, etc.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Mesh Documentation](../../3d_primitives/mesh.md) - Mesh primitive using this trait
|
||||||
|
- [MeshCollider Documentation](../../collision/mesh_collider.md) - Collision wrapper for meshes
|
||||||
|
- [Formulas Documentation](formulas.md) - Source Engine rotation formula
|
||||||
|
- [CameraTrait Documentation](camera_trait.md) - Camera transformation trait
|
||||||
|
- [Constants Documentation](constants.md) - Source Engine constants
|
||||||
|
- [API Overview](../../api_overview.md) - High-level API reference
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Last updated: 13 Nov 2025*
|
||||||
119
docs/engines/unity_engine/mesh_trait.md
Normal file
119
docs/engines/unity_engine/mesh_trait.md
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
# `omath::unity_engine::MeshTrait` — mesh transformation trait for Unity Engine
|
||||||
|
|
||||||
|
> Header: `omath/engines/unity_engine/traits/mesh_trait.hpp`
|
||||||
|
> Namespace: `omath::unity_engine`
|
||||||
|
> Purpose: provide Unity Engine-specific rotation matrix computation for `omath::primitives::Mesh`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
`MeshTrait` is a trait class that provides the `rotation_matrix` function for transforming meshes in Unity's coordinate system. It serves as a template parameter to `omath::primitives::Mesh`, enabling engine-specific rotation behavior.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Coordinate System
|
||||||
|
|
||||||
|
**Unity Engine** uses:
|
||||||
|
* **Up axis**: +Y
|
||||||
|
* **Forward axis**: +Z
|
||||||
|
* **Right axis**: +X
|
||||||
|
* **Handedness**: Left-handed
|
||||||
|
* **Rotation order**: Pitch (X) → Yaw (Y) → Roll (Z)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
namespace omath::unity_engine {
|
||||||
|
|
||||||
|
class MeshTrait final {
|
||||||
|
public:
|
||||||
|
[[nodiscard]]
|
||||||
|
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace omath::unity_engine
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Method: `rotation_matrix`
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||||
|
```
|
||||||
|
|
||||||
|
Computes a 4×4 rotation matrix from Unity-style Euler angles.
|
||||||
|
|
||||||
|
**Parameters**:
|
||||||
|
* `rotation` — `ViewAngles` containing pitch, yaw, and roll angles
|
||||||
|
|
||||||
|
**Returns**: 4×4 rotation matrix suitable for mesh transformation
|
||||||
|
|
||||||
|
**Implementation**: Delegates to `unity_engine::rotation_matrix(rotation)` defined in `formulas.hpp`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### With Mesh
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
using namespace omath::unity_engine;
|
||||||
|
|
||||||
|
// Create mesh (MeshTrait is used automatically)
|
||||||
|
Mesh my_mesh(vertices, indices);
|
||||||
|
|
||||||
|
// Set rotation using ViewAngles
|
||||||
|
ViewAngles angles;
|
||||||
|
angles.pitch = PitchAngle::from_degrees(30.0f);
|
||||||
|
angles.yaw = YawAngle::from_degrees(45.0f);
|
||||||
|
angles.roll = RollAngle::from_degrees(0.0f);
|
||||||
|
|
||||||
|
my_mesh.set_rotation(angles);
|
||||||
|
|
||||||
|
// The rotation matrix is computed using MeshTrait::rotation_matrix
|
||||||
|
auto matrix = my_mesh.get_to_world_matrix();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rotation Conventions
|
||||||
|
|
||||||
|
Unity uses a left-handed coordinate system with Y-up:
|
||||||
|
|
||||||
|
1. **Pitch** (rotation around X-axis / right axis)
|
||||||
|
* Positive pitch looks upward (+Y direction)
|
||||||
|
* Range: typically [-89°, 89°]
|
||||||
|
|
||||||
|
2. **Yaw** (rotation around Y-axis / up axis)
|
||||||
|
* Positive yaw rotates clockwise when viewed from above (left-handed)
|
||||||
|
* Range: [-180°, 180°]
|
||||||
|
|
||||||
|
3. **Roll** (rotation around Z-axis / forward axis)
|
||||||
|
* Positive roll tilts right
|
||||||
|
* Range: [-180°, 180°]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Type Alias
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
namespace omath::unity_engine {
|
||||||
|
using Mesh = primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Mesh Documentation](../../3d_primitives/mesh.md) - Mesh primitive
|
||||||
|
- [Formulas Documentation](formulas.md) - Unity rotation formula
|
||||||
|
- [CameraTrait Documentation](camera_trait.md) - Camera trait
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Last updated: 13 Nov 2025*
|
||||||
121
docs/engines/unreal_engine/mesh_trait.md
Normal file
121
docs/engines/unreal_engine/mesh_trait.md
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# `omath::unreal_engine::MeshTrait` — mesh transformation trait for Unreal Engine
|
||||||
|
|
||||||
|
> Header: `omath/engines/unreal_engine/traits/mesh_trait.hpp`
|
||||||
|
> Namespace: `omath::unreal_engine`
|
||||||
|
> Purpose: provide Unreal Engine-specific rotation matrix computation for `omath::primitives::Mesh`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
`MeshTrait` is a trait class that provides the `rotation_matrix` function for transforming meshes in Unreal Engine's coordinate system. It serves as a template parameter to `omath::primitives::Mesh`, enabling engine-specific rotation behavior.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Coordinate System
|
||||||
|
|
||||||
|
**Unreal Engine** uses:
|
||||||
|
* **Up axis**: +Z
|
||||||
|
* **Forward axis**: +X
|
||||||
|
* **Right axis**: +Y
|
||||||
|
* **Handedness**: Left-handed
|
||||||
|
* **Rotation order**: Roll (Y) → Pitch (X) → Yaw (Z)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
namespace omath::unreal_engine {
|
||||||
|
|
||||||
|
class MeshTrait final {
|
||||||
|
public:
|
||||||
|
[[nodiscard]]
|
||||||
|
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace omath::unreal_engine
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Method: `rotation_matrix`
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
static Mat4X4 rotation_matrix(const ViewAngles& rotation);
|
||||||
|
```
|
||||||
|
|
||||||
|
Computes a 4×4 rotation matrix from Unreal-style Euler angles.
|
||||||
|
|
||||||
|
**Parameters**:
|
||||||
|
* `rotation` — `ViewAngles` containing pitch, yaw, and roll angles
|
||||||
|
|
||||||
|
**Returns**: 4×4 rotation matrix suitable for mesh transformation
|
||||||
|
|
||||||
|
**Implementation**: Delegates to `unreal_engine::rotation_matrix(rotation)` defined in `formulas.hpp`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### With Mesh
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
using namespace omath::unreal_engine;
|
||||||
|
|
||||||
|
// Create mesh (MeshTrait is used automatically)
|
||||||
|
Mesh my_mesh(vertices, indices);
|
||||||
|
|
||||||
|
// Set rotation using ViewAngles
|
||||||
|
ViewAngles angles;
|
||||||
|
angles.pitch = PitchAngle::from_degrees(30.0f);
|
||||||
|
angles.yaw = YawAngle::from_degrees(45.0f);
|
||||||
|
angles.roll = RollAngle::from_degrees(0.0f);
|
||||||
|
|
||||||
|
my_mesh.set_rotation(angles);
|
||||||
|
|
||||||
|
// The rotation matrix is computed using MeshTrait::rotation_matrix
|
||||||
|
auto matrix = my_mesh.get_to_world_matrix();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rotation Conventions
|
||||||
|
|
||||||
|
Unreal uses a left-handed Z-up coordinate system:
|
||||||
|
|
||||||
|
1. **Roll** (rotation around Y-axis / right axis)
|
||||||
|
* Positive roll rotates forward axis upward
|
||||||
|
* Range: [-180°, 180°]
|
||||||
|
|
||||||
|
2. **Pitch** (rotation around X-axis / forward axis)
|
||||||
|
* Positive pitch looks upward
|
||||||
|
* Range: typically [-89°, 89°]
|
||||||
|
|
||||||
|
3. **Yaw** (rotation around Z-axis / up axis)
|
||||||
|
* Positive yaw rotates clockwise when viewed from above (left-handed)
|
||||||
|
* Range: [-180°, 180°]
|
||||||
|
|
||||||
|
**Note**: Unreal applies rotations in Roll-Pitch-Yaw order, different from most other engines.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Type Alias
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
namespace omath::unreal_engine {
|
||||||
|
using Mesh = primitives::Mesh<Mat4X4, ViewAngles, MeshTrait, float>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Mesh Documentation](../../3d_primitives/mesh.md) - Mesh primitive
|
||||||
|
- [Formulas Documentation](formulas.md) - Unreal rotation formula
|
||||||
|
- [CameraTrait Documentation](camera_trait.md) - Camera trait
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Last updated: 13 Nov 2025*
|
||||||
Reference in New Issue
Block a user