f56a1eea3b
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>
234 lines
8.2 KiB
Rust
234 lines
8.2 KiB
Rust
//! [`RenderPipeline`]: a data-driven, ordered list of composable render passes.
|
|
//!
|
|
//! Stage 4's renderer drew everything in one hardcoded pass. Stage 5 generalizes
|
|
//! that into a list of named [`RenderPass`]es that share one frame's targets and
|
|
//! run in order. 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) versus a full realistic stack, paying only for the
|
|
//! passes turned on.
|
|
//!
|
|
//! The Stage-4 forward pass is retrofitted onto this as [`ForwardPass`], so the
|
|
//! default pipeline ([`RenderPipeline::forward`]) is just `[Clear, Forward]` and
|
|
//! produces pixel-identical output. Later stages add passes (shadows,
|
|
//! post-process, overlay UI) **without touching the renderer core** — they
|
|
//! register a pass.
|
|
|
|
use crate::math::{Color, Rect, Transform, Vec2};
|
|
|
|
use super::{clear_view, Camera, ForwardRenderer, Lighting, RenderObject};
|
|
|
|
/// Everything one frame's passes operate on: the shared color target and the
|
|
/// scene view to draw.
|
|
///
|
|
/// Passes share the same `color` target (and, as the pipeline grows, depth and
|
|
/// intermediate textures), which is what makes them *composable*: a clear pass
|
|
/// fills the target, the forward pass draws into it, a future post pass reads and
|
|
/// rewrites it.
|
|
pub struct FrameContext<'a> {
|
|
/// The GPU device.
|
|
pub device: &'a wgpu::Device,
|
|
/// The GPU queue.
|
|
pub queue: &'a wgpu::Queue,
|
|
/// The color target every pass renders into.
|
|
pub color: &'a wgpu::TextureView,
|
|
/// Target size in physical pixels (the whole color target the pipeline
|
|
/// is writing into).
|
|
pub size: (u32, u32),
|
|
/// The sub-rectangle of the target that drawing is restricted to, in
|
|
/// physical pixels (`min` = upper-left, `max` = lower-right). Passes
|
|
/// configure the wgpu viewport from this and the camera uses its
|
|
/// aspect ratio for the projection.
|
|
///
|
|
/// `None` means "use the full target" — the default for headless tests
|
|
/// and for hosts that render to a whole window. The editor sets this to
|
|
/// the Viewport tab's rect from the docking shell so picking and
|
|
/// projection align with what the user sees inside the tab rather than
|
|
/// stretching across the whole window.
|
|
pub viewport_rect: Option<Rect>,
|
|
/// The background clear color (used by [`ClearPass`]).
|
|
pub clear_color: Color,
|
|
/// The camera to render from.
|
|
pub camera: &'a Camera,
|
|
/// The camera's world placement.
|
|
pub view_transform: &'a Transform,
|
|
/// Scene lighting.
|
|
pub lighting: &'a Lighting,
|
|
/// The drawables, already culled by the host (e.g. by camera
|
|
/// [`visibility`](Camera::visibility)).
|
|
pub objects: &'a [RenderObject<'a>],
|
|
}
|
|
|
|
impl FrameContext<'_> {
|
|
/// The viewport rect [`viewport_rect`](Self::viewport_rect) resolves to —
|
|
/// the explicit sub-rect when set, otherwise the full target.
|
|
pub fn resolved_viewport(&self) -> Rect {
|
|
self.viewport_rect.unwrap_or_else(|| {
|
|
Rect::from_min_size(
|
|
Vec2::ZERO,
|
|
Vec2::new(self.size.0.max(1) as f32, self.size.1.max(1) as f32),
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
/// One stage of the frame. Implement this to add a custom pass; register it on a
|
|
/// [`RenderPipeline`]. Passes are owned by the pipeline and run in order.
|
|
pub trait RenderPass {
|
|
/// Records this pass's GPU work for the frame.
|
|
fn run(&mut self, frame: &mut FrameContext<'_>);
|
|
}
|
|
|
|
struct PassEntry {
|
|
name: String,
|
|
enabled: bool,
|
|
pass: Box<dyn RenderPass>,
|
|
}
|
|
|
|
/// An ordered, named list of render passes.
|
|
///
|
|
/// Add passes with [`add_pass`](Self::add_pass), toggle them with
|
|
/// [`set_enabled`](Self::set_enabled), or drop them with [`remove`](Self::remove)
|
|
/// — all without touching any pass's implementation. [`render`](Self::render)
|
|
/// runs every enabled pass in order against one [`FrameContext`].
|
|
#[derive(Default)]
|
|
pub struct RenderPipeline {
|
|
passes: Vec<PassEntry>,
|
|
}
|
|
|
|
impl RenderPipeline {
|
|
/// An empty pipeline (no passes).
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// The default forward pipeline: a [`ClearPass`] followed by a
|
|
/// [`ForwardPass`]. Pixel-identical to the Stage-4 renderer's output.
|
|
pub fn forward(device: &wgpu::Device, color_format: wgpu::TextureFormat) -> Self {
|
|
let mut pipeline = Self::new();
|
|
pipeline.add_pass("clear", ClearPass);
|
|
pipeline.add_pass("forward", ForwardPass::new(device, color_format));
|
|
pipeline
|
|
}
|
|
|
|
/// Appends a named pass (enabled). Replaces any existing pass with the same
|
|
/// name, keeping its position.
|
|
pub fn add_pass(&mut self, name: impl Into<String>, pass: impl RenderPass + 'static) {
|
|
let name = name.into();
|
|
let entry = PassEntry {
|
|
name: name.clone(),
|
|
enabled: true,
|
|
pass: Box::new(pass),
|
|
};
|
|
match self.passes.iter_mut().find(|e| e.name == name) {
|
|
Some(existing) => *existing = entry,
|
|
None => self.passes.push(entry),
|
|
}
|
|
}
|
|
|
|
/// Inserts a pass before the pass named `before` (or at the end if not
|
|
/// found). Useful for slotting a post effect into a fixed position.
|
|
pub fn insert_before(
|
|
&mut self,
|
|
before: &str,
|
|
name: impl Into<String>,
|
|
pass: impl RenderPass + 'static,
|
|
) {
|
|
let entry = PassEntry {
|
|
name: name.into(),
|
|
enabled: true,
|
|
pass: Box::new(pass),
|
|
};
|
|
match self.passes.iter().position(|e| e.name == before) {
|
|
Some(index) => self.passes.insert(index, entry),
|
|
None => self.passes.push(entry),
|
|
}
|
|
}
|
|
|
|
/// Enables or disables the named pass. Returns whether it exists.
|
|
pub fn set_enabled(&mut self, name: &str, enabled: bool) -> bool {
|
|
match self.passes.iter_mut().find(|e| e.name == name) {
|
|
Some(entry) => {
|
|
entry.enabled = enabled;
|
|
true
|
|
}
|
|
None => false,
|
|
}
|
|
}
|
|
|
|
/// Removes the named pass. Returns whether it existed.
|
|
pub fn remove(&mut self, name: &str) -> bool {
|
|
let before = self.passes.len();
|
|
self.passes.retain(|e| e.name != name);
|
|
self.passes.len() != before
|
|
}
|
|
|
|
/// Whether a pass with this name is registered.
|
|
pub fn has_pass(&self, name: &str) -> bool {
|
|
self.passes.iter().any(|e| e.name == name)
|
|
}
|
|
|
|
/// The pass names in execution order.
|
|
pub fn pass_names(&self) -> impl Iterator<Item = &str> {
|
|
self.passes.iter().map(|e| e.name.as_str())
|
|
}
|
|
|
|
/// Runs every enabled pass in order against `frame`.
|
|
pub fn render(&mut self, frame: &mut FrameContext<'_>) {
|
|
for entry in &mut self.passes {
|
|
if entry.enabled {
|
|
entry.pass.run(frame);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A pass that clears the color target to [`FrameContext::clear_color`].
|
|
///
|
|
/// Conventionally the first pass, so later passes load over the cleared
|
|
/// background (matching the Stage-4 clear-then-draw flow).
|
|
pub struct ClearPass;
|
|
|
|
impl RenderPass for ClearPass {
|
|
fn run(&mut self, frame: &mut FrameContext<'_>) {
|
|
clear_view(frame.device, frame.queue, frame.color, frame.clear_color);
|
|
}
|
|
}
|
|
|
|
/// A pass that draws the frame's objects with the lit forward renderer.
|
|
///
|
|
/// Wraps the Stage-4 [`ForwardRenderer`]; the color target is *loaded* (so a
|
|
/// preceding [`ClearPass`] shows through), depth is managed internally.
|
|
pub struct ForwardPass {
|
|
renderer: ForwardRenderer,
|
|
}
|
|
|
|
impl ForwardPass {
|
|
/// Builds a forward pass for the given color target format.
|
|
pub fn new(device: &wgpu::Device, color_format: wgpu::TextureFormat) -> Self {
|
|
Self {
|
|
renderer: ForwardRenderer::new(device, color_format),
|
|
}
|
|
}
|
|
|
|
/// The wrapped renderer's color format.
|
|
pub fn color_format(&self) -> wgpu::TextureFormat {
|
|
self.renderer.color_format()
|
|
}
|
|
}
|
|
|
|
impl RenderPass for ForwardPass {
|
|
fn run(&mut self, frame: &mut FrameContext<'_>) {
|
|
self.renderer.render(
|
|
frame.device,
|
|
frame.queue,
|
|
frame.color,
|
|
frame.size,
|
|
frame.resolved_viewport(),
|
|
frame.camera,
|
|
frame.view_transform,
|
|
frame.lighting,
|
|
frame.objects,
|
|
);
|
|
}
|
|
}
|