diff --git a/crates/bevy_gizmos/Cargo.toml b/crates/bevy_gizmos/Cargo.toml index f44a2d1ec9e3a..aaa4f71355ad1 100644 --- a/crates/bevy_gizmos/Cargo.toml +++ b/crates/bevy_gizmos/Cargo.toml @@ -8,6 +8,9 @@ repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] +[features] +webgl = [] + [dependencies] # Bevy bevy_pbr = { path = "../bevy_pbr", version = "0.11.0-dev", optional = true } diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs index 482c62f24af91..b288d72f747dc 100644 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/src/lib.rs @@ -19,36 +19,40 @@ use std::mem; use bevy_app::{Last, Plugin, Update}; -use bevy_asset::{load_internal_asset, Assets, Handle, HandleUntyped}; +use bevy_asset::{load_internal_asset, AddAsset, Assets, Handle, HandleUntyped}; +use bevy_core::cast_slice; use bevy_ecs::{ change_detection::DetectChanges, component::Component, entity::Entity, - query::Without, + query::{ROQueryItem, Without}, reflect::ReflectComponent, schedule::IntoSystemConfigs, - system::{Commands, Query, Res, ResMut, Resource}, - world::{FromWorld, World}, + system::{ + lifetimeless::{Read, SRes}, + Commands, Query, Res, ResMut, Resource, SystemParamItem, + }, }; -use bevy_math::Mat4; use bevy_reflect::{ - std_traits::ReflectDefault, FromReflect, Reflect, ReflectFromReflect, TypeUuid, + std_traits::ReflectDefault, FromReflect, Reflect, ReflectFromReflect, TypePath, TypeUuid, }; use bevy_render::{ color::Color, - mesh::Mesh, + extract_component::{ComponentUniforms, DynamicUniformIndex, UniformComponentPlugin}, primitives::Aabb, - render_phase::AddRenderCommand, - render_resource::{PrimitiveTopology, Shader, SpecializedMeshPipelines}, + render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets}, + render_phase::{PhaseItem, RenderCommand, RenderCommandResult, TrackedRenderPass}, + render_resource::{ + BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutDescriptor, + BindGroupLayoutEntry, BindingType, Buffer, BufferBindingType, BufferInitDescriptor, + BufferUsages, Shader, ShaderStages, ShaderType, VertexAttribute, VertexBufferLayout, + VertexFormat, VertexStepMode, + }, + renderer::RenderDevice, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_transform::components::{GlobalTransform, Transform}; -#[cfg(feature = "bevy_pbr")] -use bevy_pbr::MeshUniform; -#[cfg(feature = "bevy_sprite")] -use bevy_sprite::{Mesh2dHandle, Mesh2dUniform}; - pub mod gizmos; #[cfg(feature = "bevy_sprite")] @@ -74,7 +78,10 @@ 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::() + app.add_plugin(UniformComponentPlugin::::default()) + .add_asset::() + .add_plugin(RenderAssetPlugin::::default()) + .init_resource::() .init_resource::() .init_resource::() .add_systems(Last, update_gizmo_meshes) @@ -88,47 +95,35 @@ impl Plugin for GizmoPlugin { let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; - render_app.add_systems(ExtractSchedule, extract_gizmo_data); + render_app + .add_systems(ExtractSchedule, extract_gizmo_data) + .add_systems(Render, queue_line_gizmo_bind_group.in_set(RenderSet::Queue)); #[cfg(feature = "bevy_sprite")] - { - use bevy_core_pipeline::core_2d::Transparent2d; - use pipeline_2d::*; - - render_app - .add_render_command::() - .init_resource::>() - .add_systems(Render, queue_gizmos_2d.in_set(RenderSet::Queue)); - } - + app.add_plugin(pipeline_2d::LineGizmo2dPlugin); #[cfg(feature = "bevy_pbr")] - { - use bevy_core_pipeline::core_3d::Opaque3d; - use pipeline_3d::*; - - render_app - .add_render_command::() - .init_resource::>() - .add_systems(Render, queue_gizmos_3d.in_set(RenderSet::Queue)); - } + app.add_plugin(pipeline_3d::LineGizmo3dPlugin); } fn finish(&self, app: &mut bevy_app::App) { let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; - #[cfg(feature = "bevy_sprite")] - { - use pipeline_2d::*; - - render_app.init_resource::(); - } - - #[cfg(feature = "bevy_pbr")] - { - use pipeline_3d::*; - - render_app.init_resource::(); - } + let render_device = render_app.world.resource::(); + let layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { + entries: &[BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::VERTEX, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: Some(LineGizmoUniform::min_size()), + }, + count: None, + }], + label: Some("LineGizmoUniform layout"), + }); + + render_app.insert_resource(LineGizmoUniformBindgroupLayout { layout }); } } @@ -139,12 +134,29 @@ pub struct GizmoConfig { /// /// Defaults to `true`. pub enabled: bool, - /// Draw gizmos on top of everything else, ignoring depth. + /// Line width specified in pixels. + /// + /// If `line_perspective` is `true` then this is the size in pixels at the camera's near plane. /// - /// This setting only affects 3D. In 2D, gizmos are always drawn on top. + /// Defaults to `2.0`. + pub line_width: f32, + /// Apply perspective to gizmo lines. + /// + /// This setting only affects 3D, non-orhographic cameras. /// /// Defaults to `false`. - pub on_top: bool, + pub line_perspective: bool, + /// How closer to the camera than real geometry the line should be. + /// + /// Value between -1 and 1 (inclusive). + /// * 0 means that there is no change to the line position when rendering + /// * 1 means it is furthest away from camera as possible + /// * -1 means that it will always render in front of other things. + /// + /// This is typically useful if you are drawing wireframes on top of polygons + /// and your wireframe is z-fighting (flickering on/off) with your main model. + /// You would set this value to a negative number close to 0.0. + pub depth_bias: f32, /// Configuration for the [`AabbGizmo`]. pub aabb: AabbGizmoConfig, } @@ -153,7 +165,9 @@ impl Default for GizmoConfig { fn default() -> Self { Self { enabled: true, - on_top: false, + line_width: 2., + line_perspective: false, + depth_bias: 0., aabb: Default::default(), } } @@ -227,77 +241,59 @@ fn aabb_transform(aabb: Aabb, transform: GlobalTransform) -> GlobalTransform { ) } -#[derive(Resource)] -struct MeshHandles { - list: Option>, - strip: Option>, -} - -impl FromWorld for MeshHandles { - fn from_world(_world: &mut World) -> Self { - MeshHandles { - list: None, - strip: None, - } - } +#[derive(Resource, Default)] +struct LineGizmoHandles { + list: Option>, + strip: Option>, } -#[derive(Component)] -struct GizmoMesh; - fn update_gizmo_meshes( - mut meshes: ResMut>, - mut handles: ResMut, + mut line_gizmos: ResMut>, + mut handles: ResMut, mut storage: ResMut, ) { if storage.list_positions.is_empty() { handles.list = None; } else if let Some(handle) = handles.list.as_ref() { - let list_mesh = meshes.get_mut(handle).unwrap(); - - let positions = mem::take(&mut storage.list_positions); - list_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions); + let list = line_gizmos.get_mut(handle).unwrap(); - let colors = mem::take(&mut storage.list_colors); - list_mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, colors); + list.positions = mem::take(&mut storage.list_positions); + list.colors = mem::take(&mut storage.list_colors); } else { - let mut list_mesh = Mesh::new(PrimitiveTopology::LineList); + let mut list = LineGizmo { + strip: false, + ..Default::default() + }; - let positions = mem::take(&mut storage.list_positions); - list_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions); + list.positions = mem::take(&mut storage.list_positions); + list.colors = mem::take(&mut storage.list_colors); - let colors = mem::take(&mut storage.list_colors); - list_mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, colors); - - handles.list = Some(meshes.add(list_mesh)); + handles.list = Some(line_gizmos.add(list)); } if storage.strip_positions.is_empty() { handles.strip = None; } else if let Some(handle) = handles.strip.as_ref() { - let strip_mesh = meshes.get_mut(handle).unwrap(); - - let positions = mem::take(&mut storage.strip_positions); - strip_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions); + let strip = line_gizmos.get_mut(handle).unwrap(); - let colors = mem::take(&mut storage.strip_colors); - strip_mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, colors); + strip.positions = mem::take(&mut storage.strip_positions); + strip.colors = mem::take(&mut storage.strip_colors); } else { - let mut strip_mesh = Mesh::new(PrimitiveTopology::LineStrip); + let mut strip = LineGizmo { + strip: true, + ..Default::default() + }; - let positions = mem::take(&mut storage.strip_positions); - strip_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions); + strip.positions = mem::take(&mut storage.strip_positions); + strip.colors = mem::take(&mut storage.strip_colors); - let colors = mem::take(&mut storage.strip_colors); - strip_mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, colors); - - handles.strip = Some(meshes.add(strip_mesh)); + handles.strip = Some(line_gizmos.add(strip)); } } fn extract_gizmo_data( mut commands: Commands, - handles: Extract>, + handles: Extract>, config: Extract>, ) { if config.is_changed() { @@ -308,35 +304,206 @@ fn extract_gizmo_data( return; } - let transform = Mat4::IDENTITY; - let inverse_transpose_model = transform.inverse().transpose(); - commands.spawn_batch( - [handles.list.clone(), handles.strip.clone()] - .into_iter() - .flatten() - .map(move |handle| { - ( - GizmoMesh, - #[cfg(feature = "bevy_pbr")] - ( - handle.clone_weak(), - MeshUniform { - flags: 0, - transform, - previous_transform: transform, - inverse_transpose_model, - }, - ), - #[cfg(feature = "bevy_sprite")] - ( - Mesh2dHandle(handle), - Mesh2dUniform { - flags: 0, - transform, - inverse_transpose_model, - }, - ), - ) + for handle in [&handles.list, &handles.strip].into_iter().flatten() { + commands.spawn(( + LineGizmoUniform { + line_width: config.line_width, + depth_bias: config.depth_bias, + #[cfg(feature = "webgl")] + _padding: Default::default(), + }, + handle.clone_weak(), + )); + } +} + +#[derive(Component, ShaderType, Clone, Copy)] +struct LineGizmoUniform { + line_width: f32, + depth_bias: f32, + /// WebGL2 structs must be 16 byte aligned. + #[cfg(feature = "webgl")] + _padding: bevy_math::Vec2, +} + +#[derive(Debug, Default, Clone, TypeUuid, TypePath)] +#[uuid = "02b99cbf-bb26-4713-829a-aee8e08dedc0"] +struct LineGizmo { + positions: Vec<[f32; 3]>, + colors: Vec<[f32; 4]>, + /// Whether this gizmo's topology is a line-strip or line-list + strip: bool, +} + +#[derive(Debug, Clone)] +struct GpuLineGizmo { + position_buffer: Buffer, + color_buffer: Buffer, + vertex_count: u32, + strip: bool, +} + +impl RenderAsset for LineGizmo { + type ExtractedAsset = LineGizmo; + + type PreparedAsset = GpuLineGizmo; + + type Param = SRes; + + fn extract_asset(&self) -> Self::ExtractedAsset { + self.clone() + } + + fn prepare_asset( + line_gizmo: Self::ExtractedAsset, + render_device: &mut SystemParamItem, + ) -> Result> { + let position_buffer_data = cast_slice(&line_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(&line_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: line_gizmo.positions.len() as u32, + strip: line_gizmo.strip, + }) + } +} + +#[derive(Resource)] +struct LineGizmoUniformBindgroupLayout { + layout: BindGroupLayout, +} + +#[derive(Resource)] +struct LineGizmoUniformBindgroup { + bindgroup: BindGroup, +} + +fn queue_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(&BindGroupDescriptor { + entries: &[BindGroupEntry { + binding: 0, + resource: binding, + }], + label: Some("LineGizmoUniform bindgroup"), + layout: &line_gizmo_uniform_layout.layout, }), - ); + }); + } +} + +struct SetLineGizmoBindGroup; +impl RenderCommand

