mirror of
https://github.com/bevyengine/bevy
synced 2025-01-03 08:48:57 +00:00
f6b40a6e43
# 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>
465 lines
14 KiB
Rust
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<T>](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]
|
|
}
|
|
}
|