1use bytemuck::{Pod, Zeroable};
14use wgpu::util::DeviceExt;
15
16use super::gpu::GpuContext;
17use super::lighting::LightingState;
18
19const DEFAULT_BASE_RAYS: u32 = 4;
21
22const DEFAULT_PROBE_SPACING: f32 = 8.0;
24
25const DEFAULT_INTERVAL: f32 = 4.0;
27
28const MAX_CASCADES: usize = 5;
30
31#[repr(C)]
33#[derive(Copy, Clone, Pod, Zeroable)]
34struct RadianceParams {
35 scene_dims: [f32; 4],
37 cascade_params: [f32; 4],
39 camera: [f32; 4],
41 ambient: [f32; 4],
43}
44
45#[derive(Clone, Debug)]
47pub struct EmissiveSurface {
48 pub x: f32,
49 pub y: f32,
50 pub width: f32,
51 pub height: f32,
52 pub r: f32,
53 pub g: f32,
54 pub b: f32,
55 pub intensity: f32,
56}
57
58#[derive(Clone, Debug)]
60pub struct Occluder {
61 pub x: f32,
62 pub y: f32,
63 pub width: f32,
64 pub height: f32,
65}
66
67#[derive(Clone, Debug)]
69pub struct DirectionalLight {
70 pub angle: f32,
71 pub r: f32,
72 pub g: f32,
73 pub b: f32,
74 pub intensity: f32,
75}
76
77#[derive(Clone, Debug)]
79pub struct SpotLight {
80 pub x: f32,
81 pub y: f32,
82 pub angle: f32,
83 pub spread: f32,
84 pub range: f32,
85 pub r: f32,
86 pub g: f32,
87 pub b: f32,
88 pub intensity: f32,
89}
90
91#[derive(Clone, Debug)]
93pub struct RadianceState {
94 pub enabled: bool,
95 pub emissives: Vec<EmissiveSurface>,
96 pub occluders: Vec<Occluder>,
97 pub directional_lights: Vec<DirectionalLight>,
98 pub spot_lights: Vec<SpotLight>,
99 pub gi_intensity: f32,
100 pub probe_spacing: Option<f32>,
102 pub interval: Option<f32>,
104 pub cascade_count: Option<u32>,
106}
107
108impl Default for RadianceState {
109 fn default() -> Self {
110 Self::new()
111 }
112}
113
114impl RadianceState {
115 pub fn new() -> Self {
116 Self {
117 enabled: false,
118 emissives: Vec::new(),
119 occluders: Vec::new(),
120 directional_lights: Vec::new(),
121 spot_lights: Vec::new(),
122 gi_intensity: 1.0,
123 probe_spacing: None,
124 interval: None,
125 cascade_count: None,
126 }
127 }
128}
129
130pub struct RadiancePipeline {
132 ray_march_pipeline: wgpu::ComputePipeline,
134 merge_pipeline: wgpu::ComputePipeline,
135 finalize_pipeline: wgpu::ComputePipeline,
136
137 compose_pipeline: wgpu::RenderPipeline,
139 compose_bind_group_layout: wgpu::BindGroupLayout,
140
141 compute_bind_group_layout: wgpu::BindGroupLayout,
143
144 params_buffer: wgpu::Buffer,
146
147 scene_texture: Option<SceneTexture>,
149
150 cascade_textures: Option<CascadeTextures>,
152
153 light_texture: Option<LightTexture>,
155
156 pub base_rays: u32,
158 pub probe_spacing: f32,
159 pub interval: f32,
160 pub cascade_count: u32,
161
162 sampler: wgpu::Sampler,
164 #[allow(dead_code)]
165 surface_format: wgpu::TextureFormat,
166}
167
168struct SceneTexture {
169 texture: wgpu::Texture,
170 view: wgpu::TextureView,
171 width: u32,
172 height: u32,
173}
174
175struct CascadeTextures {
176 #[allow(dead_code)]
178 tex_a: wgpu::Texture,
179 view_a: wgpu::TextureView,
180 #[allow(dead_code)]
181 tex_b: wgpu::Texture,
182 view_b: wgpu::TextureView,
183 width: u32,
184 height: u32,
185}
186
187struct LightTexture {
188 #[allow(dead_code)]
189 texture: wgpu::Texture,
190 view: wgpu::TextureView,
191 bind_group: wgpu::BindGroup,
192 #[allow(dead_code)]
193 width: u32,
194 #[allow(dead_code)]
195 height: u32,
196}
197
198impl RadiancePipeline {
199 pub fn new(gpu: &GpuContext) -> Self {
200 let shader = gpu.device.create_shader_module(wgpu::ShaderModuleDescriptor {
201 label: Some("radiance_compute_shader"),
202 source: wgpu::ShaderSource::Wgsl(include_str!("shaders/radiance.wgsl").into()),
203 });
204
205 let compute_bind_group_layout =
207 gpu.device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
208 label: Some("radiance_compute_bind_group_layout"),
209 entries: &[
210 wgpu::BindGroupLayoutEntry {
212 binding: 0,
213 visibility: wgpu::ShaderStages::COMPUTE,
214 ty: wgpu::BindingType::Buffer {
215 ty: wgpu::BufferBindingType::Uniform,
216 has_dynamic_offset: false,
217 min_binding_size: None,
218 },
219 count: None,
220 },
221 wgpu::BindGroupLayoutEntry {
223 binding: 1,
224 visibility: wgpu::ShaderStages::COMPUTE,
225 ty: wgpu::BindingType::Texture {
226 multisampled: false,
227 view_dimension: wgpu::TextureViewDimension::D2,
228 sample_type: wgpu::TextureSampleType::Float { filterable: false },
229 },
230 count: None,
231 },
232 wgpu::BindGroupLayoutEntry {
234 binding: 2,
235 visibility: wgpu::ShaderStages::COMPUTE,
236 ty: wgpu::BindingType::Texture {
237 multisampled: false,
238 view_dimension: wgpu::TextureViewDimension::D2,
239 sample_type: wgpu::TextureSampleType::Float { filterable: false },
240 },
241 count: None,
242 },
243 wgpu::BindGroupLayoutEntry {
245 binding: 3,
246 visibility: wgpu::ShaderStages::COMPUTE,
247 ty: wgpu::BindingType::StorageTexture {
248 access: wgpu::StorageTextureAccess::WriteOnly,
249 format: wgpu::TextureFormat::Rgba16Float,
250 view_dimension: wgpu::TextureViewDimension::D2,
251 },
252 count: None,
253 },
254 ],
255 });
256
257 let compute_layout =
258 gpu.device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
259 label: Some("radiance_compute_layout"),
260 bind_group_layouts: &[&compute_bind_group_layout],
261 push_constant_ranges: &[],
262 });
263
264 let ray_march_pipeline =
265 gpu.device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
266 label: Some("radiance_ray_march"),
267 layout: Some(&compute_layout),
268 module: &shader,
269 entry_point: Some("ray_march"),
270 compilation_options: Default::default(),
271 cache: None,
272 });
273
274 let merge_pipeline =
275 gpu.device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
276 label: Some("radiance_merge"),
277 layout: Some(&compute_layout),
278 module: &shader,
279 entry_point: Some("merge_cascades"),
280 compilation_options: Default::default(),
281 cache: None,
282 });
283
284 let finalize_pipeline =
285 gpu.device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
286 label: Some("radiance_finalize"),
287 layout: Some(&compute_layout),
288 module: &shader,
289 entry_point: Some("finalize"),
290 compilation_options: Default::default(),
291 cache: None,
292 });
293
294 let params_buffer = gpu.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
296 label: Some("radiance_params_buffer"),
297 contents: bytemuck::cast_slice(&[RadianceParams {
298 scene_dims: [0.0; 4],
299 cascade_params: [0.0; 4],
300 camera: [0.0; 4],
301 ambient: [1.0, 1.0, 1.0, 0.0],
302 }]),
303 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
304 });
305
306 let compose_bind_group_layout =
308 gpu.device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
309 label: Some("radiance_compose_bind_group_layout"),
310 entries: &[
311 wgpu::BindGroupLayoutEntry {
312 binding: 0,
313 visibility: wgpu::ShaderStages::FRAGMENT,
314 ty: wgpu::BindingType::Texture {
315 multisampled: false,
316 view_dimension: wgpu::TextureViewDimension::D2,
317 sample_type: wgpu::TextureSampleType::Float { filterable: true },
318 },
319 count: None,
320 },
321 wgpu::BindGroupLayoutEntry {
322 binding: 1,
323 visibility: wgpu::ShaderStages::FRAGMENT,
324 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
325 count: None,
326 },
327 ],
328 });
329
330 let compose_layout =
331 gpu.device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
332 label: Some("radiance_compose_layout"),
333 bind_group_layouts: &[&compose_bind_group_layout],
334 push_constant_ranges: &[],
335 });
336
337 let compose_shader =
338 gpu.device.create_shader_module(wgpu::ShaderModuleDescriptor {
339 label: Some("radiance_compose_shader"),
340 source: wgpu::ShaderSource::Wgsl(COMPOSE_WGSL.into()),
341 });
342
343 let compose_pipeline =
344 gpu.device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
345 label: Some("radiance_compose_pipeline"),
346 layout: Some(&compose_layout),
347 vertex: wgpu::VertexState {
348 module: &compose_shader,
349 entry_point: Some("vs_main"),
350 buffers: &[],
351 compilation_options: Default::default(),
352 },
353 fragment: Some(wgpu::FragmentState {
354 module: &compose_shader,
355 entry_point: Some("fs_main"),
356 targets: &[Some(wgpu::ColorTargetState {
357 format: gpu.config.format,
358 blend: Some(wgpu::BlendState {
359 color: wgpu::BlendComponent {
362 src_factor: wgpu::BlendFactor::Dst,
363 dst_factor: wgpu::BlendFactor::One,
364 operation: wgpu::BlendOperation::Add,
365 },
366 alpha: wgpu::BlendComponent::OVER,
367 }),
368 write_mask: wgpu::ColorWrites::ALL,
369 })],
370 compilation_options: Default::default(),
371 }),
372 primitive: wgpu::PrimitiveState {
373 topology: wgpu::PrimitiveTopology::TriangleList,
374 ..Default::default()
375 },
376 depth_stencil: None,
377 multisample: wgpu::MultisampleState::default(),
378 multiview: None,
379 cache: None,
380 });
381
382 let sampler = gpu.device.create_sampler(&wgpu::SamplerDescriptor {
383 label: Some("radiance_sampler"),
384 address_mode_u: wgpu::AddressMode::ClampToEdge,
385 address_mode_v: wgpu::AddressMode::ClampToEdge,
386 mag_filter: wgpu::FilterMode::Linear,
387 min_filter: wgpu::FilterMode::Linear,
388 ..Default::default()
389 });
390
391 Self {
392 ray_march_pipeline,
393 merge_pipeline,
394 finalize_pipeline,
395 compose_pipeline,
396 compose_bind_group_layout,
397 compute_bind_group_layout,
398 params_buffer,
399 scene_texture: None,
400 cascade_textures: None,
401 light_texture: None,
402 base_rays: DEFAULT_BASE_RAYS,
403 probe_spacing: DEFAULT_PROBE_SPACING,
404 interval: DEFAULT_INTERVAL,
405 cascade_count: 4,
406 sampler,
407 surface_format: gpu.config.format,
408 }
409 }
410
411 fn ensure_textures(&mut self, gpu: &GpuContext, scene_w: u32, scene_h: u32) {
413 let needs_recreate = self
414 .scene_texture
415 .as_ref()
416 .map(|t| t.width != scene_w || t.height != scene_h)
417 .unwrap_or(true);
418
419 if !needs_recreate {
420 return;
421 }
422
423 let scene_tex = gpu.device.create_texture(&wgpu::TextureDescriptor {
425 label: Some("radiance_scene_texture"),
426 size: wgpu::Extent3d {
427 width: scene_w,
428 height: scene_h,
429 depth_or_array_layers: 1,
430 },
431 mip_level_count: 1,
432 sample_count: 1,
433 dimension: wgpu::TextureDimension::D2,
434 format: wgpu::TextureFormat::Rgba32Float,
435 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
436 view_formats: &[],
437 });
438
439 let scene_view = scene_tex.create_view(&wgpu::TextureViewDescriptor::default());
440 self.scene_texture = Some(SceneTexture {
441 texture: scene_tex,
442 view: scene_view,
443 width: scene_w,
444 height: scene_h,
445 });
446
447 let probes_x = (scene_w as f32 / self.probe_spacing).ceil() as u32;
452 let probes_y = (scene_h as f32 / self.probe_spacing).ceil() as u32;
453 let rays_per_side = (self.base_rays as f32).sqrt().ceil() as u32;
454 let cascade_w = probes_x * rays_per_side;
455 let cascade_h = probes_y * rays_per_side;
456
457 let cascade_w = cascade_w.max(1);
459 let cascade_h = cascade_h.max(1);
460
461 let create_cascade_tex = |label: &str| -> (wgpu::Texture, wgpu::TextureView) {
462 let tex = gpu.device.create_texture(&wgpu::TextureDescriptor {
463 label: Some(label),
464 size: wgpu::Extent3d {
465 width: cascade_w,
466 height: cascade_h,
467 depth_or_array_layers: 1,
468 },
469 mip_level_count: 1,
470 sample_count: 1,
471 dimension: wgpu::TextureDimension::D2,
472 format: wgpu::TextureFormat::Rgba16Float,
473 usage: wgpu::TextureUsages::TEXTURE_BINDING
474 | wgpu::TextureUsages::STORAGE_BINDING,
475 view_formats: &[],
476 });
477 let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
478 (tex, view)
479 };
480
481 let (tex_a, view_a) = create_cascade_tex("radiance_cascade_a");
482 let (tex_b, view_b) = create_cascade_tex("radiance_cascade_b");
483
484 self.cascade_textures = Some(CascadeTextures {
485 tex_a,
486 view_a,
487 tex_b,
488 view_b,
489 width: cascade_w,
490 height: cascade_h,
491 });
492
493 let light_tex = gpu.device.create_texture(&wgpu::TextureDescriptor {
495 label: Some("radiance_light_texture"),
496 size: wgpu::Extent3d {
497 width: scene_w,
498 height: scene_h,
499 depth_or_array_layers: 1,
500 },
501 mip_level_count: 1,
502 sample_count: 1,
503 dimension: wgpu::TextureDimension::D2,
504 format: wgpu::TextureFormat::Rgba16Float,
505 usage: wgpu::TextureUsages::TEXTURE_BINDING
506 | wgpu::TextureUsages::STORAGE_BINDING,
507 view_formats: &[],
508 });
509
510 let light_view = light_tex.create_view(&wgpu::TextureViewDescriptor::default());
511
512 let light_bind_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
513 label: Some("radiance_light_bind_group"),
514 layout: &self.compose_bind_group_layout,
515 entries: &[
516 wgpu::BindGroupEntry {
517 binding: 0,
518 resource: wgpu::BindingResource::TextureView(&light_view),
519 },
520 wgpu::BindGroupEntry {
521 binding: 1,
522 resource: wgpu::BindingResource::Sampler(&self.sampler),
523 },
524 ],
525 });
526
527 self.light_texture = Some(LightTexture {
528 texture: light_tex,
529 view: light_view,
530 bind_group: light_bind_group,
531 width: scene_w,
532 height: scene_h,
533 });
534 }
535
536 fn build_scene_data(
539 &self,
540 scene_w: u32,
541 scene_h: u32,
542 radiance: &RadianceState,
543 lighting: &LightingState,
544 camera_x: f32,
545 camera_y: f32,
546 viewport_w: f32,
547 viewport_h: f32,
548 ) -> Vec<u8> {
549 let w = scene_w as usize;
550 let h = scene_h as usize;
551 let mut pixels = vec![0.0f32; w * h * 4];
553
554 let world_left = camera_x - viewport_w / 2.0;
556 let world_top = camera_y - viewport_h / 2.0;
557
558 for em in &radiance.emissives {
560 let px0 = ((em.x - world_left) as i32).max(0) as usize;
561 let py0 = ((em.y - world_top) as i32).max(0) as usize;
562 let px1 = ((em.x + em.width - world_left) as i32).min(w as i32) as usize;
563 let py1 = ((em.y + em.height - world_top) as i32).min(h as i32) as usize;
564
565 let er = em.r * em.intensity;
566 let eg = em.g * em.intensity;
567 let eb = em.b * em.intensity;
568
569 for py in py0..py1 {
570 for px in px0..px1 {
571 let idx = (py * w + px) * 4;
572 pixels[idx] += er;
573 pixels[idx + 1] += eg;
574 pixels[idx + 2] += eb;
575 }
576 }
577 }
578
579 for light in &lighting.lights {
581 let cx = (light.x - world_left) as i32;
582 let cy = (light.y - world_top) as i32;
583 let r_px = (light.radius * 0.1).max(2.0) as i32;
584
585 let er = light.r * light.intensity;
586 let eg = light.g * light.intensity;
587 let eb = light.b * light.intensity;
588
589 for dy in -r_px..=r_px {
590 for dx in -r_px..=r_px {
591 if dx * dx + dy * dy <= r_px * r_px {
592 let px = (cx + dx) as usize;
593 let py = (cy + dy) as usize;
594 if px < w && py < h {
595 let idx = (py * w + px) * 4;
596 pixels[idx] += er;
597 pixels[idx + 1] += eg;
598 pixels[idx + 2] += eb;
599 }
600 }
601 }
602 }
603 }
604
605 for spot in &radiance.spot_lights {
607 let cx = (spot.x - world_left) as i32;
608 let cy = (spot.y - world_top) as i32;
609 let r_px = 3i32;
610
611 let er = spot.r * spot.intensity;
612 let eg = spot.g * spot.intensity;
613 let eb = spot.b * spot.intensity;
614
615 for dy in -r_px..=r_px {
616 for dx in -r_px..=r_px {
617 if dx * dx + dy * dy <= r_px * r_px {
618 let px = (cx + dx) as usize;
619 let py = (cy + dy) as usize;
620 if px < w && py < h {
621 let idx = (py * w + px) * 4;
622 pixels[idx] += er;
623 pixels[idx + 1] += eg;
624 pixels[idx + 2] += eb;
625 }
626 }
627 }
628 }
629 }
630
631 for occ in &radiance.occluders {
633 let px0 = ((occ.x - world_left) as i32).max(0) as usize;
634 let py0 = ((occ.y - world_top) as i32).max(0) as usize;
635 let px1 = ((occ.x + occ.width - world_left) as i32).min(w as i32) as usize;
636 let py1 = ((occ.y + occ.height - world_top) as i32).min(h as i32) as usize;
637
638 for py in py0..py1 {
639 for px in px0..px1 {
640 let idx = (py * w + px) * 4;
641 pixels[idx + 3] = 1.0; }
643 }
644 }
645
646 bytemuck::cast_slice(&pixels).to_vec()
648 }
649
650 pub fn compute(
653 &mut self,
654 gpu: &GpuContext,
655 encoder: &mut wgpu::CommandEncoder,
656 radiance: &RadianceState,
657 lighting: &LightingState,
658 camera_x: f32,
659 camera_y: f32,
660 viewport_w: f32,
661 viewport_h: f32,
662 ) -> bool {
663 if !radiance.enabled {
664 return false;
665 }
666
667 if let Some(ps) = radiance.probe_spacing {
669 self.probe_spacing = ps;
670 }
671 if let Some(iv) = radiance.interval {
672 self.interval = iv;
673 }
674 if let Some(cc) = radiance.cascade_count {
675 self.cascade_count = cc;
676 }
677
678 let scene_w = viewport_w.ceil() as u32;
680 let scene_h = viewport_h.ceil() as u32;
681 if scene_w == 0 || scene_h == 0 {
682 return false;
683 }
684
685 self.ensure_textures(gpu, scene_w, scene_h);
686
687 let scene_tex = self.scene_texture.as_ref().unwrap();
688 let cascades = self.cascade_textures.as_ref().unwrap();
689 let light_tex = self.light_texture.as_ref().unwrap();
690
691 let scene_data = self.build_scene_data(
693 scene_w, scene_h, radiance, lighting, camera_x, camera_y, viewport_w, viewport_h,
694 );
695
696 gpu.queue.write_texture(
697 wgpu::TexelCopyTextureInfo {
698 texture: &scene_tex.texture,
699 mip_level: 0,
700 origin: wgpu::Origin3d::ZERO,
701 aspect: wgpu::TextureAspect::All,
702 },
703 &scene_data,
704 wgpu::TexelCopyBufferLayout {
705 offset: 0,
706 bytes_per_row: Some(scene_w * 16), rows_per_image: Some(scene_h),
708 },
709 wgpu::Extent3d {
710 width: scene_w,
711 height: scene_h,
712 depth_or_array_layers: 1,
713 },
714 );
715
716 let cascade_count = self.cascade_count.min(MAX_CASCADES as u32);
717
718 for c in (0..cascade_count).rev() {
722 let params = RadianceParams {
723 scene_dims: [scene_w as f32, scene_h as f32, c as f32, cascade_count as f32],
724 cascade_params: [
725 self.probe_spacing,
726 self.base_rays as f32,
727 self.interval,
728 radiance.gi_intensity,
729 ],
730 camera: [camera_x, camera_y, viewport_w, viewport_h],
731 ambient: [
732 lighting.ambient[0],
733 lighting.ambient[1],
734 lighting.ambient[2],
735 0.0,
736 ],
737 };
738
739 gpu.queue.write_buffer(
740 &self.params_buffer,
741 0,
742 bytemuck::cast_slice(&[params]),
743 );
744
745 let bind_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
747 label: Some(&format!("radiance_ray_march_bg_{c}")),
748 layout: &self.compute_bind_group_layout,
749 entries: &[
750 wgpu::BindGroupEntry {
751 binding: 0,
752 resource: self.params_buffer.as_entire_binding(),
753 },
754 wgpu::BindGroupEntry {
755 binding: 1,
756 resource: wgpu::BindingResource::TextureView(&scene_tex.view),
757 },
758 wgpu::BindGroupEntry {
759 binding: 2,
760 resource: wgpu::BindingResource::TextureView(&cascades.view_b),
761 },
762 wgpu::BindGroupEntry {
763 binding: 3,
764 resource: wgpu::BindingResource::TextureView(&cascades.view_a),
765 },
766 ],
767 });
768
769 {
770 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
771 label: Some(&format!("radiance_ray_march_{c}")),
772 timestamp_writes: None,
773 });
774 pass.set_pipeline(&self.ray_march_pipeline);
775 pass.set_bind_group(0, &bind_group, &[]);
776 pass.dispatch_workgroups(
777 (cascades.width + 7) / 8,
778 (cascades.height + 7) / 8,
779 1,
780 );
781 }
782
783 if c < cascade_count - 1 {
785 let merge_bg = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
787 label: Some(&format!("radiance_merge_bg_{c}")),
788 layout: &self.compute_bind_group_layout,
789 entries: &[
790 wgpu::BindGroupEntry {
791 binding: 0,
792 resource: self.params_buffer.as_entire_binding(),
793 },
794 wgpu::BindGroupEntry {
795 binding: 1,
796 resource: wgpu::BindingResource::TextureView(&scene_tex.view),
797 },
798 wgpu::BindGroupEntry {
799 binding: 2,
800 resource: wgpu::BindingResource::TextureView(&cascades.view_a),
801 },
802 wgpu::BindGroupEntry {
803 binding: 3,
804 resource: wgpu::BindingResource::TextureView(&cascades.view_b),
805 },
806 ],
807 });
808
809 {
810 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
811 label: Some(&format!("radiance_merge_{c}")),
812 timestamp_writes: None,
813 });
814 pass.set_pipeline(&self.merge_pipeline);
815 pass.set_bind_group(0, &merge_bg, &[]);
816 pass.dispatch_workgroups(
817 (cascades.width + 7) / 8,
818 (cascades.height + 7) / 8,
819 1,
820 );
821 }
822
823 }
826 }
827
828 {
830 let params = RadianceParams {
831 scene_dims: [scene_w as f32, scene_h as f32, 0.0, cascade_count as f32],
832 cascade_params: [
833 self.probe_spacing,
834 self.base_rays as f32,
835 self.interval,
836 radiance.gi_intensity,
837 ],
838 camera: [camera_x, camera_y, viewport_w, viewport_h],
839 ambient: [
840 lighting.ambient[0],
841 lighting.ambient[1],
842 lighting.ambient[2],
843 0.0,
844 ],
845 };
846
847 gpu.queue.write_buffer(
848 &self.params_buffer,
849 0,
850 bytemuck::cast_slice(&[params]),
851 );
852
853 let final_cascade_view = if cascade_count > 1 {
855 &cascades.view_b
856 } else {
857 &cascades.view_a
858 };
859
860 let finalize_bg = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
861 label: Some("radiance_finalize_bg"),
862 layout: &self.compute_bind_group_layout,
863 entries: &[
864 wgpu::BindGroupEntry {
865 binding: 0,
866 resource: self.params_buffer.as_entire_binding(),
867 },
868 wgpu::BindGroupEntry {
869 binding: 1,
870 resource: wgpu::BindingResource::TextureView(&scene_tex.view),
871 },
872 wgpu::BindGroupEntry {
873 binding: 2,
874 resource: wgpu::BindingResource::TextureView(final_cascade_view),
875 },
876 wgpu::BindGroupEntry {
877 binding: 3,
878 resource: wgpu::BindingResource::TextureView(&light_tex.view),
879 },
880 ],
881 });
882
883 {
884 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
885 label: Some("radiance_finalize"),
886 timestamp_writes: None,
887 });
888 pass.set_pipeline(&self.finalize_pipeline);
889 pass.set_bind_group(0, &finalize_bg, &[]);
890 pass.dispatch_workgroups((scene_w + 7) / 8, (scene_h + 7) / 8, 1);
891 }
892 }
893
894 true
895 }
896
897 pub fn compose(
903 &self,
904 encoder: &mut wgpu::CommandEncoder,
905 target: &wgpu::TextureView,
906 ) {
907 let Some(ref light_tex) = self.light_texture else {
908 return;
909 };
910
911 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
912 label: Some("radiance_compose_pass"),
913 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
914 view: target,
915 resolve_target: None,
916 ops: wgpu::Operations {
917 load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store,
919 },
920 })],
921 depth_stencil_attachment: None,
922 timestamp_writes: None,
923 occlusion_query_set: None,
924 });
925
926 pass.set_pipeline(&self.compose_pipeline);
927 pass.set_bind_group(0, &light_tex.bind_group, &[]);
928 pass.draw(0..3, 0..1); }
930}
931
932const COMPOSE_WGSL: &str = r#"
935@group(0) @binding(0)
936var t_light: texture_2d<f32>;
937
938@group(0) @binding(1)
939var s_light: sampler;
940
941struct VertexOutput {
942 @builtin(position) position: vec4<f32>,
943 @location(0) uv: vec2<f32>,
944};
945
946@vertex
947fn vs_main(@builtin(vertex_index) idx: u32) -> VertexOutput {
948 var out: VertexOutput;
949 let uv = vec2<f32>(f32((idx << 1u) & 2u), f32(idx & 2u));
950 out.position = vec4<f32>(uv * 2.0 - 1.0, 0.0, 1.0);
951 out.uv = vec2<f32>(uv.x, 1.0 - uv.y);
952 return out;
953}
954
955@fragment
956fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
957 let light = textureSample(t_light, s_light, in.uv);
958 // Output the light color — blend state does the multiplication with dst
959 return light;
960}
961"#;
962
963#[cfg(test)]
964mod tests {
965 use super::*;
966
967 #[test]
968 fn test_radiance_params_size() {
969 assert_eq!(std::mem::size_of::<RadianceParams>(), 64);
970 }
971
972 #[test]
973 fn test_emissive_surface_clone() {
974 let em = EmissiveSurface {
975 x: 10.0,
976 y: 20.0,
977 width: 32.0,
978 height: 32.0,
979 r: 1.0,
980 g: 0.5,
981 b: 0.0,
982 intensity: 2.0,
983 };
984 let em2 = em.clone();
985 assert_eq!(em2.x, 10.0);
986 assert_eq!(em2.intensity, 2.0);
987 }
988
989 #[test]
990 fn test_occluder_clone() {
991 let occ = Occluder {
992 x: 50.0,
993 y: 60.0,
994 width: 100.0,
995 height: 20.0,
996 };
997 let occ2 = occ.clone();
998 assert_eq!(occ2.width, 100.0);
999 }
1000
1001 #[test]
1002 fn test_radiance_state_default() {
1003 let state = RadianceState::default();
1004 assert!(!state.enabled);
1005 assert!(state.emissives.is_empty());
1006 assert!(state.occluders.is_empty());
1007 assert!(state.directional_lights.is_empty());
1008 assert!(state.spot_lights.is_empty());
1009 assert_eq!(state.gi_intensity, 1.0);
1010 }
1011
1012 #[test]
1013 fn test_directional_light() {
1014 let dl = DirectionalLight {
1015 angle: 1.5,
1016 r: 1.0,
1017 g: 0.9,
1018 b: 0.7,
1019 intensity: 0.8,
1020 };
1021 assert_eq!(dl.angle, 1.5);
1022 }
1023
1024 #[test]
1025 fn test_spot_light() {
1026 let sl = SpotLight {
1027 x: 100.0,
1028 y: 200.0,
1029 angle: 0.0,
1030 spread: 0.5,
1031 range: 300.0,
1032 r: 1.0,
1033 g: 1.0,
1034 b: 0.8,
1035 intensity: 1.5,
1036 };
1037 assert_eq!(sl.range, 300.0);
1038 }
1039}