bevy_pbr/light_probe/
environment_map.rs

1//! Environment maps and reflection probes.
2//!
3//! An *environment map* consists of a pair of diffuse and specular cubemaps
4//! that together reflect the static surrounding area of a region in space. When
5//! available, the PBR shader uses these to apply diffuse light and calculate
6//! specular reflections.
7//!
8//! Environment maps come in two flavors, depending on what other components the
9//! entities they're attached to have:
10//!
11//! 1. If attached to a view, they represent the objects located a very far
12//!    distance from the view, in a similar manner to a skybox. Essentially, these
13//!    *view environment maps* represent a higher-quality replacement for
14//!    [`AmbientLight`](bevy_light::AmbientLight) for outdoor scenes. The indirect light from such
15//!    environment maps are added to every point of the scene, including
16//!    interior enclosed areas.
17//!
18//! 2. If attached to a [`bevy_light::LightProbe`], environment maps represent the immediate
19//!    surroundings of a specific location in the scene. These types of
20//!    environment maps are known as *reflection probes*.
21//!
22//! Typically, environment maps are static (i.e. "baked", calculated ahead of
23//! time) and so only reflect fixed static geometry. The environment maps must
24//! be pre-filtered into a pair of cubemaps, one for the diffuse component and
25//! one for the specular component, according to the [split-sum approximation].
26//! To pre-filter your environment map, you can use the [glTF IBL Sampler] or
27//! its [artist-friendly UI]. The diffuse map uses the Lambertian distribution,
28//! while the specular map uses the GGX distribution.
29//!
30//! The Khronos Group has [several pre-filtered environment maps] available for
31//! you to use.
32//!
33//! Currently, reflection probes (i.e. environment maps attached to light
34//! probes) use binding arrays (also known as bindless textures) and
35//! consequently aren't supported on WebGL2 or WebGPU. Reflection probes are
36//! also unsupported if GLSL is in use, due to `naga` limitations. Environment
37//! maps attached to views are, however, supported on all platforms.
38//!
39//! [split-sum approximation]: https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf
40//!
41//! [glTF IBL Sampler]: https://github.com/KhronosGroup/glTF-IBL-Sampler
42//!
43//! [artist-friendly UI]: https://github.com/pcwalton/gltf-ibl-sampler-egui
44//!
45//! [several pre-filtered environment maps]: https://github.com/KhronosGroup/glTF-Sample-Environments
46
47use bevy_asset::AssetId;
48use bevy_ecs::{query::QueryItem, system::lifetimeless::Read};
49use bevy_image::Image;
50use bevy_light::EnvironmentMapLight;
51use bevy_render::{
52    extract_instances::ExtractInstance,
53    render_asset::RenderAssets,
54    render_resource::{
55        binding_types::{self, uniform_buffer},
56        BindGroupLayoutEntryBuilder, Sampler, SamplerBindingType, ShaderStages, TextureSampleType,
57        TextureView,
58    },
59    renderer::{RenderAdapter, RenderDevice},
60    texture::{FallbackImage, GpuImage},
61};
62
63use core::{num::NonZero, ops::Deref};
64
65use crate::{
66    add_cubemap_texture_view, binding_arrays_are_usable, EnvironmentMapUniform,
67    MAX_VIEW_LIGHT_PROBES,
68};
69
70use super::{LightProbeComponent, RenderViewLightProbes};
71
72/// Like [`EnvironmentMapLight`], but contains asset IDs instead of handles.
73///
74/// This is for use in the render app.
75#[derive(Clone, Copy, PartialEq, Eq, Hash)]
76pub struct EnvironmentMapIds {
77    /// The blurry image that represents diffuse radiance surrounding a region.
78    pub(crate) diffuse: AssetId<Image>,
79    /// The typically-sharper, mipmapped image that represents specular radiance
80    /// surrounding a region.
81    pub(crate) specular: AssetId<Image>,
82}
83
84/// All the bind group entries necessary for PBR shaders to access the
85/// environment maps exposed to a view.
86pub(crate) enum RenderViewEnvironmentMapBindGroupEntries<'a> {
87    /// The version used when binding arrays aren't available on the current
88    /// platform.
89    Single {
90        /// The texture view of the view's diffuse cubemap.
91        diffuse_texture_view: &'a TextureView,
92
93        /// The texture view of the view's specular cubemap.
94        specular_texture_view: &'a TextureView,
95
96        /// The sampler used to sample elements of both `diffuse_texture_views` and
97        /// `specular_texture_views`.
98        sampler: &'a Sampler,
99    },
100
101    /// The version used when binding arrays are available on the current
102    /// platform.
103    Multiple {
104        /// A texture view of each diffuse cubemap, in the same order that they are
105        /// supplied to the view (i.e. in the same order as
106        /// `binding_index_to_cubemap` in [`RenderViewLightProbes`]).
107        ///
108        /// This is a vector of `wgpu::TextureView`s. But we don't want to import
109        /// `wgpu` in this crate, so we refer to it indirectly like this.
110        diffuse_texture_views: Vec<&'a <TextureView as Deref>::Target>,
111
112        /// As above, but for specular cubemaps.
113        specular_texture_views: Vec<&'a <TextureView as Deref>::Target>,
114
115        /// The sampler used to sample elements of both `diffuse_texture_views` and
116        /// `specular_texture_views`.
117        sampler: &'a Sampler,
118    },
119}
120
121/// Information about the environment map attached to the view, if any. This is
122/// a global environment map that lights everything visible in the view, as
123/// opposed to a light probe which affects only a specific area.
124pub struct EnvironmentMapViewLightProbeInfo {
125    /// The index of the diffuse and specular cubemaps in the binding arrays.
126    pub(crate) cubemap_index: i32,
127    /// The smallest mip level of the specular cubemap.
128    pub(crate) smallest_specular_mip_level: u32,
129    /// The scale factor applied to the diffuse and specular light in the
130    /// cubemap. This is in units of cd/m² (candela per square meter).
131    pub(crate) intensity: f32,
132    /// Whether this lightmap affects the diffuse lighting of lightmapped
133    /// meshes.
134    pub(crate) affects_lightmapped_mesh_diffuse: bool,
135}
136
137impl ExtractInstance for EnvironmentMapIds {
138    type QueryData = Read<EnvironmentMapLight>;
139
140    type QueryFilter = ();
141
142    fn extract(item: QueryItem<'_, '_, Self::QueryData>) -> Option<Self> {
143        Some(EnvironmentMapIds {
144            diffuse: item.diffuse_map.id(),
145            specular: item.specular_map.id(),
146        })
147    }
148}
149
150/// Returns the bind group layout entries for the environment map diffuse and
151/// specular binding arrays respectively, in addition to the sampler.
152pub(crate) fn get_bind_group_layout_entries(
153    render_device: &RenderDevice,
154    render_adapter: &RenderAdapter,
155) -> [BindGroupLayoutEntryBuilder; 4] {
156    let mut texture_cube_binding =
157        binding_types::texture_cube(TextureSampleType::Float { filterable: true });
158    if binding_arrays_are_usable(render_device, render_adapter) {
159        texture_cube_binding =
160            texture_cube_binding.count(NonZero::<u32>::new(MAX_VIEW_LIGHT_PROBES as _).unwrap());
161    }
162
163    [
164        texture_cube_binding,
165        texture_cube_binding,
166        binding_types::sampler(SamplerBindingType::Filtering),
167        uniform_buffer::<EnvironmentMapUniform>(true).visibility(ShaderStages::FRAGMENT),
168    ]
169}
170
171impl<'a> RenderViewEnvironmentMapBindGroupEntries<'a> {
172    /// Looks up and returns the bindings for the environment map diffuse and
173    /// specular binding arrays respectively, as well as the sampler.
174    pub(crate) fn get(
175        render_view_environment_maps: Option<&RenderViewLightProbes<EnvironmentMapLight>>,
176        images: &'a RenderAssets<GpuImage>,
177        fallback_image: &'a FallbackImage,
178        render_device: &RenderDevice,
179        render_adapter: &RenderAdapter,
180    ) -> RenderViewEnvironmentMapBindGroupEntries<'a> {
181        if binding_arrays_are_usable(render_device, render_adapter) {
182            let mut diffuse_texture_views = vec![];
183            let mut specular_texture_views = vec![];
184            let mut sampler = None;
185
186            if let Some(environment_maps) = render_view_environment_maps {
187                for &cubemap_id in &environment_maps.binding_index_to_textures {
188                    add_cubemap_texture_view(
189                        &mut diffuse_texture_views,
190                        &mut sampler,
191                        cubemap_id.diffuse,
192                        images,
193                        fallback_image,
194                    );
195                    add_cubemap_texture_view(
196                        &mut specular_texture_views,
197                        &mut sampler,
198                        cubemap_id.specular,
199                        images,
200                        fallback_image,
201                    );
202                }
203            }
204
205            // Pad out the bindings to the size of the binding array using fallback
206            // textures. This is necessary on D3D12 and Metal.
207            diffuse_texture_views.resize(MAX_VIEW_LIGHT_PROBES, &*fallback_image.cube.texture_view);
208            specular_texture_views
209                .resize(MAX_VIEW_LIGHT_PROBES, &*fallback_image.cube.texture_view);
210
211            return RenderViewEnvironmentMapBindGroupEntries::Multiple {
212                diffuse_texture_views,
213                specular_texture_views,
214                sampler: sampler.unwrap_or(&fallback_image.cube.sampler),
215            };
216        }
217
218        if let Some(environment_maps) = render_view_environment_maps
219            && let Some(cubemap) = environment_maps.binding_index_to_textures.first()
220            && let (Some(diffuse_image), Some(specular_image)) =
221                (images.get(cubemap.diffuse), images.get(cubemap.specular))
222        {
223            return RenderViewEnvironmentMapBindGroupEntries::Single {
224                diffuse_texture_view: &diffuse_image.texture_view,
225                specular_texture_view: &specular_image.texture_view,
226                sampler: &diffuse_image.sampler,
227            };
228        }
229
230        RenderViewEnvironmentMapBindGroupEntries::Single {
231            diffuse_texture_view: &fallback_image.cube.texture_view,
232            specular_texture_view: &fallback_image.cube.texture_view,
233            sampler: &fallback_image.cube.sampler,
234        }
235    }
236}
237
238impl LightProbeComponent for EnvironmentMapLight {
239    type AssetId = EnvironmentMapIds;
240
241    // Information needed to render with the environment map attached to the
242    // view.
243    type ViewLightProbeInfo = EnvironmentMapViewLightProbeInfo;
244
245    fn id(&self, image_assets: &RenderAssets<GpuImage>) -> Option<Self::AssetId> {
246        if image_assets.get(&self.diffuse_map).is_none()
247            || image_assets.get(&self.specular_map).is_none()
248        {
249            None
250        } else {
251            Some(EnvironmentMapIds {
252                diffuse: self.diffuse_map.id(),
253                specular: self.specular_map.id(),
254            })
255        }
256    }
257
258    fn intensity(&self) -> f32 {
259        self.intensity
260    }
261
262    fn affects_lightmapped_mesh_diffuse(&self) -> bool {
263        self.affects_lightmapped_mesh_diffuse
264    }
265
266    fn create_render_view_light_probes(
267        view_component: Option<&EnvironmentMapLight>,
268        image_assets: &RenderAssets<GpuImage>,
269    ) -> RenderViewLightProbes<Self> {
270        let mut render_view_light_probes = RenderViewLightProbes::new();
271
272        // Find the index of the cubemap associated with the view, and determine
273        // its smallest mip level.
274        if let Some(EnvironmentMapLight {
275            diffuse_map: diffuse_map_handle,
276            specular_map: specular_map_handle,
277            intensity,
278            affects_lightmapped_mesh_diffuse,
279            ..
280        }) = view_component
281            && let (Some(_), Some(specular_map)) = (
282                image_assets.get(diffuse_map_handle),
283                image_assets.get(specular_map_handle),
284            )
285        {
286            render_view_light_probes.view_light_probe_info = EnvironmentMapViewLightProbeInfo {
287                cubemap_index: render_view_light_probes.get_or_insert_cubemap(&EnvironmentMapIds {
288                    diffuse: diffuse_map_handle.id(),
289                    specular: specular_map_handle.id(),
290                }) as i32,
291                smallest_specular_mip_level: specular_map.mip_level_count - 1,
292                intensity: *intensity,
293                affects_lightmapped_mesh_diffuse: *affects_lightmapped_mesh_diffuse,
294            };
295        };
296
297        render_view_light_probes
298    }
299}
300
301impl Default for EnvironmentMapViewLightProbeInfo {
302    fn default() -> Self {
303        Self {
304            cubemap_index: -1,
305            smallest_specular_mip_level: 0,
306            intensity: 1.0,
307            affects_lightmapped_mesh_diffuse: true,
308        }
309    }
310}