render: bright-pass material + camera (bloom stage [2])

Adds BrightPassMaterial + brightpass.wgsl: extracts luminance above a
soft-knee threshold from the HDR offscreen into a half-res Rgba16Float
target (bloom_0). Camera order -19 (after offscreen at -20). The brightpass
output is not yet composited (Task 6); this commit only adds the pass and
confirms it runs without crashing.
This commit is contained in:
xfy 2026-07-15 00:01:31 +08:00
parent 0f33d80fd3
commit 6ccd0a173b
3 changed files with 89 additions and 0 deletions

View File

@ -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<uniform> u: BrightPassUniform;
@group(#{MATERIAL_BIND_GROUP}) @binding(1) var tex: texture_2d<f32>;
@group(#{MATERIAL_BIND_GROUP}) @binding(2) var samp: sampler;
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
let hdr = textureSample(tex, samp, in.uv).rgb;
let lum = dot(hdr, vec3<f32>(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<f32>(contribution, 1.0);
}

View File

@ -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<Image>,
}
impl Material2d for BrightPassMaterial {
fn fragment_shader() -> ShaderRef {
"shaders/brightpass.wgsl".into()
}
}

View File

@ -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<Image>);
/// 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::<crate::params::BlackHoleParams>()
.add_plugins(Material2dPlugin::<BlackHoleMaterial>::default())
.add_plugins(Material2dPlugin::<crate::render::material::UpscaleMaterial>::default())
.add_plugins(Material2dPlugin::<crate::render::material::BrightPassMaterial>::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<Assets<Mesh>>,
mut materials: ResMut<Assets<BlackHoleMaterial>>,
mut upscale_materials: ResMut<Assets<crate::render::material::UpscaleMaterial>>,
mut bp_materials: ResMut<Assets<crate::render::material::BrightPassMaterial>>,
mut buffers: ResMut<Assets<ShaderBuffer>>,
mut images: ResMut<Assets<Image>>,
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))),