# Math & Core Primitives The `oxide_engine::math` module is the foundation every other system depends on. It builds on [`glam`](https://docs.rs/glam) for vectors, quaternions, and matrices, and adds the engine's higher-level geometric and utility types. This document is the usage reference for the module as delivered in **Stage 1**. For the conventions these types follow (handedness, units, color space), see [conventions.md](conventions.md). ## Importing Everything is available through the module path or the prelude: ```rust use oxide_engine::math::{Transform, Aabb, Ray, Plane, Frustum, Color, Rect, Range3}; // or, more commonly: use oxide_engine::prelude::*; ``` The prelude also re-exports the `glam` types you need for everyday work — `Vec2`, `Vec3`, `Vec4`, `Quat`, `Mat3`, `Mat4`, and `EulerRot` — so downstream crates need no direct `glam` dependency. ## Contents | Type | Role | |------|------| | [`Transform`](#transform) | Placement: translation + rotation + scale | | [`Aabb`](#aabb) | Axis-aligned bounding box (geometry/bounds/culling) | | [`Ray`](#ray) | Origin + normalized direction (picking, queries) | | [`Plane`](#plane) | Infinite plane in Hessian normal form | | [`Frustum`](#frustum) | Six-plane view volume for visibility culling | | [`Color`](#color) | Linear RGBA color with sRGB conversion | | [`Rect`](#rect) | 2D rectangle (UI, viewports, texture regions) | | [`Range3`](#range3) | 3D value range (clamp, lerp, remap) | All types are `Copy`, `PartialEq`, and `serde`-(de)serializable. --- ## Transform A 3D affine transform stored in **decomposed** form — `translation` (`Vec3`), `rotation` (`Quat`), and `scale` (`Vec3`) — so each channel stays editable without matrix round-trips. The effective matrix is `T * R * S`. ### Construction ```rust use oxide_engine::prelude::*; let a = Transform::IDENTITY; let b = Transform::from_translation(Vec3::new(0.0, 1.0, 0.0)); let c = Transform::from_rotation(Quat::from_rotation_y(90_f32.to_radians())); let d = Transform::from_scale(Vec3::splat(2.0)); let e = Transform::from_trs( Vec3::new(1.0, 2.0, 3.0), Quat::from_euler(EulerRot::XYZ, 0.1, 0.2, 0.3), Vec3::splat(1.0), ); // From / to a 4x4 matrix: let m = e.to_matrix(); // glam::Mat4 let back = Transform::from_matrix(m); let affine = e.to_affine(); // glam::Affine3A (cheaper to compose) ``` ### Composition and hierarchy `mul_transform` composes parent-first, so this is how you resolve a child into its parent's space: ```rust let parent = Transform::from_trs( Vec3::new(10.0, 0.0, 0.0), Quat::from_rotation_y(90_f32.to_radians()), Vec3::splat(2.0), ); let child_local = Transform::from_translation(Vec3::new(0.0, 0.0, 1.0)); let child_world = parent.mul_transform(&child_local); // child_world.translation == (12, 0, 0) ``` Composition is exact for uniform scale and a closest-fit approximation for non-uniform scale combined with rotation (see [conventions](conventions.md#transform-composition)). ### Applying a transform ```rust let t = Transform::from_trs( Vec3::new(1.0, 0.0, 0.0), Quat::from_rotation_z(90_f32.to_radians()), Vec3::splat(2.0), ); let p = t.transform_point(Vec3::new(1.0, 0.0, 0.0)); // affected by T, R, S let v = t.transform_vector(Vec3::new(1.0, 0.0, 0.0)); // R and S only (no translation) ``` ### Inverse `inverse()` returns a transform that undoes this one. It is exact for uniform scale; with any zero scale component the transform is not invertible and the inverse scale will contain infinities (check with `is_finite()`). ```rust let undo = t.inverse(); let identity = t.mul_transform(&undo); // ≈ Transform::IDENTITY ``` ### Direction and orientation helpers ```rust let dir_forward = t.forward(); // local -Z let dir_up = t.up(); // local +Y let dir_right = t.right(); // local +X // Build an orientation that looks from `eye` toward `target`: let cam = Transform::looking_at(Vec3::new(0.0, 0.0, 5.0), Vec3::ZERO, Vec3::Y); // cam.forward() points at the target. Degenerate (eye == target) → identity rotation. ``` ### Edge cases handled - **Zero scale** → non-invertible; the forward transform is still finite. - **Gimbal-lock orientations** (e.g. ±90° pitch) round-trip through a matrix without losing orthonormality of the basis vectors. - **Degenerate `looking_at`** (eye == target) returns identity rotation rather than producing NaNs. --- ## Aabb An axis-aligned bounding box defined by `min` and `max` corners. Used for bounds, broad-phase overlap, and frustum culling. A box is *empty* when any `min` component exceeds its `max`; `Aabb::EMPTY` is the identity for `union`. ```rust use oxide_engine::prelude::*; let a = Aabb::new(Vec3::new(1.0, 5.0, -2.0), Vec3::new(-1.0, 0.0, 3.0)); // corners auto-sorted let b = Aabb::from_center_half_extents(Vec3::ZERO, Vec3::splat(1.0)); let c = Aabb::from_points([Vec3::ZERO, Vec3::new(2.0, -1.0, 4.0), Vec3::new(-3.0, 5.0, 1.0)]); // Queries: let center = b.center(); let size = b.size(); let inside = b.contains_point(Vec3::ZERO); let near = b.closest_point(Vec3::new(5.0, 0.0, 0.0)); // Set operations: let u = a.union(&b); let i = a.intersection(&b); // Aabb::EMPTY if disjoint let hit = a.intersects(&b); // bool (touching counts) // Acceleration-structure metrics: let area = b.surface_area(); // for SAH let vol = b.volume(); let pts = b.corners(); // [Vec3; 8] // Ray test (slab method): returns entry distance t, or None. let ray = Ray::new(Vec3::new(-5.0, 0.0, 0.0), Vec3::X); if let Some(t) = b.ray_intersection(&ray) { let point = ray.at(t); } ``` A ray whose origin is inside the box returns `Some(0.0)`. --- ## Ray A half-line with an `origin` and a **normalized** `direction`. Because the direction is unit-length, the parameter `t` in `at(t)` is a true distance. ```rust use oxide_engine::prelude::*; let r = Ray::new(Vec3::ZERO, Vec3::new(0.0, 5.0, 0.0)); // direction normalized to +Y let r2 = Ray::from_to(Vec3::ZERO, Vec3::new(0.0, 0.0, 10.0)); let p = r.at(4.0); // origin + direction * 4 let valid = r.is_valid(); // false if direction was zero-length let near = r.closest_point(target); // clamped to t >= 0 let dist = r.distance_to_point(target); ``` If you pass a zero-length direction, the ray is left degenerate; check `is_valid()` before relying on it. --- ## Plane An infinite plane in Hessian normal form: a unit `normal` and a signed distance `d`, such that every point on the plane satisfies `normal·p + d = 0`. The positive half-space is the side the normal points toward. ```rust use oxide_engine::prelude::*; let p1 = Plane::from_point_normal(Vec3::new(0.0, 2.0, 0.0), Vec3::Y); let p2 = Plane::from_points(Vec3::ZERO, Vec3::X, Vec3::Y); // normal via right-hand rule → +Z let p3 = Plane::new(Vec3::new(0.0, 0.0, 4.0), 8.0); // normalized on construction let sd = p1.signed_distance(Vec3::new(0.0, 5.0, 0.0)); // +3.0 (in front) let proj = p1.project_point(Vec3::new(3.0, 7.0, -2.0)); // orthogonal projection onto plane let flipped = p1.flipped(); // same plane, reversed normal // Ray test: None if parallel or pointing away. let ray = Ray::new(Vec3::new(0.0, 5.0, 0.0), Vec3::NEG_Y); if let Some(t) = p1.ray_intersection(&ray) { let hit = ray.at(t); } ``` --- ## Frustum A view volume represented by six planes (left, right, bottom, top, near, far), each with its normal pointing **inward**. A point is inside when it lies in the positive half-space of every plane. Built from a combined view-projection matrix via the Gribb–Hartmann method; works for perspective and orthographic projections alike. ```rust use oxide_engine::prelude::*; let proj = Mat4::perspective_rh(60_f32.to_radians(), 16.0 / 9.0, 0.1, 100.0); let view = Mat4::look_at_rh(Vec3::new(0.0, 0.0, 5.0), Vec3::ZERO, Vec3::Y); let frustum = Frustum::from_view_projection(proj * view); let visible_point = frustum.contains_point(Vec3::ZERO); let bb = Aabb::from_center_half_extents(Vec3::ZERO, Vec3::splat(1.0)); let visible_box = frustum.intersects_aabb(&bb); let visible_sphere = frustum.intersects_sphere(Vec3::ZERO, 1.0); ``` `intersects_aabb` is **conservative**: it never culls a box that is actually visible, though it may very rarely keep one that is just outside a corner. That is the correct trade-off for rendering, where false positives cost a wasted draw but false negatives cause visible pop-out. --- ## Color Linear RGBA color with `f32` channels. The engine works in **linear** space; conversions to/from 8-bit sRGB are explicit. Values may exceed `1.0` to represent HDR/emissive intensity and are not clamped in storage. ```rust use oxide_engine::prelude::*; let white = Color::WHITE; let custom = Color::rgba(0.2, 0.4, 0.6, 1.0); let from_pdf = Color::from_srgb_u8(135, 206, 235); // sRGB bytes → linear let from_hex = Color::from_hex(0x87CEEB); // #87CEEB → linear let bytes = custom.to_srgb_u8(); // [u8; 4] sRGB, clamped to [0,1] let v4 = custom.to_vec4(); // Vec4 [r,g,b,a] let v3 = custom.to_vec3(); // Vec3 rgb let faded = custom.with_alpha(0.5); let mid = Color::BLACK.lerp(Color::WHITE, 0.5); // t clamped to [0,1] ``` Constants: `BLACK`, `WHITE`, `RED`, `GREEN`, `BLUE`, `TRANSPARENT`. --- ## Rect A 2D axis-aligned rectangle for UI, viewports, and texture regions. ```rust use oxide_engine::prelude::*; let r1 = Rect::new(Vec2::new(4.0, 1.0), Vec2::new(0.0, 5.0)); // corners auto-sorted let r2 = Rect::from_min_size(Vec2::ZERO, Vec2::new(3.0, 4.0)); let r3 = Rect::from_center_size(Vec2::ZERO, Vec2::splat(2.0)); let (w, h) = (r2.width(), r2.height()); let size = r2.size(); let area = r2.area(); let c = r2.center(); let empty = Rect::ZERO.is_empty(); let inside = r2.contains_point(Vec2::ONE); let hit = r2.intersects(&r3); let i = r2.intersection(&r3); // Rect::ZERO if disjoint let u = r2.union(&r3); let near = r2.closest_point(Vec2::new(5.0, -1.0)); let grown = r2.expanded(1.0); // negative shrinks ``` --- ## Range3 A 3D **value** range — an inclusive `[min, max]` interval per axis. Unlike `Aabb` (which models geometry), `Range3` models a *value range* for clamping, interpolation, and remapping. It deliberately provides `lerp`/`inverse_lerp`/ `remap`, which an `Aabb` does not. ```rust use oxide_engine::prelude::*; let r = Range3::new(Vec3::ZERO, Vec3::splat(100.0)); let unit = Range3::UNIT; // [0, 1] per axis let sym = Range3::symmetric(Vec3::splat(2.0)); // [-2, 2] per axis let span = r.span(); let center = r.center(); let clamped = r.clamp(Vec3::new(-5.0, 50.0, 200.0)); // → (0, 50, 100) let inside = r.contains(Vec3::splat(50.0)); let v = r.lerp(Vec3::splat(0.25)); // NOT clamped — extrapolates outside [0,1] let t = r.inverse_lerp(Vec3::splat(25.0)); // → 0.25 per axis (0.0 on zero-span axes) let remapped = r.remap(Vec3::splat(50.0), &unit); // 50 in [0,100] → 0.5 in [0,1] ``` `inverse_lerp` guards against division by zero: an axis with zero span yields `0.0` rather than `NaN`/`inf`. --- ## Testing and performance - **Unit tests** for every type live in that type's source file (`engine/src/math/*.rs`), covering core behavior and edge cases (zero scale, gimbal lock, degenerate rays/planes, empty boxes, zero-span ranges). - **Integration / fuzz tests** live in the `oxide-tests` crate (`stage1::fuzz_transform_chains_stay_stable` builds long random transform chains and verifies stability and inverse round-trips). - **Benchmark:** `cargo bench -p oxide-engine` runs the `transform` benchmark. The Stage 1 budget — 1,000,000 transform compositions in under 10 ms — is met with margin (~5 ms on a typical desktop). ## Runnable example ```sh cargo run -p oxide-examples --bin math_demo ``` `examples/src/bin/math_demo.rs` exercises every type above and prints the results — a good place to see the API in use end to end.