Skip to content

Commit 27215b7

Browse files
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 :) <img width="1175" alt="Screenshot 2024-03-02 at 04 53 50" src="https://github.com/bevyengine/bevy/assets/62256001/58df7e63-9376-4430-8871-32adba0cb53b"> - Note that the top vertex does not have a joint drawn. <img width="1440" alt="Screenshot 2024-03-02 at 05 03 55" src="https://github.com/bevyengine/bevy/assets/62256001/137a00cf-cbd4-48c2-a46f-4b47492d4fd9"> 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 <[email protected]>
1 parent f89af05 commit 27215b7

File tree

7 files changed

+796
-22
lines changed

7 files changed

+796
-22
lines changed

crates/bevy_gizmos/src/config.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,23 @@ use std::{
1313
ops::{Deref, DerefMut},
1414
};
1515

16+
/// An enum configuring how line joints will be drawn.
17+
#[derive(Debug, Default, Copy, Clone, Reflect, PartialEq, Eq, Hash)]
18+
pub enum GizmoLineJoint {
19+
/// Does not draw any line joints.
20+
#[default]
21+
None,
22+
/// Extends both lines at the joining point until they meet in a sharp point.
23+
Miter,
24+
/// Draws a round corner with the specified resolution between the two lines.
25+
///
26+
/// The resolution determines the amount of triangles drawn per joint,
27+
/// e.g. `GizmoLineJoint::Round(4)` will draw 4 triangles at each line joint.
28+
Round(u32),
29+
/// Draws a bevel, a straight line in this case, to connect the ends of both lines.
30+
Bevel,
31+
}
32+
1633
/// A trait used to create gizmo configs groups.
1734
///
1835
/// Here you can store additional configuration for you gizmo group not covered by [`GizmoConfig`]
@@ -135,6 +152,9 @@ pub struct GizmoConfig {
135152
///
136153
/// Gizmos will only be rendered to cameras with intersecting layers.
137154
pub render_layers: RenderLayers,
155+
156+
/// Describe how lines should join
157+
pub line_joints: GizmoLineJoint,
138158
}
139159

140160
impl Default for GizmoConfig {
@@ -145,6 +165,8 @@ impl Default for GizmoConfig {
145165
line_perspective: false,
146166
depth_bias: 0.,
147167
render_layers: Default::default(),
168+
169+
line_joints: GizmoLineJoint::None,
148170
}
149171
}
150172
}

crates/bevy_gizmos/src/lib.rs

