diff --git a/crates/bevy_gizmos/src/billboard/billboards.wgsl b/crates/bevy_gizmos/src/billboard/billboards.wgsl new file mode 100644 index 0000000000000..a961c2e48a363 --- /dev/null +++ b/crates/bevy_gizmos/src/billboard/billboards.wgsl @@ -0,0 +1,89 @@ +// TODO use common view binding +#import bevy_render::view::View + +@group(0) @binding(0) var view: View; + + +struct BillboardGizmoUniform { + size: f32, + depth_bias: f32, +#ifdef SIXTEEN_BYTE_ALIGNMENT + // WebGL2 structs must be 16 byte aligned. + _padding: vec2, +#endif +} + +@group(1) @binding(0) var billboard_gizmo: BillboardGizmoUniform; + +struct VertexInput { + @location(0) position: vec3, + @location(1) color: vec4, + @builtin(vertex_index) index: u32, +}; + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) color: vec4, +}; + +const EPSILON: f32 = 4.88e-04; + +@vertex +fn vertex(vertex: VertexInput) -> VertexOutput { + var positions = array, 6>( + vec2(-0.5, -0.5), + vec2(-0.5, 0.5), + vec2(0.5, 0.5), + vec2(-0.5, -0.5), + vec2(0.5, 0.5), + vec2(0.5, -0.5) + ); + var offset = positions[vertex.index]; + + let center_clip = view.clip_from_world * vec4(vertex.position, 1.0); + + let resolution = view.viewport.zw; + let screen_center = resolution * (0.5 * center_clip.xy / center_clip.w + 0.5); + + var color = vertex.color; + var billboard_size = billboard_gizmo.size; +#ifdef PERSPECTIVE + billboard_size /= center_clip.w; +#endif + if billboard_size < 1. { + color.a *= billboard_size; + billboard_size = 1.; + } + + let screen = screen_center + offset * billboard_size; + + var depth: f32; + if billboard_gizmo.depth_bias >= 0. { + depth = center_clip.z * (1. - billboard_gizmo.depth_bias); + } else { + // depth * (center_clip.w / depth)^-depth_bias. So that when -depth_bias is 1.0, this is equal to center_clip.w + // and when equal to 0.0, it is exactly equal to depth. + // the epsilon is here to prevent the depth from exceeding center_clip.w when -depth_bias = 1.0 + // center_clip.w represents the near plane in homogeneous clip space in bevy, having a depth + // of this value means nothing can be in front of this + // The reason this uses an exponential function is that it makes it much easier for the + // user to chose a value that is convenient for them + depth = center_clip.z * exp2(-billboard_gizmo.depth_bias * log2(center_clip.w / center_clip.z - EPSILON)); + } + + var clip_position = vec4(center_clip.w * ((2. * screen) / resolution - 1.), depth, center_clip.w); + + var result: VertexOutput; + result.clip_position = clip_position; + result.color = color; + return result; +} + +struct FragmentOutput { + @location(0) color: vec4, +}; + +@fragment +fn fragment(in: VertexOutput) -> FragmentOutput { + return FragmentOutput(in.color); +} \ No newline at end of file diff --git a/crates/bevy_gizmos/src/billboard/mod.rs b/crates/bevy_gizmos/src/billboard/mod.rs new file mode 100644 index 0000000000000..ee96f2be8b778 --- /dev/null +++ b/crates/bevy_gizmos/src/billboard/mod.rs @@ -0,0 +1,401 @@ +use std::{any::TypeId, mem}; + +use bevy_app::{App, Last, Plugin}; +use bevy_asset::{Asset, AssetApp, Assets, Handle}; +use bevy_color::LinearRgba; +use bevy_ecs::{ + component::Component, + query::ROQueryItem, + schedule::IntoSystemConfigs, + system::{ + lifetimeless::{Read, SRes}, + Commands, Res, ResMut, Resource, SystemParamItem, + }, +}; +use bevy_math::Vec3; +use bevy_reflect::TypePath; +use bevy_render::{ + extract_component::{ComponentUniforms, DynamicUniformIndex, UniformComponentPlugin}, + render_asset::{PrepareAssetError, RenderAsset, 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::TypeIdMap; +use bytemuck::cast_slice; + +use crate::{ + config::{GizmoConfigGroup, GizmoConfigStore}, + gizmos::GizmoStorage, + UpdateGizmoMeshes, +}; + +#[cfg(all(feature = "bevy_sprite", feature = "bevy_render"))] +mod pipeline_2d; +#[cfg(all(feature = "bevy_pbr", feature = "bevy_render"))] +mod pipeline_3d; + +#[cfg(feature = "bevy_render")] +const BILLBOARD_SHADER_HANDLE: Handle = Handle::weak_from_u128(6006413002665766670); + +/// A [`Plugin`] that provides an immediate mode billboard drawing api for visual debugging. +#[derive(Default)] +pub struct BillboardGizmoPlugin; + +impl Plugin for BillboardGizmoPlugin { + fn build(&self, app: &mut App) { + #[cfg(feature = "bevy_render")] + { + use bevy_asset::load_internal_asset; + load_internal_asset!( + app, + BILLBOARD_SHADER_HANDLE, + "billboards.wgsl", + Shader::from_wgsl + ); + } + app.init_asset::() + .init_resource::(); + + // These handles are safe to mutate in any order + app.allow_ambiguous_resource::(); + + #[cfg(feature = "bevy_render")] + app.add_plugins(UniformComponentPlugin::::default()) + .add_plugins(RenderAssetPlugin::::default()); + + #[cfg(feature = "bevy_render")] + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app.add_systems( + Render, + prepare_billboard_gizmo_bind_group.in_set(RenderSet::PrepareBindGroups), + ); + + render_app.add_systems(ExtractSchedule, extract_gizmo_data); + + #[cfg(feature = "bevy_sprite")] + if app.is_plugin_added::() { + app.add_plugins(pipeline_2d::BillboardGizmo2dPlugin); + } else { + bevy_utils::tracing::warn!("bevy_sprite feature is enabled but bevy_sprite::SpritePlugin was not detected. Are you sure you loaded GizmoPlugin after SpritePlugin?"); + } + #[cfg(feature = "bevy_pbr")] + if app.is_plugin_added::() { + app.add_plugins(pipeline_3d::BillboardGizmo3dPlugin); + } else { + bevy_utils::tracing::warn!("bevy_pbr feature is enabled but bevy_pbr::PbrPlugin was not detected. Are you sure you loaded GizmoPlugin after PbrPlugin?"); + } + } else { + bevy_utils::tracing::warn!("bevy_render feature is enabled but RenderApp was not detected. Are you sure you loaded GizmoPlugin after RenderPlugin?"); + } + } + + #[cfg(feature = "bevy_render")] + fn finish(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + let render_device = render_app.world().resource::(); + let billboard_layout = render_device.create_bind_group_layout( + "BillboardGizmoUniform layout", + &BindGroupLayoutEntries::single( + ShaderStages::VERTEX, + uniform_buffer::(true), + ), + ); + + render_app.insert_resource(BillboardGizmoUniformBindgroupLayout { + layout: billboard_layout, + }); + } +} + +/// An internal extension trait adding `App::init_billboard_gizmo_group`. +pub(crate) trait AppBillboardGizmoBuilder { + /// Registers [`GizmoConfigGroup`] in the app enabling the use of [Gizmos<Config>](crate::gizmos::Gizmos). + /// + /// Configurations can be set using the [`GizmoConfigStore`] [`Resource`]. + fn init_billboard_gizmo_group(&mut self) -> &mut Self; +} + +impl AppBillboardGizmoBuilder for App { + fn init_billboard_gizmo_group(&mut self) -> &mut Self { + if self.world().contains_resource::>() { + return self; + } + + let mut handles = self + .world_mut() + .get_resource_or_insert_with::(Default::default); + + handles.billboards.insert(TypeId::of::(), None); + + self.add_systems( + Last, + update_gizmo_meshes::.in_set(UpdateGizmoMeshes), + ); + + self + } +} + +/// Holds handles to the billboard gizmos for each gizmo configuration group +// As `TypeIdMap` iteration order depends on the order of insertions and deletions, this uses +// `Option` to be able to reserve the slot when creating the gizmo configuration group. +// That way iteration order is stable across executions and depends on the order of configuration +// group creation. +#[derive(Resource, Default)] +struct BillboardGizmoHandles { + billboards: TypeIdMap>>, +} + +/// Prepare gizmos for rendering. +/// +/// This also clears the default `GizmoStorage`. +fn update_gizmo_meshes( + mut billboard_gizmos: ResMut>, + mut handles: ResMut, + mut storage: ResMut>, +) { + if storage.billboard_positions.is_empty() { + handles.billboards.insert(TypeId::of::(), None); + } else if let Some(handle) = handles.billboards.get_mut(&TypeId::of::()) { + if let Some(handle) = handle { + let billboards = billboard_gizmos.get_mut(handle.id()).unwrap(); + + billboards.positions = mem::take(&mut storage.billboard_positions); + billboards.colors = mem::take(&mut storage.billboard_colors); + } else { + let billboards = BillboardGizmo { + positions: mem::take(&mut storage.billboard_positions), + colors: mem::take(&mut storage.billboard_colors), + }; + + *handle = Some(billboard_gizmos.add(billboards)); + } + } +} + +#[cfg(feature = "bevy_render")] +fn extract_gizmo_data( + mut commands: Commands, + handles: Extract>, + config: Extract>, +) { + for (group_type_id, handle) in handles.billboards.iter() { + let Some((config, _)) = config.get_config_dyn(group_type_id) else { + continue; + }; + + if !config.enabled { + continue; + } + + let Some(handle) = handle else { + continue; + }; + + commands.spawn(( + BillboardGizmoUniform { + size: config.billboard_size, + depth_bias: config.depth_bias, + #[cfg(feature = "webgl")] + _padding: Default::default(), + }, + (*handle).clone_weak(), + #[cfg(any(feature = "bevy_pbr", feature = "bevy_sprite"))] + crate::config::GizmoMeshConfig::from(config), + )); + } +} + +#[cfg(feature = "bevy_render")] +#[derive(Component, ShaderType, Clone, Copy)] +struct BillboardGizmoUniform { + size: f32, + depth_bias: f32, + /// WebGL2 structs must be 16 byte aligned. + #[cfg(feature = "webgl")] + _padding: bevy_math::Vec2, +} + +/// A gizmo asset that represents a billboard. +#[derive(Asset, Debug, Clone, TypePath)] +pub struct BillboardGizmo { + /// Positions of the gizmo's vertices + pub positions: Vec, + /// Colors of the gizmo's vertices + pub colors: Vec, +} + +#[cfg(feature = "bevy_render")] +#[derive(Debug, Clone)] +struct GpuBillboardGizmo { + position_buffer: Buffer, + color_buffer: Buffer, + vertex_count: u32, +} + +#[cfg(feature = "bevy_render")] +impl RenderAsset for GpuBillboardGizmo { + type SourceAsset = BillboardGizmo; + type Param = SRes; + + fn prepare_asset( + gizmo: Self::SourceAsset, + render_device: &mut SystemParamItem, + ) -> Result> { + let position_buffer_data = cast_slice(&gizmo.positions); + let position_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { + usage: BufferUsages::VERTEX, + label: Some("BillboardGizmo Position Buffer"), + contents: position_buffer_data, + }); + + let color_buffer_data = cast_slice(&gizmo.colors); + let color_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { + usage: BufferUsages::VERTEX, + label: Some("BillboardGizmo Color Buffer"), + contents: color_buffer_data, + }); + + Ok(GpuBillboardGizmo { + position_buffer, + color_buffer, + vertex_count: gizmo.positions.len() as u32, + }) + } +} + +#[cfg(feature = "bevy_render")] +#[derive(Resource)] +struct BillboardGizmoUniformBindgroupLayout { + layout: BindGroupLayout, +} + +#[cfg(feature = "bevy_render")] +#[derive(Resource)] +struct BillboardGizmoUniformBindgroup { + bindgroup: BindGroup, +} + +#[cfg(feature = "bevy_render")] +fn prepare_billboard_gizmo_bind_group( + mut commands: Commands, + billboard_gizmo_uniform_layout: Res, + render_device: Res, + billboard_gizmo_uniforms: Res>, +) { + if let Some(binding) = billboard_gizmo_uniforms.uniforms().binding() { + commands.insert_resource(BillboardGizmoUniformBindgroup { + bindgroup: render_device.create_bind_group( + "BillboardGizmoUniform bindgroup", + &billboard_gizmo_uniform_layout.layout, + &BindGroupEntries::single(binding), + ), + }); + } +} + +#[cfg(feature = "bevy_render")] +struct SetBillboardGizmoBindGroup; +#[cfg(feature = "bevy_render")] +impl RenderCommand

