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