Lines changed: 131 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ pub mod prelude {
4646
#[doc(hidden)]
4747
pub use crate::{
4848
aabb::{AabbGizmoConfigGroup, ShowAabbGizmo},
49-
config::{DefaultGizmoConfigGroup, GizmoConfig, GizmoConfigGroup, GizmoConfigStore},
49+
config::{
50+
DefaultGizmoConfigGroup, GizmoConfig, GizmoConfigGroup, GizmoConfigStore,
51+
GizmoLineJoint,
52+
},
5053
gizmos::Gizmos,
5154
light::{LightGizmoColor, LightGizmoConfigGroup, ShowLightGizmo},
5255
primitives::{dim2::GizmoPrimitive2d, dim3::GizmoPrimitive3d},
@@ -85,13 +88,15 @@ use bevy_render::{
8588
use bevy_utils::TypeIdMap;
8689
use bytemuck::cast_slice;
8790
use config::{
88-
DefaultGizmoConfigGroup, GizmoConfig, GizmoConfigGroup, GizmoConfigStore, GizmoMeshConfig,
91+
DefaultGizmoConfigGroup, GizmoConfig, GizmoConfigGroup, GizmoConfigStore, GizmoLineJoint,
92+
GizmoMeshConfig,
8993
};
9094
use gizmos::GizmoStorage;
9195
use light::LightGizmoPlugin;
9296
use std::{any::TypeId, mem};
9397

9498
const LINE_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(7414812689238026784);
99+
const LINE_JOINT_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(1162780797909187908);
95100

96101
/// A [`Plugin`] that provides an immediate mode drawing api for visual debugging.
97102
pub struct GizmoPlugin;
@@ -105,6 +110,12 @@ impl Plugin for GizmoPlugin {
105110
);
106111

107112
load_internal_asset!(app, LINE_SHADER_HANDLE, "lines.wgsl", Shader::from_wgsl);
113+
load_internal_asset!(
114+
app,
115+
LINE_JOINT_SHADER_HANDLE,
116+
"line_joints.wgsl",
117+
Shader::from_wgsl
118+
);
108119

109120
app.register_type::<GizmoConfig>()
110121
.register_type::<GizmoConfigStore>()
@@ -140,15 +151,17 @@ impl Plugin for GizmoPlugin {
140151
};
141152

142153
let render_device = render_app.world.resource::<RenderDevice>();
143-
let layout = render_device.create_bind_group_layout(
154+
let line_layout = render_device.create_bind_group_layout(
144155
"LineGizmoUniform layout",
145156
&BindGroupLayoutEntries::single(
146157
ShaderStages::VERTEX,
147158
uniform_buffer::<LineGizmoUniform>(true),
148159
),
149160
);
150161

151-
render_app.insert_resource(LineGizmoUniformBindgroupLayout { layout });
162+
render_app.insert_resource(LineGizmoUniformBindgroupLayout {
163+
layout: line_layout,
164+
});
152165
}
153166
}
154167

@@ -232,6 +245,7 @@ fn update_gizmo_meshes<T: GizmoConfigGroup>(
232245
mut line_gizmos: ResMut<Assets<LineGizmo>>,
233246
mut handles: ResMut<LineGizmoHandles>,
234247
mut storage: ResMut<GizmoStorage<T>>,
248+
config_store: Res<GizmoConfigStore>,
235249
) {
236250
if storage.list_positions.is_empty() {
237251
handles.list.insert(TypeId::of::<T>(), None);
@@ -254,6 +268,7 @@ fn update_gizmo_meshes<T: GizmoConfigGroup>(
254268
}
255269
}
256270

271+
let (config, _) = config_store.config::<T>();
257272
if storage.strip_positions.is_empty() {
258273
handles.strip.insert(TypeId::of::<T>(), None);
259274
} else if let Some(handle) = handles.strip.get_mut(&TypeId::of::<T>()) {
@@ -262,9 +277,11 @@ fn update_gizmo_meshes<T: GizmoConfigGroup>(
262277

263278
strip.positions = mem::take(&mut storage.strip_positions);
264279
strip.colors = mem::take(&mut storage.strip_colors);
280+
strip.joints = config.line_joints;
265281
} else {
266282
let mut strip = LineGizmo {
267283
strip: true,
284+
joints: config.line_joints,
268285
..Default::default()
269286
};
270287

@@ -294,10 +311,17 @@ fn extract_gizmo_data(
294311
continue;
295312
};
296313

314+
let joints_resolution = if let GizmoLineJoint::Round(resolution) = config.line_joints {
315+
resolution
316+
} else {
317+
0
318+
};
319+
297320
commands.spawn((
298321
LineGizmoUniform {
299322
line_width: config.line_width,
300323
depth_bias: config.depth_bias,
324+
joints_resolution,
301325
#[cfg(feature = "webgl")]
302326
_padding: Default::default(),
303327
},
@@ -311,9 +335,11 @@ fn extract_gizmo_data(
311335
struct LineGizmoUniform {
312336
line_width: f32,
313337
depth_bias: f32,
338+
// Only used by gizmo line t if the current configs `line_joints` is set to `GizmoLineJoint::Round(_)`
339+
joints_resolution: u32,
314340
/// WebGL2 structs must be 16 byte aligned.
315341
#[cfg(feature = "webgl")]
316-
_padding: bevy_math::Vec2,
342+
_padding: f32,
317343
}
318344

319345
#[derive(Asset, Debug, Default, Clone, TypePath)]
@@ -322,6 +348,8 @@ struct LineGizmo {
322348
colors: Vec<LinearRgba>,
323349
/// Whether this gizmo's topology is a line-strip or line-list
324350
strip: bool,
351+
/// Whether this gizmo should draw line joints. This is only applicable if the gizmo's topology is line-strip.
352+
joints: GizmoLineJoint,
325353
}
326354

327355
#[derive(Debug, Clone)]
@@ -330,6 +358,7 @@ struct GpuLineGizmo {
330358
color_buffer: Buffer,
331359
vertex_count: u32,
332360
strip: bool,
361+
joints: GizmoLineJoint,
333362
}
334363

335364
impl RenderAsset for LineGizmo {
@@ -363,6 +392,7 @@ impl RenderAsset for LineGizmo {
363392
color_buffer,
364393
vertex_count: self.positions.len() as u32,
365394
strip: self.strip,
395+
joints: self.joints,
366396
})
367397
}
368398
}
@@ -446,15 +476,11 @@ impl<P: PhaseItem> RenderCommand<P> for DrawLineGizmo {
446476
}
447477

448478
let instances = if line_gizmo.strip {
449-
let item_size = VertexFormat::Float32x3.size();
450-
let buffer_size = line_gizmo.position_buffer.size() - item_size;
451-
pass.set_vertex_buffer(0, line_gizmo.position_buffer.slice(..buffer_size));
452-
pass.set_vertex_buffer(1, line_gizmo.position_buffer.slice(item_size..));
479+
pass.set_vertex_buffer(0, line_gizmo.position_buffer.slice(..));
480+
pass.set_vertex_buffer(1, line_gizmo.position_buffer.slice(..));
453481

454-
let item_size = VertexFormat::Float32x4.size();
455-
let buffer_size = line_gizmo.color_buffer.size() - item_size;
456-
pass.set_vertex_buffer(2, line_gizmo.color_buffer.slice(..buffer_size));
457-
pass.set_vertex_buffer(3, line_gizmo.color_buffer.slice(item_size..));
482+
pass.set_vertex_buffer(2, line_gizmo.color_buffer.slice(..));
483+
pass.set_vertex_buffer(3, line_gizmo.color_buffer.slice(..));
458484

459485
u32::max(line_gizmo.vertex_count, 1) - 1
460486
} else {
@@ -470,6 +496,58 @@ impl<P: PhaseItem> RenderCommand<P> for DrawLineGizmo {
470496
}
471497
}
472498

499+
struct DrawLineJointGizmo;
500+
impl<P: PhaseItem> RenderCommand<P> for DrawLineJointGizmo {
501+
type Param = SRes<RenderAssets<LineGizmo>>;
502+
type ViewQuery = ();
503+
type ItemQuery = Read<Handle<LineGizmo>>;
504+
505+
#[inline]
506+
fn render<'w>(
507+
_item: &P,
508+
_view: ROQueryItem<'w, Self::ViewQuery>,
509+
handle: Option<ROQueryItem<'w, Self::ItemQuery>>,
510+
line_gizmos: SystemParamItem<'w, '_, Self::Param>,
511+
pass: &mut TrackedRenderPass<'w>,
512+
) -> RenderCommandResult {
513+
let Some(handle) = handle else {
514+
return RenderCommandResult::Failure;
515+
};
516+
let Some(line_gizmo) = line_gizmos.into_inner().get(handle) else {
517+
return RenderCommandResult::Failure;
518+
};
519+
520+
if line_gizmo.vertex_count <= 2 || !line_gizmo.strip {
521+
return RenderCommandResult::Success;
522+
};
523+
524+
if line_gizmo.joints == GizmoLineJoint::None {
525+
return RenderCommandResult::Success;
526+
};
527+
528+
let instances = {
529+
pass.set_vertex_buffer(0, line_gizmo.position_buffer.slice(..));
530+
pass.set_vertex_buffer(1, line_gizmo.position_buffer.slice(..));
531+
pass.set_vertex_buffer(2, line_gizmo.position_buffer.slice(..));
532+
533+
pass.set_vertex_buffer(3, line_gizmo.color_buffer.slice(..));
534+
535+
u32::max(line_gizmo.vertex_count, 2) - 2
536+
};
537+
538+
let vertices = match line_gizmo.joints {
539+
GizmoLineJoint::None => unreachable!(),
540+
GizmoLineJoint::Miter => 6,
541+
GizmoLineJoint::Round(resolution) => resolution * 3,
542+
GizmoLineJoint::Bevel => 3,
543+
};
544+
545+
pass.draw(0..vertices, 0..instances);
546+
547+
RenderCommandResult::Success
548+
}
549+
}
550+
473551
fn line_gizmo_vertex_buffer_layouts(strip: bool) -> Vec<VertexBufferLayout> {
474552
use VertexFormat::*;
475553
let mut position_layout = VertexBufferLayout {
@@ -497,11 +575,13 @@ fn line_gizmo_vertex_buffer_layouts(strip: bool) -> Vec<VertexBufferLayout> {
497575
position_layout.clone(),
498576
{
499577
position_layout.attributes[0].shader_location = 1;
578+
position_layout.attributes[0].offset = Float32x3.size();
500579
position_layout
501580
},
502581
color_layout.clone(),
503582
{
504583
color_layout.attributes[0].shader_location = 3;
584+
color_layout.attributes[0].offset = Float32x4.size();
505585
color_layout
506586
},
507587
]
@@ -523,3 +603,41 @@ fn line_gizmo_vertex_buffer_layouts(strip: bool) -> Vec<VertexBufferLayout> {
523603
vec![position_layout, color_layout]
524604
}
525605
}
606+
607+
fn line_joint_gizmo_vertex_buffer_layouts() -> Vec<VertexBufferLayout> {
608+
use VertexFormat::*;
609+
let mut position_layout = VertexBufferLayout {
610+
array_stride: Float32x3.size(),
611+
step_mode: VertexStepMode::Instance,
612+
attributes: vec![VertexAttribute {
613+
format: Float32x3,
614+
offset: 0,
615+
shader_location: 0,
616+
}],
617+
};
618+
619+
let color_layout = VertexBufferLayout {
620+
array_stride: Float32x4.size(),
621+
step_mode: VertexStepMode::Instance,
622+
attributes: vec![VertexAttribute {
623+
format: Float32x4,
624+
offset: Float32x4.size(),
625+
shader_location: 3,
626+
}],
627+
};
628+
629+
vec![
630+
position_layout.clone(),
631+
{
632+
position_layout.attributes[0].shader_location = 1;
633+
position_layout.attributes[0].offset = Float32x3.size();
634+
position_layout.clone()
635+
},
636+
{
637+
position_layout.attributes[0].shader_location = 2;
638+
position_layout.attributes[0].offset = 2 * Float32x3.size();
639+
position_layout
640+
},
641+
color_layout.clone(),
642+
]
643+
}

0 commit comments

Comments
 (0)