#12502 Remove limit on RenderLayers. (#13317)

# Objective

Remove the limit of `RenderLayer` by using a growable mask using
`SmallVec`.

Changes adopted from @UkoeHB's initial PR here
https://github.com/bevyengine/bevy/pull/12502 that contained additional
changes related to propagating render layers.

Changes

## Solution

The main thing needed to unblock this is removing `RenderLayers` from
our shader code. This primarily affects `DirectionalLight`. We are now
computing a `skip` field on the CPU that is then used to skip the light
in the shader.

## Testing

Checked a variety of examples and did a quick benchmark on `many_cubes`.
There were some existing problems identified during the development of
the original pr (see:
https://discord.com/channels/691052431525675048/1220477928605749340/1221190112939872347).
This PR shouldn't change any existing behavior besides removing the
layer limit (sans the comment in migration about `all` layers no longer
being possible).

---

## Changelog

Removed the limit on `RenderLayers` by using a growable bitset that only
allocates when layers greater than 64 are used.

## Migration Guide

- `RenderLayers::all()` no longer exists. Entities expecting to be
visible on all layers, e.g. lights, should compute the active layers
that are in use.

---------

Co-authored-by: robtfm <50659922+robtfm@users.noreply.github.com>
This commit is contained in:
charlotte 2024-05-16 09:15:47 -07:00 committed by GitHub
parent 05e2552a68
commit 4c3b7679ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 202 additions and 106 deletions

View file

@ -0,0 +1,19 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use bevy_render::view::RenderLayers;
fn render_layers(c: &mut Criterion) {
c.bench_function("layers_intersect", |b| {
let layer_a = RenderLayers::layer(1).with(2);
let layer_b = RenderLayers::layer(1);
b.iter(|| {
black_box(layer_a.intersects(&layer_b))
});
});
}
criterion_group!(
benches,
render_layers,
);
criterion_main!(benches);

View file

@ -28,7 +28,7 @@ mod inset;
/// The [`Camera::order`] index used by the layout debug camera. /// The [`Camera::order`] index used by the layout debug camera.
pub const LAYOUT_DEBUG_CAMERA_ORDER: isize = 255; pub const LAYOUT_DEBUG_CAMERA_ORDER: isize = 255;
/// The [`RenderLayers`] used by the debug gizmos and the debug camera. /// The [`RenderLayers`] used by the debug gizmos and the debug camera.
pub const LAYOUT_DEBUG_LAYERS: RenderLayers = RenderLayers::none().with(16); pub const LAYOUT_DEBUG_LAYERS: RenderLayers = RenderLayers::layer(16);
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
struct LayoutRect { struct LayoutRect {
@ -101,7 +101,7 @@ fn update_debug_camera(
}, },
..default() ..default()
}, },
LAYOUT_DEBUG_LAYERS, LAYOUT_DEBUG_LAYERS.clone(),
DebugOverlayCamera, DebugOverlayCamera,
Name::new("Layout Debug Camera"), Name::new("Layout Debug Camera"),
)) ))
@ -109,7 +109,7 @@ fn update_debug_camera(
}; };
if let Some((config, _)) = gizmo_config.get_config_mut_dyn(&TypeId::of::<UiGizmosDebug>()) { if let Some((config, _)) = gizmo_config.get_config_mut_dyn(&TypeId::of::<UiGizmosDebug>()) {
config.enabled = true; config.enabled = true;
config.render_layers = LAYOUT_DEBUG_LAYERS; config.render_layers = LAYOUT_DEBUG_LAYERS.clone();
} }
let cam = *options.layout_gizmos_camera.get_or_insert_with(spawn_cam); let cam = *options.layout_gizmos_camera.get_or_insert_with(spawn_cam);
let Ok(mut cam) = debug_cams.get_mut(cam) else { let Ok(mut cam) = debug_cams.get_mut(cam) else {

View file

@ -197,7 +197,7 @@ impl From<&GizmoConfig> for GizmoMeshConfig {
GizmoMeshConfig { GizmoMeshConfig {
line_perspective: item.line_perspective, line_perspective: item.line_perspective,
line_style: item.line_style, line_style: item.line_style,
render_layers: item.render_layers, render_layers: item.render_layers.clone(),
} }
} }
} }

View file

