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};
48use bevy_image::Image;
49use bevy_math::{uvec2, vec4, Rect, UVec2};
50use bevy_platform::collections::HashSet;
51use bevy_reflect::{std_traits::ReflectDefault, Reflect};
52use bevy_render::{
53    render_asset::RenderAssets,
54    render_resource::{Sampler, TextureView, WgpuSampler, WgpuTextureView},
55    renderer::RenderAdapter,
56    sync_world::MainEntity,
57    texture::{FallbackImage, GpuImage},
58    Extract, ExtractSchedule, RenderApp, RenderStartup,
59};
60use bevy_render::{renderer::RenderDevice, sync_world::MainEntityHashMap};
61use bevy_shader::load_shader_library;
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)]
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        load_shader_library!(app, "lightmap.wgsl");
188
189        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
190            return;
191        };
192        render_app
193            .add_systems(RenderStartup, init_render_lightmaps)
194            .add_systems(
195                ExtractSchedule,
196                extract_lightmaps.after(MeshExtractionSystems),
197            );
198    }
199}
200
201/// Extracts all lightmaps from the scene and populates the [`RenderLightmaps`]
202/// resource.
203fn extract_lightmaps(
204    render_lightmaps: ResMut<RenderLightmaps>,
205    changed_lightmaps_query: Extract<
206        Query<
207            (Entity, &ViewVisibility, &Lightmap),
208            Or<(Changed<ViewVisibility>, Changed<Lightmap>)>,
209        >,
210    >,
211    mut removed_lightmaps_query: Extract<RemovedComponents<Lightmap>>,
212    images: Res<RenderAssets<GpuImage>>,
213    fallback_images: Res<FallbackImage>,
214) {
215    let render_lightmaps = render_lightmaps.into_inner();
216
217    // Loop over each entity.
218    for (entity, view_visibility, lightmap) in changed_lightmaps_query.iter() {
219        if render_lightmaps
220            .render_lightmaps
221            .contains_key(&MainEntity::from(entity))
222        {
223            continue;
224        }
225
226        // Only process visible entities.
227        if !view_visibility.get() {
228            continue;
229        }
230
231        let (slab_index, slot_index) =
232            render_lightmaps.allocate(&fallback_images, lightmap.image.id());
233        render_lightmaps.render_lightmaps.insert(
234            entity.into(),
235            RenderLightmap::new(
236                lightmap.uv_rect,
237                slab_index,
238                slot_index,
239                lightmap.bicubic_sampling,
240            ),
241        );
242
243        render_lightmaps
244            .pending_lightmaps
245            .insert((slab_index, slot_index));
246    }
247
248    for entity in removed_lightmaps_query.read() {
249        if changed_lightmaps_query.contains(entity) {
250            continue;
251        }
252
253        let Some(RenderLightmap {
254            slab_index,
255            slot_index,
256            ..
257        }) = render_lightmaps
258            .render_lightmaps
259            .remove(&MainEntity::from(entity))
260        else {
261            continue;
262        };
263
264        render_lightmaps.remove(&fallback_images, slab_index, slot_index);
265        render_lightmaps
266            .pending_lightmaps
267            .remove(&(slab_index, slot_index));
268    }
269
270    render_lightmaps
271        .pending_lightmaps
272        .retain(|&(slab_index, slot_index)| {
273            let Some(asset_id) = render_lightmaps.slabs[usize::from(slab_index)].lightmaps
274                [usize::from(slot_index)]
275            .asset_id
276            else {
277                error!(
278                    "Allocated lightmap should have been removed from `pending_lightmaps` by now"
279                );
280                return false;
281            };
282
283            let Some(gpu_image) = images.get(asset_id) else {
284                return true;
285            };
286            render_lightmaps.slabs[usize::from(slab_index)].insert(slot_index, gpu_image.clone());
287            false
288        });
289}
290
291impl RenderLightmap {
292    /// Creates a new lightmap from a texture, a UV rect, and a slab and slot
293    /// index pair.
294    fn new(
295        uv_rect: Rect,
296        slab_index: LightmapSlabIndex,
297        slot_index: LightmapSlotIndex,
298        bicubic_sampling: bool,
299    ) -> Self {
300        Self {
301            uv_rect,
302            slab_index,
303            slot_index,
304            bicubic_sampling,
305        }
306    }
307}
308
309/// Packs the lightmap UV rect into 64 bits (4 16-bit unsigned integers).
310pub(crate) fn pack_lightmap_uv_rect(maybe_rect: Option<Rect>) -> UVec2 {
311    match maybe_rect {
312        Some(rect) => {
313            let rect_uvec4 = (vec4(rect.min.x, rect.min.y, rect.max.x, rect.max.y) * 65535.0)
314                .round()
315                .as_uvec4();
316            uvec2(
317                rect_uvec4.x | (rect_uvec4.y << 16),
318                rect_uvec4.z | (rect_uvec4.w << 16),
319            )
320        }
321        None => UVec2::ZERO,
322    }
323}
324
325impl Default for Lightmap {
326    fn default() -> Self {
327        Self {
328            image: Default::default(),
329            uv_rect: Rect::new(0.0, 0.0, 1.0, 1.0),
330            bicubic_sampling: false,
331        }
332    }
333}
334
335pub fn init_render_lightmaps(
336    mut commands: Commands,
337    render_device: Res<RenderDevice>,
338    render_adapter: Res<RenderAdapter>,
339) {
340    let bindless_supported = binding_arrays_are_usable(&render_device, &render_adapter);
341
342    commands.insert_resource(RenderLightmaps {
343        render_lightmaps: default(),
344        slabs: vec![],
345        free_slabs: FixedBitSet::new(),
346        pending_lightmaps: default(),
347        bindless_supported,
348    });
349}
350
351impl RenderLightmaps {
352    /// Creates a new slab, appends it to the end of the list, and returns its
353    /// slab index.
354    fn create_slab(&mut self, fallback_images: &FallbackImage) -> LightmapSlabIndex {
355        let slab_index = LightmapSlabIndex::from(self.slabs.len());
356        self.free_slabs.grow_and_insert(slab_index.into());
357        self.slabs
358            .push(LightmapSlab::new(fallback_images, self.bindless_supported));
359        slab_index
360    }
361
362    fn allocate(
363        &mut self,
364        fallback_images: &FallbackImage,
365        image_id: AssetId<Image>,
366    ) -> (LightmapSlabIndex, LightmapSlotIndex) {
367        let slab_index = match self.free_slabs.minimum() {
368            None => self.create_slab(fallback_images),
369            Some(slab_index) => slab_index.into(),
370        };
371
372        let slab = &mut self.slabs[usize::from(slab_index)];
373        let slot_index = slab.allocate(image_id);
374        if slab.is_full() {
375            self.free_slabs.remove(slab_index.into());
376        }
377
378        (slab_index, slot_index)
379    }
380
381    fn remove(
382        &mut self,
383        fallback_images: &FallbackImage,
384        slab_index: LightmapSlabIndex,
385        slot_index: LightmapSlotIndex,
386    ) {
387        let slab = &mut self.slabs[usize::from(slab_index)];
388        slab.remove(fallback_images, slot_index);
389
390        if !slab.is_full() {
391            self.free_slabs.grow_and_insert(slot_index.into());
392        }
393    }
394}
395
396impl LightmapSlab {
397    fn new(fallback_images: &FallbackImage, bindless_supported: bool) -> LightmapSlab {
398        let count = if bindless_supported {
399            LIGHTMAPS_PER_SLAB
400        } else {
401            1
402        };
403
404        LightmapSlab {
405            lightmaps: (0..count)
406                .map(|_| AllocatedLightmap {
407                    gpu_image: fallback_images.d2.clone(),
408                    asset_id: None,
409                })
410                .collect(),
411            free_slots_bitmask: (1 << count) - 1,
412        }
413    }
414
415    fn is_full(&self) -> bool {
416        self.free_slots_bitmask == 0
417    }
418
419    fn allocate(&mut self, image_id: AssetId<Image>) -> LightmapSlotIndex {
420        let index = LightmapSlotIndex::from(self.free_slots_bitmask.trailing_zeros());
421        self.free_slots_bitmask &= !(1 << u32::from(index));
422        self.lightmaps[usize::from(index)].asset_id = Some(image_id);
423        index
424    }
425
426    fn insert(&mut self, index: LightmapSlotIndex, gpu_image: GpuImage) {
427        self.lightmaps[usize::from(index)] = AllocatedLightmap {
428            gpu_image,
429            asset_id: None,
430        }
431    }
432
433    fn remove(&mut self, fallback_images: &FallbackImage, index: LightmapSlotIndex) {
434        self.lightmaps[usize::from(index)] = AllocatedLightmap {
435            gpu_image: fallback_images.d2.clone(),
436            asset_id: None,
437        };
438        self.free_slots_bitmask |= 1 << u32::from(index);
439    }
440
441    /// Returns the texture views and samplers for the lightmaps in this slab,
442    /// ready to be placed into a bind group.
443    ///
444    /// This is used when constructing bind groups in bindless mode. Before
445    /// returning, this function pads out the arrays with fallback images in
446    /// order to fulfill requirements of platforms that require full binding
447    /// arrays (e.g. DX12).
448    pub(crate) fn build_binding_arrays(&self) -> (Vec<&WgpuTextureView>, Vec<&WgpuSampler>) {
449        (
450            self.lightmaps
451                .iter()
452                .map(|allocated_lightmap| &*allocated_lightmap.gpu_image.texture_view)
453                .collect(),
454            self.lightmaps
455                .iter()
456                .map(|allocated_lightmap| &*allocated_lightmap.gpu_image.sampler)
457                .collect(),
458        )
459    }
460
461    /// Returns the texture view and sampler corresponding to the first
462    /// lightmap, which must exist.
463    ///
464    /// This is used when constructing bind groups in non-bindless mode.
465    pub(crate) fn bindings_for_first_lightmap(&self) -> (&TextureView, &Sampler) {
466        (
467            &self.lightmaps[0].gpu_image.texture_view,
468            &self.lightmaps[0].gpu_image.sampler,
469        )
470    }
471}
472
473impl From<u32> for LightmapSlabIndex {
474    fn from(value: u32) -> Self {
475        Self(NonMaxU32::new(value).unwrap())
476    }
477}
478
479impl From<usize> for LightmapSlabIndex {
480    fn from(value: usize) -> Self {
481        Self::from(value as u32)
482    }
483}
484
485impl From<u32> for LightmapSlotIndex {
486    fn from(value: u32) -> Self {
487        Self(NonMaxU16::new(value as u16).unwrap())
488    }
489}
490
491impl From<usize> for LightmapSlotIndex {
492    fn from(value: usize) -> Self {
493        Self::from(value as u32)
494    }
495}
496
497impl From<LightmapSlabIndex> for usize {
498    fn from(value: LightmapSlabIndex) -> Self {
499        value.0.get() as usize
500    }
501}
502
503impl From<LightmapSlotIndex> for usize {
504    fn from(value: LightmapSlotIndex) -> Self {
505        value.0.get() as usize
506    }
507}
508
509impl From<LightmapSlotIndex> for u16 {
510    fn from(value: LightmapSlotIndex) -> Self {
511        value.0.get()
512    }
513}
514
515impl From<LightmapSlotIndex> for u32 {
516    fn from(value: LightmapSlotIndex) -> Self {
517        value.0.get() as u32
518    }
519}