# Cinematic Rendering Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Raise the renderer from "geometrically correct but flat" to Gargantua-reference fidelity: HDR color + ACES tone mapping, multi-pass HDR bloom, domain-warped FBM disk texture, and anti-aliased round stars — all configurable via a dedicated Quality panel with tiered web defaults. **Architecture:** A 5-stage HDR pipeline replaces the 2-pass offscreen→upscale path: `Offscreen (Rgba16Float) → BrightPass → Down-pyramid → Up-pyramid → Composite+ACES`. The geodesic integrator is untouched; all changes are in compositing/shading. **Tech Stack:** Bevy 0.19, WGSL, egui 0.41. No new crate dependencies. **Spec:** `docs/superpowers/specs/2026-07-14-blackhole-cinematic-rendering-design.md` --- ## Conventions used throughout this plan **Camera `order` is `i32`; lower renders first.** The full pipeline order, set once and referenced by all tasks: | Camera | order | Renders into | |---------------|-------|-------------------| | offscreen | -20 | offscreen_hdr | | brightpass | -19 | bloom_0 | | blur down01 | -18 | bloom_1 | | blur down12 | -17 | bloom_2 | | blur up21 | -16 | bloom_1 | | blur up10 | -15 | bloom_final | | composite | 0 | window | **RenderLayers** isolate each camera's draw to its own quad: layer 0 = offscreen, 1 = composite, 2 = brightpass, 3 = down01, 4 = down12, 5 = up21, 6 = up10. **`QuadScaleFactor(f32, f32)`** — a component on every quad storing the fraction of the offscreen resolution that the quad's target fills. Used by `resize_offscreen` to rescale each quad independently: - offscreen quad: `(1.0, 1.0)` - brightpass (writes bloom_0 at half-res): `(0.5, 0.5)` - blur down01 (writes bloom_1 at quarter): `(0.25, 0.25)` - blur down12 (writes bloom_2 at eighth): `(0.125, 0.125)` - blur up21 (writes bloom_1 at quarter): `(0.25, 0.25)` - blur up10 (writes bloom_final at half): `(0.5, 0.5)` - composite quad: rescaled against the WINDOW (not offscreen), so it carries a `CompositeQuad` marker instead and is handled separately in resize. --- ## File Structure **Created:** - `assets/shaders/brightpass.wgsl` — stage [2]: extracts luminance > threshold with soft knee - `assets/shaders/blur.wgsl` — stages [3]/[4]: one shader, down/up modes via uniform - `assets/shaders/composite.wgsl` — stage [5]: ACES tone-map + bloom blend, replaces upscale.wgsl **Modified:** - `assets/shaders/black_hole.wgsl` — FBM disk noise, gaussian-speck stars, `star_aa` branch, new uniform fields read - `src/render/material.rs` — add `BrightPassMaterial`, `BlurMaterial`, `CompositeMaterial`; add new uniform fields to `BlackHoleUniforms`; remove `UpscaleMaterial` - `src/render/plugin.rs` — spawn bloom targets/materials/cameras; `Nudgable` + `QuadScaleFactor` + `CompositeQuad` markers; generalize `nudge_camera`; bloom quality rebuild on param change - `src/params.rs` — add `BloomQuality` enum + quality fields to `BlackHoleParams`; tiered defaults - `src/ui.rs` — new `Quality` panel section **Deleted:** - `assets/shaders/upscale.wgsl` --- ## Task 0: Establish green baseline **Files:** none - [ ] **Step 1: Run the existing test suite** Run: `cargo test` Expected: all physics tests pass (b_crit capture/escape, spin=0 degeneracy, spin>0 capture). - [ ] **Step 2: Confirm the desktop build runs** Run: `cargo build --release` Expected: compiles without error. - [ ] **Step 3: No commit** — verification gate only. --- ## Task 1: Offscreen format → Rgba16Float + camera order + markers **Files:** - Modify: `src/render/plugin.rs` (offscreen texture format, offscreen camera order, add `Nudgable`/`QuadScaleFactor`/`CompositeQuad` markers to offscreen + upscale cameras/quads, generalize `nudge_camera`) This switches the offscreen render target to float, sets up the camera order for the bloom pipeline, and introduces the marker components all later tasks depend on. No bloom yet — the upscale path still works. - [ ] **Step 1: Define the new marker components** In `src/render/plugin.rs`, after the `UpscaleQuad` component definition (around line 34), add: ```rust /// Marker for any camera that must be nudged each frame (Bevy 0.19 #24448 /// workaround). All render cameras carry this. #[derive(Component)] pub struct Nudgable; /// Stores the fraction of the offscreen resolution that this quad's target /// fills. Used by resize_offscreen to rescale each quad independently. /// (1.0, 1.0) = full offscreen res; (0.5, 0.5) = half; etc. #[derive(Component)] pub struct QuadScaleFactor(pub f32, pub f32); /// Marks the composite quad (renders to the window, not an offscreen Image). /// resize_offscreen rescales it against the window, not the offscreen res. #[derive(Component)] pub struct CompositeQuad; ``` - [ ] **Step 2: Change the offscreen texture format in `spawn_fullscreen_quad`** In `src/render/plugin.rs`, find the `Image::new_target_texture` call in `spawn_fullscreen_quad` (around line 84) and change the format: ```rust let offscreen = images.add(Image::new_target_texture( w, h, TextureFormat::Rgba16Float, // was Bgra8UnormSrgb — HDR for bloom headroom None, )); ``` - [ ] **Step 3: Add `QuadScaleFactor` to the offscreen quad** In `spawn_fullscreen_quad`, in the offscreen quad spawn (the `FullscreenQuad` `commands.spawn(...)`, around line 111), add `QuadScaleFactor(1.0, 1.0)` to the tuple: ```rust Transform::default().with_scale(Vec3::new(half_w, half_h, 1.0)), FullscreenQuad, QuadScaleFactor(1.0, 1.0), RenderLayers::layer(0), ``` - [ ] **Step 4: Set the offscreen camera order to -20 and add `Nudgable`** In `spawn_fullscreen_quad`, in the offscreen camera spawn (the `commands.spawn((Camera2d, Camera { order: -1, ... }))` around line 119), change `order: -1` to `order: -20` and add `Nudgable,`: ```rust Camera { order: -20, clear_color: ClearColorConfig::Custom(Color::srgb(0.1, 0.1, 0.1)), ..default() }, RenderTarget::Image(offscreen.clone().into()), Msaa::Off, OffscreenCamera, Nudgable, RenderLayers::layer(0), ``` - [ ] **Step 5: Tag the upscale quad + camera with `CompositeQuad` + `Nudgable`** In `spawn_fullscreen_quad`, in the upscale quad spawn (around line 133), add `CompositeQuad,`: ```rust Transform::default().with_scale(Vec3::new(win.width() / 2.0, win.height() / 2.0, 1.0)), UpscaleQuad, CompositeQuad, RenderLayers::layer(1), ``` In the upscale camera spawn (around line 142), add `Nudgable,`: ```rust commands.spawn((Camera2d, Msaa::Off, UpscaleCamera, Nudgable, RenderLayers::layer(1))); ``` - [ ] **Step 6: Generalize `nudge_camera` to use the `Nudgable` marker** In `src/render/plugin.rs`, replace the `nudge_camera` function signature and query: ```rust fn nudge_camera( time: Res