for SetBillboardGizmoBindGroup { + type Param = SRes; + type ViewQuery = (); + type ItemQuery = Read>; + + #[inline] + fn render<'w>( + _item: &P, + _view: ROQueryItem<'w, Self::ViewQuery>, + uniform_index: Option>, + bind_group: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let Some(uniform_index) = uniform_index else { + return RenderCommandResult::Skip; + }; + pass.set_bind_group( + I, + &bind_group.into_inner().bindgroup, + &[uniform_index.index()], + ); + RenderCommandResult::Success + } +} + +#[cfg(feature = "bevy_render")] +struct DrawBillboardGizmo; +#[cfg(feature = "bevy_render")] +impl RenderCommand

for DrawBillboardGizmo { + type Param = SRes>; + type ViewQuery = (); + type ItemQuery = Read>; + + #[inline] + fn render<'w>( + _item: &P, + _view: ROQueryItem<'w, Self::ViewQuery>, + handle: Option>, + billboard_gizmos: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let Some(handle) = handle else { + return RenderCommandResult::Skip; + }; + let Some(billboard_gizmo) = billboard_gizmos.into_inner().get(handle) else { + return RenderCommandResult::Skip; + }; + + if billboard_gizmo.vertex_count == 0 { + return RenderCommandResult::Success; + } + + let instances = { + pass.set_vertex_buffer(0, billboard_gizmo.position_buffer.slice(..)); + pass.set_vertex_buffer(1, billboard_gizmo.color_buffer.slice(..)); + + billboard_gizmo.vertex_count + }; + + pass.draw(0..6, 0..instances); + + RenderCommandResult::Success + } +} + +#[cfg(all( + feature = "bevy_render", + any(feature = "bevy_pbr", feature = "bevy_sprite") +))] +fn billboard_gizmo_vertex_buffer_layouts() -> Vec { + use VertexFormat::*; + let position_layout = VertexBufferLayout { + array_stride: Float32x3.size(), + step_mode: VertexStepMode::Instance, + attributes: vec![VertexAttribute { + format: Float32x3, + offset: 0, + shader_location: 0, + }], + }; + + let color_layout = VertexBufferLayout { + array_stride: Float32x4.size(), + step_mode: VertexStepMode::Instance, + attributes: vec![VertexAttribute { + format: Float32x4, + offset: 0, + shader_location: 1, + }], + }; + + vec![position_layout, color_layout] +} diff --git a/crates/bevy_gizmos/src/billboard/pipeline_2d.rs b/crates/bevy_gizmos/src/billboard/pipeline_2d.rs new file mode 100644 index 0000000000000..e6b78db08a3cb --- /dev/null +++ b/crates/bevy_gizmos/src/billboard/pipeline_2d.rs @@ -0,0 +1,200 @@ +use super::{ + billboard_gizmo_vertex_buffer_layouts, BillboardGizmoUniformBindgroupLayout, + DrawBillboardGizmo, GpuBillboardGizmo, SetBillboardGizmoBindGroup, +}; +use crate::{config::GizmoMeshConfig, GizmoRenderSystem}; +use bevy_app::{App, Plugin}; +use bevy_core_pipeline::core_2d::{Transparent2d, CORE_2D_DEPTH_FORMAT}; + +use bevy_ecs::{ + prelude::Entity, + schedule::IntoSystemConfigs, + system::{Query, Res, ResMut, Resource}, + world::{FromWorld, World}, +}; +use bevy_math::FloatOrd; +use bevy_render::{ + render_asset::prepare_assets, + render_phase::{ + AddRenderCommand, DrawFunctions, PhaseItemExtraIndex, SetItemPipeline, + ViewSortedRenderPhases, + }, + render_resource::*, + texture::BevyDefault, + view::{ExtractedView, Msaa, RenderLayers, ViewTarget}, + Render, RenderApp, +}; +use bevy_sprite::{Mesh2dPipeline, Mesh2dPipelineKey, SetMesh2dViewBindGroup}; + +use super::BILLBOARD_SHADER_HANDLE; + +pub struct BillboardGizmo2dPlugin; + +impl Plugin for BillboardGizmo2dPlugin { + fn build(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .add_render_command::() + .init_resource::>() + .add_systems( + Render, + queue_billboard_gizmos_2d + .in_set(GizmoRenderSystem::QueueGizmos2d) + .after(prepare_assets::), + ); + } + + fn finish(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app.init_resource::(); + } +} + +#[derive(Clone, Resource)] +struct BillboardGizmoPipeline { + mesh_pipeline: Mesh2dPipeline, + uniform_layout: BindGroupLayout, +} + +impl FromWorld for BillboardGizmoPipeline { + fn from_world(render_world: &mut World) -> Self { + BillboardGizmoPipeline { + mesh_pipeline: render_world.resource::().clone(), + uniform_layout: render_world + .resource::() + .layout + .clone(), + } + } +} + +#[derive(PartialEq, Eq, Hash, Clone)] +struct BillboardGizmoPipelineKey { + mesh_key: Mesh2dPipelineKey, +} + +impl SpecializedRenderPipeline for BillboardGizmoPipeline { + type Key = BillboardGizmoPipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + let format = if key.mesh_key.contains(Mesh2dPipelineKey::HDR) { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }; + + let shader_defs = vec![ + #[cfg(feature = "webgl")] + "SIXTEEN_BYTE_ALIGNMENT".into(), + ]; + + let layout = vec![ + self.mesh_pipeline.view_layout.clone(), + self.uniform_layout.clone(), + ]; + + RenderPipelineDescriptor { + vertex: VertexState { + shader: BILLBOARD_SHADER_HANDLE, + entry_point: "vertex".into(), + shader_defs: shader_defs.clone(), + buffers: billboard_gizmo_vertex_buffer_layouts(), + }, + fragment: Some(FragmentState { + shader: BILLBOARD_SHADER_HANDLE, + shader_defs, + entry_point: "fragment".into(), + targets: vec![Some(ColorTargetState { + format, + blend: Some(BlendState::ALPHA_BLENDING), + write_mask: ColorWrites::ALL, + })], + }), + layout, + primitive: PrimitiveState::default(), + depth_stencil: Some(DepthStencilState { + format: CORE_2D_DEPTH_FORMAT, + depth_write_enabled: true, + depth_compare: CompareFunction::GreaterEqual, + stencil: StencilState { + front: StencilFaceState::IGNORE, + back: StencilFaceState::IGNORE, + read_mask: 0, + write_mask: 0, + }, + bias: DepthBiasState { + constant: 0, + slope_scale: 0.0, + clamp: 0.0, + }, + }), + multisample: MultisampleState { + count: key.mesh_key.msaa_samples(), + mask: !0, + alpha_to_coverage_enabled: false, + }, + label: Some("BillboardGizmo Pipeline 2D".into()), + push_constant_ranges: vec![], + } + } +} + +type DrawBillboardGizmo2d = ( + SetItemPipeline, + SetMesh2dViewBindGroup<0>, + SetBillboardGizmoBindGroup<1>, + DrawBillboardGizmo, +); + +#[allow(clippy::too_many_arguments)] +fn queue_billboard_gizmos_2d( + draw_functions: Res>, + pipeline: Res, + mut pipelines: ResMut>, + pipeline_cache: Res, + billboard_gizmos: Query<(Entity, &GizmoMeshConfig)>, + mut transparent_render_phases: ResMut>, + mut views: Query<(Entity, &ExtractedView, &Msaa, Option<&RenderLayers>)>, +) { + let draw_function = draw_functions + .read() + .get_id::() + .unwrap(); + + for (view_entity, view, msaa, render_layers) in &mut views { + let Some(transparent_phase) = transparent_render_phases.get_mut(&view_entity) else { + continue; + }; + + let mesh_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples()) + | Mesh2dPipelineKey::from_hdr(view.hdr); + + let render_layers = render_layers.unwrap_or_default(); + for (entity, config) in &billboard_gizmos { + if !config.render_layers.intersects(render_layers) { + continue; + } + + let pipeline = pipelines.specialize( + &pipeline_cache, + &pipeline, + BillboardGizmoPipelineKey { mesh_key }, + ); + + transparent_phase.add(Transparent2d { + entity, + draw_function, + pipeline, + sort_key: FloatOrd(f32::INFINITY), + batch_range: 0..1, + extra_index: PhaseItemExtraIndex::NONE, + }); + } + } +} diff --git a/crates/bevy_gizmos/src/billboard/pipeline_3d.rs b/crates/bevy_gizmos/src/billboard/pipeline_3d.rs new file mode 100644 index 0000000000000..3ec4a67590034 --- /dev/null +++ b/crates/bevy_gizmos/src/billboard/pipeline_3d.rs @@ -0,0 +1,238 @@ +use super::{ + billboard_gizmo_vertex_buffer_layouts, BillboardGizmoUniformBindgroupLayout, + DrawBillboardGizmo, GpuBillboardGizmo, SetBillboardGizmoBindGroup, +}; +use crate::{config::GizmoMeshConfig, GizmoRenderSystem}; +use bevy_app::{App, Plugin}; +use bevy_core_pipeline::{ + core_3d::{Transparent3d, CORE_3D_DEPTH_FORMAT}, + prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass}, +}; + +use bevy_ecs::{ + prelude::Entity, + query::Has, + schedule::IntoSystemConfigs, + system::{Query, Res, ResMut, Resource}, + world::{FromWorld, World}, +}; +use bevy_pbr::{MeshPipeline, MeshPipelineKey, SetMeshViewBindGroup}; +use bevy_render::{ + render_asset::prepare_assets, + render_phase::{ + AddRenderCommand, DrawFunctions, PhaseItemExtraIndex, SetItemPipeline, + ViewSortedRenderPhases, + }, + render_resource::*, + texture::BevyDefault, + view::{ExtractedView, Msaa, RenderLayers, ViewTarget}, + Render, RenderApp, +}; + +use super::BILLBOARD_SHADER_HANDLE; + +pub struct BillboardGizmo3dPlugin; +impl Plugin for BillboardGizmo3dPlugin { + fn build(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .add_render_command::() + .init_resource::>() + .add_systems( + Render, + queue_billboard_gizmos_3d + .in_set(GizmoRenderSystem::QueueGizmos3d) + .after(prepare_assets::), + ); + } + + fn finish(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app.init_resource::(); + } +} + +#[derive(Clone, Resource)] +struct LineGizmoPipeline { + mesh_pipeline: MeshPipeline, + uniform_layout: BindGroupLayout, +} + +impl FromWorld for LineGizmoPipeline { + fn from_world(render_world: &mut World) -> Self { + LineGizmoPipeline { + mesh_pipeline: render_world.resource::().clone(), + uniform_layout: render_world + .resource::() + .layout + .clone(), + } + } +} + +#[derive(PartialEq, Eq, Hash, Clone)] +struct LineGizmoPipelineKey { + view_key: MeshPipelineKey, + perspective: bool, +} + +impl SpecializedRenderPipeline for LineGizmoPipeline { + type Key = LineGizmoPipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + let mut shader_defs = vec![ + #[cfg(feature = "webgl")] + "SIXTEEN_BYTE_ALIGNMENT".into(), + ]; + + if key.perspective { + shader_defs.push("PERSPECTIVE".into()); + } + + let format = if key.view_key.contains(MeshPipelineKey::HDR) { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }; + + let view_layout = self + .mesh_pipeline + .get_view_layout(key.view_key.into()) + .clone(); + + let layout = vec![view_layout, self.uniform_layout.clone()]; + + RenderPipelineDescriptor { + vertex: VertexState { + shader: BILLBOARD_SHADER_HANDLE, + entry_point: "vertex".into(), + shader_defs: shader_defs.clone(), + buffers: billboard_gizmo_vertex_buffer_layouts(), + }, + fragment: Some(FragmentState { + shader: BILLBOARD_SHADER_HANDLE, + shader_defs, + entry_point: "fragment".into(), + targets: vec![Some(ColorTargetState { + format, + blend: Some(BlendState::ALPHA_BLENDING), + write_mask: ColorWrites::ALL, + })], + }), + layout, + primitive: PrimitiveState::default(), + depth_stencil: Some(DepthStencilState { + format: CORE_3D_DEPTH_FORMAT, + depth_write_enabled: true, + depth_compare: CompareFunction::Greater, + stencil: StencilState::default(), + bias: DepthBiasState::default(), + }), + multisample: MultisampleState { + count: key.view_key.msaa_samples(), + mask: !0, + alpha_to_coverage_enabled: false, + }, + label: Some("LineGizmo Pipeline".into()), + push_constant_ranges: vec![], + } + } +} + +type DrawBillboardGizmo3d = ( + SetItemPipeline, + SetMeshViewBindGroup<0>, + SetBillboardGizmoBindGroup<1>, + DrawBillboardGizmo, +); + +#[allow(clippy::too_many_arguments)] +fn queue_billboard_gizmos_3d( + draw_functions: Res>, + pipeline: Res, + mut pipelines: ResMut>, + pipeline_cache: Res, + billboard_gizmos: Query<(Entity, &GizmoMeshConfig)>, + mut transparent_render_phases: ResMut>, + mut views: Query<( + Entity, + &ExtractedView, + &Msaa, + Option<&RenderLayers>, + ( + Has, + Has, + Has, + Has, + ), + )>, +) { + let draw_function = draw_functions + .read() + .get_id::() + .unwrap(); + + for ( + view_entity, + view, + msaa, + render_layers, + (normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass), + ) in &mut views + { + let Some(transparent_phase) = transparent_render_phases.get_mut(&view_entity) else { + continue; + }; + + let render_layers = render_layers.unwrap_or_default(); + + let mut view_key = MeshPipelineKey::from_msaa_samples(msaa.samples()) + | MeshPipelineKey::from_hdr(view.hdr); + + if normal_prepass { + view_key |= MeshPipelineKey::NORMAL_PREPASS; + } + + if depth_prepass { + view_key |= MeshPipelineKey::DEPTH_PREPASS; + } + + if motion_vector_prepass { + view_key |= MeshPipelineKey::MOTION_VECTOR_PREPASS; + } + + if deferred_prepass { + view_key |= MeshPipelineKey::DEFERRED_PREPASS; + } + + for (entity, config) in &billboard_gizmos { + if !config.render_layers.intersects(render_layers) { + continue; + } + + let pipeline = pipelines.specialize( + &pipeline_cache, + &pipeline, + LineGizmoPipelineKey { + view_key, + perspective: config.billboard_perspective, + }, + ); + + transparent_phase.add(Transparent3d { + entity, + draw_function, + pipeline, + distance: 0., + batch_range: 0..1, + extra_index: PhaseItemExtraIndex::NONE, + }); + } + } +} diff --git a/crates/bevy_gizmos/src/config.rs b/crates/bevy_gizmos/src/config.rs index 0705029ddf19c..0d57c5a7763c8 100644 --- a/crates/bevy_gizmos/src/config.rs +++ b/crates/bevy_gizmos/src/config.rs @@ -141,6 +141,18 @@ pub struct GizmoConfig { pub enabled: bool, /// Line width specified in pixels. /// + /// If `billboard_perspective` is `true` then this is the size in pixels at the camera's near plane. + /// + /// Defaults to `10.0`. + pub billboard_size: f32, + /// Apply perspective to gizmo billboards. + /// + /// This setting only affects 3D, non-orthographic cameras. + /// + /// Defaults to `false`. + pub billboard_perspective: bool, + /// Billboard size specified in pixels. + /// /// If `line_perspective` is `true` then this is the size in pixels at the camera's near plane. /// /// Defaults to `2.0`. @@ -180,6 +192,8 @@ impl Default for GizmoConfig { fn default() -> Self { Self { enabled: true, + billboard_size: 10., + billboard_perspective: false, line_width: 2., line_perspective: false, line_style: GizmoLineStyle::Solid, @@ -198,6 +212,7 @@ impl Default for GizmoConfig { ))] #[derive(Component)] pub(crate) struct GizmoMeshConfig { + pub billboard_perspective: bool, pub line_perspective: bool, pub line_style: GizmoLineStyle, pub render_layers: bevy_render::view::RenderLayers, @@ -210,6 +225,7 @@ pub(crate) struct GizmoMeshConfig { impl From<&GizmoConfig> for GizmoMeshConfig { fn from(item: &GizmoConfig) -> Self { GizmoMeshConfig { + billboard_perspective: item.billboard_perspective, line_perspective: item.line_perspective, line_style: item.line_style, render_layers: item.render_layers.clone(), diff --git a/crates/bevy_gizmos/src/gizmos.rs b/crates/bevy_gizmos/src/gizmos.rs index 50998cc6d8a08..3b8b82ae2ef10 100644 --- a/crates/bevy_gizmos/src/gizmos.rs +++ b/crates/bevy_gizmos/src/gizmos.rs @@ -25,6 +25,8 @@ pub struct GizmoStorage { pub(crate) list_colors: Vec, pub(crate) strip_positions: Vec, pub(crate) strip_colors: Vec, + pub(crate) billboard_positions: Vec, + pub(crate) billboard_colors: Vec, marker: PhantomData<(Config, Clear)>, } @@ -35,6 +37,8 @@ impl Default for GizmoStorage { list_colors: default(), strip_positions: default(), strip_colors: default(), + billboard_positions: default(), + billboard_colors: default(), marker: PhantomData, } } @@ -54,6 +58,9 @@ where self.list_colors.extend(other.list_colors.iter()); self.strip_positions.extend(other.strip_positions.iter()); self.strip_colors.extend(other.strip_colors.iter()); + self.billboard_positions + .extend(other.billboard_positions.iter()); + self.billboard_colors.extend(other.billboard_colors.iter()); } pub(crate) fn swap( @@ -64,6 +71,11 @@ where mem::swap(&mut self.list_colors, &mut other.list_colors); mem::swap(&mut self.strip_positions, &mut other.strip_positions); mem::swap(&mut self.strip_colors, &mut other.strip_colors); + mem::swap( + &mut self.billboard_positions, + &mut other.billboard_positions, + ); + mem::swap(&mut self.billboard_colors, &mut other.billboard_colors); } /// Clear this gizmo storage of any requested gizmos. @@ -72,6 +84,8 @@ where self.list_colors.clear(); self.strip_positions.clear(); self.strip_colors.clear(); + self.billboard_positions.clear(); + self.billboard_colors.clear(); } } @@ -235,6 +249,8 @@ where list_colors: Vec, strip_positions: Vec, strip_colors: Vec, + billboard_positions: Vec, + billboard_colors: Vec, marker: PhantomData<(Config, Clear)>, } @@ -249,6 +265,8 @@ where list_colors: default(), strip_positions: default(), strip_colors: default(), + billboard_positions: default(), + billboard_colors: default(), marker: PhantomData, } } @@ -265,6 +283,10 @@ where storage.list_colors.append(&mut self.list_colors); storage.strip_positions.append(&mut self.strip_positions); storage.strip_colors.append(&mut self.strip_colors); + storage + .billboard_positions + .append(&mut self.billboard_positions); + storage.billboard_colors.append(&mut self.billboard_colors); } } @@ -273,6 +295,50 @@ where Config: GizmoConfigGroup, Clear: 'static + Send + Sync, { + /// Draw a billboard in 2D at `position`. + /// + /// This should be called for each frame the line needs to be rendered. + /// + /// # Example + /// ``` + /// # use bevy_gizmos::prelude::*; + /// # use bevy_math::prelude::*; + /// # use bevy_color::palettes::basic::GREEN; + /// fn system(mut gizmos: Gizmos) { + /// gizmos.billboard_2d(Vec2::ZERO, GREEN); + /// } + /// # bevy_ecs::system::assert_is_system(system); + /// ``` + pub fn billboard_2d(&mut self, position: Vec2, color: impl Into) { + if !self.enabled { + return; + } + self.buffer.billboard_positions.push(position.extend(0.)); + self.add_billboard_color(color); + } + + /// Draw a billboard in 3D at `position`. + /// + /// This should be called for each frame the line needs to be rendered. + /// + /// # Example + /// ``` + /// # use bevy_gizmos::prelude::*; + /// # use bevy_math::prelude::*; + /// # use bevy_color::palettes::basic::GREEN; + /// fn system(mut gizmos: Gizmos) { + /// gizmos.billboard(Vec3::ZERO, GREEN); + /// } + /// # bevy_ecs::system::assert_is_system(system); + /// ``` + pub fn billboard(&mut self, position: Vec3, color: impl Into) { + if !self.enabled { + return; + } + self.buffer.billboard_positions.push(position); + self.add_billboard_color(color); + } + /// Draw a line in 3D from `start` to `end`. /// /// This should be called for each frame the line needs to be rendered. @@ -293,7 +359,7 @@ where return; } self.extend_list_positions([start, end]); - self.add_list_color(color, 2); + self.extent_list_color(color, 2); } /// Draw a line in 3D with a color gradient from `start` to `end`. @@ -517,7 +583,7 @@ where ]; self.extend_list_positions(list_positions); - self.add_list_color(polymorphic_color, 6); + self.extent_list_color(polymorphic_color, 6); } /// Draw a line in 2D from `start` to `end`. @@ -722,7 +788,7 @@ where } #[inline] - fn add_list_color(&mut self, color: impl Into, count: usize) { + fn extent_list_color(&mut self, color: impl Into, count: usize) { let polymorphic_color: Color = color.into(); let linear_color = LinearRgba::from(polymorphic_color); @@ -731,6 +797,14 @@ where .extend(iter::repeat(linear_color).take(count)); } + #[inline] + fn add_billboard_color(&mut self, color: impl Into) { + let polymorphic_color: Color = color.into(); + let linear_color = LinearRgba::from(polymorphic_color); + + self.buffer.billboard_colors.push(linear_color); + } + #[inline] fn extend_strip_positions(&mut self, positions: impl IntoIterator) { self.buffer.strip_positions.extend(positions); diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs index 10f7e9cdc9b8b..326e794dfdb56 100644 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/src/lib.rs @@ -24,12 +24,15 @@ pub enum GizmoRenderSystem { /// Adds gizmos to the [`Transparent2d`](bevy_core_pipeline::core_2d::Transparent2d) render phase #[cfg(feature = "bevy_sprite")] - QueueLineGizmos2d, + QueueGizmos2d, /// Adds gizmos to the [`Transparent3d`](bevy_core_pipeline::core_3d::Transparent3d) render phase #[cfg(feature = "bevy_pbr")] - QueueLineGizmos3d, + QueueGizmos3d, } +mod billboard; +mod lines; + #[cfg(feature = "bevy_render")] pub mod aabb; pub mod arcs; @@ -40,16 +43,10 @@ pub mod cross; pub mod curves; pub mod gizmos; pub mod grid; -pub mod primitives; -pub mod rounded_box; - #[cfg(all(feature = "bevy_pbr", feature = "bevy_render"))] pub mod light; - -#[cfg(all(feature = "bevy_sprite", feature = "bevy_render"))] -mod pipeline_2d; -#[cfg(all(feature = "bevy_pbr", feature = "bevy_render"))] -mod pipeline_3d; +pub mod primitives; +pub mod rounded_box; /// The `bevy_gizmos` prelude. pub mod prelude { @@ -70,61 +67,19 @@ pub mod prelude { pub use crate::light::{LightGizmoColor, LightGizmoConfigGroup, ShowLightGizmo}; } -#[cfg(feature = "bevy_render")] -use bevy_ecs::{ - query::ROQueryItem, - system::{ - lifetimeless::{Read, SRes}, - Commands, SystemParamItem, - }, -}; - use bevy_app::{App, FixedFirst, FixedLast, Last, Plugin, RunFixedMainLoop}; -use bevy_asset::{Asset, AssetApp, Assets, Handle}; -use bevy_color::LinearRgba; -#[cfg(feature = "bevy_render")] -use bevy_ecs::component::Component; use bevy_ecs::{ - schedule::{IntoSystemConfigs, SystemSet}, - system::{Res, ResMut, Resource}, + schedule::{IntoSystemConfigs, IntoSystemSetConfigs, SystemSet}, + system::{Res, ResMut}, }; -use bevy_math::Vec3; -use bevy_reflect::TypePath; -#[cfg(all( - feature = "bevy_render", - any(feature = "bevy_pbr", feature = "bevy_sprite"), -))] -use bevy_render::render_resource::{VertexAttribute, VertexBufferLayout, VertexStepMode}; -#[cfg(feature = "bevy_render")] -use bevy_render::{ - extract_component::{ComponentUniforms, DynamicUniformIndex, UniformComponentPlugin}, - render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets}, - render_phase::{PhaseItem, RenderCommand, RenderCommandResult, TrackedRenderPass}, - render_resource::{ - binding_types::uniform_buffer, BindGroup, BindGroupEntries, BindGroupLayout, - BindGroupLayoutEntries, Buffer, BufferInitDescriptor, BufferUsages, Shader, ShaderStages, - ShaderType, VertexFormat, - }, - renderer::RenderDevice, - Extract, ExtractSchedule, Render, RenderApp, RenderSet, -}; - +use bevy_render::{Render, RenderSet}; use bevy_time::Fixed; -use bevy_utils::TypeIdMap; -#[cfg(feature = "bevy_render")] -use bytemuck::cast_slice; -use config::{ - DefaultGizmoConfigGroup, GizmoConfig, GizmoConfigGroup, GizmoConfigStore, GizmoLineJoint, -}; +use billboard::{AppBillboardGizmoBuilder, BillboardGizmoPlugin}; +use config::{DefaultGizmoConfigGroup, GizmoConfig, GizmoConfigGroup, GizmoConfigStore}; use gizmos::{GizmoStorage, Swap}; #[cfg(all(feature = "bevy_pbr", feature = "bevy_render"))] use light::LightGizmoPlugin; -use std::{any::TypeId, mem}; - -#[cfg(feature = "bevy_render")] -const LINE_SHADER_HANDLE: Handle = Handle::weak_from_u128(7414812689238026784); -#[cfg(feature = "bevy_render")] -const LINE_JOINT_SHADER_HANDLE: Handle = Handle::weak_from_u128(1162780797909187908); +use lines::{AppLineGizmoBuilder, LineGizmoPlugin}; /// A [`Plugin`] that provides an immediate mode drawing api for visual debugging. /// @@ -134,85 +89,43 @@ pub struct GizmoPlugin; impl Plugin for GizmoPlugin { fn build(&self, app: &mut App) { - #[cfg(feature = "bevy_render")] - { - use bevy_asset::load_internal_asset; - load_internal_asset!(app, LINE_SHADER_HANDLE, "lines.wgsl", Shader::from_wgsl); - load_internal_asset!( - app, - LINE_JOINT_SHADER_HANDLE, - "line_joints.wgsl", - Shader::from_wgsl - ); - } + #[cfg(all(feature = "bevy_pbr", feature = "bevy_render"))] + app.configure_sets( + Render, + GizmoRenderSystem::QueueGizmos3d + .in_set(RenderSet::Queue) + .ambiguous_with(bevy_pbr::queue_material_meshes::), + ); + + #[cfg(all(feature = "bevy_sprite", feature = "bevy_render"))] + app.configure_sets( + Render, + GizmoRenderSystem::QueueGizmos2d + .in_set(RenderSet::Queue) + .ambiguous_with(bevy_sprite::queue_sprites) + .ambiguous_with(bevy_sprite::queue_material2d_meshes::), + ); - app.register_type::() + app.add_plugins(LineGizmoPlugin) + .add_plugins(BillboardGizmoPlugin) + .register_type::() .register_type::() - .init_asset::() - .init_resource::() // We insert the Resource GizmoConfigStore into the world implicitly here if it does not exist. .init_gizmo_group::(); #[cfg(feature = "bevy_render")] - app.add_plugins(aabb::AabbGizmoPlugin) - .add_plugins(UniformComponentPlugin::::default()) - .add_plugins(RenderAssetPlugin::::default()); + app.add_plugins(aabb::AabbGizmoPlugin); #[cfg(all(feature = "bevy_pbr", feature = "bevy_render"))] app.add_plugins(LightGizmoPlugin); - - #[cfg(feature = "bevy_render")] - if let Some(render_app) = app.get_sub_app_mut(RenderApp) { - render_app.add_systems( - Render, - prepare_line_gizmo_bind_group.in_set(RenderSet::PrepareBindGroups), - ); - - render_app.add_systems(ExtractSchedule, extract_gizmo_data); - - #[cfg(feature = "bevy_sprite")] - if app.is_plugin_added::() { - app.add_plugins(pipeline_2d::LineGizmo2dPlugin); - } else { - bevy_utils::tracing::warn!("bevy_sprite feature is enabled but bevy_sprite::SpritePlugin was not detected. Are you sure you loaded GizmoPlugin after SpritePlugin?"); - } - #[cfg(feature = "bevy_pbr")] - if app.is_plugin_added::() { - app.add_plugins(pipeline_3d::LineGizmo3dPlugin); - } else { - bevy_utils::tracing::warn!("bevy_pbr feature is enabled but bevy_pbr::PbrPlugin was not detected. Are you sure you loaded GizmoPlugin after PbrPlugin?"); - } - } else { - bevy_utils::tracing::warn!("bevy_render feature is enabled but RenderApp was not detected. Are you sure you loaded GizmoPlugin after RenderPlugin?"); - } - } - - #[cfg(feature = "bevy_render")] - fn finish(&self, app: &mut App) { - let Some(render_app) = app.get_sub_app_mut(RenderApp) else { - return; - }; - - let render_device = render_app.world().resource::(); - let line_layout = render_device.create_bind_group_layout( - "LineGizmoUniform layout", - &BindGroupLayoutEntries::single( - ShaderStages::VERTEX, - uniform_buffer::(true), - ), - ); - - render_app.insert_resource(LineGizmoUniformBindgroupLayout { - layout: line_layout, - }); } } -/// A extension trait adding `App::init_gizmo_group` and `App::insert_gizmo_config`. +/// An extension trait adding `App::init_gizmo_group` and `App::insert_gizmo_config`. pub trait AppGizmoBuilder { /// Registers [`GizmoConfigGroup`] in the app enabling the use of [Gizmos<Config>](crate::gizmos::Gizmos). /// - /// Configurations can be set using the [`GizmoConfigStore`] [`Resource`]. + /// Configurations can be set using the [`GizmoConfigStore`] [`Resource`](bevy_ecs::system::Resource). fn init_gizmo_group(&mut self) -> &mut Self; /// Insert a [`GizmoConfig`] into a specific [`GizmoConfigGroup`]. @@ -235,15 +148,12 @@ impl AppGizmoBuilder for App { .get_resource_or_insert_with::(Default::default) .register::(); - let mut handles = self - .world_mut() - .get_resource_or_insert_with::(Default::default); - - handles.list.insert(TypeId::of::(), None); - handles.strip.insert(TypeId::of::(), None); + self.init_billboard_gizmo_group::(); + self.init_line_gizmo_group::(); - // These handles are safe to mutate in any order - self.allow_ambiguous_resource::(); + self.allow_ambiguous_resource::>(); + self.allow_ambiguous_resource::>(); + self.allow_ambiguous_resource::>>(); self.init_resource::>() .init_resource::>() @@ -262,10 +172,7 @@ impl AppGizmoBuilder for App { ) .add_systems( Last, - ( - propagate_gizmos::.before(UpdateGizmoMeshes), - update_gizmo_meshes::.in_set(UpdateGizmoMeshes), - ), + (propagate_gizmos::.before(UpdateGizmoMeshes),), ); self @@ -286,17 +193,6 @@ impl AppGizmoBuilder for App { } } -/// Holds handles to the line gizmos for each gizmo configuration group -// As `TypeIdMap` iteration order depends on the order of insertions and deletions, this uses -// `Option` to be able to reserve the slot when creating the gizmo configuration group. -// That way iteration order is stable across executions and depends on the order of configuration -// group creation. -#[derive(Resource, Default)] -struct LineGizmoHandles { - list: TypeIdMap>>, - strip: TypeIdMap>>, -} - /// Start a new gizmo clearing context. /// /// Internally this pushes the parent default context into a swap buffer. @@ -365,443 +261,3 @@ pub fn propagate_gizmos( /// System set for updating the rendering meshes for drawing gizmos. #[derive(SystemSet, Clone, Debug, PartialEq, Eq, Hash)] pub struct UpdateGizmoMeshes; - -/// Prepare gizmos for rendering. -/// -/// This also clears the default `GizmoStorage`. -fn update_gizmo_meshes( - mut line_gizmos: ResMut>, - mut handles: ResMut, - mut storage: ResMut>, - config_store: Res, -) { - if storage.list_positions.is_empty() { - handles.list.insert(TypeId::of::(), None); - } else if let Some(handle) = handles.list.get_mut(&TypeId::of::()) { - if let Some(handle) = handle { - let list = line_gizmos.get_mut(handle.id()).unwrap(); - - list.positions = mem::take(&mut storage.list_positions); - list.colors = mem::take(&mut storage.list_colors); - } else { - let list = LineGizmo { - strip: false, - config_ty: TypeId::of::(), - positions: mem::take(&mut storage.list_positions), - colors: mem::take(&mut storage.list_colors), - joints: GizmoLineJoint::None, - }; - - *handle = Some(line_gizmos.add(list)); - } - } - - let (config, _) = config_store.config::(); - if storage.strip_positions.is_empty() { - handles.strip.insert(TypeId::of::(), None); - } else if let Some(handle) = handles.strip.get_mut(&TypeId::of::()) { - if let Some(handle) = handle { - let strip = line_gizmos.get_mut(handle.id()).unwrap(); - - strip.positions = mem::take(&mut storage.strip_positions); - strip.colors = mem::take(&mut storage.strip_colors); - strip.joints = config.line_joints; - } else { - let strip = LineGizmo { - strip: true, - joints: config.line_joints, - config_ty: TypeId::of::(), - positions: mem::take(&mut storage.strip_positions), - colors: mem::take(&mut storage.strip_colors), - }; - - *handle = Some(line_gizmos.add(strip)); - } - } -} - -#[cfg(feature = "bevy_render")] -fn extract_gizmo_data( - mut commands: Commands, - handles: Extract>, - config: Extract>, -) { - for (group_type_id, handle) in handles.list.iter().chain(handles.strip.iter()) { - let Some((config, _)) = config.get_config_dyn(group_type_id) else { - continue; - }; - - if !config.enabled { - continue; - } - - let Some(handle) = handle else { - continue; - }; - - let joints_resolution = if let GizmoLineJoint::Round(resolution) = config.line_joints { - resolution - } else { - 0 - }; - - commands.spawn(( - LineGizmoUniform { - line_width: config.line_width, - depth_bias: config.depth_bias, - joints_resolution, - #[cfg(feature = "webgl")] - _padding: Default::default(), - }, - (*handle).clone_weak(), - #[cfg(any(feature = "bevy_pbr", feature = "bevy_sprite"))] - config::GizmoMeshConfig::from(config), - )); - } -} - -#[cfg(feature = "bevy_render")] -#[derive(Component, ShaderType, Clone, Copy)] -struct LineGizmoUniform { - line_width: f32, - depth_bias: f32, - // Only used by gizmo line t if the current configs `line_joints` is set to `GizmoLineJoint::Round(_)` - joints_resolution: u32, - /// WebGL2 structs must be 16 byte aligned. - #[cfg(feature = "webgl")] - _padding: f32, -} - -/// A gizmo asset that represents a line. -#[derive(Asset, Debug, Clone, TypePath)] -pub struct LineGizmo { - /// Positions of the gizmo's vertices - pub positions: Vec, - /// Colors of the gizmo's vertices - pub colors: Vec, - /// Whether this gizmo's topology is a line-strip or line-list - pub strip: bool, - /// Whether this gizmo should draw line joints. This is only applicable if the gizmo's topology is line-strip. - pub joints: GizmoLineJoint, - /// The type of the gizmo's configuration group - pub config_ty: TypeId, -} - -#[cfg(feature = "bevy_render")] -#[derive(Debug, Clone)] -struct GpuLineGizmo { - position_buffer: Buffer, - color_buffer: Buffer, - vertex_count: u32, - strip: bool, - joints: GizmoLineJoint, -} - -#[cfg(feature = "bevy_render")] -impl RenderAsset for GpuLineGizmo { - type SourceAsset = LineGizmo; - type Param = SRes; - - fn prepare_asset( - gizmo: Self::SourceAsset, - render_device: &mut SystemParamItem, - ) -> Result> { - let position_buffer_data = cast_slice(&gizmo.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(&gizmo.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: gizmo.positions.len() as u32, - strip: gizmo.strip, - joints: gizmo.joints, - }) - } -} - -#[cfg(feature = "bevy_render")] -#[derive(Resource)] -struct LineGizmoUniformBindgroupLayout { - layout: BindGroupLayout, -} - -#[cfg(feature = "bevy_render")] -#[derive(Resource)] -struct LineGizmoUniformBindgroup { - bindgroup: BindGroup, -} - -#[cfg(feature = "bevy_render")] -fn prepare_line_gizmo_bind_group( - mut commands: Commands, - line_gizmo_uniform_layout: Res, - render_device: Res, - line_gizmo_uniforms: Res>, -) { - 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), - ), - }); - } -} - -#[cfg(feature = "bevy_render")] -struct SetLineGizmoBindGroup; -#[cfg(feature = "bevy_render")] -impl RenderCommand

for SetLineGizmoBindGroup { - type Param = SRes; - type ViewQuery = (); - type ItemQuery = Read>; - - #[inline] - fn render<'w>( - _item: &P, - _view: ROQueryItem<'w, Self::ViewQuery>, - uniform_index: Option>, - bind_group: SystemParamItem<'w, '_, Self::Param>, - pass: &mut TrackedRenderPass<'w>, - ) -> RenderCommandResult { - let Some(uniform_index) = uniform_index else { - return RenderCommandResult::Skip; - }; - pass.set_bind_group( - I, - &bind_group.into_inner().bindgroup, - &[uniform_index.index()], - ); - RenderCommandResult::Success - } -} - -#[cfg(feature = "bevy_render")] -struct DrawLineGizmo; -#[cfg(feature = "bevy_render")] -impl RenderCommand

for DrawLineGizmo { - type Param = SRes>; - type ViewQuery = (); - type ItemQuery = Read>; - - #[inline] - fn render<'w>( - _item: &P, - _view: ROQueryItem<'w, Self::ViewQuery>, - handle: Option>, - line_gizmos: SystemParamItem<'w, '_, Self::Param>, - pass: &mut TrackedRenderPass<'w>, - ) -> RenderCommandResult { - let Some(handle) = handle else { - return RenderCommandResult::Skip; - }; - let Some(line_gizmo) = line_gizmos.into_inner().get(handle) else { - return RenderCommandResult::Skip; - }; - - 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 - } -} - -#[cfg(feature = "bevy_render")] -struct DrawLineJointGizmo; -#[cfg(feature = "bevy_render")] -impl RenderCommand

for DrawLineJointGizmo { - type Param = SRes>; - type ViewQuery = (); - type ItemQuery = Read>; - - #[inline] - fn render<'w>( - _item: &P, - _view: ROQueryItem<'w, Self::ViewQuery>, - handle: Option>, - line_gizmos: SystemParamItem<'w, '_, Self::Param>, - pass: &mut TrackedRenderPass<'w>, - ) -> RenderCommandResult { - let Some(handle) = handle else { - return RenderCommandResult::Skip; - }; - let Some(line_gizmo) = line_gizmos.into_inner().get(handle) else { - return RenderCommandResult::Skip; - }; - - if line_gizmo.vertex_count <= 2 || !line_gizmo.strip { - return RenderCommandResult::Success; - }; - - if line_gizmo.joints == GizmoLineJoint::None { - return RenderCommandResult::Success; - }; - - let instances = { - let item_size = VertexFormat::Float32x3.size(); - // position_a - let buffer_size_a = line_gizmo.position_buffer.size() - item_size * 2; - pass.set_vertex_buffer(0, line_gizmo.position_buffer.slice(..buffer_size_a)); - // position_b - let buffer_size_b = line_gizmo.position_buffer.size() - item_size; - pass.set_vertex_buffer( - 1, - line_gizmo.position_buffer.slice(item_size..buffer_size_b), - ); - // position_c - pass.set_vertex_buffer(2, line_gizmo.position_buffer.slice(item_size * 2..)); - - // color - let item_size = VertexFormat::Float32x4.size(); - let buffer_size = line_gizmo.color_buffer.size() - item_size; - // This corresponds to the color of position_b, hence starts from `item_size` - pass.set_vertex_buffer(3, line_gizmo.color_buffer.slice(item_size..buffer_size)); - - u32::max(line_gizmo.vertex_count, 2) - 2 - }; - - let vertices = match line_gizmo.joints { - GizmoLineJoint::None => unreachable!(), - GizmoLineJoint::Miter => 6, - GizmoLineJoint::Round(resolution) => resolution * 3, - GizmoLineJoint::Bevel => 3, - }; - - pass.draw(0..vertices, 0..instances); - - RenderCommandResult::Success - } -} - -#[cfg(all( - feature = "bevy_render", - any(feature = "bevy_pbr", feature = "bevy_sprite") -))] -fn line_gizmo_vertex_buffer_layouts(strip: bool) -> Vec { - 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] - } -} - -#[cfg(all( - feature = "bevy_render", - any(feature = "bevy_pbr", feature = "bevy_sprite") -))] -fn line_joint_gizmo_vertex_buffer_layouts() -> Vec { - 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 color_layout = VertexBufferLayout { - array_stride: Float32x4.size(), - step_mode: VertexStepMode::Instance, - attributes: vec![VertexAttribute { - format: Float32x4, - offset: 0, - shader_location: 3, - }], - }; - - vec![ - position_layout.clone(), - { - position_layout.attributes[0].shader_location = 1; - position_layout.clone() - }, - { - position_layout.attributes[0].shader_location = 2; - position_layout - }, - color_layout.clone(), - ] -} diff --git a/crates/bevy_gizmos/src/line_joints.wgsl b/crates/bevy_gizmos/src/lines/line_joints.wgsl similarity index 100% rename from crates/bevy_gizmos/src/line_joints.wgsl rename to crates/bevy_gizmos/src/lines/line_joints.wgsl diff --git a/crates/bevy_gizmos/src/lines.wgsl b/crates/bevy_gizmos/src/lines/lines.wgsl similarity index 100% rename from crates/bevy_gizmos/src/lines.wgsl rename to crates/bevy_gizmos/src/lines/lines.wgsl diff --git a/crates/bevy_gizmos/src/lines/mod.rs b/crates/bevy_gizmos/src/lines/mod.rs new file mode 100644 index 0000000000000..dcf7319ff0ccb --- /dev/null +++ b/crates/bevy_gizmos/src/lines/mod.rs @@ -0,0 +1,600 @@ +//! A module adding line drawing capabilities to gizmos + +use std::{any::TypeId, mem}; + +use bevy_app::{App, Last, Plugin}; +use bevy_asset::{Asset, AssetApp, Assets, Handle}; +use bevy_color::LinearRgba; +use bevy_ecs::{ + component::Component, + query::ROQueryItem, + schedule::IntoSystemConfigs, + system::{ + lifetimeless::{Read, SRes}, + Commands, Res, ResMut, Resource, SystemParamItem, + }, +}; +use bevy_math::Vec3; +use bevy_reflect::TypePath; +use bevy_render::{ + extract_component::{ComponentUniforms, DynamicUniformIndex, UniformComponentPlugin}, + render_asset::{PrepareAssetError, RenderAsset, 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::TypeIdMap; +use bytemuck::cast_slice; + +use crate::{ + config::{GizmoConfigGroup, GizmoConfigStore, GizmoLineJoint}, + gizmos::GizmoStorage, + UpdateGizmoMeshes, +}; + +#[cfg(all(feature = "bevy_sprite", feature = "bevy_render"))] +mod pipeline_2d; +#[cfg(all(feature = "bevy_pbr", feature = "bevy_render"))] +mod pipeline_3d; + +#[cfg(feature = "bevy_render")] +const LINE_SHADER_HANDLE: Handle = Handle::weak_from_u128(7414812689238026784); +#[cfg(feature = "bevy_render")] +const LINE_JOINT_SHADER_HANDLE: Handle = Handle::weak_from_u128(1162780797909187908); + +/// A [`Plugin`] that provides an immediate mode line drawing api for visual debugging. +#[derive(Default)] +pub struct LineGizmoPlugin; + +impl Plugin for LineGizmoPlugin { + fn build(&self, app: &mut App) { + #[cfg(feature = "bevy_render")] + { + use bevy_asset::load_internal_asset; + load_internal_asset!(app, LINE_SHADER_HANDLE, "lines.wgsl", Shader::from_wgsl); + load_internal_asset!( + app, + LINE_JOINT_SHADER_HANDLE, + "line_joints.wgsl", + Shader::from_wgsl + ); + } + + app.init_asset::() + .init_resource::(); + + // These handles are safe to mutate in any order + app.allow_ambiguous_resource::(); + + #[cfg(feature = "bevy_render")] + app.add_plugins(UniformComponentPlugin::::default()) + .add_plugins(RenderAssetPlugin::::default()); + + #[cfg(feature = "bevy_render")] + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app.add_systems( + Render, + prepare_line_gizmo_bind_group.in_set(RenderSet::PrepareBindGroups), + ); + + render_app.add_systems(ExtractSchedule, extract_gizmo_data); + + #[cfg(feature = "bevy_sprite")] + if app.is_plugin_added::() { + app.add_plugins(pipeline_2d::LineGizmo2dPlugin); + } else { + bevy_utils::tracing::warn!("bevy_sprite feature is enabled but bevy_sprite::SpritePlugin was not detected. Are you sure you loaded GizmoPlugin after SpritePlugin?"); + } + #[cfg(feature = "bevy_pbr")] + if app.is_plugin_added::() { + app.add_plugins(pipeline_3d::LineGizmo3dPlugin); + } else { + bevy_utils::tracing::warn!("bevy_pbr feature is enabled but bevy_pbr::PbrPlugin was not detected. Are you sure you loaded GizmoPlugin after PbrPlugin?"); + } + } else { + bevy_utils::tracing::warn!("bevy_render feature is enabled but RenderApp was not detected. Are you sure you loaded GizmoPlugin after RenderPlugin?"); + } + } + + #[cfg(feature = "bevy_render")] + fn finish(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + let render_device = render_app.world().resource::(); + let line_layout = render_device.create_bind_group_layout( + "LineGizmoUniform layout", + &BindGroupLayoutEntries::single( + ShaderStages::VERTEX, + uniform_buffer::(true), + ), + ); + + render_app.insert_resource(LineGizmoUniformBindgroupLayout { + layout: line_layout, + }); + } +} + +/// An internal extension trait adding `App::init_line_gizmo_group`. +pub(crate) trait AppLineGizmoBuilder { + /// Registers [`GizmoConfigGroup`] in the app enabling the use of [Gizmos<Config>](crate::gizmos::Gizmos). + /// + /// Configurations can be set using the [`GizmoConfigStore`] [`Resource`]. + fn init_line_gizmo_group(&mut self) -> &mut Self; +} + +impl AppLineGizmoBuilder for App { + fn init_line_gizmo_group(&mut self) -> &mut Self { + if self.world().contains_resource::>() { + return self; + } + + let mut handles = self + .world_mut() + .get_resource_or_insert_with::(Default::default); + + handles.list.insert(TypeId::of::(), None); + handles.strip.insert(TypeId::of::(), None); + + self.add_systems( + Last, + update_gizmo_meshes::.in_set(UpdateGizmoMeshes), + ); + + self + } +} + +/// Holds handles to the line gizmos for each gizmo configuration group +// As `TypeIdMap` iteration order depends on the order of insertions and deletions, this uses +// `Option` to be able to reserve the slot when creating the gizmo configuration group. +// That way iteration order is stable across executions and depends on the order of configuration +// group creation. +#[derive(Resource, Default)] +struct LineGizmoHandles { + list: TypeIdMap>>, + strip: TypeIdMap>>, +} + +/// Prepare gizmos for rendering. +/// +/// This also clears the default `GizmoStorage`. +fn update_gizmo_meshes( + mut line_gizmos: ResMut>, + mut handles: ResMut, + mut storage: ResMut>, + config_store: Res, +) { + if storage.list_positions.is_empty() { + handles.list.insert(TypeId::of::(), None); + } else if let Some(handle) = handles.list.get_mut(&TypeId::of::()) { + if let Some(handle) = handle { + let list = line_gizmos.get_mut(handle.id()).unwrap(); + + list.positions = mem::take(&mut storage.list_positions); + list.colors = mem::take(&mut storage.list_colors); + } else { + let list = LineGizmo { + strip: false, + positions: mem::take(&mut storage.list_positions), + colors: mem::take(&mut storage.list_colors), + joints: GizmoLineJoint::None, + }; + + *handle = Some(line_gizmos.add(list)); + } + } + + let (config, _) = config_store.config::(); + if storage.strip_positions.is_empty() { + handles.strip.insert(TypeId::of::(), None); + } else if let Some(handle) = handles.strip.get_mut(&TypeId::of::()) { + if let Some(handle) = handle { + let strip = line_gizmos.get_mut(handle.id()).unwrap(); + + strip.positions = mem::take(&mut storage.strip_positions); + strip.colors = mem::take(&mut storage.strip_colors); + strip.joints = config.line_joints; + } else { + let strip = LineGizmo { + strip: true, + joints: config.line_joints, + positions: mem::take(&mut storage.strip_positions), + colors: mem::take(&mut storage.strip_colors), + }; + + *handle = Some(line_gizmos.add(strip)); + } + } +} + +#[cfg(feature = "bevy_render")] +fn extract_gizmo_data( + mut commands: Commands, + handles: Extract>, + config: Extract>, +) { + for (group_type_id, handle) in handles.list.iter().chain(handles.strip.iter()) { + let Some((config, _)) = config.get_config_dyn(group_type_id) else { + continue; + }; + + if !config.enabled { + continue; + } + + let Some(handle) = handle else { + continue; + }; + + let joints_resolution = if let GizmoLineJoint::Round(resolution) = config.line_joints { + resolution + } else { + 0 + }; + + commands.spawn(( + LineGizmoUniform { + line_width: config.line_width, + depth_bias: config.depth_bias, + joints_resolution, + #[cfg(feature = "webgl")] + _padding: Default::default(), + }, + (*handle).clone_weak(), + #[cfg(any(feature = "bevy_pbr", feature = "bevy_sprite"))] + crate::config::GizmoMeshConfig::from(config), + )); + } +} + +#[cfg(feature = "bevy_render")] +#[derive(Component, ShaderType, Clone, Copy)] +struct LineGizmoUniform { + line_width: f32, + depth_bias: f32, + // Only used by gizmo line t if the current configs `line_joints` is set to `GizmoLineJoint::Round(_)` + joints_resolution: u32, + /// WebGL2 structs must be 16 byte aligned. + #[cfg(feature = "webgl")] + _padding: f32, +} + +/// A gizmo asset that represents a line. +#[derive(Asset, Debug, Clone, TypePath)] +pub struct LineGizmo { + /// Positions of the gizmo's vertices + pub positions: Vec, + /// Colors of the gizmo's vertices + pub colors: Vec, + /// Whether this gizmo's topology is a line-strip or line-list + pub strip: bool, + /// Whether this gizmo should draw line joints. This is only applicable if the gizmo's topology is line-strip. + pub joints: GizmoLineJoint, +} + +#[cfg(feature = "bevy_render")] +#[derive(Debug, Clone)] +struct GpuLineGizmo { + position_buffer: Buffer, + color_buffer: Buffer, + vertex_count: u32, + strip: bool, + joints: GizmoLineJoint, +} + +#[cfg(feature = "bevy_render")] +impl RenderAsset for GpuLineGizmo { + type SourceAsset = LineGizmo; + type Param = SRes; + + fn prepare_asset( + gizmo: Self::SourceAsset, + render_device: &mut SystemParamItem, + ) -> Result> { + let position_buffer_data = cast_slice(&gizmo.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(&gizmo.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: gizmo.positions.len() as u32, + strip: gizmo.strip, + joints: gizmo.joints, + }) + } +} + +#[cfg(feature = "bevy_render")] +#[derive(Resource)] +struct LineGizmoUniformBindgroupLayout { + layout: BindGroupLayout, +} + +#[cfg(feature = "bevy_render")] +#[derive(Resource)] +struct LineGizmoUniformBindgroup { + bindgroup: BindGroup, +} + +#[cfg(feature = "bevy_render")] +fn prepare_line_gizmo_bind_group( + mut commands: Commands, + line_gizmo_uniform_layout: Res, + render_device: Res, + line_gizmo_uniforms: Res>, +) { + 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), + ), + }); + } +} + +#[cfg(feature = "bevy_render")] +struct SetLineGizmoBindGroup; +#[cfg(feature = "bevy_render")] +impl RenderCommand

for SetLineGizmoBindGroup { + type Param = SRes; + type ViewQuery = (); + type ItemQuery = Read>; + + #[inline] + fn render<'w>( + _item: &P, + _view: ROQueryItem<'w, Self::ViewQuery>, + uniform_index: Option>, + bind_group: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let Some(uniform_index) = uniform_index else { + return RenderCommandResult::Skip; + }; + pass.set_bind_group( + I, + &bind_group.into_inner().bindgroup, + &[uniform_index.index()], + ); + RenderCommandResult::Success + } +} + +#[cfg(feature = "bevy_render")] +struct DrawLineGizmo; +#[cfg(feature = "bevy_render")] +impl RenderCommand

for DrawLineGizmo { + type Param = SRes>; + type ViewQuery = (); + type ItemQuery = Read>; + + #[inline] + fn render<'w>( + _item: &P, + _view: ROQueryItem<'w, Self::ViewQuery>, + handle: Option>, + line_gizmos: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let Some(handle) = handle else { + return RenderCommandResult::Skip; + }; + let Some(line_gizmo) = line_gizmos.into_inner().get(handle) else { + return RenderCommandResult::Skip; + }; + + 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 + } +} + +#[cfg(feature = "bevy_render")] +struct DrawLineJointGizmo; +#[cfg(feature = "bevy_render")] +impl RenderCommand

for DrawLineJointGizmo { + type Param = SRes>; + type ViewQuery = (); + type ItemQuery = Read>; + + #[inline] + fn render<'w>( + _item: &P, + _view: ROQueryItem<'w, Self::ViewQuery>, + handle: Option>, + line_gizmos: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let Some(handle) = handle else { + return RenderCommandResult::Skip; + }; + let Some(line_gizmo) = line_gizmos.into_inner().get(handle) else { + return RenderCommandResult::Skip; + }; + + if line_gizmo.vertex_count <= 2 || !line_gizmo.strip { + return RenderCommandResult::Success; + }; + + if line_gizmo.joints == GizmoLineJoint::None { + return RenderCommandResult::Success; + }; + + let instances = { + let item_size = VertexFormat::Float32x3.size(); + // position_a + let buffer_size_a = line_gizmo.position_buffer.size() - item_size * 2; + pass.set_vertex_buffer(0, line_gizmo.position_buffer.slice(..buffer_size_a)); + // position_b + let buffer_size_b = line_gizmo.position_buffer.size() - item_size; + pass.set_vertex_buffer( + 1, + line_gizmo.position_buffer.slice(item_size..buffer_size_b), + ); + // position_c + pass.set_vertex_buffer(2, line_gizmo.position_buffer.slice(item_size * 2..)); + + // color + let item_size = VertexFormat::Float32x4.size(); + let buffer_size = line_gizmo.color_buffer.size() - item_size; + // This corresponds to the color of position_b, hence starts from `item_size` + pass.set_vertex_buffer(3, line_gizmo.color_buffer.slice(item_size..buffer_size)); + + u32::max(line_gizmo.vertex_count, 2) - 2 + }; + + let vertices = match line_gizmo.joints { + GizmoLineJoint::None => unreachable!(), + GizmoLineJoint::Miter => 6, + GizmoLineJoint::Round(resolution) => resolution * 3, + GizmoLineJoint::Bevel => 3, + }; + + pass.draw(0..vertices, 0..instances); + + RenderCommandResult::Success + } +} + +#[cfg(all( + feature = "bevy_render", + any(feature = "bevy_pbr", feature = "bevy_sprite") +))] +fn line_gizmo_vertex_buffer_layouts(strip: bool) -> Vec { + 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] + } +} + +#[cfg(all( + feature = "bevy_render", + any(feature = "bevy_pbr", feature = "bevy_sprite") +))] +fn line_joint_gizmo_vertex_buffer_layouts() -> Vec { + 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 color_layout = VertexBufferLayout { + array_stride: Float32x4.size(), + step_mode: VertexStepMode::Instance, + attributes: vec![VertexAttribute { + format: Float32x4, + offset: 0, + shader_location: 3, + }], + }; + + vec![ + position_layout.clone(), + { + position_layout.attributes[0].shader_location = 1; + position_layout.clone() + }, + { + position_layout.attributes[0].shader_location = 2; + position_layout + }, + color_layout.clone(), + ] +} diff --git a/crates/bevy_gizmos/src/pipeline_2d.rs b/crates/bevy_gizmos/src/lines/pipeline_2d.rs similarity index 94% rename from crates/bevy_gizmos/src/pipeline_2d.rs rename to crates/bevy_gizmos/src/lines/pipeline_2d.rs index 0f6552f787406..15e65bdbd3661 100644 --- a/crates/bevy_gizmos/src/pipeline_2d.rs +++ b/crates/bevy_gizmos/src/lines/pipeline_2d.rs @@ -1,9 +1,11 @@ +use super::{ + line_gizmo_vertex_buffer_layouts, line_joint_gizmo_vertex_buffer_layouts, DrawLineGizmo, + DrawLineJointGizmo, GpuLineGizmo, LineGizmo, LineGizmoUniformBindgroupLayout, + SetLineGizmoBindGroup, +}; use crate::{ config::{GizmoLineJoint, GizmoLineStyle, GizmoMeshConfig}, - line_gizmo_vertex_buffer_layouts, line_joint_gizmo_vertex_buffer_layouts, DrawLineGizmo, - DrawLineJointGizmo, GizmoRenderSystem, GpuLineGizmo, LineGizmo, - LineGizmoUniformBindgroupLayout, SetLineGizmoBindGroup, LINE_JOINT_SHADER_HANDLE, - LINE_SHADER_HANDLE, + GizmoRenderSystem, }; use bevy_app::{App, Plugin}; use bevy_asset::Handle; @@ -11,7 +13,7 @@ use bevy_core_pipeline::core_2d::{Transparent2d, CORE_2D_DEPTH_FORMAT}; use bevy_ecs::{ prelude::Entity, - schedule::{IntoSystemConfigs, IntoSystemSetConfigs}, + schedule::IntoSystemConfigs, system::{Query, Res, ResMut, Resource}, world::{FromWorld, World}, }; @@ -25,11 +27,13 @@ use bevy_render::{ render_resource::*, texture::BevyDefault, view::{ExtractedView, Msaa, RenderLayers, ViewTarget}, - Render, RenderApp, RenderSet, + Render, RenderApp, }; use bevy_sprite::{Mesh2dPipeline, Mesh2dPipelineKey, SetMesh2dViewBindGroup}; use bevy_utils::tracing::error; +use super::{LINE_JOINT_SHADER_HANDLE, LINE_SHADER_HANDLE}; + pub struct LineGizmo2dPlugin; impl Plugin for LineGizmo2dPlugin { @@ -43,19 +47,10 @@ impl Plugin for LineGizmo2dPlugin { .add_render_command::() .init_resource::>() .init_resource::>() - .configure_sets( - Render, - GizmoRenderSystem::QueueLineGizmos2d - .in_set(RenderSet::Queue) - .ambiguous_with(bevy_sprite::queue_sprites) - .ambiguous_with( - bevy_sprite::queue_material2d_meshes::, - ), - ) .add_systems( Render, (queue_line_gizmos_2d, queue_line_joint_gizmos_2d) - .in_set(GizmoRenderSystem::QueueLineGizmos2d) + .in_set(GizmoRenderSystem::QueueGizmos2d) .after(prepare_assets::), ); } diff --git a/crates/bevy_gizmos/src/pipeline_3d.rs b/crates/bevy_gizmos/src/lines/pipeline_3d.rs similarity index 95% rename from crates/bevy_gizmos/src/pipeline_3d.rs rename to crates/bevy_gizmos/src/lines/pipeline_3d.rs index 8197623b3618c..63152529e27ae 100644 --- a/crates/bevy_gizmos/src/pipeline_3d.rs +++ b/crates/bevy_gizmos/src/lines/pipeline_3d.rs @@ -1,9 +1,11 @@ +use super::{ + line_gizmo_vertex_buffer_layouts, line_joint_gizmo_vertex_buffer_layouts, DrawLineGizmo, + DrawLineJointGizmo, GpuLineGizmo, LineGizmo, LineGizmoUniformBindgroupLayout, + SetLineGizmoBindGroup, +}; use crate::{ config::{GizmoLineJoint, GizmoLineStyle, GizmoMeshConfig}, - line_gizmo_vertex_buffer_layouts, line_joint_gizmo_vertex_buffer_layouts, DrawLineGizmo, - DrawLineJointGizmo, GizmoRenderSystem, GpuLineGizmo, LineGizmo, - LineGizmoUniformBindgroupLayout, SetLineGizmoBindGroup, LINE_JOINT_SHADER_HANDLE, - LINE_SHADER_HANDLE, + GizmoRenderSystem, }; use bevy_app::{App, Plugin}; use bevy_asset::Handle; @@ -15,7 +17,7 @@ use bevy_core_pipeline::{ use bevy_ecs::{ prelude::Entity, query::Has, - schedule::{IntoSystemConfigs, IntoSystemSetConfigs}, + schedule::IntoSystemConfigs, system::{Query, Res, ResMut, Resource}, world::{FromWorld, World}, }; @@ -29,10 +31,12 @@ use bevy_render::{ render_resource::*, texture::BevyDefault, view::{ExtractedView, Msaa, RenderLayers, ViewTarget}, - Render, RenderApp, RenderSet, + Render, RenderApp, }; use bevy_utils::tracing::error; +use super::{LINE_JOINT_SHADER_HANDLE, LINE_SHADER_HANDLE}; + pub struct LineGizmo3dPlugin; impl Plugin for LineGizmo3dPlugin { fn build(&self, app: &mut App) { @@ -45,16 +49,10 @@ impl Plugin for LineGizmo3dPlugin { .add_render_command::() .init_resource::>() .init_resource::>() - .configure_sets( - Render, - GizmoRenderSystem::QueueLineGizmos3d - .in_set(RenderSet::Queue) - .ambiguous_with(bevy_pbr::queue_material_meshes::), - ) .add_systems( Render, (queue_line_gizmos_3d, queue_line_joint_gizmos_3d) - .in_set(GizmoRenderSystem::QueueLineGizmos3d) + .in_set(GizmoRenderSystem::QueueGizmos3d) .after(prepare_assets::), ); } diff --git a/examples/gizmos/2d_gizmos.rs b/examples/gizmos/2d_gizmos.rs index 0fa75863d50f8..a77750a4a1faa 100644 --- a/examples/gizmos/2d_gizmos.rs +++ b/examples/gizmos/2d_gizmos.rs @@ -82,6 +82,8 @@ fn draw_example_collection( .map(|t| (t, TEAL.mix(&HOT_PINK, (t + 300.0) / 600.0))); gizmos.curve_gradient_2d(curve, times_and_colors); + gizmos.billboard_2d(Vec2::new(200., 160.), RED); + my_gizmos .rounded_rect_2d(Isometry2d::IDENTITY, Vec2::splat(630.), BLACK) .corner_radius((time.elapsed_seconds() / 3.).cos() * 100.);