1use bytemuck::{Pod, Zeroable};
2use wgpu::util::DeviceExt;
3
4#[repr(C)]
8#[derive(Copy, Clone, Pod, Zeroable)]
9pub struct BlurUniforms {
10 pub texel_size: [f32; 2], pub offset: f32, pub direction: f32, }
14
15pub enum BlurPassKind {
21 Downsample,
22 Upsample,
23 Box { horizontal: bool },
24}
25
26pub struct BlurPipeline {
27 pub downsample_pipeline: wgpu::RenderPipeline,
28 pub upsample_pipeline: wgpu::RenderPipeline,
29 pub box_pipeline: wgpu::RenderPipeline,
30 pub uniform_bgl: wgpu::BindGroupLayout,
31 pub texture_bgl: wgpu::BindGroupLayout,
32 pub sampler: wgpu::Sampler,
33}
34
35impl BlurPipeline {
36 pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
37 let shader = device.create_shader_module(wgpu::include_wgsl!("shaders/blur.wgsl"));
38
39 let uniform_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
41 label: Some("Blur Uniform BGL"),
42 entries: &[wgpu::BindGroupLayoutEntry {
43 binding: 0,
44 visibility: wgpu::ShaderStages::FRAGMENT,
45 ty: wgpu::BindingType::Buffer {
46 ty: wgpu::BufferBindingType::Uniform,
47 has_dynamic_offset: false,
48 min_binding_size: None,
49 },
50 count: None,
51 }],
52 });
53
54 let texture_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
55 label: Some("Blur Texture BGL"),
56 entries: &[
57 wgpu::BindGroupLayoutEntry {
58 binding: 0,
59 visibility: wgpu::ShaderStages::FRAGMENT,
60 ty: wgpu::BindingType::Texture {
61 sample_type: wgpu::TextureSampleType::Float { filterable: true },
62 view_dimension: wgpu::TextureViewDimension::D2,
63 multisampled: false,
64 },
65 count: None,
66 },
67 wgpu::BindGroupLayoutEntry {
68 binding: 1,
69 visibility: wgpu::ShaderStages::FRAGMENT,
70 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
71 count: None,
72 },
73 ],
74 });
75
76 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
77 label: Some("Blur Pipeline Layout"),
78 bind_group_layouts: &[Some(&uniform_bgl), Some(&texture_bgl)],
79 immediate_size: 0,
80 });
81
82 let target = wgpu::ColorTargetState {
84 format,
85 blend: None, write_mask: wgpu::ColorWrites::ALL,
87 };
88
89 let mk_pipeline = |label: &str, entry: &str| {
90 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
91 label: Some(label),
92 layout: Some(&layout),
93 vertex: wgpu::VertexState {
94 module: &shader,
95 entry_point: Some("vs_main"),
96 buffers: &[],
97 compilation_options: Default::default(),
98 },
99 fragment: Some(wgpu::FragmentState {
100 module: &shader,
101 entry_point: Some(entry),
102 targets: &[Some(target.clone())],
103 compilation_options: Default::default(),
104 }),
105 primitive: wgpu::PrimitiveState::default(),
106 depth_stencil: None,
107 multisample: wgpu::MultisampleState::default(),
108 multiview_mask: None,
109 cache: None,
110 })
111 };
112
113 let downsample_pipeline = mk_pipeline("Blur Downsample Pipeline", "fs_downsample");
114 let upsample_pipeline = mk_pipeline("Blur Upsample Pipeline", "fs_upsample");
115 let box_pipeline = mk_pipeline("Blur Box Pipeline", "fs_box");
116
117 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
118 label: Some("Blur Sampler"),
119 address_mode_u: wgpu::AddressMode::ClampToEdge,
120 address_mode_v: wgpu::AddressMode::ClampToEdge,
121 mag_filter: wgpu::FilterMode::Linear,
122 min_filter: wgpu::FilterMode::Linear,
123 ..Default::default()
124 });
125
126 Self {
127 downsample_pipeline,
128 upsample_pipeline,
129 box_pipeline,
130 uniform_bgl,
131 texture_bgl,
132 sampler,
133 }
134 }
135
136 pub fn run_pass(
144 &self,
145 device: &wgpu::Device,
146 encoder: &mut wgpu::CommandEncoder,
147 src_view: &wgpu::TextureView,
148 dst_view: &wgpu::TextureView,
149 src_w: u32,
150 src_h: u32,
151 offset: f32,
152 kind: BlurPassKind,
153 ) {
154 let direction = match &kind {
155 BlurPassKind::Box { horizontal } => if *horizontal { 0.0 } else { 1.0 },
156 _ => 0.0,
157 };
158
159 let uniforms = BlurUniforms {
160 texel_size: [1.0 / src_w as f32, 1.0 / src_h as f32],
161 offset,
162 direction,
163 };
164
165 let uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
166 label: Some("Blur Pass Uniform"),
167 contents: bytemuck::bytes_of(&uniforms),
168 usage: wgpu::BufferUsages::UNIFORM,
169 });
170
171 let uniform_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
172 label: Some("Blur Pass Uniform BG"),
173 layout: &self.uniform_bgl,
174 entries: &[wgpu::BindGroupEntry {
175 binding: 0,
176 resource: uniform_buf.as_entire_binding(),
177 }],
178 });
179
180 let texture_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
181 label: Some("Blur Pass Texture BG"),
182 layout: &self.texture_bgl,
183 entries: &[
184 wgpu::BindGroupEntry {
185 binding: 0,
186 resource: wgpu::BindingResource::TextureView(src_view),
187 },
188 wgpu::BindGroupEntry {
189 binding: 1,
190 resource: wgpu::BindingResource::Sampler(&self.sampler),
191 },
192 ],
193 });
194
195 let pipeline = match kind {
196 BlurPassKind::Downsample => &self.downsample_pipeline,
197 BlurPassKind::Upsample => &self.upsample_pipeline,
198 BlurPassKind::Box { .. } => &self.box_pipeline,
199 };
200
201 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
202 label: Some("Blur Pass"),
203 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
204 view: dst_view,
205 resolve_target: None,
206 ops: wgpu::Operations {
207 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
208 store: wgpu::StoreOp::Store,
209 },
210 depth_slice: None,
211 })],
212 depth_stencil_attachment: None,
213 timestamp_writes: None,
214 occlusion_query_set: None,
215 multiview_mask: None,
216 });
217
218 pass.set_pipeline(pipeline);
219 pass.set_bind_group(0, &uniform_bg, &[]);
220 pass.set_bind_group(1, &texture_bg, &[]);
221 pass.draw(0..3, 0..1);
222 }
223}
224
225pub struct BlurScratch {
231 pub width: u32,
232 pub height: u32,
233 pub format: wgpu::TextureFormat,
234 pub full_a: wgpu::Texture,
236 pub full_a_v: wgpu::TextureView,
237 pub full_b: wgpu::Texture,
238 pub full_b_v: wgpu::TextureView,
239 pub mips: [wgpu::Texture; 4],
241 pub mips_v: [wgpu::TextureView; 4],
242 pub mip_w: [u32; 4],
243 pub mip_h: [u32; 4],
244}
245
246impl BlurScratch {
247 pub fn new(device: &wgpu::Device, width: u32, height: u32, format: wgpu::TextureFormat) -> Self {
248 let mk = |w: u32, h: u32, label: &str| {
249 device.create_texture(&wgpu::TextureDescriptor {
250 label: Some(label),
251 size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
252 mip_level_count: 1,
253 sample_count: 1,
254 dimension: wgpu::TextureDimension::D2,
255 format,
256 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
257 | wgpu::TextureUsages::TEXTURE_BINDING
258 | wgpu::TextureUsages::COPY_SRC
259 | wgpu::TextureUsages::COPY_DST,
260 view_formats: &[],
261 })
262 };
263
264 let full_a = mk(width, height, "Blur Full A");
265 let full_a_v = full_a.create_view(&wgpu::TextureViewDescriptor::default());
266 let full_b = mk(width, height, "Blur Full B");
267 let full_b_v = full_b.create_view(&wgpu::TextureViewDescriptor::default());
268
269 let mut mip_w = [0u32; 4];
270 let mut mip_h = [0u32; 4];
271 let mut mips_raw: Vec<wgpu::Texture> = Vec::with_capacity(4);
272 for i in 0..4 {
273 let scale = 2u32.pow(i as u32 + 1);
274 let w = (width / scale).max(1);
275 let h = (height / scale).max(1);
276 mip_w[i] = w;
277 mip_h[i] = h;
278 mips_raw.push(mk(w, h, &format!("Blur Mip {}", i)));
279 }
280
281 let mips: [wgpu::Texture; 4] = mips_raw.try_into().unwrap();
283 let mips_v: [wgpu::TextureView; 4] = std::array::from_fn(|i| {
284 mips[i].create_view(&wgpu::TextureViewDescriptor::default())
285 });
286
287 Self { width, height, format, full_a, full_a_v, full_b, full_b_v, mips, mips_v, mip_w, mip_h }
288 }
289
290 pub fn needs_resize(&self, width: u32, height: u32) -> bool {
292 self.width != width || self.height != height
293 }
294}
295
296pub fn run_kawase_blur(
303 pipeline: &BlurPipeline,
304 scratch: &BlurScratch,
305 device: &wgpu::Device,
306 encoder: &mut wgpu::CommandEncoder,
307 scene_view: &wgpu::TextureView, out_view: &wgpu::TextureView, radius: f32, ) {
311 if radius <= 0.0 {
312 pipeline.run_pass(
314 device, encoder,
315 scene_view, out_view,
316 scratch.width, scratch.height,
317 0.0, BlurPassKind::Upsample,
318 );
319 return;
320 }
321
322 if radius < 5.0 {
325 pipeline.run_pass(
327 device, encoder,
328 scene_view, out_view,
329 scratch.width, scratch.height,
330 radius, BlurPassKind::Downsample,
331 );
332 return;
333 }
334
335 let num_passes = if radius < 12.0 {
336 1usize
337 } else if radius < 25.0 {
338 2usize
339 } else if radius < 40.0 {
340 3usize
341 } else {
342 4usize
343 };
344
345 {
348 let offset = 0.5 * radius / num_passes as f32;
349 pipeline.run_pass(
350 device, encoder,
351 scene_view, &scratch.mips_v[0],
352 scratch.width, scratch.height,
353 offset, BlurPassKind::Downsample,
354 );
355 }
356 for i in 1..num_passes {
358 let offset = (i as f32 + 0.5) * radius / num_passes as f32;
359 pipeline.run_pass(
360 device, encoder,
361 &scratch.mips_v[i - 1], &scratch.mips_v[i],
362 scratch.mip_w[i - 1], scratch.mip_h[i - 1],
363 offset, BlurPassKind::Downsample,
364 );
365 }
366
367 for i in (0..num_passes - 1).rev() {
370 let offset = (i as f32 + 0.5) * radius / num_passes as f32;
371 pipeline.run_pass(
372 device, encoder,
373 &scratch.mips_v[i + 1], &scratch.mips_v[i],
374 scratch.mip_w[i + 1], scratch.mip_h[i + 1],
375 offset, BlurPassKind::Upsample,
376 );
377 }
378
379 {
381 let offset = 0.5 * radius / num_passes as f32;
382 pipeline.run_pass(
383 device, encoder,
384 &scratch.mips_v[0], out_view,
385 scratch.mip_w[0], scratch.mip_h[0],
386 offset, BlurPassKind::Upsample,
387 );
388 }
389}
390
391pub fn run_box_blur(
392 pipeline: &BlurPipeline,
393 scratch: &BlurScratch,
394 device: &wgpu::Device,
395 encoder: &mut wgpu::CommandEncoder,
396 scene_view: &wgpu::TextureView, out_view: &wgpu::TextureView, radius: f32, ) {
400 pipeline.run_pass(
402 device, encoder,
403 scene_view, &scratch.mips_v[0],
404 scratch.width, scratch.height,
405 radius, BlurPassKind::Box { horizontal: true },
406 );
407 pipeline.run_pass(
409 device, encoder,
410 &scratch.mips_v[0], out_view,
411 scratch.mip_w[0], scratch.mip_h[0],
412 radius, BlurPassKind::Box { horizontal: false },
413 );
414}