Implement bindless lightmaps. (#16653)

This commit allows Bevy to bind 16 lightmaps at a time, if the current
platform supports bindless textures. Naturally, if bindless textures
aren't supported, Bevy falls back to binding only a single lightmap at a
time. As lightmaps are usually heavily atlased, I doubt many scenes will
use more than 16 lightmap textures.

This has little performance impact now, but it's desirable for us to
reap the benefits of multidraw and bindless textures on scenes that use
lightmaps. Otherwise, we might have to break batches in order to switch
those lightmaps.

Additionally, this PR slightly reduces the cost of binning because it
makes the lightmap index in `Opaque3dBinKey` 32 bits instead of an
`AssetId`.

## Migration Guide

* The `Opaque3dBinKey::lightmap_image` field is now
`Opaque3dBinKey::lightmap_slab`, which is a lightweight identifier for
an entire binding array of lightmaps.
This commit is contained in:
Patrick Walton 2024-12-16 15:37:06 -08:00 committed by GitHub
parent 26bd1609ec
commit 35826be6f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 524 additions and 117 deletions

View File

@ -18,7 +18,7 @@ struct Color {
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
#ifdef BINDLESS
let slot = mesh[in.instance_index].material_bind_group_slot;
let slot = mesh[in.instance_index].material_and_lightmap_bind_group_slot & 0xffffu;
let base_color = material_color[slot].base_color;
#else // BINDLESS
let base_color = material_color.base_color;

View File

@ -76,10 +76,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::{
camera::{Camera, ExtractedCamera},
@ -102,6 +102,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,
@ -258,8 +259,9 @@ pub struct Opaque3dBatchSetKey {
/// For non-mesh items, you can safely fill this with `None`.
pub index_slab: Option<SlabId>,
/// The lightmap, if present.
pub lightmap_image: Option<AssetId<Image>>,
/// Index of the slab that the lightmap resides in, if a lightmap is
/// present.
pub lightmap_slab: Option<NonMaxU32>,
}
/// Data that must be identical in order to *batch* phase items together.

View File

@ -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> {
@ -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].material_and_lightmap_bind_group_slot >> 16u;
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
}

View File

@ -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
@ -32,33 +33,46 @@
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,
query::{Changed, Or},
reflect::ReflectComponent,
removal_detection::RemovedComponents,
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,
render_resource::Shader,
texture::GpuImage,
render_resource::{Sampler, Shader, TextureView, WgpuSampler, WgpuTextureView},
sync_world::MainEntity,
texture::{FallbackImage, GpuImage},
view::ViewVisibility,
Extract, ExtractSchedule, RenderApp,
};
use bevy_utils::HashSet;
use bevy_render::{renderer::RenderDevice, sync_world::MainEntityHashMap};
use bevy_utils::{default, tracing::error, HashSet};
use fixedbitset::FixedBitSet;
use nonmax::{NonMaxU16, NonMaxU32};
use crate::{ExtractMeshesSet, RenderMeshInstances};
use crate::{binding_arrays_are_usable, ExtractMeshesSet};
/// 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;
@ -100,13 +114,23 @@ 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.
///
@ -114,14 +138,43 @@ pub struct RenderLightmaps {
/// 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>,
free_slabs: FixedBitSet,
pending_lightmaps: HashSet<(LightmapSlabIndex, LightmapSlotIndex)>,
/// 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.
pub struct LightmapSlab {
/// The GPU images in this slab.
lightmaps: Vec<AllocatedLightmap>,
free_slots_bitmask: u32,
}
struct AllocatedLightmap {
gpu_image: GpuImage,
// This will only be present if the lightmap is allocated but not loaded.
asset_id: Option<AssetId<Image>>,
}
/// 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 struct LightmapSlotIndex(pub(crate) NonMaxU16);
impl Plugin for LightmapPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(
@ -146,48 +199,108 @@ impl Plugin for LightmapPlugin {
/// Extracts all lightmaps from the scene and populates the [`RenderLightmaps`]
/// resource.
fn extract_lightmaps(
mut render_lightmaps: ResMut<RenderLightmaps>,
lightmaps: Extract<Query<(Entity, &ViewVisibility, &Lightmap)>>,
render_mesh_instances: Res<RenderMeshInstances>,
render_lightmaps: ResMut<RenderLightmaps>,
changed_lightmaps_query: Extract<
Query<
(Entity, &ViewVisibility, &Lightmap),
Or<(Changed<ViewVisibility>, Changed<Lightmap>)>,
>,
>,
mut removed_lightmaps_query: Extract<RemovedComponents<Lightmap>>,
images: Res<RenderAssets<GpuImage>>,
meshes: Res<RenderAssets<RenderMesh>>,
fallback_images: Res<FallbackImage>,
) {
// Clear out the old frame's data.
render_lightmaps.render_lightmaps.clear();
render_lightmaps.all_lightmap_images.clear();
let render_lightmaps = render_lightmaps.into_inner();
// 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))
for (entity, view_visibility, lightmap) in changed_lightmaps_query.iter() {
if render_lightmaps
.render_lightmaps
.contains_key(&MainEntity::from(entity))
{
continue;
}
// Store information about the lightmap in the render world.
// Only process visible entities.
if !view_visibility.get() {
continue;
}
let (slab_index, slot_index) =
render_lightmaps.allocate(&fallback_images, lightmap.image.id());
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());
.pending_lightmaps
.insert((slab_index, slot_index));
}
for entity in removed_lightmaps_query.read() {
if changed_lightmaps_query.contains(entity) {
continue;
}
let Some(RenderLightmap {
slab_index,
slot_index,
..
}) = render_lightmaps
.render_lightmaps
.remove(&MainEntity::from(entity))
else {
continue;
};
render_lightmaps.remove(&fallback_images, slab_index, slot_index);
render_lightmaps
.pending_lightmaps
.remove(&(slab_index, slot_index));
}
render_lightmaps
.pending_lightmaps
.retain(|&(slab_index, slot_index)| {
let Some(asset_id) = render_lightmaps.slabs[usize::from(slab_index)].lightmaps
[usize::from(slot_index)]
.asset_id
else {
error!(
"Allocated lightmap should have been removed from `pending_lightmaps` by now"
);
return false;
};
let Some(gpu_image) = images.get(asset_id) else {
return true;
};
render_lightmaps.slabs[usize::from(slab_index)].insert(slot_index, gpu_image.clone());
false
});
}
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,
}
}
}
@ -215,3 +328,188 @@ 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![],
free_slabs: FixedBitSet::new(),
pending_lightmaps: default(),
bindless_supported,
}
}
}
impl RenderLightmaps {
/// Creates a new slab, appends it to the end of the list, and returns its
/// slab index.
fn create_slab(&mut self, fallback_images: &FallbackImage) -> LightmapSlabIndex {
let slab_index = LightmapSlabIndex::from(self.slabs.len());
self.free_slabs.grow_and_insert(slab_index.into());
self.slabs
.push(LightmapSlab::new(fallback_images, self.bindless_supported));
slab_index
}
fn allocate(
&mut self,
fallback_images: &FallbackImage,
image_id: AssetId<Image>,
) -> (LightmapSlabIndex, LightmapSlotIndex) {
let slab_index = match self.free_slabs.minimum() {
None => self.create_slab(fallback_images),
Some(slab_index) => slab_index.into(),
};
let slab = &mut self.slabs[usize::from(slab_index)];
let slot_index = slab.allocate(image_id);
if slab.is_full() {
self.free_slabs.remove(slab_index.into());
}
(slab_index, slot_index)
}
fn remove(
&mut self,
fallback_images: &FallbackImage,
slab_index: LightmapSlabIndex,
slot_index: LightmapSlotIndex,
) {
let slab = &mut self.slabs[usize::from(slab_index)];
slab.remove(fallback_images, slot_index);
if !slab.is_full() {
self.free_slabs.grow_and_insert(slot_index.into());
}
}
}
impl LightmapSlab {
fn new(fallback_images: &FallbackImage, bindless_supported: bool) -> LightmapSlab {
let count = if bindless_supported {
LIGHTMAPS_PER_SLAB
} else {
1
};
LightmapSlab {
lightmaps: (0..count)
.map(|_| AllocatedLightmap {
gpu_image: fallback_images.d2.clone(),
asset_id: None,
})
.collect(),
free_slots_bitmask: (1 << count) - 1,
}
}
fn is_full(&self) -> bool {
self.free_slots_bitmask == 0
}
fn allocate(&mut self, image_id: AssetId<Image>) -> LightmapSlotIndex {
let index = LightmapSlotIndex::from(self.free_slots_bitmask.trailing_zeros());
self.free_slots_bitmask &= !(1 << u32::from(index));
self.lightmaps[usize::from(index)].asset_id = Some(image_id);
index
}
fn insert(&mut self, index: LightmapSlotIndex, gpu_image: GpuImage) {
self.lightmaps[usize::from(index)] = AllocatedLightmap {
gpu_image,
asset_id: None,
}
}
fn remove(&mut self, fallback_images: &FallbackImage, index: LightmapSlotIndex) {
self.lightmaps[usize::from(index)] = AllocatedLightmap {
gpu_image: fallback_images.d2.clone(),
asset_id: None,
};
self.free_slots_bitmask |= 1 << u32::from(index);
}
/// Returns the texture views and samplers for the lightmaps in this slab,
/// ready to be placed into a bind group.
///
/// This is used when constructing bind groups in bindless mode. Before
/// returning, this function pads out the arrays with fallback images in
/// order to fulfill requirements of platforms that require full binding
/// arrays (e.g. DX12).
pub(crate) fn build_binding_arrays(&self) -> (Vec<&WgpuTextureView>, Vec<&WgpuSampler>) {
(
self.lightmaps
.iter()
.map(|allocated_lightmap| &*allocated_lightmap.gpu_image.texture_view)
.collect(),
self.lightmaps
.iter()
.map(|allocated_lightmap| &*allocated_lightmap.gpu_image.sampler)
.collect(),
)
}
/// Returns the texture view and sampler corresponding to the first
/// lightmap, which must exist.
///
/// This is used when constructing bind groups in non-bindless mode.
pub(crate) fn bindings_for_first_lightmap(&self) -> (&TextureView, &Sampler) {
(
&self.lightmaps[0].gpu_image.texture_view,
&self.lightmaps[0].gpu_image.sampler,
)
}
}
impl From<u32> for LightmapSlabIndex {
fn from(value: u32) -> Self {
Self(NonMaxU32::new(value).unwrap())
}
}
impl From<usize> for LightmapSlabIndex {
fn from(value: usize) -> Self {
Self::from(value as u32)
}
}
impl From<u32> for LightmapSlotIndex {
fn from(value: u32) -> Self {
Self(NonMaxU16::new(value as u16).unwrap())
}
}
impl From<usize> for LightmapSlotIndex {
fn from(value: usize) -> Self {
Self::from(value as u32)
}
}
impl From<LightmapSlabIndex> for usize {
fn from(value: LightmapSlabIndex) -> Self {
value.0.get() as usize
}
}
impl From<LightmapSlotIndex> for usize {
fn from(value: LightmapSlotIndex) -> Self {
value.0.get() as usize
}
}
impl From<LightmapSlotIndex> for u16 {
fn from(value: LightmapSlotIndex) -> Self {
value.0.get()
}
}
impl From<LightmapSlotIndex> for u32 {
fn from(value: LightmapSlotIndex) -> Self {
value.0.get() as u32
}
}

