Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement bindless lightmaps. #16653

Merged
merged 12 commits into from
Dec 17, 2024
9 changes: 5 additions & 4 deletions crates/bevy_core_pipeline/src/core_3d/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,10 @@ pub use main_opaque_pass_3d_node::*;
pub use main_transparent_pass_3d_node::*;

use bevy_app::{App, Plugin, PostUpdate};
use bevy_asset::{AssetId, UntypedAssetId};
use bevy_asset::UntypedAssetId;
use bevy_color::LinearRgba;
use bevy_ecs::{entity::EntityHashSet, prelude::*};
use bevy_image::{BevyDefault, Image};
use bevy_image::BevyDefault;
use bevy_math::FloatOrd;
use bevy_render::sync_world::MainEntity;
use bevy_render::{
Expand All @@ -97,6 +97,7 @@ use bevy_render::{
Extract, ExtractSchedule, Render, RenderApp, RenderSet,
};
use bevy_utils::{tracing::warn, HashMap};
use nonmax::NonMaxU32;

use crate::{
core_3d::main_transmissive_pass_3d_node::MainTransmissivePass3dNode,
Expand Down Expand Up @@ -244,8 +245,8 @@ pub struct Opaque3dBinKey {
/// the ID of another type of asset.
pub asset_id: UntypedAssetId,

/// The lightmap, if present.
pub lightmap_image: Option<AssetId<Image>>,
/// The lightmap slab, if present.
pub lightmap_slab: Option<NonMaxU32>,
}

impl PhaseItem for Opaque3d {
Expand Down
18 changes: 17 additions & 1 deletion crates/bevy_pbr/src/lightmap/lightmap.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@

#import bevy_pbr::mesh_bindings::mesh

#ifdef MULTIPLE_LIGHTMAPS_IN_ARRAY
@group(1) @binding(4) var lightmaps_textures: binding_array<texture_2d<f32>>;
@group(1) @binding(5) var lightmaps_samplers: binding_array<sampler>;
#else // MULTIPLE_LIGHTMAPS_IN_ARRAY
@group(1) @binding(4) var lightmaps_texture: texture_2d<f32>;
@group(1) @binding(5) var lightmaps_sampler: sampler;
#endif // MULTIPLE_LIGHTMAPS_IN_ARRAY

// Samples the lightmap, if any, and returns indirect illumination from it.
fn lightmap(uv: vec2<f32>, exposure: f32, instance_index: u32) -> vec3<f32> {
Expand All @@ -21,9 +26,20 @@ fn lightmap(uv: vec2<f32>, exposure: f32, instance_index: u32) -> vec3<f32> {
// control flow uniformity problems.
//
// TODO(pcwalton): Consider bicubic filtering.
#ifdef MULTIPLE_LIGHTMAPS_IN_ARRAY
let lightmap_slot = mesh[instance_index].lightmap_slot;
return textureSampleLevel(
lightmaps_textures[lightmap_slot],
lightmaps_samplers[lightmap_slot],
lightmap_uv,
0.0
).rgb * exposure;
#else // MULTIPLE_LIGHTMAPS_IN_ARRAY
return textureSampleLevel(
lightmaps_texture,
lightmaps_sampler,
lightmap_uv,
0.0).rgb * exposure;
0.0
).rgb * exposure;
#endif // MULTIPLE_LIGHTMAPS_IN_ARRAY
}
189 changes: 156 additions & 33 deletions crates/bevy_pbr/src/lightmap/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@
//! multiple meshes can share the same material, whereas sharing lightmaps is
//! nonsensical).
//!
//! Note that meshes can't be instanced if they use different lightmap textures.
//! If you want to instance a lightmapped mesh, combine the lightmap textures
//! into a single atlas, and set the `uv_rect` field on [`Lightmap`]
//! appropriately.
//! Note that multiple meshes can't be drawn in a single drawcall if they use
//! different lightmap textures, unless bindless textures are in use. If you
//! want to instance a lightmapped mesh, and your platform doesn't support
//! bindless textures, combine the lightmap textures into a single atlas, and
//! set the `uv_rect` field on [`Lightmap`] appropriately.
//!
//! [The Lightmapper]: https://github.com/Naxela/The_Lightmapper
//! [`Mesh3d`]: bevy_render::mesh::Mesh3d
Expand All @@ -32,17 +33,18 @@

use bevy_app::{App, Plugin};
use bevy_asset::{load_internal_asset, AssetId, Handle};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
component::Component,
entity::Entity,
reflect::ReflectComponent,
schedule::IntoSystemConfigs,
system::{Query, Res, ResMut, Resource},
world::{FromWorld, World},
};
use bevy_image::Image;
use bevy_math::{uvec2, vec4, Rect, UVec2};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::sync_world::MainEntityHashMap;
use bevy_render::{
mesh::{Mesh, RenderMesh},
render_asset::RenderAssets,
Expand All @@ -51,14 +53,23 @@ use bevy_render::{
view::ViewVisibility,
Extract, ExtractSchedule, RenderApp,
};
use bevy_utils::HashSet;
use bevy_render::{renderer::RenderDevice, sync_world::MainEntityHashMap};
use bevy_utils::default;
use nonmax::NonMaxU32;

use crate::{ExtractMeshesSet, RenderMeshInstances};
use crate::{binding_arrays_are_usable, ExtractMeshesSet, RenderMeshInstances};

/// The ID of the lightmap shader.
pub const LIGHTMAP_SHADER_HANDLE: Handle<Shader> =
Handle::weak_from_u128(285484768317531991932943596447919767152);

/// The number of lightmaps that we store in a single slab, if bindless textures
/// are in use.
///
/// If bindless textures aren't in use, then only a single lightmap can be bound
/// at a time.
pub const LIGHTMAPS_PER_SLAB: usize = 16;

/// A plugin that provides an implementation of lightmaps.
pub struct LightmapPlugin;

Expand Down Expand Up @@ -100,28 +111,57 @@ pub(crate) struct RenderLightmap {
/// right coordinate is the `max` part of the rect. The rect ranges from (0,
/// 0) to (1, 1).
pub(crate) uv_rect: Rect,

/// The index of the slab (i.e. binding array) in which the lightmap is
/// located.
pub(crate) slab_index: LightmapSlabIndex,

/// The index of the slot (i.e. element within the binding array) in which
/// the lightmap is located.
///
/// If bindless lightmaps aren't in use, this will be 0.
pub(crate) slot_index: LightmapSlotIndex,
}

/// Stores data for all lightmaps in the render world.
///
/// This is cleared and repopulated each frame during the `extract_lightmaps`
/// system.
#[derive(Default, Resource)]
#[derive(Resource)]
pub struct RenderLightmaps {
/// The mapping from every lightmapped entity to its lightmap info.
///
/// Entities without lightmaps, or for which the mesh or lightmap isn't
/// loaded, won't have entries in this table.
pub(crate) render_lightmaps: MainEntityHashMap<RenderLightmap>,

/// All active lightmap images in the scene.
///
/// Gathering all lightmap images into a set makes mesh bindgroup
/// preparation slightly more efficient, because only one bindgroup needs to
/// be created per lightmap texture.
pub(crate) all_lightmap_images: HashSet<AssetId<Image>>,
/// The slabs (binding arrays) containing the lightmaps.
pub(crate) slabs: Vec<LightmapSlab>,

/// Whether bindless textures are supported on this platform.
pub(crate) bindless_supported: bool,
}

/// A binding array that contains lightmaps.
///
/// This will have a single binding if bindless lightmaps aren't in use.
#[derive(Default)]
pub struct LightmapSlab {
/// The GPU images in this slab.
pub(crate) gpu_images: Vec<GpuImage>,
}

/// The index of the slab (binding array) in which a lightmap is located.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Deref, DerefMut)]
#[repr(transparent)]
pub struct LightmapSlabIndex(pub(crate) NonMaxU32);

/// The index of the slot (element within the binding array) in the slab in
/// which a lightmap is located.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Deref, DerefMut)]
#[repr(transparent)]
pub(crate) struct LightmapSlotIndex(pub(crate) NonMaxU32);

impl Plugin for LightmapPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(
Expand Down Expand Up @@ -153,41 +193,63 @@ fn extract_lightmaps(
meshes: Res<RenderAssets<RenderMesh>>,
) {
// Clear out the old frame's data.
// TODO: Should we retain slabs from frame to frame, to avoid having to do
// this?
render_lightmaps.render_lightmaps.clear();
render_lightmaps.all_lightmap_images.clear();
render_lightmaps.slabs.clear();

// Loop over each entity.
for (entity, view_visibility, lightmap) in lightmaps.iter() {
// Only process visible entities for which the mesh and lightmap are
// both loaded.
if !view_visibility.get()
|| images.get(&lightmap.image).is_none()
|| !render_mesh_instances
.mesh_asset_id(entity.into())
.and_then(|mesh_asset_id| meshes.get(mesh_asset_id))
.is_some_and(|mesh| mesh.layout.0.contains(Mesh::ATTRIBUTE_UV_1.id))
// Only process visible entities.
if !view_visibility.get() {
continue;
}

// Make sure the lightmap is loaded.
let Some(gpu_image) = images.get(&lightmap.image) else {
continue;
};

// Make sure the mesh is located and that it contains a lightmap UV map.
if !render_mesh_instances
.mesh_asset_id(entity.into())
.and_then(|mesh_asset_id| meshes.get(mesh_asset_id))
.is_some_and(|mesh| mesh.layout.0.contains(Mesh::ATTRIBUTE_UV_1.id))
{
continue;
}

// Add the lightmap to a slab.
let (slab_index, slot_index) = render_lightmaps.add((*gpu_image).clone());

// Store information about the lightmap in the render world.
render_lightmaps.render_lightmaps.insert(
entity.into(),
RenderLightmap::new(lightmap.image.id(), lightmap.uv_rect),
RenderLightmap::new(
lightmap.image.id(),
lightmap.uv_rect,
slab_index,
slot_index,
),
);

// Make a note of the loaded lightmap image so we can efficiently
// process them later during mesh bindgroup creation.
render_lightmaps
.all_lightmap_images
.insert(lightmap.image.id());
}
}

impl RenderLightmap {
/// Creates a new lightmap from a texture and a UV rect.
fn new(image: AssetId<Image>, uv_rect: Rect) -> Self {
Self { image, uv_rect }
/// Creates a new lightmap from a texture, a UV rect, and a slab and slot
/// index pair.
fn new(
image: AssetId<Image>,
uv_rect: Rect,
slab_index: LightmapSlabIndex,
slot_index: LightmapSlotIndex,
) -> Self {
Self {
image,
uv_rect,
slab_index,
slot_index,
}
}
}

Expand Down Expand Up @@ -215,3 +277,64 @@ impl Default for Lightmap {
}
}
}

impl FromWorld for RenderLightmaps {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
let bindless_supported = binding_arrays_are_usable(render_device);

RenderLightmaps {
render_lightmaps: default(),
slabs: vec![],
bindless_supported,
}
}
}

impl RenderLightmaps {
/// Returns true if the slab with the given index is full or false otherwise.
///
/// The slab must exist.
fn slab_is_full(&self, slab_index: LightmapSlabIndex) -> bool {
let size = self.slabs[u32::from(slab_index.0) as usize]
.gpu_images
.len();
if self.bindless_supported {
size >= LIGHTMAPS_PER_SLAB
} else {
size >= 1
}
}

/// Creates a new slab, appends it to the end of the list, and returns its
/// slab index.
fn create_slab(&mut self) -> LightmapSlabIndex {
let slab_index = LightmapSlabIndex(NonMaxU32::new(self.slabs.len() as u32).unwrap());
self.slabs.push(default());
slab_index
}

/// Adds a lightmap to a slab and returns the index of that slab as well as
/// the index of the slot that the lightmap now occupies.
///
/// This creates a new slab if there are no slabs or all slabs are full.
fn add(&mut self, gpu_image: GpuImage) -> (LightmapSlabIndex, LightmapSlotIndex) {
let mut slab_index = LightmapSlabIndex(NonMaxU32::new(self.slabs.len() as u32).unwrap());
if (u32::from(*slab_index) as usize) >= self.slabs.len() || self.slab_is_full(slab_index) {
slab_index = self.create_slab();
}

let slot_index = self.slabs[u32::from(*slab_index) as usize].insert(gpu_image);

(slab_index, slot_index)
}
}

impl LightmapSlab {
/// Inserts a lightmap into this slab and returns the index of its slot.
fn insert(&mut self, gpu_image: GpuImage) -> LightmapSlotIndex {
let slot_index = LightmapSlotIndex(NonMaxU32::new(self.gpu_images.len() as u32).unwrap());
self.gpu_images.push(gpu_image);
slot_index
}
}
9 changes: 5 additions & 4 deletions crates/bevy_pbr/src/material.rs
Original file line number Diff line number Diff line change
Expand Up @@ -805,11 +805,11 @@ pub fn queue_material_meshes<M: Material>(
| MeshPipelineKey::from_bits_retain(mesh.key_bits.bits())
| mesh_pipeline_key_bits;

let lightmap_image = render_lightmaps
let lightmap_slab_index = render_lightmaps
.render_lightmaps
.get(visible_entity)
.map(|lightmap| lightmap.image);
if lightmap_image.is_some() {
.map(|lightmap| lightmap.slab_index);
if lightmap_slab_index.is_some() {
mesh_key |= MeshPipelineKey::LIGHTMAPPED;
}

Expand Down Expand Up @@ -873,7 +873,8 @@ pub fn queue_material_meshes<M: Material>(
pipeline: pipeline_id,
asset_id: mesh_instance.mesh_asset_id.into(),
material_bind_group_index: Some(material.binding.group.0),
lightmap_image,
lightmap_slab: lightmap_slab_index
.map(|lightmap_slab_index| *lightmap_slab_index),
};
opaque_phase.add(
bin_key,
Expand Down
Loading
Loading