render: blur pyramid (bloom stages [3]/[4]) — down then up
Adds BlurMaterial + blur.wgsl with a 13-tap weighted Gaussian kernel (var<private> arrays — naga rejects dynamic indexing of const arrays), run in downsample then upsample passes. For the 3-level pyramid: bloom_0 (half, from brightpass) → bloom_1 (quarter) → bloom_2 (eighth), then upsample to bloom_final (half). Camera orders -18/-17/-16/-15. The bloom output is not yet composited (Task 6); this commit only wires the passes and confirms they run in sequence without crashing.
This commit is contained in:
parent
6ccd0a173b
commit
ed6639738f
52
assets/shaders/blur.wgsl
Normal file
52
assets/shaders/blur.wgsl
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
|
||||||
|
|
||||||
|
struct BlurUniform {
|
||||||
|
mode: u32, // 0 = downsample, 1 = upsample
|
||||||
|
texel_size: vec2<f32>,
|
||||||
|
blend: f32, // upsample blend factor (ignored for down)
|
||||||
|
_pad0: f32,
|
||||||
|
};
|
||||||
|
|
||||||
|
@group(#{MATERIAL_BIND_GROUP}) @binding(0) var<uniform> u: BlurUniform;
|
||||||
|
@group(#{MATERIAL_BIND_GROUP}) @binding(1) var tex: texture_2d<f32>;
|
||||||
|
@group(#{MATERIAL_BIND_GROUP}) @binding(2) var samp: sampler;
|
||||||
|
|
||||||
|
// 13-tap weighted kernel (Gaussian approximation for HDR bloom).
|
||||||
|
// NOTE: naga rejects dynamic indexing of const arrays ("may only be indexed
|
||||||
|
// by a constant"), so we use var<private> here — it allows runtime indexing.
|
||||||
|
var<private> KERNEL_OFFSETS: array<vec2<f32>, 13> = array<vec2<f32>, 13>(
|
||||||
|
vec2<f32>( 0.0, 0.0),
|
||||||
|
vec2<f32>( 1.0, 0.0), vec2<f32>(-1.0, 0.0),
|
||||||
|
vec2<f32>( 0.0, 1.0), vec2<f32>( 0.0, -1.0),
|
||||||
|
vec2<f32>( 1.0, 1.0), vec2<f32>(-1.0, 1.0),
|
||||||
|
vec2<f32>( 1.0, -1.0), vec2<f32>(-1.0, -1.0),
|
||||||
|
vec2<f32>( 2.0, 0.0), vec2<f32>(-2.0, 0.0),
|
||||||
|
vec2<f32>( 0.0, 2.0), vec2<f32>( 0.0, -2.0),
|
||||||
|
);
|
||||||
|
var<private> KERNEL_WEIGHTS: array<f32, 13> = array<f32, 13>(
|
||||||
|
0.5,
|
||||||
|
0.25, 0.25, 0.25, 0.25,
|
||||||
|
0.125, 0.125, 0.125, 0.125,
|
||||||
|
0.0625, 0.0625, 0.0625, 0.0625,
|
||||||
|
);
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
let uv = in.uv;
|
||||||
|
// Downsample and upsample share the same 13-tap weighted kernel.
|
||||||
|
// mode 0 = down (plain weighted average), mode 1 = up (scaled by blend).
|
||||||
|
var sum = vec3<f32>(0.0);
|
||||||
|
var wsum = 0.0;
|
||||||
|
for (var i: i32 = 0; i < 13; i = i + 1) {
|
||||||
|
let off = KERNEL_OFFSETS[i] * u.texel_size;
|
||||||
|
let c = textureSample(tex, samp, uv + off).rgb;
|
||||||
|
sum = sum + c * KERNEL_WEIGHTS[i];
|
||||||
|
wsum = wsum + KERNEL_WEIGHTS[i];
|
||||||
|
}
|
||||||
|
let blurred = sum / wsum;
|
||||||
|
if (u.mode == 0u) {
|
||||||
|
return vec4<f32>(blurred, 1.0);
|
||||||
|
} else {
|
||||||
|
return vec4<f32>(blurred * u.blend, 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -155,3 +155,29 @@ impl Material2d for BrightPassMaterial {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Uniform for one blur pass (bloom stages [3]/[4]).
|
||||||
|
#[derive(Clone, ShaderType)]
|
||||||
|
pub struct BlurUniform {
|
||||||
|
pub mode: u32, // 0 = downsample, 1 = upsample
|
||||||
|
pub texel_size: Vec2,
|
||||||
|
pub blend: f32, // upsample blend factor (ignored for down)
|
||||||
|
pub _pad0: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One pass of the bloom down/up pyramid. One material instance per pass
|
||||||
|
/// (Bevy binds uniforms once per material, not per entity).
|
||||||
|
#[derive(Asset, TypePath, AsBindGroup, Clone)]
|
||||||
|
pub struct BlurMaterial {
|
||||||
|
#[uniform(0)]
|
||||||
|
pub uniform: BlurUniform,
|
||||||
|
#[texture(1)]
|
||||||
|
#[sampler(2)]
|
||||||
|
pub source: Handle<Image>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Material2d for BlurMaterial {
|
||||||
|
fn fragment_shader() -> ShaderRef {
|
||||||
|
"shaders/blur.wgsl".into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -65,6 +65,16 @@ pub struct BlurCamera;
|
|||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub struct BlurQuad;
|
pub struct BlurQuad;
|
||||||
|
|
||||||
|
/// Bloom pyramid textures bloom_1, bloom_2 (bloom_0 is BloomTarget0).
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct BloomTarget1(pub Handle<Image>);
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct BloomTarget2(pub Handle<Image>);
|
||||||
|
|
||||||
|
/// The final up-sampled bloom texture read by the composite pass.
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct BloomFinalTarget(pub Handle<Image>);
|
||||||
|
|
||||||
pub struct BlackHolePlugin;
|
pub struct BlackHolePlugin;
|
||||||
|
|
||||||
impl Plugin for BlackHolePlugin {
|
impl Plugin for BlackHolePlugin {
|
||||||
@ -75,6 +85,7 @@ impl Plugin for BlackHolePlugin {
|
|||||||
.add_plugins(Material2dPlugin::<BlackHoleMaterial>::default())
|
.add_plugins(Material2dPlugin::<BlackHoleMaterial>::default())
|
||||||
.add_plugins(Material2dPlugin::<crate::render::material::UpscaleMaterial>::default())
|
.add_plugins(Material2dPlugin::<crate::render::material::UpscaleMaterial>::default())
|
||||||
.add_plugins(Material2dPlugin::<crate::render::material::BrightPassMaterial>::default())
|
.add_plugins(Material2dPlugin::<crate::render::material::BrightPassMaterial>::default())
|
||||||
|
.add_plugins(Material2dPlugin::<crate::render::material::BlurMaterial>::default())
|
||||||
.add_plugins(bevy_egui::EguiPlugin::default())
|
.add_plugins(bevy_egui::EguiPlugin::default())
|
||||||
.add_systems(Startup, spawn_fullscreen_quad)
|
.add_systems(Startup, spawn_fullscreen_quad)
|
||||||
.add_systems(Startup, crate::scene::planets::spawn_default_planet)
|
.add_systems(Startup, crate::scene::planets::spawn_default_planet)
|
||||||
@ -101,6 +112,7 @@ fn spawn_fullscreen_quad(
|
|||||||
mut materials: ResMut<Assets<BlackHoleMaterial>>,
|
mut materials: ResMut<Assets<BlackHoleMaterial>>,
|
||||||
mut upscale_materials: ResMut<Assets<crate::render::material::UpscaleMaterial>>,
|
mut upscale_materials: ResMut<Assets<crate::render::material::UpscaleMaterial>>,
|
||||||
mut bp_materials: ResMut<Assets<crate::render::material::BrightPassMaterial>>,
|
mut bp_materials: ResMut<Assets<crate::render::material::BrightPassMaterial>>,
|
||||||
|
mut blur_materials: ResMut<Assets<crate::render::material::BlurMaterial>>,
|
||||||
mut buffers: ResMut<Assets<ShaderBuffer>>,
|
mut buffers: ResMut<Assets<ShaderBuffer>>,
|
||||||
mut images: ResMut<Assets<Image>>,
|
mut images: ResMut<Assets<Image>>,
|
||||||
window: Query<&Window>,
|
window: Query<&Window>,
|
||||||
@ -197,6 +209,104 @@ fn spawn_fullscreen_quad(
|
|||||||
RenderLayers::layer(2),
|
RenderLayers::layer(2),
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// --- 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),
|
||||||
|
));
|
||||||
|
|
||||||
// --- Upscale quad (draws offscreen Image to the window) ---
|
// --- Upscale quad (draws offscreen Image to the window) ---
|
||||||
commands.spawn((
|
commands.spawn((
|
||||||
Mesh2d(meshes.add(Rectangle::new(2.0, 2.0))),
|
Mesh2d(meshes.add(Rectangle::new(2.0, 2.0))),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user