bevy/crates/bevy_gizmos/src/lib.rs
jeliag f6b40a6e43
Multiple Configurations for Gizmos (#10342)
# Objective

This PR aims to implement multiple configs for gizmos as discussed in
#9187.

## Solution

Configs for the new `GizmoConfigGroup`s are stored in a
`GizmoConfigStore` resource and can be accesses using a type based key
or iterated over. This type based key doubles as a standardized location
where plugin authors can put their own configuration not covered by the
standard `GizmoConfig` struct. For example the `AabbGizmoGroup` has a
default color and toggle to show all AABBs. New configs can be
registered using `app.init_gizmo_group::<T>()` during startup.

When requesting the `Gizmos<T>` system parameter the generic type
determines which config is used. The config structs are available
through the `Gizmos` system parameter allowing for easy access while
drawing your gizmos.

Internally, resources and systems used for rendering (up to an including
the extract system) are generic over the type based key and inserted on
registering a new config.

## Alternatives

The configs could be stored as components on entities with markers which
would make better use of the ECS. I also implemented this approach
([here](https://github.com/jeliag/bevy/tree/gizmo-multiconf-comp)) and
believe that the ergonomic benefits of a central config store outweigh
the decreased use of the ECS.

## Unsafe Code

Implementing system parameter by hand is unsafe but seems to be required
to access the config store once and not on every gizmo draw function
call. This is critical for performance. ~Is there a better way to do
this?~

## Future Work

New gizmos (such as #10038, and ideas from #9400) will require custom
configuration structs. Should there be a new custom config for every
gizmo type, or should we group them together in a common configuration?
(for example `EditorGizmoConfig`, or something more fine-grained)

## Changelog

- Added `GizmoConfigStore` resource and `GizmoConfigGroup` trait
- Added `init_gizmo_group` to `App`
- Added early returns to gizmo drawing increasing performance when
gizmos are disabled
- Changed `GizmoConfig` and aabb gizmos to use new `GizmoConfigStore`
- Changed `Gizmos` system parameter to use type based key to retrieve
config
- Changed resources and systems used for gizmo rendering to be generic
over type based key
- Changed examples (3d_gizmos, 2d_gizmos) to showcase new API

## Migration Guide

- `GizmoConfig` is no longer a resource and has to be accessed through
`GizmoConfigStore` resource. The default config group is
`DefaultGizmoGroup`, but consider using your own custom config group if
applicable.

---------

Co-authored-by: Nicola Papale <nicopap@users.noreply.github.com>
2024-01-18 15:52:50 +00:00

465 lines
14 KiB
Rust

#![warn(missing_docs)]
//! This crate adds an immediate mode drawing api to Bevy for visual debugging.
//!
//! # Example
//! ```
//! # use bevy_gizmos::prelude::*;
//! # use bevy_render::prelude::*;
//! # use bevy_math::prelude::*;
//! fn system(mut gizmos: Gizmos) {
//! gizmos.line(Vec3::ZERO, Vec3::X, Color::GREEN);
//! }
//! # bevy_ecs::system::assert_is_system(system);
//! ```
//!
//! See the documentation on [Gizmos](crate::gizmos::Gizmos) for more examples.
/// Label for the the render systems handling the
#[derive(SystemSet, Clone, Debug, Hash, PartialEq, Eq)]
pub enum GizmoRenderSystem {
/// Adds gizmos to the [`Transparent2d`](bevy_core_pipeline::core_2d::Transparent2d) render phase
#[cfg(feature = "bevy_sprite")]
QueueLineGizmos2d,
/// Adds gizmos to the [`Transparent3d`](bevy_core_pipeline::core_3d::Transparent3d) render phase
#[cfg(feature = "bevy_pbr")]
QueueLineGizmos3d,
}
pub mod aabb;
pub mod arcs;
pub mod arrows;
pub mod circles;
pub mod config;
pub mod gizmos;
#[cfg(feature = "bevy_sprite")]
mod pipeline_2d;
#[cfg(feature = "bevy_pbr")]
mod pipeline_3d;
/// The `bevy_gizmos` prelude.
pub mod prelude {
#[doc(hidden)]
pub use crate::{
aabb::{AabbGizmoConfigGroup, ShowAabbGizmo},
config::{DefaultGizmoConfigGroup, GizmoConfig, GizmoConfigGroup, GizmoConfigStore},
gizmos::Gizmos,
AppGizmoBuilder,
};
}
use aabb::AabbGizmoPlugin;
use bevy_app::{App, Last, Plugin};
use bevy_asset::{load_internal_asset, Asset, AssetApp, Assets, Handle};
use bevy_core::cast_slice;
use bevy_ecs::{
component::Component,
query::ROQueryItem,
schedule::{IntoSystemConfigs, SystemSet},
system::{
lifetimeless::{Read, SRes},
Commands, Res, ResMut, Resource, SystemParamItem,
},
};
use bevy_reflect::TypePath;
use bevy_render::{
extract_component::{ComponentUniforms, DynamicUniformIndex, UniformComponentPlugin},
render_asset::{
PrepareAssetError, RenderAsset, RenderAssetPersistencePolicy, RenderAssetPlugin,
RenderAssets,
},
render_phase::{PhaseItem, RenderCommand, RenderCommandResult, TrackedRenderPass},
render_resource::{
binding_types::uniform_buffer, BindGroup, BindGroupEntries, BindGroupLayout,
BindGroupLayoutEntries, Buffer, BufferInitDescriptor, BufferUsages, Shader, ShaderStages,
ShaderType, VertexAttribute, VertexBufferLayout, VertexFormat, VertexStepMode,
},
renderer::RenderDevice,
Extract, ExtractSchedule, Render, RenderApp, RenderSet,
};
use bevy_utils::{tracing::warn, HashMap};
use config::{
DefaultGizmoConfigGroup, GizmoConfig, GizmoConfigGroup, GizmoConfigStore, GizmoMeshConfig,
};
use gizmos::GizmoStorage;
use std::{any::TypeId, mem};
const LINE_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(7414812689238026784);
/// A [`Plugin`] that provides an immediate mode drawing api for visual debugging.
pub struct GizmoPlugin;
impl Plugin for GizmoPlugin {
fn build(&self, app: &mut bevy_app::App) {
// Gizmos cannot work without either a 3D or 2D renderer.
#[cfg(all(not(feature = "bevy_pbr"), not(feature = "bevy_sprite")))]
bevy_log::error!("bevy_gizmos requires either bevy_pbr or bevy_sprite. Please enable one.");
load_internal_asset!(app, LINE_SHADER_HANDLE, "lines.wgsl", Shader::from_wgsl);
app.register_type::<GizmoConfig>()
.add_plugins(UniformComponentPlugin::<LineGizmoUniform>::default())
.init_asset::<LineGizmo>()
.add_plugins(RenderAssetPlugin::<LineGizmo>::default())
.init_resource::<LineGizmoHandles>()
.init_resource::<GizmoConfigStore>()
.init_gizmo_group::<DefaultGizmoConfigGroup>()
.add_plugins(AabbGizmoPlugin);
let Ok(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app.add_systems(
Render,
prepare_line_gizmo_bind_group.in_set(RenderSet::PrepareBindGroups),
);
#[cfg(feature = "bevy_sprite")]
app.add_plugins(pipeline_2d::LineGizmo2dPlugin);
#[cfg(feature = "bevy_pbr")]
app.add_plugins(pipeline_3d::LineGizmo3dPlugin);
}
fn finish(&self, app: &mut bevy_app::App) {
let Ok(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
let render_device = render_app.world.resource::<RenderDevice>();
let layout = render_device.create_bind_group_layout(
"LineGizmoUniform layout",
&BindGroupLayoutEntries::single(
ShaderStages::VERTEX,
uniform_buffer::<LineGizmoUniform>(true),
),
);
render_app.insert_resource(LineGizmoUniformBindgroupLayout { layout });
}
}
/// A trait adding `init_gizmo_group<T>()` to the app
pub trait AppGizmoBuilder {
/// Registers [`GizmoConfigGroup`] `T` in the app enabling the use of [Gizmos&lt;T&gt;](crate::gizmos::Gizmos).
///
/// Configurations can be set using the [`GizmoConfigStore`] [`Resource`].
fn init_gizmo_group<T: GizmoConfigGroup + Default>(&mut self) -> &mut Self;
}
impl AppGizmoBuilder for App {
fn init_gizmo_group<T: GizmoConfigGroup + Default>(&mut self) -> &mut Self {
if self.world.contains_resource::<GizmoStorage<T>>() {
return self;
}
self.init_resource::<GizmoStorage<T>>()
.add_systems(Last, update_gizmo_meshes::<T>);
self.world
.resource_mut::<GizmoConfigStore>()
.register::<T>();
let Ok(render_app) = self.get_sub_app_mut(RenderApp) else {
return self;
};
render_app.add_systems(ExtractSchedule, extract_gizmo_data::<T>);
self
}
}
#[derive(Resource, Default)]
struct LineGizmoHandles {
list: HashMap<TypeId, Handle<LineGizmo>>,
strip: HashMap<TypeId, Handle<LineGizmo>>,
}
fn update_gizmo_meshes<T: GizmoConfigGroup>(
mut line_gizmos: ResMut<Assets<LineGizmo>>,
mut handles: ResMut<LineGizmoHandles>,
mut storage: ResMut<GizmoStorage<T>>,
) {
if storage.list_positions.is_empty() {
handles.list.remove(&TypeId::of::<T>());
} else if let Some(handle) = handles.list.get(&TypeId::of::<T>()) {
let list = line_gizmos.get_mut(handle).unwrap();
list.positions = mem::take(&mut storage.list_positions);
list.colors = mem::take(&mut storage.list_colors);
} else {
let mut list = LineGizmo {
strip: false,
..Default::default()
};
list.positions = mem::take(&mut storage.list_positions);
list.colors = mem::take(&mut storage.list_colors);
handles
.list
.insert(TypeId::of::<T>(), line_gizmos.add(list));
}
if storage.strip_positions.is_empty() {
handles.strip.remove(&TypeId::of::<T>());
} else if let Some(handle) = handles.strip.get(&TypeId::of::<T>()) {
let strip = line_gizmos.get_mut(handle).unwrap();
strip.positions = mem::take(&mut storage.strip_positions);
strip.colors = mem::take(&mut storage.strip_colors);
} else {
let mut strip = LineGizmo {
strip: true,
..Default::default()
};
strip.positions = mem::take(&mut storage.strip_positions);
strip.colors = mem::take(&mut storage.strip_colors);
handles
.strip
.insert(TypeId::of::<T>(), line_gizmos.add(strip));
}
}
fn extract_gizmo_data<T: GizmoConfigGroup>(
mut commands: Commands,
handles: Extract<Res<LineGizmoHandles>>,
config: Extract<Res<GizmoConfigStore>>,
) {
let (config, _) = config.config::<T>();
if !config.enabled {
return;
}
for map in [&handles.list, &handles.strip].into_iter() {
let Some(handle) = map.get(&TypeId::of::<T>()) else {
continue;
};
commands.spawn((
LineGizmoUniform {
line_width: config.line_width,
depth_bias: config.depth_bias,
#[cfg(feature = "webgl")]
_padding: Default::default(),
},
(*handle).clone_weak(),
GizmoMeshConfig::from(config),
));
}
}
#[derive(Component, ShaderType, Clone, Copy)]
struct LineGizmoUniform {
line_width: f32,
depth_bias: f32,
/// WebGL2 structs must be 16 byte aligned.
#[cfg(feature = "webgl")]
_padding: bevy_math::Vec2,
}
#[derive(Asset, Debug, Default, Clone, TypePath)]
struct LineGizmo {
positions: Vec<[f32; 3]>,
colors: Vec<[f32; 4]>,
/// Whether this gizmo's topology is a line-strip or line-list
strip: bool,
}
#[derive(Debug, Clone)]
struct GpuLineGizmo {
position_buffer: Buffer,
color_buffer: Buffer,
vertex_count: u32,
strip: bool,
}
impl RenderAsset for LineGizmo {
type PreparedAsset = GpuLineGizmo;
type Param = SRes<RenderDevice>;
fn persistence_policy(&self) -> RenderAssetPersistencePolicy {
RenderAssetPersistencePolicy::Keep
}
fn prepare_asset(
self,
render_device: &mut SystemParamItem<Self::Param>,
) -> Result<Self::PreparedAsset, PrepareAssetError<Self>> {
let position_buffer_data = cast_slice(&self.positions);
let position_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor {
usage: BufferUsages::VERTEX,
label: Some("LineGizmo Position Buffer"),
contents: position_buffer_data,
});
let color_buffer_data = cast_slice(&self.colors);
let color_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor {
usage: BufferUsages::VERTEX,
label: Some("LineGizmo Color Buffer"),
contents: color_buffer_data,
});
Ok(GpuLineGizmo {
position_buffer,
color_buffer,
vertex_count: self.positions.len() as u32,
strip: self.strip,
})
}
}
#[derive(Resource)]
struct LineGizmoUniformBindgroupLayout {
layout: BindGroupLayout,
}
#[derive(Resource)]
struct LineGizmoUniformBindgroup {
bindgroup: BindGroup,
}
fn prepare_line_gizmo_bind_group(
mut commands: Commands,
line_gizmo_uniform_layout: Res<LineGizmoUniformBindgroupLayout>,
render_device: Res<RenderDevice>,
line_gizmo_uniforms: Res<ComponentUniforms<LineGizmoUniform>>,
) {
if let Some(binding) = line_gizmo_uniforms.uniforms().binding() {
commands.insert_resource(LineGizmoUniformBindgroup {
bindgroup: render_device.create_bind_group(
"LineGizmoUniform bindgroup",
&line_gizmo_uniform_layout.layout,
&BindGroupEntries::single(binding),
),
});
}
}
struct SetLineGizmoBindGroup<const I: usize>;
impl<const I: usize, P: PhaseItem> RenderCommand<P> for SetLineGizmoBindGroup<I> {
type Param = SRes<LineGizmoUniformBindgroup>;
type ViewData = ();
type ItemData = Read<DynamicUniformIndex<LineGizmoUniform>>;
#[inline]
fn render<'w>(
_item: &P,
_view: ROQueryItem<'w, Self::ViewData>,
uniform_index: ROQueryItem<'w, Self::ItemData>,
bind_group: SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
pass.set_bind_group(
I,
&bind_group.into_inner().bindgroup,
&[uniform_index.index()],
);
RenderCommandResult::Success
}
}
struct DrawLineGizmo;
impl<P: PhaseItem> RenderCommand<P> for DrawLineGizmo {
type Param = SRes<RenderAssets<LineGizmo>>;
type ViewData = ();
type ItemData = Read<Handle<LineGizmo>>;
#[inline]
fn render<'w>(
_item: &P,
_view: ROQueryItem<'w, Self::ViewData>,
handle: ROQueryItem<'w, Self::ItemData>,
line_gizmos: SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
let Some(line_gizmo) = line_gizmos.into_inner().get(handle) else {
return RenderCommandResult::Failure;
};
if line_gizmo.vertex_count < 2 {
return RenderCommandResult::Success;
}
let instances = if line_gizmo.strip {
let item_size = VertexFormat::Float32x3.size();
let buffer_size = line_gizmo.position_buffer.size() - item_size;
pass.set_vertex_buffer(0, line_gizmo.position_buffer.slice(..buffer_size));
pass.set_vertex_buffer(1, line_gizmo.position_buffer.slice(item_size..));
let item_size = VertexFormat::Float32x4.size();
let buffer_size = line_gizmo.color_buffer.size() - item_size;
pass.set_vertex_buffer(2, line_gizmo.color_buffer.slice(..buffer_size));
pass.set_vertex_buffer(3, line_gizmo.color_buffer.slice(item_size..));
u32::max(line_gizmo.vertex_count, 1) - 1
} else {
pass.set_vertex_buffer(0, line_gizmo.position_buffer.slice(..));
pass.set_vertex_buffer(1, line_gizmo.color_buffer.slice(..));
line_gizmo.vertex_count / 2
};
pass.draw(0..6, 0..instances);
RenderCommandResult::Success
}
}
fn line_gizmo_vertex_buffer_layouts(strip: bool) -> Vec<VertexBufferLayout> {
use VertexFormat::*;
let mut position_layout = VertexBufferLayout {
array_stride: Float32x3.size(),
step_mode: VertexStepMode::Instance,
attributes: vec![VertexAttribute {
format: Float32x3,
offset: 0,
shader_location: 0,
}],
};
let mut color_layout = VertexBufferLayout {
array_stride: Float32x4.size(),
step_mode: VertexStepMode::Instance,
attributes: vec![VertexAttribute {
format: Float32x4,
offset: 0,
shader_location: 2,
}],
};
if strip {
vec![
position_layout.clone(),
{
position_layout.attributes[0].shader_location = 1;
position_layout
},
color_layout.clone(),
{
color_layout.attributes[0].shader_location = 3;
color_layout
},
]
} else {
position_layout.array_stride *= 2;
position_layout.attributes.push(VertexAttribute {
format: Float32x3,
offset: Float32x3.size(),
shader_location: 1,
});
color_layout.array_stride *= 2;
color_layout.attributes.push(VertexAttribute {
format: Float32x4,
offset: Float32x4.size(),
shader_location: 3,
});
vec![position_layout, color_layout]
}
}