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