Skip to main content

bevy_pbr/lightmap/
mod.rs

1//! Lightmaps, baked lighting textures that can be applied at runtime to provide
2//! diffuse global illumination.
3//!
4//! Bevy doesn't currently have any way to actually bake lightmaps, but they can
5//! be baked in an external tool like [Blender](http://blender.org), for example
6//! with an addon like [The Lightmapper]. The tools in the [`bevy-baked-gi`]
7//! project support other lightmap baking methods.
8//!
9//! When a [`Lightmap`] component is added to an entity with a [`Mesh3d`] and a
10//! [`MeshMaterial3d<StandardMaterial>`], Bevy applies the lightmap when rendering. The brightness
11//! of the lightmap may be controlled with the `lightmap_exposure` field on
12//! [`StandardMaterial`].
13//!
14//! During the rendering extraction phase, we extract all lightmaps into the
15//! [`RenderLightmaps`] table, which lives in the render world. Mesh bindgroup
16//! and mesh uniform creation consults this table to determine which lightmap to
17//! supply to the shader. Essentially, the lightmap is a special type of texture
18//! that is part of the mesh instance rather than part of the material (because
19//! multiple meshes can share the same material, whereas sharing lightmaps is
20//! nonsensical).
21//!
22//! Note that multiple meshes can't be drawn in a single drawcall if they use
23//! different lightmap textures, unless bindless textures are in use. If you
24//! want to instance a lightmapped mesh, and your platform doesn't support
25//! bindless textures, combine the lightmap textures into a single atlas, and
26//! set the `uv_rect` field on [`Lightmap`] appropriately.
27//!
28//! [The Lightmapper]: https://github.com/Naxela/The_Lightmapper
29//! [`Mesh3d`]: bevy_mesh::Mesh3d
30//! [`MeshMaterial3d<StandardMaterial>`]: crate::StandardMaterial
31//! [`StandardMaterial`]: crate::StandardMaterial
32//! [`bevy-baked-gi`]: https://github.com/pcwalton/bevy-baked-gi
33
34use bevy_app::{App, Plugin};
35use bevy_asset::{AssetId, Handle};
36use bevy_camera::visibility::ViewVisibility;
37use bevy_derive::{Deref, DerefMut};
38use bevy_ecs::{
39    component::Component,
40    entity::Entity,
41    lifecycle::RemovedComponents,
42    query::{Changed, Or},
43    reflect::ReflectComponent,
44    resource::Resource,
45    schedule::IntoScheduleConfigs,
46    system::{Commands, Query, Res, ResMut},
47    template::FromTemplate,
48};
49use bevy_image::Image;
50use bevy_math::{uvec2, vec4, Rect, UVec2};
51use bevy_platform::collections::HashSet;
52use bevy_reflect::{std_traits::ReflectDefault, Reflect};
53use bevy_render::{
54    render_asset::RenderAssets,
55    render_resource::{Sampler, TextureView, WgpuSampler, WgpuTextureView},
56    renderer::RenderAdapter,
57    sync_world::MainEntity,
58    texture::{FallbackImage, GpuImage},
59    Extract, ExtractSchedule, RenderApp, RenderStartup,
60};
61use bevy_render::{renderer::RenderDevice, sync_world::MainEntityHashMap};
62use bevy_utils::default;
63use fixedbitset::FixedBitSet;
64use nonmax::{NonMaxU16, NonMaxU32};
65use tracing::error;
66
67use crate::{binding_arrays_are_usable, MeshExtractionSystems};
68
69/// The number of lightmaps that we store in a single slab, if bindless textures
70/// are in use.
71///
72/// If bindless textures aren't in use, then only a single lightmap can be bound
73/// at a time.
74pub const LIGHTMAPS_PER_SLAB: usize = 4;
75
76/// A plugin that provides an implementation of lightmaps.
77pub struct LightmapPlugin;
78
79/// A component that applies baked indirect diffuse global illumination from a
80/// lightmap.
81///
82/// When assigned to an entity that contains a [`Mesh3d`](bevy_mesh::Mesh3d) and a
83/// [`MeshMaterial3d<StandardMaterial>`](crate::StandardMaterial), if the mesh
84/// has a second UV layer ([`ATTRIBUTE_UV_1`](bevy_mesh::Mesh::ATTRIBUTE_UV_1)),
85/// then the lightmap will render using those UVs.
86#[derive(Component, Clone, Reflect, FromTemplate)]
87#[reflect(Component, Default, Clone)]
88pub struct Lightmap {
89    /// The lightmap texture.
90    pub image: Handle<Image>,
91
92    /// The rectangle within the lightmap texture that the UVs are relative to.
93    ///
94    /// The top left coordinate is the `min` part of the rect, and the bottom
95    /// right coordinate is the `max` part of the rect. The rect ranges from (0,
96    /// 0) to (1, 1).
97    ///
98    /// This field allows lightmaps for a variety of meshes to be packed into a
99    /// single atlas.
100    pub uv_rect: Rect,
101
102    /// Whether bicubic sampling should be used for sampling this lightmap.
103    ///
104    /// Bicubic sampling is higher quality, but slower, and may lead to light leaks.
105    ///
106    /// If true, the lightmap texture's sampler must be set to [`bevy_image::ImageSampler::linear`].
107    pub bicubic_sampling: bool,
108}
109
110/// Lightmap data stored in the render world.
111///
112/// There is one of these per visible lightmapped mesh instance.
113#[derive(Debug)]
114pub(crate) struct RenderLightmap {
115    /// The rectangle within the lightmap texture that the UVs are relative to.
116    ///
117    /// The top left coordinate is the `min` part of the rect, and the bottom
118    /// right coordinate is the `max` part of the rect. The rect ranges from (0,
119    /// 0) to (1, 1).
120    pub(crate) uv_rect: Rect,
121
122    /// The index of the slab (i.e. binding array) in which the lightmap is
123    /// located.
124    pub(crate) slab_index: LightmapSlabIndex,
125
126    /// The index of the slot (i.e. element within the binding array) in which
127    /// the lightmap is located.
128    ///
129    /// If bindless lightmaps aren't in use, this will be 0.
130    pub(crate) slot_index: LightmapSlotIndex,
131
132    // Whether or not bicubic sampling should be used for this lightmap.
133    pub(crate) bicubic_sampling: bool,
134}
135
136/// Stores data for all lightmaps in the render world.
137///
138/// This is cleared and repopulated each frame during the `extract_lightmaps`
139/// system.
140#[derive(Resource)]
141pub struct RenderLightmaps {
142    /// The mapping from every lightmapped entity to its lightmap info.
143    ///
144    /// Entities without lightmaps, or for which the mesh or lightmap isn't
145    /// loaded, won't have entries in this table.
146    pub(crate) render_lightmaps: MainEntityHashMap<RenderLightmap>,
147
148    /// The slabs (binding arrays) containing the lightmaps.
149    pub(crate) slabs: Vec<LightmapSlab>,
150
151    free_slabs: FixedBitSet,
152
153    pending_lightmaps: HashSet<(LightmapSlabIndex, LightmapSlotIndex)>,
154
155    /// Whether bindless textures are supported on this platform.
156    pub(crate) bindless_supported: bool,
157}
158
159/// A binding array that contains lightmaps.
160///
161/// This will have a single binding if bindless lightmaps aren't in use.
162pub struct LightmapSlab {
163    /// The GPU images in this slab.
164    lightmaps: Vec<AllocatedLightmap>,
165    free_slots_bitmask: u32,
166}
167
168struct AllocatedLightmap {
169    gpu_image: GpuImage,
170    // This will only be present if the lightmap is allocated but not loaded.
171    asset_id: Option<AssetId<Image>>,
172}
173
174/// The index of the slab (binding array) in which a lightmap is located.
175#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Deref, DerefMut)]
176#[repr(transparent)]
177pub struct LightmapSlabIndex(pub(crate) NonMaxU32);
178
179/// The index of the slot (element within the binding array) in the slab in
180/// which a lightmap is located.
181#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Deref, DerefMut)]
182#[repr(transparent)]
183pub struct LightmapSlotIndex(pub(crate) NonMaxU16);
184
185impl Plugin for LightmapPlugin {
186    fn build(&self, app: &mut App) {
187        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
188            return;
189        };
190        render_app
191            .add_systems(RenderStartup, init_render_lightmaps)
192            .add_systems(
193                ExtractSchedule,
194                extract_lightmaps.after(MeshExtractionSystems),
195            );
196    }
197}
198
199/// Extracts all lightmaps from the scene and populates the [`RenderLightmaps`]
200/// resource.
201fn extract_lightmaps(
202    render_lightmaps: ResMut<RenderLightmaps>,
203    changed_lightmaps_query: Extract<
204        Query<
205            (Entity, &ViewVisibility, &Lightmap),
206            Or<(Changed<ViewVisibility>, Changed<Lightmap>)>,
207        >,
208    >,
209    mut removed_lightmaps_query: Extract<RemovedComponents<Lightmap>>,
210    images: Res<RenderAssets<GpuImage>>,
211    fallback_images: Res<FallbackImage>,
212) {
213    let render_lightmaps = render_lightmaps.into_inner();
214
215    // Loop over each entity.
216    for (entity, view_visibility, lightmap) in changed_lightmaps_query.iter() {
217        if render_lightmaps
218            .render_lightmaps
219            .contains_key(&MainEntity::from(entity))
220        {
221            continue;
222        }
223
224        // Only process visible entities.
225        if !view_visibility.get() {
226            continue;
227        }
228
229        let (slab_index, slot_index) =
230            render_lightmaps.allocate(&fallback_images, lightmap.image.id());
231        render_lightmaps.render_lightmaps.insert(
232            entity.into(),
233            RenderLightmap::new(
234                lightmap.uv_rect,
235                slab_index,
236                slot_index,
237                lightmap.bicubic_sampling,
238            ),
239        );
240
241        render_lightmaps
242            .pending_lightmaps
243            .insert((slab_index, slot_index));
244    }
245
246    for entity in removed_lightmaps_query.read() {
247        if changed_lightmaps_query.contains(entity) {
248            continue;
249        }
250
251        let Some(RenderLightmap {
252            slab_index,
253            slot_index,
254            ..
255        }) = render_lightmaps
256            .render_lightmaps
257            .remove(&MainEntity::from(entity))
258        else {
259            continue;
260        };
261
262        render_lightmaps.remove(&fallback_images, slab_index, slot_index);
263        render_lightmaps
264            .pending_lightmaps
265            .remove(&(slab_index, slot_index));
266    }
267
268    render_lightmaps
269        .pending_lightmaps
270        .retain(|&(slab_index, slot_index)| {
271            let Some(asset_id) = render_lightmaps.slabs[usize::from(slab_index)].lightmaps
272                [usize::from(slot_index)]
273            .asset_id
274            else {
275                error!(
276                    "Allocated lightmap should have been removed from `pending_lightmaps` by now"
277                );
278                return false;
279            };
280
281            let Some(gpu_image) = images.get(asset_id) else {
282                return true;
283            };
284            render_lightmaps.slabs[usize::from(slab_index)].insert(slot_index, gpu_image.clone());
285            false
286        });
287}
288
289impl RenderLightmap {
290    /// Creates a new lightmap from a texture, a UV rect, and a slab and slot
291    /// index pair.
292    fn new(
293        uv_rect: Rect,
294        slab_index: LightmapSlabIndex,
295        slot_index: LightmapSlotIndex,
296        bicubic_sampling: bool,
297    ) -> Self {
298        Self {
299            uv_rect,
300            slab_index,
301            slot_index,
302            bicubic_sampling,
303        }
304    }
305}
306
307/// Packs the lightmap UV rect into 64 bits (4 16-bit unsigned integers).
308pub(crate) fn pack_lightmap_uv_rect(maybe_rect: Option<Rect>) -> UVec2 {
309    match maybe_rect {
310        Some(rect) => {
311            let rect_uvec4 = (vec4(rect.min.x, rect.min.y, rect.max.x, rect.max.y) * 65535.0)
312                .round()
313                .as_uvec4();
314            uvec2(
315                rect_uvec4.x | (rect_uvec4.y << 16),
316                rect_uvec4.z | (rect_uvec4.w << 16),
317            )
318        }
319        None => UVec2::ZERO,
320    }
321}
322
323impl Default for Lightmap {
324    fn default() -> Self {
325        Self {
326            image: Default::default(),
327            uv_rect: Rect::new(0.0, 0.0, 1.0, 1.0),
328            bicubic_sampling: false,
329        }
330    }
331}
332
333pub fn init_render_lightmaps(
334    mut commands: Commands,
335    render_device: Res<RenderDevice>,
336    render_adapter: Res<RenderAdapter>,
337) {
338    let bindless_supported = binding_arrays_are_usable(&render_device, &render_adapter);
339
340    commands.insert_resource(RenderLightmaps {
341        render_lightmaps: default(),
342        slabs: vec![],
343        free_slabs: FixedBitSet::new(),
344        pending_lightmaps: default(),
345        bindless_supported,
346    });
347}
348
349impl RenderLightmaps {
350    /// Creates a new slab, appends it to the end of the list, and returns its
351    /// slab index.
352    fn create_slab(&mut self, fallback_images: &FallbackImage) -> LightmapSlabIndex {
353        let slab_index = LightmapSlabIndex::from(self.slabs.len());
354        self.free_slabs.grow_and_insert(slab_index.into());
355        self.slabs
356            .push(LightmapSlab::new(fallback_images, self.bindless_supported));
357        slab_index
358    }
359
360    fn allocate(
361        &mut self,
362        fallback_images: &FallbackImage,
363        image_id: AssetId<Image>,
364    ) -> (LightmapSlabIndex, LightmapSlotIndex) {
365        let slab_index = match self.free_slabs.minimum() {
366            None => self.create_slab(fallback_images),
367            Some(slab_index) => slab_index.into(),
368        };
369
370        let slab = &mut self.slabs[usize::from(slab_index)];
371        let slot_index = slab.allocate(image_id);
372        if slab.is_full() {
373            self.free_slabs.remove(slab_index.into());
374        }
375
376        (slab_index, slot_index)
377    }
378
379    fn remove(
380        &mut self,
381        fallback_images: &FallbackImage,
382        slab_index: LightmapSlabIndex,
383        slot_index: LightmapSlotIndex,
384    ) {
385        let slab = &mut self.slabs[usize::from(slab_index)];
386        slab.remove(fallback_images, slot_index);
387
388        if !slab.is_full() {
389            self.free_slabs.grow_and_insert(slab_index.into());
390        }
391    }
392}
393
394impl LightmapSlab {
395    fn new(fallback_images: &FallbackImage, bindless_supported: bool) -> LightmapSlab {
396        let count = if bindless_supported {
397            LIGHTMAPS_PER_SLAB
398        } else {
399            1
400        };
401
402        LightmapSlab {
403            lightmaps: (0..count)
404                .map(|_| AllocatedLightmap {
405                    gpu_image: fallback_images.d2.clone(),
406                    asset_id: None,
407                })
408                .collect(),
409            free_slots_bitmask: (1 << count) - 1,
410        }
411    }
412
413    fn is_full(&self) -> bool {
414        self.free_slots_bitmask == 0
415    }
416
417    fn allocate(&mut self, image_id: AssetId<Image>) -> LightmapSlotIndex {
418        assert!(
419            !self.is_full(),
420            "Attempting to allocate on a full lightmap slab"
421        );
422        let index = LightmapSlotIndex::from(self.free_slots_bitmask.trailing_zeros());
423        self.free_slots_bitmask &= !(1 << u32::from(index));
424        self.lightmaps[usize::from(index)].asset_id = Some(image_id);
425        index
426    }
427
428    fn insert(&mut self, index: LightmapSlotIndex, gpu_image: GpuImage) {
429        self.lightmaps[usize::from(index)] = AllocatedLightmap {
430            gpu_image,
431            asset_id: None,
432        }
433    }
434
435    fn remove(&mut self, fallback_images: &FallbackImage, index: LightmapSlotIndex) {
436        self.lightmaps[usize::from(index)] = AllocatedLightmap {
437            gpu_image: fallback_images.d2.clone(),
438            asset_id: None,
439        };
440        self.free_slots_bitmask |= 1 << u32::from(index);
441    }
442
443    /// Returns the texture views and samplers for the lightmaps in this slab,
444    /// ready to be placed into a bind group.
445    ///
446    /// This is used when constructing bind groups in bindless mode. Before
447    /// returning, this function pads out the arrays with fallback images in
448    /// order to fulfill requirements of platforms that require full binding
449    /// arrays (e.g. DX12).
450    pub(crate) fn build_binding_arrays(&self) -> (Vec<&WgpuTextureView>, Vec<&WgpuSampler>) {
451        (
452            self.lightmaps
453                .iter()
454                .map(|allocated_lightmap| &*allocated_lightmap.gpu_image.texture_view)
455                .collect(),
456            self.lightmaps
457                .iter()
458                .map(|allocated_lightmap| &*allocated_lightmap.gpu_image.sampler)
459                .collect(),
460        )
461    }
462
463    /// Returns the texture view and sampler corresponding to the first
464    /// lightmap, which must exist.
465    ///
466    /// This is used when constructing bind groups in non-bindless mode.
467    pub(crate) fn bindings_for_first_lightmap(&self) -> (&TextureView, &Sampler) {
468        (
469            &self.lightmaps[0].gpu_image.texture_view,
470            &self.lightmaps[0].gpu_image.sampler,
471        )
472    }
473}
474
475impl From<u32> for LightmapSlabIndex {
476    fn from(value: u32) -> Self {
477        Self(NonMaxU32::new(value).unwrap())
478    }
479}
480
481impl From<usize> for LightmapSlabIndex {
482    fn from(value: usize) -> Self {
483        Self::from(value as u32)
484    }
485}
486
487impl From<u32> for LightmapSlotIndex {
488    fn from(value: u32) -> Self {
489        Self(NonMaxU16::new(value as u16).unwrap())
490    }
491}
492
493impl From<usize> for LightmapSlotIndex {
494    fn from(value: usize) -> Self {
495        Self::from(value as u32)
496    }
497}
498
499impl From<LightmapSlabIndex> for usize {
500    fn from(value: LightmapSlabIndex) -> Self {
501        value.0.get() as usize
502    }
503}
504
505impl From<LightmapSlotIndex> for usize {
506    fn from(value: LightmapSlotIndex) -> Self {
507        value.0.get() as usize
508    }
509}
510
511impl From<LightmapSlotIndex> for u16 {
512    fn from(value: LightmapSlotIndex) -> Self {
513        value.0.get()
514    }
515}
516
517impl From<LightmapSlotIndex> for u32 {
518    fn from(value: LightmapSlotIndex) -> Self {
519        value.0.get() as u32
520    }
521}