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(
537 &self,
538 scene_w: u32,
539 scene_h: u32,
540 radiance: &RadianceState,
541 lighting: &LightingState,
542 camera_x: f32,
543 camera_y: f32,
544 viewport_w: f32,
545 viewport_h: f32,
546 ) -> Vec<u8> {
547 build_scene_data(scene_w, scene_h, radiance, lighting, camera_x, camera_y, viewport_w, viewport_h)
548 }
549}
550
551fn build_scene_data(
554 scene_w: u32,
555 scene_h: u32,
556 radiance: &RadianceState,
557 lighting: &LightingState,
558 camera_x: f32,
559 camera_y: f32,
560 viewport_w: f32,
561 viewport_h: f32,
562) -> Vec<u8> {
563 let w = scene_w as usize;
564 let h = scene_h as usize;
565 let mut pixels = vec![0.0f32; w * h * 4];
567
568 let world_left = camera_x - viewport_w / 2.0;
570 let world_top = camera_y - viewport_h / 2.0;
571
572 for em in &radiance.emissives {
574 let px0 = ((em.x - world_left) as i32).max(0) as usize;
575 let py0 = ((em.y - world_top) as i32).max(0) as usize;
576 let px1 = ((em.x + em.width - world_left) as i32).max(0).min(w as i32) as usize;
577 let py1 = ((em.y + em.height - world_top) as i32).max(0).min(h as i32) as usize;
578
579 let er = em.r * em.intensity;
580 let eg = em.g * em.intensity;
581 let eb = em.b * em.intensity;
582
583 for py in py0..py1 {
584 for px in px0..px1 {
585 let idx = (py * w + px) * 4;
586 pixels[idx] += er;
587 pixels[idx + 1] += eg;
588 pixels[idx + 2] += eb;
589 }
590 }
591 }
592
593 for light in &lighting.lights {
595 let cx = (light.x - world_left) as i32;
596 let cy = (light.y - world_top) as i32;
597 let r_px = (light.radius * 0.1).max(2.0) as i32;
598
599 let er = light.r * light.intensity;
600 let eg = light.g * light.intensity;
601 let eb = light.b * light.intensity;
602
603 for dy in -r_px..=r_px {
604 for dx in -r_px..=r_px {
605 if dx * dx + dy * dy <= r_px * r_px {
606 let px = (cx + dx) as usize;
607 let py = (cy + dy) as usize;
608 if px < w && py < h {
609 let idx = (py * w + px) * 4;
610 pixels[idx] += er;
611 pixels[idx + 1] += eg;
612 pixels[idx + 2] += eb;
613 }
614 }
615 }
616 }
617 }
618
619 for spot in &radiance.spot_lights {
621 let cx = (spot.x - world_left) as i32;
622 let cy = (spot.y - world_top) as i32;
623 let r_px = 3i32;
624
625 let er = spot.r * spot.intensity;
626 let eg = spot.g * spot.intensity;
627 let eb = spot.b * spot.intensity;
628
629 for dy in -r_px..=r_px {
630 for dx in -r_px..=r_px {
631 if dx * dx + dy * dy <= r_px * r_px {
632 let px = (cx + dx) as usize;
633 let py = (cy + dy) as usize;
634 if px < w && py < h {
635 let idx = (py * w + px) * 4;
636 pixels[idx] += er;
637 pixels[idx + 1] += eg;
638 pixels[idx + 2] += eb;
639 }
640 }
641 }
642 }
643 }
644
645 for occ in &radiance.occluders {
647 let px0 = ((occ.x - world_left) as i32).max(0) as usize;
648 let py0 = ((occ.y - world_top) as i32).max(0) as usize;
649 let px1 = ((occ.x + occ.width - world_left) as i32).max(0).min(w as i32) as usize;
650 let py1 = ((occ.y + occ.height - world_top) as i32).max(0).min(h as i32) as usize;
651
652 for py in py0..py1 {
653 for px in px0..px1 {
654 let idx = (py * w + px) * 4;
655 pixels[idx + 3] = 1.0; }
657 }
658 }
659
660 bytemuck::cast_slice(&pixels).to_vec()
662 }
663
664impl RadiancePipeline {
665 pub fn compute(
668 &mut self,
669 gpu: &GpuContext,
670 encoder: &mut wgpu::CommandEncoder,
671 radiance: &RadianceState,
672 lighting: &LightingState,
673 camera_x: f32,
674 camera_y: f32,
675 viewport_w: f32,
676 viewport_h: f32,
677 ) -> bool {
678 if !radiance.enabled {
679 return false;
680 }
681
682 if let Some(ps) = radiance.probe_spacing {
684 self.probe_spacing = ps;
685 }
686 if let Some(iv) = radiance.interval {
687 self.interval = iv;
688 }
689 if let Some(cc) = radiance.cascade_count {
690 self.cascade_count = cc;
691 }
692
693 let scene_w = viewport_w.ceil() as u32;
695 let scene_h = viewport_h.ceil() as u32;
696 if scene_w == 0 || scene_h == 0 {
697 return false;
698 }
699
700 self.ensure_textures(gpu, scene_w, scene_h);
701
702 let scene_tex = self.scene_texture.as_ref().unwrap();
703 let cascades = self.cascade_textures.as_ref().unwrap();
704 let light_tex = self.light_texture.as_ref().unwrap();
705
706 let scene_data = self.build_scene_data(
708 scene_w, scene_h, radiance, lighting, camera_x, camera_y, viewport_w, viewport_h,
709 );
710
711 gpu.queue.write_texture(
712 wgpu::TexelCopyTextureInfo {
713 texture: &scene_tex.texture,
714 mip_level: 0,
715 origin: wgpu::Origin3d::ZERO,
716 aspect: wgpu::TextureAspect::All,
717 },
718 &scene_data,
719 wgpu::TexelCopyBufferLayout {
720 offset: 0,
721 bytes_per_row: Some(scene_w * 16), rows_per_image: Some(scene_h),
723 },
724 wgpu::Extent3d {
725 width: scene_w,
726 height: scene_h,
727 depth_or_array_layers: 1,
728 },
729 );
730
731 let cascade_count = self.cascade_count.min(MAX_CASCADES as u32);
732
733 for c in (0..cascade_count).rev() {
737 let params = RadianceParams {
738 scene_dims: [scene_w as f32, scene_h as f32, c as f32, cascade_count as f32],
739 cascade_params: [
740 self.probe_spacing,
741 self.base_rays as f32,
742 self.interval,
743 radiance.gi_intensity,
744 ],
745 camera: [camera_x, camera_y, viewport_w, viewport_h],
746 ambient: [
747 lighting.ambient[0],
748 lighting.ambient[1],
749 lighting.ambient[2],
750 0.0,
751 ],
752 };
753
754 gpu.queue.write_buffer(
755 &self.params_buffer,
756 0,
757 bytemuck::cast_slice(&[params]),
758 );
759
760 let bind_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
762 label: Some(&format!("radiance_ray_march_bg_{c}")),
763 layout: &self.compute_bind_group_layout,
764 entries: &[
765 wgpu::BindGroupEntry {
766 binding: 0,
767 resource: self.params_buffer.as_entire_binding(),
768 },
769 wgpu::BindGroupEntry {
770 binding: 1,
771 resource: wgpu::BindingResource::TextureView(&scene_tex.view),
772 },
773 wgpu::BindGroupEntry {
774 binding: 2,
775 resource: wgpu::BindingResource::TextureView(&cascades.view_b),
776 },
777 wgpu::BindGroupEntry {
778 binding: 3,
779 resource: wgpu::BindingResource::TextureView(&cascades.view_a),
780 },
781 ],
782 });
783
784 {
785 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
786 label: Some(&format!("radiance_ray_march_{c}")),
787 timestamp_writes: None,
788 });
789 pass.set_pipeline(&self.ray_march_pipeline);
790 pass.set_bind_group(0, &bind_group, &[]);
791 pass.dispatch_workgroups(
792 (cascades.width + 7) / 8,
793 (cascades.height + 7) / 8,
794 1,
795 );
796 }
797
798 if c < cascade_count - 1 {
800 let merge_bg = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
802 label: Some(&format!("radiance_merge_bg_{c}")),
803 layout: &self.compute_bind_group_layout,
804 entries: &[
805 wgpu::BindGroupEntry {
806 binding: 0,
807 resource: self.params_buffer.as_entire_binding(),
808 },
809 wgpu::BindGroupEntry {
810 binding: 1,
811 resource: wgpu::BindingResource::TextureView(&scene_tex.view),
812 },
813 wgpu::BindGroupEntry {
814 binding: 2,
815 resource: wgpu::BindingResource::TextureView(&cascades.view_a),
816 },
817 wgpu::BindGroupEntry {
818 binding: 3,
819 resource: wgpu::BindingResource::TextureView(&cascades.view_b),
820 },
821 ],
822 });
823
824 {
825 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
826 label: Some(&format!("radiance_merge_{c}")),
827 timestamp_writes: None,
828 });
829 pass.set_pipeline(&self.merge_pipeline);
830 pass.set_bind_group(0, &merge_bg, &[]);
831 pass.dispatch_workgroups(
832 (cascades.width + 7) / 8,
833 (cascades.height + 7) / 8,
834 1,
835 );
836 }
837
838 }
841 }
842
843 {
845 let params = RadianceParams {
846 scene_dims: [scene_w as f32, scene_h as f32, 0.0, cascade_count as f32],
847 cascade_params: [
848 self.probe_spacing,
849 self.base_rays as f32,
850 self.interval,
851 radiance.gi_intensity,
852 ],
853 camera: [camera_x, camera_y, viewport_w, viewport_h],
854 ambient: [
855 lighting.ambient[0],
856 lighting.ambient[1],
857 lighting.ambient[2],
858 0.0,
859 ],
860 };
861
862 gpu.queue.write_buffer(
863 &self.params_buffer,
864 0,
865 bytemuck::cast_slice(&[params]),
866 );
867
868 let final_cascade_view = if cascade_count > 1 {
870 &cascades.view_b
871 } else {
872 &cascades.view_a
873 };
874
875 let finalize_bg = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
876 label: Some("radiance_finalize_bg"),
877 layout: &self.compute_bind_group_layout,
878 entries: &[
879 wgpu::BindGroupEntry {
880 binding: 0,
881 resource: self.params_buffer.as_entire_binding(),
882 },
883 wgpu::BindGroupEntry {
884 binding: 1,
885 resource: wgpu::BindingResource::TextureView(&scene_tex.view),
886 },
887 wgpu::BindGroupEntry {
888 binding: 2,
889 resource: wgpu::BindingResource::TextureView(final_cascade_view),
890 },
891 wgpu::BindGroupEntry {
892 binding: 3,
893 resource: wgpu::BindingResource::TextureView(&light_tex.view),
894 },
895 ],
896 });
897
898 {
899 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
900 label: Some("radiance_finalize"),
901 timestamp_writes: None,
902 });
903 pass.set_pipeline(&self.finalize_pipeline);
904 pass.set_bind_group(0, &finalize_bg, &[]);
905 pass.dispatch_workgroups((scene_w + 7) / 8, (scene_h + 7) / 8, 1);
906 }
907 }
908
909 true
910 }
911
912 pub fn compose(
918 &self,
919 encoder: &mut wgpu::CommandEncoder,
920 target: &wgpu::TextureView,
921 ) {
922 let Some(ref light_tex) = self.light_texture else {
923 return;
924 };
925
926 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
927 label: Some("radiance_compose_pass"),
928 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
929 view: target,
930 resolve_target: None,
931 ops: wgpu::Operations {
932 load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store,
934 },
935 })],
936 depth_stencil_attachment: None,
937 timestamp_writes: None,
938 occlusion_query_set: None,
939 });
940
941 pass.set_pipeline(&self.compose_pipeline);
942 pass.set_bind_group(0, &light_tex.bind_group, &[]);
943 pass.draw(0..3, 0..1); }
945}
946
947const COMPOSE_WGSL: &str = r#"
950@group(0) @binding(0)
951var t_light: texture_2d<f32>;
952
953@group(0) @binding(1)
954var s_light: sampler;
955
956struct VertexOutput {
957 @builtin(position) position: vec4<f32>,
958 @location(0) uv: vec2<f32>,
959};
960
961@vertex
962fn vs_main(@builtin(vertex_index) idx: u32) -> VertexOutput {
963 var out: VertexOutput;
964 let uv = vec2<f32>(f32((idx << 1u) & 2u), f32(idx & 2u));
965 out.position = vec4<f32>(uv * 2.0 - 1.0, 0.0, 1.0);
966 out.uv = vec2<f32>(uv.x, 1.0 - uv.y);
967 return out;
968}
969
970@fragment
971fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
972 let light = textureSample(t_light, s_light, in.uv);
973 // Output the light color — blend state does the multiplication with dst
974 return light;
975}
976"#;
977
978#[cfg(test)]
979mod tests {
980 use super::*;
981
982 #[test]
983 fn test_radiance_params_size() {
984 assert_eq!(std::mem::size_of::<RadianceParams>(), 64);
985 }
986
987 #[test]
988 fn test_emissive_surface_clone() {
989 let em = EmissiveSurface {
990 x: 10.0,
991 y: 20.0,
992 width: 32.0,
993 height: 32.0,
994 r: 1.0,
995 g: 0.5,
996 b: 0.0,
997 intensity: 2.0,
998 };
999 let em2 = em.clone();
1000 assert_eq!(em2.x, 10.0);
1001 assert_eq!(em2.intensity, 2.0);
1002 }
1003
1004 #[test]
1005 fn test_occluder_clone() {
1006 let occ = Occluder {
1007 x: 50.0,
1008 y: 60.0,
1009 width: 100.0,
1010 height: 20.0,
1011 };
1012 let occ2 = occ.clone();
1013 assert_eq!(occ2.width, 100.0);
1014 }
1015
1016 #[test]
1017 fn test_radiance_state_default() {
1018 let state = RadianceState::default();
1019 assert!(!state.enabled);
1020 assert!(state.emissives.is_empty());
1021 assert!(state.occluders.is_empty());
1022 assert!(state.directional_lights.is_empty());
1023 assert!(state.spot_lights.is_empty());
1024 assert_eq!(state.gi_intensity, 1.0);
1025 }
1026
1027 #[test]
1028 fn test_directional_light() {
1029 let dl = DirectionalLight {
1030 angle: 1.5,
1031 r: 1.0,
1032 g: 0.9,
1033 b: 0.7,
1034 intensity: 0.8,
1035 };
1036 assert_eq!(dl.angle, 1.5);
1037 }
1038
1039 #[test]
1040 fn test_spot_light() {
1041 let sl = SpotLight {
1042 x: 100.0,
1043 y: 200.0,
1044 angle: 0.0,
1045 spread: 0.5,
1046 range: 300.0,
1047 r: 1.0,
1048 g: 1.0,
1049 b: 0.8,
1050 intensity: 1.5,
1051 };
1052 assert_eq!(sl.range, 300.0);
1053 }
1054
1055 fn empty_lighting() -> LightingState {
1060 LightingState::default()
1061 }
1062
1063 #[test]
1064 fn test_build_scene_data_occluder_offscreen_left() {
1065 let mut radiance = RadianceState::default();
1066 radiance.occluders.push(Occluder {
1068 x: -200.0,
1069 y: 100.0,
1070 width: 50.0,
1071 height: 50.0,
1072 });
1073 let data = build_scene_data(800, 600, &radiance, &empty_lighting(), 400.0, 300.0, 800.0, 600.0);
1076 assert_eq!(data.len(), 800 * 600 * 4 * 4); }
1078
1079 #[test]
1080 fn test_build_scene_data_occluder_offscreen_above() {
1081 let mut radiance = RadianceState::default();
1082 radiance.occluders.push(Occluder {
1084 x: 100.0,
1085 y: -300.0,
1086 width: 50.0,
1087 height: 50.0,
1088 });
1089 let data = build_scene_data(800, 600, &radiance, &empty_lighting(), 400.0, 300.0, 800.0, 600.0);
1090 assert_eq!(data.len(), 800 * 600 * 4 * 4);
1091 }
1092
1093 #[test]
1094 fn test_build_scene_data_emissive_offscreen_left() {
1095 let mut radiance = RadianceState::default();
1096 radiance.emissives.push(EmissiveSurface {
1098 x: -500.0,
1099 y: 100.0,
1100 width: 100.0,
1101 height: 100.0,
1102 r: 1.0, g: 1.0, b: 1.0,
1103 intensity: 1.0,
1104 });
1105 let data = build_scene_data(800, 600, &radiance, &empty_lighting(), 400.0, 300.0, 800.0, 600.0);
1106 assert_eq!(data.len(), 800 * 600 * 4 * 4);
1107 }
1108
1109 #[test]
1110 fn test_build_scene_data_occluder_partially_onscreen() {
1111 let mut radiance = RadianceState::default();
1112 radiance.occluders.push(Occluder {
1114 x: 750.0,
1115 y: 550.0,
1116 width: 200.0,
1117 height: 200.0,
1118 });
1119 let data = build_scene_data(800, 600, &radiance, &empty_lighting(), 400.0, 300.0, 800.0, 600.0);
1122 let pixels: &[f32] = bytemuck::cast_slice(&data);
1123 let idx = (560 * 800 + 760) * 4; assert_eq!(pixels[idx + 3], 1.0);
1126 }
1127
1128 #[test]
1129 fn test_build_scene_data_occluder_far_offscreen() {
1130 let mut radiance = RadianceState::default();
1131 radiance.occluders.push(Occluder {
1133 x: -10000.0,
1134 y: -10000.0,
1135 width: 50.0,
1136 height: 50.0,
1137 });
1138 radiance.emissives.push(EmissiveSurface {
1139 x: -10000.0,
1140 y: -10000.0,
1141 width: 50.0,
1142 height: 50.0,
1143 r: 1.0, g: 1.0, b: 1.0,
1144 intensity: 5.0,
1145 });
1146 let data = build_scene_data(800, 600, &radiance, &empty_lighting(), 400.0, 300.0, 800.0, 600.0);
1148 assert_eq!(data.len(), 800 * 600 * 4 * 4);
1149 }
1150}