diff --git a/Cargo.toml b/Cargo.toml index d47dab1d6f7af..36b643ee30d03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ default = [ "vorbis", "x11", "filesystem_watcher", + "bevy_gizmos", "android_shared_stdcxx", "tonemapping_luts", ] @@ -98,6 +99,9 @@ bevy_ui = ["bevy_internal/bevy_ui", "bevy_core_pipeline", "bevy_text", "bevy_spr # winit window and input backend bevy_winit = ["bevy_internal/bevy_winit"] +# Adds support for rendering gizmos +bevy_gizmos = ["bevy_internal/bevy_gizmos"] + # Tracing support, saving a file in Chrome Tracing format trace_chrome = ["trace", "bevy_internal/trace_chrome"] @@ -309,6 +313,16 @@ description = "Renders a rectangle, circle, and hexagon" category = "2D Rendering" wasm = true +[[example]] +name = "2d_gizmos" +path = "examples/2d/2d_gizmos.rs" + +[package.metadata.example.2d_gizmos] +name = "2D Gizmos" +description = "A scene showcasing 2D gizmos" +category = "2D Rendering" +wasm = true + [[example]] name = "sprite" path = "examples/2d/sprite.rs" @@ -400,6 +414,16 @@ description = "A scene showcasing the built-in 3D shapes" category = "3D Rendering" wasm = true +[[example]] +name = "3d_gizmos" +path = "examples/3d/3d_gizmos.rs" + +[package.metadata.example.3d_gizmos] +name = "3D Gizmos" +description = "A scene showcasing 3D gizmos" +category = "3D Rendering" +wasm = true + [[example]] name = "atmospheric_fog" path = "examples/3d/atmospheric_fog.rs" @@ -1553,6 +1577,16 @@ description = "Simple benchmark to test per-entity draw overhead. Run with the ` category = "Stress Tests" wasm = true +[[example]] +name = "many_gizmos" +path = "examples/stress_tests/many_gizmos.rs" + +[package.metadata.example.many_gizmos] +name = "Many Gizmos" +description = "Test rendering of many gizmos" +category = "Stress Tests" +wasm = true + [[example]] name = "many_foxes" path = "examples/stress_tests/many_foxes.rs" diff --git a/crates/bevy_gizmos/Cargo.toml b/crates/bevy_gizmos/Cargo.toml new file mode 100644 index 0000000000000..cee285784ca69 --- /dev/null +++ b/crates/bevy_gizmos/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "bevy_gizmos" +version = "0.11.0-dev" +edition = "2021" +description = "Provides gizmos for Bevy Engine" +homepage = "https://bevyengine.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["bevy"] + +[dependencies] +# Bevy +bevy_pbr = { path = "../bevy_pbr", version = "0.11.0-dev", optional = true } +bevy_sprite = { path = "../bevy_sprite", version = "0.11.0-dev", optional = true } +bevy_app = { path = "../bevy_app", version = "0.11.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.11.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.11.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.11.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.11.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.11.0-dev" } +bevy_core = { path = "../bevy_core", version = "0.11.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.11.0-dev" } +bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.11.0-dev" } diff --git a/crates/bevy_gizmos/src/gizmos.rs b/crates/bevy_gizmos/src/gizmos.rs new file mode 100644 index 0000000000000..fa37416db83a6 --- /dev/null +++ b/crates/bevy_gizmos/src/gizmos.rs @@ -0,0 +1,349 @@ +use std::{f32::consts::TAU, iter}; + +use bevy_ecs::{ + system::{Deferred, Resource, SystemBuffer, SystemMeta}, + world::World, +}; +use bevy_math::{Mat2, Quat, Vec2, Vec3}; +use bevy_render::prelude::Color; + +type PositionItem = [f32; 3]; +type ColorItem = [f32; 4]; + +const DEFAULT_CIRCLE_SEGMENTS: usize = 32; + +#[derive(Resource, Default)] +pub(crate) struct GizmoStorage { + pub list_positions: Vec, + pub list_colors: Vec, + pub strip_positions: Vec, + pub strip_colors: Vec, +} + +pub type Gizmos<'s> = Deferred<'s, GizmoBuffer>; + +#[derive(Default)] +pub struct GizmoBuffer { + list_positions: Vec, + list_colors: Vec, + strip_positions: Vec, + strip_colors: Vec, +} + +impl SystemBuffer for GizmoBuffer { + fn apply(&mut self, _system_meta: &SystemMeta, world: &mut World) { + let mut storage = world.resource_mut::(); + storage.list_positions.append(&mut self.list_positions); + storage.list_colors.append(&mut self.list_colors); + storage.strip_positions.append(&mut self.strip_positions); + storage.strip_colors.append(&mut self.strip_colors); + } +} + +impl GizmoBuffer { + #[inline] + pub fn line(&mut self, start: Vec3, end: Vec3, color: Color) { + self.extend_list_positions([start, end]); + self.add_list_color(color, 2); + } + + /// Draw a line from `start` to `end`. + #[inline] + pub fn line_gradient(&mut self, start: Vec3, end: Vec3, start_color: Color, end_color: Color) { + self.extend_list_positions([start, end]); + self.extend_list_colors([start_color, end_color]); + } + + /// Draw a line from `start` to `start + vector`. + #[inline] + pub fn ray(&mut self, start: Vec3, vector: Vec3, color: Color) { + self.line(start, start + vector, color); + } + + /// Draw a line from `start` to `start + vector`. + #[inline] + pub fn ray_gradient( + &mut self, + start: Vec3, + vector: Vec3, + start_color: Color, + end_color: Color, + ) { + self.line_gradient(start, start + vector, start_color, end_color); + } + + #[inline] + pub fn linestrip(&mut self, positions: impl IntoIterator, color: Color) { + self.extend_strip_positions(positions.into_iter()); + self.strip_colors + .resize(self.strip_positions.len() - 1, color.as_linear_rgba_f32()); + self.strip_colors.push([f32::NAN; 4]); + } + + #[inline] + pub fn linestrip_gradient(&mut self, points: impl IntoIterator) { + let points = points.into_iter(); + + let (min, _) = points.size_hint(); + self.strip_positions.reserve(min); + self.strip_colors.reserve(min); + + for (position, color) in points { + self.strip_positions.push(position.to_array()); + self.strip_colors.push(color.as_linear_rgba_f32()); + } + + self.strip_positions.push([f32::NAN; 3]); + self.strip_colors.push([f32::NAN; 4]); + } + + /// Draw a circle at `position` with the flat side facing `normal`. + #[inline] + pub fn circle( + &mut self, + position: Vec3, + normal: Vec3, + radius: f32, + color: Color, + ) -> CircleBuilder { + CircleBuilder { + buffer: self, + position, + normal, + radius, + color, + segments: DEFAULT_CIRCLE_SEGMENTS, + } + } + + /// Draw a sphere. + #[inline] + pub fn sphere( + &mut self, + position: Vec3, + rotation: Quat, + radius: f32, + color: Color, + ) -> SphereBuilder { + SphereBuilder { + buffer: self, + position, + rotation, + radius, + color, + circle_segments: DEFAULT_CIRCLE_SEGMENTS, + } + } + + /// Draw a rectangle. + #[inline] + pub fn rect(&mut self, position: Vec3, rotation: Quat, size: Vec2, color: Color) { + let [tl, tr, br, bl] = rect_inner(size).map(|vec2| position + rotation * vec2.extend(0.)); + self.linestrip([tl, tr, br, bl, tl], color); + } + + /// Draw a box. + #[inline] + pub fn cuboid(&mut self, position: Vec3, rotation: Quat, size: Vec3, color: Color) { + let rect = rect_inner(size.truncate()); + // Front + let [tlf, trf, brf, blf] = rect.map(|vec2| position + rotation * vec2.extend(size.z / 2.)); + // Back + let [tlb, trb, brb, blb] = rect.map(|vec2| position + rotation * vec2.extend(-size.z / 2.)); + + let positions = [ + tlf, trf, trf, brf, brf, blf, blf, tlf, // Front + tlb, trb, trb, brb, brb, blb, blb, tlb, // Back + tlf, tlb, trf, trb, brf, brb, blf, blb, // Front to back + ]; + self.extend_list_positions(positions); + self.add_list_color(color, 24); + } + + /// Draw a line from `start` to `end`. + #[inline] + pub fn line_2d(&mut self, start: Vec2, end: Vec2, color: Color) { + self.line(start.extend(0.), end.extend(0.), color); + } + + /// Draw a line from `start` to `end`. + #[inline] + pub fn line_gradient_2d( + &mut self, + start: Vec2, + end: Vec2, + start_color: Color, + end_color: Color, + ) { + self.line_gradient(start.extend(0.), end.extend(0.), start_color, end_color); + } + + #[inline] + pub fn linestrip_2d(&mut self, positions: impl IntoIterator, color: Color) { + self.linestrip(positions.into_iter().map(|vec2| vec2.extend(0.)), color); + } + + #[inline] + pub fn linestrip_gradient_2d(&mut self, positions: impl IntoIterator) { + self.linestrip_gradient( + positions + .into_iter() + .map(|(vec2, color)| (vec2.extend(0.), color)), + ); + } + + /// Draw a line from `start` to `start + vector`. + #[inline] + pub fn ray_2d(&mut self, start: Vec2, vector: Vec2, color: Color) { + self.line_2d(start, start + vector, color); + } + + /// Draw a line from `start` to `start + vector`. + #[inline] + pub fn ray_gradient_2d( + &mut self, + start: Vec2, + vector: Vec2, + start_color: Color, + end_color: Color, + ) { + self.line_gradient_2d(start, start + vector, start_color, end_color); + } + + // Draw a circle. + #[inline] + pub fn circle_2d(&mut self, position: Vec2, radius: f32, color: Color) -> Circle2dBuilder { + Circle2dBuilder { + buffer: self, + position, + radius, + color, + segments: DEFAULT_CIRCLE_SEGMENTS, + } + } + + /// Draw a rectangle. + #[inline] + pub fn rect_2d(&mut self, position: Vec2, rotation: f32, size: Vec2, color: Color) { + let rotation = Mat2::from_angle(rotation); + let [tl, tr, br, bl] = rect_inner(size).map(|vec2| position + rotation * vec2); + self.linestrip_2d([tl, tr, br, bl, tl], color); + } + + #[inline] + fn extend_list_positions(&mut self, positions: impl IntoIterator) { + self.list_positions + .extend(positions.into_iter().map(|vec3| vec3.to_array())); + } + + #[inline] + fn extend_list_colors(&mut self, colors: impl IntoIterator) { + self.list_colors + .extend(colors.into_iter().map(|color| color.as_linear_rgba_f32())); + } + + #[inline] + fn add_list_color(&mut self, color: Color, count: usize) { + self.list_colors + .extend(iter::repeat(color.as_linear_rgba_f32()).take(count)); + } + + #[inline] + fn extend_strip_positions(&mut self, positions: impl IntoIterator) { + self.strip_positions.extend( + positions + .into_iter() + .map(|vec3| vec3.to_array()) + .chain(iter::once([f32::NAN; 3])), + ); + } +} + +pub struct CircleBuilder<'a> { + buffer: &'a mut GizmoBuffer, + position: Vec3, + normal: Vec3, + radius: f32, + color: Color, + segments: usize, +} + +impl<'a> CircleBuilder<'a> { + pub fn segments(mut self, segments: usize) -> Self { + self.segments = segments; + self + } +} + +impl<'a> Drop for CircleBuilder<'a> { + fn drop(&mut self) { + let rotation = Quat::from_rotation_arc(Vec3::Z, self.normal); + let positions = circle_inner(self.radius, self.segments) + .map(|vec2| (self.position + rotation * vec2.extend(0.))); + self.buffer.linestrip(positions, self.color); + } +} + +pub struct SphereBuilder<'a> { + buffer: &'a mut GizmoBuffer, + position: Vec3, + rotation: Quat, + radius: f32, + color: Color, + circle_segments: usize, +} + +impl SphereBuilder<'_> { + pub fn circle_segments(mut self, segments: usize) -> Self { + self.circle_segments = segments; + self + } +} + +impl Drop for SphereBuilder<'_> { + fn drop(&mut self) { + for axis in Vec3::AXES { + self.buffer + .circle(self.position, self.rotation * axis, self.radius, self.color) + .segments(self.circle_segments); + } + } +} + +pub struct Circle2dBuilder<'a> { + buffer: &'a mut GizmoBuffer, + position: Vec2, + radius: f32, + color: Color, + segments: usize, +} + +impl Circle2dBuilder<'_> { + pub fn segments(mut self, segments: usize) -> Self { + self.segments = segments; + self + } +} + +impl Drop for Circle2dBuilder<'_> { + fn drop(&mut self) { + let positions = circle_inner(self.radius, self.segments).map(|vec2| (vec2 + self.position)); + self.buffer.linestrip_2d(positions, self.color); + } +} + +fn circle_inner(radius: f32, segments: usize) -> impl Iterator { + (0..segments + 1).map(move |i| { + let angle = i as f32 * TAU / segments as f32; + Vec2::from(angle.sin_cos()) * radius + }) +} + +fn rect_inner(size: Vec2) -> [Vec2; 4] { + let half_size = size / 2.; + let tl = Vec2::new(-half_size.x, half_size.y); + let tr = Vec2::new(half_size.x, half_size.y); + let bl = Vec2::new(-half_size.x, -half_size.y); + let br = Vec2::new(half_size.x, -half_size.y); + [tl, tr, br, bl] +} diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs new file mode 100644 index 0000000000000..9d609c05a6db9 --- /dev/null +++ b/crates/bevy_gizmos/src/lib.rs @@ -0,0 +1,187 @@ +use std::mem; + +use bevy_app::{Last, Plugin}; +use bevy_asset::{load_internal_asset, Assets, Handle, HandleUntyped}; +use bevy_ecs::{ + prelude::{Component, DetectChanges}, + schedule::IntoSystemConfigs, + system::{Commands, Res, ResMut, Resource}, + world::{FromWorld, World}, +}; +use bevy_math::Mat4; +use bevy_reflect::TypeUuid; +use bevy_render::{ + mesh::Mesh, + render_phase::AddRenderCommand, + render_resource::{PrimitiveTopology, Shader, SpecializedMeshPipelines}, + Extract, ExtractSchedule, Render, RenderApp, RenderSet, +}; + +#[cfg(feature = "bevy_pbr")] +use bevy_pbr::MeshUniform; +#[cfg(feature = "bevy_sprite")] +use bevy_sprite::{Mesh2dHandle, Mesh2dUniform}; + +pub mod gizmos; + +#[cfg(feature = "bevy_sprite")] +mod pipeline_2d; +#[cfg(feature = "bevy_pbr")] +mod pipeline_3d; + +use crate::gizmos::GizmoStorage; + +/// The `bevy_gizmos` prelude. +pub mod prelude { + #[doc(hidden)] + pub use crate::{gizmos::Gizmos, GizmoConfig}; +} + +const LINE_SHADER_HANDLE: HandleUntyped = + HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 7414812689238026784); + +pub struct GizmoPlugin; + +impl Plugin for GizmoPlugin { + fn build(&self, app: &mut bevy_app::App) { + load_internal_asset!(app, LINE_SHADER_HANDLE, "lines.wgsl", Shader::from_wgsl); + + app.init_resource::() + .init_resource::() + .init_resource::() + .add_systems(Last, update_gizmo_meshes); + + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; + + render_app.add_systems(ExtractSchedule, extract_gizmo_data); + + #[cfg(feature = "bevy_sprite")] + { + use bevy_core_pipeline::core_2d::Transparent2d; + use pipeline_2d::*; + + render_app + .add_render_command::() + .init_resource::() + .init_resource::>() + .add_systems(Render, queue_gizmos_2d.in_set(RenderSet::Queue)); + } + + #[cfg(feature = "bevy_pbr")] + { + use bevy_core_pipeline::core_3d::Opaque3d; + use pipeline_3d::*; + + render_app + .add_render_command::() + .init_resource::() + .init_resource::>() + .add_systems(Render, queue_gizmos_3d.in_set(RenderSet::Queue)); + } + } +} + +#[derive(Resource, Clone, Copy)] +pub struct GizmoConfig { + /// Set to `false` to stop drawing gizmos. + /// + /// Defaults to `true`. + pub enabled: bool, + /// Draw gizmos on top of everything else, ignoring depth. + /// + /// This setting only affects 3D. In 2D, gizmos are always drawn on top. + /// + /// Defaults to `false`. + pub on_top: bool, +} + +impl Default for GizmoConfig { + fn default() -> Self { + Self { + enabled: true, + on_top: false, + } + } +} + +#[derive(Resource)] +struct MeshHandles { + list: Handle, + strip: Handle, +} + +impl FromWorld for MeshHandles { + fn from_world(world: &mut World) -> Self { + let mut meshes = world.resource_mut::>(); + + MeshHandles { + list: meshes.add(Mesh::new(PrimitiveTopology::LineList)), + strip: meshes.add(Mesh::new(PrimitiveTopology::LineStrip)), + } + } +} + +#[derive(Component)] +struct GizmoMesh; + +fn update_gizmo_meshes( + mut meshes: ResMut>, + handles: Res, + mut storage: ResMut, +) { + let list_mesh = meshes.get_mut(&handles.list).unwrap(); + + let positions = mem::take(&mut storage.list_positions); + list_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions); + + let colors = mem::take(&mut storage.list_colors); + list_mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, colors); + + let strip_mesh = meshes.get_mut(&handles.strip).unwrap(); + + let positions = mem::take(&mut storage.strip_positions); + strip_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions); + + let colors = mem::take(&mut storage.strip_colors); + strip_mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, colors); +} + +fn extract_gizmo_data( + mut commands: Commands, + handles: Extract>, + config: Extract>, +) { + if config.is_changed() { + commands.insert_resource(**config); + } + + if !config.enabled { + return; + } + + let transform = Mat4::IDENTITY; + let inverse_transpose_model = transform.inverse().transpose(); + commands.spawn_batch([&handles.list, &handles.strip].map(|handle| { + ( + GizmoMesh, + #[cfg(feature = "bevy_pbr")] + ( + handle.clone(), + MeshUniform { + flags: 0, + transform, + inverse_transpose_model, + }, + ), + #[cfg(feature = "bevy_sprite")] + ( + Mesh2dHandle(handle.clone()), + Mesh2dUniform { + flags: 0, + transform, + inverse_transpose_model, + }, + ), + ) + })); +} diff --git a/crates/bevy_gizmos/src/lines.wgsl b/crates/bevy_gizmos/src/lines.wgsl new file mode 100644 index 0000000000000..9fa8244f178e7 --- /dev/null +++ b/crates/bevy_gizmos/src/lines.wgsl @@ -0,0 +1,44 @@ +#ifdef GIZMO_LINES_3D + #import bevy_pbr::mesh_view_bindings +#else + #import bevy_sprite::mesh2d_view_bindings +#endif + +struct VertexInput { + @location(0) pos: vec3, + @location(1) color: vec4, +} + +struct VertexOutput { + @builtin(position) pos: vec4, + @location(0) color: vec4, +} + +struct FragmentOutput { + @builtin(frag_depth) depth: f32, + @location(0) color: vec4, +} + +@vertex +fn vertex(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + + out.pos = view.view_proj * vec4(in.pos, 1.0); + out.color = in.color; + + return out; +} + +@fragment +fn fragment(in: VertexOutput) -> FragmentOutput { + var out: FragmentOutput; + +#ifdef DEPTH_TEST + out.depth = in.pos.z; +#else + out.depth = 1.0; +#endif + + out.color = in.color; + return out; +} diff --git a/crates/bevy_gizmos/src/pipeline_2d.rs b/crates/bevy_gizmos/src/pipeline_2d.rs new file mode 100644 index 0000000000000..dd959182b5290 --- /dev/null +++ b/crates/bevy_gizmos/src/pipeline_2d.rs @@ -0,0 +1,121 @@ +use bevy_asset::Handle; +use bevy_core_pipeline::core_2d::Transparent2d; +use bevy_ecs::{ + prelude::Entity, + query::With, + system::{Query, Res, ResMut, Resource}, + world::{FromWorld, World}, +}; +use bevy_render::{ + mesh::{Mesh, MeshVertexBufferLayout}, + render_asset::RenderAssets, + render_phase::{DrawFunctions, RenderPhase, SetItemPipeline}, + render_resource::*, + texture::BevyDefault, + view::Msaa, +}; +use bevy_sprite::*; +use bevy_utils::FloatOrd; + +use crate::{GizmoMesh, LINE_SHADER_HANDLE}; + +#[derive(Resource)] +pub(crate) struct GizmoLinePipeline { + mesh_pipeline: Mesh2dPipeline, + shader: Handle, +} + +impl FromWorld for GizmoLinePipeline { + fn from_world(render_world: &mut World) -> Self { + GizmoLinePipeline { + mesh_pipeline: render_world.resource::().clone(), + shader: LINE_SHADER_HANDLE.typed(), + } + } +} + +impl SpecializedMeshPipeline for GizmoLinePipeline { + type Key = Mesh2dPipelineKey; + + fn specialize( + &self, + key: Self::Key, + layout: &MeshVertexBufferLayout, + ) -> Result { + let vertex_buffer_layout = layout.get_layout(&[ + Mesh::ATTRIBUTE_POSITION.at_shader_location(0), + Mesh::ATTRIBUTE_COLOR.at_shader_location(1), + ])?; + + Ok(RenderPipelineDescriptor { + vertex: VertexState { + shader: self.shader.clone_weak(), + entry_point: "vertex".into(), + shader_defs: vec![], + buffers: vec![vertex_buffer_layout], + }, + fragment: Some(FragmentState { + shader: self.shader.clone_weak(), + shader_defs: vec![], + entry_point: "fragment".into(), + targets: vec![Some(ColorTargetState { + format: TextureFormat::bevy_default(), + blend: Some(BlendState::ALPHA_BLENDING), + write_mask: ColorWrites::ALL, + })], + }), + layout: vec![self.mesh_pipeline.view_layout.clone()], + primitive: PrimitiveState { + topology: key.primitive_topology(), + ..Default::default() + }, + depth_stencil: None, + multisample: MultisampleState { + count: key.msaa_samples(), + mask: !0, + alpha_to_coverage_enabled: false, + }, + push_constant_ranges: vec![], + label: Some("gizmo_2d_pipeline".into()), + }) + } +} + +pub(crate) type DrawGizmoLines = ( + SetItemPipeline, + SetMesh2dViewBindGroup<0>, + SetMesh2dBindGroup<1>, + DrawMesh2d, +); + +#[allow(clippy::too_many_arguments)] +pub(crate) fn queue_gizmos_2d( + draw_functions: Res>, + pipeline: Res, + pipeline_cache: Res, + mut specialized_pipelines: ResMut>, + gpu_meshes: Res>, + msaa: Res, + mesh_handles: Query<(Entity, &Mesh2dHandle), With>, + mut views: Query<&mut RenderPhase>, +) { + let draw_function = draw_functions.read().get_id::().unwrap(); + let key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples()); + for mut phase in &mut views { + for (entity, mesh_handle) in &mesh_handles { + let Some(mesh) = gpu_meshes.get(&mesh_handle.0) else { continue; }; + + let key = key | Mesh2dPipelineKey::from_primitive_topology(mesh.primitive_topology); + let pipeline = specialized_pipelines + .specialize(&pipeline_cache, &pipeline, key, &mesh.layout) + .unwrap(); + phase.add(Transparent2d { + entity, + draw_function, + pipeline, + sort_key: FloatOrd(f32::MAX), + batch_range: None, + }); + } + } +} diff --git a/crates/bevy_gizmos/src/pipeline_3d.rs b/crates/bevy_gizmos/src/pipeline_3d.rs new file mode 100644 index 0000000000000..6064a60a566de --- /dev/null +++ b/crates/bevy_gizmos/src/pipeline_3d.rs @@ -0,0 +1,164 @@ +use bevy_asset::Handle; +use bevy_core_pipeline::core_3d::Opaque3d; +use bevy_ecs::{ + entity::Entity, + query::With, + system::{Query, Res, ResMut, Resource}, + world::{FromWorld, World}, +}; +use bevy_pbr::*; +use bevy_render::{ + mesh::Mesh, + render_resource::Shader, + view::{ExtractedView, ViewTarget}, +}; +use bevy_render::{ + mesh::MeshVertexBufferLayout, + render_asset::RenderAssets, + render_phase::{DrawFunctions, RenderPhase, SetItemPipeline}, + render_resource::*, + texture::BevyDefault, + view::Msaa, +}; + +use crate::{GizmoConfig, GizmoMesh, LINE_SHADER_HANDLE}; + +#[derive(Resource)] +pub(crate) struct GizmoPipeline { + mesh_pipeline: MeshPipeline, + shader: Handle, +} + +impl FromWorld for GizmoPipeline { + fn from_world(render_world: &mut World) -> Self { + GizmoPipeline { + mesh_pipeline: render_world.resource::().clone(), + shader: LINE_SHADER_HANDLE.typed(), + } + } +} + +impl SpecializedMeshPipeline for GizmoPipeline { + type Key = (bool, MeshPipelineKey); + + fn specialize( + &self, + (depth_test, key): Self::Key, + layout: &MeshVertexBufferLayout, + ) -> Result { + let mut shader_defs = Vec::new(); + shader_defs.push("GIZMO_LINES_3D".into()); + shader_defs.push(ShaderDefVal::Int( + "MAX_DIRECTIONAL_LIGHTS".to_string(), + MAX_DIRECTIONAL_LIGHTS as i32, + )); + shader_defs.push(ShaderDefVal::Int( + "MAX_CASCADES_PER_LIGHT".to_string(), + MAX_CASCADES_PER_LIGHT as i32, + )); + if depth_test { + shader_defs.push("DEPTH_TEST".into()); + } + + let vertex_buffer_layout = layout.get_layout(&[ + Mesh::ATTRIBUTE_POSITION.at_shader_location(0), + Mesh::ATTRIBUTE_COLOR.at_shader_location(1), + ])?; + let bind_group_layout = match key.msaa_samples() { + 1 => vec![self.mesh_pipeline.view_layout.clone()], + _ => { + shader_defs.push("MULTISAMPLED".into()); + vec![self.mesh_pipeline.view_layout_multisampled.clone()] + } + }; + + let format = if key.contains(MeshPipelineKey::HDR) { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }; + + Ok(RenderPipelineDescriptor { + vertex: VertexState { + shader: self.shader.clone_weak(), + entry_point: "vertex".into(), + shader_defs: shader_defs.clone(), + buffers: vec![vertex_buffer_layout], + }, + fragment: Some(FragmentState { + shader: self.shader.clone_weak(), + shader_defs, + entry_point: "fragment".into(), + targets: vec![Some(ColorTargetState { + format, + blend: None, + write_mask: ColorWrites::ALL, + })], + }), + layout: bind_group_layout, + primitive: PrimitiveState { + topology: key.primitive_topology(), + ..Default::default() + }, + depth_stencil: Some(DepthStencilState { + format: TextureFormat::Depth32Float, + depth_write_enabled: true, + depth_compare: CompareFunction::Greater, + stencil: Default::default(), + bias: Default::default(), + }), + multisample: MultisampleState { + count: key.msaa_samples(), + mask: !0, + alpha_to_coverage_enabled: false, + }, + push_constant_ranges: vec![], + label: Some("gizmo_3d_pipeline".into()), + }) + } +} + +pub(crate) type DrawGizmoLines = ( + SetItemPipeline, + SetMeshViewBindGroup<0>, + SetMeshBindGroup<1>, + DrawMesh, +); + +#[allow(clippy::too_many_arguments)] +pub(crate) fn queue_gizmos_3d( + draw_functions: Res>, + pipeline: Res, + mut pipelines: ResMut>, + pipeline_cache: Res, + render_meshes: Res>, + msaa: Res, + mesh_handles: Query<(Entity, &Handle), With>, + config: Res, + mut views: Query<(&ExtractedView, &mut RenderPhase)>, +) { + let draw_function = draw_functions.read().get_id::().unwrap(); + let key = MeshPipelineKey::from_msaa_samples(msaa.samples()); + for (view, mut phase) in &mut views { + let key = key | MeshPipelineKey::from_hdr(view.hdr); + for (entity, mesh_handle) in &mesh_handles { + if let Some(mesh) = render_meshes.get(mesh_handle) { + let key = key | MeshPipelineKey::from_primitive_topology(mesh.primitive_topology); + let pipeline = pipelines + .specialize( + &pipeline_cache, + &pipeline, + (!config.on_top, key), + &mesh.layout, + ) + .unwrap(); + phase.add(Opaque3d { + entity, + pipeline, + draw_function, + distance: 0., + }); + } + } + } +} diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 1eec1d8c5eac0..5af93188b37b3 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -71,11 +71,14 @@ subpixel_glyph_atlas = ["bevy_text/subpixel_glyph_atlas"] webgl = ["bevy_core_pipeline?/webgl", "bevy_pbr?/webgl", "bevy_render?/webgl"] # enable systems that allow for automated testing on CI -bevy_ci_testing = ["bevy_app/bevy_ci_testing", "bevy_render/ci_limits"] +bevy_ci_testing = ["bevy_app/bevy_ci_testing", "bevy_render?/ci_limits"] # Enable animation support, and glTF animation loading animation = ["bevy_animation", "bevy_gltf?/bevy_animation"] +bevy_sprite = ["dep:bevy_sprite", "bevy_gizmos?/bevy_sprite"] +bevy_pbr = ["dep:bevy_pbr", "bevy_gizmos?/bevy_pbr"] + # Used to disable code that is unsupported when Bevy is dynamically linked dynamic_linking = ["bevy_diagnostic/dynamic_linking"] @@ -122,3 +125,4 @@ bevy_text = { path = "../bevy_text", optional = true, version = "0.11.0-dev" } bevy_ui = { path = "../bevy_ui", optional = true, version = "0.11.0-dev" } bevy_winit = { path = "../bevy_winit", optional = true, version = "0.11.0-dev" } bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.11.0-dev" } +bevy_gizmos = { path = "../bevy_gizmos", optional = true, version = "0.11.0-dev", default-features = false } diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index 2c3e637de8e0f..fe3be50bf9536 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -135,6 +135,11 @@ impl PluginGroup for DefaultPlugins { group = group.add(bevy_animation::AnimationPlugin::default()); } + #[cfg(feature = "bevy_gizmos")] + { + group = group.add(bevy_gizmos::GizmoPlugin); + } + group } } diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index b2211a5e3f751..4e1c6bcb179d9 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -172,6 +172,12 @@ pub mod winit { pub use bevy_winit::*; } +#[cfg(feature = "bevy_gizmos")] +pub mod gizmos { + //! Immediate mode debug drawing. + pub use bevy_gizmos::*; +} + #[cfg(feature = "bevy_dynamic_plugin")] pub mod dynamic_plugin { //! Dynamic linking of plugins diff --git a/crates/bevy_internal/src/prelude.rs b/crates/bevy_internal/src/prelude.rs index cc3614544d0bb..f9243382a11ef 100644 --- a/crates/bevy_internal/src/prelude.rs +++ b/crates/bevy_internal/src/prelude.rs @@ -51,6 +51,10 @@ pub use crate::ui::prelude::*; #[cfg(feature = "bevy_dynamic_plugin")] pub use crate::dynamic_plugin::*; +#[doc(hidden)] +#[cfg(feature = "bevy_gizmos")] +pub use crate::gizmos::prelude::*; + #[doc(hidden)] #[cfg(feature = "bevy_gilrs")] pub use crate::gilrs::*; diff --git a/docs/cargo_features.md b/docs/cargo_features.md index 7bc715695a853..aac27b3185381 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -18,6 +18,7 @@ The default feature set enables most of the expected features of a game engine, |bevy_audio|Provides audio functionality| |bevy_core_pipeline|Provides cameras and other basic render pipeline features| |bevy_gilrs|Adds gamepad support| +|bevy_gizmos|Adds support for rendering gizmos| |bevy_gltf|[glTF](https://www.khronos.org/gltf/) support| |bevy_pbr|Adds PBR rendering| |bevy_render|Provides rendering functionality| diff --git a/examples/2d/2d_gizmos.rs b/examples/2d/2d_gizmos.rs new file mode 100644 index 0000000000000..00d2aaae6f8d3 --- /dev/null +++ b/examples/2d/2d_gizmos.rs @@ -0,0 +1,41 @@ +//! This example demonstrates Bevy's immediate mode drawing API intended for visual debugging. + +use bevy::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems(Update, system) + .run(); +} + +fn setup(mut commands: Commands) { + commands.spawn(Camera2dBundle::default()); +} + +fn system(mut gizmos: Gizmos, time: Res