Import Oxide engine (Stages 0–10) under MIT license
Full project snapshot migrated to new Gitea remote without history: engine, editor, physics, script, examples, tests, docs, and assets. Relicensed from GPLv3 to MIT and updated repo URLs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+353
@@ -0,0 +1,353 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user