From c06f1543db9b9d6db0dc2bedbcd260798a0fd78d Mon Sep 17 00:00:00 2001 From: xfy Date: Wed, 15 Jul 2026 00:22:39 +0800 Subject: [PATCH] params: BloomQuality enum + tiered web/desktop defaults + Off fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BloomQuality { Off, Low, Medium, High } controls pyramid depth (0-3). Web defaults to Low (1 level, minimal float textures), desktop to High (3 levels, full cinematic). Off skips all bloom passes and composites scene-only with ACES — the fallback path for WebGPU browsers without float-filterable support. The composite samples a 1x1 black texture when bloom is off. Note: this task gates Off vs full-On (3-level pyramid always runs when enabled). Partial pyramid for Low/Medium is deferred to the runtime rebuild in Task 8. --- src/params.rs | 23 ++++ src/render/plugin.rs | 283 +++++++++++++++++++++++-------------------- 2 files changed, 177 insertions(+), 129 deletions(-) diff --git a/src/params.rs b/src/params.rs index 251dd75..bb6ef5a 100644 --- a/src/params.rs +++ b/src/params.rs @@ -1,5 +1,26 @@ use bevy::prelude::*; +/// Bloom pyramid depth (number of bloom textures). +#[derive(Clone, Copy, PartialEq, Eq, Default)] +pub enum BloomQuality { + Off, // no bloom, scene-only ACES composite + Low, // 1 level: brightpass → composite (soft halo) + Medium, // 2 levels: brightpass → 1 down → 1 up → composite + #[default] + High, // 3 levels: brightpass → 2 down → 2 up → composite (full cinematic) +} + +impl BloomQuality { + pub fn levels(self) -> u32 { + match self { + BloomQuality::Off => 0, + BloomQuality::Low => 1, + BloomQuality::Medium => 2, + BloomQuality::High => 3, + } + } +} + /// All tunable black-hole parameters. Edited by the egui panel (Task 17), /// mirrored into BlackHoleUniforms each frame (Task 7). #[derive(Resource, Clone)] @@ -32,6 +53,7 @@ pub struct BlackHoleParams { pub bloom_threshold: f32, pub bloom_strength: f32, pub exposure: f32, + pub bloom_quality: BloomQuality, } impl Default for BlackHoleParams { @@ -57,6 +79,7 @@ impl Default for BlackHoleParams { bloom_threshold: 1.0, bloom_strength: 0.8, exposure: 1.0, + bloom_quality: if cfg!(target_arch = "wasm32") { BloomQuality::Low } else { BloomQuality::High }, } } } diff --git a/src/render/plugin.rs b/src/render/plugin.rs index 5316817..40ad254 100644 --- a/src/render/plugin.rs +++ b/src/render/plugin.rs @@ -177,144 +177,169 @@ fn spawn_fullscreen_quad( RenderLayers::layer(0), )); - // --- Bright-pass (bloom stage [2]): half-res float target --- - let bw = ((w as f32 * 0.5) as u32).max(1); - let bh = ((h as f32 * 0.5) as u32).max(1); - let bloom0 = images.add(Image::new_target_texture( - bw, bh, TextureFormat::Rgba16Float, None, - )); - commands.spawn(BloomTarget0(bloom0.clone())); - commands.spawn(( - Mesh2d(meshes.add(Rectangle::new(2.0, 2.0))), - MeshMaterial2d(bp_materials.add(crate::render::material::BrightPassMaterial { - threshold: 1.0, - source: offscreen.clone(), - })), - Transform::default().with_scale(Vec3::new(bw as f32 / 2.0, bh as f32 / 2.0, 1.0)), - BrightPassQuad, - QuadScaleFactor(0.5, 0.5), - RenderLayers::layer(2), - )); - commands.spawn(( - Camera2d, - Camera { - order: -19, - clear_color: ClearColorConfig::Custom(Color::srgb(0.0, 0.0, 0.0)), - ..default() - }, - RenderTarget::Image(bloom0.clone().into()), - Msaa::Off, - BrightPassCamera, - Nudgable, - RenderLayers::layer(2), - )); + let levels = params.bloom_quality.levels(); + let mut bloom_final_handle: Option> = None; - // --- Blur pyramid (bloom stages [3]/[4]): bloom_1, bloom_2 + down/up passes --- - let b1w = ((w as f32 * 0.25) as u32).max(1); - let b1h = ((h as f32 * 0.25) as u32).max(1); - let b2w = ((w as f32 * 0.125) as u32).max(1); - let b2h = ((h as f32 * 0.125) as u32).max(1); - let bloom1 = images.add(Image::new_target_texture( - b1w, b1h, TextureFormat::Rgba16Float, None, - )); - let bloom2 = images.add(Image::new_target_texture( - b2w, b2h, TextureFormat::Rgba16Float, None, - )); - commands.spawn(BloomTarget1(bloom1.clone())); - commands.spawn(BloomTarget2(bloom2.clone())); + if levels > 0 { + // NOTE: when bloom is enabled the full 3-level pyramid (High) always + // runs here. Partial pyramid — Low = brightpass-only, Medium = 1 down + + // 1 up — is deferred to the runtime rebuild in Task 8, which can spawn + // fewer passes. For now this is a simple Off vs On gate. + // + // --- Bright-pass (bloom stage [2]): half-res float target --- + let bw = ((w as f32 * 0.5) as u32).max(1); + let bh = ((h as f32 * 0.5) as u32).max(1); + let bloom0 = images.add(Image::new_target_texture( + bw, bh, TextureFormat::Rgba16Float, None, + )); + commands.spawn(BloomTarget0(bloom0.clone())); + commands.spawn(( + Mesh2d(meshes.add(Rectangle::new(2.0, 2.0))), + MeshMaterial2d(bp_materials.add(crate::render::material::BrightPassMaterial { + threshold: 1.0, + source: offscreen.clone(), + })), + Transform::default().with_scale(Vec3::new(bw as f32 / 2.0, bh as f32 / 2.0, 1.0)), + BrightPassQuad, + QuadScaleFactor(0.5, 0.5), + RenderLayers::layer(2), + )); + commands.spawn(( + Camera2d, + Camera { + order: -19, + clear_color: ClearColorConfig::Custom(Color::srgb(0.0, 0.0, 0.0)), + ..default() + }, + RenderTarget::Image(bloom0.clone().into()), + Msaa::Off, + BrightPassCamera, + Nudgable, + RenderLayers::layer(2), + )); - // Down pass 0→1: samples bloom0 (half-res), writes bloom1 (quarter-res). - let down01_texel = Vec2::new(1.0 / bw as f32, 1.0 / bh as f32); - commands.spawn(( - Mesh2d(meshes.add(Rectangle::new(2.0, 2.0))), - MeshMaterial2d(blur_materials.add(crate::render::material::BlurMaterial { - uniform: crate::render::material::BlurUniform { - mode: 0, texel_size: down01_texel, blend: 0.0, _pad0: 0.0, - }, - source: bloom0.clone(), - })), - Transform::default().with_scale(Vec3::new(b1w as f32 / 2.0, b1h as f32 / 2.0, 1.0)), - BlurQuad, - QuadScaleFactor(0.25, 0.25), - RenderLayers::layer(3), - )); - // Down pass 1→2: samples bloom1 (quarter), writes bloom2 (eighth). - let down12_texel = Vec2::new(1.0 / b1w as f32, 1.0 / b1h as f32); - commands.spawn(( - Mesh2d(meshes.add(Rectangle::new(2.0, 2.0))), - MeshMaterial2d(blur_materials.add(crate::render::material::BlurMaterial { - uniform: crate::render::material::BlurUniform { - mode: 0, texel_size: down12_texel, blend: 0.0, _pad0: 0.0, - }, - source: bloom1.clone(), - })), - Transform::default().with_scale(Vec3::new(b2w as f32 / 2.0, b2h as f32 / 2.0, 1.0)), - BlurQuad, - QuadScaleFactor(0.125, 0.125), - RenderLayers::layer(4), - )); - // Up pass 2→1: samples bloom2 (eighth), writes bloom1 (quarter). - let up21_texel = Vec2::new(1.0 / b2w as f32, 1.0 / b2h as f32); - commands.spawn(( - Mesh2d(meshes.add(Rectangle::new(2.0, 2.0))), - MeshMaterial2d(blur_materials.add(crate::render::material::BlurMaterial { - uniform: crate::render::material::BlurUniform { - mode: 1, texel_size: up21_texel, blend: 0.6, _pad0: 0.0, - }, - source: bloom2.clone(), - })), - Transform::default().with_scale(Vec3::new(b1w as f32 / 2.0, b1h as f32 / 2.0, 1.0)), - BlurQuad, - QuadScaleFactor(0.25, 0.25), - RenderLayers::layer(5), - )); - // Up pass 1→0: samples bloom1 (quarter), writes bloom_final (half). - let bfw = bw; - let bfh = bh; - let bloom_final = images.add(Image::new_target_texture( - bfw, bfh, TextureFormat::Rgba16Float, None, - )); - commands.spawn(BloomFinalTarget(bloom_final.clone())); - let up10_texel = Vec2::new(1.0 / b1w as f32, 1.0 / b1h as f32); - commands.spawn(( - Mesh2d(meshes.add(Rectangle::new(2.0, 2.0))), - MeshMaterial2d(blur_materials.add(crate::render::material::BlurMaterial { - uniform: crate::render::material::BlurUniform { - mode: 1, texel_size: up10_texel, blend: 0.8, _pad0: 0.0, - }, - source: bloom1.clone(), - })), - Transform::default().with_scale(Vec3::new(bfw as f32 / 2.0, bfh as f32 / 2.0, 1.0)), - BlurQuad, - QuadScaleFactor(0.5, 0.5), - RenderLayers::layer(6), - )); - // Cameras: down01=-18, down12=-17, up21=-16, up10=-15. - commands.spawn(( - Camera2d, Camera { order: -18, clear_color: ClearColorConfig::Custom(Color::srgb(0.0,0.0,0.0)), ..default() }, - RenderTarget::Image(bloom1.clone().into()), Msaa::Off, BlurCamera, Nudgable, RenderLayers::layer(3), - )); - commands.spawn(( - Camera2d, Camera { order: -17, clear_color: ClearColorConfig::Custom(Color::srgb(0.0,0.0,0.0)), ..default() }, - RenderTarget::Image(bloom2.clone().into()), Msaa::Off, BlurCamera, Nudgable, RenderLayers::layer(4), - )); - commands.spawn(( - Camera2d, Camera { order: -16, clear_color: ClearColorConfig::Custom(Color::srgb(0.0,0.0,0.0)), ..default() }, - RenderTarget::Image(bloom1.clone().into()), Msaa::Off, BlurCamera, Nudgable, RenderLayers::layer(5), - )); - commands.spawn(( - Camera2d, Camera { order: -15, clear_color: ClearColorConfig::Custom(Color::srgb(0.0,0.0,0.0)), ..default() }, - RenderTarget::Image(bloom_final.clone().into()), Msaa::Off, BlurCamera, Nudgable, RenderLayers::layer(6), - )); + // --- Blur pyramid (bloom stages [3]/[4]): bloom_1, bloom_2 + down/up passes --- + let b1w = ((w as f32 * 0.25) as u32).max(1); + let b1h = ((h as f32 * 0.25) as u32).max(1); + let b2w = ((w as f32 * 0.125) as u32).max(1); + let b2h = ((h as f32 * 0.125) as u32).max(1); + let bloom1 = images.add(Image::new_target_texture( + b1w, b1h, TextureFormat::Rgba16Float, None, + )); + let bloom2 = images.add(Image::new_target_texture( + b2w, b2h, TextureFormat::Rgba16Float, None, + )); + commands.spawn(BloomTarget1(bloom1.clone())); + commands.spawn(BloomTarget2(bloom2.clone())); + + // Down pass 0→1: samples bloom0 (half-res), writes bloom1 (quarter-res). + let down01_texel = Vec2::new(1.0 / bw as f32, 1.0 / bh as f32); + commands.spawn(( + Mesh2d(meshes.add(Rectangle::new(2.0, 2.0))), + MeshMaterial2d(blur_materials.add(crate::render::material::BlurMaterial { + uniform: crate::render::material::BlurUniform { + mode: 0, texel_size: down01_texel, blend: 0.0, _pad0: 0.0, + }, + source: bloom0.clone(), + })), + Transform::default().with_scale(Vec3::new(b1w as f32 / 2.0, b1h as f32 / 2.0, 1.0)), + BlurQuad, + QuadScaleFactor(0.25, 0.25), + RenderLayers::layer(3), + )); + // Down pass 1→2: samples bloom1 (quarter), writes bloom2 (eighth). + let down12_texel = Vec2::new(1.0 / b1w as f32, 1.0 / b1h as f32); + commands.spawn(( + Mesh2d(meshes.add(Rectangle::new(2.0, 2.0))), + MeshMaterial2d(blur_materials.add(crate::render::material::BlurMaterial { + uniform: crate::render::material::BlurUniform { + mode: 0, texel_size: down12_texel, blend: 0.0, _pad0: 0.0, + }, + source: bloom1.clone(), + })), + Transform::default().with_scale(Vec3::new(b2w as f32 / 2.0, b2h as f32 / 2.0, 1.0)), + BlurQuad, + QuadScaleFactor(0.125, 0.125), + RenderLayers::layer(4), + )); + // Up pass 2→1: samples bloom2 (eighth), writes bloom1 (quarter). + let up21_texel = Vec2::new(1.0 / b2w as f32, 1.0 / b2h as f32); + commands.spawn(( + Mesh2d(meshes.add(Rectangle::new(2.0, 2.0))), + MeshMaterial2d(blur_materials.add(crate::render::material::BlurMaterial { + uniform: crate::render::material::BlurUniform { + mode: 1, texel_size: up21_texel, blend: 0.6, _pad0: 0.0, + }, + source: bloom2.clone(), + })), + Transform::default().with_scale(Vec3::new(b1w as f32 / 2.0, b1h as f32 / 2.0, 1.0)), + BlurQuad, + QuadScaleFactor(0.25, 0.25), + RenderLayers::layer(5), + )); + // Up pass 1→0: samples bloom1 (quarter), writes bloom_final (half). + let bfw = bw; + let bfh = bh; + let bloom_final = images.add(Image::new_target_texture( + bfw, bfh, TextureFormat::Rgba16Float, None, + )); + commands.spawn(BloomFinalTarget(bloom_final.clone())); + let up10_texel = Vec2::new(1.0 / b1w as f32, 1.0 / b1h as f32); + commands.spawn(( + Mesh2d(meshes.add(Rectangle::new(2.0, 2.0))), + MeshMaterial2d(blur_materials.add(crate::render::material::BlurMaterial { + uniform: crate::render::material::BlurUniform { + mode: 1, texel_size: up10_texel, blend: 0.8, _pad0: 0.0, + }, + source: bloom1.clone(), + })), + Transform::default().with_scale(Vec3::new(bfw as f32 / 2.0, bfh as f32 / 2.0, 1.0)), + BlurQuad, + QuadScaleFactor(0.5, 0.5), + RenderLayers::layer(6), + )); + // Cameras: down01=-18, down12=-17, up21=-16, up10=-15. + commands.spawn(( + Camera2d, Camera { order: -18, clear_color: ClearColorConfig::Custom(Color::srgb(0.0,0.0,0.0)), ..default() }, + RenderTarget::Image(bloom1.clone().into()), Msaa::Off, BlurCamera, Nudgable, RenderLayers::layer(3), + )); + commands.spawn(( + Camera2d, Camera { order: -17, clear_color: ClearColorConfig::Custom(Color::srgb(0.0,0.0,0.0)), ..default() }, + RenderTarget::Image(bloom2.clone().into()), Msaa::Off, BlurCamera, Nudgable, RenderLayers::layer(4), + )); + commands.spawn(( + Camera2d, Camera { order: -16, clear_color: ClearColorConfig::Custom(Color::srgb(0.0,0.0,0.0)), ..default() }, + RenderTarget::Image(bloom1.clone().into()), Msaa::Off, BlurCamera, Nudgable, RenderLayers::layer(5), + )); + commands.spawn(( + Camera2d, Camera { order: -15, clear_color: ClearColorConfig::Custom(Color::srgb(0.0,0.0,0.0)), ..default() }, + RenderTarget::Image(bloom_final.clone().into()), Msaa::Off, BlurCamera, Nudgable, RenderLayers::layer(6), + )); + + bloom_final_handle = Some(bloom_final.clone()); + } // --- Composite quad (draws HDR scene + bloom to the window, ACES tone-mapped) --- - // bloom_final comes from the blur pyramid (Task 5). + // When bloom is off (BloomQuality::Off), bloom_final_handle is None and we + // fall back to a 1x1 black texture: the composite samples black, yielding a + // scene-only ACES composite (no bloom contribution). + let bloom_handle = bloom_final_handle.unwrap_or_else(|| { + // 1x1 black fallback — composite samples black, effectively scene-only ACES. + // 8 zero bytes = one Rgba16Float pixel (16 bits/channel × 4). + images.add(Image::new_fill( + bevy::render::render_resource::Extent3d { width: 1, height: 1, depth_or_array_layers: 1 }, + bevy::render::render_resource::TextureDimension::D2, + &[0, 0, 0, 0, 0, 0, 0, 0], + TextureFormat::Rgba16Float, + bevy::asset::RenderAssetUsages::default(), + )) + }); let composite_mat = composite_materials.add(crate::render::material::CompositeMaterial { uniform: crate::render::material::CompositeUniform { bloom_strength: 0.8, exposure: 1.0, _pad0: 0.0, _pad1: 0.0, }, scene: offscreen.clone(), - bloom: bloom_final.clone(), + bloom: bloom_handle, }); commands.spawn(( Mesh2d(meshes.add(Rectangle::new(2.0, 2.0))),