1use bevy_app::{App, Plugin, Update};
15use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer, Assets, RenderAssetUsages};
16use bevy_core_pipeline::core_3d::graph::{Core3d, Node3d};
17use bevy_ecs::{
18 component::Component,
19 entity::Entity,
20 query::{QueryState, With, Without},
21 resource::Resource,
22 schedule::IntoScheduleConfigs,
23 system::{lifetimeless::Read, Commands, Query, Res, ResMut},
24 world::{FromWorld, World},
25};
26use bevy_image::Image;
27use bevy_math::{Quat, UVec2, Vec2};
28use bevy_render::{
29 diagnostic::RecordDiagnostics,
30 render_asset::RenderAssets,
31 render_graph::{Node, NodeRunError, RenderGraphContext, RenderGraphExt, RenderLabel},
32 render_resource::{
33 binding_types::*, AddressMode, BindGroup, BindGroupEntries, BindGroupLayoutDescriptor,
34 BindGroupLayoutEntries, CachedComputePipelineId, ComputePassDescriptor,
35 ComputePipelineDescriptor, DownlevelFlags, Extent3d, FilterMode, PipelineCache, Sampler,
36 SamplerBindingType, SamplerDescriptor, ShaderStages, ShaderType, StorageTextureAccess,
37 Texture, TextureAspect, TextureDescriptor, TextureDimension, TextureFormat,
38 TextureFormatFeatureFlags, TextureSampleType, TextureUsages, TextureView,
39 TextureViewDescriptor, TextureViewDimension, UniformBuffer,
40 },
41 renderer::{RenderAdapter, RenderContext, RenderDevice, RenderQueue},
42 settings::WgpuFeatures,
43 sync_component::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(PartialEq, Eq, Debug, Copy, Clone, Hash, RenderLabel)]
72pub enum GeneratorNode {
73 Downsampling,
74 Filtering,
75}
76
77#[derive(Resource)]
79pub struct GeneratorBindGroupLayouts {
80 pub downsampling_first: BindGroupLayoutDescriptor,
81 pub downsampling_second: BindGroupLayoutDescriptor,
82 pub radiance: BindGroupLayoutDescriptor,
83 pub irradiance: BindGroupLayoutDescriptor,
84 pub copy: BindGroupLayoutDescriptor,
85}
86
87#[derive(Resource)]
89pub struct GeneratorSamplers {
90 pub linear: Sampler,
91}
92
93#[derive(Resource)]
95pub struct GeneratorPipelines {
96 pub downsample_first: CachedComputePipelineId,
97 pub downsample_second: CachedComputePipelineId,
98 pub copy: CachedComputePipelineId,
99 pub radiance: CachedComputePipelineId,
100 pub irradiance: CachedComputePipelineId,
101}
102
103#[derive(Resource, Clone, Copy, Debug, PartialEq, Eq)]
105pub struct DownsamplingConfig {
106 pub combine_bind_group: bool,
108}
109
110pub struct EnvironmentMapGenerationPlugin;
111
112impl Plugin for EnvironmentMapGenerationPlugin {
113 fn build(&self, _: &mut App) {}
114 fn finish(&self, app: &mut App) {
115 if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
116 let adapter = render_app.world().resource::<RenderAdapter>();
117 let device = render_app.world().resource::<RenderDevice>();
118
119 let limit_support = device.limits().max_storage_textures_per_shader_stage >= 6
121 && device.limits().max_compute_workgroup_storage_size != 0
122 && device.limits().max_compute_workgroup_size_x != 0;
123
124 let downlevel_support = adapter
125 .get_downlevel_capabilities()
126 .flags
127 .contains(DownlevelFlags::COMPUTE_SHADERS);
128
129 if !limit_support || !downlevel_support {
130 info!("Disabling EnvironmentMapGenerationPlugin because compute is not supported on this platform. This is safe to ignore if you are not using EnvironmentMapGenerationPlugin.");
131 return;
132 }
133 } else {
134 return;
135 }
136
137 embedded_asset!(app, "environment_filter.wgsl");
138 embedded_asset!(app, "downsample.wgsl");
139 embedded_asset!(app, "copy.wgsl");
140
141 app.add_plugins(SyncComponentPlugin::<GeneratedEnvironmentMapLight>::default())
142 .add_systems(Update, generate_environment_map_light);
143
144 let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
145 return;
146 };
147
148 render_app
149 .add_render_graph_node::<DownsamplingNode>(Core3d, GeneratorNode::Downsampling)
150 .add_render_graph_node::<FilteringNode>(Core3d, GeneratorNode::Filtering)
151 .add_render_graph_edges(
152 Core3d,
153 (
154 Node3d::EndPrepasses,
155 GeneratorNode::Downsampling,
156 GeneratorNode::Filtering,
157 Node3d::StartMainPass,
158 ),
159 )
160 .add_systems(
161 ExtractSchedule,
162 extract_generated_environment_map_entities.after(generate_environment_map_light),
163 )
164 .add_systems(
165 Render,
166 prepare_generated_environment_map_bind_groups
167 .in_set(RenderSystems::PrepareBindGroups),
168 )
169 .add_systems(
170 Render,
171 prepare_generated_environment_map_intermediate_textures
172 .in_set(RenderSystems::PrepareResources),
173 )
174 .add_systems(
175 RenderStartup,
176 initialize_generated_environment_map_resources,
177 );
178 }
179}
180
181const REQUIRED_STORAGE_TEXTURES: u32 = 12;
183
184pub fn initialize_generated_environment_map_resources(
187 mut commands: Commands,
188 render_device: Res<RenderDevice>,
189 render_adapter: Res<RenderAdapter>,
190 pipeline_cache: Res<PipelineCache>,
191 asset_server: Res<AssetServer>,
192) {
193 let storage_texture_limit = render_device.limits().max_storage_textures_per_shader_stage;
195
196 let read_write_support = render_adapter
198 .get_texture_format_features(TextureFormat::Rgba16Float)
199 .flags
200 .contains(TextureFormatFeatureFlags::STORAGE_READ_WRITE);
201
202 let combine_bind_group =
204 storage_texture_limit >= REQUIRED_STORAGE_TEXTURES && read_write_support;
205
206 let mips =
208 texture_storage_2d_array(TextureFormat::Rgba16Float, StorageTextureAccess::WriteOnly);
209
210 let (downsampling_first, downsampling_second) = if combine_bind_group {
212 let downsampling = BindGroupLayoutDescriptor::new(
214 "downsampling_bind_group_layout_combined",
215 &BindGroupLayoutEntries::sequential(
216 ShaderStages::COMPUTE,
217 (
218 sampler(SamplerBindingType::Filtering),
219 uniform_buffer::<DownsamplingConstants>(false),
220 texture_2d_array(TextureSampleType::Float { filterable: true }),
221 mips, mips, mips, mips, mips, texture_storage_2d_array(
227 TextureFormat::Rgba16Float,
228 StorageTextureAccess::ReadWrite,
229 ), mips, mips, mips, mips, mips, mips, ),
237 ),
238 );
239
240 (downsampling.clone(), downsampling)
241 } else {
242 let downsampling_first = BindGroupLayoutDescriptor::new(
245 "downsampling_first_bind_group_layout",
246 &BindGroupLayoutEntries::sequential(
247 ShaderStages::COMPUTE,
248 (
249 sampler(SamplerBindingType::Filtering),
250 uniform_buffer::<DownsamplingConstants>(false),
251 texture_2d_array(TextureSampleType::Float { filterable: true }),
253 mips, mips, mips, mips, mips, mips, ),
260 ),
261 );
262
263 let downsampling_second = BindGroupLayoutDescriptor::new(
264 "downsampling_second_bind_group_layout",
265 &BindGroupLayoutEntries::sequential(
266 ShaderStages::COMPUTE,
267 (
268 sampler(SamplerBindingType::Filtering),
269 uniform_buffer::<DownsamplingConstants>(false),
270 texture_2d_array(TextureSampleType::Float { filterable: true }),
272 mips, mips, mips, mips, mips, mips, ),
279 ),
280 );
281
282 (downsampling_first, downsampling_second)
283 };
284 let radiance = BindGroupLayoutDescriptor::new(
285 "radiance_bind_group_layout",
286 &BindGroupLayoutEntries::sequential(
287 ShaderStages::COMPUTE,
288 (
289 texture_2d_array(TextureSampleType::Float { filterable: true }),
291 sampler(SamplerBindingType::Filtering), texture_storage_2d_array(
294 TextureFormat::Rgba16Float,
295 StorageTextureAccess::WriteOnly,
296 ),
297 uniform_buffer::<FilteringConstants>(false), texture_2d_array(TextureSampleType::Float { filterable: true }), ),
300 ),
301 );
302
303 let irradiance = BindGroupLayoutDescriptor::new(
304 "irradiance_bind_group_layout",
305 &BindGroupLayoutEntries::sequential(
306 ShaderStages::COMPUTE,
307 (
308 texture_2d_array(TextureSampleType::Float { filterable: true }),
310 sampler(SamplerBindingType::Filtering), texture_storage_2d_array(
313 TextureFormat::Rgba16Float,
314 StorageTextureAccess::WriteOnly,
315 ),
316 uniform_buffer::<FilteringConstants>(false), texture_2d_array(TextureSampleType::Float { filterable: true }), ),
319 ),
320 );
321
322 let copy = BindGroupLayoutDescriptor::new(
323 "copy_bind_group_layout",
324 &BindGroupLayoutEntries::sequential(
325 ShaderStages::COMPUTE,
326 (
327 texture_2d_array(TextureSampleType::Float { filterable: true }),
329 texture_storage_2d_array(
331 TextureFormat::Rgba16Float,
332 StorageTextureAccess::WriteOnly,
333 ),
334 ),
335 ),
336 );
337
338 let layouts = GeneratorBindGroupLayouts {
339 downsampling_first,
340 downsampling_second,
341 radiance,
342 irradiance,
343 copy,
344 };
345
346 let linear = render_device.create_sampler(&SamplerDescriptor {
348 label: Some("generator_linear_sampler"),
349 address_mode_u: AddressMode::ClampToEdge,
350 address_mode_v: AddressMode::ClampToEdge,
351 address_mode_w: AddressMode::ClampToEdge,
352 mag_filter: FilterMode::Linear,
353 min_filter: FilterMode::Linear,
354 mipmap_filter: FilterMode::Linear,
355 ..Default::default()
356 });
357
358 let samplers = GeneratorSamplers { linear };
359
360 let features = render_device.features();
362 let mut shader_defs = vec![];
363 if features.contains(WgpuFeatures::SUBGROUP) {
364 shader_defs.push(ShaderDefVal::Int("SUBGROUP_SUPPORT".into(), 1));
365 }
366 if combine_bind_group {
367 shader_defs.push(ShaderDefVal::Int("COMBINE_BIND_GROUP".into(), 1));
368 }
369 #[cfg(feature = "bluenoise_texture")]
370 {
371 shader_defs.push(ShaderDefVal::Int("HAS_BLUE_NOISE".into(), 1));
372 }
373
374 let downsampling_shader = load_embedded_asset!(asset_server.as_ref(), "downsample.wgsl");
375 let env_filter_shader = load_embedded_asset!(asset_server.as_ref(), "environment_filter.wgsl");
376 let copy_shader = load_embedded_asset!(asset_server.as_ref(), "copy.wgsl");
377
378 let downsample_first = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
380 label: Some("downsampling_first_pipeline".into()),
381 layout: vec![layouts.downsampling_first.clone()],
382 push_constant_ranges: vec![],
383 shader: downsampling_shader.clone(),
384 shader_defs: {
385 let mut defs = shader_defs.clone();
386 if !combine_bind_group {
387 defs.push(ShaderDefVal::Int("FIRST_PASS".into(), 1));
388 }
389 defs
390 },
391 entry_point: Some("downsample_first".into()),
392 zero_initialize_workgroup_memory: false,
393 });
394
395 let downsample_second = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
396 label: Some("downsampling_second_pipeline".into()),
397 layout: vec![layouts.downsampling_second.clone()],
398 push_constant_ranges: vec![],
399 shader: downsampling_shader,
400 shader_defs: {
401 let mut defs = shader_defs.clone();
402 if !combine_bind_group {
403 defs.push(ShaderDefVal::Int("SECOND_PASS".into(), 1));
404 }
405 defs
406 },
407 entry_point: Some("downsample_second".into()),
408 zero_initialize_workgroup_memory: false,
409 });
410
411 let radiance = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
413 label: Some("radiance_pipeline".into()),
414 layout: vec![layouts.radiance.clone()],
415 push_constant_ranges: vec![],
416 shader: env_filter_shader.clone(),
417 shader_defs: shader_defs.clone(),
418 entry_point: Some("generate_radiance_map".into()),
419 zero_initialize_workgroup_memory: false,
420 });
421
422 let irradiance = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
424 label: Some("irradiance_pipeline".into()),
425 layout: vec![layouts.irradiance.clone()],
426 push_constant_ranges: vec![],
427 shader: env_filter_shader,
428 shader_defs: shader_defs.clone(),
429 entry_point: Some("generate_irradiance_map".into()),
430 zero_initialize_workgroup_memory: false,
431 });
432
433 let copy_pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
435 label: Some("copy_pipeline".into()),
436 layout: vec![layouts.copy.clone()],
437 push_constant_ranges: vec![],
438 shader: copy_shader,
439 shader_defs: vec![],
440 entry_point: Some("copy".into()),
441 zero_initialize_workgroup_memory: false,
442 });
443
444 let pipelines = GeneratorPipelines {
445 downsample_first,
446 downsample_second,
447 radiance,
448 irradiance,
449 copy: copy_pipeline,
450 };
451
452 commands.insert_resource(layouts);
454 commands.insert_resource(samplers);
455 commands.insert_resource(pipelines);
456 commands.insert_resource(DownsamplingConfig { combine_bind_group });
457}
458
459pub fn extract_generated_environment_map_entities(
460 query: Extract<
461 Query<(
462 RenderEntity,
463 &GeneratedEnvironmentMapLight,
464 &EnvironmentMapLight,
465 )>,
466 >,
467 mut commands: Commands,
468 render_images: Res<RenderAssets<GpuImage>>,
469) {
470 for (entity, filtered_env_map, env_map_light) in query.iter() {
471 let Some(env_map) = render_images.get(&filtered_env_map.environment_map) else {
472 continue;
473 };
474
475 let diffuse_map = render_images.get(&env_map_light.diffuse_map);
476 let specular_map = render_images.get(&env_map_light.specular_map);
477
478 if diffuse_map.is_none() || specular_map.is_none() {
480 continue;
481 }
482
483 let diffuse_map = diffuse_map.unwrap();
484 let specular_map = specular_map.unwrap();
485
486 let render_filtered_env_map = RenderEnvironmentMap {
487 environment_map: env_map.clone(),
488 diffuse_map: diffuse_map.clone(),
489 specular_map: specular_map.clone(),
490 intensity: filtered_env_map.intensity,
491 rotation: filtered_env_map.rotation,
492 affects_lightmapped_mesh_diffuse: filtered_env_map.affects_lightmapped_mesh_diffuse,
493 };
494 commands
495 .get_entity(entity)
496 .expect("Entity not synced to render world")
497 .insert(render_filtered_env_map);
498 }
499}
500
501#[derive(Component, Clone)]
503pub struct RenderEnvironmentMap {
504 pub environment_map: GpuImage,
505 pub diffuse_map: GpuImage,
506 pub specular_map: GpuImage,
507 pub intensity: f32,
508 pub rotation: Quat,
509 pub affects_lightmapped_mesh_diffuse: bool,
510}
511
512#[derive(Component)]
513pub struct IntermediateTextures {
514 pub environment_map: CachedTexture,
515}
516
517#[inline]
520fn compute_mip_count(size: u32) -> u32 {
521 debug_assert!(size.is_power_of_two());
522 32 - size.leading_zeros()
523}
524
525pub fn prepare_generated_environment_map_intermediate_textures(
527 light_probes: Query<(Entity, &RenderEnvironmentMap)>,
528 render_device: Res<RenderDevice>,
529 mut texture_cache: ResMut<TextureCache>,
530 mut commands: Commands,
531) {
532 for (entity, env_map_light) in &light_probes {
533 let base_size = env_map_light.environment_map.size.width;
534 let mip_level_count = compute_mip_count(base_size);
535
536 let environment_map = texture_cache.get(
537 &render_device,
538 TextureDescriptor {
539 label: Some("intermediate_environment_map"),
540 size: Extent3d {
541 width: base_size,
542 height: base_size,
543 depth_or_array_layers: 6, },
545 mip_level_count,
546 sample_count: 1,
547 dimension: TextureDimension::D2,
548 format: TextureFormat::Rgba16Float,
549 usage: TextureUsages::TEXTURE_BINDING
550 | TextureUsages::STORAGE_BINDING
551 | TextureUsages::COPY_DST,
552 view_formats: &[],
553 },
554 );
555
556 commands
557 .entity(entity)
558 .insert(IntermediateTextures { environment_map });
559 }
560}
561
562#[derive(Clone, Copy, ShaderType)]
564#[repr(C)]
565pub struct DownsamplingConstants {
566 mips: u32,
567 inverse_input_size: Vec2,
568 _padding: u32,
569}
570
571#[derive(Clone, Copy, ShaderType)]
573#[repr(C)]
574pub struct FilteringConstants {
575 mip_level: f32,
576 sample_count: u32,
577 roughness: f32,
578 noise_size_bits: UVec2,
579}
580
581#[derive(Component)]
583pub struct GeneratorBindGroups {
584 pub downsampling_first: BindGroup,
585 pub downsampling_second: BindGroup,
586 pub radiance: Vec<BindGroup>, pub irradiance: BindGroup,
588 pub copy: BindGroup,
589}
590
591pub fn prepare_generated_environment_map_bind_groups(
593 light_probes: Query<
594 (Entity, &IntermediateTextures, &RenderEnvironmentMap),
595 With<RenderEnvironmentMap>,
596 >,
597 render_device: Res<RenderDevice>,
598 pipeline_cache: Res<PipelineCache>,
599 queue: Res<RenderQueue>,
600 layouts: Res<GeneratorBindGroupLayouts>,
601 samplers: Res<GeneratorSamplers>,
602 render_images: Res<RenderAssets<GpuImage>>,
603 bluenoise: Res<Bluenoise>,
604 config: Res<DownsamplingConfig>,
605 mut commands: Commands,
606) {
607 let Some(stbn_texture) = render_images.get(&bluenoise.texture) else {
610 return;
611 };
612
613 assert!(stbn_texture.size.width.is_power_of_two());
614 assert!(stbn_texture.size.height.is_power_of_two());
615 let noise_size_bits = UVec2::new(
616 stbn_texture.size.width.trailing_zeros(),
617 stbn_texture.size.height.trailing_zeros(),
618 );
619
620 for (entity, textures, env_map_light) in &light_probes {
621 let base_size = env_map_light.environment_map.size.width;
623 let mip_count = compute_mip_count(base_size);
624 let last_mip = mip_count - 1;
625 let env_map_texture = env_map_light.environment_map.texture.clone();
626
627 let downsampling_constants = DownsamplingConstants {
629 mips: mip_count - 1, inverse_input_size: Vec2::new(1.0 / base_size as f32, 1.0 / base_size as f32),
631 _padding: 0,
632 };
633
634 let mut downsampling_constants_buffer = UniformBuffer::from(downsampling_constants);
635 downsampling_constants_buffer.write_buffer(&render_device, &queue);
636
637 let input_env_map_first = env_map_texture.clone().create_view(&TextureViewDescriptor {
638 dimension: Some(TextureViewDimension::D2Array),
639 ..Default::default()
640 });
641
642 let mip_storage = |level: u32| {
644 if level <= last_mip {
645 create_storage_view(&textures.environment_map.texture, level, &render_device)
646 } else {
647 create_placeholder_storage_view(&render_device)
649 }
650 };
651
652 let (downsampling_first_bind_group, downsampling_second_bind_group) =
654 if config.combine_bind_group {
655 let bind_group = render_device.create_bind_group(
657 "downsampling_bind_group_combined_first",
658 &pipeline_cache.get_bind_group_layout(&layouts.downsampling_first),
659 &BindGroupEntries::sequential((
660 &samplers.linear,
661 &downsampling_constants_buffer,
662 &input_env_map_first,
663 &mip_storage(1),
664 &mip_storage(2),
665 &mip_storage(3),
666 &mip_storage(4),
667 &mip_storage(5),
668 &mip_storage(6),
669 &mip_storage(7),
670 &mip_storage(8),
671 &mip_storage(9),
672 &mip_storage(10),
673 &mip_storage(11),
674 &mip_storage(12),
675 )),
676 );
677
678 (bind_group.clone(), bind_group)
679 } else {
680 let input_env_map_second =
682 textures
683 .environment_map
684 .texture
685 .create_view(&TextureViewDescriptor {
686 dimension: Some(TextureViewDimension::D2Array),
687 base_mip_level: min(6, last_mip),
688 mip_level_count: Some(1),
689 ..Default::default()
690 });
691
692 let first = render_device.create_bind_group(
694 "downsampling_first_bind_group",
695 &pipeline_cache.get_bind_group_layout(&layouts.downsampling_first),
696 &BindGroupEntries::sequential((
697 &samplers.linear,
698 &downsampling_constants_buffer,
699 &input_env_map_first,
700 &mip_storage(1),
701 &mip_storage(2),
702 &mip_storage(3),
703 &mip_storage(4),
704 &mip_storage(5),
705 &mip_storage(6),
706 )),
707 );
708
709 let second = render_device.create_bind_group(
710 "downsampling_second_bind_group",
711 &pipeline_cache.get_bind_group_layout(&layouts.downsampling_second),
712 &BindGroupEntries::sequential((
713 &samplers.linear,
714 &downsampling_constants_buffer,
715 &input_env_map_second,
716 &mip_storage(7),
717 &mip_storage(8),
718 &mip_storage(9),
719 &mip_storage(10),
720 &mip_storage(11),
721 &mip_storage(12),
722 )),
723 );
724
725 (first, second)
726 };
727
728 let stbn_texture_view = stbn_texture
730 .texture
731 .clone()
732 .create_view(&TextureViewDescriptor {
733 dimension: Some(TextureViewDimension::D2Array),
734 ..Default::default()
735 });
736
737 let num_mips = mip_count as usize;
739 let mut radiance_bind_groups = Vec::with_capacity(num_mips);
740
741 for mip in 0..num_mips {
742 let roughness = mip as f32 / (num_mips - 1) as f32;
745 let sample_count = 32u32 * 2u32.pow((roughness * 4.0) as u32);
746
747 let radiance_constants = FilteringConstants {
748 mip_level: mip as f32,
749 sample_count,
750 roughness,
751 noise_size_bits,
752 };
753
754 let mut radiance_constants_buffer = UniformBuffer::from(radiance_constants);
755 radiance_constants_buffer.write_buffer(&render_device, &queue);
756
757 let mip_storage_view = create_storage_view(
758 &env_map_light.specular_map.texture,
759 mip as u32,
760 &render_device,
761 );
762 let bind_group = render_device.create_bind_group(
763 Some(format!("radiance_bind_group_mip_{mip}").as_str()),
764 &pipeline_cache.get_bind_group_layout(&layouts.radiance),
765 &BindGroupEntries::sequential((
766 &textures.environment_map.default_view,
767 &samplers.linear,
768 &mip_storage_view,
769 &radiance_constants_buffer,
770 &stbn_texture_view,
771 )),
772 );
773
774 radiance_bind_groups.push(bind_group);
775 }
776
777 let irradiance_constants = FilteringConstants {
779 mip_level: 0.0,
780 sample_count: 1024,
782 roughness: 1.0,
783 noise_size_bits,
784 };
785
786 let mut irradiance_constants_buffer = UniformBuffer::from(irradiance_constants);
787 irradiance_constants_buffer.write_buffer(&render_device, &queue);
788
789 let irradiance_map =
791 env_map_light
792 .diffuse_map
793 .texture
794 .create_view(&TextureViewDescriptor {
795 dimension: Some(TextureViewDimension::D2Array),
796 ..Default::default()
797 });
798
799 let irradiance_bind_group = render_device.create_bind_group(
800 "irradiance_bind_group",
801 &pipeline_cache.get_bind_group_layout(&layouts.irradiance),
802 &BindGroupEntries::sequential((
803 &textures.environment_map.default_view,
804 &samplers.linear,
805 &irradiance_map,
806 &irradiance_constants_buffer,
807 &stbn_texture_view,
808 )),
809 );
810
811 let src_view = env_map_light
813 .environment_map
814 .texture
815 .create_view(&TextureViewDescriptor {
816 dimension: Some(TextureViewDimension::D2Array),
817 ..Default::default()
818 });
819
820 let dst_view = create_storage_view(&textures.environment_map.texture, 0, &render_device);
821
822 let copy_bind_group = render_device.create_bind_group(
823 "copy_bind_group",
824 &pipeline_cache.get_bind_group_layout(&layouts.copy),
825 &BindGroupEntries::with_indices(((0, &src_view), (1, &dst_view))),
826 );
827
828 commands.entity(entity).insert(GeneratorBindGroups {
829 downsampling_first: downsampling_first_bind_group,
830 downsampling_second: downsampling_second_bind_group,
831 radiance: radiance_bind_groups,
832 irradiance: irradiance_bind_group,
833 copy: copy_bind_group,
834 });
835 }
836}
837
838fn create_storage_view(texture: &Texture, mip: u32, _render_device: &RenderDevice) -> TextureView {
840 texture.create_view(&TextureViewDescriptor {
841 label: Some(format!("storage_view_mip_{mip}").as_str()),
842 format: Some(texture.format()),
843 dimension: Some(TextureViewDimension::D2Array),
844 aspect: TextureAspect::All,
845 base_mip_level: mip,
846 mip_level_count: Some(1),
847 base_array_layer: 0,
848 array_layer_count: Some(texture.depth_or_array_layers()),
849 usage: Some(TextureUsages::STORAGE_BINDING),
850 })
851}
852
853fn create_placeholder_storage_view(render_device: &RenderDevice) -> TextureView {
856 let tex = render_device.create_texture(&TextureDescriptor {
857 label: Some("lightprobe_placeholder"),
858 size: Extent3d {
859 width: 1,
860 height: 1,
861 depth_or_array_layers: 6,
862 },
863 mip_level_count: 1,
864 sample_count: 1,
865 dimension: TextureDimension::D2,
866 format: TextureFormat::Rgba16Float,
867 usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING,
868 view_formats: &[],
869 });
870
871 tex.create_view(&TextureViewDescriptor::default())
872}
873
874pub struct DownsamplingNode {
876 query: QueryState<(
877 Entity,
878 Read<GeneratorBindGroups>,
879 Read<RenderEnvironmentMap>,
880 )>,
881}
882
883impl FromWorld for DownsamplingNode {
884 fn from_world(world: &mut World) -> Self {
885 Self {
886 query: QueryState::new(world),
887 }
888 }
889}
890
891impl Node for DownsamplingNode {
892 fn update(&mut self, world: &mut World) {
893 self.query.update_archetypes(world);
894 }
895
896 fn run(
897 &self,
898 _graph: &mut RenderGraphContext,
899 render_context: &mut RenderContext,
900 world: &World,
901 ) -> Result<(), NodeRunError> {
902 let pipeline_cache = world.resource::<PipelineCache>();
903 let pipelines = world.resource::<GeneratorPipelines>();
904
905 let Some(downsample_first_pipeline) =
906 pipeline_cache.get_compute_pipeline(pipelines.downsample_first)
907 else {
908 return Ok(());
909 };
910
911 let Some(downsample_second_pipeline) =
912 pipeline_cache.get_compute_pipeline(pipelines.downsample_second)
913 else {
914 return Ok(());
915 };
916
917 let diagnostics = render_context.diagnostic_recorder();
918
919 for (_, bind_groups, env_map_light) in self.query.iter_manual(world) {
920 let Some(copy_pipeline) = pipeline_cache.get_compute_pipeline(pipelines.copy) else {
922 return Ok(());
923 };
924
925 {
926 let mut compute_pass =
927 render_context
928 .command_encoder()
929 .begin_compute_pass(&ComputePassDescriptor {
930 label: Some("lightprobe_copy"),
931 timestamp_writes: None,
932 });
933
934 let pass_span = diagnostics.pass_span(&mut compute_pass, "lightprobe_copy");
935
936 compute_pass.set_pipeline(copy_pipeline);
937 compute_pass.set_bind_group(0, &bind_groups.copy, &[]);
938
939 let tex_size = env_map_light.environment_map.size;
940 let wg_x = tex_size.width.div_ceil(8);
941 let wg_y = tex_size.height.div_ceil(8);
942 compute_pass.dispatch_workgroups(wg_x, wg_y, 6);
943
944 pass_span.end(&mut compute_pass);
945 }
946
947 {
949 let mut compute_pass =
950 render_context
951 .command_encoder()
952 .begin_compute_pass(&ComputePassDescriptor {
953 label: Some("lightprobe_downsampling_first_pass"),
954 timestamp_writes: None,
955 });
956
957 let pass_span =
958 diagnostics.pass_span(&mut compute_pass, "lightprobe_downsampling_first_pass");
959
960 compute_pass.set_pipeline(downsample_first_pipeline);
961 compute_pass.set_bind_group(0, &bind_groups.downsampling_first, &[]);
962
963 let tex_size = env_map_light.environment_map.size;
964 let wg_x = tex_size.width.div_ceil(64);
965 let wg_y = tex_size.height.div_ceil(64);
966 compute_pass.dispatch_workgroups(wg_x, wg_y, 6); pass_span.end(&mut compute_pass);
969 }
970
971 {
973 let mut compute_pass =
974 render_context
975 .command_encoder()
976 .begin_compute_pass(&ComputePassDescriptor {
977 label: Some("lightprobe_downsampling_second_pass"),
978 timestamp_writes: None,
979 });
980
981 let pass_span =
982 diagnostics.pass_span(&mut compute_pass, "lightprobe_downsampling_second_pass");
983
984 compute_pass.set_pipeline(downsample_second_pipeline);
985 compute_pass.set_bind_group(0, &bind_groups.downsampling_second, &[]);
986
987 let tex_size = env_map_light.environment_map.size;
988 let wg_x = tex_size.width.div_ceil(256);
989 let wg_y = tex_size.height.div_ceil(256);
990 compute_pass.dispatch_workgroups(wg_x, wg_y, 6);
991
992 pass_span.end(&mut compute_pass);
993 }
994 }
995
996 Ok(())
997 }
998}
999
1000pub struct FilteringNode {
1002 query: QueryState<(
1003 Entity,
1004 Read<GeneratorBindGroups>,
1005 Read<RenderEnvironmentMap>,
1006 )>,
1007}
1008
1009impl FromWorld for FilteringNode {
1010 fn from_world(world: &mut World) -> Self {
1011 Self {
1012 query: QueryState::new(world),
1013 }
1014 }
1015}
1016
1017impl Node for FilteringNode {
1018 fn update(&mut self, world: &mut World) {
1019 self.query.update_archetypes(world);
1020 }
1021
1022 fn run(
1023 &self,
1024 _graph: &mut RenderGraphContext,
1025 render_context: &mut RenderContext,
1026 world: &World,
1027 ) -> Result<(), NodeRunError> {
1028 let pipeline_cache = world.resource::<PipelineCache>();
1029 let pipelines = world.resource::<GeneratorPipelines>();
1030
1031 let Some(radiance_pipeline) = pipeline_cache.get_compute_pipeline(pipelines.radiance)
1032 else {
1033 return Ok(());
1034 };
1035 let Some(irradiance_pipeline) = pipeline_cache.get_compute_pipeline(pipelines.irradiance)
1036 else {
1037 return Ok(());
1038 };
1039
1040 let diagnostics = render_context.diagnostic_recorder();
1041
1042 for (_, bind_groups, env_map_light) in self.query.iter_manual(world) {
1043 let mut compute_pass =
1044 render_context
1045 .command_encoder()
1046 .begin_compute_pass(&ComputePassDescriptor {
1047 label: Some("lightprobe_radiance_map"),
1048 timestamp_writes: None,
1049 });
1050
1051 let pass_span = diagnostics.pass_span(&mut compute_pass, "lightprobe_radiance_map");
1052
1053 compute_pass.set_pipeline(radiance_pipeline);
1054
1055 let base_size = env_map_light.specular_map.size.width;
1056
1057 for (mip, bind_group) in bind_groups.radiance.iter().enumerate() {
1060 compute_pass.set_bind_group(0, bind_group, &[]);
1061
1062 let mip_size = base_size >> mip;
1064 let workgroup_count = mip_size.div_ceil(8);
1065
1066 compute_pass.dispatch_workgroups(workgroup_count, workgroup_count, 6);
1068 }
1069 pass_span.end(&mut compute_pass);
1070 drop(compute_pass);
1072
1073 {
1076 let mut compute_pass =
1077 render_context
1078 .command_encoder()
1079 .begin_compute_pass(&ComputePassDescriptor {
1080 label: Some("lightprobe_irradiance_map"),
1081 timestamp_writes: None,
1082 });
1083
1084 let irr_span =
1085 diagnostics.pass_span(&mut compute_pass, "lightprobe_irradiance_map");
1086
1087 compute_pass.set_pipeline(irradiance_pipeline);
1088 compute_pass.set_bind_group(0, &bind_groups.irradiance, &[]);
1089
1090 compute_pass.dispatch_workgroups(4, 4, 6);
1092
1093 irr_span.end(&mut compute_pass);
1094 }
1095 }
1096
1097 Ok(())
1098 }
1099}
1100
1101pub fn generate_environment_map_light(
1103 mut commands: Commands,
1104 mut images: ResMut<Assets<Image>>,
1105 query: Query<(Entity, &GeneratedEnvironmentMapLight), Without<EnvironmentMapLight>>,
1106) {
1107 for (entity, filtered_env_map) in &query {
1108 let Some(src_image) = images.get(&filtered_env_map.environment_map) else {
1110 continue;
1112 };
1113
1114 let base_size = src_image.texture_descriptor.size.width;
1115
1116 if src_image.texture_descriptor.size.height != base_size
1118 || !base_size.is_power_of_two()
1119 || base_size > 8192
1120 {
1121 panic!(
1122 "GeneratedEnvironmentMapLight source cubemap must be square power-of-two ≤ 8192, got {}×{}",
1123 base_size, src_image.texture_descriptor.size.height
1124 );
1125 }
1126
1127 let mip_count = compute_mip_count(base_size);
1128
1129 let mut diffuse = Image::new_fill(
1131 Extent3d {
1132 width: 32,
1133 height: 32,
1134 depth_or_array_layers: 6,
1135 },
1136 TextureDimension::D2,
1137 &[0; 8],
1138 TextureFormat::Rgba16Float,
1139 RenderAssetUsages::all(),
1140 );
1141
1142 diffuse.texture_descriptor.usage =
1143 TextureUsages::TEXTURE_BINDING | TextureUsages::STORAGE_BINDING;
1144
1145 diffuse.texture_view_descriptor = Some(TextureViewDescriptor {
1146 dimension: Some(TextureViewDimension::Cube),
1147 ..Default::default()
1148 });
1149
1150 let diffuse_handle = images.add(diffuse);
1151
1152 let mut specular = Image::new_fill(
1154 Extent3d {
1155 width: base_size,
1156 height: base_size,
1157 depth_or_array_layers: 6,
1158 },
1159 TextureDimension::D2,
1160 &[0; 8],
1161 TextureFormat::Rgba16Float,
1162 RenderAssetUsages::all(),
1163 );
1164
1165 specular.texture_descriptor.usage =
1167 TextureUsages::TEXTURE_BINDING | TextureUsages::STORAGE_BINDING;
1168 specular.texture_descriptor.mip_level_count = mip_count;
1169
1170 specular.data = None;
1173
1174 specular.texture_view_descriptor = Some(TextureViewDescriptor {
1175 dimension: Some(TextureViewDimension::Cube),
1176 mip_level_count: Some(mip_count),
1177 ..Default::default()
1178 });
1179
1180 let specular_handle = images.add(specular);
1181
1182 commands.entity(entity).insert(EnvironmentMapLight {
1184 diffuse_map: diffuse_handle,
1185 specular_map: specular_handle,
1186 intensity: filtered_env_map.intensity,
1187 rotation: filtered_env_map.rotation,
1188 affects_lightmapped_mesh_diffuse: filtered_env_map.affects_lightmapped_mesh_diffuse,
1189 });
1190 }
1191}