use crate::render::wgpu::rendergraph::{PassExecutionContext, PassNode};
use wgpu::util::DeviceExt;
const MIP_CHAIN_LENGTH: usize = 6;
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct BloomParams {
filter_radius: f32,
_padding0: f32,
_padding1: f32,
_padding2: f32,
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct DownsampleParams {
mip_level: u32,
_padding0: u32,
_padding1: u32,
_padding2: u32,
}
struct BloomMip {
_texture: wgpu::Texture,
view: wgpu::TextureView,
}
struct CachedBloomState {
mip_chain: Vec<BloomMip>,
downsample_bind_groups: Vec<wgpu::BindGroup>,
upsample_bind_groups: Vec<wgpu::BindGroup>,
}
pub struct BloomPass {
downsample_pipeline: wgpu::RenderPipeline,
upsample_pipeline: wgpu::RenderPipeline,
downsample_bind_group_layout: wgpu::BindGroupLayout,
upsample_bind_group_layout: wgpu::BindGroupLayout,
sampler: wgpu::Sampler,
params_buffer: wgpu::Buffer,
downsample_params_buffers: Vec<wgpu::Buffer>,
mip_chain: Vec<BloomMip>,
downsample_bind_groups: Vec<wgpu::BindGroup>,
upsample_bind_groups: Vec<wgpu::BindGroup>,
current_size: (u32, u32),
cached_states: std::collections::HashMap<(u32, u32), CachedBloomState>,
}
impl BloomPass {
pub fn new(device: &wgpu::Device, width: u32, height: u32) -> Self {
let downsample_shader =
device.create_shader_module(wgpu::include_wgsl!("../../shaders/bloom_downsample.wgsl"));
let upsample_shader =
device.create_shader_module(wgpu::include_wgsl!("../../shaders/bloom_upsample.wgsl"));
let params = BloomParams {
filter_radius: 0.005,
_padding0: 0.0,
_padding1: 0.0,
_padding2: 0.0,
};
let params_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Bloom Params Buffer"),
contents: bytemuck::cast_slice(&[params]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let mut downsample_params_buffers = Vec::with_capacity(MIP_CHAIN_LENGTH);
for mip_level in 0..MIP_CHAIN_LENGTH {
let downsample_params = DownsampleParams {
mip_level: mip_level as u32,
_padding0: 0,
_padding1: 0,
_padding2: 0,
};
let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(&format!("Bloom Downsample Params Buffer {}", mip_level)),
contents: bytemuck::cast_slice(&[downsample_params]),
usage: wgpu::BufferUsages::UNIFORM,
});
downsample_params_buffers.push(buffer);
}
let downsample_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Bloom Downsample Bind Group Layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let upsample_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Bloom Upsample Bind Group Layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let downsample_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Bloom Downsample Pipeline Layout"),
bind_group_layouts: &[&downsample_bind_group_layout],
push_constant_ranges: &[],
});
let upsample_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Bloom Upsample Pipeline Layout"),
bind_group_layouts: &[&upsample_bind_group_layout],
push_constant_ranges: &[],
});
let downsample_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Bloom Downsample Pipeline"),
layout: Some(&downsample_pipeline_layout),
vertex: wgpu::VertexState {
module: &downsample_shader,
entry_point: Some("vertex_main"),
buffers: &[],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &downsample_shader,
entry_point: Some("fragment_main"),
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rgba16Float,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
unclipped_depth: false,
polygon_mode: wgpu::PolygonMode::Fill,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
cache: None,
});
let upsample_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Bloom Upsample Pipeline"),
layout: Some(&upsample_pipeline_layout),
vertex: wgpu::VertexState {
module: &upsample_shader,
entry_point: Some("vertex_main"),
buffers: &[],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &upsample_shader,
entry_point: Some("fragment_main"),
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rgba16Float,
blend: Some(wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::One,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::One,
operation: wgpu::BlendOperation::Add,
},
}),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
unclipped_depth: false,
polygon_mode: wgpu::PolygonMode::Fill,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
cache: None,
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::FilterMode::Linear,
..Default::default()
});
let mut mip_chain = Vec::with_capacity(MIP_CHAIN_LENGTH);
let mut mip_width = width / 2;
let mut mip_height = height / 2;
for mip_level in 0..MIP_CHAIN_LENGTH {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some(&format!("Bloom Mip {}", mip_level)),
size: wgpu::Extent3d {
width: mip_width.max(1),
height: mip_height.max(1),
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba16Float,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
mip_chain.push(BloomMip {
_texture: texture,
view,
});
mip_width /= 2;
mip_height /= 2;
}
Self {
downsample_pipeline,
upsample_pipeline,
downsample_bind_group_layout,
upsample_bind_group_layout,
sampler,
params_buffer,
downsample_params_buffers,
mip_chain,
downsample_bind_groups: Vec::new(),
upsample_bind_groups: Vec::new(),
current_size: (width, height),
cached_states: std::collections::HashMap::new(),
}
}
pub fn resize(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, width: u32, height: u32) {
if self.current_size == (width, height) {
return;
}
let old_size = self.current_size;
let old_mip_chain = std::mem::take(&mut self.mip_chain);
let old_downsample_bgs = std::mem::take(&mut self.downsample_bind_groups);
let old_upsample_bgs = std::mem::take(&mut self.upsample_bind_groups);
if !old_mip_chain.is_empty() {
self.cached_states.insert(
old_size,
CachedBloomState {
mip_chain: old_mip_chain,
downsample_bind_groups: old_downsample_bgs,
upsample_bind_groups: old_upsample_bgs,
},
);
}
self.current_size = (width, height);
if let Some(cached) = self.cached_states.remove(&(width, height)) {
self.mip_chain = cached.mip_chain;
self.downsample_bind_groups = cached.downsample_bind_groups;
self.upsample_bind_groups = cached.upsample_bind_groups;
return;
}
let params = BloomParams {
filter_radius: 0.005,
_padding0: 0.0,
_padding1: 0.0,
_padding2: 0.0,
};
queue.write_buffer(&self.params_buffer, 0, bytemuck::cast_slice(&[params]));
let mut mip_width = width / 2;
let mut mip_height = height / 2;
for mip_level in 0..MIP_CHAIN_LENGTH {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some(&format!("Bloom Mip {}", mip_level)),
size: wgpu::Extent3d {
width: mip_width.max(1),
height: mip_height.max(1),
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba16Float,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
self.mip_chain.push(BloomMip {
_texture: texture,
view,
});
mip_width /= 2;
mip_height /= 2;
}
}
}
impl PassNode<crate::ecs::world::World> for BloomPass {
fn name(&self) -> &str {
"bloom_pass"
}
fn reads(&self) -> Vec<&str> {
vec!["hdr"]
}
fn writes(&self) -> Vec<&str> {
vec!["bloom"]
}
fn invalidate_bind_groups(&mut self) {
self.downsample_bind_groups.clear();
self.upsample_bind_groups.clear();
}
fn execute<'r, 'e>(
&mut self,
context: PassExecutionContext<'r, 'e, crate::ecs::world::World>,
) -> crate::render::wgpu::rendergraph::Result<
Vec<crate::render::wgpu::rendergraph::SubGraphRunCommand<'r>>,
> {
if !context.is_pass_enabled() {
return Ok(context.into_sub_graph_commands());
}
if self.downsample_bind_groups.is_empty() {
let hdr_view = context.get_texture_view("hdr")?;
let first_bind_group = context
.device
.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &self.downsample_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(hdr_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: self.downsample_params_buffers[0].as_entire_binding(),
},
],
label: Some("Bloom Downsample Bind Group 0"),
});
self.downsample_bind_groups.push(first_bind_group);
for index in 1..MIP_CHAIN_LENGTH {
let bind_group = context
.device
.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &self.downsample_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(
&self.mip_chain[index - 1].view,
),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: self.downsample_params_buffers[index].as_entire_binding(),
},
],
label: Some(&format!("Bloom Downsample Bind Group {}", index)),
});
self.downsample_bind_groups.push(bind_group);
}
}
if self.upsample_bind_groups.is_empty() {
for index in 0..(MIP_CHAIN_LENGTH - 1) {
let source_mip = MIP_CHAIN_LENGTH - 1 - index;
let bind_group = context
.device
.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &self.upsample_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(
&self.mip_chain[source_mip].view,
),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: self.params_buffer.as_entire_binding(),
},
],
label: Some(&format!("Bloom Upsample Bind Group {}", index)),
});
self.upsample_bind_groups.push(bind_group);
}
}
for index in 0..MIP_CHAIN_LENGTH {
let mut render_pass = context
.encoder
.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some(&format!("Bloom Downsample Pass {}", index)),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &self.mip_chain[index].view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
render_pass.set_pipeline(&self.downsample_pipeline);
render_pass.set_bind_group(0, &self.downsample_bind_groups[index], &[]);
render_pass.draw(0..3, 0..1);
}
for index in 0..(MIP_CHAIN_LENGTH - 1) {
let target_mip = MIP_CHAIN_LENGTH - 2 - index;
let is_final = target_mip == 0;
if is_final {
let (bloom_view, bloom_load_op, bloom_store_op) =
context.get_color_attachment("bloom")?;
let mut render_pass =
context
.encoder
.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some(&format!("Bloom Upsample Pass {} (Final)", index)),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: bloom_view,
resolve_target: None,
ops: wgpu::Operations {
load: bloom_load_op,
store: bloom_store_op,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
render_pass.set_pipeline(&self.upsample_pipeline);
render_pass.set_bind_group(0, &self.upsample_bind_groups[index], &[]);
render_pass.draw(0..3, 0..1);
} else {
let mut render_pass =
context
.encoder
.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some(&format!("Bloom Upsample Pass {}", index)),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &self.mip_chain[target_mip].view,
resolve_target: None,
ops: wgpu::Operations {
load: if index == 0 {
wgpu::LoadOp::Clear(wgpu::Color::BLACK)
} else {
wgpu::LoadOp::Load
},
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
render_pass.set_pipeline(&self.upsample_pipeline);
render_pass.set_bind_group(0, &self.upsample_bind_groups[index], &[]);
render_pass.draw(0..3, 0..1);
}
}
Ok(context.into_sub_graph_commands())
}
}