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

261
docs/projection/camera.md Normal file
View 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 librarys `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 its consistent with your matrix implementation.
* **Naming quirk**: `view_port_to_screen()` returns a **world** point from **NDC** (its an unproject). Consider renaming to `ndc_to_world()` in your codebase for clarity.
* **FOV units**: `FieldOfView` uses the projects `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*

View 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.