1use crate::kvasir::node::{ExecutionContext, KvasirNode};
2use crate::kvasir::nodes::{PassId, RES_BLOOM_A, RES_SCENE};
3use crate::kvasir::resource::ResourceId;
4
5pub struct BloomExtractNode {
6 pub inputs: Vec<ResourceId>,
7 pub outputs: Vec<ResourceId>,
8}
9
10impl BloomExtractNode {
11 pub fn new() -> Self {
12 Self {
13 inputs: vec![RES_SCENE],
14 outputs: vec![RES_BLOOM_A],
15 }
16 }
17}
18
19impl Default for BloomExtractNode {
20 fn default() -> Self {
21 Self::new()
22 }
23}
24
25impl KvasirNode for BloomExtractNode {
26 fn label(&self) -> &'static str {
27 "Bloom Extract"
28 }
29
30 fn inputs(&self) -> &[ResourceId] {
31 &self.inputs
32 }
33
34 fn outputs(&self) -> &[ResourceId] {
35 &self.outputs
36 }
37
38 fn pass_id(&self) -> PassId {
39 PassId::BloomExtract
40 }
41
42 fn execute(&self, ctx: &mut ExecutionContext) {
43 let bloom_texture = match ctx.registry.get_texture(RES_BLOOM_A) {
44 Some(v) => v,
45 None => {
46 log::error!("Missing texture for {}", stringify!(RES_BLOOM_A));
47 return;
48 }
49 };
50 let bloom_view = bloom_texture.create_view(&wgpu::TextureViewDescriptor {
52 label: Some("bloom_extract_mip0"),
53 base_mip_level: 0,
54 mip_level_count: Some(1),
55 ..Default::default()
56 });
57
58 let scene_view = match ctx.registry.get_texture_view(RES_SCENE) {
60 Some(v) => v,
61 None => {
62 log::error!("Missing texture view for {}", stringify!(RES_SCENE));
63 return;
64 }
65 };
66 let bg = ctx.get_or_create_bind_group(
67 (RES_SCENE, 0, false),
68 &ctx.renderer.texture_bind_group_layout,
69 &[
70 wgpu::BindGroupEntry {
71 binding: 0,
72 resource: wgpu::BindingResource::TextureViewArray(&vec![&scene_view; 32]),
73 },
74 wgpu::BindGroupEntry {
75 binding: 1,
76 resource: wgpu::BindingResource::Sampler(&ctx.renderer.dummy_sampler),
77 },
78 ],
79 Some("bloom_extract_scene_bg"),
80 );
81
82 let mut p = ctx.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
83 label: Some("Surtr Bloom Extract"),
84 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
85 view: &bloom_view,
86 resolve_target: None,
87 ops: wgpu::Operations {
88 load: wgpu::LoadOp::Clear(wgpu::Color {
89 r: 0.0,
90 g: 0.0,
91 b: 0.0,
92 a: 0.0,
93 }),
94 store: wgpu::StoreOp::Store,
95 },
96 depth_slice: None,
97 })],
98 ..Default::default()
99 });
100
101 p.set_pipeline(&ctx.renderer.bloom_extract_pipeline);
102 p.set_bind_group(0, &bg, &[]);
103 p.set_bind_group(1, &ctx.renderer.dummy_env_bind_group, &[]);
104 p.set_bind_group(2, &ctx.renderer.berserker_bind_group, &[]);
105 p.set_bind_group(3, &ctx.renderer.gradient_bind_group, &[]);
106 p.draw(0..3, 0..1);
107 }
108}
109
110pub struct BloomBlurNode {
111 pub inputs: Vec<ResourceId>,
112 pub outputs: Vec<ResourceId>,
113 pub width: u32,
114 pub height: u32,
115}
116
117impl BloomBlurNode {
118 pub fn new(width: u32, height: u32) -> Self {
119 Self {
120 inputs: vec![RES_BLOOM_A],
121 outputs: vec![RES_BLOOM_A],
122 width,
123 height,
124 }
125 }
126}
127
128impl KvasirNode for BloomBlurNode {
129 fn label(&self) -> &'static str {
130 "Bloom Blur"
131 }
132
133 fn inputs(&self) -> &[ResourceId] {
134 &self.inputs
135 }
136
137 fn outputs(&self) -> &[ResourceId] {
138 &self.outputs
139 }
140
141 fn pass_id(&self) -> PassId {
142 PassId::BloomBlur
143 }
144
145 fn execute(&self, ctx: &mut ExecutionContext) {
146 let bloom_tex = match ctx.registry.get_texture(RES_BLOOM_A) {
147 Some(v) => v,
148 None => {
149 log::error!("Missing texture for {}", stringify!(RES_BLOOM_A));
150 return;
151 }
152 };
153
154 let num_mips = bloom_tex.mip_level_count();
156 if num_mips < 2 {
157 return;
158 }
159
160 let kawase_uniform = &ctx.renderer.kawase_uniform;
162
163 let effective_mips = (num_mips as usize).min(5);
165 let mip_views: Vec<wgpu::TextureView> = (0..effective_mips)
166 .map(|mip| {
167 bloom_tex.create_view(&wgpu::TextureViewDescriptor {
168 label: Some(&format!("bloom_mip_{}", mip)),
169 base_mip_level: mip as u32,
170 mip_level_count: Some(1),
171 ..Default::default()
172 })
173 })
174 .collect();
175
176 let bloom_width = self.width;
177 let bloom_height = self.height;
178
179 let mut mip_scales = Vec::with_capacity(effective_mips);
181 for i in 0..effective_mips {
182 let div = (1u32 << i) as f32;
183 mip_scales.push((
184 bloom_width as f32 / div,
185 bloom_height as f32 / div,
186 (i + 1) as f32,
187 ));
188 }
189
190 for mip in 1..effective_mips {
192 let kernel_width = mip_scales[mip].2;
193 let uniform_data: [f32; 8] = [
194 mip_scales[(mip - 1)].0,
195 mip_scales[(mip - 1)].1,
196 (mip - 1) as f32,
197 kernel_width,
198 0.0,
199 0.0,
200 0.0,
201 0.0,
202 ];
203 ctx.queue
204 .write_buffer(kawase_uniform, 0, bytemuck::cast_slice(&uniform_data));
205
206 let w = mip_scales[mip].0.max(1.0) as u32;
207 let h = mip_scales[mip].1.max(1.0) as u32;
208
209 let bg = ctx.get_or_create_bind_group(
211 (RES_BLOOM_A, mip as u32, false),
212 &ctx.renderer.kawase_bind_group_layout,
213 &[
214 wgpu::BindGroupEntry {
215 binding: 0,
216 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
217 buffer: kawase_uniform,
218 offset: 0,
219 size: wgpu::BufferSize::new(32),
220 }),
221 },
222 wgpu::BindGroupEntry {
223 binding: 1,
224 resource: wgpu::BindingResource::TextureView(&mip_views[(mip - 1)]),
225 },
226 wgpu::BindGroupEntry {
227 binding: 2,
228 resource: wgpu::BindingResource::Sampler(&ctx.renderer.sampler),
229 },
230 ],
231 Some(&format!("kawase_bloom_bg_{}", mip)),
232 );
233
234 let mut p = ctx.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
235 label: Some(&format!("Kawase Bloom Down {}", mip)),
236 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
237 view: &mip_views[mip],
238 resolve_target: None,
239 ops: wgpu::Operations {
240 load: wgpu::LoadOp::Clear(wgpu::Color {
241 r: 0.0,
242 g: 0.0,
243 b: 0.0,
244 a: 0.0,
245 }),
246 store: wgpu::StoreOp::Store,
247 },
248 depth_slice: None,
249 })],
250 ..Default::default()
251 });
252 p.set_viewport(0.0, 0.0, w as f32, h as f32, 0.0, 1.0);
253 p.set_pipeline(&ctx.renderer.kawase_down_pipeline);
254 p.set_bind_group(0, &bg, &[]);
255 p.draw(0..3, 0..1);
256 }
257
258 for mip in (1..effective_mips).rev() {
260 let kernel_width = mip_scales[mip].2;
261 let uniform_data: [f32; 8] = [
262 mip_scales[mip].0,
263 mip_scales[mip].1,
264 mip as f32,
265 kernel_width,
266 0.0,
267 0.0,
268 0.0,
269 0.0,
270 ];
271 ctx.queue
272 .write_buffer(kawase_uniform, 0, bytemuck::cast_slice(&uniform_data));
273
274 let w = mip_scales[(mip - 1)].0.max(1.0) as u32;
275 let h = mip_scales[(mip - 1)].1.max(1.0) as u32;
276
277 let bg = ctx.get_or_create_bind_group(
279 (RES_BLOOM_A, (mip + 100) as u32, false), &ctx.renderer.kawase_bind_group_layout,
281 &[
282 wgpu::BindGroupEntry {
283 binding: 0,
284 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
285 buffer: kawase_uniform,
286 offset: 0,
287 size: wgpu::BufferSize::new(32),
288 }),
289 },
290 wgpu::BindGroupEntry {
291 binding: 1,
292 resource: wgpu::BindingResource::TextureView(&mip_views[mip]),
293 },
294 wgpu::BindGroupEntry {
295 binding: 2,
296 resource: wgpu::BindingResource::Sampler(&ctx.renderer.sampler),
297 },
298 ],
299 Some(&format!("kawase_bloom_up_{}", mip)),
300 );
301
302 let mut p = ctx.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
304 label: Some(&format!("Kawase Bloom Up {}", mip)),
305 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
306 view: &mip_views[(mip - 1)],
307 resolve_target: None,
308 ops: wgpu::Operations {
309 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
310 store: wgpu::StoreOp::Store,
311 },
312 depth_slice: None,
313 })],
314 ..Default::default()
315 });
316 p.set_viewport(0.0, 0.0, w as f32, h as f32, 0.0, 1.0);
317 p.set_pipeline(&ctx.renderer.kawase_up_pipeline);
318 p.set_bind_group(0, &bg, &[]);
319 p.draw(0..3, 0..1);
320 }
321
322 log::trace!(
323 "[Kvasir] bloom_blur: Kawase pyramid ({}x{})",
324 bloom_width,
325 bloom_height
326 );
327 }
328}