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:
2025-11-01 09:12:04 +03:00
parent d12a2611b8
commit 95c0873b8c
15 changed files with 1970 additions and 1 deletions

View File

@@ -0,0 +1,161 @@
# `omath::projectile_prediction::ProjPredEngineAvx2` — AVX2-accelerated ballistic aim solver
> Header: your projects `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`, youll 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*

View 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 formulas denominator `gx` approaches zero; your traits 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*

View 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 engines 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 engines 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 engines 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*

View File

@@ -0,0 +1,152 @@
# `omath::projectile_prediction::ProjPredEngineInterface` — Aim-point solver interface
> Header: your projects `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 engines 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 (NewtonRaphson 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*

View 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 targets **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 targets motion as linear.
* For highly agile targets, refresh `m_origin`/`m_velocity` every tick and re-solve; dont 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*