# 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.1–1000 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).
