mirror of
https://github.com/orange-cpp/omath.git
synced 2026-02-12 22:53:27 +00:00
Documents view angle struct and related API
Adds documentation for the `omath::ViewAngles` struct, clarifying its purpose, common usage patterns, and the definition of the types of pitch, yaw and roll. Also, adds short explanations of how to use ViewAngles and what tradeoffs exist between using raw float types and strongly typed Angle<> types.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# `omath::Mat`
|
||||
# `omath::Mat` — Matrix class (C++20/23)
|
||||
|
||||
> Header: your project’s `mat.hpp` (requires `vector3.hpp`)
|
||||
> Namespace: `omath`
|
||||
|
||||
188
docs/pathfinding/a_star.md
Normal file
188
docs/pathfinding/a_star.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# `omath::pathfinding::Astar` — Pathfinding over a navigation mesh
|
||||
|
||||
> Header: your project’s `pathfinding/astar.hpp`
|
||||
> Namespace: `omath::pathfinding`
|
||||
> Inputs: start/end as `Vector3<float>`, a `NavigationMesh`
|
||||
> Output: ordered list of waypoints `std::vector<Vector3<float>>`
|
||||
|
||||
`Astar` exposes a single public function, `find_path`, that computes a path of 3D waypoints on a navigation mesh. Internally it reconstructs the result with `reconstruct_final_path` from a closed set keyed by `Vector3<float>`.
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```cpp
|
||||
namespace omath::pathfinding {
|
||||
|
||||
struct PathNode; // holds per-node search data (see "Expected PathNode fields")
|
||||
|
||||
class Astar final {
|
||||
public:
|
||||
[[nodiscard]]
|
||||
static std::vector<Vector3<float>>
|
||||
find_path(const Vector3<float>& start,
|
||||
const Vector3<float>& end,
|
||||
const NavigationMesh& nav_mesh) noexcept;
|
||||
|
||||
private:
|
||||
[[nodiscard]]
|
||||
static std::vector<Vector3<float>>
|
||||
reconstruct_final_path(
|
||||
const std::unordered_map<Vector3<float>, PathNode>& closed_list,
|
||||
const Vector3<float>& current) noexcept;
|
||||
};
|
||||
|
||||
} // namespace omath::pathfinding
|
||||
```
|
||||
|
||||
### Semantics
|
||||
|
||||
* Returns a **polyline** of 3D points from `start` to `end`.
|
||||
* If no path exists, the function typically returns an **empty vector** (behavior depends on implementation details; keep this contract in unit tests).
|
||||
|
||||
---
|
||||
|
||||
## What `NavigationMesh` is expected to provide
|
||||
|
||||
The header doesn’t constrain `NavigationMesh`, but for A* it commonly needs:
|
||||
|
||||
* **Neighborhood queries**: given a position or node key → iterable neighbors.
|
||||
* **Traversal cost**: `g(u,v)` (often Euclidean distance or edge weight).
|
||||
* **Heuristic**: `h(x,end)` (commonly straight-line distance on the mesh).
|
||||
* **Projection / snap**: the ability to map `start`/`end` to valid nodes/points on the mesh (if they are off-mesh).
|
||||
|
||||
> If your `NavigationMesh` doesn’t directly expose these, `Astar::find_path` likely does the adapter work (snapping to the nearest convex polygon/portal nodes and expanding across adjacency).
|
||||
|
||||
---
|
||||
|
||||
## Expected `PathNode` fields
|
||||
|
||||
Although not visible here, `PathNode` typically carries:
|
||||
|
||||
* `Vector3<float> parent;` — predecessor position or key for backtracking
|
||||
* `float g;` — cost from `start`
|
||||
* `float h;` — heuristic to `end`
|
||||
* `float f;` — `g + h`
|
||||
|
||||
`reconstruct_final_path(closed_list, current)` walks `parent` links from `current` back to the start, **reverses** the chain, and returns the path.
|
||||
|
||||
---
|
||||
|
||||
## Heuristic & optimality
|
||||
|
||||
* Use an **admissible** heuristic (never overestimates true cost) to keep A* optimal.
|
||||
The usual choice is **Euclidean distance** in 3D:
|
||||
|
||||
```cpp
|
||||
h(x, goal) = (goal - x).length();
|
||||
```
|
||||
* For best performance, make it **consistent** (triangle inequality holds). Euclidean distance is consistent on standard navmeshes.
|
||||
|
||||
---
|
||||
|
||||
## Complexity
|
||||
|
||||
Let `V` be explored vertices (or portal nodes) and `E` the traversed edges.
|
||||
|
||||
* With a binary heap open list: **O(E log V)** time, **O(V)** memory.
|
||||
* With a d-ary heap or pairing heap you may reduce practical constants.
|
||||
|
||||
---
|
||||
|
||||
## Typical usage
|
||||
|
||||
```cpp
|
||||
#include "omath/pathfinding/astar.hpp"
|
||||
#include "omath/pathfinding/navigation_mesh.hpp"
|
||||
|
||||
using omath::Vector3;
|
||||
using omath::pathfinding::Astar;
|
||||
|
||||
NavigationMesh nav = /* ... load/build mesh ... */;
|
||||
|
||||
Vector3<float> start{2.5f, 0.0f, -1.0f};
|
||||
Vector3<float> goal {40.0f, 0.0f, 12.0f};
|
||||
|
||||
auto path = Astar::find_path(start, goal, nav);
|
||||
|
||||
if (!path.empty()) {
|
||||
// feed to your agent/renderer
|
||||
for (const auto& p : path) {
|
||||
// draw waypoint p or push to steering
|
||||
}
|
||||
} else {
|
||||
// handle "no path" (e.g., unreachable or disconnected mesh)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes & recommendations
|
||||
|
||||
* **Start/end snapping**: If `start` or `end` are outside the mesh, decide whether to snap to the nearest polygon/portal or fail early. Keep this behavior consistent and document it where `NavigationMesh` is defined.
|
||||
* **Numerical stability**: Prefer squared distances when only comparing (`dist2`) to avoid unnecessary `sqrt`.
|
||||
* **Tie-breaking**: When `f` ties are common (grid-like graphs), bias toward larger `g` or smaller `h` to reduce zig-zagging.
|
||||
* **Smoothing**: A* returns a polyline that may hug polygon edges. Consider:
|
||||
|
||||
* **String pulling / Funnel algorithm** over the corridor of polygons to get a straightened path.
|
||||
* **Raycast smoothing** (visibility checks) to remove redundant interior points.
|
||||
* **Hashing `Vector3<float>`**: Your repo defines `std::hash<omath::Vector3<float>>`. Ensure equality/precision rules for using float keys are acceptable (or use discrete node IDs instead).
|
||||
|
||||
---
|
||||
|
||||
## Testing checklist
|
||||
|
||||
* Start/end on the **same polygon** → direct path of 2 points.
|
||||
* **Disconnected components** → empty result.
|
||||
* **Narrow corridors** → path stays inside.
|
||||
* **Obstacles blocking** → no path.
|
||||
* **Floating-point noise** → still reconstructs a valid chain from parents.
|
||||
|
||||
---
|
||||
|
||||
## Minimal pseudo-implementation outline (for reference)
|
||||
|
||||
```cpp
|
||||
// Pseudocode only — matches the header’s intent
|
||||
std::vector<Vec3> find_path(start, goal, mesh) {
|
||||
auto [snode, gnode] = mesh.snap_to_nodes(start, goal);
|
||||
OpenSet open; // min-heap by f
|
||||
std::unordered_map<Vec3, PathNode> closed;
|
||||
|
||||
open.push({snode, g=0, h=heuristic(snode, gnode)});
|
||||
parents.clear();
|
||||
|
||||
while (!open.empty()) {
|
||||
auto current = open.pop_min(); // node with lowest f
|
||||
|
||||
if (current.pos == gnode.pos)
|
||||
return reconstruct_final_path(closed, current.pos);
|
||||
|
||||
for (auto [nbr, cost] : mesh.neighbors(current.pos)) {
|
||||
float tentative_g = current.g + cost;
|
||||
if (auto it = closed.find(nbr); it == closed.end() || tentative_g < it->second.g) {
|
||||
closed[nbr] = { .parent = current.pos,
|
||||
.g = tentative_g,
|
||||
.h = heuristic(nbr, gnode.pos),
|
||||
.f = tentative_g + heuristic(nbr, gnode.pos) };
|
||||
open.push(closed[nbr]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return {}; // no path
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
* **Why return `std::vector<Vector3<float>>` and not polygon IDs?**
|
||||
Waypoints are directly usable by agents/steering and for rendering. If you also need the corridor (polygon chain), extend the API or `PathNode` to store it.
|
||||
|
||||
* **Does `find_path` modify the mesh?**
|
||||
No; it should be a read-only search over `NavigationMesh`.
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 31 Oct 2025*
|
||||
113
docs/pathfinding/navigation_mesh.md
Normal file
113
docs/pathfinding/navigation_mesh.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# `omath::pathfinding::NavigationMesh` — Lightweight vertex graph for A*
|
||||
|
||||
> Header: your project’s `pathfinding/navigation_mesh.hpp`
|
||||
> Namespace: `omath::pathfinding`
|
||||
> Nodes: `Vector3<float>` (3D points)
|
||||
> Storage: adjacency map `unordered_map<Vector3<float>, std::vector<Vector3<float>>>`
|
||||
|
||||
A minimal navigation mesh represented as a **vertex/edge graph**. Each vertex is a `Vector3<float>` and neighbors are stored in an adjacency list. Designed to pair with `Astar::find_path`.
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```cpp
|
||||
class NavigationMesh final {
|
||||
public:
|
||||
// Nearest graph vertex to an arbitrary 3D point.
|
||||
// On success -> closest vertex; on failure -> std::string error (e.g., empty mesh).
|
||||
[[nodiscard]]
|
||||
std::expected<Vector3<float>, std::string>
|
||||
get_closest_vertex(const Vector3<float>& point) const noexcept;
|
||||
|
||||
// Read-only neighbor list for a vertex key.
|
||||
// If vertex is absent, implementation should return an empty list (see notes).
|
||||
[[nodiscard]]
|
||||
const std::vector<Vector3<float>>&
|
||||
get_neighbors(const Vector3<float>& vertex) const noexcept;
|
||||
|
||||
// True if the graph has no vertices/edges.
|
||||
[[nodiscard]]
|
||||
bool empty() const;
|
||||
|
||||
// Serialize/deserialize the graph (opaque binary).
|
||||
[[nodiscard]] std::vector<uint8_t> serialize() const noexcept;
|
||||
void deserialize(const std::vector<uint8_t>& raw) noexcept;
|
||||
|
||||
// Public adjacency (vertex -> neighbors)
|
||||
std::unordered_map<Vector3<float>, std::vector<Vector3<float>>> m_vertex_map;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick start
|
||||
|
||||
```cpp
|
||||
using omath::Vector3;
|
||||
using omath::pathfinding::NavigationMesh;
|
||||
|
||||
// Build a tiny mesh (triangle)
|
||||
NavigationMesh nav;
|
||||
nav.m_vertex_map[ {0,0,0} ] = { {1,0,0}, {0,0,1} };
|
||||
nav.m_vertex_map[ {1,0,0} ] = { {0,0,0}, {0,0,1} };
|
||||
nav.m_vertex_map[ {0,0,1} ] = { {0,0,0}, {1,0,0} };
|
||||
|
||||
// Query the closest node to an arbitrary point
|
||||
auto q = nav.get_closest_vertex({0.3f, 0.0f, 0.2f});
|
||||
if (q) {
|
||||
const auto& v = *q;
|
||||
const auto& nbrs = nav.get_neighbors(v);
|
||||
(void)nbrs;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Semantics & expectations
|
||||
|
||||
* **Nearest vertex**
|
||||
`get_closest_vertex(p)` should scan known vertices and return the one with minimal Euclidean distance to `p`. If the mesh is empty, expect an error (`unexpected` with a message).
|
||||
|
||||
* **Neighbors**
|
||||
`get_neighbors(v)` returns the adjacency list for `v`. If `v` is not present, a conventional behavior is to return a **reference to a static empty vector** (since the API is `noexcept` and returns by reference). Verify in your implementation.
|
||||
|
||||
* **Graph invariants** (recommended)
|
||||
|
||||
* Neighbor links are **symmetric** for undirected navigation (if `u` has `v`, then `v` has `u`).
|
||||
* No self-loops unless explicitly desired.
|
||||
* Vertices are unique keys; hashing uses `std::hash<Vector3<float>>` (be mindful of floating-point equality).
|
||||
|
||||
---
|
||||
|
||||
## Serialization
|
||||
|
||||
* `serialize()` → opaque, implementation-defined binary of the current `m_vertex_map`.
|
||||
* `deserialize(raw)` → restores the internal map from `raw`.
|
||||
Keep versioning in mind if you evolve the format (e.g., add a header/magic/version).
|
||||
|
||||
---
|
||||
|
||||
## Performance
|
||||
|
||||
Let `V = m_vertex_map.size()` and `E = Σ|neighbors(v)|`.
|
||||
|
||||
* `get_closest_vertex`: **O(V)** (linear scan) unless you back it with a spatial index (KD-tree, grid, etc.).
|
||||
* `get_neighbors`: **O(1)** average (hash lookup).
|
||||
* Memory: **O(V + E)**.
|
||||
|
||||
---
|
||||
|
||||
## Usage notes
|
||||
|
||||
* **Floating-point keys**: Using `Vector3<float>` as an unordered_map key relies on your `std::hash<omath::Vector3<float>>` and exact `operator==`. Avoid building meshes with numerically “close but not equal” duplicates; consider canonicalizing or using integer IDs if needed.
|
||||
* **Pathfinding**: Pair with `Astar::find_path(start, end, nav)`; the A* heuristic can use straight-line distance between vertex positions.
|
||||
|
||||
---
|
||||
|
||||
## Minimal test ideas
|
||||
|
||||
* Empty mesh → `get_closest_vertex` returns error; `empty() == true`.
|
||||
* Single vertex → nearest always that vertex; neighbors empty.
|
||||
* Symmetric edges → `get_neighbors(a)` contains `b` and vice versa.
|
||||
* Serialization round-trip preserves vertex/edge counts and neighbor lists.
|
||||
161
docs/projectile_prediction/proj_pred_engine_avx2.md
Normal file
161
docs/projectile_prediction/proj_pred_engine_avx2.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# `omath::projectile_prediction::ProjPredEngineAvx2` — AVX2-accelerated ballistic aim solver
|
||||
|
||||
> Header: your project’s `projectile_prediction/proj_pred_engine_avx2.hpp`
|
||||
> Namespace: `omath::projectile_prediction`
|
||||
> Inherits: `ProjPredEngineInterface`
|
||||
> Depends on: `Vector3<float>`, `Projectile`, `Target`
|
||||
> CPU: Uses AVX2 when available; falls back to scalar elsewhere (fields are marked `[[maybe_unused]]` for non-x86/AVX2 builds).
|
||||
|
||||
This engine computes a **world-space aim point** (and implicitly the firing **yaw/pitch**) to intersect a moving target under a **constant gravity** model and **constant muzzle speed**. It typically scans candidate times of flight and solves for the elevation (`pitch`) that makes the vertical and horizontal kinematics meet at the same time.
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```cpp
|
||||
class ProjPredEngineAvx2 final : public ProjPredEngineInterface {
|
||||
public:
|
||||
[[nodiscard]]
|
||||
std::optional<Vector3<float>>
|
||||
maybe_calculate_aim_point(const Projectile& projectile,
|
||||
const Target& target) const override;
|
||||
|
||||
ProjPredEngineAvx2(float gravity_constant,
|
||||
float simulation_time_step,
|
||||
float maximum_simulation_time);
|
||||
~ProjPredEngineAvx2() override = default;
|
||||
|
||||
private:
|
||||
// Solve for pitch at a fixed time-of-flight t.
|
||||
[[nodiscard]]
|
||||
static std::optional<float>
|
||||
calculate_pitch(const Vector3<float>& proj_origin,
|
||||
const Vector3<float>& target_pos,
|
||||
float bullet_gravity, float v0, float time);
|
||||
|
||||
// Tunables (may be unused on non-AVX2 builds)
|
||||
[[maybe_unused]] const float m_gravity_constant; // |g| (e.g., 9.81)
|
||||
[[maybe_unused]] const float m_simulation_time_step; // Δt (e.g., 1/240 s)
|
||||
[[maybe_unused]] const float m_maximum_simulation_time; // Tmax (e.g., 3 s)
|
||||
};
|
||||
```
|
||||
|
||||
### Parameters (constructor)
|
||||
|
||||
* `gravity_constant` — magnitude of gravity (units consistent with your world, e.g., **m/s²**).
|
||||
* `simulation_time_step` — Δt used to scan candidate intercept times.
|
||||
* `maximum_simulation_time` — cap on time of flight; larger allows longer-range solutions but increases cost.
|
||||
|
||||
### Return (solver)
|
||||
|
||||
* `maybe_calculate_aim_point(...)`
|
||||
|
||||
* **`Vector3<float>`**: a world-space **aim point** yielding an intercept under the model.
|
||||
* **`std::nullopt`**: no feasible solution (e.g., target receding too fast, out of range, or kinematics inconsistent).
|
||||
|
||||
---
|
||||
|
||||
## How it solves (expected flow)
|
||||
|
||||
1. **Predict target at time `t`** (constant-velocity model unless your `Target` carries more):
|
||||
|
||||
```
|
||||
T(t) = target.position + target.velocity * t
|
||||
```
|
||||
2. **Horizontal/vertical kinematics at fixed `t`** with muzzle speed `v0` and gravity `g`:
|
||||
|
||||
* Let `Δ = T(t) - proj_origin`, `d = length(Δ.xz)`, `h = Δ.y`.
|
||||
* Required initial components:
|
||||
|
||||
```
|
||||
cosθ = d / (v0 * t)
|
||||
sinθ = (h + 0.5 * g * t^2) / (v0 * t)
|
||||
```
|
||||
* If `cosθ` ∈ [−1,1] and `sinθ` ∈ [−1,1] and `sin²θ + cos²θ ≈ 1`, then
|
||||
|
||||
```
|
||||
θ = atan2(sinθ, cosθ)
|
||||
```
|
||||
|
||||
That is what `calculate_pitch(...)` returns on success.
|
||||
3. **Yaw** is the azimuth toward `Δ.xz`.
|
||||
4. **Pick the earliest feasible `t`** in `[Δt, Tmax]` (scanned in steps of `Δt`; AVX2 batches several `t` at once).
|
||||
5. **Return the aim point.** Common choices:
|
||||
|
||||
* The **impact point** `T(t*)` (useful as a HUD marker), or
|
||||
* A point along the **initial firing ray** at some convenient range using `(yaw, pitch)`; both are consistent—pick the convention your caller expects.
|
||||
|
||||
> The private `calculate_pitch(...)` matches step **2** and returns `nullopt` if the trigonometric constraints are violated for that `t`.
|
||||
|
||||
---
|
||||
|
||||
## AVX2 notes
|
||||
|
||||
* On x86/x64 with AVX2, candidate times `t` can be evaluated **8 at a time** using FMA (great for dense scans).
|
||||
* On ARM/ARM64 (no AVX2), code falls back to scalar math; the `[[maybe_unused]]` members acknowledge compilation without SIMD.
|
||||
|
||||
---
|
||||
|
||||
## Usage example
|
||||
|
||||
```cpp
|
||||
using namespace omath::projectile_prediction;
|
||||
|
||||
ProjPredEngineAvx2 solver(
|
||||
/*gravity*/ 9.81f,
|
||||
/*dt*/ 1.0f/240.0f,
|
||||
/*Tmax*/ 3.0f
|
||||
);
|
||||
|
||||
Projectile proj; // fill: origin, muzzle_speed, etc.
|
||||
Target tgt; // fill: position, velocity
|
||||
|
||||
if (auto aim = solver.maybe_calculate_aim_point(proj, tgt)) {
|
||||
// Aim your weapon at *aim and fire with muzzle speed proj.v0
|
||||
// If you need yaw/pitch explicitly, replicate the pitch solve and azimuth.
|
||||
} else {
|
||||
// No solution (out of envelope) — pick a fallback
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Edge cases & failure modes
|
||||
|
||||
* **Zero or tiny `v0`** → no solution.
|
||||
* **Target collinear & receding faster than `v0`** → no solution.
|
||||
* **`t` constraints**: if viable solutions exist only beyond `Tmax`, you’ll get `nullopt`.
|
||||
* **Geometric infeasibility** at a given `t` (e.g., `d > v0*t`) causes `calculate_pitch` to fail that sample.
|
||||
* **Numerical tolerance**: check `sin²θ + cos²θ` against 1 with a small epsilon (e.g., `1e-3`).
|
||||
|
||||
---
|
||||
|
||||
## Performance & tuning
|
||||
|
||||
* Work is roughly `O(Nt)` where `Nt ≈ Tmax / Δt`.
|
||||
* Smaller `Δt` → better accuracy, higher cost. With AVX2 you can afford smaller steps.
|
||||
* If you frequently miss solutions **between** steps, consider:
|
||||
|
||||
* **Coarse-to-fine**: coarse scan, then local refine around the best `t`.
|
||||
* **Newton on time**: root-find `‖horizontal‖ − v0 t cosθ(t) = 0` shaped from the kinematics.
|
||||
|
||||
---
|
||||
|
||||
## Testing checklist
|
||||
|
||||
* **Stationary target** at same height → θ ≈ 0, aim point ≈ target.
|
||||
* **Higher target** → positive pitch; **lower target** → negative pitch.
|
||||
* **Perpendicular moving target** → feasible at moderate speeds.
|
||||
* **Very fast receding target** → `nullopt`.
|
||||
* **Boundary**: `d ≈ v0*Tmax` and `h` large → verify pass/fail around thresholds.
|
||||
|
||||
---
|
||||
|
||||
## See also
|
||||
|
||||
* `ProjPredEngineInterface` — base interface and general contract
|
||||
* `Projectile`, `Target` — data carriers for solver inputs (speed, origin, position, velocity, etc.)
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 1 Nov 2025*
|
||||
184
docs/projectile_prediction/proj_pred_engine_legacy.md
Normal file
184
docs/projectile_prediction/proj_pred_engine_legacy.md
Normal file
@@ -0,0 +1,184 @@
|
||||
# `omath::projectile_prediction::ProjPredEngineLegacy` — Legacy trait-based aim solver
|
||||
|
||||
> Header: `omath/projectile_prediction/proj_pred_engine_legacy.hpp`
|
||||
> Namespace: `omath::projectile_prediction`
|
||||
> Inherits: `ProjPredEngineInterface`
|
||||
> Template param (default): `EngineTrait = source_engine::PredEngineTrait`
|
||||
> Purpose: compute a world-space **aim point** to hit a (possibly moving) target using a **discrete time scan** and a **closed-form ballistic pitch** under constant gravity.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
`ProjPredEngineLegacy` is a portable, trait-driven projectile lead solver. At each simulation time step `t` it:
|
||||
|
||||
1. **Predicts target position** with `EngineTrait::predict_target_position(target, t, g)`.
|
||||
2. **Computes launch pitch** via a gravity-aware closed form (or a direct angle if gravity is zero).
|
||||
3. **Validates** that a projectile fired with that pitch (and direct yaw) actually reaches the predicted target within a **distance tolerance** at time `t`.
|
||||
4. On success, **returns an aim point** computed by `EngineTrait::calc_viewpoint_from_angles(...)`.
|
||||
|
||||
If no time step yields a feasible solution up to `maximum_simulation_time`, returns `std::nullopt`.
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```cpp
|
||||
template<class EngineTrait = source_engine::PredEngineTrait>
|
||||
requires PredEngineConcept<EngineTrait>
|
||||
class ProjPredEngineLegacy final : public ProjPredEngineInterface {
|
||||
public:
|
||||
ProjPredEngineLegacy(float gravity_constant,
|
||||
float simulation_time_step,
|
||||
float maximum_simulation_time,
|
||||
float distance_tolerance);
|
||||
|
||||
[[nodiscard]]
|
||||
std::optional<Vector3<float>>
|
||||
maybe_calculate_aim_point(const Projectile& projectile,
|
||||
const Target& target) const override;
|
||||
|
||||
private:
|
||||
// Closed-form ballistic pitch solver (internal)
|
||||
std::optional<float>
|
||||
maybe_calculate_projectile_launch_pitch_angle(const Projectile& projectile,
|
||||
const Vector3<float>& target_position) const noexcept;
|
||||
|
||||
bool is_projectile_reached_target(const Vector3<float>& target_position,
|
||||
const Projectile& projectile,
|
||||
float pitch, float time) const noexcept;
|
||||
|
||||
const float m_gravity_constant;
|
||||
const float m_simulation_time_step;
|
||||
const float m_maximum_simulation_time;
|
||||
const float m_distance_tolerance;
|
||||
};
|
||||
```
|
||||
|
||||
### Constructor parameters
|
||||
|
||||
* `gravity_constant` — magnitude of gravity (e.g., `9.81f`), world units/s².
|
||||
* `simulation_time_step` — Δt for the scan (e.g., `1/240.f`).
|
||||
* `maximum_simulation_time` — search horizon in seconds.
|
||||
* `distance_tolerance` — max allowed miss distance at time `t` to accept a solution.
|
||||
|
||||
---
|
||||
|
||||
## Trait requirements (`PredEngineConcept`)
|
||||
|
||||
Your `EngineTrait` must expose **noexcept** static methods with these signatures:
|
||||
|
||||
```cpp
|
||||
Vector3<float> predict_projectile_position(const Projectile&, float pitch_deg, float yaw_deg,
|
||||
float time, float gravity) noexcept;
|
||||
|
||||
Vector3<float> predict_target_position(const Target&, float time, float gravity) noexcept;
|
||||
|
||||
float calc_vector_2d_distance(const Vector3<float>& v) noexcept; // typically length in XZ plane
|
||||
float get_vector_height_coordinate(const Vector3<float>& v) noexcept; // typically Y
|
||||
|
||||
Vector3<float> calc_viewpoint_from_angles(const Projectile&, Vector3<float> target,
|
||||
std::optional<float> maybe_pitch_deg) noexcept;
|
||||
|
||||
float calc_direct_pitch_angle(const Vector3<float>& from, const Vector3<float>& to) noexcept;
|
||||
float calc_direct_yaw_angle (const Vector3<float>& from, const Vector3<float>& to) noexcept;
|
||||
```
|
||||
|
||||
> This design lets you adapt different game/physics conventions (axes, units, handedness) without changing the solver.
|
||||
|
||||
---
|
||||
|
||||
## Algorithm details
|
||||
|
||||
### Time scan
|
||||
|
||||
For `t = 0 .. maximum_simulation_time` in steps of `simulation_time_step`:
|
||||
|
||||
1. `T = EngineTrait::predict_target_position(target, t, g)`
|
||||
2. `pitch = maybe_calculate_projectile_launch_pitch_angle(projectile, T)`
|
||||
|
||||
* If `std::nullopt`: continue
|
||||
3. `yaw = EngineTrait::calc_direct_yaw_angle(projectile.m_origin, T)`
|
||||
4. `P = EngineTrait::predict_projectile_position(projectile, pitch, yaw, t, g)`
|
||||
5. Accept if `|P - T| <= distance_tolerance`
|
||||
6. Return `EngineTrait::calc_viewpoint_from_angles(projectile, T, pitch)`
|
||||
|
||||
### Closed-form pitch (gravity on)
|
||||
|
||||
Implements the classic ballistic formula (low-arc branch), where:
|
||||
|
||||
* `v` = muzzle speed,
|
||||
* `g` = `gravity_constant * projectile.m_gravity_scale`,
|
||||
* `x` = horizontal (2D) distance to target,
|
||||
* `y` = vertical offset to target.
|
||||
|
||||
[
|
||||
\theta ;=; \arctan!\left(\frac{v^{2} ;-; \sqrt{v^{4}-g!\left(gx^{2}+2yv^{2}\right)}}{gx}\right)
|
||||
]
|
||||
|
||||
* If the **discriminant** ( v^{4}-g(gx^{2}+2yv^{2}) < 0 ) ⇒ **no real solution**.
|
||||
* If `g == 0`, falls back to `EngineTrait::calc_direct_pitch_angle(...)`.
|
||||
* Returns **degrees** (internally converts from radians).
|
||||
|
||||
---
|
||||
|
||||
## Usage example
|
||||
|
||||
```cpp
|
||||
using namespace omath::projectile_prediction;
|
||||
|
||||
ProjPredEngineLegacy solver(
|
||||
/*gravity*/ 9.81f,
|
||||
/*dt*/ 1.f / 240.f,
|
||||
/*Tmax*/ 3.0f,
|
||||
/*tol*/ 0.05f
|
||||
);
|
||||
|
||||
Projectile proj; // fill: m_origin, m_launch_speed, m_gravity_scale, etc.
|
||||
Target tgt; // fill: position/velocity as required by your trait
|
||||
|
||||
if (auto aim = solver.maybe_calculate_aim_point(proj, tgt)) {
|
||||
// Drive your turret/reticle toward *aim
|
||||
} else {
|
||||
// No feasible intercept in the given horizon
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Behavior & edge cases
|
||||
|
||||
* **Zero gravity or zero distance**: uses direct pitch toward the target.
|
||||
* **Negative discriminant** in the pitch formula: returns `std::nullopt` for that time step.
|
||||
* **Very small `x`** (horizontal distance): the formula’s denominator `gx` approaches zero; your trait’s direct pitch helper provides a stable fallback.
|
||||
* **Tolerance**: `distance_tolerance` controls acceptance; tighten for accuracy, loosen for robustness.
|
||||
|
||||
---
|
||||
|
||||
## Complexity & tuning
|
||||
|
||||
* Time: **O(T)** where ( T \approx \frac{\text{maximum_simulation_time}}{\text{simulation_time_step}} )
|
||||
plus trait costs for prediction and angle math per step.
|
||||
* Smaller `simulation_time_step` improves precision but increases runtime.
|
||||
* If needed, do a **coarse-to-fine** search: coarse Δt scan, then refine around the best hit time.
|
||||
|
||||
---
|
||||
|
||||
## Testing checklist
|
||||
|
||||
* Stationary, level target → pitch ≈ 0 for short ranges; accepted within tolerance.
|
||||
* Elevated/depressed targets → pitch positive/negative as expected.
|
||||
* Receding fast target → unsolved within horizon ⇒ `nullopt`.
|
||||
* Gravity scale = 0 → identical to straight-line solution.
|
||||
* Near-horizon shots (large range, small arc) → discriminant near zero; verify stability.
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
* All angles produced/consumed by the trait in this implementation are **degrees**.
|
||||
* `calc_viewpoint_from_angles` defines what “aim point” means in your engine (e.g., a point along the initial ray or the predicted impact point). Keep this consistent with your HUD/reticle.
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 1 Nov 2025*
|
||||
96
docs/projectile_prediction/projectile.md
Normal file
96
docs/projectile_prediction/projectile.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# `omath::projectile_prediction::Projectile` — Projectile parameters for aim solvers
|
||||
|
||||
> Header: `omath/projectile_prediction/projectile.hpp`
|
||||
> Namespace: `omath::projectile_prediction`
|
||||
> Used by: `ProjPredEngineInterface` implementations (e.g., `ProjPredEngineLegacy`, `ProjPredEngineAvx2`)
|
||||
|
||||
`Projectile` is a tiny data holder that describes how a projectile is launched: **origin** (world position), **launch speed**, and a **gravity scale** (multiplier applied to the engine’s gravity constant).
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```cpp
|
||||
namespace omath::projectile_prediction {
|
||||
|
||||
class Projectile final {
|
||||
public:
|
||||
Vector3<float> m_origin; // Launch position (world space)
|
||||
float m_launch_speed{}; // Initial speed magnitude (units/sec)
|
||||
float m_gravity_scale{}; // Multiplier for global gravity (dimensionless)
|
||||
};
|
||||
|
||||
} // namespace omath::projectile_prediction
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Field semantics
|
||||
|
||||
* **`m_origin`**
|
||||
World-space position where the projectile is spawned (e.g., muzzle or emitter point).
|
||||
|
||||
* **`m_launch_speed`**
|
||||
Initial speed **magnitude** in your world units per second. Direction is determined by the solver (from yaw/pitch).
|
||||
|
||||
* Must be **non-negative**. Zero disables meaningful ballistic solutions.
|
||||
|
||||
* **`m_gravity_scale`**
|
||||
Multiplies the engine’s gravity constant provided to the solver (e.g., `g = gravity_constant * m_gravity_scale`).
|
||||
|
||||
* Use `1.0f` for normal gravity, `0.0f` for no-drop projectiles, other values to simulate heavier/lighter rounds.
|
||||
|
||||
> Units must be consistent across your project (e.g., meters & seconds). If `gravity_constant = 9.81f`, then `m_launch_speed` is in m/s and positions are in meters.
|
||||
|
||||
---
|
||||
|
||||
## Typical usage
|
||||
|
||||
```cpp
|
||||
using namespace omath::projectile_prediction;
|
||||
|
||||
Projectile proj;
|
||||
proj.m_origin = { 0.0f, 1.6f, 0.0f }; // player eye / muzzle height
|
||||
proj.m_launch_speed = 850.0f; // e.g., 850 m/s
|
||||
proj.m_gravity_scale = 1.0f; // normal gravity
|
||||
|
||||
// With an aim solver:
|
||||
auto aim = engine->maybe_calculate_aim_point(proj, target);
|
||||
if (aim) {
|
||||
// rotate/aim toward *aim and fire
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## With gravity-aware solver (outline)
|
||||
|
||||
Engines typically compute the firing angles to reach a predicted target position:
|
||||
|
||||
* Horizontal distance `x` and vertical offset `y` are derived from `target - m_origin`.
|
||||
* Gravity used is `g = gravity_constant * m_gravity_scale`.
|
||||
* Launch direction has speed `m_launch_speed` and angles solved by the engine.
|
||||
|
||||
If `m_gravity_scale == 0`, engines usually fall back to straight-line (no-drop) solutions.
|
||||
|
||||
---
|
||||
|
||||
## Validation & tips
|
||||
|
||||
* Keep `m_launch_speed ≥ 0`. Negative values are nonsensical.
|
||||
* If your weapon can vary muzzle speed (charge-up, attachments), update `m_launch_speed` per shot.
|
||||
* For different ammo types (tracers, grenades), prefer tweaking **`m_gravity_scale`** (and possibly the engine’s gravity constant) to match observed arc.
|
||||
|
||||
---
|
||||
|
||||
## See also
|
||||
|
||||
* `ProjPredEngineInterface` — common interface for aim solvers
|
||||
* `ProjPredEngineLegacy` — trait-based, time-stepped ballistic solver
|
||||
* `ProjPredEngineAvx2` — AVX2-accelerated solver with fixed-time pitch solve
|
||||
* `Target` — target state consumed by the solvers
|
||||
* `Vector3<float>` — math type used for positions and directions
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 1 Nov 2025*
|
||||
152
docs/projectile_prediction/projectile_engine.md
Normal file
152
docs/projectile_prediction/projectile_engine.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# `omath::projectile_prediction::ProjPredEngineInterface` — Aim-point solver interface
|
||||
|
||||
> Header: your project’s `projectile_prediction/proj_pred_engine_interface.hpp`
|
||||
> Namespace: `omath::projectile_prediction`
|
||||
> Depends on: `Vector3<float>`, `Projectile`, `Target`
|
||||
> Purpose: **contract** for engines that compute a lead/aim point to hit a moving target.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
`ProjPredEngineInterface` defines a single pure-virtual method that attempts to compute the **world-space aim point** where a projectile should be launched to intersect a target under the engine’s physical model (e.g., constant projectile speed, gravity, drag, max flight time, etc.).
|
||||
|
||||
If a valid solution exists, the engine returns the 3D aim point. Otherwise, it returns `std::nullopt` (no feasible intercept).
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```cpp
|
||||
namespace omath::projectile_prediction {
|
||||
|
||||
class ProjPredEngineInterface {
|
||||
public:
|
||||
[[nodiscard]]
|
||||
virtual std::optional<Vector3<float>>
|
||||
maybe_calculate_aim_point(const Projectile& projectile,
|
||||
const Target& target) const = 0;
|
||||
|
||||
virtual ~ProjPredEngineInterface() = default;
|
||||
};
|
||||
|
||||
} // namespace omath::projectile_prediction
|
||||
```
|
||||
|
||||
### Semantics
|
||||
|
||||
* **Input**
|
||||
|
||||
* `Projectile` — engine-specific projectile properties (typical: muzzle speed, gravity vector, drag flag/coeff, max range / flight time).
|
||||
* `Target` — target state (typical: position, velocity, possibly acceleration).
|
||||
|
||||
* **Output**
|
||||
|
||||
* `std::optional<Vector3<float>>`
|
||||
|
||||
* `value()` — world-space point to aim at **now** so that the projectile intersects the target under the model.
|
||||
* `std::nullopt` — no solution (e.g., target outruns projectile, blocked by constraints, numerical failure).
|
||||
|
||||
* **No side effects**: method is `const` and should not modify inputs.
|
||||
|
||||
---
|
||||
|
||||
## Typical usage
|
||||
|
||||
```cpp
|
||||
using namespace omath::projectile_prediction;
|
||||
|
||||
std::unique_ptr<ProjPredEngineInterface> engine = /* your implementation */;
|
||||
Projectile proj = /* fill from weapon config */;
|
||||
Target tgt = /* read from tracking system */;
|
||||
|
||||
if (auto aim = engine->maybe_calculate_aim_point(proj, tgt)) {
|
||||
// Rotate/steer to (*aim)
|
||||
} else {
|
||||
// Fall back: no-lead, predictive UI, or do not fire
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation guidance (for engine authors)
|
||||
|
||||
**Common models:**
|
||||
|
||||
1. **No gravity, constant speed**
|
||||
Closed form intersect time `t` solves `‖p_t + v_t t − p_0‖ = v_p t`.
|
||||
Choose the smallest non-negative real root; aim point = `p_t + v_t t`.
|
||||
|
||||
2. **Gravity (constant g), constant speed**
|
||||
Solve ballistics with vertical drop: either numerical (Newton–Raphson on time) or 2D elevation + azimuth decomposition. Ensure convergence caps and time bounds.
|
||||
|
||||
3. **Drag**
|
||||
Typically requires numeric integration (e.g., RK4) wrapped in a root find on time-of-flight.
|
||||
|
||||
**Robustness tips:**
|
||||
|
||||
* **Feasibility checks:** return `nullopt` when:
|
||||
|
||||
* projectile speed ≤ 0; target too fast in receding direction; solution time outside `[0, t_max]`.
|
||||
* **Bounds:** clamp search time to reasonable `[t_min, t_max]` (e.g., `[0, max_flight_time]` or by range).
|
||||
* **Tolerances:** use epsilons for convergence (e.g., `|f(t)| < 1e-4`, `|Δt| < 1e-4 s`).
|
||||
* **Determinism:** fix iteration counts or seeds if needed for replayability.
|
||||
|
||||
---
|
||||
|
||||
## Example: constant-speed, no-gravity intercept (closed form)
|
||||
|
||||
```cpp
|
||||
// Solve ||p + v t|| = s t where p = target_pos - shooter_pos, v = target_vel, s = projectile_speed
|
||||
// Quadratic: (v·v - s^2) t^2 + 2 (p·v) t + (p·p) = 0
|
||||
inline std::optional<float> intercept_time_no_gravity(const Vector3<float>& p,
|
||||
const Vector3<float>& v,
|
||||
float s) {
|
||||
const float a = v.dot(v) - s*s;
|
||||
const float b = 2.f * p.dot(v);
|
||||
const float c = p.dot(p);
|
||||
if (std::abs(a) < 1e-6f) { // near linear
|
||||
if (std::abs(b) < 1e-6f) return std::nullopt;
|
||||
float t = -c / b;
|
||||
return t >= 0.f ? std::optional{t} : std::nullopt;
|
||||
}
|
||||
const float disc = b*b - 4.f*a*c;
|
||||
if (disc < 0.f) return std::nullopt;
|
||||
const float sqrtD = std::sqrt(disc);
|
||||
float t1 = (-b - sqrtD) / (2.f*a);
|
||||
float t2 = (-b + sqrtD) / (2.f*a);
|
||||
float t = (t1 >= 0.f ? t1 : t2);
|
||||
return t >= 0.f ? std::optional{t} : std::nullopt;
|
||||
}
|
||||
```
|
||||
|
||||
Aim point (given shooter origin `S`, target pos `T`, vel `V`):
|
||||
|
||||
```
|
||||
p = T - S
|
||||
t* = intercept_time_no_gravity(p, V, speed)
|
||||
aim = T + V * t*
|
||||
```
|
||||
|
||||
Return `nullopt` if `t*` is absent.
|
||||
|
||||
---
|
||||
|
||||
## Testing checklist
|
||||
|
||||
* **Stationary target**: aim point equals target position when `s > 0`.
|
||||
* **Target perpendicular motion**: lead equals lateral displacement `V⊥ * t`.
|
||||
* **Receding too fast**: expect `nullopt`.
|
||||
* **Gravity model**: verify arc solutions exist for short & long trajectories (if implemented).
|
||||
* **Numerics**: convergence within max iterations; monotonic improvement of residuals.
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
* This is an **interface** only; concrete engines (e.g., `SimpleNoGravityEngine`, `BallisticGravityEngine`) should document their assumptions (gravity, drag, wind, bounds) and units (meters, seconds).
|
||||
* The coordinate system and handedness should be consistent with `Vector3<float>` and the rest of your math stack.
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 1 Nov 2025*
|
||||
70
docs/projectile_prediction/target.md
Normal file
70
docs/projectile_prediction/target.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# `omath::projectile_prediction::Target` — Target state for aim solvers
|
||||
|
||||
> Header: `omath/projectile_prediction/target.hpp`
|
||||
> Namespace: `omath::projectile_prediction`
|
||||
> Used by: `ProjPredEngineInterface` implementations (e.g., Legacy/AVX2 engines)
|
||||
|
||||
A small POD-style container describing a target’s **current pose** and **motion** for projectile lead/aim computations.
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```cpp
|
||||
namespace omath::projectile_prediction {
|
||||
|
||||
class Target final {
|
||||
public:
|
||||
Vector3<float> m_origin; // Current world-space position of the target
|
||||
Vector3<float> m_velocity; // World-space linear velocity (units/sec)
|
||||
bool m_is_airborne{}; // Domain hint (e.g., ignore ground snapping)
|
||||
};
|
||||
|
||||
} // namespace omath::projectile_prediction
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Field semantics
|
||||
|
||||
* **`m_origin`** — target position in world coordinates (same units as your `Vector3<float>` grid).
|
||||
* **`m_velocity`** — instantaneous linear velocity. Solvers commonly assume **constant velocity** between “now” and impact unless your trait injects gravity/accel.
|
||||
* **`m_is_airborne`** — optional hint for engine/trait logic (e.g., apply gravity to the target, skip ground friction/snap). Exact meaning is engine-dependent.
|
||||
|
||||
> Keep units consistent with your projectile model (e.g., meters & seconds). If projectiles use `g = 9.81 m/s²`, velocity should be in m/s and positions in meters.
|
||||
|
||||
---
|
||||
|
||||
## Typical usage
|
||||
|
||||
```cpp
|
||||
using namespace omath::projectile_prediction;
|
||||
|
||||
Target tgt;
|
||||
tgt.m_origin = { 42.0f, 1.8f, -7.5f };
|
||||
tgt.m_velocity = { 3.0f, 0.0f, 0.0f }; // moving +X at 3 units/s
|
||||
tgt.m_is_airborne = false;
|
||||
|
||||
// Feed into an aim solver with a Projectile
|
||||
auto aim = engine->maybe_calculate_aim_point(projectile, tgt);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes & tips
|
||||
|
||||
* If you track acceleration (e.g., gravity on ragdolls), your **EngineTrait** may derive it from `m_is_airborne` and world gravity; otherwise most solvers treat the target’s motion as linear.
|
||||
* For highly agile targets, refresh `m_origin`/`m_velocity` every tick and re-solve; don’t reuse stale aim points.
|
||||
* Precision: `Vector3<float>` is typically enough; if you need sub-millimeter accuracy over long ranges, consider double-precision internally in your trait.
|
||||
|
||||
---
|
||||
|
||||
## See also
|
||||
|
||||
* `Projectile` — shooter origin, muzzle speed, gravity scale
|
||||
* `ProjPredEngineInterface` — common interface for aim solvers
|
||||
* `ProjPredEngineLegacy`, `ProjPredEngineAvx2` — concrete solvers using this data
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 1 Nov 2025*
|
||||
261
docs/projection/camera.md
Normal file
261
docs/projection/camera.md
Normal file
@@ -0,0 +1,261 @@
|
||||
# `omath::projection::Camera` — Generic, trait-driven camera with screen/world conversion
|
||||
|
||||
> Header: `omath/projection/camera.hpp` (this header)
|
||||
> Namespace: `omath::projection`
|
||||
> Template: `Camera<Mat4X4Type, ViewAnglesType, TraitClass>`
|
||||
> Requires: `CameraEngineConcept<TraitClass, Mat4X4Type, ViewAnglesType>`
|
||||
> Key features: **lazy view-projection caching**, world↔screen helpers, pluggable math via a **Trait**
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
`Camera` is a small, zero-allocation camera wrapper. It delegates the math for **view**, **projection**, and **look-at** to a **Trait** (`TraitClass`), which lets you plug in different coordinate systems or conventions without changing the camera code. The class caches the **View×Projection** matrix and invalidates it when any parameter changes.
|
||||
|
||||
Alongside the camera, the header defines:
|
||||
|
||||
* `struct ViewPort { float m_width, m_height; float aspect_ratio() const; }`
|
||||
* `using FieldOfView = Angle<float, 0.f, 180.f, AngleFlags::Clamped>;`
|
||||
|
||||
---
|
||||
|
||||
## Template & trait requirements
|
||||
|
||||
```cpp
|
||||
template<class T, class MatType, class ViewAnglesType>
|
||||
concept CameraEngineConcept = requires(
|
||||
const omath::Vector3<float>& cam_origin,
|
||||
const omath::Vector3<float>& look_at,
|
||||
const ViewAnglesType& angles,
|
||||
const omath::projection::FieldOfView& fov,
|
||||
const omath::projection::ViewPort& viewport,
|
||||
float znear, float zfar
|
||||
) {
|
||||
{ T::calc_look_at_angle(cam_origin, look_at) } noexcept -> std::same_as<ViewAnglesType>;
|
||||
{ T::calc_view_matrix(angles, cam_origin) } noexcept -> std::same_as<MatType>;
|
||||
{ T::calc_projection_matrix(fov, viewport, znear, zfar)}noexcept -> std::same_as<MatType>;
|
||||
};
|
||||
```
|
||||
|
||||
Your `Mat4X4Type` must behave like the library’s `Mat<4,4,...>` (supports `*`, `/`, `inverted()`, `.at(r,c)`, `.raw_array()`, and `static constexpr get_store_ordering()`).
|
||||
|
||||
---
|
||||
|
||||
## Quick start
|
||||
|
||||
```cpp
|
||||
using Mat4 = omath::Mat<4,4,float, omath::MatStoreType::COLUMN_MAJOR>;
|
||||
|
||||
// Example trait (sketch): assumes Y-up, column-major, left-handed
|
||||
struct MyCamTrait {
|
||||
static ViewAnglesType calc_look_at_angle(const Vector3<float>& eye,
|
||||
const Vector3<float>& at) noexcept;
|
||||
static Mat4 calc_view_matrix(const ViewAnglesType& ang,
|
||||
const Vector3<float>& eye) noexcept;
|
||||
static Mat4 calc_projection_matrix(const FieldOfView& fov,
|
||||
const ViewPort& vp,
|
||||
float znear, float zfar) noexcept;
|
||||
};
|
||||
|
||||
using Camera = omath::projection::Camera<Mat4, MyViewAngles, MyCamTrait>;
|
||||
|
||||
omath::projection::ViewPort vp{1920, 1080};
|
||||
omath::projection::FieldOfView fov = omath::angles::degrees(70.f);
|
||||
|
||||
Camera cam(/*position*/ {0,1.7f, -3},
|
||||
/*angles*/ MyViewAngles{/*...*/},
|
||||
/*viewport*/ vp, fov,
|
||||
/*near*/ 0.1f,
|
||||
/*far*/ 1000.f);
|
||||
|
||||
// Project world → screen (origin top-left)
|
||||
auto s = cam.world_to_screen<Camera::ScreenStart::TOP_LEFT_CORNER>({1, 1, 0});
|
||||
if (s) {
|
||||
// s->x, s->y in pixels; s->z in NDC depth
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```cpp
|
||||
enum class ScreenStart { TOP_LEFT_CORNER, BOTTOM_LEFT_CORNER };
|
||||
|
||||
class Camera final {
|
||||
public:
|
||||
~Camera() = default;
|
||||
|
||||
Camera(const Vector3<float>& position,
|
||||
const ViewAnglesType& view_angles,
|
||||
const ViewPort& view_port,
|
||||
const FieldOfView& fov,
|
||||
float near, float far) noexcept;
|
||||
|
||||
void look_at(const Vector3<float>& target); // recomputes view angles; invalidates cache
|
||||
|
||||
// Lazily computed and cached:
|
||||
const Mat4X4Type& get_view_projection_matrix() const noexcept;
|
||||
|
||||
// Setters (all invalidate cached VP):
|
||||
void set_field_of_view(const FieldOfView&) noexcept;
|
||||
void set_near_plane(float) noexcept;
|
||||
void set_far_plane(float) noexcept;
|
||||
void set_view_angles(const ViewAnglesType&) noexcept;
|
||||
void set_origin(const Vector3<float>&) noexcept;
|
||||
void set_view_port(const ViewPort&) noexcept;
|
||||
|
||||
// Getters:
|
||||
const FieldOfView& get_field_of_view() const noexcept;
|
||||
const float& get_near_plane() const noexcept;
|
||||
const float& get_far_plane() const noexcept;
|
||||
const ViewAnglesType& get_view_angles() const noexcept;
|
||||
const Vector3<float>& get_origin() const noexcept;
|
||||
|
||||
// World → Screen (pixels) via NDC; choose screen origin:
|
||||
template<ScreenStart screen_start = ScreenStart::TOP_LEFT_CORNER>
|
||||
std::expected<Vector3<float>, Error>
|
||||
world_to_screen(const Vector3<float>& world) const noexcept;
|
||||
|
||||
// World → NDC (aka “viewport” in this code) ∈ [-1,1]^3
|
||||
std::expected<Vector3<float>, Error>
|
||||
world_to_view_port(const Vector3<float>& world) const noexcept;
|
||||
|
||||
// NDC → World (uses inverse VP)
|
||||
std::expected<Vector3<float>, Error>
|
||||
view_port_to_screen(const Vector3<float>& ndc) const noexcept;
|
||||
|
||||
// Screen (pixels) → World
|
||||
std::expected<Vector3<float>, Error>
|
||||
screen_to_world(const Vector3<float>& screen) const noexcept;
|
||||
|
||||
// 2D overload (z defaults to 1, i.e., far plane ray-end in NDC)
|
||||
std::expected<Vector3<float>, Error>
|
||||
screen_to_world(const Vector2<float>& screen) const noexcept;
|
||||
|
||||
protected:
|
||||
ViewPort m_view_port{};
|
||||
FieldOfView m_field_of_view;
|
||||
mutable std::optional<Mat4X4Type> m_view_projection_matrix;
|
||||
float m_far_plane_distance{};
|
||||
float m_near_plane_distance{};
|
||||
ViewAnglesType m_view_angles;
|
||||
Vector3<float> m_origin;
|
||||
|
||||
private:
|
||||
static constexpr bool is_ndc_out_of_bounds(const Mat4X4Type& ndc) noexcept;
|
||||
Vector3<float> ndc_to_screen_position_from_top_left_corner(const Vector3<float>& ndc) const noexcept;
|
||||
Vector3<float> ndc_to_screen_position_from_bottom_left_corner(const Vector3<float>& ndc) const noexcept;
|
||||
Vector3<float> screen_to_ndc(const Vector3<float>& screen) const noexcept;
|
||||
};
|
||||
```
|
||||
|
||||
### Error handling
|
||||
|
||||
All conversions return `std::expected<..., Error>` with errors from `error_codes.hpp`, notably:
|
||||
|
||||
* `Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS` — clip space W=0 or NDC outside `[-1,1]`.
|
||||
* `Error::INV_VIEW_PROJ_MAT_DET_EQ_ZERO` — non-invertible View×Projection matrix.
|
||||
|
||||
---
|
||||
|
||||
## Coordinate spaces & conversions
|
||||
|
||||
### World → NDC (`world_to_view_port`)
|
||||
|
||||
1. Build (or reuse cached) `VP = P * V` (projection * view).
|
||||
2. Multiply by homogeneous column from the world point.
|
||||
3. Reject if `w == 0`.
|
||||
4. Perspective divide → NDC in `[-1,1]^3`.
|
||||
5. Reject if any component is out of range.
|
||||
|
||||
Returns `{x_ndc, y_ndc, z_ndc}`.
|
||||
|
||||
### NDC → Screen (pixels)
|
||||
|
||||
The class offers two origins:
|
||||
|
||||
* **Top-left (default)**
|
||||
|
||||
```
|
||||
x_px = (x_ndc + 1)/2 * width
|
||||
y_px = ( -y_ndc/2 + 0.5) * height // flips Y
|
||||
```
|
||||
* **Bottom-left**
|
||||
|
||||
```
|
||||
x_px = (x_ndc + 1)/2 * width
|
||||
y_px = ( y_ndc/2 + 0.5) * height
|
||||
```
|
||||
|
||||
### Screen (pixels) → NDC
|
||||
|
||||
```
|
||||
x_ndc = screen_x / width * 2 - 1
|
||||
y_ndc = 1 - screen_y / height * 2 // Top-left screen origin assumed here
|
||||
z_ndc = screen_z // Caller-provided (e.g., 0..1 depth)
|
||||
```
|
||||
|
||||
### NDC → World (`view_port_to_screen`)
|
||||
|
||||
Despite the method name, this function **unprojects** an NDC point back to world space:
|
||||
|
||||
1. Compute `VP^{-1}`; if not invertible → error.
|
||||
2. Multiply by NDC (homogeneous 4D) and divide by `w`.
|
||||
3. Return world point.
|
||||
|
||||
> Tip: to build a **world-space ray** from a screen pixel, unproject at `z=0` (near) and `z=1` (far).
|
||||
|
||||
---
|
||||
|
||||
## Caching & invalidation
|
||||
|
||||
* `get_view_projection_matrix()` computes `P*V` once and caches it.
|
||||
* Any setter (`set_*`) or `look_at()` clears the cache (`m_view_projection_matrix = std::nullopt`).
|
||||
|
||||
---
|
||||
|
||||
## Notes & gotchas
|
||||
|
||||
* **Matrix order**: The camera multiplies `P * V`. Make sure your **Trait** matches this convention.
|
||||
* **Store ordering**: The `Mat4X4Type::get_store_ordering()` is used when building homogeneous columns; ensure it’s consistent with your matrix implementation.
|
||||
* **Naming quirk**: `view_port_to_screen()` returns a **world** point from **NDC** (it’s an unproject). Consider renaming to `ndc_to_world()` in your codebase for clarity.
|
||||
* **FOV units**: `FieldOfView` uses the project’s `Angle` type; pass degrees via `angles::degrees(...)`.
|
||||
|
||||
---
|
||||
|
||||
## Minimal trait sketch (column-major, left-handed)
|
||||
|
||||
```cpp
|
||||
struct LHCTrait {
|
||||
static MyAngles calc_look_at_angle(const Vector3<float>& eye,
|
||||
const Vector3<float>& at) noexcept { /* ... */ }
|
||||
|
||||
static Mat4 calc_view_matrix(const MyAngles& ang,
|
||||
const Vector3<float>& eye) noexcept {
|
||||
// Build from forward/right/up and translation
|
||||
}
|
||||
|
||||
static Mat4 calc_projection_matrix(const FieldOfView& fov,
|
||||
const ViewPort& vp,
|
||||
float zn, float zf) noexcept {
|
||||
return omath::mat_perspective_left_handed<float, omath::MatStoreType::COLUMN_MAJOR>(
|
||||
fov.as_degrees(), vp.aspect_ratio(), zn, zf
|
||||
);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing checklist
|
||||
|
||||
* World point centered in view projects to **screen center**.
|
||||
* Points outside frustum → `WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS`.
|
||||
* Inverting `VP` fails gracefully for singular matrices.
|
||||
* `ScreenStart` switch flips Y as expected.
|
||||
* Screen→World ray: unproject `(x,y,0)` and `(x,y,1)` and verify direction passes through the camera frustum.
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 1 Nov 2025*
|
||||
79
docs/projection/error_codes.md
Normal file
79
docs/projection/error_codes.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# `omath::projection::Error` — Error codes for world/screen projection
|
||||
|
||||
> Header: `omath/projection/error_codes.hpp`
|
||||
> Namespace: `omath::projection`
|
||||
> Type: `enum class Error : uint16_t`
|
||||
|
||||
These error codes are returned by camera/projection helpers (e.g., `Camera::world_to_screen`, `Camera::screen_to_world`) wrapped in `std::expected<..., Error>`. Use them to distinguish **clipping/visibility** problems from **matrix/math** failures.
|
||||
|
||||
---
|
||||
|
||||
## Enum values
|
||||
|
||||
```cpp
|
||||
enum class Error : uint16_t {
|
||||
WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS,
|
||||
INV_VIEW_PROJ_MAT_DET_EQ_ZERO,
|
||||
};
|
||||
```
|
||||
|
||||
* **`WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS`**
|
||||
The input point cannot produce a valid on-screen coordinate:
|
||||
|
||||
* Clip-space `w == 0` (point at/infinite or behind camera plane), or
|
||||
* After projection, any NDC component is outside `[-1, 1]`.
|
||||
|
||||
* **`INV_VIEW_PROJ_MAT_DET_EQ_ZERO`**
|
||||
The **View × Projection** matrix is not invertible (determinant ≈ 0).
|
||||
Unprojection (`screen_to_world` / `view_port_to_screen`) requires an invertible matrix.
|
||||
|
||||
---
|
||||
|
||||
## Typical usage
|
||||
|
||||
```cpp
|
||||
using omath::projection::Error;
|
||||
|
||||
auto pix = cam.world_to_screen(point);
|
||||
if (!pix) {
|
||||
switch (pix.error()) {
|
||||
case Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS:
|
||||
// Cull label/marker; point is off-screen or behind camera.
|
||||
break;
|
||||
case Error::INV_VIEW_PROJ_MAT_DET_EQ_ZERO:
|
||||
// Investigate camera/projection setup; near/far/FOV or trait bug.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Unproject a screen pixel (top-left origin) at depth 1.0
|
||||
if (auto world = cam.screen_to_world({sx, sy, 1.0f})) {
|
||||
// use *world
|
||||
} else if (world.error() == Error::INV_VIEW_PROJ_MAT_DET_EQ_ZERO) {
|
||||
// handle singular VP matrix
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## When you might see these errors
|
||||
|
||||
* **Out-of-bounds**
|
||||
|
||||
* The world point lies outside the camera frustum.
|
||||
* The point is behind the camera (clip `w <= 0`).
|
||||
* Extremely large coordinates cause overflow and fail NDC bounds.
|
||||
|
||||
* **Non-invertible VP**
|
||||
|
||||
* Degenerate projection settings (e.g., `near == far`, zero FOV).
|
||||
* Trait builds `P` or `V` incorrectly (wrong handedness/order).
|
||||
* Numerical issues from nearly singular configurations.
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
* Validate camera setup: `near > 0`, `far > near`, sensible FOV (e.g., 30°–120°).
|
||||
* For UI markers: treat `WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS` as a simple **cull** signal.
|
||||
* Log `INV_VIEW_PROJ_MAT_DET_EQ_ZERO` — it usually indicates a configuration or math bug worth fixing rather than hiding.
|
||||
164
docs/rev_eng/external_rev_object.md
Normal file
164
docs/rev_eng/external_rev_object.md
Normal file
@@ -0,0 +1,164 @@
|
||||
# `omath::rev_eng::ExternalReverseEngineeredObject` — typed offsets over external memory
|
||||
|
||||
> Header: `omath/rev_eng/external_reverse_engineered_object.hpp`
|
||||
> Namespace: `omath::rev_eng`
|
||||
> Pattern: **CRTP-style wrapper** around a user-provided *ExternalMemoryManagementTrait* that actually reads/writes another process or device’s memory.
|
||||
|
||||
A tiny base class for reverse-engineered objects that live **outside** your address space. You pass an absolute base address and a trait with `read_memory` / `write_memory`. Your derived types then expose strongly-typed getters/setters that delegate into the trait using **byte offsets**.
|
||||
|
||||
---
|
||||
|
||||
## Quick look
|
||||
|
||||
```cpp
|
||||
template<class ExternalMemoryManagementTrait>
|
||||
class ExternalReverseEngineeredObject {
|
||||
public:
|
||||
explicit ExternalReverseEngineeredObject(std::uintptr_t addr)
|
||||
: m_object_address(addr) {}
|
||||
|
||||
protected:
|
||||
template<class Type>
|
||||
[[nodiscard]] Type get_by_offset(std::ptrdiff_t offset) const {
|
||||
return ExternalMemoryManagementTrait::read_memory(m_object_address + offset);
|
||||
}
|
||||
|
||||
template<class Type>
|
||||
void set_by_offset(std::ptrdiff_t offset, const Type& value) const {
|
||||
ExternalMemoryManagementTrait::write_memory(m_object_address + offset, value);
|
||||
}
|
||||
|
||||
private:
|
||||
std::uintptr_t m_object_address{};
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Trait requirements
|
||||
|
||||
Your `ExternalMemoryManagementTrait` must provide:
|
||||
|
||||
```cpp
|
||||
// Reads sizeof(T) bytes starting at absolute address and returns T.
|
||||
template<class T>
|
||||
static T read_memory(std::uintptr_t absolute_address);
|
||||
|
||||
// Writes sizeof(T) bytes to absolute address.
|
||||
template<class T>
|
||||
static void write_memory(std::uintptr_t absolute_address, const T& value);
|
||||
```
|
||||
|
||||
> Tip: If your implementation prefers returning `bool`/`expected<>` for writes, either:
|
||||
>
|
||||
> * make `write_memory` `void` and throw/log internally, or
|
||||
> * adjust `set_by_offset` in your fork to surface the status.
|
||||
|
||||
### Common implementations
|
||||
|
||||
* **Windows**: wrap `ReadProcessMemory` / `WriteProcessMemory` with a stored `HANDLE` (often captured via a singleton or embedded in the trait).
|
||||
* **Linux**: `/proc/<pid>/mem`, `process_vm_readv/writev`, `ptrace`.
|
||||
* **Device/FPGA**: custom MMIO/driver APIs.
|
||||
|
||||
---
|
||||
|
||||
## How to use (derive and map fields)
|
||||
|
||||
Create a concrete type for your target structure and map known offsets:
|
||||
|
||||
```cpp
|
||||
struct WinRPMTrait {
|
||||
template<class T>
|
||||
static T read_memory(std::uintptr_t addr) {
|
||||
T out{};
|
||||
SIZE_T n{};
|
||||
if (!ReadProcessMemory(g_handle, reinterpret_cast<LPCVOID>(addr), &out, sizeof(T), &n) || n != sizeof(T))
|
||||
throw std::runtime_error("RPM failed");
|
||||
return out;
|
||||
}
|
||||
template<class T>
|
||||
static void write_memory(std::uintptr_t addr, const T& val) {
|
||||
SIZE_T n{};
|
||||
if (!WriteProcessMemory(g_handle, reinterpret_cast<LPVOID>(addr), &val, sizeof(T), &n) || n != sizeof(T))
|
||||
throw std::runtime_error("WPM failed");
|
||||
}
|
||||
};
|
||||
|
||||
class Player final : public omath::rev_eng::ExternalReverseEngineeredObject<WinRPMTrait> {
|
||||
using Base = omath::rev_eng::ExternalReverseEngineeredObject<WinRPMTrait>;
|
||||
public:
|
||||
using Base::Base; // inherit ctor (takes base address)
|
||||
|
||||
// Offsets taken from your RE notes (in bytes)
|
||||
Vector3<float> position() const { return get_by_offset<Vector3<float>>(0x30); }
|
||||
void set_position(const Vector3<float>& p) const { set_by_offset(0x30, p); }
|
||||
|
||||
float health() const { return get_by_offset<float>(0x100); }
|
||||
void set_health(float h) const { set_by_offset(0x100, h); }
|
||||
};
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```cpp
|
||||
Player p{ /* base address you discovered */ 0x7FF6'1234'0000ull };
|
||||
auto pos = p.position();
|
||||
p.set_health(100.f);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Design notes & constraints
|
||||
|
||||
* **Offsets are byte offsets** from the object’s **base address** passed to the constructor.
|
||||
* **Type safety is on you**: `Type` must match the external layout at that offset (endian, packing, alignment).
|
||||
* **No lifetime tracking**: if the target object relocates/frees, you must update/recreate the wrapper with the new base address.
|
||||
* **Thread safety**: the class itself is stateless; thread safety depends on your trait implementation.
|
||||
* **Endianness**: assumes the host and target endianness agree, or your trait handles conversion.
|
||||
* **Error handling**: this header doesn’t prescribe it; adopt exceptions/expected/logging inside the trait.
|
||||
|
||||
---
|
||||
|
||||
## Best practices
|
||||
|
||||
* Centralize offsets in one place (constexprs or a small struct) and **comment source/version** (e.g., *game v1.2.3*).
|
||||
* Wrap fragile multi-field writes in a trait-level **transaction** if your platform supports it.
|
||||
* Validate pointers/guards (e.g., vtable signature, canary) before trusting offsets.
|
||||
* Prefer **plain old data** (`struct` without virtuals) for `Type` to ensure trivial byte copies.
|
||||
|
||||
---
|
||||
|
||||
## Minimal trait sketch (POSIX, `process_vm_readv`)
|
||||
|
||||
```cpp
|
||||
struct LinuxPvmTrait {
|
||||
static pid_t pid;
|
||||
|
||||
template<class T> static T read_memory(std::uintptr_t addr) {
|
||||
T out{};
|
||||
iovec local{ &out, sizeof(out) }, remote{ reinterpret_cast<void*>(addr), sizeof(out) };
|
||||
if (process_vm_readv(pid, &local, 1, &remote, 1, 0) != ssize_t(sizeof(out)))
|
||||
throw std::runtime_error("pvm_readv failed");
|
||||
return out;
|
||||
}
|
||||
|
||||
template<class T> static void write_memory(std::uintptr_t addr, const T& val) {
|
||||
iovec local{ const_cast<T*>(&val), sizeof(val) }, remote{ reinterpret_cast<void*>(addr), sizeof(val) };
|
||||
if (process_vm_writev(pid, &local, 1, &remote, 1, 0) != ssize_t(sizeof(val)))
|
||||
throw std::runtime_error("pvm_writev failed");
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
* **Garbled values** → wrong offset/Type, or target’s structure changed between versions.
|
||||
* **Access denied** → missing privileges (admin/root), wrong process handle, or page protections.
|
||||
* **Crashes in trait** → add bounds/sanity checks; many APIs fail on unmapped pages.
|
||||
* **Writes “stick” only briefly** → the target may constantly overwrite (server authority / anti-cheat / replication).
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 1 Nov 2025*
|
||||
142
docs/rev_eng/internal_rev_object.md
Normal file
142
docs/rev_eng/internal_rev_object.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# `omath::rev_eng::InternalReverseEngineeredObject` — raw in-process offset/VTABLE access
|
||||
|
||||
> Header: `omath/rev_eng/internal_reverse_engineered_object.hpp`
|
||||
> Namespace: `omath::rev_eng`
|
||||
> Purpose: Convenience base for **internal** (same-process) RE wrappers that:
|
||||
>
|
||||
> * read/write fields by **byte offset** from `this`
|
||||
> * call **virtual methods** by **vtable index**
|
||||
|
||||
---
|
||||
|
||||
## At a glance
|
||||
|
||||
```cpp
|
||||
class InternalReverseEngineeredObject {
|
||||
protected:
|
||||
template<class Type>
|
||||
[[nodiscard]] Type& get_by_offset(std::ptrdiff_t offset);
|
||||
|
||||
template<class Type>
|
||||
[[nodiscard]] const Type& get_by_offset(std::ptrdiff_t offset) const;
|
||||
|
||||
template<std::size_t id, class ReturnType>
|
||||
ReturnType call_virtual_method(auto... arg_list);
|
||||
};
|
||||
```
|
||||
|
||||
* `get_by_offset<T>(off)` — returns a **reference** to `T` located at `reinterpret_cast<uintptr_t>(this) + off`.
|
||||
* `call_virtual_method<id, Ret>(args...)` — fetches the function pointer from `(*reinterpret_cast<void***>(this))[id]` and invokes it as a free function with implicit `this` passed explicitly.
|
||||
|
||||
On MSVC builds the function pointer type uses `__thiscall`; on non-MSVC it uses a plain function pointer taking `void*` as the first parameter (the typical Itanium ABI shape).
|
||||
|
||||
---
|
||||
|
||||
## Example: wrapping a reverse-engineered class
|
||||
|
||||
```cpp
|
||||
struct Player : omath::rev_eng::InternalReverseEngineeredObject {
|
||||
// Field offsets (document game/app version!)
|
||||
static constexpr std::ptrdiff_t kHealth = 0x100;
|
||||
static constexpr std::ptrdiff_t kPosition = 0x30;
|
||||
|
||||
// Accessors
|
||||
float& health() { return get_by_offset<float>(kHealth); }
|
||||
const float& health() const { return get_by_offset<float>(kHealth); }
|
||||
|
||||
Vector3<float>& position() { return get_by_offset<Vector3<float>>(kPosition); }
|
||||
const Vector3<float>& position() const { return get_by_offset<Vector3<float>>(kPosition); }
|
||||
|
||||
// Virtuals (vtable indices discovered via RE)
|
||||
int getTeam() { return call_virtual_method<27, int>(); }
|
||||
void setArmor(float val) { call_virtual_method<42, void>(val); } // signature must match!
|
||||
};
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
```cpp
|
||||
auto* p = /* pointer to live Player instance within the same process */;
|
||||
p->health() = 100.f;
|
||||
int team = p->getTeam();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How `call_virtual_method` resolves the signature
|
||||
|
||||
```cpp
|
||||
template<std::size_t id, class ReturnType>
|
||||
ReturnType call_virtual_method(auto... arg_list) {
|
||||
#ifdef _MSC_VER
|
||||
using Fn = ReturnType(__thiscall*)(void*, decltype(arg_list)...);
|
||||
#else
|
||||
using Fn = ReturnType(*)(void*, decltype(arg_list)...);
|
||||
#endif
|
||||
return (*reinterpret_cast<Fn**>(this))[id](this, arg_list...);
|
||||
}
|
||||
```
|
||||
|
||||
* The **first parameter** is always `this` (`void*`).
|
||||
* Remaining parameter types are deduced from the **actual arguments** (`decltype(arg_list)...`).
|
||||
Ensure you pass arguments with the correct types (e.g., `int32_t` vs `int`, pointer/ref qualifiers), or define thin wrappers that cast to the exact signature you recovered.
|
||||
|
||||
> ⚠ On 32-bit MSVC the `__thiscall` distinction matters; on 64-bit MSVC it’s ignored (all member funcs use the common x64 calling convention).
|
||||
|
||||
---
|
||||
|
||||
## Safety notes (read before using!)
|
||||
|
||||
Working at this level is inherently unsafe; be deliberate:
|
||||
|
||||
1. **Correct offsets & alignment**
|
||||
|
||||
* `get_by_offset<T>` assumes `this + offset` is **properly aligned** for `T` and points to an object of type `T`.
|
||||
* Wrong offsets or misalignment ⇒ **undefined behavior** (UB), crashes, silent corruption.
|
||||
|
||||
2. **Object layout assumptions**
|
||||
|
||||
* The vtable pointer is assumed to be at the **start of the most-derived subobject at `this`**.
|
||||
* With **multiple/virtual inheritance**, the desired subobject’s vptr may be at a non-zero offset. If so, adjust `this` to that subobject before calling, e.g.:
|
||||
|
||||
```cpp
|
||||
auto* sub = reinterpret_cast<void*>(reinterpret_cast<std::uintptr_t>(this) + kSubobjectOffset);
|
||||
// … then reinterpret sub instead of this inside a custom helper
|
||||
```
|
||||
|
||||
3. **ABI & calling convention**
|
||||
|
||||
* Indices and signatures are **compiler/ABI-specific**. Recheck after updates or different builds (MSVC vs Clang/LLVM-MSVC vs MinGW).
|
||||
|
||||
4. **Strict aliasing**
|
||||
|
||||
* Reinterpreting memory as unrelated `T` can violate aliasing rules. Prefer **trivially copyable** PODs and exact original types where possible.
|
||||
|
||||
5. **Const-correctness**
|
||||
|
||||
* The `const` overload returns `const T&` but still reinterprets memory; do not write through it. Use the non-const overload to mutate.
|
||||
|
||||
6. **Thread safety**
|
||||
|
||||
* No synchronization is provided. Ensure the underlying object isn’t concurrently mutated in incompatible ways.
|
||||
|
||||
---
|
||||
|
||||
## Tips & patterns
|
||||
|
||||
* **Centralize offsets** in `constexpr` with comments (`// game v1.2.3, sig XYZ`).
|
||||
* **Guard reads**: if you have a canary or RTTI/vtable hash, check it before relying on offsets.
|
||||
* **Prefer accessors** returning references**:** lets you both read and write with natural syntax.
|
||||
* **Wrap tricky virtuals**: if a method takes complex/reference params, wrap `call_virtual_method` in a strongly typed member that casts exactly as needed.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
* **Crash on virtual call** → wrong index or wrong `this` (subobject), or mismatched signature (args/ret or calling conv).
|
||||
* **Weird field values** → wrong offset, wrong type size/packing, stale layout after an update.
|
||||
* **Only in 32-bit** → double-check `__thiscall` and parameter passing (register vs stack).
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 1 Nov 2025*
|
||||
165
docs/trigonometry/angle.md
Normal file
165
docs/trigonometry/angle.md
Normal file
@@ -0,0 +1,165 @@
|
||||
# `omath::Angle` — templated angle with normalize/clamper + trig
|
||||
|
||||
> Header: `omath/trigonometry/angle.hpp`
|
||||
> Namespace: `omath`
|
||||
> Template: `Angle<Type = float, min = 0, max = 360, flags = AngleFlags::Normalized>`
|
||||
> Requires: `std::is_arithmetic_v<Type>`
|
||||
> Formatters: `std::formatter` for `char`, `wchar_t`, `char8_t` → `"{}deg"`
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
`Angle` is a tiny value-type that stores an angle in **degrees** and automatically **normalizes** or **clamps** it into a compile-time range. It exposes conversions to/from radians, common trig (`sin/cos/tan/cot`), arithmetic with wrap/clamp semantics, and lightweight formatting.
|
||||
|
||||
Two behaviors via `AngleFlags`:
|
||||
|
||||
* `AngleFlags::Normalized` (default): values are wrapped into `[min, max]` using `angles::wrap_angle`.
|
||||
* `AngleFlags::Clamped`: values are clamped to `[min, max]` using `std::clamp`.
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```cpp
|
||||
namespace omath {
|
||||
|
||||
enum class AngleFlags { Normalized = 0, Clamped = 1 };
|
||||
|
||||
template<class Type = float, Type min = Type(0), Type max = Type(360),
|
||||
AngleFlags flags = AngleFlags::Normalized>
|
||||
requires std::is_arithmetic_v<Type>
|
||||
class Angle {
|
||||
public:
|
||||
// Construction
|
||||
static constexpr Angle from_degrees(const Type& deg) noexcept;
|
||||
static constexpr Angle from_radians(const Type& rad) noexcept;
|
||||
constexpr Angle() noexcept; // 0 deg, adjusted by flags/range
|
||||
|
||||
// Accessors / conversions (degrees stored internally)
|
||||
constexpr const Type& operator*() const noexcept; // raw degrees reference
|
||||
constexpr Type as_degrees() const noexcept;
|
||||
constexpr Type as_radians() const noexcept;
|
||||
|
||||
// Trig (computed from radians)
|
||||
Type sin() const noexcept;
|
||||
Type cos() const noexcept;
|
||||
Type tan() const noexcept;
|
||||
Type atan() const noexcept; // atan(as_radians()) (rarely used)
|
||||
Type cot() const noexcept; // cos()/sin() (watch sin≈0)
|
||||
|
||||
// Arithmetic (wraps or clamps per flags and [min,max])
|
||||
constexpr Angle& operator+=(const Angle&) noexcept;
|
||||
constexpr Angle& operator-=(const Angle&) noexcept;
|
||||
constexpr Angle operator+(const Angle&) noexcept;
|
||||
constexpr Angle operator-(const Angle&) noexcept;
|
||||
constexpr Angle operator-() const noexcept;
|
||||
|
||||
// Comparison (partial ordering)
|
||||
constexpr std::partial_ordering operator<=>(const Angle&) const noexcept = default;
|
||||
};
|
||||
|
||||
} // namespace omath
|
||||
```
|
||||
|
||||
### Formatting
|
||||
|
||||
```cpp
|
||||
std::format("{}", Angle<float>::from_degrees(45)); // "45deg"
|
||||
```
|
||||
|
||||
Formatters exist for `char`, `wchar_t`, and `char8_t`.
|
||||
|
||||
---
|
||||
|
||||
## Usage examples
|
||||
|
||||
### Defaults (0–360, normalized)
|
||||
|
||||
```cpp
|
||||
using Deg = omath::Angle<>; // float, [0,360], Normalized
|
||||
|
||||
auto a = Deg::from_degrees(370); // -> 10deg
|
||||
auto b = Deg::from_radians(omath::angles::pi); // -> 180deg
|
||||
|
||||
a += Deg::from_degrees(355); // 10 + 355 -> 365 -> wraps -> 5deg
|
||||
|
||||
float s = a.sin(); // sin(5°)
|
||||
```
|
||||
|
||||
### Clamped range
|
||||
|
||||
```cpp
|
||||
using Fov = omath::Angle<float, 1.f, 179.f, omath::AngleFlags::Clamped>;
|
||||
auto fov = Fov::from_degrees(200.f); // -> 179deg (clamped)
|
||||
```
|
||||
|
||||
### Signed, normalized range
|
||||
|
||||
```cpp
|
||||
using SignedDeg = omath::Angle<float, -180.f, 180.f, omath::AngleFlags::Normalized>;
|
||||
|
||||
auto x = SignedDeg::from_degrees(190.f); // -> -170deg
|
||||
auto y = SignedDeg::from_degrees(-200.f); // -> 160deg
|
||||
auto z = x + y; // -170 + 160 = -10deg (wrapped if needed)
|
||||
```
|
||||
|
||||
### Get/set raw degrees
|
||||
|
||||
```cpp
|
||||
auto yaw = SignedDeg::from_degrees(-45.f);
|
||||
float deg = *yaw; // same as yaw.as_degrees()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Semantics & notes
|
||||
|
||||
* **Storage & units:** Internally stores **degrees** (`Type m_angle`). `as_radians()`/`from_radians()` use the project helpers in `omath::angles`.
|
||||
* **Arithmetic honors policy:** `operator+=`/`-=` and the binary `+`/`-` apply **wrap** or **clamp** in `[min,max]`, mirroring construction behavior.
|
||||
* **`atan()`**: returns `std::atan(as_radians())` (the arctangent of the *radian value*). This is mathematically unusual for an angle type and is rarely useful; prefer `tan()`/`atan2` in client code when solving geometry problems.
|
||||
* **`cot()` / `tan()` singularities:** Near multiples where `sin() ≈ 0` or `cos() ≈ 0`, results blow up. Guard in your usage if inputs can approach these points.
|
||||
* **Comparison:** `operator<=>` is defaulted. With normalization, distinct representatives can compare as expected (e.g., `-180` vs `180` in signed ranges are distinct endpoints).
|
||||
* **No implicit numeric conversion:** There’s **no `operator Type()`**. Use `as_degrees()`/`as_radians()` (or `*angle`) explicitly—this intentional friction avoids unit mistakes.
|
||||
|
||||
---
|
||||
|
||||
## Customization patterns
|
||||
|
||||
* **Radians workflow:** Keep angles in degrees internally but wrap helper creators:
|
||||
|
||||
```cpp
|
||||
inline Deg degf(float d) { return Deg::from_degrees(d); }
|
||||
inline Deg radf(float r) { return Deg::from_radians(r); }
|
||||
```
|
||||
* **Compile-time policy:** Pick ranges/flags at the type level to enforce invariants (e.g., `YawDeg = Angle<float,-180,180,Normalized>`; `FovDeg = Angle<float,1,179,Clamped>`).
|
||||
|
||||
---
|
||||
|
||||
## Pitfalls & gotchas
|
||||
|
||||
* Ensure `min < max` at compile time for meaningful wrap/clamp behavior.
|
||||
* For normalized signed ranges, decide whether your `wrap_angle(min,max)` treats endpoints half-open (e.g., `[-180,180)`) to avoid duplicate representations; the formatter will print the stored value verbatim.
|
||||
* If you need **sum of many angles**, accumulating in radians then converting back can improve numeric stability at extreme values.
|
||||
|
||||
---
|
||||
|
||||
## Minimal tests
|
||||
|
||||
```cpp
|
||||
using A = omath::Angle<>;
|
||||
REQUIRE(A::from_degrees(360).as_degrees() == 0.f);
|
||||
REQUIRE(A::from_degrees(-1).as_degrees() == 359.f);
|
||||
|
||||
using S = omath::Angle<float,-180.f,180.f, omath::AngleFlags::Normalized>;
|
||||
REQUIRE(S::from_degrees( 181).as_degrees() == -179.f);
|
||||
REQUIRE(S::from_degrees(-181).as_degrees() == 179.f);
|
||||
|
||||
using C = omath::Angle<float, 10.f, 20.f, omath::AngleFlags::Clamped>;
|
||||
REQUIRE(C::from_degrees(5).as_degrees() == 10.f);
|
||||
REQUIRE(C::from_degrees(25).as_degrees() == 20.f);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 1 Nov 2025*
|
||||
107
docs/trigonometry/angles.md
Normal file
107
docs/trigonometry/angles.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# `omath::angles` — angle conversions, FOV helpers, and wrapping
|
||||
|
||||
> Header: `omath/trigonometry/angles.hpp`
|
||||
> Namespace: `omath::angles`
|
||||
> All functions are `[[nodiscard]]` and `noexcept` where applicable.
|
||||
|
||||
A small set of constexpr-friendly utilities for converting between degrees/radians, converting horizontal/vertical field of view, and wrapping angles into a closed interval.
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```cpp
|
||||
// Degrees ↔ Radians (Type must be floating-point)
|
||||
template<class Type>
|
||||
requires std::is_floating_point_v<Type>
|
||||
constexpr Type radians_to_degrees(const Type& radians) noexcept;
|
||||
|
||||
template<class Type>
|
||||
requires std::is_floating_point_v<Type>
|
||||
constexpr Type degrees_to_radians(const Type& degrees) noexcept;
|
||||
|
||||
// FOV conversion (inputs/outputs in degrees, aspect = width/height)
|
||||
template<class Type>
|
||||
requires std::is_floating_point_v<Type>
|
||||
Type horizontal_fov_to_vertical(const Type& horizontal_fov, const Type& aspect) noexcept;
|
||||
|
||||
template<class Type>
|
||||
requires std::is_floating_point_v<Type>
|
||||
Type vertical_fov_to_horizontal(const Type& vertical_fov, const Type& aspect) noexcept;
|
||||
|
||||
// Wrap angle into [min, max] (any arithmetic type)
|
||||
template<class Type>
|
||||
requires std::is_arithmetic_v<Type>
|
||||
Type wrap_angle(const Type& angle, const Type& min, const Type& max) noexcept;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### Degrees ↔ Radians
|
||||
|
||||
```cpp
|
||||
float rad = omath::angles::degrees_to_radians(180.0f); // π
|
||||
double deg = omath::angles::radians_to_degrees(std::numbers::pi); // 180
|
||||
```
|
||||
|
||||
### Horizontal ↔ Vertical FOV
|
||||
|
||||
* `aspect` = **width / height**.
|
||||
* Inputs/outputs are **degrees**.
|
||||
|
||||
```cpp
|
||||
float hdeg = 90.0f;
|
||||
float aspect = 16.0f / 9.0f;
|
||||
|
||||
float vdeg = omath::angles::horizontal_fov_to_vertical(hdeg, aspect); // ~58.0°
|
||||
float hdeg2 = omath::angles::vertical_fov_to_horizontal(vdeg, aspect); // ≈ 90.0°
|
||||
```
|
||||
|
||||
Formulas (in radians):
|
||||
|
||||
* `v = 2 * atan( tan(h/2) / aspect )`
|
||||
* `h = 2 * atan( tan(v/2) * aspect )`
|
||||
|
||||
### Wrapping angles (or any periodic value)
|
||||
|
||||
Wrap any numeric `angle` into `[min, max]`:
|
||||
|
||||
```cpp
|
||||
// Wrap degrees into [0, 360]
|
||||
float a = omath::angles::wrap_angle( 370.0f, 0.0f, 360.0f); // 10
|
||||
float b = omath::angles::wrap_angle( -15.0f, 0.0f, 360.0f); // 345
|
||||
// Signed range [-180,180]
|
||||
float c = omath::angles::wrap_angle( 200.0f, -180.0f, 180.0f); // -160
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes & edge cases
|
||||
|
||||
* **Type requirements**
|
||||
|
||||
* Converters & FOV helpers require **floating-point** `Type`.
|
||||
* `wrap_angle` accepts any arithmetic `Type` (floats or integers).
|
||||
* **Aspect ratio** must be **positive** and finite. For `aspect == 0` the FOV helpers are undefined.
|
||||
* **Units**: FOV functions accept/return **degrees** but compute internally in radians.
|
||||
* **Wrapping interval**: Behavior assumes `max > min`. The result lies in the **closed interval** `[min, max]` with modulo arithmetic; if you need half-open behavior (e.g., `[min,max)`), adjust your range or post-process endpoint cases.
|
||||
* **constexpr**: Converters are `constexpr`; FOV helpers are runtime constexpr-compatible except for `std::atan/std::tan` constraints on some standard libraries.
|
||||
|
||||
---
|
||||
|
||||
## Quick tests
|
||||
|
||||
```cpp
|
||||
using namespace omath::angles;
|
||||
|
||||
static_assert(degrees_to_radians(180.0) == std::numbers::pi);
|
||||
static_assert(radians_to_degrees(std::numbers::pi_v<float>) == 180.0f);
|
||||
|
||||
float v = horizontal_fov_to_vertical(90.0f, 16.0f/9.0f);
|
||||
float h = vertical_fov_to_horizontal(v, 16.0f/9.0f);
|
||||
assert(std::abs(h - 90.0f) < 1e-5f);
|
||||
|
||||
assert(wrap_angle(360.0f, 0.0f, 360.0f) == 0.0f || wrap_angle(360.0f, 0.0f, 360.0f) == 360.0f);
|
||||
```
|
||||
87
docs/trigonometry/view_angles.md
Normal file
87
docs/trigonometry/view_angles.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# `omath::ViewAngles` — tiny POD for pitch/yaw/roll
|
||||
|
||||
> Header: your project’s `view_angles.hpp`
|
||||
> Namespace: `omath`
|
||||
> Kind: **aggregate struct** (POD), no methods, no allocation
|
||||
|
||||
A minimal container for Euler angles. You choose the types for each component (e.g., raw `float` or the strong `omath::Angle<>` type), and plug it into systems like `projection::Camera`.
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```cpp
|
||||
namespace omath {
|
||||
template<class PitchType, class YawType, class RollType>
|
||||
struct ViewAngles {
|
||||
PitchType pitch;
|
||||
YawType yaw;
|
||||
RollType roll;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
* Aggregate: supports brace-init, aggregate copying, and `constexpr` usage when the component types do.
|
||||
* Semantics (units/handedness/ranges) are **entirely defined by your chosen types**.
|
||||
|
||||
---
|
||||
|
||||
## Common aliases
|
||||
|
||||
```cpp
|
||||
// Simple, raw degrees as floats (be careful with wrapping!)
|
||||
using ViewAnglesF = omath::ViewAngles<float, float, float>;
|
||||
|
||||
// Safer, policy-based angles (recommended)
|
||||
using PitchDeg = omath::Angle<float, -89.f, 89.f, omath::AngleFlags::Clamped>;
|
||||
using YawDeg = omath::Angle<float, -180.f, 180.f, omath::AngleFlags::Normalized>;
|
||||
using RollDeg = omath::Angle<float, -180.f, 180.f, omath::AngleFlags::Normalized>;
|
||||
using ViewAnglesDeg = omath::ViewAngles<PitchDeg, YawDeg, RollDeg>;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic construction
|
||||
|
||||
```cpp
|
||||
omath::ViewAngles<float,float,float> a{ 10.f, 45.f, 0.f }; // pitch, yaw, roll in degrees
|
||||
```
|
||||
|
||||
### With `omath::Angle<>` (automatic wrap/clamper)
|
||||
|
||||
```cpp
|
||||
ViewAnglesDeg v{
|
||||
PitchDeg::from_degrees( 95.f), // -> 89deg (clamped)
|
||||
YawDeg::from_degrees (-190.f), // -> 170deg (wrapped)
|
||||
RollDeg::from_degrees ( 30.f)
|
||||
};
|
||||
```
|
||||
|
||||
### Using with `projection::Camera`
|
||||
|
||||
```cpp
|
||||
using Mat4 = omath::Mat<4,4,float, omath::MatStoreType::COLUMN_MAJOR>;
|
||||
using Cam = omath::projection::Camera<Mat4, ViewAnglesDeg, MyCameraTrait>;
|
||||
|
||||
omath::projection::ViewPort vp{1920,1080};
|
||||
auto fov = omath::angles::degrees_to_radians(70.f); // or your Angle type
|
||||
|
||||
Cam cam(/*position*/ {0,1.7f,-3},
|
||||
/*angles*/ ViewAnglesDeg{ PitchDeg::from_degrees(0),
|
||||
YawDeg::from_degrees(0),
|
||||
RollDeg::from_degrees(0) },
|
||||
/*viewport*/ vp,
|
||||
/*fov*/ omath::Angle<float,0.f,180.f,omath::AngleFlags::Clamped>::from_degrees(70.f),
|
||||
/*near*/ 0.1f,
|
||||
/*far*/ 1000.f);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes & tips
|
||||
|
||||
* **Ranges/units**: pick types that encode your policy (e.g., signed yaw in `[-180,180]`, pitch clamped to avoid gimbal flips).
|
||||
* **Handedness & order**: this struct doesn’t impose rotation order. Your math/trait layer (e.g., `MyCameraTrait`) must define how `(pitch, yaw, roll)` map to a view matrix (common orders: ZYX or XYZ).
|
||||
* **Zero-cost**: with plain `float`s this is as cheap as three scalars; with `Angle<>` you gain safety at the cost of tiny wrap/clamp logic on construction/arithmetic.
|
||||
Reference in New Issue
Block a user