@ -269,9 +269,9 @@ fn queue_line_gizmos_2d(
let mesh_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples()) let mesh_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples())
| Mesh2dPipelineKey::from_hdr(view.hdr); | Mesh2dPipelineKey::from_hdr(view.hdr);
let render_layers = render_layers.unwrap_or_default();
for (entity, handle, config) in &line_gizmos { for (entity, handle, config) in &line_gizmos {
let render_layers = render_layers.copied().unwrap_or_default(); if !config.render_layers.intersects(render_layers) {
if !config.render_layers.intersects(&render_layers) {
continue; continue;
} }
@ -325,9 +325,9 @@ fn queue_line_joint_gizmos_2d(
let mesh_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples()) let mesh_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples())
| Mesh2dPipelineKey::from_hdr(view.hdr); | Mesh2dPipelineKey::from_hdr(view.hdr);
let render_layers = render_layers.unwrap_or_default();
for (entity, handle, config) in &line_gizmos { for (entity, handle, config) in &line_gizmos {
let render_layers = render_layers.copied().unwrap_or_default(); if !config.render_layers.intersects(render_layers) {
if !config.render_layers.intersects(&render_layers) {
continue; continue;
} }

View file

@ -303,7 +303,7 @@ fn queue_line_gizmos_3d(
(normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass), (normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass),
) in &mut views ) in &mut views
{ {
let render_layers = render_layers.copied().unwrap_or_default(); let render_layers = render_layers.unwrap_or_default();
let mut view_key = MeshPipelineKey::from_msaa_samples(msaa.samples()) let mut view_key = MeshPipelineKey::from_msaa_samples(msaa.samples())
| MeshPipelineKey::from_hdr(view.hdr); | MeshPipelineKey::from_hdr(view.hdr);
@ -325,7 +325,7 @@ fn queue_line_gizmos_3d(
} }
for (entity, handle, config) in &line_gizmos { for (entity, handle, config) in &line_gizmos {
if !config.render_layers.intersects(&render_layers) { if !config.render_layers.intersects(render_layers) {
continue; continue;
} }
@ -389,7 +389,7 @@ fn queue_line_joint_gizmos_3d(
(normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass), (normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass),
) in &mut views ) in &mut views
{ {
let render_layers = render_layers.copied().unwrap_or_default(); let render_layers = render_layers.unwrap_or_default();
let mut view_key = MeshPipelineKey::from_msaa_samples(msaa.samples()) let mut view_key = MeshPipelineKey::from_msaa_samples(msaa.samples())
| MeshPipelineKey::from_hdr(view.hdr); | MeshPipelineKey::from_hdr(view.hdr);
@ -411,7 +411,7 @@ fn queue_line_joint_gizmos_3d(
} }
for (entity, handle, config) in &line_gizmos { for (entity, handle, config) in &line_gizmos {
if !config.render_layers.intersects(&render_layers) { if !config.render_layers.intersects(render_layers) {
continue; continue;
} }

View file

@ -26,6 +26,7 @@ use crate::*;
mod ambient_light; mod ambient_light;
pub use ambient_light::AmbientLight; pub use ambient_light::AmbientLight;
mod point_light; mod point_light;
pub use point_light::PointLight; pub use point_light::PointLight;
mod spot_light; mod spot_light;
@ -1018,7 +1019,7 @@ pub(crate) fn directional_light_order(
.then_with(|| entity_1.cmp(entity_2)) // stable .then_with(|| entity_1.cmp(entity_2)) // stable
} }
#[derive(Clone, Copy)] #[derive(Clone)]
// data required for assigning lights to clusters // data required for assigning lights to clusters
pub(crate) struct PointLightAssignmentData { pub(crate) struct PointLightAssignmentData {
entity: Entity, entity: Entity,
@ -1108,7 +1109,7 @@ pub(crate) fn assign_lights_to_clusters(
shadows_enabled: point_light.shadows_enabled, shadows_enabled: point_light.shadows_enabled,
range: point_light.range, range: point_light.range,
spot_light_angle: None, spot_light_angle: None,
render_layers: maybe_layers.copied().unwrap_or_default(), render_layers: maybe_layers.unwrap_or_default().clone(),
} }
}, },
), ),
@ -1125,7 +1126,7 @@ pub(crate) fn assign_lights_to_clusters(
shadows_enabled: spot_light.shadows_enabled, shadows_enabled: spot_light.shadows_enabled,
range: spot_light.range, range: spot_light.range,
spot_light_angle: Some(spot_light.outer_angle), spot_light_angle: Some(spot_light.outer_angle),
render_layers: maybe_layers.copied().unwrap_or_default(), render_layers: maybe_layers.unwrap_or_default().clone(),
} }
}, },
), ),
@ -1199,7 +1200,7 @@ pub(crate) fn assign_lights_to_clusters(
mut visible_lights, mut visible_lights,
) in &mut views ) in &mut views
{ {
let view_layers = maybe_layers.copied().unwrap_or_default(); let view_layers = maybe_layers.unwrap_or_default();
let clusters = clusters.into_inner(); let clusters = clusters.into_inner();
if matches!(config, ClusterConfig::None) { if matches!(config, ClusterConfig::None) {
@ -1926,7 +1927,7 @@ pub fn check_light_mesh_visibility(
continue; continue;
} }
let view_mask = maybe_view_mask.copied().unwrap_or_default(); let view_mask = maybe_view_mask.unwrap_or_default();
for ( for (
entity, entity,
@ -1942,8 +1943,8 @@ pub fn check_light_mesh_visibility(
continue; continue;
} }
let entity_mask = maybe_entity_mask.copied().unwrap_or_default(); let entity_mask = maybe_entity_mask.unwrap_or_default();
if !view_mask.intersects(&entity_mask) { if !view_mask.intersects(entity_mask) {
continue; continue;
} }
@ -2016,7 +2017,7 @@ pub fn check_light_mesh_visibility(
continue; continue;
} }
let view_mask = maybe_view_mask.copied().unwrap_or_default(); let view_mask = maybe_view_mask.unwrap_or_default();
let light_sphere = Sphere { let light_sphere = Sphere {
center: Vec3A::from(transform.translation()), center: Vec3A::from(transform.translation()),
radius: point_light.range, radius: point_light.range,
@ -2036,8 +2037,8 @@ pub fn check_light_mesh_visibility(
continue; continue;
} }
let entity_mask = maybe_entity_mask.copied().unwrap_or_default(); let entity_mask = maybe_entity_mask.unwrap_or_default();
if !view_mask.intersects(&entity_mask) { if !view_mask.intersects(entity_mask) {
continue; continue;
} }
@ -2091,7 +2092,7 @@ pub fn check_light_mesh_visibility(
continue; continue;
} }
let view_mask = maybe_view_mask.copied().unwrap_or_default(); let view_mask = maybe_view_mask.unwrap_or_default();
let light_sphere = Sphere { let light_sphere = Sphere {
center: Vec3A::from(transform.translation()), center: Vec3A::from(transform.translation()),
radius: point_light.range, radius: point_light.range,
@ -2111,8 +2112,8 @@ pub fn check_light_mesh_visibility(
continue; continue;
} }
let entity_mask = maybe_entity_mask.copied().unwrap_or_default(); let entity_mask = maybe_entity_mask.unwrap_or_default();
if !view_mask.intersects(&entity_mask) { if !view_mask.intersects(entity_mask) {
continue; continue;
} }

View file

@ -173,7 +173,7 @@ pub struct GpuDirectionalLight {
num_cascades: u32, num_cascades: u32,
cascades_overlap_proportion: f32, cascades_overlap_proportion: f32,
depth_texture_base_index: u32, depth_texture_base_index: u32,
render_layers: u32, skip: u32,
} }
// NOTE: These must match the bit flags in bevy_pbr/src/render/mesh_view_types.wgsl! // NOTE: These must match the bit flags in bevy_pbr/src/render/mesh_view_types.wgsl!
@ -488,7 +488,7 @@ pub fn extract_lights(
cascade_shadow_config: cascade_config.clone(), cascade_shadow_config: cascade_config.clone(),
cascades: cascades.cascades.clone(), cascades: cascades.cascades.clone(),
frusta: frusta.frusta.clone(), frusta: frusta.frusta.clone(),
render_layers: maybe_layers.copied().unwrap_or_default(), render_layers: maybe_layers.unwrap_or_default().clone(),
}, },
render_visible_entities, render_visible_entities,
)); ));
@ -684,7 +684,12 @@ pub fn prepare_lights(
mut global_light_meta: ResMut<GlobalLightMeta>, mut global_light_meta: ResMut<GlobalLightMeta>,
mut light_meta: ResMut<LightMeta>, mut light_meta: ResMut<LightMeta>,
views: Query< views: Query<
(Entity, &ExtractedView, &ExtractedClusterConfig), (
Entity,
&ExtractedView,
&ExtractedClusterConfig,
Option<&RenderLayers>,
),
With<SortedRenderPhase<Transparent3d>>, With<SortedRenderPhase<Transparent3d>>,
>, >,
ambient_light: Res<AmbientLight>, ambient_light: Res<AmbientLight>,
@ -904,6 +909,8 @@ pub fn prepare_lights(
.len() .len()
.min(MAX_CASCADES_PER_LIGHT); .min(MAX_CASCADES_PER_LIGHT);
gpu_directional_lights[index] = GpuDirectionalLight { gpu_directional_lights[index] = GpuDirectionalLight {
// Set to true later when necessary.
skip: 0u32,
// Filled in later. // Filled in later.
cascades: [GpuDirectionalCascade::default(); MAX_CASCADES_PER_LIGHT], cascades: [GpuDirectionalCascade::default(); MAX_CASCADES_PER_LIGHT],
// premultiply color by illuminance // premultiply color by illuminance
@ -917,7 +924,6 @@ pub fn prepare_lights(
num_cascades: num_cascades as u32, num_cascades: num_cascades as u32,
cascades_overlap_proportion: light.cascade_shadow_config.overlap_proportion, cascades_overlap_proportion: light.cascade_shadow_config.overlap_proportion,
depth_texture_base_index: num_directional_cascades_enabled as u32, depth_texture_base_index: num_directional_cascades_enabled as u32,
render_layers: light.render_layers.bits(),
}; };
if index < directional_shadow_enabled_count { if index < directional_shadow_enabled_count {
num_directional_cascades_enabled += num_cascades; num_directional_cascades_enabled += num_cascades;
@ -930,7 +936,7 @@ pub fn prepare_lights(
.write_buffer(&render_device, &render_queue); .write_buffer(&render_device, &render_queue);
// set up light data for each view // set up light data for each view
for (entity, extracted_view, clusters) in &views { for (entity, extracted_view, clusters, maybe_layers) in &views {
let point_light_depth_texture = texture_cache.get( let point_light_depth_texture = texture_cache.get(
&render_device, &render_device,
TextureDescriptor { TextureDescriptor {
@ -1128,11 +1134,25 @@ pub fn prepare_lights(
// directional lights // directional lights
let mut directional_depth_texture_array_index = 0u32; let mut directional_depth_texture_array_index = 0u32;
let view_layers = maybe_layers.unwrap_or_default();
for (light_index, &(light_entity, light)) in directional_lights for (light_index, &(light_entity, light)) in directional_lights
.iter() .iter()
.enumerate() .enumerate()
.take(directional_shadow_enabled_count) .take(MAX_DIRECTIONAL_LIGHTS)
{ {
let gpu_light = &mut gpu_lights.directional_lights[light_index];
// Check if the light intersects with the view.
if !view_layers.intersects(&light.render_layers) {
gpu_light.skip = 1u32;
continue;
}
// Only deal with cascades when shadows are enabled.
if (gpu_light.flags & DirectionalLightFlags::SHADOWS_ENABLED.bits()) == 0u32 {
continue;
}
let cascades = light let cascades = light
.cascades .cascades
.get(&entity) .get(&entity)

View file

@ -33,7 +33,7 @@ struct DirectionalLight {
num_cascades: u32, num_cascades: u32,
cascades_overlap_proportion: f32, cascades_overlap_proportion: f32,
depth_texture_base_index: u32, depth_texture_base_index: u32,
render_layers: u32, skip: u32,
}; };
const DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u; const DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u;

View file

@ -398,10 +398,10 @@ fn apply_pbr_lighting(
// directional lights (direct) // directional lights (direct)
let n_directional_lights = view_bindings::lights.n_directional_lights; let n_directional_lights = view_bindings::lights.n_directional_lights;
for (var i: u32 = 0u; i < n_directional_lights; i = i + 1u) { for (var i: u32 = 0u; i < n_directional_lights; i = i + 1u) {
// check the directional light render layers intersect the view render layers // check if this light should be skipped, which occurs if this light does not intersect with the view
// note this is not necessary for point and spot lights, as the relevant lights are filtered in `assign_lights_to_clusters` // note point and spot lights aren't skippable, as the relevant lights are filtered in `assign_lights_to_clusters`
let light = &view_bindings::lights.directional_lights[i]; let light = &view_bindings::lights.directional_lights[i];
if ((*light).render_layers & view_bindings::view.render_layers) == 0u { if (*light).skip != 0u {
continue; continue;
} }

View file

@ -100,7 +100,7 @@ profiling = { version = "1", features = [
], optional = true } ], optional = true }
async-channel = "2.2.0" async-channel = "2.2.0"
nonmax = "0.5" nonmax = "0.5"
smallvec = "1.11" smallvec = { version = "1.11", features = ["const_new"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
# Omit the `glsl` feature in non-WebAssembly by default. # Omit the `glsl` feature in non-WebAssembly by default.

View file

@ -926,7 +926,7 @@ pub fn extract_cameras(
} }
if let Some(render_layers) = render_layers { if let Some(render_layers) = render_layers {
commands.insert(*render_layers); commands.insert(render_layers.clone());
} }
if let Some(perspective) = projection { if let Some(perspective) = projection {

View file

@ -418,7 +418,6 @@ pub struct ViewUniform {
frustum: [Vec4; 6], frustum: [Vec4; 6],
color_grading: ColorGradingUniform, color_grading: ColorGradingUniform,
mip_bias: f32, mip_bias: f32,
render_layers: u32,
} }
#[derive(Resource)] #[derive(Resource)]
@ -715,7 +714,6 @@ pub fn prepare_view_uniforms(
Option<&Frustum>, Option<&Frustum>,
Option<&TemporalJitter>, Option<&TemporalJitter>,
Option<&MipBias>, Option<&MipBias>,
Option<&RenderLayers>,
)>, )>,
) { ) {
let view_iter = views.iter(); let view_iter = views.iter();
@ -727,16 +725,7 @@ pub fn prepare_view_uniforms(
else { else {
return; return;
}; };
for ( for (entity, extracted_camera, extracted_view, frustum, temporal_jitter, mip_bias) in &views {
entity,
extracted_camera,
extracted_view,
frustum,
temporal_jitter,
mip_bias,
maybe_layers,
) in &views
{
let viewport = extracted_view.viewport.as_vec4(); let viewport = extracted_view.viewport.as_vec4();
let unjittered_projection = extracted_view.projection; let unjittered_projection = extracted_view.projection;
let mut projection = unjittered_projection; let mut projection = unjittered_projection;
@ -779,7 +768,6 @@ pub fn prepare_view_uniforms(
frustum, frustum,
color_grading: extracted_view.color_grading.clone().into(), color_grading: extracted_view.color_grading.clone().into(),
mip_bias: mip_bias.unwrap_or(&MipBias(0.0)).0, mip_bias: mip_bias.unwrap_or(&MipBias(0.0)).0,
render_layers: maybe_layers.copied().unwrap_or_default().bits(),
}), }),
}; };

View file

@ -28,5 +28,4 @@ struct View {
frustum: array<vec4<f32>, 6>, frustum: array<vec4<f32>, 6>,
color_grading: ColorGrading, color_grading: ColorGrading,
mip_bias: f32, mip_bias: f32,
render_layers: u32,
}; };

View file

@ -429,7 +429,7 @@ pub fn check_visibility<QF>(
continue; continue;
} }
let view_mask = maybe_view_mask.copied().unwrap_or_default(); let view_mask = maybe_view_mask.unwrap_or_default();
visible_aabb_query.par_iter_mut().for_each_init( visible_aabb_query.par_iter_mut().for_each_init(
|| thread_queues.borrow_local_mut(), || thread_queues.borrow_local_mut(),
@ -451,8 +451,8 @@ pub fn check_visibility<QF>(
return; return;
} }
let entity_mask = maybe_entity_mask.copied().unwrap_or_default(); let entity_mask = maybe_entity_mask.unwrap_or_default();
if !view_mask.intersects(&entity_mask) { if !view_mask.intersects(entity_mask) {
return; return;
} }

View file

@ -1,11 +1,12 @@
use bevy_ecs::prelude::{Component, ReflectComponent}; use bevy_ecs::prelude::{Component, ReflectComponent};
use bevy_reflect::std_traits::ReflectDefault; use bevy_reflect::std_traits::ReflectDefault;
use bevy_reflect::Reflect; use bevy_reflect::Reflect;
use smallvec::SmallVec;
type LayerMask = u32; pub const DEFAULT_LAYERS: &RenderLayers = &RenderLayers::layer(0);
/// An identifier for a rendering layer. /// An identifier for a rendering layer.
pub type Layer = u8; pub type Layer = usize;
/// Describes which rendering layers an entity belongs to. /// Describes which rendering layers an entity belongs to.
/// ///
@ -20,9 +21,15 @@ pub type Layer = u8;
/// An entity with this component without any layers is invisible. /// An entity with this component without any layers is invisible.
/// ///
/// Entities without this component belong to layer `0`. /// Entities without this component belong to layer `0`.
#[derive(Component, Copy, Clone, Reflect, PartialEq, Eq, PartialOrd, Ord)] #[derive(Component, Clone, Reflect, PartialEq, Eq, PartialOrd, Ord)]
#[reflect(Component, Default, PartialEq)] #[reflect(Component, Default, PartialEq)]
pub struct RenderLayers(LayerMask); pub struct RenderLayers(SmallVec<[u64; 1]>);
impl Default for &RenderLayers {
fn default() -> Self {
DEFAULT_LAYERS
}
}
impl std::fmt::Debug for RenderLayers { impl std::fmt::Debug for RenderLayers {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
@ -41,27 +48,25 @@ impl FromIterator<Layer> for RenderLayers {
impl Default for RenderLayers { impl Default for RenderLayers {
/// By default, this structure includes layer `0`, which represents the first layer. /// By default, this structure includes layer `0`, which represents the first layer.
fn default() -> Self { fn default() -> Self {
RenderLayers::layer(0) let (_, bit) = Self::layer_info(0);
RenderLayers(SmallVec::from_const([bit]))
} }
} }
impl RenderLayers { impl RenderLayers {
/// The total number of layers supported.
pub const TOTAL_LAYERS: usize = std::mem::size_of::<LayerMask>() * 8;
/// Create a new `RenderLayers` belonging to the given layer. /// Create a new `RenderLayers` belonging to the given layer.
pub const fn layer(n: Layer) -> Self { pub const fn layer(n: Layer) -> Self {
RenderLayers(0).with(n) let (buffer_index, bit) = Self::layer_info(n);
} assert!(
buffer_index < 1,
/// Create a new `RenderLayers` that belongs to all layers. "layer is out of bounds for const construction"
pub const fn all() -> Self { );
RenderLayers(u32::MAX) RenderLayers(SmallVec::from_const([bit]))
} }
/// Create a new `RenderLayers` that belongs to no layers. /// Create a new `RenderLayers` that belongs to no layers.
pub const fn none() -> Self { pub const fn none() -> Self {
RenderLayers(0) RenderLayers(SmallVec::from_const([0]))
} }
/// Create a `RenderLayers` from a list of layers. /// Create a `RenderLayers` from a list of layers.
@ -72,33 +77,28 @@ impl RenderLayers {
/// Add the given layer. /// Add the given layer.
/// ///
/// This may be called multiple times to allow an entity to belong /// This may be called multiple times to allow an entity to belong
/// to multiple rendering layers. The maximum layer is `TOTAL_LAYERS - 1`. /// to multiple rendering layers.
///
/// # Panics
/// Panics when called with a layer greater than `TOTAL_LAYERS - 1`.
#[must_use] #[must_use]
pub const fn with(mut self, layer: Layer) -> Self { pub fn with(mut self, layer: Layer) -> Self {
assert!((layer as usize) < Self::TOTAL_LAYERS); let (buffer_index, bit) = Self::layer_info(layer);
self.0 |= 1 << layer; self.extend_buffer(buffer_index + 1);
self.0[buffer_index] |= bit;
self self
} }
/// Removes the given rendering layer. /// Removes the given rendering layer.
///
/// # Panics
/// Panics when called with a layer greater than `TOTAL_LAYERS - 1`.
#[must_use] #[must_use]
pub const fn without(mut self, layer: Layer) -> Self { pub fn without(mut self, layer: Layer) -> Self {
assert!((layer as usize) < Self::TOTAL_LAYERS); let (buffer_index, bit) = Self::layer_info(layer);
self.0 &= !(1 << layer); if buffer_index < self.0.len() {
self.0[buffer_index] &= !bit;
}
self self
} }
/// Get an iterator of the layers. /// Get an iterator of the layers.
pub fn iter(&self) -> impl Iterator<Item = Layer> { pub fn iter(&self) -> impl Iterator<Item = Layer> + '_ {
let total: Layer = std::convert::TryInto::try_into(Self::TOTAL_LAYERS).unwrap(); self.0.iter().copied().zip(0..).flat_map(Self::iter_layers)
let mask = *self;
(0..total).filter(move |g| RenderLayers::layer(*g).intersects(&mask))
} }
/// Determine if a `RenderLayers` intersects another. /// Determine if a `RenderLayers` intersects another.
@ -108,40 +108,95 @@ impl RenderLayers {
/// A `RenderLayers` with no layers will not match any other /// A `RenderLayers` with no layers will not match any other
/// `RenderLayers`, even another with no layers. /// `RenderLayers`, even another with no layers.
pub fn intersects(&self, other: &RenderLayers) -> bool { pub fn intersects(&self, other: &RenderLayers) -> bool {
(self.0 & other.0) > 0 // Check for the common case where the view layer and entity layer
// both point towards our default layer.
if self.0.as_ptr() == other.0.as_ptr() {
return true;
}
for (self_layer, other_layer) in self.0.iter().zip(other.0.iter()) {
if (*self_layer & *other_layer) != 0 {
return true;
}
}
false
} }
/// get the bitmask representation of the contained layers /// get the bitmask representation of the contained layers
pub fn bits(&self) -> u32 { pub fn bits(&self) -> &[u64] {
self.0 self.0.as_slice()
}
const fn layer_info(layer: usize) -> (usize, u64) {
let buffer_index = layer / 64;
let bit_index = layer % 64;
let bit = 1u64 << bit_index;
(buffer_index, bit)
}
fn extend_buffer(&mut self, other_len: usize) {
let new_size = std::cmp::max(self.0.len(), other_len);
self.0.reserve_exact(new_size - self.0.len());
self.0.resize(new_size, 0u64);
}
fn iter_layers(buffer_and_offset: (u64, usize)) -> impl Iterator<Item = Layer> + 'static {
let (mut buffer, mut layer) = buffer_and_offset;
layer *= 64;
std::iter::from_fn(move || {
if buffer == 0 {
return None;
}
let next = buffer.trailing_zeros() + 1;
buffer >>= next;
layer += next as usize;
Some(layer - 1)
})
} }
} }
#[cfg(test)] #[cfg(test)]
mod rendering_mask_tests { mod rendering_mask_tests {
use super::{Layer, RenderLayers}; use super::{Layer, RenderLayers};
use smallvec::SmallVec;
#[test] #[test]
fn rendering_mask_sanity() { fn rendering_mask_sanity() {
let layer_0 = RenderLayers::layer(0);
assert_eq!(layer_0.0.len(), 1, "layer 0 is one buffer");
assert_eq!(layer_0.0[0], 1, "layer 0 is mask 1");
let layer_1 = RenderLayers::layer(1);
assert_eq!(layer_1.0.len(), 1, "layer 1 is one buffer");
assert_eq!(layer_1.0[0], 2, "layer 1 is mask 2");
let layer_0_1 = RenderLayers::layer(0).with(1);
assert_eq!(layer_0_1.0.len(), 1, "layer 0 + 1 is one buffer");
assert_eq!(layer_0_1.0[0], 3, "layer 0 + 1 is mask 3");
let layer_0_1_without_0 = layer_0_1.without(0);
assert_eq!( assert_eq!(
RenderLayers::TOTAL_LAYERS, layer_0_1_without_0.0.len(),
32, 1,
"total layers is what we think it is" "layer 0 + 1 - 0 is one buffer"
); );
assert_eq!(RenderLayers::layer(0).0, 1, "layer 0 is mask 1"); assert_eq!(layer_0_1_without_0.0[0], 2, "layer 0 + 1 - 0 is mask 2");
assert_eq!(RenderLayers::layer(1).0, 2, "layer 1 is mask 2"); let layer_0_2345 = RenderLayers::layer(0).with(2345);
assert_eq!(RenderLayers::layer(0).with(1).0, 3, "layer 0 + 1 is mask 3"); assert_eq!(layer_0_2345.0.len(), 37, "layer 0 + 2345 is 37 buffers");
assert_eq!(layer_0_2345.0[0], 1, "layer 0 + 2345 is mask 1");
assert_eq!( assert_eq!(
RenderLayers::layer(0).with(1).without(0).0, layer_0_2345.0[36], 2199023255552,
2, "layer 0 + 2345 is mask 2199023255552"
"layer 0 + 1 - 0 is mask 2" );
assert!(
layer_0_2345.intersects(&layer_0),
"layer 0 + 2345 intersects 0"
); );
assert!( assert!(
RenderLayers::layer(1).intersects(&RenderLayers::layer(1)), RenderLayers::layer(1).intersects(&RenderLayers::layer(1)),
"layers match like layers" "layers match like layers"
); );
assert!( assert!(
RenderLayers::layer(0).intersects(&RenderLayers(1)), RenderLayers::layer(0).intersects(&RenderLayers(SmallVec::from_const([1]))),
"a layer of 0 means the mask is just 1 bit" "a layer of 0 means the mask is just 1 bit"
); );
@ -162,7 +217,7 @@ mod rendering_mask_tests {
"masks with differing layers do not match" "masks with differing layers do not match"
); );
assert!( assert!(
!RenderLayers(0).intersects(&RenderLayers(0)), !RenderLayers::none().intersects(&RenderLayers::none()),
"empty masks don't match" "empty masks don't match"
); );
assert_eq!( assert_eq!(
@ -182,5 +237,10 @@ mod rendering_mask_tests {
<RenderLayers as FromIterator<Layer>>::from_iter(vec![0, 1, 2]), <RenderLayers as FromIterator<Layer>>::from_iter(vec![0, 1, 2]),
"from_layers and from_iter are equivalent" "from_layers and from_iter are equivalent"
); );
let tricky_layers = vec![0, 5, 17, 55, 999, 1025, 1026];
let layers = RenderLayers::from_layers(&tricky_layers);
let out = layers.iter().collect::<Vec<_>>();
assert_eq!(tricky_layers, out, "tricky layers roundtrip");
} }
} }

View file

@ -82,19 +82,17 @@ fn setup(
..default() ..default()
}, },
FirstPassCube, FirstPassCube,
first_pass_layer, first_pass_layer.clone(),
)); ));
// Light // Light for the first pass
// NOTE: we add the light to all layers so it affects both the rendered-to-texture cube, and the cube on which we display the texture // NOTE: Lights only work properly when in one render layer.
// Setting the layer to RenderLayers::layer(0) would cause the main view to be lit, but the rendered-to-texture cube to be unlit.
// Setting the layer to RenderLayers::layer(1) would cause the rendered-to-texture cube to be lit, but the main view to be unlit.
commands.spawn(( commands.spawn((
PointLightBundle { PointLightBundle {
transform: Transform::from_translation(Vec3::new(0.0, 0.0, 10.0)), transform: Transform::from_translation(Vec3::new(0.0, 0.0, 10.0)),
..default() ..default()
}, },
RenderLayers::all(), first_pass_layer.clone(),
)); ));
commands.spawn(( commands.spawn((
@ -136,6 +134,17 @@ fn setup(
MainPassCube, MainPassCube,
)); ));
// Light
// NOTE: we add the light to both layers so it affects both the rendered-to-texture cube, and the cube on which we display the texture
// Setting the layer to RenderLayers::layer(0) would cause the main view to be lit, but the rendered-to-texture cube to be unlit.
// Setting the layer to RenderLayers::layer(1) would cause the rendered-to-texture cube to be lit, but the main view to be unlit.
commands.spawn((
PointLightBundle {
transform: Transform::from_translation(Vec3::new(0.0, 0.0, 10.0)),
..default()
},
RenderLayers::layer(0).with(1),
));
// The main pass camera. // The main pass camera.
commands.spawn(Camera3dBundle { commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(0.0, 0.0, 15.0).looking_at(Vec3::ZERO, Vec3::Y), transform: Transform::from_xyz(0.0, 0.0, 15.0).looking_at(Vec3::ZERO, Vec3::Y),