mirror of
https://github.com/orange-cpp/omath.git
synced 2026-02-13 07:03:25 +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:
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*
|
||||
Reference in New Issue
Block a user