diff --git a/assets/shaders/brightpass.wgsl b/assets/shaders/brightpass.wgsl new file mode 100644 index 0000000..a47912f --- /dev/null +++ b/assets/shaders/brightpass.wgsl @@ -0,0 +1,23 @@ +#import bevy_sprite::mesh2d_vertex_output::VertexOutput + +struct BrightPassUniform { + threshold: f32, + _pad0: f32, + _pad1: f32, + _pad2: f32, +}; + +@group(#{MATERIAL_BIND_GROUP}) @binding(0) var u: BrightPassUniform; +@group(#{MATERIAL_BIND_GROUP}) @binding(1) var tex: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(2) var samp: sampler; + +@fragment +fn fragment(in: VertexOutput) -> @location(0) vec4 { + let hdr = textureSample(tex, samp, in.uv).rgb; + let lum = dot(hdr, vec3(0.2126, 0.7152, 0.0722)); + // Soft knee: smooth roll-off instead of a hard threshold cut. + // Fully-bright passes through; near-threshold tapers to zero. + let soft = max(lum - u.threshold, 0.0) / (lum + 0.0001); + let contribution = hdr * soft; + return vec4(contribution, 1.0); +} diff --git a/src/render/material.rs b/src/render/material.rs index 79d0065..8997101 100644 --- a/src/render/material.rs +++ b/src/render/material.rs @@ -138,4 +138,20 @@ impl Material2d for UpscaleMaterial { } } +/// Extracts luminance above a threshold from the HDR offscreen into a +/// half-res float texture (bloom stage [2]). Soft-knee, not hard cut. +#[derive(Asset, TypePath, AsBindGroup, Clone)] +pub struct BrightPassMaterial { + #[uniform(0)] + pub threshold: f32, + #[texture(1)] + #[sampler(2)] + pub source: Handle, +} + +impl Material2d for BrightPassMaterial { + fn fragment_shader() -> ShaderRef { + "shaders/brightpass.wgsl".into() + } +} diff --git a/src/render/plugin.rs b/src/render/plugin.rs index 8ee6f29..5ae35e2 100644 --- a/src/render/plugin.rs +++ b/src/render/plugin.rs @@ -49,6 +49,22 @@ pub struct QuadScaleFactor(pub f32, pub f32); #[derive(Component)] pub struct CompositeQuad; +/// The half-res Image the bright-pass writes into (bloom stage [2]). +#[derive(Component)] +pub struct BloomTarget0(pub Handle); + +/// Camera + quad markers for the bright-pass (for rebuild queries). +#[derive(Component)] +pub struct BrightPassCamera; +#[derive(Component)] +pub struct BrightPassQuad; + +/// Camera + quad markers for the blur passes (for rebuild queries). +#[derive(Component)] +pub struct BlurCamera; +#[derive(Component)] +pub struct BlurQuad; + pub struct BlackHolePlugin; impl Plugin for BlackHolePlugin { @@ -58,6 +74,7 @@ impl Plugin for BlackHolePlugin { .init_resource::() .add_plugins(Material2dPlugin::::default()) .add_plugins(Material2dPlugin::::default()) + .add_plugins(Material2dPlugin::::default()) .add_plugins(bevy_egui::EguiPlugin::default()) .add_systems(Startup, spawn_fullscreen_quad) .add_systems(Startup, crate::scene::planets::spawn_default_planet) @@ -83,6 +100,7 @@ fn spawn_fullscreen_quad( mut meshes: ResMut>, mut materials: ResMut>, mut upscale_materials: ResMut>, + mut bp_materials: ResMut>, mut buffers: ResMut>, mut images: ResMut>, window: Query<&Window>, @@ -147,6 +165,38 @@ 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), + )); + // --- Upscale quad (draws offscreen Image to the window) --- commands.spawn(( Mesh2d(meshes.add(Rectangle::new(2.0, 2.0))),