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
+186
View File
@@ -0,0 +1,186 @@
# Rendering (Stage 4 — Basic 3D Rendering)
Stage 4 turns the clear-color surface from Stage 2 into a 3D renderer: it draws
**meshes**, placed by **transforms**, shaded by **materials**, as seen through a
**camera**, lit by a directional light — all through a single-pass
**forward renderer**.
> Status: the rendering core (this document), the glTF importer, and the
> editor's 3D viewport (orbit/pan/zoom + material inspector) are all implemented.
> The engine paths are covered by headless GPU tests; the editor viewport is on
> `dev` awaiting the maintainer's manual sign-off before Stage 4 is marked done
> (tracked in [PLAN.md](../PLAN.md)).
All of these types live in `oxide_engine::render` and are re-exported from the
[prelude](getting-started.md).
## The pieces
| Type | Role |
|------|------|
| [`Vertex`] | One vertex: `position`, `normal`, `uv` (GPU-ready, `repr(C)`) |
| [`Mesh`] | CPU-side indexed triangle geometry + primitive builders |
| [`GpuMesh`] | A `Mesh` uploaded into GPU vertex/index buffers |
| [`Material`] | PBR-lite surface: `albedo`, `metallic`, `roughness` |
| [`Camera`] | Perspective projection; the *view* comes from a `Transform` |
| [`DirectionalLight`] / [`Lighting`] | One sun light + an ambient term |
| [`RenderObject`] | A drawable: `&GpuMesh` + `Material` + `Transform` |
| [`ForwardRenderer`] | Owns the pipeline + depth buffer; draws a list of objects |
[`Vertex`]: ../engine/src/render/mesh.rs
[`Mesh`]: ../engine/src/render/mesh.rs
[`GpuMesh`]: ../engine/src/render/mesh.rs
[`Material`]: ../engine/src/render/material.rs
[`Camera`]: ../engine/src/render/camera.rs
[`DirectionalLight`]: ../engine/src/render/forward.rs
[`Lighting`]: ../engine/src/render/forward.rs
[`RenderObject`]: ../engine/src/render/forward.rs
[`ForwardRenderer`]: ../engine/src/render/forward.rs
## Building geometry
Meshes are built on the CPU and uploaded once. Built-in primitives cover the
common prototyping shapes:
```rust
use oxide_engine::prelude::*;
let cube = Mesh::cube(); // unit cube, per-face normals
let plane = Mesh::plane(10.0); // 10×10 ground on XZ, facing +Y
let sphere = Mesh::uv_sphere(0.8, 32, 16); // radius, sectors, stacks
// Upload to the GPU (needs a `&wgpu::Device`, e.g. from `RenderCtx`/`Gpu`).
let gpu_cube: GpuMesh = cube.upload(device, "cube");
```
You can also build a mesh directly from `Vertex` + index data, and query its
object-space bounds with `Mesh::bounds()` (used later for culling).
### Importing glTF
Static meshes load from glTF/GLB via `oxide_engine::asset`. The node hierarchy is
flattened into world space and each primitive becomes a `GltfMesh` (geometry +
PBR-lite material + transform); missing normals are generated, missing UVs default
to zero. Skinning/animation are deferred to the animation stage.
```rust
use oxide_engine::prelude::*;
let model = load_gltf("assets/models/cube.gltf")?;
let drawables: Vec<_> = model
.meshes
.iter()
.map(|m| (m.mesh.upload(device, "gltf"), m.material, m.transform))
.collect();
// Build `RenderObject`s from `drawables` and hand them to `ForwardRenderer::render`.
```
`load_gltf_slice(&bytes)` is the in-memory variant (buffers must be embedded),
used for tests and bundled assets.
## Camera
A `Camera` holds only projection parameters (`fov_y`, `z_near`, `z_far`); its
*position and orientation* are a [`Transform`](scene.md) given at render time, so
a camera can live in the scene as an entity. Use `Transform::looking_at` to aim
it:
```rust
let camera = Camera::default(); // 60° FOV, 0.11000 range
let view = Transform::looking_at(Vec3::new(4.0, 2.5, 5.0), Vec3::ZERO, Vec3::Y);
```
The projection uses a `0..1` NDC depth range (the wgpu/Vulkan/DX/Metal
convention), matching the depth buffer the forward renderer clears to `1.0`.
## Drawing a frame
The `ForwardRenderer` is built once for a given **color target format** — the
window surface format for on-screen rendering, or e.g. `Rgba8Unorm` offscreen.
Then each frame you hand it a list of `RenderObject`s:
```rust
// Once (e.g. lazily on the first frame, when the surface format is known):
let mut renderer = ForwardRenderer::new(device, ctx.surface_format);
// Each frame, inside `App::render`:
renderer.render(
device,
queue,
ctx.view, // the target view (already cleared to the clear color)
ctx.size, // (width, height) in physical pixels
&camera,
&view, // the camera's world transform
&Lighting::default(),
&[
RenderObject { mesh: &gpu_plane, material: Material::diffuse(Color::WHITE), transform: ground },
RenderObject { mesh: &gpu_cube, material: Material::diffuse(Color::RED), transform: spin },
],
);
```
The color attachment is **loaded, not cleared**, so whatever cleared the surface
beforehand (the window's clear color from Stage 2, or a `clear_view` call) shows
through as the background. The depth buffer is owned by the renderer, resized to
match the target, and cleared to `1.0` every call.
See the full runnable example:
```sh
cargo run -p oxide-examples --bin hello_mesh # spinning cube + sphere + ground
```
## How it works
- **One pipeline, one pass.** Geometry is drawn front-to-back-agnostic; a
`Depth32Float` depth buffer with `Less` compare resolves occlusion, so draw
order does not affect the result.
- **Per-object data via dynamic uniform offsets.** Globals (view-projection,
camera position, light) live in one uniform buffer (bind group 0). Each
object's model matrix, normal matrix, and material live in a second uniform
buffer addressed with a dynamic offset (bind group 1), so an arbitrary number
of objects draw from one buffer that grows as needed.
- **PBR-lite shading.** [`shaders/lit.wgsl`](../engine/src/render/shaders/lit.wgsl)
does Lambert diffuse + ambient + a Blinn-Phong specular term whose sharpness
comes from `roughness` and whose color comes from `metallic`. It outputs linear
color; an sRGB surface converts on write.
## In the editor
The editor renders the active scene in a 3D viewport beneath its egui panels.
Entities become visible by carrying a `MeshRenderer` component (`oxide_engine::render`):
```rust
use oxide_engine::prelude::*;
// Make an entity render a cube with a custom material.
let e = scene.spawn("crate", Transform::from_translation(Vec3::new(2.0, 0.5, 0.0)));
scene.world_mut().insert_one(
e,
MeshRenderer::with_material(PrimitiveShape::Cube, Material::diffuse(Color::RED)),
).unwrap();
```
`MeshRenderer` names a built-in `PrimitiveShape` (cube/sphere/plane) rather than
embedding geometry, so it is tiny and serializable (RON) — editable from both the
inspector and, later, scripts/AI agents. The editor caches one GPU mesh per shape
and draws every `MeshRenderer` entity through the `ForwardRenderer`, with an
orbit camera (drag to orbit, right-drag to pan, scroll to zoom).
Entities are selected by **clicking them in the viewport** (a ray is cast against
each renderable's world-space bounds) or from the hierarchy panel. The inspector
edits the selection's **transform** (position, rotation as euler degrees, and
scale) and its **material** (albedo / metallic / roughness — roughness controls
specular-highlight sharpness, most visible on glossy/metallic surfaces).
## Testing
The window/viewport halves need a human eye, but the render path itself is
verified headlessly (`tests/` `stage4`): render to an offscreen texture and read
the pixels back to assert that lit geometry appears, the background shows through
elsewhere, and a near object occludes a farther one through the depth buffer.
Camera projection/view math has unit tests in `render/camera.rs`.
See also: [render-context.md](render-context.md) (surface/clear loop),
[conventions.md](conventions.md) (handedness, color space), [scene.md](scene.md)
(transforms and the hierarchy that feeds object placement).