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 Self::new_internal(&gpu.device, gpu.config.format)
201 }
202
203 pub fn new_headless(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
205 Self::new_internal(device, format)
206 }
207
208 fn new_internal(device: &wgpu::Device, surface_format: wgpu::TextureFormat) -> Self {
209 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
210 label: Some("radiance_compute_shader"),
211 source: wgpu::ShaderSource::Wgsl(include_str!("shaders/radiance.wgsl").into()),
212 });
213
214 let compute_bind_group_layout =
216 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
217 label: Some("radiance_compute_bind_group_layout"),
218 entries: &[
219 wgpu::BindGroupLayoutEntry {
221 binding: 0,
222 visibility: wgpu::ShaderStages::COMPUTE,
223 ty: wgpu::BindingType::Buffer {
224 ty: wgpu::BufferBindingType::Uniform,
225 has_dynamic_offset: false,
226 min_binding_size: None,
227 },
228 count: None,
229 },
230 wgpu::BindGroupLayoutEntry {
232 binding: 1,
233 visibility: wgpu::ShaderStages::COMPUTE,
234 ty: wgpu::BindingType::Texture {
235 multisampled: false,
236 view_dimension: wgpu::TextureViewDimension::D2,
237 sample_type: wgpu::TextureSampleType::Float { filterable: false },
238 },
239 count: None,
240 },
241 wgpu::BindGroupLayoutEntry {
243 binding: 2,
244 visibility: wgpu::ShaderStages::COMPUTE,
245 ty: wgpu::BindingType::Texture {
246 multisampled: false,
247 view_dimension: wgpu::TextureViewDimension::D2,
248 sample_type: wgpu::TextureSampleType::Float { filterable: false },
249 },
250 count: None,
251 },
252 wgpu::BindGroupLayoutEntry {
254 binding: 3,
255 visibility: wgpu::ShaderStages::COMPUTE,
256 ty: wgpu::BindingType::StorageTexture {
257 access: wgpu::StorageTextureAccess::WriteOnly,
258 format: wgpu::TextureFormat::Rgba16Float,
259 view_dimension: wgpu::TextureViewDimension::D2,
260 },
261 count: None,
262 },
263 ],
264 });
265
266 let compute_layout =
267 device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
268 label: Some("radiance_compute_layout"),
269 bind_group_layouts: &[&compute_bind_group_layout],
270 push_constant_ranges: &[],
271 });
272
273 let ray_march_pipeline =
274 device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
275 label: Some("radiance_ray_march"),
276 layout: Some(&compute_layout),
277 module: &shader,
278 entry_point: Some("ray_march"),
279 compilation_options: Default::default(),
280 cache: None,
281 });
282
283 let merge_pipeline =
284 device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
285 label: Some("radiance_merge"),
286 layout: Some(&compute_layout),
287 module: &shader,
288 entry_point: Some("merge_cascades"),
289 compilation_options: Default::default(),
290 cache: None,
291 });
292
293 let finalize_pipeline =
294 device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
295 label: Some("radiance_finalize"),
296 layout: Some(&compute_layout),
297 module: &shader,
298 entry_point: Some("finalize"),
299 compilation_options: Default::default(),
300 cache: None,
301 });
302
303 let params_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
305 label: Some("radiance_params_buffer"),
306 contents: bytemuck::cast_slice(&[RadianceParams {
307 scene_dims: [0.0; 4],
308 cascade_params: [0.0; 4],
309 camera: [0.0; 4],
310 ambient: [1.0, 1.0, 1.0, 0.0],
311 }]),
312 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
313 });
314
315 let compose_bind_group_layout =
317 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
318 label: Some("radiance_compose_bind_group_layout"),
319 entries: &[
320 wgpu::BindGroupLayoutEntry {
321 binding: 0,
322 visibility: wgpu::ShaderStages::FRAGMENT,
323 ty: wgpu::BindingType::Texture {
324 multisampled: false,
325 view_dimension: wgpu::TextureViewDimension::D2,
326 sample_type: wgpu::TextureSampleType::Float { filterable: true },
327 },
328 count: None,
329 },
330 wgpu::BindGroupLayoutEntry {
331 binding: 1,
332 visibility: wgpu::ShaderStages::FRAGMENT,
333 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
334 count: None,
335 },
336 ],
337 });
338
339 let compose_layout =
340 device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
341 label: Some("radiance_compose_layout"),
342 bind_group_layouts: &[&compose_bind_group_layout],
343 push_constant_ranges: &[],
344 });
345
346 let compose_shader =
347 device.create_shader_module(wgpu::ShaderModuleDescriptor {
348 label: Some("radiance_compose_shader"),
349 source: wgpu::ShaderSource::Wgsl(COMPOSE_WGSL.into()),
350 });
351
352 let compose_pipeline =
353 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
354 label: Some("radiance_compose_pipeline"),
355 layout: Some(&compose_layout),
356 vertex: wgpu::VertexState {
357 module: &compose_shader,
358 entry_point: Some("vs_main"),
359 buffers: &[],
360 compilation_options: Default::default(),
361 },
362 fragment: Some(wgpu::FragmentState {
363 module: &compose_shader,
364 entry_point: Some("fs_main"),
365 targets: &[Some(wgpu::ColorTargetState {
366 format: surface_format,
367 blend: Some(wgpu::BlendState {
368 color: wgpu::BlendComponent {
371 src_factor: wgpu::BlendFactor::Dst,
372 dst_factor: wgpu::BlendFactor::One,
373 operation: wgpu::BlendOperation::Add,
374 },
375 alpha: wgpu::BlendComponent::OVER,
376 }),
377 write_mask: wgpu::ColorWrites::ALL,
378 })],
379 compilation_options: Default::default(),
380 }),
381 primitive: wgpu::PrimitiveState {
382 topology: wgpu::PrimitiveTopology::TriangleList,
383 ..Default::default()
384 },
385 depth_stencil: None,
386 multisample: wgpu::MultisampleState::default(),
387 multiview: None,
388 cache: None,
389 });
390
391 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
392 label: Some("radiance_sampler"),
393 address_mode_u: wgpu::AddressMode::ClampToEdge,
394 address_mode_v: wgpu::AddressMode::ClampToEdge,
395 mag_filter: wgpu::FilterMode::Linear,
396 min_filter: wgpu::FilterMode::Linear,
397 ..Default::default()
398 });
399
400 Self {
401 ray_march_pipeline,
402 merge_pipeline,
403 finalize_pipeline,
404 compose_pipeline,
405 compose_bind_group_layout,
406 compute_bind_group_layout,
407 params_buffer,
408 scene_texture: None,
409 cascade_textures: None,
410 light_texture: None,
411 base_rays: DEFAULT_BASE_RAYS,
412 probe_spacing: DEFAULT_PROBE_SPACING,
413 interval: DEFAULT_INTERVAL,
414 cascade_count: 4,
415 sampler,
416 surface_format,
417 }
418 }
419
420 fn ensure_textures(&mut self, gpu: &GpuContext, scene_w: u32, scene_h: u32) {
422 let needs_recreate = self
423 .scene_texture
424 .as_ref()
425 .map(|t| t.width != scene_w || t.height != scene_h)
426 .unwrap_or(true);
427
428 if !needs_recreate {
429 return;
430 }
431
432 let scene_tex = gpu.device.create_texture(&wgpu::TextureDescriptor {
434 label: Some("radiance_scene_texture"),
435 size: wgpu::Extent3d {
436 width: scene_w,
437 height: scene_h,
438 depth_or_array_layers: 1,
439 },
440 mip_level_count: 1,
441 sample_count: 1,
442 dimension: wgpu::TextureDimension::D2,
443 format: wgpu::TextureFormat::Rgba32Float,
444 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
445 view_formats: &[],
446 });
447
448 let scene_view = scene_tex.create_view(&wgpu::TextureViewDescriptor::default());
449 self.scene_texture = Some(SceneTexture {
450 texture: scene_tex,
451 view: scene_view,
452 width: scene_w,
453 height: scene_h,
454 });
455
456 let probes_x = (scene_w as f32 / self.probe_spacing).ceil() as u32;
461 let probes_y = (scene_h as f32 / self.probe_spacing).ceil() as u32;
462 let rays_per_side = (self.base_rays as f32).sqrt().ceil() as u32;
463 let cascade_w = probes_x * rays_per_side;
464 let cascade_h = probes_y * rays_per_side;
465
466 let cascade_w = cascade_w.max(1);
468 let cascade_h = cascade_h.max(1);
469
470 let create_cascade_tex = |label: &str| -> (wgpu::Texture, wgpu::TextureView) {
471 let tex = gpu.device.create_texture(&wgpu::TextureDescriptor {
472 label: Some(label),
473 size: wgpu::Extent3d {
474 width: cascade_w,
475 height: cascade_h,
476 depth_or_array_layers: 1,
477 },
478 mip_level_count: 1,
479 sample_count: 1,
480 dimension: wgpu::TextureDimension::D2,
481 format: wgpu::TextureFormat::Rgba16Float,
482 usage: wgpu::TextureUsages::TEXTURE_BINDING
483 | wgpu::TextureUsages::STORAGE_BINDING,
484 view_formats: &[],
485 });
486 let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
487 (tex, view)
488 };
489
490 let (tex_a, view_a) = create_cascade_tex("radiance_cascade_a");
491 let (tex_b, view_b) = create_cascade_tex("radiance_cascade_b");
492
493 self.cascade_textures = Some(CascadeTextures {
494 tex_a,
495 view_a,
496 tex_b,
497 view_b,
498 width: cascade_w,
499 height: cascade_h,
500 });
501
502 let light_tex = gpu.device.create_texture(&wgpu::TextureDescriptor {
504 label: Some("radiance_light_texture"),
505 size: wgpu::Extent3d {
506 width: scene_w,
507 height: scene_h,
508 depth_or_array_layers: 1,
509 },
510 mip_level_count: 1,
511 sample_count: 1,
512 dimension: wgpu::TextureDimension::D2,
513 format: wgpu::TextureFormat::Rgba16Float,
514 usage: wgpu::TextureUsages::TEXTURE_BINDING
515 | wgpu::TextureUsages::STORAGE_BINDING,
516 view_formats: &[],
517 });
518
519 let light_view = light_tex.create_view(&wgpu::TextureViewDescriptor::default());
520
521 let light_bind_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
522 label: Some("radiance_light_bind_group"),
523 layout: &self.compose_bind_group_layout,
524 entries: &[
525 wgpu::BindGroupEntry {
526 binding: 0,
527 resource: wgpu::BindingResource::TextureView(&light_view),
528 },
529 wgpu::BindGroupEntry {
530 binding: 1,
531 resource: wgpu::BindingResource::Sampler(&self.sampler),
532 },
533 ],
534 });
535
536 self.light_texture = Some(LightTexture {
537 texture: light_tex,
538 view: light_view,
539 bind_group: light_bind_group,
540 width: scene_w,
541 height: scene_h,
542 });
543 }
544
545 fn build_scene_data(
546 &self,
547 scene_w: u32,
548 scene_h: u32,
549 radiance: &RadianceState,
550 lighting: &LightingState,
551 camera_x: f32,
552 camera_y: f32,
553 viewport_w: f32,
554 viewport_h: f32,
555 ) -> Vec<u8> {
556 build_scene_data(scene_w, scene_h, radiance, lighting, camera_x, camera_y, viewport_w, viewport_h)
557 }
558}
559
560fn build_scene_data(
563 scene_w: u32,
564 scene_h: u32,
565 radiance: &RadianceState,
566 lighting: &LightingState,
567 camera_x: f32,
568 camera_y: f32,
569 viewport_w: f32,
570 viewport_h: f32,
571) -> Vec<u8> {
572 let w = scene_w as usize;
573 let h = scene_h as usize;
574 let mut pixels = vec![0.0f32; w * h * 4];
576
577 let world_left = camera_x - viewport_w / 2.0;
579 let world_top = camera_y - viewport_h / 2.0;
580
581 for em in &radiance.emissives {
583 let px0 = ((em.x - world_left) as i32).max(0) as usize;
584 let py0 = ((em.y - world_top) as i32).max(0) as usize;
585 let px1 = ((em.x + em.width - world_left) as i32).max(0).min(w as i32) as usize;
586 let py1 = ((em.y + em.height - world_top) as i32).max(0).min(h as i32) as usize;
587
588 let er = em.r * em.intensity;
589 let eg = em.g * em.intensity;
590 let eb = em.b * em.intensity;
591
592 for py in py0..py1 {
593 for px in px0..px1 {
594 let idx = (py * w + px) * 4;
595 pixels[idx] += er;
596 pixels[idx + 1] += eg;
597 pixels[idx + 2] += eb;
598 }
599 }
600 }
601
602 for light in &lighting.lights {
604 let cx = (light.x - world_left) as i32;
605 let cy = (light.y - world_top) as i32;
606 let r_px = (light.radius * 0.1).max(2.0) as i32;
607
608 let er = light.r * light.intensity;
609 let eg = light.g * light.intensity;
610 let eb = light.b * light.intensity;
611
612 for dy in -r_px..=r_px {
613 for dx in -r_px..=r_px {
614 if dx * dx + dy * dy <= r_px * r_px {
615 let px = (cx + dx) as usize;
616 let py = (cy + dy) as usize;
617 if px < w && py < h {
618 let idx = (py * w + px) * 4;
619 pixels[idx] += er;
620 pixels[idx + 1] += eg;
621 pixels[idx + 2] += eb;
622 }
623 }
624 }
625 }
626 }
627
628 for spot in &radiance.spot_lights {
630 let cx = (spot.x - world_left) as i32;
631 let cy = (spot.y - world_top) as i32;
632 let r_px = 3i32;
633
634 let er = spot.r * spot.intensity;
635 let eg = spot.g * spot.intensity;
636 let eb = spot.b * spot.intensity;
637
638 for dy in -r_px..=r_px {
639 for dx in -r_px..=r_px {
640 if dx * dx + dy * dy <= r_px * r_px {
641 let px = (cx + dx) as usize;
642 let py = (cy + dy) as usize;
643 if px < w && py < h {
644 let idx = (py * w + px) * 4;
645 pixels[idx] += er;
646 pixels[idx + 1] += eg;
647 pixels[idx + 2] += eb;
648 }
649 }
650 }
651 }
652 }
653
654 for occ in &radiance.occluders {
656 let px0 = ((occ.x - world_left) as i32).max(0) as usize;
657 let py0 = ((occ.y - world_top) as i32).max(0) as usize;
658 let px1 = ((occ.x + occ.width - world_left) as i32).max(0).min(w as i32) as usize;
659 let py1 = ((occ.y + occ.height - world_top) as i32).max(0).min(h as i32) as usize;
660
661 for py in py0..py1 {
662 for px in px0..px1 {
663 let idx = (py * w + px) * 4;
664 pixels[idx + 3] = 1.0; }
666 }
667 }
668
669 bytemuck::cast_slice(&pixels).to_vec()
671 }
672
673impl RadiancePipeline {
674 pub fn compute(
677 &mut self,
678 gpu: &GpuContext,
679 encoder: &mut wgpu::CommandEncoder,
680 radiance: &RadianceState,
681 lighting: &LightingState,
682 camera_x: f32,
683 camera_y: f32,
684 viewport_w: f32,
685 viewport_h: f32,
686 ) -> bool {
687 if !radiance.enabled {
688 return false;
689 }
690
691 if let Some(ps) = radiance.probe_spacing {
693 self.probe_spacing = ps;
694 }
695 if let Some(iv) = radiance.interval {
696 self.interval = iv;
697 }
698 if let Some(cc) = radiance.cascade_count {
699 self.cascade_count = cc;
700 }
701
702 let scene_w = viewport_w.ceil() as u32;
704 let scene_h = viewport_h.ceil() as u32;
705 if scene_w == 0 || scene_h == 0 {
706 return false;
707 }
708
709 self.ensure_textures(gpu, scene_w, scene_h);
710
711 let scene_tex = self.scene_texture.as_ref().unwrap();
712 let cascades = self.cascade_textures.as_ref().unwrap();
713 let light_tex = self.light_texture.as_ref().unwrap();
714
715 let scene_data = self.build_scene_data(
717 scene_w, scene_h, radiance, lighting, camera_x, camera_y, viewport_w, viewport_h,
718 );
719
720 gpu.queue.write_texture(
721 wgpu::TexelCopyTextureInfo {
722 texture: &scene_tex.texture,
723 mip_level: 0,
724 origin: wgpu::Origin3d::ZERO,
725 aspect: wgpu::TextureAspect::All,
726 },
727 &scene_data,
728 wgpu::TexelCopyBufferLayout {
729 offset: 0,
730 bytes_per_row: Some(scene_w * 16), rows_per_image: Some(scene_h),
732 },
733 wgpu::Extent3d {
734 width: scene_w,
735 height: scene_h,
736 depth_or_array_layers: 1,
737 },
738 );
739
740 let cascade_count = self.cascade_count.min(MAX_CASCADES as u32);
741
742 for c in (0..cascade_count).rev() {
746 let params = RadianceParams {
747 scene_dims: [scene_w as f32, scene_h as f32, c as f32, cascade_count as f32],
748 cascade_params: [
749 self.probe_spacing,
750 self.base_rays as f32,
751 self.interval,
752 radiance.gi_intensity,
753 ],
754 camera: [camera_x, camera_y, viewport_w, viewport_h],
755 ambient: [
756 lighting.ambient[0],
757 lighting.ambient[1],
758 lighting.ambient[2],
759 0.0,
760 ],
761 };
762
763 gpu.queue.write_buffer(
764 &self.params_buffer,
765 0,
766 bytemuck::cast_slice(&[params]),
767 );
768
769 let bind_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
771 label: Some(&format!("radiance_ray_march_bg_{c}")),
772 layout: &self.compute_bind_group_layout,
773 entries: &[
774 wgpu::BindGroupEntry {
775 binding: 0,
776 resource: self.params_buffer.as_entire_binding(),
777 },
778 wgpu::BindGroupEntry {
779 binding: 1,
780 resource: wgpu::BindingResource::TextureView(&scene_tex.view),
781 },
782 wgpu::BindGroupEntry {
783 binding: 2,
784 resource: wgpu::BindingResource::TextureView(&cascades.view_b),
785 },
786 wgpu::BindGroupEntry {
787 binding: 3,
788 resource: wgpu::BindingResource::TextureView(&cascades.view_a),
789 },
790 ],
791 });
792
793 {
794 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
795 label: Some(&format!("radiance_ray_march_{c}")),
796 timestamp_writes: None,
797 });
798 pass.set_pipeline(&self.ray_march_pipeline);
799 pass.set_bind_group(0, &bind_group, &[]);
800 pass.dispatch_workgroups(
801 (cascades.width + 7) / 8,
802 (cascades.height + 7) / 8,
803 1,
804 );
805 }
806
807 if c < cascade_count - 1 {
809 let merge_bg = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
811 label: Some(&format!("radiance_merge_bg_{c}")),
812 layout: &self.compute_bind_group_layout,
813 entries: &[
814 wgpu::BindGroupEntry {
815 binding: 0,
816 resource: self.params_buffer.as_entire_binding(),
817 },
818 wgpu::BindGroupEntry {
819 binding: 1,
820 resource: wgpu::BindingResource::TextureView(&scene_tex.view),
821 },
822 wgpu::BindGroupEntry {
823 binding: 2,
824 resource: wgpu::BindingResource::TextureView(&cascades.view_a),
825 },
826 wgpu::BindGroupEntry {
827 binding: 3,
828 resource: wgpu::BindingResource::TextureView(&cascades.view_b),
829 },
830 ],
831 });
832
833 {
834 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
835 label: Some(&format!("radiance_merge_{c}")),
836 timestamp_writes: None,
837 });
838 pass.set_pipeline(&self.merge_pipeline);
839 pass.set_bind_group(0, &merge_bg, &[]);
840 pass.dispatch_workgroups(
841 (cascades.width + 7) / 8,
842 (cascades.height + 7) / 8,
843 1,
844 );
845 }
846
847 }
850 }
851
852 {
854 let params = RadianceParams {
855 scene_dims: [scene_w as f32, scene_h as f32, 0.0, cascade_count as f32],
856 cascade_params: [
857 self.probe_spacing,
858 self.base_rays as f32,
859 self.interval,
860 radiance.gi_intensity,
861 ],
862 camera: [camera_x, camera_y, viewport_w, viewport_h],
863 ambient: [
864 lighting.ambient[0],
865 lighting.ambient[1],
866 lighting.ambient[2],
867 0.0,
868 ],
869 };
870
871 gpu.queue.write_buffer(
872 &self.params_buffer,
873 0,
874 bytemuck::cast_slice(&[params]),
875 );
876
877 let final_cascade_view = if cascade_count > 1 {
879 &cascades.view_b
880 } else {
881 &cascades.view_a
882 };
883
884 let finalize_bg = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
885 label: Some("radiance_finalize_bg"),
886 layout: &self.compute_bind_group_layout,
887 entries: &[
888 wgpu::BindGroupEntry {
889 binding: 0,
890 resource: self.params_buffer.as_entire_binding(),
891 },
892 wgpu::BindGroupEntry {
893 binding: 1,
894 resource: wgpu::BindingResource::TextureView(&scene_tex.view),
895 },
896 wgpu::BindGroupEntry {
897 binding: 2,
898 resource: wgpu::BindingResource::TextureView(final_cascade_view),
899 },
900 wgpu::BindGroupEntry {
901 binding: 3,
902 resource: wgpu::BindingResource::TextureView(&light_tex.view),
903 },
904 ],
905 });
906
907 {
908 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
909 label: Some("radiance_finalize"),
910 timestamp_writes: None,
911 });
912 pass.set_pipeline(&self.finalize_pipeline);
913 pass.set_bind_group(0, &finalize_bg, &[]);
914 pass.dispatch_workgroups((scene_w + 7) / 8, (scene_h + 7) / 8, 1);
915 }
916 }
917
918 true
919 }
920
921 pub fn compose(
927 &self,
928 encoder: &mut wgpu::CommandEncoder,
929 target: &wgpu::TextureView,
930 ) {
931 let Some(ref light_tex) = self.light_texture else {
932 return;
933 };
934
935 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
936 label: Some("radiance_compose_pass"),
937 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
938 view: target,
939 resolve_target: None,
940 ops: wgpu::Operations {
941 load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store,
943 },
944 })],
945 depth_stencil_attachment: None,
946 timestamp_writes: None,
947 occlusion_query_set: None,
948 });
949
950 pass.set_pipeline(&self.compose_pipeline);
951 pass.set_bind_group(0, &light_tex.bind_group, &[]);
952 pass.draw(0..3, 0..1); }
954}
955
956const COMPOSE_WGSL: &str = r#"
959@group(0) @binding(0)
960var t_light: texture_2d<f32>;
961
962@group(0) @binding(1)
963var s_light: sampler;
964
965struct VertexOutput {
966 @builtin(position) position: vec4<f32>,
967 @location(0) uv: vec2<f32>,
968};
969
970@vertex
971fn vs_main(@builtin(vertex_index) idx: u32) -> VertexOutput {
972 var out: VertexOutput;
973 let uv = vec2<f32>(f32((idx << 1u) & 2u), f32(idx & 2u));
974 out.position = vec4<f32>(uv * 2.0 - 1.0, 0.0, 1.0);
975 out.uv = vec2<f32>(uv.x, 1.0 - uv.y);
976 return out;
977}
978
979@fragment
980fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
981 let light = textureSample(t_light, s_light, in.uv);
982 // Output the light color — blend state does the multiplication with dst
983 return light;
984}
985"#;
986
987#[cfg(test)]
988mod tests {
989 use super::*;
990
991 #[test]
992 fn test_radiance_params_size() {
993 assert_eq!(std::mem::size_of::<RadianceParams>(), 64);
994 }
995
996 #[test]
997 fn test_emissive_surface_clone() {
998 let em = EmissiveSurface {
999 x: 10.0,
1000 y: 20.0,
1001 width: 32.0,
1002 height: 32.0,
1003 r: 1.0,
1004 g: 0.5,
1005 b: 0.0,
1006 intensity: 2.0,
1007 };
1008 let em2 = em.clone();
1009 assert_eq!(em2.x, 10.0);
1010 assert_eq!(em2.intensity, 2.0);
1011 }
1012
1013 #[test]
1014 fn test_occluder_clone() {
1015 let occ = Occluder {
1016 x: 50.0,
1017 y: 60.0,
1018 width: 100.0,
1019 height: 20.0,
1020 };
1021 let occ2 = occ.clone();
1022 assert_eq!(occ2.width, 100.0);
1023 }
1024
1025 #[test]
1026 fn test_radiance_state_default() {
1027 let state = RadianceState::default();
1028 assert!(!state.enabled);
1029 assert!(state.emissives.is_empty());
1030 assert!(state.occluders.is_empty());
1031 assert!(state.directional_lights.is_empty());
1032 assert!(state.spot_lights.is_empty());
1033 assert_eq!(state.gi_intensity, 1.0);
1034 }
1035
1036 #[test]
1037 fn test_directional_light() {
1038 let dl = DirectionalLight {
1039 angle: 1.5,
1040 r: 1.0,
1041 g: 0.9,
1042 b: 0.7,
1043 intensity: 0.8,
1044 };
1045 assert_eq!(dl.angle, 1.5);
1046 }
1047
1048 #[test]
1049 fn test_spot_light() {
1050 let sl = SpotLight {
1051 x: 100.0,
1052 y: 200.0,
1053 angle: 0.0,
1054 spread: 0.5,
1055 range: 300.0,
1056 r: 1.0,
1057 g: 1.0,
1058 b: 0.8,
1059 intensity: 1.5,
1060 };
1061 assert_eq!(sl.range, 300.0);
1062 }
1063
1064 fn empty_lighting() -> LightingState {
1068 LightingState::default()
1069 }
1070
1071 #[test]
1072 fn test_build_scene_data_occluder_offscreen_left() {
1073 let mut radiance = RadianceState::default();
1074 radiance.occluders.push(Occluder {
1076 x: -200.0,
1077 y: 100.0,
1078 width: 50.0,
1079 height: 50.0,
1080 });
1081 let data = build_scene_data(800, 600, &radiance, &empty_lighting(), 400.0, 300.0, 800.0, 600.0);
1084 assert_eq!(data.len(), 800 * 600 * 4 * 4); }
1086
1087 #[test]
1088 fn test_build_scene_data_occluder_offscreen_above() {
1089 let mut radiance = RadianceState::default();
1090 radiance.occluders.push(Occluder {
1092 x: 100.0,
1093 y: -300.0,
1094 width: 50.0,
1095 height: 50.0,
1096 });
1097 let data = build_scene_data(800, 600, &radiance, &empty_lighting(), 400.0, 300.0, 800.0, 600.0);
1098 assert_eq!(data.len(), 800 * 600 * 4 * 4);
1099 }
1100
1101 #[test]
1102 fn test_build_scene_data_emissive_offscreen_left() {
1103 let mut radiance = RadianceState::default();
1104 radiance.emissives.push(EmissiveSurface {
1106 x: -500.0,
1107 y: 100.0,
1108 width: 100.0,
1109 height: 100.0,
1110 r: 1.0, g: 1.0, b: 1.0,
1111 intensity: 1.0,
1112 });
1113 let data = build_scene_data(800, 600, &radiance, &empty_lighting(), 400.0, 300.0, 800.0, 600.0);
1114 assert_eq!(data.len(), 800 * 600 * 4 * 4);
1115 }
1116
1117 #[test]
1118 fn test_build_scene_data_occluder_partially_onscreen() {
1119 let mut radiance = RadianceState::default();
1120 radiance.occluders.push(Occluder {
1122 x: 750.0,
1123 y: 550.0,
1124 width: 200.0,
1125 height: 200.0,
1126 });
1127 let data = build_scene_data(800, 600, &radiance, &empty_lighting(), 400.0, 300.0, 800.0, 600.0);
1130 let pixels: &[f32] = bytemuck::cast_slice(&data);
1131 let idx = (560 * 800 + 760) * 4; assert_eq!(pixels[idx + 3], 1.0);
1134 }
1135
1136 #[test]
1137 fn test_build_scene_data_occluder_far_offscreen() {
1138 let mut radiance = RadianceState::default();
1139 radiance.occluders.push(Occluder {
1141 x: -10000.0,
1142 y: -10000.0,
1143 width: 50.0,
1144 height: 50.0,
1145 });
1146 radiance.emissives.push(EmissiveSurface {
1147 x: -10000.0,
1148 y: -10000.0,
1149 width: 50.0,
1150 height: 50.0,
1151 r: 1.0, g: 1.0, b: 1.0,
1152 intensity: 5.0,
1153 });
1154 let data = build_scene_data(800, 600, &radiance, &empty_lighting(), 400.0, 300.0, 800.0, 600.0);
1156 assert_eq!(data.len(), 800 * 600 * 4 * 4);
1157 }
1158}