1use bevy_app::{App, Plugin};
4use bevy_asset::AssetId;
5use bevy_camera::{
6 primitives::{Aabb, Frustum},
7 Camera3d,
8};
9use bevy_derive::{Deref, DerefMut};
10use bevy_ecs::{
11 component::Component,
12 entity::Entity,
13 query::With,
14 resource::Resource,
15 schedule::IntoScheduleConfigs,
16 system::{Commands, Local, Query, Res, ResMut},
17};
18use bevy_image::Image;
19use bevy_light::{EnvironmentMapLight, IrradianceVolume, LightProbe};
20use bevy_math::{Affine3A, FloatOrd, Mat4, Vec3A, Vec4};
21use bevy_platform::collections::HashMap;
22use bevy_render::{
23 extract_instances::ExtractInstancesPlugin,
24 render_asset::RenderAssets,
25 render_resource::{DynamicUniformBuffer, Sampler, ShaderType, TextureView},
26 renderer::{RenderAdapter, RenderAdapterInfo, RenderDevice, RenderQueue, WgpuWrapper},
27 settings::WgpuFeatures,
28 sync_world::RenderEntity,
29 texture::{FallbackImage, GpuImage},
30 view::ExtractedView,
31 Extract, ExtractSchedule, Render, RenderApp, RenderSystems,
32};
33use bevy_shader::load_shader_library;
34use bevy_transform::{components::Transform, prelude::GlobalTransform};
35use tracing::error;
36
37use core::{hash::Hash, ops::Deref};
38
39use crate::{
40 generate::EnvironmentMapGenerationPlugin, light_probe::environment_map::EnvironmentMapIds,
41};
42
43pub mod environment_map;
44pub mod generate;
45pub mod irradiance_volume;
46
47pub const MAX_VIEW_LIGHT_PROBES: usize = 8;
52
53const STANDARD_MATERIAL_FRAGMENT_SHADER_MIN_TEXTURE_BINDINGS: usize = 16;
56
57pub struct LightProbePlugin;
63
64#[derive(Clone, Copy, ShaderType, Default)]
66struct RenderLightProbe {
67 light_from_world_transposed: [Vec4; 3],
70
71 texture_index: i32,
77
78 intensity: f32,
82
83 affects_lightmapped_mesh_diffuse: u32,
86}
87
88#[derive(ShaderType)]
91pub struct LightProbesUniform {
92 reflection_probes: [RenderLightProbe; MAX_VIEW_LIGHT_PROBES],
95
96 irradiance_volumes: [RenderLightProbe; MAX_VIEW_LIGHT_PROBES],
99
100 reflection_probe_count: i32,
102
103 irradiance_volume_count: i32,
105
106 view_cubemap_index: i32,
110
111 smallest_specular_mip_level_for_view: u32,
114
115 intensity_for_view: f32,
119
120 view_environment_map_affects_lightmapped_mesh_diffuse: u32,
125}
126
127#[derive(Resource, Default, Deref, DerefMut)]
129pub struct LightProbesBuffer(DynamicUniformBuffer<LightProbesUniform>);
130
131#[derive(Component, Default, Deref, DerefMut)]
134pub struct ViewLightProbesUniformOffset(u32);
135
136struct LightProbeInfo<C>
142where
143 C: LightProbeComponent,
144{
145 light_from_world: [Vec4; 3],
150
151 world_from_light: Affine3A,
153
154 intensity: f32,
159
160 affects_lightmapped_mesh_diffuse: bool,
163
164 asset_id: C::AssetId,
170}
171
172#[derive(Component, Default)]
184pub struct RenderViewLightProbes<C>
185where
186 C: LightProbeComponent,
187{
188 binding_index_to_textures: Vec<C::AssetId>,
190
191 cubemap_to_binding_index: HashMap<C::AssetId, u32>,
194
195 render_light_probes: Vec<RenderLightProbe>,
204
205 view_light_probe_info: C::ViewLightProbeInfo,
213}
214
215pub trait LightProbeComponent: Send + Sync + Component + Sized {
225 type AssetId: Send + Sync + Clone + Eq + Hash;
232
233 type ViewLightProbeInfo: Send + Sync + Default;
241
242 fn id(&self, image_assets: &RenderAssets<GpuImage>) -> Option<Self::AssetId>;
245
246 fn intensity(&self) -> f32;
251
252 fn affects_lightmapped_mesh_diffuse(&self) -> bool;
255
256 fn create_render_view_light_probes(
261 view_component: Option<&Self>,
262 image_assets: &RenderAssets<GpuImage>,
263 ) -> RenderViewLightProbes<Self>;
264}
265
266#[derive(Component, ShaderType, Clone)]
269pub struct EnvironmentMapUniform {
270 transform: Mat4,
272}
273
274impl Default for EnvironmentMapUniform {
275 fn default() -> Self {
276 EnvironmentMapUniform {
277 transform: Mat4::IDENTITY,
278 }
279 }
280}
281
282#[derive(Resource, Default, Deref, DerefMut)]
284pub struct EnvironmentMapUniformBuffer(pub DynamicUniformBuffer<EnvironmentMapUniform>);
285
286#[derive(Component, Default, Deref, DerefMut)]
289pub struct ViewEnvironmentMapUniformOffset(u32);
290
291impl Plugin for LightProbePlugin {
292 fn build(&self, app: &mut App) {
293 load_shader_library!(app, "light_probe.wgsl");
294 load_shader_library!(app, "environment_map.wgsl");
295 load_shader_library!(app, "irradiance_volume.wgsl");
296
297 app.add_plugins((
298 EnvironmentMapGenerationPlugin,
299 ExtractInstancesPlugin::<EnvironmentMapIds>::new(),
300 ));
301
302 let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
303 return;
304 };
305
306 render_app
307 .init_resource::<LightProbesBuffer>()
308 .init_resource::<EnvironmentMapUniformBuffer>()
309 .add_systems(ExtractSchedule, gather_environment_map_uniform)
310 .add_systems(ExtractSchedule, gather_light_probes::<EnvironmentMapLight>)
311 .add_systems(ExtractSchedule, gather_light_probes::<IrradianceVolume>)
312 .add_systems(
313 Render,
314 (upload_light_probes, prepare_environment_uniform_buffer)
315 .in_set(RenderSystems::PrepareResources),
316 );
317 }
318}
319
320fn gather_environment_map_uniform(
325 view_query: Extract<Query<(RenderEntity, Option<&EnvironmentMapLight>), With<Camera3d>>>,
326 mut commands: Commands,
327) {
328 for (view_entity, environment_map_light) in view_query.iter() {
329 let environment_map_uniform = if let Some(environment_map_light) = environment_map_light {
330 EnvironmentMapUniform {
331 transform: Transform::from_rotation(environment_map_light.rotation)
332 .to_matrix()
333 .inverse(),
334 }
335 } else {
336 EnvironmentMapUniform::default()
337 };
338 commands
339 .get_entity(view_entity)
340 .expect("Environment map light entity wasn't synced.")
341 .insert(environment_map_uniform);
342 }
343}
344
345fn gather_light_probes<C>(
348 image_assets: Res<RenderAssets<GpuImage>>,
349 light_probe_query: Extract<Query<(&GlobalTransform, &C), With<LightProbe>>>,
350 view_query: Extract<
351 Query<(RenderEntity, &GlobalTransform, &Frustum, Option<&C>), With<Camera3d>>,
352 >,
353 mut reflection_probes: Local<Vec<LightProbeInfo<C>>>,
354 mut view_reflection_probes: Local<Vec<LightProbeInfo<C>>>,
355 mut commands: Commands,
356) where
357 C: LightProbeComponent,
358{
359 reflection_probes.clear();
361 reflection_probes.extend(
362 light_probe_query
363 .iter()
364 .filter_map(|query_row| LightProbeInfo::new(query_row, &image_assets)),
365 );
366 for (view_entity, view_transform, view_frustum, view_component) in view_query.iter() {
368 view_reflection_probes.clear();
370 view_reflection_probes.extend(
371 reflection_probes
372 .iter()
373 .filter(|light_probe_info| light_probe_info.frustum_cull(view_frustum))
374 .cloned(),
375 );
376
377 view_reflection_probes.sort_by_cached_key(|light_probe_info| {
379 light_probe_info.camera_distance_sort_key(view_transform)
380 });
381
382 let mut render_view_light_probes =
384 C::create_render_view_light_probes(view_component, &image_assets);
385
386 render_view_light_probes.maybe_gather_light_probes(&view_reflection_probes);
388
389 if render_view_light_probes.is_empty() {
391 commands
392 .get_entity(view_entity)
393 .expect("View entity wasn't synced.")
394 .remove::<RenderViewLightProbes<C>>();
395 } else {
396 commands
397 .get_entity(view_entity)
398 .expect("View entity wasn't synced.")
399 .insert(render_view_light_probes);
400 }
401 }
402}
403
404pub fn prepare_environment_uniform_buffer(
407 mut commands: Commands,
408 views: Query<(Entity, Option<&EnvironmentMapUniform>), With<ExtractedView>>,
409 mut environment_uniform_buffer: ResMut<EnvironmentMapUniformBuffer>,
410 render_device: Res<RenderDevice>,
411 render_queue: Res<RenderQueue>,
412) {
413 let Some(mut writer) =
414 environment_uniform_buffer.get_writer(views.iter().len(), &render_device, &render_queue)
415 else {
416 return;
417 };
418
419 for (view, environment_uniform) in views.iter() {
420 let uniform_offset = match environment_uniform {
421 None => 0,
422 Some(environment_uniform) => writer.write(environment_uniform),
423 };
424 commands
425 .entity(view)
426 .insert(ViewEnvironmentMapUniformOffset(uniform_offset));
427 }
428}
429
430fn upload_light_probes(
437 mut commands: Commands,
438 views: Query<Entity, With<ExtractedView>>,
439 mut light_probes_buffer: ResMut<LightProbesBuffer>,
440 mut view_light_probes_query: Query<(
441 Option<&RenderViewLightProbes<EnvironmentMapLight>>,
442 Option<&RenderViewLightProbes<IrradianceVolume>>,
443 )>,
444 render_device: Res<RenderDevice>,
445 render_queue: Res<RenderQueue>,
446) {
447 if views.is_empty() {
449 return;
450 }
451
452 let mut writer = light_probes_buffer
454 .get_writer(views.iter().len(), &render_device, &render_queue)
455 .unwrap();
456
457 for view_entity in views.iter() {
459 let Ok((render_view_environment_maps, render_view_irradiance_volumes)) =
460 view_light_probes_query.get_mut(view_entity)
461 else {
462 error!("Failed to find `RenderViewLightProbes` for the view!");
463 continue;
464 };
465
466 let mut light_probes_uniform = LightProbesUniform {
469 reflection_probes: [RenderLightProbe::default(); MAX_VIEW_LIGHT_PROBES],
470 irradiance_volumes: [RenderLightProbe::default(); MAX_VIEW_LIGHT_PROBES],
471 reflection_probe_count: render_view_environment_maps
472 .map(RenderViewLightProbes::len)
473 .unwrap_or_default()
474 .min(MAX_VIEW_LIGHT_PROBES) as i32,
475 irradiance_volume_count: render_view_irradiance_volumes
476 .map(RenderViewLightProbes::len)
477 .unwrap_or_default()
478 .min(MAX_VIEW_LIGHT_PROBES) as i32,
479 view_cubemap_index: render_view_environment_maps
480 .map(|maps| maps.view_light_probe_info.cubemap_index)
481 .unwrap_or(-1),
482 smallest_specular_mip_level_for_view: render_view_environment_maps
483 .map(|maps| maps.view_light_probe_info.smallest_specular_mip_level)
484 .unwrap_or(0),
485 intensity_for_view: render_view_environment_maps
486 .map(|maps| maps.view_light_probe_info.intensity)
487 .unwrap_or(1.0),
488 view_environment_map_affects_lightmapped_mesh_diffuse: render_view_environment_maps
489 .map(|maps| maps.view_light_probe_info.affects_lightmapped_mesh_diffuse as u32)
490 .unwrap_or(1),
491 };
492
493 if let Some(render_view_environment_maps) = render_view_environment_maps {
496 render_view_environment_maps.add_to_uniform(
497 &mut light_probes_uniform.reflection_probes,
498 &mut light_probes_uniform.reflection_probe_count,
499 );
500 }
501
502 if let Some(render_view_irradiance_volumes) = render_view_irradiance_volumes {
505 render_view_irradiance_volumes.add_to_uniform(
506 &mut light_probes_uniform.irradiance_volumes,
507 &mut light_probes_uniform.irradiance_volume_count,
508 );
509 }
510
511 let uniform_offset = writer.write(&light_probes_uniform);
513
514 commands
515 .entity(view_entity)
516 .insert(ViewLightProbesUniformOffset(uniform_offset));
517 }
518}
519
520impl Default for LightProbesUniform {
521 fn default() -> Self {
522 Self {
523 reflection_probes: [RenderLightProbe::default(); MAX_VIEW_LIGHT_PROBES],
524 irradiance_volumes: [RenderLightProbe::default(); MAX_VIEW_LIGHT_PROBES],
525 reflection_probe_count: 0,
526 irradiance_volume_count: 0,
527 view_cubemap_index: -1,
528 smallest_specular_mip_level_for_view: 0,
529 intensity_for_view: 1.0,
530 view_environment_map_affects_lightmapped_mesh_diffuse: 1,
531 }
532 }
533}
534
535impl<C> LightProbeInfo<C>
536where
537 C: LightProbeComponent,
538{
539 fn new(
543 (light_probe_transform, environment_map): (&GlobalTransform, &C),
544 image_assets: &RenderAssets<GpuImage>,
545 ) -> Option<LightProbeInfo<C>> {
546 let light_from_world_transposed =
547 Mat4::from(light_probe_transform.affine().inverse()).transpose();
548 environment_map.id(image_assets).map(|id| LightProbeInfo {
549 world_from_light: light_probe_transform.affine(),
550 light_from_world: [
551 light_from_world_transposed.x_axis,
552 light_from_world_transposed.y_axis,
553 light_from_world_transposed.z_axis,
554 ],
555 asset_id: id,
556 intensity: environment_map.intensity(),
557 affects_lightmapped_mesh_diffuse: environment_map.affects_lightmapped_mesh_diffuse(),
558 })
559 }
560
561 fn frustum_cull(&self, view_frustum: &Frustum) -> bool {
564 view_frustum.intersects_obb(
565 &Aabb {
566 center: Vec3A::default(),
567 half_extents: Vec3A::splat(0.5),
568 },
569 &self.world_from_light,
570 true,
571 false,
572 )
573 }
574
575 fn camera_distance_sort_key(&self, view_transform: &GlobalTransform) -> FloatOrd {
578 FloatOrd(
579 (self.world_from_light.translation - view_transform.translation_vec3a())
580 .length_squared(),
581 )
582 }
583}
584
585impl<C> RenderViewLightProbes<C>
586where
587 C: LightProbeComponent,
588{
589 fn new() -> RenderViewLightProbes<C> {
591 RenderViewLightProbes {
592 binding_index_to_textures: vec![],
593 cubemap_to_binding_index: HashMap::default(),
594 render_light_probes: vec![],
595 view_light_probe_info: C::ViewLightProbeInfo::default(),
596 }
597 }
598
599 pub(crate) fn is_empty(&self) -> bool {
601 self.binding_index_to_textures.is_empty()
602 }
603
604 pub(crate) fn len(&self) -> usize {
606 self.binding_index_to_textures.len()
607 }
608
609 pub(crate) fn get_or_insert_cubemap(&mut self, cubemap_id: &C::AssetId) -> u32 {
612 *self
613 .cubemap_to_binding_index
614 .entry((*cubemap_id).clone())
615 .or_insert_with(|| {
616 let index = self.binding_index_to_textures.len() as u32;
617 self.binding_index_to_textures.push((*cubemap_id).clone());
618 index
619 })
620 }
621
622 fn add_to_uniform(
625 &self,
626 render_light_probes: &mut [RenderLightProbe; MAX_VIEW_LIGHT_PROBES],
627 render_light_probe_count: &mut i32,
628 ) {
629 render_light_probes[0..self.render_light_probes.len()]
630 .copy_from_slice(&self.render_light_probes[..]);
631 *render_light_probe_count = self.render_light_probes.len() as i32;
632 }
633
634 fn maybe_gather_light_probes(&mut self, light_probes: &[LightProbeInfo<C>]) {
637 for light_probe in light_probes.iter().take(MAX_VIEW_LIGHT_PROBES) {
638 let cubemap_index = self.get_or_insert_cubemap(&light_probe.asset_id);
640
641 self.render_light_probes.push(RenderLightProbe {
643 light_from_world_transposed: light_probe.light_from_world,
644 texture_index: cubemap_index as i32,
645 intensity: light_probe.intensity,
646 affects_lightmapped_mesh_diffuse: light_probe.affects_lightmapped_mesh_diffuse
647 as u32,
648 });
649 }
650 }
651}
652
653impl<C> Clone for LightProbeInfo<C>
654where
655 C: LightProbeComponent,
656{
657 fn clone(&self) -> Self {
658 Self {
659 light_from_world: self.light_from_world,
660 world_from_light: self.world_from_light,
661 intensity: self.intensity,
662 affects_lightmapped_mesh_diffuse: self.affects_lightmapped_mesh_diffuse,
663 asset_id: self.asset_id.clone(),
664 }
665 }
666}
667
668pub(crate) fn add_cubemap_texture_view<'a>(
671 texture_views: &mut Vec<&'a <TextureView as Deref>::Target>,
672 sampler: &mut Option<&'a Sampler>,
673 image_id: AssetId<Image>,
674 images: &'a RenderAssets<GpuImage>,
675 fallback_image: &'a FallbackImage,
676) {
677 match images.get(image_id) {
678 None => {
679 texture_views.push(&*fallback_image.cube.texture_view);
681 }
682 Some(image) => {
683 if sampler.is_none() {
685 *sampler = Some(&image.sampler);
686 }
687
688 texture_views.push(&*image.texture_view);
689 }
690 }
691}
692
693pub(crate) fn binding_arrays_are_usable(
717 render_device: &RenderDevice,
718 render_adapter: &RenderAdapter,
719) -> bool {
720 let adapter_info = RenderAdapterInfo(WgpuWrapper::new(render_adapter.get_info()));
721
722 !cfg!(feature = "shader_format_glsl")
723 && bevy_render::get_adreno_model(&adapter_info).is_none_or(|model| model > 610)
724 && render_device.limits().max_storage_textures_per_shader_stage
725 >= (STANDARD_MATERIAL_FRAGMENT_SHADER_MIN_TEXTURE_BINDINGS + MAX_VIEW_LIGHT_PROBES)
726 as u32
727 && render_device.features().contains(
728 WgpuFeatures::TEXTURE_BINDING_ARRAY
729 | WgpuFeatures::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING,
730 )
731}