Skip to content

Commit

Permalink
Gizmo line joints (#12252)
Browse files Browse the repository at this point in the history
# 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]>
  • Loading branch information
lynn-lumen and pablo-lua authored Mar 11, 2024
1 parent f89af05 commit 27215b7
Show file tree
Hide file tree
Showing 7 changed files with 796 additions and 22 deletions.
22 changes: 22 additions & 0 deletions crates/bevy_gizmos/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`]
Expand Down Expand Up @@ -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 {
Expand All @@ -145,6 +165,8 @@ impl Default for GizmoConfig {
line_perspective: false,
depth_bias: 0.,
render_layers: Default::default(),

line_joints: GizmoLineJoint::None,
}
}
}
Expand Down
144 changes: 131 additions & 13 deletions crates/bevy_gizmos/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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<Shader> = Handle::weak_from_u128(7414812689238026784);
const LINE_JOINT_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(1162780797909187908);

/// A [`Plugin`] that provides an immediate mode drawing api for visual debugging.
pub struct GizmoPlugin;
Expand All @@ -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::<GizmoConfig>()
.register_type::<GizmoConfigStore>()
Expand Down Expand Up @@ -140,15 +151,17 @@ impl Plugin for GizmoPlugin {
};

let render_device = render_app.world.resource::<RenderDevice>();
let layout = render_device.create_bind_group_layout(
let line_layout = render_device.create_bind_group_layout(
"LineGizmoUniform layout",
&BindGroupLayoutEntries::single(
ShaderStages::VERTEX,
uniform_buffer::<LineGizmoUniform>(true),
),
);

render_app.insert_resource(LineGizmoUniformBindgroupLayout { layout });
render_app.insert_resource(LineGizmoUniformBindgroupLayout {
layout: line_layout,
});
}
}

Expand Down Expand Up @@ -232,6 +245,7 @@ fn update_gizmo_meshes<T: GizmoConfigGroup>(
mut line_gizmos: ResMut<Assets<LineGizmo>>,
mut handles: ResMut<LineGizmoHandles>,
mut storage: ResMut<GizmoStorage<T>>,
config_store: Res<GizmoConfigStore>,
) {
if storage.list_positions.is_empty() {
handles.list.insert(TypeId::of::<T>(), None);
Expand All @@ -254,6 +268,7 @@ fn update_gizmo_meshes<T: GizmoConfigGroup>(
}
}

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

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()
};

Expand Down Expand Up @@ -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(),
},
Expand All @@ -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)]
Expand All @@ -322,6 +348,8 @@ struct LineGizmo {
colors: Vec<LinearRgba>,
/// 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)]
Expand All @@ -330,6 +358,7 @@ struct GpuLineGizmo {
color_buffer: Buffer,
vertex_count: u32,
strip: bool,
joints: GizmoLineJoint,
}

impl RenderAsset for LineGizmo {
Expand Down Expand Up @@ -363,6 +392,7 @@ impl RenderAsset for LineGizmo {
color_buffer,
vertex_count: self.positions.len() as u32,
strip: self.strip,
joints: self.joints,
})
}
}
Expand Down Expand Up @@ -446,15 +476,11 @@ impl<P: PhaseItem> RenderCommand<P> 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 {
Expand All @@ -470,6 +496,58 @@ impl<P: PhaseItem> RenderCommand<P> for DrawLineGizmo {
}
}

struct DrawLineJointGizmo;
impl<P: PhaseItem> RenderCommand<P> for DrawLineJointGizmo {
type Param = SRes<RenderAssets<LineGizmo>>;
type ViewQuery = ();
type ItemQuery = Read<Handle<LineGizmo>>;

#[inline]
fn render<'w>(
_item: &P,
_view: ROQueryItem<'w, Self::ViewQuery>,
handle: Option<ROQueryItem<'w, Self::ItemQuery>>,
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<VertexBufferLayout> {
use VertexFormat::*;
let mut position_layout = VertexBufferLayout {
Expand Down Expand Up @@ -497,11 +575,13 @@ fn line_gizmo_vertex_buffer_layouts(strip: bool) -> Vec<VertexBufferLayout> {
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
},
]
Expand All @@ -523,3 +603,41 @@ fn line_gizmo_vertex_buffer_layouts(strip: bool) -> Vec<VertexBufferLayout> {
vec![position_layout, color_layout]
}
}

fn line_joint_gizmo_vertex_buffer_layouts() -> Vec<VertexBufferLayout> {
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(),
]
}
Loading

0 comments on commit 27215b7

Please sign in to comment.