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`](crate::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 [`crate::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::{weak_handle, AssetId, Handle};
48use bevy_ecs::{
49    component::Component, query::QueryItem, reflect::ReflectComponent, system::lifetimeless::Read,
50};
51use bevy_image::Image;
52use bevy_math::Quat;
53use bevy_reflect::{std_traits::ReflectDefault, Reflect};
54use bevy_render::{
55    extract_instances::ExtractInstance,
56    render_asset::RenderAssets,
57    render_resource::{
58        binding_types::{self, uniform_buffer},
59        BindGroupLayoutEntryBuilder, Sampler, SamplerBindingType, Shader, ShaderStages,
60        TextureSampleType, 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, EnvironmentMapUniform,
70    MAX_VIEW_LIGHT_PROBES,
71};
72
73use super::{LightProbeComponent, RenderViewLightProbes};
74
75/// A handle to the environment map helper shader.
76pub const ENVIRONMENT_MAP_SHADER_HANDLE: Handle<Shader> =
77    weak_handle!("d38c4ec4-e84c-468f-b485-bf44745db937");
78
79/// A pair of cubemap textures that represent the surroundings of a specific
80/// area in space.
81///
82/// See [`crate::environment_map`] for detailed information.
83#[derive(Clone, Component, Reflect)]
84#[reflect(Component, Default, Clone)]
85pub struct EnvironmentMapLight {
86    /// The blurry image that represents diffuse radiance surrounding a region.
87    pub diffuse_map: Handle<Image>,
88
89    /// The typically-sharper, mipmapped image that represents specular radiance
90    /// surrounding a region.
91    pub specular_map: Handle<Image>,
92
93    /// Scale factor applied to the diffuse and specular light generated by this component.
94    ///
95    /// After applying this multiplier, the resulting values should
96    /// be in units of [cd/m^2](https://en.wikipedia.org/wiki/Candela_per_square_metre).
97    ///
98    /// See also <https://google.github.io/filament/Filament.html#lighting/imagebasedlights/iblunit>.
99    pub intensity: f32,
100
101    /// World space rotation applied to the environment light cubemaps.
102    /// This is useful for users who require a different axis, such as the Z-axis, to serve
103    /// as the vertical axis.
104    pub rotation: Quat,
105
106    /// Whether the light from this environment map contributes diffuse lighting
107    /// to meshes with lightmaps.
108    ///
109    /// Set this to false if your lightmap baking tool bakes the diffuse light
110    /// from this environment light into the lightmaps in order to avoid
111    /// counting the radiance from this environment map twice.
112    ///
113    /// By default, this is set to true.
114    pub affects_lightmapped_mesh_diffuse: bool,
115}
116
117impl Default for EnvironmentMapLight {
118    fn default() -> Self {
119        EnvironmentMapLight {
120            diffuse_map: Handle::default(),
121            specular_map: Handle::default(),
122            intensity: 0.0,
123            rotation: Quat::IDENTITY,
124            affects_lightmapped_mesh_diffuse: true,
125        }
126    }
127}
128
129/// Like [`EnvironmentMapLight`], but contains asset IDs instead of handles.
130///
131/// This is for use in the render app.
132#[derive(Clone, Copy, PartialEq, Eq, Hash)]
133pub struct EnvironmentMapIds {
134    /// The blurry image that represents diffuse radiance surrounding a region.
135    pub(crate) diffuse: AssetId<Image>,
136    /// The typically-sharper, mipmapped image that represents specular radiance
137    /// surrounding a region.
138    pub(crate) specular: AssetId<Image>,
139}
140
141/// All the bind group entries necessary for PBR shaders to access the
142/// environment maps exposed to a view.
143pub(crate) enum RenderViewEnvironmentMapBindGroupEntries<'a> {
144    /// The version used when binding arrays aren't available on the current
145    /// platform.
146    Single {
147        /// The texture view of the view's diffuse cubemap.
148        diffuse_texture_view: &'a TextureView,
149
150        /// The texture view of the view's specular cubemap.
151        specular_texture_view: &'a TextureView,
152
153        /// The sampler used to sample elements of both `diffuse_texture_views` and
154        /// `specular_texture_views`.
155        sampler: &'a Sampler,
156    },
157
158    /// The version used when binding arrays are available on the current
159    /// platform.
160    Multiple {
161        /// A texture view of each diffuse cubemap, in the same order that they are
162        /// supplied to the view (i.e. in the same order as
163        /// `binding_index_to_cubemap` in [`RenderViewLightProbes`]).
164        ///
165        /// This is a vector of `wgpu::TextureView`s. But we don't want to import
166        /// `wgpu` in this crate, so we refer to it indirectly like this.
167        diffuse_texture_views: Vec<&'a <TextureView as Deref>::Target>,
168
169        /// As above, but for specular cubemaps.
170        specular_texture_views: Vec<&'a <TextureView as Deref>::Target>,
171
172        /// The sampler used to sample elements of both `diffuse_texture_views` and
173        /// `specular_texture_views`.
174        sampler: &'a Sampler,
175    },
176}
177
178/// Information about the environment map attached to the view, if any. This is
179/// a global environment map that lights everything visible in the view, as
180/// opposed to a light probe which affects only a specific area.
181pub struct EnvironmentMapViewLightProbeInfo {
182    /// The index of the diffuse and specular cubemaps in the binding arrays.
183    pub(crate) cubemap_index: i32,
184    /// The smallest mip level of the specular cubemap.
185    pub(crate) smallest_specular_mip_level: u32,
186    /// The scale factor applied to the diffuse and specular light in the
187    /// cubemap. This is in units of cd/m² (candela per square meter).
188    pub(crate) intensity: f32,
189    /// Whether this lightmap affects the diffuse lighting of lightmapped
190    /// meshes.
191    pub(crate) affects_lightmapped_mesh_diffuse: bool,
192}
193
194impl ExtractInstance for EnvironmentMapIds {
195    type QueryData = Read<EnvironmentMapLight>;
196
197    type QueryFilter = ();
198
199    fn extract(item: QueryItem<'_, Self::QueryData>) -> Option<Self> {
200        Some(EnvironmentMapIds {
201            diffuse: item.diffuse_map.id(),
202            specular: item.specular_map.id(),
203        })
204    }
205}
206
207/// Returns the bind group layout entries for the environment map diffuse and
208/// specular binding arrays respectively, in addition to the sampler.
209pub(crate) fn get_bind_group_layout_entries(
210    render_device: &RenderDevice,
211    render_adapter: &RenderAdapter,
212) -> [BindGroupLayoutEntryBuilder; 4] {
213    let mut texture_cube_binding =
214        binding_types::texture_cube(TextureSampleType::Float { filterable: true });
215    if binding_arrays_are_usable(render_device, render_adapter) {
216        texture_cube_binding =
217            texture_cube_binding.count(NonZero::<u32>::new(MAX_VIEW_LIGHT_PROBES as _).unwrap());
218    }
219
220    [
221        texture_cube_binding,
222        texture_cube_binding,
223        binding_types::sampler(SamplerBindingType::Filtering),
224        uniform_buffer::<EnvironmentMapUniform>(true).visibility(ShaderStages::FRAGMENT),
225    ]
226}
227
228impl<'a> RenderViewEnvironmentMapBindGroupEntries<'a> {
229    /// Looks up and returns the bindings for the environment map diffuse and
230    /// specular binding arrays respectively, as well as the sampler.
231    pub(crate) fn get(
232        render_view_environment_maps: Option<&RenderViewLightProbes<EnvironmentMapLight>>,
233        images: &'a RenderAssets<GpuImage>,
234        fallback_image: &'a FallbackImage,
235        render_device: &RenderDevice,
236        render_adapter: &RenderAdapter,
237    ) -> RenderViewEnvironmentMapBindGroupEntries<'a> {
238        if binding_arrays_are_usable(render_device, render_adapter) {
239            let mut diffuse_texture_views = vec![];
240            let mut specular_texture_views = vec![];
241            let mut sampler = None;
242
243            if let Some(environment_maps) = render_view_environment_maps {
244                for &cubemap_id in &environment_maps.binding_index_to_textures {
245                    add_cubemap_texture_view(
246                        &mut diffuse_texture_views,
247                        &mut sampler,
248                        cubemap_id.diffuse,
249                        images,
250                        fallback_image,
251                    );
252                    add_cubemap_texture_view(
253                        &mut specular_texture_views,
254                        &mut sampler,
255                        cubemap_id.specular,
256                        images,
257                        fallback_image,
258                    );
259                }
260            }
261
262            // Pad out the bindings to the size of the binding array using fallback
263            // textures. This is necessary on D3D12 and Metal.
264            diffuse_texture_views.resize(MAX_VIEW_LIGHT_PROBES, &*fallback_image.cube.texture_view);
265            specular_texture_views
266                .resize(MAX_VIEW_LIGHT_PROBES, &*fallback_image.cube.texture_view);
267
268            return RenderViewEnvironmentMapBindGroupEntries::Multiple {
269                diffuse_texture_views,
270                specular_texture_views,
271                sampler: sampler.unwrap_or(&fallback_image.cube.sampler),
272            };
273        }
274
275        if let Some(environment_maps) = render_view_environment_maps {
276            if let Some(cubemap) = environment_maps.binding_index_to_textures.first() {
277                if let (Some(diffuse_image), Some(specular_image)) =
278                    (images.get(cubemap.diffuse), images.get(cubemap.specular))
279                {
280                    return RenderViewEnvironmentMapBindGroupEntries::Single {
281                        diffuse_texture_view: &diffuse_image.texture_view,
282                        specular_texture_view: &specular_image.texture_view,
283                        sampler: &diffuse_image.sampler,
284                    };
285                }
286            }
287        }
288
289        RenderViewEnvironmentMapBindGroupEntries::Single {
290            diffuse_texture_view: &fallback_image.cube.texture_view,
291            specular_texture_view: &fallback_image.cube.texture_view,
292            sampler: &fallback_image.cube.sampler,
293        }
294    }
295}
296
297impl LightProbeComponent for EnvironmentMapLight {
298    type AssetId = EnvironmentMapIds;
299
300    // Information needed to render with the environment map attached to the
301    // view.
302    type ViewLightProbeInfo = EnvironmentMapViewLightProbeInfo;
303
304    fn id(&self, image_assets: &RenderAssets<GpuImage>) -> Option<Self::AssetId> {
305        if image_assets.get(&self.diffuse_map).is_none()
306            || image_assets.get(&self.specular_map).is_none()
307        {
308            None
309        } else {
310            Some(EnvironmentMapIds {
311                diffuse: self.diffuse_map.id(),
312                specular: self.specular_map.id(),
313            })
314        }
315    }
316
317    fn intensity(&self) -> f32 {
318        self.intensity
319    }
320
321    fn affects_lightmapped_mesh_diffuse(&self) -> bool {
322        self.affects_lightmapped_mesh_diffuse
323    }
324
325    fn create_render_view_light_probes(
326        view_component: Option<&EnvironmentMapLight>,
327        image_assets: &RenderAssets<GpuImage>,
328    ) -> RenderViewLightProbes<Self> {
329        let mut render_view_light_probes = RenderViewLightProbes::new();
330
331        // Find the index of the cubemap associated with the view, and determine
332        // its smallest mip level.
333        if let Some(EnvironmentMapLight {
334            diffuse_map: diffuse_map_handle,
335            specular_map: specular_map_handle,
336            intensity,
337            affects_lightmapped_mesh_diffuse,
338            ..
339        }) = view_component
340        {
341            if let (Some(_), Some(specular_map)) = (
342                image_assets.get(diffuse_map_handle),
343                image_assets.get(specular_map_handle),
344            ) {
345                render_view_light_probes.view_light_probe_info = EnvironmentMapViewLightProbeInfo {
346                    cubemap_index: render_view_light_probes.get_or_insert_cubemap(
347                        &EnvironmentMapIds {
348                            diffuse: diffuse_map_handle.id(),
349                            specular: specular_map_handle.id(),
350                        },
351                    ) as i32,
352                    smallest_specular_mip_level: specular_map.mip_level_count - 1,
353                    intensity: *intensity,
354                    affects_lightmapped_mesh_diffuse: *affects_lightmapped_mesh_diffuse,
355                };
356            }
357        };
358
359        render_view_light_probes
360    }
361}
362
363impl Default for EnvironmentMapViewLightProbeInfo {
364    fn default() -> Self {
365        Self {
366            cubemap_index: -1,
367            smallest_specular_mip_level: 0,
368            intensity: 1.0,
369            affects_lightmapped_mesh_diffuse: true,
370        }
371    }
372}