View File

@ -809,11 +809,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;
}
@ -881,7 +881,8 @@ pub fn queue_material_meshes<M: Material>(
material_bind_group_index: Some(material.binding.group.0),
vertex_slab: vertex_slab.unwrap_or_default(),
index_slab,
lightmap_image,
lightmap_slab: lightmap_slab_index
.map(|lightmap_slab_index| *lightmap_slab_index),
},
asset_id: mesh_instance.mesh_asset_id.into(),
};

View File

@ -156,11 +156,17 @@ impl From<u32> for MaterialBindGroupIndex {
/// non-bindless mode, this slot is always 0.
#[derive(Clone, Copy, Debug, Default, Reflect, Deref, DerefMut)]
#[reflect(Default)]
pub struct MaterialBindGroupSlot(pub u32);
pub struct MaterialBindGroupSlot(pub u16);
impl From<u32> for MaterialBindGroupSlot {
fn from(value: u32) -> Self {
MaterialBindGroupSlot(value)
MaterialBindGroupSlot(value as u16)
}
}
impl From<MaterialBindGroupSlot> for u32 {
fn from(value: MaterialBindGroupSlot) -> Self {
value.0 as u32
}
}

View File

@ -314,8 +314,11 @@ pub struct MeshUniform {
pub current_skin_index: u32,
/// The previous skin index, or `u32::MAX` if there's no previous skin.
pub previous_skin_index: u32,
/// Index of the material inside the bind group data.
pub material_bind_group_slot: u32,
/// The material and lightmap indices, packed into 32 bits.
///
/// Low 16 bits: index of the material inside the bind group data.
/// High 16 bits: index of the lightmap in the binding array.
pub material_and_lightmap_bind_group_slot: u32,
}
/// Information that has to be transferred from CPU to GPU in order to produce
@ -356,8 +359,11 @@ pub struct MeshInputUniform {
pub current_skin_index: u32,
/// The previous skin index, or `u32::MAX` if there's no previous skin.
pub previous_skin_index: u32,
/// Index of the material inside the bind group data.
pub material_bind_group_slot: u32,
/// The material and lightmap indices, packed into 32 bits.
///
/// Low 16 bits: index of the material inside the bind group data.
/// High 16 bits: index of the lightmap in the binding array.
pub material_and_lightmap_bind_group_slot: u32,
}
/// Information about each mesh instance needed to cull it on GPU.
@ -388,23 +394,29 @@ impl MeshUniform {
mesh_transforms: &MeshTransforms,
first_vertex_index: u32,
material_bind_group_slot: MaterialBindGroupSlot,
maybe_lightmap_uv_rect: Option<Rect>,
maybe_lightmap: Option<(LightmapSlotIndex, Rect)>,
current_skin_index: Option<u32>,
previous_skin_index: Option<u32>,
) -> Self {
let (local_from_world_transpose_a, local_from_world_transpose_b) =
mesh_transforms.world_from_local.inverse_transpose_3x3();
let lightmap_bind_group_slot = match maybe_lightmap {
None => u16::MAX,
Some((slot_index, _)) => slot_index.into(),
};
Self {
world_from_local: mesh_transforms.world_from_local.to_transpose(),
previous_world_from_local: mesh_transforms.previous_world_from_local.to_transpose(),
lightmap_uv_rect: pack_lightmap_uv_rect(maybe_lightmap_uv_rect),
lightmap_uv_rect: pack_lightmap_uv_rect(maybe_lightmap.map(|(_, uv_rect)| uv_rect)),
local_from_world_transpose_a,
local_from_world_transpose_b,
flags: mesh_transforms.flags,
first_vertex_index,
current_skin_index: current_skin_index.unwrap_or(u32::MAX),
previous_skin_index: previous_skin_index.unwrap_or(u32::MAX),
material_bind_group_slot: *material_bind_group_slot,
material_and_lightmap_bind_group_slot: u32::from(material_bind_group_slot)
| ((lightmap_bind_group_slot as u32) << 16),
}
}
}
@ -887,6 +899,7 @@ impl RenderMeshInstanceGpuQueue {
impl RenderMeshInstanceGpuBuilder {
/// Flushes this mesh instance to the [`RenderMeshInstanceGpu`] and
/// [`MeshInputUniform`] tables, replacing the existing entry if applicable.
#[allow(clippy::too_many_arguments)]
fn update(
self,
entity: MainEntity,
@ -894,6 +907,7 @@ impl RenderMeshInstanceGpuBuilder {
current_input_buffer: &mut InstanceInputUniformBuffer<MeshInputUniform>,
previous_input_buffer: &mut InstanceInputUniformBuffer<MeshInputUniform>,
mesh_allocator: &MeshAllocator,
render_lightmaps: &RenderLightmaps,
skin_indices: &SkinIndices,
) -> u32 {
let first_vertex_index = match mesh_allocator.mesh_vertex_slice(&self.shared.mesh_asset_id)
@ -911,6 +925,11 @@ impl RenderMeshInstanceGpuBuilder {
None => u32::MAX,
};
let lightmap_slot = match render_lightmaps.render_lightmaps.get(&entity) {
Some(render_lightmap) => u16::from(*render_lightmap.slot_index),
None => u16::MAX,
};
// Create the mesh input uniform.
let mut mesh_input_uniform = MeshInputUniform {
world_from_local: self.world_from_local.to_transpose(),
@ -920,7 +939,9 @@ impl RenderMeshInstanceGpuBuilder {
first_vertex_index,
current_skin_index,
previous_skin_index,
material_bind_group_slot: *self.shared.material_bindings_index.slot,
material_and_lightmap_bind_group_slot: u32::from(
self.shared.material_bindings_index.slot,
) | ((lightmap_slot as u32) << 16),
};
// Did the last frame contain this entity as well?
@ -1345,6 +1366,7 @@ pub fn collect_meshes_for_gpu_building(
mut mesh_culling_data_buffer: ResMut<MeshCullingDataBuffer>,
mut render_mesh_instance_queues: ResMut<RenderMeshInstanceGpuQueues>,
mesh_allocator: Res<MeshAllocator>,
render_lightmaps: Res<RenderLightmaps>,
skin_indices: Res<SkinIndices>,
) {
let RenderMeshInstances::GpuBuilding(ref mut render_mesh_instances) =
@ -1381,6 +1403,7 @@ pub fn collect_meshes_for_gpu_building(
current_input_buffer,
previous_input_buffer,
&mesh_allocator,
&render_lightmaps,
&skin_indices,
);
}
@ -1405,6 +1428,7 @@ pub fn collect_meshes_for_gpu_building(
current_input_buffer,
previous_input_buffer,
&mesh_allocator,
&render_lightmaps,
&skin_indices,
);
mesh_culling_builder
@ -1588,7 +1612,7 @@ impl GetBatchData for MeshPipeline {
&mesh_instance.transforms,
first_vertex_index,
material_bind_group_index.slot,
maybe_lightmap.map(|lightmap| lightmap.uv_rect),
maybe_lightmap.map(|lightmap| (lightmap.slot_index, lightmap.uv_rect)),
current_skin_index,
previous_skin_index,
),
@ -1655,7 +1679,7 @@ impl GetFullBatchData for MeshPipeline {
&mesh_instance.transforms,
first_vertex_index,
mesh_instance.material_bindings_index.slot,
maybe_lightmap.map(|lightmap| lightmap.uv_rect),
maybe_lightmap.map(|lightmap| (lightmap.slot_index, lightmap.uv_rect)),
current_skin_index,
previous_skin_index,
))
@ -2245,6 +2269,7 @@ impl SpecializedMeshPipeline for MeshPipeline {
if self.binding_arrays_are_usable {
shader_defs.push("MULTIPLE_LIGHT_PROBES_IN_ARRAY".into());
shader_defs.push("MULTIPLE_LIGHTMAPS_IN_ARRAY".into());
}
if IRRADIANCE_VOLUMES_ARE_USABLE {
@ -2328,7 +2353,7 @@ pub struct MeshBindGroups {
model_only: Option<BindGroup>,
skinned: Option<MeshBindGroupPair>,
morph_targets: HashMap<AssetId<Mesh>, MeshBindGroupPair>,
lightmaps: HashMap<AssetId<Image>, BindGroup>,
lightmaps: HashMap<LightmapSlabIndex, BindGroup>,
}
pub struct MeshBindGroupPair {
@ -2348,7 +2373,7 @@ impl MeshBindGroups {
pub fn get(
&self,
asset_id: AssetId<Mesh>,
lightmap: Option<AssetId<Image>>,
lightmap: Option<LightmapSlabIndex>,
is_skinned: bool,
morph: bool,
motion_vectors: bool,
@ -2362,7 +2387,7 @@ impl MeshBindGroups {
.skinned
.as_ref()
.map(|bind_group_pair| bind_group_pair.get(motion_vectors)),
(false, false, Some(lightmap)) => self.lightmaps.get(&lightmap),
(false, false, Some(lightmap_slab)) => self.lightmaps.get(&lightmap_slab),
(false, false, None) => self.model_only.as_ref(),
}
}
@ -2381,7 +2406,6 @@ impl MeshBindGroupPair {
#[allow(clippy::too_many_arguments)]
pub fn prepare_mesh_bind_group(
meshes: Res<RenderAssets<RenderMesh>>,
images: Res<RenderAssets<GpuImage>>,
mut groups: ResMut<MeshBindGroups>,
mesh_pipeline: Res<MeshPipeline>,
render_device: Res<RenderDevice>,
@ -2393,7 +2417,7 @@ pub fn prepare_mesh_bind_group(
>,
skins_uniform: Res<SkinUniforms>,
weights_uniform: Res<MorphUniforms>,
render_lightmaps: Res<RenderLightmaps>,
mut render_lightmaps: ResMut<RenderLightmaps>,
) {
groups.reset();
@ -2475,13 +2499,13 @@ pub fn prepare_mesh_bind_group(
}
}
// Create lightmap bindgroups.
for &image_id in &render_lightmaps.all_lightmap_images {
if let (Entry::Vacant(entry), Some(image)) =
(groups.lightmaps.entry(image_id), images.get(image_id))
{
entry.insert(layouts.lightmapped(&render_device, &model, image));
}
// Create lightmap bindgroups. There will be one bindgroup for each slab.
let bindless_supported = render_lightmaps.bindless_supported;
for (lightmap_slab_id, lightmap_slab) in render_lightmaps.slabs.iter_mut().enumerate() {
groups.lightmaps.insert(
LightmapSlabIndex(NonMaxU32::new(lightmap_slab_id as u32).unwrap()),
layouts.lightmapped(&render_device, &model, lightmap_slab, bindless_supported),
);
}
}
@ -2581,14 +2605,14 @@ impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetMeshBindGroup<I> {
let is_skinned = current_skin_index.is_some();
let is_morphed = current_morph_index.is_some();
let lightmap = lightmaps
let lightmap_slab_index = lightmaps
.render_lightmaps
.get(entity)
.map(|render_lightmap| render_lightmap.image);
.map(|render_lightmap| render_lightmap.slab_index);
let Some(bind_group) = bind_groups.get(
mesh_asset_id,
lightmap,
lightmap_slab_index,
is_skinned,
is_morphed,
has_motion_vector_prepass,

View File

@ -1,11 +1,9 @@
//! Bind group layout related definitions for the mesh pipeline.
use bevy_math::Mat4;
use bevy_render::{
mesh::morph::MAX_MORPH_WEIGHTS, render_resource::*, renderer::RenderDevice, texture::GpuImage,
};
use bevy_render::{mesh::morph::MAX_MORPH_WEIGHTS, render_resource::*, renderer::RenderDevice};
use crate::render::skin::MAX_JOINTS;
use crate::{binding_arrays_are_usable, render::skin::MAX_JOINTS, LightmapSlab};
const MORPH_WEIGHT_SIZE: usize = size_of::<f32>();
@ -21,8 +19,10 @@ pub(crate) const JOINT_BUFFER_SIZE: usize = MAX_JOINTS * JOINT_SIZE;
/// Individual layout entries.
mod layout_entry {
use core::num::NonZeroU32;
use super::{JOINT_BUFFER_SIZE, MORPH_BUFFER_SIZE};
use crate::{render::skin, MeshUniform};
use crate::{render::skin, MeshUniform, LIGHTMAPS_PER_SLAB};
use bevy_render::{
render_resource::{
binding_types::{
@ -61,6 +61,16 @@ mod layout_entry {
pub(super) fn lightmaps_sampler() -> BindGroupLayoutEntryBuilder {
sampler(SamplerBindingType::Filtering).visibility(ShaderStages::FRAGMENT)
}
pub(super) fn lightmaps_texture_view_array() -> BindGroupLayoutEntryBuilder {
texture_2d(TextureSampleType::Float { filterable: true })
.visibility(ShaderStages::FRAGMENT)
.count(NonZeroU32::new(LIGHTMAPS_PER_SLAB as u32).unwrap())
}
pub(super) fn lightmaps_sampler_array() -> BindGroupLayoutEntryBuilder {
sampler(SamplerBindingType::Filtering)
.visibility(ShaderStages::FRAGMENT)
.count(NonZeroU32::new(LIGHTMAPS_PER_SLAB as u32).unwrap())
}
}
/// Individual [`BindGroupEntry`]
@ -72,7 +82,7 @@ mod entry {
use bevy_render::{
render_resource::{
BindGroupEntry, BindingResource, Buffer, BufferBinding, BufferSize, Sampler,
TextureView,
TextureView, WgpuSampler, WgpuTextureView,
},
renderer::RenderDevice,
};
@ -123,6 +133,24 @@ mod entry {
resource: BindingResource::Sampler(sampler),
}
}
pub(super) fn lightmaps_texture_view_array<'a>(
binding: u32,
textures: &'a [&'a WgpuTextureView],
) -> BindGroupEntry<'a> {
BindGroupEntry {
binding,
resource: BindingResource::TextureViewArray(textures),
}
}
pub(super) fn lightmaps_sampler_array<'a>(
binding: u32,
samplers: &'a [&'a WgpuSampler],
) -> BindGroupEntry<'a> {
BindGroupEntry {
binding,
resource: BindingResource::SamplerArray(samplers),
}
}
}
/// All possible [`BindGroupLayout`]s in bevy's default mesh shader (`mesh.wgsl`).
@ -302,17 +330,31 @@ impl MeshLayouts {
}
fn lightmapped_layout(render_device: &RenderDevice) -> BindGroupLayout {
render_device.create_bind_group_layout(
"lightmapped_mesh_layout",
&BindGroupLayoutEntries::with_indices(
ShaderStages::VERTEX,
(
(0, layout_entry::model(render_device)),
(4, layout_entry::lightmaps_texture_view()),
(5, layout_entry::lightmaps_sampler()),
if binding_arrays_are_usable(render_device) {
render_device.create_bind_group_layout(
"lightmapped_mesh_layout",
&BindGroupLayoutEntries::with_indices(
ShaderStages::VERTEX,
(
(0, layout_entry::model(render_device)),
(4, layout_entry::lightmaps_texture_view_array()),
(5, layout_entry::lightmaps_sampler_array()),
),
),
),
)
)
} else {
render_device.create_bind_group_layout(
"lightmapped_mesh_layout",
&BindGroupLayoutEntries::with_indices(
ShaderStages::VERTEX,
(
(0, layout_entry::model(render_device)),
(4, layout_entry::lightmaps_texture_view()),
(5, layout_entry::lightmaps_sampler()),
),
),
)
}
}
// ---------- BindGroup methods ----------
@ -329,17 +371,32 @@ impl MeshLayouts {
&self,
render_device: &RenderDevice,
model: &BindingResource,
lightmap: &GpuImage,
lightmap_slab: &LightmapSlab,
bindless_lightmaps: bool,
) -> BindGroup {
render_device.create_bind_group(
"lightmapped_mesh_bind_group",
&self.lightmapped,
&[
entry::model(0, model.clone()),
entry::lightmaps_texture_view(4, &lightmap.texture_view),
entry::lightmaps_sampler(5, &lightmap.sampler),
],
)
if bindless_lightmaps {
let (texture_views, samplers) = lightmap_slab.build_binding_arrays();
render_device.create_bind_group(
"lightmapped_mesh_bind_group",
&self.lightmapped,
&[
entry::model(0, model.clone()),
entry::lightmaps_texture_view_array(4, &texture_views),
entry::lightmaps_sampler_array(5, &samplers),
],
)
} else {
let (texture_view, sampler) = lightmap_slab.bindings_for_first_lightmap();
render_device.create_bind_group(
"lightmapped_mesh_bind_group",
&self.lightmapped,
&[
entry::model(0, model.clone()),
entry::lightmaps_texture_view(4, texture_view),
entry::lightmaps_sampler(5, sampler),
],
)
}
}
/// Creates the bind group for skinned meshes with no morph targets.

View File

@ -25,8 +25,9 @@ struct MeshInput {
first_vertex_index: u32,
current_skin_index: u32,
previous_skin_index: u32,
// Index of the material inside the bind group data.
material_bind_group_slot: u32,
// Low 16 bits: index of the material inside the bind group data.
// High 16 bits: index of the lightmap in the binding array.
material_and_lightmap_bind_group_slot: u32,
}
// Information about each mesh instance needed to cull it on GPU.
@ -196,6 +197,6 @@ fn main(@builtin(global_invocation_id) global_invocation_id: vec3<u32>) {
output[mesh_output_index].first_vertex_index = current_input[input_index].first_vertex_index;
output[mesh_output_index].current_skin_index = current_input[input_index].current_skin_index;
output[mesh_output_index].previous_skin_index = current_input[input_index].previous_skin_index;
output[mesh_output_index].material_bind_group_slot =
current_input[input_index].material_bind_group_slot;
output[mesh_output_index].material_and_lightmap_bind_group_slot =
current_input[input_index].material_and_lightmap_bind_group_slot;
}

View File

@ -19,8 +19,9 @@ struct Mesh {
first_vertex_index: u32,
current_skin_index: u32,
previous_skin_index: u32,
// Index of the material inside the bind group data.
material_bind_group_slot: u32,
// Low 16 bits: index of the material inside the bind group data.
// High 16 bits: index of the lightmap in the binding array.
material_and_lightmap_bind_group_slot: u32,
};
#ifdef SKINNED

View File

@ -6,7 +6,7 @@
}
fn sample_depth_map(uv: vec2<f32>, instance_index: u32) -> f32 {
let slot = mesh[instance_index].material_bind_group_slot;
let slot = mesh[instance_index].material_and_lightmap_bind_group_slot & 0xffffu;
// We use `textureSampleLevel` over `textureSample` because the wgpu DX12
// backend (Fxc) panics when using "gradient instructions" inside a loop.
// It results in the whole loop being unrolled by the shader compiler,

View File

@ -71,7 +71,7 @@ fn pbr_input_from_standard_material(
is_front: bool,
) -> pbr_types::PbrInput {
#ifdef BINDLESS
let slot = mesh[in.instance_index].material_bind_group_slot;
let slot = mesh[in.instance_index].material_and_lightmap_bind_group_slot & 0xffffu;
let flags = pbr_bindings::material[slot].flags;
let base_color = pbr_bindings::material[slot].base_color;
let deferred_lighting_pass_id = pbr_bindings::material[slot].deferred_lighting_pass_id;

View File

@ -17,7 +17,7 @@ fn prepass_alpha_discard(in: VertexOutput) {
#ifdef MAY_DISCARD
#ifdef BINDLESS
let slot = mesh[in.instance_index].material_bind_group_slot;
let slot = mesh[in.instance_index].material_and_lightmap_bind_group_slot & 0xffffu;
var output_color: vec4<f32> = pbr_bindings::material[slot].base_color;
#else // BINDLESS
var output_color: vec4<f32> = pbr_bindings::material.base_color;

View File

@ -256,7 +256,8 @@ impl Deref for BindGroup {
/// shader Bevy will instead present a *binding array* of `COUNT` elements.
/// In your shader, the index of the element of each binding array
/// corresponding to the mesh currently being drawn can be retrieved with
/// `mesh[in.instance_index].material_bind_group_slot`.
/// `mesh[in.instance_index].material_and_lightmap_bind_group_slot &
/// 0xffffu`.
/// * Bindless uniforms don't exist, so in bindless mode all uniforms and
/// uniform buffers are automatically replaced with read-only storage
/// buffers.

View File

@ -274,7 +274,7 @@ fn queue_custom_phase_item(
draw_function: draw_custom_phase_item,
pipeline: pipeline_id,
material_bind_group_index: None,
lightmap_image: None,
lightmap_slab: None,
vertex_slab: default(),
index_slab: None,
},

View File

@ -341,7 +341,7 @@ fn queue_custom_mesh_pipeline(
material_bind_group_index: None,
vertex_slab: default(),
index_slab: None,
lightmap_image: None,
lightmap_slab: None,
},
// The asset ID is arbitrary; we simply use [`AssetId::invalid`],
// but you can use anything you like. Note that the asset ID need