Files
Oxide/docs/render-pipeline.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

4.8 KiB

Render Pass Pipeline

Stage 4 drew everything in one hardcoded pass. Stage 5 generalizes that into a RenderPipeline: an ordered, named list of composable RenderPasses that share one frame's targets. A project enables only the passes it needs — this is the mechanism behind scalable fidelity: a flat unlit/low-poly look (or a stylized post effect like a VCR filter) versus a full realistic stack with shadows and post-processing, paying only for the passes turned on.

The Stage-4 forward renderer is retrofitted onto this as ForwardPass, so the default pipeline is just [Clear, Forward] and produces pixel-identical output. Later stages (shadows, post-process, overlay UI) add passes without touching the renderer core — they register a pass.

The pieces

  • RenderPass — a trait with one method, run(&mut self, frame). Implement it to add a stage of the frame.
  • FrameContext — everything a pass operates on for one frame: the shared color target, size, clear color, camera + its world transform, lighting, and the (already culled) drawables.
  • RenderPipeline — owns the passes and runs every enabled one in order.
  • Built-in passes: ClearPass (clears the color target) and ForwardPass (the lit forward draw).

Composing a frame

use oxide_engine::render::{RenderPipeline, FrameContext, ForwardPass};
# use oxide_engine::prelude::*;
# fn demo(device: &oxide_engine::wgpu::Device, queue: &oxide_engine::wgpu::Queue,
#         target: &oxide_engine::wgpu::TextureView, cube: &GpuMesh) {
// The default pipeline: Clear then Forward (pixel-identical to Stage 4).
let mut pipeline = RenderPipeline::forward(device, oxide_engine::wgpu::TextureFormat::Rgba8Unorm);

let camera = Camera::default();
let view = Transform::looking_at(Vec3::new(0.0, 0.0, 5.0), Vec3::ZERO, Vec3::Y);
let lighting = Lighting::default();
let objects = [RenderObject { mesh: cube, material: Material::diffuse(Color::RED), transform: Transform::IDENTITY }];

pipeline.render(&mut FrameContext {
    device, queue,
    color: target,
    size: (1280, 720),
    clear_color: Color::rgb(0.05, 0.06, 0.09),
    camera: &camera,
    view_transform: &view,
    lighting: &lighting,
    objects: &objects,
});
# }

Data-driven: add, toggle, remove

Passes are addressed by name and managed without touching any pass's code:

# use oxide_engine::render::RenderPipeline;
# struct Bloom; impl oxide_engine::render::RenderPass for Bloom {
#   fn run(&mut self, _f: &mut oxide_engine::render::FrameContext<'_>) {} }
# let mut pipeline = RenderPipeline::new();
pipeline.add_pass("forward", /* ForwardPass */
#   { struct F; impl oxide_engine::render::RenderPass for F { fn run(&mut self, _f: &mut oxide_engine::render::FrameContext<'_>) {} } F }
);
pipeline.add_pass("bloom", Bloom);          // a post effect (Stage 13)
pipeline.set_enabled("bloom", false);       // turn it off, keep it registered
pipeline.insert_before("forward", "shadows",
#   { struct S; impl oxide_engine::render::RenderPass for S { fn run(&mut self, _f: &mut oxide_engine::render::FrameContext<'_>) {} } S }
);                                           // slot a pass into a fixed position
pipeline.remove("bloom");                    // drop it entirely

A stylized game ships a pipeline with no post passes (and pays nothing for them); a realistic game enables shadows, SSAO, bloom, tone-mapping. Same engine, same renderer core — different pass list.

Windowed vs offscreen clearing

The window runner already clears the surface to the configured clear color before App::render runs, so the editor and windowed examples use a forward-only pipeline (no ClearPass) and let the runner clear. RenderPipeline::forward (Clear + Forward) is for offscreen/standalone rendering where nothing else clears the target — e.g. the headless render tests.

Camera layer visibility

A Camera carries a visibility LayerMask: it renders an entity only if the entity's Layer is in that mask (default [LayerMask::ALL] — sees everything). The host applies it while gathering drawables:

# use oxide_engine::prelude::*;
# use oxide_engine::layer::Layer;
# let scene = Scene::new();
# let camera = Camera::default();
# let entity = scene.entities().next();
# if let Some(entity) = entity {
let layer = scene.get::<Layer>(entity).map(|l| *l).unwrap_or_default();
if camera.sees(layer) {
    // include this entity in the draw list
}
# }

This is how a minimap camera, a first-person view-model camera, or editor-only gizmo layers are kept to their own cameras.