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>
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
//! [`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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user