# Render Pass Pipeline Stage 4 drew everything in one hardcoded pass. Stage 5 generalizes that into a [`RenderPipeline`]: an ordered, named list of composable [`RenderPass`]es 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 ```rust 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: ```rust # 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`](rendering.md) carries a `visibility` [`LayerMask`](layers.md): 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: ```rust # 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::(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. [`RenderPipeline`]: ../engine/src/render/pipeline.rs [`RenderPass`]: ../engine/src/render/pipeline.rs [`FrameContext`]: ../engine/src/render/pipeline.rs [`ClearPass`]: ../engine/src/render/pipeline.rs [`ForwardPass`]: ../engine/src/render/pipeline.rs [`Layer`]: ../engine/src/layer/components.rs