From 27215b79b0cf79c71e6728d72937413fb7a3bff2 Mon Sep 17 00:00:00 2001 From: Lynn <62256001+solis-lumine-vorago@users.noreply.github.com> Date: Mon, 11 Mar 2024 20:21:32 +0100 Subject: [PATCH] Gizmo line joints (#12252) # Objective - Adds gizmo line joints, suggestion of #9400 ## Solution - Adds `line_joints: GizmoLineJoint` to `GizmoConfig`. Currently the following values are supported: - `GizmoLineJoint::None`: does not draw line joints, same behaviour as previously - `GizmoLineJoint::Bevel`: draws a single triangle between the lines - `GizmoLineJoint::Miter` / 'spiky joints': draws two triangles between the lines extending them until they meet at a (miter) point. - NOTE: for very small angles between the lines, which happens frequently in 3d, the miter point will be very far away from the point at which the lines meet. - `GizmoLineJoint::Round(resolution)`: Draw a circle arc between the lines. The circle is a triangle fan of `resolution` triangles. --- ## Changelog - Added `GizmoLineJoint`, use that in `GizmoConfig` and added necessary pipelines and draw commands. - Added a new `line_joints.wgsl` shader containing three vertex shaders `vertex_bevel`, `vertex_miter` and `vertex_round` as well as a basic `fragment` shader. ## Migration Guide Any manually created `GizmoConfig`s must now set the `.line_joints` field. ## Known issues - The way we currently create basic closed shapes like rectangles, circles, triangles or really any closed 2d shape means that one of the corners will not be drawn with joints, although that would probably be expected. (see the triangle in the 2d image) - This could be somewhat mitigated by introducing line caps or fixed by adding another segment overlapping the first of the strip. (Maybe in a followup PR?) - 3d shapes can look 'off' with line joints (especially bevel) because wherever 3 or more lines meet one of them may stick out beyond the joint drawn between the other 2. - Adding additional lines so that there is a joint between every line at a corner would fix this but would probably be too computationally expensive. - Miter joints are 'unreasonably long' for very small angles between the lines (the angle is the angle between the lines in screen space). This is technically correct but distracting and does not feel right, especially in 3d contexts. I think limiting the length of the miter to the point at which the lines meet might be a good idea. - The joints may be drawn with a different gizmo in-between them and their corresponding lines in 2d. Some sort of z-ordering would probably be good here, but I believe this may be out of scope for this PR. ## Additional information Some pretty images :) Screenshot 2024-03-02 at 04 53 50 - Note that the top vertex does not have a joint drawn. Screenshot 2024-03-02 at 05 03 55 Now for a weird video: https://github.com/bevyengine/bevy/assets/62256001/93026f48-f1d6-46fe-9163-5ab548a3fce4 - The black lines shooting out from the cube are miter joints that get very long because the lines between which they are drawn are (almost) collinear in screen space. --------- Co-authored-by: Pablo Reinhardt <126117294+pablo-lua@users.noreply.github.com> --- crates/bevy_gizmos/src/config.rs | 22 +++ crates/bevy_gizmos/src/lib.rs | 144 ++++++++++++-- crates/bevy_gizmos/src/line_joints.wgsl | 248 ++++++++++++++++++++++++ crates/bevy_gizmos/src/pipeline_2d.rs | 161 ++++++++++++++- crates/bevy_gizmos/src/pipeline_3d.rs | 205 +++++++++++++++++++- examples/gizmos/2d_gizmos.rs | 19 +- examples/gizmos/3d_gizmos.rs | 19 +- 7 files changed, 796 insertions(+), 22 deletions(-) create mode 100644 crates/bevy_gizmos/src/line_joints.wgsl diff --git a/crates/bevy_gizmos/src/config.rs b/crates/bevy_gizmos/src/config.rs index 03675b168b4b3..04d11c8437d3d 100644 --- a/crates/bevy_gizmos/src/config.rs +++ b/crates/bevy_gizmos/src/config.rs @@ -13,6 +13,23 @@ use std::{ ops::{Deref, DerefMut}, }; +/// An enum configuring how line joints will be drawn. +#[derive(Debug, Default, Copy, Clone, Reflect, PartialEq, Eq, Hash)] +pub enum GizmoLineJoint { + /// Does not draw any line joints. + #[default] + None, + /// Extends both lines at the joining point until they meet in a sharp point. + Miter, + /// Draws a round corner with the specified resolution between the two lines. + /// + /// The resolution determines the amount of triangles drawn per joint, + /// e.g. `GizmoLineJoint::Round(4)` will draw 4 triangles at each line joint. + Round(u32), + /// Draws a bevel, a straight line in this case, to connect the ends of both lines. + Bevel, +} + /// A trait used to create gizmo configs groups. /// /// Here you can store additional configuration for you gizmo group not covered by [`GizmoConfig`] @@ -135,6 +152,9 @@ pub struct GizmoConfig { /// /// Gizmos will only be rendered to cameras with intersecting layers. pub render_layers: RenderLayers, + + /// Describe how lines should join + pub line_joints: GizmoLineJoint, } impl Default for GizmoConfig { @@ -145,6 +165,8 @@ impl Default for GizmoConfig { line_perspective: false, depth_bias: 0., render_layers: Default::default(), + + line_joints: GizmoLineJoint::None, } } } diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs index 181408d222108..2716996329d32 100755 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/src/lib.rs @@ -46,7 +46,10 @@ pub mod prelude { #[doc(hidden)] pub use crate::{ aabb::{AabbGizmoConfigGroup, ShowAabbGizmo}, - config::{DefaultGizmoConfigGroup, GizmoConfig, GizmoConfigGroup, GizmoConfigStore}, + config::{ + DefaultGizmoConfigGroup, GizmoConfig, GizmoConfigGroup, GizmoConfigStore, + GizmoLineJoint, + }, gizmos::Gizmos, light::{LightGizmoColor, LightGizmoConfigGroup, ShowLightGizmo}, primitives::{dim2::GizmoPrimitive2d, dim3::GizmoPrimitive3d}, @@ -85,13 +88,15 @@ use bevy_render::{ use bevy_utils::TypeIdMap; use bytemuck::cast_slice; use config::{ - DefaultGizmoConfigGroup, GizmoConfig, GizmoConfigGroup, GizmoConfigStore, GizmoMeshConfig, + DefaultGizmoConfigGroup, GizmoConfig, GizmoConfigGroup, GizmoConfigStore, GizmoLineJoint, + GizmoMeshConfig, }; use gizmos::GizmoStorage; use light::LightGizmoPlugin; use std::{any::TypeId, mem}; const LINE_SHADER_HANDLE: Handle = Handle::weak_from_u128(7414812689238026784); +const LINE_JOINT_SHADER_HANDLE: Handle = Handle::weak_from_u128(1162780797909187908); /// A [`Plugin`] that provides an immediate mode drawing api for visual debugging. pub struct GizmoPlugin; @@ -105,6 +110,12 @@ impl Plugin for GizmoPlugin { ); 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.register_type::() .register_type::() @@ -140,7 +151,7 @@ impl Plugin for GizmoPlugin { }; let render_device = render_app.world.resource::(); - let layout = render_device.create_bind_group_layout( + let line_layout = render_device.create_bind_group_layout( "LineGizmoUniform layout", &BindGroupLayoutEntries::single( ShaderStages::VERTEX, @@ -148,7 +159,9 @@ impl Plugin for GizmoPlugin { ), ); - render_app.insert_resource(LineGizmoUniformBindgroupLayout { layout }); + render_app.insert_resource(LineGizmoUniformBindgroupLayout { + layout: line_layout, + }); } } @@ -232,6 +245,7 @@ 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); @@ -254,6 +268,7 @@ fn update_gizmo_meshes( } } + 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::()) { @@ -262,9 +277,11 @@ fn update_gizmo_meshes( strip.positions = mem::take(&mut storage.strip_positions); strip.colors = mem::take(&mut storage.strip_colors); + strip.joints = config.line_joints; } else { let mut strip = LineGizmo { strip: true, + joints: config.line_joints, ..Default::default() }; @@ -294,10 +311,17 @@ fn extract_gizmo_data( 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(), }, @@ -311,9 +335,11 @@ fn extract_gizmo_data( 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: bevy_math::Vec2, + _padding: f32, } #[derive(Asset, Debug, Default, Clone, TypePath)] @@ -322,6 +348,8 @@ struct LineGizmo { colors: Vec, /// Whether this gizmo's topology is a line-strip or line-list strip: bool, + /// Whether this gizmo should draw line joints. This is only applicable if the gizmo's topology is line-strip. + joints: GizmoLineJoint, } #[derive(Debug, Clone)] @@ -330,6 +358,7 @@ struct GpuLineGizmo { color_buffer: Buffer, vertex_count: u32, strip: bool, + joints: GizmoLineJoint, } impl RenderAsset for LineGizmo { @@ -363,6 +392,7 @@ impl RenderAsset for LineGizmo { color_buffer, vertex_count: self.positions.len() as u32, strip: self.strip, + joints: self.joints, }) } } @@ -446,15 +476,11 @@ impl RenderCommand

for DrawLineGizmo { } 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..)); + pass.set_vertex_buffer(0, line_gizmo.position_buffer.slice(..)); + pass.set_vertex_buffer(1, line_gizmo.position_buffer.slice(..)); - 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..)); + pass.set_vertex_buffer(2, line_gizmo.color_buffer.slice(..)); + pass.set_vertex_buffer(3, line_gizmo.color_buffer.slice(..)); u32::max(line_gizmo.vertex_count, 1) - 1 } else { @@ -470,6 +496,58 @@ impl RenderCommand

for DrawLineGizmo { } } +struct DrawLineJointGizmo; +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::Failure; + }; + let Some(line_gizmo) = line_gizmos.into_inner().get(handle) else { + return RenderCommandResult::Failure; + }; + + if line_gizmo.vertex_count <= 2 || !line_gizmo.strip { + return RenderCommandResult::Success; + }; + + if line_gizmo.joints == GizmoLineJoint::None { + return RenderCommandResult::Success; + }; + + let instances = { + pass.set_vertex_buffer(0, line_gizmo.position_buffer.slice(..)); + pass.set_vertex_buffer(1, line_gizmo.position_buffer.slice(..)); + pass.set_vertex_buffer(2, line_gizmo.position_buffer.slice(..)); + + pass.set_vertex_buffer(3, line_gizmo.color_buffer.slice(..)); + + 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 + } +} + fn line_gizmo_vertex_buffer_layouts(strip: bool) -> Vec { use VertexFormat::*; let mut position_layout = VertexBufferLayout { @@ -497,11 +575,13 @@ fn line_gizmo_vertex_buffer_layouts(strip: bool) -> Vec { position_layout.clone(), { position_layout.attributes[0].shader_location = 1; + position_layout.attributes[0].offset = Float32x3.size(); position_layout }, color_layout.clone(), { color_layout.attributes[0].shader_location = 3; + color_layout.attributes[0].offset = Float32x4.size(); color_layout }, ] @@ -523,3 +603,41 @@ fn line_gizmo_vertex_buffer_layouts(strip: bool) -> Vec { vec![position_layout, color_layout] } } + +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: Float32x4.size(), + shader_location: 3, + }], + }; + + vec![ + position_layout.clone(), + { + position_layout.attributes[0].shader_location = 1; + position_layout.attributes[0].offset = Float32x3.size(); + position_layout.clone() + }, + { + position_layout.attributes[0].shader_location = 2; + position_layout.attributes[0].offset = 2 * Float32x3.size(); + position_layout + }, + color_layout.clone(), + ] +} diff --git a/crates/bevy_gizmos/src/line_joints.wgsl b/crates/bevy_gizmos/src/line_joints.wgsl new file mode 100644 index 0000000000000..974a5266286be --- /dev/null +++ b/crates/bevy_gizmos/src/line_joints.wgsl @@ -0,0 +1,248 @@ +#import bevy_render::view::View + +@group(0) @binding(0) var view: View; + + +struct LineGizmoUniform { + line_width: f32, + depth_bias: f32, + resolution: u32, +#ifdef SIXTEEN_BYTE_ALIGNMENT + // WebGL2 structs must be 16 byte aligned. + _padding: f32, +#endif +} + +@group(1) @binding(0) var joints_gizmo: LineGizmoUniform; + +struct VertexInput { + @location(0) position_a: vec3, + @location(1) position_b: vec3, + @location(2) position_c: vec3, + @location(3) 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_bevel(vertex: VertexInput) -> VertexOutput { + var positions = array, 3>( + vec2(0, 0), + vec2(0, 0.5), + vec2(0.5, 0), + ); + var position = positions[vertex.index]; + + var clip_a = view.view_proj * vec4(vertex.position_a, 1.); + var clip_b = view.view_proj * vec4(vertex.position_b, 1.); + var clip_c = view.view_proj * vec4(vertex.position_c, 1.); + + // Manual near plane clipping to avoid errors when doing the perspective divide inside this shader. + clip_a = clip_near_plane(clip_a, clip_c); + clip_b = clip_near_plane(clip_b, clip_a); + clip_c = clip_near_plane(clip_c, clip_b); + clip_a = clip_near_plane(clip_a, clip_c); + + 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 screen_c = resolution * (0.5 * clip_c.xy / clip_c.w + 0.5); + + var color = vertex.color; + var line_width = joints_gizmo.line_width; + +#ifdef PERSPECTIVE + line_width /= clip_b.w; +#endif + + // Line thinness fade from https://acegikmo.com/shapes/docs/#anti-aliasing + if line_width > 0.0 && line_width < 1. { + color.a *= line_width; + line_width = 1.; + } + + let ab = normalize(screen_b - screen_a); + let cb = normalize(screen_b - screen_c); + let ab_norm = vec2(-ab.y, ab.x); + let cb_norm = vec2(cb.y, -cb.x); + let tangent = normalize(ab - cb); + let normal = vec2(-tangent.y, tangent.x); + let sigma = sign(dot(ab + cb, normal)); + + var p0 = line_width * sigma * ab_norm; + var p1 = line_width * sigma * cb_norm; + + let screen = screen_b + position.x * p0 + position.y * p1; + + let depth = depth(clip_b); + + var clip_position = vec4(clip_b.w * ((2. * screen) / resolution - 1.), depth, clip_b.w); + return VertexOutput(clip_position, color); +} + +@vertex +fn vertex_miter(vertex: VertexInput) -> VertexOutput { + var positions = array, 6>( + vec3(0, 0, 0), + vec3(0.5, 0, 0), + vec3(0, 0.5, 0), + vec3(0, 0, 0), + vec3(0, 0.5, 0), + vec3(0, 0, 0.5), + ); + var position = positions[vertex.index]; + + var clip_a = view.view_proj * vec4(vertex.position_a, 1.); + var clip_b = view.view_proj * vec4(vertex.position_b, 1.); + var clip_c = view.view_proj * vec4(vertex.position_c, 1.); + + // Manual near plane clipping to avoid errors when doing the perspective divide inside this shader. + clip_a = clip_near_plane(clip_a, clip_c); + clip_b = clip_near_plane(clip_b, clip_a); + clip_c = clip_near_plane(clip_c, clip_b); + clip_a = clip_near_plane(clip_a, clip_c); + + 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 screen_c = resolution * (0.5 * clip_c.xy / clip_c.w + 0.5); + + var color = vertex.color; + var line_width = joints_gizmo.line_width; + +#ifdef PERSPECTIVE + line_width /= clip_b.w; +#endif + + // Line thinness fade from https://acegikmo.com/shapes/docs/#anti-aliasing + if line_width > 0.0 && line_width < 1. { + color.a *= line_width; + line_width = 1.; + } + + let ab = normalize(screen_b - screen_a); + let cb = normalize(screen_b - screen_c); + let ab_norm = vec2(-ab.y, ab.x); + let cb_norm = vec2(cb.y, -cb.x); + let tangent = normalize(ab - cb); + let normal = vec2(-tangent.y, tangent.x); + let sigma = sign(dot(ab + cb, normal)); + + var p0 = line_width * sigma * ab_norm; + var p1 = line_width * sigma * normal / dot(normal, ab_norm); + var p2 = line_width * sigma * cb_norm; + + var screen = screen_b + position.x * p0 + position.y * p1 + position.z * p2; + + var depth = depth(clip_b); + + var clip_position = vec4(clip_b.w * ((2. * screen) / resolution - 1.), depth, clip_b.w); + return VertexOutput(clip_position, color); +} + +@vertex +fn vertex_round(vertex: VertexInput) -> VertexOutput { + var clip_a = view.view_proj * vec4(vertex.position_a, 1.); + var clip_b = view.view_proj * vec4(vertex.position_b, 1.); + var clip_c = view.view_proj * vec4(vertex.position_c, 1.); + + // Manual near plane clipping to avoid errors when doing the perspective divide inside this shader. + clip_a = clip_near_plane(clip_a, clip_c); + clip_b = clip_near_plane(clip_b, clip_a); + clip_c = clip_near_plane(clip_c, clip_b); + clip_a = clip_near_plane(clip_a, clip_c); + + 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 screen_c = resolution * (0.5 * clip_c.xy / clip_c.w + 0.5); + + var color = vertex.color; + var line_width = joints_gizmo.line_width; + +#ifdef PERSPECTIVE + line_width /= clip_b.w; +#endif + + // Line thinness fade from https://acegikmo.com/shapes/docs/#anti-aliasing + if line_width > 0.0 && line_width < 1. { + color.a *= line_width; + line_width = 1.; + } + + let ab = normalize(screen_b - screen_a); + let cb = normalize(screen_b - screen_c); + let ab_norm = vec2(-ab.y, ab.x); + let cb_norm = vec2(cb.y, -cb.x); + + // We render `joints_gizmo.resolution`triangles. The vertices in each triangle are ordered as follows: + // - 0: The 'center' vertex at `screen_b`. + // - 1: The vertex closer to the ab line. + // - 2: The vertex closer to the cb line. + var in_triangle_index = f32(vertex.index) % 3.0; + var tri_index = floor(f32(vertex.index) / 3.0); + var radius = sign(in_triangle_index) * 0.5 * line_width; + var theta = acos(dot(ab_norm, cb_norm)); + let sigma = sign(dot(ab_norm, cb)); + var angle = theta * (tri_index + in_triangle_index - 1) / f32(joints_gizmo.resolution); + var position_x = sigma * radius * cos(angle); + var position_y = radius * sin(angle); + + var screen = screen_b + position_x * ab_norm + position_y * ab; + + var depth = depth(clip_b); + + var clip_position = vec4(clip_b.w * ((2. * screen) / resolution - 1.), depth, clip_b.w); + return VertexOutput(clip_position, color); +} + +fn clip_near_plane(a: vec4, b: vec4) -> vec4 { + // Move a if a is behind the near plane and b is in front. + if a.z > a.w && b.z <= b.w { + // Interpolate a towards b until it's at the near plane. + let distance_a = a.z - a.w; + let distance_b = b.z - b.w; + // Add an epsilon to the interpolator to ensure that the point is + // not just behind the clip plane due to floating-point imprecision. + let t = distance_a / (distance_a - distance_b) + EPSILON; + return mix(a, b, t); + } + return a; +} + +fn depth(clip: vec4) -> f32 { + var depth: f32; + if joints_gizmo.depth_bias >= 0. { + depth = clip.z * (1. - joints_gizmo.depth_bias); + } else { + // 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 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 = clip.z * exp2(-joints_gizmo.depth_bias * log2(clip.w / clip.z - EPSILON)); + } + return depth; +} + +struct FragmentInput { + @location(0) color: vec4, +}; + +struct FragmentOutput { + @location(0) color: vec4, +}; + +@fragment +fn fragment(in: FragmentInput) -> FragmentOutput { + // return FragmentOutput(vec4(1, 1, 1, 1)); + return FragmentOutput(in.color); +} \ No newline at end of file diff --git a/crates/bevy_gizmos/src/pipeline_2d.rs b/crates/bevy_gizmos/src/pipeline_2d.rs index f55453fa67035..8e44a6ddafc57 100644 --- a/crates/bevy_gizmos/src/pipeline_2d.rs +++ b/crates/bevy_gizmos/src/pipeline_2d.rs @@ -1,6 +1,8 @@ use crate::{ - config::GizmoMeshConfig, line_gizmo_vertex_buffer_layouts, DrawLineGizmo, GizmoRenderSystem, - LineGizmo, LineGizmoUniformBindgroupLayout, SetLineGizmoBindGroup, LINE_SHADER_HANDLE, + config::{GizmoLineJoint, GizmoMeshConfig}, + line_gizmo_vertex_buffer_layouts, line_joint_gizmo_vertex_buffer_layouts, DrawLineGizmo, + DrawLineJointGizmo, GizmoRenderSystem, LineGizmo, LineGizmoUniformBindgroupLayout, + SetLineGizmoBindGroup, LINE_JOINT_SHADER_HANDLE, LINE_SHADER_HANDLE, }; use bevy_app::{App, Plugin}; use bevy_asset::Handle; @@ -21,6 +23,7 @@ use bevy_render::{ Render, RenderApp, RenderSet, }; use bevy_sprite::{Mesh2dPipeline, Mesh2dPipelineKey, SetMesh2dViewBindGroup}; +use bevy_utils::tracing::error; use bevy_utils::FloatOrd; pub struct LineGizmo2dPlugin; @@ -33,14 +36,16 @@ impl Plugin for LineGizmo2dPlugin { render_app .add_render_command::() + .add_render_command::() .init_resource::>() + .init_resource::>() .configure_sets( Render, GizmoRenderSystem::QueueLineGizmos2d.in_set(RenderSet::Queue), ) .add_systems( Render, - queue_line_gizmos_2d + (queue_line_gizmos_2d, queue_line_joint_gizmos_2d) .in_set(GizmoRenderSystem::QueueLineGizmos2d) .after(prepare_assets::), ); @@ -52,6 +57,7 @@ impl Plugin for LineGizmo2dPlugin { }; render_app.init_resource::(); + render_app.init_resource::(); } } @@ -130,12 +136,103 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { } } +#[derive(Clone, Resource)] +struct LineJointGizmoPipeline { + mesh_pipeline: Mesh2dPipeline, + uniform_layout: BindGroupLayout, +} + +impl FromWorld for LineJointGizmoPipeline { + fn from_world(render_world: &mut World) -> Self { + LineJointGizmoPipeline { + mesh_pipeline: render_world.resource::().clone(), + uniform_layout: render_world + .resource::() + .layout + .clone(), + } + } +} + +#[derive(PartialEq, Eq, Hash, Clone)] +struct LineJointGizmoPipelineKey { + mesh_key: Mesh2dPipelineKey, + joints: GizmoLineJoint, +} + +impl SpecializedRenderPipeline for LineJointGizmoPipeline { + type Key = LineJointGizmoPipelineKey; + + 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(), + ]; + + if key.joints == GizmoLineJoint::None { + error!("There is no entry point for line joints with GizmoLineJoints::None. Please consider aborting the drawing process before reaching this stage."); + }; + + let entry_point = match key.joints { + GizmoLineJoint::Miter => "vertex_miter", + GizmoLineJoint::Round(_) => "vertex_round", + GizmoLineJoint::None | GizmoLineJoint::Bevel => "vertex_bevel", + }; + + RenderPipelineDescriptor { + vertex: VertexState { + shader: LINE_JOINT_SHADER_HANDLE, + entry_point: entry_point.into(), + shader_defs: shader_defs.clone(), + buffers: line_joint_gizmo_vertex_buffer_layouts(), + }, + fragment: Some(FragmentState { + shader: LINE_JOINT_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: None, + multisample: MultisampleState { + count: key.mesh_key.msaa_samples(), + mask: !0, + alpha_to_coverage_enabled: false, + }, + label: Some("LineJointGizmo Pipeline 2D".into()), + push_constant_ranges: vec![], + } + } +} + type DrawLineGizmo2d = ( SetItemPipeline, SetMesh2dViewBindGroup<0>, SetLineGizmoBindGroup<1>, DrawLineGizmo, ); +type DrawLineJointGizmo2d = ( + SetItemPipeline, + SetMesh2dViewBindGroup<0>, + SetLineGizmoBindGroup<1>, + DrawLineJointGizmo, +); #[allow(clippy::too_many_arguments)] fn queue_line_gizmos_2d( @@ -188,3 +285,61 @@ fn queue_line_gizmos_2d( } } } + +#[allow(clippy::too_many_arguments)] +fn queue_line_joint_gizmos_2d( + draw_functions: Res>, + pipeline: Res, + mut pipelines: ResMut>, + pipeline_cache: Res, + msaa: Res, + line_gizmos: Query<(Entity, &Handle, &GizmoMeshConfig)>, + line_gizmo_assets: Res>, + mut views: Query<( + &ExtractedView, + &mut RenderPhase, + Option<&RenderLayers>, + )>, +) { + let draw_function = draw_functions + .read() + .get_id::() + .unwrap(); + + for (view, mut transparent_phase, render_layers) in &mut views { + let mesh_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples()) + | Mesh2dPipelineKey::from_hdr(view.hdr); + + for (entity, handle, config) in &line_gizmos { + let render_layers = render_layers.copied().unwrap_or_default(); + if !config.render_layers.intersects(&render_layers) { + continue; + } + + let Some(line_gizmo) = line_gizmo_assets.get(handle) else { + continue; + }; + + if !line_gizmo.strip || line_gizmo.joints == GizmoLineJoint::None { + continue; + } + + let pipeline = pipelines.specialize( + &pipeline_cache, + &pipeline, + LineJointGizmoPipelineKey { + mesh_key, + joints: line_gizmo.joints, + }, + ); + transparent_phase.add(Transparent2d { + entity, + draw_function, + pipeline, + sort_key: FloatOrd(f32::INFINITY), + batch_range: 0..1, + dynamic_offset: None, + }); + } + } +} diff --git a/crates/bevy_gizmos/src/pipeline_3d.rs b/crates/bevy_gizmos/src/pipeline_3d.rs index bd5064e39d789..ecefb13d510cf 100644 --- a/crates/bevy_gizmos/src/pipeline_3d.rs +++ b/crates/bevy_gizmos/src/pipeline_3d.rs @@ -1,6 +1,8 @@ use crate::{ - config::GizmoMeshConfig, line_gizmo_vertex_buffer_layouts, DrawLineGizmo, GizmoRenderSystem, - LineGizmo, LineGizmoUniformBindgroupLayout, SetLineGizmoBindGroup, LINE_SHADER_HANDLE, + config::GizmoMeshConfig, line_gizmo_vertex_buffer_layouts, + line_joint_gizmo_vertex_buffer_layouts, prelude::GizmoLineJoint, DrawLineGizmo, + DrawLineJointGizmo, GizmoRenderSystem, LineGizmo, LineGizmoUniformBindgroupLayout, + SetLineGizmoBindGroup, LINE_JOINT_SHADER_HANDLE, LINE_SHADER_HANDLE, }; use bevy_app::{App, Plugin}; use bevy_asset::Handle; @@ -25,6 +27,7 @@ use bevy_render::{ view::{ExtractedView, Msaa, RenderLayers, ViewTarget}, Render, RenderApp, RenderSet, }; +use bevy_utils::tracing::error; pub struct LineGizmo3dPlugin; impl Plugin for LineGizmo3dPlugin { @@ -35,14 +38,16 @@ impl Plugin for LineGizmo3dPlugin { render_app .add_render_command::() + .add_render_command::() .init_resource::>() + .init_resource::>() .configure_sets( Render, GizmoRenderSystem::QueueLineGizmos3d.in_set(RenderSet::Queue), ) .add_systems( Render, - queue_line_gizmos_3d + (queue_line_gizmos_3d, queue_line_joint_gizmos_3d) .in_set(GizmoRenderSystem::QueueLineGizmos3d) .after(prepare_assets::), ); @@ -54,6 +59,7 @@ impl Plugin for LineGizmo3dPlugin { }; render_app.init_resource::(); + render_app.init_resource::(); } } @@ -145,12 +151,116 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { } } +#[derive(Clone, Resource)] +struct LineJointGizmoPipeline { + mesh_pipeline: MeshPipeline, + uniform_layout: BindGroupLayout, +} + +impl FromWorld for LineJointGizmoPipeline { + fn from_world(render_world: &mut World) -> Self { + LineJointGizmoPipeline { + mesh_pipeline: render_world.resource::().clone(), + uniform_layout: render_world + .resource::() + .layout + .clone(), + } + } +} + +#[derive(PartialEq, Eq, Hash, Clone)] +struct LineJointGizmoPipelineKey { + view_key: MeshPipelineKey, + perspective: bool, + joints: GizmoLineJoint, +} + +impl SpecializedRenderPipeline for LineJointGizmoPipeline { + type Key = LineJointGizmoPipelineKey; + + 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()]; + + if key.joints == GizmoLineJoint::None { + error!("There is no entry point for line joints with GizmoLineJoints::None. Please consider aborting the drawing process before reaching this stage."); + }; + + let entry_point = match key.joints { + GizmoLineJoint::Miter => "vertex_miter", + GizmoLineJoint::Round(_) => "vertex_round", + GizmoLineJoint::None | GizmoLineJoint::Bevel => "vertex_bevel", + }; + + RenderPipelineDescriptor { + vertex: VertexState { + shader: LINE_JOINT_SHADER_HANDLE, + entry_point: entry_point.into(), + shader_defs: shader_defs.clone(), + buffers: line_joint_gizmo_vertex_buffer_layouts(), + }, + fragment: Some(FragmentState { + shader: LINE_JOINT_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("LineJointGizmo Pipeline".into()), + push_constant_ranges: vec![], + } + } +} + type DrawLineGizmo3d = ( SetItemPipeline, SetMeshViewBindGroup<0>, SetLineGizmoBindGroup<1>, DrawLineGizmo, ); +type DrawLineJointGizmo3d = ( + SetItemPipeline, + SetMeshViewBindGroup<0>, + SetLineGizmoBindGroup<1>, + DrawLineJointGizmo, +); #[allow(clippy::too_many_arguments)] fn queue_line_gizmos_3d( @@ -233,3 +343,92 @@ fn queue_line_gizmos_3d( } } } + +#[allow(clippy::too_many_arguments)] +fn queue_line_joint_gizmos_3d( + draw_functions: Res>, + pipeline: Res, + mut pipelines: ResMut>, + pipeline_cache: Res, + msaa: Res, + line_gizmos: Query<(Entity, &Handle, &GizmoMeshConfig)>, + line_gizmo_assets: Res>, + mut views: Query<( + &ExtractedView, + &mut RenderPhase, + Option<&RenderLayers>, + ( + Has, + Has, + Has, + Has, + ), + )>, +) { + let draw_function = draw_functions + .read() + .get_id::() + .unwrap(); + + for ( + view, + mut transparent_phase, + render_layers, + (normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass), + ) in &mut views + { + let render_layers = render_layers.copied().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, handle, config) in &line_gizmos { + if !config.render_layers.intersects(&render_layers) { + continue; + } + + let Some(line_gizmo) = line_gizmo_assets.get(handle) else { + continue; + }; + + if !line_gizmo.strip || line_gizmo.joints == GizmoLineJoint::None { + continue; + } + + let pipeline = pipelines.specialize( + &pipeline_cache, + &pipeline, + LineJointGizmoPipelineKey { + view_key, + perspective: config.line_perspective, + joints: line_gizmo.joints, + }, + ); + + transparent_phase.add(Transparent3d { + entity, + draw_function, + pipeline, + distance: 0., + batch_range: 0..1, + dynamic_offset: None, + }); + } + } +} diff --git a/examples/gizmos/2d_gizmos.rs b/examples/gizmos/2d_gizmos.rs index a4604ed1b46c8..ccd4b1171f699 100644 --- a/examples/gizmos/2d_gizmos.rs +++ b/examples/gizmos/2d_gizmos.rs @@ -23,7 +23,8 @@ fn setup(mut commands: Commands, asset_server: Res) { commands.spawn(TextBundle::from_section( "Hold 'Left' or 'Right' to change the line width of straight gizmos\n\ Hold 'Up' or 'Down' to change the line width of round gizmos\n\ - Press '1' or '2' to toggle the visibility of straight gizmos or round gizmos", + Press '1' or '2' to toggle the visibility of straight gizmos or round gizmos\n\ + Press 'J' or 'K' to cycle through line joints for straight or round gizmos", TextStyle { font: asset_server.load("fonts/FiraMono-Medium.ttf"), font_size: 24., @@ -106,6 +107,14 @@ fn update_config( if keyboard.just_pressed(KeyCode::Digit1) { config.enabled ^= true; } + if keyboard.just_pressed(KeyCode::KeyJ) { + config.line_joints = match config.line_joints { + GizmoLineJoint::Bevel => GizmoLineJoint::Miter, + GizmoLineJoint::Miter => GizmoLineJoint::Round(4), + GizmoLineJoint::Round(_) => GizmoLineJoint::None, + GizmoLineJoint::None => GizmoLineJoint::Bevel, + }; + } let (my_config, _) = config_store.config_mut::(); if keyboard.pressed(KeyCode::ArrowUp) { @@ -119,4 +128,12 @@ fn update_config( if keyboard.just_pressed(KeyCode::Digit2) { my_config.enabled ^= true; } + if keyboard.just_pressed(KeyCode::KeyK) { + my_config.line_joints = match my_config.line_joints { + GizmoLineJoint::Bevel => GizmoLineJoint::Miter, + GizmoLineJoint::Miter => GizmoLineJoint::Round(4), + GizmoLineJoint::Round(_) => GizmoLineJoint::None, + GizmoLineJoint::None => GizmoLineJoint::Bevel, + }; + } } diff --git a/examples/gizmos/3d_gizmos.rs b/examples/gizmos/3d_gizmos.rs index 973d3f13815c1..b3f76575dc831 100644 --- a/examples/gizmos/3d_gizmos.rs +++ b/examples/gizmos/3d_gizmos.rs @@ -59,8 +59,7 @@ fn setup( Hold 'Up' or 'Down' to change the line width of round gizmos\n\ Press '1' or '2' to toggle the visibility of straight gizmos or round gizmos\n\ Press 'A' to show all AABB boxes\n\ - Press 'K' or 'J' to cycle through primitives rendered with gizmos\n\ - Press 'H' or 'L' to decrease/increase the amount of segments in the primitives", + Press 'J' or 'K' to cycle through line joins for straight or round gizmos", TextStyle { font_size: 20., ..default() @@ -170,6 +169,14 @@ fn update_config( if keyboard.just_pressed(KeyCode::Digit1) { config.enabled ^= true; } + if keyboard.just_pressed(KeyCode::KeyJ) { + config.line_joints = match config.line_joints { + GizmoLineJoint::Bevel => GizmoLineJoint::Miter, + GizmoLineJoint::Miter => GizmoLineJoint::Round(4), + GizmoLineJoint::Round(_) => GizmoLineJoint::None, + GizmoLineJoint::None => GizmoLineJoint::Bevel, + }; + } let (my_config, _) = config_store.config_mut::(); if keyboard.pressed(KeyCode::ArrowUp) { @@ -183,6 +190,14 @@ fn update_config( if keyboard.just_pressed(KeyCode::Digit2) { my_config.enabled ^= true; } + if keyboard.just_pressed(KeyCode::KeyK) { + my_config.line_joints = match my_config.line_joints { + GizmoLineJoint::Bevel => GizmoLineJoint::Miter, + GizmoLineJoint::Miter => GizmoLineJoint::Round(4), + GizmoLineJoint::Round(_) => GizmoLineJoint::None, + GizmoLineJoint::None => GizmoLineJoint::Bevel, + }; + } if keyboard.just_pressed(KeyCode::KeyA) { // AABB gizmos are normally only drawn on entities with a ShowAabbGizmo component