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:
Homer Simpson
2026-07-05 20:41:02 +02:00
parent 2afb56b329
commit 9eead719b0
157 changed files with 47270 additions and 2 deletions
+353
View File
@@ -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 GribbHartmann 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.