Files
Oxide/docs/rendering.md
T
Homer Simpson 9eead719b0 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>
2026-07-05 20:41:02 +02:00

187 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).