794 lines
29 KiB
Rust
794 lines
29 KiB
Rust
//! # Bloom Post-Process (Étape 23)
|
||
//!
|
||
//! Defines `BloomConfig` (public user-facing configuration) and the internal `BloomPipeline`
|
||
//! (GPU resources: half-res textures, blur/composite pipelines, bind groups). The bloom effect
|
||
//! is a 4-pass post-process that operates on the HDR texture before tone mapping:
|
||
//!
|
||
//! 1. **Threshold** (full → half res): extract pixels above a luminance threshold (soft-knee).
|
||
//! 2. **Blur H** (half res): horizontal separable Gaussian (9 taps).
|
||
//! 3. **Blur V** (half res): vertical separable Gaussian (9 taps).
|
||
//! 4. **Composite** (full res): `HDR += bloom × intensity`.
|
||
//!
|
||
//! The bloom is **opt-in** (`AppBuilder::with_bloom`) and only active when HDR is also enabled.
|
||
//! Without HDR, the values are already clamped to [0,1] and there is nothing "bright" to bloom.
|
||
|
||
use wgpu::{
|
||
BindGroup, BindGroupLayout, Buffer, BufferUsages, RenderPipeline, Sampler, Texture,
|
||
TextureUsages, TextureView,
|
||
};
|
||
|
||
/// User-facing bloom configuration (Étape 23).
|
||
///
|
||
/// Passed to `AppBuilder::with_bloom(config)` to enable the bloom post-process.
|
||
/// Can be updated at runtime via `App::set_bloom_config`.
|
||
#[derive(Debug, Clone)]
|
||
pub struct BloomConfig {
|
||
/// Luminance threshold (in linear HDR units). Pixels above this contribute to bloom.
|
||
/// Default: 1.0 (only overbright areas — emissives > 1.0, specular highlights).
|
||
pub threshold: f32,
|
||
/// Soft-knee width for the threshold ramp. Larger = smoother transition.
|
||
/// Default: 0.5.
|
||
pub knee: f32,
|
||
/// Bloom intensity (multiplier on the blurred result before adding to HDR).
|
||
/// Default: 0.8.
|
||
pub intensity: f32,
|
||
/// Blur radius in pixels (at half resolution). Larger = wider glow.
|
||
/// Default: 4.0.
|
||
pub radius: f32,
|
||
}
|
||
|
||
impl Default for BloomConfig {
|
||
fn default() -> Self {
|
||
Self {
|
||
threshold: 1.0,
|
||
knee: 0.5,
|
||
intensity: 0.8,
|
||
radius: 4.0,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Internal bloom pipeline state. Allocated when bloom + HDR are both active.
|
||
/// Recreated on resize.
|
||
pub(crate) struct BloomPipeline {
|
||
bright_texture: Texture,
|
||
bright_view: TextureView,
|
||
blur_texture: Texture,
|
||
blur_view: TextureView,
|
||
composite_texture: Texture,
|
||
composite_view: TextureView,
|
||
sampler: Sampler,
|
||
threshold_pipeline: RenderPipeline,
|
||
blur_pipeline: RenderPipeline,
|
||
composite_pipeline: RenderPipeline,
|
||
threshold_bg: BindGroup,
|
||
blur_bg_h: BindGroup,
|
||
blur_bg_v: BindGroup,
|
||
composite_bg: BindGroup,
|
||
threshold_uniform: Buffer,
|
||
blur_uniform_h: Buffer,
|
||
blur_uniform_v: Buffer,
|
||
composite_uniform: Buffer,
|
||
threshold_layout: BindGroupLayout,
|
||
blur_layout: BindGroupLayout,
|
||
composite_layout: BindGroupLayout,
|
||
half_w: u32,
|
||
half_h: u32,
|
||
width: u32,
|
||
height: u32,
|
||
}
|
||
|
||
impl BloomPipeline {
|
||
pub fn new(device: &wgpu::Device, width: u32, height: u32, hdr_view: &TextureView) -> Self {
|
||
let half_w = (width / 2).max(1);
|
||
let half_h = (height / 2).max(1);
|
||
|
||
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||
label: Some("bloom sampler"),
|
||
mag_filter: wgpu::FilterMode::Linear,
|
||
min_filter: wgpu::FilterMode::Linear,
|
||
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
|
||
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
||
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
||
address_mode_w: wgpu::AddressMode::ClampToEdge,
|
||
..Default::default()
|
||
});
|
||
|
||
let (bright_texture, bright_view) =
|
||
create_bloom_texture(device, half_w, half_h, "bloom bright");
|
||
let (blur_texture, blur_view) = create_bloom_texture(device, half_w, half_h, "bloom blur");
|
||
let (composite_texture, composite_view) =
|
||
create_bloom_texture(device, width, height, "bloom composite");
|
||
|
||
// Bind group layouts.
|
||
let threshold_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||
label: Some("bloom threshold bgl"),
|
||
entries: &[
|
||
wgpu::BindGroupLayoutEntry {
|
||
binding: 0,
|
||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||
ty: wgpu::BindingType::Buffer {
|
||
ty: wgpu::BufferBindingType::Uniform,
|
||
has_dynamic_offset: false,
|
||
min_binding_size: None,
|
||
},
|
||
count: None,
|
||
},
|
||
wgpu::BindGroupLayoutEntry {
|
||
binding: 1,
|
||
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: 2,
|
||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||
count: None,
|
||
},
|
||
],
|
||
});
|
||
|
||
let blur_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||
label: Some("bloom blur bgl"),
|
||
entries: &[
|
||
wgpu::BindGroupLayoutEntry {
|
||
binding: 0,
|
||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||
ty: wgpu::BindingType::Buffer {
|
||
ty: wgpu::BufferBindingType::Uniform,
|
||
has_dynamic_offset: false,
|
||
min_binding_size: None,
|
||
},
|
||
count: None,
|
||
},
|
||
wgpu::BindGroupLayoutEntry {
|
||
binding: 1,
|
||
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: 2,
|
||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||
count: None,
|
||
},
|
||
],
|
||
});
|
||
|
||
let composite_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||
label: Some("bloom composite bgl"),
|
||
entries: &[
|
||
wgpu::BindGroupLayoutEntry {
|
||
binding: 0,
|
||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||
ty: wgpu::BindingType::Buffer {
|
||
ty: wgpu::BufferBindingType::Uniform,
|
||
has_dynamic_offset: false,
|
||
min_binding_size: None,
|
||
},
|
||
count: None,
|
||
},
|
||
wgpu::BindGroupLayoutEntry {
|
||
binding: 1,
|
||
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: 2,
|
||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||
count: None,
|
||
},
|
||
wgpu::BindGroupLayoutEntry {
|
||
binding: 3,
|
||
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: 4,
|
||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||
count: None,
|
||
},
|
||
],
|
||
});
|
||
|
||
// Pipeline layouts.
|
||
let threshold_pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||
label: Some("bloom threshold pl"),
|
||
bind_group_layouts: &[Some(&threshold_layout)],
|
||
..Default::default()
|
||
});
|
||
let blur_pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||
label: Some("bloom blur pl"),
|
||
bind_group_layouts: &[Some(&blur_layout)],
|
||
..Default::default()
|
||
});
|
||
let composite_pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||
label: Some("bloom composite pl"),
|
||
bind_group_layouts: &[Some(&composite_layout)],
|
||
..Default::default()
|
||
});
|
||
|
||
// Shader modules.
|
||
let threshold_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||
label: Some("bloom threshold"),
|
||
source: wgpu::ShaderSource::Wgsl(
|
||
crate::utils::conf::BLOOM_THRESHOLD_SHADER.into(),
|
||
),
|
||
});
|
||
let blur_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||
label: Some("bloom blur"),
|
||
source: wgpu::ShaderSource::Wgsl(
|
||
crate::utils::conf::BLOOM_BLUR_SHADER.into(),
|
||
),
|
||
});
|
||
let composite_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||
label: Some("bloom composite"),
|
||
source: wgpu::ShaderSource::Wgsl(
|
||
crate::utils::conf::BLOOM_COMPOSITE_SHADER.into(),
|
||
),
|
||
});
|
||
|
||
// Shared fragment target state (all 3 passes output to Rgba16Float).
|
||
let fragment_targets = &[Some(wgpu::ColorTargetState {
|
||
format: wgpu::TextureFormat::Rgba16Float,
|
||
blend: Some(wgpu::BlendState::REPLACE),
|
||
write_mask: wgpu::ColorWrites::ALL,
|
||
})];
|
||
|
||
// Threshold pipeline.
|
||
let threshold_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||
label: Some("bloom threshold pipeline"),
|
||
layout: Some(&threshold_pl),
|
||
vertex: wgpu::VertexState {
|
||
module: &threshold_module,
|
||
entry_point: Some("vs_main"),
|
||
buffers: &[],
|
||
compilation_options: Default::default(),
|
||
},
|
||
fragment: Some(wgpu::FragmentState {
|
||
module: &threshold_module,
|
||
entry_point: Some("fs_main"),
|
||
compilation_options: Default::default(),
|
||
targets: fragment_targets,
|
||
}),
|
||
primitive: wgpu::PrimitiveState {
|
||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||
..Default::default()
|
||
},
|
||
depth_stencil: None,
|
||
multisample: Default::default(),
|
||
multiview_mask: None,
|
||
cache: None,
|
||
});
|
||
|
||
// Blur pipeline.
|
||
let blur_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||
label: Some("bloom blur pipeline"),
|
||
layout: Some(&blur_pl),
|
||
vertex: wgpu::VertexState {
|
||
module: &blur_module,
|
||
entry_point: Some("vs_main"),
|
||
buffers: &[],
|
||
compilation_options: Default::default(),
|
||
},
|
||
fragment: Some(wgpu::FragmentState {
|
||
module: &blur_module,
|
||
entry_point: Some("fs_main"),
|
||
compilation_options: Default::default(),
|
||
targets: fragment_targets,
|
||
}),
|
||
primitive: wgpu::PrimitiveState {
|
||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||
..Default::default()
|
||
},
|
||
depth_stencil: None,
|
||
multisample: Default::default(),
|
||
multiview_mask: None,
|
||
cache: None,
|
||
});
|
||
|
||
// Composite pipeline.
|
||
let composite_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||
label: Some("bloom composite pipeline"),
|
||
layout: Some(&composite_pl),
|
||
vertex: wgpu::VertexState {
|
||
module: &composite_module,
|
||
entry_point: Some("vs_main"),
|
||
buffers: &[],
|
||
compilation_options: Default::default(),
|
||
},
|
||
fragment: Some(wgpu::FragmentState {
|
||
module: &composite_module,
|
||
entry_point: Some("fs_main"),
|
||
compilation_options: Default::default(),
|
||
targets: fragment_targets,
|
||
}),
|
||
primitive: wgpu::PrimitiveState {
|
||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||
..Default::default()
|
||
},
|
||
depth_stencil: None,
|
||
multisample: Default::default(),
|
||
multiview_mask: None,
|
||
cache: None,
|
||
});
|
||
|
||
// Uniform buffers (32 bytes each — WGSL uniform alignment requires padding;
|
||
// vec2 has align 8, vec3 has align 16, so structs are larger than their field sum).
|
||
let threshold_uniform = device.create_buffer(&wgpu::BufferDescriptor {
|
||
label: Some("bloom threshold uniform"),
|
||
size: 32,
|
||
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
|
||
mapped_at_creation: false,
|
||
});
|
||
let blur_uniform_h = device.create_buffer(&wgpu::BufferDescriptor {
|
||
label: Some("bloom blur H uniform"),
|
||
size: 32,
|
||
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
|
||
mapped_at_creation: false,
|
||
});
|
||
let blur_uniform_v = device.create_buffer(&wgpu::BufferDescriptor {
|
||
label: Some("bloom blur V uniform"),
|
||
size: 32,
|
||
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
|
||
mapped_at_creation: false,
|
||
});
|
||
let composite_uniform = device.create_buffer(&wgpu::BufferDescriptor {
|
||
label: Some("bloom composite uniform"),
|
||
size: 32,
|
||
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
|
||
mapped_at_creation: false,
|
||
});
|
||
|
||
// Bind groups.
|
||
let threshold_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||
label: Some("bloom threshold bg"),
|
||
layout: &threshold_layout,
|
||
entries: &[
|
||
wgpu::BindGroupEntry {
|
||
binding: 0,
|
||
resource: threshold_uniform.as_entire_binding(),
|
||
},
|
||
wgpu::BindGroupEntry {
|
||
binding: 1,
|
||
resource: wgpu::BindingResource::TextureView(hdr_view),
|
||
},
|
||
wgpu::BindGroupEntry {
|
||
binding: 2,
|
||
resource: wgpu::BindingResource::Sampler(&sampler),
|
||
},
|
||
],
|
||
});
|
||
|
||
let blur_bg_h = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||
label: Some("bloom blur bg H"),
|
||
layout: &blur_layout,
|
||
entries: &[
|
||
wgpu::BindGroupEntry {
|
||
binding: 0,
|
||
resource: blur_uniform_h.as_entire_binding(),
|
||
},
|
||
wgpu::BindGroupEntry {
|
||
binding: 1,
|
||
resource: wgpu::BindingResource::TextureView(&bright_view),
|
||
},
|
||
wgpu::BindGroupEntry {
|
||
binding: 2,
|
||
resource: wgpu::BindingResource::Sampler(&sampler),
|
||
},
|
||
],
|
||
});
|
||
|
||
let blur_bg_v = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||
label: Some("bloom blur bg V"),
|
||
layout: &blur_layout,
|
||
entries: &[
|
||
wgpu::BindGroupEntry {
|
||
binding: 0,
|
||
resource: blur_uniform_v.as_entire_binding(),
|
||
},
|
||
wgpu::BindGroupEntry {
|
||
binding: 1,
|
||
resource: wgpu::BindingResource::TextureView(&blur_view),
|
||
},
|
||
wgpu::BindGroupEntry {
|
||
binding: 2,
|
||
resource: wgpu::BindingResource::Sampler(&sampler),
|
||
},
|
||
],
|
||
});
|
||
|
||
let composite_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||
label: Some("bloom composite bg"),
|
||
layout: &composite_layout,
|
||
entries: &[
|
||
wgpu::BindGroupEntry {
|
||
binding: 0,
|
||
resource: composite_uniform.as_entire_binding(),
|
||
},
|
||
wgpu::BindGroupEntry {
|
||
binding: 1,
|
||
resource: wgpu::BindingResource::TextureView(hdr_view),
|
||
},
|
||
wgpu::BindGroupEntry {
|
||
binding: 2,
|
||
resource: wgpu::BindingResource::Sampler(&sampler),
|
||
},
|
||
wgpu::BindGroupEntry {
|
||
binding: 3,
|
||
resource: wgpu::BindingResource::TextureView(&bright_view),
|
||
},
|
||
wgpu::BindGroupEntry {
|
||
binding: 4,
|
||
resource: wgpu::BindingResource::Sampler(&sampler),
|
||
},
|
||
],
|
||
});
|
||
|
||
Self {
|
||
bright_texture,
|
||
bright_view,
|
||
blur_texture,
|
||
blur_view,
|
||
composite_texture,
|
||
composite_view,
|
||
sampler,
|
||
threshold_pipeline,
|
||
blur_pipeline,
|
||
composite_pipeline,
|
||
threshold_bg,
|
||
blur_bg_h,
|
||
blur_bg_v,
|
||
composite_bg,
|
||
threshold_uniform,
|
||
blur_uniform_h,
|
||
blur_uniform_v,
|
||
composite_uniform,
|
||
threshold_layout,
|
||
blur_layout,
|
||
composite_layout,
|
||
half_w,
|
||
half_h,
|
||
width,
|
||
height,
|
||
}
|
||
}
|
||
|
||
pub fn resize(
|
||
&mut self,
|
||
device: &wgpu::Device,
|
||
width: u32,
|
||
height: u32,
|
||
hdr_view: &TextureView,
|
||
) {
|
||
let half_w = (width / 2).max(1);
|
||
let half_h = (height / 2).max(1);
|
||
|
||
let (bright_texture, bright_view) =
|
||
create_bloom_texture(device, half_w, half_h, "bloom bright");
|
||
let (blur_texture, blur_view) = create_bloom_texture(device, half_w, half_h, "bloom blur");
|
||
let (composite_texture, composite_view) =
|
||
create_bloom_texture(device, width, height, "bloom composite");
|
||
|
||
self.threshold_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||
label: Some("bloom threshold bg"),
|
||
layout: &self.threshold_layout,
|
||
entries: &[
|
||
wgpu::BindGroupEntry {
|
||
binding: 0,
|
||
resource: self.threshold_uniform.as_entire_binding(),
|
||
},
|
||
wgpu::BindGroupEntry {
|
||
binding: 1,
|
||
resource: wgpu::BindingResource::TextureView(hdr_view),
|
||
},
|
||
wgpu::BindGroupEntry {
|
||
binding: 2,
|
||
resource: wgpu::BindingResource::Sampler(&self.sampler),
|
||
},
|
||
],
|
||
});
|
||
|
||
self.blur_bg_h = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||
label: Some("bloom blur bg H"),
|
||
layout: &self.blur_layout,
|
||
entries: &[
|
||
wgpu::BindGroupEntry {
|
||
binding: 0,
|
||
resource: self.blur_uniform_h.as_entire_binding(),
|
||
},
|
||
wgpu::BindGroupEntry {
|
||
binding: 1,
|
||
resource: wgpu::BindingResource::TextureView(&bright_view),
|
||
},
|
||
wgpu::BindGroupEntry {
|
||
binding: 2,
|
||
resource: wgpu::BindingResource::Sampler(&self.sampler),
|
||
},
|
||
],
|
||
});
|
||
|
||
self.blur_bg_v = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||
label: Some("bloom blur bg V"),
|
||
layout: &self.blur_layout,
|
||
entries: &[
|
||
wgpu::BindGroupEntry {
|
||
binding: 0,
|
||
resource: self.blur_uniform_v.as_entire_binding(),
|
||
},
|
||
wgpu::BindGroupEntry {
|
||
binding: 1,
|
||
resource: wgpu::BindingResource::TextureView(&blur_view),
|
||
},
|
||
wgpu::BindGroupEntry {
|
||
binding: 2,
|
||
resource: wgpu::BindingResource::Sampler(&self.sampler),
|
||
},
|
||
],
|
||
});
|
||
|
||
self.composite_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||
label: Some("bloom composite bg"),
|
||
layout: &self.composite_layout,
|
||
entries: &[
|
||
wgpu::BindGroupEntry {
|
||
binding: 0,
|
||
resource: self.composite_uniform.as_entire_binding(),
|
||
},
|
||
wgpu::BindGroupEntry {
|
||
binding: 1,
|
||
resource: wgpu::BindingResource::TextureView(hdr_view),
|
||
},
|
||
wgpu::BindGroupEntry {
|
||
binding: 2,
|
||
resource: wgpu::BindingResource::Sampler(&self.sampler),
|
||
},
|
||
wgpu::BindGroupEntry {
|
||
binding: 3,
|
||
resource: wgpu::BindingResource::TextureView(&bright_view),
|
||
},
|
||
wgpu::BindGroupEntry {
|
||
binding: 4,
|
||
resource: wgpu::BindingResource::Sampler(&self.sampler),
|
||
},
|
||
],
|
||
});
|
||
|
||
self.bright_texture = bright_texture;
|
||
self.bright_view = bright_view;
|
||
self.blur_texture = blur_texture;
|
||
self.blur_view = blur_view;
|
||
self.composite_texture = composite_texture;
|
||
self.composite_view = composite_view;
|
||
self.half_w = half_w;
|
||
self.half_h = half_h;
|
||
self.width = width;
|
||
self.height = height;
|
||
}
|
||
|
||
#[allow(dead_code)]
|
||
pub fn composite_view(&self) -> &TextureView {
|
||
&self.composite_view
|
||
}
|
||
|
||
pub fn composite_texture(&self) -> &Texture {
|
||
&self.composite_texture
|
||
}
|
||
|
||
pub fn record_passes(
|
||
&self,
|
||
encoder: &mut wgpu::CommandEncoder,
|
||
queue: &wgpu::Queue,
|
||
config: &BloomConfig,
|
||
) {
|
||
let threshold_data = [config.threshold, config.knee, 0.0, 0.0];
|
||
queue.write_buffer(
|
||
&self.threshold_uniform,
|
||
0,
|
||
bytemuck::cast_slice(&threshold_data),
|
||
);
|
||
|
||
let blur_h_data = [1.0 / self.half_w as f32, 0.0, config.radius, 0.0];
|
||
queue.write_buffer(&self.blur_uniform_h, 0, bytemuck::cast_slice(&blur_h_data));
|
||
|
||
let blur_v_data = [0.0, 1.0 / self.half_h as f32, config.radius, 0.0];
|
||
queue.write_buffer(&self.blur_uniform_v, 0, bytemuck::cast_slice(&blur_v_data));
|
||
|
||
let composite_data = [config.intensity, 0.0, 0.0, 0.0];
|
||
queue.write_buffer(
|
||
&self.composite_uniform,
|
||
0,
|
||
bytemuck::cast_slice(&composite_data),
|
||
);
|
||
|
||
// Pass 1: Threshold (HDR full → bright half)
|
||
{
|
||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||
label: Some("bloom threshold"),
|
||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||
view: &self.bright_view,
|
||
resolve_target: None,
|
||
depth_slice: None,
|
||
ops: wgpu::Operations {
|
||
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
||
store: wgpu::StoreOp::Store,
|
||
},
|
||
})],
|
||
depth_stencil_attachment: None,
|
||
..Default::default()
|
||
});
|
||
pass.set_viewport(
|
||
0.0,
|
||
0.0,
|
||
self.half_w as f32,
|
||
self.half_h as f32,
|
||
0.0,
|
||
1.0,
|
||
);
|
||
pass.set_pipeline(&self.threshold_pipeline);
|
||
pass.set_bind_group(0, &self.threshold_bg, &[]);
|
||
pass.draw(0..3, 0..1);
|
||
}
|
||
|
||
// Pass 2: Blur H (bright half → blur half)
|
||
{
|
||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||
label: Some("bloom blur H"),
|
||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||
view: &self.blur_view,
|
||
resolve_target: None,
|
||
depth_slice: None,
|
||
ops: wgpu::Operations {
|
||
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
||
store: wgpu::StoreOp::Store,
|
||
},
|
||
})],
|
||
depth_stencil_attachment: None,
|
||
..Default::default()
|
||
});
|
||
pass.set_viewport(
|
||
0.0,
|
||
0.0,
|
||
self.half_w as f32,
|
||
self.half_h as f32,
|
||
0.0,
|
||
1.0,
|
||
);
|
||
pass.set_pipeline(&self.blur_pipeline);
|
||
pass.set_bind_group(0, &self.blur_bg_h, &[]);
|
||
pass.draw(0..3, 0..1);
|
||
}
|
||
|
||
// Pass 3: Blur V (blur half → bright half)
|
||
{
|
||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||
label: Some("bloom blur V"),
|
||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||
view: &self.bright_view,
|
||
resolve_target: None,
|
||
depth_slice: None,
|
||
ops: wgpu::Operations {
|
||
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
||
store: wgpu::StoreOp::Store,
|
||
},
|
||
})],
|
||
depth_stencil_attachment: None,
|
||
..Default::default()
|
||
});
|
||
pass.set_viewport(
|
||
0.0,
|
||
0.0,
|
||
self.half_w as f32,
|
||
self.half_h as f32,
|
||
0.0,
|
||
1.0,
|
||
);
|
||
pass.set_pipeline(&self.blur_pipeline);
|
||
pass.set_bind_group(0, &self.blur_bg_v, &[]);
|
||
pass.draw(0..3, 0..1);
|
||
}
|
||
|
||
// Pass 4: Composite (HDR full + bright half → composite full)
|
||
{
|
||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||
label: Some("bloom composite"),
|
||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||
view: &self.composite_view,
|
||
resolve_target: None,
|
||
depth_slice: None,
|
||
ops: wgpu::Operations {
|
||
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
||
store: wgpu::StoreOp::Store,
|
||
},
|
||
})],
|
||
depth_stencil_attachment: None,
|
||
..Default::default()
|
||
});
|
||
pass.set_viewport(
|
||
0.0,
|
||
0.0,
|
||
self.width as f32,
|
||
self.height as f32,
|
||
0.0,
|
||
1.0,
|
||
);
|
||
pass.set_pipeline(&self.composite_pipeline);
|
||
pass.set_bind_group(0, &self.composite_bg, &[]);
|
||
pass.draw(0..3, 0..1);
|
||
}
|
||
}
|
||
}
|
||
|
||
fn create_bloom_texture(
|
||
device: &wgpu::Device,
|
||
width: u32,
|
||
height: u32,
|
||
label: &str,
|
||
) -> (Texture, TextureView) {
|
||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||
label: Some(label),
|
||
size: wgpu::Extent3d {
|
||
width,
|
||
height,
|
||
depth_or_array_layers: 1,
|
||
},
|
||
mip_level_count: 1,
|
||
sample_count: 1,
|
||
dimension: wgpu::TextureDimension::D2,
|
||
format: wgpu::TextureFormat::Rgba16Float,
|
||
usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING,
|
||
view_formats: &[],
|
||
});
|
||
let view = texture.create_view(&Default::default());
|
||
(texture, view)
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn bloom_config_default() {
|
||
let cfg = BloomConfig::default();
|
||
assert_eq!(cfg.threshold, 1.0);
|
||
assert_eq!(cfg.knee, 0.5);
|
||
assert_eq!(cfg.intensity, 0.8);
|
||
assert_eq!(cfg.radius, 4.0);
|
||
}
|
||
|
||
#[test]
|
||
fn bloom_config_clone() {
|
||
let cfg = BloomConfig {
|
||
threshold: 2.0,
|
||
knee: 1.0,
|
||
intensity: 1.5,
|
||
radius: 6.0,
|
||
};
|
||
let cloned = cfg.clone();
|
||
assert_eq!(cloned.threshold, 2.0);
|
||
assert_eq!(cloned.intensity, 1.5);
|
||
}
|
||
}
|