
# Objective Fixes a part of #14274. Bevy has an incredibly inconsistent naming convention for its system sets, both internally and across the ecosystem. <img alt="System sets in Bevy" src="https://github.com/user-attachments/assets/d16e2027-793f-4ba4-9cc9-e780b14a5a1b" width="450" /> *Names of public system set types in Bevy* Most Bevy types use a naming of `FooSystem` or just `Foo`, but there are also a few `FooSystems` and `FooSet` types. In ecosystem crates on the other hand, `FooSet` is perhaps the most commonly used name in general. Conventions being so wildly inconsistent can make it harder for users to pick names for their own types, to search for system sets on docs.rs, or to even discern which types *are* system sets. To reign in the inconsistency a bit and help unify the ecosystem, it would be good to establish a common recommended naming convention for system sets in Bevy itself, similar to how plugins are commonly suffixed with `Plugin` (ex: `TimePlugin`). By adopting a consistent naming convention in first-party Bevy, we can softly nudge ecosystem crates to follow suit (for types where it makes sense to do so). Choosing a naming convention is also relevant now, as the [`bevy_cli` recently adopted lints](https://github.com/TheBevyFlock/bevy_cli/pull/345) to enforce naming for plugins and system sets, and the recommended naming used for system sets is still a bit open. ## Which Name To Use? Now the contentious part: what naming convention should we actually adopt? This was discussed on the Bevy Discord at the end of last year, starting [here](<https://discord.com/channels/691052431525675048/692572690833473578/1310659954683936789>). `FooSet` and `FooSystems` were the clear favorites, with `FooSet` very narrowly winning an unofficial poll. However, it seems to me like the consensus was broadly moving towards `FooSystems` at the end and after the poll, with Cart ([source](https://discord.com/channels/691052431525675048/692572690833473578/1311140204974706708)) and later Alice ([source](https://discord.com/channels/691052431525675048/692572690833473578/1311092530732859533)) and also me being in favor of it. Let's do a quick pros and cons list! Of course these are just what I thought of, so take it with a grain of salt. `FooSet`: - Pro: Nice and short! - Pro: Used by many ecosystem crates. - Pro: The `Set` suffix comes directly from the trait name `SystemSet`. - Pro: Pairs nicely with existing APIs like `in_set` and `configure_sets`. - Con: `Set` by itself doesn't actually indicate that it's related to systems *at all*, apart from the implemented trait. A set of what? - Con: Is `FooSet` a set of `Foo`s or a system set related to `Foo`? Ex: `ContactSet`, `MeshSet`, `EnemySet`... `FooSystems`: - Pro: Very clearly indicates that the type represents a collection of systems. The actual core concept, system(s), is in the name. - Pro: Parallels nicely with `FooPlugins` for plugin groups. - Pro: Low risk of conflicts with other names or misunderstandings about what the type is. - Pro: In most cases, reads *very* nicely and clearly. Ex: `PhysicsSystems` and `AnimationSystems` as opposed to `PhysicsSet` and `AnimationSet`. - Pro: Easy to search for on docs.rs. - Con: Usually results in longer names. - Con: Not yet as widely used. Really the big problem with `FooSet` is that it doesn't actually describe what it is. It describes what *kind of thing* it is (a set of something), but not *what it is a set of*, unless you know the type or check its docs or implemented traits. `FooSystems` on the other hand is much more self-descriptive in this regard, at the cost of being a bit longer to type. Ultimately, in some ways it comes down to preference and how you think of system sets. Personally, I was originally in favor of `FooSet`, but have been increasingly on the side of `FooSystems`, especially after seeing what the new names would actually look like in Avian and now Bevy. I prefer it because it usually reads better, is much more clearly related to groups of systems than `FooSet`, and overall *feels* more correct and natural to me in the long term. For these reasons, and because Alice and Cart also seemed to share a preference for it when it was previously being discussed, I propose that we adopt a `FooSystems` naming convention where applicable. ## Solution Rename Bevy's system set types to use a consistent `FooSet` naming where applicable. - `AccessibilitySystem` → `AccessibilitySystems` - `GizmoRenderSystem` → `GizmoRenderSystems` - `PickSet` → `PickingSystems` - `RunFixedMainLoopSystem` → `RunFixedMainLoopSystems` - `TransformSystem` → `TransformSystems` - `RemoteSet` → `RemoteSystems` - `RenderSet` → `RenderSystems` - `SpriteSystem` → `SpriteSystems` - `StateTransitionSteps` → `StateTransitionSystems` - `RenderUiSystem` → `RenderUiSystems` - `UiSystem` → `UiSystems` - `Animation` → `AnimationSystems` - `AssetEvents` → `AssetEventSystems` - `TrackAssets` → `AssetTrackingSystems` - `UpdateGizmoMeshes` → `GizmoMeshSystems` - `InputSystem` → `InputSystems` - `InputFocusSet` → `InputFocusSystems` - `ExtractMaterialsSet` → `MaterialExtractionSystems` - `ExtractMeshesSet` → `MeshExtractionSystems` - `RumbleSystem` → `RumbleSystems` - `CameraUpdateSystem` → `CameraUpdateSystems` - `ExtractAssetsSet` → `AssetExtractionSystems` - `Update2dText` → `Text2dUpdateSystems` - `TimeSystem` → `TimeSystems` - `AudioPlaySet` → `AudioPlaybackSystems` - `SendEvents` → `EventSenderSystems` - `EventUpdates` → `EventUpdateSystems` A lot of the names got slightly longer, but they are also a lot more consistent, and in my opinion the majority of them read much better. For a few of the names I took the liberty of rewording things a bit; definitely open to any further naming improvements. There are still also cases where the `FooSystems` naming doesn't really make sense, and those I left alone. This primarily includes system sets like `Interned<dyn SystemSet>`, `EnterSchedules<S>`, `ExitSchedules<S>`, or `TransitionSchedules<S>`, where the type has some special purpose and semantics. ## Todo - [x] Should I keep all the old names as deprecated type aliases? I can do this, but to avoid wasting work I'd prefer to first reach consensus on whether these renames are even desired. - [x] Migration guide - [x] Release notes
389 lines
14 KiB
Rust
389 lines
14 KiB
Rust
//! Clustered decals, bounding regions that project textures onto surfaces.
|
||
//!
|
||
//! A *clustered decal* is a bounding box that projects a texture onto any
|
||
//! surface within its bounds along the positive Z axis. In Bevy, clustered
|
||
//! decals use the *clustered forward* rendering technique.
|
||
//!
|
||
//! Clustered decals are the highest-quality types of decals that Bevy supports,
|
||
//! but they require bindless textures. This means that they presently can't be
|
||
//! used on WebGL 2, WebGPU, macOS, or iOS. Bevy's clustered decals can be used
|
||
//! with forward or deferred rendering and don't require a prepass.
|
||
//!
|
||
//! On their own, clustered decals only project the base color of a texture. You
|
||
//! can, however, use the built-in *tag* field to customize the appearance of a
|
||
//! clustered decal arbitrarily. See the documentation in `clustered.wgsl` for
|
||
//! more information and the `clustered_decals` example for an example of use.
|
||
|
||
use core::{num::NonZero, ops::Deref};
|
||
|
||
use bevy_app::{App, Plugin};
|
||
use bevy_asset::{load_internal_asset, weak_handle, AssetId, Handle};
|
||
use bevy_derive::{Deref, DerefMut};
|
||
use bevy_ecs::{
|
||
component::Component,
|
||
entity::{Entity, EntityHashMap},
|
||
prelude::ReflectComponent,
|
||
query::With,
|
||
resource::Resource,
|
||
schedule::IntoScheduleConfigs as _,
|
||
system::{Query, Res, ResMut},
|
||
};
|
||
use bevy_image::Image;
|
||
use bevy_math::Mat4;
|
||
use bevy_platform::collections::HashMap;
|
||
use bevy_reflect::Reflect;
|
||
use bevy_render::{
|
||
extract_component::{ExtractComponent, ExtractComponentPlugin},
|
||
render_asset::RenderAssets,
|
||
render_resource::{
|
||
binding_types, BindGroupLayoutEntryBuilder, Buffer, BufferUsages, RawBufferVec, Sampler,
|
||
SamplerBindingType, Shader, ShaderType, TextureSampleType, TextureView,
|
||
},
|
||
renderer::{RenderAdapter, RenderDevice, RenderQueue},
|
||
sync_world::RenderEntity,
|
||
texture::{FallbackImage, GpuImage},
|
||
view::{self, ViewVisibility, Visibility, VisibilityClass},
|
||
Extract, ExtractSchedule, Render, RenderApp, RenderSystems,
|
||
};
|
||
use bevy_transform::{components::GlobalTransform, prelude::Transform};
|
||
use bytemuck::{Pod, Zeroable};
|
||
|
||
use crate::{
|
||
binding_arrays_are_usable, prepare_lights, GlobalClusterableObjectMeta, LightVisibilityClass,
|
||
};
|
||
|
||
/// The handle to the `clustered.wgsl` shader.
|
||
pub(crate) const CLUSTERED_DECAL_SHADER_HANDLE: Handle<Shader> =
|
||
weak_handle!("87929002-3509-42f1-8279-2d2765dd145c");
|
||
|
||
/// The maximum number of decals that can be present in a view.
|
||
///
|
||
/// This number is currently relatively low in order to work around the lack of
|
||
/// first-class binding arrays in `wgpu`. When that feature is implemented, this
|
||
/// limit can be increased.
|
||
pub(crate) const MAX_VIEW_DECALS: usize = 8;
|
||
|
||
/// A plugin that adds support for clustered decals.
|
||
///
|
||
/// In environments where bindless textures aren't available, clustered decals
|
||
/// can still be added to a scene, but they won't project any decals.
|
||
pub struct ClusteredDecalPlugin;
|
||
|
||
/// An object that projects a decal onto surfaces within its bounds.
|
||
///
|
||
/// Conceptually, a clustered decal is a 1×1×1 cube centered on its origin. It
|
||
/// projects the given [`Self::image`] onto surfaces in the +Z direction (thus
|
||
/// you may find [`Transform::looking_at`] useful).
|
||
///
|
||
/// Clustered decals are the highest-quality types of decals that Bevy supports,
|
||
/// but they require bindless textures. This means that they presently can't be
|
||
/// used on WebGL 2, WebGPU, macOS, or iOS. Bevy's clustered decals can be used
|
||
/// with forward or deferred rendering and don't require a prepass.
|
||
#[derive(Component, Debug, Clone, Reflect, ExtractComponent)]
|
||
#[reflect(Component, Debug, Clone)]
|
||
#[require(Transform, Visibility, VisibilityClass)]
|
||
#[component(on_add = view::add_visibility_class::<LightVisibilityClass>)]
|
||
pub struct ClusteredDecal {
|
||
/// The image that the clustered decal projects.
|
||
///
|
||
/// This must be a 2D image. If it has an alpha channel, it'll be alpha
|
||
/// blended with the underlying surface and/or other decals. All decal
|
||
/// images in the scene must use the same sampler.
|
||
pub image: Handle<Image>,
|
||
|
||
/// An application-specific tag you can use for any purpose you want.
|
||
///
|
||
/// See the `clustered_decals` example for an example of use.
|
||
pub tag: u32,
|
||
}
|
||
|
||
/// Stores information about all the clustered decals in the scene.
|
||
#[derive(Resource, Default)]
|
||
pub struct RenderClusteredDecals {
|
||
/// Maps an index in the shader binding array to the associated decal image.
|
||
///
|
||
/// [`Self::texture_to_binding_index`] holds the inverse mapping.
|
||
binding_index_to_textures: Vec<AssetId<Image>>,
|
||
/// Maps a decal image to the shader binding array.
|
||
///
|
||
/// [`Self::binding_index_to_textures`] holds the inverse mapping.
|
||
texture_to_binding_index: HashMap<AssetId<Image>, u32>,
|
||
/// The information concerning each decal that we provide to the shader.
|
||
decals: Vec<RenderClusteredDecal>,
|
||
/// Maps the [`bevy_render::sync_world::RenderEntity`] of each decal to the
|
||
/// index of that decal in the [`Self::decals`] list.
|
||
entity_to_decal_index: EntityHashMap<usize>,
|
||
}
|
||
|
||
impl RenderClusteredDecals {
|
||
/// Clears out this [`RenderClusteredDecals`] in preparation for a new
|
||
/// frame.
|
||
fn clear(&mut self) {
|
||
self.binding_index_to_textures.clear();
|
||
self.texture_to_binding_index.clear();
|
||
self.decals.clear();
|
||
self.entity_to_decal_index.clear();
|
||
}
|
||
}
|
||
|
||
/// The per-view bind group entries pertaining to decals.
|
||
pub(crate) struct RenderViewClusteredDecalBindGroupEntries<'a> {
|
||
/// The list of decals, corresponding to `mesh_view_bindings::decals` in the
|
||
/// shader.
|
||
pub(crate) decals: &'a Buffer,
|
||
/// The list of textures, corresponding to
|
||
/// `mesh_view_bindings::decal_textures` in the shader.
|
||
pub(crate) texture_views: Vec<&'a <TextureView as Deref>::Target>,
|
||
/// The sampler that the shader uses to sample decals, corresponding to
|
||
/// `mesh_view_bindings::decal_sampler` in the shader.
|
||
pub(crate) sampler: &'a Sampler,
|
||
}
|
||
|
||
/// A render-world resource that holds the buffer of [`ClusteredDecal`]s ready
|
||
/// to upload to the GPU.
|
||
#[derive(Resource, Deref, DerefMut)]
|
||
pub struct DecalsBuffer(RawBufferVec<RenderClusteredDecal>);
|
||
|
||
impl Default for DecalsBuffer {
|
||
fn default() -> Self {
|
||
DecalsBuffer(RawBufferVec::new(BufferUsages::STORAGE))
|
||
}
|
||
}
|
||
|
||
impl Plugin for ClusteredDecalPlugin {
|
||
fn build(&self, app: &mut App) {
|
||
load_internal_asset!(
|
||
app,
|
||
CLUSTERED_DECAL_SHADER_HANDLE,
|
||
"clustered.wgsl",
|
||
Shader::from_wgsl
|
||
);
|
||
|
||
app.add_plugins(ExtractComponentPlugin::<ClusteredDecal>::default())
|
||
.register_type::<ClusteredDecal>();
|
||
|
||
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
|
||
return;
|
||
};
|
||
|
||
render_app
|
||
.init_resource::<DecalsBuffer>()
|
||
.init_resource::<RenderClusteredDecals>()
|
||
.add_systems(ExtractSchedule, extract_decals)
|
||
.add_systems(
|
||
Render,
|
||
prepare_decals
|
||
.in_set(RenderSystems::ManageViews)
|
||
.after(prepare_lights),
|
||
)
|
||
.add_systems(
|
||
Render,
|
||
upload_decals.in_set(RenderSystems::PrepareResources),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// The GPU data structure that stores information about each decal.
|
||
#[derive(Clone, Copy, Default, ShaderType, Pod, Zeroable)]
|
||
#[repr(C)]
|
||
pub struct RenderClusteredDecal {
|
||
/// The inverse of the model matrix.
|
||
///
|
||
/// The shader uses this in order to back-transform world positions into
|
||
/// model space.
|
||
local_from_world: Mat4,
|
||
/// The index of the decal texture in the binding array.
|
||
image_index: u32,
|
||
/// A custom tag available for application-defined purposes.
|
||
tag: u32,
|
||
/// Padding.
|
||
pad_a: u32,
|
||
/// Padding.
|
||
pad_b: u32,
|
||
}
|
||
|
||
/// Extracts decals from the main world into the render world.
|
||
pub fn extract_decals(
|
||
decals: Extract<
|
||
Query<(
|
||
RenderEntity,
|
||
&ClusteredDecal,
|
||
&GlobalTransform,
|
||
&ViewVisibility,
|
||
)>,
|
||
>,
|
||
mut render_decals: ResMut<RenderClusteredDecals>,
|
||
) {
|
||
// Clear out the `RenderDecals` in preparation for a new frame.
|
||
render_decals.clear();
|
||
|
||
// Loop over each decal.
|
||
for (decal_entity, clustered_decal, global_transform, view_visibility) in &decals {
|
||
// If the decal is invisible, skip it.
|
||
if !view_visibility.get() {
|
||
continue;
|
||
}
|
||
|
||
// Insert or add the image.
|
||
let image_index = render_decals.get_or_insert_image(&clustered_decal.image.id());
|
||
|
||
// Record the decal.
|
||
let decal_index = render_decals.decals.len();
|
||
render_decals
|
||
.entity_to_decal_index
|
||
.insert(decal_entity, decal_index);
|
||
|
||
render_decals.decals.push(RenderClusteredDecal {
|
||
local_from_world: global_transform.affine().inverse().into(),
|
||
image_index,
|
||
tag: clustered_decal.tag,
|
||
pad_a: 0,
|
||
pad_b: 0,
|
||
});
|
||
}
|
||
}
|
||
|
||
/// Adds all decals in the scene to the [`GlobalClusterableObjectMeta`] table.
|
||
fn prepare_decals(
|
||
decals: Query<Entity, With<ClusteredDecal>>,
|
||
mut global_clusterable_object_meta: ResMut<GlobalClusterableObjectMeta>,
|
||
render_decals: Res<RenderClusteredDecals>,
|
||
) {
|
||
for decal_entity in &decals {
|
||
if let Some(index) = render_decals.entity_to_decal_index.get(&decal_entity) {
|
||
global_clusterable_object_meta
|
||
.entity_to_index
|
||
.insert(decal_entity, *index);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Returns the layout for the clustered-decal-related bind group entries for a
|
||
/// single view.
|
||
pub(crate) fn get_bind_group_layout_entries(
|
||
render_device: &RenderDevice,
|
||
render_adapter: &RenderAdapter,
|
||
) -> Option<[BindGroupLayoutEntryBuilder; 3]> {
|
||
// If binding arrays aren't supported on the current platform, we have no
|
||
// bind group layout entries.
|
||
if !clustered_decals_are_usable(render_device, render_adapter) {
|
||
return None;
|
||
}
|
||
|
||
Some([
|
||
// `decals`
|
||
binding_types::storage_buffer_read_only::<RenderClusteredDecal>(false),
|
||
// `decal_textures`
|
||
binding_types::texture_2d(TextureSampleType::Float { filterable: true })
|
||
.count(NonZero::<u32>::new(MAX_VIEW_DECALS as u32).unwrap()),
|
||
// `decal_sampler`
|
||
binding_types::sampler(SamplerBindingType::Filtering),
|
||
])
|
||
}
|
||
|
||
impl<'a> RenderViewClusteredDecalBindGroupEntries<'a> {
|
||
/// Creates and returns the bind group entries for clustered decals for a
|
||
/// single view.
|
||
pub(crate) fn get(
|
||
render_decals: &RenderClusteredDecals,
|
||
decals_buffer: &'a DecalsBuffer,
|
||
images: &'a RenderAssets<GpuImage>,
|
||
fallback_image: &'a FallbackImage,
|
||
render_device: &RenderDevice,
|
||
render_adapter: &RenderAdapter,
|
||
) -> Option<RenderViewClusteredDecalBindGroupEntries<'a>> {
|
||
// Skip the entries if decals are unsupported on the current platform.
|
||
if !clustered_decals_are_usable(render_device, render_adapter) {
|
||
return None;
|
||
}
|
||
|
||
// We use the first sampler among all the images. This assumes that all
|
||
// images use the same sampler, which is a documented restriction. If
|
||
// there's no sampler, we just use the one from the fallback image.
|
||
let sampler = match render_decals
|
||
.binding_index_to_textures
|
||
.iter()
|
||
.filter_map(|image_id| images.get(*image_id))
|
||
.next()
|
||
{
|
||
Some(gpu_image) => &gpu_image.sampler,
|
||
None => &fallback_image.d2.sampler,
|
||
};
|
||
|
||
// Gather up the decal textures.
|
||
let mut texture_views = vec![];
|
||
for image_id in &render_decals.binding_index_to_textures {
|
||
match images.get(*image_id) {
|
||
None => texture_views.push(&*fallback_image.d2.texture_view),
|
||
Some(gpu_image) => texture_views.push(&*gpu_image.texture_view),
|
||
}
|
||
}
|
||
|
||
// Pad out the binding array to its maximum length, which is
|
||
// required on some platforms.
|
||
while texture_views.len() < MAX_VIEW_DECALS {
|
||
texture_views.push(&*fallback_image.d2.texture_view);
|
||
}
|
||
|
||
Some(RenderViewClusteredDecalBindGroupEntries {
|
||
decals: decals_buffer.buffer()?,
|
||
texture_views,
|
||
sampler,
|
||
})
|
||
}
|
||
}
|
||
|
||
impl RenderClusteredDecals {
|
||
/// Returns the index of the given image in the decal texture binding array,
|
||
/// adding it to the list if necessary.
|
||
fn get_or_insert_image(&mut self, image_id: &AssetId<Image>) -> u32 {
|
||
*self
|
||
.texture_to_binding_index
|
||
.entry(*image_id)
|
||
.or_insert_with(|| {
|
||
let index = self.binding_index_to_textures.len() as u32;
|
||
self.binding_index_to_textures.push(*image_id);
|
||
index
|
||
})
|
||
}
|
||
}
|
||
|
||
/// Uploads the list of decals from [`RenderClusteredDecals::decals`] to the
|
||
/// GPU.
|
||
fn upload_decals(
|
||
render_decals: Res<RenderClusteredDecals>,
|
||
mut decals_buffer: ResMut<DecalsBuffer>,
|
||
render_device: Res<RenderDevice>,
|
||
render_queue: Res<RenderQueue>,
|
||
) {
|
||
decals_buffer.clear();
|
||
|
||
for &decal in &render_decals.decals {
|
||
decals_buffer.push(decal);
|
||
}
|
||
|
||
// Make sure the buffer is non-empty.
|
||
// Otherwise there won't be a buffer to bind.
|
||
if decals_buffer.is_empty() {
|
||
decals_buffer.push(RenderClusteredDecal::default());
|
||
}
|
||
|
||
decals_buffer.write_buffer(&render_device, &render_queue);
|
||
}
|
||
|
||
/// Returns true if clustered decals are usable on the current platform or false
|
||
/// otherwise.
|
||
///
|
||
/// Clustered decals are currently disabled on macOS and iOS due to insufficient
|
||
/// texture bindings and limited bindless support in `wgpu`.
|
||
pub fn clustered_decals_are_usable(
|
||
render_device: &RenderDevice,
|
||
render_adapter: &RenderAdapter,
|
||
) -> bool {
|
||
// Disable binding arrays on Metal. There aren't enough texture bindings available.
|
||
// See issue #17553.
|
||
// Re-enable this when `wgpu` has first-class bindless.
|
||
binding_arrays_are_usable(render_device, render_adapter)
|
||
&& cfg!(not(any(target_os = "macos", target_os = "ios")))
|
||
}
|