Add dashed lines (#16884)

# Objective

- Fixes #16873

## Solution

- Added  `GizmoLineStyle::Dashed {gap_scale, line_scale}`
- The `gap_scale` and `line_scale` describe the lengths of the gaps and
visible line-segments in terms of line-widths. For example, if
`gap_scale == 1.0` and `line_scale == 3.0` the gaps are square and the
the visible segments are three line-widths long.
- The new `GizmoLineStyle` can be used both in 3D and 2D and with both
perspective and orthographic cameras.
- Updated the `2d_gizmos` and `3d_gizmos` examples to include the new
line-style.
- Display a warning, when using negative `gap_scale` or `line_scale`.
- Notably, `Hash` and `Eq` are manually implemented for `GizmoLineStyle`
since both are not implemented for `f32` which prevents deriving these
traits for `GizmoLineStyle`.

## Testing

- The results can be verified visually

---

## Showcase
The following images depict dashed lines with `gap_scale == 3.0` and
`line_scale == 5.0` in perspective 3D and orthographic 2D.


![linestyle-dashed-2d](https://github.com/user-attachments/assets/3541cc55-63c2-4600-882b-3da61f9472bd)

![linestyle-dashed-3d](https://github.com/user-attachments/assets/6b106352-8e74-44a0-b481-46510d4f9148)

---------

Co-authored-by: Hennadii Chernyshchyk <genaloner@gmail.com>
This commit is contained in:
Lynn 2024-12-18 21:43:58 +01:00 committed by GitHub
parent d8796ae8b6
commit c425fc7f32
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 115 additions and 5 deletions

View File

@ -14,6 +14,7 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect, TypePath};
use bevy_utils::TypeIdMap; use bevy_utils::TypeIdMap;
use core::{ use core::{
any::TypeId, any::TypeId,
hash::Hash,
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
panic, panic,
}; };
@ -36,7 +37,7 @@ pub enum GizmoLineJoint {
} }
/// An enum used to configure the style of gizmo lines, similar to CSS line-style /// An enum used to configure the style of gizmo lines, similar to CSS line-style
#[derive(Copy, Clone, Debug, Default, Hash, PartialEq, Eq, Reflect)] #[derive(Copy, Clone, Debug, Default, PartialEq, Reflect)]
#[non_exhaustive] #[non_exhaustive]
pub enum GizmoLineStyle { pub enum GizmoLineStyle {
/// A solid line without any decorators /// A solid line without any decorators
@ -44,6 +45,34 @@ pub enum GizmoLineStyle {
Solid, Solid,
/// A dotted line /// A dotted line
Dotted, Dotted,
/// A dashed line with configurable gap and line sizes
Dashed {
/// The length of the gap in `line_width`s
gap_scale: f32,
/// The length of the visible line in `line_width`s
line_scale: f32,
},
}
impl Eq for GizmoLineStyle {}
impl Hash for GizmoLineStyle {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
match self {
Self::Solid => {
0u64.hash(state);
}
Self::Dotted => 1u64.hash(state),
Self::Dashed {
gap_scale,
line_scale,
} => {
2u64.hash(state);
gap_scale.to_bits().hash(state);
line_scale.to_bits().hash(state);
}
}
}
} }
/// A trait used to create gizmo configs groups. /// A trait used to create gizmo configs groups.

View File

@ -81,7 +81,7 @@ use bevy_ecs::{
schedule::{IntoSystemConfigs, SystemSet}, schedule::{IntoSystemConfigs, SystemSet},
system::{Res, ResMut, Resource}, system::{Res, ResMut, Resource},
}; };
use bevy_math::Vec4; use bevy_math::{Vec3, Vec4};
use bevy_reflect::TypePath; use bevy_reflect::TypePath;
#[cfg(all( #[cfg(all(
@ -419,6 +419,9 @@ fn extract_gizmo_data(
handles: Extract<Res<GizmoHandles>>, handles: Extract<Res<GizmoHandles>>,
config: Extract<Res<GizmoConfigStore>>, config: Extract<Res<GizmoConfigStore>>,
) { ) {
use bevy_utils::warn_once;
use config::GizmoLineStyle;
for (group_type_id, handle) in &handles.handles { for (group_type_id, handle) in &handles.handles {
let Some((config, _)) = config.get_config_dyn(group_type_id) else { let Some((config, _)) = config.get_config_dyn(group_type_id) else {
continue; continue;
@ -438,12 +441,30 @@ fn extract_gizmo_data(
0 0
}; };
let (gap_scale, line_scale) = if let GizmoLineStyle::Dashed {
gap_scale,
line_scale,
} = config.line.style
{
if gap_scale <= 0.0 {
warn_once!("When using gizmos with the line style `GizmoLineStyle::Dashed{{..}}` the gap scale should be greater than zero.");
}
if line_scale <= 0.0 {
warn_once!("When using gizmos with the line style `GizmoLineStyle::Dashed{{..}}` the line scale should be greater than zero.");
}
(gap_scale, line_scale)
} else {
(1.0, 1.0)
};
commands.spawn(( commands.spawn((
LineGizmoUniform { LineGizmoUniform {
world_from_local: Affine3::from(&Affine3A::IDENTITY).to_transpose(), world_from_local: Affine3::from(&Affine3A::IDENTITY).to_transpose(),
line_width: config.line.width, line_width: config.line.width,
depth_bias: config.depth_bias, depth_bias: config.depth_bias,
joints_resolution, joints_resolution,
gap_scale,
line_scale,
#[cfg(feature = "webgl")] #[cfg(feature = "webgl")]
_padding: Default::default(), _padding: Default::default(),
}, },
@ -471,9 +492,12 @@ struct LineGizmoUniform {
depth_bias: f32, depth_bias: f32,
// Only used by gizmo line t if the current configs `line_joints` is set to `GizmoLineJoint::Round(_)` // Only used by gizmo line t if the current configs `line_joints` is set to `GizmoLineJoint::Round(_)`
joints_resolution: u32, joints_resolution: u32,
// Only used if the current configs `line_style` is set to `GizmoLineStyle::Dashed{_}`
gap_scale: f32,
line_scale: f32,
/// WebGL2 structs must be 16 byte aligned. /// WebGL2 structs must be 16 byte aligned.
#[cfg(feature = "webgl")] #[cfg(feature = "webgl")]
_padding: f32, _padding: Vec3,
} }
/// A collection of gizmos. /// A collection of gizmos.

View File

@ -8,9 +8,12 @@ struct LineGizmoUniform {
world_from_local: mat3x4<f32>, world_from_local: mat3x4<f32>,
line_width: f32, line_width: f32,
depth_bias: f32, depth_bias: f32,
_joints_resolution: u32,
gap_scale: f32,
line_scale: f32,
#ifdef SIXTEEN_BYTE_ALIGNMENT #ifdef SIXTEEN_BYTE_ALIGNMENT
// WebGL2 structs must be 16 byte aligned. // WebGL2 structs must be 16 byte aligned.
_padding: vec2<f32>, _padding: vec3<f32>,
#endif #endif
} }
@ -28,6 +31,7 @@ struct VertexOutput {
@builtin(position) clip_position: vec4<f32>, @builtin(position) clip_position: vec4<f32>,
@location(0) color: vec4<f32>, @location(0) color: vec4<f32>,
@location(1) uv: f32, @location(1) uv: f32,
@location(2) line_fraction: f32,
}; };
const EPSILON: f32 = 4.88e-04; const EPSILON: f32 = 4.88e-04;
@ -126,7 +130,9 @@ fn vertex(vertex: VertexInput) -> VertexOutput {
var clip_position = vec4(clip.w * ((2. * screen) / resolution - 1.), depth, clip.w); var clip_position = vec4(clip.w * ((2. * screen) / resolution - 1.), depth, clip.w);
return VertexOutput(clip_position, color, uv); let line_fraction = 2.0 * line_gizmo.line_scale / (line_gizmo.gap_scale + line_gizmo.line_scale);
uv /= (line_gizmo.gap_scale + line_gizmo.line_scale) / 2.0;
return VertexOutput(clip_position, color, uv, line_fraction);
} }
fn clip_near_plane(a: vec4<f32>, b: vec4<f32>) -> vec4<f32> { fn clip_near_plane(a: vec4<f32>, b: vec4<f32>) -> vec4<f32> {
@ -147,6 +153,7 @@ struct FragmentInput {
@builtin(position) position: vec4<f32>, @builtin(position) position: vec4<f32>,
@location(0) color: vec4<f32>, @location(0) color: vec4<f32>,
@location(1) uv: f32, @location(1) uv: f32,
@location(2) line_fraction: f32,
}; };
struct FragmentOutput { struct FragmentOutput {
@ -168,3 +175,15 @@ fn fragment_dotted(in: FragmentInput) -> FragmentOutput {
return FragmentOutput(vec4(in.color.xyz, in.color.w * alpha)); return FragmentOutput(vec4(in.color.xyz, in.color.w * alpha));
} }
@fragment
fn fragment_dashed(in: FragmentInput) -> FragmentOutput {
#ifdef PERSPECTIVE
let uv = in.uv;
#else
let uv = in.uv * in.position.w;
#endif
let alpha = 1.0 - floor(min((uv % 2.0) / in.line_fraction, 1.0));
return FragmentOutput(vec4(in.color.xyz, in.color.w * alpha));
}

View File

@ -118,6 +118,7 @@ impl SpecializedRenderPipeline for LineGizmoPipeline {
let fragment_entry_point = match key.line_style { let fragment_entry_point = match key.line_style {
GizmoLineStyle::Solid => "fragment_solid", GizmoLineStyle::Solid => "fragment_solid",
GizmoLineStyle::Dotted => "fragment_dotted", GizmoLineStyle::Dotted => "fragment_dotted",
GizmoLineStyle::Dashed { .. } => "fragment_dashed",
}; };
RenderPipelineDescriptor { RenderPipelineDescriptor {

View File

@ -124,6 +124,7 @@ impl SpecializedRenderPipeline for LineGizmoPipeline {
let fragment_entry_point = match key.line_style { let fragment_entry_point = match key.line_style {
GizmoLineStyle::Solid => "fragment_solid", GizmoLineStyle::Solid => "fragment_solid",
GizmoLineStyle::Dotted => "fragment_dotted", GizmoLineStyle::Dotted => "fragment_dotted",
GizmoLineStyle::Dashed { .. } => "fragment_dashed",
}; };
RenderPipelineDescriptor { RenderPipelineDescriptor {

View File

@ -106,6 +106,9 @@ pub(crate) fn extract_linegizmos(
) { ) {
use bevy_math::Affine3; use bevy_math::Affine3;
use bevy_render::sync_world::{MainEntity, TemporaryRenderEntity}; use bevy_render::sync_world::{MainEntity, TemporaryRenderEntity};
use bevy_utils::warn_once;
use crate::config::GizmoLineStyle;
let mut values = Vec::with_capacity(*previous_len); let mut values = Vec::with_capacity(*previous_len);
for (entity, gizmo, transform, render_layers) in &query { for (entity, gizmo, transform, render_layers) in &query {
@ -115,6 +118,21 @@ pub(crate) fn extract_linegizmos(
} else { } else {
0 0
}; };
let (gap_scale, line_scale) = if let GizmoLineStyle::Dashed {
gap_scale,
line_scale,
} = gizmo.line_config.style
{
if gap_scale <= 0.0 {
warn_once!("when using gizmos with the line style `GizmoLineStyle::Dashed{{..}}` the gap scale should be greater than zero");
}
if line_scale <= 0.0 {
warn_once!("when using gizmos with the line style `GizmoLineStyle::Dashed{{..}}` the line scale should be greater than zero");
}
(gap_scale, line_scale)
} else {
(1.0, 1.0)
};
values.push(( values.push((
LineGizmoUniform { LineGizmoUniform {
@ -122,6 +140,8 @@ pub(crate) fn extract_linegizmos(
line_width: gizmo.line_config.width, line_width: gizmo.line_config.width,
depth_bias: gizmo.depth_bias, depth_bias: gizmo.depth_bias,
joints_resolution, joints_resolution,
gap_scale,
line_scale,
#[cfg(feature = "webgl")] #[cfg(feature = "webgl")]
_padding: Default::default(), _padding: Default::default(),
}, },

View File

@ -142,6 +142,10 @@ fn update_config(
if keyboard.just_pressed(KeyCode::KeyU) { if keyboard.just_pressed(KeyCode::KeyU) {
config.line.style = match config.line.style { config.line.style = match config.line.style {
GizmoLineStyle::Solid => GizmoLineStyle::Dotted, GizmoLineStyle::Solid => GizmoLineStyle::Dotted,
GizmoLineStyle::Dotted => GizmoLineStyle::Dashed {
gap_scale: 3.0,
line_scale: 5.0,
},
_ => GizmoLineStyle::Solid, _ => GizmoLineStyle::Solid,
}; };
} }
@ -169,6 +173,10 @@ fn update_config(
if keyboard.just_pressed(KeyCode::KeyI) { if keyboard.just_pressed(KeyCode::KeyI) {
my_config.line.style = match my_config.line.style { my_config.line.style = match my_config.line.style {
GizmoLineStyle::Solid => GizmoLineStyle::Dotted, GizmoLineStyle::Solid => GizmoLineStyle::Dotted,
GizmoLineStyle::Dotted => GizmoLineStyle::Dashed {
gap_scale: 3.0,
line_scale: 5.0,
},
_ => GizmoLineStyle::Solid, _ => GizmoLineStyle::Solid,
}; };
} }

View File

@ -237,6 +237,10 @@ fn update_config(
if keyboard.just_pressed(KeyCode::KeyU) { if keyboard.just_pressed(KeyCode::KeyU) {
config.line.style = match config.line.style { config.line.style = match config.line.style {
GizmoLineStyle::Solid => GizmoLineStyle::Dotted, GizmoLineStyle::Solid => GizmoLineStyle::Dotted,
GizmoLineStyle::Dotted => GizmoLineStyle::Dashed {
gap_scale: 3.0,
line_scale: 5.0,
},
_ => GizmoLineStyle::Solid, _ => GizmoLineStyle::Solid,
}; };
} }
@ -264,6 +268,10 @@ fn update_config(
if keyboard.just_pressed(KeyCode::KeyI) { if keyboard.just_pressed(KeyCode::KeyI) {
my_config.line.style = match my_config.line.style { my_config.line.style = match my_config.line.style {
GizmoLineStyle::Solid => GizmoLineStyle::Dotted, GizmoLineStyle::Solid => GizmoLineStyle::Dotted,
GizmoLineStyle::Dotted => GizmoLineStyle::Dashed {
gap_scale: 3.0,
line_scale: 5.0,
},
_ => GizmoLineStyle::Solid, _ => GizmoLineStyle::Solid,
}; };
} }