Skip to main content

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::{
49    query::{QueryData, QueryItem},
50    system::lifetimeless::Read,
51};
52use bevy_image::Image;
53use bevy_light::{EnvironmentMapLight, ParallaxCorrection};
54use bevy_math::{Affine3A, Quat, Vec3};
55use bevy_render::{
56    extract_instances::ExtractInstance,
57    render_asset::RenderAssets,
58    render_resource::{
59        binding_types, BindGroupLayoutEntryBuilder, Sampler, SamplerBindingType, TextureSampleType,
60        TextureView,
61    },
62    renderer::{RenderAdapter, RenderDevice},
63    texture::{FallbackImage, GpuImage},
64};
65
66use core::{num::NonZero, ops::Deref};
67
68use crate::{
69    add_cubemap_texture_view, binding_arrays_are_usable, RenderLightProbeFlags,
70    MAX_VIEW_LIGHT_PROBES,
71};
72
73use super::{LightProbeComponent, RenderViewLightProbes};
74
75/// Like [`EnvironmentMapLight`], but contains asset IDs instead of handles.
76///
77/// This is for use in the render app.
78#[derive(Clone, Copy, PartialEq, Eq, Hash)]
79pub struct EnvironmentMapIds {
80    /// The blurry image that represents diffuse radiance surrounding a region.
81    pub diffuse: AssetId<Image>,
82    /// The typically-sharper, mipmapped image that represents specular radiance
83    /// surrounding a region.
84    pub specular: AssetId<Image>,
85}
86
87/// All the bind group entries necessary for PBR shaders to access the
88/// environment maps exposed to a view.
89pub(crate) enum RenderViewEnvironmentMapBindGroupEntries<'a> {
90    /// The version used when binding arrays aren't available on the current
91    /// platform.
92    Single {
93        /// The texture view of the view's diffuse cubemap.
94        diffuse_texture_view: &'a TextureView,
95
96        /// The texture view of the view's specular cubemap.
97        specular_texture_view: &'a TextureView,
98
99        /// The sampler used to sample elements of both `diffuse_texture_views` and
100        /// `specular_texture_views`.
101        sampler: &'a Sampler,
102    },
103
104    /// The version used when binding arrays are available on the current
105    /// platform.
106    Multiple {
107        /// A texture view of each diffuse cubemap, in the same order that they are
108        /// supplied to the view (i.e. in the same order as
109        /// `binding_index_to_cubemap` in [`RenderViewLightProbes`]).
110        ///
111        /// This is a vector of `wgpu::TextureView`s. But we don't want to import
112        /// `wgpu` in this crate, so we refer to it indirectly like this.
113        diffuse_texture_views: Vec<&'a <TextureView as Deref>::Target>,
114
115        /// As above, but for specular cubemaps.
116        specular_texture_views: Vec<&'a <TextureView as Deref>::Target>,
117
118        /// The sampler used to sample elements of both `diffuse_texture_views` and
119        /// `specular_texture_views`.
120        sampler: &'a Sampler,
121    },
122}
123
124/// Information about the environment map attached to the view, if any. This is
125/// a global environment map that lights everything visible in the view, as
126/// opposed to a light probe which affects only a specific area.
127pub struct EnvironmentMapViewLightProbeInfo {
128    /// The index of the diffuse and specular cubemaps in the binding arrays.
129    pub(crate) cubemap_index: i32,
130    /// The smallest mip level of the specular cubemap.
131    pub(crate) smallest_specular_mip_level: u32,
132    /// The scale factor applied to the diffuse and specular light in the
133    /// cubemap. This is in units of cd/m² (candela per square meter).
134    pub(crate) intensity: f32,
135    /// Whether this lightmap affects the diffuse lighting of lightmapped
136    /// meshes.
137    pub(crate) affects_lightmapped_mesh_diffuse: bool,
138    /// World space rotation applied to the environment light cubemaps.
139    pub(crate) rotation: Quat,
140}
141
142impl ExtractInstance for EnvironmentMapIds {
143    type QueryData = Read<EnvironmentMapLight>;
144
145    type QueryFilter = ();
146
147    fn extract(item: QueryItem<'_, '_, Self::QueryData>) -> Option<Self> {
148        Some(EnvironmentMapIds {
149            diffuse: item.diffuse_map.id(),
150            specular: item.specular_map.id(),
151        })
152    }
153}
154
155/// Returns the bind group layout entries for the environment map diffuse and
156/// specular binding arrays respectively, in addition to the sampler.
157pub(crate) fn get_bind_group_layout_entries(
158    render_device: &RenderDevice,
159    render_adapter: &RenderAdapter,
160) -> [BindGroupLayoutEntryBuilder; 3] {
161    let mut texture_cube_binding =
162        binding_types::texture_cube(TextureSampleType::Float { filterable: true });
163    if binding_arrays_are_usable(render_device, render_adapter) {
164        texture_cube_binding =
165            texture_cube_binding.count(NonZero::<u32>::new(MAX_VIEW_LIGHT_PROBES as _).unwrap());
166    }
167
168    [
169        texture_cube_binding,
170        texture_cube_binding,
171        binding_types::sampler(SamplerBindingType::Filtering),
172    ]
173}
174
175impl<'a> RenderViewEnvironmentMapBindGroupEntries<'a> {
176    /// Looks up and returns the bindings for the environment map diffuse and
177    /// specular binding arrays respectively, as well as the sampler.
178    pub(crate) fn get(
179        render_view_environment_maps: Option<&RenderViewLightProbes<EnvironmentMapLight>>,
180        images: &'a RenderAssets<GpuImage>,
181        fallback_image: &'a FallbackImage,
182        render_device: &RenderDevice,
183        render_adapter: &RenderAdapter,
184    ) -> RenderViewEnvironmentMapBindGroupEntries<'a> {
185        if binding_arrays_are_usable(render_device, render_adapter) {
186            // Initialize the diffuse and specular texture views with the fallback texture.
187            let mut diffuse_texture_views = vec![];
188            let mut specular_texture_views = vec![];
189            let mut sampler = None;
190
191            if let Some(environment_maps) = render_view_environment_maps {
192                for &cubemap_id in &environment_maps.binding_index_to_textures {
193                    add_cubemap_texture_view(
194                        &mut diffuse_texture_views,
195                        &mut sampler,
196                        cubemap_id.diffuse,
197                        images,
198                        fallback_image,
199                    );
200                    add_cubemap_texture_view(
201                        &mut specular_texture_views,
202                        &mut sampler,
203                        cubemap_id.specular,
204                        images,
205                        fallback_image,
206                    );
207                }
208            }
209
210            // Pad out the bindings to the size of the binding array using fallback
211            // textures. This is necessary on D3D12 and Metal.
212            diffuse_texture_views.resize(MAX_VIEW_LIGHT_PROBES, &*fallback_image.cube.texture_view);
213            specular_texture_views
214                .resize(MAX_VIEW_LIGHT_PROBES, &*fallback_image.cube.texture_view);
215
216            return RenderViewEnvironmentMapBindGroupEntries::Multiple {
217                diffuse_texture_views,
218                specular_texture_views,
219                sampler: sampler.unwrap_or(&fallback_image.cube.sampler),
220            };
221        }
222
223        if let Some(environment_maps) = render_view_environment_maps
224            && let Some(cubemap) = environment_maps.binding_index_to_textures.first()
225            && let (Some(diffuse_image), Some(specular_image)) =
226                (images.get(cubemap.diffuse), images.get(cubemap.specular))
227        {
228            return RenderViewEnvironmentMapBindGroupEntries::Single {
229                diffuse_texture_view: &diffuse_image.texture_view,
230                specular_texture_view: &specular_image.texture_view,
231                sampler: &diffuse_image.sampler,
232            };
233        }
234
235        RenderViewEnvironmentMapBindGroupEntries::Single {
236            diffuse_texture_view: &fallback_image.cube.texture_view,
237            specular_texture_view: &fallback_image.cube.texture_view,
238            sampler: &fallback_image.cube.sampler,
239        }
240    }
241}
242
243impl LightProbeComponent for EnvironmentMapLight {
244    type AssetId = EnvironmentMapIds;
245
246    // Information needed to render with the environment map attached to the
247    // view.
248    type ViewLightProbeInfo = EnvironmentMapViewLightProbeInfo;
249
250    type QueryData = Option<Read<ParallaxCorrection>>;
251
252    fn id(&self, image_assets: &RenderAssets<GpuImage>) -> Option<Self::AssetId> {
253        if image_assets.get(&self.diffuse_map).is_none()
254            || image_assets.get(&self.specular_map).is_none()
255        {
256            None
257        } else {
258            Some(EnvironmentMapIds {
259                diffuse: self.diffuse_map.id(),
260                specular: self.specular_map.id(),
261            })
262        }
263    }
264
265    fn intensity(&self) -> f32 {
266        self.intensity
267    }
268
269    fn flags(
270        &self,
271        maybe_parallax_correction: &<Self::QueryData as QueryData>::Item<'_, '_>,
272    ) -> RenderLightProbeFlags {
273        let mut flags = RenderLightProbeFlags::empty();
274        if self.affects_lightmapped_mesh_diffuse {
275            flags.insert(RenderLightProbeFlags::AFFECTS_LIGHTMAPPED_MESH_DIFFUSE);
276        }
277        if maybe_parallax_correction.is_some_and(|parallax_correction| {
278            !matches!(*parallax_correction, ParallaxCorrection::None)
279        }) {
280            flags.insert(RenderLightProbeFlags::ENABLE_PARALLAX_CORRECTION);
281        }
282        flags
283    }
284
285    fn create_render_view_light_probes(
286        view_component: Option<&EnvironmentMapLight>,
287        image_assets: &RenderAssets<GpuImage>,
288    ) -> RenderViewLightProbes<Self> {
289        let mut render_view_light_probes = RenderViewLightProbes::new();
290
291        // Find the index of the cubemap associated with the view, and determine
292        // its smallest mip level.
293        if let Some(EnvironmentMapLight {
294            diffuse_map: diffuse_map_handle,
295            specular_map: specular_map_handle,
296            intensity,
297            affects_lightmapped_mesh_diffuse,
298            rotation,
299            ..
300        }) = view_component
301            && let (Some(_), Some(specular_map)) = (
302                image_assets.get(diffuse_map_handle),
303                image_assets.get(specular_map_handle),
304            )
305        {
306            render_view_light_probes.view_light_probe_info =
307                Some(EnvironmentMapViewLightProbeInfo {
308                    cubemap_index: render_view_light_probes.get_or_insert_cubemap(
309                        &EnvironmentMapIds {
310                            diffuse: diffuse_map_handle.id(),
311                            specular: specular_map_handle.id(),
312                        },
313                    ) as i32,
314                    smallest_specular_mip_level: specular_map.texture_descriptor.mip_level_count
315                        - 1,
316                    intensity: *intensity,
317                    affects_lightmapped_mesh_diffuse: *affects_lightmapped_mesh_diffuse,
318                    rotation: *rotation,
319                });
320        };
321
322        render_view_light_probes
323    }
324
325    fn get_world_from_light_matrix(&self, original_transform: &Affine3A) -> Affine3A {
326        // Take the `rotation` field into account.
327        *original_transform * Affine3A::from_quat(self.rotation)
328    }
329
330    fn parallax_correction_bounds(
331        &self,
332        maybe_parallax_correction: &<Self::QueryData as QueryData>::Item<'_, '_>,
333    ) -> Vec3 {
334        match *maybe_parallax_correction {
335            Some(&ParallaxCorrection::Custom(bounds)) => bounds,
336            Some(&ParallaxCorrection::Auto) => Vec3::splat(0.5),
337            Some(&ParallaxCorrection::None) | None => Vec3::ZERO,
338        }
339    }
340}
341
342impl Default for EnvironmentMapViewLightProbeInfo {
343    fn default() -> Self {
344        Self {
345            cubemap_index: -1,
346            smallest_specular_mip_level: 0,
347            intensity: 1.0,
348            affects_lightmapped_mesh_diffuse: true,
349            rotation: Quat::IDENTITY,
350        }
351    }
352}