1use bevy_app::{App, Plugin, Update};
15use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer, Assets, RenderAssetUsages};
16use bevy_core_pipeline::mip_generation::{
17 self, DownsamplePass, DownsamplePipeline, DownsamplePipelineKey, DownsamplingConstants,
18};
19use bevy_ecs::{
20 component::Component,
21 entity::Entity,
22 query::Without,
23 resource::Resource,
24 schedule::IntoScheduleConfigs,
25 system::{Commands, Query, Res, ResMut},
26};
27use bevy_image::Image;
28use bevy_math::{Quat, UVec2, Vec2};
29use bevy_render::{
30 diagnostic::RecordDiagnostics,
31 init_gpu_resource,
32 render_asset::RenderAssets,
33 render_resource::{
34 binding_types::*, AddressMode, BindGroup, BindGroupEntries, BindGroupLayoutDescriptor,
35 BindGroupLayoutEntries, CachedComputePipelineId, ComputePassDescriptor,
36 ComputePipelineDescriptor, DownlevelFlags, Extent3d, FilterMode, MipmapFilterMode,
37 PipelineCache, Sampler, SamplerBindingType, SamplerDescriptor, ShaderStages, ShaderType,
38 SpecializedComputePipelines, StorageTextureAccess, Texture, TextureAspect,
39 TextureDescriptor, TextureDimension, TextureFormat, TextureSampleType, TextureUsages,
40 TextureView, TextureViewDescriptor, TextureViewDimension, UniformBuffer,
41 },
42 renderer::{RenderAdapter, RenderContext, RenderDevice, RenderQueue},
43 sync_component::{SyncComponent, SyncComponentPlugin},
44 sync_world::RenderEntity,
45 texture::{CachedTexture, GpuImage, TextureCache},
46 Extract, ExtractSchedule, Render, RenderApp, RenderStartup, RenderSystems,
47};
48
49use bevy_light::{EnvironmentMapLight, GeneratedEnvironmentMapLight};
64use bevy_shader::ShaderDefVal;
65use core::cmp::min;
66use tracing::info;
67
68use crate::Bluenoise;
69
70#[derive(Resource)]
72pub struct GeneratorBindGroupLayouts {
73 pub downsampling_first: BindGroupLayoutDescriptor,
74 pub downsampling_second: BindGroupLayoutDescriptor,
75 pub radiance: BindGroupLayoutDescriptor,
76 pub irradiance: BindGroupLayoutDescriptor,
77 pub copy: BindGroupLayoutDescriptor,
78}
79
80#[derive(Resource)]
82pub struct GeneratorSamplers {
83 pub linear: Sampler,
84}
85
86#[derive(Resource)]
88pub struct GeneratorPipelines {
89 pub downsample_first: CachedComputePipelineId,
90 pub downsample_second: CachedComputePipelineId,
91 pub copy: CachedComputePipelineId,
92 pub radiance: CachedComputePipelineId,
93 pub irradiance: CachedComputePipelineId,
94}
95
96#[derive(Resource, Clone, Copy, Debug, PartialEq, Eq)]
98pub struct DownsamplingConfig {
99 pub combine_bind_group: bool,
101}
102
103pub struct EnvironmentMapGenerationPlugin;
104
105impl Plugin for EnvironmentMapGenerationPlugin {
106 fn build(&self, _: &mut App) {}
107 fn finish(&self, app: &mut App) {
108 if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
109 let adapter = render_app.world().resource::<RenderAdapter>();
110 let device = render_app.world().resource::<RenderDevice>();
111
112 let limit_support = device.limits().max_storage_textures_per_shader_stage >= 6
114 && device.limits().max_compute_workgroup_storage_size != 0
115 && device.limits().max_compute_workgroup_size_x != 0;
116
117 let downlevel_support = adapter
118 .get_downlevel_capabilities()
119 .flags
120 .contains(DownlevelFlags::COMPUTE_SHADERS);
121
122 if !limit_support || !downlevel_support {
123 info!("Disabling EnvironmentMapGenerationPlugin because compute is not supported on this platform. This is safe to ignore if you are not using EnvironmentMapGenerationPlugin.");
124 return;
125 }
126 } else {
127 return;
128 }
129
130 embedded_asset!(app, "environment_filter.wesl");
131 embedded_asset!(app, "copy.wesl");
132
133 app.add_plugins(SyncComponentPlugin::<GeneratedEnvironmentMapLight, Self>::default())
134 .add_systems(Update, generate_environment_map_light);
135
136 let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
137 return;
138 };
139
140 render_app
141 .add_systems(
142 ExtractSchedule,
143 extract_generated_environment_map_entities.after(generate_environment_map_light),
144 )
145 .add_systems(
146 Render,
147 (
148 prepare_generated_environment_map_bind_groups
149 .in_set(RenderSystems::PrepareBindGroups),
150 prepare_generated_environment_map_intermediate_textures
151 .in_set(RenderSystems::PrepareResources),
152 (downsampling_system, filtering_system)
153 .chain()
154 .after(RenderSystems::PrepareBindGroups)
155 .before(RenderSystems::Render),
156 ),
157 )
158 .add_systems(
159 RenderStartup,
160 initialize_generated_environment_map_resources
161 .after(init_gpu_resource::<DownsamplePipeline>)
162 .after(init_gpu_resource::<SpecializedComputePipelines<DownsamplePipeline>>),
163 );
164 }
165}
166
167pub fn initialize_generated_environment_map_resources(
170 mut commands: Commands,
171 render_device: Res<RenderDevice>,
172 render_adapter: Res<RenderAdapter>,
173 pipeline_cache: Res<PipelineCache>,
174 asset_server: Res<AssetServer>,
175 downsample_pipeline: Res<DownsamplePipeline>,
176 mut specialized_downsample_pipelines: ResMut<SpecializedComputePipelines<DownsamplePipeline>>,
177) {
178 let combine_bind_group =
180 mip_generation::can_combine_downsampling_bind_groups(&render_adapter, &render_device);
181
182 let downsample_key_first = DownsamplePipelineKey {
183 texture_format: TextureFormat::Rgba16Float,
184 array_texture: true, combine_bind_groups: combine_bind_group,
186 pass: DownsamplePass::First,
187 };
188 let downsample_key_second = DownsamplePipelineKey {
189 pass: DownsamplePass::Second,
190 ..downsample_key_first
191 };
192
193 let radiance = BindGroupLayoutDescriptor::new(
195 "radiance_bind_group_layout",
196 &BindGroupLayoutEntries::sequential(
197 ShaderStages::COMPUTE,
198 (
199 texture_2d_array(TextureSampleType::Float { filterable: true }),
201 sampler(SamplerBindingType::Filtering), texture_storage_2d_array(
204 TextureFormat::Rgba16Float,
205 StorageTextureAccess::WriteOnly,
206 ),
207 uniform_buffer::<FilteringConstants>(false), texture_2d_array(TextureSampleType::Float { filterable: true }), ),
210 ),
211 );
212
213 let irradiance = BindGroupLayoutDescriptor::new(
214 "irradiance_bind_group_layout",
215 &BindGroupLayoutEntries::sequential(
216 ShaderStages::COMPUTE,
217 (
218 texture_2d_array(TextureSampleType::Float { filterable: true }),
220 sampler(SamplerBindingType::Filtering), texture_storage_2d_array(
223 TextureFormat::Rgba16Float,
224 StorageTextureAccess::WriteOnly,
225 ),
226 uniform_buffer::<FilteringConstants>(false), texture_2d_array(TextureSampleType::Float { filterable: true }), ),
229 ),
230 );
231
232 let copy = BindGroupLayoutDescriptor::new(
233 "copy_bind_group_layout",
234 &BindGroupLayoutEntries::sequential(
235 ShaderStages::COMPUTE,
236 (
237 texture_2d_array(TextureSampleType::Float { filterable: true }),
239 texture_storage_2d_array(
241 TextureFormat::Rgba16Float,
242 StorageTextureAccess::WriteOnly,
243 ),
244 ),
245 ),
246 );
247
248 let layouts = GeneratorBindGroupLayouts {
249 downsampling_first: downsample_key_first.bind_group_layout(),
250 downsampling_second: downsample_key_second.bind_group_layout(),
251 radiance,
252 irradiance,
253 copy,
254 };
255
256 let linear = render_device.create_sampler(&SamplerDescriptor {
258 label: Some("generator_linear_sampler"),
259 address_mode_u: AddressMode::ClampToEdge,
260 address_mode_v: AddressMode::ClampToEdge,
261 address_mode_w: AddressMode::ClampToEdge,
262 mag_filter: FilterMode::Linear,
263 min_filter: FilterMode::Linear,
264 mipmap_filter: MipmapFilterMode::Linear,
265 ..Default::default()
266 });
267
268 let samplers = GeneratorSamplers { linear };
269
270 let shader_defs: Vec<ShaderDefVal> = if cfg!(feature = "bluenoise_texture") {
272 vec![ShaderDefVal::Int("HAS_BLUE_NOISE".into(), 1)]
273 } else {
274 vec![]
275 };
276
277 let env_filter_shader = load_embedded_asset!(asset_server.as_ref(), "environment_filter.wesl");
278 let copy_shader = load_embedded_asset!(asset_server.as_ref(), "copy.wesl");
279
280 let radiance = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
282 label: Some("radiance_pipeline".into()),
283 layout: vec![layouts.radiance.clone()],
284 immediate_size: 0,
285 shader: env_filter_shader.clone(),
286 shader_defs: shader_defs.clone(),
287 entry_point: Some("generate_radiance_map".into()),
288 zero_initialize_workgroup_memory: false,
289 constants: vec![],
290 });
291
292 let irradiance = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
294 label: Some("irradiance_pipeline".into()),
295 layout: vec![layouts.irradiance.clone()],
296 immediate_size: 0,
297 shader: env_filter_shader,
298 shader_defs: shader_defs.clone(),
299 entry_point: Some("generate_irradiance_map".into()),
300 zero_initialize_workgroup_memory: false,
301 constants: vec![],
302 });
303
304 let copy_pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
306 label: Some("copy_pipeline".into()),
307 layout: vec![layouts.copy.clone()],
308 immediate_size: 0,
309 shader: copy_shader,
310 shader_defs: vec![],
311 entry_point: Some("copy".into()),
312 zero_initialize_workgroup_memory: false,
313 constants: vec![],
314 });
315
316 let pipelines = GeneratorPipelines {
317 downsample_first: specialized_downsample_pipelines.specialize(
318 &pipeline_cache,
319 &downsample_pipeline,
320 downsample_key_first,
321 ),
322 downsample_second: specialized_downsample_pipelines.specialize(
323 &pipeline_cache,
324 &downsample_pipeline,
325 downsample_key_second,
326 ),
327 radiance,
328 irradiance,
329 copy: copy_pipeline,
330 };
331
332 commands.insert_resource(layouts);
334 commands.insert_resource(samplers);
335 commands.insert_resource(pipelines);
336 commands.insert_resource(DownsamplingConfig { combine_bind_group });
337}
338
339pub fn extract_generated_environment_map_entities(
340 query: Extract<
341 Query<(
342 RenderEntity,
343 &GeneratedEnvironmentMapLight,
344 &EnvironmentMapLight,
345 )>,
346 >,
347 mut commands: Commands,
348 render_images: Res<RenderAssets<GpuImage>>,
349) {
350 for (entity, filtered_env_map, env_map_light) in query.iter() {
351 let Some(env_map) = render_images.get(&filtered_env_map.environment_map) else {
352 continue;
353 };
354
355 let diffuse_map = render_images.get(&env_map_light.diffuse_map);
356 let specular_map = render_images.get(&env_map_light.specular_map);
357
358 if diffuse_map.is_none() || specular_map.is_none() {
360 continue;
361 }
362
363 let diffuse_map = diffuse_map.unwrap();
364 let specular_map = specular_map.unwrap();
365
366 let render_filtered_env_map = RenderEnvironmentMap {
367 environment_map: env_map.clone(),
368 diffuse_map: diffuse_map.clone(),
369 specular_map: specular_map.clone(),
370 intensity: filtered_env_map.intensity,
371 rotation: filtered_env_map.rotation,
372 affects_lightmapped_mesh_diffuse: filtered_env_map.affects_lightmapped_mesh_diffuse,
373 };
374 commands
375 .get_entity(entity)
376 .expect("Entity not synced to render world")
377 .insert(render_filtered_env_map);
378 }
379}
380
381#[derive(Component, Clone)]
383pub struct RenderEnvironmentMap {
384 pub environment_map: GpuImage,
385 pub diffuse_map: GpuImage,
386 pub specular_map: GpuImage,
387 pub intensity: f32,
388 pub rotation: Quat,
389 pub affects_lightmapped_mesh_diffuse: bool,
390}
391
392#[derive(Component)]
393pub struct IntermediateTextures {
394 pub environment_map: CachedTexture,
395}
396
397#[inline]
400fn compute_mip_count(size: u32) -> u32 {
401 debug_assert!(size.is_power_of_two());
402 32 - size.leading_zeros()
403}
404
405pub fn prepare_generated_environment_map_intermediate_textures(
407 light_probes: Query<(Entity, &RenderEnvironmentMap)>,
408 render_device: Res<RenderDevice>,
409 mut texture_cache: ResMut<TextureCache>,
410 mut commands: Commands,
411) {
412 for (entity, env_map_light) in &light_probes {
413 let base_size = env_map_light.environment_map.texture_descriptor.size.width;
414 let mip_level_count = compute_mip_count(base_size);
415
416 let environment_map = texture_cache.get(
417 &render_device,
418 TextureDescriptor {
419 label: Some("intermediate_environment_map"),
420 size: Extent3d {
421 width: base_size,
422 height: base_size,
423 depth_or_array_layers: 6, },
425 mip_level_count,
426 sample_count: 1,
427 dimension: TextureDimension::D2,
428 format: TextureFormat::Rgba16Float,
429 usage: TextureUsages::TEXTURE_BINDING
430 | TextureUsages::STORAGE_BINDING
431 | TextureUsages::COPY_DST,
432 view_formats: &[],
433 },
434 );
435
436 commands
437 .entity(entity)
438 .insert(IntermediateTextures { environment_map });
439 }
440}
441
442#[derive(Clone, Copy, ShaderType)]
444#[repr(C)]
445pub struct FilteringConstants {
446 mip_level: f32,
447 sample_count: u32,
448 roughness: f32,
449 noise_size_bits: UVec2,
450}
451
452#[derive(Component)]
454pub struct GeneratorBindGroups {
455 pub downsampling_first: BindGroup,
456 pub downsampling_second: BindGroup,
457 pub radiance: Vec<BindGroup>, pub irradiance: BindGroup,
459 pub copy: BindGroup,
460}
461
462pub fn prepare_generated_environment_map_bind_groups(
464 light_probes: Query<(Entity, &IntermediateTextures, &RenderEnvironmentMap)>,
465 render_device: Res<RenderDevice>,
466 pipeline_cache: Res<PipelineCache>,
467 queue: Res<RenderQueue>,
468 layouts: Res<GeneratorBindGroupLayouts>,
469 samplers: Res<GeneratorSamplers>,
470 render_images: Res<RenderAssets<GpuImage>>,
471 bluenoise: Res<Bluenoise>,
472 config: Res<DownsamplingConfig>,
473 mut commands: Commands,
474) {
475 let Some(stbn_texture) = render_images.get(&bluenoise.texture) else {
478 return;
479 };
480
481 assert!(stbn_texture.texture_descriptor.size.width.is_power_of_two());
482 assert!(stbn_texture
483 .texture_descriptor
484 .size
485 .height
486 .is_power_of_two());
487 let noise_size_bits = UVec2::new(
488 stbn_texture.texture_descriptor.size.width.trailing_zeros(),
489 stbn_texture.texture_descriptor.size.height.trailing_zeros(),
490 );
491
492 for (entity, textures, env_map_light) in &light_probes {
493 let base_size = env_map_light.environment_map.texture_descriptor.size.width;
495 let mip_count = compute_mip_count(base_size);
496 let last_mip = mip_count - 1;
497 let env_map_texture = env_map_light.environment_map.texture.clone();
498
499 let downsampling_constants = DownsamplingConstants {
501 mips: mip_count - 1, inverse_input_size: Vec2::new(1.0 / base_size as f32, 1.0 / base_size as f32),
503 _padding: 0,
504 };
505
506 let mut downsampling_constants_buffer = UniformBuffer::from(downsampling_constants);
507 downsampling_constants_buffer.write_buffer(&render_device, &queue);
508
509 let input_env_map_first = env_map_texture.clone().create_view(&TextureViewDescriptor {
510 dimension: Some(TextureViewDimension::D2Array),
511 ..Default::default()
512 });
513
514 let mip_storage = |level: u32| {
516 if level <= last_mip {
517 create_storage_view(&textures.environment_map.texture, level, &render_device)
518 } else {
519 create_placeholder_storage_view(&render_device)
521 }
522 };
523
524 let (downsampling_first_bind_group, downsampling_second_bind_group) =
526 if config.combine_bind_group {
527 let bind_group = render_device.create_bind_group(
529 "downsampling_bind_group_combined_first",
530 &pipeline_cache.get_bind_group_layout(&layouts.downsampling_first),
531 &BindGroupEntries::sequential((
532 &samplers.linear,
533 &downsampling_constants_buffer,
534 &input_env_map_first,
535 &mip_storage(1),
536 &mip_storage(2),
537 &mip_storage(3),
538 &mip_storage(4),
539 &mip_storage(5),
540 &mip_storage(6),
541 &mip_storage(7),
542 &mip_storage(8),
543 &mip_storage(9),
544 &mip_storage(10),
545 &mip_storage(11),
546 &mip_storage(12),
547 )),
548 );
549
550 (bind_group.clone(), bind_group)
551 } else {
552 let input_env_map_second =
554 textures
555 .environment_map
556 .texture
557 .create_view(&TextureViewDescriptor {
558 dimension: Some(TextureViewDimension::D2Array),
559 base_mip_level: min(6, last_mip),
560 mip_level_count: Some(1),
561 ..Default::default()
562 });
563
564 let first = render_device.create_bind_group(
566 "downsampling_first_bind_group",
567 &pipeline_cache.get_bind_group_layout(&layouts.downsampling_first),
568 &BindGroupEntries::sequential((
569 &samplers.linear,
570 &downsampling_constants_buffer,
571 &input_env_map_first,
572 &mip_storage(1),
573 &mip_storage(2),
574 &mip_storage(3),
575 &mip_storage(4),
576 &mip_storage(5),
577 &mip_storage(6),
578 )),
579 );
580
581 let second = render_device.create_bind_group(
582 "downsampling_second_bind_group",
583 &pipeline_cache.get_bind_group_layout(&layouts.downsampling_second),
584 &BindGroupEntries::sequential((
585 &samplers.linear,
586 &downsampling_constants_buffer,
587 &input_env_map_second,
588 &mip_storage(7),
589 &mip_storage(8),
590 &mip_storage(9),
591 &mip_storage(10),
592 &mip_storage(11),
593 &mip_storage(12),
594 )),
595 );
596
597 (first, second)
598 };
599
600 let stbn_texture_view = stbn_texture
602 .texture
603 .clone()
604 .create_view(&TextureViewDescriptor {
605 dimension: Some(TextureViewDimension::D2Array),
606 ..Default::default()
607 });
608
609 let num_mips = mip_count as usize;
611 let mut radiance_bind_groups = Vec::with_capacity(num_mips);
612
613 for mip in 0..num_mips {
614 let roughness = mip as f32 / (num_mips - 1) as f32;
617 let sample_count = 32u32 * 2u32.pow((roughness * 4.0) as u32);
618
619 let radiance_constants = FilteringConstants {
620 mip_level: mip as f32,
621 sample_count,
622 roughness,
623 noise_size_bits,
624 };
625
626 let mut radiance_constants_buffer = UniformBuffer::from(radiance_constants);
627 radiance_constants_buffer.write_buffer(&render_device, &queue);
628
629 let mip_storage_view = create_storage_view(
630 &env_map_light.specular_map.texture,
631 mip as u32,
632 &render_device,
633 );
634 let bind_group = render_device.create_bind_group(
635 Some(format!("radiance_bind_group_mip_{mip}").as_str()),
636 &pipeline_cache.get_bind_group_layout(&layouts.radiance),
637 &BindGroupEntries::sequential((
638 &textures.environment_map.default_view,
639 &samplers.linear,
640 &mip_storage_view,
641 &radiance_constants_buffer,
642 &stbn_texture_view,
643 )),
644 );
645
646 radiance_bind_groups.push(bind_group);
647 }
648
649 let irradiance_constants = FilteringConstants {
651 mip_level: 0.0,
652 sample_count: 1024,
654 roughness: 1.0,
655 noise_size_bits,
656 };
657
658 let mut irradiance_constants_buffer = UniformBuffer::from(irradiance_constants);
659 irradiance_constants_buffer.write_buffer(&render_device, &queue);
660
661 let irradiance_map =
663 env_map_light
664 .diffuse_map
665 .texture
666 .create_view(&TextureViewDescriptor {
667 dimension: Some(TextureViewDimension::D2Array),
668 ..Default::default()
669 });
670
671 let irradiance_bind_group = render_device.create_bind_group(
672 "irradiance_bind_group",
673 &pipeline_cache.get_bind_group_layout(&layouts.irradiance),
674 &BindGroupEntries::sequential((
675 &textures.environment_map.default_view,
676 &samplers.linear,
677 &irradiance_map,
678 &irradiance_constants_buffer,
679 &stbn_texture_view,
680 )),
681 );
682
683 let src_view = env_map_light
685 .environment_map
686 .texture
687 .create_view(&TextureViewDescriptor {
688 dimension: Some(TextureViewDimension::D2Array),
689 ..Default::default()
690 });
691
692 let dst_view = create_storage_view(&textures.environment_map.texture, 0, &render_device);
693
694 let copy_bind_group = render_device.create_bind_group(
695 "copy_bind_group",
696 &pipeline_cache.get_bind_group_layout(&layouts.copy),
697 &BindGroupEntries::with_indices(((0, &src_view), (1, &dst_view))),
698 );
699
700 commands.entity(entity).insert(GeneratorBindGroups {
701 downsampling_first: downsampling_first_bind_group,
702 downsampling_second: downsampling_second_bind_group,
703 radiance: radiance_bind_groups,
704 irradiance: irradiance_bind_group,
705 copy: copy_bind_group,
706 });
707 }
708}
709
710fn create_storage_view(texture: &Texture, mip: u32, _render_device: &RenderDevice) -> TextureView {
712 texture.create_view(&TextureViewDescriptor {
713 label: Some(format!("storage_view_mip_{mip}").as_str()),
714 format: Some(texture.format()),
715 dimension: Some(TextureViewDimension::D2Array),
716 aspect: TextureAspect::All,
717 base_mip_level: mip,
718 mip_level_count: Some(1),
719 base_array_layer: 0,
720 array_layer_count: Some(texture.depth_or_array_layers()),
721 usage: Some(TextureUsages::STORAGE_BINDING),
722 })
723}
724
725fn create_placeholder_storage_view(render_device: &RenderDevice) -> TextureView {
728 let tex = render_device.create_texture(&TextureDescriptor {
729 label: Some("lightprobe_placeholder"),
730 size: Extent3d {
731 width: 1,
732 height: 1,
733 depth_or_array_layers: 6,
734 },
735 mip_level_count: 1,
736 sample_count: 1,
737 dimension: TextureDimension::D2,
738 format: TextureFormat::Rgba16Float,
739 usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING,
740 view_formats: &[],
741 });
742
743 tex.create_view(&TextureViewDescriptor::default())
744}
745
746pub fn downsampling_system(
747 query: Query<(&GeneratorBindGroups, &RenderEnvironmentMap)>,
748 pipeline_cache: Res<PipelineCache>,
749 pipelines: Option<Res<GeneratorPipelines>>,
750 mut ctx: RenderContext,
751) {
752 let Some(pipelines) = pipelines else {
753 return;
754 };
755
756 let Some(downsample_first_pipeline) =
757 pipeline_cache.get_compute_pipeline(pipelines.downsample_first)
758 else {
759 return;
760 };
761
762 let Some(downsample_second_pipeline) =
763 pipeline_cache.get_compute_pipeline(pipelines.downsample_second)
764 else {
765 return;
766 };
767
768 let Some(copy_pipeline) = pipeline_cache.get_compute_pipeline(pipelines.copy) else {
769 return;
770 };
771
772 let diagnostics = ctx.diagnostic_recorder();
773 let diagnostics = diagnostics.as_deref();
774
775 for (bind_groups, env_map_light) in &query {
776 {
778 let mut compute_pass =
779 ctx.command_encoder()
780 .begin_compute_pass(&ComputePassDescriptor {
781 label: Some("lightprobe_copy"),
782 timestamp_writes: None,
783 });
784
785 let pass_span = diagnostics.pass_span(&mut compute_pass, "lightprobe_copy");
786
787 compute_pass.set_pipeline(copy_pipeline);
788 compute_pass.set_bind_group(0, &bind_groups.copy, &[]);
789
790 let tex_size = env_map_light.environment_map.texture_descriptor.size;
791 let wg_x = tex_size.width.div_ceil(8);
792 let wg_y = tex_size.height.div_ceil(8);
793 compute_pass.dispatch_workgroups(wg_x, wg_y, 6);
794
795 pass_span.end(&mut compute_pass);
796 }
797
798 {
800 let mut compute_pass =
801 ctx.command_encoder()
802 .begin_compute_pass(&ComputePassDescriptor {
803 label: Some("lightprobe_downsampling_first_pass"),
804 timestamp_writes: None,
805 });
806
807 let pass_span =
808 diagnostics.pass_span(&mut compute_pass, "lightprobe_downsampling_first_pass");
809
810 compute_pass.set_pipeline(downsample_first_pipeline);
811 compute_pass.set_bind_group(0, &bind_groups.downsampling_first, &[]);
812
813 let tex_size = env_map_light.environment_map.texture_descriptor.size;
814 let wg_x = tex_size.width.div_ceil(64);
815 let wg_y = tex_size.height.div_ceil(64);
816 compute_pass.dispatch_workgroups(wg_x, wg_y, 6); pass_span.end(&mut compute_pass);
819 }
820
821 {
823 let mut compute_pass =
824 ctx.command_encoder()
825 .begin_compute_pass(&ComputePassDescriptor {
826 label: Some("lightprobe_downsampling_second_pass"),
827 timestamp_writes: None,
828 });
829
830 let pass_span =
831 diagnostics.pass_span(&mut compute_pass, "lightprobe_downsampling_second_pass");
832
833 compute_pass.set_pipeline(downsample_second_pipeline);
834 compute_pass.set_bind_group(0, &bind_groups.downsampling_second, &[]);
835
836 let tex_size = env_map_light.environment_map.texture_descriptor.size;
837 let wg_x = tex_size.width.div_ceil(256);
838 let wg_y = tex_size.height.div_ceil(256);
839 compute_pass.dispatch_workgroups(wg_x, wg_y, 6);
840
841 pass_span.end(&mut compute_pass);
842 }
843 }
844}
845
846pub fn filtering_system(
847 query: Query<(&GeneratorBindGroups, &RenderEnvironmentMap)>,
848 pipeline_cache: Res<PipelineCache>,
849 pipelines: Option<Res<GeneratorPipelines>>,
850 mut ctx: RenderContext,
851) {
852 let Some(pipelines) = pipelines else {
853 return;
854 };
855
856 let Some(radiance_pipeline) = pipeline_cache.get_compute_pipeline(pipelines.radiance) else {
857 return;
858 };
859 let Some(irradiance_pipeline) = pipeline_cache.get_compute_pipeline(pipelines.irradiance)
860 else {
861 return;
862 };
863
864 let diagnostics = ctx.diagnostic_recorder();
865 let diagnostics = diagnostics.as_deref();
866
867 for (bind_groups, env_map_light) in &query {
868 {
870 let mut compute_pass =
871 ctx.command_encoder()
872 .begin_compute_pass(&ComputePassDescriptor {
873 label: Some("lightprobe_radiance_map"),
874 timestamp_writes: None,
875 });
876
877 let pass_span = diagnostics.pass_span(&mut compute_pass, "lightprobe_radiance_map");
878
879 compute_pass.set_pipeline(radiance_pipeline);
880
881 let base_size = env_map_light.specular_map.texture_descriptor.size.width;
882
883 for (mip, bind_group) in bind_groups.radiance.iter().enumerate() {
885 compute_pass.set_bind_group(0, bind_group, &[]);
886
887 let mip_size = base_size >> mip;
889 let workgroup_count = mip_size.div_ceil(8);
890
891 compute_pass.dispatch_workgroups(workgroup_count, workgroup_count, 6);
893 }
894
895 pass_span.end(&mut compute_pass);
896 }
897
898 {
901 let mut compute_pass =
902 ctx.command_encoder()
903 .begin_compute_pass(&ComputePassDescriptor {
904 label: Some("lightprobe_irradiance_map"),
905 timestamp_writes: None,
906 });
907
908 let irr_span = diagnostics.pass_span(&mut compute_pass, "lightprobe_irradiance_map");
909
910 compute_pass.set_pipeline(irradiance_pipeline);
911 compute_pass.set_bind_group(0, &bind_groups.irradiance, &[]);
912
913 compute_pass.dispatch_workgroups(4, 4, 6);
915
916 irr_span.end(&mut compute_pass);
917 }
918 }
919}
920
921pub fn generate_environment_map_light(
923 mut commands: Commands,
924 mut images: ResMut<Assets<Image>>,
925 query: Query<(Entity, &GeneratedEnvironmentMapLight), Without<EnvironmentMapLight>>,
926) {
927 for (entity, filtered_env_map) in &query {
928 let Some(src_image) = images.get(&filtered_env_map.environment_map) else {
930 continue;
932 };
933
934 let base_size = src_image.texture_descriptor.size.width;
935
936 if src_image.texture_descriptor.size.height != base_size
938 || !base_size.is_power_of_two()
939 || base_size > 8192
940 {
941 panic!(
942 "GeneratedEnvironmentMapLight source cubemap must be square power-of-two ≤ 8192, got {}×{}",
943 base_size, src_image.texture_descriptor.size.height
944 );
945 }
946
947 let mip_count = compute_mip_count(base_size);
948
949 let mut diffuse = Image::new_fill(
951 Extent3d {
952 width: 32,
953 height: 32,
954 depth_or_array_layers: 6,
955 },
956 TextureDimension::D2,
957 &[0; 8],
958 TextureFormat::Rgba16Float,
959 RenderAssetUsages::all(),
960 );
961
962 diffuse.texture_descriptor.usage =
963 TextureUsages::TEXTURE_BINDING | TextureUsages::STORAGE_BINDING;
964
965 diffuse.texture_view_descriptor = Some(TextureViewDescriptor {
966 dimension: Some(TextureViewDimension::Cube),
967 ..Default::default()
968 });
969
970 let diffuse_handle = images.add(diffuse);
971
972 let mut specular = Image::new_fill(
974 Extent3d {
975 width: base_size,
976 height: base_size,
977 depth_or_array_layers: 6,
978 },
979 TextureDimension::D2,
980 &[0; 8],
981 TextureFormat::Rgba16Float,
982 RenderAssetUsages::all(),
983 );
984
985 specular.texture_descriptor.usage =
987 TextureUsages::TEXTURE_BINDING | TextureUsages::STORAGE_BINDING;
988 specular.texture_descriptor.mip_level_count = mip_count;
989
990 specular.data = None;
993
994 specular.texture_view_descriptor = Some(TextureViewDescriptor {
995 dimension: Some(TextureViewDimension::Cube),
996 mip_level_count: Some(mip_count),
997 ..Default::default()
998 });
999
1000 let specular_handle = images.add(specular);
1001
1002 commands.entity(entity).insert(EnvironmentMapLight {
1004 diffuse_map: diffuse_handle,
1005 specular_map: specular_handle,
1006 intensity: filtered_env_map.intensity,
1007 rotation: filtered_env_map.rotation,
1008 affects_lightmapped_mesh_diffuse: filtered_env_map.affects_lightmapped_mesh_diffuse,
1009 });
1010 }
1011}
1012
1013impl SyncComponent<RenderApp, EnvironmentMapGenerationPlugin> for GeneratedEnvironmentMapLight {
1014 type Target = RenderEnvironmentMap;
1015}