bevy/examples/shader/custom_phase_item.rs
Joona Aalto 7b1c9f192e
Adopt consistent FooSystems naming convention for system sets (#18900)
# 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
2025-05-06 15:18:03 +00:00

389 lines
14 KiB
Rust

//! Demonstrates how to enqueue custom draw commands in a render phase.
//!
//! This example shows how to use the built-in
//! [`bevy_render::render_phase::BinnedRenderPhase`] functionality with a
//! custom [`RenderCommand`] to allow inserting arbitrary GPU drawing logic
//! into Bevy's pipeline. This is not the only way to add custom rendering code
//! into Bevy—render nodes are another, lower-level method—but it does allow
//! for better reuse of parts of Bevy's built-in mesh rendering logic.
use bevy::{
core_pipeline::core_3d::{Opaque3d, Opaque3dBatchSetKey, Opaque3dBinKey, CORE_3D_DEPTH_FORMAT},
ecs::{
component::Tick,
query::ROQueryItem,
system::{lifetimeless::SRes, SystemParamItem},
},
prelude::*,
render::{
extract_component::{ExtractComponent, ExtractComponentPlugin},
primitives::Aabb,
render_phase::{
AddRenderCommand, BinnedRenderPhaseType, DrawFunctions, InputUniformIndex, PhaseItem,
RenderCommand, RenderCommandResult, SetItemPipeline, TrackedRenderPass,
ViewBinnedRenderPhases,
},
render_resource::{
BufferUsages, ColorTargetState, ColorWrites, CompareFunction, DepthStencilState,
FragmentState, IndexFormat, MultisampleState, PipelineCache, PrimitiveState,
RawBufferVec, RenderPipelineDescriptor, SpecializedRenderPipeline,
SpecializedRenderPipelines, TextureFormat, VertexAttribute, VertexBufferLayout,
VertexFormat, VertexState, VertexStepMode,
},
renderer::{RenderDevice, RenderQueue},
view::{self, ExtractedView, RenderVisibleEntities, VisibilityClass},
Render, RenderApp, RenderSystems,
},
};
use bytemuck::{Pod, Zeroable};
/// A marker component that represents an entity that is to be rendered using
/// our custom phase item.
///
/// Note the [`ExtractComponent`] trait implementation: this is necessary to
/// tell Bevy that this object should be pulled into the render world. Also note
/// the `on_add` hook, which is needed to tell Bevy's `check_visibility` system
/// that entities with this component need to be examined for visibility.
#[derive(Clone, Component, ExtractComponent)]
#[require(VisibilityClass)]
#[component(on_add = view::add_visibility_class::<CustomRenderedEntity>)]
struct CustomRenderedEntity;
/// Holds a reference to our shader.
///
/// This is loaded at app creation time.
#[derive(Resource)]
struct CustomPhasePipeline {
shader: Handle<Shader>,
}
/// A [`RenderCommand`] that binds the vertex and index buffers and issues the
/// draw command for our custom phase item.
struct DrawCustomPhaseItem;
impl<P> RenderCommand<P> for DrawCustomPhaseItem
where
P: PhaseItem,
{
type Param = SRes<CustomPhaseItemBuffers>;
type ViewQuery = ();
type ItemQuery = ();
fn render<'w>(
_: &P,
_: ROQueryItem<'w, Self::ViewQuery>,
_: Option<ROQueryItem<'w, Self::ItemQuery>>,
custom_phase_item_buffers: SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
// Borrow check workaround.
let custom_phase_item_buffers = custom_phase_item_buffers.into_inner();
// Tell the GPU where the vertices are.
pass.set_vertex_buffer(
0,
custom_phase_item_buffers
.vertices
.buffer()
.unwrap()
.slice(..),
);
// Tell the GPU where the indices are.
pass.set_index_buffer(
custom_phase_item_buffers
.indices
.buffer()
.unwrap()
.slice(..),
0,
IndexFormat::Uint32,
);
// Draw one triangle (3 vertices).
pass.draw_indexed(0..3, 0, 0..1);
RenderCommandResult::Success
}
}
/// The GPU vertex and index buffers for our custom phase item.
///
/// As the custom phase item is a single triangle, these are uploaded once and
/// then left alone.
#[derive(Resource)]
struct CustomPhaseItemBuffers {
/// The vertices for the single triangle.
///
/// This is a [`RawBufferVec`] because that's the simplest and fastest type
/// of GPU buffer, and [`Vertex`] objects are simple.
vertices: RawBufferVec<Vertex>,
/// The indices of the single triangle.
///
/// As above, this is a [`RawBufferVec`] because `u32` values have trivial
/// size and alignment.
indices: RawBufferVec<u32>,
}
/// The CPU-side structure that describes a single vertex of the triangle.
#[derive(Clone, Copy, Pod, Zeroable)]
#[repr(C)]
struct Vertex {
/// The 3D position of the triangle vertex.
position: Vec3,
/// Padding.
pad0: u32,
/// The color of the triangle vertex.
color: Vec3,
/// Padding.
pad1: u32,
}
impl Vertex {
/// Creates a new vertex structure.
const fn new(position: Vec3, color: Vec3) -> Vertex {
Vertex {
position,
color,
pad0: 0,
pad1: 0,
}
}
}
/// The custom draw commands that Bevy executes for each entity we enqueue into
/// the render phase.
type DrawCustomPhaseItemCommands = (SetItemPipeline, DrawCustomPhaseItem);
/// A single triangle's worth of vertices, for demonstration purposes.
static VERTICES: [Vertex; 3] = [
Vertex::new(vec3(-0.866, -0.5, 0.5), vec3(1.0, 0.0, 0.0)),
Vertex::new(vec3(0.866, -0.5, 0.5), vec3(0.0, 1.0, 0.0)),
Vertex::new(vec3(0.0, 1.0, 0.5), vec3(0.0, 0.0, 1.0)),
];
/// The entry point.
fn main() {
let mut app = App::new();
app.add_plugins(DefaultPlugins)
.add_plugins(ExtractComponentPlugin::<CustomRenderedEntity>::default())
.add_systems(Startup, setup);
// We make sure to add these to the render app, not the main app.
app.get_sub_app_mut(RenderApp)
.unwrap()
.init_resource::<CustomPhasePipeline>()
.init_resource::<SpecializedRenderPipelines<CustomPhasePipeline>>()
.add_render_command::<Opaque3d, DrawCustomPhaseItemCommands>()
.add_systems(
Render,
prepare_custom_phase_item_buffers.in_set(RenderSystems::Prepare),
)
.add_systems(Render, queue_custom_phase_item.in_set(RenderSystems::Queue));
app.run();
}
/// Spawns the objects in the scene.
fn setup(mut commands: Commands) {
// Spawn a single entity that has custom rendering. It'll be extracted into
// the render world via [`ExtractComponent`].
commands.spawn((
Visibility::default(),
Transform::default(),
// This `Aabb` is necessary for the visibility checks to work.
Aabb {
center: Vec3A::ZERO,
half_extents: Vec3A::splat(0.5),
},
CustomRenderedEntity,
));
// Spawn the camera.
commands.spawn((
Camera3d::default(),
Transform::from_xyz(0.0, 0.0, 1.0).looking_at(Vec3::ZERO, Vec3::Y),
));
}
/// Creates the [`CustomPhaseItemBuffers`] resource.
///
/// This must be done in a startup system because it needs the [`RenderDevice`]
/// and [`RenderQueue`] to exist, and they don't until [`App::run`] is called.
fn prepare_custom_phase_item_buffers(mut commands: Commands) {
commands.init_resource::<CustomPhaseItemBuffers>();
}
/// A render-world system that enqueues the entity with custom rendering into
/// the opaque render phases of each view.
fn queue_custom_phase_item(
pipeline_cache: Res<PipelineCache>,
custom_phase_pipeline: Res<CustomPhasePipeline>,
mut opaque_render_phases: ResMut<ViewBinnedRenderPhases<Opaque3d>>,
opaque_draw_functions: Res<DrawFunctions<Opaque3d>>,
mut specialized_render_pipelines: ResMut<SpecializedRenderPipelines<CustomPhasePipeline>>,
views: Query<(&ExtractedView, &RenderVisibleEntities, &Msaa)>,
mut next_tick: Local<Tick>,
) {
let draw_custom_phase_item = opaque_draw_functions
.read()
.id::<DrawCustomPhaseItemCommands>();
// Render phases are per-view, so we need to iterate over all views so that
// the entity appears in them. (In this example, we have only one view, but
// it's good practice to loop over all views anyway.)
for (view, view_visible_entities, msaa) in views.iter() {
let Some(opaque_phase) = opaque_render_phases.get_mut(&view.retained_view_entity) else {
continue;
};
// Find all the custom rendered entities that are visible from this
// view.
for &entity in view_visible_entities.get::<CustomRenderedEntity>().iter() {
// Ordinarily, the [`SpecializedRenderPipeline::Key`] would contain
// some per-view settings, such as whether the view is HDR, but for
// simplicity's sake we simply hard-code the view's characteristics,
// with the exception of number of MSAA samples.
let pipeline_id = specialized_render_pipelines.specialize(
&pipeline_cache,
&custom_phase_pipeline,
*msaa,
);
// Bump the change tick in order to force Bevy to rebuild the bin.
let this_tick = next_tick.get() + 1;
next_tick.set(this_tick);
// Add the custom render item. We use the
// [`BinnedRenderPhaseType::NonMesh`] type to skip the special
// handling that Bevy has for meshes (preprocessing, indirect
// draws, etc.)
//
// The asset ID is arbitrary; we simply use [`AssetId::invalid`],
// but you can use anything you like. Note that the asset ID need
// not be the ID of a [`Mesh`].
opaque_phase.add(
Opaque3dBatchSetKey {
draw_function: draw_custom_phase_item,
pipeline: pipeline_id,
material_bind_group_index: None,
lightmap_slab: None,
vertex_slab: default(),
index_slab: None,
},
Opaque3dBinKey {
asset_id: AssetId::<Mesh>::invalid().untyped(),
},
entity,
InputUniformIndex::default(),
BinnedRenderPhaseType::NonMesh,
*next_tick,
);
}
}
}
impl SpecializedRenderPipeline for CustomPhasePipeline {
type Key = Msaa;
fn specialize(&self, msaa: Self::Key) -> RenderPipelineDescriptor {
RenderPipelineDescriptor {
label: Some("custom render pipeline".into()),
layout: vec![],
push_constant_ranges: vec![],
vertex: VertexState {
shader: self.shader.clone(),
shader_defs: vec![],
entry_point: "vertex".into(),
buffers: vec![VertexBufferLayout {
array_stride: size_of::<Vertex>() as u64,
step_mode: VertexStepMode::Vertex,
// This needs to match the layout of [`Vertex`].
attributes: vec![
VertexAttribute {
format: VertexFormat::Float32x3,
offset: 0,
shader_location: 0,
},
VertexAttribute {
format: VertexFormat::Float32x3,
offset: 16,
shader_location: 1,
},
],
}],
},
fragment: Some(FragmentState {
shader: self.shader.clone(),
shader_defs: vec![],
entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState {
// Ordinarily, you'd want to check whether the view has the
// HDR format and substitute the appropriate texture format
// here, but we omit that for simplicity.
format: TextureFormat::bevy_default(),
blend: None,
write_mask: ColorWrites::ALL,
})],
}),
primitive: PrimitiveState::default(),
// Note that if your view has no depth buffer this will need to be
// changed.
depth_stencil: Some(DepthStencilState {
format: CORE_3D_DEPTH_FORMAT,
depth_write_enabled: false,
depth_compare: CompareFunction::Always,
stencil: default(),
bias: default(),
}),
multisample: MultisampleState {
count: msaa.samples(),
mask: !0,
alpha_to_coverage_enabled: false,
},
zero_initialize_workgroup_memory: false,
}
}
}
impl FromWorld for CustomPhaseItemBuffers {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
let render_queue = world.resource::<RenderQueue>();
// Create the vertex and index buffers.
let mut vbo = RawBufferVec::new(BufferUsages::VERTEX);
let mut ibo = RawBufferVec::new(BufferUsages::INDEX);
for vertex in &VERTICES {
vbo.push(*vertex);
}
for index in 0..3 {
ibo.push(index);
}
// These two lines are required in order to trigger the upload to GPU.
vbo.write_buffer(render_device, render_queue);
ibo.write_buffer(render_device, render_queue);
CustomPhaseItemBuffers {
vertices: vbo,
indices: ibo,
}
}
}
impl FromWorld for CustomPhasePipeline {
fn from_world(world: &mut World) -> Self {
// Load and compile the shader in the background.
let asset_server = world.resource::<AssetServer>();
CustomPhasePipeline {
shader: asset_server.load("shaders/custom_phase_item.wgsl"),
}
}
}