for SetLineGizmoBindGroup { + type ViewWorldQuery = (); + type ItemWorldQuery = Read>; + type Param = SRes; + + #[inline] + fn render<'w>( + _item: &P, + _view: ROQueryItem<'w, Self::ViewWorldQuery>, + uniform_index: ROQueryItem<'w, Self::ItemWorldQuery>, + bind_group: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + pass.set_bind_group( + I, + &bind_group.into_inner().bindgroup, + &[uniform_index.index()], + ); + RenderCommandResult::Success + } +} + +struct DrawLineGizmo; +impl RenderCommand

for DrawLineGizmo { + type ViewWorldQuery = (); + type ItemWorldQuery = Read>; + type Param = SRes>; + + #[inline] + fn render<'w>( + _item: &P, + _view: ROQueryItem<'w, Self::ViewWorldQuery>, + handle: ROQueryItem<'w, Self::ItemWorldQuery>, + line_gizmos: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let Some(line_gizmo) = line_gizmos.into_inner().get(handle) else { + return RenderCommandResult::Failure; + }; + + pass.set_vertex_buffer(0, line_gizmo.position_buffer.slice(..)); + pass.set_vertex_buffer(1, line_gizmo.color_buffer.slice(..)); + + let instances = if line_gizmo.strip { + u32::max(line_gizmo.vertex_count, 1) - 1 + } else { + line_gizmo.vertex_count / 2 + }; + + pass.draw(0..6, 0..instances); + + RenderCommandResult::Success + } +} + +fn line_gizmo_vertex_buffer_layouts(strip: bool) -> Vec { + let stride_multiplier = if strip { 1 } else { 2 }; + use VertexFormat::*; + vec![ + // Positions + VertexBufferLayout { + array_stride: Float32x3.size() * stride_multiplier, + step_mode: VertexStepMode::Instance, + attributes: vec![ + VertexAttribute { + format: Float32x3, + offset: 0, + shader_location: 0, + }, + VertexAttribute { + format: Float32x3, + offset: Float32x3.size(), + shader_location: 1, + }, + ], + }, + // Colors + VertexBufferLayout { + array_stride: Float32x4.size() * stride_multiplier, + step_mode: VertexStepMode::Instance, + attributes: vec![ + VertexAttribute { + format: Float32x4, + offset: 0, + shader_location: 2, + }, + VertexAttribute { + format: Float32x4, + offset: Float32x4.size(), + shader_location: 3, + }, + ], + }, + ] } diff --git a/crates/bevy_gizmos/src/lines.wgsl b/crates/bevy_gizmos/src/lines.wgsl index b616476796e06..85a0d11da0e89 100644 --- a/crates/bevy_gizmos/src/lines.wgsl +++ b/crates/bevy_gizmos/src/lines.wgsl @@ -1,48 +1,105 @@ -#ifdef GIZMO_LINES_3D +#ifdef GIZMO_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 LineGizmoUniform { + line_width: f32, + depth_bias: f32, +#ifdef SIXTEEN_BYTE_ALIGNMENT + // WebGL2 structs must be 16 byte aligned. + _padding: vec2, +#endif } +@group(1) @binding(0) +var line_gizmo: LineGizmoUniform; + +struct VertexInput { + @location(0) position_a: vec3, + @location(1) position_b: vec3, + @location(2) color_a: vec4, + @location(3) color_b: vec4, + @builtin(vertex_index) index: u32, +}; + struct VertexOutput { - @builtin(position) pos: vec4, + @builtin(position) clip_position: vec4, @location(0) color: vec4, -} +}; -struct FragmentOutput { -#ifdef GIZMO_LINES_3D - @builtin(frag_depth) depth: f32, +@vertex +fn vertex(vertex: VertexInput) -> VertexOutput { + var positions = array, 6>( + vec3(0., -0.5, 0.), + vec3(0., -0.5, 1.), + vec3(0., 0.5, 1.), + vec3(0., -0.5, 0.), + vec3(0., 0.5, 1.), + vec3(0., 0.5, 0.) + ); + let position = positions[vertex.index]; + + // algorithm based on https://wwwtyro.net/2019/11/18/instanced-lines.html + let clip_a = view.view_proj * vec4(vertex.position_a, 1.); + let clip_b = view.view_proj * vec4(vertex.position_b, 1.); + let clip = mix(clip_a, clip_b, position.z); + + let resolution = view.viewport.zw; + let screen_a = resolution * (0.5 * clip_a.xy / clip_a.w + 0.5); + let screen_b = resolution * (0.5 * clip_b.xy / clip_b.w + 0.5); + + let x_basis = normalize(screen_a - screen_b); + let y_basis = vec2(-x_basis.y, x_basis.x); + + var color = mix(vertex.color_a, vertex.color_b, position.z); + + var line_width = line_gizmo.line_width; + var alpha = 1.; + +#ifdef PERSPECTIVE + line_width /= clip.w; #endif - @location(0) color: vec4, -} -@vertex -fn vertex(in: VertexInput) -> VertexOutput { - var out: VertexOutput; + // Line thinness fade from https://acegikmo.com/shapes/docs/#anti-aliasing + if line_width < 1. { + color.a *= line_width; + line_width = 1.; + } + + let offset = line_width * (position.x * x_basis + position.y * y_basis); + let screen = mix(screen_a, screen_b, position.z) + offset; - out.pos = view.view_proj * vec4(in.pos, 1.0); - out.color = in.color; + var depth: f32; + if line_gizmo.depth_bias >= 0. { + depth = clip.z * (1. - line_gizmo.depth_bias); + } else { + let epsilon = 4.88e-04; + // depth * (clip.w / depth)^-depth_bias. So that when -depth_bias is 1.0, this is equal to clip.w + // and when equal to 0.0, it is exactly equal to depth. + // the epsilon is here to prevent the depth from exceeding clip.w when -depth_bias = 1.0 + // clip.w represents the near plane in homogenous 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 convinient for them + depth = clip.z * exp2(-line_gizmo.depth_bias * log2(clip.w / clip.z - epsilon)); + } - return out; + var clip_position = vec4(clip.w * ((2. * screen) / resolution - 1.), depth, clip.w); + + return VertexOutput(clip_position, color); } -@fragment -fn fragment(in: VertexOutput) -> FragmentOutput { - var out: FragmentOutput; +struct FragmentInput { + @location(0) color: vec4, +}; -#ifdef GIZMO_LINES_3D -#ifdef DEPTH_TEST - out.depth = in.pos.z; -#else - out.depth = 1.0; -#endif -#endif +struct FragmentOutput { + @location(0) color: vec4, +}; - out.color = in.color; - return out; +@fragment +fn fragment(in: FragmentInput) -> FragmentOutput { + return FragmentOutput(in.color); } diff --git a/crates/bevy_gizmos/src/pipeline_2d.rs b/crates/bevy_gizmos/src/pipeline_2d.rs index f3ab5be25cba8..5e903b8e305da 100644 --- a/crates/bevy_gizmos/src/pipeline_2d.rs +++ b/crates/bevy_gizmos/src/pipeline_2d.rs @@ -1,68 +1,101 @@ +use crate::{ + line_gizmo_vertex_buffer_layouts, DrawLineGizmo, LineGizmo, LineGizmoUniformBindgroupLayout, + SetLineGizmoBindGroup, LINE_SHADER_HANDLE, +}; +use bevy_app::{App, Plugin}; use bevy_asset::Handle; use bevy_core_pipeline::core_2d::Transparent2d; + use bevy_ecs::{ prelude::Entity, - query::With, + schedule::IntoSystemConfigs, system::{Query, Res, ResMut, Resource}, world::{FromWorld, World}, }; use bevy_render::{ - mesh::{Mesh, MeshVertexBufferLayout}, render_asset::RenderAssets, - render_phase::{DrawFunctions, RenderPhase, SetItemPipeline}, + render_phase::{AddRenderCommand, DrawFunctions, RenderPhase, SetItemPipeline}, render_resource::*, texture::BevyDefault, view::{ExtractedView, Msaa, ViewTarget}, + Render, RenderApp, RenderSet, }; -use bevy_sprite::*; +use bevy_sprite::{Mesh2dPipeline, Mesh2dPipelineKey, SetMesh2dViewBindGroup}; use bevy_utils::FloatOrd; -use crate::{GizmoMesh, LINE_SHADER_HANDLE}; +pub struct LineGizmo2dPlugin; + +impl Plugin for LineGizmo2dPlugin { + fn build(&self, app: &mut App) { + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return }; + + render_app + .add_render_command::() + .init_resource::>() + .add_systems(Render, queue_line_gizmos_2d.in_set(RenderSet::Queue)); + } + + fn finish(&self, app: &mut App) { + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return }; + + render_app.init_resource::(); + } +} -#[derive(Resource)] -pub(crate) struct GizmoLinePipeline { +#[derive(Clone, Resource)] +struct LineGizmoPipeline { mesh_pipeline: Mesh2dPipeline, - shader: Handle, + uniform_layout: BindGroupLayout, } -impl FromWorld for GizmoLinePipeline { +impl FromWorld for LineGizmoPipeline { fn from_world(render_world: &mut World) -> Self { - GizmoLinePipeline { + LineGizmoPipeline { mesh_pipeline: render_world.resource::().clone(), - shader: LINE_SHADER_HANDLE.typed(), + uniform_layout: render_world + .resource::() + .layout + .clone(), } } } -impl SpecializedMeshPipeline for GizmoLinePipeline { - type Key = Mesh2dPipelineKey; +#[derive(PartialEq, Eq, Hash, Clone)] +struct LineGizmoPipelineKey { + mesh_key: Mesh2dPipelineKey, + strip: bool, +} - 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), - ])?; +impl SpecializedRenderPipeline for LineGizmoPipeline { + type Key = LineGizmoPipelineKey; - let format = if key.contains(Mesh2dPipelineKey::HDR) { + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + let format = if key.mesh_key.contains(Mesh2dPipelineKey::HDR) { ViewTarget::TEXTURE_FORMAT_HDR } else { TextureFormat::bevy_default() }; - Ok(RenderPipelineDescriptor { + 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: self.shader.clone_weak(), + shader: LINE_SHADER_HANDLE.typed(), entry_point: "vertex".into(), - shader_defs: vec![], - buffers: vec![vertex_buffer_layout], + shader_defs: shader_defs.clone(), + buffers: line_gizmo_vertex_buffer_layouts(key.strip), }, fragment: Some(FragmentState { - shader: self.shader.clone_weak(), - shader_defs: vec![], + shader: LINE_SHADER_HANDLE.typed(), + shader_defs, entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format, @@ -70,57 +103,61 @@ impl SpecializedMeshPipeline for GizmoLinePipeline { write_mask: ColorWrites::ALL, })], }), - layout: vec![self.mesh_pipeline.view_layout.clone()], - primitive: PrimitiveState { - topology: key.primitive_topology(), - ..Default::default() - }, + layout, + primitive: PrimitiveState::default(), depth_stencil: None, multisample: MultisampleState { - count: key.msaa_samples(), + count: key.mesh_key.msaa_samples(), mask: !0, alpha_to_coverage_enabled: false, }, + label: Some("LineGizmo Pipeline 2D".into()), push_constant_ranges: vec![], - label: Some("gizmo_2d_pipeline".into()), - }) + } } } -pub(crate) type DrawGizmoLines = ( +type DrawLineGizmo2d = ( SetItemPipeline, SetMesh2dViewBindGroup<0>, - SetMesh2dBindGroup<1>, - DrawMesh2d, + SetLineGizmoBindGroup<1>, + DrawLineGizmo, ); #[allow(clippy::too_many_arguments)] -pub(crate) fn queue_gizmos_2d( +fn queue_line_gizmos_2d( draw_functions: Res>, - pipeline: Res, + pipeline: Res, + mut pipelines: ResMut>, pipeline_cache: Res, - mut specialized_pipelines: ResMut>, - gpu_meshes: Res>, msaa: Res, - mesh_handles: Query<(Entity, &Mesh2dHandle), With>, + line_gizmos: Query<(Entity, &Handle)>, + line_gizmo_assets: Res>, mut views: Query<(&ExtractedView, &mut RenderPhase)>, ) { - let draw_function = draw_functions.read().get_id::().unwrap(); - let key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples()); - for (view, mut phase) in &mut views { - let key = key | Mesh2dPipelineKey::from_hdr(view.hdr); - 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 { + let draw_function = draw_functions.read().get_id::().unwrap(); + + for (view, mut transparent_phase) in &mut views { + let mesh_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples()) + | Mesh2dPipelineKey::from_hdr(view.hdr); + + for (entity, handle) in &line_gizmos { + let Some(line_gizmo) = line_gizmo_assets.get(handle) else { continue }; + + let pipeline = pipelines.specialize( + &pipeline_cache, + &pipeline, + LineGizmoPipelineKey { + mesh_key, + strip: line_gizmo.strip, + }, + ); + + transparent_phase.add(Transparent2d { entity, draw_function, pipeline, - sort_key: FloatOrd(f32::MAX), + sort_key: FloatOrd(0.), batch_range: None, }); } diff --git a/crates/bevy_gizmos/src/pipeline_3d.rs b/crates/bevy_gizmos/src/pipeline_3d.rs index 6064a60a566de..bfd1ed1a4cd2f 100644 --- a/crates/bevy_gizmos/src/pipeline_3d.rs +++ b/crates/bevy_gizmos/src/pipeline_3d.rs @@ -1,53 +1,83 @@ +use crate::{ + line_gizmo_vertex_buffer_layouts, DrawLineGizmo, GizmoConfig, LineGizmo, + LineGizmoUniformBindgroupLayout, SetLineGizmoBindGroup, LINE_SHADER_HANDLE, +}; +use bevy_app::{App, Plugin}; use bevy_asset::Handle; -use bevy_core_pipeline::core_3d::Opaque3d; +use bevy_core_pipeline::core_3d::Transparent3d; + use bevy_ecs::{ - entity::Entity, - query::With, + prelude::Entity, + schedule::IntoSystemConfigs, system::{Query, Res, ResMut, Resource}, world::{FromWorld, World}, }; -use bevy_pbr::*; -use bevy_render::{ - mesh::Mesh, - render_resource::Shader, - view::{ExtractedView, ViewTarget}, +use bevy_pbr::{ + MeshPipeline, MeshPipelineKey, SetMeshViewBindGroup, MAX_CASCADES_PER_LIGHT, + MAX_DIRECTIONAL_LIGHTS, }; use bevy_render::{ - mesh::MeshVertexBufferLayout, render_asset::RenderAssets, - render_phase::{DrawFunctions, RenderPhase, SetItemPipeline}, + render_phase::{AddRenderCommand, DrawFunctions, RenderPhase, SetItemPipeline}, render_resource::*, texture::BevyDefault, - view::Msaa, + view::{ExtractedView, Msaa, ViewTarget}, + Render, RenderApp, RenderSet, }; -use crate::{GizmoConfig, GizmoMesh, LINE_SHADER_HANDLE}; +pub struct LineGizmo3dPlugin; +impl Plugin for LineGizmo3dPlugin { + fn build(&self, app: &mut App) { + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return }; -#[derive(Resource)] -pub(crate) struct GizmoPipeline { + render_app + .add_render_command::() + .init_resource::>() + .add_systems(Render, queue_line_gizmos_3d.in_set(RenderSet::Queue)); + } + + fn finish(&self, app: &mut App) { + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return }; + + render_app.init_resource::(); + } +} + +#[derive(Clone, Resource)] +struct LineGizmoPipeline { mesh_pipeline: MeshPipeline, - shader: Handle, + uniform_layout: BindGroupLayout, } -impl FromWorld for GizmoPipeline { +impl FromWorld for LineGizmoPipeline { fn from_world(render_world: &mut World) -> Self { - GizmoPipeline { + LineGizmoPipeline { mesh_pipeline: render_world.resource::().clone(), - shader: LINE_SHADER_HANDLE.typed(), + uniform_layout: render_world + .resource::() + .layout + .clone(), } } } -impl SpecializedMeshPipeline for GizmoPipeline { - type Key = (bool, MeshPipelineKey); +#[derive(PartialEq, Eq, Hash, Clone)] +struct LineGizmoPipelineKey { + mesh_key: MeshPipelineKey, + strip: bool, + perspective: bool, +} + +impl SpecializedRenderPipeline for LineGizmoPipeline { + type Key = LineGizmoPipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + let mut shader_defs = vec![ + "GIZMO_3D".into(), + #[cfg(feature = "webgl")] + "SIXTEEN_BYTE_ALIGNMENT".into(), + ]; - 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, @@ -56,109 +86,106 @@ impl SpecializedMeshPipeline for GizmoPipeline { "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()] - } - }; + if key.perspective { + shader_defs.push("PERSPECTIVE".into()); + } - let format = if key.contains(MeshPipelineKey::HDR) { + let format = if key.mesh_key.contains(MeshPipelineKey::HDR) { ViewTarget::TEXTURE_FORMAT_HDR } else { TextureFormat::bevy_default() }; - Ok(RenderPipelineDescriptor { + let view_layout = if key.mesh_key.msaa_samples() == 1 { + self.mesh_pipeline.view_layout.clone() + } else { + self.mesh_pipeline.view_layout_multisampled.clone() + }; + + let layout = vec![view_layout, self.uniform_layout.clone()]; + + RenderPipelineDescriptor { vertex: VertexState { - shader: self.shader.clone_weak(), + shader: LINE_SHADER_HANDLE.typed(), entry_point: "vertex".into(), shader_defs: shader_defs.clone(), - buffers: vec![vertex_buffer_layout], + buffers: line_gizmo_vertex_buffer_layouts(key.strip), }, fragment: Some(FragmentState { - shader: self.shader.clone_weak(), + shader: LINE_SHADER_HANDLE.typed(), shader_defs, entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format, - blend: None, + blend: Some(BlendState::ALPHA_BLENDING), write_mask: ColorWrites::ALL, })], }), - layout: bind_group_layout, - primitive: PrimitiveState { - topology: key.primitive_topology(), - ..Default::default() - }, + layout, + primitive: PrimitiveState::default(), depth_stencil: Some(DepthStencilState { format: TextureFormat::Depth32Float, depth_write_enabled: true, depth_compare: CompareFunction::Greater, - stencil: Default::default(), - bias: Default::default(), + stencil: StencilState::default(), + bias: DepthBiasState::default(), }), multisample: MultisampleState { - count: key.msaa_samples(), + count: key.mesh_key.msaa_samples(), mask: !0, alpha_to_coverage_enabled: false, }, + label: Some("LineGizmo Pipeline".into()), push_constant_ranges: vec![], - label: Some("gizmo_3d_pipeline".into()), - }) + } } } -pub(crate) type DrawGizmoLines = ( +type DrawLineGizmo3d = ( SetItemPipeline, SetMeshViewBindGroup<0>, - SetMeshBindGroup<1>, - DrawMesh, + SetLineGizmoBindGroup<1>, + DrawLineGizmo, ); #[allow(clippy::too_many_arguments)] -pub(crate) fn queue_gizmos_3d( - draw_functions: Res>, - pipeline: Res, - mut pipelines: ResMut>, +fn queue_line_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)>, + line_gizmos: Query<(Entity, &Handle)>, + line_gizmo_assets: 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., - }); - } + let draw_function = draw_functions.read().get_id::().unwrap(); + + for (view, mut transparent_phase) in &mut views { + let mesh_key = MeshPipelineKey::from_msaa_samples(msaa.samples()) + | MeshPipelineKey::from_hdr(view.hdr); + + for (entity, handle) in &line_gizmos { + let Some(line_gizmo) = line_gizmo_assets.get(handle) else { continue }; + + let pipeline = pipelines.specialize( + &pipeline_cache, + &pipeline, + LineGizmoPipelineKey { + mesh_key, + strip: line_gizmo.strip, + perspective: config.line_perspective, + }, + ); + + transparent_phase.add(Transparent3d { + entity, + draw_function, + pipeline, + distance: 0., + }); } } } diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index d3e9a532a44cf..8c8fa2c4dfb8c 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -75,7 +75,7 @@ x11 = ["bevy_winit/x11"] subpixel_glyph_atlas = ["bevy_text/subpixel_glyph_atlas"] # Optimise for WebGL2 -webgl = ["bevy_core_pipeline?/webgl", "bevy_pbr?/webgl", "bevy_render?/webgl"] +webgl = ["bevy_core_pipeline?/webgl", "bevy_pbr?/webgl", "bevy_render?/webgl", "bevy_gizmos?/webgl"] # enable systems that allow for automated testing on CI bevy_ci_testing = ["bevy_app/bevy_ci_testing", "bevy_time/bevy_ci_testing", "bevy_render?/bevy_ci_testing", "bevy_render?/ci_limits"] diff --git a/examples/2d/2d_gizmos.rs b/examples/2d/2d_gizmos.rs index 5cd70149675f4..da7a639ba06be 100644 --- a/examples/2d/2d_gizmos.rs +++ b/examples/2d/2d_gizmos.rs @@ -8,12 +8,21 @@ fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) - .add_systems(Update, system) + .add_systems(Update, (system, update_config)) .run(); } -fn setup(mut commands: Commands) { +fn setup(mut commands: Commands, asset_server: Res) { commands.spawn(Camera2dBundle::default()); + // text + commands.spawn(TextBundle::from_section( + "Hold 'Left' or 'Right' to change the line width", + TextStyle { + font: asset_server.load("fonts/FiraMono-Medium.ttf"), + font_size: 24., + color: Color::WHITE, + }, + )); } fn system(mut gizmos: Gizmos, time: Res